像写windows程序那样写单片机程序之高精度延迟函数

      很久没有继续这个主题写东西了,积累太慢了,不敢太快就拿出来献丑~_~必须经过实际项目的检验才敢与大家分享,否则有误人子弟之嫌。我每做一个项目都会把用到的芯片功能或其他相对独立的功能封装出来,以供后面的项目使用。这样以后的程序基本上就是在搭积木,把各功能模块组装一下基本就可以了。不过,功能模块的封装是需要慢慢体会的,要做到通用、易用也不是很容易。我写单片机程序的感觉像是在塑造一件艺术品,尽量完美体现。这样下来,每一次的收获都会不小。做过的东西,总需要留下点什么......

      好了,前面铺垫了不少废话,下面该来点“干货”了。说到延迟函数,就不得不提汇编,毕竟汇编语句的指令周期很好计算。可是我们希望用纯粹的c来做,精度够吗?这个问题嘛,其实c语言可以做到的,不过稍微费点劲。于是,又有人会问:既然用汇编容易做到,那还费那么大劲用c来做干什么?这个问题本着实用主义来说,问得没错,毕竟有简单的为什么不用简单的而非要用复杂的呢?我想也许可以用下面话来回答:如果我们仅仅是本着唯实用来做东西,会错过很多美好的事物。如果用c来实现精度相对较高的、实用的延迟函数其实是需要很多方面的知识的。不信我举几个例子:比如,c语句的执行效率问题、c的函数调用本质、函数返回本质、函数参数传递的过程......这些东西对于使用c语言的功力可是有大大的提高的。也就是所谓的“内功”,因为这涉及到了编译的一些概念。

 

     依然像前文一样,先给出头文件的定义。里面的一些关键字和引用的相关头文件前文也介绍过了,就不赘述了。

 

#ifndef _MY_DELAY_H_
#define _MY_DELAY_H_
#include "oscfrequencydef.h"
#include "const.h"
/***************************************
此延迟函数只适用12MHz 或 24MHz 的晶振
****************************************/

#ifndef OSC_FREQUENCY

  #error undefined OSC_FREQUENCY

#elif OSC_FREQUENCY == 12

#elif OSC_FREQUENCY == 24

#else

  #error OSC_FREQUENCY must be equal to 12MHz or 24 MHz

#endif

void DelayMS(BYTE Elapse);      //单位:ms
void DelayUS(BYTE Elapse);     //单位:us

//在中断中使用,防止重入问题
void DelayMSINT(BYTE Elapse); 
void DelayUSINT(BYTE Elapse);

#endif

12MHz晶振的情况讨论的比较多,因为51单片机是12分频的,单周期指令就是1us比较好计算。24MHz的情况大同小于,只是指令周期的差异而已。微秒级的延迟函数非常简单,就两句话:

void DelayUS(BYTE Elapse)
{

     Elapse /= 2;
     while(--Elapse);

}

我们再看一下对应的汇编代码:

   29:         Elapse /= 2;
C:0x0003    EF       MOV      A,R7
C:0x0004    C3       CLR      C
C:0x0005    13       RRC      A
C:0x0006    FF       MOV      R7,A
 
    30:         while(--Elapse);
    31: 
    32:         
C:0x075F    DFFE     DJNZ     R7,DelayUS(C:075F)

        从汇编代码中应该就明白了我为什么要把Elapse除以2,因为DJNZ 是2个机器周期,所以循环次数得减半。如此看来,12MHz晶振下的微秒级延迟,仅仅是多了个参数除以2的这四条指令,也就是只多了4us。精度还是满高的嘛。不过......我们有点高兴得太早了,我们犯了一个明显的只见树木不见森林的错误。错在了,我们只看到了函数内部的实现而忘记了函数传参、调用、返回的过程。这三个过程也是需要时间的。

         函数传参需要1个周期,函数调用就是跳转指令,需要2个周期,返回需要2个周期,连同上面的也都加起来,一共多了9个周期,即9us。这下就完整了。不过......(怎么还有不过?)我们还有一个很微妙的小问题没有注意到,Elapse的奇偶性问题。如果是偶数没问题,但如果是奇数呢?奇数的话就会少延迟了一个周期,合计下来就是延迟了8个周期了。好了,这下基本就完整了,我们归纳一下:

当参数为偶数时,延迟会多9us

当参数为奇数时,延迟会多8us

        这个微妙级的延迟误差在10us之内,而且是个常数,不随延迟时间的变化而变化。既然是常数时间,那么传参的时候可以减去这个常量。比如要延迟100us,那么只要调用DelayUS(91)或DelayUS(92)就会有99us或101us的延迟(注意奇偶数的延迟时间),只有1us的误差。精度还不错吧?但是怀疑c语言不能“精确延迟”的人还是会很傲慢地看着我问:“你延迟一个8us以内的我看看?”是啊,我们的函数至少需要“固定误差”8us,难道还能再减少吗?哈哈,我们又犯了固定思维的错误了,谁规定我们写程序必须用c语言的原始语句,而不能调用编译器的库函数?诸位想到我说什么了吧?对!keil里面有_nop_()函数,也就是汇编的NOP指令。如果需要8us以内的延迟,我们直接用8个_nop_()不就可以了吗?何必非要在延迟函数一棵树上吊死?再看一下我们延迟100us的例子,如果使用DelayUS(91)再加上一个_nop_()那岂不是就是完完全全的“分秒不差”了?“世界上不缺少美,缺少的是发现美的眼睛”~_~

        需要注意的是,参数不能为0,否则--Elapse就变为了255。尽管我们已经实现了这个颇具使用价值的延迟函数,但还是有一些问题值得思考的:为什么要用while而不用for循环?为什么是递减的循环而不是递加的循环?即使是递减的循环为什么得是--i而不是i--的形式?这些问题的答案总体上说可以一句话概括:不同的循环形式编译器编译出来的汇编代码是不一样的,效率也是不一样的。虽然从汇编我们可以计算出精确的机器周期,但是对比看来,只有递减操作且是前置递减形式产生的汇编代码最容易计算。

 

        微妙级的延迟函数介绍完了,该说一下毫秒级的了吧?这个问题嘛......我想省略~_~|不过我还是稍微提醒一下吧,既然微秒级的已经有了,毫秒级的还远吗?1ms的延迟不就是4个250us的延迟吗?有了1ms的延迟100ms的延迟还远吗?~_~完全可以把一个微秒级的延迟加上循环变为毫秒级的延迟。当然,里面依然有函数调用、传参和返回的时间需要考虑。但还是有人不服,会问:“那几秒、几分甚至几小时、几天的延迟怎么办?”唔......这种问题摆明了是在挑衅,不过也不是没有办法。几秒、几分的延迟我们可以用定时器来实现(对定时器的实现后面可是重头戏哦,一定不要错过),再多的定时就需要用专门芯片了(比如ds1302),否则单片机自带的定时器时间长了误差会很大。其实,提这个问题本身就是个问题,我们的程序中为什么要那么长时间的延迟让单片机白白在那里等着?如果需要延迟秒级以上,那么很大程度上就应该重新考虑程序的结构了。

     

        细心的人会发现,我的头文件中把延迟函数分为了两类,一类是普通应用,一类给中断调用。我们知道,在函数调用中有一个可重入性的问题,是为了避免这个问题我才把相同的东西写了两遍。从而保证在中断中调用不会影响。

        我想,现在可以对全文做一点小小的总结了。c语言较之汇编的优点是简洁的语法结构和编写方式,但相对于汇编的劣势是有时速度和效率不及汇编。然而,如果我们利用c语言但又明白其语句汇编后的形式,那岂不两全其美?所以,用c语言,但又知其所以然,这样的程序写出来绝不比汇编差。即使是稍微差一点,但节省的开发效率也能补偿回来。我喜欢把一个程序中用到的最核心的功能最先实现,然后再逐步扩展。这样会越做越容易。就像我们实现了微秒级的延迟后,再实现毫秒级就会容易许多。又啰啰嗦嗦了不少,还好能看到这句话的人还是能忍受下来的~_~

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值