1. 串口通信基础与STM32F1标准库配置在嵌入式开发中串口通信是最基础也最常用的通信方式之一。STM32F1系列MCU内置了多个USART外设通过标准库可以快速实现串口通信功能。我刚开始接触STM32时最头疼的就是串口配置后来发现只要掌握几个关键点其实并不复杂。首先需要了解的是USART和UART的区别。USART通用同步异步收发器相比UART多了同步通信功能但在异步模式下两者使用方式基本相同。STM32F103C8T6的USART1引脚固定为PA9TX和PA10RX这个硬件设计让我们省去了引脚映射的烦恼。标准库配置串口主要分为五个步骤开启时钟USART1属于APB2总线GPIOA也是APB2设备GPIO初始化TX配置为复用推挽输出RX配置为上拉输入USART参数设置包括波特率、数据位、停止位等中断配置如果需要接收数据使能USART这里有个容易踩坑的地方波特率计算。标准库会自动根据系统时钟计算分频系数但前提是系统时钟配置正确。我曾经因为忘记修改系统时钟配置导致实际波特率与设定值偏差过大而无法通信。void Serial_Init(void) { // 开启时钟 RCC_APB2PeriphClockCmd(RCC_APB2Periph_USART1 | RCC_APB2Periph_GPIOA, ENABLE); // GPIO初始化 GPIO_InitTypeDef GPIO_InitStructure; GPIO_InitStructure.GPIO_Pin GPIO_Pin_9; GPIO_InitStructure.GPIO_Mode GPIO_Mode_AF_PP; GPIO_InitStructure.GPIO_Speed GPIO_Speed_50MHz; GPIO_Init(GPIOA, GPIO_InitStructure); GPIO_InitStructure.GPIO_Pin GPIO_Pin_10; GPIO_InitStructure.GPIO_Mode GPIO_Mode_IPU; GPIO_Init(GPIOA, GPIO_InitStructure); // USART初始化 USART_InitTypeDef USART_InitStructure; USART_InitStructure.USART_BaudRate 115200; USART_InitStructure.USART_WordLength USART_WordLength_8b; USART_InitStructure.USART_StopBits USART_StopBits_1; USART_InitStructure.USART_Parity USART_Parity_No; USART_InitStructure.USART_Mode USART_Mode_Tx | USART_Mode_Rx; USART_InitStructure.USART_HardwareFlowControl USART_HardwareFlowControl_None; USART_Init(USART1, USART_InitStructure); // 使能USART USART_Cmd(USART1, ENABLE); }2. 串口数据收发基础实现实现基础收发功能是串口通信的第一步。发送数据相对简单直接调用USART_SendData函数即可。但要注意的是每次发送前需要检查发送寄存器是否为空否则可能导致数据丢失。接收数据则有两种方式查询式和中断式。查询式适合简单应用但在实际项目中中断接收才是更实用的方案。我第一次用查询方式接收数据时就遇到了数据丢失的问题因为主循环处理其他任务时可能错过串口数据。中断接收需要配置NVIC这里有个细节STM32的中断优先级分组需要先设置。我建议使用NVIC_PriorityGroup_2这样可以灵活配置抢占优先级和子优先级。// 中断配置 USART_ITConfig(USART1, USART_IT_RXNE, ENABLE); NVIC_InitTypeDef NVIC_InitStructure; NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); NVIC_InitStructure.NVIC_IRQChannel USART1_IRQn; NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority 1; NVIC_InitStructure.NVIC_IRQChannelSubPriority 1; NVIC_InitStructure.NVIC_IRQChannelCmd ENABLE; NVIC_Init(NVIC_InitStructure); // 中断服务函数 void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_RXNE) ! RESET) { uint8_t data USART_ReceiveData(USART1); // 处理接收到的数据 USART_ClearITPendingBit(USART1, USART_IT_RXNE); } }在实际项目中我通常会封装一些实用的发送函数比如发送字符串、数组、数字等。这样可以提高代码复用性。特别有用的是重定向printf函数通过重写fputc就能直接使用printf进行格式化输出调试时非常方便。// 重定向printf int fputc(int ch, FILE *f) { USART_SendData(USART1, (uint8_t)ch); while(USART_GetFlagStatus(USART1, USART_FLAG_TXE) RESET); return ch; }3. 数据包格式设计与实现单字节通信在实际项目中很少使用大多数情况下我们需要传输结构化数据。这就涉及到数据包的设计。我经历过因为数据包设计不合理导致的通信混乱后来才明白好的数据包格式有多重要。常见的数据包格式有两种固定长度和可变长度。固定长度实现简单但不够灵活可变长度更节省带宽但实现复杂些。我建议初学者先从固定长度开始等熟悉了再尝试可变长度。数据包必须包含包头和包尾这是区分不同数据包的关键。但这里有个陷阱如果数据内容恰好和包头包尾相同怎么办我遇到过这种情况导致数据被错误截断。解决方案有三种限制数据范围比如只使用0-127用128-255作为控制字符使用转义字符类似HDLC协议的做法增加包头包尾长度降低冲突概率// HEX数据包示例 void Serial_SendPacket(uint8_t *data, uint16_t len) { USART_SendData(USART1, 0xFF); // 包头 for(int i0; ilen; i) { USART_SendData(USART1, data[i]); } USART_SendData(USART1, 0xFE); // 包尾 }对于文本协议可以使用特定的字符组合作为分隔符比如\r\n。这种方式的优点是直观调试时可以直接看到数据内容。Modbus ASCII模式就是典型的文本协议。4. 状态机在数据包解析中的应用状态机是解析数据包的利器。刚开始我试图用一堆if-else来判断数据包状态结果代码又长又难维护。改用状态机后逻辑清晰了很多。状态机的核心思想是将解析过程分为几个状态根据当前状态和接收到的数据决定下一个状态。典型的串口数据包解析包含以下几个状态等待包头状态接收数据状态等待包尾状态typedef enum { STATE_WAIT_HEADER, STATE_RECEIVING_DATA, STATE_WAIT_FOOTER } ParserState; void USART1_IRQHandler(void) { static ParserState state STATE_WAIT_HEADER; static uint8_t dataIndex 0; uint8_t data USART_ReceiveData(USART1); switch(state) { case STATE_WAIT_HEADER: if(data 0xFF) { state STATE_RECEIVING_DATA; dataIndex 0; } break; case STATE_RECEIVING_DATA: if(dataIndex MAX_PACKET_LEN) { packetBuffer[dataIndex] data; } else { state STATE_WAIT_HEADER; // 数据过长重置状态 } break; case STATE_WAIT_FOOTER: if(data 0xFE) { // 完整数据包接收完成 ProcessPacket(packetBuffer, dataIndex); } state STATE_WAIT_HEADER; break; } USART_ClearITPendingBit(USART1, USART_IT_RXNE); }在实际项目中状态机还需要考虑超时处理。如果长时间没有收到完整数据包应该重置状态机避免因为数据丢失导致通信中断。我曾经因为没做超时处理设备在偶尔丢包后就再也无法正常通信。5. 错误处理与性能优化可靠的串口通信离不开完善的错误处理。STM32的USART提供了多种错误标志位如溢出错误、噪声错误、帧错误等。刚开始我忽略了这些错误处理结果遇到问题时调试非常困难。void USART1_IRQHandler(void) { if(USART_GetITStatus(USART1, USART_IT_ORE) ! RESET) { // 溢出错误处理 USART_ClearITPendingBit(USART1, USART_IT_ORE); USART_ReceiveData(USART1); // 读DR寄存器清除错误 } // 其他错误处理... }性能优化方面DMA是最有效的手段。特别是高速通信或大数据量传输时使用DMA可以大大减轻CPU负担。配置USART DMA需要注意以下几点DMA通道选择要正确内存和外设地址要设置正确传输完成中断处理void DMA_Configuration(void) { DMA_InitTypeDef DMA_InitStructure; // 开启DMA时钟 RCC_AHBPeriphClockCmd(RCC_AHBPeriph_DMA1, ENABLE); // 发送DMA配置 DMA_DeInit(DMA1_Channel4); DMA_InitStructure.DMA_PeripheralBaseAddr (uint32_t)USART1-DR; DMA_InitStructure.DMA_MemoryBaseAddr (uint32_t)txBuffer; DMA_InitStructure.DMA_DIR DMA_DIR_PeripheralDST; DMA_InitStructure.DMA_BufferSize TX_BUFFER_SIZE; DMA_InitStructure.DMA_PeripheralInc DMA_PeripheralInc_Disable; DMA_InitStructure.DMA_MemoryInc DMA_MemoryInc_Enable; DMA_InitStructure.DMA_PeripheralDataSize DMA_PeripheralDataSize_Byte; DMA_InitStructure.DMA_MemoryDataSize DMA_MemoryDataSize_Byte; DMA_InitStructure.DMA_Mode DMA_Mode_Normal; DMA_InitStructure.DMA_Priority DMA_Priority_High; DMA_InitStructure.DMA_M2M DMA_M2M_Disable; DMA_Init(DMA1_Channel4, DMA_InitStructure); USART_DMACmd(USART1, USART_DMAReq_Tx, ENABLE); }另一个优化点是环形缓冲区的使用。对于不稳定的数据流环形缓冲区可以平滑数据接收避免数据丢失。实现环形缓冲区时要注意读写指针的原子操作防止中断和主循环同时修改指针导致错误。6. 实际项目中的串口通信技巧在实际项目中串口通信往往会遇到各种意想不到的问题。这里分享几个我踩过的坑和解决方案波特率误差问题晶振精度不够会导致通信错误。解决方法是用更高精度的晶振或者使用STM32的内部时钟校准功能。电平兼容问题STM32是3.3V电平与5V设备通信时需要电平转换。我常用的是TXS0108E这类双向电平转换芯片。长线传输问题超过1米的传输距离建议使用RS485。我曾经用普通串口连接3米外的设备结果通信极不稳定改用RS485后问题解决。抗干扰处理工业环境中可以在线上加磁环软件上增加数据校验。CRC校验比简单的校验和更可靠。// CRC16计算示例 uint16_t Calc_CRC16(uint8_t *data, uint16_t length) { uint16_t crc 0xFFFF; for(uint16_t i0; ilength; i) { crc ^ data[i]; for(uint8_t j0; j8; j) { if(crc 0x0001) { crc 1; crc ^ 0xA001; } else { crc 1; } } } return crc; }调试技巧方面我习惯用逻辑分析仪抓取波形可以直观看到每个字节的传输情况。对于复杂的通信问题二分法排查很有效先确认物理层是否正常再检查数据链路层最后看应用层协议。7. 高级应用自定义通信协议当项目复杂度增加时可能需要设计自定义通信协议。一个好的协议应该考虑以下几点帧起始和结束标记地址字段多设备时命令或功能码数据长度数据内容校验字段超时重传机制我设计过一个简单的问答式协议包含以下字段包头2字节0xAA55设备地址1字节命令码1字节数据长度1字节数据N字节CRC162字节#pragma pack(push, 1) typedef struct { uint16_t header; uint8_t addr; uint8_t cmd; uint8_t len; uint8_t data[256]; uint16_t crc; } CustomProtocol; #pragma pack(pop)协议设计时还要考虑字节序问题。我建议统一使用小端模式或者显式地处理字节序转换。跨平台通信时这个问题尤其重要。对于需要高速传输的场景可以考虑使用HDLC-like的帧结构加入比特填充和透明传输机制。虽然实现复杂些但可靠性更高。