

这篇就是这趟的流水账,包括走错的路。
先把 Linux 侧理顺:不接显示器,走 headless
我不打算给它接显示器,直接 headless。那整个流程就是:先连上板子拿到 shell,再做基础配置,最后按需把桌面关掉。
新板子出厂已经是 Debian,先别急着刷。USB-C 连电脑,板子上电,LED 点阵出现无限符号 → 爱心动画,说明系统正常起来了。这时候不用 App Lab 的 GUI 也能进系统,有两条路。
方式 A:ADB 进 shell。 最快,不用配网。PC 上装 adb,Windows 直接下 platform-tools,Linux sudo apt install adb,然后:
adb devices # 确认能看到板子 adb shell # 进入板上 Debian
进去就是命令行,这就是我的 Linux 环境了。
方式 B:SSH。 ADB 适合首次接触,长期开发还是 SSH 舒服。先在板上连 WiFi:
nmcli device wifi list sudo nmcli device wifi connect "SSID" password "密码" ip addr # 记下拿到的 IP
之后 PC 上 ssh <用户名>@<板子IP> 就行。
进系统后的基础配置也就那几条:
sudo apt update && sudo apt upgrade sudo apt install vim git curl build-essential
这里要说清楚一件容易混的事:apt upgrade 更新的是 Debian 自己源里的包,和 Arduino 那套组件的更新完全是两回事,别指望一条命令全搞定。
关桌面:
sudo systemctl set-default multi-user.target sudo reboot
重启后开机直接进命令行,不再启动图形桌面。
到这一步 Linux 侧就绪。但有个关键点得提前想到:后面烧 STM32 依赖板上的 adbd 和 openocd 调试桥。所以裁剪的时候得手下留情——adbd 服务必须留着,别卸;Arduino 那个组件更新弹窗里 adbd / adb 相关的建议让它更新或至少保留;arduino:zephyr 那个板端 loader,我要用原生 Zephyr 反正会覆盖掉(能恢复);App Lab 纯 GUI 的部分可以不管。
装adb
Windows 上兴冲冲敲下去:
PS C:Usersl> adb devices adb : 无法将"adb"项识别为 cmdlet、函数、脚本文件或可运行程序的名称。请检查名称的拼写,如果包括路径,请确保路径正确,然后再试一次。 所在位置 行:1 字符: 1 + adb devices + ~~~ + CategoryInfo : ObjectNotFound: (adb:String) [], CommandNotFoundException + FullyQualifiedErrorId : CommandNotFoundException
哦吼,adb 没装。行,补上。Win10/11 自带 winget,最省事:
winget install Google.PlatformTools
装完必须关掉当前 PowerShell 窗口重开,刷新 PATH,不然还是找不到。
如果 winget 不给力,就手动下:去 https://developer.android.com/tools/releases/platform-tools 下 "SDK Platform-Tools for Windows",解压到一个固定位置比如 C:platform-tools。临时用就 cd C:platform-tools 然后 .adb devices;想全局用就把这个目录加进系统环境变量 PATH,重开窗口。
几个可能的岔子我先记在这,免得回头抓瞎:
adb devices 列表是空的但命令能跑了 —— 检查 USB-C 线是不是数据线,有些线只能充电。换个口或换根线。
出现设备但状态是 unauthorized —— UNO Q 这边一般不弹授权,adb kill-server 再 adb devices 重试。
winget 报找不到源 —— 先 winget source update 再装。
重开窗口,再来一次:
PS C:Usersl> adb devices List of devices attached 719271120 device
成了,板子在线。
进 shell,顺便确认系统状态
adb shell
进去后提示符变成板上的了,这就是 UNO Q 的 Debian 环境。先摸清几件事:
whoami # 当前是什么用户 cat /etc/os-release # 确认系统版本,应该是 Debian 13 trixie ip addr # 看网络状态,有没有 IP
然后连 WiFi,为的是后面能 SSH、摆脱 USB 线:
nmcli device wifi list sudo nmcli device wifi connect "SSID" password "密码"
连上后再 ip addr,在 wlan0 那段找 inet 后面的地址,类似 192.168.x.x,记下来——这是后面 SSH 要用的板子 IP。
插曲:那个还在下东西的 App Lab
搞这些的同时,App Lab 那边一直在后台下东西。组件更新,跨境慢得离谱,卡了我一两个小时,我误以为电脑开魔法就行,实际上是需要板子走代理,或者走局域网代理的。
这事儿本身不重要,但它把一个问题顶到我面前了:我到底需不需要 Arduino 那一整套?
要回答这个,得先把两件一直被混在一起的东西拆开。
把 openocd 和"双核通信"彻底拆开
这两件事互相独立,我之前一直没分清楚。
第一件:openocd —— 是"烧录/调试"用的。
这是把编译好的固件灌进 STM32 的手段,以及之后单步调试用的。它走的是 SWD(ARM 的调试协议),和两颗芯片运行时怎么通信毫无关系。
UNO Q 上 STM32 没有独立引出的 SWD 口,所以让 MPU(那颗 QRB2210)兼职当调试器:MPU 上跑 openocd,通过板内的 GPIO 模拟 SWD 信号连到 STM32 的调试脚。我在电脑上敲烧录命令 → 通过 USB/adb 转发到 MPU 上的 openocd → openocd 用 SWD 把固件写进 STM32 的 flash。烧完就结束了。
openocd 只在"烧录/调试"这个动作时用,固件跑起来之后它不参与。 日志里那句 Cortex-M33 detected、flash size 2048 KiB,就是 openocd 通过 SWD 摸到了 STM32。
第二件:STM32 ↔ MPU 运行时通信 —— 是"两颗芯片干活时互相说话"用的。
这是固件已经在 STM32 上跑起来后,STM32 和 Linux 之间交换数据的通道。比如 STM32 采集了传感器数据要传给 Linux 处理,或者 Linux 要命令 STM32 输出个 PWM。
硬件上,两颗芯片之间连了两条物理线:LPUART1(串口)和 SPI3(高速,目前预留)。Arduino 官方在这上面封了一层叫 Bridge / Router 的 RPC 协议(就是那个 arduino-router),让两边能方便地互调。但这是 Arduino 的封装,不是硬件强制的。
画出来就很清楚了:
【烧录时】电脑 --USB/adb--> MPU(跑 openocd) --SWD(调试线)--> STM32 flash ↑ 这条链只在"灌固件/调试"时存在,openocd 干这个 【运行时】STM32 <--LPUART1/SPI3(数据线)--> MPU(Linux) ↑ 这条是两颗芯片跑起来后交换数据,和 openocd 无关
一句话:openocd 走 SWD 负责"把程序装进去",LPUART1/SPI3 负责"程序跑起来后两边聊天"。两条不同的物理连接,两个不同的用途。
想明白这个,裁剪就有底了。我把 arduino-router 卸了,保留 arduino-cli 和它自带的 openocd。这意味着:
烧录通道现成可用,烧固件没问题。
Arduino 那套现成 RPC 没了。如果我的 Zephyr 程序需要和 Linux 交换数据,得自己在 STM32 侧(Zephyr 里配 UART 或 SPI)和 Linux 侧(打开对应的 /dev/tty* 或 spidev)各写一端,自己定协议。
如果只是让 STM32 独立跑——闪个灯、自己采样自己处理,不传给 Linux——那这条通信我现在压根不用管。
我先走后者。
差点自己给自己挖坑:USB gadget
接下来我想配 USB gadget。手停在半空,先问了自己一句:配它干嘛?
USB gadget 是 Linux 那颗(MPU) 的功能,和 STM32、Zephyr、openocd 一点关系都没有。它是让板子的 USB 口从"设备"角色对外模拟成某种外设——网卡(RNDIS/ECM,让电脑通过 USB 线和板子组网)、串口、U 盘等等。
我想要的其实是第一种:通过 USB 线让电脑和板子组网,不用 WiFi,插上 USB-C 就能 ssh 到板子。
但这里有个 UNO Q 特有的冲突,幸好想到了:板子这个 USB-C 口现在已经被 adb 占着用了。 我前面 adb shell、后面烧 STM32,全靠它。而 ADB 本身就是通过 USB gadget 的一个 function 实现的。我要是再配一个 RNDIS 网卡 gadget,就得搞 composite gadget(一个 USB 口同时提供多个 function:adb + 网卡),否则很容易把 adb 挤掉——那烧 STM32 的通道就断了。
停手。整条烧录链现在靠 USB-adb 撑着,USB gadget 配置弄不好会把 USB 口的角色改掉,adb 掉线,烧录也就断了。这时候动它纯属给能跑的流程加难度。
不过这倒逼出一个我一直没搞清的问题:烧录和 adb 到底什么关系?
烧录和 adb 的关系
这俩关系很紧,但不是一回事。分层看。
物理层:一根 USB 线,上面跑着 adb。 板子的 USB-C 口连电脑,这根线上跑的是 adb 协议。adb 提供几种能力:adb shell 给我一个板子的命令行;adb push/pull 传文件;adb forward 端口转发,把板子上某个网络端口映射到我电脑的本地端口。
烧录是"借道"adb 的端口转发。 关键就在 adb forward 这。整条链是这样的:
电脑上的 west/gdb ↓ 连 localhost:3333 adb forward tcp:3333 tcp:3333 ← adb 把这个端口从板子转发到电脑 ↓ 板子上的 openocd(监听 3333 端口,提供 gdb server) ↓ SWD 调试线 STM32 flash
具体就是日志里那句 Listening on port 3333 for gdb connections——openocd 在板子上开了个 3333 端口的 gdb 服务器。但我的 gdb 在电脑上,够不着板子的端口。于是用 adb forward tcp:3333 tcp:3333,让电脑的 localhost:3333 直通到板子的 3333。这样电脑上的 gdb 连本地 3333,数据经 adb 隧道到板子的 openocd,openocd 再用 SWD 写进 STM32。
所以:烧录 = openocd(在板子上,干实际的 SWD 写入)+ adb forward(在 USB 线上,把 openocd 的端口搬到电脑上让 gdb 够得着)。
adb 是"运输通道",openocd 是"干活的人",SWD 是"最后一段接到 STM32 的线"。adb 本身不懂烧录、不碰 STM32,它只是个通用隧道。
这也就解释了为什么不能乱动 USB gadget:adb 是通过 USB gadget 的一个 function 实现的,把 gadget 配置改成纯网卡模式,adb 这个 function 可能被顶掉 → adb forward 没了 → 电脑的 gdb 连不上板子的 openocd → 烧录链断。openocd 和 STM32 都还好好的,但我从电脑够不着了。
顺着这个思路还能想出另一条更干净的路:既然 adb 只是个隧道,那能不能不用 adb?能。转发的本质是"把板子的 3333 端口暴露给电脑",完全可以换成 SSH 隧道:
ssh -L 3333:localhost:3333 arduino@<板子IP>
板子上照样跑 openocd 监听 3333,电脑 gdb 连 localhost:3333,走 SSH 隧道到板子。这样烧录就不依赖 adb / USB 了,走网络。我之前想配 USB gadget 组网、或者用 WiFi,最终目的可能就是这个——摆脱 USB 线,靠网络又 SSH 又烧录。
梳理清楚之后,选择其实就是"烧录的运输通道用哪个":
| A. 现状 | USB + adb forward | 啥都不用配,现在就能烧 |
| B. WiFi | SSH 隧道 | 板子连 WiFi,配好 SSH |
| C. USB 网卡 | SSH 隧道 | 配 composite USB gadget(adb+网卡),较复杂 |
先用 A 把第一个固件烧通,验证整条链没问题。跑通了再看要不要折腾 B/C。一上来就 C,那是给能跑的流程加难度。
gdb 兼了两个活
真正上手烧的时候,我被 gdb 绕住了一下——我以为它只是调试器,怎么烧录也用它?
gdb = GNU Debugger,调试器没错。但在这个场景里它兼了两个活:
烧录器:敲的 load,就是 gdb 把固件写进 STM32 flash——成功的那步就是它干的。
调试器:烧完之后单步、看变量、下断点,这是它的本职。
为什么烧录要用 gdb?因为板子上的 openocd 开了个 gdb server(3333 端口)。openocd 的意思是"我这有个 gdb 服务,谁来连我,我就帮谁操作 STM32"。gdb 就是那个"连上去发命令的客户端":
我敲 gdb 命令(load / continue) ↓ gdb 连 localhost:3333(经 adb 隧道) ↓ 板子上的 openocd 收到,翻译成 SWD 操作 ↓ STM32
所以 gdb 是我操控 openocd 的遥控器。我敲 load,gdb 告诉 openocd "把这个固件写进去",openocd 就用 SWD 写。
烧完之后,STM32 是暂停状态(halted),我停在 (gdb) 提示符里。要让它跑起来得给两条命令:
monitor reset
monitor 前缀表示"这条命令转发给 openocd 执行",reset 就是复位 STM32。
continue
简写 c,让 STM32 从暂停状态放开、开始运行。
敲完 continue,STM32 就开始跑 hello_world 了(虽然串口输出我暂时看不到)。gdb 这时会"挂住",因为程序在跑,它在旁边盯着。
退出的话:如果是 (gdb) 提示符能输入,直接 quit,会问 Quit anyway? (y or n),输 y 回车,简写 q 也行。如果 gdb 卡住没提示符(敲了 continue、光标不动),先 Ctrl + C 把它拉回 (gdb),再 quit → y。
退出 gdb 不影响已经烧进去的程序。 固件在 STM32 的 flash 里,gdb 退了它照跑。遥控器放下了,STM32 该干嘛干嘛。
另外两个终端:板子上 arduino-debug 跑着的那个,不用了就 Ctrl + C 停掉 openocd,下次烧录重新起就行;adb forward 那个转发一直挂着无所谓,想清就 adb forward --remove tcp:3333。
west flash 此路不通
手动 gdb 烧通了,我自然想用标准的 west flash。结果:
(.venv) l@l:~/zephyrproject$ west flash -- west flash: rebuilding ninja: no work to do. -- west flash: using runner stm32cubeprogrammer mass erase requested reset after flashing requested FATAL ERROR: required program /home/l/STMicroelectronics/STM32Cube/STM32CubeProgrammer/bin/STM32_Programmer_CLI not found; install it or add its location to PATH
这个报错其实是意料之中的。UNO Q 的 west flash 还没接好,它默认去找 stm32cubeprogrammer 这个 runner,但那需要 ST 官方的 STM32CubeProgrammer 工具——而且就算装了,那工具也需要独立的 SWD 调试器硬件,UNO Q 这种"MPU 模拟 SWD"的架构它用不了。
所以 west flash 这条路在 UNO Q 上走不通,别在它身上花时间。手动 gdb load 那套才是这块板子的正确烧录方法:
终端1(板子):arduino-debug # 起 openocd 终端2(电脑):adb forward tcp:3333 tcp:3333 终端3(电脑):arm-zephyr-eabi-gdb build/zephyr/zephyr.elf (gdb) target remote localhost:3333 (gdb) load # ← 这步就是烧录,已验证成功 (gdb) monitor reset (gdb) continue
顺带把 arduino-debug 是什么也说清楚:它就是 Arduino 提供的一个封装脚本,作用是在板子上把 openocd 用正确的配置(SWD 引脚、目标芯片)拉起来,让它监听 3333 端口。我不用自己写 openocd 的 cfg,直接用它就行。
还不甘心:能不能让 west flash 只传数据?
我心里的理想模型是这样的——电脑只管发命令和传数据,openocd 老老实实在板子上跑:
电脑:gdb(只发命令、传固件数据) ↓ 只传数据,不起 openocd 板子:openocd(arduino-debug 起的,它在干活) ↓ SWD STM32
想了一下才反应过来:这不就是我现在 gdb 方式的实际行为吗? 电脑侧的 gdb 纯粹是个"发命令+推数据"的客户端,openocd 在板子上。我的诉求早就满足了,只是它挂在 gdb 名下,不叫 west flash。
那 west flash 能不能也变成"只传数据不拉 openocd"?west 的 openocd runner 默认行为是"自己拉 openocd + 发命令"两件事捆一起,要拆开,就得让它跳过"拉 openocd"、只保留"发命令"。查一下这版支不支持:
west flash --context -r openocd 2>&1 | grep -iE 'gdb|server|no.?init|running' west flash -r openocd --help 2>&1 | tail -40
我在找的是这类参数(不同 Zephyr 版本名字不一样):--gdb-serial 之类指定连已有 gdb server 的、或者 "attach to running" 这种。
坦白讲,openocd runner 通常没有"连远程已运行 openocd"的干净开关。它的设计就是"我起 openocd 我发命令"。真正支持"只当客户端连远程 server"的,其实是 gdb 相关的机制,而不是 openocd runner。Zephyr 里让 west 用 gdb 连一个已经起好的 gdb server 这种模式,在 debug 里反而好使——这也正是为什么我之前走 gdb(debug 路线)能通。
说到底,west 把"起 server"和"传数据"耦合在了 flash 命令里,而 UNO Q 需要这两者分离。分离后的形态,就是我现在的 gdb 方式。
所以结论没变,但我心里舒服了:我要的"电脑只传数据、openocd 在板子",gdb 方式已经做到了,这不是妥协,这就是正解。west flash 不适合 UNO Q,是因为它把两件事捆死了。剩下唯
串口调试通道固件烧进去、continue 敲下去,程序在跑了——但我啥也看不见。hello_world 也好、led_display 的日志也好,输出得从某根 UART 出来,我得先搞清楚它从哪儿出、怎么接到我眼前。
官方那套是走 arduino-router 的 Bridge:STM32 的串口经 RPC 转发到 Linux 侧,再拿 Arduino 的 Monitor 看。router 我前面已经卸了,这条断了。上了原生 Zephyr,console 就是一根裸 UART,得自己接。
console 到底从哪根线出
Zephyr 的 console/log 绑在设备树里 zephyr,console 指的那个 UART 上。UNO Q 的 STM32 有不止一根 UART,先分清:
D0/D1 排针:板边那两个 Arduino 风格引脚,是一根 UART。用它得外接一个 USB-TTL 转接器,飞根线到电脑。我 headless、一根 USB-C 走天下,不想再加线加转接器。
LPUART1(内部线):STM32 和 MPU 之间物理连着的那根串口(前面拆"运行时通信"时提过)。它在 Linux 侧直接就是一个 /dev/ttyHS1。
第二根才是我要的:console 走 LPUART1,STM32 一头把日志打进去,MPU 一头 cat /dev/ttyHS1 就能读。全程不出板子,USB-C 那根继续只管 adb。链路:
STM32(Zephyr console) --LPUART1(内部)--> /dev/ttyHS1 --> MPU 上 cat
默认 console 大概率指在 D0/D1 那根,所以烧完直接 cat 是空的——不是程序没输出,是输出跑到我没接的那根线上了。
overlay 把 console 改到 lpuart1
不动 C 代码,加个 devicetree overlay,把 zephyr,console(用 shell 的话连 zephyr,shell-uart)重指到 lpuart1:
dts/ {
chosen {
zephyr,console = &lpuart1;
zephyr,shell-uart = &lpuart1;
};
};
&lpuart1 {
status = "okay";
current-speed = <115200>;
};重新 build,按前面 gdb 那套重烧。
读串口,看 banner
设备腾出来,配好波特率直接 cat:
stty -F /dev/ttyHS1 115200 raw -echocat /dev/ttyHS1
再让固件从头跑一遍,串口这头就出来了:
*** Booting Zephyr OS build v4.4.0-8295-g0bd807ceeb75 ***
我要赚赏金
