Про Ардуино и не только

Содержание

Прерывания в Ардуино. Внешние прерывания, PCINT, WDT

Продолжаем тему использования прерываний в Ардуино. В предыдущей публикации мы познакомились с функциями среды Ардуино для работы с внешними прерываниями. Сегодня рассмотрим какие еще прерывания присутствуют в AVR микроконтроллерах и разберем несколько примеров их использования. А начнем мы с самого начала — с таблицы векторов прерываний.

Содержание

Таблица векторов прерываний

При появлении запроса на прерывание микроконтроллер приостанавливает выполнение основной программы и переходит к обработчику прерывания. Но как он находит этот самый обработчик? Для этого используется таблица векторов прерываний. Каждому прерыванию соответствует свой вектор, который по сути является командой перехода на функцию-обработчик. Располагается эта таблица как правило в младших адресах памяти программ: по нулевому адресу находится вектор сброса, далее идут вектора прерываний. Поскольку источниками прерываний служат внешние и периферийные устройства микроконтроллера, то их набор зависит от конкретной модели микроконтроллера. Для ATmega328/P таблица векторов имеет следующий вид:

№ Адрес Источник Описание Вектор
1 0x0000 RESET Вектор сброса
2 0x0002 INT0 Внешнее прерывание 0 INT0_vect
3 0x0004 INT1 Внешнее прерывание 1 INT1_vect
4 0x0006 PCINT0 Прерывание 0 по изменению состояния выводов PCINT0_vect
5 0x0008 PCINT1 Прерывание 1 по изменению состояния выводов PCINT1_vect
6 0x000A PCINT2 Прерывание 2 по изменению состояния выводов PCINT2_vect
7 0x000C WDT Таймаут сторожевого таймера WDT_vect
8 0x000E TIMER2_COMPA Совпадение A таймера/счетчика T2 TIMER2_COMPA_vect
9 0x0010 TIMER2_COMPB Совпадение B таймера/счетчика T2 TIMER2_COMPB_vect
10 0x0012 TIMER2_OVF Переполнение таймера/счетчика T2 TIMER2_OVF_vect
11 0x0014 TIMER1_CAPT Захват таймера/счетчика T1 TIMER1_CAPT_vect
12 0x0016 TIMER1_COMPA Совпадение A таймера/счетчика T1 TIMER1_COMPA_vect
13 0x0018 TIMER1_COMPB Совпадение B таймера/счетчика T1 TIMER1_COMPB_vect
14 0x001A TIMER1_OVF Переполнение таймера/счетчика T1 TIMER1_OVF_vect
15 0x001C TIMER0_COMPA Совпадение A таймера/счетчика T0 TIMER0_COMPA_vect
16 0x001E TIMER0_COMPB Совпадение B таймера/счетчика T0 TIMER0_COMPB_vect
17 0x0020 TIMER0_OVF Переполнение таймера/счетчика T0 TIMER0_OVF_vect
18 0x0022 SPI STC Передача по SPI завершена SPI_STC_vect
19 0x0024 USART_RX USART прием завершен USART_RX_vect
20 0x0026 USART_UDRE Регистр данных USART пуст USART_UDRE_vect
21 0x0028 USART_TX USART передача завершена USART_TX_vect
22 0x002A ADC Преобразование АЦП завершено ADC_vect
23 0x002C EE READY Готовность EEPROM EE_READY_vect
24 0x002E ANALOG COMP Прерывание от аналогового компаратора ANALOG_COMP_vect
25 0x0030 TWI Прерывание от модуля TWI (I2C) TWI_vect
26 0x0032 SPM READY Готовность SPM SPM_READY_vect

