前言

根据前面对51单片机的了解和基础理论知识的掌握,接下来就正式进入单片机开发——实践。引用最近几天追的电视剧《觉醒年代》里新文化领袖陈独秀、李大钊等革命前辈们一直在说的和在践行的马克思的一句话“实践是检验真理的唯一标准”。“理论基础决定上层建筑”这句话到哪儿都是适用的,同样在这里,在前续知识的了解和掌握就进入了程序编写以及电路调试等工程开展中,真正做到理论与实践结合,这里采用的是C语言来编写的51单片机开发程序,这里通篇可能出现一些不专业的话术和专业名词,这里希望看到文章的朋友仅把此文当作参考,对于存在错误的地方可在下方评论区进行留言或者私信博主允于指导和纠正。那么接下来进入我们的入门之旅。

总体思维脑图

这里我把这单片机入门具体应用分类绘制成了一张简易的思维脑图,可以通过预览这里的思维脑图建立大概的知识框架以及对应的整文目录预览以及后续的入门学习有个整体的学习思想,具体思维脑图如下:

image-20210530172238579

开发工具应用

这里单片机的概念以及最小系统前面两篇文章已经介绍过了,这里就不再过多的赘述,这里在单片机实践开始之前,我们应该先想个事情,我们应该采用什么样的工具来辅助我们进行软件的开发,用上一款得心应手的工具软件对我们开发来说,那一定是事半功倍的。那么我先介绍一下几款要用的开发工具:

  • 编程开发工具——Keil
  • 单片机电路仿真工具——Proteus
  • 单片机电路原理图、PCB绘制工具——Altium Designer

具体软件的安装包以及软件的安装教程这里可以在本博客进行搜索找到,对应的介绍也已经在文章中进行描述,可以自行前往了解,那么对于具体该怎么去操作利用,那么在接下来的时间以及文章篇幅允许下将进行具体的介绍,如果这篇文章没有,那么也不要灰心,对应软件的教程网络上一搜一大把,后期在我有时间的基础上我也将进行博文的描述或者进行录制视频来进行进一步的描述。

IO控制应用

从这里开始正式开始本篇博文的重点以及核心知识点。IO口在经过前面两张文章尤其第51单片机开发入门(2)的介绍已经比较完善,在51单片机里共计有四组单片机IO口,对应的功能、理论知识前面可以进行查阅。我们整篇文章将阐述怎么利用它,以及利用它来实现什么功能。

一位输出:流水灯

电路功能分析

这里首先了解一位是指单片机指利用一组IO口进行操作,那么对应在最小系统基础上完成与8 个发光二极管的驱动电路设计;另外需要编写测试程序,实现循环点亮8 个灯,时间间隔约1 秒。

硬件电路设计

image-20210529194741874

这里将每个LED发光二极管的阴极接到了对应的单片机P1端口上,同时每个LED发光二极管的阳极通过串联一个限流电阻连接到VCC(+5v)电源上。当P1端口输出为低电平时,LED点亮,反之LED熄灭。

软件程序编写

在硬件电路完成的基础上,我们开始软件程序的编写,首先我们先梳理一下单片机是怎么工作的。单片机接通电源后,开始工作,P1端口依次输出低电平并且每次间隔1秒钟,轮流交替。那么程序编写方面该如何实现呢?我们接下来通过三种不同方法的程序来实现。

第一种:位操作

  • 第一步、确定LED的端口,定义单片机IO口
/*******LED位定义*********/
sbit LED0=P1^0;//LED0位定义
sbit LED1=P1^1;//LED1位定义
sbit LED2=P1^2;//LED2位定义
sbit LED3=P1^3;//LED3位定义
sbit LED4=P1^4;//LED4位定义
sbit LED5=P1^5;//LED5位定义
sbit LED6=P1^6;//LED6位定义
sbit LED7=P1^7;//LED7位定义
  • 第二步、根据设计要求,编写延时函数
/******延时函数******/
void delay_ms(unsigned char ms)
{
unsigned int i,j;
for(i=0;i<ms;i++)
{
for(j=0;j<333;j++);
}
}

这里注意程序中编写的函数需要在程序中进行函数声明。

  • 第三步、根据设计要求,编写主函数程序
/******位操作方式******/
/******主函数******/
void main(void)
{
while(1)
{
LED0=0;delay_ms(1000);LED0=1;//LED0点亮,其余熄灭
LED1=0;delay_ms(1000);LED1=1;//LED1点亮,其余熄灭
LED2=0;delay_ms(1000);LED2=1;//LED2点亮,其余熄灭
LED3=0;delay_ms(1000);LED3=1;//LED3点亮,其余熄灭
LED4=0;delay_ms(1000);LED4=1;//LED4点亮,其余熄灭
LED5=0;delay_ms(1000);LED5=1;//LED5点亮,其余熄灭
LED6=0;delay_ms(1000);LED6=1;//LED6点亮,其余熄灭
LED7=0;delay_ms(1000);LED7=1;//LED7点亮,其余熄灭
}
}

通过while(1){}死循环操作,使得单片机在一位一位的点亮和熄灭,但这里发现需要编写的变量比较多,虽然能够实现对应设计要求,但是看起来也比较复杂,可读性不强,程序在编写过程中也十分容易出错,所以我们在程序设计过程也要追求程序的简化,增强可读性,那么开始第二种方法。

第二种:移位操作

  • 第一步、确定LED的端口,宏定义单片机的IO口
#define	LED P1;//单片机LEDIO口宏定义

