单片机按键系统设计参考

        本篇所讲述的按键设计,仅仅只是在裸机和FreeRTOS进行的一些小项目下的总结。如有不妥之处,还请海涵。

        下面按键将以4*4键盘为例,并且按键是由中断触发,所以没有长按短按的功能,仅仅只能点击。

基本思路

1,switch语句

        最简单的按键设计不外乎是使用switch语句,加上两个变量。这种方案简单明了,但只适合一些特别简单的操作,如控制LED的开关。

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
    keycode = KEY_RAM & 0xF;//获取键值
    kesign=1;//置键有效

}
    if (keysign)
    {
        keysign = 0;//重置键
        switch (keycode)
        {
            case 0:
                /*code*/
                break;

            case 1:
                /*code*/
                break;

                /*……*/
        }
    }

2,switch语句+状态变量

  如果是实现更复杂的功能,单单使用switch语句就显得不太合适了。比如说设计录音、放音、擦除等功能时,由于按键之间并非简单独立的,需要考虑按键功能之间可能存在的冲突问题。

        现考虑一个情景:按下录音后,又按下放音。显然录音和放音同时进行是不合适的,不过它们之间有着很强的联系,即录音时不能放音,放音时不能录音。

        那么这时候或许你会想到设几个变量来管理按键功能之间的关系,这种想法是很不错的。

    bool playstate=0,recordstate=0;


    if (keysign)
    {
        keysign = 0;//重置键
        switch (keycode)
        {
            case 0://录音
                if(playstate)
                    break;
                recordstate!=recordstate;//取反(非)以切换状态
                /*code0*/
                break;

            case 1://放音
                if(recordstate)
                    break;
                playstate!=playstate;
                /*code1*/
                break;

                /*……*/
        }
    }

    

3,key类+模块化管理

    不过当键盘功能稍微多一点,键盘功能之间逻辑关系更复杂一些,这种方案实现来就会显得尤为困难,且代码不好组织,可读性也会很差。

        这时,你就可以考虑使用类(或结构体),以面向对象的思想来解决这个问题。下面展现的这个类,你会发现多了一个变量flag,这个变量的作用其实与上面代码中的recordstateplaystate并无二致,只是用来表示各键盘的状态而已。

        只不过不同的是,这时flag为16位的变量,正好与16个按键一一对应,那么就可以利用flag的每一位来表示每个按键的开关状态。这样表示有个很大的好处,那就是可以使用复杂的逻辑运算、位运算来实现各种复杂的功能交互。

下面是Key类

class Key
{
public:
    Key();
    ~Key();

    /*工具*/

    uint8_t transcode(const uint16_t &keyflag); // 键标转键值
    uint16_t transflag(const uint8_t &keycode); // 键值转键标
    void reverseflag(uint8_t &keycode);         // 对某键值使键标取反
    void reverseflag(uint16_t &keyflag);
    bool isvalid();    // 判断键是否有效
    bool operateotherkey(const bool &keystatus, uint16_t keyky, const bool &option); // 想要操作其他键时,判断是否需要操作
    void setcode(const uint16_t &keyflag);    // 将某个键置1
    void resetcode(const uint16_t &keyflag); // 将某个键置0
    bool iskeyopen(const uint16_t &keyflag);   // 判断某键是否开启

public:
    uint8_t code;  // 键值
    uint16_t flag; // 键标
    bool sign;     // 置键有效标志
};

下面是Key类的成员函数实现

Key::Key()
{
    code = 0x00;
    sign = 0;
    flag = 0x00;
}

Key::~Key()
{
}

// 键标转键值
uint8_t Key::transcode(const uint16_t &keyflag)
{
    switch (keyflag) // 置键有效
    {
    case keyk0:
        return 0x0;
        break;
    case keyk1:
        return 0x1;
        break;
    case keyk2:
        return 0x2;
        break;
    case keyk3:
        return 0x3;
        break;
    case keyk4:
        return 0x4;
        break;
    case keyk5:
        return 0x5;
        break;
    case keyk6:
        return 0x6;
        break;
    case keyk7:
        return 0x7;
        break;
    case keyk8:
        return 0x8;
        break;
    case keyk9:
        return 0x9;
        break;
    case keyka:
        return 0xA;
        break;
    case keykb:
        return 0xB;
        break;
    case keykc:
        return 0xC;
        break;
    case keykd:
        return 0xD;
        break;
    case keyke:
        return 0xE;
        break;
    case keykf:
        return 0xF;
        break;
    default:
        return 0xF;
    }
}

