news 2026/9/9 9:06:26

树莓派Pico USB-CDC虚拟串口与select非阻塞读取实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
树莓派Pico USB-CDC虚拟串口与select非阻塞读取实战指南

做嵌入式开发,串口是打交道最多的老朋友了。以前调板子,得备一个USB转TTL模块,接线、对上波特率、电平匹配,稍不注意就是乱码或者烧口。树莓派Pico这块板子给了我一个非常省心的路子——它原生支持USB-CDC,USB线一插,电脑上直接就多出一个虚拟串口,不用额外硬件,不用手动装驱动的场景,再加上MicroPython里用select做非阻塞读取,整套流程下来,调试和上位机通信都变得特别顺。

这篇文章我就把这个组合拿出来完整拆一遍:USB-CDC虚拟串口是怎么回事,为什么选select而不是简单用readline,以及怎样用Pico做一个能被串口指令控制、带舵机执行、还能回显状态的完整小项目。内容不只贴代码,还会把原理、参数选择、踩过的坑一起讲清楚。适合刚拿到Pico想玩串口通信的新手,也适合做上位机联调、机器人控制这类项目的老手拿来当参考。

1. 为什么用USB-CDC虚拟串口:选型思路和背后的逻辑

1.1 USB-CDC和物理UART的区别:一个接线,一个免驱

在Pico这类板子上,串口通信有两条路。一条是芯片里的UART外设,通过GPIO引脚输出信号,需要外部接USB转TTL模块才能和电脑通信;另一条就是USB-CDC,全称USB Communications Device Class,它把USB连接抽象成一个串口,主机看到的设备就是一个COM口或者/dev/ttyACM设备。

两条路最大的区别在于硬件成本和易用性。UART方案需要额外模块、杜邦线,还得处理3.3V和5V电平转换的问题,电平一旦不匹配,轻则数据乱码,重则烧芯片。USB-CDC方案只需要一根USB数据线,Pico的RP2040芯片内置了USB控制器,MicroPython固件又把CDC默认做成了REPL通道,插上就能用。这个“插上就能用”在调试阶段非常关键,减少了大量“线没接对”这种低级但致命的错误。

而且USB-CDC在原理上仍然是字符流,和UART一样按字节收发,所以应用层代码不用大改,区别只在于底层传输介质从串口线换成了USB。对写逻辑的程序员来说,这个抽象很友好,代码里读到的还是stdin,写出的还是stdout,不感知物理链路的变化。

1.2 内置USB外设和MicroPython的默认行为

RP2040内置了USB 1.1全速控制器,可以工作在设备模式。MicroPython官方固件在编译时默认开启了USB-CDC支持,具体说就是:把USB端点0和端点1配置成CDC数据通道,同时把这个通道绑定到标准输入输出上。这意味着你打开Thonny或者任何串口终端,连上那个虚拟串口,敲进去的字符会一条一条进到Python解释器,print出来的东西也会从这里送出来。

有个细节很多新手不知道:Pico出厂固件是没有这个功能的,拿到手第一步通常要刷MicroPython固件,方法就是按住板子上的BOOTSEL键再插USB线,电脑会弹出一个小U盘,把.uf2固件文件拖进去就完成了。刷完之后,电脑设备管理器里才会出现串口。这里要提醒一句,如果你想要USB Host、也就是让Pico去当主设备插U盘或者USB键盘,那需要刷社区编译的特殊固件,本文所有内容都基于官方固件的设备模式,不做USB Host展开。

MicroPython在这个通道上做了个很顺的设计:它把USB CDC同时作为REPL交互终端。也就是说你print出来的内容,不只是你的程序输出,还能被REPL直接用。这个特性在开发阶段特别好用,程序里加几行print,马上就能在终端里看到运行状态,不需要额外接调试线。

2. select机制在MicroPython中的正确打开方式

2.1 select不是在查数据库,是I/O多路复用

