目录
为了方便大家灵活改变寄存器的值,下面列出三种改变寄存器值的常用操作
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
在我们这个大信息时代,生活中 常常需要我们记住各种各样的密码以及使用各种各样的密码,甚至在使 用简单的计算器等工具时,我们都需 要进行输入操作,而搭载输入操作功能的基本器件就是我们的按键,本节我们主要学习由单片机控制的独立按键操作。
独立按键就是典型的输入设备,我们通过一个例子来学习下怎么检测按键按下,实现输入功能。
首先我们来看看常用微动开关的结构,就像下图一样简单:

一般我们有两种消抖的方法:
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控制数码管:静态及动态数码管的使用




所以只需要设置”段选“和”位选“就可以驱动数码管啦,了解了原理,我们就一起来让一个数码管显示”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~