[分享][交流]发一个通用按键模块,简单易用 [2014-3-24 Update]
本帖最后由 Gorgon_Meducer 于 2014-3-24 22:51 编辑傻孩子工作室作品
这是一个通用的按键模块,使用简单!支持最多255个按键,每个按键都支持包括按下检测、
松手检测、长按、连按的效果。内置去抖滤波处理,内置按键队列。而且是平台无关,全状
态开发的!
更新日志
- 2014-3-24
a. 修改key_queue接口:增加中断保护修改注释风格
b. 更新Demo工程,并将key_task放入到system tick 异常处理程序中
下面我们开始进入正题:
0、 准备工作:
你现在需要一个有串口、有两个独立按键、有独立LED指示灯的单片机系统。为了体现平台无
关,请参考文档搭建平台,该平台提供了串口收发函数、LED控制
接口函数、独立按键读取函数。这样我们就拥有一个共同讨论的基础。
我现在使用的是lpc1227的板子,提供了一个完整基于lpc1227的范例,但是基于之前搭建的平
台,代码移植起来是非常容易的。
在平台搭建完成的基础上,你仅仅需要提供一个获取按键键值函数就可以使用按键模块了。
1、 解压模块
将按键模块源代码下载下来,你可以看到里面有一个app_cfg.h文件,一个oopc.h文件,一个
key文件夹,将其解压到和工程同级的目录中,同时在你的工程中包含源文件key.c(key文件内)
和key_queue.c(key的子文件夹key_queue内)。
2、编写按键获取键值函数
你需要编写自己的按键键值获取函数。例如我在main.c中编写好了该函数:#define KEY_1 1
#define KEY_2 2
/*************************************************************************
* Function Name: get_key_scan_value
* Parameters: void
* Return: return key value
*
* Description:
*
*************************************************************************/
uint8_t get_key_scan_value(void)
{
if (IS_KEY1_DOWN()) { //该宏在平台搭建中有说明
return KEY_1;
}
if (IS_KEY2_DOWN()) { //该宏在平台搭建中有说明
return KEY_2;
}
return KEY_NULL;
}
这里只是使用两个简单的独立按键。在实际工程应用中,可以根据你的情况来调整。需要
注意的是返回的键值不能为0 (它已经被KEY_NULL使用了)。
3、注册按键获取键值函数
打开主目录下的app_cfg.h,添加代码,注意宏GET_KEY_SCAN_VALUE(),示例如下:#ifndef __APP_CFG_H__
#define __APP_CFG_H__
#include <stdbool.h>
#include <stddef.h>
#include "oopc.h
…
#define KEY_QUEUE_SIZE 10
#define KEY_COUNT 100
#define KEY_LONG_TIME 30000
#define KEY_REPEAT_TIME 20000
//在这里声明自己编写的按键键值获取函数
extern uint8_t get_key_scan_value(void);
//用宏来注册
#define GET_KEY_SCAN_VALUE() get_key_scan_value()
#endif /* __APP_CFG_H__ */
4、接口使用说明
经过以上的步骤,按键模块已经移植完成了。使用时,在main.c函数中包含头文件key.h:#include ".\key\key.h"然后调用接口函数即可。接口函数是什么?有哪些?
嗯,我们先来看看接口头文件”key.h”:#ifndef __KEY_H__
#define __KEY_H__
#include ".\app_cfg.h"
#include "key_interface.h"
#define KEY_NULL 0
extern void init_key_task(void);
extern bool key_task(void);
extern bool get_key(key_event_t* ptKey);
#endif /* __KEY_H__ */
可以看到,该模块提供了三个函数,这就是接口函数!介绍如下:
void init_key_task(void)为初始化按键模块函数,只需要在初始化时调用一次即可。
bool key_task(void)为按键任务函数,在超级循环或者定时器中断处理程序中被周期性的调用int main(void)
{
…
init_key_task();
…
while(1) {
…
//需要在超级循环或者定时器中断处理程序中被周期性的调用
key_task();
…
}
}
bool get_key(key_event_t* ptKey) 用来获取按键:
当返回false时,表示没有获取到有效按键;
当返回true时,表示获取到有效按键,通过传入的参数读取按键信息。
结构体key_event_t定义在key_interface.h中,该结构体包含一个按键的全部信息:键值和按键事件类型。
key_interface.h
typedef struct{
uint8_t chKeyValue;
key_status_t tEvent;
}key_event_t;
简单实用范例:
do {
key_event_t tKey;
if(!get_key(&tKey)) {
break;
}
if ((KEY_1 == tKey.chKeyValue) && (KEY_LONG_PRESSED == tKey.tEvent)) {
//! 识别KEY_1的长按键事件
…
}
} while(false)
说了那么多,还没有上代码呢,请看下面的DEMO:
#include “key.h”
/*************************************************************************
* Function Name: main
* Parameters: none
*
* Return: none
*
* Description: main
*
*************************************************************************/
int main(void)
{
…
// init_key_task ()初始化时调用一次即可
init_key_task ();
…
while(1) {
// key_task()需要在超级循环或者定时器中断处理程序中被周期性的调用
key_task();
//在这里添加你的按键应用代码
key_app();
…
}
}
/*************************************************************************
* Function Name: key_app
* Parameters: void
* Return: return FSM state
*
* Description:
*
*************************************************************************/
static bool key_app(void)
{
static uint8_t s_chState = KEY_APP_START;
static key_event_t s_tKey = {KEY_NULL, KEY_UP};
switch (s_chState) {
case KEY_APP_START:
s_chState = KEY_APP_GET;
//break;
case KEY_APP_GET:
if (get_key(&s_tKey)) { //调用获取按键,根据按键键值和事件类型不同做不同的处理
if (KEY_1 == s_tKey.chKeyValue) {
if (KEY_DOWN == s_tKey.tEvent) {
s_chState = KEY_APP_DOWN;
} else if (KEY_UP == s_tKey.tEvent) {
s_chState = KEY_APP_UP;
} else if (KEY_PRESSED == s_tKey.tEvent) {
s_chState = KEY_APP_PRESSED;
} else if (KEY_LONG_PRESSED == s_tKey.tEvent) {
s_chState = KEY_APP_LONG_PRESSED;
} else if (KEY_REPEAT == s_tKey.tEvent) {
s_chState = KEY_APP_REPEAT;
}
} else if (KEY_2 == s_tKey.chKeyValue) {
if (KEY_DOWN == s_tKey.tEvent) {
s_chState = KEY_APP_DOWN2;
} else if (KEY_UP == s_tKey.tEvent) {
s_chState = KEY_APP_UP2;
} else if (KEY_PRESSED == s_tKey.tEvent) {
s_chState = KEY_APP_PRESSED2;
} else if (KEY_LONG_PRESSED == s_tKey.tEvent) {
s_chState = KEY_APP_LONG_PRESSED2;
} else if (KEY_REPEAT == s_tKey.tEvent) {
s_chState = KEY_APP_REPEAT2;
}
}
}
break;
case KEY_APP_DOWN:
if (!print_key1_down()) { //这是一个通过串口打印字符串"KEY1 DOWN\r\n"的状态机
KEY_APP_RESET_FSM();
return false;
}
break;
case KEY_APP_UP:
if (!print_key1_up()) { //这是一个通过串口打印字符串"KEY1 UP\r\n"的状态机
KEY_APP_RESET_FSM();
return false;
}
break;
…
//省略一部分代码
}
}
完整的范例,可以点这里下载:
效果图:
4、参数调整
经过上面的几个步骤,按键模块已经可以使用了,当然不同MCU的速度不同,所以提供
了以下宏来调整参数,使之更符合你的情况。
//内置队列大小
#define KEY_QUEUE_SIZE 10
//按键连续检测次数
#define CHECK_KEY_COUNT 20
//长按计数
#define KEY_LONG_TIME 30000
//连击计数
#define KEY_REPEAT_TIME 20000
5、注意事项:
A.本模块是基于全状态开发的,占用处理器时间及少,同时key_task()函数也需要在超
级循环或者定时器中断处理程序中被周期性的调用。
B.有效键值不能为0 (KYE_NULL已经占用了键值0)
个人水平有限,欢迎拍砖!
数据流图
状态图
高手,真的是高手,果然是精华帖,先MARK一下,下次再来看{:smile:} 好贴,顶起 呵呵,不错。我已经用了好几年了。和你的方式一模一样,调用接口也一致,不同的是我把键值和按键状态封装在一个字节里。这样实际按键数量只支持16个(4bits for key value , 4 bits for key event)
如果和你那样,分开两个字节,也就和你一样,可以支持255键了。
接口:
调用方式:
方式和楼主位基本一致,就不传源码了。 没有矩阵的吗?单按键口IO耗费比较大呀 mcu_lover 发表于 2013-7-17 19:38
呵呵,不错。我已经用了好几年了。和你的方式一模一样,调用接口也一致,不同的是我把键值和按键状态封装在 ...
这都是傻孩子老师教的,算是培训的阶段总结吧 mark,学习
bbsview 发表于 2013-7-17 19:39
没有矩阵的吗?单按键口IO耗费比较大呀
无论是独立按键还是矩阵按键,都可以使用这个模块,它们只有获取键值函数不一样,这是由用户自己编写的 mcu_lover 发表于 2013-7-17 19:38 static/image/common/back.gif
呵呵,不错。我已经用了好几年了。和你的方式一模一样,调用接口也一致,不同的是我把键值和按键状态封装在 ...
这代码写得好漂亮啊。
按键状态3个位不够用吗 本帖最后由 zhanghuhhhhh 于 2013-7-17 20:36 编辑
买了傻孩子的书。发现函数和变量名,没有楼上的mcu_lover 写的好。有可能是写大的工程多了,养成的习惯,希望傻孩子,以后写书,学学杜洋或其它作者,代码写的争取小孩都明白,变量名中有的下划线,没有必要让初学者看啊。大工程有可能是必要的。
这个通用模块很好,只是如果函数名,变量名,短点,易懂点。
结构体尽量不用就更好了。
名子短,用于工程中出问题,移植者会再加长的。 学习了,楼主和mcu_lover的方法都不错,话说很期待mcu_lover博文更新。 SNOOKER 发表于 2013-7-17 20:22 static/image/common/back.gif
这代码写得好漂亮啊。
按键状态3个位不够用吗
按键识别如下几种状态,每种状态分别有一位标志对应:
按键按下
按键弹起
按键长按
按键连按
共需要4 bits。 zhanghuhhhhh 发表于 2013-7-17 20:28 static/image/common/back.gif
买了傻孩子的书。发现函数和变量名,没有楼上的mcu_lover 写的好。有可能是写大的工程多了,养成的习惯, ...
额,不知道如何表述,所以翻了翻代码大全
我很同意这句话:为变量命名最重要的考虑事项是,该名字要完全,准确地描述出该变量所代表的事物。
所以变量名我倒是不觉得长,倒是不易懂这的确是不够好,希望指出,我好改进
作为模块我觉得避免和以后命名冲突是很重要的,一个模块写好后就不应该随便修改
结构体我倒是觉得是一个很好的东西。。。 zhanghuhhhhh 发表于 2013-7-17 20:28 static/image/common/back.gif
买了傻孩子的书。发现函数和变量名,没有楼上的mcu_lover 写的好。有可能是写大的工程多了,养成的习惯, ...
结构体和指针是C的精华,不用的话很多东西没办法实现,只能拳打脚踢单打独斗了 呵呵。我感觉学了一点入门了以后,那些看似复杂了点的代码实际帮了我们很大的忙,看起来复杂,用起来很方便。 围观学习。。。 zhanghuhhhhh 发表于 2013-7-17 20:28 static/image/common/back.gif
买了傻孩子的书。发现函数和变量名,没有楼上的mcu_lover 写的好。有可能是写大的工程多了,养成的习惯, ...
谢谢你的建议,我书中的编码风格已经是五年前的东西了,现在已经完全不同了。
萝卜青菜各有所爱吧。我还是比较喜欢长命名风格。另外,所谓模块,就是写好了
原则上不会修改的,这次的模块正是我培训学员如何进行模块化封装的一个结果。
需要发生变化的部分比如按键扫瞄,各类参数的宏配置输入等等都处理好了。使用
的时候是绝对不允许修改模块中的代码。坚持这一点,你才能体会出模块化的好处,
也才能自己也进行面向接口开发的实践。 各人面向的价层不一样吧。
杜洋的书里的代码,不用宏,不用指针,不用结构体。小学生也跟着学。淘宝双皇冠。
初学者,看你的书有点难度,估计水平高了。就好点了。
新书什么时候出来啊。
我再买来学习一下。 发一下我的按键程序吧。
是用本坛的“三行按键”基础上改的,阿莫论坛,就是神!
互相学习,提高吧。
#include "../hfile/config.h"
uint16 GCountLongPressTime;
uint8 GTrigger=0;
uint8 GHold=0;
uint8 g_KeyValue=0x00;
uint8 g_LongPressKeyValue=0x00;
/**********************************************************************
//this routine maybe put in the mainlop.no need decrease counter.
//it take the loop as a delay!
**********************************************************************/
uint8 GetPinValue(void )
{
uint8 KeyPort=0xff;
uint8 Key0value=0x00;
uint8 Key1value=0x00;
uint8 Key2value=0x00;
uint8 Key3value=0x00;
//if(0==PIN(B,3)) #define PIN(m,n)
if(
0==(
!(uint8)(GET_BIT(DDRB).bit3=0)&&\
(GET_BIT(PORTB).bit3=1)&&\
GET_BIT(PINB).bit3
//(uint8)( GET_BIT(PORTB).bit3=1)&&\//
)
)
Key0value=0x01;
if(0==PIN(D,4))
Key1value=0x02;
if(0==PIN(D,3))
Key2value=0x04;
if(0==PIN(B,2))
Key3value=0x08;
KeyPort=KeyPort&(~Key0value)&(~Key1value)&(~Key2value)&(~Key3value);
return KeyPort;
}
void ThreeLineKeyRead(void)//读按键
{
uint8 virtualPinValue=GetPinValue();
uint8 ReadData=0;
ReadData=virtualPinValue^0xff;
GTrigger=ReadData&(ReadData^GHold);
GHold=ReadData;
}
voidWhichKeyIsShortPress(void)//短按键
{
if(GTrigger&0x01)
{
g_KeyValue=KEYCH;//左数第1个按键
}
if(GTrigger&0x02)
{
g_KeyValue=KEYADD;//左数第2个按键
}
if(GTrigger&0x04)
{
g_KeyValue=KEYDEC;//左数第3个按键
}
if(GTrigger&0x08)
{
g_KeyValue=KEYSET;//左数第4个按键
}
if(g_KeyValue!=0)g_SystemTime60s=0;
}
voidWhichKeyIsLongPress(void) //长按键
{
if(GHold){GCountLongPressTime++;}
else {
GCountLongPressTime=0;
g_LongPressKeyValue=0;
}
if(GCountLongPressTime>500)//65500
{
if(GHold&0x02)
{
g_LongPressKeyValue=longKEYADD;//左数第2个按键
}
if(GHold&0x04)
{
g_LongPressKeyValue=longKEYDEC;//左数第3个按键
}
if(GHold&0x08)
{
g_LongPressKeyValue=longKEYSET;//左数第4个按键
}
}
}
void KeyRead(void)
{
ThreeLineKeyRead();//读按键
WhichKeyIsShortPress();
WhichKeyIsLongPress();
}
不错,很工整! zhanghuhhhhh 发表于 2013-7-17 21:51 static/image/common/back.gif
发一下我的按键程序吧。
是用本坛的“三行按键”基础上改的,阿莫论坛,就是神!
多谢分享 原作者的程序
void KeyRead( void )
{
unsigned char ReadData = P1^0xff; // 1// 当前I/O状态(“真”或“假”)
Trg = ReadData & (ReadData ^ Hold); // 2 // 如果 当前状态为“真”,且当和“上次状态”不一致,则Trg为“真”
//第二句的意思是:如果当前检测到按键按下且上次未按下,则是一次有效的触发;如果连续按下,则只视为一次有效触发。
Up = (~ReadData) & (ReadData ^ Hold);
//Up= Hold&(ReadData^Hold);
Hold = ReadData; // 3 // 当前状态过时,即成为“上次状态”。
/*
unsigned char ReadData = PINB^0xff; // 1
Trg = ReadData & (ReadData ^ Cont); // 2
Up= Cont&(ReadData^Cont);
Cont = ReadData; // 3
ReadData= P0 & 0X1C;
ReadData ^= 0X1C;
*/
}
标记下,以后能用到!谢谢分享! 标记一下,备用 学习,mark,谢谢
好东西,,, 阅读完了代码,很标准的状态机按键程序,初学者可以学习一下。
很工整,以后能用上的。 不错不错。好好学习下。 不错。mark 学习,mark mark下回去好好研读下 不错学习了 不错收藏!~ 不错 {:smile:} mark 值得好好学习 mark 收藏了 mcu_lover 发表于 2013-7-17 19:38 static/image/common/back.gif
呵呵,不错。我已经用了好几年了。和你的方式一模一样,调用接口也一致,不同的是我把键值和按键状态封装在 ...
有当年 农民讲习所 思想风格的程序... 这个按键方式好用 lz能否从顶层讲解一下,整体的流程。
这样对于看code也是有效的。
传递的更多是思想。 收藏了哈..俺是初学者..正需要这些呢.会一定加把劲好好学些的..~~~~ 恩恩 学习了 cool,绑定 mark一下 mark 发一个通用按键模块,简单易用 好贴,很强大. mark,学习了! 标记下,以后能用到!谢谢分享! y574924080 发表于 2013-7-17 20:09 static/image/common/back.gif
无论是独立按键还是矩阵按键,都可以使用这个模块,它们只有获取键值函数不一样,这是由用户自己编写的 ...
Sir:
没有矩阵的吗?
Thank you. jlian168 发表于 2013-8-14 09:05 static/image/common/back.gif
Sir:
没有矩阵的吗?
Thank you.
这个由用户自己实现来的 zzjjhh250 发表于 2013-7-30 11:42 static/image/common/back.gif
lz能否从顶层讲解一下,整体的流程。
这样对于看code也是有效的。
传递的更多是思想。 ...
迟些时候哈,最近有点忙。。 mark一记
谢谢,学习了. 学习一下,以后可能会用到 不知道配置一个按键需要多少字节RAM? 按键程序,继续收藏! mark void breath_led(void)
{
static uint16_t s_hwCounter = 0;
static int16_t s_nGray = 0xFF;
s_hwCounter++;
if (!(s_hwCounter & (_BV(9)-1))) {
s_nGray++;
if (s_nGray == 0x1FF) {
s_nGray = 0;
}
}
set_led_gradation(ABS(s_nGray - 0xFF));
}
看了下你呼吸灯的程序段里静态局部变量 s_hwCounter 就随便它一直加,不加任何措施 ?这也行? anvy178 发表于 2013-8-31 13:34
void breath_led(void)
{
static uint16_t s_hwCounter = 0;
会溢出,这里利用了这点 catwill 发表于 2013-8-31 13:58 static/image/common/back.gif
会溢出,这里利用了这点
溢出的 结果是 0还行 不是的话 就惨了 anvy178 发表于 2013-8-31 14:12
溢出的 结果是 0还行 不是的话 就惨了
无符号数,溢出的结果是0 本帖最后由 anvy178 于 2013-8-31 15:55 编辑
catwill 发表于 2013-8-31 15:06 static/image/common/back.gif
无符号数,溢出的结果是0
找到了 标记学习,按键经典程序. 感谢分享! 学习学习,谢谢 不错名师出高徒 {:victory:} 模块化的程序便于重复移植使用。其他人如果想用,那就必须体会到这个程序的优点。 初学者..正需要这些呢 可以有按键连击吗?
许多情况下按键需要有按住一个键不松手,一定时间后按键会自动按一个固定时间连续输出。这个键一般用在数据的加减设置中。希望楼主能增加这个功能。 wtliu 发表于 2013-9-30 09:15
可以有按键连击吗?
许多情况下按键需要有按住一个键不松手,一定时间后按键会自动按一个固定时间连续输出 ...
已经支持这个功能啊 谢谢分享 好贴,Mark {:smile:}不错,收藏了 mark 学习啊学习…… 谢谢分享~~~ 收藏一个,赞一个。 mark 这个肯定会用到 怎么弄得和系统一样,如果写简单的程序 没必要这么复杂吧 tacbo2012 发表于 2013-10-10 18:21 static/image/common/back.gif
怎么弄得和系统一样,如果写简单的程序 没必要这么复杂吧
不复杂……只是个简单的模块…… 谢谢,看看{:smile:} 标记学习。 标记一下,备用. 看的很纠结看不懂的说 四轴飞行器 发表于 2013-10-10 20:44 static/image/common/back.gif
看的很纠结看不懂的说
罗马都不是一日建成的,不必纠结。首先从会用开始,你觉得如何呢? Gorgon_Meducer 发表于 2013-10-11 11:03 static/image/common/back.gif
罗马都不是一日建成的,不必纠结。首先从会用开始,你觉得如何呢?
罗马是二日建成的?
最近搞定了按一下蜂鸣器响一下的难题,就是引用论坛里新的按键程序
想在弄个连按和长按功能,可惜你的看的很纠结啊 好东西,学习了 四轴飞行器 发表于 2013-10-11 11:06 static/image/common/back.gif
罗马是二日建成的?
最近搞定了按一下蜂鸣器响一下的难题,就是引用论坛里新的按键程序
这是一个无需修改代码就可以使用的模块,你应该先根据帖子的内容学会使用,然后以后再考虑看
具体的实现。 Gorgon_Meducer 发表于 2013-10-11 13:00 static/image/common/back.gif
这是一个无需修改代码就可以使用的模块,你应该先根据帖子的内容学会使用,然后以后再考虑看
具体的实现 ...
key 和gpio 口 初始化的·····在哪里{:sweat:}key 值要是放在 枚举里面好不··· kalo425 发表于 2013-10-11 16:40 static/image/common/back.gif
key 和gpio 口 初始化的·····在哪里 key 值要是放在 枚举里面好不··· ...
Key和GPIO的初始化是用户自己处理,用户需要的是提供一个 键盘扫描函数(我们叫做前端处理),
这个函数的作用就是根据你的实际电路完成键盘扫描,并将扫描到的按键值作为函数返回值返回。
如果你已经完成了这样一个函数,那么具体如何将其注册到模块里面,就看楼主位了。
注意,键盘扫描函数是不需要作任何去抖操作的,直接返回键值即可。
这个模块的作用是提供中期和后期处理,包括去抖,识别按键,长按,重复按键,并将检测到的
按键放到一个模块自带的键盘缓冲区里面(对用户透明),同时还能提供诸如KEY_DOWN和KEY_UP
这样的信息。
不错的按键法 Gorgon_Meducer 发表于 2013-10-11 18:11 static/image/common/back.gif
Key和GPIO的初始化是用户自己处理,用户需要的是提供一个 键盘扫描函数(我们叫做前端处理),
这个函数 ...
谢谢你耐心的解答·····我看到fsm关键字了···还有个例子···参考下····你们好强··· kalo425 发表于 2013-10-11 19:48 static/image/common/back.gif
谢谢你耐心的解答·····我看到fsm关键字了···还有个例子···参考下····你们好强··· ...
期待你的使用感想和改进意见。 不错,学习中 很想知道,那个呼吸灯是怎么实现的说 不错,很工整! 请问傻子LED模块中,
static void set_led_gradation(uint16_t hwLevel)
{
static uint16_t s_hwCounter = 0;
if (hwLevel >= s_hwCounter) {
LED2_ON();
} else {
LED2_OFF();
}
s_hwCounter++;
s_hwCounter &= 0x1FF;
}
#define ABS(__N) ((__N) < 0 ? -(__N) : (__N))
#define _BV(bit) (1<<(bit))
void breath_led(void)
{
static uint16_t s_hwCounter = 0;
static int16_t s_nGray = 0xFF;
s_hwCounter++;
if (!(s_hwCounter & (_BV(9)-1))) {
s_nGray++;
if (s_nGray == 0x1FF) {
s_nGray = 0;
}
}
set_led_gradation(ABS(s_nGray - 0xFF));
}
set_led_gradation(ABS(s_nGray - 0xFF));
这句应该如何理解呢,为什么?为什么要取绝对值呢,能讲解下原理吗?? vpntest 发表于 2013-10-28 21:43 static/image/common/back.gif
请问傻子LED模块中,
static void set_led_gradation(uint16_t hwLevel)
{
set_led_gradation()的参数表示的就是亮度,
取绝对值就是为了实现由小到大,再由大到小,
该实现的功能是:LED由明到暗,再由暗到明,如此循环往复 更新数据流图到楼主位,流程图也将尽快更新 学习 虽然没有,完全看懂,但应该是可以用了,很好,很强大