Программирование прерываний

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

Не стоит также вызывать другие функции из функции-обработчика прерывания, поскольку компилятор в машинный код AVR использует соглашение о вызове функций "вызывающий сохраняет регистры", поэтому при вызове функции из функции-обработчика прерывания на функцию-обработчик возлагается ответственность - сохранить все регистры, которых в RISC-ядре микроконтроллера Arduino ни много, ни мало, а 32. Это означает, что код функции-обработчика сразу прирастает в размере на 64 команды, которые что ещё ужаснее - обращаются к ОЗУ, то есть каждая из этих команд выполняется за 2 машинных такта.

128 машинных тактов в случае Arduino - это 8 микросекунд. Чтобы определить, много это или мало, стоит рассмотреть типовой пример. Например, прерывание по переполнению таймера-счётчика 2 для того, чтобы генерировать сигнал на PWM-выводе с частотой дискретизации 15625 КГц, вызывается 62500 раз в секунду. Если добавить в функцию-обработчик вызов функции, то только на сохранение регистров будет потрачена половина временного интервала между прерываниями, что сразу замедлит работу программы в 2 раза. Иначе говоря, 8 микросекунд - это немного в сравнении например с секундой, но это очень много при сравнении например с 16 микросекундами.

Для того, чтобы определить обработчик прерывания, нужно сделать два действия:

  1. Написать функцию-обработчик

  2. Разрешить прерывание где-нибудь в коде основной программы - например в теле функции setup().

Чтобы компилятор знал, что данная функция является обработчиком прерывания, функцию следует определить особым образом. Например, я подключил кнопку к выводу Digital 2 через подтягивающий вывод к земле резистор (см. Как подключить к Arduino...) и хочу, чтобы при нажатии на эту кнопку зажигался светодиод на выводе Digital 13.

Для этого мне потребуется знать название вектора прерывания. Он называется INT0_vect. Соответственно, я определяю названия для выводов, переменную счётчика и функцию:

#define LEDPIN 13 // Вывод светодиода

#define BTNPIN 2 // Вывод кнопки

volatile int count = 0; // Переменная счётчика (volatile означает указание компилятору не оптимизировать код её чтения,

// поскольку её значение изменяется внутри обработчика прерывания)

ISR(INT0_vect)

{

count = 25; // Инициализировать счётчик

}

Cхема подключения кнопки для данного примера:

Схема подключения кнопки к выводу INT0 платы Aruino Duemilanove / Uno

Первая стадия выполнена. Вторая стадия потребует от меня чтения документации на микроконтроллер, в данном случае ATmega328p (скачать).

В документации я читаю главу 12, посвящённую внешним прерываниям (External Interrupts). В конце главы описаны управлящие биты в регистрах микроконтроллера.

Чтобы установить бит в регистре в значение 1, не изменяя значения других битов, используется команда вида:

регистр |= (1 << номер_бита);

А чтобы установить бит в регистре в значение 0, так же не изменяя значения других битов, используется команда вида:

регистр &= ~ (1 << номер_бита);

Для установки в 1 или 0 нескольких битов сразу биты объединяются оператором битового OR - символ | (вертикальная черта):

регистр |= (1 << номер_одного_бита) | (1 << номер_другого_бита) | (1 << номер_третьего_бита);

регистр &= ~ (1 << номер_одного_бита) | (1 << номер_другого_бита) | (1 << номер_третьего_бита);

Если часть битов в регистре надо установить в 1, а другую часть в 0, то это можно сделать только двумя командами - командой установки бита в 1 и командой обнуления бита.

Поскольку большинство битов определены в файле <iom328p.h>, то за редким исключением я могу использовать названия регистров и битов, которые указаны в документации.

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

// Режимы вызова прерывания INT0

#define INT0_SENSE_LOW_LEVEL 0 // Прерывание при низком уровне на выводе

#define INT0_SENSE_LEVEL_CHANGE 1 // Прерывание при изменении уровня

#define INT0_SENSE_FALLING_EDGE 2 // Прерывание по фронту на спад (когда 1 переходит в 0)

#define INT0_SENSE_RISING_EDGE 3 // Прерывание по фронту на подъём (когда 0 переходит в 1)


// Управляющая функция для прерывания INT0

// mode - режим вызова прерывания

// enable - разрешить/запретить прерывание

void int0Control (uint8_t mode, bool enable)

{

EIMSK &= ~ (1 << INT0); // Запретить прерывание (так как следующая команда устанавливает

// режим INT0_SENSE_LOW_LEVEL)

EICRA &= ~ (1 << ISC00) | (1 << ISC01); // Обнуляем биты ISC00 и ISC01 в регистре EICRA

EICRA |= mode; // Устанавливаем режим вызова прерывания INT0

if (enable)

EIMSK |= (1 << INT0); // Разрешить прерывание

}

Теперь можно написать остальную часть скетча. Функция setup():

void setup(){

pinMode(LEDPIN, OUTPUT); // Вывод светодиода в режим вывода

pinMode(BTNPIN, INPUT); // Вывод кнопки в режим ввода

int0Control(INT0_SENSE_RISING_EDGE, true); // Разрешить прерывание по фронту на подъём

// (в данном случае при нажатии на кнопку)

interrupts(); // Разрешить прерывания глобально

}

Функция loop():

void loop(){

if(count==0) {

digitalWrite(LEDPIN, LOW); // Выключить светодиод, если счётчик равен 0...

}

else {

digitalWrite(LEDPIN, HIGH); // ... иначе включить светодиод,

--count; // и уменьшить счётчик на 1.

}

delay(10); // Подумать 10 миллисекунд.

}

Таким образом у меня получилась следующая логика. Поскольку при нажатии на кнопку на выводе Digital 2 в данной схеме подключения происходит смена логического нуля на единицу (возрастающий фронт импульса), то соответственно на это событие и вызывается прерывание INT0_vect.

При каждом вызове прерывания происходит инициализация переменной-счётчика в значение 25, что в совокупности с задержкой в функции loop() соответствует времени в 250 мс. На это время будет включен светодиод по условиям проверки, выполняемой функцией loop() раз в 10 мс.

То есть, если нажать на кнопку и удерживать её нажатой, то светодиод всё равно будет выключен спустя 250 мс, как и в случае, если кнопку отпустить. А если попробовать понажимать кнопку с частотой, большей 4 Гц, то светодиод будет некоторое время удерживаться во включенном состоянии.

Код скетча можно скачать как файл INO.