51单片机延时函数设计
际算出来的量是延时 0.105664 秒,差 2 个机器周期。这个差值被表示为以 机器周期为单位的量显示出来,在本例是-2。那么,先按照延时 0.105664 秒编写程序,然后紧跟着添加 2 个 nop 就 OK 了。 3. 如果差值是正的怎么办?例如延时 0.123458 秒,实际算出来的量是延时 0.123460 微秒,总不能减去 2 个 nop 吧。答:你可以这样,把延时迟数值稍 微改小一些,比如 0.123456,那样的话,计算出来的差值是零,然后再添加 2 个 nop 就 OK 了。 4. 太大或者太小的延时时间怎么办?答:如果延时 6 个时钟周期,直接写 6 个 nop 不是更好吗。如果延时 100 秒,可以每次延时 10 秒,分 10 次调用。貌 似延时 100 秒也没有必要精确到一个机器周期的样子。 5. 如果差值是-10000,岂不是要写 10000 个 nop 吗?答:你好笨,汉字一写一 划,二写二划,三写三划,这么说万字要写 10000 划了吗?如果差 10000 个, 可以再搞一个延时 10000 的延时调用不就行了,大鱼吃小鱼,小鱼吃虾米, 虾米喝海水吗,大事化小,小事化无。再说了,貌似这个计算程序不会弄出 10000 来的。 第三步:嵌入 C 程序中。 上面是汇编的代码,用 C 直接写精确的延时程序是不可能的,只能混合编程, 把汇编程序嵌入进去。当然代码要做一些变化以适应 C 语言的语法。程序需要 6 个内存空间用来保存变量,像下面这样,在 main 之前:
#pragma asm DELAY000: mov DelayCounterOuter, DelayConstantOuter DELAY001: mov DelayCounterMiddle,DelayConstantMiddle DELAY002: mov DelayCounterInner, DelayConstantInner DELAY003: djnz DelayCounterInner, DELAY003 djnz DelayCounterMiddle,DELAY002 djnz DelayCounterOuter, DELAY001 #pragma endasm }
天津工业大学信息与通信工程学院 宋培林
延时函数是最经常被使用的一种子程序,可以用空循环来做,也可以使用基
于定时器的中断来做。两种方法各有优缺点,各自适用不同的应用场合。以下讲
解我设计的可以精确到一个时钟周期的延时函数。注意,时间单位是机器周期,
不是具体的多少多少毫秒、微秒等。
第一步:汇编实现。资源开销:需要 3 个寄存器和 3 个内存单元。
58:
unsigned char = 0;
C:0x11A5 E4
CLR
A
C:0x11A6 FF
MOV
R7,A
59:
unsigned char j = 0;
60:
for(i = 0;i < 29;i++)
61:
{
62:
for(j = 0;j < 62;j++)
C:0x11A7 E4
CLR
A
C:0x11A8 FE
代码太复杂我搞不定它的规律;一层的话又嫌延时时间短,两层折中一下。
2. 变量 i 和 j 必须从 0 开始,到小于某个数值为止(即 for 语句里面的那个数
字,不能带上等于号)。
那么变量 i 和 j 的初始值如何得到呢?参看图 2,选择纯 C 模式,接下来的 使用方法与前面类似,只不过这次只有两层循环变量:中间层计数初始值旁边的 那个数字对应 i,最内层计数初始值旁边的那个数字对应 j,最外层计数初始值 旁边的那个数字无意义。
注意#0XXh 不能是#0h。那么前 3 行就是赋值,第 4 行是调用,包括前 3 行代码
在内,上面的程序一共运行了多少个机器周期呢?
[(2*delayr7+2+2)*delayr6+2+2]*delayr5+2+2+2+2+2+2 假定要延时 100 个机器周期的时间,那么把循环初始数值算好,给 delayr5、
汇编程序最开始,应该定义好 delayr5、delayr6 和 delayr7,比如像下面这样:
示例代码 1:
delayr5 data 30h
delayr6 data 31h
delayr7 data 32h
也就是说,delayr5 占据了地址编号为 30h 的内部 RAM,delayr6 占据了地址编
寄存器用来循环计数,内存单元用来保存循环次数的初始数值。假定寄存器
使用 r5、r6 和 r7,对应的内存单元用标号记为 delayr5、delayr6 和 delayr7。
使用 3 个寄存器和 3 个内存单元就是要做 3 层嵌套循环,r5 和 delayr5 用于控
制最外层,r6 和 delayr6 用于控制中间层,r7 和 delayr7 用于控制最内层。在
还要提醒你:以上只是 C 代码转变为汇编代码的典型实现。如果你的程序比 较复杂,那么可能内部 BANK0 的 8 个寄存器不够用,会切换 BANK,那么代码就 不一定是上面那个。这样的话,这个程序就不精确了。
图2
delayr6 和 delayr7 赋值就行了,也就是替换示例代码 3 中的那些#0XXh。计算 初始值可不是一个简单的活,我设计了一个 VC 程序,用于计算这些数值。 第二步:如何得到循环次数的初始数值。
程序界面如图 1 所示。
图1 1. 以 Hz 为单位设定晶振频率(只能输入数字,不要输入“Hz” )。 2. 像 AT89C51 这种芯片,一个机器周期包括 12 个时钟周期,那么要把 12 这个
如下的调用顺序:
示例代码 3:
;之前的其它代码
mov delayr5,#0XXh ;赋初始值,2 个机器周期
mov delayr6,#0XXh ;赋初始值,2 个机器周期
mov delayr7,#0XXh ;赋初始值,2 个机器周期
lcall DELAY
;2 个机器周期
;之后的其它代码
在示例代码 3 中,#0XXh 代表某些合适的数值(后面再说如何得到这些数值),
示例代码 4: unsigned char data DelayConstantInner = 0;//内层延时时间初值 unsigned char data DelayConstantMiddle = 0;//中层延时时间初值 unsigned char data DelayConstantOuter = 0;//外层延时时间初值 unsigned char data DelayCounterInner = 0;//内层延时计数器 unsigned char data DelayCounterMiddle = 0;//中层延时计数器 unsigned char data DelayCounterOuter = 0;//外层延时计数器 声明延时子程序,如果这个子程序被放在 main 之前,那么声明与实现可以 一起做,如下: 示例代码 5: void TimeDelay() {
96:
}
97:
C:0x11B3 22
RET
示例代码 7 一共运行多长时间呢?[4×j + 6] ×i + 6(单位机器周期)。
这个时间包括示例代码 7 全部代码行运行时间(从隐含的 LCALL 算起,直到隐含
的 RET 指令。)。注意:
1. 只能有两层嵌套循环,变量 i(外层)和 j(内层)。三层以上,编译出来的
为了保持精确性和与汇编的兼容性,应该这样调用: 示例代码 6 DelayConstantInner = XXX; DelayConstantMiddle = XXX; DelayConstantOuter = XXX; TimeDelay()
这样,生成的汇编代码与上面的纯汇编写法完全一致。XXX 的最小值是 1, 最大值是 255,切记不能为 0。
我找到了解决之道,参看示例代码 7。
示例代码 7
unsigned char i = 0;
unsigned char j = 0;
for(i = 0;i < 29;i++)
{
for(j = 0;j < 62;j++)
{
_nop_();
}
_nop_();
}
编译器做出来的代码类似如下格式(蓝色行是汇编代码,红色行是 C 源代码):
号为 31h 的内部 RAM,delayr7 占据了地址编号为 32h 的内部 RAM。具体的地址
根据程序需要自行修改。好了,现在看一下延时函数的设计:
示例代码 2:
DELAY:
mov r5,delayr5 ;2 个机器周期
DELAY1:
mov r6,delayr6 ;2 个机器周期
DELAY2:
mov r7,delayr7 ;2 个机器周期
MOV
R6,A
63:
{
64:
_nop_();
C:0x11A9 00
NOP
65:
}
C:0x11AA 0E
INC
R6
C:0x11AB BE3EFB CJNE R6,#0x3E,C:11A9
66:
_nop_();
C:0x11AE 00
NOP
67:
}
C:0x11AF 0F
INC
R7
C:0x11B0 BF1DF4 CJNE R7,#0x1D,C:11A7
DELAY3:
djnz r7,DELAY3 ;2 个机器周期
djnz r6,DELAY2 ;2 个机器周期
djnz r5,DELAY1 ;2 个机器周期
ret
;2 个机器周期