2.3 简单汇编语言程序设计

2.3.1 程序设计语言

程序设计首先遇到的问题,就是用何种语言与机器对话,如何把人的思想和安排告诉机器,并且能使机器接受、听懂。一般有两种办法:一种是直接用机器接受的语言对话,另一种是通过翻译间接地与机器对话。因此,在计算机中有三种基本语言:机器语言、汇编语言和高级语言。

1.机器语言

机器语言是以二进制数“0”和“1”表示的指令的集合。每台机器都有自己的指令系统。每条指令的编码送入处理机后,CPU的指令译码器就可以译出它的含义,并告诉机器应该执行什么操作。

用机器语言编写的程序见表2-3。

表2-3 机器语言编写的程序格式

程序中每个字节占一个单元地址。指令虽然用十六进制编写,但送入存储器时均已转换成二进制数。

机器语言可以省去翻译过程,也可以使程序编写得比较简练。但这样编写的程序容易出错、难读、难于书写。例如在表2-3中所列的程序若不熟悉指令的机器码,就很难弄清它是哪条指令构成的,是完成什么任务的程序,甚至连操作码和操作数也难以区分。因此在实际应用中很不方便。如果程序很大,困难就更大。因此对于新型计算机和新的调试软件,机器语言基本不被用户使用。

2.汇编语言

为了克服机器语言的缺点,人们创造了一种比较直观的容易记忆的助记符,来代替指令的机器码,这就形成了符号语言。按一定的特约规则书写符号程序,就形成了汇编语言,这是一种面向机器的程序设计语言。用汇编语言编写的源程序和机器指令几乎是一一对应的,见表2-4。因此可以说汇编语言仅仅是机器语言的某种改进,它属于面向机器的低级编程语言,不同计算机的汇编语言是不同的,本章探讨的是MCS-51单片机的汇编语言。

表2-4 两数求和程序

3.高级语言

高级语言是面向过程的、独立于计算机的通用语言,利用高级语言编程,人们可以不必去了解计算机的内部结构,编程人员把主要精力集中在解题、算法和过程的研究上。目前单片机的C语言程序设计也被广泛使用,它的程序结构、算法、变量、表达式、函数和其他C语言编写要求一样,而用C51编写的单片机应用程序则不用具体组织分配存储器资源和处理端口数据,但对数据类型与变量的定义必须要与单片机的存储结构相关联,否则编译器不能正确地映射定位。

本章将简单介绍汇编语言的程序设计方法。

2.3.2 伪指令

所谓伪指令就是汇编控制指令,仅提供汇编信息,没有指令代码。

本小节介绍常用伪指令及功能。

1.ORG——起始地址指令

格式:ORG N

N为十进制或十六进制常数,代表地址,指明程序和数据块起始地址,即指出该指令下一条指令的地址。

例如:

上述程序中指出了指令MOV R0,#30H所在的地址为2000H;而DB 32H,43H,‘A’这条伪指令所在的地址为3000H。

2.DB——定义字节型常数的指令

格式:DB X1,X2,…Xn

其中Xi为8位数据或ASCII码

例如:

则(1000H)=01H

(1002H)=02H

又如:

则(1100H)=30H;0的ASCII码

(1101H)=31H;1的ASCII码

3.DW——定义双字节伪指令

格式:DW X1,X2,…Xn

其中Xi为双字节数据

例如:

则(2000H)=25H

(2001H)=46H

(2002H)=01H

(2003H)=78H

4.EQU——数据赋值伪指令

格式:X EQU n

X为用户定义的标号,n为常数、工作寄存器或特殊功能寄存器,该伪指令是将n的值赋值给标号X,且只能赋值一次。

例如:

则DPTR=2000H

A=0FH

5.BIT——位赋值伪指令

格式:X BIT位地址

例如:CLK BIT P1.0

6.DATA——数据赋值伪指令

格式:字符名DATA表达式

功能:将右边表达式的值赋给左边的字符名。

此伪指令的功能与EQU类似,它们的区别在于:

1)DATA可以先使用再定义,它可以放在程序的开头和结尾,也可以放在程序的其他位置,比EQU指令灵活。

2)EQU指令可以把一个汇编符号(如R1)赋给一个字符名称,而DATA伪指令则不能。DATA伪指令在程序中用来定义数据或地址。

7.END ——汇编结束伪指令

格式:END

当汇编程序遇到该命令后,结束汇编过程,其后的指令将不再处理。

2.3.3 基本程序设计方法

程序是指令的集合。一个好的程序不仅应完成规定的功能,而且还应该占据内存最少、执行时间最短。一般程序设计过程可分为以下几步:

(1)分析课题,确定解题思路

实际问题是多种多样的,不可能有统一的模式,必须具体问题具体分析。对于同一个问题,也存在多种多样的解题方案,应通过比较从中挑选最佳方案。这是程序设计的第一步。

(2)建立系统的数学模型,确定控制算法和操作步骤

建立好系统的数学模型,明确算法对于程序设计非常重要,不同的算法程序执行的效率不同,例如乘法运算可以左移,也可以加法,还可以用乘法指令,也可以用查表完成。不同的方法程序的复杂度和执行时间差别很大。

(3)流程图可以直观地表示程序的执行过程或解题步骤和方法。同时它给出程序的结构,体现整体与部分的关系,将复杂的程序分成若干简单的部分,将给编写程序带来方便。

(4)编写程序

根据流程图的指示,编写出每一模块的具体程序,再按流程图的走向加上特定的语句连接成全部程序。

1.顺序程序

顺序程序是最简单的一种程序结构,又叫直线程序,它是按指令的顺序依次执行的程序,也是所有程序设计中最重要、最基本的程序设计方法。分支和循环程序设计都是在顺序程序设计的基础上实现的。

例2-10】将0~15共16个立即数送到内部RAM30H开始的单元。

编程思路:本题题意非常清楚,也就是将0送到内部RAM的30H单元,将1送到内部RAM中的31H单元,以此类推,我们可以用顺序语句实现。

例2-11】将内部RAM30H单元的压缩BCD码拆成两个非压缩的BCD码存储到内部31H、32H中。

编程思路:本题是一个拆字程序,比如30H中存的数据为#3FH,将它拆分为#03H和#0FH,分别存入31H和32H单元。首先确定算法,先把原数保存,然后再和#0FH进行与操作,取出低位数据,再用原数和#F0H进行与操作,取出高位,再将低位和高4位互换,分别保存。子程序如下:

例2-12】单字节压缩BCD码转换成二进制码子程序。

编程思路:本题设两个BCD码d0、d1,表示的两位十进制数压缩于R2中,其中R2高4位存十位,低4位存个位,转换成二进制的算法为:(d1d0)BCD=d1∗10+d0,流程图如图2-14所示,具体程序如下:

图2-14 例2-12流程图

2.分支程序

在一个实际应用中,程序不可能是顺序执行的,通常需根据实际问题设定条件,通过对条件是否满足的判断,产生一个或多个分支,以决定程序的流向,这种程序称为分支程序。分支程序的特点就是程序中含有条件转移指令。MCS-51中直接用来判断分支条件的指令有JZ、JNZ、JC、JNC、CJNE、DJNZ、JB、JNB、JBC等。正确合理地运用条件转移指令是编写分支程序的关键。

(1)单分支程序

例2-13】设内部RAM20H、21H两个单元中存有两个无符号数,试比较它们的大小,并将较大者存入20H单元中,较小者存入21H单元中。

编程思路:可以两个数相减,判断差的正负性即判断Cy的值是0还是1;或者用CJNE指令比较两个数,判断Cy的值。程序流程图如图2-15所示。具体程序如下:

图2-15 例2-13流程图

说明:也可以用JNC指令或CJNE指令,请读者自行完成。

(2)多重分支

例2-14】设变量x存放于R2中,函数Y存放于R3中。试按下式要求给Y赋值:Y=

编程思路:可以先判断x是否为0,不为0判断最高位是1还是0,最高位是1则为负,最高位是0则为正。程序流程图如图2-16所示,子程序如下:

图2-16 例2-14流程图

(3)散转程序设计

