Содержание

OSA : Учебник. Урок 4 - Бинарные семафоры

Тема

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

Теория

Бинарный семафор - внутренняя системная переменная, которая может принимать значения "0" и "1". Фактически - это битовая переменная, которая может быть установлена/сброшена/проверена любой задачей. Чаще всего бинарный семафор применяется тогда, когда из двух задач в один момент времени может работать только одна. Например, из всех работающих задач две задачи одновременно захотели записать какие-то данные во внешнюю EEPROM. Если данных для записи много, то процесс записи может затянуться (поскольку операция записи одной ячейки требует несколько миллисекунд). Разумеется, расходовать время ожидания записи каждой ячейки вхолостую не хочется, ведь в это время по-прежнему можно опрашивать клавиатуру, управлять какими-то внешними исполнительными устройствами и т.д. Т.е. желательно на время ожидания управление передавать другим задачам. Но что делать, если одна из этих других задач тоже хочет произвести запись? Ведь она обратится к внешней EEPROM в тот момент, когда та уже занята записью. Вот здесь на помощь и приходят бинарные семафоры. Из всех желающих произвести запись задач та, которая получает управление первой, сбрасывает семафор в "0", говоря тем самым всем остальным задачам, что EEPROM уже занята. Теперь все остальные задачи, желающие произвести запись, видя, что семафор сброшен (т.е. EEPROM уже кем-то используется), будут ждать его установки (т.е. освобождения EEPROM). По завершении записи первая задача вновь устанавливает семафор в "1", говоря остальным, что EEPROM освободилась и может быть использована другими задачами.

Одновременно в программе могут использоваться несколько семафоров: один - для EEPROM, один - для UART, один - еще для каких-то целей и т.д. В OSA доступное количество ограничивается свободной RAM-памятью: один байт вмещает в себя 8 бинарных семафоров. Сами бинарные семафоры хранятся во внутреннем системном массиве, размер которого определяется на этапе компиляции и зависит от предполагаемого количество используемых семафоров. Это количество указывается программистом явно в файле osacfg.h (заданием константы OS_BSEMS). Для работы с бинарными в OSA есть несколько сервисов:

OS_Bsem_Set Установка семафора
OS_Bsem_Reset Сброс семафора
OS_Bsem_Switch Переключение семафора
bool OS_Bsem_Check Проверка семафора
OS_Bsem_Wait Ожидание семафора.
OS_Bsem_Wait_TO Ожидание семафора с выходом по таймауту.

В параметрах каждому сервису передается номер бинарного семафора. Семафоры нумеруются от 0 до OS_BSEMS-1. Например:

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

Разберемся с работой бинарных семафоров на практике.

Проект

Для знакомства с бинарными семафорами напишем программу, состоящую из двух задач, управляющих светодиодами:

Следуя инструкциям, описанным в первом уроке, создадим проект в папке "c:\tutor\t4" и назовем его "tutor4.c". Остановимся только на конфигурировании.

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

Запустим утилиту OSAcfg_Tool.exe и создадим новый файл конфигурации для нашего проекта (кнопка "Browse…" и выбор папки проекта в открывшемся диалоговом окне).

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

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

Имя BS_LEDS_ARE_FREE, которое мы указали, мы будем использовать в качестве идентификатора семафора при обращении к нему в программе. Его, конечно, можно было и не задавать, оставив поле имен (ID name) пустым, но тогда придется обращаться к семафору по номеру, например:

    OS_Bsem_Wait(0);    // Ждем нулевой семафор

Однако, запись:

    OS_Bsem_Wait(BS_LEDS_ARE_FREE);    // Ждем освобождения порта со светодиодами

намного нагляднее и информативнее. Тем более, если в программе будут использоваться несколько семафоров, то без имен в них можно будет запутаться. Задание же имен в списке ID names в утилите OSAcfg_Tool сделает наш код намного нагляднее и позволит снизить вероятность ошибки.

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

Убеждаемся, что файл OSAcfg.h создан в папке "c:\tutor\t4". Обратим внимание, что в файле OSAcfg.h появилась конструкция enum, в которой описан идентификатор нашего бинарного семафора:

enum OSA_BSEMS_ENUM
{
    BS_LEDS_ARE_FREE           // Для блокировки светодиодов
};

Описание задач

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

Задача для зажигания светодиодов:

void Task_LEDS_ON (void)
{
    static char s_i;      // Счетчик
    static char s_cMask;  // Маска для зажигания светодиодов
 
    for (;;)
    {
        s_cMask = 1;                    // Первым зажигаем нулевой светодиод
        for (s_i = 0; s_i < 8; s_i++)
        {
            PORT_LEDS |= s_cMask;       // Зажигаем текущий светодиод
            s_cMask <<= 1;              // Берем очередной светодиод
            OS_Delay(100);              // Пауза в 100 мс
        }
    }
}

Эта задача по очереди с интервалом в 100 мс зажжет все 8 светодиодов.

Теперь опишем задачу гашения светодиодов, для разнообразия сделаем в ней время паузы отличное от первой задачи:

void Task_LEDS_OFF (void)
{
    static char s_i;      // Счетчик
    static char s_cMask;  // Маска для гашения светодиодов
 
    for (;;)
    {
        s_cMask = 0x80;                 // Первым гасим седьмой светодиод
        for (s_i = 0; s_i < 8; s_i++)
        {
            PORT_LEDS &= ~s_cMask;      // Гасим текущий светодиод
            s_cMask >>= 1;              // Берем очередной светодиод
            OS_Delay(150);              // Пауза в 150 мс
        }
    }
}

Эта задача по очереди с интервалом в 150 мс гасит все 8 светодиодов.

Не забудем в функцию main() добавить сервисы, которые:

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

Полный текст программы

Итак, полный текст нашей программы теперь будет выглядеть так:

#include <pic.h>
#include <osa.h>
 
 
 
//------------------------------------------------------------------------------
// Задаем биты конфигурации:
//   - внутренний RC-генератор
//   - отключаем WDT
//   - отключаем низковольтное программирование
//   - отключаем функцию отладки
//------------------------------------------------------------------------------
 
__CONFIG(INTIO & WDTDIS & PWRTEN & MCLRDIS & LVPDIS & UNPROTECT & BORDIS
               & IESODIS & FCMDIS & DEBUGDIS);
 
 
//------------------------------------------------------------------------------
//  Определеяем порт для работы со светодиодами
//------------------------------------------------------------------------------
 
#define PORT_LEDS   PORTD
 
//------------------------------------------------------------------------------
//  Параметры таймера:
//  - прескейлер = 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
 
 
//******************************************************************************
//  Глобальные переменные
//******************************************************************************
 
char m_cCounter1;
char m_cCounter2;
 
 
 
 
 
//******************************************************************************
//  Прерывание. Возникает каждую мс
//******************************************************************************
 
void interrupt isr (void)
{
    OS_EnterInt();
    if (TMR2IF)
    {
        OS_Timer();
        TMR2IF = 0;
    }
    OS_LeaveInt();
}
 
 
 
//******************************************************************************
//  Функции-задачи
//******************************************************************************
 
void Task_LEDS_ON (void)
{
    static char s_i;      // Счетчик
    static char s_cMask;  // Маска для зажигания светодиодов
 
    for (;;)
    {
        s_cMask = 1;                    // Первым зажигаем нулевой светодиод
        for (s_i = 0; s_i < 8; s_i++)
        {
            PORT_LEDS |= s_cMask;       // Зажигаем текущий светодиод
            s_cMask <<= 1;              // Берем очередной светодиод
            OS_Delay(100);              // Пауза в 100 мс
        }
    }
}
 
//------------------------------------------------------------------------------
 
void Task_LEDS_OFF (void)
{
    static char s_i;      // Счетчик
    static char s_cMask;  // Маска для гашения светодиодов
 
    for (;;)
    {
        s_cMask = 0x80;                 // Первым гасим седьмой светодиод
        for (s_i = 0; s_i < 8; s_i++)
        {
            PORT_LEDS &= ~s_cMask;      // Гасим текущий светодиод
            s_cMask >>= 1;              // Берем очередной светодиод
            OS_Delay(150);              // Пауза в 150 мс
        }
    }
}
 