这里也可以不用宏定义,直接在程序中写对应的IO号,但是最好养成习惯,在单片机程序编写过程中最好用宏定义,那么后期在更换IO口时只需要,在这里更改就可以了,而不用整个程序中去搜索更改对应的IO标号。

  • 第二步、根据设计要求,编写延时函数

这里延时程序和上面第一种方法的函数一致,这里就不再重复,具体可以参考上面第一种方法的延时函数程序。

  • 第三步、根据设计要求,编写主函数程序
/******移位操作方式******/
/******主函数******/
void main(void)
{
while(1)
{
unsigned char temp=0xfe,i;
temp=0xfe;    // 1111 1110
for(i=0;i<8;i++)
{
LED=temp;
temp=(temp<<1)+1; //1111 1101
delay_ms(1000); //延时1s
}
}
}

这里通过设定P0初始值为0xfe(1111 1110)通过每间隔1秒循环8次左移,完成程序设计要求。

第三种:数组方式

  • 第一步、确定LED的端口,宏定义单片机的IO口
#define	LED P1;//单片机LEDIO口宏定义

这里也可以不用宏定义,直接在程序中写对应的IO号,但是最好养成习惯,在单片机程序编写过程中最好用宏定义,那么后期在更换IO口时只需要,在这里更改就可以了,而不用整个程序中去搜索更改对应的IO标号。

  • 第二步、变量定义
/*****变量定义*****/
unsigned char LED_DAT[8]={0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f};

通过数组将8种状态所对应的字节归纳定义为数组变量。

  • 第三步、根据设计要求,编写延时函数

这里延时程序和上面第一种方法的函数一致,这里就不再重复,具体可以参考上面第一种方法的延时函数程序。

  • 第四步、根据设计,要求编写主函数
void main(void)
{
while(1)
{
unsigned char i;
for(i=0;i<8;i++)
{
LED=LED_DAT[I];
delay_ms(1000);
}
}
}

通过数组种对应元素的变化,改变P1端口对应输出的字节,控制LED灯进行流水灯操作。

烧录程序,仿真调试

这里通过Proteus仿真工具根据电路原理图进行绘制对应的单片机最小系统以及所需要的外围电路,将前面编写的单片机程序编译生成的HEX文件烧录进单片机里,查看具体效果。

image-20210529205911680

整体程序整理:

#include <reg51.h>

/*****引脚定义*****/
sbit LED0=P1^0;
sbit LED1=P1^1;
sbit LED2=P1^2;
sbit LED3=P1^3;
sbit LED4=P1^4;
sbit LED5=P1^5;
sbit LED6=P1^6;
sbit LED7=P1^7;

/*****变量定义*****/
unsigned char LED_DAT[8]={0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f};

/*****函数声明*****/
void delay_ms(unsigned char ms) ;
void shagua();//傻瓜方式
void shuzu();//数组方式
void yiwei();//移位方式

/*****主函数*****/
void main(void)
{
while(1)
{
shagua();
yiwei();
shuzu();
}
}

void shagua()//傻瓜方式
{
LED0=0;delay_ms(1000);LED0=1;//LED0点亮,其余熄灭
LED1=0;delay_ms(1000);LED1=1;//LED1点亮,其余熄灭
LED2=0;delay_ms(1000);LED2=1;//LED2点亮,其余熄灭
LED3=0;delay_ms(1000);LED3=1;//LED3点亮,其余熄灭
LED4=0;delay_ms(1000);LED4=1;//LED4点亮,其余熄灭
LED5=0;delay_ms(1000);LED5=1;//LED5点亮,其余熄灭
LED6=0;delay_ms(1000);LED6=1;//LED6点亮,其余熄灭
LED7=0;delay_ms(1000);LED7=1;//LED7点亮,其余熄灭
}

