汇编/Assembler

汇编/Assembler

汇编部分学习建议

本章内容比较复杂,个人认为死记硬背并不适合,书上、PPT以及第十一章的学习通作业题里有很多很好的例子,包括邓老师在12周周一那节课讲的那几道题,都是很好的例子,可以多去看看这些例子。详细分析、了解每个例子中每一条指令的含义,CPU在这些执行这些指令时都干了些什么,有哪些寄存器发生了变化,变化是什么样的。这样学习应该会比较快速的上手汇编指令的阅读。

汇编指令这学期的单片机考试不会考设计编写程序,因此只要会阅读就可以了,作业题里需要自己设计的程序可以直接把答案翻出来分析。

因此本文的汇编部分除了一些基础的需要记得内容,大部分是一些例子分析。


在学习开始前,需要先了解一些符号注释的含义,这些符号注释是为了在描述51单片机指令系统的功能时更加简单易懂

  1. RiRn:表示当前工作寄存器区中的工作寄存器,i取0或1,表示R0或R1。n取0-7,表示R0-R7.
  2. #data:表示包含在指令中的8位立即数。
  3. #data16:表示包含在指令中的16位立即数。
  4. rel:以补码形式表示的8位相对偏移量,范围为-128-127,主要用在相对寻址的指令中。
  5. addr16addr11 :分别表示16位直接地址和11位直接地址。
  6. direct:表示直接寻址的地址。
  7. bit:表示可位寻址的直接位地址。
  8. (X):表示X单元中的内容。
  9. ((X)):表示以X单元的内容为地址的存储器单元内容,即(X)作地址,该地址单元的内容用((X))表示。
  10. /->符号:“/”表示对该位操作数取反,但不影响该位的原值。“->”表示操作流程,将箭尾一方的内容送入箭头所指另一方的单元中去。

寻址方式

立即寻址

利用的变量 使用的空间
#data ROM

立即寻址就是直接在数之前加#,表示将#后面的数存到对应的寄存器中

例如:

1
MOV A,#54H

即将54H这个数存到A寄存器中。此时 (A)=54H

直接寻址

利用的变量 使用的空间
direct、SFR 片内RAM和SFR

