SXSBJSXYT

保持热爱,奔赴山海

 

嵌入式开发之状态机思维

"在嵌入式系统中,状态机不是可选设计模式,而是控制复杂性的生存必需品。" ——《嵌入式系统设计模式》

(一)何为状态机?

        状态机(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 双向链表的初始化

 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.2 在链表头部插入一个节点

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.3 在链表表头前插入一个节点

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.4 从链表删除一个节点

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 状态机总览框架图 

posted on 2025-03-30 22:15  SXSBJSXYT  阅读(10)  评论(0)    收藏  举报  来源

导航