51单片机——中断系统

中断系统

一.中断系统基础知识:

1.中断系统结构示意图:

在这里插入图片描述

2.中断源(5个):

外部中断0(INT0)、定时器中断0(T0)、外部中断1(INT1)、定时器中断1(T1)、串行口中断(RXD、TXD)。

3.中断源的优先级:

默认情况下按INT0—>T0—>INT1—>T1—>(RXD、TXD) 的顺序划分优先级,INT0优先级最高,(RXD、TXD)优先级最低。如若改变优先级,需要对IP优先级寄存器单独配置,下面会介绍。

4.中断处理过程示意图:

在这里插入图片描述
**过程简述:**在执行主程序(main函数)的时候,收到中断响应,立即去执行中断的程序,执行完返回来继续执行主函数(main函数)里的程序的过程。 //结合左图更好理解

5.各个中断源触发中断的条件:

INT0: P3.2引脚输入低电平 / 触发下降沿,标志位IE0=1(编程中不用配置),申请中断;
**T0:**当产生计数溢出时触发标志位TF0=1(编程中不用配置),申请中断;
INT1: P3.3引脚输入低电平 / 触发下降沿,标志位IE1=1(编程中不用配置),申请中断;
**T1:**当产生计数溢出时触发标志位TF1=1(编程中不用配置),申请中断;
**串行口:**当一个串行帧发送或接受完毕时,RI或TI由0变到1(编程中要重置),申请中断;

6.中断系统的相关寄存器:

6.1 必有的寄存器:

顾名思义就是不管外部中断、定时器还是串行口 大家都有的:

①IE(中断允许寄存器):

在这里插入图片描述
**EA:**相当于总开关,EA=1,所有中断源的中断将被打开,所以编程中要使用任何一个中断的话,EA是必须要 打开。
其他的: ES、ET1、EX1、ET0、EX0 分别对应串行口、定时器1、外部中断1、定时器0、外部中断0。相当于小开关,单独控制一个中断源。所以编程中要使用任一中断的话,都必有EA+( ES、ET1、EX1、ET0、EX0 )对应的一个打开。

②IP(中断优先级寄存器):

在这里插入图片描述
PS、PT1、PX1、PT0、PX0 分别对应串行口、定时器1、外部中断1、定时器0、外部中断0。如果只用到一个 中断源,这个IP可以不配置,如果用到不止一个中断源,而又没配置这个IP,那么默认为自然级别顺序(前面 讲过,从INT0到串行口由高到低)。如果配置了优先级该怎么进行前面也讲了(第4点)。

6.2 中断源间共有:

意思就是外部中断、定时器、串行口之间两两共有的

TCON寄存器:(外部中断和定时器共有)

在这里插入图片描述
等讲到每个中断源再具体展开讲。

6.3 独有的寄存器:

顾名思义也就是在中断系统属于外部中断、定时器、串行口单独拥有的。

①TMOD(定时器特有):

在这里插入图片描述
只能在用到定时器中断时使用,外部中断,串行口用不了,具体的放在每个中断源里讲。

②SCON和PCON(串行口特有):

SCON
PCON
只能用于串行口中断,其他中断用不了,等讲到串行口再展开讲。

7.中断函数:

中断函数是启用某个中断必不可少的部分,它的格式为 “void+中断函数名(void)+interrupt+中断号”,我这里用代码敲出来,方便大家复制,这里举例5个中断源就写5个中断函数。

void      Int0_Routine(void)           interrupt 0;   //外部中断0的中断函数,最后的“0”很重要
void      Timer0_Rountine(void)        interrupt 1;   //定时器0的中断函数,最后的“1”很重要
void      Int1_Rountine(void)          interrupt 2;   //外部中断1的中断函数,最后的“2”很重要
void      Timer1_Routine(void)         interrupt 3;   //定时器1的中断函数,最后的“3”很重要
void      UART_Routine(void)           interrupt 4;   //串行口的中断函数,最后的“4”很重要

其中 Int0_Routine(void) 所在列只是函数名,你想取什么名字,只要编译器允许的情况下随便你取。主要关注(0-4)即中断号,切记要一一对应,避免张冠李戴。

二、外部中断(INT0和INT1)

这里先补充个知识点: 可位寻址和不可位寻址:可位寻址就是一个寄存器里的任何一个标志位可以单独拎出来赋值(例:TCON里的IT0,可以初始化的时候直接使IT0=1),它也可以整个寄存器一起赋值;而不可位寻址就是一个寄存器必须整个赋值,不能单独拎出一个标志位来赋值(例:TMOD寄存器,初始化的时候只能TMOD=0x~~)。

1.相关寄存器的分析:

外部中断、定时器、串行口都共有的就不展开讲了,前面也已介绍完。
在这里插入图片描述
在这里插入图片描述
我们可以先看中断系统的示意图,要想使用外部中断 INT0 ,从左往右看我们要配置3个寄存器:TCON、IE和IP。TCON是外部中断和定时器共有的,如下图所示:
在这里插入图片描述
左边属于定时器部分,右边才是外部中断部分,这里只看右边部分,IT0表示外部中断0,IT1表示外部中断1。(属于可位寻址)。

红色圈出来的部分IT0和IT1是编程时需要赋值的,虽然中断系统示意图上标了IE0和IE1,但它们只是中断申请标志位,并不用实际赋值,它们由IT0和IT1决定,也就是给IT0和IT1赋值后他们就为1,然后提出中断。外部中断0中IT0=0表示低电平触发,随后IE0就等于1,即申请中断,IT0=1表示下降沿触发,同时IE0等于1申请中断。所谓下降沿触发,看如下电路图可以看到外部中断0接了P3.2引脚,外部中断1接了P3.3引脚。同时P3.2和P3.3引脚也接了独立按键,所以简单来说选择下降沿触发中断就是通过独立按键来启动中断。(比如用的是外部中断0,那么执行主函数的时候按一下P3.2,就会跳出主函数到外部中断0那部分去执行程序,等外部中断的程序执行完了再回到主程序接着执行剩下的)。外部中断1同理,只是按键不同。
在这里插入图片描述
在这里插入图片描述
接下来看IE(中断允许寄存器)寄存器,首先总中断EA是必须要开的,所以EA=1,如果要使用外部中断0(INT0)就让EX0=1,如果要使用外部中断1(INT1)就让EX1=1。

然后是这个IP(优先级寄存器),如果只是用到某个中断源,可以不配置。若是两个以上,不配置就是默认自然优先级,建议还是要配置的。配置的话就是PX0或PX1=0是低优先级,PX0或PX1=1是高优先级。

最后就是中断函数了,要使用外部中断0(INT0),那么中断函数就是 void Int0_Routine(void) interrupt 0;

如果要使用外部中断1,那么中断函数是void Int1_Routine(void) interrupt 2。注意:使用多少个中断就有多少个中断函数。

2.外部中断代码框架:

2.1 伪代码:

{
1.选择低电平触发还是下降沿触发;
2.确定优先级;
3.开启总中断和对应的中断允许开关;
}

2.2.初始化代码:(这里以外部中断0为例)

#include<reg52.h>
int main()
{
    IT0=1;              //选择下降沿触发。
    PX0=0//低优先级,高优先级为1
    EX0=1//开启外部中断0允许
    EA=1;               //开启总中断。
    while(1)
    {
        
    }
}

void Int0_Routine(void) interrupt 0      //因为是外部中断0,所以对应中断号为0
{
    
}

3.外部中断案例分享:

题目:让8位数码管的前3位显示数字,触发外部中断0时,让这个数字每按一下按键,数字加1。触发外部中断1时,每按一下按键,数字减1.(用外部中断完成)
最好使用模块化编程。

#include <REGX52.H>
#include<INTRINS.H>
sbit C = P2^4;
sbit Ba = P2^3;
sbit A = P2^2;
void int0();
void wei_num(char number);
void Delay(int xms);
int shu=0; 
char num_shu[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F,0x77,0x7C,0x39,0x5E,0x79,0x71};
void main()
{
        int0();
        
        while(1)
        {
                wei_num(1);
                P0=num_shu[shu/100];
                Delay(1);
                P0=0x00;
                wei_num(2);
                P0=num_shu[(shu/10)%10];
                Delay(1);
                P0=0x00;
                wei_num(3);
                P0=num_shu[shu%10];
                Delay(1);
                P0=0x00;
        }
}
void int0()                      //初始化外部中断0
{
        EA=1;
        EX0=1;
        IT0=1;
        EX1=1;
        IT1=1;
}
void wei_num(char number)           //这里我的是普中51单片机,数码管接了个74HC138芯片,得这样去实现
{
        switch(number)
        {
                case 1:C=1,Ba=1,A=1;break;
                case 2:C=1,Ba=1,A=0;break;
                case 3:C=1,Ba=0,A=1;break;
                case 4:C=1,Ba=0,A=0;break;
                case 5:C=0,Ba=1,A=1;break;
                case 6:C=0,Ba=1,A=0;break;
                case 7:C=0,Ba=0,A=1;break;
                case 8:C=0,Ba=0,A=0;break;
        }
}

