前言
嵌入式开发时,我们经常会用到各种机械按钮,由于机械按钮的抖动特性,一般需要保持一个状态一段时间不变才能认为按键真的按下去/抬起来了,不然可能会出现明明只点击了一下,效果确是连击了好几下的情况。另一方面,我们为了最大化的发挥按钮的作用,还经常需要实现长按,连击等功能。在网上翻了好多人的按钮实现,发现都是面向过程写的。也不是说不行,但是每加个按钮就得为它重新写一遍各种处理函数和变量,要是需要改下当前实现加个功能,有N个按钮就得改N遍,累不累呀。
另外还发现按键消抖很多人都是直接在函数中调用个for循环的延迟函数等个10ms那样,这样的话其实有很多问题:一是单片机在这段时间就完全在进行无意义的循环无法处理其他事情;一是如果你只用了一个线程来处理键盘的话,那就无法同时处理两个按键的事件了,除非你为每个按键单独创建一个线程,那花销也太大了。当然经过精巧的设计还是可以解决这些问题的。
为了方便大家实现各种常用功能(其实主要是给自己以后用吧),我把这些常用的功能进行了封装。目前实现要求用户每隔一段时间轮询一下所有按钮的状态,并分别调用一次每个按钮对象的TimeTick方法将其当前状态传入,然后TimeTick方法就会把这个按钮发生了什么事件(按下、抬起、长按、单连击)返回给你,然后就可以很方便的按照自己的需要进行处理了。具体的实现细节完全被隐藏。
注1(为什么使用轮询):
使用轮询的方式是权衡后的决定,本来有想过使用异步中断的方式来实现,但是那样就需要其他的方法来计时,增加了设计难度和对使用者的水平要求;另一方面,像那种需要扫描的键盘,我暂时没想到怎么通过中断的方式来实现,所以最后还是决定用轮询的方式。这样的结果是即使没有任何按钮被按下,单片机还是得每隔一小段时间就遍历处理遍所有按钮,增大了单片机的负担,但好在这种负担其实并不大(当然还要取决于你有多少个按钮,一般不会太多),基本就是每1ms占用一小个时间片那样。
注2(为什么使用同步):
一般来说,事件都是通过异步的方式通知的(我是这么觉得的),但考虑到按钮只是键盘的一个最小单元,一般来说还会被进一步封装,所以为了方便用户个性化使用,决定直接把事件作为函数返回值。
模块介绍
特性
总结一下这个模块实现的特性:
- 内存分配:要求用户自己手动为每个按钮对象分配内存,这是考虑到嵌入式开发一般不动态分配内存而作的决定。
- 轮询:要求用户每隔一段时间为每个按钮对象调用一次TimeTick方法,每次调用为一个Tick,也就是模块中使用的时间单位。推荐时间间隔1ms,可以使用软件/硬件定时器或者其他方法。
- 面向对象:内部实现对用户完全透明,用户只需要按照要求设置参数,调用对应方法即可实现需求,不需要深究具体实现(当然你要看也可以,源码都给了)。
- 按键独立:任意两个按键间完全独立,可以轻松的实现一个按钮长按的同时,另一个按钮在连击,再加一个按钮也在长按……
- 可定制:可通过在头文件的CONFIGURATION块中更改参数以裁剪代码、控制程序逻辑。
- 内存占用:和配置有关,如支持所有功能,代码约需占用380字节ROM,另外每个按钮对象占用3字节RAM;如不需要连击和长按功能,则代码约需占用200字节ROM,另每个按钮对象则降到只需1字节RAM。
- 时间限制:对于当前实现,消抖时间不超31个tick,连击不超15次,连击间隔时间、长按触发时间、长按重复间隔时间不超4095个tick。也就是说要是一个tick是1ms的话,长按最多支持4秒。
事件
支持以下几个事件:
- down/按下事件: 即按钮被按下(闭合)了,经过了消抖。
- up/抬起事件:即按钮被松开(断开)了,经过了消抖。
- click/点击事件:包括单击和连击,如支持连击,则通过clickCnt字段提取当前连击多少次;如在抬起后特定时间内没有重新按下按钮,则连击终止。点击事件肯定伴随着up事件。
- long-press/长按事件:按钮在特定tick时间内保持按下状态则会触发一次长按事件,如果支持长按重复,则会在第一次触发长按后每隔一段时间再次触发长按事件。
运行逻辑
模块基本的逻辑如下:一个按钮对象在初始化后处于松开状态,然后如果在消抖时间内一直保持按下状态则会变更当前状态为按下,同时触发down事件。然后如果在长按事件被触发之前(经过消抖时间,后面略)抬起了,则触发click事件与up事件,如果保持按下事件足够久,就会触发第一次长按事件,然后就会每间隔一段时间重复触发长按事件;然后如抬起按钮,是否触发click事件取决于配置,但一定会触发up事件;然后如在特定时间内再次按下,则在再次抬起且判定为click事件时clickCnt值为2即双击;否则连击停止。
这个逻辑受到头文件的CONFIGURATION块中参数影响,具体边界条件时的响应和每个参数的定义见对应注释。
软件框架
可以按照下述框架使用这个模块,具体使用时根据需要进行修改:
MECBTN_STATE aBtn; // 为一个按钮对象分配内存
int main(){
...
// initialize the button object 初始化按钮对象
MecBtn_Init(&aBtn);
// start the timer. 启动软件定时器,当然你也可以使用定时中断等方式
TimerStart();
...
}
// 定时器定时调用的函数
void Timer_1ms(void){
MECBTN_EVENTFLAG rst;
INT8U stateNow,clickCnt;
stateNow = getBtnState(); // 获取按钮当前状态,记住,TRUE为按下(闭合),FALSE为松开(断开)
rst = MecBtn_TimeTick(&aBtn,stateNow);
if(MecBtn_Event_any_happened(rst)){
if(MecBtn_Event_down_happened(rst)){
// down event
}
if(MecBtn_Event_up_happened(rst)){
// up event
}
if(MecBtn_Event_click_happened(rst)){
// click event
clickCnt = MecBtn_clickTimes(rst);
switch(clickCnt){
case 1: // single click event
...
break;
case 2: // double click event
...
break;
...
}
}
if(MecBtn_Event_lPress_happened(rst)){
// long-press event
}
}
}
各种代码
模块源码
以下是.h文件
/*
*******************************************************************************************
*
*
* MECHANICAL BUTTON MODULE
* 机械按钮模块
*
* File : MecBtn.h
* By : Lin Shijun(http://blog.csdn.net/lin_strong)
* Date: 2017/12/20
* version: V1.0
* History: 2017/12/20 V1.0 the prototype
*
* NOTE(s): 1. the module encapsulates the common functions of mechanical buttons, including
* jitters elimination, long press, multiple click count.
* 这个模块封装了机械按钮常用的几个功能:按键消抖、长按、连击
* the module provides convenient judgment of several events,including down event
* , up event,click event (along with a clickcnt parameter to indicate count of
* click, optional), long-press event (support repeat, optional).
* 模块提供了多个按钮事件的判断,包括:down即按下(闭合)事件、up即抬起(断开)事件、
* click即点击事件(可选的,附带一个clickcnt参数来获取连击了多少次)、long-press即长按
* 事件(可选的,可以重复触发)。
* 2. user can configure the macro arguments to custom the code and behavior of the
* module. For details, refer to the comment of each arguments in CONFIGURATION
* block.
* 用户可以通过配置宏参数来对模块进行定制。详见CONFIGURATION块中各参数的注释。
* for current implementation, each button will occupy 3 bytes of RAM; and if you
* don't need multi-click and long-press function, memory overhead can reduce to
* 1 byte for each button.
* 对于当前的实现,每个按钮要占用3字节RAM内存,如果你不使用连击和长按功能的话,内存占用
* 可以减小到1字节。
* 3. to use the module, user should allocate a MECBTN_STATE struct for each button,
* and poll the state of each button and pass it to MECBTN_STATE object,and get
* the events from value returned. All buttons are mutually independent.
* 为了使用该模块,用户应该为每个按钮分配一个MECBTN_STATE结构体,轮询按钮的状态并传递给
* MECBTN_STATE对象,然后从返回值中得到发生的事件。所有的按钮是相互独立的。
* you can use a software/hardware timer to implement polling, each poll is a 'tick',
* and polling polling interval is recommended to between 1ms and 10ms.
* 可以使用一个定时器来实现轮询,每次轮询即为一次tick,轮询间隔推荐设为1ms到10ms。
* 4. example:
* MECBTN_STATE aBtn;
* int main(){
* ...
* // initialize the button object
* MecBtn_Init(&aBtn);
* // start the timer.
* TimerStart();
* ...
* }
* void Timer_1ms(void){
* MECBTN_EVENTFLAG rst;
* INT8U stateNow,clickCnt;
* stateNow = getBtnState();
* rst = MecBtn_TimeTick(&aBtn,stateNow);
* if(MecBtn_Event_any_happened(rst)){
* if(MecBtn_Event_down_happened(rst)){
* // down event
* }
* if(MecBtn_Event_up_happened(rst)){
* // up event
* }
* if(MecBtn_Event_click_happened(rst)){
* // click event
* clickCnt = MecBtn_clickTimes(rst);
* switch(clickCnt){
* case 1: // single click event
* ...
* break;
* case 2: // double click event
* ...
* break;
* ...
* }
* }
* if(MecBtn_Event_lPress_happened(rst)){
* // long-press event
* }
* }
* }
*
*********************************************************************************************
*/
#ifndef MECBTN_H
#define MECBTN_H
/*
********************************************************************************************
* MISCELLANEOUS
********************************************************************************************
*/
#ifndef FALSE
#define FALSE 0
#endif
#ifndef TRUE
#define TRUE 1
#endif
#ifndef NULL
#define NULL 0x00
#endif
/*
*******************************************************************************************
* CONFIGURATION 配置
*******************************************************************************************
*/
// 配置各种常数(单位:tick)
//
/* Ticks count for jitters elimination (0 — MECBTN_JITTERCNT_MAX)
Definition: At which tick the press-state will be changed and the up or down event
will be triggered after the first tick that changes press-state and states of press
remain the same. 0 for triggered at first tick.
消抖时间,单位tick,取值0到MECBTN_JITTERCNT_MAX。
定义: 从第一次转变状态起(不含)一直保持状态到第几个tick算作状态改变,也就是这次tick会
触发up或down事件。0表示不消抖,直接触发。 */
#define MECBTN_JE_TIME 9
/* Minimum interval ticks between multiple clicks. ((MECBTN_JE_TIME+2) — MECBTN_DURCNT_MAX)
Definition: At which tick multiple-click will be finished after the last tick that
triggered the click event and then no down event happened.
两次连击间的最短间隔时间,单位tick,取值(MECBTN_JE_TIME+2)到MECBTN_DURCNT_MAX。
定义: 从上次发生单击事件的Tick起(不含)的到第几个tick‘前’还没有触发下一次down事件
(注意是down事件而不是click/up事件),则停止连击。注意:即使第n个tick正好触发了down
事件,连击也会终止。取值范围不能小于(消抖时间+2)是因为要是小于这个值,根本就触发不
了连击,那你直接关掉连击呀。
*/
#define MECBTN_MULTICLICK_INTERVAL_TIME 500
/* Ticks count for first long-press event to happen. (1 — MECBTN_DURCNT_MAX)
Definition: At which tick the long-press event will be triggered after the first tick
that triggered down event, 1 for trigger at next tick.
按住多久算发生长按事件,单位tick,取值1到MECBTN_DURCNT_MAX。
定义: 从触发down事件的Tick起(不含),连续到第几个tick‘前’还没有触发任何事件,则触发第
一次长按事件。取1即为在下一个tick就触发。
*/
#define MECBTN_LONGPRESS_TIME 800
/* Ticks count for repeat long-press event. (1 — MECBTN_DURCNT_MAX)
Definition: At which tick the long-press event will be triggered after the last tick
that triggered long-press event, 1 for trigger at next tick.
重复发生长按事件的时间,单位tick,取值1到MECBTN_DURCNT_MAX。
定义: 从上一次触发长按事件的Tick起(不含),如保持pressed状态到第几个tick‘前’,则再次
触发一次长按事件。取1即为在下一个tick就触发。
*/
#define MECBTN_LONGPRESS_REPT_TIME 300
// 是否检查输入参数(节省几行代码)
#define MECBTN_SUPPORT_ARGUMENT_CHECK FALSE
// 是否支持长按?
#define MECBTN_SUPPORT_LONGPRESS FALSE
// 是否支持多次点击
#define MECBTN_SUPPORT_MULT_CLICK FALSE
#if (MECBTN_SUPPORT_LONGPRESS == TRUE)
// 是否支持长按后重复发生长按事件(MECBTN_SUPPORT_LONGPRESS需为TRUE)
#define MECBTN_SUPPORT_LONGPRESS_PEPT TRUE
// 长按后的抬起按钮还算单击么(MECBTN_SUPPORT_LONGPRESS需为TRUE)
#define MECBTN_SUPPORT_CLICK_AFTER_LONGPRESS FALSE
#endif
/*
*******************************************************************************************
* TYPE DEFINE
*******************************************************************************************
*/
//使用ucos-II时可以直接使用OS的定义
//#include "os_cpu.h"
// 定义变量,根据硬件修改
typedef unsigned short INT16U;
typedef unsigned char INT8U;
// 状态机结构体,由用户分配空间,但这些字段是私有的。用户不应该直接操作内部字段以保证行为一致性
// struct for managing the state of a mechanical button.
// user should allocate memory for each button, but should not manipulate the struct directly.
typedef struct {
union {
struct {
unsigned int jitterCnt:5; // 消抖计数
unsigned int pressedLastTick:1; // 上一Tick的状态
unsigned int pressed:1; // 认为其有没有按下(因为按键抖动的原因,所以两个不见得一致)
unsigned int lPressed:1; // 是否已经发生了长按
} Bits;
INT8U Byte;
} stateRegA;
// 在即不支持连击又不支持长按时可以省掉这个字节。也就是只提供消抖和单击功能
#if((MECBTN_SUPPORT_MULT_CLICK == TRUE) || (MECBTN_SUPPORT_LONGPRESS == TRUE))
union{
struct {
unsigned int clickCnt:4; // 单击次数计数
unsigned int durCnt:12; // 持续时间计数
} Bits;
INT16U Word;
} stateRegB;
#endif
} MECBTN_STATE,* pMECBTN_STATE;
// max value of multi-click count
// 连击计数的最大值 ((1 << 4) - 1)= 15
#define MECBTN_CLICKCNT_MAX ((1 << 4) - 1)
// max ticks count value for jitters elimination
// 消抖时间的最大值 ((1 << 5) - 1) = 31
#define MECBTN_JITTERCNT_MAX ((1 << 5) - 1)
// max value for multi-click interval, long-press ticks count and long-press repeat ticks count
// 连击间隔时间、长按触发时间、长按重复间隔时间的最大值 ((1 << 12) - 1) = 4095
#define MECBTN_DURCNT_MAX ((1 << 12) - 1)
typedef union {
INT8U Byte;
struct {
unsigned int down :1; // 闭合按钮事件
unsigned int up :1; // 断开按钮事件
unsigned int click :1; // 单击事件
unsigned int lPress :1; // 长按事件
unsigned int clickCnt :4; // 高4字节存储连击次数(只在发生单击事件时有效)
} Flags;
} MECBTN_EVENTFLAG;
#define MECBTN_EVENT_MASK 0x0f // 提取事件的掩码
#define MECBTN_EVENT_MASK_LPRESS 0x08
#define MECBTN_EVENT_MASK_CLICK 0x04
#define MECBTN_EVENT_MASK_UP 0x02
#define MECBTN_EVENT_MASK_DOWN 0x01
#define MECBTN_EVENTFLAG_isEqual(eventflag1,eventflag2) ((eventflag1).Byte == (eventflag2).Byte)
/*
******************************************************************************************
* CONSTANT
******************************************************************************************
*/
#define MECBTN_PRESSED TRUE
#define MECBTN_NONPRESSED FALSE
/*
******************************************************************************************
* Function 函数
******************************************************************************************
*/
// 返回按钮是否当前处于按下状态
// return whether the button is pressed.
#define MecBtn_State_isPressed(Btn) ((Btn).stateRegA.Bits.pressed)
// 返回按钮是否当前正在长按状态中(它是TRUE的话自然isPressed也是TRUE)
// return whether the button is long-pressed.
#define MecBtn_State_islPressed(Btn) ((Btn).stateRegA.Bits.lPressed)
// 返回按钮当前已经连击了多少次,最大MECBTN_CLICKCNT_MAX,然后不会再升,0表示还没单击过
// 只在MECBTN_SUPPORT_MULT_CLICK为TRUE时有效
// return how many times the button is clicked currently, will not exceed MECBTN_CLICKCNT_MAX.
// only valid when MECBTN_SUPPORT_MULT_CLICK is TRUE.
#define MecBtn_State_clickCnt(Btn) ((Btn).stateRegB.Bits.clickCnt)
/*
*********************************************************************************************
* MecBtn_Event_XXXX_happen(MECBTN_EVENTFLAG eventFlag)
*
* Description : to determine whether there is a XXXX event
* 用于判断是否发生了某事件
*
* Arguments : eventFlag value returned by MecBtn_TimeTick()
* 由MecBtn_TimeTick()返回的值
*
* Return : true if the event happened
*
* MECBTN_ERR_POINTER_EMPTY if pBtn or pEvent is NULL.
* Note(s) :
*********************************************************************************************
*/
#define MecBtn_Event_any_happened(eventFlag) ((eventFlag).Byte & MECBTN_EVENT_MASK)
#define MecBtn_Event_down_happened(eventFlag) ((eventFlag).Flags.down)
#define MecBtn_Event_up_happened(eventFlag) ((eventFlag).Flags.up)
#define MecBtn_Event_click_happened(eventFlag) ((eventFlag).Flags.click)
#define MecBtn_Event_lPress_happened(eventFlag) ((eventFlag).Flags.lPress)
// return : the clicked times up to now
#define MecBtn_clickTimes(eventFlag) ((eventFlag).Byte >> 4)
/*
*********************************************************************************************
* MecBtn_Init()
*
* Description : make sure that initialize the MECBTN_STATE struct via this function rather
* than by yourself.
* 确保用这个函数来初始化状态机
*
* Arguments : pBtn pointer to the MECBTN_STATE object;
* 指向MECBTN_STATE对象(结构体实体)的指针;
*
* Return : TRUE if success
* FALSE if pBtn is NULL
*
* MECBTN_ERR_POINTER_EMPTY if pBtn or pEvent is NULL.
* Note(s) :
*********************************************************************************************
*/
INT8U MecBtn_Init(pMECBTN_STATE pBtn);
/*
*********************************************************************************************
* MecBtn_TimeTick;
*
* Description : Indicate a time tick and pass the state of button (pressed or not)
* 通知状态机一次时间tick并传递当前按钮的状态
*
* Arguments : pBtn pointer to the MECBTN_STATE object;
* 指向MECBTN_STATE对象(结构体实体)的指针;
* isPressedThisTick the pressed state, MECBTN_NONPRESSED for not pressed, MECBTN_PRESSED for pressed.
* 按钮是否这次被按下了,0代表当前没按下(未接通),1代表按下了
*
* Return : eventFlag return the event happened at this tick(bitMode).
* 返回此刻发生的事件(位模式,使用提供的函数来获取信息)
*
* MECBTN_ERR_POINTER_EMPTY if pBtn or pEvent is NULL.
* Note(s) :
*********************************************************************************************
*/
MECBTN_EVENTFLAG MecBtn_TimeTick(pMECBTN_STATE pBtn, INT8U isPressedThisTick);
/*
************************************************************************************
* ERROR CHECK 错误检查
************************************************************************************
*/
#if (MECBTN_SUPPORT_LONGPRESS == TRUE)
#if (MECBTN_LONGPRESS_TIME < 1 || MECBTN_LONGPRESS_TIME > MECBTN_DURCNT_MAX)
#error "MECBTN_LONGPRESS_TIME is out of range."
#endif
#if(MECBTN_SUPPORT_LONGPRESS_PEPT == TRUE)
#if (MECBTN_LONGPRESS_REPT_TIME < 1 || MECBTN_LONGPRESS_REPT_TIME > MECBTN_DURCNT_MAX)
#error "MECBTN_LONGPRESS_REPT_TIME is out of range."
#endif
#endif
#else
#undef MECBTN_SUPPORT_LONGPRESS_PEPT
#undef MECBTN_SUPPORT_CLICK_AFTER_LONGPRESS
#define MECBTN_SUPPORT_LONGPRESS_PEPT FALSE
#define MECBTN_SUPPORT_CLICK_AFTER_LONGPRESS FALSE
#endif
#if (MECBTN_JE_TIME < 0 || (MECBTN_JE_TIME > MECBTN_JITTERCNT_MAX))
#error "MECBTN_JE_TIME is out of range."
#endif
#if (MECBTN_SUPPORT_MULT_CLICK == TRUE)
#if ((MECBTN_MULTICLICK_INTERVAL_TIME < (MECBTN_JE_TIME + 2)) || \
(MECBTN_MULTICLICK_INTERVAL_TIME > MECBTN_DURCNT_MAX))
#error "MECBTN_MULTICLICK_INTERVAL_TIME is out of range."
#endif
#endif
#endif // of MECBTN_H
以下是.c源文件
/*
*******************************************************************************************
*
*
* MECHANICAL BUTTON MODULE
* 机械按钮模块
*
* File : MecBtn.c
* By : Lin Shijun(http://blog.csdn.net/lin_strong)
* Date: 2017/12/20
* version: V1.0
* History: 2017/12/20 V1.0 the prototype
*
* NOTE(s):
*
*********************************************************************************************
*/
/*
*********************************************************************************************
* INCLUDES
*********************************************************************************************
*/
#include "MecBtn.h"
/*
*********************************************************************************************
* CONSTANTS
*********************************************************************************************
*/
#define B000 0
#define B001 1
#define B010 2
#define B011 3
#define B100 4
#define B101 5
#define B110 6
#define B111 7
/*
*********************************************************************************************
* LOCAL FUNCTION DECLARE
*********************************************************************************************
*/
// 是否上次是按下状态
#define MecBtn_State_isPressedLastTick(Btn) ((Btn).stateRegA.Bits.pressedLastTick)
// 消抖寄存器的值
#define MecBtn_State_jitterCnt(Btn) ((Btn).stateRegA.Bits.jitterCnt)
// dur寄存器的值
#define MecBtn_State_durCnt(Btn) ((Btn).stateRegB.Bits.durCnt)
// 是否在多次连击中,>0表示TRUE,要用于赋值的话只保证可以赋值FALSE
#define MecBtn_State_isMulClicking(Btn) ((Btn).stateRegB.Bits.clickCnt)
// 重新加载Dur寄存器值,用于第一次长按
#define ReloadDurCnt_First(Btn) (MecBtn_State_durCnt(Btn) = MECBTN_LONGPRESS_TIME)
// 为支持重复长按重新加载Dur寄存器值
#define ReloadDurCnt_Rep(Btn) (MecBtn_State_durCnt(Btn) = MECBTN_LONGPRESS_REPT_TIME)
// 为支持连击中断重新加载Dur寄存器值
#define ReloadDurCnt_MC(Btn) (MecBtn_State_durCnt(Btn) = MECBTN_MULTICLICK_INTERVAL_TIME)
// 重新加载jitter寄存器值
#define ReloadJitterCnt(Btn) (MecBtn_State_jitterCnt(Btn) = MECBTN_JE_TIME)
// 进行一次抖动计数,如到达消抖时间就会复位消抖计数并改变Pressed状态
// return:0 无变换
// 1 状态发生了变化(发生了抬起或按下)
static INT8U jitterTick(pMECBTN_STATE pBtn,INT8U isPressedThisTick);
// 进行一次耗时计数,负责消除连击计数以及判断是否发生长按
// return:0 未发生长按
// 1 发生了长按事件
static INT8U durTick(pMECBTN_STATE pBtn);
// 在状态改变时,即jitterTick返回值为1时进一步负责判断是否发生了单击事件,如支持连击,则连击次数
// 存在MecBtn_clickCnt(Btn)中
// return:0 未发生单击
// 1 发生了单击事件
static INT8U isClickEvent(pMECBTN_STATE pBtn,INT8U isPressedThisTick);
/*
*********************************************************************************************
* MecBtn_Init()
*
* Description : make sure that initialize the MECBTN_STATE struct via this function rather
* than by yourself.
* 确保用这个函数来初始化状态机
*
* Arguments : pBtn pointer to the MECBTN_STATE object;
* 指向MECBTN_STATE对象(结构体实体)的指针;
*
* Return : TRUE if success
* FALSE if pBtn is NULL
*
* MECBTN_ERR_POINTER_EMPTY if pBtn or pEvent is NULL.
* Note(s) :
*********************************************************************************************
*/
INT8U MecBtn_Init(pMECBTN_STATE pBtn){
#if(MECBTN_SUPPORT_ARGUMENT_CHECK == TRUE)
if(pBtn == NULL)
return FALSE;
#endif
pBtn->stateRegA.Bits.jitterCnt = MECBTN_JE_TIME;
pBtn->stateRegA.Bits.lPressed = FALSE;
pBtn->stateRegA.Bits.pressed = FALSE;
pBtn->stateRegA.Bits.pressedLastTick = FALSE;
#if ((MECBTN_SUPPORT_MULT_CLICK == TRUE) || (MECBTN_SUPPORT_LONGPRESS == TRUE))
#if(MECBTN_SUPPORT_MULT_CLICK == TRUE)
pBtn->stateRegB.Bits.clickCnt = 0;
#else
pBtn->stateRegB.Bits.clickCnt = 1;
#endif
pBtn->stateRegB.Bits.durCnt = 0;
#endif
return TRUE;
}
/*
*********************************************************************************************
* MecBtn_TimeTick()
*
* Description : Indicate a time tick and pass the state of button (pressed or not)
* 通知状态机一次时间tick并传递当前按钮的状态
*
* Arguments : pBtn pointer to the MECBTN_STATE object;
* 指向MECBTN_STATE对象(结构体实体)的指针;
* isPressed the press-state this tick, MECBTN_NONPRESSED for not pressed,
* MECBTN_PRESSED for pressed.
* 这次tick中按钮是否被按下了,MECBTN_NONPRESSED代表当前没按下(未接通),
* MECBTN_PRESSED代表按下了
*
* Return : eventFlag return the event happened at this tick(bitMode).
* 返回此刻发生的事件(位模式,使用提供的函数来获取信息)
*
* MECBTN_ERR_POINTER_EMPTY if pBtn or pEvent is NULL.
* Note(s) :
*
*********************************************************************************************
*/
MECBTN_EVENTFLAG MecBtn_TimeTick(pMECBTN_STATE pBtn, INT8U isPressedThisTick){
#define Btn (*pBtn)
MECBTN_EVENTFLAG rst;
INT8U bEvent;
rst.Byte = 0;
#if(MECBTN_SUPPORT_ARGUMENT_CHECK == TRUE)
if(pBtn == NULL)
return rst;
isPressedThisTick = !!isPressedThisTick; // 标准化BOOL值
#endif
#if((MECBTN_SUPPORT_MULT_CLICK == TRUE) || (MECBTN_SUPPORT_LONGPRESS == TRUE))
// 先Tick一下延续时间,看有没有长按事件
if(durTick(pBtn))
rst.Flags.lPress = TRUE;
#endif
// 然后Tick消抖看有没按钮变化事件
if(jitterTick(pBtn,isPressedThisTick)){ // !!isPressedThisTick for change 1..N to 1
if(isPressedThisTick)
rst.Flags.down = TRUE;
else
rst.Flags.up = TRUE;
if(isClickEvent(pBtn,isPressedThisTick)){
rst.Flags.click = TRUE;
#if(MECBTN_SUPPORT_MULT_CLICK == TRUE)
rst.Flags.clickCnt = MecBtn_State_clickCnt(Btn);
#endif
}
}
#if(MECBTN_SUPPORT_LONGPRESS == TRUE)
#endif
return rst;
#undef Btn
}
/*
*********************************************************************************************
* LOCAL FUNCTION
*********************************************************************************************
*/
#if((MECBTN_SUPPORT_MULT_CLICK == TRUE) || (MECBTN_SUPPORT_LONGPRESS == TRUE))
// 进行一次耗时计数,只负责处理是否发生长按事件及相关寄存器
// return:FALSE 未发生长按
// TRUE 发生了长按事件
static INT8U durTick(pMECBTN_STATE pBtn){
#define Btn (*pBtn)
if(MecBtn_State_isPressed(Btn)){
#if(MECBTN_SUPPORT_LONGPRESS == TRUE)
#if(MECBTN_SUPPORT_LONGPRESS_PEPT == FALSE)
if(MecBtn_State_islPressed(Btn) == FALSE) // 不支持长按重复时就可以在已经长按时跳过后续操作了
#endif
if(--MecBtn_State_durCnt(Btn) == 0){ // 按下时dur内的值减到0意味着触发长按
MecBtn_State_islPressed(Btn) = TRUE;
#if(MECBTN_SUPPORT_LONGPRESS_PEPT == TRUE)
ReloadDurCnt_Rep(Btn); // 支持长按重复时寄存器加载下次触发的计数值
#endif
return TRUE;
}
#endif // of (MECBTN_SUPPORT_LONGPRESS == TRUE)
}
#if(MECBTN_SUPPORT_MULT_CLICK == TRUE)
else{
if(MecBtn_State_isMulClicking(Btn)){ // 未按下时dur内的值减到0意味着停止连击
if(--MecBtn_State_durCnt(Btn) == 0)
// 连击停止事件
MecBtn_State_isMulClicking(Btn) = FALSE;
}
}
#endif
return FALSE;
#undef Btn
}
#endif
/* 只负责处理当前Tick是否导致闭合状态的改变事件及相关寄存器,不处理单击事件
* 程序的处理过程基本按照下表
* the process follow the table, where X for no action, R for refresh, T for tick ,
* C for state converted :
*
* | isPressed | isPressedLastTick | isPressedThisTick | jitter Count |
* | 0 | 0 | 0 | X |
* | 0 | 0 | 1 | R |
* | 0 | 1 | 0 | R |
* | 0 | 1 | 1 | T |
* | 1 | 0 | 0 | T |
* | 1 | 0 | 1 | R |
* | 1 | 1 | 0 | R |
* | 1 | 1 | 1 | X |
*/
static INT8U jitterTick(pMECBTN_STATE pBtn,INT8U isPressedThisTick){
#define Btn (*pBtn)
INT8U PressState;
// 将三个量按位模式组合
PressState = ((INT8U)MecBtn_State_isPressed(Btn) << 2) | ((INT8U)MecBtn_State_isPressedLastTick(Btn) << 1)
| isPressedThisTick;
switch(PressState){
default: // 0b001 , 0b010 , 0b101 , 0b110
MecBtn_State_isPressedLastTick(Btn) = isPressedThisTick;
ReloadJitterCnt(Btn);
// 故意不break,这样就兼容了MECBTN_JE_TIME为0的情况
case B011:case B100:
// 如果计数器减到0说明发生了状态改变事件
if(MecBtn_State_jitterCnt(Btn)-- == 0){
ReloadJitterCnt(Btn);
MecBtn_State_isPressed(Btn) = isPressedThisTick;
return TRUE;
}
break;
case B000:case B111:
break;
}
return FALSE;
#undef Btn
}
// 在状态改变时,即jitterTick返回值为TRUE时进一步负责判断是否发生了单击事件,并处理相关字段变化。
// 如支持连击,则连击次数存在MecBtn_clickCnt(Btn)中,负责与单击有关的寄存器
// return:0 未发生单击
// 1 发生了单击事件
static INT8U isClickEvent(pMECBTN_STATE pBtn,INT8U isPressedThisTick){
#define Btn (*pBtn)
INT8U islPressed;
if(!isPressedThisTick){ // 抬起时清零lPress标志位,加载Dur寄存器,根据配置决定算不算一次单击事件
#if(MECBTN_SUPPORT_LONGPRESS == TRUE )
#if (MECBTN_SUPPORT_CLICK_AFTER_LONGPRESS != TRUE)
islPressed = MecBtn_State_islPressed(Btn);
#endif
MecBtn_State_islPressed(Btn) = FALSE;
#if (MECBTN_SUPPORT_CLICK_AFTER_LONGPRESS != TRUE)
if(!islPressed){
#endif
#endif
#if(MECBTN_SUPPORT_MULT_CLICK == TRUE) // 支持连击时才有clickCnt的事,而且需要加载Dur寄存器以支持连击中断
if(MecBtn_State_clickCnt(Btn)++ >= MECBTN_CLICKCNT_MAX)
MecBtn_State_clickCnt(Btn) = MECBTN_CLICKCNT_MAX;
ReloadDurCnt_MC(Btn);
#endif
return TRUE;
#if(MECBTN_SUPPORT_LONGPRESS == TRUE && MECBTN_SUPPORT_CLICK_AFTER_LONGPRESS != TRUE)
}
#endif
}
#if(MECBTN_SUPPORT_LONGPRESS == TRUE)
// 按下的时候需要重新加载下dur计数器
else
ReloadDurCnt_First(Btn);
#endif // of (MECBTN_SUPPORT_LONGPRESS == TRUE)
return FALSE;
#undef Btn
}
#undef MecBtn_State_isPressedLastTick
#undef MecBtn_State_jitterCnt
#undef MecBtn_State_durCnt
#undef MecBtn_State_isMulClicking
#undef ReloadDurCnt_First
#undef ReloadDurCnt_Rep
#undef ReloadJitterCnt
#undef B000 0
#undef B001 1
#undef B010 2
#undef B011 3
#undef B100 4
#undef B101 5
#undef B110 6
#undef B111 7
使用示例
下面简单示范下在uCos-II,MC9S12XEP100环境下,使用两个按钮分别控制8个LED灯(在PORTB上)的前后4个的闪烁的代码。只展示main文件中相关内容,无关内容已全部略去。
#include "includes.h"
#include "MecBtn.h"
#include "LED.h"
#define KEY_dir DDRH
#define F1 PTIH_PTIH4
#define F1_dir DDRH_DDRH4
#define F2 PTIH_PTIH7
#define F2_dir DDRH_DDRH7
/*
*********************************************************************************************************
* STACK SPACE DECLARATION
*********************************************************************************************************
*/
static OS_STK AppTaskStartStk[APP_TASK_START_STK_SIZE];
static OS_STK AppTaskScanBtnStk[APP_TASK_SCANBTN_STK_SIZE];
/*
*********************************************************************************************************
* TASK FUNCTION DECLARATION
*********************************************************************************************************
*/
static void AppTaskStart(void *p_arg);
static void AppTaskScanBtn(void *p_arg);
/*
*********************************************************************************************************
* LOCAL VARIABLE DECLARE
*********************************************************************************************************
*/
static MECBTN_STATE Btn_F1; // 按钮F1对象
static MECBTN_STATE Btn_F2; // 按钮F2对象
/*
*********************************************************************************************************
* LOCAL FUNCTION DECLARE
*********************************************************************************************************
*/
// 初始化看门狗
static void INIT_COP(void);
// 初始化灯和按键
static void Btn_Init(void)
{
F1_dir=0; // 设置F1键为输入
F2_dir=0; // 设置F2键为输入
MecBtn_Init(&Btn_F1); // 初始化对应的机械按钮对象
MecBtn_Init(&Btn_F2); // 初始化对应的机械按钮对象
}
/*
*********************************************************************************************************
* MAIN FUNCTION
*********************************************************************************************************
*/
void main(void) {
INT8U err;
BSP_IntDisAll(); /* Disable ALL interrupts to the interrupt controller */
OSInit(); /* Initialize uC/OS-II */
err = OSTaskCreate(AppTaskStart,
NULL,
(OS_STK *)&AppTaskStartStk[APP_TASK_START_STK_SIZE - 1],
APP_TASK_START_PRIO);
OSStart();
}
static void AppTaskStart (void *p_arg)
{
INT8U err;
(void)p_arg; /* Prevent compiler warning */
BSP_Init();
LED_Init(LED_ALL);
Btn_Init();
err = OSTaskCreate(AppTaskScanBtn,
NULL,
(OS_STK *)&AppTaskScanBtnStk[APP_TASK_SCANBTN_STK_SIZE - 1],
APP_TASK_SCANBTN_PRIO);
while (DEF_TRUE)
{
OSTimeDlyHMSM(1,0,0,0);
}
}
static void AppTaskScanBtn(void *p_arg){
MECBTN_EVENTFLAG rst;
INT8U clickCnt;
while (DEF_TRUE) {
rst = MecBtn_TimeTick(&Btn_F1,!F1); // 这块开发板上,低电平时为按下
if(MecBtn_Event_click_happened(rst)){
// click event
clickCnt = MecBtn_clickTimes(rst);
switch(clickCnt){
case 0: // 当前实现中,如果不支持连击,这个值是0
LED_Toggle(LED_1);
break;
case 4:case 3:case 2:case 1: // F1键控制低4个LED
LED_Toggle(NumToLED(clickCnt));
break;
default:
break;
}
}
if(MecBtn_Event_lPress_happened(rst)){
// long-press event
LED_Toggle(LED_1|LED_2|LED_3|LED_4); // 每次长按事件时反转LED1-4
}
rst = MecBtn_TimeTick(&Btn_F2,!F2); // 这块开发板上,低电平时为按下
if(MecBtn_Event_click_happened(rst)){
// click event
clickCnt = MecBtn_clickTimes(rst);
switch(clickCnt){
case 0:
LED_Toggle(LED_5);
break;
case 4:case 3:case 2:case 1: // F2键控制高4个LED
LED_Toggle(NumToLED(clickCnt + 4));
break;
default:
break;
}
}
if(MecBtn_Event_lPress_happened(rst)){
// long-press event
LED_Toggle(LED_5|LED_6|LED_7|LED_8); // 每次长按事件时反转LED5-8
}
OSTimeDlyHMSM(0,0,0,1); // 延迟1ms再次轮询
}
}
测试用例
这是我自己写的测试,蛮放出来。
/*
*******************************************************************************************
*
*
* MECHANICAL BUTTON MODULE TEST
* 机械按钮模块配套测试
*
* File : MecBtn.c
* By : Lin Shijun(http://blog.csdn.net/lin_strong)
* Date: 2017/12/20
* version: V1.0
* History: 2017/04/24 V1.0 the prototype
*
* NOTE(s):
*
*********************************************************************************************
*/
#include <stdio.h>
#include "../MecBtn.h"
static MECBTN_STATE aBtn;
// 循环times次Tick的pressed状态中是否发生了事件
static INT8U MecBtnTest_loop_hasEvent(int times,INT8U pressed){
int i;
for(i = 0; i < times; i++)
if(MecBtn_Event_any_happened(MecBtn_TimeTick(&aBtn,pressed)))
return TRUE;
return FALSE;
}
// 循环times次Tick的未按下状态中是否发生了事件
static INT8U MecBtnTest_up_hasEvent(int times){
return MecBtnTest_loop_hasEvent(times,MECBTN_NONPRESSED);
}
static INT8U MecBtnTest_down_hasEvent(int times){
return MecBtnTest_loop_hasEvent(times,MECBTN_PRESSED);
}
// 循环times次Tick的同个状态,返回最后一次的eventFlag
static MECBTN_EVENTFLAG MecBtnTest_loop(int times,INT8U pressed){
int i;
static INT8U emptyEvent = 0;
if(times == 0)
return *(MECBTN_EVENTFLAG *)(&emptyEvent); // 空MECBTN_EVENTFLAG结构的表示方法
for(i = 0; i < times - 1; i++)
MecBtn_TimeTick(&aBtn,pressed);
return MecBtn_TimeTick(&aBtn,pressed);
}
// 保持times次的抬起(断开),返回最后一次的状态
static MECBTN_EVENTFLAG MecBtnTest_up(int times){
return MecBtnTest_loop(times, MECBTN_NONPRESSED);
}
// 保持times次的按下(闭合),返回最后一次的状态
static MECBTN_EVENTFLAG MecBtnTest_down(int times){
return MecBtnTest_loop(times, MECBTN_PRESSED);
}
static void MecBtnTest_press(void){
MecBtnTest_down(MECBTN_JE_TIME + 1);
return;
}
// 测试消抖
static INT8U JE_test(){
MecBtn_Init(&aBtn);
if(MecBtnTest_down_hasEvent(MECBTN_JE_TIME)) return FALSE;
if(MecBtnTest_up_hasEvent(2)) return FALSE;
return TRUE;
}
static INT8U Down_test(){
MecBtn_Init(&aBtn);
if(MecBtnTest_down_hasEvent(MECBTN_JE_TIME)) return FALSE;
if(MecBtnTest_down(1).Byte != MECBTN_EVENT_MASK_DOWN) return FALSE;
return TRUE;
}
static INT8U Up_test(){
MECBTN_EVENTFLAG flagTmp,reg;
MecBtn_Init(&aBtn);
// 这个测试的默认条件:起码MECBTN_LONGPRESS_TIME比MECBTN_JE_TIME大2
if(MECBTN_LONGPRESS_TIME < MECBTN_JE_TIME + 2)
return TRUE;
reg.Byte = 0;
MecBtnTest_press(); // 按下按钮
// up+click事件
if(MecBtnTest_down_hasEvent(MECBTN_LONGPRESS_TIME - MECBTN_JE_TIME - 2)) return FALSE;
if(MecBtnTest_up_hasEvent(MECBTN_JE_TIME)) return FALSE;
flagTmp = MecBtnTest_up(1);
// 构造应该有的结果
reg.Byte = MECBTN_EVENT_MASK_UP | MECBTN_EVENT_MASK_CLICK;
#if (MECBTN_SUPPORT_MULT_CLICK == TRUE)
reg.Flags.clickCnt = 1;
#endif
#if ((MECBTN_SUPPORT_LONGPRESS == TRUE))
if( MECBTN_LONGPRESS_TIME == 1){ // MECBTN_LONGPRESS_TIME == 1的边际条件要特殊处理。。
reg.Flags.lPress = TRUE;
#if (MECBTN_SUPPORT_CLICK_AFTER_LONGPRESS == FALSE)
reg.Flags.click = FALSE;
reg.Flags.clickCnt = 0;
#endif
}
#else
// 不支持长按时up肯定会伴随click事件
reg.Flags.click = TRUE;
#endif
if(!MECBTN_EVENTFLAG_isEqual(flagTmp,reg)) // 只应该发生单击和抬起事件,且单击计数为1
return FALSE;
// up + long-press (/+ click) 事件
MecBtnTest_press();
if(MecBtnTest_down_hasEvent(MECBTN_LONGPRESS_TIME - MECBTN_JE_TIME - 1)) return FALSE;
if(MecBtnTest_up_hasEvent(MECBTN_JE_TIME)) return FALSE;
flagTmp = MecBtnTest_up(1);
reg.Byte = MECBTN_EVENT_MASK_UP;
#if (MECBTN_SUPPORT_LONGPRESS == TRUE)
reg.Flags.lPress = TRUE;
#if (MECBTN_SUPPORT_CLICK_AFTER_LONGPRESS == TRUE)
reg.Flags.click = TRUE;
#endif
#else
// 不支持长按时up肯定会伴随click事件
reg.Flags.click = TRUE;
#endif
if((flagTmp.Byte & MECBTN_EVENT_MASK) != (reg.Byte & MECBTN_EVENT_MASK))
return FALSE;
return TRUE;
}
// 主要测试连击计数功能,click功能实际已经在up测试中测试了
static INT8U Click_test(){
MECBTN_EVENTFLAG reg;
INT8U i,max;
// 这个测试的默认条件:起码MECBTN_LONGPRESS_TIME比MECBTN_JE_TIME大2
if(MECBTN_LONGPRESS_TIME < MECBTN_JE_TIME + 2)
return TRUE;
#if (MECBTN_SUPPORT_MULT_CLICK == TRUE)
max = MECBTN_CLICKCNT_MAX; // 支持连击要测试到最大值都正常
#else
max = 1; // 不支持连击时只用循环1次
#endif
MecBtn_Init(&aBtn);
// 连击计数测试
reg.Byte = 0;
reg.Flags.click = TRUE;
reg.Flags.up = TRUE;
for(i = 0; i < max; i++){
MecBtnTest_press(); // 按下按钮
MecBtnTest_up(MECBTN_JE_TIME);
#if (MECBTN_SUPPORT_MULT_CLICK == TRUE)
reg.Flags.clickCnt++;
#endif
if(!MECBTN_EVENTFLAG_isEqual(MecBtnTest_up(1),reg))
return FALSE;
}
// 再点击一次计数应该不变
MecBtnTest_press(); // 按下按钮
MecBtnTest_up(MECBTN_JE_TIME);
if(!MECBTN_EVENTFLAG_isEqual(MecBtnTest_up(1),reg))
return FALSE;
#if (MECBTN_SUPPORT_MULT_CLICK == TRUE) // 如支持连击,需再测试下连击中断
if(MecBtnTest_up_hasEvent(MECBTN_MULTICLICK_INTERVAL_TIME - 1)) return FALSE;
// 这边出问题说明连击中断比规定提前了
if(MecBtn_State_clickCnt(aBtn) != MECBTN_CLICKCNT_MAX) return FALSE;
if(MecBtnTest_up_hasEvent(1)) return FALSE;
// 这边出问题说明连击没有按照要求中断
if(MecBtn_State_clickCnt(aBtn) != 0) return FALSE;
#endif
return TRUE;
}
// 主要测试重复长按功能,首次长按功实际已经在up测试中测试了
static INT8U lPress_test(){
#if(MECBTN_SUPPORT_LONGPRESS == TRUE)
MECBTN_EVENTFLAG flagTmp,reg;
INT8U i;
MecBtn_Init(&aBtn);
// 连击计数测试
reg.Byte = 0;
reg.Flags.lPress = TRUE;
MecBtnTest_press(); // 按下按钮
// 第一次的触发长按的时长是MECBTN_LONGPRESS_TIME
if(MecBtnTest_down_hasEvent(MECBTN_LONGPRESS_TIME - 1)) return FALSE;
if(!MECBTN_EVENTFLAG_isEqual(MecBtnTest_down(1),reg)) return FALSE;
#if(MECBTN_SUPPORT_LONGPRESS_PEPT == TRUE)
// 测试10次重复长按
for(i = 0; i < 9; i++){
if(MecBtnTest_down_hasEvent(MECBTN_LONGPRESS_REPT_TIME - 1)) return FALSE;
if(!MECBTN_EVENTFLAG_isEqual(MecBtnTest_down(1),reg)) return FALSE;
}
#endif
// 最后一次可能会有click事件
reg.Flags.up = TRUE;
#if (MECBTN_SUPPORT_CLICK_AFTER_LONGPRESS == TRUE)
reg.Flags.click = TRUE;
#if (MECBTN_SUPPORT_MULT_CLICK == TRUE)
reg.Flags.clickCnt = 1;
#endif
#endif
#if(MECBTN_SUPPORT_LONGPRESS_PEPT == TRUE)
if(MECBTN_LONGPRESS_REPT_TIME < MECBTN_JE_TIME + 2) return TRUE; // 边际条件暂不支持
if(MecBtnTest_down_hasEvent(MECBTN_LONGPRESS_REPT_TIME - MECBTN_JE_TIME - 1)) return FALSE;
#else
if(MecBtnTest_down_hasEvent(MECBTN_LONGPRESS_TIME + MECBTN_JE_TIME + 100)) return FALSE;
// 不支持连续长按时抬起不应该触发长按事件
reg.Flags.lPress = FALSE;
#endif
if(!MECBTN_EVENTFLAG_isEqual(MecBtnTest_up(MECBTN_JE_TIME+1),reg)) return FALSE;
#endif // of (MECBTN_SUPPORT_LONGPRESS == TRUE)
return TRUE;
}
INT8U MecBtnTest_TimeTick(void){
MECBTN_EVENTFLAG flagTmp;
// 测试消抖
if(!JE_test()) return FALSE;
// 测试按下事件
if(!Down_test()) return FALSE;
// 测试抬起事件
if(!Up_test()) return FALSE;
// 测试连击计数
if(!Click_test()) return FALSE;
// 长按测试
if(!lPress_test()) return FALSE;
//*/
return TRUE;
}
int main(){
if(MecBtnTest_TimeTick())
printf("MecBtn模块通过测试。");
else
printf("MecBtn模块未通过测试。");
return 0;
}
后记
1.这个模块放到单片机上运行时发现
union {
struct {
unsigned int pressedLastTick:1; // 上一Tick的状态
unsigned int pressed:1; // 认为其有没有按下(因为按键抖动的原因,所以两个不见得一致)
unsigned int lPressed:1; // 是否已经发生了长按
unsigned int jitterCnt:5; // 消抖计数
} Bits;
INT8U Byte;
} stateRegA;
这样的顺序放字段时,运行XXXXX.jitterCnt- - 时会出错,jitterCnt数值直接变为了15,在windows上测试时却没问题,然后我把jitterCnt放到了最低位就没有问题了,这个事情蛮困扰我的,希望有大神解惑。
2.对于特定场合,如只需要判断16个按键的单击,甚至消抖都不用的情况,这个模块的效率确实远不如那些专用的算法,面向对象的威力只有在需要实现很复杂的逻辑时才能体现出来。
--------------------- 本文来自 夏日白云 的CSDN 博客 ,全文地址请点击:https://blog.csdn.net/lin_strong/article/details/78897160?utm_source=copy