Использование millis() и micros() внутри процедуры прерывания

В документации для attachInterrupt() говорится:

... millis() полагается на прерывания для подсчета, поэтому он никогда не будет увеличиваться внутри ISR. Поскольку для работы функции delay() требуются прерывания, она не будет работать, если вызывается внутри ISR. micros() работает изначально, но начнет вести себя беспорядочно через 1-2 мс. ...

Чем micros() отличается от millis() (за исключением, конечно, их точности)? Означает ли приведенное выше предупреждение, что использование micros() внутри процедуры прерывания всегда является плохой идеей?

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

, 👍14


4 ответа


Лучший ответ:

20

Другие ответы очень хороши, но я хочу подробнее рассказать о том, как работает micros (). Он всегда считывает текущий аппаратный таймер (возможно,TCNT0), который постоянно обновляется аппаратным обеспечением (фактически, каждые 4 мкс из-за прескалера 64). Затем он добавляет счетчик переполнения таймера 0, который обновляется прерыванием переполнения таймера (умноженным на 256).

Таким образом, даже внутри ISR вы можете полагаться на обновление micros (). Однако, если вы будете ждать слишком долго, вы пропустите обновление переполнения, и тогда возвращенный результат снизится (т. е. вы получите 253, 254, 255, 0, 1, 2, 3 и т.д.)

Это micros() - немного упрощено для удаления определений для других процессоров:

unsigned long micros() {
    unsigned long m;
    uint8_t oldSREG = SREG, t;
    cli();
    m = timer0_overflow_count;
    t = TCNT0;
    if ((TIFR0 & _BV(TOV0)) && (t < 255))
        m++;
    SREG = oldSREG;
    return ((m << 8) + t) * (64 / clockCyclesPerMicrosecond());
}

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


TLDR;

  • Не делайте задержек внутри ISR
  • Если вы должны это сделать, вы можете провести время с помощью micros (), но не millis(). Также возможна задержка в микросекундах ().
  • Не задерживайтесь более чем на 500 мкс или около того, иначе вы пропустите переполнение таймера.
  • Даже короткие задержки могут привести к тому, что вы пропустите входящие последовательные данные (при скорости 115200 бод вы будете получать новый символ каждые 87 мкс).
,

Не в состоянии понять приведенное ниже утверждение, которое используется в micros(). Не могли бы вы поподробнее, пожалуйста? Поскольку ISR записан, флаг TOV0 будет снят, как только будет введен ISR, и, следовательно, условие ниже может не сбыться!. если ((TIFR0 & _BV(TOV0)) && (t < 255)) м++;, @Rajesh

