前面的文章基于按键功能介绍了状态机原理和按键状态机示例,实现了按键点击、双击、长按等状态的检测。
在这篇文章中,我们继续使用状态机编程来实现一个更有趣的功能——全自动洗衣机。
1 全自动洗衣机功能分析
下图是全自动洗衣机的控制面板:
面板上有4个按钮:
开始/暂停:用于开始或暂停洗衣任务
面板上还有数码管显示当前工作状态和剩余时间。 可显示的工作模式有:
在本文中,我们将根据这款洗衣机的操作方法,实现相应的洗衣机控制逻辑。 需要注意的事项是:
2 绘制状态图
根据以上分析的全自动洗衣机的功能以及我们自己使用洗衣机的经验,可以得出以下全自动洗衣机的状态图:
第一步是打开机器电源。 洗衣机可以进行自检,检查洗衣机各部件是否正常。 这个过程非常短。
然后就处于空闲状态。 此时,用户可以设置水位和洗涤模式。 如果不设置,则默认水位和洗涤模式。
然后,当触发开始按钮时,清洁开始。 一般流程为:加水、清洗、沥干、甩干、结束。
根据不同的清洗方式,加水、清洗、排水三个过程会循环一定次数。
另外,在不同的工作阶段,按暂停键暂停洗衣任务,按继续继续洗衣任务。
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){//省略...
对于本文需要的四个按钮的检测,可以定义四个数据结构,分别调用四次状态机函数,实现代码复用。
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~8int g_WashMode = 2; //暂定为清洗次数:1~3int 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小屏,实现不同状态的直观显示。