设计参考——单片机按键系统

        本篇所讲述的按键设计,仅仅只是在裸机和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;
        }
    }

  • 23
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
目 录 摘 要 2 一、 方案比较、设计与论证 2 2 显示界面方案 2 3 输入方案: 3 二、理论分析与计算 3 1.交通灯显示时序的理论分析与计算 3 2.交通灯显示时间的理论分析与计算 5 三、电路图及设计文件 6 1.灯控制电路设计 6 2.倒计时显示电路设计 7 四、程序设计思路与流程图 7 1.主程序流程图 7 2.按键子程序流程图 8 五、测试、数据及结果分析 8 1.状态灯显示测试 8 2.数码管的测试 9 3.整体电路测试 9 六、总结 9 参考文献 9 附 录 10 摘 要 本系统单片机系统、键盘、LED 显示、交通灯演示系统组成。系统包括人行道、左转、右转、以及基本的交通灯的功能 。系统除基本交通灯功能外,还具有倒计时、时间设置、紧急情况处理、分时段调整信 号灯的点亮时间以及根据具体情况手动控制等功能。 关键词:AT89S51;交通规则 一、 方案比较、设计与论证 1 电源提供方案 为使模块稳定工作,须有可靠电源。我们考虑了两种电源方案 方案一:采用独立的稳压电源。此方案的优点是稳定可靠,且有各种成熟电路可供选 用;缺点是各模块都采用独立电源,会使系统复杂,且可能影响电路电平。 方案二:采用单片机控制模块提供电源。改方案的优点是系统简明扼要,节约成本; 缺点是输出功率不高。 综上所述,我们选择第二种方案。 2 显示界面方案 该系统要求完成倒计时、状态灯等功能。基于上述原因,我们考虑了三种方案: 方案一:完全采用数码管显示。这种方案只显示有限的符号和数码字苻,无法胜任题 目要求。 方案二:完全采用点阵式LED 显示。这种方案实现复杂,且须完成大量的软件工作;但功能强大,可方便的显示各种 英文字符,汉字,图形等。 方案三:采用数码管与点阵LED 相结合的方法因为设计既要求倒计时数字输出,又要求有状态灯输出等,为方便观看并 考虑到现实情况,用数码管与LED灯分别显示时间与提示信息。这种方案既满足系统功能 要求,又减少了系统实现的复杂度。权衡利弊,第三种方案可互补一二方案的优缺,我 们决定采用方案三以实现系统的显示功能。 3 输入方案: 题目要求系统能手动设灯亮时间、紧急情况处理,我们讨论了两种方案: 方案一:采用8155扩展I/O 口及键盘,显示等。该方案的优点是: 使用灵活可编程,并且有RAM,及计数器。若用该方案,可提供较多I/O 口,但操作起来稍显复杂。 方案二: 直接在IO口线上接上按键开关。因为设计时精简和优化了电路,所以剩余的资源还比较 多,我们使用四个按键,分别是K1、K2、K3、K4。 由于该系统对于交通灯及数码管的控制,只用单片机本身的I/O 口就可实现,且本身的计数器及RAM已经够用,故选择方案二。 二、理论分析与计算 1.交通灯显示时序的理论分析与计算 对于一个交通路口来说,能在最短的时间内达到最大的车流量,就算是达到了最佳的 性能,我们称在单位时间内多能达到的最大车流为车流量,用公式:车流量= 车流 / 时间 来表示。 先设定一些标号如图2-1 所示。 说明:此图为直方图,上边为北路口灯,右边为东路口灯,下边为南路口灯,左边为 西路口灯。图2-2 所示为一种红绿灯规则的状态图,分别设定为S1、S2、S3、S4,交通灯以这四的状态为 一个周期,循环执行(见图2-3)。 请注意图2-1b和图2-1d,它们在一个时间段中四个方向都可以通车,这种状态能在 一定的时间内达到较大的车流量,效率特别高。依据上述的车辆行驶的状态图,可以列 出各个路口灯的逻辑表,由于相向的灯的状态图是一样的,所以只需写出相邻路口的灯 的逻辑表;根据图2-3 可以看出,相邻路口的灯,它们的状态在相位上相差180°。因此最终只需写出一组S1、 S2、S3、S4的逻辑状态表。 如表2-1 所示。 表中的"×"代表是红灯亮(也代表逻辑上的0)," "是代表绿灯亮(也代表逻辑上的 1),依上表,就可以向相应的端口送逻辑值。 2.交通灯显示时间的理论分析与计算 东西和南北方向的放行时间的长短是依据路口的各个方向平时的车流量来设定,并且 S1、S2、S3、S4各个状态保持的时间之有严格的对应关系,其公式如下所示。 T-S1+T-S2=T-S3 T-S2=T-S4 T-S1=T-S3 我们可以依据上述的标准来改变车辆的放行时间。按照一般的规则,一个十字路口可 分为主干道和次干道,主干道的放行时间大于次干道的放行时间,我们设定值时也应以 此为参考。 三、电路图及设计文件 1.灯控制电路设计 由于32个LED 来实现红绿灯状态,若直接接在单片机的口线,路口倒计时的显示就不能实现,所以本 次设计中采用一种新型的电路如图3-1 所示。 图中74LS04的作用是倒相和驱动,它输出的电流大约48mA,实际测试发现足以满足要求 ,而且发光管也能达

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值