一、概述

    本文面向已经懂得软件基本操作的职业老手,如果是未使用过该软件的小鲜肉,请移步基础篇。这里以STM32芯片为例对工具进行讲解,其他品牌的芯片在调试方面上可能存在差异。

二、软件说明

    Keil提供了包括C编译器、宏汇编、链接器、库管理和一个功能强大的仿真调试器等在内的完整开发方案,通过一个集成开发环境(μVision)将这些部分组合在一起。
    目前软件对中文的支持不友好,不建议安装网上的一些汉化包之类的。另外建立的工程文件路径也尽量不要存在中文,否则可能会出现一些异常。
演示版本:5.24a

三、软件使用

3.1 基本调试操作

    首先点击”Debug->Start/Stop Debug Session”或下图2位置,进入调试。

    如果前面工程配置里选择了复位调试,则进入调试后,会停在main函数头部

    复位(Reset):对程序进行复位操作,根据烧录器不同的复位方式配置会触发不同的复位类型。

    全速运行(Run):使当前程序开始正常全速运行,直到程序遇到断点时停止。

    停止运行(Stop):当程序全速运行时,点击此按键可停止程序运行,停的是当前点击时的程序运行位置。

    单步调试(Step):根据当前调试的窗口的语言,执行单条语句。如果遇到函数,则会进入函数内部。如果是在反汇编窗口中,则只执行一条汇编指令。

    单步跳过调试(Step Over):如果是在C语言窗口中,则是按单条语句执行,与单步调试不同的是,遇到函数不会进入函数内部,而是直接全速运行函数,并跳到下一条语句。

    单步返回调试(Step Out):如果是在C语言窗口中,则是直接全速运行当前函数后面所有内容,直到函数返回上一级。

    插入/移除断点:如果当前光标所在行未有断点,则插入断点(前提是当前行可以插入,如果无法插入会显示一个感叹号),在有断点的情况下则是移除断点。插入断点后,当前行前面会有个红圆表示断点位置。也可以通过直接点击红圆位置进行插入/移除断点的操作。另一种断点方式,是通过指令来控制,当然也可以使用Keil提供的界面化操作,设置某个变量读或写时触发断点。不过目前貌似有部分芯片不支持这种操作。注:断点最多只能打7个。

    使能/禁止断点:开启或禁止当前光标所在行的断点。禁止后红圆变成白圆。

    禁止所有断点:禁止当前所有的断点。

    删除所有断点:删除所有断点。

3.1 调试窗口

  • 变量查看窗口——Watch1,Watch2

    通过”View->Watch Windows->Watch1、Watch2″可以选择打开Watch窗口,也可以在工具栏这里打开。再点击一次则可以关闭。

    通过选中一个变量,右键添加入对应的Watch窗口,可以追踪查看当前变量的变化状态。注意,只有全局变量可以全程监视,临时变量只有在进入当前函数中才可监视到其数据,用static关键词修饰的变量无法监视。

    如果当前变量没有实时更新,则需要点击”View->Periodic Window Update”将其勾选上。

    在”Watch”窗口中,可以查看当前变量名称、值、数据类型,如果当前变量类型为结构体,则可以以对应的结构形式进行展开查看。

  • 内存查看窗口——Memory

    通过”View->Memory Windows->Memory1/2/3/4″打开Memory窗口,也可以通过工具栏
