如今,家电越来越普及,智能化程度也越来越高,对软件的质量也相应提出了新的要求。
对于一个业内软件开发者,经常要“拷问”自己的问题就是:“我该怎么写才容易改?我该怎么写才不容易出错?”。在软件相关行业中,代码更改几乎总是会发生的,而且可能不是一次两次。造成更改的原因有很多种,如:需求改变、添加/删除功能、软件自身有bug等等。在家电业中,在你对一款产品编写代码时,你应该意识到,它可能还有后续衍生产品,可能会添加或更改一些功能。在业内混过的coder都应该经历过这样的情况:在产品批产前或批产后,发现软件还存在bug,于是加班找bug,加班返工,被老板批,被上司骂,很不舒服。
更改在所难免,我们是否可以让“改”变得更容易;bug无法避免,我们是否可以尽量减少它。下面我就谈谈我的一些经验,抛砖引玉。不足之处,不吝赐教。
在下面的讨论中,我们谈论的都是家电,不再每次都重复这两个字了。
从大的架构上讲,使用的绝大多数都是前后台。将系统要执行的任务放在while循环中,将一些对响应速度要求高的代码放在中断服务程序中。如下所示:
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函数结构。*/
...
}
看看上面的代码,很明显能看出它有以下几点稍显不足:
实际上,上面那些不足都是可以避免的。具体思想如下(有限状态机概念):
各个状态对应有相对独立的.c文件,其中包含该状态自己的按键处理、显示控制、负载控制及时间控制等函数。在运行时,当符合某状态迁移条件时(如,在关机状态按电源键迁移到设置状态,预约状态时间到迁移到工作状态等),将目标状态的各逻辑处理函数放入预先定义好的函数指针变量中。主系统循环只要从函数指针变量中取得对应函数的地址并调用它就可以了。其实挺简单是不是。带来的好处是显而易见的,比如工作状态出现了工作逻辑或显示逻辑问题,我知道问题肯定在State_Work.c中,我打开它并且编辑它,而且我确切知道我不会影响其它任何状态。要添加一个新的状态同样很方便,新建一个.c文件,写入该状态的逻辑处理函数就OK了。没有重复的状态检测,整个结构清爽干净。同时速度也快了些。
具体的实现代码我就不写在这里了,有兴趣的可以下载附件中的文件看看。
附件中的代码我只写了3个状态,下面看看各状态运行时截图:
文章评论(0条评论)
登录后参与讨论