Положение вектора сброса определяется значением фьюза BOOTRST. Когда данный фьюз не запрограммирован (что является его состоянием по умолчанию), вектор сброса находится по адресу 0x0000, с него начинается выполнение программы. Если фьюз BOOTRST запрограммирован, то после сброса микроконтроллер начнет выполнять код, расположенный в секции загрузчика. По такому принципу работают Ардуино и подобные ей платы: после сброса микроконтроллера управление получает загрузчик, который в течение некоторого времени ожидает команды от компьютера. При получении команды на запись новой программы загрузчик принимает ее и размещает в памяти программ. После этого, а также в случае отсутствия команд в течение отведенного времени, загрузчик передает управление основной программе.

По аналогии с вектором сброса можно задать расположение таблицы векторов прерываний: в младших адресах памяти программ или в секции загрузчика. За это отвечает бит IVSEL регистра MCUCR. По умолчанию (после сброса микроконтроллера) данный бит сброшен в 0 и вектора прерываний располагаются начиная с адреса 0x0002. При установке бита IVSEL в 1 вектора прерываний "переносятся" в секцию загрузчика. Более детально бит IVSEL будет рассмотрен далее. В следующей таблице приведено расположение векторов сброса и прерываний при различных комбинациях BOOTRST и IVSEL.

BOOTRST IVSEL Адрес вектора сброса Адрес начала таблицы векторов прерываний
1 0 0x0000 0x0002
1 1 0x0000 Начальный адрес секции загрузчика + 0x0002
0 0 Начальный адрес секции загрузчика 0x0002
0 1 Начальный адрес секции загрузчика Начальный адрес секции загрузчика + 0x0002

С учетом сказанного начало программы на языке ассемблер для ATmega328/P может иметь следующий вид:

При включении питания и генерации сигнала сброса схемой Power-on Reset микроконтроллер выполнит команду, расположенную по адресу 0x0000 (либо на нее будет переход из загрузчика, как в случае с Ардуино). Этой командой является безусловный переход к метке RESET, с нее начинается стартовая инициализация и выполнение программы пользователя. Если в ходе работы программы поступит, например, запрос внешнего прерывания INT1 и его обработка будет разрешена, то микроконтроллер перейдет к вектору INT1 (к адресу 0x0004), который в свою очередь перенаправит микроконтроллер на обработчик данного прерывания.

Если программа не использует прерывания, то может располагаться сразу с адреса 0x0000.

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

Регистр MCUCR, бит IVSEL

Как было сказано, за положение таблицы векторов прерываний отвечает бит IVSEL регистра MCUCR (MicroController Unit Control Register — регистр управления микроконтроллером). Он содержит следующие биты:

Структура регистра MCUCR (ATmega328P)

Бит IVSEL (Interrupt Vector Select) по умолчанию сброшен и таблица векторов прерываний начинается с адреса 0x0002. Чтобы переназначить ее в секцию загрузчика необходимо записать в этот бит 1. Для этого сначала нужно разрешить его изменение, установив бит IVCE (Interrupt Vector Change Enable). Затем в течение 4 тактов записать новое значение в IVSEL. При установке IVCE автоматически запрещается обработка всех прерываний. Их обработка будет вновь разрешена после изменения бита IVSEL или по истечении 4 тактов. Разумеется, изменение IVSEL не переносит физически таблицу векторов прерываний, мы лишь сообщаем микроконтроллеру, где он должен ее искать: в секции программ или в секции загрузчика. Зачем это нужно, я думаю, понятно: если загрузчик использует прерывания, то он должен иметь соответствующие обработчики и таблицу векторов. По завершении своей работы загрузчик сбрасывает IVSEL, чтобы прерывания обслуживались обработчиками основной программы.

Остальные биты регистра MCUCR интереса для нас сейчас не представляют. Это биты BODS и BODSE, запрещающие работу схемы BOD при уходе микроконтроллера в сон. И бит PUD, который используется для глобального отключения подтягивающих резисторов.

Обработка прерываний

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

Для глобального разрешения/запрещения прерываний предназначен бит I регистра SREG. Для разрешения прерываний он должен быть установлен в 1, для запрещения — сброшен в 0. Именно этим битом манипулируют рассмотренные в предыдущей публикации функции sei, cli и interrupts, noInterrupts. Кроме того для каждого прерывания предусмотрен индивидуальный разрешающий его обработку бит.

Все прерывания можно разделить на два типа. Прерывания первого типа генерируются при наступлении некоторого события, в результате которого устанавливается флаг прерывания. Затем, если прерывание разрешено и бит I регистра SREG установлен, в счетчик команд загружается адрес вектора данного прерывания. При этом флаг данного прерывания аппаратно сбрасывается (исключение составляет флаг интерфейса TWI, который сбрасывается только программно). Он также может быть сброшен программно записью в него значения 1. Установить флаг прерывания программно (например, с целью эмулировать возникновение прерывания) невозможно.

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

Прерывания второго типа не имеют флагов, они генерируются в течение всего времени, пока присутствуют условия для их генерации. Соответственно, если запрос прерывания поступит в тот момент, когда его обработка запрещена и исчезнет до его разрешения, обработчик выполнен не будет и мы "потеряем" данный запрос. Если после выполнения обработчика условие для генерации прерывания все еще присутствует, то он будет выполнен повторно. Примером прерываний данного типа являются внешние прерывания по низкому уровню.

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

Очередность обработки прерываний

Упомянутые ранее флаги прерываний регистрируют запросы на прерывания даже когда их обработка запрещена. При последующем разрешении прерывания будут обслуживаться по очереди в соответствии с их приоритетом. Так же происходит при одновременном поступлении нескольких запросов. Приоритет прерывания определяется его положением в таблице векторов. В приведенной ранее таблице векторов наивысшим приоритетом обладает внешнее прерывание INT0, затем INT1 и так далее до SPM READY.

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

Сигнал сброса не является прерыванием, он обрабатывается вне очереди.

Время отклика на запрос прерывания

Возврат из обработчика в основную программу занимает 4 такта, в течение которых происходит восстановление счетчика команд из стека и установка бита I регистра SREG.

Ключевое слово ISR

Ранее отмечалось, что организацией таблицы векторов прерываний занимается компилятор. В AVR-GCC для каждого типа микроконтроллера уже предопределена таблица с необходимым набором векторов. Нам только остается указать компилятору, какому прерыванию какой обработчик соответствует, чтобы он внес актуальный адрес обработчика в таблицу. Для этого используется макрос ISR. Его синтаксис следующий:

Параметр vector определяет прерывание, для которого мы хотим создать обработчик. Возможные значения параметра для ATmega328/P приведены в таблице прерываний в графе Вектор. Информацию о всех допустимых значениях данного параметра вы можете найти в документации к AVR Libc: https://www.nongnu.org/avr-libc/user-manual/group__avr__interrupts.html

Необязательный параметр attributes может принимать значения: ISR_BLOCK, ISR_NOBLOCK, ISR_NAKED и ISR_ALIASOF(vect). Допускается комбинировать значения, указывая их через пробел.

В качестве примера использования ключевого слова ISR рассмотрим определение функции-обработчика для сторожевого таймера:

Всё предельно просто: в скобках указываем вектор прерывания, для которого предназначен данный обработчик; код обработчика приводим внутри фигурных скобок. И, конечно, в основной программе нужно разрешить работу WDT и настроить его на генерацию прерываний.

Проставление адресов в таблице векторов — не единственная работа, которую выполняет компилятор при обработке макроса ISR. Он также заботится о том, чтобы содержимое используемых в обработчике регистров сохранилось при входе, а затем восстановилось при выходе из обработчика. Для этих целей используется стек. Например, при компиляции программы с приведенным выше обработчиком для WDT я получил следующий код:

