前面几篇文章,以按键功能,介绍了状态机的原理与按键状态机实例,实现按键单击、双击、长按等状态的检测。

本篇,继续使用状态机编程,来实现一个更有趣的功能——全自动洗衣机。

1 全自动洗衣机功能分析

下面是一个全自动洗衣机的控制面板:

面板上有4个按键:

  • 电源:控制洗衣机通电与断电
  • 水位:选择洗衣时需要的水位,有1~8个水位
  • 程序:选择不同的洗衣模式,有1~10个模式
    • 01:标准
    • 02:轻柔
    • 03:快速
    • 10:桶风干
  • 启动/暂停:用于启动或暂停洗衣任务

面板上还有一个数码管,用于显示当前的工作状态与剩余时间,可显示的工作模式有:

  • AA:浸泡
  • BB:洗涤
  • CC:漂洗
  • DD:脱水

本篇,就按照这款洗衣机的操作方式实现对应的洗衣机控制逻辑。需注意的是:

  • 实际的洗衣机有水位检测传感器,本篇中,暂用时间延时模拟水位的增加,且默认开机时水位为0
  • 实际的洗衣机中的洗衣模式,会根据不同的模式自动设置清洗次数每次清洗的时间以及清洗力度,本篇为了简化起见,清洗模式的设置仅用于区分不同的清洗次数
  • 某些特殊的清洗模式,如单独的脱水,桶风干等,本篇暂不实现
  • 对于状态的显示 ,本篇先以串口打印的实现展示,下篇使用OLED小屏幕来显示不同清洗状态的图标等信息

2 画状态图

根据上面分析的全自动洗衣机的功能,以及我们自己使用洗衣机时的经验,可以画出如下的全自动洗衣机的状态图:

首先是上电开机,洗衣机可能会开机自检,检测洗衣机的各个部件是否正常,次过程很短。

然后就处于空闲状态,此时用户可以设置水位与清洗模式,若不设置,则为默认的水位与洗衣模式。

接着触发开始按键后,就开始清洗了,一般流程就是:加水、清洗、排水、甩干、结束。

根据不同的清洗模式,加水、清洗和排水这3个过程会循环执行一定的次数。

另外,在不同的工作阶段,按下暂停键可以让洗衣任务暂停,再按继续可让洗衣任务继续。

3 编程实现

3.1 多按键检测功能

本篇介绍的洗衣机的按键,仅需要检测按键单击即可,不需要双击与长按功能,因此,可使用之前文章介绍的最基础的按键状态机来为洗衣机状态机提供按键事件。

之前介绍的按键状态机,只有一个按键,本篇需要用到4个按键(除去电源键,3个也可以),因此,需要对按键状态机稍加修改,实现按键状态机的复用。

之前介绍的按键状态机,使用了几个全局变量来表示状态,更合理的做法是将其封装起来:

1
2
3
4
5
6
typedef struct {
KEY_STATUS keyStatus; //当前循环结束的(状态机的)状态
KEY_STATUS nowKeyStatus; //当前状态(每次循环后与pKeyFsmData->keyStatus保持一致)
KEY_STATUS lastKeyStatus; //上次状态(用于记录前一状态以区分状态的来源)
int keyIdx;
}KeyFsmData;

注意,额外增加了一个按键索引值,用来告诉状态机要检测哪个按键。

再将原来的按键状态机程序,通过入参的形式将上述定义的结构体传入,并通过函数返回的形式返回按键是否被按下。

这样修改后的按键状态机,就是一个独立的模块了,可以通过传入不同的参数,实现不同按键的检测。

1
2
3
4
5
6
7
bool key_press_check(KeyFsmData *pKeyFsmData)
{
bool bPress = false;

switch(pKeyFsmData->keyStatus)
{
//省略...

对于本篇需要的4个按键的检测,就可以定义4个数据结构体,分别调用4次状态机函数即可,实现代码的复用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
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法编写对应的程序即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
#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]);
}
}

这里将洗衣机不同状态时的处理逻辑,都分别使用函数来实现,并将其返回值作为下一个要跳转的状态。

各个状态的处理函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
/*开机初始化自检*/ 
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;
}

对于按键的获取,这里定义了几个对应的功能按键,并从按键状态机函数中获取按键索引,再转为洗衣机程序所需的对应功能按键:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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调用一次,使其运行起来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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小屏幕实现不同状态的可视化展示。