直接寻址就是给出操作数的地址,直接访问这个地址里面的内容,对地址内的内容进行读取和写入。访问SFR,只能使用直接寻址,可以写SFR的名称,也可以写SFR的地址。(SFR相关内容请查阅: 特殊功能寄存区

例如:片内RAM中3FH地址存有数据FFH

1
MOV A,3FH

即将3FH中的数据取出来放到A中:(3FH)->(A)

此时 (A)=FFH

寄存器寻址

利用的变量 使用的空间
Rn, A ,B, Cy, DPTR 片内RAM

在指令选定的某寄存器中存放或读取操作数,以完成指令规定的操作

常用R0-R7,A, B, DPTR, Cy作为寄存器寻址方式中的寄存器

例如:片内RAM中R7中存有数据A9H

1
MOV A,R7

即将R7中的数据取出来放到A中:(R7)->(A)

此时 (A)=A9H

寄存器间接寻址

这种方式可以类比C语言的指针,在寄存器前加上@就是间接寻址,就是取出指定寄存器存放的数据,通过这个数据指定的地址来找到对应地址的内容。

比如某人(源寄存器)告诉你(目标寄存器),在某个房间(源寄存器中存的数据),这个房间里有一份文件(这个数据对应地址内的内容)需要你去取。这条指令就是MOV 你,@某人

这里举例说明:

1
2
3
;(R1)=80H
;(80H)=2FH
MOV A,@R1

这里的分号即为程序注释,类比C语言中的\\

就是将R180H取出,再到80H找到里面存放的数据,最后将这个数据存到A中:((R1))->(A)

此时 (A)=2FH

变址寻址

变址寻址一般用于ROM的查表指令MOVC,在@后会出现两个寄存器相加,这两个寄存器分别被称为基址寄存器(DPTR和PC)和变址寄存器(A)。变址寻址时,将这两个寄存器内容相加,得到的结果为操作数的地址,将ROM中对应的该地址中的内容取出并存到目的操作数中。

例如:

1
2
3
4
;ROM中(10DCH)=1FH
;(A)=DCH
;(DPTR)=1000H
MOVC A,@A+DPTR

即将(DPTR)与(A)相加,得到10DCH,访问10DCH,将10DCH的数据取出,覆盖A中的数据。

操作后 (A)=1FH

相对寻址

将程序计数器PC的当前值与指令所给出的偏移量rel相加,其和为跳转指令的转移地址;

当前PC值是指存储相对转移指令的首地址加上该指令的字节数(即相对寻址是相对于下一条指令的首地址);

偏移量rel是有符号的单字节数。-128-+ 127(00H-FFH),负数表示从当前地址向前转移,正数表示从当前地址向后转移。

1
2
3
4
5
L1:ORG 200H
SJMP 20H
MOV A,#20H
L2:ORG 222H
MOV A,#30H

该程序L1部分起始地址为200HL2222H,当程序执行到SJMP后,因为SJMP的字节数是2字节,所以MOV A,#20H的首地址是202H,但是因为向后跳转了20H,即SJMP后PC指向的地址是222H,所以直接跳转到执行MOV A,#30H,而MOV A,#20H永远不会执行。

即操作后 (A)=30H

位寻址

利用的变量 使用的空间
bit 片内RAM的位寻址区以及可以位寻址的SFR

位寻址即可以对地址中每一位进行操作

例如:

1
2
;(24H)=1100 1011B
MOV C,24H.0

即将24H的第0位的数据取出放到C中。

操作后 (C)=1

数据传送类指令

MOV指令

片内RAM数据传送指令/MOV

这个指令在上面寻址方式中涉及了很多,大体就是将源操作数指定的内容送到目标操作数中,理解起来比较简单,主要需要注意的是寻址方式的不同,这里就不再多述,主要讲一讲MOVC、MOVX和特殊的数据传送指令。

片外RAM数据传送指令/MOVX

MOVX实现了CPU对片外RAM或I/O口进行数据传送,必须采用寄存器间接寻址的方法,通过累加器A来完成。

MOVX只能采用间接寻址,有且仅有以下四条指令:

1
2
3
4
MOVX A,@DPTR   ;(A)<-((DPTR))
MOVX A,@Ri ;(A)<-((Ri))
MOVX @DPTR,A ;(DPTR))<-(A)
MOVX @Ri,A ;(Ri))<-(A)

MOVX我个人感觉比较难理解的是,“片外”这个过程是在哪里发生的?

下面根据我自己的理解直接举一个例子:

片外RAM中8000H单元中有内容66H,现要将该内容送到片内的R0中。

1
2
3
MOV DPTR,#8000H
MOVX A,@DPTR
MOV R0,A

现在对这个程序每一条指令进行分析:

  1. MOV DPTR,#8000H:将8000H存放在DPTR中(为什么是DPTR不是Ri?因为8000H有16位数据,Ri存放8位,DPTR存放16位数据);

  2. MOVX A,@DPTR:将DPTR里存放的地址取出,找到片外RAM的对应地址,并将这个地址里的内容,然后传送到A中;

  3. MOV R0,A:将A里的内容传送到R0中

  4. 最后的每个寄存器的内容:(DPTR)=8000H,(A)=66H,(R0)=66H。

这里可以注意到,“片外”这一过程只发生在MOVX处,即MOV DPTR,#8000H这里我们并不知道存到DPTR里的地址到底是片内还是片外,只有用了MOVX后CPU才知道要到片外RAM的对应地址去寻找内容,假如在这条指令后跟的是MOV A,@DPTR,那就会到片内RAM的对应地址去寻找内容(当然片内RAM是没有这么大空间的)

