QP: 一种替代前后台和RTOS的程序框架

1 篇文章 3 订阅

QP: 一种替代前后台和RTOS的程序框架

1、什么是QP

QP(量子平台)是一个基于活动对象(Active Objects )和层次式状态机( Hierarchical State Machines)的开源实时嵌入式框架(real-time embedded frameworks)和运行环境。QP系列由QP/C和QP/C++框架组成,它们受到严格的质量控制,有完整的文档,并且在灵活的双重许可模型下可用。QP的官网是QP™ Real-Time Embedded Frameworks (RTEFs)

QP/C和QP/C++实时嵌入式框架(RTEF)提供了现代的开源软件架构,它将事件驱动的并发模型(称为活动对象,又名actor)与层次式状态机结合在一起。该体系结构本质上支持并自动执行并发编程的最佳实践。这使得应用程序比传统实时操作系统(RTOS)的“裸”线程和无数阻塞机制更安全、响应更快、更易于管理。QP框架还提供了更高层次的抽象,以有效地将图形建模和代码生成应用于深度嵌入式系统,例如基于ARM Cortex-M的mcu。

下图是传统的顺序式编程(RTOS)和事件驱动式编程(RTEF)的可用的机制对比图

图1 传统的顺序式编程(RTOS)和事件驱动式编程(RTEF)的可用的机制

由图中RTOS和RTEF对比中可以发现,RTOS是阻塞式的,而RTEF是非阻塞式的,RTEF的实时性更高。它们的共同点是都有任务和消息队列的概念。RTOS的时间延时、信号量、互斥锁、事件标志、邮箱等机制都是阻塞型的,当请求的资源得不到满足时会进行阻塞等待;而RTEF不能被阻塞。

2、目标人群

嵌入式系统的事件驱动型编程技术 第二版》详细讲解了QP的所有技术细节,它是以C代码为基础进行讲解,除了讲解理论知识,还用C代码例子完整实现。

本文的目标人群为以下对事件驱动型编程和现代状态机感兴趣的软件开发者:

  • 嵌入式程序员会发现一个完整的,立即可以用的事件驱动型构架,用来开发应用系统。本文描叙了状态机编码策略。同样重要的,一个与之配套的执行并发状态机的实时框架。这两个因素是互补的,离开了对方,每个因素都不能发挥它的最大潜能。
  • 寻求一个实时内核或实时操作系统的嵌入式系统开发者会发现,QP事件驱动型平台可以做到RTOS可以做到的任何事情,而且事实上,QP包含了一个完全可抢占的实时内核(QK)和一个简单的协作式调度器(QV)。
  • 无线传感器网络等超低功耗系统的设计者会发现,如何把基于事件驱动状态机的解决方案裁剪以适合最小的微控制器。超轻量级的QP-nano版本在仅1-2KB字节的ROM中融合了一个层次式事件处理器,一个实时框架和一个协作式或者完全可抢占式的内核。
  • 对于复杂性的应用,大规模的大型并行服务应用的设计者会发现,结合了层次式状态机的事件驱动型解决方法很容易扩展,在管理非常大数目的状态化构件,例如客户任务方面,它非常理想。可以证明,QP的嵌入式设计理念对每个构件的时间和空间性能都提供了关键的支持。
  • 使用C或C++的图形用户界面开发者和计算机游戏程序员会发现QP很优雅的辅助了GUI库。QP提供了高层的基于层次式状态机的“屏幕逻辑”,而由 GUI 库处理底层的窗口部件 (widget)并在屏幕上画图。

3、介绍

几乎所有计算机系统,特别是嵌入式系统,是事件驱动型的,这意味着它们持续等待某些外部或者内部的事件发生,比如一个时钟节拍 (tick),一个数据包的到来,一个按键被按下,或者一次鼠标的点击。确认事件后,这类系统产生相应的反应,去执行相应的计算,去操作硬件,或者去产生“软”事件去触发其他的内部软件构件。(这就是为什么事件驱动型系统也被称作反应系统(reactive system) 的原因)。软件一旦完成了事件处理,就退回到等待下一个事件发生的状态。

你肯定熟悉基本的顺序控制,使用这种方法时,一个程序在它执行路径的不同地方等待事件,它或者主动的轮询事件,或者被动的阻塞于一个旗语(semaphore) 或其他的操作系统原语。尽管这种事件驱动型系统的编程方法在很多情况下起作用,但是,当系统有许多可能的事件源,而你也不能预测事件的到达时间和次序,而且及时处理事件变得至关重要时,这种方法不能很好的工作。问题在于,当顺序式程序在等待某类事件时,它没做任何其他工作,也不对其他事件起反应。

显然,我们需要的是一个程序结构,它可以对不同的可能事件反应,任何事件可以在不能预测的时刻以不能预测的次序到达。在嵌入式系统里,比如家用电器,手机,工业控制器,医疗设备和其他系统中,这个问题非常普遍。在现代桌面计算机中,比如在使用一个网页浏览器,文字处理器,或者速算表时,这个问题也很突出。绝大多数这些程序有一个现代的图形用户界面(GUI),它显然可以处理多种事件。所有当代的GUI 系统以及许多嵌入式应用,都采用了一个共同的程序结构,可以优雅的解决需要及时的处理异步事件的难题。这种程序结构通常被称为“事件驱动型编程”。

  • 控制的倒置 (Inversion of Control)

    事件驱动型编程需要一个完全不同的思考方式,它和传统的顺序式编程方法例如“超级循环”,或传统的RTOS的任务不同。绝大多数的现代事件驱动型系统根据好莱坞原则被构造,“不要呼叫(调用)我们,我们会呼叫(调用)您” (Don’t call us, we will call you.)。因此,当它等待一个事件时,这个事件驱动型系统没有控制权,事实上,它甚至没有被激活。仅当一个事件到达了,程序才被调用去处理这个事件,然后它又很快的放弃控制权。这种安排允许这个事件驱动型系统同时等待许多事件,结果系统对所有需要处理的事件都能保持反应。

    这个方案有三个重要的后果。第一,它意味着一个事件驱动型系统被自然的分解到应用程序里面,由应用程序处理事件,而监督者是事件驱动的平台,由它等待事件并把它们分发给应用程序。第二,控制存在于事件驱动平台的基础设施 (infrastructure) 中,因此从应用程序的角度看,和传统的顺序式程序相比,控制被倒置了。第三,事件驱动型应用程序必须在处理完每个事件后交出控制权,因此和顺序式程序不同的是,运行时上下文和程序计数器不能被保留在基于堆栈的变量中。相反,事件驱动应用程序变成了一个状态机,或者实际上一组合作的状态机,并在静态变量里保留从一个事件到另一个事件的上下文。

  • 事件驱动型框架的重要性

    控制的倒置,在所有事件驱动型系统中非常典型,因此它使事件驱动型基础设施(infrastructure)具有了一个应用程序框架(framework) 的全部特性,而不再是一个工具集。当使用工具集时,比如使用传统的操作系统或RTOS,你编写应用程序的主要部分,当你需要重用(reuse)时,你调用工具集。而当你使用框架时,你重用应用程序的主要部分,只需编写它调用的代码。

    另一个重点是,如果想把许多事件驱动型状态机组合到一个系统中,事件驱动型框架就是必需的。要执行并发状态机,不能仅仅像使用传统的RTOS那样只需调用API。状态机需要一个底层构架(框架)为每一个状态机,事件队列,以及基于事件的时间服务提供最小的,运行到完成(RTC)的执行上下文。这点非常关键。状态机不能在“真空”中操作,而且如果没有一个事件驱动型框架的支持,它就没有实用性。

  • 主动对象计算模型

    QP提供了分解事件驱动型系统的两个最有效的技术:层次式状态机和事件驱动型框架。这两个要素的结合被称为主动对象计算模型 (active object computing model)。术语“主动对象”是从UML而来,表示一个自治的(autonomous) 对象,它和其他主动对象通过事件进行异步交互。UML还提议使用状态图的UML变体为事件驱动型主动对象的行为建模。

    QP中,主动对象使用事件驱动型框架QF实现,QF是QP事件驱动型平台的主要构件。QF框架依次的执行主动对象,处理全部线程安全的事件交换细节,以及主动对象的内部处理过程。通过对事件进行排队,并依次把它们(以每次一个的方式)派发给主动对象的内部状态机,QF保证状态机执行预设的运行到完成的语义。

    层次式状态机和事件驱动型框架结合的概念基础早已有之。事实上,它们已经被广泛的使用了最少20年。几乎今天市场上全部成功的设计自动化工具都是基于层次式状态机(状态图),并在内部包含一个类似QF的事件驱动型实时框架。

  • 以代码为中心的开发方式

    嵌入式系统的事件驱动型编程技术 第二版》所采取的实现方法是以代码为中心的(code-centric) ,简约的和底层的。这个特征不是贬义的,它只是意味你将会学到在不用巨型工具的情况下,如何把层次式状态机和主动对象直接映射成C或者C++代码。问题的关键不是工具――问题是“理解”。

    现代化的设计自动化工具确实很强大,但是它们不适合所有人。此书展示的以代码为中心的实现方法为这些开发者提供了一个重量级工具的轻量级替代品。

    最重要的是,没有工具能替代你对概念的理解。例如,在某个关键的状态转换中,确定哪个进入/退出动作以什么序列执行,这些不是你能通过运行设计工具显示的动画能发现的。答案来自于你对底层状态机实现的理解。即使你决定以后使用自动化设计工具,即使它使用和本书不同的状态图实现技术,因为你具有对底层实现机理的理解,你将会有更大的信心和更有效率。

  • 关注实际的问题

    状态机和事件驱动型框架不能仅仅被看作是一些特征的集合,因为有些特征被隔离开来后并没有意义。只有考虑设计而不是编码时,才能有效的使用这些强大的概念。为了从这个角度理解状态机,你必须从事件驱动型编程的角度来理解问题。讨论事件驱动型编程的问题,为什么它们是问题,以及状态机和主动对象计算模式如何帮助解决这个问题。因此,我希望可以使你前进,每次一小步,直到最后层次式状态机和事件驱动型框架成为你的解决问题的自然地思考方法,并放弃那种使用传统的多层嵌套的if和else编程方式,或者在传统的RTOS中通过旗语和事件标志来传递事件的方法。

  • 面向对象

    即使QP/C使用C作为主要的编程语言,也将彻底地使用面向对象的设计原则。同实际上所有的应用框架一样,QP使用基本的封装概念(类)和单一继承作为主要的,为实际应用定制,特例化和扩展框架的实现机制。使用C语言的读者不要担心这些概念可能对你太新了。在C语言的层次,封装和继承是很简单的编码技术,下面简单介绍一下C语言的继承实现。

  • C 语言里的单一继承 (Single Inheritance)

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

    ​图2 (a) C里的结构派生,(b)内存配置(c)UML类图

    如图1(b) 所示,这种结构的嵌套总是把数据成员super放在派生结构的每一个实例的开始处,这是被C标准保证的。具体来说,“指向一个结构对象的指针,在适当的转换后,指向它的最初的成员。在一个结构对象中可能有未命名的填充部分,但不是放在开始处”。 这种内存配置,使你可以把一个指向派生的ScoreEvt结构的指针当作一个指向QEvent基本结构的指针一样处理。这些都是合法的,可移植的,而且是C标准所保证的。因此,你总是可以安全的传递一个指向ScoreEvt的指针,给任何需要一个指向QEvent的指针的函数。(严格来讲,在C里你应该显式的转换这个指针。在OOP里,这种转换被称为向上类型转换 (upcasting) ,而且它总是安全的。)结果,所有为QEvent结构设计的函数自动的适用于ScoreEvt 结构和其他从QEvent派生的结构。图1© 的UML类图,说明了ScoreEvt结构和QEvent结构的继承关系。QP非常广泛的使用单继承,不仅用来派生带有变量的事件,也用来派生状态机和主动对象。

4、UML状态机

状态机是描叙和实现必须及时地对外界输入事件反应的事件驱动型系统的方法。先进的UML状态机展示了状态机理论和符号的当前状态。在事件驱动型应用程序中实际使用UML状态机的方法,帮助写出高效的,具有被充分理解的行为,并且可维护的软件,而不是编写有着复杂的if-else的“意大利面条”代码。

传统的顺序式程序能被构建为一个单一控制流程,使用标准的构造,比如循环和嵌套的函数调用。这类程序使用程序计数器(PC)的位置,进程调用树,以及在栈上分配的临时变量来表示绝大多数执行上下文。

相反,事件驱动式程序需要一系列的精细粒度(fine-granularity) 的事件处理函数来处理事件。这些事件处理函数必须执行的很快并返回主事件循环,这样在调用树里不需维护上下文和程序计数器。另外,所有的栈变量在调用不同的事件处理函数时不会被保留。因此,事件驱动式程序非常依赖于通过使用静态变量维护在从一个事件处理函数转换到下一个执行函数时的执行上下文。因此,事件驱动型编程的其中一个最大的挑战在于管理使用数据表示的执行上下文。这里的主要问题是上下文数据必须以某种方法反馈到事件处理函数代码的控制流,这样每一个事件处理函数就能仅仅执行和当前上下文相对应的动作。传统上,这种依靠上下文的方式经常导致嵌套很深的 if-else 结构,用来引导基于上下文数据的控制流。

如果你能消除哪怕一小部分这些条件分支(“意大利面条”代码),软件将更容易被理解,测试和维护,并且这些代码的繁杂执行路径数码会极大的减少,通常是成数量级的减少。基于状态机的技巧能完全完成这个任务 —戏剧性的减少通过代码的不同的路径并监护在每个分支点的条件测试。

4.1 基本的状态机概念

事件 - 动作范型能被扩展,明确的包含对运行上下文的依靠。如它所揭示的,绝大多数事件驱动型系统的行为可以被分解成相对小数目的块 (chunk),在每个单独的块的事件响应实际上仅取决于当前的事件类型,不再取决于过去事件的顺序(上下文)。也就是说,事件 - 动作范型继续被使用,但仅局限在每个单独的块里。

基于这个概念的一个通用的为行为建模的方法是使用一个有限状态机 (FSM) 。这种方法里,“行为的块”被叫做状态行为的改变(例如,响应任何事件的改变)对应着状态改变,被称为状态转换。 FSM是一个界定对全部行为的约束的很有效的方法。“在一个状态里”意味着系统仅响应全部被允许事件的一个子集,产生可能响应的一个子集,并直接改变状态到所有可能状态的一个子集。

FSM的概念在编程时很重要,因为它让事件处理明确的取决于事件类型和系统的执行上下文(状态)。当被正确使用,状态机是一个强大的“意大利面条减少者”,它戏剧性的裁剪掉流过代码的执行路径的数量,简化了在每个分支入口的条件测试,简化了不同执行模式之间的转换 。

4.1.1 状态(state)

状态非常有效的捕捉系统历史的相关方面。例如,当你在一个键盘击键,它产生的字符代码,根据CAPS LOCK 是否被激活,代码将是大写或小写字符。从而键盘的行为可以被分成2个块(状态):default状态和caps_locked状态。(绝大多数键盘事件上有一个指示键盘是在caps_locked状态的LED)。键盘的行为仅取决于它的历史的某些方面,也就是Caps Lock键是否被按下过,而不取决于比如说以前有多少其他的按键被输入以及如何输入。状态能被抽象出所有可能(但不相关)的事件序列并仅捕捉到相关的事件。

把这个概念和编程联系起来,意味着不用许多变量,标志和复杂逻辑来记录事件历史,而主要依靠一个状态变量,它能被假定为一些有限的已经被确定的值(在键盘这个例子里是两个值)。状态变量的值清楚的定义了系统在任何给定时刻的当前状态 (current state) 。状态的概念减少了在代码中分辨执行上下文的问题,只要测试一个状态变量而不是许多变量,从而排除了大量的条件逻辑运算。实际上,在所有最基本的状态机实现技术里,比如在第三章讨论的嵌套式 switch 语句技术,明确的测试状态变量从代码中消失了,从而进一步减少了意大利面条代码(你将在第三章和第四章体会到它的作用)。更进一步,在不同状态间转换也被极大的简化了,因为你需要有条理方式的指定一个状态变量而不是去改变许多变量。

4.1.2 状态图

FSM有一个叫状态图 (state diagram) 的图型表达方法。这些图是有向图,节点代表状态,连接线代表状态转换。例如,下图展示了一个对应于计算机键盘状态机的UML状态转换图。在UML里,状态表示为圆角矩形,标签是状态名。转换被表示为箭头,标签是触发事件,它后面是可选需执行动作的列表。初始转换 (initial transistion)从一个实心圆点出发,确定了当系统在最初开始时的启动状态。每个状态图必须有一个这样的转换,它不能带标签,因为这样的转换不需事件触发。初始转换能够带有关联的动作。

图3 UML状态图表示计算机键盘状态机

状态图基本知识的简要描述

状态图展示了某个给定上下文类的静态状态空间,事件造成从一个状态到另一个状态的转换,动作是结果。
图4 状态和一个转换

上展示了状态的表示选项,和一个状态转换的符号。一个状态总是使用圆角矩形框表示。状态的名字用粗体字写在顶部。在名字下方,一个状态可以有一个可选的内部转换隔间,使用一条水平线和状态名字分割开。内部转换隔间可以包含进入动作(跟在保留符号entry后的动作),退出动作(跟在保留的符号exit后的动作),和其他内部转换(例如,在图B.5里被EVT触发的转换)。

一个状态转换使用一个从源状态边界出发并指向目标状态边界的箭头表示。一个转换最少必须使用触发信号作为标签。触发后面可以有可选的一个事件变量,一个监护条件,一个动作的列表和一个被发送事件的列表。
图5 组合状态,初始转换和最后状态

上图展示了一个组合状态(超状态),它包含其他的状态(子状态)。每个组合状态可以有一个独立的到指定初始子状态的初始转换。尽管图B.6仅展示了一层嵌套,这个子状态也可以是组合状态。

图6 正交区域和伪装态

上展示了组合状态stateA,它具有由一条虚线分割开的正交区域(AND-状态)和2个伪装态:选择点(choicepoint)和深历史状态(deep history) 。

4.1.3 状态图和流程图的比较

状态机方法的新手常把状态图和流程图 (flowchart) 搞混。 UML规范在这方面没有帮助,因为它把活动图归结在状态机包 (state machine package)里。活动图本质上是复杂的流程图。

下图展示了状态图和流程图的对比。状态机( (a) 部分)对明确的触发执行动作。相反,流程图( (b) 部分)不需要明确的触发,而是在完成了活动后在它的图上自动的从一个节点转换另一个节点。

图7 比较状态机(a)和活动图(流程图)(b)

和状态图对比,流程图倒置了顶点和弧的含义。在状态图里处理过程(转换)是和弧相关联的。而在流程图里,处理过程和顶点相关联。当状态机停在某个状态等待一个事件发生时,它是空闲的。当流程图停在某个节点时,它忙于处理操作。上图试着通过把状态图的弧和流程图的处理阶段排列对比来展示这两个角色的倒置。

可以把流程图和制造业的装配线相比,因为流程图描叙了一些任务从开始到结束的进程(例如,通过编译器把源代码输入转化成目标代码输出)。状态图通常没有进程的标识。例如,一个计算机键盘,当它在caps_locked状态时,和在default状态相比,它没有在一个更高级的阶段。它简单的对事件产生不同的反应。状态机的一个状态是用来确定一个特定的行为而不是处理过程的一个阶段的有效方法。

状态机和流程图之间的区别特别重要,因为这两个概念代表了两个完全相反的编程范型:事件驱动型编程(状态机)和转换性编程(流程图)。你不断的思考有效的事件才能设计有效率的状态机。相反,在流程图里事件(如果它们被考虑的话)仅是被次要考虑的。

4.1.4 扩展状态机

对软件系统里状态的一个可能的解释是,每一个状态代表了整个程序内存的有效值的一个集合。甚至对只有几个基本变量的最简单的程序,这个解释都会导致一个天文数字的状态数量。比如,一个32位整数能有超过40亿个不同的状态。显然,这个解释不切实际,因此程序变量通常被从状态里分离开。何况,系统的完整状况(被称为扩展状态)是定性(qualitative)(状态)和定量(quantitative)(扩展状态变量)的结合。在这种解释里,一个变量的改变不总是暗示着在系统的行为在定性方面有一个改变,从而不会导致一个状态的改变。

带有变量补充的状态机被称为扩展状态机 (extended state machine) 。扩展状态机能把基础方法应用于更加复杂的问题,如果没有包含扩展状态变量,这个方法就不实用。例如,假设键盘的行为取决于到目前为止在它上面输入的字符的数量,并且比如说,在1000次击键后,键盘破裂并进入到一个最终状态。使用没有内存的状态机对这个行为建模,你将需要引入1000个状态(例如在状态stroke123按下键会到状态stroke124),这明显的是不实际的主张。或者,你能构建一个带有一个key_count倒数计数器变量的扩展状态机。这个计数器将被初始化成1000,并在每次击键后递减,而不用改变状态。计数器到0时,状态机将会转换到最终状态 (final state) 。

下图的状态图是一个扩展状态机的实例,这里系统的完整状况(称为扩展状态)是定性方面—状态 - 和定量方面—扩展状态变量(比如倒数计数器key_count)的结合。在扩展状态机里,一个变量的某次改变不总是意味着系统的行为在定性方面的一次改变,从而不会总是导致一次状态的改变。

扩展状态机的明显优点是灵活性。例如,把“键盘”的寿命从1000扩展到10000次击键,完全不会把扩展状态机搞得更复杂。所需的修改仅是在初始转换里改变key_count的初始化值。

图8 “键盘”的扩展状态机,带有扩展状态变量key_count和不同的监护条件

4.1.5 监护条件(Guard Condition)

扩展状态机的这个灵活性需要付出代价,因为在定性方面和定量方面产生了复杂的耦合。这个耦合通过附着在转换上的监护条件发生,如上图所示。

监护条件(简称监护)是基于扩展状态变量和事件参数(见下一节对事件和事件参数的讨论)动态评估的布尔表达式。监护条件仅在表达式为真时才允许动作或转换,而在表达式为假时禁止它们,从而影响一个状态机的行为。在UML符号里,监护条件在方括号里出现(如 [key_count == 0] )。

对监护条件的需求是在状态机方法里增加内存(扩展状态变量)的直接后果。请谨慎使用,扩展状态变量和监护条件建立了一个难以置信强大的机制,能够极大的简化设计。当实际编写一个扩展状态机时,监护条件变成一些if和else语句,而它们是你想使用状态机最先消除的。过份的使用监护条件,将发现自己努力半天又回到了起点(“意大利面条”代码),监护条件实际上开始处理系统的所有相关状况。

实际上,滥用扩展状态变量和监护条件是基于状态机的设计的结构性退化的主要原因。通常在日常工作中,增加另外一个扩展状态变量和另外一个监护条件(另外一个if和else)而不是把相关的行为放入一个新的系统的定性方面—状态中,看来很有诱惑性,特别是对那些刚接触状态机方法的程序员。这类结构性退化直接对应于增加或减少状态数目(实际的或感知的)所需开销。

成为高效率状态机设计者的主要一个挑战是培养一种感觉,知道行为的某些部分将会被捕捉成“定性”方面(状态),而另外一些部分最好被当做“定量”方面(扩展状态变量)。一般的,你将主动寻找机会去捕捉事件历史(什么发生了)作为系统的“状态”,而不是在扩展状态变量中存储这些信息。

捕捉行为做为定量的“状态”也有它的不利条件和局限性。首先,状态以及在状态机里的转换拓扑必须是静态的并在编译时被固定,这是非常大的局限而且不灵活。确实,你能容易的设计“状态机”,让它可以在运行时更改它们自己(这常发生在你试着用状态机重新编码“意大利面条”时)。然而,这像是在编写自我改写代码,这实际上在编程的早期时代发生过,但是很快的被作为一个一般性的坏主意不再被考虑。因此,“状态”仅能捕捉行为表现里被称为先验的 (a priori) 并且不会在未来改变的静态方面。

例如,在计算器捕捉一个小数点的输入作为一个单独的状态entering_the_fractional_part_of_a_number是很好的主意,因为一个数字只能有一个小数部分,这是先验的,而且在未来不会改变的。然而,不使用扩展状态变量和监护条件去实现一个“便宜键盘”实际上是不可能的。这个例子指出了量化“状态”的主要弱点,它不能存储太多的信息(比如很多的击键次数)。扩展状态变量和监护条件是这样一个给状态机增加额外的运行时灵活性的机制。

4.1.6 事件(Event)

一个事件是对系统有重大意义的一个在时间和空间上所发生的事情。严格的讲,UML规范里,术语event指所发生事情的类型而不是发生事情的任何具体的实例。例如,Keystroke是键盘的一个事件,但是每次的按键不是一个事件而是一个Keystroke事件的具体的实例。对键盘有关的另一个事件也许是Power-on,但是在明天某个时间点打开电源将是Power-on事件的一个实例。

一个事件能有相关联的参数,允许事件实例传达不仅某些感兴趣的事件发生了,而且还有关于发生事情的量化信息。例如,在一个计算机键盘上按下一个键会产生Keystroke事件,它有相关联的参数,用来携带字符的扫描码以及Shift,Ctrl和Alt的状态。

一个事件实例比产生这个事件的所发生的事情存活的更长,并可以传递这个所发生的事件给一个或更多的状态机。一旦它被产生,事件实例走过一个由三个阶段组成的处理周期。第一,当事件实例被接收时并等待处理时它被接收(比如它被放在一个事件队列里)。然后,事件实例被派送给状态机,这里它变成当前事件。最后,当状态机结束了对这个事件实例的处理后,它被消耗。一个被消耗的事件实例不再对处理有效。

4.1.7 动作和转换(Action and Transition)

当一个事件实例被派送,状态机通过执行动作来响应,比如改变一个变量,执行输入输出,调用一个函数,产生另一个事件实例。或者变成另一个状态。任何和当前事件关联的参数值对被这个事件直接导致的所有动作都有效。

从一个状态切换到另一个状态被称为状态转换,引发它的事件被称为触发事件 (triggering event) ,或简单的被称为触发 (trigger) 。在键盘例子里,如果当CapsLock键被按下时键盘在default状态,键盘将进入到 caps_locked 状态。然而,如果键盘已经在caps_locked状态,按下CapsLock键会引发一个不同的转换—从caps_locked到default。在这2个事例里,按下CapsLock就是这个触发事件。

在扩展状态机里,一个转换可以有一个监护条件,这意味着仅当监护条件被评估为真时转换才能“启动”。一个状态对一个同样的触发可以有许多转换,只要它们没有重叠的监护条件;然而,在一个共同的触发发生时,这种情况会在评估监护条件的次序上产生问题。UML规范特意不规定任何特定的次序,相反,UML让设计者去设计监护条件,这样它们评估的次序就无关紧要了。在实际应用上,这意味这监护条件表达式没有副作用,最少不会改变其他有相同触发的监护条件的评估。

4.1.8 运行 - 到 - 完成执行模型 (Run-to-Completion Execution Model)

所有的状态机体系,包括UML状态图,普遍的假设一个状态机在它能开始处理下一个事件前完成对每个事件的处理。这个执行模型被称为运行 - 到 - 完成,或RTC。

在RTC模型里,系统在分散的不可分割的RTC步骤里处理事件。新到的事件不能中断当前事件的处理,而且必须被存储(通常是存储在一个事件队列里),直到状态机又变成空闲。这些语义完全避免了在一个单一的状态机里的任何内部并发问题。RTC模型也克服了处理和转换相关联的动作时的概念性问题,因为状态机在动作过程中没有处于一个明确定义的状态(在2个状态之间)。在处理事件时,系统没有响应(不可观测性),因此在那个时段这种不清楚的状态没有实际的意义。

然而请注意,RTC不意味着状态机必须独占CPU直到 RTC步骤被完成。可抢占性约束只适用状态机在忙于处理事件的任务上下文的情况。在一个多任务处理环境里,其他(和繁忙的状态机的任务上下文无关的)任务也可以运行,可能抢占当前执行的状态机。只要其他状态机互相间不共享变量或其他资源,就不会有并发性的危险。

