由前篇可知,在DelayX10us()函数中用for循环延时会产生10个机器周期的固定误差,其中X传值、调用函数、子函数返回共5个机器周期,这是只要调用带参数子函数都有的、固定不变的;for循环判断x>0并跳转产生额外的5个机器周期的误差。
改进
根据《在单片机KeilC开发环境中设计精确的延时函数》中提到的内容,可将for循环改为while(--x),以消除for循环产生的额外5个机器周期的误差。
注意:应使用while(--x),这样对应生成的汇编语句才是DJNZ。如果使用while(x--),将额外产生几个指令,导致此延时函数不准。
更改后的程序如下:
//非精确延时10*X us,固定误差5us
//@12.000MHz 12T
void DelayX10us(unsigned char x)
{
unsigned char i;
do
{
_nop_();
i=3;
while(--i);
} while (--x);
}
反汇编分析
如前,采用level8的优化等级,反汇编后的代码如下:
计算一下延时时间:
x | 固定延时 | 循环延时 | 总计 |
1 | 5 | (1+1+2*3+2)*1 | 15 |
10 | 5 | (1+1+2*3+2)*10 | 105 |
100 | 5 | (1+1+2*3+2)*100 | 1005 |
可见,误差被缩减到5us了。
官方毫秒级延时分析
对于上述改进后的DelayX10us函数,X最大值为255,所以其最大延时为255*10=2550us=2.55ms。如果要获得更长的延时怎么办呢?在这里需注意,不能单纯将X改为unsigned int以获得更长延时,因为8051是8位单片机,对于16位的int类型,需要分成高8位、低8位运算,在'while(--x);'这句将不只需要2个机器周期。所以我们重新定义一个毫秒级的延时函数。STC官方的延时1ms程序如下:
//@12.000MHz, STC官方版本
void Delay1ms() //此处没有赋值那个机器周期
{
unsigned char i, j;
i = 2;
j = 239; //请注意j赋值的位置
do
{
while (--j);
} while (--i);
}
按之前的办法计算延时周期数t=函数调用LCALL+i j赋值+循环+返回RET= 2 + 1+1+(239*2+2)*2 +2= 966[在此感谢群友Smiles指出漏加的2个赋值周期]。与预期的1000个机器周期相差较大,为什么呢?
此处需注意变量j赋值的位置是在循环外,当外层循环执行到第二次(i=1)时,j不会赋值为239,故不能用239*2来计算。这里利用了一个溢出的小技巧。简单分析过程如下:
i=2,j=239赋值(2个机器周期)后,进入内层循环执行'while(--j);'共239次,j=0,跳出。由之前的分析可知,'while(--j);'的汇编代码为DJNZ指令,为2周期指令,周期数t1=2+239*2。
然后执行到外层循环'while(--i);',2个机器周期,i=1,DJNZ指令跳转到内层循环继续执行,周期数t2=2。
再次进入内层循环后,j=0,DJNZ命令为先执行寄存器减1,再判断是否为0,所以执行--j后,j溢出为0xFF(十进制255),不等于0,语句'while(--j);'继续执行,直到j再次自减到0,跳出。周期数t3=256*2。
再次到外层循环,'while(--i);',i=0,跳转到函数末尾准备返回。周期数t4=2。
函数调用和返回周期数t5=2+2。此处调用函数没有参数传递,所以没有赋值那1个周期。
总的周期数t=t1+t2+t3+t4+t5=2+239*2+2+256*2+2+4=1000。正好!
毫秒级延时函数改造
现在我们将官方只能延时1ms函数改造为可以延时多个ms,按之前的方法,给延时代码套一个do..while循环。
do
{
i = 2; j = 239;
do { while (--j); }
while (--i); }
while (--x);
此时延时机器周期数=1+2+(2+239*2+2+256*2+2+2)*X+2=998*X+5,倍增量不是1000,存在误差。x=1时,t=1003;x=10时,t=9985;x=100时,t=99805,已有接近200us的误差了,所以我们将括号内的机器周期改为1000,只需改j=240,即可使总周期数T=1+2+(2+240*2+2+256*2+2+2)*X+2=1000*X+5,固定误差5us,更改后的代码如下:
/**
* 晶振12MHz,12T模式下延时1*x ms,固定误差5us。
*/
void DelayX1ms(unsigned char x)
{
unsigned char i, j;
do
{
i = 2;
j = 240;
do
{
while (--j);
} while (--i);
} while (--x);
}
仿照这个模式即可写出任意固定误差5us的延时程序了。
另外,对于几微秒的延时,就建议采用_nop_()延时了。同时考虑移植性,不建议在程序中直接写多个_nop_()来延时。将所有延时函数写在Delay.h和Delay.c文件中,其余程序通通调用这个库,以后要更改,比如换了STC的1T单片机,延时需要修改,也只需要改这两个文件就可以了。
最后,附上我在用的延时函数库,所有X倍延时的固定误差=5us。
#ifndef __DELAY_H__
#define __DELAY_H__
typedef unsigned char UINT8
//定义默认设置:晶振12MHz,模式12T
#define FSOC_12M_MOD_12T
/**
* 晶振12MHz,12T模式下的延时。
*/
#ifdef FSOC_12M_MOD_12T
#define NOP() _nop_()
#define Delay1us() NOP()
#define Delay2us() NOP();NOP()
#define Delay5us() NOP();NOP();NOP();NOP();NOP()
#endif
void DelayX10us(UINT8 X);
void DelayX1ms(UINT8 X);
void DelayX10ms(UINT8 X);
void DelayX1s(UINT8 X);
#endif
/**
**********************************************************
****** Copyright(C), 2010-2016, NULL Co.,Ltd ******
**********************************************************
*@Tittle : 通用延时函数
*@Version : v1.1
*@Author : Liy
*@Dat : 2016-08-10 15:03:15
*@Desctription : 延时函数库
*@History :
* #v1.1 2016-08-10 15:03:41
* 1. 删除固定延时函数,仅保留X倍延时;
* 2. 优化X倍延时函数的时间,所有X倍延时函数误差控制为5us。
* #v1.0 2016-08-03 16:44:18
* 1. 完成12MHz、12T模式下常用延时函数
**********************************************************
**********************************************************
*/
#include 'Delay.h'
/**
* 晶振12MHz,12T模式下延时
*/
#ifdef FSOC_12M_MOD_12T
/**
* 晶振12MHz,12T模式下延时10*X us,固定误差5us。
* X最大值255
*/
void DelayX10us(UINT8 X)
{
UINT8 i;
do
{
_nop_();
i = 3;
while (--i);
} while (--X);
}
/**
* 晶振12MHz,12T模式下延时1*X ms,固定误差5us。
* X最大值255
*/
void DelayX1ms(UINT8 X)
{
UINT8 i, j;
do
{
i = 2;
j = 240;
do
{
while (--j);
} while (--i);
} while (--X);
}
/**
* 晶振12MHz,12T模式下延时10*X ms,固定误差5us。
* 周期数N=((2+(114*2+2)+(256*2+2)*19+2))*X+5
* X最大值255
*/
void DelayX10ms(UINT8 X)
{
UINT8 i, j;
do
{
i = 20;
j = 114;
do
{
while (--j);
} while (--i);
} while (--X);
}
/**
* 晶振12MHz,12T模式下延时10*X s,固定误差5us。
* 周期数N=(1+3+(((123*2+2)*1+(256*2+2)*153) +2 ) + ((256*2+2)*256+2)*7+2)*X+5
* X最大值255
*/
void DelayX1s(UINT8 X)
{
UINT8 i, j, k;
do
{
NOP();
i = 8;
j = 154;
k = 123;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
} while (--X);
}
#endif
好了,简单了解了下汇编,软件延时也基本精确了,结束。
设计资源 培训 开发板 精华推荐
- LT6657BHMS8-2.5 负分流模式基准的典型应用电路
- T12外壳面板
- 使用 NXP Semiconductors 的 ISP1161 的参考设计
- LTC2150IUJ-14、14 位、170Msps 模数转换器的典型应用
- LT6657AHMS8-2.5 低压差基准电压源的典型应用电路
- RREF02 +5V 精密电压基准堆栈的典型应用
- 当 LTC2380CMS-16 中启用数字增益压缩时,使用 LT6350 的典型应用被配置为接受 ±10V 输入信号,同时运行一个 5.5V 单电源
- 使用 Intel 的 IXF1010 的参考设计
- LTC3110IFE NiMH 电池备份/再充电应用的典型应用电路
- 使用 Semtech 的 SH3100 的参考设计