所谓GPIO,就是通用型输入输出(General Purpose Input/Output),GPIO试验是单片机引脚的基本输入输出功能。现在来实现这样一个场景:四个按键作为开关、四个LED作为响应,每个开关控制一盏灯,按一次开灯,再按一次关灯。再接入一个蜂鸣和另外两个按键作为音量加和音量减,音量加每按一次蜂鸣器音量增加5%,音量减每按一次音量减少5%。
一、仿真电路图设计
1、最小系统板
单片机只是一个芯片,它想要跑起来肯定还需要若干外围电路。
①电源:在Protues仿真中芯片的VCC和GND被略掉了,VCC就是接电源的正极,GND就是接地(电源负极),AT89C51需要电压为5V的电源供电,这款单片机的引脚输出高电平为5V左右、低电平为0V左右。
②振荡电路:振荡电路以晶振为核心,搭配一些电容组成。单片机的XTAL1、XTAL2就是外接晶振用的。在Protues仿真电路中,也可以不外接晶振,更快捷的方法是双击AT89C51芯片直接设置振荡频率,但为了和真实的电路保持一致,上图中还是接入了晶振,振荡频率设置为11.0592MHz(为什么单片机的晶振用这么一个奇怪的频率,而不直接使用整数频率的晶振?这个和串口波特率有关,等用到51单片机的串口的时候就会明白)。这个振荡频率和我们经常提到的电脑CPU频率本质上是一回事,指的是芯片运行时的时钟频率。当然,稍高档的芯片都会支持分频、倍频、超频,这个以后会讨论。
③复位电路:电路如上图所示,按下按键会使单片机复位。实际电路中具体要参考芯片手册实现。
仿真电路中即使不搭建振荡电路、复位电路,只有一个单独的AT89C51芯片,也可正常运行,因为Protues默认做了这些配置。但实际制作电路板的时候,这些器件必不可少。
上图中仔细观察会发现每个电路接口处会出现一个方形点,有红色、蓝色、灰色。其中红色方点代表高电平状态、蓝色方点代表低电平状态、灰色方点代表高阻态(高电阻状态,单片机输出哪种电平给它,或外接哪种电平给它,它就是哪种状态)。
2、LED模块
上图中LED以共阳极的接法接入电路,即LED的正极接电源,负极端接单片机。共阴极的接法是LED负极接地,正极接单片机。共阳极接法单片机输出低电平时LED亮起,输出高电平时LED熄灭。共阴极接法则相反。之所以采用了共阳极接法,是因为单片机的灌流(电流灌入单片机)能力略高于其电流输出驱动能力。在Protues中表现不太明显,即使采用共阴极接法,用单片机输出的电流仍然可以点亮LED,但在现实中使用实体单片机时,会发现单片机能直接驱动的电子器件很少很少,多是通过驱动芯片、放大电路、继电器等元件去驱动其他电子器件的。即使是灌流能力,单片机也比较弱,详细可以参考对应的芯片手册,如AT89C51是在100mA左右,电流过大会导致芯片烧毁,虽然在Protues中没有烧毁这种概念(不过话又说回来,不烧掉一堆元件,炸掉一堆管子也成不了真正的电子工程师)。双击LED可以看到,其启动电压为2.2V,电流为10mA,所以此处接了470Ω的限流电阻,保证电流能驱动起来LED,但又不至于有太大的电流输入到单片机中。
3、按键模块
按键模块比较容易理解,单片机P1默认是高电平(因为其内部有上拉电阻,把P1引脚的输出电平上拉为高电平,同理,下拉电阻会把引脚下拉成低电平。AT89C51的P0默认为高阻态,P1~P3都是默认为高电平),当按键按下时,对应的引脚接地,变成了低电平。按键抬起后,该引脚又变成了高电平。
4、蜂鸣器模块
蜂鸣器模块就加入了一个放大电路,基极电流微小的变化能引起集电极电流较大的变化,且二者符合一定的比例关系。所以P3^7引脚通过发出PWM波(PWM就是脉冲宽度调制,也就是占空比可变的脉冲波形。就是一个周期中高低电平所占的百分比可控),控制基极电流大小,进而控制集电极电流大小,再进而控制蜂鸣器的音量。Protues中蜂鸣器所需电压为12V,而单片机所需电压为5V,为便于仿真,图中直接使用了5V、12V两种电源端子,在实际应用中,一个设备不可能直接提供两种输入电压,而是提供一种输入电压,然后经过变压、分压等方式得到其他所需电压。
二、软件架构设计
区区一个开关灯也需要架构设计吗?当然,软件入门之初就应该思考其架构设计,这样在以后设计大型项目或者分析复杂的大型嵌入式系统时,才能够清晰明确。
对于没有跑操作系统的单片机裸机程序设计,可以参考上图所示的两种方式。
左侧是较为简单的系统,其核心是把与操作硬件相关的驱动程序归为一层,业务程序归为另一层。驱动层向应用层提供标准的函数接口,这样即使硬件层发生变化,只修改驱动层程序就可以了,只要驱动程序提供给应用层的接口未发生变化,应用层就无需修改。同理,如果只是业务需求发生了变化,硬件未发生改动,则驱动层基本上不需要修改。除非驱动层原来提供给应用层的接口无法满足新的业务需求。
右侧为较复杂的单片机系统,除了原有的硬件层、驱动层、应用层外,另加入了Framework层,这一层主要对标准操作做封装,向应用层提供标准的API(函数接口)、控制策略组合、各种函数组合。举个例子,当单片机接入LCD屏幕的时候,驱动层直接和LCD硬件设备打交道,控制LCD屏幕;Framework层提供在LCD上画出长方形、正方形、直线等API供应用层调用,并调用驱动层函数实现了这些标准方法。
三、Keil5新建工程
1、新建项目工程
打开keil5,选择“Project -- New uVision Project”
接下来会弹出Device选择框,选项如图所示。然后搜索下AT89C51,选中AT89C51,点击OK。
之后会弹出如图所示对话框,选择“是”。SATARTUP.A51里面有一些初始化操作,包括初始化SP、启动时对RAM数据清零等。如果删除SATARTUP.A51,单片机重启后(非断电重启),之前RAM空间的数据会保留。其实无论选择“是”或“否”,代码中都已包含该文件,这是跳入C函数之前执行的一段汇编代码,不加就用默认的启动代码;加了但是没修改这段代码,还是相当于使用默认的启动代码(这段代码刚刚添加上时和默认启动代码一致),这时加和没加都一样。如果采用汇编语言编程,建议不添加,因为需要改模块名称。如果采用C语言编程,建议添加,避免忘记某些初始化工作。
2、按照架构设计在项目工程下新建分组
之后点击三个方块摞在一起的那个图标,对刚刚新建的工程做一些配置。Project Targets是工程Target的名称,可以根据需要做出修改。Groups是源代码文件分组,可以修改,蓝色方框圈出的那个图标可以添加Groups,这里把SATARTUP.A51文件所在的文件夹名称改成了SATARTUP,新建了DRIVER分组,用于存放驱动程序,新建了APP分组,用于存放应用程序。注意,此处的分组并不是文件夹,也不会生成文件夹,而是Keil用于管理项目工程使用的分组,文件夹稍后会创建。
3、创建程序源码文件
之后该添加源文件了,点击“File -- New”创建文件
点击保存,就会有弹出框,让把文件保存到指定位置,本项目中我们可以这样存放文件
在项目目录下创建APP文件夹和DRIVER文件夹,里面分别创建source和include文件夹,其中source文件夹用于存放.c源文件,include文件夹用于存放.h头文件。上一步创建的文件是哪一类,就保存到哪个文件夹下,便于管理。如led驱动led.c文件放置到DRIVER/source文件夹下,led.h放置到DRIVER/include文件夹下。
4、程序源码文件关联到项目工程
文件创建完后,其实并没有和项目工程关联起来。这里注意,上面1、2步是创建配置项目工程,3步是创建源代码文件,现在两者都创建好了,但并未关联,需要把源文件添加到项目工程才算关联起来。添加方法为右键点击分组,点击如图所示选项,找到3步中创建的源文件,添加到对应分组。注意只添加.c文件,本项目添加完成后如下图所示。
5、添加头文件
上一步只添加了.c文件,那么.h文件是怎么和工程关联起来的呢?要知道使用一个.c文件中的方法,.h都是必不可少的。
①点击那个魔法棒图标
②选中C51选项卡
③点击蓝色框内的图标,选中你放置.h文件的文件夹,此处可以添加多个包含.h文件的文件夹,因为本项目比较简单,APP层只有一个.c文件,没有.h文件,所以这里只添加了DRIVER层的.h文件所在目录。选中后,系统已自动替换成了相对目录(头文件相对于项目所在的目录)。
6、配置编码格式
编码格式也顺便配置一下,否则在编写代码时,如果使用汉字注释会变成乱码。
四、LED驱动程序设计
AT89C51是一种8位单片机,即总线位数为8位。有四组IO口,即P0~P3,每组IO口有八个引脚,对应着一个8位寄存器,即8个bit位。每个引脚对应着一个bit位。这个bit位为0时,引脚输出低电平,这个bit位为1时,引脚输出高电平。反之亦然,外部对这个引脚输入低电平时,内部对应的bit位可以读取到0,对这个引脚输入高电平时,内部对应bit可以读取到1。对LED的操作就是想要开关它,那么这样设计它的驱动程序:
led.h文件,定义了四个函数,从上到下分别为开灯、关灯、获取灯的当前状态、操作灯。参数site表示当前灯是哪一个,on_off表示要打开还是关闭。
#ifndef _LED_H_
#define _LED_H_
void led_on(unsigned char site);
void led_off(unsigned char site);
char get_led_status(unsigned char site);
void led_operate(unsigned char site,unsigned char on_off);
#endif
led.c
#include <reg52.h>
#include "led.h"
sbit led_d1 = P0^0;
sbit led_d2 = P0^1;
sbit led_d3 = P0^2;
sbit led_d4 = P0^3;
void led_on(unsigned char site){
switch(site){
case 0:
led_d1 = 0;
break;
case 1:
led_d2 = 0;
break;
case 2:
led_d3 = 0;
break;
case 3:
led_d4 = 0;
break;
default:
break;
}
}
void led_off(unsigned char site){
switch(site){
case 0:
led_d1 = 1;
break;
case 1:
led_d2 = 1;
break;
case 2:
led_d3 = 1;
break;
case 3:
led_d4 = 1;
break;
default:
break;
}
}
char get_led_status(unsigned char site){
switch(site){
case 0:
return led_d1;
case 1:
return led_d2;
case 2:
return led_d3;
case 3:
return led_d4;
default:
return -1;
}
}
//on_off 0:开灯 1:关灯
void led_operate(unsigned char site,unsigned char on_off){
if(on_off == 0){
led_on(site);
}else if(on_off == 1){
led_off(site);
}
}
五、按键驱动程序设计
目前还没有接触到中断,所以按键的状态需要程序循环扫描获得。由于按键机械特性所致,按键按下与放开时都有一个抖动过程,就是当按键按下时电平并不是瞬间达到低电平并保持不变,而是抖动之后才稳定下来的。按键释放时也是一样。
所以在程序设计中有一个去抖动操作,“如果发现按键按下(读取到低电平) → 等10毫秒 → 按键仍是按下状态(仍是低电平)”,说明按键已经稳定按下了。然后等待按键释放(再次读取到高电平),按键释放后即为完成了一次完整的按键操作,可以进行相关动作了。可以这样设计驱动程序:
key.h
#ifndef _KEY_H_
#define _KEY_H_
char scan_keyboard();
#endif
key.c
#include <reg52.h>
#include "delay.h"
#include "key.h"
sbit key1 = P1^0;
sbit key2 = P1^1;
sbit key3 = P1^2;
sbit key4 = P1^3;
sbit key5 = P1^4;
sbit key6 = P1^5;
char scan_keyboard(){ //返回当前操作过的按键位置
char site = -1;
if(key1 == 0){
delayms(10);
if( key1 == 0){
while(!key1);
site = 0;
}
}else if(key2 == 0){
delayms(10);
if( key2 == 0){
while(!key2);
site = 1;
}
}else if(key3 == 0){
delayms(10);
if( key3 == 0){
while(!key3);
site = 2;
}
}else if(key4 == 0){
delayms(10);
if( key4 == 0){
while(!key4);
site = 3;
}
}else if(key5 == 0){
delayms(10);
if( key5 == 0){
while(!key5);
site = 4;
}
}else if(key6 == 0){
delayms(10);
if( key6 == 0){
while(!key6);
site = 5;
}
}
return site;
}
delayms为毫秒级延时函数,其代码如下:
#ifndef _DELAY_H_
#define _DELAY_H_
void delayms(unsigned int xms);
#endif
#include "delay.h"
void delayms(unsigned int xms){ //毫秒级延时函数
unsigned int i,j;
for(i=xms;i>0;i--){
for(j=110;j>0;j--);
}
}
就是执行循环操作,其他什么也不做。根据晶振频率计算,晶振11.0592MHz,执行110次无其他逻辑的循环大约耗时1ms。(这个并不太准,和时钟周期、指令周期、状态周期、机器周期等都有关系,精确的计时不会使用这种,而是会使用定时器中断实现。)
六、蜂鸣器驱动程序设计
蜂鸣器的控制相对就比较简单了,使用了一个引脚,引脚高电平时蜂鸣器有电流通过,低电平时,蜂鸣器无电流通过。通过控制一个周期内高低电平的占比,就可以控制通过蜂鸣器的电流,进而控制其音量。本项目中一个周期可以设置为20节拍,每次按下音量按键时对高电平增或减一个节拍,即每次音量增减5%。
buzzer.h
#ifndef _BUZZER_H_
#define _BUZZER_H_
void buzzer_open();
void buzzer_off();
#endif
buzzer.c
#include <reg52.h>
#include "buzzer.h"
sbit buzzer = P3^7;
void buzzer_open(){
buzzer = 1;
}
void buzzer_off(){
buzzer = 0;
}
七、应用程序设计开发
前面的都搞定了,应用程序就比较容易了。裸机单片机有一个共同的特点,就是程序都是跑在一个死循环中的,没错,就是死循环。除非断电,这个死循环算是结束了,再上电,这个死循环又开始了。为什么呢?因为如果不是个死循环,单片机一个任务跑完就再也不会自动跑第二次。蜂鸣器部分其实设计的有点粗糙,按键被按下等待释放的过程中,蜂鸣器是一直在响的,因为等待按键释放时这个死循环被卡住了,相当于延长了蜂鸣器的某个节拍。不操作按键时,蜂鸣器的音量和预期一致。本此实验主要体验Keil5的使用、单片机软件架构和AT89C51的输入输出。
application.c
#include "led.h"
#include "key.h"
#include "buzzer.h"
#define MAX_VOL 20
#define MIN_VOL 1
void main(){
unsigned char volume = 10;
//周期计数,忽略键盘扫描,用于蜂鸣器控制
//如volume = 10,即20个周期有10个输出高电平
unsigned char cycle = 0;
while(1){
unsigned char key_site = scan_keyboard(); //扫描按键状态
char led_status = -1;
cycle++;
switch(key_site){
case 0: //按键一被按了一次
led_status = get_led_status(0);
if(led_status == 0){
led_operate(0,1); //D1状态改变
}else if(led_status == 1){
led_operate(0,0); //D1状态改变
}
break;
case 1:
led_status = get_led_status(1);
if(led_status == 0){
led_operate(1,1); //D2状态改变
}else if(led_status == 1){
led_operate(1,0); //D2状态改变
}
break;
case 2:
led_status = get_led_status(2);
if(led_status == 0){
led_operate(2,1); //D3状态改变
}else if(led_status == 1){
led_operate(2,0); //D3状态改变
}
break;
case 3:
led_status = get_led_status(3);
if(led_status == 0){
led_operate(3,1); //D4状态改变
}else if(led_status == 1){
led_operate(3,0); //D4状态改变
}
break;
case 4: //蜂鸣器音量+
if(volume < MAX_VOL){
volume++;
}
break;
case 5: //蜂鸣器音量-
if(volume > MIN_VOL){
volume--;
}
break;
default:
break;
}
if(cycle >= 0){
buzzer_open();
}if(cycle >= volume){
buzzer_off();
}if(cycle >= (MAX_VOL+1)){
cycle = 0;
}
}
}
八、项目编译并仿真
1、点击魔法棒 --- OutPut --- 创建HEX文件
2、点击编译,看信息确认编译是否成功,如果失败则根据信息找出原因
3、编译成功后会在项目目录的Objects文件夹下生成.hex文件
4、Protues如果在运行需要先停止仿真,双击AT89C51芯片,在弹出框中Program File一项选中刚才的那个.hex文件。点击“确定”,再次启动仿真,运行的就是我们刚才编写的程序了。
九、资料下载
源码与仿真电路下载地址:https://download.csdn.net/download/qq_54140018/87654148
芯片手册与参考资料下载地址: https://download.csdn.net/download/qq_54140018/87646124