赞
踩
QP 状态机
传统有限状态机 FSM
二维状态表
一维状态表
QP 实时嵌入式框架优点
QP™/C Real-Time Embedded Framework (RTEF) 是专门为 real-time embedded (RTE) 系统量身定制的 Active Object model of computation 的轻量级实现。QP 既是用于构建由 active objects(actors) 组成的应用程序的软件基础设施,也是用以确定方式执行 active objects 的运行时环境。另外,QP 支持 Hierarchical State Machines(分层状态机)用它来指定 Active Objects 的行为。你可以将 QP/C RTEF 认为是一个现代的,真正的事件驱动的实时操作系统。
什么叫主动对象计算模型?
什么是分层状态机?
QP/C RTEF 的主要目标是:
提供一个现代的、事件驱动的并发模型,该模型基于并发编程的最佳实践,被称为计算的主动对象(Actor)模型,它本质上比传统的基于传统实时操作系统(RTOS)的 “共享状态的并发、互斥和阻塞” 方法更安全;
提供一种高效的、双向可追踪的层次状态机实现,用于指定活动对象的内部行为;
提供比 “裸” 的 RTOS 线程更接近问题域的更高层次的抽象;
为应用现代技术 (如可视化建模、分层状态机和代码自动生成) 提供正确的抽象;
在高层建模概念 (如UML) 和传统编程语言 (如 C 或 c++) 之间架起语义的桥梁。
QP™/C实时嵌入式框架(RTEF)提供了一个现代的、可重用的嵌入式应用程序体系结构,它结合了称为活动对象(actor)的并发模型和分层状态机。这种架构通常比传统的实时操作系统(RTOS)的“自由线程”更安全、响应更快、更容易理解。它还提供了足够高的抽象级别和正确的抽象,以有效地将建模和代码生成应用于深度嵌入式系统。
活动对象的行为在QP™/C中通过分层状态机(UML statecharts)来指定。该框架支持用C语言对UML状态机进行手工编码,并通过免费的图形化QM™基于模型的设计(MBD)工具实现完全的代码自动生成。
使用 QP 框架构建的系统:
在顶部,您可以看到QP应用程序,它由多个事件驱动的活动对象组成。
QP应用程序由支持活动对象、状态机和其他服务的QP框架(以黄色显示)执行。
QP框架可以利用其中一个内置的实时内核“独立”运行
另外,QP框架也可以运行在第三方RTOS内核或通用操作系统(如Linux/POSIX或Windows)之上。
底部和侧面是目标系统和板级支持包(BSP),由硬件和基础软件(如设备驱动程序)组成。
**事件驱动模式:**QP框架的本质特征是事件驱动。这意味着在QP中,任何处理都是由事件决定的,这些事件在没有阻塞(或忙轮询)的情况下尽可能快地处理。
**控制反转:**与大多数事件驱动系统一样,QP框架基于控制反转,即代码执行的控制权在QP框架中,而不是在基于QP的QP应用程序中。具体来说,为了处理事件,QP框架调用QP应用程序,而不是反过来。当然,QP应用程序也可以调用QP框架提供的服务,但主要的控制流程总是从QP框架开始。
**主动对象:**QP 应用程序由活动对象(actor)组成,它们是严格封装的软件对象,在它们自己的执行线程中运行,并通过交换事件进行异步通信。(状态机+事件队列+控制的线程)
**状态机:**QP中的每个活动对象都有一个状态机,用来指定活动对象的事件驱动行为。QP应用程序开发人员的主要工作是细化主动对象和其他(被动)事件驱动组件的内部状态机。
事件实例(又称消息)是专门为事件驱动系统(如QP应用程序)中的通信和同步而设计的对象。一旦生成,事件实例(也称为消息)就会传播到系统,在那里它可以触发各种操作。这些操作可能涉及生成次要事件实例。
本节从在状态机中使用事件的角度介绍事件。活动对象部分将讨论其他概念(如可变事件和不可变事件)和需求,其中包括QP框架中的事件管理和队列。
**事件信号:**事件总是有关于产生事件实例(“what happened”)的发生类型的信息,在本文档中将其称为Signal。事件信号通常是枚举常量,表示在所有可能(枚举)事件集合中,哪个特定事件生成了Event实例。
事件信号携带有关产生给定事件实例的事件的信息。QP应用程序需要容易地访问信号,以确定如何处理给定的事件。
**事件参数:**事件可以有(可选的)相关参数,允许事件实例传递有关该发生的定量信息。例如,通过按下计算机键盘上的一个键而产生的击键事件可能有相关的参数,其中包含字符扫描码以及Shift、Ctrl和Alt键的状态。
事件驱动系统通过响应事件来工作。一般来说,系统对给定事件的响应取决于该事件的性质(在其信号中捕获)和系统接收到的事件历史。
在实践中,并非所有“过去事件的历史”都是相关的。仅由系统对未来事件的响应所产生的方面组成的简化历史称为相关历史。
**状态:**是一个系统过去历史的等价类,所有的状态都是等价的因为给定任何一个过去历史系统的未来行为都是相同的。因此,“状态”的概念是系统相关历史的最有效表示。它是仅捕获与未来行为相关方面并抽象出所有不相关方面的最小信息。
**转换:**是在系统的生命周期内从一种状态转换到另一种状态的过程。在事件驱动系统中,状态的变化只能由一个事件引起。触发转换的事件称为触发事件或转换触发。
**状态机:**是所有状态(相关历史的等价类)加上所有状态转换(状态变化规则)的集合。状态机形式化的一个重要好处是状态机可以以状态图的形式进行图形化表示。
层次状态机:(UML statechart)是对传统状态机的一种高级形式扩充。UML状态机相对于经典状态机最重要的创新是引入了层次嵌套状态。状态嵌套的价值在于避免了在传统的"扁平"状态机形式中不可避免的重复。状态嵌套的语义允许子状态只定义与超状态的行为差异,从而促进了行为的共享和重用。
**状态机实现策略:**状态机,特别是分层状态机,可以以许多不同的方式实现。实现状态机的一种特定方式在这里称为状态机实现策略,它可以由以下属性表征:
时间效率(CPU周期)
数据空间的效率(内存占用)
代码空间效率(ROM占用)
单体式与不同粒度级别的分区式
可维护性(手动编码)
可维护性(通过代码自动生成)
从设计(例如,状态图)到代码的可追溯性
从代码到设计的可追溯性
其他,质量属性(非功能需求)
没有哪一种状态机实现策略可以对所有情况都是最优的,因此QP框架应该支持多种可互换的策略(参见REQ-QP-02_20)。
状态机内部的事件处理称为将事件分发给状态机,它需要QP框架和QP应用程序之间的交互:
**“状态机规范”**在QP应用程序内部提供,并根据QP框架中选择的状态机实现策略定义的规则进行准备。通常,实现策略将状态机表示为多个元素,如状态、转换等。
“状态机规范”可以指状态机代码(当状态机是手动编码时)或状态机模型(当状态机在建模工具中指定时,如“QM”)。无论哪种方式,强烈建议将状态机实现视为状态机元素的规范,而不仅仅是代码。这种“指定”状态机的概念,而不是对其进行编码,可以通过选择一种具有表现力的、完全可跟踪的状态机实现策略来加强,请参见REQ-QP-02_40。可跟踪实现的优点是,所有抽象级别(从设计到编码)中的每个工件都明确地表示状态机的一个元素。
**状态机执行:**状态机在QP框架中由“状态机处理器”执行,该处理器决定调用“状态机规范”的哪些元素。一旦被调用,“状态机规范”所选择的部分就会执行一些操作,并返回到“状态机处理器”(QP框架),其中包含关于发生了什么事情的状态信息。例如,返回的状态可能通知“状态机处理器”需要进行状态转换,或者需要将事件传播到层次状态机中的超状态。
运行到完成(RTC)处理:“状态机处理器”是一个被动软件组件,需要从某些控制线程显式调用它,以便将每个事件分发给给定的状态机对象。最重要的限制是,在将另一个事件分派到同一状态机对象之前,分派操作必须运行到完成(运行到完成处理)。
RTC事件处理意味着,状态机不应该阻塞或忙轮询事件(例如信号量-wait或忙-delay),因为每个这样的阻塞或忙轮询调用都表示在等待一个事件,该事件将在调用解除阻塞后立即交付。问题是这样的“后门”事件在原始RTC步骤完成之前交付,因此违反了RTC语义。状态机内部的阻塞也扩展了RTC处理,并使状态机对新事件无响应。
下图显示了由多个协作的活动对象组成的QP应用程序,这些活动对象共同交付所需的功能:
每个活动对象都有自己的事件队列,并专门通过该队列接收所有事件。另一方面,事件不仅可以由活动对象产生,还可以由中断(isr)或其他软件组件产生。活动对象基础设施,如本例中的QP框架,负责以确定性和线程安全的方式交付和排队事件。
**封装:**也许活动对象最重要的特性(活动对象的名称正是由此而来)是它们的严格封装。封装意味着活动对象不共享数据或任何其他资源。图03_01通过一个围绕每个活动对象的厚的、不透明的封装外壳说明了这一点,并以灰色显示内部状态机,因为它们实际上不应该从外部可见。
**异步交流:**所有事件都是异步传递给活动对象的,这意味着事件生产者只是将事件发送到接收活动对象的事件队列,而不排队等待事件的实际处理。
QP框架不区分由中断产生的外部事件和由活动对象产生的内部事件。如图03_01所示,活动对象可以向任何其他活动对象(包括self)提交事件。所有事件都会被统一对待,不管它们的来源是什么。
**运行到完成(RTC):**每个活动对象都以运行完成(run-to-completion, RTC)的方式处理事件,这必须由底层的QP框架保证。RTC意味着活动对象一次处理一个事件,只有在前一个事件处理完毕后才能处理下一个事件。RTC事件处理是状态机正确执行的基本要求。
RTC事件处理并不意味着活动对象独占CPU,直到RTC步骤完成。在多线程环境中,其他线程或中断(与繁忙的活动对象的线程上下文无关)可能正在运行,可能抢占当前正在执行的活动对象。但是在每次抢占之后,被抢占的活动对象从它停止的地方继续处理当前事件,最终完成它的RTC步骤。
**非阻塞:**传统的操作系统大多采用阻塞的方式来管理线程和线程间的通信,如等待时间延迟或等待信号量。然而,阻塞(如RTC步骤的中间部分)与RTC事件处理需求不兼容。这是因为每个阻塞调用实际上都是传递事件的另一种方式(通过解除阻塞并从阻塞调用中返回事件来传递事件)。这种发生在RTC步骤中间的“后门”事件传递违背了RTC的语义,因为在解除阻塞后,活动对象需要同时处理两个事件(原始事件和解除阻塞后的新事件)。
在RTC步骤中阻塞(或轮询事件)的另一个有害后果是,活动对象对发送到其事件队列的事件失去响应。这反过来会导致活动对象错过它们的硬实时截止日期,还会导致事件队列溢出。
最后,阻塞(或轮询事件)意味着预期的事件序列是硬编码的,这在本质上是不灵活和不可扩展的,特别是在需要添加新事件或系统必须处理多个事件序列的情况下。
**控制线程:**在UML中,活动对象被定义为:“具有自己的控制线程的对象”[UML 2.5]。如果QP框架运行在传统的多线程内核(例如,传统的RTOS或通用操作系统)上,情况可能确实如此。但是,正确的活动对象执行实际上只需要在每个RTC (Run-to-Completion)步骤期间提供一个线程。仅仅等待事件的活动对象根本不需要线程。这为在不支持传统线程概念的调度器中使用活动对象提供了可能性。在这种情况下,活动对象仅在需要时才能协作获得对CPU的访问。
明确区分RTC的概念和抢占的概念是非常重要的[OMG 07]。特别是,RTC并不意味着活动对象线程必须独占CPU,直到RTC步骤完成。事实上,RTC步骤可能会被中断或在同一CPU上执行的其他线程抢占。这种线程抢占是由底层多任务内核的调度策略决定的,而不是由计算的活动对象模型决定的。当被抢占的活动对象再次被分配CPU时间时,它将从抢占点恢复事件处理,并最终完成其RTC步骤。只要抢占线程和被抢占线程不共享任何资源(请参阅封装),就没有并发风险。
**主动对象优先级:**在实时应用中,通过为线程分配优先级来确定工作的优先级是非常有用的。QP框架更进一步,要求每个活动对象都有一个唯一的优先级。活动对象优先级随后用作活动对象的紧凑、唯一标识符。
QP框架的主要职责之一是高效、安全地将事件从不同的生产者传递到活动对象。系统的任何部分都可以产生事件,而不一定是活动对象。例如,isr、设备驱动程序或在框架之外运行的遗留代码可以产生事件。另一方面,只有活动对象可以使用事件,因为只有活动对象才有事件队列。
**直接事件分发:**最简单的机制允许生产者直接将事件发送到接收者活动对象的事件队列。图04_01展示了这种通信形式,用粗而实的箭头连接事件生产者和消费者活动对象。
直接事件发布是一种“推式”的通信机制,在这种机制中,无论接收者是否愿意,他们都会收到未经请求的事件。直接事件发帖是理想的情况下,一组活动对象,或一个活动对象和ISR,形成一个子系统提供特定的服务,如通信堆栈,GPS能力,移动电话中的数字相机子系统,或类似。这种事件传递风格要求事件生产者“知道”接收者以及他们对各种事件的兴趣。发送方需要的“知识”至少是指向接收方活动对象的句柄(例如,指针)。
直接事件发布机制可能会增加组件之间的耦合,特别是当事件的接收方硬编码在发送方内部时。然而,减少耦合的一种方法是允许接收方在运行时向生成方注册,这样事件生成方就不需要对接收方进行硬编码。
发布订阅:
发布-订阅模型是一种解耦事件生产者和事件消费者的流行方法。发布-订阅是一种“拉式”通信机制,接收者只接收请求的事件。发布-订阅模型的属性有:
事件的生产者和消费者不需要知道对方(松耦合)。
通过该机制交换的事件必须是公开的,并且对所有参与方都必须具有相同的语义。
需要mediator7来接受已发布的事件并将其传递给感兴趣的订阅者。
多对多交互(对象到对象)被一对多交互(对象到中介)取代。
发布-订阅事件交付在图04_01中显示为一个“软件总线”,活动对象通过指定的接口“插入”到该总线中。对某些事件感兴趣的活动对象通过QP框架订阅一个或多个grs_evt_sig“事件信号”。
事件生产者向框架发出事件发布请求。这样的请求可以从多个源异步发起,而不一定只来自活动对象。例如,事件可以通过中断或设备驱动程序发布。QP框架通过提供以下服务来管理所有这些交互:
为活动对象提供一种订阅和退订特定grs_evt_sig "事件信号"的方法;
为发布事件提供一般可访问的接口。
定义并实现线程安全的事件传递策略(包括多个活动对象订阅事件时的多播事件)。
发布-订阅的一个明显含义是框架必须存储订阅者信息,而它必须允许将多个订阅者活动对象与grs_evt_sig“事件信号”关联。框架还必须允许在运行时修改订阅者信息(动态订阅和退订)。
**事件内存管理:**在任何事件驱动的系统中,事件的产生和消费都是频繁的,因此它们本质上是高度动态的。每个实时框架最关键的方面之一是管理事件使用的内存,因为随着不断产生新的事件,显然必须经常重用这些内存。该框架的主要挑战是确保在所有活动对象完成事件的RTC处理之前,事件内存不会被重用。事实上,如运行到完成处理中所述,在当前事件仍在使用时破坏它就构成了违反RTC语义的行为,这是最难解决的bug之一。
**不可改变的事件:**并非系统中的所有事件实例都有参数或正在变化的参数。例如,某些事件实例可以是常量,其信号和参数是固定的。这种不可变的事件对象可以在任意数量的并发线程和中断之间安全地共享,并且可以静态地分配一次,而不是每次都创建和回收。QP框架应该支持不可变事件,以优化和简化事件内存管理。
**可变的事件:**许多事件实例,特别是带有参数的事件,不能轻易地使其不可变。例如,只有一个16位参数的事件可能有64K个值,创建这么多不可变事件(可能是在数组中)是相当不切实际的。对于这种情况,QP框架需要支持可变事件。
**零拷贝事件:**将整个事件复制到消息队列中是传统RTOS所能做到的最好的方式,因为RTOS在事件离开队列后不进行控制。另一方面,实时框架可以更加高效,因为由于控制倒置,框架实际上管理了事件的整个生命周期。如图6 - 5(B)、6.8和6.9(A)所示,一个实时框架负责从活动对象的事件队列中提取事件,然后将事件分发给RTC处理。在RTC步骤完成后,框架重新获得对事件的控制。在这一点上,框架“知道”事件已经被处理,因此框架可以自动回收事件。图6 - 12展示了添加到活动对象生命周期中的垃圾收集步骤(事件回收)。
实时框架还可以轻松地控制事件的分配。框架可以简单地提供一个API函数,应用程序代码必须调用该函数来分配新的事件。例如,QF框架为此提供了宏Q_NEW()。通过添加事件创建和自动垃圾收集步骤,该框架可以控制事件从摇篮到坟墓的生命周期。这又允许框架实现受控的、线程安全的事件内存共享,从应用程序的角度来看,这与真正的事件复制是不可区分的。这种内存管理称为零拷贝事件传递(zero-copy event delivery)。
传统RTOS中可用的时间管理包括延迟调用任务(delay())或对各种内核对象(例如信号量或事件标志)的定时阻塞。这些阻塞机制在不允许阻塞的基于对象的主动系统中用处不大。相反,为了与计算的主动对象模型兼容,时间管理必须基于事件驱动范式,在这种范式中,每个有趣的事件都表现为一个事件实例。
**时间事件:**实时框架通过时间事件(time Events,有时称为定时器)来管理时间。时间事件是一个UML术语,表示一个时间点。在指定的时间,事件发生[UML 2.5]。这些时间事件的基本使用模型如下:活动对象分配一个或多个时间事件对象(提供它们的存储空间)。当活动对象需要安排一个超时时间时,它会设置一个time事件,以便在未来的某个时间发布自己。
**系统滴答时钟:**每个实时系统,包括传统的阻塞内核,都需要一个称为系统时钟的周期性时钟源。系统时钟周期通常是一个周期性中断,以预定的速率发生,通常在10Hz和100Hz之间。您可以将系统时钟的滴答看作系统的“心跳”。系统时钟时钟的实际频率取决于应用程序所需的时钟分辨率。时标速率越快,时间管理的开销就越大。系统时钟周期必须调用QP框架中的一个特殊函数,使框架有机会定期更新时间事件。
上图以有点夸张的方式给出了以一个时标间隔编程的周期性时间事件的各种延迟。从不同的时间间隔可以看出,time事件的传递总是会有抖动。当接收活动对象的优先级降低时,抖动会变得更糟。在高负载系统中,抖动甚至可能超过一个时钟周期。特别地,只有一个时钟周期的时间事件可能立即过期,因为系统时钟周期相对于活动的对象执行来说是异步的。为保证至少有一个时标超时,需要为两个时标设置一个时间事件。还要注意,时间事件通常不会因为事件排队而丢失。这与传统RTOS的时钟周期不同,后者在重负载期间可能会丢失。
由活动对象构建的运行中的应用程序是一个高度结构化的事务,其中所有重要的系统交互都通过实时框架和事件处理器执行状态机。这种安排为应用软件跟踪技术提供了一个独特的机会。简而言之,软件跟踪类似于在代码中添加printf()语句(称为代码插装),以记录有趣的离散事件,以便随后从目标系统中检索和分析。当然,一个好的软件跟踪工具可以比原始的printf()更少干扰,而且功能更强大。
通过检测实时框架代码,您可以获得关于运行系统的空前丰富的信息,比任何传统的RTOS都要详细和全面得多。(当然,这是控制反转的另一个好处。)来自框架的软件跟踪数据允许您为系统中的所有活动对象生成完整的、带有时间戳的序列图和详细的状态机活动。这种能力可以构成应用程序的整个测试策略的基础。此外,单个活动对象是单元测试的自然实体,您可以简单地通过向活动对象注入事件并收集跟踪数据来执行单元测试。框架级别的软件跟踪使您可以获得所有这些全面的信息,即使没有向应用程序级别的代码添加任何工具。
QV是一个简单的协作内核(以前称为“Vanilla”内核)。该内核每次执行一个活动对象,在处理每个事件之前执行基于优先级的调度。由于状态机中事件处理的持续时间很短,简单的QV内核通常足以满足许多实时系统的需求。
在活动对象的每一个RTC步骤之后,QV调度器都会被调用,以选择要执行的下一个活动对象。QV调度器总是选择在其事件队列中有任何事件的优先级最高的活动对象。然后,QV调度器从该队列中提取下一个事件,并将其分发给与活动对象相关联的状态机。状态机运行到完成,然后QV调度器运行并重复该循环。
请注意,因为状态机总是在每个RTC步骤之后返回到QV调度器,所以可以使用一个栈来处理所有状态机(内存友好的架构)。
当所有事件队列都为空时,QV调度器也可以非常容易地检测到,此时它可以调用idle回调,让应用程序将CPU和外设设置为低功耗睡眠模式(功耗友好的架构)。
由于简单性、可移植性和低资源消耗,QV调度器非常有吸引力。它允许您将问题划分为活动对象,并有序地执行这些活动对象。这个调度器的线程级响应是整个系统中最长的RTC步骤,但是因为事件驱动的活动对象不会阻塞,所以RTC步骤往往非常短(通常只有几微秒)。此外,通常你可以通过向self发送一个事件并返回(“提醒”状态模式),将较长的RTC步骤分解为较短的部分。然后,self-post事件触发持续较长的处理。
有时,分解较长的RTC步骤是不现实的,因此简单QV内核的线程级响应可能太慢。在这种情况下,用户需要使用抢占式内核。抢占式内核的最大优点是在时域上有效地解耦了高优先级线程和低优先级线程。高优先级线程执行的时效性几乎与低优先级线程无关。当然,天下没有免费的午餐。抢占式内核引发了与竞态条件相关的一类全新问题。所以你需要非常小心地分享任何资源。
下面的包图显示了使用QP框架的主要组件和上下文。该体系结构是分层的,上层的QP应用程序使用中间的QP框架的服务,后者又使用底部的内核/实时操作系统的服务。
QP应用程序
顶部是QP应用程序,它由活动对象和事件组成。应用程序不是QP的一部分,而是由QP框架派生而来,QP框架提供了应用程序中使用的基类和API。
QP框架
应用程序正下方的QP框架提供了QP应用程序特化的基类和api。QP框架还提供了在嵌入式目标上执行QP应用程序的运行时环境。
QP提供了QActive抽象基类,用于在QP应用程序中特化为具体的活动对象。QActive抽象基类继承自QHsm基类,后者提供状态机实现。QP还提供了QEvt基类,用于派生带有参数的事件。
Kernel/RTOS/GPOS
底部的“Kernel/RTOS”包执行活动对象。由于这可以通过几种不同的方式完成,QP规范以内置内核、传统的RTOS和通用操作系统环境的形式提供了几种选项。
内置内核允许QP框架在没有任何第三方RTOS的情况下独立运行。QP还可以配置(在编译时),以便与许多传统的rtos和通用操作系统(如Windows和Linux)一起工作。
面向对象
QP框架基本上是面向对象的,这意味着框架本身和QP应用程序基本上是由类组成的。因此,该体系结构设计描述采用面向对象的观点,利用类、封装、继承和多态性的概念。
面向对象的观点并不强制选择面向对象的编程语言。在传统的过程语言(如C)中,面向对象的概念可以应用于设计模式。在AppNote“C中的面向对象编程”中描述了C编程语言的一组这样的模式。
导出QP应用
从框架派生应用程序的主要机制是继承框架提供的基类并为手头的应用程序特化它们。
下面的类图显示了组成QP框架的主要类及其与应用程序级代码的关系,例如Flyn 'n’射击游戏示例应用程序(在图的底部1/3处)。
在QP框架中,每个活动对象的行为是通过层次状态机(UML statechart)来描述的,它是描述事件驱动行为最有效、最优雅的技术。UML状态机相对于经典的有限状态机(fsm)最重要的创新是分层状态嵌套。状态嵌套的价值在于避免了重复,而重复在传统的“扁平”状态机模型中是不可避免的,也是导致状态转移爆炸的主要原因。状态嵌套的语义允许子状态只定义与超状态的行为差异,从而促进了行为的共享和重用。
Quantum leap应用程序注意UML状态机的速成课程介绍了通过示例支持的主要状态机概念。
UML状态机的QP框架实现的标志是可追溯性,它是将每个状态机元素直接、精确和明确地映射到人类可读、可移植的代码。对于关键任务系统,如医疗设备或航空电子系统,保持从需求到设计到代码的可追溯性是至关重要的。
本节描述如何使用QP™/C实时嵌入式框架实现分层状态机,这是一个相当机械的过程,只包含一些简单的规则。(事实上,QP™/C中编码状态机的过程已经由基于QM模型的设计和代码生成工具自动化了。)
为了集中讨论,本节使用位于目录qpcpp/examples/workstation/calc中的计算器示例。psic2书(第4.6节“用QEP实现HSMs的步骤总结”)使用了这个例子。
本节解释如何对层次状态机的下列(标记)元素进行编码:
[1]
The top-most initial pseudo-state
[2]
A state (nested in the implicit top
state)
[3]
An entry action to a state
[4]
An exit action from a state
[5]
An initial transition nested in a state
[6]
A regular state transition
[7]
A state (substate) nested in another state (superstate)
[8]
Even more deeply nested substate
[9]
A choice point with a guard
[1]最初始的伪状态
[2]一个状态(嵌套在隐式的顶层状态中)
[3]进入状态的动作
[4]状态的退出动作
[5]嵌套在某个状态中的初始转换
[6]常规状态转换
[7]一个嵌套在另一个状态(超状态)中的状态(子状态)
[8]更深层嵌套的子状态
[9]一个带守卫的选择点
本节描述QHsm状态机实现策略,适用于QP™/C中分层状态机的手动编码。另一种QP™/C也支持的QMsm状态机实现策略不在本节中讨论,因为代码需要由QM建模工具自动生成。
状态机声明:
层次状态机在QP™/C中表示为QHsm抽象基类的子类,该类定义在头文件qpc\include\qep.h中。请注意,像QMsm、QActive和QMActive这样的抽象类也是QHsm的子类,因此它们的子类也可以有状态机。
状态机定义:
状态机类的定义是状态机的实际代码。您需要定义(即编写代码)在状态机类声明中声明的所有“state-handler”成员函数。
编写“状态处理程序”函数的一个重要方面是,它们总是在分派事件的过程中被调用。“状态处理器”的目的是执行特定的操作,然后告诉事件处理器需要用状态机做什么。例如,如果您的“state-handler”执行一个状态转换,它会执行一些操作,然后它调用特殊的QHsm_tran_()函数,其中它指定此状态转换的状态。然后,state-handler从tran()函数返回状态,并通过这个返回值通知dispatch操作需要对状态机执行什么操作。基于这些信息,事件处理器可能决定调用这个或其他状态处理程序函数来处理同一个当前事件。下面的代码示例应该能更清楚地说明这一点。
QP/C框架可以很容易地适应各种操作系统、处理器体系结构和编译器。对QP/C软件的移植称为移植,而QP/C框架的设计是为了使移植更加容易。
QP/C分布包含许多QP/C端口,这些端口分为以下三类。
PC-Lint-Plus(通用C编译器)QP/C“端口”到PC-Lint Plus静态分析工具(一个“编译器”)
本机端口使用其中一个内置内核(QV、QK或QXK),使QP/C“本机”运行在裸金属处理器上。
第三方RTOS端口采用QP/C运行在第三方实时操作系统(RTOS)上。
第三方操作系统移植采用QP/C运行在第三方操作系统(OS)上,如Windows或Linux。
[1]本机端口位于以CPU架构命名的子目录中,例如ARM -cm表示ARM Cortex-M。在该目录下,子目录qk和qv分别包含用于qk和qv内核的端口。
用于第三方RTOS的[2]端口位于以RTOS命名的子目录中,如uc-os2用于uc-os2 RTOS。在该目录下,子目录(如ARM -cm)包含了指定CPU体系结构的示例,如这里的ARM Cortex-M。
第三方操作系统的[3]端口位于以操作系统命名的子目录下,如win32 API (Windows操作系统)为“win32”。(注意:桌面操作系统的构建,如Windows或Linux,包含用于调试、发布和Spy构建配置的预构建QP库)。
初始化 QF 框架所需的和底层的RT内核
初始化 QF 框架所需的:时间事件、主动对象队列、就绪表初始化、中断初始化。
底层的RT内核:中断初始化。
时间事件是具有时间流逝概念的特殊 QF 事件。时间事件的基本使用模型如下:
主动对象分配一个或多个 QTimeEvt 对象(为它们提供存储空间)。
当主动对象需要安排一个超时时间时,它会触发其中一个time事件,使其只触发一次(one-shot)或周期性地触发。
每次事件超时都独立于其他事件,因此 QF 应用程序可以发出多个并行超时请求(来自相同或不同的主动对象)。
当 QF 检测到适当的时刻已经到达时,它直接将时间事件插入到接收者的事件队列中。然后,接收方像处理其他事件一样处理 time 事件。
时间事件,与任何其他 QF 事件一样,都派生自 QEvt 基类。一般情况下,用户将按原样使用时间事件,但也可以通过添加更多的数据成员和/或操作特定时间事件的特定函数,进一步从中派生出更特定的时间事件。
在内部,武装时间事件被组织成链表,每个链表对应一个支持的滴答率。每次调用 QTIMEEVT_TICK_X()
宏时都会扫描这些链表。列表中只有武装(超时)时间事件,因此只有武装时间事件会消耗CPU周期。
主动对象是封装任务(每个任务包含一个事件队列和一个状态机),通过发送和接收事件相互异步通信。在活动对象中,事件以 run-to-completion (RTC) 方式处理,而QF封装了线程安全的事件交换和排队的所有细节。
QActive 表示一个使用 QHsm-style 的状态机实现策略的活动对象。该策略是为手工编码量身定制的,但也得到了 QM 建模工具的支持。产生的代码比 QHsm-style 的实现策略要慢。
QPSet表示按优先级顺序排列的活动对象集合。该集合能够存储多达QF_MAX_ACTIVE成员。
将异常优先级和IRQ优先级初始化为安全值。
在ARMv7-M或更高版本上,该QK端口通过BASEPRI寄存器禁用中断。但是,该方法不能禁用中断优先级为零,这是reset之外所有中断的默认优先级。下面的代码将系统优先级和所有IRQ优先级更改为安全值QF_BASEPRI, QF临界区可以禁用该值。这避免了在应用程序程序员忘记显式设置所有“内核感知”中断的优先级的情况下破坏QF临界区。
板子资源初始化:时钟、GPIO 等。
主动对象和时间事件的初始化。
主动对象的注册、事件队列的初始化、状态机的初始化。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。