这篇介绍 搞嵌入式萌新 听别人吹牛的计算机概念——状态机
一、什么是状态机?
可能有些编写嵌入式程序的人,听过别人吹他已经用状态机方法写好了代码。听起来逼格贼高,但是实际上,即使你没了解过状态机,但我们在编写嵌入式程序,尤其是驱动编写,都会有意无意以(类似)状态机思想来编写程序,只不过写得时候不知道这个叫状态机。
二、状态机
状态机,也就是 State Machine ,不是指一台实际机器,而是指一个数学模型,一种思想。重复一下:状态机是有限状态自动机的简称,是现实事物运行规则抽象而成的一个数学模型。
2.1 状态机的四大概念
- State ,状态
- 一个状态机至少要包含两个状态。例如bool类型,有 true 和 false 两个状态。
- Event ,事件
- 事件就是执行某个操作的触发条件或者口令。不同状态对应产生各自事件。
- Action ,动作
- 事件发生以后要执行动作。例如事件是“按开门按钮”,动作是“开门”。
- Transition ,变换
- 通过多种动作满足一定条件,开始切换状态
主要概念还是状态,后三者往往在代码实现的时候糅合在一起比较模糊。
2.2 举例应用
状态机,是(快速)写(驱动)程序的好帮手。
街上的自动售货机中明显能看到状态机逻辑;我们做一下简化,假设这是一台只卖2元一瓶的汽水的售货机,只接受五毛和一块的硬币。
- 初始状态是”未付款“,中间状态有”已付款5毛“,”已付款1块“,”已付款1.5块“,”已足额付款“,四个状态。
- 状态切换的触发条件是”投一块硬币“和”投5毛硬币“两种。
- 到达“已足额付款”状态,还要进行余额清零和弹出汽水操作。然后重新进入初始化状态
所以如果画出一张完整的状态转换图,也会是比较复杂的一张图了。而实际中的售货机对应的状态机就会更加复杂了。
2.3 实际代码实现
实际从C的代码层面,更容易看到状态机的影子。最经典的就是 switch
和 enum
的搭配了。
switch
罗列出状态机的所有可能状态。enum
产生对应的各种状态。代码如下:
1 | typedef enum |
这样就是很常见、很普通的状态机写法,很适用于快速开发底层驱动。当然,这是一个很简单的例子。还有更复杂的状态机,甚至多状态机相互影响切换其他状态机的状态。状态机也只是一种思想而已。
可以去看FreeModbus的通讯库。它就是一个很经典的状态机写法,但是涉及到多状态机。它的状态机有:(轮询)事件,串口发送中断,串口接收中断,其中还有个定时器能够变换 串口接收中断 状态机。
2.4 FreeModbus库讲解(RTU模式为例)
- Modbus通讯驱动初始化后,(使能函数)将 串口接收中断 置为 初始化状态
- 串口接收中断 在初始化状态下,打开定时器
- 等待定时器溢出后,触发定时器中断,关闭定时器,定时器中断处理 串口接收中断状态机,发现是 初始化状态 。此时再将 串口接收中断 置为 空闲状态,同时将 将
eMBPoll
的 事件状态机 置为 初始化。 - 以上就是初始化完成,接下来就是数据收发的流程了
- 串口接收中断触发,空闲状态下打开定时器,且获取一个接收字节。此时 Modbus串口接收中断状态机 置为 接收状态。
- 每次串口接收中断触发,刷新定时器时间,防止定时器溢出。接收状态下,接收字节如果不溢出,则继续接收(溢出报错)。
- 当串口接收中断延迟一段时间(或者不再接收到数据),该时间让定时器溢出。此时定时器中断,并将 Modbus串口接收中断 置为 空闲状态
- 由于定时器溢出,视为接收一帧完整的数据帧。将 Poll的 事件状态机 置为 接收完成。
- eMBPoll的 事件状态机 为
Frame received
接收完成,开始核对数据。数据长度>4(ID+功能码+校验) && CRC校验成功 - 核对成功后,(用指针方式获取数据PDU,数据长度= 总长度-地址域(ID 1字节)-CRC(检验 2字节)。核对失败,则报
MB_EIO
的错误。 - 经过核对,校验成功后,如果ID正确 或 为0。则将eMBPoll的 事件状态机 置为
Execute function
执行数据函数(事件)。开始对数据进行处理。 - 这里对功能码的选择处理,是采用一个结构体数组,每个结构体成员内含 1个功能码+功能码对应要执行的(回调)函数。然后for循环,匹配出对应的功能码,并进行处理;如果功能码为0则直接跳出。
- 如果ID号不是广播地址0,则从机会进行响应。之前进行对应功能码处理函数 得出结果,如果报错,则后续的响应
功能码|0x80 + 错误代码
。 - 举例,返回 01 83 02 C0 F1。即 读错误(0x03&0x80),非法数据地址(0x02),后面两个为CRC校验。
- 在处理完后会返回一个enum状态值。如果状态 不为
MB_ENOERR
,即内部使用的错误代码,根据内部使用的enum
错误代码,switch
生成 对外的错误代码 - 返回的CRC会在发送前先把CRC校验完成。
- 发送前,检查能不能数据接收,能接收则报硬件错误
MB_EIO
,因为协议规定只能单向收发 - 发送一个字节数据,然后开启发送中断,一直发送,直到完成
- 发送完成后,将 eMBPoll的 事件状态机 置为 发送完成
- 事件状态机 为 发送完成状态,该状态触发 将 发送中断状态机 置为 发送空闲
以上就是FreeModbus库实现Modbus RTU
通讯的方式。其他模式就大同小异,里面很多具体的实现,采用了指针的方式,尤其是函数指针,在初始化函数内可见一斑。写的很好,这份源码还是很推荐看的。