嵌入式开发之状态机思维
"在嵌入式系统中,状态机不是可选设计模式,而是控制复杂性的生存必需品。" ——《嵌入式系统设计模式》
(一)何为状态机?
状态机(Finite State Machine, FSM)是一种将系统行为抽象为有限状态集合、状态转移条件和对应动作的数学模型。在嵌入式领域,其核心价值在于:
-
事件驱动:响应外部中断/消息而非轮询
-
确定性:相同输入必得相同状态迁移
-
模块化:复杂逻辑分解为离散状态单元
在一个资源受限的裸机环境下,当业务比较复杂时,如果尝试使用状态机的思想去实现,编程的难度会大大降低,后期软件的升级维护也会变得更加方便。
(二)如何实现一个状态机?
本文主要分两部分进行讲解,先讲解双向链表的实现,然后逐步实现一个基于双向链表的状态机。
2.1 双向链表
研究过RTOS线程的朋友们一定很清楚双向链表,下面的这部分源码也是从RTOS中移植过来的。实现状态机时,使用双向链表主要是为了把不是连续内存的函数入口用链表串起来,方便后续的管理。
2.1.1双向链表节点
/**
* Double List structure
*/
struct rt_list_node
{
struct rt_list_node *next; /**< point to next node. */
struct rt_list_node *prev; /**< point to prev node. */
};
typedef struct rt_list_node rt_list_t; /**< Type for lists. */
这是一个自引用结构体,指针类型为struct rt_list_node *,表示节点之间通过相同结构体相互连接。节点里面有两个 rt_list_t 类型的节点指针 next 和 prev,分别用来指向链 表中的下一个节点和上一个节点。
2.1.2初始化链表节点
/**
* @brief initialize a list
*
* @param l list to be initialized
*/
rt_inline void rt_list_init(rt_list_t *l)
{
l->next = l->prev = l;
}
rt_list_t 类型的节点的初始化,就是将节点里面的 next 和 prev 这两个节点指针指向节点本身。
2.1.3在链表表头后面插入一个节点
/**
* @brief insert a node after a list
*
* @param l list to insert it
* @param n new node to be inserted
*/
rt_inline void rt_list_insert_after(rt_list_t *l, rt_list_t *n)
{
l->next->prev = n;
n->next = l->next;
l->next = n;
n->prev = l;
}
这里主要分为4部分,让 l 的后继节点的 prev 指向 n,然后让 n 的 next 指向 l 的原后继节点,接下来让 l 的 next 指向 n,最后让 n 的 prev 指向 l。
2.1.4 在链表表头前面插入一个节点
/**
* @brief insert a node before a list
*
* @param n new node to be inserted
* @param l list to insert it
*/
rt_inline void rt_list_insert_before(rt_list_t *l, rt_list_t *n)
{
l->prev->next = n;
n->prev = l->prev;
l->prev = n;
n->next = l;
}
这里主要分为4部分,让 l 的原前一个节点的 next 指向 n,然后让 n 的 prev指向 l 的原前继节点,接下来让 l 的 prev 指向 n,最后让 n 的 next 指向 l。
2.1.5从链表删除一个节点
/**
* @brief remove node from list.
* @param n the node to remove from the list.
*/
rt_inline void rt_list_remove(rt_list_t *n)
{
n->next->prev = n->prev;
n->prev->next = n->next;
n->next = n->prev = n;
}
这里主要分为3部分,让n节点的下一个节点的prev指向n节点的上一个节点。然后让n节点的上一个节点的next指向n节点的下一个节点。最后将n节点的next与prev分别指向n。
2.1.6判断链表是否为空
/**
* @brief tests whether a list is empty
* @param l the list to test.
*/
rt_inline int rt_list_isempty(const rt_list_t *l)
{
return l->next == l;
}
这里直接判断节点的下一个节点是不是自己,若是自己return 1代表链表为空,非空则return 0。
2.1.7获取链表长度
/**
* @brief get the list length
* @param l the list to get.
*/
rt_inline unsigned int rt_list_len(const rt_list_t *l)
{
unsigned int len = 0;
const rt_list_t *p = l;
while (p->next != l)
{
p = p->next;
len ++;
}
return len;
}
这里是一直在判断l节点的下一个节点是不是自己,如果不是就判断l的下一个节点的下一个节点是不是自己,同时记录链表长度。当while条件不成立时退出循环,返回链表长度值。
2.2 状态机初始化
2.2.1状态机结构体变量定义
struct double_list_node
{
struct double_list_node *next;
struct double_list_node *prev;
};
typedef struct double_list_node double_list_t;
struct FSM_DATA
{
__IO int event_code; //事件
__IO int status; //状态
__IO int delay; //延时
__IO int prv_state; //上一个状态
__IO int next_state; //下一个状态
};
struct fsm_list_t
{
double_list_t list; //节点
struct FSM_DATA f_data;
};
2.2.2可变参数宏
//!<初始化状态机链表数组
#define FSM_LIST(...) struct fsm_list_t* fsm_list[]={__VA_ARGS__, NULL}
extern struct fsm_list_t* fsm_list[];
可变参数宏的实现形式和变参函数差不多,用...表示变参列表,变参列表由不确定的参数组成,各个参数之间用逗号隔开。可变参数宏使用C99标准新增加的一个__VA_ARGS__预定义标识符来表示前面的变参列表。预处理器在将宏展开时,会用变参列表替换掉宏定义中的所有__VA_ARGS__标识符。
2.2.3初始化
typedef int (*fsm_fun)(void *arg);
enum SYS_CHECK_ENUM
{
SYS_START = 0x00,
SYS_CHECK,
SYS_CHECK_DONE,
SYS_CHECK_ERR,
SYS_CHECK_DELAY = 0xFF
};
struct fsm_cmd_container
{
int event_code;
fsm_fun callback_fun;
struct double_list_node node;
};
//!<初始化状态机链表数组
FSM_LIST(&fsm_sys_check);
int sys_check_start(void *arg)
{
struct FSM_DATA *fsm_data = (struct FSM_DATA*)arg;
//·····
return NONE_ERR;
}
int sys_check(void *arg)
{
struct FSM_DATA *fsm_data = (struct FSM_DATA*)arg;
//·····
return NONE_ERR;
}
int sys_check_done(void *arg)
{
struct FSM_DATA *fsm_data = (struct FSM_DATA*)arg;
//·····
return NONE_ERR;
}
int sys_check_err(void *arg)
{
struct FSM_DATA *fsm_data = (struct FSM_DATA*)arg;
//·····
return NONE_ERR;
}
void fsm_cmd_insert(double_list_t *list,char event_code,fsm_fun callback)
{
struct fsm_cmd_container* _node_ptr = (struct fsm_cmd_container*)malloc(sizeof(struct fsm_cmd_container));
_node_ptr->event_code = event_code;
_node_ptr->callback_fun = callback;
double_list_insert_before(list,&_node_ptr->node);
}
void sys_check_fsm_init(void)
{
double_list_init(&fsm_sys_check.list);
fsm_cmd_insert(&fsm_sys_check.list,SYS_START,sys_check_start);
fsm_cmd_insert(&fsm_sys_check.list,SYS_CHECK,sys_check);
fsm_cmd_insert(&fsm_sys_check.list,SYS_CHECK_DONE,sys_check_done);
fsm_cmd_insert(&fsm_sys_check.list,SYS_CHECK_ERR,sys_check_err);
fsm_cmd_insert(&fsm_sys_check.list,SYS_CHECK_DELAY,fsm_delay);
}
这里sys_check_fsm_init函数是先初始化一个double_list_t *类型的根节点,然后在fsm_cmd_insert函数中动态申请了大小为struct fsm_cmd_container的内存用于将事件与执行函数和申请的新节点绑定,然后将新节点的node成员插在根节点的尾部。
2.3 状态机状态查询
int fsm_process(void)
{
struct fsm_cmd_container* _fsm_ptr;
uint32_t loop = 0;
int ret;
//!<遍历所有状态机
for ( loop = 0; fsm_list[loop] != NULL ; loop++ )
{
//!<查找状态机对应的状态
_fsm_ptr = fsm_callback_find(fsm_list[loop]);
if(_fsm_ptr != 0)
{
//!<执行对应的函数
ret = _fsm_ptr->callback_fun(&fsm_list[loop]->f_data);
}
else
{
ret = -1;
}
}
return ret;
}
fsm_process函数一直在遍历所有状态机,我这里只定义了一个状态机fsm_sys_check用于演示。当fsm_list[loop] != NULL时,就去调用fsm_callback_find函数查询状态机对应的状态。fsm_callback_find函数的返回值是地址,若_fsm_ptr不为0,则执行状态机的对应回调函数。
fsm_callback_find函数的内容如下,它是struct fsm_cmd_container *类型的,入口参数为struct fsm_list_t *类型。这里先定义了两个指针变量,struct double_list_node *类型的变量list_ptr,struct fsm_cmd_container *类型的变量fsm_node_ptr。接下来的for循环先是将list_ptr的根节点指向了传进来的状态机根节点的next,然后判断状态机是否为空或者是否完成遍历,若非空在执行完循环体里面的内容后,list_ptr指向原节点的next进行下一次循环。当遍历完成list_ptr == &f_list->list后退出循环。
struct fsm_cmd_container* fsm_callback_find(struct fsm_list_t *f_list)
{
struct double_list_node *list_ptr;
struct fsm_cmd_container *fsm_node_ptr;
for( list_ptr = f_list->list.next; list_ptr != &f_list->list; list_ptr = list_ptr->next)
{
fsm_node_ptr = cmd_entry( list_ptr, struct fsm_cmd_container, node );
if(fsm_node_ptr->event_code == f_list->f_data.event_code)
{
return fsm_node_ptr;
}
}
return 0;
}
2.3.1container_of宏的解释
fsm_callback_find函数中调用了cmd_entry( list_ptr, struct fsm_cmd_container, node )函数,cmd_entry是对container_of宏的二次定义。
#define container_of(ptr, type, member) \
((type *)((char *)(ptr) - (unsigned long)(&((type *)0)->member)))
#define cmd_entry(ptr, type, member) container_of(ptr, type, member)
| 代码片段 | 说明 |
|---|---|
(type *)0 | 将 0 转换为 type 类型的指针(结构体起始地址假设为 0) |
&((type *)0)->member | 计算成员 member 在结构体中的偏移量(单位为字节) |
(unsigned long) | 将偏移量转换为无符号长整型(确保指针运算正确) |
(char *)(ptr) | 将成员指针转换为 char* 类型(按字节计算偏移) |
(type *) | 将最终结果转换回结构体指针类型 |
举个例子:
struct my_struct {
int data; // 4字节
char flag; // 1字节
struct list_head { // 包含两个指针
struct list_head *next;
struct list_head *prev;
} list; // 在32位系统中占8字节(每个指针4字节)
};
----------------------------------------------------------------
my_struct结构体布局:
0x0000 | data (4字节)
0x0004 | flag (1字节)
0x0008 | list (占8字节)
【注意】:
list的地址是0x0008而不是0x0005是因为编译器自动插入 3字节的填充(0x0005~0x0007),使得 list 从 0x0008 开始,满足4字节对齐要求。
假设list地址为0x1234。type类型为struct my_struct,member为结构体的list成员,代入container_of宏中计算。
#define container_of(ptr, type, member) \
((type *)((char *)(ptr) - (unsigned long)(&((type *)0)->member)))
操作过程:
1.已知 list 的地址为 0x1234(即 ptr = 0x1234)。
2.计算 list 的偏移量:&((struct my_struct*)0)->list = 0x0008。
3.起始地址 = 0x1234 - 0x0008 = 0x122C。
4.最终得到 struct my_struct* 指针指向 0x122C。
现在,搞清楚了fsm_callback_find函数中的fsm_node_ptr = cmd_entry( list_ptr, struct fsm_cmd_container, node );这行代码。
它其实就是根据结构体node成员地址减去node在结构体中的偏移地址算出该结构体的基地址。外层的_fsm_ptr指针变量指向该结构体的基地址,执行其回调函数。
2.4 main函数
int main(){
sys_check_init();//链表初始化
while (1){
//!<状态机遍历
fsm_process();
}
}
2.5 状态机总览框架图

浙公网安备 33010602011771号