为了把目前所掌握的设计、电路、软件等技能综合起来运用,本节设计了一个应用实例——流水灯,看看单片机如何在具体系统中发挥作用以及单片机系统开发的一般性过程。实例还是以需求分析→电路设计→软件设计→仿真→实际系统调试为线索。
流水灯,就是若干个发光二极管排列成一列或别的形状,在单片机的控制下依次发光,让人感觉流动的效果。在夜晚,美丽的霓虹灯装点着城市,有的如高山流水,有的如不断变幻的彩虹,这都是从下面这个流水灯系统扩展而来的。
流水灯系统的功能设计为:单片机控制着8支发光二极管,每一时刻只有一支发光二极管点亮,如图4-50所示,延时200ms后熄灭而与之相邻的发光二极管点亮。直到点亮最后一个发光二极管后又以相反的方向依次点亮发光二极管。从视觉上有一支发光二极管向左右两边跑来跑去,颇有流水的效果。
图4-50 单片机控制8支发光二极管实现流水灯效果
从单片机最简系统出发,给I/O口添加8支发光二极管,得系统电路如图4-51所示。当然,除了使用P2口外还可以使用P0、P1、P3完成这个实例。这里省略了下载接口部分,可以参考图4-49来连接,本书实例的电路图都没有画出下载接口部分,但我们在实际制作中都要加上,以实现与下载线的连接。
图4-51 流水灯系统电路图
根据需求分析,只要程序使如图4-51所示单片机的P2口依次输出低电平就可以实现流水灯的效果,如图4-52所示,相当于一个“0”在P2口的8个位中先向右“跑动”,再向左“跑动”。
现在问题变成了如何让这个“0”在P2口中“跑动”起来。在51单片机中有一个很方便的指令供我们使用——RRC A。这是一个让进位标志位C在累加器A中从左向右移动的指令。
那什么是进位标志位C和累加器A呢?在单片机中有一个叫做“程序状态字”的特殊功能寄存器,英文缩写是PSW。PSW有8位(1个字节),如图4-53所示,最高位叫做进位标志位C,有时也标记为CY。其他位分别为辅助进位标志位(AC)、标志0(F0)、工作寄存器组别位1(RS1)、工作寄存器组别位0(RS0)、溢出标志(OV)、奇偶标志位(P)。
图4-52 “0”就是流动的灯
图4-53 PSW的8位
单片机在执行加法(或减法)运算过程中如果有进位(或借位)则PSW中的进位标志位C被硬件自动置1,如果没有进位(或借位)则为0。这个标志位就好像我们小学时用笔计算加法(或减法)时,在等号线上标记的进位(借位)一样,如图4-53所示。
累加器A是单片机另一个1个字节长度的特殊功能寄存器,可以在单片机运行时存储数据,是一个相当重要的寄存器。它与程序状态字PSW不同的是,累加器A与程序运行的状态无关,可提供给用户任意使用。在汇编程序中,可以简单地把累加器A看成是一个变量,进行赋值、运算等操作。关于程序状态字PSW和累加器A在后面章节还要讲到。
回到指令“RRC A”上,它的功能是让程序状态字PSW的进位标志位C在累加器A中右移一位。假设一开始进位标志位C=0且累加器A的8个位全为1,如图4-54(a)所示。当执行指令“RRC A”时,C=0进入了A的最高位(最左侧),原来最高位的1被0“挤”到它右边的邻居位上,而这个邻居位则被“挤”到它的右边邻居位上,重复这个向右“挤”的过程,直到最低位(最右侧)被“挤出”累加器A,如图4-54(b)所示,最低位被“挤出”来则进入了进位C中,使C=1。
在执行指令“RRC A”以前(图4-54(a)),累加器A=0FFH,执行之后(图4-54(b)),累加器A=7FH。如果再执行一次“RRC A”,则累加器A=0BFH(图4-54(c))。每执行一次指令,0就在累加器A中向右“挤”一位。
图4-54 RRC指令过程
指令“RRC A”中,“RRC”是助记符,含义是“进位C在累加器A中从左向右轮换一次”。“A”是操作数,代表累加器A。
我们发现,图4-54(b)、(c)中所示的累加器A的数值与图4-52矩阵中的前两行是一致的,只要再通过一个指令“MOV P2,A”,就可实现累加器A的数据从P2口输出,从而控制对应的发光二极管发光。
所以,继续重复执行“RRC A”和“MOV P2,A”就可以实现如图4-52所示的原理,在P2口的8个位上依次输出0,再配合延时就能实现流水灯的效果。
执行8次“RRC A”指令后,即发光二极管向一个方向“流动”8次之后需要向反方向“流动”,也就是需要“0”向反方向搬运。这时只需要一个与RRC操作完全相反的指令——“RLC A”即可。指令“RLC A”是一个让进位标志位C在累加器A中从右向左轮换的指令。
由于要执行8次“RRC A”或“RLC A”,还需要一个指令进行计数,这条指令是“DJNZ Rn,rel”,它的功能是将Rn减1,如果Rn≠0,则跳到rel处。
其中“DJNZ”是助记符,“Rn”代表工作寄存器R0、R1、R2、R3、R4、R5、R6、R7中的任意一个,工作寄存器Rn的长度也是一个字节,可在程序中看成是一个变量,所以Rn的最大值是0FFH。指令中“rel”代表的是相对地址(relative address)。一般我们使用标号作为DJNZ中的rel。
比如指令“DJNZ R1,LOOP”实现将R1的值减1,如果R1≠0,则跳到LOOP处。下面我们从程序4-1中看如何使用这条指令。
程序4-1:循环指令DJNZ的使用
首先,“MOV R1,#8”指令把立即数8载入工作寄存器R1,此时R1=8。第2行“LOOP”是一个标号,“RRC A”让进位标志位C在累加器A中从左向右轮换一次。第3行“DJNZ R1,LOOP”将R1减1,看R1是不是等于0,如果不等于,则跳到“LOOP”标号处循环执行。
这样一来,每执行一次“DJNZ R1,LOOP”,R1的值就少1,程序回到LOOP,进位标志位C在累加器A中从左向右轮换一次,完成一个循环。直到这个循环把R1减到0后,指令“DJNZ R1,LOOP”失效,程序转到这条指令的下一条指令开始执行。
有以上的基础再来看流水灯程序就容易得多了。程序4-2中加入了丰富的注释。我们只需要一边读这个程序,一边在纸上写下各个变量(A、C、P2、R1、R2、R3、R4、R5等)的值,加上前面的分析就可以很容易理解程序的思路了。
程序4-2:流水灯程序(对应图4-51)
指令小贴士:
接下来,请大家把本节的电路图(见图4-51)和程序4-1分别在Proteus和μVision中仿真和调试一下,感受利用辅助软件进行流水灯系统的开发过程。