Все написанные нами до сих пор программы выполнялись по жестко заданному алгоритму без возможности хоть как-то его скорректировать. При этом те или иные действия осуществлялись только в тот момент времени, когда до них доходила очередь. А если нам нужно на какие-нибудь события реагировать мгновенно, а не ждать, пока до них дойдет очередь? Или, например, нужно дождаться реакции от какого-либо периферийного модуля контроллера? Глупо было бы по всей программе ставить проверки в надежде поймать нужные нам события. Вот здесь-то нам и помогут прерывания. Само название их уже говорит за себя. Это специальные подпрограммы, не вызываемые явно из основной программы, а прерывающие ход ее выполнения при наступлении какого-либо события (например, изменение состояния входа, переполнение таймера, окончание цикла преобразования АЦП).
Как же так получается, что ни с того ни с сего выполняется какая-то подпрограмма, которую никто не вызывал, да, к тому же, и прерывающая в любом месте ход выполнения основной программы?
Суть тут вот в чем. В каждом контроллере определен изначально список событий, которые могут вызвать прерывание, причем в каждом контроллере этот список свой в зависимости от количества и функциональных возможностей периферийных модулей. События, вызывающие прерывания, могут быть как внешними (например, изменение состояния входа, прием байта по интерфейсу), так и внутренними (например, переполнение таймера, окончание цикла записи АЦП). Рассмотрим такой список применительно к нашему контроллеру ATtiny13, и посмотрим, какие прерывания нам могут пригодиться, а какие вряд ли. Вообще, список этот имеется в самом конце каждого файла с расширением inc, так что можно легко узнать, какие могут присутствовать прерывания у того или иного контроллера. Приведу здесь выписку из файла tn13def.inc с некоторыми пояснениями для тех, кто не понимает по-аглицки. Заодно и рассмотрим еще несколько особенностей языка ассемблера.
;**** Interrupt Vectors ****
.equ INT0addr =$001 ;External Interrupt0
.equ PCINT0addr =$002 ;Pin Change Interrupt0
.equ TIM0_OVF0addr =$003 ;Overflow0 Interrupt
.equ EE_RDYaddr =$004 ;EEPROM write complete
.equ ANA_COMPaddr =$005 ;Analog Comparator Interrupt
.equ TIM0_COMPAaddr =$006 ;Timer/Counter0 Compare Match A
.equ TIM0_COMPBaddr =$007 ;Timer/Counter0 Compare Match B
.equ WDTaddr =$008 ;Watchdog Timeout
.equ ADCaddr =$009 ;ADC Conversion Complete HandleИтак, заголовок гласит "
Interrupt Vectors", что означает "Векторы прерываний". Почему именно векторы, расскажу попозже.
Все последующие строки начинаются с одной и той же директивы equ. Она имеет следующий синтаксис. Первым аргументом ее является символическое имя. Любое, на вкус пользователя (типа объявления имени переменных или констант в языках высокого уровня). Далее ставится знак равенства, а за ним значение, присваиваемое указанному имени. По своему действию эта директива похожа на объявление константы в языках высокого уровня. Значение, присвоенное указанному имени, нельзя изменить в дальнейшем.
Еще одно новшество в синтаксисе - знак $ перед числом. Знак этот указывает, что последующее за ним число записано не в десятичной, а в шестнадцатиричной форме. Для указания этой же формы записи в языке ассемблера есть еще один префикс 0x. То есть записи $4E и 0x4E для ассемблера аналогичны и означают шестнадцатиричное число 4Е. Для записи двоичных чисел, как я упоминал уже ранее в одном из предыдущих шагов, используется префикс 0b (например, 0b000100101).
Но вернемся все же к прерываниям. Сообразительный читатель уже смог подсчитать, что контроллер ATtiny13 имеет 9 различных источников прерываний. Рассмотрим их вкратце. Более подробно о них я расскажу, когда до них дойдет очередь в наших программах.
1.
External Interrupt0 - Внешнее прерывание 0. Это прерывание генерируется, если произойдет одно из предустановленных изменений состояния входа, обозначенного, как int0. Если посмотреть на список выводов контроллера (второй шаг), то можно увидеть, что этот вход объединен с выводом РВ1. В зависимости от настроек это прерывание может генерироваться при изменении состояния входа РВ1 из "0" в "1", из "1" в "0", или на протяжении всего времени, пока на входе РВ1 будет "0".
2.
Pin Change Interrupt0 - Прерывание по изменению состояния выводов. В отличие от предыдущего, это прерывание может быть вызвано любым изменением состояния любого из выводов, обозначенного как PCINTx (где х может быть от 0 до 5). В настройках задается, какие именно из входов могут вызвать данное прерывание.
Рассмотренные прерывания относятся к внешним, так как реагируют на наступление событий извне. Все остальные прерывания - внутренние, они генерируются какими-либо периферийными модулями.
3.
Overflow Interrupt0 - Прерывание по переполнению таймера 0. В контроллере ATtiny13 присутствует всего один 8-битный таймер, называемый "таймер 0". О режимах его работы я расскажу чуть ниже, а пока лишь замечу, что данное прерывание возникает при переполнении счетчика таймера, то есть при изменении его значения от 255 к 0.
4.
EEPROM Write Complete - Прерывание по завершению записи в энергонезависимую память.
5. Analog Comparator Interrupt - Прерывание от аналогового компаратора. В принципе это прерывание тоже можно отнести к внешним. У контроллера ATtiny13, как и всех остальных, есть аналоговый компаратор, имеющий два входа - неинвертирующий (AIN0 - РВ0) и инвертирующий (AIN1 - РВ1). Когда величина напряжения на неинвертирующем входе становится больше напряжения на инвертирующем входе, генерируется данное прерывание.
6.
Timer/Counter0 Compare Match A - Прерывание по совпадению значения счетчика таймера 0 со значением, записанным в специальном регистре OCR0A.
7.
Timer/Counter0 Compare Match B - Прерывание по совпадению значения счетчика таймера 0 со значением, записанным в регистре OCR0B.
8.
Watchdog Timeout - Прерывание по срабатыванию сторожевого таймера. Вообще сторожевой таймер - это довольно интересная штука. Она служит для отслеживания зависания программы и автоматического сброса контроллера при его обнаружении. При этом генерируется вот такое прерывание, чтоб пользователь мог знать, что же именно вызвало рестарт программы.
9.
ADC Convertion Complete Handle - Прерывание по окончанию цикла преобразования АЦП. Модуль АПЦ, используемый в контроллерах, преобразует аналоговый сигнал в цифровой код не мгновенно, на это ему требуется некоторое время. Так вот по окончании этого самого времени и генерируется данное прерывание.
Наиболее часто по своему опыту я использовал прерывания 1, 2, 3 и 6. Прерывания 4, 5, 7 и 8 не довелось применять ни разу, возможно, ввиду специфики разрабатываемых устройств.
Теперь рассмотрим, каким же образом работает сам механизм прерываний в микроконтроллере.
Мы уже договорились, что прерывания не вызываются из самой программы, а генерируются при наступлении определенных событий. Но обработчики прерываний-то нужно писать нам, и именно в тексте программы. Как же сказать контроллеру, что написанный нами кусок кода должен выполняться именно при наступлении какого-либо прерывания? Именно для этого и служит так называемая таблица векторов прерываний. Эта таблица располагается в начале памяти программ, причем номер прерывания соответствует адресу, по которому должен располагаться вызов подпрограммы обработки прерывания.
Попробую объяснить попроще на конкретном примере. Допустим, мы задумали выполнять какое-либо действие по переполнению счетчика таймера 0. По вышеприведенной таблице находим, что данное прерывание находится под номером 3. Это значит, что в памяти программ в ячейке с адресом 3 должна находиться команда перехода на подпрограмму, которая будет выполняться при наступлении данного прерывания. Обычно используется команда безусловного перехода rjmp. Каким же образом указать контроллеру, что мы хотим поместить данную команду именно по этому адресу?
Для этого используется еще одна директива org. Ее аргументом является число, соответствующее адресу, с которого последующая команда будет занесена в память контроллера.
Мы тут уже достаточно много понаписывали, давайте теперь разберем конкретную задачу. Задание оставим то же, что и в третьем шаге. Заставим мигать светодиод LED2, но теперь с использованием не задержек, а прерывания от таймера 0. Текст программы представлен ниже.
.include "F:\Prog\AVR\asm\Appnotes\tn13def.inc"
.org 0 ;Задание нулевого адреса старта программы
rjmp reset ;Безусловный переход к метке reset
.org 3 ;Задание адреса прерывания по переполнению таймера 0
rjmp timer0_ovf ;Безусловный переход к метке timer0_ovf
reset:
ldi r16, RAMEND ;Загрузка в регистр r16 адреса верхней границы ОЗУ
out SPL, r16 ;Копирование значения из r16 в регистр указателя стека SPL
ldi r16, (1<<TOIE0) ;Загрузка в регистр r16 "1", смещенной на TOIE0
out TIMSK0,r16 ;Копирование значения из регистра r16 в регистр TIMSK0
ldi r16, (1<<CS00)|(1<<CS02);Загрузка двух "1", смещенных на CS00 и CS02
out TCCR0B,r16 ;Копирование значения из регистра r16 в регистр TCCR0B
ldi r17,(1<<4) ;Загрузка в регистр r17 "1", смещенной на 4 разр. влево
out DDRB, r17 ;Копирование из r17 в DDRB (РВ4 - выход)
clr r16 ;Очистка регистра r16
sei ;Глобальное разрешение прерываний
main: ;Основной цикл программы
rjmp main ;Вернуться к метке main
timer0_ovf: ;Прерывание по переполнению таймера 0
eor r16,r17 ;Исключающее ИЛИ регистров r16 и r17
out PORTB,r16 ;Копирование из r16 в PORTB
reti ;Возврат из прерыванияКак видите, объем наших программ все более возрастает, а количество неизвестных команд все более уменьшается. Как обычно, рассмотрим не встречавшиеся нам ранее.
Команда clr имеет один операнд - РОН. Результатом ее выполнения является очистка указанного РОН, то есть запись во все его биты значения "0". По своему действию аналогична записи "ldi r16, 0".
Команда sei не имеет операндов. Она разрешает включение механизма обработки прерываний в микроконтроллере. По умолчанию этот механизм отключен, и контроллеру совершенно одинаково, по какому адресу располагается какая команда. Если же его включить, то активируется таблица векторов прерываний, и теперь при наступлении какого-либо прерывания будет выполняться обращение к соответствующему адресу.
Команда reti также не имеет операндов. Этак команда аналогична команде ret по своему назначению, только командой ret завершалась обычная подпрограмма, а reti завершает подпрограмму обработки прерывания. В чем же отличие между ними? Дело в том, что при наступлении прерывания в статусном регистре SREG (о котором я уже упоминал ранее) сбрасывается флаг, разрешающий прерывания. Таким образом, пока происходит обработка уже произошедшего прерывания, запрещается наступление новых прерываний. Так вот, команда reti, помимо восстановления счетчика программ из стека, снова устанавливает флаг разрешения прерываний. Так все хитро тут устроено.
Теперь рассмотрим работу каждой строки.
1 строка. Подключение файла tn13def.inc к нашему ассемблерному файлу.
2 строка. Директивой org задается нулевой адрес в памяти программ, по которому будет располагаться следующая команда.
3 строка. Командой rjmp осуществляется переход на метку reset.
Зачем это нужно? На самом деле во всех контроллерах присутствует еще одно прерывание, которое не требует отдельного разрешения и выполняется всегда после выхода контроллера из состояния сброса. Вектор обработки этого прерывания находится как раз по нулевому адресу, поэтому именно с этого адреса мы даем команду безусловного перехода к основной части программы. Ранее мы этого не делали, потому что и так всегда первой выполнялась команда, стоящая первой по тексту. Но теперь в условиях задействования прерываний нам придется каждый раз поступать именно так.
4 строка. Директивой org задается адрес 3. Если посмотреть на таблицу векторов прерываний, то можно видеть, что по этому адресу находится прерывание по переполнению таймера 0.
5 строка. Командой rjmp осуществляется переход на метку timer0_ovf. Именно с этой метки в нашей программе начинается обработчик прерывания по переполнению таймера 0.
Таким образом, в результате выполнения строк 4 и 5, контроллер будет знать, в какую часть программы ему переходить при переполнении таймера 0.
6 строка. Пустая для отделения таблицы векторов прерываний от основной программы
7 строка. Метка reset. Отсюда будет стартовать наша программа при включении контроллера.
8 строка. Командой ldi в регистр r16 загружается адрес верхней границы ОЗУ.
9 строка. Копирование содержимого регистра r16 в регистр указателя стека SPL
10 строка. Здесь мы встречаем уже знакомую нам по предыдущему шагу конструкцию загрузки в регистр единицы, смещенной влево. Только вот величина смещения, названная TOIE0, выглядит несколько непонятно. Поясню ее чуть позднее, наберитесь терпения.
11 строка. Копирование содержимого r16 в опять же неизвестный нам регистр с именем TIMSK0.
Итак, после выполнения строк 10 и 11 в неизвестном нам РВВ TIMSK0 оказывается единица в бите с опять же неизвестным нам названием TOIE0. Начнем разбираться, что оно такое.
TIMSK0 - это регистр ввода-вывода, который содержит в себе биты, разрешающие или запрещающие различные прерывания от таймера 0. Тут мы подходим к одному важному моменту. Даже если мы задействовали механизм прерываний в контроллере, не все возможные источники прерываний смогут их вызвать. Помимо глобального разрешения всех прерываний нужно еще индивидуально разрешать те прерывания, которые нам необходимы. Поскольку в программе мы решили использовать только одно прерывание по переполнению таймера 0, то только его мы и разрешим, а все остальные оставим неактивными. Так вот, к чему я веду... Установка в единицу бита TOIE0 регистра TIMSK0 как раз и разрешает именно это прерывание. Кроме того, с помощью регистра TIMSK0 можно установить и остальные прерывания, связанные с таймером 0: бит OCIE0A разрешает прерывание по совпадению с регистром OCR0A, а бита OCIE0B - прерывание по совпадению с регистром OCR0B.
Подытожим. В результате выполнения строк 10-11 разрешается прерывание по переполнению таймера 0.
12 строка. Снова имеем знакомую конструкцию сдвига, но теперь аж двух единиц. При этом одна единица смещается на бит CS00, а вторая - на бит CS02, и между полученными значениями выполняется операция побитового ИЛИ "|", что равносильно сложению в обычной алгебре. Таким образом, имеем в регистре r16 не одну, а две единицы, каждую в нужном месте.
13 строка. Копирование содержимого регистра r16 в регистр TCCR0B.
Теперь снова разъяснение смысла того, что мы сделали. Таймер 0 может работать в различных режимах и иметь разную частоту тактирования. Для его настройки служат два регистра TCCR0A и TCCR0B. Сейчас я не буду расписывать что для чего служит. По мере необходимости я буду снабжать вас новыми знаниями. Пока же нам нужно знать только одно - как задать частоту работы таймера. Для этого имеется так называемый предделитель. Коэффициент деления предделителя задается тремя битами CS00, CS01 и CS02, расположенными в регистре TCCR0B. Зависимость его от значений этих битов представлена в таблице:
Делитель | CS02 | CS01
| CS00 |
1 | 0 | 0
| 1
|
8 | 0 | 1
| 0
|
64 | 0 | 1
| 1
|
256 | 1 | 0
| 0
|
1024 | 1 | 0
| 1
|
Поскольку мы установили в единицу биты CS02 и CS00, то тактовая частота контроллера будет разделена на 1024. Попробуем теперь посчитать частоту мигания светодиода.
Тактовая частота контроллера равно 1 МГц, делитель частоты для таймера равен 1024, тогда тактовая частота таймера будет составлять 1000000/1024 = 977 Гц. Это значит, что 977 раз в секунду будет происходить наращивание счетчика таймера на 1. Переполнение наступает при достижении счетчиком значения 255, при этом он сбрасывается в 0. Значит, прерывание будет генерироваться 977 / 256 = 3,8 раза в секунду. Поскольку при каждом генерировании прерывания светодиод изменяет свое состояние на противоположное, то полная частота мигания составит 3,8 / 2 = 1,9 Гц. То есть светодиод будет мигать приблизительно 2 раза в секунду.
Если нужна большая частота, то можно уменьшить множитель. А вот для уменьшения частоты нужно применять разные ухищрения, типа введения дополнительной инкрементируемой переменной. Может быть, если будет настроение, поведаю об этом в одном из последующих шагов. А пока вернемся к дальнейшему рассмотрению программы.
Подытожим еще раз. В строках 12-13 мы задали тактовую частоту для таймера 0, равную 1/1024 частоты процессора.
Таким образом, в строках 10-13 мы настроили таймер 0 и разрешили прерывание по его переполнению.
14 строка. Загрузка в регистр r17 единицы, смещенной на 4 разряда влево.
15 строка. Копирование регистра r17 в регистр DDRB для инициализации вывода РВ4 как цифрового выхода.
16 строка. Очистка регистра r16. Регистр этот будет использоваться для переключения светодиода LED2, и в принципе его можно было бы не очищать, поскольку на выход настроен всего один вывод. Но тогда бы происходило постоянное включение-выключение подтягивающих резисторов на тех входах, где осталась бы "1" в регистре r16, а это уже не комильфо. Так что очищаем, это правила хорошего тона в программировании.
17 строка. Команда sei разрешает глобально механизм обработки прерываний. Даже если мы уже разрешили прерывание, задав бит TOIE0 в регистре TIMSK0, оно все равно останется неактивным, пока не будет установлен флаг прерываний в регистре SREG командой sei.
18 строка. Пустая для отделения блока инициализации от основного цикла программы
19 строка. Метка main - начало основного цикла программы
20 строка. Команда безусловного перехода к метке main. Окончание основного цикла программы.
"Как же так?" - спросит удивленный читатель - "Неужели в главном цикле ничего не делается?!". Да, ничего. Именно так должна выглядеть правильно составленная программа для контроллера. Сначала инициализация всех необходимых модулей, а затем пустой цикл с прерываниями. Все нужные действия будут выполняться по мере возникновения тех или иных событий, в нашем случае - при переполнении счетчика таймера 0.
21 строка. Пустая для отделения основного цикла программы от обработчика прерывания.
22 строка. Метка timer0_ovf - начало обработчика прерывания по переполнению таймера 0. Именно сюда нас отправляла команда безусловного перехода в пятой строке.
23 строка. Уже знакомая нам операция исключающего ИЛИ между регистрами r16 и r17. Поскольку в регистр r17 мы в строке 14 загрузили единицу только в четвертый разряд, то в регистре r16 будет переключаться именно этот бит, а он, как мы помним, соответствует выводу РВ4, на котором находится светодиод LED2.
24 строка. Копирование содержимого r16 в PORTB для непосредственного управления светодиодом LED2.
25 строка. Команда reti возвращает нас из прерывания снова к бесконечному циклу, который будет крутиться в ожидании следующего прерывания.
Вот, собственно, и все, на этот раз. Я стал замечать, что с каждым разом статьи мои становятся все больше и пространее. Но что поделать: сложность материала тоже растет.
В заключение традиционные уже задания для самостоятельного выполнения.
1. Изменить программу таким образом, чтобы мигал не только светодиод LED2, но и LED1, притом, чтобы мигание осуществлялось в противофазе. Но с одним условием. Объем hex-файла не должен измениться. Думайте, данных для выполнения этого задания вам достаточно.
2. Попытайтесь изменить программу так, чтобы светодиод мигал с частотой 0,5 Гц. Я уже говорил, что для этого придется использовать некоторые ухищрения, но, в принципе, вы тоже можете сообразить, как это сделать.
3. Добавить в программу обработку кнопок SB1 и SB2. При нажатии кнопки SB1 должно начинаться мигание светодиода LED2, а при нажатии кнопки SB2 - прекращаться. Дам небольшую подсказку. Обработку нажатия кнопок можно осуществлять в основном цикле программы, а начало или прекращение мигания производить путем установки или очистки бита TOIE0 в регистре TIMSK0.
Ну вот теперь уж точно все!
Если у вас возникнут вопросы, задавайте их на форуме или здесь в виде комментариев к статье.
Желаю успехов!