"我板子呢?"这个问题,在过去的几周里几乎成了我每天坐在工位前的第一句话。不是因为爱丢东西,而是因为自从我彻底切到WSL环境下写STM32的嵌入式C++代码之后,编译、构建、版本管理都顺得离谱,唯独到了烧录这一步,开发板明明插在电脑上,WSL里却像什么都没发生一样。这大概是所有端着WSL做嵌入式的人都会撞上的墙:IDE里的烧录按钮在Linux侧使不上劲,Windows侧的烧录工具又不认识你用CMake折腾出来的那些二进制。所以这篇就来聊聊,在WSL环境里到底怎么才能把手里的STM32开发板顺利地烧录起来,把这句“我板子呢”变成“板子在WSL里好好待着呢”。按照这个系列的进度,到这一步其实已经能写出能在片子上跑起来的C++代码了,但代码写完总得烧进去看现象。网上关于WSL烧录的帖子不少,但大多零散,有的说WSL1直接开串口,有的说用网络调试器,看完更糊涂。我花了两天时间把自己踩过的坑和最终跑通的方案完整梳理了一遍,这里面包括USB设备怎么透传、OpenOCD怎么装、烧录命令怎么敲、烧录失败怎么排查,还有几个不看一眼文档绝对发现不了的隐藏问题。这篇尽量讲透,让后面的人不用再经历一次"板子失踪案"。1. 起因:开发板明明在USB口上,WSL里却像没插一样1.1 "我板子呢"的经典重现先描述一个大多数人都会遇到的场景:你在Windows下用烧录工具成功烧了一次,确认板子能跑,然后想回WSL里改代码。写完代码、编译、生成了.bin,拿起线准备烧,这时WSL终端里的lsusb输出光秃秃的,只有那个虚拟的xHCI host controller,根本没有ST-Link的影子。你会下意识地摸一下USB线,发现板子的电源灯亮着,于是你怀疑是不是线坏了,换一根线,还是不行。然后你打开Windows设备管理器,板子好好的,在"端口"和"通用串行总线设备"下都看得见。这时候就会冒出那句经典的"我板子呢?"。这不是板子丢了,而是WSL2和Windows之间存在一层"隔离墙"。WSL2本质上是跑在Hyper-V虚拟机里的一个轻量Linux,它拥有独立的Linux内核和独立的USB设备子系统,Windows上插着的USB设备并不会自动"透传"过去。WSL1倒是没有这堵墙,它直接翻译Windows系统调用,所以你可以通过/dev/ttyS0之类的方式访问串口,但WSL1又不能跑很多嵌入式工具链要用的Linux内核特性。这让人很难受。1.2 是WSL1还是WSL2,决定了你接下来的路径很多人一上来就找"WSL烧录STM32"的教程,结果发现旧教程里写着用WSL1直接访问COM口,于是照着操作,在WSL2里怎么都找不到设备,开始怀疑人生。我们先把这个概念掰清楚:如果你用的是WSL1,那么宿主机上的COM口会以串口设备的形式映射到Linux里,比如/dev/ttyS0,配合一个Windows端的USB转UART桥接,确实可以在WSL1里操作串口。但WSL2不行,WSL2是真正的虚拟化环境,它不能直接看到宿主机USB设备。从当前主流的开发习惯来看,大部分人的WSL都升级到了WSL2(通过wsl --set-version命令可以查到),因为WSL2的I/O性能和兼容性更好。所以这篇文章的主角是WSL2下的烧录方案。顺带说一句,如果你还在用WSL1,其实不太建议为烧录单独迁回WSL1,因为后面你会遇到更多边边角角的问题,不如一次性把USB透传搞定,以后调试器、串口、烧录器都能一并复用。1.3 为什么非要跟WSL较劲,Windows下烧录不香吗每次有人听说我在WSL里折腾烧录,第一反应都是:你这不是自找麻烦?Windows下打开烧录软件点一下不就完事了。确实,如果一天就烧一次,Windows下点几下没毛病。但我的实际痛点是:代码已经全切到WSL侧的CMake工程里了,编译、链接、代码格式化、静态检查全在Linux侧,每次改完代码切到Windows去烧录,中间要经历"拷贝文件、找工具、选文件、点烧录"这一串重复劳动,稍微多烧几次就烦了。更别说我想在脚本里实现"编译通过后自动烧录"的流程,Windows GUI完全没法配合。把烧录迁到WSL里,得到的是一整套命令行工作流:改代码、编译、烧录、看串口,全在同一个终端里完成。脚本化之后,以后同事拿到工程只要执行一条命令,板子就亮起来了。这种体验上的提升,值得花一次功夫把USB透传和研究清楚。这也是我写这篇的初衷。2. WSL2下的USB穿透方案:usbipd-win的正确使用姿势2.1 USB/IP是什么,为什么选它WSL2不能直通USB,那怎么把板子交到WSL手里?最成熟、社区验证最多的方案是使用usbipd-win加上Linux内核里的usbip客户端。usbip的全称是USB over IP,它把一个USB设备从一台机器"分享"给另外一台机器,Windows这边作为USB设备共享服务器,WSL2作为客户端接收设备。听起来很复杂,其实安装和操作只要几步。注意,这里说的不是虚拟机的"USB直通"功能,而是更灵活的设备共享协议。为什么不用Hyper-V的增强会话或Windows自带的USB重定向?因为那些是给完整虚拟机用的,WSL2没有图形界面管理工具,直接在命令行里敲命令更顺手。usbipd-win从很早就开始维护,目前版本比较稳定,配合WSL2的内核可以直接工作。我自己用下来感觉最舒服的一点是它支持--auto-attach参数,可以设定某个设备在插入时自动挂到WSL2里,不需要每次手动attach,省去了很多麻烦。2.2 Windows侧:安装、绑定、attach的完整命令先来看Windows侧要做什么。在Windows 11或较新的Windows 10上,你可以通过winget直接安装usbipd-win,命令如下:winget install usbipd装完之后,PowerShell或者CMD需要以管理员身份运行,因为后面要操作USB设备。先列出当前计算机上的USB设备:usbipd list输出会显示一个BUSID列表,每行就是一个USB设备,一列是设备描述。比如你的STM32板载ST-Link会显示类似"STMicroelectronics STLink dongle"这样的名字。找到之后,把它绑定(bind)。绑定的意思是让这个设备可以被WSL共享出来,命令格式是:usbipd bind --busid <BUSID>如果希望以后每次插入这个设备时自动挂到某个WSL发行版,可以加一个auto-attach参数:usbipd bind --busid <BUSID> --auto-attach绑定之后,设备在Windows里就会被"摘下来",好像被拔掉一样,因为它已经准备好交给WSL了。然后执行attach命令:usbipd attach --wsl --busid <BUSID>如果只有一个WSL发行版,直接写--wsl即可。如果有多个发行版,用--wsl <发行版名字>指定。attach成功之后,设备就会出现在WSL2里。此时在WSL终端里执行lsusb,应该能看到对应的设备了。需要特别提醒的是,每次重启Windows或者重启WSL,attach状态都会丢失,需要重新执行attach。auto-attach也不是完全万能,它只处理"插入时自动attach",如果重启之后设备已经插着,还是需要手动attach一次。我习惯把attach命令写成一个alias,这样重新进入WSL后一条命令就能恢复。2.3 WSL侧:usbip客户端工具的配置WSL2的Linux内核默认带usbip支持,但还需要安装用户空间工具,不同发行版命令略有不同。Ubuntu/Debian下执行:sudo apt update sudo apt install linux-tools-generic usbip hwdata注意,linux-tools-generic这个包安装之后,usbip命令可能在/usr/lib/linux-tools/<内核版本>/usbip路径下,不在PATH里。你可以直接找到它,或者做一个软链接:sudo ln -s /usr/lib/linux-tools/$(uname -r)/usbip /usr/local/bin/usbip其实在我们执行attach时,通常直接用usbipd的命令就行,因为usbipd-win也就是调用了usbip协议。但如果想手动debug,可以用usbip list -r <Windows主机的IP>来查看远程设备列表。一般用不上,知道有这回事就行。装好之后,在WSL里运行lsusb,看到类似"STMicroelectronics ST-LINK"的就是设备到达了。如果设备对应的是USB转串口,在lsusb里也会显示。接下来就可以在WSL里直接访问这个设备。2.4 多发行版、多设备场景下的管理细节如果你装了多个WSL发行版(比如一个Ubuntu用于日常开发,一个Debian用来做实验),attach命令必须指定目标发行版,否则usbipd-win不知道往哪儿送。每次attach都要写发行版名字,很容易写错。我自己的做法是,给每个发行版写一个不同的脚本,脚本里exportWSL_DISTRO_NAME再执行attach。这样就算发行版多也不会乱。还有一个细节:lsusb看到的设备可能有多个。比如一块ST官方开发板,板载ST-Link和USB虚拟串口其实是两个USB功能,枚举出来是两个独立设备。如果你把带调试功能的那个设备attach进去了,但串口功能那个设备还留在Windows里,WSL里就看不到/dev/ttyACM0。所以attach之前先确认一下,究竟哪个busid对应你需要的功能。用usbipd list输出里的描述信息就能判断。3. 烧录工具链的安装与配置:OpenOCD还是st-flash?3.1 两条路线该怎么选设备能看见了,只是第一步。接下来要选一个WSL里能跑的烧录工具。目前STM32烧录工具几条主流路线:OpenOCD:跨平台开源调试和烧录工具,支持ST-Link、J-Link等多种调试器,社区活跃,支持大量目标芯片,适合复杂调试和自动化烧录。st-flash(stlink-tools):ST-Link专用的命令行烧录工具,轻量,命令简单,适合快速烧录,但只支持ST-Link。官方图形烧录工具:ST官方工具,Linux版CLI也能跑,但在WSL2里权限问题处理起来比较别扭,而且重。我做嵌入式开发的时候比较喜欢OpenOCD,因为它不仅能烧录,还能配合gdb做调试,这一篇烧录,下一篇调试也用得上。st-flash更简单直观,适合不折腾的人。下面我把两条路都列出来,你按自己的风格选。3.2 OpenOCD的安装与基础验证在Ubuntu下安装OpenOCD很方便:sudo apt install openocd但这里有个坑:Ubuntu官方仓库里的OpenOCD版本可能偏旧,而新出的STM32系列芯片可能需要更新的OpenOCD才有支持。如果你用的是比较常规的STM32F1/F4,仓库版足够。如果用的是新款芯片,建议从源码编译。源码编译也不复杂:sudo apt install libtool autoconf automake pkg-config libusb-1.0-0-dev git clone https://github.com/openocd-org/openocd.git cd openocd ./bootstrap ./configure --prefix=/usr/local make -j$(nproc) sudo make install编译安装之后,检查版本:openocd --version与此同时,还需要安装arm的交叉编译器。如果你的项目里已经用了CMake,可能已经装了。没装的话:sudo apt install gcc-arm-none-eabi装完跑一下:arm-none-eabi-gcc --version3.3 st-flash的安装与使用st-flash是ST-Link工具集里的命令行烧录工具,在Ubuntu下也可以直接装:sudo apt install stlink-tools安装完成后,可以用st-info --probe来查看当前连接的ST-Link设备。如果设备识别正常,会显示芯片型号和容量。烧录的命令很简单:st-flash write firmware.bin 0x08000000st-flash只能写bin文件,不能直接写elf,所以需要先把编译产物转化为bin格式。OpenOCD则可以直接写elf,比较省事。从这一点来说,我用OpenOCD更多。3.4 为什么我不建议在WSL里用官方GUI烧录工具官方烧录工具的Linux版本虽然提供了CLI,但用起来有两个问题。第一,它依赖特定的图形库,WSL2如果没有启用WSLg,跑起来可能报找不到显示器的错,虽然CLI模式不需要GUI,但安装包体积和依赖确实不小。第二,它对USB设备的访问默认走的是自己的驱动栈,在WSL2的透传环境下,偶尔会出现检测不到设备的情况,还得去改配置。相比之下,OpenOCD直接走libusb,跟usbipd透传过来的设备配合得很自然。当然,如果你在Windows下已经习惯用官方工具,也没必要非换不可。但从自动化角度讲,CLI工具越轻量越好。我现在的工程就是CMake里加一个自定义目标,编译完直接调OpenOCD,一步到位。3.5 在WSL里验证设备权限的快速检查不管用哪个工具,第一步都要确认WSL能看到设备。插上板子,在WSL里执行:lsusb如果你看到类似1689:5730 STMicroelectronics这样的设备ID,说明透传成功。但即便看到了,工具可能还是提示libusb_open failed或者permission denied。这是因为Linux对USB设备有权限控制,普通用户不能直接访问。解决办法是配置udev规则,把当前用户加入dialout组,并让ST-Link设备允许用户读写。在WSL2里,udev规则也是生效的,但需要额外注意WSL的init系统处理方式,后面我会专门讲这个坑。4. 烧录实操:从编译到OpenOCD烧录的完整链路4.1 一次最小的烧录命令假设你的工程已经用CMake编译出了一个elf文件,叫app.elf。在WSL里用OpenOCD烧录,命令如下:openocd -f interface/stlink.cfg -f target/stm32f1x.cfg -c "program app.elf verify reset exit"其中-f指定接口配置和目标芯片配置。interface/stlink.cfg是ST-Link接口配置文件,target/stm32f1x.cfg是STM32F1系列的目标芯片配置。如果你用的是F4,改成stm32f4x.cfg即可。命令行的含义是:启动OpenOCD,加载ST-Link接口和芯片配置,然后执行program命令烧录app.elf,烧完校验(verify),复位(reset),退出(exit)。这一条命令是日常使用频率最高的,没有之一。如果烧录的是bin文件,也可以:openocd -f interface/stlink.cfg -f target/stm32f1x.cfg -c "program app.bin 0x08000000 verify reset exit"注意bin文件需要在program命令里指定起始地址,elf文件不需要,因为elf本身就包含加载地址的信息。4.2 编译时要确认的链接地址和格式在嵌入式开发里,一个很隐蔽的问题是:编译出来的elf文件用的地址和OpenOCD默认烧录的地址是否一致。如果CMake里链接脚本设置的FLASH起始地址不是0x08000000(比如跑Bootloader时是0x08010000),你直接用OpenOCD烧elf也能正确写入,因为elf里有地址信息。但如果你手动把elf转成bin,然后烧录时写错地址,程序肯定跑不起来。我建议能写elf就直接写elf,少一步转换就少一个出错点。如果你的构建流程里已经用objcopy生成了bin,那务必确认命令行里给的地址和链接脚本一致。可以用arm-none-eabi-objdump -h app.elf查看各段的VMA地址。4.3 烧录过程中常见的权限问题与udev规则我第一次在WSL里跑上面的命令,看到的报错是:Error: open failed Failed to open device: LIBUSB_ERROR_ACCESS意思是没有访问USB设备的权限。当时我明明是root执行的,为什么还不行?后来一查,WSL2默认使用普通的USB设备权限模型,即使root也需要udev规则或正确的dev节点权限。而且,WSL2里系统启动时可能不会加载udev,这会导致设备节点可能没有按预期设置权限。解决办法有两个。一个是直接在命令前加sudo:sudo openocd -f interface/stlink.cfg -f target/stm32f1x.cfg -c "program app.elf verify reset exit"如果OpenOCD是在root下运行,大概率能绕过权限问题。但每次都sudo很麻烦,而且如果你的交叉编译器、OpenOCD都需要root,那CMake设置烧录脚本时也会别扭。我更推荐把当前用户加入dialout组和plugdev组:sudo usermod -aG dialout $USER sudo usermod -aG plugdev $USER然后添加一条udev规则,让ST-Link设备的权限自动放宽。创建文件/etc/udev/rules.d/99-stlink.rules,内容如下:SUBSYSTEM=="usb", ATTR{idVendor}=="0483", ATTR{idProduct}=="3748", MODE="0666", GROUP="plugdev"这里0483是ST的USB厂商ID,3748是ST-Link的常见产品ID。如果你的板载ST-Link是其他ID,可以用lsusb查一下。写完规则后执行:sudo udevadm control --reload-rules然后在WSL里重新插拔设备(或者用usbipd的detach/attach重新挂载一次),再运行openocd,就不再需要sudo了。4.4 芯片配置匹配以及Reset Mode引发的烧录失败还有一个高概率翻车点:OpenOCD的target配置和芯片型号不匹配。比如你的板子是STM32F103,烧录配置里写成stm32f4x.cfg,OpenOCD虽然能连上调试器,但和芯片握手时就会报识别不到IDCODE的错误。我还遇到过连接不上、一直卡在Error: target not halted的情况,排查到最后发现是目标芯片进入了某种模式。这时候的通用解救方法是:板子上找到BOOT0引脚,拉到高电平,让芯片进入系统存储器模式,然后重新上电,再烧录一次。因为系统存储器模式下的芯片不会跑用户代码,也不会设置调试引脚复用,调试器比较容易控制。很多板子上BOOT0就是一个小跳线或拨码开关,拨到1位置就是高电平。烧录成功后再拨回来,复位一下,程序就跑起来了。4.5 一个容易让人误判的坑:ST-Link固件版本过旧如果你用的是那种条形的ST-Link/V2或者板载ST-Link,可能会遇到OpenOCD报错说固件版本太低。以前Windows下用官方烧录工具时,它会自动提示升级固件。在WSL下,没有那个GUI工具,但可以在Windows侧下载ST-Link固件升级工具,先在Windows环境里升级一次。升级完再透传到WSL,OpenOCD就能正常识别。这个坑特别容易让人误以为是WSL的USB透传失败,从而把排查方向搞乱。我自己就浪费了半小时,反复detach、attach,结果发现是固件版本问题。所以建议一开始就把ST-Link的固件升级到最新,减少干扰项。5. 把烧录流程固化到日常开发里:脚本、自动化和调试5.1 写一个烧录脚本,顺手解决"板子又不见了"的问题既然烧录命令这么长,最好把它封装成脚本。这里提供一个简单的bash脚本,放到项目根目录的flash.sh里:#!/bin/bash set -e BOARD_CFG=${TARGET_CFG:-stm32f1x.cfg} ELF_FILE=${1:-build/app.elf} if ! lsusb | grep -q "0483"; then echo "[ERROR] 没有检测到ST-Link设备,请确认USB透传是否成功。" echo "尝试执行: usbipd attach --wsl --busid <busid>" exit 1 fi openocd -f interface/stlink.cfg -f target/${BOARD_CFG} \ -c "program ${ELF_FILE} verify reset exit"脚本里的if判断很实用:每次烧录前先检测lsusb中是否存在ST-Link设备,如果不存在,就给出提示,提醒你执行attach命令。别小看这个判断,它能把"我板子呢?"这个灵魂拷问从每天一次变成偶尔几次。配合CMake,可以加一个自定义目标:add_custom_target(flash COMMAND ${CMAKE_COMMAND} -E env TARGET_CFG=stm32f4x.cfg bash ${CMAKE_SOURCE_DIR}/flash.sh $<TARGET_FILE:app> DEPENDS app )这样执行make flash即可完成编译+烧录。5.2 串口监视也必须切到WSL里烧录完程序,紧接着要做的事就是看串口输出。如果你的板子通过USB转串口接到电脑,透传之后在WSL里会显示为/dev/ttyACM0或/dev/ttyUSB0。直接用cat或者minicom:cat /dev/ttyACM0如果提示权限不足,同样需要把用户加到dialout组,或者用sudo。串口参数要注意,默认的波特率通常是115200,但cat命令不会设置波特率,内核默认使用9600,所以用cat直接读可能乱码。建议用stty设置:stty -F /dev/ttyACM0 115200 raw -echo cat /dev/ttyACM0也可以安装minicom或者picocom,体验更好。这里有一个在WSL下很常见的附加问题:如果ST-Link的VCP和调试器是一个复合设备,透传时会同时出现两个USB设备,你需要选择正确的那个。比如某款探索板板载的ST-Link会枚举出一个ST-Link接口和一个VCP串口接口,分别对应不同的busid。attach的时候要选择包含串口的那个,否则你在WSL里找不到ttyACM0。5.3 调试器也不落下:OpenOCD + GDB远程调试烧录只是第一步,嵌入式的重头戏其实是调试。OpenOCD本身自带一个GDB服务器,烧录完成之后不要退出,而是让它继续监听端口。启动命令可以写成:openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "program app.elf verify reset halt" &注意这里用的是halt而不是exit,烧录后会停下来,等待GDB连接。然后用另一个终端启动arm-none-eabi-gdb:arm-none-eabi-gdb app.elf (gdb) target remote localhost:3333 (gdb) monitor reset halt (gdb) load (gdb) continue这样一来,在WSL里也可以获得真正的调试体验,单步、断点、查看寄存器都行。如果你前端用编辑器里的remote配置,体验还会更好。我现在的习惯是:在编辑器里配一个WSL工具链,烧录和调试都在WSL里完成,Windows端的图形烧录工具基本退役了。5.4 关于USB透传后板子失去响应的那些玄学最后聊几个我实际遇到过、容易让人抓狂的边角问题。一是Windows侧设备管理器里ST-Link设备消失。这其实是bind之后正常现象,设备已经被标记为共享,Windows上不再显示。如果你发现Windows侧也找不到设备,执行usbipd detach --busid <BUSID>把它释放回来,它就会重新出现在Windows上。如果你绑定了设备但忘记attach,Windows里也会看不到,这不代表设备坏了。二是WSL2里断电重启后,要重新attach。如果你用auto-attach,可以少一步,但Linux内核模块如果没加载,usbip attach会报错。此时执行:sudo modprobe vhci-hcd将vhci-hcd模块加载上去,再attach。WSL2默认模块是有的,但偶尔没自动加载。三是烧录大固件时OpenOCD突然卡死。我遇到过几次,最后发现是USB线质量太差,电流不稳导致ST-Link掉线。这问题无解,只能换一根带屏蔽的短线。对了,尽量别用电脑前置USB口,供电弱的时候烧录到一半失败的概率很大。这些事看起来琐碎,但每一个都能让人对着终端发半小时呆。把这条链路理顺之后,你会发现用WSL做嵌入式开发是真的舒服:编译在Linux侧,工具链完全自由,烧录调试也能在同一个终端里完成,不再需要来回切换Windows软件。我现在坐在工位上,开发板安安静静地插在USB口上,WSL里的终端已经准备好了。那句"我板子呢?"最近出现的频率越来越低,因为脚本和attach别称已经替我记住了所有环节。希望这一篇也能让你在WSL里顺利找到自己的板子。
"我板子呢?"这个问题,在过去的几周里几乎成了我每天坐在工位前的第一句话。不是因为爱丢东西,而是因为自从我彻底切到WSL环境下写STM32的嵌入式C++代码之后,编译、构建、版本管理都顺得离谱,唯独到了烧录这一步,开发板明明插在电脑上,WSL里却像什么都没发生一样。这大概是所有端着WSL做嵌入式的人都会撞上的墙:IDE里的烧录按钮在Linux侧使不上劲,Windows侧的烧录工具又不认识你用CMake折腾出来的那些二进制。所以这篇就来聊聊,在WSL环境里到底怎么才能把手里的STM32开发板顺利地烧录起来,把这句“我板子呢”变成“板子在WSL里好好待着呢”。按照这个系列的进度,到这一步其实已经能写出能在片子上跑起来的C++代码了,但代码写完总得烧进去看现象。网上关于WSL烧录的帖子不少,但大多零散,有的说WSL1直接开串口,有的说用网络调试器,看完更糊涂。我花了两天时间把自己踩过的坑和最终跑通的方案完整梳理了一遍,这里面包括USB设备怎么透传、OpenOCD怎么装、烧录命令怎么敲、烧录失败怎么排查,还有几个不看一眼文档绝对发现不了的隐藏问题。这篇尽量讲透,让后面的人不用再经历一次"板子失踪案"。1. 起因:开发板明明在USB口上,WSL里却像没插一样1.1 "我板子呢"的经典重现先描述一个大多数人都会遇到的场景:你在Windows下用烧录工具成功烧了一次,确认板子能跑,然后想回WSL里改代码。写完代码、编译、生成了.bin,拿起线准备烧,这时WSL终端里的lsusb输出光秃秃的,只有那个虚拟的xHCI host controller,根本没有ST-Link的影子。你会下意识地摸一下USB线,发现板子的电源灯亮着,于是你怀疑是不是线坏了,换一根线,还是不行。然后你打开Windows设备管理器,板子好好的,在"端口"和"通用串行总线设备"下都看得见。这时候就会冒出那句经典的"我板子呢?"。这不是板子丢了,而是WSL2和Windows之间存在一层"隔离墙"。WSL2本质上是跑在Hyper-V虚拟机里的一个轻量Linux,它拥有独立的Linux内核和独立的USB设备子系统,Windows上插着的USB设备并不会自动"透传"过去。WSL1倒是没有这堵墙,它直接翻译Windows系统调用,所以你可以通过/dev/ttyS0之类的方式访问串口,但WSL1又不能跑很多嵌入式工具链要用的Linux内核特性。这让人很难受。1.2 是WSL1还是WSL2,决定了你接下来的路径很多人一上来就找"WSL烧录STM32"的教程,结果发现旧教程里写着用WSL1直接访问COM口,于是照着操作,在WSL2里怎么都找不到设备,开始怀疑人生。我们先把这个概念掰清楚:如果你用的是WSL1,那么宿主机上的COM口会以串口设备的形式映射到Linux里,比如/dev/ttyS0,配合一个Windows端的USB转UART桥接,确实可以在WSL1里操作串口。但WSL2不行,WSL2是真正的虚拟化环境,它不能直接看到宿主机USB设备。从当前主流的开发习惯来看,大部分人的WSL都升级到了WSL2(通过wsl --set-version命令可以查到),因为WSL2的I/O性能和兼容性更好。所以这篇文章的主角是WSL2下的烧录方案。顺带说一句,如果你还在用WSL1,其实不太建议为烧录单独迁回WSL1,因为后面你会遇到更多边边角角的问题,不如一次性把USB透传搞定,以后调试器、串口、烧录器都能一并复用。1.3 为什么非要跟WSL较劲,Windows下烧录不香吗每次有人听说我在WSL里折腾烧录,第一反应都是:你这不是自找麻烦?Windows下打开烧录软件点一下不就完事了。确实,如果一天就烧一次,Windows下点几下没毛病。但我的实际痛点是:代码已经全切到WSL侧的CMake工程里了,编译、链接、代码格式化、静态检查全在Linux侧,每次改完代码切到Windows去烧录,中间要经历"拷贝文件、找工具、选文件、点烧录"这一串重复劳动,稍微多烧几次就烦了。更别说我想在脚本里实现"编译通过后自动烧录"的流程,Windows GUI完全没法配合。把烧录迁到WSL里,得到的是一整套命令行工作流:改代码、编译、烧录、看串口,全在同一个终端里完成。脚本化之后,以后同事拿到工程只要执行一条命令,板子就亮起来了。这种体验上的提升,值得花一次功夫把USB透传和研究清楚。这也是我写这篇的初衷。2. WSL2下的USB穿透方案:usbipd-win的正确使用姿势2.1 USB/IP是什么,为什么选它WSL2不能直通USB,那怎么把板子交到WSL手里?最成熟、社区验证最多的方案是使用usbipd-win加上Linux内核里的usbip客户端。usbip的全称是USB over IP,它把一个USB设备从一台机器"分享"给另外一台机器,Windows这边作为USB设备共享服务器,WSL2作为客户端接收设备。听起来很复杂,其实安装和操作只要几步。注意,这里说的不是虚拟机的"USB直通"功能,而是更灵活的设备共享协议。为什么不用Hyper-V的增强会话或Windows自带的USB重定向?因为那些是给完整虚拟机用的,WSL2没有图形界面管理工具,直接在命令行里敲命令更顺手。usbipd-win从很早就开始维护,目前版本比较稳定,配合WSL2的内核可以直接工作。我自己用下来感觉最舒服的一点是它支持--auto-attach参数,可以设定某个设备在插入时自动挂到WSL2里,不需要每次手动attach,省去了很多麻烦。2.2 Windows侧:安装、绑定、attach的完整命令先来看Windows侧要做什么。在Windows 11或较新的Windows 10上,你可以通过winget直接安装usbipd-win,命令如下:winget install usbipd装完之后,PowerShell或者CMD需要以管理员身份运行,因为后面要操作USB设备。先列出当前计算机上的USB设备:usbipd list输出会显示一个BUSID列表,每行就是一个USB设备,一列是设备描述。比如你的STM32板载ST-Link会显示类似"STMicroelectronics STLink dongle"这样的名字。找到之后,把它绑定(bind)。绑定的意思是让这个设备可以被WSL共享出来,命令格式是:usbipd bind --busid <BUSID>如果希望以后每次插入这个设备时自动挂到某个WSL发行版,可以加一个auto-attach参数:usbipd bind --busid <BUSID> --auto-attach绑定之后,设备在Windows里就会被"摘下来",好像被拔掉一样,因为它已经准备好交给WSL了。然后执行attach命令:usbipd attach --wsl --busid <BUSID>如果只有一个WSL发行版,直接写--wsl即可。如果有多个发行版,用--wsl <发行版名字>指定。attach成功之后,设备就会出现在WSL2里。此时在WSL终端里执行lsusb,应该能看到对应的设备了。需要特别提醒的是,每次重启Windows或者重启WSL,attach状态都会丢失,需要重新执行attach。auto-attach也不是完全万能,它只处理"插入时自动attach",如果重启之后设备已经插着,还是需要手动attach一次。我习惯把attach命令写成一个alias,这样重新进入WSL后一条命令就能恢复。2.3 WSL侧:usbip客户端工具的配置WSL2的Linux内核默认带usbip支持,但还需要安装用户空间工具,不同发行版命令略有不同。Ubuntu/Debian下执行:sudo apt update sudo apt install linux-tools-generic usbip hwdata注意,linux-tools-generic这个包安装之后,usbip命令可能在/usr/lib/linux-tools/<内核版本>/usbip路径下,不在PATH里。你可以直接找到它,或者做一个软链接:sudo ln -s /usr/lib/linux-tools/$(uname -r)/usbip /usr/local/bin/usbip其实在我们执行attach时,通常直接用usbipd的命令就行,因为usbipd-win也就是调用了usbip协议。但如果想手动debug,可以用usbip list -r <Windows主机的IP>来查看远程设备列表。一般用不上,知道有这回事就行。装好之后,在WSL里运行lsusb,看到类似"STMicroelectronics ST-LINK"的就是设备到达了。如果设备对应的是USB转串口,在lsusb里也会显示。接下来就可以在WSL里直接访问这个设备。2.4 多发行版、多设备场景下的管理细节如果你装了多个WSL发行版(比如一个Ubuntu用于日常开发,一个Debian用来做实验),attach命令必须指定目标发行版,否则usbipd-win不知道往哪儿送。每次attach都要写发行版名字,很容易写错。我自己的做法是,给每个发行版写一个不同的脚本,脚本里exportWSL_DISTRO_NAME再执行attach。这样就算发行版多也不会乱。还有一个细节:lsusb看到的设备可能有多个。比如一块ST官方开发板,板载ST-Link和USB虚拟串口其实是两个USB功能,枚举出来是两个独立设备。如果你把带调试功能的那个设备attach进去了,但串口功能那个设备还留在Windows里,WSL里就看不到/dev/ttyACM0。所以attach之前先确认一下,究竟哪个busid对应你需要的功能。用usbipd list输出里的描述信息就能判断。3. 烧录工具链的安装与配置:OpenOCD还是st-flash?3.1 两条路线该怎么选设备能看见了,只是第一步。接下来要选一个WSL里能跑的烧录工具。目前STM32烧录工具几条主流路线:OpenOCD:跨平台开源调试和烧录工具,支持ST-Link、J-Link等多种调试器,社区活跃,支持大量目标芯片,适合复杂调试和自动化烧录。st-flash(stlink-tools):ST-Link专用的命令行烧录工具,轻量,命令简单,适合快速烧录,但只支持ST-Link。官方图形烧录工具:ST官方工具,Linux版CLI也能跑,但在WSL2里权限问题处理起来比较别扭,而且重。我做嵌入式开发的时候比较喜欢OpenOCD,因为它不仅能烧录,还能配合gdb做调试,这一篇烧录,下一篇调试也用得上。st-flash更简单直观,适合不折腾的人。下面我把两条路都列出来,你按自己的风格选。3.2 OpenOCD的安装与基础验证在Ubuntu下安装OpenOCD很方便:sudo apt install openocd但这里有个坑:Ubuntu官方仓库里的OpenOCD版本可能偏旧,而新出的STM32系列芯片可能需要更新的OpenOCD才有支持。如果你用的是比较常规的STM32F1/F4,仓库版足够。如果用的是新款芯片,建议从源码编译。源码编译也不复杂:sudo apt install libtool autoconf automake pkg-config libusb-1.0-0-dev git clone https://github.com/openocd-org/openocd.git cd openocd ./bootstrap ./configure --prefix=/usr/local make -j$(nproc) sudo make install编译安装之后,检查版本:openocd --version与此同时,还需要安装arm的交叉编译器。如果你的项目里已经用了CMake,可能已经装了。没装的话:sudo apt install gcc-arm-none-eabi装完跑一下:arm-none-eabi-gcc --version3.3 st-flash的安装与使用st-flash是ST-Link工具集里的命令行烧录工具,在Ubuntu下也可以直接装:sudo apt install stlink-tools安装完成后,可以用st-info --probe来查看当前连接的ST-Link设备。如果设备识别正常,会显示芯片型号和容量。烧录的命令很简单:st-flash write firmware.bin 0x08000000st-flash只能写bin文件,不能直接写elf,所以需要先把编译产物转化为bin格式。OpenOCD则可以直接写elf,比较省事。从这一点来说,我用OpenOCD更多。3.4 为什么我不建议在WSL里用官方GUI烧录工具官方烧录工具的Linux版本虽然提供了CLI,但用起来有两个问题。第一,它依赖特定的图形库,WSL2如果没有启用WSLg,跑起来可能报找不到显示器的错,虽然CLI模式不需要GUI,但安装包体积和依赖确实不小。第二,它对USB设备的访问默认走的是自己的驱动栈,在WSL2的透传环境下,偶尔会出现检测不到设备的情况,还得去改配置。相比之下,OpenOCD直接走libusb,跟usbipd透传过来的设备配合得很自然。当然,如果你在Windows下已经习惯用官方工具,也没必要非换不可。但从自动化角度讲,CLI工具越轻量越好。我现在的工程就是CMake里加一个自定义目标,编译完直接调OpenOCD,一步到位。3.5 在WSL里验证设备权限的快速检查不管用哪个工具,第一步都要确认WSL能看到设备。插上板子,在WSL里执行:lsusb如果你看到类似1689:5730 STMicroelectronics这样的设备ID,说明透传成功。但即便看到了,工具可能还是提示libusb_open failed或者permission denied。这是因为Linux对USB设备有权限控制,普通用户不能直接访问。解决办法是配置udev规则,把当前用户加入dialout组,并让ST-Link设备允许用户读写。在WSL2里,udev规则也是生效的,但需要额外注意WSL的init系统处理方式,后面我会专门讲这个坑。4. 烧录实操:从编译到OpenOCD烧录的完整链路4.1 一次最小的烧录命令假设你的工程已经用CMake编译出了一个elf文件,叫app.elf。在WSL里用OpenOCD烧录,命令如下:openocd -f interface/stlink.cfg -f target/stm32f1x.cfg -c "program app.elf verify reset exit"其中-f指定接口配置和目标芯片配置。interface/stlink.cfg是ST-Link接口配置文件,target/stm32f1x.cfg是STM32F1系列的目标芯片配置。如果你用的是F4,改成stm32f4x.cfg即可。命令行的含义是:启动OpenOCD,加载ST-Link接口和芯片配置,然后执行program命令烧录app.elf,烧完校验(verify),复位(reset),退出(exit)。这一条命令是日常使用频率最高的,没有之一。如果烧录的是bin文件,也可以:openocd -f interface/stlink.cfg -f target/stm32f1x.cfg -c "program app.bin 0x08000000 verify reset exit"注意bin文件需要在program命令里指定起始地址,elf文件不需要,因为elf本身就包含加载地址的信息。4.2 编译时要确认的链接地址和格式在嵌入式开发里,一个很隐蔽的问题是:编译出来的elf文件用的地址和OpenOCD默认烧录的地址是否一致。如果CMake里链接脚本设置的FLASH起始地址不是0x08000000(比如跑Bootloader时是0x08010000),你直接用OpenOCD烧elf也能正确写入,因为elf里有地址信息。但如果你手动把elf转成bin,然后烧录时写错地址,程序肯定跑不起来。我建议能写elf就直接写elf,少一步转换就少一个出错点。如果你的构建流程里已经用objcopy生成了bin,那务必确认命令行里给的地址和链接脚本一致。可以用arm-none-eabi-objdump -h app.elf查看各段的VMA地址。4.3 烧录过程中常见的权限问题与udev规则我第一次在WSL里跑上面的命令,看到的报错是:Error: open failed Failed to open device: LIBUSB_ERROR_ACCESS意思是没有访问USB设备的权限。当时我明明是root执行的,为什么还不行?后来一查,WSL2默认使用普通的USB设备权限模型,即使root也需要udev规则或正确的dev节点权限。而且,WSL2里系统启动时可能不会加载udev,这会导致设备节点可能没有按预期设置权限。解决办法有两个。一个是直接在命令前加sudo:sudo openocd -f interface/stlink.cfg -f target/stm32f1x.cfg -c "program app.elf verify reset exit"如果OpenOCD是在root下运行,大概率能绕过权限问题。但每次都sudo很麻烦,而且如果你的交叉编译器、OpenOCD都需要root,那CMake设置烧录脚本时也会别扭。我更推荐把当前用户加入dialout组和plugdev组:sudo usermod -aG dialout $USER sudo usermod -aG plugdev $USER然后添加一条udev规则,让ST-Link设备的权限自动放宽。创建文件/etc/udev/rules.d/99-stlink.rules,内容如下:SUBSYSTEM=="usb", ATTR{idVendor}=="0483", ATTR{idProduct}=="3748", MODE="0666", GROUP="plugdev"这里0483是ST的USB厂商ID,3748是ST-Link的常见产品ID。如果你的板载ST-Link是其他ID,可以用lsusb查一下。写完规则后执行:sudo udevadm control --reload-rules然后在WSL里重新插拔设备(或者用usbipd的detach/attach重新挂载一次),再运行openocd,就不再需要sudo了。4.4 芯片配置匹配以及Reset Mode引发的烧录失败还有一个高概率翻车点:OpenOCD的target配置和芯片型号不匹配。比如你的板子是STM32F103,烧录配置里写成stm32f4x.cfg,OpenOCD虽然能连上调试器,但和芯片握手时就会报识别不到IDCODE的错误。我还遇到过连接不上、一直卡在Error: target not halted的情况,排查到最后发现是目标芯片进入了某种模式。这时候的通用解救方法是:板子上找到BOOT0引脚,拉到高电平,让芯片进入系统存储器模式,然后重新上电,再烧录一次。因为系统存储器模式下的芯片不会跑用户代码,也不会设置调试引脚复用,调试器比较容易控制。很多板子上BOOT0就是一个小跳线或拨码开关,拨到1位置就是高电平。烧录成功后再拨回来,复位一下,程序就跑起来了。4.5 一个容易让人误判的坑:ST-Link固件版本过旧如果你用的是那种条形的ST-Link/V2或者板载ST-Link,可能会遇到OpenOCD报错说固件版本太低。以前Windows下用官方烧录工具时,它会自动提示升级固件。在WSL下,没有那个GUI工具,但可以在Windows侧下载ST-Link固件升级工具,先在Windows环境里升级一次。升级完再透传到WSL,OpenOCD就能正常识别。这个坑特别容易让人误以为是WSL的USB透传失败,从而把排查方向搞乱。我自己就浪费了半小时,反复detach、attach,结果发现是固件版本问题。所以建议一开始就把ST-Link的固件升级到最新,减少干扰项。5. 把烧录流程固化到日常开发里:脚本、自动化和调试5.1 写一个烧录脚本,顺手解决"板子又不见了"的问题既然烧录命令这么长,最好把它封装成脚本。这里提供一个简单的bash脚本,放到项目根目录的flash.sh里:#!/bin/bash set -e BOARD_CFG=${TARGET_CFG:-stm32f1x.cfg} ELF_FILE=${1:-build/app.elf} if ! lsusb | grep -q "0483"; then echo "[ERROR] 没有检测到ST-Link设备,请确认USB透传是否成功。" echo "尝试执行: usbipd attach --wsl --busid <busid>" exit 1 fi openocd -f interface/stlink.cfg -f target/${BOARD_CFG} \ -c "program ${ELF_FILE} verify reset exit"脚本里的if判断很实用:每次烧录前先检测lsusb中是否存在ST-Link设备,如果不存在,就给出提示,提醒你执行attach命令。别小看这个判断,它能把"我板子呢?"这个灵魂拷问从每天一次变成偶尔几次。配合CMake,可以加一个自定义目标:add_custom_target(flash COMMAND ${CMAKE_COMMAND} -E env TARGET_CFG=stm32f4x.cfg bash ${CMAKE_SOURCE_DIR}/flash.sh $<TARGET_FILE:app> DEPENDS app )这样执行make flash即可完成编译+烧录。5.2 串口监视也必须切到WSL里烧录完程序,紧接着要做的事就是看串口输出。如果你的板子通过USB转串口接到电脑,透传之后在WSL里会显示为/dev/ttyACM0或/dev/ttyUSB0。直接用cat或者minicom:cat /dev/ttyACM0如果提示权限不足,同样需要把用户加到dialout组,或者用sudo。串口参数要注意,默认的波特率通常是115200,但cat命令不会设置波特率,内核默认使用9600,所以用cat直接读可能乱码。建议用stty设置:stty -F /dev/ttyACM0 115200 raw -echo cat /dev/ttyACM0也可以安装minicom或者picocom,体验更好。这里有一个在WSL下很常见的附加问题:如果ST-Link的VCP和调试器是一个复合设备,透传时会同时出现两个USB设备,你需要选择正确的那个。比如某款探索板板载的ST-Link会枚举出一个ST-Link接口和一个VCP串口接口,分别对应不同的busid。attach的时候要选择包含串口的那个,否则你在WSL里找不到ttyACM0。5.3 调试器也不落下:OpenOCD + GDB远程调试烧录只是第一步,嵌入式的重头戏其实是调试。OpenOCD本身自带一个GDB服务器,烧录完成之后不要退出,而是让它继续监听端口。启动命令可以写成:openocd -f interface/stlink.cfg -f target/stm32f4x.cfg -c "program app.elf verify reset halt" &注意这里用的是halt而不是exit,烧录后会停下来,等待GDB连接。然后用另一个终端启动arm-none-eabi-gdb:arm-none-eabi-gdb app.elf (gdb) target remote localhost:3333 (gdb) monitor reset halt (gdb) load (gdb) continue这样一来,在WSL里也可以获得真正的调试体验,单步、断点、查看寄存器都行。如果你前端用编辑器里的remote配置,体验还会更好。我现在的习惯是:在编辑器里配一个WSL工具链,烧录和调试都在WSL里完成,Windows端的图形烧录工具基本退役了。5.4 关于USB透传后板子失去响应的那些玄学最后聊几个我实际遇到过、容易让人抓狂的边角问题。一是Windows侧设备管理器里ST-Link设备消失。这其实是bind之后正常现象,设备已经被标记为共享,Windows上不再显示。如果你发现Windows侧也找不到设备,执行usbipd detach --busid <BUSID>把它释放回来,它就会重新出现在Windows上。如果你绑定了设备但忘记attach,Windows里也会看不到,这不代表设备坏了。二是WSL2里断电重启后,要重新attach。如果你用auto-attach,可以少一步,但Linux内核模块如果没加载,usbip attach会报错。此时执行:sudo modprobe vhci-hcd将vhci-hcd模块加载上去,再attach。WSL2默认模块是有的,但偶尔没自动加载。三是烧录大固件时OpenOCD突然卡死。我遇到过几次,最后发现是USB线质量太差,电流不稳导致ST-Link掉线。这问题无解,只能换一根带屏蔽的短线。对了,尽量别用电脑前置USB口,供电弱的时候烧录到一半失败的概率很大。这些事看起来琐碎,但每一个都能让人对着终端发半小时呆。把这条链路理顺之后,你会发现用WSL做嵌入式开发是真的舒服:编译在Linux侧,工具链完全自由,烧录调试也能在同一个终端里完成,不再需要来回切换Windows软件。我现在坐在工位上,开发板安安静静地插在USB口上,WSL里的终端已经准备好了。那句"我板子呢?"最近出现的频率越来越低,因为脚本和attach别称已经替我记住了所有环节。希望这一篇也能让你在WSL里顺利找到自己的板子。