单片机按键系统设计参考
本篇所讲述的按键设计,仅仅只是在裸机和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,这个变量的作用其实与上面代码中的recordstate、playstate并无二致,只是用来表示各键盘的状态而已。
只不过不同的是,这时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的二进制位取反以达到切换状态的效果。为了便于管理,把各个键的功能封装为对应按键的启闭函数,如k1close、k2open
这样,只要确定好各个按键应该实现的功能以及关闭它的方法,那么就可以很快地实现对应kxclose、kxopen函数,进而通过简单地调用,利用逻辑运算与位运算来更快更清晰地实现按键功能之间的交互
// 按键绑定
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;
}
}
作者:浮梦终焉