发新帖本帖赏金 100.00元(功能说明)我要提问
返回列表
打印
[MM32生态]

【MM32+模块】系列:04、按键检测

[复制链接]
1496|18
手机看帖
扫描二维码
随时随地手机跟帖
跳转到指定楼层
楼主
本帖最后由 xld0932 于 2022-4-6 14:08 编辑

#申请原创#   @21小跑堂

在项目中,按键检测是我们最常用的功能;对于芯片资源比较富裕时,我们可以用独立按键,在功能上既可以被独立检测,也可以实现组合检测的功能;如果按键比较多时,我们可以考虑矩阵按键,比如将之前需要16个引脚实现的独立按键,通过8个引脚实现4*4的按键矩阵,这样在按键数量保持不变的情况下,减小了MCU使用引脚的资源;当然在MCU资源很紧张,又要实现多个按键功能的时候,我们可以使用ADC来实现按键检测,这种通过1ADC通道的检测方式只能够实现每一个按键的独立检测,无法做按键组合的效果了;当然还有其它的按键检测方式,结合MCU资源和按键功能定义,总能找到一种适合你项目的按键检测方案。

本文结合淘宝上买到的3个常用的按键模块,来分别讲述一下基于MM32F0140在不同方式下按键检测的实现:
1、独立按键
2、矩阵按键
3ADC按键

独立按键


独立按键方式每一个按键需要占用一个MCU引脚资源,根据硬件设计可以配置成上拉或下拉输入,通过检测按键是否处于低/高电平来判断按键是否被按下,检测按键是否处于高/低电平来判断按键是否又被释放;每一个按键都可以独立被检测,每个按键可以实现独立的个体功能,也可以同时检测两个或者多个按键来实现组合按键的功能,实现比较灵活,缺点就是随着按键数量的增多,所占用的MCU也会同步递增。


独立按键实现代码1每个按键独立功能实现

void KEY_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_AHBPeriphClockCmd(RCC_AHBENR_GPIOB, ENABLE);

    GPIO_StructInit(&GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin  = GPIO_Pin_0  | GPIO_Pin_1  | GPIO_Pin_2 |
                                   GPIO_Pin_10 | GPIO_Pin_11;
    GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    TASK_Append(TASK_ID_KEY, KEY_Scan, 10);
}

void KEY_SubScan(uint8_t *State, uint8_t *Count, uint8_t Value, char *Name)
{
    if(*State == 0)
    {
        if(Value != Bit_RESET) *Count += 1;
        else                   *Count  = 0;

        if(*Count > 5)
        {
            *Count = 0; *State = 1;
            printf("\r\n%s Pressed", Name);
        }
    }
    else
    {
        if(Value == Bit_RESET) *Count += 1;
        else                   *Count  = 0;

        if(*Count > 5)
        {
            *Count = 0; *State = 0;
            printf("\r\n%s Release", Name);
        }
    }
}

void KEY_Scan(void)
{
    static uint8_t KeyState[KEY_NUMBER] = {0, 0, 0, 0, 0};
    static uint8_t KeyCount[KEY_NUMBER] = {0, 0, 0, 0, 0};

    KEY_SubScan(&KeyState[0], &KeyCount[0], GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0),  "BLACK  KEY");
    KEY_SubScan(&KeyState[1], &KeyCount[1], GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1),  "RED    KEY");
    KEY_SubScan(&KeyState[2], &KeyCount[2], GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_2),  "GREEN  KEY");
    KEY_SubScan(&KeyState[3], &KeyCount[3], GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_10), "YELLOW KEY");
    KEY_SubScan(&KeyState[4], &KeyCount[4], GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_11), "BLUE   KEY");
}

独立按键运行效果1


独立按键实现代码2每个按键独立功能实现,且支持长按、短按、连击上报功能

#define KEY_NUMBER          5

#define KEY_STATE_IDLE      0
#define KEY_STATE_PRESSED   1
#define KEY_STATE_RELEASE   2

typedef struct
{
    uint32_t            RCCn;
    GPIO_TypeDef       *GPIOn;
    uint16_t            PINn;
    GPIOMode_TypeDef    Mode;
    BitAction           IDLE;
    char               *Name;
} KEY_GROUP_TypeDef;

typedef struct
{
    uint8_t  State;
    uint8_t  Value;
    uint8_t  Count;
    uint16_t Debounce;
    uint16_t Interval;
    uint8_t  Trigger;
} KEY_TypeDef;

KEY_GROUP_TypeDef KEY_GROUP[KEY_NUMBER] =
{
    {RCC_AHBENR_GPIOB, GPIOB, GPIO_Pin_0 , GPIO_Mode_IPD, Bit_RESET, "BLACK  KEY"},
    {RCC_AHBENR_GPIOB, GPIOB, GPIO_Pin_1 , GPIO_Mode_IPD, Bit_RESET, "RED    KEY"},
    {RCC_AHBENR_GPIOB, GPIOB, GPIO_Pin_2 , GPIO_Mode_IPD, Bit_RESET, "GREEN  KEY"},
    {RCC_AHBENR_GPIOB, GPIOB, GPIO_Pin_10, GPIO_Mode_IPD, Bit_RESET, "YELLOW KEY"},
    {RCC_AHBENR_GPIOB, GPIOB, GPIO_Pin_11, GPIO_Mode_IPD, Bit_RESET, "BLUE   KEY"},
};


/* Private variables ---------------------------------------------------------*/
KEY_TypeDef UserKEY[KEY_NUMBER] =
{
    {0, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0, 0},
    {0, 0, 0, 0, 0, 0},
};

void KEY_Init(void)
{
    GPIO_InitTypeDef GPIO_InitStructure;

    for(uint8_t i = 0; i < KEY_NUMBER; i++)
    {
        RCC_AHBPeriphClockCmd(KEY_GROUP[i].RCCn, ENABLE);

        GPIO_StructInit(&GPIO_InitStructure);
        GPIO_InitStructure.GPIO_Pin  = KEY_GROUP[i].PINn;
        GPIO_InitStructure.GPIO_Mode = GPIO_Mode_IPD;
        GPIO_Init(KEY_GROUP[i].GPIOn, &GPIO_InitStructure);
    }

    TASK_Append(TASK_ID_KEY, KEY_Scan, 10);
}

uint8_t KEY_Read(uint8_t Index)
{
    return GPIO_ReadInputDataBit(KEY_GROUP[Index].GPIOn, KEY_GROUP[Index].PINn);
}

void KEY_SubScan(uint8_t Index)
{
    /* 读取当前的按键值 */
    uint8_t Value = KEY_Read(Index);

    switch(UserKEY[Index].State)
    {
        /*
         * 在没有按键被按下的状态, 检测到有按键被按下, 进行消抖处理, 然后切换状态
         */
        case KEY_STATE_IDLE:
            if(Value != KEY_GROUP[Index].IDLE)
            {
                if(UserKEY[Index].Debounce++ == 1)
                {
                    UserKEY[Index].State    = KEY_STATE_PRESSED;
                    UserKEY[Index].Value    = Value;
                    UserKEY[Index].Debounce = 0;
                }
            }
            else
            {
                UserKEY[Index].Debounce = 0;
            }
            break;

        /*
         * 在确认有按键被按下的状态, 仍然检测到有按键被按下, 继续进行消抖处理, 然后切换状态
         * 如果没有检测到有按键被按下, 说明上一个状态读取到的按键值是误触发, 返回到按键IDLE
         */
        case KEY_STATE_PRESSED:
            if(Value == UserKEY[Index].Value)
            {
                if(UserKEY[Index].Debounce++ == 1)
                {
                    UserKEY[Index].State    = KEY_STATE_RELEASE;
                    UserKEY[Index].Debounce = 0;
                    UserKEY[Index].Interval = 100;     /* 长按触发时间 */
                    UserKEY[Index].Trigger  = 0;
                }
            }
            else
            {
                UserKEY[Index].State    = KEY_STATE_IDLE;
                UserKEY[Index].Debounce = 0;
            }
            break;

        /*
         * 在这个状态就是确认有按键被按下, 等待按键被释放的状态; 根据按键被按下的时间长短,
         * 来判断当前是短按还是长按, 是否触发长按检测和处理
         */
        case KEY_STATE_RELEASE:
            if(Value != UserKEY[Index].Value)
            {
                if(UserKEY[Index].Trigger == 0)
                {
                    printf("\r\n%s Short Click", KEY_GROUP[Index].Name);
                }

                UserKEY[Index].State    = KEY_STATE_IDLE;
                UserKEY[Index].Value    = 0;
                UserKEY[Index].Debounce = 0;
                UserKEY[Index].Interval = 0;
                UserKEY[Index].Trigger  = 0;
            }
            else
            {
                if(UserKEY[Index].Debounce++ == UserKEY[Index].Interval)
                {
                    printf("\r\n%s Long  Click", KEY_GROUP[Index].Name);
               
                    UserKEY[Index].Debounce = 0;
                    UserKEY[Index].Interval = 25;      /* 长按上键时间 */
                    UserKEY[Index].Trigger  = 1;
                }
            }
            break;

        default: break;
    }
}

