我们已经了解了很多的 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变量使用在前面公布的文章中,鱼鹰曾经说过一个词,颠覆认知:打了多年的单片机调试断点到底应该怎么设置?| 颠覆认知也说过一个串口通讯的问题:
KEIL 调试的 ini 文件有什么用?通过运用上面的知识,总算是解决了项目串口通信的问题,测试多天,不再出现这个问题了。那么鱼鹰是如何解决的呢?且听鱼鹰道来。这个通信错误的现象是,前一个数据帧尚未发送完成,后面又来一个数据帧,导致传输数据错乱了,这种现象在多任务中很常见,所以鱼鹰很早就对它进行了研究:
信号量保护之禁止中断
因为鱼鹰知道,资源互斥的现象很平常,只有深刻理解了其中的原理,你才能更好的掌握它。而这个通信过程的互斥资源有两个,第一个是发送缓存数组的资源,另一个就是串口。在这里因为没有采用队列的方式发送数据,所以缓存数组和串口是绑定在一起的。
也就是说,一旦数组中含有发送数据,那么马上就开始进行发送,中间不应该断开,所以可以把数组和串口当成一个资源使用(特别需要注意的是,一定要在对数组赋值之前获得互斥锁,而不是在串口发送前获取,因为只要这样,才能保证你发出的数组数据不会被其他程序修改)。
很明显,这个资源是互斥的,所以可以使用一个变量作为访问该资源的锁,每次访问之前看资源是否被使用,如果被使用了就等待。而锁的释放是由发送完成中断完成的,即只有将全部数据发送完成之后这把锁才能释放,进而由其他任务使用。
那么如何利用上面提到的技能查找这类问题呢?
这里的关键点还是在锁,因为如果这把锁被正确使用的话,是不应该出现两个数据帧混乱的情况的,需要对这把锁重点关注,那么该如何关注呢?
就是用前面的断点窗口,对这个变量(锁)设置写入访问断点,如下:
这里要说明一点,单片机所有的全局变量都是可以在命令行直接获取数据的(事实上如果你将断点设置在函数内部,也可以获取局部变量的值),比如你在命令行直接输入 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 还是会刷新数据的,这个时候的数据是可以信任的。
(关于这个还有一篇文章KEIL 下如何准确测量代码执行时间?)
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 时间也不会对整个系统有太大影响,前提是你别窃取太多了。
编辑:jq
-
cpu
+关注
关注
68文章
10825浏览量
211140 -
监视器
+关注
关注
1文章
780浏览量
33099 -
电机
+关注
关注
142文章
8932浏览量
145092
原文标题:KEIL 调试经验总结
文章出处:【微信号:zhuyandz,微信公众号:FPGA之家】欢迎添加关注!文章转载请注明出处。
发布评论请先 登录
相关推荐
评论