В данном случае команды push сохраняют в стеке значения регистров r0, r1 и регистра статуса SREG (зачем это делается я объясню в пункте Пролог и эпилог функции-обработчика прерывания). Помимо них в стеке уже лежит адрес для возврата в основную программу. И это при том, что обработчик ничего не делает. Если бы в нем выполнялись команды, использующие регистры общего назначения, то их содержимое тоже было бы сохранено в стеке. Теперь должно быть понятно, о какой потере памяти я говорил ранее в случае разрешения вложенных прерываний. Впрочем, если вы контролируете ситуацию и чрезмерный расход памяти исключен, ничто не мешает разрешить обработку вложенных прерываний. И вот тут мы плавно подошли назначению ISR_NOBLOCK в макросе ISR.

Параметр ISR_NOBLOCK

При указании значения ISR_NOBLOCK в качестве второго параметра макроса ISR компилятор добавит в обработчик команду установки бита I, разрешая тем самым обработку вложенных прерываний. Конечно, можно самим вставить в начале обработчика функцию interrupts или sei, но, как мы видели, компилятор дополняет обработчик кодом для сохранения содержимого регистров в стеке и прерывания будут разрешены только после выполнения данного кода. Параметр ISR_NOBLOCK дает указание компилятору вставить команду sei в самом начале обработчика до сохранения регистров. Это позволит сократить время реакции на запрос прерывания в тех случаях, когда это необходимо. Пример использования и генерируемый компилятором код приведены ниже.

Здесь можно упомянуть про команду sei, которую мы видим и в сгенерированном компилятором коде, и встречаем в скетчах Ардуино. sei и cli — это команды языка ассемблер для AVR микроконтроллеров. Данные команды соответственно устанавливают и сбрасывают I бит регистра SREG. Функции sei и cli, которые мы используем в скетчах — это макросы, объявленные в файле interrupt.h (является частью AVR Libc), которые в свою очередь компилируются в одну из приведенных команд. Функции interrupts и noInterrupts являются частью IDE Arduino, они объявлены в файле Arduino.h следующим образом:

Это те же самые sei и cli, для которых определили боле удобные имена.

Параметр ISR_BLOCK

Значение ISR_BLOCK дает указание компилятору не разрешать вложенные прерывания, что является его поведением по умолчанию. Поэтому описание обработчика с параметром ISR_BLOCK равносильно его описанию без такового.

Параметр ISR_NAKED

В некоторых случаях код, генерируемый компилятором для сохранения и восстановления значений регистров внутри обработчика, может быть не оптимальным. Например, приведенный выше обработчик для WDT не выполняет вообще никаких действий, тем не менее значения трех регистров сохраняются в стеке. Если нас не устраивает генерируемый компилятором код, то можно подавить его добавление в обработчик, указав во втором параметре макроса ISR значение ISR_NAKED. В этом случае в обработчик не будут добавлены ни код для сохранения регистров, ни даже команда возврата в основную программу reti, ответственность за корректную работу обработчика ложится на нас. Пример использования ISR_NAKED:

reti — это ассемблерная команда для возврата из обработчика в основную программу. Но в приведенном фрагменте вызов reti() — это обращение к макросу, он объявлен всё в том же файле interrupt.h и компилируется в одноименную команду микроконтроллера.

Параметр ISR_ALIASOF

Использование ISR_ALIASOF позволяет сообщить компилятору, что данное прерывание разделяет обработчик с другим прерыванием. Это бывает полезно в тех случаях, когда обработчики двух и более прерываний полностью идентичны (или могут быть приведены к общему виду). Хороший пример — общий обработчик для нескольких прерываний PCINT:

Для приведенного кода компилятор свяжет все 3 вектора PCINT с одним общим обработчиком.

Пролог и эпилог функции-обработчика прерывания

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

SREG — регистр статуса. Данный регистр содержит флаги, значения которых изменяются при выполнении различных команд. Например, флаг нуля (Zero flag) устанавливается в единицу, если в результате выполнения логической или арифметической операции получен 0. Другие флаги сигнализируют о факте переноса, или получения отрицательного результата и так далее. Сохранить содержимое данного регистра при входе в обработчик — золотое правило, которое соблюдается и в AVR-GCC. Однако SREG не может быть напрямую помещен в стек командой push. Поэтому его содержимое сначала считывается в регистр r0.

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

r1 — регистр общего назначения, в контексте AVR-GCC используется как нулевой регистр ("zero register") — в нем всегда должен быть 0. Подразумевая это, данный регистр используется, например, при необходимости сравнения с 0 или при записи 0 в ячейку памяти. Именно поэтому в прологе присутствует команда очистки регистра r1:
чтобы быть уверенным, что в нем содержится 0. Зачем тогда сохранять его в стек, если он всегда содержит 0? В том-то и дело, что не всегда: команды умножения помещают в регистр r1 старший байт результата. Если прерывание возникло в тот момент, когда результат умножения еще не был обработан основной программой, то регистр r1 может содержать не нулевое значение. Поэтому его содержимое тоже помещается в стек.

Вектор BADISR_vect

Ситуация когда для разрешенного прерывания не задан обработчик является ошибкой. AVR-GCC при формировании таблицы векторов для всех прерываний, не имеющих собственного обработчика, задает обработчик "по умолчанию". Этот обработчик содержит единственную команду — переход на вектор сброса. При необходимости можно переопределить данный обработчик, для этого используется вектор BADISR_vect:

Таким образом вместо типового сброса можно задать иное поведение для непредусмотренных прерываний.

Ключевое слово EMPTY_INTERRUPT

В редких случаях от обработчика не требуется выполнение вообще никаких действий. Например, если мы используем прерывание только для вывода микроконтроллера из спящего режима. В этом случае можно определить для него пустой обработчик (используя ISR), но более грамотным решением будет использование макроса EMPTY_INTERRUPT. В отличие от макроса ISR он не будет добавлять в обработчик пролог и эпилог и единственной его командой будет возврат в основную программу — reti. Ниже приведен пример использования данного макроса для прерывания от WDT:

Вешние прерывания INTx

Разобравшись с логикой обработки прерываний и с синтаксисом макроса ISR, можно применить новые знания на практике. В предыдущей статье мы познакомились с функциями attachInterrupt и detachInterrupt для работы с внешними прерываниями. Давайте теперь попробуем обойтись без функций IDE и выполним все необходимые действия для обработки внешних прерываний самостоятельно.

Как уже отмечалось, кроме бита I, разрешающего обработку прерываний глобально, существуют биты, разрешающие обработку прерываний индивидуально. Для внешних прерываний — это два младших бита регистра EIMSK (External Interrupt Mask Register):

Структура регистра EIMSK (ATmega328P)

Для того чтобы разрешить обработку прерываний на входе INT0 необходимо установить одноименный бит регистра EIMSK. Для разрешения прерываний от INT1 следует, соответственно, установить бит INT1 регистра. По умолчанию (после сброса микроконтроллера) оба бита сброшены и обработка внешних прерываний запрещена.

Для задания типа отслеживаемых событий на входах INTx используется регистр EICRA (External Interrupt Control Register A). Назначение его битов следующее:

Структура регистра EICRA (ATmega328P)

  • 00 — при наличии сигнала низкого уровня;
  • 01 — при изменении сигнала от высокого уровня к низкому и наоборот;
  • 10 — при изменении сигнала от высокого уровня к низкому;
  • 11 — при изменении сигнала от низкого уровня к высокому.

И последний, третий, регистр, имеющий отношение к обработке внешних прерываний — это регистр флагов EIFR (External Interrupt Flag Register):

Структура регистра EIFR (ATmega328P)
Бит INTF0 устанавливается в 1 при выполнении условия генерации прерывания на входе INT0 в соответствии с конфигурацией битов ISC0x регистра EICRA. Затем, если обработка прерываний от INT0 разрешена и бит I установлен в 1, то будет выполнен соответствующий обработчик и значение бита будет сброшено. Также значение бита INTF0 можно сбросить программно, записав в него значение "1". Если конфигурацией битов ISC0x регистра EICRA определено генерировать прерывания при наличии низкого уровня на входе INT0, то данный флаг в обработке не задействуется и в нем будет значение "0".

Бит INTF1 аналогичен INTF0, но сигнализирует об обнаружении запроса прерывания на входе INT1.

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

  1. Задать обработчик, используя ключевое слово ISR.
  2. Определить тип событий на входе, генерирующих запрос прерывания (регистр EICRA).
  3. Разрешить обработку внешнего прерывания (регистр EIMSK).
  4. Установить бит I, разрешающий обработку прерываний глобально (регистр SREG).

В функции setup после задания режима работы пинов выполняется сброс бита ISC00 и установка ISC01 регистра EICRA. Таким образом их комбинация обеспечит отслеживание изменения сигнала на входе INT0 от высокого уровня к низкому. Далее мы разрешаем обработку прерываний INT0, бит I регистра SREG у нас уже установлен. Обработчик определен с использованием макроса ISR, в качестве параметра (вектора прерывания) указано значение INT0_vect. Логика программы всё та же, что и в прошлой публикации с использованием attachInterrupt: в обработчике изменяем значение переменной, а в функции loop используем эту переменную для управления светодиодом. Для проверки работы скетча установите кнопку между вторым пином и землей.

Прерывания при изменении состояния вывода (Pin Change Interrupts, PCINT)

Как следует из названия, прерывания данного типа генерируются при любом изменении состояния вывода. И пусть нам недоступны прерывания по низкому уровню, только по нарастающему или только по спадающему фронту сигнала, как в случае с внешними прерываниями INTx, но зато мы уже не ограничены двумя входами: прерывания по изменению состояния вывода доступны практически на всех выводах Ардуино. Для Ардуино УНО (и других плат на базе ATmega328/P) эти выводы:

  • D8 .. D13 — генерируют запрос прерывания PCINT0
  • A0 .. A5 — генерируют запрос прерывания PCINT1
  • D0 .. D7 — генерируют запрос прерывания PCINT2

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

Из-за отсутствия в IDE Arduino функций, облегчающих использование прерываний по изменению состояния вывода (как в случае с внешними прерываниями INTx), они менее известны и реже используются ардуинщиками. На самом деле ничего сложного в их использовании нет, в чем мы сейчас и убедимся.

Для работы с PCINT предусмотрены регистры PCICR, PCIFR и три регистра PCMSKx. Рассмотрим каждый из их.

Назначение битов регистра PCICR (Pin Change Interrupt Control Register):

Структура регистра PCICR (ATmega328P)

  • PCIE0 — значение "1" в этом бите разрешает обработку прерываний группы PCINT0.
  • PCIE1 — значение "1" в этом бите разрешает обработку прерываний группы PCINT1.
  • PCIE2 — значение "1" в этом бите разрешает обработку прерываний группы PCINT2.

Структура регистра PCIFR (ATmega328P)

  • PCIF0 — значение "1" в этом бите сигнализирует об обнаружении запроса прерывания PCINT0.
  • PCIF1 — значение "1" в этом бите сигнализирует об обнаружении запроса прерывания PCINT1.
  • PCIF2 — значение "1" в этом бите сигнализирует об обнаружении запроса прерывания PCINT2.
Бит Обозначение вывода (IDE Arduino) Номер вывода (Микроконтроллер) PCINTx
Регистр PCMSK0
0 D8 14 PCINT0
1 D9 15 PCINT1
2 D10 16 PCINT2
3 D11 17 PCINT3
4 D12 18 PCINT4
5 D13 19 PCINT5
6 9 PCINT6
7 10 PCINT7
Регистр PCMSK1
0 A0 23 PCINT8
1 A1 24 PCINT9
2 A2 25 PCINT10
3 A3 26 PCINT11
4 A4 27 PCINT12
5 A5 28 PCINT13
6 1 PCINT14
Регистр PCMSK2
0 D0 2 PCINT16
1 D1 3 PCINT17
2 D2 4 PCINT18
3 D3 5 PCINT19
4 D4 6 PCINT20
5 D5 11 PCINT21
6 D6 12 PCINT22
7 D7 13 PCINT23

Выводы микроконтроллера ATmega328/P с номерами 9 и 10 используются в Ардуино для подключения резонатора; вывод 1 — это вход Reset. Поэтому придется отказаться от их использования в качестве входов прерываний. Бита PCINT15 в ATmega328/P нет в принципе.

  1. Задать обработчик для соответствующего прерывания PCINT, используя макрос ISR.
  2. Разрешить генерацию прерываний интересующим выводом микроконтроллера (регистр группы PCMSKx).
  3. Разрешить обработку прерывания PCINT, которое генерирует интересующий вывод (регистр PCICR).
  4. Установить бит I, разрешающий обработку прерываний глобально (регистр SREG).

Описанные ранее манипуляции с регистрами микроконтроллера в данном примере вынесены в функцию pciSetup. Кроме установки нужных битов в регистрах PCMSKx и PCICR функция также сбрасывает флаг обнаружения запроса прерывания в регистре PCIFR. Это поможет избежать непреднамеренного вызова функции-обработчика. После функции pciSetup идут три обработчика для прерываний PCINT0, PCINT1 и PCINT2, но используется в скетче только первый из них. Остальные я добавил чтобы показать, как они описываются, их можно смело удалить. Для определения источника прерывания в обработчике сохраняется предыдущее значение пинов D8..D13 в переменной oldPINB и сравнивается с текущим. Если значение какого-либо пина изменилось, то выполняем соответствующий ему блок кода. В данном случае мы изменяем значение переменной state, чтобы управлять светодиодом. Функция setup задействует встроенные подтягивающие резисторы и разрешает прерывания при изменении состояния выводов D8 и D9.

Для проверки скетча установите кнопки между указанными выводами и землей. Поскольку запрос прерывания генерируется при изменении состояния вывода, то обработчик будет вызываться как при нажатии, так и при отпускании кнопки. Но здесь это не принципиально.

Прерывания от сторожевого таймера

Рассмотрим еще один пример работы с прерываниями, на этот раз от сторожевого таймера (Watchdog Timer, WDT). Мы уже использовали прерывания WDT для выхода из спящего режима, теперь можем изучить их более детально.

Вообще генерация прерывания для пробуждения микроконтроллера не единственная функция сторожевого таймера. Он может использоваться как простой таймер, если не требуется точность отмеряемых интервалов времени. В режиме генерации сигнала сброса сторожевой таймер используют когда возможно зависание системы: в ходе нормальной работы программа регулярно сбрасывает сторожевой таймер, предотвращая сброс микроконтроллера; в случае зависания программы по истечении заданного времени сторожевой таймер генерирует сигнал сброса. Это позволяет повысить надежность микроконтроллерной системы. Также возможно совмещение режимов генерации прерывания и сброса, в этом случае сначала будет вызван обработчик прерывания, что позволит сохранить важные данные, а затем при следующем таймауте WDT будет сгенерирован сигнал сброса микроконтроллера.

Управление работой сторожевого таймера осуществляется при помощи регистра WDTCSR.

Назначение битов регистра WDTCSR (Watchdog Timer Control Register):

Структура регистра WDTCSR (ATmega328P)

  • WDIF (Watchdog Interrupt Flag) — флаг запроса прерывания. Устанавливается в 1 по истечении установленного интервала времени, когда сторожевой таймер сконфигурирован на генерацию прерываний. Флаг сбрасывается аппаратно при выполнении обработчика или путем программной записи в него значения "1".
  • WDIE (Watchdog Interrupt Enable) — бит, разрешающий обработку прерываний от WDT. Когда этот бит установлен и WDE сброшен, сторожевой таймер сконфигурирован на генерацию прерываний. При установленных WDIE и WDE сторожевой таймер сначала будет генерировать запрос прерывания, сбрасывая при этом WDIE, затем следующий таймаут таймера инициирует сигнал сброса микроконтроллера.
  • WDP[3] (Watchdog Timer Prescaler 3) — третий бит делителя частоты WDT.
  • WDCE (Watchdog Change Enable) — бит, разрешающий изменение бита WDE и значения делителя (WDP[3:0]). Для их изменения WDCE должен быть установлен в "1". По истечении четырех тактов данный бит автоматически сбрасывается, поэтому его следует устанавливать непосредственно перед изменением WDE и WDP[3:0].
  • WDE (Watchdog System Reset Enable) — данный бит разрешает генерацию сигнала сброса сторожевым таймером. Его изменение контролируется битом WDCE, кроме того он не может быть сброшен до тех пор, пока установлен бит WDRF регистра MCUSR.
  • WDP[2:0] (Watchdog Timer Prescaler 2, 1, и 0) — младшие три бита делителя тактового сигнала WDT. Допустимые комбинации битов WDP[3:0] и соответствующие им интервалы времени приведены в таблице:
WDP3 WDP2 WDP1 WDP0 Число тактов генератора WDT Интервал времени
0 0 0 0 2K (2048) 16мс
0 0 0 1 4K (4096) 32мс
0 0 1 0 8K (8192) 64мс
0 0 1 1 16K (16384) 0.125с
0 1 0 0 32K (32768) 0.25с
0 1 0 1 64K (65536) 0.5с
0 1 1 0 128K (131072)
0 1 1 1 256K (262144)
1 0 0 0 512K (524288)
1 0 0 1 1024K (1048576)
1 0 1 0 Зарезервировано
1 0 1 1
1 1 0 0
1 1 0 1
1 1 1 0
1 1 1 1

Напоминаю также о конфигурационном бите WDTON, влияющем на работу сторожевого таймера.

  1. Установить биты WDCE и WDE (вне зависимости от предыдущего значения WDE).
  2. В течение следующих четырех тактов установить нужное значение битов WDE, WDP[3:0] и сбросить бит WDCE. Это должно быть сделано в рамках одной команды.

В пакет AVR Libc входит заголовочный файл wdt.h. С ним работа с WDT сводится к вызову функций запуска, остановки и сброса таймера. В следующем скетче Ардуино погружается в сон и просыпается по прерыванию от сторожевого таймера. Для работы с таймером как раз используются функции файла wdt.h.

Что интересного есть в приведенном коде? Во-первых, мы удачно применили макрос EMPTY_INTERRUPT, рассмотренный ранее. Кроме того использование макросов из заголовочного файла wdt.h позволило сделать скетч короче. Но здесь есть и нюанс: вызов wdt_enable устанавливает бит WDE, что нам не нужно (нас интересует только прерывание). Поэтому мы сами устанавливаем бит WDIE, таким образом таймер сконфигурирован на генерацию прерывания и сброса. Это означает, что бит WDIE будет автоматически сбрасываться каждый раз при вызове обработчика и, если его не установить повторно, то следующий таймаут WDT уже приведет к сбросу микроконтроллера. По логике программы нам не нужен WDT после пробуждения, поэтому мы его останавливаем вызовом wdt_disable и сброс микроконтроллера нам не грозит. Но эту особенность нужно иметь в виду при работе со сторожевым таймером.

И напоследок фрагмент кода, который можно использовать для сброса микроконтроллера по таймауту сторожевого таймера:


Источник: tsibrov.blogspot.com