单片机因具有体积小、功能强、成本低以及便于实现分布式控制而有非常广泛的应用领域。单片机开发者在编制各种应用程序时经常会遇到实现精确延时的问题,比如按键去抖、数据传输等操作都要在程序中插入一段或几段延时,时间从几十微秒到几秒。有时还要求有很高的精度,如使用单总线芯片DS18B20时,允许误差范围在十几微秒以内,否则,芯片无法工作。为此我特意的将如何在C51程序中精确延时进行了一个小结。
用51汇编语言写程序时,这种问题很容易得到解决,而目前开发嵌入式系统软件的主流工具为C语言,因此很有必要了解用C51写延时程序时需要的一些技巧。
实现延时通常有两种方法:
一种是硬件延时,要用到定时器/计数器,这种方法可以提高CPU的工作效率,也能做到精确延时;
一种是软件延时,这种方法主要采用循环体进行。
1 使用定时器/计数器实现精确延时
单片机系统一般常选用11.059 2 MHz、12 MHz或6 MHz晶振。第一种更容易产生各种标准的波特率,后两种的一个机器周期分别为1 μs和2 μs,便于精确延时。假设使用频率为12 MHz的晶振。最长的延时时间可达2^16=65 536 μs。若定时器工作在方式2,则可实现极短时间的精确延时;如使用其他定时方式,则要考虑重装定时初值的时间(重装定时器初值占用2个机器周期)。
在实际应用中,定时常采用中断方式,如进行适当的循环可实现几秒甚至更长时间的延时。使用定时器/计数器延时从程序的执行效率和稳定性两方面考虑都是最佳的方案。但应该注意,C51编写的中断服务程序编译后会自动加上PUSH ACC、PUSH PSW、POP PSW和POP ACC语句,执行时占用了4个机器周期;如程序中还有计数值加1语句,则又会占用1个机器周期。这些语句所消耗的时间在计算定时初值时要考虑进去,从初值中减去以达到最小误差的目的。
2 软件延时与时间计算
在很多情况下,定时器/计数器经常被用作其他用途,这时候就只能用软件方法延时。下面介绍几种软件延时的方法。
2.1 短暂延时
可以在C文件中通过使用带_NOP_( )语句的函数实现,定义一系列不同的延时函数,如Delay10us( )、Delay25us( )、Delay40us( )等存放在一个自定义的C文件中,需要时在主程序中直接调用。如延时10 μs的延时函数可编写如下:
void Delay_10us()
{
_NOP();
_NOP();
_NOP();
_NOP();
_NOP();
_NOP();
}
Delay_10us( )函数中共用了6个_NOP_( )语句,每个语句执行时间为1 μs。主函数调用Delay_10us( )时,先执行一个LCALL指令(2 μs),然后执行6个_NOP_( )语句(6 μs),最后执行了一个RET指令(2 μs),所以执行上述函数时共需要10 μs。
可以把这一函数当作基本延时函数,在其他函数中调用,即嵌套调用延时函数,以实现较长时间的延时;但需要注意,如在Delay40us( )中直接调用4次Delay10us( )函数,得到的延时时间将是42 μs,而不是40 μs。这是因为执行Delay40us( )时,先执行了一次LCALL指令(2 μs),然后开始执行第一个Delay10us( ),执行完最后一个Delay10us( )时,直接返回到主程序。依此类推,如果是两层嵌套调用,如在Delay80us( )中两次调用Delay40us( ),则也要先执行一次LCALL指令(2 μs),然后执行两次Delay40us( )函数(84 μs),所以,实际延时时间为86 μs。简言之,只有最内层的函数执行RET指令。该指令直接返回到上级函数或主函数。如在Delay80μs( )中直接调用8次Delay10us( ),此时的延时时间为82 μs。通过修改基本延时函数和适当的组合调用,上述方法可以实现不同时间的延时。
对于_NOP()函数,相信有不少人会感到疑惑,这里我就详细的介绍一下_NOP();函数:
_NOP();函数是用来产生空指令来进行延时的,在汇编语言中写几个nop指令就可以达到延时的效果。
注意:
1、 调用库函数是一定要包含头文件#include<intrins.h>,在该库中声明了void _NOP(void);
2、 这个函数相当汇编NOP指令,延时几微秒。NOP指令为单周期指令,可由晶振频率算出延时时间,对于12M晶振,延时1uS。
关于C51的延时函数要注意:
求在大于10us,采用C51中的循环语句来实现。
在选择C51中循环语句时,要注意以下几个问题
第一、定义的C51中循环变量,尽量采用无符号字符型变量。
第二、在FOR循环语句中,尽量采用变量减减来做循环。
第三、在do…while,while语句中,循环体内变量也采用减减方法。
这因为在C51编译器中,对不同的循环方法,采用不同的指令来完成的。
下面举例说明:
unsigned char i;
for(i=0;i<255;i++);
unsigned char i;
for(i=255;i>0;i--);
其中,第二个循环语句C51编译后,就用DJNZ指令来完成,相当于如下指令:
MOV 09H,#0FFH
LOOP: DJNZ 09H,LOOP
指令相当简洁,也很好计算精确的延时时间。
既然能用高级语言进行延时,当然也能用最基本的汇编语言进行相关的延时,下面我们来简单的了解下(注:对于汇编语言我也只略懂一点,这是我在网上相关的资料学习到的):
在C51中通过预处理指令#pragma asm和#pragma endasm可以嵌套汇编语言语句。用户编写的汇编语言紧跟在#pragma asm之后,在#pragma endasm之前结束。
如: #pragma asm
…
汇编语言程序段
…
#pragma endasm
延时函数可设置入口参数,可将参数定义为unsigned char、int或long型。根据参数与返回值的传递规则,这时参数和函数返回值位于R7、R7R6、R7R6R5中。在应用时应注意以下几点:
◆ #pragma asm、#pragma endasm不允许嵌套使用;
◆ 在程序的开头应加上预处理指令#pragma asm,在该指令之前只能有注释或其他预处理指令;
◆ 当使用asm语句时,编译系统并不输出目标模块,而只输出汇编源文件;
◆ asm只能用小写字母,如果把asm写成大写,编译系统就把它作为普通变量;
◆ #pragma asm、#pragma endasm和 asm只能在函数内使用。
2.3 使用示波器确定延时时间
熟悉硬件的开发人员,也可以利用示波器来测定延时程序执行时间。方法如下:编写一个实现延时的函数,在该函数的开始置某个I/O口线如P1.0为高电平,在函数的最后清P1.0为低电平。在主程序中循环调用该延时函数,通过示波器测量P1.0引脚上的高电平时间即可确定延时函数的执行时间。(有条件的哥们可以试试)
2.4 使用Keil_C中的性能分析器计算延时时间:
这里我就详细介绍下,这是很实用的方法:
我们先在Keil_C中敲入如下代码:
#include<reg52.h>
#define uint unsigned int
sbit led=P1^0;
main()
{
uint i,j,x,y,k,n,m;
while(1)
{
led=0;
for(i=1000;i>0;i--)//延时1
for(j=110;j>0;j--);
led=1;
for(x=0;x<500;x++)//延时2
for(y=0;y<130;y++);
led=0;
for(n=500;n>0;n--)//延时3
for(m=114;m>0;m--);
led=1;
k=90;//延时4
while(k>0)k--;
led=0;
}
}
下面将介绍如何建立Keil工程,并分析延时时间的精确计算方法,利用keil可以比较方便、精确的计算程序延时时间。
1. 建立keil工程。
启动keil,选择“Project”à“New uVision Project”à输入工程名称,确定,弹出下面对话框,选择Atmel:
接着选择:AT89C52
选择好芯片之后会弹出下面的对话框,选择“否”:
点击“File”à “New”,新建一个Text,用以输入程序。程序输入以后保存,保存名称需要和工程名称一致,如果使用C语言则以.c作为保存格式,使用汇编语言保存格式为.asm。保存之后如图:
下一步就是把刚才保存的.c程序导入到工程中。选中上图中左上角的“Source Group 1”,单击右键,选择“Add Files to Group 'Source Group 1'..”,在弹出的对话框选择刚才保存的.c文件,Add,即完成导入。
下面这一步设置对于使用keil精确计算延时时间很关键。选中下图左上角的Target 1:
单击右键,选择“Options for Target 'Target 1'..”,弹出下面对话框,把Xtal(Mhz)的24.0修改成当前使用的晶振频率,这里改为11.0592Mhz。
如果还要生成.hex文件用于下载到芯片上,可以选择“Output”,钩选“Creat HEX File”选项,如下:
这样就建立好了一个keil工程。通过程序调试,“Save”,“Build”,生成.hex文件,下载到芯片就可以直接使用了。
1. 接着讲第二个知识点,就是如何精确计算延时的时间。
我们选择开头给出的程序为例,延时1的程序如下:
for(i=1000;i>0;i--)//延时1
for(j=110;j>0;j--);
要精确计算它的延时时间,可以通过设置断点来实现。断点设置如下图:
在延时1的开头和下一句语句的开头分别设置断点A1和A2,然后全速运行,运行到A1处,程序停止,记录这时的运行时间t1,继续全速运行,遇到断点A2,程序停止,记录此刻的时间t2。那么延时1的延时时间就是t=t2-t1。
下面是具体步骤:
(1) 设置断点。如上图所示,在程序开头的数字处双击左键,就会出现一个红色的点,这就是断点。如果要消去断点,同样可以双击断点。
(2) 进入调试模式。单击窗口上的调试按键快捷图标:
即可进入调试模式。初次进入调试模式的界面如下:
首先介绍一下几个重要按钮。如下图所示:
红色数字1上面的图标:将程序复位到主函数的最开开始处,准备重新运行程序;
红色数字2上面的图标:全速运行,运行程序时中间不停止,直到遇到断点;
红色数字3上面的图标:停止全速运行。
红色数字4上面的图标:进入子函数内部。
红色下划线上的sec就是程序从开始运行到当前停止处所用的时间。
(3) 先复位。即点击上图中红字1上面的图标。
(4) 全速运行,记录运行时间。即点击上图红字2上面的图标。遇到09处的第一个断点,系统会自动停止运行,停在第一个断点处。此时右边记录的时间sec就是程序从开始运行到当前断点处所经历的时间为t1=423.18us。如下图所示:
(5) 继续全速运行,第二次记录运行时间。遇到11处的第二个断点,系统停止运行,此时已运行时间为t2=966140.41us。如下图所示:
(6) 计算延时时间t。从上面得到的数据可以计算出时间
t= t2-t1= 966140.41us- 423.18us= 965717.23us= 965.71723ms。
通过上面6个步骤,就可以精确,方便地计算出延时程序的时间,对于实现精确延时,只需要调节参数,再稍加计算就OK。需要注意的是,在上图的调试模式下修改程序参数,是无法生效的。复位之后全速运行,显示的十间仍然是修改之前的参数在起作用。所以如果修改程序参数,需要到编辑模式下,重新下载,然后再进入调试模式,才可以计算精确时间。同时,在建立Keil工程的时候,一定要记得修改晶振的参数,这很关键,如果晶振不对,要实现相同的延时时间,程序参数的设置也就不一样。
可以使用同样的方法计算延时2,延时3,延时4的精确延时时间。它们的延时时间分别是:498040.37us、500220.27us、783.42us。大家可以自己练习。
最后还要指出一点的是:
上面使用的Keil版本是Keil uVision4,大家也可以使用Keil 3或者Keil 2来做,只是软件的界面,图标等有差别,但都可以实现相同的功能。
本文出自 “成长之路” 博客,转载请与作者联系!