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版本不对,折腾了两天才搞定。
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是个比较稳妥的值。
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 让芯片停在复位状态,然后开始调试。
monitor reset halt,然后 load 把程序烧进去,最后 monitor reset init 让芯片从main函数开始跑。这样能确保调试环境是干净的。
1.3.2 常用GDB命令速查
| 命令 | 缩写 | 作用 |
|---|---|---|
| break | b | 设置断点,如 b main 或 b file.c:100 |
| continue | c | 继续执行,直到遇到断点 |
| next | n | 单步执行,不进入函数内部 |
| step | s | 单步执行,进入函数内部 |
| p | 打印变量值,如 p my_var |
|
| info registers | i r | 查看所有寄存器值 |
| backtrace | bt | 查看函数调用栈 |
| list | l | 显示当前行附近的源代码 |
| monitor | mon | 向OpenOCD发送命令,如 mon reset halt |
这些命令里,我个人最常用的是 b、n、p 和 bt。尤其是 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
看到 #1 是 my_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 就永远得不到执行。这就是问题所在。
k_sleep 或 k_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就会停下来,告诉你是在哪一行被修改的。这个功能在排查「变量莫名其妙被改」的问题时特别好用。
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的内核,看看任务调度、内存管理这些核心机制是怎么工作的。下一章,咱们聊聊「任务与线程调试」。