STM32 串口与 DMA 进阶:如何打造高实时性交互体验

STM32 串口与 DMA 进阶:如何打造高实时性交互体验

从最基础的点亮一颗 LED,到让单片机能够通过串口流畅地与用户的电脑控制台实现类似于“请求-执行-反馈”的交互(比如你输入数字 1 点亮灯,输入 4 开启 ADC 连续采样),这中间跨越的是外设协同编程思维的鸿沟。

本文从串口收发的陷阱讲起,并逐层递进,推演出一套最适用于具备严苛时序要求的高实时性交互框架:IDLE 断帧 + DMA


一、printf 移植:底层 fputc 那些事儿

要在项目中使用 printf 输出日志菜单,就必须打通 C 库底层的发送通道。C 语言默认把字符串往显存终端发,而在单片机这儿我们只能重定向挂接去某个 USART 外设。

// 重定向 fputc
int fputc(int ch, FILE *f) {
    USART_SendData(USART1, (uint8_t)ch);
    // 这里非常重要:你一定要确保它传过去了!
    while (USART_GetFlagStatus(USART1, USART_FLAG_TXE) == RESET);
    return ch;
}

警惕 USART_FLAG_TC vs USART_FLAG_TXE:

  • TXE (Transmit Data Register Empty): 数据已从暂存器移到了发送移位寄存器里去组装并排着队送走。检查它最快。但数据可能还在排队,并未全发去硬件引脚上。
  • TC (Transmission Complete): 这里表示真的没活儿干了。如果你打算在发完之后立马关掉设备的供电睡眠,等 TC 是最安全的保障。

致命的初始化依赖 Bug

有的新手刚配好头文件就在 main 开头打印 printf("hello world"),随后才在下面去调用自己写的 UART_Init()。这不仅什么也看不见,更有可能使程序陷入死等的 TXE 的底层状态而再也无法往下运行!


二、接收数据战争:轮询 VS 逐字中断

对于接收菜单这种“单字节命令”,我们总会去选择更省力的形式。

1. 简陋的轮询(Polling)

这是让你的 CPU 在无尽的 while(1) 里面跑断腿:

while (1) {
    if (__HAL_UART_GET_FLAG(&huart1, UART_FLAG_RXNE) != RESET) {
        char data = UART1->DR;
        // 执行对应任务...
    }
}

场景比喻:每隔 5 秒去楼下跑一趟包裹柜。 致命缺陷:如果你的主干循环里还有耗时操作(HAL_Delay),你必然会跑丢数据。更灾难的是:如果你还需要 CPU 时刻计算和发送脉冲控制像 WS2812 这样有严苛时序要求的灯带,这种抽帧轮询做法直接让系统瘫痪!

2. 传统串口逐字节中断(RXNE IT)

为了对抗 CPU 的空余浪费,利用外设中断管线让 CPU 接收通知。

void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) {
    if (rxBuffer == '4') {
        Start_ADC_DMA(); 
    }
    // 重新开启下一次中断接收待命
    HAL_UART_Receive_IT(&huart1, &rxBuffer, 1);
}

场景比喻:快递员来了专门给你打电话,你暂停手头去办。 这套流程完全能够搞定单字符的菜单。但当你想发送一个不确定长度的长字符串命令时,让整个 CPU 处于几毫秒内反复触发并停止响应的“被打断”状态也会极度消耗主频的带宽。


三、终极答案:IDLE + DMA 的自动流水线

这是业界真正处理高并发字符串通讯最青睐的解决方案!

什么是 IDLE + DMA?

它相当于一个拥有智能封包打包器的高级仓库通道:

  1. 纯净背景搬砖:你只需提前找一块大数组 RxBuffer。告诉 DMA 大将:“串口收到什么字节你别管,你直接按地址一个个怼进这块内存里面去,别来烦我(CPU)。”
  2. 硬件断帧感知:当外面的电脑用 115200 发送完长长的 Hello STM32! 封包时。串口必然会随之处于没有任何波形的“空闲停顿”。这就瞬间激活了串口的 IDLE(总线空闲)状态。
  3. 只惊动一次 CPU:当这句整包传输完成后。外围触发一次唯一的高昂的中断 USART_IT_IDLE。你这才醒过来去处理 DMA 在后台拼好的成果。

必须掌握的核心流程

中断回调里的逻辑至关重要,你还要重新重装 DMA 的倒数计数器来开启下一轮游戏:

// 1. 读 SR 再读 DR 以此来暴力清除 IDLE 通知
(void)USART3->SR;
(void)USART3->DR;

// 2. 关停 DMA 传送器(为了安全修改内部地址等计数器)
DMA_Cmd(DMA1_Channel3, DISABLE);

// 3. 把总剩余容量进行相减,得到具体接收了啥
USART3_RxCount = USART3_RX_BUFFER_SIZE - DMA_GetCurrDataCounter(DMA1_Channel3);

// 4. 重装计数准备下次
DMA_SetCurrDataCounter(DMA1_Channel3, USART3_RX_BUFFER_SIZE);
DMA_Cmd(DMA1_Channel3, ENABLE);

如此处理,哪怕是你随意发送多长、多大的交互请求。这套基于硬件级别接力的设计永远不会令你在接收命令的瞬间,看到你的 LED 发生一丝意外的卡顿!

Logo

© 2026 Shane

Twitter Github RSS