UML状态图的实用C/C 设计(QP状态机)

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) )。为了让事件驱动型编程模式能工作,你必须只编写“非阻塞”代码。

(0)

相关推荐