Главная > AVR > INT0 в AVR: темный угол даташита

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);. Тем не менее, в данном примере это несущественно.

О, а смотрите, какая тут девушка в очках в хоре поет…

Рубрики:AVR
  1. Dolphin_Soft
    11/10/2015 в 18:12

    Чтобы осознавать такую простую фичу, читаем INT0 — как НЕМАСКИРУЕМОЕ ПРЕРЫВАНИЕ

    • YS
      16/10/2015 в 22:07

      INT0 не является немаскируемым, т.к. может быть запрещено.

      • Dolphin_Soft
        11/11/2015 в 12:53

        Ок, давай так сделаем:
        Перед блокировкой прерываний добавь:
        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;

        делать нельзя, нужно сначала запретить прерывания.

      • Dolphin_Soft
        13/11/2015 в 01:23

        Не заметил обновление статьи сразу 🙂 Теперь все верно. (про EIFR|=_BV(INTF0)).

  2. 29/08/2014 в 21:43

    Я как-то всегда связывал прерывания с событиями. Событие — это изменение в системе. Нарастающий фронт, ниспадающий фронт — это всё изменения в системе. Значит, это есть события. А значит, они вписываются в парадигму прерываний. Всё логично. А если в системе ничего не меняется — где ж тут событие-то? Всю жизнь на лапке единичный (нулевой) уровень — это не событие, это неизменное состояние. Реагировать на неизменное состояние — это как-то глупо и смахивает на некомпетентность. Поэтому я тоже не понимаю смысла прерываний-по-уровню. С моей точки зрения это какой-то «анахренизм». Я тоже так думаю, что разработчикам АВР-контроллеров было сложно в первых микроконтроллерах разместить на входах портов триггеры, которые бы «взводились» по событию.

    • YS
      30/08/2014 в 11:47

      Да, судя по всему это и правда сделано не от хорошей жизни. Но, кстати, как мне опять же подсказал Rtmip, в mega8, например, нет PCINT. Потому единственный способ извне разбудить МК, находящийся в состоянии сна, — вот такое вот странное прерывание, либо system reset.

  3. Vga
    27/08/2014 в 21:07

    EIFR|=_BV(INTF0); // Clear flag

    Классическая ошибка. И комментарий не менее классически врет не соответсвует коду.

    • Vga
      27/08/2014 в 21:09

      Упс, страйк куда-то подевался.

    • YS
      27/08/2014 в 21:12

      Здесь плевать на остальные флаги. А так запись выглядит привычней.

      • Vga
        30/08/2014 в 22:47

        Подобные строчки (и привычка их писать) — мина замедленного действия.
        А подобные строчки в учебных примерах кода — оружие массового поражения.

        • YS
          31/08/2014 в 17:01

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

          • Vga
            31/08/2014 в 23:04

            ОК, это было бы как минимум не хуже.

  4. Vga
    27/08/2014 в 20:45

    Он написан в стиле «кошмарный сон Vga» — на Си, насыщен директивами условной компиляции, макрозаменами и даже содержит немного магических чисел, в общем, я оттянулся.

    Зачем экивоки? Пиши честно «набыдлокодил, наслаждайтесь».

  1. No trackbacks yet.

Оставьте комментарий