【朱老师课程总结 侵删】
第一部分、章节目录
本节很重要的一种工作方式:中断,以后会用的很多!整个嵌入式开发过程!
第二部分、章节介绍
1-7节:讲独立按键,1-5节讲轮询方式,6-7节讲中断处理方式
8-9节:讲矩阵键盘!
1.9.1.按键相关知识
本节讲解按键的工作原理,CPU如何处理按键以及独立按键和矩阵式按键的区别。
1.9.2.独立按键编程
本节首先讲解独立按键的原理图和接线方法,然后编程监测1个按键,并最终将其扩展为监测8个按键。
1.9.3.键值检测与显示
本节结合数码管显示内容,将独立按键的键值显示在数码管上。
1.9.4.按键的消抖
本节讲解什么是按键的抖动和消抖,并且实际编程进行消抖处理。
1.9.5.完整的按键检测
本节讲解完整按键过程(按下、保持和弹起),并且改造上节课的代码使之完美运行。
1.9.6.中断的引入
本节引入中断的概念,并且以看电影收快递的例子让大家理解中断的意义。
1.9.7.使用单片机外部中断来处理按键
本节参考数据手册中的示例程序实际编程使用外部中断来处理独立按键。
1.9.8.矩阵键盘的原理
本节讲解矩阵键盘的电路原理和接线,然后分析了矩阵键盘按键检测和键值获取的方法。
1.9.9.矩阵键盘编程实战
本节按照上节分析的方法进行编程处理矩阵键盘,获取键值并且显示在数码管上。
第三部分、随堂记录
1.9.1.按键相关知识
按键很常见,也是很简单的设备,就是一个输入设备,里面是个弹簧!
1.9.1.1、按键工作原理
(1)内部机械结构
相当于只有两个引脚,只有这两个引脚可以接外面的电路!
(2)电路连接与原理图中图标可以理解成共阴!
(3)按键电路接法、上拉电阻。这才是按键完整电路!
上拉是为了让引脚默认是高电平(保证输入),但是上拉的力量扛不住接地(安泰俄斯),所以按键没有按下时上拉保证了IO引脚输入为1,而按下后绝对为0.
这下知道了,按下按键,给单片机传入的值是0!
按下和弹起的区别就是接不接地的问题,也就是引脚输入为1还是0的问题。
(4)按键这个设备对我们的意义:按键对于我们CPU来说是一个输入设备,输入的是人的操作。CPU通过监测按键连接的IO引脚的电平输入是1还是0就知道外部有没有人按下这个按键。相当于人通过按按键给CPU输入了一个信号,这个信号可以被CPU监测到从而指导CPU去做一定的工作。
1.9.1.2、CPU如何处理按键
(1)轮询式。所谓轮询式就是CPU不断的隔很小时间(几十毫秒)去查看有没有按键被按下,如果按下就处理按键,如果没按下就过一会再来查看。(按键什么时候被按下CPU是无法预知的)
(2)中断式,后面再讲,靠的是中断向量表和中断函数!
1.9.1.3、按键电路接法分类
上面有原理图
(1)独立按键
按键是彼此独立!
(2)矩阵按键
按键之间有关联!
实物图:

1.9.2.独立按键编程
1.9.2.1、原理图和接线分析
(1)8个独立按键接法一样:都是一端接GND,另一端接单片机端口上
(2)接线:按键插座JP1接到P1端口,接完之后P1端口8个IO分别对应8个按键(P1.0对应K1、P1.1对应K2····P1.7对应K8)
(3)为了用LED点亮或熄灭来指示按键是否按下,还要给LED接线。P0端口接LEDJ19。
1.9.2.2、先监测1个按键(用LED作为指示)
(1
键Key1,单片机在循环中每隔很短的时间就检测K1对应的P1.0引脚的输入电平是1还是0.如果是1则表示按键没有按下,熄灭LED作为指示。延时等待下一次检验;如果是0表示按键已经按下了,点亮一颗LED作为指示。
#include <reg52.h>
//当前处理的是Key1,对应P1.0
//控制的是LED1,对应的是P0.0
sbit KEY1 = P1^0;
sbit LED1 = P0^0;
void delay(void)
{
unsigned char i = 10;
unsigned char j = 100;
while(i--)
while(j--);
}
void main(void)
{
LED1 = 0;
while(1)
{
if(KEY1 == 1)
{
//没有按键
LED1 = 1; //熄灭
}
else
{
//有按键
LED1 = 0; //亮
}
delay();
}
}
1.9.2.3、扩展为监测8个独立按键(这个更简单)大家想一下!
注意:
(1)如果要监测的按键数量少,可以用位监测。如果多则可以直接用端口字节变量来监测。
(2)独立按键多个按键之间是彼此独立的,所以允许多个按键同时按下而不会影响。(矩阵按键就只能一次按下一个按键,不能多个同时按下)试试一次按下多个!
1.9.3.键值检测与显示
1.9.3.1、何为键值 按键编码
(1)一般的产品中按键都有很多,对于整个程序来说一般都是把按键进行编码,给每个按键一个对应的编码值,就叫做按键的键值。
(2)在正式的比较庞大的程序中,按键的检测部分和处理部分都是隔开的。这两部分隔开有利于各自部分的程序编写,两部分之间用键值来连接。按键监测部分负责监测按键,一旦发生一个按键事件就产生一个键值,然后将键值传递给按键处理部分。
1.9.3.2、加入数码管显示键值
思路:
整个程序包括2部分:
按键监测部分:按下哪个键,就把键值赋值为几;
按键处理部分:将接收到的键值显示在独立数码管上。
代码实现1:
#include <reg51.h>
/*********************程序说明***************************************
本代码利用sbit位变量,实现数码管可以显示key1、key2、key3的键值
JP1(按键)-P2
J8(静态数码管)-P0
按键是低电平有效,数码管显示要参考之前的静态数码管的段码表(第七课)
/*********************变量定义************************************/
sbit key1 = P2^0;
sbit key2 = P2^1;
sbit key3 = P2^2;
typedef unsigned char u8;
// 独立数码管的段码表
u8 val[] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90, 0x88, 0x83, 0xc6, 0xa1, 0x86, 0x8e};
//按键处理函数,将键值显示到数码管
void display(u8 keynum);
void main(void)
{
u8 keynum = 0;
while(1)
{
//按键检测部分
if(key1 == 0)
{
keynum = 1;
}
if(key2 == 0)
{
keynum = 2;
}
if(key3 == 0)
{
keynum = 3;
}
//按键处理部分,数码管显示键值
display(keynum);
}
}
void display(u8 keynum)
{
P0 = val[keynum];
}
上面的代码如果显示每个键的键值,需要写8个if-else语句,而且,你按下key1,再去按key2,键值没有变化(也就是说,后按的不能被检测到),有什么好的办法嘛?
关键时刻,位运算先生来解围
代码实现2:可以显示所有按键键值!
#include <reg51.h>
/*********************程序说明***************************************
JP1(按键)-P2
J8(静态数码管)-P0
*/
typedef unsigned char u8;
// 独立数码管的段码表
u8 val[] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90, 0x88, 0x83, 0xc6, 0xa1, 0x86, 0x8e};
//按键处理函数,将键值显示到数码管
void display(u8 keynum);
void main(void)
{
//键值
unsigned char keynum = 0;
while (1)
{
//按键检测部分
unsigned char i = 0;
//for循环实现了后按的按键也能被检测到
for (i=0; i<8; i++)
{
//0x01<<i(0-7) 分别对应0x01,0x02,0x04,0x08,0x10,0x20,0x40,0x80
//& 是判断P2的第i位是否为0
if ((P2 & (0x01<<i)) == 0)
{
keynum = i + 1;
}
}
//按键处理部分
display(keynum);
}
}
void display(u8 keynum)
{
P0 = val[keynum];
}
1.9.4.按键的消抖
本节课主要讲按键消抖问题,什么是抖动呢?大家先看个案例!
1.9.4.1、案例:按键按一次数码管显示数字加1
#include <reg51.h>
/**************代码说明**************************
本程序实现每按一个按键,数码管数字+1,0-F循环。
P2--按键
P0--静态数码管
*************************************************/
sbit key1 = P2^0;
typedef unsigned char u8;
// 独立数码管的段码表
u8 val[] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90, 0x88, 0x83, 0xc6, 0xa1, 0x86, 0x8e};
u8 dnum = 0; //最开始的数码管显示的值
void AddDisplay(void)
{
dnum++;
P0 = val[dnum%16];
}
void delay(void)
{
u8 i = 100;
u8 j = 100;
while(i--)
while(j--);
}
void main(void)
{
//键值
//unsigned char keynum = 0;
while (1)
{
if(key1 == 0)
{
AddDisplay();
}
}
}
观看现象之后的原因就是因为抖动!因为按的不够快,你按下的
1.9.4.2、什么是抖动?
(1)按键按下和弹起时的电平变化图
这张图很清楚,按下的时候,抬的不够快,所以出现按键会抖动!
(2)抖动如何产生
按键由于是机械结构,真实的物理器件,按下的时候难免产生抖动,一般抖动会在按下的时候与松开的时候产生,抖动时间大概是10ms
(10ms大约是10*120次whiile循环 )
(3)抖动的危害:
在抖动时间范围内引脚的电平变化是不定的,如果程序在这一段范围内去判断引脚的电平从而来判断有无按键,则有很大可能性会误判。
1.9.4.3、如何消抖
(1)硬件消抖:利用电容的充放电特性来对抖动过程中产生的电压毛刺进行平滑处理,从而实现消抖。但实际应用中,这种方式的效果往往不是很好,而且还增加了成本和电路复杂度,所以实际中使用的并不多。
(2)软件消抖,既然在硬件上不可能完全消除抖动,软件设计上就要想办法绕开抖动造成的影响,这是一种被动(逃避式)的消抖。
加延时绕过抖动期!
#include <reg51.h>
/**************代码说明**************************
本程序实现每按一个按键,数码管数字+1,0-F循环。
P2--按键
P0--静态数码管
*************************************************/
sbit key1 = P2^0;
typedef unsigned char u8;
// 独立数码管的段码表
u8 val[] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90, 0x88, 0x83, 0xc6, 0xa1, 0x86, 0x8e};
u8 dnum = 0; //最开始的数码管显示的值
void AddDisplay(void)
{
dnum++;
P0 = val[dnum%16];
}
//利用单片机小精灵
void delay20ms(void) //误差 0us
{
unsigned char a,b;
for(b=215;b>0;b--)
for(a=45;a>0;a--);
}
void main(void)
{
//键值
//unsigned char keynum = 0;
while (1)
{
if(key1 == 0)
{
delay20ms();
if(key1 == 0)
AddDisplay();
while(!key1); //避免长按
}
delay20ms();
}
}
1.9.5.完整的按键检测
本节就是实现while(!key1) //避免长按
1.9.6.中断的引入
1.9.6.1、任务:独立数码管循环显示0-F,同时按键控制LED亮灭
(1)分析能否实现?
#include <reg51.h>
/**************接线说明**************************
P0--静态数码管
P1.0 LED1
P1.1 LED2
P2.0 KEY1
P2.1 KEY2
*************************************************/
sbit key1 = P2^0;
sbit key2 = P2^1;
sbit led1 = P1^0;
sbit led2 = P1^1;
// 独立数码管的段码表
unsigned char val[16] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90, 0x88, 0x83, 0xc6, 0xa1, 0x86, 0x8e};
/*********************************** 函数声明 ***********************/
// 延时函数
void delay(void)
{
unsigned char i, j;
for (i=0; i<200; i++)
for (j=0; j<200; j++);
}
/******************************全局变量定义*************************/
void main(void)
{
unsigned char i = 0;
while (1)
{
// 任务1:数码管显示
for (i=0; i<16; i++)
{
P0 = val[i];
delay();
}
// 任务2:按键监测及led控制
if (key1 == 1)
led1 = 1;
else
led1 = 0;
if (key2 == 1)
led2 = 1;
else
led2 = 0;
}
}
实践证明可以实现功能,但是按键监测控制LED这边非常不灵敏。按键快速按下弹起,根本没有LED的变化!原因是什么?
原因:注意观察动态数码管显示的for循环,只有当for循环完全运行结束后,才能去响应你的按键操作!所以单片机自己有点不够用。
(2)逐步认识到单片机只有一个“主线任务”的特点
单片机的CPU都是单核的,每个时刻只能做好一个任务,单片机花费时间最多的任务就叫主线任务,就像上面这个任务,CPU在一段时间内的主线任务是点亮静态数码管,控制LED等只是一瞬间的事!
虽然上面程序也能实现多任务,但是,不能做到及时响应,必须得主线任务暂时停下来,才能响应其他任务,如何时间多任务及时响应呢?
(3)多任务时如何及时响应?
用中断(interrupt),看下一节介绍!
1.9.6.2、中断的思路
(1)“主线任务”为常规任务,默认运行
(2)中断发生后CPU暂停主线任务转去处理中断任务(啪的一下就转过去了,很快哈!),完成后再回来接着执行主线任务。
下面这张图帮助大家理解:
开发手册P105!
1.9.6.3、中断的意义
(1)中断处理能力让CPU可以全力处理主线任务(看书)而不用担心会错过中断任务(举例:接电话、开门)
(2)中断式比轮询式更适合处理异步事件,效率更高。
(3)中断中处理的事件的特点是:无法预料、处理时间短(中断任务必须是耗时非常短的,如果时间太长,会影响主线任务!)、响应要求急。
1.9.7.使用单片机外部中断来处理按键
先看手册P105和原理图
1.9.7.1、外部中断INT0和INT1
(1)何为外部中断。
中断源来自于单片机外部就叫外部中断,51支持4个外部中断。分别对应4个引脚。每一个外部中断都对应一个特定的单片机IO引脚(譬如INT0对应P3.2,这个是单片机在设计时候定好的,是无法改变的)。我们软件只需要对P3.2做一些相关配置(初始化),P3.2就可以响应外部的中断事件。当硬件产生了一个外部中断时,CPU就会收到一个中断信号,从而转去执行外部中断对应的处理程序(这个处理程序也是我们软件需要去编写提供的)。
(2)外部中断对应哪个引脚?
咱们使用P3.2接Key1(JP1_K1)
1.9.7.2、参考数据手册中示例代码写程序(手册P118)
(1)编程使用INT0处理按键(P3.2)
(2)程序解释
IT0 = 1; //set INT0 interrupt type (1:Falling 0:Low level)
EX0 = 1; //enable INT0 interrupt
EA = 1; //open global interrupt switch
IT0/EX0/EA是三个寄存器,其实就是位变量,和P0、P1是一样都是在reg51.h中用sbit定义的!
IT0 中断控制寄存器位
IT0这一位用来设置INT0(P3.2)中断的触发模式:
=1 下降沿触发(Falling):引脚输入有一个1->0的跳变就触发中断
=0 低电平触发(low level): 引脚输入只要是0就触发中断INT0 就是P3.2
EX0 中断允许寄存器
EX0这一位是INT0的开关。
=0 外部中断在单片机内部被关闭,禁止INT0中断,此时CPU无法收到INT0的中断信息所以不会处理INT0;
=1 允许INT0中断
EA 中断允许寄存器
是全局的中断开关。
=0 CPU屏蔽所有的中断申请
=1 CPU开放中断
EA的作用是使中断允许形成两级控制。即各中断源首先受EA控制;其次还受各中断源自己的中断允许控制位(EX0)控制。
外部中断服务程序
//External interrupt0 service routine
//函数名exint0 可以随便起
void exint0() interrupt 0 //INT0, interrupt 0 (location at 0003H)
{
P0++;
}
在main中已经配置好了INT0的外部中断,只要INT0有外部中断,就会自动调用中断服务程序。
咱们在上面功能不完善的程序基础上修改!
#include <reg51.h>
/**************代码说明**************************
本程序采用中断方式,实现了静态数码管显示0-F并且按下Key1时,LED1亮
P0--静态数码管
P1.0 LED1
P3.2 KEY1
*************************************************/
sbit key1 = P3^2;
sbit led1 = P1^0;
// 独立数码管的段码表
unsigned char val[16] = {0xc0, 0xf9, 0xa4, 0xb0, 0x99, 0x92, 0x82, 0xf8, 0x80, 0x90, 0x88, 0x83, 0xc6, 0xa1, 0x86, 0x8e};
/*********************************** 函数声明 ***********************/
// 延时函数
void delay(void)
{
unsigned char i, j;
for (i=0; i<200; i++)
for (j=0; j<200; j++);
}
/******************************中断服务程序*************************/
void Eint_Key() interrupt 0
{
led1 = 0;
delay();
led1 = 1;
}
/***************************主函数*********************************/
void main(void)
{
unsigned char i = 0;
//INT0 外部中断开启
IT0 = 1;
EX0 = 1;
EA = 1;
led1 = 1;
while (1)
{
//主线任务
for (i=0; i<16; i++)
{
P0 = val[i];
delay();
}
}
}
1.9.7.3、总结
(1)中断能力是CPU本身设计时支持的,并不是编程制造出来的
(2)程序员只要负责2件事即可:主程序中初始化中断、定义中断处理程序。
(3)当中断条件发生时,硬件会自动检测到并且通知CPU,CPU会自动去执行中断处理程序,这一切都是CPU设计时定下的,不需要编程干预。
1.9.8.矩阵键盘的原理
1.9.8.1、矩阵键盘的原理图分析
(1)横向和纵向分割
横向四个是一组,纵向四个是一组!
引脚:1-4管的是横向的,5-8管的是纵向的
(2)按键两端分别接不同的IO引脚(独立按键只有只接一个引脚)
(3)按键的物理作用不变
按下接通电路,弹起断开电路。
1.9.8.2、矩阵键盘的工作过程
按键就是输入设备,只要按下,程序要知道按的是哪个按键。
(1)程序先送(IO引脚输出)0x0f
引脚:8 7 6 5 4 3 2 1
电平:0 0 0 0 1 1 1 1
(2)读取IO引脚输入,就可以判断哪一行按下了
(3)程序再送(IO引脚输出)0xf0
(4)从收到的数据(IO引脚输入)判断哪一列按下了
引脚:8 7 6 5 4 3 2 1
输出0x0f
输入:0 0 0 0 1 1 1 0 0x0e 第0行
0 0 0 0 1 1 0 1 0x0d 第1行
0 0 0 0 1 0 1 1 0x0b 第2行
0 0 0 0 0 1 1 1 0x07 第3行
输出0xf0
输入:1 1 1 0 0 0 0 0 0xe0 第0列
1 1 0 1 0 0 0 0 0xd0 第1列
1 0 1 1 0 0 0 0 0xb0 第2列
0 1 1 1 0 0 0 0 0x70 第3列
(5)综合2次得到的行和列位置,计算出键值
1.9.8.3、矩阵键盘的特点
(1)优点:省单片机IO
(2)缺点:不能同时按下多个按键
假如同时按下两个按键,会有四个按键被识别出来
1.9.9.矩阵键盘编程实战
接线:P0--静态数码管、P3--矩阵键盘
矩阵键盘按下,对应独立数码管显示键值。
代码
/********接线说明****************
P0--静态数码管
P3--矩阵键盘(1-8:P3.0-P3.7)
*********************************/
#include<reg51.h>
#define SDT P0 //静态数码管
#define KEY P3 //矩阵键盘
typedef unsigned char u8;
void delay10ms(void) //误差 0us
{
unsigned char a,b,c;
for(c=1;c>0;c--)
for(b=38;b>0;b--)
for(a=130;a>0;a--);
}
// 独立数码管的段码表
unsigned char val[16] =
{
0xc0, 0xf9, 0xa4, 0xb0,
0x99, 0x92, 0x82, 0xf8,
0x80, 0x90, 0x88, 0x83,
0xc6, 0xa1, 0x86, 0x8e
};
void main(void)
{
u8 KeyValue; //存放键值
while(1)
{
//检测行
KEY = 0x0f;
if(KEY != 0x0f)
{
delay10ms(); //消抖
//第一回合找出行
switch(KEY)
{
case(0x0e): KeyValue = 0; break; //第0行
case(0x0d): KeyValue = 1; break;
case(0x0b): KeyValue = 2; break;
case(0x07): KeyValue = 3; break; //第3行
default: break;
}
//第二回合找出列
KEY = 0xf0;
switch(KEY)
{
case(0xe0): KeyValue = KeyValue*4+0; break; //第0列
case(0xd0): KeyValue = KeyValue*4+1; break;
case(0xb0): KeyValue = KeyValue*4+2; break;
case(0x70): KeyValue = KeyValue*4+3; break; //第3列
default: break;
}
}
SDT = val[KeyValue];
}
}
在真实开发时,要求按键可以快速响应,想到的方法肯定是中断了,对于咱们使用的矩阵按键有八个输出,每个输出都接到中断复用引脚,显然不现实!
如何实现快速响应呢?
大致思路就是先要判断是否有按键按下,再去得到健值。
比如下面这个键盘最后有四路输出,不管哪个按键按下,都会通过74LS21触发外部中断,这时候再扫描按键得到健值传到单片机就可以了。
所以要在扫描健值上下功夫,感兴趣的话可以了解下线反转法,效率更高!
本节程序下载链接:按键
本节课结束!