Как стать автором
Обновить

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

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

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

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

В первую очередь такой подход обеспечивает гибкость. Это позволяет создавать сложные инкапсулированные объекты, которые реагируют на множество событий. Пример из недавней практики — класс RFID считывателя, который изначально предполагал работу только с командной строкой. В последствие, Handler превратился в конечный автомат, а к сообщениям от командной строки добавились сообщения от кпопки, таймера, датчика движения и монитора уровня батарейки.
Диаграмма


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

Рассмотрим изложенное на примере простой программы на С++. Я не буду приводить описание класса 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 response\n");
        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’ов (по одному на каждое состояние)
  • время, затраченное на обработку несколький сообщений одним потоком, меньше, чем если бы каждый тип сообщения обрабатывался отдельным потоком за счет отсутствия переключений контекста

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

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

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн