Pull to refresh

STM32 и FreeRTOS. 2. Семафорим по-черному

Reading time 7 min
Views 65K
Часть первая, про потоки

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



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

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

bool sok=false;

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

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



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

Первая проблема в том, что приносящий сок постоянно спрашивает «сок принести надо?». Это раздражает продавца и нарушает первое правило программиста встроенных устройств «если функции нечего делать, то отдай управление планировщику». Казалось бы, ну воткнем 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, то есть я) и дает программисту по башке, ибо все подобные запреты и прочее очень сильно просаживают производительность и пускают под откос практически все, что завязано на временные промежутки.

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

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

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

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

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

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

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

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


Как понимаете, данные в конце дня совершенно не сойдутся. И именно поэтому у нас в студии запрещено использовать бинарные семафоры как класс — скорости контроллеров всегда разные (разница в скорости STM32L1/MSP430 на минимальной частоте и STM32F4 на максимальной больше двух порядков, а код работает один), а ловить такие ошибки очень сложно.

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

П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), но это те же самые бинарные семафоры. А рекурсивные мутексы — это счетные семафоры. Сделаны абсолютно так же, работают абсолютно так же, только «можно делать» состояние у них не «больше нуля», как у обычных, а «только ноль». Используются для разделения к ресурсам и переменным. Предлагаю придумать примеры применения самим (Тут почему-то все придумывают историю про туалет и ключ. Ни разу не было про «флаг передовика» или «печать фирмы». Видимо, специфика :)

Результат работы кода проще показать на видео, чем описывать словами



На этом этапе народ начинает спорить о применимости семафоров в уже написанном коде и обычно доходит до того, что в FreeRTOS называется event flags/bits/group. После краткого гуглежа на эту тему программисты расходится довольными и умиротворенными :)

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

Следующая часть, про очереди
Tags:
Hubs:
+23
Comments 5
Comments Comments 5

Articles