5.4.2 按键扫描(单片机最简洁的键盘扫描程序详解)
2023-01-31 来源:zhihu
Proteus 原理图
一、要点
学会按键扫描输入判断
学会防抖动原理
学会按键扫描与按键菜单分开处理的模式
按键较少情况可以一起处理
按键较多推荐分开处理,程序层次分明
语法结构:#define 别名 原始值
宏是值的别名,也就是用一个名字完全替代某些数值、文字 、公式
这里开始用宏的目的是为了后续修改程序方便,也利于阅读代码
例如 P3_1 = 1 代表红灯亮,我们将 P3_1 起一个外号叫做State_Red_Led
State_Red_Led = 1 表示点亮红色状态指示灯,一目了然
后续如果我们需要将接口P3_1换到P4_1之类,只需要修改 #define State_Red_LED P3_1 变为 #define State_Red_LED P4_1
#define KEY_POWER 0x01 中间两个空格,KEY_POWER 完全等于 0x01
二、完整的C语言代码
#define SYS_CLK 12000000L//设置定时器、串口频率参数
#define KEY_POWER 0x01
#define KEY_DEC 0x02
#define KEY_SET 0x04
#define KEY_ADD 0x08
#define KEY_TIMER 0x10
#define State_Red_LED P3_1
#define State_Green_LED P3_2
#define State_Blue_LED P3_3
#include unsigned char keyDownValue = 0; unsigned char keyDownValueing = 0; unsigned char keyUpValue = 0; unsigned char delay4LongKeyScan = 0; bit mainLoopTimeFlag = 0; void KeyScan(); /* * 函数名称: KeyScan * 函数功能: 扫描独立键盘及输入 * 入口变量: 无 * 返回数据: 无 * 附加说明: 无 * 参考链接: https://blog.csdn.net/weixin_42880082/article/details/118991832 */ void KeyScan(){ unsigned char readKeyValue = ((P1)^0xFF); keyDownValue = (readKeyValue&(readKeyValue^keyDownValueing)); keyUpValue = ((readKeyValue^keyDownValue)^keyDownValueing); keyDownValueing = readKeyValue; if((keyDownValue&KEY_POWER)){ State_Red_LED = 1; State_Green_LED = 0; State_Blue_LED = 0; } if((keyDownValue&KEY_DEC)){ State_Red_LED = 0; State_Green_LED = 1; State_Blue_LED = 0; } if((keyDownValue&KEY_SET)){ State_Red_LED = 0; State_Green_LED = 0; State_Blue_LED = 1; } if((keyDownValue&KEY_ADD)){ State_Red_LED = 1; State_Green_LED = 1; State_Blue_LED = 0; } if((keyDownValue&KEY_TIMER)){ State_Red_LED = 1; State_Green_LED = 0; State_Blue_LED = 1; } } void T_IRQ0(void) interrupt 1 using 1{ mainLoopTimeFlag = 1; } void Timer0Init(void) //100微秒@12.000MHz { TMOD &= 0xf0; //设置定时器模式 TMOD |= 0x01; //设置定时器模式 TL0 = 0x9c; //设定定时初值 TH0 = 0xff; //设定定时初值 } void setup() { // 点亮P3_0接口LED 证明电源上电 P3_0 = 1; Timer0Init(); TR0 = 1; EA = 1; ET0 = 1; P3_1 = 0; P3_2 = 0; P3_3 = 0; } void loop() { if(mainLoopTimeFlag == 1){ mainLoopTimeFlag = 0; KeyScan(); } } int main(void) { setup(); while(1){ loop(); } return 1; } 四、一步步图形化编程 1、创建宏,使用宏的意义三、完整图形化代码
#define SYS_CLK 12000000L//设置定时器、串口频率参数
#define KEY_POWER 0x01
#define KEY_DEC 0x02
#define KEY_SET 0x04
#define KEY_ADD 0x08
#define KEY_TIMER 0x10
#define State_Red_LED P3_1
#define State_Green_LED P3_2
#define State_Blue_LED P3_3
#define KEY_POWER 0x01 //电源按键
#define KEY_DEC 0x02 //递减按键
#define KEY_SET 0x04 //设置按键
#define KEY_ADD 0x08 //递增按键
#define KEY_TIMER 0x10 //定时按键
为什么五个按键后面的值是 0x01 0x02 0x04 0x08 0x10,而不是用1 2 3 4 5 代替
五个按键 分别接到了 P1.0 ~ P1.4 接口
0x01 是十六进制的写法,转换为二进制 00000001
0x02 是十六进制的写法,转换为二进制 00000010
0x04 是十六进制的写法,转换为二进制 00000100
0x08 是十六进制的写法,转换为二进制 00001000
0x10 是十六进制的写法,转换为二进制 00010000
发现规律了吗?P1 是个8位的接口,P0.1 对应最右边的一位 00000001 对应电源按键
当power按键按下去时,P0.1 从00000001变为00000000 内部的程序就能检测到。
其他按键类似,所以我们开始可以将P1这个接口的寄存器变为11111111,那一位变成0就对应那个按键按下,可以对应8个按键,本产品我们用到了5个按键 P1.5 P1.6 P1.7未用
另外注意单片机接口从P1.0 ~P1.7 而不是P1.1~P1.8 开始,C语言程序中后续学到的数组、引脚控制都是从0~7代表8位的值,而不是从1开始的。
2、创建全局变量
语法结构:type variable_name = value;
语法结构:类型 变量名 = 值; 类型与变量名之间必须有空格,等号两边空格可有可无
开始定义变量时,是可以不赋值的,也就是说 = 和=后面的部分可以没有,但是实际上类似keil这种编程软件后台已经把变量自动初始化了,也就是自动赋予了一个默认值,不过建议最好是自己赋初始值,避免出错。
变量类型,最大意义是标明变量需要多大的存储空间,unsigned char 无符号字符型占用8个位的存储空间,也就是类似于我们快递柜,占了8个格子。
unsigned char 和 char 类型的唯一区别是,8个格子最左侧(最高位)格子填充1代表负数,填充0 代表正数,但是仍然都是占用8个格子
变量的根本意义是提前规划好存储空间里准备去放置未知的数,犹如我们中学学到的x,y,它不是凭空就有,而是我们的大脑开辟了一个空间放置,如果写在纸上,纸面的空间被x,y占用了,但是这个x,y是未知数,也就是经过运算或者转换才能具体确定值,电脑里面存放的这个变量和实际上我们脑袋里存放的没有什么区别。
各种类型的变量,最终都是转换为一个数字,然后转换为一个二进制数字,二进制数字的0,1最终转换为一个开关信号,转换为一个有和无(高和低)电信号存储或者在某个时间段不消失。
一个文件中,在所有函数的前面定义的变量是全局变量,可以被文件中所有函数使用
程序受两个基本的规则约束,一个是时间,一个是空间,全局的意义就是空间约束
实际所有看得见的事物都脱离不了时间和空间,一段程序的位置就决定了它的作用范围
unsigned char keyDownValue = 0; //按键按下时取值变量
unsigned char keyDownValueing = 0; //长按按键时的取值变量
unsigned char keyUpValue = 0; //放开按键时的取值变量
unsigned char delay4LongKeyScan = 0; //长按按键时的延迟时间变量 //本节课未用
bit mainLoopTimeFlag = 0; //主循环延时执行变量
注意:图形化代码,并赋值为空,但是实际看右边代码自动初始化为0,这是有编辑器自动完成的。
最终形成的C语言代码一致。图形化代码只是辅助,最终的C语言代码是执行最后编译时使用!
3、定义键盘扫描函数
键盘扫描函数的完整图形代码块
C语言代码块
/*
* 函数名称: KeyScan
* 函数功能: 扫描独立键盘及输入
* 入口变量: 无
* 返回数据: 无
* 附加说明: 无
* 参考链接: https://blog.csdn.net/weixin_42880082/article/details/118991832
*/
void KeyScan(){
unsigned char readKeyValue = ((P1)^0xFF);
keyDownValue = (readKeyValue&(readKeyValue^keyDownValueing));
keyUpValue = ((readKeyValue^keyDownValue)^keyDownValueing);
keyDownValueing = readKeyValue;
if((keyDownValue&KEY_POWER)){
State_Red_LED = 1;
State_Green_LED = 0;
State_Blue_LED = 0;
}
if((keyDownValue&KEY_DEC)){
State_Red_LED = 0;
State_Green_LED = 1;
State_Blue_LED = 0;
}
if((keyDownValue&KEY_SET)){
State_Red_LED = 0;
State_Green_LED = 0;
State_Blue_LED = 1;
}
if((keyDownValue&KEY_ADD)){
State_Red_LED = 1;
State_Green_LED = 1;
State_Blue_LED = 0;
}
if((keyDownValue&KEY_TIMER)){
State_Red_LED = 1;
State_Green_LED = 0;
State_Blue_LED = 1;
}
}
函数语法结构:
return_type function_name( parameter list )
{
body of the function
}
解释:
返回类型 函数名(到函数内部执行的变量列表)
{
函数内的执行语句
return 变量; //这个变量的类型必须和返回类型一致
}
函数的作用是,把一些程序执行语句集中起来放到一块,可以被其他地方的函数或程序使用。程序清晰化,另外是将需要运算或者传递的数据包括起来,执行特定的操作,并返回运算结果。
本节课的键盘扫描函数 void KeyScan() 没有返回数据,所以用void表示,代表无的意思,没有输入的变量所以只有()
void KeyScan() //这是函数最简洁的写法
{
}
4、创建局部变量与创建表达式
局部变量与全局变量的定义方式相同,局部变量一般放在函数内部
局部变量有时候称为函数的局部变量,局部变量只能被定义它的函数使用,不能被其他函数使用
以上这么多只是程序的一部分基础,涉及到程序实现的功能在这一小节展现
先看完整的图形代码
unsigned char readKeyValue = ((P1)^0xFF);
keyDownValue = (readKeyValue&(readKeyValue^keyDownValueing));
keyUpValue = ((readKeyValue^keyDownValue)^keyDownValueing);
keyDownValueing = readKeyValue;
这四句网络搜索到的核心代码实现了键盘扫描的主要功能,具体那位大神最先实现的无法查到。但是非常高明。大部分键盘扫描至少10句20句代码才能完成。
上面代码比较复杂需要一行行解释,另外就是出现了数学运算
https://www.runoob.com/cprogramming/c-operators.html
unsigned char readKeyValue = ((P1)^0xFF);
定义了一个局部变量readKeyValue (这个变量因为是在KeyScan函数内写的,所以是局部变量)
在定义变量的同时为其赋值,赋值的意思就是为readKeyValue占用的空间中填充上0或1
unsigned char 是无符号整数,这个估计是最常用的一种变量类型,无符号整数这种类型的意义是,在单片机内部的存储空间中开辟8位的一个小存储块,犹如快递柜的8个格子。每个格子里面只能放0或1,用二进制标识就是00000000、11111111、00100000等等。
这8个格子放0或1,能够最大表示的数是 11111111 = 0xFF (十进制255)
也就是这个变量能存放的数是0~255(从零开始 所以是256个数)
如果你写 readKeyValue = 300; 是不对的,因为300存不到这8个格子中,存不下。
8个格子太多,我们分别用2个格子、3个格子、4个格子存二级制数来试试最多能存多少,注意每个格子只能存0或者1
2个格子填充(均以二级制数参考,最多存放四个数)
3个格子的情况 (最多存放8个数)
四个格子的情况 (最多存放16个数, 从0开始)
规律,就是X个格子最大存放的数就是2x
所以8个格子是 28=256
((P1)^0xFF) = P1^0xFF
这个写法可以 去掉里面的括号,因为只有P1,没有其他计算
P1 是一个8位的寄存器,也是8个格子,这些格子里面存放的是P1.0~P1.7 各个引脚的状态,是0还是1,是0代表这个引脚是低电平,1代表这个引脚是高电平
0xFF 是十六进制的写法,一位十六进制和四位二进制是一一对应的 也就是说F = 1111
0xFF = 11111111
^是一种位运算符,也就是异或运算符,它将8个格子与另外8个格子对应的位一一运算,基本规则是相同为0,不同为1,实现了取反码的功能,原先为0变为1 原先为1变为0;
假设P1=11111100 0xFF=11111111
P1^0xFF = 11111100^11111111=00000011
11111100
^ 11111111
00000011
相同为0 不同为1
最后结果 00000011 最后两位为1 代表P1.0 P1.1 两个按键同时按下去了
P1 = 11111011
P1^0xFF = 11111011^11111111=00000100 代表第三个按键按下 KEY_SET (P1.2)按下去了。
保存最后变反码的8位二进制数放到readKeyValue
keyDownValue = (readKeyValue&(readKeyValue^keyDownValueing));
这句话又出现一个位运算符&,规则是见0为0,全1为1
括号里面的先进行计算和我们初中学的括号作用无差别
从右往左计算
keyDownValueing 存放的是一直按键一直按住不放的数值
readKeyValue 是我们刚才计算的按下的位值
readKeyValue^keyDownValueing 的作用是,当前值与一直按着的键值运算,相同为0,不同为1,运算结果意义就是0时,此按键还在一直按住没有释放。为1时,已经释放。
readKeyValue&(readKeyValue^keyDownValueing) 把这个值再判断下
这几行代码比较难于理解
我们参考链接:三行按键扫描程序2
我们再参考链接:三行实现按键扫描
我们实际用Excel进行计算下这几个公式。
可以下载文件
这一小节用文字描述可能不够清晰,会视频课再细讲。
如果不能理解,针对按键扫描的这几行代码,我们直接借用即可,不需要深入理解。
5、键盘扫描判断语句
上一小节我们分析了keyDownValue是短按按键时的值,只要判断它就可以知道那个按键按下
if((keyDownValue&KEY_POWER)){ State_Red_LED = 1; State_Green_LED = 0; State_Blue_LED = 0; }
语法结构:
if(逻辑运算表达式) { 表达式为真,执行代码写在这里。 }
(keyDownValue&KEY_POWER) , & 这个位运算符号是见 0 为 0 全 1 为 1 只有当keyDownValue 最右一位是 1(00000001),这个 if 判断才为真,我们提前定义了KEY_POWER == 0x01 ,两个计算的结果就是判断第一位是不是为 1,判断第一个按键(power键)短按了。
C 判断 | 菜鸟教程www.runoob.com/cprogramming/c-decision.html
图形编码与C语言编码
6、调用键盘扫描函数
注意本节课按键扫描函数在主函数中直接调用,主定时器只是100us的情况,实际应用需要延时20ms再调用键盘扫描函数。下节课修正。
防抖我们下一节课和长按键一起讲
调用扫描函数比较简单,就是找个位置来使用 KeyScan()这个函数
7、proteus仿真,分别按5个按键,RGB灯红、绿、蓝、黄、紫色依次变化