Часть первая, про потоки

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



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

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

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

Следующая часть, про очереди