技术要点表驱动设计网上关于表驱动的介绍有很多并且实现起来也是各式各样但是总体而言使用这种方法的核心还是抽象与辨识这要求开发者必须对要实现的应用有一个全面的了解和认知这样才能从无序中识别出有序的主体部分而这部分必然成为程序的框架部分是静态的固定的而其余的依托框架存在的零散部分属于易变的可替换的。要做到以上辨识和分离的操作还真的需要不少功底。其实只要涉及到某种范式的使用都或多或少的有一些瓶颈我现在也正在学习这种方式它的难点在于怎么构造这些表表的结构该怎么设计可以认为这个表就是一个结构化的解空间所谓解空间就是对表进行输入的所有响应。所以一个好的表必然有一个可以覆盖可能输入情况的所有响应当然除了解空间的确定外求解的方法或者说把输入变换到解空间的方法也是不可缺少的这些都极大考验开发者的能力和水平。相比于我之前写代码的思路那都是直截了当的平心而论从任一个局部来看这种代码完全是心中所思所想的一一映射也就是完全把我们的思考过程转化为代码这样的代码应该是很符合逻辑的不是吗。但是从局部上升的系统的整体就会发现局部的片面和松散这样的代码的每个部分都会符合原本的预期但也仅此而已一把钥匙只能配一把锁但唯有铁丝才不挑锁。表驱动与状态机结合上面只介绍了表驱动的概念现在介绍一种应用场景那就是实现状态机。我们都知道在程序中的状态机就是通过定义一组有限状态和其转移的规则来控制系统行为的模型一般有4个组成部分状态、事件、转换和动作那么要想基于表驱动实现状态机就需要把这4个部分添加到表结构中如下所示typedef struct{ State CurState; Event CurEvent; void (*Action)(void); State NextState; }TransitionItem_t;这样基本的结构就是表中每一项的内容而一个表中有多少项就看开发者自己怎么理解问题和解析问题的了。举个例子现在有一个按键和一个LED灯要实现单击时LED灯常亮在常亮过程中长按时LED闪烁在闪烁过程中单击LED常灭。在遇到这种应用时不要直接开始写代码无论多简单先分析一下画个状态图然后根据状态图定义状态表上面这个状态表就可以完全设计到我们的代码中如下所示TransitionItem_t FsmTable[] { {LED_OFFKEY_CLICK,LightUp,LED_ON}, {LED_ONKEY_LONG,Toggle,LED_TOGGLE}, {LED_TOGGLEKEY_CLICK,LightOff,LED_OFF} }那么现在表结构有了或者说解空间有了怎么求解呢最简单的就是查表对比两个元素当前状态以及发生的事件在这个例子中就是(LED_OFF,KEY_CLICK)、(LED_ON,KEY_LONG)以及(LED_TOGGLEKEY_CLICK)有没有点像多元离散函数可以结合着加深对比理解。只要符合这三个定义好的组合之一那么必然发生动作如果不属于三个之一那么什么反应都没有也不影响现有状态驱动代码如下所示void RunFsm(TransitionItem_t *fsm,int fsm_size){ /* 判断是否为空指针 */ if(fsm){ for(int i 0;ifsm_size;i){ /* 条件对比 */ if(fsm[i].CurState GlobalState fsm[i].CurEvent GlobalEvent){ /* 执行动作 */ fsm[i].Action(); /* 执行状态转移 */ GlobalState fsm[i].NextState; } } } }Bootloader简介Bootloader是嵌入式系统中一段特殊的引导程序用于固件加载或更新等功能在芯片发生复位时首先会进入Bootloader进行引导决定是否更新或跳转应用程序。在这里使用的是小容量的单片机所以Bootloader程序尽量精简这样可以给应用程序让出更多的空间一般简单的应用下我们会进行分区分为Bootloader区和App区如下图所示可以看到大致上最简单的分区方案就是这样其中app有效标志放在了Bootloader区末尾这个标志就是Bootloader进行引导的条件。对于固件升级来说实际上是通过Bootloader对App区域进行擦写动作数据来源于外部内容就是App工程编译出来的bin文件如果在没有其他额外Flash的支持下进行升级的风险还是蛮大的所以需要有一定的安全校验措施。总的来说要实现一个基本的Bootloader流程还是比较简单的大致如下图所示可以看到上图的流程中显示了两种复位情况即上电复位和软件复位其中软件复位是从应用程序中进行的复位是专门为固件升级设定的表示存在更新请求当然可以附加更多的信息同时软件复位有一个特点就是不会改变RAM的数据。不管哪种复位只要存在更新请求就会第一时间把App有效标志位清除这样如果在更新过程中发生任何问题标志位都是无效的避免发生错误跳转的问题只有完成全部的更新流程包括校验等操作后才会标记有效。同时如果是正常的上电复位或者更新失败后都会停留在Bootloader中等待超时判断有效标志决定是否跳转。综合实践现在经过上面的介绍已经大致对涉及的技术要求有了一定的了解那么接下来就是对插件式的Bootloader进行设计了。有一个很核心的概念就是策略与机制分离对应到我们的设计中机制就是Bootloader功能策略就是实现功能的方式。乍一看策略还比较好理解机制要怎么理解呢可以认为是一种容器、框架或模型甚至就是一套固化规则策略千千万但都必须映射到机制的规则域中才能在机制的世界中存在。当然每个人有每个人的理解不同的理解也产生不同的代码现在介绍一下我对Bootloader机制的理解先上图大致描述一下从上图中可以看到有4个主要的元素也可以看作节点包括通信流程、协议交互流程、Boot流程以及Flash驱动可以认为要实现一个最基础的Bootloader必须要有这4个元素而这些也是机制所固有的成分所有策略都是作用在这4个元素之上的。其中为什么Flash与其他3个不一样是因为在实现中Flash读写都是一次性的并且由Boot流程直接控制而流程的控制就可以使用状态机来实现本质上来说状态机是一个独立的机制而现在通过表驱动方法增强了这一机制的通用性和灵活性只需要更换状态表就可以实现策略的改变。那么现在局部元素都介绍完了该怎么在各元素之间建立关联呢可以有消息、订阅发布等方式但是这些都大材小用了通过梳理发现完全可以使用链式通知的方式来建立关系。在上图中可以看到流向中标识的1.1、1.2等字样用前后级来描述的话就是由前级主动通知后级该做什么否则后级什么都不做当然这个前后级可以不是固定的前后级是相对的前后级可以想象一下环形链表以上就是我所设计的机制内容它的约束或者说应用条件可以归纳以下几点单向流控链式触发机制结构固定需识别或转化策略以满足结构要求下面给出实现机制的主要代码状态机的结构/* 状态表结构定义 */ typedef struct { /* 状态表事件项 */ uint32_t Event; /* 状态表状态项 */ uint32_t State; /* 状态表动作项 */ int (*Action)(uint32_t *event, void *arg); /* 状态表转移项 */ uint32_t NextState; } TransitionItem_t; typedef struct /* 基类状态机定义 */ { /* 状态表 */ TransitionItem_t *FsmTable; /* 当前状态 */ uint32_t CurState; /* 当前发生事件 */ uint32_t CurEvent; /* 状态表尺寸 */ uint32_t FsmSize; } BaseFsm_t;定义了状态表的结构以及基类状态机的结构基类状态机提供给4个元素使用其次是运行状态机的驱动代码int RunFsm(BaseFsm_t *fsm, void *arg) { /* 防止空指针错误 */ if (fsm-FsmTable ! NULL) { for (uint32_t i 0; i fsm-FsmSize; i) { /* (状态,事件)元组对比 */ if (fsm-CurState fsm-FsmTable[i].State fsm-CurEvent fsm-FsmTable[i].Event) { /* 防止空指针错误 */ if (fsm-FsmTable[i].Action ! NULL) { /* 执行动作 */ fsm-FsmTable[i].Action(fsm-CurEvent, arg); } /* 状态转移 */ fsm-CurState fsm-FsmTable[i].NextState; break; } } } else { return -1; } return 0; }接下来是通信流程的结构设计/* 通信流程的结构设计 */ typedef struct { struct { /* 流程控制状态表 */ BaseFsm_t *Fsm; /* 驱动器由外部提供 主要是每种通信驱动方式差异比较大做不到通用 */ void *Driver; /* 用于通知流程路径中下一节点适用于单一路径 */ void *Linkto; } PrivateArea; /* 初始化 传入状态机和驱动器 都由用户根据框架自定义 */ void (*Init)(BaseFsm_t *fsm, void *driver, void *link); /* 获取状态机 */ BaseFsm_t *(*GetFsm)(void); /* 获取通信驱动器 */ void *(*GetDriver)(void); /* 获取下一节点 */ void *(*GetLink)(void); } DataStreamHandle_t;通信节点可以认为是整个机制的触发节点因为所有的事件流都可以由通信来引导包括无通信造成的超时事件流继承了上面的基类状态表扩展了通信必要的驱动器还有一些方法定义。其次是交互协议流程的结构定义typedef struct { struct { /* 流程控制状态表 */ BaseFsm_t *Fsm; /* 用于通知流程路径中下一节点适用于单一路径 */ void *Linkto; /* 协议体 - 封装与解析协议中的数据或协议特征 */ void *ProtoBody; } PrivateArea; /* 初始化 传入状态机和协议体 都由用户根据框架自定义 */ void (*Init)(BaseFsm_t *fsm, void *link, void *data_body); /* 获取状态机 */ BaseFsm_t *(*GetFsm)(void); /* 获取协议体 */ void *(*GetProtoBody)(void); /* 获取下一节点 */ void *(*GetLink)(void); } ProtocalHandle_t;同样的继承了基类的状态表同时扩展了一个协议体属性这个属性的结构是自定义的体现协议的通式用于封装和解析数据使用。最后就是Boot流程的结构定义typedef struct { struct { /* 流程控制状态表 */ BaseFsm_t *Fsm; /* flash驱动器 */ void *FlashDriver; /* 用于通知流程路径中下一节点适用于单一路径 */ void *Linkto; } PrivateArea; void (*Init)(BaseFsm_t *fsm, void *link, void *driver); BaseFsm_t *(*GetFsm)(void); void *(*GetFlashDriver)(void); void *(*GetLink)(void); } BootloaderHandle_t;基本和上面的结构大同小异至此整个机制的结构就定义完成了接下来就是框架的搭建了。首先一般的通信接收功能都会在中断中进行因为这样实时性最高能及时的处理数据信息所以我们的通信流程的触发可以结合接收中断来进行以CAN接收中断举例void HAL_CAN_RxFifo0MsgPendingCallback(CAN_HandleTypeDef *hcan) { uint8_t i 0; if (hcan-Instance CAN1) { HAL_CAN_GetRxMessage(hcan, CAN_RX_FIFO0, CANxRxHeader, CANRecvBuf); if (CANxRxHeader.DLC 0) { /* 作为触发条件 */ CanMsgRecved 1; } } }这样通过状态机就可以控制通信的流程同时传入通信用的驱动器因为我们设计的时候都是采用void *这种抽象指针所以灵活程度很高。然后就是协议交互流程和Boot流程的使用这些都可以放到以某一时基为周期运行的代码块中while (1) { /* 1ms时基 */ if (TIMEBASE_HOOK(TimeBaseScope, OPT_TIMEBASE_1MS)) { TIMEBASE_DROP(TimeBaseScope, OPT_TIMEBASE_1MS); /* 单路径流程总是通过前级主动通知后级该做什么(自定义事件)同时后级继承(传入)前级的遗产(输出内容) */ /* 放入状态机在直接接收数据的地方 */ RunFsm(DataStreamHandle.GetFsm(), DataStreamHandle.GetDriver()); /* 协议状态机传入数据收发接口的原生数据形式 比如can报文形式串口形式 */ RunFsm(ProtocolHandle.GetFsm(), DataStreamHandle.GetDriver()); /* boot流程状态机 */ RunFsm(BootloaderHandle.GetFsm(), ProtocolHandle.GetDataBody()); } }因为通过链式传递消息通知在单路径流程中总是可以通过前级主动通知后级该做什么(自定义事件)同时后级继承(传入)前级的遗产(输出内容)。以上整个机制的框架的完成了接下来主要就是策略的定义了其实就是识别出自定义策略中的事件、状态以及转移并把这些以状态表的方式呈现出来。首先给出通信状态图通信流程中定义的事件和状态宏定义以及状态表定义/* 通信事件流自定义宏 */ /* 无事件 */ #define E_DATA_NONE 0 /* 有数据进入 */ #define E_DATA_IN (CAST_U32(0X01) 0) /* 数据超时 */ #define E_DATA_TIMEOUT (CAST_U32(0X01) 1) /* 数据接收结束 */ #define E_DATA_END (CAST_U32(0X01) 2) /* 数据请求协议处理 因为只有自己知道自己什么情况当然得主动通知 */ #define E_DATA_LINK_PTO (CAST_U32(0X01) 3) /* 等待协议给过来消息 */ #define E_DATA_RET_PTO (CAST_U32(0X01) 4) /* 自定义状态宏 */ /* 空状态 */ #define S_DATA_NONE 0 /* 接收状态 */ #define S_DATA_READING (CAST_U32(0X01) 0) /* 数据接收完成状态 */ #define S_DATA_READ_END (CAST_U32(0X01) 1) /* 等待协议反馈状态 */ #define S_DATA_WAIT_PTO (CAST_U32(0X01) 2) /* 通信事件流自定义宏 */ TransitionItem_t DataStreamTable[] { {E_DATA_IN, S_DATA_NONE, BufData, S_DATA_READING}, {E_DATA_TIMEOUT, S_DATA_READING, ResetData, S_DATA_NONE}, {E_DATA_END, S_DATA_READING, LinkProto, S_DATA_READ_END}, {E_DATA_LINK_PTO, S_DATA_READ_END, NULL, S_DATA_WAIT_PTO}, {E_DATA_RET_PTO, S_DATA_WAIT_PTO, SendResp, S_DATA_NONE} };其次是交互协议状态图对应的事件、状态以及状态表定义如下/* 协议收到通信的通知 */ #define E_PROTO_RECV_DATA (CAST_U32(0X01) 0) /* 协议解析完成 */ #define E_PROTO_PARSE_OK (CAST_U32(0X01) 1) /* Boot反馈 */ #define E_PROTO_RET_BOOT (CAST_U32(0X01) 2) /* 空状态 */ #define S_PROTO_NONE 0 /* 解析状态 */ #define S_PROTO_PARSE (CAST_U32(0X01) 1) /* 等待Boot反馈状态 */ #define S_PROTO_WAIT_BOOT (CAST_U32(0X01) 2) /* 状态表定义 */ TransitionItem_t ProtoTable[] { {E_PROTO_RECV_DATA, S_PROTO_NONE, ParseData, S_PROTO_PARSE}, {E_PROTO_PARSE_OK, S_PROTO_PARSE, LinkBoot, S_PROTO_WAIT_BOOT}, {E_PROTO_RET_BOOT, S_PROTO_WAIT_BOOT, RetData, S_PROTO_NONE} };最后是Boot流程的状态图对应的事件、状态以及状态表定义如下/* Boot收到协议的通知 */ #define E_BOOT_RECV_PROTO (CAST_U32(0X01) 0) /* Boot起始命令 */ #define E_BOOT_CMD_START (CAST_U32(0X01) 1) /* Boot更新命令 */ #define E_BOOT_CMD_UPDATE (CAST_U32(0X01) 2) /* Boot结束命令 */ #define E_BOOT_CMD_END (CAST_U32(0X01) 3) /* Boot跳转命令 */ #define E_BOOT_CMD_JUMP (CAST_U32(0X01) 4) #define S_BOOT_NONE 0 /* Boot准备状态 */ #define S_BOOT_READY (CAST_U32(0X01) 1) /* Boot更新状态 */ #define S_BOOT_UPDATE (CAST_U32(0X01) 2) /* Boot结束状态 */ #define S_BOOT_END (CAST_U32(0X01) 3) /* 状态表定义 */ TransitionItem_t BootTable[] { {E_BOOT_RECV_PROTO | E_BOOT_CMD_START, S_BOOT_NONE ResetBoot, S_BOOT_READY}, {E_BOOT_RECV_PROTO | E_BOOT_CMD_UPDATE, S_BOOT_READY, Update, S_BOOT_UPDATE}, {E_BOOT_RECV_PROTO | E_BOOT_CMD_UPDATE, S_BOOT_UPDATE, Update, S_BOOT_UPDATE}, {E_BOOT_RECV_PROTO | E_BOOT_CMD_END, S_BOOT_UPDATE, Check, S_BOOT_END}, {E_BOOT_RECV_PROTO | E_BOOT_CMD_JUMP, S_BOOT_END, JumpApp, S_BOOT_NONE} };可以看到状态表中的事件项都是多种事件的组合因为我们的事件宏定义是通过移位操作进行的所以提供了组合事件的能力。至此Bootloader的机制和策略都制定完毕了这里只演示了比较简单的策略功能但因为机制提供了策略更换的能力所以实现更复杂的功能只需要制定相应的策略状态表即可。