image.png

  1. #include "qpc.h" // qpc框架头文件
  2. #include "evt_def.h" // 事件定义头文件
  3. #include "bsp.h" // 硬件初始化
  4. #include "ao_led.h" // LED状态机
  5. Q_DEFINE_THIS_MODULE("Main") // 定义当前的模块名称,此名称在QS和断言中会使用。
  6. ao_led_t led; // 状态机LED对象
  7. int main(void)
  8. {
  9. static QSubscrList sub_sto[MAX_PUB_SIG]; // 定义订阅缓冲区
  10. static QF_MPOOL_EL(m_evt_t) sml_pool_sto[128]; // 定义事件池
  11. QF_init(); // 状态机框架初始化
  12. QF_psInit(sub_sto, Q_DIM(sub_sto)); // 发布-订阅缓冲区的初始化
  13. QF_poolInit(sml_pool_sto, // 事件池的初始化
  14. sizeof(sml_pool_sto),
  15. sizeof(sml_pool_sto[0]));
  16. ao_led_ctor(&led); // 状态机的构建
  17. return QF_run(); // 框架启动
  18. }

image.png

void QF_onStartup(void) {
    bsp_init();                                         // 硬件初始化
}
void QF_onCleanup(void) {}
void QV_onIdle(void) {}
void Q_onAssert(char_t const * const module, int_t const loc)
{
    (void)module;
    (void)loc;
    while (1);
}

image.png

int func_add(int x, int y)
{
    Q_ASSERT(x < 100);
    Q_ASSERT(y < 100);

    return (x + y);
}

image.png

#include "bsp.h"
#include "stm32f10x.h"
#include "qpc.h"

void bsp_init(void)
{
    SysTick_Config(SystemCoreClock / 1000);         // 时间基准为1ms
    NVIC_SetPriority(SysTick_IRQn, 0);              // 设置中断优先级
}

void SysTick_Handler(void)
{
    QF_TICK_X(0U, &l_SysTick_Handler);              // 时间基准
}

image.png

#include "qpc.h"

#define AO_LED_QUEUE_LENGTH                 32

// LED类的定义
typedef struct ao_led_tag{
    QActive super;                                      // 对QActive类的继承

    QEvt const *evt_queue[AO_LED_QUEUE_LENGTH];         // 事件队列
    QTimeEvt timeEvt;                                   // 延时事件

    bool status;                                        // LED状态
} ao_led_t;

// LED的类方法 构造函数
void ao_led_ctor(ao_led_t * const me);

image.png
image.png

// 活动对象(AO,Active Object)LED的构建
void ao_led_ctor(ao_led_t * const me)
{
    // LED对象的变量初始化
    me->status = false;

    // 活动对象的构建
    QActive_ctor(&me->super, Q_STATE_CAST(&state_init));
    // 时间对象的构建
    QTimeEvt_ctorX(&me->timeEvt, &me->super, Evt_Time_500ms, 0U);
    // 活动对象的启动
    QACTIVE_START(  &me->super,
                    1,                              // 优先级
                    me->evt_queue,                  // 事件队列
                    AO_LED_QUEUE_LENGTH,            // 事件队列深度
                    (void *)0,                      // 任务栈,RTOS相关,可忽略
                    0U,                             // 任务栈深度,RTOS相关,可忽略
                    (QEvt *)0);
}

image.png

// 初始状态
static QState state_init(ao_led_t * const me, void const * const par)
{
    // 事件Evt_Time_500ms的订阅
    QActive_subscribe(&me->super, Evt_Time_500ms);

    return Q_TRAN(&state_on);
}

image.png
image.png

// LED的on状态
static QState state_on(ao_led_t * const me, QEvt const * const e)
{
    switch (e->sig) {
        case Q_ENTRY_SIG:                           // 状态的进入事件
            me->status = true;                      // 打开LED灯
            QTimeEvt_armX(&me->timeEvt, 500, 0U);   // 500ms后发送时间事件
            return Q_HANDLED();                     // 通知框架,事件已处理

        case Q_EXIT_SIG:                            // 状态的退出事件
            QTimeEvt_disarm(&me->timeEvt);
            return Q_HANDLED();

        case Evt_Time_500ms:
            return Q_TRAN(&state_off);              // 通知框架,状态转移至state_off

        default:
            return Q_SUPER(&QHsm_top);              // 其他事件,在此时不处理
    }
}

