【说在前面的话】
玩过STC89C52单片机的应该都用过串口,因为此单片机就是用串口下载程序的。那51 单片机的串口除了下载程序还有别的用途吗?当然就是可以用来和其他单片机进行通信。今天我们就讲一下单片机和电脑是怎么进行串口通信的。
【接线图】
单片机要和电脑进行串口通信,首先要把单片机连接到电脑,如下图
图中我们知道,单片机的IO口是不能直接和电脑相连的,需要一个usb转串口的转接板,转接板的USB口接到电脑,另一端接到单片机的IO口。
【串行通信】
简单讲串口通信是按位(bit)发送和接收数据的。尽管比按字节(byte)的并行通信要慢,但是串口可以在使用一根线发送数据的同时用另一根线接收数据。所以它很简单并且能够实现远距离的通信。那只有两根线是怎么进行串行通信的呢?(准确的说是3根,因为还有一根电源地线,即GND)
这就需要通信双方要有统一的串口协议(好比俩人交流,要使用相同的语言,如果一个说英语,一个说汉语是没法沟通的)。其中,最重要的就是波特率。
波特率简单讲就是串行通信每秒钟发送(或接收)的数据位数。假设发送一位数据所需要的时间为T,则波特率为1/T。而常用的串口波特率为9600、4800、115200等(一般是1200的倍数),如下图。
有了波特率那我们再看看两根线是怎么通信的,
首先RX为接收数据线,TX为发送数据线,在通信开始前,这两条线都为高电平。
当要发送数据时,先发一位起始位(即低电平),此时通信双方就知道接下来就是8位数据了。
当8位数据都发送完成后,在发送一位停止位(即高电平)就可以了,这样一个字节的数据就发送完成了,如下图
当然,串口通信还可能会有一位奇偶校验位,(是一种简单的校验数据是否正确的检错方式)。其实没有校验位也是可以的,我们今天就不使用校验位,不过还是有必要简单讲一下。
对于有奇偶校验的情况,串口会设置校验位(数据位后面的一位),用一位值来确保传输的数据有偶数个或者奇数个逻辑高电平。例如,如果数据是00001111,那么对于偶校验,校验位为0,保证逻辑高的位数是偶数个。如果是奇校验,校验位为1,这样就有5个逻辑高位了。这样做的好处就是当我们在传输数据时,如果有一位数据发生了翻转,接收端是可以校验出数据发生了错误,缺点就是我们又多发了一位数据,从而降低了传输速率。【设置串口寄存器】
知道了串口通信协议,我们就看看怎么在51单片机中设置串口相关的寄存器的。最主要的就是两个SCON和PCON寄存器。其中
SCON为串行控制寄存器,如下图
SM0和SM1一起指定串行通信的工作方式(这里我们设置为工作方式1),如下图所示
SM2为多机通信控制位。
多机通信是在方式2和方式3下进行的,这里我们不做介绍;
在方式1时,SM2=1则只有收到有效的停止位时才会激活接收中断标志位(RI);
在方式2时,SM2必须为0。
REN为允许串行接收位,由软件置1或清0。REN=1表示允许串口接收数据,REN=0表示禁止串口接收数据。
TB8为发送的第9位数据(这次我们用不到)。
在方式2或3时,TB8为要发送的第9位数据 在方式1时,TB8作为奇偶校验位 RB8为接收的第9位数据(这次我们也用不到)。
TI为发送中断标志位。TI=1表示1帧数据发送结束(此位由硬件置1)。TI位的状态可供软件查询,也可以申请中断。注意:TI位必须由软件清0
RI为接收中断标志位。RI=1表示1帧数据接收完毕,并申请中断。此位由硬件置1且必须由软件清0。
PCON为电源控制寄存器,如下图
其中,仅最高位SMOD与串口有关。
SMOD为波特率选择位。当SMOD=1时,此时的波特率为SMOD=0时的2倍,因此也称SMOD位为波特率倍增位。(今天我们设置SMOD位为1)
【波特率设置】
串口相关的寄存器我们介绍完了,那最重要的波特率该怎么设置呢?
说到波特率,其实就是设置发送1位数据的时间,而设置精确的时间,肯定是要用到定时器了,这里我们就使用定时器1来产生波特率的时钟。在实际设置波特率时,我们用定时器1的方式2(即自动重装初值)来设置波特率比较理想,因为它不需要软件来重装初值,这样就可以避免因软件重装初值带来的定时误差,所以计算出的波特率比较准确。
下面我们就简单看一下与定时器1相关的几个寄存器,首先讲一下定时器控制寄存器TCON,如下图
TF1为计数溢出标志位。
TR1为计数运行控制位。TR1=1时,允许T1开始计数;TR1=0时,禁止T1计数。
其他位我们今天用不到。
接下来讲一下TMOD寄存器,如下图
其中前4位是设置定时器1的,我们简单讲一下
GATE为门控位,GATE=0时,定时器1由控制位TR1来控制;GATE=1时,定时器1由外部中断引脚上的电平与TR1共同控制。(今天我们设置此位为0)
C/T为模式选择位。C/T = 0为定时器工作模式,C/T = 1为计数器工作模式。(今天我们设置此位为0,定时器工作模式)
M1、M0为设置工作方式,如下表所示
M1 M0 工作方式 0 0 方式0,为13位定时器/计数器 0 1 方式1,为16位定时器/计数器 1 0 方式2,为8位自动重装定时器,当计数溢出时将TH1中的数值自动重装入TL1(今天我们就用此方式) 1 1 方式2,定时器1此时无效,停止计数
我们知道了定时器1的寄存器,那定时值要设置成多少呢?
这时就要看看波特率的计算公式了,如下图
其中X就是我们要设置的定时器的值
而fosc为单片机的晶振频率,我的板子上的晶振为12M,SMOD我们设置为1.
好了,接下来我们就计算一下波特率为9600时,X的值,如下图
计算出来为249.489,不是一个整数值,说明还是有误差的。所以这里需要注意一下
在使用串口通信时,如果波特率为9600,外部晶振最好选择11.0592MHz,这样计算出的值就没有小数部分,即降低了波特率的误差。如下图所示
【串口驱动程序】
好了,接下来我们就看看串口驱动程序该怎么写。首先我们先写一下串口初始化程序,如下
void init_uart()
{
0x50; //设置串口为工作方式1 =
PCON |= 0x80; //设置SMOD=1
0; //清除发送中断 =
RI = 0; //清除接收中断
0x20; //设置定时器1为工作方式2 =
TMOD &= ~0x10;
TH1 = 249;//设置波特率为9600
TL1 = 249;
TR1 = 1;//使能定时器1
}
程序很简单,我们把波特率设置成了9600(直接把249赋值给TH1和TL1就可以了)
然后,在写一个串口发送数据的函数,如下
void uart_send_data(unsigned char c) {
SBUF = c;
while(TI==0);//等待发送完成
TI=0;//清0
}
这个也很简单,就是把要发送的数据赋值给SBUF寄存器就可以了,然后查询TI位是否为1,为1则发送完成,最后把TI清0。
接着,我们在写一个发送字符串的函数,如下
void uart_send_str(unsigned char *str,char num) {
char i;
//判断字符串是否为空
if(str == NULL){
return;
}
//发送数据
for(i = 0; i < num; i++){
uart_send_data(str[i]);
}
}
首先判断字符串是否为空,是空则返回什么都不发送
接着就是循环发送数据就可以了。
最后我们在写一个串口发送读取到ds1302时间的任务函数,每两秒调用一次此任务函数,如下
void uart_task(){
uart_send_str(data1302_display,8);
uart_send_str("\r\n",2);
}
void tasks_init(){
...
add_task(2000,18,&uart_task);
}
打开电脑串口助手,程序运行效果如下
图中我们发现是2秒发送一次数据给电脑,但是有时候会收到一个字母q,这个就是我们选取12M晶振导致的误差所致。所以在串口通信中,选取合适的外部晶振还是很重要的。
接下来就是今天的彩蛋环节
其实,上面的串口发送程序还是有一个小问题,没有讲,我们放到这个环节讲。
哈哈,这个问题相信大家也看出来了,那就是我们在发送数据时使用了while循环一直在那里死等数据发送完成。
那为啥运行的时候没有出现什么问题呢?
那是因为我们发送的数据比较少,也就10个字符(波特率为9600,发送一个字符大约1ms),所需的时间也不多,而且串口发送数据只需要给SBUF寄存器赋值就可以,赋值后就由硬件一位一位发送,我们只要等着就可以,期间定时器中断还是可以响应的。
虽然不会出什么大问题,但至少是有一些隐患的,所以我们还是要解决一下的。
由于我们发送一个字符大约需要1ms(波特率为9600),一个字符是8位,在加一个起始位和一个停止位共10位数据,简单计算一下,如下图
知道了发送一个字符需要的时间,我们就可以优化一下我们的发送程序了。大家可以这样想,发送一个字符需要的时间为1ms多一点,那如果我给他预留2ms来发送一个字符,那肯定是可以发送完成的。由此,我们可以在添加一个发送数据的任务,每2ms发送一个字符(如果有字符要发送),这样我们在给SBUF寄存器赋值后就不用死等了(* ̄︶ ̄)
好,下面我们就看看程序该怎么修改,首先我们定义一个数组来存放要发送的数据,如下
char uart_send_datas[UART_SEND_DATA_NUM];
char uart_send_num = 0;
uart_send_num = 0,表示当前有0个字符需要发送(即没有字符需要发送)
接下来,我们在写一个往数组中添加数据的函数,如下
void uart_send_add_data(char c){
if(uart_send_num < UART_SEND_DATA_NUM){
uart_send_datas[uart_send_num] = c;
uart_send_num++;
}
}
只要判断数组没有满就可以继续添加要发送的数据
有了这个函数,在写一个添加字符串的函数,如下
void uart_send_add_str(unsigned char *str,char num) {
char i;
if(str == NULL){
return;
}
for(i = 0; i < num; i++){
uart_send_add_data(str[i]);
}
}
添加完数据,就该启动串口发送数据了,所以我们在添加一个启动发送数据的函数,如下
char uart_send_flag = 0;
void uart_start_send(){
uart_send_flag = 1;
}
这个函数很简单,flag=0表示不发送数据,flag=1表示发送数据
那发送ds1302读取出来的时间任务函数就可以这样修改了,如下
void uart_task(){
uart_send_add_str(data1302_display,8);
uart_send_add_str("\r\n",2);
uart_start_send();//启动发送
}
先添加要发送的数据,然后启动发送就可以了
接下来就该写串口发送数据的任务函数了,如下
void uart_send_task(){
static char i = 0;
if(uart_send_flag == 1){
//发送数据
if(TI == 1){
TI = 0;
SBUF = uart_send_datas[i];
i++;
//发送完成
if(i >= uart_send_num){
i = 0;
uart_send_num = 0;
uart_send_flag = 0;
}
}
}
}
当uart_send_flag =1时,开始发送数据
TI=1表示当前数据已经发送完成,可以接着发送下一个数据。所以我们给TI清0后就可以直接给SBUF赋值
当发送的字符个数等于uart_send_num时,表示要发送的数据已经全部发送完了,此时我们要把uart_send_num 和uart_send_flag 清零,等待下一次数据的发送。
我们在任务初始化中添加这两个任务就可以了,如下
void tasks_init(){
...
add_task(2000,18,&uart_task);
add_task(2,0,&uart_send_task);
}
到这里,你以为串口就可以成功发送数据了吗?
答案是还不可以。。。
我们在看一下发送数据的任务函数,如下图
图中我们发现,只有在TI=1时才可以发送数据。当我们第一次发送数据时,TI位为0(因为TI位是由硬件置1的),所以就导致一直没有数据发送出去,那该怎么办呢?
其实也简单,我们在初始化串口时,先随便发送一个数据就可以了,这时,串口发送完成这个字符后就会把TI位置1了,修改后的程序如下
void init_uart()
{
SCON = 0x50; //设置串口为工作方式1
PCON |= 0x80; //设置SMOD=1
TI = 0; //清除发送中断
RI = 0; //清除接收中断
TMOD |= 0x20; //设置定时器1为工作方式2
TMOD &= ~0x10;
TH1 = 249;//设置波特率为9600
TL1 = 249;
TR1 = 1;//使能定时器1
0; =
}
在14行中,给SBUF随便赋一个值就可以了(* ̄︶ ̄)
到这里,我们今天的串口发送程序就写完。下一篇我们会继续讲串口接收数据,期待大家点赞+关注,这会加速我对文章更新的速度哦
欢迎订阅 嵌入式小书虫