KEIL 调试经验总结

来源:公众号【鱼鹰谈单片机】

作者:鱼鹰Osprey

通过前面的多篇文章(已整理成专辑)我们已经了解了很多的 KEIL 调试方法,但是到底该怎么使用这些方法呢?这篇文章将介绍个人的调试经验。

本节分为如下几部分内容:

1) 虚拟串口

2) 变量使用

3) 数组输出

4) 时间获取

5) LOG 输出

6) 注意事项(KEIL 调试的缺陷)

虚拟串口

首先是虚拟串口,为什么要虚拟串口,这里的虚拟串口又是什么意思?

在线仿真的时候我们根本不需要虚拟串口,因为单片机一般来说都有串口,所以不需要虚拟的串口,但是在软件仿真情况下又该如何呢?

有些时候我们可能没有开发板,但项目很急,需要提前做,该怎么办?KEIL 的软件仿真可以帮你解决大部分问题,它可以帮你验证程序逻辑问题,也能验证硬件配置是否正确,相当不错的功能。比如你的串口配置是否有问题,进入仿真模式:

从上面的仿真图你可以了解到在软件仿真模式下,你可以正常使用 printf(),scanf()函数,输入输出操作由串口窗口进行,所以还是很方便的,但是需要注意的是:即使是软件仿真,它的发送时间和你的设定波特率也是有关系的,即发送完一个字节后,必须延时后才能在串口窗口看到数据,这个时间和现实时间可能不匹配(即 115200 不一定是 1 秒就发送 115200  bit,可能更多,也可能更少),但道理是一样,因为它对串口的工作过程进行了完全模拟在这里你还可以选择显示方式。

这里看到我之前写的一个注释:

但实际测试发现并不会丢失最后一个字符,不知道当时咋测试的。继续正题。有一个问题,如果说你想把这个数据传给其他上位机呢?比如你想把串口数据传输到一个虚拟示波器显示(事实上 KEIL 也能显示波形,但是功能比较简单),又该怎么办?

这个时候你可以虚拟一个 COM 口,将串口数据绑定到 COM 中:

在命令行中输入红色方框中的内容,既可达到目的。当然你可以将其保存为.ini 文件,具体使用方法请看鱼鹰相关笔记。

上面的命令参考链接:http://www.keil.com/support/man/docs/uv4cl/uv4cl_cm_assign.htm?_ga=2.225757210.1084600666.1557148734-475076243.1554469739

完成绑定后,KEIL 软件的串口 1 数据除了会发送到 KEIL 串口窗口中(【View】【Serial Windows】),也会发送到 COM1 口中(当然数据输入也是从 COM1 中进行输入的)。

但是因为 KEIL 对 COM1 进行了绑定,也就是你的 KEIL 正在使用 COM1 口,那么其他软件肯定是无法打开这个 COM1 口的,那么这样又该怎么查看 COM1 的数据呢?

此时你可以使用一个软件虚拟两个串口,这里我用 vspd.exe 软件虚拟 COM1 和 COM2,并且将 COM1 和 COM2 进行了连接,这里就是说 COM1 发送的数据会被 COM2 接收,接收同理,这样一来,KEIL 中的串口数据就能发送到 COM2 中了,因为 COM2 并没有被任何软件所使用,所以你可以使用串口助手打开(其他串口软件类似)。

因为在这里是虚拟的串口,所以说你的波特率参数并没有用处,即你的串口配置成 115200,你的上位机以 9600 接收也是可以的。

但是你会发现,这个中文支持不如 KEIL 自带的窗口好用,但通过将串口绑定到其他 COM 口的方法能扩展它的串口仿真功能,还是相当不错的。

事实上,KEIL 除了仿真串口,也能仿真 I2C、CAN,比如下面的:

对此感兴趣的可以参考链接:http://www.keil.com/support/man/docs/uv4/uv4_sm_can_communication.htm

只有深刻理解了其中的原理,你才能更好的掌握它。而这个通信过程的互斥资源有两个,第一个是发送缓存数组的资源,另一个就是串口。在这里因为没有采用队列的方式发送数据,所以缓存数组和串口是绑定在一起的,也就是说,一旦数组中含有发送数据,那么马上就开始进行发送,中间不应该断开,所以可以把数组和串口当成一个资源使用(特别需要注意的是,一定要在对数组赋值之前获得互斥锁,而不是在串口发送前获取,因为只要这样,才能保证你发出的数组数据不会被其他程序修改)。很明显,这个资源是互斥的,所以可以使用一个变量作为访问该资源的锁,每次访问之前看资源是否被使用,如果被使用了就等待。而锁的释放是由发送完成中断完成的,即只有将全部数据发送完成之后这把锁才能释放,进而由其他任务使用。

那么如何利用上面提到的技能查找这类问题呢?

