Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1523989
  • 博文数量: 226
  • 博客积分: 3997
  • 博客等级: 少校
  • 技术积分: 2369
  • 用 户 组: 普通用户
  • 注册时间: 2010-06-19 17:26
个人简介

Never save something for a special occasion. Every day in your life is a special occasion.

文章分类

全部博文(226)

文章存档

2018年(5)

2017年(11)

2016年(1)

2015年(17)

2014年(14)

2013年(30)

2012年(5)

2011年(52)

2010年(107)

分类: 其他平台

2013-06-12 16:37:55

dos模拟按键识别 单击、双击、长按

 

本文研究一种 基于DOS平台的按键功能模拟方案,实现以下功能:

1、识别单击、双击

2、识别长按(3s)

 

先来认识 dos 提供的2个按键相关的api

键盘驱动程序检测到按键后将按键放入按键队列,供系统读取。

系统通过函数kbhit, bioskey 可从按键队列读取按键,大致区别如下:

int kbhit() 非阻塞调用,检测是否有按键按下,但不将其从按键队列中清除。通常配合 getch 使用读取按键值。

int bioskey(cmd) tc编译环境下的函数,有2种调用方式:

cmd=0时,阻塞调用,行为类似 getch

cmd=1时,非阻塞调用,行为类似 kbhit

另外,当cmd=3 bioskey 还能检测功能(ctrl,Shift,Alt)键是否按下。

 

适应于单片机的 按键识别状态机如下

程序每100ms检测一次按键电平,若从0(无效)到1(有效),则识别为KeyDown

若连接30(3s)都为1,则进入KeepPress状态,识别为长按(3s);

3s内电平变为0,则进入Click状态,但还不确定是单击;

进入Click状态后若300ms内又按下,则进入DblClick状态,识别为双击;

进入Click状态后若300ms内未再按下,则识别为单击,进入Idle状态。

 

根据以上状态图在dos下实现的模拟按键检测

#include

#include

#include

#include

 

int main()

{

    char ch=0;

    char chbak=0;

 

    printf("hello C\n");

 

    for(;;)

    {

        const char* up_evt = "up";

        const char* down_evt = "down";

        const char * e = NULL;

 

        delay(100); // 2

 

        ch = 0;

        if(kbhit())

        { 

            ch = 0;

            chbak = 0;

            while( kbhit() && (ch=getch()))

            {

                printf("getchar:%c\n", ch);

                if(chbak != ch) /*  检测到按键 */

                {

                    if(chbak) /*  不同的按键 */

                    {

                        ungetch(ch);

                        ch = chbak;

                        break;

                    }

                    chbak = ch; /*  保存此次按键 */

                    break;

                }               

            }

 

            if(ch)

                     {

                            printf("Key down:%c\n", ch);

                            e = &down_evt;

                     }

 

            if(ch == '\33')

            {

                printf("Bye\n");

                fflush(stdout);

                break;

            }           

        }

        else

        {

            if(chbak!=0)

            {

                e = up_evt;

                printf("======Key UP:%c\n", chbak);

                chbak = 0;

            }

        }

 

        if(e)

            printf("EVENT:%s\n", e);       

    }

 

return 0;

}

 

程序每100ms读取按键队列,若kbhit返回真,则识别有键按键,发送KD事件;若kbhit返回假,且前一100ms内有键按下,则识别按键释放,发送KU事件。

 

这个程序不能如期工作,测试发现:“保持按键按下时 kbhit返回真 不总是成立。

为什么呢?这与按键驱动程序有关:

l  为了快速输入同一字符,字符间隔定义为80ms(注3)

l  为正确识别用户“快速输入同一字符”的意图,将识别出按键按下后的450ms作为长按识别区,长按识别区内不向按键队列中每80ms放入一个键值。(长按识别区后每 80ms 向按键队列中放入一个键值。) 

3

80ms,450ms只是估计值。


键盘驱动程序工作原理示意图

从示意图中可以看到,长按时若在长按识别区2次调用kbhit+getch,则第2次调用kbhit()将返回false

这个图也说明:

1OS不直接检测按键,而是通过api获取按键值;

2OS至少经过“长按识别区”的时间才能区分“单击”和“长按”。

 

为了检测长按,将检测间隔改为delay(450),同时修改状态机如下:

 

程序每450ms检测读取按键队列,若有按键则发送事件KD,进入KeyDown状态;

与次读取按键队列时若有按键,则发送事件KDcntPressKD进行计数。

若连接多次(3000/450)都有KD,则进入KeepPress状态,识别为长按。

若在识别为长按前读取按键队列返回false,则发送事件 KU,识别为n(n=12),进入MyIdle开始下一轮按键识别。

 

读取按键队列的代码如下:

 

#include

#include

#include

#include

 

#define LONG_PRESS_IND 450/*ms*/

#define CNT_PRESS_3S (3000/LONG_PRESS_IND - 1)

 

static SimKeyDrv l_skd;

 

int main()

{

    char ch=0;

    char chbak=0;    // 保存从按键队列中读取到的键值

    int flagKU = 0;

 

    SimKeyDrv_ctor(&l_skd);

    QFsm_init((QFsm*)&l_skd, (QEvt*)0);

 

    printf("hi qfsm\n");

 

    for(;;)

    {

        static KeyUp  up_evt = {KU_SIG, 0, 0, 0};

        static KeyDown down_evt = {KD_SIG, 0, 0, 0};

        static TickEvt  tick_evt = {TICK_SIG, 0, 0, 0};

        QEvt  * e = NULL;

 

        if(++tick_evt.fine_time == 10)

            tick_evt.fine_time = 0;

 

        QFsm_dispatch((QFsm*)&l_skd, (QEvt*)&tick_evt);

 

        delay(LONG_PRESS_IND);   // 读取按键按键队列的时间间隔

 

        if(kbhit())

        {

            while(kbhit() && (ch=getch())) /* 处理重复键值,最多2 */

            {

                //printf("dbg getchar:%c(%c)\n", ch, chbak);

                if(chbak != ch)

                {

                    if(chbak) /* 若干重复键值后遇到不同的键值 */

                    {

                        ungetch(ch); /* 放回队列在下次处理 */

                        ch = chbak;  /* 本次要处理的键值 */

                        flagKU = 1;  /* 处理下一键值前当前键已释放*/

                        break;

                    }

                    chbak = ch; /* 保存以备ungetch */

                    break;

                }

            }

 

            if(ch == '\33')

            {

                printf("Bye\n");

                fflush(stdout);

                break;

            }

 

            // 应该在发送 KD 事件后再发送 KU 事件,但这个bug不影响需求“模拟单击、双击、长按”的实现。

            // bug: press "aab", expect "click2a,click1b"

            if(flagKU)

            {

                flagKU = 0;

                //printf("======dbg Key UP2:%c(%c)\n", ch,chbak);

                e = (QEvt*)&up_evt;

                ((KeyUp*)e)->key = ch;

                chbak = 0;

            }

            else

            {

                //printf("dbg Key down:%c(%c)\n", ch,chbak);

                e = (QEvt*)&down_evt;

                ((KeyDown*)e)->key = ch;

            }

 

 

        }

        else

        {

            if(ch!=0)

            {

                //printf("======dbg Key UP:%c(%c)\n", ch,chbak);

                e = (QEvt*)&up_evt;

                ((KeyUp*)e)->key = ch;

                ch = 0;

                chbak = 0;

            }

        }

 

        if(e)

        {

            //printf("EVENT:%p\n", e);

            QFsm_dispatch((QFsm*)&l_skd, e);

        }

    }

 

 

return 0;

}

 

读取按键后交由QFsm处理状态图对应的逻辑(也可以使用原始的双层switch-case实现):

/*****************************************************************************

* Model: SimKeyDrv2.qm

* File:  E:\Program\QP\qpc\examples\80x86\dos\watcom\l\SimKeyDriver/SKD2.c

*

* This code has been generated by QM tool (see state-machine.com/qm).

* DO NOT EDIT THIS FILE MANUALLY. All your changes will be lost.

*

* This program is open source software: you can redistribute it and/or

* modify it under the terms of the GNU General Public License as published

* by the Free Software Foundation.

*

* This program is distributed in the hope that it will be useful, but

* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY

* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License

* for more details.

*****************************************************************************/

/* @(/3/0) .................................................................*/

/*

Simulate Key Driver base on DOS.

 

*/

 

// Include

#include "qp_port.h"

 

 

#include

#include

#include

#include

 

 

// Signals

typedef enum SimKeyDrvSignalTag{

    KD_SIG = Q_USER_SIG,

    KU_SIG,

    TICK_SIG

}SimKeyDrvSignal;

 

 

// events

/* @(/1/0) .................................................................*/

typedef struct KeyDownTag {

/* protected: */

    QEvt super;

 

/* public: */

    uint8_t key;

} KeyDown;

 

 

/* @(/1/1) .................................................................*/

typedef struct KeyUpTag {

/* protected: */

    QEvt super;

 

/* public: */

    uint8_t key;

} KeyUp;

 

 

/* @(/1/2) .................................................................*/

typedef struct TickEvtTag {

/* protected: */

    QEvt super;

 

/* public: */

    uint8_t fine_time;

} TickEvt;

 

 

 

 

// oa

/* @(/2/0) .................................................................*/

typedef struct SimKeyDrvTag {

/* protected: */

    QFsm super;

 

/* public: */

    uint8_t key;

    uint8_t preKey;

    uint8_t cntPress;

} SimKeyDrv;

 

/* public: */

static void SimKeyDrv_ctor(SimKeyDrv * const me);

 

/* protected: */

static QState SimKeyDrv_initial(SimKeyDrv * const me, QEvt const * const e);

static QState SimKeyDrv_MyIdle(SimKeyDrv * const me, QEvt const * const e);

static QState SimKeyDrv_KeyDown(SimKeyDrv * const me, QEvt const * const e);

static QState SimKeyDrv_KeepPress(SimKeyDrv * const me, QEvt const * const e);

 

 

// constractor

//$define(AOs::SimKeyDrv::ctor)

 

/* @(/2/0) .................................................................*/

/* @(/2/0/3) ...............................................................*/

static void SimKeyDrv_ctor(SimKeyDrv * const me) {

    QFsm_ctor(&me->super, (QStateHandler)&SimKeyDrv_initial);/* superclass ctor */

 

}

/* @(/2/0/4) ...............................................................*/

/* @(/2/0/4/0) */

static QState SimKeyDrv_initial(SimKeyDrv * const me, QEvt const * const e) {

    me->cntPress = 0;

    //me->key = 0;

    //me->preKey = 0;

 

    return Q_TRAN(&SimKeyDrv_MyIdle);

}

/* @(/2/0/4/1) .............................................................*/

static QState SimKeyDrv_MyIdle(SimKeyDrv * const me, QEvt const * const e) {

    QState status_;

    switch (e->sig) {

        /* @(/2/0/4/1/0) */

        case KD_SIG: {

            status_ = Q_TRAN(&SimKeyDrv_KeyDown);

            break;

        }

        default: {

            status_ = Q_IGNORED();

            break;

        }

    }

    return status_;

}

/* @(/2/0/4/2) .............................................................*/

static QState SimKeyDrv_KeyDown(SimKeyDrv * const me, QEvt const * const e) {

    QState status_;

    switch (e->sig) {

        /* @(/2/0/4/2) */

        case Q_ENTRY_SIG: {

            me->cntPress = 1;

            printf("Entry:KeyDown\n");

            status_ = Q_HANDLED();

            break;

        }

        /* @(/2/0/4/2/0) */

        case KU_SIG: {

            printf("=======Click %d, %c!\n", me->cntPress, ((KeyUp*)e)->key);

            status_ = Q_TRAN(&SimKeyDrv_MyIdle);

            break;

        }

        /* @(/2/0/4/2/1) */

        case KD_SIG: {

            me->cntPress++;

            /* @(/2/0/4/2/1/0) */

            if (me->cntPress > (3000/450)) {

                me->key = ((KeyDown*)e)->key;

                status_ = Q_TRAN(&SimKeyDrv_KeepPress);

            }

            /* @(/2/0/4/2/1/1) */

            else {

                status_ = Q_HANDLED();

            }

            break;

        }

        default: {

            status_ = Q_IGNORED();

            break;

        }

    }

    return status_;

}

/* @(/2/0/4/3) .............................................................*/

static QState SimKeyDrv_KeepPress(SimKeyDrv * const me, QEvt const * const e) {

    QState status_;

    switch (e->sig) {

        /* @(/2/0/4/3) */

        case Q_ENTRY_SIG: {

            printf("=======Keep Press 3s, %c\n", me->key);

 

            status_ = Q_HANDLED();

            break;

        }

        /* @(/2/0/4/3/0) */

        case KU_SIG: {

            printf("=======Press tick %d, %c!\n",

            me->cntPress,

            me->key);

            status_ = Q_TRAN(&SimKeyDrv_MyIdle);

            break;

        }

        default: {

            status_ = Q_IGNORED();

            break;

        }

    }

    return status_;

}

 

 

 
测试:


总结:

状态机思想很明晰。别看这里状态处理函数显得复杂,相对成堆 if-else 写出的意大利面条式代码,qm生成的状态处理函数规整而易维护。最后总结重点如下:

1、  此实验目标是在dos下模拟按键识别出单击、长按,还有双击。

2、  明白这点:os通过api读取按键队列,而直接识别按键的是键盘驱动程序。由于驱动程序的长按识别区的存在,长按时kbhit()可能返回false,这取决于os对按键队列的读取间隔。

3、  每次读取时忽略2次以上的重复键值。

4、  看状态图,而不要把注意力焦点在代码上。

5、  使用QM生成和维护状态机处理函数,掌握QPQFsm使用。

阅读(6780) | 评论(0) | 转发(1) |
给主人留下些什么吧!~~