void KEY_Scan(void)
{
    for(uint8_t i = 0; i < KEY_NUMBER; i++)
    {
        KEY_SubScan(i);
    }
}

独立按键运行效果2


矩阵按键


矩阵按键所使用的MCU资源相比于独立按键要节省很多,缺点就是在一个矩阵中的按键无法实现组合按键检测的功能,按键检测都需要逐一识别,如果真需要实现组合按键的功能,可以先按下某一按键松开后,再一定时间段内再按下其它按键的方式来实现,但这样做在检测单一按键功能的时候,肯定会增加检测单一按键的时长了,如何运用,需要结合项目来取舍了。


矩阵按键实现代码:
void KEY_Init(void)
{
    TASK_Append(TASK_ID_KEY, KEY_Scan, 10);
}

uint8_t KEY_ScanX(void)
{
    uint8_t Value = 0;

    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_AHBPeriphClockCmd(RCC_AHBENR_GPIOA, ENABLE);

    /* 配置X为下拉输入模式 */
    GPIO_StructInit(&GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IPD;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    RCC_AHBPeriphClockCmd(RCC_AHBENR_GPIOB, ENABLE);

    /* 配置Y为输出模式 */
    GPIO_StructInit(&GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_Out_PP;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    /* Y输出高电平 */
    GPIO_WriteBit(GPIOB, GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_10, Bit_SET);

    if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_4) == Bit_SET) Value |= 0x01;
    if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_5) == Bit_SET) Value |= 0x02;
    if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_6) == Bit_SET) Value |= 0x04;
    if(GPIO_ReadInputDataBit(GPIOA, GPIO_Pin_7) == Bit_SET) Value |= 0x08;

    return Value;
}

uint8_t KEY_ScanY(void)
{
    uint8_t Value = 0;

    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_AHBPeriphClockCmd(RCC_AHBENR_GPIOA, ENABLE);

    /* 配置X为输出模式 */
    GPIO_StructInit(&GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_Out_PP;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    /* X输出低电平 */
    GPIO_WriteBit(GPIOA, GPIO_Pin_4 | GPIO_Pin_5 | GPIO_Pin_6 | GPIO_Pin_7, Bit_RESET);

    RCC_AHBPeriphClockCmd(RCC_AHBENR_GPIOB, ENABLE);

    /* 配置Y为上拉输入模式 */
    GPIO_StructInit(&GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_0 | GPIO_Pin_1 | GPIO_Pin_2 | GPIO_Pin_10;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_IPU;
    GPIO_Init(GPIOB, &GPIO_InitStructure);

    if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_0 ) == Bit_RESET) Value |= 0x10;
    if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_1 ) == Bit_RESET) Value |= 0x20;
    if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_2 ) == Bit_RESET) Value |= 0x40;
    if(GPIO_ReadInputDataBit(GPIOB, GPIO_Pin_10) == Bit_RESET) Value |= 0x80;

    return Value;
}

void KEY_Scan(void)
{
    uint8_t KEY_TABLE[16][2] =
    {
        {0x88, '1'}, {0x84, '2'}, {0x82, '3'}, {0x81, 'A'},
        {0x48, '4'}, {0x44, '5'}, {0x42, '6'}, {0x41, 'B'},
        {0x28, '7'}, {0x24, '8'}, {0x22, '9'}, {0x21, 'C'},
        {0x18, '*'}, {0x14, '0'}, {0x12, '#'}, {0x11, 'D'},
    };

    uint8_t Value = KEY_ScanX() | KEY_ScanY();

    for(uint8_t i = 0; i < 16; i++)
    {
        if(Value == KEY_TABLE[i][0])
        {
            printf("\r\nKEY[%c] : 0x%02x", KEY_TABLE[i][1], KEY_TABLE[i][0]);
        }
    }

    while(KEY_ScanX() | KEY_ScanY());   /* 等待按键抬起 */
}

矩阵按键运行效果:


ADC按键


ADC按键是最节省MCU资源的一种按键检测方式,在一条ADC通道上按键被按下后,通过改变分压电阻的方式来改变ADC通道的采样电压,不同的电压对应一个按键;ADC按键方式一个ADC通道只占用一个MCU引脚资源,但这种方式虽说节省MCU资源,但在一个ADC通道上也不可以挂载太多的按键,因为在系统运行过程中,ADC的值是存在波动的,为了能够准确的检测出是哪个按键被按下,就需要将分压电阻后的电压值明显的区分开,不能因为系统电压的波动导致按键检测的重叠;在电路设计和元器件选型上,分压电阻需要使用高精度的,在布板时需要考虑系统或者外界的干扰源,尽量减小对按键电压采样的影响,通过合理的分配按键电压节点,可以提升按键挂载数量,以及按键检测的准确度。


ADC按键实现代码:

void KEY_Init(void)
{
    ADC_InitTypeDef  ADC_InitStructure;
    GPIO_InitTypeDef GPIO_InitStructure;

    RCC_APB2PeriphClockCmd(RCC_APB2ENR_ADC1, ENABLE);

    ADC_StructInit(&ADC_InitStructure);
    ADC_InitStructure.ADC_Resolution         = ADC_Resolution_12b;        
    ADC_InitStructure.ADC_PRESCARE           = ADC_PCLK2_PRESCARE_16;         
    ADC_InitStructure.ADC_Mode               = ADC_Mode_Imm;              
    ADC_InitStructure.ADC_ContinuousConvMode = DISABLE;
    ADC_InitStructure.ADC_DataAlign          = ADC_DataAlign_Right;         
    ADC_Init(ADC1, &ADC_InitStructure);

    ADC_RegularChannelConfig(ADC1, ADC_Channel_3, 0, ADC_Samctl_240_5);

    ADC_Cmd(ADC1, ENABLE);

    RCC_AHBPeriphClockCmd(RCC_AHBENR_GPIOA, ENABLE);

    GPIO_StructInit(&GPIO_InitStructure);
    GPIO_InitStructure.GPIO_Pin   = GPIO_Pin_3;
    GPIO_InitStructure.GPIO_Speed = GPIO_Speed_50MHz;
    GPIO_InitStructure.GPIO_Mode  = GPIO_Mode_AIN;
    GPIO_Init(GPIOA, &GPIO_InitStructure);

    TASK_Append(TASK_ID_KEY, KEY_Scan, 10);
}

uint8_t KEY_Read(void)
{
    uint16_t Value = 0, Delta = 50;

    ADC_SoftwareStartConvCmd(ADC1, ENABLE);
    while(!ADC_GetFlagStatus(ADC1, ADC_IT_EOC));
    ADC_ClearFlag(ADC1, ADC_FLAG_EOC);

    Value = ADC_GetConversionValue(ADC1);

    if(Value < (0 + Delta))
    {
        return 1;
    }
    else if((Value > (570  - Delta)) && (Value < (570  + Delta)))
    {
        return 2;
    }
    else if((Value > (1320 - Delta)) && (Value < (1370 + Delta)))
    {
        return 3;
    }
    else if((Value > (2020 - Delta)) && (Value < (2020 + Delta)))
    {
        return 4;
    }
    else if((Value > (2950 - Delta)) && (Value < (2950 + Delta)))
    {
        return 5;
    }
    else
    {
        return 0;
    }
}

void KEY_Scan(void)
{
    static uint8_t KeyCount = 0;
    static uint8_t KeyState = 0;
    static uint8_t KeyValue = 0;

    char *KEY_NAME[6] = {"NUL", "SW1", "SW2", "SW3", "SW4", "SW5"};

    uint8_t Value = KEY_Read();

    if(KeyState == 0)
    {
        if(Value != 0)
        {
            if(KeyCount++ > 5)
            {
                KeyCount = 0;
                KeyState = 1;
                KeyValue = Value;
            }
        }
        else
        {
            KeyCount = 0;
        }
    }
    else
    {
        if(Value != KeyValue)
        {
            if(KeyCount++ > 5)
            {
                printf("\r\n%s Clicked", KEY_NAME[KeyValue]);

                KeyCount = 0;
                KeyState = 0;
                KeyValue = 0;
            }
        }
        else
        {
            KeyCount = 0;
        }
    }
}

ADC按键运行效果:


附件
软件工程源代码: 04.KEY.zip (710.09 KB)


   