这个图标打开。打开的状态下再按一次则可关闭。


    在Memory窗口中输入想要查看内存的起始地址,另外右上角的锁可以把当前界面锁定下来。

    另外如果查看的是Ram的地址,那其中的数据也可以直接通过此窗口进行修改。

  • 系统视窗——System Viewer Windows(这个根据不同芯片会有不同的展示)

    可以在”Peripherals”选项栏中选择”System Viewer”系统视窗中对应的外设,选择”Core Peripherals”则是内核调试窗口。另外系统视窗也可以通过工具栏中这个位置可以打开。
    这个窗口用于查看当前单片机外设及内核寄存器的值,在调试外设底层时经常会使用到。
    在这个窗口中可以直接修改外设寄存器的值,当然部分只读寄存器是无法修改的,有些则是需要在特定条件下才可以设置生效,具体就得看对应的芯片手册里寄存器的说明了。

  • 调度关系窗口——Call Stack Window

    可以在”View->Call Stack Window”打开此窗口,也可以在工具栏这个位置打开 。

    这个窗口用于查看当前程序调度关系,当出现有进入硬件错误异常调试时可以快速定位到是哪里触发的异常。这个窗口是的调度关系是从当前程序堆栈里获取的数据并将其图形化,所以如果当前堆栈数据被破坏,则此窗口也将无法查看调度关系。
    该窗口里显示的调度关系是从下至上调用的,最上面的表示当前程序所处的函数。展开对应的函数,可以查看各层调用函数跳转之前保存的一些临时变量等信息。

  • 寄存器窗口——Register Window

    该窗口可在”View->Registers Window”处打开,也可以在工具栏这个位置打开。

    这个窗口用于查看当前内核的相关寄存器,如汇编里常说的15个通用寄存器。当然调试中比较常用的是其中的SP、LR、PC三个寄存器。SP为当前栈的地址位置,PC为当前程序地址,LR为函数跳转前的地址,即当前函数返回的地址。

    另外”Banked”中的MSP为当前程序系统主栈,PSP则为操作系统的任务栈,这两者的区别是,如果使用了操作系统,则当前任务中的所有调度关系使用的是任务栈,而类似中断这种内核的操作使用的是主栈;如果未使用操作系统,则只会使用主栈,不会使用任务栈。

    通过”Internal”可以查看当前是处在中断还是任务中,Mode为”Thread”表示是在线程/任务中,或者是函数中(非中断),为”Handle”表示是在中断中;Privilege为”Privelege”表示当前处于特权模式。Stack为”PSP”表示当前使用的是任务栈,为”MSP”则表示使用的是主栈。

  • 反汇编调试窗口——Disassembly Window

    该窗口在”View->Disassembly Window”中可以打开,也可以在工具栏中这个位置打开。

    该窗口是通过bin文件反汇编出来的汇编文件(汇编跟二进制原本就是一一对应的关系)。当设置了优化等级后,部分C语言的调试会变得困难(汇编跟C语言不是一一对应,而程序运行又是完全根据汇编来走的),此时可能需要使用汇编窗口进行调试。

  • 命令窗口——Command Window

    该窗口在”View->Command Window”中可以打开,也可能在工具栏这个位置打开。

    这个窗口可用来输入一些控制命令,比如保存输出当前内存地址等。在命令窗口中输入如下导出指令,按下回车即可导出数据

save filename startAddr,endAddr

filename:导出数据的文件名,无论后缀是什么,导出的格式都是十六进制文件。没有输入路径时,文件自动保存在当前工程根目录下。
startAddr, endAddr:需要导出数据的起始地址和结束地址,也可以通过表达式写出来。

例如:

save ExportData.hex 0x08000000, 0x08000000+0x2000

  • 函数地址表——Symbols Window

    该窗口在”View->Symbols Window”中打开,也可能在工具栏这个位置打开。

    可以查看当前所有程序的函数调用关系及其所在地址。

  • 串口调试窗口——Serial Windows

    该窗口在”View->Serial Windows”中打开,也可以在工具栏这个位置打开。

暂未使用过,后续再添加。

  • 逻辑分析窗口——Analysis Windows

    该窗口在”View->Analysis Windows”中打开,也可以在工具栏这个位置打开。

    这个貌似只能在软件模拟仿真中使用,如果使用硬件调试,需要硬件支持。

  • 跟踪窗口——Trace Windows

    该窗口可以在”View->Trace Windows”中打开,也可以在工具栏这个位置打开。
暂时未使用过,后续再添加。

四、调试应用

  • HardFault(硬件错误)

    这个可以算是最常见的一个问题了,在开发过程中多多少少会遇到过程序死机的问题,而死机的大部分原因都是进入的HardFault中断,即常说的硬件错误中断。要想知道这个怎么调试,首先得清楚这是什么,怎么触发。
    触发原因:内存溢出,堆栈溢出,数组越界,中断错误,除0(在某些编译器下会有错误)等。前面三个,可以归结为都是内存异常操作导致,但因为其出现方式不一样,所以调试方式也不同。
    从现象反推,当出现这个错误时,第一时间查看函数的调度关系,看最后是死在哪个位置。如果不是堆栈溢出,一般来说是可以直接查到进入硬件错误前的最后执行的代码位置的。当然内存溢出跟数组越界也有可能导致无法查看调度关系,因为这个调度关系就是从程序运行栈里取出数据进行展示,所以当栈数据被破坏,则无法使用此方式进行调试。
    知道死机位置后(其实大概率就是因为某个异常指针的引用导致的问题),此时就去查找异常指针出现的原因。首先从逻辑层面看,异常指针是否是因为某个逻辑给指针赋了个错误值。
    其次是数据越界的角度来看,在Map文件中查找该指针的内存地址,查看其内存前后是否存在一些数组或结构体,然后去检查前后数组或结构体的操作是否存在下标溢出,指针偏移错误等问题。
    还有最后一种,就是直接从内存里获取数据作为指针地址进行引用,此类用法一般是在日志操作或GUI中比较常用,这种情况就要去内存数据来源是否存在问题。
    除以上三种可能性外,还有一种可能对一些人是涉及知识盲区的,就是引用地址没有地址对齐。这一部分是涉及内核的一些知识。这里简单讲下,对于M0内核,指针引用地址需要根据其引用的数据类型进行对齐。比如以下代码:

int main(void){uint32_t *p = 0x20000001;*p = 20;/* 这句一执行就会导致异常 */}

还有一种是操作指针本身的地址没有4字节对齐,也会出现问题。如下代码:

/* 实际这样子定义编译器会报错(在Keil中编译),这里只是为了直观表示 */uint32_t *p __attribute__((at(0x20000001)));int main(void){p = 0x20000010;/* 这句一执行就会导致异常 */}
  • 复位

    复位有几种类型,一是看门狗复位,二是软件复位,三是硬件复位。复位类型可以通过芯片自带的复位寄存器进行查看。不过查看前需要手动清除所有复位标志,不然其复位标志会一直保留着。
    先讲下看门狗复位,当单片机开启看门狗后,很多问题都会变成复位问题,比如上面说的HardFault,因为HardFault也是一个中断,只是默认中断里是一个While(1)的死循环,所以当进入中断后,一段时间没有喂狗操作,就会触发复位。或者一些操作陷入死循环的,均是同理。这里我们把这一类问题都归为死循环问题。处理方式,先把看门狗关掉,然后调试看停在哪个死循环中,如果是HardFault,那就看上面硬件错误的处理方式。如果是其他死循环,那就看是什么条件触发的。死循环的问题相对来说比较好找。
    另外一种比较难处理的看门狗复位问题,莫过于某些操作时间过长,导致喂狗不及时。比如读写Flash时,通常会关闭中断,当大量读写时,其操作时间不可小靓,未开看门狗的情况下会有肉眼可见的程序卡顿,开了看门狗的情况下则通常会触发程序复位。这种类型的问题,通过关闭看门狗可能也无法定位到具体位置,因为程序还可以正常执行,只是在某些程序段会变得比较卡顿。对于这种问题,最好的方式是通过代码对比,通过对比原本没出问题的代码和出问题代码的差异性,锁定问题大体出现的位置,再通过程序执行时间进行估算。也可以借助一个独立的定时器,在一些时间操作较长的可疑之处计时。比如程序调用了某个底层未开源函数,那可以在调用前后打印定时器的计数,来计算函数运行的时间。当然也可以通过Keil自带的调试计数值来计算运行时间。
    软件复位就比较好找了,一般是需要人为调试内核的复位接口进行复位,所以只要查看是哪些位置触发的调用复位函数的条件就可以锁定问题点。
    硬件复位就只能从外围电路进行切入了,考虑干扰、连锡等问题。当然有些硬件复位是通过一个硬件看门狗进行复位的,如果是这种应用,那参考内部看门狗的问题排查方式。

  • 逻辑时序类调试

    时序类的用断点调试法就很难做到了,特别是那种时序要求很严格的。就比如Modbus通信,协议是规定了一帧数据中每两个字节间隔时间不能超过1.5字符。所以想要在一帧数据中,按一个字节一个字节断点调试从机是不可能的,主机不会给你休息的时间。这时候就必须得添加一些测试代码了,添加测试代码最重要的一个原则,是不能变更原本的功能。所以一般在数据流向的关键路径上添加一些监控变量,通过监控变量的变化来识别时序是否出现错误。
    另外也可以使用逻辑分析窗口,把对应的变量添加进窗口中,通过时间变化查看变量对应的变化关系,以此来判断逻辑时序是否正常。

  • 内存调试

    如果有涉及boot或日志记录功能的编写,那肯定会涉及大量内存的对比及调试,这时候可以利用上面提到的小技巧,在命令窗口那里输入save filename.hex StartAddr, EndAddr把对应的内存数据打印出来。

  • 底层外设调试

    这个打开对应外设的寄存器界面,对着芯片用户手册查看每个寄存器的功能进行调试,只有对寄存器功能熟悉了才有对应的调试手段。

五、注意事项

1、有时候在watch窗口中,变量值不会刷新,这时候就需要查看一下”View->Periodic Window Update”是否已勾选,如果没勾选,变量只有在第一次添加或停止调试时才会刷新。另外当窗口里一次性加载了一个很大的数组,当展开数组时,变量刷新也会变得很慢,并且软件会变卡顿。

2、当选择了非0级优化时,调试可能会变得困难,具体表现在断点调试。比如现在下面的代码,代码优化的关系,有可能把case0、1、2里的return 1都合并成一行,导致运行调试时,无论当前程序进入了哪个分支,使用断点时都只会进其中一个。所以当开启代码优化等级后,需要注意断点调试将变得不可信。另外优化编译后,有部分代码也将无法打断点(被优化的代码)。

switch (xx){case 0:{do_something0();return 1;}case 1:{do_something1();return 1;}case 2:{do_something2();return 1;}default:{return 0;}}

此时应该去看汇编的实现,其执行顺序与汇编一致。

3、目前发现有部分工程在一些电脑上调试时,打断点后在删除断点之前退出调试,会导致Keil崩溃,只能结束进程重启。

4、当开启内部看门狗并且未打开调试关看门狗功能时,停止运行一段时间后会复位。

5、在全速运行时,有时打断点会无效,取消断点也无效,貌似是Keil本身的问题。

六、相关知识

    Keil5软件使用-基础使用篇、Keil5软件使用-进阶工程配置篇、Keil软件包-知识宝藏库