UART通信方式简明教程
Uart 即最常见的串口通信方式,相信你点进来之前就对此有或多或少的了解了,不对基本介绍做赘述,
Uart协议采用两根数据总线,一根是RXD,一根是TXD,怎么区分呢?R是Receive接收的意思,T是Transmit发送的意思
协议需要掌握的有3个:时序、寄存器
Uart协议标准的构成为:起始位0+8位数据位+结束位1
这里分为含有Uart控制器的MCU和模拟UART的MCU讲解具体实现:
含有Uart控制器的MCU例如stc89c52(RC),
我们首先使用定时器1作为串行通信的波特率发生器。T1溢出率经2分频(或不分频)后又经16分频作为串行发送或接收的移位脉冲。移位脉冲的速率即波特率。
说白了就是我们多个MCU通信,需要一个共同的时钟,或者说每秒钟内说话的字数需要一致,否则无法正常说话。
说到现在,该谈谈怎么去发送和接收数据了,stc89c52(RC)内置了缓冲寄存器SBUF,你需要发送数据的时候就把数据写到SBUF里面即可
如果是想要接受数据呢?那就去读取SBUF好了!甚至为了使得这个过程更简单,两个过程使用的是同一个寄存器,同一个地址0x99
一段代码带你更清楚的理解这个过程
#include<reg52.h> #define uint unsigned int void UART_Init(); uint num ; int main() { UART_Init(); while(1); } void UART_Init() { //配置定时器,使其成为“波特率发生器”,让接收和发送的设置直接能够同步 TMOD = 0x20; //定时器0工作模式2,自动重装8位计数器 TH1 = 0xfd;//配置定时器参数 TL1 = 0xfd;//定时器溢出时,会自动将高8位中的值赋值给低8位.比特率9600 TR1 = 1;//T1置数运行位 SM0 = 0;//上图有介绍 SM1 = 1;//上图有介绍 REN = 1;//REN是接收运行控制位 EA = 1;//EA为中断允许控制位,即打开中断 ES = 1;//EA为串口中断允许控制位,即打开串口中断 } void UART() interrupt 4 //串口中断 { if(RI == 1) //如果接收到计算机发的数据,当接收完一个数据的时候,MCU会让RI也就是接收中断位置1 { num = SBUF; //读SBUF寄存器,取出数据 RI = 0; //清除标志位,让MCU继续接收数据 SBUF = ++num; //把数据加1后再发送给计算机 } if(TI == 1) //如果发送完毕 { TI = 0; //清除标志位 } }
无UART控制器的MCU,我们可以采用模拟的方式实现如上过程
#include "config.h" /***************************************************************************/ typedef bit BOOL; typedef unsigned char uchar; typedef unsigned int uint; #define Timer0_Reload (65536 - MAIN_Fosc / BaudRate / 3) #define D_RxBitLenth 9 //9: 8 + 1 stop #define D_TxBitLenth 9 //9: 1 stop bit sbit RXB = P3^0; //define UART TX/RX port sbit TXB = P3^1; uchar data TBUF,RBUF; uchar data TDAT,RDAT; uchar data TCNT,RCNT; //发送和接收检测 计数器(3倍速率检测) uchar data TBIT,RBIT; //发送和接收的数据计数器 uchar data t, r; uchar data buf[16]; bit TING,RING; //正在发送或接收一个字节 bit REND; //接收完的标志位 #define RxBitLenth 9 //8个数据位+1个停止位 #define TxBitLenth 9 //8个数据位+1个停止位 //----------------------------------------- //UART模块的初始变量 initial UART module variable void UART_INIT() { TING = 0; RING = 0; REND = 0; TCNT = 0; RCNT = 0; } void main() { InternalRAM_enable(); // ExternalRAM_enable(); Timer0_1T(); Timer0_AsTimer(); Timer0_16bitAutoReload(); Timer0_Load(Timer0_Reload); Timer0_InterruptEnable(); Timer0_Run(); EA = 1; //打开总中断 open global interrupt switch UART_INIT(); //UART模块的初始变量 while (1) { if (REND) //如果接收完,把接收到的值存入接收buff { REND = 0; buf[r++ & 0x0f] = RBUF; } if(!TING) //发送空闲 { if (t != r) //有要发送的数据 { TBUF = buf[t++ & 0x0f]; TING = 1; } } } } //----------------------------------------- //定时器0中断程序for UART 以波特率3倍的速度采样判断 开始位 Timer interrupt routine for UART void tm0(void) interrupt 1 { if (RING) { if (--RCNT == 0) //接收数据以定时器的1/3来接收 { RCNT = 3; //重置接收计数器 接收数据以定时器的1/3来接收 reset send baudrate counter if (--RBIT == 0) //接收完一帧数据 { RBUF = RDAT; //存储数据到缓冲区 save the data to RBUF RING = 0; //停止接收 stop receive REND = 1; //接收完成标志设置 set receive completed flag } else { RDAT >>= 1; //把接收的单b数据 暂存到 RDAT(接收缓冲) if (RXB) RDAT |= 0x80; //shift RX data to RX buffer } } } else if (!RXB) //判断是不是开始位 RXB=0; { RING = 1; //如果是则设置开始接收标志位 set start receive flag RCNT = 4; //初始化接收波特率计数器 initial receive baudrate counter RBIT = RxBitLenth; //初始化接收的数据位数(8个数据位+1个停止位) initial receive bit number (8 data bits + 1 stop bit) } if (TING) //发送开始标志位 judge whether sending { if (--TCNT == 0) //发送数据以定时器的1/3来发送 { TCNT = 3; //重置发送计数器 reset send baudrate counter if (TBIT == 0) //发送计数器为0 表明单字节发送还没开始 { TXB = 0; //发送开始位 send start bit TDAT = TBUF; //把缓冲的数据放到发送的buff load data from TBUF to TDAT TBIT = TxBitLenth; //发送数据位数 (8数据位+1停止位) initial send bit number (8 data bits + 1 stop bit) } else //发送计数器为非0 正在发送数据 { if (--TBIT == 0) //发送计数器减为0 表明单字节发送结束 { TXB = 1; //送停止位数据 TING = 0; //发送停止位 stop send } else { TDAT >>= 1; //把最低位送到 CY(益处标志位) shift data to CY TXB = CY; //发送单b数据 write CY to TX port } } } } }
这个是STC公司官方的历程,写的还不错,给人一种刻意把简单的东西写复杂的感觉,我们一点一点来分析他们的代码,先附上config.h防止有同学看的很迷茫
#ifndef __CONFIG_H #define __CONFIG_H /*********************************************************/ //#define MAIN_Fosc 5529600L //定义主时钟 110 ~ 4800 //#define MAIN_Fosc 11059200L //定义主时钟 110 ~ 9600 //#define MAIN_Fosc 12000000L //定义主时钟 110 ~ 9600 #define MAIN_Fosc 22118400L //定义主时钟 220 ~ 19200 //#define MAIN_Fosc 24000000L //定义主时钟 220 ~ 19200 //#define MAIN_Fosc 33177600L //定义主时钟 220 ~ 38400 #define BaudRate 19200 //模拟串口波特率 /*********************************************************/ #include "STC15Fxxxx.H" #endif
我们一点一点来分析
typedef bit BOOL; typedef unsigned char uchar; typedef unsigned int uint;
我是很难理解为什么单片机开发很多人喜欢自己起名字,如果真的那么多优点,为什么没有成标准?
#define Timer0_Reload(65536 - MAIN_Fosc / BaudRate / 3) #define D_RxBitLenth9//9: 8 + 1 stop #define D_TxBitLenth9//9: 1 stop bit
声明了3个常量,分别定义了重载时间、发烧接收位的长度
sbit RXB = P3^0; //define UART TX/RX port sbit TXB = P3^1;
声明P3^0和1口分别为接收和发送端口
uchar data TBUF,RBUF; uchar data TDAT,RDAT; uchar data TCNT,RCNT;//发送和接收检测 计数器(3倍速率检测) uchar data TBIT,RBIT;//发送和接收的数据计数器 uchar data t, r; uchar data buf[16];
由于模拟UART,没有缓冲寄存器SBUF,所以作者设置了TBUF和RBUF分别为发送和接收数据缓冲寄存器
而TDAT和RDAT是什么呢?他们被用来接收数据,接收到了之后将数据传送至相应的缓冲寄存器
t和r是干嘛用的呢?简单的说,就是同步。接收了多少钱,它就要发送多少次。这俩变量可以结合以下理解
while (1) { if (REND) //如果接收完,把接收到的值存入接收buff { REND = 0; buf[r++ & 0x0f] = RBUF; } if(!TING) //发送空闲 { if (t != r) //有要发送的数据 { TBUF = buf[t++ & 0x0f]; TING = 1; } } } }
其实准确的说,我认为俩变量t和r并没有初始化,大多数的编译器会自动填上0,但是我认为这是一个错误的示例。
bit TING,RING;//正在发送或接收一个字节 bit REND; //接收完的标志位
定义了几个位变量,用来表示是否正在发送或者接收,REND->Receive End 接收结束
#define RxBitLenth 9 //8个数据位+1个停止位 #define TxBitLenth 9 //8个数据位+1个停止位
定义了两个常量,即接收和发送数据的字节位数。
//UART模块的初始变量 initial UART module variable void UART_INIT() { TING = 0;//当前没有在发送 RING = 0;//当前没有在接收 REND = 0;//当前接收没有接收 TCNT = 0;//发送和接收检测计数器(3倍速率检测) RCNT = 0;//发送和接收检测计数器(3倍速率检测) }
接下来就是主程序了,但是我想在讲解主程序之前先讲解一下中断
//定时器0中断程序for UART 以波特率3倍的速度采样判断 开始位 Timer interrupt routine for UART void tm0(void) interrupt 1 { if (RING) { if (--RCNT == 0) //接收数据以定时器的1/3来接收 { RCNT = 3; //重置接收计数器 接收数据以定时器的1/3来接收 reset send baudrate counter if (--RBIT == 0) //接收完一帧数据 { RBUF = RDAT; //存储数据到缓冲区 save the data to RBUF RING = 0; //停止接收 stop receive REND = 1; //接收完成标志设置 set receive completed flag } else { RDAT >>= 1; //把接收的单b数据 暂存到 RDAT(接收缓冲) if (RXB) RDAT |= 0x80; //shift RX data to RX buffer } } } else if (!RXB) //判断是不是开始位 RXB=0; { RING = 1; //如果是则设置开始接收标志位 set start receive flag RCNT = 4; //初始化接收波特率计数器 initial receive baudrate counter RBIT = RxBitLenth; //初始化接收的数据位数(8个数据位+1个停止位) initial receive bit number (8 data bits + 1 stop bit) } if (TING) //发送开始标志位 judge whether sending { if (--TCNT == 0) //发送数据以定时器的1/3来发送 { TCNT = 3; //重置发送计数器 reset send baudrate counter if (TBIT == 0) //发送计数器为0 表明单字节发送还没开始 { TXB = 0; //发送开始位 send start bit TDAT = TBUF; //把缓冲的数据放到发送的buff load data from TBUF to TDAT TBIT = TxBitLenth; //发送数据位数 (8数据位+1停止位) initial send bit number (8 data bits + 1 stop bit) } else //发送计数器为非0 正在发送数据 { if (--TBIT == 0) //发送计数器减为0 表明单字节发送结束 { TXB = 1; //送停止位数据 TING = 0; //发送停止位 stop send } else { TDAT >>= 1; //把最低位送到 CY(益处标志位) shift data to CY TXB = CY; //发送单b数据 write CY to TX port } } } } }
程序是很容易理解的,如果不能理解请前往定时器相关章节学习
void main() { InternalRAM_enable(); // ExternalRAM_enable(); Timer0_1T(); Timer0_AsTimer(); Timer0_16bitAutoReload(); Timer0_Load(Timer0_Reload); Timer0_InterruptEnable(); Timer0_Run(); EA = 1; //打开总中断 open global interrupt switch UART_INIT(); //UART模块的初始变量 while (1) { if (REND) //如果接收完,把接收到的值存入接收buff { REND = 0; buf[r++ & 0x0f] = RBUF; } if(!TING) //发送空闲 { if (t != r) //有要发送的数据 { TBUF = buf[t++ & 0x0f]; TING = 1; } } } }
这个主程序做了什么呢?
初始化串口->接收数据->发送数据
InternalRAM_enable(); // ExternalRAM_enable(); Timer0_1T(); Timer0_AsTimer(); Timer0_16bitAutoReload(); Timer0_Load(Timer0_Reload); Timer0_InterruptEnable(); Timer0_Run();
这些函数就不作赘述了,大家可以查看MCU的库
while (1) { if (REND) //如果接收完,把接收到的值存入接收buff { REND = 0; buf[r++ & 0x0f] = RBUF; } if(!TING) //发送空闲 { if (t != r) //有要发送的数据 { TBUF = buf[t++ & 0x0f]; TING = 1; } } }
程序无限判断是否接收完成,和是否在发送空闲状态,如果接受到数据就把它存到寄存器里面,然后再发送出去,如果发送到的数据不比接收到的多,就把数据发送过去,这里存在一个明显问题
那就是
t != r
请问作者怎么解决如果发生溢出或者错误导致t!=r的情况?如果错误了,那么程序会陷入无限发送的死循环中,尚且t和r都没有初始化不谈。