通俗点来说,有间谍(DPTR或Ri)某天收到消息说有个东西需要你转送给某人(A),但是他要到某某游泳馆的某个储物柜里去找这个东西,但是到了游泳馆后发现,游泳馆是有男更衣室(片外RAM)和女更衣室(片内RAM)的,他并不知道是在哪个更衣室,联系了上级后告诉你要到男更衣室去找(MOVX),这才从男更衣室找到东西并转送给了A(MOVX A,@DPTR)。

查表指令/MOVC

查表指令其实就是 变址寻址 的具体应用,需要注意的是只有A、DPTR、PC可以执行。

堆栈操作指令/PUSH、POP

堆栈这个东西大体是个啥其实上学期的算法与数据结构已经学习过了,这里需要了解的PUSHPOP执行的时候单片机内发生了些啥

堆栈操作指令看似只有操作助记符以及操作数,但实际变化的还有SP寄存器。

SP寄存器里面存放的数据指向了堆栈的栈顶,也就是入栈和出栈的数据都是在栈顶的地址进行执行的。

PUSH指令就是先将SP加一,将栈顶移动,然后再将内容存放到SP指向的地址中

而POP指令就是先将SP指向的内容取出,再移动栈顶,即将SP减一

下面举一条例子:

1
2
3
4
5
6
7
MOV SP,#40H
MOV A,#20H
MOV B,#30H
PUSH A
PUSH B
POP A
POP B

对每一条指令进行分析:

  1. 前三条指令相当于进行了一个赋值,即(A)=20H、(B)=30H且此时栈顶指向40H;

  2. 将A压入栈,此时SP先+1,也就是栈顶移向41H,然后将A中的内容存入栈顶,即41H;

  3. 继续将B压入栈,重复上述过程,执行完此指令后,单片机寄存器内各内容如下:(A)=20H (B)=30H (SP)=42H (41H)=20H (42H)=30H;

  4. 将栈顶内容弹出并送到A中,SP减一。需要注意的是,栈顶此时指向42H,即送到A中的内容是42H的内容,也就是30H;

  5. 继续将栈顶内容弹出送至B,SP减一,执行完此指令后,单片机寄存器内各内容如下:(A)=30H (B)=20H (SP)=40H

  6. 其实这些内容分析起来并不难,但是容易搞混的是弹出时先弹出的是栈顶的内容,并不是先入栈的内容,也就是算法课中提到的“后入先出”。

交换指令/XCH

交换指令有以下几条:

1
2
3
4
5
XCH A,direct    ;(A)<->(direct)
XCH A,@Ri ;(A)<->((Ri))
XCH A,Rn ;(A)<->(Rn)
XCHD A,@Ri ;(A3-0)<->((Ri))3-0
SWAP A ;(A7-4)<->(A3-0)

前三条就是两个操作数里的内容进行交换,第四条是将两个操作数的低四位的内容交换,最后一条是将A中高四位和低四位交换。

算术运算类指令

在这部分开始之前,我们需要先回忆一下存储器章节中学到的 PSW 寄存器(PPT是在第十章中)。对于这部分的内容,我们需要用到的只有第7位的Cy,第6位的AC,第2位的OV以及第0位的P。

Cy - 进位标志位:第7位。在算术运算时,最高位若有进位/借位Cy=1;否则,Cy=0;在位处理器中,它是位累加器

AC - 半进位标志位:第6位。当低四位高四位产生进位或借位时,AC=1;否则,AC=0

OV - 溢出标志位:第2位。作加减法时:最高位或次高位之一有进位或者借位,OV=1,否则为OV=0,即最高位和次高位同时都有或没有进/借位,OV都为0;作乘法时,积大于255 (B不为0)OV=1;作除法时,除数为0 (B为0),OV= 1

P - 奇偶标志位:第0位。指令执行完,累加器A中”1”的个数是奇数还是偶数;P=1,表示A中”1“的个数为奇数;P=0表示A中”1“的个数为偶数。这个位的值会随着A中内容改变实时变化

加法指令/ADD、ADDC

ADD和ADDC都是将源操作数指出的内容与A相加,并将结果存在A中。不同的是,ADDC需要在加上进位,即如果Cy位为1,那么在做ADDC操作时,要再加上一,即(A)=(源操作数)+(A)+(Cy)。

