单片机应用系统是合理的硬件与完善的软件的有机组合。软件就是各种指令依某种规律组合形成的程序。程序设计(或软件设计)的任务是利用计算机语言对系统预完成的任务进行描述和规定。
80C51单片机的程序设计主要采用两种语言, 一种是汇编语言,另一种是高级语言(如C51)。采用高级语言进行程序设计,对系统硬件资源的分配比用汇编语言简单,且程序的阅读和修改比较容易,适于编写较大一点的程序。汇编语言生成的目标程序占存储空间少、运行速度快,具有效率高、实时性强的优点,适于编写短小高效的程序。
由于汇编语言是面向机器的语言,对单片机的硬件资源操作直接方便、概念清晰,尽管对编程人员的硬件知识要求较高,但对于学习和掌握单片机的硬件结构极为有利。所以,这里我们仅对汇编语言进行介绍。
4.1 程序编制的方法和技巧
4.1.1 程序编制的步骤
一、预完成任务的分析
首先,要对单片机应用系统预完成的任务进行深入的分析,明确系统的设计任务、功能要求和技术指标。其次,要对系统的硬件资源和工作环境进行分析。这是单片机应用系统程序设计的基础和条件。
二、进行算法的优化
算法是解决具体问题的方法。一个应用系统经过分析、研究和明确规定后, 对应实现的功能和技术指标可以利用严密的数学方法或数学模型来描述,从而把一个实际问题转化成由计算机进行处理的问题。同一个问题的算法可以有多种,结果也可能不尽相同,所以,应对各种算法进行分析比较,并进行合理的优化。比如,用迭代法解微分方程,需要考虑收敛速度的快慢(即在一定的时间里能否达到精度要求)。而有的问题则受内存容量的限制而对时间要求并不苛刻。对于后一种情况,速度不快但节省内存的算法则应是首选。
三、程序总体设计及流程图绘制
经过任务分析、算法优化后,就可以进行程序的总体构思,确定程序的结构和数据形式,并考虑资源的分配和参数的计算等。然后根据程序运行的过程, 勾画出程序执行的逻辑顺序,用图形符号将总体设计思路及程序流向绘制在平面图上,从而使程序的结构关系直观明了,便于检查和修改。
通常,应用程序依功能可以分为若干部分,通过流程图可以将具有一定功能的各部分有机地联系起来,并由此抓住程序的基本线索,对全局可以有一个完整的了解。清晰正确的流程图是编制正确无误的应用程序的基础和条件,所以,绘制一个好的流程图,是程序设计的一项重要内容。
流程图可以分为总流程图和局部流程图。总流程图侧重反映程序的逻辑结构和各程序模块之间的相互关系。局部流程图反映程序模块的具体实施细节。对于简单的应用程序,可以不画流程图。但当程序较为复杂时,绘制流程图是一个良好的编程习惯。
常用的流程图符号有开始和结束符号、工作任务符号、判断分支符号、程序连接符号、程序流向符号等,如图 4.1 所示。
此外,还应编制资源分配表,包括数据结构和形式、参数计算、通信协议、各子程序的入口和出口说明等。
4.1.2 编制程序的方法和技巧
一、采用模块化程序设计方法
单片机应用系统的程序一般由包含多个模块的主程序和各种子程序组成。每一程序模块都要完成一个明确的任务,实现某个具体的功能,如发送、接收、延时、打印、显示等。采用模块化的程序设计方法,就是将这些不同的具体功能程序进行独立的设计和分别调试,最后将这些模块程序装配成整体程序并进行联调。
模块化的程序设计方法具有明显的优点。把一个多功能的、复杂的程序划分为若干个简单的、功能单一的程序模块,有利于程序的设计和调试,有利于程序的优化和分工,提高了程序的阅读性和可靠性,使程序的结构层次一目了然。所以,进行程序设计的学习,首先要树立起模块化的程序设计思想。
二、尽量采用循环结构和子程序
采用循环结构和子程序可以使程序的长度减少,占用内存空间减少。对于多重循环,要注意各重循环的初值和循环结束条件,避免出现程序无休止循环的“死循环”现象。对于通用的子程序,除了用于存放子程序入口参数的寄存器外,子程序中用到的其它寄存器的内容应压入堆栈进行现场保护,并要特别注意堆栈操作的压入和弹出的平衡。对于中断处理子程序除了要保护程序中用到的寄存器外,还应保护标志寄存器。这是由于在中断处理过程中难免对标志寄存器中的内容产生影响,而中断处理结束后返回主程序时可能会遇到以中断前的状态标志为依据的条件转移指令,如果标志位被破坏,则程序的运行就会发生混乱。
4.1.3 汇编语言的语句格式
80C51 单片机汇编语言的语句行由 4 个字段组成,汇编程序能对这种格式正确地进行识别。这 4 个字段的格式为:
[标号:]操作码 [操作数] [;注释]
括号内的部分可以根据实际情况取舍。每个字段之间要用分隔符分隔,可以用作分隔符的符号有空格、冒号、逗号、分号等。如:
LOOP:MOV A,# 7FH ;A←7FH
一、标号
标号是语句地址的标志符号,用于引导对该语句的非顺序访问。有关标号的规定为:
(1)标号由 1~8 个 ASCII 字符组成。第一个字符必须是字母,其余字符可以是字母、数字或其它特定字符;
(2)不能使用该汇编语言已经定义了的符号作为标号。如指令助记符、寄存器符号名称等;
(3)标号后边必须跟冒号。
二、操作码
操作码用于规定语句执行的操作。它是汇编语句中惟一不能空缺的部分。它用指令助记符表示。
三、操作数
操作数用于给指令的操作提供数据或地址。在一条汇编语句中操作数可能是空缺的,也可能包括一项,还可能包括两项或三项。各操作数间以逗号分隔。操作数字段的内容可能包括以下几种情况:
(1)工作寄存器名;
(2)特殊功能寄存器名;
(3)标号名;
(4)常数;
(5)符号“$”,表示程序计数器 PC 的当前值;
(6)表达式。
四、注释
注释不属于汇编语句的功能部分,它只是对语句的说明。注释字段可以增加程序的可读性,有助于编程人员的阅读和维护。注释字段必须以分号“;”开头,长度不限,当一行书写不下时,可以换行接着书写,但换行时应注意在开头使用分号“;”。
五、数据的表示形式
80C51 汇编语言的数据可以有以下几种表示形式:
- 二进制数,末尾以字母 B 标识。如:1000 1111B;
- 十进制数,末尾以字母 D 标识或将字母 D 省略。如:88D,66;
- 十六进制数,末尾以字母 H 标识。如:78H,0A8H(应注意的是,十六进制数以字母 A~F 开头时应在其前面加上数字“0”);
- ASCII 码,以单引号括起来标识。如:‘AB’,‘1245’。
4.2 源程序的编辑和汇编
由于通用微型计算机的普及,现在单片机应用系统的程序设计都借助于通用微型计算机来完成。首先,在微机上利用各种编辑软件编写单片机的汇编语言源程序,然后使用交叉汇编程序对源程序进行汇编,并将获得的目标程序经仿真器或通用编程器写到单片机或程序存储器中,进而完成应用程序的调试。
4.2.1 源程序的编辑与汇编
一、源程序的编辑
源程序的编写要依据 80C51 汇编语言的基本规则,特别要用好常用的汇编命令(即伪指令)。例如下面的程序段:
ORG 0040H
MOV A, #7FH
MOV R1, #44H
END
这里的 ORG 和 END 是两条伪指令,其作用是告诉汇编程序此汇编源程序的起止位置。编辑好的源程序应以“ . ASM ”扩展名存盘,以备汇编程序调用。
二、源程序的汇编
将汇编语言源程序转换为单片机能执行的机器码形式的目标程序的过程叫汇编。汇编常用的方法有两种,一是手工汇编,二是机器汇编。
手工汇编时,把程序用助记符指令写出后,通过手工方式查指令编码表, 逐个把助记符指令翻译成机器码,然后把得到的机器码程序(以十六进制形式) 键入到单片机开发机中,并进行调试。由于手工汇编是按绝对地址进行定位的, 所以,对于偏移量的计算和程序的修改非常不便。通常只在程序较小或开发条件有限制时才使用。
机器汇编是在常用的个人计算机 PC 上,使用交叉汇编程序将汇编语言源程序转换为机器码形式的目标程序。此时汇编工作由计算机完成,生成的目标程序由 PC 机传送到开发机上,经调试无误后,再固化到单片机的程序存储器ROM 中。机器汇编与手工汇编相比具有极大的优势,所以是汇编工作的首选。
源程序经过机器汇编后,形成的若干文件中含有两个主要文件,一个是列表文件,另一个是目标码文件。因汇编软件的不同,文件的格式及信息会有一些不同,但主要信息如下:
列表文件主要信息为:
地 址 目标码 汇编程序
ORG 0040H
0040H 747F MOV A,#7FH
0042H 7944 MOV R1,#44H
END
目标码文件主要信息为:
首地址 末地址 目标码
0040H 0044H 747F7944
该目标码文件由 PC 机的串行口传送到开发机后,接下来的任务就是仿真调试了。
4.2.2 伪指令
我举一个例子就是,我们在读报告的时候有时候会在页脚写上“下转第三页”,就是跳过第二页直接看第三页,但是我们并不把“下转第三页”这五个字读出来,那么“转第三页”这五个字就相当于伪指令。怎么样是不是通俗易懂!!!
伪指令是汇编程序能够识别并对汇编过程进行某种控制的汇编命令。它不是单片机执行的指令,所以没有对应的可执行目标码,汇编后产生的目标程序中不会再出现伪指令。标准的 80C51 汇编程序定义了许多伪指令,下面仅对一些常用的进行介绍。
一、起始地址设定伪指令 ORG
格式为:
ORG 表达式
该指令的功能是向汇编程序说明下面紧接的程序段或数据段存放的起始地址。表达式通常为 16 进制地址,也可以是已定义的标号地址。如:
ORG 8000H
START:MOV A,#30H
此时规定该段程序的机器码从地址 8000H 单元开始存放。
在每一个汇编语言源程序的开始,都要设置一条 ORG 伪指令来指定该程序在存储器中存放的起始位置。若省略 ORG 伪指令,则该程序段从 0000H 单元开始存放。在一个源程序中,可以多次使用 ORG 伪指令规定不同程序段或数据段存放的起始地址,但要求地址值由小到大依序排列,不允许空间重叠。
二、汇编结束伪指令 END
格式为: END
该指令的功能是结束汇编。
汇编程序遇到 END 伪指令后即结束汇编。处于 END 之后的程序,汇编程序将不处理。
三、字节数据定义伪指令 DB
格式为: [标号:] DB 字节数据表
功能是从标号指定的地址单元开始,在程序存储器(ROM)中定义字节数据。
字节数据表可以是一个或多个字节数据、字符串或表达式。该伪指令将字节数据表中的数据根据从左到右的顺序依次存放在指定的存储单元中,一个数据占一个存储单元。例如:
DB “how are you?”
把字符串中的字符以 ASCII 码的形式存放在连续的 ROM 单元中。又如:
DB –2,–4,–6,8,10,18
把 6 个数转换为十六进制表示(FEH,FCH,FAH,08H,0AH,12H),并连续地存放在 6 个 ROM 单元中。
该伪指令常用于存放数据表格。如要存放显示用的十六进制的字形码,可以用多条 DB 指令完成:
DB 0C0H,0F9H,0A4H,0B0H
DB 99H,92H,82H,0F8H
DB 80H,90H,88H,83H
DB 0C6H,0A1H,86H,84H
四、字数据定义伪指令 DW
格式为: [标号:] DW 字数据表
功能是从标号指定的地址单元开始,在程序存储器中定义字数据。
该伪指令将字或字表中的数据根据从左到右的顺序依次存放在指定的存储单元中。应特别注意:16 位的二进制数,高 8 位存放在低地址单元,低 8 位存放在高地址单元。
例如:
ORG 1400H
DATA: DW 24AH,3CH
汇编后,(1400H)=32H,(1401H)= 4AH,(1402H)=00H,(1403H)=3CH。
五、空间定义伪指令 DS
格式为:[标号:] DS 表达式
功能是从标号指定的地址单元开始,在程序存储器中保留由表达式所指定的个数的存储单元作为备用空间,并都填以零值。
例如:
ORG 3000H
BUF: DS 50
汇编后,从地址 3000H 开始保留 50 个存储单元作为备用单元。
六、赋值伪指令 EQU
格式为:符号名 EQU 表达式
功能是将表达式的值或特定的某个汇编符号定义为一个指定的符号名。例如:
LEN EQU 10
SUM EQU 21H
BLOCK EQU 22H
CLR A
MOV R7,#LEN
MOV R0,#BLOCK
LOOP:ADD A,@R0
INC R0
DJNZ R7,LOOP
MOV SUM,A
END
该程序的功能是,将 BLOCK 单元开始存放的 10 个无符号数进行求和,并将结果存入 SUM 单元中。
七、位地址符号定义伪指令 BIT
格式为: 符号名 BIT 位地址表达式
功能是将位地址赋给指定的符号名。其中,位地址表达式可以是绝对地址, 也可以是符号地址。
例如:
ST BIT P1.0
将P1.0 的位地址赋给符号名 ST,在其后的编程中就可以用ST 来代替P1.0。
4.3 基本程序结构
4.3.1 顺序程序
顺序程序是指无分支、无循环结构的程序。其执行流程是依指令在存储器中的存放顺序进行的。
一、数据传送
例 内部 RAM 的 2AH~2EH 单元中存储的数据如图 4.2 所示。试编写程序实现图 4.3 所示的数据传送结果。
方法一:
MOV A,2EH ;2 字节,1 个机器周期
MOV 2EH,2DH ;3 字节,2 个机器周期
MOV 2DH,2CH ;3 字节,2 个机器周期
MOV 2CH,2BH ;3 字节,2 个机器周期
MOV 2BH,#00H ;3 字节,2 个机器周期
方法二:
CLR A ;1 字节,1 个机器周期
XCH A,2BH ;2 字节,1 个机器周期
XCH A,2CH ;2 字节,1 个机器周期
XCH A,2DH ;2 字节,1 个机器周期
XCH A,2EH ;2 字节,1 个机器周期
以上两种方法均可以实现所要求的传送任务。方法一使用 14 个字节的指令代码,执行时间为 9 个机器周期;方法二仅用了 9 个字节的代码,执行时间也减少到了 5 个机器周期。实际应用中应尽量采用指令代码字节数少、执行时间短的高效率程序,即注意程序的优化。
二、查表程序
例 有一变量存放在片内 RAM 的 20H 单元,其取值范围为:00H~05H。要求编制一段程序,根据变量值求其平方值,并存入片内 RAM 的 21H 单元。
程序如下:
在程序存储器的一片存储单元中建立起该变量的平方表。用数据指针 DPTR 指向平方表的首址,则变量与数据指针之和的地址单元中的内容就是变量的平方值。程序流程图如图 4.4 所示。
采样 MOVC A,@A+PC 指令也可以实现查表功能,且不破坏 DPTR 的内容,从而可以减少保护 DPTR 的内容所需的开销。但表格只能存放在 MOVC A,@A+PC 指令后的 256 字节内,即表格存放的地点和空间有一定的限制。
三、简单运算
由于 80C51 指令系统中只有单字节加法指令,因此对于多字节的相加运算必须从低位字节开始分字节进行。除最低字节可以使用 ADD 指令外,其它字节相加时要把低字节的进位考虑进去,这时就应该使用 ADDC 指令。
例 双字节无符号数加法。
设被加数存放在内部 RAM 的 51H、50H 单元,加数存放在内部 RAM 的61H、60H 单元,相加的结果存放在内部 RAM 的 51H、50H 单元,进位存放在位寻址区的 00H 位中。实现此功能的程序段如下:
MOV R0, #50H ;被加数的低字节地址
MOV R1, #60H ;加数的低字节地址
MOV A, @R0 ;取被加数低字节
ADD A, @R1 ;加上加数低字节
MOV @R0, A ;保存低字节相加结果
INC R0 ;指向被加数高字节
INC R1 ;指向加数高字节
MOV A, @R0 ;取被加数高字节
ADDC A, @R1 ;加上加数高字节(带进位加)
MOV @R0, A ;存高字节相加结果
MOV 00H, C ;保存进位
4.3.2 分支程序
通常情况下,程序的执行是按照指令在程序存储器中存放的顺序进行的, 但根据实际需要也可以改变程序的执行顺序,这种程序结构就属于分支结构。分支结构可以分成单分支、双分支和多分支几种情况。
单分支结构如图 4.5 所示。若条件成立,则执行程序段 A,然后继续执行该指令下面的指令;如条件不成立,则不执行程序段 A,直接执行该指令的下条指令。
双分支结构如图 4.6 所示。若条件成立,执行程序段 A;否则执行程序段 B。
多分支结构如图 4.7 所示。先将分支按序号排列,然后按照序号的值来实现多分支选择。分支程序在单片机系统中有较多的应用,具体的实现上存在着许多技巧, 可以通过阅读一些典型的程序逐渐增加这方面的知识。
一、单分支程序
例 求双字节补码。
设在内部 RAM 的 addr1 和 addr+1 单元存有一个双字节数(高位字节存于高地址单元)。编写程序将其读出取补后再存入 addr2 和 addr2+1 单元。
首先对低字节取补,然后判其结果是否为全“0”。若是,则高字节取补,否则高字节取反。程序段如下:
START: MOV R0,#addr1 ;原码低字节地址送 R0
MOV R1,#addr2 ;补码低字节地址送 R1
MOV A, @R0 ;原码低字节送 A
CPL A ;A 内容取补
INC A
MOV @R1,A ;存补码低字节
INC R0 ;调整地址,指向下一单元
INC R1
JZ ZERO ;(A)=0 时转 ZERO
MOV A,@R0 ;原码高字节送 A
CPL A
MOV @R1,A ;高字节反码存入 addr2+1 单元
SJMP LOOP1
ZERO: MOV A,@R0 ;高字节取补存入 addr2+1 单元
CPL A
INC A
MOV @R1,A
LOOP1:RET
二、双分支程序
例设变量 x 以补码的形式存放在片内 RAM 的 30H 单元,变量 y 与 x 的关系是:当 x > 0 时,y = x;当 x = 0 时,y = 20H;当 x < 0 时,y = x + 5。编制程序,根据 x 的大小求 y 并送回原单元。程序段如下:
START: MOV A,30H
JZ NEXT
ANL A,#80H ;判断符号位
JZ LP
MOV A,#05H
ADD A,30H
MOV 30H,A
SJMP LP
NEXT: MOV 30H,#20H
LP: SJMP $
三、多分支程序
例根据 R7 的内容转向相应的处理程序。
设R7 的内容为 0~N,对应的处理程序的入口地址分别为 PP0~PPN。程序段如下:
START: MOV DPTR,#TAB ;置分支入口地址表首址
MOV A,R7 ;分支转移序号送 A
ADD A,R7 ;分支转移序号乘以 2
MOV R3,A ;暂存于 R3
MOVC A,@A+DPTR
XCH A,R3 ;取高位地址
INC A
MOVC A,@A+DPTR ;取低位地址
MOV DPL,A ;处理程序入口地址低 8 位送 DPL
MOV DPH,R3 ;处理程序入口地址高 8 位送 DPL
CLR A
JMP @A+DPTR
TAB: DW PP0
DW PP1
DW PPN
4.3.3 循环程序
在程序设计中,经常需要控制一部分指令重复执行若干次,以便用简短的程序完成大量的处理任务。这种按某种控制规律重复执行的程序称为循环程序。循环程序有先执行后判断和先判断后执行两种基本结构,如图 4.8 所示。
图 4.8(a)为“先执行后判断”的循环程序结构图。其特点是一进入循环, 先执行循环处理部分,然后根据循环控制条件判断是否结束循环。若不结束, 则继续执行循环操作;若结束,则进行结束处理并退出循环。
图 4.8(b)为“先判断后执行”的循环程序结构图。其特点是将循环的控制部分放在循环的入口处,先根据循环控制条件判断是否结束循环。若不结束, 则继续执行循环操作;若结束,则进行结束处理并退出循环。
一、先执行后判断
例 50ms 延时程序。
若晶振频率为 12MHz,则一个机器周期为 1μs。执行一条 DJNZ 指令需要2 个机器周期,即 2μs。采用循环计数法实现延时,循环次数可以通过计算获得,并选择先执行后判断的循环结构。程序段如下:
DEL: MOV R7,#200 ;1 μs
DEL1:MOV R6,#123 ;1 μs
NOP ;1 μs
DEL2:DJNZ R6,DEL2 ;2 μs,计(2×123)μs
DJNZ R7,DEL1 ;2 μs,计 [(2×123+2+2)×200+1] μs,约 50ms
RET
例 无符号数排序程序。在片内 RAM 中,起始地址为 30H 的 8 个单元中存放有 8 个无符号数。试对这些无符号数进行升序排序。
数据排序常用的方法是冒泡排序法。这种方法的过程类似水中气泡上浮, 故称冒泡法。执行时从前向后进行相邻数的比较,如数据的大小次序与要求的顺序不符就将这两个数互换,否则不互换。对于升序排序,通过这种相邻数的互换,使小数向前移动,大数向后移动。从前向后进行一次冒泡(相邻数的互换),就会把最大的数换到最后。再进行一次冒泡,就会把次大的数排在倒数第二的位置。
设 R7 为比较次数计数器,初始值为 07H,位地址 00H 为数据互换标志位。程序段如下:
START: CLR 00H ;互换标志清 0
MOV R7,#07H ;各次冒泡比较次数
MOV R0,#30H ;数据区首址
LOOP: MOV A,@R0 ;取前数
MOV 2BH,A ;暂存
INC R0
MOV 2AH,@R0 ;取后数
CLR C
SUBB A,@R0 ;前数减后数
JC NEXT
MOV @R0,2BH ;前数小于后数,不互换
DEC R0
MOV @R0,2AH ;两数交换
INC R0 ;准备下一次比较
SETB 00H ;置互换标志
NEXT: DJNZ R7,LOOP ;进行下一次比较
JB 00H,START ;进行下一轮冒泡
SJMP $
二、先判断后执行
例将内部 RAM 中起始地址为 data 的数据串传送到外部 RAM 中起始地址为 buffer 的存储区域内,直到发现‘$ ’字符停止传送。由于循环次数事先不知道,但循环条件可以测试到,所以,采用先判断后执行的结构比较适宜。程序段如下:
MOV R0,#data
MOV DPTR,#buffer
LOOP0: MOV A,@R0
CJNE A,#24H,LOOP1 ;判断是否为‘ $ ’字符
SJMP LOOP2 ;是‘ $ ’字符,转结束
LOOP1: MOVX @DPTR,A ;不是‘ $ ’字符,执行传送
INC R0
INC DPTR
SJMP LOOP0 ;传送下一数据
LOOP2:
4.3.4 子程序及其调用
一、子程序的调用
在实际应用中,经常会遇到一些带有通用性的问题,如数值转换、数值计算等,在一个程序中可能要使用多次。这时可以将其设计成通用的子程序供随时调用。利用子程序可以使程序结构更加紧凑,使程序的阅读和调试更加方便。
子程序的结构与一般的程序并无多大区别,它的主要特点是,在执行过程中需要由其它程序来调用,执行完后又需要把执行流程返回到调用该子程序的主程序。
子程序调用时要注意两点,一是现场的保护和恢复,二是主程序与子程序的参数传递。
二、现场保护与恢复
在子程序执行过程中常常要用到单片机的一些通用单元,如工作寄存器
R0~R7、累加器 A、数据指针 DPTR 以及有关标志和状态等。而这些单元中的内容在调用结束后的主程序中仍有用,所以需要进行保护,称为现场保护。在执行完子程序,返回继续执行主程序前恢复其原内容,称为现场恢复。保护与恢复的方法有以下两种:
1.在主程序中实现
其特点是结构灵活。示例如下:
PUSH PSW ;保护现场
PUSH ACC ;
PUSH B ;
MOV PSW,#10H ;换当前工作寄存器组
LCALL addr16 ;子程序调用
POP B ;恢复现场
POP ACC ;
POP PSW ;
2.在子程序中实现
其特点是程序规范、清晰。示例如下:
SUB1: PUSH PSW ;保护现场
PUSH ACC ;
PUSH B ;
MOV PSW,#10H ;换当前工作寄存器组
POP B ;恢复现场
POP ACC ;
POP PSW ;
RET
应注意的是,无论哪种方法保护与恢复的顺序都要对应,否则程序将会发生错误。
三、参数传递
由于子程序是主程序的一部分,所以,在程序的执行时必然要发生数据上的联系。在调用子程序时,主程序应通过某种方式把有关参数(即子程序的入口参数)传给子程序。当子程序执行完毕后,又需要通过某种方式把有关参数(即子程序的出口参数)传给主程序。在 80C51 单片机中,传递参数的方法有三种。
1.利用累加器或寄存器
在这种方式中,要把预传递的参数存放在累加器 A 或工作寄存器 R0~R7 中,即在主程序调用子程序时,应事先把子程序需要的数据送入累加器 A 或指定的工作寄存器中,当子程序执行时,可以从指定的单元中取得数据,执行运算。反之,子程序也可以用同样的方法把结果传送给主程序。
例 编写程序,实现 c=a2+b2。设 a、b、c 分别存于内部 RAM 的 30H、31H、32H 三个单元中。程序段如下:
START:MOV A,30H ;取 a
ACALL SQR ;调用查平方表
MOV R1,A ;a2 暂存于 R1 中
MOV A,31H ;取 b
ACALL SQR ;调用查平方表
ADD A,R1 ;a2+b2 存于 A 中
MOV 32H,A ;存结果
SJMP $
SQR: MOV DPTR,#TAB ;子程序
MOVC A,@A+DPTR ;
RET
TAB: DB 0,1,4,9,16 ,25,36,49,64,81
2.利用存储器
当传送的数据量比较大时,可以利用存储器实现参数的传递。在这种方式中,事先要建立一个参数表,用指针指示参数表所在的位置。当参数表建立在内部 RAM 时,用 R0 或 R1 作参数表的指针。当参数表建立在外部 RAM 时, 用 DPTR 作参数表的指针。
例 将 R0 和 R1 指向的内部 RAM 中两个 3 字节无符号整数相加,结果送到由 R0 指向的内部 RAM 中。入口时,R0 和 R1 分别指向加数和被加数的低位字节;出口时,R0 指向结果的高位字节。低字节在高地址,高字节在低地址。程序段如下:
NADD: MOV R7,#3 ;三字节加法
CLR C ;
NADD1:MOV A,@R0 ;取加数低字节
ADDC A,@R1 ;被加数低字节加
MOV @R0, A ;
DEC R0
DEC R1
DJNZ R7,NADD1
INC R0
RET
3.利用堆栈
利用堆栈传递参数是在子程序嵌套中常采用的一种方法。在调用子程序前,用 PUSH 指令将子程序中所需数据压入堆栈。进入执行子程序时,再用 POP 指令从堆栈中弹出数据。
例 把内部 RAM 中 20H 单元中的 1 个字节十六进制数转换为 2 位 ASCII码,存放在 R0 指示的两个单元中。程序段如下:
MAIN: MOV A,20H ;
SWAP A
PUSH ACC ;参数入栈
ACALL HEASC
POP ACC
MOV @R0,A ;存高位十六进制数转换结果
INC R0 ;修改指针
PUSH 20H ;参数入栈
ACALL HEASC
POP ACC
MOV @R0,A ;存低位十六进制数转换结果
HEASC: MOV R1,SP ;借用 R1 为堆栈指针
DEC R1
DEC R1 ;R1 指向被转换数据
XCH A,@R1 ;取被转换数据
ANL A,#0FH ;取 1 位十六进制数
ADD A,#2 ;偏移量调整,所加值为 MOVC 与 DB间字节数
MOVC A,@A+PC ;查表
XCH A,@R1 ;1 字节指令,存结果于堆栈
RET ;1 字节指令
ASCTAB:DB 30H,31H,32H,33H,34H,35H,36H,37H
DB 38H,39H,41H,42H,43H,44H,45H,46H
一般说来,当相互传递的数据较少时,采用寄存器传递方式可以获得较快的传递速度。当相互传递的数据较多时,宜采用存储器或堆栈方式传递。如果是子程序嵌套,最好采用堆栈方式。
4. 4 常用程序举例
4.4.1 算术运算程序
一般说来,单片机应用系统的任务就是对客观实际的各种物理参数进行测试和控制。所以,数据的运算是避免不了的。尽管数据运算并不是 80C51 单片机的优势所在,但运用一些编程技巧和方法,对于大部分测控应用中的运算,80C51 单片机还是能够胜任的。
一、多字节数的加、减运算
80C51 单片机的指令系统提供的是字节运算指令,所以在处理多字节数的加减运算时,要合理地运用进位(借位)标志。
例 多字节无符号数的加法。
设两个 N 字节的无符号数分别存放在内部 RAM 中以 DATA1 和 DATA2 开始的单元中。相加后的结果要求存放在 DATA2 数据区。
程序段如下:
MOV R0,#DATA1 ;
MOV R1,#DATA2 ;
MOV R7,#N ;置字节数
CLR C ;
LOOP: MOV A,@R0 ;
ADDC A,@R1 ;求和
MOV @R1,A ;存结果
INC R0 ;修改指针
INC R1 ;
DJNZ R7,LOOP ;
二、多字节数乘法运算
例 双字节无符号数的乘法。
设双字节的无符号被乘数存放在 R3、R2 中,乘数存放在 R5、R4 中,R0 指向积的高位。算法及流程图如图 4.9 所示。
程序段如下:
MULTB: MOV R7,#04 ;结果单元清 0
LOOP: MOV @R0,#00H ;
DJNZ R7,LOOP ;
ACALL BMUL ;
SJMP $
BMUL: MOV A,R2 ;
MOV B,R4 ;
MUL AB ;低位乘
ACALL RADD ;
MOV A,R2 ;
MOV B,R5 ;
MUL AB ;交叉乘
DEC R0 ;
ACALL RADD ;
MOV A,R4 ;
MOV B,R3 ;
MUL AB ;交叉乘
DEC R0 ;
DEC R0 ;
ACALL RADD ;
MOV A,R5 ;
MOV B,R3 ;
MUL AB ;高字节乘
DEC R0 ;
ACALL RADD ;
DEC R0
RET
RADD: ADD A,@R0 ;
MOV @R0,A ;
MOV A,B ;
INC R0 ;
ADDC A,@R0 ;
MOV @R0,A ;
INC R0 ;
MOV A,@R0
ADDC A,#00H
MOV @R0,A
RET
4.4.2 码型转换程序
单片机能识别和处理的是二进制码,而输入输出设备(如 LED 显示器、微型打印机等)则常使用 ASCII 码或 BCD 码。为此,在单片机应用系统中经常需要通过程序进行二进制码与 BCD 码或 ASCII 码的相互转换。
由于二进制数与十六进制数有直接的对应关系,所以,为了书写和叙述方便,下面将用十六进制数代替二进制数。
一、十六进制数与 ASCII 码间的转换
十六进制数与 ASCII 码的对应关系如表 4.1 所示。由表可见,当十六进制数在 0~9 之间时,其对应的 ASCII 码值为该十六进制数加 30H;当十六进制数在 A~F 之间时,其对应的 ASCII 码值为该十六进制数加 37H。
例 将 1 位十六进制数(即 4 位二进制数)转换成相应的 ASCII 码。
设十六进制数存放在 R0 中,转换后的 ASCII 码存放于 R2 中。实现程序如下:
HASC: MOV A, R0 ;取 4 位二进制数
ANL A, #0FH ;屏蔽掉高 4 位
PUSH ACC ;4 位二进制数入栈
CLR C ;清进(借)位位
SUBB A,#0AH ;用借位位的状态判断该数在 0~9 还是A~F 之间
POP ACC ;弹出原 4 位二进制数
JC LOOP ;借位位为 1,跳转至 LOOP
ADD A,#07H ;借位位为 0,该数在 A~F 之间,加 37H
LOOP: ADD A,#30H ;该数在 0~9 之间,加 30H
MOV R2, A ;ASCII 码存于R2
RET
例 将多位十六进制数转换成 ASCII 码。
设地址指针 R0 指向十六进制数低位,R2 中存放字节数,转换后地址指针R0 指向 ASCII 码的高位。R1 指向要存放的 ASCII 码的低位地址。实现程序如下:
HTASC: MOV A,@R0 ;取低 4 位二进制数
ANL A,#0FH ;
ADD A,#15 ;偏移量修正
MOVC A,@A+PC ;查表
MOV @R1,A ;存 ASCII 码
INC R1 ;
MOV A ,@R0 ;取十六进制高 4 位
SWAP A
ANL A,#0FH ;
ADD A,#06H ;偏移值修正
MOVC A,@A+PC ;
MOV @R1,A
INC R0 ;指向下一单元
INC R1 ;
DJNZ R2,HTASC ;ASCII 码存于 R2 RET
ASCTAB:DB 30H,31H,32H,33H,34H,35H,36H,37H
DB 38H,39H,41H,42H,43H,44H,45H,46H
二、BCD 码与二进制数之间的转换
在计算机中,十进制数要用 BCD 码来表示。通常,用 4 位二进制数表示一位 BCD 码,用 1 个字节表示 2 位 BCD 码(称为压缩型 BCD 码)。
例 双字节二进制数转换成 BCD 码。
设(R2R3)为双字节二进制数,(R4R5R6)为转换完的压缩型 BCD 码。
实现程序如下:
DCDTH: CLR A ;
MOV R4,A ;R4 清 0
MOV R5,A ;R5 清 0
MOV R6,A ;R6 清 0
MOV R7,#16 ;计数初值
LOOP: CLR C ;
MOV A,R3 ;
RLC A ;
MOV R3,A ;R3 左移一位并送回
MOV A,R2 ;
RLC A ;
MOV R2,A ;R2 左移一位并送回
MOV A,R6 ;
ADDC A,R6 ;
DA A ;
MOV R6,A ;(R6)乘 2 并调整后送回
MOV A,R5 ;
ADDC A,R5 ;
DA A ;
MOV R5,A ;(R5)乘 2 并调整后送回
MOV A,R4 ;
ADDC A,R4 ;
DA A ;
MOV R4,A ;(R4)乘 2 并调整后送回
DJNZ R7,LOOP ;
本 章 小 结
汇编语言的源程序结构紧凑、灵活,汇编成的目标程序效率高,具有占存储空间少、运行速度快、实时性强等优点。但因它是面向机器的语言,所以它缺乏通用性,编程复杂繁琐,但应用相当广泛。
在进行程序设计时,首先需要对单片机应用系统预完成的任务进行深入的分析,明确系统的设计任务、功能要求、技术指标。然后,要对系统的硬件资源和工作环境进行分析和熟悉。经过分析、研究和明确规定后,利用数学方法或数学模型来对其进行描述,从而把一个实际问题转化成由计算机进行处理的问题。进而,对各种算法进行分析比较,并进行合理的优化。
模块化的程序设计方法具有明显的优点,所以,进行程序设计的学习,开始时就应该建立起模块化的设计思想。采用循环结构和子程序可以使程序的容量大大减少,提高程序的效率,节省内存。
80C51 汇编语言的语句行由 4 个字段组成,汇编程序能对这种格式正确地识别。伪指令是汇编程序能够识别的汇编命令,它不是单片机执行的指令,没有对应的机器码,仅用来对汇编过程进行某种控制。
汇编语言程序设计是实践性较强的一种单片机应用技能,它需要较多的编程训练和实际应用经验的积累。本章仅列出了一些最为基本的程序段示例以供参考。
思考题及习题
- 80C51 单片机汇编语言有何特点?
- 利用 80C51 单片机汇编语言进行程序设计的步骤如何?
- 常用的程序结构有哪几种?特点如何?
- 子程序调用时,参数的传递方法有哪几种?
- 什么是伪指令?常用的伪指令功能如何?
- 设被加数存放在内部 RAM 的 20H、21H 单元,加数存放在 22H、23H 单元,若要求和存放在 24H、 25H 中,试编写出 16 位数相加的程序。
- 编写一段程序,把外部 RAM 中 1000H~1030H 单元的内容传送到内部 RAM 的30H~60H 单元中。
- 编写程序,实现双字节无符号数加法运算,要求 (R1R0)+(R7R6)→(61H60H)。
- 若 80C51 的晶振频率为 6MHz,试计算延时子程序的延时时间。DELAY:MOV R7,#0F6HLP:MOV R6,#0FAH DJNZ R6,$ DJNZ R7,LP RET
- 在内部 RAM 的 21H 单元开始存有一组单字节不带符号数,数据长度为 30H,要求找出最大数存入 IG 单元。
- 编写程序,把累加器 A 中的二进制数变换成 3 位 BCD 码,并将百、十、个位数分别存放在内部 RAM 的 50H、51H、52H 单元中。
- 编写子程序,将 R1 中的 2 个十六进制数转换为 ASCII 码后存放在 R3 和 R4 中。
- 编写程序,求内部 RAM 中 50H~59H 十个单元内容的平均值,并存放在 5AH 单元。