资讯动态

STM32串口不定长接收:DMA+空闲中断方案详解

发布时间:2026/10/7 1:29:50 来源:尧图企业网站定制
直接说结论这套 DMA 接收 空闲中断的组合是我在 STM32F103 上做串口通信时用过的组合里性价比最高、代码量最少、稳定性也最靠谱的一套。串口是这个平台上最常见的通信方式但“不定长数据怎么收”一直是个绕不开的坎。你用定长帧、用逐字节中断、用定时器辅助判超时各有一套代价。这篇文章我把这套方案的选型逻辑、原理细节、CubeMX 配置、核心代码、以及我实际调试中踩过的坑一次讲清楚适合刚做完点灯想进阶的初学者也适合正在为串口收发不稳定挠头的老哥。你不需要多高深的功底跟着步骤把工程跑起来再回头理解机制会比单纯抄代码扎实得多。现象大家都遇到过设备明明正常发数据上位机收到的字符串却偶尔缺头少尾下位机这边一帧数据长度是变的用固定长度的接收数组根本没法拆帧中断接收一旦数据密集CPU 忙着进进出出别的事全被挤到一边。STM32F103 的 USART 配合 DMA 控制器再加上 USART 硬件自带的空闲中断正好能把这三件事同时解决——搬运数据不占 CPU帧边界由硬件判断长度任意不用自己数。这套思路做产品也是主流做法不是我拍脑袋想的花活。我这篇不会只丢给你代码我会把“为什么这么配”“为什么这里要这么处理”都讲明白。先解释方案为什么优于传统做法再拆解 IDLE 和 DMA 的原理然后走一遍 CubeMX 配置流程最后给出接收发送的核心代码和排查实录。你照着操作两小时内让板子跑起来你理解了机制以后换芯片、换库、换协议都能自己改不用到处求参考。1. 方案选型为什么非要用 DMA 空闲中断1.1 逐字节中断接收的代价传统的串口接收最直接的做法是开启接收中断每收到一个字节就在中断里把数据存进数组。代码长这样// 伪代码示意 void USART1_IRQHandler(void) { if (USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { rx_buf[rx_cnt] USART_ReceiveData(USART1); } }看着很顺但你要面对三个现实问题。第一CPU 被频繁打断。波特率 115200 时一个字节大约 87 微秒意味着每秒有 11520 次中断。每次中断要压栈、跳转、读数据、写数组、出栈CPU 大量时间耗在中断进出上。如果你的主循环还有显示屏刷新、传感器采集、PID 运算这类任务数据一密定时就不准。第二字节间隙没法判断。中断只能告诉你“收到一个字节”不能告诉你“这一帧结束了”。一帧 30 字节的数据可能分几次到达你怎么知道攒够了没只能定时器插一脚超过 N 毫秒没收到字节就当帧结束。这个 N 值写大了延时写小了拆帧调试起来很闹心。第三高波特率低负载时容易丢。我实测过 460800 波特率下如果主循环忙着一件事超过一个字节时间RXNE 标志又没及时读走新数据直接覆盖旧数据产生 ORE 溢出错误后面的帧全乱套。1.2 定长接收方案的致命缺陷有人会说那我干脆让协议层固定长度每帧 16 字节收满 16 字节处理一次。这个方法在纯内网、私有协议的场景下勉强能跑但一接触实际业务就暴露问题。拿最常见的 Modbus-RTU 举例它的帧长度取决于功能码和数据域数量从 8 字节到 253 字节不等怎么定还有 GPS 模块的 NMEA 语句每条语句长度几十到上百字符不等而且靠逗号分隔定长根本没法套。更麻烦的是粘包现象发送方把两帧数据连续发出来接收方如果只按定长切可能前半段是 A 帧的尾巴、后半段是 B 帧的头拼出来的全是乱码。定长方案的正确用法是“帧头 长度域 数据 校验”这种结构但这属于协议设计层面的事了和“底层怎么把数据收全”是两码事。底层收全了不定长数据再由协议层解析才是正路。1.3 DMA 空闲中断的组合优势DMA 在这套方案里扮演的角色就是“不需要 CPU 干预的搬运工”。它可以把 USART 接收到的数据自动搬进内存缓冲区搬完一批才打断 CPU 一次。而 USART 的空闲中断是硬件层面检测“总线上超过一个字节时间没有新数据”的状态这时候就能断定一帧数据接收完毕。我把常见方案对比了一下你感受下差别方案CPU 占用帧边界判断实现复杂度适用场景逐字节中断极高需定时器辅助低数据量很小定时器 逐字节超时判断较高可判断中低速、帧间隔稳定DMA 接收 空闲中断极低硬件判断中通用性强主流首选环形缓冲 逐字节中断较高需协议层解析高极低延迟响应表格里能看出DMA 空闲中断在绝大多数场景下是综合最优解。CPU 占用低帧边界是硬件给的不需要猜超时时间代码结构也清爽。1.4 这套方案的边界我也得泼点冷水不是所有项目都适合硬套这套方案。如果你的应用需要“每收到一个字节立刻响应”比如模拟 UART、IR 解码这类对单字节时序极其敏感的场景DMA 会把数据攒在缓冲区里实时性反而不如逐字节中断。如果你的 RAM 非常紧张比如一些老款 51 内核的国产芯片腾不出一个 128 字节的 DMA 缓冲区做连续搬运那也得斟酌。不过 STM32F103C8T6 这类芯片自带 20KB RAM分配 1KB 做串口缓冲完全无压力它不在这类约束内。2. 原理拆解IDLE 信号与 DMA 搬运机制2.1 空闲中断的本质很多新手把空闲中断理解成“帧结束中断”这个说法容易误导。它的准确含义是USART 接收线在经历了一段活动之后检测到连续一个字节时间的空闲电平。换句话说就是总线上“超过一个字节时间没有任何新数据进来”硬件就置位 IDLE 标志。一个字节时间怎么算串口协议中一个字节通常是 10 个位起始位 1 数据位 8 停止位 1波特率 9600 时一个位时间是 104 微秒一个字节就是约 1.04 毫秒。波特率 115200 时一个字节约 86.8 微秒。这个空闲时间阈值由硬件自动判断根本不需要你写定时器去量。关键点来了IDLE 标志位产生后不会自动清除它需要你先读一次 USART 状态寄存器再读一次数据寄存器硬件才会把 IDLE 标志清零。很多教程只提“进入空闲中断”没提怎么退出中断我见过好几个人栽在这里进了中断就出不来程序直接卡死。2.2 为什么空闲中断适合不定长接收从原理上理解如果发送方一次发送一帧数据帧内字节与字节之间有固定间隔这个间隔远小于一个字节时间而帧与帧之间发送方通常会停顿几十微秒甚至几毫秒。这个“间隔大于一个字节时间”的窗口就是硬件的空闲检测窗口。所以硬件能准确区分“帧中字节”和“帧间间隙”一旦检测到后者就会产生中断告诉你从上次处理到现在总线上已收完一整帧数据。至于这一帧有几个字节你不用预先知道只要去 DMA 缓冲区里数一下新增了多少数据就行。这就是“不定长接收”的底层逻辑长度不是靠协议定义的而是靠总线活动状态划出来的。这也是为什么 NMEA 那种变长、变内容、靠换行符结尾的协议用这套方案能轻松接住。2.3 DMA 在串口接收中的角色DMA 在这里干的事情是把 USART 数据寄存器里的字节自动搬运到你指定的内存数组。它不需要 CPU 逐字节处理只需要你告诉它三个要素从哪搬外设寄存器地址、搬到哪数组首地址、搬多少个数组长度。这里有个常见误区DMA 的“搬多少个”是指它从启动到停止一共搬多少字节而不是“总共能存多少字节”。如果你用非循环模式搬完设定的 N 个字节后 DMA 就停了后面再来数据没人管串口数据直接丢失。所以接收 DMA 要用循环模式数据搬完一圈后自动从头开始继续搬这样它才能作为长期运行的“数据仓库”。2.4 半传输中断与环形缓冲的关系提到 DMA 循环接收很多人会接着问是不是还要配合半传输中断 DMA 可以设定在搬运到缓冲区一半时产生中断这叫半传输中断。在一些高速率、大数据量、双缓冲切换的方案里半传输中断很关键因为它可以把缓冲区切成两半CPU 处理前半段时DMA 往后半段搬互不干扰。但在我们这套“空闲中断 DMA 循环”的组合里半传输中断不是必须的。空闲中断已经告诉我们“一帧结束了”此时需要处理的是从上次断点到当前 DMA 写指针之间新增的数据。只要你能正确读取 DMA 当前写到哪了就能准确切出帧数据不需要半传输来辅助。半传输的方案适合“不知道什么时候来数据、来了还不能打断”的高速流式场景和我们的“不定长帧接收”场景目标不同。3. CubeMX 工程搭建与关键参数配置3.1 引脚与时钟先说时钟这是很多人配置完 USART 波特率偏差大的根源。STM32F103 的 USART1 挂在 APB2 总线上时钟最高 72MHzUSART2、USART3 挂在 APB1 总线上时钟最高 36MHz。CubeMX 生成工程时如果外部晶振没配对、PLL 倍频没设置对APB2 分频系数不同最后算出来的波特率就有偏差高波特率下直接乱码。我建议在 CubeMX 的时钟树页面里先把 System Clock Mux 调到 PLLCLK再把 HCLK 拉到 72MHz观察左边的 PCLK1 是 36MHz、PCLK2 是 72MHz确认无误后再去配置串口。引脚方面默认 USART1_TX 是 PA9USART1_RX 是 PA10。如果你打算同时测试使用另一组串口USART2 是 PA2/PA3。做实验的时候用一个 USB 转 TTL 模块把 TX 接 PA10、RX 接 PA9、GND 接板子 GND。交叉连接是必须的很多第一次动手的人把 TX 接 TX结果死活没反应。3.2 USART 参数配置CubeMX 界面上USART1 的 Parameter Settings 里波特率我一般设 1152008 数据位无校验1 停止位。这是最通用的配置上位机、USB 转 TTL、各类传感器模块默认都是这个参数。有些模块硬件上用了奇偶校验那你在 CubeMX 里把 Parity 设为 Even此时数据位要改成 9。注意数据位和校验位是联动的9 位数据里含 1 位校验有效数据还是 8 位。这个细节在 DMA 接收时影响不大因为 DMA 搬的是整个寄存器内容但在你解析数据时要知道软件里收到的是 9 位打包数据需要自己把校验位跳过。3.3 DMA 配置细节在 CubeMX 的 DMA Settings 页面需要添加两个 DMA 请求一个是 USART1_RX一个是 USART1_TX。RX 通道方向选 Peripheral To Memory模式选 Circular 循环模式数据宽度都选 Byte。传输增量里外设地址不增量因为串口数据寄存器是固定的内存地址增量因为要依次填数组。优先级默认 Medium 足够除非你的系统里同时跑着高级 DMA 任务。TX 通道方向选 Memory To Peripheral模式选 Normal 正常模式。发送是“一次性任务”发出 N 个字节就结束发完不需要循环。数据宽度同样 Byte传输方向记得反过来内存地址增量外设地址不增量。有个细节容易被忽略DMA 的 Buffer Size 这里不用填CubeMX 生成的初始化代码里HAL_UART_Transmit_DMA 和 HAL_UART_Receive_DMA 会在调用时动态设置大小。你在 DMA 配置界面里写的 Number 和实际调用时给的值要一致不然可能产生意外截断。我一般在这里让它默认然后在代码里控制。3.4 NVIC 中断优先级配置接下来配置 NVIC。在 System Core 菜单的 NVIC 页面把 USART1 global interrupt 和 DMA1 channel 中断都打开。优先级怎么设我的习惯是串口中断抢占优先级设 1DMA 中断设 2。如果系统里还有定时器中断让定时器抢占优先级为 0最高。这样做的理由是定时器是时间基准不能被打断串口空闲中断负责帧边界判断也比较重要DMA 的传输完成中断相对没那么紧急放在后面没问题。如果你后面接 FreeRTOS还要特别注意FreeRTOS 要求把中断优先级分组设成 NVIC_PriorityGroup_4全抢占式并且所有中断抢占优先级不能高于 5否则可能破坏临界区的互斥保护。这套方案在裸机下完全没问题但如果你计划跑 RTOS最好现在就按它的规则配置免得后面改起来麻烦。3.5 生成代码后的初始化检查生成工程后打开 main.c检查 MX_USART1_UART_Init 是否先调用再检查 MX_DMA_Init最后看 MX_GPIO_Init。顺序错了会导致外设初始化异常不过 CubeMX 生成的顺序一般没问题。另一个要检查的是串口波特率是否和时钟匹配。CubeMX 生成的初始化代码里 MX_USART1_UART_Init 会调用 HAL_UART_Init它会根据你配置的 UART_InitStruct 计算 USARTDIV。如果你改了时钟树后没重新生成代码波特率表还是按旧时钟算的就会出现偏差。4. 核心代码实现接收框架与发送细节4.1 接收缓冲区的数据结构我用一个全局数组作为接收 DMA 的缓冲区再加一个变量记录“上次处理到的位置”。#define RX_BUF_SIZE 256 uint8_t g_usart1_rxBuf[RX_BUF_SIZE]; uint16_t g_usart1_lastPos 0; // 上次处理时 DMA 写到了哪个位置 volatile uint8_t g_usart1_frameFlag 0; // 帧接收完成标志 uint16_t g_usart1_frameLen 0; // 本次帧数据长度RX_BUF_SIZE 取 256 是因为 STM32F103 的 DMA 计数器是 16 位最大支持 65535但一般 256 字节足够缓存好几帧数据而且方便做取模运算。缓冲区大小至少要大于最大帧长度这个自己评估。你还要想清楚数组大小是 2 的幂。虽然 DMA 计数器不是直接按“2 的整数幂”取模来用但在我们的指针计算场景里2 的幂能简化运算吗其实这里用普通取模就行。不过数组对齐和取模计算在下一节会展开先看流程。4.2 空闲中断处理流程在 HAL 库中USART 的全局中断入口是USART1_IRQHandler它内部已经判断了各种标志并调用回调函数。但空闲中断比较特殊不能完全依赖默认处理需要你在中断里手动判断和清标志。一个成熟的做法是重写HAL_UART_IRQHandler之后的处理或者更简单地直接在USART1_IRQHandler里增加 IDLE 判断void USART1_IRQHandler(void) { uint32_t isrflags READ_REG(huart1.Instance-SR); if (isrflags USART_SR_IDLE) { // 清除 IDLE 标志先读 SR再读 DR READ_REG(huart1.Instance-SR); READ_REG(huart1.Instance-DR); // 计算当前 DMA 写到了缓冲区哪个位置 uint16_t currentPos RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart1_rx); // 得到上次处理位置到当前位置的这段新数据长度 if (currentPos g_usart1_lastPos) { g_usart1_frameLen currentPos - g_usart1_lastPos; } else { g_usart1_frameLen RX_BUF_SIZE - g_usart1_lastPos currentPos; } g_usart1_lastPos currentPos; g_usart1_frameFlag 1; } HAL_UART_IRQHandler(huart1); }这代码里有几个点必须解释。为什么currentPos RX_BUF_SIZE - counter因为 DMA 的计数器 COUNT 表示当前还剩多少字节没搬完。假如缓冲区 256 字节DMA 启动时 COUNT256搬了 10 字节后 COUNT246那么 DMA 当前写到的位置就是 256-24610即第 10 个字节后面。这个计算思路直接决定了你能不能准确定位帧数据。为什么清标志要读两次寄存器这是参考手册明确写的IDLE 标志的清除条件是“先读 SR、再读 DR”。有些芯片勘误表中要求读 SR 后延时几个周期再读 DR但标准库和 HAL 库里的READ_REG宏足够满足时序要求。为什么用 frameFlag 而不直接在中断里解析中断里做协议解析是大忌会拉长中断时间。正确做法是中断里只置标志、记长度主循环轮询到标志后处理数据。4.3 主循环里的帧处理主循环中检测到 frameFlag 置位后就可以把缓冲区里的有效数据提出来了。while (1) { if (g_usart1_frameFlag) { g_usart1_frameFlag 0; ProcessFrame(g_usart1_rxBuf, g_usart1_lastPos, g_usart1_frameLen); } }这里ProcessFrame的参数我传了上次位置、当前帧长度一个是便于从缓冲区里取数据一个是告诉你帧尺寸。要注意的是如果缓冲区是循环的数据可能被分成两段——一段在缓冲区尾部一段在缓冲区头部。比如上一次处理位置是 250当前 DMA 位置是 10帧长度就是 16 字节但数据实际是buf[250]...buf[255]加buf[0]...buf[9]组成的。处理这种回绕问题时需要先把后半段拷到临时数组或者用分段解析。void ProcessFrame(uint8_t *buf, uint16_t start, uint16_t len) { uint8_t tmp[128]; uint16_t i; // 处理环形回绕 for (i 0; i len; i) { tmp[i] buf[(start - len i RX_BUF_SIZE) % RX_BUF_SIZE]; } // 到这里 tmp 就是完整的一帧数据长度 len ParseCommand(tmp, len); }上面求(start - len i RX_BUF_SIZE) % RX_BUF_SIZE的意义是从start - len这个位置开始取如果为负就加上 RX_BUF_SIZE 回到缓冲区尾部。这个公式能同时处理“无回绕”和“有回绕”两种情况因为它取模了。这也是我说缓冲区大小最好选 2 的整数次幂的原因虽然这里取了模但在某些平台用位与操作可以更高效。4.4 接收长度计算里的边界我在实际调这块时发现了几个边界坑。第一DMA 计数器读取不是原子操作要防中断打断。如果主循环正在读__HAL_DMA_GET_COUNTER此时一个字节到达计数器变了可能读到旧值。这在低速时无所谓但高速时可能造成 1 字节的偏差。解决方法是读计数器前暂时关闭串口 DMA 中断或者采用一次性读取备份值后再恢复的方式。在我们的场景中空闲中断已经关掉了 DMA 相关操作主循环读取时中断是关闭的不是。所以稳妥起见可以在读 counter 时临界区保护uint16_t readPos; __disable_irq(); readPos RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart1_rx); __enable_irq();不过这种保护要小心别关掉了系统其他中断。既然空闲中断里也会修改 lastPos建议这两个量都用 volatile 修饰并且读取和更新的代码放在同一个临界区内。第二缓冲区满了怎么办如果发送方一帧数据超过 RX_BUF_SIZEDMA 循环模式会把最早的字节覆盖掉空闲中断到来时 currentPos 已经绕了一圈帧数据不完整。这种场景我一般通过上层协议的分帧机制来避免协议规定单帧最大长度比如 128 字节而缓冲区 256 字节就永远不会溢出。4.5 DMA 发送的关键细节接收搞定发送也不难但比很多人想象中多两个坑。调用发送 DMA 的接口是HAL_UART_Transmit_DMA(huart1, tx_data, len);这个函数调用后立即返回数据在后台通过 DMA 发送。你需要注意一是不能在发送过程中又调用一次否则会触发 HAL 的 busy 状态返回 HAL_BUSY。所以最好加个发送忙标志static volatile uint8_t txBusy 0; void UART_SendBytes(uint8_t *data, uint16_t len) { while (txBusy); // 等待上一次发送完成 txBusy 1; HAL_UART_Transmit_DMA(huart1, data, len); // 完成回调里把 txBusy 清 0 } void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { txBusy 0; } }二是发送完成的回调在 DMA 中断上下文里执行不要在回调里做耗时操作。如果你在回调里又立刻开始下一次 DMA 发送可能会和正在进行的 DMA 通道冲突。标准做法是回调里只清标志主循环看到标志后发下一包。5. 实战踩坑与调试心得5.1 空闲中断一使能就误触发这是我见过最多的坑。代码里刚执行__HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE);紧接着就进了一次空闲中断帧标志被置位但缓冲区里根本没数据。原因很简单USART 已经使能接收线上处于空闲状态硬件检测到空闲条件成立IDLE 标志立刻置位。这不是故障是正常行为。解决办法是使能空闲中断前先启动 DMA 接收并且在 DMA 启动完成后以“接收到第一个字节”作为时间点再使能空闲中断。或者在空闲中断里判断帧长度是否为 0如果为 0 就直接忽略不置标志if (g_usart1_frameLen 0) { return; // 一上来就空闲或者刚清完标志 }我用的是“长度为零直接忽略”的方法简单有效不需要动初始化顺序。5.2 收到的帧数据错位总是偏移一两个字节这个问题我排查了很久。后来发现是 DMA 配置的地址宽度和外设地址没对齐。具体来说就是 CubeMX 里 RX 通道的数据宽度误选成了 Half Word16 位导致 DMA 按 16 位搬运缓冲区里字节顺序被打乱。串口数据寄存器是 8 位的DMA 必须按 Byte 宽度搬运。只要 CubeMX 里 RX 和 TX 通道的数据宽度都选 Byte就不会出现这个问题。另外还要注意DMA 外设地址增量必须关闭内存地址增量必须打开方向别选反。还有一个低级错误是 RX 引脚没配置成复用功能。CubeMX 里 PA10 必须设置为 USART1_RX 的 Alternate Function如果你不小心把它设成了 GPIO_Input数据线根本连不到 USART 外设自然收不到任何东西。5.3 DMA 计数器读到 0 的问题我早期版本是这样写的currentPos RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart1_rx);在某个极端时序下DMA 刚好把 256 字节搬完计数器瞬间变成 0此时 currentPos 是 256。但缓冲区数组下标最大是 255如果直接用 256 去访问就是越界。解决方法是取模currentPos % RX_BUF_SIZE;。或者可以在启动 DMA 时把缓冲区大小设成 RX_BUF_SIZE而把 currentPos 限制在 0~255 的范围uint16_t currentPos (RX_BUF_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart1_rx)) % RX_BUF_SIZE;这里有个隐藏逻辑DMA 计数器的变化是连续的当前位置不会超过缓冲区大小就算读到 0 也只发生在刚绕一圈那一瞬间取模之后回到 0逻辑仍然正确。5.4 ORE 溢出导致 DMA 停摆高速率下另一个经典问题OREOverrun Error溢出错误一旦发生对应数据丢失而 DMA 接收在这种错误出现后可能无法恢复后续数据全都进不来。我排查到的场景是一帧数据一个接一个死死地塞进来串口速率又高而主循环处理一帧很慢DMA 缓冲区在连续搬运中如果 USART 的 RXNE 置位了但 DMA 还没及时把数据搬走新数据就把旧数据覆盖了置位 ORE。解决办法有两条路。一是在空闲中断里也检查__HAL_UART_GET_FLAG(huart1, UART_FLAG_ORE)发现后立即清错误标志并重启 DMA 接收。二是提高处理效率让缓冲区更大、处理更简单。最稳妥的写法if (__HAL_UART_GET_FLAG(huart1, UART_FLAG_ORE) ! RESET) { __HAL_UART_CLEAR_OREFLAG(huart1); // 重启 DMA 接收 HAL_UART_Receive_DMA(huart1, g_usart1_rxBuf, RX_BUF_SIZE); }注意重启 DMA 前要把HAL_UART_DMAStop或者直接调用HAL_UART_Receive_DMA重新绑定HAL 库里可以直接重新启动它会先停止再启动。5.5 多串口项目里的注意点如果你不止用一个串口方案完全一样只是 DMA 通道不同。USART1 用的是 DMA1 通道 4 接收、通道 3 发送USART2 是 DMA1 通道 6 接收、通道 5 发送USART3 是 DMA1 通道 3 接收、通道 2 发送。别问我为什么记这么清楚查表查多了自然就背下来了。如果你芯片是 F103 的高密度型号串口对应的 DMA 通道也一样不用额外配置。多串口时每个串口都要有独立的缓冲区、独立的 lastPos、独立的帧标志中断回调里根据huart-Instance区分void HAL_UART_TxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) txBusy1 0; else if (huart-Instance USART2) txBusy2 0; }5.6 调试技巧与设备选择最后聊点调试工具的心得。这种 DMA 接收方案光靠串口助手看不清楚数据时序。我建议准备两样东西一是带时间戳的串口调试工具。它能显示每帧数据到达的时间间隔帮你判断空闲中断是否准确。如果每帧时间间隔忽大忽小别急着怀疑代码先看看是不是 USB 转 TTL 模块不稳定或者波特率有偏移。二是逻辑分析仪便宜的二三十块的就够用。把 RX 线夹上直接看波形上帧与帧之间的间隔一眼就能看出“空闲时间”是多少以此核对空闲中断的判定是否合理。我吃过大亏一直以为代码有 bug结果发现是那个几块钱的 USB 转 TTL 模块在 921600 波特率下波形畸变严重根本上不了这么高速率。稳定的通信链路才是这套方案的前提模块性能不行再好的代码也白搭。最后分享几个我实际使用中的心得这套 DMA 空闲中断的不定长接收我从标准库时代用到 HAL 库时代从 F103 用到 F407 和 GD32 的同类芯片逻辑几乎没变过。核心思想就一句话让硬件和 DMA 做重复劳动CPU 只在“一帧结束”这个关键节点介入。把这条机制吃透你再去看其他 MCU 的串口接收都是触类旁通。我个人的经验是稳定性的关键往往不在代码多巧妙而在细节。比如清 IDLE 标志必须先读 SR 再读 DR比如 DMA 读取计数器的临界区保护比如缓冲区大小要大于最大帧长这些点每一个都让我的板子死过机、丢过数据。你们调试时遇到问题不要一上来就怀疑芯片有问题按这个顺序排查串口参数对不对、DMA 方向对不对、缓冲区大小够不够、中断优先级对不对九成问题出在配置而不是芯片。后面如果你们有兴趣我还可以聊聊怎么把这套接收框架扩展成支持多个协议的通用组件以及 DMA 发送的数据排队和超时处理。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