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 调试——反而更省事。

小提示: 如果你的调试器同时支持 JTAG 和 SWD,默认通常是 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

注意: 如果你用的是 Windows,建议用 MSYS2 或者 WSL。别在原生 Windows 下折腾 OpenOCD,驱动问题会让你怀疑人生。我曾经在 Windows 上花了一整天装驱动,最后换到 Linux 十分钟搞定。

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 调试能快速定位问题。

避坑指南: 我曾经遇到过烧录后灯不亮,折腾了半天发现是复位引脚没拉高。用 GDB 的 monitor reset halt 命令检查复位状态,才发现问题。所以建议你第一次点灯时,一定用 GDB 调试一遍,别偷懒。

4.6 调试环境配置总结

整个调试环境的流程,我用一张图来总结:

RISC-V 调试环境架构图 主机(Host PC) GDB + OpenOCD + 调试器驱动 USB 调试器(J-Link / ST-Link / CMSIS-DAP) JTAG / SWD 协议转换 SWD(2线) 目标板(RISC-V MCU) RISC-V 内核 | Debug Module | Flash | RAM GPIO → LED(PC13) 图:调试环境数据流(GDB → OpenOCD → 调试器 → 目标芯片)

说白了,整个调试链路就是:GDB 发命令 → OpenOCD 翻译 → 调试器转成 SWD 信号 → 芯片 Debug Module 执行。任何一个环节断了,灯都不会亮。

4.7 常见问题与排查

第一次点灯不成功?别慌,我列几个常见坑:

  • OpenOCD 找不到调试器:检查 USB 驱动,Linux 下用 lsusb 看设备是否识别
  • 连接超时:检查 SWD 接线,特别是 SWDIO 和 SWCLK 有没有接反
  • 烧录成功但灯不亮:用 GDB 检查 GPIO 寄存器,看时钟有没有使能
  • 复位后程序不运行:检查 Boot 引脚电平,有些芯片需要拉高或拉低才能从 Flash 启动
我的经验: 遇到问题先看 OpenOCD 的日志输出。它会把每个步骤都打印出来,比如「连接成功」「读取芯片 ID」「擦除 Flash」等。看到哪一步报错,问题就定位到哪一步。这比瞎猜快多了。

好了,到这一步,你的 RISC-V 开发环境已经完整搭建起来了。能编译、能烧录、能调试。接下来就是愉快的点灯时光——虽然只是让一个 LED 闪烁,但这背后是整个工具链的胜利。


公众号:蓝海资料掘金营,微信deep3321