Skip to content

05 - 调试方法

本章目标:掌握 STM32F103C8T6 项目的调试方法,学会使用 VSCode + Cortex-Debug 进行断点调试、变量监视和故障排查。


📋 目录


🔧 调试环境配置

硬件要求

组件说明
调试器ST-Link V2 或 J-Link
连接方式SWD (SWDIO, SWCLK, GND)
目标板STM32F103C8T6 Blue Pill

软件要求

1. 安装 VSCode 扩展

在 VSCode 中安装 Cortex-Debug 扩展:

扩展 ID: marus25.cortex-debug

安装步骤:

  1. 打开 VSCode
  2. Ctrl+Shift+X 打开扩展面板
  3. 搜索 "Cortex-Debug"
  4. 点击 "Install" 安装

2. 安装 OpenOCD

Linux (Ubuntu/Debian):

bash
sudo apt update
sudo apt install openocd

macOS:

bash
brew install openocd

Windows:OpenOCD 官网 下载预编译版本,或使用 MSYS2:

bash
pacman -S mingw-w64-x86_64-openocd

3. 验证安装

bash
# 检查 OpenOCD 版本
openocd --version

# 检查 ARM 工具链
arm-none-eabi-gdb --version

调试器连接

ST-Link V2          Blue Pill
─────────           ──────────
  SWDIO    <───>      SWDIO
  SWCLK    <───>      SWCLK
  GND      <───>      GND
  3.3V     <───>      3.3V (可选,用于供电)

⚠️ 注意:确保 Blue Pill 的 BOOT0 跳线设置为 0(接地),否则无法进入调试模式。


⚠️ 已知问题与修复

launch.json 配置错误

问题描述:项目模板中的 .vscode/launch.json 配置存在可执行文件路径错误。

错误配置

json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "STM32 Debug",
            "type": "cortex-debug",
            "request": "launch",
            "servertype": "openocd",
            "cwd": "${workspaceRoot}",
            "executable": "build/STM32Demo.elf",  // ❌ 错误:文件不存在
            "configFiles": [
                "interface/stlink.cfg",
                "target/stm32f1x.cfg"
            ],
            "runToEntryPoint": "main",
            "svdFile": ""
        }
    ]
}

正确配置

json
{
    "version": "0.2.0",
    "configurations": [
        {
            "name": "STM32 Debug",
            "type": "cortex-debug",
            "request": "launch",
            "servertype": "openocd",
            "cwd": "${workspaceRoot}",
            "executable": "build/STM32F1.elf",  // ✅ 正确:实际生成的文件名
            "configFiles": [
                "interface/stlink.cfg",
                "target/stm32f1x.cfg"
            ],
            "runToEntryPoint": "main",
            "svdFile": ""
        }
    ]
}

修复步骤

  1. 打开配置文件

    bash
    # 在项目根目录下
    code .vscode/launch.json
  2. 修改可执行文件路径

    • "executable": "build/STM32Demo.elf"
    • 改为 "executable": "build/STM32F1.elf"
  3. 保存文件

    • Ctrl+S 保存
  4. 验证修复

    bash
    # 确认 ELF 文件存在
    ls -la build/STM32F1.elf

为什么会出现这个问题?

原因说明
模板命名不一致CMakeLists.txt 中项目名为 STM32F1,但 launch.json 使用了示例名称
文件名来源STM32F1.elfproject(STM32F1 ...) 定义生成
影响调试器无法找到可执行文件,导致启动失败

🚀 启动调试会话

前置条件

