单片机程序的设计
程序设计是单片机开发最重要的工作,程序设计就是利用单片机的指令系统,根据应用系统(即目标产品)的要求编写单片机的应用程序,其实我们前面已经开始这样做过了,这一课我们不是讲如何来设计具体的程序,而是教您设计单片机程序的基本方法。不过在讲解之前还是有必要先了解一下单片机的程序设计语言。一.程序设计语言这里的语言与我们通常理解的语言是有区别的,它指的是为开发单片机而设计的程序语言,如果您没有学过程序设计可能不太明白,我给大家简单解释一下,您知道微软的VB,VC 吗?VB,VC 就是为某些工程应用而设计的计算机程序语言,通俗地讲,它是一种设计工具,只不过这种工具是用来设计计算机程序的。要想设计单片机的程序当然也要有这样一种工具(说设计语言更确切些)
单片机的设计语言基本上有三类:
1.完全面向机器的机器语言机器语言就是能被单片机直接识别和执行的语言,计算机能识别什么?以前我们讲过--是数字“0”或“1”,所以机器语言就是用一连串的“0”或“1”来表示的数字。比如:MOV A,40H;用机器语言来表示就是11100101 0100000 ,很显然,用机器语言来编写单片机的程序不太方便,也不好记忆,我们必须想办法用更好的语言来编写单片机的程序,于是就有了专门为单片机开发而设计的语言:
2.汇编语言汇编语言也叫符号化语言,它使用助记符来代替二进制的“0”和“1”,比如:刚才的MOV A,40H 就是汇编语言指令,显然用汇编语言写成的程序比机器语言好学也好记,所以单片机的指令普遍采用汇编指令来编写,用汇编语言写成的程序我们就叫它源程序或源代码。可是计算机不能识别和执行用汇编语言写成的程序啊?怎么办?当然有办法,我们可以通过“翻译”把源代码译成机器语言,这个过程就叫做汇编,汇编工作现在都是由计算机借助汇编程序自动完成的,不过在以前,都是靠手工来做的。
值得注意的是,汇编语言也是面向机器的,它仍是一种低级语言。每一类计算机都有它自己的汇编语言,比如:51 系列有它的汇编语言,PIC 系列也有它的汇编语言,微机也有它自己的汇编语言,它们的指令系统是各不相同的,也就是说,不同的单片机有不同的指令系统,它们之间是不通用的,这就是为什么世界上有很多单片机类型的缘故。为了解决这个问题,人们想了很多的办法,设计了许多的高级计算机语言,而其中最适合单片机编程的要数C 语言。
3.C 语言—高级单片机语言 C 语言是一种通用的计算机程序设计语言,它既可以用来编写通用计算机的系统程序,也可以用来编写一般的应用程序,由于它具有直接操作计算机硬件的功能,所以非常适合用来编写单片机程序,与其他的计算机高级程序设计语言相比,它具有以下的特点:
(1)。语言规模小,使用简单在现有的计算机设计程序中,C 语言的规模是最小的,ANSIC 标准的C 语言一共只有32 个关键字,9 种控制语句,然而它的书写形式却比较灵活,表达方式简洁,使用简单的方法就可以构造出相当复杂的数据类型和程序结构。
(2)。可以直接操作计算机硬件 C 语言能够直接访问单片机的物理空间地址(KEIL C51 软件中的C51 编译器更具有直接操作51 单片机内部存储器和I/O 口的能力),亦可直接访问片内或片外存储器,还可以进行各种位操作。
(3)。表达能力强,表达方式灵活 C 语言有丰富的数据结构类型,可以采用整型、实型、字符型、数组类型、指针类型、结构类型、联合类型、枚举类型等多种数据类型来实现各种复杂数据结构的运算。利用C 语言提供的多种运算符,我们可以组成各种表达式,还可以采用多种方法来获得表达式的值,从而使程序设计具有更大的灵活性。
(4)。可进行结构化设计单片机教程(MCS-51 系列) 结构化程序是单片机程序设计的组成部分,C 语言中的函数相当于汇编语言中的子程序,KEIL C51 的编译器提供了一个函数库,其中包含有许多标准函数,如各种数学函数、标准输入输出函数等,此外还可以根据用户需要编制满足某种特殊需要的自定义函数。C 语言程序就是由许多个函数组成的,一个函数即相当于一个程序模块,所以C 语言可以很容易地进行结构化程序设计。
(5)。可移植性前面我们讲过,由于单片机的结构不同,所以不同类型的单片机就要用不同的汇编语言来编写程序,而C 语言则不同,它是通过汇编来得到可执行代码的,所以不同的机器上有80% 的代码是公用的,一般只要对程序稍加修改,甚至不加修改就可以方便地把代码移植到另一种单片机中。这对于已经掌握了一种单片机的编程原理,又想用另一种单片机的人来说,可以大大地缩短学习周期,我们将在教程的下册中专门来讲解C 语言的应用及其编程原理。不过作为单片机初学者想要学会C 语言并不是一件容易的事,因此对于大多数人来说,汇编语言仍是编写单片机程序的主要语言。我们上册的教程将全部以汇编语言来编写单片机的程序。了解了单片机编程的设计语言,下面我们来看单片机编程的基本过程和步骤。
二.单片机程序设计的步骤单片机的程序设计通常包括根据任务建立数学模型、绘制程序流程图、编写程序及汇编三个步骤。
1.建立数学模型数学实在是太有用了,在单片机的程序设计领域,根据任务建立数学模型是程序设计的关键工作。比如,在一个测量系统中,从模拟通道输入的温度、压力、流量等信息与该信号的实际值是非线性关系,这就需要我们对其进行线性化处理,此时就要用到指数和函数等数学变量来进行计算;再比如,在直接数字化控制的系统中,常采用PID 控制算法来进行系统的运算,此时又要用到数学中的微分和积分运算等等。因此,数学模型对于单片机的程序设计是非常重要的。只不过作为初学者,我们还没有复杂到如此程度,因此,详细的内容就不讲解了。下面的绘制程序流程图可是初学者的基本功,请大家务必仔细看一下。
2.绘制流程图所谓流程图,就是用各种符号、图形、箭头把程序的流向及过程用图形表示出来。绘制流程图是单片机程序编写前最重要的工作,通常我们的程序就是根据流程图的指向采用适当的指令来编写的,下面的图形和箭头就是我们绘制流程图用的工具(图中左边所示)。 绘制流程图时,首先画出简单的功能流程图(粗框图),再对功能流程图进行扩充和具体化,即对存储器、标志位等单元做具体的分配和说明,把功能图上的每一个粗框图转化为具体的存储器或单元,从而绘制出详细的程序流程图,即细框图。下面举个例子给大家演示一下,请看下面的程序:
主程序:
LOOP:SETB P1.0 ;
LCALL DELAY ;
CLR P1.0 ;
LCALL DELAY ;
LJMP LOOP ;
子程序:
DELAY:MOV R7,#250;
D1:MOV R6,#250;
D2:DJNZ R6,D2 ;
DJNZ R7,D1 ;
RET ;
END。
还记得吗,这是我们第四课中做过的LED 灯闪烁的实验,以前我们曾对程序进行过分析,现在让我们用流程图来把这段程序的主程序部分画出来,看上图的右边部分。这就是程序的流程图,在单片机的编程过程中,绘制流程图能看清楚程序执行的步骤以及程序的流向,事实上,程序的编写就是根据流程图的功能完成的。下面我们来把第十五课中的那个程序也用流程图画出来。
程序如下:
ORG 0000H ;
LJMP START ;
ORG 30H ;
START:MOV SP,#5FH;
MOV P1,#0FFH ;
MOV P3,#0FFH ;
L1:JNB P3.5,L2 ;
P3.5 上接有一只按键,它按下时,P3.5=0 JNB P3.6,L3 ;P3.6 上接有一只按键,它按下时,P3.6=0
LJMP L1 ;
L2:CLR P1.0 ;亮LED1
LJMP L1 ;
L3:SETB P1.0 ;暗LED1
LJMP L1 ;
END。
先不看图,自己画一下,看是不是同我画的一样。在实际的程序设计中,根据框图,采用适当的指令编写出实现流程图的源程序就是我们编写程序的最后工作。
3.编写程序和汇编程序编写完之后,我们要把它汇编成机器语言,这种机器语言就是十六进制文件,后缀名为*.HEX 文件,以前还要把它转换成二进制文件,后缀名为*.BIN 文件,不过现在的编程器都能直接读入十六进制文件,就不需要转换了,最后用编程器把程序写入单片机。这些以前都讲过了,这里就不重复了。
下面来讲本课的主题—程序设计的方法。
单片机程序设计的方法要想搞清楚程序设计的方法,我们首先要知道单片机到底有哪几类程序?
单片机的程序分为结构化程序、子程序和综合程序三个大类,先来看结构化程序。
1.结构化程序的设计方法在单片机的程序中,既有复杂的程序,也有简单的程序,但不论哪种程序,它们都是由一个个基本的程序结构组成的,这些基本结构有顺序结构、分支结构和循环结构。
(1)。顺序结构程序的设计顺序结构的程序一般用来处理比较简单的算术或逻辑问题,它的执行过程是按照程序存储器PC 自动加1 的顺序执行的,主要用数据传递类指令和数据运算类指令来实现。比如我们前面第六课中的I/O 口输入实验就是典型的顺序结构的程序。试试看,把这个程序的流程图写出来。下面再看一个例子:将内部RAM 中20H 单元和30H 单元的无符号数相加,存入R0(高位)和R1(低位)中。先画出流程图: 根据流程图编写源代码如下:
MOV A,20H ;
ADD A,30H ;
MOV R0,A ;
CLR A ;
ADDC A,#00H ;
MOV R0,A ;
MOV A,30H ;
ADD A,R1 ;
MOV R1,A ;
CLR A ;
ADDC A,R0 ;
MOV R0,A ;
这就是顺序结构程序,程序的原理我就不分析了,我们接着讲分支结构的程序设计。这里说明一点,最近有朋友提出这一课的有些程序看不懂,的确如此,这一课的有几个程序实例我们从来没有学过,之所以放在这里,原本是为了让大家理解程序设计的方法,举几个示例证明一下,没想到反而增加了大家的难度。其实这些示例你不需要刻意的去理解它,只要明白它的设计方法就可以了,因为这一张的主要内容是程序设计的方法,而不是程序执行的原理和结果。如果以后有更好的示例我会修改一下。
(2)。分支结构程序的设计所谓分支结构就是利用条件转移指令,使程序执行某一指令后,根据所给的条件是否满足来改变程序执行的顺序,也就是本条指令执行完后,并不是象顺序结构那样执行下一条指令,而是看本条指令所给的条件是否满足,如果满足条件就跳转到其他的指令,如果不满足就顺序执行;当然也可以是满足条件顺序执行,而不满足条件跳转执行,看十五课实验程序中的下面两条:
L1:JNB P3.5,L2 ;P3.5 上接有一只按键,它按下时,P3.5=0
JNB P3.6,L3 ;P3.6 上接有一只按键,它按下时,P3.6=0 这就是分支结构的程序,如果P3.5 为“0”,就转移;反之就顺序执行。
当然也可以改成P3.5=0 顺序执行;而P3.5=1 则转移,不过此时的程序就要用JB 指令了。在51 系列单片机中,可以直接用于分支程序的指令有JB(JNB)、JC(JNC)、JZ(JNZ)、CJNE 、JBC 等这几条,它们可以完成诸如正负判断、大小判断和溢出判断等等。
在分支结构的指令设计中,大家必须注意☺:执行一条判断指令只可以形成两路分支,如果要形成多路分支,就必须进行多次判断,也就是多条指令连续判断。下面给大家举两个例子:
A.单分支结构的程序实例假设有两个数在内部RAM 单元的40H 和41H 中,现在要求找出其中较大的一个数,并将较大的数存入40H 中,而将较小的一个数存入41H 中。根据程序的要求,我们先画出程序的流程图。 再根据流程图写出程序的源代码如下:
MOV A,40H ;
CLR C ;
SUBB A,41H ;
JNC WAIT ;
MOV A,41H ;
XCH A,41H ;
MOV 40H,A ;
WAIT:SJMP
WAIT;
END。
程序的原理请大家自行分析一下。
接下来再举一个多分支结构的实例,看下面的程序:
MOV A,20H ;取数JZ ZERO ;A=0,转移;A=1,顺序执行
JB ACC。7,STORE ;A 为负数,转移ADD A,#3 ;A 为正数,则加3
SJMP STORE ;ZERO:MOV A,#20 ;
STORE:MOV 21H,A ;
自己画一下本例的流程图,再和上面的右图比较一下,看是不是一样。这里有一条指令给大家解释一下:JB ACC.3,STORE;ACC.3 表示累加器A 中的D3 位,这条指令的意思就是看一下累加器中的D3 位(也就是第四位)是正还是负,第四位是什么呢?在这里就是“0”(20H 的二进制10000000)。明白了吗?接下来再讲第三种循环结构的程序设计。
(3)循环结构程序的设计
循环程序是最常用的程序结构形式,在单片机的程序设计中,有时要碰到一段程序要重复执行多次的情况,此时就要用到循环结构程序,比如第四课中的实验--LED 灯闪烁程序的子程序:
DELAY:MOV R7,#250;(1)
D1:MOV R6,#250;(2)
D2:DJNZ R6,D2 ;(3)
DJNZ R7,D1 ;(4)
RET ;(5)
END。
在这段程序中,为了延时需要多次执行DJNZ 指令,此时若用循环结构指令就可以大大地简化程序的设计,减少程序占用的存储器空间。循环结构指令一般有以下四个部分组成:
A.初始化部分初始化部分主要用来设置循环的初始值,包括预值数、计数器和数据指针的初值。比如上例中的#250 就是预值数初值。
B.循环处理部分循环处理部分是程序的主体部分,也称为程序体,通过它可以完成程序处理的任务。
C.循环控制部分 循环控制部分可以控制程序循环的次数,并修改预值数或计数器和指针的值,检查该循环是否执行了足够的次数,如果到了足够的次数,就采用条件转移指令或判断指令来控制循环的结束。比如上例中的(3)、(4)指令就是当R6 或R7 中的值为“0”时就转移。
D.循环结束部分循环结束后必须返回,一般用RET 或RETI (中断返回,以后会讲到)指令。这里注意☺:以上四个部分中,第一和第四部分只能执行一次,而第二和第三部分可以执行多次。也可以将处理部分和控制部分位置对调。
在循环程序设计中,循环控制部分是程序设计的关键环节,常用的循环控制方式有计数器控制和条件控制两种。计数器控制就是把要循环的次数(即预值数)放入计数器中,程序每循环一次,计数器的值就减1,一直到计数器的内容为零时,循环结束,一般用DJNZ 指令;
而条件控制方式常预先不知道要循环的次数,只知道循环的有关条件,此时就可以根据给定的条件标志位来判断程序是否继续,一般参照分支结构方法中的条件来判别指令并执行。下面举几个例子来分别解释一下,希望大家能以此类推。
程序一:用计数器控制的单重循环程序源程序如下:
CLR A ;
MOV R2,20H ;
MOV R1,22H ;
LOOP:ADD A,@R1 ;
INC R1 ;
DJNZ R2,LOOP ;
MOV 21H,A ;
这段程序的作用是从22H 单元开始存放一个数据块,其长度存放在20H 单元中,将数据块求和,要求将和存放入21H 单元中,和不超过255。下面再举一个条件控制的循环程序。
程序二:用条件控制的单重循环程序设字符串存放在内部RAM 的21H 开始的单元中,以结束作标志,要求计算出该字符串的长度,并将其存放在20H 单元中。源程序如下:
CLR A ;
MOV R0, #21H ;将地址指针指向21H 单元
LOOP:CJNZ @R0,#24H,NEXT ;与比较 SJMP COMP ;找到结束
NEXT:INC A ;不为“0”,计数器加1 INC R0 ;修改地址指针 SJMP LOOP ;
COMP:MOV 20H,A ;存放结果试试看,自己把上面两段程序的流程图画出来。
下面再看一个例子:
DELAY:MOV R7,#250 ;
D1:MOV R6,#250 ;
D2:DJNZ R6,D2 ;
DJNZ R7,D1 ;
RET ;
END。
这是一段约125mS 的延时程序,现在我们来把它改成下面表格中的程序(右边的程序):
DELAY:MOV R7,#250;
DELAY:MOV R7,#250;
D1:MOV R6,#250;
D1:MOV R6,#250;
D2:DJNZ R6,D2;
D2:MOV R5,#250; DJNZ R7 ,D1;
D3:DJNZ R5,
D3 ; RET;
DJNZ R6,D2 ;
DJNZ R7,D1 ;
RET;
END。 从这里可以引出一个概念:程序的嵌套。什么是嵌套,比如早上我骑自行车从家里到单位去上班,当走到半路上时,太太叫我去孩子学校拿点东西;到了学校,老师又叫我把学校的一台电脑修一下;修好电脑,一个朋友又打电话叫我去他那里拿了一本《单片机与嵌入式系统》杂志,完了之后再去上班;这就是生活中的嵌套。
在单片机的程序设计中,也有类似的现象,有时为了达到某个目的,往往要在一段循环程序中再加入另一段循环程序,这就是单片机的程序嵌套。通常我们把一个循环体中不再包含循环的叫做单重嵌套;如果一个循环体中还包括有循环,则叫做多重嵌套。上面的表格中左边的程序就是单重嵌套,而右边的程序则是多重嵌套。另外须注意☺:在多重嵌套中,不允许各个循环体互相交叉,也不允许从外循环跳入内循环,否则编译时会出错。了解了结构化程序的设计,下面再来看子程序的设计方法。
子程序的设计方法
什么是子程序?如何设计子程序?要解释这个问题,让我们先同样从生活中的一个例子说起,请看下面的数学题目:28*(33+65)+47*(33+65)+875*(33+65)。在这道题中,我们一般是怎么算的?也许大家都知道,一般总是先把(33+65)=98 代出来,然后再用(28+47+875)*98 来计算最后的结果,为什么会这样?这是因为在这道题中,我们多次用到了(33+65 )这个中间结果。在单片机的程序设计中,有时也有这样的情况,比如下面的程序:
主程序
LOOP:SETB P1.0 ;(1)
LCALL DELAY ;(2)
CLR P1.0 ;(3)
LCALL DELAY ;(4)
LJMP LOOP ;(5)
子程序
DELAY:MOV R7,#250 ;(6)
D1:MOV R6,#250 ;(7)
D2:DJNZ R6,D2 ;(8)
DJNZ R7,D1 ;(9)
RET ;(10)
END。(11)
这是大家非常熟悉的LED 灯延时程序,在这段程序中,两次调用到了DELAY 这段程序,为了简化程序的设计,我们就把DELAY 这段程序单独地列了出来,这段列出的程序我们就叫它子程序,而调用子程序的程序我们则叫它主程序(LOOP 的程序段)。在主程序执行时,每当要用到子程序时,我们 就用LCALL 指令来调用子程序,子程序执行完之后,必须返回主程序,返回就用RET 指令,这我们以前都讲过了,这里不再重复。
另外,如果子程序执行的过程中,还要再次调用其他的子程序,这种现象我们就称它为子程序的嵌套。这里有个问题,在子程序的执行过程中,有时可能要使用到累加器和某些工作寄存器,而在调用子程序前,这些寄存器中可能已经存放有主程序的中间结果,它们在子程序返回后仍要使用,这样就需要在进入子程序之前,将要使用的累加器和寄存器中的内容预先转移到安全的地方保存起来,这叫现场保护;当子程序执行完即将返回主程序之前,还要将这些内容先取出来,送回到累加器和原来的工作寄存器中,这个过程叫恢复现场。保护现场和恢复现场通常使用堆栈,即在进入子程序之前,将需要保护的数据压入堆栈,在返回之前再将压入的数据弹出到原来的工作单元中,恢复原来的状态。看下面的例子:
LOOP:PUSH 03H ;将03H 单元中的值压入堆栈保护
PUSH ACC ;将累加器中的值压入堆栈保护 ⋯⋯ ⋯⋯
POP ACC ;将ACC 中的值从堆栈弹出
POP 03H ;恢复03H 单元中的内容
RET ;从子程序返回
由于堆栈的操作是“后进先出,先进后出”,所以编写指令时,必须把后压入堆栈的数据先弹出来才能保证恢复到原来的状态。在实际的程序设计中,由于每个应用程序的不同,还必须根据具体的情况来考虑是否需要保护,哪些数据需要保护等等,这就是单片机的堆栈为什么能够变化的原因。关于堆栈的操作先讲这些,后面的实验中我们还将结合具体的实验来分析,接下来再看另一种程序--综合程序的设计方法。
综合程序的设计方法综合程序有查表程序、散转程序、数据排序程序、代码转换程序等等,作为初学者,要想全面的掌握也确实有一定的难度,所以只给大家简单地提一下,详细的内容就留到下则的课程中再来解释。
本课总结程序设计是单片机开发最重要的工作,掌握程序设计的基本步骤和方法对于单片机的软件编写是至关重要的,这一课的内容较多,对于一时无法搞清的部分,大家可以结合以后的实际应用慢慢去理解,不要急于求成,千万记住一点,学习使用单片机绝不是一朝一夕的事。