void shuzu()//数组方式
{
unsigned char i;
for(i=0;i<8;i++)
{
P1=LED_DAT[i];
delay_ms(1000;
}
}

void yiwei()//移位方式
{
unsigned char temp=0xfe,i;
temp=0xfe;    // 1111 1110
for(i=0;i<8;i++)
{
P1=temp;
temp=(temp<<1)+1; //1111 1101
delay_ms(100);
}
}

/******延时函数******/
void delay_ms(unsigned char ms)
{
unsigned int i,j;
for(i=0;i<ms;i++)
{
for(j=0;j<333;j++);
}
}

一位输入:独立按键

电路功能分析

这里首先了解一位是指单片机指利用一组IO口进行操作,那么对应在最小系统基础上完成2个独立按键与8 个发光二极管的驱动电路设计;另外需要编写测试程序,实现实现按键控制循环点亮8 个灯,时间间隔约1 秒。

硬件电路设计

image-20210529223106503

通过上图发现:这里将每个LED发光二极管的阴极接到了对应的单片机P1端口上,同时每个LED发光二极管的阳极通过串联一个限流电阻连接到VCC(+5v)电源上。当P1端口输出为低电平时,LED点亮,反之LED熄灭。

在上面LED流水灯的基础之上,引入添加了独立按键模块,根据电路设计要求,具体添加的按键模块,根据按键开关(非自锁开关)原理。当按键没有按下的时候,VCC(+5v)经过470欧姆限流电阻 输入进单片机的IO口,按键所对应的单片机IO口输入信号为高电平;当按键按下的时候,输入信号对地,这样子按键输入信号对地,即输入信号为低电平,单片机输入信号为低电平。通过高低电平检测,判断按键是否按下。

单独按键主要功能简化:

按键K1按键时,P3.0输入为“0“;

按键K1松下时,P3.0输入为“1“。

图中,按键和电阻可以交换位置,交换后输入逻辑刚好相反。

image-20210529225721797

按键抖动问题

  • 按键抖动问题分析

这里认识了解了按键单个按键的工作原理后,我们还需要了解按键抖动的影响 :当用手按下一个按键时,往往所按按键在闭合位置和断开位置之间弹跳几下才会稳定到闭合状态;在释放一个按键时,也会出现类似的情况。这就是按键抖动。这是由机械结构的固有特性决定的,不可避免。

按键抖动的持续时间大小不一,一般在10ms 左右。

image-20210529230310588

  • 按键抖动问题举例

    当存在按键抖动时,下面程序将**对抖动时的每一个下降沿进行计数**,从而出错。因此应当采取一定的措施消除抖动的影响

image-20210529230859768

# include <reg51.h>
# define uchar unsigned char
sbit KEY = P2^0;

void main( )
{
uchar ucCounter;
uchar ucPreKey;
ucCounter = 0x00; //计数初值
KEY = 1; //准双向口
ucPreKey = 1;
while(1)
{
if ((KEY==0) && (ucPreKey==1))
{
ucCounter++; //计数值加一
}
ucPreKey = KEY; //存储前一状态
}
}

  • 按键抖动问题消除方法

要求消除按键抖动的影响,实现一次按键计数值只增加一次。

实现方法:

软件:延迟再扫描

硬件:外接额外电路 (触发器、滤波电路)

# include <reg51.h>
# define uchar unsigned char
sbit KEY = P2^0;

void main( )
{
uchar ucCounter, ucPreKey ;
ucCounter = 0x00; //计数初值
KEY = 1; //准双向口
ucPreKey = 1;
while(1)
{
if ((KEY==0) && (ucPreKey==1))
{
delay(2); //延时20ms
if(KEY == 0)
{
ucCounter++; //计数值加一
}
}
ucPreKey = KEY; //存储前一状态
}
}

软件程序编写

在硬件电路完成的基础上,我们开始软件程序的编写,首先我们先梳理一下单片机是怎么工作的。单片机接通电源后,开始工作,两个独立按键在这里,一个按键按下时控制P1口所接的LED灯进行左移,另一个按键按下时控制P1口所接的LED灯右移,那么程序编写方面该如何实现呢?

  • 第一步、确定LED的端口,宏定义单片机的IO口以及确定两个独立按键的端口,位定义两个按键
//独立按键管脚IO口定义
sbit KEY1=P3^0;
sbit KEY2=P3^1;
//8个LED灯端口宏定义
#define LED P1
  • 第二步、变量定义
/*****变量定义*****/
unsigned char LED_DAT[8]={0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f};

通过数组将8种状态所对应的字节归纳定义为数组变量。

  • 第三步、根据设计要求,编写延时函数
void delay_ms(unsigned char ms)
{
unsigned int i,j;
for(i=0;i<ms;i++)
{
for(j=0;j<333;j++);
}
}
  • 第四步、根据设计要求,编写按键扫描函数
unsigned char key_scan()    //按键扫描函数
{
unsigned char keynum;
keynum=0;
if(KEY1==0) //按键1按下
{
delay_ms(10); //软件消除按键抖动
if(KEY1==0)
{
while(KEY1==1);
keynum=1; //按键1按下输出变量为1
}
}
if(KEY2==0)
{
delay_ms(10); //软件消除按键抖动
if(KEY2==0) //按键2按下
{
while(KEY2==1);
keynum=2; //按键2按下输出变量为2
}
}
return(keynum);
}

通过按键扫描函数,当没有按键按下时,输出按键变量keynum=0;当按键1按下时,输出按键变量keynum=1;当按键2按下时,输出按键变量keynum=2;这里按键进行延时消抖。

  • 第五步、根据设计要求,编写数组左移以及右移函数
//******数组操作******// 
void shuzu_you()//数组方式_右移
{
char i=0;
for(i=0;i<8;i++) //循环右移
{
P1=LED_DAT[i];
delay_ms(1000);
}
}

void shuzu_zuo()//数组方式_左移
{
char n;
for(n=7;n>=0;n--) //循环左移
{
P1=LED_DAT[n];
delay_ms(1000);
}
}

这里先将数组的左移以及右移函数提前写好,到时候在主函数种可以进行调用,但是同时也可以在主函数中进行左移、右移操作,同时也可以进行添加相关头文件,直接调用函数进行左移、右移。

  • 第六步、根据设计要求,编写主函数
void main()
{
while(1)
{

if(key_scan()==1) //按键1按下
{
shuzu_you(); //执行右移函数
}
if(key_scan()==2) //按键2按下
{
shuzu_zuo(); //执行左移函数
}
}
}

在上面已经梳理清楚了独立按键控制8个LED左移流水灯以及右移流水灯,那么通过while(1){}死循环将单片机一开始就先进行按键扫描,当按键没有任何操作的时候,按键值没有任何变化为0;当按键发生变化的时候,对应的按键值返回到键盘扫描函数,再根据if()的判断函数或者可以利用Switch()选择函数来进行编写后来实现具体的功能。

烧录程序,仿真调试

这里通过Proteus仿真工具根据电路原理图进行绘制对应的单片机最小系统以及所需要的外围电路,将前面编写的单片机程序编译生成的HEX文件烧录进单片机里,查看具体效果。

image-20210529233900482

整体程序梳理;

#include <reg51.h>

//独立按键管脚IO口定义
sbit KEY1=P3^0;
sbit KEY2=P3^1;
//8个LED灯端口宏定义
#define LED P1

/*****函数声明*****/
void shuzu_zuo();
void shuzu_you();
unsigned char key_scan();
void delay_ms(unsigned char ms);

/*****变量定义*****/
unsigned char LED_DAT[8]={0xfe,0xfd,0xfb,0xf7,0xef,0xdf,0xbf,0x7f};

/*****延时函数*****/
void delay_ms(unsigned char ms)
{
unsigned int i,j;
for(i=0;i<ms;i++)
{
for(j=0;j<333;j++);
}
}

//******数组操作******//
void shuzu_you()//数组方式_右移
{
char i=0;
for(i=0;i<8;i++) //循环右移
{
P1=LED_DAT[i];
delay_ms(1000);
}
}
void shuzu_zuo()//数组方式_左移
{
char n;
for(n=7;n>=0;n--) //循环左移
{
P1=LED_DAT[n];
delay_ms(1000);
}
}

unsigned char key_scan() //按键扫描函数
{
unsigned char keynum;
keynum=0;
if(KEY1==0) //按键1按下
{
delay_ms(10); //软件消除按键抖动
if(KEY1==0)
{
while(KEY1==1);
keynum=1; //按键1按下输出变量为1
}
}
if(KEY2==0)
{
delay_ms(10); //软件消除按键抖动
if(KEY2==0) //按键2按下
{
while(KEY2==1);
keynum=2; //按键2按下输出变量为2
}
}
return(keynum);
}

void main()
{
while(1)
{
if(key_scan()==1) //按键1按下
{
shuzu_you(); //执行右移函数
}
if(key_scan()==2) //按键2按下
{
shuzu_zuo(); //执行左移函数
}
}
}

多位输出:数码管

数码管:由发光二极管阵列构成。用于显示数字和简单英文字符。

image-20210530103128115

数码管的工作原理

  • 数码管的结构

共阳极数码管

共阴极数码管

image-20210530104142680

image-20210530104236551

  • 数码管的显示

段码,数码管显示的内容:

image-20210530104337742

位码,(即com端)数码管是否点亮

  • 数码管的段码分析(以共阴极数码管为例)

image-20210530104500992

上方图片即为共阴极数码管的段选表,通过不同段位的电平情况,显示不同的字符。dp为小数点位

  • 多个数码管的控制

静态显示,每个数码管单独控制,所有数码管同时点亮

image-20210530104847053

📌📌动态显示,动态显示,段码共用,位码分别控制,每个数码管**循环点亮**

image-20210530104946687

I/O端口1不断送待显示字符的段选码,I/O端口2不断送出不同的位扫描码,并使每位显示字符停留显示一段时间,一般为1ms~5ms,利用眼睛的视觉暂留,从显示器上便可以见到相当稳定的数字显示。

单个数码管显示

电路功能分析

根据前面的数码管的基础原理的分析和学习,在51单片机足校系统的基础上添加单个共阴极数码管循环显示0-9对应的编码。

硬件电路设计

image-20210530110125640

这里在51单片机最小系统的基础上添加了一个共阴极数码管,这里注意电路的连接,同时在该基础上,在单片机连接数码管的IO口上同时连接8个LED灯,以此来查看数码管编码发生变化时对应的各端口的变化情况。

📢(这里注意数码管段选端口以及LED端口接在P0端口上需要接上上拉电阻,这里采用8脚排阻

软件程序编写

在硬件电路完成的基础上,我们开始软件程序的编写,首先我们先梳理一下单片机是怎么工作的。单片机接通电源后,开始工作,单片机IO口P0输出不同字节的电平,控制数码管段选端,那么程序编写方面该如何实现呢?

  • 第一步、确定LED的端口,宏定义单片机的IO口以及确定两个独立按键的端口,位定义两个按键
//8个LED灯、单个数码管端口宏定义
#define LED P1
  • 第二步、变量定义
unsigned char shumaguan[16]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,
0x7d,0x07,0x7f,0x6f,0x77,0x7c,
0x39,0x5e,0x79,0x71}; //共阴数码管代码表"0-F"

这里将共阴极数码管的段选表通过数组来记录下来,那么在下面数码管显示程序中只要通过选定数组中对应的元素即可显示相对应的数值。

  • 第三步、根据要求,编写延时函数
void delay_ms(unsigned char ms)
{
unsigned int i,j;
for(i=0;i<ms;i++)
{
for(j=0;j<333;j++);
}
}

这里延时函数已经是老生常谈了,那么后面的延时函数我就不再过多的赘述。

  • 第四步、根据设计要求,编写主函数程序
/*****主函数*****/
void main(void)
{
int i;
while(1)
{
for(i=0;i<10;i++) //0~9顺序循环
{
LED=shumaguan[i];
delay(50000);
}
}
}

在上面已经梳理清楚了独立按键控制8个LED左移流水灯以及右移流水灯,那么这里数码管显示不同的字符也一样通过while(1){}死循环将单片机一开始就通过for循环,依次从数组中0-9个元素依次循环选择,并且通过端口赋值,使得数码管循环显示不同的字符。编写后来实现具体的功能。

烧录程序,仿真调试

这里通过Proteus仿真工具根据电路原理图进行绘制对应的单片机最小系统以及所需要的外围电路,将前面编写的单片机程序编译生成的HEX文件烧录进单片机里,查看具体效果。

image-20210530110125640

整体程序梳理如下:

#include <reg51.h>

//8个LED灯、单个数码管端口宏定义
#define LED P1

/*****函数声明*****/
void delay_ms(unsigned char ms);

/*****变量定义*****/
unsigned char shumaguan[16]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,
0x7d,0x07,0x7f,0x6f,0x77,0x7c,
0x39,0x5e,0x79,0x71}; //共阴数码管代码表"0-F"

