GD32F303串口DMA发送数据避坑指南:为什么你的发送函数会卡住?

张开发
2026/4/15 23:20:08 15 分钟阅读

分享文章

GD32F303串口DMA发送数据避坑指南:为什么你的发送函数会卡住?
GD32F303串口DMA发送数据避坑指南为什么你的发送函数会卡住调试嵌入式系统时串口通信是最基础也最常用的功能之一。而DMA直接内存访问技术的引入可以大幅减轻CPU负担提高系统效率。但在实际应用中许多开发者在使用GD32F303的串口DMA发送功能时经常会遇到程序卡死、数据发送不完整等问题。本文将深入分析这些问题的根源并提供切实可行的解决方案。1. DMA发送卡死的常见原因分析当开发者配置好DMA发送功能后经常会遇到程序在执行发送函数时卡住的情况。这种现象通常由以下几个原因导致1.1 DMA传输完成中断TC与传输使能/禁用的时序问题DMA传输完成中断Transfer Complete interrupt简称TC中断是DMA控制器在完成数据传输后触发的中断。但在实际应用中开发者经常会忽略一个关键细节DMA通道的使能Enable和禁用Disable操作并不是即时生效的。void UART1_DMA_Transmit(const uint8_t *data, uint16_t len) { dma_channel_disable(DMA0, DMA_CH6); // 禁用DMA通道 dma_memory_address_config(DMA0, DMA_CH6, (uint32_t)data); dma_transfer_number_config(DMA0, DMA_CH6, len); dma_channel_enable(DMA0, DMA_CH6); // 重新使能DMA通道 }这段看似合理的代码实际上隐藏着一个潜在的风险如果在禁用DMA通道后立即重新配置并启用而此时上一次传输的TC中断还未被处理就可能导致中断状态混乱进而引发程序卡死。1.2 发送状态管理缺失另一个常见问题是缺乏完善的发送状态管理机制。许多开发者会使用一个简单的标志位来表示DMA是否正在发送volatile uint8_t tx_busy 0; void DMA0_Channel6_IRQHandler(void) { if(dma_interrupt_flag_get(DMA0, DMA_CH6, DMA_INT_FLAG_FTF)) { dma_interrupt_flag_clear(DMA0, DMA_CH6, DMA_INT_FLAG_FTF); tx_busy 0; // 标记发送完成 } }但这种简单的状态管理在面对连续快速发送请求时很容易出现状态判断错误导致数据覆盖或丢失。2. 阻塞等待与非阻塞回调的发送策略对比针对DMA发送功能开发者通常采用两种策略阻塞等待发送完成和非阻塞回调通知。两种方式各有优缺点适用于不同场景。2.1 阻塞等待发送完成阻塞等待方式会在发送函数中等待DMA传输完成期间CPU处于忙等待状态void UART1_DMA_Transmit_Blocking(const uint8_t *data, uint16_t len) { while(tx_busy); // 等待上一次发送完成 tx_busy 1; dma_channel_disable(DMA0, DMA_CH6); dma_memory_address_config(DMA0, DMA_CH6, (uint32_t)data); dma_transfer_number_config(DMA0, DMA_CH6, len); dma_channel_enable(DMA0, DMA_CH6); while(tx_busy); // 等待本次发送完成 }优点实现简单直观保证发送顺序与调用顺序一致缺点占用CPU资源降低系统效率在实时性要求高的系统中可能造成响应延迟2.2 非阻塞回调通知非阻塞方式通过回调函数通知发送完成不占用CPU等待时间typedef void (*UART_TX_Callback_t)(void); UART_TX_Callback_t Uart1TxCallback NULL; void UART1_DMA_Transmit_NonBlocking(const uint8_t *data, uint16_t len, UART_TX_Callback_t callback) { if(tx_busy) return; // 正在发送拒绝新请求 tx_busy 1; Uart1TxCallback callback; dma_channel_disable(DMA0, DMA_CH6); dma_memory_address_config(DMA0, DMA_CH6, (uint32_t)data); dma_transfer_number_config(DMA0, DMA_CH6, len); dma_channel_enable(DMA0, DMA_CH6); } void DMA0_Channel6_IRQHandler(void) { if(dma_interrupt_flag_get(DMA0, DMA_CH6, DMA_INT_FLAG_FTF)) { dma_interrupt_flag_clear(DMA0, DMA_CH6, DMA_INT_FLAG_FTF); tx_busy 0; if(Uart1TxCallback) Uart1TxCallback(); } }优点不占用CPU等待时间提高系统整体效率缺点实现相对复杂需要处理发送队列管理如果需要保证发送顺序3. DMA发送前的必要操作为什么需要禁用通道在每次DMA发送前禁用通道是一个容易被忽视但至关重要的操作。以下是几个必须这样做的原因防止配置冲突在DMA传输过程中修改配置可能导致不可预测的行为清除残留状态确保所有相关标志位和状态寄存器被正确重置保证配置生效某些配置只有在通道禁用时才能被修改dma_channel_disable(DMA0, DMA_CH6); // 必须的步骤 /* 配置内存地址、传输长度等 */ dma_channel_enable(DMA0, DMA_CH6); // 重新使能注意禁用DMA通道后需要等待至少2个时钟周期才能确保通道完全停止。在实际应用中通常会在禁用后插入一个小延迟或检查通道使能状态。4. 构建健壮的DMA发送状态机为了确保DMA发送的可靠性建议实现一个完整的状态机来管理发送过程。以下是一个增强版的状态机实现4.1 状态定义typedef enum { UART_TX_IDLE, // 空闲状态可以接收新发送请求 UART_TX_PREPARING, // 正在准备发送配置DMA UART_TX_SENDING, // 数据正在发送中 UART_TX_WAIT_RETRY // 等待重试用于出错情况 } UART_TX_State_t;4.2 增强版发送函数#define MAX_RETRY_COUNT 3 #define TX_TIMEOUT_MS 100 typedef struct { const uint8_t *data; uint16_t len; uint8_t retry_count; UART_TX_Callback_t callback; } UART_TX_Request_t; static volatile UART_TX_State_t tx_state UART_TX_IDLE; static UART_TX_Request_t current_request; static uint32_t tx_start_time; void UART1_DMA_Transmit_Enhanced(const uint8_t *data, uint16_t len, UART_TX_Callback_t callback) { if(tx_state ! UART_TX_IDLE) { // 可以在这里实现发送队列或者直接返回错误 return; } current_request.data data; current_request.len len; current_request.retry_count 0; current_request.callback callback; tx_state UART_TX_PREPARING; Prepare_DMA_Transfer(); } void Prepare_DMA_Transfer(void) { dma_channel_disable(DMA0, DMA_CH6); while(dma_channel_enable_get(DMA0, DMA_CH6)); // 确保通道已禁用 dma_memory_address_config(DMA0, DMA_CH6, (uint32_t)current_request.data); dma_transfer_number_config(DMA0, DMA_CH6, current_request.len); tx_start_time Get_System_Tick(); dma_channel_enable(DMA0, DMA_CH6); tx_state UART_TX_SENDING; } void DMA0_Channel6_IRQHandler(void) { if(dma_interrupt_flag_get(DMA0, DMA_CH6, DMA_INT_FLAG_FTF)) { dma_interrupt_flag_clear(DMA0, DMA_CH6, DMA_INT_FLAG_FTF); if(tx_state UART_TX_SENDING) { tx_state UART_TX_IDLE; if(current_request.callback) current_request.callback(); } } if(dma_interrupt_flag_get(DMA0, DMA_CH6, DMA_INT_FLAG_ERR)) { dma_interrupt_flag_clear(DMA0, DMA_CH6, DMA_INT_FLAG_ERR); if(current_request.retry_count MAX_RETRY_COUNT) { current_request.retry_count; tx_state UART_TX_WAIT_RETRY; // 可以设置一个定时器稍后重试 } else { tx_state UART_TX_IDLE; // 通知上层发送失败 } } } void Check_TX_Timeout(void) { if(tx_state UART_TX_SENDING) { if(Get_System_Tick() - tx_start_time TX_TIMEOUT_MS) { dma_channel_disable(DMA0, DMA_CH6); tx_state UART_TX_IDLE; // 处理超时错误 } } }这个增强版实现包含了以下关键特性明确的状态管理发送超时检测错误重试机制非阻塞回调通知5. 双通道DMA的特别注意事项GD32F303支持双通道DMA这为串口通信提供了更大的灵活性但也带来了一些额外的注意事项通道优先级当多个DMA通道同时工作时需要合理设置通道优先级资源竞争避免两个通道同时访问相同的外设或内存区域中断处理确保不同通道的中断处理程序不会互相干扰// 设置DMA通道优先级示例 DMA_InitStruct.priority DMA_PRIORITY_HIGH; // 高优先级通道 dma_init(DMA0, DMA_CH5, DMA_InitStruct); DMA_InitStruct.priority DMA_PRIORITY_MEDIUM; // 中优先级通道 dma_init(DMA0, DMA_CH6, DMA_InitStruct);在实际项目中我曾遇到一个典型的双通道问题当接收通道DMA_CH5和发送通道DMA_CH6同时工作时如果优先级设置不当会导致接收数据丢失。通过调整发送通道为低优先级接收通道为高优先级问题得到了解决。

更多文章