четверг, 13 июня 2019 г.

Прерывания в Ардуино. Часть 2

Прерывания в Ардуино. Внешние прерывания, 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 может иметь следующий вид:

Адреса  Метки     Команды               Комментарии
0x0000            jmp RESET             ; Reset
0x0002            jmp INT0              ; IRQ0
0x0004            jmp INT1              ; IRQ1
0x0006            jmp PCINT0            ; PCINT0
0x0008            jmp PCINT1            ; PCINT1
0x000A            jmp PCINT2            ; PCINT2
0x000C            jmp WDT               ; Watchdog Timeout
0x000E            jmp TIM2_COMPA        ; Timer2 CompareA
0x0010            jmp TIM2_COMPB        ; Timer2 CompareB
0x0012            jmp TIM2_OVF          ; Timer2 Overflow
0x0014            jmp TIM1_CAPT         ; Timer1 Capture
0x0016            jmp TIM1_COMPA        ; Timer1 CompareA
0x0018            jmp TIM1_COMPB        ; Timer1 CompareB
0x001A            jmp TIM1_OVF          ; Timer1 Overflow
0x001C            jmp TIM0_COMPA        ; Timer0 CompareA
0x001E            jmp TIM0_COMPB        ; Timer0 CompareB
0x0020            jmp TIM0_OVF          ; Timer0 Overflow
0x0022            jmp SPI_STC           ; SPI Transfer Complete
0x0024            jmp USART_RXC         ; USART RX Complete
0x0026            jmp USART_UDRE        ; USART UDR Empty
0x0028            jmp USART_TXC         ; USART TX Complete
0x002A            jmp ADC               ; ADC Conversion Complete
0x002C            jmp EE_RDY            ; EEPROM Ready
0x002E            jmp ANA_COMP          ; Analog Comparator
0x0030            jmp TWI               ; 2-wire Serial
0x0032            jmp SPM_RDY           ; SPM Ready
;
0x0034  RESET:    ldi r16,high(RAMEND)  ; Main program start
0x0035            out SPH,r16           ; Set Stack Pointer to top of RAM
0x0036            ldi r16,low(RAMEND)
0x0037            out SPL,r16
0x0038            sei                   ; Enable interrupts

При включении питания и генерации сигнала сброса схемой 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 бита. Затем в счетчик команд загружается адрес вектора. Как правило по этому адресу находится команда перехода к обработчику, выполнение которой занимает еще 3 такта. Если в момент обнаружения прерывания выполняется команда, длящаяся несколько тактов, то обработка прерывания начнется после завершения данной команды. Если запрос прерывания поступает, когда микроконтроллер находится в спящем режиме, то в дополнение к времени пробуждения, которое зависит от режима и настроек фьюзов, время отклика увеличивается еще на 4 такта.

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


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

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

Параметр 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 рассмотрим определение функции-обработчика для сторожевого таймера:

ISR (WDT_vect) {
  //Наш код здесь
}

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

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

push    r1
push    r0
in      r0, SREG
push    r0
clr     r1
; Наш код здесь
pop     r0
out     SREG, r0
pop     r0
pop     r1
reti

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


Параметр ISR_NOBLOCK

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

ISR (WDT_vect, ISR_NOBLOCK) {
  //Наш код здесь
}

sei
push    r1
push    r0
in      r0, SREG
push    r0
clr     r1
; Наш код здесь
pop     r0
out     SREG, r0
pop     r0
pop     r1
reti

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

#define interrupts() sei()
#define noInterrupts() cli()

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


Параметр ISR_BLOCK

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


Параметр ISR_NAKED

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

ISR(TIMER1_OVF_vect, ISR_NAKED)
{
  PORTB |= _BV(0);
  reti();
}

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


Параметр ISR_ALIASOF

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

ISR(PCINT0_vect){
  // Наш код здесь
}
ISR(PCINT1_vect, ISR_ALIASOF(PCINT0_vect));
ISR(PCINT2_vect, ISR_ALIASOF(PCINT0_vect));

Для приведенного кода компилятор свяжет все 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:
clr     r1
чтобы быть уверенным, что в нем содержится 0. Зачем тогда сохранять его в стек, если он всегда содержит 0? В том-то и дело, что не всегда: команды умножения помещают в регистр r1 старший байт результата. Если прерывание возникло в тот момент, когда результат умножения еще не был обработан основной программой, то регистр r1 может содержать не нулевое значение. Поэтому его содержимое тоже помещается в стек.


Вектор BADISR_vect

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

ISR(BADISR_vect) {
  //Наш код здесь
}

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


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

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

EMPTY_INTERRUPT(WDT_vect);


Вешние прерывания 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)

Значения битов ISC00 и ISC01 определяют события какого типа приводят к генерации прерывания INT0:
  • 00 - при наличии сигнала низкого уровня;
  • 01 - при изменении сигнала от высокого уровня к низкому и наоборот;
  • 10 - при изменении сигнала от высокого уровня к низкому;
  • 11 - при изменении сигнала от низкого уровня к высокому.
Назначение битов ISC10 и ISC11 аналогично ISC0x с той разницей, что они определяют тип событий для прерывания INT1.

И последний, третий, регистр, имеющий отношение к обработке внешних прерываний - это регистр флагов 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).

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

#define ledPin 13
#define interruptPin 2 // Кнопка между цифровым пином 2 (вход INT0) и GND
volatile byte state = LOW;

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(interruptPin, INPUT_PULLUP); // Подтягиваем второй пин к питанию
  EICRA &= ~(1 << ISC00); //Сбрасываем ISC00
  EICRA |= (1 << ISC01); // Устанавливаем ISC01 - отслеживаем FALLING на INT0
  EIMSK |= (1 << INT0); // Разрешаем прерывание INT0
}

void loop() {
  digitalWrite(ledPin, state);
}

ISR(INT0_vect) {
  state = !state;
}

В функции 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 (Pin Change Interrupt Flag Register):

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



  • PCIF0 - значение "1" в этом бите сигнализирует об обнаружении запроса прерывания PCINT0.
  • PCIF1 - значение "1" в этом бите сигнализирует  об обнаружении запроса прерывания PCINT1.
  • PCIF2 - значение "1" в этом бите сигнализирует  об обнаружении запроса прерывания PCINT2.


Три регистра PCMSK0, PCMSK1 и PCMSK2 (Pin Change Mask Register) используются для указания входов, которым разрешено генерировать сигнал запроса прерывания. Соответствие битов регистров PCMSKx выводам микроконтроллера ATmega328/P (для 28-выводного DIP корпуса) и их обозначениям в IDE Arduino приведено в следующей таблице:

Бит Обозначение вывода (IDE Arduino)Номер вывода (Микроконтроллер)PCINTx
Регистр PCMSK0
0D814PCINT0
1D915PCINT1
2D1016PCINT2
3D1117PCINT3
4D1218PCINT4
5D1319PCINT5
6-9PCINT6
7-10PCINT7
Регистр PCMSK1
0A023PCINT8
1A124PCINT9
2A225PCINT10
3A326PCINT11
4A427PCINT12
5A528PCINT13
6-1PCINT14
Регистр PCMSK2
0D02PCINT16
1D13PCINT17
2D24PCINT18
3D35PCINT19
4D46PCINT20
5D511PCINT21
6D612PCINT22
7D713PCINT23

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

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

И для примера очередной скетч управления встроенным светодиодом. В этот раз для включения и выключения светодиода будут задействованы отдельные кнопки. Это поможет понять принцип нахождения пина-источника прерывания.

#define ledPin 13      // Пин для светодиода
#define setLedOnPin 8  // Пин кнопки включения светодиода
#define setLedOffPin 9 // Пин кнопки выключения светодиода

volatile uint8_t state = 0;
uint8_t oldPINB = 0xFF;

void pciSetup(byte pin) {
  *digitalPinToPCMSK(pin) |= bit (digitalPinToPCMSKbit(pin));  // Разрешаем PCINT для указанного пина
  PCIFR  |= bit (digitalPinToPCICRbit(pin)); // Очищаем признак запроса прерывания для соответствующей группы пинов
  PCICR  |= bit (digitalPinToPCICRbit(pin)); // Разрешаем PCINT для соответствующей группы пинов
}

ISR (PCINT0_vect) { // Обработчик запросов прерывания от пинов D8..D13
  uint8_t changedbits = PINB ^ oldPINB;
  oldPINB = PINB;

  if (changedbits & (1 << PB0)) { // Изменился D8
    state = 1; // Зажигаем светодиод
  }

  if (changedbits & (1 << PB1)) { // Изменился D9
    state = 0; // Гасим светодиод
  }

  //if (changedbits & (1 << PB2)) { ... } - аналогичные условия для остальных пинов
}