/*****延时函数*****/
void delay_ms(unsigned char ms)
{
unsigned int i,j;
for(i=0;i<ms;i++)
{
for(j=0;j<333;j++);
}
}

/*****主函数*****/
void main(void)
{
int i;
while(1)
{
for(i=0;i<10;i++) //0~9顺序循环
{
LED=shumaguan[i];
delay_ms(5000);
}
}
}

多位数码管显示

电路功能分析

添加多位数码管模块电路,利用单片机输出控制实现四位共阴极数码管循环0-9999;并且按键1按下显示1234,按键2按下恢复显示循环0-9999。

硬件电路设计

image-20210530113450824

这里首先分析一下三极管的段选端,这里段选端口接到单片机的P0端口上,这里和前面的单个数码管一样在P0端口需要接上上拉电阻。这里数码管的位选端口通过NPN三极管以及几个电阻组成了数码管的驱动电路,这里拿位选端口1举例来说,当位端口输出低电平时候,Q1NPN二极管截至,位选端1为低电平;当位端口输出高电平时候,Q1NPN二极管导通,位选端1为高电平。

同时按键电路的设计:当按键1按下时,单片机P3.0检测到电平变化,进行按键对应功能操作。当按键2按下时,单片机P3.1检测到电平变化恢复数码管循环0-9999显示。

📢(这里注意三极管采用的是NPN三极管

软件程序编写

在硬件电路完成的基础上,我们开始软件程序的编写,首先我们先梳理一下单片机是怎么工作的。单片机接通电源后,开始工作,两个独立按键在这里,一个按键按下时控制数码管显示1234,另一个按键按下时控制数码管进行0-9999循环显示,那么程序编写方面该如何实现呢?

  • 第一步、确定 数码管的位选端口,宏定义单片机的段选 IO 口以及确定两个独立按键的端口,位定义两个按键
/*****数码管段选宏定义*****/
#define SEG_Port P0

/*****数码管位选管脚定义*****/
sbit wei1=P2^0;
sbit wei2=P2^1;
sbit wei3=P2^2;
sbit wei4=P2^3;
//按键端口定义
sbit KEY1=P3^0;
sbit KEY2=P3^1;
  • 第二步、变量定义
/*****变量定义*****/
unsigned char shumaguan[16]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,
0x7d,0x07,0x7f,0x6f,0x77,0x7c,
0x39,0x5e,0x79,0x71}; //共阴极数码管的段选表
unsigned int dis_dat=0;//0-65535; 5535 -> 0000
unsigned char cnt=0,stu_flag=0;
  • 第三步、根据设计要求,编写延时函数

void delay_ms(unsigned char ms)
{
unsigned int i,j;
for(i=0;i<ms;i++)
{
for(j=0;j<333;j++);
}
}
  • 第四步、根据设计要求,编写按键扫描函数
/*****按键扫描函数函数****/
unsigned char key_scan() //按键扫描函数
{
unsigned char keynum;
keynum=0;
if(KEY1==0) //按键1按下
{
delay_ms(10); //软件消除按键抖动
if(KEY1==0)
{
while(KEY1==1);
keynum=1; //按键1按下输出变量为1
}
}
if(KEY2==0)
{
delay_ms(10); //软件消除按键抖动
if(KEY2==0) //按键2按下
{
while(KEY2==1);
keynum=2; //按键2按下输出变量为2
}
}
return(keynum);
}

通过按键扫描函数,当没有按键按下时,输出按键变量 keynum=0;当按键 1 按下时,输出按键变量 keynum=1;当按键 2 按下时,输出按键变量 keynum=2;这里按键进行延时消抖。

  • 第五步、根据设计要求,编写数码管显示函数(四位整数)
/*****数码管显示函数****/
void dis_seg(unsigned int dat)//显示四位整型数据的函数
{
/*****千位*****/
SEG_Port=shumaguan[dat/1000];//送段码到数码管
wei1=0;//控制第一位数码管公共端截止
delay_ms(3);
wei1=1;
SEG_Port=0xff;//消影

/*****百位*****/
SEG_Port=shumaguan[dat/100%10];
wei2=0;
delay_ms(3);
wei2=1;
SEG_Port=0xff;

/*****十位*****/
SEG_Port=shumaguan[dat/10%10];
wei3=0;
delay_ms(3);
wei3=1;
SEG_Port=0xff;

/*****个位*****/
SEG_Port=shumaguan[dat%10];
wei4=0;
delay_ms(3);
wei4=1;
SEG_Port=0xff;
}

这里将四位数码管每一位的显示都利用求余和除法来计算得到,这里需要主要的是数码管动态显示,记得每一位都需要做消影处理

  • 第六步、根据整体设计要求,这里编写主函数调用函数
