17 April 2014

Time Triggered design — еще один подход к проектированию ПО для встраиваемых систем

Programming microcontrollers
Когда-то давно я написал статью о принципах проектирования приложения для встраиваемых систем. Тогда я сказал, что есть два основных принципа — бесконечный цикл и ОС реального времени. А вот совсем недавно услышал, что есть еще и третий подход — так называемый Time Triggered Design.

В качестве ознакомления с подходом был использована книга «Patterns for time-triggered embedded systems» автора Michael J. Pont, для заинтересовавшихся — www.safetty.net/publications/pttes
Попытаюсь здесь коротко изложить концепцию.

В основе концепции лежат следующие идеи:
  • в системе есть единственное периодическое прерывание — tick;
  • задачи не имеют приоритетов;
  • управление следующей задаче передается только после завершения выполняющейся сейчас задаче.

Такой набор принципов еще называют кооперативным планировщиком задач (cooperative scheduler). Классические RTOS используют то, что называется планировщиком с вытеснением (preemptive scheduler).

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

Псевдокод, демонстрирующий этот принцип.

void main(void)
{
    scheduler_init();
    add_task(Function_A, 2);
    add_task(Function_B, 10);
    add_task(Function_C, 15);
    scheduler_start();
    while(1)
    {
        dispatch_tasks();
    }
}


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

Структура, описывающая контекст задачи:

typedef struct
{
    void (* pTask)(void);
    uint32 Period;
    uint32 PeriodCur;
    uint8 RunMe;
} task_descriptor_t;


Действительно, по сравнению с RTOS, «накладных расходов» гораздо меньше — указатель на функцию, периодичность запуска, текущее значение — сколько еще тиков ждать запуска, и сколько раз задача должна быть запущена.

task_descriptor_t all_task_list[MAX_TASKS];


Список задач — обычный массив заранее заданной длины.

Сам шедулер вешается на прерывание таймера, настроенное происходить с заданной периодичностью, например, 1 мс — тот самый тик.

void scheduler_update(void) interrupt 
{
    foreach (task in all_task_list)
    {
        task.PeriodCur--;
        if (task.PeriodCur == 0)
        {
            task.PeriodCur = task.Period;
            task.RunMe++;
        }
    }
}


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

И наконец — диспетчер. Который крутится в бесконечном цикле.

void dispatch_tasks(void)
{
    foreach (task in all_task_list)
    {
        if (task.RunMe > 0)
        {
            task.pTask();
            task.RunMe--;
        }
    }
}


Все так же идем по списку задач, и если у задачи счетчик запусков больше нуля, запускаем эту задачу, непосредственно вызывая ее функцию, и декрементируем счетчик запусков.

Собственно все!

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

Но мне не нравится. И вот почему.

  1. Требование одного и только одного прерывания в системе означает, что вся работа с периферией должна происходить в polling mode. Что накладывает свои ограничения. И снижает время реакции системы.
  2. Проход по списку задач. Т.е. перед тем, как управление дойдет до последней задачи в списке, в худшем случае будут вызваны все предыдущие задачи списка. Время реакции на внешнее событие снова далеко не самое предсказуемое.
  3. Если что-то случится с одной из задач — до последней в списке дело может и не дойти. У нас же кооперативный режим, и каждая задача должна сама возвращать управление диспетчеру!
  4. Ограничение на время выполнения отдельно взятой задачи. Из которого следует, что в случае любого более-менее продолжительного действия нам вместо простого линейного кода придется городить машины состояний.


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

/*------------------------------------------------------------------*-
SPI_X25_Write_Byte()
Store a byte of data on the EEPROM.
-*------------------------------------------------------------------*/
void SPI_X25_Write_Byte(const tWord ADDRESS, const tByte DATA)
{
    // 0. We check the status register
    SPI_X25_Read_Status_Register();

    // 1. Pin /CS is pulled low to select the device
    SPI_CS = 0;

    // 2. The 'Write Enable' instruction is sent (0x06)
    SPI_Exchange_Bytes(0x06);

    // 3. The /CS must now be pulled high
    SPI_CS = 1;

    // 4. Wait (briefly)
    SPI_Delay_T0();

    // 5. Pin /CS is pulled low to select the device
    SPI_CS = 0;

    // 6. The 'Write' instruction is sent (0x02)
    SPI_Exchange_Bytes(0x02);

    // 7. The address we wish to read from is sent.
    // NOTE: we send a 16-bit address:
    // - depending on the size of the device, some bits may be ignored.
    SPI_Exchange_Bytes((ADDRESS >> 8) & 0x00FF); // Send MSB
    SPI_Exchange_Bytes(ADDRESS & 0x00FF); // Send LSB

    // 8. The data to be written is shifted out on MOSI
    SPI_Exchange_Bytes(DATA);

    // 9. Pull the /CS pin high to complete the operation
    SPI_CS = 1;
}


