在嵌入式开发里,USB 相关的东西一直有点“高门槛”的意思,尤其是当你手头只有一个树莓派 Pico 的时候——就一个 Micro-USB 口,又当供电又当下载又当调试。很多朋友一开始都觉得,这板子是不是太“素”了点?其实恰恰是这样一个不起眼的 USB 口,只要你会玩 USB-CDC 虚拟串口,它就能变成一台真正意义上的“上位机通信终端”,再配合 MicroPython 和 select 多路复用,完全可以实现一套非常轻量、稳定的通信系统。
这篇文章我就把自己在实际项目里折腾 Pico 虚拟串口、select 非阻塞调度、以及 MicroPython 底层驱动的完整心得整理出来。不吹概念,直接讲怎么搭、怎么踩坑、怎么把通信做得又稳又省心,适合已经在用 MicroPython 写 Pico 程序、想进一步把通信架构做扎实的朋友参考。
1. 内容整体设计与思路拆解
1.1 为什么选择 USB-CDC 而不是 UART
先说实话,UART 串口在单片机通信里仍然是绝对主力,但是一旦你要对接 PC、手机或者树莓派这类“大设备”,UART 有几个痛点特别明显:电平要转换(3.3V vs 5V 逻辑)、插拔依赖 USB-TTL 转换器、驱动兼容性参差不齐、部分新电脑还没有传统串口。USB-CDC 是 USB 通信设备类(Communication Device Class)里专门做串口模拟的协议,Pico 原生 USB 控制器直接支持,固件一烧进去,电脑端自动识别成一个 COM 口(Windows)或者 /dev/ttyACM0(Linux),不需要任何额外硬件,一根线就同时解决了供电、下载和通信。
从实际体验来说,USB-CDC 最吸引我的地方是它把“设备端”和“上位机端”的链路统一了。也就是说,你不需要摸清楚 UART 那边到底用的是哪一颗电平转换芯片、有没有焊对 RX/TX、地线是不是共地,只需要保证 USB 线物理连通、固件里启用了 USB 设备服务,剩下的事情就是像操作一个普通串口那样收发数据。
1.2 MicroPython 固件层面的支持情况
树莓派 Pico 官方 MicroPython 固件默认就启用了 USB-CDC 作为 REPL 串口。很多人一开始容易误解“REPL 能用不就是串口能用吗”,实际上 REPL 走的是 MicroPython 内部封装好的交互通道,主动权在解释器手上;而你自己的业务代码要用虚拟串口收发数据,必须通过machine.UART的 CDC 模式,或者直接引用 Pico 特定的usb_cdc模块。
需要注意,官方固件里usb_cdc默认不一定会暴露成普通的 UART 接口,不同版本差别挺大。我习惯的做法是优先刷新版官方固件,并且尽量选择支持select模块的构建。部分第三方固件虽然功能花哨,但在 select 支持上有缺斤少两的情况,这在后文我会专门讲。如果你的项目只需要纯 CDC 通信、不想要 REPL 干扰,还可以考虑裁剪固件、把 REPL 重新映射到 UART0,这样 USB-CDC 就能完全腾出来给业务层用,很多量产设备的做法就是这个思路。
1.3 为什么引入 select 机制
串口数据到底什么时候来?来多少?传统做法就是死循环轮询,或者开线程阻塞读取。轮询的代价是 CPU 空转、数据可能丢;线程在 MicroPython 这种资源极度受限的环境里又容易出现栈溢出、调度抖动。select 是 Unix/Linux 世界非常经典的 I/O 多路复用机制,它能让你“同时关心多个文件描述符的可读/可写状态”,而线程/主循环在数据没到位时进入系统睡眠,不占用 CPU,一旦数据就绪,select 会立刻返回并告诉你哪个设备能操作了。
把这种思路用在 Pico 的 USB-CDC 上特别合适。因为 USB 通信天然就是突发式的——你永远不知道上位机什么时候会丢一条指令过来,被动地靠循环去读,很难兼顾“响应及时”和“低功耗”。select 的引入,相当于给通信逻辑装了一个“事件闹钟”,而且这个闹钟是硬件级别的,不是软件空转。
我在实际项目里用 select 之后,主循环的代码变得非常清晰:先 select 等数据,有数据才处理,没数据该睡就睡(甚至可以配合machine.lightsleep()做低功耗)。这比用while uart.any()好太多,也远比用time.sleep+ 轮询的组合更容易调试——因为你不再需要估算“数据多久能到,我该休息多少毫秒才不会漏数据”。
2. 核心细节解析与实操要点
2.1 USB-CDC 在 MicroPython 中的两种工作形态
MicroPython 在 Pico 上实现 USB-CDC 主要有两套路径。第一套是直接用os.dupterm_notify和 REPL 相关的机制,这主要是给交互终端用的;第二套是把 USB-CDC 对象挂载为一个真正的 stream 类对象,供用户程序read、write、select。我在项目里使用的是第二套,因为它更干净、行为也更容易预测。
如果你刷的是较新的官方 MicroPython 固件,可以在 REPL 里直接试试:
import usb_cdc print(dir(usb_cdc))正常情况你会看到类似data、console这两个通道。usb_cdc.console对应着交互 REPL 通道,如果你把 REPL 继续留在这个通道上,那业务代码再用它读写就会有冲突,最典型的表现就是你自己print到 console 和 REPL 的提示符混在一起。项目实战时我建议把 REPL 移走,或者在固件里重新导向,让usb_cdc.data成为干净的业务通信管道。
2.2 让虚拟串口“可被 select”的关键条件
很多人在 Pico 上用 select 时发现,代码一跑就报OSError: [Errno 22] EINVAL,或者 select 直接返回但读不到字节。问题几乎都出在“对象不满足 stream 协议”或者“底层驱动不支持非阻塞挂起”上。USB-CDC 的驱动在 MicroPython 中能不能被 select,固件版本差异非常明显。老版本固件里,CDC 设备对象虽然有read和write,但内部没有完整实现ioctl里的 poll 回调,select 自然就无从生效。
所以实操第一步,请务必确认固件版本。我的建议是直接到树莓派官方 MicroPython 页面下载最新稳定版,然后跑一遍这个小实验:
import select import usb_cdc r, w, x = select.select([usb_cdc.data], [], [], 1) print(r, w, x)正常情况下,select会正常执行不会报错,1 秒后返回三个空列表。如果你的环境异常,优先考虑换固件,不要浪费时间在代码层面硬凑。
这里补充一个“为什么不能硬凑”的底层逻辑:select依赖 poll 机制去注册回调,USB-CDC 的中断回调只有在 USB 总线层面收满一个包、或者发生 RESET/SOF 等事件时才会触发。如果固件驱动层没有把这个事跟 poll 挂钩,用户态程序无论怎么加循环也等不到“数据就绪”的通知,这是驱动层的性质,不是业务代码能补的。
2.3 select 超时参数和微控制器的兼容性差异
在 PC 上用 select,超时精度是毫秒级、微秒级,随便写。但是在 Pico 这种 MCU 上,超时参数的语义和硬件定时器精度挂在一起,select.select(rlist, wlist, xlist, timeout)的 timeout 在文档里以秒为单位,可以传浮点数。我自己实测下来,小数值在内部是能被 MicroPython 的moduselect正确转换成 tick 的,但是如果你传一个非常小、小到小于系统时钟最小粒度的值,它可能会被当成“立即返回”处理。
另外还有个冷门坑:当你不传 timeout 参数(即阻塞等待),Pico 的 select 会进入一个无限等待状态。这个等待通常是在底层事件标志上挂起的,但如果你同时想用machine.lightsleep()来省电,就要特别小心——lightsleep可能把 USB 外设也睡糊涂,导致 USB 枚举直接断开。我后面会专门讲这个问题,这里先记住一句:USB-CDC 和 light sleep 的兼容性需要单独验证,不能默认没问题。
2.4 虚拟串口与普通 UART 在行为上的差异
有人可能会问,同样是串口,虚拟串口和 UART 在程序里用起来是不是差不多?行为上有几个不可忽略的差异。第一,UART 是逐字节缓冲,你随时可以uart.any()拿到缓冲计数;而 USB-CDC 以数据包为传输单元,你在 PC 端发 5 个字节,可能作为一个 USB 包到达,也可能因为 PC 端驱动缓冲、TCP/Nagle 类似策略等被合并或拆分。第二,UART 的write通常如果你发太快,驱动的 FIFO 满了会阻塞或者掉数据;USB-CDC 的write则是往 USB 端点 FIFO 里写,满了会触发 USB 层的流控,你并不总能实时感知。第三,USB-CDC 设备在主机断电、线缆断开、主机休眠这些场景下,设备端可能会检测到disconnect或者 stall,而 UART 只要还在上电状态就一直认为链路是通的。
所以你在设计通信协议时,不能像传统 UART 一样信任“发出去就是发出去了”,一定要在应用层做帧校验、超时重传、或者至少要有心跳机制。我在项目里采用的做法是:上位机每 50ms 发一个心跳,Pico 必须 500ms 内回一个 ack,否则上位机主动重启 CDC 句柄。这套机制前前后后帮我在调试连接稳定性上省了非常多的精力,尤其是设备反复插拔以后,链路自己恢复不了,但重开句柄几乎都能救回来。
3. 实操过程与核心环节实现
3.1 环境准备:固件烧录、库文件与基础硬件连接
在开始写通信逻辑之前,先把基础环境确认好,不然后面到处是“奇怪的问题”。
硬件清单其实很简单:一块树莓派 Pico(建议 Pico 或 Pico W 都行,但 Pico W 的无线和 USB 共存时偶尔会有小干扰,纯通信场景用普通 Pico 更干净)、一根质量不错的 Micro-USB 数据线(别用只能充电的线)、一台安装了串口助手的电脑。
固件部分,我推荐去树莓派官方 MicroPython 页面下载.uf2文件。按住 Pico 板子上的 BOOTSEL 键,再用 USB 线接电脑,会出现一个名为 RPI-RP2 的 U 盘,把 uf2 拖进去就自动烧录完成。烧录完设备管理器里会多出一个 USB 串行设备(COM 口),在 Linux 下就是 /dev/ttyACM0,这一步是验证 USB-CDC 枚举是否成功的最快方式。
我画过一个最简单的自检流程,不建议跳过:先用串口助手打开 COM 口,波特率随便填(CDC 是虚拟串口,波特率不影响实际传输),然后按 Pico 上的复位键,如果串口助手能跳出一段 MicroPython 的启动 banner,就说明 CDC 通道完全可用。如果这里都不通,后文所有工作都无从谈起,所以这一步值得花两分钟确认。
3.2 业务代码整体框架:主循环 + select + 状态机
贴一个我在项目里实际使用的精简框架。这个框架里,USB CDC 被当成唯一的“数据管道”,主循环用 select 同时监听它和可选的 UART 调试口。这样做的核心好处是:代码里不会出现“忙于轮询而漏掉 USB 消息”的问题,而且逻辑层次非常清晰。
import select import usb_cdc import machine import time # 先把 REPL 转移到 UART0,避免和 CDC 数据通道抢资源 uart_debug = machine.UART(0, 115200) os.dupterm(uart_debug) # 此时 usb_cdc.data 成为纯业务通信通道 poll_obj = select.poll() poll_obj.register(usb_cdc.data, select.POLLIN) while True: events = poll_obj.poll(100) # 超时100ms,单位毫秒 if not events: # 无数据时可做周期性任务、喂狗、更新传感器等 pass for event in events: fd, ev = event if ev & select.POLLIN: # 能走到这里,说明 CDC 接收缓冲区至少有1个字节 data = fd.read(64) # 按块读取,避免一次读太多卡住协议 handle_packet(data)这里有两个细节值得展开讲。第一,poll_obj.poll(100)的超时单位是毫秒,注意它是从select.select那里沿袭来的,但是 sign 和文档一样,容易混淆;第二,fd.read(64)里的 64 不是随意选的,它对应了 USB 全速设备一个端点包的最大字节数。CDC 底层批量传输端点通常是 64 字节,你一次读太多,反而会迫使底层去凑满整个请求;一次读 64,刚好一个包一个包地消费,逻辑上最好对齐。
3.3 数据帧设计与粘包处理
PC 端发数据,跟 TCP 一样会“粘包”。比如你要 Pico 执行一条MOTOR:100指令,PC 端也许连续发了两条指令,Pico 一次 read 拿到的可能是整串MOTOR:100MOTOR:200,也可能被截成半个包。不要指望底层帮你分帧,协议设计必做。
我的习惯是把帧设计成“帧头 + 长度 + 有效负载 + 校验”的结构。帧头用两个固定字节比如0xAA 0x55,长度是 1 字节,有效负载最长 255 字节,校验用简单的累加和。这样一个包最大也不会超过 259 字节,在 CDC 里不会产生分帧歧义;实现起来也不复杂。
FRAME_HEADER = b'\xaa\x55' def parse_stream(buffer): """从字节流中解析出完整帧,返回剩余未处理数据""" while True: if len(buffer) < 4: break # 至少要有帧头2字节 + 长度1字节 + 校验1字节 if buffer[0] != 0xAA or buffer[1] != 0x55: # 帧头不匹配,丢弃一个字节继续扫 buffer = buffer[1:] continue length = buffer[2] if len(buffer) < 4 + length: break # 包还没收完,等下一批数据 payload = buffer[3:3+length] checksum = buffer[3+length] if sum(payload) & 0xFF == checksum: handle_command(payload) buffer = buffer[4+length:] else: # 校验失败,把整帧丢弃 buffer = buffer[4+length:] return buffer为了配合这个解析器,主循环里的消费逻辑要维护一个全局的rx_buffer,每次read完就 append 进去,然后交给parse_stream解析。这个模式跟 PC 端网络编程的 ring buffer 异曲同工,只是实现被简化到了极致。
3.4 接上真实的“树莓派 Pico 控制舵机”场景
既然热搜词里特别关注了“树莓派 pico 控制舵机”,我就把这个应用场景完整串起来。舵机控制本身不复杂,无外乎 PWM 输出的脉冲宽度控制在 0.5ms 到 2.5ms 之间,对应舵机 0 到 180 度。Pico 上用machine.PWM很容易实现。难点在于:当你的上位机通过虚拟串口发来SERVO:90这类指令时,Pico 不仅要正确解析这条指令,还要在“同时可能还有别的数据不断到达”的情况下,保证每个指令都有稳定、平滑的执行效果。
这种“又要通信、又要控制”的典型场景,恰恰是 select 机制最能发挥价值的地方。我建议你用独立函数封装舵机控制,而主循环只负责“解析指令、调度执行”,两者不要混在一起。比如这样:
from machine import Pin, PWM servo = PWM(Pin(15)) servo.freq(50) # 50Hz,20ms周期 def set_servo(angle): # 角度范围0~180,脉宽范围0.5ms~2.5ms duty_us = int(500 + (angle / 180) * 2000) # 单位微秒 servo.duty_us(duty_us)配合handle_command里的分发逻辑:
def handle_command(payload: bytes): try: cmd = payload.decode().strip() if cmd.startswith('SERVO:'): angle = int(cmd.split(':')[1]) set_servo(angle) except Exception as e: print('cmd error:', e)这里有个我踩过的坑:舵机 PWM 频率设为 50Hz,Pico 的 PWM 时钟在 125MHz 下,duty_us的分辨率足够,但如果同时开了 WiFi(Pico W)或者 USB 频繁传输,偶尔会出现抖动。所以我建议 PWM 用的是独立引脚,驱动电流尽量外加独立的舵机电源,不要在 Pico 的 3.3V 上直接拉大电流舵机。
3.5 上位机联调与验证方法
Pico 端的代码写完,一定要做联调,这是整个项目能不能落地的关键环节。我最常用的上位机工具是 Python 的pyserial,简单直接,适合快速验证协议。一个最简单的测试逻辑:
import serial import time ser = serial.Serial('/dev/ttyACM0', 115200, timeout=1) for angle in [0, 45, 90, 135, 180]: cmd = f'SERVO:{angle}\n'.encode() ser.write(cmd) time.sleep(1)如果你用的是 Windows,COM 口号需要自己查设备管理器。pyserial发出去以后,观察舵机是否依次转到位,同时看 Pico 端是否收到了正确的帧。打印调试信息可以通过dupterm转移到 UART0,这样print的内容就不会污染 USB-CDC 的业务链路,也不会干扰上位机发指令。
我不知道有没有人跟我一样,一开始图省事直接把print打到 USB-CDC,结果发现上位机读回来的数据里混了一堆>>>提示符和 banner 文本。这种问题排查起来很隐蔽,因为看起来像设备“乱码”,实际上是你把 REPL 通道跟数据通道混用了。尽早把 REPL 转移到 UART0,是避免这类问题最简单的手段。
4. 常见问题与排查技巧实录
4.1 select 报错或者不生效,怎么办
这是提问率最高的一类问题。首先要做版本确认:升级固件是最直接的试错方案,但改完固件后记得重跑一遍最基础的 select 自检代码。如果你使用的是第三方固件并且出现OSError: [Errno 22],大概率是该固件没有为 USB-CDC 设备实现真正的 poll 回调,没法通过打补丁解决,唯一可靠的方向就是切回官方固件或重新编译固件。
其次是确认“注册对象”是否正确。usb_cdc.console和usb_cdc.data是两条不同的流,选错了对象就等不到数据。如果你希望 USB-CDC 完全作为业务通道,就一定要把 REPL 通过os.dupterm转走。之前讲过 REPL 和业务读写同一个通道会互相干扰,在 select 场景下,这种干扰往往表现为“select 永远有数据,但 read 读出来的是 REPL 控制字节”,让人一头雾水。
4.2 上位机显示乱码或者输出夹杂提示符
乱码大概率是“数据通道与 REPL 通道混用”造成的。使用os.dupterm(uart_debug)后,REPL 被转给了 UART0,通常在拔掉串口助手连接后,USB-CDC 的 console 通道不再输出任何内容,usb_cdc.data只负责业务数据。注意dupterm只能同时绑定一个终端,如果你调用两次,最后一次生效,这是 MicroPython 的设计行为。
另外还有个常见细节:如果上位机打开串口时 Pico 正好在输出启动 banner,那这一轮数据流会被污染。所以嵌入式设备做业务通信时,建议把“启动打印”全部放到调试串口(UART0),USB-CDC 业务通道尽量保持“静默”,等你正式开启通信协议后才开始收发。
4.3 USB 断开重连后,设备“假死”怎么解决
这个问题最难缠。现象是:程序跑得好好的,拔掉 USB 线再插回去,电脑能识别新节点,但 Pico 程序已经没有反应了。多发生在“整机断电重连”的情况下,部分固件的 USB-CDC 驱动在经历总线复位后有概率挂掉。
我自己摸索出来的处理经验有两条。第一条是在设备端增加“USB 状态监控”,以固定周期检查usb_cdc.connected或者底层的中断回调,一旦发现断开,主循环立刻清理所有缓冲区和状态,等待枚举完成。注意usb_cdc.connected是个只读属性,不同固件中名称可能略有差异,如果找不到就用dir(usb_cdc.data)去列出当前属性和方法。第二条是上位机端的“重建策略”:当检测到串口失去响应时,不要单纯重试读写,而是关闭整个serial.Serial句柄,等待 1~2 秒,再重新打开。实测这种“重开句柄”比在同一个句柄里疯狂重传有效得多,毕竟 USB 枚举层面如果都异常了,上层协议很难单独恢复。
4.4 Pico 休眠模式与 USB-CDC 的兼容性问题
说句掏心窝的话,不要轻易在启用 USB-CDC 的同时使用machine.lightsleep或machine.deepsleep来省电。USB 是要靠主机不断轮询的,设备端一旦睡眠,USB 控制器无法响应主机的 SOF 包,主机会在一段时间后判定设备异常并断开连接,等你醒来再想恢复链路,就得重新枚举。这中间的时间和握手成本,往往比你把主频降到最低但保持 USB 活跃还要高。
如果你确实需要考虑低功耗场景,我的建议是把通信链路和低功耗策略做物理隔离——需要长期待机时直接关闭 USB-CDC 的发送,但保持 USB 外设活跃;真要做到整体睡眠,请在睡眠前主动让 USB 脱离,醒来后重建链路,不要指望 MicroPython 帮你自动恢复。
4.5 常见问题速查表
为了方便对照排查,我把典型问题和处理方向整理成一张表,大家可以直接参考:
| 现象 | 可能原因 | 解决思路 |
|---|---|---|
| select 调用即报 Errno 22 | 固件不支持 CDC poll 回调 | 换官方最新固件或重新编译 |
| select 超时返回,但没数据 | REPL 占用了 CDC 数据通道 | 用 os.dupterm 转移 REPL 到 UART |
| 收到乱码与提示符 | 业务通道与交互通道混流 | 分开两条通道,禁用 CDC 启动横幅 |
| 拔插后设备无响应 | USB 枚举状态未重建 | 上位机重开串口句柄,设备端需要监控连接状态 |
| 舵机抖动、指令丢帧 | PWM 与 USB 调度竞争,或供电不足 | 独立舵机电源、适当调低频率、指令帧增加校验 |
5. 工具选型与开发环境建议
5.1 固件的选择:官方版还是第三方定制版
“选固件”这件事被很多人低估了。官方固件的优势是稳定、API 齐全、升级及时,而且select的底层支持比较可靠;缺点是你没法随意裁剪,功能冗余但占用 Flash 并不大。第三方定制固件通常会在你用到某些冷门外设时更友好,但在 USB 栈这种“边缘模块”上,优化不一定跟得上官方,甚至有人魔改过后的usb_cdc行为会出现偏差。
我的建议是:如果你的项目对 USB-CDC 的稳定性要求很高,一切以官方固件为基准。先跑通完整的通信链路,再去评估是否需要定制。不要拿着一个花了三小时编译的第三方固件回头来找“为什么我的 select 不工作”,那会浪费更多时间。
5.2 上位机调试工具的选择
除了 Pythonpyserial之外,有几个工具我强烈推荐你备着。minicom在 Linux 下看原始串口文本很方便;CoolTerm在 Mac 和 Windows 下非常适合做十六进制收发测试;如果你需要模拟协议自动化测试,pyserial加一段简单的 Python 脚本足够应付 90% 场景。不要在一开始就上重量级的串口调试软件,那些工具各有各的好,但它们的自动加换行、自动时间戳等特性反而会干扰你对原始数据流的判断。
个人最常用的联调流程是:先在 CoolTerm 里手动发一帧SERVO:90验证舵机动作,再用 Python 脚本做批量循环和异常指令测试。这两种方式互补,前者快速定位物理链路问题,后者验证协议健壮性。
5.3 MicroPython 代码的组织结构
MicroPython 代码量一大,单文件堆起来调试会非常痛苦,建议用包的方式组织项目。目录结构大致像这样:
project/ main.py # 入口,初始化并启动主循环 comm/ __init__.py cdc_handler.py # USB-CDC 与 select 封装 protocol.py # 帧解析与指令分发 devices/ __init__.py servo.py # 舵机控制 config.py # 引脚、参数、协议常量集中管理main.py 里只做三件事:加载 config、初始化各模块、启动 select 主循环。业务指令的解析和执行被拆到 protocol.py 和 devices/ 里,这样你要扩展新的设备控制命令,只在 protocol.py 增加一个分支就行,不需要翻主循环代码。MicroPython 对__init__.py的支持稍弱,但是按目录导入.py模块是完全正常的,放心用。
6. 项目经验总结与再扩展
6.1 从通信底层到上层应用的心得体会
整套系统跑通以后,你会明显感觉到“以 USB-CDC 为骨架、以 select 为调度器”的架构,在 Pico 这种小资源平台上具备很强的通用性。通信链路稳定之后,上层加多少个传感器、多少种控制指令,都只是协议分发层面的“填空”。
不要小看 Pico 这样一个几块钱的板子,利用好 USB-CDC 和 select,它完全可以承担“低成本 USB 转接网关”“桌面小设备控制器”“传感器数据采集上报终端”等多种角色。相比 Arduino、STM32 的方案,它的开发效率高非常多——MicroPython 在 REPL 里改一行指令立刻生效,不需要编译烧录等待,做快速原型迭代的时候这种反馈速度太重要了。
6.2 项目还能往哪些方向扩展
围绕这套底座,有几个很自然的扩展方向。第一,把 USB-CDC 的数据桥接到 WiFi 上,Pico W 就能变成一台“USB 转 WiFi 串口服务器”,上位机从远程直接读写本地 USB 设备;第二,把 select 监听扩展到多个对象,比如同时监听 USB-CDC、外部 UART、甚至 I2C 从设备的中断标志,这样一台板子就能统一调度多种通信接口,真正实现“I/O 多路复用”的价值;第三,如果你需要更高的吞吐,可以研究官方固件的ioctl接口,或者直接基于 C SDK 写 TinyUSB 的 callback,把 USB-CDC 的缓冲区改成 DMA 驱动,这样通信效率还能上一个台阶。
6.3 给后来者的一些叮嘱
最后说回实操层面。我见过太多人刚开始搞 Pico 通信,就把精力全押在“代码怎么写”上,却忽略了固件版本、通道分离、供电、协议设计这些基础问题。这些基础问题不处理好,代码再漂亮也会在关键时刻给你来一下暴击。所以我的建议是:动手之前先花半天时间把固件环境、串口链路、select 自检这些“地基”彻底摸一遍,确认稳了再往上盖楼;开发过程中每一层都做最小验证,不要等所有代码写完再一次性测。这样你永远知道问题出在哪一层,排查成本会低一个数量级。
从我个人的使用体验来说,USB-CDC + select + MicroPython 这套组合在树莓派 Pico 上完全够用,而且踩坑空间远比想象中少。你只要按照这篇的思路把底层链路和通信协议捋顺,后面无论是做舵机控制、数据采集,还是跟电脑上的 GUI 程序做联动,都会非常顺手。