Влияет ли `millis()` на длинные ISR?
У меня есть проект, который часто использует таймеры и прерывания. Много процессорного времени тратится на обработку ISR в течение длительного периода времени.
Повлияет ли это на код внутри основного цикла, который полагается на функцию millis(), поскольку на обработку ISR тратится значительное количество времени?
4 ответа
Лучший ответ:
После некоторого копания в ядре кажется, что Arduino обновляет millis()
с помощью 8-битного таймера: он использует переполнение со значением прескалера 64. Проще говоря, он настроен таким образом, что определенный фрагмент кода (ISR) запускается примерно один раз в миллисекунду в системе 16 МГц (и пропорционально реже в более медленных системах с тактовой частотой).
Когда этот ISR запускается, он увеличивает переменную, которая используется функцией millis()
.
Таким образом, даже если какой-то не прерываемый код выполняется в течение доли миллисекунды, это не имеет значения, так как бит для этого вектора прерывания все равно останется установленным.
Однако возникает проблема, когда ISR запускается реже, чем следовало бы. Это может быть результатом использования noInterrupts()
в течение длительного периода времени или когда выполняется слишком много других прерываний и не хватает процессора для всех задач. Если многие другие ISR имеют более высокие приоритеты прерываний, вполне возможно, что код для millis()
никогда не будет запущен. В этом случае у вас будут более серьезные проблемы, чем это, но это то, что нужно иметь в виду, когда вы пишете код.
Общее эмпирическое правило ISR по-прежнему действует: держите его коротким!!!
Примечание: если вам требуется предельная точность, встроенная функция - не лучший выбор. RTC - хорошая идея для длительного хранения времени.
Я думаю, что вы можете иметь прерывания, прерывающие другие прерывания-подпрограммы (вызывая sei()
внутри ISR
). Но это скользкий путь. Например, переполнение памяти может происходить намного быстрее., @Gerben
"micros()", похоже, имеет ту же проблему, и в моем тесте было то же самое неправильное время, что и "millis ()"., @Ulrich Stern
Этот вопрос не ограничивается ISR. Функция [write()](https://github.com/adafruit/Adafruit_TLC59711/blob/master/Adafruit_TLC59711.cpp#L63 ) в библиотеке Adafruit TLC59711 отключает прерывания, возможно, на много миллисекунд, в результате чего моя первоначальная попытка определить время этой функции с помощью micros ()
завершается неудачей., @Ulrich Stern
@UlrichStern, Tehee, adafruit заставляет вас думать, что их библиотека очень быстрая, приостанавливая ваш "временной ориентир"; D, @Paul
@Paul, при синхронизации библиотеки Adafruit возвращаемое значение micros()
все еще увеличивалось настолько, что среда выполнения не была явно неправильной, и я был одурачен на некоторое время. :) (Adafruit не отключал прерывания, чтобы обмануть меня, я думаю, см. Мою библиотеку [Wiki](https://github.com/ulrichstern/Tlc59711/wiki#how-does-the-tlc59711-know-when-a-transfer-is-done).), @Ulrich Stern
АБСОЛЮТНО
любой длинный ISR предотвратит инкрементирование millis(). Это может быть сомнительно медленный перекос этих часов.
Функция ISR всегда начинается с блокировки прерываний командой cli() при запуске и заканчивается их включением командой sei(). Блокирование других ISR вместе с прерыванием переполнения Timer0, которое используется для увеличения счетчика millis.
Любое расширенное использование noInterrupts() или cli() или подразумеваемое использование с ISR должно быть тщательно коротким по продолжительности. Нет никакого общего вреда в выполнении interrupts() или sei() в ISR, чтобы снова разрешить прерывания.
Пример:
в библиотеке VS1053 у меня есть запрос данных на PIN INT. Какой ISR может считываться с SD-карты, что может занять очень много времени. Поэтому я снова включаю interrupts() перед чтением SD-карты, но все еще в ISR.
В вашем примере основной цикл никогда не возобновляется во время этой длинной процедуры прерывания, верно? Так что это оборотная сторона. Приятно знать, что включение прерываний внутри других прерываний не так "опасно", как я думал., @Gerben
Я думаю, что только ISR (или цепочка ISR) длиной более миллисекунды предотвратит его прерывание. Вы можете заблокировать его на 0,5 мс, и он будет обновляться как самое следующее, что нужно сделать (прервать)., @Paul
Повлияет ли это на код внутри основного цикла, который полагается на функцию millis(), поскольку на обработку ISR тратится значительное количество времени?
Чтобы поставить на нем цифру для вас ...
Частота вызова переполнения таймера 0 ISR
Код, вызываемый (по умолчанию) Вектор прерывания переполнения таймера 0 (TIM0_OVF_vect) используется millis
и micros
для возврата их результатов. Его цель - подсчитать переполнение таймера 0.
Чтобы получить точные результаты, этот ISR не должен пропустить переполнение. Таймер настроен на тиканье каждые 4 мкс в системе 16 МГц (из-за прескалера 64: 64 * 62,5 нс = 4000 нс
) и переполнение каждые 1,024 мс (1024 мкс) - потому что он переполняется после 256 тиков (4 мкс * 256 = 1024 мкс
).
Поскольку существует только один флаг переполнения, если ISR пропускает переполнение, то и millis, и micros будут отсутствовать на 1,024 мс (или больше, если он пропускает несколько переполнений).
Поэтому, чтобы быть уверенным в улавливании этого переполнения, ISR должен быть вызван в течение 1,024 мс (вероятно, немного меньше из-за времени, затраченного на ввод ISR, скажем: 1 мс).
Приоритет прерывания
На ATmega328P (например, используемом в Arduino Uno) это приоритеты вектора прерывания:
1 Reset
2 External Interrupt Request 0 (pin D2) (INT0_vect)
3 External Interrupt Request 1 (pin D3) (INT1_vect)
4 Pin Change Interrupt Request 0 (pins D8 to D13) (PCINT0_vect)
5 Pin Change Interrupt Request 1 (pins A0 to A5) (PCINT1_vect)
6 Pin Change Interrupt Request 2 (pins D0 to D7) (PCINT2_vect)
7 Watchdog Time-out Interrupt (WDT_vect)
8 Timer/Counter2 Compare Match A (TIMER2_COMPA_vect)
9 Timer/Counter2 Compare Match B (TIMER2_COMPB_vect)
10 Timer/Counter2 Overflow (TIMER2_OVF_vect)
11 Timer/Counter1 Capture Event (TIMER1_CAPT_vect)
12 Timer/Counter1 Compare Match A (TIMER1_COMPA_vect)
13 Timer/Counter1 Compare Match B (TIMER1_COMPB_vect)
14 Timer/Counter1 Overflow (TIMER1_OVF_vect)
15 Timer/Counter0 Compare Match A (TIMER0_COMPA_vect)
16 Timer/Counter0 Compare Match B (TIMER0_COMPB_vect)
17 Timer/Counter0 Overflow (TIMER0_OVF_vect)
18 SPI Serial Transfer Complete (SPI_STC_vect)
19 USART Rx Complete (USART_RX_vect)
20 USART, Data Register Empty (USART_UDRE_vect)
21 USART, Tx Complete (USART_TX_vect)
22 ADC Conversion Complete (ADC_vect)
23 EEPROM Ready (EE_READY_vect)
24 Analog Comparator (ANALOG_COMP_vect)
25 2-wire Serial Interface (I2C) (TWI_vect)
26 Store Program Memory Ready (SPM_READY_vect)
Из этого списка видно, что TIMER0_OVF_vect является номером 17 в этом списке, поэтому любое более раннее приоритетное прерывание будет иметь приоритет, например, внешние прерывания, прерывания смены PIN, другие таймеры (однако не SPI / Serial / ADC / I2C).
Если бы переполнение только что произошло, то у вас было бы практически 2 мс благодати (потому что у вас есть 1 мс до следующего, а затем еще 1 мс, прежде чем вам нужно будет это заметить). Однако если переполнение вот-вот произойдет, то у вас есть только 1 мс льготного периода.
Я упоминаю об этом потому, что если у вас есть внешнее событие прерывания 0 (INT0_vect) и ISR занимает 500 мкс, а затем внешнее событие прерывания 1 (INT1_vect) в течение этого времени (так что будет обслуживаться другой ISR), то прерывание таймера может быть заблокировано на некоторое время.
Вот почему все ISR должны быть короткими. Недостаточно того, что некоторые из них таковы.
Повторное включение прерываний
Я настоятельно не рекомендую этого делать. Библиотеки не предназначены для повторного ввода, и как только вы начнете включать прерывания в одном ISR, вы можете обнаружить, что он сам вызывается снова, когда он находится на полпути к первому вызову. Вы также можете прервать библиотечную функцию (например, memcpy), которая не была предназначена для этого.
И, конечно, если вы повторно включаете прерывания внутри ISR, потому что ISR занимает много времени: ну, это именно та ситуация, когда вы можете вызвать это повторное включение.
Дополнительная информация:
- Прерывает
- Таймеры
- Переполнение миллиса
Прерывание таймера может даже нарушить измерение импульсов с помощью micros() из прерывания on change. Прерывание таймера занимает около 6,5us на Arduino Uno или mega. Если прерывание изменения происходит сразу после прерывания таймера, то прерывание изменения будет отложено до 6,5us до тех пор, пока прерывание таймера не завершится. Кроме того, счетчик микросекунд обновляется только один раз в 4us, что приводит к дополнительной неопределенности до 4us.
Таким образом, измерение может быть до 10,5us out. Это следует учитывать при измерении длительности импульсов.
- Использование millis() и micros() внутри процедуры прерывания
- Почему необходимо использовать ключевое слово volatile для глобальных переменных при обработке прерываний в ардуино?
- Серийное прерывание
- Как прервать функцию цикла и перезапустить ее?
- Аппаратное прерывание срабатывает случайным образом
- Какой правильный способ запроса устройства I2C из процедуры обслуживания прерывания?
- Чтение квадратурного энкодера в реальном времени с полным разрешением только с одним прерыванием на ATmega328
- Как выйти из прерывания таймера (ISR(TIMER1_COMPA_vect))
Кроме того, все, что основано на millis(), также потерпит неудачу? Я думаю, что (новая основанная на программном обеспечении) библиотека сервоприводов и, возможно, даже softwareserial (?) Также могут возникнуть проблемы с синхронизацией? Хотя я не уверен, что они оба основаны на этом., @Paul