☰
WSL2下STM32开发板USB映射与烧录实战:usbipd-win+OpenOCD方案
2026/10/16 4:12:22 网站建设 项目流程

1. 先说结论:WSL2 里看不到板子,不是板子坏了

这个系列写到第七篇,编译链路、CMake 构建在 WSL 里都已经跑顺了,真正让我对着屏幕喊出“我板子呢?”的,是烧录这一步——WSL 里敲lsusb,列出来的只有虚拟的 USB 控制器,STM32 开发板的调试器明明插在电脑上,却像消失了一样。

先给结论:在 WSL2 里看不到板子,绝大多数时候不是板子坏了,也不是调试器坏了,而是 WSL2 根本没有把宿主机上的 USB 设备“转交”给 Linux 环境。

很多嵌入式开发者跟我一开始的思路一样:Windows 下装个 STM32CubeProgrammer,WSL 里只负责编译,烧录再切回 Windows 双击一下。这样确实能干活,但每次编译完都要切换系统、打开 GUI、选文件、点烧录,非常割裂。既然前面已经把工具链全部 Linux 化了,烧录这关也应该在 WSL 里打通。这篇文章就是我踩完坑之后沉淀出的完整方案,适合所有不想装双系统、想用 WSL 完成 STM32“编辑-编译-烧录-调试”闭环的人。

1.1 WSL1 和 WSL2 在“USB 视图”上的本质差异

先解释清楚为什么会出现这种“看不见”的情况,这关系到后面所有排错思路。

WSL1 是一个系统调用翻译层,它没有自己的内核,而是把 Linux 的系统调用翻译成 Windows NT 内核调用。在这种情况下,设备模型在很大程度上沿用了 Windows 的驱动框架,USB 设备挂载后,WSL1 里访问/dev/ttyUSB0这类节点是通过 Windows 驱动间接实现的,所以反而能比较“自然”地碰到宿主机设备。

WSL2 就不一样。WSL2 是标准的轻量虚拟机,运行在 Hyper-V 虚拟化平台上,它有自己的 Linux 内核、自己的驱动栈、自己的/dev设备树。你插在 Windows 上的 USB 设备,物理上连接在宿主机,虚拟机里的内核不会自动看到它。这就像合租房里住在独立卧室,客厅插座上插着充电器,卧室里的你不可能直接摸到那个充电头,除非有人拉一根延长线从门缝接进来。

搞清楚这一点,后面所有操作就都有逻辑了:我们要做的,就是把客厅插座上的设备,通过一根“延长线”接到卧室里。

1.2 usbip 这套“USB 延长线”是怎么工作的

这根“延长线”的标准名字叫 USB/IP,Linux 内核自带的协议。思路不算复杂:宿主机操作系统把 USB 设备抽象成一个“可以被网络访问的资源”,虚拟机里的操作系统通过内核模块把这个远程资源挂载成自己的 USB 设备。对 WSL2 里的程序来说,它看到的就是一个本地 USB 设备,读写、控制都能走标准 Linux 驱动。

Windows 侧对应的服务端实现,是一个叫 usbipd-win 的开源工具。它是 Windows 端的“服务器”,负责把你的 ST-Link、USB 转串口、J-Link 这些设备通过 USB/IP 协议暴露出来;WSL2 端则作为一个“客户端”,把这个远程设备 attach(附加)进来。两者之间通过 WSL2 的虚拟网络通信,所以你会发现 attach 时需要知道 Windows 的 IP 或者使用 usbipd 提供的封装命令。

用生活类比再解释一遍:Windows 是仓库管理员,usbipd-win 是仓库物流系统,WSL2 是远在另一个城市的工厂。你给物流系统发指令“把这盒芯片发过去”,仓库那边打包、装车,工厂收到货拆包后,生产线上的工人觉得芯片就在本地仓库里。整个过程对上层应用完全透明。

这也就是为什么我们不能在 WSL2 里直接打开 Windows 的某个 USB 设备节点,而必须先 bind(绑定放行),再 attach(挂载进来)。

1.3 先用三个命令确认踩的是不是这个坑

在自己动手绑定之前,建议先花一分钟确认问题方向确实是对的,免得白折腾。

第一步,打开 Windows PowerShell(普通权限就行),插入你的 ST-Link 或者开发板自带的调试器,然后输入:

usbipd list

注意,这一步需要你已经在 Windows 上安装过 usbipd-win 工具。如果还没装,下一节会讲。这条命令会列出宿主机上所有 USB 设备以及它们的绑定状态。如果这里看不到你的板子,说明问题更基础——可能是驱动没装好、USB 线有问题、板子没上电,这跟 WSL 没关系,先去设备管理器看有没有未知设备。

第二步,回到 WSL2 终端,输入:

lsusb

正常情况下,如果从来没有 attach 过,这里只能看到一些虚拟设备,比如1d6b:0002 Linux Foundation 2.0 root hub,找不到任何0483开头的设备。0483是 ST-Link 调试器的 USB Vendor ID,后面跟着的是 Product ID,比如374b。

第三步,看内核日志:

dmesg | tail -20

如果完全没有任何 USB 插入事件,说明设备确实没进来。如果你看到usb 1-2: new high-speed USB device number 3 using vhci_hcd一类的输出,说明 USB/IP 已经生效,只是设备列表显示方式和你预期的不一样。

这三步做完,基本就能确定是否需要在 WSL 里做 USB 设备共享。我第一回踩坑时就是直接跳到最后一步openocd烧录,报错“找不到设备”,绕了好大一圈才发现是压根没 attach。

2. 从“看不到”到“看得到”:usbipd-win 绑定实操与报错排查

确认问题方向之后,接下来的任务就很明确:在 Windows 侧绑定设备,把它 attach 进 WSL2。这节我把从安装到验证的完整流程写清楚,每个步骤都附带我实际执行时的输出和踩坑记录。

2.1 装好 usbipd-win,并确认 WSL2 内核支持

Windows 侧需要安装 usbipd-win。最省事的方式是用 winget 直接装:

winget install usbipd-win

装完后,需要开一个新的“管理员权限”的 PowerShell 窗口,普通权限终端在执行 bind 和 attach 时会提示权限不足。安装过程中如果提示需要安装 USB/IP 驱动,选择同意即可。

接下来确认 WSL2 内核版本足够新。老版本内核可能缺少 USB/IP 相关的内核模块和用户态工具。建议直接更新到最新:

wsl --update

更新完重新进入 WSL,检查内核版本:

uname -r

我当前的环境是 5.15 之后的内核,USB/IP 的 vhci-hcd 驱动已经默认编译进去。如果你发现版本比较旧,先更新再说。另外提醒一句:WSL2 发行版本身也要保证是用 WSL2 模式跑的,不是 WSL1,可以用wsl -l -v查看。如果某个发行版还停在 WSL1,整个 USB/IP 方案就不适用,需要手动转换版本。

2.2 标准三步:list、bind、attach

正常流程可以压缩成三步,熟练之后十秒钟就能完成一次 attach。

先在管理员 PowerShell 里查看设备:

usbipd list

输出大概长这样:

BUSID VID:PID DEVICE STATE 4-2 0483:374b ST-LINK/V2 Not attached

第一列 BUSID 是设备在 USB 总线上的编号,第四列 STATE 表示当前是否被附加到了某个虚拟机。如果 STATE 显示Attached,说明已经被 WSL 拿走了,再看lsusb应该能发现设备。

首次使用某个设备,需要先 bind 一次。bind 的含义是把这个设备标记为“允许被附加”,相当于在物流系统里给货物办了出库手续:

usbipd bind --busid=4-2

这一步只需要做一次,设备会保持“已绑定”状态。但注意,绑定的只是“允许”关系,真正把它接入当前运行的 WSL2 实例,还需要 attach:

usbipd attach --wsl --busid=4-2

在较新版本的 usbipd-win 中,--wsl参数会自动完成 WSL 侧的挂载动作,不需要你手动在 WSL 里敲usbip attach。如果这条命令执行后没有任何报错,回到 WSL 终端验证:

lsusb

这时候应该能看到类似这样的行:

Bus 001 Device 003: ID 0483:374b ST-LINK/V2

看到这行字,恭喜,板子“进来”了。

值得说明的是,如果你用的 usbipd-win 版本较老,或者你想手动走传统 usbip 流程,也可以在 WSL 侧加载内核模块:

sudo modprobe vhci-hcd sudo usbip attach -r <Windows的IP> -b 4-2

这种方式需要先安装 usbip 用户态工具,比较折腾。建议直接用usbipd attach --wsl的封装命令,减少变量。

2.3 一张表讲完我这几天遇到的报错

这几天的实操里,我把能踩的报错基本踩了个遍,整理成一张表,方便你遇到问题时直接对号入座。

报错或异常现象根因解决方案
usbipd: error: 未找到设备或 list 里没有目标设备设备没插好、驱动未安装、USB 口供电不足换口换线,查看 Windows 设备管理器确认驱动正常
usbipd: error: 设备未绑定bind 步骤被跳过先执行usbipd bind --busid=4-2
attach 成功但 WSL 里 lsusb 仍看不到内核版本过旧,或用了 WSL1执行wsl --update,并确认发行版是 WSL2
usbip: error: open /dev/devicesvhci-hcd 内核模块未加载执行sudo modprobe vhci-hcd
attach 报 device busy / 设备被占用Windows 下某个程序正在独占该 USB 设备关闭 STM32CubeProgrammer、串口助手等程序,重新 attach
attach 后设备反复掉线USB 线质量差、前置 USB 口供电不稳换线,或插到主机背面原生 USB 口

我最常撞上的,就是 Windows 侧有程序偷偷占着设备。比如之前用 STM32CubeProgrammer 烧过录,程序虽然关了一半,但后台的调试服务进程还没退出,这时 attach 就会失败。解决方式也很朴素:把相关软件全部退出,或者在任务管理器里检查有没有残留进程,干净了再 attach。

2.4 复合设备与 Windows 程序占用的隐藏坑

这里要单独说一个很多人忽略的细节:ST-Link 这类调试器往往是一个复合 USB 设备。它不只有一个功能,通常同时包含调试接口(SWD/JTAG)和虚拟串口。在 Windows 里,你会看到它占用多个 COM 口;在 usbipd list 里,它显示成一行,但底层其实是个包含多个 Interface 的组合体。

这意味着当你 attach 整个设备之后,不仅仅是烧录器进入了 WSL,它的虚拟串口也会跟着进来。这本来是好事,因为后面我们还要用串口看 printf 输出。但副作用是,一旦 attach,Windows 这边的串口号会立即消失。如果你在 Windows 侧开着串口监视软件,那边会瞬间丢数据,甚至导致 attach 报错。

所以实操中要养成一个习惯:attach 之前,先确认 Windows 侧没有在占用这个设备。反过来也一样,如果你拔掉板子回到 Windows 使用,先确认 WSL 里没有程序还握着串口,否则 Windows 侧也可能抢不到设备。

还有一个观察:某些开发板会把 ST-Link 和板载虚拟串口做成同一个 USB 设备,你 attach 时会看到设备列表里多出两个 CDC 串口节点。这是正常现象,别以为是自己操作错了。

3. 烧录这步棋:OpenOCD、st-flash 与 Linux 权限的最后一公里

设备成功进入 WSL,只是万里长征走了一半。烧录这件事本身在 Linux 下还有一层绕不开的权限问题,以及工具链选型问题。我最后敲定的方案是 OpenOCD 为主、st-flash 为辅,这节讲清楚为什么这么选,以及实际烧录的一整套命令。

3.1 为什么在线烧录选 OpenOCD 而不是 GUI

很多从 Windows 开始接触 STM32 的开发者,习惯了 STM32CubeProgrammer 的图形界面,点几个按钮就能烧录。但在这个系列里,前面几篇已经把工程构建完全改为 CMake + GCC 命令行流程,烧录环节如果再依赖 GUI,整个自动化闭环就断了。

OpenOCD 是目前嵌入式调试烧录场景里最“标准”的开源工具。它支持 ST-Link、J-Link、CMSIS-DAP 等多种调试器,芯片支持列表里也能找到覆盖全系列的 STM32 配置。更重要的是,它天然就是命令行驱动的,可以很自然地和make flash、ninja flash这类构建目标结合,写完代码一条命令编译加烧录,不用切窗口。

st-flash 是更轻量的选择,来自 stlink 开源项目,专门针对 ST-Link 调试器,烧写 bin 文件非常快,但功能相对简单。OpenOCD 则更像瑞士军刀,除了烧录还能做调试、RTOS 感知、Flash 保护等高级操作。我的建议是两者都装,日常快速烧 bin 用 st-flash,需要精细控制或者挂 GDB 调试时用 OpenOCD。

三款工具的对比也列出来供参考:

工具配置复杂度命令行友好度适用场景WSL 兼容性
OpenOCD中高,支持脚本化调试 + 烧录 + 自动化好
st-flash低很高快速烧录 bin好
STM32CubeProgrammer CLI较高中等,重量级官方生态,支持加密和选项字节可用但繁琐

3.2 最小可用的烧录命令与参数拆解

安装工具在 Ubuntu 系的 WSL 发行版里很简单:

sudo apt update sudo apt install -y openocd stlink-tools

装完后确认版本:

openocd --version st-info --version

如果你的 WSL 发行版软件源里版本太旧,也可以从源码编译最新版,但对绝大多数 F1/F4/G0 系列芯片来说,发行版自带的版本足够稳定,没必要折腾。

真正烧录的时候,我用 OpenOCD 的命令是这样:

openocd \ -f interface/stlink.cfg \ -f target/stm32f1x.cfg \ -c "program build/app.elf verify reset exit"

逐个参数拆开看:

-f interface/stlink.cfg告诉 OpenOCD,你手上用的调试器是 ST-Link。如果你用的是 J-Link,这里换成interface/jlink.cfg。如果你是某第三方开发板自带的 CMSIS-DAP 调试器,换成interface/cmsis-dap.cfg。

-f target/stm32f1x.cfg是目标芯片配置。F1 系列用这个,F4 系列换成stm32f4x.cfg,G0 系列用stm32g0x.cfg,配置文件命名基本和芯片系列一一对应。

-c "program build/app.elf verify reset exit"是 OpenOCD 启动后要执行的 TCL 命令序列。program表示烧录,后面跟 ELF 文件路径;verify烧完马上回读校验;reset复位芯片让程序跑起来;exit完成后退出 OpenOCD。这里我强烈建议保留verify,烧录失败但校验通过能拦住我至少三次低级问题。

如果只想快速烧个 bin,用 st-flash 更直接:

st-flash write build/app.bin 0x08000000

0x08000000是 STM32 Flash 的起始地址,bin 文件没有地址信息,必须手动指定;ELF 文件自带地址段,所以 OpenOCD 的program命令不需要你手动指定地址。

3.3 WSL 下的 USB 权限:udev 规则与 systemd 的关系

设备 attach 进来、命令看起来也没问题,但 OpenOCD 一跑就报错,最常见的是这两句:

Error: open failed libusb error: LIBUSB_ERROR_ACCESS

这就是 Linux 的经典权限问题:USB 设备节点默认属于 root,当前普通用户没有读写权限。在原生 Linux 开发板或者桌面机上,标准做法是写 udev 规则,把设备节点权限放开或分配给指定用户组。WSL2 里思路一样,但有一个前提要注意。

先看你的 WSL2 是否启用了 systemd。新版 WSL 可以像正常 Linux 那样用 systemd 管理服务,在/etc/wsl.conf里加一行配置即可:

[boot] systemd=true

改完在 PowerShell 里执行wsl --shutdown再重新进 WSL,然后检查:

ps -p 1 -o comm=

如果输出systemd,说明 udev 规则可以正常工作。如果输出还是别的进程,说明 systemd 没启用,这时 udevadm 可能不可用,规则写了也不生效。

在 systemd 可用的前提下,创建规则文件:

sudo vim /etc/udev/rules.d/99-stlink.rules

内容这样写:

SUBSYSTEM=="usb", ATTR{idVendor}=="0483", ATTR{idProduct}=="374b", MODE="0666", GROUP="plugdev" SUBSYSTEM=="usb", ATTR{idVendor}=="0483", ATTR{idProduct}=="3748", MODE="0666", GROUP="plugdev"

然后重载规则:

sudo udevadm control --reload-rules sudo udevadm trigger

我实际使用中发现,WSL2 对热插拔设备的规则重载偶尔不那么灵敏,最省心的做法是加完规则后直接把板子拔掉重插,再重新 attach 一次。另外也可以把当前用户加进plugdev组:

sudo usermod -aG plugdev $USER

改完用户组需要重开 WSL 才生效。如果你实在不想折腾 udev,临时方案是直接 chmod 对应设备节点,但 USB 设备节点路径每次插拔都可能变,治标不治本,不推荐长期用。

3.4 烧录过程常见报错与验证手段

第一次烧录成功之前,报错几乎是必经之路。我整理几个高频错误:

Error: couldn't set the SCK frequency,通常是 SWD 接线接触不良,或者线太长,降低时钟频率试一下。很多 ST-Link 板子默认 4 线 SWD:SWCLK、SWDIO、GND、3V3,少一根或者接触不好都会报告警。

Error: target not halted,一般是芯片处于低功耗模式,或者供电不稳,检查复位引脚和 BOOT0 引脚状态。如果之前烧过让芯片进入 stop 模式的程序,可以先按住复位键再烧录。

Error: unable to find a matching interface,基本就是 interface 配置文件选错,比如明明是 ST-Link 却用了 jlink.cfg。

烧录成功的标志很明确,OpenOCD 输出里会有:

** Programming Started ** ** Programming Finished ** ** Verify Started ** ** Verified OK ** ** Resetting Target **

st-flash 烧录结束时也会打印Flash written and verified之类的信息。看到这个,程序已经在板子上跑起来了,可以顺手观察板载 LED 或者串口输出验证一下。

这一刻的满足感还是很真实的。从编译链接全靠 WSL,到烧录也终于不用切回 Windows,整个开发闭环算是真正打通了。

4. 拔了又插怎么办:热插拔、自动绑定与串口观看 printf

烧录打通了,但如果只用一次,随时都要手动敲命令,那这套方案还是不够顺手。实际项目开发中,板子会不停地插拔,程序会反复烧录,串口数据也要随时看。这节的几个经验,能让“板子呢”变成“板子一直都在”。

4.1 attach 关系的“有效期”和正确拔插顺序

首先要明确一个现实:attach 不是永久绑定的。WSL2 实例重启后,之前的 attach 关系不会自动恢复;板子拔掉再插上,WSL 里同样不再持有设备。这不是 usbipd-win 的缺陷,而是 USB 协议本身的特性——USB 设备在物理断开后就等于丢失,重新枚举必须重新挂载。

正确的热插拔顺序很重要。我的习惯是:

  1. 先物理插入板子,等 Windows 环境识别完成。
  2. 在 PowerShell 里执行usbipd list,确认设备出现了。
  3. 再执行usbipd attach --wsl --busid=4-2,把设备交给 WSL。

反过来,从 WSL 切回 Windows 使用前,先确认 WSL 里没有程序占用设备,然后执行 detach:

usbipd detach --busid=4-2

如果不 detach 直接关闭 WSL,大多数情况下设备状态会变成Not attached,但偶尔会留在Detached或者Attached的悬挂状态,下次 attach 会报错。遇到这种状态,可以先usbipd detach再重新 attach,一般能解决。

另一个经验是:不要在一个终端里长时间握着设备。比如你开了 minicom 看串口,又想在 Windows 那边用官方工具读芯片信息,两边会抢设备,表现为一边正常一边频繁报错。嵌入式调试是单资源的活,谁占用得提前想清楚。

4.2 做一个一键 attach + 烧录的小脚本

手动流程熟练之后,我建议把它脚本化。最实用的是两个脚本配合,一个在 Windows 侧负责 attach,一个在 WSL 侧负责烧录。

Windows 侧 PowerShell 脚本,可以写成一个attach.ps1:

$stlink = usbipd list | Select-String "0483" | Select-Object -First 1 if ($stlink -match '^\S+\s+(\S+)\s+') { $busid = $matches[1] usbipd bind --busid $busid usbipd attach --wsl --busid $busid Write-Host "Attached $busid" } else { Write-Host "没找到 ST-Link,先插板子" }

这个脚本的核心思路是从usbipd list输出里过滤出包含0483的那一行,提取 BUSID,然后自动完成 bind 和 attach。注意它的局限:如果你同时插了两块 ST-Link,脚本只会选取第一块。更严谨的做法是在脚本外显式传入 BUSID,或者通过 VID:PID 精确匹配目标设备。

WSL 侧可以写一个flash.sh,把“检查设备是否在”和“烧录”合并:

#!/bin/bash set -e if ! lsusb | grep -q "0483:374b"; then echo "板子不在 WSL 里,请先在 Windows 管理员 PowerShell 执行 attach 脚本" lsusb exit 1 fi openocd \ -f interface/stlink.cfg \ -f target/stm32f1x.cfg \ -c "program build/app.elf verify reset exit"

把它放进工程目录,配合 CMake 的add_custom_target,就能做到make flash一条命令自动检查设备并烧录。我实际使用下来,唯一需要记住的就是“如果报找不到板子,先去 Windows 侧跑一遍 attach.ps1”。

这里有个小细节:lsusb | grep "0483"判断的是 USB Vendor ID,如果设备 attach 进来了但处于异常状态,可能 ID 一样但烧录失败。脚本只是做个快速前置检查,真正的校验还要靠 OpenOCD 的 verify 机制。

4.3 串口监视是另一块雷区

烧录完成只是第一步,程序跑起来之后,最常做的事情是看串口输出。很多基于 C++ 的嵌入式工程都会用 printf 重定向到串口,在 WSL 里怎么看数据?先回答一个高频问题:在 WSL2 里,串口号不是 Windows 的 COM3 或 COM5,而是 Linux 风格的/dev/ttyACM0或/dev/ttyUSB0。

ST-Link 的虚拟串口通常以/dev/ttyACM0出现,USB 转串口模块(比如 CP2102、CH340)通常以/dev/ttyUSB0出现。前提是这些设备已经通过 USB/IP attach 进来了。

看串口数据我推荐 minicom,足够轻量:

sudo apt install -y minicom minicom -D /dev/ttyACM0 -b 115200

-b 115200是波特率,一定要和工程的 USART 配置保持一致,否则全是乱码。也可以用 picocom,参数更简洁:

picocom /dev/ttyACM0 -b 115200

如果提示权限不足,除了前面说的 udev 规则,还可以把当前用户加进dialout组:

sudo usermod -aG dialout $USER

这同样需要重开 WSL 生效。顺便多说一句:minicom 退出时按Ctrl+A再按X,很多人卡在这。picocom 退出是Ctrl+A再按Ctrl+X。

串口数据观感上容易遇到两种坑。一种是 reset 刷屏,因为 ST-Link 的虚拟串口跟随目标芯片的复位状态,芯片一复位,串口设备会重新枚举,minicom 会断开。这不是配置问题,把板子上的复位引脚接好,或者程序里不要反复复位即可。另一种是设备节点在拔插后从 ttyACM0 变成 ttyACM1,这是因为 Linux 内核枚举顺序变了,用ls /dev/ttyACM*看一下再打开就行。

4.4 把“我板子呢”变成六条排查口诀

整个流程跑顺之后,我再也没在群里喊过“我板子呢”,因为每次出现问题,我都会按一套固定顺序排查,两分钟内定位。这里把最终的排查口诀分享出来:

  1. Windows 侧先问一句:usbipd list,看板子在不在。不在就去查 USB 线和驱动。
  2. 确认已 bind:设备行有没有被绑定,没有就usbipd bind --busid=4-2。
  3. 执行 attach:usbipd attach --wsl --busid=4-2,看有没有报 busy。
  4. WSL 侧验证 lsusb:能看到0483开头就说明设备进来了。
  5. 看权限:OpenOCD 报LIBUSB_ERROR_ACCESS就查 udev 规则和用户组。
  6. 再烧录:make flash或 OpenOCD 命令,注意看 verify 是否通过。

这个顺序从底层到上层,每一步的失败都能直接指向前一步的原因。我自己 90% 的问题出在第 3 步——不是忘了 attach,就是 Windows 某个程序偷偷占着设备。

最后再分享一个小体会:这套方案跑顺之后,我在 WSL 里烧录的时间跟以前在 Windows 里几乎没差别,多出来的几秒钟主要是等 attach。区别最大的是心态——以前编译完要切系统,心里总有一条无形的边界;现在一条终端搞定所有事,C++ 工程的全链路自动化才算真正闭环。下一期如果继续写,我会接着聊聊怎么把 GDB 调试也搬进 WSL,那是另一个值得单独讲的故事。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询