51单片机:生成精准的软件延时函数——以STC8演示
目录
一、延时函数的基本结构
二、计算延时函数的变量
三、C11 代码实现
1.main.c
2.delay.c
3.delay.h
4.说明
四、代码下载——Github
毕业设计涉及IOT的内容,目前什么也不会,只能从复习单片机开始。在用STC官方工具STC-ISP(V6.87B)生成软件延时函数时,发现它有两个错误:
1)一个是最多只能生成循环变量为3的延时函数,延时长达多秒时显然三个循环变量已经不足,给出的是错误的延时函数。
2)检查发现当初始化循环变量为0时,Keil C51 编译器会编译为:
CLR A
MOV Rx,A
而非直接编译为:
MOV Rx,#0
这导致了多出了一个 CLR 指令(一个机器周期)的时间,包括STC8系列的已知51单片机执行MOV Rx,A 和 MOV Rx,#0 指令所用的都是一个机器周期。 也就是说,循环变量是一字节的 unsigned char 时,初始化为 0 要比初始化为其他值 ( 1 ~ 255 ) 多消耗一个时间周期的。
当有多个循环变量初始化为 0 时,也只会多出一个时间周期,因为遇到第一个会 CLR A ,后面再出现初始化为 0 就直接 MOV Rx,A 。
是时候自己打造一个针对 Keil C51 生成精确软件延时函数的程序了!
一、延时函数的基本结构
延时函数怎么写才能确保 Keil C51 编译器生成的汇编指令代码可以精准调整呢?
1. 首先我们函数必不可少的是调用延时函数,这将被编译为 LCALL 指令(ACALL指令机器周期与LCALL相同),延时函数返回需要 RET 指令,这两个指令机器周期之和是必不可少的。
需要的延时时间如果小于这个值,就不需要编写延时函数。刚好等于时,不需要在函数内写任何内容。
2. 采用 do-while 循环,为什么呢?因为51单片机中实现多重循环最简单的形式就是嵌套DJNZ指令。
DJNZ指令是什么格式呢?先执行循环体 —— 循环变量减1 —— 循环变量不为0就跳转继续执行循环体,为0就不跳转结束循环。
;三重嵌套循环
LOOP:
DJNZ R5,LOOP
DJNZ R6,LOOP
DJNZ R7,LOOP
这在C语言中,和什么结构一致呢?那就是 do-while + 前置自减运算,最内层由于没有循环体,改用 while 语句是一样的。
do
{do{while (--i)continue;} while (--j);
} while (--k);
采用这种结构作为循环部分,可以很轻松的通过改变循环变量调整循环耗费的机器周期了,因为循环部分执行的指令的只有 DJNZ 指令,需要注意的是 DJNZ 指令在STC8单片机中跳转和不跳转耗费的机器周期是不同的,在之后计算中需要考虑这一点,在DJNZ跳转和不跳转时耗费相同机器周期的单片机中将这两个变量设置为相同值就可以。
3. 确定所用的循环结构之后,需要确认的就是循环变量了。循环变量在C语言中采用什么类型呢,首先肯定是整型。DJNZ指令操作的都是一字节数,也就是范围在 0 ~ 255 之间,那么与之对应的就是采用 unsigned char 类型作为循环变量的类型。
4. 循环变量的初始化需要耗时,在之前已经说过,Keil C51在初始化为非 0 值时,采用的是立即数方式。
MOV Rx,#number
而在初始化为 0 时,采用的是清零寄存器 A ,赋值A 给 Rx 的方式。多个变量初试化为 0 时,只会执行 CLR A 指令一次!
CLR A
MOV Rx,A
可以得出:
- 每初始化一个循环变量,就需要耗费一个 MOV 指令时间(这两种方式,目标通用寄存器Rx的MOV指令耗时一定是一样的,一般为一个机器周期);
- 在至少有一个初始化变量为 0 时,需要多计算一个 CLR 指令时间。
5. 单单的通过循环变量的大小不足以精确的达到每一个想要的延时周期,因为 DJNZ 指令并不是单周期指令,我们需要通过 nop 指令来补充。
#基本结构:
void Delay(void)
{//1.声明循环变量,并初始化//2.可选的若干个nop函数//3.do-while嵌套循环
}
二、计算延时函数的变量
设定好延时函数的基本结构后,我们只需要设定两类值就可以精准的确定一个延时函数的延时时间。
- do-while 嵌套循环的若干个循环变量的初试值。
- nop 函数的个数,可能 0 个,也可能很多,这需要在确定循环变量初试值后计算。
设置当前 51 单片机的各指令周期(以STC8为例):
//指令的机器周期 默认NOP=1
#define DJNZ_NOT_JUMP 2
#define DJNZ_JUMP 3
#define CALL 3
#define RET 3
#define MOV 1
#define CLR 1
- 把延时时间转换为机器周期数。鉴于设置时钟频率是 Mhz 为单位的,采用微秒为延时函数的最小单位可以方便计算。如:在时钟频率(CPU = 24Mhz),机器周期/时钟周期(SPEED = 12)的单片机上,需要延时 t us,就是延时 round(t * CPU / SPEED) 个机器周期,考虑时钟周期很可能不是整数,把结果四舍五入。
- 确认延时 x 机器周期时,最少需要多少个循环变量。一个循环变量时可以最多延时几个周期呢?设置循环变量 i = 0,此时循环 256 次后结束循环,也就是执行了 255 次跳转的 DJNZ 指令,1 次不跳转的 DJNZ 指令。计算出一个循环变量的最大周期数后,考虑两个循环变量时,此时循环体内不是空的了,而是有第一次循环,它最大耗费周期数已知 T1 。也就是执行了 255 次 T1 和 跳转的DJNZ ,1 次 T1 和 不跳转的DJNZ。
递推计算出1 ~ 4 个循环变量时,最大的循环周期数。key[ i ] 表示 i 个变量时最大循环数,key[ 0 ] = 0
key[i] = (key[i - 1] + DJNZ_NOT_JUMP) + (key[i - 1] + DJNZ_JUMP) * 255;
不考虑 4 个以上的循环变量数,4 个循环变量足以完成大多情况,24Mhz,SPEED = 1 时,数百秒的周期都可以做到。
- 当延时周期 - CALL - RET 后,确定需要用几个循环变量,每多增一个就需要减去 MOV 个周期用于这个循环变量初始化。考虑到 DJNZ 不跳转时也不为单周期,比一个循环变量最大周期数大 1 的延时,并不需要二重循环,只需要用 nop 补充。
while (k1 - MOV >= key[ind] + DJNZ_NOT_JUMP && ind != Maxind)k1 -= MOV, ++ind; //找寻至少需要多少个循环变量
- 从最外层循环开始,逐一确定循环变量初始值。需要考虑何时需要用 nop 补充,具体看代码。
数组 P 存储循环变量初始值和 nop 数量,ks为延时周期剩余量,临时存储每层循环耗费后剩余的周期数,这便于之后的回溯修正。这是因为计算结束后,nop 的数量可能是负值。
void delay_re(int t)
{for (int i = t; i >= 1; --i){for (int x = 1; x <= 256; ++x){LL ans = DJNZ_NOT_JUMP;if (x >= 2)ans += (x - 1) * (key[i - 1] + DJNZ_JUMP);if (ks[i] - ans < key[i - 1] + DJNZ_NOT_JUMP + MOV){ks[i - 1] = ks[i] - ans;P[i] = x % 256;break;}}}P[0] = (int)ks[0]; //转移剩余的周期作为NOP
}
- 计算出负数量的 nop 是必然的,这是因为之前提到的 :DJNZ 循环耗费的周期不可能每次都刚好,因为它本身不是单周期指令,需要 nop 补足空余周期。上面的函数采用多计算一个循环的做法,遇到 nop 计算值是负数时,需要将循环减一。
怎么个循环减一呢?找到最内层不是 1 的循环变量,将它减一后计算空余出的 nop 补充上即可。
例:最简单的情况,我需要循环部分延时 x 周期(设 DJNZ 指令不跳转时 2 周期,跳转时 3 周期)
do{}
while(--i);
//对应汇编
LOOP:
DJNZ i,LOOP
①x = 2
//结果:i = 1,nop = 0
②x = 5
//结果:i = 2,nop = 0
③x = 3
//结果:i = 2,nop = -2
找到最内层不是 1 的循环变量,就是 i ,把它减一,nop 加上它减一导致多出来的周期数 3 ,nop = 1 。
简单的情况就是最内层不是 1 的循环变量正好是最内层的循环变量。不是最内层循环变量时,可以肯定这是因为内层循环变量逐层进位导致的,可以类比成256进制数,1 代表 0,2 ~ 255 代表 1 ~ 254,0 代表 255。减一的循环变量不是最内层变量时,内层的所有变量必然都是 1 ,所以把它们都改成最大值 0 即可。
if (delay_re(jnd), P[0] < 0) //是否有不合理nop值
{jnd = 1;while (P[jnd] == 1)++jnd; //回溯寻找首个不是1的位置P[jnd] = (P[jnd] + 255) % 256; //当前位置值减一,不能直接减一,要考虑(0-1) = 255P[0] = (int)ks[jnd - 1] + DJNZ_JUMP; //nop补上缺少的值for (int i = 1; i < jnd; ++i)P[i] = 0; //全填入0,满足最大值
}
- 验证当前循环变量初始值有没有 0 。如果有必须将延时周期减一以执行 CLR 指令,先查看 nop 指令个数是否大于 0 ,有 nop 指令,就减一次 nop 指令;如果没有就必须重新计算延时周期减 x(x = 1,2,3...) 的循环变量初始值,直到满足条件后,补充 nop += x 。
三、C11 代码实现
1.main.c
#include "delay.h"int main(void)
{FILE *fp = NULL;
#ifndef CHECKfp = fopen(FILE_NAME, "w");int a, b;printf("输入时钟频率CPU(Mhz)、机器周期/时钟周期SPEED\n");printf("CPU=");scanf("%lf", &CPU);printf("SPEED=");scanf("%d", &SPEED);printf("当前延时函数时间单位是 " DS " ,输入要生成的延时函数范围[a,b]\n");printf("a=");scanf("%d", &a);printf("b=");scanf("%d", &b);for (int i = a; i <= b; ++i)delay(i);printf("生成完毕,请查看" FILE_NAME);fclose(fp);
#elif defined(us)CPU = 6.0, SPEED = 1;for (int i = 1; i <= 10000000; ++i)//检查1us~10sdelay(i);
#endifreturn 0;
}
2.delay.c
#include "delay.h"#define Maxind 5 //循环变量的最大值,超出立即引发程序错误double CPU;
int SPEED;bool iskey_init = false; //是否已初始化最大值
LL ks[Maxind]; //剩余量缓存
LL key[Maxind]; //最大值
int P[Maxind]; //ijk值记录
int ind; //循环变量个数void delay_init(void); //首次运行时,初始化最大值数组
bool delay_us(LL k); //数据初始化,调整nop值
void delay_re(int); //初次生成初始值+nop
void delay_print(FILE *, int); //打印生成函数,CHECK宏开启时,关闭输出。
bool delay_check(LL); //CHECK宏开启时,检验延时函数正确性,错误的生成将报错。(已校验,无错误)void delay_call(FILE *fp, int t)
{if (!iskey_init)delay_init(), iskey_init = true;
#if defined(us)LL x = t;
#elif defined(ms)LL x = t * 1000LL;
#elif defined(s)LL x = t * 1000000LL;
#endifx = round(x * CPU / SPEED); //取最近周期数int pr = 0;while (delay_us(x - pr)) //免除有i/j/k等于0且nop无法补偿的情况++pr;P[0] += pr; //用nop补偿
#ifdef CHECKdelay_check(x);
#elsedelay_print(fp, t); //打印函数
#endif
}
void delay_init(void) //初始化最大值
{for (int i = 1; i != Maxind; ++i)key[i] = (key[i - 1] + DJNZ_NOT_JUMP) + (key[i - 1] + DJNZ_JUMP) * 255;
}
bool delay_us(LL k)
{LL k1 = k - CALL - RET; //CALL&RETif (k1 < 0){fprintf(stderr, "error:do not need function\n");exit(1); //少于编写延时函数最少的机器周期}//--------------------------------------------//for (int i = 0; i != Maxind; ++i)P[i] = 0; //初始化Pind = 0; //初始化ind//--------------------------------------------//if (!k1)return false; //刚好CALL+RET构成//--------------------------------------------//while (k1 - MOV >= key[ind] + DJNZ_NOT_JUMP && ind != Maxind)k1 -= MOV, ++ind; //找寻至少需要多少个循环变量if (ind == Maxind){fprintf(stderr, "error:too more loop\n\n");exit(1); //超出允许的最大循环变量数!}//--------------------------------------------//ks[ind] = k1; //初始化ks//--------------------------------------------//int jnd = ind;if (delay_re(jnd), P[0] < 0) //是否有不合理nop值{jnd = 1;while (P[jnd] == 1)++jnd; //回溯寻找首个不是1的位置P[jnd] = (P[jnd] + 255) % 256; //当前位置值减一,不能直接减一,要考虑(0-1) = 255P[0] = (int)ks[jnd - 1] + DJNZ_JUMP; //nop补上缺少的值for (int i = 1; i < jnd; ++i)P[i] = 0; //全填入0,满足最大值}//--------------------------------------------//bool re = false;for (int i = 1; i <= ind; ++i)if (P[i] == 0)re = true; //发现等于0的变量,keil编译时0会用CLR A赋值,导致多一个nop时间if (re && P[0] >= CLR) //nop不为0则可以用nop补偿P[0] -= CLR, re = false;return re;
}
void delay_re(int t)
{for (int i = t; i >= 1; --i){for (int x = 1; x <= 256; ++x){LL ans = DJNZ_NOT_JUMP;if (x >= 2)ans += (x - 1) * (key[i - 1] + DJNZ_JUMP);if (ks[i] - ans < key[i - 1] + DJNZ_NOT_JUMP + MOV){ks[i - 1] = ks[i] - ans;P[i] = x % 256;break;}}}P[0] = (int)ks[0]; //转移剩余的周期作为NOP
}
void delay_print(FILE *fp, int t)
{
#ifdef PRINT_DEFPRINT_FP(PRINT_BRGIN);PRINT_FP(PRINT_MAIN);PRINT_FP(PRINT_END);
#elsePRINT_FP(PRINT_BRGIN);PRINT_FP("unsigned char ");for (int i = 1; i <= ind; ++i)PRINT_FP(PRINT_INIT);for (int i = 1; i <= P[0]; ++i)PRINT_FP(PRINT_NOP);for (int i = 2; i <= ind; ++i)PRINT_FP("do{");if (ind >= 1)PRINT_FP("while(--i);");for (int i = 2; i <= ind; ++i)PRINT_FP(PRINT_WHILE);if (ind >= 1)PRINT_FP("\n");PRINT_FP("}\n");
#endif
}
bool delay_check(LL x)
{LL ans = CALL + RET + MOV * ind + P[0]; //CALL+RET+MOV+NOPbool iszero = false;for (int i = 1; i <= ind; ++i){ans += DJNZ_NOT_JUMP;if (!P[i] && iszero == false)++ans, iszero = true; //CLRint Pt = (P[i] + 255) % 256;if (Pt)ans += (Pt) * (key[i - 1] + DJNZ_JUMP);}bool re = x != ans;if (re){printf("delay_check Warning:ans = %lld,x = %lld\n", ans, x);printf("ind = %d,nop = %d\n", ind, P[0]);printf("P[1] = %d,P[2] = %d,P[3] = %d\n", P[1], P[2], P[3]);}return re;
}
3.delay.h
#ifndef _delay_H_
#define _delay_H_
#include <math.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
//----------------------------------------------------------------------//
//----------------------------------------------------------------------////生成文件名
#define FILE_NAME "Delay.txt"//延时函数的单位(优先级:us>ms>s)
#define us "us"
#define ms "ms"
#define s "s"//开启打印宏函数模式
// #define PRINT_DEF
//开启检查模式
//#define CHECK
//指令的机器周期 默认NOP=1
#define DJNZ_NOT_JUMP 2
#define DJNZ_JUMP 3
#define CALL 3
#define RET 3
#define MOV 1
#define CLR 1//----------------------------------------------------------------------//
//----------------------------------------------------------------------////时钟频率(MHZ)
extern double CPU;
//机器周期/时钟周期
extern int SPEED;#if defined(us)
#define DS us
#elif defined(ms)
#define DS ms
#elif defined(s)
#define DS s
#endiftypedef long long int LL;#define PRINT_FP(x) fprintf(fp, x)
#ifdef PRINT_DEF
#define PRINT_BRGIN "#ifdef _DELAY_%d" DS "_\n", t
#define PRINT_MAIN "_DELAY_fun_(%d, %s, %d, %d, %d, %d, %d, %d)\n", PRINT_ARGV
#define PRINT_ARGV t, DS, ind, P[0], P[1], P[2], P[3], P[4]
#define PRINT_END "#endif\n"
#else
#define PRINT_BRGIN "void Delay%d" DS "(void)\n{\n\t", t
#define PRINT_INIT "%c=%d%s", 'i' + i - 1, P[i], (i == ind) ? (";\n\t") : (",")
#define PRINT_NOP "_nop_()%s", (i == P[0]) ? (";\n\t") : (",")
#define PRINT_WHILE "}while(--%c);", 'i' + i - 1
#endifvoid delay_call(FILE *, int); //调用&验证
#define delay(x) delay_call(fp, x)#endif
4.说明
至少应当是 C99 标准编译, delay.h 文件里前部分的宏可修改:FILE_NAME 输出的文件名;us/ms/s 时间单位,定义有优先级,比如开着 us 就无视 ms 和 s ;打印宏函数,输出宏函数,可以配合宏(下面给出)减少延时函数文件大小;检查模式,可以测试输出的函数是否错误,改程序用,当前已经修正无误;指令的机器周期,修改为当前 51单片机 正确的指令周期。
展开宏(用于配合宏输出模式)
如:CPU=24,SPEED=1,us单位下输出 10,000,000 us = 10 s 延时宏函数
#ifdef _DELAY_10000000us_
_DELAY_fun_(10000000, us, 4, 0, 31, 134, 194, 5)
#endif
展开后:
void Delay10000000us(void)
{unsigned char i = 31, j = 134, k = 194, l = 5;;do{do{do{while (--i)continue;} while (--j);} while (--k);} while (--l);
}
#endif
和直接输出函数是一样的:
void Delay10000000us(void)
{unsigned char i=31,j=134,k=194,l=5;do{do{do{while(--i);}while(--j);}while(--k);}while(--l);
}
附:对应宏展开
typedef unsigned char uchar;
#define _DELAY_fun_(sec, mode, ind, nop, x, y, z, p) \void Delay##sec##mode##(void) \{ \_DELAY_##ind##_(x, y, z, p); \_DELAY_NOP##nop##_; \_DELAY_WHILE##ind##_; \}
#define _DELAY_NOP0_
#define _DELAY_NOP1_ _nop_()
#define _DELAY_NOP2_ _nop_(), _nop_()
#define _DELAY_NOP3_ _nop_(), _nop_(), _nop_()
#define _DELAY_NOP4_ _nop_(), _nop_(), _nop_(), _nop_()
#define _DELAY_NOP5_ _nop_(), _nop_(), _nop_(), _nop_(), _nop_()
#define _DELAY_NOP6_ _nop_(), _nop_(), _nop_(), _nop_(), _nop_(), _nop_()
#define _DELAY_0_(x, y, z, p)
#define _DELAY_1_(x, y, z, p) uchar i = x
#define _DELAY_2_(x, y, z, p) uchar i = x, j = y
#define _DELAY_3_(x, y, z, p) uchar i = x, j = y, k = z
#define _DELAY_4_(x, y, z, p) uchar i = x, j = y, k = z, l = p
#define _DELAY_WHILE0_
#define _DELAY_WHILE1_ while (--i)
#define _DELAY_WHILE2_ \do \{ \while (--i) \continue; \} while (--j)
#define _DELAY_WHILE3_ \do \{ \do \{ \while (--i) \continue; \} while (--j); \} while (--k)
#define _DELAY_WHILE4_ \do \{ \do \{ \do \{ \while (--i) \continue; \} while (--j); \} while (--k); \} while (--l)
四、代码下载——Github
下载地址:https://github.com/MapleBelous/51-Delay-On-Keil5-
END
51单片机:生成精准的软件延时函数——以STC8演示相关推荐
- 51单片机的几种精确延时
实现延时通常有两种方法:一种是硬件延时,要用到定时器/计数器,这种方法可以提高CPU的工作效率,也能做到精确延时:另一种是软件延时,这种方法主要采用循环体进行. 今天主要介绍软件延时,关于硬件延时,之 ...
- 51单片机生成二维码
最近搞了个单片机生成二维码,步骤如下 1.下载QRCode生成的驱动源代码,这个驱动是c语言编写的可以移植到各种c语言写的工程上去,下面附上下载链接: https://download.csdn.ne ...
- 51单片机(STC)串口无阻塞发送函数
目录 一.简介 1.1.开发环境 1.2.功能描述 二.串口程序 2.1.串口配置 2.2.变量定义 2.3.中断函数 2.4.发送函数 一.简介 1.1.开发环境 KeilC51,单片机型号STC1 ...
- 51单片机生成C语言矩形波,基于51单片机产生占空比和频率可调的方波信号发生器(附全部代码)...
本帖最后由 suqianfu 于 2020-4-11 22:29 编辑 大佬,我添加了一点注释,不知道理解得对不对 #include ...
- 基于51单片机STC89C52RC的直流电机软件PWM控制的基本原理
电机驱动芯片L293D介绍: 在这里直流电机的控制采用L293D芯片.L293D是一款单片集成的高电压.高电流.4通道电机驱动,设计用于连接标准DTL或TTL逻辑电平,驱动电感负载(诸如继电线圈.DC ...
- 51单片机汇编学习笔记4——子函数
这一小节讲一下子函数的编写格式和调用. 子函数的调用 先讲一下子函数的格式 以之前讲到的延时函数为例 :延时函数 DELAYS :MOV R1,#0FFH ;往R1寄存器中放入一个数(立即寻址)0ff ...
- 单片机定时器精准定时_通过51单片机定时器/计数器实现精确延时
MCS-51单片机内部共有两个16位可编程定时器,计数器,即TO.Tl.既有定时功能,又有计数的功能.每个定时器都是由两个8位的特殊功能寄存器THi和TLi组成(i=0.1).TMOD是TO和Tl的工 ...
- 延时1us程序12mhz晶振c语言,51单片机12M晶振的延时程序
这是本人慢慢调出来的参数,有误差是必须的,除非用汇编才会精确,后续我会更新修改,尽量精确. 调试环境:Keil V4.02本文引用地址:http://www.eepw.com.cn/article/2 ...
- 51单片机生成C语言矩形波,单片机产生方波、锯齿波、三角波程序
单片机 产生方波.锯齿波.三角波程序 #include#define uchar unsigned char #define uint unsigned int unsigned char x=0,m ...
最新文章
- 说出一些数据库优化方面的经验?
- 【转】JAVA 并发性和多线程 -- 读感 (二 线程间通讯,共享内存的机制)
- android studio编译提示错误:android Error:(21, 19) 错误: 程序包R不存在
- 从一个小故事聊聊字符编码那些事
- python多进程加快for循环_python多进程 通过for循环 join 的问题
- Windows Mobile系列手机操作系统
- 漫画:什么是动态规划?(整合版)
- 苹果高通虽已和解 但5G iPhone最快仍要明年才能推出
- MediaMuxer的使用
- 概率图模型(05): 揭示局部概率模型, 稀疏化网络表示(Structured-CPDs)
- 【免费域名】freenom免费申请域名步骤
- 动动同步微信无法连接服务器,动动运动,动动计步器加到微信可是不能连接到微信运动...
- 开启加盟模式,喜茶能否借此越过山丘?
- 今日头条新闻采集爬虫分享
- 摄像头之自动驾驶中的应用
- 1688商品详情SKU
- 句子深度假说——冯志伟
- STM32输入捕获原理与配置
- gridmanager使用于本地数据,使用function来模拟返回后端数据。
- 华硕K42JC安装显卡驱动后进不了系统解决方法