STM32 и FreeRTOS. 2. Семафорим.

Это черновая версия, финальная опубликована на хабре

В реальной жизни часто случается так, что некоторые события происходят с разной переодичностью. Скажем, заказ сока в “Макдональдсе”, нажатие кнопки пользователем или заказ лыж в прокате. А наш могучий микроконтроллер должен все это обрабатывать. Но как это сделать наиболее удобно?

Рассмотрим процесс заказа сока в макдаке. Покупатель говорит “хочу сок”, один продавец пробивает стоимость в чек, а другой смотрит в экранчик и идет наливать сок. Налил, принес и опять смотрит в экранчик, чего надо принести. И так продолжается до бесконечности или до конца смены. Мы заменяем человеческий труд машинным, поэтому автоматизируем это!

Обычный программист, прочитав предыдущую статью, радостно садится и пишет примерно такой код.

bool sok=false;

void thread1(void)
{
for(;;) if(user_want_juice) sok=true; else sok=false;
}

void thread2(void)
{
for(;;;) if(sok) prinesi_sok();
}

Логика думаю понятна: один поток контролирует ситуацию и ставит флаг, когда надо принести сок. А другой контролирует этот флаг и приносит сок, если надо. Где проблемы?

Первая проблема в том, что приносящий сок постоянно спрашивает “сок принести надо?”. Это раздражает продавца и нарушает первое правило программиста встроенных устройств “если функции нечего делать, то отдай управление планировщику”. Казалось бы, ну воткнем osDelay(1), что бы другие задачи отработали или просто понизим приоритет и пускай крутится, ведь железка-то железная выдержит … В том-то и дело, что не выдержит. Перегрев, недостаток ресурсов и так далее и тому подобное …

А вторая проблема зарыта гораздо глубже. Дело в компиляторах,оптимизациях и многопоточности. Вот для примера: у нас подносчик сока такой шустрый, что может обслужить сразу трех продавцов. В итоге легко может получиться классическая “гонка”, когда процессы передерутся между собой.

Продавец1 (далее П1) “Мне нужен сок!” sok=true;
Соконосец (далее С) (просыпаясь) “О! Сок нужен, пошел”
П2 “Мне тоже сок нужен” sok=true;
П3 “Мне тоже!” sok=true;
C (принес сок), П1 – на тебе сок. sok=false;

П2 и П3 в печали.

Программист думает, думает и придумывает бинарный счетчик вместо логического. Смотрит, что получилось.

П1 “Соку!” sok++;
C “Счас”
П2 “Соку!” sok++
П3 “Мне тоже два сока!” sok++;sok++;
С -П1 “на сок!” sok- -;
C (sok>0?) “О, еще надо!”
С – П2 “держи” sok- -;
C “и еще надо?”
С- П3 “велкам” sok- -;
C – и еще разок
С -п3 “пжлста” sok- -;
C “О, сока больше никому не надо, пойду спать”.

Код работает красиво, понятно и в принципе ошибок не должно быть. Программист закрывает задачу и приступает к следующей. В конце концов перед финальной сборкой проекта обычно начинается нехватка ресурсов и кто-то хитрый включает оптимизацию или просто меняет контроллер на многоядерный. И все. задача выше перестает работать как положенно. Причем что самое гадкое, иногда она работает как положено, а под отладчиком в 99% случаев она вообще ведет себя идеально. Программист плачет, рыдает и начинает следовать шаманским советам типа “объяви переменную как static или volatile”.

А что происходит в реальности?

Когда соконосец выполняет операцию sok- -; в реальности происходит следующее:

1. Берем во временную переменную значение sok
2. Уменьшаем значение временной переменной на 1
3. Записываем значение временной переменной в sok

А теперь вспоминаем, что у нас многопоточная (и может многоядерная) система. Потоки реально могут выполняться параллельно. И поэтому в реальности, а не под отладчиком, происходит следующее (или продавец и соконосец одновременно обратились к одной переменной)

