Жизнь после RTOS - модель акторов QP/C++ | UML

Компьютер — это конечный автомат. Потоковое программирование нужно тем, кто не умеет программировать конечные автоматы. — Алан Кокс

Quantum Platform (QP) — это семейство программных продуктов для разработки встраиваемого ПО, в том числе и для микроконтроллеров. QP предлагает реализацию как на «плюсах» (QP/C++), так и на Си (QP/C и QP-nano), что даёт возможность разрабатывать приложения для широкого спектра различных архитектур и целевых платформ. Во многих случаях QP может рассматриваться как альтернатива для RT(OS), хотя также предусмотрена возможность запускаться и поверх уже имеющейся православной ОС.

Приложение на QP состоит из активных объектов (акторов) с приоритетами, которые по совместительству представляют из себя иерархические конечные автоматы. Ядро QP, в зависимости от конфигурации, позволяет активным объектам работать либо кооперативно QV (Vanilla), либо каждый в своей нити QK. Активный объект — «лентяй», он сам по себе ничего не делает кроме как ожидает пинка интересующие его события и реагирует на них — например записывает чего-нибудь в порт, переходит в новое состояние или даже асинхронно отсылает конечное число сообщений (сигналов) другим активным объектам, после чего снова переходит в режим ожидания. Длительное выполнение обработчика события как правило является недопустимым, поскольку при этом сам активный объект (или вообще всё приложение в случае с кооперативного ядра) не может реагировать на другие события и очевидно, что такое поведение противоречит концепции реального времени. А никто и не говорил, что будет легко :)

Код активных объектов можно набирать как ручками в виде обычных классов (да, в Си можно эмулировать классы), так и воспользоваться специальным инструментом для визуального моделирования UML диаграмм состояний QM (QP Modeler) c последующей автоматической генерацией кода. В данном материале будет делаться акцент на визуальном моделировании, в связи с чем будет немного кода и много красивых картинок. Начнём с самого простого — мигающих лампочек.

MSP430.js | исходники

screenshot

Для компиляции проекта нужна только IDE Code Composer Studio (CCS), C++ компилятор там уже встроен. Проше всего импортировать сразу весь архив с исходниками: File -> Import -> CCS Projects -> Select archive file. На картинке изображена среда моделирования QM и диаграмма состояний UML мигающих лампочек (файл blink.qm в исходниках). Частота переключения лампочек определяется при активации таймера и при BSP::TICKS_PER_SEC это одна секунда. Для автоматической генерации диаграммы в исходный код требуется задать специальные файлы шаблоны, в которых для QM нужно явно прописать $declare и $define. В принципе можно было бы даже обойтись и без указателя QP::QActive * const AO_BlinkyPtr, он нужен только для лучшей инкапсуляции активного объекта BlinkyAO — это классическая идиома.

blinky.h

#ifndef blinky_h
#define blinky_h

namespace blinky {
    $declare(AOs::AO_BlinkyPtr)
}
// declare other elements...

#endif // project_h

blinky.cpp

#include "qpcpp.h"
#include "blinky.h"
#include "bsp.h"

namespace blinky {

    enum {
        TIMEOUT_SIG = QP::Q_USER_SIG,
    };

    $declare(AOs::BlinkyAO)

    // Local objects -------------------------------------------------------------
    static BlinkyAO __self_ao; // the single instance active object

    // Global-scope objects ------------------------------------------------------
    QP::QActive * const AO_BlinkyPtr = &__self_ao; // "opaque" AO pointer

    $define(AOs::BlinkyAO)

}

// define other elements...

Базовый класс события QEvent состоит из сигнала QSignal sig и поля служебной информации uint8_t dynamic_, этот класс можно наследовать и добавлять туда дополнительные поля с данными, характерными для конкретного события. Идентификаторы сигналов для активных объектов должны начинаться не с нуля а с Q_USER_SIG, так как в QP уже есть сигналы, зарезервированные системой, например Q_EMPTY_SIG, Q_ENTRY_SIG, Q_INIT_SIG, Q_EXIT_SIG. Вся низкоуровневая логика взаимодействия с конкретной железкой находится в файле bsp.cpp и там же реализован обработчик ошибок (куда ж без них то) QP. Ну а собственно процесс инициализации самого QP и запуск активных объектов в данном простейшем случае выглядит так:

main.cpp

#include "qpcpp.h"
#include "bsp.h"
#include "blinky.h"

