INT0 в AVR: темный угол даташита
На эту особенность поведения внешнего прерывания в AVR мое внимание обратил коллега по форуму Радиокота Rtmip. Вопрос меня заинтересовал, и я провел небольшое исследование, результаты которого предлагаются вниманию читателей ниже.
Разумеется, перед тем, как приняться за исследование, нам понадобится правильная музыка — я в тишине редко работаю.
Итак, суть вопроса. Как известно, прерывания (все, и не только в AVR) устроены следующим образом: есть управляющие регистры, значения в них локально включают/выключают/настраивают интересующие прерывания; есть регистры флагов — когда происходит событие, которому суждено вызвать прерывание, флаг интересующего прерывания взводится аппаратно, после чего, если прерывания глобально разрешены, переход к обработчику производится немедленно, а если глобально запрещены — переход к обработчику производится по факту активного состояния интересующего флага как только прерывания будут снова разрешены.
Внешнее прерывание INT0, служащее темой этой статьи, может быть сгенерировано в нескольких случаях, в частности, по фронту импульса на ножке, по спаду импульса, по изменению состояния и по низкому уровню на ножке. И вот тут начинается интересное — если вчитаться в даташит, можно прочесть, что буде прерывание сконфигурировано по низкому уровню, его флаг всегда сброшен.
Как так? А как же тогда вызов обработчика? А что будет, если прерывание случилось в момент, когда прерывания глобально запрещены? Как контроллер узнает, что оно было? И узнает ли?
Чтобы ответить на эти вопросы, я расчехлил макетку, написал небольшой тестик, код которого приведен листингом ниже, и провел несколько экспериментов.
Результаты:
1. Да, при конфигурировании прерывания по уровню флаг прерывания всегда сброшен.
2. При этом, если прерывания глобально разрешены, прерывание вызывается без установки флага! Чудеса!
3. Если уровень на ножке случился в момент, когда прерывания были запрещены (и пропал до их разрешения), событие будет потеряно. Контроллер никак о нем не узнает, в отличие от режимов реакции на фронт/спад/изменение уровня.
Разумеется, вывод верен не только для INT0, но и для INT1 — они идентичны.
В чем глубинный смысл такого режима? Если смотреть на вещи философски, то можно предположить, что режим чувствительности к уровню был добавлен исключительно ради того, чтобы выводить контроллер из состояния пониженного энергопотребления, поскольку прерывания по фронту/спаду/изменению не работают, когда тактовый генератор выключен, а вот прерывание по уровню работает и успешно пробуждает МК.
Но ведь перевести МК в активный режим можно и любым прерыванием из набора PCINT. Зачем нужна такая странная обработка INT0? Ответ можно найти, если почитать даташит на что-нибудь ископаемое из серии AVR; например, на AT90S1200. В те годы еще не было никаких новомодных PCINT, зато вполне были режимы пониженного энергопотребления и необходимость выходить из них еще каким-то образом кроме сброса системы — вот и придумали хитрый финт с INT0. Ныне это, очевидно, не более чем дань совместимости, поскольку прерывание по уровню в целом неудобно — срабатывает постоянно, пока присутствует уровень (есть потенциальный шанс подвесить контроллер), да и обычно реакции требует именно изменение состояния ножки — нажатие кнопки, срабатывание датчика, что-то еще в этом роде. Потому, собственно, мало кто обращает внимание на описываемые особенности.
Ниже приведен код теста. Он написан в стиле «кошмарный сон Vga» — на Си, насыщен директивами условной компиляции, макрозаменами и даже содержит немного магических чисел, в общем, я оттянулся. 😀 Программа позволяет тестировать прерывание INT0 в разных режимах и разными методами, в зависимости от определенных директив препроцессора. Для режимов 1 и 2 (тесты прерывания и флагов) требуется внешний подтягивающий резистор и кнопка, режим 3 (тест прерывания в период глобального запрещения прерываний) работает автономно.
#include <avr/io.h> #include <avr/interrupt.h> #include <util/delay.h> // 1 - test flag, 2 - test interrupt, 3 - test interrupt generated while interrupts are globally disabled #define TEST_TYPE 3 #define LOW_LEVEL_INTMASK 0 #define FALLING_EDGE_INTMASK _BV(ISC01) #define ANY_CHANGE_INTMASK _BV(ISC00) //The interrupt under test will be configured in this mode #define TESTED_MODE_MASK LOW_LEVEL_INTMASK #define INTERRUPT_LED _BV(PB1) #define FLAG_LED _BV(PB2) volatile uint8_t interrupt_was_processed=0; ISR (INT0_vect) { interrupt_was_processed=1; } void main(void) { #if TEST_TYPE==3 PORTD|=_BV(PD2); // Set default value DDRD|=_BV(PD2); // OMG, INT0 pin is an output! Yeah, it is permitted, and we'll be generating software interrupt. #else DDRD&=~_BV(PD2); // Make sure INT0 pin is an input #endif DDRB|=INTERRUPT_LED | FLAG_LED; // Debug LEDs //Flash sequence to make sure LEDs are properly connected PORTB|=INTERRUPT_LED | FLAG_LED; _delay_ms(500); PORTB&=~(INTERRUPT_LED | FLAG_LED); _delay_ms(500); PORTB|=INTERRUPT_LED | FLAG_LED; _delay_ms(500); PORTB&=~(INTERRUPT_LED | FLAG_LED); _delay_ms(500); EICRA=TESTED_MODE_MASK; EIMSK=_BV(INT0); // Turn on INT0 #if TEST_TYPE==2 sei(); #endif while (1) { #if TEST_TYPE==2 if (interrupt_was_processed) { PORTB|=INTERRUPT_LED; _delay_ms(50); PORTB&=~INTERRUPT_LED; _delay_ms(50); PORTB|=INTERRUPT_LED; _delay_ms(50); PORTB&=~INTERRUPT_LED; _delay_ms(50); interrupt_was_processed=0; } #elif TEST_TYPE==1 if (EIFR & _BV(INTF0)) { PORTB|=FLAG_LED; _delay_ms(50); PORTB&=~FLAG_LED; _delay_ms(50); PORTB|=FLAG_LED; _delay_ms(50); PORTB&=~FLAG_LED; _delay_ms(50); EIFR|=_BV(INTF0); // Clear flag } #elif TEST_TYPE==3 // Now, get ready. cli(); interrupt_was_processed=0; PORTB&=~(INTERRUPT_LED | FLAG_LED); // Interrupts are globally disabled. Generate software interrupt, falling edge on INT0 pin: PORTD&=~_BV(PD2); _delay_ms(1); // Just to be sure PORTD|=_BV(PD2); //A-a-a-and... Test it! sei(); _delay_ms(100); // Holding breath... //A-a-a-and... if (interrupt_was_processed) { PORTB|=INTERRUPT_LED | FLAG_LED; // WIN! MAGIC! } else { PORTB|=FLAG_LED; // Or nothing happened? } _delay_ms(1000); // Take some time to chill after marvellous result #else #error Unknown test mode #endif } }
UPD:
Как верно отметил в комментариях ув. Vga, запись EIFR|=_BV(INTF0) не совсем корректна. Такая запись очистит все флаги в EIFR, поскольку, согласно логике операции, сначала будет прочитано содержимое EIFR, после этого в нем будет установлен бит INTF0 (бесполезное действие, т.к. он и так там стоит в этой ветке условия), при этом остальные единицы, разумеется, останутся в прочитанном значении. После этого прочитанное значение, в данных условиях по сути представляющее собой копию EIFR, будет записано обратно. Поскольку очистка флага согласно документации производится записью в него единицы, все флаги будут очищены. Таким образом, рассматриваемая запись аналогична EIFR|=0; или, более точно, EIFR=EIFR;.
Корректная очистка только одного флага выглядит как EIFR=_BV(INTF0);. Тем не менее, в данном примере это несущественно.
О, а смотрите, какая тут девушка в очках в хоре поет…
Чтобы осознавать такую простую фичу, читаем INT0 — как НЕМАСКИРУЕМОЕ ПРЕРЫВАНИЕ
INT0 не является немаскируемым, т.к. может быть запрещено.
Ок, давай так сделаем:
Перед блокировкой прерываний добавь:
PORTC=EIFR;
_delay_us(50);
PORTC=0;
а после блокировки убери sei();
Естественно нужно настроить порт С на вывод.
Поверь, очень удивишься!
То что ты назвал запретом прерывания int0, ни что иное как — отмена обработчика, само же прерывание, на аппаратном уровне — происходит.
Чтобы этого избежать, думаю нужно обрабатывать не int0, а его «несты», типа PCINT0
Интересно что сей факт не документирован.
И еще, как заметил Vga, EIFR|=_BV(INTF0); запись не просто не желательная, а неправильная, вот выдержка из libc: http://www.nongnu.org/avr-libc/user-manual/FAQ.html#faq_intbits
Вот еще на заметку:
This may be especially important when clearing a bit:
var &= ~mask; /* wrong way! */
The bitwise «not» operator (~) will also promote the value in mask to an int. To keep it an 8-bit value, typecast before the «not» operator:
var &= (unsigned char)~mask;
и еще, вот так:
EICRA=_BV(ISC01) | _BV(ISC00);//TESTED_MODE_MASK;
делать нельзя, нужно сначала запретить прерывания.
Не заметил обновление статьи сразу 🙂 Теперь все верно. (про EIFR|=_BV(INTF0)).
Я как-то всегда связывал прерывания с событиями. Событие — это изменение в системе. Нарастающий фронт, ниспадающий фронт — это всё изменения в системе. Значит, это есть события. А значит, они вписываются в парадигму прерываний. Всё логично. А если в системе ничего не меняется — где ж тут событие-то? Всю жизнь на лапке единичный (нулевой) уровень — это не событие, это неизменное состояние. Реагировать на неизменное состояние — это как-то глупо и смахивает на некомпетентность. Поэтому я тоже не понимаю смысла прерываний-по-уровню. С моей точки зрения это какой-то «анахренизм». Я тоже так думаю, что разработчикам АВР-контроллеров было сложно в первых микроконтроллерах разместить на входах портов триггеры, которые бы «взводились» по событию.
Да, судя по всему это и правда сделано не от хорошей жизни. Но, кстати, как мне опять же подсказал Rtmip, в mega8, например, нет PCINT. Потому единственный способ извне разбудить МК, находящийся в состоянии сна, — вот такое вот странное прерывание, либо system reset.
Классическая ошибка. И комментарий не менее классически врет не соответсвует коду.
Упс, страйк куда-то подевался.
Здесь плевать на остальные флаги. А так запись выглядит привычней.
Подобные строчки (и привычка их писать) — мина замедленного действия.
А подобные строчки в учебных примерах кода — оружие массового поражения.
Это, однако, не учебный пример кода. Это тест, текст которого я мог бы вообще не приводить, а только огласить результаты, предложив желающим перепроверить самостоятельно.
ОК, это было бы как минимум не хуже.
Зачем экивоки? Пиши честно «набыдлокодил, наслаждайтесь».