void Delay(int xms)                   延时函数。            
{
        unsigned char i, j;
        while(xms)
        {
                _nop_();
                i = 2;
                j = 199;
                do
                {
                        while (--j);
                } while (--i);
                xms--;
        }
        
}

void int_0() interrupt 0
{
        shu++;             //每按一次按键P3_2时shu++
        if(shu==255)
        {
                shu=0;
        }
}

void int_1() interrupt 2
{
        shu--;            //每按一次按键P3_3时shu--
        if(shu==0)
        {
                shu=255;
        }
}

4.效果展示:

外部中断案例

三.定时器/计数器

1.相关寄存器的分析:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
同样,通过中断系统结构示意图,定时器也是要配置TCON、IE、IP这三个寄存器。除此之外,定时器还需要配置多一个寄存器——TMOD。

1.1 TCON寄存器(可位寻址):

上面说过他是与外部中断一起的。如下图:

在这里插入图片描述
同样,左边才是定时器,”0“是定时器0,”1“是定时器1,圈红的TR0表示定时器0的,TR1表示定时器1的,他们是定时器开启允许位,在编程中需要配置,TR0=1表示开启定时器0,TR1=1表示开启定时器1。TF0和TF1是溢出标志位,溢出时为1,申请中断,编程中初始化中不需要配置。

1.2 TMOD定时器模式寄存器:(不可位寻址)

TMOD定时器模式寄存器(图一):

在这里插入图片描述
定时器/计数器工作方式示意图(图二):

在这里插入图片描述
如图一,高四位为定时器1,低四位为定时器0,注意每次只能使用一个定时器,这里用定时器1举例。
GATE:门控制位,这里为”0“就好,如果为”1“,由图二可知,还要结合外部中断才能触发。
C/T:选择定时器还是计数器,若C/T位为”0“,表示定时器模式,若C/T位为”1“,表示计数器模式。定时器模式的脉冲来自单片机自带的晶振(1个晶振周期等于12个机器周期),而计数器模式的脉冲来自P3.4(定时器0)和P3.5(定时器1)引脚。这里一般选择C/T=0的定时器模式。
M1和M0:定时器工作方式,注意不要跟工作模式混淆了。这里有4种工作方式(记住方式1和方式2就行)

在这里插入图片描述
上一个工作模式是指在定时器和计数器之间选择,而这个工作方式是作用于图二的TH1和TL1的(因为是定时器1),这里工作方式0和3不常用,不做举例。

TH1和TL1标志位:

在这里插入图片描述
如图所示,TH1和TL1是用来存放脉冲的,TH1是高8位,TL1是低八位,先说上面的工作方式1和方式2,方式1是16位的,简单把他理解为TH1和TL1都要用上,到底怎么存放先不管,待会讲;方式2是8位的,简单的理解位只用到其中一个来储存,这里要注意,”可自动重装入“的意思是溢出后自己回到初始值,这就体现了另一个存放位的作用了,因为他存放着初始常量,等那个溢出的时候就可以把初始常量给他。相反,方式1中两个存放位都用上了,没位置来存放初始常量(当然他存放量比方式2大得多),所以每次要在他溢出后赋予它初始常量,赋初始常量这一步在中断函数里实现。

所以要根据定时的大小选择工作方式1还是工作方式2,总的下来TMOD也是可以配置了。例如:TMOD=0x01(不可位寻址);二进制是0000 0001;用的是定时器0,前四位直接为0,GATE=0;C/T=0,选择的是定时器;M1=0,M0=1,选择模式1(16位);

1.3 TH1和TL1的配置:

定时器0为TH0和TL0

1.3.1 工作原理:

在这里插入图片描述
工作原理就是来一个脉冲就加1,直到加到满了溢出,那到底啥时候溢出呢?其实就是看存储容量。工作方式1和方式2不同,方式1为16位,说明TH1和TL1都用上了,那么他的存储量就是2的16次方,也就是65536。方式2是8位,那么他的储存量就是2的8次方,即256。这里再次强调,方式1需要在中断函数里重新赋予初始常量,而方式2不需要。

1.3.2 计算TH1和TL1的初值:

接下来步入正题,上面的工作方式看不懂也无所谓,那到底为啥要给TH1和TL1赋予初值呢,首先定时器的工作原理是溢出就触发中断,那么溢出就要靠TH1和TL1的容量来决定。

