Pull to refresh

Comments 45

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

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

Да, полученные значения будут отставать от реальных, но главное, что они будут валидными всегда.
1. volatile в определении переменных не указываете — тоже очевидно, опустим? ;)
2. А если в нашей «высоконагруженной системе» бомбанёт ряд длительных прерываний вот в этом месте?
if (TmpH!=High) TmpL= ReadReg(TimerCounter);
… а таймер будет достаточно скоростным — имеем реальный риск всё же неверно считать данные.

Я бы крайне рекомендовал, если синхронизация настолько важна, всё же выключать прерывания на время считывания таких критичных значений. Конструкция проще, вероятность допустить ошибку меньше.
В том то и дело, что все равно мы считаем правильные данные. Да, они будут отставать от текущего значения, но они НЕ будут невалидными, в этом вся фишка.
Нет. Решение Джека Гансли — bulletproof, ваше — выдает правильное значение с некоторой веротяностью, большей, чем для решения «в лоб» в самом начале статьи.
if (TmpH!=High) TmpL= ReadReg(TimerCounter);

Допустим, вы считываете новое значение High, т.к. оно отличается от предыдущего, мы принимаем решение считать по новой TmpL. Теперь допустим, мы классически вываливаемся после принятия решения, но до считывания TmpL. В это время High опять обновляется. Теперь мы возвращаемся, считываем TmpL и имеем некорректную пару — старый TmpH и новый TmpL.
Обычно таймер времени переполняется гораздо дольше, чем самое долгое прерывание. Как мне кажется, этот алгоритм для таких случаев, но это надо специально отметить в описании.
Разумеется, описанный алгоритм покрывает все более-менее реальные случаи. Тем не менее, алгоритм с циклом ничем не хуже и покрывает все.
Адгоритм с циклов может выполняться значительное время, что Джек и указал.
Мой алгоритм работает для всех случаев, я пропустил одно присваивание в тексте, теперь исправлено, и он дает правильный результат во всех случаев. Я пост дополнил более расширенным вариантом.
Да не работает ваш алгоритм в определенных условиях. Например, если значение 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;
}
Алгоритм с циклом может выполняться значительное время, что Джек и указал.

Не-а. То-есть, конечно, может, но какова вероятность того, что у нас фазы будут так совпадать? Сдается мне, гораздо ниже, чем длительное прерывание и т.д. (см. критику выше). Или ниже, где izyk еще раз описал то же самое.
Эта пара ( старый TmpH и новый TmpL) является абсолютно корректной. Она не отражает текущее состояние счетчика, но гарантировано возвращает правильное время, то есть не прошлое и не будущее, а одно из настоящих, а это — главное.
Нет. У вас может high измениться несколько раз между проверкой условия и считыванием low (теоритически, если таймер очень скорострельный). В итоге у вас high от одной итерации, low — от совсем другой.
Представьте:
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.
Практически — я согласен — вы правы, эту «случайную» пару можно считать «настоящим» временем. И в практической реализации это действительно нормально. Просто алгоритм с циклом абсолютно корректен. Ваш — компромиссный.
Рассмотрим любой из первых двух алгоритмов. Вот они отработали, получили текущее время, разрешили прерывание (тот, кто запрещал), произошло прерывания, счетчик убежал вперед и мы получаем то же самое, 11/38, хотя на самом деле пара 12/25 (к примеру). В высоко-нагруженной системе вернуть истинное время просто невозможно, и здесь ничего не поделать. Главное, чтобы полученное значение не было артефактом.
В UNIX системах так и пишут, что задание задержки гарантирует только то, что она не истечет ДО требуемого момента, а вот на сколько может его превзойти — не определено.
НО если мы правильно используем полученные данные в стиле следующего псевдокода, то все хорошо
1) выставляем (к примеру) 1 на выходе,
2) считываем текущее время,
3) прибавляем к нему требуемую задержку,
… делаем что-то
4) читаем текущее время и сравниваем с границей,
5) если граница перейдена, снимаем 1 с выхода,
то все три верных решения нам гарантируют, что 1 простоит на выходе минимум задержку времени.
Обратите внимание на последовательность 1 и 2 — именно так, иначе неверно.

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;
}

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

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

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

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

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

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

