4、工具链搭建(下):使用 OpenOCD 与 JTAG/SWD 调试器、配置调试环境、第一个点灯程序
上一节我们把编译工具链搞定了,能编译出二进制文件。但说实话,光能编译还不够——你没法把程序烧进去,更没法调试。这就像你造了一辆车,但没轮子,推都推不动。
这一节我们解决「怎么把程序跑起来」的问题。核心工具就是 OpenOCD,配合一个 JTAG/SWD 调试器。我个人习惯用 J-Link 或者 CMSIS-DAP,便宜又好用。你手头如果有 ST-Link 或者 DAPLink,也完全没问题。
4.1 调试器与调试协议:JTAG vs SWD
先搞清楚两个概念:JTAG 和 SWD。
| 特性 | JTAG | SWD |
|---|---|---|
| 引脚数 | 5 根(TMS, TCK, TDI, TDO, nTRST) | 2 根(SWDIO, SWCLK) |
| 速度 | 较快,但受限于线缆 | 与 JTAG 相当,但更简洁 |
| 调试能力 | 支持边界扫描、多核调试 | 仅支持调试,不支持边界扫描 |
| 适用场景 | FPGA、复杂 SoC | ARM/RISC-V MCU 调试 |
对于 RISC-V 嵌入式开发,我建议直接用 SWD。为什么?因为引脚少,布线简单,而且大部分 RISC-V MCU 都支持。我在项目中遇到过一块板子,JTAG 接口被其他外设占用了,最后只能用 SWD 调试——反而更省事。
4.2 安装 OpenOCD
OpenOCD(Open On-Chip Debugger)是开源调试神器。它负责把 GDB 的调试命令翻译成 JTAG/SWD 信号,送到芯片内部。
安装方式很简单:
# Ubuntu/Debian
sudo apt-get install openocd
# macOS (Homebrew)
brew install openocd
# 从源码编译(推荐,版本最新)
git clone https://github.com/openocd-org/openocd.git
cd openocd
./bootstrap
./configure --enable-jlink --enable-cmsis-dap
make -j4
sudo make install
我个人习惯从源码编译。为什么?因为 apt 源里的版本往往比较老,有些新芯片的支持没有。而且编译时你可以选择开启哪些调试器驱动,比如 --enable-jlink 和 --enable-cmsis-dap。
4.3 配置 OpenOCD 脚本
OpenOCD 启动时需要两个配置文件:
- 接口配置:描述你用的调试器(比如 J-Link、ST-Link)
- 目标配置:描述你调试的芯片(比如 GD32VF103、CH32V307)
以我常用的 GD32VF103 为例,接口配置如下:
# jlink_gd32vf103.cfg
source [find interface/jlink.cfg]
transport select swd
adapter speed 1000
目标配置:
# target_gd32vf103.cfg
set CHIPNAME gd32vf103
set ENDIAN little
source [find target/nuclei.cfg]
# 设置复位方式
reset_config srst_only srst_nogate
# 设置 Flash 参数
flash bank gd32vf103_flash nuclei 0x08000000 0x100000 2 4 $_TARGETNAME
嗯,这里要注意:不同芯片的配置差别很大。比如 GD32VF103 用的是 Nuclei 内核,配置脚本在 target/nuclei.cfg。如果你用的是 CH32V307,那就要用 target/wch-riscv.cfg。
启动 OpenOCD 的命令:
openocd -f jlink_gd32vf103.cfg -f target_gd32vf103.cfg
如果一切正常,你会看到类似这样的输出:
Info : Listening on port 3333 for gdb connections
Info : Listening on port 6666 for tcl connections
Info : Listening on port 4444 for telnet connections
看到 Listening on port 3333 就说明 OpenOCD 已经跑起来了,正在等待 GDB 连接。
4.4 第一个点灯程序
好了,环境搭好了,该让灯亮起来了。这是嵌入式开发的「Hello World」。
先写一个最简单的点灯程序:
// led_blink.c
#include <stdint.h>
// GD32VF103 的 GPIO 寄存器地址
#define RCU_BASE 0x40021000
#define GPIOC_BASE 0x40011000
#define RCU_APB2EN (*(volatile uint32_t *)(RCU_BASE + 0x18))
#define GPIOC_CTL1 (*(volatile uint32_t *)(GPIOC_BASE + 0x04))
#define GPIOC_OCTL (*(volatile uint32_t *)(GPIOC_BASE + 0x0C))
void delay(volatile uint32_t count) {
while (count--) {
__asm__ volatile ("nop");
}
}
int main(void) {
// 使能 GPIOC 时钟
RCU_APB2EN |= (1 << 4);
// 配置 PC13 为推挽输出,最大速度 50MHz
GPIOC_CTL1 = (GPIOC_CTL1 & ~(0xF << 20)) | (0x3 << 20);
while (1) {
// 点亮 LED(PC13 低电平有效)
GPIOC_OCTL |= (1 << 13);
delay(500000);
// 熄灭 LED
GPIOC_OCTL &= ~(1 << 13);
delay(500000);
}
}
编译命令:
riscv64-unknown-elf-gcc -march=rv32imac -mabi=ilp32 -nostdlib -T link.ld -o led_blink.elf led_blink.c
这里需要链接脚本 link.ld,指定代码段、数据段在 Flash 和 RAM 中的位置。篇幅有限,我就不贴完整脚本了,你可以在网上找到对应芯片的模板。
4.5 烧录与调试
编译出 .elf 文件后,有两种方式把它烧进去:
方式一:直接用 OpenOCD 烧录
openocd -f jlink_gd32vf103.cfg -f target_gd32vf103.cfg -c "program led_blink.elf verify reset exit"
这条命令会:烧录 → 校验 → 复位 → 退出。一气呵成。
方式二:通过 GDB 调试
先启动 OpenOCD(保持运行),然后在另一个终端启动 GDB:
riscv64-unknown-elf-gdb led_blink.elf
(gdb) target remote localhost:3333
(gdb) load
(gdb) monitor reset halt
(gdb) continue
我个人更推荐第二种方式。为什么?因为你可以单步调试、设断点、看寄存器。第一次点灯不亮是常有的事,用 GDB 调试能快速定位问题。
monitor reset halt 命令检查复位状态,才发现问题。所以建议你第一次点灯时,一定用 GDB 调试一遍,别偷懒。
4.6 调试环境配置总结
整个调试环境的流程,我用一张图来总结:
说白了,整个调试链路就是:GDB 发命令 → OpenOCD 翻译 → 调试器转成 SWD 信号 → 芯片 Debug Module 执行。任何一个环节断了,灯都不会亮。
4.7 常见问题与排查
第一次点灯不成功?别慌,我列几个常见坑:
- OpenOCD 找不到调试器:检查 USB 驱动,Linux 下用
lsusb看设备是否识别 - 连接超时:检查 SWD 接线,特别是 SWDIO 和 SWCLK 有没有接反
- 烧录成功但灯不亮:用 GDB 检查 GPIO 寄存器,看时钟有没有使能
- 复位后程序不运行:检查 Boot 引脚电平,有些芯片需要拉高或拉低才能从 Flash 启动
好了,到这一步,你的 RISC-V 开发环境已经完整搭建起来了。能编译、能烧录、能调试。接下来就是愉快的点灯时光——虽然只是让一个 LED 闪烁,但这背后是整个工具链的胜利。
公众号:蓝海资料掘金营,微信deep3321