void main(void)
{
while(1)
{
switch(key_scan()) //按键扫描函数判断
{
case 1:stu_flag=1;break;
case 2:stu_flag=0;break;
default:break;
}
if(stu_flag==1)
{
dis_seg(1234);//按键1按下显示1234
}
else //按键2按下恢复0-9999
{
for(cnt=0;cnt<10;cnt++)
{
dis_seg(dis_dat);
}
dis_dat++;
dis_dat%=10000;
}
}
}

在按键扫描函数以及数码管显示函数完成的情况下,这里只需要在主函数中调用即可,这里利用Switch语句来判断选择两个按键的功能,同时给每个按键对应的功能添加上去,并且根据题目要求需要添加0-9999循环显示,这里就利用for循环语句进行编写。至此,该多位数码管程序就全都完成了。

烧录程序,仿真调试

这里通过 Proteus 仿真工具根据电路原理图进行绘制对应的单片机最小系统以及所需要的外围电路,将前面编写的单片机程序编译生成的 HEX 文件烧录进单片机里,查看具体效果。

整体程序梳理;

#include<reg51.h>

/*****数码管段选宏定义*****/
#define SEG_Port P0

/*****数码管位选管脚定义*****/
sbit wei1=P2^0;
sbit wei2=P2^1;
sbit wei3=P2^2;
sbit wei4=P2^3;
//按键端口定义
sbit KEY1=P3^0;
sbit KEY2=P3^1;

/*****变量定义*****/
unsigned char shumaguan[16]={0x3f,0x06,0x5b,0x4f,0x66,0x6d,
0x7d,0x07,0x7f,0x6f,0x77,0x7c,
0x39,0x5e,0x79,0x71}; //共阴极数码管的段选表

unsigned int dis_dat=0;//0-65535; 5535 -> 0000
unsigned char cnt=0,stu_flag=0;

/*****函数声明*****/
void dis_seg(unsigned int dat);
unsigned char key_scan();

/*****延时函数****/
void delay_ms(unsigned char ms)
{
unsigned int i,j;
for(i=0;i<ms;i++)
{
for(j=0;j<333;j++);
}
}

/*****数码管显示函数****/
void dis_seg(unsigned int dat)//显示四位整型数据的函数
{
/*****千位*****/
SEG_Port=shumaguan[dat/1000];//送段码到数码管
wei1=0;//控制第一位数码管公共端截止
delay_ms(3);
wei1=1;
SEG_Port=0xff;//消影

/*****百位*****/
SEG_Port=shumaguan[dat/100%10];
wei2=0;
delay_ms(3);
wei2=1;
SEG_Port=0xff;

/*****十位*****/
SEG_Port=shumaguan[dat/10%10];
wei3=0;
delay_ms(3);
wei3=1;
SEG_Port=0xff;

/*****个位*****/
SEG_Port=shumaguan[dat%10];
wei4=0;
delay_ms(3);
wei4=1;
SEG_Port=0xff;
}

/*****按键扫描函数函数****/
unsigned char key_scan() //按键扫描函数
{
unsigned char keynum;
keynum=0;
if(KEY1==0) //按键1按下
{
delay_ms(10); //软件消除按键抖动
if(KEY1==0)
{
while(KEY1==1);
keynum=1; //按键1按下输出变量为1
}
}
if(KEY2==0)
{
delay_ms(10); //软件消除按键抖动
if(KEY2==0) //按键2按下
{
while(KEY2==1);
keynum=2; //按键2按下输出变量为2
}
}
return(keynum);
}

void main(void)
{
while(1)
{
switch(key_scan()) //按键扫描函数判断
{
case 1:stu_flag=1;break;
case 2:stu_flag=0;break;
default:break;
}
if(stu_flag==1)
{
dis_seg(1234);//按键1按下显示1234
}
else //按键2按下恢复0-9999
{
for(cnt=0;cnt<10;cnt++)
{
dis_seg(dis_dat);
}
dis_dat++;
dis_dat%=10000;
}
}
}

多位输入:矩阵按键

行列式键盘的控制原理

当单片机系统所需的按键数量比较多时,如果继续采用一个IO引脚控制一个按键的接口方式,所需的IO引脚较多;为了节约引脚资源,一般采用行列式键盘的接口方式。

如下图,需要控制16个按键。

如果采用行列式键盘只需要8个引脚就可以了。

image-20210530182835980

分析电路

当没有按键按下时

列信号P1.4~P1.5输入均为**‘1’**

当有按键按下时

如果行信号P1.0~P1.3输出为‘0’,则按键对应的列信号输入为****‘0’

编程思路

  • 行列式键盘的控制(扫描法)

1、判断是否有按键按下

2、确定按下的是哪一个按键(确定按键代码)

🎯:简要来说就是确定行,确定坐标也就得到了所按下按键的坐标。

为了识别按键,必须对键盘中的按键进行编码,每一个按键都有一个确定的按键代码。如果使用非编码键盘,按键的编码方式可以由用户自由定义。

步骤如下:

1、全扫描:

四行一起扫描,以判断是否有键按下;同时采用了延迟再扫描的方法进行消抖。

2、逐行扫描:

四行依次扫描,判断按键的行列位置;

3、生成按键代码

使用不同的编程语句可以生成不同的按键代码。

image-20210530183712053

  • 行列式键盘的控制(线反转法)

扫描法要逐列扫描查询,有时则要多次扫描。而线反转法则很简练,无论被按键是处于第一列或最后一列,均只需经过两步便能获得此按键所在的行列值

(1)让行线编程为输入线,列线编程为输出线 ,并使输出线输出为全低电平,则行线中电平由高变低的所在行为按键所在行。

