从51到ARM裸机开发实验(003) AT89C51 GPIO实验

        所谓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

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值