一、51单片机汇编程序的延时函数
(一)指令周期、时钟周期
1.指令周期
指令周期:单片机的指令周期定义为执行一条指令所需的时间。它由多个时钟周期组成,每个时钟周期是由晶振提供的脉冲信号决定的。每个指令周期内,单片机依次执行一系列的操作,包括取指令、译码、执行指令、存储结果等。不同的指令可能需要不同数量的时钟周期来完成。指令周期的长度决定了单片机在单位时间内能够执行的指令数量,因此,指令周期越短,单片机的执行速度越快。
2.时钟周期
时钟周期:单片机的时钟周期定义为晶振提供的脉冲信号完成一个完整周期的时间。每个时钟周期的长度是由晶振的频率决定的,一般以纳秒为单位表示。时钟周期是单片机进行所有操作的基本计时单位,包括指令执行、数据传输、中断处理等。单片机通过统一的时钟信号来同步各个模块的操作,确保它们按预期的顺序和时间完成。时钟周期的长度直接影响单片机的工作速度,较短的时钟周期意味着单片机能够以更高的速度执行指令和处理数据。
(二)不同种类的延时函数
1.单重循环短暂延时
短暂的延时通过单重循环延时函数实现,下面举例一个单重循环的代码例子并对其解释。
DELAY_LOOP:
MOV R1, #250 ; 设置循环计数器的初始值为 250
LOOP_START:
DJNZ R1, LOOP_START ; 循环计数器递减,直到为0时跳出循环
RET ; 返回
延时计算:DJNZ的指令周期为两个机器周期,在12MHz的晶振单片机中,机器周期为1毫秒,即执行DJNZ指令需要2us,没执行一次循环计数器递减,即R1的值减1,不断重复直至R7减到0,指令结束。即LOOP_START语句执行250次,延时250×2us。
代码解释:
-
DELAY_LOOP:
:这是一个标签,用于标识延时函数的起始位置。 -
MOV R1, #250
:将立即数 250 赋值给寄存器 R1。这行代码设置循环计数器的初始值为 250。 -
LOOP_START:
:这是一个标签,用于标识循环的开始位置。 -
DJNZ R1, LOOP_START
:递减寄存器 R1 的值,并检查是否为零。如果 R1 不为零,则跳转回LOOP_START
标签处,继续执行循环。当 R1 减到零时,循环结束,代码将继续执行后面的指令。 -
RET
:表示函数结束并返回。在延时函数中,这会使程序返回到调用该延时函数的位置。
2.多重循环较长延时
(1)二重循环延时函数
DOUBLE_LOOP:
MOV R7, #10 ; 设置外层循环计数器的初始值为 10
OUTER_LOOP:
MOV R6, #5 ; 设置内层循环计数器的初始值为 5
INNER_LOOP:
; 在这里放置内层循环的操作
DJNZ R6, INNER_LOOP ; 内层循环计数器递减,当 R6 不为零时跳转回 INNER_LOOP 标签处
DJNZ R7, OUTER_LOOP ; 外层循环计数器递减,当 R7 不为零时跳转回 OUTER_LOOP 标签处
RET ; 返回
代码解释:
-
DOUBLE_LOOP:
:这是一个标签,用于标识嵌套循环的起始位置。 -
MOV R7, #10
:将立即数 10 赋值给外层循环计数器 R7。这行代码设置外层循环的初始值为 10。 -
OUTER_LOOP:
:这是一个标签,用于标识外层循环的开始位置。 -
MOV R6, #5
:将立即数 5 赋值给内层循环计数器 R6。这行代码设置内层循环的初始值为 5。 -
INNER_LOOP:
:这是一个标签,用于标识内层循环的开始位置。 -
在
INNER_LOOP
标签处,你可以放置内层循环需要执行的操作。可以在这个位置执行任意汇编指令。 -
DJNZ R6, INNER_LOOP
:内层循环计数器 R6 递减一次,然后检查是否为零。如果 R6 不为零,则跳转回INNER_LOOP
标签处继续执行内层循环。如果 R6 递减至零,则跳出内层循环,继续执行下面的指令。 -
DJNZ R7, OUTER_LOOP
:外层循环计数器 R7 递减一次,然后检查是否为零。如果 R7 不为零,则跳转回OUTER_LOOP
标签处继续执行外层循环。如果 R7 递减至零,则跳出外层循环,继续执行下面的指令。 -
RET
:表示函数结束并返回。在这段代码中,它将使程序返回到调用该嵌套循环的位置。
任务要求中给出一个周期性点亮LED灯代码中的延时函数,采用了双重循环的方式实现。
DELAY: MOV R7,#250
D1: MOV R6,#250
D2: DJNZ R6,D2 ;250*2=500μs
DJNZ R7,D1 ;250*500μs=0.125S
RET
延时时间计算:
- 第一句中MOV指令占一个机器周期,即MOV指令运行一个机器周期且全代码中无一语句跳转至第一句,则第一句代码只执行一次且为单周期。
- 第二句中由于第四局语句判断R7的值是否为0,不为0则跳转至第二句即第二句语句执行了250次,全部是从第四句跳转过来的,且为单周期。
- 第三句中跳转至本句单独运行250次后执行第四句,并且由于第四句跳转至第二句任要运行250次,因此第三句总共运行250×250=62500次,并且DJNZ指令占两个机器周期,因此第三局执行62500×2=125000次。
- 第四句通过顺序下来并跳转至第二句总共运行了250次,且DJNZ占2个机器周期,故第四句占用500个机器周期。
总计:1+250+125000+500=125751次,乘以机器周期1微妙,得到延时时长大致为0.125秒。
代码解释:
-
DELAY: MOV R7, #250
:将立即数 250 赋值给寄存器 R7。这行代码设置外层循环计数器的初始值为 250。 -
D1: MOV R6, #250
:将立即数 250 赋值给寄存器 R6。这行代码设置内层循环计数器的初始值为 250。 -
D2: DJNZ R6, D2
:内层循环计数器 R6 递减一次,并检查是否为零。如果 R6 不为零,则跳转回D2
标签处继续执行内层循环。这行代码实现了一个内层循环230时钟周期的延时。 -
DJNZ R7, D1
:外层循环计数器 R7 递减一次,并检查是否为零。如果 R7 不为零,则跳转回D1
标签处继续执行外层循环。这行代码实现了一个外层循环230 * 250 = 57500 时钟周期的延时。 -
RET
:表示函数结束并返回。在这段代码中,它将使程序返回到调用该延时函数的位置。
任务问题:若要使LED每隔1S亮灭进行周期性变化,上述双重循环还能否实现?程序如何修改?
回答:由于51单片机的每个寄存器存储的最大值为256个字节,因此二重循环无法达到延时1秒的效果,因此此处为了实现延时1秒的效果而采用三重循环。
DELAY:MOV R5,#25
D1: MOV R6,#100
D2: MOV R7,#200
DJNZ R7,$ ;200*2=400μs
DJNZ R6,D2 ;400*100=40000μs
DJNZ R5,D1 ;40000*25=1000000μs
RET
上述代码通过计算得到的延时时间为约为1秒。
3.NOP指令
NOP指令是一种汇编语言中的指令,它的全称是"No Operation",意思是“无操作”,一般用来控制CPU时间,达到时钟延时的效果。
NOP指令也为单周期指令即在12MHz单片机中,延时1微妙。
以延时1秒为例,可加入NOP指令如下代码所示:
DELAY_1_SECOND:
MOV R3, #100 ; 设置外层循环计数器的初始值,具体值根据实际CPU频率调整
MOV R2, #200 ; 设置中间循环计数器的初始值,具体值根据实际CPU频率调整
MOV R1, #100 ; 设置内层循环计数器的初始值,具体值根据实际CPU频率调整
DELAY_OUTER_LOOP:
DJNZ R3, DELAY_OUTER_LOOP_END ; 外层循环计数器递减,当 R3 不为零时跳转到 DELAY_OUTER_LOOP_END 标签处
DELAY_MIDDLE_LOOP:
DJNZ R2, DELAY_MIDDLE_LOOP_END ; 中间循环计数器递减,当 R2 不为零时跳转到 DELAY_MIDDLE_LOOP_END 标签处
MOV R2, #200 ; 重置中间循环计数器的值
DJNZ R1, DELAY_MIDDLE_LOOP ; 内层循环计数器递减,当 R1 不为零时跳转到 DELAY_MIDDLE_LOOP 标签处
NOP ; 内层循环结束后执行NOP指令,增加延时精度
DELAY_MIDDLE_LOOP_END:
NOP ; 空指令,耗时1个周期
DELAY_OUTER_LOOP_END:
NOP ; 空指令,耗时1个周期
RET ; 返回
二、汇编语言查表法求平方数
(一)汇编程序及其解读
要求:用查表法完成求平方数的程序,求出1~9的平方数
代码如下:
ORG 0000H ;定义下一条的指令放在地址为0000H存储器
LJMP A1 ;长跳转指令至A1
ORG 0080H
A1: NOP
NOP
MOV SP,#60H
MOV DPTR,#2000H
MOV A,#03H
MOVC A,@A+DPTR
A2: SJMP A2
ORG 2000H
DB 01h,04h,09h,10h,19h,24h,31h,40h,51h
END
代码解读:
1、程序起始:
ORG 0000H
:指定下一条指令的地址为0x0000,这是程序的起始地址。LJMP A1
:执行长跳转到标签A1处,即程序跳转到0x0080处执行。
2、A1处操作:
A1: NOP
:在0x0080处定义了一个标签A1,接着是一个NOP指令,这个NOP指令是空操作,不做任何事情。NOP
:另一条NOP指令,同样是空操作。
3、设置栈指针和数据指针:
MOV SP, #60H
:将栈指针SP的值设置为0x60,这样可以指定栈的初始位置。MOV DPTR, #2000H
:将数据指针DPTR的值设置为0x2000,这样可以指定数据存储器的初始位置。
4、数据传输和循环:
MOV A, #03H
:将累加器A的值设置为0x03。MOVC A, @A+DPTR
:这个指令从数据存储器中读取A+DPTR地址处的内容,并将其存储在累加器A中。这个操作通常用于从外部存储器中读取数据。
5、循环部分:
A2: SJMP A2
:在0x2000处定义了一个标签A2,然后执行无条件短跳转,跳转到标签A2处,形成一个无限循环。
6、数据存储:
ORG 2000H
:指定下一条指令的地址为0x2000。DB 01h, 04h, 09h, 10h, 19h, 24h, 31h, 40h, 51h
:在0x2000处定义了一系列字节数据,依次为0x01, 0x04, 0x09, 0x10, 0x19, 0x24, 0x31, 0x40, 0x51。
7、程序结束:
END
:指示程序结束。
运行过程:
- 程序首先从地址0x0000处开始执行,跳转到标签A1处(地址0x0080)。
- 在A1处执行了一些初始化操作,设置了栈指针和数据指针。
- 然后,在循环中,累加器A从数据存储器中读取数据,并且程序会不断地循环执行这个过程。
- 最后,程序进入到一个无限循环中,不再执行其他操作。
(二)在Edsim中验证程序
将上述代码放置在Edsim51中仿真的得到下面的结果:
(三)Proteus中验证程序
1.打开Proteus创建新项目之后将代码复制粘贴,点击Debug后选择第一项。
最后得证且得到的结果如下图:
三、普中开发板应用实例
LED模块原理图
如图所示,右上方得到从上到下为P2端口的P2_0到P2_7引脚,左方为VCC即当芯片给予LED灯低电平时二极管导通,LED灯亮。
(一)LED灯周期性闪烁
1.C语言实现
#include <REGX52.H>
#include <INTRINS.H>
void Delay1ms(unsigned int xms) //@12.000MHz
{
unsigned char i, j;
while(xms)
{
i = 2;
j = 239;
do
{
while (--j);
} while (--i);
xms--;
}
}
void main()
{
while(1)
{
P2_0=0;//1111 1110
Delay1ms(1000);
P2_0=1;//1111 1111
Delay1ms(1000);
}
}
代码解释:
-
#include <REGX52.H>
和#include <INTRINS.H>
: 这两行代码包含了 8051 单片机的寄存器定义和一些内部函数的头文件,以便在代码中使用单片机的特定功能和寄存器。 -
void Delay1ms(unsigned int xms)
: 这是一个延时函数,用于在程序执行过程中创建一定的延时。它接受一个参数xms
,表示要延时的毫秒数。 -
while(xms) {...}
: 这是一个 while 循环,用于执行指定毫秒数的延时。在每次循环迭代中,会递减xms
直到为 0。 -
i = 2; j = 239; do { ... } while (--i);
: 这是一个嵌套的 do-while 循环,用于创建精确的延时。内部的while (--j)
循环用于消耗 CPU 时间,以实现精确的延时。 -
void main() {...}
: 这是主函数,程序的入口点。在其中包含了一个无限循环while(1)
,用于不断地执行 LED 状态的改变。 -
P2_0=0;
和P2_0=1;
: 这两行代码用于控制单片机的 P2_0 引脚(即第 0 号端口)的输出状态。通过赋值为 0 和 1,分别控制该引脚输出低电平和高电平,从而控制外部设备的状态(如 LED 灯)。 -
Delay1ms(1000);
: 这两行代码分别调用了Delay1ms
函数,实现了 1000 毫秒的延时。在每次延时结束后,会改变 P2_0 引脚的状态,从而控制 LED 的亮灭。
2.汇编代码实现
ORG 0000h
LJMP MAIN
ORG 0100H
MAIN:
LOOP:
MOV A,#0FEH //0111 1111
MOV P2,A
LCALL DELAY
MOV A,#OFFH //11111111
MOV P2,A
LCALL DELAY
LJMP LOOP
//延时1秒的函数
DELAY:MOV R5,#10
D1: MOV R6,#200
D2: MOV R7,#250
DJNZ R7,$
DJNZ R6,D2
DJNZ R5,D1
RET
END
代码解释:
-
ORG 0000h
: 这行指令将程序的起始地址设置为 0000h,这通常是程序的入口点。 -
LJMP MAIN
: 这行指令是跳转到MAIN
标签处执行程序的入口。 -
ORG 0100H
: 这行指令将接下来的代码放置在地址 0100H 处。在 8051 中,通常使用 0100H 作为程序的存储空间。 -
MAIN:
: 这是一个标签,表示程序的主程序入口点。 -
LOOP:
: 这是一个标签,用于创建一个循环。 -
MOV A,#0FEH
: 这行指令将立即数 0FEH(0111 1111)加载到累加器 A 中。 -
MOV P2,A
: 这行指令将累加器 A 的值写入端口 P2,控制外部设备(如 LED)的状态。 -
LCALL DELAY
: 这行指令调用延时函数DELAY
,用于创建一个延时。 -
MOV A,#OFFH
: 这行指令将立即数 OFFH(1111 1111)加载到累加器 A 中,将 LED 关闭。 -
MOV P2,A
: 这行指令将累加器 A 的值写入端口 P2,关闭 LED。 -
LCALL DELAY
: 这行指令再次调用延时函数DELAY
,用于创建一个延时。 -
LJMP LOOP
: 这行指令是一个无条件跳转到LOOP
标签处,实现了循环执行。 -
DELAY:
: 这是一个延时函数的标签。 -
MOV R5,#10
: 这行指令将立即数 10 加载到寄存器 R5 中,用作循环计数器。 -
D1:
和D2:
: 这是两个标签,用于控制延时循环。 -
MOV R6,#200
和MOV R7,#250
: 这两行指令分别将立即数 200 和 250 加载到寄存器 R6 和 R7 中,用作内部循环计数器。 -
DJNZ R7,$
、DJNZ R6,D2
和DJNZ R5,D1
: 这些是减少并跳转指令,用于内部循环和外部循环的控制。 -
RET
: 这行指令表示延时函数的结束,并返回到调用它的位置。 -
END
: 这是程序的结束标记。
3.程序烧入单片机
4.效果展示
(1)C语言效果
汇编代码效果一样。
(二)LED实现流水灯
1.C语言实现
#include <REGX52.H>
#include <INTRINS.H>
void Delay500ms() //@12.000MHz
{
unsigned char i, j, k;
_nop_();
i = 4;
j = 205;
k = 187;
do
{
do
{
while (--k);
} while (--j);
} while (--i);
}
void main()
{
while(1)
{
P2=0xFE;//1111 1110
Delay500ms();
P2=0xFD;//1111 1101
Delay500ms();
P2=0xFB;//1111 1011
Delay500ms();
P2=0xF7;//1111 0111
Delay500ms();
P2=0xEF;//1110 1111
Delay500ms();
P2=0xDF;//1101 1111
Delay500ms();
P2=0xBF;//1011 1111
Delay500ms();
P2=0x7F;//0111 1111
Delay500ms();
}
}
代码解释:将P2口赋予不同的十六进制数改变每个引脚的电平来实现流水灯操作,每个普通的灯亮灭间加上延时函数即可。
2.汇编代码实现
MAIN:
MOV P2, #11111110B
LCALL DELAY
MOV P2, #11111101B
LCALL DELAY
MOV P2, #11111011B
LCALL DELAY
MOV P2, #11110111B
LCALL DELAY
MOV P2, #11101111B
LCALL DELAY
MOV P2, #11011111B
LCALL DELAY
MOV P2, #10111111B
LCALL DELAY
MOV P2, #01111111B
CALL DELAY
LJMP MAIN
DELAY:
MOV R7, #3 ;@11.0592MHz
D1:
PUSH 30H
PUSH 31H
MOV 30H, #180
MOV 31H, #68
NEXT:
DJNZ 31H, NEXT
DJNZ 30H, NEXT
POP 31H
POP 30H
DJNZ R7, D1
RET
END
3.效果展示
(1)C语言效果
汇编代码效果一样。