单片机中的 _nop_() 延时以及其相关的基础扩展_nop指令,大厂面试必备

480 阅读5分钟

但是我们在单片机中编程,现在都是使用 C 语言,对于 C 语言本身来说,是没有空语句的。

但是我们在做51单片机的开发中,在库文件中提供了一个void _nop_(void);函数,这个函数声明一般在 intrins.h 头文件当中,我们只需要 #include <intrins.h> 就可以使用 _nop_(); 函数了。

比如:
在这里插入图片描述

如果我们在有的开发环境中找不到<intrins.h>头文件,比如 STM32 开发要使用 nop 函数,直接在程序中写成如下即可:

\_\_asm\_\_("nop");

比如:

在这里插入图片描述

我们已经知道了 nop 是空语句,什么都不做,但是在这里我们还是得明确的知道 一个 _nop_()表示空循环一个机器指令的时间。

2.2 nop 函数消耗的时间

那么在我们的单片机中,一个 nop 的时间是多少呢?、

上面说到,一个 nop 表示一个机器周期,那么一个机器周期是多少?

机器周期当然与主频有关,在单片机中指的就是晶振的频率。

首先基本的东西还是要知道的 一个机器周期包含12个晶振周期。 所以我们可以通过下面的计算得知 nop 函数消耗的时间:

假设单片机 12M 晶振,晶振周期1/12微秒,一个机器周期包含12个晶振周期,所以12M晶振时机器周期 = 12x(1/12)us = 1us 。
.
所以12M 晶振中一个 nop 表示延时1us;
6M 晶振中延时2us,24M 晶振中延时 0.5 us

至于其他的晶振频率,我们可以按照上面的计算代入即可。

对于 _nop_() 函数 其实在我以前的文章 BH1750 传感器实战教学 —— 驱动移植篇 中有过说明:

在这里插入图片描述

三、用 _nop_() 延时的注意事项

到此,我们已经可以知道在我们的程序中,一个 nop 函数执行所需要时间,我们可以利用多个 _nop_() 函数来实现一些 us 级别的延时。

比如我以前一些帖子里面提到的在 51 上面的 I2C 通讯:

在这里插入图片描述

在上图中,就是一个简单的 I2C 其实信号的实现方式, 在上图中,有说明 多几个 nop 少几个 nop 无所谓,实际上现在看来是有问题的,这让我付出了代价,这一点我后面会在写某个传感器测试博文的时候会提到。

3.1 函数调用对延时的影响

那么本文这里要说明的是一些使用时候的问题,依然是我以前文中提到的,在 STM32 HAL 库中没有 us 延时,所以我一直用的是:

void delay\_us(uint32\_t Delay)
{
	uint32\_t cnt = Delay \* 8;   // 32Mhz ,其他频率其他倍数
	uint32\_t i = 0;
	for(i = 0; i < cnt; i++)\_\_NOP();
}

于是乎,对于本次使用的 16MHZ 晶振的 51 芯片,我改成了如下:

void delay\_us(uint32 Delay)
{
  uint32 cnt = Delay \* 4;   // 32Mhz 8 ,其他频率其他倍数 16Mhz慢一点 4
  uint32 i = 0;
  for(i = 0; i < cnt; i++)\_nop\_();
}

然后自然的把上面的 I2C_Start 改成如下:

void I2C\_Start1(void)
{
   sda\_high();
   delay\_us(5);
   scl\_high();
   delay\_us(10);
   sda\_low();
   delay\_us(10);
   scl\_low(); //使SCL置低,准备发送或者接受数据
   delay\_us(10);
}

反正改完以后传感器通讯是不正确的,于是乎最后上了示波器,惊讶的发现,在我使用的 51 上面采用上面的方式的波形图如下(注意看波形的时间):

在这里插入图片描述

是不是很意外,时间周期居然可以达到 ms 级别,就是使用一个一个循环调用 nop 的函数……,我一个 I2C 传感器的初始化工作,居然持续了好几秒时间……

而在 STM32 平台下面,我观察到的波形图如下(us级别算是正常的):

在这里插入图片描述

虽然知道调用函数会占用时间,但是上面的情况也太离谱了点,即便我最后把循环里面的 *4 都直接删除,波形周期还是 ms 级别。

这…… 真的是有点太离谱了,一个简单的 nop 延时函数在实际上会有这么久的延时……

反正最后我还是去掉了函数,采用直接使用很多个 nop 函数直接写的方式,如下图的上面部分:

在这里插入图片描述

实际上,除了调用函数,在函数中的使用什么语法也决定了这个函数执行的时间长短,这个问题对于我们现在大家常用的 ARM 内核来说,可能都不太容易发现,或者影响没那么大,但是对于老一点的 51 内核,影响就大了,但是大到上面这种程度,也是我没想到的。

3.2 调用函数中的语句对延时的影响

那说到除了调用函数,函数中的语句是如何影响时间的呢,这因为在C51编译器中,对不同的循环方法,采用不同的指令来完成的,对于不同的指令,单片机执行所需要的的时间也是不一样的。

3.2.1 单片机执行一条指令所需要的时间

完成一条指令需要的时间,也就是指令周期。

指令周期就是 单片机 取出一条指令并执行这条指令所需要的时间。指令周期,是从取指令、分析指令到执行完所需的全部时间。

指令周期一般由若干个机器周期组成(我们上面讲过,一个 _nop_() 就是一个机器周期),他是以机器周期为单位的!!!

其实通过我们前文的介绍,我们已经知道如何算一个 单片机的 机器周期(一个 nop 的时间,他是由 12 个时钟周期组成的),我们只需要知道这条指令是由几个机器周期组成的就可以,这一点会在单片机的使用手册中有说明,比如下图:

在这里插入图片描述

上图中上面一些指令需要 12 个时钟周期,就是一个机器周期,最后一个需要 2个 机器周期。

大家都能看到其实后面有一个 6T 模式的说明,很容易理解,就是1机器周期等于 6 个时钟周期的模式,这样会使得单片机执行效率提升 2 倍 ,现代单片机有许多都有这种高效率模式。


了解完了指令周期,那我们是不是很容易的就明白了,在函数中为什么不同的语句会对延时产生不同影响了。

这里呢,我就不对不同的语句进行单独的分析了,大家有时间可以自己生成汇编文件自己研究,这里我就从网上截取了部分说明:

在选择C51中循环语句时,要注意以下几个问题
.
第一、定义的 C51 中循环变量,尽量采用无符号字符型变量。
.
第二、在 for 循环语句中,尽量采用变量减减来做循环。
.
第三、在do…while,while语句中,循环体内变量也采用减减方法。

我们要知道的是,上面的做法都是为了减少额外的时间开销,使得我们想要的延时时间更加准确。

四、指令周期、机器周期、时钟周期

在文章上面部分由反复的提到过几个概念:指令周期,机器周期,时钟周期。

为了防止有的小伙伴还是迷迷糊糊的,这里来简单的总结一下(以 8051 单片机为例):

时钟周期 = 1/ 晶振频率
单片机的心跳,基本时间单位
.
机器周期 = 时钟周期 * 12
单片机的基本操作周期,一个机器周期,单片机完成一项基本操作,如取指令,读/写存储器
.
指令周期 :
CPU 执行一条指令所需要的时间, 以机器周期为单位。
指令周期所需要的的机器周期,可以通过单片机使用手册中的指令表查询得到。

当然,其实与上面这些概念相关的还有一个状态周期,他等于 2个 时钟周期,这里也提一下。

结语

本文通过一个简单的 _nop_() 函数,我们探讨了在单片机中实现 us 延时的一些时间问题以及注意事项,进而引出了一些时间周期的基本概念,相信能让大家在日后使用到的时候能够更好的理解与计算自己所需要的延时时间。

img img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上物联网嵌入式知识点,真正体系化!

由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、电子书籍、讲解视频,并且后续会持续更新

如果你需要这些资料,可以戳这里获取