STM32F407 HAL库实战:DMA_Normal模式下的UART轮询循环发送机制

张开发
2026/4/3 19:09:04 15 分钟阅读
STM32F407 HAL库实战:DMA_Normal模式下的UART轮询循环发送机制
1. 为什么需要DMA_Normal模式下的UART轮询发送在嵌入式开发中串口通信是最基础也最常用的功能之一。当我们需要频繁发送数据时传统的阻塞式发送会占用大量CPU资源而中断方式又可能带来不可预期的时序问题。这时候DMA直接内存访问技术就成了救星。但很多开发者可能不知道DMA的Normal模式配合轮询机制能带来意想不到的优势。我最近在一个工业传感器项目中就采用了这种方案实测下来发现它特别适合周期性发送固定长度数据的场景比如每100ms发送一次设备状态数据定时上传传感器采集的测量值发送心跳包维持通信连接这种方式的最大好处是完全不依赖中断不会打断主程序流程同时又能保证数据发送的实时性。想象一下你的主程序正在处理一个重要的控制算法突然被串口发送中断打断可能会导致时序错乱。而DMA_Normal轮询的方案就像有个勤快的助手默默地帮你把数据发出去完全不会打扰你的工作。2. 硬件配置与初始化2.1 硬件连接检查以STM32F407和USART1为例首先确保硬件连接正确PA9USART1_TX连接至接收设备的RXPA10USART1_RX连接至接收设备的TX共地线连接必不可少我遇到过不少新手忽略共地的问题导致通信异常。记住电压是相对的没有共同的参考地通信就像两个说不同语言的人根本无法理解对方。2.2 USART初始化使用HAL库初始化串口非常直观但有几个参数需要特别注意void MX_USART1_UART_Init(void) { huart1.Instance USART1; huart1.Init.BaudRate 115200; // 根据实际需求调整 huart1.Init.WordLength UART_WORDLENGTH_8B; huart1.Init.StopBits UART_STOPBITS_1; huart1.Init.Parity UART_PARITY_NONE; huart1.Init.Mode UART_MODE_TX_RX; huart1.Init.HwFlowCtl UART_HWCONTROL_NONE; huart1.Init.OverSampling UART_OVERSAMPLING_16; HAL_UART_Init(huart1); }波特率选择是个容易踩坑的地方。115200bps对于大多数应用足够了但如果传输距离较长建议降低到9600或以下。我曾经在一个工业现场因为电磁干扰导致115200通信不稳定降到57600后问题立刻解决。2.3 DMA初始化关键点DMA的配置相对复杂一些需要特别注意数据流向和模式选择hdma_usart1_tx.Instance DMA2_Stream7; hdma_usart1_tx.Init.Channel DMA_CHANNEL_4; hdma_usart1_tx.Init.Direction DMA_MEMORY_TO_PERIPH; // 内存到外设 hdma_usart1_tx.Init.PeriphInc DMA_PINC_DISABLE; // 外设地址不递增 hdma_usart1_tx.Init.MemInc DMA_MINC_ENABLE; // 内存地址递增 hdma_usart1_tx.Init.PeriphDataAlignment DMA_PDATAALIGN_BYTE; hdma_usart1_tx.Init.MemDataAlignment DMA_MDATAALIGN_BYTE; hdma_usart1_tx.Init.Mode DMA_NORMAL; // 重点Normal模式 hdma_usart1_tx.Init.Priority DMA_PRIORITY_MEDIUM;这里最容易出错的是地址递增设置。如果MemInc设置错误DMA会反复发送缓冲区的第一个字节而不是整个缓冲区。我早期就犯过这个错误调试了半天才发现问题。3. DMA_Normal模式的工作原理3.1 Normal模式与Circular模式的区别很多开发者对这两种模式的区别感到困惑。简单来说Circular模式DMA会自动重新加载配置形成循环发送Normal模式DMA发送完一次后自动停止需要手动重启Circular模式看似方便但在实际项目中我发现它有几个缺点无法灵活控制发送时机数据更新时可能造成撕裂现象部分旧数据和新数据混合调试时更难追踪发送状态而Normal模式虽然需要手动控制但可控性更强特别适合需要精确控制发送时机的场景。3.2 轮询机制的实现原理我们的方案核心是轮询DMA的EN位使能位。在Normal模式下DMA开始传输时EN1传输完成后硬件自动将EN0我们通过检查EN位状态来判断是否完成传输如果EN0说明可以重新配置并启动下一次传输这个过程完全在主循环中完成不需要任何中断参与。就像定期查看邮箱是否有新邮件而不是每次有新邮件都让邮差敲门通知你。4. 完整实现代码解析4.1 DMA启动函数详解先来看启动DMA发送的关键函数#define TX_LEN 10 uint8_t uart_send_buffer[TX_LEN] Hello DMA; void DMA_UART1_SendStart(void) { // 确保DMA被禁用 __HAL_DMA_DISABLE(hdma_usart1_tx); // 等待DMA完全停止 while ((hdma_usart1_tx.Instance-CR DMA_SxCR_EN) ! 0); // 配置传输数据量 hdma_usart1_tx.Instance-NDTR TX_LEN; // 设置外设地址串口数据寄存器 hdma_usart1_tx.Instance-PAR (uint32_t)USART1-DR; // 设置内存地址发送缓冲区 hdma_usart1_tx.Instance-M0AR (uint32_t)uart_send_buffer; // 清除传输完成标志 __HAL_DMA_CLEAR_FLAG(hdma_usart1_tx, DMA_FLAG_TCIF3_7); // 启用DMA __HAL_DMA_ENABLE(hdma_usart1_tx); // 启用串口的DMA发送功能 huart1.Instance-CR3 | USART_CR3_DMAT; }这里有几个关键点需要注意在重新配置DMA前必须确保它已经完全停止NDTR寄存器设置的是传输数据量不是字节数每次启动前清除标志位是个好习惯避免遗留状态影响4.2 主循环中的轮询逻辑主程序中的轮询逻辑非常简单高效void main(void) { // 系统初始化... DMA_UART1_SendStart(); // 首次启动DMA while (1) { // 其他任务处理... // 检查DMA是否完成 if ((hdma_usart1_tx.Instance-CR DMA_SxCR_EN) 0) { // 更新发送内容示例 uart_send_buffer[0]; // 重启DMA hdma_usart1_tx.Instance-NDTR TX_LEN; __HAL_DMA_CLEAR_FLAG(hdma_usart1_tx, DMA_FLAG_TCIF3_7); __HAL_DMA_ENABLE(hdma_usart1_tx); } // 其他任务... } }这种设计的美妙之处在于低干扰不会打断主程序流程高效率只在DMA空闲时进行简单检查灵活可控可以自由决定何时更新数据和重启发送5. 实际应用中的优化技巧5.1 数据更新策略在实际项目中我们通常需要发送不断变化的数据。这时候有几种策略双缓冲机制准备两个缓冲区一个用于DMA发送一个用于数据更新数据就绪标志设置标志位指示新数据是否准备好定时采样固定时间间隔更新数据我在一个温度监控系统中使用了双缓冲方案效果非常好uint8_t uart_buffer1[TX_LEN], uart_buffer2[TX_LEN]; uint8_t *active_buffer uart_buffer1; uint8_t *update_buffer uart_buffer2; void UpdateSensorData(void) { // 在update_buffer中更新数据 update_buffer[0] ReadTemperature(); // ...其他数据 // 交换缓冲区 uint8_t *temp active_buffer; active_buffer update_buffer; update_buffer temp; }5.2 错误处理与稳定性任何通信系统都需要考虑错误处理。对于我们的方案建议添加超时检测防止DMA卡死错误计数连续错误达到阈值后采取恢复措施状态监控记录发送成功/失败次数一个简单的超时检测实现uint32_t start_time HAL_GetTick(); while ((hdma_usart1_tx.Instance-CR DMA_SxCR_EN) ! 0) { if (HAL_GetTick() - start_time 100) // 100ms超时 { DMA_RecoveryProcedure(); break; } }5.3 性能优化建议经过多个项目实践我总结出几个优化点对齐内存地址确保缓冲区地址按4字节对齐提高DMA效率合理设置优先级如果系统中有多个DMA流合理设置优先级减少内存拷贝直接在被发送的数据结构上添加DMA支持例如使用GCC的特性确保对齐__attribute__((aligned(4))) uint8_t uart_send_buffer[TX_LEN];6. 常见问题与解决方案6.1 DMA不启动或发送不完整这是最常见的问题通常有几个原因时钟未使能忘记开启DMA或USART时钟缓冲区太小NDTR设置值大于实际缓冲区大小地址错误外设或内存地址设置不正确调试建议检查所有相关时钟是否使能使用调试器查看DMA寄存器值逐步验证每个配置步骤6.2 数据错乱或重复如果发现接收端数据不正常检查MemInc/PermInc设置是否正确确认缓冲区没有被意外修改验证波特率是否匹配我曾经遇到过一个棘手的问题发送的数据偶尔会重复。最后发现是因为在DMA还未完成时就修改了缓冲区内容。解决方案是严格检查EN位状态后再更新数据。6.3 系统响应变慢如果发现主循环执行变慢检查轮询频率是否过高考虑添加小延迟减少CPU占用评估是否真的不能使用中断一个折中方案是定时检查而不是每次循环都检查static uint32_t last_check 0; if (HAL_GetTick() - last_check 10) // 每10ms检查一次 { last_check HAL_GetTick(); if ((hdma_usart1_tx.Instance-CR DMA_SxCR_EN) 0) { // 重启DMA... } }7. 进阶应用场景7.1 多串口管理当系统需要管理多个串口时我们可以抽象出一个通用发送函数typedef struct { UART_HandleTypeDef *huart; DMA_HandleTypeDef *hdma; uint8_t buffer[TX_LEN]; } UART_Channel; void UART_DMASend(UART_Channel *ch) { if ((ch-hdma-Instance-CR DMA_SxCR_EN) 0) { ch-hdma-Instance-NDTR TX_LEN; ch-hdma-Instance-PAR (uint32_t)ch-huart-Instance-DR; ch-hdma-Instance-M0AR (uint32_t)ch-buffer; __HAL_DMA_CLEAR_FLAG(ch-hdma, DMA_FLAG_TCIF3_7); __HAL_DMA_ENABLE(ch-hdma); ch-huart-Instance-CR3 | USART_CR3_DMAT; } }7.2 与RTOS配合使用在RTOS环境中这种方案可以与任务调度完美结合。例如在FreeRTOS中void vUARTSendTask(void *pvParameters) { while (1) { // 等待信号量指示有新数据需要发送 xSemaphoreTake(xDataReadySemaphore, portMAX_DELAY); // 启动DMA发送 DMA_UART1_SendStart(); // 轮询等待完成 while ((hdma_usart1_tx.Instance-CR DMA_SxCR_EN) ! 0) { vTaskDelay(pdMS_TO_TICKS(1)); // 让出CPU } } }7.3 大数据块分片发送当需要发送超过DMA最大长度的数据时可以采用分片策略#define MAX_DMA_LEN 65535 // DMA最大传输长度 #define BIG_DATA_LEN 100000 void SendLargeData(uint8_t *data, uint32_t len) { uint32_t sent 0; while (sent len) { uint32_t chunk MIN(MAX_DMA_LEN, len - sent); // 等待当前传输完成 while ((hdma_usart1_tx.Instance-CR DMA_SxCR_EN) ! 0); // 设置分片数据 hdma_usart1_tx.Instance-NDTR chunk; hdma_usart1_tx.Instance-M0AR (uint32_t)(data sent); __HAL_DMA_CLEAR_FLAG(hdma_usart1_tx, DMA_FLAG_TCIF3_7); __HAL_DMA_ENABLE(hdma_usart1_tx); sent chunk; } }在实际项目中这种DMA_Normal模式配合轮询的方案已经被证明是可靠且高效的。它不仅适用于简单的数据发送还可以扩展到更复杂的通信协议实现。关键是要理解其工作原理根据具体需求灵活调整实现细节。

更多文章