UML状态图的实用C/C++设计(QP状态机)-笔记一

本文介绍了UML状态图的表示方式,强调了状态嵌套的重要性和如何排除状态重复。同时,文章详细讨论了QP框架的事件处理机制,包括直接发送和发行-订阅两种方式,并解释了QF如何处理线程安全的事件交换。此外,文中还探讨了C语言中结构单继承实现事件的策略,以及在QP框架下如何设计和使用状态机。

QP官网网址:http://www.state-machine.com

本书下载地址:

笔记有点乱,没办法,书里也是断断续续的介绍。

 

UML:

UML状态图:状态用节点表示,转换用弧线连接在节点间。状态节点用圆角方框表示。状态名用粗体,写在状态框顶部的名字格里。在名字下面有一个可选的内部转换格,用一条水平线分开。内部转换格包含进入动作内部转换格包含进入动作(在保留符号entry后的动作)退出动作(在保留符号 exit 后的动作)和其他内部转换(比如在图中里被 TIME_TICK触发的那些动作)。状态转换用从源状态边界指向目的状态边界的箭头表示。最低要求,一个转换必须用触发事件来做标签。触发可以有可选的多个事件参数,一个监护条件和一个动作列表。

最终状态在 UML 符号里是牛眼 (bull’s eye) 符号,通常的用法是指示状态机对象的析构(destruction)。

                                      

                                                                      图1 状态图示例

        状态嵌套的威力在于它被设计于用来排除可能会发生的重复。高层的active 状态被称为超状态 (superstate) ,它的抽象定义是,状态机不能直接在这个状态里,只能在它里面嵌套的某一个状态里,这个状态被称为活动子状态。 UML 和状态嵌套有关的语法说明,任何事件先被当前正在活动的子状态 (substate) 处理,如果这个子状态不能处理这个事件,状态机尝试在较高一层超状态的上下文处理这个事件。当然, UML 里的状态嵌套不限于仅一层,而且,这个处理事件的简单规则可以递归的应用于任何多层的嵌套。

 

                         

                                                                 图2 QP状态机主框架

上图展示了QEP事件处理器,QF实时框架,QS,QK,Qvanilla。

QF简介:

QF框架两种事件派送机制:

1. 简单的直接发送。比较简单的机制,通过函数 QActive_postFIFO() 和 QAcive_postLIFO() 直接进行事件发布,一个事件的产生者直接把这个事件发布到作为这个事件消费者的主动对象的事件队列。

2. 灵活的发行 - 订阅事件传递机制,通过函数 QF_publish()和 QActive_subscribe() 支持。事件的产生者把事件发行给框架,框架然后把它传递给所有订阅了这些事件的主动对象。

 

QF处理全部的线程安全的事件交换和排队细节。

QF里的主动对象是封装后的状态机,QF事件队列只拥有指向对象的指针。

QF框架可以管理3个不同大小块的事件池(大中小)。小事件池必须在中等事件池前初始化。

 

在 QF 里,系统的任何部分,而不仅局限于主动对象,都能够产生事件。例如,中断服务程序(ISR) 或设备驱动程序也能产生事件。另一方面,只有主动对象能消费事件,因为只有主动对象才拥有事件队列。

 

QF 的事件管理的最重要的特征是,框架只传递指向事件的指针,而不是事件本身。 QF 从不复制事件(零复制策略)。

 

        时间事件由应用程序分配,但是由 QF 框架管理。 QF 提供函数去启动 (arming)一个时间事情,例如一次性超时的 QTimeEvt-postIn() ,和周期性时间事件的 QTimeEvt_postEvery() 。启动一个时间事件实际上通知 QF 框架。

 

在 QP 里,事件被表示为使用框架提供的 QEvent结构的实例。具体来说, QEvent结构包含成员 sig ,它表示事件的信号。通过继承过程来添加事件参数。如下文“ C 语言里的单一继承”所描叙。

 

继承是基于已有结构派生新的结构,从而重用和组织代码的一种能力。你可以非常简单的在 C 里实现单继承,只要字面上把基础结构作为派生结构的第一个成员即可。例如,图 3( a )显示了通过把QEvent实例作为 ScoreEvt 的第一个成员嵌入,从而从基础结构 QEvent派生出结构 ScoreEvt 。为了使这个用法更加突出,我总是把基本结构成员命名为 super。

                                

                                                         图3 

你总是可以安全的传递一个指向 ScoreEvt 的指针,给任何需要一个指向 QEvent的指针的函数。(严格来讲,在 C 里你应该显式的转换这个指针。在 OOP里,这种转换被称为向上类型转换 (upcasting) ,而且它总是安全的。)结果,所有为 QEvent结构设计的函数自动的适用于 ScoreEvt 结构和其他从 QEvent派生的结构。图 3(c) 的 UML 类图,说明了 ScoreEvt 结构和 QEvent结构的继承关系。

QP 非常广泛的使用单继承,不仅用来派生带有变量的事件,也用来派生状态机和主动对象。当然,QP 的 C++ 版本使用固有的 C++ 类派生而不是“结构的派生”。

在 C 和 C++ 里,一个函数指针 QHsm_top() 可以写成 QHsm_top 或 &QHsm_top 。尽管QHsm_top 表达更加简洁,我更喜欢使用表达式 &QHsm_top ,以表明这里我确实需要一个函数指针 &QHsm_top。

 

事件在大多数应用构件间是共享的。

应用程序的信号不是从 0 开始,而是有常数为 Q_USER_SIG 的偏移。这是因为 QP 保留了最低的几个信号给内部使用,并提供常数 Q_USER_SIG 作为偏移值,用户信号从它开始枚举。

全局指针代表在应用程序里的主动对象,并被用来直接发送事件给主动对象。因为这些指针可以在编译时间被初始化,我喜欢把它们声明为 const,这样它们能被放在 ROM 里。这些主动对象指针是“不透明的” (opaque) ,因为它们不能访问整个主动对象,只能访问从QActive 结构继承的那部分。

 

top 状态是 UML 的概念,表明在一个层次式状态机里的状态层次的终极根 (ultimate root)

当实现状态处理函数时你需要注意在这些函数 QEP 事件处理器而不是你的代码管理QEP将根据不同的原因调用某个状态处理函数:层次事件处理,执行入口和出口动作,触发初始转换,或仅仅引出某给定状态处理函数的超状态。因此,你不能假设某个状态处理函数仅仅用来处理在那些在 case 语句后的信号。你必须避免任何在 switch 语句之外的代码,特别是那些有副作用的代码。

 

传统的解决办法是完全针对手头的问题,而状态机方案是通用性的。

                                   

                                               图4

        Quickstart程序和“飞行和射击”实例相比有更多的循环式控制流程,因为传统的解决办法是完全针对手头的问题,而状态机方案是通用性的。 Quickstart应用程序的结构和传统的顺序式程序非常相似,它尽量从头到尾保持控制。它不时的停住,忙等待某个事件,代码基本上没有准备好去处理任何它在等待的事件以外的其他的事件。这些都是由于设计的不灵活性导致。增加新的事件是困难的,因为需要更改的代码的整个结构被设计为只接收很特定的事件,而且需要被剧烈的改变从而容纳新的事件。同样,当忙等待屏幕更新事件时(等效为“飞行和射击”游戏里的 TIME_TICK事件),程序不会对任何其他的事件产生反应。任务级响应难以被确定,通常它和事件的类型有关。通过硬编码确定的等待时序适合现有的事件但是也许不会适合新的事件。

        相反,“飞行和射击”游戏有更加简单的控制流程,纯粹的由事件驱动并且完全通用(见图1.11(b) )。每个主动对象的上下文表示为一个状态机的当前状态,而不是某处的代码。这样,根据当前上下文在代码某处进行“事件循环”变得没必要了。相反,状态机把上下文当做一个很小的数据成员的方法是很有效率的。在处理完每个事件后,状态机能返回为被设计成能处理全部事件的公共事件循环。如果必要的话,状态机自然的挑选每一个事件并移动到下一个状态。很容易在这种设计里增加新的事件,因为状态机在任何时候可以响应任何事件。一个事件驱动的基于状态机的应用程序,和传统的程序相比,更加灵活,更有弹性。

       当编码基本上不成问题时,大部分编程工作就花在设计应用程序上了。

        一个事件驱动程序运行时,不停的查看可能的事件,当一个事件被探测到,就分派这个事件到合适的状态机构件(见图 1.11(b) )。为了让这个方法可以工作,必须连续频繁的查看事件。这意味者状态机必须很快的运行,这样程序能返回来查看事件。为了符合这个要求,状态机不能进入到需要忙等待一段较长或不确定时间的状态。这种状态最普遍的例子就是在一个状态处理函数内的 while 循环,退出循环的条件,比如按键输入,不受程序的控制。这类一个无限循环的程序结构,被称为“阻塞“代码 ,你可以在 Quickstart应用程序里看到这些例子(见图 1.11(a) )。为了让事件驱动型编程模式能工作,你必须只编写“非阻塞”代码。