RTC处理的关键优点是简单。它最大的不利条件是状态机的响应由它最长的 RTC步骤决定。为实现较短的RTC步骤常常会明显的让实时设计复杂化。

4.1.9 反应性系统里的行为重用

所有反应性系统似乎用一种相似的方法重用行为。例如,所有GUI的观感特性由同样的模式产生,这被Windows宗师Charles Petzold称为Ultimate Hook。这个模式非常精辟:一个GUI系统首先分派每个事件到应用程序(例如 Windows 调用在应用程序里面的一个特定的函数,而把事件作为一个参数)。如果没有被应用程序处理,这个事件流回系统。这样就建立了一个层次式的事件处理次序。应用程序,概念上在较低的层次,首先处理每个事件;从而应用程序可以选择并用它喜欢的任何方法反应。同时,所有没有被处理的事件流回较高的层次(换言之,回到GUI系统),这里它们依照标准的观感被处理。这是差异化编程的一个例子,因为应用程序员仅需要编码和标准系统行为的不同部分。

4.1.10 层次式嵌套状态

Harel状态图通过结合状态机体系而为Umtimate Hook模式带来一个符合逻辑的结论。状态图对经典FSM最重要的革新是引入了层次式嵌套状态(这就是为什么状态图也被称为层次式状态机HSM的原因)。和状态嵌套关联的语义如后所示(见下图(a) ):如果一个系统在嵌套的状态 s11( 称为子状态 ) 里,它也(隐含的)在环绕的状态 s1( 称为超状态 ) 里。这个状态机将试着处理任何在状态s11上下文的事件,概念上它在较低层次。然而,如果状态s11没有指示如何去处理这个事件,这个事件不会像在传统的“平面”状态机里那样被默默的丢弃,相反,它被自动的在超状态s1的较高层上下文里被处理。

这就是系统在状态s11和s1的含义。当然,状态嵌套没有限制只能有一层,这个事件处理的简单规则可以被递归地应用于任何层数的嵌套。

图9 UM符号,层次式嵌套状态(a) ,一个面包炉的状态模型,状态toasting和baking共享一个从状态heating到状态door_open的转换

包含其他状态的状态被称为复合状态 (composite state) ,没有内部结构的状态被称为简单状态 (simple state)。一个嵌套的状态当它没有被其他状态包含时被称为直接子状态 (direct substate),否则,它被归类于过渡性嵌套子状态 (transitively nested substate) 。

因为符合状态的内部结构可以任意复杂,任何层次式状态机可以被看作一个(高阶)复合状态的内部结构。定义一个复合状态作为状态机层次的终极根有概念上的便利。在UMI规范里,每个状态机有一个顶状态 (top state) (每个状态机层次的抽象根),它包含了整个状态机的所有的其他元素。画出这个全包含的顶状态的图形是可选的。

如你所看到的,层次式状态分解的语义被设计成通过直接支持 Ultimate Hook 模式,从而帮助共享行为表现。子状态(嵌套状态)仅需要定义与超状态(包围状态)不同的部分。一个子状态可以容易的通过忽略公共处理的事件,从而重用它的超状态的公共行为,然后这些公共行为自动的被高层的状态处理。通过这个方法,子状态能共享它们的超状态的全部行为。例如,在烤面包机的一个状态模型里(上图 (b) ),状态 toasting和 baking 共享在它们超状态 heating 定义的公共转换 DOOR_OPEN 到达状态 door_open。

状态层次里最常被强调的方面是抽象——一个古老但是强大的对付复杂性的技术。与同时面对一个复杂系统的全部方面相反,我们常常可能忽略(抽象出)系统的一些部分。层次式状态是一个理想的隐藏内部细节的机制,因为设计者能容易的放大 (zoom out) 或缩小 (zoom in) 视野,从而隐藏或展示嵌套的状态。尽管抽象本身没有减少全部系统的复杂性,它还是有价值的,因为它减少了你在同一时间需要处理的细节的数量。

抽象本身也许很有价值,但不能简单的通过把复杂性隐藏在复合状态内来欺骗你自己。然而,复合状态不但隐藏了而且通过强大的重用机制(Umltimate Hood模式)积极的减少了复杂性。没有这种重用,甚至稍微增加系统的复杂性都常会导致状态和转换的数量爆炸式的增加。例如,如果把上图( b )的状态图转换成经典平面状态机,必须在两个地方重复一个转换(从heating到door_open)——从 toasting转换到 door_open和从 baking 状态到 door_open。避免这样的重复性使HSM可以和系统复杂性相应的增加。当被建模系统的复杂性增加,重用的机会也在增加,从而抵消了在传统的FSM里状态和转换的爆炸式增长。

4.1.11 行为继承 (Behavioral Inheritance)

事实上,层次式状态有简单但深刻的语义。嵌套状态也不仅是“当一组事件应用于几个子状态时大型的图形简化法” 。状态和转换的数目的真实的减少,远超过整洁的图形。也就是说,简化了的图形只是通过状态嵌套从而重用行为的副产品。

状态嵌套的基本特性来自把抽象和层次结合,这是传统上减少复杂性的方法,在软件里被称为继承。在OOP里,类继承的概念描叙了对象的类之间的关系。类继承描叙了类之间“是…”的关系。例如,类Bird也许从类Animal派生。如果一个对象“是” bird (类 Bird 的实例),它自动的“是”Animal,因为所有适用于动物的操作(如进食,消化,生殖)也适用于鸟。但是鸟更加特殊,因为一般它们有不适用于动物的操作。例如,flying()适用于鸟但不适用于鱼。

如在上一节看到的,继承的所有这些基本的特性同样的适用于嵌套状态(只要把“类”用“状态”替换),这并不神奇,因为如同面向对象的类继承一样,状态嵌套是基于相同的基础性的“是…”分类法。例如,在一个烤面包机的状态模型里,状态 toasting嵌套在状态 heating 里。如果烤面包机是在toasting状态,它自动的是在 heating 状态,因为所有属于 heating 的行为也适用于 toasting(例如必须开启加热器)。但是toasting更加特别,因为它有些一般不能适用于 heating 的行为。例如,给面包上色(浅或深)适用于 toasting但是不适用于 baking 。

在嵌套状态的情况下,“是某种…” (is-a-kind-of)关系仅需要被“是在某种状态内…” (is-in-a state)关系替换。除此以外,它也同样的基本分类法。状态嵌套允许一个子状态从它的祖先(超状态)继承行为;因此,它被称为行为继承。

: 术语“行为继承”不是UML规范的概念。请注意行为继承描叙了子状态和超状态的关系,不要把它和适用全体状态机的传统的(类)继承搞混。

4.1.12 进入和退出动作 (Entry and Exit Actions)

UML的状态图里的每个状态机都可以有可选的进入动作,它在进入一个状态时被执行,同时也可以有可选的退出动作,在退出一个状态时被执行。进入动作和退出动作和状态联合,而不是和转换联合。无论一个状态被以什么方法进入或退出,所有它的进入和退出动作将被执行。因为这个特性,状态图表现的像Moore机。状态进入和退出的UML符号是在状态名字格子的下方,保留字 entry(或 exit )后面跟着斜杠和任意动作的列表(见下图 )。

进入和退出动作的价值是它们提供了可担保的初始化和清理方法,非常像OOP里类的构造函数和析构函数。例如,考虑下图的 door_open状态,它对应于烤面包炉的炉门被打开时的行为。这个状态有一个非常重要的保障安全的要求:当炉门在打开时总是关闭加热器。另外当炉门被打开,应该点亮内部照明灯。

当然,能够通过给每一个转换到door_open(用户也许在baking状态或toasting状态的任何时候,或当炉子完全没有被使用时)的路径增加合适的动作(关掉加热器和打开灯)来对这个行为建模。也不能忘记在每一个离开door_open状态的转换时熄灭内部炉灯。但这种解决方案会造成在很多转换里重复的动作。更重要的是,从改变状态机的角度来看,这种方法容易导致错误(例如,另一个程序员在处理一个新的特征比如“顶部烤棕色”时,也许忘记在转换到door_open时关掉加热器)。

图10 带有进入和退出动作的烤面包机的状态机

进入和退出动作允许用一种更安全,更简单和更直觉的方式来实现你所需要的行为。如上图所示,你能指定从heating的退出动作关掉加热器,到door_open的进入动作打开炉灯,从door_open的退出动作关掉炉灯。使用进入和退出动作比在转换上加入动作优越,因为它避免了在这些转换上的动作的重复,消除了当炉门打开时加热器保持工作的基本的安全性风险。退出动作的语义担保了无论什么转换路径,当面包炉不在heating状态时加热器将会被关闭。

因为每当一个状态被进入时,与之联合的进入动作被自动执行,它们常常决定操作的条件或状态的一致性,非常像一个类构造函数在决定被构建对象的特性。例如,heating状态的特性通过加热器被打开这个事实被确定。在进入heating的任何子状态前这个条件必须确立,因为到heating子状态的进入动作,比如toasting,依靠heating超状态的适当的初始化,并仅执行和这个初始化不同的部分。因此,进入动作的执行必须总是按从最外层状态到最里层状态的次序被处理。

毫不奇怪,这个次序模仿了当类构造函数被调用时的次序。一个类的构造总是从类继承的最根部开始,沿着全部的继承层次下降到要被实例化的类。退出动作的执行,对应于析构函数调用,按精确的相反的次序执行,从最里层的状态开始(对应于从最远的派生类开始)。

4.1.13 内部转换 (Internal Transistions)

比较常见的情况是,一个事件造成一些内部动作被执行但又不导致一个状态的改变(状态转换)。这种情况下,所有被执行的动作构成了内部转换。例如,当在键盘上打字时,它通过产生不同的字符码来响应。除非敲击CapsLock键,键盘的状态不会改变(没有状态转换发生)。在UML里,这种情况将被建模成内部转换,如下图所示。内部转换的UML符号使用一般的和退出(或进入)动作一样的语法,除了不是用单词entry(或exit)外,内部转换使用触发事件作为标签(如在下图里由ANY_KEY事件触发的内部转换)。

图11 带有内部转换的键盘状态机的UML状态图

当没有进入动作和退出动作时,内部转换和自转换一样(在自转换里,目标状态和源状态一致)。事实上,在一个经典的Mealy自动机里,动作只和状态转换联合,不改变状态而执行动作的唯一方法就是通过一个自转换。然而,在有进入动作和退出动作时,如在UML状态图里,一个自转换会导致进入和退出动作的执行,因此它和一个内部转换就完全不同了。

和自转换相反,在执行内部转换时不会执行进入和退出动作,即使内部转换是从一个超过当前活动状态较高层的层次继承的。从超状态继承的内部转换在任何的嵌套层都如同它们被直接在当前活动状态被定义一样执行。

5 QP框架概述

图12 QP框架组件及其与硬件和应用程序的关系框图

如上图所示,是QP的整个框架和组件。

  • 最底层的目标硬件是QP运行的载体,比如stm32f103目标板
  • 左侧的BSP是板级支持包
  • QS是QP的软件跟踪测试组件,用于代码跟踪、调试
  • QEP是层次式事件处理器,作为状态机的载体
  • QF是活动对象框架,主要实现了每个状态机的初始化和事件的分发功能
  • 内建的内核包括:QV(协作式非抢占内核)、QK(抢占式内核)、QXK(另一种抢占式内核)
  • QP除了可以跑在内建的内核(QV、QK等)上,还可以跑在其他传统的RTOS或Windows等操作系统上
  • 最上层的活动对象就是用户写的应用代码

如上所述,每个活动对象由一个状态机实现,多个状态机之间的通信与交互(事件派发)通过QF实时框架来实现。如下图

图13 直接事件发送和发行-订阅事件发送,同时存在同一个应用程序内

事件派发主要有2种方式:

直接事件发送

直接让生产者发送事件给接收者活动对象的事件队列。这个方法需要框架最低限度的参与。框架仅提供一个公共操作(框架API的一个函数),它运行任何生产者直接发送一个事件给指定的活动对象。例如,QF实时框架提供了操作QActive_postFIFO(),它是QActive类的操作。当然框架负责用一个线程安全的方式来实现这个函数。上图用粗线表示这个通讯的形式,实心箭头连接事件生产者和消费者活动对象。

直接事件发送是一个“推”式通讯机制,这里接收者接收没有请求的事件,不管它们需要或不需要这些事件。一群活动对象或一个活动对象和一个ISR,从一个子系统提供一个特定的服务,比如一个通讯堆栈,GPS功能,手机的数码相机子系统等等,在这些情况下,直接事件派发是很理想的。

这个事件传递方式需要事件生产者非常了解接收者。发送者需要知道的“知识”比仅有一个指向接收者活动对象的指针更多—发送者必须也要知道特定对象可能感兴趣的事件的类型。这种知识,散布在参与应用程序的组件中,使组件之间的耦合非常强烈和在运行时不灵活。例如,也许很难给子系统增加新的活动对象,因为现有事件生产者不会知道新对象的情况,并不会给它们发送事件。

发行 - 订阅

发行 - 订阅模型是把事件产生者和事件消费者解耦的一个流行方法。发现 - 订阅是一个“拉”式通讯机制,这里接收者仅接收被请求的事件。发现 - 订阅模型的特性如下:

  • 事件的产生者和消费者不需要互相了解对方(松耦合)。
  • 通过这个机制的事件交换必须被公开的了解,全部参与者必须有相同的语义。
  • 需要一个介质去接收所发行的事件,再把它们派发给感兴趣的订阅者。
  • 多对多交互作用(对象 - 到 - 对象)被一对多交互作用(对象 - 到 - 介质)所取代。

展示在上图的发行 - 订阅事件派发就像一个“软件总线”,活动对象通过特定的接口“插入”它。活动对象感兴趣的事件通过框架订阅一个或多个事件信号,事件产生者给框架产生事件发布请求。这些请求可从许多源头—例如从中断或设备驱动程序—异步发起,而不仅限于从活动对象。框架管理通过提供以下服务来管理所有这些交互作用:

  1. 为活动对象提供一个API来订阅和取消订阅特定的事件信号。例如QF实时框架提供函数QActive_subscribe(),QActive_unsubscribe(),和QActive_unsubscribeAll() 。
  2. 为发行事件提供一个通用的可存取接口。例如,QF提供了QF_publish()函数,以及
  3. 定义和实现一个线程安全的派发策略(包括当一个事件被多个活动对象订阅时的多路传输multicasting events)

发行 - 订阅的一个显然的暗示是,框架必须存储订阅者的信息,它必须允许把一个事件信号和多于一个订阅者联合起来。框架也必须允许在运行时修改订阅者信息(动态订阅和取消订阅)。QF实时框架支持动态订阅和取消订阅。

6 使用QP的开发方式

因QP的简洁性,即使是嵌套的状态机程序,也完全可以通过文本编辑器进行编写,在早期,QM未出现之前,QP是推荐直接通过代码进行架构和编写程序的。之后,有了QM,可以将其作为第一选择。

QM(QP Modeler)是一个免费的基于模型的设计(MBD)和自动代码生成工具,用于设计基于现代有限状态机(UML状态图)和Q实时嵌入式框架的软件。它的优点如下:

  • 与市场上大多数其他建模工具相比,QM更加简单,且以代码为中心。与其他建模工具相比,可以以20%的复杂性获得80%的好处。
  • QM包含许多调整,使设计现代分层状态机比其他类似工具更容易。
  • QM生成高效,高质量的misra兼容的C或c++代码。

部分QM的页面截图如下

  • 在QM中使用分层状态机

    图14 在QM中使用分层状态机

  • 在QM中生成代码

    图15 在QM中生成代码

  • 设计一个子状态机捕获QM中的常见行为

    图16 设计一个子状态机捕获QM中的常见行为

7 写在最后

上面只是比较泛泛的描述了QP相关的简单内容,想要将其应用到自己的开发、设计中,还有很多的路需要走,《嵌入式系统的事件驱动型编程技术 第二版》是官方出版的关于QP的详细描述,尽管此书已经问世很多年,与最新版本的QP实现有了不同,但其还是入门QP的最佳选择。它的主要内容如下:

  1. 它从一个飞行射击游戏出发,介绍了使用QP将其实现的设计方法和步骤。
  2. 介绍UML状态机相关的内容。
  3. 以定时炸弹为例,它由嵌套的Switch语句、状态表、面向对象的状态设计模式等传统的状态设计模式实现出发,详细描述了其优缺点,然后推出QP的QEP FSM实现方法(在最新的QP实现中,不再提供此组件,其已融合进QEP的HSM)。
  4. 描述了QEP的HSM(嵌套状态机)实现方法。
  5. 介绍了包括终极钩子、提示器、延迟事件、正交构件、转换到历史状态等状态模式。
  6. 介绍了实时框架的概念
  7. QF实时框架的实现和协作式内核QV的实现
  8. QF实时框架的移植和配置
  9. 可抢占式内核QK的介绍
  10. 软件跟踪QS套件的介绍
  11. 最后是QP-nano的介绍
  • 16
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值