【教程】如何为单片机编写可移植性强的按键扫描程序(一)

ngHackerX86

  按键检测是单片机经常要进行的任务,不过,通常由于这一任务实在太过简单,通常大家在实现的时候并不会考虑其可移植性,导致后期任务写得越来越复杂之后,程序不易阅读且不易移植。

  所以这一次呢,笔者用STM32做例子,简单分享一点提高按键检测程序可移植性的经验。

  (PS. 部分未做讲解的方法在以前的文章中有,地址:https://segmentfault.com/a/11...

  首先看个最简单的例子,轮询检测按键的代码可能是这样的(伪·伪代码):

uint8_t Key_Scan(void)
{
    if(ANY KEY PRESS)
    {
        delay for sometime;
        if(ANY KEY PRESS)
        {
            if( KEY1 PRESS) {
                return key_value1;
            }
            else if(KEY2 PRESS) {
                return Key_value2;
            }
        }
    }
    return 0;
}

  这个检测的逻辑是没有问题的,虽然延时会浪费性能,而且不能同时检测多个按键,但是能够实现基本的检测按键是否按下的功能,代码实现大概是这个样子的:

uint8_t Key_Scan(void)
{
    if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2))
    {
        delay_ms(20);
        if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2))
        {
            if(!GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0)) {
                return 1;
            }
            else if(!GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_1)) {
                return 2;
            }
            else if(!GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_2)) {
                return 3;
            }        
        }
    }
    return 0;
}

   而有的童鞋可能会写成这个样子:

uint16_t Key_Ind_Scan()
{
    uint16_t key_value = 0;
    key_value = GPIO_ReadInputData(GPIOA)&0x0007;
    if(key_value != 0x0007)
    {
        delay_ms(20);
        if(key_value != 0x0007)
        {
            switch(key_value)
            {
                case 0x0006:
                    key_value = 1;
                    return key_value;
                case 0x0005:
                    key_value = 2;
                    return key_value;
                case 0x0003:
                    key_value = 3;
                    return key_value;
            }
        }
    }
    else 
    {
        key_value = 0;
                return key_value;
    }
    
}

  一次性读取整个端口的数据带来了一些好处,一次性获取了所有按键的状态,有助于进行组合键的判断,但是这样一是不便于阅读和理解,二来给移植造成了困难,如果所有的按键并不位于同一端口怎么办?那还需要自己读取以后把不同的数据拼在一起······对了!可以把数据拼在一起!

  我们可以这样干!

uint8_t Key_Scan(void)
{
    uint8_t key_value = 0;
    if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2))
    {
        delay_ms(20);
        if(GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0|GPIO_Pin_1|GPIO_Pin_2))
        {
            key_value += GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_0);
            key_value <<= 1;
            key_value += GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_1);
            key_value <<= 1;
            key_value += GPIO_ReadInputDataBit(GPIOA,GPIO_Pin_2);
            key_value <<= 1;    
            
            return key_value;
        }
    }
    return 0;
}

  在这个例子中,我们每读取一个按键的状态,就把key_value向左移一位,这样,扫描完成后,每个按键的状态都对应key_value的某一Bit的值,这样,通过对key_value值的判断,可以支持组合键了,而且代码也易懂。

  但从可移植性的角度来看,还是很糟糕,数据混杂在驱动程序内,如果增加按键的话还要对驱动程序进行修改。

  所以,怎么改进呢?用宏?那样你还是得写参数到驱动程序里,想想我们之前写IIC驱动的时候是怎么干的吧,对!就是那个!结构体数组。

  首先我们建立一个结构体来描述连接按键的引脚:

typedef struct {
    GPIO_TypeDef *GPIOx;
    uint16_t Pin;
}Birch_Key_Pin_T;

  然后建立一个结构体数组,把按键所连接引脚的信息存进去。

/* 记录独立按键引脚信息的结构体数组 */
static Birch_Key_Pin_T s_KeyIndPin[] = {
    {.GPIOx = GPIOA, .Pin = GPIO_Pin_0},
    {.GPIOx = GPIOA, .Pin = GPIO_Pin_1},
    {.GPIOx = GPIOA, .Pin = GPIO_Pin_2},
};

  写检测函数的实现之前先用伪代码(伪·伪代码)描述一下吧。

uint8_t Key_Scan(void)
{
    uint8_t key_value = 0;
    if(ANY KEY PRESS)
    {
        delay for sometime;
        if(ANY KEY PRESS)
        {
            for(var init;var<num_key;var++)
            {
                key_value <<= 1;
                key_value += ReadPinStatus(GPIO of Key,Pin of Key);
            }
            return key_value;
        }
    }
    return 0;
}

  其实有个小问题,要判断ANY KEY PRESS这个条件是否为真,就要把键值全部读取一遍,那么,其实可以做一个Key_ReadALL函数,把检测是否有按键按下和返回按键键值的活一块给干了。

uint16_t Key_Ind_ReadALL(void)
{
    uint16_t usKeyValue = 0;
    for(uint8_t i=0;i<s_KeyIndPinNum;i++) {
        usKeyValue <<= 1;
        usKeyValue += GPIO_ReadInputDataBit(s_KeyIndPin[i].GPIOx,s_KeyIndPin[i].Pin)?1:0;
    }
    return usKeyValue;
}

  在这里稍稍说明一下,这里的变量命名部分参考了MISRA C规范,us开头表示变量为uint16_t类型,“s_”前缀表示为局部变量,仅允许在定义的文件内访问。Ind是independent的缩写,表示独立按键,虽然谷歌开源项目风格指南指出尽量不要使用意义不明的缩写,但是实在太长就缩写了

  左移放在读取之前是为了保证最低一Bit对应最后一次读取的按键,使用三目运算符是为了让运算看起来更明显地表现出“加个1或者0”这样的特性,虽然去掉也完全不影响。

  然后,读取函数就是这样子:

uint16_t Key_Ind_Scan(void)
{
    uint16_t usKeyValue = 0;
    if(Key_Ind_ReadALL()) {//若有按键按下
        Bsp_Delay_ms(20);
        usKeyValue = Key_Ind_ReadALL();
        if(0 != usKeyValue) {//若仍有
            return usKeyValue;
        }
    }
    return 0;
}

  可能有小伙伴发现s_KeyIndPinNum这个变量不知道打哪冒出来的,这个是在key.c文件的开头定义的。

/* 存储独立按键引脚数目的静态变量 */
static uint8_t s_KeyIndPinNum = 0;

  然后,在独立按键初始化函数中被赋值。

void Key_Ind_Init(void)
{
    GPIO_InitTypeDef GPIOInitStructrue;
    
    s_KeyIndPinNum = sizeof(s_KeyIndPin)/sizeof(s_KeyIndPin[0]);
    for(uint8_t i=0;i<s_KeyIndPinNum;i++)
    {
        RCC_APB2PeriphClockCmd((uint32_t)(1<<(((uint32_t)s_KeyIndPin[i].GPIOx - APB2PERIPH_BASE)>>10)) \
        ,ENABLE);
        
        GPIOInitStructrue.GPIO_Mode = GPIO_Mode_IPU;
        GPIOInitStructrue.GPIO_Speed = GPIO_Speed_50MHz;
        GPIOInitStructrue.GPIO_Pin = s_KeyIndPin[i].Pin;
        
        GPIO_Init(s_KeyIndPin[i].GPIOx,&GPIOInitStructrue);
    }
    
}

  用类似的思路,也可以实现矩阵键盘的扫描,笔者也自己做了相应的实现,但由于是第一次编写,代码稍显累赘,所以这里就不做讲解了,一并放到了工程中,感兴趣的小伙伴可以自行查看。

  地址:https://gitee.com/multicolore...

(测试代码基于STM3F103C8T6,V3.5版本库函数)

阅读 1.5k

在嵌入式的道路上疯狂跑偏

16 声望
15 粉丝
0 条评论

在嵌入式的道路上疯狂跑偏

16 声望
15 粉丝
文章目录
宣传栏