在启动调试之前,确保:

  1. ✅ 项目已编译(存在 build/STM32F1.elf
  2. ✅ 调试器已连接(ST-Link/J-Link)
  3. ✅ 目标板已供电
  4. ✅ launch.json 配置正确

启动步骤

方法 1:快捷键启动

  1. 打开 VSCode
  2. F5 启动调试
  3. 选择 "STM32 Debug" 配置(如有多个配置)

方法 2:调试面板启动

  1. 点击左侧活动栏的 "Run and Debug" 图标(或按 Ctrl+Shift+D
  2. 在顶部下拉菜单选择 "STM32 Debug"
  3. 点击绿色播放按钮

调试启动流程

┌─────────────────┐
│  按 F5 启动     │
└────────┬────────┘

┌─────────────────┐
│ VSCode 读取     │
│ launch.json     │
└────────┬────────┘

┌─────────────────┐
│ 启动 OpenOCD    │
│ 连接调试器      │
└────────┬────────┘

┌─────────────────┐
│ 加载 ELF 文件   │
│ 到 GDB          │
└────────┬────────┘

┌─────────────────┐
│ 程序停在 main() │
│ 等待用户操作    │
└─────────────────┘

启动成功标志

当调试会话成功启动时,您会看到:

  1. 底部状态栏:显示 "Cortex-Debug: Running" 或 "Paused"
  2. 调试工具栏:出现在编辑器顶部
  3. 调试控制台:显示 GDB 输出信息
  4. 断点高亮:如果设置了断点,程序会暂停在那里

🎯 断点调试

设置断点

方法 1:点击行号

在代码编辑器中,点击行号左侧的空白区域,会出现一个红色圆点,表示断点已设置。

  45 │     HAL_Init();
  46 │     SystemClock_Config();
  47 │     MX_GPIO_Init();
🔴48 │     MX_USART1_UART_Init();  // ← 点击这里设置断点
  49 │ 
  50 │     while (1)
  51 │     {

方法 2:右键菜单

  1. 右键点击代码行
  2. 选择 "Add Breakpoint"

方法 3:快捷键

  • 将光标移动到目标行
  • F9 切换断点

断点类型

类型图标说明
普通断点🔴 红色圆点程序执行到此暂停
条件断点🔴 红色圆点 + "?"满足条件时暂停
日志断点💬 菱形输出日志但不暂停
禁用断点⚪ 空心圆点断点已禁用

设置条件断点

  1. 右键点击断点
  2. 选择 "Edit Breakpoint..."
  3. 输入条件表达式,例如:
    c
    i == 100
  4. 程序只有在 i == 100 时才会暂停

断点管理

断点面板(左侧调试面板)中可以:

  • 查看所有断点
  • 启用/禁用断点
  • 删除断点
  • 编辑断点条件

🎮 调试控制

基本操作

操作快捷键说明
继续运行F5运行到下一个断点
单步跳过F10执行当前行,不进入函数
单步进入F11进入函数内部
单步返回Shift+F11执行完当前函数并返回
重启调试Ctrl+Shift+F5重新启动调试会话
停止调试Shift+F5终止调试会话

执行流程示例

当前代码:
    48 │     MX_USART1_UART_Init();  // ← 当前位置
    49 │ 
    50 │     while (1)
    51 │     {
    52 │         HAL_GPIO_TogglePin(LED_GPIO_Port, LED_Pin);

操作:按 F10 (Step Over)
结果:执行 MX_USART1_UART_Init(),然后停在第 50 行

操作:按 F11 (Step Into)
结果:进入 MX_USART1_UART_Init() 函数内部

调试工具栏

┌─────────────────────────────────────────────────────────┐
│ ▶️  ⏭️  ⏬  ⏫  🔄  ⏹️                                   │
│ │   │   │   │   │   │                                   │
│ │   │   │   │   │   └── 停止调试                        │
│ │   │   │   │   └────── 重启调试                        │
│ │   │   │   └────────── 单步返回                        │
│ │   │   └────────────── 单步进入                        │
│ │   └────────────────── 单步跳过                        │
│ └────────────────────── 继续运行                        │
└─────────────────────────────────────────────────────────┘

👀 变量监视与寄存器查看

变量面板

在左侧调试面板的 变量 区域,可以查看:

局部变量

自动显示当前作用域内的局部变量:

c
// 在断点处暂停时
void blink_led(void) {
    uint32_t counter = 0;      // ← Locals 区域显示
    GPIO_PinState state;       // ← Locals 区域显示
    
    while (1) {
        counter++;             // ← 值会实时更新
        ...
    }
}

全局变量

Watch 面板中添加全局变量:

  1. 点击 "Watch" 区域的 "+" 按钮
  2. 输入变量名,如 g_uart_buffer
  3. 变量值会实时显示

监视表达式

添加监视

  1. 在 Watch 面板点击 "+"
  2. 输入表达式,例如:
    • counter - 变量值
    • array[0] - 数组元素
    • ptr->member - 结构体成员
    • GPIOA->ODR - 外设寄存器

实用监视表达式

表达式说明
HAL_GetTick()当前系统滴答计数
GPIOA->ODRGPIOA 输出数据寄存器
USART1->DRUSART1 数据寄存器
RCC->CR时钟控制寄存器
*(uint32_t*)0x20000000查看特定内存地址

寄存器查看

Call Stack 面板下方,展开 Registers 可以查看:

ARM Cortex-M3 核心寄存器

寄存器说明
R0-R12通用寄存器
SP (R13)堆栈指针
LR (R14)链接寄存器
PC (R15)程序计数器
xPSR程序状态寄存器

特殊寄存器

寄存器说明
MSP主堆栈指针
PSP进程堆栈指针
PRIMASK异常屏蔽寄存器
CONTROL控制寄存器

外设寄存器查看

如果配置了 SVD 文件,可以在 Peripherals 面板查看外设寄存器:

配置 SVD 文件

launch.json 中添加:

json
{
    "svdFile": "${workspaceFolder}/STM32F103.svd"
}

查看外设

  1. 在调试暂停时,展开 Peripherals 面板
  2. 选择外设(如 GPIOA)
  3. 查看寄存器值
Peripherals
├── GPIOA
│   ├── CRL: 0x44444444
│   ├── CRH: 0x44444444
│   ├── IDR: 0x0000FFFF
│   ├── ODR: 0x00000000
│   └── ...

💡 调试技巧

1. LED 视觉调试

使用 LED 作为程序状态的视觉指示器:

c
// 在关键位置切换 LED
void critical_function(void) {
    LED_ON();  // 进入函数
    
    // ... 执行关键代码 ...
    
    LED_OFF(); // 函数完成
}

状态编码

通过 LED 闪烁模式表示不同状态:

c
void indicate_error(uint8_t error_code) {
    for (int i = 0; i < error_code; i++) {
        LED_ON();
        HAL_Delay(200);
        LED_OFF();
        HAL_Delay(200);
    }
    HAL_Delay(1000);  // 间隔 1 秒
}

// 使用示例
if (init_failed) {
    indicate_error(3);  // 闪烁 3 次 = 初始化失败
}

2. printf 风格调试(串口输出)

通过串口输出调试信息:

配置串口重定向

c
// 在 main.c 或单独文件中添加
#include <stdio.h>

// 重定向 printf 到 USART1
#ifdef __GNUC__
int __io_putchar(int ch) {
    HAL_UART_Transmit(&huart1, (uint8_t *)&ch, 1, HAL_MAX_DELAY);
    return ch;
}
#endif

使用示例

c
// 在关键位置输出调试信息
void process_data(uint8_t *data, uint32_t len) {
    printf("Processing %lu bytes\r\n", len);
    
    for (uint32_t i = 0; i < len; i++) {
        printf("data[%lu] = 0x%02X\r\n", i, data[i]);
    }
    
    printf("Processing complete\r\n");
}

查看串口输出

使用串口终端工具(如 minicom、screen、PuTTY):

bash
# Linux/macOS
minicom -D /dev/ttyUSB0 -b 115200

# 或使用 screen
screen /dev/ttyUSB0 115200

3. 断言检查

使用断言在开发阶段捕获错误:

c
#include <assert.h>

void set_gpio_pin(GPIO_TypeDef *GPIOx, uint16_t Pin) {
    // 检查参数有效性
    assert(GPIOx != NULL);
    assert(Pin < 16);
    
    // ... 设置 GPIO ...
}

自定义断言处理

c
// 在 stm32f1xx_hal_conf.h 或单独文件中
#define assert_param(expr) ((expr) ? (void)0 : assert_failed((uint8_t *)__FILE__, __LINE__))

void assert_failed(uint8_t *file, uint32_t line) {
    printf("Assertion failed: %s:%lu\r\n", file, line);
    while (1);  // 停在这里等待调试
}

4. 看门狗调试

使用独立看门狗(IWDG)检测死循环:

c
// 初始化看门狗
void init_watchdog(void) {
    hiwdg.Instance = IWDG;
    hiwdg.Init.Prescaler = IWDG_PRESCALER_256;
    hiwdg.Init.Reload = 0xFFF;
    HAL_IWDG_Init(&hiwdg);
}

// 在主循环中喂狗
while (1) {
    HAL_IWDG_Refresh(&hiwdg);  // 喂狗
    
    // ... 主循环代码 ...
}

💡 提示:如果程序卡死在看门狗复位,说明主循环中有阻塞代码。

5. 内存调试

检查栈溢出

c
// 在链接脚本中检查栈使用
// 或使用静态分析工具

// 运行时检查栈指针
uint32_t get_stack_usage(void) {
    extern uint32_t _estack;  // 栈顶地址
    extern uint32_t _Min_Stack_Size;
    
    uint32_t sp;
    __asm volatile ("mov %0, sp" : "=r" (sp));
    
    return (_estack - sp);
}

堆内存检查

c
// 如果使用动态内存分配
void check_heap(void) {
    extern uint32_t _end;
    extern uint32_t _Heap_Limit;
    
    printf("Heap start: 0x%08lX\r\n", (uint32_t)&_end);
    printf("Heap limit: 0x%08lX\r\n", (uint32_t)&_Heap_Limit);
}

🔍 故障排查流程

调试无法启动

问题诊断流程图

┌─────────────────────────────────────────────────────────┐
│ 调试无法启动                                             │
└─────────────────────┬───────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ 检查硬件连接                                            │
│ - ST-Link 是否正确连接?                                │
│ - 目标板是否供电?                                      │
│ - BOOT0 是否为 0?                                      │
└─────────────────────┬───────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ 检查 OpenOCD                                            │
│ - openocd --version 是否正常?                          │
│ - 是否有其他 OpenOCD 进程占用?                         │
└─────────────────────┬───────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ 检查 ELF 文件                                           │
│ - build/STM32F1.elf 是否存在?                          │
│ - launch.json 路径是否正确?                            │
└─────────────────────┬───────────────────────────────────┘

┌─────────────────────────────────────────────────────────┐
│ 检查 VSCode 扩展                                        │
│ - Cortex-Debug 是否已安装?                             │
│ - 是否有版本冲突?                                      │
└─────────────────────────────────────────────────────────┘

常见错误及解决

错误信息可能原因解决方法
Error: open failed调试器未连接检查 USB 连接
Error: target not found目标芯片未响应检查 SWD 连接、BOOT0 设置
Error: executable not foundELF 文件不存在先编译项目
Error: program not halted程序正在运行按复位按钮后重试

程序异常行为

硬件故障(Hard Fault)

当程序进入 Hard Fault 时:

c
// 在 stm32f1xx_it.c 中添加
void HardFault_Handler(void) {
    printf("Hard Fault!\r\n");
    printf("SCB->HFSR = 0x%08lX\r\n", SCB->HFSR);
    printf("SCB->CFSR = 0x%08lX\r\n", SCB->CFSR);
    while (1);
}

常见故障原因

故障类型可能原因排查方法
栈溢出递归太深、大数组减小栈使用、增大栈空间
空指针未初始化指针检查指针使用前是否初始化
数组越界索引超出范围检查数组边界条件
时钟错误外设时钟未使能检查 RCC 时钟配置

调试器连接问题

OpenOCD 连接测试

bash
# 手动启动 OpenOCD 测试连接
openocd -f interface/stlink.cfg -f target/stm32f1x.cfg

成功输出示例:

Open On-Chip Debugger 0.11.0
...
Info : clock speed 1000 kHz
Info : STLINK V2J14S0 (API v2) VID:PID 0483:3748
Info : Target voltage: 3.2
Info : stm32f1x.cpu: hardware has 6 breakpoints, 4 watchpoints
Info : starting gdb server for stm32f1x.cpu on 3333

权限问题(Linux)

bash
# 添加 udev 规则
sudo tee /etc/udev/rules.d/99-stlink.rules << EOF
SUBSYSTEM=="usb", ATTR{idVendor}=="0483", ATTR{idProduct}=="3748", MODE="0666"
EOF

# 重新加载规则
sudo udevadm control --reload-rules
sudo udevadm trigger

❓ 常见问题

Q1: 调试时程序不暂停在断点?

可能原因

  • 断点设置在优化掉的代码
  • 编译优化级别过高

解决方法

cmake
# 在 CMakeLists.txt 中降低优化级别
add_compile_options(-O0 -g3)  # 无优化,最大调试信息

Q2: 变量值显示不正确?

可能原因

  • 编译器优化导致变量被优化掉
  • 变量作用域已结束

解决方法

c
// 使用 volatile 防止优化
volatile uint32_t debug_counter;

// 或在编译时禁用优化
// add_compile_options(-O0)

Q3: 无法进入 HAL 库函数?

解决方法

  1. 确保 HAL 库编译时包含调试信息
  2. CMakeLists.txt 中添加:
    cmake
    add_compile_options(-g3)  # 最大调试信息

Q4: 调试速度很慢?

可能原因

  • SWD 时钟频率过低
  • 断点数量过多

解决方法

json
// 在 launch.json 中提高时钟频率
"swdClock": 4000,  // 4 MHz

Q5: 如何查看汇编代码?

在调试暂停时:

  1. 右键点击代码编辑器
  2. 选择 "Open Disassembly View"
  3. 查看对应的汇编指令

Q6: 如何调试中断?

c
// 在中断处理函数中设置断点
void EXTI0_IRQHandler(void) {
    // 在这里设置断点
    if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0)) {
        __HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
        // 中断处理代码
    }
}

📚 进阶主题

RTOS 调试

如果使用 FreeRTOS,需要配置:

json
// launch.json 添加
"rtos": "FreeRTOS"

多核调试

对于多核芯片(如 STM32H7),需要配置多个调试目标:

json
{
    "configurations": [
        {
            "name": "Cortex-M7",
            "type": "cortex-debug",
            "cpu": "Cortex-M7"
        },
        {
            "name": "Cortex-M4",
            "type": "cortex-debug",
            "cpu": "Cortex-M4"
        }
    ]
}

半主机模式(Semihosting)

使用半主机模式通过调试器输出 printf:

c
// 初始化代码
extern void initialise_monitor_handles(void);
initialise_monitor_handles();

// printf 会通过调试器输出
printf("Debug message\r\n");

📖 参考资料


📝 总结

本章介绍了 STM32F103C8T6 项目的调试方法,包括:

内容要点
环境配置Cortex-Debug 扩展、OpenOCD 安装
已知问题launch.json 可执行文件路径错误
断点调试设置断点、单步执行、继续运行
变量监视局部变量、全局变量、寄存器查看
调试技巧LED 调试、printf 调试、断言检查
故障排查连接问题、Hard Fault、权限问题

下一章06_stm32_basics - 深入了解 STM32 架构和外设。

上一章04_build_and_flash - 学习编译和烧录流程。

Built with VitePress