散转程序是一种并行多分支程序。它根据系统的输入或运算结果,分别转向各个处理程序。与分支程序不同的是散转程序多采用指令JMP @A+DPTR实现,根据输入或运算结果,确定A或DPTR的内容,直接跳转到相应的分支程序中。而分支程序一般采用条件转移指令或比较转移指令实现程序的跳转。下面给出两个散转程序的例子。

例2-15】编程实现双字节乘法,乘法示意图如图2-17所示,用分支转移指令JMP @A+DPTR实现程序。程序流程图如图2-18所示。参考程序如下:

说明:根据R3R2中的值程序跳转到指定子程序处执行。例如R3R2=0003,先取R3=00H,因为LJMP PROGN指令占3个字节,所以R3∗3后的值送到累加器A中,再与表的首地址高8位相加得到新的DPH;然后取低8位R2=03H的值乘以3后高8位再与DPTR的高8位相加,A中为偏移量,再执行JMP @A+DPTR语句后直接跳转到相应地址。

图2-17 乘法示意图

图2-18 多分支程序流程图

例2-16】设计可多达128路分支的出口程序。

由于AJMP指令是双字节指令,因此采用RL A左移指令,是把入口R2的值乘以2,保证找到TABL表中的第n条AJMP指令,利用JMP @A+DPTR语句使程序直接跳转到对应子程序执行,散转指令和后面讲到的查表指令有本质的区别。

3.循环程序

循环程序设计就是把一段程序多次反复执行。比如把30H~50H的内容传送到70H~90H,如果用顺序结构就要用32条传送语句,每次传送过程中只是操作数不同,这时就可以采用循环结构设计,既可以缩短程序,又减少了程序所占用的空间,一般情况下循环程序包括3部分。

1)循环初值:相当于循环体的初始化,如设置循环次数、间接寻址的首地址等。

2)循环体:需要多次重复执行的语句体。

3)循环控制:修改指针和循环控制变量,或判断循环结束调件。

循环结构有2种,一种是单重循环,另一种就是双重或多重循环。

单重循环:简单的循环,循环体中不嵌套循环。

多重循环:循环体中又套用循环结构,常用的是双重循环,一般对于初学者不建议使用层层嵌套的循环结构。

例2-17】将例2-10的程序用循环结构编写。

流程图如图2-19所示,程序如下:

图2-19 例2-16流程图

从例2-17可以看出循环结构程序由循环初值、循环体、循环控制3部分组成,循环变量通常采用能间接寻址的寄存器R0、R1,循环次数可以采用工作寄存器或直接地址。语句明显比顺序结构少很多,该结构减少了很多重复的语句,程序简短、占用存储空间少。

例2-18】求n个单字节数相加的和,设数据在40H开始单元,数据长度在30H单元,结果存放在31H、32H中。设累加和不超过两个字节。

编程思路:本题循环次数在30H单元,间接寻址单元从40H开始,可以采用DJNZ direct,rel语句。程序流程图如图2-20所示,程序如下:

例2-19】试编写程序,将内部数据存储器中连续存放的若干数据由小到大排列起来。

编程思路:将两个相邻的数据相比较,如果前数大于后数,两个数的位置互换,否则位置不变,所有数据比较完后找到最大的数,并存在最后一个单元中。第二次比较在剩余的数据中进行,找到剩余数据中最大的数,以此类推即可完成数据的排序。设数据的个数为n,第一次比较n-1次,第二次比较需n-2次等。在理论上需要进行n-1次循环比较才能完成。事实上有可能提前完成排序过程。如果在某次循环比较过程中,位置没有发生互换,说明数据排序已经完成。为此在程序中设置数据表示位F0,当R3为0时其F0没变成1则程序结束。

程序流程图如图2-21所示,程序如下:

图2-20 例2-18流程图

图2-21 例2-19流程图

本排序算作起泡法排序,是相邻两数进行比较,然后交换。由双层循环完成的,内层循环的循环次数是变化的,外层循环没有采用循环指令,是一个循环次数不定的循环。

例2-20】设计100ms延时程序。

编程思路:延时程序是计算每条指令执行的次数和每条指令执行所需时间的乘积之和。当系统晶振使用12MHz时,一个机器周期为1μs,执行1条DJNZ指令需要2μs,因此执行该指令50000次,就可以达到延时100ms的目的,因51系列单片机是8位机,每个寄存器最大数是256,单层循环最多延时256×2μs,达不到100ms的延时时间,因此需要双层循环。具体程序如下:

延时时间计算:(1×1+1×200+1×200+2×200×248+2×200+2×1)=100.003ms。

以上例题基本上属于循环次数固定的情况,一般采用DJNZ指令来控制循环,当循环次数不固定的时候,通常通过给定的条件标志来判断循环是否结束,一般采用条件比较指令CJNE来实现。

例2-21】把内部RAM中起始地址在BLK1的数据块传送到外部RAM中,起始地址在BLK2的区域中,遇到空格字符的ASCII则传送结束。

编程思路:由已知条件可知,数据传送过程中是不断地重复执行的操作,但这个程序只能通过一个条件控制循环结束,属于循环次数未知的循环程序,空格字符的ASCII是20H,利用CJNE指令将每个要传送的数据与20H比较,如果相同则不再传送,不同则继续传送。部分程序如下:

4.查表程序

在单片机应用系统中,查表程序使用频繁,由于利用它能避免进行复杂的运算或转换过程,故它广泛应用于显示、打印字符的转换以及数据补偿、计算、转换程序中。

查表就是根据自变量x的值,在表中查找y,使y=fx)。xy可以是各种类型的数据。表的结构也是多种多样的。表格既可以放在程序存储器中,也可以放在数据存储器中,一般情况下,自变量x是有规律变化的数据,可以根据这一规律形成地址,对应的y则存放于该地址单元中;对于x是没有变化规律的数据,则在表中存放x及其对应的y值。前者形成的表格是有序的,后者形成的表格是无序的。

例2-22】已知一位十进制数存放在R0中,编写程序求它的平方,并将结果放在R1中。

说明:使用MOVC A,@A+DPTR查表指令时必须先把表的首地址送给DPTR,然后把要查找数据在表格中的相对地址送给A,再把A+DPTR代表的ROM地址中的内容取出送到累加器A中,完成查表。下面使用MOVC A,@A+PC指令完成上面的程序。

A+PC的值不仅随A的值变化,也与查表指令在程序中的位置有关。本程序中MOVC A,@A+PC的指令距TAB表的首地址相差2B,因此给初值加2处理,这里‘2’称作偏移量。

两条查表指令设计的查表程序不同在于:用MOVC A,@A+PC指令,其表格位置与查表指令间的间隔数应小于256;而用MOVC A,@A+DPTR指令,表格可在64KB范围内任意位置。一般情况下都使用MOVCA,@A+DPTR指令。

例2-23】设有一巡检报警装置,需要对16路值进行比较,当每一路输入值超过该路的报警值时,实现报警。要求编制一个查表子程序,依据路数xi,查表得yi的报警值。

编程思路:xi为路数,查表时按照0,1,2,…,15取值,故为单字节规划量。表格构造见表2-5。

表2-5 表格构造

程序入口:(R2)=路数xi。

程序出口:(R4R3)=对应xi的报警值yi。

查表子程序如下:

本例题采用查表指令,而且表格中的数据是双字节的数据,因此需要找到待查数据首地址应使路数乘2,本程序中,使A左移一位,先查出报警值的高字节送R5中,再查低字节送入R3中。

5.子程序

在一个单片机系统的程序中,往往有许多地方需要执行同样的运算或操作。例如,各种函数的加减乘除运算、代码转换以及延时程序等。通常将这些经常重复使用的、能完成某种基本功能的程序段单独编制子程序,以供不同程序或同一程序反复调用。在程序中需要执行这种操作的地方先执行一条调用指令,转到子程序中完成规定操作后再返回原来程序中继续执行下去,这就是所谓的子程序结构。采用子程序结构,可使程序简化,便于调试、交流和共享资源。

(1)子程序的调用与返回

子程序第一条指令所在地址称为入口地址,该指令前必须有标号,最好以子程序能完成的功能名为标号。例如延时子程序名一般设为DELAY,查表子程序设为TABLE。

