8. Что такое прерывания в микроконтроллере AVR

При программировании микроконтроллеров встречается такое понятие как прерывание, вектор прерывания, обработчик прерывания. Что же это такое? Уже само слово "прерывание" намекает, что что то должно прерываться. Но что? В случае с МК прерывается выполнение главного цикла программы который выглядит так:

int main(void)
{
    while(1)//это главный замкнутый цикл
    {
		
		//любой код между этими скобками и есть
        //главный цикл микроконтроллера
    }
}

При подаче питания, МК  заходит в цикл while()  и вечно там крутится пока есть питание. И оттуда он не выходит, пока не произойдет что либо, например сработает прерывание. Чтобы легче понять, приведем пример. Предположим вы моете посуду и вдруг вам позвонили в двери, какова ваша реакция? Откладываем мытье посуды (выходим из цикла), идем к двери (заходим в прерывание), разбираемся в чем там дело (выполнение кода в прерывании), уходим от двери обратно к мытью посуды (вышли из прерывания обратно в главный цикл, туда откуда вернулись). Моем посуду дальше, вдруг звонит телефон. Опять бросаем мытье посуды и идем к телефону (выходим из главного цикла и идем в прерывание, на этот раз это уже другое прерывание), смотрим кто звонит и решаем вопросы с ним (выполнили код в прерывании), решили вопросы, возвращаемся к посуде (вышли из прерывания и вернулись в главный цикл обратно туда откуда вышли). Вот как то так и работают прерывания в микроконтроллере.

Вообще в МК прерываний много, например в Atmega32 их аж 21 штука.

И все эти прерывания важны и нужны при написании программ. Используются они по необходимости, в зависимости от того что мы делаем или хотим сделать.

Вообще чтобы начать использовать прерывания в программе, надо сперва подключить библиотеку <avr/interrupt.h>

#include <avr/io.h>
#include <avr/interrupt.h>//Библиотека для работы с прерываниями

int main(void)
{
    while(1)
    {
		
    }
}

Далее  чтобы прерывание начало работать его надо включить. А включаются прерывания в определенных регистрах определенными битами. Но даже после этих действий включенное прерывание еще не будет работать. Во всех моделях МК AVR есть специальный такой бит, называется он "глобальное разрешение работы всех прерываний" - или по другому флаг I находится он в регистре SREG. Если этот бит I не включить, то даже если мы включили какое либо прерывание оно еще не будет работать. Этот глобальный бит I включается командой sei(), а выключается командой cli().

Сейчас рассмотрим какой нибудь конкретный пример, это INT0, INT1. Называются они External Interrupt или внешние прерывания. Каждое из этих прерываний закреплено к конкретному выводу МК. В нашем случае это выводы PD2(INT0), PD3(INT1). Для работы с этими прерываниями существуют специальные регистры. Вот они:

MCUCR=(0<<ISC11)|(0<<ISC10)|(0<<ISC01)|(0<<ISC00);
 /*
--------------------------------------------------------------------
 ISC11        ISC10               Условие
--------------------------------------------------------------------
   0            0          по НИЗКОМУ уровню
   0            1          резерв
   1            0          по спадающему фронту на выводе
   1            1          по нарастающему фронту на выводе
--------------------------------------------------------------------
--------------------------------------------------------------------
 ISC01        ISC00               Условие
--------------------------------------------------------------------
   0            0          по НИЗКОМУ уровню
   0            1          резерв
   1            0          по спадающему фронту на выводе
   1            1          по нарастающему фронту на выводе
------------------------------------------------------------------*/
 
 GICR=(0<<INT1)|(0<<INT0);
 /*
INT1 разрешение на прерывание c INT1
INT0 разрешение на прерывание с INT0

GIFR - регистр флагов прерывания
	INTF1 - прерывание INT1         
	INTF0 - прерывание INT1 */

Всего три регистра MCUCR, GICR и GIFR. Битами в MCUSR настраиваем как эти прерывания будут работать, по каким условиям. Есть три варианта:

1. По низкому уровню - это когда кнопка нажата и на выводе 0 вольт.
2. По спадающему фронту на выводе - это когда напряжение на выводе начинает уменьшаться с 5 вольт до 0 вольт. И в какой то определенный момент времени при понижении напряжения сработает прерывание.
3. По нарастающему фронту на выводе - это когда напряжение на выводе начинает увеличиваться с 0 вольт до 5 вольт. И в какой то определенный момент времени при повышении напряжения сработает прерывание.

В регистре GICR мы разрешаем или запрещаем работу прерываний. Регистр GIFR это регистр флагов. О нем чуть позже.
Теперь соберем такую схему и напишем программу.

При нажатии на кнопку должно сработать прерывание и включить светодиод, при следующем нажатии кнопки прерывание снова сработает и уже выключит светодиод. Код получится таким:

#include <avr/io.h>
#include <avr/interrupt.h>

//Битовые макросы
#define ClearBit(reg, bit)       reg &= (~(1<<(bit)))
#define SetBit(reg, bit)          reg |= (1<<(bit))
#define BitIsClear(reg, bit)    ((reg & (1<<(bit))) == 0)
#define BitIsSet(reg, bit)       ((reg & (1<<(bit))) != 0)

//Макроопределения

#define LED PC4
#define PORT_LED PORTC
#define DDR_LED DDRC

#define BUTTON PD2
#define PORT_BUTTON PORTD
#define PIN_BUTTON PIND
#define DDR_BUTTON DDRD

ISR(INT0_vect)//А это и есть наш прерывание INT0 (обработчик прерывания)
{
		
}

