Available Languages?:

TNKernel : Часто задаваемые вопросы

Документация

Где скачать документацию в PDF?

Документацию PDF можно скачать по ссылке

Объем стека задачи

Какими средствами возможно просчитать необходимый объем стека для задачи?

Только эмпирически, исходя из конфигурации системы. Можно выделить для начала стек побольше, а потом в ходе работы контролировать его заполняемость отладчиком (стек перед запуском задачи инициализируется значениями 0xFFFF, в ходе работы видно как он заполняется).

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

Если используются "тяжелые" функции из стандартных библиотек (printf() и т. п.) - обязательно нужно контролировать, какой объем стека требуют они. Как правило, довольно много.

Если говорить о TNKernel, то для среднего приложения можно установить размер стека задачи, равный 128 слов (256 байт). Исходя из контроля стека в ходе отладки, обычно эта цифра уменьшается.

Переполнение стека

Отслеживается ли ситуация, когда задача «вылезла» за отведенную для нее область стека? Если да, то что будет 1) с этой задачей. 2) с другими задачами?

У микроконтроллеров PIC24/dsPIC есть механизм аппаратного контроля указателя стека – если значение указателя становится больше чем величина регистра SPLIM, возникает немаскируемое исключение (прерывание).

В TNKernel SPLIM сохраняется индивидуально для каждой задачи, что позволяет в реальном времени отслеживать переполнение стека. Отслеживания опустошения стека нет, да оно и не нужно, так как может возникнуть только если само ядро работает неправильно (а тогда контроль стека не нужен вовсе). Ну или при ручной манипуляции с указателем стека, что на языке Си сделать не просто.

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

Использование SLEEP

Уст-во на PIC24F с применением TNKernel работает в 2-х режимах - питание от батарейки и нормальное функционирование. Интересует процесс перехода на батарейное питание.


Сейчас делаю так - на ноге INT1 отображается состояние (подача внешнего питания)… если в процессе работы от внешнего питания оно выключается, я в прерывании переключаю режим тактирования на 32768 Гц, (завершаю высокоприоритетные задачи, низкоприоритетные (которые можно просто выключить)), отключаю системный тик TNKernel и засыпаю, периодически просыпаясь для определения - не подали ли питание. Если подали питание, то через программный сброс и полную инициализацию TNKernel продолжаю работать.

Правильно ли я делаю и каковы рекомендации по работе в таких режимах?

Смысла делать полный сброс нет.

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

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

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

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

TN_NO_ERROR_CHECKING

Для чего нужен TN_NO_ERROR_CHECKING?

Об этом можно почитать тут.

TN_DEBUG

Для чего нужен TN_DEBUG?

Об этом можно почитать тут.

Разделяемые ресурсы

Пытаюсь прочувствовать механизм вытеснения:


void TN_TASK Task1 (void *par)
{
    for (;;)
    {
        LATBbits.LATB1 = 0;
        tn_task_sleep(10);
        LATBbits.LATB1 = 1;
    }
}
 
void TN_TASK Task2 (void *par)
{
    for (;;)
    {
        LATBbits.LATB2 = !PORTBbits.RB2;
    }
}
 
tn_sys_interrupt (_T2Interrupt)
{
    IFS0bits.T2IF = 0;
    tn_tick_int_processing();
    LATBbits.LATB4 = !PORTBbits.RB4;
}

Смутило поведение выводов - оно не соответствует тому, которое можно предположить. В чем дело?

Дело в том, что в этом примере в разных задачах используется один и тот же ресурс (порт), и этот ресурс не блокируется.

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

Способов решения проблемы всего два:

  • использовать мютексы, которые для этого и предназначены
  • работать с разделяемым ресурсом в критической секции (часть программы, в которой запрещено переключение контекста)

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

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

