第3章 流水灯重现——第一次操刀

第2章向读者介绍了几个工具(在以后的章节中会运用到),本章主要是跟大家一起来学习AVR单片机的I/O相关知识并运用——重现流水灯。让咱们从现在开始真正踏上单片机之路吧!

3.1 I/O简述

本节将对AVR单片机的I/O相关知识进行介绍,并和5 1单片机的I/O进行比较。这里具体介绍的类型是ATmega88V。

3.1.1 I/O寄存器

AVR单片机的I/O框图如图3.1所示。

图3.1 AVR单片机的I/O框图

AVR单片机的所有I/O端口都具有读/写修改功能,可以输出或吸收大电流,并直接驱动LED。每个I/O端口都有3个寄存器(用来设置读/写操作),分别为数据寄存器PORTx、数据方向寄存器DDRx和端口输入引脚寄存器PINx。其中数据寄存器和数据方向寄存器为读/写寄存器,而端口输入引脚寄存器为只读寄存器。将数据方向寄存器清零,数据寄存器置位,再将寄存器MCUCR(MCU控制寄存器)的PUD位置位,可使I/O端口处于高阻态。

ATmega88V有23个可编程的I/O端口,其中PORTB、PORTC和PORTD分别有7个、6个和7个I/O引脚,对于一般的应用来说是足够的。在运用过程中,不一定所有的I/O引脚都会使用到。如果有引脚未被使用,建议给其一个确定电平。例如,在一般情况下,初始化设备时,应将所有I/O端口统统设置一次,以确保I/O端口工作在已知的状态下。

另外,大多数I/O端口都具有第二功能。

3.1.2 与51单片机的比较

使用AVR单片机时,需要设置I/O端口的方向,如果不设置,将以默认的状态存在。而51单片机的I/O端口是准双向输出类型的。准双向输出类型可用做输出和输入功能而不需要重新配置I/O端口的方向。5 1单片机的I/O端口只有一个寄存器Px。

举例说明:需要设置某个I/O端口为输出高电平,则AVR单片机需要设置DDRx和PORTx 2个寄存器;而5 1单片机只需要设置Px为高即可。对于输入,AVR单片机仍需要设置2个寄存器,而5 1单片机直接读取Px的值即可。

在此做这个比较,是为了让读者了解AVR单片机与5 1单片机I/O端口设置的不同,在编写程序时,这是需要特别注意的地方,尤其是对于刚从5 1单片机转过来准备学习AVR单片机的工程师而言。长时间使用一款单片机会导致工程师将很多编写习惯不知不觉地用在其他单片机上,这是大部分工程师都会犯的错误。

3.2 让流水灯动起来

在很多不同种类的单片机教程中都是以流水灯作为第一个实验的,最为突出的是在51单片机的教程中。这里也以它为例进行介绍。

3.2.1 流水灯程序

流水灯程序用于实现对PD口下的8个LED灯进行操作:每500ms亮一个,当全部灯点亮后,每500ms灭一个,如此重复。这里还没接触到定时器的相关知识,因此可以使用库文件来实现延时500ms,不过由于该延时不是很准确,所以需要在代码工程中的2个地方做相应的设置:在源文件中添加#include<util/delay.h>;在工程选项中写入当前使用的晶振频率,如图3.2所示。图3.2中给出了一些重要信息,如工程所选择的器件型号“atmega88”,使用的晶振频率为“7372800”,优化级别默认为-0s。优化级别有以下几种。

(1)-00:无优化。

(2)-01:减少代码尺寸和执行时间,不进行需要大量编译时间的优化。

(3)-02:几乎执行所有优化,而不考虑代码大小和执行时间。

(4)-03:执行“-02”所有的优化及内联函数,重命名寄存器的优化。

(5)-0s:针对代码大小的优化,执行所有“-02”优化而不增加代码的大小。

开发时,应选择适合自己的一个优化级别。图3.2中框选标出的1 ~4这4个复选框的意义依次如下:

(1)所有char认为是unsined char;

(2)所有bitfields认为是unsigned;

(3)定义结构体时,连续存储;

(4)定义枚举类型时,使用最大需要的存储空间。

图3.2 工程选项