状态模式是GoF23个模式中最常用的之,这篇小文不打算涉及方方面面的内容,只想在状态模式的高效运用方面谈下自己的心得体会。   状态模式是用来设计状态机的,因此下面的叙述中将它们等同理解。有关状态机设计方面的书籍,我这里隆重推荐本:《Practical Statecharts in C/C++ Quantum Programming for Embedded Systems》,中文名叫做《嵌入式系统的微模块化程序设计实用状态图C/C++实现》,北航出版的,作者是Miro Samek博士,长期从事嵌入式实时系统的开发,具有丰富的经验。如果你想对状态机领域进行比较深入的研究,这本书绝对不容错过。   让我们先来看看比较“古老”的状态机实现,假设你还是用C语言。般而言,我们用得到状态机系统都可以称为事件(消息)驱动系统,系统往往处于某个状态,等待外部的激励。这些激励可以是外部的事件、定时器超时等等,系统收到这些事件后,进行相应的处理,然后跃迁到新的状态(状态也可能不变)继续等待下个激励的到来,最后直到相应的事务处理完毕为止。   典型的状态机实现中需要考虑几个要素:状态、消息(及其内容)、消息处理函数以及系统上下文等。系统处于某个状态,收到某个消息后,解析出消息内容,然后调用相应的消息处理函数进行处理,而消息处理函数往往会用到状态机的上下文数据,消息处理完毕系统会跃迁到新的状态。   典型代码大致如下:   switch (state)   {    case STATE1:    switch (msg)    {    case MSG1:    HandleMsg1(msgpara,context);    nextstate(STATE2);    break;    case MSG2:    HandleMsg2(msgpara,context);    nextstate(STATE3);    break;    /*......*/    }    case STATE2:    switch (msg)    {    case MSG3:    HandleMsg3(msgpara,context);    nextstate(STATE3);    break;    /*......*/    }    /*......*/   }   可以看到这就是所谓的平面状态机,特点就是先枚举状态,然后再枚举消息,如果找不到的话,就将消息丢弃。   为了使状态机更高效的运行,这里有几个小技巧,稍为总结下。   (1)把接收概率大的消息放在前面   把同个状态下最有可能收到的消息放在前面。个状态下可能要处理很多消息,这视乎你状态划分的粒度大小。每个消息收到的机会并不是均等的,有些消息系统收到的概率很大,有些很小,因此把接收概率大的消息放在前面,这样可以减少case消息时的比较次数,相应的执行效率就提高了。对于状态机的运行而言,这样的节省当然微乎其微,但假如你的系统同时运行成千上万个这种状态机时,那么就有必要考虑下这种优化了。   (2)查表法   第(1)种方法再怎么优化,也需要枚举状态和消息,假如能把这方面的开销变成零,那么效率自然可以进步提升。我们可以想象把消息处理函数指针放在个二维数组(表)中,其中维代表状态,另外维代表消息序号,那么通过p[state][msg]就可以定位到当前状态下当前消息的处理函数。对些简单的应用,甚至可以把新状态也存放在这张二维表中,这样的好处是用户不需要显示调用状态跃迁函数。当然对于些状态有不同执行路径的情况,状态的跃迁可能就要放在消息处理函数之中。   (3)消息先分段再查表   般而言,状态机的状态数目不会很多,当然接收的消息数目也是有限的。但般来说,消息是不连续的,这样应用查表法可能内存的开销就比较大,尤其是消息序号比较稀疏的时候,内存更加浪费。   在般的嵌入式软件开发中,我发现往往可以将消息进行归类分段,比方说个接口的消息定义成段。这样虽然消息不连续,但通过分段后可以将消息放在个较紧凑的内存空间中,在这个空间里再运用查表法,就有可能达到效率和空间开销的平衡。注意,我是说有可能,并不是定,这取决于具体情况。系统收到消息后,先判断消息处于哪个分段,然后调用p[state][msg - offset]来进行处理
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值