8051常用的汇编程序

参考:51汇编语言程序结构与程序示例

一些规范

  • 大小写与命名:
    1. 命令全部小写,寄存器全部大写
    2. ROM 中的常量全部大写(因为这是常量)
    3. RAM 中的变量第一个单词首字母小写,往后每个单词首字母大写,单词之间无分隔(即小驼峰)
    4. 函数标号每个单词的首字母大写,单词之间无分隔(即大驼峰)
    5. 如果是 bit 变量,那么在第 3 点的基础上,后面要加 _bit
  • 缩进:
    1. 标号与后面的代码之间要缩进 4 个空格
    2. 两个标号之间不要求缩进
  • 寄存器的使用:
    1. 开始时最好将 mov SP, #5FH,将 60H 到 7FH 这 32 个字节用于堆栈
    2. 20H~2FH 用于 bit 变量,30H~5FH 用于字节变量
    3. 合理配置 RS1、RS0 来选择用哪组 Rn,建议默认情况下用 00,子函数用 01、10,中断用 11
    4. 大小端要区分好,我个人使用的是小端,即 LSB 放低字节。

延时程序

  延时程序是经常使用的程序,一般设计成具有通用性的循环结构子程序。在设计延时子程序时,延时的最小单位为机器周期,所以要注意晶振的频率。

Delay:
    ; 软件延时
    ; 参数:无
    ; 返回:无
    ; 影响:R1, R2
    mov R2, #01H
    mov R1, #26H
    djnz R1, $
    djnz R2, $-4 
    ret

  上面每条指令的延时时间都是 24 个晶振周期(包括 ret),所以我们可以计算出该函数的执行时间为:24+R2(1+R1+1)24+24 (还要乘上晶振周期)。上面已经是 12 MHz 晶振下的 1ms 延时.

保存/恢复现场程序

  由于我们调用函数前,经常需要保存 A、B、Rn 的值,并在之后恢复,所以不妨写成函数方便使用。

Save:
    ; 保护现场
    ; 参数:无
    ; 返回:无
    ; 影响:堆栈
    push Acc
    push B
    mov A, R0
    push Acc
    mov A, R1
    push Acc
    mov A, R2
    push Acc
    mov A, R3
    push Acc
    ret
Restore:
    ; 恢复现场
    ; 参数:无
    ; 返回:无
    ; 影响:A, B, R0~3, 堆栈
    pop Acc
    mov R3, A
    pop Acc
    mov R2, A
    pop Acc
    mov R1, A
    pop Acc
    mov R0, A
    pop B
    pop Acc
    ret

  可以根据需要增删需要保护的寄存器,只需要满足先进后出即可。如果不需要保护这么多寄存器,也可以利用 RS0、RS1 来切换寄存器组。

查表程序

  查表程序是一种常用程序,可以完成数据计算、转换、补偿等各种功能,具有程序简单、执行速度快等优点。在AT89C51中,数据表格存放在程序存储器ROM中。编写程序时,可以通过 DBDW 伪指令,以表格的形式将数据列于 ROM。用于查表的指令有 MOVC A,@A+DPTRMOVC A,@A+PC

  用 DPTR 做基址寄存器时,寻址范围为 64kB 空间,查表分 3 个步骤:① 基址值(表格首地址)→DPTR中;② 变址值(项与表格首地址的间隔)→A;③ 执行 MOVC A, @A+DPTR。示例:将一位 16 进制数转换为 ASCII 码。

ORG 00H
MOV R0,#0BH ;设(R0)=BH
MOV A,R0 ;读数据
ANL A,#0FH ;屏蔽高4位
MOV DPTR,#TAB ;置表格首地址
MOVC A,@A+DPTR ;查表
MOV R0,A ;回存
SJMP $

ORG 50H
TAB: 
    DB 30H,31H,32H,33H,34H,35H,36H,37H,38H,39H ;0~9的ascii码
    DB 41H,42H,43H,44H,45H,46H ;A~F的ascii码