流水灯程序如下:

            /**********************************************
            project:流水灯工程
            IDE:AVR Studio 4+Winavr20070525
            device:atmega88
            author:lg
            date:2012-07-11 21:10
            Goal:点亮LED,掌握GPIO的运用
            ***********************************************/
            #include "main.h"
            /*****************************
                设备初始化
            *****************************/
            void DEVICE_init(void)
            {
                //----------PB口
                DDRB=0X00;     //未使用也定义
                PORTB=0X00;
                //----------PC口
                DDRC=0X00;   //未使用也定义
                PORTC=0X00;
                //----------PD口
                DDRD| =0XFF;  //设置PD口的0~7口方向为输出
                PORTD=0X00;  //设置PD口的0~3口电平为低
            }
            //----------------------MAIN-----------------------
            int main(void)
            {
                //------设备初始化
                DEVICE_init();
                //-----------循环-----------
                while(1)
                {
                //-------------点亮LED0~7----------------
                    PORTD| =_BV(0);        //点亮LED0
                    _delay_ms(500);         //延时500ms
                    PORTD| =_BV(1);        //点亮LED1
                    _delay_ms(500);
                    PORTD| =_BV(2);        //点亮LED2
                    _delay_ms(500);
                    PORTD| =_BV(3);        //点亮LED3
                    _delay_ms(500);
                    PORTD| =_BV(4);        //点亮LED4
                    _delay_ms(500);
                    PORTD| =_BV(5);        //点亮LED5
                    _delay_ms(500);
                    PORTD| =_BV(6);        //点亮LED6
                    _delay_ms(500);
                    PORTD| =_BV(7);        //点亮LED7
                    _delay_ms(500);
                //-------------灭掉LED0~7----------------
                    PORTD&=~_BV(0);       //点亮LED0
                    _delay_ms(500);         //延时500ms
                    PORTD&=~_BV(1);       //点亮LED1
                    _delay_ms(500);
                    PORTD&=~_BV(2);       //点亮LED2
                    _delay_ms(500);
                    PORTD&=~_BV(3);       //点亮LED3
                    _delay_ms(500);
                    PORTD&=~_BV(4);      //点亮LED4
                    _delay_ms(500);
                    PORTD&=~_BV(5);      //点亮LED5
                    _delay_ms(500);
                    PORTD&=~_BV(6);      //点亮LED6
                    _delay_ms(500);
                    PORTD&=~_BV(7);      //点亮LED7
                    _delay_ms(500);
                }
            }

在这里有一个地方需要注意,就是_BV是在sfr_defs.h中定义的,其定义为#define _BV (bit)(1 <<(bit))。当没有其他编译,而在移植时希望尽可能减少修改量时,就可以将该定义添加到代码中。

3.2.2 流水灯重现

流水灯最早是用5 1单片机实现的,这里将其在AVR单片机上重现。由于没有实际的板子,故这里采用仿真软件来实现。

下面根据前面介绍的Proteus的运用,重新建立一个流水灯的工程。首先添加MCU、晶振、电阻、电压表和示波器。在这里,电压表用来查看引脚的电压,示波器用来观察各个引脚的延时。仿真电路如图3.3所示。注意:仿真电路跟实际电路是有区别的,如仿真电路中的晶振需要接2个匹配的负载电容。

图3.3 仿真电路

然后向MCU添加流水灯程序的可执行文件,并将晶振的频率设置为7.3728MHz,其他根据需要进行相应的设置。完成一系列设置后,单击界面右下角中的,开始仿真,如图3.4所示。从图中可以看到电压表的值一直在变化(从0到3.3V变化),说明I/O端口在来回不停地被置高清零。还可以通过示波器来查看仿真结果。一个示波器只能有4个输入源,则8个I/O端口需要使用2个示波器。在仿真中选中示波器后单击鼠标右键,再单击“Digital Oscilloscope”,会弹出如图3.5所示的界面。

图3.4 仿真界面

图3.5 虚拟示波器界面1

在图3.5中显示出了4条不同颜色的线,黄色、蓝色、红色、绿色(从上到下)分别对应的是PD0、PD1、PD2和PD3。界面的右侧是示波器的相关设置,4个通道选择的都是DC源,电压单位是1 V/格,扫描时间为50ms/格。将扫描时间调整为20ms/格,如图3.6所示,这样就比较容易看出延时的间隔。3.2.1节介绍的流水灯程序中实现的是500ms的间隔,而从图3.6中得到的只有40ms左右的一个间隔,与程序要求相差太多,但是在这里先不做调整,只是简单地给读者介绍一下这个可以用来测量程序中所要求的时序的工具而已。实际中也有类似的工具,可以用来代替示波器,如逻辑分析仪,其在价格和使用方面都是比较适合单片机工程师的。在程序运行过程中可以看到LED0 ~LED7不停地来回亮灭,这样流水灯便得以重现了。如果你手上有硬件调试板,则可以直接在板上运行以查看效果。

图3.6 虚拟示波器界面2

3.3 小结

本章对AVR单片机的I/O相关知识做了一些简单叙述,并通过实际程序工程来说明了I/O端口的一些设置及运用。本章还回顾了第2章提到的Proteus软件的运用,并且简单介绍了如何运用它来查看程序所实现的功能。希望读者在程序运行过程中积累出一套属于自己的调整步骤或方法。