int main(void)
{
	
	DDR_LED = 1<<LED;//Вывод PC4 будет выходом
	PORT_LED= 0<<LED;//Выключим выход.

    DDR_BUTTON = 0<<BUTTON;  //Вывод PD2 будет входом
	PORT_BUTTON= 1<<BUTTON;  //Включаем подтяжку питания МК к входу PD4
	
	
	MCUCR=(0<<ISC11)|(0<<ISC10)|(1<<ISC01)|(0<<ISC00);//Настроим прерывание INT0 по спадающему уровню.
    GICR=(0<<INT1)|(1<<INT0);//Разрешили работу только прерывания INT0.

	sei();//Включили глобальный флаг резрешения прерываний
	
    while(1)
    {
	
    }
}

Как видим появился обработчик прерывания

	ISR(INT0_vect)//А это и есть наш прерывание INT0 (обработчик прерывания)
	{
		//здесь пишем код
	}

ISR именно так он и начинается, а какой именно это обработчик прерывания указыватся в скобках ISR(INT0_vect). Таким образом оформляется прерывание в AtmelStudio, в других IDE это может выглядеть по другому. А если надо оформить другое прерывание как узнать что писать в скобках? При создании проекта в AtmelStudio автоматически подключаются различные библиотечные файлы, в одном из них и указаны имена прерываний (или векторов прерываний, что одно и тоже). В нашем случае это файл iom32.h.

Открываем файл и смотрим названия векторов прерывания
Как видим есть три варианта оформления обработчика. INT2_vect_num, INT2_vect или  SIG_INTERRUPT2 выбираем любой по желанию. Теперь осталось только добавить нужный нам код в обработчик прерывания.

#include <avr/io.h>
#include <avr/interrupt.h>

//Битовые макросы
#define ClearBit(reg, bit)       reg &= (~(1<<(bit)))
#define SetBit(reg, bit)          reg |= (1<<(bit))
#define BitIsClear(reg, bit)    ((reg & (1<<(bit))) == 0)
#define BitIsSet(reg, bit)       ((reg & (1<<(bit))) != 0)

//Макроопределения
#define LED PC4
#define PORT_LED PORTC
#define DDR_LED DDRC

#define BUTTON PD2
#define PORT_BUTTON PORTD
#define PIN_BUTTON PIND
#define DDR_BUTTON DDRD

ISR(INT0_vect)//А это и есть наш прерывание INT0 (обработчик прерывания)
	{
		InvBit(PORT_LED,LED);//каждый раз когда мы здесь, инвертируем выход.
	}


int main(void)
{
	DDR_LED = 1<<LED;//Вывод PC4 будет выходом
	PORT_LED= 0<<LED;//Выключим выход.

    DDR_BUTTON = 0<<BUTTON;  //Вывод PD2 будет входом
	PORT_BUTTON= 1<<BUTTON;  //Включаем подтяжку питания МК к входу PD4
	
	MCUCR=(0<<ISC11)|(0<<ISC10)|(1<<ISC01)|(0<<ISC00);//Настроим прерывание INT0 по спадающему уровню.
    GICR=(0<<INT1)|(1<<INT0);//Разрешили работу только прерывания INT0.

	sei();//Включили глобальный флаг резрешения прерываний
	
    while(1)
    {
		
    }
}

Залив программу в плату AtmegaBoard32 можно увидеть что программа работает как и задуманно.

Теперь обсудим как это работает. При понижении напряжения на выводе INT0, при определенном значении срабатывает флаг INTF0 в регистре GIFR. Данный флаг устанавливается в 1. При этом в главном цикле программы завершаетя выполнение последней (ассемблерной) команды и МК выходит из главного цикла while() и прыгает в обработчик. При этом флаг INTF0 сам аппаратно сбрасывается в 0, глобальный флаг разрешения прерываний тоже сбрасывается. Далее выполняется код внутри обработчика. Последняя команда в обработчике RETI включает обратно глобальный флаг разрешения прерываний и МК возвращается обратно в главный цикл. Для тех кто пишет на СИ ассемблерные команды скрыты, но они есть и их можно увидеть в дизассемблинге программы AtmelStudio.

Как видим обработчик  разворачивается в ассемблерный код состоящий из 18-ти команд. Точка входа в прерывание это команды PUSH R1 и PUSH R0, этими командами в стек прячутся значения рабочих регистров R1 и R0. В конце прерывания значения этих регистров восстанавливаются обратно командой POP и напоследок команда RETI выбрасывает программный счетчик МК обратно в главный цикл.

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

1. Сами по себе прерывания прерывают основной цикл программы, что уже само по себе не очень хорошо. Ведь бывают такие места в главном цикле которые никак нельзя прерывать другими действиями. Заранее зная это необходимо перед таким кодом отключить прерывания cli(), а потом когда код будет выполнен обратно включить sei().

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

3. Атомарный доступ. Это значит неделимая операция. Предположим МК обрабатывает двухбайтную переменную считывает ее значение, при этом в прерывании значение этой двухбайтной переменной увеличивается на 100. Чтение двухбайтной переменной происходит в несколько команд и если вдруг в это время вклиниться прерывание и изменит нашу переменную, то по возвращении в главный цикл в нашей переменной будет уже другое значение. Поэтому как уже говорилось выше, такие критичные места обрамляются командами CLI и SEI, либо отключается только то прерывание, которое может изменить переменную в неподходящее время.

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

Возможно скажете вы смысл было использовать прерывание ради кнопки и светодиода? В целом да это можно было сделать и проще, но это был просто тестовый пример. А вот теперь вопрос, у вас есть МК и синусоидальный сигнал в розетке 220 вольт. И  МК нужно знать в какой момент времени, синусоида переходит через 0. Неважно зачем это нужно, нужно и все тут. Как это сделать? В следующей статье и обсудим как можно с помощью прерывания INT0 определить переход синусоиды через ноль, так называемый детектор нуля.