当今的嵌入式系统开发领域中,高效的调试工具对于工程师来说至关重要。它们能够极大地减少开发周期中的错误追踪时间,并加速产品的上市时间。MDK作为业界领先的嵌入式开发工具之一,其内置的调试功能被广大开发者所赞誉。这些功能不仅提供了对代码执行的深入洞察,还允许开发者在实时环境中监控和修改系统行为。通过本文,我们将一起探索 MDK 的调试功能如何成为嵌入式开发者的得力助手,以及它如何助力我们构建更加稳定、高效的嵌入式应用。
1. 源码下载及前置阅读
- STM32F103C8T6模板工程
- MDK5安装包
- 芯片固件包
如果你是个零基础的小白,连 STM32 都没见过,我也给你准备了一个保姆级教程,手把手教你搭建好 STM32 开发环境,并教你如何下载程序,简直业界良心!
零基础快速上手STM32开发(手把手保姆级教程):https://www.lxlinux.net/e/stm32/stm32-quick-start-for-beginner.html
如果你连代码都不知道怎么烧录到 STM32 的,可以参考下文,提供了 5 种代码烧录方式:
新手小白如果连 MDK 的使用都不熟悉,那么可以通过下文先熟悉一下 MDK 的使用:
2. MDK仿真调试配置
在 MDK 仿真前,我们需要对其进行环境配置。
- 打开 Keil5 软件,打开或者新建一个工程。
- 点击 Options for Target,也就是我们俗称的魔法棒。
- 选择 C/C++ 选项卡,在进行仿真时,我们需要将 MDK 的优化调成 Level 0 等级。
- 点击 Debug 选项卡,把「Load Application at Startup」和「Run to main」勾上,
Load Application at Startup 是在启动调试时是否加载应用程序,如果此选项去掉则不会自动将程序下载到单片机,直接调试。如果此选项打勾则每次进入调试前先下载应用程序,然后进入调试。
Run to main() 可以使程序执行到 main() 函数。进入调试模式后,程序自动运行到 main 函数处。
- 打开 Settings ,可以看到关于仿真器的设置,可以在这里配置仿真器。默认情况下,大部分都是自动配置好的,无需额外修改。
-
我这里使用的是 ST-Link ,所以选的是 ST-Link 。如果你的 ST-Link 正常且插在电脑上了,右边 SW Device 会正常显示,表示仿真器与开发板连接成功了。
Debug Adapter 是你下载器的型号,这里要选择你使用的下载器型号。
Serial 是指仿真器的序列号,当你选择完仿真器之后,Serial 才会显示你下载器的序列号。
在下面的 Version:FW 是下载器的版本号,本文使用的下载器是 ST-Link V2 所以显示的 HW 是 V2 。
Port 是选择仿真器的调试模式,ARM 芯片有两种调试模式,分别是 SW 和 JTAG 。
相对于 JTAG 模式,SW 占用更少的信号线,而且功能一样支持代码下载与调试,所以强烈推荐使用 SW 调试模式。
如果 ST-Link 没有插上或设备异常,则会提示 No ST-Link detected。
- 最后打开 Utilities 选项卡将 Use Debug Driver 打勾,再点击 OK 确定一下,MDK 仿真的调试配置就完成了。
3. 各个调试按钮的作用
接着我们编译一下代码。再点击这个像放大镜一样的按钮就可以开始仿真了(这个按钮同时也可以退出仿真)。
进入仿真之后的界面如下图示。
界面左边显示寄存器的地址和程序运行时间,上方是汇编语言窗口,需要一定的汇编基础才能看得懂。界面左下方是命令窗口,这个窗口会显示一些打印信息,也可以在这个窗口输入一些命令。右下角的窗口是关于函数及变量在内存中的地址信息。
在左上方有一排关于调试的小按钮:
它们的功能从左到右分别是:复位、全速运行、停止、进入函数、执行过此函数、跳出函数、执行到光标处、显示下一个将运行的代码
- 复位:重新执行程序;
- 全速运行:开始执行程序;(快捷键为 F5 )
- 停止:停止执行程序;
- 进入函数:进入当前行代码中;(快捷键为 F11 )
- 执行过此函数:执行当行代码;(快捷键为 F10 )
- 跳出函数:跳出当前程序代码;(快捷键为 Ctrl+F11 )
- 执行到光标处:自动执行代码至蓝色光标处;(快捷键为 Ctrl+F10 )
- Show Next Statement:显示下一行即将要执行的程序。
这里我以流水灯作为案例来详细的说明一下各个按钮的使用方法,使用的单片机型号为 STM32F103C8T6 最小系统板。连接 ST-Link 上电,接线图如下:
ST-Link V2 | STM32 |
---|---|
SWCLK | SWCLK |
SWDIO | SWDIO |
GND | GND |
3.3V | 3V3 |
在 STM32F103C8T6 最小系统板上有个自带的小灯,这颗 LED 灯连接在 PC13 管脚上。
首先我们需要写一个小灯的初始化程序,这里可以不用封装,直接写在 main.c 里面。
#define LED_CLK() __HAL_RCC_GPIOC_CLK_ENABLE()
#define LED_GPIO GPIOC
#define LED_PIN GPIO_PIN_13
void led_init(void)
{
GPIO_InitTypeDef gpio_initstruct;
LED_CLK(); /* IO口时钟使能 */
gpio_initstruct.Pin = LED_PIN; /* LED0引脚 */
gpio_initstruct.Mode = GPIO_MODE_OUTPUT_PP; /* 推挽输出 */
gpio_initstruct.Pull = GPIO_PULLUP; /* 上拉 */
gpio_initstruct.Speed = GPIO_SPEED_FREQ_HIGH; /* 高速 */
HAL_GPIO_Init(LED_GPIO, &gpio_initstruct); /* 初始化LED0引脚 */
}
主函数代码如下:
int main(void)
{
HAL_Init(); /* 初始化HAL库 */
sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟, 72Mhz */
delay_init(72); /* 延时初始化 */
led_init(); /* LED初始化 */
while(1)
{
HAL_GPIO_WritePin(LED_GPIO,LED_PIN,GPIO_PIN_SET); /*使LED灭掉*/
delay_ms(500);
HAL_GPIO_WritePin(LED_GPIO,LED_PIN,GPIO_PIN_RESET);/*使LED亮起*/
delay_ms(500);
}
}
点击放大镜进入仿真界面,然后再点击左上角的开始按钮(或者使用快捷键 F5 )开始执行代码。
可以看到 STM32F103C8T6 最小系统板上的 PC13 小灯在不断闪烁。这时候我们在 19 行打上一个断点,再点击全速运行按钮:
可以看到当前程序运行到了第 19 行然后暂停了,STM32F103C8T6 最小系统板上的 PC13 小灯则会保持长亮。
(需要注意的是,调试停在中断处的时候,内核会停止,但是外设将会继续运行)
接着我们在 21 行再打上一个断点,然后再点击全速运行按钮。
会发现代码执行到了 21 行就停下了,这时 STM32F103C8T6 最小系统板上的 PC13 小灯会灭掉。
我们点击右上角复位的按钮,然后选中 led_init(); 这行代码,点击执行到光标处按钮(或者快捷键 Ctrl+F10 ),将代码执行至这一行。
(需要注意必须要退出调试模式再重新进入调试模式,否则代码会一直在死循环当中执行,导致代码无法执行到你想要执行的那行)
这时候我们点击进入函数按钮(或者快捷键 F11 ),则进入到 LED 的初始化函数当中。
我们可以看到当前程序执行到了 LED 的初始化函数中。这时,点击「执行过此函数按钮」(或者快捷键 F10 ),如下图示。
点击一下上图按钮,程序将执行一行代码。而且,如果当前行是函数,将不进入此函数内部代码,直接执行完函数代码并进入到下一行。
如果你不想查看 led_init() 函数里的代码了,你可以点击「跳出函数按钮」(或者快捷键 Ctrl+F11 ),可以按钮可以直接跳出当前代码程序,准备执行下一行。
(跳出函数的话会默认执行完当前程序代码,然后跳到下一行)
4. 查看程序段/函数执行的时间
在代码中,我们经常会使用延时函数。那么,我们如何确定延时函数真的延时了我们所设置的时间呢(比如 500ms)?
我们可以通过代码调试功能来确定延时时间的准确性。
首先打开魔法棒,在 Debug 中打开 Settings 选项。
再点击 Trace 选项卡,可以找到时钟频率。
默认情况下的时钟频率是 8MHz ,这里我们改成 72MHz 。
我们选中 20 行,然后使用「执行到光标处按钮」(快捷键为 Ctrl+F10 )运行至 20 行。如下:
在左下方的 Sec 表示程序运行了多久,单位是秒( s ),我们使用「执行过此函数」(快捷键为 F10 )执行一下延时函数,看看延时函数是否准确。结果如下:
使用此结果减去初始的时间,可以看到大约延时了 0.5s 左右,也就是 500ms ,可以认为延时函数的时间是非常准确的。
5. 工具栏常用窗口按钮介绍
在调试按钮的左侧有一排窗口按钮,用于打开各种调试窗口(也可以通过菜单栏的 View 打开)。
- 第一个 Command Window 按钮,可以打开 Command 窗口,这个窗口可以显示一些打印的信息,还能输入命令。
- 第二个按钮是 Disassembly Window ,这个窗口显示了汇编语言。你可能需要一些汇编语言的基础才能看得懂,这个窗口显示的光标表示下一条即将运行的汇编语言是什么。
- 第三个按钮是 Symbols Window ,这是符号窗口,用于查看一些符号类型。本文暂时不对这个窗口进行深入学习。
- 第四个按钮是 Registers Window ,这是一个寄存器窗口。这个窗口可以直观的查看一些内核的寄存器,还有程序运行时间等等。
- 第五个按钮是 Call Stack Window ,它是用于查看函数调用关系 & 局部变量。本文学习的是仿真工具一些基础的使用,所以不对这个窗口进行深入学习。
- 第六个按钮是 Watch Windows ,用于查看函数首地址以及变量的值。
Watch 窗口还可以设置变量在被读或写后自动停止运行,对于代码调试非常有帮助。可以定义一个函数,将该函数的名称输入到窗口中,然后向函数传入一个数,在这个窗口你可以看到该函数的寄存器首地址,传完数之后你可以看到该数的类型是什么。
点击该按钮,弹出的界面如下:
举个栗子来演示一下这个过程。例如,在程序中定义一个全局变量 int temp;
,在 while 循环中执行 temp++ 。右击 temp ,选择 Add “temp” to… 添加到 Wacth 1 。
在左下方的 Watch 1 窗口就可以看到 temp 的首地址和定义类型了。如下图:
这里我们使用「执行过此函数按钮」(快捷键为 F10 ),执行一遍程序,会发现 temp 的值发生了改变,执行了一次 temp++ 。如下图:
Wacth 窗口不止这些功能,Wacth 窗口还可以设置变量在被读或写后会自动停止运行。这个功能在调试行数比较多的项目非常有用,因为在很多场景下,有个变量有可能在几十上百处被修改,如果没有这个功能,排查变量被修改将变得非常困难。
那么如何设置这个功能呢?
首先右击 temp ,会出现一个列表,点击 Set Access Breakpoint at “temp” 。如下图:
这时候会弹出一个窗口,在这个窗口你可以设置函数是否要在被读或者被写的自动停止运行。设置完之后点击 Define 就行了。
Wacth 窗口对于我们代码调试还是有非常大的作用,大家可以好好掌握。
- 第七个按钮是 Memory Windows ,可用于查看内存的地址与值。界面如下图示:
同样的,我们举一个例子。
首先我们在程序中定义一个 uint8_t 类型的数值 temp[10] ,然后给数组的前五个数赋值 {1,2,3,4,15} ,其他元素默认值为 0 。在循环中我们进行 temp[0]++ ,只对第一个元素自增。
编译一下,打开仿真工具,再打开Memory 窗口,在窗口中输入 temp ,这样我们就可以看到 temp 数组在内存中的地址与值。前面我们给数组赋值 {1,2,3,4,15} ,在 Memory 窗口可以看到 temp 数组被十六进制表示出来 “01 02 03 04 0F” 。如下图:
(注意,M3/M4/M7内核是小端模式,内存的值要倒着读)
Memory 窗口可以帮助我们快速的查看变量在内存中的地址和值,便于我们调试程序。
- 第八个按钮是 Serial Windows ,这是用于查看串口的窗口。
-
下面这个按钮是 Peripheral 窗口,于查看寄存器的值。Core Peripherals 是内核寄存器,其他的都是外设寄存器。如下图:
打开之后是这个样子的(我这里打开的是 GPIOC )。如下图:
在这里你可以看到一些寄存器的值,如果你想看某个值,你可以点击左边的 + 号,在 + 号中有更详细的信息。
通过这个窗口,可以查看到自己配置寄存器是否正确,确保外设能按照你的预期运行。
6. 小结
总的来说,想要将单片机学的更好,MDK 仿真工具就必不可少。MDK 仿真工具不仅功能强大,而且操作起来的难度也不高,对新手也十分的友好,最重要的是它能够帮助我们更好地理解和调试程序代码,学好使用 MDK 仿真工具,可以让你的技术更上一层楼。