Использование millis() и micros() внутри процедуры прерывания
В документации для attachInterrupt()
говорится:
...
millis()
полагается на прерывания для подсчета, поэтому он никогда не будет увеличиваться внутри ISR. Посколькудля работы функции delay()
требуются прерывания, она не будет работать, если вызывается внутри ISR.micros()
работает изначально, но начнет вести себя беспорядочно через 1-2 мс. ...
Чем micros()
отличается от millis()
(за исключением, конечно, их точности)? Означает ли приведенное выше предупреждение, что использование micros()
внутри процедуры прерывания всегда является плохой идеей?
Контекст - Я хочу измерить низкую заполняемость импульсов, поэтому мне нужно запустить свою процедуру при изменении входного сигнала и записать текущее время.
@Petr, 👍14
4 ответа
Лучший ответ:
Другие ответы очень хороши, но я хочу подробнее рассказать о том, как работает 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 мкс).
Цитируемая фраза не является предупреждением, это просто утверждение о том, как все работает.
Нет ничего изначально неправильного в использовании 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
Нет ничего плохого в том, чтобы использовать millis()
или micros()
в подпрограмме прерывания.
Неправильно использовать их неправильно.
Главное здесь то, что, пока вы находитесь в режиме прерывания, "часы не тикают". millis()
и micros()
не изменятся (ну, micros()
сначала изменится, но как только он пройдет ту волшебную миллисекундную точку, где требуется миллисекундный тик, все развалится.)
Таким образом, вы, конечно, можете позвонить в millis()
или micros ()
, чтобы узнать текущее время в вашем ISR, но не ожидайте, что это время изменится.
Именно об этом отсутствии изменений во времени предупреждается в приведенной вами цитате. delay()
зависит от изменения миллиса ()
, чтобы узнать, сколько времени прошло. Так как это не меняет, функция delay()
никогда не сможет закончиться.
Таким образом, по сути, millis()
и micros()
сообщат вам время, когда был вызван ваш ISR, независимо от того, когда в вашем ISR вы их используете.
Нет, обновления " micros ()". Он всегда считывает регистр аппаратного таймера., @Nick Gammon
Вы никогда не должны ничего делать внутри ISR, кроме как установить флаг, сообщающий, что вы получили прерывание, и вернуться.
Хотя верно, что код ISR должен быть как можно короче, это утверждение является чрезмерным. Есть много ситуаций, когда уместно заняться чем-то другим. На самом деле, просто устанавливать флаг бесполезно: аппаратное обеспечение, по крайней мере, на AVR, делает это за вас, без необходимости включать прерывание., @Edgar Bonet
@EdgarBonet, я не уверен, но я думаю, что пользователи с высокой репутацией могут проголосовать за удаление ответа с отрицательной оценкой, @Juraj
@Juraj: Действительно, вы правы., @Edgar Bonet
Возможно, это отличается от совета, который вы получили, но он упрощает код и поощряет хорошую структуру программы. Откройте свой разум и посмотрите, как это предложение устранит проблемы в примере кода. Единственное, что "чрезмерно", - это мои 30 лет программирования прошивки для IBM :), @bill
- Arduino непрерывно считывает значение АЦП с помощью прерывания
- Использование TIMER0_COMPB_vect
- Кнопка с таймером переключения и функцией сброса времени + светодиод обратной связи
- Использовать timer0, не влияя на millis() и micros().
- Влияет ли `millis()` на длинные ISR?
- 4-битный счетчик вверх и вниз
- Включить и отключить отдельные прерывания
- ATtiny85 AC Phase Control для регулировки яркости лампочки
Не в состоянии понять приведенное ниже утверждение, которое используется в 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