1. 从硬件到代码:Pico 串口通信的整体思路
先说个很实在的判断:串口(UART)是嵌入式开发里最“皮实”也最常用的通信手段,不管是调试输出、传感器读取,还是和 PC、其他单片机打交道,几乎绕不开它。树莓派 Pico 这块板子虽然价格便宜,但它的串口资源并不寒酸——RP2040 芯片内部集成了两个独立的 UART 外设,加上 MicroPython 对 machine.UART 的封装非常简单,上手门槛比用 C 写寄存器低了一大截。我见过不少朋友拿到 Pico 的第一件事就是点灯,第二件事往往就是想通过串口打印点东西,或者和电脑互发数据,这个需求非常典型。
这篇内容围绕“Pico 串口通信”这件事,打算把硬件层面的引脚资源、电气特性讲清楚,再带着你把 MicroPython 下的收发代码写顺,最后聊聊调试工具和实战中容易踩的坑。适合刚接触 Pico 的初学者,也适合已经在用 Pico 做小项目、想系统梳理串口用法的朋友。如果你只是想快速让 Pico 和电脑通上话,直接从第 3 章的代码开始也行;但如果后面要做传感器采集、多机通信这些复杂场景,建议把第 2 章的硬件细节过一遍,很多诡异问题其实都出在引脚和电平上。
在开始之前先说明一下,这篇文章针对的是最常用的 Pico 开发板(RP2040 芯片),MicroPython 固件以官方版本为例。如果你用的是 Pico W,串口部分完全一致,只是多了无线功能,不影响本文内容。
2. 先搞懂硬件:引脚、电平与资源分配
2.1 RP2040 的两个 UART 外设到底能接哪些引脚
Pico 的 RP2040 芯片内置了两个硬件 UART,官方命名是 UART0 和 UART1。这两个外设并不是“绑死”在某两个引脚上的,而是通过芯片内部的引脚复用矩阵,可以把 UART 信号映射到多组 GPIO 上,这点和 STM32 的 AF 复用有点类似,但配置更灵活。
我整理了一份常用映射表,大家平时用到的引脚基本都在这了:
| UART 外设 | TX 引脚可选 | RX 引脚可选 |
|---|---|---|
| UART0 | GP0、GP12、GP16 | GP1、GP13、GP17 |
| UART1 | GP4、GP8、GP20 | GP5、GP9、GP21 |
这里有个细节要注意:MicroPython 的machine.UART初始化时,如果你指定了tx和rx引脚,它会自动配置引脚复用,不需要像 C 语言那样手动操作 GPIO 功能选择寄存器。但这也意味着,同一时刻一个 UART 外设只能使用一组 TX/RX 引脚,你没法让 UART0 同时在 GP0 和 GP12 上输出数据。
按我自己的使用习惯,默认首选GP0(TX)和 GP1(RX),因为这两个引脚在 Pico 主板的丝印上直接标注了,位置也顺手,接线不容易出错。如果这两个引脚被其他功能占用了,比如接了按键或者 LED,再考虑换到 GP4/GP5 或 GP16/GP17 这组。
还有一点需要特别提醒:UART0 和 UART1 是独立的外设,可以同时使用。这意味着如果你项目里既要和电脑通信,又要和传感器通信,可以一个 UART 接 USB 转串口模块,另一个 UART 接传感器,互不干扰。我在做多传感器采集时经常这么干,省去了软件模拟串口的麻烦。
2.2 3.3V 电平逻辑:为什么不能直接接 5V 设备
Pico 的 GPIO 电平是 3.3V,这是很多人容易忽略的一个“坑”。市面上大量传感器模块、GPS 模块、蓝牙模块是 5V 电平逻辑,如果你直接把 Pico 的 RX 引脚接到 5V 设备的 TX 引脚上,轻则通信数据乱码,重则烧毁 Pico 的 GPIO 引脚。
举一个我踩过的例子:有一次我把一个 5V 电平的 GPS 模块直接连到 Pico 的 GP1(RX)上,结果串口收到的全是乱码,查了半天波特率、接线,最后用万用表一量,RX 引脚上被拉到 5V 了,虽然没烧板子,但通信完全不可用。后来加了一个电平转换模块(3.3V←→5V 双向)才恢复正常。
所以在硬件设计阶段就要想清楚:所有连接到 Pico UART 的外设,电平必须匹配 3.3V。如果你的设备是 5V 的,中间需要加电平转换芯片,比如 TXS0108E、MAX232 这类,或者用电阻分压的方式(不推荐,速率高时信号会变形)。另外,Pico 的 TX 引脚输出 3.3V 电平,接到 5V 设备的 RX 引脚通常是安全的(因为很多 5V 芯片的 RX 高电平阈值在 2.5V 左右,3.3V 足够识别),但严谨起见,还是建议加电平转换。
2.3 USB 转串口:Pico 本身自带一个,但你可能还需要外接一个
Pico 板载的 USB 接口在 MicroPython 下默认是一个虚拟串口(CDC),可以用来执行 REPL(交互式解释器),也能用print()输出调试信息。对纯调试场景来说,直接用 USB 线连电脑就够了,不需要额外买 USB 转 TTL 模块。
但如果你想让 Pico 和外部设备通信,比如接一个 GPS 模块、一个 Arduino、或者另一块 Pico,那你需要把 UART 引脚通过 USB 转 TTL 模块连接到电脑,或者直接用杜邦线连接两个设备的 TX/RX(注意交叉连接)。这时候板载 USB 虚拟串口就不够用了,因为它的数据通道被 MicroPython 的 REPL 占用了——不过你依然可以用os.dupterm()之类的技巧把 REPL 重定向到 UART,但那是后话。
我个人的建议是:调试代码用 USB 虚拟串口,设备通信用硬件 UART 引脚,两条路线分开,互不干扰。这样 MicroPython 的打印信息走 USB,传感器数据走硬件串口,排查问题的时候一目了然。
3. MicroPython 串口编程:从初始化到数据收发
3.1 最基础的初始化:UART 参数的选取逻辑
MicroPython 里操作串口的核心是machine.UART类。初始化代码非常简单:
from machine import UART, Pin uart0 = UART(0, baudrate=115200, tx=Pin(0), rx=Pin(1), bits=8, parity=None, stop=1)这里有几个参数值得展开说说:
- UART(0):选择 UART0 外设。如果你要用 UART1,这里就写 1,同时 tx/rx 引脚要选 UART1 对应的引脚(比如 GP4/GP5)。
- baudrate=115200:波特率。常见的选择有 9600、57600、115200。115200 是绝大多数场景下的“默认值”,速度快、误码率也能接受。如果通信距离长或者线材质量差,适当降到 9600 会更稳定。
- tx=Pin(0), rx=Pin(1):指定 TX 和 RX 引脚。注意是
Pin(0)而不是0,MicroPython 的 UART 初始化要求传入 Pin 对象。 - bits=8, parity=None, stop=1:8 数据位、无校验、1 停止位。这是最通用的配置,叫做 8N1。除非你连接的设备明确要求其他格式,否则就用这个。
关于波特率,补充一个常识:通信双方(Pico 和外设)的波特率必须完全一致,否则收据全是乱码。如果你不确定外设的波特率,可以先从 9600 试起,这是很多传感器模块的默认波特率。
3.2 发送数据:write 与 print 的本质区别
MicroPython 发送串口数据有两种方式:
# 方式一:使用 UART 对象的 write 方法 uart0.write("hello\r\n") # 方式二:使用 print 配合重定向(仅限 USB 虚拟串口) print("hello")很多初学者会把这两种方式搞混。简单说,uart0.write()是把数据从指定的硬件 UART 引脚发送出去,而print()在默认情况下是把数据发到 USB 虚拟串口(也就是电脑上看到的那个串口)。如果你想让print()的内容走硬件 UART,需要用os.dupterm(uart0)把 REPL 重定向过去,但这样 USB 虚拟串口就失效了,不太建议新手这么折腾。
所以,给设备发指令用write(),给电脑屏幕打印调试信息用print(),各司其职。
实际项目中,发送数据往往不是简单的字符串,而是要拼装协议帧。比如我要控制一个舵机模块,协议可能是“帧头+设备地址+命令+数据+校验和”:
import ustruct # 假设协议是: 0xAA 0x55 0x01 0x02 0xF8 frame = bytearray([0xAA, 0x55, 0x01, 0x02]) checksum = sum(frame) & 0xFF frame.append(checksum) uart0.write(frame)这里顺便提一下ustruct模块,它在处理二进制数据、浮点数转换时特别有用。比如你要发送一个 float 类型的温度值,直接字符串转换又慢又占字节,用ustruct.pack('f', temp)打包成 4 字节直接发出去就干净利落。
3.3 接收数据:read、readline 与中断方式如何选
接收是串口通信里最容易出问题的环节,MicroPython 提供了几种接收方式:
轮询读取:
if uart0.any() > 0: data = uart0.read() print(data)uart0.any()返回接收缓冲区中可读取的字节数,大于 0 说明有数据。这是最简单、最可控的方式,适合主循环里定期查询的场景。但缺点是你必须不断调用any(),如果主循环里还有其他耗时操作,可能会漏掉数据。
读取一行:
line = uart0.readline() if line: print(line.decode('utf-8').strip())readline()会一直等到收到换行符(\n)才返回,适合处理文本协议的数据,比如 GPS 的 NMEA 语句、AT 指令的响应等。注意它可能导致程序阻塞等待,所以最好确认对端会发送换行符。
中断 + 缓冲区:
如果你想实现“收到数据立即处理”,可以用中断。MicroPython 的 UART 支持irq触发,虽然 Pico 的 MicroPython 固件对 UART 中断支持不如 ESP32 那么完善,但基本用法没问题:
from machine import UART, Pin import micropython uart0 = UART(0, baudrate=115200, tx=Pin(0), rx=Pin(1)) rx_data = bytearray() micropython.alloc_emergency_exception_buf(100) def uart_handler(uart): while uart.any(): rx_data.extend(uart.read()) uart0.irq(handler=uart_handler, trigger=UART.IRQ_RXANY)这里trigger=UART.IRQ_RXANY表示只要有数据就触发中断。中断处理函数里不要做耗时的操作(比如 print、网络请求),只做数据存入缓冲区这种轻量任务,然后主循环再处理缓冲区的数据。这是嵌入式开发的一个基本原则:中断里做最少的事,把复杂逻辑放到主循环。
3.4 与 PC 通信的完整示例:Pico 收发回显
下面给一个可以直接抄作业的完整示例,功能很简单:Pico 初始化 USB 虚拟串口和硬件 UART,电脑通过 USB 串口发送字符,Pico 接收后原样返回,并加一个[OK]后缀。这个流程可以用来验证整个链路是否通畅。
from machine import UART, Pin import time # 初始化硬件 UART0,引脚使用 GP0/GP1 uart0 = UART(0, baudrate=115200, tx=Pin(0), rx=Pin(1)) # 初始化 USB 虚拟串口(也就是 print 输出的那个通道) # 不需要额外代码,print 默认走这里 while True: # 检查硬件串口是否有数据 if uart0.any(): data = uart0.read() # 回显到硬件串口 uart0.write(data) # 同时通过 USB 虚拟串口打印到电脑 print("Received:", data)把 Pico 的 GP0 接到 USB 转 TTL 模块的 RXD,GP1 接到 TXD,模块插到电脑上,打开串口助手发送数据,就能看到回显。
这里有个细节:如果是 USB 转 TTL 模块和 Pico 连接,两端要交叉连接,即 Pico 的 TX 接模块的 RX,Pico 的 RX 接模块的 TX,还要共地(GND 接 GND)。共地这一步很多人会忘,结果导致偶尔能收到数据但大部分时间乱码。
4. 调试工具实战:怎么让数据“看得见、查得清”
4.1 Thonny 内置串口:适合 MicroPython 开发初期
如果你在用 Thonny 写 MicroPython 代码,那它右下角的 Shell 窗口其实就是一个 USB 虚拟串口终端。你可以直接在那里看到print()输出,也可以在 Shell 里输入代码实时执行。对初期调试来说非常方便,不需要额外开串口工具。
但 Thonny 的串口功能很基础,不能自定义发送内容、不能看十六进制显示、没有时间戳。所以一旦涉及协议调试、数据量大的场景,我会切到专业的串口工具。
4.2 串口助手类工具:Windows 下的首选
Windows 下我用得最多的是SSCOM和MobaXterm自带串口功能。SSCOM 的优点是轻量、免安装、支持定时发送、支持十六进制收发,非常适合调试自定义协议。比如上面提到的舵机控制协议,我需要手动拼一个AA 55 01 02 F8这样的帧发给 Pico,在 SSCOM 里勾选“十六进制发送”,填好数据点击发送即可。
这类工具选择上的一个要点:尽量选支持“显示时间戳”和“日志保存”的工具。因为串口调试时经常需要对比“某条指令发出后多久收到响应”,或者把一整段日志导出分析。SSCOM 和 MobaXterm 都支持这些功能,能省很多事。如果你在 Linux 环境下工作,也可以用 minicom 或者 screen,但说实话体验不如 Windows 下这些图形化工具顺手。
4.3 逻辑分析仪:硬件级排查的终极武器
当串口数据还是乱码、或者你有理由怀疑电平不对的时候,就该上逻辑分析仪了。我用的是一款几十块钱的 8 通道逻辑分析仪,配合 PulseView 软件,直接夹在 Pico 的 TX/RX 引脚上抓波形,能清晰看到每一位的电平变化,从而判断波特率是否匹配、信号是否被拉低、有无毛刺干扰。
用逻辑分析仪排查乱码问题的基本思路是:
- 在 PulseView 里设置解码协议为 UART,波特率填你实际的设置。
- 抓取一段发送波形,查看解码出的十六进制数据是否和预期一致。
- 如果解码结果和设备实际发出的一模一样,说明硬件链路没问题,问题在接收端(可能是 Pico 端口配置或软件逻辑);如果解码出来就是错的,那很可能是波特率不对或电平不符合。
我第一次用逻辑分析仪时,就发现一个“玄学乱码”的根源:杜邦线太长(超过 20cm)加上接触不良,导致信号边沿出现大量毛刺,接收端误码率飙升。换成短线、重新插紧之后问题就消失了。这种问题用串口助手很难定位,逻辑分析仪一抓一个准。
4.4 波特率与校验参数速查表
调试时经常要试不同的波特率,这里有一张速查表,按设备类型推荐常规参数:
| 设备类型 | 典型波特率 | 数据格式 | 备注 |
|---|---|---|---|
| GPS 模块(NMEA) | 9600 | 8N1 | 文本协议,用 readline 解析 |
| 大多数传感器模块 | 9600 / 115200 | 8N1 | 需查看模块手册 |
| 蓝牙模块(HC-05/HC-06) | 9600 | 8N1 | AT 指令通常在 9600 |
| ESP32 / Arduino 间通信 | 115200 | 8N1 | 两边必须一致 |
| 工业设备(Modbus) | 9600 / 19200 | 8N1 或 8E1 | 注意校验位设置 |
注意:改波特率前先看手册或芯片丝印,不要盲猜。有些传感器模块默认 115200,有些默认 9600,猜错了你会浪费很多时间。
5. 常见问题与排查技巧实录
5.1 现象与原因快速对照表
我把自己和朋友们在 Pico 串口通信中遇到过的问题整理了一下,直接用表格对照,方便你定位:
| 现象 | 可能原因 | 排查思路 |
|---|---|---|
| 完全收不到数据 | 接线错误、TX/RX 没交叉 | 用万用表量电平,确认连接;检查共地 |
| 数据乱码 | 波特率不匹配、电平不匹配 | 调波特率、加电平转换;用逻辑分析仪抓波形 |
| 偶尔收到但丢数据 | 线材过长、接触不良、供电不足 | 缩短杜邦线、换线、检查电源稳定性 |
| 只发不收 | RX 引脚配置错误、对端 TX 没信号 | 确认引脚映射表,用逻辑分析仪看对端是否有输出 |
| print 能看到但 uart0.write 没输出 | 引脚选错、UART 外设冲突 | 检查 tx 引脚定义,确认没有和其他外设复用了引脚 |
| 接上某些模块后 Pico 无法启动 | 引脚电平冲突、电流过大 | 断开外设重新上电,逐一分步排查外部设备 |
第一类“完全收不到数据”,我见过最多的原因就是接线交叉搞反了。Pico 的 TX 要接对端的 RX,Pico 的 RX 接对端的 TX,这个“交叉”很多人会忘,尤其当两端都标着 TX、RX 的时候。还有一个小技巧:先用一块 USB 转 TTL 模块做自发自收测试(模块 TX 接 RXD,即自己接自己),如果自发自收正常,说明模块没问题,再排查和 Pico 的接线。
5.2 引脚冲突:一个很容易被忽略的问题
Pico 的 GPIO 是高度复用的,你可以在同一个 GPIO 上启用 UART、I2C、SPI 甚至 PWM,但 MicroPython 在初始化时会覆盖之前的引脚配置。举个例子:
from machine import Pin, I2C, UART # 先初始化 I2C,使用 GP0 (SCL) 和 GP1 (SDA) i2c = I2C(0, scl=Pin(0), sda=Pin(1), freq=400000) # 然后又初始化 UART0,也用了 GP0 和 GP1 uart0 = UART(0, baudrate=115200, tx=Pin(0), rx=Pin(1))这种情况下,后面的 UART 初始化会覆盖之前的 I2C 引脚配置,导致 I2C 设备突然不工作了。这种“隐性冲突”在复杂项目里非常坑,因为代码里没有报错,纯粹是逻辑层面的覆盖。我的建议是:做一张引脚占用表,在项目文档里列出每个 GPIO 的功能分配,每次新增外设前先查表,避免冲突。
5.3 缓冲区溢出与数据粘包处理
MicroPython 的 UART 接收缓冲区默认是 256 字节(可以通过init的rxbuf参数修改)。如果你接收的数据量大、速度快,而主循环处理不过来,缓冲区满了之后新数据会被丢弃。这会导致“偶尔丢几个字节”的奇怪现象。
处理办法有两个方向的思路:
一是增大缓冲区:
uart0 = UART(0, baudrate=115200, tx=Pin(0), rx=Pin(1), rxbuf=1024)二是尽快读取,在主循环里及时read()清空缓冲区。如果数据是按帧到达的(比如 10 字节一帧),可以在每次读取后判断是否凑够一帧再处理。另外,处理“粘包”问题时(即一帧数据被拆成多段到达),需要自己实现一个简易的帧解析器,按帧头/帧尾或固定长度进行分帧。
# 简易分帧:按换行符分帧 buffer = bytearray() while True: if uart0.any(): buffer.extend(uart0.read()) while b'\n' in buffer: line, buffer = buffer.split(b'\n', 1) process_line(line)5.4 实测心得:为什么我的 Pico 舵机控制总是抖动
最后分享一个具体案例。有朋友私信问“树莓派 Pico 控制舵机”时抖动严重、响应卡顿。我一看代码,他用的是软件模拟 PWM(time.sleep方式控制舵机脉宽),同时又在主循环里轮询串口接收数据。结果每次接收数据时要执行read()和print(),这些操作占用了大量时间,导致舵机脉宽输出不均匀、抖动。
这个问题的本质是:串口接收和 PWM 输出在同一个阻塞式主循环里互相争抢 CPU。解决办法有两个方向:
一是把串口接收放到中断里,主循环只处理数据、控制舵机:
from machine import Pin, PWM, UART import time servo = PWM(Pin(15), freq=50) uart0 = UART(0, baudrate=115200, tx=Pin(0), rx=Pin(1)) cmd_buffer = bytearray() def uart_handler(uart): while uart.any(): cmd_buffer.extend(uart.read()) uart0.irq(handler=uart_handler, trigger=UART.IRQ_RXANY) while True: if cmd_buffer: # 解析命令并控制舵机 cmd = bytes(cmd_buffer) cmd_buffer.clear() # 设置舵机占空比...二是在不要求高实时性的场景,直接把串口数据解析放到主循环的一个时间片中,控制好每次处理的耗时。我在实际项目中推荐方案一,中断能最大程度保证舵机信号的连续性。这和串口通信本身关系不大,但却是通信与执行协同时的常见问题,拿出来提醒一下。
写在最后
串口通信在 Pico 开发里真的算是入门到进阶的必经之路。硬件上记住两件事:电平 3.3V 别乱接 5V,TX/RX 要交叉、必须共地。软件上记住两件事:发送用write()、调试打印用print(),接收优先用中断加缓冲区。调试工具方面,初期 Thonny 够用,数据复杂了就上 SSCOM 这类串口助手,还查不出来就上逻辑分析仪。
踩过几次坑之后,我最大的体会是:串口通信出问题,“软件问题”往往只是表象,真正的原因十有八九在硬件链路上——接触不良、线太长、电平不对、共地缺失。所以不要一上来就反复改代码,先用万用表和逻辑分析仪把链路确认干净,再回头查程序,能省下大把时间。这块搞明白了,后面做传感器采集、设备联调、甚至多板通信都会顺手很多。