MCU+RTOS调试
在做项目时,百分之三十的时间写代码,还有百分之70的时间用于调试。本期将以Keil为例进行调试章节的讲解,目的在于做出一个标准化的调试步骤,方便大家学习如何调试代码。内容分为基础调试、中级调试及进阶调试三部分,本次主要讲解基础调试。
1. 引言
在做项目时,百分之三十的时间写代码,还有百分之70的时间用于调试。本期将以Keil为例进行调试章节的讲解,目的在于做出一个标准化的调试步骤,方便大家学习如何调试代码。内容分为基础调试、HardFault异常分析、波形仿真及RTOS调试,本次主要讲解基础调试。
2. 基础调试
2.1 调试设置
在Keil中,我们通常通过下载调试器进行调试。常见的下载调试器有DAP/Stlink等,本次讲解以Stlink为例。首先,需要点击魔术棒进行设置,点击魔术棒,再点击DeBug--点击红框中的Use,选择Stlink Debugger,同时将其勾选;如下图所示:

接下来点击Utilities--取消勾选use debug driver,如下图所示:

选择Stlink Debugger,如下图所示,最后点击确定。

2.2 调试界面讲解
点击Keil工具栏的 “d”符号进入调试界面(前提是Stlink已经连接板子),如下图所示:

进入后调试界面如下图所示:

接下来介绍一下debug工具栏,如下图所示:

复位:其功能等同于硬件上按复位按钮。相当于实现了一次硬复位。按下该按钮之后,代 码会重新从头开始执行。
执行到断点处:该按钮用来快速执行到断点处,有时候你并不需要观看每步是怎么执行的, 而是想快速的执行到程序的某个地方看结果,这个按钮就可以实现这样的功能,前提是你在查 看的地方设置了断点。
停止运行:此按钮在程序一直执行的时候会变为有效,通过按该按钮,就可以使程序停止 下来,进入到单步调试状态。
执行进去:该按钮用来实现执行到某个函数里面去的功能,也就是进入函数中去执行每一行语句,这个过程会让你看到。
执行过去:在碰到有函数的地方,通过该按钮就可以单步执行过这个函数,而不进入这个 函数单步执行。
执行出去:该按钮是在进入了函数单步调试的时候,有时候你可能不必再执行该函数的剩 余部分了,通过该按钮就直接一步执行完函数余下的部分,并跳出函数,回到函数被调用的位 置。
执行到光标处:该按钮可以迅速的使程序运行到光标处,其实是挺像执行到断点处按钮功 能,但是两者是有区别的,断点可以有多个,但是光标所在处只有一个。
汇编窗口:通过该按钮,就可以查看汇编代码,这对分析程序很有用。
堆栈局部变量窗口:通过该按钮,显示Call Stack+Locals窗口,显示当前函数的局部变量 及其值,方便查看。
观察窗口:MDK5提供2个观察窗口(下拉选择),该按钮按下,会弹出一个显示变量的窗 口,输入你所想要观察的变量/表达式,即可查看其值,是很常用的一个调试窗口。
内存查看窗口:MDK5提供4个内存查看窗口(下拉选择),该按钮按下,会弹出一个内存查看窗口,可以在里面输入你要查看的内存地址,然后观察这一片内存的变化情况。是很常用 的一个调试窗口 串口打印窗口:MDK5提供4个串口打印窗口(下拉选择),该按钮按下,会弹出一个类似 串口调试助手界面的窗口,用来显示从串口打印出来的内容。
逻辑分析窗口:该图标下面有3个选项(下拉选择),我们一般用第一个,也就是逻辑分析 窗口(Logic Analyzer),点击即可调出该窗口,通过SETUP按钮新建一些IO口,就可以观察这 些IO口的电平变化情况,以多种形式显示出来,比较直观。
系统查看窗口:该按钮可以提供各种外设寄存器的查看窗口(通过下拉选择),选择对应外设,即可调出该外设的相关寄存器表,并显示这些寄存器的值,方便查看设置的是否正确。
2.3 基础调试步骤
该调试步骤是笔者个人总结的,仅供参考。首先点击调试,进入调试界面后,此时程序会执行到main函数处,这是因为我们勾选了run to main。
先点击单步调试,点击执行进去或者是点击执行过去,通常点击执行过去可加快调试速度。目的是检查main函数中的各个硬件初始化函数是否正常运行,看有没有卡在哪个初始化函数无法继续往下执行。注意:通常在第一步出现程序卡住的问题大概率是硬件初始化函数的顺序不对,比如说在lcd初始化函数里边调用了串口的printf函数,但是串口初始化函数在lcd初始化函数后边调用,这个时候就会出现程序卡住的情况。
如果需要快速执行到某个地方,可以添加断点,再点击执行到断点处即可。也可以选择不添加断点,直接点击执行到断点处,此时程序会全速运行,此时你看通过stlink连接的板子显示的现象是否与你预期的一致,如果不一致或者是没现象,代表程序卡住了。此时,点击停止,程序会自动定位到代码卡住的地方,方便快速纠错。
2.4 Debug代码
除了Keil界面的基础调试之外,我们还可以配合调试代码,去观察程序运行的状态。比如说可以在代码中加入小灯调试代码或者串口输出的代码,如下图所示:

被红框标记的就是加入的调试代码(不使用时将其注释掉),第一个标记处的debug代码可以帮助程序员快速判断该任务有没有被调度;第二个标记处的debug代码可以判断接收的数据是否是预期值,这样在发生问题时可以快速定位并排除问题。
此外,在不确定某个中断任务是否发生时,也可以在中断服务函数内部加入led灯亮灭的debug代码(如下图所示)。在进入中断函数时led灯亮,代表中断发生。这种直观的调试现象是经常使用的,能帮助我们快速定位bug。

2.5 外设寄存器调试
有时候无法判断写的代码是否正确设置了某个外设的寄存器,就可以通过观看外设寄存器是否正确触发去进行调试,具体方法如下图所示(以USART1为例):

点击菜单栏的 Peripherals-->System Viewer-->USART-->USART1,此时IDE的右侧会出现如下图所示的界面。

图中左边是 串口1的默认设置状态,从中可以看到所有与串口相关的寄存器。在执行完串口初始化函数后,如图中右边所示,此时可以查看串口1的各个寄存器设置状态,从而判断我们写的代码是否有问题,只有这里的设置正确了之后,才有可能在硬件上正确的执行。同样这样的方法也 可以适用于很多其他外设。
2.6 AI调试
当通过前述几个小节所讲的硬件调试的方法还是无法定位问题时,此时可以借助AI去辅助定位。把你遇到的问题、所用的硬件描述清楚,喂给AI。这种方法能快速帮你进行问题定位,但是AI的回答不一定完全准确,因此注意甄别。
3. 波形仿真调试
当我们在项目中用IIC通信或者是PWM遇到问题时,通常会使用示波器/逻辑分析仪等对波形进行相应分析。而在Keil中,也提供了仿真的逻辑分析仪功能,可以去对IIC等波形进行分析。要想使用该功能,首先需要点击魔术棒,点击bebug,点击use simulator,并且设置Dialog DLL的相应参数,如下图所示:

按照上图设置好以后,进入debug界面,点击view,点击实时更新窗口方便波形的实时输出。点击逻辑分析仪,点击setup,点击新建,输入需要监听的端口,看这个端口输出的波形,比如说我需要查看PB6,PB7的iic波形,那么我就要输入PORTB.7,PORTB.6,接下来把analog改成Bit,选择颜色,之后点击close关闭。
最后点击运行,此时就可以看到波形。如果你看不到,那么就慢慢调节鼠标进行放大或者缩小就可以看到了。如下图所示:

4. HardFault异常分析
4.1 HardFault异常简介
HardFault异常是CM3处理器内部的一种异常类型。一般来说,当出现栈溢出、野指针(非法访问内存地址)、对齐错误以及在中断函数中调用RTOS的API函数,但是RTOS的API又没有初始化时,就会进入HardFault异常。这个时候你看板子上的现象时,会发现所有现象都停止了。此外,连接下载器进入Debug模式,程序全速运行后,按住停止,此时程序会停在HardFault异常函数处,如下图所示:

可以看到,HardFault异常实际是一个死循环函数,你也可以选择在循环内部加入一些Debug代码。
4.2 HardFault异常问题定位
在发现程序已经进入HardFault异常后,如何快速定位是什么原因造成的呢。有以下几种方法可以借鉴。
4.2.1 查看Fault报告
进入debug界面后,点击菜单栏Peripherals -->Core Peripherals-- >Fault Reports打开fault reports,如下图所示:

当发生HardFault异常时,如图所示的各个Faults下边的白色框会被打上勾。此时就可以通过查阅《CM3权威指南》手册,翻到最后一页去看是什么原因造成的。举例:比如当发现IMPRECISERR前边的白色框被打勾时,可以通过手册查找到如下图所示的信息:

通过上图发现,当IMPRECISERR置位时,是数据传送过程中发生了错误(数据遗失等)。了解到这一信息后,我们重新在debug界面按下复位,点击单步运行,看程序运行到哪一行代码或者是运行到哪一个函数时发生了HardFault异常。这样,就能快速定位到出问题的函数或者代码。当然,有的时候也有可能不是代码的问题。比如有次笔者在调试时,出现了上图中的数据传输错误,最后定位的问题是我接收的固件数据有遗失。
4.2.2 查看堆栈窗口
同样是在debug界面,点击运行程序停止后,此时程序会跳转到HardFault函数内,此时点击菜单栏view--调用堆栈窗口,如下图所示:

此时会debug界面底部出现调用堆栈窗口,如下图所示:

接下来右键点击图中的HardFault_Handler,再点击Show Caller Code,如下图所示:

点击完成之后,此时可以看到程序会跳转到出现问题的地方,举个例子,当笔者点击Show Caller Code后,此时程序跳转到如下图所示地方:

此时,再往上翻一下具体调用这行代码的是哪个函数,如下图所示:

可以看到,就是在中断里边调用了RTOS的中断级API,才会发生HardFault。那为什么会这样呢?检查后发现,原来是在硬件初始化过程中,当定时器中断函数初始化好之后,会进入中断函数运行,但是此时RTOS的初始化还未完成,导致出现了野指针,造成了HardFault。
总结:可以用上述两种方法进行HardFault的错误分析,两种方法可以配合使用。
5. RTOS调试
5.1 堆栈溢出调试
在RTOS中,堆栈溢出是程序出现Bug的主要原因,当堆栈溢出时,容易进入HardFault模式,因此对堆栈容量的把控至关重要,接下来笔者将以FreeRTOS为例去说一下如何去查看RTOS中堆栈是否溢出。
首先,在我们创建任务时,会对任务的堆栈大小进行分配;通常采用动态方法去创建任务(即调用xTaskCreate函数),因此我们需要查看所创建任务的堆栈,就要先知道所分配的堆栈内存指针,从而去查看堆栈内存大小,那么如何知道堆内存指针呢?实际上就是创建任务时的任务句柄参数,那为什么是任务句柄呢?答案在xTaskCreate函数源码中,如下图所示:

图中的pxCreateTask变量就是任务句柄,可以看到,从堆分配的内存指针(pxNewTCB)赋给了任务句柄参数。
当知道以原理之后,接下来进入Debug界面,点击菜单栏的view-->watch window,如下图所示:

然后,先点击运行(因为是动态分配,程序只有在运行时才分配内存,和静态分配不同),接下来在Watch window中输入任务句柄(也是一个指针),即可得到该指针的地址,如下图所示:

在得到任务句柄的地址,即所创建任务的堆栈内存地址后,点击菜单栏-->view-->Memory Window,输入上述步骤得到的堆栈内存地址即可查看你所创建任务的堆栈大小,如下图所示:

可以看到图中显示,这段内存大部分都是0xA5(因为FreeRTOS 在初始化任务栈时,会用固定填充值0XA5填满未使用部分),说明栈还很空;如果全被覆盖了,说明栈溢出了。
此外,还可以采用堆栈水位线的判断方法,此处简单说一下。
-
开启
configCHECK_FOR_STACK_OVERFLOW(值设 2),在vApplicationStackOverflowHook()中打断点。 -
使用
uxTaskGetStackHighWaterMark()定期记录剩余栈空间,判断栈大小是否需要优化。
5.2 Event Recorder
在Keil中,还可以使用事件记录器进行RTOS的调试,以观测各个任务的调度,但是缺点在于配置比较复杂,笔者不太建议。如果通过以上方法都没有调试成功,可以尝试该工具,也可以选择其他RTOS可视化软件进行调试。关于此调试工具,有兴趣的读者可自行搜索,笔者不再过多阐述。
6. 总结
上述讲的调试方法仅是帮助大家定位到问题时,如何排查。实际上,当遇到一个错误现象时,应该学会最重要的思想--反推。从现象反推原因,慢慢梳理清楚逻辑关系,这才是调试的核心。
7. 参考文献
[1] CM3权威开发指南。
[2] 【STM32】HardFault问题详细分析及调试-CSDN博客
[3] 【keil 5】进阶玩法:逻辑分析仪的使用(软件仿真)_keil 逻辑分析仪 只有软件仿真才能使用-CSDN博客
ps:如有需要MCU+RTOS项目的同学,请联系1840813505@qq.com,备注CSDN。
更多推荐



所有评论(0)