背景
在23种设计模式的状态模式中,类的行为是基于它的状态改变的。这种类型的设计模式属于行为型模式。
名词释义
状态模式,其实就是平时所说的状态机,一般指的是FSM(Finite State Machine)有限状态机,分Moore和Mealy两种类型。但这里不讨论具体的状态机形式,只讲其核心思想。状态机分为状态切换和状态执行两部分,将行为和切换条件分离。
C语言应用
其实这个模式对于嵌入式C语言来讲也并不陌生,在C语言中,流程化设计跟状态设计一直是被人用来区分新人跟老手的一个分水岭,但其实两者各有各的好处。作为分水岭存在的,其实是对流程的状态分割,并以分布式形式体现,而状态机的实现方式,恰好对这种设计理念起到引导使用的作用。所以学好状态机,是学好嵌入式设计最重要的一步。
例子
举一个之前第一次看到时很震撼的例子吧,题目:在一串字符串"stssrtsstrttsr"中找到字符串"str"的个数。
- 流程实现
#include <stdint.h>
#include <string.h>
/* Sourct为字符串源数据,tag为需要查找的目标字符串 */
uint8_t Source[] = "stssrtsstrttsr";
uint8_t Tag[] = "str";
/* sourct为字符串源数据头指针,tag为需要查找的目标字符串头指针 */
/* 返回值为str字符串的个数 */
uint8_t GetStrNum(uint8_t *source,
uint8_t *tag)
{
uint8_t num = 0;
uint8_t i, j;
/* 遍历一次源数据,每进一个字符判断一次目标字符串 */
for (i = 0; i < (strlen(source) - strlen(tag) + 1); i++)
{
for (j = 0; j < strlen(tag); j++)
{
if (source[i + j] != tag[j])
{
break;
}
}
/* j可以加满,说明找到了对应字符串 */
if (j == strlen(tag))
{
num++;
}
}
return num;
}
int main(void)
{
printf("源数据中存在str字符串的个数为:%d\n", GetStrNum(Source, Tag));
return 0;
}
这里可以看到,除了对源数据遍历了一次,还对目标字符串遍历了一次。如果源数据及目标数据很长且源数据有一部分相同时,这个查找效率会变得很低。
那有没有什么办法可以只查找一次就能得出结果的?当然有,开始进入主题喽,我们试下用状态机来实现。
- 普通状态机
#include <stdint.h>
#include <string.h>
/* Source为字符串源数据,tag为需要查找的目标字符串 */
uint8_t Source[] = "stssrtsstrttsr";
uint8_t Tag[] = "str";
/* 状态枚举 */
eunm emTagSta
{
TAGSTA_idle, /* 空闲状态 */
TAGSTA_s, /* 识别到s */
TAGSTA_st, /* 已识别到st */
};
/* source为字符串源数据头指针,tag为需要查找的目标字符串头指针 */
/* 返回值为str字符串的个数 */
uint8_t GetStrNum(uint8_t *source,
uint8_t *tag)
{
uint8_t num = 0;
uint8_t i;
/* 对源数据从头到尾遍历一遍 */
for (i = 0; i < (strlen(source) - strlen(tag) + 1); i++)
{
switch (sta)
{
/* 空闲状态下,只识别字符's' */
case TAGSTA_idle:
{
if (source[i] == tag[0])
{
sta = TAGSTA_s;
}
break;
}
/* 识别到t,则进入下一状态,识别到s,则保持当前状态,识别到其他的,则跳回空闲状态 */
case TAGSTA_s:
{
if (source[i] == tag[1])
{
sta = TAGSTA_st;
}
else if (source[i] == tag[0])
{
sta = TAGSTA_s;
}
else
{
sta = TAGSTA_idle;
}
break;
}
/* 识别到'r'则个数+1,并恢复为空闲状态 */
case TAGSTA_st:
{
if (source[i] == tag[2])
{
num++;
}
else if (source[i] == tag[0])
{
sta = TAGSTA_s;
}
sta = TAGSTA_idle;
break;
}
/* 出现异常时,恢复空闲状态 */
defaule:
{
sta = TAGSTA_idle;
break;
}
}
}
return num;
}
int main(void)
{
printf("源数据中存在str字符串的个数为:%d\n", GetStrNum(Source, Tag));
return 0;
}
到这里可能有人会有疑问:这样子如果我换个目标字符串不是整个状态机都得改了么?确实,所以这里这个例子使用状态机并不是很合理,只是提供这么一个思路。这种类型的状态一般是用在通信上,特别是字符型的通信,因为通信上协议一旦定下来,其内容一般是不会怎么变化的,所以使用以上这种方式是一个绝佳的方案。
- 状态机跟链表结合
#include <stdint.h>
#include <stdlib.h>
struct tagStaList
{
void (*Func)(void *);
void *Pt;
struct tagStaList *Next;
};
/* 定义链表头指针 */
static struct tagStaList *ListHead = NULL;
/* 添加链节点 */
void AddList(struct tagStaList *next)
{
struct tagStaList *list = ListHead;
if (NULL == ListHead)
{
ListHead = next;
}
else
{
for (;NULL != list->Next; list = list->Next);
list->Next = next;
}
}
void StaMachine(void)
{
struct tagStaList *list = HeadList;
/* 链接状态-按状态执行顺序链接 */
AddList();
/* 遍历状态执行 */
for (;NULL != list->Next; list = list->Next)
{
list->Func(list->Pt);
}
}
- 状态机跟表驱动结合
struct tagSta
{
void (*Func)(void *);
void *Pt;
};
void FuncIdle(void *pt)
{
/* do something */
}
void Func1(void *pt)
{
/* do something */
}
void Func2(void *pt)
{
/* do something */
}
/* 状态执行表 */
struct tagSta StaTable[] =
{
{FuncIdle, NULL},
{Func1, NULL},
{Func2, NULL},
};
void StaMachine(void)
{
static uint8_t sta = 0;
/* 状态切换 */
sta = (sta + 1) % (sizeof(StaTable) / sizeof(StaTable[0]));
/* 状态执行 */
StaTable[sta].Func(StaTable[sta].Pt);
}
再举个通信的例子,对于通信报文的解析,一般想法是拿到一帧完整报文再进行内容拆解,即识别帧头、地址、数据、帧尾等信息,所有的解析均在一个周期内完成,在大部分上位机中开发都不例外,因为大部分API都是只能获取整帧报文的数据。那既然用到单片机开发,那就得干点单片机才能干的高效操作,以串口为例,底层获取数据的接口是一个字节,所以最高效的手法可以是每获取到一个字节就进行解析,这样在接收完数据时即完成了数据的解析。具体可以参考FreeModbus里对于ASCII报文的解析。接收解析部分的源码就贴在下面,可以学习学习。
/* ----------------------- Defines ------------------------------------------*/
#define MB_ASCII_DEFAULT_CR '\r' /*!< Default CR character for Modbus ASCII. */
#define MB_ASCII_DEFAULT_LF '\n' /*!< Default LF character for Modbus ASCII. */
#define MB_SER_PDU_SIZE_MIN 3 /*!< Minimum size of a Modbus ASCII frame. */
#define MB_SER_PDU_SIZE_MAX 256 /*!< Maximum size of a Modbus ASCII frame. */
#define MB_SER_PDU_SIZE_LRC 1 /*!< Size of LRC field in PDU. */
#define MB_SER_PDU_ADDR_OFF 0 /*!< Offset of slave address in Ser-PDU. */
#define MB_SER_PDU_PDU_OFF 1 /*!< Offset of Modbus-PDU in Ser-PDU. */
/* ----------------------- Type definitions ---------------------------------*/
typedef enum
{
STATE_RX_IDLE, /*!< Receiver is in idle state. */
STATE_RX_RCV, /*!< Frame is beeing received. */
STATE_RX_WAIT_EOF /*!< Wait for End of Frame. */
} eMBRcvState;
BOOL
xMBASCIIReceiveFSM( void )
{
BOOL xNeedPoll = FALSE;
UCHAR ucByte;
UCHAR ucResult;
assert( eSndState == STATE_TX_IDLE );
( void )xMBPortSerialGetByte( ( CHAR * ) & ucByte );
switch ( eRcvState )
{
/* A new character is received. If the character is a ':' the input
* buffer is cleared. A CR-character signals the end of the data
* block. Other characters are part of the data block and their
* ASCII value is converted back to a binary representation.
*/
case STATE_RX_RCV:
/* Enable timer for character timeout. */
vMBPortTimersEnable( );
if( ucByte == ':' )
{
/* Empty receive buffer. */
eBytePos = BYTE_HIGH_NIBBLE;
usRcvBufferPos = 0;
}
else if( ucByte == MB_ASCII_DEFAULT_CR )
{
eRcvState = STATE_RX_WAIT_EOF;
}
else
{
ucResult = prvucMBCHAR2BIN( ucByte );
switch ( eBytePos )
{
/* High nibble of the byte comes first. We check for
* a buffer overflow here. */
case BYTE_HIGH_NIBBLE:
if( usRcvBufferPos < MB_SER_PDU_SIZE_MAX )
{
ucASCIIBuf[usRcvBufferPos] = ( UCHAR )( ucResult << 4 );
eBytePos = BYTE_LOW_NIBBLE;
break;
}
else
{
/* not handled in Modbus specification but seems
* a resonable implementation. */
eRcvState = STATE_RX_IDLE;
/* Disable previously activated timer because of error state. */
vMBPortTimersDisable( );
}
break;
case BYTE_LOW_NIBBLE:
ucASCIIBuf[usRcvBufferPos] |= ucResult;
usRcvBufferPos++;
eBytePos = BYTE_HIGH_NIBBLE;
break;
}
}
break;
case STATE_RX_WAIT_EOF:
if( ucByte == ucMBLFCharacter )
{
/* Disable character timeout timer because all characters are
* received. */
vMBPortTimersDisable( );
/* Receiver is again in idle state. */
eRcvState = STATE_RX_IDLE;
/* Notify the caller of eMBASCIIReceive that a new frame
* was received. */
xNeedPoll = xMBPortEventPost( EV_FRAME_RECEIVED );
}
else if( ucByte == ':' )
{
/* Empty receive buffer and back to receive state. */
eBytePos = BYTE_HIGH_NIBBLE;
usRcvBufferPos = 0;
eRcvState = STATE_RX_RCV;
/* Enable timer for character timeout. */
vMBPortTimersEnable( );
}
else
{
/* Frame is not okay. Delete entire frame. */
eRcvState = STATE_RX_IDLE;
}
break;
case STATE_RX_IDLE:
if( ucByte == ':' )
{
/* Enable timer for character timeout. */
vMBPortTimersEnable( );
/* Reset the input buffers to store the frame. */
usRcvBufferPos = 0;;
eBytePos = BYTE_HIGH_NIBBLE;
eRcvState = STATE_RX_RCV;
}
break;
}
return xNeedPoll;
}
适用范围
- 大量使用if-else的场景,有大量条件分支时,可考虑使用状态机实现。
- 对有明显时序划分的,也可通过状态机实现分时执行。
优势
- 对新手来讲,有很好的引导思维的作用,多使用状态分割,多考虑分布式的结构。
- 状态切换一目了然,有助与对所有事项进行全面思考,不容易存在遗漏。
劣势
- 在多线程中使用时,需要多考虑同步异步的问题。
- 状态变多后,状态机会变得异常复杂,每增加一个状态机时需要考虑各个到其他每个状态的切换关系。当然这里可以通过分割子状态机解决此问题。