目录
usart相关基础概念
usart通讯协议
uart硬件连接
usart代码
uart轮询等待
uart中断接收
printf重定向
dma串口空闲中断接收变长数据
简单线性数组
循环缓冲区环形FIFO
RS232、RS422、RS485
与uart的关系
RS232
RS422
RS485
usart相关基础概念
usart通讯协议
uart是异步全双工串行通讯,usart是同步全双工串行通讯,根据不同的芯片是否支持同步的串口通讯,异步没有时钟线,依赖rx和tx两条信号线传输,而同步串口通讯会多一条sclk线,但是传输数据方式都是一致,数据是逐帧发送的,每个数据帧包括起始位、数据位、校验位、停止位


- 数据位:长度可为5-8位,一般为8位有效数据位
- 校验位:用于校验传输的数据是否准确,采用的校验方法是奇校验和偶校验,校验不完全严谨
- 停止位:一般有0.5位、1位、1.5位、2位,只有同步串口才可以配置为0.5位1.5位
- 空闲帧:在连续发完一个字符串后,如果一个数据帧长度的字节都为高电平则为空闲帧,表示字符串已经发送完
- 波特率:在异步通讯中由于没有时钟信号所以两个通讯设备之间需要预定好波特率,即每个码元的长度,以便对信号进行解码,常见的有4800、9600、115200等(注意比特率是指每秒传输的bit数,在二进制中比特率等于波特率)
uart硬件连接
异步uart没有时钟线只有rx和tx,是将主设备的rx和tx分别连接到从设备的tx和rx,然后许多开发板上都可能有usb转TTL模块,这样就可以将usb传来的数据转成TTL电平,TTL电平是单片机IO口直接输出的逻辑电平,通常用0V代表逻辑0,+3.3V或+5V代表逻辑1,这样与电脑用usb来连接救恩那个识别到一个虚拟的com口
usart代码
这里就不用软件模拟串口通信协议了,与iic和spi类似,这里介绍的是stm32提供的硬件usart,stm32提供了5种串口模式,有同步的usart和异步的uart,这里的IrDa是红外接收信号功能,LIN是局部连接网络功能,多用于汽车方面

然后关于串口通讯的应用可以通过轮询等待或者中断来接收和传送数据,还有printf重定向,以及更加高级的dma串口空闲中断来处理数据

- Mode:这里选择的是异步模式,还有Synchronous(同步模式)、Single Wire(单线模式)只用一根线收发,节省引脚、IrDA / LIN / SmartCard红外、LIN 总线、智能卡等特殊协议模式
- Hardware Flow Control (RS232):禁用硬件流控
- Basic Parameters(基本参数):有波特率、数据长度(包含校验位)、校验方式、停止位
- Advanced Parameters(高级参数):这里可以配置只发或者只收,或者收发双向使能,Over Sampling是过采样倍数,用于提高接收精度,不常用
uart轮询等待
/* Private user code ———————————————————*/
/* USER CODE BEGIN 0 */
uint8_t buffer[50]={0};
uint16_t size=0;
/* USER CODE END 0 */
/**
* @brief The application entry point.
* @retval int
*/
int main(void)//其余的HAL库配置代码没有显示出来
{
//发送一个字符串
HAL_UART_Transmit(&huart1,(uint8_t *)"Hello,world!",12,1000);//它这里的轮询等待还需要一个超时时间的参数
/* USER CODE END 2 */
/* Infinite loop */
/* USER CODE BEGIN WHILE */
while (1)
{
//接收一个定长字符串然后原样返回
if( HAL_UART_Receive(&huart1,buffer,12,1000)==HAL_OK)//这里的接收必须为12个才能接收得到,超过12个会截断,少于12个收不到
{
HAL_UART_Transmit(&huart1,buffer,12,1000);//这里发送的长度也要和接收一致,不然的话多出来的是buffer数组里面\\0
}
//接收一个变长字符串然后原样返回
if(HAL_UARTEx_ReceiveToIdle(&huart1,buffer,50,&size,HAL_MAX_DELAY) == HAL_OK)
//这里的size是可能接收到的字符串的长度,就是缓冲区的长度,*Rxlen才是最终接收到的字符串的长度
{
HAL_UART_Transmit(&huart1,buffer,size,HAL_MAX_DELAY);
}
/* USER CODE END WHILE */
/* USER CODE BEGIN 3 */
}
/* USER CODE END 3 */
uart中断接收
int main()
{
while (1)
{
//以中断方式接收数据
HAL_UARTEx_ReceiveToIdle_IT(&huart1,buffer,50);//因为是以中断方式接收,所以不用等待时间这个参数
if(isOver)
{
HAL_UART_Transmit(&huart1,buffer,size,HAL_MAX_DELAY);
isOver=0;
}
}
}
void USART1_IRQHandler(void)
{
/* USER CODE BEGIN USART1_IRQn 0 */
/* USER CODE END USART1_IRQn 0 */
HAL_UART_IRQHandler(&huart1);
/* USER CODE BEGIN USART1_IRQn 1 */
/* USER CODE END USART1_IRQn 1 */
}
/* USER CODE BEGIN 1 */
extern uint8_t isOver;
extern uint16_t size;
// void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
// {
// isOver=1;
// }这个回调函数不能体现字符串的长度从而不能达到变长效果
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huasrt,uint16_t Size)
{
//这个回调函数有字符串长度的参数,可以通过这个传递给size
isOver=1;
size=Size;
}
printf重定向
printf重定向是指将printf的输出重定向到串口,这样想要printf的内容就可以通过串口发送出去了,注意要在keil种勾选“Use MicroLIB”
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
dma串口空闲中断接收变长数据
简单线性数组
代码思路:
先是定义接收完成标志和接收到的数据长度记录这一帧数据的实际长度,还有接收缓冲区存放dma收到的数据,这里设置了最大每次为200字节,根据实际情况修改,然后进入中断时先获取标志,中断发生进入中断先清楚标志防止无法再次发生中断,然后是停止DMA,因为当前 DMA 正在把数据搬运到rx_buff,收到 IDLE 时,DMA 可能还没搬完 200 个字节,但一帧已经结束,所以先停止 DMA,防止继续接收新数据覆盖旧数据,然后就是计算接收数据的实际长度存放在rx_len变量中,然后将接受完成标志位置1,然后主函数是使能串口空闲中断和DMA接收,然后是数据处理函数Receive,当接收完成进入到自定义的函数处理这个数据,然后清空这个缓冲区为下一次作准备,然后清除标志,清除长度,最后再重新启动DMA接收
相关hal库代码:
volatile uint8_t rx_end_flag;
volatile uint8_t rx_len;
uint8_t rx_buff[200];
void USART2_IRQHandler(void)
{
/* USER CODE BEGIN USART2_IRQn 0 */
uint32_t flag = 0;
uint32_t temp;
flag = __HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE);
if(flag != RESET)
{
__HAL_UART_CLEAR_IDLEFLAG(&huart2);
HAL_UART_DMAStop(&huart2);
temp=__HAL_DMA_GET_COUNTER(&hdma_usart2_rx);
rx_len=200-temp;
rx_end_flag=1;
}
/* USER CODE END USART2_IRQn 0 */
HAL_UART_IRQHandler(&huart2);
/* USER CODE BEGIN USART2_IRQn 1 */
/* USER CODE END USART2_IRQn 1 */
}
int main()
{
__HAL_UART_ENABLE_IT(&huart2,UART_IT_IDLE);
HAL_UART_Receive_DMA(&huart2,rx_buff,200);
void Receive(void)
{
if(rx_end_flag==1)
{
Receive_Data(rx_buff);
memset(rx_buff,0,rx_len);
rx_len=0;
rx_end_flag=0;
HAL_UART_Receive_DMA(&huart2,rx_buff,200);
}
}
}
这里是采用DMA搬运不定长接收数据,实时性好,高效,但是这里采用的是简单线性数组来充当缓冲区,buff的大小根据最大数据帧而规定,使用不是很灵活,如果处理太慢,下一帧会覆盖上一帧未处理的数据,可以采用循环缓冲区环形FIFO,这样可以支持连续接收和分段处理,更适合高频率
循环缓冲区环形FIFO

