STM32 і FreeRTOS. 1. Розвага з потоками

Даний цикл з 5 статей розрахований на тих, кому стало мало можливостей звичних «тінек» і ардуинок, але всі спроби перейти на більш потужні контролери закінчувалися невдачею або не приносили стільки задоволення, скільки могли. Всі ниженаписанное обговорювалось мною багато разів «лікнепі» програмістів нашої студії (які часто зізнавалися, що перехід з «тінек» на «стмки» відкриває стільки можливостей, що потрапляєш в ступор, не знаючи, за що хапатися), тому смію сподіватися, що користь буде усім. При прочитанні мається на увазі, що читач — людина цікавий і сам зміг знайти і поставити Keil, STM32Cube і натискати кнопки «OK». Для практики я використовую оціночну плату STM32F3DISCOVERY, бо вона дешева, на ній стоїть потужний процесор і є купа светодиодиков.

Кожна стаття розрахована на «повторення» і «осмислення» десь на один околовечерний годину, бо будинок, сім'я або відпочинок…





Дуже часто (та що там часто, майже завжди) мікроконтролери застосовують в умовах, коли необхідно відстежувати відразу кілька параметрів. Або навпаки, управляти одночасно декількома пристроями.

Ось завдання для прикладу: у нас є 4 виходи, на яких необхідно виводити імпульси різної тривалості з різними паузами. Все, що у нас є — це системний таймер, який вважає в мілісекундах.

Ускладнюємо завдання в дусі «сам себе замучаю на ардуїнов». Таймери зайняті іншим, PWM не підходить, бо не на всіх ніжках він працює, та й не заженеш його на потрібні режими зазвичай. Трохи подумавши, сідаємо й пишемо приблизно такий код

// ініціалізація
int time1on=500; // Час, поки вихід 1 повинен бути включений
int time1off=250; // Час, поки вихід 1 повинен бути вимкнений
unsigned int now=millis();
....
// де-то в циклі
if(millis()<now+time1on)
{
port1=ON;
}
else
{
port1=OFF;
if(millis()>now+time1on+time1off)
{
now=millis();
}
}


І так або приблизно так для всіх 4 портів. Виходить пристойна онуча на кілька екранів, але ця онуча працює і працює досить швидко, що для мікроконтролера важливо.

Потім раптово програміст зауважує, що при кожному циклі смикається порт, навіть якщо його стан не змінюється. Править всю онучу. Потім число портів з такими ж потребами збільшується в два рази. Програміст плює і переписує все в одну функцію типу PortBlink(int port num).

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

Щастя? А ось фігу. Замовник щось отаке причепив і це лічену може легко загальмувати процес на секунди… Починається стогони, програмісти правлять в черговий раз код, остаточно перетворюючи його в нечитаний треш, менеджери викочують дикі прайси замовнику за додавання функціоналу, замовник матюкається і вирішує ніколи більше не зв'язуватися з вбудованими рішеннями.

(типу реклама і вихваляння) А все чому? Тому що спочатку було прийнято неправильне рішення про платформі. Якщо є можливість, ми пропонуємо наворочену платформу навіть для примітивних завдань. З досвіду вартість розробки і підтримки потім виявляються набагато нижче. Ось і зараз для управління 8мю виходами я візьму STM32F3, який може працювати на 72МГц. (пошепки) насправді просто у мене під рукою демоплата з ним (смаїл). Була ще з L1, але ми її ненавмисно використовували в одному з проектів.

Відкриваємо STM32Cube, вибираємо плату, включаємо галочку біля FreeRTOS і збираємо проект як зазвичай. Нам нічого такого не треба, тому залишаємо все за замовчуванням.

Що таке FreeRTOS? Це операційна система майже реального часу для мікроконтролерів. Тобто все, що ви чули про операційні системи типу багатозадачності, семафорів та інших мутексов. Чому FreeRTOS? Просто її підтримує STM32Cube ;-). Є купа інших подібних систем — та ж ChibiOS. По своїй суті вони всі однакові, тільки розрізняються командами та їх форматом. Тут я не збираюся переписувати гору книг і інструкцій по роботі з операційними системами, просто пробегусь широкими мазками по найбільш цікавим речам, які дуже сильно допомагають програмістам у їх нелегкій роботі.

