Available Languages?:

OSA : Учебник. Урок 3 - Задержки

Тема

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

Для формирования задержек в задачах есть сервис OS_Delay(). Целью урока является исследование работы этого сервиса.

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

Теория

OS_Delay()

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

Сервис OS_Delay вызывается с числовым параметром, например:

    OS_Delay(10);

Данная запись говорит планировщику о том, что задача будет находиться в задержке в течение какого-то времени. Какого именно? В нашем примере в параметре указано число 10, но что оно значит? Очевидно, что задержка с параметром 20 продлится дольше, чем 10. Только 10 и 20 чего? Остановимся здесь подробнее.

Т.к. микроконтроллеры могут решать совершенно разные задачи, которые предъявляют разные требования к скорости работы самого контроллера, невозможно придумать какую-то универсальную временную единицу, от которой можно было бы отталкиваться при формировании задержек для разных проектов. Один контроллер предполагается тактировать кварцевым генератором 20 МГц, другому хватит встроенного генератора 4 МГц, третий обходится с генератором 32 КГц. Операционная система не знает, на какой частоте будут работать контроллеры с конкретной программой; частоту используемого генератора будет знать только разработчик проекта. Поэтому именно сам программист должен подсказать операционной системе ту самую временную единицу, которая будет базовой для формирования всех задержек. Назовем эту временную единицу системным тиком - минимальный квант времени для отсчета задержек.

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

OS_Timer

Операционная система для каждой активной задачи хранит свою переменную-счетчик и свой бит состояния этого счетчика (считает/не считает). Когда задача выполняет сервис OS_Delay, счетчику, привязанному к этой задаче, присваивает значение, указанное в параметре этого сервиса, а бит состояния устанавливает в "считает". После этого управление передается планировщику, и он, пока видит, что у задачи бит состояния установлен в значение "считает", задачу не запускает на выполнение. Счетчик постепенно уменьшается, и, когда он доходит до нуля, бит состояние устанавливается в "не считает", что служит сигналом планировщику, что задача снова готова к выполнению.

Примечание. Переменная-счетчик, применяемая для отсчета задержек в задачах, называется таймером задачи.

Как быстро будет уменьшаться этот счетчик? В OSA есть сервис OS_Timer, при каждом вызове которого все активные счетчики (с битом состояния = "считает") уменьшаются на 1. В нем же производится проверка на завершение счета и сброс бита состояние в "не считает". Программист сам решает, когда и с каким периодом вызывать этот сервис. Чаще всего он вызывается в обработчике прерывания по переполеннию какого-нибудь таймера. Удобно (но вовсе не обязательно) вызывать сервис OS_Timer с одинаковым периодом.

Системный тик будет равен периоду вызова OS_Timer().

Таким образом, программист сам решает, что означает параметр 10 в вызове сервиса OS_Delay. Предположим, OS_Timer вызывается в обработчике прерывания TMR2, которое происходит каждые 250 мкс. Тогда время системного тика будет равно 250 мкс, а OS_Delay(10) будет длиться 10*250 мкс = 2.5 мс.

Очевидно, что в ходе программы можно менять длительность системного тика (например, в режиме пониженного энергопотребления системный тик удобно приравнять к периоду таймера watchdog). Очевидно также, что если ни разу не вызывать сервис OS_Timer, то все задачи, вызвавшие сервис OS_Delay, окажутся в состоянии вечного ожидания, т.к. счетчики, привязанные к задачам, никогда не обнулятся.

Некоторые свойства OS_Delay

  • Вызов OS_Delay(0) функционально эквивалентен OS_Yield().
  • Параметр сервиса OS_Delay не должен превышать разрядности таймеров задач (по умолчанию = 16 бит). Подробнее про разрядность речь пойдет ниже.
  • Последовательный вызов OS_Delay с параметрами X и Y эквивалентен одному вызову OS_Delay с параметром X + Y (если сумма не выходит за границы разрядности таймера).

Ну, а теперь, рассмотрим работу OS_Delay и OS_Timer на конкретном примере.

Проект

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

Для начала создадим проект в оболочке MPLab по шагам, описанным в документации.

1. Создание проекта

  1. создаем рабочую папку "c:\tutor\t3";
  2. в ней создаем пустой файл "tutor3.c";
  3. запускаем MPLab (если в ней открыт проект, то закрываем его через меню "File\Close Workspace");
  4. через меню "Project\Project Wizard…" создаем новый проект:
    1. в качестве процессора выбираем 16F887 (или тот, который вы собираетесь использовать);
    2. выбираем инструмент (toolsuite): HI-TECH PICC Toolsuite;
    3. создаем файл проекта с именем tutor3.mcp в папке "c:\tutor\t3";
    4. добавляем в проект созданный нами пустой файл tutor3.c и файл системы "c:\osa\osa.c";
    5. нажимаем кнопку "Готово".

Проект создан, можно переходить ко второму шагу.

2. Конфигурирование проекта (osacfg.h)

Создаем файл конфигурации для нашего проекта, для чего воспользуемся утилитой OSAcfg_Tool.exe. После запуска программы перед нами диалоговое окно, позволяющее нам в интерактивном режиме сделать все настройки ОС для конкретного проекта. Для начала укажем программе, где проект будет располагаться, для этого кнопкой "Browse…" открываем диалоговое окно выбора файла и в нем выбираем рабочую папку "c:\tutor\t3", где будет располагаться наш файл конфигурации. Нажимаем "OK". Программа сообщит нам, что файл еще не существует и будет создан при сохранении. Жмем "OK".

Теперь в секции "System" устанавливаем параметр "Tasks" = 3 (у нас будут три задачи). Теперь нам нужно сказать системе, что мы будем использовать задержки в программе. Для этого нам нужно включить таймеры задач: в секции "Timers" устанавливаем галочку напротив пункта "Task timers".

После этого жмем кнопку "Save" в нижней части экрана, читаем сообщение, что файл успешно сохранен, давим "OK" и выходим из программы конфигуратора по кнопке "Exit".

Убеждаемся, что файл OSAcfg.h создан в папке "c:\tutor\t3".

3. #include <osa.h>

Включаем в tutor3.c используемые заголовочные файлы: pic.h и osa.h.

#include <pic.h>
#include <osa.h>

4. Указание путей

Через меню MPLab "Project\Build options…\Project" открываем параметры проекта и во вкладке "Directories" добавляем два пути в "Include Search Path":

  • "c:\osa" - путь к файлам операционной системы;
  • "c:\tutor\t3" - путь к файлам проекта.

5. Функция main()

В функцию main() добавляем вызов двух обязательных системных сервисов: OS_Init() и OS_Run().

#include <pic.h>
#include <osa.h>
 
void main (void)
{
    OS_Init();
    OS_Run();
}

Теперь, когда заготовка для проекта готова, можно приступать к написанию программы.

Программа

Логически программа разделена на четыре части:

  • определения - здесь будут определены константы, включены заголовочные файлы, задана конфигурация контроллера;
  • задачи - каждая задача будет управлять своим светодиодом;
  • инициализация - здесь будет инициализирована периферия контроллера и операционная система;
  • прерывание - в нем мы будем вызывать сервис управления задержками OS_Timer.

Определения

Итак, начнем с определений. Что нам нужно?

  1. включить заголовочные файлы: pic.h и osa.h;
  2. задать биты конфигурации контроллера;
  3. определить выводы контроллера, к которым подключены светодиоды.
#include <pic.h>
#include <osa.h>
 
//------------------------------------------------------------------------------
// Задаем биты конфигурации:
//   - внутренний RC-генератор
//   - отключаем WDT
//   - отключаем низковольтное программирование
//   - отключаем функцию отладки
//------------------------------------------------------------------------------
 
__CONFIG(INTIO & WDTDIS & PWRTEN & MCLRDIS & LVPDIS & UNPROTECT & BORDIS
               & IESODIS & FCMDIS & DEBUGDIS);
 
 
//------------------------------------------------------------------------------
//  Определяем выводы для светодиодов
//------------------------------------------------------------------------------
 
#define PIN_LED1    RD0
#define PIN_LED2    RD2
#define PIN_LED3    RD4
 
//------------------------------------------------------------------------------
//  Параметры таймера:
//  - прескейлер = 4,
//  - постскейлер = 1,
//  - предел счета = 250
//
//  Тактовая частота контроллера = 4 МГц.
//
//  Период возникновения прерывания по TMR2 получается
//  равным 4 * 1 * 250 * Tcyc = 1 ms
//
//------------------------------------------------------------------------------
#define PR2_CONST       250-1
#define TMR2_PRS        1                           // prs = 4
#define TMR2_POST       0                           // post = 1
#define T2CON_CONST     (TMR2_POST<<3) | TMR2_PRS

Задачи

Как уже было сказано, для демонстрации мы заведем по одной задаче на каждый светодиод. Внутри задачи будет содержаться вызов сервиса OS_Delay и операция переключения светодиода. Таким образом, получится, что каждый светодиод будет переключаться с интервалом, указанным в параметре к сервису OS_Delay. Например, задача управления первым светодиодом будет выглядеть так:

void Task_T1 (void)
{
    for (;;)
    {
        PIN_LED1 ^= 1;       // Меняем состояние первого светодиода
        OS_Delay(500);       // Переводим задачу в режим ожидания на
                             // время, равное 500 системным тикам
    }
}

По аналогии будут запрограммированы две другие задачи для управления светодиодами 2 и 3, но с той разницей, что параметрами сервиса OS_Delay будут числа 1000 и 1500.

Инициализация

Инициализация периферии будет вынесена для наглядности в отдельную функцию init(), которая будет вызываться из функции main(). Мы должны проинициализировать следующее:

  • установить все порты ввода/вывода на выход;
  • инициализировать таймер;
  • инициализировать модуль прерываний.
void init (void)
{
    //------------------------------------------------------------------------------
    //  Настройка портов I/O
    //------------------------------------------------------------------------------
 
    PORTA = 0;
    PORTB = 0;
    PORTC = 0;
    PORTD = 0;
 
    TRISA = 0;
    TRISB = 0;
    TRISC = 0;
    TRISD = 0;
 
    //------------------------------------------------------------------------------
    //  Настройка таймера 2
    //------------------------------------------------------------------------------
 
    PR2 = PR2_CONST;
    T2CON = T2CON_CONST | 0x04;
 
    //------------------------------------------------------------------------------
    //  Настройка прерываний
    //------------------------------------------------------------------------------
 
    PIR1 = 0;
    PIR2 = 0;
    INTCON = 0;
 
    TMR2IE = 1;         // Разрешаем прерывание по TMR2
    PEIE = 1;           // Разрешаем периферийные прерывания
                        // Глобальные бит разрешения прерываний будет
                        // установлен непосредственно перед запуском
                        // планировщика в функции main()
}

Остальная часть кода инициализации будет размещена в main().

void main (void)
{
    init();             // Инициализация периферии
    OS_Init();          // Инициализация системы
 
    // Создаем задачи
    OS_Task_Create(3, Task_T1);
    OS_Task_Create(3, Task_T2);
    OS_Task_Create(3, Task_T3);
 
    OS_EI();            // Разрешаем прерывания
    OS_Run();           // Запускаем планировщик
}

Обратим внимание на новый сервис OS_EI(). Фактически он всего-навсего устанавливает бит GIE. Но мы предполагаем, что любая программа может быть перенесена на другой контроллер (например, на PIC24, где бита GIE нет), поэтому изначально будет писать программу так, чтобы по возможности свести к минимуму усилия по портированию. Сервис OS_EI() для всех контроллеров описан по-своему, поэтому предпочтительнее использовать именно его, а не явное разрешение прерываний через установку бита GIE.

Прерывание

Как уже говорилось ранее, сервис, управляющий отсчетом задержек (фактически - управляющий таймерами задач), обычно вызывается в периодическом месте программы. Интервал вызовов этого сервиса называется системным тиком. В большинстве случаев удобно, чтобы OS_Timer вызывался с одним и тем же интервалом времени, что обеспечит программу системным тиком фиксированной длительности. Самый удобный способ (но не единственный) - вызывать его в обработчике прерывания по таймеру. В нашей программе мы воспользуемся таймером TMR2.

void interrupt isr (void)
{
    OS_EnterInt();
    if (TMR2IF)
    {
        OS_Timer();
        TMR2IF = 0;
    }
    OS_LeaveInt();
}

Прерывание будет возникать 1 раз в миллисекунду, следовательно, системный тик в нашей программе также будет равен 1 мс.

Весь текст программы

Для удобства здесь приведен весь текст программы целиком:

//******************************************************************************
//  Программа для исследования работы задержек в OSA
//******************************************************************************
 
#include <pic.h>
#include <osa.h>
 
 
 
//------------------------------------------------------------------------------
// Задаем биты конфигурации:
//   - внутренний RC-генератор
//   - отключаем WDT
//   - отключаем низковольтное программирование
//   - отключаем функцию отладки
//------------------------------------------------------------------------------
 
__CONFIG(INTIO & WDTDIS & PWRTEN & MCLRDIS & LVPDIS & UNPROTECT & BORDIS
               & IESODIS & FCMDIS & DEBUGDIS);
 
 
//------------------------------------------------------------------------------
//  Определяем выводы для светодиодов
//------------------------------------------------------------------------------
 
#define PIN_LED1    RD0
#define PIN_LED2    RD2
#define PIN_LED3    RD4
 
//------------------------------------------------------------------------------
//  Параметры таймера:
//  - прескейлер = 4,
//  - постскейлер = 1,
//  - предел счета = 250
//
//  Тактовая частота контроллера = 4 МГц.
//
//  Период возникновения прерывания по TMR2 получается
//  равным 4 * 1 * 250 * Tcyc = 1 ms
//
//------------------------------------------------------------------------------
 
#define PR2_CONST       250-1
#define TMR2_PRS        1                           // prs = 4
#define TMR2_POST       0                           // post = 1
#define T2CON_CONST     (TMR2_POST<<3) | TMR2_PRS
 
 
//******************************************************************************
//  Прерывание. Возникает каждую мс
//******************************************************************************
 
void interrupt isr (void)
{
    OS_EnterInt();
    if (TMR2IF)
    {
        OS_Timer();
        TMR2IF = 0;
    }
    OS_LeaveInt();
}
 
//******************************************************************************
//  Описание задач управления светодиодами
//******************************************************************************
 
void Task_T1 (void)
{
    for (;;)
    {
        PIN_LED1 ^= 1;       // Меняем состояние первого светодиода
        OS_Delay(500);       // Переводим задачу в режим ожидания на
                             // время, равное 500 системным тикам
    }
}
 
//------------------------------------------------------------------------------
 
void Task_T2 (void)
{
    for (;;)
    {
        PIN_LED2 ^= 1;       // Меняем состояние второго светодиода
        OS_Delay(1000);      // Переводим задачу в режим ожидания на
                             // время, равное 1000 системным тикам
    }
}
 
//------------------------------------------------------------------------------
 
void Task_T3 (void)
{
    for (;;)
    {
        PIN_LED3 ^= 1;       // Меняем состояние третьего светодиода
        OS_Delay(1500);      // Переводим задачу в режим ожидания на
                             // время, равное 1500 системным тикам
    }
}
 
//******************************************************************************
//  Инициализация периферии
//******************************************************************************
 
void init (void)
{
    //------------------------------------------------------------------------------
    //  Настройка портов I/O
    //------------------------------------------------------------------------------
 
    PORTA = 0;
    PORTB = 0;
    PORTC = 0;
    PORTD = 0;
 
    TRISA = 0;
    TRISB = 0;
    TRISC = 0;
    TRISD = 0;
 
    //------------------------------------------------------------------------------
    //  Настройка таймера 2
    //------------------------------------------------------------------------------
 
    PR2 = PR2_CONST;
    T2CON = T2CON_CONST | 0x04;
 
    //------------------------------------------------------------------------------
    //  Настройка прерываний
    //------------------------------------------------------------------------------
 
    PIR1 = 0;
    PIR2 = 0;
    INTCON = 0;
 
    TMR2IE = 1;         // Разрешаем прерывание по TMR2
    PEIE = 1;           // Разрешаем периферийные прерывания
                        // Глобальные бит разрешения прерываний будет
                        // установлен непосредственно перед запуском
                        // планировщика в функции main()
 
}
 
//******************************************************************************
//  MAIN
//******************************************************************************
 
void main (void)
{
    init();             // Инициализация периферии
    OS_Init();          // Инициализация системы
 
    // Создаем задачи
    OS_Task_Create(3, Task_T1);
    OS_Task_Create(3, Task_T2);
    OS_Task_Create(3, Task_T3);
 
    OS_EI();            // Разрешаем прерывания
    OS_Run();           // Запускаем планировщик
}
 
//******************************************************************************
//  END
//******************************************************************************

Как работает программа

К тому моменту, как будет вызван планировщик, у нас созданы три задачи. Т.к. все они равноприоитетны и готовы к выполнению (после создания задача сразу становится готовой), то планировщик выберет для запуска ту, которая была создана первой, т.е. Task_T1. Получив управление, задача Task_T1 первым изменит состояние светодиода (светодиод был погашен, и теперь он зажжется). Далее она вызывает сервис OS_Delay с параметром 500. При этом происходит следующее:

  1. задача инициализирует свой внутренний таймер (переменную-счетчик) значением 500;
  2. переводит себя в состояние ожидания (т.е. она не сможет получить управление, пока не произойдет ожидаемое ей событие, а именно, - пока не обнулится ее таймер);
  3. передает управление планировщику.

Теперь планировщик видит, что готовы к выполнению только две задачи (одна уже в режиме ожидания): Task_T2 и Task_T3. Т.к. приоритеты их равны, то она выбирает следующую по очереди (порядок очереди определяется последовательностью создания задач сервисов OS_Task_Create), т.е. Task_T2. Эта задача, получив управление, выполняет те же самые операции, только ее внутренний таймер будет инициализирован значением 1000. Задача также становится в режим ожидания, а управление возвращается планировщику.

Теперь готова к выполнению только одна задача - Task_T3, и планировщик передает ей управление. Так же как и первые две, она зажигает свой светодиод, после чего уходит в режим ожидания, инициализируя свой внутренний таймер значением 1500. После этого управление опять возвращается планировщику.

Что у нас получилось в результате? Готовых задач нет, поэтому планировщик будет крутиться теперь сам в себе. Есть три задачи, стоящие в режиме ожидания завершения задержки, с разными значениями таймеров: 500, 1000 и 1500. Эти задачи будут оставаться в режиме ожидания, пока их таймеры не обнулятся.

Как уже говорилось, за таймерами задач следит сервис OS_Timer, который при каждом вызове уменьшает все активные таймеры на 1. Мы вызываем этот сервис в прерывании, возникающем раз в миллисекунду. Т.е. каждую миллисекунду значения таймеров будут уменьшаться на 1. Таким образом, через 500 мс таймер задачи Task_T1 будет обнулен, и она переведется в режим готовности. А так как она окажется единственной задачей, готовой к выполнению, то планировщик сразу же передаст ей управление. Значения таймеров для задач Task_T2 и Task_T3 за это время уменьшатся на 500 и станут равны 500 и 1000, соответственно. Задача Task_T1, получив управление, повторит все действия, которые были проделаны при предыдущем ее запуске: изменит состояние светодиода (теперь уже погасит его) и вызовом сервиса OS_Delay снова уйдет в задержку, вернув управление планировщику.

На этот момент времени готовых задач опять нет. Значения таймеров будут 500, 500 и 1000. Теперь через 500 мс (т.е. через 500 вызовов сервиса OS_Timer) у нас одновременно обнулятся таймеры задач Task_T1 и Task_T2, и они обе перейдут в режим готовности. В данном случае трудно предсказать, какая из задач получит управление первой. Предположим, что это будет Task_T1. После того как она отработает (зажжет светодиод и уйдет в задержку), сразу же запустится задача Task_T2, т.к. она уже готова к выполнению. После отработки она также уходит в задержку, передав управление планировщику. И планировщик снова ждет, когда какая-нибудь из задач стает готовой (это произойдет через 500 мс, готовыми станут Task_T1 и Task_T3).

Схематически работу программы можно изобразить так:

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

Примечание по рассинхронизации

Стоит отметить один важный момент, касающийся счета таймеров задач. Прерывание, в котором производится декремент всех активных таймеров может возникнуть в любой момент, и это может привести к рассинхронизации задач. Поясню на конкретном примере. По нашей схеме предполагается, что задача Task_T1 будет выполняться в два раза чаще, чем Task_T2. Однако есть одна тонкость. Предположим, что таймер первой задачи досчитал до 0, а таймер второй задачи - до 500. Планировщику на то, чтобы просмотреть список задач, выбрать готовую и передать ей управление, требуется некоторое время. Если длительность системного тика сравнительно мала, то за это время может успеть произойти еще одно прерывание, т.е. еще один вызов OS_Timer, который в свою очередь вычтет еще одну единицу из таймера задачи Task_T2 (таймер задачи Task_T1 и так уже нулевой, поэтому он останется без изменений). После получения управления Task_T1 обновит свой таймер, и он примет значение = 500, а таймер задачи Task_T2 будет к этому времени уже = 499. Таким образом, получается некоторая рассинхронизация. В общем случае в зависимости от количества задач, от их состояний, от периода вызова OS_Timer, от того, как задачи забирают под себя время процессора, такая рассинхронизация может происходить чаще или реже. В нашем конкретном примере она будет очень редким явлением, но в более навороченной программе, это будет происходить чаще. Поэтому в общем случае не стоит использовать OS_Delay для точной синхронизации задач друг с другом.

Запуск

Запуск в железе

Выполним сборку нашей программы (Ctrl+F10) и прошьем ее в контроллер. Убедимся, что светодиоды работают именно так, как описано в предыдущем параграфе, т.е. один светодиод мигает раз в секунду, другой - раз в две секунды, третий - раз в три секунды. Теперь можно переходить к параграфу "Эксперименты", где мы попытаемся внимательнее исследовать свойства сервиса OS_Delay. Но сначала пройдем нашу программу по шагам в симуляторе.

Запуск в симуляторе

Расставим брейкпоинты, как показано на рисунке:

Откроем окно "Watch", куда добавим внутренний массив дескрипторов задач OS_Task_Vars (это единственный способ проследить за таймерами задач). Раскроем весь массив, как показано на рисунке, чтобы мы смогли наблюдать отдельно за таймером каждой задачи. Теперь установим параметры отображения для каждого таймера, для этого на поле Timer для каждого дескриптора жмем правую кнопку мышки и во всплывающем меню выбираем пункт "Properties…". В открывшемся диалоговом окне задаем значение "Decimal" в поле "Format" и справа от него устанавливаем галочку в пункте "Signed", как показано на картинке:

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

Теперь выберем симулятор для отладки через пункт меню "Debugger/Select Tool…/MPLAB SIM". Выполним кое-какие настройки: откроем окно "Debugger/Settings…" и в поле "Processor frequency" зададим значение 4 МГц. Откроем окно "Debugger/Stopwatch" (секундомер), в котором будем засекать время выполнения различных кусков программы. Теперь можно приступать к отладке.

Нажимаем F9 (Run). Программа начнет выполняться и остановится на первой встретившейся точке останова, предварительно выполнив все шаги инициализации и создания задач. Первой встретившейся точкой останова будет PIN_LED1 ^= 1 в задаче Task_T1. Если мы сейчас нажмем F9 (Run), то окажемся на второй точке останова, но сможем в Watch-окне увидеть, что при выполнении OS_Delay в первой задаче у нас изменилось значение ее таймера (поле Timer), оно стало равным -500. Нажав F9 (Run) еще раз, мы перейдем на третью точку останова (в задаче Task_T3), обнаружив, что и значение поле Timer для второй задачи теперь стало -1000.

Прежде чем выполнять очередную задачу, поставим еще одну точку останова внутри прерывания:

Теперь нажимаем F9. Поставив третью задачу в режим ожидания, программа начинает крутиться в планировщике, который раз в миллисекунду будет прерываться по переполнению таймера TMR2. При первом же переполнении программа остановится на брейкпоинте, который мы установили в прерывании. Указатель симулятора сейчас находится перед вызовом сервиса управления таймерами OS_Timer. Обратим внимание на состояния таймеров задач в окне Watch и убедимся в том, что они имеют значения: -500, -1000, -1500. Сервис OS_Timer выполним командой отладчика Step Over (F8), после чего увидим, что значения всех таймеров увеличилось на 1, и они приняли значения: -499, -999, -1499. Теперь, нажимая F9 (Run) мы будет все время попадать на брейкпоинт, установленный в прерывании, т.к. задачи находятся в режиме ожидания и управление не получают. При каждом попадании в прерывание мы убеждаемся, что значения таймеров по одному шагу приближаются к нулю. Кроме того, по значениям в окне Stopwatch (секундомер) мы можем убедиться, что прерывание происходит раз в миллисекунду.

Так мы разобрались с тем, как в программе инициализируются таймеры и как они обновляются сервисом OS_Timer. Чтобы не нажимать F9 (Run) 500 раз, ожидая, когда же обнулится таймер первой задачи, снимем точку останова, которую мы поставили в прерывании.

Теперь, нажав F9 (Run), мы запустим программу на выполнение так, что она остановится только тогда, когда хотя бы одна из задач, где установлены брейкпоинты, получит управление. Это произойдет через 500 вызовов сервиса OS_Timer - мы снова попадем в задачу Task_T1. По окну Watch можем убедиться в том, что таймер первой задачи теперь обнулен, а по окну Stopwatch - что прошло 500 мс.

Нажимая F9 (Run) и следя за тем, в каком порядке задачи получают управление, мы через окно Stopwatch будем видеть, что это происходит как раз с заданными нами периодами. Т.е. задача Task_T1 будет выполняться раз в 500 мс, задача Task_T2 - раз в секунду, Task_T3 - в полторы секунды. Таким образом, мы в симуляторе по шагам проверили то, что только что видели в железе. Теперь можно поэкспериментировать.

Эксперименты

Эксперимент 1 - приоритеты

В уроке, посвященном изучению сервиса OS_Yield, мы сделали эксперимент с заменой приоритета одной из задач, что привело к тому, что она не смогла получить управление. Попробуем сделать то же самое в данной программе:

    // Создаем задачи
    OS_Task_Create(2, Task_T1);    // Приоритет повышен
    OS_Task_Create(3, Task_T2);
    OS_Task_Create(4, Task_T3);    // Приоритет понижен

Выполняем сборку (Ctrl+F10) и прошиваем программу в контроллер. Светодиоды будут мигать так же, как и до изменения приоритетов: первый - раз в секунду, второй - раз в две секунды, третий - раз в три секунды. Это говорит о том, что все задачи получают управление. В чем же отличие от такой же ситуации с OS_Yield?

Вспомним еще раз: OS_Yield, передавая управление планировщику, оставляет задачу готовой к выполнению. А планировщик передает управление самой приоритетной из готовых. Поэтому в случае с OS_Yield задача с более низким приоритетом просто не могла получить управление из-за того, что всегда была в наличии готовая задача с более высоким приоритетом.

В случае с OS_Delay все не так. OS_Delay, в отличие от OS_Yield, переводит задачу в режим ожидания. А задача, находящаяся в режиме ожидания, не рассматривается планировщиком как кандидат на запуск, поэтому менее приоритетные задачи получают шанс получить управление. Исключение составляет лишь вызов OS_Delay(0), который, как уже писалось, является функциональным эквивалентом OS_Yield.

Эксперимент 2 - конкатенация

Среди свойств OS_Delay упоминалось такое: "Последовательный вызов OS_Delay с параметрами X и Y эквивалентен одному вызову OS_Delay с параметром X + Y". Рассмотрим его на практике. Перепишем задачу Task_T1 следующим образом:

void Task_T1 (void)
{
    for (;;)
    {
        PIN_LED1 ^= 1;
        OS_Delay(250);
        OS_Delay(250);
    }
}

Соберем программу (Ctrl+F10) и прошьем контроллер. Мы увидим, что светодиод 1 мигает так же и с той же частотой. Дело в том, что функционально мы ничего не изменили. После выполнения первого OS_Delay(250) задача уйдет в ожидание на 250 системных тиков. Как только задержка закончится, задача станет готовой к выполнению, и когда получит управление, она вернется на вызов второго OS_Delay, который также переведет ее в режим ожидания на 250 системных тиков. Т.е. получается, что мы выдерживаем ту же самую задержку в 500 тиков, только другим способом.

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

Теперь перепишем задачу Task_T1 так:

void Task_T1 (void)
{
    for (;;)
    {
        PIN_LED1 = 1;
        OS_Delay(500);
        PIN_LED1 = 0;
        OS_Delay(500);
    }
}

Соберем программу (Ctrl+F10) и прошьем контроллер. Работа программы также останется без изменений. Мы сделали то же самое, что и было, но только опять же другим способом. Раньше состояние светодиода менялось через каждые 500 тиков, а теперь оно явно задается так же через 500 тиков. Т.е. суммарное время горения и негорения светодиода и в том и в другом случае равно 1 секунде. Но последняя форма записи позволяет нам изменить скважность (например, для экономии заряда батарейки):

        PIN_LED1 = 1;
        OS_Delay(50);
        PIN_LED1 = 0;
        OS_Delay(950);

Здесь мы видим, что время горения составляет всего 50 тиков, а время негорения - 950. На глаз вспышки будут хорошо заметны, а светодиод стал потреблять в 10 раз меньше тока.

Эксперимент 3 - системный тик

Поговорим немного о выборе системного тика для проекта. Очевидно, что для нашего случая такой частый вызов OS_Timer избыточен. Для нашей конкретной задачи подошел бы системный тик длительностью, например, в 50 мс, в 100 мс или в 500 мс. Перепишем немного нашу программу:

  • изменим период генерации прерывания по TMR2 с 1 мс на 10 мс;
  • сократим значения всех аргументов OS_Delay в 10 раз:
#define TMR2_POST      9       // Постскейлер = 10
...
//--------------------------------------------
void Task_T1 (void)
{
    for (;;)
    {
        PIN_LED1 = 1;
        OS_Delay(5);
        PIN_LED1 = 0;
        OS_Delay(95);
    }
}
//--------------------------------------------
void Task_T2 (void)
{
    for (;;)
    {
        PIN_LED2 ^= 1;
        OS_Delay(100);
    }
}
//--------------------------------------------
void Task_T3 (void)
{
    for (;;)
    {
        PIN_LED3 ^= 1;
        OS_Delay(150);
    }
}
//--------------------------------------------

Выполним сборку (Ctrl+F10) и прошьем контроллер. Работа устройства опять же не изменилась: светодиоды мигают с теми же частотами. Фактически мы и не меняли логику работы программы. Задержки как были 500, 1000 и 1500 мс, так и остались. Просто изменилось время системного тика. Но что мы можем выиграть, удлинив время системного тика? Обратим внимание, что после деления всех значений аргументов в вызовах сервиса OS_Delay, все они у нас стали меньше 255. А это значит, что для конкретного случая под каждый таймер задач будет достаточно одного байта, а не двух (по умолчанию таймеры двухбайтовые).

Посмотрим на статистику, выданную компилятором при сборке:

Memory Summary:
    Program space        used   18Ah (   394) of  2000h words   (  4.8%)
    Data space           used    1Eh (    30) of   170h bytes   (  8.2%)
    EEPROM space         used     0h (     0) of   100h bytes   (  0.0%)
    Configuration bits   used     1h (     1) of     2h words   ( 50.0%)
    ID Location space    used     0h (     0) of     4h bytes   (  0.0%)

Примечание. Статистика может немного отличаться в зависимости от версии компилятора. Данная статистика приведена для PICC STD 9.60PL3.

Как видно, у нас занято 30 байт оперативной памяти. И мы знаем, что 6 из них заняты таймерами задач во внутренних переменных операционной системы (3 таймера по 2 байта). Так же мы знаем, что в конкретной программе значения таймеров никогда не превышают по модулю значения 150 (для таймеров используется только отрицательная область значений, поэтому для однобайтового таймера допустимы значения -1..-255). Следовательно, старший байт каждого таймера висит мертвым грузом.

Запустим программу конфигуратор OSAcfg_Tool и откроем в ней конфигурацию для проекта C:\TUTOR\T3 (теперь это можно сделать через выкидной список Path, т.к. там сохраняются все пути, которые были введены). Верхняя правая часть диалогового окна отведена для конфигурирования таймеров. У нас стоит галочка рядом с пунктом Enable для Task timers. А справа от этого пункта есть ComboBox, в котором можно выбрать размерность таймера задач. По умолчанию там стоит (system), т.е. использовать размерность системного таймера, которая задается чуть выше и по умолчанию имеет тип default (int). Изменим тип таймера задач на char:

Теперь сохраняем (кнопка "Save") и выходим (кнопка "Exit"). В файл OSAcfg.h добавится строчка:

#define OS_TTIMER_SIZE    1

Пересоберем проект (Ctrl+F10) и прошьем контроллер. Программа работает, как и раньше. А теперь заглянем в статистику компилятора:

Memory Summary:
    Program space        used   173h (   371) of  2000h words   (  4.5%)
    Data space           used    1Bh (    27) of   170h bytes   (  7.3%)
    EEPROM space         used     0h (     0) of   100h bytes   (  0.0%)
    Configuration bits   used     1h (     1) of     2h words   ( 50.0%)
    ID Location space    used     0h (     0) of     4h bytes   (  0.0%)

Мы увидим, что использованная RAM сократилась на 3 байта (отрезали по одному байту от каждого таймера). Но обратим внимание еще на то, что используемая ROM-память также сократилась (на 23 байта). Это связано с тем, что:

  • во-первых, сократился код сервиса OS_Timer, который теперь обрабатывает однобайтовые переменные, вместо двухбайтовых;
  • во-вторых, в сервис OS_Delay теперь также передается однобайтовый параметр, опять экономится место;
  • в-третьих, само нутро сервиса OS_Delay теперь тоже работает с однобайтовыми числами.

