树莓派与STM32串口通信实战:从硬件连接到协议设计

发布时间:2026/7/31 12:44:37
树莓派与STM32串口通信实战:从硬件连接到协议设计 1. 项目概述为什么我们要折腾串口通信最近在整理工作室的物料翻出来几块吃灰的树莓派4B和STM32的开发板看着它们一个念头就冒出来了能不能让这两个不同“门派”的硬件核心用一种最经典、最底层的方式“聊聊天”于是就有了这个“基于树莓派4B与STM32的UART串口通信实验”。这听起来可能有点“复古”毕竟现在各种高级总线协议满天飞但在我看来串口UART依然是嵌入式开发者的“必修课”和“试金石”。串口通信到底是什么你可以把它想象成两个人用对讲机通话。双方需要事先约定好用哪个频道波特率、什么时候开始说起始位、怎么说数据位、怎么知道说完了停止位以及万一听错了怎么办校验位。UART就是负责这套“对话规则”的硬件模块。它简单、可靠、几乎无处不在从单片机调试、传感器数据读取到设备间的简单控制都离不开它。这个项目的核心目标很明确打通树莓派运行Linux系统与STM32裸机或RTOS环境之间的数据通道。树莓派作为上位机可以发送控制指令、接收传感器数据STM32作为下位机负责执行具体操作、采集信息并回传。我将从硬件连线、通信协议设计、双方代码实现到最后的调试排错完整地走一遍流程。所有代码都会开源你完全可以跟着步骤亲手复现这个通信链路。无论你是想学习嵌入式通信基础还是正在为某个具体项目寻找设备联调方案这个实验都能给你提供扎实的参考。2. 硬件准备与连接别让物理层成为第一个坑动手写代码之前正确的硬件连接是成功的一半。连接错误轻则通信失败重则损坏设备这一步必须仔细。2.1 核心硬件清单你需要准备以下硬件树莓派4B任何版本均可建议安装 Raspberry Pi OS原 Raspbian系统。STM32开发板我这里以最常见的STM32F103C8T6蓝色药丸板为例其他系列如F4、H7原理完全一致只需注意引脚映射。USB转TTL串口模块这是关键桥梁。推荐使用CP2102或CH340芯片的模块稳定性好驱动易找。杜邦线若干用于连接。注意绝对不要直接将树莓派的GPIO引脚3.3V电平与USB转TTL模块的引脚通常是5V电平直接交叉连接必须通过STM32板载的UART转USB电路如果有或电平转换电路进行连接。我们这里采用更通用、更安全的方式树莓派与STM32通过USB转TTL模块间接通信。2.2 安全连接方案详解为什么采用间接连接树莓派的GPIO UART通常指/dev/ttyS0或/dev/serial0默认用于蓝牙控制台直接使用较为复杂且易冲突。而树莓派的USB口非常稳定我们让STM32通过其自带的USB转串口或外接USB转TTL模块变成一个“USB串口设备”然后插入树莓派的USB口。这样在树莓派上STM32就被识别为一个普通的/dev/ttyUSB0或/dev/ttyACM0设备编程模型极其简单。具体连接步骤STM32侧连接找到你STM32开发板上的UART1发送引脚USART1_TX通常是PA9和接收引脚USART1_RX通常是PA10。将STM32的USART1_TX (PA9)连接到USB转TTL模块的RX引脚。将STM32的USART1_RX (PA10)连接到USB转TTL模块的TX引脚。将STM32的GND与USB转TTL模块的GND相连。切记TX接RXRX接TX交叉连接同时确保双方共地。树莓派侧连接将USB转TTL模块通过Micro-USB或USB-A口视模块而定插入树莓派的任意一个USB接口。连接示意图逻辑关系树莓派 USB Port --- USB转TTL模块 --- STM32 USART1 (内部虚拟为ttyUSB0) (TX/RX交叉) (PA9/PA10)这种连接方式的优势在于安全隔离USB接口提供了电气隔离避免了因误操作导致树莓派GPIO损坏的风险。即插即用树莓派系统会自动加载驱动无需额外配置GPIO复用。稳定性高USB通信协议本身有纠错比直接GPIO电平通信更抗干扰。便于调试你还可以将USB转TTL模块插到电脑上用串口助手同时监听数据流方便三方调试。3. 通信协议设计与代码实现解析硬件通路建立后我们需要为数据制定“语法”。没有协议的串口通信就像两个人各说各的方言无法理解。3.1 设计一个简单实用的帧协议直接发送原始字符串如“LED ON”在简单场景下可行但不健壮。我设计一个非常轻量但实用的帧结构包含帧头、数据长度、命令/数据、校验和帧尾。帧格式定义[帧头0xAA] [帧头0x55] [数据长度N] [命令字] [数据区...] [校验和] [帧尾0x0D] [帧尾0x0A]帧头 (2字节)0xAA, 0x55用于标识一帧数据的开始降低误触发概率。数据长度 (1字节)表示从命令字开始到数据区结束的字节数。方便接收方动态解析。命令字 (1字节)定义操作类型例如 0x01 表示控制LED0x02 表示请求传感器数据。数据区 (N-1字节)具体参数。例如控制LED时数据区可为1字节0x00关0x01开。校验和 (1字节)通常为从数据长度到数据区所有字节的累加和取低8位用于验证数据在传输中是否出错。帧尾 (2字节)0x0D, 0x0A\r\n作为结束标志兼容部分终端显示。3.2 STM32下位机代码实现基于HAL库STM32端作为数据解析与执行单元需要可靠地接收、解析并执行命令。我们使用中断状态机的方式这是嵌入式处理串口数据的经典模式。核心代码解析 (usart.c部分)// 定义帧状态 typedef enum { FRAME_IDLE, FRAME_HEADER1, FRAME_HEADER2, FRAME_LENGTH, FRAME_CMD_DATA, FRAME_CHECKSUM, FRAME_TAIL } FrameState_t; FrameState_t frameState FRAME_IDLE; uint8_t rxBuffer[256]; // 接收缓冲区 uint8_t dataLength 0; uint8_t dataIndex 0; uint8_t expectedLength 0; uint8_t calculatedChecksum 0; // 串口中断回调函数 void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { uint8_t rxByte; if(huart-Instance USART1) { rxByte your_received_byte; // 从硬件寄存器读取 switch(frameState) { case FRAME_IDLE: if(rxByte 0xAA) frameState FRAME_HEADER1; break; case FRAME_HEADER1: if(rxByte 0x55) frameState FRAME_HEADER2; else frameState FRAME_IDLE; // 头错误复位状态机 break; case FRAME_HEADER2: dataLength rxByte; expectedLength rxByte; calculatedChecksum rxByte; // 校验和从长度字节开始累加 dataIndex 0; if(dataLength 0 dataLength sizeof(rxBuffer) - 5) { frameState FRAME_CMD_DATA; } else { frameState FRAME_IDLE; // 长度异常复位 } break; case FRAME_CMD_DATA: rxBuffer[dataIndex] rxByte; calculatedChecksum rxByte; if(--expectedLength 0) { frameState FRAME_CHECKSUM; } break; case FRAME_CHECKSUM: if(rxByte calculatedChecksum) { frameState FRAME_TAIL; } else { frameState FRAME_IDLE; // 校验失败丢弃 } break; case FRAME_TAIL: // 通常检查0x0D, 0x0A这里简化处理收到校验和后即认为帧有效 frameState FRAME_IDLE; // **调用命令解析函数** parseAndExecuteCommand(rxBuffer[0], rxBuffer[1], dataLength - 1); break; } // 重新使能接收中断 HAL_UART_Receive_IT(huart1, rxByte, 1); } }命令解析与执行示例void parseAndExecuteCommand(uint8_t cmd, uint8_t* data, uint8_t len) { switch(cmd) { case 0x01: // 控制LED if(len 1) { HAL_GPIO_WritePin(LED_GPIO_Port, LED_Pin, data[0] ? GPIO_PIN_SET : GPIO_PIN_RESET); // 可以回传一个应答帧 sendResponse(0x01, 0x00); // 命令执行成功 } break; case 0x02: // 请求ADC值 { uint16_t adcVal readADC(); uint8_t respData[2] { (adcVal 8) 0xFF, adcVal 0xFF }; sendResponse(0x02, respData, 2); } break; default: sendResponse(0xFF, 0xFF); // 未知命令错误 break; } }实操心得在STM32的中断服务函数或回调函数中处理速度一定要快避免长时间占用中断导致其他任务饿死或数据丢失。像parseAndExecuteCommand这种可能耗时的操作最好只是设置一个标志位然后在主循环中处理。状态机是处理流式协议的神器逻辑清晰易于扩展。3.3 树莓派上位机代码实现Python pyserial树莓派端我们使用Python的pyserial库它封装了底层串口操作简单易用。我们将实现帧的组包、发送、接收与解析。核心代码解析 (raspberry_uart.py部分)import serial import time import struct class UARTCommander: def __init__(self, port/dev/ttyUSB0, baudrate115200): 初始化串口连接 :param port: 串口设备路径可以通过 ls /dev/ttyUSB* 或 ls /dev/ttyACM* 查看 :param baudrate: 波特率必须与STM32端严格一致 try: self.ser serial.Serial(portport, baudratebaudrate, bytesizeserial.EIGHTBITS, parityserial.PARITY_NONE, stopbitsserial.STOPBITS_ONE, timeout1) # 设置读超时 if self.ser.is_open: print(f成功打开串口 {port}) self.ser.flushInput() # 清空输入缓冲区 self.ser.flushOutput() # 清空输出缓冲区 except serial.SerialException as e: print(f打开串口失败: {e}) self.ser None def _calculate_checksum(self, data_bytes): 计算校验和简单累加和取低8位 return sum(data_bytes) 0xFF def pack_frame(self, cmd, dataNone): 根据协议组包 :param cmd: 命令字整数 :param data: 数据部分字节列表或None :return: 打包好的字节串 if data is None: data [] # 数据长度 命令字(1字节) 数据长度 length 1 len(data) # 构建从长度开始到数据结束的部分用于计算校验和 frame_body bytes([length, cmd]) bytes(data) checksum self._calculate_checksum(frame_body) # 组装完整帧 frame b\xAA\x55 frame_body bytes([checksum]) b\x0D\x0A return frame def send_command(self, cmd, dataNone, need_responseTrue, timeout2): 发送命令并可选地等待响应 :return: 如果need_response为True返回解析后的响应数据字典否则返回None if not self.ser or not self.ser.is_open: print(串口未打开) return None frame self.pack_frame(cmd, data) self.ser.write(frame) print(f发送: {frame.hex( ).upper()}) if not need_response: return None # 等待并解析响应 start_time time.time() rx_buffer bytearray() state IDLE while time.time() - start_time timeout: if self.ser.in_waiting 0: rx_byte self.ser.read(1) if not rx_byte: continue rx_buffer.extend(rx_byte) # 简化的接收状态机实际项目应像STM32端一样完整实现 if len(rx_buffer) 6: # 最小帧长度 # 查找帧头 try: idx rx_buffer.find(b\xAA\x55) if idx 0 and len(rx_buffer) idx 6: frame_start idx resp_length rx_buffer[frame_start 2] if len(rx_buffer) frame_start 5 resp_length: # 确保帧完整 frame_end frame_start 5 resp_length full_frame rx_buffer[frame_start:frame_end] # 验证帧尾和校验和此处省略详细验证 # 提取响应数据 resp_cmd full_frame[3] resp_data full_frame[4:-3] # 去掉头、长度、命令、校验和、尾 rx_buffer rx_buffer[frame_end:] # 移除已处理帧 return {cmd: resp_cmd, data: resp_data} except Exception as e: print(f解析响应时出错: {e}) rx_buffer.clear() state IDLE time.sleep(0.01) # 短暂休眠避免CPU空转 print(等待响应超时) return None def close(self): if self.ser and self.ser.is_open: self.ser.close() print(串口已关闭) # 使用示例 if __name__ __main__: commander UARTCommander(/dev/ttyUSB0, 115200) if commander.ser: # 示例1控制LED开 response commander.send_command(0x01, [0x01]) if response: print(f收到响应: 命令{response[cmd]}, 数据{response[data].hex()}) time.sleep(1) # 示例2请求ADC值 response commander.send_command(0x02) if response and len(response[data]) 2: adc_value (response[data][0] 8) | response[data][1] print(fADC值: {adc_value}) commander.close()注意事项树莓派上Python脚本的串口读写是阻塞I/O。send_command函数中的接收循环使用了超时机制避免程序卡死。在生产环境中更推荐使用多线程或异步IO如asynciopyserial-asyncio来处理串口的监听和发送实现真正的全双工通信。4. 系统联调与深度问题排查实录代码写完连接好硬件最激动人心也最折磨人的环节——联调就开始了。这里我分享几个一定会遇到且极具代表性的问题及其排查思路。4.1 通信完全无响应的排查流程这是最常见的情况发送了数据但STM32没反应树莓派也收不到任何回音。第一步确认物理连接电平与交叉再次用万用表或肉眼仔细检查TX-RX是否交叉连接。这是新手最容易犯的错误。共地确保树莓派、USB转TTL模块、STM32三者的GND是连接在一起的。没有共地电平参考点不同通信必然失败。电源确认STM32已正常供电并启动。可以观察其电源指示灯或用户LED是否闪烁。第二步确认串口设备与参数树莓派找设备在树莓派终端执行ls /dev/ttyUSB*和ls /dev/ttyACM*。插入USB转TTL模块前后各执行一次看多出了哪个设备。通常就是/dev/ttyUSB0。确保你的Python脚本中使用的端口号正确。权限问题非root用户可能无法访问串口设备。可以临时用sudo运行脚本或永久将用户加入dialout组sudo usermod -a -G dialout $USER然后注销重新登录生效。波特率等参数百分之百确认树莓派和STM32代码中的波特率、数据位、停止位、校验位完全一致。哪怕一个数字不对通信就是乱码或失败。常用波特率有9600, 115200, 921600等。第三步分段隔离测试这是定位问题的黄金法则。测试STM32发送将STM32程序改为只发送不同断的测试数据如每秒发送一次“Hello”不接收。将USB转TTL模块从树莓派上拔下插到你的Windows/Mac电脑上。用电脑上的串口调试助手如Putty、SecureCRT、Arduino IDE串口监视器打开对应COM口设置相同参数。如果能稳定收到STM32发来的数据证明STM32的发送电路、USB转TTL模块、以及模块到电脑的链路是好的。测试STM32接收将STM32程序改为只接收并回显收到任何数据原样发回。在电脑的串口调试助手中手动发送一些数据。如果调试助手能收到一模一样的回显证明STM32的接收和整个环回链路是好的。测试树莓派发送将USB转TTL模块插回树莓派。在树莓派上可以用一个极简的Python脚本或Shell命令直接发送数据到该串口设备同时电脑端用串口调试助手监听这个USB转TTL模块。例如在树莓派终端执行echo -e \xAA\x55\x01\x01\x01\x03\r\n /dev/ttyUSB0。如果电脑端能收到正确数据证明树莓派的发送链路是好的。通过以上三步基本能锁定问题出在发送端、接收端还是中间的连接/配置上。4.2 数据错乱、丢包或解析失败的排查如果能通信但数据不对问题就进入了“软”层面。1. 波特率偏差与时钟精度问题现象接收到的数据偶尔是乱码特别是高速率如115200以上时。原因分析UART通信对时钟精度有要求。STM32的内部RC振荡器HSI精度可能只有1%在高速率下累积误差可能导致采样点偏移从而误码。树莓派的时钟源通常很准。解决方案降低波特率尝试将波特率降至9600或19200看是否稳定。使用外部晶振为STM32焊接外部高速晶振如8MHz并配置系统时钟使用此外部晶振HSE精度可达几十ppm大幅提升稳定性。校准STM32内部时钟如果必须使用HSI可以尝试通过STM32的时钟校准单元进行微调较复杂。2. 缓冲区溢出与数据覆盖问题现象数据丢失或者解析到的帧不完整。原因分析STM32侧如果接收中断处理函数HAL_UART_RxCpltCallback执行太慢或者没有及时重新使能接收中断可能在处理上一字节时下一字节已经到来并被硬件覆盖丢失。树莓派侧Python脚本的接收循环处理不够快或者pyserial的读缓冲区in_waiting被快速填满后未及时读取。解决方案STM32确保中断回调函数尽可能精简。如果使用HAL库检查是否在回调函数末尾调用了HAL_UART_Receive_IT(huart1, rx_buffer, 1)来重新启动接收。对于高速或大数据量考虑使用DMA直接存储器访问来接收串口数据彻底解放CPU。树莓派增加接收缓冲区的读取频率或者使用更大的缓冲区。在send_command函数中确保在每次发送后有足够的时间和处理逻辑来清空接收缓冲区。3. 协议解析逻辑漏洞问题现象有时能解析正确有时不能对错误数据敏感。原因分析状态机设计有缺陷比如没有处理好帧头识别错误后的状态复位或者在接收数据过程中被意外打断。解决方案增强状态机鲁棒性在每个状态判断时如果收到的字节不符合预期必须立即跳转回FRAME_IDLE并清空相关计数器和缓冲区准备接收下一帧。这是防止“错帧”粘连的关键。添加超时机制在STM32端可以启用一个定时器。从收到第一个帧头开始计时如果在一定时间内如50ms没有收到完整的帧则强制复位状态机到FRAME_IDLE。这可以应对传输中途中断的情况。打印调试信息在STM32端将接收到的原始字节通过另一个串口或切换引脚打印出来与树莓派发送的原始字节进行比对这是最直接的调试手段。4.3 稳定性优化与抗干扰建议当基本通信实现后可以考虑以下优化来提升工业环境下的可靠性电气隔离如果通信双方距离较远超过1米或处在不同电源系统、有电机等干扰源考虑使用隔离型USB转串口模块或光耦对UART信号进行隔离避免地环路干扰损坏设备。增加硬件流控如果数据量非常大可以启用UART的硬件流控RTS/CTS。通过额外的两根线告知对方“我缓冲区满了请暂停发送”防止数据丢失。这需要在代码和硬件连接上都进行配置。软件重传机制在应用层协议上可以增加帧序号和应答机制。树莓派发送一帧后等待STM32返回一个“ACK”确认帧。如果超时未收到则自动重发最多重试N次。这能有效应对偶发的数据包丢失。数据完整性强化将简单的累加和校验升级为CRC循环冗余校验。CRC对于检测突发性多位错误的能力远强于累加和。STM32的硬件CRC外设和Python的binascii.crc32库可以很方便地实现。5. 项目扩展与进阶玩法思考这个基础的串口通信框架搭建好后它就像一个坚固的管道你可以往里注入各种有趣的内容构建更复杂的系统。玩法一打造简易物联网网关让树莓派通过Wi-Fi或以太网连接到互联网STM32连接温湿度传感器如DHT22、光照传感器等。STM32定期采集数据并通过串口发送给树莓派树莓派运行一个Python服务将数据封装成JSON格式通过MQTT协议上传到云平台如阿里云IoT、Home Assistant或者你自己的服务器。这样你就拥有了一个低成本、可高度定制的物联网数据采集节点。玩法二实现固件远程升级OTA这是一个非常实用的进阶功能。基本思路是树莓派从网络下载新的STM32固件文件bin格式然后通过串口使用自定义的升级协议将固件数据分块发送给STM32。STM32端需要实现一个Bootloader程序它负责接收数据写入到指定的Flash区域然后跳转到新程序执行。这需要深入理解STM32的内存映射和启动流程。玩法三多设备总线式通信如果有很多个STM32设备需要与一个树莓派通信可以为每个STM32设置一个唯一的设备地址ID。在通信协议的数据区增加“目标地址”字段。树莓派发送的每一帧都指定目标地址所有设备都能收到但只有地址匹配的设备才会处理并回复。这样就实现了类似Modbus RTU的一主多从通信网络。玩法四图形化上位机控制用Python的Tkinter、PyQt或者更简单的remi库为你的树莓派程序开发一个图形界面。在界面上放置按钮来控制LED用图表实时显示STM32传回的传感器数据曲线。这能让你的项目瞬间变得直观和“高大上”起来。整个项目从硬件连接到软件调试再到稳定性打磨和功能扩展几乎涵盖了嵌入式串口通信的所有核心知识点。我把自己在调试过程中遇到的坑和解决方案都详细记录了下来尤其是那个“状态机复位不彻底导致解析错乱”的问题足足花了我一个下午才定位到。希望这份超详细的总结和开源的代码能帮你少走弯路顺利建立起属于你自己的稳定通信链路。

相关新闻

最新新闻

日新闻

周新闻

月新闻