volatile uint8 Flag_SysTick;
int main(void)
{
static uint8 Process_index;
while(true)
{
if (Flag_SysTick)
{
Flag_SysTick = false;
Task_0();
Process_index ++;
switch(Process_index)
{
case 1:
Task_1();
break;
case 2:
Task_2();
break;
case 3:
Task_3();
break;
case 4:
Task_4();
break;
case 5:
Task_5();
Process_index = 0;
break;
default:
Process_index = 0;
break;
}
}
}
return 0;
}
void Int_0(void) interrupt 0
{
Flag_SysTick = true;
}
void Int_1(void) interrupt 1
{
...
}
...
对于家电软件来说,这种结构已经足够好了。
在上面这种大的结构上,我们如何去精心安排,得到易写易改不易错的软件呢?
先来举个例子,看看传统方法的不足,再来想想解决办法。
产品往往有多种状态,在各状态下,往往有不同的显示规则,按键响应规则,负载控制规则及时间处理规则。下面是一款洗衣机的各个状态及其描述:
状态 | 描述 | 按键响应规则描述 | 显示规则 | 负载控制 | 时间处理 |
State_Off | 关机状态 | 按启动键,进入设置状态 | 无显示 | 关闭所有负载 | 无时间处理 |
State_Set | 设置状态 | … | … | … | 10分钟无用户操作,进入关机状态 |
State_FuzzyCheck | 模糊检状态 | … | … | … | … |
State_Error | 故障状态 | … | … | … | … |
State_Service | 服务状态 | … | … | … | … |
State_VersionSetting | 变种设置状态 | … | … | … | … |
State_UITest | 界面检测状态 | … | … | … | … |
State_NoWaterCheck | 无水检状态 | … | … | … | … |
State_FactoryTest | 工厂模式 | … | … | … | … |
State_BurnTest | 老化模式 | … | … | … | … |
State_SpinTest | 连脱测试 | … | … | … | … |
State_?? | 后续可能添加 | … | … | … | … |
大部分产品规格书都会有上图类似的说明,按状态分章节描述各状态的按键响应规则、显示控制规则、负载控制规则及时间处理规则。按照上表所描述的规则,我们的软件传统上是这样写的:
typedef enum
{
STATE_OFF,
STATE_SET,
STATE_FUZZY_CHECK,
STATE_ERROR,
STATE_SERVICE,
STATE_VERSION_SET,
STATE_UI_TEST,
STATE_NO_WATER_CHECK,
STATE_FACTORY_TEST,
STATE_BURN_TEST,
STATE_SPIN_TEST
}State_t;
State_t System_CurrentState;
/* 按键处理函数 */
void KeyAction_Deal(void)
{
if (System_CurrentState == STATE_OFF)
{
/* 按键处理 */
if (Key_ActionType == KEY_SHORT)
{
if(Key_Code == KEY_POWER)
{
...
}
else if (Key_Code == KEY_START)
{
...
}
}
else if (Key_ActionType == KEY_LONG)
{
if(Key_Code == KEY_POWER)
{
...
}
else if (Key_Code == KEY_START)
{
...
}
}
...
}
else if (System_CurrentState == STATE_SET)
{
/* 按键处理 */
...
...
}
else if (System_CurrentState == STATE_FUZZY_CHECK)
{
/* 按键处理 */
...
...
}
else if (System_CurrentState == STATE_ERROR)
{
/* 按键处理 */
...
...
}
else if (System_CurrentState == STATE_SERVICE)
{
/* 按键处理 */
...
...
}
else if (System_CurrentState == STATE_VERSION_SET)
{
/* 按键处理 */
...
...
}
else if (System_CurrentState == STATE_UI_TEST)
{
/* 按键处理 */
...
...
}
else if (System_CurrentState == STATE_NO_WATER_CHECK)
{
/* 按键处理 */
...
...
}
else if (System_CurrentState == STATE_FACTORY_TEST)
{
/* 按键处理 */
...
...
}
else if (System_CurrentState == STATE_BURN_TEST)
{
/* 按键处理 */
...
...
}
else if (System_CurrentState == STATE_SPIN_TEST)
{
/* 按键处理 */
...
...
}
...
}
/* 显示控制函数 */
void Display_Ctr(void)
{
/* 同 KeyAction_Deal函数结构。*/
...
}
/* 负载控制函数 */
void Load_Ctr(void)
{
/* 同 KeyAction_Deal函数结构。*/
...
}
/* 时间控制函数 */
void Timer_Ctr(void)
{
/* 同 KeyAction_Deal函数结构。*/
...
}
看看上面的代码,很明显能看出它有以下几点稍显不足:
1. 同样的逻辑判断出现了4次。对当前系统状态的判断,在上面4个处理函数中都有出现。
2. 逻辑嵌套较深。
3. 缺乏灵活性。对某一状态进行修改,都要在庞大的if else中找到对应的分支进行修改。(最好是一个状态一个文件,改这个状态就只打开这个文件。)
4. 单片机系统资源的浪费。要执行第n个分支中的语句,都要经历前面n-1个判断,而且还重复了多次。
实际上,上面那些不足都是可以避免的。具体思想如下(有限状态机概念):
各个状态对应有相对独立的.c文件,其中包含该状态自己的按键处理、显示控制、负载控制及时间控制等函数。在运行时,当符合某状态迁移条件时(如,在关机状态按电源键迁移到设置状态,预约状态时间到迁移到工作状态等),将目标状态的各逻辑处理函数放入预先定义好的函数指针变量中。主系统循环只要从函数指针变量中取得对应函数的地址并调用它就可以了。其实挺简单是不是。带来的好处是显而易见的,比如工作状态出现了工作逻辑或显示逻辑问题,我知道问题肯定在State_Work.c中,我打开它并且编辑它,而且我确切知道我不会影响其它任何状态。要添加一个新的状态同样很方便,新建一个.c文件,写入该状态的逻辑处理函数就OK了。没有重复的状态检测,整个结构清爽干净。同时速度也快了些。
这里贴上几个核心文件代码,完整代码请下载附件。
为方便大家调试运行,工程为Visual Studio 2013工程(可执行文件见附件)。移植到你的单片机中应该不难,大家可试试。
工程结构如下:
core.h头文件
#ifndef CORE_H
#define CORE_H
typedef void(*task_t)(void);
typedef struct
{
task_t keyDeal_Handler;
task_t displayCtr_Handler;
task_t loadCtr_Handler;
task_t timeCtr_Handler;
}State_t;
extern State_t State;
void State_Backup(void);
void State_Recovery(void);
void State_KeyActionDeal(void);
void State_DisplayCtr(void);
void State_LoadCtr(void);
void State_TimeCtr(void);
#endif
#include <stdio.h>
#include "system/system.h"
#include "core.h"
State_t State;
static State_t State_old;
void State_Backup(void)
{
State_old = State;
}
void State_Recovery(void)
{
State = State_old;
}
void State_KeyActionDeal(void)
{
State.keyDeal_Handler();
}
void State_DisplayCtr(void)
{
printf("---------------------------操作说明---------------------------\n");
printf("数字小键盘:0 -> 电源 1 -> 加 2 -> 开始 3 -> 减\n");
printf("~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~\n");
State.displayCtr_Handler();
}
void State_LoadCtr(void)
{
System_Load1Enable = FALSE;
System_Load2Enable = FALSE;
System_Load3Enable = FALSE;
State.loadCtr_Handler();
}
void State_TimeCtr(void)
{
State.timeCtr_Handler();
}
state_work.h
#ifndef STATE_WORK_H
#define STATE_WORK_H
void TransitionTO_StateWork(void);
#endif
state_work.c
#include <stdio.h>
#include "system/system.h"
#include "key/key.h"
#include "FSM/core.h"
#include "FSM/state_off/state_off.h"
#include "state_work.h"
static bool_t isPaused;
static void Key_actionDeal(void)
{
if (Key_ActionType == KEY_ACTION_SHORT)
{
if (Key_Code == KEY_POWER)
{
TransitionTO_StateOff();
}
else if (Key_Code == KEY_START)
{
isPaused = !isPaused;
}
printf("\a");
}
}
static void Display_ctr(void)
{
printf("======State_Work状态!======\n\n");
printf("----------操作说明----------\n");
printf("按 电源键 进入关机状态。\n按 开始键 切换暂停启动。\n工作一分钟结束进入关机状态!\n\n");
printf("----------负载状态----------\n");
if (System_Load1Enable)
{
printf("负载1:工作\n");
}
else
{
printf("负载1:不工作\n");
}
if (System_Load2Enable)
{
printf("负载2:工作\n");
}
else
{
printf("负载2:不工作\n");
}
if (System_Load3Enable)
{
printf("负载3:工作\n\n");
}
else
{
printf("负载3:不工作\n\n");
}
printf("----------测试变量值----------\n");
printf("System_TestCount = %d;\n", System_TestCount);
}
static void Load_ctr(void)
{
if (!isPaused)
{
System_Load1Enable = TRUE;
System_Load2Enable = TRUE;
System_Load3Enable = TRUE;
}
}
static void Time_ctr(void)
{
if (!isPaused)
{
System_TestCount++;
if (System_TestCount > 600)
{
TransitionTO_StateOff();
}
}
}
void TransitionTO_StateWork(void)
{
State.keyDeal_Handler = Key_actionDeal;
State.displayCtr_Handler = Display_ctr;
State.loadCtr_Handler = Load_ctr;
State.timeCtr_Handler = Time_ctr;
}
还有一些类似和通用文件,我就不贴了,看附件。
下面贴几张各状态运行效果图:
关机状态:
设置状态:
工作状态:
采用这种分状态和状态迁移的方式去编码,你会发现你的的思路是清晰的,不会因为状态的过多而引入不必要的复杂度。加一个状态,也就是加一个.c文件而已。