Прерывания

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

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

Вот пример из бытовой жизни - сидите Вы на кухне, пьете чай с малиновым вареньем и вкусняшками неприменно, и ждете гостей. А как узнать, что кто-то пришел? Тут два варианта: либо мы каждые пять минут будем отвлекаться от варенья, в смысле, чая и бегать проверять, а не стоит ли кто за дверью, либо купить дверной звонок и спокойненько ждать на нагретом месте, пока кто-нибудь в него не позвонит.

Так вот, когда гость звонит - это событие. Соответственно, мы прерываемся и кидаемся к двери.

Итак, у микросхемы есть прерывания. И не одно. Прерывания делятся на внешние - такие срабатывают при определённом напряжении на некоторых выводах микросхемы (INT0, INT1 и также иногда целый порт PCINT) - и внутренние - при переполнении счётчика, срабатывании сторжевого таймера, при использовании USART, при прерывании аналогового компаратора, АЦП и прочей периферии.

Соответственно, возникает проблема приоритета. Это как мы все также сидим и пьем чай, но звонят нам уже не только в дверь, но и по телефону... И ведь не разорвешься, что-то нужно сделать первым. Поэтому в даташите есть таблица векторов прерываний. Чем меньше номер прерывания, тем более оно приоритетно.

Здесь получается несколько тонкостей...

Вот произошло событие - пошел запрос на прерывание, то есть выставляется так называемый "флаг запроса на прерывание". Если все хорошо, прерывание разрешено, то жизнь прекрасна и происходит его обработка.

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

НО! Получается, что даже если прерывание обрабатывается, не факт, что событие, которое его вызвало, ещё живо... Это как позвонили в дверь и по телефону одновременно, Вы ответили по телефону, а гости уже решили, что никого дома нету и ушли. И вроде как событие - звонок в дверь - было, а за дверью никого нет.

Ещё проблема - что пока обрабатывается другое прерывание и флаг запроса уже поднят, событие может произойти ещё несколько раз. Ответили на звонок по телефону, открываем дверь - а там уже целая куча гостей! Страшно? Страшно...

 

Ещё одна особенность использования прерываний - да и не только прерываний: реентерабельность (или повторная входимость).

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

Иными словами, если на кухне во то время, пока Вы встречаете гостей, никто не утащит себе вкусняшки, то значит все реентерабельно)

В общем, серезная штука - если её не учитывать, можно долго мучится с "а чего же она не работает?!". Нужно её учитывать, например, если обрабатываются несколько прерываний, и каждое изменяет какую-то глобальную переменную...

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

Рассмотрим работу с внешними прерываниями: нам нужно, во-первых, настроить, по какому событию будет происходить прерывание, а, во-вторых, разрешить микросхеме вообще обрабатывать это самое прерывание.

За первое в микросхеме ATmega8 отвечает регистр MCUCR - биты ISC11-ISC10, отвечающие за INT1, и ISC01-ISC00, отвечающие за INT0.

ISC11ISC10Событие, по которому генерируется прерывание
0 0 Нижний уровень на выводе INT1
0 1 Любое изменение уровня на выводе INT1
1 0 Нисходящий фронт сигнала (смена 1 на 0) на выводе INT1
1 1 Восходящий фронт сигнала (смена 0 на 1) на выводе INT1

Таблица 1. Определение событий для генерации прерывания по INT1

Соответственно, с INT0 аналогично.

А теперь остается разрешить прерывания по нужному нам выводу - в регистре GIGR есть биты INT0 и INT1; поставить на нужный "1" - и внешнее прерывание разрешено! Но ещё рано радоваться - помимо внешних прерываний надо разрешить прерывания вообще - ставим крайний левый бит I регистра SREG в "1". Это же можно сделать и ассемблерной командой: asm sei;

Рассмотрим простой примерчик: на ножку INT0 (D.2) микросхемы ATmega8 присоединена кнопка (к ножке и на ноль); нажимаем - возникает прерывание и включается светодиодик на выводе B.0. Светодиод, соответственно, подключен к ножке и на единицу:

//программа для ATmega8 при нажатии на кнопку на выводе INT0 (D.2) - присоединена к 0 - 
//включает по внешнему прерыванию светодиодик на выводе B.0 - присоединён к 1
//переопределяем типы
typedef unsigned char byte;

sbit ddrButton at ddD2_bit;  //кнопка генерации
sbit pinButton at pinD2_bit;
sbit portButton at portD2_bit;

sbit ddrLight at ddB0_bit;  //вывод для светодиода, на выход с подтяжкой, включается 0 по кнопке
sbit portLight at portB0_bit;

byte flagButton = 0; //флаг нажатия кнопки; нажата - 1

void INT0_interrupt() org IVT_ADDR_INT0 //нужно написать как минимум пустую функцию -
                                       //потому что компилятор сам не создает. Иначе не работает
{
   flagButton = 1;
}

//обработка нажатия кнопки - с учётом дребезга
void buttonLight()
{
   if(flagButton) //если нажата кнопка
   {
      portLight = 0;   //включаем светодиод
      delay_ms(500);
      portLight = 1;   //выключаем светодиод
      flagButton = 0;
   }
}

void main()
{
   //инициализация всех используемых портов
   ddrB =  0;
   portB = 0;
   ddrD =  0;
   portD = 0;
   //инициализация кнопки - на вход с подтяжкой
   portButton = 1;
   ddrButton = 0;
   //инициализация светодиодика, которая включается по 0 и нажатию кнопки - на выход и в 1
   portLight = 1;
   ddrLight = 1;

   //настраиваем внешние прерывания
   MCUCR.ISC00 = 0;  //прерывание генерируется по логическому 0 на INT0
   MCUCR.ISC01 = 0;
   GICR.INT0 = 1; //разрешаем внешнее прерывание INT0
   asm sei;//SREG.B7 = 1; //разрешаем прерывания в принципе (бит I); команды аналогичны
    while(1)
   {
      buttonLight();     
   }
}

Немного о синтаксисе. Функция прерывания написана так: void имя_функции() org IVT_ADDR_INT0.

Ключевое слово org указывает, что дальше будет идти адрес прерывания из даташита. У нас же есть название прерывания из библиотечки: набираем IVT и и дальше нажимаем Ctrl + Пробел (люблю я такие вещички ˆˆ). Ещё вместо слова org можно использовать iv, судя по справке компилятора.

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

А ещё может оказаться так, что обработка прерывания нам вообще не нужна - например, достаточно, что схема вышла из спящего режима. Но в этом случае все равно надо писать функцию прерывания, пусть даже пустую - так называемую "заглушку". В принципе, некоторые компиляторы автоматически пишут пустые функции для каждого прерывания, но это не наш случай - приходится делать ручками.

 

 

Автор - Moriam

Обсудить на форуме