如果理解不了,举个通俗易懂的例子,比如,装一桶水要5分钟,水装满溢出后你就洗澡,好,那么那刚开始拿的是一个空桶,装满要5分钟,可是你想2分钟就有澡洗,那你是不是要提前装入3分钟的水量,然后剩下2分钟让它自然装水,2分钟后水溢出,这是你就相当于定时了2分钟。这个空桶就相当于TH1+TL1的容量65536,水满溢出就相当于TH1和TL1溢出提出中断,那么这个3分钟的水量就是给TH1和TL1赋的初值,这个定的2分钟就是TH1和TL1要定的时间。大概就是这个意思。

还是那个问题65536这么大怎么算?其实65536就是65536个机器周期,上面说过
———1个晶振周期=12个机器周期。
———1个机器周期=1微秒
———1毫秒=1000微秒

所以65536也才65.536毫秒,很奇怪是不是?那我要定时1s、2s、甚至5s怎么办?别急,我们慢慢来。

所以说我们定时肯定不能超过65.36毫秒的,那我们可以凑个整,定个(50、20、10、甚至1)毫秒,你们可能会说1毫秒也太小了吧?待会你就知道了。举个例子,定时个50毫秒。

那么50毫秒=50000微秒。那个我要往TH1和TL1里放多少合适呢?因为超过65536就溢出,所以(?+50000)=65536,转换一下就是65536-50000=15536,所以初值要放15536。

那要怎么去表示呢?有两种方法表示:

方法一:

TH1=65536-50000/256//因为TH18位,所以要除于256来取15536的前8位放在TH1里。
TL1=65536-50000%256//同样,对256取余就是为了取15536的剩下低8位存在TL1里。

方式二:

TH1=0x3C//转换成了16进制,就是把15536/256和15536%256的结果转回对应的16进制。
TL1=0xB0//比较建议用这种方法。

1.4 IE(中断允许寄存器):

EA必须打开,所以EA=1,然后对应的要开启定时器0,ET0=1,要开启定时器1,ET1=1;

1.5 IP(中断优先级寄存器):

没有配置情况下属于自然优先级(定时器0排第2,定时器1排第4),仅用到一个中断可以不配置,若是想定时器0的优先级高于其他,那么PT0=1;如果想要定时器1优先级高于其他,PT1=1。

到这里所有要开启定时器中断需要配置的寄存器都已配置完,那么接下来看看怎么进行定时器中断初始化。

2.定时器中断代码框架:

2.1 伪代码:

{
1.开启定时器允许位(TR0/TR1);
2.选择定时器工作模式和工作方式(TMOD);
3.赋予定时器初值(TH1和TL1);
4.开启总中断和各个中断源的中断;
5.考虑优先级;
}

2.2 初始化代码:(以定时器1为例)

#include<reg52.h>
int main()
{
    TR1=1;               //开启定时器允许位;
    TMOD=0x10;           //0001 0000;定时器模式,方式1
    TH1=0x3C;            //以定50毫秒为例
    TL1=0xB0;
    EA=1;
    ET1=0;
    PT1=0while(1)
    {
        
    }
}

void Timer1_Rountine(void) interrupt 3
{
    int count;             //定义一个变量,用来统计多少个50毫秒。
    TH1=0x3C;              //这里就是上面说的重新赋予初值常量,如果不重新赋予,那么50毫秒过后就结束了。
    TL1=0xB0;
    count++;               //count++,让它每次以50毫秒递增。
    if(count>=20)          //限制count加到20就停止,50ms*20次刚好1s。
    {
        count=0;           //让count=0,就是重新计时,也就是1s到了后,重新以50ms递增,直到又到下一个1s,一直循环。
        定时器中断语句块;   //这里意思是跳出主函数来到这里要实现的程序。
    }
}

所以说,回到之前那个问题,通过给TH1和TL1赋予初值,形成定时50ms,20ms,10ms,1ms的时候会不会时间太短的问题,通过观察上面的中断函数可以发现,50ms只是一个作为一个子时间,看作一个固定的常量,想要1s就是20个50ms,想要2s就是40个50ms。通过在中断函数里定义一个常量,让它递增,通过控制递增的次数来得到最终的时间。

3.定时器案例分享:

题目:用定时器和8位数码管制作一个时钟。(时—分—秒,比如12—30—40,因为数码管实现不了”:”,所以用12—30—40代替12:30:40,没有影响,只是美观问题。

#include <REGX52.H>
#include <INTRINS.H>
void Nixie(unsigned char location);      //声名数码管位选函数。
void Delay(int xms);                     //声明延时函数。
void Time1Init();                        //声明定时器1初始化函数。
sbit Aa=P2^2;                            //定义按键
sbit Bb=P2^3;
sbit Cc=P2^4;

char Nixie_num[]={0x3F,0x06,0x5B,0x4F,0x66,0x6D,0x7D,0x07,0x7F,0x6F};//创建数码管数字数组。

int sec,min,hour;                  //定义三个变量,分别是秒、分,时。

void main()
{
        
        Time1Init();               //调用定时器1初始化函数
        while(1)
        {
                Nixie(3);          //调用数码管位选函数。
                P0_6=1;            //打印出“——”符号        
                Delay(1);       //这里的延时是对数码管的快速扫描,达到同时显示多位数码管的效果,有种我够快你就看不清楚的感觉
                P0=0x00;        //这里和上一条差不多,快速关一次,去显示下一个数码管,速度很快,肉眼看不出来  
                Nixie(6);
                P0_6=1;
                Delay(1);
                P0=0x00;
                
                Nixie(1);
                P0=Nixie_num[hour/10];  //这个的意思是取十位
                Delay(1);
                P0=0x00;
                Nixie(2);
                P0=Nixie_num[hour%10];  //取个位
                Delay(1);
                P0=0x00;
                
                Nixie(4);
                P0=Nixie_num[min/10];
                Delay(1);
                P0=0x00;
                Nixie(5);
                P0=Nixie_num[min%10];
                Delay(1);
                P0=0x00;
                
                Nixie(7);
                P0=Nixie_num[sec/10];
                Delay(1);
                P0=0x00;
                Nixie(8);
                P0=Nixie_num[sec%10];
                Delay(1);
                P0=0x00;
        }
}
void Time1Init()                  //定时器1的初始化函数
{
        TMOD=0x10;                //二进制:0001 0000,定时器1且方式1
        TH1=0x3C;                 //定时50ms,到时间溢出,TF1=1;触发中断(高四位)
        TL1=0xB0;                 //(低四位)
        TR1=1;                    //开启定时器1。
        ET1=1;                    //开启定时器1的中断允许位。
        EA=1;                     //开启总中断。
}
void Nixie(unsigned char location)                 //38译码器对数码管的位置进行选择
{
        switch(location)
        {
                case 1:Cc=1;Bb=1;Aa=1;break;
                case 2:Cc=1;Bb=1;Aa=0;break;
                case 3:Cc=1;Bb=0;Aa=1;break;
                case 4:Cc=1;Bb=0;Aa=0;break;
                case 5:Cc=0;Bb=1;Aa=1;break;
                case 6:Cc=0;Bb=1;Aa=0;break;
                case 7:Cc=0;Bb=0;Aa=1;break;
                case 8:Cc=0;Bb=0;Aa=0;break;
        }
}

void Delay(int xms)                     //延时函数
{
        unsigned char i, j;
        while(xms)
        {
                _nop_();
                i = 2;
                j = 199;
                do
                {
                        while (--j);
                } while (--i);
                xms--;
        }
}

void Time1() interrupt 3               //定时器1的中断函数,注意中断号是3
{
        int count;                    //定义一个变量,让它递增。
        TH1=0x3C;                     //定时器1的方式1是需要重新赋予初始常量的
        TL1=0xB0;
        count++;                      //统计有多少个50ms
        if(count>=20)                 //控制定时1s
        {                                             
                count=0;              //到了1s就清零,让它重新从0开始加,等加到下一个1s又清零,不断循环
                sec++;                   //这里开始为中断语句块。
                if(sec==60)
                {
                        sec=0;
                        min++;
                        if(min==60)
                        {
                                min=0;
                                hour++;
                                if(hour==24)
                                {
                                        hour=0;
                                }
                        }
                }
        }
}

4.效果展示:

定时器案例——简易时钟

四、串行口(通信)

1.理论知识部分:

为什么叫通信?简单把他理解为是两个设备之间的交流,可以是单片机给电脑发送数据、电脑发送数据控制单片机、或者单片机和其他设备之间通信。串行口就是他们进行通信的方式,那不可能随随便便就通信成功吧,(就像两个人交流,必须语言要通吧,如果不通是不是得要翻译),那就涉及到了UART(就是串行口)的协议,两个设备之间要商量好才能进行通信。

这里用单片机和电脑举例,大家都知道电脑用的是USB,而单片机用的是UART,很明显通信协议不一样。UART用的是TTL电平通信,内含TXD和RXD两个引脚,也就是两条通信线(如下图)。

在这里插入图片描述
TXD是发送,RXD是接收。不难发现,两个设备之间TXD和RXD是交叉连接的,这是因为一个设备发送的时,另一台设备是接收的状态。刚才说到电脑是USB通信,UART是用TTL电平通信的,那么想要两个设备之间能通信,单片机采取了对电脑的信号进行转换,转换成单片机能收到的TTL电平信号,用到的是USB转TTL模块(了解就行),这样就解决了两个设备通信的正常通信。但通信的数据正不正确还得另说,因为这又涉及到了两个设备之间的传输速率,也就是待会要讲的波特率,电脑波特率很简单,可以设置,难的是单片机波特率的设置,可以这么理解,是电脑根据单片机设置的波特率再设置自己的波特率,因为两者必须一样,不然传输的数据可能乱码或者直接不显示。

讲波特率之前还要了解串行口通信是有很多类的:(51单片机用的是异步全双工通信,这里也只讲涉及到的)

1.按传输方向不同可分为3种:单工半双工全双工

单工:只有一方发送或者接受,也就是只有一根通信线(例如遥控器只发送,收音机只接收)

半双工:双方都能发送和接受,只有一条通信线,也就是不能同时进行,同一时刻只能一方发送或接收。(例如最典型的对讲机,只有一方发送完了,另一方才能接收听到)

全双工:双方都能发送和接受,有两根通信线,双方可以同步进行(例如典型的电话)

2.按同步时钟信号可以分为2种:同步通信、异步通信。(这里只介绍异步通信)

异步通信:记住两个东西,数据帧波特率。(后面再详细讲)

2.串口内部工作原理:

在这里插入图片描述
发送原理:TXD为发送线,要发送的数据存储在SBUF中,此时的SBUF作为发送缓冲分区,它按TH1和TL1设置好的波特率发送数据,通过TI为1,说明一帧数据发送成功,紧接着把TI置零,继续发送下一帧数据。

接收原理:RXD为接收线,电脑的数据通过它传给单片机,SBUF此时作为接收缓存器,它也是通过TH1和TL1设置好的波特率接收数据,RI为1说明接收完成。一般定义一个变量,接收电脑发过来的数据,然后解析这个数据,让单片机执行对应操作。

有如下面的代码(中断函数里的接收和发送代码)

void main()
{
	unsigned char send_Data;     //定义一个变量,为要发送的数据
	while(1)
	{
		SBUF=send_Data;         //将这个要发送的变量赋值给发送缓冲区
		while(TI==0);         //等待TI=0,即发送完成
		TI=0;              //发送完成立即将TI置0

		if(flag==1)       //检测接收到数据的标志位
		{
			//对accept_Data进行解析,然后执行对应操作。
		}
	}
}
void Uart() interrupt 4
{
	unsigned char flag=0;    //定义一个接收到数据的标志位
	unsigned char accept_Data;   //定义一个数据接收变量,SBUF接收缓冲区接收到的数据赋值给它
	if(RI)      //判断是否接收到数据,若接收到数据就往下执行
	{
		RI=0;      //接收到数据后马上将RI置0
		accept_Data=SBUF;     //将SBUF接收缓冲区里的数据赋值给接收变量
		flag=1;   //标志位生效,表示接收到了数据
	}
}

3.相关寄存器分析:

在这里插入图片描述
在这里插入图片描述
到了串行口这里用到的寄存器是SCON、IE、IP以及串行口独有的PCON。

3.1 SCON(串行口控制寄存器):(可位寻址)

在这里插入图片描述
SM0和SM1:和定时器的工作方式类似,SM0和SM1是串行口的工作方式,同样有4种:方式0、方式1、方式2、方式3,如下图所示:(重点掌握方式1和方式2,只讲方式1和方式2)
在这里插入图片描述
首先来了解波特率

波特率是决定串行口发送数据的速率的,常见的波特率有1200、2400、4800、9600、19200,我们以9600为例。9600的波特率就是1s发送9600个码元(二进制的0或1)。方式0和方式2有固定的波特率,没什么好讲的。方式1和方式3波特率可变就是波特率要自己计算好波特率,通过定时器1的溢出来实现。注意这里其实不叫定时器1,也就是说定时器1在串行口中有另一个名字,叫波特率发生器(注意只能是定时器1,定时器0不行,且规定了要用定时器的工作方式2,可自动重装8位的那个)。这里只讲方式1,回到刚才9600的波特率就是1s发送9600个码元(二进制的0或1),那么发送1个码元就是1/9600s,这时得有一个东西提醒串口每隔1/9600s就要发送一个数据位(码元),那么这时候波特率发生器就用上了,设置好初值,让它定时1/9600s,每过1/9600s就溢出一次,因为用的是方式2自动重装,那么就不用考虑重新赋初值的问题,所以总的来说每过1/9600s溢出时就会提醒串口发一个数据位。

根据单片机晶振的不同,所产生的波特率也不一样,会有偏差,我们以11.0592的晶振频率为例,下面是常见的波特率对应的T1方式2的初值(上面的原理不懂也没关系,会用就行)

在这里插入图片描述

还要理解10位(8+2)异步收发 和11位(9+2)异步收发 ,异步收发也就是异步通信,下面对数据帧做详细解释。
在这里插入图片描述
所谓数据帧的位数要根据选择的工作方式决定的,最常用方式1(10位),其次是方式2(11位,多了校验位)。这里用方式1举例(所以忽略校验位)。

发送:刚开始是空闲的,写入的数据存储在发送缓冲区SBUF中,等起始位为0时,开始以设置好的波特率速度传输数据位,并告诉另一个设备准备接收数据,等到了传输到第10位数据位(即停止位)时,停止位为1,此时为1帧,发送中断标志位TI=1,说明1帧数据发送完成,申请中断,数据就发送过去了。

接收:同样,起始位为0说明从另一台设备开始按设置好的波特率传输数据位过来,等到传到第10个数据位(即停止位)时,为1帧数据,接收中断标志位RI=1,说明1帧数据接收完成。申请中断,数据存储到接收缓冲区SBUF中。

这些是工作原理,至于怎么确定起始位和停止位完全不用知道,因为SBUF会处理好这些,我们要做的就是设置好T1初值(波特率发生器),然后写入合理的发送数据和接收数据进SBUF就行了。

SM2:这里用到方式1,所以不需要具体可自行查阅,我这默认只要两台设备通信,SM2为0就行了。

REN:它是接收允许控制位,也就说你想接收数据就必须打开它,即REN=1;

TB8和RB8:跟工作方式1无关,涉及到11位(9+2)异步收发才需要用到,即多了1位校验位,通过判断输入数据位的奇偶来判断传输的数据是否有效,这里只介绍工作方式1,所以这里TB8和RB8为0。

TI和RI:发送中断和接收中断标志位。通过发送或接收1位数据帧来触发他们是否为1,为1时触发中断,即发送完数据或接收完数据。在串口初始化中不需要配置,但无论哪种工作方式都要进行清零,因为只有清零了才会发送或接收下一个数据。首先要对TI和RI判断,例如,当TI或RI等于0时,处于发送着或接收着数据,当TI或RI不等于0了,说明发完或接收完一个数据,这时要紧接着给TI和RI清零,即TI=0或RI=0,好让它继续发送或接收下一个数据。

到这里SCON寄存器所有标志位就讲完了,它是可位寻址,所以SM0=0;SM1=1;END=1;表示启用工作模式1且允许接收,其他默认。也可以是SCON=0x50;表示启用工作模式1且允许接收。

3.2 PCON寄存器:

在这里插入图片描述
这个寄存器叫电源控制寄存器,不是只有一个标志位,其他的被我去掉了,而且这个SMOD标志位跟电源也扯不上关系,因为他是控制波特率速率的标志位,SMOD=0表示保持原来的波特率速率,SMOD=1,且要工作方式1、2、3表示波特率加倍。

3.3 TCOM 定时器模式寄存器:

在这里插入图片描述
因为要用到定时器T1作为波特率发生器,且要选择工作方式2,所以要用到TCOM寄存器,注意他是不可位寻址,所以可以说这个寄存器的值的是固定的。即TMOD=0x20;还有定时器1开启的允许位TR1=1;注意,定时器1只是作为波特率发生器,除了要配置初值让它溢出和允许开启定时器1外,其他啥也不用动,不用打开定时器1的中断,也不用写定时器1的中断函数。

当然也还要赋TH1和TL1的初值,选择工作方式2。

TH1=0xFD//这里用11.0592晶振9600波特率举例,这样即1/9600s溢出一次。
TL1=0xFD//因为这是工作方式2,这个做初值常量给它自动重装。

3.4 IE(中断允许寄存器):

这里需要打开ES串口中断允许位,所以ES=1,还有不可少的EA=1总中断。

3.5 IP(中断优先级寄存器):

没有配置情况下属于自然优先级(串行口排最后),仅用到一个中断可以不配置,若是想串行口的优先级高于其他,那么PS=1。

4.串行口中断代码框架:

3.1 伪代码:

{
1.串行口工作方式SM0和SM1; //可与2一起构成SCON;
2.接收允许位;
3.如若波特率加倍,需要考虑PCON的SMOD;
4.定时器模式TMOD;
5.定时器1开启允许位TR1;
6.TH1和TL1初值;
7.串行口中断允许位ES;
8.总中断EA;
9.是否需要优先级PS;
}

3.2 初始化代码:

#include<reg52.h>
void main()
{
    SCON=0x50;       //二进制:0101 0000,开启工作方式1且允许接收数据。
    TMOD=0x20;       //选择定时器1的工作方式2
    TR1=1;           //开启定时器1
    TH1=0xFD;        //定时1/9600s,即每1/9600s溢出一次。
    TL1=0xFD;
    ES=1;            //打开串口中断允许位。
    EA=1;            //打开总中断。
    while(1)
    {
        
    }
}

void UART_Routine(void)   interrupt 4
{
    if(TI)                //如果TI发送标志位不为0,能进入这个条件说明已经发送数据出去了
    {
        TI=0;             //这时我们需使TI=0,让它继续发送下一个数据。
    }
    if(RI)                //如果RI接收标志位不为0,能进入这个条件说明已经接收数据成功了
    {
        RI=0;             //这时我们需使RI=0,让它继续接收下一个数据。
    }
}

5.串口案例分享:

题目:单片机向电脑每隔1秒发送一个数据,数据从0开始,一直递增;且电脑通过发送数据0-8控制单片机8盏LED灯亮灭,若是发送的数据不是0-8,那么单片机向电脑发回一个数据,且灯全灭。

#include <REGX52.H>
#include <INTRINS.H>

void UartInit();                  //声明串行口初始化函数。
void Delay1ms(int xms);           //声明延时函数。

void main()
{
        unsigned char number;      //定义一个变量接收来自电脑写入的数据。
        unsigned char sec;         //定义一个发送给电脑的变量
        UartInit();                //调用串行口初始化函数
        while(1)
        {
                SBUF=sec;          //往发送缓存器写入数据
                sec++;             //改变数据的值,让数据每次加1
                while(TI==0);      //若数据位加到TI=1,则跳出while循环,发送以为数据
                TI=0;              //立即清零,为发送下一位数据做准备。
                Delay1ms(1000);    //每隔1s发一个数据,这里可能会跟定时器1溢出混淆,不是开了定时器1吗?怎么还要另外延时
                                   //定时器1并没有开中断,他只是作为波特率发生器,溢出只是提醒串口需要发送一个码元(数据位)
                if(RI)             //能进入条件说明已经收到数据了
                {
                        RI=0;      //马上清零,为接收下一位数据做准备。
                        number=SBUF;   //定义的变量接住电脑发来的数据,也可以直接用SBUF
                        switch(number)                //判断电脑发来的数据,并执行对应的操作
                        {
                                case 1: P2=0xFE;break;
                                case 2: P2=0xFD;break;
                                case 3: P2=0xFB;break;
                                case 4: P2=0xF7;break;
                                case 5: P2=0xEF;break;
                                case 6: P2=0xDF;break;
                                case 7: P2=0xBF;break;
                                case 8: P2=0x7F;break;
                                default :SBUF=0x0E;P2=0xFF;break;  
                        }
                }
        }
}

void UartInit()            //串行口初始化函数
{
        SCON=0x50;         //启用工作方式1且允许接收数据。
        TMOD=0x20;         //选择定时器1的工作方式2
        TH1=0xFD;          //定时1/9600s,记满溢出。
        TL1=0xFD;          //不断给TH1赋予初始常量
        ES=1;              //开启串行口中断允许位
        EA=1;              //开启总中断。
        TR1=1;             //开启定时器启用允许位。
}


void Delay1ms(int xms)       //延时函数
{
        unsigned char i, j;
        while(xms)
        {
                _nop_();
                i = 2;
                j = 199;
                do
                {
                        while (--j);
                } while (--i);
                xms--;
        }
}

void Uart() interrupt 4      //串行口中断函数 注意中断号为4
{
        
        
}

6.效果展示:

串口通讯案例

  • 39
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值