前言
上一节,说了模块的总体设计思想。但是还没介绍设计细节,让我们逐步深入说说实现细节。
本节以串口的消息处理为主题进行探讨
消息
循环的队列
首先消息得被接受,一般向串口的处理,都会分配一个循环队列。如果是裸机的话,在中断中不断向循环队列推数据,在主循环中,不断接收数据。
在esp32-idf环境下,串口已经由框架帮我生成了循环队列,所以我们这一步的实现可以省略。
循环队列的实现方式主要包括以下几个关键点:
- 初始化:循环队列通常使用一个固定大小的数组来存储元素,并初始化两个指针(front和rear),分别指向队列的头部和尾部。初始时,这两个指针都指向数组的起始位置。
- 入队操作:当有新元素要加入队列时,首先检查队列是否已满(即rear指针加一后是否等于front指针)。如果未满,则将新元素放在rear指针指向的位置,并将rear指针向前移动一位。如果队列已满,则不能进行入队操作。
- 出队操作:当需要从队列中移除一个元素时,首先检查队列是否为空(即front指针和rear指针是否相等)。如果队列不为空,则将front指针指向的元素移除,并将front指针向前移动一位。
- 判断队列状态:循环队列的一个重要特性是能够区分队列为空和队列为满的状态。由于循环使用空间,当front和rear指针相等时,既可能是队列为空,也可能是队列为满。为了区分这两种情况,通常的做法是在创建队列时多分配一个数组位置,即让rear指针在循环时比front指针多走一步来表示队列满。
- 空间利用:循环队列通过环形结构有效地利用了数组空间,避免了普通队列在删除元素后空间无法立即被重新利用的问题。这使得循环队列在处理大量数据时更加高效。
消息帧
为了确保数据能够正确无误地从发送端传输到接收端,我们一般将数据按照一定的格式组织起来。虽然不同的通信协议对于消息帧的定义和格式有所不同,但基本都包含了以下部分:
- 起始标志:标识消息帧的开始。
- 长度或类型:指示消息帧的长度或者类型,帮助接收端解析消息。
- 负载(Payload):实际传输的数据内容。
- 结束标志:标识消息帧的结束。
- 校验码:用于检测传输过程中是否发生错误,常见的有CRC校验
为了文章简练,就不提if else
的种逐个判断的思想了。用状态机的思想,去处理消息解析会清晰很多。
状态机
这里简要介绍一下状态机是什么:=。
状态机(state machine)有5个要素,分别是状态(state)、迁移(transition)、事件(event)、动作(action)、条件(guard)。
- 状态: 一个系统在某一时刻所存在的稳定的工作情况,系统在整个工作周期中可能有多个状态。例如一部电动机共有正转、反转、停转这 3 种状态。一个状态机需要在状态集合中选取一个状态作为初始状态。
- 迁移:系统从一个状态转移到另一个状态的过程称作迁移,迁移不是自动发生的,需要界对系统施加影响。停转的电动机自己不会转起来,让它转起来必须上电。
- 事件:某一时刻发生的对系统有意义的事情,状态机之所以发生状态迁移,就是因为出现了事件。对电动机来讲,加正电压、加负电压、断电就是事件。
- 动作:在状态机的迁移过程中,状态机会做出一些其它的行为,这些行为就是动作,动作是状态机对事件的响应。给停转的电动机加正电压,电动机由停转状态迁移到正转状态,同时会启动电机,这个启动过程可以看做是动作,也就是对上电事件的响应。
- 条件:状态机对事件并不是有求必应的,有了事件,状态机还要满足一定的条件才能发生状态迁移。还是以停转状态的电动机为例,虽然合闸上电了,但是如果供电线路有问题的话,电动机还是不能转起来。
状态与迁移
在我们消息处理中,每个消息的判断成功都可以视为一个状态,比如:
S1:起始标志匹配、S2:长度信息合理、S3:结束标志匹配、S4:匹配失败
那我们就可以设计如下代码
enum MSG_FSM_STATE
{
MSG_IDLE,
MSG_HEADER00_MATCH, //header 为uint32_t,4bytes, 第一个匹配
MSG_HEADER01_MATCH, //header 为uint32_t,4bytes, 第二个匹配
MSG_HEADER02_MATCH, //header 为uint32_t,4bytes, 第三个匹配
MSG_HEADER03_MATCH, //header 为uint32_t,4bytes, 第四个匹配
MSG_LEN_OK,
MSG_CHECKSUM_OK,
MSG_TAIL00_MATCH, //tail 为uint32_t,4bytes, 第一个匹配
MSG_TAIL01_MATCH, //tail 为uint32_t,4bytes, 第一个匹配
MSG_TAIL02_MATCH, //tail 为uint32_t,4bytes, 第一个匹配
MSG_TAIL03_MATCH, //tail 为uint32_t,4bytes, 第一个匹配
};
状态的迁移基本都是线性的,只是再任意一个状态匹配出错后进 MSG_IDLE
状态
事件与动作
事件很单一,每次收到数据就是一个事件。在消息处理这种简单场景下,事件可以是一个。
这里我们可以使用:列表+函数指针的方式进行罗列和统一处理。这样我们的接口很统一,后续添加命令也比较方便。
首先,罗列以下事件、前状态、动作、后状态。代码只是对思想的呈现,这个地方梳理清除后,代码怎么写都可以。
由于消息处理这部分很简单,没有复杂的状态跳转,基本都是状态线性递增,出错后复位到0状态。所以伪代码如下:
rtv = 事件动作
if(ok)
state++
else
state = 0
TIPS:由于处理流程上只有事件动作不同,因此这里我们可以用#define
统一流程,将事件动作作为参数传递近来,如:
#define MSG_ACT(state,f) {\
int rtv = 0;\
rtv = f();\
if(rtv)\
state++;\
else
state = 0;\
state\
state是状态,f是函数指针,如果f是带参数的可以对应的修改。
条件
由于消息处理条件简单,就没有前置条件的判断了。
消息处理只是状态机的小应用之一,状态机的力量远远不只于此,后面的代码中,我们会经常看到它的身影。
消息处理
当我们完成了对消息的解析,就需要跳转到对于动作函数中,去处理消息的参数arg和给出消息的返回值rsp。
同样为了简练不介绍简单的if else
或者 switch case
,依然是我们的老朋友:列表+函数指针。
小结
至此,我们完成了对消息处理部分的详细设计,这部分也是代码的核心部分。