子程序调用指令为:LCALL或ACALL,该指令应放在主程序中,这两条指令后跟的语句标号就是子程序名,具有寻找子程序入口地址的功能,而且在转入子程序前能自动使主程序断点地址入栈,具有保护主程序断点地址的功能。返回指令RET放在子程序的末尾,它使程序指针返回到调用子程序指令的下一条指令位置,起到恢复断点功能。子程序调用和返回关系如图2-22所示。

图2-22 子程序的调用和返回

(2)参数的现场保护

在转入子程序时,特别是进入中断服务程序时,要特别注意现场保护问题。即主程序使用的内部RAM的内容、各工作寄存器的内容、累加器A的内容、DPTR以及PSW等特殊功能寄存器内容都不应该因转向子程序运行而改变。如果子程序使用的寄存器与主程序使用的寄存器有冲突,则应在转入子程序后首先采取措施保护现场。方法是将要保护的单元的内容压入堆栈保存起来,在执行返回指令时将压入的数据再弹出到原工作单元,恢复主程序原来的状态,即恢复现场。

(3)主程序和子程序的参数传递

子程序调用时,要特别注意主程序与子程序之间的信息交换。在调用一个子程序时,主程序应先把与调用相关的参数(入口地址)放到某些特定的位置,子程序在运行时可以从约定的位置得到相关的参数。同样在子程序结束前,也应把子程序运行后的处理结果(出口参数)送到约定的位置。当子程序返回后,主程序可从这些位置得到需要的结果。参数传递的方法大致以下有3种。

1)利用工作寄存器R0~R7或者累加器A传递参数

在调用子程序前应先把数据存入工作寄存器或累加器中。调用子程序后就使用这些寄存器或累加器中的值进行各种操作和运算,子程序执行完返回后,这些寄存器和累加器中的值仍可被主程序使用。这种参数传递在汇编语言中比较常用。其优点是方法简单、速度快,缺点是传递的参数不能太多。

例2-24】设计一单字节有符号数的加法程序。

编程思路:该程序的功能为(R2)+(R3)→(R7)。R2和R3中为有符号数的原码,R7中存放计算结果的原码。程序如下:

求补码子程序如下:

说明:本程序是加法程序,参数传递时通过累加器A完成的,主程序是将被转换的数据存放到A中,子程序是将被转换的有符号数据求补码后重新存放在A中。主程序从A中得到运算结果。

2)存储器传递参数

数据一般在存储器中,可以用指针来指示数据的位置,这样可以大大节省传送数据的工作量,在内部RAM中,可以使用R0和R1作为存储器的指针,外部存储器可以使用DPTR0或DPTR1作为指针,进行数据传递。

例2-25】比较两个数据串是否完全相等,若完全相等,A=0,否则A=FFH

编程思路:设两个数据串分别存放在内部RAM的两个存储区(BLOCK1和BLOCK2),数据串的长度在R2中存放.编程时可以使R0指向第一数据块的首地址,R1指向第二数据块的首地址,子程序调用时,只用MOV R0,#BLOCK0和MOV R1,#BLOCK1就可以实现,具体程序如下:

3)利用堆栈传送

在主程序调用子程序前,可将子程序所需要的参数通过PUSH指令压入堆栈。在执行子程序时可用寄存器间接寻址访问堆栈,从中取出所需要的参数并在返回主程序之前将其结果送到堆栈中。当返回主程序后,可用POP指令从堆栈中取出子程序提供的处理结果。由于使用了堆栈区,应特别注意SP所指示的单元。在调用子程序时,注意断点处的地址也要压入堆栈,占用两个单元。在返回主程序时,要把堆栈指针指向断点地址,以便能正确返回。在通常情况下,PUSH指令和POP指令总是成对使用,否则会影响子程序的返回。

例2-26】在20H单元存放两位十六进制数,编程将它们分别转换成ASCII码并存入21H、22H单元。

说明:主程序通过堆栈将要转换的十六进制数送入子程序,子程序的转换结果再通过堆栈送到主程序。只要在调入前将入口参数压栈,在调用后把要返回的参数弹出。注意的是ACALL指令不仅能转向子程序,同时也调整了SP的值,因此在子程序中要注意调整SP的值。