ISR (PCINT1_vect) { // Обработчик запросов прерывания от пинов A0..A5
  // Обработка аналогична PCINT0_vect, только изменить на PINC, oldPINC, PCx
}

ISR (PCINT2_vect) { // Обработчик запросов прерывания от пинов D0..D7
  // Обработка аналогична PCINT0_vect, только изменить на PIND, oldPIND, PDx
}

void setup() {
  pinMode(ledPin, OUTPUT);
  pinMode(setLedOnPin,  INPUT_PULLUP); // Подтянем пины-источники PCINT к питанию
  pinMode(setLedOffPin, INPUT_PULLUP);
  pciSetup(setLedOnPin); // И разрешим на них прерывания
  pciSetup(setLedOffPin);
}


void loop() {
  digitalWrite(ledPin, state);
}

Описанные ранее манипуляции с регистрами микроконтроллера в данном примере вынесены в функцию 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, влияющем на работу сторожевого таймера.

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

volatile bool f = 0;

void setup() {
  Serial.begin(9600);
  cli(); // Запрещаем прерывания на время изменения WDE и WDP
  asm("wdr"); // Сбрасываем WDT
  // Разрешаем изменение значения предделителя WDT:
  WDTCSR |= (1 << WDCE) | (1 << WDE);
  // Устанавливаем бит WDP3 для выбора интервала 4с и разрешаем прерывания от WDT:
  WDTCSR = (1 << WDP3) | (1 << WDIE );
  sei(); // Разрешаем прерывания
}

void loop() {
  if (f) {
    Serial.print(millis());
    Serial.println(" WDT!");
    f = 0;
  }
}

ISR(WDT_vect){
  f = 1;
}

Если вы загрузите приведенный скетч в Ардуино, то увидите, что после запуска сторожевого таймера прерывания генерируются примерно каждые 4 секунды. Перезапуск таймера не требуется. В скетче присутствует ассемблерная команда wdr, она выполняет сброс таймера. Сброс сторожевого таймера рекомендуется делать перед изменением битов WDP, в противном случае их изменение в меньшую сторону может привести к таймауту WDT.

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

#include <avr/wdt.h>
#include <avr/sleep.h>

#define ledPin 13 // Пин для светодиода
bool f = 0;

void setup() {
  pinMode(ledPin, OUTPUT);
}

void loop() {
  // Здесь может быть условие для перехода в спящий режим
  wdt_enable(WDTO_4S); // Разрешаем работу таймера и задаем интервал
  // Определенные в wdt.h значения: WDTO_15MS, WDTO_30MS, WDTO_60MS, WDTO_120MS, 
  // WDTO_250MS, WDTO_500MS, WDTO_1S, WDTO_2S, WDTO_4S, WDTO_8S
  WDTCSR |= (1 << WDIE); // Разрешаем прерывания от сторожевого таймера
  set_sleep_mode(SLEEP_MODE_PWR_DOWN); //Устанавливаем интересующий нас режим
  sleep_mode(); // Переводим МК в спящий режим. Пробуждение по WDT
  // После выполнения обработчика WDT работа программы продолжится отсюда
  wdt_disable(); // Останавливаем WDT, он нам больше не нужен
  // Дальше выполняем какие-либо действия и опять уходим в сон
  digitalWrite(ledPin, (f=!f));
}

EMPTY_INTERRUPT(WDT_vect);

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

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

#include <avr/wdt.h>
...
wdt_enable(WDTO_15MS); // Сброс по WDT через ~16мс
while(1); // Ожидаем сброс


Заключение

Итак, мы познакомились с логикой обработки прерываний в AVR микроконтроллерах и средствами AVR Libc для их использования. Материал получился непростой, но, мне кажется, достаточно интересный. Надеюсь, он помог вам найти ответы на интересующие вопросы. Если что-то осталось непонятным, задавайте вопросы в комментариях, я постараюсь на них ответить.

33 комментария:

  1. Добрый день. Есть ли возможность переназначить прерывание INT1 на другой физический вход? Например с D3 на D4?

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Нет. Это невозможно.

      Удалить
    2. Подскажите правильно ли я понял. Мне нужно прерывание по D4. Получается для этого я могу использовать только PCINT2. Но у меня используется D0\D1 как СОМ порт и INT0. Тк PCINT2 активируется по любому изменению D0..D7 то обработчик прерывания будет постоянно вызываться. Есть ли способ отфильтровать вызов прерывания только по D4 и желательно только по фронту?

      Удалить
    3. Почитайте внимательнее про PCINT. Регистры PCMSKx отвечают за то, каким входам разрешено генерировать прерывания.

      Удалить
  2. Добрый день, благодарю за быстрый ответ. Разобрался с регистрами, не сразу понял что данная маска относиться только к текущему прерыванию.
    Если я правильно понял то Delay() и вызов процедур непосредственно из прерывания не работает. Подскажите как правильно сделать задержку выполнения программы на несколько секунд при активации прерывания. Вызов Sleep Mode так же приводит к зависанию. Возврат в основной код программы недопустим. или как альтернатива, если есть такая возможность, то возврат после прерывания в определенное место программы для дальнейшей обработки.

    ОтветитьУдалить
    Ответы
    1. Ваша основная программа всё равно же вертится в каком-то цикле. Добавьте в нем проверку флага: когда он true, делайте задержку или то что нужно. А сам флаг устанавливайте в обработчике прерывания.

      Удалить
    2. Владимир, это понятно, но конкретно в моем случае периодически идет передача по COM порту и при появлении в цикле мне надо ее прервать. Пока вижу только вариант полного перезапуска с переходом на нулевой адрес.

      Удалить
    3. Моя рекомендация в силе:
      1 Объявляете volatile переменную, например, f. Записываете в нее 0.
      2 Внутри цикла передачи по COM порту добавляете условие if (f) break; При наличии внешних циклов добавляете в них такое же условие. Или сразу return вместо break, вам виднее.
      3 Переменную f устанавливаете в 1 внутри обработчика прерывания.

      Таким образом при нажатии на кнопку ваш цикл прервется и вы окажетесь за его пределами, где опять же можете проверить значение f, чтобы понять, что цикл прерван именно кнопкой, сбросить f в 0 и т.д.
      Замечу, что нажатие на кнопку это "долгое событие" и его не обязательно отлавливать прерыванием. Можно добавить опрос кнопки в цикл обмена с ком портом, результат будет тот же.

      Что касается выхода в произвольное место скетча, то это goto (локальный переход, т.е. в пределах функции) и setjmp, longjmp (для перемещения между функциями, в том числе из прерывания). goto не рекомендуется использовать в принципе. setjmp и longjmp на мой взгляд труднее для понимания чем предложенный ранее вариант.

      Удалить
    4. Про кнопку уже сам додумал) Перепутал с другим вопросом.

      Удалить
  3. Классно пишете) Очень полезный материал и доходчиво!

    ОтветитьУдалить
  4. Добавил себе в блокнотик, продолжайте в том же духе! 73 UA6EM

    ОтветитьУдалить
  5. Здравствуйте! Спасибо за статью
    pinMode(ledPin, OUTPUT);
    pinMode(interruptPin, INPUT_PULLUP); // Подтягиваем второй пин к питанию
    EICRA &= ~(1 << ISC01); //Сбрасываем ISC01
    EICRA |= (1 << ISC00); // Устанавливаем ISC00 - отслеживаем FALLING на INT0
    EIMSK |= (1 << INT0); // Разрешаем прерывание INT0

    Я правильно понял? ISC01 = 0, ISC00 = 1 , это режим отлова перехода с высокого на низкий уровень? или туда обратно? Спасибо!

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Похоже, я перепутал биты. Для FALLING ISC01 должен быть установлен, а ISC00 - сброшен (10). Сейчас в них 01, получается что отслеживаем CHANGE. В примере поправлю, там нужен FALLING. Спасибо за наводку.

      Удалить
    2. А так огромное спасибо!!! Всё компилится! Ещё вопросик, может некорректный конечно))) А можно предусмотреть несколько функций для внешнего прерывания? делаю робота, в одном случае прерывания планирую использовать для синхронизации колёс от датчиков холла на INT0 и INT1, в другом случае использовать прерывания для выравнивания робота от датчиков линии (ну например тумблер ставим на пин и меняем функционал), чтобы не прошивать каждый раз

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

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

      Удалить
    4. Спасибо, буду экспериментировать!

      Удалить
  6. Спасибо большое за ваш труд и дай БОГ вам здоровья! -как-раз то что нужно!

    ОтветитьУдалить
  7. Или я что-то путаю или в сводной таблице с регистрами PCMSK0-2 есть ошибки. Например PCINT0 судя по даташиту соответствует выводу 12, в таблице номер 14. Нужна проверка

    ОтветитьУдалить
    Ответы
    1. Полагаю, вы смотрите распиновку для контроллера в 32-выводном корпусе. А у меня в статье для DIP корпуса. Добавлю пометку.

      Удалить
  8. Есть ардуино нано 328, есть нано 4808, при этом братья китайцы при запросе как шить отправляют к прошивке Everi но это 4809. Возникает вопрос как же шить скетч. при попытки просто сменить плату с нано 328 на Эвери ругается на MCUCR - что делать и что менять в скетче? (Скетч для "spot welder arduino" на 328 плату с выбором в прошивальщике 328 шьётся нормально, при установке 4208 и любых других вариантах в прошивальщике - ругань) При этом в скетче MCUCR упоминается 1 раз!!

    ОтветитьУдалить
  9. void reset_mcusr(void) __attribute__((naked)) __attribute__((section(".init3")));
    void reset_mcusr(void) {
    MCUSR = 0;
    wdt_disable();

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Не могу ничего сказать по этому поводу, т.к. не работал с every

      Удалить
  10. Здравствуйте! Хотябы почему MCUSR = 0
    и больше я не вижу где её использовали

    ОтветитьУдалить
    Ответы
    1. Добрый вечер!
      Сброс MCUSR в начале программы - это обычное дело для avr микроконтроллеров (для тех, в которых он есть).
      А ругается потому, что ATmega4808/4809 не имеют такого регистра.

      Удалить
  11. Этот комментарий был удален автором.

    ОтветитьУдалить
  12. Здравствуйте. Большое спасибо за статью.
    Я был бы очень благодарен если бы вы подсказали как будет выглядеть код для управления встроенным светодиодом отдельными кнопками для arduino mega.

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      В целом он будет выглядеть так же. В плане внешних прерываний 2560 отличается от 328 бОльшим числом входов, поддерживающих их. Поэтому в нём два управляющих регистра: EICRA и EICRB. Остальное всё то же самое. Посмотрите даташит на ATmega2560, раздел 15.
      Настройка PCINT, по-моему, и вовсе не отличается.
      У меня нет готового примера для 2560. А писать и проверять, сами понимаете, никто не захочет, если вы сами не пробуете.

      Удалить
  13. День добрый Владимир!
    В одном из роликов на ютубе автор проводил сравнение процессоров avr и stm и озвучил, что в ARDUINO IDE неправильно реализована функция attachInterrupt(), лечится перед вызовом функции - EIFR = 0x01;
    Он не только озвучил, но и показал наглядно на измерении временных отрезков логическим анализатором. Что-то можете сказать по этому поводу?

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Прошу прощения за долгий ответ.

      Да, есть такая особенность в реализации attachInterrupt. На гите неоднократно предлагалось исправить ее, добавив сброс флагов EIFR. Например, в этой теме: https://github.com/arduino/ArduinoCore-avr/issues/244
      - отказ объяснили последующими проблемами с библиотекой Adafruit CC3300.

      Утверждать, что это именно неправильная реализация - даже не знаю. Наверное, принимать решение о ложности/значимости флага должен сам программист и при необходимости сбрасывать его. Может разработчики придерживались такой позиции при написании функции? В любом случае сомневаюсь, что они тупо не учли тот факт, что EIFR/GIFR может быть установлен до вызова attachInterrupt и это сразу приведет к вызову обработчика

      Удалить
  14. Добрый день! Очень хорошая статья! Спасибо!
    У меня вопрос в главе "Очередность обработки прерываний".
    Например, выполняется прерывание X первого типа и прерывания запрещены, в это время наступает прерывание Y, тоже первого типа. Соответственно, устанавливается флаг возникновения прерывания Y.
    Если в это время, еще раз наступит прерывание Y, то что произойдет? Ничего? т.о одно прерывание будет потеряно?

    ОтветитьУдалить
    Ответы
    1. Добрый день!
      Всё верно, прерывание будет потеряно.

      Удалить