关于产品开发中状态机设计的讨论极少,很多程序员可能从来都不曾遇到过,但状态机确是一个必须讨论的话题。状态机之所以重要,一方面是因为很多产品在商用时涉及到冗余备份,以保证系统可靠、稳定、安全地运行,从而对软件运行状态从应用上提出了需求;另一方面则因为良好的状态机往往可以使得软件设计中很多系统级的难题迎刃而解,降低软件设计复杂度。简单地讲,可以将状态机作为一种简单的事件分类机制。
状态机在很多领域均有提及,例如随机过程、操作系统等,本文所述状态机类似于操作系统中的进程状态。在关于表驱动的文章中提及TDM模块需求的案例,本文则在此基础上增加产品运行状态相关的需求,以通过实际案例阐述状态机设计。
假设程序员面对下述需求:1)TDM模块是整个产品的一个软件模块,主要完成STM-1/4路由的添加、删除、修改、查询等基本动作,相关的动作命令由用户通过管理维护平台统一下发并转至TDM模块,TDM模块需要根据接收到的命令来完成不同的功能。2)基于可靠性及稳定性等考虑,该产品商用运行时需要进行冗余备份,正常情况下由主用设备完成具体工作,而当主用设备无法正常工作时系统则需要立即切换到备用设备。3)由于增加了状态相关需求,假设TDM模块的入口函数声明如下:
VOID Tdm_Entry(WOTD16 wState, WORD16 wEvent, BYTE *pucIn, WORD32 wLen);
上述关于TDM模块基本功能的需求,已在本系列文章的表驱动文章中进行了讨论,本文不再赘述。而涉及设备冗余备份的需求其实屡见不鲜,例如电厂中央控制室的控制服务器备份,而系统进行冗余备份后,一般情况下主用设备处于主工作状态,备用设备则处于备工作状态。进一步具体到TDM模块的软件设计开发,且从程序员的角度进行抽象,则TDM模块也存在两种运行状态,即主工作状态和备工作状态,不同状态下的操作是有约束的,例如不能允许主用设备与备用设备仅可以添加路由,否则会造成系统冲突。TDM模块的两种状态并不是固定不变的,当主用设备故障后系统切换到备用设备,此时原来的主用设备变更为备用设备,其工作状态由主工作状态跃迁到备工作状态,同时备用设备变更为主用设备,其工作状态则从备工作状态切换到主工作状态,以保证TDM模块运行状态与系统一致。
对比表驱动文章中TDM模块的入口函数,会发现本文上述入口函数多了一个入参wState,其表征TDM模块的运行状态为主工作台或备工作态。一般情况下,入口函数的调度管理一般也是由操作系统进行,而TDM模块的状态也是由操作系统维护管理,因此入口函数中的入参wState用于传入模块当前状态,以保证状态机的正常运行。但是,模块运行状态的跃迁则由程序员根据设计方案自行维护,例如下述示例代码中借助函数Tdm_SetState()进行跃迁。当然,模块也可以使用全局变量避开操作系统自己维护管理运行状态,但毫无疑问风险极大。
基于上述需求分析,TDM模块的状态机设计如下图所示,其状态跃迁路径上的事件即为跃迁条件。
根据上图,TDM模块初始处于STATE_INIT初始化状态,以完成内存检测、数据初始化、芯片或FPGA寄存器设置等初始化工作功能,继而根据设备运行的主备情况进行状态跃迁。当设备为主用设备时,在STATE_INIT状态完成初始化后跃迁到STATE_MASTER主工作状态,该状态下需要响应STM-1/4路由的添加、删除、修改、查询等动作,同时在主用设备故障时需要相应EV_MASTER_TO_SLAVE事件,使自身转为备用设备。当设备为备用设备时,在STATE_INIT状态完成初始化后跃迁到STATE_SLAVE备工作状态,该状态下的权限相对小一点,仅需要响应STM-1/4路由的查询动作,而添加、删除、修改三个动作则被禁止,以避免与主用设备冲突,当然主用设备故障时需要相应EV_SLAVE_TO_MASTER事件以切换为主用设备。
本文并不关心上述需求的具体实现,但针对以上描述并结合状态机设计完成基本代码架构工作,驱动形式则采用表驱动,其中并不涉及过多的软件设计开发准则。以下为上述需求的示例代码。
#include
using namespace std;
typedef void VOID;
typedef unsigned char BOOL;
typedef unsigned char BYTE;
typedef unsigned short WORD16;
typedef unsigned int WORD32;
typedef float FLOAT;
typedef double DOUBLE;
#define EV_INIT (WORD16)0
#define EV_MASTER_POWERON (WORD16)1
#define EV_SLAVE_POWERON (WORD16)2
#define EV_ADD (WORD16)3
#define EV_DELETE (WORD16)4
#define EV_MODIFY (WORD16)5
#define EV_QUERY (WORD16)6
#define EV_MASTER_TO_SLAVE (WORD16)7
#define EV_SLAVE_TO_MASTER (WORD16)8
#define STATE_INIT (WORD16)0
#define STATE_MASTER (WORD16)1
#define STATE_SLAVE (WORD16)2
VOID Tdm_Entry(WORD16 wState, WORD16 wEvent, BYTE *pucIn, WORD32 wLen);
WORD16 Tdm_Init(WORD16 wState, WORD16 wEvent, BYTE *pucIn, WORD32 wLen);
WORD16 Tdm_MasterPowerOn(WORD16 wState, WORD16 wEvent, BYTE *pucIn, WORD32 wLen);
WORD16 Tdm_SlavePowerOn(WORD16 wState, WORD16 wEvent, BYTE *pucIn, WORD32 wLen);
WORD16 Tdm_Add(WORD16 wState, WORD16 wEvent, BYTE *pucIn, WORD32 wLen);
WORD16 Tdm_Delete(WORD16 wState, WORD16 wEvent, BYTE *pucIn, WORD32 wLen);
WORD16 Tdm_Modify(WORD16 wState, WORD16 wEvent, BYTE *pucIn, WORD32 wLen);
WORD16 Tdm_Query(WORD16 wState, WORD16 wEvent, BYTE *pucIn, WORD32 wLen);
WORD16 Tdm_Defalut(WORD16 wState, WORD16 wEvent, BYTE *pucIn, WORD32 wLen);
WORD16 Tdm_MasterToSlave(WORD16 wState, WORD16 wEvent, BYTE *pucIn, WORD32 wLen);
WORD16 Tdm_SlaveToMaster(WORD16 wState, WORD16 wEvent, BYTE *pucIn, WORD32 wLen);
VOID Tdm_SetState(WORD16 wState);
typedef WORD16 (*PFHANDLE)(WORD16 wState, WORD16 wEvent, BYTE *pucIn, WORD32 wLen);
typedef struct
{
WORD16 m_wEvent;
PFHANDLE m_pHandle;
}T_EventTable;
typedef struct
{
WORD16 wState;
T_EventTable *ptEventTable;
WORD16 wEventCnt;
}T_StateTable;
T_EventTable g_atInitEventTable[] =
{
{EV_INIT, Tdm_Init},
{EV_MASTER_POWERON, Tdm_MasterPowerOn},
{EV_SLAVE_POWERON, Tdm_SlavePowerOn},
};
T_EventTable g_atMasterEventTable[] =
{
{EV_ADD, Tdm_Add},
{EV_DELETE, Tdm_Delete},
{EV_MODIFY, Tdm_Modify},
{EV_QUERY, Tdm_Query},
{EV_MASTER_TO_SLAVE, Tdm_MasterToSlave},
};
T_EventTable g_atSlaveEventTable[] =
{
{EV_QUERY, Tdm_Query},
{EV_SLAVE_TO_MASTER, Tdm_SlaveToMaster},
};
T_StateTable g_atStateTable[] =
{
{STATE_INIT, g_atInitEventTable, sizeof(g_atInitEventTable)/sizeof(T_EventTable)},
{STATE_MASTER, g_atMasterEventTable, sizeof(g_atMasterEventTable)/sizeof(T_EventTable)},
{STATE_SLAVE, g_atSlaveEventTable, sizeof(g_atSlaveEventTable)/sizeof(T_EventTable)},
};
int main(int argc, _TCHAR* argv[])
{
WORD16 wState = STATE_INIT;
WORD16 wEvent = EV_SLAVE_POWERON;
WORD32 wLen = 128;
BYTE *pucIn = new BYTE[wLen];
memset(pucIn, 0x00, sizeof(BYTE)*wLen);
Tdm_Entry(wState, wEvent, pucIn, wLen);
delete [] pucIn;
return 0;
}
VOID Tdm_Entry(WORD16 wState, WORD16 wEvent, BYTE *pucIn, WORD32 wLen)
{
WORD16 wStateCnt = sizeof(g_atStateTable) / sizeof(T_StateTable);
PFHANDLE pHandle = Tdm_Defalut;
for (WORD16 wStateIndex=0; wStateIndex
VOID Tdm_Entry(WORD16 wState, WORD16 wEvent, BYTE *pucIn, WORD32 wLen)
{
WORD16 wEventCnt = g_atStateTable[wState].wEventCnt;
PFHANDLE pHandle = Tdm_Defalut;
for (WORD16 wEventIndex=0; wEventIndex
以上针对所假设的TDM模块需求,设计并实现了一种简单的状态机。然而,状态机的并非仅能相应产品开发需求,很多时候借助状态机还可以解决软件设计中的一些系统级问题,例如大量的写文件操作耗时较长,此时模块的运行状态比较独特,可以单独作为一种状态进行隔离。
不可否认,良好的状态机设计是产品开发中的开山利器,但恶劣的状态机设计则是噩梦,会引入各种难以定位的故障。很多时候,过于完整的状态机设计虽然理论上并无缺陷,但对于软件系统的稳定性、可维可测性等则是极大的挑战;与之相反,过于简单的状态机设计一定程度上可能已经丧失了状态机的天然优势,将更多的问题留在软件设计过程中去解决。繁与简的问题是相互对立且不可调和的,但总体上状态机的设计应该遵循“大道至简”的原则。当然,具体问题具体解决,很多时候状态机的设计是需要考虑整个系统或功能模块的系统方案的。
本文对于状态机设计的讨论可能并不深入,但可以此抛砖引玉,有志者可进一步深入研究。