Вот две строки, я гений, прочь сомненья
Даешь восторги, лавры и цветы…


Данный пост посвящен довольно таки старой задаче о считывании таймера, с которой лично я ознакомился в книге Джека Гансли (The Art of Designing Embedded Systems (Second Edition), (2008) by Jack Ganssle) в которой рассматривается борьба с гонками в асинхронных устройства. Сформулирована проблема и показаны 4 способа ее решения (2 неправильных и 2 правильных), рассмотрены их недостатки, в общем, добротная работа в стиле Джека (я к нему отношусь очень хорошо). К сожалению, на мой взгляд, даже работающие решения не имели должной степени элегантности, но более красивое долго не приходило в голову, а вчера неожиданно осенило. Так что я считаю себя вправе изложить данную проблему в ее историческом контексте, поскольку придумал очень элегантное решение (сам себя не похвалишь, весь день ходишь как оплеванный).

Формулируем задачу: у нас имеется аппаратный счетчик, который засинхронизирован от некоторой сиcтемной частоты и может быть использован для измерения времени. Однако, в силу аппаратных ограничения, его вместимости недостаточно для формирования продолжительных промежутков времени, поэтому создается программное расширение разрядности счетчика. Реализуется оно следующим образом — при переполнении счетчика происходит прерывание и подпрограмма обработки прерывания модифицирует некую глобальную переменную, которая совокупно с самим счетчиком образует расширенный счетчик времени, что-нибудь вроде
unsigned int High; 
interrupt TimerOv(void) {
   High++;
}
Тогда получение текущего времени мы может написать следующим образом
 unsigned long int GetTimer(void) { 
   return (High<<sizeof(int))+ ReadReg(TimerCounter);
}
Все просто, понятно, и неправильно, как и следовало ожидать. Здесь проблема лежит на поверхности и видна каждому, кто писал подобные программы — в процессе чтения двух половин расширенного таймера может произойти переполнение младшей части, поскольку она асинхронна, и тогда две части не будут валидны относительно друг друга. При этом мы можем получить в качестве значения расширенного таймера как прошедший момент времени, так и будущий, и оба эти случая хуже.

Есть очевидное решение проблемы — на время считывания остановить аппаратную часть таймера, но такое решение неприемлемо из-за влияния на подсчитываемое время.

Попробуем другое очевидное решение — запретить на время считывания прерывания и мы получаем
 unsigned long int GetTimer(void) { 
   DisableInt();
   unsigned long Tmp=(High<<sizeof(int))+ ReadReg(TimerCounter);
   EnableInt();
   return Tmp;
}
Конечно же, когда я пишу про запрет прерывания, подразумевается сохранения текущего значения с восстановлением его в конце, но эти тонкости опускаем, они не столь важны сейчас. Что в этом решении не хорошо: 1) мы запрещаем прерывания, а это не радует, и 2) это решение работать не будет. Да, именно так, хотя метод апробированный, но не для этого случая. Дело в том, что запрещение прерывания на запрещает работу таймера (он аппаратный), поэтому если в период запроса прерывания произойдет переполнение счетчика, мы получим младшую часть равной нулю, а старшая не модифицируется, то есть данные не валидные.

Возможная модификация данного решения приводит к следующему коду
 unsigned long int GetTimer(void) { 
   DisableInt();
   unsigned long TmpH=High;
   unsigned long TmpL=ReadReg(TimerCounter);
   if (TimerOvBit()) {
      TmpH++;
      TmpL=ReadReg(TimerCounter);
    };
   EnableInt();
   TmpH=(TmpH<<sizeof(int))+ TmpL;
   return Tmp;
}
Вот это первое правильное решение. У этого решения есть недостатки: 1) мы все-таки запрещаем прерывания, 2) нам потребуется дополнительный аппаратный бит и нужно о нем позаботиться в обслуживании прерывания, 3) мы сделали определенные предположения о модификации старшей части. Существуют ли другие решения?

Да подобное решение существует и найдено Джеком и мной (честное слово, самостоятельно). Код этого решения следующий
 unsigned long int GetTimer(void) { 
   unsigned long TmpH,TmpL;
   do {
       TmpH=High;
       TmpL= ReadReg(TimerCounter);
   while (TmpH!=High);
   return (TmpH<<sizeof(int))+TmpL;
}
Это второе верное решение, обратим внимание на то, что считывание счетчика обрамлено обращениями к старшей части, иначе неправильно. Вообще то данный подход сильно напоминает неблокирующие алгоритмы при конкурентном обращении к ресурсам, которые мне чем-то нравятся, есть в них некоторая элегантность. У данного способа есть много преимуществ, но один недостаток — если наша система сильно нагружена, то мы можем надолго зависнуть в цикле, как пишет Джек, время исполнения становится непредсказуемым.

И вот тут придумана небольшая модификация данного алгоритма, а именно
 unsigned long int GetTimer(void) { 
   unsigned long TmpH,TmpL;
   TmpH=High;
   TmpL= ReadReg(TimerCounter);
   if (TmpH!=(TmpH=High)) TmpL= ReadReg(TimerCounter);
   return (TmpH<<sizeof(int))+TmpL;
}
Это третье верное решение и мне оно, как автору, нравится больше всего. Мы не запрещаем прерывания, не требуем ничего от аппаратуры, не делаем никаких предположений, получаем всегда верный результат, то есть 1) никогда не получим значения расширенного таймера, предшествующее моменту входа в процедуру и 2) никогда не получим значение, следующее за моментом выхода из процедуры, имеем совершенно предсказуемое поведение и все это практически бесплатно. Конечно, если у нас высоконагруженная система, то мы можем на выходе получить время, существенно меньшее времени выхода из процедуры, но это свойственно в той же мере и двум другим верным способам. Если же нам действительно нужно текущее значение времени с минимальным отклонением, то мы должны всю обработку проводить с запрещенными прерываниями, и это явно не наш случай.

Как мне совершенно верно указали в комментах, компилятор может неправильно скомпилить ключевую строку алгоритма, поэтому вот исправленная версия
 unsigned long int GetTimer(void) { 
   unsigned long TmpHFirst,TmpH,TmpL;
   TmpHFirst=High;
   TmpL= ReadReg(TimerCounter);
   if (TmpHFirst!=(TmpH=High)) TmpL= ReadReg(TimerCounter);
   return (TmpH<<sizeof(int))+TmpL;
}

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

И в заключение еще один интересный момент. Нас не интересует время, как таковое, обычно мы используем текущее время для того, чтобы выставить относительно него некоторый момент в будущем, и по достижении него что-либо сделать. Так вот, по Вашему мнению, являются ли следующие две строки эквивалентными?
 unsigned long TmpWait;
   TmpWait=GetTimer()+SomeDelay;
   while (TmpWait > GetTimer()) ; /* первая строка */ 
   while ((TmpWait - GetTimer()) > 0) ; /*вторая строка */
}
Эти строки неэквивалентны при определенных условиях и интересно рассмотреть, почему. Заинтересовавшихся отправлю к Linux руководствам, ищите вблизи термина jiffies.

Комментарии (39)


  1. mark_ablov
    21.12.2015 17:14

    > while ((TmpWait — GetTimer()) > 0)
    Даже неинтересно. false только когда GetTimer() == TmpWait, если дискретности не хватает на измерение, то это будет нечасто.

    бтв, ваше решение ломается на высокочастотном таймере с узким регистром счетчика. Понятно, что это очень теоретический МК, и на практике это просто никогда не встретится, но в рамках теоретизирования — почему нет?


    1. GarryC
      21.12.2015 22:26

      Не совсем верно, посмотрите случай когда начальное значение близко к переполнению, там интересно.

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


  1. sguwenka
    21.12.2015 17:21

    1. volatile в определении переменных не указываете — тоже очевидно, опустим? ;)
    2. А если в нашей «высоконагруженной системе» бомбанёт ряд длительных прерываний вот в этом месте?

    if (TmpH!=High) TmpL= ReadReg(TimerCounter);
    … а таймер будет достаточно скоростным — имеем реальный риск всё же неверно считать данные.

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


    1. GarryC
      21.12.2015 18:14

      В том то и дело, что все равно мы считаем правильные данные. Да, они будут отставать от текущего значения, но они НЕ будут невалидными, в этом вся фишка.


      1. MacIn
        21.12.2015 23:01
        +3

        Нет. Решение Джека Гансли — bulletproof, ваше — выдает правильное значение с некоторой веротяностью, большей, чем для решения «в лоб» в самом начале статьи.

        if (TmpH!=High) TmpL= ReadReg(TimerCounter);
        

        Допустим, вы считываете новое значение High, т.к. оно отличается от предыдущего, мы принимаем решение считать по новой TmpL. Теперь допустим, мы классически вываливаемся после принятия решения, но до считывания TmpL. В это время High опять обновляется. Теперь мы возвращаемся, считываем TmpL и имеем некорректную пару — старый TmpH и новый TmpL.


        1. ProLimit
          22.12.2015 01:41

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


          1. MacIn
            22.12.2015 01:55

            Разумеется, описанный алгоритм покрывает все более-менее реальные случаи. Тем не менее, алгоритм с циклом ничем не хуже и покрывает все.


            1. GarryC
              22.12.2015 09:18

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


              1. izyk
                22.12.2015 10:11

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

                unsigned long int GetTimer(void) {
                   unsigned long TmpHFirst,TmpH,TmpL;
                   TmpHFirst=High;
                   TmpL= ReadReg(TimerCounter);
                   if (TmpHFirst!=(TmpH=High)) {
                      //Вот тут произошло прерывание, изменилось значение High, а вы уже считали старое.
                      TmpL= ReadReg(TimerCounter);  // Считываете значение соответствующее новому значению High.
                   }
                   return (TmpH<<sizeof(int))+TmpL;
                }


              1. MacIn
                22.12.2015 15:56
                +1

                Алгоритм с циклом может выполняться значительное время, что Джек и указал.

                Не-а. То-есть, конечно, может, но какова вероятность того, что у нас фазы будут так совпадать? Сдается мне, гораздо ниже, чем длительное прерывание и т.д. (см. критику выше). Или ниже, где izyk еще раз описал то же самое.


        1. GarryC
          22.12.2015 09:53

          Эта пара ( старый TmpH и новый TmpL) является абсолютно корректной. Она не отражает текущее состояние счетчика, но гарантировано возвращает правильное время, то есть не прошлое и не будущее, а одно из настоящих, а это — главное.


          1. MacIn
            22.12.2015 15:58

            Нет. У вас может high измениться несколько раз между проверкой условия и считыванием low (теоритически, если таймер очень скорострельный). В итоге у вас high от одной итерации, low — от совсем другой.


            1. MacIn
              22.12.2015 16:06

              Представьте:
              1) вы считываете high, он равен 10
              1.5) считываете low, он равен 50
              2) считываете high повторно, он равен 11
              3) принимаете решение перечитать low
              4) вываливаетесь
              5) high изменяется на 12
              6) low изменяется на 38
              7) считываете low
              получаем: high = 11, low = 38, хотя на самом деле пара — 12/38.
              Практически — я согласен — вы правы, эту «случайную» пару можно считать «настоящим» временем. И в практической реализации это действительно нормально. Просто алгоритм с циклом абсолютно корректен. Ваш — компромиссный.


              1. GarryC
                23.12.2015 08:50

                Рассмотрим любой из первых двух алгоритмов. Вот они отработали, получили текущее время, разрешили прерывание (тот, кто запрещал), произошло прерывания, счетчик убежал вперед и мы получаем то же самое, 11/38, хотя на самом деле пара 12/25 (к примеру). В высоко-нагруженной системе вернуть истинное время просто невозможно, и здесь ничего не поделать. Главное, чтобы полученное значение не было артефактом.
                В UNIX системах так и пишут, что задание задержки гарантирует только то, что она не истечет ДО требуемого момента, а вот на сколько может его превзойти — не определено.
                НО если мы правильно используем полученные данные в стиле следующего псевдокода, то все хорошо
                1) выставляем (к примеру) 1 на выходе,
                2) считываем текущее время,
                3) прибавляем к нему требуемую задержку,
                … делаем что-то
                4) читаем текущее время и сравниваем с границей,
                5) если граница перейдена, снимаем 1 с выхода,
                то все три верных решения нам гарантируют, что 1 простоит на выходе минимум задержку времени.
                Обратите внимание на последовательность 1 и 2 — именно так, иначе неверно.


                1. izyk
                  23.12.2015 11:01

                  1) выставляем (к примеру) 1 на выходе,
                  2) считываем текущее время (реально сейчас, на момент возврата из функции, 12.02, а мы считываем 11.02), но единица установлена уже не меньше чем 1.00,
                  3) прибавляем к нему требуемую задержку (к примеру 0.30, получаем 11.32),
                  … делаем что-то
                  4) читаем текущее время и сравниваем с границей (реально сейчас 12.14, и мы считываем 12.14),
                  5) если граница перейдена, снимаем 1 с выхода (12.14 > 11.32, снимаем 1 с задержкой как минимум 0.12+1.00=1.12 вместо 0.30).

                  Но позвольте, какое отношение это имеет к ОСРВ. Если вас устраивает такая точность, тогда весь код должен быть(повторюсь):

                  unsigned long int GetTimer(void) { 
                     return (High<<sizeof(int))+0;
                  }


                  1. GarryC
                    23.12.2015 11:30

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


                    1. izyk
                      23.12.2015 11:46

                      del


                      1. GarryC
                        23.12.2015 12:03

                        ? Я Вас убедил?


                        1. izyk
                          23.12.2015 12:49

                          Кажется, я начинаю понимать, о чем вы.
                          Но какой смысл в такой процедуре, зачем вам вообще значение «TimerCounter», если ваша система не гарантирует точность меньше max(TimerCounter), достаточно значения High. А если вы гарантируете такую точность, то входными данными для данной процедуры должно быть — во время ее выполнение возможно только одно прерывание таймера.
                          И это надо проверять на случай сбоя.

                          Если сравнивать вашу процедуру с процедурой из книжки, в случае одного прерывания во время выполнения они эквивалентны. А в случае нескольких прерываний, более эффективна, т.к. возвращает первое подходящее значение.
                          Но во втором случае это означает, что ваша система не гарантирует точность меньше max(TimerCounter). И ее (процедуру) можно упростить.

                          unsigned long int GetTimer(void) { 
                             return (High<<sizeof(int))+0;
                          }


                          1. GarryC
                            23.12.2015 15:02

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


  1. ToSHiC
    21.12.2015 19:15
    +4

    Кажется, в вашей реализации баг.

    Допустим, High = 10, счётчик переполняется при 1000.

    TmpH=High; // = 10
    TmpL= ReadReg(TimerCounter); //  = 999
    // тут случилось прерывание и High заинкрементилось
    if (TmpH!=High) TmpL= ReadReg(TimerCounter); // TmpL = 001
    return (TmpH<<sizeof(int))+TmpL; // Вернули 10.001
    


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


    1. GarryC
      21.12.2015 22:23

      Точно, спасибо большое, немного неправильно записал, конечно верхняя часть перечитывается, сейчас подправлю в тексте, но перечитывается 1 раз, без цикла. Еще раз спасибо, подправил.


      1. ToSHiC
        22.12.2015 00:13

        Тогда не сработает, возникнет прерывание, которое будет длиться дольше, чем 1 цикл переполнения. На подобные штуки нужно смотреть с другой стороны (тут разбираю вариант с while циклом):
        1. Идеальный случай, никакого переполнения. Избыточность — 1 команда чтения, 1 команда сравнения, 1 команда условного перехода, которая сфейлится. Вероятность такого исхода зависит от конкретной программы, других возможных прерываний, скорости переполнения счётчика.
        2. Неидеальный случай, который будет случаться иногда — дополнительно 1 чтение из памяти и одно чтение из регистра счётчика к варианту 1.
        3. Плохой случай, если много прерываний, или они длинные. В этом случае придётся перечитывать много раз. Может случиться, если этот кусок кода имеет совсем уж низкий приоритет. Но работать будет.

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


  1. imwode
    21.12.2015 22:08
    +2

    Меня смущает сочетание «аппаратный счетчик» и sizeof(int)
    Во всяких микроконтроллерах, как правило, соседствуют таймеры разной разрядности, а sizeof(int) зависит от компилятора. Можно неслабо так нарваться…


    1. GarryC
      21.12.2015 22:20

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


      1. imwode
        21.12.2015 23:15
        +1

        Не, эта какая-то негодная и опасная перестраховка непонятно от чего непонятно зачем :-)


      1. encyclopedist
        22.12.2015 02:30
        +1

        sizeof(int) выдаёт размер инта в байтах (точнее, в символах, sizeof(char) == 1 по определению), а не в битах. Поэтому для сдвига его надо умножить на CHAR_BITS.


  1. Amomum
    21.12.2015 22:28

    TmpWait=GetTimer()+SomeDelay;
    while (TmpWait > GetTimer()); /* первая строка */
    while ((TmpWait — GetTimer()) > 0); /*вторая строка */

    Насколько я понимаю, первая строка неправильная, т.к. не учитывает переполнение, которое могло случится при вычислении TmpWait.


    1. Ocelot
      21.12.2015 22:39
      +1

      Они обе не учитывают переполнение, а во второй еще и вычитаются две беззнаковые переменные, так что результат тоже будет беззнаковый. И выражение (0-1) там даст 0xFFFFFFFF, а не (-1), как ожидалось.


      1. MacIn
        21.12.2015 23:04

        В Windows аналогичная проблема для GetTickCount.


      1. Amomum
        21.12.2015 23:14
        +1

        Вероятно, вы правы. Я обычно пишу

        prevTime = GetTime();
        while( GetTime() - prevTime < delay );
        

        Сходу не могу сообразить, но вроде бы вторая строка этому не эквивалентна; не помню досконально правила неявного приведения типа.


        1. Ocelot
          21.12.2015 23:40

          Ноуп! Тут тоже ошибка. Допустим, prevTime=900, delay=200, а GetTime() переполняется на 1000. Тогда выход из цикла произойдет сразу после переполнения, через 100 тиков вместо ожидаемых 200.

          UPD: нет, туплю, все вроде ок.


  1. izyk
    22.12.2015 02:05

    Если в вашей системе возможно что-то отличное от следующего:

    unsigned long int GetTimer(void) { 
       unsigned long TmpH,TmpL;
       TmpH=High;
       TmpL= ReadReg(TimerCounter);
       if ( TmpH != High || TmpH++ != High ) exit(error);
       return (TmpH<<sizeof(int))+TmpL;
    }

    то значение «TimerCounter» для вас не должно иметь смысла(ИМХО). И вы его можете просто не учитывать (выполнять округление).


    1. izyk
      22.12.2015 02:52

      Накосячил. Более правильно, наверное, так.

      unsigned long int GetTimer(void) { 
         unsigned long TmpH,TmpL;
         TmpH=High;
         TmpL=ReadReg(TimerCounter);
         if ( TmpH+1 == High ) TmpL=ReadReg(TimerCounter);
         if ( TmpH != High || TmpH++ != High ) exit(error);
         return (TmpH<<sizeof(int))+TmpL;
      }

      Многовато if, но вроде должно работать. Заодно своеобразный watchdog получился.
      А если значение High во время вызова функции может увеличится более чем один раз, и это нормально, тогда функция должна быть такой:
      unsigned long int GetTimer(void) { 
         return (High<<sizeof(int))+0;
         // Или просто
         return High;
      }

      Но это мое мнение. И вполне могу предположить что оно не верное.


      1. izyk
        22.12.2015 14:35
        +1

        Не, мой вариант не правильный.
        Кстати, первый «правильный», тоже не правильный.

        unsigned long int GetTimer(void) { 
           DisableInt();
           unsigned long TmpH=High;
           if (TimerOvBit()) TmpH++; //Это условие не сработало здесь.
           //А Здесь происходит переполнение и следующей строкой "ReadReg", вместо значения близкого к максимуму, считает близкое к нулю.
           TmpH=(TmpH<<sizeof(int))+ ReadReg(TimerCounter);
           EnableInt();
           return Tmp;
        }

        Единственный правильный вариант:
        unsigned long int GetTimer(void) { 
           unsigned long TmpH,TmpL;
           do {
               TmpH=High;
               TmpL= ReadReg(TimerCounter);
           while (TmpH!=High);
           return (TmpH<<sizeof(int))+TmpL;
        }

        А если нужно предотвратить зависание, нужно ввести счетчик циклов.


        1. MacIn
          22.12.2015 16:08

          А если нужно предотвратить зависание, нужно ввести счетчик циклов.

          Плюс за здравую мысль. Если цикл занимает уже слишком долго, мы выдаем компромиссный вариант.


  1. encyclopedist
    22.12.2015 02:33

        if (TmpH!=(TmpH=High)) TmpL= ReadReg(TimerCounter);
    
    — это неопределенное поведение. В условии одна и та же переменная читается и модифицируется.


    1. GarryC
      22.12.2015 09:20

      Да, Вы правы, у меня работает, но это как повезет. Дополнил пост непротиворечивой функцией.


  1. mark_ablov
    21.12.2015 17:14

    > while ((TmpWait — GetTimer()) > 0)
    Даже неинтересно. false только когда GetTimer() == TmpWait, если дискретности не хватает на измерение, то это будет нечасто.

    бтв, ваше решение ломается на высокочастотном таймере с узким регистром счетчика. Понятно, что это очень теоретический МК, и на практике это просто никогда не встретится, но в рамках теоретизирования — почему нет?


    1. GarryC
      21.12.2015 22:26

      Не совсем верно, посмотрите случай когда начальное значение близко к переполнению, там интересно.

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


  1. sguwenka
    21.12.2015 17:21

    1. volatile в определении переменных не указываете — тоже очевидно, опустим? ;)
    2. А если в нашей «высоконагруженной системе» бомбанёт ряд длительных прерываний вот в этом месте?

    if (TmpH!=High) TmpL= ReadReg(TimerCounter);
    … а таймер будет достаточно скоростным — имеем реальный риск всё же неверно считать данные.

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


    1. GarryC
      21.12.2015 18:14

      В том то и дело, что все равно мы считаем правильные данные. Да, они будут отставать от текущего значения, но они НЕ будут невалидными, в этом вся фишка.


      1. MacIn
        21.12.2015 23:01
        +3

        Нет. Решение Джека Гансли — bulletproof, ваше — выдает правильное значение с некоторой веротяностью, большей, чем для решения «в лоб» в самом начале статьи.

        if (TmpH!=High) TmpL= ReadReg(TimerCounter);
        

        Допустим, вы считываете новое значение High, т.к. оно отличается от предыдущего, мы принимаем решение считать по новой TmpL. Теперь допустим, мы классически вываливаемся после принятия решения, но до считывания TmpL. В это время High опять обновляется. Теперь мы возвращаемся, считываем TmpL и имеем некорректную пару — старый TmpH и новый TmpL.


        1. ProLimit
          22.12.2015 01:41

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


          1. MacIn
            22.12.2015 01:55

            Разумеется, описанный алгоритм покрывает все более-менее реальные случаи. Тем не менее, алгоритм с циклом ничем не хуже и покрывает все.


            1. GarryC
              22.12.2015 09:18

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


              1. izyk
                22.12.2015 10:11

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

                unsigned long int GetTimer(void) {
                   unsigned long TmpHFirst,TmpH,TmpL;
                   TmpHFirst=High;
                   TmpL= ReadReg(TimerCounter);
                   if (TmpHFirst!=(TmpH=High)) {
                      //Вот тут произошло прерывание, изменилось значение High, а вы уже считали старое.
                      TmpL= ReadReg(TimerCounter);  // Считываете значение соответствующее новому значению High.
                   }
                   return (TmpH<<sizeof(int))+TmpL;
                }


              1. MacIn
                22.12.2015 15:56
                +1

                Алгоритм с циклом может выполняться значительное время, что Джек и указал.

                Не-а. То-есть, конечно, может, но какова вероятность того, что у нас фазы будут так совпадать? Сдается мне, гораздо ниже, чем длительное прерывание и т.д. (см. критику выше). Или ниже, где izyk еще раз описал то же самое.


        1. GarryC
          22.12.2015 09:53

          Эта пара ( старый TmpH и новый TmpL) является абсолютно корректной. Она не отражает текущее состояние счетчика, но гарантировано возвращает правильное время, то есть не прошлое и не будущее, а одно из настоящих, а это — главное.


          1. MacIn
            22.12.2015 15:58

            Нет. У вас может high измениться несколько раз между проверкой условия и считыванием low (теоритически, если таймер очень скорострельный). В итоге у вас high от одной итерации, low — от совсем другой.


            1. MacIn
              22.12.2015 16:06

              Представьте:
              1) вы считываете high, он равен 10
              1.5) считываете low, он равен 50
              2) считываете high повторно, он равен 11
              3) принимаете решение перечитать low
              4) вываливаетесь
              5) high изменяется на 12
              6) low изменяется на 38
              7) считываете low
              получаем: high = 11, low = 38, хотя на самом деле пара — 12/38.
              Практически — я согласен — вы правы, эту «случайную» пару можно считать «настоящим» временем. И в практической реализации это действительно нормально. Просто алгоритм с циклом абсолютно корректен. Ваш — компромиссный.


              1. GarryC
                23.12.2015 08:50

                Рассмотрим любой из первых двух алгоритмов. Вот они отработали, получили текущее время, разрешили прерывание (тот, кто запрещал), произошло прерывания, счетчик убежал вперед и мы получаем то же самое, 11/38, хотя на самом деле пара 12/25 (к примеру). В высоко-нагруженной системе вернуть истинное время просто невозможно, и здесь ничего не поделать. Главное, чтобы полученное значение не было артефактом.
                В UNIX системах так и пишут, что задание задержки гарантирует только то, что она не истечет ДО требуемого момента, а вот на сколько может его превзойти — не определено.
                НО если мы правильно используем полученные данные в стиле следующего псевдокода, то все хорошо
                1) выставляем (к примеру) 1 на выходе,
                2) считываем текущее время,
                3) прибавляем к нему требуемую задержку,
                … делаем что-то
                4) читаем текущее время и сравниваем с границей,
                5) если граница перейдена, снимаем 1 с выхода,
                то все три верных решения нам гарантируют, что 1 простоит на выходе минимум задержку времени.
                Обратите внимание на последовательность 1 и 2 — именно так, иначе неверно.


                1. izyk
                  23.12.2015 11:01

                  1) выставляем (к примеру) 1 на выходе,
                  2) считываем текущее время (реально сейчас, на момент возврата из функции, 12.02, а мы считываем 11.02), но единица установлена уже не меньше чем 1.00,
                  3) прибавляем к нему требуемую задержку (к примеру 0.30, получаем 11.32),
                  … делаем что-то
                  4) читаем текущее время и сравниваем с границей (реально сейчас 12.14, и мы считываем 12.14),
                  5) если граница перейдена, снимаем 1 с выхода (12.14 > 11.32, снимаем 1 с задержкой как минимум 0.12+1.00=1.12 вместо 0.30).

                  Но позвольте, какое отношение это имеет к ОСРВ. Если вас устраивает такая точность, тогда весь код должен быть(повторюсь):

                  unsigned long int GetTimer(void) { 
                     return (High<<sizeof(int))+0;
                  }


                  1. GarryC
                    23.12.2015 11:30

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


                    1. izyk
                      23.12.2015 11:46

                      del


                      1. GarryC
                        23.12.2015 12:03

                        ? Я Вас убедил?


                        1. izyk
                          23.12.2015 12:49

                          Кажется, я начинаю понимать, о чем вы.
                          Но какой смысл в такой процедуре, зачем вам вообще значение «TimerCounter», если ваша система не гарантирует точность меньше max(TimerCounter), достаточно значения High. А если вы гарантируете такую точность, то входными данными для данной процедуры должно быть — во время ее выполнение возможно только одно прерывание таймера.
                          И это надо проверять на случай сбоя.

                          Если сравнивать вашу процедуру с процедурой из книжки, в случае одного прерывания во время выполнения они эквивалентны. А в случае нескольких прерываний, более эффективна, т.к. возвращает первое подходящее значение.
                          Но во втором случае это означает, что ваша система не гарантирует точность меньше max(TimerCounter). И ее (процедуру) можно упростить.

                          unsigned long int GetTimer(void) { 
                             return (High<<sizeof(int))+0;
                          }


                          1. GarryC
                            23.12.2015 15:02

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


                            1. izyk
                              23.12.2015 17:12

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

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


  1. ToSHiC
    21.12.2015 19:15
    +4

    Кажется, в вашей реализации баг.

    Допустим, High = 10, счётчик переполняется при 1000.

    TmpH=High; // = 10
    TmpL= ReadReg(TimerCounter); //  = 999
    // тут случилось прерывание и High заинкрементилось
    if (TmpH!=High) TmpL= ReadReg(TimerCounter); // TmpL = 001
    return (TmpH<<sizeof(int))+TmpL; // Вернули 10.001
    


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


    1. GarryC
      21.12.2015 22:23

      Точно, спасибо большое, немного неправильно записал, конечно верхняя часть перечитывается, сейчас подправлю в тексте, но перечитывается 1 раз, без цикла. Еще раз спасибо, подправил.


      1. ToSHiC
        22.12.2015 00:13

        Тогда не сработает, возникнет прерывание, которое будет длиться дольше, чем 1 цикл переполнения. На подобные штуки нужно смотреть с другой стороны (тут разбираю вариант с while циклом):
        1. Идеальный случай, никакого переполнения. Избыточность — 1 команда чтения, 1 команда сравнения, 1 команда условного перехода, которая сфейлится. Вероятность такого исхода зависит от конкретной программы, других возможных прерываний, скорости переполнения счётчика.
        2. Неидеальный случай, который будет случаться иногда — дополнительно 1 чтение из памяти и одно чтение из регистра счётчика к варианту 1.
        3. Плохой случай, если много прерываний, или они длинные. В этом случае придётся перечитывать много раз. Может случиться, если этот кусок кода имеет совсем уж низкий приоритет. Но работать будет.

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


  1. imwode
    21.12.2015 22:08
    +2

    Меня смущает сочетание «аппаратный счетчик» и sizeof(int)
    Во всяких микроконтроллерах, как правило, соседствуют таймеры разной разрядности, а sizeof(int) зависит от компилятора. Можно неслабо так нарваться…


    1. GarryC
      21.12.2015 22:20

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


      1. imwode
        21.12.2015 23:15
        +1

        Не, эта какая-то негодная и опасная перестраховка непонятно от чего непонятно зачем :-)


      1. encyclopedist
        22.12.2015 02:30
        +1

        sizeof(int) выдаёт размер инта в байтах (точнее, в символах, sizeof(char) == 1 по определению), а не в битах. Поэтому для сдвига его надо умножить на CHAR_BITS.


  1. Amomum
    21.12.2015 22:28

    TmpWait=GetTimer()+SomeDelay;
    while (TmpWait > GetTimer()); /* первая строка */
    while ((TmpWait — GetTimer()) > 0); /*вторая строка */

    Насколько я понимаю, первая строка неправильная, т.к. не учитывает переполнение, которое могло случится при вычислении TmpWait.


    1. Ocelot
      21.12.2015 22:39
      +1

      Они обе не учитывают переполнение, а во второй еще и вычитаются две беззнаковые переменные, так что результат тоже будет беззнаковый. И выражение (0-1) там даст 0xFFFFFFFF, а не (-1), как ожидалось.


      1. MacIn
        21.12.2015 23:04

        В Windows аналогичная проблема для GetTickCount.


      1. Amomum
        21.12.2015 23:14
        +1

        Вероятно, вы правы. Я обычно пишу

        prevTime = GetTime();
        while( GetTime() - prevTime < delay );
        

        Сходу не могу сообразить, но вроде бы вторая строка этому не эквивалентна; не помню досконально правила неявного приведения типа.


        1. Ocelot
          21.12.2015 23:40

          Ноуп! Тут тоже ошибка. Допустим, prevTime=900, delay=200, а GetTime() переполняется на 1000. Тогда выход из цикла произойдет сразу после переполнения, через 100 тиков вместо ожидаемых 200.

          UPD: нет, туплю, все вроде ок.


  1. izyk
    22.12.2015 02:05

    Если в вашей системе возможно что-то отличное от следующего:

    unsigned long int GetTimer(void) { 
       unsigned long TmpH,TmpL;
       TmpH=High;
       TmpL= ReadReg(TimerCounter);
       if ( TmpH != High || TmpH++ != High ) exit(error);
       return (TmpH<<sizeof(int))+TmpL;
    }

    то значение «TimerCounter» для вас не должно иметь смысла(ИМХО). И вы его можете просто не учитывать (выполнять округление).


    1. izyk
      22.12.2015 02:52

      Накосячил. Более правильно, наверное, так.

      unsigned long int GetTimer(void) { 
         unsigned long TmpH,TmpL;
         TmpH=High;
         TmpL=ReadReg(TimerCounter);
         if ( TmpH+1 == High ) TmpL=ReadReg(TimerCounter);
         if ( TmpH != High || TmpH++ != High ) exit(error);
         return (TmpH<<sizeof(int))+TmpL;
      }

      Многовато if, но вроде должно работать. Заодно своеобразный watchdog получился.
      А если значение High во время вызова функции может увеличится более чем один раз, и это нормально, тогда функция должна быть такой:
      unsigned long int GetTimer(void) { 
         return (High<<sizeof(int))+0;
         // Или просто
         return High;
      }

      Но это мое мнение. И вполне могу предположить что оно не верное.


      1. izyk
        22.12.2015 14:35
        +1

        Не, мой вариант не правильный.
        Кстати, первый «правильный», тоже не правильный.

        unsigned long int GetTimer(void) { 
           DisableInt();
           unsigned long TmpH=High;
           if (TimerOvBit()) TmpH++; //Это условие не сработало здесь.
           //А Здесь происходит переполнение и следующей строкой "ReadReg", вместо значения близкого к максимуму, считает близкое к нулю.
           TmpH=(TmpH<<sizeof(int))+ ReadReg(TimerCounter);
           EnableInt();
           return Tmp;
        }

        Единственный правильный вариант:
        unsigned long int GetTimer(void) { 
           unsigned long TmpH,TmpL;
           do {
               TmpH=High;
               TmpL= ReadReg(TimerCounter);
           while (TmpH!=High);
           return (TmpH<<sizeof(int))+TmpL;
        }

        А если нужно предотвратить зависание, нужно ввести счетчик циклов.


        1. MacIn
          22.12.2015 16:08

          А если нужно предотвратить зависание, нужно ввести счетчик циклов.

          Плюс за здравую мысль. Если цикл занимает уже слишком долго, мы выдаем компромиссный вариант.


  1. encyclopedist
    22.12.2015 02:33

        if (TmpH!=(TmpH=High)) TmpL= ReadReg(TimerCounter);
    
    — это неопределенное поведение. В условии одна и та же переменная читается и модифицируется.


    1. GarryC
      22.12.2015 09:20

      Да, Вы правы, у меня работает, но это как повезет. Дополнил пост непротиворечивой функцией.