USART 全称通用同步异步收发器,日常我们最常使用的是异步通信模式,也就是常说的 UART。异步通信最核心的特点,
就是通信双方不需要共用同一根时钟信号线,只依靠 TX 发送引脚、RX 接收引脚两根数据线,再搭配 GND 共地,就能
够完成数据交互,这也是串口硬件接线简单、通用性极强的根本原因。
在嵌入式开发里,串口绝对是我们和外设沟通最基础、出场率最高的通信接口。不管是调试打印日志,还是和传感器、蓝牙模块传输数据,STM32 的 USART 异步串口永远是绕不开的一环。
很多新手最开始只会固定长度收发,等到碰到每行字节长度不固定、报文随机长短的数据时,就很容易出现丢数据、串包、接收错乱的问题。
今天我们就来实战不定长字节收发。
USART内部的秘密!
STM32 内部 USART 外设,`自带独立的发送移位寄存器、接收移位寄存器`。发送的时候,CPU 把字节写入数据寄存器,硬件就会自动把数据搬运到移位寄存器,
逐位向外输出电平,整个移位过程由硬件自主完成,CPU 不需要逐位操控引脚;接收流程正好相反,`RX 引脚不断检测电平`,一旦捕捉到起始位的下降沿,硬件
便按照波特率节拍采样电平,`把串行的高低电平拼装成完整的一字节数据,存入接收寄存器`,之后触发接收中断,通知 CPU 取走数据。
这里也是不定长数据收发难点的源头:`USART 硬件本身只认识单字节`。硬件只会一个字节一个字节地接收,它不会自动识别一整条报文什么时候结束、
整条数据包一共有多少字节。硬件没有 “数据包” 的概念,只有源源不断的字节流。
一定要学会变通
想要搞定不定字节的收发,本质就是我们依靠中断、空闲帧检测、DMA 这些外设功能,在连续不断的字节流里,人为分辨出一帧完整数据包的边界。
弄懂 USART 硬件底层的收发逻辑、帧结构、硬件寄存器的工作逻辑之后,我们再去对比空闲中断、DMA 接收、定时器超时这几种主流不定长接收方案,
就很容易理解每种方式的优劣和适用场景。
常见的方案
| 常见方案 | 缺点 | 优势 |
|---|---|---|
| 普通接收中断 + 环形缓冲区缓存 | CPU 会被频繁打断,波特率越高,中断开销越严重,极易出现来不及进中断、字节丢失 | 易于理解 |
| USART 空闲帧中断 | 两条报文间隔太短、字节之间没有空闲电平,就会发生两帧数据粘连; | 硬件自带帧结束检测,不需要软件不停检索结束符 |
| DMA 搬运 + 空闲中断 | 可忽略 | 高性能首选方案 |
我们的选择
当然选择DMA 搬运 + 空闲中断啦!不仅在HAL代码简洁 并且 在CubeMX中很简单就可配置
立刻进入实战!!!
CubeMX的配置
开启串口DMA

开启串口中断

HAL库代码
重定fputc函数 方便打印数据
int fputc(int ch, FILE *f)
{
HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, 0xFFFF);
return ch;
}
必要头文件
#include <stdio.h>
#include <string.h>
初始化开启DMA中断
#define RX_BUFFER_SIZE 1024 //缓冲区大小
uint8_t rx_buffer[RX_BUFFER_SIZE];//接收缓冲区
uint8_t rx_done = 0; // 接收完成标志
uint16_t rx_len = 0; // 本次接收到的数据长度
//DMA中断
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
中断回调函数
// 空闲中断回调
void HAL_UARTEx_RxEventCallback(UART_HandleTypeDef *huart, uint16_t Size)
{
if (huart->Instance == USART1)
{
rx_len = Size;
rx_done = 1;
/*
进入中断时,这里也可以运行其他的代码 看个人需求
*/
//重启DMA中断
HAL_UARTEx_ReceiveToIdle_DMA(&huart1, rx_buffer, RX_BUFFER_SIZE);
}
}
while中运行的代码
//测试收发(PC13上LED的开关)
if(rx_done) //判断是否有接收到数据
{
rx_done = 0; //清除标志位
printf("Rx Len: %d\r\n", rx_len); // 打印实际长度
//比较函数
if(memcmp(rx_buffer, "ON", 2)==0)
{
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_RESET);
printf("OLED ON");
}
else if(memcmp(rx_buffer, "OFF", 3)==0)
{
HAL_GPIO_WritePin(GPIOC,GPIO_PIN_13,GPIO_PIN_SET);
printf("OLED OFF");
}
else
{
printf("NO AT");
}
}
啦啦啦啦啦 到这里就可以随心所欲的收发数据啦 嘻嘻嘻!!!
有缘再会,这里是小祁,拜拜!
浙公网安备 33010602011771号