END

  用 PC 做基址寄存器时,基址 PC 是当前程序计数器的内容,即查表指令的下一条指令的首址,查表范围是查表指令后 256B 空间。查表分3个步骤:① 变址值(项与表格首地址的间隔)→(A);② 偏移量(查表下一条指令的首地址到表格首地址的间隔)+(A)→(A);③ 执行MOVC A,@A+PC。示例:将一位16进制数转换为ASCII码。

ORG 00H
MOV R0,#07H ;设(R0)=7H
MOV A,R0 ;读数据
ANL A,#0FH ;屏蔽高4位
ADD A,#03H ;加偏移量
MOVC A,@A+PC ;查表
MOV R0,A ;回存,1字节
SJMP $ ;2字节

TAB:
DB 30H,31H,32H,33H,34H ;0~4的ascii码
DB 35H,36H,37H,38H,39H ;5~9的ascii码
DB 41H,42H,43H,44H,45H,46H ;A~f的ascii码
END

BCD相关

压缩BCD转非压缩BCD

BCD_Extract:
    ; 单字节压缩BCD转非压缩BCD(所以函数名叫解压)
    ; 参数:R0 压缩BCD的地址
    ;      R1 非压缩BCD的低位
    ;      R2 非压缩BCD的高位
    ; 注释:不会改变原压缩BCD,会改变 R1 的值
    mov A, @R0
    mov @R1, #0
    xchd A, @R1
    swap A
    mov R1, R2
    xch A, @R1
    ret
BCD_ExtractSeq:
    ; 连续压缩BCD转非压缩BCD(seq 即 sequence)
    ; 参数:R0 压缩BCD的最低位地址
    ;      R1 非压缩BCD的最低位地址
    ;      R2 压缩BCD的字节数(非压缩BCD字节数除以2)
    ; 注释:由于是小端,所以会往高地址存放结果
    ;      不会影响原压缩BCD的值
    BCD_ExtractSeq_Fetch:
        mov A, @R0
    BCD_ExtractSeq_Convert:
        mov @R1, #0
        xchd A, @R1
        swap A
        inc R1
        mov @R1, #0
        xchd A, @R1
    BCD_ExtractSeq_Next:
        inc R0
        DJNZ R2, BCD_ExtractSeq_Fetch;
    ret

非压缩BCD转压缩BCD

BCD_Compress:
    ; 单字节非压缩BCD转压缩BCD(所以函数名叫压缩)
    ; 参数:R0 非压缩BCD的低位
    ;      R1 非压缩BCD的高位
    ; 返回:A 压缩BCD
    ; 注释:不会改变原非压缩BCD
    mov A, @R1
    swap A
    orl A, @R0
    ret
BCD_CompressSeq:
    ; 连续非压缩BCD转压缩BCD(seq 即 sequence)
    ; 参数:R0 压缩BCD的最低位地址
    ;      R1 非压缩BCD的最低位地址
    ;      R2 压缩BCD的字节数(非压缩BCD字节数除以2)
    ; 注释:由于是小端,所以会往高地址存放结果
    ;      不会影响原非压缩BCD的值
    BCD_CompressSeq_Fetch_Odd:
        mov A, @R1
        djnz R2, BCD_CompressSeq_Fetch_Even
        mov @R0, A
        ret
    BCD_CompressSeq_Fetch_Even:
        inc R1
        swap A
        orl A, @R1
        swap A
        mov @R0, A
        inc R0
        djnz R2, BCD_CompressSeq_Fetch_Odd
    ret

单字节二进制转BCD

  MCU 内部进行数据计算和存储时,多采用二进制码;在数据的输入/输出中,多采用 BCD 码。编程中经常会遇到各种码制的转换问题。

  十进制数常用 BCD 码表示,BCD 码有两种形式:一种是一个字节放一位 BCD 码,适用于显示或输出;另一种是压缩的 BCD 码,一个字节放两个 BCD 码,节省存储单元。

  单字节二进制(或 16 进制)数转换为 BCD 码的一般方法是把二进制数除以 100,得到百位数,余数除以 10 的商和余数分别是十位数、个位数。

