【STM32F429开发板用户手册】第19章 STM32F429的GPIO应用之按键FIFO
最新教程下载:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255
第19章 STM32F429的GPIO应用之按键FIFO
本章教程为大家介绍STM32F429的GPIO应用之按键FIFO,这个方案已经在实际项目中千锤百炼,比较实用。
目录
第19章 STM32F429的GPIO应用之按键FIFO
19.1 初学者重要提示
19.2 按键硬件设计
19.2.1 硬件设计
19.2.2 GPIO内部结构分析按键
19.3 按键FIFO的驱动设计
19.3.1 按键FIFO的原理
19.3.2 按键FIFO的实现
19.3.3 按键检测程序分析
19.3.4 按键检测采用中断方式还是查询方式
19.4 按键板级支持包(bsp_key.c)
19.4.1 函数bsp_InitKeyHard
19.4.2 函数bsp_GetKey
19.4.3 函数bsp_KeyScan10ms
19.5 按键FIFO驱动移植和使用
19.6 实验例程设计框架
19.7 实验例程说明(MDK)
19.8 实验例程说明(IAR)
19.9 总结
19.1 初学者重要提示
- 学习本章节前,务必保证已经学习了第15,16和17章。
- 按键FIFO驱动扩展和移植更简单,组合键也更好用。支持按下、弹起、长按和组合键。
19.2 按键硬件设计
V6开发板有三个独立按键和一个五向摇杆,下面是三个独立按键的原理图:
注意,K1(S1)、K2(S2)和K3(S3)按键的上拉电阻是接在5V电压上,因为这三个按键被复用为PS/2键盘鼠标接口,而PS/2是需要5V供电的(注,V5和V6开发板做了PS/2复用)。实际测试,K1、K2、K3按键和PS/2键盘是可以同时工作的。
下面是五向摇杆的原理图:
通过这个硬件设计,有如下两个知识点为大家做介绍:
19.2.1 硬件设计
按键和CPU之间串联的电阻起保护作用。按键肯定是存在机械抖动的,开发板上面的硬件没有做硬件滤波处理,即使设计了硬件滤波电路,软件上还是需要进行滤波。
- 保护GPIO,避免软件错误将IO设置为输出,如果设置为低电平还好,如果设置输出的是高电平,按键按下会直接跟GND(低电平)连接,从而损坏MCU。
- 保护电阻也起到按键隔离作用,这些GPIO可以直接用于其它实验。
19.2.2 GPIO内部结构分析按键
详细的GPIO模式介绍,请参考第15章的15.3小节,本章仅介绍输入模式。下面我们通过一张图来简单介绍GPIO的结构。
红色的线条是GPIO输入通道的信号流向,作为按键检测IO,这些需要配置为浮空输入。按键已经做了5V上拉,因此GPIO内部的上下拉电阻都选择关闭状态。
19.3 按键FIFO的驱动设计
bsp_key按键驱动程序用于扫描独立按键,具有软件滤波机制,采用FIFO机制保存键值。可以检测如下事件:
- 按键按下。
- 按键弹起。
- 长按键。
- 长按时自动连发。
我们将按键驱动分为两个部分来介绍,一部分是FIFO的实现,一部分是按键检测的实现。
bsp_key.c 文件包含按键检测和按键FIFO的实现代码。
bsp.c 文件会调用bsp_InitKey()初始化函数。
bsp.c 文件会调用bsp_KeyScan按键扫描函数。
bsp_timer.c 中的Systick中断服务程序调用 bsp_RunPer10ms。
中断程序和主程序通过FIFO接口函数进行信息传递。
函数调用关系图:
19.3.1 按键FIFO的原理
FIFO是First Input First Output的缩写,先入先出队列。我们这里以5个字节的FIFO空间进行说明。Write变量表示写位置,Read变量表示读位置。初始状态时,Read = Write = 0。
我们依次按下按键K1,K2,那么FIFO中的数据变为:
如果Write!= Read,则我们认为有新的按键事件。
我们通过函数bsp_GetKey读取一个按键值进行处理后,Read变量变为1。Write变量不变。
我们继续通过函数bsp_GetKey读取3个按键值进行处理后,Read变量变为4。此时Read = Write = 4。两个变量已经相等,表示已经没有新的按键事件需要处理。
有一点要特别的注意,如果FIFO空间写满了,Write会被重新赋值为0,也就是重新从第一个字节空间填数据进去,如果这个地址空间的数据还没有被及时读取出来,那么会被后来的数据覆盖掉,这点要引起大家的注意。我们的驱动程序开辟了10个字节的FIFO缓冲区,对于一般的应用足够了。
设计按键FIFO主要有三个方面的好处:
- 可靠地记录每一个按键事件,避免遗漏按键事件。特别是需要实现按键的按下、长按、自动连发、弹起等事件时。
- 读取按键的函数可以设计为非阻塞的,不需要等待按键抖动滤波处理完毕。
- 按键FIFO程序在嘀嗒定时器中定期的执行检测,不需要在主程序中一直做检测,这样可以有效地降低系统资源消耗。
19.3.2 按键FIFO的实现
在bsp_key.h 中定了结构体类型KEY_FIFO_T。这只是类型声明,并没有分配变量空间。
#define KEY_FIFO_SIZE 10 typedef struct {uint8_t Buf[KEY_FIFO_SIZE]; /* 键值缓冲区 */uint8_t Read; /* 缓冲区读指针1 */uint8_t Write; /* 缓冲区写指针 */uint8_t Read2; /* 缓冲区读指针2 */ }KEY_FIFO_T;
在bsp_key.c 中定义s_tKey结构变量, 此时编译器会分配一组变量空间。
static KEY_FIFO_T s_tKey; /* 按键FIFO变量,结构体 */
一般情况下,只需要一个写指针Write和一个读指针Read。在某些情况下,可能有两个任务都需要访问按键缓冲区,为了避免键值被其中一个任务取空,我们添加了第2个读指针Read2。出厂程序在bsp_Idle()函数中实现的按K1K2组合键截屏的功能就使用的第2个读指针。
当检测到按键事件发生后,可以调用 bsp_PutKey函数将键值压入FIFO。下面的代码是函数的实现:
/* ********************************************************************************************************* * 函 数 名: bsp_PutKey * 功能说明: 将1个键值压入按键FIFO缓冲区。可用于模拟一个按键。 * 形 参: _KeyCode : 按键代码 * 返 回 值: 无 ********************************************************************************************************* */ void bsp_PutKey(uint8_t _KeyCode) {s_tKey.Buf[s_tKey.Write] = _KeyCode;if (++s_tKey.Write >= KEY_FIFO_SIZE){s_tKey.Write = 0;} }
这个bsp_PutKey函数除了被按键检测函数调用外,还可以被其他底层驱动调用。比如红外遥控器的按键检测,也共用了同一个按键FIFO。遥控器的按键代码和主板实体按键的键值统一编码,保持键值唯一即可实现两套按键同时控制程序的功能。
应用程序读取FIFO中的键值,是通过bsp_GetKey函数和bsp_GetKey2函数实现的。我们来看下这两个函数的实现:
/* ********************************************************************************************************* * 函 数 名: bsp_GetKey * 功能说明: 从按键FIFO缓冲区读取一个键值。 * 形 参: 无 * 返 回 值: 按键代码 ********************************************************************************************************* */ uint8_t bsp_GetKey(void) {uint8_t ret;if (s_tKey.Read == s_tKey.Write){return KEY_NONE;}else{ret = s_tKey.Buf[s_tKey.Read];if (++s_tKey.Read >= KEY_FIFO_SIZE){s_tKey.Read = 0;}return ret;} }/* ********************************************************************************************************* * 函 数 名: bsp_GetKey2 * 功能说明: 从按键FIFO缓冲区读取一个键值。独立的读指针。 * 形 参: 无 * 返 回 值: 按键代码 ********************************************************************************************************* */ uint8_t bsp_GetKey2(void) {uint8_t ret;if (s_tKey.Read2 == s_tKey.Write){return KEY_NONE;}else{ret = s_tKey.Buf[s_tKey.Read2];if (++s_tKey.Read2 >= KEY_FIFO_SIZE){s_tKey.Read2 = 0;}return ret;} }
返回值KEY_NONE = 0, 表示按键缓冲区为空,所有的按键时间已经处理完毕。按键的键值定义在 bsp_key.h文件,下面是具体内容:
typedef enum {KEY_NONE = 0, /* 0 表示按键事件 */KEY_1_DOWN, /* 1键按下 */KEY_1_UP, /* 1键弹起 */KEY_1_LONG, /* 1键长按 */KEY_2_DOWN, /* 2键按下 */KEY_2_UP, /* 2键弹起 */KEY_2_LONG, /* 2键长按 */KEY_3_DOWN, /* 3键按下 */KEY_3_UP, /* 3键弹起 */KEY_3_LONG, /* 3键长按 */KEY_4_DOWN, /* 4键按下 */KEY_4_UP, /* 4键弹起 */KEY_4_LONG, /* 4键长按 */KEY_5_DOWN, /* 5键按下 */KEY_5_UP, /* 5键弹起 */KEY_5_LONG, /* 5键长按 */KEY_6_DOWN, /* 6键按下 */KEY_6_UP, /* 6键弹起 */KEY_6_LONG, /* 6键长按 */KEY_7_DOWN, /* 7键按下 */KEY_7_UP, /* 7键弹起 */KEY_7_LONG, /* 7键长按 */KEY_8_DOWN, /* 8键按下 */KEY_8_UP, /* 8键弹起 */KEY_8_LONG, /* 8键长按 *//* 组合键 */KEY_9_DOWN, /* 9键按下 */KEY_9_UP, /* 9键弹起 */KEY_9_LONG, /* 9键长按 */KEY_10_DOWN, /* 10键按下 */KEY_10_UP, /* 10键弹起 */KEY_10_LONG, /* 10键长按 */ }KEY_ENUM;
必须按次序定义每个键的按下、弹起和长按事件,即每个按键对象(组合键也算1个)占用3个数值。我们推荐使用枚举enum, 不用#define的原因:
- 便于新增键值,方便调整顺序。
- 使用{ } 将一组相关的定义封装起来便于理解。
- 编译器可帮我们避免键值重复。
我们来看红外遥控器的键值定义,在bsp_ir_decode.h文件。因为遥控器按键和主板按键共用同一个FIFO,因此在这里我们先贴出这段定义代码,让大家有个初步印象。
/* 定义红外遥控器按键代码, 和bsp_key.h 的物理按键代码统一编码 */ typedef enum {IR_KEY_STRAT = 0x80,IR_KEY_POWER = IR_KEY_STRAT + 0x45,IR_KEY_MENU = IR_KEY_STRAT + 0x47, IR_KEY_TEST = IR_KEY_STRAT + 0x44,IR_KEY_UP = IR_KEY_STRAT + 0x40,IR_KEY_RETURN = IR_KEY_STRAT + 0x43,IR_KEY_LEFT = IR_KEY_STRAT + 0x07,IR_KEY_OK = IR_KEY_STRAT + 0x15,IR_KEY_RIGHT = IR_KEY_STRAT + 0x09,IR_KEY_0 = IR_KEY_STRAT + 0x16,IR_KEY_DOWN = IR_KEY_STRAT + 0x19,IR_KEY_C = IR_KEY_STRAT + 0x0D,IR_KEY_1 = IR_KEY_STRAT + 0x0C,IR_KEY_2 = IR_KEY_STRAT + 0x18,IR_KEY_3 = IR_KEY_STRAT + 0x5E,IR_KEY_4 = IR_KEY_STRAT + 0x08,IR_KEY_5 = IR_KEY_STRAT + 0x1C,IR_KEY_6 = IR_KEY_STRAT + 0x5A,IR_KEY_7 = IR_KEY_STRAT + 0x42,IR_KEY_8 = IR_KEY_STRAT + 0x52,IR_KEY_9 = IR_KEY_STRAT + 0x4A, }IR_KEY_E;
我们下面来看一段简单的应用。这个应用的功能是:主板K1键控制LED1指示灯;遥控器的POWER键和MENU键控制LED2指示灯。
#include "bsp.h"int main(void) {uint8_t ucKeyCode;bsp_Init();IRD_StartWork(); /* 启动红外解码 */while(1){bsp_Idle();/* 处理按键事件 */ucKeyCode = bsp_GetKey();if (ucKeyCode > 0){/* 有键按下 */switch (ucKeyCode){case KEY_DOWN_K1: /* K1键按下 */bsp_LedOn(1); /* 点亮LED1 */break;case KEY_UP_K1: /* K1键弹起 */bsp_LedOff(1); /* 熄灭LED1 */break; case IR_KEY_POWER: /* 遥控器POWER键按下 */bsp_LedOn(1); /* 点亮LED2 */break;case IR_KEY_MENU: /* 遥控器MENU键按下 */bsp_LedOff(1); /* 熄灭LED2 */break; case MSG_485_RX: /* 通信程序的发来的消息 *//* 执行通信程序的指令 */break;default:break;}}} }
看到这里,想必你已经意识到bsp_PutKey函数的强大之处了,可以将不相关的硬件输入设备统一为一个相同的接口函数。
在上面的应用程序中,我们特意添加了一段红色的代码来解说更高级的用法。485通信程序收到有效的命令后通过 bsp_PutKey(MSG_485_RX)函数可以通知APP应用程序进行进一步加工处理(比如显示接收成功)。这是一种非常好的任务间信息传递方式,它不会破坏程序结构。不必新增全局变量来做这种事情,你只需要添加一个键值代码。
对于简单的程序,可以借用按键FIFO来进行少量的信息传递。对于复杂的应用,我们推荐使用bsp_msg专门来做这种任务间的通信。因为bsp_msg除了传递消息代码外,还可以传递参数结构。
19.3.3 按键检测程序分析
在bsp_key.h 中定了结构体类型KEY_T。
#define KEY_COUNT 10 /* 按键个数, 8个独立建 + 2个组合键 */typedef struct {/* 下面是一个函数指针,指向判断按键手否按下的函数 */uint8_t (*IsKeyDownFunc)(void); /* 按键按下的判断函数,1表示按下 */uint8_t Count; /* 滤波器计数器 */uint16_t LongCount; /* 长按计数器 */uint16_t LongTime; /* 按键按下持续时间, 0表示不检测长按 */uint8_t State; /* 按键当前状态(按下还是弹起) */uint8_t RepeatSpeed; /* 连续按键周期 */uint8_t RepeatCount; /* 连续按键计数器 */ }KEY_T;
在bsp_key.c 中定义s_tBtn结构体数组变量。
static KEY_T s_tBtn[KEY_COUNT]; static KEY_FIFO_T s_tKey; /* 按键FIFO变量,结构体 */
每个按键对象都分配一个结构体变量,这些结构体变量以数组的形式存在将便于我们简化程序代码行数。
使用函数指针IsKeyDownFunc可以将每个按键的检测以及组合键的检测代码进行统一管理。
因为函数指针必须先赋值,才能被作为函数执行。因此在定时扫描按键之前,必须先执行一段初始化函数来设置每个按键的函数指针和参数。这个函数是 void bsp_InitKey(void)。它由bsp_Init()调用。
/* ********************************************************************************************************* * 函 数 名: bsp_InitKey * 功能说明: 初始化按键. 该函数被 bsp_Init() 调用。 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void bsp_InitKey(void) {bsp_InitKeyVar(); /* 初始化按键变量 */bsp_InitKeyHard(); /* 初始化按键硬件 */ }
下面是bsp_InitKeyVar函数的定义:
/* ********************************************************************************************************* * 函 数 名: bsp_InitKeyVar * 功能说明: 初始化按键变量 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ static void bsp_InitKeyVar(void) {uint8_t i;/* 对按键FIFO读写指针清零 */s_tKey.Read = 0;s_tKey.Write = 0;s_tKey.Read2 = 0;/* 给每个按键结构体成员变量赋一组缺省值 */for (i = 0; i < KEY_COUNT; i++){s_tBtn[i].LongTime = KEY_LONG_TIME; /* 长按时间 0 表示不检测长按键事件 */s_tBtn[i].Count = KEY_FILTER_TIME / 2; /* 计数器设置为滤波时间的一半 */s_tBtn[i].State = 0; /* 按键缺省状态,0为未按下 */s_tBtn[i].RepeatSpeed = 0; /* 按键连发的速度,0表示不支持连发 */s_tBtn[i].RepeatCount = 0; /* 连发计数器 */}/* 如果需要单独更改某个按键的参数,可以在此单独重新赋值 *//* 摇杆上下左右,支持长按1秒后,自动连发 */bsp_SetKeyParam(KID_JOY_U, 100, 6);bsp_SetKeyParam(KID_JOY_D, 100, 6);bsp_SetKeyParam(KID_JOY_L, 100, 6);bsp_SetKeyParam(KID_JOY_R, 100, 6); }
注意下一下 Count 这个成员变量,没有设置为0。为了避免主板上电的瞬间,检测到一个无效的按键按下或弹起事件。我们将这个滤波计数器的初值设置为正常值的1/2。bsp_key.h中定义了滤波时间和长按时间。
/*按键滤波时间50ms, 单位10ms。只有连续检测到50ms状态不变才认为有效,包括弹起和按下两种事件即使按键电路不做硬件滤波,该滤波机制也可以保证可靠地检测到按键事件 */ #define KEY_FILTER_TIME 5 #define KEY_LONG_TIME 100 /* 单位10ms, 持续1秒,认为长按事件 */
uint8_t KeyPinActive(uint8_t _id)(会调用函数KeyPinActive判断状态)函数就是最底层的GPIO输入状态判断函数。
/* ********************************************************************************************************* * 函 数 名: KeyPinActive * 功能说明: 判断按键是否按下 * 形 参: 无 * 返 回 值: 返回值1 表示按下(导通),0表示未按下(释放) ********************************************************************************************************* */ static uint8_t KeyPinActive(uint8_t _id) {uint8_t level;if ((s_gpio_list[_id].gpio->IDR & s_gpio_list[_id].pin) == 0){level = 0;}else{level = 1;}if (level == s_gpio_list[_id].ActiveLevel){return 1;}else{return 0;} }/* ********************************************************************************************************* * 函 数 名: IsKeyDownFunc * 功能说明: 判断按键是否按下。单键和组合键区分。单键事件不允许有其他键按下。 * 形 参: 无 * 返 回 值: 返回值1 表示按下(导通),0表示未按下(释放) ********************************************************************************************************* */ static uint8_t IsKeyDownFunc(uint8_t _id) {/* 实体单键 */if (_id < HARD_KEY_NUM){uint8_t i;uint8_t count = 0;uint8_t save = 255;/* 判断有几个键按下 */for (i = 0; i < HARD_KEY_NUM; i++){if (KeyPinActive(i)) {count++;save = i;}}if (count == 1 && save == _id){return 1; /* 只有1个键按下时才有效 */} return 0;}/* 组合键 K1K2 */if (_id == HARD_KEY_NUM + 0){if (KeyPinActive(KID_K1) && KeyPinActive(KID_K2)){return 1;}else{return 0;}}/* 组合键 K2K3 */if (_id == HARD_KEY_NUM + 1){if (KeyPinActive(KID_K2) && KeyPinActive(KID_K3)){return 1;}else{return 0;}}return 0; }
在使用GPIO之前,我们必须对GPIO进行配置,比如打开GPIO时钟,设置GPIO输入输出方向,设置上下拉电阻。下面是配置GPIO的代码,也就是bsp_InitKeyHard()函数:
/* ********************************************************************************************************* * 函 数 名: bsp_InitKeyHard * 功能说明: 配置按键对应的GPIO * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ static void bsp_InitKeyHard(void) { GPIO_InitTypeDef gpio_init;uint8_t i;/* 第1步:打开GPIO时钟 */ALL_KEY_GPIO_CLK_ENABLE();/* 第2步:配置所有的按键GPIO为浮动输入模式(实际上CPU复位后就是输入状态) */gpio_init.Mode = GPIO_MODE_INPUT; /* 设置输入 */gpio_init.Pull = GPIO_NOPULL; /* 上下拉电阻不使能 */gpio_init.Speed = GPIO_SPEED_FREQ_VERY_HIGH; /* GPIO速度等级 */for (i = 0; i < HARD_KEY_NUM; i++){gpio_init.Pin = s_gpio_list[i].pin;HAL_GPIO_Init(s_gpio_list[i].gpio, &gpio_init); } }
我们再来看看按键是如何执行扫描检测的。
按键扫描函数bsp_KeyScan10ms ()每隔10ms被执行一次。bsp_RunPer10ms函数在systick中断服务程序中执行。
我们再来看看按键是如何执行扫描检测的。 按键扫描函数bsp_KeyScan10ms ()每隔10ms被执行一次。bsp_RunPer10ms函数在systick中断服务程序中执行。 void bsp_RunPer10ms(void) {bsp_KeyScan10ms(); /* 扫描按键 */ }
bsp_KeyScan10ms ()函数的实现如下:
/* ********************************************************************************************************* * 函 数 名: bsp_KeyScan10ms * 功能说明: 扫描所有按键。非阻塞,被systick中断周期性的调用,10ms一次 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void bsp_KeyScan10ms(void) {uint8_t i;for (i = 0; i < KEY_COUNT; i++){bsp_DetectKey(i);} }
每隔10ms所有的按键GPIO均会被扫描检测一次。bsp_DetectKey函数实现如下:
/* ********************************************************************************************************* * 函 数 名: bsp_DetectKey * 功能说明: 检测一个按键。非阻塞状态,必须被周期性的调用。 * 形 参: IO的id, 从0开始编码 * 返 回 值: 无 ********************************************************************************************************* */ static void bsp_DetectKey(uint8_t i) {KEY_T *pBtn;pBtn = &s_tBtn[i];if (IsKeyDownFunc(i)){if (pBtn->Count < KEY_FILTER_TIME){pBtn->Count = KEY_FILTER_TIME;}else if(pBtn->Count < 2 * KEY_FILTER_TIME){pBtn->Count++;}else{if (pBtn->State == 0){pBtn->State = 1;/* 发送按钮按下的消息 */bsp_PutKey((uint8_t)(3 * i + 1));}if (pBtn->LongTime > 0){if (pBtn->LongCount < pBtn->LongTime){/* 发送按钮持续按下的消息 */if (++pBtn->LongCount == pBtn->LongTime){/* 键值放入按键FIFO */bsp_PutKey((uint8_t)(3 * i + 3));}}else{if (pBtn->RepeatSpeed > 0){if (++pBtn->RepeatCount >= pBtn->RepeatSpeed){pBtn->RepeatCount = 0;/* 常按键后,每隔RepeatSpeed * 10ms发送1个按键 */bsp_PutKey((uint8_t)(3 * i + 1));}}}}}}else{if(pBtn->Count > KEY_FILTER_TIME){pBtn->Count = KEY_FILTER_TIME;}else if(pBtn->Count != 0){pBtn->Count--;}else{if (pBtn->State == 1){pBtn->State = 0;/* 发送按钮弹起的消息 */bsp_PutKey((uint8_t)(3 * i + 2));}}pBtn->LongCount = 0;pBtn->RepeatCount = 0;} }
对于初学者,这个函数看起来比较吃力,我们拆分进行分析。
pBtn = &s_tBtn[i];
读取相应按键的结构体地址,程序里面每个按键都有自己的结构体。
static KEY_T s_tBtn[KEY_COUNT]; if (IsKeyDownFunc(i)) {这个里面执行的是按键按下的处理 } else {这个里面执行的是按键松手的处理或者按键没有按下的处理 }
执行函数IsKeyDownFunc(i)做按键状态判断。
/* ********************************************************************************** 下面这个if语句主要是用于按键滤波前给Count设置一个初值,前面说按键初始化的时候 已经设置了Count = KEY_FILTER_TIME/2 ********************************************************************************** */ if (pBtn->Count < KEY_FILTER_TIME) {pBtn->Count = KEY_FILTER_TIME; }/* ********************************************************************************** 这里实现KEY_FILTER_TIME时间长度的延迟 ********************************************************************************** */ else if(pBtn->Count < 2 * KEY_FILTER_TIME) {pBtn->Count++; } /* ********************************************************************************** 这里实现KEY_FILTER_TIME时间长度的延迟 ********************************************************************************** */ else { /* ********************************************************************************** 这个State变量是有其实际意义的,如果按键按下了,这里就将其设置为1,如果没有按下这个 变量的值就会一直是0,这样设置的目的可以有效的防止一种情况的出现:比如按键K1在某个 时刻检测到了按键有按下,那么它就会做进一步的滤波处理,但是在滤波的过程中,这个按键 按下的状态消失了,这个时候就会进入到上面第二步else语句里面,然后再做按键松手检测滤波 ,滤波结束后判断这个State变量,如果前面就没有检测到按下,这里就不会记录按键弹起。 ********************************************************************************** */if (pBtn->State == 0){pBtn->State = 1;/* 发送按钮按下的消息 */bsp_PutKey((uint8_t)(3 * i + 1));}if (pBtn->LongTime > 0){if (pBtn->LongCount < pBtn->LongTime){/* 发送按钮持续按下的消息 */if (++pBtn->LongCount == pBtn->LongTime){/* 键值放入按键FIFO */bsp_PutKey((uint8_t)(3 * i + 3));}}else{if (pBtn->RepeatSpeed > 0){if (++pBtn->RepeatCount >= pBtn->RepeatSpeed){pBtn->RepeatCount = 0;/* 长按键后,每隔10ms发送1个按键 */bsp_PutKey((uint8_t)(3 * i + 1));}}}} }
19.3.4 按键检测采用中断方式还是查询方式
检测按键有中断方式和GPIO查询方式两种。我们推荐大家用GPIO查询方式。
从裸机的角度分析
中断方式:中断方式可以快速地检测到按键按下,并执行相应的按键程序,但实际情况是由于按键的机械抖动特性,在程序进入中断后必须进行滤波处理才能判定是否有效的按键事件。如果每个按键都是独立的接一个IO引脚,需要我们给每个IO都设置一个中断,程序中过多的中断会影响系统的稳定性。中断方式跨平台移植困难。
查询方式:查询方式有一个最大的缺点就是需要程序定期的去执行查询,耗费一定的系统资源。实际上耗费不了多大的系统资源,因为这种查询方式也只是查询按键是否按下,按键事件的执行还是在主程序里面实现。
从OS的角度分析
中断方式:在OS中要尽可能少用中断方式,因为在RTOS中过多的使用中断会影响系统的稳定性和可预见性(抢占式调度的OS基本没有可预见性)。只有比较重要的事件处理需要用中断的方式。
查询方式:对于用户按键推荐使用这种查询方式来实现,现在的OS基本都带有CPU利用率的功能,这个按键FIFO占用的还是很小的,基本都在1%以下。
19.4 按键板级支持包(bsp_key.c)
按键驱动文件bsp_key.c主要实现了如下几个API:
- KeyPinActive
- IsKeyDownFunc
- bsp_InitKey
- bsp_InitKeyHard
- bsp_InitKeyVar
- bsp_PutKey
- bsp_GetKey
- bsp_GetKey2
- bsp_GetKeyState
- bsp_SetKeyParam
- bsp_ClearKey
- bsp_DetectKey
- bsp_DetectFastIO
- bsp_KeyScan10ms
- bsp_KeyScan1ms
所有这些函数在本章的19.3小节都进行了详细讲解,本小节主要是把需要用户调用的三个函数做个说明。
19.4.1 函数bsp_InitKeyHard
函数原型:
/* ********************************************************************************************************* * 函 数 名: bsp_InitKey * 功能说明: 初始化按键. 该函数被 bsp_Init() 调用。 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void bsp_InitKey(void) {bsp_InitKeyVar(); /* 初始化按键变量 */bsp_InitKeyHard(); /* 初始化按键硬件 */ }
函数描述:
此函数主要用于按键的初始化。
使用举例:
底层驱动初始化直接在bsp.c文件的函数bsp_Init里面调用即可。
19.4.2 函数bsp_GetKey
函数原型:
/* ********************************************************************************************************* * 函 数 名: bsp_GetKey * 功能说明: 从按键FIFO缓冲区读取一个键值。 * 形 参: 无 * 返 回 值: 按键代码 ********************************************************************************************************* */ uint8_t bsp_GetKey(void) {uint8_t ret;if (s_tKey.Read == s_tKey.Write){return KEY_NONE;}else{ret = s_tKey.Buf[s_tKey.Read];if (++s_tKey.Read >= KEY_FIFO_SIZE){s_tKey.Read = 0;}return ret;} }
函数描述:
此函数用于从FIFO中读取键值。
使用举例:
调用此函数前,务必优先调用函数bsp_InitKey进行初始化。
/* ********************************************************************************************************* * 函 数 名: main * 功能说明: c程序入口 * 形 参: 无 * 返 回 值: 错误代码(无需处理) ********************************************************************************************************* */ int main(void) {uint8_t ucKeyCode; /* 按键代码 */bsp_Init(); /* 硬件初始化 *//* 进入主程序循环体 */while (1){ /* 按键滤波和检测由后台systick中断服务程序实现,我们只需要调用bsp_GetKey读取键值即可。 */ucKeyCode = bsp_GetKey(); /* 读取键值, 无键按下时返回 KEY_NONE = 0 */if (ucKeyCode != KEY_NONE){switch (ucKeyCode){case KEY_DOWN_K1: /* K1键按下 */printf("K1键按下\r\n");break;case KEY_DOWN_K2: /* K2键按下 */printf("K2键按下\r\n");break;default:/* 其它的键值不处理 */break;}}} }
19.4.3 函数bsp_KeyScan10ms
函数原型:
/* ********************************************************************************************************* * 函 数 名: bsp_KeyScan10ms * 功能说明: 扫描所有按键。非阻塞,被systick中断周期性的调用,10ms一次 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void bsp_KeyScan10ms(void) {uint8_t i;for (i = 0; i < KEY_COUNT; i++){bsp_DetectKey(i);} }
函数描述:
此函数是按键的主处理函数,用于检测和存储按下、松手、长按等状态。
使用举例:
调用此函数前,务必优先调用函数bsp_InitKey进行初始化。
另外,此函数需要周期性调用,每10ms调用一次。
- 如果是裸机使用,将此函数放在bsp.c文件的bsp_RunPer10ms函数里面即可,这个函数是由滴答定时器调用的,也就是说,大家要使用按键,定时器的初始化函数bsp_InitTimer一定要调用。
- 如果是RTOS使用,需要开启一个10ms为周期的任务调用函数bsp_KeyScan10ms。
19.5 按键FIFO驱动移植和使用
按键移植步骤如下:
- 第1步:复制bsp_key.c和bsp_key.c到自己的工程。
- 第2步:根据自己使用的独立按键个数和组合键个数,修改几个地方。
#define HARD_KEY_NUM 8 /* 实体按键个数 */ #define KEY_COUNT (HARD_KEY_NUM + 2) /* 8个独立建 + 2个组合按键 */
- 第3步:根据使用的引脚时钟,修改下面函数:
/* 使能GPIO时钟 */ #define ALL_KEY_GPIO_CLK_ENABLE() { \__HAL_RCC_GPIOB_CLK_ENABLE(); \__HAL_RCC_GPIOC_CLK_ENABLE(); \__HAL_RCC_GPIOG_CLK_ENABLE(); \__HAL_RCC_GPIOH_CLK_ENABLE(); \__HAL_RCC_GPIOI_CLK_ENABLE(); \};
- 第4步:根据使用的具体引脚,修改如下函数,第3列参数低电平表示按下或者高电平表示按下:
/* GPIO和PIN定义 */ static const X_GPIO_T s_gpio_list[HARD_KEY_NUM] = {{GPIOI, GPIO_PIN_8, 0}, /* K1 */{GPIOC, GPIO_PIN_13, 0}, /* K2 */{GPIOH, GPIO_PIN_4, 0}, /* K3 */{GPIOG, GPIO_PIN_2, 0}, /* JOY_U */ {GPIOF, GPIO_PIN_10, 0}, /* JOY_D */{GPIOG, GPIO_PIN_3, 0}, /* JOY_L */ {GPIOG, GPIO_PIN_7, 0}, /* JOY_R */ {GPIOI, GPIO_PIN_11, 0}, /* JOY_OK */ };
- 第5步:根据使用的组合键个数,在函数IsKeyDownFunc里面添加相应个数的函数:
/* 组合键 K1K2 */if (_id == HARD_KEY_NUM + 0){if (KeyPinActive(KID_K1) && KeyPinActive(KID_K2)){return 1;}else{return 0;}}
第2行ID表示HARD_KEY_NUM + 0的组合键,HARD_KEY_NUM + 1表示下一个组合键,以此类推。
另外就是,函数KeyPinActive的参数是表示检测哪两个按键,设置0的时候表示第4步里面的第1组按键,设置为1表示第2组按键,以此类推。
- 第6步:主要用到HAL库的GPIO驱动文件,简单省事些可以添加所有HAL库.C源文件进来。
- 第7步:移植完整,应用方法看本章节配套例子即可。
特别注意,别忘了每10ms调用一次按键检测函数bsp_KeyScan10ms。
19.6 实验例程设计框架
通过程序设计框架,让大家先对配套例程有一个全面的认识,然后再理解细节,本次实验例程的设计框架如下:
第1阶段,上电启动阶段:
- 这部分在第14章进行了详细说明。
第2阶段,进入main函数:
- 第1部分,硬件初始化,主要是HAL库、系统时钟、滴答定时器、按键等。
- 第2部分,应用程序设计部分,实现了一个按键应用。
- 第3部分,按键扫描程序每10ms在滴答定时中断执行一次。
19.7 实验例程说明(MDK)
配套例子:
V6-003_按键检测(软件滤波,FIFO机制)
实验目的:
- 学习按键的按下,弹起,长按和组合键的实现。
实验内容:
- 启动一个自动重装软件定时器,每100ms翻转一次LED2。
实验操作:
- 3个独立按键和5向摇杆按下时均有串口消息打印。
- 5向摇杆的左键和右键长按时,会有连发的串口消息。
- 独立按键K1和K2按键按下,串口打印消息。
上电后串口打印的信息:
波特率 115200,数据位 8,奇偶校验位无,停止位 1
程序设计:
系统栈大小分配:
硬件外设初始化
硬件外设的初始化是在 bsp.c 文件实现:
/* ********************************************************************************************************* * 函 数 名: bsp_Init * 功能说明: 初始化所有的硬件设备。该函数配置CPU寄存器和外设的寄存器并初始化一些全局变量。只需要调用一次 * 形 参:无 * 返 回 值: 无 ********************************************************************************************************* */ void bsp_Init(void) {/* STM32H429 HAL 库初始化,此时系统用的还是F429自带的16MHz,HSI时钟:- 调用函数HAL_InitTick,初始化滴答时钟中断1ms。- 设置NVIV优先级分组为4。*/HAL_Init();/* 配置系统时钟到168MHz- 切换使用HSE。- 此函数会更新全局变量SystemCoreClock,并重新配置HAL_InitTick。*/SystemClock_Config();/* Event Recorder:- 可用于代码执行时间测量,MDK5.25及其以上版本才支持,IAR不支持。- 默认不开启,如果要使能此选项,务必看V5开发板用户手册第8章*/ #if Enable_EventRecorder == 1 /* 初始化EventRecorder并开启 */EventRecorderInitialize(EventRecordAll, 1U);EventRecorderStart(); #endifbsp_InitKey(); /* 按键初始化,要放在滴答定时器之前,因为按钮检测是通过滴答定时器扫描 */bsp_InitTimer(); /* 初始化滴答定时器 */bsp_InitUart(); /* 初始化串口 */bsp_InitExtIO(); /* 初始化扩展IO */bsp_InitLed(); /* 初始化LED */ }
每10ms调用一次按键检测:
按键检测是在滴答定时器中断里面实现,每10ms执行一次检测。
/* ********************************************************************************************************* * 函 数 名: bsp_RunPer10ms * 功能说明: 该函数每隔10ms被Systick中断调用1次。详见 bsp_timer.c的定时中断服务程序。一些处理时间要求 * 不严格的任务可以放在此函数。比如:按键扫描、蜂鸣器鸣叫控制等。 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void bsp_RunPer10ms(void) {bsp_KeyScan10ms(); }
主功能:
主功能的实现主要分为两部分:
- 启动一个自动重装软件定时器,每100ms翻转一次LED2
- 按键消息的读取,检测到按下后,做串口打印。
/* ********************************************************************************************************* * 函 数 名: main * 功能说明: c程序入口 * 形 参: 无 * 返 回 值: 错误代码(无需处理) ********************************************************************************************************* */ int main(void) {uint8_t ucKeyCode; /* 按键代码 */bsp_Init(); /* 硬件初始化 */PrintfLogo(); /* 打印例程名称和版本等信息 */PrintfHelp(); /* 打印操作提示 */bsp_StartAutoTimer(0, 100); /* 启动1个100ms的自动重装的定时器 *//* 进入主程序循环体 */while (1){bsp_Idle(); /* 这个函数在bsp.c文件。用户可以修改这个函数实现CPU休眠和喂狗 *//* 判断定时器超时时间 */if (bsp_CheckTimer(0)) {/* 每隔100ms 进来一次 */ bsp_LedToggle(2); }/* 按键滤波和检测由后台systick中断服务程序实现,我们只需要调用bsp_GetKey读取键值即可。 */ucKeyCode = bsp_GetKey(); /* 读取键值, 无键按下时返回 KEY_NONE = 0 */if (ucKeyCode != KEY_NONE){switch (ucKeyCode){case KEY_DOWN_K1: /* K1键按下 */printf("K1键按下\r\n");break;case KEY_UP_K1: /* K1键弹起 */printf("K1键弹起\r\n");break;case KEY_DOWN_K2: /* K2键按下 */printf("K2键按下\r\n");break;case KEY_UP_K2: /* K2键弹起 */printf("K2键弹起\r\n");break;case KEY_DOWN_K3: /* K3键按下 */printf("K3键按下\r\n");break;case KEY_UP_K3: /* K3键弹起 */printf("K3键弹起\r\n");break;case JOY_DOWN_U: /* 摇杆UP键按下 */printf("摇杆上键按下\r\n");break;case JOY_DOWN_D: /* 摇杆DOWN键按下 */printf("摇杆下键按下\r\n");break;case JOY_DOWN_L: /* 摇杆LEFT键按下 */printf("摇杆左键按下\r\n");break;case JOY_LONG_L: /* 摇杆LEFT键长按 */printf("摇杆左键长按\r\n");break;case JOY_DOWN_R: /* 摇杆RIGHT键按下 */printf("摇杆右键按下\r\n");break;case JOY_LONG_R: /* 摇杆RIGHT键长按 */printf("摇杆右键长按\r\n");break;case JOY_DOWN_OK: /* 摇杆OK键按下 */printf("摇杆OK键按下\r\n");break;case JOY_UP_OK: /* 摇杆OK键弹起 */printf("摇杆OK键弹起\r\n");break;case SYS_DOWN_K1K2: /* 摇杆OK键弹起 */printf("K1和K2组合键按下\r\n");break;default:/* 其它的键值不处理 */break;}}} }
19.8 实验例程说明(IAR)
配套例子:
V6-003_按键检测(软件滤波,FIFO机制)
实验目的:
- 学习按键的按下,弹起,长按和组合键的实现。
实验内容:
- 启动一个自动重装软件定时器,每100ms翻转一次LED2。
实验操作:
- 3个独立按键和5向摇杆按下时均有串口消息打印。
- 5向摇杆的左键和右键长按时,会有连发的串口消息。
- 独立按键K1和K2按键按下,串口打印消息。
上电后串口打印的信息:
波特率 115200,数据位 8,奇偶校验位无,停止位 1
程序设计:
系统栈大小分配:
硬件外设初始化
硬件外设的初始化是在 bsp.c 文件实现:
/* ********************************************************************************************************* * 函 数 名: bsp_Init * 功能说明: 初始化所有的硬件设备。该函数配置CPU寄存器和外设的寄存器并初始化一些全局变量。只需要调用一次 * 形 参:无 * 返 回 值: 无 ********************************************************************************************************* */ void bsp_Init(void) {/* STM32H429 HAL 库初始化,此时系统用的还是F429自带的16MHz,HSI时钟:- 调用函数HAL_InitTick,初始化滴答时钟中断1ms。- 设置NVIV优先级分组为4。*/HAL_Init();/* 配置系统时钟到168MHz- 切换使用HSE。- 此函数会更新全局变量SystemCoreClock,并重新配置HAL_InitTick。*/SystemClock_Config();/* Event Recorder:- 可用于代码执行时间测量,MDK5.25及其以上版本才支持,IAR不支持。- 默认不开启,如果要使能此选项,务必看V5开发板用户手册第8章*/ #if Enable_EventRecorder == 1 /* 初始化EventRecorder并开启 */EventRecorderInitialize(EventRecordAll, 1U);EventRecorderStart(); #endifbsp_InitKey(); /* 按键初始化,要放在滴答定时器之前,因为按钮检测是通过滴答定时器扫描 */bsp_InitTimer(); /* 初始化滴答定时器 */bsp_InitUart(); /* 初始化串口 */bsp_InitExtIO(); /* 初始化扩展IO */bsp_InitLed(); /* 初始化LED */ }
每10ms调用一次按键检测:
按键检测是在滴答定时器中断里面实现,每10ms执行一次检测。
/* ********************************************************************************************************* * 函 数 名: bsp_RunPer10ms * 功能说明: 该函数每隔10ms被Systick中断调用1次。详见 bsp_timer.c的定时中断服务程序。一些处理时间要求 * 不严格的任务可以放在此函数。比如:按键扫描、蜂鸣器鸣叫控制等。 * 形 参: 无 * 返 回 值: 无 ********************************************************************************************************* */ void bsp_RunPer10ms(void) {bsp_KeyScan10ms(); }
主功能:
主功能的实现主要分为两部分:
- 启动一个自动重装软件定时器,每100ms翻转一次LED2
- 按键消息的读取,检测到按下后,做串口打印。
/* ********************************************************************************************************* * 函 数 名: main * 功能说明: c程序入口 * 形 参: 无 * 返 回 值: 错误代码(无需处理) ********************************************************************************************************* */ int main(void) {uint8_t ucKeyCode; /* 按键代码 */bsp_Init(); /* 硬件初始化 */PrintfLogo(); /* 打印例程名称和版本等信息 */PrintfHelp(); /* 打印操作提示 */bsp_StartAutoTimer(0, 100); /* 启动1个100ms的自动重装的定时器 *//* 进入主程序循环体 */while (1){bsp_Idle(); /* 这个函数在bsp.c文件。用户可以修改这个函数实现CPU休眠和喂狗 *//* 判断定时器超时时间 */if (bsp_CheckTimer(0)) {/* 每隔100ms 进来一次 */ bsp_LedToggle(2); }/* 按键滤波和检测由后台systick中断服务程序实现,我们只需要调用bsp_GetKey读取键值即可。 */ucKeyCode = bsp_GetKey(); /* 读取键值, 无键按下时返回 KEY_NONE = 0 */if (ucKeyCode != KEY_NONE){switch (ucKeyCode){case KEY_DOWN_K1: /* K1键按下 */printf("K1键按下\r\n");break;case KEY_UP_K1: /* K1键弹起 */printf("K1键弹起\r\n");break;case KEY_DOWN_K2: /* K2键按下 */printf("K2键按下\r\n");break;case KEY_UP_K2: /* K2键弹起 */printf("K2键弹起\r\n");break;case KEY_DOWN_K3: /* K3键按下 */printf("K3键按下\r\n");break;case KEY_UP_K3: /* K3键弹起 */printf("K3键弹起\r\n");break;case JOY_DOWN_U: /* 摇杆UP键按下 */printf("摇杆上键按下\r\n");break;case JOY_DOWN_D: /* 摇杆DOWN键按下 */printf("摇杆下键按下\r\n");break;case JOY_DOWN_L: /* 摇杆LEFT键按下 */printf("摇杆左键按下\r\n");break;case JOY_LONG_L: /* 摇杆LEFT键长按 */printf("摇杆左键长按\r\n");break;case JOY_DOWN_R: /* 摇杆RIGHT键按下 */printf("摇杆右键按下\r\n");break;case JOY_LONG_R: /* 摇杆RIGHT键长按 */printf("摇杆右键长按\r\n");break;case JOY_DOWN_OK: /* 摇杆OK键按下 */printf("摇杆OK键按下\r\n");break;case JOY_UP_OK: /* 摇杆OK键弹起 */printf("摇杆OK键弹起\r\n");break;case SYS_DOWN_K1K2: /* 摇杆OK键弹起 */printf("K1和K2组合键按下\r\n");break;default:/* 其它的键值不处理 */break;}}} }
19.9 总结
这个方案在实际项目中已经经过千锤百炼,大家可以放心使用。建议熟练掌握其用法。
【STM32F429开发板用户手册】第19章 STM32F429的GPIO应用之按键FIFO相关推荐
- 【STM32F407开发板用户手册】第19章 STM32F407的GPIO应用之按键FIFO
最新教程下载:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255 第19章 STM32F407的GPIO应用之按键FIF ...
- 【STM32H7教程】第19章 STM32H7的GPIO应用之按键FIFO
完整教程下载地址:http://www.armbbs.cn/forum.php?mod=viewthread&tid=86980 第19章 STM32H7的GPIO应用之按键FIF ...
- 【STM32H7教程】第19章 STM32H7的GPIO应用之按键FIFO
完整教程下载地址:http://forum.armfly.com/forum.php?mod=viewthread&tid=86980 第19章 STM32H7的GPIO应用之按键 ...
- 【STM32F429开发板用户手册】第46章 STM32F429的DMA2D应用之刷色块,位图和Alpha混合
最新教程下载:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255 第46章 STM32F429的DMA2D应用之刷色块, ...
- 【STM32F429开发板用户手册】第33章 STM32F429的SPI总线应用之驱动DAC8563(双通道,16bit分辨率,正负10V)
最新教程下载:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255 第33章 STM32F429的SPI总线应用之驱动DA ...
- 【STM32F429开发板用户手册】第26章 STM32F429的定时器应用之TIM1-TIM14的中断实现
最新教程下载:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255 第26章 STM32F429的定时器应用之TIM1-T ...
- 【STM32F429开发板用户手册】第35章 STM32F429的FMC总线应用之驱动AD7606(8通道同步采样, 16bit, 正负10V)
最新教程下载:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255 第35章 STM32F429的FMC总线应用之驱动AD ...
- 【STM32F429开发板用户手册】第41章 STM32F429的LTDC应用之LCD汉字显示和2D图形显示
最新教程下载:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255 第41章 STM32F429的LTDC应用之LCD汉字 ...
- 【STM32F429开发板用户手册】第18章 STM32F429的GPIO应用之跑马灯
最新教程下载:http://www.armbbs.cn/forum.php?mod=viewthread&tid=93255 第18章 STM32F429的GPIO应用之跑马灯 本 ...
最新文章
- MySQL 里的 Timestrap 和 DateTime 和 Java 中的 Date
- 好程序员web前端CSS选择符(选择器):表示要定义样式的对象
- ECharts x时间轴不连续实现
- linux在所有文件中查找某一个字符
- 看风水用什么罗盘最好_兰花用什么花盆栽植最好?
- React开发(166):ant design form 设置值
- 最短路径 一 Dijkstra 模板(O(n^2))
- python 获取照片拍摄时间_Python实现获取照片拍摄日期并重命名的方法
- html5三角函数怎么用,HTML5(五)html5<canvas路径和三角函数的故事>(下)
- 高手对中科院考博英语的体会
- FreeRTOS-TaskNotify
- 适用于 Windows 10 的触摸板手势
- 如何用艺术字制作水印?教你一招快速制作专属水印
- 2021-04-15 三级管npn和pnp的区别
- 运行时 Entry name .... .. collided
- 在vscode中使用latex高效书写论文教程
- 禁止input密码自动填充及浏览器记住密码完整解决方案
- word怎么删除空白页,Word Mac删除空白页面的图文方法
- Unbuntu远程电脑死机怎么解决
- 激光雷达赛道“白刃战”?硅光芯片级FMCW技术进入量产周期