当前位置:首页 > MCU > 正文内容

UART通信方式简明教程

chanra1n4年前 (2020-01-23)MCU3614

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都没有初始化不谈。

扫描二维码推送至手机访问。

版权声明:本文由我的FPGA发布,如需转载请注明出处。

本文链接:https://www.myfpga.cn/index.php/post/121.html

分享给朋友:

“UART通信方式简明教程” 的相关文章

XN297L 三线SPI 51单片机 STC15W401AS驱动 全网首发

XN297L 三线SPI 51单片机 STC15W401AS驱动 全网首发

//main.c  //作者:ChanRa1n Myfpga.cn //转载请写明出自! #include <STC15F2K60S2.H> #include <RF.h> void main()//发送模式...

STM32G0核心板V1.0开源页面

STM32G0核心板V1.0开源页面

原理图PCB项目工程文件STM32G0核心板.zip可以直接打印,Gerber_PCB_STM32G0核心板_2.zipBOMIDNameDesignatorFootprintQuantityManufacturer PartManufacturerSupplierSupplier PartPric...

Ai-M61-32S开发环境搭建 BL616/BL618 Windows/Linux

Ai-M61-32S开发环境搭建 BL616/BL618 Windows/Linux

这两天正在研究BL616的板子,想搞出来USB相关做项目用(需求是USB 2.0高速),都把AI-M62的加购了,准备这两天付款(毕竟运费6块,得看看店里有没有啥可以一起买的)。突然看到发的内容,可以白嫖。真的是想啥来啥,想要BL616的,结果白嫖BL618的。。。“就像是想吃奶了,娘来了。”你可能...

基于nRF52840实现一个FIDO2安全密钥

基于nRF52840实现一个FIDO2安全密钥

项目参考了1.https://github.com/google/OpenSK 2.https://github.com/adafruit/Adafruit_nRF52_Bootloader 3.https://github.com/canokeys/canokey-nrf52 坑已经踩完了,大家可...