1
2
3
4
5
6
7
8
;(A)=7AH
;(R0)=30H
;(30H)=E8H
;(PSW)=81H

ADD A,R0
ADDC A,30H
ADDC A,#01H

分析:

  1. ADD A,R0:即7AH+30H->0111 1010B + 0011 0000B = 1010 1010B->AAH。不难发现最高位以及D3位没有进位,即Cy、AC都为0,但是次高位向最高位有进位,则OV为1,且此时A中的“1”有4个,P为0。即该条指令执行之后(A)=AAH,(PSW)=04H。

  2. ADDC A,30H:这里使用的是ADDC,也就是要加上Cy位,即AAH+E8H+0->1010 1010B + 1110 1000B + 0B = 1 1001 0010B,注意这里只有低八位才是放在A中的,进位放在了Cy中,此时最高位以及D3位都有进位,所以Cy、AC都为1,而次高位也向最高位进了一位,所以OV为0,最后A中有三个“1”,P为1。即该条指令执行之后(A)=92H,(PSW)=C1H。

  3. ADDC A,#01H:这里也是ADDC,这条主要是为了展示Cy有进位时的计算,即92H+01H+1->1001 0010B + 0000 0001B + 1B = 1001 0100B。这里所有位都没有进位,且“1”的个数为3,即该条指令执行之后(A)=94H,(PSW)=01H。

减法指令/SUBB

减法指令都是带借位的,即将A减去源操作数的内容以及Cy,并将结果存在A中,即(A)=(A)-(源操作数)-(Cy)。

1
2
3
4
;(A)=7AH
;(PSW)=81H

SUBB A,#30H

分析:因为PSW最高位Cy为1,那么算式为7AH-30H-1->0111 1010B - 0011 0000B - 1B = 0100 1001B,没有任何的借位,且“1”的个数为3,即该条指令执行之后(A)=49H,(PSW)=01H。

乘除法指令/MUL、DIV

1
2
MUL AB
DIV AB

以上两条指令分别代表(A) * (B)(A) / (B)。乘法的结果将高八位存在B中低八位存在A中;除法的商存在A中,余数存在B中。

乘除法时Cy位固定置0,而OV分为以下两种情况:

  1. 乘法时,当结果大于255,即执行乘法指令B中存的数不为0,OV为1;

  2. 除法时,若除数为0,即执行除法指令B中存的数为0,根据除法的特性,除数为0后,A、B中的值为不确定值,此时OV为1。

1
2
3
4
5
6
7
MOV A,#7AH
MOV B,#02H
MUL AB

MOV A,#7AH
MOV B,#02H
DIV AB

分析:

  1. MUL AB:7AH * 02H = 122 * 2 = 244 = F4H = 1111 0100B,小于255没有溢出,即(B)=0,OV=0,“1”的个数为5,即该条指令执行之后(A)=F4H,(B)=0,(PSW)=01H。

  2. DIV AB:7AH / 02H = 122 / 2 = 61 = 3DH = 0011 1101B,除数不为0,即OV=0,“1”的个数为5,即该条指令执行之后(A)=3DH,(B)=0,(PSW)=01H。

加一指令、减一指令/INC、DEC

类比C语言的自加自减运算符,此处不再赘述。需要注意的是,如果是对A中内容操作,那么指令也会同时影响PSW中的奇偶标志位P。

十进制调整指令/DA

在ADD、ADDC后的结果,因为计算机是按照二进制计算,不符合十进制运算规则,结果可能不是正确的十进制BCD码,此时需要在ADD、ADDC后加一条十进制调整指令

1
DA A

即可对A中内容进行调整。

逻辑操作类指令

逻辑与、或、异或/ANL、ORL、XRL

ANL/ORL/XRL分别对应按位与/或/异或,即将A中内容与源操作数所指内容进行逻辑按位操作。

比如:

1
2
3
4
MOV A,FAH
ANL A,0FH
ORL A,05H
XRL A,F0H

