Простой программный таймер для конечных автоматов

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

Сам проект ничего особого, простая логика на конечных автоматах. Но мне понравился там как реализован таймер. Я обычно предпочитаю динамический таймер, а идею глобального времени только высказывал, но так и не применил, т.к. в основном все делал на диспетчере или RTOS и там этот подход не особо удобен. Но если логика построена на простом суперцикле с набором функций-автоматов в main цикле, то такая реализация таймера фактически стала классикой. Вот, пользуясь случаем, заполняю этот пробел. Архитектура тут не важна. Главное чтоб был таймер способный давать прерывание раз в тик. Тик обычно 1мс.

▌Принцип работы и использование
У нас есть глобальная переменная TimeMs которая инкрементируется по прерыванию таймера раз в 1мс. Когда мы хотим поставить выдержку, то просто берем текущее значение TimeMs прибавляем к нему нашу выдержку и запоминаем все это в статичной переменной, пусть будет Delay, которая определена непосредственно в той функции автомата которая эту задержку использует. И при каждом следующем входе в автомат она будет проверять нет ли у нас условия Delay >= TimeMs.

То есть автомат мигалка будет выглядеть так:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
void Blink (void)
{
 
// Переменные объявленные как static не исчезают после выхода из функции. А сохраняют свое состояние.
static uint8_t blink_state = 0;     // Переменная состояния конечного автомата
static uint32_t Delay;              // Переменная программного таймера мигалки, время моргания
 
switch (blink_state)
  {
  case 0:                           // Первый вход, инициализация и первый поджиг. 
               {
               Delay = MainTimerSet(1000);   // Ставим задержку на 1000мс
               LED_ON();                     // Зажигаем диодик
               blink_state = 1;              //  Переходим в следующее состояние автомата
               break;                        //  Выход из состояния
               }
 
  case 1:                                    // Первая стадия рабочего цикла (Не горим) 
             {            
             if ( !MainTimerIsExpired(Delay) ) break;      // Если 1с не прошла - сразу выходим
                                                           // Функция MainTimerIsExpired проверяет по
                                                           // таймерной переменной TimeMs не
                                                           // стал ли Delay меньше чем TimeMs
 
             LED_OFF();                                    // Если секунда прошла, то гасим диодик
             Delay = MainTimerSet(500);                    // Запоминаем время выключенной фазы 0.5с
             blink_state = 2;                              // Переключаем автомат во вторую стадию
             break;                                        // Выход из состояния
             }
 
case 2:                                    // Вторая стадия рабочего цикла (Горим) 
             {            
             if ( !MainTimerIsExpired(Delay) ) break;      // Если 0,5с не прошла - сразу выходим
 
             LED_ON();                                     // Если секунда прошла, то зажигаем диодик
             Delay = MainTimerSet(1000);                   // Запоминаем время включенной фазы 1с
             blink_state = 1;                              // Переключаем автомат в первую стадию
             break;                                        // Выход из состояния
             }
 
default: break
  }
}

Ну, а в самом Main вызов автоматов разных задач выглядит как то так:

