基于STM32F411系列_USATR外设知识总结
一.常用的串口通讯
1.串口通信核心分类(同步/异步)
注:日常单片机 TTL、RS232、RS485 串口,全部默认是异步通信。
2.波特率核心原理
- 定义:波特率表示1秒钟发送的二进制bit位数,作用是让收发双方数据严格对应、精准解析。
- 异步串口帧结构:单片机发送 1 字节有效数据时,必须额外携带1位起始位 + 1位停止位,单次传输实际占用 10 bit。
- 举例计算(115200 波特率):
每秒字节数 = 115200 ÷ 10 = 11520 byte/s
3.TTL 串口电平与特性(单片机原生电平)
- 单片机硬件适配:常规 3.3V 单片机硬件限制,实际以 2.4V ~ 3.3V 判定为高电平逻辑1。
- 硬件本质:单片机直接输出的串口电平,默认就是 TTL 电平。
- 通信帧规则:和所有异步串口一致,发送1字节数据需拼接起始位、停止位,单次传输10个bit。
- 优缺点:电平电压范围极小,极易受外界电磁干扰,传输距离极短,常规有效传输距离 1m 以内。
4.RS232 串口电平与特性
- 硬件要求:单片机只能输出 TTL 电平,无法直接输出 RS232 电平,必须外接 MAX232 电平转换芯片完成电压和逻辑翻转。
- 硬件接线:标准三线制通信 TX / RX / GND。
- 通信模式:和 TTL 串口一致,属于异步单端通信,依赖公共 GND 做参考。
- 优缺点:相比 TTL 电平电压范围宽,抗干扰能力大幅提升,标准最大传输距离可达 15m。
5.RS485 差分通信原理与特性
- 传输方式:采用差分平衡传输,依靠 A、B 两根信号线的电压差值判断逻辑,不依赖单一 GND 参考,彻底解决地电位偏移问题 但只能半双工通讯。
- 共模干扰抵抗核心机制:工业环境中的外界干扰噪声,会同时叠加在 A、B 两根信号线上,形成共模电压;RS485 电路只识别 A、B 之间的电压差值,会自动抵消两路相同的干扰电压,完美过滤共模干扰,适配强干扰工业场景。
- 逻辑判定标准:
- 驱动器输出(实际测量电压):Va-Vb = +2V ~ +6V → 逻辑 1
- 驱动器输出(实际测量电压):Va-Vb = -6V ~ -2V → 逻辑 0
- 补充关键考点(必考):接收端识别阈值更宽松,只要差分电压绝对值 ≥ 200mV 就能正确识别逻辑,这也是485抗干扰、远距离传输的容错余量来源。
- 核心优势:抗干扰能力极强、传输距离远,最大有效传输距离可达 1200m;支持一主多从总线架构,可实现多设备组网通信但是连接单片机需要485转换芯片。
- 传输速度大概能达到10Mbps 10 000000 bit/s 即每秒能传送1M字节的数据随距离增加。
6.三种串口核心总结
- TTL:单片机原生电平、距离短、易受干扰、板内短距离调试使用
- RS232:电平翻转、电压范围大、抗干扰更强、点对点短距离外接设备通信(15m)
- RS485:差分传输、抗共模干扰、远距离、支持多机组网,工业通信首选
二.串口通讯硬件基本原理
三.串口通讯标准库学习
- 先开 USART1 和 GPIOA 的时钟
- 再把 PA9/PA10 配成 AF
- GPIO_PinAFConfig() 一定用 GPIO_PinSource9/10
- 然后 USART_Init()
- 最后 USART_Cmd(ENABLE)
Q:void USART_SendData(USART_TypeDef* USARTx, uint16_t Data) 传入的数据是uint16,但是我怎么可以写入uint8照样可以发出去?数据发送寄存器和接收寄存器公用一个DR寄存器吗
A:uint16_t Data 不是说你必须发 16 位,而是因为这个 API 要兼容不同的数据长度配置,尤其是 9-bit 模式。函数内部实际上做了掩码:
四.串口通讯HAL库学习
1、串口通讯常用的函数,基于USART_IT中断
HAL_UART_Receive_IT(&huart1,buffer_uart_data,5);
由于是定长中断触发,所以HAL_UART_Receive_IT函数对应的5字节触发回调函数
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
HAL 库的中断接收HAL_UART_Receive_IT是一次性的,因此在中断函数中还要进行下次使能。
2.串口基于DMA中断接收数据
使用DMA需要注意所设置的数据宽度是多少,如果是字,那么接收一个字节就会放进4byte的内存中 最低位。
因为 DMA 接收完成后,最终也会调用同一个回调函数:HAL_UART_RxCpltCallback
完成后,进的都是同一个回调:HAL_UART_RxCpltCallback
HAL针对于USART单独封装了一个DMA的APIHAL_UART_Receive_DMA
在UART_Start_Receive_DMA函数中完成了DMA函数指针的绑定(DMA中断的注册)
3.CPU的SRAM的搬运速度和AHB总线速度相关
AHB = 16 MHz → 一次搬运循环(50 次)耗时 33 us
AHB = 100 MHz → 一次搬运循环(50 次)耗时 4 us
你写的 (uint32_t)addr = data 是 AHB 上的读写操作
中断一(DMA 半满中断),中断二(DMA 全满中断),中断三(串口空闲中断)之间的关系:
当使用 HAL_UARTEx_ReceiveToIdle_DMA 函数时,假设参数三 Size 是 32 字节(半满 16 字节)。
当发送 17 个字节时,会先进入中断一(DMA 半满中断),然后再进入中断三(串口空闲中断)。
当发送 15 个字节时,会直接进入中断三(串口空闲中断)。
当发送 33 个字节时,会先进入中断一(DMA 半满中断),然后进入中断二(DMA 全满中断),然后进入中断三(串口空闲中断)。并且,由于是 33 个字节,如果这个时候 DMA 的配置为 DMA_NORMAL 模式,则第 33 个字节会被丢掉。如果,DMA 被配置为 DMA_CIRCULAR,则第 33 个字节会覆盖最开始的第一个字节。
3.1半满全满中断介绍
HAL_UART_Receive_DMA(&huart1, recv_data, 20);
NDTR 会被配置为20,每当写入一个数据NDTR就会-1;但是要注意这里是写入一个数据,而不是接收一个数据。
如果打开了FIFO,Threshold设置为Full 那么由于DMA FIFO深度是4*word =4*4字节=16字节
那么就会接收数据一直存在FIFO里,且Threshold配置为FULL,那当FIFO满了会触发 突发传输 一次性把16字节数据全写进SRAM。
那么此时NDTR的值直接从20变为了20-16=4;直接触发半满中断,再发4个字节数据触发全满中断。
如果设置为Threshold=Full,DMA FIFO满了,UART 内部 RX FIFO 满了,外部还在发数据,并且此时CPU在访问DMA的目的地-SRAM导致DMA无法写入,那么此时会丢UART数据。
所以Threshold=Full 会让 “抗阻塞余量” 变小,高速 / 高负载下建议用 1/2 或 1/4 阈值更安全。
开 FIFO 后是批量搬运,NDTR 是跳变,但 HT 逻辑仍然按 “累计量过半” 来算
void DMA2_Stream2_IRQHandler(void)
{
/* USER CODE BEGIN DMA2_Stream2_IRQn 0 */
/* USER CODE END DMA2_Stream2_IRQn 0 */
HAL_DMA_IRQHandler(&hdma_usart1_rx);
/* USER CODE BEGIN DMA2_Stream2_IRQn 1 */
/* USER CODE END DMA2_Stream2_IRQn 1 */
}
- HT 中断(Half Transfer)半传输→ 你设置 20 字节 → 收到 ≥10 字节就进
- TC 中断(Transfer Complete)传输完成→ 20 字节收完 → 进
- TE 中断(Transfer Error)传输错误
- FE 中断(FIFO Error)FIFO 上溢 / 下溢
- DME 中断(Direct Mode Error)
3.2全满半满中断函数
void HAL_UART_RxHalfCpltCallback(UART_HandleTypeDef *huart)
{
if(&huart1 == huart)
{
printf("half_interrupt\r\n");
}
}
void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart)
{
if(&huart1 == huart)
{
HAL_UART_Receive_DMA(&huart1, recv_data, 20);
printf("full_interrupt\r\n");
}
}
3.3Burst Size 配置介绍
如果DMA正在向SRAM写入数据,而此时CPU需要访问SRAM,那么AHB总线仲裁就会让DMA停止写入,转而给CPU权限。如果DMA写入的数据正是CPU要拿的数据,那么会导致出现问题。因此只有两种方式防止DMA被打断:1、互斥锁 2、Burst
USART的Burst Size 意味着每接受一个字节触发多少次的DMA请求。
memory的Burst Size意味着每次触发多少次突发传输。并且此突发传输不会被高优先级的任务打断。即:将AHB总线锁死 只会锁死当前正在使用的那一条 AHB 总线。
对于USART 接收一个字节就触发一次DMA请求,如Buest Size 设置为4 那么就会连续触发4次的DMA请求,发生如下情况
USART接收到 0x12
此时DMA的FIFO里存入 0x12 0x12 0x12 0x12 (连着存四个)
---所以一般设置USART的buestSize =single
Burst Size 的设置和FIFO的阈值字节数以及数据宽度有关(根据Threshold 所配置的)
FIFO 阈值字节数 = N × (BURST × SIZE),N=1,2,3,4;
就是每当FIFO满了就开始写入,一次写入MSIZE = 1B (N) 就是4;
3.4接收不定长数据-串口空闲中断介绍
HAL_UART_Receive_DMA() 是标准定长 DMA 接收 API,工作模式标记为 HAL_UART_RECEPTION_STANDARD;
而 HAL_UARTEx_RxEventCallback 是扩展事件回调,仅当接收模式为 HAL_UART_RECEPTION_TOIDLE(空闲帧模式)时才会被 HAL 调度执行,二者底层完全隔离,天然不会联动。
1.HAL_UART_Receive_DMA(标准 DMA 接收)
仅开启 DMA TC(传输完成)、HT(半满)中断;不会开启串口 IDLE 空闲中断;
huart->ReceptionType = HAL_UART_RECEPTION_STANDARD;
DMA 收满Size字节(TC 全满) → 进入 HAL_UART_RxCpltCallback
DMA 收到一半数据(HT 半满) → 进入 HAL_UART_RxHalfCpltCallback
完全不涉及任何 IDLE 空闲事件,HAL 中断服务函数里不会调用 RxEventCallback。
2. HAL_UARTEx_ReceiveToIdle_DMA(空闲帧 DMA 接收,你要的版本)
自动同时开启 DMA HT/TC 中断 + USART IDLE 空闲中断;
huart->ReceptionType = HAL_UART_RECEPTION_TOIDLE;
总线空闲 IDLE / DMA 半满 HT / DMA 填满 TC,全部统一进入 HAL_UARTEx_RxEventCallback;
在回调内可用 HAL_UARTEx_GetRxEventType(huart) 判断是哪种事件。
3.在HAL_UART_IRQHandler中会根据具体情况进行中断事件分发
1.IDLE 空闲中断的触发前提必须总线出现至少 1 个字符时长的空闲电平,硬件才会置 IDLE 标志、触发中断; 当多帧数据首尾紧挨着、帧之间无任何间隔,整条数据流是连续电平,IDLE 标志永远不会置位。
2.此时的尴尬局面 DMA 缓冲区还没收满预设Size,不会触发 TC 全满中断;又没有空闲,IDLE 中断也不进;接收缓存持续不断写入新数据,旧数据没有机会被业务代码读取解析,最终 DMA 循环缓存覆盖旧帧、普通 DMA 缓存直接溢出丢包。
3.HT 半满中断的补偿作用DMA 接收达到缓冲区一半长度时强制触发中断,提前把前半段数据取走解析,提前释放缓存空间,不让缓存被持续填满积压。
防止串口通信中数据丢失或错误,我通常从以下几个维度出发:一是使用 DMA 结合空闲中断和环形缓冲区机制,减少 CPU 干预同时避免缓冲溢出;二是启用硬件或软件校验机制如 CRC、奇偶校验,确保数据正确性;三是从协议层设计鲁棒的帧结构、加上超时与重传机制防止数据错乱;四是提高串口中断优先级并将解包逻辑下沉至后台任务;五是硬件层面选用 RS-485 和滤波抗干扰设计。实际项目中根据使用场景做不同策略组合,以保证串口通信可靠。
五.串口环形缓冲区学习
环形缓冲区设计千变万化,但是都应该有下面几种能力设计出稳定通用的中间件。
1.面向对象的程序设计能力
六.需要注意的点
6.1 uint16_t cmd 和 uint8_t cmd[2] 的区别
1.存储结构
uint16_t cmd:连续 2 字节,代表一个 16 位整数,有高低字节区分(大小端由芯片内核决定,Cortex-M 默认小端)
uint8_t cmd[2]:两个独立 8 字节变量,仅单纯两块内存,无 “16 位数值” 语义
2.操作语义完全不同
uint16_t a = 0x1234;
uint8_t b[2] = {0x34,0x12}; // 小端下内存布局和a一致,但类型含义不一样
a = 0x5678; // 直接整体赋值16位数
b[0] = 0x78; b[1] = 0x56; // 必须分别操作字节
6.2memcpy(&data_struct.sw_ver,&temp[2],2);
假设 data_struct.sw_ver 是 uint16_t 类型:
((uint8_t*)&data_struct.sw_ver)[0] = temp[2];
((uint8_t*)&data_struct.sw_ver)[1] = temp[3];
6.3关键坑:大小端问题
更多推荐


















所有评论(0)