嵌入式串口通信:轮询、中断与DMA实战解析

张开发
2026/4/14 20:50:46 15 分钟阅读

分享文章

嵌入式串口通信:轮询、中断与DMA实战解析
1. 串口通信基础与常见应用场景串口通信作为嵌入式系统中最基础也最常用的通信方式之一已经存在了几十年。在当前的嵌入式开发中UART通用异步收发传输器仍然是MCU与外围设备通信的首选方案。我经历过不少项目从简单的传感器数据采集到复杂的工业控制协议传输串口都扮演着关键角色。为什么串口如此经久不衰首先是硬件成本极低大多数MCU都内置多个UART外设其次是协议简单只需要TX、RX两根线就能实现全双工通信再者是软件生态成熟各种操作系统和开发框架都提供了完善的支持。在实际项目中我常用串口连接GPS模块、无线通信模块如蓝牙、LoRa、传感器如温湿度、气压等设备。不过串口通信也有其局限性。异步通信没有时钟信号双方需要事先约定相同的波特率传输距离较短通常不超过15米缺乏硬件流控时容易因缓冲区溢出丢失数据。这些特性决定了我们在设计串口数据处理方案时需要特别关注数据完整性和实时性。2. 轮询方式处理串口数据2.1 基本原理与实现轮询是最直接的串口数据处理方式其核心思想是通过主循环不断检查串口接收寄存器状态。当检测到有新数据到达时立即读取并处理。这种方式实现简单适合对实时性要求不高的场景。在STM32 HAL库环境下典型的轮询实现代码如下while(1) { if(HAL_UART_Receive(huart1, rx_data, 1, 100) HAL_OK) { // 处理接收到的rx_data process_data(rx_data); } // 其他任务处理 other_tasks(); }这段代码会阻塞等待最多100ms如果期间收到数据就立即处理否则超时后继续执行其他任务。在实际项目中我通常会将超时时间设置为0实现非阻塞轮询while(1) { if(HAL_UART_Receive(huart1, rx_data, 1, 0) HAL_OK) { process_data(rx_data); } // 其他任务可以及时得到执行 other_tasks(); }2.2 优缺点分析与适用场景轮询方式的优势在于实现简单不需要复杂的中断配置对RAM需求小不需要大缓冲区调试方便数据流清晰可见但缺点也很明显CPU利用率高频繁查询浪费资源实时性差可能丢失高速数据难以处理突发的大量数据根据我的经验轮询方式适合以下场景低波特率通信9600bps非实时性要求的调试输出资源极其有限的8位MCU系统简单的命令行交互界面提示使用轮询方式时务必确保主循环执行时间短于单个字节的传输时间波特率倒数否则会丢失数据。例如115200bps下一个字节传输时间约87μs。3. 中断驱动方式处理串口数据3.1 中断机制工作原理中断方式利用MCU的中断控制器在串口接收到数据时自动触发中断服务程序(ISR)。这种方式解放了CPU使其可以专注于其他任务只在有数据到达时才被中断处理。以STM32为例配置串口接收中断的基本步骤在CubeMX中使能UART全局中断实现HAL_UART_RxCpltCallback回调函数启动中断接收HAL_UART_Receive_IT(huart1, rx_data, 1);当接收到一个字节后HAL库会自动调用弱定义的HAL_UART_RxCpltCallback我们需要重写它void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart-Instance USART1) { process_data(rx_data); // 重新启用中断接收 HAL_UART_Receive_IT(huart1, rx_data, 1); } }3.2 环形缓冲区实现单纯的中断接收每个字节都触发处理效率不高更常见的做法是结合环形缓冲区。我在多个项目中都采用了这种方案#define BUF_SIZE 256 uint8_t rx_buf[BUF_SIZE]; volatile uint16_t head 0, tail 0; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart-Instance USART1) { rx_buf[head] rx_data; if(head BUF_SIZE) head 0; HAL_UART_Receive_IT(huart1, rx_data, 1); } } // 主循环中处理缓冲区数据 void process_buffer() { while(head ! tail) { process_data(rx_buf[tail]); if(tail BUF_SIZE) tail 0; } }3.3 性能优化与注意事项通过多年实践我总结了中断方式的几个优化点中断优先级设置串口中断优先级不宜过高避免影响关键时序任务DMA配合使用对于高速通信考虑使用DMA减轻CPU负担缓冲区大小选择根据波特率和处理速度缓冲区应能容纳至少100ms的数据量临界区保护多线程访问缓冲区时需要关中断或使用互斥锁常见问题排查数据丢失检查中断优先级是否被其他高优先级中断抢占数据错乱确认缓冲区索引变量的volatile修饰系统卡死避免在ISR中进行耗时操作或打印调试信息4. DMA方式处理串口数据4.1 DMA工作原理与配置DMA直接内存访问控制器可以在不占用CPU资源的情况下自动将串口接收到的数据搬运到指定内存区域。这种方式特别适合高速、大数据量的串口通信。STM32的DMA串口接收配置示例#define DMA_BUF_SIZE 256 uint8_t dma_rx_buf[DMA_BUF_SIZE]; // CubeMX中配置UART DMA接收 HAL_UART_Receive_DMA(huart1, dma_rx_buf, DMA_BUF_SIZE);DMA接收完成后会触发中断我们可以处理数据void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart-Instance USART1) { // 处理接收到的数据 process_dma_data(dma_rx_buf, DMA_BUF_SIZE); // 重新启动DMA接收 HAL_UART_Receive_DMA(huart1, dma_rx_buf, DMA_BUF_SIZE); } }4.2 双缓冲区技术为避免处理数据时错过新数据我常使用双缓冲区方案uint8_t dma_buf1[DMA_BUF_SIZE], dma_buf2[DMA_BUF_SIZE]; volatile uint8_t *active_buf dma_buf1; void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if(huart-Instance USART1) { uint8_t *processed_buf (active_buf dma_buf1) ? dma_buf2 : dma_buf1; process_dma_data(processed_buf, DMA_BUF_SIZE); active_buf processed_buf; HAL_UART_Receive_DMA(huart1, active_buf, DMA_BUF_SIZE); } }4.3 DMA接收长度检测技巧DMA的一个挑战是如何确定实际接收的数据长度。有几种常用方法空闲中断检测配置串口空闲中断配合DMA传输计数器特定帧结束符如换行符、自定义协议尾定时器超时在一定时间内没有新数据认为帧结束空闲中断实现示例// 启用空闲中断 __HAL_UART_ENABLE_IT(huart1, UART_IT_IDLE); // 中断处理 void USART1_IRQHandler() { if(__HAL_UART_GET_FLAG(huart1, UART_FLAG_IDLE)) { __HAL_UART_CLEAR_IDLEFLAG(huart1); uint16_t len DMA_BUF_SIZE - __HAL_DMA_GET_COUNTER(hdma_usart1_rx); process_dma_data(dma_rx_buf, len); HAL_UART_Receive_DMA(huart1, dma_rx_buf, DMA_BUF_SIZE); } HAL_UART_IRQHandler(huart1); }5. 协议解析与数据处理技巧5.1 常见串口协议格式在实际项目中串口数据通常遵循特定协议格式。我处理过的常见格式包括简单文本协议ASCII字符换行符结束如NMEA 0183$GPGGA,123519,4807.038,N,01131.000,E,1,08,0.9,545.4,M,46.9,M,,*47\r\n二进制固定长度协议固定头长度数据校验#pragma pack(1) typedef struct { uint8_t header; // 0xAA uint8_t cmd; uint16_t len; uint8_t data[256]; uint16_t crc; } BinaryProtocol;Modbus RTU地址功能码数据CRC[地址][功能码][数据长度][数据][CRC16]5.2 状态机解析实现对于复杂协议我推荐使用状态机实现解析器。以下是一个简单状态机示例typedef enum { STATE_HEADER1, STATE_HEADER2, STATE_LENGTH, STATE_DATA, STATE_CRC } ParserState; ParserState state STATE_HEADER1; uint8_t rx_len, rx_cnt; uint8_t rx_packet[256]; void parse_byte(uint8_t byte) { static uint16_t calc_crc; switch(state) { case STATE_HEADER1: if(byte 0xAA) state STATE_HEADER2; break; case STATE_HEADER2: if(byte 0x55) state STATE_LENGTH; else state STATE_HEADER1; break; case STATE_LENGTH: rx_len byte; rx_cnt 0; calc_crc crc16_init(); calc_crc crc16_update(calc_crc, 0xAA); calc_crc crc16_update(calc_crc, 0x55); calc_crc crc16_update(calc_crc, rx_len); state STATE_DATA; break; case STATE_DATA: rx_packet[rx_cnt] byte; calc_crc crc16_update(calc_crc, byte); if(rx_cnt rx_len) state STATE_CRC; break; case STATE_CRC: if(calc_crc (byte 8 | rx_packet[rx_len-1])) { process_packet(rx_packet, rx_len-1); } state STATE_HEADER1; break; } }5.3 数据校验方法选择数据校验是确保通信可靠性的关键。根据项目需求我常用的校验方法有累加和校验简单快速适合低要求场景uint8_t checksum(uint8_t *data, uint8_t len) { uint8_t sum 0; for(int i0; ilen; i) sum data[i]; return sum; }CRC校验可靠性高推荐CRC16-CCITT或CRC32uint16_t crc16(uint8_t *data, uint16_t len) { uint16_t crc 0xFFFF; for(uint16_t i0; ilen; i) { crc ^ data[i]; for(uint8_t j0; j8; j) { if(crc 1) crc (crc 1) ^ 0xA001; else crc 1; } } return crc; }异或校验介于累加和和CRC之间uint8_t xor_check(uint8_t *data, uint8_t len) { uint8_t result 0; for(int i0; ilen; i) result ^ data[i]; return result; }6. 实际项目经验分享6.1 工业环境下的抗干扰设计在工业现场串口通信容易受到各种干扰。通过多个项目积累我总结了以下抗干扰措施硬件层面使用RS485代替TTL电平增加差分传输抗干扰能力添加TVS二极管防护静电和浪涌采用屏蔽双绞线屏蔽层单点接地在长距离传输时增加终端电阻软件层面提高通信波特率干扰持续时间相对变短增加数据包重传机制采用更严格的校验方式如CRC32实现超时重发和链路自检功能6.2 多串口管理策略在需要管理多个串口的系统中如网关设备我通常采用以下架构统一接口抽象typedef struct { UART_HandleTypeDef *huart; uint8_t (*process)(uint8_t *data, uint16_t len); uint8_t rx_buf[256]; // 其他状态变量 } UART_Device;事件驱动处理void uart_event_handler(UART_Device *dev) { if(dev-huart-ErrorCode ! HAL_UART_ERROR_NONE) { handle_uart_error(dev); return; } uint16_t len get_received_length(dev); if(len 0) { if(dev-process(dev-rx_buf, len) ! 0) { // 处理失败 } } }资源分配策略高优先级通道使用DMA空闲中断低优先级通道使用普通中断环形缓冲区根据波特率动态调整缓冲区大小6.3 调试技巧与工具推荐串口调试过程中我积累了一些实用技巧逻辑分析仪抓包使用Saleae或DSView分析信号质量和时序数据可视化工具Tera Term支持宏脚本和日志记录Docklight强大的协议分析和脚本测试功能CoolTerm跨平台简单易用自定义调试协议设计包含时间戳、通道信息的调试帧格式压力测试方法使用Python脚本生成随机测试数据逐步提高发送速率直到出现错误统计误码率和丢包率注意在调试DMA接收时常见问题是忘记重新启动DMA传输。我习惯在错误回调中也加入重启逻辑void HAL_UART_ErrorCallback(UART_HandleTypeDef *huart) { if(huart-Instance USART1) { HAL_UART_Receive_DMA(huart, dma_rx_buf, DMA_BUF_SIZE); } }7. 性能测试与优化案例7.1 不同方式的性能对比我曾在一个项目中系统测试过各种接收方式的性能测试条件STM32F407 168MHz波特率115200bps持续发送1000字节数据包测试结果接收方式CPU占用率最高可靠波特率丢包率(115200)轮询95%230400bps0%中断(单字节)45%921600bps0%中断环形缓冲15%1.5Mbps0%DMA(单缓冲)5%3Mbps0.1%DMA双缓冲5%3Mbps0%7.2 内存优化实践在资源受限的STM32F10320K RAM项目中我通过以下策略优化内存使用动态缓冲区分配根据当前波特率动态调整缓冲区大小uint16_t get_optimal_buf_size(uint32_t baudrate) { // 按100ms数据量计算最小64字节 uint16_t size baudrate / 10 / 10; // 波特率/(10位/字节)/10 return size 64 ? 64 : (size 256 ? 256 : size); }共用内存池多个串口共享同一内存区域#define MEM_POOL_SIZE 512 uint8_t mem_pool[MEM_POOL_SIZE]; void init_uart_buffers() { uart1.rx_buf mem_pool[0]; uart1.buf_size 256; uart2.rx_buf mem_pool[256]; uart2.buf_size 256; }压缩协议设计使用紧凑的二进制协议代替文本协议7.3 低功耗设计技巧对于电池供电设备串口通信的功耗优化很重要自动波特率检测避免持续接收消耗功率硬件流控使用通过RTS/CTS控制数据流减少无效接收DMA唤醒机制配合低功耗模式收到数据后唤醒CPU间歇工作模式定时开启串口接收其余时间关闭实现示例void enter_low_power_mode() { // 关闭串口接收 HAL_UART_DeInit(huart1); // 配置唤醒源为UART RX引脚 HAL_PWR_EnableWakeUpPin(PWR_WAKEUP_PIN1); // 进入STOP模式 HAL_PWR_EnterSTOPMode(PWR_LOWPOWERREGULATOR_ON, PWR_STOPENTRY_WFI); // 唤醒后重新初始化 SystemClock_Config(); MX_USART1_UART_Init(); HAL_UART_Receive_DMA(huart1, dma_rx_buf, DMA_BUF_SIZE); }

更多文章