这里的关键点还是在锁,因为如果这把锁被正确使用的话,是不应该出现两个数据帧混乱的情况的,需要对这把锁重点关注,那么该如何关注呢?就是用前面的断点窗口,对这个变量(锁)设置写入访问断点,如下:

这里要说明一点,单片机所有的全局变量都是可以在命令行直接获取数据的(事实上如果你将断点设置在函数内部,也可以获取局部变量的值),比如你在命令行直接输入 lock,那么结果如下:

0x00000001 就是这个全局变量 lock 的值。回到刚才锁的话题。在断点窗口设置之后,你就会在 Command 命令窗口中得到如下消息:

正常的 lock 数据流程应该是这样,1~0~1~0~1……也就是上锁、释放锁交替进行的,但是鱼鹰在观察有错误通信的代码发现,出现了 1~0~0 这种情况,也就是说一次上锁之后,出现了两次释放锁的过程,那这是怎么回事?

鱼鹰前面说过,释放锁的位置在串口发送完成中断中(此处是 DMA 传输完成中断),难道有其他位置对它进行了清零操作?为了确定这个位置继续增加调试信息(注意如果重新设置了一个新断点,需要把之前的断点删除):

这次将写入时的 PC 指针打印出来(需要注意,ST-LINK 不能实时更新这个 PC 数据,但 CMSIS-DAP 可以,暂时未找到解决办法),你会发现清零的位置是一样的(没有实际环境,模拟的情况):    

可以看到,清零操作时的 PC 指针都是一样的,也就是说,释放锁的过程确实只有一处,那么为什么会出现两次释放呢?那肯定是启动了两次 DMA 传输,导致两次进入传输完成中断,从而释放了两次锁。这里再说一点,怎么通过 PC 指针找到对应源代码的位置:

之后输入你的 PC 指针即可:

但是按理来说,如果上锁操作正常的话,应该不会出现两次启动 DMA 传输的过程才是,所以由此可以判断出,肯定有至少一处地方没有上锁而直接使用了互斥资源,才最终导致了异常情况。

通过对互斥资源在整个系统的使用情况,发现确实有一个地方使用了互斥资源,但是却没有对互斥资源上锁(当时由于某种原因,只对锁是否可用进行了判断,但判断完成后,并没有对它加锁)。当加上了这把锁之后,通信就此正常了(实际情况要比这个更复杂一些)。

为什么说颠覆认知,就是因为掌握了这个技能后,很多问题可以迎刃而解。就比如项目中有一个变量莫名奇妙的变化了,通过对每一次写入操作的监控,发现了数据变化顺序出现了问题,不该出现的变化却出现了,从而深入下去找到变化的原因,并最终解决了这个项目问题。可以说掌握了这个技能,鱼鹰用它解决了很多以前难以解决的问题,所以我才会对它推崇备至!

从前面讲述的内容可以知道,原来 printf 函数不仅可以打印一些字符串,还能获取运行在单片机上所有的数据(包括你定义的全局变量、静态变量、外设寄存器、CPU 寄存器),了解了这个,你再也不用在你原来的代码中添加调试代码后再删除了,使用这个方法有以下几点好处:

1、命令行中的 printf 函数打印数据比串口打印速度更快,极大地减少了调试语句对原本代码的影响。

2、再也不会忘记删除代码了,因为这些语句根本就没有下载到单片机中,只要退出调试模式,就不会对程序造成任何影响。

3、只要你使用 KEIL,有一个可以设置断点的调试器(不管是 ST-LINK、CMSIS-DAP、还是 J-LINK)都可以采用这种方式调试,极大的方便了开发。

说到程序的变量,事实上我们也可以在 KEIL 内部定义一个变量,即使用 DEFINE 命令定义一个变量名,这个变量不存在于单片机中,而只存在 KEIL 软件中,所以不用担心存储空间不够的问题,但是因为单片机和 KEIL 共用同一套符号系统,所以,你定义的变量名不能和单片机的全局变量名相同。至于你定义的这个变量用来干什么,那完全就是你自己的事情了。

数组输出

前面的内容说了程序中的变量都可以通过 printf 函数打印出来,但在公众号公布的文章说过,它毕竟不是标准的 C 语言函数,所以它不支持指针,所以也不支持数组,那么我们该如何输出数组呢?

这个时候其实要用到公众号公布的另一篇关于 ini 的使用问题的文章。看完那篇文章之后,在 ini 文件中输入以下代码,并编译执行:

然后,在你的程序中添加如下代码:

这里的 OspreyARR 数组是我们准备输出的数据,OspreyPointer 这个数据用于保存数组的地址。因为不支持指针,只能换种方式来达到相同的效果了。然后对上面的断点设置如下:

事实上我们也可以设置成这样:

这里的 0x2000 004E 就是数组的地址,但是因为每次编译之后,数组的地址可能都不一样,所以使用一个变量 OspreyPointe 实时保存这个地址,这样你需要显示什么数据,只要修改这个变量的值即可,不需要修改断点窗口的值。

全速运行之后,你就可以获得如下结果:

可以看到,数组中的所有数据都打印出来了,同时将当前打印的地址、长度信息也打印出来了。

数组查看这个技能有什么好处?下位机和上位机通信是很正常的是,而通信错误再正常不过了,那么怎么实时获取通信过程的数据呢,以前靠串口 printf,现在靠更高级的 KEIL printf,就是这么简单,串口助手都省了。

除此之外,我们还可以对接收或者发送的数据进行解析,方便阅读,比如下面的是我在工作中根据自己的通信协议做的一个简单解析:

时间获取

上面介绍了获取数据的方法,但是很多时候,我们不仅需要数据进行分析,还需要获取数据时间,有时候时间是很关键的一环。

那么该如何获取时间呢?

以前一般使用 SysTick 获取时间,但是当你使用操作系统的时候,你会发现这个时钟被操作系统占用了,那么怎么办?抢吗?肯定不行,那么只能找替代方案了,那么找谁,普通定时器?高级定时器?都不是,这里鱼鹰推荐 DWT。为什么推荐它呢?1、很多 STM32 单片机都集成了这个模块2、它的精度是 CPU 运行周期,即它是由 CPU 系统时钟驱动的,即你的内核时钟频率是 72 M,那么它的频率也是如此,所以精度很高。当我们使用定时器中断的时候,如果需要看你的定时中断是否及时处理了(如果没有及时处理,那么两次进入定时中断的时间肯定是不同的),那么使用 DWT 是不二人选,因为即使你的中断延迟了一个指令的执行时间,它也能发现,因为多运行一条指令,那么 DWT 的计数器必然会增加,所以如果时间要求高的话,可以直接获取计数器的值。但很多时候,可能并不需要那么精准的时间,而只需要大概的实际时间,而且为了方便使用,鱼鹰使用相对时间(即上次和这次执行时间之差),所以可以使用下面的这个函数(上面这个函数用于直接获取计数器的值,0xE0001004 为 DWT 计数器的地址,事实上 DWT 使用是需要初始化配置的,但 KEIL 在进入 Debug 模式后会自行配置,不需要你操心):

并且为了和正常时间匹配,对 DWT 时间进行了换算,单片机系统时间设置为 72M,所以我这里除以 72 用于换算成 us 时间,另外为了更加精确,使用了浮点型数据(关于为什么加 0xFFFF FFFF 请看公众号相关文章,鱼鹰就不在此详述了)。那么该怎么使用呢?我在公众号的视频中其实已经展示了这个方法,现在详细介绍这个方法:首先设置一个你需要的断点,然后在 Command 里面输入你的 printf()调试信息:

这样就可以了(前提是你已经使用 ini 文件包含了上述内容)。其实单纯获取时间信息是用处不大的,你还可以结合前面的变量和数组数据显示,一起输出到命令窗口,这样你就能获得这个断点的执行频率和变量的数据,但是这里需要说明一点的就是,鱼鹰在 KEIL V5.14 下用调试器 CMSIS-DAP 可以同时在 Watch 窗口和命令窗口中实时刷新数据,但是使用 ST-LINK 时发现命令窗口的数据变量值是不能实时刷新,也就是说这个变量始终是一个数据,并没有改变。但是后来在使用 V5.25 版本时,CMSIS-DAP 调试下,命令窗口能刷新,Watch 窗口却不能刷新了,所以各种情况需要道友自行分析,不能认为数据不变就是真的不变了,很可能是软件或调试器的问题,但能确定的一点就是,当你将程序暂停时,Watch 还是会刷新数据的,这个时候的数据是可以信任的。

LOG 输出

不知道你是否羡慕别人的上位机程序能够实时的打印 LOG 数据,是否梦想着自己的嵌入式程序有一天也能实现?事实上真的可以。

在嵌入式开发时,受单片机资源的限制,很多时候都是用串口打印数据,高级一点的用 J-Scope 之类的工具,但是用串口有比较多限制:

1. 需要实现串口驱动程序,并占用为数不多的串口资源

2. 串口速度比较慢

3. 需要一个类似串口助手的上位机

4. 数据接收后需要自己保存这些数据

5. 不能设置断点,调试受到很大的限制

6. 调试代码在调试完之后得删除,万一忘记了,就会影响性能但是用了 KEIL 自带的 LOG 打印功能,就不存在这些问题,它的输出速度就是调试器的速度,调试器多快,你的打印就有多块(但是打印数据也别太多,需要针对性的打印,后面会说原因),而调试器速度一般都是 M 级别的,对于一般情况完全够用了。

