
任务二 闪烁灯
任务描述
使用跳转分支指令设计一个延时程序,编写延时程序实现LED灯闪烁。
任务分析
通过本任务的学习和训练,我们将实现以下目标:(1)掌握51单片机的分支跳转指令。
(2)掌握单片机延时程序的嵌套结构。
任务实施
一、汇编语言控制转移指令
通常情况下,单片机是由PC的自加一顺序运行的。控制转移类指令能使程序功能更加灵活,通过给PC赋值实现程序的分支跳转。控制转移类指令可分为6个子类。
1.无条件转移指令
顾名思义,无条件转移指令是强制性地更改PC的值,不需要其他条件。无条件转移指令共有4条。
◆ AJMP addr11;addr11→(PC10-0)
说明:短跳转指令,实现2K字节范围内的无条件转移,指令执行后将addr11送入
PC低11位(10-0),高5位保持不变,因此跳转的目标地址必须与执行后PC值的高5位相同,以保证在2K字节范围内跳转。AJMP跳转示意图如图119所示。
例如:AJMP LK

执行前,PC的值为指令“AJMPLK”所在的地址。执行后,PC的值为指令标签LK所代表的地址。
图1-19 AJMP跳
转示意图
补充:使用AJMP指令相当于将64K分成了32个区间,每个区间2KB字节,每次跳转只能在固定的某一个2K区间完成。
◆ SJMP rel;(PC)+rel→(PC)
说明:程序分支指令,执行该指令的过程中PC加2之后再与rel相加得出目标地址,rel是8位带符号的二进制补码数,因此SJMP可实现向前128字节向后127字节的跳转功能或原地死循环。
例如:当前“SJMP 22H”指令的地址为1234H,执行该指令过程中PC加2之后,
再计算(PC)+22H=1258H后赋予PC,该指令执行结束。另外可以使用“LOOP:
SJMP LOOP”让程序原地踏步,在转移类指令中可以使用“$”来代表当前指令所在地
址标签,因此“LOOP:SJMP LOOP”可用“SJMP $”代替。
注意:正整数的二进制补码与原码相同,如2的补码为0000 0010B,即02H。负整数的补码为其绝对值原码的反码加1,如-2的绝对值为2,原码为0000 0010B,反码为
1111 1101B,加1后为1111 1110B,即FEH。
◆ LJMP addr16
说明:长跳指令,将16位地址addr16赋予PC实现64KB地址范围的跳转,即ad
dr16→(PC)。
例如:LJMP MAIN
执行前,PC的值为指令“SJMP MAIN”的地址。
执行后,PC的值为指令标签MAIN所在指令的地址。
◆ JMP@A+DPTR
说明:间接跳转指令,将DPTR+A的值赋予PC实现程序的分支跳转,通常给予
DPTR一个固定的值作为基址,A作为变址,即(A+DPTR)→(PC)。
例如:JMP@A+DPTR
执行前,PC为指令“JMP@A+DPTR”的地址。
执行后,PC的值为A+DPTR,将从此地址开始执行。2.条件转移指令
需要一定条件才执行程序的转移,当规定的条件满足,则进行转移,否则顺序执行。包括字节条件与位条件判断,共有7条指令。
◆ JZ rel
说明:如果累加器A=0,则(PC)+rel→(PC),否则顺序执行。rel为8位带符号
二进制补码,可向前128字节或向后127字节分支转移。该指令框图如图1-20所示。

例如:JZ LOOP
若执行前A=0,则执行后跳至LOOP标签所代表的地址,否则顺序执行。
◆ JNZ rel
说明:如果累加器A≠0,则(PC)+rel→
(PC),否则顺序执行。
◆ JC rel
图1-20 有条件跳转指令框图
说明:如果PSW寄存器位CY=1,则
(PC)+rel→(PC),否则顺序执行。
◆ JNC rel
说明:如果CY≠0,则(PC)+rel→(PC),否则顺序执行。◆ JBbit,rel
说明:如果bit位值为1,则(PC)+rel→(PC),否则顺序执行。例如:JB54H,RGB
若执行前地址为54H的位内容为1,则跳转至RGB标签所代表的地址执行。
◆ JNB bit,rel
说明:如果bit位的值为0,则(PC)+rel→(PC),否则顺序执行。◆ JBCbit,rel
说明:如果bit位的值为1,则(PC)+rel→(PC),然后bit位清零,否则顺序
执行。
3.子程序调用指令
在一个程序中经常会碰到反复多次执行某个程序段的情况,如果重复编写这个程序段会使整个程序变得冗长而杂乱,可读性与可维护性都极低。采用子程序结构便可解决此问题,将在程序中重复出现的程序段作为一个子程序,在程序需要时再调用即可,子程序调用指令的功能便是为了实现子程序的调用与返回。
◆ ACALL addr11
说明:短调用指令,能实现2K字节范围内的子程序调用,执行后PC的低11位值为
addr11,高5位不变,即目标地址与执行后PC值的高5位必须相同,另外为了保护断点,把PCL(低8位)与PCH(高8位)
分别顺序压入堆栈,即(SP)+1→(SP),(PCL)→((SP)),(SP)+1→(SP),(PCH)→((SP)),addr11→(PC10-0 )。

ACALL子程序调用示意图如图121所示。
例如:ACALL DELAY
图1-21 ACALL子程序
调用示意图
执行前,PC为指令“ACALL DELAY”的地址。
执行后,PC的值为指令标签DELAY所代表的地址。
◆ LCALL addr16
说明:长调用指令,可实现64KB地址范围跳转,即(SP)+1→(SP),(PCL)→((SP)),(SP)+1→(SP),(PCH)→((SP)),addr16→(PC)。
例如:LCALL DIS
执行前,PC为指令“LCALL DIS”的地址。
执行后,PC的值为指令标签DIS所代表的地址。
◆ RET
说明:子程序返回指令,无参数。将堆栈中的地址恢复到PC,即((SP))→
(PCH),(SP)-1→(SP),((SP))→(PCL),(SP)-1→(SP)。
◆ RETI
说明:中断服务子程序返回指令,无参数,与RET类似,当中断条件成立,则转向执行相应中断服务子程序,同时也要保护断点,遇RETI返回断点处继续执行主程序。不同的是该指令会清除相应的中断标志位。
4.比较转移指令
根据比较结果决定是否跳转,指令格式为“CJNES1,S2,rel”,前两个操作数为8
位无符号二进制数。S1可为A、Rn或@Ri,S2可为direct或#data,偏移地址rel为8
位二进制补码数,即可向前128字节或向后127字节跳转。另外,如果S1<S2,则进位
标志CY=1。
◆ CJNE A,direct,rel
说明:如果(A)≠(direct)则跳转至目标地址,否则顺序执行。
例如:CJNE A,5BH,GOK 24
执行前,(A)=39H,(5BH)=A5H,(C)=0B,PC为将要执行的指令的地址。
执行后,(A)=39H,(5BH)=A5H,(C)=1B,PC指令标签GOK所代表的地址。◆ CJNEA,#data,rel
说明:若(A)≠data则跳转至目标地址,否则顺序执行。
◆ CJNERn,#data,rel
说明:若(Rn)≠data则跳转至目标地址,否则顺序执行。
◆ CJNE@Ri,#data,rel
说明:若((Ri))≠data则跳转至目标地址,否则顺序执行。5.计数循环指令
功能是将源操作数的内容减1,并判断是否为0,是则顺序运行,否则跳转。源操作数可以为工作寄存器或存储器地址,使用该指令可以实现循环计数功能,主要应在延时程序中。
◆ DJNZ Rn,rel
说明:先将(Rn)的内容减1,若(Rn)≠0则跳转,否则顺序运行。
◆ DJNZ direct,rel
说明:将(direct)的内容减1,若(direct)≠0则跳转,否则顺序运行。
6.标签在控制转移类中的使用
前面已经介绍了AJMP addr11指令的基本功能和用法,AJMP是2K字节的跳转指
令,addr11表示一个11位的二进制数。执行完AJMP后PC低11位的值为addr11,能实
现程序的分支跳转。
假如现在需要根据P1口的输入状态不断地刷新P0口的输出状态,程序如下

该程序编译后总共5字节,“MOV P0,P1”指令3字节存储在0000H~0002H中,完成读取输入刷新输出的功能。“AJMP 00H”指令2字节存储在0003H~0004H中,功
能是跳转到0000H单元(即PC=0000H)重新执行第一个指令,完成不断刷新输出的功能。但实际中并不会使用“AJMP 00H”的形式表示,因为一般程序中还会有其他的功能程序段,其跳转目标地址不清楚或者很难计算,这种形式很不方便,看下面的程序。

因此使用指令标签来表示,即在指令前加一个标签表示该指令所在的ROM地址,使用该标签代替十六进制数形式如下。

