子程序结构也是汇编语言重要的程序组织形式。恰当地使用子程序,可以使整个程序结构清楚,便于阅读和理解,并可减小源程序和机器语言代码的长度。虽然每调用一次子程序都要附加保护断点、现场等操作,增加了程序的执行时间,但从总的方面来说,付出的代价是值得的。
在一个程序中,往往有许多地方需要执行同样的运算或操作。例如,求各种函数、加、减、乘、除运算、代码转换及延时程序等,这些程序是在程序设计中经常可以用到的。如果编程过程中每遇到这样的操作都编写一段程序,则会使编程工作变得十分烦琐,也会占用大量的程序存储器。通常将这些能完成某种基本操作并具有相同操作的程序段单独编制成子程序,以供不同程序或同一程序反复调用。在程序中需要执行这种操作的地方执行一条调用指令,转到子程序中完成规定操作,并能返回原来程序继续执行,这就是所谓的子程序结构。
在程序设计中恰当地使用子程序有如下优点。
(1)不必重复书写同样的程序,提高了编程效率。
(2)编程的逻辑结构简单,便于阅读。
(3)缩短了源程序和目标程序的长度,节省了程序存储器空间。
(4)使程序模块化、通用化,便于交流、共享资源。
(5)便于按某种功能调试。
(1)通用性。子程序必须适应于各种应用程序的调用,因而子程序的参数应是可变的。
(2)可浮动性。子程序可以不加任何修改而放置在存储器的任何区域。这就要求在子程序设计中应避免使用绝对转移指令,子程序的首地址也应该用符号地址。
(3)可递归性和可重入性。可递归性是指子程序可以调用自己,可重入性是指一个子程序可以同时被多个程序调用。这两个特性主要是对大规模复杂系统程序的要求,对一般应用可不作要求。
(1)子程序结构。
用汇编语言编制程序时,要注意以下两个问题。
① 子程序开头的标号区段一定要有一个使用户了解其功能的标志,该标志即子程序的入口地址,以便在主程序中使用绝对调用指令“ACALL”或长调用指令“LCALL”调用子程序。
② 子程序结尾必须使用一条子程序返回指令“RET”,它具有恢复主程序断点的功能,以便断点出栈送至PC,继续执行主程序。一般来说,子程序的调用指令和返回指令要成对使用。
(2)参数传递。
汇编语言程序的子程序结构中,参数的传递要靠程序设计者自己安排数据的存放和工作单元的选择问题。
(3)现场保护。
进入子程序后,应注意除了要处理的参数数据和要传递回主程序的参数外,有关的内部RAM单元和工作寄存器的内容,以及各标志的状态都不应因调用子程序而改变,这就存在现场保护问题。方法是:一进入子程序,就将子程序中所使用的或会被改变内容的工作单元的内容压入堆栈;在子程序完成处理将要返回前,把堆栈中的数据弹出到原来对应的工作单元,恢复原来状态,再返回。对于所使用的工作寄存器的保护可用改变工作寄存器组的方法。
(4)子程序接口说明文件。
子程序接口说明对子程序结构没有实质的影响,它是一些说明子程序功能的文字,便于程序的使用及程序的调试和修改。一般来说,子程序接口的说明主要包括下面几方面。
① 子程序名称。
② 子程序中所使用的寄存器和工作单元。
③ 入口参数及格式。详细说明各入口参数的意义,若传递的是地址或通过堆栈传递的数据,还应说明在内部RAM或堆栈中的参数的格式、顺序、意义等。
④ 出口参数及格式。
⑤ 子程序中所使用的寄存器和工作单元。
⑥ 子程序调用的其他子程序名称。
(5)需要思考和注意的问题。
① 能否从一个子程序内部直接跳转到另一个子程序执行。
② 能否使用转移指令从主程序跳到子程序。
③ 能否使用转移指令从子程序跳回主程序。上述问题,如果从堆栈角度思考,将不难得到正确的结论。
子程序调用时,要特别注意主程序与子程序之间的信息交换问题。在调用一个子程序时,主程序应先把有关参数(子程序入口条件)放到某些特定的位置,子程序在运行时,可以从约定的位置得到有关参数。同样子程序结束前,也应把处理结果(出口条件)送到约定位置。返回后,主程序便可以从这些位置得到需要的结果,这就是参数传递。参数传递大致可分为以下几种方法。
(1)子程序无需传递参数。
这类子程序所需的参数是子程序赋予的,不需要主程序给出。
(2)用累加器和工作寄存器传递参数。
这种方法要求所需的入口参数在转入子程序之前将它们存入累加器A和工作寄存器R0~R7中。在子程序中就用累加器A和工作寄存器R0~R7中的数据进行操作,返回时,出口参数即操作结果也就存放在累加器A和工作寄存器R0~R7中。参数传递采用这种方法最直接、简单,而且运算速度高,但是工作寄存器数量有限,不能传递更多的参数。
【例4-18】 通过调用子程序实现延时100 ms。
解:子程序和主程序如下。
;子程序名称:DLIMS
;功能:延时1ms~256ms,fosc=12MHz
;入口参数:R3=延时的毫秒数(二进制表示)
;出口参数:无
;使用寄存器:R2、R3
;调用:无
DLIMS: MOV R2,#250
LOOP: NOP ;内层循环为1ms=250×(1+l+2)=l 000μs
NOP ;NOP为1μs
DJNZ R2,LOOP ;DJNZ为2μs
DJNZ R3,DLIMS
RET
;主程序
PUSH PSW ;保护程序状态字
MOV PSW,#08H ;选择工作寄存器组1
MOV R3,#100 ;入口参数100
ACALL DLIMS ;调用子程序
POP PSW ;恢复程度状态字
(3)通过操作数地址传递参数。
子程序中所需要的参数存放在数据存储器RAM中。调用子程序之前的入口参数为R0、R1或DPTR间接指出的地址,出口参数(即操作结果)仍为R0、Rl或DPTR间接指出的地址。一般内部RAM由R0、R1作地址指针,外部RAM由DPTR作地址指针。这种方法可以节省传递数据的工作量,可实现变字长运算。
【例4-19】 n字节求补子程序。
解:参考程序如下。
;子程序名称:QUBU
;入口参数:(R0)=求补数低字节指针
(R7)=n-l字节
;出口参数:(R0)=求补后的高字节指针
;使用寄存器:R0、R7
;调用:无
QUBU: MOV A,@R0 ;取低字节数
CPL A ;取反
ADD A,#01H ;加1
MOV @R0,A ;取补后送回
NEXT: INC R0 ;调整数据指针
MOV A,@R0 ;取下一个数
CPL A ;取反
ADDC A,#01H ;加l并加上低位的进位
MOV @R0,A ;取补后送回
DJNZ R7,NEXT ;R7为主程序传递的参数,即n-1
RET
(4)用堆栈传递参数。
堆栈可以作为传递参数的工具。使用堆栈进行参数传递时,主程序使用“PUSH”指令把参数压入堆栈中,子程序可以通过堆栈指针来间接访问堆栈中的参数,并且可以把出口参数送回堆栈中。返回主程序后,可以使用“POP”指令得到这些参数。这种方法的优点是简单、易行,并可传递较多的参数。
注意:通过堆栈传递参数时,不能在子程序的开头通过压入堆栈来保护现场,而应在主程序中先保护现场,然后压入要传递的参数。另外,在子程序返回后,应使堆栈恢复到原来的深度,这样才能保证后续堆栈操作的正确性,如恢复现场等,并且不会因为每调用一次子程序,堆栈深度就会加深,而使堆栈发生溢出。
【例4-20】 在HEX单元存有两个十六进制数,试编程分别把它们转换成ASCII码存入ASC和ASC+1单元。
解:本题子程序采用查表方式完成一个十六进制数的ASCII码转换,主程序完成入口参数的传递和子程序的两次调用,以满足题目要求。相应程序如下:
ORG 1200H
PUSH HEX ;入口参数压栈
ACALL HASC ;求低位十六进制数的ASCII码
POP ASC ;出口参数存入ASC
MOV A,HEX ;十六进制数送A
SWAP A ;高位十六进制数送低4位
PUSH ACC ;入口参数压栈
ACALL HASC ;求高位十六进制数的ASCII码
POP ASC+1 ;出口参数送ASC+1单元
SJMP $ ;结束
HASC: DEC SP
DEC SP ;入口参数地址送SP
POP ACC ;入口参数送A
ANLA,#0FH ;取出入口参数低4位
ADDA,#07H ;地址调整
MOVC A,@A+PC ;查相应ASCII码
PUSH ACC ;出口参数压栈
INC SP
INC SP ;SP指向断点地址高8位
RET ;返回主程序
ASCTAB:DB '0','1','2','3','4','5','6','7'
DB '8','9','A','B','C','D','E','F'
END
在上述程序中,参数是通过堆栈完成传送的,堆栈传送子程序参数时要注意堆栈指针的指向。为简便起见,本程序中“字符名称”的定义省略,此后的程序实例中也将省略对“字符名称”的定义语句。