这是在学校里面单片机的教学情况,社会上学习单片机的情况又怎样呢?随着电子元器件技术的发展,硬件电路的制作越来越简单容易,有些器件可以直接把管脚焊接在一起,甚至无需用电路板连接即可使用,让大家感到头疼的还是编程。
尤其是业余单片机爱好者,没有受过专业的编程语言训练,大多数情况下是使用别人写好的程序,最多是在别人的程序上修改。我们看有关单片机制作的文章,内容大都是介绍原理,说到编程,往往只是说可以到哪儿下载。说实在的,编程语言就是出现在文章里,懂得的人不必去看,不懂的人看也看不懂,也是费力不讨好。
单片机如何"说话"单片机的汇编语言,既然称作"语言",它跟我们的自然语言是有相似之处的。比如它有语句,语句要符合语法规则。
说到"规范"二字,有的朋友会说,既然是"语言",只要语句正确,语法正确,想怎么说就怎么说,想怎么写就怎么写,只要编译通过,功能能够实现就可以,难道还要有什么"规范"不成?答案是,当然需要。我们在学习自然语言时也是要有规范的。语文课本里的文章,我们不是把它们叫做"范文"吗。当然,自然语言的使用可以非常灵活自由,但也要看是哪一类的文章。像诗歌、散文这类的文章,语言的使用可以非常灵活,而像一些应用文,语言的使用就要受到限制,如我们学习写请假条,寻人启事,会议通知之类的文章,就要遵守一定的格式。我们在进行应用文写作的时候,只有遵守这些"格式",才能写出合格的应用文来。
汇编语言是我们跟单片机打交道所使用的语言,我们使用汇编语言跟单片机"说话",让它听从我们的指挥,首先是让它能听懂我们的"说话",那就是要正确使用指令。单片机的"大脑"还没有我们人脑这么聪明,我们在说话时能揣测对方的意思,而单片机只能严格按照我们的约定来执行我们的命令。其次是如何"说话".汇编语言属于工程语言,工程语言的精髓就是规范。它的规则更加严谨,书写要求更加严格。越是规范严谨的语言,学习起来就越应该有法可依。而找到了这个"法",我们的学习就会向前迈出一大步。汇编语言里有什么样的规范呢?
在进行汇编语言的教学时,我们首先向学生们强调,汇编语言程序由三部分组成:①预定义部分;②主程序部分;③子程序部分。这就是汇编语言程序编写的规范格式。当然,有些简单的程序,可能会缺少某一部分,但是我们还是从一开始就向学生们强调,简单的程序也要尽量写出这三部分。因为随着程序内容的增加,这三部分的结构与层次的重要意义就会越发地显现出来。下面我们以一个最简单的单片机控制电路为例子,介绍这种规范程序的写法,并逐一介绍每部分的内容与含义。
图1是单片机系统的三个管脚p0.1,p0.2,p0.3与三个发光二极管的电路连接图。从图1中我们可以看到,只要控制单片机p0这三个脚的电位,我们就可以随意控制这三个LED灯的亮灭。我们的控制要求是:LED1亮1s灭1s,接着LED2亮1s灭1s,接着LED3亮1s灭1s,结束。
图1 单片机系统与LED的电路连接图
电路功能很简单,编程思路可以这样来叙述。如图2。
图2 电路编辑思路
程序编写也很简单,大多数人认为程序可以直接写出来,请看下面的程序示例一。
//程序功能:三个LED灯依次各亮灭1s
STart: clr p0.0 //点亮第一个LED灯
acall delay1s
setb p0.0 //熄灭第一个LED灯
acall delay1s
clr p0.1 //点亮第二个LED灯
acall delay1s
setb p0.1 //熄灭第二个LED灯
acall delay1s
clr p0.2 //点亮第三个LED灯
acall delay1s
setb p0.2 //熄灭第三个LED灯
acall delay1s
ajmp $ //待机状态
delay1s: //延时1s子程序
mov r5,#50
d3: mov r6,#100
d2: mov r7,#100
d1: djnz r7,d1
djnz r6,d2
djnz r5,d3
ret
end //程序结束
"说话"也有规范
上面的程序,我们经过编译,下载,运行,完全能实现预计的功能。但是我们要说这种程序就是没有规范的程序写法。
这就像我们写文章,这只能算是一份草稿,虽然意思讲清楚了,但是有些句法还不符合规范,结构层次也不清楚,所以还不能算是一篇合格的文章。那么,规范的写法又有怎样的要求呢?下面我们对照着规范写法的三部分内容来看一下。
规范写法的第一部分是"预定义",预定义部分就是要求我们在使用单片机管脚接口的时候先要给接口定义一个名称,而不要直接使用单片机接口名。如我们在程序中不要直接使用p0.0之类的。另外,我们在使用RAM中的存储单元的时候,也不要直接使用单元地址,也要在预定义部分给它定义一个单元名称。如我们要把一个计数值存储在30h存储单元里,我们就可以在预定义部分写上"counter equ 30h"语句,以后在程序中,我们直接使用"counter"这个名字就可以了。这样写的好处就是以后如果电路中单片机的管脚接口有所变动,或是存储单元需要修改,我们只需在预定义部分改动一下,而程序部分则一点也不需要动。这就是预定义的方便之处。
"主程序"与"子程序"的区分则更加重要。可以这样说吧,在编写程序时,能够实现某些具体功能的程序段,我们不要把它放在主程序里面,而要把它写成子程序。如上面的程序示例中,延时1s程序写成子程序,这样很好,但是让LED灯亮灭的这些功能程序段也应当写成子程序,这样就会更好。那有朋友问了,你都写成了子程序,那主程序部分干什么?问得好,其实编写程序,主程序部分我们尽量让它不去做具体的事情,因为它还有更重要的事情去做。我们把具体的事情放给子程序去做,而主程序,我们是让它扮演指挥,协调,检查子程序的工作。看到了吗,主程序和子程序就是这样的关系,主程序是我们的大脑,而子程序则是我们的手和脚,它们是指挥和被指挥的关系。那么,主程序如何"指挥"子程序呢,具体的说,就是"调用".
从开始写程序,我们的脑海里就应该建立起"预定义""主程序""子程序"这三个模块,在具体写程序的时候,我们就是向这三个模块里填充内容,而这就是我们所说的规范写法。
基于这样的思想,上面示例一的程序,要怎样写才是符合规范的呢,请看下面的程序示例二。
//程序功能:三个LED灯依次各亮灭1s,(用规范的写法改写)
//第一部分:预定义
led_light1 bit p0.0 //定义管脚
led_light2 bit p0.1
led_light3 bit p0.2
org 0000h //程序开始
ljmp main
org 0030h
//第二部分:主程序
main:
acall led1 //调用led1子程序
acall led2 //调用led2子程序
acall led3 //调用led3子程序
ajmp $ //待机状态
//第三部分:子程序
led1: //led1子程序
clr led_light1 //点亮第一个LED灯
acall delay1s
setb led_light1 //熄灭第一个LED灯
acall delay1s
ret
led2: //led2子程序
clr led_light2 //点亮第二个LED灯
acall delay1s
setb led_light2 //熄灭第二个LED灯
acall delay1s
ret
led3: //led3子程序
clr led_light3 //点亮第三个LED灯
acall delay1s
setb led_light3 //熄灭第三个LED灯
acall delay1s
ret
delay1s: //延时1s子程序
(中间内容略)
ret
end //程序结束
请注意预定义部分除了"定义管脚",我们使用了伪指令"org"定义了"程序开始",这样是为了避开5个中断服务子程序的入口地址部分,使程序从0030h开始。而"main"程序里只有三条调用指令,就完成了指挥的功能。只有这样写程序,主程序部分才能够发挥它应有的作用。而所有的具体功能的实现,我们都放到了子程序里,这样的程序结构看起来就清楚多了。
当然,这个程序因为简单,我们没有感觉到这种规范的写法有什么好处,反而觉得它比第一种方法还要复杂。实际上,随着电路的功能越来越多,程序的内容也会跟着越来越多,那个时候,你就会越来越发现我们这种规范写法的优越性来了。
因为电路的主要功能,我们可以到主程序部分去查找,而具体的实现功能的方法,我们则可以到子程序部分去查找,这样的程序结构让写程序的人觉得有章可循,循序渐进;让看程序的人也觉得层次清晰,一目了然。
果真是这样吗?下面我们改变一下这个电路的功能,让这三个灯的亮灭循环进行下去,那么这个程序应当怎样写呢?其实很简单,我们只要在示例程序二的主程序(main)里稍微改动一下就可以。请看改动过的main程序:
main:
loop: acall led1
acall led2
acall led3
ajmp loop //循环
当然,这种改动过于简单,在这里只是想让大家看看,main程序其实只有两种工作状态,一种是待机状态,一种就是循环状态。
以上的程序,我们都是用的软件定时,这对单片机系统来说是不划算的。因为这样,CPU绝大部分时间都消耗在了计数上面。实际上CPU还有更重要的事情去处理,我们要把CPU从计数里解放出来。下面我们使用定时器计时来实现我们的电路功能,那么,程序应当怎样来写呢?从上面的编程思路框图中,我们可以看到,LED灯的亮灭有6种状态,下面是一种编程方法,大家请看编程示例三:
//程序功能:三个LED灯依次各亮灭1s,用定时器延时
//第一部分:预定义
led_light1 bit p0.0 //定义管脚
led_ light2 bit p0.1
led_ light3 bit p0.2
counter equ 30h //定义计数寄存器
org 0000h //程序开始
ljmp main
org 000bh
ljmp int_t0 //定时器T0中断入口地址
org 0030h
//第二部分:主程序
main:
acall init_t0
ajmp $ //等待中断
//第三部分:子程序
init_t0: ;初始化定时器T0子程序
mov tmod,#01h
mov tl0,#low(65536-50000) //50ms初值
mov th0,#high(65536-50000)
setb ea
setb et0
setb tr0
mov counter,#0
mov r2,#0
ret
int_t0;定时器T0中断子程序
mov tl0,#low(65536-50000)
mov th0,#high(65536-50000)
inc counter
mov r0,counter
cjne r0,#20,lp1
mov counter,#00h //12MHz晶振,定时1s
acall led_flash
lp1: reti
led_flash; LED灯闪子程序
mov dptr,#table //散转程序
mov a,r2
add a,r2
jmp @a+dptr
table: ajmp led1
ajmp led2
ajmp led3
ajmp led4
ajmp led5
ajmp led6
led1: clr led_ light1 //led状态1
mov r2,#1
ajmp lp2
led2: setb led_ light1 //led状态2
mov r2,#2
ajmp lp2
led3: clr led_ light2 //led状态3
mov r2,#3
ajmp lp2
led4: setb led_ light2 //led状态4
mov r2,#4
ajmp lp2
led5 clr led_ light3 //led状态5
mov r2,#5
ajmp lp2
led6: setb led_ light3 //led状态6
mov r2,#0
clr tr0 //定时器停止计数
lp2: ret
end //程序结束
在这个程序里,大家需要注意这样几个问题:
1.在主程序部分main里,除了初始化T0之外,主程序什么也没有做,这就对了。因为我们总是强调主程序还要有更重要的事情去处理,所以它要把一些小事情,具体的事情放手给子程序去处理。这就好像我们吃饭时用筷子夹菜,我们不要时时用脑子想"要把夹的菜放进嘴里",我们的手就会自动把菜放进我们的嘴里。因为这样的小事情就不要再麻烦我们的大脑了,只有这样把大脑解放出来,我们在吃饭时大脑才可以思考其他的事情,才可以跟其他人交谈而又不耽误吃饭。单片机只有能处理许多复杂的事情,才能够显示出它的强大功能来,所以我们在编程时一定注意让主程序部分少做具体的事,多做指挥的事。
2.一个完整的程序绝对不是从第一行到最后一行这样依次写下来的。我们说规范的程序由三部分组成,有的语句是在写程序的时候边写边补充进去的。例如我们在写定时器定时1s的部分时,需要一个计数存储单元,于是我们便在"第一部分预定义"里加进了"counter equ 30h "这条语句。如果程序中要使用堆栈,我们还要先给堆栈指针SP赋值,以规定堆栈栈顶的位置。实际的程序编写就是如此。
3.在本程序中,我们使用了散转语句,其实这样真有点杀鸡用牛刀了。还可以有更简单的写法。我们这样写,一方面是想让大家试一试散转语句的用法,而另一方面是想向大家表明我们只注重方法(程序的规范写法)而不强调技巧。
说到学习单片机,使用C语言编程是大势所趋。但是话又说回来,单片机的学习毕竟与硬件电路有很多的联系,而学习汇编语言则会对单片机的硬件结构有更多的了解。所以学习汇编语言与学习C语言并不矛盾。使用C语言编程的,可以了解一下汇编语言,以便更深理解单片机的结构;使用汇编语言编程的,如果想尽快进入应用领域,则应该再学习C语言。而我们这种汇编程序的规范写法是与C语言的编程思想完全一致的。
也就是说有了这种规范写法的训练,再学习C语言,那真是易如反掌。就像你学习开车,学会了开手动档的汽车,让你开自动档的汽车,你会有什么感觉?一定是不在话下吧。但是反过来会怎样呢?所以从汇编语言学习单片机的朋友不会吃亏,如果再学会了C语言,那真是如虎添翼呢。
希望我们这种程序的规范写法能对正在学习汇编语言的朋友有所帮助。当然,看一两篇文章并不能够学会汇编语言,重要的还是多写多练,才能真正进入单片机编程的殿堂。