//定义的枚举
typedef enum
{
KEY_UP =1, //按键按下
Edge_Lead=2, //前沿抖动
KEY_DOWN =3, //按键松开
Edge_Back=4, //后沿抖动
} KEY_Status;
主函数:
#include "stm32f4xx.h"
#include "led.h"
#include "delay.h"
#include "key.h"
#include "timer.h"
//定义了按键状态的结构体
KEY_Status KEY2_Status;
//10ms定时开关
uint8_t Timer10ms_Flag=0;
uint8_t cnt=0 ;
int main(void)
{
delay_init(168);
KEY_Init();
MyGPIO_Init();
TIM3_Int_Init(400-1,8400-1);//10ms的定时器
KEY2_Status =KEY_UP ; //按键的初始状态
while(1)
{
//当10ms定时时间到
if(Timer10ms_Flag==1)
{
Timer10ms_Flag=0;
switch(KEY2_Status)
{
case KEY_UP:
if(KEY2==0) KEY2_Status =Edge_Lead;
cnt=0;
break;
case Edge_Lead:
cnt++;
if(KEY2==1) KEY2_Status =KEY_UP;
else if(cnt==2)
{
KEY2_Status =KEY_DOWN;
cnt=0;
}
break;
case KEY_DOWN:
if(KEY2==1)
KEY2_Status =Edge_Back;
cnt=0;
break;
case Edge_Back:
cnt++;
if(KEY2==0) KEY2_Status=KEY_DOWN;
else if(cnt==2)
{
KEY2_Status =KEY_UP;
cnt=0;
}
break;
default :
break;
}
if(KEY2_Status ==KEY_DOWN)
{
LED2_ON();
}
if(KEY2_Status ==KEY_UP)
{
LED2_OFF();
}
}
}
}
1 全自动洗衣机功能分析
下面是一个全自动洗衣机的控制面板:
面板上有4个按键:
电源:控制洗衣机通电与断电
水位:选择洗衣时需要的水位,有1~8个水位
程序:选择不同的洗衣模式,有1~10个模式
01:标准
02:轻柔
03:快速
…
10:桶风干
启动/暂停:用于启动或暂停洗衣任务
面板上还有一个数码管,用于显示当前的工作状态与剩余时间,可显示的工作模式有:
AA:浸泡
BB:洗涤
CC:漂洗
DD:脱水
本篇,就按照这款洗衣机的操作方式实现对应的洗衣机控制逻辑。需注意的是:
实际的洗衣机有水位检测传感器,本篇中,暂用时间延时模拟水位的增加,且默认开机时水位为0
实际的洗衣机中的洗衣模式,会根据不同的模式自动设置清洗次数与每次清洗的时间以及清洗力度,本篇为了简化起见,清洗模式的设置仅用于区分不同的清洗次数
某些特殊的清洗模式,如单独的脱水,桶风干等,本篇暂不实现
对于状态的显示 ,本篇先以串口打印的实现展示,下篇使用OLED小屏幕来显示不同清洗状态的图标等信息
2 画状态图
根据上面分析的全自动洗衣机的功能,以及我们自己使用洗衣机时的经验,可以画出如下的全自动洗衣机的状态图:
首先是上电开机,洗衣机可能会开机自检,检测洗衣机的各个部件是否正常,次过程很短。
然后就处于空闲状态,此时用户可以设置水位与清洗模式,若不设置,则为默认的水位与洗衣模式。
接着触发开始按键后,就开始清洗了,一般流程就是:加水、清洗、排水、甩干、结束。
根据不同的清洗模式,加水、清洗和排水这3个过程会循环执行一定的次数。
另外,在不同的工作阶段,按下暂停键可以让洗衣任务暂停,再按继续可让洗衣任务继续。
3 编程实现
3.1 多按键检测功能
本篇介绍的洗衣机的按键,仅需要检测按键单击即可,不需要双击与长按功能,因此,可使用之前文章介绍的最基础的按键状态机来为洗衣机状态机提供按键事件。
之前介绍的按键状态机,只有一个按键,本篇需要用到4个按键(除去电源键,3个也可以),因此,需要对按键状态机稍加修改,实现按键状态机的复用。
之前介绍的按键状态机,使用了几个全局变量来表示状态,更合理的做法是将其封装起来:
typedef struct {
KEY_STATUS keyStatus; //当前循环结束的(状态机的)状态
KEY_STATUS nowKeyStatus; //当前状态(每次循环后与pKeyFsmData->keyStatus保持一致)
KEY_STATUS lastKeyStatus; //上次状态(用于记录前一状态以区分状态的来源)
int keyIdx;
}KeyFsmData;
注意,额外增加了一个按键索引值,用来告诉状态机要检测哪个按键。
再将原来的按键状态机程序,通过入参的形式将上述定义的结构体传入,并通过函数返回的形式返回按键是否被按下。
这样修改后的按键状态机,就是一个独立的模块了,可以通过传入不同的参数,实现不同按键的检测。
bool key_press_check(KeyFsmData *pKeyFsmData)
{
bool bPress = false;
switch(pKeyFsmData->keyStatus)
{
//省略...
对于本篇需要的4个按键的检测,就可以定义4个数据结构体,分别调用4次状态机函数即可,实现代码的复用。
KeyFsmData pKey0FsmData;
KeyFsmData pKey1FsmData;
KeyFsmData pKey2FsmData;
KeyFsmData pKey3FsmData;
void key_check_init(KeyFsmData *pKeyFsmData, int keyIdx)
{
pKeyFsmData->keyStatus = KS_RELEASE;
pKeyFsmData->lastKeyStatus = KS_RELEASE;
pKeyFsmData->nowKeyStatus = KS_RELEASE;
pKeyFsmData->keyIdx = keyIdx;
}
void multi_key_check_init()
{
key_check_init(&pKey0FsmData, 0);
key_check_init(&pKey1FsmData, 1);
key_check_init(&pKey2FsmData, 2);
key_check_init(&pKey3FsmData, 3);
}
int g_keyPressIdx = -1;
void multi_key_check()
{
bool key0 = key_press_check(&pKey0FsmData);
bool key1 = key_press_check(&pKey1FsmData);
bool key2 = key_press_check(&pKey2FsmData);
bool key3 = key_press_check(&pKey3FsmData);
if (key0 | key1 | key2 | key3)
{
//printf("key0:%d, key1:%d, key2:%d, key3:%d, \r\n", key0, key1, key2, key3);
if (key0)
{
g_keyPressIdx = 0;
}
else if (key1)
{
g_keyPressIdx = 1;
}
else if (key2)
{
g_keyPressIdx = 2;
}
else if (key3)
{
g_keyPressIdx = 3;
}
}
}
int get_press_key_idx()
{
int idx = g_keyPressIdx;
g_keyPressIdx = -1;
return idx;
}
其中,multi_key_check函数,放到50ms周期的定时器中断服务函数中,使按键状态机程序运行起来。
get_press_key_idx函数,用于洗衣机程序来获取不同按键的按下事件,每次获取后,将按键事件清除(g_keyPressIdx设为无效值-1)
3.2 洗衣功能
按照上面绘制的洗衣机状态图,使用switch-case法编写对应的程序即可:
#define WASHER_STATUS_ENUM(STATUS)\
STATUS(WS_INIT)/*开机初始化自检*/ \
STATUS(WS_IDLE)/*空闲(等待模式设置)状态*/ \
STATUS(WS_ADD_WATER)/*加水状态*/ \
STATUS(WS_WASH)/*清洗状态*/ \
STATUS(WS_DRAIN_WATER)/*排水状态*/ \
STATUS(WS_SPIN_DRY)/*甩干状态*/ \
STATUS(WS_PAUSE)/*暂停状态*/ \
STATUS(WS_NUM)/*状态总数(无效状态)*/
typedef enum
{
WASHER_STATUS_ENUM(ENUM_ITEM)
}WASHER_STATUS;
const char* washer_status_name[] = {
WASHER_STATUS_ENUM(ENUM_STRING)
};
WASHER_STATUS g_washerStatus = WS_INIT; //当前循环结束的(状态机的)状态
WASHER_STATUS g_nowWasherStatus = WS_INIT; //当前状态(每次循环后与pKeyFsmData->keyStatus保持一致)
WASHER_STATUS g_lastWasherStatus = WS_INIT; //上次状态(用于记录前一状态以区分状态的来源)
int g_WorkLoopCnt = 0;
int g_WaterLevel = 5; //1~8
int g_WashMode = 2; //暂定为清洗次数:1~3
int g_curWashCnt = 0;
void washer_run_loop()
{
switch(g_washerStatus)
{
/*开机初始化自检*/
case WS_INIT:
g_washerStatus = washer_do_init();
break;
/*空闲(等待模式设置)状态*/
case WS_IDLE:
g_washerStatus = washer_do_idle();
break;
/*加水状态*/
case WS_ADD_WATER:
g_washerStatus = washer_do_add_water();
break;
/*清洗状态*/
case WS_WASH:
g_washerStatus = washer_do_wash();
break;
/*排水状态*/
case WS_DRAIN_WATER:
g_washerStatus = washer_do_drain_water();
break;
/*甩干状态*/
case WS_SPIN_DRY:
g_washerStatus = washer_do_spin_dry();
break;
/*暂停状态*/
case WS_PAUSE:
g_washerStatus = washer_do_pause();
break;
default: break;
}
if (g_washerStatus != g_nowWasherStatus)
{
g_lastWasherStatus = g_nowWasherStatus;
g_nowWasherStatus = g_washerStatus;
//printf("new washer status:%d(%s)\r\n", g_washerStatus, washer_status_name[g_washerStatus]);
}
}
这里将洗衣机不同状态时的处理逻辑,都分别使用函数来实现,并将其返回值作为下一个要跳转的状态。
各个状态的处理函数如下:
/*开机初始化自检*/
WASHER_STATUS washer_do_init()
{
WASHER_STATUS nextStatus = WS_INIT;
g_WorkLoopCnt++;
if (10 == g_WorkLoopCnt) //自检结束
{
printf("default water level:%d\r\n", g_WaterLevel);
printf("default wash mode:%d\r\n", g_WashMode);
printf("washer idle...\r\n");
g_WorkLoopCnt = 0;
nextStatus = WS_IDLE;
}
return nextStatus;
}
/*空闲(等待模式设置)状态*/
WASHER_STATUS washer_do_idle()
{
WASHER_STATUS nextStatus = WS_IDLE;
const WASHER_KEY key = check_key_press();
switch(key)
{
case W_KEY_START_PAUSE:
g_WorkLoopCnt = 0;
nextStatus = WS_ADD_WATER;
printf("add water...\r\n");
break;
case W_KEY_WATER_LEVEL:
g_WaterLevel = (g_WaterLevel == 8) ? 1 : (g_WaterLevel+1);
printf("set water level:%d\r\n", g_WaterLevel);
break;
case W_KEY_WASH_MODE:
g_WashMode = (g_WashMode == 3) ? 1 : (g_WashMode+1);
printf("set wash mode:%d\r\n", g_WashMode);
break;
default: break;
}
return nextStatus;
}
/*加水状态*/
WASHER_STATUS washer_do_add_water()
{
WASHER_STATUS nextStatus = WS_ADD_WATER;
const WASHER_KEY key = check_key_press();
if (key == W_KEY_START_PAUSE)
{
nextStatus = WS_PAUSE;
printf("[%s] pause...\r\n", __func__);
}
else
{
g_WorkLoopCnt++;
if (g_WaterLevel == (g_WorkLoopCnt / 10)) //加水结束
{
g_WorkLoopCnt = 0;
nextStatus = WS_WASH;
printf("[%s] done\r\n", __func__);
printf("wash...\r\n");
}
}
return nextStatus;
}
/*清洗状态*/
WASHER_STATUS washer_do_wash()
{
WASHER_STATUS nextStatus = WS_WASH;
const WASHER_KEY key = check_key_press();
if (key == W_KEY_START_PAUSE)
{
nextStatus = WS_PAUSE;
printf("[%s] pause...\r\n", __func__);
}
else
{
g_WorkLoopCnt++;
if (6 == (g_WorkLoopCnt / 10)) //清洗结束
{
g_WorkLoopCnt = 0;
nextStatus = WS_DRAIN_WATER;
printf("[%s] done\r\n", __func__);
printf("drain water...\r\n");
}
}
return nextStatus;
}
/*排水状态*/
WASHER_STATUS washer_do_drain_water()
{
WASHER_STATUS nextStatus = WS_DRAIN_WATER;
const WASHER_KEY key = check_key_press();
if (key == W_KEY_START_PAUSE)
{
nextStatus = WS_PAUSE;
printf("[%s] pause...\r\n", __func__);
}
else
{
g_WorkLoopCnt++;
if (3 == (g_WorkLoopCnt / 10)) //排水结束
{
printf("[%s] done\r\n", __func__);
g_curWashCnt++;
printf("current wash and drain count:%d(target:%d)\r\n", g_curWashCnt, g_WashMode);
g_WorkLoopCnt = 0;
if (g_WashMode == g_curWashCnt)
{
printf("spin dry...\r\n");
nextStatus = WS_SPIN_DRY;
}
else
{
printf("add water...\r\n");
nextStatus = WS_ADD_WATER;
}
}
}
return nextStatus;
}
/*甩干状态*/
WASHER_STATUS washer_do_spin_dry()
{
WASHER_STATUS nextStatus = WS_SPIN_DRY;
const WASHER_KEY key = check_key_press();
if (key == W_KEY_START_PAUSE)
{
nextStatus = WS_PAUSE;
printf("[%s] pause...\r\n", __func__);
}
else
{
g_WorkLoopCnt++;
if (100 == g_WorkLoopCnt) //甩干结束
{
g_WorkLoopCnt = 0;
nextStatus = WS_IDLE;
printf("[%s] done\r\n", __func__);
printf("enter idle mode\r\n");
}
}
return nextStatus;
}
/*暂停状态*/
WASHER_STATUS washer_do_pause()
{
WASHER_STATUS nextStatus = WS_PAUSE;
const WASHER_KEY key = check_key_press();
if (key != W_KEY_NULL)
{
switch (g_lastWasherStatus)
{
case WS_ADD_WATER: nextStatus = WS_ADD_WATER; printf("resume...\r\n"); break;
case WS_WASH: nextStatus = WS_WASH; printf("resume...\r\n"); break;
case WS_DRAIN_WATER: nextStatus = WS_DRAIN_WATER; printf("resume...\r\n"); break;
case WS_SPIN_DRY: nextStatus = WS_SPIN_DRY; printf("resume...\r\n"); break;
default: break;
}
}
return nextStatus;
}
对于按键的获取,这里定义了几个对应的功能按键,并从按键状态机函数中获取按键索引,再转为洗衣机程序所需的对应功能按键:
typedef enum
{
W_KEY_NULL, //没有按键按下
W_KEY_POWER, //电源键按下
W_KEY_WATER_LEVEL, //水位键按下
W_KEY_WASH_MODE, //清洗模式键按下
W_KEY_START_PAUSE //启动/暂停键按下
}WASHER_KEY;
WASHER_KEY check_key_press()
{
WASHER_KEY washerKey = W_KEY_NULL;
int idx = get_press_key_idx();
if (idx != -1)
{
switch(idx)
{
case 0: washerKey = W_KEY_POWER; break; //电源键按下
case 1: washerKey = W_KEY_WATER_LEVEL; break; //水位键按下
case 2: washerKey = W_KEY_WASH_MODE; break; //清洗模式键按下
case 3: washerKey = W_KEY_START_PAUSE; break; //启动/暂停键按下
default: break;
}
//printf("%s idx:%d -> washerKey:%d\r\n", __func__, idx, washerKey);
}
return washerKey;
}
洗衣机状态机主程序,可以放到main函数中,每隔100ms调用一次,使其运行起来:
int main(void)
{
delay_init();
KEY_Init();
uart_init(115200);
TIM3_Int_Init(500-1,7200-1); //调用定时器使得50ms产生一个中断
printf("hello\r\n");
while(1)
{
washer_run_loop();
delay_ms(100);
}
}
3.3 测试
将代码烧写到STM32板子中,通过3个按键(电源键暂不使用)操作,并通过串口打印,查看全自动洗衣机的运行情况:
4 总结
本篇实现了一款全自动洗衣机的基础洗衣控制流程,可实现不同水位与清洗次数的设置,以及任务的暂停与继续。此外,通过对之前按键状态机的进一步优化修改,实现了按键状态机的复用,实现多个按键的检测。下篇文章将进一步进行功能优化,添加OLED小屏幕实现不同状态的可视化展示。
状态机
状态机在软件编程中非常重要,一个思路清晰而且高效的程序,必然有状态机的身影浮现。比如在按键命令解析程序中,本来是在状态1中,触发一个按键后切换到状态2,再触发另一个按键切换到状态3,或者返回到状态1。按键的击键过程也是一种状态的切换,也可以看着是一个状态机,一个按键的击键过程包括:按下、抖动、闭合、抖动和释放等状态。我们只要把这些状态机的思想想办法用程序表示出来就可以了。
3、按键的状态机实现
我们这里用状态机是为解决问题的,那么我们就要从问题本身去思考。为了实现按键扫描,达到按键短按和长按的功能,可以根据一个按键从按下按键到释放按键的整个过程将按键分为4个状态:
S0:等待按键按下
S1:按键按下
S2:等待按键短按释放
S3:等待按键长按释放
假设按键按下为低电平“0”,按键未按下为高电平“1”,按键的整个过程我们就可以通过状态转移图表示出来,如图1所示。
图1:按键的状态转移图
首先,按键的初始状态为S0,当检测到输入为1时,表示按键没有按下,保持S0;当输入为0时,表示按键按下,状态转入S1。在S1状态中,检测输入信号是否为0,如果为0,执行按键程序转入S2;如果为1,表示之前的按键操作是干扰信号,回到S0。在S2状态中,如果输入信号是1,则回到S0,表示按键短按已经释放;如果按键没有释放,输入为0时,就开始计时,计时没有结束前一直在S2,当计时结束了,转入S3,表示按键一直按着,为长按功能,在S2计时过程中,输入从0变为1也会回到S0。在S3状态中,输入信号为1,返回S0,表示按键长按释放;输入信号为0,执行相应的按键程序,也可以计时,等计时结束执行按键程序,达到按键连击的功能。这就是采用状态机进行按键检测达到短按与长按的整个过程。
下面以四个按键接在P1的P1.7、P1.6、P1.5、P1.4,设计状态机按键扫描程序。
程序代码如下:
#defineS00//状态0
#defineS11//状态1
#defineS22//状态2
#defineS33//状态3
voidkey()
{staticunsignedcharstate=S0,key_time;
unsignedcharkey;
key=P1&0xf0;//屏蔽P1低四位
switch(state)//检测状态
{caseS0://状态0
if(key!=0xf0)state=S1;break;//判断输入是否为0,为0转入状态1
caseS1://状态1
if(key==0xf0)state=S0;//判断输入是否为1,为1返回状态0
else//否则,转入状态2,执行按键程序
{state=S2;
switch(key)
{case0xe0:/*按键1执行程序*/
break;
case0xd0:/*按键2执行程序*/break;
case0xb0:/*按键3执行程序*/break;
case0x70:/*按键4执行程序*/break;}}
break;
caseS2://状态2
if(key==0xf0)state=S0;//判断输入是否为1,为1返回状态0
elseif(++key_time==100){key_
time=0;state=S3;}break;
//否则开始计时,计时结束转入状态3
caseS3://状态3
if(key==0xf0)state=S0;//判断输入是否为1,为1返回状态0
elseif(++key_time==5)//否则开始计时,计时结束按键连击
{key_time=0;
switch(key){
case0xe0:break;
case0xd0:break;
case0xb0:break;
case0x70:break;}
}break;}}
4、中断处理按键消抖
通常使用的按键都是机械弹性按键,也就是轻触开关。机械按键在触点的闭合和断开的过程中会产生抖动,一个按键在按下时不会立刻稳定的导通,在释放时也一样,不会一下子就断开,在按下和释放瞬间都会有一连串的抖动现象。按键的抖动时间有按键的机械特性决定,一般情况为5ms~10ms。这种抖动人是感觉不出来的,但是单片机的运行速度是微秒级的,这里可以设计一个定时中断来检测按键的状态,通过定时中断来消除按键抖动问题。因此可以把定时器的时间设置为10ms,每隔10ms进入一次中断检测一次按键的状态。
5、总结
以按键功能,介绍了状态机的原理与按键状态机实例,实现按键单击、双击、长按等状态的检测。本文介绍的这种以状态机来实现按键检测的方法,与一般的按键检测方法相对比,能完成案件的多种状态的检测,实现按键的短按和长按功能。采用状态机编写的按键程序也大大的改进了按键消抖对CPU运行时间消耗的问题。程序代码简单,维护方便,适用范围广。