使用特权

评论回复

打赏榜单

21小跑堂 打赏了 100.00 元 2022-04-08
理由:恭喜通过原创奖文章审核!请多多加油哦!

沙发
xld0932|  楼主 | 2022-4-7 22:08 | 只看该作者
基于MM32分享了3种比较常用的按键检测方式:独立按键检测、矩阵按键检测、ADC按键检测

使用特权

评论回复
板凳
chenqianqian| | 2022-4-9 10:10 | 只看该作者
学习了,谢谢讲解。

使用特权

评论回复
地板
xld0932|  楼主 | 2022-4-13 11:50 | 只看该作者
chenqianqian 发表于 2022-4-9 10:10
学习了,谢谢讲解。

点击头像“关注我”一起进步,在我的主页可以学习更多技术干货哦

使用特权

评论回复
5
王久强| | 2022-4-13 11:59 | 只看该作者
楼主,你经常用灵动的单片机做项目吗?有没有什么常规量大的芯片推荐啊?

使用特权

评论回复
6
xld0932|  楼主 | 2022-4-13 13:15 | 只看该作者
王久强 发表于 2022-4-13 11:59
楼主,你经常用灵动的单片机做项目吗?有没有什么常规量大的芯片推荐啊? ...

我们现在用的是MM32F0140系列的MCU做项目的,具体用哪个芯片根据你的功能需求,问一下灵动的代理了;一般官网上呈现出来的都是主推的吧……

使用特权

评论回复
7
王久强| | 2022-4-13 14:52 | 只看该作者
好的好的,谢谢

使用特权

评论回复
8
xld0932|  楼主 | 2022-4-14 08:45 | 只看该作者
本帖最后由 xld0932 于 2022-4-14 13:37 编辑

#申请原创#   @21小跑堂

另外还有几个常见的按键模块如下图所示:

五向控制模块:其中COM端可以连接VCC或者GND,在连接VCC时,GPIO需要设置成IPD模式,判断按下是应该是处于Bit_SET状态;在连接GND时,GPIO需要设置成IPU模式,判断按下时应该是处于Bit_RESET状态。


4*4矩阵按键:


3*4矩阵键盘:


4*4矩阵键盘:



更新软件工程源代码: 04.KEY.zip (710.78 KB)

使用特权

评论回复
9
xld0932|  楼主 | 2022-4-29 14:54 | 只看该作者
突然想到,还有电容触摸按键在后面再安排分享

使用特权

评论回复
10
tpgf| | 2022-5-2 12:00 | 只看该作者
怎么去抖的呀

使用特权

评论回复
11
八层楼| | 2022-5-2 12:07 | 只看该作者
这样做比较节省引脚啊

使用特权

评论回复
12
观海| | 2022-5-2 13:09 | 只看该作者
一共需要多少个引脚啊

使用特权

评论回复
13
guanjiaer| | 2022-5-2 13:21 | 只看该作者
整个思路都非常清晰啊

使用特权

评论回复
14
heimaojingzhang| | 2022-5-2 14:04 | 只看该作者
一般这种使用寿命是多久啊

使用特权

评论回复
15
keaibukelian| | 2022-5-2 14:13 | 只看该作者
第一次了解ADC按键

使用特权

评论回复
16
xld0932|  楼主 | 2022-5-2 16:21 | 只看该作者
keaibukelian 发表于 2022-5-2 14:13
第一次了解ADC按键

嗯,用的还挺多的呢

使用特权

评论回复
17
mutable| | 2022-5-5 15:18 | 只看该作者
之前,还定制过按键了

使用特权

评论回复
18
koala889| | 2022-5-15 08:01 | 只看该作者
ADC按键,是不是一个adc端可以挂载多个按键

使用特权

评论回复
19
xld0932|  楼主 | 2022-5-15 08:13 | 只看该作者
koala889 发表于 2022-5-15 08:01
ADC按键,是不是一个adc端可以挂载多个按键

是的,在硬件上每个按键被按下之后所对应的ADC值,应有效的区分开来,不会因为ADC的采样波动而影响按键的误识别

使用特权

评论回复
发新帖 本帖赏金 100.00元(功能说明)我要提问
您需要登录后才可以回帖 登录 | 注册

本版积分规则

认证:上海灵动微电子股份有限公司资深现场应用工程师
简介:诚信·承诺·创新·合作

67

主题

2993

帖子

29

粉丝