Здравствуйте. Данная статья описывает одну из возможных реализаций паттерна Handler для FreeRTOS, предназначенного для обмена сообщениями между потоками. Статья предназначена в первую очередь для людей использующих операционные системы в проектах для микроконтроллеров, энтузиастов DIY и людей изучающий ОСРВ и микроконтроллеры.
Предполагается, что читатель знаком с основными терминами относящимися к ОСРВ, такими как очередь и поток. Более подробно ознакомиться с FreeRTOS можно в постах qdx FreeRTOS: введение и FreeRTOS: межпроцессное взаимодействие.
Те кто участвовал а проектах для микроконтроллеров используя FreeRTOS, возможно сталкивался с тем, что стандартный API достаточно скуден, что приводит к необходимости написания дополнительного кода, который во многом повторяется. В моем случае ощущался недостаток инструментов для взаимодействия между потоками, а именно отсутствие унифицированной системы обмена сообщениями. Обычно для обмена инфомацией между потоками и синхронизации используются те или иные формы очередей. При этом тип информации содержащейся в очереди каждый раз разный, что снижает возможность повторного использования кода.
Использование унифицированной формы сообщения часто позволяет объединить несколько потоков в один Worker Thread, который обрабатывает полученные сообщения в порядке очереди.
Идея схожа с использование класса Handler в Android, поэтому названия бессовестно позаимствованы.
В основе подхода лежит использование для обработки нескольких типов сообщений одного потока, который извлекает сообщения из очереди, вызывает соответствующий обработчик и переходит к следующему сообщению.
Поток блокируется на очереди, таким образом если сообщений нет, управление передается другим потокам. Как только в очередь помещается новое сообщение, поток разблокируется и сообщение обрабатывается. Сообщения могут быть посланы обработчиками прерываний, другими потоками, другими Handler’ами или самому себе.
Как и любой поток, Worker Thread (или Looper) может быть вытеснен другим потоком с более высоким приоритетом. Использовапние несколький Looper’ов с разными приоритетами позволяет добиться своевременной обработки наиболее важных сообщений.
Диаграмма
Пример реализации
Рассмотрим изложенное на примере простой программы на С++. Я не буду приводить описание класса 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