兄弟们,有没有这种经历:想做点嵌入式的东西,兴致勃勃翻出积灰的开发板,结果第一句话是“我板子呢?”,好不容易在某个抽屉角落找到了,插上ST-Link,打开终端,却发现自己在WSL里,然后脑子里冒出第二个问题——“WSL里面到底能不能烧录STM32?”
答案是能,而且折腾明白之后特别顺手。这篇文章我打算把WSL烧录STM32开发板的完整链路拆开揉碎讲一遍,从USB设备怎么“穿透”到WSL里,到OpenOCD怎么配置、烧录命令怎么写,再到我踩过的那些坑,一次说清楚。适合已经装了WSL、平时用VSCode写嵌入式C++、又不想为了烧录切回Windows命令行的朋友。
1. 先把一个核心问题说透:WSL凭什么能操作USB硬件
很多人对WSL有个误解,以为它就是Windows里开了一个Linux窗口,是个“玩具环境”,干不了跟硬件打交道的事。其实WSL2本质上是一个轻量级虚拟机,跑的可是正儿八经的Linux内核。既然是Linux内核,那理论上它就能访问USB设备、加载驱动、跟开发板通信。
但“理论上能”和“实际能用”之间还差了关键的一步——怎么把物理USB设备从Windows主机“转交”给WSL这个虚拟机。这一步不是WSL自带的,得靠一个叫usbip的协议去实现。
1.1 为什么放着原生工具不用,非要在WSL里烧录
先解释一下动机,不然很多人觉得这是脱裤子放屁。我做嵌入式C++开发的工作流是这样的:代码全部放在WSL的Linux文件系统里,用CMake组织工程,编译靠arm-none-eabi-gcc,调试用VSCode连WSL的Remote Development。整个流程在Linux侧非常顺畅,Makefile、CMake脚本、编译参数、链接脚本都是纯Linux风格。
问题是到了烧录这一步,以前我得切回Windows,打开STM32CubeProgrammer或者STM32CubeMX里的烧录功能,把编译好的bin文件再拖进去烧一次。来回切换非常打断节奏,尤其当你在持续改代码、持续烧录验证的时候,每烧一次就要切一次窗口,时间一长真的会烦。
所以最理想的状态是:编完代码,在WSL的终端里敲一条命令,烧录、校验、复位,一气呵成。这是整个事情的出发点。
1.2 USB设备转发这层原理,用生活类比讲清楚
usbip的原理,我一般用一个快递柜的类比来解释。
Windows这边相当于一个“快递站”,USB设备就是快递包裹。usbipd-win这个工具是快递站的管理员,它负责登记有哪些包裹(USB设备)、放在哪个柜子里(端口号)。WSL则是“快递接收方”,它需要申请把某一个柜子里的包裹“寄”过来。申请的动作就是usbipd attach,寄过来之后,WSL里的Linux内核就能看到这个USB设备,像本地设备一样使用。
这个方案有几个特点:
- 设备始终物理连接在Windows主机上,数据通过虚拟总线转发给WSL。
- WSL2使用的是轻量级虚拟化,转发路径很短,延迟可忽略。
- 转发是会话级的,WSL重启或设备拔出后需要重新attach。
- 不需要关闭Windows侧对设备的占用,但要避免两边抢设备。
注意:要区分WSL1和WSL2。WSL1走的是Windows内核兼容层,USB转发的方式和要求有细微差别。我建议直接用WSL2,各方面兼容性更好。
2. 烧录前的环境准备:把USB设备“送”进WSL
你要烧录STM32,至少需要两个物理层面的东西:一块STM32开发板(我这边主要是STM32F103C8T6,也就是俗称的蓝丸),一个调试烧录器(ST-Link V2,或者V3)。ST-Link通过USB连Windows主机,通过SWD四根线连开发板。
这个环节的目标只有一个:让WSL里的Linux系统能看到这个ST-Link设备。
2.1 确定你的WSL版本和Linux发行版
首先确认WSL版本,打开PowerShell:
wsl --status wsl --version如果显示的是WSL2,且内核版本在5.10以上,基本就没问题。如果你的WSL还是1.x的老版本,建议先升级:
wsl --updateLinux发行版我用的Ubuntu 22.04,里面自带的工具链和驱动支持都全,不用额外折腾源。
2.2 安装usbipd-win:这步是Windows侧的“快递管理员”
usbipd-win是一个开源工具,需要以Windows管理员权限安装,装完以后在服务列表里会自动运行。安装方式很简单,从官方GitHub仓库下载最新msi安装包,双击安装就行。
装好之后,打开PowerShell(管理员),执行:
usbipd list这时会列出所有USB设备,找到ST-Link那一行。它的BusId通常是类似3-2这样的格式,记住它。
然后绑定这个设备,绑定之后设备就允许被转发到WSL了:
usbipd bind --busid 3-2最后一步是附加到WSL:
usbipd attach --wsl --busid 3-2这一步成功后,你在WSL的终端里执行:
lsusb应该能看到类似STMicroelectronics ST-LINK的条目,说明设备已经被“递”进来了。
实操心得:
usbipd attach这个命令每次WSL重启后都要重新执行。如果不想每次都手动敲,可以建一个Windows任务计划,或者写一个简单的.bat脚本放桌面。后面排查章节我会给一个自动化的脚本方案。
2.3 在WSL里配置权限:udev规则和用户组
设备虽然能被lsusb看到,但不代表你有权限操作它。Linux下访问USB设备默认需要root权限,或者设备节点的权限被正确设置。如果不处理,OpenOCD启动时会报Permission denied,甚至更直白的no device found。
解决办法有两种:
一种是临时提权,每次都用sudo运行OpenOCD,简单粗暴但不优雅。另一种是一劳永逸的udev规则。
在WSL的Ubuntu里,创建udev规则文件:
sudo nano /etc/udev/rules.d/99-stlink.rules写入:
SUBSYSTEM=="usb", ATTR{idVendor}=="0483", ATTR{idProduct}=="3748", MODE="0666", GROUP="dialout"ST-Link V2的Vendor ID是0483,Product ID是3748。如果是ST-Link V3,Product ID会不一样,可以用lsusb看到的实际值来填。
然后让你的用户加入dialout组:
sudo usermod -aG dialout $USER接下去两条命令让规则生效:
sudo udevadm control --reload-rules sudo udevadm trigger注意:udev在WSL2里不是完全功能正常的,有时候reload不会立即生效。我实际测试下来,重启一次WSL后规则肯定能生效。
wsl --shutdown然后重新打开终端即可。
3. 烧录工具选型:OpenOCD还是st-flash
设备通了,权限没问题了,接下来选烧录工具。市面上的开源工具里,最主流就是OpenOCD和st-flash。我的项目全部都基于OpenOCD,下面说说为什么。
3.1 OpenOCD稳在哪,st-flash适合什么场景
先看一张对比表:
| 维度 | OpenOCD | st-flash |
|---|---|---|
| 支持的调试器 | ST-Link、J-Link、CMSIS-DAP等一大堆 | 主要是ST-Link |
| 支持的芯片 | 非常多,不限于ST | 主要是STM32 |
| 烧录 + 调试 | 都支持,能和GDB配合 | 只烧录,没法调试 |
| 脚本化程度 | 极高,可以自定义命令序列 | 比较简单,够用但不够灵活 |
| 配置复杂度 | 配置多,上手有门槛 | 几乎没有配置 |
| 适合场景 | 工程化、自动化、C++项目长期迭代 | 临时烧一个hex到某块板子 |
st-flash更像一个轻量级的烧录器,拿来即用,命令就一条:
st-flash write firmware.bin 0x08000000但这玩意儿遇到批量生产、Flash加密、自定义校验、脚本集成这些场景就力不从心了。OpenOCD像一个瑞士军刀,烧录只是它的一个功能,它还能做Flash擦除、读取、比对、复位运行、GDB调试。对于我这种用CMake管理项目的嵌入式C++选手,OpenOCD可以直接嵌进CMake的custom target里,一条make flash就搞定烧录,这个体验是st-flash给不了的。
3.2 OpenOCD安装两种方式,推荐直接源码编译
Ubuntu的APT仓库里有OpenOCD,安装很方便:
sudo apt update sudo apt install openocd但要注意版本问题。Ubuntu 22.04仓库里的是0.11.0,这个版本对ST-Link V3的支持不完整,对一些新板子也识别不佳。我吃了这个亏之后,改成源码编译。其实编译不复杂,依赖就几样:
sudo apt install libtool autoconf automake pkg-config libusb-1.0-0-dev libhidapi-dev texinfo然后从官方仓库拉源码:
git clone https://github.com/openocd-org/openocd.git cd openocd ./bootstrap ./configure --enable-stlink make -j$(nproc) sudo make install编译完之后验证一下:
openocd --version看到0.12.0以上的版本号,说明你用的是最新代码,对新一代调试器和芯片支持明显更稳。
3.3 没有ST-Link时的替代方案:串口ISP烧录
聊到工具,我顺手提一句:如果你的ST-Link还没到,手头只有一块板子和一根USB转TTL线,一样能烧录。STM32的ROM里固化了一段Bootloader,可以通过USART1接收数据。做法是把板子上的BOOT0引脚拉高,然后通过串口工具发送固件。
工具用stm32flash:
sudo apt install stm32flash命令大概是:
stm32flash -w firmware.bin -v -g 0x0 -b 115200 /dev/ttyUSB0这个方案的缺点是速度慢、没有调试功能,而且每烧一次都要手动切BOOT0跳线。但对于应急场景,确实是保底方案。我建议你在板子旁边常备一根USB转TTL线,不占地方,关键时刻真能救命。
4. 烧录全流程实操:从接线到芯片复位跑起来
讲完了原理和工具,下面是我实际上手操作的完整流程。这部分我尽量写得详细,因为很多问题不是出在命令上,而是出在细节上。
4.1 接线检查:连线错了,后面全白搭
先检查物理接线。ST-Link V2和STM32F103C8T6通过SWD接口连接,只需要四根线:
| ST-Link引脚 | 开发板引脚 |
|---|---|
| SWDIO | PA13 |
| SWCLK | PA14 |
| GND | GND |
| 3.3V | 3.3V |
这块板的SWD接口一般有专门的排针位置,如果没有,直接从ST-Link飞线到PA13和PA14也可以。要注意的是电源,如果开发板是独立供电,那ST-Link的3.3V可以悬空不接;如果开发板没有独立供电,那3.3V一定要接上。我遇到的不少“烧录失败”问题,根源就是3.3V没接,芯片根本没上电。
实操心得:ST-Link的3.3V输出能力有限,如果用蓝丸驱动LED矩阵或者小电机,建议开发板单独供5V,ST-Link只做信号传输。别把负载都压在ST-Link上,容易掉电复位,烧录失败率会飙升。
4.2 环境自检:烧录前花30秒确认链路
每次准备烧录前,我习惯按顺序跑一遍自检,不浪费时间,能快速定位是哪个环节出问题。
Windows侧(PowerShell,管理员模式):
usbipd list确认ST-Link设备还处于Attached状态。如果显示Detached,说明需要重新执行:
usbipd attach --wsl --busid 3-2WSL侧:
lsusb确认能看到ST-Link设备。然后验证权限:
ls -l /dev/bus/usb/ 2>/dev/null如果看不到usb目录或者没有权限,检查udev规则是否生效,必要时重启WSL。
也可以顺便确认一下内核日志:
dmesg | tail -20如果看到USB相关的报错,大概率是驱动或权限问题,这一步能提前暴露问题,避免后面OpenOCD报错时一头雾水。
4.3 OpenOCD烧录命令逐段拆解
OpenOCD烧录的核心命令是这样的:
openocd \ -f interface/stlink.cfg \ -f target/stm32f1x.cfg \ -c "program firmware.bin 0x08000000 verify reset exit"看起来不长,但每个参数都有讲究。
-f interface/stlink.cfg:指定调试器接口配置文件。ST-Link V2用这个,ST-Link V3要改成stlink-v3.cfg。选错配置文件是我早期经常犯的错,症状是OpenOCD启动时报unable to open device,但实际上硬件在Windows下是好的。
-f target/stm32f1x.cfg:指定目标芯片配置文件。STM32F103系列用stm32f1x.cfg,其它芯片要对应换。比如STM32F4系列就用stm32f4x.cfg,选错会直接导致Flash烧录失败,因为erase算法和Flash地址完全不对。
-c参数后面跟的是OpenOCD命令:
program firmware.bin 0x08000000:把bin文件烧到Flash起始地址0x08000000。verify:烧录完成后自动校验,强烈建议加上。烧录成功不代表数据正确,校验一下多一层保险。reset:烧录完自动复位芯片,程序立刻开始跑。exit:跑完自动退出OpenOCD,不进入交互调试模式。
所以完整的语义是:初始化ST-Link,连接STM32F103,烧录bin到Flash,校验一遍,然后复位运行芯片。
4.4 烧录过程中,读懂OpenOCD的输出再判断成功
很多人烧录时看到一堆输出不知道怎么看,其实OpenOCD的日志非常有规律。一次成功烧录的输出大概长这样:
Info : Listening on port 3333 for gdb connections Info : STLINK V2J29S7 (API v2) VID:PID 0483:3748 Info : Target voltage: 3.3V Info : clock speed 1000 kHz Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints Info : starting gdb server for stm32f1x.cpu Info : flash size = 128k, block size = 1k Info : Padding image section 0 with 8 bytes Info : wrote 24576 bytes from file firmware.bin in 2.279417s (10.531 KiB/s) Info : Verified OK Info : resetting target Info : target halted due to debug-request, current mode: Thread关键信息逐行解析:
STLINK V2J29S7:ST-Link固件版本,这一步说明调试器识别成功。Target voltage: 3.3V:芯片供电正常,如果这里显示0V,检查3.3V接线。flash size = 128k:芯片识别的Flash容量,如果识别成64k而你的芯片是128k,说明芯片型号识别有误。wrote 24576 bytes:实际写入的字节数,跟bin文件大小对得上就说明没毛病。Verified OK:校验通过,这是烧录成功最直接的信号。resetting target:复位完成,程序应该已经跑起来了。
如果看到Error: target not halted或者Error: flash write failed,就根据后面的报错内容查,下面这个章节我把自己踩过的几个高频坑列全。
5. 烧录失败排查实录:那些让人抓狂的问题
这段就是我说的“踩坑精华”,每个问题都是我实际遇到过、花时间排查过的,不写空话直接上干货。
5.1 高频问题速查表
| 症状 | 根因 | 解决办法 |
|---|---|---|
no device found | ST-Link没有attach到WSL | 重新执行usbipd attach --wsl --busid 3-2 |
Permission denied | udev规则没生效 | 检查99-stlink.rules,重启WSL |
unable to open device | ST-Link被Windows一侧的驱动占用 | 用Zadig把驱动换成libusb-win32,然后重新attach |
target voltage: 0V | 3.3V没接或者板子没上电 | 检查供电线路 |
flash write failed | 芯片配置文件选错 | 确认用stm32f1x.cfg而不是f4x.cfg |
Error: connect failed | openocd没退出就重新执行 | 先杀掉残留的openocd进程,pkill openocd |
Error: init mode failed | SWD接线不对或接触不良 | 检查SWDIO/SWCLK/GND三根线,重新插拔 |
5.2 案例一:WSL重启后设备“消失”了
这是所有问题里出现频率最高的。WSL每次重启之后,之前attach的USB设备会自动全部解脱,你要是直接跑OpenOCD,就会看到no device found。
我想过各种办法去解决这个麻烦,最后的方案是写一个一键脚本。在Windows桌面放个attach_usb.bat:
@echo off usbipd attach --wsl --busid 3-2另外我建议把usbipd bind设为开机自动执行。在Windows的任务计划程序里建一个计划任务,触发器选“启动时”,操作用usbipd bind --busid 3-2。这样即使WSL重启了,双击一下bat就能恢复环境。
5.3 案例二:Windows驱动抢占导致OpenOCD连不上
有一次我折腾了半天,设备也能attach,lsusb也能看到,但OpenOCD就是报unable to open device。查了一圈,发现Windows侧的ST-Link驱动默认被系统识别成了HID设备,usbipd转发出去之后WSL访问不到底层的USB接口。
解决办法是安装Zadig,打开后在Options里选择List All Devices,找到ST-Link,把驱动换成libusb-win32。换完驱动后Windows自带的STM32CubeProgrammer可能会认不到ST-Link,这是正常的,因为驱动变了。烧录全走WSL侧OpenOCD,这段风险是可接受的。
5.4 案例三:芯片Flash校验不通过
Verified failed这个报错,区别于flash write failed,它是写可能写进去了,但读出来比对不一致。最常见的原因是供电不稳。遇到这种情况不要急着重刷,先检查供电。蓝丸板子如果用USB给开发板供电,同时ST-Link又接了3.3V,两个电源之间可能有电压差,导致Flash写入时序异常。
我的做法是开发板只用一个电源,要么USB供电不接ST-Link的3.3V,要么ST-Link供电就不接外部USB电源。电源域干净了,校验不通过的概率会大幅下降。
5.5 案例四:老版本的OpenOCD不识别新芯片
有一段时间我拿到一块新的板子,芯片是STM32F401CCU6,但用0.11.0版本的OpenOCD去烧录,直接报unknown device。后来排查发现是OpenOCD版本太老,对新芯片的支持还没合入主分支。
这个问题的解法在前面已经说过了——源码编译新版OpenOCD。我建议做嵌入式长期开发的朋友,从一开始就直接源码安装最新版,省得一两个月后遇到新芯片又要重编一次。
6. 把烧录集成进CMake:嵌入式C++项目的正确玩法
既然是嵌入式C++编程之旅,那最后这部分必须讲讲怎么把烧录动作跟CMake工程整合,真正实现一条命令编译加烧录。
6.1 在CMakeLists里加一个自定义烧录目标
我习惯在CMakeLists.txt的末尾加上这样一段:
# 获取bin文件路径 set(APP_BIN ${CMAKE_BINARY_DIR}/${PROJECT_NAME}.bin) # 定义flash目标 add_custom_target(flash COMMAND ${CMAKE_COMMAND} -E echo "Flashing ${APP_BIN} ..." COMMAND openocd -f interface/stlink.cfg -f target/stm32f1x.cfg -c "program ${APP_BIN} 0x08000000 verify reset exit" DEPENDS ${PROJECT_NAME}.bin COMMENT "Flash firmware to STM32 via OpenOCD" )然后再加一个生成bin的规则,因为OpenOCD烧bin更方便,链接生成的默认是elf:
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD COMMAND ${CMAKE_OBJCOPY} -O binary $<TARGET_FILE:${PROJECT_NAME}> ${CMAKE_BINARY_DIR}/${PROJECT_NAME}.bin )这样每次编译完成后自动生成bin,然后再编译一遍工程:
cmake --build build -j$(nproc)接下来只需要一个命令:
cmake --build build --target flash编译、生成bin、调OpenOCD烧录、校验、复位,全链路自动跑完。整个过程没有任何切换操作,就是改代码、编译、烧录、看结果,这才是嵌入式开发该有的效率。
6.2 双目标板场景下的配置复用
如果你手里有多块不同的板子,比如一块F103、一块F401,那可以把芯片配置抽出来,用CMake的cache变量控制:
set(OPENOCD_TARGET "stm32f1x.cfg" CACHE STRING "OpenOCD target config") set(APP_FLASH_ADDR "0x08000000" CACHE STRING "Flash address")在flash目标里引用这两个变量:
COMMAND openocd -f interface/stlink.cfg -f target/${OPENOCD_TARGET} -c "program ${APP_BIN} ${APP_FLASH_ADDR} verify reset exit"这样切换板子时只需:
cmake -DOPENOCD_TARGET=stm32f4x.cfg ..不用改任何代码逻辑。
7. 最后分享一点我自己的体会和踩坑心得
整套WSL烧录方案跑通之后,我最大的感触是:嵌入式开发环境真正回归了Linux侧的统一。过去那种“代码在Linux里写、烧录切到Windows”的割裂感消失了,整个流程从头到尾都在一个终端里完成,这对我写C++工程来说,体验提升非常明显。
如果你问我有没有必要一开始就搞OpenOCD加CMake集成,我的回答是分阶段来。刚上手时用st-flash先跑通烧录链路,感受到“板子能烧进去”的正反馈很重要。当你开始做有一定规模的项目,编译产物要迭代几十次、上百次的时候,再花半小时把它集成进CMake,绝对不亏。
最后一个实用小技巧:OpenOCD烧录失败时,第一步永远不是去改命令,而是先跑一遍第4.2节里的自检三条命令。很多问题出在USB attach状态、驱动抢占这些环境因素上,而不是命令本身写错了。把环境确认清楚,烧录问题基本解决了一大半。