51单片机:生成精准的软件延时函数——以STC8演示

目录

一、延时函数的基本结构

二、计算延时函数的变量

三、C11 代码实现

1.main.c

2.delay.c

3.delay.h

4.说明

四、代码下载——Github


毕业设计涉及IOT的内容,目前什么也不会,只能从复习单片机开始。在用STC官方工具STC-ISP(V6.87B)生成软件延时函数时,发现它有两个错误:

1)一个是最多只能生成循环变量为3的延时函数,延时长达多秒时显然三个循环变量已经不足,给出的是错误的延时函数。

51单片机:生成精准的软件延时函数——以STC8演示_第1张图片

 

51单片机:生成精准的软件延时函数——以STC8演示_第2张图片

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 。

51单片机:生成精准的软件延时函数——以STC8演示_第3张图片

是时候自己打造一个针对 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);

51单片机:生成精准的软件延时函数——以STC8演示_第4张图片

采用这种结构作为循环部分,可以很轻松的通过改变循环变量调整循环耗费的机器周期了,因为循环部分执行的指令的只有 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嵌套循环
}

二、计算延时函数的变量

设定好延时函数的基本结构后,我们只需要设定两类值就可以精准的确定一个延时函数的延时时间。

  1. do-while 嵌套循环的若干个循环变量的初试值。
  2. 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) = 255
    P[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 CHECK
    fp = 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~10s
        delay(i);
#endif
    return 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;
#endif

    x = round(x * CPU / SPEED); //取最近周期数
    int pr = 0;
    while (delay_us(x - pr)) //免除有i/j/k等于0且nop无法补偿的情况
        ++pr;
    P[0] += pr; //用nop补偿
#ifdef CHECK
    delay_check(x);
#else
    delay_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&RET
    if (k1 < 0)
    {
        fprintf(stderr, "error:do not need function\n");
        exit(1); //少于编写延时函数最少的机器周期
    }
    //--------------------------------------------//
    for (int i = 0; i != Maxind; ++i)
        P[i] = 0; //初始化P
    ind = 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) = 255
        P[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_DEF
    PRINT_FP(PRINT_BRGIN);
    PRINT_FP(PRINT_MAIN);
    PRINT_FP(PRINT_END);
#else
    PRINT_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+NOP
    bool iszero = false;
    for (int i = 1; i <= ind; ++i)
    {
        ans += DJNZ_NOT_JUMP;
        if (!P[i] && iszero == false)
            ++ans, iszero = true; //CLR
        int 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 
#include 
#include 
#include 
//----------------------------------------------------------------------//
//----------------------------------------------------------------------//

//生成文件名
#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
#endif

typedef 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
#endif

void 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单片机)