树莓派 Pico USB 机制解析:从 CDC 虚拟串口到 MicroPython 实战

发布时间:2026/9/7 12:17:30
树莓派 Pico USB 机制解析:从 CDC 虚拟串口到 MicroPython 实战
树莓派 Pico 这块小板子插上 USB 线能让电脑认出串口、跑 MicroPython REPL但很多人没把这背后的机制当回事——以为它只是一个“烧录程序的通道”。实际上Pico 的 USB 口背后是 RP2040 芯片里完整的 USB 控制器再往上走是 USB 1.1 全速协议、设备枚举、CDC 虚拟串口这一整套协议栈。你如果把硬件原理和外设架构搞明白再配合 MicroPython 去控制Pico 就不再只是一块点灯板而是能和电脑、手机甚至其他嵌入式设备做实时通信的 USB 外设。这篇内容适合刚买 Pico 的新手也适合玩过 Arduino、想进一步理解 USB 底层的开发者。1. 从整体看 Pico 的 USB特性、选型与设计定位1.1 USB 1.1 全速够用但不将就树莓派 Pico 官方资料里写的 USB 规格是“USB 1.1 host/device”这里有两个容易误解的点。第一RP2040 内置的是 USB 1.1 控制器同时支持 Host 和 Device 两种模式。但 MicroPython 官方固件默认只把 Pico 当 Device 用也就是插到电脑上作为一个外设。第二USB 1.1 的传输速率是 12Mbps。很多人以为 USB 1.1 就是很古老的 1.5Mbps 低速其实不是这样——USB 1.1 规范里规定了两档速率1.5Mbps Low Speed 和 12Mbps Full Speed。Pico 用的是 Full Speed也就是 12Mbps。后来 USB 2.0 标准也沿用了这档速度所以你在很多资料里会看到 USB 2.0 Full Speed 的说法它和 USB 1.1 在速率上是同一档。为什么不上高速的 USB 2.0 High-Speed480Mbps本质是成本和功耗的权衡。RP2040 是一颗面向 MCU 市场的双核 Cortex-M0 控制器它不需要像 PC 那样动辄几百 MB/s 的传输带宽。12Mbps 已经足够承载串口控制、小批量数据、甚至音频流。对嵌入式项目来说USB 1.1 控制器在硅片面积、时钟需求、功耗上都比高速控制器低一大截。这个思路在硬件选型时很常见——看起来参数低但放在实际场景里刚好够用还能省电省成本那它就是最合理的选择。1.2 主从模式与 MicroPython 固件的默认视角RP2040 的 USB 控制器支持 Host 和 Device 双角色这意味着理论上 Pico 可以当 USB 主机比如接一个 USB 键盘。但 MicroPython 官方固件目前默认只提供 Device 模式下的 CDC 串口功能你要想真正把 Pico 当 USB Host 用得走 TinyUSB 或 Pico SDK 的 Host 示例工程开发方式完全是另一套体系。从 MicroPython 的角度看固件在板子上电时就会初始化好 USB Device 控制器注册一个 CDC ACM 设备类然后把这个虚拟串口作为 REPL 的输入输出通道。所以你在 Thonny、Mu 或任意串口工具里看到的 COM 口不是真实的 UART 物理串口而是 USB CDC 协议虚拟出来的。这一点特别重要因为很多人误以为 Pico 上 UART0 引脚就是 USB 串口其实完全是两回事。Pico 的物理 UART 引脚GP0、GP1 等和 USB 虚拟串口之间在默认固件下没有直接映射关系。你必须在程序里手动把sys.stdin / sys.stdout指向 USB CDC和machine.UART桥接起来数据才会在两者之间流转。提示插上 Pico 后电脑识别出的 COM 口本质是“USB 转串口”的虚拟串口协议走的是 USB CDC ACM。它和板上的物理 UART 引脚没有直接关系除非你在代码里手动转发数据。这个能力能做什么往小了说是电脑和单片机之间的双向数据传输往大了说可以做一个上位机实时监控系统、一个 USB 转 485 网关、一个以 MCU 为核心的 USB 外设原型。热搜词里那些“usb 转串口”“usb 控制舵机”“usb 转 ttl”的问题本质上都是同一个场景电脑作为主机通过 USB 链路控制另一个设备。Pico 的 USB CDC 串口就是这条链路的起点。2. 硬件原理RP2040 USB 控制器与外设架构2.1 从引脚到数据通路的架构拆解硬件上RP2040 直接把 USB 控制器的 D、D- 引脚接到芯片封装上在 Pico 开发板上通过 MicroUSB 座引出。这和很多开发板用 CP2102、CH340、FT232R 这类外部 USB-UART 桥接芯片的做法完全不同Pico 不需要额外的 USB 转串口芯片。这里有个容易搞混的点FT232R 是 FTDI 公司的 USB-UART 桥接芯片插上后电脑会识别成“USB Serial PortCOMx”。Pico 因为用的是 RP2040 内置 USB 控制器插上后 Windows 会识别成“Board CDC”或者“USB 串行设备”。两者从用户视角看都是虚拟串口但底层完全不同。如果你在 Windows 上装了 FT232R 驱动却识别不了 Pico不用意外因为你手里根本没有 FT232R 这颗芯片。这也是“ft232r usb uart 驱动安装”“ft232r usb uart 驱动 win10”这类搜索词反复出现的原因——大家总是把串口芯片和虚拟串口混为一谈。数据通路大概是这样的Pico 的 MicroPython 程序通过sys.stdout / sys.stdin读写 USB CDC 设备 → RP2040 内部 USB 控制器的 FIFO → 协议引擎把数据打包成 USB 包 → D / D- 差分信号传输 → 主机PC的 USB 控制器接收 → 操作系统 CDC 驱动把数据还原成串口读写接口 → 应用层Thonny、pyserial收到字节流。RP2040 的 USB 控制器在设备模式下提供 16 个端点8 个 IN 8 个 OUT。MicroPython 固件通常只占用两个端点做 CDC 串口剩下的大部分端点留给自定义固件使用。如果你以后想实现自定义 USB 设备比如自定义 HID、音频设备或批量传输设备这些端点资源就是你的硬件基础。Pico SDK 和 TinyUSB 都把这层开放出来了只是 MicroPython 层默认没有暴露太多接口。2.2 枚举、描述符与 D / D- 的角色USB 通信的第一步永远是枚举。为什么一插上 Pico电脑立刻能弹出 COM 口因为背后有一套完整的“握手”流程在运行设备插入D 线上的 1.5kΩ 上拉电阻把 D 拉到 3.3V。这是全速设备的特征低速设备则是拉 D-。主机检测到 D 高电平知道有设备插入然后对总线发起复位SE0 状态。设备收到复位信号后把地址设为 0等待主机访问。主机发送 GET_DESCRIPTOR 请求读取设备描述符包含 VID、PID、bcdUSB 等字段。主机再分配新地址、读取配置描述符、选择配置枚举完成。Pico 的 MicroPython 固件在出厂时已经配置好了这些描述符所以你可以在系统信息里看到 idVendor 是 0x2E8A树莓派官方的 Vendor IDidProduct 对应不同板型。如果你用 Pico SDK 自己写固件并修改描述符再重新烧录系统里显示的名称和 VID/PID 就会变这也是为什么有些定制设备需要单独装驱动。D 上拉电阻是枚举成败的关键开关。Pico 开发板已经在板内集成了这个上拉电阻RP2040 内置 PHY 里也有可控上拉逻辑所以用户看不到也不需要外搭。但如果你在面包板上自己搭 RP2040 最小系统做 USB 设备一定要记得给 D 引脚接一个 1.5kΩ 上拉到 3.3V否则主机不会认为有设备插入也就不会触发枚举流程。这是自己搭板子时“插上没反应”的头号原因调试时优先查这个点。2.3 电气细节电平、上拉与滤波USB 全速设备工作在 3.3V 电平下这对 RP2040 非常友好。数据线 D / D- 的驱动由 PHY 负责不占用普通 GPIO 逻辑。自己画 PCB 时D / D- 要走差分线尽量等长、少过孔差分阻抗控制在 90Ω 附近。经常有人在折腾“usb d d- 电容大小”这个问题。如果指的是数据线上串联的滤波电容或共模电感后面的对地电容一般在 2.2nF 到 4.7nF 之间用来滤除共模噪声。但这个参数更多是给 USB 2.0 高速信号用的全速 12Mbps 下要求宽松很多。Pico 官方设计里 D / D- 直连 RP2040中间不额外串电阻靠芯片内置 PHY 处理。自己设计时可以在 D / D- 线上预留串联 22Ω 到 33Ω 的电阻位用于匹配阻抗和抑制边沿振铃。如果在面包板上做测试不做这些处理通常也能跑起来因为走线很短、噪声不大。供电问题也经常被问到。Pico 板上的 VBUS5V 输入通过板载电路连接到 3V3 稳压器给整个板子供能。USB 枚举依赖的是 3.3V 域的 D / D- 逻辑所以只要板子电源正常整个 USB 子系统就正常。但如果供电电流不足就可能出现枚举不稳定、Pico 反复重启的现象。用电脑前面板 USB 口这种供电较弱的接口给 Pico 供电再接上舵机一类的负载板子很容易被拉掉电。我现在的习惯是涉及电机、舵机、无线模块时用独立 5V 电源给负载供电Pico 只通过 USB 取主控部分的电流两者共地即可。3. 用 MicroPython 控制 USB 的核心细节3.1 MicroPython 与 USB 相关的模块和 APIMicroPython 官方固件把 USB 相关能力封装得比较“隐形”。具体来说REPL 终端默认挂在 USB CDC 上也就是sys.stdin、sys.stdout、sys.stderr都指向 USB 虚拟串口machine.UART是物理 UART和 USB 没有直接关系你刷的 rp2-pico-w 或普通 rp2-pico 固件USB 通道都是 REPL。所以“用 MicroPython 控制 USB”最常见的形式不是调用某个usb模块而是通过操作sys.stdin/sys.stdout和电脑通信。你可以把 USB 虚拟串口看成一个字节流管道电脑往管道里写字节Pico 这边从sys.stdin读反过来Pico 往sys.stdout写字节电脑从串口工具里看到输出。理解了这个模型后面所有代码都顺了。3.2 用 USB CDC 串口做双向通信先给一个最简单、可以直接上手的示例通过电脑串口向 Pico 发送“on”或“off”控制板载 LED。import sys import time from machine import Pin led Pin(LED, Pin.OUT) while True: line sys.stdin.readline() if line is None: continue cmd line.strip() if cmd on: led.value(1) print(LED ON) elif cmd off: led.value(0) print(LED OFF) else: print(unknown:, cmd)运行前有个地方要注意REPL 本身也占用 stdin/stdout可能会把输入逐字符回显干扰你的命令。更稳妥的方式是把代码保存为main.py拔掉 USB 重新上电让脚本自动运行或者在 Thonny 里按“运行当前脚本”后直接使用底部 Shell 输入命令测试。如果要用 PC 端程序配合pyserial 是最直接的工具import serial import time ser serial.Serial(COM8, 115200, timeout1) time.sleep(0.2) ser.write(bon\n) print(ser.read(100)) ser.write(boff\n) print(ser.read(100)) ser.close()这里有个经验CDC 虚拟串口的波特率设置在实际 USB 通信中不参与数据速率控制数据已经通过 USB 包传输了。你在 pyserial 里写 115200 只是习惯并不会限制收发速度。真正限制吞吐的是 REPL 的解析方式、Python 脚本的处理速度以及 USB 端点缓冲区的大小。REPL 与用户脚本共享sys.stdin确实是个坑。在 Thonny 里输入行有时候会被当作 REPL 命令解释。我的经验是现场测试用 pyserial 最干净Thonny 适合在调试阶段用。两者不要同时盯着同一个 COM 口。3.3 进阶用 MicroPython 模拟 USB HID 键鼠官方 MicroPython 固件默认不带 HID 类支持只实现了 CDC。但如果你想用 Pico 模拟键盘、鼠标、游戏手柄这类 USB HID 设备有两条路刷 Adafruit 的 CircuitPython 固件。它自带usb_hid模块声明 HID 设备后调用keyboard.press()就能模拟按键非常方便用 MicroPython 搭配第三方 HID 方案或者自己编译固件把 TinyUSB 的 HID 类加进去。这条路要会改配置、重新编译固件门槛高一些但对做产品原型很有价值。我自己的经验是单纯做 HID 原型CircuitPython 更省心。模拟键盘输密码、模拟鼠标移动Adafruit HID 库封装得很薄几行代码就能搞定。而 MicroPython 更擅长做业务逻辑和硬件控制。比较合理的架构是一块 Pico 当主控跑业务逻辑另一块刷 CircuitPython 当 HID 转换器两块板子之间用 UART 通信把“宿主系统识别成键鼠”和“设备端跑复杂逻辑”这两件事分开处理。注意修改 USB 描述符需要重新刷固件。如果你还在用 MicroPython但需要自定义 VID/PID 或设备名就得去编译自己的固件而不是在 Python 层配置。这个点很多人到最后才发现提前知道能省很多时间。4. 实操从零跑通一个 USB 控制实例4.1 硬件准备、接线与驱动识别这次演示的项目是“电脑通过 USB 控制 Pico 驱动舵机”这也是最近很多人搜的场景。准备材料树莓派 Pico 一块普通版或 Pico W 都可以USB 控制机制完全一致MicroUSB 数据线一根注意要能传数据不要拿只能供电的充电线SG90 舵机一个或者其他支持 50Hz PWM 信号的舵机5V 舵机供电电源。USB 5V 电流有限舵机启动瞬间电流大单独供电更稳面包板和杜邦线若干。接线很简单舵机信号线接 Pico 的 GP15VCC 接外部 5VGND 与 Pico 的 GND 共地。Pico 通过 USB 线连接到电脑。插上 USB 后在设备管理器里应该能看到一个 COM 口名字可能是“Board CDC”“Pico”或“USB 串行设备COMx”。如果没出现检查两点线是不是数据线Pico 是否处于正常启动状态。这里有个容易踩的坑按住 BOOTSEL 键插入 USBPico 会进入 USB 大容量存储模式此时电脑识别出的是一个 U 盘而不是 COM 口。注意BOOTSEL 模式 U 盘USB 控制器被配置成 MSC 存储设备正常运行 CDC 串口。同一个 USB 口、同一个控制器枚举出的设备类型不一样。你看不到 COM 口时先确认当前启动模式。4.2 完整示例USB 指令控制舵机动作完整 MicroPython 代码直接放出来。功能是从 USB CDC 读取一行指令格式是angle 90然后把舵机转到对应角度。import sys import time from machine import Pin, PWM servo PWM(Pin(15)) servo.freq(50) def set_angle(angle): # SG90 脉宽范围约 0.5ms0度~ 2.5ms180度 # 周期 20msduty_u16 满量程 65535 pulse_ms 0.5 (angle / 180.0) * 2.0 duty int((pulse_ms / 20.0) * 65535) servo.duty_u16(duty) while True: line sys.stdin.readline() if not line: continue line line.strip() if line.startswith(angle): try: a int(line.split()[1]) a max(0, min(180, a)) set_angle(a) print(OK, a) except Exception as e: print(ERR, e) elif line left: set_angle(0) print(OK 0) elif line right: set_angle(180) print(OK 180)关于 PWM 参数很多人直接抄代码但不理解 pulse 和 duty 的关系。RP2040 的 PWM 频率设为 50Hz 后一个周期是 20ms。duty_u16取值 0 到 65535表示占空比 0% 到 100%。所以先算某角度对应的脉宽毫秒再除以 20ms 得到占空比最后乘 65535 得到 16 位计数值。这个换算方式在换用其他舵机或调整频率时通用建议记下来。PC 端配合 pyserial 发送指令的脚本import serial import time ser serial.Serial(COM8, 115200, timeout1) time.sleep(0.2) ser.write(bangle 90\n) time.sleep(0.5) resp ser.read(64) print(resp) ser.write(bangle 0\n) time.sleep(0.5) resp ser.read(64) print(resp) ser.close()用 PuTTY、MobaXterm、Arduino 串口监视器也可以只要能发送文本行。注意发送时统一用\n或\r\nPico 端的strip()会处理掉多余的回车和换行。4.3 踩坑记录为什么电脑端指令收不到或乱码我实操时碰到过一个典型问题Pico 上电后串口工具发指令没反应但 REPL 还能用。原因是sys.stdin.readline()在 REPL 和脚本同时存在时输入并没有落到你的程序里——REPL 抢先处理了。解决办法有三个把代码保存为main.py拔掉 USB 重新上电让脚本开机自动运行Thonny 保持关闭按 CtrlC 中断 REPL 后再用 pyserial 连接编译固件时把 REPL 的 USB 接口关闭只保留数据 CDC。另一个常见问题是“发了 on 但 LED 没反应”。最后发现是串口工具发送的内容里带了多余字符。用strip()处理后基本能解决。发送端最好统一用\n或\r\nPico 端兼容处理。还有一个关于“usb 转串口”的常见误区如果你用 USB 转 TTL 模块连接 Pico 的物理 UART 引脚那走的是真实的 UART和 USB CDC 虚拟串口完全不是一回事。前者需要额外的 USB-UART 桥接芯片比如 FT232R、CH340后者直接走 Pico 的 USB 控制器。这两类设备在系统里都叫 COM 口但底层协议、接线方式、波特率设置都不一样别混用。5. 常见问题与排查技巧实录5.1 插上 Pico 后设备管理器报“未知 USB 设备设备描述符请求失败”这是 Windows 下很常见的枚举失败提示。常见的排查方向和对应处理供电不足。换一个 USB 口或者用带外部供电的 USB HUB。USB 数据线损坏或只有充电功能。换一根数据线测试不要看线“粗”就以为能传数据。RP2040 固件异常或进入奇怪状态。按住 BOOTSEL 键再插入如果还是报错大概率是硬件或线材问题如果能识别成 U 盘说明 USB 控制器没坏重新刷写固件即可。自己画板子时 D 上拉没做好。确认 D 引脚有 1.5kΩ 上拉到 3.3V。Pico 开发板不用管自搭最小系统时要特别注意。搞硬件的人都知道报“设备描述符请求失败”时90% 的根因是物理层问题也就是供电、线材、接触不良、上拉电阻缺失。软件层面反而很少出这类问题。5.2 串口不稳定、反复断开重连我调试 USB CDC 时遇到过“设备每隔几秒消失又出现”的现象。多数时候是供电问题Pico 从电脑 USB 取电再接了舵机或传感器瞬时电流太大USB 口触发过流保护连接断开。解决方法是给外设单独供电并保证 GND 共地。如果负载电流波动比较大在 USB 进入 Pico 的位置加一个 100μF 以上的电解电容缓冲。另一个原因是 USB 延长线或有源 HUB 的信号质量差。12Mbps 全速对线材要求不高但劣质延长线依然可能让 D / D- 的边沿畸变。现场调试时我会准备两三条质量好的短线备用。“换线”这个操作看着简单但真能解决一大批 USB 疑难杂症。5.3 波特率、REPL 和自定义协议之间的“三国杀”Pico 的 USB CDC 同时承担 REPL 功能和你的脚本收发数据功能时很容易互相干扰。如果打算长期当 USB 控制设备用建议在固件层面关掉 REPL或者把 REPL 放到其他通道。MicroPython 官方固件没有提供运行时开关但可以在mpconfigboard.h里配置 CDC 不启用 REPL只作为数据端口。这个改动需要重新编译固件对做产品原型很实用。如果不想编译固件也可以用代码“抢”输入上电后立刻进入自己的读取循环避开 REPL 的输入解释。这个办法适合快速验证但不适合长期稳定运行。5.4 排查速查表现象可能原因处理方式插入后无任何反应数据线、USB 口、Pico 供电换数据线、换 USB 口、用 BOOTSEL 模式验证“未知 USB 设备设备描述符请求失败”供电不足、线材差、板子枚举异常换线、换口、重刷固件有 COM 口但打开失败COM 口被占用、REPL 未释放关闭 Thonny、重插 USB、更换 COM 口号串口能打开但发指令没反应REPL 抢占输入、换行符不匹配保存为 main.py 运行、确认换行符、检查解析逻辑舵机抖动不走PWM 频率/占空比换算错误、供电不足用示波器量脉宽、外部供电设备一会消失一会出现电流过大、延长线信号差单独供电、加电容、换短线5.5 进阶排查手段抓包普通排查解决不了时就要上协议级调试。在 PC 端可以用 Wireshark 配合 USBPcap 抓 USB 总线上 Pico 的枚举包和控制传输报文看主机是否成功发起了 SET_ADDRESS、GET_DESCRIPTOR设备响应是否正常。这个工具对理解 USB 协议特别有帮助尤其是自己写 USB 描述符、自定义端点的场景。抓包能看到的信息很具体主机是否检测到设备插入、设备返回的设备描述符内容VID/PID、端点数量、配置描述符是否正确、某个端点是否出现非正常的 NAK 或 STALL。如果你没有 USB 分析仪软件抓包是成本最低的入门方案。我做自定义 HID 时键鼠描述符写错了就是靠 Wireshark 里看到的 GET_DESCRIPTOR 响应和 STALL 来定位问题的。数据分析的思路是枚举阶段失败优先看 SET_ADDRESS 之后的响应配置阶段失败优先看配置描述符里的端点声明和接口描述符运行阶段出包优先看端点上的 NAK 频率和传输错误标志。把这些定位清楚很多 USB 问题就不再是玄学了。我在实际使用中的一个体会是Pico 的 USB 功能表面上是插一根线就能写代码但它其实是 RP2040 芯片里被刻意保留的一块“聚宝盆”。从 CDC 串口到 HID 键鼠再到自定义端点全都能靠 MicroPython 这条路慢慢摸出来。我刚接触时也被 COM 口、REPL、FT232R 这些名词绕晕过踩过不少驱动和供电的坑。如果你正准备用 Pico 做 USB 控制类项目我的建议很简单先把枚举这张“握手图”记在脑子里再动手跑一个“电脑发指令、Pico 回状态”的最小闭环后面再谈 HID、再谈自定义固件都会顺很多。