在嵌入式开发过程中,调试是最让人头疼的环节之一。传统的调试方式主要有两种:串口打印和IO口调试。这两种方法我都用过很多次,说实话,效率真的不高。
先说串口调试,最大的问题是硬件限制。很多小型嵌入式设备根本没有串口接口,或者因为PCB空间限制无法引出串口。更麻烦的是,当设备死机时,串口往往也跟着罢工,这时候你根本拿不到任何调试信息。我遇到过好几次这种情况,设备莫名其妙死机,串口一片寂静,只能干瞪眼。
IO口调试就更原始了,通常只能通过LED闪烁或者示波器观察几个GPIO的状态。这种方式能获取的信息量极其有限,对于复杂的程序逻辑调试基本没用。有一次我调试一个RTOS任务调度问题,用IO口调试折腾了一周都没找到原因,最后还是靠JLink Commander解决了问题。
JLink Commander的优势就很明显了。首先,它通过调试接口直接访问芯片内核,不受外设限制。即使设备死机,也能获取到关键寄存器状态和内存数据。其次,它能实时读取RAM和Flash中的数据,结合map文件可以直接查看代码中的变量值。最重要的是,它能和JLink RTT配合使用,实现无干扰的实时日志输出。
要使用JLink Commander,首先得准备好硬件。我建议至少使用JLink V9以上的调试器,V8虽然也能用,但性能和稳定性差不少。我手头有个V8和一个V9,实测V9的连接速度和稳定性明显更好。
目标设备需要支持SWD或JTAG调试接口。现在大多数Cortex-M系列芯片都支持SWD,只需要4根线(VCC、GND、SWDIO、SWCLK)就能调试,比传统的20针JTAG方便多了。不过要注意,有些国产芯片的SWD接口可能不太标准,这时候可能需要调整连接速度。
软件方面需要安装JLink驱动,建议用最新版本。我目前用的是V7.88b,这个版本对RTT的支持比较完善。安装过程很简单,去SEGGER官网下载安装包,一路下一步就行。
安装完成后,可以在命令行输入JLinkExe启动JLink Commander。第一次连接设备时,系统会提示选择芯片型号。这里有个小技巧:如果列表里没有你的芯片型号,可以选择同系列的通用型号。比如我用富芮坤FR8018时,就选的Cortex-M3。
连接参数设置也很重要。SWD接口默认速度是1000kHz,但如果连接不稳定,可以尝试降低速度。我一般先用自动速度连接,如果失败再手动调整。命令是这样的:
bash复制J-Link> speed 4000
J-Link> device Cortex-M3
J-Link> connect
halt和go是最常用的两个命令。halt让芯片暂停运行,go让芯片继续运行。这两个命令配合使用,可以实现类似断点的效果。
我调试死机问题时,会先用halt暂停芯片,然后查看寄存器状态。执行halt后,终端会显示PC指针、SP指针等关键寄存器值。比如:
bash复制J-Link> halt
PC = 08001234, SP = 2000FFFC
这里PC指针指向的地址就是当前执行的代码位置。结合map文件,可以快速定位到出问题的函数。
go命令的使用也很简单:
bash复制J-Link> go
但要注意,有些芯片在halt状态下外设可能不正常工作,所以调试完记得用go恢复运行。
内存操作是调试的核心功能。mem命令可以读取内存数据,支持8/16/32位读取。我常用的是mem32,因为大多数变量都是32位的。
比如要读取0x20000000开始的16个字节:
bash复制J-Link> mem32 0x20000000 4
这里第二个参数是字数,不是字节数。因为mem32每次读4字节,所以读16字节需要设置4。
write命令用于写入内存。我经常用它来修改全局变量的值,测试不同情况下的程序行为。例如:
bash复制J-Link> write32 0x20000000 0x12345678
这个命令把0x12345678写入了0x20000000地址。
结合map文件调试是最实用的技巧。map文件是编译器生成的,包含了所有函数和变量的地址信息。当程序死机时,先用halt获取PC指针,然后在map文件中搜索这个地址,就能找到出问题的函数。
我有个实际案例:设备偶尔会死机,PC指针显示在0x08001234。查map文件发现这是task_scheduler函数。进一步查看该函数的局部变量,发现一个指针变量被错误地修改了。最终发现是堆栈溢出导致的。
另一个技巧是用setpc修改PC指针。这在测试异常处理时很有用。比如:
bash复制J-Link> setpc 0x08001111
这个命令把PC指针设到0x08001111,然后执行go,程序就会从新地址开始运行。
RTT(Real Time Transfer)是SEGGER开发的一种实时数据传输技术。相比串口,RTT有几个明显优势:
RTT的工作原理是在目标内存中开辟一块缓冲区,主机通过调试接口访问这个缓冲区。因为不占用串口资源,所以特别适合那些没有串口或者串口被占用的设备。
要在项目中使用RTT,首先需要添加SEGGER_RTT库。这个库可以在SEGGER官网下载,也可以使用JLink安装目录下的版本。
初始化很简单,只需要包含头文件并调用初始化函数:
c复制#include "SEGGER_RTT.h"
void main() {
SEGGER_RTT_Init();
while(1) {
SEGGER_RTT_printf(0, "System running, tick=%d\n", HAL_GetTick());
}
}
输出日志使用SEGGER_RTT_printf,用法和printf差不多。第一个参数是通道号,0表示默认通道。
在JLink Commander中使用RTT需要先启用RTT功能:
bash复制J-Link> exec EnableRTT
然后可以另开一个终端,使用JLinkRTTClient查看日志:
bash复制JLinkRTTClient
RTT支持多通道,可以用来分类输出日志。比如通道0输出普通日志,通道1输出错误日志,通道2输出调试信息。
我通常这样配置:
c复制#define LOG_NORMAL 0
#define LOG_ERROR 1
#define LOG_DEBUG 2
SEGGER_RTT_ConfigUpBuffer(LOG_NORMAL, "Normal", NULL, 0, SEGGER_RTT_MODE_NO_BLOCK_SKIP);
SEGGER_RTT_ConfigUpBuffer(LOG_ERROR, "Error", NULL, 0, SEGGER_RTT_MODE_NO_BLOCK_SKIP);
SEGGER_RTT_ConfigUpBuffer(LOG_DEBUG, "Debug", NULL, 0, SEGGER_RTT_MODE_NO_BLOCK_SKIP);
RTT还支持下行通道,可以用来从主机发送命令给设备。这在产品测试时特别有用,不需要额外接口就能实现测试命令的下发。
去年我遇到一个棘手的死机问题:设备运行几天后会随机死机。因为死机时串口已经不工作,传统方法很难调试。
我的解决步骤是:
halt命令暂停芯片mem命令读取堆栈内容最终发现是一个任务堆栈溢出,覆盖了相邻的内存区域。通过增大该任务的堆栈大小解决了问题。
在开发电机控制算法时,需要实时监控电流、速度等参数。传统方法是用串口输出,但会影响控制时序。
我用RTT解决了这个问题:
SEGGER_RTT_Write输出关键参数这样既不影响实时性,又能获取高质量的调试数据。
我们还把JLink Commander用在了量产测试中。测试脚本通过JLink Commander接口:
相比传统的测试方案,这套系统更稳定可靠,测试速度也更快。一个典型的测试脚本如下:
bash复制#!/bin/bash
# 进入JLink Commander
JLinkExe <<EOF
device STM32F103
speed 4000
connect
erase
loadfile firmware.hex
verifybin firmware.hex 0x08000000
w4 0x0800FFFC 0x12345678
go
exit
EOF
这套方案已经在我们生产线稳定运行了2年多,大大提高了生产效率。