延时实现V1.7(事实上以下实现方式应该是鱼鹰在使用V2.7版本很久后才采用的方式,但因为内容的相关性,换个顺序介绍)
是不是很简单,简单到让你怀疑它的功能!这里不再使用systick,而是使用DWT(关于这个模块,鱼鹰后期可能专门写一个小节介绍它,欢迎关注鱼鹰谈单片机),为什么使用它呢?1、不占用操作系统的心跳时钟;2、精度非常高,系统时钟的精度,也就是说,即使你多执行了一条指令,它也能发现!3、延时足够长,168 M频率下可延时 25 秒多(0xFFFF FFFF / 168),这对于大多数需求都足够了,只要你的执行周期或者延时时间在此之内的都没有问题。4、属于不用白不用的资源(cortex-m3、m4都有这个模块,像鱼鹰这么节约的人,肯定要用上的)。采用上述实现方式有什么好处:1、解决了可重入问题。2、函数内尽可能的减少不必要的语句执行时间。3、精度高,延时长(采用DWT的优势,而不是实现方式的优势)。现在我们来看看这段代码如何实现:微秒和168相乘,是为了换算168M主频下的延时时钟数(1 MHz = 1 us)。接着获取当前时钟,作为开始计时的时刻,最后当前时间(由硬件更改该值)与计时时刻比较,当发现时间增加到大于延时时钟数时,即可跳出循环,此时即达到了延时目的。以上代码似乎不难理解,但是有经验的道友可能会问,你的变量大小是有限的(在这里是4字节),你不怕溢出吗?溢出了之后,计时时刻可能会比当前时刻更大,那么使用减法会不会有问题?在大一的时候鱼鹰就在思考这个问题了,两个无符号的数相减,如果前者比后者小,会发生什么问题?这样是否就达不到准确延时的目的了?是否需要考虑溢出的情况?有人会说这只需要一个判断语句就能轻松搞定了,当前者比后者小的时候特殊处理即可,比如4和5相减,特殊处理即可。但鱼鹰一直觉得应该有一种比较好的方式去解决,直到大四实习看到 FIFO 的源码,鱼鹰才豁然开朗,终于找到了(对 FIFO 感兴趣的可以去看鱼鹰的另一篇笔记,很详细的介绍了一个非常有意思的公式,而鱼鹰也在那篇笔记中说到可以利用这种方式去做延时,只是里面写的有点bug,事实上不算bug,只是有种脱裤子放屁的感觉,各位道友可以去看看当时的实现方式,而对于 FIFO, 鱼鹰目前也有了更多的经验,发现那篇笔记的实现方式采用 % 取余运算效率较低,还有一种更高效的方式,而且建议能用判断语句,就别用 % 处理)!那么目前这种看似没有处理溢出的方式是否真的能够适应溢出的情况,答案是肯定的,那么原理何在?我们经常可以看到钟表(非数字手表),指针从1一直转到12,然后又从1开始,周而复始。当其从1转回到1时,即经历了12个小时,但是如果你在超过半天时间后来查看钟表时,虽然你之前看到的是1,现在看到的还是1,但实际上已经过去了24小时了!同理,计算机的世界亦是如此,如果说一个字节的最大值是255,那么这里的255就是钟表里的12,字节溢出后变为0,而钟表溢出后就是1,这种特性是由计算机和钟表本身决定的,不随外界变化而变化,当我们能够利用这种特性是,你会发现能简化很多东西(这个函数需要靠自己去悟,别人很难说清)。DWT计数器变量大小为4个字节,也就是说最大值为0xFFFF FFFF,那么我们来思考以下几个问题:1、这个函数的最大延时是多少?2、这个函数的使用是否真的没有一点隐患?3、它凭什么是可重入函数?4、是否适用于所有定时器?第一个问题,最大延时,前面鱼鹰已经计算过了,25秒多,那么精确的时间是多少呢?(0xFFFF FFFF / 168) us,那么为什么不是((0xFFFF FFFF + 1)/ 168) us?这个留给道友去思考。第二个问题,使用隐患问题,这个问题其实在说明钟表例子时已经说明了,如果你在超过它最大延时的时候再回来查询这个值,你会发现最终延时远远超出了。事实上,你的延时函数不可能被打断25秒多(如果真的打断这么长时间,你就要好好考虑了),但是你不得不考虑这个问题,因为你在下次使用过这种方式时,你不能确定是否真的能使用DWT这种超级延时外设,有可能你的最大延时是1 毫秒(比如一个定时器被你设定1毫秒溢出),那么你的延时函数被打断 1 毫秒后再回来执行是很可能的,所以你除了要考虑它的最大延时时间,还要考虑它最大被打断时间(DWT不用考虑这么多,因为25秒对于单片机来说实在是太长了)。事实上,除了这个隐患还有另一个,这个留在V2.7版本讨论。第三、所谓可重入,简单来说,就是这个函数能否当成两个函数执行,而不影响他们的功能,更实际一点的话就是,当在主函数和中断函数同时调用(注意用词,执行不恰当)这个函数时是否会造成功能紊乱(在这里表现为延时不准)。在这个函数中,这里的共享资源是DWT,按理说共享资源都需要进行保护,这样才能可重入,但是因为这个函数只对DWT进行读取操作,而不进行写操作(写操作由硬件自动完成),所以不存在修改共享资源的情况,也就是它可重入的原因,那么为什么那些参数、局部变量也是可重入的?这个基础扎实的话应该能懂,如果不懂就在评论区留言好了。第四个问题,思想可适用于所有定时器,但注意只是思想,当你的定时器是递减的,比如systick,那么需要做一点点修改,谨记(当然168这个数也得正确设置,如果不知道怎么修改,可留言)!事实上这种实现方式鱼鹰最近(2020-01)在安富莱电子(强烈建议工作的道友参考安富莱例程和文档,因为这些代码应该都是由一位大佬写的,非常专业,而正点原子更适合初学者)和RT-Thread文档中都有看到相关描述,只是在此之前鱼鹰并没有看到过类似代码,而是由FIFO源码深入思考受到启发,进而实现了以上代码。而当看到安富莱延时实现时才发现,实现代码惊人相似(实际上在此之前鱼鹰一直采用非死等方式实现延时的,但后来发现死等方式也很有必要,比如模拟I2C总线,所以利用非死等V2.7的实现思想实现了死等)。

推荐阅读:
-THE END-
如果觉得文章对你有帮助,欢迎转发、分享给朋友,感谢你的支持!
如果对本文有问题,欢迎留言!即使没有问题也可以留下走心评论。
如需转载请联系我。
微信公众号「鱼鹰谈单片机」
每周一更单片机知识
长按后识别图中二维码关注