正数肯定大于负数,它们的差若为正,是正常运算,无溢出(OV=0);若差为负,则不正常,一定溢出(OV=1)。
负数肯定小于正数,它们的差若为负,是正常运算,无溢出(OV=0);若差为正,则不正常,一定溢出(OV=1)。
由此可以得到程序的流程图3.2。
CLRC
MOVA,ONE;取X到A
SUBBA,TWO;X-Y
JZXMAX;X=Y
JBACC.7,NEG;X-Y为负转NEG
JBOV,YMAX;X-Y>0,OV=1,Y>X
Y
N
N
Y
Y
N
N
图3.2例5的流程图
SJMPXMAX;X-Y>0,OV=0,X>Y
NEG:
JBOV,XMAX;X-Y<0,OV=1,X>Y
YMAX:
MOVA,TWO;Y>X
SJMPRMAX
XMAX:
MOVA,ONE;X>Y
RMAX:
MOVMAX,A;送较大值至MAX
RET
ONEDATA30H
TWODATA31H
MAXDATA32H
END
例6128分支程序。
根据R3的值(00H-7FH),分支到128个不同的分支入口。
解:
多分支程序根据分支数目的不同,可由不同的设计方法。
若分支的入口在2kB范围内分布,可使用AJMP指令,参考程序如下:
MOVA,R3
RLA;A←A
2
MOVDPTR,#BRTAB
JMP@A+DPTR
BRTAB:
AJMPROUT00
AJMPROUT01
:
AJMPROUT127
END
可以看出,从BRTAB开始存放着一系列AJMP指令。
程序的工作是两次转移的方式:
先根据R3的值,用JMP指令转移到从BRTAB开始的某一条AJMP指令,然后再用这条AJMP指令转移到相应的分支入口ROUTnn。
当然,各个分支入口地址ROUTnn要通过伪指令或其它方式来定义。
由于AJMP是双字节指令,因此提前使偏移量A乘以2,以便转向正确的位置。
每个分支的入口地址(ROUT00-ROUT127)必须和其相应的AJMP指令在同一个2k存贮区内。
也就是说,分支入口地址的安排仍有相当的限制。
如改用长转移LJMP指令,则分支入口就可以在64kB的范围内任意安排,但程序要作相应的修改。
请读者自行设计。
三、循环程序
循环程序也是一种程序的组织形式。
在程序执行时,往往同样的一组操作需要重复许多次,当然可以重复使用同样的指令来完成,但若使用循环程序,重复执行同一条指令许多次来完成重复操作,就大大简化了程序。
例如要做1到100的加法,没有必要去写100条加法指令,而可以只写一条加法指令并使之执行100次,每次执行时操作数也作相应的变化,同样能完成原来规定的操作。
循环程序一般由四部分组成:
1.1. 置循环初值,即确定循环开始时的状态,如使工作单元请0,计数器置初值等。
2.2. 循环体部分,即要求重复执行的部分。
这部分程序应特别注意,因为它要重复执行许多次(如100次),因此,若能少写一条指令,实际上就是少执行100条指令。
反之亦然。
3.3. 循环修改部分,循环程序必须在一定的条件下结束,否则就要形成死循环。
因此,每循环一次就要注意是否需要修改达到循环结束的条件,若需要修改时,一定不要忘记,以便在一定情况下能结束循环。
4.4. 循环控制部分,根据循环结束条件,判断是否结束循环。
以上四部分可以有两种组织形式,如图3.3所示。
为了构成循环程序,DJNZ指令是很有用的,特别是在根据计数器的值决定循环是否结束时可以直接使用。
但也可以根据其它条件来判断循环结束条件。
例7从BLOCK单元开始存放一组无符号数,一般称为一个数据块。
数据块长度放在LEN单元,编写一求和程序,将和存入SUM单元,设和不超过8位二进制数。
解:
这是一个典型的循环程序例子。
在置初值时,将数据块长度置入一个工作寄存器,将数据块首地址送入另一个工作寄存器,一般称它为数据块地址指针。
每做一次加法之后,修改地址指针,以便取出下一个数来相加。
并且使计数器减1,至计数器减到0时,求和结束,把和存入SUM即可。
参考程序如下:
N
Y
退出循环
Y
N
图3.3循环程序组织方式
LENDATA20H
SUMDATA21H
BLOCKDATA22H
CLRA;清累加器
MOVR2,LEN;数据块长度送R2
MOVR1,#BLOCK;数据块首地址送R1
LOOP:
ADDA,@R1;循环做加法
INCR1;修改地址指针
DJNZR2,LOOP;修改计数器并判断
MOVSUM,A;存和
RET
以上程序在计数器初值不为零时是没有问题的。
但若是数据块的长度有可能为零,则将出现问题,当R2初值为零,减1之后将为FFH,故要做256次加法之后才会停止,显然和原意不符。
若考虑到这种情况,则可按图3.3(b)的方式来编写程序:
在做加法之前,先判断一次R2的初值是否为零。
整个程序仍基本套用原来的形式:
CLRA
MOVR2,LEN
MOVR1,#BLOCK
INCR2
SJMPCHECK
LOOP:
ADDA,@R1
INCR1
CHECK:
DJNZR2,LOOP
MOVSUM,A
RET
例8多字节加法程序。
有10组三字节的被加数和加数,分别存在两个数据块中,首地址分别存于寄存器R0和R1中,求这10组数的10组和,各组的和仍送回以R0为指针的单元。
解:
单字节无符号数和的最大值不能超过255,在实际应用时显然是很不够的,因此常用若干个字节来表示一个数.若是双字节无符号数,则最大值可为65535,若用三字节表示,则最大值可为16777215,这在很多场合已是足够了。
这个问题要用双重循环程序来完成。
因为多字节加法本身就需要用循环来完成,而10组多字节数的求和又需通过10次循环来计算,这样就出现了循环嵌套的情况。
这个题目算完以后,原有的被加数都被冲掉,由各组的和来代替。
编程时,设两个三字节数的和仍为三字节。
由于是多字节相加,所以相加时要用ADDC指令。
MOVTEMP,R0;保留指针的起始值
MOVTEMP+1,R1
MOVR3,#10;R3存几组数相加
LOOP:
MOVR2,#3;R2存字节数
CLRC;清
准备相加
LOOP1:
MOVA,@R0;取被加数
ADDCA,@R1;加一个字节
MOV@R0,A;存部分和
INCR0;指向下一字节
INCR1
DJNZR2,LOOP1;组内循环
DJNZR3,LOOP;10组数循环
MOVR0,TEMP;恢复指针到起始值
MOVR1,TEMP+1
RET
TEMPDATA20H
END
程序执行以后,10组和存于原来10个被加数的位置,为便于调用,将指针R0和R1恢复成起始的位置。
以上程序作些修改以后就可用来求若干组多字节数的总和,这个问题作为习题留给读者。
四、查表程序
在很多情况下,通过查表比通过计算解决问题要简便得多。
在编程序时也有类似的情况:
有时通过查表程序比通过运算程序要简单得多,编程也较为容易。
在MCS—51中查表时的数据表格是存放在程序ROM而不是数据RAM,在编程时可以很方便地通过DB伪指令把表格的内容存入ROM。
用于查表的指令有两条:
MOVCA,@A+DPTR
MOVCA,@A+PC
使用DPTR作为基地址查表比较简单,可通过三步操作来完成:
(1)
(1) 将所查表格的首地址存入DPTR数据指针寄存器;
(2)
(2) 将所查表的项数(即在表中位置是第几项)送到累加器A;
(3)(3) 执行查表指令MOVCA,@A+DPTR,进行读数,查表的结果送回累加器A。
若用PC内容作为基地址来查表,所需操作有所不同,但也可以分为三步:
(1)
(1) 将所查表的项数(即在表中是第几项)送到累加器A,在MOVCA,@A+PC指令之前先写上一条ADDA,#data指令,data的值待定;
(2)
(2) 计算MOVCA,@A+PC指令执行后的地址到所查表的首地址之间的距离,即算出这两个地址之间其它指令所占的字节数,把这个结果作为A的调整量取代加法指令中的data值;
(3)执行查表指令MOVCA,@A+PC进行查表,查表结果送到累加器A。
在用DPTR作为基址进行查表时,可以通过传送指令让DPTR的值和表的首地址一致。
但在用PC作为基址时,却不大可能做到这一点,因为PC的值是由MOVCA,@A+PC指令所在的地址加1以后的值所决定的。
因此,必须要作上面步骤中规定的地址调整。
用PC作为基址虽然稍微麻烦一些,但可以不占用DPTR寄存器,所以仍是常用的一种查表方法。
例9将十六进制数转换为ASCII码。
设十六进制数存放在R0寄存器的低4位,转换后的ASCII码仍送回R0寄存器。
解:
作为对比,这个问题用两种方法来解,一种是计算求解,一种是查表求解。
0~9的ASCII码为30H~39H,而A~F的ASCII码为41H~46H。
计算求解的思路是当R0≤9时,加上30H就变成相应的ASCII码。
若R0>9,则加上37H才能完成变换。
以上思路可以用分支程序来实现。
下面介绍的则是不用分支的一种解法。
先让R0加上90H,并作十进调整,然后再用ADDC指令使R0再加上40H,并作十进制调整,所得结果就是转换后的结果。
当原来的R0≤9时,以上运算不影响十进制个位的值,而十位上是9+4=13,1留在
之中,在十位上只留下3,即相当于加30H;
当原来的R0>9时,个位十进制数就要调整,结果为R0-10(加6调整和减10等价),同时有半进位加到十位上,使十位为9+1=10,即调整后十为0,
为1,下一次再用ADDC加上40H时,实际就是加上了41H,从而完成了十六进制到ASCII码的转换。
MOVA,R0;取转换值到A
ANLA,#0FH;屏蔽高4位
ADDA,#90H
DAA
ADDCA,#40H
DAA
MOVR0,A;转换结果送回R0
RET
若使用查表程序,则整个程序更为简单,也很容易理解:
MOVA,R0
ANLA,#0FH
ADDA,#2;地址调整
MOVCA,@A+PC
MOVR0,A
RET
ASCTAB:
DB‘0,1,2,3,4’
DB‘5,6,7,8,9’
DB‘A,B,C,D,E,F’
END
在这个查表程序中,从MOVC指令到表的首地址ASCTAB之间只有两条一字节指令,所以PC的调整量为2。
例10一组十六进制数转换为ASCII码。
每个字节内存放两个十六进制数。
十六进制数据块首地址存于R0寄存器,存放ASCII码区域的首地址存于R1寄存器,数据块长度存于R2寄存器。
程序执行后R0和R1仍应指向原来的位置。
解:
由于每个字节存放两个十六进制数,因此要拆开转换两次,每次都通过查表求相应的ASCII码。
由于两次查表所用的MOVC指令在程序的不同位置,因此,两次对PC地址调整的值不同的。
可以先将整个程序写完,两条加法指令中的加数待程序写完后再填入。
MOVTEMP,R0;暂存指针值
MOVTEMP+1,R1
LOOP:
MOVA,@R0;取二个十六进制数
ANLA,#0FH;保留低4位
ADDA,#19;第一次地址调整
MOVCA,@A+PC;第一次查表
MOV@R1,A;存第一次转换结果
INCR1
MOVA,@R0;重新取出被转换数
SWAPA;准备处理高4位
ANLA,#0FH
ADDA,#10;第二次地址调整
MOVCA,@A+PC;第二次查表
MOV@R1,A;存第二次转换结果
INCR1;修改指针
INCR0
DJNZR2,LOOP;R2≠0再循环
MOVR0,TEMP;恢复指针原值
MOVR1,TEMP+1
RET
ASCTAB:
DB‘0,1,2,3,4’
DB‘5,6,7,8,9’
DB‘A,B,C,D,E,F’
TEMP:
DATA20H
END
利用查表程序还可以完成BCD—七段码的转换,从而取代硬件七段译码电路。
查表程序本身并无复杂之处,需要注意的是七段码的取值。
因为七段发光显示器有共阳极和共阴极两种,共阳极是低电平为有效输入,共阴极是高电平为有效输入,因此不同的器件会有不同的码值。
另外管脚信号与码位的对应关系也会影响码值,即管脚可以由高到低排列(7~1),也可以由低到高排列(1~7)。
下面一组0~9的七段代码是针对共阳极显示管,管脚信号由高到低排列,例如对于0的代码为:
01000000(0的七段码)
即40H。
这组10个代码为:
40H,79H,24H,30H,19H,12H,02H,78H,00H,18H。
具体程序参见第七章。
五、子程序
在用汇编语言编写程序时,也应考虑恰当地使用子程序,从而使整个程序的结构清楚,阅读和理解都方便。
使用子程序还可以减少源程序和目标程序的长度。
在多次调用同样的程序段时,采用子程序就不必每次重复书写同样的指令,而只需书写一次,翻译成目标码时,也只需翻译一次。
当然从程序的执行来看,每调用一次子程序都要附加保护断点、进栈、出栈等操作,增加程序的执行时间。
但一般来说,付出这些代价总是值得的。
在汇编语言源程序中使用子程序,要注意两个问题,即参数传递和现场保护问题。
在调用汇编语言子程序时会遇到一个参数如何传递的问题。
在调用高级语言子程序时参数的传递是很方便的。
通过调用语句中的实参数以及子程序语句中的形式参数之间的对应,很容易完成参数的往返传递。
而用指令调用汇编语言子程序并不附带任何的参数,参数的互相传递靠编程者自己安排。
其实质就是如何安排数据的存放以及工作单元的选择问题。
参数传递一般可采用以下方法:
(1)传递数据。
将数据通过工作寄存器R0-R7或者累加器来传送。
即在调用子程序之前把数据送入寄存器或者累加器。
调用以后就用这些寄存器或者累加器中的数据来进行操作。
子程序执行以后,结果仍由寄存器或累加器送回。
(2)传递地址。
数据存放在数据寄存器中,参数传递时只通过R0、R1、DPTR传递数据所存放的