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安装步骤:
- 打开 VSCode
- 按
Ctrl+Shift+X打开扩展面板 - 搜索 "Cortex-Debug"
- 点击 "Install" 安装
2. 安装 OpenOCD
Linux (Ubuntu/Debian):
sudo apt update
sudo apt install openocdmacOS:
brew install openocdWindows: 从 OpenOCD 官网 下载预编译版本,或使用 MSYS2:
pacman -S mingw-w64-x86_64-openocd3. 验证安装
# 检查 OpenOCD 版本
openocd --version
# 检查 ARM 工具链
arm-none-eabi-gdb --version调试器连接
ST-Link V2 接线图
ST-Link V2 Blue Pill
───────── ──────────
SWDIO <───> SWDIO
SWCLK <───> SWCLK
GND <───> GND
3.3V <───> 3.3V (可选,用于供电)⚠️ 注意:确保 Blue Pill 的 BOOT0 跳线设置为 0(接地),否则无法进入调试模式。
⚠️ 已知问题与修复
launch.json 配置错误
问题描述:项目模板中的 .vscode/launch.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": ""
}
]
}正确配置
{
"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": ""
}
]
}修复步骤
打开配置文件
bash# 在项目根目录下 code .vscode/launch.json修改可执行文件路径
- 将
"executable": "build/STM32Demo.elf" - 改为
"executable": "build/STM32F1.elf"
- 将
保存文件
- 按
Ctrl+S保存
- 按
验证修复
bash# 确认 ELF 文件存在 ls -la build/STM32F1.elf
为什么会出现这个问题?
| 原因 | 说明 |
|---|---|
| 模板命名不一致 | CMakeLists.txt 中项目名为 STM32F1,但 launch.json 使用了示例名称 |
| 文件名来源 | STM32F1.elf 由 project(STM32F1 ...) 定义生成 |
| 影响 | 调试器无法找到可执行文件,导致启动失败 |
🚀 启动调试会话
前置条件
在启动调试之前,确保:
- ✅ 项目已编译(存在
build/STM32F1.elf) - ✅ 调试器已连接(ST-Link/J-Link)
- ✅ 目标板已供电
- ✅ launch.json 配置正确
启动步骤
方法 1:快捷键启动
- 打开 VSCode
- 按
F5启动调试 - 选择 "STM32 Debug" 配置(如有多个配置)
方法 2:调试面板启动
- 点击左侧活动栏的 "Run and Debug" 图标(或按
Ctrl+Shift+D) - 在顶部下拉菜单选择 "STM32 Debug"
- 点击绿色播放按钮
调试启动流程
┌─────────────────┐
│ 按 F5 启动 │
└────────┬────────┘
▼
┌─────────────────┐
│ VSCode 读取 │
│ launch.json │
└────────┬────────┘
▼
┌─────────────────┐
│ 启动 OpenOCD │
│ 连接调试器 │
└────────┬────────┘
▼
┌─────────────────┐
│ 加载 ELF 文件 │
│ 到 GDB │
└────────┬────────┘
▼
┌─────────────────┐
│ 程序停在 main() │
│ 等待用户操作 │
└─────────────────┘启动成功标志
当调试会话成功启动时,您会看到:
- 底部状态栏:显示 "Cortex-Debug: Running" 或 "Paused"
- 调试工具栏:出现在编辑器顶部
- 调试控制台:显示 GDB 输出信息
- 断点高亮:如果设置了断点,程序会暂停在那里
🎯 断点调试
设置断点
方法 1:点击行号
在代码编辑器中,点击行号左侧的空白区域,会出现一个红色圆点,表示断点已设置。
45 │ HAL_Init();
46 │ SystemClock_Config();
47 │ MX_GPIO_Init();
🔴48 │ MX_USART1_UART_Init(); // ← 点击这里设置断点
49 │
50 │ while (1)
51 │ {方法 2:右键菜单
- 右键点击代码行
- 选择 "Add Breakpoint"
方法 3:快捷键
- 将光标移动到目标行
- 按
F9切换断点
断点类型
| 类型 | 图标 | 说明 |
|---|---|---|
| 普通断点 | 🔴 红色圆点 | 程序执行到此暂停 |
| 条件断点 | 🔴 红色圆点 + "?" | 满足条件时暂停 |
| 日志断点 | 💬 菱形 | 输出日志但不暂停 |
| 禁用断点 | ⚪ 空心圆点 | 断点已禁用 |
设置条件断点
- 右键点击断点
- 选择 "Edit Breakpoint..."
- 输入条件表达式,例如:c
i == 100 - 程序只有在
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() 函数内部调试工具栏
┌─────────────────────────────────────────────────────────┐
│ ▶️ ⏭️ ⏬ ⏫ 🔄 ⏹️ │
│ │ │ │ │ │ │ │
│ │ │ │ │ │ └── 停止调试 │
│ │ │ │ │ └────── 重启调试 │
│ │ │ │ └────────── 单步返回 │
│ │ │ └────────────── 单步进入 │
│ │ └────────────────── 单步跳过 │
│ └────────────────────── 继续运行 │
└─────────────────────────────────────────────────────────┘👀 变量监视与寄存器查看
变量面板
在左侧调试面板的 变量 区域,可以查看:
局部变量
自动显示当前作用域内的局部变量:
// 在断点处暂停时
void blink_led(void) {
uint32_t counter = 0; // ← Locals 区域显示
GPIO_PinState state; // ← Locals 区域显示
while (1) {
counter++; // ← 值会实时更新
...
}
}全局变量
在 Watch 面板中添加全局变量:
- 点击 "Watch" 区域的 "+" 按钮
- 输入变量名,如
g_uart_buffer - 变量值会实时显示
监视表达式
添加监视
- 在 Watch 面板点击 "+"
- 输入表达式,例如:
counter- 变量值array[0]- 数组元素ptr->member- 结构体成员GPIOA->ODR- 外设寄存器
实用监视表达式
| 表达式 | 说明 |
|---|---|
HAL_GetTick() | 当前系统滴答计数 |
GPIOA->ODR | GPIOA 输出数据寄存器 |
USART1->DR | USART1 数据寄存器 |
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 中添加:
{
"svdFile": "${workspaceFolder}/STM32F103.svd"
}查看外设
- 在调试暂停时,展开 Peripherals 面板
- 选择外设(如 GPIOA)
- 查看寄存器值
Peripherals
├── GPIOA
│ ├── CRL: 0x44444444
│ ├── CRH: 0x44444444
│ ├── IDR: 0x0000FFFF
│ ├── ODR: 0x00000000
│ └── ...💡 调试技巧
1. LED 视觉调试
使用 LED 作为程序状态的视觉指示器:
// 在关键位置切换 LED
void critical_function(void) {
LED_ON(); // 进入函数
// ... 执行关键代码 ...
LED_OFF(); // 函数完成
}状态编码
通过 LED 闪烁模式表示不同状态:
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 风格调试(串口输出)
通过串口输出调试信息:
配置串口重定向
// 在 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使用示例
// 在关键位置输出调试信息
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):
# Linux/macOS
minicom -D /dev/ttyUSB0 -b 115200
# 或使用 screen
screen /dev/ttyUSB0 1152003. 断言检查
使用断言在开发阶段捕获错误:
#include <assert.h>
void set_gpio_pin(GPIO_TypeDef *GPIOx, uint16_t Pin) {
// 检查参数有效性
assert(GPIOx != NULL);
assert(Pin < 16);
// ... 设置 GPIO ...
}自定义断言处理
// 在 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)检测死循环:
// 初始化看门狗
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. 内存调试
检查栈溢出
// 在链接脚本中检查栈使用
// 或使用静态分析工具
// 运行时检查栈指针
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);
}堆内存检查
// 如果使用动态内存分配
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 found | ELF 文件不存在 | 先编译项目 |
Error: program not halted | 程序正在运行 | 按复位按钮后重试 |
程序异常行为
硬件故障(Hard Fault)
当程序进入 Hard Fault 时:
// 在 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 连接测试
# 手动启动 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)
# 添加 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: 调试时程序不暂停在断点?
可能原因:
- 断点设置在优化掉的代码
- 编译优化级别过高
解决方法:
# 在 CMakeLists.txt 中降低优化级别
add_compile_options(-O0 -g3) # 无优化,最大调试信息Q2: 变量值显示不正确?
可能原因:
- 编译器优化导致变量被优化掉
- 变量作用域已结束
解决方法:
// 使用 volatile 防止优化
volatile uint32_t debug_counter;
// 或在编译时禁用优化
// add_compile_options(-O0)Q3: 无法进入 HAL 库函数?
解决方法:
- 确保 HAL 库编译时包含调试信息
- 在
CMakeLists.txt中添加:cmakeadd_compile_options(-g3) # 最大调试信息
Q4: 调试速度很慢?
可能原因:
- SWD 时钟频率过低
- 断点数量过多
解决方法:
// 在 launch.json 中提高时钟频率
"swdClock": 4000, // 4 MHzQ5: 如何查看汇编代码?
在调试暂停时:
- 右键点击代码编辑器
- 选择 "Open Disassembly View"
- 查看对应的汇编指令
Q6: 如何调试中断?
// 在中断处理函数中设置断点
void EXTI0_IRQHandler(void) {
// 在这里设置断点
if (__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_0)) {
__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_0);
// 中断处理代码
}
}📚 进阶主题
RTOS 调试
如果使用 FreeRTOS,需要配置:
// launch.json 添加
"rtos": "FreeRTOS"多核调试
对于多核芯片(如 STM32H7),需要配置多个调试目标:
{
"configurations": [
{
"name": "Cortex-M7",
"type": "cortex-debug",
"cpu": "Cortex-M7"
},
{
"name": "Cortex-M4",
"type": "cortex-debug",
"cpu": "Cortex-M4"
}
]
}半主机模式(Semihosting)
使用半主机模式通过调试器输出 printf:
// 初始化代码
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 - 学习编译和烧录流程。