STM32 и FreeRTOS. 5. Приносим пользу и добро!

Как-то раз попросили меня посмотреть на одно очень дорогостоящее устройство. Проблема была одна: среди использующих это устройство возникло стойкое убеждение, что 99,99% его цены происходит от того факта, что производитель этого устройства монополист в своей сфере и деваться пользователям этого устройства некуда.

Вооружившись осциллографом, я полез внутрь.

Через некоторое время поиски привели к двум проводкам, которые были в жгуте, соединявшем блоки устройства. Осциллограмма показала, что в проводках почти обычный USART. Почти — потому что «туда» данные бежали на скорости 9600, а обратно на 115200.

Продолжение тут

STM32 и FreeRTOS. 4. Шаг в сторону HAL

Одним из основных препятствий для перехода на STM32 является обилие текстов, инструкций и мануалов, описывающих работу с контроллером. Виновником этого обилия стала сама STMicroelectronics, которая поначалу планомерно запутывала своих пользователей, а затем предлагала неверные варианты выхода.

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

Но не так давно ST поняла, в какую яму она угодила и начала усиленно из нее выбираться, привлекая новые силы. И именно благодаря этому сейчас время старта сократилось до несуразно маленьких величин. Как это выглядит на практике?

Статья на хабре

STM32 и FreeRTOS. 3. Очереди

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

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

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

Пойдем со стороны пользователя. Com-порт, или USART (universal asynchronous receiver/transmitter) – штука очень нежная и капризная. Основной каприз заключается в том, что ее нельзя оставлять без внимания. По одной простой причине – для экономии выводов в 99% случаев выводят только сигналы приема и передачи, оставляя сигналы разрешения приема и передачи за бортом. И стоит микропроцессору хоть чуть-чуть замешкаться, как символ будет потерян. Судите сами: магические цифры 9600/8N1 говорят нам, что в секунду по линии летит 9600 бод, а один символ кодируется 9 импульсами. В итоге максимальная скорость передачи составляет 9600/9 = 1066 байт в секунду. Или чуть меньше одной миллисекунды на байт. А если по ТЗ скорость передачи 115200? Там может прилететь 128 байт за миллисекунду. А ведь микроконтроллеру надо еще обработать эти данные. Кажется, что все плохо, но в реальности куча устройств работает и не доставляет проблем. Какие же решения применяются?

Cамым распространенным (и правильным) решением является перекладка всей работы по приему или передаче символов на плечи микроконтроллера. Делать аппаратный usart порт научились сейчас все, поэтому типовое решение в большинстве случаев выглядит вот так:

void URARTInterrupt()
{
a=GetCharFromUSART();
if(a!=0x13)
buffer[count++]=a;
else
startProcessing();
}

Где проблема? Во-первых, проблема в вызове startProcessing. Стоит ей хоть чуть-чуть задержаться с работой, как очередной символ будет потерян. STM32L1 на минимальной частоте успевает за 1мс обработать 84 команды. В итоге при более-менее развесистой логике полученная конструкция будет терять символы.

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

Обычно где-то на этом месте я замечаю удивленные глаза народа и возмущенные выкрики в духе “ну такие алгоритмы же работают в куче проектов и ничего, никаких проблем”. Да, работают. Но в каких проектах? Только в тех, где можно все общение с контроллером перевести на синхронную логику. Вот пример диалога пользователя (П) и контроллера (К)

П: ATZ (Контроллер, ты живой?)
К: ОК (Ага, я тут)
П: М340,1 (Сделай чего-то)
(тут может быть пауза, иногда очень большая)
К: ОК (Сделал типа)

Где проблемы? Во-первых, нельзя послать несколько команд подряд. Значит либо надо будет менять логику или делать сложные команды с несколькими параметрами. Во-вторых, между запросом и ответом не может быть никаких других запросов и ответов. А вдруг пользователь в процессе движения нажмет кнопку? Что делать? Выход только один – использовать родную природу порта, а именно его асинхронность. В результате диалог между пользователем и контроллером выглядит примерно так

П: ATZ (жив?)
К: ОК (ага)
П: M340,1
К: К1=1 (кнопку нажали)
П: Е333,25,2
(тишина)
К: Е=1 (задачу Е выполнил)
К: М=1 (задачу М выполнил)

Конечно, логика обработки подобного потока немного усложняется, зато благодаря такой асинхронности мы сразу получаем множество преимуществ перед “классической школой”.

Во-первых, резко повышается отзывчивость интерфейса с пользователем. Ну работает там где-то моторчик или считается что-то, но это же не повод “замирать” или “тормозить”. А когда пользователь понимает, что величина тормозов практически не зависит от мощности контроллера, у него возникают резонные вопросы …
Во-вторых, легко реализовать совместное управление. Скажем, у меня на плате 8 выводов. Легко можно сделать так, что бы первые 3 управлялись через USART1, вторые 3 через USB-CDC, а последние два – совместно. И все это на одном контроллере.
И наконец, очень легко отвязать логику от аппаратного интерфейса. Нам все равно, откуда приходят команды – от соседнего процесса, от пользователя или вообще от другого контроллера.

А теперь со всеми этими задумками взглянем на противоположную сторону – на сторону исполнителя. Он достаточно медлителен, что бы попросту не успеть обработать команды по порядку. И время запуска-остановки моторчика очень большое, поэтому хорошо бы сделать примитивную оптимизацию в духе “если поступило две команды на поворот в одну сторону, то поверни за раз”.

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

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

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

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

Где-то в начале кода определим очередь

xQueueHandle q;

Код светодиодов поменяем по принципу “в очереди больше Н элементов? зажигаем, если нет, то нет”

if(uxQueueMessagesWaiting(q)>1)
HAL_GPIO_WritePin(GPIOE,GPIO_PIN_9,GPIO_PIN_SET);
else
НAL_GPIO_WritePin(GPIOE,GPIO_PIN_9,GPIO_PIN_RESET);
osDelay(100);

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

q = xQueueCreate( 8, sizeof( unsigned char ) );

Ну и код кнопки для помещения символов в очередь

if(HAL_GPIO_ReadPin(GPIOA,GPIO_PIN_0)==GPIO_PIN_SET)
{
unsigned char toSend;
xQueueSend( q, ( void * ) &toSend, portMAX_DELAY );
}
osDelay(500);

Чего не хватает? Воркера, который забирает из очереди задания. Пишем.

static void WorkThread(void const * argument)
{
for(;;)
{
unsigned char rec;
xQueueReceive( q, &( rec ), portMAX_DELAY );
osDelay(1000);
}
}

Как видите, все параметры аналогичны тем, что используются в семафорах. Результат проще показать.

Как обычно, использующийся код доступен по адресу http://kaloshin.ru/stm32/freertos/stage3.rar

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

STM32 и FreeRTOS. 1. Развлекаемся с потоками

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

IMG_0278

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

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

Ладно, буду считать что прочитали в интернете и прониклись. Смотрим, что поменялось

Где-то в начале

static void StartThread(void const * 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(void const * argument);
static void PE9Thread(void const * argument);
static void PE10Thread(void const * argument);
static void PE11Thread(void const * argument);
static void PE12Thread(void const * argument);
static void PE13Thread(void const * argument);
static void PE14Thread(void const * argument);
static void PE15Thread(void const * argument);

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

И перенесем туда код для зажигания светодиода
static void PE8Thread(void const * 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() вставляется вот такой вот ужас.

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 не забываем)

1ms

Думаю, для первой части достаточно 🙂

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