分析:

  1. ANL A,0FH:即1111 1010B和0000 1111B按位与,逻辑与遇0则0,即结果为0000 1010B=0AH;

  2. ORL A,0AH:即0000 1010B和0000 0101B按位或,逻辑或遇1则1,即结果为0000 1111B=0FH;

  3. XRL A,F0H:即0000 1111B和1111 0000B按位异或,逻辑异或遇1取反,遇0不变,即结果为1111 1111B=FFH。

从上述程序不难发现,逻辑与、或、异或运算可以快速对操作数进行置数操作:

  • 需要将某几位置0,就可以逻辑与一个对应位为0、其他位为1的数;

  • 需要将某几位置1,就可以逻辑或一个对应位为1、其他位为0的数;

  • 需要将某几位取反,就可以逻辑异或一个对应为为1、其他位为0的数。

循环移位指令/RL、RR、RLC、RRC

即将累加器A中的数进行循环移位,L对应左移,R对应右移,C代表要和Cy位一起循环移位。

取反、清零指令/CPL、CLR

CPL A - 将A中内容按位取反;

CLR A - 将A中内容清零。

控制转移类指令

控制转移类指令其实最后都是将PC计数器跳转到reladdr11addr16所指的地方。

无条件转移指令/LJMP、SJMP、AJMP、JMP

指令 功能
LJMP 长转移指令,3字节,该指令包含16位地址,可以转移的范围为ROM的0000H-FFFFH
AJMP 绝对转移指令或短转移指令,2字节,包含11位地址,范围是下一条指令后的2KB区域内
SJMP 相对转移指令,2字节,采用相对寻址,转移到相对于下条指令首地址后+rel

PC由103H变为1E00H

1
2
ORG 100H
LJMP 1E00H

PC由132H变为260H

1
2
ORG 130H
AJMP 260H

PC由202H变为20AH

1
2
ORG 200H
SJMP 8

条件转移指令

即满足条件后跳转。

1
2
3
4
5
6
7
8
9
10
11
12
JZ rel                  ;当(A)==0时跳转
JNZ rel ;当(A)!=0时跳转
CJNE A, direct, rel ;当(A)==direct时,执行下一条,不跳转
;当(A)>direct时,跳转,且Cy=0
;当(A)<direct时,跳转,且Cy=1
CJNE A, #data, rel ;同上
CJNE Ra, #data, rel ;同上
CJNE @Ri, #data, rel ;同上
DJNZ Rn, rel ;(Rn)=(Rn)-1,然后比较
;当(Rn)!=0时,跳转
;当(Rn)==0时,执行下一条,不跳转
DJNZ direct,rel ;同上

JZ、JNZ、CJNE可类比C语言中的if语句,满足即执行跳转,不满足即跳过。但是不管跳转执行与否,这些语句都是执行了对应的字节数和周期数。

而DJNZ则可类比以下C语言代码:

1
2
3
4
i--;
if (i != 0) {
//跳转后的部分
}

但一般DJNZ用来做的可以类比以下C语言代码:

1
2
3
4
do {
i--;
//跳转后的部分
} while (i != 0)

将片外RAM中1000H-1030H的数据块全部迁移到片内RAM的30H-60H中

1
2
3
4
5
6
7
MOV DPTR,#1000H             ;(DPTR)=1000H
MOV R0,#30H ;(R0)=30H
LOOP: MOVX A,@DPTR ;将片外的((DPTR))传送给A
INC DPTR ;(DPTR)=(DPTR)+1
INC R0 ;(R0)=(R0)+1
CJNE R0,#61H,LOOP ;如果(R0)!=61H,重新跳转到LOOP
;如果(R0)==61H,不跳转

调用子程序及返回指令/LCALL、ACALL、RET、RETI

1
2
3
4
LCALL addr16    ;长调用指令,3字节,调用16位子程序
ACALL addr11 ;短调用指令,2字节,调用下一条指令2KB范围的程序
RET
RETI

子程序就相当于C语言中的函数,通过LCALL和ACALL来调用,每个子程序中必须要由RET返回指令,中断服务子程序则必须有RETI中断返回指令

