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,省去了外接转换模块的麻烦。接线时要注意:
- PA9(USART1_TX)接CH340的RX
- PA10(USART1_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参数配置:波特率、数据位等
- 使能USART
void 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字节(所有数据的累加和)
- 包尾:0xFE
void 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 i=0; i<len; 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_END)
typedef 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米(RS485,9600波特率,带中继器)。
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连接调试终端
关键点是给每个串口分配不同的优先级:
- USART1(WiFi):最高优先级,实时性要求高
- USART2(Zigbee):中等优先级
- 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的设备