环形缓冲区实际上是基于固定大的小数据的先进先出的数据结构,但是与前面的简单线性结构不同,它通过它通过两个指针写指针(head) 和 读指针(tail)在逻辑上形成一个闭合的环,使得当指针到达数组末尾时能够回到起始位置,从而循环利用存储空间,不用像前面要关闭DMA后处理完数据再打开,通俗的描述就是一个履带上传送货物,货物(数据)从一端放上,从另一端取下,当传送带转到底时,又会绕回到开头
- 这里就不一一介绍代码思路了,可以自己慢慢琢磨一下
#define FIFO_SIZE 256
//环形FIFO结构体
typedef struct
{
uint8_t buf[FIFO_SIZE];
volatile uint16_t head;
volatile uint16_t tail;
} uart_fifo_t;
//全局FIFO实例
uart_fifo_t g_uart2_fifo;
volatile uint8_t g_rx_end_flag = 0;
volatile uint16_t g_rx_len = 0;
/*====================FIFO操作函数====================*/
//初始化FIFO
void fifo_init(uart_fifo_t *fifo)
{
memset(fifo->buf, 0, FIFO_SIZE);
fifo->head = 0;
fifo->tail = 0;
}
//检查FIFO是否为空
uint8_t fifo_is_empty(uart_fifo_t *fifo)
{
return (fifo->head == fifo->tail);
}
//检查FIFO是否已满
uint8_t fifo_is_full(uart_fifo_t *fifo)
{
return ((fifo->head + 1) % FIFO_SIZE == fifo->tail);
}
//向FIFO写入1字节(在中断中调用)
uint8_t fifo_write_byte(uart_fifo_t *fifo, uint8_t data)
{
if (fifo_is_full(fifo)) {
return 0; // 满,写入失败
}
fifo->buf[fifo->head] = data;
fifo->head = (fifo->head + 1) % FIFO_SIZE;
return 1;
}
//从FIFO读取1字节(在主循环调用)
uint8_t fifo_read_byte(uart_fifo_t *fifo, uint8_t *data)
{
if (fifo_is_empty(fifo))
{
return 0; //空,读取失败
}
*data = fifo->buf[fifo->tail];
fifo->tail = (fifo->tail + 1) % FIFO_SIZE;
return 1;
}
//获取FIFO中有效数据长度
uint16_t fifo_get_len(uart_fifo_t *fifo)
{
return (fifo->head – fifo->tail + FIFO_SIZE) % FIFO_SIZE;
}
//清空FIFO
void fifo_clear(uart_fifo_t *fifo)
{
fifo->head = 0;
fifo->tail = 0;
}
/*==================== USART2中断处理 ====================*/
extern DMA_HandleTypeDef hdma_usart2_rx;
void USART2_IRQHandler(void)
{
uint32_t flag = __HAL_UART_GET_FLAG(&huart2, UART_FLAG_IDLE);
if (flag != RESET)
{
__HAL_UART_CLEAR_IDLEFLAG(&huart2);
HAL_UART_DMAStop(&huart2);
//计算DMA剩余字节数,得到本次帧长度
uint16_t temp = __HAL_DMA_GET_COUNTER(&hdma_usart2_rx);
uint16_t g_rx_len = FIFO_SIZE – temp;
g_rx_end_flag = 1;
}
HAL_UART_IRQHandler(&huart2);
}
int main(void)
{
fifo_init(&g_uart2_fifo);
__HAL_UART_ENABLE_IT(&huart2, UART_IT_IDLE);
HAL_UART_Receive_DMA(&huart2, g_uart2_fifo.buf, FIFO_SIZE);
while (1)
{
if(g_rx_end_flag)
{
…//自定义函数,从 FIFO 中提取一帧数据并处理
g_rx_end_flag = 0;
g_rx_len = 0;
HAL_UART_Receive_DMA(&huart2, g_uart2_fifo.buf, FIFO_SIZE);
}
}
}
RS232、RS422、RS485
与uart的关系
他们是通信协议栈种不同层面的概念,uart是数据链路层协议,定义了数据的帧格式,而rs232、rs485、rs422是物理层的标准,他们定义了传输uart信号的电气特性(电压、电流、连接方式等)
RS232
- RS232规定电压范围在+3v~+15v范围内的是逻辑0,范围在-3v~-15v范围内的是逻辑1,这与前面介绍的TTL范围更大,电压摆幅大,抗干扰比TTL强,传输距离更远而以达到十几米甚至几十米
- RS232本质上也是一对一通信,物理接线是txrx交叉接线,gnd相连即可,但是如果是TTL uart的设备与RS232设备连接需要RS232电平转换芯片
- RS232支持硬件流控,硬件流控是通过额外的信号线来确保通讯的正常进行,防止丢失数据,在RS232中,硬件流控常用的是RTS和CTS两根线
- RTS是输出,表示准备好可以发送了,CTS是输入,表示准备好可以接收了
- 正常时接收方把CTS置为有效,发送方检测到CTS有效则继续发送数据
- 接收方忙时接收方把CTS置为无效,发送方检测到CTS无效则停止发送
- MCU的UART外设一般也可以配置RTS和CTS,与RS232类似,只不过电平是TTL电平
RS422
- RS422是一种差分串行通信标准,主要用于全双工、点对点或少量节点的高速、长距离通信
- 差分信号为Tx+Tx-和Rx+Rx-,正负差分电压差为+2V~+6V为逻辑1,正负差分电压差为-2V~-6V为逻辑0,允许 A/B 线对地的电压在−7V~+7V之间变化
- RS422也可以用于多节点通讯,但是都是点对点为主,有两对差分线不需要收发切换,主要用于高速点对点和精密设备
RS485
- RS485是一种差分串行通信标准,专门用来解决长距离、多节点、抗干扰的问题,定义了差分信号的电气特性和多点总线拓扑,不是完整的协议栈(上层可以是自定义协议、Modbus RTU 等)
- 支持多设备挂在同一总线上,支持一主多从,可以在几十米到上千米内稳定通信,因为差分信号,在电机、变频器、继电器等干扰环境下仍能可靠传输,主要用于工业总线,多设备采集
- 这里的差分信号是用A/B线对,A-B>+200mv为逻辑1,A-B<-200mv为逻辑0,这样干扰同时进入A和B相减后大部分被抵消所以抗共模干扰能力很强,允许 A、B 两线对地的电压在-7v~+12v之间变化,因此长线、地电位差大、有电机干扰时,通信仍较稳定
- RS485是半双工通信时同一时刻只能有一个设备再发,其他都在收,所以在 MCU 端,一般用 一根 GPIO 控制 RS485 收发器的 DE/RE 引脚:DE = 1,RE = 0:发送模式(驱动器使能)DE = 0,RE = 1:接收模式(接收器使能),但是也可以时全双工4线制,使用两对差分线,一对用于发送,一对用于接收
- 在RS485总线结构中,发送完数据后要尽快切回接收状态,避免占用总线,因为在半双工模式下发送器和接收器共用一个数据通道,发送完还停留在发送模式则听不到别人的应答,而且进入缓冲区的数据也不会触发中断被处理,实际工程中用延时+查询发送完成标志或DMA发送完成中断来切回接收
-
RS485总线两端要接120Ω的终端电阻,是因为为了进行阻抗匹配,防止信号反射,当高速信号在传输线缆中传播到末端时,如果遇到不匹配的阻抗,信号能量会反射回来,与原始信号叠加,导致波形畸变,终端电阻的阻值与电缆的特性阻抗(通常为120Ω)相匹配,可以吸收这部分能量,消除反射
-
RS485还有用于失效保护的偏置电阻,当总线处于空闲状态(所有设备都不发送数据)时,总线电平不确定,容易受干扰,偏置电阻(一个上拉、一个下拉)的作用是在空闲时,强制在A、B线之间建立一个微小的正向电压差,使总线处于确定的逻辑‘1’状态,防止接收器误判
-
一个标准的RS485收发器的输入阻抗被定义为一个“单位负载”,标准规定一条总线上最多可以驱动32个单位负载,现在有很多高输入阻抗的收发器(如1/4负载、1/8负载),使用这些芯片可以大大增加总线上的节点数量,最多可达256个

