Здравствуйте. Данная статья описывает одну из возможных реализаций паттерна 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’ов.
Спасибо за внимание.