Гаразд, буду вважати що прочитали в інтернеті і перейнялися. Дивимося, що змінилося

Десь на початку main.c

static void StartThread(const void * argument);


і після всіх инициализаций

/* Create Start thread */
osThreadDef(USER_Thread, StartThread, osPriorityNormal, 0, configMINIMAL_STACK_SIZE);
osThreadCreate (osThread(USER_Thread), NULL);

/* Start scheduler */
osKernelStart NULL, NULL);



І порожня StartThread з одним нескінченним циклом і osDelay(1);

Здивовані? А між тим перед вами практично 90% функціоналу, які ви будете використовувати. Перші два рядки створюють потік з нормальним пріоритетом, а останній рядок запускає в роботу планувальник завдань. І все це пишність укладається в 6 кілобайт флеша.

Але нам треба перевірити роботу. Міняємо osDelay на наступний код

HAL_GPIO_WritePin(GPIOE,GPIO_PIN_8,GPIO_PIN_RESET);
osDelay(500);
HAL_GPIO_WritePin(GPIOE,GPIO_PIN_8,GPIO_PIN_SET);
osDelay(500);


Компілюємо і заливаємо. Якщо все зроблено правильно, то у нас повинен замигать синій светодиодик (на STM32F3Discovery на PE8-PE15 розпаяна купка світлодіодів, тому якщо у вас інша плата, то змініть код)

А тепер візьмемо і растиражируем отриману функцію для кожного світлодіода.

static void PE8Thread(const void * argument);
static void PE9Thread(const void * argument);
static void PE10Thread(const void * argument);
static void PE11Thread(const void * argument);
static void PE12Thread(const void * argument);
static void PE13Thread(const void * argument);
static void PE14Thread(const void * argument);
static void PE15Thread(const void * argument);


Додамо потік для кожного світлодіода
osThreadDef(PE8_Thread, PE8Thread, osPriorityNormal, 0, configMINIMAL_STACK_SIZE);
osThreadCreate (osThread(PE8_Thread), NULL);


І перенесемо туди код для запалювання світлодіода
static void PE8Thread(const void * argument)
{
for(;;)
{
HAL_GPIO_WritePin(GPIOE,GPIO_PIN_8,GPIO_PIN_RESET);
osDelay(500);
HAL_GPIO_WritePin(GPIOE,GPIO_PIN_8,GPIO_PIN_SET);
osDelay(500);
}
}


Загалом все однотипно.

Компілюємо, заливаємо… і отримуємо фігу. Повну. Ні один світлодіод не блимає.

Шляхом страшної налагодження методом коментування з'ясовуємо, що 3 потоку працюють, а 4 — вже немає. В чому проблема? Проблема виділення пам'яті для шедулера і стека.

Дивимося в FreeRTOSConfig.h

#define configMINIMAL_STACK_SIZE ((unsigned short)128)
#define configTOTAL_HEAP_SIZE ((size_t)3000)


3000 байтів на все і кожної задачі 128 байт. Плюс ще десь треба зберігати інформацію про завдання та інше корисному. Ось тому, якщо нічого не робити, планувальник при нестачі пам'яті навіть не стартує.

Судячи з факам, якщо включити повну оптимізацію, то сам FreeRTOS візьме 250 байт. Плюс на кожну задачу по 128 байт для стека, 64 для внутрішнього списку і 16 для імені завдання. Вважаємо: 250+3*(128+64+16)=874. Навіть до кілобайта не дотягує. А у нас 3…

В чому проблема? Поставляється з STM32Cube версія FreeRTOS занадто стара (7.6.0), що б отримати vTaskInfo, тому я заходжу збоку:

Перед і після створення потоку я поставив наступне (fre — це звичайний size_t)

fre=xPortGetFreeHeapSize();


Встромляємо брекпоинты і отримуємо наступні цифри: перед створенням завдання було 2376 вільних байт, а після 1768. Тобто на одну задачу йде 608 байт. Перевіряємо ще. Отримуємо цифри 2992-2376-1768-1160. Цифра збігається. Шляхом простих логічних умовиводів розуміємо, що ті цифри з фака взяті для якогось дохлого процесора, з включеними оптимизациями та вимкненими всякими модулями. Далі дивимося і розуміємо, що старт шедулера отьедает ще приблизно 580 байт.