Сейчас у нас три задачи, которые почти ничего не делают, а кроме них ничего нет. Поэтому выигрыш 3х байт оперативной памяти и 23 слов программной памяти не ощутим. Но когда в программе будет 10 задач и под 50 вызовов OS_Delay, да еще куча всяких переменных и подпрограмм, тогда мы почувствуем, что выигрыш в 10 байт RAM и в 200 слов ROM уже ощутим. Также не лишним будет упомянуть, что и скорость работы OS_Timer возрастает, т.к. однобайтовые переменные обрабатываются быстрее.

Системный тик

О каких параметрах нам нужно думать, выбирая время системного тика?

Диапазон задержек

В первую очередь, конечно же, о диапазоне формируемых задержек. Например, совершенно понятно, что если системный тик = 10 мс, то мы не сможем сформировать задержку в 5 мс. С другой стороны, имея системный тик в 1 мс, при разрядности таймера задач по умолчанию в 16 бит мы сможем сформировать задержку только в 65.5 секунд, что может оказаться недостаточным для некоторых приложений (например, регистрация температуры и влажности производится раз в 10 минут = 600 секунд). Кроме того, как уже было описано выше, иногда есть возможность (а иногда и необходимость) сократить размерность таймера задач до одного байта.

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

Следует добавить, что на особые случаи есть возможность делать таймеры задач 32-битными.

Точность задержек

При написании программы следует помнить, что точность формирования задержки равна одному системному тику. Дело в том, что, вызывая сервис OS_Delay, мы не знаем, через какое время будет вызван OS_Timer в первый раз. Если прерывание уже на подходе, а мы вызываем в задаче OS_Delay(10), то практически сразу за вызовом этого сервиса таймер задачи уменьшится на 1. В результате чего задача прождет не 10, а 9 системных тиков. Об этой особенности надо помнить и по возможности избегать вызовов OS_Delay с параметром 1, т.к. в этом случае задача может вообще ничего не прождать, а получить управление следующим же шагом.

Чем выше требуемая точность, тем меньше должно быть время системного тика. При этом надо помнить, что чем больше значение параметра в вызове сервиса OS_Delay, тем больше относительная точность формирования задержки. Если требуется высокая точность при длинных задержках, то есть смысл увеличить разрядность таймеров задач.

Однако не стоит забывать о трех вещах:

  1. OSA - кооперативная ОС, и только сама задача может решить, когда отдавать управление планировщику. Поэтому как бы точно не была задана задержка в задаче, например, Task_A, она не получит управление раньше, чем задача, например, Task_B отдаст управление планировщику, даже если ее приоритет ниже.
  2. Сам планировщик на поиск готовых задач и сравнение их приоритетов затрачивает некоторое время. Поэтому если время системного тика сопоставимо или меньше времени работы планировщика, то точность не улучшится.
  3. Наконец, сам сервис OS_Timer выполняется не мгновенно, а за какой-то промежуток времени (в нашем примере 21 такт для 3х 16-разрядных таймеров). Чем больше активных таймеров, тем дольше выполняется OS_Timer. Если системный тик будет выбран достаточно маленьким, чтобы прерывание успевало выполниться до того, как весь обработчик прерывания отработает, то можно зависнуть в прерывании до тех пор, пока OS_Timer не разгрузится (не обнулит часть таймеров).

Прочие условия

К прочим условиям можно отнести все, что угодно. Например, можно вызывать OS_Timer с периодом WDT, который будет использоваться для пробуждения контроллера из Sleep-режима:

void Task_Sleep (void)
{
    for (;;)
    {
        OS_SLEEP();
        OS_CLRWDT();
        OS_Timer();
        OS_Yield();
    }
}

Очевидно, что системный тик в данном случае будет равен периоду WDT. Обратим внимание на два новых сервиса OS_SLEEP и OS_CLRWDT. Эти сервисы так же, как и OS_EI, были добавлены для упрощения переноса кода на другую платформу.

Также к прочим условиям можно отнести удобство, например, использование круглых чисел: 1 мс, 10 мс, 100 мс, 1 сек.

Можно сделать системный тик зависимым от внешних условий, например, вызывать OS_Timer при приходе каждого импульса на вывод RB0.

Заключение

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

Задание

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

Рекомендации к решению:

  • включить WDT в битах конфигурации контроллера;
  • увеличить количество задач в файле конфигурации ОС;
  • убрать вызов OS_Timer из прерывания, иначе, иногда выполняясь, он будет приводить к более быстрому обнулению таймеров;
  • пересчитать все аргументы вызовов OS_Delay с учетом периода WDT.
 
osa/tutorial/tutor3.txt · Последние изменения: 09.12.2009 17:16 От osa_chief
 
Creative Commons License Powered by PHP Valid XHTML 1.0 Valid CSS Driven by DokuWiki