:::: MENU ::::

Простой семафор

Данная серия статей посвящена описанию работы RTOS – написанной на основе TNKernel. Исходники с дополнением описанным в данной статье можно скачать по ссылке.

Если вы хотите написать свою RTOS или разобраться в TNKernel читайте так-же:

Простой семафор

Семафор — используется для разделения использования задачами разделяемых ресурсов (например одних и тех-же ножек, usart, spi).

Рассмотрим как реализовать его на примере нашей RTOS.

Еще раз напомним такой пример:

Две задачи должны использовать один и тот-же USART. Одновременно они это делать не должны. И если вдруг задача 1 использует USART и произошла смена контекста на задачу 2. И задача 2 тоже хочет использовать USART, то семафор должен помешать задаче 2.

Семафор это переменная(счетчик). Вначале она инициализируется значением — максимальное количество задач, которые могут одновременно использовать разделяемый ресурс.

Каждый раз при входе задачи в разделяемый ресурс данная переменная уменьшается на 1, а при выходе увеличивается на 1. Вторая задача проверяет эту переменную и если она больше нуля — вход разрешен, меньше — запрещен. При входе и выходе задача 2, так-же изменяет эту переменную.

У нас семафоры будут реализованы через очередь задач (связанные списки). Для каждого семафора — своя очередь. У семафора есть максимальная величина задаваемая в начале — количество задач, которое может одновременно занять ресурс. Если эта величина равна единице (=1), то получается двоичный семафор и ресурс может занять только одна задача — самый распространенный вариант семафора( для spi, usart и т.п.). Задача, занимая семафор, будет уменьшать эту величину семафора на 1 и помещаться в очередь семафора. А освобождая семафор — будет увеличивать на 1 и удалять задачу из очереди данного семафора.

Итак, у нас есть параметры для каждого семафора — структура:

// параметры семафора
typedef struct _sem_param_init
{
  link_init sem_queue;
  uint8_t count;
  uint8_t max_count;
} sem_param_init;

sem_queue — очередь для данного семафора, представлена в виде связанных списков (подробнее см. Работа планировщика RTOS).

count — сам счетчик семафора, который будет уменьшаться при занятие задачей ресурса.

max_count — максимальный размер счетчика count.

Перед тем как использовать семафор его нужно будет создать. Для этого в файле user_func.c объявляем:

sem_param_init sem_test;

А в функции start_user_tasks вызываем функцию:

sem_create(&sem_test, 1); 

Функция sem_create — создает очередь для семафора, который мы объявили выше с параметром count = maxcount = 1.

void sem_create(sem_param_init *sem,
                                uint8_t val)
{
    reset_queue_sem(&(sem->sem_queue));
    sem->count = val;
    sem->max_count = val;
}

reset_queue_sem — сбрасывает очередь для данного семафора — связанный список указывает сам на себя.

void reset_queue_sem(link_init *link_sem)
{
    link_sem->next = link_sem;
    link_sem->prev = link_sem;
}

В задаче для того, чтобы занять ресурс надо вызвать sem_in(&sem_test), а для того, чтобы освободить sem_out(&sem_test);

// захват семафора
// при захвате счетчика уменьшается, если счетчик = 0
// то в разделяемый ресурс зайти нельзя - ставим в очередь семафоров
void sem_in(sem_param_init *sem)
{
    uint8_t priority;

    arm_disable_interrupts();

    // можно войти
    if(sem->count > 0)
    {
        sem->count--;
    }
    // нельзя войти
    else
    {
        // удаление текущей задачи из очереди задач
        // и добавление в очередь данного семафора      
        queue_remove_task(&(curr_run_task->task_queue));
        queue_sem_add(curr_run_task, sem);

        // переход на сл. задачу в очереди приоритетов
        priority = queue_find_priority_next_task();
        next_task_to_run = task_param_from_task_queue(task_queue_priority[priority].next);

        switch_context();       
        arm_enable_interrupts();
    }

    arm_enable_interrupts();
}

Если ресурс занять можно (sem->count > 0) — то уменьшается sem->count и работа задачи продолжается.

Если занять ресурс нельзя (sem->count < 0) — то задача, которая хотела занять семафор, удаляется из очереди планирования (очередь задач по приоритету) queue_remove_task(&(curr_run_task->task_queue)). И добавляется в очередь для данного семафора queue_sem_add(curr_run_task, sem).

void queue_sem_add(task_param_init *task, sem_param_init *sem)
{
    link_init *temp_link;

    // ищем последний элемент в очереди
    temp_link = &(sem->sem_queue);
    while((*temp_link).next != temp_link)
    {
        temp_link = temp_link->next;
    }   

    // добавляем элемент данной очереди в конец очереди
    temp_link->next = &(task->task_queue);
    task->task_queue.prev = temp_link;
    task->task_queue.next = &task->task_queue; 
}

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

После удаления задачи из очереди планирования (по приоритетам) и добавления ее в очередь данного семафора, необходимо найти сл. задачу в очереди планировщика по приоритету queue_find_priority_next_task и назначить ее как следующую задачу для запуска. После включается планировщик switch_context и происходит смена контекста.

Освобождение семафора:

// освобождаем семафор - увеличиваем на 1 счетчик
void sem_out(sem_param_init *sem)
{
    task_param_init *next_task;

    arm_disable_interrupts();

    // очередь семафора не пустая (первый элемент не указывает сам на себя) - увеличиваем счетчик
    // и вкл. следующую задачу в очереди на планирование 
    if((sem->sem_queue).next != &(sem->sem_queue))
    {
        // получение сл. задачи
        next_task = task_param_from_task_queue((sem->sem_queue).next);

        // удаляем из очереди семафора и вкл. в очередь планирования (по приоритетам) 
        queue_remove_task(&(next_task->task_queue));
        queue_task_add(next_task);
    }
    else
    {
        sem->count++;
        // count не должен быть больше максимального значения
        if(sem->count > sem->max_count)
        {
            sem->count = sem->max_count;
        }
    }

    arm_enable_interrupts();
}

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

Таким образом, вся работа с семафорами происходит в функциях sem_in и sem_out.

Для примера можно применить данный код:

align_attr_start unsigned int task_1_stack[128] align_attr_end;
task_param_init task_1;
void task_1_func(void *par);

align_attr_start unsigned int task_2_stack[128] align_attr_end;
task_param_init task_2;
void task_2_func(void *par);

sem_param_init sem_test;

uint32_t tm;

void task_1_func(void *par)
{
    while(1)
    {
        sem_in(&sem_test);
        GPIO_SetBits(GPIOB, GPIO_Pin_7);
        task_move_tail_queue_priority();                

        tm = 10000;
        while(tm != 0)
            tm--;

        sem_out(&sem_test);
    }
}

void task_2_func(void *par)
{
    while(1)
    {
        sem_in(&sem_test);
        GPIO_ResetBits(GPIOB, GPIO_Pin_7);
        task_move_tail_queue_priority();

        tm = 20000;
        while(tm != 0)
            tm--;

        sem_out(&sem_test);
    }   
}

void start_user_tasks(void)
{   
    sem_create(&sem_test, 1);

    task_create(&task_1,
                            task_1_func,
                            NULL,
                            1,
                            &task_1_stack[0],
                            128);

    task_create(&task_2,
                            task_2_func,
                            NULL,
                            1,
                            &task_2_stack[0],
                            128);
}