// LED的Off状态
static QState state_off(ao_led_t * const me, QEvt const * const e)
{
    switch (e->sig) {
        case Q_ENTRY_SIG:
            me->status = false;                     // 关闭LED灯
            QTimeEvt_armX(&me->timeEvt, 500, 0U);
            return Q_HANDLED();

        case Q_EXIT_SIG:
            QTimeEvt_disarm(&me->timeEvt);
            return Q_HANDLED();

        case Evt_Time_500ms:
            return Q_TRAN(&state_on);

        default:
            return Q_SUPER(&QHsm_top);              // 其他事件,在此时不处理
    }
}

image.png


原文链接:

  1. Quantum LeaPs(QP)的资料
  2. QP™ 实时嵌入式框架 (RTEF)

书籍:C/C++ 中的实用 UML 状态图,第 2 版


QP官方网站
QP对比RTOS有什么好处?
QP/C的SDK手册 介绍QPC的框架、Getting Started、API和参考例程。

QM的软件手册 QP的UML建模软件,可以自动生成基于QP框架的代码。
QP官方的交流论坛 是获取QP使用帮助最好的方式。

QP™ 实时嵌入式框架 (RTEF)


QP™(量子平台)是一系列轻量级实时嵌入式框架 ( RTEF ),用于基于活动对象设计模式构建 事件驱动的 嵌入式软件。QP 系列由 QP/C 和 QP/C++ 框架组成,它们受到严格的质量控制、完整的文档,并在灵活的双重许可模式下可用。

MCU 的实时嵌入式框架


QP/C 和 QP/C++ 实时嵌入式框架 (RTEF) 提供轻量级、可重用的软件架构,该架构将事件驱动的并发模型(称为 活动对象 (actor))与 有限状态机相结合 。这种架构本质上支持并自动执行并发编程的最佳实践。这导致应用程序比传统实时操作系统 (RTOS) 的裸线程和无数阻塞机制 更安全 、响应更快且更易于管理。QP 框架还提供了足够 高的抽象级别, 可以有效地将图形建模和代码生成应用于深度嵌入式系统,例如基于 ARM Cortex-M 的微控制器。
QP/C 在线手册
GitHub 上的 QP/C
下载 QP/C/C++™ 作为 QP-Bundle 的一部分
QP/C++ 在线手
GitHub 上的 QP/C++

image.png

QP™ 亮点

由事件驱动的活动对象组成的 QP™ 应用程序比基于传统 RTOS 线程的等效解决方案消耗 更少的内存, 尤其是 RAM。
QP™ RTEF 与基于优先级的抢占式内核相结合,适用于硬实时应用。事实上, 非阻塞 活动对象比传统阻塞 RTOS 线程更适合 RMS/RMA 方法。
经过 15 多年的不断改进,QP™ 框架是嵌入式软件市场上同类产品中最成熟和最受欢迎的产品。

支持现代状态机

活动对象的行为通过现代 有限状态机 (UML 状态图)在 QP/C 和 QP/C++ 中指定。QP 框架支持使用 C (QP/C) 或 C++ (QP/C++) 以及基于模型的设计 (MBD) 手动编码 UML 状态机,并通过 免费的 QM™ 基于模型的设计工具 自动生成代码.

独立(裸机)操作

QP™ RTEF 可以 独立 运行,完全取代传统的 RTOS。这些框架包含一系列内置实时内核,例如协作 QV 内核、抢占式非阻塞 QK 内核和独特的抢占式双模式(阻塞/非阻塞)QXK 内核。为 ARM Cortex-M (M0-M7) 以及其他 CPU提供了独立的 QP 端口和即用型示例。
您不需要使用传统的 RTOS 来通过 QP 实现抢占式多任务处理。内置的抢占式QK 和 QXK 内核支持基于优先级的抢占式多任务处理,与速率单调调度完全兼容, 以实现有保证的硬实时性能。这些抢占式内核与活动对象的从运行到完成的执行语义完美匹配,而且比传统的阻塞式 RTOS 内核更简单、更快、更高效。

QP™ RTEF 还可以与许多传统的第三方 RTOS 配合使用。为多个 RTOS(例如 embOS、ThreadX、MicroC/OS、FreeRTOS 等)提供了 QP 端口和即用型示例
您可能考虑使用传统 RTOS 内核来执行事件驱动 QP™ 应用程序的最重要原因是与现有软件的兼容性。例如,许多通信栈(TCP/IP、USB、CAN 等)是为传统的阻塞内核设计的。此外,许多遗留代码需要阻塞机制,例如信号量或时间延迟。传统的 RTOS 允许您将现有软件组件作为与事件驱动 QP™ 活动对象并行的常规“阻塞”线程运行。
QP/C RTOS 端口
QP/C++ RTOS 端口

通用操作系统支持

QP™ RTEF 还可以与通用操作系统一起使用,例如 Linux (POSIX)、Windows 和 macOS。
通用操作系统的 QP™ 端口本身就很有趣。例如,QP 到 POSIX 的端口支持实时扩展,可以与嵌入式 Linux以及 QNX、INTEGRITY、VxWorks 等 RTOS 的 POSIX 子系统配合使用。同样,QP 到 Windows 的端口可以与 Windows IoT 或 Windows Embedded Compact 配合使用。
但是操作系统支持对于在桌面工作站上开发深度嵌入的代码也很有趣,这被称为“双目标”。

QP/C 和 QP/C++ 特性比较

特征 质检/质控 QP/C++
最新版本(修订历史)
最新发布日期
6.9.3
2021-04-12
6.9.3
2021-04-12
预期目标系统
(代表性硬件)
32 位/16 位 MCU
(ARM Cortex-M)
32 位/16 位 MCU
(ARM Cortex-M)
由免费的QM™ 基于模型的设计工具支持
活动对象的最大数量 64 64
具有任意参数的动态事件
自动事件回收
直接事件发布 (FIFO)
直接事件发布 (LIFO)
发布订阅事件传递
事件延期
系统时钟滴答率数 可配置 (0..15) 可配置 (0..15)
每个活动对象的时间事件数 无限 无限
———————————————————————- 状态机 - ———————————————————————
分层状态机(QHsm-strategy
分层状态机(QMsm-strategy
子机和子机状态
———————————————————————- 内置内核 ———————————————————————-
抢占式非阻塞内核 (QK)
抢占式阻塞双模内核(QXK)
———————————————————————- 第三方实时操作系统/操作系统 ———————————————————————-
可移植到第 3 方 RTOS 内核
POSIX 的可用端口(Linux、QNX、INTEGRITY 等)
Windows 的可用端口
———————————————————————- 测试/分析———————————————————————-
QP/Spy™ 软件追踪
QUTest™ 单元测试工具
MISRA 合规性 MISRA-C:2004
AUTOSAR-C++ 合规性 AUTOSAR-C++ 14
PC-Lint-Plus 支持包
———————————————————————-授权许可 — ——————————————————————-
开源许可 (GPL)
闭源(商业)许可

传统 QP™ 开发套件 (QDK)

QP 开发套件 (QDK) 是用于各种嵌入式处理器、工具集和电路板的单独 QP 端口和示例。

为什么是“遗产”?

从 QP 5.4.0 版开始,所有官方支持的端口和示例都捆绑在 QP 下载中,而不是作为单独的 QP 开发套件 (QDK) 分发。为早期 QP 版本发布的 QDK 称为“legacy-QDK”,可从 SourceForge 下载
查看/下载可用的旧版 QDK


嵌入式软件开发中,虽然很多的开发工具已经支持C++的开发,但是因为有时考虑运行效率和编程习惯,还是有很多人喜欢用C来开发嵌入式软件。Miro Samek说:“我在开发现场发现,很多嵌入式软件开发者(绝大多数是电子工程师)都有工作背景而且他们常常不喜欢用C++。”【1】
面向对象编程(OOP)是一种方法,而不依赖于某特定语言。一般认为,OOP只能使用面向对象的语言,如Smalltalk、C++或Java等固有支持OOP的语言。但是,OOP并不依赖于特殊的语言,只要实现了OOP的封装、继承和多态性这三大基本特性,使用非固有支持面向对象的语言,也可以进行面向对象的编程。
面向对象编程方法的三大基本特性是:

  • 封装性—-封装数据(属性)与函数(方法)到类中的能力;
  • 继承性—-基于已有的类定义新类的能力;
  • 多态性—-在运行时具有一致接口的对象相互替换的能力。

虽然这些特性与面向对象语言有关,但几乎可以用任何语言实现面向对象的这三大基本特性,包括C语言和汇编语言等。事实上,不管实现语言为何,任何大的软件系统几乎都会以某种形式使用抽象(封装)、继承或多态性。

1.C语言的封装

1.1 封装的本质

在C++中,一个类中包括属性和方法,声明一个类例子如下:

class A {
    int n;                 //属性
    A();                   //构造函数
    void function1(int x){...}; //方法
}

在C++中,用A生成两个对象a1和a2时,如下

A a1 = new A();
A a2 = new A();

对于非模板类A,在内存中,对所有的对象,方法function1只有一个拷贝,而对每个对象,都有自己独立的一份属性拷贝,也就是有多个属性拷贝,所谓创建一个对象,也就是创建一套属性。
调用a1.function1()和a2.function1()时,每个对象使用自己的属性n1和n2,而方法代码在内存中是同一套。这就相当于调用functin1(n1,x)和function1(n2,x)。
在C语言实现中,把所有的属性放到一个结构中,所有方法中第一个参数用一个指向自己属性结构的一个指针me, 如下:
function1(struct *me, …);
这个方法对所有对象有用,具体对哪个对象的属性操作,可以用me来区分。

1.2 具体例子

生成两个文件person.h和person.c,定义一个Person结构类型(也就是类名称),把属性放到结构中方法中的第一个参数me是Person的引用指针
pers1.2on.h文件:
在头文件中以结构定义类成员属性,声明构造方法和析构方法,声明public方法。

#ifndef PERSON_H
#define PERSON_H

    /*1.定义类成员属性(没有隐藏)*/
    typedef struct PersonTag {
        String name__; /*属性*/
        uint8_t age__; /*属性*/
    }Person;  

    /*2.声明构造和析构函数*/
    void Person_Ctor(Person *me, uint8_t age);/*第一个参数指向属性*/
    void Person_Xtor(Person *me);/*第一个参数指向属性*/


    /*3.声明public方法*/
    void Person_setName(Person *me, String name);/*第一个参数指向属性*/
    String Person_getName(Person *me);/*第一个参数指向属性*/

    /*4.声明private方法(要在person.c文件中声明)*/ 

#endif

person.c文件:
在.c文件中声明private方法,并实现所有的方法。

#include "person.h"

/*1.声明private方法, 用static修饰*/
static void Person_setAge(Person *me, uint8_t age);/*第一个参数指向属性*/
static uint8_t Person_getAge(Person *me);/*第一个参数指向属性*/

/*2.实现构造和析构函数*/
void Person_Ctor(Person *me, uint8_t age) {
    me->age__ = age;
}
void Person_Xtor(Person *me) {
    ...
}

/*3.实现public方法*/
void Person_setName(Person *me, String name) {
    me->name__ = name;
}
String Person_getName(Person *me) {
    return me->name__;
}

/*4.实现private方法*/
void Person_setAge(Person *me, uint8_t age) {
    me->age__ = age;
}
uint8_t Person_getAge(Person *me) {
    return me->age__;
}

app.c中使用Person类:

#include "person.h"  /*包含头*/
void main() {
    String tmp;
    Person l_ps1; /*创建一个实例,实际就是一个属性*/
    Person l_ps2; /*创建另一个实例,实际就是另一个属性*/

    Person_Ctor(l_ps1, 30);      /*显式调用构造函数*/
    Person_setName(l_ps1,"张三"); /*使用其public方法*/
    tmp = Person_getName(l_ps1); /*使用其public方法*/

    Person_Ctor(l_ps2, 30);       /*使用其public方法*/
    Person_setName(l_ps2, "李四"); /*使用其public方法*/
    tmp = Person_getName(l_ps2);  /*使用其public方法*/
}

2.C语言的继承

Student继承自Person, 继承类属性结构的第一个参数指向父类属性,这样可以满足Liskov替换原则
student.h文件:
在头文件中以结构定义类成员属性,并声明构造、析构函数和public方法。

#ifndef STUDENT_H
#define STUDENT_H
#include "person.h"  /*包含父类头*/

    /*1.定义类属性(没有隐藏)*/
    typedef struct StudentTag {
        Person super;   /*第一个参数指向父类Person,继承父类属性*/
        uint8_t code__; /*子类添加的属性*/
    }Student;  

    /*2.声明构造和析构方法*/
    void Student_Ctor(Student *me, uint8_t code);
    void Student_Xtor(Student *me);

    /*3.声明public方法*/
    void Student_setCode(Student *me, uint8_t code);
    uint8_t Student_getCode(Student *me);

    /*4.声明private方法(在.c文件中声明)*/ 

#endif

student.c文件:
在.c文件中声明private方法,实现所有的构造方法和析构方法,实现所有的public和private方法。

#include "student.h"

/*1.声明private方法*/
static void XXX(Student *me, ...);


/*2.构造和析构方法实现*/
void Student_Ctor(Student *me, String name, uint8_t code) {
    Person_Ctor((Person*)me, name);/*先调用父类构造函数*/
    me->code__ = code;
}
void Person_Xtor(Person *me) {
    ...
}

/*3.public方法实现*/
void Student_setCode(Student *me, uint8_t code) {
    me->code__ = code;
}

app.c中使用Student类:

#include "Student.h"  /*包含头*/
void main() {
    String tmp;
    Student l_st1; /*创建一个实例,实际就是一个属性*/
    Student l_st2; /*创建另一个实例,实际就是另一个属性*/

    Student_Ctor(l_st1, "张三, 25);  /*显式调用构造函数*/
    Student_setCode(l_st1, 30); //使用其public方法
    tmp = Student_getCode(l_st1); //使用其public方法
}

3.C语言的多态性

利用C语言的万能指针void *p可以实现多态。略。

4.总结

用C语言可以实现面向对象的三大特性,适合于嵌入式系统的编程,QP的C版本就是这样实现的。现在很多的开源软件,包括postgreSQL、GObject等都采用C语言实现面向对象的方法。采用这个方法,使程序具有更好的可读性、可扩展性,同时保持了C语言的高效性。


五种状态设计模式:

  • 终极钩子模式
  • 提醒者模式
  • 延迟事件模式
  • 正交组件模式
  • 转换到历史模式

    1.终极钩子模式

    俗语:老爸对儿子说,你可以按你的特殊方式去做事,但如果你不做,我会做。
    目的:公共的处理功能放到父状态,并可以在子状态中重载,以实现特殊的功能。为什么叫终极钩子,因为如果子状态不处理,总会在父状态中得到处理,是个终极的处理。
    问题:父状态提供公共一致的事件处理方式,如在GUI设计中,提供一致的界面样式,为所有的子状态提供默认的一致界面,如果需要,子状态也可以重载默认的事件处理方式。
    解决方案:使用差异化编程,公共的事件处理放到父状态中,子状态中重载特殊行为。子状态(specific)中的A事件处理重载了父状态A事件处理,子状态中的A事件处理就是特殊的事件处理。如果某个子状态不处理A,则在父状态中处理A。
    image.png

    2.提示器模式

    俗语:我闲的时候,你通知我做,我就做;我在做事的时候,你就不能通知我了,等我闲时再说。
    目的:产生一个事件,并发送给自身,以使状态图拓扑更加灵活。
    问题:在状态建模时,一些相关的功能耦合太强,用提示器模式,可以解耦。如定时查询传感器功能与处理数据功能,当查询到传感器接完数据时,通过发送一个提示事件让处理数据功能进行处理。
    image.png
    解决方案:两个功能设计为一个层次状态机结构,查询功能设计到父状态中,处理功能设计到子状态中,用一个提示器事件(DATA_READY),提示处理功能来处理查询传感器的得到的数据。
    image.png
    可以用提示器模式把一个长任务分解为多个小任务来执行。

    3.延迟事件模式

    俗语:我很忙,有事我先记下,我闲时,我再按我记下的去做。
    目的:通过修改事件的顺序来简化状态机。用于处理事务。
    问题: 当前正在处理一个事务时,因事务是原子性的,不可分割的,这时要是来了新的事件,可能会破坏事务处理的原子性。如ATM终端。
    解决方案: 在处理事务时,接收事件并保存到本地队列中,当本事务处理完时再从本地队列中召回保存的事件。
    image.png

    4.正交组件模式

    俗语:虽然你是你,我是我,但你听我的没错。
    目的:把状态机做为一个组件来使用,把原来正交区域的独立关系转化为主-从关系。
    问题: 许多对象包含相对独立的具有状态行为的部分,在UML状态图中实现方式是,把这种松散关联的功能放到独立的正交区域,而采用正交区域的解决方法,成本较高。如闹钟的功能实现。
    解决方案: 使用对象合成来代替正交区域,把行为独立的两部分分成两个状态机,其中一个包含另一个。正交区域的两个对象,使一个对象成为容器,包含另一个组件对象,由容器对象负责对组件对象的初始化及调用。通过直接向组件派发事件,容器可以主动触发组件初始化和状态转换;而组件是通过发送事件给容器来通知容器的。

5.转换到历史模式

俗语: 我在工作中,当有更重要的事时,我马上去做,做完后,我会回到我被中断工作前的状态。
目的:从某个组合状态转换出来时,记住最近的活动子状态,在返回时,还返回到这个活动子状态。
问题:在高层组合状态处理事件时,常常需要处理一些更重要的事件,当处理完时,系统必须返回组合状态最近的子状态。
解决方案: 在组合状态机的属性中加一个状态函数变量xxx_history,在状态机进入某状态时,保存当前状态到这个状态函数变量中。在退出组合状态后返回时,按照保存在状态函数变量值,转换到最后退出组合状态前的状态。
image.png
参考:
【1】Miro Samek《UML状态图的实用C/C++设计—-嵌入式系统的事件驱动型编程技术》第二版


QP总体结构

  • QP是一个基于事件驱动的嵌入式系统软件框架,其总体结构如下图。
  • AO活动对象由事件队列和层次状态机两部分组成,每个AO占有一个优先级;
  • QF量子框架由五个数据结构及操作组成,其数据结构采用了uCOS-II相似的结构;
  • QP有两种调度方式:合作式调度Qvanilla和抢占式调度QK,按实际要求选择一种;
  • QS跟踪调试工具;
  • GUI,TCP/IP通信等其它功能,可以以AO插件的方式加入到平台中。

image.png
参考:
【1】http://www.state-machine.com
【2】【嵌入式系统工匠坊—-ESCW】


1.QP简介:

量子平台(Quantum Platform, 简称QP)是一个用于实时嵌入式系统的软件框架,QP是轻量级的、开源的、基于层次式状态机的、事件驱动的平台。
QP包括事件处理器(QEP)、轻量级的事件驱动框架(QF)、任务调度微内核(QK)和实时跟踪调试器(QS)四个部分。
利用QP可以开发出结构清晰的嵌入式应用程序(使用C或C++语言)。
image.png

2.QP之QF简介

QF是QP的核心,可以把QF理解为软件总线(software bus,SBus)。在这个软件总线上连接着很多的AO和硬件中断服务程序ISR,QF负责事件的存贮、分发和回收等功能,也就是事件驱动。
QF是一个事件驱动框架。应用程序可以创建事件实例,放到事件池中,并分发事件到相应注册了这个事件的活动对象(状态机,任务)AO中。应用程序可以注册特定的事件,当有事件发生时,QF就会分发事件到AO中。应用程序是由多个AO组成,每个AO可以认为是一个任务。把硬件中断程序也当做AO看待,只是优先级比较高。
QF量子框架由五个数据结构及操作组成,其数据结构采用了uCOS-II相似的结构。
image.png

3.QF分发事件

如图 1所示,QF是个软件总线,在这个软件总线上连接着AO和ISR。QF中包含5个主要的数据结构(受uCOS II影响很深,要看一下uCOS II内核)。
有两种事件的分发方式:
(1)直接分发事件
一个AO直接分发事件到另一个AO,就是直接投递事件。这种方式分发事件的AO要知道目标AO。
使用void QActive_postFIFO(QActive me, QEvent const e) 函数分发事件。
(2)P/S分发事件
利用QF中的AO订阅事件表,以P/S(Publish-Subscribe出版-订阅)方式分发事件,也就是P/S投递事件。 这种方式分发事件AO不用知道目标AO,QF查找AO订阅事件表就知道把事件分发到什么地方。P/S投递方式减少了AO之间的耦合度。
使用void QF_publish(QEvent const e)函数分发事件。
image.png
框架QF包括活动对象AO,每个AO包含一个AO队列(QEQueue)和状态机。QF中还有5个主要的数据结构表,其中有3个AO相关的数据结构:AO订阅事件表,AO队列状态表,AO注册表;有1个事件池QMPool,用于保存事件实例,其它事件都是对这个事件实例的引用;有1个*时间事件链表QtimeEvt
,用于定时事件处理。图 2是相应数据表与QF和AO的关系。
image.png

4.QF运行

QF运行时,包括的过程有:
(1)QF初始化;
(2)AO和ISR产生事件;
(3)分发事件到AO队列;
(4)循环调度分派事件到状态机。
在把事件放到AO队列中时,会置位AO队列状态表相应位;从AO队列中取出事件时,会清零AO队列状态表相应位。总之,放事件到AO队列或从AO队列取事件,都会修改AO队列状态表。
image.png
(1)QF初始化
在QF运行之前,首先要进行初始化,要创建AO、创建事件池、创建AO订阅事件表,创建AO队列等一系统开始工作。
image.png
(2)AO和ISR产生事件
在AO或ISR中,用Q_NEW()宏可以创建用户事件实例,并放到事件池中。
如用 UserEvt *pe = Q_NEW(UserEvt , UserSig1)
创建了一个用户事件pe。可以分发这个用户事件pe到AO队列中。
其中,UserEvt是增加了参数的用户自定义事件,它是继承自QEvent,UserEvt定义如下,

typedef struct UserEvtTag {
QEvent
super; / 继承自QEvent /
uint8_t parameter;/增加的事件参数 /
} UserEvt;
UserSig1是用户定义的信号,用枚举来定义,定义如下,
enum UserSignals {
User1SIG = QUSER_SIG,/ 用户定义第一个信号 /
User2_SIG, / 第二个信号 /
User3_SIG, / 第三个信号 /
...

/ 其它信号 /
};

(3)分发事件到AO队列

可以用QActivepostFIFO(TargetAO, (QEvent )pe)直接分发事件或用QF_publish((QEvent )pe)间接分发事件到目标AO队列中。使用直接分发事件方式时,不需要AO订阅事件表QF_subscrList的参与,而间接分发事件时要用到AO订阅事件表。
这两种分发事件都会修改AO队列状态表QF_readySet
(QK中,用QK_readySet),置位相应位,而在Vanilla调度或QK调度会清除AO队列状态表相应位。
(4)循环调度分派事件到状态机
- 有两种调度方式,一种是超级循环调度Vanilla方式,这种方式的任务是非抢占式的;
- 另一种方式是QK调度方式,QK是一个微调度内核,这种方式任务是抢占式的。
两种调度方式的流程:
image.pngVanilla调度流程
image.pngQK调度流程

5.总结

QF是一个框架,用于事件的产生、保存、分发事件。有两种事件分发方式,并支持两种对任务的调度方式;可以理解QF为一个软件总线,其上安装了很多的AO和ISR。最多可以支持63个AO+ISR。
参考:
【1】QP量子平台、量子编程:http://www.state-machine.com


对QP中RTC的理解

1.概念

RTC(Run To Completion)是运行到完成为止的意思。在状态机中,从源状态到目标状态的转换动作要运行到完成。
从字面上来看,这个过程像是不可中断的,但实际并不是,这个过程可以被硬件中断程序打断
在本状态机中,一个事件处理要是执行了,本状态机的其它事件到来时,就只能在队列中等待,不可中断这个正在处理的过程,也就是要运行到完成(RTC)。这个事件处理完后,才能对队列中的其它事件进行处理。所以在活动对象(Active Object,AO)中总是有一个队列,这个队列用来保存发送到本状态机的事件的。

2. 几种情况

  • 更高级任务的事件到来

异步事件
如果在任务L处理事件a时,硬件中断到来了,中断了任务L的事件a处理过程(保存断点),硬件中断中可能产生一个更高级任务H的事件b(异步事件),则在中断返回前执行调度器,处理高级任务H的事件b, 之后,从上边保存的断点返回,继续执行没有完成的任务L的事件a。事件a不是真正的RTC,被事件b中断了。
同步事件
如果在任务L处理事件a时,post了一个事件c(同步事件), 事件c是更高级任务H的,则post()中也调用调度器,先处理任务H的事件c, 之后继续执行任务L的事件a中post()后的代码。事件a不是真正的RTC,被事件c中断了。

  • 同级任务的事件到来

如果在任务L处理事件a时,硬件中断中产生了事件d(异步事件),或处理事件a中post了事件d(同步事件), 事件d是属于任务L的,则保存事件d到对应的任务L的队列中,先处理完事件a,之后由QK的循环处理事件d。事件a是RTC的。

  • 更低级任务的事件到来

如果在任务L处理事件a时,硬件中断中产生了事件d(异步事件),或处理事件a中post了事件d(同步事件), 事件d是属于比任务L更低的任务LL的,则保存事件d到对应的任务LL的队列中,先处理完事件a,之后由QK的循环处理事件d。事件a是RTC的。


SST-超级简单任务调度器结构分析

SST(Super Simple Task) 是一个基于任务优先级、抢占式、事件驱动、RTC、单堆栈的超级简单任务调度器,它基于Rober Ward一篇论文的思想,Miro Samek用C重新编程实现的,它是QP中QK的的基本思想。
QK加上状态机事件处理的方法QEP,再加上任务的注册与事件的保存与分发功能QF,再加上串口调试功能QSpy,再加上基于模型驱动的开发QM,就成了QP。
2015年,QP获得“嵌入式计算设计”顶级发明奖

1.SST层次结构

QP状态机 - 图27

2.SST总体结构

QP状态机 - 图28

  • 发送事件(产生事件)SST_post(目标任务优先级prio,信号sig,参数para ),发送事件到prio任务对应事件队列,置1队列状态位。
  • 调度(消耗事件)SSTschedule(void ),调度任务函数,队列空时,清0队列状态位。
  • 队列状态表置1SSTreadySet |= tcb->mask_ 队列中放入事件时,对应位置1;
  • 队列状态表清0SSTreadySet &= ~tcb->mask_ 队列中无事件时,对应位清0.

    3.SST TCB结构

    QP状态机 - 图29

    4.发送事件SST_post()流程图

    QP状态机 - 图30

    5.调度SST_schedule()流程图

    image.png

    6.头文件关系

    image.png

    7.进入中断宏、,退出中断宏、及中断中的使用

  • 中断的进入宏 ```c

    define SSTISR_ENTRY(pin, isrPrio_) do { \

    (pin) = SST_currPrio; \ /1.保存被中断的任务优先级(当前优先级)到pin中/ SSTcurrPrio = (isrPrio_); \ /2.设置ISR的优先级isrPrio为当前优先级/ SST_INT_UNLOCK(); \ /3.开中断/ } while (0)


- **中断的退出宏**
```c
#define SST_ISR_EXIT(pin_, EOI_command_) do { \
  SST_INT_LOCK(); \          /*1.关中断*/
  (EOI_command_); \          /*2.结束当前中断*/
  SST_currPrio_ = (pin_); \  /*3.恢复被中断前任务优先级为当前任务*/
  SST_schedule_(); \         /*4.调度*/
} while (0)
  • 中断中使用宏例子
    //键盘中断ISR 
    static void interrupt kbdISR(){
      uint8_t pin;
      uint8_t key = inport(0x60); //取键值
      displayPreemptions(SST_currPrio_, KBD_ISR_PRIO); //测试用,显示被抢占次数 
      SST_ISR_ENTRY(pin, KBD_ISR_PRIO);        /*1.进入中断*/
      SST_post(KBD_TASK_PRIO, KBD_SIG, key);   /*2.发事件到KbdTask任务*/
      SST_ISR_EXIT(pin, outportb(0x20, 0x20)); /*3.退出中断*
    }
    

结束语

如果把Rober Ward的论文比喻成一粒不起眼的种子,Miro Samek的SST就是一个小树苗,而Miro Samek的QP则长成了参天大树,这棵树已经成长了十年,2015年,QP获得“嵌入式计算设计”顶级发明奖【1】。
参考:
【1】QP获得“嵌入式计算设计”2015年顶级发明奖


QP之QK原理

QK是一个很小的抢占式微内核调度程序,它专用用QP中。
QK的思想源于SST,Miro Samek重写了自己前期编的SST(Super Simple Task)代码。
QK循环查询AO队列的状态表QKreadySet,查找当前最高级的AO(活动对象,任务),分发最高级AO队列中的事件到相对应的AO中。并在某事件消耗完时,回收事件。
QK的流程图如下:
image.png


程序控制的软件复位方法

微处理器复位方法:

  • 用硬件引脚RESET的复位方法;
  • 用看门狗的复位方法;
  • 自己程序控制的复位方法(本文方法,叫软件复位)

    软件复位实现方法

  1. 首先声明一个函数指针,指向复位后要开始执行指令的位置:
    void (*app_start)(void) = 0x0000;
    可以根据具体微处理器复位后的位置,修改后边的地址。
  2. 之后,在自己的程序中想复位的地方调用app_start(); 也就是跳到0x0000位置执行指令。
  3. 可以根据不同微处理器复位的地址,修改上边的地址(0x0000)。实现上你可以跳到任何位置执行,或调用函数执行。

    实例测试

    以下以Arduino为开发环境来测试。注意:在AVR微处理器中,为了使Arduino复位时不再进入引导区,使AVR设置BOOTRST设置为1。
    使用函数指针复位方法如下,你能想象到下边的程序会使LED灯闪亮吗?

    ///0. 引脚定义
    #define LED_PIN   13  //定义LED引脚为13号数据引脚(DIG13)
    static void (*reset_this_CPU)(void) = 0x0000; // ***复位本CPU函数指针
    ///1. 初始化
    void setup() {
    pinMode(LED_PIN, OUTPUT); // 初始化DIG13为输出  
    digitalWrite(LED_PIN, HIGH); //DIG13输出高电平
    delay(1000);                 //延时1秒
    digitalWrite(LED_PIN, LOW);  //DIG13输出低电平
    delay(1000);                 //延时1秒  
    reset_this_CPU(); //***跳到0x0000地址指针,也就是复位
    }
    ///2. 循环
    void loop() {
    
    //此处什么也没有啊!
    }