С. Берем во временную переменную значение sok
П. Берем во временную переменную 2 значение sok;
С. Уменьшаем значение временной переменной на 1
П. Увеличиваем значение временной переменной 2 на 1.
П. Записываем значение временной переменной 2 в sok
С. Записываем значение временной переменной в sok

В результате в sok у нас совершенно не то значение, которое мы ожидали. Обнаружив данный факт, программист рвет на себе свитер с оленями, восклицая что-то типа “я же читал об этом, ну как я мог забыть!” и оборачивает код работы с переменной в обертку, которая запрещает многопоточный доступ к этой переменной. Обычно это связно с запретом переключения задач и прочими штуками. Запускает оптимизации – все работает отлично. Но тут приходит архитектор проекта (или главный технарь в studiovsemoe.com, то есть я) и дает программисту по башке, ибо все запреты и прочее очень сильно просаживают производительность и пускают под откос практически все, что завязано на временные промежутки.

Казалось бы, почти безвыходная ситуация, ибо придется переписывать кучу кода, да еще так, что бы не нарушить первое правило программиста … Но я внезапно подобрел и дал ссылку на статью, которую вы сейчас читаете.

В любой приличной ОС есть семафоры. В FreeRTOS есть два типа семафоров – “бинарный” и “счетный”. Все их отличие в том, что бинарный – это частный случай счетного, когда счетчик равен 1. Плюс бинарный очень опасно применять в высокоскоростных системах.

В чем преимущество семафоров против обычной переменной?

Во-первых, можно попросить планировщик задач не выделять ресурсов потоку, пока данный семафор не изменит свое состояние. То есть соконосец не будет больше терроризировать продавца “а нужен ли сок?”, а будет спать где-то в подсобке до тех пор, пока кому-то не понадобится сок.

И во-вторых, планировщик задач выполняет все необходимые проверки для сохранения целостности семафора и его значения. То есть можно запускать на один семафор несколько соконосцев, которые таскают сок с разной скоростью и они не передерутся между собой и не притащат сока больше, чем необходимо.

В чем проблема с высокоскоростными системами и бинарными семафорами? В принципе ситуация полностью аналогична первому примеру про соконосцев, только перевернутой с ног на голову.

Представим себе, что контролером в макдак взяли тормознутого паренька (далее К). Его задача простая – подсчитать, сколько сока заказали.

П1-С “Сок!”
К – “О, сок заказали, надо нарисовать единичку!”
П2-С “Сок!”
П3-С “Соку!”
(все это время К, высунув язык, рисует единичку)
К – Так, единичку нарисовали, ждем следующего заказа.

Как понимаете, данные в конце дня совершенно не сойдутся. И именно поэтому у нас в студии запрещено использовать бинарные семафоры как класс – скорости контроллеров всегда разные, а ловить такие ошибки очень сложно.

Как работают счетные семафоры? Возьмем для примера все тот же макдак, отдел приготовления бутербродов (или как там одним словом называются гамбургеры с бигмаками?). С одной стороны есть куча продавцов, которые продают бигмаки. Бигмаки продаются по одному, по два или по десятку разом. В общем, не угадаешь. А с другой стороны есть делатель бигмаков. Он может быть одним, молодым и неопытным, а может быть матерым и быстрым. Или все сразу. Продавцу на это пофиг – ему нужен бигмак и кто его сделает ему все равно. В результате:

П1 “Нужен 1 бигмак” (ставит семафор в 1ку)
Д1 “Ок, я могу делать 1 бимак”. (молодой, оказался ближе, снимает семафор в 0)
П2 “Нужно 3 бигмака” (увеличивает семафор на 3)
Д2 “Ок, я могу сделать 1 бигмака” (следующим в очереди на ожидание. семафор в 2)
(тут приходит Д1)
Д1 “сделал бигмак, еще один могу сделать” (семафор 1)
Д2 “ок, я свой сделал, сделаю счас еще один”. Семафор 0
(приходит назад, он быстрый)
Д2 “Еще бигмаки надо? я подожду 10 тиков, если нет, то уйду”
Д1 “Все, сделал. Разбудите, как еще надо будет” (планировщик тормозит тред)
Д2 “Чего, не надо? ну я ушел. загляну через Нцать тиков”

В итоге бигмаки может делать один человек, а могут 10 – разница будет только в числе произведенных бигмаков в единицу времени. Ладно, хватит про бигмаки и макдональдсы, надо реализовывать все это в коде. Опять же берем плату и код из прошлого примера. У нас 8 светодиодов, которые мигают по разному, с разной скоростью. Вот пусть будет один сделанный “мырг” равен одному “бутерброду”. На плате есть пользовательская кнопка, поэтому сделаем так, что бы одно нажатие требовало 1 “бутерброд”. А если кнопку держим, что пусть требует по 5 “бутербродов в секунду”.

Где-нибудь в “глобальном” коде создаем семафор.

xSemaphoreHandle BigMac;

В коде треда StartThread инициализируем семафор

BigMac = xSemaphoreCreateCounting( 100, 0 );

То есть максимум мы можем заказать 100 бигмаков, а сейчас их надо 0

И изменим код бесконечного цикла на следующий

if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)==GPIO_PIN_SET)
{
xSemaphoreGive(BigMac);
}
osDelay(200);

То есть если кнопка (а она на плате прицелена к PА0) нажата, то каждые 200мс мы выдаем один семафор/требуем бигмак.

И к каждому коду мигания светодиодиком добавим

xSemaphoreTake( BigMac, portMAX_DELAY);
HAL_GPIO_WritePin(GPIOE,GPIO_PIN_15,GPIO_PIN_RESET);
...

То есть ждем семафора BigMac до опупения (portMAX_DELAY). Можно поставить любое число тиков или через макрос portTICK_PERIOD_MS задать число миллисекунд для ожидания.

Внимание! В коде я не стал вводить никаких проверок для повышения его читабельности.

Компилируем, запускаем. Чем дольше держим кнопку, тем больше светодиодиков мигает. Отпускаем – перестают. Но один, самый быстрый (и дальний в очереди) у меня не замигал – ему просто не хатает “заказов”. Ок, увеличиваю скорость до 50мс на каждый бутерброд. Теперь заказов хватает всем и все мигают. Отпускаешь кнопку – они некоторое время продолжают мигать, делая собранные заказы. Что бы было совсем хорошо, я разрешил заказывать бигмаков аж 60 тыщ (можно до unsigned long) и период заказа поставил 10мс.

Теперь все стало совсем красиво – нажал, светодиодики замигали. Чем дольше держишь кнопку, тем дольше мигают светодиодики после отпускания. Полная аналогия реальной жизни.

Что бы продолжить аналогию с реальной жизнью, вспомним, что это в макдаке всегда есть какие-то сборщики бутербродов. То есть продавец может не оборачиваясь, махнуть “надо бутерброд” и кто-нибудь его сделает. А если это обычная столовая в необеденное время? Там кассирша может хоть обмахаться – никто просто не увидит, ибо все кроме нее смотрят очередной сериал. Кассирше надо понять, чего хочет забредший в неурочное время посетитель и крикнуть что-то типа “Татьяна Васильевна, выйдите пожалуйста, тут суп налить надо”.

Для таких адресных случаев семафоры использовать нет смысла. В старый версиях FreeRTOS можно было просто через API разбудить задачу (“там суп надо”), а в новых появился вызов vTaskNotify (отличие только в передаваемом параметре “там борщ надо”), использование которого полностью аналогично семафорам, но адресно. По сравнению с обычными обещают дикое повышение производительности, но на данный момент мы масштабных тестов не проводили.

Есть еще один подвид семафоров – мутексы (mutex), но это те же самые бинарные семафоры. А рекурсивные мутексы – это счетные семафоры. Сделаны абсолютно так же, работают абсолютно так же, только “можно делать” состояние у них не “больше нуля”, как у обычных, а “только ноль”. Используются для разделения к ресурсам и переменным. Предлагаю придумать примеры применения самим.

Результат работы кода

Как обычно, полный код с обновлениями из поста можно найти тут http://kaloshin.ru/stm32/freertos/stage2.rar