1
2
3
4
5
6
7
8
9
10
void main (void)
{
while (1)
  {
  KeyScan();       //  Автомат отвечающий за сканирование клавы бла бла бла
  LCD_process();   // Автомат работающий с дисплеем бла бла бла
 
  Blink();         // Наша мигалка
 
  }

Теперь немного о реализации самой библиотечки таймера.

▌Время
В коде библиотеки должна быть определена глобальная функция времени. Область видимости — модуль таймера, дальше не обязательно. Разрядность исходя из длительности задержки и длины тика. Причем максимально возможное число тиков не должно быть больше чем половина максимального значения переменной. Т.е. для 16 разрядной переменной максимально возможное беззнаковое число это 65535, а половина, соответственно 32767, т.е. если у вас TimeMs 16 разрядное, то максимальная выдержка будет 32767 тика или 32.767 секунды. Почему только половина? А это особенность реализации защиты от переполнения. Ниже покажу. Поэтому удобней всего брать сразу 32 разрядное значение.

1
static volatile uint32_t TimeMs = 0;

▌Инициализация в начале программы

1
2
3
4
5
6
7
void MainTimerInit(void)
{
TimeMs = 0;	// Обнуляем переменную времени. 
 
// Запускаем аппаратный таймер
TimerInit();
}

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// Обработчик прерывания таймера
void TimerInterrupt_Handler()
{
 TimeMs++;  // Увеличиваем переменную времени. 
}
 
// Тут надо не проебать атомарность.  Данный кусок кода был скопипащен с STM32  и тут у меня все операции 
// атомарны, т.к. сам проц 32 разрядный и инкремент делает за один такт. Если же реализация будет на 
// каком-либо 8 разрядном контроллере, то надо обеспечить атомарность операции. Чтобы никто не смел
// прервать выполнение. 
// Для AVRGCC это делается заключением инкремента в такую вот конструкцию:
// ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
//	{
//		TimeMs++;
//	}
//
// И не забыть подключить библиотечку атомарных операций  #include <util/atomic.h>
// То же касается и других операций с TimeMs которые будут далее. Помните про это!!!

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

1
2
3
4
uint32_t MainTimerGetMs(void)
{
return TimeMs;
}

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

1
2
3
4
uint32_t MainTimerSet(const uint32_t AddTimeMs)
{
return TimeMs + AddTimeMs;
}

Результат полагается сохранить и периодически спрашивать….

▌А не пора ли?

1
2
3
4
5
bool MainTimerIsExpired(const uint32_t Timer)
{
	if ((TimeMs - Timer) < (1UL << 31)) return (Timer <= TimeMs);
	return false;
}

Теперь разберем как сделана отработка переполнения таймера. Ведь рано или поздно наступит момент, когда в переменной TimeMs, скажем, будет значение FF FF FF 00, а мы захотим выдержку на 0x101 тиков. В результате сложения в MainTimerSet получим переполнение беззнакового числа и результат для сохранения в Timer = 00 00 00 01.

И если тупо сравнивать стал ли TimeMs >= чем Timer, то уже в следующем проходе автомата FF FF FF 00 будет однозначно больше чем 00 00 00 01 и все наши выдержки полетят к чертям.

Конечно, можно взять да бабахнуть сразу uint64_t на все таймерные переменные и запаса тиков до переполнения нам там хватит на пол миллиарда лет. Но вдруг вы хотите прожить дольше? В конце концов, что такое пол миллиарда лет по сравнению с вечностью? Вот и я про то же!

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

Вот эта вот шняга

1
1UL << 31

просто берет и ставит 1 в последний бит нашего 32 битного числа. Получается 0х80000000. Что в знаковой форме минимально возможное число и есть.

Естествтенно если TimeMs иметь другую разрядность, скажем ,16, то будет уже

1
1UL << 15

и так далее.

А после выдаем true или false в зависимости от выяснения на какой половине диапазона мы находимся. Вот в этом вот условии

1
Timer <= TimeMs

Если до сих пор не догнали суть, то попробуйте на листочке просчитать разные комбинации Time и TimeMs вблизи границ диапазонов, используя виндовый калькулятор в hex режиме или погоняйте в симуляторе. Причем рекомендую сделать там разрядность переменных 8 бит иначе в ноликах запутаетесь :)

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

Исходники выдранные из проекта:
MainTimer.c
MainTimer.h

