一、什么是有限状态机
1.1 有限状态机的定义
有限状态机(FSM: Finite-state machine)是一种用来进行对象行为建模的工具,其作用主要是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。它把复杂的控制逻辑分解成有限个稳定状态,在每个状态上判断事件,变连续处理为离散数字处理,符合计算机的工作特点。同时,因为有限状态机具有有限个状态,所以可以在实际的工程上实现。但这并不意味着其只能进行有限次的处理,相反,有限状态机是闭环系统,有限无穷,可以用有限的状态,处理无穷的事务。在计算机科学中,有限状态机被广泛用于建模应用行为、硬件电路系统设计、软件工程,编译器、网络协议、和计算与语言的研究。
1.2 有限状态机的要素
根据有限状态机的定义,可以提炼出几个构成要素,首先是被分解为有限个当前状态,在每个状态上判断事件是否发生,外界事件触发后的响应动作,事件响应后的状态迁移等四个要素。
下面用一个图示和一个数据结构分别描述一个状态机单元:
typedef struct FsmTable_s
{
uint8_t event; /* 触发事件 */
uint8_t CurState; /* 当前状态 */
void (*eventActFun)(void *); /* 动作函数 */
uint8_t NextState; /* 跳转状态 */
}FsmTable_T;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
1.3 有限状态机的C语言实现
前面用一个结构体FsmTable_s描述一个当前状态及其触发事件、动作函数和跳转状态等。如果要描述该状态机所有的状态,则可以将每个状态以数组或链表的形式组织起来,考虑到数组较简单且我们需要时刻直到当前所处的状态,故再用一个结构体表示一个状态机如下:
typedef struct FSM_s
{
FsmTable_T *FsmTable; /* 状态迁移表 */
uint8_t curState; /* 状态机当前状态 */
uint8_t stuMaxNum; /* 状态机状态迁移数量 */
}FSM_T;
- 1
- 2
- 3
- 4
- 5
- 6
该结构体中一个指针配合一个状态数量构成一个状态数组,再加上当前状态便可描述该状态机的所有状态及当前所处的状态。
由前面状态机的定义可知,状态机是由事件驱动的,我们拿到一个状态机首先要了解其有哪些状态、当前处于哪个状态,其次要清楚当前状态有事件触发时如何响应,所以状态机的实现最重要的是状态机的初始化和事件响应。前面已经给出了描述状态机的数据结构,下面给出状态机的初始化函数与事件响应函数如下:
/*==================================================================
* Function : FSM_Init
* Description : 状态机初始化
* Input Para : pFsm状态机对象,pTable状态迁移表,stuMaxNum迁移表数量
* curState当前状态
* Output Para :
* Return Value:
==================================================================*/
void FSM_Init(FSM_T *pFsm, FsmTable_T *pTable, uint8_t stuMaxNum, uint8_t curState)
{
pFsm->FsmTable = pTable;
pFsm->curState = curState;
pFsm->stuMaxNum = stuMaxNum;
}
/*==================================================================
* Function : FSM_EventHandle
* Description : 状态机处理函数
* Input Para : pFsm状态机对象, event触发事件, parm动作执行参数
* Output Para :
* Return Value:
==================================================================*/
void FSM_EventHandle(FSM_T *pFsm, uint8_t event, void *parm)
{
FsmTable_T *pAcTable = pFsm->FsmTable;
void (*eventActFun)(void *) = NULL;
uint8_t NextState;
uint8_t CurState = pFsm->curState;
uint8_t flag = 0;
for (uint8_t i = 0; i < pFsm->stuMaxNum; i++)// 遍历状态表
{
if (event == pAcTable[i].event && CurState == pAcTable[i].CurState)
{
flag = 1;
eventActFun = pAcTable[i].eventActFun;
NextState = pAcTable[i].NextState;
break;
}
}
if (flag)
{
if (eventActFun != NULL)
{
eventActFun(parm); // 执行相应动作
}
pFsm->curState = NextState; // 状态转换
}
else
{
// do nothing
}
}
- 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
一个状态机一般都有起始状态和终止状态,从起始状态执行若干次有限的状态迁移后总能到达终止状态,但如果终止状态后迁移到起始状态构成一个闭环系统,状态机就可以处理无限多的事件响应。
一个状态机某一时刻只能处于其中的一种状态,而不能同时处于多种状态,所以状态机的不同状态间是相互独立互斥的,可以利用状态机的这种特性进行任务分割,减少事件响应的执行指令数量,使代码逻辑更直观。
二、状态机在MCU程序设计中的应用
状态机应用范围挺广的,从游戏开发(比如超级马里奥)到网络协议(比如WIFI状态机)都可以找到状态机模型的身影。本文想从操作系统演变的前身来谈状态机模型,在无操作系统的环境中使用状态机的事件驱动模型,选择MCU再合适不过。
2.1 MCU的多任务程序结构设计
MCU由于内部资源的限制,如非必要,通常不会使用OS (Operating System)。对于无 OS的系统,流行的设计是主程序(主循环 ) + (定时)中断,这种结构虽然符合自然想法,不过却有很多不利之处,首先是中断可以在主程序的任何地方发生,随意打断主程序。其次主程序与中断之间的耦合性较大,这种做法使得主程序与中断缠绕在一起,必须仔细处理以防不测。
那么换一种思路,如果把主程序全部放入(定时)中断中会怎么样?这么做至少可以立即看到几个好处: 系统可以处于低功耗的休眠状态,将由中断唤醒进入主程序; 如果程序跑飞,则中断可以拉回;没有了主从之分(其他中断另计),程序易于模块化。
为了把主程序全部放入(定时)中断中,必须把程序划分成一个个的模块,即任务,每个任务完成一个特定的功能,例如扫描键盘并检测按键。 设定一个合理的时基 (tick), 例如 5, 10 或 20 ms, 每次定时中断,把所有任务执行一遍,为减少复杂性,一般不做动态调度(最多使用固定数组以简化设计,做动态调度就接近OS了),这实际上是一种无优先级时间片轮循的变种。来看看主程序的构成:
void main()
{
…. // Initialize
while (true) {
IDLE; //sleep
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
接下来是定时器中断处理函数,将主程序需要完成的任务分解为N个任务,当定时器触发中断时,依次执行所有的任务,示例代码如下:
void Timer_Interrupt()
{
SetTimer(); //设置定时器
ResetStack(); //重置堆栈
Enable_Timer_Interrupt; //使能定时器中断
RunTask1(); //任务一
RunTask2(); //任务二
…
RunTaskN(); //任务N
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
为简化程序逻辑,可以使用一个函数指针数组来管理多个任务,示例代码如下:
#define CountOfArray(x) (sizeof(x)/sizeof(x[0]))
typedef void (*FUNCTIONPTR)(); //函数指针类型
const FUNCTIONPTR[] tasks = {
RunTask1,
RunTask2,
…
RunTaskN
};
void Timer_Interrupt()
{
SetTimer();
ResetStack();
Enable_Timer_Interrupt;
FUNCTIONPTR pFunc;
for (i=0; i<CountOfArray (tasks), i++){
pFunc = tasks[i];
if(pFunc != NULL){
(*pFunc)();
}
}
}
- 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
上面的程序已经在MCU上实现了多任务处理,而且把主程序放到了中断处理函数中执行,让MCU在每个定时周期执行完任务后有休息的时间,同时降低了功耗。但中断处理函数内的任务不宜过多,其执行时间不能超过定时周期,为保证任务的执行不会扰乱定时中断的触发,需要将中断处理函数中的任务进行分割处理。
MCU上运行的任务,大多涉及到与外设的通讯,自然离不开中断触发与事件触发,状态机正好是事件驱动模型,能将任务分割为有限的状态,所以这里的任务使用状态机模型进行分片处理再合适不过。
2.2 使用状态机分割任务
在实际的MCU程序开发中,不同的任务依据触发事件和状态进行分割也并不是太难,比较经典的是WIFI模块与蓝牙模块协议栈的开发,其完全使用状态机模型实现协议栈的功能。由于协议栈较复杂,以后有机会再详述,下面侧重状态机对任务的分割,至于分割依据视具体情况而定,这里就先略去了。
下面先给出一个状态机分割任务的示例,使用switch-case实现:
void RunTaskN(state)
{
switch (state) { //根据不同的状态,执行相应的任务
case 0:
state0();
break;
case 1:
state1();
break;
…
case M:
stateM();
break;
default:
state = 0;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
当然这里的switch-case也可以用函数指针数组实现,其中的状态state就是数组下标,如果状态是字符串表示,可以通过enum枚举类型将字符串对应为数字下标,使用函数指针数组的代码示例如下:
const FUNCTIONPTR[] states = {
state0,
state1,
…,
stateM
};
void RunTaskN(state)
{
(*states[state])();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
对于不能用状态机分割的任务,可以根据其执行时间,使用关中断的方式独占执行,或者在另一个定时中断处理函数中执行。
上面的多任务程序都放到一个定时中断处理函数中执行实际上是简化模型了,通常情况下每个任务都有自己的触发事件和中断处理函数,每个任务又可以划分为很多不同的状态,每次事件触发中断进入相应的中断处理函数中根据所处的状态执行相应的动作,能减少中断处理函数的执行时间,使程序开发更直观。
前面已经给出了一个WIFI状态机的文章链接,下面再贴出一段BLE事件响应函数供参考:
/**@brief Function for handling BLE events.
*
* @param[in] p_ble_evt Bluetooth stack event.
* @param[in] p_context Unused.
*/
static void ble_evt_handler(ble_evt_t const * p_ble_evt, void * p_context)
{
uint32_t err_code;
switch (p_ble_evt->header.evt_id)
{
case BLE_GAP_EVT_CONNECTED:
NRF_LOG_INFO("Connected");
err_code = bsp_indication_set(BSP_INDICATE_CONNECTED);
APP_ERROR_CHECK(err_code);
m_conn_handle = p_ble_evt->evt.gap_evt.conn_handle;
err_code = nrf_ble_qwr_conn_handle_assign(&m_qwr, m_conn_handle);
APP_ERROR_CHECK(err_code);
break;
case BLE_GAP_EVT_DISCONNECTED:
NRF_LOG_INFO("Disconnected");
// LED indication will be changed when advertising starts.
m_conn_handle = BLE_CONN_HANDLE_INVALID;
break;
case BLE_GAP_EVT_PHY_UPDATE_REQUEST:
{
NRF_LOG_DEBUG("PHY update request.");
ble_gap_phys_t const phys =
{
.rx_phys = BLE_GAP_PHY_AUTO,
.tx_phys = BLE_GAP_PHY_AUTO,
};
err_code = sd_ble_gap_phy_update(p_ble_evt->evt.gap_evt.conn_handle, &phys);
APP_ERROR_CHECK(err_code);
} break;
case BLE_GAP_EVT_SEC_PARAMS_REQUEST:
// Pairing not supported
err_code = sd_ble_gap_sec_params_reply(m_conn_handle, BLE_GAP_SEC_STATUS_PAIRING_NOT_SUPP, NULL, NULL);
APP_ERROR_CHECK(err_code);
break;
case BLE_GATTS_EVT_SYS_ATTR_MISSING:
// No system attributes have been stored.
err_code = sd_ble_gatts_sys_attr_set(m_conn_handle, NULL, 0, 0);
APP_ERROR_CHECK(err_code);
break;
case BLE_GATTC_EVT_TIMEOUT:
// Disconnect on GATT Client timeout event.
err_code = sd_ble_gap_disconnect(p_ble_evt->evt.gattc_evt.conn_handle,
BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
APP_ERROR_CHECK(err_code);
break;
case BLE_GATTS_EVT_TIMEOUT:
// Disconnect on GATT Server timeout event.
err_code = sd_ble_gap_disconnect(p_ble_evt->evt.gatts_evt.conn_handle,
BLE_HCI_REMOTE_USER_TERMINATED_CONNECTION);
APP_ERROR_CHECK(err_code);
break;
default:
// No implementation needed.
break;
}
}
- 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
上面是一个事件响应函数,虽然只是一段局部代码,也可以看出状态机模型的运用。首先该函数的执行就需要事件触发,p_ble_evt->header.evt_id保存了当前的状态,根据当前状态执行相应的动作,满足恰当的条件时当前状态还会迁移到其他状态(通过对状态全局变量赋值实现)。
前面给出了状态机模型的C语言实现,对于事件驱动模型最重要的就是初始状态、当前状态、事件响应,在基于SDK上开发Wi-Fi和BLE程序(状态机模型)时,最重要的工作自然也是编写初始化函数设定初始状态(对应上面的ble_evt_handler事件响应函数的初始化函数名是static void ble_stack_init(void)),然后是编写事件响应函数当事件触发时在不同的状态做出相应的执行动作。
评论记录:
回复评论: