一些规范
- 大小写与命名:
- 命令全部小写,寄存器全部大写
- ROM 中的常量全部大写(因为这是常量)
- RAM 中的变量第一个单词首字母小写,往后每个单词首字母大写,单词之间无分隔(即小驼峰)
- 函数标号每个单词的首字母大写,单词之间无分隔(即大驼峰)
- 如果是 bit 变量,那么在第 3 点的基础上,后面要加
_bit
- 缩进:
- 标号与后面的代码之间要缩进 4 个空格
- 两个标号之间不要求缩进
- 寄存器的使用:
- 开始时最好将
mov SP, #5FH
,将 60H 到 7FH 这 32 个字节用于堆栈 - 20H~2FH 用于 bit 变量,30H~5FH 用于字节变量
- 合理配置 RS1、RS0 来选择用哪组 Rn,建议默认情况下用 00,子函数用 01、10,中断用 11
- 大小端要区分好,我个人使用的是小端,即 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中。编写程序时,可以通过 DB
或 DW
伪指令,以表格的形式将数据列于 ROM。用于查表的指令有 MOVC A,@A+DPTR
和 MOVC 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符号位,执行不带符号的除法,最后送商的符号。