说到select,很多人第一反应是SQL的SELECT语句,或者Linux命令行里的select命令,但这里说的是Python标准库select模块,以及它背后的I/O多路复用机制。名字一样,完全是两码事。在这个项目里,select的核心作用,是让我们能“不阻塞”地检查串口有没有新数据。

如果你用input()或者sys.stdin.readline()直接读数据,程序会卡在那里等输入。这个设计在交互式终端里没问题,但在嵌入式控制场景里就是灾难——你等串口指令的时候,舵机没法定时刷新,传感器没法采样,LED没法闪烁,整个程序变成了一根筋。select的作用就是帮你“看一眼”有没有数据到来,有就处理,没有就继续跑主循环,配合超时参数,还能实现精确的时间片轮转。

拿排队打饭来类比:阻塞读就像你站在打饭窗口前死等,窗口没人出来你也只能站着;select轮询则是过几秒钟跑去窗口看一眼,有饭就打,没饭就回去干别的活。两种方式在等待这件事上是一回事,但对其他工作的影响差别巨大。

2.2 MicroPython的select API和CPython的差异

MicroPython的select模块是标准库的简化版,用法上大致相同:select.select(rlist, wlist, xlist, timeout)。参数含义分别是可读列表、可写列表、异常列表和超时秒数,返回值是三个元组,表示实际就绪的对象。在Pico上,最常放进rlist的就是sys.stdin还有socket对象。

要注意MicroPython的select不像CPython那样什么文件对象都支持。它更偏向流式对象,比如usocket、文件流、UART对象,以及sys.stdin。在你的代码里,把sys.stdin放进第一个列表,它就能监听USB虚拟串口的可读状态。timeout参数也很讲究,设成None表示一直等,设成0表示立即返回,设成一个浮点数就是最长等待秒数。

对于超时值,我实测下来有个经验:如果主循环里还有别的周期任务,比如舵机PWM刷新、传感器采集,超时设成0.02到0.1秒之间比较合适。太短会让CPU在小间隔空转白费电,太长会导致指令响应变慢,体感上就像“卡死”了一样。假如你设了0.5秒,串口数据发过来可能要等半秒才处理,控制类项目根本没法接受。

2.3 用select同时管串口和其他事件源

select最值钱的地方,是能同时监听多个事件源。比如你想一边处理USB串口指令,一边监听网卡收到的数据,或者同时监听两个串口,一个接GPS模块一个接主板,普通的阻塞读只能一个串口一个串口地等,select却可以一起等,谁来了先处理谁。

在MicroPython里实现多源监听很简单,把多个对象都塞进rlist就行:

import select import sys import socket rlist = [sys.stdin, some_socket] wlist = [] xlist = [] while True: readable, _, _ = select.select(rlist, wlist, xlist, 0.1) for r in readable: if r is sys.stdin: # 处理串口指令 pass elif r is some_socket: # 处理网络数据 pass

这个模式在处理复杂项目时几乎成了标准写法,代码结构清晰,不会出现一个通道忙等拖死其他通道的情况。即使你现在只跑串口一个事件,提前学会这种多路复用的写法,后面加功能也容易得多。

3. 从零搭建一个虚拟串口控制的舵机项目

3.1 硬件准备和接线

这部分我们做一个实际可用的项目:用Pico虚拟串口接收角度指令,控制一个舵机转到指定角度,并回显执行结果。要准备的东西不多:

一个树莓派Pico主板,一根USB数据线,一个SG90舵机,三根杜邦线,一块面包板。SG90是5V供电的,Pico的3.3V引脚带不动它,所以供电需要单独处理。推荐用一个5V电源给舵机供电,同时和Pico共地。共地这个操作经常被忽略,但不共地的话,信号线上的电平参考基准不一致,舵机就会乱抖甚至不动。

接线方式很简单:舵机棕色线接GND,红色线接5V外部电源正极,橙色信号线接Pico的GP15。这里把GP15作为PWM输出引脚,和外部电源的负极同时接到Pico的GND上,形成共地。别直接把舵机红色线插到Pico的3.3V,大电流会把板子上的稳压电路拖垮,这个坑我见过不止一次。

3.2 固件刷写和串口驱动检查

先刷MicroPython固件。去MicroPython官网下载RP2系列最新的.uf2文件,然后按住Pico上的BOOTSEL键不放,用USB线连接电脑,等电脑上出现一个名为RPI-RP2的U盘,把.uf2文件复制进去。复制完Pico会自动重启,这时电脑上就会多出一个串口设备。

在Windows下,设备管理器里的“端口(COM和LPT)”下面会看到类似“USB Serial Device (COMx)”的条目,这就是虚拟串口。Linux和macOS下一般显示为/dev/ttyACM0或者/dev/ttyACM1。如果没看到设备,先检查用的USB线是不是数据线,现在很多线只能充电不能传数据,这个问题出现频率出奇地高。

串口号确认后,可以在电脑上用串口工具打开验证一下。Windows推荐用MobaXterm的串口会话,或者干脆用Python的pyserial库快速测试,Linux下用screen或miniterm都行。波特率这里随便填,因为USB-CDC是虚拟串口,波特率设置其实不影响USB传输速率,脚本里写成115200是为了兼容习惯,实际数据走的是USB全速协议。

3.3 Pico端代码:select循环加指令解析

下面是Pico端完整的MicroPython代码,逻辑分成三块:PWM初始化、角度转换、select监听主循环。每一块都有注释,方便直接改来用。

from machine import Pin, PWM import select import sys import time SERVO_PIN = 15 servo = PWM(Pin(SERVO_PIN)) servo.freq(50) # 50Hz,周期20ms,SG90的标准工作频率 def set_angle(angle): # 0°~180° 映射到 0.5ms~2.5ms 高电平脉冲 # 20ms周期内,占空比 = 脉冲宽度 / 周期 pulse_us = 500 + (angle / 180) * 2000 duty_ns = int(pulse_us * 1000) servo.duty_ns(duty_ns) def main(): print("SERVO-CDC READY") while True: # 50ms超时,兼顾响应速度和主循环刷新 ready, _, _ = select.select([sys.stdin], [], [], 0.05) if not ready: continue line = sys.stdin.readline() if not line: continue text = line.strip() if text == "ID?": print("PICO-SERVO-V1") elif text.startswith("S"): try: value = int(text[1:]) value = max(0, min(180, value)) set_angle(value) print("OK", value) except ValueError: print("ERR INVALID_ANGLE") else: print("ERR UNKNOWN_CMD") main()

这里指令格式设计成文本行协议:ID?表示查询设备,S90表示让舵机转到90度,每条指令以换行符结束。select的0.05秒超时能让主循环每秒刷新20次,既不浪费CPU时间,对外部指令的响应也够快。有人会问,为什么要用text[1:]这种字符串切片解析,直接split不好吗?这里因为指令格式固定,S后面直接跟数字,切片加int转换是最简单高效的做法,逻辑清楚不容易出错。

3.4 上位机发送控制指令

上位机这边我用Python的pyserial库来发指令,最简单的测试就是打开Python交互环境:

import serial import time ser = serial.Serial("COM5", 115200, timeout=1) time.sleep(0.5) # 等待设备重启完成 ser.write(b"ID?\r\n") time.sleep(0.1) print(ser.readline()) # 应该输出 b'PICO-SERVO-V1\r\n' ser.write(b"S180\r\n") time.sleep(0.1) print(ser.readline()) # 应该输出 b'OK 180\r\n' ser.close()

两个细节值得注意。第一,write发送的是bytes类型,不是str,所以b前缀不能省;第二,每条指令后面要加\r\n,这是我在协议里预期的帧尾,如果发送端不带换行,Pico那边的readline会一直等下去,指令永远不执行。如果你用的是串口助手类软件,记得打开“发送新行”选项,或者自己手动加一个换行。

