На «Гиктаймсе» опубликовали конспект первой лекции курса Олега Артамонова по программированию микроконтролеров. Курс, конечно, немного экзотический в сравнении с любым интернетовским руководством по тем же STM32 — в нем рассматривается программирование с использованием операционной системы RIOT. Никакого вам CubeMX, никакой FreeRTOS — но в целом материал не особо привязан к конкретной ОС и «железу» и ориентирован скорее на то, чтобы продемонстрировать подходы к программированию для микроконтролеров «вообще».
Для тех, кому проще воспринимать видео — на Youtube выкладываются и видеозаписи лекций:
https://www.youtube.com/playlist?list=PLJEYfuHbcEIApuZR4L5tRiBCwTZCYeTNY
Но при всех заявленных и видимых достоинствах этих лекций, хвалить их целиком пока рано — поэтому перейду к всякой ерунде. Как водится, половина удовольствия от чтения «околоэлектронных» материалов на «Хабре» и «Гиктаймсе» — это комментарии, где обычно ссаными тряпками гоняют ардуинщиков. В этот раз к «гонимым» добавились также те, кто не осилил ничего, кроме всевозможных HAL и StdPeriphLib от производителя, и те, кто почему-то считает микроконтролером Raspberry Pi. Но все это не заслуживало бы упоминания — если бы не один комментарий:
…например, в Contiki — там многозадачность с инвалидностью третьей группы, там надо в треде либо без switch-case, либо без сообщений жить. Этому в университете всех учить не надо, кому в жизни не посчастливится — сами научатся.
https://geektimes.ru/company/samsung/blog/299187/#comment_10699171
Полез смотреть, что же за альтернативный подход к многозадачности исповедуют авторы операционной системы Contiki — и обнаружил там совершенно замечательную штуку. Оказывается, тамошнее подобие «потоков» обычной RTOS реализовано довольно необычно, исключительно средствами языка C.
Для начала — вот такой хитрый пример кода, который обычно называется Duff’s device — «Прием Даффа», в честь Тома Даффа, обратившего внимание на то, что метки в конструкции switch
языка C позволяют нарушать «блочную» структуру программы — например, перейти сразу внутрь цикла:
switch (count % 8) { case 0: do { *to = *from++; case 7: *to = *from++; case 6: *to = *from++; case 5: *to = *from++; case 4: *to = *from++; case 3: *to = *from++; case 2: *to = *from++; case 1: *to = *from++; } while ((count -= 8) > 0); }
Кстати говоря, Duff’s Device упомянут sharpc в известном «Теоретическом минимуме для программиста«. Конструкция довольно дикая, мало чем отличающаяся от GOTO — и хочу заметить, что особых преимуществ по скорости (в оригинале она использовалась для того, чтобы развернуть цикл в
memcpy
) на современных процессорах она не дает. Знать о ней, наверное, надо, а вот применять — только по необходимости.
А теперь сделаем еще один шаг вперед — обратите внимание, что такой переход позволяет «сохранить» текущее положение «внутри» выполняемой функции. Сначала пример без макросов (взятый со странички Адама Дункельса про protothreads и немного измененный):
volatile int counter; int example( int *lc ) { switch ( *lc ) { case 0: printf( "First run!\n" ); while ( 1 ) { *lc = 9; case 9: if ( !(counter > 10) ) return 0; printf( "Threshold reached\n" ); counter = 0; } } *lc = 0; return 2; }
Будем вызывать эту функцию примерно таким образом:
int main( void ) { int lc = 0; while ( 1 ) { example( &lc ); printf( "Back in main, counter = %d\n", counter ); counter++; } return 0; }
Обратите внимание, что строчка First run! напечатается только один раз, несмотря на то, что функция вызывается многократно. Фактически, таким нехитрым приемом реализован механизм ожидания событий — нетрудно догадаться, что в промежутках между вызовами нашей функции counter
может изменяться как угодно (на это я намекаю, объявив его volatile
). А теперь определим несколько макросов:
struct pt { int lc; }; #define PT_BEGIN(pt) switch((pt)->lc) { case 0: #define PT_WAIT_UNTIL(pt, c) pt->lc = __LINE__; case __LINE__: if(!(c)) return 0 #define PT_END(pt) } (pt)->lc = 0; return 2 #define PT_INIT(pt) (pt)->lc = 0
И перепишем пример, используя их:
volatile int counter; int example( struct pt *pt ) { PT_BEGIN( pt ); printf( "First run!\n" ); while ( 1 ) { PT_WAIT_UNTIL( pt, counter > 10 ); printf( "Threshold reached\n" ); counter = 0; } PT_END( pt ); } int main( void ) { struct pt example_pt; PT_INIT( &example_pt ); while ( 1 ) { example( &example_pt ); printf( "Back in main, counter = %d\n", counter ); counter++; } return 0; }
Хочу добавить, что в компиляторе из Microsoft Visual Studio этот пример не работает, если при компиляции указан параметр /ZI (он по умолчанию установлен для конфигурации Debug) — можно заменить его на /Zi.
Оцените, что получилось — исключительно средствами языка C реализован простенький, из говна и палок, механизм кооперативной многозадачности. Если добавить к нему планировщик и, скажем, какой-нибудь таймер, то получится та самая операционная система Contiki — названная в честь построенной из тех же говна и палок лодки Тура Хейердала. Теоретически, в Contiki есть и механизм вытесняющей многозадачности — но реализован он не для всех архитектур, в отличие от кооперативной, для которой достаточно лишь компилятора C. Это позволяет «портировать» ядро Contiki куда угодно.
Какие же у этого подхода недостатки? Начну с очевидного — использовать одновременно и protothreads, и конструкцию switch
нельзя. Другая нехорошая штука — при переходе потока в состояние ожидания и выходе в планировщик теряются значения всех локальных переменных внутри функции этого потока. Это довольно неприятно, так как требует постоянно держать в голове «нестандартное» поведение программы. Бороться с этим можно либо объявляя локальные переменные потока, как static
, либо используя глобальные переменные. Наконец, ждать событий можно только в основной функции потока, что резко ограничивает «полет фантазии» в реализации какой-то нетривиальной логики.
Впрочем, все это позволяет реализовать некоторое количество не очень сложных примеров — которые и составляют большую часть «дистрибутива» Contiki.