ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

树莓派Pico串口通信实战:UART编程、调试工具与踩坑指南

树莓派Pico串口通信实战:UART编程、调试工具与踩坑指南 串口通信这个东西搞单片机的人迟早要跟它打交道。树莓派 Pico 也好STM32、ESP32 也好想在设备之间传数据最朴素可靠的方式通常还是 UART。前段时间我拿树莓派 Pico 做了几个小项目从最开始连串口助手都收不到数据到后来能同时挂串口舵机、跟 STM32 互认再拿逻辑分析仪对着波形分析协议这条路走下来踩了不少坑。这篇就把 Pico 的串口硬件特性、MicroPython 下的 UART 编程以及我目前常用的调试工具和排查套路一次讲清楚。内容偏实践适合刚接触 Pico 的新手也适合从 Arduino、STM32 转过来的老朋友。搜索“Pico 串口”时偶尔会翻到 Unity 里报 indexoutofrangeexception 的讨论那说的是 Pico VR 头显的渲染问题跟我们要讲的树莓派 Pico 完全不是一个东西学着学着别被带偏就行。1. Pico 的串口家底UART0、UART1 和那条“假串口”1.1 硬件上到底有几个串口RP2040 芯片内置了 2 个硬件 UART板子丝印上通常把 UART0 默认安排在 GPIO0TX和 GPIO1RXUART1 则默认 GPIO4TX和 GPIO5RX。但这里有个很多新手不知道的特点在 MicroPython 里这两个 UART 的功能引脚并不是锁死的初始化的时候可以随意指定。比如把 UART1 挪到 GPIO8 和 GPIO9完全合法代码照样跑。这个特性在项目里非常救命。GPIO0、GPIO1 往往被各种扩展板和传感器占用比如接了旋转编码器、I2C 屏幕、按键矩阵这时候想在不动硬件的前提下把串口让出来直接改初始化参数就行。RP2040 的引脚复用逻辑可以理解成一个视频矩阵切换器UART 外设是一台信号源GPIO 是多个输出口你想把信号源切到哪个口都行但一条线上不能同时接两台信号源。所以有两个硬性限制同一个 UART 的 TX 和 RX 不能设在同一个引脚两个 UART 之间也不要共用同一个引脚如果真设了MicroPython 通常在初始化时就会直接报 ValueError不会等到运行到一半才暴露。板子丝印上写的 GP0、GP1 只是“默认推荐”不是“唯一选择”。我见过不少人被这个丝印框住以为串口必须用 GPIO0/1结果硬件设计得很别扭。真正的规则很简单只要引脚没被其他外设占用随便安排。1.2 电平3.3V TTL 才是 Pico 的语言Pico 的 GPIO 输出高电平大约是 3.3V低电平接近 0V这叫 3.3V TTL 逻辑电平。串口通信里“电平标准”决定了一个高电平到底是多少伏这个参数定错了波形再漂亮也没用。常见的电平标准主要有这几类TTL 3.3VPico、ESP32、大部分 STM32、CH340 模块多数现代单片机都是这个范围可以直接互联。TTL 5V老 Arduino、一些 5V 单片机、部分 USB 转 TTL 模块在 5V 跳线挡位逻辑高电平是 5V需要谨慎处理。RS232正负电压表示逻辑常见 ±12V是老式电脑串口的标准。Pico 的引脚直接接上去轻则没反应重则烧 GPIO。RS485差分信号A/B 两根线传输适合长距离、强干扰环境必须通过 MAX485 之类的收发器芯片才能接到 Pico。很多新手拿着 USB 转 RS232 的老线去连单片机插上去看半天没反应不是代码不行是电平从一开始就不匹配。反过来虽然 Pico 的 TX 输出到 5V 设备一般勉强能读但 Pico 的 RX 引脚千万不要直接吃 5V 信号因为 RP2040 的 GPIO 不是 5V 耐受引脚。这个问题我放到最后专门讲因为它真的是能烧芯片的级别。1.3 板载 USB 的“假串口”USB CDC把 Pico 用 USB 线连到电脑Windows 设备管理器里会多出一个 COM 口Linux 下则是 /dev/ttyACM0这就是 USB CDC 虚拟串口。这个虚拟串口的职责是跑 MicroPython REPL、接收 print() 输出、下载脚本它不是一个真实的 UART。有个典型误解是既然 Pico 插上电脑就有了 COM 口那我把传感器的 TX 接到 Pico 的 USB 口旁边的排针上是不是就能通信了答案是不行。USB CDC 和 Pin比如 GPIO0没有任何物理联系UART 对外接设备使用的信号在 GPIO 引脚上。想跟外部传感器、另一个单片机通信必须从 GPIO0/1 或 GPIO4/5 之类的引脚牵线出去外面再接 USB 转 TTL 模块或者对方的 UART 引脚。还有一个佐证能帮你理解 CDC 和 UART 的区别USB CDC 下波特率基本是“摆设”你串口助手设置成 9600 还是 115200 都能正常连接因为它是 USB 模拟出来的串口并不真的需要两端进行定时匹配。而真实 UART 双方只要波特率差一点解码出来就是乱码。搞清这两个东西的区别能避开一大堆莫名其妙的“通信失败”。2. MicroPython 下的 UART 开发从打招呼到完整收发协议2.1 构造 UART 的第一个参数坑先看一段最常用的初始化代码from machine import UART, Pin uart0 UART(0, baudrate115200, txPin(0), rxPin(1)) uart1 UART(1, baudrate9600, txPin(4), rxPin(5))第一个参数是 UART 编号0 或 1。后面是波特率、TX 引脚、RX 引脚。有几个细节值得注意tx 和 rx 传的是 Pin 对象不是数字 0 和 1很多人从 Arduino 转过来会在这写错。如果省略 tx、rx 参数比如只写UART(0, 115200)Pico 上会自动用默认引脚。我建议任何时候都显式写出来代码可读性强排查问题的时候一眼就知道串口到底接在哪个脚上。波特率必须两端完全一致。常用的是 9600 和 1152009600 更稳115200 更快。更高比如 460800在好的 USB 转 TTL 模块上没问题但在杜邦线加面包板的场景下很容易出错不推荐。默认的 8N1 即数据位 8、无校验、1 个停止位是串口通信里最常见的配置绝大多数传感器和单片机默认就是它可以先用这个跑通再改。初始化时还可以设置timeout和timeout_char这两个参数直接影响read()和readline()的阻塞行为后面收数据的时候很重要。2.2 发送和接收 API 到底怎么选MicroPython 的 UART 对象提供了几个核心方法每个都有自己的脾性。write(buf)用于发送参数要传 bytes 或 bytearray。发字符串一定要记得编码写成bhello或者hello.encode()直接传字符串会报错。它返回实际发送的字节数。read(nNone)是接收的主力。不传参数时它会把当前缓冲区里所有的字节一次性返回传入数字就最多读 n 个。有个关键行为当缓冲区里没有数据时read()返回None。这看起来没什么但如果你写了data uart0.read()然后直接len(data)程序就会在数据没来的时候崩溃。readline()按行读取读到换行符\n为止适合文本协议。但二进制协议千万别依赖它因为一帧二进制数据里完全可能恰好含有0x0A这个换行符一行还没结束就会被切断。any()返回当前接收缓冲区里有多少字节可用这几乎是所有有效轮询方案的基础。实际项目里最常犯的错是以为read()能一次读回完整的一帧。比如对端一次发了 8 字节你调用read()的时候可能只到了 3 字节于是你拿到 3 字节剩下的 5 字节要等下一轮才能读到。所以应用层必须自己做“组帧”while True: n uart0.any() # 缓冲区里已经有几个字节 if n 8: # 凑够一帧 8 字节再读 frame uart0.read(8) process_frame(frame) # 解析这一帧 time.sleep_ms(20)更通用一点的做法是维护一个累积缓冲区不断把读到的数据追加进去然后按帧头、帧尾、长度字段来切包。这样即使一帧数据被拆成好几段到达也能正确拼回来。2.3 接收姿势轮询、定时器和缓冲区最常见的接收循环是while True里查any()有数据就read()。简单但问题在于主循环一旦跑耗时逻辑比如刷新 OLED、写文件、做浮点运算数据就可能积压甚至被覆盖。更稳的做法是开一个定时器每隔几毫秒查一次any()把数据搬进自己的缓冲区。类似这样from machine import UART, Pin, Timer uart1 UART(1, baudrate115200, txPin(4), rxPin(5), rxbuf1024) rx_buffer bytearray() def _poll_uart(t): while uart1.any(): rx_buffer.extend(uart1.read(uart1.any())) timer Timer() timer.init(period5, modeTimer.PERIODIC, callback_poll_uart)在这个方案里定时器回调只负责把数据从 UART 底层缓冲区搬到自己的rx_buffer主循环再按协议去解析。为什么不用外部中断因为 MicroPython 对 UART 中断的封装在 Pico 上并不一致社区里最常见的稳定方案就是定时器轮询。另外注意回调里千万别做耗时操作比如print()、复杂解析、写 Flash这些都会阻塞底层反而导致丢数据。把数据塞进缓冲区剩下的交给主循环。还有一个容易被忽略的参数是rxbuf。如果对端会一次性发上百字节而主程序还要频繁做其他事情建议在初始化时调大比如rxbuf1024。缓冲区太小数据来得比你读得快后面的字节就会把前面的冲掉。2.4 实战一通过 UART 给串口总线舵机发指令很多做机器人项目的朋友会搜“树莓派 Pico 控制舵机”但舵机其实分两类。普通航模舵机是 PWM 控制跟串口没关系Futaba、辉盛这些用 50Hz 脉宽信号驱动接在 GPIO 上输出 PWM 就行。另一类是串口总线舵机它内部有控制板一根串口线就能挂多台舵机靠 ID 区分。串口总线舵机的指令帧结构大同小异一般是帧头 舵机 ID 指令码 数据 校验。不同品牌的具体协议不一样但套路一致。示例代码框架def checksum(data: bytearray) - int: return sum(data) 0xFF def send_servo(uart, servo_id: int, position: int): frame bytearray([0x55, 0xAA, servo_id, 0x03, 0x01, position 0xFF]) frame.append(checksum(frame)) uart.write(frame)这里的0x55 0xAA是很多舵机协议用的帧头0x03是指令码具体含义要以你手里舵机模块的手册为准。把协议手册翻出来对着改指令码和校验公式就行。接线的时候注意串口总线舵机通常是 5V 供电逻辑电平也可能不是干净的 3.3V。很多串口舵机模块是 5V TTLPico 的 RX 直接接上去有超压风险要么用带逻辑转换的舵机控制板要么在舵机模块和 Pico 之间加电平转换。如果只是先验证很多舵机模块的 RX 也能识別 3.3V但长期使用我建议按模块手册做电平匹配。2.5 实战二Pico 与 STM32 的串口互认做嵌入式的人手里往往同时有 STM32 和 Pico两者串口通信也是高频需求。接线非常简单但方向必须对Pico 的 TX 接 STM32 的 RXPico 的 RX 接 STM32 的 TX然后 GND 跟 GND 连一起。以 STM32F103 为例USART1 的 TX 在 PA9RX 在 PA10你就把 Pico GP4TX接到 PA10Pico GP5RX接到 PA9。import time from machine import UART, Pin uart UART(1, baudrate115200, txPin(4), rxPin(5)) while True: uart.write(bPing\n) time.sleep_ms(500) if uart.any(): msg uart.readline() print(STM32 says:, msg) time.sleep_ms(500)STM32 端如果逻辑电平是 3.3V那两边可以直接连。如果是 5V 的板子同样需要考虑电平转换。另外如果对方是 ESP32也可以把 UART0、UART1、UART2 中挑一个来通信唯一要注意的是 ESP32 的 UART0 默认会输出启动日志最好别用来接业务数据换 UART1 或 UART2 更干净。调试任何串口互联第一步永远是先做“回环测试”把 Pico 自己的 TX 和 RX 用杜邦线直接短接程序里uart.write(btest)之后立刻uart.read()如果读回来了说明 UART 初始化、底层驱动、缓冲区都没问题。然后把线拆开接到对方的板子上问题范围就缩小到“接线”和“电平”这两件事上。我强烈建议把这个流程养成习惯能省下一大半排错时间。3. 调试工具的选型和使用让串口不再“盲调”3.1 USB 转 TTL 模块怎么选串口调试离不开 USB 转 TTL 模块市场上有三类芯片最常见。模块芯片价格特点适用场景CH340便宜几块钱到十几块Windows 下驱动好找Mac 有时要装驱动入门学习、临时调试CP2102中等跨平台免驱体验较好稳定性相对好日常开发、Mac 用户FT232贵几十到上百驱动极其成熟兼容性最好抗干扰强生产环境、长期联调选型时还要注意模块上有没有 3.3V/5V 的电平切换跳线。很多 CH340 模块默认输出 5V 电平如果你把它接到 Pico 的 RX 上必须先跳线到 3.3V 再连。判断方法很简单模块上一般会有一个跳帽或开关写着 3.3V/5V或者用万用表量一下 TX 引脚的待机电平高电平是 3.3V 就说明模块当前在 3.3V 挡位。另外有些模块会通过 VCC 引脚向目标板供电。Pico 本身用 USB 供电就够了别同时再从模块接一个 5V 进来两个电源并行供电容易造成电流倒灌或者异常发热。3.2 Win/Linux/Mac 下的串口助手和权限处理Windows 上插上 USB 转 TTL 或 Pico 后去设备管理器“端口COM 和 LPT”里看 COM 编号。Pico 的板载 USB CDC 通常显示为类似“Board CDC”或者直接带 RP2040 字样USB 转 TTL 模块则显示芯片名比如 CH340。工具方面我常用 SSCOM 或友善串口调试助手界面老但是稳定。想在 VSCode 里解决也可以用串口监视器插件。Linux 下设备名通常是/dev/ttyUSB0或/dev/ttyACM0。命令行工具我最常用picocom语法简洁picocom -b 115200 /dev/ttyUSB0退出快捷键是CtrlA然后CtrlX。minicom也很好用但要先-s配置串口参数对新手来说稍微重一点。如果遇到Permission denied说明当前用户不在 dialout 组里执行sudo usermod -a -G dialout $USER然后重新登录一次一般就能访问串口设备了。Mac 下设备名是/dev/tty.usbserial-xxx或者/dev/cu.usbmodemxxx图形工具可以用 CoolTerm命令行用screen也行screen /dev/cu.usbmodemxxx 115200连上 Pico 的 USB CDC 后按一下复位键串口助手窗口里应该能看到 MicroPython 的提示符这就说明 REPL 环境正常你可以在里面直接敲 Python 指令测试。3.3 在 VMware 里调试 Linux 串口串口映射的坑很多人的开发环境是 Windows 宿主机加上 VMware 里的 Linux 虚拟机物理 USB 转 TTL 模块插在 Windows 上虚拟机里的代码想访问串口有两条路。第一条路直接把 USB 设备直通给虚拟机。操作路径虚拟机设置 → USB 控制器 → 点击“添加” → 选择 USB 转串口设备。Linux 虚拟机里会看到/dev/ttyUSB0。第二条路把物理串口映射成虚拟机的串口。路径大致是虚拟机设置 → 添加 → 串行端口 → 选择“使用物理串行端口” → 在列表里选中 Windows 的 COM 口比如 COM3。Linux 里会看到/dev/ttyS0然后用picocom -b 115200 /dev/ttyS0连接。这里最大的坑是同一个物理串口同一时间只能被一个系统占用。你在 Windows 宿主机上开着串口助手不关虚拟机里的 Linux 再连同一个串口结果只会是“打不开”或者“收到一堆乱码”。排查这种问题时先把宿主机上占用 COM 口的软件全部关掉再去虚拟机里试。我见过有人在这个问题上耗了一下午其实就是宿主机后台有个串口监听工具一直占着设备。3.4 逻辑分析仪一针见血看串口波形如果只能推荐一样串口调试设备我一定选逻辑分析仪。几十元的 8 通道 24MHz 逻辑分析仪就够用了配合 PulseView 或 Saleae Logic 软件可以直接看到 UART 波形还能自动解码。操作也简单CH0 夹 Pico 的 TXCH1 夹 Pico 的 RXGND 跟开发板共地。软件里添加 UART 协议解码器设置好波特率再点捕获。这时候你能看到每个字节的波形长什么样。两个设备之间到底有没有真的发数据。波特率是否匹配。如果波形解码出来全是乱码可以试试让软件自动估算波特率看看实际位宽跟配置差多少。帧与帧之间的间隔对 Modbus 这类强调帧间隔的协议特别有用。逻辑分析仪最大的价值是让“感觉像是没数据”变成“确实没数据”或“发了但协议不对”。它像是一个能回放的监控摄像头把串口通没通这个问题变成客观事实不用靠猜。3.5 从 UART 到 RS485工业总线的扩展思路如果要在几十米、上百米的距离上通信或者周围有电机、变频器之类的强干扰源TTL 串口扛不住。这种场景就得上 RS485。Pico 外接 MAX485 模块硬件接线一般是Pico 的 UART TX 接模块 DIUART RX 接模块 RODE/RE 两个引脚接一起再接到 Pico 的一个普通 GPIO 来控制收发方向。半双工控制的代码逻辑是这样from machine import UART, Pin import time uart UART(1, baudrate9600, txPin(4), rxPin(5)) dir_pin Pin(6, Pin.OUT) dir_pin.off() # 默认接收状态 def send_485(data): dir_pin.on() # 进入发送模式 uart.write(data) uart.flush() # 等数据全部送出 time.sleep_ms(1) dir_pin.off() # 切回接收模式这里要注意 MAX485 模块的电平。模块常见 5V 供电但 RO 输出高电平也接近 5V接到 Pico RX 前最好确认模块是否支持 3.3V 逻辑或者加一个分压/电平转换。很多便宜的 MAX485 模块可以直接用 3.3V 供电工作用之前翻一下模块手册最稳妥。RS485 上最常见的应用协议是 Modbus RTU帧结构是从站地址 功能码 数据 CRC16 校验。它要求同一帧内的字符间隔小于 1.5 个字符时间帧与帧之间大于 3.5 个字符时间。9600 波特率下3.5 字符大约 3.6ms所以发送完一帧以后要稍微等一下再切换方向否则下一个从站还没来得及处理你这边又把线占住了。4. 实战踩坑清单乱了、丢了、通了却不说话4.1 乱码的真凶波特率和“看似正常的波形”乱码是最常见的故障也是原因最多样的故障。首要怀疑对象永远是波特率不一致。一端 9600一端 115200收方看到的就是一串乱码。这个问题的特点是对面明明有信号却没有一个字符是正常的。第二个常见原因是接线太长或者面包板接触不良。TTL 电平的抗干扰能力有限杜邦线超过 20 厘米就要小心超过 1 米乱码概率直线上升。手头没有好线材先把波特率降到 9600通常能缓解。第三个非常隐蔽的原因是没共地。两个设备各自通过 USB 供电看起来都工作正常但它们的地电位参考点不一样信号没法形成稳定的回路。表现就是偶尔收到几个字节其余全是乱码或者完全没数据。解决办法就一句话GND 接 GND一根线的事。排查乱码的顺序我建议是先回环自测排除自身问题再上逻辑分析仪看波形和实际波特率最后才怀疑协议代码。大部分时候问题出在物理层而不是你的 Python 代码。4.2 接线第一大坑TX 对 TX、忘了共地UART 全双工通信的接线是交叉的A 的 TX 接 B 的 RXB 的 TX 接 A 的 RX。新手最容易犯的错就是把同名引脚接到一起TX 对 TX、RX 对 RX两边都在发谁也收不到谁。共地这件事也要反复强调。两个设备的地必须是同一个电位参考GND 不连信号的“高”和“低”就没有基准。我在实际调试中见过不止一次设备悬空时用万用表测 TX 和 RX 电压都正常但一接上就没数据最后都是因为地没连。串口通信的接线可以简化成一句话一根共地、两根交叉先把这三点确认好再谈协议。4.3 GPIO 复用冲突不报错的隐形杀手MicroPython 里边初始化 I2C 用 GPIO4/5转身又创建 UART1 也用 GPIO4/5很多时候不会立刻报错但同一根引脚被两个外设同时接管最后的状态完全看谁后初始化通信结果自然乱套。我现在的习惯是项目里单独建一个config.py把所以引脚分配集中管理from machine import Pin, I2C, UART UART_PINS {tx: Pin(4), rx: Pin(5)} I2C_PINS {scl: Pin(6), sda: Pin(7)}任何外设要用引脚先从配置文件里取绝不散落在各个模块里。一旦要改引脚分配改一个文件就够了不会改 A 模块忘了 B 模块。这个习惯帮我避免了很多“程序明明没问题但就是不通”的灵异事件。4.4 缓冲区溢出和二进制帧被 readline 拆碎对端一次性发几百字节你这边主循环又在做屏幕刷新MicroPython 内置的 UART 缓冲区默认只有几百字节一旦积压就会丢数据。对策很简单初始化时调大rxbuf比如 1024 或 2048然后定时器频繁把数据搬走别让它积压。处理二进制协议时千万别用readline()。二进制数据里 0x0A、0x0D 都可能出现在任意位置按行读取会把一帧从中间切断。正确做法是按长度读取先用any()判断积累了多少字节凑够长度再read(n)或者维护一个累积缓冲区靠帧头和长度字段切帧。一个典型的帧解析状态机长这样frame_buffer bytearray() def feed_bytes(data): global frame_buffer frame_buffer.extend(data) while True: # 查找帧头 start frame_buffer.find(b\xAA\x55) if start -1: frame_buffer bytearray() return # 检查长度字段帧头校验 if len(frame_buffer) start 4: length frame_buffer[start 2] total start 3 length 1 if len(frame_buffer) total: frame frame_buffer[start:total] del frame_buffer[:total] handle_frame(frame) continue return这不是唯一的写法但思路是通用的永远不要假设一次 read 就能拿到完整一帧把数据积累起来再按既定规则切才是靠谱的协议处理方式。4.5 最后的电压、供电和“5V 兼容”陷阱RP2040 的 GPIO 不是 5V 耐受引脚外部输入超过 3.3V 就可能逐渐损坏内部电路。很多 USB 转 TTL 模块默认输出 5V 电平直接接到 Pico RX 上短期可能没明显现象长期就是 GPIO 莫名其妙损坏、不复位、烧固件这类“玄学问题”。如果必须用 5V 模块最好的办法是选带 3.3V 电平模式的型号或者用 TXS0108 这类双向电平转换芯片。除了信号电平供电也要留意。Pico 板载 3V3 稳压器输出能力有限如果你在 3V3 引脚上同时挂了串口舵机、OLED、传感器电压可能被拉低串口通信就会时好时坏。模块耗电大的时候单独供电GND 保持连通信号线还是那几条问题就解决了。还有一个小排查点看看是不是插错了引脚。GP0 和 GP1 在 Pico 板边上是相邻的两个脚很多人接线的时候看了丝印实际把杜邦线插到了旁边的 GPIO 口从外观上很容易忽略。拿万用表蜂鸣档顺着杜邦线量一遍比盯着板子猜靠谱得多。按照我自己现在的习惯任何串口项目启动之前都会先做一个最短路径验证软件只写三行代码把 TX 和 RX 短接发一个字节读回来通了再接外设。这套“先回环、再点对点、再总线”的顺序让我从大量“看似通但又不通”的怪问题里解脱出来。串口通信原理不复杂但硬件细节、电平匹配、协议解析这几个环节堆在一起确实能把人磨到怀疑人生。希望这篇关于 Pico 串口硬件、MicroPython UART 编程和调试工具的梳理能帮你少走一点弯路。
返回列表