`micros () " не является ISR. Это нормальная функция. Флаг TOV0 позволяет протестировать аппаратное обеспечение, чтобы увидеть, произошло ли переполнение таймера (но еще не обработано)., @Nick Gammon

Поскольку тест выполняется с отключенными прерываниями, вы знаете, что флаг не изменится во время теста., @Nick Gammon

То есть вы хотите сказать, что после cli() внутри функции micros (), если произойдет какое-либо переполнение, это необходимо проверить? В этом есть смысл. В противном случае, даже если micros не является функцией, TIMER0_OVF_vect ISR очистит TOV0 в TIFR0, в чем я сомневался., @Rajesh

Также, почему TCNT0 сравнивается с 255, чтобы увидеть, меньше ли оно 255? После cli (), если TCNT0 достигнет 255, что произойдет?, @Rajesh

Даже при отключенных прерываниях аппаратные таймеры будут продолжать подсчитываться. Я думаю, что тест на < 255 связано с возможной ситуацией, когда флаг переполнения был установлен, но таймер все еще находится на отметке 255 (так что он еще не совсем переполнен). Следующий по сравнению с 255 будет равен 0., @Nick Gammon

*TIMER0_OVF_vect ISR очистит TOV0 в TIFR0, в чем я сомневался* - возможно, таймер переполнен, и ISR был запланирован, но еще не запущен., @Nick Gammon

В частности, после строки "t = TCNT0;" таймер может переполниться *только сейчас*, поэтому нам нужно проверить флаг переполнения, и он " t " меньше 255, и флаг переполнения установлен, тогда он, вероятно, переполнен между этими двумя инструкциями., @Nick Gammon


6

Цитируемая фраза не является предупреждением, это просто утверждение о том, как все работает.

Нет ничего изначально неправильного в использовании millis() или micros() в правильно написанной процедуре прерывания.

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

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

Короче говоря: правильно написанная процедура прерывания не вызовет и не вызовет проблем с millis() или micros().

Редактировать: Что касается “почему micros() "начинает вести себя беспорядочно"”, как объясняется навеб-странице “изучение функции Arduino micros”, код micros() на обычном Uno функционально эквивалентен

unsigned long micros() {
  return((timer0_overflow_count << 8) + TCNT0)*(64/16);
}

Это возвращает четырехбайтовую длину без знака, состоящую из трех младших байтов из timer0_overflow_count и одного байта из регистра подсчета таймера-0.

Счетчик timer0_overflow_count увеличивается примерно один раз в миллисекунду обработчиком прерываний TIMER0_OVF_vect, как описано в обзоре веб-страницы функции arduino millis.

Перед запуском обработчика прерываний аппаратное обеспечение AVR отключает прерывания. Если бы (например) обработчик прерываний выполнялся в течение пяти миллисекунд с отключенными прерываниями, было бы пропущено по крайней мере четыре переполнения таймера 0. [Прерывания, написанные в коде C в системе Arduino, не являются реентерабельными (способны корректно обрабатывать несколько перекрывающихся исполнений в одном и том же обработчике), но можно написать реентерабельный обработчик языка ассемблера, который восстанавливает прерывания до того, как начнется трудоемкий процесс.]

Другими словами, переполнения таймера не “накапливаются”; всякий раз, когда переполнение происходит до того, как было обработано прерывание от предыдущего переполнения, счетчик millis() теряет миллисекунду, а несоответствие в timer0_overflow_count, в свою очередь, также делает micros() неправильным на миллисекунду.

Что касается “менее 500 мкс” в качестве верхнего ограничения по времени для обработки прерываний, “чтобы предотвратить блокировку прерывания таймера слишком надолго”, вы можете увеличить время чуть менее 1024 мкс (например, 1020 мкс), и millis() все равно будет работать большую часть времени. Однако я рассматриваю обработчик прерываний, который занимает более 5 мкс, как вялый, более 10 мкс как ленивый, более 20 мкс как улиткообразный.

,

Не могли бы вы подробнее рассказать о том, "как все работает", в частности, почему "micros ()" "начинает вести себя беспорядочно"? И что вы подразумеваете под "правильно написанной процедурой прерывания"? Я предполагаю, что это означает "короче 500us" (чтобы предотвратить блокировку прерывания таймера слишком долго), "использование изменчивых переменных для связи" и "по возможности не вызывать код библиотеки", есть ли что-нибудь еще?, @Petr

@PetrPudlák, см. правку, @James Waldby - jwpat7


9

Нет ничего плохого в том, чтобы использовать millis() или micros() в подпрограмме прерывания.

Неправильно использовать их неправильно.

Главное здесь то, что, пока вы находитесь в режиме прерывания, "часы не тикают". millis() и micros() не изменятся (ну, micros() сначала изменится, но как только он пройдет ту волшебную миллисекундную точку, где требуется миллисекундный тик, все развалится.)

Таким образом, вы, конечно, можете позвонить в millis() или micros (), чтобы узнать текущее время в вашем ISR, но не ожидайте, что это время изменится.

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

Таким образом, по сути, millis() и micros() сообщат вам время, когда был вызван ваш ISR, независимо от того, когда в вашем ISR вы их используете.

,

Нет, обновления " micros ()". Он всегда считывает регистр аппаратного таймера., @Nick Gammon


0

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

,

Хотя верно, что код ISR должен быть как можно короче, это утверждение является чрезмерным. Есть много ситуаций, когда уместно заняться чем-то другим. На самом деле, просто устанавливать флаг бесполезно: аппаратное обеспечение, по крайней мере, на AVR, делает это за вас, без необходимости включать прерывание., @Edgar Bonet

@EdgarBonet, я не уверен, но я думаю, что пользователи с высокой репутацией могут проголосовать за удаление ответа с отрицательной оценкой, @Juraj

@Juraj: Действительно, вы правы., @Edgar Bonet

Возможно, это отличается от совета, который вы получили, но он упрощает код и поощряет хорошую структуру программы. Откройте свой разум и посмотрите, как это предложение устранит проблемы в примере кода. Единственное, что "чрезмерно", - это мои 30 лет программирования прошивки для IBM :), @bill