通过前面的学习,我们已经了解了单片机内部的结构,并且也已经知道,要控制单片机,让它为我们干学,要用指令,我们已学了几条指令,但很零散,从现在开始,我们将要系统地学习8051单片机的指令部份。
一、概述
1、指令的格式
寻址方式
指令的一个重要组成部分是操作数,由它指定参与运算的数据或数据所在的存储器单元或寄存器或I/O接口的地址。指令中所规定的寻找操作数的方式就是寻址方式。每一种计算都具有多种寻址方式,寻址方式越多,计算机的功能就越强,灵活性就越大。寻址方式的多少及寻址功能是反映指令系统优劣的主要因素之一。要掌握指令系统也可从寻址方式入手。
MCS-51指令系统的寻址方式有7种:立即寻址(#data)、寄存器寻址(Rn)、间接寻址(@Ri、@DPTR)直接寻址direct、变址寻址(A+)、相对寻址(rel)和特定寄存器寻址(A)。有些书把A当寄存器寻址,把位寻址单独作一种寻址方式,不管怎么分类其目的是为了便于记忆、掌握111条指令。
1.立即寻址(#data)
操作数包含在指令字节中,操作数直接出现在指令中,并存放在程序存储器中,这种方式称为立即寻址。
立即寻址指令的操作数是一个8位或16位的二进制常数,它前面以“#”号标识,例如:ADD A,#56H,即#56H与累加器A(设为31H)内容相加,结果(87H)存于累加器A中。这条指令的机器码为2456H.
2.寄存器寻址(Rn)
由指令指出某一个寄存器中的内容作为操作数,这种寻址方式称为寄存器寻址。在这种寻址方式中,指令的操作码中包含了参加操作的工作寄存器R0~R7的代码(指令操作码字节的低3位指明所寻址的工作寄存器)。例如:ADD A,Rn中的Rn,当n为0、1、2时,机器码分别为28、29、2A.
3.间接寻址(@Ri/@DPTR)
由指令指出某一个寄存器内容作为操作数的地址。这种寻址方式称为寄存器间接寻址。访问外部RAM时,可使用R0,R1或DPTR作为地址指针,寄存器间接寻址用符号“@”表示。
例如:MOV A,@RO(机器码E7)是指:若RO内容为66(内部RAM地址单元66H),而66H单元中内容是27H,则指令的功能是将27H这个数送到累加器A.
4.直接寻址(direct)
在指令中直接给出操作数所在存储单元的地址(一个8位二进制数),称为直接寻址。直接地址用direct表示,
直接寻址方式中操作数存储的空间有三种:
(1).内部数据存储器的128个字节单元(00H~7FH)
(2).位地址空间(有些书把这种寻址方式单独作一种寻址方式)
(3).特殊功能寄存器, 特殊功能寄存器只能用直接寻址方式进行访问。
5.基址加变址寻址(@A+PC/@A+DPTR)
以16位寄存器(DPTR或PC)作为基址寄存器,加上地址偏移量(累加器A中的8位无符号数)形成操作数的地址。
变址寻址方式有两类:
(1).以程序计数器的值为基址例如指令:
MOVC A,@A+PC; ;(A)←((A)+(PC))
指令的功能是先使PC指向本指令下一条指令地址(本指令以完成),然后PC地址与累加器内容相加,形成变址寻址的单元地址内容送A。
(2).以数据指针DPTR为基址,以数据指针内容和累加器内容相加形成地址,例如:
MOV DPTR #4200H ;给DPTR赋值
MOV A,#10H ;给A赋值
MOVC A ,@A+DPTR ;变址寻址方式(A)←((A)+(DPTR))
三条指令的执行结果是将4210H单元内容送A中。
6.相对寻址(rel)
以程序计数器PC的当前值为基址,加上相对寻址指令的字节长度,再加上指令中给定的偏移量rel的值(rel是一个8位带符号数,用二进制补码表示),形成相对寻址的地址。
例如指令:
JNZ rel (或rel = 23H,机器码为7023)
当A≠0时,程序跳到这条指令后面,相差23个字节运行下一条指令。
7.特定寄存器寻址
累加器A和数据针DPTR这两个使用最频繁的寄存器又称为特殊寄存器。对特定寄存器的操作指令,指令不再需要指出其地址字节,指令码本身隐含了操作对象A或DPTR。
例如:
INC A (指令码04) ;累加器加1
MOV A,#12H (指令码7412) ;数12送累加器
INC DPTR (指令码A3) ;数据指针内容加1
综上所述,寻址方式与存储器结构有密切关系。一种寻址方式只适合于对一部分存储器进行操作,在使用时要加以注意。
我们已知,要让计算机做事,就得给计算机以指令,并且我们已知,计算机很“笨”,只能懂得数字,如前面我们写进机器的75H,90H,00H等等,所以指令的第一种格式就是机器码格式,也说是数字的形式。但这种形式实在是为难我们人了,太难记了,于是有另一种格式,助记符格式,如MOV P1,#0FFH,这样就好记了。 这两种格式之间的关系呢,我们不难理解,本质上它们完全等价,只是形式不一样而已。
2、汇编
我们写指令使用汇编格式,而计算机和单片机只懂机器码格式,所以要将我们写的汇编格式的指令转换为机器码格式,这种转换有两种办法:手工汇编和机器汇编。手工汇编实际上就是查表,因为这两种格式纯粹是格式不一样,所以是一一对应的,查一张表格就行了。不过手工查表总是嫌麻烦,所以就有了计算机软件,用计算机软件来替代手工查表,这就是机器汇编。
二、单片机的寻址
让我们先来复习一下我们学过的一些指令:MOV P1,#0FFH,MOV R7,#0FFH这些指令都是将一些数据送到对应的位置中去,为什么要送数据呢?第一个因为送入的数能让灯全灭掉,第二个是为了要实现延时,从这里我们能看出来,在用单片机的编程语言编程时,经常要用到数据的传递,事实上数据传递是单片机编程时的一项重要工作,一共有28条指令(单片机共111条指令)。下面我们就从数据传递类指令开始吧。
分析一下MOV P1,#0FFH这条指令,我们不难得出结论,第一个词MOV是命令动词,也就是决定做什么事情的,MOV是MOVE少写了一个E,所以就是“传递”,这就是指令,规定做什么事情,后面还有一些参数,分析一下,数据传递必须要有一个“源”也就是你要送什么数,必须要有一个“目的”,也就是你这个数要送到什么地方去,显然在上面那条单片机指令中,要送的数(源)就是0FFH,而要送达的地方(目的地)就是P1这个寄存器。在数据传递类指令中,均将目的地写在指令的后面,而将源写在最后。
这条指令中,送给P1是这个数本身,换言之,做完这条指令后,我们能明确地知道,P1中的值是0FFH,但是并不是任何时候都能直接给出数本身的。例如,在我们前面给出的单片机延时程序例是这样写的:
MAIN: SETB P1.0 ;(1)
LCALL DELAY ;(2)
CLR P1.0 ;(3)
LCALL DELAY ;(4)
AJMP MAIN ;(5)
;以下子程序
DELAY: MOV R7,#250 ;(6)
D1: MOV R6,#250 ;(7)
D2: DJNZ R6,D2 ;(8)
DJNZ R7,D1 ;(9)
RET ;(10)
END ;(11)
表1
-----------------------------------------------------
MAIN: SETB P1.0 ;(1)
MOV 30H,#255
LCALL DELAY ;
CLR P1.0 ;(3)
MOV 30H,#200
LCALL DELAY ;(4)
AJMP MAIN ;(5)
;以下子程序
DELAY: MOV R7,30H ;(6)
D1: MOV R6,#250 ;(7)
D2: DJNZ R6,D2 ;(8)
DJNZ R7,D1 ;(9)
RET ;(10)
END ;(11)
表2
这样一来,我每次调用延时程序延时的时间都是相同的(大致都是0.13S),如果我提出这样的要求:灯亮后延时时间为0.13S灯灭,灯灭后延时0.1秒灯亮,如此循环,这样的程序还能满足要求吗?不能,怎么办?我们能把延时程序改成这样(见表2):调用则见表2中的主程,也就是先把一个数送入30H,在子程序中R7中的值并不固定,而是根据30H单元中传过来的数确定。这样就能满足要求。
从这里我们能得出结论,在数据传递中要找到被传递的数,很多时候,这个数并不能直接给出,需要变化,这就引出了一个概念:如何寻找操作数,我们把寻找操作数所在单元的地址称之为寻址。在这里我们直接使用数所在单元的地址找到了操作数,所以称这种办法为直接寻址。除了这种办法之外,还有一种,如果我们把数放在工作寄存器中,从工作寄存器中寻找数据,则称之为寄存器寻址。例:MOV A,R0就是将R0工作寄存器中的数据送到累加器A中去。提一个问题:我们知道,工作寄存器就是内存单元的一部份,如果我们选择工作寄存器组0,则R0就是RAM的00H单元,那么这样一来,MOV A,00H,和MOV A,R0不就没什么区别了吗?为什么要加以区别呢?的确,这两条指令执行的结果是完全相同的,都是将00H单元中的内容送到A中去,但是执行的过程不一样,执行第一条指令需要2个周期,而第二条则只需要1个周期,第一条指令变成最终的目标码要两个字节(E5H 00H),而第二条则只要一个字节(E8h)就能了。
这么斤斤计较!不就差了一个周期吗,如果是12M的晶体震荡器的话,也就1个微秒时间了,一个字节又能有多少?
不对,如果这条指令只执行一次,也许无所谓,但一条指令如果执行上1000次,就是1毫秒,如果要执行1000000万次,就是1S的误差,这就很可观了,单片机做的是实时控制的事,所以必须如此“斤斤计较”。字节数同样如此。
再来提一个问题,现在我们已知,寻找操作数能通过直接给的方式(立即寻址)和直接给出数所在单元地址的方式(直接寻址),这就够了吗?
看这个问题,要求从30H单元开始,取20个数,分别送入A累加器。
就我们目前掌握的办法而言,要从30H单元取数,就用MOV A,30H,那么下一个数呢?是31H单元的,怎么取呢?还是只能用MOV A,31H,那么20个数,不是得20条指令才能写完吗?这里只有20个数,如果要送200个或2000个数,那岂不要写上200条或2000条命令?这未免太笨了吧。为什么会出现这样的状况?是因为我们只会把地址写在指令中,所以就没办法了,如果我们不是把地址直接写在指令中,而是把地址放在另外一个寄存器单元中,根据这个寄存器单元中的数值决定该到哪个单元中取数据,比如,当前这个寄存器中的值是30H,那么就到30H单元中去取,如果是31H就到31H单元中去取,就能解决这个问题了。怎么个解决法呢?既然是看的寄存器中的值,那么我们就能通过一定的办法让这里面的值发生变化,比如取完一个数后,将这个寄存器单元中的值加1,还是执行同一条指令,可是取数的对象却不一样了,不是吗。通过例程来说明吧。
MOV R7,#20
MOV R0,#30H
LOOP:MOV A,@R0
INC R0
DJNZ R7,LOOP
这个例程中大部份指令我们是能看懂的,第一句,是将立即数20送到R7中,执行完后R7中的值应当是20。第二句是将立即数30H送入R0工作寄存器中,所以执行完后,R0单元中的值是30H,第三句,这是看一下R0单元中是什么值,把这个值作为地址,取这个地址单元的内容送入A中,此时,执行这条指令的结果就相当于MOV A,30H。第四句,没学过,就是把R0中的值加1,因此执行完后,R0中的值就是31H,第五句,学过,将R7中的值减1,看是否等于0,不等于0,则转到标号LOOP处继续执行,因此,执行完这句后,将转去执行MOV A,@R0这句话,此时相当于执行了MOV A,31H(因为此时的R0中的值已是31H了),如此,直到R7中的值逐次相减等于0,也就是循环20次为止,就实现了我们的要求:从30H单元开始将20个数据送入A中。
这也是一种寻找数据的办法,由于数据是间接地被找到的,所以就称之为间址寻址。注意,在间址寻址中,只能用R0或R1存放等寻找的数据。
指令系统
数据传送指令
数据传送指令包括数据的传送、交换、堆栈数据的压入与弹出,是最基本、使用率最高的一类指令。助记符有MOV、MOVX、MOVC、XCH、XCHD、SWAP、PUSH、POP共八种。
1.MOV类指令及功能(16条)
这类指令的功能是从源操作数到目的操作数的数据传送。
MOV A, Rn ;Rn→A,寄存器Rn的内容送到累加器A
MOV A, direct ;(direct)→A,直接地址中的内容送A
MOV A, @Ri ;(Ri)→A,Ri间址的内容送A
MOV A, #data ;data→A,立即数送A
MOV Rn,, A ;A→Rn,累加器A中的内容送寄存器Rn
MOV Rn, direct ;(direct)→Rn;直接地址中的内容送Rn
MOV Rn, #data ;data→Rn;立即数送Rn
MOV direct, A ;A→(direct),A中的内容送入直接地址中
MOV direct, Rn ;Rn→(direct),寄存器内容送入直接地址中
MOV direct, direct ;(direct) →(direct),源操作数直接地址的内容送入
;目的操作数的直接地址中
MOV direct, @Ri ;(Ri)→(direct),Ri间址内容送入直接地址中
MOV direct, #data ;data→(direct),立即数送入直接地址中
MOV @Ri, A ;A→(Ri),A中内容送到Ri间址单元中
MOV @Ri, direct ;(direct)→(Ri),直接地址中内容送入Ri间址单元中
MOV @Ri, #data ;data→(Ri),立即数送入Ri间址单元中
MOV DPTR, #data16 ;data16→DPTR,16位常数送入数据指针DPTR中,高8;位送入DPH,低8位送入DPH,低8位送入DPL中。从上述指令可以看出目的操作数有A累加器、Rn寄存器、直接地址direct及间接地址@Ri,源操作数除此之外还多一种立即数data。