ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Docker容器中USB设备透传实战:OpenOCD连接ST-Link的完整指南

Docker容器中USB设备透传实战:OpenOCD连接ST-Link的完整指南 最近在整理一套固件自动化烧录方案想把 STM32 的烧录动作搬进 Docker 容器里这样同一个项目里的同事不管用 Windows 还是 Ubuntu都能用同一套 OpenOCD 环境干活。结果容器一启动OpenOCD 连 ST-Link 都认不出来报错还五花八门。折腾了几天把 ST-Link、Docker、OpenOCD 这条链路上的坑基本踩了一遍。如果你也打算把 ST-Link 接进容器或者正被 OpenOCD 各种报错折磨这篇文章应该能帮你省下不少时间。我先把结论性的话放前面问题的核心不在 OpenOCD而在 Docker 默认不会把宿主机上的 USB 设备暴露给容器。只要容器里看不到 ST-Link后面调再多 OpenOCD 参数都是白费。所以这篇文章的主线就是“如何让容器里的 OpenOCD 稳定访问 ST-Link”顺带把 OpenOCD 的配置文件结构、烧录命令、常见报错全部拆开讲一遍。1. 先定位这报错到底是谁抛出来的1.1 三种常见的失败形态很多人一看到 OpenOCD 报错就在 OpenOCD 的命令行上折腾但实际上大部分错误根本轮不到 OpenOCD 报出来。我总结下来Docker ST-Link OpenOCD 的失败形态就三种第一种OpenOCD 进程能启动但 libusb 访问设备失败。OpenOCD 依赖 libusb 访问 USB 设备当容器里没有设备节点或者设备节点权限不够就会抛出类似Error: open failed或libusb_open() failed with LIBUSB_ERROR_ACCESS的错误。第二种OpenOCD 直接说找不到设备。比如Info : attempted to configure interface stlink but the device was not found。这种情况基本可以断定容器里压根就没有 /dev/bus/usb 下的设备节点。第三种OpenOCD 还没参与前面的 IDE 或者脚本先罢工了。典型的就是那句非常著名的cant perform jtag flash, because openocd server is not running!。这句话不是 OpenOCD 打的而是 CLion 之类的工具在启动 OpenOCD 服务失败后给的提示。我见过很多人被第三种报错困住一直去搜 OpenOCD 参数其实先把“容器里能不能看到 ST-Link”这个问题解决后面 80% 的报错都会自动消失。1.2 先在宿主机上确认 ST-Link 是“活的”在碰容器之前先确认宿主机本身能正常识别 ST-Link。这一步很关键不能跳过。很多情况下ST-Link 插在电脑上但驱动不对、线有问题、供电不足宿主机都识别不到后面容器里自然更不可能认识。在 Linux 宿主机上直接跑lsusb正常识别 ST-Link/V2 会看到类似这样的输出不同版本的 ST-Link ID 会略有差异Bus 001 Device 004: ID 0483:3748 STMicroelectronics ST-LINK/V2 Bus 001 Device 005: ID 0483:374b STMicroelectronics ST-LINK/V2-1 Bus 001 Device 006: ID 0483:374f STMicroelectronics ST-LINK/V3如果 lsusb 里看不到任何 0483 开头的设备先别往 Docker 上想优先检查USB 线是不是只带供电没有数据线很多廉价线只连了电源两针。ST-Link 的驱动是否装好Windows 下建议装 ST 官方的 ST-Link USB Driver。如果用了一堆 USB HUB尽量先直接插主板 USB 口排除供电不足。注意ST-Link 上的指示灯亮不代表数据链路正常我遇到过灯亮但 lsusb 完全找不到的情况最后换了一根线就好了。2. 排查链路完整走一遍从 lsusb 到 udev2.1 宿主机能看到容器里看不到如果宿主机 lsusb 已经能看到 0483:3748接下来进入容器里跑一下docker run --rm -it ubuntu:22.04 bash容器内先安装 usbutilsapt-get update apt-get install -y usbutils lsusb大概率你会看到容器里的 lsusb 只有一些虚拟设备根本找不到 ST-Link。原因就是 Docker 默认不挂载宿主机 /dev/bus/usb 目录。容器隔离的本质之一就是设备隔离USB 设备默认不会透传进去。这时候有两种做法一是用--device参数把具体的设备节点映射进去二是直接把整个/dev/bus/usb目录挂进容器。前者更精确但麻烦在于 USB 设备的总线号和设备号是动态的每次重新插拔都可能变化后者更省事我用得最多。最简单的验证方法docker run --rm -it \ --volume /dev/bus/usb:/dev/bus/usb \ ubuntu:22.04 bash进容器后再跑lsusb如果能看到 0483:3748说明设备透传已经通了。但这里往往还会踩第二个坑——权限。2.2 权限问题LIBUSB_ERROR_ACCESS设备节点虽然进去了但 OpenOCD 用普通用户访问时常常会遇到LIBUSB_ERROR_ACCESS。这个报错的意思是“设备节点存在但当前用户没有读写权限”。展开说一下Linux 下 /dev/bus/usb/001/004 这种设备节点的默认权限通常只有 root 和 root 所在的组能读写。你在容器里如果用的不是 root 用户或者 UID 对不上libusb 就打不开设备。解法有两个方向一是容器内直接用 root 跑 OpenOCD简单粗暴但不符合最小权限原则也不利于多人复用。二是给设备节点配上 udev 规则让普通用户也有读写权限。这也是我推荐的做法。在宿主机上新建一个规则文件sudo vim /etc/udev/rules.d/99-stlink.rules内容如下SUBSYSTEMusb, ATTR{idVendor}0483, ATTR{idProduct}3748, MODE0666, GROUPplugdev SUBSYSTEMusb, ATTR{idVendor}0483, ATTR{idProduct}374b, MODE0666, GROUPplugdev SUBSYSTEMusb, ATTR{idVendor}0483, ATTR{idProduct}374f, MODE0666, GROUPplugdev然后重载规则sudo udevadm control --reload-rules sudo udevadm trigger重新插拔 ST-Link再跑lsusb确认设备还在然后看设备节点权限是否已经变成 666ls -l /dev/bus/usb/001/004如果输出是crw-rw-rw-说明权限问题解决。容器内加一句--volume /dev/bus/usb:/dev/bus/usbOpenOCD 就有机会访问设备了。2.3 容器内用户组也要注意如果你在 docker run 里用了--user指定普通用户且宿主机上的设备节点权限只对plugdev组开放那你还需要把容器进程加入对应的组。比如docker run --rm -it \ --volume /dev/bus/usb:/dev/bus/usb \ --group-add plugdev \ --user $(id -u):$(id -g) \ openocd-image bash当然如果规则文件里直接写了MODE0666那任何用户都可以读写--group-add就不是必须的了。我更推荐用 0666因为容器里的 UID 和宿主机 UID 经常对不上最省心。3. 让容器看到 USB三种透传方案的取舍3.1 Linux 下直接挂目录不要绑死 device在 Linux 上最直接的是用--privileged启动容器但这东西太粗暴等于把宿主机的所有设备都塞进容器安全上不推荐。更精细的做法是--device /dev/bus/usb。有个细节要注意--device可以传入目录这样 Docker 会把目录下一级的所有设备节点都映射进去。所以可以直接写docker run --rm -it \ --device /dev/bus/usb \ --volume /dev/bus/usb:/dev/bus/usb \ openocd-image bash--device让容器能访问设备节点--volume /dev/bus/usb:/dev/bus/usb是为了让容器里的 /dev/bus/usb 目录结构完整。其实只挂目录也能访问但加上--device更稳因为某些情况下只挂目录会出现设备节点权限异常。如果你用 docker-compose对应写法是services: openocd: image: openocd-image devices: - /dev/bus/usb:/dev/bus/usb volumes: - /dev/bus/usb:/dev/bus/usb3.2 Windows 下Docker Desktop 加 WSL2 怎么弄Windows 上的问题比较特殊。Docker Desktop 在 Windows 环境下跑容器底层通常走的是 WSL2。这时候即使你在 Windows 里能看到 ST-Link 设备WSL2 虚拟机里也不一定能看到需要借助 usbipd-win 这个工具把 USB 设备转发到 WSL2 里。基本流程是先在 Windows 终端里安装 usbipd-win可以用 wingetwinget install --interactive --exact dorssel.usbipd-win然后用管理员权限跑usbipd list输出里会列出一堆 USB 设备找到 ST-Link 对应的 busid比如3-2。先把设备绑定到 WSL2usbipd bind --busid 3-2然后在 WSL2 终端里附着设备usbipd attach --wsl --busid 3-2在 WSL2 里跑lsusb看到 0483:3748 之后再在 WSL2 里启动 Docker 容器用前面说的挂载方式透传即可。这个方案虽然多了一步但在 Windows 上确实是最实用的。要注意的是每次重启电脑或重新插拔 ST-Link可能都需要重新 attach 一次所以最好把这个命令写进一个初始化脚本里。3.3 Mac 下的尴尬局面Mac 上用 Docker Desktop 直通 USB 设备比较尴尬。Docker Desktop 的 USB passthrough 功能在非 Linux 平台上是付费特性免费版基本上用不了。如果你的开发环境在 macOS 上我建议直接把 OpenOCD 装在宿主机上或者用虚拟机跑 Linux再把 USB 设备直通到虚拟机里绕开 Docker Desktop 的限制。我不推荐为了这个事专门去买 Docker Desktop 的付费版本在 macOS 上直接用 brew 装 openocd 反而更快brew install openocd当然如果你真的希望在 Mac 上也能统一容器环境可以考虑把容器跑在一台 Linux 服务器上ST-Link 物理连接在那台服务器上其他同事通过脚本远程调用但这会引入网络传输和权限管理成本适合团队级流水线不适合个人调试。4. OpenOCD 那一堆 -f 参数到底在干什么4.1 先懂三层配置有朋友直接照抄网上的 OpenOCD 命令跑通了也不知道那串-f参数是什么意思。其实 OpenOCD 的配置文件逻辑很清晰主要分三层interface 层定义调试器比如interface/stlink.cfg这一层告诉 OpenOCD 你现在用的是 ST-Link。target 层定义目标芯片比如target/stm32f4x.cfg这一层包含芯片的内核型号、复位方式、内存布局。board 层可选通常具体开发板会有现成的 board 配置文件比如board/st_nucleo_f4.cfg里面会同时引用 interface 和 target。如果搜到只写 st_nucleo_f4 的帖子说明那块开发板把 ST-Link 焊在板上可以不用单独指定 interface。但你非要接外部 ST-Link就得自己组合 interface target 两个文件。4.2 从启动到烧录的完整命令最常见的烧录命令是这样openocd \ -f interface/stlink.cfg \ -f target/stm32f4x.cfg \ -c adapter speed 4000 \ -c program build/app.elf verify reset exit拆开解释一下-f interface/stlink.cfg选择 ST-Link 作为调试器。-f target/stm32f4x.cfg选择 STM32F4 作为目标。-c adapter speed 4000把 SWD/JTAG 通信速率设为 4000 kHz也就是 4 MHz。这个速度不是越高越好我一般从 4000 开始试不稳定就降到 1000。-c program build/app.elf verify reset exit烧录镜像烧完校验verify然后复位芯片运行reset最后退出exit。如果只想启动一个 OpenOCD 服务然后通过 gdb 连上去调试可以改成openocd -f interface/stlink.cfg -f target/stm32f4x.cfg默认情况下OpenOCD 会监听 3333 端口的 GDB 连接和 4444 端口的 telnet 连接。调试时可以开两个终端一个跑 OpenOCD另一个跑 arm-none-eabi-gdb。4.3 “cant perform jtag flash, because openocd server is not running!” 是怎么来的这是 CLion 这类 IDE 在集成 OpenOCD 时常见的错误。真实过程是IDE 在点击烧录按钮后会先尝试启动一个 OpenOCD 进程如果你在 IDE 里配置的 OpenOCD 路径不对、配置文件不对、或者设备权限有问题OpenOCD 进程根本没起来IDE 就抛出“服务器没在运行”的提示。解决办法就三步在宿主机上手动运行 OpenOCD 命令确认能正常连接 ST-Link、能读到芯片。如果手动可以但 IDE 不行大概率是 IDE 里 OpenOCD 的可执行文件路径或参数不对去 IDE 的 Embedded Development 设置里把路径改成容器内或宿主机上的实际路径。如果你是用 Docker 跑的 OpenOCD还要确保 IDE 本身能访问 Docker 命令行并且 ST-Link 设备已经被正确透传到容器里。这个报错最大的迷惑性在于它听起来像是 OpenOCD 服务挂了实际上问题往往出在“还没轮到启动服务”的环节。5. 实测高频坑与报错对照表5.1 报错对照表我把实际操作中容易遇到的报错按场景整理成了表格方便你直接搜索定位。报错或现象大概率原因最直接的解决办法libusb_open() failed with LIBUSB_ERROR_ACCESS当前用户没有设备节点权限添加 udev 规则重载后重插 ST-Linklibusb_open() failed with LIBUSB_ERROR_NOT_FOUND容器里没有透传 USB 设备用--device /dev/bus/usb或挂载目录Error: open failedOpenOCD 找不到配置中的适配器确认interface/stlink.cfg路径无误Info : cannot find stlink device根本没识别到 ST-Link检查 lsusb、USB线、驱动Error: unable to open CMSIS-DAP device配置文件选错了接口ST-Link 应使用 stlink.cfg而不是 cmsis-dap.cfgError: init mode failed (unable to connect to the target)芯片方面的问题降低 SWD 速度检查接线检查读保护cant perform jtag flash, because openocd server is not running!IDE 没能启动 OpenOCD 进程检查 IDE 中 OpenOCD 路径、配置参数、设备透传烧录到一半卡死或校验失败供电不稳、SWD线太长、速度过高降速到 1000 kHz用短杜邦线外加电容稳住 VCC5.2 SWD 接线和读保护这两个老生常谈的问题在 Docker、OpenOCD 这一层折腾的同时别忘了硬件那头的两颗雷。第一颗是 SWD 接线。很多人买的是 ST-Link V2 带 20pin 转接板总记不住引脚。我的建议是不要背引脚图拿到转接板后先看丝印再对照 ST 官方手册。ST-Link 的 2x10 排针上核心必接的就四根SWDIO、SWCLK、GND、VCC。不要以为必须有 NRST实际上很多板子不接 NRST 也能连但某些芯片复位时序特殊建议有条件就一起接上。如果用杜邦线尽量控制在 10 厘米以内超过 15 厘米在 4 MHz 下很容易出现时序问题。另外VCC 不要接错外部供电或者板载供电选一种不要同时给 ST-Link 和目标板两块供电打架。第二颗是读保护。如果芯片曾经被设置过 RDP读保护OpenOCD 连接时会出现初始化失败、无法下载、读出来全 0 这类问题。这时候直接用 ST 官方工具 STM32 ST-LINK Utility 或 STM32CubeProgrammer 解除读保护比在 OpenOCD 里硬刚高效得多。注意解除读保护通常会触发全片擦除属于正常现象。5.3 ST-Link 固件版本和 Docker 镜像的兼容另外一个不太起眼但真的能坑人的点是 ST-Link 固件版本。老的 ST-Link V2 如果固件太旧在 OpenOCD 里可能表现得很怪比如能识别设备但连不上芯片。先用 STM32CubeProgrammer 里的 Firmware Upgrade 工具把 ST-Link 固件升级到最新能排除很多“莫名其妙”的故障。然后说 Docker 镜像。很多开源的嵌入式 Docker 镜像会把 OpenOCD 打成老版本老版本对 ST-Link V3 的支持可能不完整。如果遇到“设备能识别但 OpenOCD 不认”的情况优先检查 OpenOCD 版本openocd --version建议至少使用 0.12 或更新版本。如果你用的发行版仓库里的 OpenOCD 太老可以在 Dockerfile 里从源码编译安装后面我会给示例。5.4 容器重启后设备又不见了容器重新创建后USB 设备是否还能用完全取决于启动命令里有没有配置设备映射。如果你用的是docker compose确保 devices 和 volumes 配置都在。如果每次都是docker run临时跑建议封装成一个脚本或者 Makefile 目标这样不会漏掉参数。我在实际使用中还发现如果 ST-Link 断开后再插上但容器是长期运行的有时会出现宿主机能看到设备、容器里却访问不稳定的情况。最快的解决办法是重建容器或者重新 attach 一次 USB 设备省得在容器里反复折腾。6. 把这套方案固化到项目里6.1 一个可用的 Dockerfile如果你决定把 OpenOCD 塞进容器我提供一个相对干净的 Dockerfile 基础款FROM ubuntu:22.04 RUN apt-get update apt-get install -y \ openocd \ usbutils \ netcat-openbsd \ rm -rf /var/lib/apt/lists/* WORKDIR /work ENTRYPOINT [openocd]如果担心 apt 里的 OpenOCD 版本太老可以从源码编译但编译时间会长一些FROM ubuntu:22.04 AS build RUN apt-get update apt-get install -y \ autoconf automake libtool pkg-config libusb-1.0-0-dev \ git make gcc \ rm -rf /var/lib/apt/lists/* RUN git clone --depth 1 -b v0.12.0 \ https://github.com/openocd-org/openocd.git /tmp/openocd RUN cd /tmp/openocd \ ./bootstrap \ ./configure --enable-stlink \ make -j$(nproc) \ make install FROM ubuntu:22.04 RUN apt-get update apt-get install -y \ libusb-1.0-0 usbutils \ rm -rf /var/lib/apt/lists/* COPY --frombuild /usr/local/bin/openocd /usr/local/bin/openocd COPY --frombuild /usr/local/share/openocd /usr/local/share/openocd WORKDIR /work ENTRYPOINT [openocd]编译时注意--enable-stlink不然 ST-Link 支持可能被裁剪掉。6.2 docker-compose 固化示例项目根目录建一个docker-compose.ymlservices: openocd: build: . image: openocd-toolchain:latest devices: - /dev/bus/usb:/dev/bus/usb volumes: - /dev/bus/usb:/dev/bus/usb - ./build:/work/build command: -f interface/stlink.cfg -f target/stm32f4x.cfg -c adapter speed 4000 -c program /work/build/app.elf verify reset exit注意command里直接接 OpenOCD 参数因为镜像的 ENTRYPOINT 已经是 openocd 了。如果你在 Windows Docker Desktop WSL2 的环境里跑这组配置前提是先把 ST-Link 通过 usbipd-win attach 到 WSL2 里否则/dev/bus/usb下根本没有设备节点。6.3 Makefile 集成我自己还习惯在项目里放一个 Makefile把烧录动作统一起来.PHONY: flash flash: docker compose run --rm openocd .PHONY: flash-shell flash-shell: docker compose run --rm --entrypoint bash openocd这样同事拿到项目后只需要make flash就能完成烧录不需要自己在宿主机上装 OpenOCD也不需要考虑配置文件路径。对于 CI 环境只需要在流水线里执行同样的命令就能用同一套工具链完成固件验证。个人经验补充别在设备映射上省事最后说点个人体会。这一套方案里最不值得“优化”的就是设备映射。有人觉得--privileged最省心直接加上一了百了但这样容器的隔离性就大打折扣了尤其在团队共享 CI Runner 上很容易出问题。也有人觉得每次手动--device /dev/bus/usb/xxx更精确但 USB 设备号会变脚本维护成本高。相比之下挂载整个/dev/bus/usb目录加 udev 规则是平衡安全性和易用性最好的方案。OpenOCD 这条命令链在这个场景里反而简单只要版本别太老、配置文件别瞎拆基本一次就能跑通。真正耗时间的往往是那些看起来“不是问题”的问题USB 线只通电不通数据、WSL2 里没 attach、udev 规则没重载。排查的时候一定从宿主机 lsusb 开始一级一级往下查别一上来就扎到 OpenOCD 参数里。
返回列表