int main() {
    static QP::QEvt const *blinky_queueSto[10];    // Event queue storage

    BSP::init(); // initialize the Board Support Package
    QP::QF::init(); // initialize the framework and the underlying RT kernel

    // publish-subscribe not used, no call to QF::psInit()
    // dynamic event allocation not used, no call to QF::poolInit()

    blinky::AO_BlinkyPtr->start(1U,                   // priority
                     blinky_queueSto, Q_DIM(blinky_queueSto), // event queue
                     (void *)0, 0U);                  // stack (unused)


    return QP::QF::run(); // run the QF application
}

С мигающими лампочками всё понятно, но нужно что-то поинтересней — предлагаю построить аналог для ранее рассмотренного приложения под FreeRTOS. В системе три активных объекта — левые лампочки, правые лампочки и дисплей. Кооперативное ядро QP позволяет сразу же забыть о проблеме атомарного доступа с портам. Теперь самое время чуть более подробно остановиться на событиях и какими они бывают. Итак в приложении добавляются глобальные события от кнопок BTNS_LEFT_SIG и BTNS_RIGHT_SIG. События в QP бывают двух видов:

  • статические — неизменяемые (immutable) события. За создание экземпляра статического события отвечает разработчик приложения. В нашем случае оба события содержат дополнительную информацию о нажатой кнопке, их значение изменяется и такие события не попадают в категорию статических

  • динамические — создаются макросом Q_NEW в области динамической памяти и существуют до тех пор, пока не будут обработаны всеми активными объектами, которым они предназначены. Для динамических событий при инициализации приложения нужно создавать пул с помощью вызова QP::QF::poolInit. Так создаётся событие от кнопок BtnEvt *btnEvt = Q_NEW(BtnEvt, BTNS_LEFT_SIG); btnEvt->value = 42;

В дополнение ко всему для более рационального использования ресурсов микроконтроллера QP поддерживает два способа доставки сообщений:

  • напрямую — отправитель явно вызывает метод у экземпляра активного объекта получателя. Например можно послать сообщение самому себе me->POST(&UPDATE_EVENT, me)

  • мультикаст — отправитель ничего не знает о получателях, каждый получатель динамически подписывается / отписывается от интересующих его событий. Такой способ хорошо подходит для кнопок. Для мультикаста при инициализации приложения нужно создать пул с помощью вызова QP::QF::psInit. Так отсылается сообщение от кнопок QP::QF::PUBLISH(btnEvt, (void *)0);

screenshot

На рисунке диаграмма состояний активного объекта с правыми лампочками. У него одно единственное состояние, единственное что он делает это слушает кнопку и в зависимости от её значения устанавливает цвет для правых лампочек. Событие не обязательно должно менять состояние активного объекта, реакция на событие может быть любой на усмотрение разработчика.

screenshot

Здесь активный объект занимается выводом данных на дисплей, отсылая сообщение самому себе. Для экономии памяти микроконтроллера событие DISPLAY_UPDATE может быть статическим static QP::QEvt const UPDATE_EVENT = { DISPLAY_UPDATE_SIG, 0U, 0U }; т.к. никаких дополнительных полей с данными в нём нет. В текущей реализации у данного активного объекта должен быть наименьший приоритет в системе.

screenshot

Активный объект для левых лампочек самый интересный. Тут иерархический конечный автомат. Иерархический автомат содержит в себе другие автоматы. В каком бы состоянии не находится активный объект для левых лампочек, он всегда слушает событие о нажатой кнопочке, реакция на событие одинаковая. Иерархический автомат позволяет избежать дублирования – проще реализовать требуемое поведение один раз в родительском состоянии, чем повторять одну и ту же логику в дочерних состояниях. Еще один полезный приём, позволяющий значительно упростить жизнь разработчику – использовать историю предыдущих состояний (Transition to History), лампочки помнят свой цвет в состоянии мигания.

MSP430.js | исходники

К QP прилагается серьёзная книга «Practical UML Statecharts in C/C++, Second Edition: Event-Driven Programming for Embedded Systems» от основателя компании Quantum Leaps. В данной книге есть отдельная глава с описанием паттернов, которые часто применяются при проектировании конечных автоматов. На сайте Quantum Leaps также представлены дополнительные обучающие материалы и полезные инструменты для разработки QP приложений – юнит тесты QUTest, QSpy.

Далее игра «Змейка» на QP/C++.

links

social