单片机
返回首页

5.4.2 按键扫描(单片机最简洁的键盘扫描程序详解)

2023-01-31 来源:zhihu

Proteus 原理图

一、要点

  • 学会按键扫描输入判断

  • 学会防抖动原理

  • 学会按键扫描与按键菜单分开处理的模式

    • 按键较少情况可以一起处理

    • 按键较多推荐分开处理,程序层次分明




    二、完整的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 别名 原始值

    • 宏是值的别名,也就是用一个名字完全替代某些数值、文字 、公式

    • 这里开始用宏的目的是为了后续修改程序方便,也利于阅读代码

    • 例如 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


#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灯红、绿、蓝、黄、紫色依次变化

进入单片机查看更多内容>>
相关视频
  • RISC-V嵌入式系统开发

  • SOC系统级芯片设计实验

  • 云龙51单片机实训视频教程(王云,字幕版)

  • 2022 Digi-Key KOL 系列: 你见过1GHz主频的单片机吗?Teensy 4.1开发板介绍

  • TI 新一代 C2000™ 微控制器:全方位助力伺服及马达驱动应用

  • MSP430电容触摸技术 - 防水Demo演示

精选电路图
  • PIC单片机控制的遥控防盗报警器电路

  • 红外线探测报警器

  • 短波AM发射器电路设计图

  • 使用ESP8266从NTP服务器获取时间并在OLED显示器上显示

  • 开关电源的基本组成及工作原理

  • 用NE555制作定时器

    相关电子头条文章