Продолжаем тему использования прерываний в Ардуино. В предыдущей публикации мы познакомились с функциями среды Ардуино для работы с внешними прерываниями. Сегодня рассмотрим какие еще прерывания присутствуют в AVR микроконтроллерах и разберем несколько примеров их использования. А начнем мы с самого начала - с таблицы векторов прерываний.
Содержание
Таблица векторов прерываний
Регистр MCUCR, бит IVSEL
Обработка прерываний
Очередность обработки прерываний
Время отклика на запрос прерывания
Ключевое слово ISR
- Параметр ISR_NOBLOCK
- Параметр ISR_BLOCK
- Параметр ISR_NAKED
- Параметр ISR_ALIASOF
Пролог и эпилог функции-обработчика прерывания
Вектор BADISR_vect
Ключевое слово EMPTY_INTERRUPT
Вешние прерывания INTx
Прерывания при изменении состояния вывода (Pin Change Interrupts, PCINT)
Прерывания от сторожевого таймера
Заключение
Регистр MCUCR, бит IVSEL
Обработка прерываний
Очередность обработки прерываний
Время отклика на запрос прерывания
Ключевое слово ISR
- Параметр ISR_NOBLOCK
- Параметр ISR_BLOCK
- Параметр ISR_NAKED
- Параметр ISR_ALIASOF
Пролог и эпилог функции-обработчика прерывания
Вектор BADISR_vect
Ключевое слово EMPTY_INTERRUPT
Вешние прерывания INTx
Прерывания при изменении состояния вывода (Pin Change Interrupts, PCINT)
Прерывания от сторожевого таймера
Заключение
Таблица векторов прерываний
При появлении запроса на прерывание микроконтроллер приостанавливает выполнение основной программы и переходит к обработчику прерывания. Но как он находит этот самый обработчик? Для этого используется таблица векторов прерываний. Каждому прерыванию соответствует свой вектор, который по сути является командой перехода на функцию-обработчик. Располагается эта таблица как правило в младших адресах памяти программ: по нулевому адресу находится вектор сброса, далее идут вектора прерываний. Поскольку источниками прерываний служат внешние и периферийные устройства микроконтроллера, то их набор зависит от конкретной модели микроконтроллера. Для 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 - регистр управления микроконтроллером). Он содержит следующие биты:Бит 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. Для разрешения прерываний он должен быть установлен в 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):
Для того чтобы разрешить обработку прерываний на входе INT0 необходимо установить одноименный бит регистра EIMSK. Для разрешения прерываний от INT1 следует, соответственно, установить бит INT1 регистра. По умолчанию (после сброса микроконтроллера) оба бита сброшены и обработка внешних прерываний запрещена.
Для задания типа отслеживаемых событий на входах INTx используется регистр EICRA (External Interrupt Control Register A). Назначение его битов следующее:
Значения битов ISC00 и ISC01 определяют события какого типа приводят к генерации прерывания INT0:
- 00 - при наличии сигнала низкого уровня;
- 01 - при изменении сигнала от высокого уровня к низкому и наоборот;
- 10 - при изменении сигнала от высокого уровня к низкому;
- 11 - при изменении сигнала от низкого уровня к высокому.
И последний, третий, регистр, имеющий отношение к обработке внешних прерываний - это регистр флагов EIFR (External Interrupt Flag Register):
Бит INTF0 устанавливается в 1 при выполнении условия генерации прерывания на входе INT0 в соответствии с конфигурацией битов ISC0x регистра EICRA. Затем, если обработка прерываний от INT0 разрешена и бит I установлен в 1, то будет выполнен соответствующий обработчик и значение бита будет сброшено. Также значение бита INTF0 можно сбросить программно, записав в него значение "1". Если конфигурацией битов ISC0x регистра EICRA определено генерировать прерывания при наличии низкого уровня на входе INT0, то данный флаг в обработке не задействуется и в нем будет значение "0".
Бит INTF1 аналогичен INTF0, но сигнализирует об обнаружении запроса прерывания на входе INT1.
Итак, для разрешения внешних прерываний и задания режима их обработки необходимо выполнить следующие действия:
- Задать обработчик, используя ключевое слово ISR.
- Определить тип событий на входе, генерирующих запрос прерывания (регистр EICRA).
- Разрешить обработку внешнего прерывания (регистр EIMSK).
- Установить бит 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):
- PCIE0 - значение "1" в этом бите разрешает обработку прерываний группы PCINT0.
- PCIE1 - значение "1" в этом бите разрешает обработку прерываний группы PCINT1.
- PCIE2 - значение "1" в этом бите разрешает обработку прерываний группы PCINT2.
Назначение битов регистра PCIFR (Pin Change Interrupt Flag Register):
- 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 нет в принципе.
Для разрешения прерываний при изменении состояния вывода необходимо выполнить следующие действия:
- Задать обработчик для соответствующего прерывания PCINT, используя макрос ISR.
- Разрешить генерацию прерываний интересующим выводом микроконтроллера (регистр группы PCMSKx).
- Разрешить обработку прерывания PCINT, которое генерирует интересующий вывод (регистр PCICR).
- Установить бит 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.
Для проверки скетча установите кнопки между указанными выводами и землей. Поскольку запрос прерывания генерируется при изменении состояния вывода, то обработчик будет вызываться как при нажатии, так и при отпускании кнопки. Но здесь это не принципиально.
Вообще генерация прерывания для пробуждения микроконтроллера не единственная функция сторожевого таймера. Он может использоваться как простой таймер, если не требуется точность отмеряемых интервалов времени. В режиме генерации сигнала сброса сторожевой таймер используют когда возможно зависание системы: в ходе нормальной работы программа регулярно сбрасывает сторожевой таймер, предотвращая сброс микроконтроллера; в случае зависания программы по истечении заданного времени сторожевой таймер генерирует сигнал сброса. Это позволяет повысить надежность микроконтроллерной системы. Также возможно совмещение режимов генерации прерывания и сброса, в этом случае сначала будет вызван обработчик прерывания, что позволит сохранить важные данные, а затем при следующем таймауте WDT будет сгенерирован сигнал сброса микроконтроллера.
Управление работой сторожевого таймера осуществляется при помощи регистра WDTCSR.
Назначение битов регистра WDTCSR (Watchdog Timer Control Register):
Прерывания от сторожевого таймера
Рассмотрим еще один пример работы с прерываниями, на этот раз от сторожевого таймера (Watchdog Timer, WDT). Мы уже использовали прерывания WDT для выхода из спящего режима, теперь можем изучить их более детально.Вообще генерация прерывания для пробуждения микроконтроллера не единственная функция сторожевого таймера. Он может использоваться как простой таймер, если не требуется точность отмеряемых интервалов времени. В режиме генерации сигнала сброса сторожевой таймер используют когда возможно зависание системы: в ходе нормальной работы программа регулярно сбрасывает сторожевой таймер, предотвращая сброс микроконтроллера; в случае зависания программы по истечении заданного времени сторожевой таймер генерирует сигнал сброса. Это позволяет повысить надежность микроконтроллерной системы. Также возможно совмещение режимов генерации прерывания и сброса, в этом случае сначала будет вызван обработчик прерывания, что позволит сохранить важные данные, а затем при следующем таймауте WDT будет сгенерирован сигнал сброса микроконтроллера.
Управление работой сторожевого таймера осуществляется при помощи регистра WDTCSR.
Назначение битов регистра WDTCSR (Watchdog Timer Control Register):
- 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) | 1с |
0 | 1 | 1 | 1 | 256K (262144) | 2с |
1 | 0 | 0 | 0 | 512K (524288) | 4с |
1 | 0 | 0 | 1 | 1024K (1048576) | 8с |
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] необходимо выполнить следующие действия:
- Установить биты WDCE и WDE (вне зависимости от предыдущего значения WDE).
- В течение следующих четырех тактов установить нужное значение битов 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.
Что интересного есть в приведенном коде? Во-первых, мы удачно применили макрос EMPTY_INTERRUPT, рассмотренный ранее. Кроме того использование макросов из заголовочного файла wdt.h позволило сделать скетч короче. Но здесь есть и нюанс: вызов wdt_enable устанавливает бит WDE, что нам не нужно (нас интересует только прерывание). Поэтому мы сами устанавливаем бит WDIE, таким образом таймер сконфигурирован на генерацию прерывания и сброса. Это означает, что бит WDIE будет автоматически сбрасываться каждый раз при вызове обработчика и, если его не установить повторно, то следующий таймаут WDT уже приведет к сбросу микроконтроллера. По логике программы нам не нужен WDT после пробуждения, поэтому мы его останавливаем вызовом wdt_disable и сброс микроконтроллера нам не грозит. Но эту особенность нужно иметь в виду при работе со сторожевым таймером.
И напоследок фрагмент кода, который можно использовать для сброса микроконтроллера по таймауту сторожевого таймера:
#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); // Ожидаем сброс
Добрый день. Есть ли возможность переназначить прерывание INT1 на другой физический вход? Например с D3 на D4?
ОтветитьУдалитьДобрый день!
УдалитьНет. Это невозможно.
Подскажите правильно ли я понял. Мне нужно прерывание по D4. Получается для этого я могу использовать только PCINT2. Но у меня используется D0\D1 как СОМ порт и INT0. Тк PCINT2 активируется по любому изменению D0..D7 то обработчик прерывания будет постоянно вызываться. Есть ли способ отфильтровать вызов прерывания только по D4 и желательно только по фронту?
УдалитьПочитайте внимательнее про PCINT. Регистры PCMSKx отвечают за то, каким входам разрешено генерировать прерывания.
УдалитьДобрый день, благодарю за быстрый ответ. Разобрался с регистрами, не сразу понял что данная маска относиться только к текущему прерыванию.
ОтветитьУдалитьЕсли я правильно понял то Delay() и вызов процедур непосредственно из прерывания не работает. Подскажите как правильно сделать задержку выполнения программы на несколько секунд при активации прерывания. Вызов Sleep Mode так же приводит к зависанию. Возврат в основной код программы недопустим. или как альтернатива, если есть такая возможность, то возврат после прерывания в определенное место программы для дальнейшей обработки.
Ваша основная программа всё равно же вертится в каком-то цикле. Добавьте в нем проверку флага: когда он true, делайте задержку или то что нужно. А сам флаг устанавливайте в обработчике прерывания.
УдалитьВладимир, это понятно, но конкретно в моем случае периодически идет передача по COM порту и при появлении в цикле мне надо ее прервать. Пока вижу только вариант полного перезапуска с переходом на нулевой адрес.
УдалитьМоя рекомендация в силе:
Удалить1 Объявляете volatile переменную, например, f. Записываете в нее 0.
2 Внутри цикла передачи по COM порту добавляете условие if (f) break; При наличии внешних циклов добавляете в них такое же условие. Или сразу return вместо break, вам виднее.
3 Переменную f устанавливаете в 1 внутри обработчика прерывания.
Таким образом при нажатии на кнопку ваш цикл прервется и вы окажетесь за его пределами, где опять же можете проверить значение f, чтобы понять, что цикл прерван именно кнопкой, сбросить f в 0 и т.д.
Замечу, что нажатие на кнопку это "долгое событие" и его не обязательно отлавливать прерыванием. Можно добавить опрос кнопки в цикл обмена с ком портом, результат будет тот же.
Что касается выхода в произвольное место скетча, то это goto (локальный переход, т.е. в пределах функции) и setjmp, longjmp (для перемещения между функциями, в том числе из прерывания). goto не рекомендуется использовать в принципе. setjmp и longjmp на мой взгляд труднее для понимания чем предложенный ранее вариант.
Про кнопку уже сам додумал) Перепутал с другим вопросом.
УдалитьКлассно пишете) Очень полезный материал и доходчиво!
ОтветитьУдалитьСпасибо!
УдалитьДобавил себе в блокнотик, продолжайте в том же духе! 73 UA6EM
ОтветитьУдалитьЗдравствуйте! Спасибо за статью
ОтветитьУдалить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 , это режим отлова перехода с высокого на низкий уровень? или туда обратно? Спасибо!
Добрый день!
УдалитьПохоже, я перепутал биты. Для FALLING ISC01 должен быть установлен, а ISC00 - сброшен (10). Сейчас в них 01, получается что отслеживаем CHANGE. В примере поправлю, там нужен FALLING. Спасибо за наводку.
А так огромное спасибо!!! Всё компилится! Ещё вопросик, может некорректный конечно))) А можно предусмотреть несколько функций для внешнего прерывания? делаю робота, в одном случае прерывания планирую использовать для синхронизации колёс от датчиков холла на INT0 и INT1, в другом случае использовать прерывания для выравнивания робота от датчиков линии (ну например тумблер ставим на пин и меняем функционал), чтобы не прошивать каждый раз
УдалитьСамый простой способ - это использование для этих целей флага. Какая реакция на прерывание вам нужна, такое значение флага и устанавливаете. А внутри обработчика сделать проверку флага и выполнять соответствующий его значению код.
УдалитьДругой вариант - использовать указатель на функцию-обработчик. Вместо изменения значения флага будете изменять значение указателя, чтобы он ссылался то на одну, то на другую функцию. А внутри обработчика INT0 сделать вызов функции по этому указателю. По такому принципу происходит обработка внешних прерываний при использовании attachInterrupt.
Спасибо, буду экспериментировать!
УдалитьСпасибо большое за ваш труд и дай БОГ вам здоровья! -как-раз то что нужно!
ОтветитьУдалитьБлагодарю!
УдалитьИли я что-то путаю или в сводной таблице с регистрами PCMSK0-2 есть ошибки. Например PCINT0 судя по даташиту соответствует выводу 12, в таблице номер 14. Нужна проверка
ОтветитьУдалитьПолагаю, вы смотрите распиновку для контроллера в 32-выводном корпусе. А у меня в статье для DIP корпуса. Добавлю пометку.
УдалитьЕсть ардуино нано 328, есть нано 4808, при этом братья китайцы при запросе как шить отправляют к прошивке Everi но это 4809. Возникает вопрос как же шить скетч. при попытки просто сменить плату с нано 328 на Эвери ругается на MCUCR - что делать и что менять в скетче? (Скетч для "spot welder arduino" на 328 плату с выбором в прошивальщике 328 шьётся нормально, при установке 4208 и любых других вариантах в прошивальщике - ругань) При этом в скетче MCUCR упоминается 1 раз!!
ОтветитьУдалитьvoid reset_mcusr(void) __attribute__((naked)) __attribute__((section(".init3")));
ОтветитьУдалитьvoid reset_mcusr(void) {
MCUSR = 0;
wdt_disable();
Добрый день!
УдалитьНе могу ничего сказать по этому поводу, т.к. не работал с every
Здравствуйте! Хотябы почему MCUSR = 0
ОтветитьУдалитьи больше я не вижу где её использовали
Добрый вечер!
УдалитьСброс MCUSR в начале программы - это обычное дело для avr микроконтроллеров (для тех, в которых он есть).
А ругается потому, что ATmega4808/4809 не имеют такого регистра.
Этот комментарий был удален автором.
ОтветитьУдалитьЗдравствуйте. Большое спасибо за статью.
ОтветитьУдалитьЯ был бы очень благодарен если бы вы подсказали как будет выглядеть код для управления встроенным светодиодом отдельными кнопками для arduino mega.
Добрый день!
УдалитьВ целом он будет выглядеть так же. В плане внешних прерываний 2560 отличается от 328 бОльшим числом входов, поддерживающих их. Поэтому в нём два управляющих регистра: EICRA и EICRB. Остальное всё то же самое. Посмотрите даташит на ATmega2560, раздел 15.
Настройка PCINT, по-моему, и вовсе не отличается.
У меня нет готового примера для 2560. А писать и проверять, сами понимаете, никто не захочет, если вы сами не пробуете.
День добрый Владимир!
ОтветитьУдалитьВ одном из роликов на ютубе автор проводил сравнение процессоров avr и stm и озвучил, что в ARDUINO IDE неправильно реализована функция attachInterrupt(), лечится перед вызовом функции - EIFR = 0x01;
Он не только озвучил, но и показал наглядно на измерении временных отрезков логическим анализатором. Что-то можете сказать по этому поводу?
Добрый день!
УдалитьПрошу прощения за долгий ответ.
Да, есть такая особенность в реализации attachInterrupt. На гите неоднократно предлагалось исправить ее, добавив сброс флагов EIFR. Например, в этой теме: https://github.com/arduino/ArduinoCore-avr/issues/244
- отказ объяснили последующими проблемами с библиотекой Adafruit CC3300.
Утверждать, что это именно неправильная реализация - даже не знаю. Наверное, принимать решение о ложности/значимости флага должен сам программист и при необходимости сбрасывать его. Может разработчики придерживались такой позиции при написании функции? В любом случае сомневаюсь, что они тупо не учли тот факт, что EIFR/GIFR может быть установлен до вызова attachInterrupt и это сразу приведет к вызову обработчика
Добрый день! Очень хорошая статья! Спасибо!
ОтветитьУдалитьУ меня вопрос в главе "Очередность обработки прерываний".
Например, выполняется прерывание X первого типа и прерывания запрещены, в это время наступает прерывание Y, тоже первого типа. Соответственно, устанавливается флаг возникновения прерывания Y.
Если в это время, еще раз наступит прерывание Y, то что произойдет? Ничего? т.о одно прерывание будет потеряно?
Добрый день!
УдалитьВсё верно, прерывание будет потеряно.
Я загрузил этот пример
ОтветитьУдалить#include
...
wdt_enable(WDTO_15MS); // Сброс по WDT через ~16мс
while(1); // Ожидаем сброc
И больше не могу загружать в Ардуино никакие другие программы,
постоянно происходит сброс процессора. как выйти из этой ситуации