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 引脚可选UART0GP0、GP12、GP16GP1、GP13、GP17UART1GP4、GP8、GP20GP5、GP9、GP21这里有个细节要注意MicroPython 的machine.UART初始化时如果你指定了tx和rx引脚它会自动配置引脚复用不需要像 C 语言那样手动操作 GPIO 功能选择寄存器。但这也意味着同一时刻一个 UART 外设只能使用一组 TX/RX 引脚你没法让 UART0 同时在 GP0 和 GP12 上输出数据。按我自己的使用习惯默认首选GP0TX和 GP1RX因为这两个引脚在 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 的 GP1RX上结果串口收到的全是乱码查了半天波特率、接线最后用万用表一量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, baudrate115200, txPin(0), rxPin(1), bits8, parityNone, stop1)这里有几个参数值得展开说说UART(0)选择 UART0 外设。如果你要用 UART1这里就写 1同时 tx/rx 引脚要选 UART1 对应的引脚比如 GP4/GP5。baudrate115200波特率。常见的选择有 9600、57600、115200。115200 是绝大多数场景下的“默认值”速度快、误码率也能接受。如果通信距离长或者线材质量差适当降到 9600 会更稳定。txPin(0), rxPin(1)指定 TX 和 RX 引脚。注意是Pin(0)而不是0MicroPython 的 UART 初始化要求传入 Pin 对象。bits8, parityNone, stop18 数据位、无校验、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, baudrate115200, txPin(0), rxPin(1)) rx_data bytearray() micropython.alloc_emergency_exception_buf(100) def uart_handler(uart): while uart.any(): rx_data.extend(uart.read()) uart0.irq(handleruart_handler, triggerUART.IRQ_RXANY)这里triggerUART.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, baudrate115200, txPin(0), rxPin(1)) # 初始化 USB 虚拟串口也就是 print 输出的那个通道 # 不需要额外代码print 默认走这里 while True: # 检查硬件串口是否有数据 if uart0.any(): data uart0.read() # 回显到硬件串口 uart0.write(data) # 同时通过 USB 虚拟串口打印到电脑 print(Received:, data)把 Pico 的 GP0 接到 USB 转 TTL 模块的 RXDGP1 接到 TXD模块插到电脑上打开串口助手发送数据就能看到回显。这里有个细节如果是 USB 转 TTL 模块和 Pico 连接两端要交叉连接即 Pico 的 TX 接模块的 RXPico 的 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 模块NMEA96008N1文本协议用 readline 解析大多数传感器模块9600 / 1152008N1需查看模块手册蓝牙模块HC-05/HC-0696008N1AT 指令通常在 9600ESP32 / Arduino 间通信1152008N1两边必须一致工业设备Modbus9600 / 192008N1 或 8E1注意校验位设置注意改波特率前先看手册或芯片丝印不要盲猜。有些传感器模块默认 115200有些默认 9600猜错了你会浪费很多时间。5. 常见问题与排查技巧实录5.1 现象与原因快速对照表我把自己和朋友们在 Pico 串口通信中遇到过的问题整理了一下直接用表格对照方便你定位现象可能原因排查思路完全收不到数据接线错误、TX/RX 没交叉用万用表量电平确认连接检查共地数据乱码波特率不匹配、电平不匹配调波特率、加电平转换用逻辑分析仪抓波形偶尔收到但丢数据线材过长、接触不良、供电不足缩短杜邦线、换线、检查电源稳定性只发不收RX 引脚配置错误、对端 TX 没信号确认引脚映射表用逻辑分析仪看对端是否有输出print 能看到但 uart0.write 没输出引脚选错、UART 外设冲突检查 tx 引脚定义确认没有和其他外设复用了引脚接上某些模块后 Pico 无法启动引脚电平冲突、电流过大断开外设重新上电逐一分步排查外部设备第一类“完全收不到数据”我见过最多的原因就是接线交叉搞反了。Pico 的 TX 要接对端的 RXPico 的 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, sclPin(0), sdaPin(1), freq400000) # 然后又初始化 UART0也用了 GP0 和 GP1 uart0 UART(0, baudrate115200, txPin(0), rxPin(1))这种情况下后面的 UART 初始化会覆盖之前的 I2C 引脚配置导致 I2C 设备突然不工作了。这种“隐性冲突”在复杂项目里非常坑因为代码里没有报错纯粹是逻辑层面的覆盖。我的建议是做一张引脚占用表在项目文档里列出每个 GPIO 的功能分配每次新增外设前先查表避免冲突。5.3 缓冲区溢出与数据粘包处理MicroPython 的 UART 接收缓冲区默认是 256 字节可以通过init的rxbuf参数修改。如果你接收的数据量大、速度快而主循环处理不过来缓冲区满了之后新数据会被丢弃。这会导致“偶尔丢几个字节”的奇怪现象。处理办法有两个方向的思路一是增大缓冲区uart0 UART(0, baudrate115200, txPin(0), rxPin(1), rxbuf1024)二是尽快读取在主循环里及时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 控制舵机”时抖动严重、响应卡顿。我一看代码他用的是软件模拟 PWMtime.sleep方式控制舵机脉宽同时又在主循环里轮询串口接收数据。结果每次接收数据时要执行read()和print()这些操作占用了大量时间导致舵机脉宽输出不均匀、抖动。这个问题的本质是串口接收和 PWM 输出在同一个阻塞式主循环里互相争抢 CPU。解决办法有两个方向一是把串口接收放到中断里主循环只处理数据、控制舵机from machine import Pin, PWM, UART import time servo PWM(Pin(15), freq50) uart0 UART(0, baudrate115200, txPin(0), rxPin(1)) cmd_buffer bytearray() def uart_handler(uart): while uart.any(): cmd_buffer.extend(uart.read()) uart0.irq(handleruart_handler, triggerUART.IRQ_RXANY) while True: if cmd_buffer: # 解析命令并控制舵机 cmd bytes(cmd_buffer) cmd_buffer.clear() # 设置舵机占空比...二是在不要求高实时性的场景直接把串口数据解析放到主循环的一个时间片中控制好每次处理的耗时。我在实际项目中推荐方案一中断能最大程度保证舵机信号的连续性。这和串口通信本身关系不大但却是通信与执行协同时的常见问题拿出来提醒一下。写在最后串口通信在 Pico 开发里真的算是入门到进阶的必经之路。硬件上记住两件事电平 3.3V 别乱接 5VTX/RX 要交叉、必须共地。软件上记住两件事发送用write()、调试打印用print()接收优先用中断加缓冲区。调试工具方面初期 Thonny 够用数据复杂了就上 SSCOM 这类串口助手还查不出来就上逻辑分析仪。踩过几次坑之后我最大的体会是串口通信出问题“软件问题”往往只是表象真正的原因十有八九在硬件链路上——接触不良、线太长、电平不对、共地缺失。所以不要一上来就反复改代码先用万用表和逻辑分析仪把链路确认干净再回头查程序能省下大把时间。这块搞明白了后面做传感器采集、设备联调、甚至多板通信都会顺手很多。