树莓派 Pico 串口通信入门:MicroPython 编程与调试实战
1. 为什么 Pico 的串口值得单独写一篇树莓派 Pico 这块小板子很多人买回来第一件事就是点灯第二件事大概是让板子跑个 MicroPython 然后打印Hello World。但真正到了要跟传感器通信、接显示屏、调试物联网设备甚至做 Server 端联调的时候绕不开的就是 UART 串口。可以说串口是你在 Pico 上做一切“板外交互”的地基。我最初接触 Pico 的时候也天真地以为串口就是两根线、一收一发能通就行。结果真上手后发现硬件引脚怎么复用得想清楚MicroPython 里UART()的参数怎么配才不丢数据调试时是看print还是用逻辑分析仪这中间全是隐形门槛。再加上很多人习惯拿它跟 Arduino 比Pico 的串口数量、引脚映射、中断收发机制又不一样抄 Arduino 的代码过来大概率直接翻车。这篇文章不打算泛泛讲串口理论我会直接拆成四块硬件特性是什么、MicroPython 里怎么写最稳、调试工具有哪些怎么选、以及我实际踩过的一些坑。适合刚入手 Pico 的玩家也适合用 Pico 做了几个小项目、但想进一步提升串口通信稳定性的朋友。2. Pico 串口硬件特性盘点2.1 两颗 UART 到底够不够用RP2040 芯片上集成了两个硬件 UARTUART0 和 UART1每个都支持标准的全双工收发。全双工意思就是可以同时发送和接收数据这对做控制类场景很重要我们后面讲的舵机控制和读取传感器反馈很多时候需要“边发命令边听应答”。不过“有 UART0 和 UART1”这件事只是第一步真正的限制在于引脚映射。每个 UART 都有一组默认引脚同时也允许映射到板上其他引脚。对新手来说熟悉的往往是默认那组UART0 TX 默认 GP0RX 默认 GP1UART1 TX 默认 GP4RX 默认 GP5这两个默认组合已经覆盖绝大多数场景比如接 GPS 模块、接蓝牙模块、接 ESP8266 透传。但如果你做的项目里GP0 已经被 PWM比如驱动舵机占了或者 GP1 要留着做按键扫描此时就得用 UART0 的备用引脚映射TX 映射到 GP12RX 映射到 GP13。于是我给新手的第一个建议拿到板子后不要直接接线先想清楚每个 GPIO 的职责。引脚复用是 Pico 做多路串口设计的核心思路但也容易让你布线时踩进冲突的坑里后面我们会专门讲怎么排查这种问题。2.2 电平标准与接线的物理常识Pico 的 GPIO 电平是 3.3V这决定了它的 UART 引脚不能直接接 5V 的设备比如旧款 Arduino 的 5V 串口输出、某些 5V 供电的 GPS 模块。很多新手在接线时最大的误区是只要 TX 接 RX、RX 接 TX、GND 共地就行忽略了电平匹配。如果对方设备标明是 5V 逻辑你还需要在通信线上加电平转换模块。不是所有板子“稍微过压”都能扛住RP2040 的引脚不是完全耐 5V 的。另外接线长度也要控制。串口的波特率越高对线材和布线越敏感。我测试过在 115200 波特率下用 20 厘米长的杜邦线连接传感器基本没问题一旦把线换成 1 米以上就会出现偶发乱码。所以布线原则是能短则短尽量用双绞线或者屏蔽线特别是走长距离通信时这个细节能省掉很多排查时间。2.3 从单片机串口到板级串口的区别这里还想做一个概念上的对比。当我们说“Pico 串口通信”既指 RP2040 芯片引出的 UART 引脚单片机串口也指板载 USB 转串口MicroPython 的 REPL 终端。两者是完全不同的链路芯片 UART通过 GP0/GP1 等引脚对外通信接传感器、接 PC 串口助手需要 USB 转 TTL 工具USB 串口Pico 板载的 microUSB 连接到电脑后会被识别成一个虚拟串口设备MicroPython 的交互式解释器REPL就走这个通道很多人刚接触 Pico 时会在 Thonny 的 Shell 窗口里看到 print 输出就以为那就是串口调试。严格说这是 USB CDC 虚拟串口并不是芯片 UART 引脚上的数据。理解这两者的区别是后面进行真正串口设备调试的基础。3. MicroPython 串口编程从入门到顺手3.1 初始化参数的“灵魂三问”MicroPython 的machine.UART接口写起来不复杂但初始化时那几个参数如果不理解含义很容易写出“能跑但是不稳定”的代码。以一个典型的传感器交互为例from machine import UART, Pin import time # 使用 UART1默认引脚 TXGP4, RXGP5 uart1 UART(1, baudrate9600, txPin(4), rxPin(5), bits8, parityNone, stop1)这里主要解释两个容易被忽略的点。第一是波特率它决定了每秒传输多少 bit。9600 和 115200 是嵌入式里最常见的两个值传感器模块一般默认 9600像 ESP8266 AT 固件、多数 GPS 模块都是这个值第二是tx和rx引脚参数在高版本 MicroPython 固件里可以不传直接走默认引脚但如果你希望用备用引脚比如前面提到的 GP12/GP13必须显式指定。bits8、parityNone、stop1这三个组合是通用的 8N1 格式绝大多数串口设备都认这个格式。除非你明确知道模块手册里写的是 7 位数据位或带校验位否则不需要改动。初始化之后只需注意一点MicroPython 不会自动在初始化时把 TX 置为高电平。如果你接的设备对串口空闲电平有要求比如某些 RS485 模块可能需要在初始化后手动拉高这个细节在调试中比较隐蔽。3.2 收发数据读和写的三种姿势写数据这件事MicroPython 提供了两种常用方法write()和writeinto()。区别不大实际项目里用write()最多因为它直接接受字符串或者字节串uart1.write(bAT\r\n) # 发送 AT 指令例如接 ESP8266 uart1.write(Hello Pico\n) # 直接传字符固件会自动编码推荐在通信时统一用 bytes 类型也就是前面加b前缀。如果你用字符串传中文MicroPython 会用 UTF-8 编码部分下位机对这种编码不敏感但部分模块需要的是纯 ASCII直接传字符串可能出乱码。读数据则有几种思路。最简单的是轮询式用any()判断缓冲区是否有数据if uart1.any(): data uart1.read() print(data)这种方式适合测试脚本逻辑清晰也不容易出错。缺点是如果数据不是一次到齐的你读到的一半可能是个不完整的协议帧。更稳的做法是把读数据放到while循环里定义一个接收缓冲区先把滚动数据存起来等收到结束符或完整帧长度后再统一解析。MicroPython 也支持 UART 中断接收通过irq()方法绑定处理函数def uart_handler(uart): data uart.read() print(rx:, data) uart1.irq(triggerUART.IRQ_RX, handleruart_handler)但这套机制在 MicroPython 固件版本里支持程度不太一致实测在 Pico 上某些固件更新后中断回调会偶发失效。如果你项目的实时性要求很高建议直接用 C/C SDK 写接收中断如果只是用 MicroPython 做快速原型验证轮询加缓冲区方式其实足够稳。3.3 发送十六进制数据的正确姿势很多传感器模块的指令是十六进制数组比如“读取版本号”对应的指令可能是0xAA 0x55 0x01 0x00。新手容易犯的错误是直接把字符串AA550100写进uart.write()结果模块完全没反应。正确做法是构造字节数组cmd bytes([0xAA, 0x55, 0x01, 0x00]) uart1.write(cmd)如果你需要动态拼包可以这样cmd bytearray() cmd.append(0xAA) cmd.append(0x55) cmd.append(0x01) cmd.append(0x00) # 计算校验和 checksum (0xAA 0x55 0x01 0x00) 0xFF cmd.append(checksum) uart1.write(bytes(cmd))要点是模块要什么格式你就按什么字节结构给它不要在中途做编码转换。很多人卡在“模块没反应”这类问题上最后排查出来就是编码不对。3.4 一个可复用的串口收发小框架下面是我整理的一个通用框架。每次接新模块时我习惯先套这个框架验证能不能通信再往里面填业务逻辑from machine import UART, Pin import time class SerialManager: def __init__(self, uart_id1, tx_pin4, rx_pin5, baud9600): self.uart UART(uart_id, baudratebaud, txPin(tx_pin), rxPin(rx_pin)) self.rx_buffer bytearray() def send_raw(self, data: bytes): self.uart.write(data) def send_str(self, s: str): self.uart.write(s.encode(utf-8)) def read_available(self): while self.uart.any(): self.rx_buffer.extend(self.uart.read()) return bytes(self.rx_buffer) def clear_buffer(self): self.rx_buffer.clear() # 使用示例 ser SerialManager() ser.send_str(AT\r\n) time.sleep(0.1) resp ser.read_available() print(Response:, resp) ser.clear_buffer()这个框架的好处是把收发分离你只需要关心send_*和read_available两个接口不用每次都重复写any()判断逻辑。实测下来调试效率提升明显。4. 串口调试工具实战对比与选型4.1 五类工具的定位与适用场景串口调试这件事单靠一个“串口助手”并不能覆盖所有场景。我按用途把常用工具分成了五类基础串口助手用来发指令、看 ASCII/HEX 返回USB 转 TTL 模块用来把 Pico 的 UART 引脚接到电脑逻辑分析仪用来观察波形和时序确认数据是否真的发出去了串口监听/分析软件比如 Python pyserial 脚本、VSPD 虚拟串口显示器类工具像 Pico 自带的 REPL 终端Thonny、minicom先区分一个基本概念如果你是用 Pico 的 USB 口microUSB连电脑那么在设备管理器里看到的 COM 口是 USB CDC 虚拟串口只能用于 MicroPython REPL 交互和打印输出。如果你想跟 Pico 的 UART 引脚通信也就是 GP0/GP1 那一路需要用 USB 转 TTL 模块把 Pico 的 TX/RX 接到电脑。4.2 一些主流工具的使用心得先说说挺常用的 USB 转 TTL 模块。市面上最常见的方案是 CP2102 和 CH340前者稳定一点后者便宜大碗。Mac 用户要注意 CH340 在 macOS 上偶尔掉驱动出现断连时先看ls /dev/tty.*有没有设备。Windows 上一般装好驱动后在“设备管理器”里看 COM 口号就行。再比如用 PySerial 做压力测试是最灵活的组合。普通串口助手手动点按钮发数据效率太低真实项目里往往要连续发几百条指令验证稳定性。我经常写这样的脚本import serial import time ser serial.Serial(COM10, 115200, timeout1) cmd bytes([0xAA, 0x55, 0x03, 0x00]) ok_count 0 total 200 for i in range(total): ser.write(cmd) resp ser.read(10) if len(resp) 0: ok_count 1 time.sleep(0.02) print(f{ok_count}/{total} received response) ser.close()这对判断模块的响应率、丢包率非常直观。然后是逻辑分析仪。如果你怀疑数据根本没从 Pico 引脚出来或者波形乱七八糟逻辑分析仪是定位问题最快的工具。逻辑分析仪把 TX/RX 信号按时间轴显示成高/低电平序列你可以肉眼看到波特率是否匹配、起始位是否正常。需要强调的是当信号完全无响应时建议先用逻辑分析仪观测硬件波形而不是反复调软件。因为串口通信“无声无息”的问题多半是物理连接、电平、引脚映射或波特率调软件很难定位。4.3 从热词中延伸的两个经典调试场景我注意到最近搜索“unity pico会出现indexoutofrangeexception: renderpassindex”以及“宿主机 windows 如何通过串口与 vmware 中 linux 通信”的人越来越多这两个其实都是串口联调的典型例子。先说 Unity 和 Pico。很多人做 VR 项目用 Unity 作为上位机界面用 Pico 当硬件控制器。Unity 通过串口发数据给 PicoPico 解析之后再执行动作比如开灯或者调电机。此时 Unity 里用的 SerialPort 类底层调用的也是系统串口 API思路和单片机串口完全一致差异只是解析高频数据时注意丢包。反正我在 Unity 里串口通信时第一件事就是设置较短的 ReadTimeout避免主线程卡死。再说 Windows 宿主机和 VMware 里 Linux 的串口共享。这个问题常见于嵌入式开发编译和烧录在 Windows 上完成但运行环境在虚拟机里的 Linux 上。最简单的方式是让 Windows 和 Linux 共享同一个 COM 口VMware 支持“Connect”这个串口设备给虚拟机但注意物理串口同一时间只能被一个系统占用你需要关闭宿主机的串口监视程序。更稳的方案是宿主机跑一个 TCP 转串口服务虚拟机里用 socat 把 TCP 端口映射成虚拟串口。这样两边都能访问同一个物理串口不冲突。这个方案我实际用下来稳定性尚可特别适合需要两边同时观察日志的场景。5. 串口通信典型应用实战舵机控制5.1 通过串口命令控制舵机的整体思路串口通信的经典应用之一就是 Pico 作为舵机控制板接收上位机PC 或手机发来的角度指令然后产生 PWM 信号控制舵机。舵机控制使用的是 50Hz 的 PWM也就是周期 20ms占空比在 2.5%~12.5% 之间对应 0°~180°。RP2040 的 PWM 精度足够高MicroPython 里可以很方便地控制。串口在这套系统里的角色是“指令入口”。上位机发来类似S90这样的文本指令Pico 解析出角度值后映射到 PWM 占空比这就是一个完整的串口控制链路。5.2 从串口指令到 PWM 输出的完整代码下面这段代码实现了一个通过 UART 控制舵机的完整流程。接收串口字符串解析角度值然后设置 PWM 占空比from machine import UART, Pin, PWM import time # 初始化 UART1TXGP4, RXGP5 uart1 UART(1, baudrate9600, txPin(4), rxPin(5)) # 初始化舵机 PWM 引脚 GP15 servo PWM(Pin(15)) servo.freq(50) # 50Hz 20ms 周期 def angle_to_duty(angle): # 0度 - 2.5%占空比180度 - 12.5%占空比 # 填入 0~65535 的占空比范围 min_duty int(65535 * 0.025) max_duty int(65535 * 0.125) return int(min_duty (max_duty - min_duty) * angle / 180) buffer b while True: if uart1.any(): data uart1.read() if data: buffer data # 按换行符分割完整指令 if b\n in buffer: lines buffer.split(b\n) for line in lines: line line.strip() if not line: continue try: # 指令形如 S90 if line[0] ord(S): angle int(line[1:]) angle max(0, min(180, angle)) servo.duty_u16(angle_to_duty(angle)) uart1.write(fOK {angle}\n.encode()) except ValueError: uart1.write(bERR\n) buffer b这段代码虽然借鉴了网上流行思路但我做了一些改进用缓冲区而不是每次收到一个字节就执行一次用换行符作为指令结束标志比固定长度解析更健壮增加角度限幅保护舵机。接线要注意舵机信号线接 GP15电源线接 5V地线接 GND。注意不要直接用 3.3V 给舵机供电多数舵机在负载状态下 3.3V 会掉压导致抖动。5.3 为什么这种架构适合“上位机 下位机”模式这种“串口指令 下位机执行”的架构在机器人项目里几乎是标配。上位机PC、手机蓝牙串口助手、Unity 程序负责复杂的运算和界面交互下位机 Pico 只做实时性强的底层层操作比如 PWM 输出和 IO 控制。我这段时间把 Pico 接入 Unity也就是用类似方式实现Unity 端通过SerialPort.Write(S90\n)发送指令Pico 端解析并控制舵机。实测在 19200 波特率下指令发送到舵机转动的延迟可以控制在 20ms 以内对大部分交互场景完全够用。如果你也打算把 Pico 接到 Unity 或者其他上位机建议在协议设计上加入起始符和结束符。比如S90\nS是起始符90是数据\n是结束符。这样即使偶尔出现粘包也能利用起始符分割指令避免错误执行。6. 常见问题与排查技巧6.1 为什么收不到数据这是串口调试里遇到频率最高的问题没有之一。常见原因按概率排序接线没共地。UART 是异步串行通信收发双方必须共地否则参考电压不一致。TX 和 RX 接反了。对调一下就能解决。波特率不一致。模块是 9600你这边 115200收到的全是乱码或者什么都没显示。引脚占用冲突。GP0/GP1 可能已经被别的功能占用此时数据自然不会从你期望的引脚出来。电平不匹配。3.3V 接收方接了 5V 信号虽说不至于立刻烧毁但可能无法稳定识别。排障顺序建议先看硬件接线共地否、TX/RX 是否对调再用逻辑分析仪测波形最后才怀疑代码。很多新手顺序反了代码改了大半天最后发现是杜邦线松了。6.2 为什么会乱码乱码几乎都是波特率不匹配。还有少数情况是双方格式不一致你是 8N1对面模块是 7E17位偶校验结果就是每个字节都错位。如果你用 USB 转 TTL 模块确认模块上的跳线或拨码开关是否配置正确。有些模块默认 5V 模式没有跳成 3.3V这也会导致波形畸变。另一个冷门但常见的原因是“接地环路”。如果上位机 PC 和 Pico 两边都接了不同的电源适配器且两个适配器的地之间存在电压差串口线上会有电流流动导致信号畸变。解决方法是只用单边供电或者用隔离模块。6.3 为什么打印显示正常但 UART 引脚没输出这个问题在 Pico 上很典型原因我刚才提过MicroPython 的 print 打印走的是 USB 虚拟串口跟 UART 引脚是两码事。很多人写了几行 print 发现 Thonny 窗口有输出就以为串口通了实际 GP0/GP1 上什么信号都没有。想验证 UART 引脚是否在发数据最简单的方法是把 TX 引脚接地然后用 USB 转 TTL 模块连接在电脑上用串口助手看有没有数据。如果你手头有逻辑分析仪把这个抓手挂上去看波形是更直观的方案。由于 REPL 默认占用 UART0 的引脚你如果初始化 UART0 就会发现 REPL 没输出了。解决办法是换用 UART1或者调整 REPL 的映射需要在固件层操作比较麻烦不推荐新手直接上。6.4 串口调试问题速查表现象最可能原因优先排查项完全无输出接线错误或未共地检查 TX/RX/GND 连接收到数据全是乱码波特率不一致两边设成完全相同偶尔丢字节缓冲不足或线过长加长延时/缩短线缆执行了两次指令上位机多发或粘包在指令中加结束符print 有输出但引脚没信号REPL 和 UART 混为一谈检查是否在 UART 引脚上接设备引脚电平异常外设电压不匹配加电平转换模块这张表的经验来自我实际调试过的多个场景。个人感觉串口问题看起来五花八门但根因不外乎电平、接线、波特率、引脚冲突四类。先把这四类排除掉再去折腾协议和代码能少走一半弯路。6.5 电磁干扰与信号完整性最后补一个前面提过但值得专门强调的点线缆长度对高速串口的影响。理论上 UART 在 115200 波特率下几十厘米线缆都问题不大但如果你用的是面包板加长杜邦线又跟电机驱动线走在一起还是会偶发干扰。对策有两种。一是降低波特率到 9600 或 19200代价是传输速度变慢但稳定性显著提升二是使用带屏蔽的线缆或者双绞线减少外部电磁干扰的影响。我在驱动舵机时PWM 信号线和串口线保持了一定距离避免舵机 PWM 对串口 RX 线产生串扰。这个细节在电机类项目中尤其要注意。7. 我的一点体会与扩展建议说实话Pico 的串口通信并不复杂但它在整个嵌入式项目里起到的作用经常被低估。串口是你对外的眼睛也是你给他的命令。学会正确配置 UART、理解引脚复用、掌握收发逻辑、选对调试工具这套组合拳打下来基本能应对 90% 的 Pico 外设通信需求。我实际做过的项目里Pico 串口最稳的一个场景是长期运行的环境监测节点。节点通过 UART 接二氧化碳传感器每 5 秒读一次数据再用板载 WiFi 模块通过串口把数据传到 MQTT broker。连续运行了两周串口链路没有丢过一次数据。能做到这一点靠的不是运气而是初始化时把超时、缓冲区、电平都处理好。如果你后面打算深入这块有几个方向值得继续玩用 Pico 做串口转 WiFi 网关让老旧传感器联网用两个 Pico 做全双工串口通信测试自己写一份简单的通信协议或者在 CircuitPython 和 MicroPython 之间切换对比两者 UART 行为的差异。串口是不起眼的基础功但基础功越扎实后续做复杂项目的翻车概率就越低。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →