树莓派与STM32串口通信实战:从接线到协议解析

发布时间:2026/10/20 7:53:40
树莓派与STM32串口通信实战:从接线到协议解析 1. 先捋清楚树莓派和STM32为什么要凑到同一条串口线上做嵌入式的人多少都有一点点“看不上”串口协议的心理觉得它老、慢、还要自己定格式。可我这两年做得最多的系统恰恰就是树莓派和STM32之间靠一根串口线把整个架构串起来的。树莓派跑Linux擅长处理图像、网络、人机交互这些“重脑力活”STM32裸机或者跑个小型实时内核擅长做电机控制、传感器采集、信号处理这些“手速活”。两个芯片凑到一起不是炫技而是各自负责自己最擅长的那一段最后用一条UART线当“对话管道”。那问题来了为什么偏偏是串口而不是I2C、SPI、USB或者以太网我见过不少新手一上来就想着用USB或者Wi-Fi结果把简单问题复杂化了。串口的优势在于“简单、通用、点对点”只需要两个信号脚一根地线就可以把一个完整的数据包从决策层送到执行层还特别容易调试。I2C和SPI虽然速度更快、抗干扰能力也不错但要么需要额外的片选、时钟线要么驱动层里还牵扯主从时序在Linux系统里配置起来远没有串口那么直接。USB的驱动栈复杂得让人头大以太网虽然稳定但成本高、体积大在板级通信里属于杀鸡用牛刀。我在模拟项目X里就是这么干的树莓派负责识别和路径规划识别结果通过串口打包发给STM32STM32收到指令后立刻去驱动电机并且回传当前速度、里程和故障码。整条链路的数据量其实非常小一秒钟几百字节就足够但要求响应快、可追踪、不能因为偶尔丢一个字节就乱套。这种场景下串口几乎是唯一一个“逻辑最简单、排查最直接”的选项。1.1 树莓派和STM32各自适合管哪一头先明确分工后面做协议的时候才能知道谁发命令、谁回状态。我自己的习惯是这样划分的树莓派负责视觉识别、路径决策、远程指令解析、日志上传、人机界面。它本质上是一个“会思考的调度员”。STM32负责定时采样、闭环控制、PWM输出、紧急停车的硬件保护、以及所有需要“毫秒级响应”的外设操作。它是个“手快脚快的执行员”。串口管道负责把树莓派的高层指令翻译成STM32能执行的底层动作把STM32采到的状态反馈给树莓派做决策。1.2 为什么最终选了串口而不是其他接口我在做方案选型的时候列过一张对比表每次写到文档里都觉得特别直观放这里给大家参考接口连线复杂度软件栈实时性适合场景串口UART两线地线简单Linux直接读文件中规中矩点对点小数据量I2C两线但主从地址麻烦中需要处理从机时序一般板内短距离传感器SPI至少四线中片选逻辑要自己管高高速数据采集USB四线协议复杂很复杂驱动适配头痛中大块数据/外设扩展以太网至少四线还要变压器较复杂高多机互联和远程管理从这张表能看出来如果只是树莓派和STM32两片芯片之间的“内部对话”串口的性价比是最高的。尤其要调试的时候随便拿一个USB转TTL的小板子接到树莓派上就能用电脑旁路监听整个通信过程这比I2C和SPI那些时序问题直观太多了。2. 硬件接线电平、交叉、共地这三件事别想当然很多第一次做树莓派和STM32联调的朋友栽的第一个跟头就是接线。这不是夸张。市面上各种开发板的引脚定义五花八门树莓派那排40针的丝印又不是每一个都有标注而STM32板子上往往还要自己看原理图确认。去年我帮A同学排查了半天最后发现他把TX和TX直接连在了一起——两边的发送脚碰头数据自然哪里都到不了。所以接线这块我真的建议当回事来记。2.1 先确认电平树莓派和STM32的TTL电平到底能不能直连树莓派的GPIO串口电平是3.3V绝大多数STM32开发板也是3.3V电平所以两者之间一般可以直接接不需要额外加电平转换芯片。但这里有两个坑需要分辨清楚第一如果你用的是某些“一键下载”小板子或者带有5V参考电平的转换模块输出电平可能不是3.3V而是5V那直连树莓派GPIO就有烧毁IO的风险。稳妥的做法是先用电压表或者逻辑分析仪量一下空闲电平3.3V左右才放心接。第二千万不要把STM32板子上的5V供电引脚当成信号直接接到树莓派GPIO两者的“高电平标准”完全不同。我见过有人抱着侥幸心理接上去的结果树莓派那个GPIO之后就一直不稳定后来一查内部已经受了不可逆损伤。稳妥起见宁可多做一根地线、多量一次电压也不要冒这个险。2.2 物理接线顺序TX接RXRX接TX地线必须共地接线规则其实就一句发送接接收接收接发送最后一定要共地。如果用树莓派的GPIO14和GPIO15作为串口那么树莓派GPIO14TXD接到STM32的RX引脚树莓派GPIO15RXD接到STM32的TX引脚。共地线则是把树莓派的GND接到STM32上的任何一个GND脚。具体的引脚一般这样选信号树莓派引脚STM32常见默认引脚TXGPIO14物理Pin 8STM32的USART1_TX / PA9RXGPIO15物理Pin 10STM32的USART1_RX / PA10GND任意GND如物理Pin 6任意GND如果你用的是多个串口的STM32型号不一定要用USART1也可以把USART2、USART3的引脚引出来。但第一次联调时我个人建议固定在USART1上因为CubeMX默认生成工程时配置最顺后面排查也容易对上号。还有个小经验连线的杜邦线尽量短最好在15厘米以内超过这个长度波特率一高就容易出现误码和毛刺。特别是两边在同一个实验桌上绕一两圈走线完全没必要。3. 树莓派侧先抢回/dev/serial0的控制权树莓派跑Linux系统这是它功能强大的原因但也是串口被“挪用”的根源。树莓派默认为把UART分配给系统控制台使用。你在显示屏上看到的启动日志、登录提示很可能就是从串口输出出来的。如果不关掉这些系统层面的占用那你的应用代码根本没机会正常读写串口。我还记得第一次调项目的时候用Python打开串口一点反应都没有后来一查串口文件被控制台进程占着双双冲突。这是树莓派侧最常见也最好解决的一个问题。3.1 在系统里把串口控制台关掉把UART让给业务程序树莓派的系统配置文件主要涉及两个地方/boot/config.txt和/boot/cmdline.txt。另外在raspi-config里也可以安全地做一部分配置。我的建议是宁可花时间把配置逐项检查一遍也别只依赖一个工具因为不同固件版本的实现细节有差异。先运行sudo raspi-config进入“Interface Options → Serial Port”。系统会问两个问题是否使用串口登录shell是否启用串口硬件第一次是问系统控制台是否占用串口选NO第二次是问是否启用串口硬件选YES。这样相当于把串口从“登录通道”变成“业务通道”。然后再检查配置文件。打开/boot/cmdline.txt正常情况下这里面有一串内核启动参数其中可能含有consoleserial0,115200这一段就是内核把串口当控制台的元凶。把这一段删掉保留其他参数然后保存。接着在/boot/config.txt里确认有这两行之一enable_uart1 dtoverlaydisable-btenable_uart1是明确把UART打开dtoverlaydisable-bt则是把蓝牙占用的那个UART释放给GPIO串口用。如果不加后面这条在某些树莓派型号上GPIO串口可能会落到一个频率不稳的mini UART上面波特率会随着内核变频发生漂移这就是很多乱码问题的幕后黑手。熟悉一点后你会发现树莓派3和4的串口默认状态可能不一样所以每换一块板子就把这两个配置重头看一遍是最省时间的。3.2 用Python快速验证串口通路是否畅通配置完成后重启系统。检查/dev/serial0这个软链接是否存在指向的是ttyAMA0还是ttyS0。如果指向ttyAMA0表示你用的是上电稳定时钟的PL011 UART波特率比较可靠如果指向ttyS0说明miniuart还在被使用最好回到上一步加上dtoverlaydisable-bt再测试。验证串口最直接的办法是回环测试。找一根杜邦线把树莓派本身的TXD和RXD短接然后运行下面这个Python脚本import serial ser serial.Serial(/dev/serial0, baudrate115200, timeout0.2) ser.write(bhello loopback\r\n) data ser.read(64) print(data) ser.close()如果一切正常你会看到终端打印出bhello loopback\r\n。这一步很重要因为它能证明树莓派侧的内核配置、Python环境和串口硬件都没问题。回环测完以后再跟STM32联调时就能把“树莓派配置错误”这个疑点先排除掉温度大减。4. STM32侧中断接收加状态机才是和树莓派打交道的正确姿势树莓派一侧搞定之后STM32侧就是另一个大门槛。很多初学者在STM32上写串口代码习惯用一个阻塞式的HAL_UART_Transmit发送接收就用HAL_UART_Receive死等。这在两个人面对面聊天时没问题可作为设备通信这种“不知道对方什么时候说话”的场景就显得太过笨拙。一旦树莓派那边发来一串不定长的数据而STM32正在忙别的事情数据就会从缓冲区里悄无声息地消失。4.1 CubeMX里把串口配置好重点打开接收中断我一般用STM32CubeMX做初始化。时钟树先按板子实际晶振配置必要时把主频拉到72MHz或者更高。然后在USART1选项中打开异步模式波特率设为115200数据位8、无校验、停止位1。这组参数要和树莓派完全一致少一位都会满屏乱码。关键的一步是使能USART1的全局中断。在CubeMX里NVIC设置页勾选USART1 global interrupt然后生成代码。这样底层的串口接收才会进入到中断回调机制里。很多人忘了这一步结果代码逻辑很漂亮可中断永远不触发数据就那么躺在寄存器里半天没人管。我第一次踩的时候还以为是哪里芯片坏了后来发现只是NVIC没开这种低级错误真的让人哭笑不得。4.2 收到一个字节就解析一个字节用状态机拆帧串口通信本质上是逐个字节到达的即便树莓派一次写出去8个字节STM32这边也可能因为总线忙、优先级高被打散成好几次中断。所以最稳妥的做法不是一次性接收完所有字节而是每来一个字节就喂给一个状态机去逐步组装。下面这段代码就是我实际项目里一直在用的最小状态机骨架typedef uint8_t RX_STATE; #define STATE_WAIT_HEAD1 0 #define STATE_WAIT_HEAD2 1 #define STATE_READ_LEN 2 #define STATE_READ_CMD 3 #define STATE_READ_DATA 4 #define STATE_READ_SUM 5 typedef struct { RX_STATE state; uint8_t len; uint8_t cmd; uint8_t data[64]; uint8_t count; uint8_t sum; } UartFrame; UartFrame rx_frame {STATE_WAIT_HEAD1, 0, 0, {0}, 0, 0}; void uart_parse_byte(uint8_t byte) { switch (rx_frame.state) { case STATE_WAIT_HEAD1: if (byte 0xAA) rx_frame.state STATE_WAIT_HEAD2; break; case STATE_WAIT_HEAD2: if (byte 0x55) rx_frame.state STATE_READ_LEN; else rx_frame.state STATE_WAIT_HEAD1; break; case STATE_READ_LEN: rx_frame.len byte; rx_frame.count 0; rx_frame.sum byte; rx_frame.state STATE_READ_CMD; break; case STATE_READ_CMD: rx_frame.cmd byte; rx_frame.sum ^ byte; if (rx_frame.len 0) rx_frame.state STATE_READ_SUM; else rx_frame.state STATE_READ_DATA; break; case STATE_READ_DATA: rx_frame.data[rx_frame.count] byte; rx_frame.sum ^ byte; if (rx_frame.count rx_frame.len) rx_frame.state STATE_READ_SUM; break; case STATE_READ_SUM: if (byte rx_frame.sum) { // 一帧完整数据已经到达在这里处理指令 uart_on_frame(rx_frame); } rx_frame.state STATE_WAIT_HEAD1; break; default: rx_frame.state STATE_WAIT_HEAD1; break; } } void HAL_UART_RxCpltCallback(UART_HandleTypeDef *huart) { if (huart-Instance USART1) { extern uint8_t rx_byte; uart_parse_byte(rx_byte); HAL_UART_Receive_IT(huart1, rx_byte, 1); } }初始化的时候只需要调用一次HAL_UART_Receive_IT(huart1, rx_byte, 1)之后每收到一个字节中断就会自动再装填一次。代码看起来不复杂但它把“如何应对不定长数据包”这个核心问题解决了。4.3 发送端也按帧格式来别用printf裸发接收做好之后发送也必须有仪式感。STM32回给树莓派的报文最简单的构造方式是把帧头、长度、命令、数据和校验和拼到同一个数组里一次性发送。这样树莓派那边解析起来也方便不需要在意中间有没有换行、空字符这些文本概念。我写的发送函数大概是这个样子void uart_send_frame(uint8_t cmd, uint8_t *data, uint8_t len) { uint8_t frame[64 5]; uint8_t sum 0; frame[0] 0xAA; frame[1] 0x55; frame[2] len; sum ^ len; frame[3] cmd; sum ^ cmd; for (uint8_t i 0; i len; i) { frame[4 i] data[i]; sum ^ data[i]; } frame[4 len] sum; HAL_UART_Transmit(huart1, frame, len 5, HAL_MAX_DELAY); }需要回传一个“电压值16.5V”之类的数据时只需要把数据塞进数组然后调用这个函数即可。如果只是测试链路我常发一个uart_send_frame(0x01, NULL, 0)来表示“我在线”。收到这样的包树莓派就能准确地知道传感器节点是活着的。5. 通信协议设计帧头、长度、校验位一个都不能省很多人树莓派和STM32调通了就开始简单粗暴地发字符串树莓派发OKSTM32回YES看起来好像没问题。可一旦实际项目跑起来加入多个命令、多段数据、多帧连续发送就会遇到一个很头疼的现象——帧粘连。你发出去两条指令那边却一会儿只能收到一条一会儿多了一截尾巴。这不是什么玄学而是因为你没有定义清晰的“帧边界”。5.1 直接发字符串的坑粘包、乱码、收不全树莓派调用一次ser.write发送两个字符串底层驱动不保证两段数据之间有明显间隔。串口接收端只知道字节流它判断“这一条指令结束了吗”的唯一依据就是你定义的帧结构。如果单纯用\r\n作为结束符那数据内容里一旦恰好出现回车换行就会被截断如果一次发出去的信息稍微长一点接收端不知道什么时候算完缓冲区可能等成半截。我见过一个典型场景树莓派给STM32发“start”字符串结果两秒后又发了一个“stop”STM32只识别到了startstop这样的连续文本解析全部失败。问题的根源就在于接收方根本不知道什么时候该“断句”。所以通信协议本质上是给底层字节流加一层“语法规则”。5.2 推荐的最小二进制帧格式我自己比较常用的最小帧格式是帧头AA 55、长度、命令、数据区、校验和。具体字段含义如下字段长度说明帧头11字节固定0xAA帧头21字节固定0x55长度1字节数据区字节数不含命令字命令1字节例如0x01心跳、0x02查询状态数据N字节实际业务数据校验和1字节长度、命令、数据逐字节异或为什么用异或校验而不是CRC因为这两个芯片之间数据量小、错误率低异或已经足够拦下绝大多数毛刺导致的错误帧而且代码实现只要几行排查起来也简单。如果实际环境电磁干扰很强、要求极高可靠性再升级成CRC16也不迟帧格式可以保持不变只是校验算法替换一下。树莓派侧构造帧的Python代码大概是这样的def build_frame(cmd, data): length len(data) payload bytes([length, cmd]) data checksum 0 for b in payload: checksum ^ b return b\xaa\x55 payload bytes([checksum])STM32侧就是前面那个状态机。两边用同一种规则说话链路从底层到应用层就都能对齐。5.3 心跳包、超时与重发机制协议只解决“格式”还不能解决“对方是不是还活着”的问题。因此还需要一个简单的心跳机制树莓派每隔500毫秒发一条心跳帧命令字0x01数据区为空STM32每收到一条心跳就回一条同类型帧。树莓派如果连续5个周期没收到回包就把这个下位机标记为离线在界面上提示告警。这种方式成本极低但让整个系统有了“感知链路健康”的能力调起来非常省心。此外业务指令一旦发出最好带一个序号字段。比如命令字0x02表示“执行动作”数据区第一个字节是序号0到255STM32处理完成后回传的响应帧里把同一个序号原样带回来。这样树莓派侧一看序号就知道是哪条指令的回执不会把多个异步结果搞混。这套“请求-应答”机制说起来简单但真的能避免后期大量莫名其妙的联动问题。6. 联调排错从完全没反应到稳定跑一天的完整路径等到树莓派和STM32两边的代码都写好了真正精彩的环节才刚开始——联调。第一次上电十有八九不是这边没反应就是那边乱码。这时候千万别急着改代码而是要像医生问诊一样先按症状排查。我把自己平时排查的路径整理成了一套流程跟着走一遍大部分问题都能在十几分钟里定位。6.1 常见症状与根因对照表症状最可能的原因处理建议完全没反应接线错误、树莓派串口被控制台占用、打开的是错误端口号先做单板回环测试并检查/dev/serial0满屏乱码波特率不匹配、mini UART时钟漂移、电平噪声过大统一波特率补上dtoverlaydisable-bt缩短连线能收到但只有一半数据协议长度字段错误、发送缓冲区被拆包加状态机解析长度按帧处理偶尔丢失一帧杜邦线接触不良、中断被高优先级占用重新插拔给线束做固定检查NVIC优先级手碰到线就乱码接地点接触不良检查共地最好让GND就近连接6.2 排查顺序先自测、再旁路、最后靠工具我的排查顺序永远是“自下而上”先确认每一端单独工作正常再检查整条链路。具体步骤大概是这样的树莓派回环测试。TXD短接RXD用Python发一串字节看能不能收到。这一步能排除引脚、驱动、配置问题。STM32端回环。把USART1的TX和RX短接同样在程序里发一字节看能不能触发接收解析。这一步能排除发送或接收硬件问题。正常交叉连接但先只发送一个固定字节比如0xAA。树莓派写、STM32收到后回原样树莓派打印。加入帧格式先发最简单的空数据帧验证帧头、长度、校验和。最后再上完整业务协议。整个过程中minicom是一个特别趁手的树莓派调试工具。用minicom -D /dev/serial0 -b 115200打开串口后可以直接看到原始字节回显虽然它是终端界面看起来有点复古但比任何花哨工具都直接。另外如果手头有逻辑分析仪把树莓派的TXD、RXD分别挂到两路通道就可以看波形。注意捕获时的采样率至少是波特率的八倍以上抓到波形后直接量单位宽度能一眼看出波特率到底对不对。6.3 从最小案例开始逐步加码才是硬道理一次本地跑通容易做到稳定跑一整天才是难点。我的做法是先把系统运行在只有心跳包的模式下挂机几小时看会不会积累错误然后再逐步添加数据刷新频率从每秒1条指令到每秒50条指令每上一个台阶都观察串口缓冲区有没有溢出。实际过程中有一次我以为协议写得毫无瑕疵结果把数据频率从10Hz提到50Hz时STM32那边就出现漏帧最后查出来是接收中断优先级不够高被定时器中断挤掉了。从那以后我的每个系统设计都会专门标注好串口接收中断的优先级并给发送缓冲区留足余量。7. 几个值得长期坚持的串口开发习惯串口通信这东西说到底就是“字节进、字节出”。代码写得再花哨最后拼的还是工程习惯。我在这条路上踩过不少坑也总结了几条非常朴素但实用的习惯。第一每一次调试必须留原始十六进制日志。不要只记录“收到数据”这种人类可读的信息而是把实际收到的原始字节用十六进制打印出来。比如树莓派侧可以写个简单的hexdump函数把ser.read返回的每个字节都打印成两位十六进制。这样做有两大好处一是出问题时能精确判断是数据内容错了还是帧结构错了二是能直观看到有没有多余的空字节、重复字节夹在数据里。第二协议里的每一个命令都要有明确编号和定义不要用字符串“从字面上去猜”。我在项目初期就用Excel或者Markdown表格维护一份命令表命令字、数据区含义、响应格式、超时时间。两边程序同时跟着这份文档走。别看这一步琐碎等代码量涨上来以后它能让维护成本直线下降。第三串口通信程序不要裸奔。哪怕只是写一个十几行的测试脚本也要考虑超时、异常、缓冲区清理这些边角情况。Python的serial.Serial打开以后如果对端没上电写操作不会立刻报错但读操作会一直超时返回空STM32这边也一样如果状态机长期卡在某个状态必须有一个看门狗或者超时清零机制否则一个错误字节就能让后续所有帧全部错位。拿这次项目我自己来说前期因为没有给解析器加超时复位一个跑飞的帧头直接导致后面二十帧全被丢掉。后来我在状态机里加了一个“如果超过50毫秒没收到下一个字节则强制回到等待帧头状态”问题彻底消失。这个思路跟引用看门狗的感觉有点像它不需要多高深的技术却一直是嵌入式通信里最实用的保底手段。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询