OpenEdv-开源电子网

 找回密码
 立即注册
正点原子全套STM32/Linux/FPGA开发资料,上千讲STM32视频教程免费下载...
查看: 4151|回复: 7

STM32F4——按键状态机(触发可配置)

[复制链接]

3

主题

9

帖子

0

精华

初级会员

Rank: 2

积分
98
金钱
98
注册时间
2019-5-27
在线时间
23 小时
发表于 2019-7-31 11:05:03 | 显示全部楼层 |阅读模式
本帖最后由 李邦 于 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:按键状态机扫描和按键按下之后的处理函数。
目标:通用,方便删除和添加,可配置有效电平,满足基本的需求。

  1. // KEY驱动层
  2. #ifndef _KEY_H_
  3. #define _KEY_H_

  4. #include "main.h"

  5. // 按键初始化
  6. void KeyInit(void);

  7. // 判断按键是否被按下,根据实际情况添加和删除此处接口
  8. uint8_t Key0IsDown(void);
  9. uint8_t Key1IsDown(void);
  10. uint8_t Key2IsDown(void);
  11. uint8_t KeyWkUPIsDown(void);

  12. #endif /* _KEY_H_ */

复制代码
  1. #include "key.h"

  2. // 配置有效电平
  3. typedef enum
  4. {
  5.   KEY_INIT_IS_ACTIVE = 0,
  6.   KEY_LOW_IS_ACTIVE  = ​1,
  7.   KEY_HIGH_IS_ACTIVE = 2,
  8. } key_active_t;

  9. #define KEY_READ(gpio, pin)  HAL_GPIO_ReadPin(gpio, pin)
  10. #define KEY_IS_PRESSED(gpio, pin, tag) do{ \
  11.   if(KEY_INIT_IS_ACTIVE == tag) \
  12.   {\
  13.     return 0; \
  14.   }\
  15.   return (KEY_READ(gpio, pin) == tag); \
  16. } while(0);

  17. // 按键结构配置
  18. typedef struct
  19. {
  20.   GPIO_TypeDef *gpio;
  21.   uint16_t pin;
  22.   uint8_t tag; // 输入有效电平配置
  23. } key_port_t;

  24. // 所有的按键添加和删除都要关注此数组,按键配置表
  25. static key_port_t key_items[] =
  26. {
  27.   {KEY0_GPIO_Port, KEY0_Pin, KEY_LOW_IS_ACTIVE},
  28.   {KEY1_GPIO_Port, KEY1_Pin, KEY_LOW_IS_ACTIVE},
  29.   {KEY2_GPIO_Port, KEY2_Pin, KEY_LOW_IS_ACTIVE},
  30.   {WK_UP_GPIO_Port, WK_UP_Pin, KEY_HIGH_IS_ACTIVE},
  31. };

  32. void KeyInit(void)
  33. {
  34.   // STM32CubeMX已经配置了所有的GPIO,所以此处就不添加了,
  35.   // 如果不是使用STM32CubeMX,此处添加GPIO的初始化代码
  36. }

  37. // 判断按键是否被按下
  38. static uint8_t key_is_pressed(uint8_t index)
  39. {
  40.   if(KEY_LOW_IS_ACTIVE == key_items[index].tag)
  41.   {
  42.     if(KEY_READ(key_items[index].gpio, key_items[index].pin) == 0)
  43.     {
  44.       return 1;
  45.     }
  46.   }
  47.   else if(KEY_HIGH_IS_ACTIVE == key_items[index].tag)
  48.   {
  49.     if(KEY_READ(key_items[index].gpio, key_items[index].pin) == 1)
  50.     {
  51.       return 1;
  52.     }
  53.   }

  54.   return 0;
  55. }

  56. uint8_t Key0IsDown(void)
  57. {
  58.   return key_is_pressed(0); // key0在按键配置表的索引是0,
  59. }

  60. uint8_t Key1IsDown(void)
  61. {
  62.   return key_is_pressed(1); // key1在按键配置表的索引是1,
  63. }

  64. uint8_t Key2IsDown(void)
  65. {
  66.   return key_is_pressed(2); // key2在按键配置表的索引是2,
  67. }

  68. uint8_t KeyWkUPIsDown(void)
  69. {
  70.   return key_is_pressed(3); // key在按键配置表的索引是3,
  71. }

复制代码

    驱动层我们需要关心的是:key_items这个数组,配置触发的有效电平。当然添加一个按键就需要在.h和.c里面加一个xxxIsDown()接口供应用层使用。

  1. // 我们写一个简单的按键队列,按下按键之后加入队列
  2. #ifndef _KEY_QUEUE_H_
  3. #define _KEY_QUEUE_H_

  4. #include <stdint.h>

  5. typedef enum
  6. {
  7.   KEY_QUEUE_EMPTY = 0,
  8.   KEY_QUEUE_FULL  = 1,
  9.   KEY_QUEUE_OK    = 2,
  10. } key_queue_status_t;

  11. #define KEY_QUEUE_SIZE  5

  12. typedef struct
  13. {  
  14.   uint16_t front;
  15.   uint16_t rear;
  16.   uint16_t size;
  17.   uint8_t data[KEY_QUEUE_SIZE];
  18. } key_queue_t;

  19. // 出入队列
  20. uint8_t KeyQueuePush(key_queue_t *q, uint8_t data);
  21. uint8_t KeyQueuePop(key_queue_t *q, uint8_t *data);

  22. #endif /* _KEY_QUEUE_H_ */

复制代码
  1. #include "key_queue.h"

  2. void KeyQueueInit(key_queue_t *q)
  3. {
  4.   q->size = 0;
  5.   q->front = 0;
  6.   q->rear = 0;
  7. }

  8. uint8_t KeyQueuePush(key_queue_t *q, uint8_t data)
  9. {
  10.   if(((q->rear % KEY_QUEUE_SIZE) == q->front) && (q->size == KEY_QUEUE_SIZE))
  11.   {
  12.     return KEY_QUEUE_FULL;
  13.   }

  14.   q->data[q->rear] = data;
  15.   q->rear = (q->rear + 1) % KEY_QUEUE_SIZE;
  16.   q->size++;

  17.   return KEY_QUEUE_OK;
  18. }

  19. uint8_t KeyQueuePop(key_queue_t *q, uint8_t *data)
  20. {
  21.   if((q->front == q->rear) && (q->size == 0))
  22.   {
  23.     return KEY_QUEUE_EMPTY;
  24.   }
  25.   
  26.   *data = q->data[q->front];
  27.   q->front = (q->front + 1) % KEY_QUEUE_SIZE;
  28.   q->size--;

  29.   return KEY_QUEUE_OK;
  30. }

复制代码


    这两个文件不需要任何修改,算是一个硬件无关的队列,所有添加和删除按键都与这两个文件无关。

  1. // Key应用层 按键扫描状态机
  2. #ifndef KEY_SCAN_H_
  3. #define KEY_SCAN_H_

  4. void KeyScan(void);    // 按键扫描状态机,此接口放在10ms定时器即可
  5. void KeyProcess(void); // 按键按下之后的处理函数,此接口放在main死循环里面即可

  6. #endif /* KEY_SCAN_H_ */