(2) 再把行线编程为输出线,列线编程为输入线,并使输出线输出为全低电平,则列线中电平由高变低所在列为按键所在列。

image-20210530184108898

典型编程:

#include < reg51.h >
#define uchar unsigned char
#define uint unsigned int

void delay(uchar ucData);
uchar kbscan( );

void main( )
{
uchar key ;
while(1)
{
key = kbscan( ) ; //扫描按键,获取按键代码
switch( key )
{
case 0x00 : … … ; //如果不按键, …
break ;
case 0x11 : … … ; //如果按下键1, …
break ;
case 0x21 : … … ; //如果按下键2, …
break ;
… … ;
default : break ;
}
}
}

电路功能分析

采用 4*4 键盘与6 位共阴(CC:Common Cathode)数码管模拟一电话拨号与显示;

(1)、基本功能
没有按键时,数码管不显示;
按下 1 键,最低位显示,按住还是显示1;
松开后再按下 2,低两位显示12(要求有移位功能);
依次类推实现 6 位拨号功能。

(2)、扩展功能
增加退格与修改功能

硬件电路设计

image-20210530185238835

这里通过将电路模块化处理,这里将电路分为三大部分。第一部分,51单片机最小系统,这里毋庸置疑;第二部分,输入部分——矩阵键盘以及第三部分输出部分——6位共阴极数码管以及其数码管驱动电路。

通过这样的细化后,我们就可以进行软件程序对应的编写。在此之前,我们先分析一下数码管驱动电路,分析一下数码管的段选端以及位选端口.这里采用的是6位共阴极数码管,那么对应的数码管位选端口也就是6个,那么这里位选端口直接接到了7406(六高压输出反相缓冲器)上,再由7406的管脚接到单片机的P2端口上,同时将数码管的段选端口接到了单片机的P0端口。

软件程序编写

在硬件电路完成的基础上,我们开始软件程序的编写,首先我们先梳理一下单片机是怎么工作的。程序也可以进行分模块编写,同时在我们日常程序的编写中也要注意,将程序进行模块编程,这样不仅方便我们及时查阅程序代码,同时便于我们及时调试修改程序,后期将出一篇文博文专门介绍模块化编程。

那我们书归正传,这里我根据题目设计要求,需要我们进行矩阵按键的处理以及数码管的输出控制,那么再程序方面我们也是分这么两个大方向,我们首先确定矩阵按键采用什么方法(扫描法还是线反转法),那么接下来就开始吧。

  • 根据题目要求,编写矩阵按键程序

(KEY4X4.c)

#include "key4x4.h"
#include "delay.h"

unsigned char j=0; //按键次数
unsigned char str[7];//定义变量

//扫描键盘,判断哪行那列有键按下
void keyscan(void)
{
unsigned char temp;
KEY_PORT=0xfe;//开始读取第一行
temp=KEY_PORT;//读取KEY_PORT口
temp=temp&0xf0;//判断是否有键按下 按位相与
if(temp!=0xf0)//有键按下
{
delay_ms(10); //按键消抖
temp=KEY_PORT;
temp=temp&0xf0;//再次判断是否有键按下
if(temp!=0xf0) //有键按下
{
temp=KEY_PORT;//按键值赋值给temp变量
switch(temp)//判断按键值
{
case(0xee):str[j]=0;j++; //第一行第一列为'0' 键
break;
case(0xde):str[j]=1;j++; // 第一行第二列为'1' 键
break;
case(0xbe):str[j]=2;j++; // 第一行第三列为'2' 键
break;
case(0x7e):str[j]=3;j++; // 第一行第三列为'3' 键
break;
}
while(temp!=0xf0) //判断按键是否抬起,如果按键未抬起,while语句成立,再次读取KEY_PORT的值,进行判断,直至按键抬起 ,退出循坏
{
temp=KEY_PORT; //按键值赋值给temp变量
temp=temp&0xf0; //判断是否有键按下 按位相与
}
}
}
KEY_PORT=0xfd; //开始读取第二行
temp=KEY_PORT; //读取KEY_PORT口
temp=temp&0xf0;
if(temp!=0xf0) //有键按下
{
delay_ms(10); //按键消抖
temp=KEY_PORT; //按键值赋值给temp变量
temp=temp&0xf0;
if(temp!=0xf0)
{
temp=KEY_PORT;
switch(temp)
{
case(0xed):str[j]=4;j++; //第二行第一列为'4' 键
break;
case(0xdd):str[j]=5;j++; //第二行第二列为'5' 键
break;
case(0xbd):str[j]=6;j++; //第二行第三列为'6' 键
break;
case(0x7d):str[j]=7;j++; //第二行第四列为'7' 键
break;
}
while(temp!=0xf0)
//判断按键是否抬起,如果按键未抬起,while语句成立,再次读取KEY_PORT的值,进行判断,直至按键抬起 ,退出循坏
{
temp=KEY_PORT;
temp=temp&0xf0;
}
}
}
KEY_PORT=0xfb;
temp=KEY_PORT;
temp=temp&0xf0;
if(temp!=0xf0)
{
delay_ms(10);
temp=KEY_PORT;
temp=temp&0xf0;
if(temp!=0xf0)
{
temp=KEY_PORT;
switch(temp)
{
case(0xeb):str[j]=8;j++;break; //第三行第一列为'8' 键
case(0xdb):str[j]=9;j++;break; //第三行第二列为'9' 键
case(0xbb): if(j>0) j=j-1;break; //第三行第三列为'退格' 键
case(0x7b): j=0;break; //第三行第四列为'清零' 键
}
while(temp!=0xf0) //等待按键释放,未释放执行此程序
{
temp=KEY_PORT;
temp=temp&0xf0;
}
}
}
}

