На «Гиктаймсе» опубликовали конспект первой лекции курса Олега Артамонова по программированию микроконтролеров. Курс, конечно, немного экзотический в сравнении с любым интернетовским руководством по тем же 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.