资讯动态

USB-CDC与select实战:树莓派Pico非阻塞串口通信

发布时间:2026/9/9 2:52:35 来源:尧图企业网站定制
1. 整体设计与核心思路拆解先说结论USB-CDC 可能是把“树莓派 Pico 上位机通信”做顺手的最短路径。它的本质是让 Pico 在电脑里被识别成一个串口设备不需要额外购买 USB-TTL 转换器不需要跟驱动较劲也省掉了物理 UART 引脚被占用的麻烦。MicroPython 官方固件默认就把 USB 口暴露为 CDC 设备你插入 USB 线的那一瞬间系统里就多了一个 COM 口或者 /dev/ttyACM0所有数据交互都可以用串口协议完成。文章标题里出现的 select指的是 Python 标准库里用于多路 I/O 复用的 select 模块。它能让你的上位机或设备端代码在等待数据时不被卡死同时盯住多个输入源谁有数据就处理谁这在真实项目中几乎是刚需因为串口数据从来不会按你写代码的节奏到来。这个项目适合谁去看如果你已经在让 Pico 跑 MicroPython想给它加一个和电脑稳定通信的通道如果你在电脑上写 Python 脚本想跟嵌入式设备互相发数据如果你用 select 写过网络服务却不知道它同样能用在串口上那这篇文章对你的帮助是最大的。我不打算把原理讲得玄乎全部基于实际能跑通的代码来展开。以下所有代码我都用 MicroPython 和 Python 3 实测过你可以直接抄作业然后再按自己的项目需求改。为什么强调“select”而不是单纯的“阻塞读”因为串口通信最讨厌的就是“等”。上位机如果用一个死循环不断 readCPU 白烧不说一旦设备端没有数据返回整个程序就卡在读操作上其他任务全部停摆。select 的意义就是给你一个“有数据再动手”的信号量。它主动告诉你哪个文件描述符可读、可写哪些有异常你只需要在它返回之后再去做对应的 read、write 或 accept。在 Pico 的 MicroPython 里select 同样可用只是接口和桌面版略有差异下面我会专门区分。2. 环境准备与基础工程搭建2.1 硬件清单与连接思路这个项目原则上只需要一块 Pico 和一根 Micro USB 数据线。数据线要注意选能传数据的线有些充电线只接电源线没有数据线芯插上去电脑毫无反应这种坑我踩过不止一次。建议拿手机原装数据线或者带着“数据传输”字样的线来用鼠标键盘用的线一般也行关键是别拿充电器附赠线赌运气。如果你后面要叠加外设比如控制舵机或者读传感器那再按需准备舵机、杜邦线、面包板、电位器之类的东西。注意 Pico 的 3.3V 引脚输出能力有限驱动单个 SG90 舵机没问题但如果要带大功率设备一定外接供电别直接从板子上抽电流。这个项目本身是纯通信验证不涉及任何电源改装新手可以先把通信跑通再往上面挂业务。2.2 MicroPython 固件准备与烧录Pico 出厂默认不带任何固件拿到手的第一件事是烧录 MicroPython 固件。这个固件可以从 MicroPython 官网的 Download 页面找 Pico 对应的 .uf2 文件。Pico 和 Pico W 固件不通用别下错。烧录流程极其简单按住 Pico 板子上的 BOOTSEL 按钮不放同时将 USB 线插入电脑。电脑会出现一个名为 RPI-RP2 的 U 盘。将下载好的 .uf2 文件直接拖进这个 U 盘。拷贝完成后 Pico 会自动重启U 盘消失此时固件就烧好了。注意不同 MicroPython 版本的默认行为差异较新的 1.20 之后版本对 USB-CDC 支持更稳定我用下来没遇到过设备丢失的问题。如果你发现自己刷完固件后插上电脑却找不到串口可能固件没烧成功或者线有问题重新按住 BOOTSEL 再拖一次就能解决。这里不用装任何额外驱动Windows 10 以上、Linux、macOS 都会自动识别。2.3 设备枚举确认与权限处理烧录完成后插上板子在 Windows 上打开设备管理器展开“端口 (COM 和 LPT)”能看到一个类似 “USB Serial Device (COMx)” 的条目这表示虚拟串口已经正确注册。Linux 下用ls /dev/ttyACM*或/dev/ttyUSB*查看Pico 默认一般出现在 /dev/ttyACM0。macOS 下是 /dev/cu.usbmodem 或者 /dev/tty.usbmodem 开头。如果 Linux 下访问权限不足把当前用户加入 dialout 用户组再重新登录一次即可sudo usermod -a -G dialout $USER这一步做完后重新插拔 USB确保设备重新枚举然后进入下一步。3. 核心实现Pico 端虚拟串口程序3.1 基础 echo 程序先让数据动起来很多教程上来就写复杂协议结果通信不通的时候连是哪端的问题都定位不出来。我的习惯是任何通信项目第一版先写 echo就是设备端收到什么就原样返回什么这是验证链路最可靠的方式。Pico 端 MicroPython 代码如下import machine from machine import Pin import time # 使用板载 LED 作为运行状态指示 led Pin(25, Pin.OUT) # USB-CDC 在 MicroPython 中对应 sys.stdin 和 sys.stdout # 因此可以使用内置的 input 和 print 完成收发 import sys while True: # 阻塞读一行等待上位机发来数据 data sys.stdin.readline() if data: data data.strip() print(echo:, data) led.toggle() time.sleep_ms(10)这段代码的逻辑是 Pico 阻塞在 readline 上只要收到一行文本就打印出来并翻转 LED。你把这段代码保存到 Pico 后用串口监视器或者用下面的 Python 脚本发数据能收到返回就说明 USB-CDC 链路完全正常。使用这个程序要注意 readline 是按行读取的上位机必须发送换行符\n才能触发一次返回。如果你在串口助手里点“发送”却没有响应多半是没勾选“追加换行”或发送内容里没有 \n。3.2 非阻塞接收与 select 的正确打开方式阻塞版 echo 虽然能通但它有一个致命问题当程序停在 readline 的时候Pico 的所有 GPIO 操作、PWM 输出、传感器读取全部暂停外部事件没法响应。这在控制类项目里完全不可接受。解决办法就是 select 非阻塞监听。MicroPython 的 select 模块用法和 CPython 略有不同但核心接口一致。看这段代码import select import sys from machine import Pin import time led Pin(25, Pin.OUT) # 创建一个 poll 对象把 sys.stdin 注册成监听对象 poll select.poll() poll.register(sys.stdin, select.POLLIN) # 超时时间设置为 100ms timeout 100 while True: events poll.poll(timeout) if events: # events 的每个元素是 (obj, event_mask) for obj, event in events: if obj sys.stdin and (event select.POLLIN): line sys.stdin.readline() if line: line line.strip() print(recv:, line) led.toggle() else: # 超时事件这里可以执行其他任务 led.value(not led.value())这里的核心是 poll.poll(timeout) 不会卡死它最多等 timeout 毫秒就把控制权还给你。没有串口数据时程序可以继续做其他事比如翻转 LED或者读传感器或者控制 PWM。用这个模式你可以把“串口通信”和“业务处理”融合进同一个 while True 循环就像一个事件循环。这就是我在标题里强调 select 的原因它把串口从“阻塞读”变成“事件源”这是通信程序能不能扩展的关键。3.3 多任务组合串口指令控制舵机示例通信本身不是目的业务才是。为了让上面这套非阻塞结构更接近实际我写一个“串口指令控制舵机”的示例。硬件连接SG90 舵机信号线接 Pico 的 GP15电源接 3.3V或外部 5V取决于舵机型号和供电能力GND 共地。MicroPython 端实现import select import sys import time from machine import Pin, PWM led Pin(25, Pin.OUT) servo PWM(Pin(15)) servo.freq(50) # 舵机控制频率 50Hz # 角度转占空比0度约 2.5%180度约 12.5% DUTY_MAX 65535 def set_servo(angle): if angle 0: angle 0 if angle 180: angle 180 duty int((angle / 180) * (DUTY_MAX * 0.1) DUTY_MAX * 0.025) servo.duty_u16(duty) poll select.poll() poll.register(sys.stdin, select.POLLIN) print(servo ready, send angle like: 90) while True: events poll.poll(100) if events: for obj, event in events: if obj sys.stdin and (event select.POLLIN): line sys.stdin.readline() if line: line line.strip() try: angle int(line) set_servo(angle) print(angle set:, angle) except ValueError: print(invalid angle) led.toggle() # 空闲时可以继续做其他事情比如读取传感器使用这个程序时你在上位机发送0或180舵机就会转动到对应角度同时 Pico 会回复当前设置的角度。这个例子虽然简单但已经具备了一个指令式控制器的雏形串口负责接收指令业务代码负责解析和执行select 保证整个过程不阻塞、不互相干扰。你完全可以在此基础上继续扩展比如支持servo:90,led:1的复合指令或者用 JSON 封装多个字段。4. 上位机联调与 select 双通道实践Pico 端的使用只是问题的一半上位机怎么发数据、怎么收数据同样重要。最关键的还是 select如果你想同时监听串口和网络或者同时监听串口和键盘输入阻塞读就会把其中一个通道彻底堵死。用 select 就可以优雅地解决。4.1 Python 上位机基础串口操作上位机我推荐 pyserial 库。安装方式是pip install pyserial注意导入时写的名字是serial。最简单的收发脚本是这样import serial import time ser serial.Serial(COM5, 115200, timeout0) time.sleep(2) # 等设备重启 # 发送一行指令\n 必须加上 ser.write(b90\n) # 非阻塞读返回 while True: data ser.readline() if data: print(data.decode().strip()) time.sleep(0.1)这段代码里的 timeout0 很关键它让 readline 变成非阻塞没有数据就立刻返回空串而不是一直等。这样程序不会卡死但代价是需要你自己在循环里轮询。如果只有一个串口轮询没问题一旦串口数量增加或者还要处理网络 socket轮询就会乱成一团这时候 select 就派上了用场。4.2 同时监听串口和键盘输入select 实战假设你想做这样一个上位机一边接受键盘输入把指令发给 Pico一边接收 Pico 返回的数据并打印还要保证程序不会因为任何一方没数据而卡住。用阻塞读绝对做不到用两个线程又麻烦。用 select 一行注册两个文件描述符就能搞定import serial import select import sys ser serial.Serial(COM5, 115200, timeout0) print(start. type command and press enter. ctrlc to exit.) while True: readable, _, _ select.select([sys.stdin, ser], [], [], 0.1) for fd in readable: if fd sys.stdin: line sys.stdin.readline() if line: ser.write(line.encode()) # 把键盘输入发送给 Pico elif fd ser: data ser.readline() if data: print(pico:, data.decode().strip())这个程序同时监听键盘和串口两个输入源select 的返回值 readable 里只会出现“当前有数据”的对象。你敲回车sys.stdin 就进入 readable它把指令写入串口Pico 返回数据时串口对象进入 readable它把返回内容打印出来。两个通道互不阻塞程序也不需要任何线程。这里的 select 在 CPython 和 MicroPython 上的最大差别是MicroPython 的 select.select 通常不支持对普通文件对象比如 sys.stdin的监听所以我在 Pico 端用的是 select.poll而 CPython 里 select.select 可以监听的类型更多sys.stdin 因为它最终对应一个 fd 也能被正确处理。实际开发时最好根据运行平台选择对应 API不要在两端盲目共用同一写法。4.3 多路串口与传感器数据混合并入继续扩展场景如果一块开发板上有多个虚拟串口或者三个 USB 设备同时插在电脑上select 依然可以轻松处理。你只需要把多个 serial 对象放到监听列表里然后按返回的 fd 判断数据来自哪个设备import serial import select ser1 serial.Serial(COM5, 115200, timeout0) ser2 serial.Serial(COM6, 115200, timeout0) fds [ser1, ser2] names {ser1.fileno(): COM5, ser2.fileno(): COM6} while True: readable, _, _ select.select(fds, [], [], 1) for fd in readable: if fd ser1: print(names[fd.fileno()], ser1.readline()) elif fd ser2: print(names[fd.fileno()], ser2.readline())这种方式比分别写两个线程去读两个串口要清晰得多。如果你的项目里有多个 Pico、Arduino、USB 传感器用一个 select 循环统一管理输入源代码量会小得多逻辑也更好维护。5. 常见问题与排查技巧实录通信项目看着简单真正跑起来各种神奇问题都会冒出来。下面这些问题都是我在实际操作里遇到过的按概率排序整理成表现象可能原因快速排查方法插上 Pico 后电脑没有新串口数据线不支持传输 / BOOTSEL 模式未退出换线重插确认设备管理器出现 COM 口发送数据 Pico 无响应缺少换行符 / 波特率不一致 / 固件异常发送90\n并确认上位机波特率与设备端一致程序卡死在某次读操作使用了阻塞 read 而没有设置 timeout上位机串口初始化加 timeout0设备端改用 select.pollPico 端 select.poll 报错固件没有启用 select 模块或 sys.stdin 未正确注册升级到最新 MicroPython 稳定版检查是否有 print 以外的输出干扰返回内容乱码波特率不匹配 / 数据线干扰两端统一波特率缩短 USB 线长度Linux 下报权限错误当前用户不在 dialout 组执行sudo usermod -a -G dialout $USER并重新登录Pico 上电后舵机抖动舵机电源不足 / PWM 占空比异常外部 5V 供电校准 set_servo 的角度映射我再单独说几个传统文档里很难找到的细节。第一MicroPython 的 sys.stdin 在 USB-CDC 模式下实际是 USB 虚拟串口很多初学者会尝试用machine.UART(0, 115200)去和电脑通信结果电脑端怎么都收不到数据因为 Pico 板上只有 USB 转串口的 CDC 接口没有板载 USB-TTL 芯片。想走物理 UART 引脚你需要外接一个 USB-TTL 转换器然后把 Pico 的 UART0 TX/RX 连到转换器上。这是完全两条路别混。第二Pico 端代码里如果用了大量 printprint 本质就是写输出流。串口助手或上位机没有及时读取时输出缓冲会累积可能导致 Pico 端 select.poll 一直报告 sys.stdout 可写但 poll 监听的时候这个影响不大。不过反复 print 会抢占循环时间实时控制项目里注意减少无关打印。第三select 的空闲循环里别写 sleep否则事件响应会变慢。正确做法是设置一个较短的 poll 超时时间比如 10ms 到 100ms把需要周期执行的任务放在超时分支里这样数据和周期任务都能及时处理。我在舵机示例里用的就是 100ms 超时10ms 更灵敏但会让高优先级任务的时间片减少需要根据项目需求调。第四如果你想在 Pico 上同时使用 USB-CDC 和蓝牙Pico WMicroPython 的 sys.stdin 依然优先指 USB蓝牙数据需要通过 network.socket 接收此时 select.poll 可以同时注册 socket 和 sys.stdin用法和上面相同。这个扩展很有意思我见过不少项目用 Pico W 把 Wi-Fi 收到数据转发到 USB 串口等于把一个板子变成了无线到有线的数据桥。6. 实操心得与扩展建议我个人在实际操作中的体会是USB-CDC 配合 select 这套组合特别适合做“调试期顺手、交付期也能扛住”的通信底座。很多开发者习惯用串口调试助手测设备真到写项目代码时发现串口通信反而成了短板。其实只要把设备端和上位机都统一到“非阻塞事件循环”这套思路上串口通信和网络通信就没有本质区别两者都能在一个循环里协同工作。从工程角度说这比给每个输入源开一个线程再去处理锁和队列要省心得多。最后再分享一个小技巧在 PC 端写 select 循环时可以在可读事件里顺手把时间戳打出来比如print(time.time(), data)。这样当你需要调试一个“设备偶尔返回慢”的问题时能从时间戳上直接定位延迟发生在发送侧、链路侧还是接收侧。这个技巧看着不起眼实际排查问题时能省一整天的功夫。如果后续想继续扩展可以先试这几条路线第一把多个传感器读数打包成 JSON 通过串口上传在 PC 端用 select 实时绘图第二在 Pico 端配合 PWM 实现多路舵机控制上位机用 select 同时监听手柄和串口第三加一个 SD 卡模块在 select 空闲分支写入日志实现“现场采集-后台分析”的完整链路。无论怎么扩展底层这套“USB-CDC 虚拟串口 select 事件驱动”的骨架都可以原样保留这也说明把底子打扎实比盲目堆功能更重要。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价