先确定哪一行的按键发生了操作,开始读取第一行,读取KEY_PORT口。判断是否有键按下 按位相与(这里0xf0),这里需要注意按键的消抖的处理,这里将按键产生的数值赋值给定义的变量。同时这里需要注意按键的长按的问题。判断按键是否抬起,如果按键未抬起,while语句成立,再次读取KEY_PORT的值,进行判断,直至按键抬起 ,退出循坏。通过确定行和列后,得到按键扫描的返回值进行对应功能的操作。这里通过每个按键定义返回的不同的数值,确定不同的功能。

(KEY4X4.h)

#ifndef _KEY4x4_H_
#define _KEY4x4_H_
#include <reg51.h>

//矩阵按键管脚IO口宏定义
#define KEY_PORT P3

/*****函数声明*****/
void keyscan(void);//扫描键盘,判断哪行那列有键按下

#endif

这里对.h函数进行函数声明;进行矩阵按键管脚IO口宏定义

  • 根据题目要求,编写数码管程序

(SEG.c)

#include "seg.h"
#include "delay.h"

extern unsigned char j; //调用按键次数
extern unsigned char str[7];//调用数组变量

//共阴极数码管段码表
unsigned char code seg_dat[]={
0x3f,/*0*/
0x06,/*1*/
0x5b,/*2*/
0x4f,/*3*/
0x66,/*4*/
0x6d,/*5*/
0x7d,/*6*/
0x07,/*7*/
0x7f,/*8*/
0x6f,/*9*/};

void display(void) //数码管显示函数
{
unsigned char i;
Wei_PORT = 0x00;//数码管位码线端口初始化
for(i=0;i<j;i++)
{
Wei_PORT=0x01<<i;//(左移)送位码(0x01,0x02,0x04,0x08,0x10,0x20)
SEG_PORT=seg_dat[str[j-i-1]]; //str[]数组用来存储按键值,j-1-i作为指针指向seg_dat数组,保证之前输入的往左移
delay_ms(3);
}
if(j==7) //如果按键按下超过超过6位,也是数值显示位7位以上,显示最后按下的按键的值
{j=1;
str[0]=str[6];} //按到第七个键时,重新从最低位开始
}

(SEG.h)

#ifndef _SEG_H_
#define _SEG_H_
#include <reg51.h>

//数码管管脚宏定义
#define Wei_PORT P0 //定义数码管位码线端口宏定义
#define SEG_PORT P2 //定义数码管段码线端口宏定义

/*****函数声明*****/
void display(void);//数码管显示函数

#endif

这里对.h函数进行函数声明;进行矩阵数码管段选管脚IO口宏定义,同时定义数码管位选端口。

  • 根据设计要求,编写按键扫描函数

(DELAY.c)

#include "delay.h"

//延时函数(1)--1ms
void delay_ms(unsigned char ms)
{
unsigned int i,j;
for(i=0;i<ms;i++)
{
for(j=0;j<333;j++);
}
}

(DELAY.h)

#ifndef _DELAY_H_
#define _DELAY_H_

/*****函数声明*****/
void delay_ms(unsigned char ms);//延时函数

#endif

  • 根据整体设计要求,这里编写主函数调用函数
#include <reg51.h>
#include "key4x4.h"
//#include "delay.h"
#include "seg.h"

void main()
{
while(1)
{
keyscan();//扫描键盘,判断哪行那列有键按下,返回按键次数以及存储数组str[]
display();//通过按键次数j,存储数组str[],指向LED显示的段码数组
}
}

这里单片机一开机就先检测键盘具体情况,通过扫描键盘确定是哪行哪列按键按下,记录按键次数以及存储数组中,在通过数码管显示的函数,确定数码管的显示。

烧录程序,仿真调试

这里通过 Proteus 仿真工具根据电路原理图进行绘制对应的单片机最小系统以及所需要的外围电路,将前面编写的单片机程序编译生成的 HEX 文件烧录进单片机里,查看具体效果。

image-20210530193743438

总结

这里将IO口控制应用进行了分类,首先对一位输出进行阐述:流水灯控制,在流水灯控制方面采用了三种方法:复杂的进行一位一位控制;采用数组方式;采用移位操作方式。第二部分进行一位输入控制:独立按键,具体阐述了按键操作的工作原理以及独立按键存在的抖动问题,并且通过软件硬件的控制消去按键的抖动。第三部分介绍了多位输出数码管:这里也是分成了两大部分,单个数码管显示以及多位数码管显示,以及数码管的动静态显示。在上面按键的学习基础上对实验内容也进行了整合,通过样例进行了分析解释。最后把矩阵键盘原理进行了分析,以及经典电路,样例程序进行理解分析,对于矩阵键盘两种方法都进行了介绍,同时分析出各自的优缺点。和前面一样都用了一个经典的样例程序进行分析阐述。

到这里为止,IO口控制应用就先到这里了,后续将添加多位输出:LCD1602的显示控制,如果你认为文章中有什么问题和错误,可以在下方留言或者直接与我联系,我将第一时间回复以及纠正。同时如果你认为该片文章对你有什么帮助,可以点击下方打赏,对博主一点小奖励,恰波奶茶🥂🥂,你的打赏是对我最大的鼓励,谢谢。