Операционная система из говна и палок

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

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *