嵌入式软件设计之美-以实际项目应用MVC框架与状态模式(上)

2022-11-28 14:13:50 浏览数 (1)

来源 | 嵌入式应用研究院 整理&排版 | 嵌入式应用研究院

笔者在职场工作多年,维护过屎山级别的项目代码,也参与过大大小小的软件开发。我逐渐明白了写代码最重要的并不是炫技,而是让其他维护这个项目的人能够更快的上手去拓展项目的功能,以便能够更好的传承下去。

在实际的嵌入式应用开发过程中,我们常常能够听到软件系统的分层设计,根据不同的产品软件设计,它们可能拥有应用层、系统层、驱动层等等。在我看来,这些层的描述太大,但是它不得不存在;因为它能够从宏观上让每个了解它的人知道它到底有什么东西,大部分情况下给人感觉是:哇,好牛逼! 然后,就没有然后了。

实际上,真正能够体现设计的牛逼并不是简单的描述几个层就可以了,真正的设计,它至少拥有两个面的维度,即数据平面、控制平面。当然,如果我们分得更细一点,它可以是数据平面、控制平面和管理平面。至于怎么定义每个平面的作用,早在1979年,就已经有一位叫做Trygve Reenskaug的大佬为我们设计好了所谓的MVC框架。直到如今,MVC框架广泛应用于现代应用软件设计中,也是嵌入式应用软件设计中最常用的设计模式之一。那么,什么是MVC框架呢?

1、MVC框架

MVC框架,是软件系统模块化设计的一种方法,它给软件系统划分为三个大的部分,分别是Model(模型)、View(视图)、Controller(控制器)

  • Model模型

模型就是负责具体功能、业务逻辑实现的,它通常是一个产品内部的一些业务逻辑组成;例如,接下来我们要做的一个项目里有一个MQ-2传感器,MQ-2传感器的气体检测流程可以认为是一个模型。

  • View视图

视图就是负责展示、响应其它模块处理结果的。例如,有一款设备拥有一个LCD屏幕,然后上面移植了一个GUI系统,它用于显示当前MQ-2传感器的数据,那么这个GUI系统就是一个视图。当MQ-2传感器检测到的阈值超出我们所设定的阈值时,蜂鸣器或者LED报警了,那么蜂鸣器、LED也可以认为是一个视图。当然,视图不局限于以上这些内容,视图也可以是IOT前端、也可以是一个Shell终端,甚至可以是一个进程或者线程。

  • Controller控制器

控制器就是用来接收用户输入的。通常,一个设备上可能有按键、触摸屏、鼠标等输入设备,那么当用户控制输入设备时,根据产品内部的业务逻辑,界面可能会发生跳转(视图),用户看不到的另一面会启动应用业务逻辑(模型),然后设备内部的业务逻辑处理完毕后,又会通知界面,例如弹窗或者仅仅是界面上控件数据更新(视图)。

有了MVC架构以后,我们可以为我们接下来的项目做以下软件模块的划分了,先预告一下,我们要做一个简单的气体检测装置,它会延用我们之前分享过的内容进一步规范化:

表驱动 状态机法AD传感器驱动检测框架

让传感器数据更直观之LCD曲线显示

基于小熊派气体传感器MQ-2综合实践

  • Model

传感器数据获取、传感器流程检测。

  • View

LCD GUI(或者腾讯云、腾讯连连小程序)显示,显示传感器数据、检测状态等

  • Controller

开发板上的按键、IOT终端下发命令(或腾讯连连小程序下发命令)

那么,这三个模块怎么来通信呢?一般情况下,可以有两种方式,一种是通过消息传递,另外一种是通过回调函数传递(类似事件回调触发机制)。很显然,我们的项目会考虑上RTOS,那么,通过消息队列来传输数据就再好不过了!这样的话,我们每个模块之间的通信一旦设计好了,那么未来需要做的事情仅仅只需要关心:

  • 消息发送端:数据怎么传,要传什么数据
  • 接收消息端:数据怎么接,接完之后要做什么

解决了发送、接收数据的问题以后,第二步就是要设计传感器的检测流程了,它也是我们MVC架构中模型部分最重要的内容了,它是整个项目的核心业务。对于传感器设备检测来说,无非就是几种分析状态:

  • 传感器设备处于空闲状态
  • 传感器设备处于校准状态
  • 传感器设备处于检测中状态
  • 传感器设备处于获取检测结果状态

从一种状态切换到另一种状态,那么一定是由某个事件触发的,进而产生一定的动作,然后完成状态的迁移,我们将这种称为状态机。在设计模式中,状态机称为状态模式,状态模式也是嵌入式软件应用设计中最常用的模式。

2、状态模式

状态模式是设计模式中行为型模式的一种,它允许对象在内部状态发生改变时改变它的行为。对于小白来说,状态模式的概念讲述是比较抽象的,那么,我可以举一个接下来要做的气体检测仪项目来加深大家对状态模式的理解,以下是这个项目的简单的状态迁移图:

在状态机的基本概念里,它的基本组成要素主要由现态、条件、动作、次态。我们能看到,检测仪主要有四个状态,分别是IDLE、CALI、DETECT、RESULT,每个状态其实都是由相应的条件来进行触发产生动作进而产生下一个状态的。从以上的状态迁移图里,我们能很清晰地将每个状态的迁移过程描述出来:

  • 1.对于IDLE来说,主要有:
代码语言:javascript复制
IDLE→START→CALI

解析:当检测仪状态为IDLE时,检测仪的操作者需要发起一个START事件来让检测仪的状态由IDLE进入到CALI状态中。这样的操作场景通常是检测仪的操作者按下一个开始检测的按钮,检测仪由空闲状态转为基准采集状态,此时检测仪可能会开启各类传感器,在此期间,各项传感器指标需要与当前环境进行融合,使各项传感器指标处于稳定状态,这样才有利于后续检测结果精确。

  • 2.对于CALI来说,主要有:
代码语言:javascript复制
CALI→NEXT→DETECT
CALI→STOP→IDLE

解析:当检测仪状态处于CALI时,如果此时条件满足,则需要发起一个NEXT事件让检测仪状态由CALI进入DETECT中。这个NEXT事件可以是CALI过程稳定后自动触发的,也可以是由用户手动触发的,这个需要根据产品需求进行定义。如果此时条件不满足则维持现态CALI,当检测仪的操作者发起STOP事件时,通常是按下了某个返回或者退出的按键,则此时检测的状态由CALI迁移到了IDLE态。

  • 3.对于DETECT来说,主要有:
代码语言:javascript复制
DETECT→NEXT→RESULT
DETECT→PREV→CALI
DETECT→STOP→IDLE

解析:当检测仪状态处于DETECT时,此时内部会进行一系列的动作,那么它得有结果呀,结果就是由NEXT事件来产生的,这样的应用场景通常是传感器各项技术指标整合的检测算法运算结果与检测仪所设定的阈值进行比较后自行触发的,最终将由DETECT状态迁移到了RESULT。当然,如果当前处于DETECT状态,而DETECT状态的过程存在偏差,此时检测仪操作者可以通过某个按键,触发PERV事件,将DETECT状态迁移到上个状态CALI;当然,检测仪操作者也可以直接选择停止检测,他只需要发起一个STOP事件(通常是一个返回按键)即可让检测仪的状态由DETECT迁移到IDLE状态。

  • 4.对于RESULT来说,主要有:
代码语言:javascript复制
RESULT→PREV→DETECT
RESULT→STOP→IDLE

解析:当检测仪状态处于RESULT时,一般情况下检测仪要么是通过声光报警,要么是直接在LCD上进行展示,要么是都有。检测仪的操作者可以继续检测,那么他只需要发起一个PREV事件就能让检测仪的状态由RESULT迁移到DETECT,设备又能够继续进行检测了,这就是所谓的快检模式。当然,检测仪操作者也可以发起一个STOP事件,让检测仪回到IDLE状态。

对以上状态迁移过程熟悉后,我们第二步要做的事情就是设计符合我们业务场景的枚举以及结构体。首先是传感器的状态,有IDLE、CALI、DETECT、RESULT,用枚举来体现就是:

代码语言:javascript复制
//传感器状态
enum SensorState_t
{
    IDLE = 0,
    CALI,
    DETECT,
    RESULT
};

每种状态是由特定的事件进行触发,那么传感器的事件主要有:

代码语言:javascript复制
//传感器事件
enum SensorEvent_t
{
    START = 0,
    STOP,
    NEXT,
};

有了状态和事件之后,我们需要使用一个结构体来抽象从现态->事件->次态这个过程,那么我们可以这么来设计:

代码语言:javascript复制
struct SensorStateItem_t
{
    enum SensorState_t CurState;
    enum SensorEvent_t Event;
    enum SensorState_t NextSate;
};

因此,我们能够根据我们所描述的状态规划出一张表,而这张表就清晰的描述出现态->事件->次态的这几个过程了:

代码语言:javascript复制
struct SensorStateItem_t SensorStateTable[] = 
{
    {IDLE,   START, CALI},      //IDLE->START->CALI
    {CALI,   NEXT,  DETECT},    //CALI->NEXT->DETECT
    {CALI,   STOP,  IDLE},      //CALI->STOP->IDLE
    {DETECT, NEXT,  RESULT},    //DETECT->NEXT->RESULT
    {DETECT, PREV,  CALI},      //DETECT->PREV->CALI
    {DETECT, STOP,  IDLE},      //DETECT->STOP->IDLE
    {RESULT, PREV,  DETECT},    //RESULT->PREV->DETECT
    {RESULT, STOP,  IDLE},      //RESULT->STOP->IDLE
};

有了业务模型以后,接下来就是编写代码逻辑了!预知详情,且看下回分享!今天的分享到此为止,如果对您有帮助,欢迎一键三连!

参考资料

[1]https://www.runoob.com/design-pattern/mvc-pattern.html

[2]https://refactoringguru.cn/design-patterns/state

0 人点赞