初级会员

- 积分
- 98
- 金钱
- 98
- 注册时间
- 2019-5-27
- 在线时间
- 23 小时
|
本帖最后由 李邦 于 2019-7-31 13:53 编辑
​ 本篇将会讲述GPIO输入的相关操作,典型的输入就是按键的操作。我们会自己封装一个状态机去处理按键,接口设计尽量通用便于移植。
一、按键检测原理
按键机械触点闭合、断开时,由于触点的弹性作用,按键不会马上稳定接通或者断开,即我们时常听到的“抖动”。
按键去抖动的方法有两种,硬件去抖动,软件去抖动。
1、硬件消抖
利用电容放电的延时,消除纹波,从而简化软件处理,软件只需要判断高低电平即可。
2、软件消抖
常用的方法是延时消抖和状态机。延时消抖大大影响了系统的性能,不是一种提倡的方式,状态机算是最优的方式。
二、STM32CubeMX配置
此原理图有4个按键,一个高电平有效,3个低电平有效,且都无外接上下拉电阻,所以需要软件设置上下拉。我们依然使用STM32CubeMX设计,新建一个STM32F407ZGT6的工程,时钟与时基配置
led和按键的配置
时钟配置
生成代码
打开keil然后编译配置
三、软件设计
key.h和key.c:按键驱动层,提供了按键触发的接口;
key_queue.h和key_queue.c:按键按下之后,将值加入队列;
key_scan.h和key_scan.c:按键状态机扫描和按键按下之后的处理函数。
目标:通用,方便删除和添加,可配置有效电平,满足基本的需求。
- // KEY驱动层
- #ifndef _KEY_H_
- #define _KEY_H_
- #include "main.h"
- // 按键初始化
- void KeyInit(void);
- // 判断按键是否被按下,根据实际情况添加和删除此处接口
- uint8_t Key0IsDown(void);
- uint8_t Key1IsDown(void);
- uint8_t Key2IsDown(void);
- uint8_t KeyWkUPIsDown(void);
- #endif /* _KEY_H_ */
复制代码- #include "key.h"
- // 配置有效电平
- typedef enum
- {
- KEY_INIT_IS_ACTIVE = 0,
- KEY_LOW_IS_ACTIVE = ​1,
- KEY_HIGH_IS_ACTIVE = 2,
- } key_active_t;
- #define KEY_READ(gpio, pin) HAL_GPIO_ReadPin(gpio, pin)
- #define KEY_IS_PRESSED(gpio, pin, tag) do{ \
- if(KEY_INIT_IS_ACTIVE == tag) \
- {\
- return 0; \
- }\
- return (KEY_READ(gpio, pin) == tag); \
- } while(0);
- // 按键结构配置
- typedef struct
- {
- GPIO_TypeDef *gpio;
- uint16_t pin;
- uint8_t tag; // 输入有效电平配置
- } key_port_t;
- // 所有的按键添加和删除都要关注此数组,按键配置表
- static key_port_t key_items[] =
- {
- {KEY0_GPIO_Port, KEY0_Pin, KEY_LOW_IS_ACTIVE},
- {KEY1_GPIO_Port, KEY1_Pin, KEY_LOW_IS_ACTIVE},
- {KEY2_GPIO_Port, KEY2_Pin, KEY_LOW_IS_ACTIVE},
- {WK_UP_GPIO_Port, WK_UP_Pin, KEY_HIGH_IS_ACTIVE},
- };
- void KeyInit(void)
- {
- // STM32CubeMX已经配置了所有的GPIO,所以此处就不添加了,
- // 如果不是使用STM32CubeMX,此处添加GPIO的初始化代码
- }
- // 判断按键是否被按下
- static uint8_t key_is_pressed(uint8_t index)
- {
- if(KEY_LOW_IS_ACTIVE == key_items[index].tag)
- {
- if(KEY_READ(key_items[index].gpio, key_items[index].pin) == 0)
- {
- return 1;
- }
- }
- else if(KEY_HIGH_IS_ACTIVE == key_items[index].tag)
- {
- if(KEY_READ(key_items[index].gpio, key_items[index].pin) == 1)
- {
- return 1;
- }
- }
- return 0;
- }
- uint8_t Key0IsDown(void)
- {
- return key_is_pressed(0); // key0在按键配置表的索引是0,
- }
- uint8_t Key1IsDown(void)
- {
- return key_is_pressed(1); // key1在按键配置表的索引是1,
- }
- uint8_t Key2IsDown(void)
- {
- return key_is_pressed(2); // key2在按键配置表的索引是2,
- }
- uint8_t KeyWkUPIsDown(void)
- {
- return key_is_pressed(3); // key在按键配置表的索引是3,
- }
复制代码
驱动层我们需要关心的是:key_items这个数组,配置触发的有效电平。当然添加一个按键就需要在.h和.c里面加一个xxxIsDown()接口供应用层使用。
- // 我们写一个简单的按键队列,按下按键之后加入队列
- #ifndef _KEY_QUEUE_H_
- #define _KEY_QUEUE_H_
- #include <stdint.h>
- typedef enum
- {
- KEY_QUEUE_EMPTY = 0,
- KEY_QUEUE_FULL = 1,
- KEY_QUEUE_OK = 2,
- } key_queue_status_t;
-
- #define KEY_QUEUE_SIZE 5
-
- typedef struct
- {
- uint16_t front;
- uint16_t rear;
- uint16_t size;
- uint8_t data[KEY_QUEUE_SIZE];
- } key_queue_t;
-
- // 出入队列
- uint8_t KeyQueuePush(key_queue_t *q, uint8_t data);
- uint8_t KeyQueuePop(key_queue_t *q, uint8_t *data);
- #endif /* _KEY_QUEUE_H_ */
复制代码- #include "key_queue.h"
-
- void KeyQueueInit(key_queue_t *q)
- {
- q->size = 0;
- q->front = 0;
- q->rear = 0;
- }
-
- uint8_t KeyQueuePush(key_queue_t *q, uint8_t data)
- {
- if(((q->rear % KEY_QUEUE_SIZE) == q->front) && (q->size == KEY_QUEUE_SIZE))
- {
- return KEY_QUEUE_FULL;
- }
-
- q->data[q->rear] = data;
- q->rear = (q->rear + 1) % KEY_QUEUE_SIZE;
- q->size++;
-
- return KEY_QUEUE_OK;
- }
-
- uint8_t KeyQueuePop(key_queue_t *q, uint8_t *data)
- {
- if((q->front == q->rear) && (q->size == 0))
- {
- return KEY_QUEUE_EMPTY;
- }
-
- *data = q->data[q->front];
- q->front = (q->front + 1) % KEY_QUEUE_SIZE;
- q->size--;
-
- return KEY_QUEUE_OK;
- }
复制代码
这两个文件不需要任何修改,算是一个硬件无关的队列,所有添加和删除按键都与这两个文件无关。
- // Key应用层 按键扫描状态机
- #ifndef KEY_SCAN_H_
- #define KEY_SCAN_H_
- void KeyScan(void); // 按键扫描状态机,此接口放在10ms定时器即可
- void KeyProcess(void); // 按键按下之后的处理函数,此接口放在main死循环里面即可
- #endif /* KEY_SCAN_H_ */
复制代码- #include "key_scan.h"
- #include "key.h" // 按键驱动头文件
- #include "key_queue.h" // 按键队列的头文件
- #include "led.h"
- // 按键的状态机结构定义
- // 按键状态
- typedef enum
- {
- KEY_STATE_INIT, // 缺省按键状态
- KEY_STATE_UP, // 按键弹起状态
- KEY_STATE_DOWN, // 按键按下状态
- KEY_STATE_LONG, // 按键长按状态
- KEY_STATE_AUTO, // 按键自动连发状态
- } key_state_t;
-
- // 按键滤波时间20ms, 单位10ms。
- // 只有连续检测到20ms状态不变才认为有效,包括弹起和按下两种事件
- // 此处可根据自己的情况设置时间,比如可以认为持续3S为长按等
- #define KEY_FILTER_TIME 2 // 单位10ms 滤波消抖20ms
- #define KEY_LONG_TIME 100 // 单位10ms 持续1秒,认为长按事件
- #define KEY_REPEAT_TIME 100 // 单位10ms 持续1秒,自动连发
-
- typedef uint8_t (*key_cb)(void);
- typedef struct
- {
- uint8_t state; // 按键当前状态(按下还是弹起)
- uint8_t last; // 上一次按键的状态
- uint8_t count; // 滤波消抖计数器
- uint16_t long_time; // 按键按下持续时间, 0表示不检测长按
- uint16_t long_count; // 长按计数器
- uint8_t repeat_speed; // 自动连发次数
- uint8_t repeat_count; // 自动连发计数器
- key_queue_t *queue; // 按键队列
- key_cb is_down_func; // 按键按下的判断函数,1表示按下
- } key_t;
- static key_queue_t key_queue = {0};
- // 此处根据实际的按键数量添加和删除,驱动文件里面提供多少按键,此处就添加多少数据
- static key_t key_scan_items[] =
- {
- {KEY_STATE_INIT, KEY_STATE_INIT, KEY_FILTER_TIME, KEY_LONG_TIME, 0, KEY_REPEAT_TIME, 0, &key_queue, Key0IsDown},
- {KEY_STATE_INIT, KEY_STATE_INIT, KEY_FILTER_TIME, KEY_LONG_TIME, 0, KEY_REPEAT_TIME, 0, &key_queue, Key1IsDown},
- {KEY_STATE_INIT, KEY_STATE_INIT, KEY_FILTER_TIME, KEY_LONG_TIME, 0, KEY_REPEAT_TIME, 0, &key_queue, Key2IsDown},
- {KEY_STATE_INIT, KEY_STATE_INIT, KEY_FILTER_TIME, KEY_LONG_TIME, 0, KEY_REPEAT_TIME, 0, &key_queue, KeyWkUPIsDown},
- };
- #define KEY_NUM (sizeof(key_scan_items) / sizeof(key_scan_items[0]))
- // 按键状态机
- static void key_scan_ext(key_t *entry, uint8_t i)
- {
- switch(entry->state)
- {
- case KEY_STATE_INIT:
- case KEY_STATE_UP:
- {
- entry->state = KEY_STATE_DOWN; // 按键被按下
- break;
- }
-
- case KEY_STATE_DOWN:
- {
- if(entry->long_time > 0)
- {
- if(entry->long_count < entry->long_time)
- {
- if(++entry->long_count >= entry->long_time)
- {
- entry->state = KEY_STATE_LONG;
- // 此处可添加长按之后,发送一次数据
- //KeyQueuePush(entry->queue, (i + 1)); // 长按响应一次
- }
- }
- }
-
- break;
- }
-
- case KEY_STATE_LONG:
- {
- if(entry->repeat_speed > 0) // 自动连发时间到 自动连发事件 每隔repeat_count的时间发送一次
- {
- if(++entry->repeat_count >= entry->repeat_speed)
- {
- entry->repeat_count = 0;
- KeyQueuePush(entry->queue, (i + 1)); // 长按一直响应
- }
- }
-
- break;
- }
- }
-
- entry->last = entry->state; // 最新的按键状态
- }
-
- static void key_scan(uint8_t i)
- {
- key_t *entry = &key_scan_items[i];
-
- if(entry->is_down_func())
- {
- if(entry->count < KEY_FILTER_TIME) // 消抖
- {
- ++entry->count;
- }
- else
- {
- key_scan_ext(entry, i); // 按键扫描状态机
- }
- }
- else
- {
- if(entry->count > KEY_FILTER_TIME)
- {
- entry->count = KEY_FILTER_TIME;
- }
- else if(entry->count > 0)
- {
- --entry->count;
- }
- else
- {
- if(KEY_STATE_DOWN == entry->last) // 一次完整的按键到这里就弹起了,长按不在此处
- {
- // 按键按下之后可以加入到队列中,这里的队列可以自己写;如果带系统可以使用系统的消息队列等方式。
- KeyQueuePush(entry->queue, (i + 1)); // key0对应索引是1 ==> KEY1_CMD
- }
- entry->last = KEY_STATE_UP;
- entry->state = KEY_STATE_UP; // 按键弹起状态
- }
-
- entry->long_count = 0;
- entry->repeat_count = 0; // 清空计数器
- }
- }
- // 按键扫描状态机,此接口放在10ms定时器即可
- void KeyScan(void)
- {
- uint8_t i;
-
- for(i = 0; i < KEY_NUM; ++i)
- {
- key_scan(i);
- }
- }
- //=============================================================================================================
- // 按键按下之后,会将值加入到队列中,我们读取队列数据,然后扫描列表匹配功能
- static void key1_cb(void);
- static void key2_cb(void);
- static void key3_cb(void);
- static void key4_cb(void);
-
- #define KEY1_CMD 1
- #define KEY2_CMD 2
- #define KEY3_CMD 3
- #define KEY4_CMD 4
-
- typedef struct
- {
- uint8_t cmd;
- void (* key_handle_cb)(void);
- } key_handle_t;
-
- static const key_handle_t key_entries[] =
- {
- {KEY1_CMD, key1_cb},
- {KEY2_CMD, key2_cb},
- {KEY3_CMD, key3_cb},
- {KEY4_CMD, key4_cb},
- {0xFF, NULL },
- };
-
- static void key1_cb(void)
- {
- OutputHigh(LED0_VALUE);
- }
-
- static void key2_cb(void)
- {
- OutputHigh(LED1_VALUE);
- }
-
- static void key3_cb(void)
- {
- OutputToggle(LED0_VALUE);
- }
-
- static void key4_cb(void)
- {
- OutputToggle(LED1_VALUE);
- }
- static void key_process(uint8_t event)
- {
- const key_handle_t *entry;
-
- for(entry = key_entries; entry->key_handle_cb; ++entry)
- {
- if(event == entry->cmd)
- {
- entry->key_handle_cb();
- break;
- }
- }
- }
- // 按键按下之后的处理函数,此接口放在main死循环里面即可
- void KeyProcess(void)
- {
- uint8_t i, event;
-
- if(KeyQueuePop(&key_queue, &event) == KEY_QUEUE_OK)
- {
- key_process(event);
- }
- }
复制代码 这两个文件提供了两个接口,一个是KeyScan(),按键扫描状态机的实现,key_scan_items是关键,主要是填充key_t这个结构体。状态机:初始状态,按下,长按,弹起等,判断按下之后将值加入队列。 KeyProcess():这个函数放在main()循环里面,循环读队列得到加入队列时的值,然后扫描key_entries数组,然后调用其回调处理。
- /* USER CODE BEGIN WHILE */
- while (1)
- {
- // 实际项目中,KeyScan()放在定时器或者任务中
- KeyScan();
- KeyProcess();
- HAL_Delay(10); // 我们目前还没有到定时器那一块,暂时适用HAL_Delay代替,
- /* USER CODE END WHILE */
- /* USER CODE BEGIN 3 */
- }
- /* USER CODE END 3 */
复制代码 在main的while(1)里面调用这两个函数即可,我这里只是测试,所以添加了一个HAL_Delay(10);编译,debug调试,然后就可以测试了。key0,key1是按下之后灯亮,key2,wk_up,短按取反,是长按之后灯1S闪烁一次。

|
|