Ваш вариант (в котором был баг) с точки зрения 1 и 2 случаев эквивалентен, но сфейлится в случае 3 (и ещё если просто очень долгое прерывание было). При этом с точки зрения количества исполненных инструкций он идентичен, разница только в команде, на которую будет указывать условный переход. На самом деле, вариант с while мне больше нравится даже с эстетической точки зрения.
Меня смущает сочетание «аппаратный счетчик» и sizeof(int)
Во всяких микроконтроллерах, как правило, соседствуют таймеры разной разрядности, а sizeof(int) зависит от компилятора. Можно неслабо так нарваться…
Ну здесь подразумевалось, что таймер разрядностью в слово. Если меньше, то тоже не страшно, а вот если больше, то можно было бы и нарваться. У Джека было просто 16, это я слегка перестраховался.
Не, эта какая-то негодная и опасная перестраховка непонятно от чего непонятно зачем :-)
sizeof(int) выдаёт размер инта в байтах (точнее, в символах, sizeof(char) == 1 по определению), а не в битах. Поэтому для сдвига его надо умножить на CHAR_BITS.
TmpWait=GetTimer()+SomeDelay;
while (TmpWait > GetTimer()); /* первая строка */
while ((TmpWait — GetTimer()) > 0); /*вторая строка */

Насколько я понимаю, первая строка неправильная, т.к. не учитывает переполнение, которое могло случится при вычислении TmpWait.
Они обе не учитывают переполнение, а во второй еще и вычитаются две беззнаковые переменные, так что результат тоже будет беззнаковый. И выражение (0-1) там даст 0xFFFFFFFF, а не (-1), как ожидалось.
В Windows аналогичная проблема для GetTickCount.
Вероятно, вы правы. Я обычно пишу
prevTime = GetTime();
while( GetTime() - prevTime < delay );

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

UPD: нет, туплю, все вроде ок.
Если в вашей системе возможно что-то отличное от следующего:
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» для вас не должно иметь смысла(ИМХО). И вы его можете просто не учитывать (выполнять округление).
Накосячил. Более правильно, наверное, так.
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;
}

Но это мое мнение. И вполне могу предположить что оно не верное.
Не, мой вариант не правильный.
Кстати, первый «правильный», тоже не правильный.
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;
}

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

Плюс за здравую мысль. Если цикл занимает уже слишком долго, мы выдаем компромиссный вариант.
    if (TmpH!=(TmpH=High)) TmpL= ReadReg(TimerCounter);
— это неопределенное поведение. В условии одна и та же переменная читается и модифицируется.
Да, Вы правы, у меня работает, но это как повезет. Дополнил пост непротиворечивой функцией.
Интересно услышать чем Вы компилировали, мой выдал код, в котором это условие никогда не выполняется, а при оптимизации, совсем его выкидывает.
Обычным IAR, но с выключенными оптимизациями.
Уж если Вы задаёте вопрос, то задавайте его корректно, иначе, в приведенном Вами примере:
unsigned long TmpWait;
   TmpWait=GetTimer()+SomeDelay;
   while (TmpWait > GetTimer()) ; /* первая строка */ 
   while ((TmpWait - GetTimer()) > 0) ; /*вторая строка */
}

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

Вероятно Вы хотели написать так?
Ну а в этом случае, само собой, необходимо учитывать переполнения, которые приведут к истинности условия №1 многократно, за период таймера, когда второе условие выполнится корректно, даже при переполнении.
Исправьте, если я не прав.
Мне кажется, что все таки Вы не правы.
Я хочу дождаться некоторого момена времени после текущего, вычисляю желаемый момент относительно текущего "TmpWait=GetTimer()+SomeDelay;"
и жду, пока он настанет "while (TmpWait > GetTimer())".
В некотором смысле обе строки ожидания эквивалентны, разница будет видна только когда произойдет переполнение в строке вычисления желаемого момента, тогда первый вариант ожидания сработает неверно (не будет ожидать вообще) а вот второй вариант вполне корректен.
Но тогда второе условие у Вас невыполнимо (в смысле — истинно всегда), либо явно преобразуйте типы в знаковые.
Впрочем мой вариант имеет ту же ошибку :)
Ну да, точно, там приводят к знаковому.
Sign up to leave a comment.

Articles