本帖最后由 hotman_x 于 2014-5-11 08:26 编辑
事实上没有什么做到精确延时的通用方法,方法有二:
一是用汇编,根据晶振频率,对照数据手册,换算出指令周期,实现精确延时;
二是用C,通过实测调整,如果不是要求非常精密,也差不多少。
我先贴一个自己做的,抛转引玉。这个方案基于方法一,用于 STC12C5A60S2,在主频 22.1184mHz 下精确到微秒。可以直接换算到常用频率 11.0592mHz,只不过变成精确到 2 微秒就是了。
另外,我用的是坛子里比较少见的 SDCC 编译器,不过,汇编没多大区别,很容易换成 keil。
分析(STC12C5A60S2,主频 22.1184mHz):
CPU为所谓的 1T 机,即不对主频进行分频(传统8051CPU会对主频进行12分频使用)。这样的话:
1. 明显的,大约 22 个指令周期为 1 μs;
2. 根据1,每 100 μs 误差约为 -0.54 μs,大约相当于12个指令周期。
3. 根据2,每8 μs补偿一个指令周期,那么:每100 μs 误差 +0.0072 μs,误差率低于 0.01%,可以接受。
微秒级延时函数,误差在1个指令周期之内。
- void delayUs(unsigned char us) __naked { // 6 入口
- us; // 抑制变量未用警告
- __asm
- ; 第一个 μs 共 22T。用于出入口(已用 10T)
- ; 第一个 μs 的前半部分,7T
- MOV A, DPL ; 2 DPL中是函数参数
- DEC A ; 2 减去第 1 个 μs
- JZ 0001$ ; 3 计数为0则直接跳到 1μs 的后半部分
- ; 以下是第二个 μs,共22T。用于准备主循环
- ; 第二个 μs 的前半部分,11T+3T
- MOV DPL, R0 ; 3 保存R0
- MOV R0, A ; 2 以下用 R0 作为 μs 计数器
- MOV A, #6 ; 2 补偿计数,去掉已过的 2μs
- DJNZ R0, 0003$ ; 4 减去第二个 μs。计数不为 0 则进入正常周期
- SJMP 0002$ ; 3 直接跳到 2μs 的后半部分
- ; 以下是 3μs (含)之后周期延时部分,每周期 22T+1T(遇 8μs 周期时)
- ; 补偿
- 0003$:
- JNZ 0041$ ; 3 如果未到补偿的时刻(A>0),跳过补偿部分
- ; 这里是补偿部分,5T,比非补偿部分多1T
- MOV A, #7 ; 2 每 8μs 补偿一次,当前这次自行减去
- SJMP 0042$ ; 3 回避非补偿部分
- ; 这里是非补偿部分 4T
- 0041$:
- DEC A ; 2 计算补偿周期
- NOP ; 1
- NOP ; 1
- ; 计算完补偿(7T)之后剩余的部分,15T
- 0042$:
- CJNE A, DPL, 0043$ ; 5
- 0043$:
- CJNE A, DPL, 0044$ ; 5
- 0044$:
- NOP ; 1
- DJNZ R0, 0003$ ; 4 μs计数
- ; 补偿 2μs 前半部分最后被跳过的 SJMP 指令
- SJMP 0022$ ; 3
- 0022$:
- ; 以下是 2μs 的后半部分,8T
- 0002$:
- SJMP 0021$ ; 3
- 0021$:
- NOP ; 1
- MOV R0, DPL ; 4
- ; 以下是 1μs 的后半部分,5T
- 0001$:
- CJNE A, DPL, 0011$ ; 5
- 0011$:
- RET ; 4
- __endasm;
- }
复制代码
毫秒级延时函数,误差依然是1个指令周期。
- void delayMs(unsigned char ms) __naked { // 6
- ms; // 抑制警告
- __asm
- ; 第一个ms,共 35T + 10T(进出) + 5T(周期补偿) + 800us + 196us
- ; 本应是34T+10T,但考虑错过了一次8us补偿,所以加到35T
- ; 1ms 部分,共 40T(含周期补偿,不含出入部分)
- ; 1ms 前半,21T
- PUSH _R0 ; 4
- PUSH _R1 ; 4
- MOV R0, DPL ; 4 用 R0 计 ms 数
- MOV R1, #4 ; 3 前800us的循环计数
- MOV DPL, 198 ; 3 相当于从第一次循环中减去 2 us
- SJMP 0001$ ; 3 直接跳到 1ms 后半,直接跳入周期,所以补偿5T(见开头处)
- ; 主体循环部分。每周期 45T + 998us
- ; 本应 44T,考虑错过一次 8us 补偿
- 0002$:
- MOV R1, #4 ; 2
- 0021$:
- MOV DPL, #200 ; 3 x 4 = 12
- 0001$:
- LCALL _delayUs ; 200us x 4 = 800us
- NOP ; 1 x 4 = 4
- NOP ; 1 x 4 = 4
- DJNZ R1, 0021$ ; 4 x 4 = 16
- MOV DPL, #198 ; 3
- LCALL _delayUs ; 198us
- DJNZ R0, 0002$ ; 4
- ; 1ms 的后半 19T
- NOP ; 1
- MOV R0, #2 ; 2
- 0011$:
- DJNZ R0, 0011$ ; 4 x 2 = 8
- POP _R1 ; 4
- POP _R0 ; 4
- RET ; 4
- __endasm;
- }
复制代码
最后,秒级延时函数。有了前面的基础,不必那么讲究了,直接用C写,误差也不过是10微秒这个级别。
- void delayS(unsigned char sec) {
- unsigned char i;
- unsigned char j;
- for(i = 0; i < sec; ++i) {
- for(j = 0; j < 4; ++j)
- delayMs(250);
- }
- }
复制代码
这个就是前面说的“方法二”了。完全实用主义的态度,其实是最常见的。大凡用51单片机的,大概都这样搞过,呵呵。
一周热门 更多>