FRESH为什么可以代替指令“MOV P0,P1”的地址呢?这是在编译时自动计算的,编译过程中会将其实际ROM地址代替FRESH标签,知道如何使用即可。
另外在控制转移类中可以使用“$”来代表当前指令的指令标签,常用于等待延时与循环延时或进入死循环,如上面的“JB P3.4,$”是用于按键等待延时,如果P3.4为1则跳回到该指令一直循环判断,如果为0则顺序执行。或使用“DJNZ Rn,$”来进行延时,当(Rn)减1后不为0,则跳回到该指令继续循环执行,直至(Rn)为0后才顺序执行,这就能达到延时的效果。
二、汇编语言空操作指令
空操作指令是一条特殊的跳转指令,功能是跳转到下一条指令的地址,相当于什么都不做只等待一个机器周期,该指令用于特殊延时场合。
◆ NOP
说明:无参数,空操作。
三、延时程序设计
单片机的运算速度在几兆赫兹到几十兆赫兹的范围内不等,以12MHz的51单片机为例,每个机器周期刚好为1μs,每条指令执行时间为1μs、2μs或4μs。对于人眼来说,100ms的响应时间都是无法分辨出来的。假如闪烁频率为1s,要使人能看到LED灯的闪烁,延时要求为500ms,则必须使用延时程序段来实现。在与某些低速设备交互数据时单片机同样需要等待,这也是对延时另一个主要的需求应用,在后面的实战中将会描述。
延时的基本结构就是循环,根据给定的条件不停地循环运行直至条件成立退出循环结构,在程序执行过程中实现延时。从前面的控制转移类指令介绍中,我们知道可用于循环
计数的指令DJNZ Rn,rel或DJNZ direct,rel,它们功能基本相同,只是源操作数不同,
将(Rn)或(direct)的值减1,然后判断是否为0,不为0则跳转,为0则顺序执行。
例如:

第一条MOV指令在R3中存入立即数200,第二条指令将R3减1为199后,判断是否为0,不为0则跳回该DJNZ指令继续循环执行,如此循环执行200次该指令后R3的值变为0,然后顺序执行下一条指令。循环的时间为指令执行时间乘以执行次数,即2×200=400μs(DJNZ的双周期指令)。严格说来第1条指令也属于延时范围内,但是它只被执行1次因此可忽略不计。上面的例子中源操作数为一个字节,最长延时为255(FFH)×2=510μs,时间远远不够,因此采用二重循环结构:

在两条赋值指令之后进入循环,首先在第一条循环计数指令执行200次后R4为0,
接着执行DJNZ R3,CIR2指令,R3减1后不为0则跳转到CIR2标签处,给R4赋值后
又一次开始200次的循环,循环总次数为20×200=4000次。使用两重循环最长循环时长
可以达到255×255×2μs,约130ms。离500ms尚有差距,因此需再增加循环次数。

上面为三重循环的延时程序,其执行过程与两重类似,三重的循环最长循环时长可以
达到2553×2μs,约33s。上面的程序延时为25×50×200×2μs,约0.5s。上面的几个例
子中都是以12MHz的时钟周期为前提条件的,不同的时钟周期执行的时间有差异,频率越高执行速度越快,时间越短。
延时在LED灯控制中的应用:
LED灯的闪烁就是一个通、断电的重复过程,让某一个引脚不断地重复输出高电平、延时、输出低电平、延时即可。例如让P1.0端口的灯闪烁,设计程序如下。


上面的程序即可实现P1.0端口的LED灯的闪烁,以12MHz晶振计算延时约25×50×200×2≈0.5s。程序中出现了两个相同的延时程序段,显得有点冗长多余,因此可以将它作为一个子程序段。

将延时程序编写为一个子程序DELAY,然后在程序中通过LCALL调用,显然上面的程序源码更加简洁明了,编译后使用的程序存储器空间更少。
在新建的“闪烁灯.ASM”文件中输入上面的程序,如图1-22所示。
编译后下载到实验板中,然后单片机复位开始运行,就可以看到P1.0的LED灯以约1s的频率在闪烁。
能力检测
(1)能够独立实现点亮实验板上的每一个小灯,并且可以实现小灯的点亮、关闭以及闪烁。
(2)独立完成流水灯右移操作以及流水灯先左移后右移等简单的花样操作。

图1-22 在编辑栏输入闪烁灯程序