嵌入式系统与单片机|技术阅读
登录|注册

您现在的位置是:嵌入式系统与单片机 > 技术阅读 > 串口通信与分时轮询调度器

串口通信与分时轮询调度器

【说在前面的话】

玩过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为串行控制寄存器,如下图

  • SM0SM1一起指定串行通信工作方式(这里我们设置为工作方式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(今天我们就用此方式
    11方式2,定时器1此时无效,停止计数


我们知道了定时器1的寄存器,那定时值要设置成多少呢?


这时就要看看波特率的计算公式了,如下图

  • 其中X就是我们要设置的定时器的值

  • 而fosc为单片机的晶振频率,我的板子上的晶振为12M,SMOD我们设置为1.

好了,接下来我们就计算一下波特率为9600时,X的值,如下图

计算出来为249.489,不是一个整数值,说明还是有误差的。所以这里需要注意一下

在使用串口通信时,如果波特率为9600,外部晶振最好选择11.0592MHz,这样计算出的值就没有小数部分,即降低了波特率的误差。如下图所示

【串口驱动程序】

好了,接下来我们就看看串口驱动程序该怎么写。首先我们先写一下串口初始化程序,如下

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}
  • 程序很简单,我们把波特率设置成了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寄存器赋值后就不用死等了(* ̄︶ ̄)

好,下面我们就看看程序该怎么修改,首先我们定义一个数组来存放要发送的数据,如下

#define UART_SEND_DATA_NUM 11char 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_numuart_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   SBUF = 0;  }
  • 在14行中,给SBUF随便赋一个值就可以了(* ̄︶ ̄)


到这里,我们今天的串口发送程序就写完。下一篇我们会继续讲串口接收数据,期待大家点赞+关注,这会加速我对文章更新的速度哦

欢迎订阅    嵌入式小书虫