最近在继续做项目,因为在某款8051核心芯片上使用了I2C通信,需要_nop_()函数来实现us的delay,所以写一篇文章来介绍一系列基本概念引起的正好合适通过 _nop_() 函数。
本文内容:nop的理解,单片机中延迟nop功能的一些注意事项,单片机中的基本指令周期、机器周期等一些基本概念的讲解。
本文内容:
2. 微控制器中的`_nop_()`函数
3. 使用`_nop_()`延迟时的注意事项
4.指令周期、机器周期、时钟周期
结论
1.NOP指令
_nop_() 函数生成 NOP 指令。 首先简单介绍一下NOP指令,基本介绍一下流程:
NOP 是编程语言中常用的指令。 它的全称是No Operation,意思是没有操作指令。
NOP是汇编语言中的伪指令。 通过一系列NOP编程语句,任何可以被程序访问的寄存器都不会改变。
NOP指令的作用:
我们知道指令和数据对齐可以有效提高程序性能。 使用NOP指令可以使指令字对齐,从而提高效率。 例如,一条指令占用3个字节,增加一条NOP指令使该指令4字节对齐。
通过NOP指令产生一定的延迟,该延迟与CPU的频率有关,适合一些低频单片机的情况。
当计算机正在进行输入或输出过程中时,可以使用NOP指令等待计算机缓冲区被清除,总线恢复正常。 其实也可以算是一种拖延。
2.单片机中的_nop_()函数 1.C语言中的NOP
如果我们使用汇编语言,我们可以直接使用NOP指令。 写一个nop就可以了,比如下面的例子:
.text ; 代码段开始
.syntax unified ;
start:
mov r0, #0x55 ; 将0x55存储在寄存器R0中
nop ; 插入NOP指令
mov r1, #0xAA ; 将0xAA存储在寄存器R1中
add r2, r0, r1 ; 将R0和R1相加并将结果存储在R2中
但是当我们在微控制器中编程时,我们现在使用C语言。 对于C语言本身来说,不存在空语句。
但是我们在开发51单片机的时候,提供了一个void _nop_(void); 库文件中的函数。 该函数声明一般在intrinsic.h头文件中。 我们只需要#include 即可使用_nop_(); 功能。
例如:
我们已经知道nop是一条空语句,什么也不做,但是这里我们还是要清楚地知道,_nop_()代表的是一个空循环中一条机器指令的时间。
2、nop函数消耗的时间
那么在我们的微控制器中,nop 有多长? ,
上面说了,一个nop代表一个机器周期,那么一个机器周期是多少呢?
机器周期当然与主频率相关,在微控制器中主频率指的是晶振的频率。
首先,您需要了解的基本知识是一个机器周期包含 12 个晶振周期。 所以我们可以通过下面的计算知道nop函数消耗的时间:
假设单片机有12M晶振,晶振周期为1/12微秒。 一个机器周期包含12个晶振周期,因此12M晶振的机器周期=12x(1/12)us=1us。 。
因此12M晶振中1个nop代表1us的延迟;
6M晶振的延迟为2us,24M晶振的延迟为0.5us。
对于其他晶振频率,我们可以按照上面的计算进行代换。
_nop_()函数其实在我之前的文章《BH1750传感器实战教学-驱动移植》中已经有讲解:
3.使用_nop_()延迟时需要注意的事项
至此,我们已经可以知道程序中执行一个nop函数需要的时间了。 我们可以使用多个 _nop_() 函数来实现一些 us 级的延迟。
比如我之前的一些帖子中提到的51上的I2C通信:
上图中,是一个简单的I2C。 事实上,信号已经实现了。 上图中说明了nop多几个或者少几个nop都没有关系。 其实现在看来是有问题的,这让我付出了代价。 稍后我会在撰写有关传感器测试的博客文章时提到这一点。
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核心,影响会更大,但达到上述程度,是我没想到的。
2、调用函数中的语句对延迟的影响
除了调用函数之外,函数中的语句对时间有什么影响呢? 这是因为在C51编译器中,使用不同的指令来完成不同的循环方法。 对于不同的指令,单片机需要执行的时间也不同。
微控制器执行一条指令所需的时间:
完成一条指令所需的时间称为指令周期。
指令周期是微控制器获取指令并执行它所需的时间。 指令周期是指从取指令、分析指令到完成执行所需的整个时间。
指令周期一般由几个机器周期组成(上面我们提到,一个_nop_()就是一个机器周期),并且是以机器周期来衡量的! ! !
其实通过我们前面的介绍,我们已经知道了如何计算单片机的机器周期(一个nop的时间由12个时钟周期组成)。 我们只需要知道这条指令是由多少个机器周期组成的。 是的,这个在单片机的用户手册中会有解释,如下图:
上图中有些指令需要12个时钟周期,也就是1个机器周期,最后一条需要2个机器周期。
大家可以看到后面其实还有6T模式的说明。 这很容易理解。 这是一种1个机器周期等于6个时钟周期的模式。 这将使微控制器的执行效率提高2倍。 许多现代微控制器都具有如此高的效率。 模型。
了解了指令周期后,我们就不容易理解为什么函数中的不同语句对延迟的影响不同。
在这里,我不会对不同的说法进行单独的分析。 如果有时间,可以生成自己的汇编文件,自己研究一下。 这里我从网上截取了一些说明:
在C51中选择循环语句时,应注意以下问题。
首先,在C51中定义循环变量时,尽量使用无符号字符变量。 。
其次,在for循环语句中,尽量使用变量减法来进行循环。 。
第三,在do…while和while语句中,循环体内的变量也采用减法的方法。
我们需要知道的是,以上方法都是为了减少额外的时间开销,让我们想要的延迟时间更加准确。
4.指令周期、机器周期、时钟周期
文章上半部分多次提到了几个概念:指令周期、机器周期、时钟周期。
为了防止一些朋友混淆,这里简单总结一下(以8051单片机为例):
时钟周期 = 1/晶体频率
单片机的心跳,基本时间单位。
机器周期=时钟周期*12
单片机的基本操作周期,一个机器周期,单片机完成一次基本操作,如取指令、读/写存储器等。
指令周期:
CPU 执行一条指令所花费的时间,以机器周期为单位。
指令周期所需的机器周期可以通过查询单片机用户手册中的指令表得到。
当然,其实还有一个与上述概念相关的状态周期,相当于2个时钟周期,这里也提到了。
5. 结论
本文通过一个简单的_nop_()函数来讨论在微控制器中实现延迟的一些时间问题和注意事项,然后介绍时间周期的一些基本概念。 相信以后大家都能用得上。 更好地理解和计算您需要的延迟时间。
结尾