В приведенном примере в две задачи используют один и тот же порт - PORTB. При этом задача Task2 выполняет не атомарную операцию с портом. Такая же не атомарная операция видна и в системном прерывании. Вот что получим, скомпилировав исходник:

    mov.b   #PORTB,    0x0000
    ze.b    0x0000,    0x0000
    lsr     0x0000,    #2,  0x0000
    btg     0x0000,    #0
    and.b   0x0000,    #1,  0x0000
    sl      0x0000,    #2,  0x0000
    mov.w   #LATB,     0x0002
    mov.b   [0x0002],  0x0002
    bclr    0x0002,    #2
    ior.b   0x0002,    0x0000,0x0002
    mov.w   0x0002,    0x0000
    mov.b   0x0000,    #LATB

Если вытеснение произойдет между считыванием значения регистра PORTB и записью результата в LATB, то вполне возможно, что задача получит управление, когда PORTB будет иметь совсем другое значение…

Если изменить пример, защитив действия с портом критической секцией, то все будет работать правильно:

void TN_TASK Task1 (void *par)
{
    for (;;)
    {
        tn_sys_enter_critical();
        LATBbits.LATB1 = 0;
        tn_sys_exit_critical();
 
        tn_task_sleep(10);
 
        tn_sys_enter_critical();
        LATBbits.LATB1 = 1;
        tn_sys_exit_critical();
    }
}
 
void TN_TASK Task2 (void *par)
{
    for (;;)
    {
        tn_sys_enter_critical();
        LATBbits.LATB2 = !PORTBbits.RB2;
        tn_sys_exit_critical();
    }
}
 
tn_sys_interrupt (_T2Interrupt)
{
    IFS0bits.T2IF = 0;
    tn_tick_int_processing();
 
    LATBbits.LATB4 = !PORTBbits.RB4;
}

Кстати, запись LATBbits.LATB4 = !PORTBbits.RB4; гораздо лучше будет выглядеть вот так:

LATB ^= (1 << 4);

Скорей всего, получим следующее:

    btg     0x02CA,    #4

Вызов функций

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

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

Тут нужно еще ввести такое понятие как реентерабельная функция - такая функция которая может допускает вложенность вызовов - т.е. может быть вызвана из основного кода и из прерывания (или несколькими ядрами процессора).

Реентерабельная функция никогда не использует глобальных переменных, не использует статических переменных - т.е. работает только с теми данными, которые переданы через параметры. Понятное дело, что реентерабельная фунция не должна вызывать нереентерабельные.

Thread-safe - примерно то же самое, т.е. можно сказать, что любую реентерабельную функцию можно вызывать как thread-safe - из любой задачи RTOS без использования методов синхронизации. Тем не менее, это не одно и тоже. Любую НЕ thread-safe функцию можно превратить в thread-safe используя мютексы, критические секции и пр. С нереентерабельными такой фокус не пройдет - нужно менять реализацию таким образом, чтоб она не использовала сторонних данных - только те, которые предоставляются вызывающей функцией.

Таким образом термин thread-safe относится только к приложениям, в которых используется операционная система, в то время как реентерабельность - свойство функции вне зависимости от контекста использования.

Короче, для начала нужно помнить, что большинство стандартных функций НЕ thread-safe. Например, если используешь динамическое выделение памяти или sprintf() в разных задачах - всегда оборачивай их вызов в мютекс.

Тонкости Round-Robin

Заметил такую особенность: создаю четыре задачи - в последовательности Task1, Task2, Task3 и Task4. При старте системы первой начинает работу задача Task1, потом Task4, следом Task3 и, соответственно, после Task3 переключается на Task2.

Я думал, что задачи должны были крутиться по порядку создания Task1, Task2, Task3 и потом только Task4. Если создать три задачи - аналогичная ситуация: запускается сперва Task1 потом Task3 и после Task3 - задача Task2. Чем это объясняется?

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

А вообще round-robin не специфицирует очередность запуска задач. Оно немного для другого предназначено. Если хочешь обеспечить точную последовательность запуска, нужно использовать объекты синхронизации (семафоры и пр.)

 
tnkernel/faq.txt · Последние изменения: 02.02.2011 12:52 От admin
 
Creative Commons License Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki