1、Zephyr调试入门:调试环境搭建、OpenOCD配置、GDB基础用法、常用调试命令

调试嵌入式系统,说白了就是跟硬件和代码「斗智斗勇」。我刚开始接触Zephyr时,最头疼的不是RTOS本身,而是调试环境怎么都搭不起来。你想想看,代码写好了,烧进去跑不起来,连个打印都没有,那感觉就像在黑夜里摸路。

这一章,咱们就把调试环境彻底搞定。我会把OpenOCD怎么配、GDB怎么用、常用调试命令有哪些,全都掰开揉碎了讲清楚。

1.1 调试环境搭建

调试Zephyr项目,我个人的习惯是「三步走」:先装工具链,再配调试器,最后验证连通性。别嫌麻烦,这一步做好了,后面能省下大把时间。

1.1.1 必备工具清单

工具 用途 推荐版本
OpenOCD 调试器与GDB的桥梁 0.11.0+
GDB (arm-none-eabi-gdb) 源码级调试 10.x+
Zephyr SDK 包含工具链和调试器 0.16.0+
Segger J-Link / ST-Link 硬件调试器 最新固件

嗯,这里要注意:Zephyr SDK其实已经自带了arm-none-eabi-gdb和OpenOCD。我建议直接用SDK里的版本,省得自己编译踩坑。我曾经在Ubuntu 20.04上自己编译OpenOCD,结果因为libusb版本不对,折腾了两天才搞定。

我的小技巧: 安装完Zephyr SDK后,运行 source zephyr-env.sh 之前,先检查一下 $ZEPHYR_SDK_INSTALL_DIR/sysroots/x86_64-pokysdk-linux/usr/bin/ 目录下有没有openocd和arm-none-eabi-gdb。有的话,直接加到PATH里就行。

1.1.2 硬件连接检查

调试器连上开发板后,别急着跑OpenOCD。先确认几件事:

  • USB线:别用那种只能充电的线,要带数据功能的。我吃过这个亏,连上后OpenOCD一直报「no device found」,换了根线就好了。
  • 驱动:Windows下需要安装驱动(比如ST-Link的驱动),Linux下要配置udev规则。Zephyr官方文档里有现成的udev规则文件,直接复制到 /etc/udev/rules.d/ 就行。
  • 跳线帽:有些开发板需要短接特定的跳线才能进入调试模式。比如STM32F4 Discovery板,要把CN3的跳线帽从「ST-LINK」侧拨到「SWD」侧。

1.2 OpenOCD配置

OpenOCD的配置,说白了就是告诉它「你的调试器是什么型号,连的芯片是什么,用哪种协议」。我个人习惯把配置写在 openocd.cfg 文件里,这样每次调试时直接加载就行。

1.2.1 基础配置模板

以STM32F4系列 + ST-Link调试器为例,一个典型的配置长这样:

# openocd.cfg
source [find interface/stlink.cfg]
source [find target/stm32f4x.cfg]

# 设置SWD模式
transport select hla_swd

# 调试时钟频率(单位:kHz)
adapter speed 1000

# 复位配置
reset_config srst_only srst_nogate

为什么这么配?我来解释一下:

  • source [find interface/stlink.cfg]:加载ST-Link调试器的配置。OpenOCD自带了各种调试器的配置文件,放在 interface/ 目录下。
  • source [find target/stm32f4x.cfg]:加载目标芯片的配置。这里指定了STM32F4系列的内存映射、Flash算法等。
  • transport select hla_swd:选择SWD协议。ST-Link支持JTAG和SWD两种模式,SWD只需要两根线(SWDIO和SWCLK),更简单。
  • adapter speed 1000:调试时钟设为1MHz。太快了可能不稳定,太慢了调试响应慢。1000是个比较稳妥的值。
注意: 如果你用的是J-Link调试器,要把 interface/stlink.cfg 换成 interface/jlink.cfg。另外,J-Link的SWD配置是 transport select swd,没有 hla_ 前缀。这个细节我当年搞混过,OpenOCD报了一堆看不懂的错误。

1.2.2 启动OpenOCD

配置写好后,在终端里运行:

openocd -f openocd.cfg

如果一切正常,你会看到类似这样的输出:

Info : STLINK V2J29S7 (API v2) VID:PID 0483:3748
Info : Target voltage: 3.3V
Info : clock speed 1000 kHz
Info : stm32f4x.cpu: hardware has 6 breakpoints, 4 watchpoints

看到「hardware has 6 breakpoints, 4 watchpoints」这行,就说明OpenOCD已经成功连上芯片了。这时候OpenOCD会在后台监听3333端口(GDB连接端口)和4444端口(telnet命令端口)。

1.3 GDB基础用法

OpenOCD跑起来后,咱们就可以用GDB来调试了。GDB的全称是GNU Debugger,虽然名字里带个「GNU」,但它对Zephyr的支持非常好。

1.3.1 启动GDB并连接目标

打开另一个终端,运行:

arm-none-eabi-gdb build/zephyr/zephyr.elf

这里的 zephyr.elf 是编译生成的带调试信息的可执行文件。注意,一定要用这个文件,不能用 .hex.bin,因为它们没有符号信息。

进入GDB后,连接OpenOCD:

(gdb) target remote localhost:3333

连接成功后,GDB会停在芯片的复位向量处。这时候你可以输入 monitor reset halt 让芯片停在复位状态,然后开始调试。

我的习惯: 我会在GDB里先执行 monitor reset halt,然后 load 把程序烧进去,最后 monitor reset init 让芯片从main函数开始跑。这样能确保调试环境是干净的。

1.3.2 常用GDB命令速查

命令 缩写 作用
break b 设置断点,如 b mainb file.c:100
continue c 继续执行,直到遇到断点
next n 单步执行,不进入函数内部
step s 单步执行,进入函数内部
print p 打印变量值,如 p my_var
info registers i r 查看所有寄存器值
backtrace bt 查看函数调用栈
list l 显示当前行附近的源代码
monitor mon 向OpenOCD发送命令,如 mon reset halt

这些命令里,我个人最常用的是 bnpbt。尤其是 bt,当程序跑飞或者卡死在某个地方时,看一眼调用栈,基本就能定位问题。

1.4 常用调试命令实战

光说不练假把式。咱们用一个实际场景来演示一下这些命令怎么用。

1.4.1 场景:调试一个任务挂起问题

假设你的Zephyr程序里有一个任务,本该每隔1秒打印一次「Hello」,但跑了10秒后突然不打印了。这时候怎么查?

第一步,在GDB里设置断点:

(gdb) b z_impl_k_sleep
(gdb) b k_thread_abort

为什么设这两个断点?z_impl_k_sleep 是任务休眠时调用的函数,如果任务一直在休眠,说明它没被唤醒。k_thread_abort 是任务被终止时调用的函数,如果触发了这个断点,说明任务被意外杀死了。

第二步,继续运行:

(gdb) c

如果程序停在了 z_impl_k_sleep,用 bt 查看调用栈:

(gdb) bt
#0  z_impl_k_sleep (duration=...) at kernel/sleep.c:123
#1  0x08001234 in my_task (arg1=0x0, arg2=0x0, arg3=0x0) at main.c:45
#2  0x08005678 in z_thread_entry (entry=0x8001234, ...) at kernel/thread.c:256

看到 #1my_task 在调用 k_sleep,说明任务确实在休眠。这时候检查一下休眠时间对不对:

(gdb) p duration
$1 = 1000

嗯,1000ms,没问题。那问题可能出在唤醒机制上。再检查一下有没有其他任务在抢占CPU:

(gdb) info threads
  Id   Target Id         Frame
  1    Thread 0x20000000 (my_task) z_impl_k_sleep ...
  2    Thread 0x20000100 (idle)    z_impl_k_sleep ...
* 3    Thread 0x20000200 (high_prio_task) 0x0800abcd in some_function ...

看到了吗?high_prio_task 正在运行,而且它的优先级比 my_task 高。如果这个高优先级任务一直不释放CPU,my_task 就永远得不到执行。这就是问题所在。

关键点: Zephyr是抢占式RTOS,高优先级任务会一直运行,直到它主动让出CPU(比如调用 k_sleepk_yield)。如果高优先级任务里有死循环,低优先级任务就永远没机会跑。

1.4.2 查看内存和寄存器

有时候问题出在变量值不对。比如一个全局变量 counter 本该递增,但实际值却不变。可以用 print 查看:

(gdb) p counter
$1 = 42
(gdb) p &counter
$2 = (int *) 0x20001000

如果怀疑是内存被意外修改了,可以用 watch 命令设置观察点:

(gdb) watch counter
Hardware watchpoint 1: counter
(gdb) c
Continuing.
Hardware watchpoint 1: counter
Old value = 42
New value = 43
main.c:50 in my_task ()

这样,只要 counter 的值发生变化,GDB就会停下来,告诉你是在哪一行被修改的。这个功能在排查「变量莫名其妙被改」的问题时特别好用。

注意: 硬件观察点(watchpoint)的数量是有限的。比如STM32F4只有4个观察点。如果你设了5个,GDB会报错。另外,观察点对性能有影响,调试完记得删掉(用 info watchpoints 查看,delete 1 删除)。

1.5 调试中的常见坑

最后,分享几个我在项目中踩过的坑,希望能帮你少走弯路。

  • 断点打不上:有时候你设了断点,但GDB说「Cannot insert breakpoint」。这通常是因为代码被优化掉了。检查一下CMakeLists.txt里有没有加 -O0 编译选项。我建议调试时用 -O0 -g,发布时再用优化。
  • OpenOCD连接超时:如果OpenOCD启动后一直报「timeout waiting for target」,先检查硬件连接,再试试降低 adapter speed。我曾经在一条很长的杜邦线上跑2MHz的时钟,结果死活连不上,降到500kHz就好了。
  • GDB卡住不动:有时候 continue 后GDB就卡住了,按Ctrl+C也没反应。这时候可以试试在OpenOCD的终端里按Ctrl+C,或者直接拔掉调试器的USB线重连。嗯,这招虽然粗暴,但管用。

调试环境搭好了,命令也熟了,接下来就可以深入Zephyr的内核,看看任务调度、内存管理这些核心机制是怎么工作的。下一章,咱们聊聊「任务与线程调试」。