在日常开发中,总会遇到一些出人意料的bug,程序跑飞,上电就挂,程序没有按预期执行诸如此类的问题,没有好的调试方法,真的很难定位问题,更别说解决了。在这里分享我用过的一些调试方法,抛砖引玉。
MCU调试
学生时代大家都知道在线调试,这个可以实时查看外设寄存器、cpu通用寄存器数据以及堆栈,可以打上软断点(有数量限制)。这种调试属于侵入式调试,有缺点。
咱们以stm32为例,使用 J-Link 进行调试时,涉及的调试技术和接口主要是 JTAG 或 SWD(Serial Wire Debug),这两种接口允许调试器控制目标微控制器的状态,执行读取内存、设置断点等操作。这种调试方式属于侵入式调试,因为它会对系统的正常运行产生干扰。接下来,我将详细解释这种调试的具体实现及其原理。
调试接口
STM32 微控制器通常支持以下两种调试接口:
- JTAG(Joint Test Action Group):这是早期常见的调试协议,使用多个引脚进行调试,通常需要 5-6 根信号线(如 TCK、TMS、TDI、TDO、TRST 等)。这种接口提供了比较丰富的调试和测试功能。
- SWD(Serial Wire Debug):这是 ARM Cortex-M 微控制器系列的标准调试接口,它是 JTAG 的简化版,只需要 2 根信号线(SWDIO 和 SWCLK)就能进行全功能的调试和编程,因此更节省引脚资源,尤其适用于嵌入式系统。
侵入式调试原理
侵入式调试的核心是在调试器与 MCU 之间建立一种交互式控制,通过调试接口向 MCU 发出命令,读取 MCU 状态或改变其执行流程。调试器会通过调试接口连接 MCU 的内部调试单元(例如 ARM Cortex-M 核心中的 CoreSight 调试架构)。具体操作如下:
步骤:
- 连接调试接口:J-Link 通过 JTAG 或 SWD 连接到 STM32 芯片,调试器可以通过这些接口访问 MCU 的内部寄存器、内存和其他调试资源。
- 调试器控制 MCU:
- 单步执行(Single Stepping):调试器能够让 MCU 一次执行一条指令,方便用户逐行查看代码的执行情况。
- 设置断点(Breakpoint):用户可以在程序的某个位置设置断点,当 MCU 执行到这个位置时,它会自动暂停,让用户检查程序状态。
- 修改寄存器/内存:调试器可以直接修改 MCU 内部寄存器或存储器的值,允许用户在不重新编译代码的情况下改变程序的行为。
- 暂停和恢复运行:在调试过程中,调试器会暂停 MCU 的正常执行,这就是侵入式调试的侵入性部分。暂停后,调试器可以获取当前的寄存器值、内存内容等调试信息。调试结束后,调试器会恢复 MCU 的运行。
调试时的影响:
- 实时性丢失:由于在调试过程中 MCU 被暂停执行,因此 MCU 不再能够实时响应外部事件。这在某些需要实时响应的系统中可能会引发问题。
- 调试开销:虽然 SWD 是一种较为轻量的调试方式,但依然需要占用部分系统资源,如调试接口的引脚、调试单元的部分 CPU 时钟等。
非侵入式调试对比
相比之下,非侵入式调试不会干扰系统的正常运行。ARM Cortex-M 核心提供了 ETM(Embedded Trace Macrocell) 等调试模块,允许程序运行时记录执行流,并通过外部工具分析,但不改变程序的运行状态。这种方式不需要暂停 MCU,也不会影响其实时性。调试工具有J-trace、劳特巴赫(好用),缺点贵,和canoe有一拼。
J-link调试
J-Link 调试器主要依赖编译生成的 可调试文件 来执行调试操作,而非直接使用 .hex
文件。
通常,调试器需要的是包含符号信息的文件,例如:
- .elf(Executable and Linkable Format):这是最常用的文件格式,包含完整的调试符号和程序代码。调试器通过读取这个文件,能够将内