Разработка для встроенных систем на основе Quantum Leaps
Quantum Leaps или QP это семейство фреймворков с открытыми исходными кодами для разработки встроенных систем. QP может работать с основной ОС (Linux, Windows, FreeRTOS и др) или без нее. QP портирован на огромное количество различных семейств процессоров. Приложение на QP проектируется в виде нескольких конечных автоматов на основе UML диаграмм состояний (Statecharts). Далее пример проектирования очень простого приложения на основе конечных автоматов.
1. Получаем фреймворк
Для начала заходим на сайт проекта и качаем исходники библиотеки. Далее качаем SDK с примерами для Win32. В примере показано решение задачи о обедающих философах в консоли, на Win32 API и MFC. В папке с документами также находится полное описание процесса проектирования консольного приложения. Пример довольно сложный, поэтому, что бы начать с чего-то очень простого я решил придумать и решить собственную задачу.
2. Постановка задачи
Грузчик Боб работает на конвейере, который доставляет ящики на склад. Если на конвейере есть ящик, тот Боб берет его и складывает в кучу. Если ящика нет, то Боб стоит и ждет. Когда ящик появляется на конвейере то звонит звонок, что бы привлечь внимание Боба. Когда боб забирает ящик, то он нажимает кнопку и показывает, что по конвейеру можно передавать следующий ящик.
3. Диаграмма состояний
Опишем задачу в виде диаграммы состояний. В задаче есть два автомата (активных объекта)
.
Первый автомат BOB начинает свою жизнь в состоянии FREE (грузчик не занят работой и готов взять ящик). BOB периодически генерирует сигнал Timeout_sig. При этом он должен передать сигнал Timeout автомату CONVEYOR. CONVEYOR при получении сигнала Timeout должен перейти в состояние FULL (на конвейере появился ящик) и отправить BOB уведомление в виде сигнала Ready. BOB получив сигнал Ready должен перейти в состояние WORK (забрал ящик) и отправить назад сигнал Get. При получении Get объект CONVEYOR переходит в состояние EMPTY. BOB при генерации Timeout_sig возвращается в состояние FREE.
4. Разработка приложения
Основная задача разработчика на QP это по диаграмме состояний написать классы активных объектов. Объекты должны обрабатывать полученные сигналы и отправлять нужные сигналы при определенных условиях.
Содержимое проекта
Проект на QP обычно делится на такие части: 1) Глобальных переменные и константы – qpbob.h 2) Аппаратно-зависимый код – bsp.h, bsp.cpp 3) Активные объекты – bob.cpp, conveyor.cpp 4) Точка входа – main.cpp
Определяем сигналы
По диаграмме состояний определяем сигналы, которые есть в системе и определяем их виде перечисления в файле qpbob.h.
#ifndef qpbob_h
#define qpbob_h
//(1) Сигналы
enum DPPSignals {
READY = Q_USER_SIG,
GET,
TIMEOUT,
TERMINATE_SIG,
MAX_PUB_SIG,
MAX_SIG
};
//(2) Глобальные указатели на активные объекты
extern QActive * const AO_BOB;
extern QActive * const AO_CONVEYOR;
#endif
* This source code was highlighted with Source Code Highlighter.
Пояснения:
1) Первый пользовательский сигнал должен начинаться с константы Q_USER_SIG. Перечисление содержит ряд служебных сигналов: TERMINATE_SIG – сигнал завершения приложения, при получении, активные объекты должны корректно остановить свою работу; MAX_PUB_SIG – сигналы объявленные после этого могут передаваться только напрямую от объекта к объекту, а остальные сигналы доступны всем объектам системы; MAX_SIG – максимальный номер сигнала в системе. Тут не указан сигнал TIMEOUT_SIG так как он используется только внутри активного объекта Bob.
2) Глобальные указатели на активные объекты системы. Инициализируются при создании объектов.
Пишем аппаратно-зависимый код
QP можно использовать с большим количеством аппаратных платформ. Описание активных объектов и их взаимодействие не зависит от конкретной платформы. Код который зависит от аппаратуры лучше писать отдельно от активных объектов. В нашем примере активные объекты используют функции открытые в файле bsp.h
#ifndef bsp_h
#define bsp_h
//(1) Частота работы внутреннего таймера
#define BSP_TICKS_PER_SEC 1
//(2) Функции приложения привязанные к апаратной части
void BSP_init(int argc, char *argv[]);
void BSP_print(char const *str);
#endif // bsp_h
* This source code was highlighted with Source Code Highlighter.
Пояснения:
1) Задаем частоту работы внутреннего таймера системы.
2) Функция BSP_init инициализирует аппаратную часть. Функция BSP_print выводит в консоль текст проставляя временной штамп в виде количества десятых долей секунды. Реализация функций находится в файле bsp.cpp.
#include "qp_port.h"
#include "qpbob.h"
#include "bsp.h"
#include
#include
#include
#include
Q_DEFINE_THIS_FILE
//(1)Счетчик времени
unsigned int TimeCounter;
//(2)Функция ставит штамп времени
void BSP_timeStamp()
{
printf("%d - ",TimeCounter);
}
//(3) Признак работы idle-потока
static uint8_t l_running;
//(4) Потоковая функция idle-потока
static DWORD WINAPI idleThread(LPVOID par) {
TimeCounter=0;
(void)par;
l_running = (uint8_t)1;
while (l_running) {
Sleep(100);
TimeCounter++;
//(5)Если нажат Esc то программа посылает завершающий сигнал
if (_kbhit()) {
if (_getch() == '\33') {
QF::publish(Q_NEW(QEvent, TERMINATE_SIG));
BSP_print("Esc exit");
}
}
}
return 0;
}
//(6) Инициализация приложения
void BSP_init(int argc, char *argv[]) {
HANDLE hIdle;
//(7)Установка частоты внутреннего таймера
QF_setTickRate(BSP_TICKS_PER_SEC);
//(8) Создание idle потока
hIdle = CreateThread(NULL, 1024, &idleThread, (void *)0, 0, NULL);
Q_ASSERT(hIdle != (HANDLE)0);
SetThreadPriority(hIdle, THREAD_PRIORITY_IDLE);
printf("QP example"
"\nQEP %s\nQF %s\n"
"Press ESC to quit...\n",
QEP::getVersion(),
QF::getVersion());
}
//............................................................................
void QF::onStartup(void) {
}
//(9)..........................................................................
void QF::onCleanup(void) {
l_running = (uint8_t)0;
}
//............................................................................
void BSP_print(char const *str)
{
BSP_timeStamp();
printf("%s\n", str);
}
//(10)
void Q_onAssert(char const Q_ROM * const Q_ROM_VAR file, int line) {
fprintf(stderr, "Assertion failed in %s, line %d", file, line);
exit(0);
}
* This source code was highlighted with Source Code Highlighter.
Пояснения:
1) Переменная для подсчета времени работы от запуска системы.
2) Выводит на экран значение счетчика времени.
3) Пока значение переменной не 0 idle-поток работает.
4) В системе запускается idle-поток, для слежения за клавиатурой.
5) При нажатии Esc в систему отправляется сообщение для завершения работы TERMINATE_SIG.
6) Функция инициализирует приложение.
7) Частота работы таймера системы.
8) Запуск idle-потока.
9) После остановки системы закрывается idle-поток.
10) Реализация функции для вывода отладочной информации.
Запуск системы
Система запускается в функции main файла main.cpp. Нужно создать все необходимые объекты и передать управление активным объектам.
#include "qp_port.h"
#include "qpbob.h"
#include "bsp.h"
//(1) Очереди сигналов
static QEvent const *l_bobQueueSto[10];
static QEvent const *l_conQueueSto[10];
//(2) Список подписчиков на сигналы
static QSubscrList l_subscrSto[MAX_PUB_SIG];
//(3) Пул событий, здесь не используется
static union SmallEvents {
void *min_size;
} l_smlPoolSto[10];
int main(int argc, char *argv[]) {
//(4) Инициализируем BSP
BSP_init(argc, argv);
//(5) Инициализируем фреймворк
QF::init();
//(6) Настройка
QS_FILTER_ON(QS_ALL_RECORDS);
QS_FILTER_OFF(QS_QF_INT_LOCK);
QS_FILTER_OFF(QS_QF_INT_UNLOCK);
QS_FILTER_OFF(QS_QF_ISR_ENTRY);
QS_FILTER_OFF(QS_QF_ISR_EXIT);
QS_FILTER_OFF(QS_QF_TICK);
QS_FILTER_OFF(QS_QK_SCHEDULE);
//(7) Регистрируем пул событий
QS_OBJ_DICTIONARY(l_smlPoolSto);
//(8) Инициализируем список подписчиков
QF::psInit(l_subscrSto, Q_DIM(l_subscrSto));
//(9) Инициализируем пул событий
QF::poolInit(l_smlPoolSto, sizeof(l_smlPoolSto), sizeof(l_smlPoolSto[0]));
//(10) Запускаем активные объекты
AO_CONVEYOR->start(2,
l_conQueueSto, Q_DIM(l_conQueueSto),
(void *)0, 1024, (QEvent *)0);
AO_BOB->start(3,
l_bobQueueSto, Q_DIM(l_bobQueueSto),
(void *)0, 1024, (QEvent *)0);
//(11)Запускаем приложение
QF::run();
return 0;
}
* This source code was highlighted with Source Code Highlighter.
Пояснения: 1) Каждому активному объекту нужен массив для хранения сигналов. 2) Нужен массив для хранения подписок объектов на сигналы. Если объект хочет получать публичный сигнал он должен быть подписанным на него. 3) Сигналов с дополнительными данными нужен специальный пул (здесь не используется). 4) Инициализируем аппаратную часть. 5) Инициализируем систему. 6) Настройка системы. 7) Регистрируем пул событий. 8) Инициализируем список подписчиков. 9) Инициализируем пул событий. 10) Запускаем активные объекты. 11) Запускаем систему. Теперь активные объекты взаимодействуют между собой и приложение выполняется.
Активные объекты
Активные объекты системы пишутся на основе диаграммы состояний. Рассмотрим подробно реализацию объекта Bob файл bob.cpp. Активные объекты наследуются от класса QActive.
#include "qp_port.h"
#include "qpbob.h"
#include "bsp.h"
Q_DEFINE_THIS_FILE
class Bob : public QActive {
private:
//(1) собственное событие таймера
QTimeEvt m_timeEvt;
public:
Bob();
private:
//(2) Состояния
//Функции вызываются в определенном состоянии при получении сигнала
static QState initial(Bob *me, QEvent const *e);
static QState free(Bob *me, QEvent const *e);
static QState work(Bob *me, QEvent const *e);
};
static Bob l_Bob;
#define TIMEOUT_TIME 1
//(3) Перечисление для маркера внутреннего события таймера
enum InternalSignals {
TIMEOUT_SIG = MAX_SIG
};
QActive * const AO_BOB = &l_Bob;
//............................................................................
Bob::Bob() : QActive((QStateHandler)&Bob::initial),
m_timeEvt(TIMEOUT_SIG){//(4) Тут ставим конструктор внутреннего события таймера
}
//............................................................................
QState Bob::initial(Bob *me, QEvent const *) {
BSP_print("Bob initial");
//Перечисляем объекты
QS_OBJ_DICTIONARY(&l_Bob);
QS_OBJ_DICTIONARY(&l_Bob.m_timeEvt);
//Пеерчисляем состояния
QS_FUN_DICTIONARY(&QHsm::top);
QS_FUN_DICTIONARY(&Bob::initial);
QS_FUN_DICTIONARY(&Bob::free);
QS_FUN_DICTIONARY(&Bob::work);
//Перечиляем сигналы
QS_SIG_DICTIONARY(READY, me);
QS_SIG_DICTIONARY(TIMEOUT_SIG, me);
//(5)Подписываемся на сигналы
me->subscribe(READY);
me->subscribe(TERMINATE_SIG);
//(6)Запускаем внутренне событие таймера с задданым таймаутом
me->m_timeEvt.postIn(me, TIMEOUT_TIME);
//(7) Инициируем переход в другое состояние
return Q_TRAN(&Bob::free);
}
//............................................................................
QState Bob::free(Bob *me, QEvent const *e) {
BSP_print("Bob free");
//(8) Обработка сигналов
switch (e->sig) {
//(9)Стандартный сигнал входа в состояние
case Q_ENTRY_SIG: {
return Q_HANDLED();
}
//(10)Обработка нашего сигнала
case READY:
{
BSP_print("O my box READY. Going work");
return Q_TRAN(&Bob::work);
}
case TIMEOUT_SIG:
{
BSP_print("Tick Free - Bob");
//(11)Посылаем следующий тик таймера
me->m_timeEvt.postIn(me, TIMEOUT_TIME);
//(12)Посылаем событие без параметров
QEvent* be=Q_NEW(QEvent,TIMEOUT);
QF::publish(be);
return Q_HANDLED();
}
//(13)Обработка сигнала выключения
case TERMINATE_SIG: {
BSP_print("Bob terminate.----------------------------------------------");
QF::stop();//(14) Остановка приложения
//(15) Возвращаем признак обработаного сигнала
return Q_HANDLED();
}
}
return Q_SUPER(&QHsm::top);
}
QState Bob::work(Bob *me, QEvent const *e) {
BSP_print("Bob work");
switch (e->sig) {
case Q_ENTRY_SIG: {
QEvent* be=Q_NEW(QEvent,GET);
QF::publish(be);
BSP_print("Bob public GET");
return Q_HANDLED();
}
case TIMEOUT_SIG:
{
BSP_print("Tick Work - Bob");
BSP_print("I am going to free.");
me->m_timeEvt.postIn(me, TIMEOUT_TIME);
QEvent* be=Q_NEW(QEvent,TIMEOUT);
QF::publish(be);
return Q_TRAN(&Bob::free);
}
case TERMINATE_SIG: {
BSP_print("Bob terminate.----------------------------------------------");
QF::stop();
return Q_HANDLED();
}
}
return Q_SUPER(&QHsm::top);
}
* This source code was highlighted with Source Code Highlighter.
Пояснения 1) Bob должен периодически генерировать сообщение TIMEOUT_SIG для этого используем собственный таймер. 2) Активный объект всегда находится в одном из своих состояний. При получении сигнала он передается функции, которую нужно вызывать в этом состоянии. Функции состояний имеют свою специальную сигнатуру. Объект Bob имеет два состояния FREE и WORK но также нужно описать начальное состояние INITIAL для запуска объекта. 3) Для внутреннего таймера нужно создать внутренний сигнал. 4) Конструктор объекта должен вызывать конструктор таймера. 5) Объект должен подписаться на публичные сигналы, которые хочет получать. 6) Таймер запускается и через заданный интервал пришлет сигнал TIMEOUT_SIG. 7) Из состояния INITIAL переводим объект в состояние FREE. 8) В состоянии FREE при получении сигнала вызывается функция free в которой, нужно обработать сигнал. 9) Кроме пользовательский сигналов есть стандартные сигналы входа в состояние и выхода из состояния. 10) Пример обработки пользовательского сигнала. После обработки сигнала переходим в состояние WORK. 11) при получении сигнала TIMEOUT_SIG нужно перезарядить таймер, что бы он прислал TIMEOUT_SIG через заданный интервал. 12) Также посылается открытый сигнал в систему. Этот сигнал ждет объект Conveyor. 13) Если объект занимает некоторые ресурсы, то он должен обрабатывать сигнал остановки системы и корректно освобождать ресурсы. Активный объект, который был запущен последним (в нашем случае Bob) должен обработать сигнал завершения и остановить систему. 14) Остановка системы. 15) Если сигнал обрабатывается но не меняет состояние объекта, то нужно возвращать признак обработки. Объект Conveyor из файла conveyor.cpp проще и в нем Вы разберетесь сами.
#include "qp_port.h"
#include "qpbob.h"
#include "bsp.h"
Q_DEFINE_THIS_FILE
class Conveyor : public QActive {
private:
public:
Conveyor();
private:
//состояния
static QState initial(Conveyor *me, QEvent const *e);
static QState empty(Conveyor *me, QEvent const *e);
static QState full(Conveyor *me, QEvent const *e);
};
static Conveyor l_Conveyor;
QActive * const AO_CONVEYOR = &l_Conveyor;
//............................................................................
Conveyor::Conveyor() : QActive((QStateHandler)&Conveyor::initial)
{
}
//............................................................................
QState Conveyor::initial(Conveyor *me, QEvent const *) {
BSP_print("Conveyor initial");
QS_OBJ_DICTIONARY(&l_Conveyor);
QS_FUN_DICTIONARY(&QHsm::top);
QS_FUN_DICTIONARY(&Conveyor::initial);
QS_FUN_DICTIONARY(&Conveyor::empty);
QS_FUN_DICTIONARY(&Conveyor::full);
QS_SIG_DICTIONARY(GET, me);
QS_SIG_DICTIONARY(TIMEOUT, me);
me->subscribe(GET);
me->subscribe(TIMEOUT);
return Q_TRAN(&Conveyor::empty);
}
//............................................................................
QState Conveyor::empty(Conveyor *me, QEvent const *e) {
BSP_print("Conveyor empty");
switch (e->sig) {
case TIMEOUT:
{
BSP_print("Tick Empty - Conveyor");
BSP_print("Conveyor going to full.");
return Q_TRAN(&Conveyor::full);
}
}
return Q_SUPER(&QHsm::top);
}
QState Conveyor::full(Conveyor *me, QEvent const *e) {
BSP_print("Conveyor full");
switch (e->sig) {
//(1) Стандартный сигнал входа в состояние
case Q_ENTRY_SIG: {
QEvent* be=Q_NEW(QEvent,READY);
QF::publish(be);
BSP_print("Conveyor READY.");
}
case GET:
{
BSP_print("Bob GET box.");
return Q_TRAN(&Conveyor::empty);
}
}
return Q_SUPER(&QHsm::top);
}
* This source code was highlighted with Source Code Highlighter.
Этот пример разработки приложения на основе конечных автоматов для встроенных систем с использованием QP не претендует на реализм и полную корректность. Но думаю, поможет желающим освоить QP. Дальнейшее изучение QP можно продолжить на основе материалов с официального сайта.
Исходники: только код; проект в VS.
P.S. Поразило то, что статья осталась без внимания на Хабре.