51单片机的串口,是个全双工的串口,发送数据的同时,还可以接收数据。

当串行发送完毕后,将在标志位 TI 置 1,同样,当收到了数据后,也会在 RI 置 1。

无论 RI 或 TI 出现了 1,只要串口中断处于开放状态,单片机都会进入串口中断处理程序。

在中断程序中,要区分出来究竟是发送引起的中断,还是接收引起的中断,然后分别进行处理。

看到过一些书籍和文章,在串口收、发数据的处理方法上,很多人都有不妥之处。

接收数据时,基本上都是使用“中断方式”,这是正确合理的。

即:每当收到一个新数据,就在中断函数中,把 RI 清零,并用一个变量,通知主函数,收到了新数据。

发送数据时,很多的程序都是使用的“查询方式”,就是执行 while(TI ==0); 这样的语句来等待发送完毕。

这时,处理不好的话,就可能带来问题。

看了一些网友编写的程序,发现有如下几条容易出错:

1.有人在发送数据之前,先关闭了串口中断!等待发送完毕后,再打开串口中断。

这样,在发送数据的等待期间内,如果收到了数据,将不能进入中断函数,也就不会保存的这个新收到的数据。

这种处理方法,就会遗漏收到的数据。

2.有人在发送数据之前,并没有关闭串口中断,当 TI = 1 时,是可以进入中断程序的。

但是,却在中断函数中,将 TI 清零!

这样,在主函数中的while(TI ==0);,将永远等不到发送结束的标志。

3.还有人在中断程序中,并没有区分中断的来源,反而让发送引起的中断,执行了接收中断的程序。

对此,做而论道发表自己常用的方法:

接收数据时,使用“中断方式”,清除 RI 后,用一个变量通知主函数,收到新数据。

发送数据时,也用“中断方式”,清除 TI 后,用另一个变量通知主函数,数据发送完毕。

这样一来,收、发两者基本一致,编写程序也很规范、易懂。

更重要的是,主函数中,不用在那儿死等发送完毕,可以有更多的时间查看其它的标志。

实例:

求一个PC与单片机串口通信的程序,要求如下:

1、如果在电脑上发送以$开始的字符串,则将整个字符串原样返回(字符串长度不是固定的)。

2、如果接收到1,则将P10置高电平,接收到0,P10置低电平。(用来控制一个LED)

单片机是STC89C52RC/晶振11.0592/波特率要求是9600或4800。

问题补充:可能会将【$ABCD,123456,987654ccc,aasdasd,aaaa,sssd,4D】这样的字符串(字符串长度约为50-150个字符)传送给单片机,只能能原样返回。

最佳答案:

下列程序,已经调试成功。

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117


#include <REG52.H>


sbit LED = P1^0;


unsigned char UART_buff;


bit New_rec = 0, Send_ed = 1, Money = 0;


//----------------------------------------------


void main (void)


{


SCON = 0x50;   //串口方式1, 8-n-1, 允许接收.


TMOD = 0x20;   //T1方式2


TH1 = 0xFD;    [url=]//9600bps@11.0592MHz[/url]


TL1 = 0xFD;


TR1 = 1;


ES  = 1;       //开中断.


EA  = 1;


while(Money == 0);    //等着交费,呵呵,等着接收$.


while(1)  {


if ((New_rec == 1) && (Send_ed == 1))  {  //如果收到新数据及发送完毕


SBUF = UART_buff; //那就发送.


New_rec = 0;


Send_ed = 0;


} }


}


//----------------------------------------------


void ser_int (void) interrupt 4


{


if(RI == 1) {  //如果收到.


RI = 0;      //清除标志.


New_rec = 1;


UART_buff = SBUF;  //接收.


if(UART_buff == '1')  LED = 1;


if(UART_buff == '0')  LED = 0;


if(UART_buff == '$')  Money = 1;


}


else  {        //如果送毕.


TI = 0;      //清除标志.


Send_ed = 1;


}


}


//----------------------------------------------

串口接收程序是基于串口中断的,单片机的串口每次接收到一字节数据产生一次中断,然后再读取某个寄存器就可以得到串口接收的数据了。然而在实际应用当中,基本上不会有单字节接收的情况。一般都是基于一定串口通信协议的多字节通信。在422或者485通信中,还可能是一个主机(一般是计算机)带多个从机(相应的有单片机的板卡)。这就要求我们的单片机能够在连续接收到的串口数据序列中识别出符合自己板卡对应的通信协议,来进行控制操作,不符合则不进行任何操作。简而言之就是,单片机要在一串数据中找到符合一定规律的几个字节的数据。

先来说下怎样定串口协议吧。这个协议指的不是串口底层的协议,而是前面提到的数据帧协议。一般都是有帧头(2~3个字节吧),数据(长度根据需要),结束位(1位,有时候设计成校验字节,最简单的校验也就是前面所有数据求和)。

比如0xaa 0x55 +(数据部分省略)+校验和(除了aa 55 之外数据的和),如果要是多板卡的话有时候还要在帧头后面加一个板选字节(相当于3字节帧头了)。

第一次写串口接收程序的时候,我首先想到的就是定义一个全局变量(实际上最好是定义局部静态变量),初始值设置为0,然后每进一次中断+1,然后加到串口通信协议的长度的时候再清零。然后判断帧头、校验。写完了之后我自己都觉得不对,一旦数据错开了一位,后面就永远都接收不到数了。无奈看了一下前辈们的代码,跟我的思路差不多,只不过那个计数值跟接收到的数据时同时判断的,而且每次中断都要判断,一旦不对计数的那个变量就清零。

废话少说,直接上一段代码让大家看看就明白了。(通信协议姑且按照简单的aa 55 一个字节数据 一个字节校验,代码是基于51单片机的)。接收成功则在中断程序中把串口接收成功标志位置1。

然后串口中断部分

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84


void ser()interrupt 4


{


static unsigned char count;//串口接收计数的变量


RI=0;//手动清某个寄存器,大家都懂的


receive[count]=SBUF;


if(count==0&&receive[count]==0xaa)//同时判断count跟收到的数据


{


count=1;


}


else if(count==1&&receive[count]==0x55)


{


count=2;


}


else if(count==2)


{


count++;


}


else if(count==3&&receive[count]== receive [2])//判断校验和,数据多的话是求//和,或者其他的校验方法,也可能是固定的帧尾


{


count=0;


uart_flag =1;//串口接收成功标志,为1时在主程序中回复,然后清零


ES=0;      //关中断,回复完了再ES=1;


}


else


{


count=0;//判断不满足条件就将计数值清零


}


}

第一次做的串口大概就按照这个方法写完了(我后来看过其他的代码,有人用switch语句写的,逻辑跟这个也差不多,不过我还是感觉用if else来写清晰一些),

不过在测试的时候发现了bug,如果数据帧发送一半,然后突然停止,再来重新发,就会丢失一帧的数据。比如先接受到aa 55,然后断了,再进来aa 55 01 01,就不受控制了。后来我也想到一个bug,如果在多设备通信中,属于其他设备的的帧数据最后一位是aa(或者最后两位为aa 55 ,或者最后3位为aa 55 板选),下一次通信的数据就接收不到了。

当时对于数据突然中断的bug,没有想到很好的解决办法,不过这种情况几率极小,所以一直用这个方法写也没有问题。多设备通信最后一位恰好是aa的几率也很小,出问题的可能也很小。当时项目里面的控制数据跟校验恰好不可能出现aa,于是我把if(count==0&&receive[count]==0xaa)改成了if(receive[count]==0xaa)其他都没变,解决了,没有bug了。

后来我又写了几次单片机程序,才想到了一些解决问题的方法——不过改天再接着写吧,太累了,明天还要上班呢。

在后来的项目中,真的遇到了数据位跟校验位都可能出现aa的情况。我考虑到每次数据都是连续发送的(至少我们用labwindows做的上位机程序是这样的),成功接收到了一帧数据是要有一定时间回复的,也就是说如果接收到一半,但是很长时间没接收到数据,把计数值count清零就ok啦。涉及时间的问题自然要用定时器来实现啦。

这次的通信协议如下,串口波特率19200,2个帧头aa 55 ,一个板选,6字节数据,一个校验字节(除帧头外其他数据的和)。

全局变量定义

1
2
3
4
5
6
7
8
9


unsigned char boardAddr;//板选地址,通过检测几个io引脚,具体怎么得到的就不写了,很简单的


unsigned char g_DatRev [10]={0};//接收缓存


bit retFlag=0//为1代表串口接收到了一帧数据

串口初始化函数,晶振22.1184

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42


void init_uart()


{


SCON = 0x50;                 //串口方式1允许接收


TMOD = 0x21;                //定时器1,方式2,8位自动重载,同时配置定时器0,工作方式1


PCON = 0x80;                // 波特率加倍


TH1 = 0xfa;


TL1 = 0xfa;               //写入串口定时器初值


TH0=(65536-2000)/256;    //写入定时器0初值,串口传输一个字节时间为(1/19200)*10,计算得0.52ms


TL0=(65536-2000)%256;   //定时器0定时大约1ms多


EA=1;


ET0=1;                  //波特率:19200    22.1184M  初值:250(0xfa)


IE |= 0x90;


TR1 = 1;


}

串口中断函数

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159


void UART_INT(void) interrupt 4


{


static unsigned char count;//串口接收计数的变量





RI = 0;


g_DatRev[count] = SBUF;


if(g_DatRev[count]==0xaa&&count==0)             //帧头


{


count=1;


}


else if(count==1&&g_DatRev[count]==0x55)


{


count=2;


}


else if (count==2&&g_DatRev[2] == boardAddr)


{


CK = g_DatRev[count];


count=3;








}





else if(count>=3&&count<9)


{





CK += g_DatRev[count];


count ++;





}


else if(count == 9&&CK==g_DatRev[9])


{


ES = 0;


retFlag = 1;


count=0;


}


else


{


count=0;


}


resettimer();


}





//判断count不为0的话就启动定时器


void resettimer()


{


TR0=0;


TH0=(65536-2000)/256;


TL0=(65536-2000)%256;


if(count!=0)


{


TR0=1;


}


}

定时器中断函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21


void T0_time()interrupt 1


{


TR0=0;


TH0=(65536-2000)/256;


TL0=(65536-2000)%256;


count=0;


}

这种方法的确是本人自己想出来的,别人可能也这样做过,但我这个绝对不是抄袭或者模仿来的。这样写的确可以避免前面提到过的bug,不过代价是多用了一个定时器的资源,而且中断函数里的内容更多了,占用了更多的时间。

要是能把第一种方法改进一下就好了,主要是那个校验不能为aa的那个bug,因为毕竟传输到一半突然断了的可能性是非常小的。后来我想第一个判断if(count==0&&receive[count]==0xaa)好像有点太严格了,考虑到第二字节的帧头,跟板选地址不可能为aa,于是把这个改写为if(count>=0&&count<=2&& receive[count]==0xaa),这样就把bug出现的几率降到了非常小,也只是在前一帧结尾数据恰好为 aa 55 板选 的时候才出现,几率是多少大家自己算一下吧,呵呵。这样我自己觉得,昨天写的那种方法改进到这个程度,应该算可以啦,反正我是很满意了。

实际上我还想过其他的方法,比如缓存的数组采用移位寄存的方式。拿前面的4个字节的协议为例。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51


void ser()interrupt 4


{


unsigned char i;


RI=0;





for(i=0;i<3;i++)


{


receive[i]=receive[i+1];


}


receive[3]=SBUF;


if(reveive[0]==0xaa&&receive[1]==0x55&&receive[2]==receive[3])


{


ret_flag=1;


ES = 0;


}





}

这段代码看上去可是简单明了,这样判断可是不错啊,同时判断帧头跟校验不会产生前面提到的bug。说实话当时我刚想出这种方法并写出来的时候,马上就被我给否了。那个for循环可真是很占时间的啊,延时函数都是这样写的。每次都循环一下,这延时太长,通信速度太快的话就不能接收到下一字节数据了。最要命的是这个时间的长度是随着通信协议帧的字节数增加而增加的,如果一次要接收几十个字节,肯定就玩完了。这种方法我一次都没用过。

不过我居然又想出来了这种方法的改良措施,是前两天刚想出来的,呵呵,还没有实践过呢。

下面代码的协议就按第二段程序(定时器清零的那个协议,一共10字节)

全局变量

1
2
3
4
5
6
7
8
9


bit ret_flag;


unsigned char receive[256]={0};


unsigned char boardaddress;

中断函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57


void ser()interrupt 4


{





static unsigned char i=0;


static unsigned char total=0;


RI=0;


receive[i]=SBUF;


total=total-receive[i-7]+receive[i-1];





if(receive[i-9]==0xaa&&receive[i-8]==0x55


&&receive[i-7]==boardaddress&&receive[i]==total


)


{


ret_flag=1;


ES = 0;


}


i++;





}

之所以要定义256个长度的数组,就是为了能够让数组“首尾相接”。因为0 -1 = 255 , 255+1 = 0。而且我在计算校验的时候也改进了算法,不会因为数据长度的增加而增加计算校验值的时间。这种方法也是我不久前才想出来的,所以还没有经过实际的验证。上面的代码可能会有逻辑上的错误,如果真有错误,有网友看出来的话,请在下面留言告诉我。这个方法也是我原创的哦,别人也肯能会想到,不过我这个绝对不是抄袭别人的。

上面的代码最大的缺点就是变量定义的太多了,太占ram资源了,编译的时候可能会出现错误,毕竟51单片机才128字节的ram(有的资源也很丰富的,比如c8051系列的),这一下子就是256字节的变量。不过对于资源多一些的单片机,这样写还是可以的。要是能有4bit在一起的数据类型就好了,呵呵,verilog代码里面是可以的,C语言里貌似不行啊。

要想能在例如51单片机上运行,只能按照下面的折中方式了,也就是把i相关的量都与一个0x0f

全局变量

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15


bit ret_flag;


unsigned char receive[16]={0};// 可以考虑在定义时加上idata,毕竟还可能是32


//或者64长度的数组呢unsigned char idata receive[16]={0};





unsigned char boardaddress;

中断函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57


void ser()interrupt 4


{





static unsigned char i=0;


static unsigned char total=0;


RI=0;


receive[i&0x0f]=SBUF;


total=total-receive[(i-7)&0x0f]+receive[(i-1)&0x0f];





if(receive[(i-9)&0x0f]==0xaa&&receive[(i-8)&0x0f]==0x55


&&receive[(i-7)&0x0f]==boardaddress&&receive[i&0x0f]==total


)


{


ret_flag=1;


ES = 0;


}


i++;





}