xubenben0313 发表于 2023-3-30 03:55:26

STM32F103C8T6标准库函数实现多按键检测 | 状态机短按、长按识别

前言

制作航模遥控器需要用到多按键检测,实现过程中主要参考了以下两篇文章,尤其是第一篇收获最大,作者的代码思想很好,但文中部分代码有误,实际运行时检测到的IO电平是错误的,花费了一天时间才调通,简单记录一下。
1.电路连接

使用STM32F103C8T6蓝色板,按键采用共阴极连接。
6个按键:
CH1Left 接PB5
CH1Right 接PB4
CH2Up 接PA15
CH2Down 接PB3
CH4Left 接PA12
CH4Right 接PA11
串口USB-TTL接法:
GND 电源地
3V3 接3.3V
TXD 接PB7
RXD 接PB6
ST-LINK V2接法:
GND 电源地
3V3 接3.3V
SWCLK 接DCLK
SWDIO 接DIO


2.程序实现

key.h - 主要定义结构体和函数预定义
#ifndef __KEY_H#define __KEY_H       #include "stm32f10x.h"#include "stm32f10x_gpio.h" typedef struct // 构造按键初始化类{        GPIOMode_TypeDef GPIO_Mode; // 初始化按键模式        GPIO_TypeDef* GPIOx; // 初始化按键口        uint16_t GPIO_Pin_x; // 初始化按键引脚好        uint32_t RCC_APB2Periph_GPIOx; // 初始化时钟}Key_Init; typedef enum _KEY_STATUS_LIST // 按键状态{        KEY_NULL = 0x00, // 无动作        KEY_SURE = 0x01, // 确认状态        KEY_UP   = 0x02, // 按键抬起        KEY_DOWN = 0x04, // 按键按下        KEY_LONG = 0x08, // 长按}KEY_STATUS_LIST; typedef struct _KEY_COMPONENTS // 状态机类{    FunctionalState KEY_SHIELD; //按键屏蔽,DISABLE(0):屏蔽,ENABLE(1):不屏蔽        uint8_t KEY_COUNT;              //按键长按计数    BitAction KEY_LEVEL;      //最终按键状态,按下Bit_SET(1),抬起Bit_RESET(0)    BitAction KEY_DOWN_LEVEL;   //按下时,按键IO实际的电平    KEY_STATUS_LIST KEY_STATUS;       //按键状态    KEY_STATUS_LIST KEY_EVENT;      //按键事件    BitAction (*READ_PIN)(Key_Init Key);//读IO电平函数}KEY_COMPONENTS;typedef struct // 按键类{        Key_Init Key; // 继承初始化父类        KEY_COMPONENTS Status; // 继承状态机父类}Key_Config;typedef enum // 按键注册表{        CH1Left,        CH1Right,        CH2Up,        CH2Down,        CH4Left,        CH4Right,// 用户添加的按钮名称        KEY_NUM, // 必须要有的记录按钮数量,必须在最后}KEY_LIST;void KEY_Init(void);//IO初始化void Creat_Key(Key_Init* Init); // 初始化按钮函数void ReadKeyStatus(void); // 状态机函数void TIM3_Init(u16 arr,u16 psc);#endif原文中Key用的是指针,结果导致读电平函数GPIO_ReadInputDataBit()寻址错误,才使得读出的电平有误。
typedef struct // 按键类{        Key_Init *Key; // 继承初始化父类        KEY_COMPONENTS Status; // 继承状态机父类}Key_Config;key.c - TIM3定时器初始化,定时检测按键状态;有限状态机实现
#include "stm32f10x.h"#include "key.h"#include "sys.h" #include "delay.h"#include "usart.h" //参考链接https://blog.csdn.net/qq_42679566/article/details/105892105,原文错误已修正 Key_Config Key_Buf;        // 创建按键数组#define KEY_LONG_DOWN_DELAY 30         // 设置30个TIM3定时器中断=600ms算长按        #define DBGMCU_CR(*((volatile u32 *)0xE0042004))        /*通用定时器3中断初始化,使用TIM3控制按键定时检测时钟选择为APB1的2倍,而APB1为36M* 参数:arr:自动重装值。                psc:时钟预分频数*/void TIM3_Init(u16 arr,u16 psc){        TIM_TimeBaseInitTypeDef TIM_TimeBaseInitStructure;        NVIC_InitTypeDef NVIC_InitStructure;                RCC_APB1PeriphClockCmd(RCC_APB1Periph_TIM3, ENABLE);                 //时钟使能         TIM_TimeBaseInitStructure.TIM_Period = arr; //自动重装载寄存器周期的值        TIM_TimeBaseInitStructure.TIM_Prescaler=psc;//预分频值        TIM_TimeBaseInitStructure.TIM_CounterMode=TIM_CounterMode_Up; // 向上计数        TIM_TimeBaseInitStructure.TIM_ClockDivision=TIM_CKD_DIV1; //时钟分割为0,仍然使用72MHz        TIM_ITConfig(TIM3, TIM_IT_Update, ENABLE);//允许更新中断        TIM_TimeBaseInit(TIM3,&TIM_TimeBaseInitStructure);                        NVIC_InitStructure.NVIC_IRQChannel = TIM3_IRQn;        NVIC_InitStructure.NVIC_IRQChannelPreemptionPriority=0;//抢占优先级0        NVIC_InitStructure.NVIC_IRQChannelSubPriority = 3;                //子优先级3        NVIC_InitStructure.NVIC_IRQChannelCmd = ENABLE;                        //IRQ通道使能        NVIC_Init(&NVIC_InitStructure);        //根据指定的参数初始化NVIC寄存器                TIM_Cmd(TIM3,ENABLE);} void TIM3_IRQHandler(void)   //TIM3中断{        if (TIM_GetITStatus(TIM3, TIM_IT_Update) != RESET)//检查TIM3更新中断发生与否        {                // 中断处理代码                ReadKeyStatus();//调用状态机                u8 i,status;                for(i = 0;i < KEY_NUM;i++)            {                        status = Key_Buf.Status.KEY_EVENT;                        //if(status!=KEY_NULL) printf("%d,%d\n",i,status);//事件处理                        if(status==KEY_DOWN) printf("%d短按\n",i);                        if(status==KEY_LONG) printf("%d长按\n",i);                }                TIM_ClearITPendingBit(TIM3, TIM_IT_Update);//清除TIMx更新中断标志         }} //按键初始化函数void KEY_Init(void) //IO初始化{         Key_Init KeyInit=        {                 {GPIO_Mode_IPU, GPIOB, GPIO_Pin_5, RCC_APB2Periph_GPIOB},         // 初始化按键CH1Left                {GPIO_Mode_IPU, GPIOB, GPIO_Pin_4, RCC_APB2Periph_GPIOB},         // 初始化按键CH1Right                {GPIO_Mode_IPU, GPIOA, GPIO_Pin_15, RCC_APB2Periph_GPIOA},         // 初始化按键CH2Up                {GPIO_Mode_IPU, GPIOB, GPIO_Pin_3, RCC_APB2Periph_GPIOB},         // 初始化按键CH2Down                {GPIO_Mode_IPU, GPIOA, GPIO_Pin_12, RCC_APB2Periph_GPIOA},         // 初始化按键CH4Left                {GPIO_Mode_IPU, GPIOA, GPIO_Pin_11, RCC_APB2Periph_GPIOA},         // 初始化按键CH4Right        };        Creat_Key(KeyInit); // 调用按键初始化函数                //STM32没有彻底释放PB3作为普通IO口使用,切换到SW调试可释放PB3、PB4、PA15        RCC_APB2PeriphClockCmd(RCC_APB2Periph_AFIO,ENABLE);        GPIO_PinRemapConfig(GPIO_Remap_SWJ_JTAGDisable,ENABLE);        DBGMCU_CR &=0xFFFFFFDF;//如果没有这段代码,PB3就会一直是低电平} static BitAction KEY_ReadPin(Key_Init Key) //按键读取函数{return (BitAction)GPIO_ReadInputDataBit(Key.GPIOx,Key.GPIO_Pin_x);} void Creat_Key(Key_Init* Init){        uint8_t i;         GPIO_InitTypeDefGPIO_InitStructure;        for(i = 0;i < KEY_NUM;i++)        {                Key_Buf.Key = Init; // 按钮对象的初始化属性赋值                RCC_APB2PeriphClockCmd(Key_Buf.Key.RCC_APB2Periph_GPIOx, ENABLE);//使能相应时钟                GPIO_InitStructure.GPIO_Pin = Key_Buf.Key.GPIO_Pin_x;        //设定引脚                                        GPIO_InitStructure.GPIO_Mode = Key_Buf.Key.GPIO_Mode;         //设定模式                                GPIO_Init(Key_Buf.Key.GPIOx, &GPIO_InitStructure);       //初始化引脚                // 初始化按钮对象的状态机属性                Key_Buf.Status.KEY_SHIELD = ENABLE;                Key_Buf.Status.KEY_COUNT = 0;                Key_Buf.Status.KEY_LEVEL = Bit_RESET;                if(Key_Buf.Key.GPIO_Mode == GPIO_Mode_IPU) // 根据模式进行赋值                        Key_Buf.Status.KEY_DOWN_LEVEL = Bit_RESET;                else                        Key_Buf.Status.KEY_DOWN_LEVEL = Bit_SET;                Key_Buf.Status.KEY_STATUS = KEY_NULL;                Key_Buf.Status.KEY_EVENT = KEY_NULL;                Key_Buf.Status.READ_PIN = KEY_ReadPin;        //赋值按键读取函数        }} static void Get_Key_Level(void) // 根据实际按下按钮的电平去把它换算成虚拟的结果{    uint8_t i;      for(i = 0;i < KEY_NUM;i++)    {      if(Key_Buf.Status.KEY_SHIELD == DISABLE)            continue;      if(Key_Buf.Status.READ_PIN(Key_Buf.Key) == Key_Buf.Status.KEY_DOWN_LEVEL)            Key_Buf.Status.KEY_LEVEL = Bit_SET;      else            Key_Buf.Status.KEY_LEVEL = Bit_RESET;    }} void ReadKeyStatus(void){    uint8_t i;          Get_Key_Level();          for(i = 0;i < KEY_NUM;i++)    {      switch(Key_Buf.Status.KEY_STATUS)      {            //状态0:没有按键按下            case KEY_NULL:                if(Key_Buf.Status.KEY_LEVEL == Bit_SET)//有按键按下                {                  Key_Buf.Status.KEY_STATUS = KEY_SURE;//转入状态1                                        Key_Buf.Status.KEY_EVENT = KEY_NULL;//空事件                }                else                {                  Key_Buf.Status.KEY_EVENT = KEY_NULL;//空事件                }                break;            //状态1:按键按下确认            case KEY_SURE:                if(Key_Buf.Status.KEY_LEVEL == Bit_SET)//确认和上次相同                {                  Key_Buf.Status.KEY_STATUS = KEY_DOWN;//转入状态2                                        Key_Buf.Status.KEY_EVENT = KEY_DOWN;//按下事件                  Key_Buf.Status.KEY_COUNT = 0;//计数器清零                }                else                {                  Key_Buf.Status.KEY_STATUS = KEY_NULL;//转入状态0                  Key_Buf.Status.KEY_EVENT = KEY_NULL;//空事件                }                break;            //状态2:按键按下            case KEY_DOWN:                if(Key_Buf.Status.KEY_LEVEL != Bit_SET)//按键释放,端口高电平                {                  Key_Buf.Status.KEY_STATUS = KEY_NULL;//转入状态0                  Key_Buf.Status.KEY_EVENT = KEY_UP;//松开事件                }                else if((Key_Buf.Status.KEY_LEVEL == Bit_SET)                                        && (++Key_Buf.Status.KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超过KEY_LONG_DOWN_DELAY没有释放                {                  Key_Buf.Status.KEY_STATUS = KEY_LONG;//转入状态3                  Key_Buf.Status.KEY_EVENT = KEY_LONG;//长按事件                                        Key_Buf.Status.KEY_COUNT = 0;//计数器清零                }                else                {                  Key_Buf.Status.KEY_EVENT = KEY_NULL;//空事件                }                break;            //状态3:按键连续按下            case KEY_LONG:                if(Key_Buf.Status.KEY_LEVEL != Bit_SET)//按键释放,端口高电平                {                  Key_Buf.Status.KEY_STATUS = KEY_NULL;//转入状态0                  Key_Buf.Status.KEY_EVENT = KEY_UP;//松开事件                }                else if((Key_Buf.Status.KEY_LEVEL == Bit_SET)               && (++Key_Buf.Status.KEY_COUNT >= KEY_LONG_DOWN_DELAY)) //超过KEY_LONG_DOWN_DELAY没有释放                {                  Key_Buf.Status.KEY_EVENT = KEY_LONG;//长按事件                  Key_Buf.Status.KEY_COUNT = 0;//计数器清零                }                else                {                  Key_Buf.Status.KEY_EVENT = KEY_NULL;//空事件                }                break;                        default:                                break;      }        }}main.c - 主函数调用TIM3初始化
#include "delay.h"#include "usart.h"#include "stm32f10x.h"#include "key.h"int main(){    delay_init();//初始化延时函数    NVIC_PriorityGroupConfig(NVIC_PriorityGroup_2); //设置NVIC中断分组2,2位抢占优先级和2位子优先级    usart_init(115200);//初始化串口1,波特率为115200    TIM3_Init(19999,71);//1MHz,每20ms检测按键一次;    KEY_Init();                //KEY初始化    while(1){      delay_ms(1);    }}3.实现效果

zpshao 发表于 2023-3-30 04:06:40

好物收藏

lhl428 发表于 2023-3-30 04:14:44

这个代码还有很大的优化空间

荒野冷风 发表于 2023-3-30 04:20:02

转发了
页: [1]
查看完整版本: STM32F103C8T6标准库函数实现多按键检测 | 状态机短按、长按识别