嵌入式C++教程实战之Linux下的单片机编程:从零搭建 STM32 开发工具链(3)WSL2 USB 透传,让 ST-Link 穿越虚拟化边界
嵌入式C教程实战之Linux下的单片机编程从零搭建 STM32 开发工具链3WSL2 USB 透传让 ST-Link 穿越虚拟化边界PS之前的标号搞错了不过没事整个是独立的章节所以换到3来了相关仓库依旧已经开源https://github.com/Awesome-Embedded-Learning-Studio/Tutorial_AwesomeModernCPP前言这是整个折腾路上最大的坑如果你一路跟着前面的教程走过来现在你的 WSL2 环境里已经有了 ARM 工具链有了 OpenOCD甚至可能已经编译出了你的第一个固件文件。当你兴冲冲地插上 ST-Link 调试器准备把程序烧录到 STM32 里面时现实会给你当头一棒——WSL2 根本看不到 USB 设备。我现在正在经历这个阶段lsusb 的输出里空空如也不要说什么 ST-Link连个鼠标都看不到。这不是你操作的问题这是 WSL2 架构的先天缺陷。WSL2 用的是 Hyper-V 虚拟化技术Linux 作为一个真正的虚拟机跑在 Windows 下面但微软并没有把 USB 设备透传的功能做进去。你的 ST-Link 插在 Windows 的 USB 口上被 Windows 驱动接管了Linux 那边完全不知道它的存在。这个问题困扰了我好几天。我在网上找各种资料有人推荐用虚拟机方案有人说直接放弃 WSL2 装原生 Ubuntu。但我不想放弃因为 WSL2 的其他部分实在太方便了——和 Windows 文件系统集成、终端体验、包管理这些都是原生 Linux 难以企及的。最终我找到了 usbipd-win 这个项目它是微软官方维护的工具专门用来解决 WSL2 的 USB 透传问题。今天我们就把这个坑彻底填平让 ST-Link 能够顺利地从 Windows 穿越到 WSL2然后完成你的第一次 OpenOCD 烧录。WSL2 的 USB 问题到底是怎么回事让我们先理解清楚问题的根源。WSL2 虽然感觉上像是 Windows 里的一个 Linux 程序但它实际上是一个完整的虚拟机。当你打开 WSL2 终端时你是在和一个名为 “WSL” 的 Hyper-V 虚拟机交互。这个虚拟机有自己的内核、自己的内存管理、自己的设备树。USB 设备在 PC 架构里是由主机控制器管理的你的主板有好几个 USB 控制器每个控制器下面挂着多个 USB 口。当一个 USB 设备插入时控制器会给它分配一个地址然后操作系统加载相应的驱动程序来和这个设备通信。问题在于 WSL2 的虚拟机里USB 控制器是虚拟的它连接不到物理的 USB 控制器所以物理插入的设备对 WSL2 来说是不可见的。Windows 主机能够看到你的 ST-Link设备管理器里也正常识别了但 WSL2 的 Linux 内核看不到。这就是为什么我们需要一个透传机制把 Windows 看到的 USB 设备借给 WSL2 用。usbipd-win 就是做这个事情的它实现了 USB/IP 协议可以让 USB 设备通过网络协议栈从一台机器传输到另一台机器。在 WSL2 的场景下就是从 Windows 传输到 WSL2 这个虚拟机器。现在让我们开始配置。Windows 侧安装和配置 usbipd-win首先你要确保你用的是 WSL2 而不是 WSL1。WSL1 是个翻译层它直接用 Windows 的内核所以 USB 问题在 WSL1 里根本不存在——但 WSL1 也有很多其他限制比如不支持 Docker所以大部分人现在都用 WSL2。你可以在 PowerShell 里用wsl --list --verbose确认一下如果你的版本是 1.x那需要升级到 2。接下来我们安装 usbipd-win。这个工具在微软的官方包管理器 winget 上有安装非常简单。打开一个管理员权限的 PowerShell 终端注意一定要管理员权限因为 USB 设备的操作需要特权。执行winget install usbipd安装完成后你应该可以用usbipd命令了。现在先查看一下系统中有哪些 USB 设备usbipd list这个命令会列出所有 USB 设备你会看到长长的列表包括你的鼠标、键盘、摄像头等等。每个设备都有一个 BUSID格式是类似 “1-5” 或 “2-3” 这样的。你的 ST-Link 应该也在列表里可能显示为 “STMicroelectronics ST-LINK…” 或者类似的名称。记住它的 BUSID比如我的显示为 “1-8”。接下来你需要把这个设备绑定到 usbipd-win。绑定是一个只需要做一次的操作它告诉 Windows 这个设备以后可以被透传。绑定之后设备在 Windows 设备管理器里会消失它的驱动会被卸载转而由 usbipd-win 接管。执行绑定命令usbipd bind--busid 1-8把1-8替换成你实际看到的 BUSID。如果成功你会看到确认信息。现在设备已经从 Windows 的视野里消失了你可以在设备管理器里确认一下ST-Link 那一项应该已经不见了。但这时候 WSL2 还看不到设备因为绑定只是准备工作你还需要把设备附接到 WSL2。这个 attach 操作是每次重启 WSL2 或者重新插拔设备后都需要做的。让我们执行usbipd attach--wsl--busid 1-8这个命令会把设备通过 USB/IP 协议传输到 WSL2。--wsl参数指定目标是我们默认的 WSL 发行版。现在设备应该已经出现在 WSL2 里了。bind 和 attach 的区别很重要bind 是一次性操作告诉 Windows “这个设备以后可以被透传”而 attach 是每次都要做的相当于我现在把这个设备连接到 WSL2。你重启电脑后 bind 状态会保留但 attach 会丢失需要重新执行。Linux 侧验证设备透传现在回到你的 WSL2 终端。你可以用lsusb命令查看 USB 设备列表lsusb|grep-istlink如果一切顺利你应该看到类似这样的输出Bus 001 Device 005: ID 0483:3748 STMicroelectronics ST-LINK/V2或者可能是0483:374b这取决于你的 ST-Link 版本。V2 版本是 3748V2-1 是 374b但这对 OpenOCD 来说区别不大它两个都支持。设备号信息在这行输出里很重要Bus 001 Device 005意味着这个设备在/dev/bus/usb/001/005。这个设备节点文件是我们后续要用来访问 ST-Link 的接口。现在我们让 WSL2 能够访问这个设备。在原生 Linux 系统里你通常会配置 udev 规则让系统自动给 USB 设备设置正确的权限。但在 WSL2 里udev 默认是不工作的——WSL2 启动时会跳过 udev 的服务启动这导致 udev 规则根本不会生效。这是 WSL2 的另一个坑。你可以尝试创建 udev 规则文件/etc/udev/rules.d/49-stlinkv2.rules内容是# STM32 ST-LINK/V2 SUBSYSTEMusb, ATTR{idVendor}0483, ATTR{idProduct}3748, MODE0666 # STM32 ST-LINK/V2-1 SUBSYSTEMusb, ATTR{idVendor}0483, ATTR{idProduct}374b, MODE0666然后在原生 Ubuntu 上你需要sudo udevadm control --reload-rules sudo udevadm trigger来重新加载规则。但在 WSL2 里这些命令可能不会有任何效果因为 udev 服务根本没在跑。所以我们需要另一个办法手动修改设备权限。WSL 下的权限处理那个令人崩溃的 LIBUSB_ERROR_ACCESS当你第一次尝试用 OpenOCD 连接 ST-Link 时你很可能会遇到LIBUSB_ERROR_ACCESS错误。这个错误的意思很明确OpenOCD 没有权限访问/dev/bus/usb/001/005这个设备文件。解决方法很简单粗暴用 sudo 修改权限sudochmod666/dev/bus/usb/001/005但问题在于每次你重新 attach USB 设备后设备号可能会变。有时候 ST-Link 是 Device 005下次重启 WSL2 后可能变成 Device 006。所以手动输命令很麻烦我们需要一个自动化脚本。我写了一个简单的fix_stlink.sh脚本它会自动找到 ST-Link 的设备节点并修改权限#!/bin/bash# 自动修复 ST-Link 权限的脚本# 用 lsusb 找到 ST-Link 设备提取总线号和设备号我这边是类似ST-Link建议你自己lsusb先看看再修一下这个脚本BUSDEV$(lsusb|grep-istlink|awk{print /dev/bus/usb/$2/substr($4,1,3)})if[-z$BUSDEV];thenecho没有找到 ST-Link 设备请先在 Windows 侧执行 usbipd attachexit1fiecho找到 ST-Link 设备:$BUSDEVsudochmod666$BUSDEVecho权限已设置为 666这个脚本的工作原理是用lsusb | grep -i stlink找到 ST-Link 那一行然后用 awk 提取总线号第二列和设备号第四列的前三个字符。substr($4,1,3)这个技巧是因为 lsusb 输出的设备号后面带个冒号比如 “005:”我们只取前三个字符。你可以把这个脚本放在~/bin/目录下添加执行权限chmod x ~/bin/fix_stlink.sh然后每次重新 attach USB 设备后运行一下。或者你可以把它加到你的.bashrc或.zshrc里的某个 alias比如alias fix-stlink~/bin/fix_stlink.sh这样以后只需要输fix-stlink就行了。OpenOCD 烧录实战见证奇迹的时刻现在设备透传了权限也设置了我们可以开始真正烧录固件了。OpenOCD 的配置文件系统非常灵活你需要指定两个配置文件一个是接口配置interface描述你用的什么调试器另一个是目标配置target描述你要烧录什么芯片。对于 ST-Link V2 和 STM32F103C8T6配置文件分别是interface/stlink.cfg— ST-Link 调试器接口target/stm32f1x.cfg— STM32F1 系列芯片OpenOCD 会自动搜索它的配置文件目录通常在/usr/share/openocd/scripts/下所以你不需要写完整路径。最基本的手动烧录命令是这样openocd-finterface/stlink.cfg-ftarget/stm32f1x.cfg\-cprogram firmware.bin verify reset exit 0x08000000让我解释一下这个命令的各个部分。-f参数指定配置文件这里我们指定了两个。-c参数是直接在命令行执行 OpenOCD 的命令而不是用配置文件里的。program firmware.bin告诉 OpenOCD 烧录名为firmware.bin的二进制文件。verify表示烧录后自动校验确保数据写入正确。reset会在烧录完成后复位芯片让它从头开始执行新程序。exit告诉 OpenOCD 做完这些就退出而不是继续监听 GDB 连接。最后的0x08000000是 STM32F103 的 Flash 起始地址这是 ARM Cortex-M 系列的标准地址。如果你需要完全擦除芯片后再烧录比如你之前烧过大程序现在要烧个小程序不擦除的话可能有残留数据可以加个erase命令openocd-finterface/stlink.cfg-ftarget/stm32f1x.cfg\-cflash erase_address 0x08000000 0x20000\-cprogram firmware.bin verify reset exit 0x08000000flash erase_address 0x08000000 0x20000会擦除从 0x08000000 开始的 128KB FlashSTM32F103C8T6 的总容量。0x20000是十六进制换算成十进制正好是 131072 字节 128KB。在实际项目里你不会每次都手动打这么长的命令。用 CMake 的 flash 目标会更方便cmake--buildbuild--targetflash这会在build/目录下找到生成的固件文件自动调用 OpenOCD 烧录。前提是你之前在 CMakeLists.txt 里配置好了 flash 目标具体可以参考前面的教程。常见错误排查当烧录失败时在这个过程中你可能会遇到各种错误让我总结一下最常见的几种和对应的解决方案。LIBUSB_ERROR_ACCESS是最常见的一个表示 OpenOCD 没有权限访问 USB 设备。解决方法就是重新跑一下fix_stlink.sh脚本或者手动sudo chmod 666那个设备节点。如果你重新 attach 了 USB 设备设备号可能变了所以需要重新设置权限。Error: open failed这个错误比较笼统通常意味着 OpenOCD 根本找不到 USB 设备。这时候第一步是确认设备是否成功透传到 WSL2用lsusb | grep -i stlink检查一下。如果看不到设备回到 Windows 侧重新执行usbipd attach --wsl --busid X-X。如果设备在那里但 OpenOCD 还是报错可能是权限问题继续按 LIBUSB_ERROR_ACCESS 的流程排查。Error: unable to find a matching device通常意味着 OpenOCD 的配置文件和实际硬件不匹配。比如你用的实际上是 STM32F4 系列芯片但配置文件写的是stm32f1x.cfg或者你用的是 J-Link 调试器但配置文件写的是stlink.cfg。检查一下你的硬件型号和配置文件是否对应。还有一种情况是 WSL2 完全看不到任何 USB 设备lsusb的输出为空。这时候可能是 usbipd-win 没有正确工作或者 WSL2 的内核模块没有加载。你可以用lsmod | grep usbip在 WSL2 里检查 USB/IP 相关模块是否加载。如果没有加载可以尝试sudo modprobe vhci-hcd但通常 WSL2 的内核配置应该已经包含了这些模块。原生 Ubuntu 用户的简明指南如果你用的是原生 Ubuntu Linux不是 WSL2恭喜你事情要简单得多。你不需要 usbipd-win因为你的 Linux 内核可以直接访问 USB 设备。你只需要配置 udev 规则让系统自动给 ST-Link 设置正确的权限。创建/etc/udev/rules.d/49-stlinkv2.rules文件内容是# STM32 ST-LINK/V2 SUBSYSTEMusb, ATTR{idVendor}0483, ATTR{idProduct}3748, MODE0666, TAGuaccess # STM32 ST-LINK/V2-1 SUBSYSTEMusb, ATTR{idVendor}0483, ATTR{idProduct}374b, MODE0666, TAGuaccess然后重新加载 udev 规则sudoudevadm control --reload-rulessudoudevadm trigger拔插一下 ST-Linkudev 会自动应用新规则。之后你的普通用户账号就能直接访问设备了不需要 sudo 也不需要每次手动修改权限。原生 Linux 的 udev 系统工作得很完善这是它相比 WSL2 的一个优势。结语跨平台的代价折腾完 WSL2 的 USB 透传你现在应该能够在 WSL2 环境里完成完整的 STM32 开发流程了编辑代码、编译固件、烧录芯片一切都在一个统一的环境里进行。虽然 usbipd-win 的 attach 操作有点繁琐但把它写进一个小脚本或者 PowerShell 函数后日常使用也还算方便。WSL2 这个方案本质上是个折中——它让你在 Windows 上获得接近原生 Linux 的开发体验但代价是需要在某些地方绕些弯路。USB 透传只是其中之一后面你可能还会遇到串口设备透传、网络配置等问题。但好消息是这些坑都有解决方案而且一旦配置好了后续的使用就顺畅了。下一篇文章我们会进入真正的嵌入式开发从点灯开始一步步探索 STM32 的外设编程。你会看到现代 C 如何让嵌入式代码变得更简洁、更安全。现在先把你的开发环境彻底打通烧录工具链练熟我们很快就可以开始写真正的代码了。