现在看怎么使用,使用的话,其实很简单,就是几条指令的事情,在你的 ini 文件最后输入以下命令:

这样从这条命令以下的所有内容都会保存在 DEBUG_LOG_OUT.txt 中(所以如果你不想把 ini 文件的其他内容保存在 LOG 中的话,那么就把这条命令放在 ini 文件最后即可)。

现在解释一下这几条命令,LOG OFF 表示将 LOG 文件关闭,即使你没有打开一个 LOG 文件,执行该命令也不会出错,这条命令主要是防止一个 LOG 文件重复打开的错误,加上这条命令就不会了。

第二条命令,即将 Command 窗口的数据保存在 DEBUG_LOG_OUT.txt 中,注意这里有个 > ,而 DEBUG_LOG_OUT 这个文件名就随你意了,但是实验的时候按这个来,等你确定会了之后就可以随便取你喜欢的名字了,出了问题自己对比一下就知道了。

然后再说一点,这里使用的是相对路径,即你的工程文件下的路径,如果你想往上一层,你可以使用 ../ 表示在这个工程上的一个文件夹下输出 LOG 文件。

退出调试模式之后,KEIL 将自动保存 Command 数据到文件中(也就是说在此之前你是看不到这些调试数据的),现在看看我的调试 LOG:

一次设定之后,LOG 打印就不需要你操心了,即使调试器通信错误,它也会把之前输出的数据保存下来的。

看到这里,你应该知道 ini 文件到底有多重要了吧,你的所有调试命令都可以用它保存并在进入调试模式后自动执行,比如说你有一个断点,很复杂,不想每次设置,那么你可以在设置完一次后,从命令窗口将这个命令复制到 ini 文件中,比如像这样:

这样你每次进入调试模式后,那些断点就会被自动设置了,根本不用你操心,而且如果需要修改的话,也是直接在编辑器中修改后重新编译就行,马上就能生效,不再需要从断点窗口设置了。

而这里有个删除所有断点的命令,这是为了防止和之前设置的断点冲突,所以一次性全部删除了(事实上,可以删除某一个断点,但需要断点序号,而断点序号每一次都可能不一样,所以选择直接全部删了方便)。而为了更好的配合这些功能,可以把下面的 Breakpoints 勾选去掉,这样它就不会保存关于断点的设置了,而为了让 Toolbox 在关闭后还能每次自动显示出来,也可以去掉 Toolbox 的勾选。

另外再说一点,KEIL 支持把某一块内存数据保存成文件,这个命令是 SAVE,感兴趣的话可以去官网了解一下。

注意事项

上面说了 KEIL 命令调试的很多优点,现在说说它的缺点:

1、KEIL 命令调试不支持指针,这个已经多次强调了,要实现指针的功能,只能间接使用。2、对程序运行造成一定的影响(事实上这个不关 KEIL 的事,是调试系统本身的问题)

前面说过,调试器可以说是第三方监视器,虽然几乎没有侵入性(事实上对 CPU 还是有影响的),但是它还是会窃取 CPU 时钟的,而且在执行断点的时候,虽然由 ini 文件定义的函数由 KEIL 执行了,实际上上执行这些函数也是需要时间的,那这个时间怎么来,就是通过暂停 CPU 后去执行这些代码,这个你可以通过 DWT 计数器看出来,因为只有 CPU 执行了 DWT 才会计数,但是你会发现在执行这些代码时,DWT 是没有进行计数的(在 KEIL 函数的前后获取 DWT 计数,可以发现计数值不变):

也就是说 CPU 和 KEI 是在交替使用系统时钟的。平常来看,由于 KEIL 执行速度很快,看不出来问题,但到中断的时候却会出现问题。

情况是这样的,驱动步进电机时,鱼鹰使用了这种调试方法打印每次进入定时器中断的时间,发现即使使用最高精度的情况下(CPU 运行时钟),每次进入中断的时间看似都是固定的,但步进电机还是表现出失步情况,也就是说系统内部时间看起来每次进入中断时间一样,但是实际情况是,已经丢失了时间(好好理解这句话),这个时间损耗就在运行这些命令上,而一旦把这些命令输出删去,就会发现电机不再出现失步了。

这是一个比较大的缺陷,但是在一般情况下是不会有多大问题的,因为一般情况下窃取一点 CPU 时间也不会对整个系统有太大影响,前提是你别窃取太多了。

声明:本内容为作者独立观点,不代表电子星球立场。未经允许不得转载。授权事宜与稿件投诉,请联系:editor@netbroad.com
觉得内容不错的朋友,别忘了一键三连哦!
赞 3
收藏 3
关注 151
成为作者 赚取收益
全部留言
0/200
成为第一个和作者交流的人吧