Pull to refresh

Чуть больше о многозадачности в микроконтроллерах

Reading time 7 min
Views 7.5K

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


Был выбран микроконтроллер, с ядром из очень распространенного семейства ARM Cortex M. Из знакомых многим, а не только автору, вариантов с цифрами 0,3,4 и 7 был выбран M4, поскольку, оказался под рукой.


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


Пример на ассемблере был приведен, дабы показать, что на переключение тратится не более 80 тактов. И на 72-ух мегагерцах тактовой частоты получается чуть больше 1 микросекунды. Значит, тик, размером 50 микросекунд, будет не таким уж и дорогим. Всего 2 процента накладных расходов. Поэтому, как говорил один из любимых персонажей автора, «желательно помучиться».


Итак, у нас есть N задач, каждой из которых гарантирован для работы отрезок (тик T) времени и гарантировано повторение этого отрезка не позднее, чем через (N-1)T тиков плюс задержка, не превышающая D. Эта досадная задержка, к счастью, ограничена максимально возможным размером по времени, который равен сумме длительностей срабатывания всех разрешенных прерываний. Другими словами, больше всего не повезет той задаче, у которой перед очередным получением тика, случатся все возможные на данный период прерывания. На большее время задачу задержать не удастся. Она неизбежно получит свой слот времени не позднее, чем через (N-1)T+D микросекунд. По-моему, это называется hard real-time.


Задачи должны выполнять свои задания и докладывать об исполнении. Кому докладывать? Видимо, кому-то главному, а их бывает, как правило, значительно меньше, чем подчиненных (по правде сказать, автор встречал и исключения, когда на трех работников с обеими левыми руками приходилось четыре начальника, из которых только один знал и уважал слово «адекватность»).


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


Но наши не могут ждать! В смысле, задачи. Они из трудного реального времени. Предположим, две задачи считывают показания раз в одну секунду, а третья должна каждые 10 миллисекунд что-то мерить, складывать в стопочку, и докладывать наверх. А ей говорят: “Абажжите, мы не закончили с шефами”.


Видимо, придется обратиться, мягко говоря, к не совсем реальному времени (soft real-time).


Пускай у нас будет специальная задача, которая умеет ждать и любит это делать. Ресурсом, который она станет обслуживать, будет канал связи. В него, как известно, сразу все не впихнешь.
Зато, сразу можно прикинуть какая скорость должна быть у канала, чтобы ничего не потерялось. Для этого надо знать с какой производительностью работают все наши графоманы, тьфу, задачи. Очевидно, надо также вычислить размеры буфера или буферов, из которых все посылки будут отправляться наверх (или вправо).


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


Пару слов о канале связи с оператором, а проще говоря, с человеком. Здесь канал двунаправленный, но направление наружу наиболее интересно. Сразу оговорюсь, есть одно обстоятельство, которое, даже при сильном желании, исключить невозможно. Это перегрузка канала. При тестировании мы должны до нее дойти и должны иметь механизм увидеть ее наступление. Не спорю, обманывать не хорошо, но недоговаривать-то немножко можно. Вон, Герасим, этим вообще злоупотреблял.


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


Для примера, пусть мы имеем дуплексный последовательный интерфейс без квитирования со скоростью 115200 бод. Например, RS422 в комплектации “эконом” два провода – туда, два — обратно. Его возможность — это примерно 10000 байтов в секунду. Примем средний размер сообщения для человека равным 50 байтам. Получаем 200 сообщений в секунду или одно сообщение за 5 миллисекунд. Если у нас три задачи хотят что-то сообщать, то пусть они это делают раз в 15 миллисекунд каждая. В среднем, конечно. А если не в среднем, то потребуются серьезные статистические расчеты или натурный эксперимент. Выберем последнее. Мы, ведь, научились обнаруживать пропажу сообщений и все увидим на экране эмулятора терминала.
Итак, пусть три задачи создают индивидуальные сообщения. Пусть сообщения отличаются по важности или срочности содержания и наши задачи кладут их в соответствующий буфер. Выделим эти три кольцевых буфера для трех уровней срочности как показано на рисунке 1.



Четвертая задача выбирает из этих буферов сообщение по нами утвержденному плану и пытается поставить на отправку. Если пока отправка не представляется возможной, то четвертая задача оценивает сколько она может поспать и делает это. После сна у нее уже есть необходимое место в кольцевом буфере на отправку.


