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

Частина перша, про потоки

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



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

Звичайний програміст, прочитавши попередню статтю, радісно сідає і пише приблизно такий код.

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);


Тобто якщо кнопка (а вона на платі прицелена до РА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.ua/stm32/freertos/stage2.rar

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.