历史上的今天
返回首页

历史上的今天

今天是:2026年01月31日(星期六)

2023年01月31日 | 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灯红、绿、蓝、黄、紫色依次变化

推荐阅读

史海拾趣

Conditioning Semiconductor Devices Corp公司的发展小趣事

Conditioning Semiconductor Devices Corp(简称CSDC)起初是一家小型半导体公司,专注于研发低功耗的芯片技术。随着智能设备的普及,市场对节能型半导体的需求激增。CSDC通过不断的研发投入,成功开发了一种全新的低功耗技术,显著降低了设备的能耗,迅速在市场上获得认可,从而实现了业务的快速增长。

敦泰(FOCALTECH)公司的发展小趣事
电冰箱不制冷可能由多种电路问题引起,如电源线路故障(如插头未插紧、插座无电等)、压缩机启动电路故障(如启动器损坏、压缩机线圈断路等)、温控电路故障(如温控器失灵、温度传感器损坏等)或制冷系统电路故障(如制冷剂泄漏、毛细管堵塞等)。建议首先检查电源是否正常,然后逐步排查压缩机、温控器和制冷系统电路。
千志电子(CCO)公司的发展小趣事

随着技术的不断进步和市场的日益成熟,千志电子开始注重电阻产业的深耕。公司不仅专注于电阻的生产,还逐渐向电阻专用设备、原材料等领域延伸。2006年,千志电子成立了深圳市鑫兴志实业有限公司,主要生产电阻相关的生产机器如切割机、焊接机、成型机、涂装机等。同时,千志电子还成立了千志电子科技(湖北)有限公司,生产各类型电阻器、设备及电阻所需原材料如碳棒、线材等。这一战略调整使千志电子形成了从原材料到设备的完整产业链,提高了生产效率和产品质量,进一步巩固了其在电阻行业的领先地位。

ELINA INDEK公司的发展小趣事

作为一家领先的电子公司,因美纳深知自己的社会责任。公司积极参与各种公益活动,推动基因测序技术在医疗、环保等领域的应用。同时,因美纳还注重可持续发展,通过采用环保材料、优化生产流程等方式降低对环境的影响。这种积极履行社会责任和推动可持续发展的做法赢得了社会各界的广泛赞誉。

ELMOS公司的发展小趣事

ELMOS公司自创立以来,就专注于汽车电子领域的发展。多年来,公司不断积累在模拟混合信号集成电路设计方面的专业知识,形成了深厚的技术底蕴。这种长期的技术积累使得ELMOS在汽车和工业物理接口领域拥有广泛的产品线,特别是在供电和DC/DC方面积累了丰富的经验。这种技术积累不仅为ELMOS赢得了市场的认可,也为公司的持续发展奠定了坚实的基础。

Abbatron公司的发展小趣事

在电子行业的激烈竞争中,Abbatron公司以其创新的技术赢得了市场的认可。某年,公司研发团队成功开发出了一款新型的高效能芯片,这款芯片不仅性能卓越,而且功耗极低,引起了业界的广泛关注。通过这一技术突破,Abbatron公司在市场上占据了有利地位,并逐渐成为了行业内的佼佼者。

问答坊 | AI 解惑

matlab第三课

数组与矩阵运算 这是今天讨论的重点!…

查看全部问答>

TFT液晶彩图显示法(特别适合初学者,详细……)

TFT液晶彩图显示法,很详细,特别适合初学者,高手也可参考参考…… 值得保存………

查看全部问答>

求职时被HR立即否决的9种人

一般来说,下面几类人,容易被HR快速拒绝。 开口言钱者不要 报酬不是不可以问,但得讲究时机和氛围。如果刚一交谈,就开门见山、直奔主题地问起薪酬待遇,会让企业感到很不舒服。 纠缠不休者不要。 招聘都遵循一定的流程,说几时给消息就几时给 ...…

查看全部问答>

wince 6.0 驱动基础问题,请教高手

刚开接触wince 6.0 驱动开发。有些基础问题没有搞清楚,往高手指点,请详细些。 1. sources文件中都会有一个动态库的入口,这个动态库的入口(DLLMain)是什么的?指的是驱动加载时的入口吗?我看了一个简单的按键驱动,在dllmain处只是简单的初始 ...…

查看全部问答>

定制的win ce 5.0 COM1不能输出调试信息,请问这是怎么回事。

我也使用RETAILMSG函数在驱动中添加调试信息,但同样没有调试信息输出。 我修改过WINCE500\\PLATFORM\\smdk2440\\KERNEL\\HAL目录下的debug.c文件中的“NODEBUG”我也修改为0了。 #define                ...…

查看全部问答>

一道汇编题?

25. 内存地址是0000H,若有4KB的存储空间,其内存的最终地址是(  ). A.400H        B.FFFH         C.4FFH      D.1000H 答案是什么?为什么?请高手 ...…

查看全部问答>

北京佳能诚聘c/c++嵌入式开发人员!

公司:佳能 职位:研发工程师 语言:c/c++ 平台:WINCE 经验:3年以上 地点:北京 有意向者,请速联系 msn:mygy2006@hotmail.com 13910500391…

查看全部问答>

wince4.2如何调用软键盘

我装了wince4.2 再装了EVC4.0 后来装了STANDARD_SDK.msi 写了个简单的hello程序 想调用SIP 头文件包含了#include \"sipapi.h\" 工程里添加了连接coredll.lib,调用SipShowIM(SIPF_ON); 怎么就是编译不过去 提示如下 Mystest.obj : error LNK2019: ...…

查看全部问答>

ARM入门

我刚开始上班,好多东西都不懂,经理把板子给我了.让我自己弄. 问其他同事,他们都说忙,好郁闷呀! 主机,ARM开发板,uC OS/II,ADS1.2集成开发环境,JTAG仿真器.都有了. 我下一步该做什么. 是不是安装uC OS/II,然后在上面编程序. 我以前从来没有接触 ...…

查看全部问答>

力科公司确立其在示波器领域的绝对领导地位

力科公司确立其在示波器领域的绝对领导地位…

查看全部问答>