Загалом, приймаємо для розрахунків 610 байт на завдання з мінімальним стеком і ще 580 байт для самої ОС. Разом у TOTAL_HEAP_SIZE треба записати 610*9+580=6070. Округлимо і віддамо 6100 байт — нехай жирує.

Компілюємо, заливаємо і спостерігаємо, як блимають всі світлодіоди разом. Пробуємо зменшити стек до 6050 — знову нічого не працює. Отже, ми підрахували правильно :)

Тепер можна побавитися і позадавать для кожного светодиодика свої проміжки "імпульсу" і «паузи». В принципі, якщо оновити FreeRTOS або посидіти в коді, то легко дати точність на рівні 0,01 мс (за замовчуванням 1 твк — 1мс).

Погодьтеся, працювати з 8ю завданнями поодинці набагато приємніше, ніж в одній з 8ю одночасно? В реальності у нас в проектах зазвичай крутиться по 30-40 потоків. Скільки було б смертей програмістів, якщо всю їх обробку запхати в одну функцію я навіть підрахувати боюся :)

Наступним кроком нам необхідно розібратися з пріоритетами. Як і в реальному житті, деякі завдання «рівніші» інших і їм необхідно більше ресурсів. Для початку замінимо одну мигалку мигалкою ж, але зробленої неправильно, коли пауза робиться не засобами ОС, а простим циклом.

Тобто замість osDelay() вставляється ось такий жах.

unsigned long c;
for(int i=0;i < 1000000;i++)
{
c++;
}


Число циклів зазвичай підбирається експериментально (бо якщо таких затримок кілька, то купа головного болю в розрахунках забезпечено). Естети можуть підчитати час виконання команд.

Замінюємо, компілюємо, запускаємо. Светодиодики блимають і раніше, але якось мляво. Перегляд осцилографом дає зрозуміти, що замість рівних кордонів (типу 50мс горимо і 50мс не горимо), кордони стали плавати на 1-2мс (очей, як не дивно, це помічає). Чому? Тому що FreeRTOS не система реального часу і може дозволити собі такі вольності.

А тепер давайте піднімемо пріоритет цієї задачі на один крок, до osPriorityAboveNormal. Запустимо і побачимо самотньо миготливий світлодіод. Чому?

Тому що планувальник розподіляє завдання за пріоритетами. Що він бачить? Завдання з найвищим пріоритетом постійно вимагає процесор. Що в результаті? Іншим завданням часу на роботу не залишається.

А тепер знизимо пріоритет на один крок від нормального, до osPriorityBelowNormal. В результаті планувальник, давши попрацювати нормальним завдань, віддає ресурси, що залишилися «поганий».

Звідси можна легко вивести перше правило програміста: якщо функції нема чого робити, то віддай управління планувальником.

У FreeRTOS є два варіанти «почекай»

Перший варіант «просто почекай N тиків». Звичайна пауза, без будь-яких надмірностей: скільки сказали почекати, стільки і чекаємо. Це vTaskDelay (osDelay просто синонім). Якщо подивитися на час під час виконання, то буде приблизно наступне (приймемо корисна завдання виконується 24мс):

… [0ms] — передача управління — робота [24ms] пауза в 100мс [124ms] — передача управління — робота [148ms] пауза в 100мс [248ms]…

Легко побачити, що із-за часу, необхідного на роботу, передача управління відбувається не кожні 100 мс, як можна було б припустити. Для таких випадків є vTaskDelayUntil. З нею тимчасова лінія буде виглядати ось так

… [0ms] — передача управління — робота [24ms] пауза в 76мс [100ms] — передача управління — робота [124ms] пауза в 76мс [200ms]…

Як видно, отримує завдання управління у чітко визначені часові проміжки, що нам і потрібно. Для перевірки точності планувальника в одному з потоків я попросив робити паузи по 1мс. На картинці можете оцінити точність роботи з 9ю потоками (про StartThread не забуваємо)



На цьому я закінчую, бо народ настільки занурюється в гру з пріоритетами і з'ясуванням «коли воно зламається», що простіше замовкнути і дати побавитися.

Повністю зібраний проект з усіма вихідними кодами можна взяти за адресою kaloshin.ua/stm32/freertos/stage1.rar


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

0 коментарів

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