开篇先讲个真实场景。我用WSL2做STM32的C++开发,平时编译、链接、看反汇编全在Linux侧完成,顺手得很。直到有一天,我写好一段新的串口驱动,准备把它烧进板子验证,插上ST-Link和开发板,Windows那边弹出"设备已就绪",WSL里执行lsusb却一片空。那一瞬间脑子里就三个字:我板子呢?后来把整个折腾过程理顺了,才发现WSL里烧录STM32其实不复杂,只是需要先绕过一个核心障碍——WSL2默认根本不给你直通USB设备。这篇文章就把这套完整方案写清楚,从原理到踩坑,给同样在WSL里做嵌入式开发的人一条能直接走通的路。
1. 先搞清楚你的板子去哪了:WSL的设备隔离机制
1.1 WSL2为什么"看不见"USB设备
很多人第一次在WSL里插开发板,最直观的困惑是:Windows上明明识别到了设备,WSL里却什么都看不到。这不是驱动问题,也不是板子坏了,而是WSL2的架构导致的。
WSL2跑在一个轻量级虚拟机里,通过虚拟网络和宿主机通信。这个虚拟机默认没有挂载任何USB控制器,所以你在物理机上插的USB设备,对WSL2来说就像插在另一台电脑上一样,完全不可见。你可以在WSL里执行ls /dev/ttyUSB*、lsusb,结果都是空的——不是命令打错,而是设备根本没进到这个系统里。
WSL1倒是没有这个问题,因为WSL1是系统调用翻译层,设备文件直接映射,USB串口能直接用。但WSL1不支持真正的Linux内核,很多嵌入式编译链、性能敏感的工具跑起来不方便。我早期图省事用WSL1,后来编译一个带模板元编程的C++项目,编译速度实在难受,就彻底迁到WSL2了。迁完之后要烧程序,就撞上了USB设备不可见这堵墙。
解决思路其实很朴素:既然WSL2是虚拟机,那就让宿主机把USB设备"让"给这个虚拟机。这个"让"的动作,行业里叫USB设备透传,WSL2生态里最通用的实现方式是USB/IP协议配合usbipd-win。
1.2 先自查:用lsusb和dmesg确认环境
在动手改方案之前,我建议你花两分钟做一次环境自查,确认问题到底出在哪层。步骤就三步:
- 在WSL里跑
lsusb,看有没有任何USB设备列表。如果输出空,说明设备确实没透传进来。 - 在WSL里跑
dmesg | grep -i usb,看内核有没有识别到USB控制器。WSL2默认会有一个虚拟的USB控制器,但通常没有任何设备挂上去。 - 在Windows的PowerShell里跑
usbipd list,这一步需要你装好usbipd-win之后才能用,它能列出宿主机上所有USB设备的总线ID和连接状态。
我当时做完这三步,结论非常清晰:Windows里ST-Link状态正常,总线ID是1-4,设备名显示为STMicroelectronics STLink Dongle,WSL里lsusb干干净净——问题就出在透传环节。
还有个值得留意的点:如果你用的是串口模块(比如CH340或CP2102转出来的USB转串口),即便不透传,WSL2里也能通过一些间接手段访问,但稳定性很差。做嵌入式开发,尤其涉及SWD调试、复位、读取芯片ID这类操作,必须走USB透传把调试器完整送进WSL,不能只想着通个串口就完事。
2. 方案选型:把USB设备"透传"进WSL的几条路
2.1 主力方案:usbipd-win的USB/IP转发
当前WSL2下访问USB设备最成熟的方案,是微软官方在README里推荐的usbipd-win。它做的事情本质上是利用USB/IP协议,把宿主机上的某个USB设备"共享"出来,再让WSL2这个虚拟机把设备"接"进去。
名字听着复杂,用起来逻辑很清楚:宿主机上选定一个设备,执行绑定,然后WSL里执行连接,设备就从Windows侧"拔"出去、在Linux侧"插"进来。绑定的粒度是设备级,不是接口级,所以对于ST-Link这种复合设备(既有调试接口又有虚拟串口),透传后两边功能在Linux里都会可见,调试和串口监视可以同时做。
我遇到过一个问题需要提前说明:绑定某个设备后,Windows侧会立即失去这个设备。如果你习惯了在Windows的CubeProgrammer里烧录,绑定的瞬间那个工具会直接报"设备丢失"。这不是故障,而是设备被移交了。反过来,在WSL里断开之后,Windows侧又恢复正常。所以习惯上你要决定:某一时间段,设备归Windows还是归WSL,不能两边同时抢。
2.2 备选方案:WSL1直连与串口转接
如果你只是偶尔烧个程序,不想装usbipd-win,有两条备选路线。
第一条是切回WSL1。WSL1对串口设备可以直接访问,最常见的就是接CH340模块的串口,在WSL1里能看到/dev/ttyUSB0。但WSL1跑不了OpenOCD这种依赖完整内核驱动能力的程序吗?其实OpenOCD在WSL1里也能跑,但坑不少。比如WSL1缺少完整的USB HID访问语义,ST-Link的SWIM模式、JTAG时序在某些版本上会有兼容性问题,是能用但不稳定的状态。加上WSL1的编译性能确实拉胯,我不建议为了烧录单独维护一个WSL1发行版。
第二条是走串口层面,不碰USB。有些板子自带串口下载模式(比如通过BOOT引脚进入系统Bootloader),你可以把它当成一个串口设备处理。但这条路和SWD调试相比信息量少太多,只能烧固件不能调试,出了问题你没法在线看寄存器、打断点,体验打折扣。
所以我的结论很直接:既然都做到系列第7篇了,说明你已经投入精力在WSL里搭嵌入式C++工具链,那USB透传是绕不开的必修课,直接上usbipd-win,一劳永逸。
2.3 调试器选型对透传的影响
透传方案定了之后,调试器本身也会影响体验。我前后用过三种设备:原装ST-Link、某宝上的ST-Link V2克隆版,以及一个带板载调试器的开发板(板上直接焊着ST-Link)。
原装ST-Link在透传后,OpenOCD识别得非常干净,命令openocd -f interface/stlink.cfg基本不需要额外参数,直接能找到设备。克隆版就麻烦一些,它在USB枚举时上报的厂商信息、序列号经常不规范,OpenOCD有时会因为设备权限或者描述符解析失败而拒绝连接。解决办法一般是追加上报ID或改udev规则,具体我在第4节里展开。
板载调试器的情况要特别注意:很多开发板的板载ST-Link同时扮演调试器和虚拟串口两个角色,透传之后它会作为一个复合设备出现在Linux里,通常能看到/dev/ttyACM0和调试器节点两个入口。这种方案实际用起来最舒服,因为调试和串口日志不用切换设备,都在WSL里完成。
3. 实操记录:完成一次完整的"WSL内烧录STM32"流程
3.1 宿主机侧:安装usbipd-win并绑定ST-Link
环境假设:Windows 11,已装WSL2和一个Ubuntu发行版。第一次做,先装usbipd-win。
安装方式我推荐用winget。在PowerShell里执行:
winget install usbipd装完需要管理员权限才能操作设备的绑定和共享,所以后面的操作记得用管理员身份的PowerShell。
安装完成后,先查看当前所有USB设备:
usbipd list输出会显示类似这样:
1-4 STMicroelectronics STLink Dongle Not shared 2-2 USB Serial Device Not shared这里1-4是总线ID,STMicroelectronics STLink Dongle是设备描述,Not shared表示当前没有共享。注意看设备描述,别认错。比如有的CH340串口模块显示USB Serial,你如果把串口模块透传进去而调试器留在Windows,那就白折腾了。
确认设备后,先绑定,再共享,两步可以合成一步:
usbipd bind --busid 1-4 usbipd attach --wsl --busid 1-4bind的作用是把设备标记为"可共享",这一步的效果是持久的,重启后依然生效。attach才是真正把设备"送到"WSL里。如果WSL里有多个发行版,attach后面要加--distribution参数指定目标发行版。
这里有个细节:attach执行完并不是永远保持,WSL重启之后需要重新执行attach。bind是一次性的,attach不是。所以后面我做了脚本来自动处理,第3.4节会说。
3.2 WSL侧:安装usbip客户端与OpenOCD
设备从Windows透传过来,WSL侧还得有配套的客户端工具,否则设备到了门口也进不来。在WSL的终端里执行:
sudo apt install linux-tools-generic hwdata安装完成后,usbip工具通常会被安装到/usr/lib/linux-tools/<版本号>/usbip,但路径不直接暴露在PATH里。每次用都要敲全路径很烦,我直接建了个软链接:
sudo ln -s /usr/lib/linux-tools/*/usbip /usr/local/sbin/usbip这时候在WSL里执行lsusb,应该能看到ST-Link了。如果看不到,大概率是权限问题或者内核模块没加载,排查方法在第4节。
接下来装OpenOCD。Ubuntu的软件源里有OpenOCD,直接:
sudo apt install openocd这里有个经验:版本别追求新,稳定第一。Ubuntu源里的OpenOCD可能不是最新版,但对ST-Link V2的支持已经非常成熟,新版本在JTAG时序、速度设置上确实有提升,但源码编译OpenOCD的依赖多、功耗大,如果不是需要新的目标芯片配置,用发行版自带版本就够了。
我顺手还装了stlink-tools,因为有时候只想单纯读个芯片ID或者备份固件,没必要启动完整的OpenOCD服务:
sudo apt install stlink-tools3.3 开始烧录:从ELF到开发板
环境打通之后,烧录这个动作本身反而不复杂。假设你的项目用CMake构建,编译产物是一个ELF文件,比如build/stm32_fw.elf。
先验证OpenOCD能不能看到调试器:
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "init; halt"解释一下这两行配置:interface/stlink.cfg告诉OpenOCD用ST-Link作为调试器,target/stm32f4x.cfg告诉它目标芯片是什么型号,如果你的芯片是STM32F103,那就是stm32f1x.cfg,选对应的就行。init; halt让OpenOCD初始化调试链路,并在目标芯片复位时立刻暂停,防止在篡改Flash时芯片还在跑旧程序。
正常情况下,OpenOCD会输出类似这样的日志:
Info : STLINK V2 J17S4 (API v2) VID:PID 0483:3748 Info : Target voltage: 3.3 V Info : clock speed 4000 kHz Info : stm32f4x.cpu: hardware has 6 breakpoints, 4 watchpoints看到这些,说明链路通畅,可以烧录了。
完整烧录命令:
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "program build/stm32_fw.elf verify reset exit"这条命令的含义:把build/stm32_fw.elf写入芯片Flash,写完自动校验,然后复位芯片让新固件跑起来,最后退出OpenOCD。verify必加,它能立刻发现写入错误,省得你烧完发现板子没反应,还要分心排除是写入问题还是代码问题。
如果项目还生成了hex或bin文件,也可以用:
openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "program build/stm32_fw.bin 0x08000000 verify reset exit"bin文件没有地址信息,必须显式给出烧录地址。STM32的Flash起始地址通常是0x08000000,针对的是主Flash区。如果你用的是OTA架构,应用程序放后面,地址就要对应调整,比如从0x08008000开始。
3.4 把烧录用脚本固化下来
人工敲命令总会敲错,而且每次烧录都要开一个终端跑OpenOCD,体验比较"原始"。我把烧录流程固化成了一个脚本flash.sh,放在项目根目录下:
#!/usr/bin/env bash set -euo pipefail BUILD_DIR=${BUILD_DIR:-build} TARGET_CFG=${TARGET_CFG:-target/stm32f4x.cfg} ELF_FILE=${ELF_FILE:-${BUILD_DIR}/stm32_fw.elf} if [ ! -f "${ELF_FILE}" ]; then echo "[ERROR] ELF file not found: ${ELF_FILE}" echo "Run cmake --build ${BUILD_DIR} first." exit 1 fi echo "[INFO] Starting OpenOCD flash: ${ELF_FILE}" openocd -f interface/stlink.cfg \ -f "${TARGET_CFG}" \ -c "program ${ELF_FILE} verify reset exit"把脚本放到/usr/local/bin或者加个alias都行。实测下来,配合CMake的ninja构建,整个流程就是:改代码 → 编译 → 执行./flash.sh→ 看板子跑起来,一气呵成。
另外一个小技巧:每次手动执行usbipd attach很烦,我写了一个Windows侧的批处理,开机后执行方便快速把调试器让给WSL:
usbipd attach --wsl --busid 1-4不过在WSL里用到Device TPM等需要设备独占的场景时,记得解除attach,否则Windows侧程序访问不到设备。
4. 常见问题与排查技巧实录
4.1 绑定成功后WSL里还是看不到设备
这是最让人崩溃的情况:Windows侧usbipd list显示Attached,WSL里lsusb却还是空白。
第一排查点是内核模块。usbip-client需要加载vhci-hcd模块,执行:
sudo modprobe vhci-hcd如果提示modprobe: FATAL: Module not found,说明内核里没有这个模块。WSL2默认内核是微软提供的版本,功能裁剪过。解决方式是更新WSL内核,或者手动编译一个开启usbip的内核。手动编译内核对于大多数人不现实,优先尝试wsl --update把内核升级到最新版,再查看是否自带vhci-hcd支持。
第二个坑是Windows侧的"安全中心"拦截了设备重分配。有些设备绑定之后,Windows的安全软件会因为"设备被移除"而触发保护,把attach操作回滚。在Windows事件查看器里能看到相关日志。这种情况我遇到一次,把安全软件对USB设备的实时保护关掉之后解决,具体策略看你用的什么软件,这就不展开了。
第三个坑在权限。WSL里lsusb能看到设备,但OpenOCD打开设备时报权限错误。这个很常见,因为设备文件默认属于root用户组。看一眼:
ls -l /dev/bus/usb/001/如果设备节点是root:root,普通用户跑OpenOCD就得加sudo。更优雅的方式是配udev规则,见4.4节。
4.2 OpenOCD连接调试器报错
OpenOCD启动时报Error: open failed或unable to open CMSIS-DAP device,多数情况是权限,但我还遇到过一次很有意思的情况:克隆版ST-Link在透传后,USB端口路径发生了变化,OpenOCD内部缓存的VID:PID解析失败了。
排查步骤:
先确认WSL侧能看到设备且权限正确:
lsusb用
st-info --probe尝试直接探测,如果能读到芯片信息,说明ST-Link本身没坏,问题出在OpenOCD和调试器的握手环节。如果是克隆版,OpenOCD里可以尝试降低连接速度,或者在配置里显式指定适配器驱动:
openocd -f interface/stlink.cfg -c "adapter speed 1000; init; halt"有些克隆版对高速握手支持不好,把1000 kHz降下来,握手成功率会高不少。
还有一个非常容易被忽略的问题:目标板供电不足。ST-Link从USB取电,透传后如果USB口经过扩展坞转发,供电能力可能跟不上,OpenOCD探测时目标电压只有1.8V,直接报电压异常。这种情况换一个直连的USB口,或者给板子单独供电,立刻就能解决。
4.3 烧录进去后程序不跑
烧录时报verify failed是最直接的问题,说明Flash写入数据对不上。常见原因:
- 地址写错了。比如给STM32F103烧一个本该从
0x08008000启动的带Bootloader程序,你直接烧到0x08000000,verify大概率失败,即便侥幸通过,运行也会乱。 - 芯片读保护开启了。STM32有RDP(Read Out Protection)等级,等级一及以上时,OpenOCD能连接但无法写入Flash,报
Cannot access memory。解决方法是先用STM32官方工具或者OpenOCD命令解除保护,但注意解除保护通常会擦除整个Flash,板子里的Bootloader也没了,操作前务必确认。
烧录成功但板子没反应,优先检查启动方式。以STM32F4为例,BOOT0和BOOT1引脚决定从哪里启动。如果两个引脚状态不对,芯片可能跳进系统Bootloader而不是你写的Flash程序。用万用表量一下引脚电平,或者直接看开发板上的跳线帽位置,就能排除掉这个因素。
程序跑起来但行为不对,这种情况我建议用串口输出加灯闪烁来定位。WSL里直接看/dev/ttyACM0的串口日志很方便,用minicom或者screen:
screen /dev/ttyACM0 115200透传之后虚拟串口也在WSL里,这一步完全不需要回到Windows。
4.4 权限与udev规则备忘
在WSL里折腾ST-Link,不写udev规则的话,每次都要sudo,非常影响心情。我用的规则文件放在/etc/udev/rules.d/99-stlink.rules,内容如下:
SUBSYSTEM=="usb", ATTR{idVendor}=="0483", ATTR{idProduct}=="3748", MODE="0666"0483是ST-Link的厂商ID,3748是常见的ST-Link V2产品ID。克隆版的产品ID可能不同,先用lsusb查一下实际的值再写。写完执行:
sudo udevadm control --reload-rules sudo udevadm trigger加载之后,插拔一次设备或重新attach一次,普通用户就能直接访问了。
顺带提一种更好查的规则思路:不要锁死某个产品ID,而是针对整个厂商ID开放。做个人开发环境无所谓,但如果你在团队共享机器上,还是锁具体产品ID比较安全,避免误开放其他USB设备权限。
5. 后续扩展:把烧录整合进自动化流程
既然已经能在WSL里烧录了,再往前走一步,可以把它整合到CI或者一键构建流程里。我在项目里加了一个make flash目标,本质上就是调用第3.4节的脚本,同时把CMake的构建路径、目标芯片配置统一收口到CMakeLists里:
add_custom_target(flash COMMAND bash ${CMAKE_SOURCE_DIR}/flash.sh DEPENDS stm32_fw COMMENT "Flash firmware via OpenOCD" )这样改完代码只需要:
make flash编译和烧录自动串联,写代码的节奏顺畅多了。
另一个实用的扩展是版本校验。在烧录前自动读取当前芯片内固件的版本(如果固件里烧了版本号,存在固定Flash地址),和目标版本做对比,不一致才烧录。这个改动对量产场景价值更大,个人开发时做个简单的目标文件存在性校验就够了。
我个人在实际操作中还有一个习惯:烧录前总是先看一眼OpenOCD输出的目标电压和芯片ID。电压低于3.0V我不烧,芯片ID和配置里不一致我不烧。等过几次因为电压低导致写入异常的情况,你就能理解这个习惯了。做嵌入式开发,板子事小,时间成本事大,宁可多花十秒确认,也不要盲目操作然后排查半天。
最后分享一个小技巧:WSL里跑OpenOCD时,如果感觉烧录速度慢,看看是不是USB透传的链路经过USB 2.0的Hub,HUB转发会拉低事务速率。把ST-Link插到直连的USB 3.0口,烧录一个512KB的固件,体感速度能差出一倍。这个差异虽然不是决定性的,但每次省下的几秒钟,累积起来也是不少时间。