51单片机GPIO介绍

51单片机4类GPIO口特点说明:

  1. P0口是漏极开路,要使其输出高电平,必须外接上拉电阻,通常选择4.7K~10K阻值
  2. P0、P1、P2几乎都用做普通I/O口使用,即可作为输入,有课作为输出
  3. P3口即可用作普通I/O口,又可作为第二功能使用,比如窗口、外部中断、计数器等

GPIO概念

  GPIO(general purpose intput output)是通用输入输出端口的简称,可以通过软件来控制其输入和输出。51单片机芯片的GPIO引脚与外部设备连接起来,从而实现与外部通讯、控制以及数据采集的功能。不过 GPIO 最简单的应用还属点亮 LED 灯了,只需通过软件控制 GPIO 输出高低电平即可。当然 GPIO还可以作为输入控制,比如在引脚上接入一个按键,通过电平的高低判断按键是否按下。

  GPIO引脚图中带有Px.x等字样的均属于GPIO引脚。从引脚图可以看出,GPIO占用了芯片大部分的引脚,共达32个,分为4组,P0、P1、P2、P3,每组为8可IO,而且在P3组中每个IO都具备额外功能,只要通过相应的寄存器设置即可配置对应的附加功能,同一时刻,每个引脚只能使用该引脚的一个功能。

GPIO结构框图于工作原理

P0端口

  P0端口还有8位引脚,下图为其中一个,其他几个与之完全一致,因此只需了解当中一个即可。如下图所示:

  由上图可见,P0端口由锁存器、输入缓冲器、切换开关、一个非门、一个与非门及场效应管驱动电路构成。再看图的最右边,标号为P0.x引脚的图标,也就是说P0.x引脚可以是P0.0到P0.7的任何一位,即在P0口由8可于上图相同的电路组成。

输入缓冲器

  在 P0口中,有两个三态的缓冲器,在学数字电路时,我们已知道,三态门有三个状态,即在输出端可以是高电平、低电平,同时还有一种就是高阻状态(或称为禁止状态),大家看上图,上面一个是读锁存器的缓冲器,也就是说,要读取D锁存器输出端Q的数据,那就得使读锁存器的这个缓冲器的三态控制端(上图中标号为’读锁存器’端)有效。下面一个是读引脚的缓冲器,要读取 P0.x引脚上的数据,也要使标号为’读引脚’的这个三态缓冲器的控制端有效,引脚上的数据才会传输到我们单片机的内部数据总线上。

D锁存器

  构成一个锁存器,通常要用一个时序电路,时序的单元电路在学数字电路时我们已知道,一个触发器可以保存一位的二进制数(即具有保持功能),在51单片机的 32 根 I/O 口线中都是用一个D触发器来构成锁存器的。大家看上图中的D锁存器,D端是数据输入端,CP(CLK)是控制端(也就是时序控制信号输入端),Q是输出端,Q 非是反向输出端。
  对于D触发器来讲,当D输入端有一个输入信号,如果这时控制端 CP没有信号(也就是时序脉冲没有到来),这时输入端D的数据是无法传输到输出端Q及反向输出端Q非的。如果时序控制端CP的时序脉冲一旦到了,这时D端输入的数据就会传输到Q及Q非端。数据传送过来后,当CP时序控制端的时序信号消失了,这时,输出端还会保持着上次输入端D的数据(即把上次的数据锁存起来了)。如果下一个时序控制脉冲信号来了,这时D端的数据才再次传送到Q端,从而改变Q端的状态。

多路开关

  在 51 单片机中,当内部的存储器够用(也就是不需要外扩展存储器时,这里讲的存储器包括数据存储器及程序存储器)时,P0 口可以作为通用的输入输出端口(即 I/O)使用,对于 8031(内部没有 ROM)的单片机或者编写的程序超过了单片机内部的存储器容量,需要外扩存储器时,P0口就作为’地址/数据总线使用。那么这个多路选择开关就是用于选择是做为普通 I/O 口使用还是作为数据/地址’总线使用的选择开关了。大家看上图,当多路开关与下面接通时P0 口是作为普通的 I/O 口使用的,当多路开关是与上面接通时,P0 口是作为’地址/数据’总线使用的。

场效应管输出驱动

  从上图中可以看出,P0 口的输出是由两个 MOS 管组成的推拉式结构,也就是说,这两个 MOS 管一次只能导通一个,当 V1 导通时,V2 就截止,当 V2 导通时V1 截止。

P1端口

  P1 口的结构最简单,用途也单一,仅作为数据输入/输出端口使用。输出的信息有锁存,输入有读引脚和读锁存器之分。P1端口的一位结构见下图:

  由图可见,P1 端口与 P0 端口的主要差别在于,P1 端口用内部上拉电阻 R 代替了 P0 端口的场效应管 V1,并且输出的信息仅来自内部总线。由内部总线输出的数据经锁存器反相和场效应管反相后,锁存在端口线上,所以,P1 端口是具有输出锁存的静态口。
  要正确地从引脚上读入外部信息,必须先使场效应管关断,以便由外部输入的信息确定引脚的状态。为此,在作引脚读入前,必须先对该端口写入 l。具有这种操作特点的输入/输出端口,称为准双向 I/O 口。8051 单片机的 P1、P2、P3 都是准双向口。P0 端口由于输出有三态功能,输入前,端口线已处于高阻态,无需先写入 l 后再作读操作

P2端口

  P2 端口的一位结构见下图:

  由图可见,P2 端口在片内既有上拉电阻,又有切换开关 MUX,所以 P2 端口在功能上兼有 P0 端口和 P1 端口的特点。这主要表现在输出功能上,当切换开关向下接通时,从内部总线输出的一位数据经反相器和场效应管反相后,输出在端口引脚线上;当多路开关向上时,输出的一位地址信号也经反相器和场效应管反相后,输出在端口引脚线上。
  P2 口既可作为 I/O 口使用,也可作为地址总线使用,通常主要用作 I/O 口使用,地址总线使用不作分析。

P3端口

  P3 口是一个多功能口,它除了可以作为 I/O 口外,还具有第二功能,P3 端口的一位结构见下图:

  由上图可见,P3 端口和 Pl 端口的结构相似,区别仅在于 P3 端口的各端口线有两种功能选择。当处于第一功能时,第二输出功能线为 1,此时,内部总线信号经锁存器和场效应管输入/输出,其作用与 P1 端口作用相同,也是静态准双向 I/O 端口。当处于第二功能时,锁存器输出 1,通过第二输出功能线输出特定的内含信号,在输入方面,即可以通过缓冲器读入引脚信号,还可以通过替代输入功能读入片内的特定第二功能信号。由于输出信号锁存并且有双重功能,故P3 端口为静态双功能端口。

LED简介

  LED 即发光二极管。它具有单向导电性,通过 5mA 左右电流即可发光,电流越大,其亮度越强,但若电流过大,会烧毁二极管,一般我们控制在 3mA-20mA之间,通常我们会在LED管脚上串联一个电阻,目的就是为了限制通过发光二极管的电流不要太大,因此这些电阻又可以称为“限流电阻”。当发光二极管发光
时,测量它两端电压约为 1.7V,这个电压又叫做发光二极管的“导通压降”。

  下图左右分别为直插式发光二极管和贴片式发光二极管实物图。发光二极管正极又称阳极,负极又称阴极,电流只能从阳极流向阴极。直插式发光二极管长脚为阳极,短脚为阴极。仔细观察贴片式发光二极管正面的一端有彩色标记,通常有标记的一端为阴极。

硬件设计

  开发板上LED模块电路如下图所示:

  该模块独立,可自由连接单片机IO口,因此D1-D8可连接到单片机的P20-P27口。图中LED采用共阳接法,即所有LED阳极管脚接电源VCC,阴极管脚通过一个470Ω的限流电阻连接到P2口上。如果想要51单片机控制LED发光就必须通过单片机管脚在P2口上输出低电平,若为高电平则熄灭。

软件设计

点亮第一个LED

  本实验要实现的功能是:点亮D1指示灯,即让P2.0管脚输出一个低电平。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include "reg52.h"

// 将P2.0管脚定义为LED1
sbit LED1 = P2^0;

void main()
{
// LED1端口设置为低电平
LED1 = 0;
while(1)
{

}
}

通过sbit

LED闪烁实验

  如果要实现LED闪烁,只需要循环让D1指示灯先亮一会后熄灭。

