单片机按键交互状态机_单片机用户交互状态机 - CSDN
  • 单片机常用按键电路

    千次阅读 2014-09-28 15:01:51
    单片机组成的小系统中,有的需要人机交互功能,按键是最常见的输入方式。最常见的按键电路大致有,一对一的直接连接和动态扫描的矩阵式连接两种。  一对一的直接连接就是一个按键直接对应一个CPU的输入口,比如...

        单片机组成的小系统中,有的需要人机交互功能,按键是最常见的输入方式。最常见的按键电路大致有,一对一的直接连接和动态扫描的矩阵式连接两种。

        一对一的直接连接就是一个按键直接对应一个CPU的输入口,比如下图

    单片机常用按键电路

       
    左右两个电路作用一样,区别是左边CPU的输入端常态为高电位,按下按键时为低电位;右边的常态为低电位,按下按键是高电位。

        这样的电路简单直接,一个按键独占一个端口,在按键数量较少端口数量富裕时可以直接使用。但很多场合需要的按键数比较多,要尽量少地占用端口就必须使用矩阵式的按键链接。如下图:

    单片机常用按键电路

        图中将按键按行列矩阵的方式排列,其中的每一行公用一根行线,每一列公用一根列线。以此图为例,16个按键,按一对一方式连接的话需要16个端口,而按这样的矩阵方式链接只需要8个端口,所需端口数大量减少。按键数Knum=line*row,而端口数Pnum=line+row,其中的li单片机常用按键电路ne和row分别代表行数和列数。

        图中的Px,Py为CPU的IO端口,在本例中可以使用不同的端口也可以使用同一个8位端口。上拉电阻不是必须,单片机IO口内部有上拉电路时此处就可省略。

        这个电路的工作原理是采用程序扫描的方式检测某个按键状态。比如将Px口的4位全置为低电平,这时如果没有任何按键按下的话,从Py口读回的4位应全为高,而如果有某一键按下,则对应按下键的那一列的位读回值将为低。这样就能知道按下键所在的列;接着确定按键所处行,把Py口的输入值作为输出,Px全部置高并读取输入,就能得到按键所在行位置,于是就确定了所按按键的行列位置。

        扫描可以有两种实现的方法,一种是全行全列扫描,一种是逐行全列扫描。

     

        上面的例子其实就是全行全列扫描方式,见流程框图。

        其特点是,一个流程就能到是否有按键按下,并能确定按下按检测行列值,检测步骤简单迅速。但作为行列接口的Px,Py必须是双向的,亦即同时具有输入输出功能,单片机的端口基本都能满足。

        但如果端口非双向,或按键数量大,端口数紧张需进一步减少端口时,也许就需要别的方式来解决。

        还是拿上面的电路做例子,全行全列扫描是在检测到有按键按下时,先检测列然后再确定行。

     

    单片机常用按键电路

       

     

     

       换种检测方式,就是先给定行,再检测列。比如行端口Px每次输出不是全部,而是只有一位输出为低,也就是预先给定了行,那么对应行有按键按下时,Py读回的值就代表按键所在列。Px口按位逐一输出低,每次读回Py值,这样的处理方式,更贴近扫描的含义。因为按键是机械动作,相对单片机运行速度来说,一次扫描流程足够检测到按键按下的动作。这种扫描方式就是逐行全列扫描。见流程图。

     

     

     

     

     

     

       这种扫描方式的特点是逐行扫描,有多少行就扫多少次,当有按键按下时,行列数就确定了。虽然显得麻烦点,但好处是Px只需是输出而Py只是输入,Px输出每次只有唯一的一位为低,这样的特点就可以对端口数进行简化,比如使用译码器。如图所示:
    单片机常用按键电路
        由图可以看出,同样按键数,增加一个138译码器之后,CPU所用端口数就减为5了。

    单片机常用按键电路



        Px口的3位只需输出0到7,译码器输出就能得到和前面一样的行扫描信号。这时候的程序处理流程,和上面的略有不同,主要是行的表示上不同。上面是行数的对应位表示对应行,下面的是行数的对应值就是对应行。程序框图如所示。

        逐行扫描还有另外一个用处,就是当系统中有需要动态扫描的装置比如LED数码管或点阵时,行扫描线就可以为其提供动态扫描信号,这样也是为了减少端口使用数量,达到信号复用并减少代码量的目的。


     

     

     

     

     

        除了上面提到的几种按键电路,还有一种按键电路,使用更少的端口数量,如图

    单片机常用按键电路

        该电路同矩阵式按键电路一样,所不同的是行列端口使用的是同一个端口,并且矩阵的一条对角线上按键由二极管代替。如此图所示,

    按键数Knum=Pnum*(Pnum-1),其中Pnum就是使用的端口数。

        以4个端口数为例,

    一对一连接方式只能是4个按键;

    不带译码器最多4个按键,

    使用2-4译码器或3-8译码器方式最多8个按键

    而这种电路可以达到12个按键。此电路程序部分和不带译码器的一样,只是注意对角线上被二极管替代的地方没有按键。

    展开全文
  • 按键是人机交互最简单也是最廉价的方式之一,要实现一个或者多个按键的有效扫描并处理,这里附上我修改过的代码:实现的代码主要包含有四个部分:第一部分:按键的初始化部分void Key_Configuration(void) ...

    按键是人机交互最简单也是最廉价的方式之一,要实现一个或者多个按键的有效扫描并处理,这里附上我修改过的代码:

    实现的代码主要包含有四个部分:

    第一部分:按键的初始化部分

    void Key_Configuration(void)
    {
    	return;
    }

    这里需要根据所使用的IC来做不同的配置方式,我使用的是51内核,在初始化的过程I/O口默认做了准双向若上拉处理,按键低电平有效,所以就没有处理直接跳出去。

    第二部分:按键的电平读取

    //只读取初次按键电平状态,在状态机中进一步处理
    static u8 Key_Read(void)
    {
        if(!READ_KEY1)  
    		return KEY1_PRES;
        if(!READ_KEY2)  
    		return KEY2_PRES;       
        if(!READ_KEY3)  
    		return KEY3_PRES;
        if(!READ_KEY4)  
    		return KEY4_PRES;
        if(!READ_KEY5)  
    		return KEY5_PRES;
    	
    	return KEY_NONE;
    }

    根据使用的具体环境及功能,这里每次读取电平只读取一个有效的电平并且有优先级,由代码可以看出优先级的顺序为:KEY1>KEY2>KEY3>KEY4>KEY5。当然需要使用多少个按键根据项目的需求来定,理论支持多少个独立按键都是可以的。

    第三部分:状态机的按键判定部分

    //状态机
    static u8 Key_Scan(void)
    {
    	static u8 state = 0; //按键初始化状态
    	static u8 KEY_LAST=0,KEY_NOW=0; //记录两次电平的状态
    	u8 KEY_VALUE=0;
    
    	KEY_NOW = Key_Read();//读按键值
       
    	switch(state)
    	{
    		case 0:
    		{
    			if(KEY_NOW != KEY_LAST)	state = 1; //有按键按下
    		}break;
    		case 1: 
    		{
    			if(KEY_NOW == KEY_LAST)	state = 2; //消斗之后按键有效
    			else state = 0; //认为误触
    		}break; 
    		case 2: //消斗之后
    		{
    			if(KEY_NOW == KEY_LAST) //还是按下的状态 
    			{
    			  	state = 3;
    			}
    			else//松开了,短按
    			{
    				state = 0; 
    				
    				KEY_VALUE = KEY_LAST|KEY_SHORT;  //返回键值短按	
    			}
    		}break;
    		
    		case 3: //判断长按短按
    		{
    			if(KEY_NOW == KEY_LAST) 
    			{
    			    static u8 cnt = 0;
    				if(cnt++ > 120) //1200ms
    				{
    					cnt = 0; 
    					state = 4;
    					KEY_VALUE = KEY_LAST|KEY_LONG; //返回键值长按
    				}			  
    			}
    			else
    			{
    				state = 0;
    				KEY_VALUE = KEY_LAST|KEY_SHORT; //返回键值短按			
    			}
    		}break;
    		case 4://长按松手检测
    		{
    			if(KEY_NOW != KEY_LAST) 
    				state = 0;
    		}break;
    	}//switch
    	
    	KEY_LAST = KEY_NOW; //更新
    	return KEY_VALUE;
    }

    这部分也是整个的核心代码部分,首先定义了三个静态变量,按键的状态state,当前读取的键值KEY_NOW,上一次的键值KEY_LAST,以及返回的判定后的有效键值KEY_VALUE。接下来一步步研究:

    state初始值为0,进入switch和case,每次进入case判断的时间间隔是由第四部分来确定的,这里给出我选用的是10ms

    判定0,只要上一次记录的键值和本次读取到的键值不想等,则进入1。这里还有另一个作用,那就是短按的松手检测。这也是没有else的原因,具体如何实现请看状态4。

    判定1,上一次和这一次的键值相等,注意case 0后KEY_LAST已经被更新,也就是说10ms后这次读取到的键值还等于上一次的键值,这里我们认为是有效的按下,而并非误触和干扰造成的,这种情况下进入2。否则就返回到0。

    判定2,这里是从1过来的,也就是此时的按键键值是有效的,这里还是来判断上次更新的KEY_LAST及这次读取到的新的键值,如果不想等,证明手已经松开(单一按键的情况)。这样就识别成了短按,state重新回到0并返回短按的键值量,在0。如果按键还没有被释放那就有长按的趋势了,进入3。

    判定3,这里也很简单,判定键值有没有释放,每次进来就开始计次,10ms进入一次,这里计120次也就是需要1200ms的时间达到条件并且把返回的键值赋值成长按,同时进入4,反之没有达到时间就识别成短按并重新进入0。

    判定4,这里代码的作用主要是作为长按的松手检测,道理也很简单,按键没有释放,那肯定历史键值和当前的键值相等并且不为0,等按键释放的后,读取的键值肯定为0,这就跳出了状态4。

    第四部分:实体函数及被调用函数

    static void KEY1_ShortHander(void)
    {
    
    }
    static void KEY1_LongHander(void)
    {
    
    }
    static void KEY2_ShortHander(void)
    {
    
    }
    static void KEY2_LongHander(void)
    {
    
    }
    static void KEY3_ShortHander(void)
    {
    
    }
    static void KEY3_LongHander(void)
    {
    
    }
    static void KEY4_ShortHander(void)
    {
    
    }
    static void KEY4_LongHander(void)
    {
    
    }
    static void KEY5_ShortHander(void)
    {
    
    }
    static void KEY5_LongHander(void)
    {
    
    }
    
    
    void Key_Hander(void)					//按键处理函数
    {
    	u8 KEY_NUM=0;
    	static u32 LAST=0;
    	if(Systick_ms-LAST<10)	return;
    	LAST = Systick_ms;	
    	
    	KEY_NUM = Key_Scan();  //按键扫描值
    	if(KEY_NUM == KEY_NONE) return;
    	
    	//有按键按下
    	if(KEY_NUM & KEY_SHORT) //短按
    	{    
    		if(KEY_NUM & KEY1_PRES)//KEY1_PRES
    		{				
    			KEY1_ShortHander();
    		}
    		else if(KEY_NUM & KEY2_PRES)//KEY2_PRES
    		{
    			KEY2_ShortHander();
    		}		
    		else if(KEY_NUM & KEY3_PRES)//KEY3_PRES
    		{
    			KEY3_ShortHander();
    		}
    		else if(KEY_NUM & KEY4_PRES)//KEY4_PRES
    		{
    			KEY4_ShortHander();
    		}
    		else if(KEY_NUM & KEY5_PRES)//KEY5_PRES
    		{
    			KEY5_ShortHander();
    		}
    	}
    	else if(KEY_NUM & KEY_LONG) //长按 
    	{
    		if(KEY_NUM & KEY1_PRES)//KEY1_PRES
    		{
    			KEY1_LongHander();
    		}
    		else if(KEY_NUM & KEY2_PRES)//KEY2_PRES
    		{
    			KEY2_LongHander();
    		}		
    		else if(KEY_NUM & KEY3_PRES)//KEY3_PRES
    		{
    			KEY3_LongHander();
    		}
    		else if(KEY_NUM & KEY4_PRES)//KEY4_PRES
    		{
    			KEY4_LongHander();
    		}
    		else if(KEY_NUM & KEY5_PRES)//KEY5_PRES
    		{
    			KEY5_LongHander();
    		}
    	}	
    }
    

    这部分代码是比较清晰的,通过前面3部分的分析,在这里调用前3部分的得到键值的结果,然后判定结果做相应的函数和功能处理,这里给的直接是函数体,用户可以直接在函数体里面添加响应的代码。但更有效的方式应该是把执行函数换成标记变量,这样程序执行会更加有条理并且不错误的占用时间片。

    最后附上头文件:

    #ifndef __FY_KEY_H
    #define __FY_KEY_H
    
    #include "fy_includes.h"
    
    #define READ_KEY1	P30
    #define READ_KEY2	P14
    #define READ_KEY3	P13
    #define READ_KEY4	P10
    //#define READ_KEY5	P17
    
    #define KEY1_PRES 0x01
    #define KEY2_PRES 0x02
    #define KEY3_PRES 0x04
    #define KEY4_PRES 0x08
    #define KEY5_PRES 0x10
    
    #define KEY_SHORT  0x40
    #define KEY_LONG   0x80
    #define KEY_NONE      0
    
    #define HOT_KEY_PRES	KEY1_PRES
    #define PWR_KEY_PRES	KEY2_PRES
    #define H2_KEY_PRES	KEY3_PRES
    #define MP3_KEY_PRES	KEY4_PRES
    
    void Key_Configuration(void);
    void Key_Hander(void);
    
    #endif
    
    /*********************************************END OF FILE**********************************************/
    
    
     By Urien 2018年3月19日 10:47:36
    展开全文
  • 单片机按键FIFO

    千次阅读 2020-06-12 10:19:36
    一般的单片机系统,按键作为人机交互工具是必不可少的,但是普通的按键需要消抖处理,极大的增加了程序开销,降低系统实时性。 安富莱的FIFO按键,无需延时处理消抖,可以记录按键按下、弹起、长按、组合按,并且...

    本文代码参考安富莱按键FIFO

    FIFO数据结构如果不清楚可以参考博文简单的FIFO

    一般的单片机系统,按键作为人机交互工具是必不可少的,但是普通的按键需要消抖处理,极大的增加了程序开销,降低系统实时性。

    安富莱的FIFO按键,无需延时处理消抖,可以记录按键按下、弹起、长按、组合按,并且移植起来也十分方便。之前在做一个项目时,用到一个矩阵键盘,移植了这个按键FIFO程序,用起来效果很不错。

    主要流程就是开启一个10ms的定时器中断,在中断中扫描按键状态,并对按键状态进行分析消抖处理,如果按键动作,将按键动作压入FIFO中,在主循环中读取FIFO,获取按键状态。
    在这里插入图片描述

    使用时首先要调用初始化函数,此函数有两个子函数,分别完成变量初始化和板子硬件初始化。

    /*
    *********************************************************************************************************
    *	函 数 名: bsp_InitKey
    *	功能说明: 初始化按键. 该函数被 bsp_Init() 调用。
    *	形    参:  无
    *	返 回 值: 无
    *********************************************************************************************************
    */
    void bsp_InitKey(void)
    {
    	bsp_InitKeyVar();		/* 初始化按键变量 */
    	bsp_InitKeyHard();		/* 初始化按键硬件 */
    }
    

    移植时需要注意对应IO硬件初始化修改成自己的,这里使用的STC8A单片机,3*4矩阵键盘,使用的IO通过宏定义封装起来。
    在这里插入图片描述

    
    /* 矩阵键盘 */
    #define C1_PIN             1
    #define C1_GPIO_PORT       2
    
    #define C2_PIN             2
    #define C2_GPIO_PORT       2
    
    #define C3_PIN             3
    #define C3_GPIO_PORT       2
    
    #define C4_PIN             4
    #define C4_GPIO_PORT       2
    
     
    // Row1, Row2, Row3, Row4
    #define R1_PIN             1
    #define R1_GPIO_PORT       4
    
    #define R2_PIN             0
    #define R2_GPIO_PORT       2
    
    #define R3_PIN             2
    #define R3_GPIO_PORT       4
    
    
    /*
    *********************************************************************************************************
    *	函 数 名: bsp_InitKeyHard
    *	功能说明: 配置按键对应的GPIO
    *	形    参:  无
    *	返 回 值: 无
    *********************************************************************************************************
    */
    static void bsp_InitKeyHard(void)
    {
    	//KEY 初始化
        PIN_InitPushPull(C1_GPIO_PORT, C1_PIN);
    	PIN_InitPushPull(C2_GPIO_PORT, C2_PIN);
    	PIN_InitPushPull(C3_GPIO_PORT, C3_PIN);
    	PIN_InitPushPull(C4_GPIO_PORT, C4_PIN);
    	PIN_InitOpenDrain(R1_GPIO_PORT, R1_PIN);
    	PIN_InitOpenDrain(R2_GPIO_PORT, R2_PIN);
    	PIN_InitOpenDrain(R3_GPIO_PORT, R3_PIN);
    	
    }
    
    

    按键参数初始化需要注意设置连发速度来确定对应按键是否支持连按。还要自行修改判断按键按下函数和按键个数

    /*
    	按键滤波时间50ms, 单位10ms。
    	只有连续检测到50ms状态不变才认为有效,包括弹起和按下两种事件
    	即使按键电路不做硬件滤波,该滤波机制也可以保证可靠地检测到按键事件
    */
    #define KEY_FILTER_TIME   5
    #define KEY_LONG_TIME     100		     	/* 单位10ms, 持续1秒,认为长按事件 */
    #define KEY_COUNT    13	   					/* 按键个数, 12个独立建 + 1 个组合键 */
    static KEY_T xdata s_tBtn[KEY_COUNT];
    static KEY_FIFO_T xdata s_tKey;		/* 按键FIFO变量,结构体 */
    
    /* 检测按键按下函数 */
    static uint8_t IsKeyDown0(void)  {P(C1_GPIO_PORT, C1_PIN) = 1;  P(C2_GPIO_PORT, C2_PIN) = 1;  P(C3_GPIO_PORT, C3_PIN) = 1; P(C4_GPIO_PORT, C4_PIN) = 0; if (P(R2_GPIO_PORT, R2_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown1(void)  {P(C1_GPIO_PORT, C1_PIN) = 0;  P(C2_GPIO_PORT, C2_PIN) = 1;  P(C3_GPIO_PORT, C3_PIN) = 1; P(C4_GPIO_PORT, C4_PIN) = 1; NOP(50); if (P(R1_GPIO_PORT, R1_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown2(void)  {P(C1_GPIO_PORT, C1_PIN) = 0;  P(C2_GPIO_PORT, C2_PIN) = 1;  P(C3_GPIO_PORT, C3_PIN) = 1; P(C4_GPIO_PORT, C4_PIN) = 1; if (P(R2_GPIO_PORT, R2_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown3(void)  {P(C1_GPIO_PORT, C1_PIN) = 0;  P(C2_GPIO_PORT, C2_PIN) = 1;  P(C3_GPIO_PORT, C3_PIN) = 1; P(C4_GPIO_PORT, C4_PIN) = 1; if (P(R3_GPIO_PORT, R3_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown4(void)  {P(C1_GPIO_PORT, C1_PIN) = 1;  P(C2_GPIO_PORT, C2_PIN) = 0;  P(C3_GPIO_PORT, C3_PIN) = 1; P(C4_GPIO_PORT, C4_PIN) = 1; if (P(R1_GPIO_PORT, R1_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown5(void)  {P(C1_GPIO_PORT, C1_PIN) = 1;  P(C2_GPIO_PORT, C2_PIN) = 0;  P(C3_GPIO_PORT, C3_PIN) = 1; P(C4_GPIO_PORT, C4_PIN) = 1; if (P(R2_GPIO_PORT, R2_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown6(void)  {P(C1_GPIO_PORT, C1_PIN) = 1;  P(C2_GPIO_PORT, C2_PIN) = 0;  P(C3_GPIO_PORT, C3_PIN) = 1; P(C4_GPIO_PORT, C4_PIN) = 1; if (P(R3_GPIO_PORT, R3_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown7(void)  {P(C1_GPIO_PORT, C1_PIN) = 1;  P(C2_GPIO_PORT, C2_PIN) = 1;  P(C3_GPIO_PORT, C3_PIN) = 0; P(C4_GPIO_PORT, C4_PIN) = 1; if (P(R1_GPIO_PORT, R1_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown8(void)  {P(C1_GPIO_PORT, C1_PIN) = 1;  P(C2_GPIO_PORT, C2_PIN) = 1;  P(C3_GPIO_PORT, C3_PIN) = 0; P(C4_GPIO_PORT, C4_PIN) = 1; if (P(R2_GPIO_PORT, R2_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown9(void)  {P(C1_GPIO_PORT, C1_PIN) = 1;  P(C2_GPIO_PORT, C2_PIN) = 1;  P(C3_GPIO_PORT, C3_PIN) = 0; P(C4_GPIO_PORT, C4_PIN) = 1; if (P(R3_GPIO_PORT, R3_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown10(void) {P(C1_GPIO_PORT, C1_PIN) = 1;  P(C2_GPIO_PORT, C2_PIN) = 1;  P(C3_GPIO_PORT, C3_PIN) = 1; P(C4_GPIO_PORT, C4_PIN) = 0; NOP(50); if (P(R1_GPIO_PORT, R1_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown11(void) {P(C1_GPIO_PORT, C1_PIN) = 1;  P(C2_GPIO_PORT, C2_PIN) = 1;  P(C3_GPIO_PORT, C3_PIN) = 1; P(C4_GPIO_PORT, C4_PIN) = 0; if (P(R3_GPIO_PORT, R3_PIN) == 0) return 1;else return 0;}
    
    /* 组合按键 key1 && key2 */
    static uint8_t IsKeyDown12(void)  { return IsKeyDown1() && IsKeyDown2();}
    /*
    *********************************************************************************************************
    *	函 数 名: bsp_InitKeyVar
    *	功能说明: 初始化按键变量
    *	形    参:  无
    *	返 回 值: 无
    *********************************************************************************************************
    */
    static void bsp_InitKeyVar(void)
    {
    	uint8_t xdata i;
    
    	/* 对按键FIFO读写指针清零 */
    	fifo_init(&s_tKey);
    	
    	/* 给每个按键结构体成员变量赋一组缺省值 */
    	for (i = 0; i < KEY_COUNT; i++)
    	{
    		s_tBtn[i].LongTime = KEY_LONG_TIME;			/* 长按时间 0 表示不检测长按键事件 */
    		s_tBtn[i].Count = KEY_FILTER_TIME / 2;		/* 计数器设置为滤波时间的一半 */
    		s_tBtn[i].State = 0;							/* 按键缺省状态,0为未按下 */
    		//s_tBtn[i].KeyCodeDown = 3 * i + 1;				/* 按键按下的键值代码 */
    		//s_tBtn[i].KeyCodeUp   = 3 * i + 2;				/* 按键弹起的键值代码 */
    		//s_tBtn[i].KeyCodeLong = 3 * i + 3;				/* 按键被持续按下的键值代码 */
    		s_tBtn[i].RepeatSpeed = 0;						/* 按键连发的速度,0表示不支持连发 */
    		s_tBtn[i].RepeatCount = 0;						/* 连发计数器 */
    	}
    
    
    	/* 判断按键按下的函数 */
    	s_tBtn[0].IsKeyDownFunc = IsKeyDown0;
    	s_tBtn[1].IsKeyDownFunc = IsKeyDown1;
    	s_tBtn[2].IsKeyDownFunc = IsKeyDown2;
    	s_tBtn[3].IsKeyDownFunc = IsKeyDown3;
    	s_tBtn[4].IsKeyDownFunc = IsKeyDown4;
    	s_tBtn[5].IsKeyDownFunc = IsKeyDown5;
    	s_tBtn[6].IsKeyDownFunc = IsKeyDown6;
    	s_tBtn[7].IsKeyDownFunc = IsKeyDown7;
    	s_tBtn[8].IsKeyDownFunc = IsKeyDown8;
    	s_tBtn[9].IsKeyDownFunc = IsKeyDown9;
    	s_tBtn[10].IsKeyDownFunc = IsKeyDown10;
    	s_tBtn[11].IsKeyDownFunc = IsKeyDown11;
    	
    	/* 组合按键 */
    	s_tBtn[12].IsKeyDownFunc = IsKeyDown12;
    }
    

    按键扫描函数,需要开启一个10ms的定时器中断,在中断中对按键状态进行扫描。

    
    /*
    *********************************************************************************************************
    *	函 数 名: bsp_DetectKey
    *	功能说明: 检测一个按键。非阻塞状态,必须被周期性的调用。
    *	形    参:  按键结构变量指针
    *	返 回 值: 无
    *********************************************************************************************************
    */
    static void bsp_DetectKey(uint8_t i)
    {
    	KEY_T xdata *pBtn;
    
    	/*
    		如果没有初始化按键函数,则报错
    		if (s_tBtn[i].IsKeyDownFunc == 0)
    		{
    			printf("Fault : DetectButton(), s_tBtn[i].IsKeyDownFunc undefine");
    		}
    	*/
    
    	pBtn = &s_tBtn[i];
    	if (pBtn->IsKeyDownFunc())
    	{
    		if (pBtn->Count < KEY_FILTER_TIME)
    		{
    			pBtn->Count = KEY_FILTER_TIME;
    		}
    		else if(pBtn->Count < 2 * KEY_FILTER_TIME)
    		{
    			pBtn->Count++;
    		}
    		else
    		{
    			if (pBtn->State == 0)
    			{
    				pBtn->State = 1;
    
    				/* 发送按钮按下的消息 */
    				bsp_PutKey((uint8_t)(3 * i + 1));
    			}
    
    			if (pBtn->LongTime > 0)
    			{
    				if (pBtn->LongCount < pBtn->LongTime)
    				{
    					/* 发送按钮持续按下的消息 */
    					if (++pBtn->LongCount == pBtn->LongTime)
    					{
    						/* 键值放入按键FIFO */
    						bsp_PutKey((uint8_t)(3 * i + 3));
    					}
    				}
    				else
    				{
    					if (pBtn->RepeatSpeed > 0)
    					{
    						if (++pBtn->RepeatCount >= pBtn->RepeatSpeed)
    						{
    							pBtn->RepeatCount = 0;
    							/* 常按键后,每隔10ms发送1个按键 */
    							bsp_PutKey((uint8_t)(3 * i + 1));
    						}
    					}
    				}
    			}
    		}
    	}
    	else
    	{
    		if(pBtn->Count > KEY_FILTER_TIME)
    		{
    			pBtn->Count = KEY_FILTER_TIME;
    		}
    		else if(pBtn->Count != 0)
    		{
    			pBtn->Count--;
    		}
    		else
    		{
    			if (pBtn->State == 1)
    			{
    				pBtn->State = 0;
    
    				/* 发送按钮弹起的消息 */
    				bsp_PutKey((uint8_t)(3 * i + 2));
    			}
    		}
    
    		pBtn->LongCount = 0;
    		pBtn->RepeatCount = 0;
    	}
    }
    /*
    *********************************************************************************************************
    *	函 数 名: bsp_KeyScan
    *	功能说明: 扫描所有按键。非阻塞,被systick中断周期性的调用
    *	形    参:  无
    *	返 回 值: 无
    *********************************************************************************************************
    */
    void bsp_KeyScan(void)
    {
    	uint8_t xdata i;
    
    	for (i = 0; i < KEY_COUNT; i++)
    	{
    		bsp_DetectKey(i);
    	}
    }
    

    在主函数中,非堵塞方式获取按键键值

    /*
    *********************************************************************************************************
    *	函 数 名: bsp_GetKey
    *	功能说明: 从按键FIFO缓冲区读取一个键值。
    *	形    参:  无
    *	返 回 值: 按键代码
    *********************************************************************************************************
    */
    uint8_t bsp_GetKey(void)
    {
    	uint8_t xdata ret;
    
    	if (fifo_pop(&s_tKey, &ret) == 1)
    	{
    		return KEY_NONE;
    	}
    	else
    	{
    		return ret;
    	}
    }
    
    

    完整代码如下
    bsp_key.h

    /*!
      * @file     BSP_KEY.h
      *
      * @brief    按键驱动文件
      *
      * @company  
      *
      * @author   不咸不要钱
      *
      * @note     无
      *
      * @version  
      *
      * @date     2019/10/18 星期五
      */ 
    #ifndef __LQ_KEY_H
    #define __LQ_KEY_H
    
    #include "LQ_GPIO.h"
    
    
    
    /* 矩阵键盘 */
    #define C1_PIN             1
    #define C1_GPIO_PORT       2
    
    #define C2_PIN             2
    #define C2_GPIO_PORT       2
    
    #define C3_PIN             3
    #define C3_GPIO_PORT       2
    
    #define C4_PIN             4
    #define C4_GPIO_PORT       2
    
     
    // Row1, Row2, Row3, Row4
    #define R1_PIN             1
    #define R1_GPIO_PORT       4
    
    #define R2_PIN             0
    #define R2_GPIO_PORT       2
    
    #define R3_PIN             2
    #define R3_GPIO_PORT       4
    
    
    
    
    #define KEY_COUNT    13	   					/* 按键个数, 12个独立建 + 1 个组合键 */
    
    
    
    /* 按键ID, 主要用于bsp_KeyState()函数的入口参数 */
    typedef enum
    {
    	KID_K0,
    	KID_K1, 
    	KID_K2,
    	KID_K3,
    	KID_K4,
    	KID_K5,
    	KID_K6,
    	KID_K7,
    	KID_K8,
    	KID_K9,
    	KID_K10,
    	KID_K11,
    	KID_K12,
    	
    }KEY_ID_E;
    
    /*
    	按键滤波时间50ms, 单位10ms。
    	只有连续检测到50ms状态不变才认为有效,包括弹起和按下两种事件
    	即使按键电路不做硬件滤波,该滤波机制也可以保证可靠地检测到按键事件
    */
    #define KEY_FILTER_TIME   5
    #define KEY_LONG_TIME     100			/* 单位10ms, 持续1秒,认为长按事件 */
    
    /*
    	每个按键对应1个全局的结构体变量。
    */
    typedef struct
    {
    	/* 下面是一个函数指针,指向判断按键手否按下的函数 */
    	uint8_t (*IsKeyDownFunc)(void); /* 按键按下的判断函数,1表示按下 */
    
    	uint8_t  Count;			/* 滤波器计数器 */
    	uint16_t LongCount;		/* 长按计数器 */
    	uint16_t LongTime;		/* 按键按下持续时间, 0表示不检测长按 */
    	uint8_t  State;			/* 按键当前状态(按下还是弹起) */
    	uint8_t  RepeatSpeed;	/* 连续按键周期 */
    	uint8_t  RepeatCount;	/* 连续按键计数器 */
    }KEY_T;
    
    /*
    	定义键值代码, 必须按如下次序定时每个键的按下、弹起和长按事件
    
    	推荐使用enum, 不用#define,原因:
    	(1) 便于新增键值,方便调整顺序,使代码看起来舒服点
    	(2) 编译器可帮我们避免键值重复。
    */
    typedef enum
    {
    	KEY_NONE = 0,			/* 0 表示按键事件 */
    
    	KEY_0_DOWN,			    /* 0键按下 */
    	KEY_0_UP,				/* 0键弹起 */
    	KEY_0_LONG,			    /* 0键长按 */
    	
    	KEY_1_DOWN,				/* 1键按下 */
    	KEY_1_UP,				/* 1键弹起 */
    	KEY_1_LONG,				/* 1键长按 */
    
    	KEY_2_DOWN,				/* 2键按下 */
    	KEY_2_UP,				/* 2键弹起 */
    	KEY_2_LONG,				/* 2键长按 */
    
    	KEY_3_DOWN,				/* 3键按下 */
    	KEY_3_UP,				/* 3键弹起 */
    	KEY_3_LONG,				/* 3键长按 */
    
    	KEY_4_DOWN,				/* 4键按下 */
    	KEY_4_UP,				/* 4键弹起 */
    	KEY_4_LONG,				/* 4键长按 */
    
    	KEY_5_DOWN,				/* 5键按下 */
    	KEY_5_UP,				/* 5键弹起 */
    	KEY_5_LONG,				/* 5键长按 */
    
    	KEY_6_DOWN,				/* 6键按下 */
    	KEY_6_UP,				/* 6键弹起 */
    	KEY_6_LONG,				/* 6键长按 */
    
    	KEY_7_DOWN,				/* 7键按下 */
    	KEY_7_UP,				/* 7键弹起 */
    	KEY_7_LONG,				/* 7键长按 */
    
    	KEY_8_DOWN,				/* 8键按下 */
    	KEY_8_UP,				/* 8键弹起 */
    	KEY_8_LONG,				/* 8键长按 */
    
    	KEY_9_DOWN,				/* 9键按下 */
    	KEY_9_UP,				/* 9键弹起 */
    	KEY_9_LONG,				/* 9键长按 */
    
    	KEY_10_DOWN,			/* 10键按下 */
    	KEY_10_UP,				/* 10键弹起 */
    	KEY_10_LONG,			/* 10键长按 */
    	
    	KEY_11_DOWN,			/* 11键按下 */
    	KEY_11_UP,				/* 11键弹起 */
    	KEY_11_LONG,			/* 11键长按 */
    	
    	KEY_12_DOWN,			/* 12键按下 */
    	KEY_12_UP,				/* 12键弹起 */
    	KEY_12_LONG,			/* 12键长按 */
    }KEY_ENUM;
    
    /* 按键FIFO用到变量 */
    #define FIFO_SIZE	15
    
    /*! fifo缓冲区类型 */
    #define FIFO_TYPE    uint8_t
    
    /*! fifo缓冲区满后 是否覆盖旧数据 0进行覆盖  1报错入栈失败*/
    #define FIFO_COVER   0
    typedef struct
    {
        FIFO_TYPE  buff[FIFO_SIZE];       /* FIFO 缓冲区 */
    
        uint32_t   fifoLen;               /* FIFO 缓冲区有效数据长度 */
    
        uint32_t   fifoWrite;             /* 缓冲区写指针 */
        uint32_t   fifoRead;              /* 缓冲区读指针 */
    
    }fifo_t;
    
    typedef fifo_t KEY_FIFO_T;
    
    /* 供外部调用的函数声明 */
    void bsp_InitKey(void);
    void bsp_KeyScan(void);
    void bsp_PutKey(uint8_t _KeyCode);
    uint8_t bsp_GetKey(void);
    uint8_t bsp_GetKey2(void);
    uint8_t bsp_GetKeyState(KEY_ID_E _ucKeyID);
    void bsp_SetKeyParam(uint8_t _ucKeyID, uint16_t _LongTime, uint8_t  _RepeatSpeed);
    void bsp_ClearKey(void);
    
    
    
    
    #endif
    

    bsp_key.c

    /*!
      * @file     BSP_KEY.c
      *
      * @brief    按键驱动文件
      *
      * @company  
      *
      * @author   不咸不要钱
      *
      * @note     无
      *
      * @version  
      *
      * @date     2019/10/18 星期五
      */ 
    #include "bsp_key.h"
    #include "lq_gpio.h"
    #include "stdio.h"
    
    
    
    static KEY_T xdata s_tBtn[KEY_COUNT];
    static KEY_FIFO_T xdata s_tKey;		/* 按键FIFO变量,结构体 */
    
    static void bsp_InitKeyVar(void);
    static void bsp_InitKeyHard(void);
    static void bsp_DetectKey(uint8_t i);
    
    static uint8_t IsKeyDown0(void)  {P(C1_GPIO_PORT, C1_PIN) = 1;  P(C2_GPIO_PORT, C2_PIN) = 1;  P(C3_GPIO_PORT, C3_PIN) = 1; P(C4_GPIO_PORT, C4_PIN) = 0; if (P(R2_GPIO_PORT, R2_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown1(void)  {P(C1_GPIO_PORT, C1_PIN) = 0;  P(C2_GPIO_PORT, C2_PIN) = 1;  P(C3_GPIO_PORT, C3_PIN) = 1; P(C4_GPIO_PORT, C4_PIN) = 1; NOP(50); if (P(R1_GPIO_PORT, R1_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown2(void)  {P(C1_GPIO_PORT, C1_PIN) = 0;  P(C2_GPIO_PORT, C2_PIN) = 1;  P(C3_GPIO_PORT, C3_PIN) = 1; P(C4_GPIO_PORT, C4_PIN) = 1; if (P(R2_GPIO_PORT, R2_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown3(void)  {P(C1_GPIO_PORT, C1_PIN) = 0;  P(C2_GPIO_PORT, C2_PIN) = 1;  P(C3_GPIO_PORT, C3_PIN) = 1; P(C4_GPIO_PORT, C4_PIN) = 1; if (P(R3_GPIO_PORT, R3_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown4(void)  {P(C1_GPIO_PORT, C1_PIN) = 1;  P(C2_GPIO_PORT, C2_PIN) = 0;  P(C3_GPIO_PORT, C3_PIN) = 1; P(C4_GPIO_PORT, C4_PIN) = 1; if (P(R1_GPIO_PORT, R1_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown5(void)  {P(C1_GPIO_PORT, C1_PIN) = 1;  P(C2_GPIO_PORT, C2_PIN) = 0;  P(C3_GPIO_PORT, C3_PIN) = 1; P(C4_GPIO_PORT, C4_PIN) = 1; if (P(R2_GPIO_PORT, R2_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown6(void)  {P(C1_GPIO_PORT, C1_PIN) = 1;  P(C2_GPIO_PORT, C2_PIN) = 0;  P(C3_GPIO_PORT, C3_PIN) = 1; P(C4_GPIO_PORT, C4_PIN) = 1; if (P(R3_GPIO_PORT, R3_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown7(void)  {P(C1_GPIO_PORT, C1_PIN) = 1;  P(C2_GPIO_PORT, C2_PIN) = 1;  P(C3_GPIO_PORT, C3_PIN) = 0; P(C4_GPIO_PORT, C4_PIN) = 1; if (P(R1_GPIO_PORT, R1_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown8(void)  {P(C1_GPIO_PORT, C1_PIN) = 1;  P(C2_GPIO_PORT, C2_PIN) = 1;  P(C3_GPIO_PORT, C3_PIN) = 0; P(C4_GPIO_PORT, C4_PIN) = 1; if (P(R2_GPIO_PORT, R2_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown9(void)  {P(C1_GPIO_PORT, C1_PIN) = 1;  P(C2_GPIO_PORT, C2_PIN) = 1;  P(C3_GPIO_PORT, C3_PIN) = 0; P(C4_GPIO_PORT, C4_PIN) = 1; if (P(R3_GPIO_PORT, R3_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown10(void) {P(C1_GPIO_PORT, C1_PIN) = 1;  P(C2_GPIO_PORT, C2_PIN) = 1;  P(C3_GPIO_PORT, C3_PIN) = 1; P(C4_GPIO_PORT, C4_PIN) = 0; NOP(50); if (P(R1_GPIO_PORT, R1_PIN) == 0) return 1;else return 0;}
    static uint8_t IsKeyDown11(void) {P(C1_GPIO_PORT, C1_PIN) = 1;  P(C2_GPIO_PORT, C2_PIN) = 1;  P(C3_GPIO_PORT, C3_PIN) = 1; P(C4_GPIO_PORT, C4_PIN) = 0; if (P(R3_GPIO_PORT, R3_PIN) == 0) return 1;else return 0;}
    
    /* 组合按键 key1 && key2 */
    static uint8_t IsKeyDown12(void)  { return IsKeyDown1() && IsKeyDown2();}
    
    /*!
     * @brief    fifo初始化
     *
     * @param    fifo_t    :  FIFO
     *
     * @return   无
     *
     * @note     无
     *
     * @see      fifo_t  tempFifo;
     *           fifo_init(tempFifo);  //fifo初始化
     *
     * @date     2020/5/21
     */
    void fifo_init(fifo_t *fifo)
    {
        fifo->fifoLen   = 0;
        fifo->fifoRead  = 0;
        fifo->fifoWrite = 0;
    }
    
    
    /*!
     * @brief    fifo压入数据
     *
     * @param    fifo_t    :  FIFO
     * @param    data      :  入栈数据
     *
     * @return   0 :成功   1 :失败
     *
     * @note     FIFO_COVER 宏定义进行判断缓冲区满后的操作
     *
     * @see      fifo_t  tempFifo;
     *           fifo_push(tempFifo, 120);  //fifo中压入一个数据
     *
     * @date     2020/5/21
     */
    uint8_t fifo_push(fifo_t *fifo, FIFO_TYPE dat)
    {
        fifo->fifoLen++;
    
        /* 判断缓冲区是否已满 */
        if(fifo->fifoLen > FIFO_SIZE)
        {
            fifo->fifoLen = FIFO_SIZE;
    
    #if FIFO_COVER
            return 1;
    #else
            if(++fifo->fifoRead >= FIFO_SIZE)
            {
                fifo->fifoRead = 0;
            }
    #endif
        }
    
        fifo->buff[fifo->fifoWrite] = dat;
    
        if(++fifo->fifoWrite >= FIFO_SIZE)
        {
            fifo->fifoWrite = 0;
        }
    
        return 0;
    
    }
    
    /*!
     * @brief    fifo弹出数据
     *
     * @param    fifo_t    :  FIFO
     * @param    data      :  出栈数据
     *
     * @return   0 :成功   1 :失败
     *
     * @note     无
     *
     * @see      fifo_t  tempFifo;
     *           FIFO_TYPE tempData;
     *           fifo_push(tempFifo, 120);       //fifo中压入一个数据
     *           fifo_pop(tempFifo, &tempData);  //fifo中弹出一个数据
     *
     * @date     2020/5/21
     */
    uint8_t fifo_pop(fifo_t *fifo, FIFO_TYPE *dat)
    {
        /* 缓冲区为空 */
        if(fifo->fifoLen == 0)
        {
            return 1;
        }
    
        fifo->fifoLen--;
    
        *dat = fifo->buff[fifo->fifoRead];
    
        if(++fifo->fifoRead >= FIFO_SIZE)
        {
            fifo->fifoRead = 0;
        }
    
        return 0;
    }
    
    /*
    *********************************************************************************************************
    *	函 数 名: bsp_InitKey
    *	功能说明: 初始化按键. 该函数被 bsp_Init() 调用。
    *	形    参:  无
    *	返 回 值: 无
    *********************************************************************************************************
    */
    void bsp_InitKey(void)
    {
    	bsp_InitKeyVar();		/* 初始化按键变量 */
    	bsp_InitKeyHard();		/* 初始化按键硬件 */
    }
    
    
    /*
    *********************************************************************************************************
    *	函 数 名: bsp_PutKey
    *	功能说明: 将1个键值压入按键FIFO缓冲区。可用于模拟一个按键。
    *	形    参:  _KeyCode : 按键代码
    *	返 回 值: 无
    *********************************************************************************************************
    */
    void bsp_PutKey(uint8_t _KeyCode)
    {
    	fifo_push(&s_tKey, _KeyCode);
    }
    
    
    /*
    *********************************************************************************************************
    *	函 数 名: bsp_GetKey
    *	功能说明: 从按键FIFO缓冲区读取一个键值。
    *	形    参:  无
    *	返 回 值: 按键代码
    *********************************************************************************************************
    */
    uint8_t bsp_GetKey(void)
    {
    	uint8_t xdata ret;
    
    	if (fifo_pop(&s_tKey, &ret) == 1)
    	{
    		return KEY_NONE;
    	}
    	else
    	{
    		return ret;
    	}
    }
    
    
    /*
    *********************************************************************************************************
    *	函 数 名: bsp_GetKeyState
    *	功能说明: 读取按键的状态
    *	形    参:  _ucKeyID : 按键ID,从0开始
    *	返 回 值: 1 表示按下, 0 表示未按下
    *********************************************************************************************************
    */
    uint8_t bsp_GetKeyState(KEY_ID_E _ucKeyID)
    {
    	return s_tBtn[_ucKeyID].State;
    }
    
    
    /*
    *********************************************************************************************************
    *	函 数 名: bsp_SetKeyParam
    *	功能说明: 设置按键参数
    *	形    参:_ucKeyID : 按键ID,从0开始
    *			_LongTime : 长按事件时间
    *			 _RepeatSpeed : 连发速度
    *	返 回 值: 无
    *********************************************************************************************************
    */
    void bsp_SetKeyParam(uint8_t _ucKeyID, uint16_t _LongTime, uint8_t  _RepeatSpeed)
    {
    	s_tBtn[_ucKeyID].LongTime = _LongTime;			/* 长按时间 0 表示不检测长按键事件 */
    	s_tBtn[_ucKeyID].RepeatSpeed = _RepeatSpeed;			/* 按键连发的速度,0表示不支持连发 */
    	s_tBtn[_ucKeyID].RepeatCount = 0;						/* 连发计数器 */
    }
    
    /*
    *********************************************************************************************************
    *	函 数 名: bsp_ClearKey
    *	功能说明: 清空按键FIFO缓冲区
    *	形    参:无
    *	返 回 值: 按键代码
    *********************************************************************************************************
    */
    void bsp_ClearKey(void)
    {
    	s_tKey.fifoRead = s_tKey.fifoWrite;
    }
    
    
    /*
    *********************************************************************************************************
    *	函 数 名: bsp_InitKeyHard
    *	功能说明: 配置按键对应的GPIO
    *	形    参:  无
    *	返 回 值: 无
    *********************************************************************************************************
    */
    static void bsp_InitKeyHard(void)
    {
    	//KEY 初始化
        PIN_InitPushPull(C1_GPIO_PORT, C1_PIN);
    	PIN_InitPushPull(C2_GPIO_PORT, C2_PIN);
    	PIN_InitPushPull(C3_GPIO_PORT, C3_PIN);
    	PIN_InitPushPull(C4_GPIO_PORT, C4_PIN);
    	PIN_InitOpenDrain(R1_GPIO_PORT, R1_PIN);
    	PIN_InitOpenDrain(R2_GPIO_PORT, R2_PIN);
    	PIN_InitOpenDrain(R3_GPIO_PORT, R3_PIN);
    	
    }
    
    
    /*
    *********************************************************************************************************
    *	函 数 名: bsp_InitKeyVar
    *	功能说明: 初始化按键变量
    *	形    参:  无
    *	返 回 值: 无
    *********************************************************************************************************
    */
    static void bsp_InitKeyVar(void)
    {
    	uint8_t xdata i;
    
    	/* 对按键FIFO读写指针清零 */
    	fifo_init(&s_tKey);
    	
    	/* 给每个按键结构体成员变量赋一组缺省值 */
    	for (i = 0; i < KEY_COUNT; i++)
    	{
    		s_tBtn[i].LongTime = KEY_LONG_TIME;			/* 长按时间 0 表示不检测长按键事件 */
    		s_tBtn[i].Count = KEY_FILTER_TIME / 2;		/* 计数器设置为滤波时间的一半 */
    		s_tBtn[i].State = 0;							/* 按键缺省状态,0为未按下 */
    		//s_tBtn[i].KeyCodeDown = 3 * i + 1;				/* 按键按下的键值代码 */
    		//s_tBtn[i].KeyCodeUp   = 3 * i + 2;				/* 按键弹起的键值代码 */
    		//s_tBtn[i].KeyCodeLong = 3 * i + 3;				/* 按键被持续按下的键值代码 */
    		s_tBtn[i].RepeatSpeed = 0;						/* 按键连发的速度,0表示不支持连发 */
    		s_tBtn[i].RepeatCount = 0;						/* 连发计数器 */
    	}
    
    
    	/* 判断按键按下的函数 */
    	s_tBtn[0].IsKeyDownFunc = IsKeyDown0;
    	s_tBtn[1].IsKeyDownFunc = IsKeyDown1;
    	s_tBtn[2].IsKeyDownFunc = IsKeyDown2;
    	s_tBtn[3].IsKeyDownFunc = IsKeyDown3;
    	s_tBtn[4].IsKeyDownFunc = IsKeyDown4;
    	s_tBtn[5].IsKeyDownFunc = IsKeyDown5;
    	s_tBtn[6].IsKeyDownFunc = IsKeyDown6;
    	s_tBtn[7].IsKeyDownFunc = IsKeyDown7;
    	s_tBtn[8].IsKeyDownFunc = IsKeyDown8;
    	s_tBtn[9].IsKeyDownFunc = IsKeyDown9;
    	s_tBtn[10].IsKeyDownFunc = IsKeyDown10;
    	s_tBtn[11].IsKeyDownFunc = IsKeyDown11;
    	
    	/* 组合按键 */
    	s_tBtn[12].IsKeyDownFunc = IsKeyDown12;
    }
    
    /*
    *********************************************************************************************************
    *	函 数 名: bsp_DetectKey
    *	功能说明: 检测一个按键。非阻塞状态,必须被周期性的调用。
    *	形    参:  按键结构变量指针
    *	返 回 值: 无
    *********************************************************************************************************
    */
    static void bsp_DetectKey(uint8_t i)
    {
    	KEY_T xdata *pBtn;
    
    	/*
    		如果没有初始化按键函数,则报错
    		if (s_tBtn[i].IsKeyDownFunc == 0)
    		{
    			printf("Fault : DetectButton(), s_tBtn[i].IsKeyDownFunc undefine");
    		}
    	*/
    
    	pBtn = &s_tBtn[i];
    	if (pBtn->IsKeyDownFunc())
    	{
    		if (pBtn->Count < KEY_FILTER_TIME)
    		{
    			pBtn->Count = KEY_FILTER_TIME;
    		}
    		else if(pBtn->Count < 2 * KEY_FILTER_TIME)
    		{
    			pBtn->Count++;
    		}
    		else
    		{
    			if (pBtn->State == 0)
    			{
    				pBtn->State = 1;
    
    				/* 发送按钮按下的消息 */
    				bsp_PutKey((uint8_t)(3 * i + 1));
    				P27 = 1;                   //开启蜂鸣器
    			}
    
    			if (pBtn->LongTime > 0)
    			{
    				if (pBtn->LongCount < pBtn->LongTime)
    				{
    					/* 发送按钮持续按下的消息 */
    					if (++pBtn->LongCount == pBtn->LongTime)
    					{
    						/* 键值放入按键FIFO */
    						bsp_PutKey((uint8_t)(3 * i + 3));
    					}
    				}
    				else
    				{
    					if (pBtn->RepeatSpeed > 0)
    					{
    						if (++pBtn->RepeatCount >= pBtn->RepeatSpeed)
    						{
    							pBtn->RepeatCount = 0;
    							/* 常按键后,每隔10ms发送1个按键 */
    							bsp_PutKey((uint8_t)(3 * i + 1));
    						}
    					}
    				}
    			}
    		}
    	}
    	else
    	{
    		if(pBtn->Count > KEY_FILTER_TIME)
    		{
    			pBtn->Count = KEY_FILTER_TIME;
    		}
    		else if(pBtn->Count != 0)
    		{
    			pBtn->Count--;
    		}
    		else
    		{
    			if (pBtn->State == 1)
    			{
    				pBtn->State = 0;
    
    				/* 发送按钮弹起的消息 */
    				bsp_PutKey((uint8_t)(3 * i + 2));
    				P27 = 0;                   //关闭蜂鸣器
    			}
    		}
    
    		pBtn->LongCount = 0;
    		pBtn->RepeatCount = 0;
    	}
    }
    
    /*
    *********************************************************************************************************
    *	函 数 名: bsp_KeyScan
    *	功能说明: 扫描所有按键。非阻塞,被systick中断周期性的调用
    *	形    参:  无
    *	返 回 值: 无
    *********************************************************************************************************
    */
    void bsp_KeyScan(void)
    {
    	uint8_t xdata i;
    
    	for (i = 0; i < KEY_COUNT; i++)
    	{
    		bsp_DetectKey(i);
    	}
    }
    
    

    思考

    这个按键底层驱动可以检测到底层按下弹起,是否支持双击?
    双击实现可以基于此底层驱动,不需要修改代码,只需要在修改应用层,比如检测到某按键按下,开启一个软件定时器,在定时器定时结束前,再次检测到按下,则为双击。

    展开全文
  • 51单片机开发系列五_矩阵按键扫描

    千次阅读 2014-05-05 13:54:39
    51单片机开发系列五 ...在嵌入式系统中,用的最多的输入设备就是按键,用户的应用需求可通过相应按键传递到系统软件中,软件转而完成用户请求,实现简单的人机交互。笔者此处就矩阵按键的实现作一个简单的介绍。

    51单片机开发系列五

    矩阵按键扫描

    象棋小子    1048272975

    在嵌入式系统中,用的最多的输入设备就是按键,用户的应用需求可通过相应按键传递到系统软件中,软件转而完成用户请求,实现简单的人机交互。笔者此处就矩阵按键的实现作一个简单的介绍。

    1. 按键输入概述

    按键是一种常开型按钮开关,平时键的二个触点处于断开状态,按下键时它们才闭合。按键控制电路就是用来实时监视按键,当有键接下时,电路监控中的输入引脚电平发生变化,检测到这种变化后,控制电路进行按键扫描,定位按键的位置,并把相关的按键信息反馈回上一层应用中。常见的按键输入设计有独立式按键,矩阵式按键。独立式按键每个键占用一个IO口,电路配置灵活,软件简单,但按键较多时,IO口浪费大。矩阵式按键适用于按键数量较多的场合,由行线和列线组成,按键位于行列的交叉点上。节省IO口。通常按键控制电路通过查询方式或中断方式去检测按键的输入,查询方式需占用一定的cpu资源,查询频率太低可能造成按键输入丢失,太高浪费cpu资源,通常按键查询频率约50HZ较合适。中断方式需占用cpu一路外部中断,但不会占用cpu资源,只要有按键按下时,cpu即可马上检测到输入,进行扫描并得到按键值。

    2. 硬件设计

    笔者此处采用4x4的矩阵按键设计,当然,矩阵键盘可通过四个肖特基二极管构成四输入的与门(可参考笔者这篇文章<浅谈小信号肖特基二极管在数字电路中的应用>),连接到单片机的外部中断引脚,从而实现中断方式检测按键输入。为兼容目前开发板常见的矩阵按键设计,笔者把4x4的矩阵按键接口接在P1口,通过查询方式检测按键输入。


    图2-1 4x4矩阵按键

    3. 驱动实现

    由于我们采用的是查询方式按键设计,因此单片机需一定的频率去扫描P1口的按键,通常这个频率约50HZ较合适,为保证这个扫描频率,通常是通过定时器产生时标周期性进行执行扫描。P1.4~P1.7列线通过上拉电阻接到VCC上,P1.0~P1.3行线产生相应的扫描信号,无按键,列线处于高电平状态,有键按下,列线电平状态将由与此列线相连的行线电平决定。行线电平为低,则列线电平为低,行线电平为高,则列线电平为高。

    按键扫描函数如下,该函数需周期执行,以扫描按键的状态。以51单片机为例,P1.0~P1.3逐行输出扫描信号,在Key.h模块头文件实现接口宏KeyOutputSelect()

    #define KeyOutputSelect(Select) {P1 = ~(1<<(Select));}

    输出扫描线后,需要读取对应扫描线的按键状态(P1.4~P1.7),同样在Key.h模块头文件实现引脚状态读取接口宏KeyGetPinState()

    #define KeyGetPinState()        (P1>> 4)

    读取了对应扫描线下的按键引脚状态,就需判断哪些引脚电平为0(按下),对读到的引脚状态进行取反转换成对引脚状态变量进行搜1算法,得到键值的速度能达到最快,并且多个按键同时按下时也能够正确得到优先级最高的按键。按键有效按下会得到0~15的键值,无按键按下时得到键值16。

    voidKeyScan()

    {

        unsigned char i;

        unsigned char KeyValue;

        unsigned char PinState;

        if (KeyState.State == STATE_DISABLE) {

            return; // 按键禁用时,不对键盘进行扫描

        }

    // 键值为0~15,未按键键值为16,任意多的键按下均能

    // 正确返回优先级最高的键值

        KeyValue = 0;

        for (i=0; i<4; i++) {

            KeyOutputSelect(i); // 输出扫描线

            // 得到对应扫描线时的按键状态

            PinState = KeyGetPinState();

            // 有键按下时,PinState中有0的位置即为键值位置

            PinState = ~PinState;

    // 搜索Pinstate第一个为1的位                   

            if (!(PinState & 0xf)) {           

                KeyValue += 4;             

                continue; // 该扫描线没有按键按下,进入下一扫描线                       

            }

            // 该扫描线有键按下,对半进行检索1的位置                            

            if (!(PinState & 0x3)) {           

                KeyValue += 2; // 低2位(P1.4~P1.5)没有按下             

                PinState >>= 2; // 移位检索(P1.6~P1.7)             

            }                              

            if (!(PinState & 0x1)){           

                KeyValue += 1;             

            }

            break; // 有鍵按下,退出继续扫描  

        }

     

        KeyStore(KeyValue); // 保存按键状态  

    }

    得到了按键值后,我们需要对按键值进行处理并根据按键状态把可能产生的按键消息保存进缓冲区中,以便用户程序读取处理。按键通常有按下、松手、长按这几个状态,需要支持按下检测、松手检测、长按、连击的功能,并且需要对按键进行去抖滤波。按键的状态往往会在这几种情况进行切换,因此,对按键进行状态机编程是相当清晰的思路。我们在KeyStore()函数中实现对按键状态的转移判断,在模块中我们通过按键状态结构变量KeyState来跟踪记录按键的状态

    typedef struct {

        unsigned char State; // 按键的各个状态转移

        unsigned int TimeCount;  // 用来跟踪各个状态的计时

    } KEY_STATE;

    static KEY_STATE KeyState; // 按键状态机状态转移

    检测到相应的按键事件后(KEY_UP、KEY_DOWN、KEY_LONG),需产生相应的按键消息保存进按键缓存区,通常可以开辟一个按键队列缓存,以便保存多个产生的按键消息,不会因用户代码未能及时处理按键而造成按键丢失,笔者此处为避免复杂,以一个按键缓冲为例,按键事件结构变量KeyBuffer用来保存按键消息

    typedef struct {

        unsigned char Value;

        unsigned char State;

    } KEY_EVENT;

    // 按键扫描得到的键值存放在KeyBuffer中,包含键值及键状态

    static volatile KEY_EVENT KeyBuffer;

    按键消抖以及长按均是需要以时间为判断标准,我们在模块中定义消抖时间以及长按时间判决以及相应的状态宏

    // 按键的扫描周期为20ms

    #define WOBBLE_COUNT    1  // 按键消抖计数,1个按键扫描周期(20ms)

    #define LONG_COUNT      100 // 长按100个扫描周期判断为长按(2S)

     

    #define STATE_INIT         0x0 // 按键初始化状态

    #define STATE_WOBBLE       0x1 // 按键消抖状态

    #define STATE_LONG          0x2 // 按键长按检测状态

    #define STATE_RELEASE      0x3 // 按键释放状态

    #define STATE_DISABLE      0x4 // 按键禁用状态

    完整的KeyStore()函数实现如下

    static voidKeyStore(unsigned char Value)

    {

        static unsigned char LastValue;

        switch (KeyState.State) {

        case STATE_INIT: // 初始状等待按键

            if (Value < KEY_NULL) {

            // 记录下按下的键并进入消抖状态

                LastValue = Value;

                KeyState.TimeCount = WOBBLE_COUNT -1;

                KeyState.State = STATE_WOBBLE;

            }

            break;

        case STATE_WOBBLE:

            if (KeyState.TimeCount) {

                KeyState.TimeCount--; // 消抖计时未到

                break;

            }

            // 消抖后再次判断为同一键值则认为键按下保存键值

            // 并进入到长按检测态中,否则认为干扰,回到初始态

            if (Value ==LastValue) {

                KeyBuffer.Value = LastValue;

                KeyBuffer.State = KEY_DOWN;

                KeyState.TimeCount = LONG_COUNT - 1;

                KeyState.State = STATE_LONG;   

            } else {

                KeyState.State = STATE_INIT;

            }

            break;

        case STATE_LONG:

            if (Value == LastValue) {

                if(KeyState.TimeCount) {

                    KeyState.TimeCount--; // 长按计时未到

                    break;

                }

                // 长按确定后,保存长按的键值,并循环长按计时

                KeyBuffer.Value = LastValue;

                KeyBuffer.State = KEY_LONG;

                KeyState.TimeCount = LONG_COUNT - 1;           

            } else {

                // 长按时按键改变则认为长按的键释放,

                // 进入释放按键态

                KeyState.State =STATE_RELEASE;

            }

            break;

        case STATE_RELEASE:

            // 保存按键弹起的键值,并返回到初始化状态

            KeyBuffer.Value= LastValue;

            KeyBuffer.State = KEY_UP;

            KeyState.State = STATE_INIT;

            break;

        default:

            KeyState.State = STATE_INIT;

            break;

        }

    }

    按键扫描可以作为一个任务,需大约20ms进行执行扫描一次,笔者此处例程用数码管显示相应的按键值,数码管扫描函数需2ms进行一位的扫描,因此数码管扫描也可当作一个任务,另外还需一个按键处理任务用来处理按键扫描任务得到按键消息。安排一定量的任务以及保证实时是需要一定的编程模式进行程序设计。笔者此处介绍一种分时处理的思想,这种思想就是用定时器重复进行一定间隔的计时,产生时间片,任务根据这个时标来确定运行、挂起、超时等情况。对于抢占式操作系统如ucos、Linux等操作系统来说,这个定时器对于操作系统就如同人的心脏,操作系统在每个SystemTick会进行各种状态的处理,如任务的延时时间到、任务超时、有高优先级的任务需运行进行上下文切换抢占等。8位单片机运行像ucos这样的很小型操作系统也几乎没有实际意义,因为单操作系统就占用了8位单片机的大部分资源。但我们仍然可以用时间分片的思想来设计我们的8位单片机系统。对于一般任务,我们可以在定时器中断服务程序中判断任务是否需运行,实际的代码执行在完成定时器中断服务后进行调度,这可称为合作型任务,即一旦这个任何运行,只有退出了,其它合作型任务才能得到运行,因为合作型任务是不可抢占的,如果一个任务设计不好,如用了软件延时Delay_ms()让cpu空等浪费,将造成整个合作型任务均没有实时性。确实耗时长的任务应分状态,细分成小任务,保证任务的实时。对于一些需强实时的任务,比如必须精确地在某一时间点执行的任务,可以在定时器中断中执行,此时的任务是抢占型任务,即可打断合作型任务,执行完自身后再中断返回继续执行合作型任务。抢占型任务可能出现的问题就是会同时访问共用的资源,而这一资源往往只能被一个任务独占,造成访问冲突,例如合作型任务正在往串口发送数据”1234”,但发送到”1”时,这时定时器中断来了,执行抢占式任务,抢占式任务也要访问串口发送数据”abcd”,这时在PC机上接收到的串口数据就为”1abcd234”,造成了错误。因此对于抢占型操作系统,访问独占资源,都是需要关调度器或加锁的方法进行访问。

    笔者为了让读者有一个认识,把数码管扫描任务及按键扫描任务当作抢占式任务,按键处理任务当作合作型任务。当然,设计一个调度器根据需求是需要实现不同的功能函数的,如调度器数据结构、初始化函数,定时器中断服务程序,调度器增加任务函数,调度器执行任务函数,调度器删除任务函数等。为避免复杂,笔者实现定时器中断服务函数,每个任务的数据结构只有两个任务变量TaskPeriod以及TaskRun,其中TaskPeriod变量用来时标计数,是必须的,TaskRun为任务执行标志,不为0时表明任务需调度执行,因此实际每个任务只需时标计数变量即可。

     

    // 按键处理任务执行周期及执行标记

    staticvolatile unsigned char DoKeyRun = 0;

    staticvolatile unsigned char DoKeyPeriod = 0;

     

    // 定时器2ms中断处理作为时标

    voidT0_Interrupt() interrupt 1

    {

        static unsigned char KeyScanPeriod = 0;

        TH0 = (65536-2000) / 256;

        TL0 = (65536-2000) % 256;

        DigitalTube_Scan(); // 刷新数码管

        KeyScanPeriod++; // 按键扫描时标计数

        if (KeyScanPeriod >= 10) { // 每隔20ms进行按键扫描

            KeyScanPeriod = 0;

            KeyScan();

        }

        DoKeyPeriod++; // 按键处理时标计数

        if (DoKeyPeriod >= 11) { // 每隔22ms进行按键处理

            DoKeyPeriod = 0;

            DoKeyRun++;

        }

    }

    每隔2ms执行实时任务DigitalTube_Scan()以及每隔20ms执行KeyScan(),而对于按键处理任务DoKey()需在按键扫描可能得到键值后才需调度处理,设为22ms执行处理一次即可,在定时器中按键处理任务的时标计数到了22ms后,才设置相关的执行计数标志DoKeyRun,告知调度器需执行按键任务。

        while(1) {

            if (DoKeyRun > 0) {

                DoKeyRun = 0;

                DoKey();

            }

            // 加入睡眠函数,节省功耗

        }

    调度器简单的判断按键处理任务的执行标志,若需执行,则调用DoKey()执行任务。所有任务执行完后,此时调试器已经无事可做,一般让cpu进入休眠,可以极大的节省功耗,如这个例程,单片机任务并不重,估计90%的时间都在休眠。下一个时间片到来后会唤醒cpu执行定时器中断程序并如此重复任务的调度。

    其它任务通过KeyGetValue()函数获得按键扫描的值及按键状态(KEY_UP、KEY_DOWN、KEY_LONG),抢占式任务KeyScan()产生按键消息到缓存区,在任务中调用KeyGetValue()访问按键缓存区时,应先关中断禁止调度,防止访问临界区时发生中断,执行抢占式任务KeyScan()可能同时修改按键缓冲区,KeyGetValue()访问完临界区后即可再开中断。

    unsigned charKeyGetValue(unsigned char *pState)

    {

        unsigned char KeyValue;

        if (KeyState.State == STATE_DISABLE) {

            *pState = KEY_NULL;

            return KEY_NULL;

        }

    // 按键扫描任务放在中断中,在访问以下中断任务临界变量时,

    // 应关中断,保证不在访问这些变量时产生中断,中断扫描任务

    // 同时访问这些变量,造成冲突

        IntDisable();

        *pState = KeyBuffer.State;

        KeyValue = KeyBuffer.Value;

        KeyBuffer.State = KEY_NULL; // 按键得到后,清空缓冲区

        KeyBuffer.Value = KEY_NULL;

        IntEnable(); // 访问完临界变量后再开中断

     

        return KeyValue;

    }

    在例程中,DoKey()任务为简单的得到键值并在数码管进行显示,按键长按2s会进行长按计数,完整代码见文章给出的链接,包括完整的Keil源码及Proteus仿真工程。

    void DoKey()

    {

        unsigned char KeyState;

        unsigned char KeyValue;

        unsigned char *pBuffer;

        // 获得数码管显存,以作更新数据显示

        pBuffer = DigitalTube_GetBuffer();

        KeyValue = KeyGetValue(&KeyState);

        switch (KeyState) {

        case KEY_UP:

            // 按键15按下后禁用键盘

            if (KeyValue == 15) {

                pBuffer[0] = 11;

                pBuffer[1] = 11;

                pBuffer[2] = 11;

                pBuffer[3] = 11;               

                KeyDisable();

            } else {                       

                pBuffer[2] = KeyValue / 10;

                pBuffer[3] = KeyValue % 10;

                pBuffer[0] = 10;

            }

            break;

        case KEY_LONG: // 每隔2s长按处理

            if (pBuffer[0] < 9) {

                pBuffer[0]++;

            } else {

                pBuffer[0] = 0;

            }

            break;

        default:

            break;

        }                  

    }

    4. 附录

    本章节Keil工程源码及Proteus仿真,可直接验证效果。Keys.rar包含矩阵按键模块源码Keys.c/Keys.h、数码管模块源码DigitalTube.c/DigitalTube.h以及示例源码main.c,可供下载学习。

    http://pan.baidu.com/s/1pJNQf15

     

    展开全文
  • 论嵌入式单片机软件架构

    千次阅读 多人点赞 2019-01-25 22:20:55
    我先把总结出的关键字写出来吧:**流水式、中断前后台、任务式、状态机、分层式,操作系统多任务**;这些就是我总结出的关键字,写这篇博客我决定不参照其他博客,只把我心中所想以及以前的代码示例或者记录翻出来,...
  • 单片机模块化一:按键思考

    千次阅读 2019-04-03 11:13:35
    从事单片机工作有几年了,一直想做一个系列总结,正好赶上今天下雨,俗话说:下雨天,宅家... 按键单片机系统中最常用的一个东东,简单人机交互界面基本都有按键存在。犹记得刚开始接触单片机时候,读取一个按键IO...
  • 单片机应用系统的基本组成

    千次阅读 2016-11-21 12:50:11
    单片机在其应用系统中所处的位置及功能来看,一个单片机应用系统不外乎以下几个部分:前向通道、后向通道、中央控制器、人机交互通道、信息交互通道。前向通道用于获取各种信息;后向通道用于输出控制作用;中央...
  • 如何在51单片机实现闹钟功能

    千次阅读 2020-03-27 23:11:02
    51单片机制作万年历过程中的闹钟部分,主要说明设计算法,软件特性可以在proteus上仿真。闹钟是人机交互的一部分,因此闹钟的实现与具体的人机交互方式息息相关,本系统采用4x4矩阵键盘作为人间交互的接口。
  •  在单片机系统中,当输入按键较多时,在硬件设计和软件编程之间总存在着矛盾。对于不同的单片机系统需要进行专用的键盘硬件设计和编程调试,通用性差,使项目开发复杂化。标准PC键盘在工艺与技术上都已相当成熟,...
  • 一个简单交互界面的实现

    万次阅读 2018-01-05 15:55:11
    这个界面本来是今年做NXP智能车的时候为了更改参数方便设计的,这其实又是一套用状态机的思想来实现的一个显示框架。 这个显示界面只是一个操作逻辑,所以它的的移植性还是挺高的,虽然本来设计的时候只是在OLED...
  • Android与单片机的信息传输方案

    千次阅读 2019-03-18 08:29:39
    本篇文章将围绕App与单片机的蓝牙通信来说说lz最近进行开发的一些方案与思考 此文分为三部分: 单片机的PWM与串口通信 Android的蓝牙开发 单片机与Android App的通信方案 环境 单片机 单片机:STC89C52 c语言...
  • 基于51单片机的RFID智能门禁系统

    千次阅读 多人点赞 2019-04-20 22:47:57
    1.概述 (1)本设计采用STC89C52作为主控芯片,专用读卡器模块用来读射频卡的信息,当有卡进入到读卡器读卡的范围内时就会读取到相应的卡序列号,并根据得到的卡序列号做出相应的操作。...人机交互通道部分采用了...
  • 掌握单片机简单人机交互接口电路的设计方法。 进一步熟悉 C8051F020 单片机 I/O 口交叉开关设置和 I/O 端口的使用。 掌握独立式按键和 LED 显示接口电路和程序设计方法。 二、实验内容 按键(3 个)和 LED(4 个)...
  • 不用怀疑,单片机的万能语言就是状态机。还希望大家不要条件反射式的看到状态机就以为我要讲什么VHDL的东西——状态机是一种思维模式,是计算机理论的立足之本(不相信请参考清华大学出版社的《自动机理论与应用》)...
  • 第十一节:同一个按键短按与长按的区别触发。 开场白: 上一节讲了类似电脑键盘组合按键触发的功能,这节要教会大家一个知识点:如何在上一节的基础上,略作修改,就可以实现同一个按键短按与长按的区别触发。 ...
  • 基于STM32F4单片机对步进电机的控制(有代码)

    万次阅读 多人点赞 2019-01-05 11:13:08
    步进电机是将电脉冲控制信号转变为角位移或线位移的一种常用的数字控制执行元件,又称为脉冲电机。在驱动电源的作用下,步进电机受到脉冲的...因此,通过控制输入电脉冲的数目、频率及电动绕组的通电顺序就可以...
  • 做工业控制的时候,很多开发者喜欢一步一步来,第一步做什么,接着做什么,发生了某个事件执行某个动作处理,这是最简单也是最符合人的一般思维的方式,简单的控制系统这么做无疑是非常轻松...那么,状态机控制就是...
  • 按键:人机交互控制,主要用于对系统的控制,信号的释放等。因此在这里,FPGA上应用的按键消抖动,也不得不讲! 一、为什么要消抖动 如上图所示,在按键被按下的短暂一瞬间,由于硬件上的抖动,往往会产生几毫秒的...
  • 摘要:  介绍了在C 语言环境下,在LCD 液晶显示屏上实现多级嵌套菜单的一... 单片机; C 语言; LCD  中图分类号:TP311. 1 文献标识码:B  收稿日期:2005 - 11 - 21 0 引言  由于C 语言的结构性和模块化,采用C 语
1 2 3 4 5 ... 20
收藏数 505
精华内容 202
关键字:

单片机按键交互状态机