拿到树莓派 Pico 的第一天,很多人会翻来覆去找芯片,然后问一句:“这块板子的 USB 转串口芯片在哪?”其实 Pico 压根没有那玩意儿。也正因为没有,RP2040 的 USB 才真正算得上“原生 USB”——你可以让这块小板子在电脑上变成一个键盘、一个鼠标、一个游戏手柄、一个 U 盘,甚至一个虚拟串口,全都在 MicroPython 代码里直接控制。这篇内容就围绕 Pico USB 的硬件原理、外设架构和 MicroPython 软件控制三块展开,适合刚上手 Pico、想用 USB 做外设的朋友,也适合被“设备描述符请求失败”折腾到头大的玩家。
在开始前我把话放前面:Pico 的 USB 不是那种“接上就能跑”的串口工具,它的底层逻辑、电气标准和外设模型都和普通单片机开发板不一样。搞懂了这些,后面玩 USB 抓包、模拟 HID 设备、甚至做 USB Host 才会顺手很多。
1. 先看硬件底子:RP2040 的 USB 到底强在哪
很多人把 Pico 当成“能跑 MicroPython 的 Arduino”,但它的 USB 设计思路和 Arduino 的 USB 转串口方案完全是两码事。Arduino 板上通常有一颗 CH340、FT232R 或者 FT231X 芯片,负责把 USB 信号翻译成 UART 串口信号,所以你在电脑上看到的始终是一个“COM 口”。Pico 不走这条路,它把 USB 控制器和收发器直接集成进了 RP2040 芯片内部,整条 USB 链路都是原生的,没有中间翻译层。
1.1 USB 控制器、PHY 与引脚映射
从 RP2040 数据手册来看,这颗芯片内置的是 USB 1.1 控制器,支持全速(Full Speed,12 Mbps)模式,既可以做设备(Device),也可以做主机(Host)。这是一个非常关键的点:绝大多数单片机开发板的 USB 只能用做设备端,也就是插到电脑上当外设用,但 Pico 还能主动去读取 U 盘、连接 USB 键盘鼠标,这就是 Host 能力。控制器后面还集成了 PHY(物理层收发器),所以你不需要在外面再接 USB PHY 芯片,两根信号线直接就能拉到 USB 座子上。
具体到引脚,RP2040 的 USB 信号对应两组引脚:GPIO14 是 USB_DM(数据负线)通常也叫 D-,GPIO15 是 USB_DP(数据正线)也叫 D+。Pico 板载的 USB 插座内部就是接到这两根线上。这里要提一个重要细节:USB 全速设备规范要求 D+ 线上有一个 1.5kΩ 上拉电阻,用来告诉主机“这是一个全速设备”。RP2040 内部集成了这个上拉电阻,所以在设备模式下,当主机把总线复位后,Pico 可以自动在 D+ 上产生上拉信号,完成设备速度协商。这也是为什么 Pico 不需要外接任何 USB 转接芯片就能被电脑识别的原因。
板上还有一个 12MHz 晶振,别小看它。USB 全速模式的位时钟需要 12MHz 参考频率,这个晶振不仅给 USB 用,还给整个芯片的 PLL(锁相环)提供了基准。如果你以后尝试自己画 RP2040 的板子,晶振这颗料千万不能省,USB 枚举失败十有八九和时钟不准或晶振布局太差有关。
1.2 为什么没有 USB 转串口芯片反而更方便
Pico 不带 CH340、不带 FT232R,这在刚接触单片机的人看来非常不习惯,因为没法像 Arduino 那样直接在串口监视器里看打印。但从做产品的角度看,这是聪明设计:USB 转串口芯片本身就是一条“间接链路”,它把 USB 端点翻译成 UART,意味着你只能跟单片机收发字节流,无法直接模拟鼠标、键盘、游戏手柄这类标准的 USB HID 设备。没有翻译芯片,Pico 的 USB 端点可以自定义成任意标准设备,自由度一下打开了。
那调试日志去哪看?其实也不用慌,MicroPython 固件默认会把 USB 枚举成一个 CDC 虚拟串口,你在电脑里一样能看见 COM 口,一样能看到 print 输出。这个虚拟串口和硬件 UART 有一个本质区别:它是通过 USB 描述符直接模拟出来的,底层不会占用硬件 UART 外设。所以GPIO0 和 GPIO1 这对 UART0 引脚可以拿去做别的通信,不用为调试让路。
顺便说一句,市面上常见的 USB 转 TTL、USB 转 485 模块,以及那些需要安装驱动程序的 FT231X、FT232R 设备,跟 Pico 的 USB 完全是两码事。如果你在设备管理器里看到的是 USB 串行设备,那多半是外接的 USB 转串口模块,不是 Pico 自身。这一区分能避免后面排查问题时的各种误解。
1.3 BOOTSEL 按键背后的 USB 秘密
Pico 上唯一一个对 USB 影响很大的元件,是板子上的 BOOTSEL 按键。按住这个按键再插 USB 线,RP2040 会进入芯片出厂时固化的 ROM Bootloader。这个时候芯片不执行用户程序,而是把自己模拟成一个 USB 大容量存储设备(MSC),也就是一个 U 盘。电脑上会弹出一个小容量磁盘,里面放着 UF2 格式的固件文件,把编译好的 .uf2 直接拖进去,芯片就会自动写入固件并重新启动。
这个过程的本质,是 ROM 里的引导代码实现了完整的 USB MSC 描述符和 FAT 文件系统操作。也就是说,哪怕你手头没有 ST-Link、没有 J-Link、没有 USB 转 TTL 模块,只用一根数据线就能给 Pico 烧录。我经常用这个按键配合 MicroPython 固件来回切换环境,整个过程大概五秒,比很多开发板重新下载程序快多了。
还有个容易忽略的细节:Kboot 模式(即 BOOTSEL 模式)下,USB 的描述符、端点、甚至 VID/PID 都来自 ROM 里固定的一套,跟用户程序无关。所以当你的 Pico 插入电脑后,如果设备管理器里偶尔出现的是“树莓派 RP2 Boot”而不是正常的 MicroPython 设备,说明芯片要么进入 BOOTSEL 模式,要么用户程序崩了导致 USB 没有被正确初始化。这个现象不是硬件坏了,是正常的 ROM 引导行为。
2. USB 枚举与描述符:一次“插入-识别”的完整握手
很多朋友遇到“未知 USB 设备(设备描述符请求失败)”,第一反应是驱动问题,于是满世界找驱动。实际上 USB 枚举失败的大多数原因不在驱动,而在设备固件、硬件线路和协议时序上。把 USB 枚举原理搞清楚,这类问题一半能自己判断。
2.1 枚举过程到底做了什么
当把 Pico 插进电脑 USB 口,主机并不会“一眼认出”设备是谁。它会先检测到 D+ 线上的上拉信号,然后发起一系列控制传输来询问设备信息,这个过程就是 USB 枚举。常规流程大概是:主机向地址 0 发送 GET_DESCRIPTOR 请求,设备返回设备描述符,主机给设备分配一个唯一的地址,再继续读取配置描述符、接口描述符、端点描述符等。拿到这些信息后,主机加载匹配的驱动,设备才真正“上线”。
我打个比方:USB 枚举就像小区门口保安登记访客。设备描述符是你的名字,配置描述符是你能干什么,接口描述符是你的联系方式,端点描述符是你常用的出入口。保安先问“你是谁”,再问“你来干什么”,最后登记完才放你进去。如果哪一步答非所问,保安就只能把访客拦在外面,对应到 Windows 就是那句刺眼的“设备描述符请求失败”。
在 Pico 上用 MicroPython 做开发时,固件已经帮你把标准描述符写好了,你不需要自己撸描述符表。但如果采用了第三方自定义固件,或者自己在 C 语言 SDK 里改了描述符,那就需要非常小心:描述符里报告的长度必须和实际数据长度一致,接口描述符必须放在配置描述符里,端点地址要和实际收发端点对应。任何一处不匹配,主机都可能直接放弃设备。
2.2 三种最常用的 USB 类:CDC、HID、MSC
Pico 的 USB 端点可以在固件里被配置成不同用途,不同用途对应不同的类(Class)。MicroPython 官方固件默认组合一般是 CDC + MSC,CircuitPython 固件默认提供 CDC + MSC + HID,而自定义固件可以按需配成任意组合。这里先理清三种最常见类的特点:
| USB 类 | 操作系统视角 | 适合场景 | 典型例子 |
|---|---|---|---|
| CDC(通信设备类) | 虚拟串口 | 日志调试、串口透传 | print 输出、数据采集 |
| HID(人机交互设备类) | 键盘/鼠标/游戏手柄 | 免驱动外设 | 宏键盘、脚踏板、模拟摇杆 |
| MSC(大容量存储类) | U 盘 | 文件传输、固件升级 | BOOTSEL 模式、拖拽烧录 |
选择标准很简单:如果需要跟电脑双向收发大量数据,CDC 最方便,操作系统自带串口驱动,像打开文件一样读写 COM 口就行;如果需要做即插即用的输入设备,比如自制一个键盘宏,那就选 HID,不需要安装额外驱动;如果只是想拖几个文件进 Pico,MSC 最直观。要注意的是,同一根 USB 线上可以同时暴露多个类,比如 CDC + MSC 一起出现,所以它们不是非此即彼的关系。
2.3 描述符出问题会有什么表现
描述符一旦出错,现象非常诡异。我遇到过几种典型情况:设备能识别,但一打开串口就报“另一个程序正在使用此设备”;设备被识别成未知设备,但换一台电脑又正常;在 Windows 上正常,插入 Linux 后 dmesg 里连续报错。这些都不是驱动损坏,而是描述符里某些字段不符合协议规范,比如字符串描述符的索引越界,或者配置描述符总长度计算错误。
还有一种很常见的坑:在电脑上插过太多 USB 设备后,系统缓存了旧的设备信息,即使 Pico 固件已经改了 VID/PID,Windows 仍然用旧驱动去匹配。这时候把设备管理器里那个灰色带感叹号的设备卸载,拔掉线,重新插上,往往就能恢复正常。做 USB 外设开发的朋友应该养成一个好习惯:每次更换固件后,先看一眼设备描述符里的 VID、PID、bcdDevice 这些字段,确认设备是不是真的“按新身份”出现了。
3. MicroPython 默认 USB 行为与外设架构
现在进入大家最关心的软件层面。Pico 上跑 MicroPython,USB 的大部分行为其实由固件在启动时初始化好,普通 print、REPL 输入输出都是走 USB CDC 通道完成的。这一节我按实际使用场景来拆解。
3.1 默认 CDC 虚拟串口怎么用
官方 MicroPython 固件烧录之后,用 USB 线连接电脑,系统会识别出一个虚拟串口。在 Windows 上设备管理器里多了一个 COM 口,在 Linux 上通常是 /dev/ttyACM0 或者 /dev/ttyUSB0。你只需要用任意串口终端软件,比如 PuTTY、Thonny、screen,连接这个串口,就能进入 MicroPython 的 REPL 交互环境。
这个虚拟串口和普通硬件串口的区别是:它由 USB 协议模拟,数据不经过 UART 硬件。所以在 MicroPython 里,如果你想在 USB 虚拟串口上输出内容,直接用 print() 即可,不需要像硬件串口那样配置 UART 波特率。波特率、数据位、停止位这些参数对 USB CDC 来说完全不生效,随便设置都不会影响通信。很多新手在串口助手里调来调去波特率,发现没任何变化,其实这是正常现象。
如果想让 USB 虚拟串口在程序启动时就自动进入某个模式,或者把输出重定向到别的地方,MicroPython 提供了 os.dupterm 接口。比如你在做项目时既想用 REPL,又不想让 USB 串口输出干扰逻辑,可以把 REPL 摘掉,让 USB 串口纯粹跑你的协议数据。这个操作在量产调试时非常有用。
3.2 MicroPython 里能不能模拟 HID 设备
这是被问得最多的问题:“我想用 Pico 做一个键盘,MicroPython 能不能直接调?”官方 MicroPython 固件默认并不包含 HID 设备描述符,所以你打开串口能看 REPL,但操作系统不认为它是一个键盘。要实现 HID 模拟,通常有两条路:一是自行编译带自定义 USB 描述符的 MicroPython 固件,二是我个人更推荐的——换用 CircuitPython 固件,它把 usb_hid 做成开箱即用模块,语法仍然属于 MicroPython 3.x 分支,过渡成本极低。
如果你坚持用官方 MicroPython,也不是完全没路。MicroPython 对 RP2040 提供了 USBDevice 相关支持,可以通过自定义描述符、端点和回调函数把设备重新枚举成 HID 类型,但代码量明显偏大,涉及 CDC 和 HID 描述符组合时更容易踩坑。对大多数项目来说,CircuitPython 直接把鼠标、键盘、游戏手柄的类库做得更完整,所以后续实操演示我会用 CircuitPython 来写代码,核心原理依旧适用于整个 MicroPython 生态。
3.3 USB Host 模式:让 Pico 去连接别人
上面的 USB 设备模式是“Pico 当外设”,USB Host 模式则是“Pico 当主人”。MicroPython 从特定版本开始给 RP2040 带来了 usb_host 模块,配合 Pico 官方 SDK 的 USB Host 底层驱动,可以读取 U 盘、识别 USB 键盘鼠标。基本思路是初始化一个主机端口,然后通过回调函数接收设备事件。常见代码框架是这样的:
import usb_host def device_callback(device_id): print("device connected:", device_id) port = usb_host.Port(0, device_callback=device_callback)这段代码在不同的 MicroPython 固件里 API 可能存在差异,如果运行时报错,先在当前固件里执行 help(usb_host) 查看实际支持的方法。需要用 USB Host 模式时,还有一个硬件前提:作为主机的 Pico 要对外提供 5V VBUS 电源,普通的 USB 设备如 U 盘的电源完全来自 VBUS,如果只靠 Pico 自身的 USB 口供电,常常会出现带不动的情况。我通常会用带外部电源的 USB Host 扩展板,或者通过 GPIO 控制一个电源开关芯片,避免电流不够导致 U 盘反复失联。
4. 实操:用 MicroPython 系固件做一个 USB 键盘/鼠标
这一节是给想直接上手的朋友准备的完整流程。通过这块小板子做一个能在电脑上真正使用的 HID 设备,从烧固件到跑通代码,一次走完。
4.1 固件选择与烧录步骤
如果目标是做 HID 设备,我建议直接用 CircuitPython 固件。它跟 MicroPython 语法几乎一致,最大的优势是内置 usb_hid 和参数完全开放的 USB 描述符,省掉大量自定义底层的功夫。下载对应 Pico 的 .uf2 固件文件,按住 BOOTSEL 键,插入 USB 线,然后把这个 .uf2 拖进弹出的 RP2 盘里,等待它自动重启即可。
烧写完成后,电脑上会出现一个名为 CIRCUITPY 的 U 盘,这就是 CircuitPython 的虚拟文件系统。你需要在这个 U 盘里创建一个 code.py 文件,系统会每次上电自动执行它。另外推荐在 lib 目录里放一份 Adafruit HID 库,也就是把 adafruit_hid 文件夹从 CircuitPython 库合集复制进去,这样写代码时就能方便地调用鼠标、键盘类。
4.2 模拟鼠标的几分钟快速实现
先试一个最简单的效果:让系统鼠标自动向右移动。新建 code.py,写入以下内容:
import time import usb_hid from adafruit_hid.mouse import Mouse mouse = Mouse(usb_hid.devices) while True: mouse.move(x=100) time.sleep(0.02)保存后,CircuitPython 会自动重新加载代码,你会发现电脑鼠标开始一阵一阵往右跑。这里的关键在于 usb_hid.devices 这个对象包含了设备描述符里暴露的所有 HID 设备接口,而 Mouse 类会往对应的报告端点发送标准 HID Report。操作系统收到这份报告后,会把它当作真实鼠标事件处理,完全不关心发报告的主控芯片到底是谁。
如果想做随机移动、按键连点、甚至根据某个 GPIO 输入来触发移动,都只需要在这个循环里加条件和事件。我实际做过一个临时演示用的自动点击器,用来帮忙跑一遍漫长的网页表单测试,就是基于这段代码改出来的,除了要控制报告发送频率外,几乎不用动底层。
4.3 模拟键盘:一键触发一串快捷键
模拟键盘比鼠标更有实用价值。比如做一个一键打开文件夹或者一键发送组合快捷键的“宏键盘”,核心代码非常短:
import time import usb_hid from adafruit_hid.keyboard import Keyboard from adafruit_hid.keycode import Keycode kbd = Keyboard(usb_hid.devices) while True: time.sleep(3) kbd.send(Keycode.WINDOWS, Keycode.R) time.sleep(0.3) kbd.send(*[Keycode.C, Keycode.M, Keycode.D]) kbd.send(Keycode.ENTER)这段代码的效果是:每三秒模拟一次 Win+R 打开运行框,再输入 cmd 并回车。实际做宏键盘的时候,可以把触发源换成按键引脚,或者用 Pico 的 ADC 读取摇杆,便能做一个自己定制的游戏摇杆控制器。要特别注意的是,kbd.send 发送的是组合键按下再释放的完整动作,如果你要模拟长按,需要用 press 和 release 两个方法分别控制按下和抬起。
4.4 为什么 CDC 不能替代 HID
经常有人想走捷径:用 MicroPython 的 print 往虚拟串口发字符串,然后电脑端用脚本读取 COM 口来执行操作。这条路不能说错,但它有两个天然劣势:第一,CDC 虚拟串口需要电脑端有程序在监听,而 HID 设备是操作系统级识别的,所有窗口都通用;第二,CDC 发的是字节流,操作系统不会把它解释成标准键鼠指令,而 HID 报告有标准格式,操作系统会自动解析。
如果你做的是一个需要临时跑脚本的工具,CDC 方案完全够用。但如果你做的是“插上就能当键盘用”的产品原型,或者拿给别人用的时候不想装任何软件,那必须走 HID。这个取舍在做方案选型时就要想清楚。
5. 常见故障排查:为什么电脑不认你的 Pico
Pico 的 USB 问题,一半出在硬件接线和供电,一半出在固件描述符和操作系统匹配。这里把最典型的几个场景整理成速查表。
5.1 五个高频现象与对应处理
| 现象 | 可能原因 | 处理办法 |
|---|---|---|
| 插上电脑毫无反应 | USB 线只有充电功能,没有数据线芯 | 换一根确认能传数据的线 |
| 提示“设备描述符请求失败” | DP/DM 信号异常、DP 上拉失效、供电不足 | 换后置 USB 口;用逻辑分析仪看 D+ 是否被拉高 |
| 显示为“RP2 Boot” | 芯片处于 BOOTSEL 模式或用户程序未启动 | 重新上电,按住 BOOTSEL 手动烧固件 |
| 串口能识别但打不开 | 该 COM 口已被其他程序占用 | 关闭占用程序,或重新插拔更换 COM 口 |
| 代码运行后 USB 掉线 | 枚举时固件卡死或看门狗复位 | 检查固件是否自定义了 USB 描述符,打印到 USB 的频率是否过高 |
印象最深的一次是客户拿板子过来,说 Pico 偶发“掉盘”,排查很久发现是使用了一根超过两米的 USB 延长线。USB 全速的电气信号虽然只有 12 Mbps,但对线材质量和接触电阻依然敏感,Pico 这类低成本板子上的 USB 座焊接质量参差不齐,配合劣质线材很容易在枚举阶段判死刑。所以我的第一条建议永远是:先换线,再查硬件,最后怀疑固件。
5.2 用系统日志辅助定位问题
Linux 上排查 USB 问题比 Windows 直观得多。把 Pico 插入后,立刻执行 dmesg | grep usb,你会看到从 USB 端口状态、设备速度、描述符读取、配置分配到设备注册的完整链路。如果系统报告类似 “device descriptor read/64, error -71” 这样的错误,基本上可以断定这是电气层面的问题,常见原因是 D+ 上拉不稳、接触不良或者设备反复重启。如果 dmesg 里没有报错但设备节点没有出现,多半是描述符里的类或端点配置有问题,主机拒绝加载驱动。
Windows 上虽然没有这么清晰的日志,但设备管理器里把“查看 → 显示隐藏的设备”打开,能看到历史遗留的灰色设备。把多余的和冲突的旧设备全部卸载,再重新扫描硬件,很多奇奇怪怪的问题都能消失。我经常开玩笑说,Windows 的 USB 驱动栈是“记忆大师”,记得太多旧设备并不是好事,果断清理比反复重装驱动更有效。
5.3 驱动、线材与电源:最后一个被怀疑的角色
Pico 的原生 USB 设备不需要额外安装厂商驱动,但很多用户会额外买 USB 转 TTL 模块、USB 转 485 模块来调试,这才需要安装 FT231X、FT232R 或 CH340 驱动。这两条链路经常被混淆:一个走“RP2040 原生 USB”,一个走“外部 UART 转 USB 芯片”。如果你调试时连的是外接模块,却纠结 Pico 自身怎么没反应,那就南辕北辙了。
供电问题也值得单独说。Pico 通过 USB 供电时,如果同时接了很多外设,比如舵机、LED 灯带、屏幕,总电流超过 500mA 后很容易把 USB 电压拉垮,导致设备反复枚举、掉线,甚至直接让 USB 控制器进入过流保护。给 Pico 用独立 5V 电源供电,再共地连接电脑,是一个稳妥的解决办法。
6. USB 抓包与协议分析:怎么把“看不见的握手”变成可见日志
有时候靠现象猜问题猜不准,最直接的办法就是把 USB 总线上的数据包抓下来看。对于 USB 全速设备,抓包并不需要多昂贵的仪器。
6.1 低成本抓包方案
方案一,使用带 USB 解码功能的逻辑分析仪。把逻辑分析仪的通道接到 Pico USB 座的 D+ 和 D-,同时接好 GND,采样率建议设置在 50MS/s 以上,触发条件设为 D+ 上升沿。插入 Pico 后,分析仪能捕捉到从复位到枚举完成的一串包文,包括 SOF、SETUP、IN/OUT 事务。免费软件如 PulseView 的 USB 协议解码器就能把时序解码成可读的字段。
方案二,在 Linux 主机侧用 usbmon 抓包。先挂载 debugfs,然后通过 Wireshark 的 usbmon 接口抓取内核 USB 子系统收到的所有事件。这个方案不需要额外硬件,非常适合分析“固件描述符到底怎么被主机读取的”。Wireshark 里会显示每一次控制传输请求的是什么描述符、设备返回了什么内容、主机有没有因为数据异常而放弃。
6.2 用抓包定位一个真实案例
有一次我自己写自定义描述符,想在 Pico 上同时保留 CDC 和 HID,结果 Windows 只能识别其中一个。现象看起来是驱动冲突,但用逻辑分析仪抓包后发现,主机在读取配置描述符之后,紧接着连续发了几次 GET_DESCRIPTOR 请求,设备固件却迟迟没有正确回应端点描述符,导致主机只接受了一半配置。问题定位到代码后,发现是描述符长度计算少算了两个字节。
如果没有抓包,这种问题靠猜需要折腾好几天。我现在的习惯是:只要涉及到“插上后系统提示无法识别”,优先接抓包工具,先看 D+ 上拉和总线复位时序,再看枚举阶段的描述符返回,基本半小时内能圈定问题范围。与其反复重拔插碰运气,不如让协议栈告诉我们真相。
6.3 进阶应用:做成协议分析小工具
Pico 本身也算一个不错的 USB 开发平台,不少人用它做 USB 协议转换器、USB 分析仪前端、甚至自制 MIDI 控制器。加上 MicroPython/CircuitPython 的快速开发特性,几百行代码就能把 Pico 变成具备特定功能的 USB 外设。像 ESP32-S3 那类带 USB 的芯片常见于 USB 摄像头应用,而 Pico 更适合做 USB 人机交互、存储和串口类设备,选型时合理利用各自的接口能力就好。
我在实际项目里会把“能不能用 HID 解决”“需不需要 USB Host 来读外部设备”“数据量是否落在 CDC 吞吐范围内”这三个问题列出来,先做方案筛选。Pico 虽然只有 12 Mbps 的全速带宽,但它原生 USB 带来的灵活性,在很多实际场景里比 ARM 核的主频高低更值钱。
说实话,我在 Pico 上踩过最大的坑,不是代码写错,而是惯性思维太严重。以前玩 STM32 开发板,插上就自动出 COM 口,对 USB 底层完全无感;换到 Pico 之后,第一次模拟出键盘时那种“这居然真的是键盘”的体验,才让我真正理解了 USB 协议栈的价值。如果你现在也被 USB 识别问题困住,不妨从硬件原理那一节开始过一遍,逐层排查;如果只是单纯想做个好玩的宏键盘,直接跳到实操部分按流程走就行。这种小板子最不怕折腾,试错成本几乎为零。