MSPM0 串口不定长中断接收完美方案:RX FIFO + 超时中断 + 乒乓双缓冲(附完整工程代码)| 电赛备赛 📅 2026/7/13 1:39:34 前言上手 TI MSPM0 做串口开发的同学大概率都踩过同一个坑这颗芯片没有传统 MCU 常见的「IDLE 空闲中断」想做不定长帧接收只能用RX 超时中断RX Timeout。但网上很多教程要么只贴代码不讲原理要么忽略了最关键的 FIFO 阈值配置导致大家抄完代码后发现超时中断死活不触发、最后一个字节总是丢、连续发帧数据直接覆盖……本文彻底讲透 MSPM0 串口超时中断的工作原理拆解「FIFO 阈值设多少才对」这个核心问题并基于1/2 FIFO 阈值 单字节读取 乒乓双缓冲的思路给出一套可直接复用、零数据覆盖风险的驱动代码。一、先搞懂核心为什么你的超时中断触发不了1.1 超时中断的本质带前提的「空闲中断」MSPM0 的 RX 接收超时中断功能上等价于其他 MCU 的串口空闲中断用来判断一帧数据结束但它的触发有两个必须同时满足的硬性条件参考IT官方数据手册RX FIFO 非空硬件接收 FIFO 里必须还有未被 CPU 读取的数据超时周期无新数据在配置的超时时间内没有新的字节进入 FIFO。简单说只有 FIFO 里「攒着数据」且总线停了才会触发超时代表一帧发完了。1.2 他的坑FIFO 阈值设 1 字节MSPM0 串口硬件 FIFO 深度为 4 字节而RX FIFO Threshold Level 是决定何时触发RX接收中断的临界。必须把他设置为大于等于1/2FIFO -- 即有两个数据来才会触发 RX 接收中断。很多人图省事把阈值设为1 字节这会直接导致超时中断彻底失效来 1 个字节 → FIFO 达阈值 → 触发 RX 中断CPU 读走这 1 字节 → FIFO 变空FIFO 为空 → 不满足「FIFO 非空」的超时触发前提 → 超时中断永远不会来这就是「超时中断没反应」的根本原因。1.3 正确解法1/2 FIFO 阈值 只读 1 字节我的核心设计思路非常巧妙FIFO 阈值设为 1/22 字节只有攒够 2 字节才会触发 RX 中断RX 中断里只读 1 字节读完后 FIFO 里还剩 1 字节始终保持「非空」状态帧结束后FIFO 里剩余的 1 字节满足非空条件超时时间一到就正常触发超时中断读出最后 1 字节完成整帧接收。这个设计完美满足了超时中断的触发条件单字节短帧、多字节长帧都能完美适配。二、整体方案设计2.1 核心架构硬件层利用 UART 自带的 4 级 RX FIFO 超时中断模拟空闲中断实现帧结束检测驱动层采用「中断置标志 主循环处理」的架构中断内只做数据搬运不处理业务缓冲层双缓冲区乒乓机制彻底解决单缓冲区数据覆盖问题应用层弱定义回调函数驱动与业务完全解耦可直接复用。2.2 乒乓双缓冲机制开辟两块独立的接收缓冲区职责分离写入缓冲区中断只往这块写数据接收当前帧就绪缓冲区主循环只从这块读数据处理上一帧。切换时机超时中断触发一帧结束时立刻把当前写入缓冲区标记为就绪然后切换写入指针到另一块空闲缓冲区重置索引准备接收下一帧。优势主循环处理上一帧的同时新帧可以无缝写入另一块缓冲区完全不会出现数据覆盖也不会丢字节。三、CCS Config 配置这里以 UART0 为例四、完整工程代码4.1 驱动头文件uart.h#ifndef __UART__H #define __UART__H #include ti_msp_dl_config.h #include string.h /* 单块接收缓冲区大小根据实际最大帧长调整 */ #define UART_RX_BUF_SIZE 48 /* 对外接口函数 */ void UART_SendChar(UART_Regs *uart, uint8_t data); void UART_SendStr(UART_Regs *uart, uint8_t * data); void UART_Idle_Init(void); void UART_Proc(void); /* 帧接收完成回调函数弱定义应用层重写即可 */ void uart_rx_frame_callback(uint8_t *buf, uint16_t len); #endif // __UART__H4.2 驱动实现文件uart.c#include uart.h #define UART_IDLE_INST UART_0_INST #define UART_IRQn UART0_INT_IRQn /* 乒乓缓冲核心变量 */ static uint8_t s_rxBuf[2][UART_RX_BUF_SIZE]; // 两块乒乓接收缓冲区 static uint16_t s_rxLen[2]; // 对应缓冲区的有效数据长度 static __IO uint8_t s_writeBufIdx 0; // 当前正在写入的缓冲区编号(0/1) static __IO uint8_t s_readyBufIdx 0; // 已就绪、待主循环处理的缓冲区编号 static __IO bool s_frameReady false; // 完整帧就绪标志 static __IO uint16_t s_writeIdx 0; // 当前写入缓冲区的字节偏移 /** * brief 内部静态函数读取1字节FIFO数据追加到当前写入缓冲区 * note 缓冲区满则直接丢弃新数据硬截断 */ static void UART_Rx_AppendFIFO(void) { uint8_t recvByte DL_UART_receiveData(UART_IDLE_INST); if (s_writeIdx UART_RX_BUF_SIZE - 1) { s_rxBuf[s_writeBufIdx][s_writeIdx] recvByte; } // 缓冲区已满数据直接丢弃截断 } /* 串口发送函数 */ void UART_SendChar(UART_Regs *uart, uint8_t data){ while(DL_UART_isBusy(uart) true){ } DL_UART_transmitData(uart, data); } void UART_SendStr(UART_Regs *uart, uint8_t * data){ while(*data ! 0 data ! 0){ UART_SendChar(uart, *data); } } /* 串口超时接收初始化 */ void UART_Idle_Init(void) { NVIC_ClearPendingIRQ(UART_IRQn); NVIC_EnableIRQ(UART_IRQn); /* 配置接收超时时间单位为字符周期可根据波特率微调 */ DL_UART_setRXInterruptTimeout(UART_IDLE_INST, 5); /* 清空硬件FIFO复位乒乓所有状态 */ DL_UART_Main_receiveData(UART_IDLE_INST); s_writeBufIdx 0; s_readyBufIdx 0; s_frameReady false; s_writeIdx 0; } /* 主循环轮询处理函数 */ void UART_Proc(void) { if (s_frameReady true) { uint8_t *readyBuf s_rxBuf[s_readyBufIdx]; uint16_t frameLen s_rxLen[s_readyBufIdx]; /* 补字符串结束符方便字符串操作 */ readyBuf[frameLen] \0; /* 调用应用层业务回调 */ uart_rx_frame_callback(readyBuf, frameLen); /* 处理完成清除就绪标志 */ s_frameReady false; } } /* UART0 中断服务函数 */ void UART0_IRQHandler(void) { uint32_t intStatus DL_UART_getPendingInterrupt(UART_IDLE_INST); switch (intStatus) { /* FIFO达到阈值读取1字节保留FIFO非空状态保证超时可触发 */ case DL_UART_MAIN_IIDX_RX: UART_Rx_AppendFIFO(); DL_UART_clearInterruptStatus(UART_IDLE_INST, DL_UART_INTERRUPT_RX); break; /* 接收超时一帧结束读取剩余1字节执行乒乓切换 */ case DL_UART_MAIN_IIDX_RX_TIMEOUT_ERROR: /* 读入FIFO中剩余的帧尾数据 */ UART_Rx_AppendFIFO(); /* 保存当前缓冲区的有效数据长度 */ s_rxLen[s_writeBufIdx] s_writeIdx; /* 标记当前缓冲区为就绪态 */ s_readyBufIdx s_writeBufIdx; s_frameReady true; /* 切换写入缓冲区重置索引准备下一帧 */ s_writeBufIdx 1 - s_writeBufIdx; s_writeIdx 0; DL_Uart_clearInterruptStatus(UART_IDLE_INST, DL_UART_INTERRUPT_RX_TIMEOUT_ERROR); break; /* 其他中断错误等清标志退出 */ default: DL_UART_clearInterruptStatus(UART_IDLE_INST, intStatus); break; } } /* 弱定义回调应用层不重写则默认空实现 */ __attribute__((weak)) void uart_rx_frame_callback(uint8_t *buf, uint16_t len) { /* 防止编译器报未使用变量警告 */ (void)buf; (void)len; }4.3 主函数调用示例main.c/* * Copyright (c) 2021, Texas Instruments Incorporated * All rights reserved. */ #include ti_msp_dl_config.h #include uart.h int main(void) { SYSCFG_DL_init(); UART_Idle_Init(); while (1) { UART_Proc(); } } /** * brief 重写接收完成回调回显接收到的整帧数据 */ void uart_rx_frame_callback(uint8_t *buf, uint16_t len) { UART_SendStr(UART_0_INST, (uint8_t*)RX: ); UART_SendStr(UART_0_INST, buf); UART_SendStr(UART_0_INST, (uint8_t*)\r\n); }五、不同帧长的工作流程验证5.1 1 字节短帧1 字节进入 FIFO未达 2 字节阈值不触发 RX 中断总线空闲超时时间到触发超时中断读出 FIFO 中 1 字节写入当前缓冲区标记帧就绪乒乓切换缓冲区主循环检测到标志回调处理 1 字节数据。5.2 2 字节帧第 2 字节进入 FIFO达阈值触发 RX 中断RX 中断只读 1 字节FIFO 剩 1 字节保持非空无后续数据超时触发读出最后 1 字节标记帧完成切换缓冲区主循环处理完整 2 字节数据。5.3 N 字节长帧每攒够 2 字节触发一次 RX 中断只读 1 字节FIFO 始终留 1 字节最后不足 2 字节的余数留在 FIFO 中帧结束超时触发读出剩余字节乒乓切换主循环处理整帧数据六、避坑指南与常见问题6.1 超时中断完全不触发看看是不是 FIFO 阈值设成了 1 字节或者没开 RX FIFO。去 SysConfig 里确认阈值是 1/2且确保开启了接收超时中断和接收中断。6.2 超时时间设多少合适DL_UART_setRXInterruptTimeout的参数单位是「波特率时钟周期」一般设 2~5 即可。七、总结这套方案充分利用了 MSPM0 串口的硬件 FIFO 和超时中断特性用非常简洁的逻辑实现了稳定的不定长串口接收加上乒乓缓冲后彻底解决了数据覆盖问题弱回调的设计也让驱动可以直接复用到不同项目里。写下本篇文章也是因为我在仿照stm32写空闲中断接收时候卡了很久研究了很久找了大量的资料发现MSPM0设计的和stm32的不同之处希望本篇文章能帮助到各位正在电赛备赛的同学们。我会持续更新嵌入式相关内容。