USART(串口通信协议)实战:从零构建STM32数据收发系统
1. USART串口通信基础入门第一次接触STM32的USART串口通信时我完全被那些专业术语搞懵了。什么波特率、数据位、停止位听起来就像天书一样。但后来我发现串口通信其实就像两个人用对讲机通话只不过是把声音换成了电信号。最基础的串口通信只需要两根线TX发送和RX接收。想象一下TX就是你的嘴巴RX就是耳朵。当两个设备通信时A设备的TX要接B设备的RX反过来也一样。这个交叉连接的原则我刚开始总是搞反结果数据死活传不过去后来用万用表量了半天才发现问题。电平标准也是个容易踩坑的地方。常见的有TTL电平3.3V/5V和RS232电平±15V。我有个朋友不小心把5V TTL设备直接接到RS232口上结果啪的一声芯片就冒烟了。所以不同电平设备间一定要用转换芯片比如MAX232。2. STM32硬件连接实战2.1 最小系统搭建我用的是STM32F103C8T6最小系统板也就是常说的蓝莓派。这个板子自带USB转串口芯片CH340G省去了外接转换模块的麻烦。接线时要注意PA9USART1_TX接CH340的RXPA10USART1_RX接CH340的TX共地线一定要接不然会出现数据乱码第一次调试时我犯了个低级错误忘记在代码里开启GPIO时钟。结果折腾了半天用示波器一看引脚根本没输出信号。这个教训让我养成了检查时钟配置的习惯。2.2 电平转换方案选型如果需要连接RS232设备我有几个方案实测效果不错MAX3232芯片稳定可靠支持3.0-5.5V供电ADM3202低功耗版本适合电池供电场景USB转串口线直接使用现成模块比如FT232RL特别提醒使用RS485时要注意终端电阻匹配。有次在现场调试通信距离超过50米就丢包后来在总线两端各加了个120Ω电阻就解决了。3. 关键参数配置详解3.1 波特率计算玄机波特率就像两个人说话的语速必须保持一致才能听懂。STM32的波特率计算公式是波特率 fCK / (16 * USARTDIV)其中USARTDIV是个固定点小数整数部分存于USART_BRR[15:4]小数部分存于USART_BRR[3:0]。我常用的几个波特率配置9600适合低速调试115200最常用的调试波特率460800需要高速传输时使用921600极限速度对时钟精度要求高3.2 数据帧格式设计一个完整的数据帧包含起始位1位低电平数据位8或9位校验位可选停止位1/1.5/2位高电平校验方式我推荐偶校验能检测单bit错误。曾经有个项目因为电磁干扰导致数据出错加上校验后问题立即显现出来。4. 驱动代码编写实战4.1 初始化流程标准的初始化步骤开启时钟包括USART和GPIO时钟GPIO配置TX设为复用推挽输出RX设为上拉输入USART参数配置波特率、数据位等使能USARTvoid USART1_Init(uint32_t baudrate) { // 1. 开启时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // 2. GPIO配置 GPIO_InitTypeDef GPIO_InitStruct; GPIO_InitStruct.GPIO_Pin GPIO_Pin_9; // TX GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStruct.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStruct); GPIO_InitStruct.GPIO_Pin GPIO_Pin_10; // RX GPIO_InitStruct.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStruct); // 3. USART配置 USART_InitTypeDef USART_InitStruct; USART_InitStruct.USART_BaudRate baudrate; USART_InitStruct.USART_WordLength USART_WordLength_8b; USART_InitStruct.USART_StopBits USART_StopBits_1; USART_InitStruct.USART_Parity USART_Parity_No; USART_InitStruct.USART_Mode USART_Mode_Tx | USART_Mode_Rx; USART_InitStruct.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_Init(USART1, USART_InitStruct); // 4. 使能 USART_Cmd(USART1, ENABLE); }4.2 中断接收实现查询方式会占用CPU资源我推荐使用中断接收// 在初始化中添加 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); NVIC_EnableIRQ(USART1_IRQn); // 中断服务函数 void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART1); // 处理接收到的数据 USART_ClearITPendingBit(USART1, USART_IT_RXNE); } }5. 数据包协议设计5.1 HEX数据包实现我常用的HEX数据包格式包头0xFF数据长度1字节有效载荷N字节校验和1字节所有数据的累加和包尾0xFEvoid Send_HEX_Packet(uint8_t *data, uint8_t len) { uint8_t checksum 0; USART_SendData(USART1, 0xFF); // 包头 while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, len); // 长度 while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); checksum len; for(int i0; ilen; i) // 数据 { USART_SendData(USART1, data[i]); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); checksum data[i]; } USART_SendData(USART1, checksum); // 校验和 while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); USART_SendData(USART1, 0xFE); // 包尾 while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); }5.2 文本协议设计对于人机交互文本协议更友好。我常用的格式$CMD,param1,param2,...,paramN*\r\n例如$SETTEMP,25.5*\r\n解析时要注意字符串处理我推荐使用sscanf函数char buffer[64]; if(sscanf(buffer, $SETTEMP,%f*, temp) 1) { // 成功解析温度值 }6. 状态机实现可靠通信6.1 状态机设计思路状态机是解决通信协议解析的利器。我通常定义三个状态等待包头STATE_IDLE接收数据STATE_RECEIVING校验包尾STATE_CHECK_ENDtypedef enum { STATE_IDLE, STATE_RECEIVING, STATE_CHECK_END } UART_State; UART_State rx_state STATE_IDLE; uint8_t rx_buffer[64]; uint8_t rx_index 0; uint8_t expected_length 0; void Process_UART_Byte(uint8_t data) { switch(rx_state) { case STATE_IDLE: if(data 0xFF) // 检测到包头 { rx_state STATE_RECEIVING; rx_index 0; } break; case STATE_RECEIVING: rx_buffer[rx_index] data; if(rx_index expected_length) { rx_state STATE_CHECK_END; } break; case STATE_CHECK_END: if(data 0xFE) // 检测到包尾 { // 处理完整数据包 Process_Packet(rx_buffer, rx_index); } rx_state STATE_IDLE; break; } }6.2 超时机制实现为防止半包问题我增加了超时判断uint32_t last_rx_time 0; void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART1); last_rx_time HAL_GetTick(); Process_UART_Byte(data); USART_ClearITPendingBit(USART1, USART_IT_RXNE); } } void Check_Timeout(void) { if((rx_state ! STATE_IDLE) (HAL_GetTick() - last_rx_time 100)) // 100ms超时 { rx_state STATE_IDLE; // 重置状态 // 可以记录超时错误 } }7. 常见问题排查指南7.1 数据乱码问题遇到数据乱码时我通常这样排查检查波特率双方必须完全一致检查时钟配置特别是外部晶振频率设置检查电平匹配TTL和RS232不能直接连接检查接地共地不良会导致信号畸变有次遇到每隔几个字节就出错的情况最后发现是电源纹波太大在VDD和地之间加了个100nF电容就解决了。7.2 通信距离限制延长通信距离的实用技巧降低波特率9600比115200传得更远使用RS485差分信号抗干扰能力强添加终端电阻匹配阻抗减少反射使用屏蔽双绞线抑制电磁干扰在工业现场我见过最远的可靠通信距离是1200米RS4859600波特率带中继器。8. 性能优化技巧8.1 DMA传输应用大数据量传输时一定要用DMA。配置步骤开启DMA时钟配置DMA通道绑定USART和DMA启动传输void USART1_DMA_Init(void) { // 1. 开启DMA时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 2. 配置DMA DMA_InitTypeDef DMA_InitStruct; DMA_InitStruct.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStruct.DMA_MemoryBaseAddr (uint32_t)tx_buffer; DMA_InitStruct.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStruct.DMA_BufferSize 0; DMA_InitStruct.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStruct.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStruct.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStruct.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStruct.DMA_Mode DMA_Mode_Normal; DMA_InitStruct.DMA_Priority DMA_Priority_High; DMA_InitStruct.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel4, DMA_InitStruct); // 3. 绑定USART和DMA USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); } void USART1_Send_DMA(uint8_t *data, uint16_t len) { while(DMA_GetCmdStatus(DMA1_Channel4) ENABLE); // 等待上次传输完成 DMA_Cmd(DMA1_Channel4, DISABLE); DMA_SetCurrDataCounter(DMA1_Channel4, len); DMA1_Channel4-CMAR (uint32_t)data; DMA_Cmd(DMA1_Channel4, ENABLE); }8.2 环形缓冲区实现为避免数据丢失我实现了环形缓冲区#define BUF_SIZE 256 typedef struct { uint8_t buffer[BUF_SIZE]; uint16_t head; uint16_t tail; } RingBuffer; RingBuffer rx_buf {0}; void RingBuf_Put(uint8_t data) { uint16_t next (rx_buf.head 1) % BUF_SIZE; if(next ! rx_buf.tail) // 缓冲区未满 { rx_buf.buffer[rx_buf.head] data; rx_buf.head next; } } uint8_t RingBuf_Get(uint8_t *data) { if(rx_buf.head rx_buf.tail) // 缓冲区空 return 0; *data rx_buf.buffer[rx_buf.tail]; rx_buf.tail (rx_buf.tail 1) % BUF_SIZE; return 1; }9. 实际项目经验分享在智能家居项目中我需要用串口同时与多个设备通信。解决方案是使用USART1连接WiFi模块使用USART2连接Zigbee协调器使用USART3连接调试终端关键点是给每个串口分配不同的优先级USART1WiFi最高优先级实时性要求高USART2Zigbee中等优先级USART3调试最低优先级void NVIC_Configuration(void) { NVIC_InitTypeDef NVIC_InitStruct; // USART1中断配置最高优先级 NVIC_InitStruct.NVIC_IRQChannel USART1_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority 0; NVIC_InitStruct.NVIC_IRQChannelSubPriority 0; NVIC_InitStruct.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStruct); // USART2中断配置 NVIC_InitStruct.NVIC_IRQChannel USART2_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority 1; NVIC_Init(NVIC_InitStruct); // USART3中断配置最低优先级 NVIC_InitStruct.NVIC_IRQChannel USART3_IRQn; NVIC_InitStruct.NVIC_IRQChannelPreemptionPriority 2; NVIC_Init(NVIC_InitStruct); }10. 进阶功能探索10.1 硬件流控制实战当通信速率超过115200时建议启用硬件流控制RTS/CTS。配置步骤使能USART的硬件流控制功能配置RTS和CTS引脚在设备端也启用流控制// 修改USART初始化 USART_InitStruct.USART_HardwareFlowControl USART_HardwareFlowControl_RTS_CTS; // 配置RTS/CTS引脚 GPIO_InitStruct.GPIO_Pin GPIO_Pin_11; // CTS GPIO_InitStruct.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStruct); GPIO_InitStruct.GPIO_Pin GPIO_Pin_12; // RTS GPIO_InitStruct.GPIO_Mode GPIO_Mode_AF_PP; GPIO_Init(GPIOA, GPIO_InitStruct);10.2 多机通信实现STM32的USART支持多机通信模式通过地址匹配实现设备筛选设置USART为多机通信模式配置设备地址发送地址帧唤醒目标设备USART_InitStruct.USART_Mode USART_Mode_Tx | USART_Mode_Rx; USART_InitStruct.USART_WordLength USART_WordLength_9b; // 9位数据模式 USART_Init(USART1, USART_InitStruct); // 设置本机地址 USART_SetAddress(USART1, 0x02); USART_WakeUpConfig(USART1, USART_WakeUp_AddressMark); // 发送地址帧第9位为1表示地址 USART_SendData(USART1, 0x01 | 0x100); // 唤醒地址0x01的设备