这里就有一个延时问题,如何来产生延时呢?

  我们知道单片机执行每条代码指令都是需要时间的,在前面介绍C语言时讲解过循环语句,因此只需编写一个循环函数,让 CPU不干其它事,专门在那循环运行即可实现延时功能。依据人的肉眼余晖效应,延时时间不能太短,否则就无法观察到LED闪烁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include "reg52.h"

// 将P2.0管脚定义为LED1
sbit LED1 = P2^0;

// 对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned char u8;

void delay_10us(u16 ten_us)
{
while(ten_us--);
}

void main()
{
while(1)
{
LED1 = 0; // 点亮
delay_10us(50000); // 大约延时450ms
LED1 = 1; // 熄灭
delay_10us(50000);
}
}

Keil仿真查看延时时间

  上述代码中我们传递实参是50000,得到的延时大约是450ms,如何来验证呢?可以通过KEIL自带的软件仿真功能,操作如下

  1. 打开实验工程,点击魔术棒,选择“Target”选项卡,在Xtal(Mhz)文本框中输入12M,改值表示开发板上实际使用外部晶振大小,如果开发版上使用外部晶振是11.0592M,则修改为对应值,然后点击OK
  2. 点击仿真按钮,进入仿真界面,如下所示:
  3. 点击RST按钮,重新复位系统参数,此时参数列表中sec则为0,然后在需要查看调试代码数字前面用鼠标双击即可打上断电。如果再次双击,即可取消该断电。
  4. 通过执行延时前后的时间对比得出delay_10us(50000)运行的实际时间为0.450212s

LED流水灯实验

  如果要实现LED流水灯,只需循环让D1-D8指示灯逐个点亮,本实验同样需要用到延时。要实现循环点亮,可以使用最容易理解的方法:依次点亮D1、D2、D3…同时保持其他LED灯为熄灭状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
#include "reg52.h"

// 使用宏定义P2端口
#define LED_PORT P2

// 对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned char u8;

void delay_10us(u16 ten_us)
{
while(ten_us--);
}

void main()
{
LED_PORT = 0xFF; // 全部熄灭
while(1)
{
LED_PORT = 0xFE; // 11111110 点亮D1
delay_10us(50000);
LED_PORT = 0xFD; // 11111101 点亮D2
delay_10us(50000);
LED_PORT = 0xFB; // 11111011 点亮D3
delay_10us(50000);
LED_PORT = 0xF7; // 11110111 点亮D4
delay_10us(50000);
LED_PORT = 0xEF; // 11101111 点亮D5
delay_10us(50000);
LED_PORT = 0xDF; // 11011111 点亮D6
delay_10us(50000);
LED_PORT = 0xBF; // 10111111 点亮D7
delay_10us(50000);
LED_PORT = 0x7F; // 01111111 点亮D8
delay_10us(50000);
}
}

左移运算符实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include "reg52.h"

// 使用宏定义P2端口
#define LED_PORT P2

// 对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned char u8;

void delay_10us(u16 ten_us)
{
while(ten_us--);
}

void main()
{
u8 i;
LED_PORT = 0xFF; // 全部熄灭
while(1)
{
for(i = 0;i < 8;i++)
{
// 将00000001右移i位,然后取反将结果赋值给LED_PORT
LED_PORT=~(0x01<<i);
delay_10us(50000);
}
}
}

移位库函数实现

  Keil C51软件内还有对应的移位库函数,左移函数是_crol_(),右移函数是_cror_(),要使用则两个函数必须包含intrins.h头文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include "reg52.h"
#include "intrins.h"

// 使用宏定义P2端口
#define LED_PORT P2

// 对系统默认数据类型进行重命名
typedef unsigned int u16;
typedef unsigned char u8;

void delay_10us(u16 ten_us)
{
while(ten_us--);
}

void main()
{
u8 i;
LED_PORT = 0xFE;
while(1)
{
for(i = 0;i < 7;i++)
{
LED_PORT = _crol_(LED_PORT,1); // 左移一位
delay_10us(50000);
}
for(i = 0;i < 7;i++)
{
LED_PORT = _cror_(LED_PORT,1); // 右移一位
delay_10us(50000);
}
}
}