51单片机入门第二课:GPIO

目录

Part1:回顾第一课

Part2:认识单片机引脚,GPIO的输入和输出

GPIO的作为输出端口(点灯蜂鸣器等):

为了方便大家灵活改变寄存器的值,下面列出三种改变寄存器值的常用操作

GPIO的作为输入端口(按键):

一般我们有两种消抖的方法:

Part3:通过GPIO控制数码管:静态及动态数码管的使用

Task:数码管时钟(按键与数码管的应用)

结语:


 

Part1:回顾第一课

第一课我们学习了如何创建新工程,并将hex文件下载到单片机,还学习了通过赋值语句改变寄存器P2的值,来改变单片机P2引脚上的电平高低,从而控制P2引脚所连接的LED的亮灭,以及如何通过位移操作("<<")对寄存器中的值进行整体位移来实现流水灯的效果。而改变单片机引脚电平的操作其实就是GPIO操作,这节课我们一起来重新认识GPIO的输入输出功能。

Part2:认识单片机引脚,GPIO的输入和输出

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

下面我们一起来看看GPIO结构:

我们所使用的51单片机型号为STC89C52RC, 此芯片共有下图40 引脚:

当然并不是所有引脚都是 GPIO , 51 单片机这40个引脚可以分为这五大类:

1)电源引脚: 引脚图中的 VCC、 GND 都属于电源引脚。
2)晶振引脚: 引脚图中的 XTAL1(X1)、 XTAL2(X2) 都属于晶振引脚。
3)复位引脚: 引脚图中的 RES 属于复位引脚, 用于单片机复位。
4)下载引脚: 51 单片机的串口功能引脚(TXD、 RXD) 作为下载引脚和串口使用。
5)GPIO 引脚: 引脚图中带有 Px.x 等字样的均属于 GPIO 引脚。 从引脚图可以看出,GPIO 占用了芯片大部分的引脚, 共4X8=32 个, 分为了 4 组(P0、 P1、P2、 P3), 每组为 8 个GPIO(我们使用的STC89C52RC还有额外的P4口), 只要通过相应的寄存器设置即可配置对应的引脚电平以及附加功能。

值得注意的是51 单片机所有 GPIO 口都是双向的, 也就是可以作为输入也可以作为输出使用。而常用的四组GPIO口中由于 P0 口是漏极开路的, 所以要操作 P0 口必须像上面图中P0口一样外接上拉电阻, 其他P1、 P2、 P3 口芯片内部自带上拉电阻, 可以不加, 如果要增强 GPIO 口驱动能力, 可以再外接上拉电阻。

GPIO的作为输出端口(点灯蜂鸣器等):

作为输出端口,我们上节课已经学过,可以给GPIO寄存器赋值改变对应GPIO引脚上的电平(典型的例子就是上节课我们的点灯以及让蜂鸣器响起来)这里简单回顾下蜂鸣器的程序:


#include <REGX52.H>
 
sfr P4 = 0xe8;        //该型号有额外的P4寄存器,而这个没有在头文件中定义,需要先定义地址再使用
sbit Beep = P4^1;     //P4^1指P4寄存器的第一位,给他取个别名叫Beep
 
void Delay(unsigned char xms);		//延时x个10ms的函数(略)
 
int main(void)
{
  while(1)            
  {
    Beep=0;           //将P4_1引脚设为低电平蜂鸣器
    Delay(100);
    Beep=1;           //将P4_1引脚设为高电平关闭蜂鸣器
    Delay(100);       //响一会儿,不响一会
  }                
}

为了方便大家灵活改变寄存器的值,下面列出三种改变寄存器值的常用操作

1)寄存器整体赋值

P2 = 0xFE; //将十六进制FE赋给P2寄存器

 2)改变寄存器单个位的值

//方式1
P2_0 = 1;    //单独将P2寄存器的最低位置零,即将P2.0口置位高电平 

//方式2
P2^0 = 1;    //效果同上,此处使用"^"符号来间接寻址

//方式3
sbit LED = P2^0;    //位定义(给该位取别名,比如这里的LED代表P2寄存器0位),此句放于main函数之外
LED = 1;     //效果同上

  3)寄存器的位移操作

//位移操作符"<<"
P2 = 0x01;        //此时P2寄存器中的值:0000 0001
P2 = 0x01 << 1;   //此时P2寄存器中的值:0000 0010
P2 = 0x01 << 2;   //此时P2寄存器中的值:0000 0100
P2 = 0x01 << 3;   //此时P2寄存器中的值:0000 1000
...
P2 = 0x01 << 7;   //此时P2寄存器中的值:0001 0000