子程序调用一般的难点在于对堆栈的调整。

执行调用指令时,先将PC的内容加上对应指令的字节数,指向下一条指令地址(断点地址),并将断点地址压入堆栈(先压低八位,再压高八位),然后再将PC的内容指向子程序的地址。

执行完子程序就会执行返回指令,执行返回指令时,就会将栈顶的数据弹出,先传送给PC的高八位,然后SP减一,再把新的栈顶的值传送给低八位。

位操作指令

位操作指令用法和对应指令普通情况下相同,只是改为对位进行操作

位操作指令

伪指令

计算机不做任何操作无对应的机器码不产生目标程序不影响程序的执行,仅仅是能够帮助进行汇编的一些指令。它主要用来指定程序或数据的起始位置,给出些连续存放数据的地址或为中间运算结果保留一 部分存储空间以及表示源程序结束等等。

汇编起始指令/ORG

指定该部分程序在程序存储器中的起始地址

格式:

1
[标号:] ORG 表达式

方框内为可选部分

跟在该指令后的程序段或数据块的起始地址就是ORG后给出的地址

例如:

1
2
3
ORG 200H
MOV A,#100
LOOP: DJNZ A,LOOP

此处MOV A...A,LOOP这一段的起始地址就为200H,即MOV A,#100的首地址为200H

汇编结束指令/END

END用在程序末尾,编译器对END后的指令不会再进行汇编。

1
2
3
4
5
ORG 200H
MOV A,#100
LOOP: DJNZ A,LOOP
END
SJMP LOOP

该程序中,SJMP指令没有被编译,也永远不会执行。

赋值指令/EQU

EQU指令可以类比于C语言中的#define宏定义:

1
符号名 EQU 表达式

此后在程序中就可用定义的符号名来代表表达式的内容;

该指令需先定义后使用,应放在程序开头。

例如:

1
2
3
4
E9 EQU R3
L1 EQU 20H
MOV E9,#30H ;(R3)=30H
MOV A,L1 ;(A)=20H

定义字节、字指令/DB、DW

DB和DW都是表示从ROM的某一地址开始依次存储数据(每个数据要用逗号隔开),不同的是DB存放的是8位数据,而DW存放的是16位数据。

1
2
[标号:] DB 表达式
[标号:] DW 表达式

表达式可以是常数或ASCII码

e.g.

1
2
3
4
5
ORG 200H
DB 0A3H,73,'A',46H,09

ORG 520H
DW 0A66H,9BH,'B'

DB后的结果是:(200H)=A3H,(201H)=49H,(202H)=41H,(203H)=46H,(204H)=9H。

DW后的结果是:(520H)=0AH,(521H)=66H,(522H)=00H,(523H)=9BH,(524H)=00H,(525H)=42H。

DB比较简单,就是将每个数据依次向后存放即可;

需要注意的是DW,ROM每个地址只能存放8位的数据,因此DW的数据需要分为高八位和低八位依次向后存放,比如这里的0A66H就是分为了0AH和66H分别存放在520H和521H当中。

同时要注意的是,DW是存放的16位数据,就算DW后面跟的数据只有8位,也一定要补零补成16位,比如这里的9BH,虽然只写了8位,但是在存放时是存放的009AH。

定义位地址指令/BIT

与EQU相似,但是被定义的变成了位地址

1
F1 BIT PSW.1

汇编后,F1就是PSW.1

数据地址赋值指令/DATA

与EQU在给地址赋值时相似,区别在于DATA定义的符号登记在符号表中,可以先使用在定义。

1
2
MOV DPRT,#M6
M6 DATA 4A00H

定义存储空间指令/DS

DS可以类比于C语言的数组声明,即向ROM申请一段存储空间预留。

1
2
3
ORG 100H
DS 30H
DB 56H,8AH

即从100H开始预留48个地址单元且不赋值,所以DB指令就是从131H开始操作,即(131H)=56H,(132H)=8AH。

作者

MerePT

发布于

2022-05-17

许可协议