Сегодня будем разбирать понятие прерывания и как его использовать. Естественно не обойдется без учебной программы, но на этот раз моргать светодиодами не будем. Хорош уже. Сделаем подобие дверного звонка.
Задача:
заставить микроконтроллер по нажатию кнопки издавать звуковой сигнал.
Схема для нашего примера . Файлы проекта .
Создаем в старом workspace проект ring.
Задаем настройки проекта для конфигурации Release:
Выбираем тип микроконтроллера.
General Options > Target > Processor configuration
У меня это ATmega8535.
Разрешаем использование имен битов определенных в хидер файле
В General Options > System ставим галочку Enable bit definitions in I/O-Include files
До сих пор мы не пользовались именами битов, но сегодня они нам понадобятся.
Меняем тип выходного файла.
Linker > Output.
B поле Output file cтавим галочку Override default и заменяем расширение d90 на hex
В поле Format выбираем Other и в выпадающем меню Output format выбираем тип файла intel-standart
Сохраняем проект и workspace.
______________________________ Прерывание ___________________________
Представьте себе ситуацию. Вы сидите на работе и корпите над очередной микроконтроллерной програмулиной. Подходит к вам начальник и говорит: “Слушай, Паш, нам осциллографы в отдел закупили - Tektronix, четырехканальные. Помоги Васе притащить их”. Вы думаете: ”Ну, е-мое, только мысль поперла.. и на тебе”. А начальник так смотрит на вас, а глаза у него такие добрые, такие добрые. Как тут ему откажешь. Ну, вы все бросаете и идете с другом за осциллографами. Притащили. Отчитались. И снова сели за свою программу. Вот примерно так и выглядит механизм прерывания.
Довольно просто, но есть ряд принципиальных моментов.
Во-первых:
- вы делали свою работу
- параллельно кто-то покупал осциллографы
- по наступлению события «осциллографы закупили» - вы прерываете выполнение своей работы
- некоторое время вы занимаетесь другой работой – тащите осциллографы
- потом вы возвращаетесь на рабочее место и продолжаете делать свою работу с того места, на котором остановились
Во-вторых:
- вы вполне могли бы послать начальника и никуда не идти
- уйдя за осциллографами, вы могли задержаться там надолго, а то и вовсе не вернуться
- вернувшись на рабочее место, вы могли бы уже позабыть свои гениальные идеи
Все это очень похоже на то, что происходит в микроконтроллере. Микроконтроллеры AVR имеют в своем составе целую тучу периферийных устройств (таймеры/счетчики, аналого-цифровой преобразователь, аналоговый компаратор, асинхронный приемопередатчик…и т.д). Мощь микроконтроллера в том, что все эти устройства могут работать параллельно и независимо друг от друга, а также параллельно выполняемой программе. Каждое периферийное устройство может вызывать прерывание по наступлению определенного события. Прерывание будет происходить только в том случае, если оно разрешено. Разрешение прерываний устанавливается для каждого устройства отдельно. Помимо этого есть флаг глобального разрешения/запрещения всех прерываний – это I флаг в регистре SREG. При возникновении прерывания микроконтроллер сохраняет содержимое счетчика команд PC в стеке, то есть запоминает место, на котором его прервали. Загружает в счетчик команд адрес соответствующего вектора прерывания и переходит на этот адрес. Попадает на команду безусловного перехода по которой переходит на подпрограмму обработки прерывания. Запрещает прерывания сбросом флага I, выполняет подпрограмму. Выполнив подпрограмму обработки прерывания, микроконтроллер разрешает прерывания, устанавливая флаг I, и восстанавливает содержимое счетчика команд, то есть возвращается на то же место программы, на котором его прервали.
По идее, обработчик прерывания не должен повреждать содержимое регистров микроконтроллера, потому что они могут содержать данные программы выполняемой в тот момент. Для этого в начале подпрограммы обработки прерывания содержимое регистров микроконтроллера сохраняют в стеке, а в конце подпрограммы восстанавливают. Таким образом, выйдя из прерывания, микроконтроллер сможет продолжить выполнение программы, как ни в чем не бывало. Программируя на ассемблере, сохранение регистров прописывает сам программист, на Си – это делает компилятор.
_______________________________________________________________
Теперь поговорим о таймере. ATmega8535 имеет на борту три таймера/счетчика - два восьмиразрядных (T0, T2) и один шестнадцатиразрядный (T1). Мы будем использовать восьмиразрядный таймер/счетчик T0. В состав этого таймера входят три регистра - регистр управления TCCR0, счетный регистр TCNT0 и регистр сравнения OCR0. Когда таймер запущен, счетный регистр TCNT0 увеличивает свое значение на единицу на каждый перепад тактового сигнала. Частота тактового сигнала выбирается из нескольких возможных значений в регистре управления TCCR0. Также с помощью этого регистра устанавливается режим работы таймера. Таймер T0 может вызвать прерывание по наступлению события “переполнение” – это когда переполняется счетный регистр TCNT0 и по наступлению события “совпадение” – это когда значение счетного регистра TCNT0 становится равным значению регистра сравнения OCR0. Флаги разрешающие эти прерывания находятся в регистре TIMSK.
Мы настроим таймер/счетчик Т0 так, чтобы он вызывал прерывание по событию “совпадение” с частотой 5 кГц. В функции обработчике будем инвертировать состояние вывода микроконтроллера, к которому подключен пьезодинамик. Таким образом, частота пищания пьезика будет равна 2,5 кГц. (Подключен именно пьезодинамик! Не перепутайте. У пьезодинамика сопротивление зависит от частоты и на 2,5 КГц оно обычно еденицы Ком, поэтому его можно подключать к выводу микроконтроллера напрямую, без ограничительного резистора).
Теперь о программе. Построчно писать программу уже не получится, поэтому я сразу приведу ее текст. Ниже мы последовательно разберем все ее строки, и все станет понятно. Макросы я намеренно не стал использовать, программа маленькая, не хочется ее загромождать.
int
main(void
) //настраиваем таймер Т0
//разрешаем прерывания
//основной цикл программы – опрос кнопки
//обработчик прерывания таймера Т0
Настройка портов
В нашей схеме к порту D подключена кнопка и пьезодинамик. Вывод, к которому подключена кнопка, нужно настроить на вход и включить подтягивающий резистор. Вывод, к которому подключен пьезодинамик, нужно настроить на выход. DDRD = (0< Настройка таймера
Режим работы таймера Т0 – СТС(сброс при совпадении), тактовый сигнал – clk/8. Отражаем это в регистре TCCR0 TCCR0 = (1< Обнуляем на всякий случай счетный регистр TCNT0 В регистр сравнения OCR0 записываем 0xc8. Почему? Потому что я посчитал это на калькуляторе
. Ну а на бумаге этот расчет выглядит так. Подробное описание таймера T0 смотрите в документации на ATMega8535. Таймер мы настроили, разрешаем общее прерывание, используя встроенную функцию. __enable_interrupt();
Опрос кнопки
Когда кнопка не нажата, вывод микроконтроллера через внутренний подтягивающий резистор подключен к питанию, то есть на выводе присутствует единичка, когда кнопка нажата, вывод замкнут на землю, то есть на выводе ноль. Чтобы определить нажата ли кнопка, нужно считать содержимое регистра PIND и проверить значение нулевого бита (к PD0 подключена кнопка). Опрашивать кнопку будем в бесконечном цикле. while
(1) Не забывайте == это не оператор присваивания =. Обработка нажатия/отпускания кнопки
По нажатию кнопки мы будем разрешать прерывание таймера T0, по отпусканию - запрещать. Для этого будем манипулировать битом OCIE0 регистра TIMSK TIMSK = (1< TIMSK = 0; //запрещаем прерывание
Поскольку мы используем всего один таймер, то нет нужды в установке или сбросе отдельных битов. Функция прерывания задается с помощью директивы #pragma
vector= и служебного слова __interrupt.
Функция должна иметь тип void и не должна принимать никаких параметров. #pragma
vector = Address Name
– имя функции, выбираем на наше усмотрение Для нашей задачи функция-обработчик прерывания выглядит так #pragma
vector = TIMER0_COMP_vect Ну вот собственно и все. Надеюсь все понятно. Частенько бывает, что микросхемка должна работать-работать себе спокойненько, а на какое-то событие бросать все дела и выполнять что-то иное. А потом - снова возвращаться к первоначальному делу... Это как в часах, например, - они показывают время до тех пор, пока не настанет время будильника. И вроде как никаких внешних воздействий - нажатия там кнопки, ресета - а микросхема сама переключается. Это и можно реализовать с помощью прерываний - сигналов, сообщающих процессору о наступлении какого-либо события. Вот пример из бытовой жизни - сидите Вы на кухне, пьете чай с малиновым вареньем и вкусняшками неприменно, и ждете гостей. А как узнать, что кто-то пришел? Тут два варианта: либо мы каждые пять минут будем отвлекаться от варенья, в смысле, чая и бегать проверять, а не стоит ли кто за дверью, либо купить дверной звонок и спокойненько ждать на нагретом месте, пока кто-нибудь в него не позвонит. Так вот, когда гость звонит - это событие. Соответственно, мы прерываемся и кидаемся к двери. Итак, у микросхемы есть прерывания. И не одно. Прерывания делятся на внешние - такие срабатывают при определённом напряжении на некоторых выводах микросхемы (INT0, INT1 и также иногда целый порт PCINT) - и внутренние - при переполнении счётчика, срабатывании сторжевого таймера, при использовании USART, при прерывании аналогового компаратора, АЦП и прочей периферии. Соответственно, возникает проблема приоритета. Это как мы все также сидим и пьем чай, но звонят нам уже не только в дверь, но и по телефону... И ведь не разорвешься, что-то нужно сделать первым. Поэтому в даташите есть таблица векторов прерываний. Чем меньше номер прерывания, тем более оно приоритетно. Здесь получается несколько тонкостей... Вот произошло событие - пошел запрос на прерывание, то есть выставляется так называемый "флаг запроса на прерывание". Если все хорошо, прерывание разрешено, то жизнь прекрасна и происходит его обработка. А вот если прерывание запрещено - например, уже происходит обработка более приоритетного прерывания - тогда этот флаг запроса так и остается висеть до момента разрешения прерываний. После этого чип проверяет регистр запроса в порядке приоритета, и если есть флаг - обрабатывает его. НО! Получается, что даже если прерывание обрабатывается, не факт, что событие, которое его вызвало, ещё живо... Это как позвонили в дверь и по телефону одновременно, Вы ответили по телефону, а гости уже решили, что никого дома нету и ушли. И вроде как событие - звонок в дверь - было, а за дверью никого нет. Ещё проблема - что пока обрабатывается другое прерывание и флаг запроса уже поднят, событие может произойти ещё несколько раз. Ответили на звонок по телефону, открываем дверь - а там уже целая куча гостей! Страшно? Страшно... Ещё одна особенность использования прерываний - да и не только прерываний: реентерабельность (или повторная входимость). Реентерабельная программа - та, которую могут вызвать несколько пользователей (или процессов), и при этом как минимум не возникнет ошибка, а как максимум - не будет потери вычислений - например, другому пользователю не придется выполнять код снова. Иными словами, если на кухне во то время, пока Вы встречаете гостей, никто не утащит себе вкусняшки, то значит все реентерабельно) В общем, серезная штука - если её не учитывать, можно долго мучится с "а чего же она не работает?!". Нужно её учитывать, например, если обрабатываются несколько прерываний, и каждое изменяет какую-то глобальную переменную... Обычно прерывания НЕ реентерабильны. То есть, в момент работы прерывания нельзя повторно вызвать это же прерывание.. Именно для защиты от повторного вхождения в обработчик прерывания автоматически запрещаются в момент его обработки (если вам захотелось разрешить прерывания в процедуре обработки прерываний, надо десять, двадцать раз подумать, прежде чем сделать такой опрометчивый шаг). Рассмотрим работу с внешними прерываниями: нам нужно, во-первых, настроить, по какому событию будет происходить прерывание, а, во-вторых, разрешить микросхеме вообще обрабатывать это самое прерывание. За первое в микросхеме ATmega8 отвечает регистр MCUCR - биты ISC11-ISC10, отвечающие за INT1, и ISC01-ISC00, отвечающие за INT0. Таблица 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, судя по справке компилятора. Ещё маленькое замечание: во-первых, прерывание никогда не должно быть громоздким - несколько строк и все. Это связано с тем, что во время обработки прерывания микросхема не может отвлечься на что-либо иное, азначит, если у нас прерываний несколько, то можно пропустить наступление какого-нибудь события. А ещё может оказаться так, что обработка прерывания нам вообще не нужна - например, достаточно, что схема вышла из спящего режима. Но в этом случае все равно надо писать функцию прерывания, пусть даже пустую - так называемую "заглушку". В принципе, некоторые компиляторы автоматически пишут пустые функции для каждого прерывания, но это не наш случай - приходится делать ручками. Прерывание
(interrupt) – событие, требующие немедленной реакции со стороны процессора. Реакция состоит в том, что процессор прерывает обработку текущей программы (прерываемой программы
) и переходит к выполнению некоторой другой программы (прерывающей программы
), специально предназначенной для данного события. По завершении этой программы процессор возвращается к выполнению прерванной программы. Каждое событие, требующее прерывания, сопровождается сигналом прерывания
, оповещающим об этом вычислительную машину, и называемым запросом прерывания
. Состояние программы
представляет собой совокупность состояний всех запоминающих элементов в соответствующий момент времени (например, после выполнения последней команды). При возникновении прерывания микроконтроллер сохраняет в стеке содержимое счетчика команд и загружает в него адрес соответствующего вектора прерывания. Последней командой подпрограммы обработки прерывания должна быть команда, которая осуществляет возврат в основную программу и восстановление предварительно сохраненного счетчика команд. Во время выполнения обработчика прерывания некоторая информация может подвергнуться изменению. Поэтому при переходе к обработчику прерывания необходимо сохранить элементы, подвергающиеся изменению. Набор таких элементов представляет собой вектор состояния программы
. При этом другая информация о состоянии ячеек памяти не существенна или может быть восстановлена программным путем. Вектор начального состояния
содержит всю необходимую информацию для начального запуска программы. Во многих случаях вектор начального состояния содержит только один элемент – начальный адрес запускаемой программы. Вектор прерывания
является вектором начального состояния прерывающей программы (обработчика) и содержит всю необходимую информацию для перехода к обработчику, в том числе его начальный адрес. Каждому типу прерываний соответствует свой вектор прерывания, который инициализирует выполнение соответствующего обработчика. Обычно векторы прерывания хранятся в специально выделенных фиксированных ячейках памяти с короткими адресами, представляющих собой таблицу векторов прерываний
. Для перехода к соответствующей прерывающей программе процессор должен располагать вектором прерывания и адресом этого вектора. По этому адресу, как правило, находится команда безусловного перехода к подпрограмме обработки прерывания. Как правило, управление запоминанием и возвратом возложено на обработчик прерывания. В этом случае обработчик состоит из трех частей – подготовительной (пролог
) и заключительной (эпилог
), обеспечивающих переключение программ, и собственно прерывающей программы, выполняющей затребованные запросом операции. Время реакции определяется как временной интервал от момента поступления запроса прерывания до начала выполнения прерывающей программы. При наличии нескольких источников запросов должен быть установлен определенный порядок обслуживания поступающих запросов, называемый приоритетными соотношениями
или дисциплиной обслуживания
. Совокупность всех возможных типов прерывания процессора представляет собой систему прерывания
микроконтроллера. Дисциплина обслуживания определяет, какой из нескольких запросов, поступивших одновременно, подлежит обработке в первую очередь, и имеет ли право данный запрос прерывать тот или иной обработчик прерывания. Если запрос прерывания окажется не обслуженным к моменту прихода нового запроса от того же источника (того же приоритета), то возникает насыщение системы прерываний
. При этом часть запросов прерывания будет утрачена, что для нормальной работы микроконтроллера недопустимо. Характеристиками системы прерывания
являются: Маскирование прерываний
используется для сообщения микроконтроллеру о необходимости реагировать на каждый тип прерывания или игнорировать его. Маска
прерывания представляет двоичный код, разряды которого поставлены в соответствие источникам запроса прерываний. Единичный бит в двоичном коде сообщает микроконтроллеру о необходимости обработки прерываний такого типа. Нулевой бит напротив не позволяет микроконтроллеру переходить к обработке прерываний указанного типа. Первым делом о том что такое прерывание. На этом покончим с теорией и перейдём к практике (хотя теории ещё будет ниже). Как вы уже наверное догадались мы напишем прерывание (которое генерируется кнопкой) которое будет зажигать и тушить диод.
#include Условимся, что прерывание (физически) не будет включать выключать питание на ноге контроллера (как это делается я уже рассматривал), а всего лишь будет изменять флаг. При определённых значениях которого и будет включаться и выключаться диод. Int num = 1;
Теперь объявим прерывание: ISR(SIG_INTERRUPT1){
if (num == 1)
num = 0;
else
num = 1;
}
Как видите в скобках макроса указан так называемый вектор прерывания. Этот вектор указывает компилятору для какого входа будет сгенерировано прерывание. Для INT1 - это SIG_INTERRUPT1. Для АЦП (ADC) например это - SIG_ADC. (весь перечень отлично описан в книге "Шпак Ю.А. Программирование на языке Си для AVR и PIC микроконтроллеров".) Sei(); // в целом
GIMSK |= (1< Когда это сделано нужно настроить поведение прерывания. Оно может быть сгенерировано по разному. Думаю как перевести это вы и так поймёте. MCUCR = (0< Теперь установим весь порт С как выход: DDRC = 0xff; // порт С - выход
Ну а это уже должно быть понятно: While (1){
if (num == 1)
PORTC |= 1; // включаем первый выход С
else
PORTC &= ~1; // выключаем первый выход С
_delay_ms(100); // ждём 100мс
}
Ждать не обязательно. Тем более что это уменьшает быстродействие. Но мне так хочется.
#define F_CPU 8000000UL // 8MHz
#include Компилируем hex и собираем схему в Proteus. Наслаждаемся работой прерывания при изменении положения кнопки. В состав AVR микроконтроллеров входит большое число периферийных устройств (ADC, Timer/Counters, EXTI, Analog Comparator, EEPROM, USART, SPI, I2C и т.д.), каждое из которых может выполнять определенные действия над данными/сигналами и пр. информацией. Эти устройства встроены в микроконтроллер для повышения эффективности приложения и снижения затрат при разработке всевозможных устройств на базе AVR микроконтроллеров.
Процессор общается/управляет периферийными устройствами посредством регистров ввода/вывода (I/O Registers), которые располагаются в памяти данных (Data Memory), что позволяет использовать их как обычные переменные. У каждого устройства имеются свои регистры ввода/вывода.
Все регистры ввода/вывода (I/O Registers) можно поделить на три группы: регистры данных, регистры управления и регистры состояния.
При помощи регистров управления (Control Registers) реализуется настройка устройства для работы в том или ином режиме, с определенной частотой, точностью и т.д., а при помощи регистров данных (Data Registers) считывается результат работы данного устройства (аналого-цифровое преобразование, принятые данные, значение таймера/счетчика и т.д.). Казалось бы, ничего сложного здесь нет (вообще-то здесь и вправду ничего сложного нет:)), включил устройство, указал желаемый режим работы а потом только остается стричь купоны читать готовенькие данные и использовать их в вычислениях. Весь вопрос заключается в том "когда” читать эти самые данные (завершило устройство работу или все еще обрабатывает данные), ведь все периферийные устройства работают параллельно с ядром микроконтроллера, да еще и на разных частотах. Встает вопрос реализации общения и синхронизации между процессором и периферийным устройством.
Как вы уже наверное догадались, для реализации общения и синхронизации между устройством и процессором используются "регистры состояния” (Status Registers), в которых хранится текущее состояние работы того или иного устройства. Каждому состоянию, в котором может находиться устройство, соответствует "бит в регистре состояния” (флаг), текущее значение которого, "говорит” о текущем состоянии данного устройства или его отдельно взятой функции (работа завершена/не завершена, ошибка при обработке данных, регистр пуст и т.д.).
Механизм общения, между процессором и периферийным устройством, реализуется путем опрашивания флагов (flag polling), отвечающих за ту или иную функцию данного устройства. В зависимости от значения того или иного флага (состояние устройства), можно менять ход исполнения программы (ветвление). К примеру:
Проверка если определенный флаг установлен (произошло некое событие) :
if
(RegX &
(1
<<
Flag)
)
// если флаг в регистре RegX установлен
Ожидание завершения какого либо действия (событие) :
while(!(RegX & (1< Опрашивание флагов – занятие довольно ресурсоемкое, как в плане размера программы, так и в плане быстродействия программы. Поскольку общее число флагов в AVR микроконтроллерах довольно велико (преимущество), то реализация общения, между процессором и устройством, путем опроса флагов приводит к снижению КПД (быстродействие кода/размер кода) написанной вами программы, к тому же программа становится очень запутанной, что способствует появлению ошибок, которые трудно обнаружить даже при детальной отладке кода.
Для того чтобы повысить КПД программ для AVR микроконтроллеров, а также облегчить процесс создания и отладки данных программ, разработчики снабдили все периферийные устройства "источниками прерываний” (Interrupt sources
), у некоторых устройств может быть несколько источников прерывания.
При помощи источников прерываний реализуется механизм синхронизации
, между процессором и периферийным устройством, то есть процессор начнет прием данных, опрос флагов и др. действия над периферийным устройством только тогда, когда устройство будет к этому готово (сообщит о завершении обработке данных, ошибке при обработке данных, регистр пуст, и т.д.), путем генерации "запроса на обработку прерывания” (Interrupt request
), в зависимости от значения некоторого флага (состояние устройства / функции / события).
В литературе, очень часто, всю цепочку событий, начиная от "запроса на обработку прерывания” (IRQ) и до "процедуры обработки прерывания” (ISR), сокращенно называют – прерывание (Interrupt
).
Что такое прерывание?
Прерывание (Interrupt) – сигнал, сообщающий процессору о наступлении какого-либо события. При этом выполнение текущей последовательности команд приостанавливается и управление передаётся процедуре обработки прерывания, соответствующая данному событию, после чего исполнение кода продолжается ровно с того места где он был прерван (возвращение управления). (Wiki)
Процедура обработки прерывания
(Interrupt Service Routine) – это ни что иное как функция/подпрограмма, которую следует выполнить при возникновении определенного события. Будем использовать именно слово "процедура”, для того чтобы подчеркнуть ее отличие от всех остальных функций.
Главное отличие процедуры от простых функций состоит в том что вместо обычного "возврата из функции” (ассемблерная команда RET), следует использовать "возврат из прерывания” (ассемблерная команда RETI) – "RETurn from Interrupt
".
Свойства AVR прерываний:
Прерывание Reset, в отличие от всех остальных, нельзя запретить. Такие прерывания еще называют Non-maskable interrupts.
Таблица векторов прерываний, кроме вектора Reset, может быть перемещена в начало Boot раздела Flash памяти, установив бит IVSEL в регистре GICR. Вектор сброса также может быть перемещен в начало Boot раздела Flash памяти, путем программирования фьюз бита – BOOTRST.
Рис.1 Таблица векторов прерываний ATmega16
Прототип процедуры обработки прерывания
Чтобы объявить некоторую функцию в качестве процедуры обработки того или иного прерывания, необходимо следовать определенным правилам прототипирования, чтобы компилятор/компоновщик смогли правильно определить и связать нужное вам прерывание с процедурой ее обработки.
Во первых процедура обработки прерывания не может ничего принимать в качестве аргумента (void), а также не может ничего возвращать (void). Это связано с тем что все прерывания в AVR асинхронные, поэтому не известно в каком месте будет прервано исполнение программы, у кого принимать и кому возвращать значение, а также для минимизации времени входа и выхода из прерывания.
void
isr(void
)
Во вторых, перед прототипом функции следует указать что она является процедурой обработки прерывания. Как вам известно, в языке Си исполняется только тот код что используется в функции main. Поскольку процедура обработки прерывания в функции main нигде не используется, то для того чтобы компилятор не "выкинул” ее за ненадобностью, перед прототипом процедуры следует указать что эта функция является процедурой обработки прерывания.
Прототип процедуры обработки прерывания в среде AVR Studio
#include ISR(XXX_vect)
В AVR Studio (AVR GCC), каждая процедура обработки прерывания начинается с макроопределения ISR, после чего, в круглых скобках следует конструкция:
XXX_vect
где "XXX” это имя вектора прерывания. Все имена векторов, для определенного AVR микроконтроллера, можно найти в "таблице векторов прерываний” даташита данного микроконтроллера или в его заголовочном файле. К примеру, "таблица векторов прерываний” для микроконтроллера ATmega16 приведена на рис.1, где в колонке Source, приведены все имена векторов прерываний. Также имена можно посмотреть в заголовочном файле данного микроконтроллера (C:\Program Files\Atmel\AVR Tools\AVR Toolchain\avr\include\avr\iom16.h), см. рис.2. Все что нам надо сделать, это найти в таблице имя нужного нам вектора и к нему прибавить суффикс "_vect". Рис.2 Заголовочный файл ATmega16 для AVR Studio Для примера, напишем процедуру обработки прерывания по приему байта через USART (USART, Rx Complete) : ISR(USART_RXC_vect)
Кстати: перед тем как использовать любое прерывание в AVR Studio, следует включить в проект заголовочные файлы io.h и interrupt.h:
#include Более подробно об обработчиках прерываний в AVR Studio (AVR GCC) можно почитать в разделе Introduction to avr-libc’s interrupt handling.
Прототип процедуры обработки прерывания в среде ImageCraft
#pragma interrupt_handler В среде ImageCraft, прототип процедуры обработки прерывания выглядит следующим образом:
void
<
handler_name>
(void
)
где #pragma interrupt_handler где Рис.3 Заголовочный файл ATmega16 для ImageCraft IDE К примеру процедура обработки прерывания по приему байта через USART (USART, Rx Complete) в среде ImageCraft, будет выглядит так: #pragma interrupt_handler usart_rxc_isr: iv_USART_RXC
Более подробно о процедурах обработки прерывания в ImageCraft IDE можно найти в меню Help->Programming the AVR->Interrupt Handlers среды разработки. Иногда, если несколько обработчиков прерывания должны делать одно и то же, то для экономии памяти программ, можно направить несколько векторов прерывания на одну и ту же процедуру обработки прерывания.
В среде AVR Studio это выглядит так: ISR(INT0_vect)
Сначала идет процедура обработки прерывания для определенного вектора, в данном случае INT0. Все остальные процедуры могут ссылаться на любой обработчик прерывания при помощи конструкции: ISR(YYY_vect,
ISR_ALIASOF(XXX_vect)
)
;
где YYY это имя вектора прерывания который ссылается на ранее объявленный обработчик прерывания для вектора XXX. В среде ImageCraft это выглядит так: #pragma interrupt_handler #pragma interrupt_handler где векторы XXX и YYY ссылаются на один и тот же обработчик прерывания Как работает прерывание в AVR микроконтроллерах?
1. Предположим произошел "запрос на обработку прерывания
” (IRQ). Кстати: если одновременно произойдут несколько запросов на обработку прерывания, то первым будет обработано прерывание с самым высоким приоритетом, все остальные запросы будут обработаны по завершению высокоприоритетного прерывания.
2. Проверка.
Если бит активации данного прерывания установлен (Interrupt enable bit), а также I-бит (бит всеобщего разрешения прерываний) регистра состояния процессора (SREG) установлен, то процессор начинает подготовку процедуры обработки прерывания, при этом бит всеобщего разрешения прерываний (I-бит регистра SREG) сбрасывается, запрещая таким образом все остальные прерывания. Это происходит для того чтобы никакое другое событие не смогло прервать обработку текущего прерывания. Кстати: если в процедуре обработки прерывания установить I-бит в состояние лог. единицы, то любое активированное прерывание может в свою очередь прервать обработку текущего прерывания. Такие прерывания называются вложенные (Nested interrupts).
3. Подготовка
. Процессор завершает выполнение текущей ассемблерной команды, после чего помещает адрес следующей команды в стек (PC->STACK). Далее процессор проверяет какой источник прерывания подал "запрос на обработку прерывания” (IRQ), после чего воспользовавшись вектором данного источника (ссылка) из таблицы векторов (который железно закреплен за каждым источником прерывания), переходит в процедуру обработки прерывания (инструкция JMP). На все, про все процессор тратит минимум 4 такта! (в зависимости от момента появления запроса и длительность исполнения текущей инструкции). Это очень хорошее время реакции на IRQ, по сравнению с микроконтроллерами других производителей. Кстати: если IRQ произойдет, когда микроконтроллер находится в спящем режиме (sleep mode), время реакции на IRQ увеличивается еще на четыре такта, плюс время заложенное в фьюз битах SUT1 и SUT0 (Start-Up Time).
{
//настраиваем порты ввода-вывода
DDRD = (0<
TCCR0 = (1<
OCR0 = 0xc8;
__enable_interrupt();
while
(1){
if
((PIND & (1<
TIMSK = 0;
}
return
0;
}
__interrupt
void
Timer0CompVect(void
)
{
PORTD ^= (1<
Тактовая частота микроконтроллера 8 МГц
Тактовый сигнал таймера равен 8000000 Гц/8 = 1000000 Гц.
Время одного такта таймера 1/1000000 = 1 мкс
Время одного такта нужной нам частоты 1/5000 Гц = 200 мкс
Сколько тактов таймера укладывается в 200 мкс? 200/1 = 200 тактов
200 в шестнадцатеричной системе = 0xс8
{
if
((PIND & (1<
}
else
{
//если нет - молчать как рыба
}
}
Функция прерывания
_____________________ Cинтаксис функции прерывания
_____________________
__interrupt
void
Name(void
)
{
//здесь располагается наш код
}
Address
– адрес вектора прерывания, можно задавать числом, можно именами определенными в заголовочном файле микроконтроллера (iom8535.h – раздел Interrupt Vector Definitions)______________________________________________________________
__interrupt
void
Timer0CompVect(void
)
{
PORTD ^= (1<
}
В следующий статье заставим микроконтроллер играть мелодию.
t p
– время реакции системы на прерывание;
t з
– время запоминания состояния прерываемой программы;
t ппр
– время собственно прерывающей программы;
t в
– время восстановления состояния прерванной программы
В случае если во время обработки прерывания поступает запрос на прерывание с более высоким уровнем приоритета, управление передается обработчику прерывания более высокого приоритета, при этом работа обработчика прерывания с более низким уровнем приоритета приостанавливается. Возникает вложенность прерываний
. Максимальное число программ, которые могут приостанавливать друг друга называется глубиной прерываний
.
Как правило, кроме маскирования прерываний, существует также бит глобального разрешения прерываний, нулевое значение которого отключает все обработчики прерываний (кроме аппаратного сброса и перехода к началу исполняемой программы).
Кроме двоичного кода маски прерываний существует также двоичный код флагов прерываний
, который позволяет обработчику прерываний установить источник возникновения прерывания в случае если источников с указанным запросом в микроконтроллере несколько.
Прерывание (interrupt) - это своеобразная функция, которая будет выполнена при поступлении сигнала на какой нибудь вход контроллера.
При работе в AVR Studio прерывания создаются при помощи макросов ISR()
, SIGNAL()
и INTERRUPT()
. Они помечают некоторую функцию как обработчик прерывания. Их различие в том, что INTERRUPT()
и ISR()
определяют функцию обработчик для случая, когда разрешено общее прерывание (обработчик может быть прерван), а SIGNAL()
для случая когда общее прерывание запрещено.
Соберём в ISIS такую схему:
Итак, откройте студию и создайте стандартный проект.
Для использования прерываний включим заголовочный файл:
Зададим этот флаг глобально:
Теперь перейдём к функции main нашей "программы".
Нам необходимо разрешить прерывания в целом и для INT1 в частности:
Скачиваем datasheet (ссылка есть при создании проекта) и находим в разделе прерывания (interrupt) такую таблицу:
Установим состояние генерации прерывания при каждом "логическом изменении на INT1".
Программа целиком:
{
// делай что-то
}
{
}
{
// Тело обработчика прерывания
}
#include
void
<
handler_name>
(void
)
{
// Тело обработчика прерывания
}
void
usart_rxc_isr(void
)
{
// Тело обработчика прерывания
}
{
// Do something
}
ISR(INT1_vect,
ISR_ALIASOF(INT0_vect)
)
;
void
<
handler_name>
(void
)
{
// Тело обработчика прерывания
}
#pragma interrupt_handler
void
<
handler_name>
(void
)
{
// Тело обработчика прерывания
}