GPIO的作为输入端口(按键):

我们已经学习了将GPIO当作输出端口使用,也就是通过改变寄存器的值来改变芯片引脚上的电平。如果我们想要把GPIO当作输入端口读取寄存器里的值其实也很简单,当单片机引脚上的电平发生改变,对应寄存器里的值也会发生改变,比如按下按键,引脚上的电平由高电平变成低电平,该引脚对应寄存器里的那一位bit就会从1变成0,我们可以通过一个变量把它读出来,比如:

unsigned char value;
value = P3_3;    //把当前P3.3引脚上的电平值赋给value

在我们这个大信息时代,生活中 常常需要我们记住各种各样的密码以及使用各种各样的密码,甚至在使 用简单的计算器等工具时,我们都需 要进行输入操作,而搭载输入操作功能的基本器件就是我们的按键,本节我们主要学习由单片机控制的独立按键操作。

独立按键就是典型的输入设备,我们通过一个例子来学习下怎么检测按键按下,实现输入功能。

首先我们来看看常用微动开关的结构,就像下图一样简单:

理想情况下,当我们按下按键,两边导通单片机引脚上的电平就由高转低(单片机引脚是弱上拉,强下拉,接地会使单片机引脚电平被拉下来),然而理想很丰满,现实很骨感,按下按键电平并不会马上变低,而是电压要抖动一小会儿,此时我们就要进行按键的消抖。需要按键消抖通常的按键所用开关为机械弹性开关,当机械触点断开、闭合时,由于机械触点的弹性作用,一个按键开关在闭合时不会马上稳定地接通,在断开时也不会一下子断开。因而在闭合及断开的瞬间均伴随有一连串的抖动,为了不产生这种现象而作的措施就是按键消抖。用图形描述这个过程就是:

一般我们有两种消抖的方法:

1)硬件消抖
硬件消抖就是在按键上并联一个电容,如图,利用电容的充放电特性来对抖动过程中产生的电压毛刺进行平滑处理,从而实现消抖。

2)软件消抖

软件消抖就是通过程序进行二次判断是否按下,如果检测到按键按下后一般过20ms还是检测到按键按下就当做有效的按下,或者和江协科技视频里的一样检测到按键按下后再检测到按键松开才视为一次有效的按下。我们来看一个例子:

unsigned char keyPressed = 0;

void main()
{
    while(1)    //不停地检测按键是否按下,循环执行
    {
        if(P3_3 == 0)        //P3_3引脚上的按键按下,该引脚上的电平由高电平变到低电平P3_3=0
        {
            Delayms(20);     //延时20ms跳过抖动的时间
            if(P3_3 == 0)    //再次检测按键是否按下
            {
                keyPressed = 1;    //确认按下,执行操作
            } 
        }
    }
}

我们也可以自己写个函数来读取四个独立按键的值,这个例子里我们不断检测哪个按键按下并点亮板子上对应的LED灯:

#include <REGX52.H>

void Delayms(unsigned int tc) //函数延时 tc 毫秒,子函数
{
    unsigned int i;
    while(tc != 0) 
    {
        for(i = 0; i < 122; i++);
        tc--;
    }
}

unsigned char keyRead(void)
{
    if(P3_3==0){Delayms(20);if(P3_3==0)return 1;}    //按下key1按键返回1
    if(P3_7==0){Delayms(20);if(P3_7==0)return 2;}    //按下key2按键返回2
    if(P3_6==0){Delayms(20);if(P3_6==0)return 3;}    //按下key3按键返回3
    if(P3_5==0){Delayms(20);if(P3_5==0)return 4;}    //按下key4按键返回4
    return 0;                                        //什么键都不按就返回0
}

void main()
{
    unsigned char keyValue = 0;
    while(1)
    {
        keyValue = keyRead();    //读取哪个按键按下了
        switch(keyValue)
        {
            case 0:    P2=0xFF; break;    //没按灯全部灭
            case 1:    P2=0xFE; break;    //按下key1第1个LED亮
            case 2:    P2=0xFD; break;    //按下key2第2个LED亮
            case 3:    P2=0xFB; break;    //按下key3第3个LED亮
            case 4:    P2=0xF7; break;    //按下key4第4个LED亮
        }
    }
}

下面我们来看看这个程序运行的效果,是否和我们想象的一致:

按键

Part3:通过GPIO控制数码管:静态及动态数码管的使用

数码管相信大家都不陌生,数码管也称 LED 数码管,颜色有红,绿,蓝,黄等几种。led 数码管广泛用 于仪表,时钟,车站,家电等场合。比如下图的时钟。
下面我们一起来看看数码管的电路结构,看下图我们可以知道数码管的每个段其实就是一个小LED,通过组合显示我们想要的数字。数码管分共阴型和共阳型。共阴型:阴极连在一起接地;共阳型:阳极连在一起接电源正极。如下图:
 
如果阴极为数码管的公共端,按照发光二极管的原理,当阳极接电源正极,阴极接电源负极,发光二极管点亮。换句话说,共阴极的数码管,当段选引脚 a,b,c,d,e,f,g 脚分别接到电源的正极,而 GND 脚接的电源负极,7个发光二极管同时点亮,并且显示“8”还有注意的是右下角“DP”或 “H”是小数点。共阳二极管取反就是了。
 
假设有一个共阴极的数码管, 公共端已经接到低电平。段选管脚:DP,G,F,E,D,C,B,A 连接到单片机 P1 端口 7,6,5,4,3,2,1,0 管脚。如果要显示数字“0”需要点亮 A,B,C,D,E,F 六个 LED,即连接这六个 LED 的引脚输出高电平,其余为低电平。(共阳管取反即可)
我们可以将所有要显示数字所需的寄存器值组合成一个数组来当作一个码表存放,将码表定义在一个数组之中,型如:
code unsigned char LedHexCode[]={0xc0 , 0xf9 , 0xa4 , 0xb0 , 0x99 , 0x92 ,0x82 ,
0xf8 , 0x80 ,0x90 , 0x88 , 0x83 , 0xc6 , 0xa1 , 0x86 , 0x8e}; //共阳数码管码表
调用方式例如:P1=LedHexCode[0]显示数字 0;
 
那怎么使用码表呢,举个例子:我们给 P1 口赋值就是 P1 = 0xc0,(由于学院发的单片机开发板的数码 管是共阳的,所以使用共阳数码管码表),简单来说,就是 P1 有八个口同时赋值(必须使用十六进制进行赋值)。所以P1的八个引脚负责点亮数码管里的几个灯(我们称之为数码管的“段选”),那控制六个数码管岂不是要6组P1这样的GPIO,显然不行,我们的芯片只有四组GPIO呢,所以我们加入六个PNP型三极管(如下图),通过P0的六个引脚控制指定的数码管通电来只让我们指定的亮起来(我们称之为数码管的“位选”),这样就可以减少单片机引脚的使用数量。
 

