一、调试前的准备
使用J-Link将PC和单片机连接
以STM32入门教程中的“读写内部FLASH”实验为例,使用Keil打开该工程
按照下图所示的步骤进行操作,如果使用J-Link仿真器,则在第三步选择ST-Link Debugger
按照下图所示的步骤进行操作,输出选项卡需要勾选调试信息
按照下图所示的步骤进行操作,代码生成的优化选项中,优化等级需要选为0,否则Keil在编译程序的过程中可能会过度优化,导致编译得到的二进制机器码和C语言代码不是等价的,在调试过程中会引起代码执行顺序错乱
完成以上步骤后,编译程序,然后将其下载到单片机中
二、Ozone调试工程快速创建
1、新建工程
(1)打开Ozone,在菜单栏选择“File”→“New”→“Project”,新建一个工程。(如果是初次使用,那么打开Ozone后会自动弹出向导,引导用户新建工程)
(2)在Device选择MCU型号(本教程使用STM32F103C8T6),并在Peripherals导入对应的.svd文件(芯片厂商提供,可生成外设寄存器树,直接在上位机查看和修改外设寄存器)。
(3)主机接口选择USB;目标接口视情况选择,对于ARM芯片,通过查看数据手册关于调试的章节,可以确认是否支持SWD,支持即可选择,而非ARM芯片仅支持JTAG;目标接口速率可以选择2MHz,若硬件稳定,则可以提速。
(4)选择编译输出的.elf文件,如果是使用Keil编译,则选择编译输出的.axf文件(.axf文件是ARM扩展版.elf文件),路径尽量不包含中文和全角字符。
(5)创建完成后,在菜单栏选择“File”→“Save Project as...”,即可保存调试工程。
2、三种调试启动模式
(1)Download & Reset Program:擦除Flash,将固件烧录到Flash中,然后复位MCU,释放复位后程序从头全速运行。
(2)Attach to Running Program:连接后程序保持运行,可观察设备实时的动态变量,不会破坏RAM堆栈、寄存器。
(3)Attach & Halt Program:连接后停止正在运行的程序,直接冻结现场,立刻捕获当前寄存器、堆栈、内存快照,不会破坏RAM堆栈、寄存器。
三、Ozone基础调试功能
1、断点系统与变量访问
(1)使用Ozone调试,Flash区不限制硬件断点数量(Cortex-M只有4~8个硬件断点,Keil调试受限于硬件数量),因为Ozone自动在Flash写入BKPT断点指令,缓存原始指令,断点取消后自动恢复代码。
(2)源码断点(程序断点):
①在代码浏览区的左侧有一些灰色的区域,鼠标左键点击,即可在区域对应的代码打上源码断点,当单片机运行到这些代码的时候,会触发停止运行的条件;如果需要取消源码断点,鼠标左键点击它对应的红色圆圈,即可取消断点。
②鼠标右键点击源码断点对应的红色圆圈,选择“Edit Breakpoint”,在弹出的窗口中可以设置断点触发的前置条件。
[1]Condition填入条件表达式,如cnt>100。如果选择“Trigger when true”,那么断点命中时,需要满足条件表达式才会真正停机;如果选择“Trigger on change”,那么断点命中时,表达式数值发生过变化,才会真正停机。
[2]Skpi填入断点命中次数,假如设置为499,那么断点命中500次才会真正停机。
(3)数据断点(访问断点):
①数据断点,它是针对变量(或者说内存地址)而言的,如果一个变量被设置了数据断点,那么当程序对该变量进行访问时,会触发停止条件。
②数据断点的设置方式:
[1]在菜单栏选择“View”→“Watched Data”,打开数据监测窗口。
[2]直接编辑表达式,输入变量名或变量地址,即可监测变量的值,编辑Value属性还可以直接在线修改变量的值,但是需要注意,对于非静态局部变量,仅程序运行到该函数栈帧内时有效,函数退出后栈内存失效。
[3]鼠标右键上一步添加的变量,选择“Set Data Breakpoint”,即可为该变量添加数据断点。
[4]数据断点有三个参数,设置完成后,点击“OK”即可,能看到变量被打上黄色圆圈标记。
Expression:数据断点的表达式,也就是变量名或变量地址
Access Condition:数据断点的触发条件,可选择仅读访问触发(Write)、仅写访问触发(Read)、读写访问均可触发(Read/Write)
Access Size:数据断点对应的变量字节长度,可选择1/2/4 Bytes或自动识别(Automatic)
(4)向量捕获断点:
①如果设置了向量捕获断点,那么程序一旦进入异常处理函数,将会立刻停机,自动弹出异常窗口,直接显示故障原因,不用手动进HardFault函数挖栈。
②向量捕获断点的设置方式:
[1]在菜单栏选择“View”→“Break & Tracepoints”,打开断点管理窗口(断点管理窗口会管理所有类型的断点,可以在其中配置断点是否使能)。
[2]鼠标右键窗口的空白区域,选择“Vector Catch”。
[3]将所有错误类型全部勾选。
(5)任务断点:
①如果程序使用了FreeRTOS/μC-OS,则可以设置断点仅在执行指定任务时可以触发,用于排查多任务干扰bug。
②任务断点的设置方式:
[1]按照源码断点的方式在对应代码处设置断点。
[2]鼠标右键断点,选择“Edit Breakpoint...”,打开断点编辑窗口。
[3]如果程序使用了FreeRTOS并且有任务定义,则可在Task选项的下拉框选择相应的任务,这样,只有当前执行任务匹配所选任务时,断点才会触发。
2、实时变量波形绘图
(1)在菜单栏选择“View”→“Data Sampling”,打开数据采样窗口。
(2)切换Data Sampling窗口的选项卡为Setup,将想要观察动态变化的变量从Watched Data窗口拖到Data Sampling窗口,或者手动添加表达式(全局/静态变量名)。
(3)切换Data Sampling窗口的选项卡为Samples,可以看到每隔一定的采样时间(采样频率Sampling Freq可配置),Ozone都会对变量值进行采样,生成若干数据点,鼠标右键窗口,选择“Export...”,即可导出.csv格式文件,然后使用其它工具(如MATLAB)绘图。
3、其它调试窗口
(1)Call Stack:
①打开方式:在菜单栏选择“View”→“Call Stack”。
②函数调用栈可显示函数的调用链路,在出现Hardfault时,还可直接定位bug函数。
(2)Registers(Peripherals):
①打开方式:在菜单栏选择“View”→“Registers”。
②寄存器窗口显示了单片机的所有寄存器存储情况,如果导入了相应的.svd文件,还可直接查看外设寄存器的存储情况。
(3)Memory:
①打开方式:在菜单栏选择“View”→“Memory”。
②Memory支持整片内存的查看,对于可写区域可以直接在线修改。
(4)Disassembly:
①打开方式:在菜单栏选择“View”→“Disassembly”。
②Disassembly窗口展示汇编语言(与C语言混排,展示对应关系),支持单步汇编级调试,可定位异常指令。
(5)Source Files:
①打开方式:在菜单栏选择“View”→“Source Files”。
②Source Files窗口展示程序中的代码文件(不一定包含工程的所有文件,在编译过程中未使用的代码文件不会显示),双击对应的代码文件可在代码浏览窗口打开它,按下Ctrl+F可以查找想要搜索的字段,在菜单栏也提供了若干Find工具搜索变量和函数。
4、程序运行控制键
(1)Halt program execution:控制单片机停止运行程序,代码浏览页面会跳转到停止点。
(2)Resumes program execution:控制单片机继续运行程序,直到触发停止运行的条件,或手动停止运行。
(3)Reset & Break At Symbol:控制单片机复位,运行到程序入口符号(main函数/Reset_Handler)后立刻暂停CPU。
(4)Reset & Halt:控制单片机复位,在复位向量地址处停机。
(5)Reset & Run:控制单片机复位,复位后全速运行程序,直到触发停止运行的条件,或手动停止运行。
(6)Steps into the current subroutine:控制单片机从当前停止点对应的C代码开始运行程序,并停止在下一条C代码,也即只运行一条C代码,如果遇到函数调用,会转入函数中,停在函数开头。
(7)Steps over the current subroutine:控制单片机从当前停止点对应的C代码开始运行程序,并停止在下一条C代码,也即只运行一条C代码,如果遇到函数调用,会一步跳过,不会转入函数(即使在执行函数的时候触发了断点)。
(8)Steps out the current subroutine:控制单片机从当前停止点对应的C代码开始运行程序,直到从当前运行的函数中转出,或触发停止运行的条件,单片机停止运行程序。
5、RTT日志打印
(1)SEGGER_RTT不占用USART硬件串口、不占用定时器,仅靠J-Link调试通道传输,可实现零开销实时打印日志。
(2)单片机工程的工作:
①在J-Link完整安装包中找到RTT源码(如果没有则从SEGGER官网下载即可,截至2026年依然可行),取出如下文件,添加到Keil工程中。(如果后续编译报错,可选择移除SEGGER_RTT_ASM_ARMv7M.s)
②包含头文件SEGGER_RTT.h,并在使用前调用SEGGER_RTT_Init函数,初始化RTT模块(一般在初始化其它模块时一同对其进行初始化)。
SEGGER_RTT_Init();③RTT支持多通道分离日志,一共16个通道(编号0~15),调用SEGGER_RTT_printf函数可打印日志,函数的使用方式和printf类似,只是多了一个函数参数——通道编号。
SEGGER_RTT_printf(<number>, "<打印的日志字符串及格式控制>", <输出表列>);④举例:修改main.c,在主函数中打印日志。
#include "stm32f10x.h" // Device header #include "Delay.h" #include "OLED.h" #include "Store.h" #include "Key.h" #include "SEGGER_RTT.h" uint8_t KeyNum; //定义用于接收按键键码的变量 int main(void) { /*模块初始化*/ OLED_Init(); //OLED初始化 Key_Init(); //按键初始化 Store_Init(); //参数存储模块初始化,在上电的时候将闪存的数据加载回Store_Data,实现掉电不丢失 /*显示静态字符串*/ OLED_ShowString(1, 1, "Flag:"); OLED_ShowString(2, 1, "Data:"); while (1) { KeyNum = Key_GetNum(); //获取按键键码 if (KeyNum == 1) //按键1按下 { Store_Data[1] ++; //变换测试数据 Store_Data[2] += 2; Store_Data[3] += 3; Store_Data[4] += 4; Store_Save(); //将Store_Data的数据备份保存到闪存,实现掉电不丢失 SEGGER_RTT_printf(0, "Key value = %d\r\n", KeyNum); } if (KeyNum == 2) //按键2按下 { Store_Clear(); //将Store_Data的数据全部清0 SEGGER_RTT_printf(0, "Key value = %d\r\n", KeyNum); } OLED_ShowHexNum(1, 6, Store_Data[0], 4); //显示Store_Data的第一位标志位 OLED_ShowHexNum(3, 1, Store_Data[1], 4); //显示Store_Data的有效存储数据 OLED_ShowHexNum(3, 6, Store_Data[2], 4); OLED_ShowHexNum(4, 1, Store_Data[3], 4); OLED_ShowHexNum(4, 6, Store_Data[4], 4); } }(3)在Ozone菜单栏选择“View”→“Terminal”,即可打开RTT终端窗口,窗口中能够显示日志信息。
四、RTOS系统级调试
1、RTOS内核感知调试
(1)本节使用FreeRTOS教程中计数型信号量实验的工程进行演示,同样地,该工程也需要做调试前的准备(回看第一节)。
(2)在FreeRTOSConfig.h中添加如下宏定义(注意不要重复定义,如果原本已有定义,则修改已有定义),然后编译工程。
#define configUSE_TRACE_FACILITY 1 #define configUSE_STATS_FORMATTING_FUNCTIONS 1 #define configRECORD_STACK_HIGH_WATER_MARK 1(3)Ozone加载RTOS解析插件:
①按照之前的步骤新建工程。
②如果弹出如下警告窗口,点击左下角“Apply selected fix-ups”,修复完成后点“Reload Project”重新加载工程。
③在Console窗口输入命令“Project.SetOSPlugin ("C:\Program Files\SEGGER\Ozone\Plugins\OS\FreeRTOSPlugin_CM4.js");”。(导入的.js文件取决于单片机的架构,本例使用的是Cotex-M4架构,所以导入FreeRTOSPlugin_CM4.js)
④在Ozone菜单栏选择“View”→“FreeRTOS”,即可打开任务视图窗口。
(4)开始调试,当程序停止时,任务视图窗口才会更新显示。
2、SystemView系统时序追踪
(1)SystemView是SEGGER配套J-Link的免费(非商业用途)RTOS全时序追踪工具(可从SEGGER官网获取),底层依靠RTT传输内核事件,实时抓取MCU运行全过程,生成可视化时间轴图谱,专门解决嵌入式实时系统卡顿、调度异常、功耗高、中断风暴等疑难问题,兼容FreeRTOS、RT-Thread、uC/OS等主流RTOS。
(2)SystemView工作原理简介:
①目标系统中的应用程序调用SystemView APls,对需要记录的信息进行采集,例如任务切换、中断进出、数据采样等。
②SystemView模块调用RTT APIs,将数据写入位于目标系统RAM里的RTT bufer。
③在不影响目标程序运行的情况下,J-Link通过调试接口读取RTT buffer的内容。
④SystemView主机软件通过J-Link DLL实时获取RTT数据,并进行图形化显示。
(3)使用SystemView前,需要将RTT代码和SystemView代码整合在嵌入式应用程序中:
①RTT源码整合在上一节中有说明,本节不再赘述。
②在SystemView完整安装包中找到SystemView源码(如果没有则从SEGGER官网下载即可,截至2026年依然可行),取出如下文件,添加到Keil工程中。
③在FreeRTOSConfig.h中添加如下宏定义(注意不要重复定义,如果原本已有定义,则修改已有定义),并包含SEGGER_SYSVIEW_FreeRTOS.h文件。
#include "SEGGER_SYSVIEW_FreeRTOS.h" #define configUSE_TRACE_FACILITY 1 #define configRECORD_STACK_HIGH_WATER_MARK 1 #define configGENERATE_RUN_TIME_STATS 0 #define configUSE_STATS_FORMATTING_FUNCTIONS 1④在main.c文件中包含SEGGER_SYSVIEW.h文件,并初始化SystemView。
#include "stm32f10x.h" // Device header #include "OLED.h" #include "FreeRTOS.h" #include "task.h" #include "FreeRTOS_experiment.h" #include "Key.h" #include "LED.h" #include "SEGGER_SYSVIEW.h" int main(void) { /*模块初始化*/ OLED_Init(); //OLED初始化 Key_Init(); //Key初始化 LED_Init(); //LED初始化 /* SystemView 核心初始化 */ SEGGER_SYSVIEW_Conf(); /* 开始录制 */ SEGGER_SYSVIEW_Start(); FreeRTOS_Test(); while (1) { } }⑤如果编译报了RAM空间不足的错误,则尝试减小分配给FreeRTOS的堆,修改以下宏定义。
#define configTOTAL_HEAP_SIZE ( ( size_t ) ( 10 * 1024 ) )(4)只要目标芯片的SWD/JTAG带宽足够,SEGGER J-Link支持多应用同时访问同一调试器,首先使用Ozone运行程序,然后单独打开SystemView连接同一个J-Link,即可进行录制。关于SystemView的使用,可参考另外的教程,此处不过多赘述。