// 键值转键标
uint16_t Key::transflag(const uint8_t &keycode)
{
    return 1 << keycode;
}

// 判断键是否有效
bool Key::isvalid()
{
    if (sign) // 键有效则置零并返回1
    {
        sign = 0;
        return 1;
    }
    else
        return 0;
}

// 键标取反
void Key::reverseflag(uint8_t &keycode)
{
    flag ^= (transflag(keycode)); // 异或运算,0不变,1取反
}

void Key::reverseflag(uint16_t &keyflag)
{
    flag ^= (keyflag); // 异或运算,0不变,1取反
}

/*
 *功能:想要在option的情况下操作其他键时,用于判断是否满足启闭条件
 *参数:keystatus 把本键置的值 keyky 想要操作的键值(已宏定义过)  option 为布尔值,0/1分别表示设置其他键启闭
 *逻辑:不需要逻辑
 *说明:
 */
bool Key::operateotherkey(const bool &keystatus, uint16_t keyky, const bool &option)
{
    if (keystatus)
        setcode(transflag(code));
    else
        resetcode(transflag(code));
    if (option ^ (flag & keyky))
    {
        if (option)
            setcode(keyky); // 选中的键置1已视开启
        else
            resetcode(keyky); // 选中的键置零已视关闭
        return 1;
    }
    else
        return 0;
}

// 将某个键置1
void Key::setcode(const uint16_t &keyflag)
{
    flag |= keyflag;
}

// 将某个键置0
void Key::resetcode(const uint16_t &keyflag)
{
    flag &= ~keyflag;
}

bool Key::iskeyopen(const uint16_t &keyflag)
{
    return flag & keyflag ? 1 : 0;
}

        下面是按键扫描函数,每当按下按键时,先把对应flag的二进制位取反以达到切换状态的效果。为了便于管理,把各个键的功能封装为对应按键的启闭函数,如k1closek2open

        这样,只要确定好各个按键应该实现的功能以及关闭它的方法,那么就可以很快地实现对应kxclosekxopen函数,进而通过简单地调用,利用逻辑运算与位运算来更快更清晰地实现按键功能之间的交互

// 按键绑定
void System::keybond() {
  key->sign = 0;               // 重置键效
  key->reverseflag(key->code); // 键标取反

  switch (key->code) {
  case 0x0: // 按键K0
    if (key->operateotherkey(1, keyk1 | keyk2 | keyk3 | keyk4, 0)) {
      k1close();
      k2close();
      HAL_TIM_Base_Stop_IT(&htim6); // 关闭计时器
    }
    /*再打开本键*/
    k0open();
    break;

  case 0x1: // 按键K1
    if (key->flag & keyk1) {
      /*先启闭其他键,如果需要的话*/
      if (key->operateotherkey(1, keyk2 | keyk3 | keyk4, 0)) {
        k2close();
        k3close();
        k4close();
      }

      k1open();
    } else {
      k1close();
    }
    break;

  case 0x2: // 按键K2
    if (key->flag & keyk2) {
      if (key->operateotherkey(1, keyk1, 0)) {
        k1close(); // 只需关闭录音
        playaddr = 0;
        sec = 0, csec = 0;
      }
      k2open();
    } else {
      k2close();
    }
    break;

  case 0x3:                     // 按键K3
    if (!key->iskeyopen(keyk1)) // 如果录音开启,那么就不执行慢放
    {
      if (key->flag & keyk3) {

        if (key->operateotherkey(1, keyk4, 0)) {
          LCD_ShowChineseStringBig(307, 180, 76, 2, YELLOW); // 关闭快进
        }
        k3open();

      } else {
        k3close();
      }
    }
    break;

  case 0x4:                     // 按键K4
    if (!key->iskeyopen(keyk1)) // 如果录音开启,那么就不执行快进
    {
      if (key->flag & keyk4) {

        if (key->operateotherkey(1, keyk3, 0)) {
          LCD_ShowChineseStringBig(307, 220, 78, 2, YELLOW); // 关闭慢放
        }
        k4open();

      } else {
        k4close();
      }
    }
    break;

  case 0x5: // 按键K5
    break;
  case 0x6: // 按键K6
    break;
  case 0x7: // 按键K7
    break;
  case 0x8: // 按键K8
    break;
  case 0x9: // 按键K9
    break;
  case 0xA: // 按键KA
    break;
  case 0xB: // 按键KB
    break;
  case 0xC: // 按键KC
    break;
  case 0xD: // 按键KD
    break;
  case 0xE: // 按键KE
    break;
  case 0xF: // 按键KF
    break;
  default: // 异常状态
    break;
  }
}

4,Key类+状态机

        你会发现前面经常会提到状态二字,不过仅仅涉及到了开关这种简单的二值状态。而实际应用中往往不只有两种状态,这就需要用到状态机的思想了。事实上,这次方案只是对上一个方案的补充,这里所谓“状态机的思想”不过是扩展了一下单个键所占据的二进制位数。

        这次成员变量只有两个,因为另一个用二值信号量来代替了(我使用的是RTOS,如果裸机开发的话就继续用keysign)。此时state用于代替之前的flag,由于一个按键功能往往不会太多,这里以最大4个状态为例。如果你需要的按键状态比较多,那么可以用一个指针来代替state,同时实现一个含参构造,以便在Key构造时输入最大状态数来分配相应的位域。

        由于使用二进制位来表示状态,可读性不强。这时可以搭配枚举体来使用(这里先提一下思路)

class Key
{
public:
    Key();
    ~Key();

    /*工具*/
    uint8_t stateHandler(uint8_t maxKeyStates);//处理按键并返回判断状态
    [[nodiscard]] uint8_t getState(uint8_t keycode) const;         //获取对应键的状态
    void resetState(uint8_t keycode);             //清零对应键位状态
    /*…………*/

public:
    uint8_t code;  // 键值
    uint32_t state;//状态
};

         下面的stateHandler函数其实是处理按键状态的,按一下,对应的状态位就会累加,从而切换到下一个状态。一般默认0为关闭状态,这样也便于进行逻辑运算

Key *key = new Key;
Key::Key()
{
    code = 0;
    state = 0;
}

uint8_t Key::stateHandler(uint8_t maxKeyStates)
{
    uint8_t temp = (state >> (code * 2)) & 0x3;//取出对应键位的值
    temp = ((temp + 1) & 0x3) % maxKeyStates;//切换状态
    state = (state & ~(0x3 << (code * 2))) | (temp << (code * 2));//设置新状态,先清零再设置
    return temp;
}
uint8_t Key::getState(uint8_t keycode) const
{
    return state&(0x3<<keycode*2);
}
void Key::resetState(uint8_t keycode)
{
    state &= ~(0x3 << (keycode * 2)); // 清除指定键位的状态
}

/*…………*/

Key::~Key() = default;

        目前只展现了切换状态的成员函数,至于更复杂的按键交互函数,

可以根据需要自己尝试一下 ( ´◔︎ ‸◔︎`)

        xSemaphoreTake(static_cast<QueueHandle_t>(keyBinarySemHandle), portMAX_DELAY);
        switch (key->code)
        {
            case keyk0:
                /*只有两个状态,启闭*/
                if (key->stateHandler(2))
                {
                }
                else
                {
                }

                break;
                
            case keyk3:
                /*在4个状态里切换*/
                switch (key->stateHandler(4))
                {
                    case 1:
                        break;
                    case 2:
                        break; 
                    case 3:
                        break;
                    default:
                        /*关闭*/
                        break;
                }
            default:
                break;
        }
    }

作者:浮梦终焉

物联沃分享整理
物联沃-IOTWORD物联网 » 单片机按键系统设计参考

发表回复