разработка и программирование электронных устройств

TICS RTOS. Основы планирования задач

В прошлой статье я описал настройку DOS-эмулятора DOSbox , привел примеры для операционной системы TICS , компилируемые под MS-DOS с помощью Borland Turbo C++ 3.1. Запуская примеры в пошаговом режиме и анализируя используемую оперативную память я пришел к выводу, что полноценная работа TICS возможна только на микроконтроллерах с объемом ОЗУ выше десяти килобайт.

Большинство AVR микроконтроллеров остаются недоступны для ОСРВ TICS. Имеет смысл перенести систему лишь на старшие модели (например Atmega1284 ). Более интересной может оказаться адаптация TICS для использования на микроконтроллерах с ядром Cortex-M3.

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

Как известно, структура программы для микроконтроллера содержит бесконечный цикл внутри функции main(), в теле которого поочередно вызываются различные функции.

int main(void)
{
	init();
	while( TRUE )
	{
		func1();
		func2();
		func3();
	}
	return 0;
}

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

Если создать массив указателей на вызываемые функции, то исходный код сократится до одной единственной функции start_tasks(), вызываемой внутри тела основной функции main().

int main(void)
{
	init();
	start_tasks();
	return 0;
}

Внутри функции start_tasks() выполняется бесконечный цикл, в теле которого происходит последовательный вызов задач через указатель на функцию задачи и передача параметров задаче через указатель на аргументы функции. Признаком окончания очереди задач task_queue является нулевой указатель на очередную задачу в очереди.

void start_tasks(void)
{
    task_t * tp;
 
    while( TRUE )
    {
        tp = task_queue;
 
        while( tp->task_ptr != NULL )
        {
            tp->task_ptr( tp->arg_ptr );
            tp++;
        }
    }
}

Очередь задач task_queue представляет собой массив указателей типа task_t на структуры задач .

typedef void (* task_ptr_t )(void * );
typedef struct task_s {
    void (* task_ptr)(void *);
    void * arg_ptr;
}task_t;
static task_t       task_queue[ MAX_TASKS ];

Не случайно внутри структуры task_s тип указателя на аргументы функции объявлен как пустой (void *). Это позволит передать в задачу( а также из нее ) аргументы любого типа, в том числе указатели на различные структуры данных.

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

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

int make_task( task_ptr_t ptr, void * arg, int di_opt )
{
    int r = ERROR;
    DI(di_opt);    
    if( task_counter >= MAX_TASKS ) {
        error_no = ERR_MAXTASK;
    }
    else {
        if( (task_counter + 1) == ( MAX_TASKS - 1 ) ){
            task_queue[ task_counter + 1 ].task_ptr = NULL;
            task_queue[ task_counter + 1 ].arg_ptr = NULL;
        }
            task_queue[ task_counter ].task_ptr = ptr;
            task_queue[ task_counter ].arg_ptr = arg;
            task_counter++;
            r = task_counter;
    }    
    EI(di_opt);
    return r;
}

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

Если назначаемая задача является предпоследней в очереди, то на место следующей ( последней ) задачи в очереди записывается нулевой указатель. Макросы DI(di_opt) и EI(di_opt) предназначены соответственно для запрещения и разрешения прерываний в зависимости от опции di_opt, которая может принимать одно из двух возможных значений — TRUE или FALSE.

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

int init_RTK(int di_opt)
{
   int r = ERROR;
   uint16_t counter;   
   DI(di_opt);
   if( !task_counter ) {
        error_no = ERR_NOTASKS;
        EI(di_opt);
        return r;
   }
    if( task_counter < ( MAX_TASKS - 1 ) ) {
            task_queue[ task_counter ].task_ptr = NULL;
            task_queue[ task_counter ].arg_ptr = NULL;
    }
    counter = TCNT_get( F_CPU );    
    if( counter == ERROR ) {
        error_no = ERR_NOFREQ;
        EI(di_opt);
        return r;
    }
    init_timer( counter, FALSE );
   r = OK;
   EI(di_opt);
   return r;
}

На момент вызова функции init_RTK() все задачи должны быть инициализированы, то есть попросту говоря все вызовы make_task() должны быть произведены до выполнения инициализации с помощью init_RTK() .
В переменной task_counter храниться следующий за последней назначенной задачей номер.

Теперь настало время поговорить о таймерах. В качестве системного таймера наш простейший планировщик для микроконтроллеров AVR использует первый 16-разрядный таймер TIMER1 в режиме таймера-счетчика с обработкой прерывания по переполнению.

Таймер настраивается таким образом,что переполнение его значения происходит каждые 5 мсек времени. Внутри обработчика прерывания от таймера выполняется увеличение значения глобальной переменной Tics, относительно которой производятся отсчеты временных интервалов.

void TIMER1_OVF_vect(void)
{
    Tics++;
    TCNT1 = start_timer_counter;
}

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

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

#define timeout(t)                  (Tics >= t)
#define start_timer( dt )           Tics + dt

Чтобы стало понятней , рассмотрим применение этих макросов на примере.

uint64_t timer;
timer = start_timer(100L);
while(!timeout(timer))
;

Программа зациклиться до тех пор, пока значение Tics не достигнет ранее установленного относительно переменной Tics значения, то есть через 100 циклов по 5 мсек после запуска программного таймера.

Как видите организация таймеров предельно проста. Но при такой структуре возникает два тонких момента. Во-первых обратите внимание на макрос timeout(t). В нем выполняется сравнение «больше или равно». Это связано с тем, что между операциями анализа временного интервала может пройти больший промежуток времени, нежели 5 мсек( один тик ). Поэтому значение Tics может «проскочить» равенство установленному таймеру.

Во – вторых, проблема возникает на краю диапазона значения переменной счетчика тиков( тип uint64_t ). При переполнении значения счетчика значение Tics может оказаться больше установленного значения таймера,что вызовет немедленное срабатывание.

Бесперебойную работу программного таймера способен обеспечить следующий вариант макроса timeout(t).

#define timeout(t)                  (Tics == t)

Но в этом случае, как было отмечено ранее, момент равенства можно «проскочить», если между сравнениями пройдет временной промежуток выше 5 мсек. Для стабильной работы программы с различным временем «перебора» очереди задач можно установить коэффициент деления, который увеличит тик таймера.

#define TICS_DIV     (20)     // 20x5ms=100ms systick
void TIMER1_OVF_vect(void)
{
	tics_5ms++;
	if (!(TICS_DIV - tics_5ms)) {
		tics_5ms = 0;
	    Tics++;
	}
    TCNT1 = start_timer_counter;
}

Архив с примером проекта для Eclipse можно скачать как обычно с моего репозитория. Называется он State_machine_AVR.zip и представляет из себя простейший автомат состояний, выполняющий мерцание светодиодом.

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

Итак , рассмотрим концептуальный уровень работы нашей программы конечного автомата.

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

Задача конечного автомата представляет собой функцию без возвращаемого значения и с значением текущего состояния автомата в качестве аргумента функции. Внутри этой функции размещается конструкция switch с возможными состояниями в качестве анализируемых значений.

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

Задача led_flash_task выполняет мерцание светодиодом с частотой 1 Гц.

void led_flash_task(state_machine_t * data )
{
    switch(data->state) {
        case LED_INIT :
            set_bit(LED_PORT,LED_BIT);
            data->timer = start_timer(100L);
            data->state = LED_OFF;
            break;
        case LED_OFF :
            if ( timeout(data->timer) ) {
                clr_bit(LED_PORT,LED_BIT);
                data->timer = start_timer(100L);
                data->state = LED_ON;
            }
            break;
        case LED_ON :
            if ( timeout(data->timer) ) {
                set_bit(LED_PORT,LED_BIT);
                data->timer = start_timer(100L);
                data->state = LED_OFF;
            }
            break;
    }
}

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

Как видите, ничего сложного в планировании выполнения задач нет. Конечно, мы рассмотрели один из самых простых планировщиков задач. Однако и в сложных операционных системах реального времени принципы планирования задач остаются неизменны на протяжении многих лет.

Продолжение следует…

1 Comment to TICS RTOS. Основы планирования задач

  1. Volldemar's Gravatar Volldemar
    3 июля 2011 at 14:18 | Permalink

    Ссылка на оригинальную документацию:

    http://www.concentric.net/~tics/ticsds.htm
    http://www.concentric.net/~tics/ticsinfo.htm

Leave a Reply

You must be logged in to post a comment.