59 thoughts on “Простой программный таймер для конечных автоматов”

  1. А мне больше нравится делать так:

    if (GetTick() — last_update_time > 100) {
    // что-то делаем
    last_update_time = GetTick();
    }

    Проблемы с переполнением в этом случае нет.

      1. Настанет, но вычитание сработает корректно. Вот, допустим, у нас переменная таймера в восемь бит и в last_update_time попало 0xFF. Дальше таймер протикал пять тактов до 0x04. Вычитаем 0x04-0xFF, получаем 5, как и было задумано.

  2. Это очень сложно и неочевидно. Имеем лишние проверки условий. Половина значений счетчика не работает.
    Если в переменной Delay хранить значение текущего момента, то все становится проще.
    И все делается автоматически.

    Delay = MainTimerGetMs()
    if ( MainTimerGetMs() — Delay >= DELAY_VALUE )
    {
    // Делаем что положено по времени
    }

    до переполнения далеко
    Delay запомнили например 10
    MainTimerGetMs() дало 20
    имеем 20-10 = 10 прошло тиков.

    случай переполнения счетчик uint8_t
    Delay запомнили например 250
    MainTimerGetMs() дало 10
    имеем 10-250 = 16 !!! все правильно посчитает
    если мы указали правильный беззнаковый тип!!!

    Почему так — такие правила
    0-1 = 1111 1111
    К нулю как бы добавляется 1 в старший разряд
    и потом делается вычитание
    1 0000 0000

    0000 0001
    =
    1111 1111

    Можно проверить нашу 10 дополняем
    1 0000 1010

    1111 1010
    =
    0001 0000 (16)

    То что произошел заем мы можем посмотреть во флагах
    но нас это не беспокоит

  3. Не, не работает:
    ставим TimeMs = 0xFFFFFFF0;
    засекаем 10 тактов: uint32_t t1 = MainTimerSet(10);
    и проверяем следующие 20 тактов: 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0 0

      1. Полный код:

        int main() {
            TimeMs = 0xFFFFFFF0;
            uint32_t t1 = MainTimerSet(10);
            for(uint32_t i = 0; i < 20; ++i) {
                printf("%d ", MainTimerIsExpired(t1));
                SysTick_Handler();
            }
        
            return 0;
        }
        

        Вывод: 0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 0 0 0 0
        Т.е. сначала таймер "не пора" (ok); через 10 мс — "пора" (ok); еще через 6 мс — "не пора" (oops).

        1. Хм. Внезапно. А вот это реальный косяк, ведь при приближении T1 к переполнению количество единичек снижается, и на
          FE будет всего одни шанс поймать событие. Пропустил — все.

          1. Фикс очень простой:

            
            uint32_t MainTimerRemainingMs(const uint32_t Timer)
            {
                if ((TimeMs - Timer) > (1UL < < 31))
            
                    return (Timer - TimeMs);
                else
                    return 0;
            }
            
            
            bool MainTimerIsExpired(const uint32_t Timer)
            {
                return ((TimeMs - Timer) < (1UL << 31));
            
            }
            
            
  4. // ну зочем такие приседания? 
    
    volatile uint32_t delay;
    //volatile uint32_t delay2; // для КА №2
    //volatile uint32_t delay3; // для еще чегонить
    
    void TimerInterrupt_Handler(){ // 1ms tick
      if(delay)delay--;
      //if(delay2)delay--;
      //if(delay3)delay--;
    }
    
    //...
      case 0: {
                   delay = 1000;
                   LED_ON();
                   blink_state++;
                   } break;
       case 1: {            
                 if(delay) break;
                 LED_OFF();
                 delay = 500;
                 blink_state = 0;
                 }break;
    //...
    
    /*
    а вот еще хотел показать кунгфу - буфер событий из одной переменной
    */
    
    volatile uint32_t ms1events; // можно назвать milliseconds =)
    
    void TimerInterrupt_Handler(){ // 1ms tick
      ms1events++;
    }
    
    //... где-то в бубенях, без претензий на жесткое синхро, // но чтобы в среднем за большой интервал делалось
    // заданное количество дел плюс-минус лапоть
    // при условии, что система полюбому разгребет
    // накиданные задачи, но не всегда вовремя.
    // можно вызывать из вечного супер-цикла
    
    void I2C(UART/Touch/PredictCalc/USB/Logging)Tasks(){
      while(ms1events--){
        //
        // do something
        //
      }
    }
    //...
    
    
    1. У тебя под каждый таймер переменная. А если их 100? Это какого размера обработчик прерывания будет.

      1. Даже если их 100, то обработаются они достаточно быстро. Ну и ресурсов на обслуживание таких счетчиков надо гораздо меньше.

        1. Достаточно понятие очень и очень относительное. Кому достаточно? Суди сам, возьмем 8 разрядную систему и 16 разрядный счетчик, например.

          if — сам сравнение не менее двух команд, так как сравнить надо оба байта. Плюс переход команда, дальше декремент еще две команды итого по 5 команд на счетчик. А если счетчиков много, то умножаем на все это. Не забываем про то, что все это должно быть атомарным в прерывании. Плюс вход и выход в прерывание с прогрузом регистров. Т.е. у нас на десятки (!) тактов блокируется система полностью. Тогда как в реально требовательных системах приходится даже вход-выход делать по минимуму, чтобы вскочил-выскочил максимально быстро. И все это ради чего? Ради таймеров? Да ну нахер. Потому и делают декремент одной переменной. Это быстрый вход, по команде на байт числа, без условий, без нихуя, что даже дворд обработается за десяток тактов в худшем случае. А считать лучше смещения как в моем случае или как выше в комментах предлагали более изящный (хотя и менее читаемый) способ.

      2. А, примерно понимаю ситуацию — проект красиво структурирован и разбит на достаточно независимые компоненты. Тогда пихание тучи счётчиков в обработчик таймера выглядит весьма коряво.

        1. И это тоже. Обработчик то один и разбить его на кучу модулей не выйдет. Опять же область видимости зря расширять на все подряд тоже не хорошо.

    2. // ой, косяк. перед
      delay = 1000;
      // надо таки вставить
      if(delay) break;
      // ну и понадеемся, что глобальная переменная будет в 0
      // при начале работы КА

  5. Интересно, но в свете замечания dev, статья какая то незавершенная.
    DI HALT подправь диспетчер в статье пожалуйста.

  6. // Тут надо не проебать атомарность. Данный кусок кода был скопипащен с STM32 и тут у меня все операции
    // атомарны, т.к. сам проц 32 разрядный и инкремент делает за один такт.

    Шутите! Cortex — Mx — это RISC, там нет атомарного инкремента! Инкремент переменной будет выглядеть типа как

    LDR r0,[r1]
    ADDS r0,r0,#1
    STR r0,[r1]

    где в r1 — адрес переменной.

      1. Не, ну на круг в данном примении — не страшно, но утверждение об якобы атомарности 32-битных инкрементов в комменте может увести слабых психикой и неопытных на скользкий путь в будущем.

        Я на тему клюнул, т.к. уже лет почти 10 все наши системы работают на квази-многозадачной кооперативке, где диспетчер построен именно по принципу топика. И тема сравнения счетчика, которой в комментах посвящено так много внимания, на самом деле проста, если сделать вычитание и сравнение на дистанцию. Если дистанция не больше самого круга, то карты сходятся всегда.
        В нашей системе счетчик тикает через SysTick_Handler, который имеет самый >высокий< приоритет.
        Кстати, если нужен действительно атомарный счетчик, на котором можно делать и меньшие задержки, чем 1ms, можно заюзать DWT (который, правда, как правило отсутствует в -M0 кортексах, но судя по форумам, толпа ниже -M4 уже и не опускается): счетчик DWT — 32-х битный, тикающий с тактовой частотой ядра.

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

              Проблемы переполнения счетчика нет, пока вы определяете задержки/дистанции длиной не более самого цикла счетчика. Для системы измерения на счетчике нельзя использовать дистанции более его размерности, то есть более 255 для байтового счетчика или 4 миллиарда с лишком для 32-бит. Об этом и написал с примером двоичного вычисления DuMOHsmol.

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

              Другое дело — и, видимо, в этом суть Вашего вопроса ко мне, — как быть при построении диспетчера из темы топика, что наш DI HALT замутил. Здесь Ваше чутьё не подводит: в общем случае (это важное замечание!) максимальная дистанция/задержка/удаление точки срабатывания должно быть не далее ПОЛОВИНЫ максимальной величины счетчика (то есть, половина окружности, если визуально как часы представить). Остальное сделает арифметика вычитания беззнаковых величин со знаковым результатом. Мой кусок кода «решателя» на основе DWT выглядит как

              // —————————————————————————
              //
              // Provides comparision of the given time point with the current
              // DWT value. Returns TRUE if the ‘tp’ is NOT reached yet.
              //
              static __inline uint8_t DWT_Compare(uint32_t tp)
              {
              return (int)(DWT_Get() — tp) < 0;
              }

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

              Почему?
              Вообще, если объединить оба применения, то допустимая дистанция зависит от функции сравнения: если Ваш диспетчер будет сравнивать на полное РАВЕНСТВО, то можно работать и на полной дистанции, если же, как в коде выше, сравнение на БОЛЬШЕ-РАВНО (или строго МЕНЬШЕ), то — только на половине дистанции. С DWT по-другому и не получится, т.к. этот счетчик крутится себе синхронно с тактом ядра, никаких прерываний, и если в цикле мерять задержки, то полного равенства практически никогда не будет.

            2. Должен подкорректировать предыдущий пост, закралась ошибка копирования:
              Вместо: «И я слежу за тем, чтобы tp была фактически 31-разрядной величиной, то есть, всегда положительной int32_t.
              » следует читать: «при вычислении очередной tp я слежу за тем, чтобы дистанция была фактически 31-разрядной величиной, то есть, всегда положительной int32_t.»

          1. Разрядность счетчика должна быть такой, чтобы переполнения не было в принципе. Либо, если счетчик почти заполнен, но арифметикой в случае переполнения диапазон в пределах задуманного. Если время превысило все мыслимые пределы, значит это аварийная ситуация. При создании программных таймеров нужно составить ТЗ. Какие задачи ставите. Дискретность системного тика. Диапазоны. Мои требования были такие: дискретность 1 мс, 10 мс. Однократный, перезапускаемый, отложенное выполнение, немедленное исполнение. Проблему с переполнением я решил просто. Я не выполняю арифметических действий со счетчиком. Я контролирую состояние счетчика. Если значение счетчика изменилось, значит очередной дисрет-отсчет временного интервала-тика произошел.

            struct soft_timer *ptr = ptr_timer;

            if (ptr -> status)
            {
            if (ptr -> sys_tick_prev != sys_tick)

            Это решает очередную проблему. Установку таймеров на паузу. Как вы видите, при моем подходе эта проблема решена автоматически.

        1. Да, справедливо. Без знания архитектуры и особенностей компилятора такие заявления кидать не стоит. Поправлю при случае.

  7. К диспетчерам пока только приглядываюсь, поэтому постоянно использую службу таймеров из цикла статей “Switch-программирование”. Сравнил с таймером из статьи (студия 4.19). Вот некоторые цифры (МТ – наш таймер, GT – таймер из “switch”):

    GT таймер: поскольку все таймеры прощелкиваются в прерывании системного тика (обычно 1 мс), то количество таймеров прямо влияет на длительность прерывания. При этом если таймер не запущен, то он не обрабатывается (проверяется лишь его статус – RUNNING или нет). Т.е. в самом худшем случае будут обрабатываться все используемые таймеры. Минимальное время в прерывании (ни один таймер не обрабатывается) ~80 тактов. Начиная с трех остановленных таймеров ~122 такта, на каждый последующий таймер уходит по 12 тактов. Если таймер запущен, то на его инкремент уходит еще плюс 22 такта. Да, таймеры всего лишь uint16_t, т.е максимальная задержка чуть больше минуты.
    Итого на 10 таймеров время в прерывании займет примерно 450 тактов.
    МТ-таймер. Если разрядность 16 или 32, то время в прерывании около сотни тактов. Если х64, то в полтора раза больше. Есть одно неудобство с таймерами младших разрядностей – не получится использовать функции постановки на паузу и продолжения счета. Поэтому решил остановиться на варианте с х64. При частоте 16 МГц время в прерывании около 10 микросекунд (при количестве таймеров не больше 255).

    Мой вариант функций для этого таймера:
    https://community.atmel.com/projects/timer-fsm

    Вариант функции мигалки:
    void Blink() {
    volatile static u08 state=0;
    switch (state) {
    case 0:
    StartGTimer(timer_led_white,500);
    if (ExpGTimer(timer_led_white)) {
    LedWhiteOn();
    state=1;
    }
    break;
    case 1:
    StartGTimer(timer_led_white,500);
    if (ExpGTimer(timer_led_white)) {
    LedWhiteOff();
    state=0;
    }
    break;
    }
    }

    1. boris911, я только начинаю знакомиться с программированием МК, но мне явилось предложение сначала делать проверку на if (ExpGTimer(timer_led_white)), а уж по результатам проверки входить или не входить в Blink(). Имхо, проверить флаг гораздо проще, чем войти в функцию, не так ли? Буду рад вашему комментарию.

      1. Можно, если проверяется вся функция. А если таймер внутри алгоритма самой функции, к примеру, надо сделать паузу в алгоритме switch, то здесь проверка только внутри.
        Ну и так просто нагляднее и удобнее.

    1. И второй вопрос: на чём основан выбор для флагов u64, а не массива булек? Подозреваю, что u64 и экономичней по памяти, и быстрее в обработке, чем массив. Но мне как нубу хочется, чтобы вы сказали это явно.

      1. Массив булек сильно сложно обрабатывать. Это же чтение-модификация запись+логика. А флаги должны быть быстрыми, так что проще давать им разрядность ядра или байта (если архитектура умеет байты поштучно быстро лопатить) и не парить мозги. Ну и потом флаги легко превращаются в семафоры-счетчики или что-то подобное если надо вдруг. С булькой что ты сделаешь?

      2. Не люблю бульки ))). Проще поставить тип функции u08, чем переключаться на false и true, тем более что в дефайнах у меня обычно продублировано: ON 1, OFF 0, YES 1, NO 0 и так далее.

  8. boris911, есть у меня сомнения: сразу после инициации каков будет ответ на запрос if (ExpGTimer(timer_sys_led))? TIMER_STOPPED? То есть таймер истёк? Я сделал так (см. ниже) и теперь туплю, произойдёт ли ПЕРВЫЙ запуск таймера.

    /******************************************
    Автомат SysLED
    *******************************************/

    void Blink_SysLED() {

    volatile static u08 state = 0;

    switch(state) {

    case 0:
    SysLED_On(); // ВКЛючаем SysLED
    StartGTimer(timer_sys_led,10); // запускаем таймер на 100 мс
    state=1; // меняем состояние свича на 1
    return; // выходим из функции

    case 1:
    SysLED_Off(); // ВЫКЛючаем SysLED
    StartGTimer(timer_sys_led,40); // запускаем таймер на 400 мс
    state=0; // меняем состояние свича на 0
    return; // выходим из функции
    }
    }

    // ГЛАВНЫЙ ЦИКЛ
    for(;;)
    {

    if (ExpGTimer(timer_sys_led)) { // если таймер SysLED истёк
    Blink_SysLED(); // мигаем SysLED
    }

    }
    }

    Систик 10 мсек

    1. VladyMile
      Не произойдёт. В любом случае надо чтобы таймер обязательно стартовал только один раз перед функцией. Понадобятся дополнительные флаги для этого. В чем выигрыш? Не нужно усложнять.

      1. boris911, я понял, благодарю.
        Вы не серчайте, но я пойду своим путём: путём максимального сокращения в общем лупе входов в (любые!) функции.
        Возможно, я делаю не оптимально с точки зрения читабельности, но у меня же просто тренировка/обучение без перспектив промышленного применения знаний.

        Тогда я запущу этот таймер на 1…2 систика на этапе сразу после конфигурации, ещё до лупа. Чтобы он уже «есть» к моменту проверки в лупе. И флагов не надо.

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

  9. Добрался до логического анализатора.
    С флагами для каждого таймера получается очень нехорошо. Чем выше ID таймера – тем дольше он обрабатывается:
    ID==0 > t==3,7 us;
    ID==1 > t==4.3 us;
    ID==8 > t==6.1 us;
    ID==16 > t==16 us;
    ID==32 > t==21 us;
    ID==50 > t==26.5 us;
    ID==63 > t==31 us.
    Особенно это плохо, учитывая, что там стоит инструкция CLI. В прерывании обработка счетчика с обнулением флага однократной проверки занимает 5.5 микросекунд.
    При этом проверка без флага в основном цикле занимает чуть меньше 6 микросекунд . Если счетчик дотикал, то время проверки будет на 0.5 мкс больше – 6.3 мкс (на изменение статуса таймера). Заодно уменьшилось на 1 микросекунду время в прерывании до 4.5 мкс.
    Таким образом, оставил таймеры без однократной проверки, и так всё достаточно быстро.
    Теперь рассмотрим варианты меньших разрядностей. При разрядности u16 максимальная задержка может составлять чуть больше 32 секунд, при u32 – чуть больше 24 суток. Недостатки — нет возможности поставить таймер на паузу и снять с неё (хотя не так часто это и нужно). Зато выигрыш по быстродействию приличный.
    Таймер u16: время в прерывании 0.8 мкс, проверка таймера 1.9 мкс, если таймер готов, то 2.5 мкс.
    Таймер u32: время в прерывании 1.5 мкс, проверка таймера 2.7 мкс, если таймер готов, то 3.7 мкс.
    Для сравнения:
    Таймер u64: время в прерывании 4.5 мкс, проверка таймера 6 мкс, если таймер готов, то 6.5 мкс.
    Количество таймеров до 255. Протестировано на мега328р, 16 МГц.
    https://yadi.sk/d/_00F38x53Xvw3V

  10. Конструкцию из switch можно заменить кучей функций, которые будут менять указатель на функцию. Ну вот как-то так:

    void (*next_action)(void) = step0; // указатель на какую-то там функцию это переменная состояния конечного автомата

    void step0(void) {
    // do step 0
    next_action = step1; // переходим в другое состояние конечного автомата
    }

    void step1(void) {
    // do step 1
    next_action = step2;
    }

    void step2(void) {
    // do step 2
    next_action = step1;
    }

    1. А в чем профит? Кроме того, что это по сути GOTO, превращающее код в непонятную хуйню. Плюс проблемы с видимостью переменных, а точнее их надо делать глобальными тогда все.

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

        1. ДА ну ты брось. Это дичь какая то получается. Во-первых, выгоды никакой практически. Свитч внутри тот же индексный переход. Дизассемблируй и сам увидишь все. Во-вторых, автомат существует не сам по себе, а управляет чем то. А это переменные. Что с ними делать? Когда все в одной фукнции мы их обьявляем локальными, они могут быть даже не статичными, в пределах одного автомата это пофигу если состояние меняется без выхода. А тут придется делать глобальными, с областью видимости как минимум внутри модуля. А это уже косяк, переменные не должны быть видны дальше чем это реально нужно. В третьих, наглядности там получается ничуть не больше. Мы вместо одного автомата имеем 100500 функций также валяющихся плоским списком.

          1. >Свитч внутри тот же индексный переход. Дизассемблируй и сам увидишь все.

            Если в switch используются метки по порядку (типа case 0, case 1, case2…), из switch сгенерируется массив из адресов меток и будет там по индексу (аргументу switch) прыгать на метку из этого самого массива. https://godbolt.org/g/4wBWVq .

            Если же метки нельзя компактно уложить в массив, чтобы по индексу взять, получим кучку сравнений и обычные переходы https://godbolt.org/g/5iQdMN

            Некоторые компиляторы, например тот же GCC под AVR даже «плотноупакованный» switch не скомпилирует в массив из меток и переход по индексу, а тупо сделает кучу сравнений https://godbolt.org/g/YNRER4 так что полагаться на такую оптимизацию можно не всегда

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

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

            Объявления локальных переменных можно в каждой подобной функции просто продублировать. Глобальными придется делать только те, которые были бы статичными если делать через switch.
            Ну и если метнуться в сторону C++, можно использовать неймспейсы для ограничения области видимости.

  11. DI HALT, boris911 и всевсевсе!

    Низкий поклон вам за такой приятный и доступный моему пониманию код!

    Вчера всего с ~15 раза (исправлял косяки сопряжения кодов) прошла успешная компиляция, а сегодня с первого раза запустил модель в Протеусе.

    Восторгу нет предела! :)

    Ребят, большой любви, крепкого здоровья и материального изобилия вам! :)

    P.S. Знали бы вы, сколько я трахался с временнЫми интервалами в своих поделках — вы бы поняли мою искреннюю и столь непосредственную признательность за все ваши объяснения и код. :)

      1. DI, мы уже обсуждали диспетчеры. Диспетчеры дают кажущуюся легкость программирования. Но в них есть скрытая существенная засада. Простейший пример: автомат световых эффектов. Пусть устройство проигрывает эффект. В очереди задач болтаются задачи на выполнение. А теперь мы нажимаем на смену режима. Запускается новый режим и в очередь закидываются новые задачи. Но старые то остались болтаться в очереди. И они спустя заданное время и выстрелят. И речь идет о световых эффектах. Кроме временного эстетического неудобства последствий нет. А если новичок загорится желанием сделать блок управления станком? И кому-то, минимум, что-то отдавит, оттяпает. Максимум — смерть оператора. Кто виноват?

        Вывод, отказ от диспетчеров, переход на автоматное программирование.

        Все проблемы начинающих решают конечные автоматы. Я уже много лет сижу на конечных автоматах. И до сих пор так и не использовал в своих проектах ни диспетчеры, ни RTOS.

        Дальнейшее развитие этого вопроса отдельная тема на форуме. Формат комментариев не позволит полноценно обсудить эту немаленькую темы.

        Чуть позже я сяду и допишу свою статью. Что-то уже начал выкладывать в сообществе.
        http://we.easyelectronics.ru/demiurg1978/realizaciya-programmnogo-taymera-avr.html

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

          1. 1 — Откуда нам знать, какие задачи удалять из очереди?
            2 — Если у тебя конечные автоматы в диспетчере задач, тем более теряется смысл использования диспетчера. Его задача тупейшая — проворот очереди. То есть, программист создал или использует какой-то лисапед и охреневает, как у него все лихо крутится. Практического смысла 0.
            Тогда уж сразу так, что я и делаю:
            //========================================================================
            __C_task main (void)
            {
            wdt_enable (WDTO_15_MS);

            sleep_mode_init ();

            init_soft_timers ();
            Init_Events ();

            io_init ();

            __enable_interrupt ();

            #ifdef __LOGO__
            logo ();
            #endif

            while (1)
            {
            __watchdog_reset ();

            proc_device ();

            kbd_drv ();
            info_service ();
            proc_outputs ();

            Process_Events ();
            }
            }
            //========================================================================

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

              2. Не теряется. Время обработки диспетчера при пустой очереди (а обычно она пустая) статичное. Плюс всегда можно примерно предположить сколько там еще в очереди будет задач, обычно это не более 3-5. А вот с ростом числа автоматов растет и время обработки их. Вон у меня сейчас один проект на автоматах, не мной начатый, так их там около 80 штук друг за дружкой гроздью навешано, штук 20 периферии и еще логики всякой очень дофига и это уже начинает сильно напрягать. Приходится тасовать их в порядке важности. Чтобы что-нибудь не проебать. Я бы делал изначально не так, а завернул все в ОС какую и было бы в итоге сильно сильно все проще.

Добавить комментарий

Ваш e-mail не будет опубликован.

Этот сайт использует Akismet для борьбы со спамом. Узнайте как обрабатываются ваши данные комментариев.