
把树莓派 Pico 插进电脑的那一瞬间多数人只是看到“多了一个 U 盘”或者“多了一个 COM 口”很少有人会追问这个几十块钱的小板子到底是怎么在 USB 总线上完成自我介绍、建立通信、传输数据的这个问题的答案恰好串起了硬件原理、外设架构和 MicroPython 软件控制三条线。这篇文章我想用做嵌入式开发的视角把 Pico 的 USB 从物理层到应用层拆开讲清楚。内容包括D/D- 差分信号和设备枚举流程、RP2040 内部的 USB 控制器与 PHY 架构、MicroPython 里用usb.device模块实现自定义 HID 键盘和 CDC 串口以及实操中常见的 USB 设备描述符请求失败、驱动异常、复位循环等问题的排查方法。适合刚接触 Pico 的嵌入式新手也适合想拿 Pico 做 USB 外设、HID 输入工具或虚拟串口的 DIY 玩家。1. 从 USB 物理信号到枚举流程先把基础概念理顺1.1 D/D- 差分信号与全速 12MbpsPico 在 USB 世界里的“身位”USB 通信本质上是两根线的事。无论 Type-A、Micro-USB 还是 Type-C数据传输都依赖 D 和 D- 这对差分线。说是“差分”核心意思就是主机和设备看的是两根线的电压差而不是某一根线对地的绝对电压。这样外界的共模噪声会同时叠加到两根线上相减之后基本被抵消抗干扰能力比单端信号强得多。Pico 的 USB 属于 USB 2.0 Full Speed速率固定 12Mbps不是 High Speed480Mbps更不是 Low Speed1.5Mbps。Full Speed 设备的识别方式很有意思设备端在 D 线上接一个 1.5kΩ 上拉电阻到 3.3V。主机检测到 D 被拉高就知道接入的是一个 Full Speed 设备如果上拉在 D- 上那就是 Low Speed。Pico 板载电路里已经处理了这个上拉电阻不需要自己外接。这里我要顺带说一个经常被问到的点。不少人在设计 Type-C 接口时看到 CC 引脚有 5.1kΩ 下拉电阻就疑惑“那怎么切换到主机模式”。其实 5.1kΩ 下拉本身就是让设备声明自己是一个 Device也就是被主机供电、被主机枚举的对象。要切到 Host 模式CC 上通常要换成上拉电阻具体阻值取决于供电能力同时控制器的 USB 功能也得原生支持 Host。而 RP2040 这颗芯片并不支持 USB Host这部分后面我会单独展开。Pico 用的是 Micro-USB 接口根本没有 CC 引脚所以这个问题在 Pico 上不成立但在做 Type-C 转接板时一定要想清楚。1.2 枚举是怎么发生的设备如何一步步进入系统视野USB 设备和电脑通信不是插上就能直接收发数据的。主机上电后首先要做“枚举”——也就是通过一系列标准控制请求把设备的基本信息问清楚再给设备分配地址最后让设备进入配置状态。简化后的枚举流程大致是这样主机检测到 D 上拉认为有新设备接入于是给设备复位设备在地址 0 上收到主机的 GET_DESCRIPTOR 请求返回设备描述符主机再次复位设备发送 SET_ADDRESS 分配一个唯一地址之后主机继续请求配置描述符、接口描述符、端点描述符、字符串描述符最后发送 SET_CONFIGURATION设备才算真正进入工作状态。在这一串流程里任何一步出错主机端表现基本都是“未知 USB 设备设备描述符请求失败”或者“无法识别的设备”。我调试 USB 设备时只要看到这个报错第一反应就是先别改代码先看枚举在哪一步断了。Windows 的设备管理器里能看到设备的 VID/PID 是否读取出来Linux 下可以用lsusb -v看枚举结果。Pico 上常见的枚举失败原因后面第五节我会列一个排查清单。1.3 端点、管道和传输类型看懂通信的“通道”枚举过程中设备描述符里会包含端点信息。端点是 USB 通信的最小逻辑单元可以理解成设备内部一个个“信箱”。每个端点有编号和方向比如端点 0 是双向的专门用于控制传输端点 1 IN 表示设备向主机发送数据端点 2 OUT 表示主机向设备发送数据。端点按用途分成几类传输类型。控制传输用来做枚举和标准请求设备刚接入时用的就是它。批量传输适合大数据块可靠性高比如虚拟串口CDC的数据通道通常用批量端点。中断传输不是真的“中断”而是设备保证在固定时间间隔内被主机问询一次适合键盘鼠标这类 HID 设备。RP2040 的 USB 控制器硬件支持控制、批量、中断这几类对大多数应用已经完全够了。2. RP2040 的 USB 外设架构控制器、PHY、DPRAM 与 48MHz 时钟2.1 芯片内部“USB 三件套”怎么协同工作RP2040 的 USB 模块不是一个单一外设它由三块协同工作的部分构成USB 控制器负责协议状态机、包收发、CRC 校验、事务调度PHY 负责把数字信号变成 D/D- 上的差分模拟信号同时负责检测总线状态DPRAM 是一块双端口 RAM用来存放端点的数据缓冲区。软件读写数据时并不需要直接操作 PHY。比如 MicroPython 里调用dev.write()数据会被放到 DPRAM 中对应的端点缓冲区然后由 USB 控制器按 USB 协议帧格式打包交给 PHY 发送。反过来主机发来的数据由 PHY 接收控制器校验后写入 DPRAM再触发中断MicroPython 层就能读到了。这个“控制器 PHY DPRAM”的结构在 STM32、CH32 等单片机上也能看到类似影子只是寄存器地址和细节不同。我一开始直接看数据手册时被一堆EP_CTRL、BUF_CTRL、DPRAM相关寄存器绕晕了。后来用了一个笨办法在 MicroPython 里初始化一个 CDC 设备然后在 C SDK 的源码里搜读写 DPRAM 的代码对照寄存器手册逐步看才真正把流程串起来。对多数使用 MicroPython 的开发者来说这些寄存器不用全部掌握但知道“数据从哪进去、最终从哪根线出去”这个链路排查问题会轻松很多。2.2 为什么 48MHz 偏偏不能省时钟与位时序USB Full Speed 的比特率是 12Mbps每一位在总线上持续约 83.3ns。控制器要精确分辨每一位内部时钟必须和这个位时间严格对齐。RP2040 的 USB PHY 需要外部提供一个 48MHz 时钟SOC 内部有时钟分频和 PLL 路径来产生这个频率由USBCTRL寄存器组控制。如果 48MHz 没使能或者不准确PHY 的收发时序就会乱最典型的表现就是设备能被识别到供电但枚举永远不成功。MicoPython 固件在启动时会把这一套时钟配置好所以很多时候大家意识不到这层依赖。一旦你想脱离现成固件用 C SDK 或者写裸机程序必须要检查是否调用了usb_start_clock()类似的初始化。我见过不少人折腾 USB 时把这步漏掉结果折腾半天以为是接线问题其实只是时钟没开。2.3 明确一个边界Pico 只能做 USB 设备做不了主机RP2040 的数据手册写得很清楚它的 USB 控制器只支持 Device 模式不支持 Host 模式也没有 OTG。所谓“让 Pico 识别 U 盘”“让 Pico 读取 USB 键盘”这类需求原生硬件是做不到的。当然社区里有人用 PIO 做低速或全速 USB Host也有人外接 MAX3421E 这类 USB Host 控制器芯片这些方案确实存在但复杂度明显上升。如果你只想像“插个 U 盘给 Pico 读写文件”这样简单使用建议换一颗原生支持 Host 的单片机不要硬磕 RP2040。这个边界认清楚能少走很多弯路。3. MicroPython 里的 USB 控制从 REPL 串口到自定义 HID/CDC3.1 默认固件里 USB 承担了什么UF2 下载与 REPLPico 的 USB 在出厂时就有两个重要任务。按住 BOOTSEL 键插入电脑Pico 会进入 UF2 下载模式此时系统里出现一个名为“RPI-RP2”的 U 盘把 MicroPython 固件的.uf2文件拖进去固件就写好了。这个 U 盘模式本质上是 Pico 的 ROM bootrom 在枚举一个 USB MSC 大容量存储设备。刷入官方 MicroPython 固件后Pico 重新上电系统里会多出一个串口设备这就是 REPL 环境。它内部也是通过 USB CDC 实现的也就是说你第一次在 Thonny 里敲 Python 代码时已经在实际使用 Pico 的 USB 外设了。这个默认行为是理解后面自定义 USB 设备的基础。3.2 用 usb.device 模块替换默认 USB 配置MicroPython rp2 移植版从 1.20 左右开始提供usb.device模块可以在运行时重新配置 USB 设备描述符把默认的 REPL 串口替换成自定义的 CDC、HID 等设备。这个模块提供了CDC和HID两个类分别对应虚拟串口和人机交互设备。用法大致是import usb.device from usb.device import CDC dev CDC() usb.device.init(dev)执行init之后USB 会重新枚举。根据不同固件版本init可能还支持builtin_driver参数用来决定是否保留 MicroPython 内置的 REPL 串口。我建议动手前先运行help(usb.device)看一下你当前固件的实际接口因为不同版本之间参数名确实有差异。3.3 实战一Pico 变身 USB HID 键盘HID 键盘是目前最实用、最容易出效果的 USB 应用之一。把下面的代码存成main.py放进去Pico 一上电就会被电脑识别成一个标准键盘然后在光标处自动输出一串字符。整个过程无需额外驱动。import time import usb.device from usb.device import HID # HID 键盘报告描述符标识这是一个标准键盘 report_desc bytes(( 0x05, 0x01, # Usage Page (Generic Desktop) 0x09, 0x06, # Usage (Keyboard) 0xA1, 0x01, # Collection (Application) 0x05, 0x07, # Usage Page (Key Codes) 0x19, 0xE0, # Usage Minimum (224) 0x29, 0xE7, # Usage Maximum (231) 0x15, 0x00, # Logical Minimum (0) 0x25, 0x01, # Logical Maximum (1) 0x75, 0x01, # Report Size (1) 0x95, 0x08, # Report Count (8) 0x81, 0x02, # Input (Data, Variable, Absolute) 0x95, 0x01, # Report Count (1) 0x75, 0x08, # Report Size (8) 0x81, 0x01, # Input (Constant) 0x95, 0x05, # Report Count (5) 0x75, 0x01, # Report Size (1) 0x05, 0x08, # Usage Page (LEDs) 0x19, 0x01, # Usage Minimum (1) 0x29, 0x05, # Usage Maximum (5) 0x91, 0x02, # Output (Data, Variable, Absolute) 0x95, 0x01, # Report Count (1) 0x75, 0x03, # Report Size (3) 0x91, 0x01, # Output (Constant) 0x95, 0x05, # Report Count (5) 0x75, 0x08, # Report Size (8) 0x15, 0x00, # Logical Minimum (0) 0x25, 0x65, # Logical Maximum (101) 0x05, 0x07, # Usage Page (Key Codes) 0x19, 0x00, # Usage Minimum (0) 0x29, 0x65, # Usage Maximum (101) 0x81, 0x00, # Input (Data, Array) 0xC0, # End Collection )) dev HID(report_descreport_desc, poll_interval10) usb.device.init(dev, builtin_driverFalse) KEY_A 0x04 time.sleep(1) # 按下 A dev.write(bytearray((0, 0, KEY_A, 0, 0, 0, 0, 0))) time.sleep(0.05) # 释放 A dev.write(bytearray((0, 0, 0, 0, 0, 0, 0, 0)))HID 键盘报告是 8 字节。第 1 字节是修饰键比如 Ctrl、Shift、Alt第 2 字节保留后 6 字节是同时按下的按键码0x04 对应字母 A0x00 表示没有按键。所以按下 A 时要发[0,0,4,...]松开时再发一个全 0 报文否则系统会认为键一直按着。3.4 实战二用自定义 CDC 与上位机通信CDC 虚拟串口的典型使用场景是 Pico 与电脑之间做数据收发。以下代码会创建一个自定义 CDC 设备并把收到的数据原样返回相当于一个 USB 回环测试。import usb.device from usb.device import CDC dev CDC() usb.device.init(dev) while True: data dev.read(64) if data: dev.write(data)这种回环功能很适合验证链路是否畅通。你可以用任意串口工具打开对应的 COM 口或/dev/ttyACM0发一段字符串应该能原样收到。真正常用的是把这里的数据逻辑替换成你自己的协议解析比如命令控制、传感器数据上传等。需要特别留意的是如果init时没有保留内置驱动执行完这段代码后原本的 MicroPython REPL 串口就没了。此时不要再想着用串口终端连上去调试想重新拿到 REPL只能按住 BOOTSEL 重新刷固件。4. 完整实操流程烧录、验证与 USB 抓包4.1 第一步给 Pico 装好 MicroPython 固件官方 MicroPython 固件可以在 MicroPython 官网的 Download 页面找到针对 Pico 的.uf2文件通常是rp2-pico-xxxxxx.uf2这样的命名。烧录步骤很简单先按住 Pico 板上的 BOOTSEL 键不要松再用 USB 线连接电脑等系统识别出“RPI-RP2”U 盘后再松开按键最后把.uf2文件拖进这个 U 盘。这里我踩过一个坑有些 USB 线只有供电能力没有数据线芯。插上去系统没有任何反应还以为板子坏了。所以第一优先级是换一根确定能传数据的线再考虑别的。烧录完成后打开系统设备管理器Windows或者运行lsusbLinux应该能看到新的串口设备。Windows 下通常显示为“USB 串行设备”Linux 下是/dev/ttyACM0。此时用 Thonny 或 minicom 连接就能进入 REPL 了。4.2 第二步让 HID 示例动起来把上一节的 HID 键盘代码保存为main.py放到 Pico 的 MicroPython 文件系统根目录。断开再重新插上电脑会枚举出一个 HID 键盘设备大约 1 秒后光标所在位置会自动输入字母 A。如果你想做更复杂的键盘功能只需要修改报告描述符和按键码。比如发修饰键加普通键可以同时设置第 0 字节和第 3 字节。实际测试时要注意如果发送太快部分操作系统会丢弃 HID 事件所以报告之间建议加上 20ms 以上的间隔。这也是 HID 设备实际使用中很重要的一点尤其是做自动打字宏时速度太快反而容易丢键。4.3 第三步用抓包工具看 USB 总线上的真实事务USBT抓包可以让之前讲的“枚举流程”变成肉眼可见的数据流。很多文章只讲概念我建议有条件的一定要实际抓一次包。Linux 下比较方便的方式是usbmon加 Wireshark。先确认系统装了 Wireshark然后加载usbmon模块用 Wireshark 选择usbmon接口抓包。抓包时插上 Pico能看到一批 SETUP 事务里面就是 GET_DESCRIPTOR、SET_ADDRESS、SET_CONFIGURATION 这些请求。展开各个描述符的响应设备描述符里的 VID0x2E8ARaspberry Pi、PID、端点描述符等信息一目了然。Windows 下可以借助 USBPcap 这类抓包工具或者直接用逻辑分析仪挂在 D/D- 上采波形。逻辑分析仪对硬件级调试很有用但要注意采样率必须足够高建议 16MHz 以上否则采不到正确的 USB 差分时序。抓包这件事最大的价值在于它让你不再把 USB 当成一个“黑盒子”而是能看到设备在总线上每一个字节的流动。4.4 如果要往更复杂的方向走从 MicroPython 到 C SDKMicroPython 的usb.device模块适合快速验证但它的灵活性和性能都有限。如果要做复合设备比如同时实现 HID 键盘和 CDC 串口、自定义厂商类端点、高速大批量传输就得切换到官方 C SDK 或裸机开发。C SDK 里有一套完整的 USB 设备例子比如usb/device/dev_hid_composite、usb/device/dev_lcd等。在 C 代码里你可以直接操作端点缓冲区、配置描述符数组甚至实现厂商请求。代价是代码量明显增加调试门槛也更高。我的建议是先从 MicroPython 把业务逻辑验证通确认硬件链路没有问题再决定是否值得迁移到 C。5. 常见报错与排查技巧实录5.1 “未知 USB 设备设备描述符请求失败”到底卡在哪这个报错是 USB 开发者的老朋友了。根据我自己的经验优先级排序大概是线材接触不良 供电不足 时钟未使能 描述符错误 枚举时序问题。排查顺序我整理成一个表现象优先检查项处理方式插上后系统无任何反应USB 线是否含数据线芯换线重试提示“设备描述符请求失败”供电是否稳定VBUS 电压是否正常换 USB 口或换供电枚举时而成功时而失败接线、接触、USB 口滤波电容换口、检查焊接重刷自定义固件后报错描述符或时钟初始化有误回到官方固件逐项排查对 Pico 来说最常见的情况是代码里usb.device.init()传入了不兼容的参数导致枚举没完成就复位。这时候先按住 BOOTSEL 重新刷回官方固件确认板子硬件没问题再定位代码。5.2 CDC 串口驱动问题Pico 与 FT232R/CH340 这类 USB-UART 芯片的区别很多人在用 USB 转 TTL 模块时遇到过 FT232R、CH340 等芯片的驱动问题需要单独装驱动才能在系统里看到 COM 口。Pico 的原生 USB CDC 不需要额外安装驱动Windows 10 以上、主流 Linux 发行版和 macOS 都会把它识别为通用串口设备。如果你在设备管理器里看到 CDC 串口带黄色感叹号多数不是 Pico 的问题而是系统 USB 驱动状态异常。可以尝试卸载设备后重新扫描硬件或者重启电脑。顺带一提如果你手里同时有 USB-UART 桥接模块和 Pico做串口调试时建议想清楚用哪条链路Pico 的 USB CDC 不需要外部桥接芯片但占用自身 USB 接口外部 USB-UART 芯片则占用 UART 引脚两者各有用处。5.3 插上就反复重连复位循环的排查Pico 插上电脑后设备管理器里设备图标反复出现又消失多半是设备在不停复位。常见原因有三个代码执行到一半崩溃导致看门狗复位USB 初始化不成功触发了异常VBUS 供电被大电流拉垮。遇到这种情况先别急着分析代码。按住 BOOTSEL 重新进入 UF2 模式优先确认硬件能稳定枚举。接着刷回官方固件确认板子没问题。最后再把自定义代码一段段加回来定位是初始化阶段崩还是运行阶段崩。这个方法我用了很多次每次都有效。5.4 舵机/电机一启动 USB 就掉线电源是隐形杀手网上很多 Pico 控制舵机的教程会把舵机直接接到 VBUS 上这其实是隐患。舵机启动瞬间电流可能达到几百毫安甚至更高一旦超过 USB 口能够提供的电流限制VBUS 电压会跌落USB PHY 工作就不稳定设备往往直接重连甚至掉线。正确做法是给舵机单独供电同时把 Pico 的 GND 和舵机电源的 GND 连在一起保证参考电位一致。Pico 本身继续用 USB 供电即可。这个“共地但分离电源”的原则在驱动电机、舵机、大功率 LED 时都适用属于嵌入式里最基础也最容易忽略的坑。5.5 REPL 不见了怎么恢复一旦执行了usb.device.init(dev)且覆盖了内置驱动原本的 REPL 串口会被替换成自定义设备。这时候你打开串口终端可能发现设备无法连接或者串口直接消失。这不是硬件坏了只是 USB 描述符被改掉了。恢复方法很简单按住 BOOTSEL 重新上电进入 UF2 模式把官方 MicroPython.uf2再拖一次所有内容恢复原样。这也提醒我在测试自定义 USB 代码时先用main.py之外的名字保存代码或者在init之前加一个延时给自己留出中断操作的时间窗口避免改完代码后连 REPL 也进不去。5.6 USB 抓包常见坑权限、接口与过滤器如果抓包时发现 Wireshark 看不到 usbmon 接口第一件事查权限usbmon设备通常需要 root 权限。Linux 下临时用sudo wireshark可以解决但更规范的做法是把当前用户加入wireshark组。抓包时建议过滤 USB 总线地址或者 VID/PID否则同一台电脑上其他 USB 设备的流量会非常吵。抓包时要注意USB 是轮询广播式总线主机和设备之间的大多数事务由主机发起。抓包看到的不只是“设备给主机发了什么”更多是“主机问设备要了什么”。理解这一点很多 HID 设备的数据上报行为才解释得通。我个人在实际操作中有个很深刻的感受Pico 的 USB 虽然是全速 12Mbps远不如现代高速设备快但做 HID 输入工具、虚拟串口、嵌入式设备调试通道这个速度完全够用而且自带的 CDC 功能省掉了一颗 USB 转串口芯片电路设计干净很多。后续如果想玩得更深可以从两个方向入手一是基于 C SDK 做批量传输尝试更复杂的数据吞吐场景二是研究 PIO 模拟低速 USB Host 的社区方案补上 RP2040 原生不支持 Host 的短板。这个小板子能挖的东西远比它几十块钱的价格多得多。