В буферах различной срочности, конечно, хранятся не сами сообщения, а их адреса (ссылки). При этом самим задачам совсем не требуется ждать. Нормально? Нет, не совсем. Так не работает, и вот почему. Каждый из этих трех кольцевых буферов являются разделяемым ресурсом. Представим, задача 1 собралась положить адрес в средний буфер. Она считывает слово, проверяет, что место пустое, то есть значение — нулевое и (в этот момент она сменяется другой задачей 2, которая хочет сделать точно так же и ей это удается) первая задача, вернувшись, кладет туда слово, затирая все, что удалось второй. Вот коллега просит слова. Я, кажется, знаю, что он скажет.
-Да все очень просто, можно, запретить прерывания на время проверки и ничего плохого не случится, это же совсем ненадолго.
-Верно, ненадолго, но сколько раз? Сколько времени мы отнимем, у задачи? И у которой из них? Забыл предупредить, мы не запрещаем прерывания никогда, нам запрещает это делать наша секта тяжелого реального времени (hard real-time).
-А если не запрещать прерывания, то можно попросить наш переключатель задач положить туда адрес сообщения. Он это может сделать атомарно.
-Да, может, но потом захочется его еще о чем-нибудь попросить, потом еще. И зачем тогда мы добивались 72-х градусов, чтобы потом разбавить все водой? Простите, я имел ввиду 72 такта на переключение контекста.
Попробуем поступить проще, как на рисунке 2.



В этом случае у каждой задачи есть свой буфер или свой набор буферов, если хочется разной срочности, напыщенности и важности. Лично мне, как простому оператору, пока хватает одинаковой важности для всех.


Такая схема не заставляет бороться за ресурс. Теперь у нас вполне рабочий вариант. Одно только не нравится. А что, если у задач слева на рисунке нет ничего на отправку? Тогда задаче справа разумнее будет попросить, чтобы ее разбудили, когда появится причина, а не просыпаться самой только для того, чтобы снова завести будильник. Задачам слева сподручнее это делать. К тому же, в прошлой заметке упоминалась функция, которая помогает разбудить товарища.


Предвижу рационализаторское предложение: «Пусть прерывание от последовательного порта (UART) само занимается тем, чем сейчас занимается задача 4, будет экономия». Сделать так можно, но не думаю, что это хорошо. Попробую пояснить. Задачи слева, действительно, могут сами активизировать процедуру прерывания UART, и она начнет работать и не остановится, пока все не сделает. Процедура прерывания должна теперь делать все то, что раньше делала задача 4. Время, затраченное на обработку прерывания, разбухнет, ни одна задача не сможет включиться, пока не закончится очередной «загул». И что мы скажем нашим товарищам из кружка упорного реального времени? А ведь нам говорили, что отклик на всякое внешнее прерывание должен быть максимально коротким. Это просто хороший тон. Или, другими словами: надо делать хорошо, плохо и без тебя получится.


На рисунке 3 поясняется, каков порядок действий и какие вызовы где расположены.



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


Сразу скажу — дурацкий пример. Не из-за гондольеро с их вечным “надо бы добавить, барин”. Нет, глупо, на самом деле, смешивать в одном интерфейсе такие разные по важности входные данные. Действительно, если тебе необходимо знать ускорение, то, наверняка, для того, чтобы быстро прикинуть, когда убрать педаль газа, или повернуть закрылки, или зажмурить глаза, наконец. Эта информация нужна часто. А вот давление, оно меняется медленно и придется пролететь вниз метра три, чтобы в младших разрядах затеплилась жизнь.


Что же касается сохраняемой памяти, а кто вообще ее на этот SPI посадил? А, второго SPI нет? И не предвидится? Деваться некуда, надо что-то делать. Перенаправим стрелки в противоположную сторону на рисунке 2 и начинаем думать.


Задача 4 теперь обслуживает SPI и просыпается только по его сигналам. Ее связь с задачей 1, которая хочет что-то положить в сохраняемую память, направлена наружу и осуществляется посредством очереди. Здесь также необходимо предусмотреть механизм, чтобы следить за переполнением кольцевого буфера. Добычу значений ускорения и давления задача 4 должна обеспечивать без участия двух потребляющих задач. Просто крутиться надо и успевать. Теперь можем набросать пояснительную картинку и написать пояснительную записку. На рисунке 4 эти
действия показаны схематично (или блок-схемно).



Проверка Underflow – эти действия помогают узнать, успевает ли значение ускорения смениться до того, как их снова прочитает потребляющая задача. Эта проверка показана отдельным действием на рисунке 4 только для того, чтобы заострить на ней внимание. На самом деле, этот шаг происходит вместе со считыванием значения акселерометра по схеме, как показано на рисунке 5.



Надо заметить, что здесь присутствует разделяемый ресурс, поскольку место хранения результата является еще и индикатором действий (семафором). Здесь возможны гонки, говоря языком схемотехники, но для нас это не является упущением. Ведь проскакивание в закрывающуюся дверь транспорта только в жизни можно посчитать за удачу. Здесь это уверенно будем считать опозданием.


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


Ну вот, осталось теперь найти что-нибудь подходящее из железа и поэкспериментировать как следует. Это, думаю, будет следующая история.

Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
0
Comments 9
Comments Comments 9

Articles