cover_image

程序中延时功能实现原理:软延时

钟胜平 客舟读书听雨眠
2022年07月06日 06:24

硬件 API 长什么样儿?一文中我们使用了延时函数来保证硬件 API 要求遵守的时序协议。示例:

void write_zero(){    DS18B20_DQ = 0;     delay_60us();    DS18B20_DQ = 1;        // 总线恢复时间    delay_2us();}

代码中的 delay_2us() 和 delay_60us() 都是延时函数并且它们属于软延时,用软件模拟的延时。与之相对的是硬延时,即用定时器等硬件来更精确地设置延时。

软延时的原理就是让 CPU 执行一些指令,并且执行这些指令所用的时间等于想要延时的时间,同时执行这些指令不应该影响程序的结果。例如,可以使用空循环:

for (i = 0; i < 1000; i++) {  // noop}

现在,问题转变为如何计算指令执行时间,比如执行上例的循环到底需要多长时间?要回答这个问题需要弄清楚 CPU 时序,明白 CPU 是怎么执行指令的。下文使用 8051 单片机举例,因为它的实现特别简单。


CPU 振荡周期(时钟周期)

驱动 CPU 不停地自动执行下一条指令的关键部件是“时钟”。时钟不间断地产生高低电平的脉冲信号来驱动电路。如下图所示:

图片


晶体振荡器(简称晶振)使用了特殊的石英晶体元件,它在特定条件下能够产生稳定的周期性振荡信号,经过特殊处理后可以用作时钟。晶振可以长下面这个样子:

图片


时钟频率是时钟的一项关键指标。它表示 1 秒内时钟脉冲周期性变化的次数,单位为赫兹 Hz。时钟频率反映了脉冲信号周期性变化的快慢,频率越高振荡速度越快。它也在一定程序上决定了 CPU 速度的快慢。

拿上图中的晶振举例,上面的 12.000 表示频率为 12MHz,即 1 秒钟完成 12, 000, 000 个振荡周期。因此,1 个振荡周期用时为 (1秒 / 12, 000, 000),即 1/12 微秒 (μs)。

图片

CPU 机器周期(machine cycle)

机器周期用于衡量计算机指令的执行时间。例如,执行指令 A 需要 1 个机器周期、执行指令 B 需要 2 个机器周期等等。在 51 单片机中,机器周期与振荡周期的关系为(假设使用了 12MHz 晶振):

图片

接下来,我们需要知道每条指令的执行需要多少个机器周期。方法是查询芯片厂商的技术文档,因为这是由指令的详细设计和实现所决定的。


示例1,NOP 空操作指令,需要 1 机器周期,即 1 微秒:

图片


示例2,DIV 除法指令,需要 4 机器周期,即 4 微秒:

图片


开始编程

延时 1 微秒:

// 注意,这是一个特殊的库函数!它不表示函数调用!!// 编译器会将其编译为一个 NOP 汇编指令_nop_();


延时 2 微秒

// 注意,这是一个特殊的库函数!它不表示函数调用!!// 编译器会将其编译为一个 NOP 汇编指令_nop_();_nop_();


延时 4 微秒:

void delay_4us(){  // no-op}
delay_4us();

解释:这里我们定义了一个空函数 delay_4us(),然后直接调用这个空函数就相当于延时 4 微秒。因为,调用函数使用 LCALL 汇编指令,需要 2 个机器周期;从函数返回使用 RET 汇编指令,需要 2 个机器周期;一共 4 个机器周期,即 4 微秒。


延时 5 微秒:

void delay_5us(){  _nop_();}
delay_5us();


延时 100 微秒:

void delay_100us(){  unsigned char i;
_nop_(); i = 47; while (--i);}
delay_100us();

解释:此例又增加使用了 MOV 和 减 1 非零转移指令。如果延时较长,则需要使用某种形式的循环,不可能拷贝 N 个 NOP 指令的。


总结

指令和周期还有很多技术细节本文并没有描述。软延时不总是可靠的,原因之一是因为中断系统的存在。在软延时空跑指令的时候,有可能中断被触发,这时 CPU 转而去处理中断请求;等中断处理完毕,CPU 才会回到之前的延时程序,此刻的延时时间早就过去了。因此,更准确的延时/定时功能需要使用定时器中断功能。

计算机科学 · 目录
上一篇硬件 API 长什么样儿?下一篇程序中延时功能实现原理:定时器
继续滑动看下一个
客舟读书听雨眠
向上滑动看下一个