前言在嵌入式串口开发中传统的逐字节中断接收会频繁打断 CPU在高波特率、大数据量场景下不仅 CPU 占用高还存在丢数风险。而 DMA 接收虽然能实现数据自动搬运但普通单缓冲模式下缓冲区满后如果 CPU 不及时处理新数据就会覆盖旧数据。本文基于 TI MSPM0 系列的 DriverLib 驱动库利用DMA 半传输中断Early Interrupt 传输完成中断将单块缓冲区一分为二实现乒乓双缓冲效果同时采用弱函数回调实现驱动层与应用层解耦整套方案 CPU 占用极低逻辑清晰易维护非常适合定长协议帧的串口接收场景。注意这里是定长接收也就是说外部发来数据长度必须是已知。为何不采用类似于stm32的dmaidle中断接收呢还是考虑到MSPM0设计的特殊性需要开一个定时器去进行空闲帧判断因为mspm0触发idle的条件是 fifo里还有数据且未来无数据仿照stm32写法会漏数据。当然大家可以采用接收中断加超时中断的写法只是我认为相比于那样写本次方案进入中断更少对整个系统更健康。一、核心设计思路1.1 双缓冲乒乓接收原理我们不额外开辟两块独立缓冲区而是将一块接收缓冲区平分为前半区和后半区配合 DMA 的两个中断实现乒乓操作DMA 自动把串口数据搬运到缓冲区CPU 完全不参与当前半区存满时触发 DMA 半传输中断此时 CPU 可以安全读取前半区数据同时 DMA 会继续往后半区搬运数据互不干扰当后半区存满整缓冲区满时触发 DMA 传输完成中断CPU 读取后半区数据同时重新配置 DMA开启下一轮接收如此循环往复实现 “数据搬运” 和 “数据处理” 并行从根源上避免数据覆盖1.2 弱函数回调解耦驱动层只提供接收能力和回调接口不耦合具体业务逻辑驱动内定义__attribute__((weak))弱属性的回调函数默认实现为空应用层只需重定义同名函数即可编写自己的业务逻辑优势驱动代码可以通用复用修改业务逻辑不需要动驱动源码1.3 中断 “只置标志不做处理”中断服务函数内仅做状态标志位置位所有数据处理全部放在主循环的轮询函数中执行保证中断响应速度极快不会因为中断内处理耗时逻辑导致丢数避免中断内调用复杂函数、长耗时操作带来的风险主循环按业务节奏处理数据逻辑更直观调试更方便二、CCS Config 配置界面这里采用 uart0 举例三、完整代码实现3.1 驱动头文件uart_dma.h#ifndef __UART_DMA_H #define __UART_DMA_H #include ti_msp_dl_config.h #include string.h /* 接收缓冲区总大小根据实际定长帧长度调整 */ #define UART_DMA_RX_BUF_SIZE 30 //这里假设你已知发来数据长度是15那15x230 /* 全局接收缓冲区应用层可直接访问 */ extern uint8_t uart_dma_rxBuffer[UART_DMA_RX_BUF_SIZE]; /* 驱动对外接口 */ void UART_DMA_Init(void); void uart_dma_proc(void); //放在主循环中运行 /* 弱回调函数应用层可重定义 */ void uart_dma_halfFull_callback(void); // 半满回调前半区数据就绪 void uart_dma_full_callback(void); // 全满回调后半区数据就绪 #endif3.2 驱动实现uart_dma.c#include uart_dma.h /* 硬件实例配置根据自己的引脚/通道修改 */ #define UART_DMA_INSTANCE UART_0_INST #define UART_DMA_CHANNEL DMA_CH0_CHAN_ID /* 接收状态枚举 */ typedef enum{ UART_NOT_RECEIVED 0, UART_HALF_RECEIVED 1, UART_RECEIVED_DONE 2, }UART_ReceiveStatus_t; /* 接收缓冲区 */ uint8_t uart_dma_rxBuffer[UART_DMA_RX_BUF_SIZE] {0}; /* 接收状态标志中断和主循环共享加__IO防编译器优化 */ static __IO UART_ReceiveStatus_t rx_flag UART_NOT_RECEIVED; /** * brief 串口DMA接收初始化 */ void UART_DMA_Init(void){ /* 配置DMA源地址串口接收数据寄存器 */ DL_DMA_setSrcAddr(DMA, UART_DMA_CHANNEL, (uint32_t)(UART_DMA_INSTANCE-RXDATA)); /* 配置DMA目的地址接收缓冲区首地址 */ DL_DMA_setDestAddr(DMA, UART_DMA_CHANNEL, (uint32_t)uart_dma_rxBuffer[0]); /* 配置DMA总传输长度 缓冲区总大小 */ DL_DMA_setTransferSize(DMA, UART_DMA_CHANNEL, UART_DMA_RX_BUF_SIZE); /* 配置提前中断阈值半传输缓冲区一半时触发中断 */ DL_DMA_Full_Ch_setEarlyInterruptThreshold( DMA, UART_DMA_CHANNEL, DL_DMA_EARLY_INTERRUPT_THRESHOLD_HALF ); /* 使能DMA半传输中断 传输完成中断 */ DL_DMA_enableInterrupt(DMA, DL_DMA_FULL_CH_INTERRUPT_EARLY_CHANNEL0 | DL_DMA_INTERRUPT_CHANNEL0 ); /* 配置DMA中断优先级并使能 */ NVIC_ClearPendingIRQ(DMA_INT_IRQn); NVIC_EnableIRQ(DMA_INT_IRQn); /* 使能DMA通道开始等待接收 */ DL_DMA_enableChannel(DMA, UART_DMA_CHANNEL); /* 使能串口接收DMA完成中断备用兜底也可只用DMA中断 */ NVIC_ClearPendingIRQ(UART0_INT_IRQn); NVIC_EnableIRQ(UART0_INT_IRQn); DL_UART_Main_receiveData(UART_DMA_INSTANCE); // 清空串口接收FIFO } /** * brief 主循环轮询处理函数需在while(1)中持续调用 */ void uart_dma_proc(void) { /* 全满处理后半区数据并重开DMA */ if (rx_flag UART_RECEIVED_DONE) { rx_flag UART_NOT_RECEIVED; uart_dma_full_callback(); /*因为配置的模式是single repeat 所以下面这四行可以不加当然加了也没事。 如果配置的是single则加上下面这四行*/ /*DL_DMA_setSrcAddr(DMA, UART_DMA_CHANNEL, (uint32_t)(UART_DMA_INSTANCE-RXDATA)); DL_DMA_setDestAddr(DMA, UART_DMA_CHANNEL, (uint32_t) uart_dma_rxBuffer[0]); DL_DMA_setTransferSize(DMA, UART_DMA_CHANNEL, UART_DMA_RX_BUF_SIZE); DL_DMA_enableChannel(DMA, UART_DMA_CHANNEL); */ } /* 半满处理前半区数据DMA继续搬运后半区 */ else if(rx_flag UART_HALF_RECEIVED){ rx_flag UART_NOT_RECEIVED; uart_dma_halfFull_callback(); } } /** * brief 串口0中断服务函数 * note 作为DMA接收完成的兜底中断也可直接只用DMA中断 */ void UART0_IRQHandler(void) { uint32_t intStatus DL_UART_getPendingInterrupt(UART_DMA_INSTANCE); switch (intStatus) { case DL_UART_IIDX_DMA_DONE_RX: rx_flag UART_RECEIVED_DONE; DL_UART_clearInterruptStatus(UART_DMA_INSTANCE, intStatus); break; default: DL_UART_clearInterruptStatus(UART_DMA_INSTANCE, intStatus); break; } } /** * brief DMA中断服务函数 * note 半传输中断在这里处理 */ void DMA_IRQHandler(void) { uint32_t intStatus DL_DMA_getPendingInterrupt(DMA); if (intStatus DL_DMA_FULL_CH_EVENT_IIDX_EARLY_IRQ_DMACH0) { rx_flag UART_HALF_RECEIVED; DL_DMA_clearInterruptStatus(DMA, intStatus); } /* 传输完成中断也可放在这里和串口中断二选一即可 */ } /* 弱回调函数应用层不重定义则默认空实现 */ __attribute__((weak)) void uart_dma_halfFull_callback(void) {} __attribute__((weak)) void uart_dma_full_callback(void) {}3.3 应用层调用示例main.c#include ti_msp_dl_config.h #include uart_dma.h /* 串口发送单个字符按需实现 */ void UART_SendChar(UART_Regs *uart, uint8_t data) { while(DL_UART_isTXFIFOFull(uart)); DL_UART_transmitData(uart, data); } int main(void) { SYSCFG_DL_init(); // SDK图形化配置生成的初始化 UART_DMA_Init(); // 串口DMA接收初始化 while (1) { uart_dma_proc(); // 主循环轮询处理 } } /** * brief 重定义半满回调回显前半区数据 */ void uart_dma_halfFull_callback(void) { for(uint8_t i 0; i (UART_DMA_RX_BUF_SIZE/2); i) { UART_SendChar(UART_0_INST, uart_dma_rxBuffer[i]); } } /** * brief 重定义全满回调回显后半区数据 */ void uart_dma_full_callback(void) { for(uint8_t i (UART_DMA_RX_BUF_SIZE/2); i UART_DMA_RX_BUF_SIZE; i) { UART_SendChar(UART_0_INST, uart_dma_rxBuffer[i]); } }四、关键细节与避坑指南4.1 半传输中断的正确配置MSPM0 的 DMA 半传输中断叫Early Interrupt提前中断不是所有库的HALF_TRANSFER命名配置时要调用DL_DMA_Full_Ch_setEarlyInterruptThreshold阈值选DL_DMA_EARLY_INTERRUPT_THRESHOLD_HALF不要配错。4.2 中断内必须只置标志绝对不要在中断服务函数里做数据解析、串口发送、延时等耗时操作。中断的核心职责是最快速度标记事件处理逻辑全部交给主循环这是保证高波特率下不丢数的关键。当然写本文章的用意也就是让大家不用再去写中断服务函数里的东西了他的触发判断标志位判断与清除全部包含在内只需要重写回调函数放心地在回调函数里完成你的逻辑就行。4.3 关于全满后是否重配 DMA如果选择的是single模式MSPM0 的 DMA 传输完成后通道会自动停止因此全满回调处理完后需要重新设置源地址、目的地址、传输大小再重新使能通道才能开启下一轮接收。半满时 DMA 还在继续工作不需要重配。选择single repeat模式则只需在init配置一次就好了。4.4 弱函数的使用注意回调函数重定义时函数名、参数、返回值必须和驱动内的弱函数完全一致同一个工程里只能有一个强定义的实现否则会报重复定义错误如果不需要某个回调可以不重定义默认空实现不会报错4.5 适用场景说明本方案是定长接收设计适合协议帧长度固定的场景比如固定 30 字节一帧。如果是不定长数据需要额外叠加串口空闲中断来判断帧结束本方案的双缓冲思路同样可以复用。五、方案优势总结CPU 占用极低数据搬运全由 DMA 完成CPU 只在半满、全满时处理数据适合低功耗、多任务场景无数据覆盖风险双缓冲乒乓机制让 “接收” 和 “处理” 并行半区处理互不干扰代码解耦复用弱回调设计让驱动层和业务层完全分离驱动代码可以复用到不同项目调试维护简单主循环处理逻辑中断逻辑极简排查问题更方便