Простой и ясный линейный код, вроде бы. Но при попытке его применить в описанном выше дизайне проявятся следующие проблемы:
  • SPI_Exchange_Bytes() использует внутри себя блокирующие циклы ожидания готовности периферии — у нас же только polling, помните? (код приводить не буду, его и так уже много здесь, просто поверьте, что они там есть). А т.к. периферия может вдруг отказать, для каждого цикла ожидания устанавливается тайм-аут. Который в этой функции равен аж 5 мс! В итоге в простой функции у нас есть пять вызовов функции обмена байт по SPI, каждый из которых в худшем случае может занять 5 мс. Помните про требование, чтобы задача завершалась за время, значительно меньшее, чем время одного тика (которое у нас 1 мс)? И что же, теперь вместо простого и ясного кода работы с SPI EEPROM мне придется писать сложную машину состояний так, чтобы за один вызов не передавалось более одного байта? И все равно даже один вызов SPI_Exchange_Bytes() сможет занять 5 мс при неблагоприятном раскладе, а для дальнейшего уменьшения возможной задержки придется переписать и еще более простую функцию SPI_Exchange_Bytes() так, чтобы тайм-аут в ней вызывался не одним куском в 5 мс, а маленькими кусочками по 100 мкс при каждом вызове? Единственное, что мне хочется сказать: «Они это серьезно?».
  • Вот мне в моем реальном проекте необходимо по SPI в FLASH передавать 1 мегабайт данных. Теперь посчитаем — если вызов задачи произойдет раз в миллисекунду, а за один вызов я не смогу передать более чем 1 байт информации — сколько времени я буду передавать 1 мегабайт? Обойти это, конечно, можно, передавая не один байт, а несколько, но при этом код усложнится еще больше — мне ведь еще придется следить, чтобы суммарное потраченное время не превышало, скажем, 300 мкс, т.к. требование, чтобы вызов задачи был короче тика, все еще в силе!
  • Не говоря уже о том, что при возможности включить прерывание от SPI задача упростится еще больше — я просто запишу блок данных для передачи в буфер, пошлю первый байт, а в обработчике прерывания об окончании отправки пошлю следующий байт из буфера. Но второе прерывание в системе сломает основу Time triggered design, так что про это придется забыть.


Ну и как-то странно выглядит уже то, что авторы почему-то не привели пример кода вышеприведенного примера работы с SPI, адаптированного к своему же дизайну. Испугались, наверное.

Рассматривается еще и т.н. гибридный шедулер, когда разрешается еще одно прерывание, в контексте которого может выполняться еще одна, высокоприоритетная задача. Но все равно общей сути это не меняет.

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

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

Но для ситуаций, где событий больше, где гораздо проще использовать прерывания для работы с периферией, и где время реакции и стабильность более-менее критичны, и где нужно использовать производительность контроллера по максимуму — я все же останусь сторонником использования RTOS. Пусть в системе будет определенная непредсказуемость и необходимость грамотного использования средств синхронизации — выгод от более строгого отделения задач друг от друга и от особенностей шедулера все равно, как мне кажется, больше.
Tags:микроконтроллерыrtosпроектированиеtime triggered design
Hubs: Programming microcontrollers
+4
4.4k 13
Comments 4
Popular right now
Art Director/Head of Design Department
from 240,000 ₽Direct Line DevelopmentМосква
Angular UI Team Lead
from 4,000 to 5,000 $AWWCOR Inc.Remote job
Product designer with interaction skills
from 2,000 to 3,000 $StarOfServiceRemote job
Senior UI Angular Developer
from 3,600 to 4,300 $AWWCOR Inc.Remote job