复制代码
  1. #include "key_scan.h"
  2. #include "key.h" // 按键驱动头文件
  3. #include "key_queue.h" // 按键队列的头文件
  4. #include "led.h"

  5. // 按键的状态机结构定义
  6. // 按键状态
  7. typedef enum
  8. {
  9.   KEY_STATE_INIT, // 缺省按键状态
  10.   KEY_STATE_UP,   // 按键弹起状态
  11.   KEY_STATE_DOWN, // 按键按下状态
  12.   KEY_STATE_LONG, // 按键长按状态
  13.   KEY_STATE_AUTO, // 按键自动连发状态
  14. } key_state_t;

  15. // 按键滤波时间20ms, 单位10ms。
  16. // 只有连续检测到20ms状态不变才认为有效,包括弹起和按下两种事件
  17. // 此处可根据自己的情况设置时间,比如可以认为持续3S为长按等
  18. #define KEY_FILTER_TIME 2   // 单位10ms    滤波消抖20ms
  19. #define KEY_LONG_TIME   100 // 单位10ms    持续1秒,认为长按事件
  20. #define KEY_REPEAT_TIME 100 // 单位10ms    持续1秒,自动连发

  21. typedef uint8_t (*key_cb)(void);
  22. typedef struct
  23. {
  24.   uint8_t state;        // 按键当前状态(按下还是弹起)
  25.   uint8_t last;         // 上一次按键的状态
  26.   uint8_t count;        // 滤波消抖计数器
  27.   uint16_t long_time;   // 按键按下持续时间, 0表示不检测长按
  28.   uint16_t long_count;  // 长按计数器
  29.   uint8_t repeat_speed; // 自动连发次数
  30.   uint8_t repeat_count; // 自动连发计数器
  31.   key_queue_t *queue;   // 按键队列  
  32.   key_cb is_down_func;  // 按键按下的判断函数,1表示按下
  33. } key_t;

  34. static key_queue_t key_queue = {0};

  35. // 此处根据实际的按键数量添加和删除,驱动文件里面提供多少按键,此处就添加多少数据
  36. static key_t key_scan_items[] =
  37. {
  38.   {KEY_STATE_INIT, KEY_STATE_INIT, KEY_FILTER_TIME, KEY_LONG_TIME, 0, KEY_REPEAT_TIME, 0, &key_queue, Key0IsDown},
  39.   {KEY_STATE_INIT, KEY_STATE_INIT, KEY_FILTER_TIME, KEY_LONG_TIME, 0, KEY_REPEAT_TIME, 0, &key_queue, Key1IsDown},
  40.   {KEY_STATE_INIT, KEY_STATE_INIT, KEY_FILTER_TIME, KEY_LONG_TIME, 0, KEY_REPEAT_TIME, 0, &key_queue, Key2IsDown},
  41.   {KEY_STATE_INIT, KEY_STATE_INIT, KEY_FILTER_TIME, KEY_LONG_TIME, 0, KEY_REPEAT_TIME, 0, &key_queue, KeyWkUPIsDown},
  42. };

  43. #define KEY_NUM  (sizeof(key_scan_items) / sizeof(key_scan_items[0]))

  44. // 按键状态机
  45. static void key_scan_ext(key_t *entry, uint8_t i)
  46. {
  47.   switch(entry->state)
  48.   {
  49.   case KEY_STATE_INIT:
  50.   case KEY_STATE_UP:
  51.   {
  52.     entry->state = KEY_STATE_DOWN; // 按键被按下
  53.     break;
  54.   }

  55.   case KEY_STATE_DOWN:
  56.   {
  57.     if(entry->long_time > 0)
  58.     {
  59.       if(entry->long_count < entry->long_time)
  60.       {
  61.         if(++entry->long_count >= entry->long_time)
  62.         {
  63.           entry->state = KEY_STATE_LONG;
  64.           // 此处可添加长按之后,发送一次数据
  65.           //KeyQueuePush(entry->queue, (i + 1)); // 长按响应一次
  66.         }
  67.       }
  68.     }

  69.     break;
  70.   }

  71.   case KEY_STATE_LONG:
  72.   {
  73.     if(entry->repeat_speed > 0) // 自动连发时间到  自动连发事件  每隔repeat_count的时间发送一次
  74.     {
  75.       if(++entry->repeat_count >= entry->repeat_speed)
  76.       {
  77.         entry->repeat_count = 0;
  78.         KeyQueuePush(entry->queue, (i + 1)); // 长按一直响应
  79.       }
  80.     }

  81.     break;
  82.   }
  83.   }

  84.   entry->last = entry->state; // 最新的按键状态
  85. }

  86. static void key_scan(uint8_t i)
  87. {
  88.   key_t *entry = &key_scan_items[i];

  89.   if(entry->is_down_func())
  90.   {
  91.     if(entry->count < KEY_FILTER_TIME) // 消抖
  92.     {
  93.       ++entry->count;
  94.     }
  95.     else
  96.     {
  97.       key_scan_ext(entry, i); // 按键扫描状态机
  98.     }
  99.   }
  100.   else
  101.   {
  102.     if(entry->count > KEY_FILTER_TIME)
  103.     {
  104.       entry->count = KEY_FILTER_TIME;
  105.     }
  106.     else if(entry->count > 0)
  107.     {
  108.       --entry->count;
  109.     }
  110.     else
  111.     {
  112.       if(KEY_STATE_DOWN == entry->last) // 一次完整的按键到这里就弹起了,长按不在此处
  113.       {
  114.        // 按键按下之后可以加入到队列中,这里的队列可以自己写;如果带系统可以使用系统的消息队列等方式。
  115.         KeyQueuePush(entry->queue, (i + 1)); // key0对应索引是1 ==> KEY1_CMD
  116.       }
  117.       entry->last  = KEY_STATE_UP;
  118.       entry->state = KEY_STATE_UP; // 按键弹起状态
  119.     }

  120.     entry->long_count = 0;
  121.     entry->repeat_count = 0; // 清空计数器
  122.   }
  123. }

  124. // 按键扫描状态机,此接口放在10ms定时器即可
  125. void KeyScan(void)
  126. {
  127.   uint8_t i;
  128.   
  129.   for(i = 0; i < KEY_NUM; ++i)
  130.   {
  131.     key_scan(i);
  132.   }
  133. }

  134. //=============================================================================================================
  135. // 按键按下之后,会将值加入到队列中,我们读取队列数据,然后扫描列表匹配功能
  136. static void key1_cb(void);
  137. static void key2_cb(void);
  138. static void key3_cb(void);
  139. static void key4_cb(void);

  140. #define KEY1_CMD         1
  141. #define KEY2_CMD         2
  142. #define KEY3_CMD         3
  143. #define KEY4_CMD         4

  144. typedef struct
  145. {
  146.   uint8_t cmd;
  147.   void (* key_handle_cb)(void);
  148. } key_handle_t;

  149. static const key_handle_t key_entries[] =
  150. {
  151.   {KEY1_CMD, key1_cb},
  152.   {KEY2_CMD, key2_cb},
  153.   {KEY3_CMD, key3_cb},
  154.   {KEY4_CMD, key4_cb},
  155.   {0xFF, NULL  },
  156. };

  157. static void key1_cb(void)
  158. {
  159.   OutputHigh(LED0_VALUE);
  160. }

  161. static void key2_cb(void)
  162. {
  163.   OutputHigh(LED1_VALUE);
  164. }

  165. static void key3_cb(void)
  166. {
  167.   OutputToggle(LED0_VALUE);
  168. }

  169. static void key4_cb(void)
  170. {
  171.   OutputToggle(LED1_VALUE);
  172. }

  173. static void key_process(uint8_t event)
  174. {
  175.   const key_handle_t *entry;

  176.   for(entry = key_entries; entry->key_handle_cb; ++entry)
  177.   {
  178.     if(event == entry->cmd)
  179.     {
  180.       entry->key_handle_cb();
  181.       break;
  182.     }
  183.   }
  184. }

  185. // 按键按下之后的处理函数,此接口放在main死循环里面即可
  186. void KeyProcess(void)
  187. {
  188.   uint8_t i, event;
  189.   
  190.   if(KeyQueuePop(&key_queue, &event) == KEY_QUEUE_OK)
  191.   {
  192.     key_process(event);
  193.   }
  194. }  

复制代码
   这两个文件提供了两个接口,一个是KeyScan(),按键扫描状态机的实现,key_scan_items是关键,主要是填充key_t这个结构体。状态机:初始状态,按下,长按,弹起等,判断按下之后将值加入队列。    KeyProcess():这个函数放在main()循环里面,循环读队列得到加入队列时的值,然后扫描key_entries数组,然后调用其回调处理。


  1.   /* USER CODE BEGIN WHILE */
  2.   while (1)
  3.   {
  4.     // 实际项目中,KeyScan()放在定时器或者任务中
  5.     KeyScan();
  6.     KeyProcess();
  7.     HAL_Delay(10); // 我们目前还没有到定时器那一块,暂时适用HAL_Delay代替,
  8.     /* USER CODE END WHILE */

  9.     /* USER CODE BEGIN 3 */
  10.   }
  11.   /* USER CODE END 3 */
复制代码
在main的while(1)里面调用这两个函数即可,我这里只是测试,所以添加了一个HAL_Delay(10);编译,debug调试,然后就可以测试了。key0,key1是按下之后灯亮,key2,wk_up,短按取反,是长按之后灯1S闪烁一次。





正点原子逻辑分析仪DL16劲爆上市
回复

使用道具 举报

3

主题

9

帖子

0

精华

初级会员

Rank: 2

积分
98
金钱
98
注册时间
2019-5-27
在线时间
23 小时
 楼主| 发表于 2019-8-1 09:17:20 | 显示全部楼层
回复 支持 反对

使用道具 举报

4

主题

231

帖子

0

精华

高级会员

Rank: 4

积分
755
金钱
755
注册时间
2018-12-7
在线时间
131 小时
发表于 2019-8-1 09:24:36 | 显示全部楼层
图片看不见
回复 支持 反对

使用道具 举报

3

主题

9

帖子

0

精华

初级会员

Rank: 2

积分
98
金钱
98
注册时间
2019-5-27
在线时间
23 小时
 楼主| 发表于 2019-8-1 09:33:03 | 显示全部楼层

我这边能看见啊,会不会是网速的原因???
回复 支持 反对

使用道具 举报

0

主题

47

帖子

0

精华

高级会员

Rank: 4

积分
695
金钱
695
注册时间
2019-1-26
在线时间
67 小时
发表于 2019-8-11 12:00:11 | 显示全部楼层
牛!!!!!!!!!!!!!!!
回复 支持 反对

使用道具 举报

5

主题

179

帖子

0

精华

论坛元老

Rank: 8Rank: 8

积分
8195
金钱
8195
注册时间
2016-9-7
在线时间
1113 小时
发表于 2019-8-11 19:47:08 | 显示全部楼层
楼主,我也看不到图啊。
回复 支持 反对

使用道具 举报

0

主题

3

帖子

0

精华

新手入门

积分
7
金钱
7
注册时间
2019-8-14
在线时间
1 小时
发表于 2019-8-14 15:51:49 | 显示全部楼层
thanks
回复 支持 反对

使用道具 举报

0

主题

16

帖子

0

精华

初级会员

Rank: 2

积分
189
金钱
189
注册时间
2017-3-17
在线时间
47 小时
发表于 2020-3-28 10:51:10 | 显示全部楼层
谢谢楼主分享. 有一个问题: 如果希望按键像PC键盘一样, 能产生KeyUp, KeyDown事件, 该如何修改呢? 如果还需要KeyChanged事件, 又该怎么改呢? 谢谢!
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则



关闭

原子哥极力推荐上一条 /2 下一条

正点原子公众号

QQ|手机版|OpenEdv-开源电子网 ( 粤ICP备12000418号-1 )

GMT+8, 2025-5-3 06:15

Powered by OpenEdv-开源电子网

© 2001-2030 OpenEdv-开源电子网

快速回复 返回顶部 返回列表