//******************************************************************************
//  Инициализация периферии
//******************************************************************************
 
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_LEDS_ON);
    OS_Task_Create(3, Task_LEDS_OFF);
 
    // Разрешение прерываний
    OS_EI();
 
    // Запуск планировщика
    OS_Run();
}
 
//******************************************************************************
//  end of file
//******************************************************************************

Работа программы

Без синхронизации

Соберем проект (Ctlr+F10) и прошьем его в контроллер. Увидим мы совсем не то, что задумали: вместо того, чтобы сначала по очереди зажечь все светодиоды, а потом по очереди их погасить, программа их зажигает и гасит одновременно. Дело в том, что задачи живут сами по себе и ни одна из них не знает о том, что делает другая. Вот схема работы нашей программы.

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

С синхронизацией

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

Сервис OS_Bsem_Wait автоматически сбрасывает семафор, когда задача получает управление.

После того, как задача зажжет (погасит) все светодиоды, она должна освободить ресурс, т.е. установить семафор, чтобы другие задачи знали, что можно ресурс занимать.

/*****************************************/
void Task_LEDS_ON (void)
{
    static char s_i;      // Счетчик
    static char s_cMask;  // Маска для зажигания светодиодов
 
    for (;;)
    {
        OS_Bsem_Wait(BS_LEDS_ARE_FREE); // Ждем, когда можно будет работать
                                        // со светодиодами
        s_cMask = 1;                    // Первым зажигаем нулевой светодиод
        for (s_i = 0; s_i < 8; s_i++)
        {
            PORT_LEDS |= s_cMask;       // Зажигаем текущий светодиод
            s_cMask <<= 1;              // Берем очередной светодиод
            OS_Delay(100);              // Пауза в 100 мс
        }
        OS_Bsem_Set(BS_LEDS_ARE_FREE);  // Освобождаем ресурс
    }
}
/*****************************************/
void Task_LEDS_OFF (void)
{
    static char s_i;      // Счетчик
    static char s_cMask;  // Маска для гашения светодиодов
 
    for (;;)
    {
        OS_Bsem_Wait(BS_LEDS_ARE_FREE); // Ждем, когда можно будет работать
                                        // со светодиодами
        s_cMask = 0x80;                 // Первым гасим седьмой светодиод
        for (s_i = 0; s_i < 8; s_i++)
        {
            PORT_LEDS &= ~s_cMask;      // Гасим текущий светодиод
            s_cMask >>= 1;              // Берем очередной светодиод
            OS_Delay(150);              // Пауза в 150 мс
        }
        OS_Bsem_Set(BS_LEDS_ARE_FREE);  // Освобождаем ресурс
    }
}
/*****************************************/

Остался последний штрих. Т.к. при инициализации системы все бинарные семафоры сбрасываются, то перед началом работы мы должны установить семафор, разрешающий работу со светодиодами. Это можно сделать, например, в функции main после создания задач:

    // Создание задач
    OS_Task_Create(3, Task_LEDS_ON);
    OS_Task_Create(3, Task_LEDS_OFF);
 
    // Разрешить работу со светодиодами
    OS_Bsem_Set(BS_LEDS_ARE_FREE);
 
    // Разрешение прерываний
    OS_EI();

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

Соберем проект (Ctrl+F10) и прошьем контроллер. Теперь мы видим, что программа работает так, как мы задумали с самого начала. Разберемся подробнее.

Итак, начальные условия:

Когда планировщик начинает работу, он перебирает все задачи в поисках готовых и сравнивает их приоритеты, чтобы вычислить, какую задачу нужно запускать. В нашем случае обе задачи готовы и имеют одинаковый приоритет, поэтому планировщик запустит ту из них, которая была создана первой, т.е. Task_LEDS_ON. Задача, получив управление, входит в свой бесконечный цикл for, где первым делом натыкается на сервис ожидания OS_Bsem_Wait. Разберемся подробнее, что произойдет. Задача видит, что семафор установлен, поэтому она могла бы продолжить выполнение. Но она ничего не знает об остальных задачах программы. Что если этот же семафор ожидает другая задача, имеющая более высокий приоритет? Ведь в этом случае более приоритетная задача должна получить управление. Поэтому задача не продолжает выполнение, а передает управление планировщику, чтобы он проверил, нет ли более важных задач. Сама задача Task_LEDS_ON останется при этом готовой к выполнению.

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

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

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

После зажжения всех светодиодов задача вновь устанавливает семафор, сообщая остальным задачам, что ресурс (порт, к которому подключены светодиоды, свободен). Теперь она возвращается на начало своего цикла и опят натыкается на сервис OS_Bsem_Wait. Она видит, что семафор установлен, но ей надо проверить, нет ли других задач ожидающих этот же семафор, поэтому управление передается планировщику. А планировщик видит, что есть две задачи с одинаковым приоритетом, ожидающие семафор, и он отдает управление той, которая следующая по очереди (чтобы не выполнять всегда одну и ту же), т.е. задаче Task_LEDS_OFF.

Задача Task_LEDS_OFF, получив управление, сразу же сбрасывает семафор (мы уже говорили, что это автоматически делается сервисом OS_Bsem_Wait), а потом работает по той же схеме, что и задача Task_LEDS_ON, только не зажигает светодиоды, а гасит их.

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

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

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

Изменим приоритет одной из задач в функции main:

    OS_Task_Create(4, Task_LEDS_OFF);

Соберем проект (Ctrl+F10) и прошьем контроллер. Мы увидим, что светодиоды зажглись с интервалом в 100 мс и остались этом состоянии. Дело в том, что теперь получается так, что одновременно семафор ожидается двумя задачами с разными приоритетами. Планировщик каждый раз выбирает ту из них, чей приоритет выше, т.е. задачу Task_LEDS_ON.

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

        ...
        OS_Bsem_Set(BS_LEDS_ARE_FREE);  // Освобождаем ресурс
 
        OS_Delay(1);                    // За время этой задержки задача с более
                                        // низким приоритетом успеет стать единственной
                                        // готовой к выполнению
    }

Что произойдет? После установки семафора BS_LEDS_ARE_FREE задача Task_LEDS_ON не сразу же начинает ждать этот семафор, а с некоторой задержкой, в течение которой она будет находиться в режиме ожидания. При этом планировщик увидит, что единственной готовой к выполнению задачей является Task_LEDS_OFF. тут уже приоритет первой задачи значения не имеет, т.к. в сравнении приоритетов участвуют только готовые задачи, а задача Task_LEDS_ON - в ожидании. Получив управление, Task_LEDS_OFF сразу же сбрасывает семафор и начинает выполнять свои операции по гашению светодиодов. По завершении миллисекундной задержки задача Task_LEDS_ON подходит к сервису OS_Bsem_Wait, но на этот момент семафор уже сброшен, поэтому этой задаче ничего не остается делать, кроме как ждать освобождения ресурса, занятого менее приоритетной задачей.

Исключение

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

Эксперимент 2 - два сервиса ожидания подряд

Попробуем переписать задачу Task_LEDS_ON, добавив еще один сервис ожидания семафора:

    for (;;)
    {
        OS_Bsem_Wait(BS_LEDS_ARE_FREE);
        OS_Bsem_Wait(BS_LEDS_ARE_FREE);
        ...

Рассмотрим, что произойдет в этом случае. Задача Task_LEDS_ON, получив управление, первым сервисом OS_Bsem_Wait сбросит семафор. Ко второму сервису она подойдет с уже сброшенным семафором и дальше пройти не сможет. Т.е. программа повиснет, т.к. есть две задачи, ожидающие один и тот же семафор, который нигде, кроме, самих задач не устанавливается. А так как задачи управления не получают, то и семафор они установить не смогут.

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

Заключение

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