news 2026/9/11 8:34:11

树莓派Pico USB-CDC与select实现非阻塞虚拟串口通信控制舵机

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
树莓派Pico USB-CDC与select实现非阻塞虚拟串口通信控制舵机

最近我把一个树莓派 Pico 项目从硬件 UART 迁移到 USB-CDC,说实话,过程比我想象中顺太多。USB-CDC 的意思是让 Pico 通过自带 USB 口直接枚举成一个电脑上的虚拟串口,再配合 MicroPython 的 select 做非阻塞收发,代码能写得非常干净。这个方案特别适合做上位机指令控制、调试日志输出、以及和 PC 端 Python 脚本联调,实际用下来比外接 USB 转 TTL 模块省事得多,也少了一堆接线烦恼。

这篇文章会从 USB-CDC 的原理讲起,再到固件准备、select 的使用、完整代码实现和舵机控制案例,最后把我踩过的坑都列出来。适合刚接触树莓派 Pico 和 MicroPython 的玩家,也适合想把手头传感器、舵机、LED 等外设接入 PC 统一控制的开发者。只要照着步骤走,基本半小时内就能跑通一个靠谱的虚拟串口通信链路。

1. USB-CDC 到底解决了什么问题

很多人第一次接触嵌入式通信,第一反应就是 UART 串口:TX 接 RX、RX 接 TX、共地,然后靠一个 USB 转 TTL 芯片连电脑。这套做法本身没毛病,但多了硬件转换器就多了成本、接线和驱动问题。USB-CDC 把“串口”直接塞进了 USB 协议里,设备插上电脑后,操作系统会识别成一个标准串口设备,不需要额外芯片,也不需要单独装驱动。

1.1 虚拟串口和硬件串口的区别

USB-CDC 的完整名称是 USB Communication Device Class,是一种 USB 设备类规范。它定义了设备如何通过 USB 总线模拟出传统串口的收发行为。对应用层来说,你在电脑上打开 COM 口,往里面写字节,Pico 就能通过 USB 中断收到数据;Pico 往 USB 接口写数据,电脑端串口工具就能读到。整个过程对使用者完全透明,底层 USB 协议栈由 MicroPython 固件处理。

传统 UART 则是纯硬件电平协议,CPU 通过寄存器操作发送接收,物理上必须有 RX/TX 两根线。如果电脑没有原生串口接口,就需要一颗 USB 转 UART 芯片,例如 CH340、CP2102,把 USB 包转换成 UART 电平。初中生都能看懂这个差别:一个需要额外芯片,一个直接用板载 USB 口。Pico 的 RP2040 芯片原生支持 USB 控制器,固件里跑一个 CDC 类驱动就能模拟出串口,这也是为什么它能在不接任何模块的情况下直接和电脑通信。

对比项硬件 UARTUSB-CDC 虚拟串口
物理连接TX/RX/地线,需要 USB 转 TTL 模块仅需 Pico 的 USB 口连接电脑
驱动依赖CH340/CP2102 等驱动操作系统自带 CDC ACM 驱动
通信速率可设置波特率由 USB 协议决定,无需设置波特率
调试便利性需要确认接线、共地、电平即插即用,设备管理器直接看到 COM 口
适用场景低功耗传感器、工业设备、长线通信上位机调试、固件交互、虚拟控制台

这里有一个容易误解的点:虚拟串口本质上不需要设置波特率,因为在 USB 协议里它走的是批量传输端点,不是真实的异步串口电平。电脑上的串口工具默认显示 9600 或 115200,那只是给用户一个心理安慰,实际通信速率由 USB 带宽决定,通常远高于普通 UART。所以你在 MicroPython 里写 stdin/stdout,根本不用管波特率匹配,免去了一个经典坑。

1.2 Pico 的 USB-CDC 在 MicroPython 中的默认形态

树莓派 Pico 官方 MicroPython 固件启动后,会默认把 USB CDC 抽象为标准输入输出,也就是 sys.stdin 和 sys.stdout。这意味着你既可以用 Thonny 里的 REPL 交互,也可以用串口工具向 Pico 发送数据,MicroPython 的内核会把来自 USB 的字节流交给用户程序处理。

用官方固件的时候,Pico 插上 USB 线,电脑上会多出一个串口设备,Windows 叫 COMx,Linux 一般叫 /dev/ttyACM0,macOS 叫 /dev/cu.usbmodem 开头的一串。注意它和普通 USB 转串口的 /dev/ttyUSB0 不一样,ACM 设备通常不需要额外驱动,Linux 下更稳。

MicroPython 里最直接的测试就是写两行代码:

import sys print("hello usb-cdc")

然后在 REPL 里看到输出没有任何难度。但如果要在用户程序里读电脑发来的数据,就不能只靠 print,而是要从 sys.stdin 读。这个“标准输入输出”的设计非常舒服,因为你的串口程序完全可以用文件操作的方式去读写,不需要像硬件 UART 那样先初始化 machine.UART 对象、再配置波特率。

我实际项目里最喜欢的一点是:同一根 USB 线同时承担供电、下载程序和通信三件事。以前用 Arduino 接舵机控制,除了 USB 线还得接一个 USB 转 TTL 模块,调试台上全是杜邦线。Pico 这种方式让桌面清爽很多,尤其是临时搭个测试环境,插上就跑。

2. 开发环境准备:固件、烧录、串口确认

2.1 固件版本选择与烧录流程

MicroPython 官方对 RP2040 支持已经很成熟,建议直接到官网下载最新的 Pico 固件,文件后缀是 .uf2。Pico W 和 Pico 的固件不通用,下载时注意区分。如果想用更底层的 CircuitPython 也可以,但 select 相关接口和 MicroPython 略有差异,本文所有代码都基于 MicroPython 官方固件。

烧录非常简单:

  1. 按住 Pico 板子上的 BOOTSEL 按键不放。
  2. 用 USB 线连接 Pico 和电脑。
  3. 电脑上会出现一个名为 RPI-RP2 的 U 盘。
  4. 把下载好的 .uf2 固件文件拖进 U 盘。
  5. 等待几秒,Pico 自动重启,U 盘消失,变成一个 CDC 串口设备。

整个过程只有几十秒,比往 ESP32 里烧 MicroPython 还快,因为它不需要额外的烧录工具。拖进去之后固件就写到板载 Flash 里,再次启动直接进入 MicroPython 环境。调试工具我推荐 mpremote,它是一个 Python 命令行工具,安装后可以直接操作 Pico 的文件系统和 REPL:

pip install mpremote

用 mpremote 连接和复位都很顺手:

mpremote connect /dev/ttyACM0 run test.py mpremote connect /dev/ttyACM0 reset

如果你习惯图形界面,Thonny 也能完成同样的工作,但 mpremote 更适合脚本化操作,比如写完代码一键上传、一键运行。

2.2 三系统下确认虚拟串口枚举成功

确认枚举成功的方法各系统不太一样,我列一个速查表:

操作系统查看位置典型设备名注意事项
Windows设备管理器 -> 端口(COM和LPT)COM3、COM4 等如果出现未知设备,多半是线材或固件问题
Linuxls /dev/ttyACM*/dev/ttyACM0普通用户可能需要加入 dialout 组
macOSls /dev/cu.usbmodem*/dev/cu.usbmodem1101无需额外驱动,插上即出现

Linux 下第一次使用,如果遇到 Permission denied,执行:

sudo usermod -a -G dialout $USER

然后注销重新登录,或者临时提权:

sudo chmod 666 /dev/ttyACM0

Windows 下一般不用装驱动。如果设备管理器里看到的是带感叹号的未知设备,先换一根数据线试试。很多手机充电线只有电源线没有数据线,Pico 能通电但无法枚举出串口,这是新手最容易踩的坑。我手头至少有三根“只能充电不能传数据”的 Micro USB 线,全部是买小电器送的。

枚举成功之后,可以用任意串口工具打开这个 COM 口,波特率随便填,只要打开串口,Pico 就会收到 USB 中断。实际测试过程中,我更喜欢先跑一个极简回环程序验证链路:

import sys while True: data = sys.stdin.buffer.read(1) if data: sys.stdout.buffer.write(data) sys.stdout.buffer.flush()

电脑上发什么,Pico 就回什么。能跑通这个,说明 USB-CDC 这个虚拟串口链路已经完整建立,接下来就可以正式上 select 了。

3. select 是实现非阻塞串口收发的关键

3.1 轮询、阻塞与非阻塞到底怎么选

很多初学者写串口接收,第一版通常是这样的死循环:

while True: data = sys.stdin.buffer.readline() print("got:", data)

这个写法最大的问题是 readline 会阻塞。如果电脑端只发送了一个不带换行符的字节,MicroPython 的 readline 会一直等下去,整个程序卡住,后面的舵机、LED、传感器逻辑全部停摆。即使加上超时参数,代码也会变得难维护,因为你必须在同一个循环里同时处理“等待串口数据”和“周期性任务”。

select 就是来解决这个问题的。它在 Unix 系统里已经被用了几十年,核心思想是让你同时监视多个文件描述符是否有数据可读、是否可写、是否出错,并且可以设置超时时间。MicroPython 把这一套机制精简之后放进了标准 select 模块,典型接口是 select.select 和 select.poll。

对嵌入式场景来说,select 最大的价值不是“多路同时监听”,而是“非阻塞地知道有没有数据”。有了它,主循环可以每 10 毫秒检查一次串口缓冲区,没有数据就继续干别的事,有数据才读取处理。这比定时器和中断的方案简单直观得多,尤其适合 MicroPython 这种解释型环境,因为你不需要深入底层去写回调函数。

MicroPython 的 select.poll 用法和 CPython 非常接近:

import select import sys poller = select.poll() poller.register(sys.stdin, select.POLLIN)

poll 对象维护了一个注册表,register 之后内核会在每次 poll 调用时检查这些流的状态。POLLIN 表示“有数据可读”,如果流上出现 POLLIN 事件,说明可以非阻塞地读取。MicroPython 也支持 POLLOUT、POLLERR 等事件,但日常串口场景基本只用 POLLIN。

3.2 select.select 与 poll 的差异以及超时参数的意义

标准 Python 里 select.select 接受四个参数:可读流列表、可写流列表、异常流列表、超时秒数。MicroPython 也实现了这个接口,但有些移植版对列表长度有限制。poll 在 API 上更精简,而且操作效率更高,所以我更推荐用它。

超时参数是 select 的灵魂。poll(0) 表示立即返回,不等待;poll(None) 表示无限期等待;poll(100) 表示最多等待 100 毫秒,超时后返回空列表。这个参数决定了你的主循环响应速度。

举个例子:你要做一个 50Hz 舵机控制,每 20 毫秒刷新一次占空比,同时又想实时接收上位机指令。如果用阻塞 readline,舵机刷新会被串口数据卡住。用 select.poll(5) 就可以做到每 5 毫秒扫一次串口,一帧数据到达后立刻处理,没有数据就继续跑舵机刷新逻辑。实测下来 Pico 的 CPU 占用也不高。

import select import sys import time poller = select.poll() poller.register(sys.stdin, select.POLLIN) while True: events = poller.poll(5) for event in events: if event[1] & select.POLLIN: line = sys.stdin.buffer.readline() if line: print("cmd:", line.strip()) # 这里可以继续执行其他周期任务 time.sleep_ms(5)

这里有一个隐藏细节:select 返回“有数据可读”并不代表能读到一整行。如果电脑端发来的是“abc”三个字节,没有换行符,readline 会因为等不到 \n 而阻塞,哪怕刚才 select 已经报告了 POLLIN。解决办法有两个:一是上位机协议规定每条指令必须带 \r\n 结束符;二是不要用 readline,改用 read 或 readinto 自己拼缓冲。后面我会专门写一个解决粘包的方法。

4. 实战拆解:虚拟串口加 select 实现指令控制

4.1 最小 CDC 收发程序

先把一个最小的、可靠的 USB-CDC 收发框架搭出来。这个程序我几乎所有 Pico 串口项目都会复用,代码量不大,但结构很稳:

import select import sys poller = select.poll() poller.register(sys.stdin, select.POLLIN) def read_available(): lines = [] while True: events = poller.poll(0) if not events: break for event in events: if event[1] & select.POLLIN: chunk = sys.stdin.buffer.read(64) if not chunk: return lines lines.append(chunk) return lines while True: data = read_available() for line in data: try: text = line.decode().strip() except UnicodeDecodeError: continue if text: print("Echo:", text) sys.stdout.buffer.flush()

这里我用 poll(0) 做即时扫描,把所有缓冲区内能读到的数据都读出来,避免数据留在底层。sys.stdin.buffer.read(64) 最多读 64 字节,不会因为等待指定长度而阻塞,因为底层没有数据时它返回空字节串。read_available 这个函数把“是否有数据”和“读取数据”解耦,主循环里可以放心调用。

在 Pico 官方固件上,这个框架跑起来非常稳定。电脑端用 Python 的 pySerial 发送测试:

import serial import time ser = serial.Serial("COM3", 115200, timeout=0.1) ser.write(b"hello pico\r\n") time.sleep(0.2) print(ser.read_all())

注意 PySerial 里虽然写波特率,但 USB-CDC 下实际不生效,只是占位。打开串口后发送一行带 \r\n 的文本,Pico 的 select 会立刻感知到,然后回显一行。跑通这一步,你的虚拟串口链路就完全打通了。

4.2 用文本指令控制板载 LED 与舵机

串口本身只是通道,真正价值在于远程控制外设。我拿目前很火的“树莓派 Pico 控制舵机”场景做一个扩展:通过上位机发送文本指令,Pico 解析后控制板载 LED 和舵机转动。

先定义一份简单的协议:

指令含义示例
LEDON点亮板载 LEDLEDON
LEDOFF熄灭板载 LEDLEDOFF
SERVO angle控制舵机转到指定角度,0-180SERVO 90
PING测试链路连通性PING

舵机接线和 PWM 参数很重要。最常见的 SG90 舵机有三根线:棕色地线、红色电源线、橙色信号线。Pico 的 PWM 输出能力有限,最好给舵机外接 5V 电源,Pico 和舵机共地。信号线接 Pico 的 GPIO15,我用的是第 15 脚,可以自定义。

SG90 的控制信号是 50Hz 的 PWM,周期 20 毫秒,脉宽 0.5 毫秒到 2.5 毫秒对应 0 度到 180 度。Pico 的 PWM 频率用 50Hz,占空比范围 2.5%~12.5%,换算成 16 位的 duty 值大约是 1638 到 8191。为了精确,我用 MicroPython 的 machine.PWM 实现:

from machine import Pin, PWM servo = PWM(Pin(15, Pin.OUT)) servo.freq(50) def set_servo(angle): angle = max(0, min(180, angle)) duty = 1638 + int(angle / 180 * (8191 - 1638)) servo.duty_u16(duty)

完整程序结合 select 和指令解析:

import select import sys from machine import Pin, PWM poller = select.poll() poller.register(sys.stdin, select.POLLIN) led = Pin("LED", Pin.OUT) led.off() servo = PWM(Pin(15, Pin.OUT)) servo.freq(50) def set_servo(angle): angle = max(0, min(180, angle)) duty = 1638 + int(angle / 180 * (8191 - 1638)) servo.duty_u16(duty) def handle_cmd(text): if text == "LEDON": led.on() return "LED ON" elif text == "LEDOFF": led.off() return "LED OFF" elif text == "PING": return "PONG" elif text.startswith("SERVO"): parts = text.split() if len(parts) == 2: try: angle = int(parts[1]) set_servo(angle) return "SERVO OK " + str(angle) except ValueError: return "ERROR ANGLE" return "ERROR CMD" while True: events = poller.poll(10) if events: chunk = sys.stdin.buffer.read(64) if chunk: for line in chunk.split(b"\r\n"): if line.strip(): text = line.decode().strip() resp = handle_cmd(text) sys.stdout.buffer.write((resp + "\r\n").encode()) sys.stdout.buffer.flush()

注意我直接在 chunk 上按 \r\n 切分,而不是用 readline。这样可以避免 select 报告可读后 readline 等不到换行而阻塞的问题。上位机每次发送指令都带上 \r\n,Pico 就算一次收到多条指令也能逐条处理。实测用串口助手连续发送一百条 SERVO 指令,动作响应都很顺滑,没有出现舵机抽搐或卡死的情况。

4.3 PC 端 Python 脚本联动测试

光有 Pico 端还不够,PC 端也得有个趁手的测试脚本。pySerial 是最常用的库,写一个简单的控制台程序,可以交互式输入指令:

import serial ser = serial.Serial("COM3", 115200, timeout=0.2) def send_cmd(cmd): ser.write((cmd + "\r\n").encode()) result = ser.read_until(b"\r\n") return result.decode().strip() if __name__ == "__main__": while True: cmd = input("cmd> ").strip() if cmd in ("exit", "quit"): break if cmd: print(send_cmd(cmd))

这个脚本会把用户输入的行发送到 Pico,然后阻塞读取 Pico 的响应。select 的作用在 PC 端也存在:你可以用 select.select 同时监视串口和键盘输入,实现“不下电也能退出”的交互。我写过一版:

import serial import sys import select import time ser = serial.Serial("COM3", 115200, timeout=0) while True: rlist, _, _ = select.select([ser, sys.stdin], [], [], 0.1) if ser in rlist: data = ser.read(ser.in_waiting or 1) if data: sys.stdout.write(data.decode(errors="ignore")) if sys.stdin in rlist: line = sys.stdin.readline().strip() if line in ("exit", "quit"): break ser.write((line + "\r\n").encode())

这样 PC 端也能非阻塞地同时处理终端输入和串口回包,调试体验和 Pico 端保持一致。select 是跨平台的,Python 桌面端和 MicroPython 端的用法高度统一,学一次两边都能用。

5. 踩坑记录与排查手册

5.1 USB 枚举与设备名不稳定的坑

USB-CDC 最大的痛点不是通信,而是设备名在不同系统、不同 USB 口下会变化。Windows 下同一个 Pico 插不同的 USB 口,可能从 COM3 变成 COM5;Linux 下重启后 /dev/ttyACM0 可能被其他设备占掉。

Linux 下可以用符号链接解决:

ls /dev/serial/by-id/

然后通过固定 ID 访问:

ls -l /dev/serial/by-id/usb-Raspberry_Pi_Pico_*

Windows 下可以在设备管理器里手动把串口号固定,或者用注册表方式给 VID/PID 分配固定 COM 号。RP2040 的 VID 是 0x2E8A,PID 是 0x0005,USB CDC 模式下是 0x000A。搞清楚了这几个 ID,写自动化脚本时就不会因为 COM 口跳变而抓狂。

另一个坑是线材问题。我前面提过充电线只供电不通信,这几乎是 USB CDC 通信失败的第一大原因。检测方法很简单:在设备管理器里看枚举状态,如果只有 USB 大容量存储设备 RPI-RP2 而没有 COM 口,那多半是固件没烧对或者线材数据线断开。

如果设备管理器里连 RPI-RP2 都没出现,可以先按住 BOOTSEL 重新上电。此时出现 U 盘说明硬件没问题,拖入固件后出现 COM 口说明 USB-CDC 枚举成功。整个过程按顺序排查,基本十分钟内能定位到问题。

5.2 select 和 stdin 的兼容性细节

MicroPython 不同移植版对 select 的支持程度不完全一样。官方 Pico 固件上,sys.stdin.buffer 可以被 poll 注册,实测没有任何问题。但某些精简固件或者使用自定义 main.py 重定向了标准输入输出之后,poll 可能注册失败,抛出 OSError。遇到这种情况,先确认固件版本,再检查代码里是否调用了 os.dupterm 之类重定向函数。

还有一个常见坑:print 和 sys.stdout.buffer.write 混用导致输出错乱。print 会走 stdout 的文本层,可能带上额外的缓冲处理,而 buffer.write 是直接写字节。如果两者混用,顺序可能不固定,尤其是缓冲区刷新时机不一致的情况下。我的经验是:要么全用 print,要么全用 buffer.write,别在同一个程序里混着来。尤其是要返回指令响应时,统一用:

sys.stdout.buffer.write(b"OK\r\n") sys.stdout.buffer.flush()

flush 是必须的。MicroPython 的 stdout 在 USB CDC 上可能有缓冲,不 flush 的话,PC 端可能很久才能收到数据,甚至数据滞留在缓冲区里直到下一次写入才批量发出。

如果程序里需要同时跑多个 main 流程,而 select 只监视串口,可以考虑 MicroPython 的 _thread 模块,开一个独立线程跑串口接收,主线程跑业务逻辑。但 MicroPython 的线程是软线程,GIL 仍然存在,遇到 CPU 密集型计算时串口响应会延迟。我一般不用线程,而是把业务逻辑拆成状态机放进主循环,select 轮询驱动,这样最简单也最稳定。

5.3 数据粘包和 readline 阻塞的终极解法

USB-CDC 数据到达时间不确定,很可能一次 select 返回时缓冲区里堆积了多帧指令,或者一条指令被拆成两个包。这就是粘包与半包问题。readline 只能处理“带换行符的完整行”,如果数据被拆包,readline 会一直等待,导致程序卡死。

我推荐的解法是“按块读取 + 自定义缓冲 + 按换行切分”。思路很简单:每次 select 报告可读,就 read 一段字节,追加到一个全局缓冲区里,然后检查缓冲区中是否有 \r\n。有就根据换行符把完整指令切出来,余下字节留在缓冲区等待下一次。代码逻辑清晰,不依赖操作系统对行的处理。

import select import sys poller = select.poll() poller.register(sys.stdin, select.POLLIN) rx_buf = bytearray() def process_line(line): text = line.decode(errors="ignore").strip() if not text: return # 业务处理 print("echo:", text) sys.stdout.buffer.flush() while True: events = poller.poll(10) if events: chunk = sys.stdin.buffer.read(64) if chunk: rx_buf.extend(chunk) while True: idx = rx_buf.find(b"\r\n") if idx == -1: # 尝试单 \n idx = rx_buf.find(b"\n") if idx == -1: break line = bytes(rx_buf[:idx]) del rx_buf[:idx+1] if line or (idx == 0): process_line(line)

这个模式把 select、粘包、半包问题一次解决。实测用 USB 串口发送一包包含 20 条指令的数据,Pico 逐条解析执行,顺序完全正确。如果指令之间只有 \n 没有 \r,find 的逻辑也能兼容,不会漏数据。

缓冲区建议不要无限增长。如果上位机发送了一大坨没有换行符的数据,rx_buf 会越来越大,占用内存。可以加一个上限,超过 1024 字节就清空并记录错误,防止异常上位机程序把 Pico 内存耗尽。这也是嵌入式开发的常识:任何外部输入都要有边界保护。

5.4 调试过程中的其他经验

实际跑舵机控制时,我发现舵机电源和 Pico 共地特别重要。如果舵机电源和 Pico USB 地没有连到一起,信号线会产生参考地漂移,舵机可能抖动甚至乱转。解决办法很简单,把舵机电源的 GND 和 Pico 的 GND 接在一起。

Pico 板载 LED 可以用来做状态指示,比如串口收到合法指令时闪一下,收到非法指令时闪两下,这让现场排错变得非常快。我习惯在 main.py 里加一个简单的 heartbeat 灯,每隔一秒翻转一次,确认程序没有陷入死循环。select 方案下,如果主循环因为某个阻塞调用卡住,heartbeat 会立刻停止,问题定位就快了。

如果你用的是 Pico W,注意板载 LED 在无线模块控制下,和 Pico 经典版的 GPIO25 LED 略有不同。用 machine.Pin("LED") 这种写法可以同时兼容 Pico 和 Pico W,不需要关心具体 GPIO 号。舵机最好接在 GPIO0 到 GPIO15 这些支持 PWM 的引脚上,MicroPython 官方固件大部分引脚都支持 PWM,但不同引脚输出的通道会有复用限制,同一时刻如果多个 PWM 共用同一个 slice,频率必须一致,否则改变一个通道的频率会影响另一个。我实测舵机接 GPIO15 没有和其他 PWM 冲突,如果你想接多路舵机,最好查一下 RP2040 数据手册,把舵机分配到不同 PWM slice 的引脚上。

写在最后的小建议

USB-CDC 加 select 这套组合,我后来在好几个项目里复用,甚至做了一个简易的上位机调参工具,通过 PC 端 Python 脚本实时修改控制参数,不用重新烧录固件。调试效率提升非常明显,省掉了反复插拔线材的麻烦。如果你第一次在 Pico 上跑串口通信,建议先照着最小收发框架跑通,再逐步扩展 LED 和舵机控制,不要一上来就写复杂的协议栈。等把 select 的轮询节奏把握好了,你会发现在 MicroPython 里处理串口通信真的是一件很轻松的事。

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

嵌入式Linux下Modbus RTU传感器采集全攻略

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

作者头像 李华
网站建设 2026/9/11 8:31:13

GEO/AEO时代重写Schema Markup:WordPress与Shopify实战指南

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

作者头像 李华
网站建设 2026/9/11 8:29:32

大文件断点续传上传插件实战:切片、哈希与Web Worker

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

作者头像 李华