状态机

  这篇介绍 搞嵌入式萌新 听别人吹牛的计算机概念——状态机

一、什么是状态机?

  可能有些编写嵌入式程序的人,听过别人吹他已经用状态机方法写好了代码。听起来逼格贼高,但是实际上,即使你没了解过状态机,但我们在编写嵌入式程序,尤其是驱动编写,都会有意无意以(类似)状态机思想来编写程序,只不过写得时候不知道这个叫状态机。

二、状态机

  状态机,也就是 State Machine ,不是指一台实际机器,而是指一个数学模型,一种思想。重复一下:状态机是有限状态自动机的简称,是现实事物运行规则抽象而成的一个数学模型。

2.1 状态机的四大概念

  • State ,状态
    • 一个状态机至少要包含两个状态。例如bool类型,有 true 和 false 两个状态。
  • Event ,事件
    • 事件就是执行某个操作的触发条件或者口令。不同状态对应产生各自事件。
  • Action ,动作
    • 事件发生以后要执行动作。例如事件是“按开门按钮”,动作是“开门”。
  • Transition ,变换
    • 通过多种动作满足一定条件,开始切换状态

  主要概念还是状态,后三者往往在代码实现的时候糅合在一起比较模糊。

2.2 举例应用

  状态机,是(快速)写(驱动)程序的好帮手。

街上的自动售货机中明显能看到状态机逻辑;我们做一下简化,假设这是一台只卖2元一瓶的汽水的售货机,只接受五毛和一块的硬币。

  • 初始状态是”未付款“,中间状态有”已付款5毛“,”已付款1块“,”已付款1.5块“,”已足额付款“,四个状态。
  • 状态切换的触发条件是”投一块硬币“和”投5毛硬币“两种。
  • 到达“已足额付款”状态,还要进行余额清零和弹出汽水操作。然后重新进入初始化状态

所以如果画出一张完整的状态转换图,也会是比较复杂的一张图了。而实际中的售货机对应的状态机就会更加复杂了。

2.3 实际代码实现

  实际从C的代码层面,更容易看到状态机的影子。最经典的就是 switchenum 的搭配了。

switch 罗列出状态机的所有可能状态。enum 产生对应的各种状态。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
typedef enum
{
DISCONNECT,
CONNECT,
RECONNECT,
}_enumGPRSState;

void Func(_enumGPRSState GPRSState)
{
switch(GPRSState)
{
case DISCONNECT:break; //执行对应的事件、动作
case CONNECT:break; //执行对应的事件、动作
case RECONNECT:break; //执行对应的事件、动作
default:break;//特殊处理
}
}

这样就是很常见、很普通的状态机写法,很适用于快速开发底层驱动。当然,这是一个很简单的例子。还有更复杂的状态机,甚至多状态机相互影响切换其他状态机的状态。状态机也只是一种思想而已。

  可以去看FreeModbus的通讯库。它就是一个很经典的状态机写法,但是涉及到多状态机。它的状态机有:(轮询)事件,串口发送中断,串口接收中断,其中还有个定时器能够变换 串口接收中断 状态机。

2.4 FreeModbus库讲解(RTU模式为例)

  1. Modbus通讯驱动初始化后,(使能函数)将 串口接收中断 置为 初始化状态
  2. 串口接收中断 在初始化状态下,打开定时器
  3. 等待定时器溢出后,触发定时器中断,关闭定时器,定时器中断处理 串口接收中断状态机,发现是 初始化状态 。此时再将 串口接收中断 置为 空闲状态,同时将 将eMBPoll的 事件状态机 置为 初始化。
  4. 以上就是初始化完成,接下来就是数据收发的流程了
  5. 串口接收中断触发,空闲状态下打开定时器,且获取一个接收字节。此时 Modbus串口接收中断状态机 置为 接收状态。
  6. 每次串口接收中断触发,刷新定时器时间,防止定时器溢出。接收状态下,接收字节如果不溢出,则继续接收(溢出报错)。
  7. 当串口接收中断延迟一段时间(或者不再接收到数据),该时间让定时器溢出。此时定时器中断,并将 Modbus串口接收中断 置为 空闲状态
  8. 由于定时器溢出,视为接收一帧完整的数据帧。将 Poll的 事件状态机 置为 接收完成。
  9. eMBPoll的 事件状态机 为Frame received接收完成,开始核对数据。数据长度>4(ID+功能码+校验) && CRC校验成功
  10. 核对成功后,(用指针方式获取数据PDU,数据长度= 总长度-地址域(ID 1字节)-CRC(检验 2字节)。核对失败,则报 MB_EIO的错误。
  11. 经过核对,校验成功后,如果ID正确 或 为0。则将eMBPoll的 事件状态机 置为Execute function 执行数据函数(事件)。开始对数据进行处理。
  12. 这里对功能码的选择处理,是采用一个结构体数组,每个结构体成员内含 1个功能码+功能码对应要执行的(回调)函数。然后for循环,匹配出对应的功能码,并进行处理;如果功能码为0则直接跳出。
  13. 如果ID号不是广播地址0,则从机会进行响应。之前进行对应功能码处理函数 得出结果,如果报错,则后续的响应 功能码|0x80 + 错误代码
  14. 举例,返回 01 83 02 C0 F1。即 读错误(0x03&0x80),非法数据地址(0x02),后面两个为CRC校验。
  15. 在处理完后会返回一个enum状态值。如果状态 不为MB_ENOERR,即内部使用的错误代码,根据内部使用的enum错误代码,switch生成 对外的错误代码
  16. 返回的CRC会在发送前先把CRC校验完成。
  17. 发送前,检查能不能数据接收,能接收则报硬件错误MB_EIO,因为协议规定只能单向收发
  18. 发送一个字节数据,然后开启发送中断,一直发送,直到完成
  19. 发送完成后,将 eMBPoll的 事件状态机 置为 发送完成
  20. 事件状态机 为 发送完成状态,该状态触发 将 发送中断状态机 置为 发送空闲

  以上就是FreeModbus库实现Modbus RTU通讯的方式。其他模式就大同小异,里面很多具体的实现,采用了指针的方式,尤其是函数指针,在初始化函数内可见一斑。写的很好,这份源码还是很推荐看的。

-------------本文结束感谢您的阅读-------------