4. 数据协议设计:怎么避免粘包、乱码和指令卡死

4.1 文本行协议还是二进制帧协议

虚拟串口本质是字节流,本身没有“一条消息”的概念,所以应用层必须自己定边界。最常用的方案有两种:文本行协议和二进制帧协议。

文本行协议用换行符作为一条指令结束的标志,优点是直观可读,串口助手里直接就能看明白内容,调试太方便了。适合指令种类少、数据量小的控制场景,像我们上面的舵机控制就是典型代表。缺点是传输浮点数据时解析麻烦,也不适合传大段二进制数据。

二进制帧协议则是自己定义一个帧结构,比如帧头、长度、数据、校验,一般用struct打包。优点是紧凑、解析快、支持复杂数据类型,但调试时肉眼没法看,得靠专门工具解析。需要传输传感器批量数据、浮点数组这类场景,可以过渡到这种方式。

协议选型有个很实际的建议:先别为不存在的复杂度买单。一开始就用文本行协议,等确认要传大量数据了,再在下一版升级成二进制帧。我在实际项目里见过不少人上来就搭了复杂的帧协议,结果连调试阶段都走得磕磕绊绊,其实很多应用文本行已经够用。

4.2 粘包和半包问题的处理思路

串口通信里粘包和半包是最常见的两个问题。粘包是两条指令粘在一起到达,半包是一条指令被拆成了几段。在虚拟串口上,因为USB传输本身会分包,情况会更复杂一点。

如果采用readline这种按行读取的方式,半包问题基本能自动解决——readline只有读到换行符才返回,数据没齐就一直等。但这会带来一个副作用:如果发送端漏了换行符,readline会一直阻塞。为了规避这一点,配合select的超时机制就非常重要,select负责定期检查数据是否到来,readline只负责在数据到达后取整行,两者配合就不会卡死主循环。

粘包问题出现在高并发或者一次性发送多条指令的场景,比如发送端连续write了“S90”“S180”,在接收端可能一次readline就读到了“S90\nS180\n”两个完整指令。这种场景下,读取循环里就不能只调用一次readline,要写成循环,把所有已到达的数据一次性全部读完再处理:

while True: ready, _, _ = select.select([sys.stdin], [], [], 0.05) if not ready: continue while sys.stdin in select.select([sys.stdin], [], [], 0)[0]: line = sys.stdin.readline() if line: handle(line.strip())

这段代码用两层循环:外循环负责等数据到达,内循环把缓冲区里所有剩余指令全部读干净。别小看这个细节,在真正的高频控制场景里,粘包处理不好,指令执行就会一顿一顿,甚至乱序。

4.3 一个通用指令解析框架

如果只是舵机控制,上面那段代码已经够用了。但很多项目并不只有一个指令类型,可能又要控灯、又要控电机、还要查状态,这时可以整理出一套统一的指令分发逻辑。核心就两样:先按分隔符拆行,再做键值映射。

比如指令格式可以设计成“CMD=KEY:VALUE”,像“MOTOR=X:100”表示设置电机X的值为100,“LED=Y:ON”表示打开Y灯,解析时先取等号左边作为命令名,再处理右边参数。代码可以这样组织:

def handle_cmd(cmd): if "=" not in cmd: print("ERR NO_EQUALS") return name, params = cmd.split("=", 1) if name == "LED": led_key, state = params.split(":", 1) set_led(led_key, state == "ON") elif name == "MOTOR": motor_key, speed = params.split(":", 1) set_motor_speed(motor_key, int(speed)) else: print("ERR UNKNOWN_CMD")

这套框架的好处是新增指令只需要加一个elif分支,不会影响已有逻辑。即使将来指令数量到了二三十个,维护起来也还撑得住。等指令规模再往大走,再去考虑把命令注册表改成字典映射的方式,让每条命令绑定一个函数,解析时直接查表调用。模块化是一步步来的,没必要第一版就追求完美架构。

5. 常见问题和排查技巧实录

5.1 串口识别不了、驱动异常怎么办

这是出现频率最高的问题。先分清是硬件问题还是固件问题:换一根确认能传数据的USB线再试,很多“识别不了”其实是充电线在捣乱。其次看Pico是不是真的刷了MicroPython固件,出厂C语言测试固件刷完之后串口行为可能不同。如果电脑上还是只看到RPI-RP2的U盘,说明设备还在BOOTSEL模式,拔了重新插一下即可。

Windows如果出现设备管理器里是未知设备,一般是驱动缓存的问题。可以右键卸掉这个设备,然后重新插拔USB线,让系统重新枚举。Linux下如果看到/dev/ttyACM0存在但打不开,多半是权限问题,把用户加进dialout组再重新登录就好。

5.2 select收不到数据的几个隐蔽原因

代码看起来完全没问题,select就是在超时,eralist里面永远是空的,这种问题最常见的根源是:终端工具占用了串口。Thonny、PuTTY、MobaXterm、miniterm,任意一个至少一个程序把串口打开着,你的MicroPython脚本就读不到数据,因为USB-CDC同一时间基本只有一个客户端能完整拿到这个设备。所以调试时记得把Thonny的串口连接断开,再跑自己的Python脚本。

另一个坑是REPL本身还在占用输入通道。MicroPython的USB-CDC同时承担REPL功能,如果终端里正处于输入状态,select读到的可能是空行或者REPL的调试输出混在一起。解决方法是尽量让程序自己print和read,而不要同时手动往REPL里敲命令,人和程序抢一个输入通道,很容易互踢。

第三类问题是MicroPython固件版本太老,部分早期版本对USB-CDC流对象和select的兼容性有bug,表现为时好时坏。解决方法是升级固件到官方最新稳定版,然后重新部署运行,这一步能解决不少莫名其妙的问题。

5.3 舵机抖动和供电问题的排查

舵机抖动大部分时候不是代码问题,是供电问题。SG90启动电流能达到几百毫安,瞬态电流甚至超过1A,靠Pico的3.3V稳压器供电,电压一掉,PWM信号也跟着失真,舵机就会高频抖动。标准做法是给舵机单独一个5V电源,或者至少用一个能输出1A以上的稳压模块,然后和Pico共地。

PWM频率设置也要注意。SG90最常见的标准是50Hz,对应20ms周期。如果你把freq改成200Hz,大多数模拟舵机根本没法正常工作,会听到持续的滋滋声。另外角度映射范围不能盲目用0到180,不同舵机的脉宽范围有差异,有些舵机0度对应0.4ms高电平,180度对应2.5ms,还有些是0.5ms到2.4ms。拿到舵机先看数据手册,用之前先小范围测试一下极限角度,避免撞到机械限位齿上。

5.4 串口问题排查速查表

整理一个对照表,直接按现象找方案。

现象可能原因解决方法
电脑找不到串口用的充电线,或未刷固件换数据线,重新刷MicroPython固件
设备管理器显示未知设备驱动识别失败右键卸载设备后重新插拔
Linux下打开串口报权限错误当前用户不在dialout组执行 sudo usermod -aG dialout $USER
串口能开但select一直是空的Thonny或终端工具占用串口关闭其他占用串口的所有软件
发送指令没反应,REPL正常指令末尾缺换行符发送时补上\r\n
一条消息解析成两次处理发送端自动多发了换行接收端用strip去掉空白后处理
舵机抖动、角度不准供电不足或PWM频率不对外部5V供电,freq设为50Hz
代码正常但偶尔卡死半包时readline阻塞用select超时限制等待时长
数据时好时坏MicroPython固件过旧升级到最新官方稳定版固件

5.5 调试阶段值得养成的几个习惯

调试串口项目,我建议养成一个习惯:把设备端的状态打印设计成机器能读的文本,而不是只给人看的提示语。像前面代码里的“OK 180”“ERR INVALID_ANGLE”,格式统一,上位机直接可以根据这些字符串做自动化测试,不用人工去看屏幕。这相当于给设备定义了第二套状态协议,对后期维护帮助很大。

另外,不要小看串口工具的“发送新行”和“hex模式”这两个选项。调试文本协议时一定要开启发送新行,否则你肉眼看到的指令和实际字节流对不上。而hex模式可以在乱码时帮你看清楚每一个字节的原始值,用来判断编码问题再合适不过。

6. 实测下来的一些心得和小技巧

最后再讲一点实际使用中感悟比较深的东西。USB-CDC虚拟串口加select这套组合,最大的好处不是节省了一根USB转TTL线,而是让调试循环变得特别短。以前边改代码边看串口输出,至少要经过“改代码、上传、复位、观察”四步,现在Pico插着USB线,改完用工具上传,几乎是无缝衔接,整个开发节奏快了很多。

select的超时参数选值,我后来总结出一个经验:不要盲目用小超时去“提高响应速度”,而要先想想主循环里还有什么周期任务。如果只是控制舵机的话,20到50毫秒的超时就够了,响应已经够快,CPU也不会空转发烫。如果除了串口还要做按键扫描、显示刷新,超时值甚至可以拉到100毫秒,把刷新和扫描放在主循环里,让select来兜底。MicroPython没有真正的多线程,所以代码结构上尽量把事情都放在一个主循环里编排,这比硬套线程要可靠得多。

还有一个让我受益很大的习惯:串口协议设计一定要留一个类似ID?这样的查询指令。看似简单,但它既是联调时确认链路通不通的探针,也是设备是否复位的标志。每次上位机连上设备先发一条ID?看回复,链路状态瞬间就清楚了,省掉了反复猜疑的环节。这种小设计在复杂系统里会成倍地省时间。希望这篇文章能帮你把Pico的虚拟串口玩得更顺,少走我当年走过的弯路。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 9:05:33

STM32F103C8T6步进电机驱动实战:从CubeMX配置到调试避坑

简介:面向STM32F103C8T6开发者的步进电机驱动完整例程,基于HAL库实现,涵盖GPIO推挽输出、定时器PWM生成、中断换相以及细分驱动等关键环节,适合刚入门电机控制的学生、工程师,也适合需要参考实际硬件接线的项目开发者。…

作者头像 李华
网站建设 2026/9/9 9:04:41

从AI直播到Voice Agent:实时语音交互背后的推理优化与关键技术栈解析

最近MiniMax H3 MAX的AI直播在圈里讨论度很高,直播间里一个数字人用几乎以假乱真的语气和用户实时对话,还能在几十秒内生成带情绪的语音回复。很多人第一反应是“这又是哪家搞营销的”,但真正做语音技术的人盯着的其实是另一个问题&#xff1…

作者头像 李华
网站建设 2026/9/9 9:03:55

Robot Framework失败自动重跑机制:从原理到Jenkins集成实践

做RF(Robot Framework)自动化测试的同学,大概率都遇到过这种情况:一个用例昨天还好好跑过,今天CI(持续集成)上突然红了一波,你点开日志一看,定位元素超时、网络抖动、环境…

作者头像 李华
网站建设 2026/9/9 9:00:29

ViL整车在环测试:从原理到工程落地的智能驾驶验证指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/9 9:00:04

基于遗传算法的风电混合储能容量优化配置及MATLAB实现

我做了几年风电功率预测和储能配置的项目,接触过不少用 MATLAB 做容量优化的需求,其中“基于遗传算法的风电混合储能容量优化配置”这个方向问的人最多。原因也简单:风电出力天生波动,光靠单一储能削峰填谷要么贵得离谱&#xff0…

作者头像 李华