Программные таймеры. Часть 1

25/10/2009 - 21:50

Исходный код библиотеки    

Для наглядности я объединил хедер и сишный файл.


//состояния таймера - неработающий, активный, отработавший
enum StateTimer {IDLE, ACTIVE, DONE};

//структура программного таймера
typedef struct{
  unsigned int time;           //через какое время запустить
  unsigned int period;         //период повторения
  enum StateTimer state;    //текущее состояние
  void (*pFunc)(void);         //указатель на функцию
}SoftTimer;

//максимальное число таймеров
#define MAX_TIMERS  4

//число созданных таймеров
unsigned char AmountTimers = 0;

//массив указателей на таймеры
SoftTimer* SoftTimers[MAX_TIMERS];

//функция создания программного таймера
void CreateTimer(SoftTimer *CurSoftTimer, unsigned int time, unsigned int period , enum StateTimer state, void (*pFunc)(void)){
  SoftTimers[AmountTimers] = CurSoftTimer;
  CurSoftTimer->time = time;
  CurSoftTimer->period = period;
  CurSoftTimer->state = state;
  CurSoftTimer->pFunc = pFunc;
  AmountTimers++;
}

//функция проверки таймеров
void CheckTimer(void){
  for(unsigned char i = 0; i < AmountTimers; i++){
    if (SoftTimers[i]->state == ACTIVE){
      if (SoftTimers[i]->time == 0){
        SoftTimers[i]->pFunc();
        if (SoftTimers[i]->period != 0) SoftTimers[i]->time = (SoftTimers[i]->period-1);
        else SoftTimers[i]->state = DONE;
      }
      else(SoftTimers[i]->time)--;
    }
  }

Пояснение к коду

Структура программного таймера
 
Программный таймер должен:
- уметь запускаться сразу или через заданное время
- работать в режиме однократного или переодического запуска
- иметь три состояния – остановлен, активный, закончил работу
- сигнализировать об окончании счета

На основании этих требований определяем структуру SoftTimer.

//состояния таймера - неработающий, активный, отработавший
enum StateTimer {IDLE, ACTIVE, DONE};

//структура программного таймера
typedef struct{
  unsigned int time;           //через какое время запустить
  unsigned int period;         //период повторения
  enum StateTimer state;    //текущее состояние
  void (*pFunc)(void);         //указатель на функцию
}SoftTimer;

   Для временнЫх переменных я выбрал тип данных int. Это более расточительно в плане ресурсов, но зато позоляет получать временные интервалы большой длительности.
   time – определяет через какое время таймер сработает
   period – определяет период работы таймера. Если период равен 0, то таймер будет работать в режиме однократного запуска.
  state – хранит текущее состояние таймера, она типа enum. Можно было определить режимы работы с помощью define, но так более правильно.
  Функция, на которую в структуре определен указатель, будет запускаться по окончании счета таймера.

Массив указателей на таймеры

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

//максимальное число таймеров
#define MAX_TIMERS  4

//число созданных таймеров
unsigned char AmountTimers = 0;

//массив указателей на таймеры
SoftTimer* SoftTimers[MAX_TIMERS];

Создание таймера

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

void CreateTimer(SoftTimer *CurSoftTimer, unsigned int time, unsigned int period , enum StateTimer state, void (*pFunc)(void)){
  SoftTimers[AmountTimers] = CurSoftTimer;
  CurSoftTimer->time = time;
  CurSoftTimer->period = period;
  CurSoftTimer->state = state;
  CurSoftTimer->pFunc = pFunc;
  AmountTimers++;
}

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

Функция опроса таймеров

   Функция опроса таймеров запускается в прерывании аппаратного таймера/счетчика

//функция проверки таймеров
void CheckTimer(void){
  for(unsigned char i = 0; i < AmountTimers; i++){
    if (SoftTimers[i]->state == ACTIVE){
      if (SoftTimers[i]->time == 0){
        SoftTimers[i]->pFunc();
        if (SoftTimers[i]->period != 0) SoftTimers[i]->time = (SoftTimers[i]->period-1);
        else SoftTimers[i]->state = DONE;
      }
      else(SoftTimers[i]->time)--;
    }
  }
}

   В цикле for последовательно опрашиваются программные таймеры. Если состояние таймера – активный и счетчик таймера time равен нулю -  запускается его функция. Если таймер периодический, то затем он перезапускается, если нет, ему присваивается состояние - отработавший. Если таймер "активный", но его счетчик еще не равен нулю, выполняется декремент счетчика. 
 

Тестовый проект

   Итак, необходимый минимум готов. Можно проверить программные таймеры/счетчики в деле. Для этих целей я написал простой проект – 4 программных таймера запускающихся одновременно, но с разной частотой. Функции таймеров инвертируют биты порта В и по этим сигналам легко проверить работу алгоритма, правда для этого нужен осциллограф. Привожу содержимое main.c. Полная версия проекта для IARa и WINAVR в конце статьи.

//программирование микроконтроллеров AVR на Си
//тестовый проект с программными таймерами
//Pashgan   ChipEnable.ru

#include <ioavr.h>
#include <intrinsics.h>
#include "Timers.h"
#include "bits_macros.h"

//объявляем переменные типа SoftTimer
SoftTimer timer1;
SoftTimer timer2;
SoftTimer timer3;
SoftTimer timer4;

//расписываем функции программных таймеров
void Clk1(void){
  InvBit(PORTB, 0);
}

void Clk2(void){
  InvBit(PORTB, 1);
}

void Clk3(void){
  InvBit(PORTB, 2);
}

void Clk4(void){
  InvBit(PORTB, 3);
}

int main( void )
{
  //инициализация порта
  PORTB = 0x00;
  DDRB = 0xff;
 
  //инициализация таймера Т0 - прерывания каждую ms
  TIMSK = (1<<OCIE0);
  TCCR0=(1<<WGM01)|(0<<WGM00)|(0<<COM01)|(0<<COM00)|(0<<CS02)|(1<<CS01)|(1<<CS00);
  TCNT0 = 0;
  OCR0 = 0x7d;
 
  //инициализируем таймеры
  CreateTimer(&timer1, 0, 1, ACTIVE, Clk1);
  CreateTimer(&timer2, 0, 2, ACTIVE, Clk2);
  CreateTimer(&timer3, 0, 3, ACTIVE, Clk3);
  CreateTimer(&timer4, 0, 4, ACTIVE, Clk4);
 
   //разрешаем прерывания и тупим в цикле

  __enable_interrupt();
  while(1);
  return 0;
}

//********************************
//Прерывание таймера/счетчика Т0
#pragma vector = TIMER0_COMP_vect
__interrupt void Timer0CompVect(void)
{
  CheckTimer();
}

Заключение

   Алгоритм, конечно, расточительный. Во-первых в прерывании запускается функция. Во-вторых перебор таймеров происходит последовательно - чем больше программных таймеров, тем больше времени это займет. В-третьих функции программных таймеров тоже запускаются в прерывании. Как можно оптимизировать? Не использовать много таймеров. Сделать функцию CheckTimer встраиваемой. Период работы аппаратного таймера выбрать большим. Функции программных таймеров сделать короткими. 

Напоследок картинка с осциллографа
результат работы программы

Файлы

Проект для IARа.   Проект для WINAVR.


Comments   

# Guest 2009-11-03 08:06
Программный таймер в такой реализации бесполезен без диспетчера задач.

гораздо эффективней при такой организации сделать просто "Системное время" скажем 32байтное, учитывающее каждый такт процессора. И относительно него отслеживать все процессы.
# Guest 2009-11-03 11:38
вполне имеет право на существование. и вовсе не бесполезно. да и не все программы с диспетчером делаются.
# Pashgan 2009-11-04 01:35
нет чтоб поддержать .... он сразу в критику пустился. дойдет и до диспетчера дело.
# Guest 2010-02-06 15:25
Pashgan, а зачем Вы в проектах на "WinAVR" используете хидер "bits_macros.h" ? Он, на мой взгляд, только больше путает новичков! В WinAVR есть внутр. ф-ция bit_is_clear, созвучная в Вашей из хидера BitIsClear! Да и Ваша "привязана" к порту B.
# Pashgan 2010-02-07 19:55
Так проще переносить проекты под разные компиляторы.
# Guest 2010-03-15 22:16
осталось объявить в этой проге пятый таймер..
# qwerty 2011-05-13 16:05
если создать последний таймер большим и периодическим, то предыдущие если они одноразовые, исполнятся и станут не более чем пустой памятью, использовать их нельзя.
в целом не очень, даже для новичков.
# Pashgan 2011-09-21 20:36
Есть некоторые недостатки, но ученые бьются над ними.
# Guest 2011-12-13 05:23
Дистрибутив проекта для WINAVR неправильный. Там тоже самое, что и в IAR.
# Илья21 2013-08-17 16:22
Спасибо тебе большое Pashgan! Без твоих статей я бы не с чем не разобрался
# Pashgan 2013-08-17 17:20
Я рад, что материал помог тебе в чем-то разобраться.
# Outbreak 2013-09-30 13:13
Делать набор таймеров методом инкрементирован ия счётчиков - это весьма печально.
Существует намного более удобный способ и основан он на обычных мат операциях.
Допустим, у нас имеется 16-битный счётчик, не останавливающий ся при переполнении. Известно, что 0x0000 - UINT16MAX = 1.
Как только мы решили запустить счётчик, инициализируем соответствующую переменную его значением в текущий момент времени.
Дальше в прерывании таймера или в периодически вызываемой функции просто пробегаемся по массиву наших переменных и вычитаем из текущего значения счётчика их величины.
Пример релазии для STM32, период таймера 0,5 мс:
#define CNT16_GET() (uint16_t)(TIM- >CNT) /*0.5 ms */
#define CNT16_EVENT(_CN T, _MS) ((uint16_t)(CNT 16_GET() - (uint16_t)(_CNT )) > (uint16_t)(2*_M S))
#define CNT16_UPDATE(_C NT) _CNT = CNT16_GET()
#define CNT16_DIFF(_CNT 0, _CNT1) (uint16_t)(((ui nt16_t)_CNT0 - (uint16_t)_CNT1 ) >> 1) /*1 ms */
#define CNT16_DIFF_CURR (_CNT) CNT16_DIFF(CNT1 6_GET(), _CNT)
# Outbreak 2013-09-30 13:15
Небольшой коментарий к коду:
CNT16_EVENT - флаг срабатывания таймера,
CNT16_UPDATE - обновление переменной,
остальное не важно.
# Pashgan 2013-09-30 18:30
ну как печально.. как простое решение имеет место быть.
# Outbreak 2013-09-30 20:47
Как показала практика - реализация методом инкрементирован ия не удачная по нескольким причинам,а именно:
1) повышенный расход ресурсов (инкремент + прерывания);
2) не всегда высокая точность, т.к. прерывание не очень-то хочется делать слишком частыми. Если в качестве таймера использовать таймер RTOS - разрешающая способность выходит ну явно не лучше 1 мс, а скорее даже 5-10. Приведённые расслуждения о точности справедливы только для случая программировани я методом шагового автомата, при этом время для каждой задачи должно быть явно не больше требуемой точности.

Достоинства приведённого мною метода:
1) потребление ресурсов на таймер минимально, особенности если используется каскад таймеров с различными периодами в пределах одной функции-проверк и срабатывания одних таймеров можно легко выполнять при срабатывании других.
2) создание таймера - определение одной (счётчик) или двух переменных (счётчик и период, если T = 0, считаем, что таймер не активен; как вариант ещё - счётчик и флаг) в любой точке шагового автомата.
Недостатки:
1) для задач предельно жесткого реального времени данный метод не даём существенных преимуществ, т.к. всё равно надо работать в прерывании.
2) можно пропустить момент срабатывания таймера при малых периодах таймера и сложных, не поддающихся переписыванию на шаговый автомат.

Кстати, callback-функци и, вызываемые из прерывания - не очень хорошая идея, т.к. подходят только для очень быстрых функций.
# Pashgan 2013-10-02 10:40
О недостатках я написал в конце материала.
Quote:
Алгоритм, конечно, расточительный. Во-первых в прерывании запускается функция. Во-вторых перебор таймеров происходит последовательно - чем больше программных таймеров, тем больше времени это займет. В-третьих функции программных таймеров тоже запускаются в прерывании. Как можно оптимизировать? Не использовать много таймеров. Сделать функцию CheckTimer встраиваемой. Период работы аппаратного таймера выбрать большим. Функции программных таймеров сделать короткими.
Тогда я подсмотрел это решение в демо программе платы AVR Butterfly, только там не использовалась структура.

У вас недостаточно прав для комментирования.