Обработка сообщений в ОСРВ на примере FreeRTOS

в 19:49, , рубрики: Без рубрики

Квадрат Здравствуйте. Данная статья описывает одну из возможных реализаций паттерна Handler для FreeRTOS, предназначенного для обмена сообщениями между потоками. Статья предназначена в первую очередь для людей использующих операционные системы в проектах для микроконтроллеров, энтузиастов DIY и людей изучающий ОСРВ и микроконтроллеры.
Предполагается, что читатель знаком с основными терминами относящимися к ОСРВ, такими как очередь и поток. Более подробно ознакомиться с FreeRTOS можно в постах qdx FreeRTOS: введение и FreeRTOS: межпроцессное взаимодействие.
Те кто участвовал а проектах для микроконтроллеров используя FreeRTOS, возможно сталкивался с тем, что стандартный API достаточно скуден, что приводит к необходимости написания дополнительного кода, который во многом повторяется. В моем случае ощущался недостаток инструментов для взаимодействия между потоками, а именно отсутствие унифицированной системы обмена сообщениями. Обычно для обмена инфомацией между потоками и синхронизации используются те или иные формы очередей. При этом тип информации содержащейся в очереди каждый раз разный, что снижает возможность повторного использования кода.
Использование унифицированной формы сообщения часто позволяет объединить несколько потоков в один Worker Thread, который обрабатывает полученные сообщения в порядке очереди.

Идея схожа с использование класса Handler в Android, поэтому названия бессовестно позаимствованы.
В основе подхода лежит использование для обработки нескольких типов сообщений одного потока, который извлекает сообщения из очереди, вызывает соответствующий обработчик и переходит к следующему сообщению.
Поток блокируется на очереди, таким образом если сообщений нет, управление передается другим потокам. Как только в очередь помещается новое сообщение, поток разблокируется и сообщение обрабатывается. Сообщения могут быть посланы обработчиками прерываний, другими потоками, другими Handler’ами или самому себе.

Как и любой поток, Worker Thread (или Looper) может быть вытеснен другим потоком с более высоким приоритетом. Использовапние несколький Looper’ов с разными приоритетами позволяет добиться своевременной обработки наиболее важных сообщений.

Диаграмма

Обработка сообщений в ОСРВ на примере FreeRTOS

Пример реализации

Рассмотрим изложенное на примере простой программы на С++. Я не буду приводить описание класса Thread, достаточно упомянуть что наследники Thread должны переопределить метод run(), который является телом потока.

Каждое сообщение это структура:

struct MESSAGE {
    /** Handler responsible for handling this message */ 
    Handler *handler; 
    /** What message is about */ 
    char what; 
    /** First argument */ 
    char arg1; 
    /** Second argument */ 
    char arg2; 
    /** Pointer to the allocated memory. Handler should cast to the proper type, 
     * according to the message.what */ 
    void *ptr; 
};

Пример реализации потока Looper:

Looper::Looper(uint8_t messageQueueSize, const char *name, unsigned short stackDepth, char priority): Thread(name, stackDepth, priority) {
    messageQueue = xQueueCreate(messageQueueSize, sizeof(Message));
}

void Looper::run() {
    Message msg;
    for (;;) {
        if (xQueueReceive(messageQueue, &msg, portMAX_DELAY)) {
            // Call handleMessage from the handler
            msg.handler->handleMessage(msg);
        }
    }
}

xQueueHandle Looper::getMessageQueue(){
    return messageQueue;
}

Пример реализации абстрактного Handler (не все методы):

Handler::Handler(Looper *looper) {
    messageQueue = looper->getMessageQueue();
}

bool Handler::sendMessage(char what, char arg1, char arg2, void *ptr) {
    Message msg;
    msg.handler = this;
    msg.what = what;
    msg.arg1 = arg1;
    msg.arg2 = arg2;
    msg.ptr = ptr;
    return xQueueSend(messageQueue, &msg, 0);
}

Пример реализации Handler:

Нужно переопределить один виртуальный метод, которой и будет вызывать Looper.

void ExampleHandler::handleMessage(Message msg) {
#ifdef DEBUG
    debugTx->putString("ExampleHandler.handleMessage(");
    debugTx->putInt(msg.what, 10);
    debugTx->putString(")n");
#endif
    TxBuffer *responseTx;
    switch (msg.what) {
    case EVENT_RUN_SPI_TEST:
        responseTx = (TxBuffer*)msg.ptr;
        testSpi();
        // Пример использования прикрепленного указателя
        responseTx->putString("Some responsen");
        break;

    case EVENT_BLINK:
         // Пример использования аргументов сообщения
        led->blink(msg.arg1, msg.arg2);
        break;
    }
}

Пример реализации main:

main используется для создания потоков, обработчиков и прочей инициализации.

int main( void ) {
    // Создание потока
    Looper looper = Looper(10, "LPR", 500, configNORMAL_PRIORITY);
    // Создание на нем обработчика
    ExampleHandler exampleHandler = ExampleHandler(&looper);
    // Создание интерпретатора команд
    CommandInterpreter interpreter = CommandInterpreter();
    // Регистрация обработчика. Теперь когда интерпретатор
    // получит команду Strings_SpiExampleCmd, он пошлет в
    // обработчик сообщение с темой EVENT_RUN_SPI_TEST
    interpreter.registerCommand(Strings_SpiExampleCmd, Strings_SpiExampleCmdDesc, &exampleHandler, EVENT_RUN_SPI_TEST);
    interpreter.registerCommand(Strings_BlinkCmd, Strings_BlinkCmdDesc, &exampleHandler, EVENT_BLINK);

    vTaskStartScheduler();
    /* Should never get here, stop execution and report error */
    while(true) ledRGB.set(PINK);
    return 0;
}

Исходники примера

Заключение

Использование такого подхода имеет ряд преимуществ:

  • Расширенить существующий Handler или добавить новый проще чем создать новый поток
  • Можно написать несколько компонентов для повторного использования, таких как интерпретатор коммандной строки или обработчик прерываний от кнопок, которые будут посылать сообщения зарегестрированным Handler’ам
  • Поскольку сообщения выполняются на одном потоке, отсутствует возможность гонок
  • Использование одного потока значительно снижает затраты памяти на стек
  • В процессе разработки Handler можно заменить на конечный автомат, который представляет собой несколько Handler’ов, по одному на каждое состояние
  • Время затраченное на обработку несколький сообщений одним потоком меньше, чем если бы каждый тип сообщения обрабатывался отдельным потоком за счет отсутствия переключений контекста

На обработчики сообщений (Handler) накладываются некоторые ограничения:

  • Обработчики не должны блокировать поток. Если происходить блокировка, то вся очередь сообщений будет ждать, а поток — простаивать
  • Также обработка сообщения не должназанимать слишком много времени
  • Сложнее предсказать время реакции на событие из-за того, что обработка сообщений проиходит по-очереди, а не пседво-одновременно (по time slice)

Конечно, не все потоки могут использовать предложенную модель. При необходимости жесткого реального времени не удастся иметь несколько Handler'ов на одном потоке (один — можно). Однако практика показывает, что все остальные потоки достаточно простые и практически не требуют взаимодействия с другими потоками. Это либо потоки, которые читают что-либо (из серийного порта или USB) и посылают сообщения ответственному обработчику, либо потоки выполняющие затратные по времени операции (вывод на дисплей). Основная же логика прошивки может быть успешно описана с помошью Handler’ов.

Спасибо за внимание.

Автор: yuriykulikov


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js