所以只需要设置”段选“和”位选“就可以驱动数码管啦,了解了原理,我们就一起来让一个数码管显示”1“吧,附上我们的code(例程较多,尽量学习弄懂每一句逻辑的原理(ε=ε=ε=┏(゜ロ゜;)┛):

#include <REGX52.H>

void Delayms(unsigned int tc) //函数延时 tc 毫秒,子函数
{
    unsigned int i;
    while(tc != 0) 
    {
        for(i = 0; i < 122; i++);
        tc--;
    }
}
#define duan  P1              //给P1寄存取个别名叫duan(段选的意思,控制要显示的数字)
#define wei   P0_0;           //给P0寄存器的第零位取别名叫wei(位选的意思,控制P0_0接的PNP三极管的通断,也就是是否给第一个数码管通电)
unsigned char code table[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90}; //共阳极三极管的码表从“0”到“9”也可以按自己想要的自己组合码表

void main(void)
{
  while(1)
  {
    duan=table[1];            //段选给“1”的码表里的值,显示“1”
    wei=0;                    //位选给0,P0_0接的PNP三极管的导通,第一个数码管通电,可以亮
  }
}

成功了,效果就像:

下面我们通过改变不同的码值让数字动起来吧:

#include <REGX52.H>

void Delayms(unsigned int tc) //函数延时 tc 毫秒,子函数
{
    unsigned int i;
    while(tc != 0) 
    {
        for(i = 0; i < 122; i++);
        tc--;
    }
}
#define duan  P1              
#define wei   P0_0           
unsigned char code table[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90}; 

void main(void)
{
  unsigned char i = 0;
  while(1)
  {
    duan=table[i];            //显示的数字给从0到9
    wei=0;
    i++;                 
    i%=10;                    //让i在0-9循环
    Delayms(500);             //单片机运行棏比较快,延时500毫秒让每个数字显示得久一点
  }
}

 它运行起来就像:

数码管计数器

我们也可以通过改变位选让数字显示的位置动起来:

#include <REGX52.H>

void Delayms(unsigned int tc) //函数延时 tc 毫秒,子函数
{
    unsigned int i;
    while(tc != 0) 
    {
        for(i = 0; i < 122; i++);
        tc--;
    }
}
#define duan  P1              
#define wei   P0            //注意,这里我们要改变6个引脚的电平,所以将wei定义成P0整个寄存器,而不是先前的P0_0只表示一位          
unsigned char code table[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90}; //”0“~”9“数字的码表

void main(void)
{
  unsigned char i = 0;
  wei=0x01;                 
  while(1)
  {
    duan=table[6];           //显示"6"
    wei=~(0x01<<i);          //和流水灯例程相似的思想,循环让每个数码管都通一次电
    i++;                 
    i%=6;                    //让i在0-6循环
    Delayms(500);            //位选切换时必须延时大于一毫秒
  }
}

 运行起来大概像这样:

666

那我们怎么实现动态数码管呢,同时显示六位数字,我们先自行了解一下余辉效应,和人眼的视觉暂存,大体来说原理就是:只要我们把数字存在的位置换的足够快,人眼看起来就是同时显示的了,为了帮助大家更直观地理解,我写了个程序来演示:

数码管余辉演示

懂了余辉效应和视觉暂存的原理,我们可以写个小函数来一次性显示六位,这也叫模块化编程,这样main里面的逻辑就清晰了很多:

(注意换位置显示是延时一定大于1毫秒不然和上一次的余辉重叠,看起来就像乱码)

#include <REGX52.H>

void Delayms(unsigned int tc) //函数延时 tc 毫秒,子函数
{
    unsigned int i;
    while(tc != 0) 
    {
        for(i = 0; i < 122; i++);
        tc--;
    }
}

#define duan  P1              
#define wei   P0           
unsigned char code table[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90}; //”0“~”9“数字的码表
void showNum(unsigned char num1, unsigned char num2, unsigned char num3,
             unsigned char num4, unsigned char num5, unsigned char num6)  
{
    duan=table[num1];   //显示第1位的数
    wei=~(0x01<<0);
    Delayms(1);    

    duan=table[num2];   //显示第2位的数
    wei=~(0x01<<1);
    Delayms(1); 
  
    duan=table[num3];   //显示第3位的数
    wei=~(0x01<<2);
    Delayms(1); 
  
    duan=table[num4];   //显示第4位的数
    wei=~(0x01<<3);
    Delayms(1); 
  
    duan=table[num5];   //显示第5位的数
    wei=~(0x01<<4);
    Delayms(1); 
  
    duan=table[num6];   //显示第6位的数
    wei=~(0x01<<5);
    Delayms(1); 
}

void main(void)
{                 
  while(1)
  {
    showNum(1,1,4,5,1,4); //显示一次”114514“,一直循环
  }
}

我们看看调用自己写的函数一次性显示六位数字的效果: 

Task:数码管时钟(按键与数码管的应用)

#include <REGX52.H>

void Delayms(unsigned int tc) //函数延时 tc 毫秒,子函数
{
    unsigned int i;
    while(tc != 0) 
    {
        for(i = 0; i < 122; i++);
        tc--;
    }
}

#define duan  P1              
#define wei   P0           
unsigned char code table[]={0xc0,0xf9,0xa4,0xb0,0x99,0x92,0x82,0xf8,0x80,0x90}; //”0“~”9“数字的码表
void showNum(unsigned char num1, unsigned char num2, unsigned char num3,
             unsigned char num4, unsigned char num5, unsigned char num6, unsigned char time) //time用于我们一次要显示多少毫秒 
{
    duan=table[num1];   //显示第1位的数
    wei=~(0x01<<0);
    Delayms(time/6);    

    duan=table[num2];   //显示第2位的数
    wei=~(0x01<<1);
    Delayms(time/6); 
  
    duan=table[num3];   //显示第3位的数
    wei=~(0x01<<2);
    Delayms(time/6); 
  
    duan=table[num4];   //显示第4位的数
    wei=~(0x01<<3);
    Delayms(time/6); 
  
    duan=table[num5];   //显示第5位的数
    wei=~(0x01<<4);
    Delayms(time/6); 
  
    duan=table[num6];   //显示第6位的数
    wei=~(0x01<<5);
    Delayms(time/6); 
}

void main(void)
{
  unsigned char hour=0, minute=0, second=0;
  while(1)
  {
    showNum(hour/10,hour%10,minute/10,minute%10,second/10,second%10, 6);   //为了演示加快速度只显示6毫秒,正常时钟该1000毫秒,想想为什么/10,%10
    second++;
    if(second==60){second=0;minute++;}
    if(minute==60){minute=0;hour++;}
    if(hour==60){hour=0;}
  }
}

简单看看效果:

时钟

结语:

时间匆忙,可能有许多勘误处有待修改,如果有不太懂的或者想听的欢迎留言讨论Debug~

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值