提高调试效率:从断点的使用开始
断点(break point)是嵌入式调试中最基础的功能,它的基本功能非常简单:在程序运行的某个地方提前设置一个停止操作,CPU运行到该位置之后就会自动暂停,这个时候我们就可以查看CPU各种寄存器的工作状态。根据断点机制不同,断点一般分为如下几种:
- 硬件断点:这个需要CPU/MCU支持,可以直接在CPU内部寄存器写入期望断点的PC位置,硬件断点的数量一般是有限的,当调试器退出或者MCU重新上电之后就会丢失。
- 软件断点:软件断点最开始主要用于RAM中设置断点,它的实现机制调试器是将RAM中原有的指令替换为一个断点指令,当CPU执行到该指令之后就会自动暂停,这个时候调试器便会将原有的指令放回原来位置。这个过程都是调试器自动实现的,对用户而言一般情况和Flash断点没有差异。现在很多调试器也开始支持在Flash中设置软件断点,不过Flash内容是非易失的,所以有可能在调试器意外断开之后断点还保留,这样后面直接运行程序可能就会出现问题。不过因为软件断点只需要CPU支持断点指令,所以数量上基本是没有限制的。
- 数据断点:数据断点是在CPU对特定地址访问时会触发的一种断点,在设置断点的时候可以设置具体的访问形式,比如地址,是读访问还是写访问,或者是字节,半字还是字访问模式。设置好数据断点之后,CPU对特定数据访问时会自动暂停,不过因为实现机制上CPU必须访问了目标地址才会触发数据断点,所以实际上程序断点一般刚好在刚访问完数据后触发。数据断点需要硬件支持,所以数量一般也是有限的。
- Trace断点:这种断点要结合Trace工具使用。
以上基本就是MCU调试中会使用的几种断点,一般情况我们都是使用程序断点就可以覆盖大部分的调试情况,不过灵活的使用数据断点可以更为有效和快速的定位问题。下面我们针对程序断点和数据断点分别介绍一下适用情况和使用的一些注意事项。
程序断点
程序断点是我们最为常用的一种调试手段,可以定位大部分程序问题。这里我们主要说一下程序断点的一些使用注意事项。
前面提到程序断点分为硬件和软件两种,虽然大部分情况二者没有差异,不过某些特殊情况就可能出现问题。
意外的Flash操作
比如在一个使用案例中,用户正在调试Flash相关的代码,为了定位问题,打了很多断点,其中就有Flash断点,调试过程发现软件总是会意外的操作Flash,而实际代码中找不到任何Flash的额外操作。经过长时间调试才发现,用户在执行Flash 命令之前打了一个断点,而这个断点刚好是Flash操作,所以打断点操作和CPU运行到断点的时候均会触发Flash操作,这个操作对用户来讲是透明的,调试过程很难往这个方向想。
意外的数据读取
有很多用户发现,调试过程中有些模块运行不正常,但是不调试就没有问题,看起来有点像是薛定谔的猫,你不看它它就没问题。实际上这个问题并没有这么高深,一般情况下出现这个问题都是因为用户在调试界面打开了模块寄存器窗口,当断点触发时候,调试器会自动更新模块寄存器窗口数据,这个时候像SPI/UART的接收数据,ADC的转换数据等就会被调试器读走,这种数据寄存器一般都是FIFO实现的,读一个少一个,所以CPU读的时候就会少数据。调试器的这个操作一般也会被忽略,所以看起来就像是调试就有问题,不调试就没有问题。
都有谁暂停
调试的时候常出现的另一个问题就是定时器工作似乎不太正常了,这个实际上是因为某些定时器还在继续运行导致的,比如定时器溢出后会直接pending一个中断,程序再次运行之后就会立即进中断,或则会PWM没有输出了(PWM和CPU一起暂停)。对于这种情况要根据需要配置对应的定时模块在CPU halt状态是否继续工作。当然除了定时器以外还有DMA也可以在中断的时候保持运行。而对于WDG来讲,如果断点的时候WDG继续运行,那么就会出现CPU以外复位的情况。
当然还有一些情况是不适合进行断点的,比如运行电机的时候,无论配置PWM是停止输出还是保持输出,都可能出现PWM状态不更新,导致电机损坏的问题。
变量的不一样看法
对于电机应用或者其它实时数据采集的应用,我们一般都会非常关心一些变量的更新情况,甚至需要绘制相应的曲线。这个时候变量的实时更新就比较重要了。一些软件提供变量的准实时读取,可以绘制一些波形实现类似示波器的功能。比如J-Scope在载入elf文件后就可以对所有全局变量进行周期性更新。另外很多IDE还可以实时对一些全局数据进行写入操作,这对于一些PID参数的调试非常有利。这种数据的读取和更新是纯粹通过调试接口的总线实现的,并不会影响正常的程序执行,只是在系统总线中额外插入了一些数据访问,而系统总线的占用率往往都是低于50%的。
数据断点
数据断点平常使用的比较少,但是合理的使用可以帮助我们快速的定位问题,这里举几个简单的例子:
快速定位变量访问
良好的编码规范一般会要求变量的访问都是可以控制的,比如对全局变量的访问要通过函数来实现,当然为了优化速度,这里函数可以定义为inline的形式,或者通过编译器优化来实现inline的访问方式。这个要求遵守起来确实有点繁琐,但是调试的时候就比较合适,我们可以直接在对应的函数打断点来定位对变量的各种操作。不过有些时候这个规范并不是那么要遵守,这个时候要定位变量的访问就特别适合通过数据断点来实现。
另外对于堆栈溢出的定位,也可以在堆栈低端位置设置断点来实现对越界情况的监测。在CM33中,Core内部直接做了对于堆栈地址的溢出保护,可以直接设置堆栈溢出地址即可实现对堆栈异常的检测。
快速定位外设寄存器的访问
除了可以定位变量访问以外,实际MCU中外设寄存器也可以认为是一种全局变量。如果希望定位对MCU某个模块寄存器的访问情况,也特别适合通过数据断点来实现。
数据断点特别适合验证对于具体问题的怀疑,比如现象上看起来像是对某个寄存器操作导致的问题,但是程序中对这个寄存器操作的地方非常多,通过普通断点必须在多个地方打断点。如果通过数据断点则只需要设置一个断点即可找到所有对寄存器访问的地方,包括逻辑分析不会怀疑的地方,而这些地方则往往就是问题的关键。
保护现场
对于一些特别难以复现的情况,一旦问题出现,保存现场信息非常重要,因为一旦MCU被复位,则现场信息将全部丢失。这个时候我们就需要通过一些支持attach的调试器实现对程序异常的分析。比如Ozone软件就可以读取elf,同时还可以attache到MCU调试接口而不必有复位情况介入。如果MCU正在低功耗模式或者调试端口被关闭或者改为其它功能则还是可能出现复位的情况,这点要注意。
Attach到MCU之后我们就可以直接暂停程序看程序的运行状态和寄存器以及变量的实时数据,从而快速定位问题。
总结
总的来说,合理的使用各种断点可以帮我们快速的定位问题,节省debug的时间。当然找bug最主要的还是要先猜测bug产生的原因,然后顺着猜测的原因不断验证。另外也要善于从MCU的现场信息分析问题原因,比如PC的位置,堆栈上各种函数的调用,堆栈指针的位置还有hardfault的信息等等。如果产生了hardfault,实际从CPU的一些内部模块寄存器还是可以得到很多有用的信息的。
最后更新于 2023-06-17 00:27:50 并被添加「MCU」标签,已有 1371 位童鞋阅读过。
本作品采用知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议进行许可。