Byte2BCD_compressed:
    ; 单字节转BCD
    ; 参数:A 待转换的字节
    ; 返回:A 低四位和高四位分别存放个位和十位,B 低四位存放百位
    ; 影响:A, B, R0, PSW
    mov B, #100 ;100作为除数送入B中
    div AB      ;16进制数除以100
    mov R0, A   ;百位数送R0,余数在B中
    mov A, #10  ;分离十位数和个位数
    xch A, B    ;余数送入A中,除数10放在B中
    div AB      ;分离出十位放在A中,各位放在B中
    swap A      ;十位交换到A的高4位
    add A, B    ;将个位送入A的低4位
    mov B, R0   ;将百位送入B
    ret

BCD转单字节二进制

  将压缩BCD码按其高、低4位分别转换为二进制数。示例:

STAR: MOV R2,#89H ;表示BCD码为89
MOV A,R2 ;(A)←(R2)
ANL A,#0F0H ;屏蔽低4位
SWAP A ;高4位与低4位交换
MOV B,10 ;乘数
MUL AB ;相乘
MOV R3,A ;(R3)←(A)
MOV A,R2 ;(A)←(R2)
ANL A,#0FH ;屏蔽高4位
ADD A,R3 ;(A)←(A)+(R3)
MOV R3,A ;(R3)←(A)
SJMP $
END

数据排序程序:

  经常要对数据进行排序,排序的方法有按从小到大的次序和按从大到小的次序排。示例为将放于片内RAM的50H~5AH单元中的单字节无符号正整数按从小到大的次序重新排列(冒泡法)。

ORG 00H
SORT: MOV R0,#50H ;指针送R0
MOV R7,#0AH ;每次冒泡比较的次数
CLR F0 ;交换标志清0
LOOP: MOV A,@R0 ;取前一个数
MOV R2,A ;暂存前一个数于R2
INC R0 ;取后一个数
MOV 30H,@R0 ;后一个数暂存于30H
CLR C ;清进位为0
CJNE A,30H,LP1 ;前后两数比较
SJMP LP2
LP1: JC LP2 ;前数≦后数,不交换
MOV A,@R0
DEC R0 ;前数>后数,交换
XCH A,@R0
INC R0
MOV @R0,A
SETB F0 ;置交换标志
LP2: DJNZ R7,LOOP ;进行下一次比较
JB F0,SORT ;一趟循环中有交换进行下一趟冒泡
SJMP $ ;无交换退出
END

算术计算程序:

  示例1:求解Y=(3×X+4)×5÷8+1。X的取值范围为0~15,存放在30H,设X=4.

ORG 00H
LJMP STAR
ORG 100H
STAR: MOV 30H,#4 ;X=4, (30H)=4
MOV A,30H
CLR C
RLC A ;2X
ADD A,30H ;(A)=3X
MOV 31H,A ;(31H)=(A)=3X
MOV A,#4
ADD A,31H ;(A)=3X+4
MOV B,#5
MUL AB ;(A)=5(3X+4)
MOV B,#8
DIV AB ;(A)=5(3X+4)/8
DEC A ;(A)=[5(3X+4)/8]-1
MOV 31H,A ;结果在31H中,余数在B中
SJMP $
END

  示例2:实现。设a、b、c存于片内RAM的3个单元R2、R3、R4中。用查平方表子程序来得到平方值,在主程序中完成相加。(设a、b为0~9之间的数,a=6,b=4)

ORG 00H
MOV R2,#6 ;赋值(R2)=6
MOV R3,#4 ;赋值(R3)=4
MOV A,R2 ;取第一个被加的数据a
ACALL SQR ;第一次调用,得到a的平方值
MOV R1,A ;暂存于R1中
MOV A,R3 ;取第二个被加的数据b
ACALL SQR ;第二次调用,得到b的平方值
ADD A,R1 ;完成相加
MOV R4,A ;存结果到R4
SJMP $
SQR: INC A ;查表位置调整
MOVC A,@A+PC ;查平方表
RET ;子程序返回

TAB: DB 0,1,4,9,16,25,36,49,64,81
END

  示例3:n个正整数求和。正整数皆为单字节数,按顺序存放在片内RAM以50H为首地址的连续存储单元中,数据个数n存于R2中。双字节和数存放在R3、R4中。

ORG 00H
MOV 50H,#23H ;为寄存器50H~54H预置数据
MOV 51H,#05H
MOV 52H,#0FFH
MOV 53H,#44H
MOV 54H,#60H

;以下4条指令为置循环初值
MOV R2,#5 ;数据个数计数器R2置数
MOV R3,#00H ;结果高位存储器R3清零
MOV R4,#00H ;结果低位存储器R4清零
MOV R0,#50H ;寄存器(R0)=50H

;以下6条指令为循环体
LOOP: MOV A,R4
ADD A,@R0
MOV R4,A
CLR A
ADDC A,R3
MOV R3,A

;以下3条分别为循环修改、循环控制、退出循环

INC R0 ;循环修改
DJNZ R2,LOOP ;循环控制
SJMP $ ;退出循环
END

  示例4:双字节数取补子程序,数存R4R5,结果送回

CMPT: MOV  A,R5
CPL  A
ADD  A,#1
MOV  R5,A
MOV A,R4
CPL A
ADDC A,#0
MOV R4,A
RET

  正数的补码与原码相同,负数的补码是按位取反再最低位加1。采用补码后,加减运算简单。

  示例5:双字节原码左移一位子程序,数值在R2R3,结果送回,不改变符号位

DRL1: MOV A,R3
CLR C
RLC A
MOV R3,A
MOV A,R2
RLC A
MOV ACC.7,C ;恢复符号
MOV R2,A
RET

  对于二进制数,左移一位相当于乘以2,右移一位相当于除以2,对于带符号数要保持符号位不变。

  示例6:双字节原码右移一位子程序。数值在R2R3,结果送回,不改变符号位

DRR1: MOV A,R2
MOV C,ACC.7 ;保护符号位
CLR ACC.7 ;移入0
RRC A
MOV R2,A
MOV A,R3
RRC A
MOV R3,A
RET

  示例7:双字节补码右移一位子程序。数值在R2R3,结果送回,不改变符号位

CRR1: MOV A,R2
MOV C,ACC.7 ;保护符号位
RRC A ;移入符号位
MOV R2,A
MOV A,R3
RRC A
MOV R3,A
RET

  示例8:双字节无符号数相加子程序。数存R2R3和R6R7,结果送R4R5

NADD: MOV A,R3
ADD A,R7
MOV R5,A
MOV A,R2
ADDC A,R6
MOV R4,A
RET

  示例9:双字节无符号数相减子程序。数存R2R3和R6R7,结果送R4R5

NSUB1: MOV A,R3
CLR C
SUBB A,R7
MOV R5,A
MOV A,R2
SUBB A,R6
MOV R4,A
RET

  示例10:双字节原码加减运算子程序。数存R2R3和R6R7,结果送R4R5

DSUB: MOV A,R6 ;减法入口
CPL ACC.7 ;取反符号位
MOV R6,A
DADD: MOV A,R2 ;加法入口
MOV C,ACC.7
MOV F0,C ;保存被加数符号位
XRL A,R6
MOV C,ACC.7 ;C=1,两数异号;C=1,两数同号
MOV A,R2
CLR ACC.7 ;清0被加数符号
MOV R2,A
MOV A,R6
CLR ACC.7 ;清1加数符号
MOV R6,A
JC DAB2
ACALL NADD ;同号执行加法
MOV A,R4
JB ACC.7,DABE
DAB1: MOV C,F0 ;恢复运算结果
MOV ACC.7,C
MOV R4,A
RET
DABE: SETB C
RET ;溢出

DAB2: ACALL NSUB1 ;异号执行减法
MOV A,R4
JNB ACC.7,DAB1
ACALL CMPT ;不够减,取补
CPL F0 ;符号位取反
SJMP DAB1

  示例11:无符号二进制数乘法。数存R2R3和R6R7,结果送R4R5R6R7

NMUL: MOV R4,#0
MOV R5,#0
MOV R0,#16 ;16位二进制数
CLR C
NMLP: MOV A,R4 :右移一位
RRC A
MOV R4,A
MOV A,R5
RRC A
MOV R5,A
MOV A,R6
RRC A
MOV R6,A
MOV A,R7
RRC A
MOV R7,A
JNC NMLN ;C为移出的乘数最低位,若为0,不执行加法
MOV A,R5 ;执行加法
ADD A,R3
MOV R5,A
MOV A,R4
ADDC A,R2
MOV R4,A
NMLN: DJNZ R0,NMLP ;执行16次
MOV A,R4 ;最后再右移一位
RRC A
MOV R4,A
MOV A,R5
RRC A
MOV R5,A
MOV A,R6
RRC A
MOV R6,A
MOV A,R7
RRC A
MOV R7,A
RET

  示例12:无符号双字节快速乘法。数存R2R3和R6R7,结果送R4R5R6R7

QMUL: MOV A,R3
MOV B,R7
MUL AB ;R3XR7
XCH A,R7 ;R7=(R3XR7)L
MOV R5,B ;R5=(R3XR7)H
MOV B,R2
MUL AB ;R2XR7
ADD A,R5
MOV R4,A
CLR A
ADDC A,B
MOV R5,A ;R5=(R2XR7)H
MOV A,R6
MOV B,R3
MUL AB ;R3XR6
ADD A,R4
XCH A,R6
XCH A,B
ADDC A,R5
MOV R5,A
MOV F0,C ;暂存CY
MOV A,R2 ;R2XR6
MUL AB
ADD A,R5
MOV R5,A
CLR A
MOV ACC.0,C
MOV C,F0 ;加以前加法的进位
ADDC A,B
MOV R4,A
RET

  示例13:双字节原码乘法。数存R2R3和R6R7,结果送R4R5R6R7

IMUL: MOV A,R2
XRL A,R6
MOV C,ACC.7
MOV F0,C ;暂存积的符号
MOV A,R2
CLR ACC.7 ;清0被乘数符号位
MOV R2,A
MOV A,R6
CLR ACC.7 ;清0乘数符号位
MOV R6,A
ACALL NMUL ;调用无符号双字节乘法子程序
MOV A,R4
MOV C,F0 ;回送积的符号
MOV ACC.7,C
MOV R4,A
RET

  对源码表示的带符号的二进制数乘法,在乘法之前按负负得正、正负得负得出积的符号,然后清符号位,执行无符号乘法,最后送积的符号。

  示例14:双字节无符号数比较法相除,数存R2R3和R6R7,商送R4R5,余数送R6R7

NDIV1: MOV A,R3 ;先比较是否发生溢出
CLR C
SUBB A,R7
MOV A,R2
SUBB A,R6
JNC NDVE1
MOV B,#16 ;无溢出,执行除法
NDVL1: CLR C ;执行左移1位,移入为0
MOV A,R5
RLC A
MOV R5,A
MOV A,R4
RCL A
MOV R4,A
MOV A,R3
RLC A
MOV R3,A
XCH A,R2
RLC A
XCH A,R2
MOV F0,C ;保存移出的最高位
CLR C
SUBB A,R7 ;比较部分余数与除数
MOV R1,A
MOV A,R2
SUBB A,R6
JB F0,NDVM1
JC NDVD1
NDVM1: MOV R2,A ;执行减法(回送减法结果)
MOV A,R1
MOV R3,A
INC R5 ;商为1
NDVD1: DJNZ B,NDVL1 ;循环16次
CLR F0 ;正常出口
RET

NDVE1: SETB F0 ;溢出
RET

  示例15:双字节原码相除。数存R2R3和R6R7,商送R4R5,余数送R6R7

IDIV: MOV A,R2
XRL A,R6
MOV C,ACC.7
MOV 00H,C ;保存符号位
MOV A,R2
CLR ACC.7 ;清0被除数符号位
MOV R2,A
MOV A,R6
CLR ACC.7 ;清0除数符号位
MOV R6,A
ACALL NDIV1 ;调用无符号双字节除法子程序
MOV A,R4
JB ACC.7,IDIVE
MOV C,00H ;回送商的符号
MOV ACC.7,C
MOV R4,A
RET

IDIVE: SETB F0 ;溢出
RET

  有符号原码除法与原码乘法一样,只要在除法之前先计算商的符号,然后清0符号位,执行不带符号的除法,最后送商的符号。