При программировании МК доступен один очень мощный инструмент - прерывания (interrupts). Прерывание - это событие, вызываемое периферией МК. Их может быть много разных - например изменение состояния пина, завершение отправки или получение байта данных по интерфейсу связи, срабатывание аппаратного таймера и так далее. Список прерываний и информация как их настроить есть в документации на конкретный МК.
В Arduino-ядре доступен только один тип прерываний - внешний (external interrupt, прерывание по сигналу с пина), про него есть отдельный урок
Механизм прерываний позволяет "подключить" функцию-обработчик, которая будет вызываться при наступлении соответствующего события. Эта функция вызывается непосредственно в тот момент, когда сработало прерывание - выполнение текущей инструкции прерывается (неважно, что это было - задержка, вычисление...), выполняется функция-обработчик прерывания, затем процессор возвращается к основному коду программы с того места, где прервался.
Прерывания позволяют асинхронно и независимо от основной программы делать некоторые вещи, не обращаясь к ним напрямую и не "опрашивая" их в главном цикле, например - отправить байт из буфера при завершении отправки предыдущего. На прерываниях может быть написана часть программы, которая работает независимо от главного цикла и выполняется с ней параллельно, без использования операционных систем и прочих высокоуровневых инструментов.
Обработчик прерывания #
Если код в обработчике прерывания выполняется относительно долго, то он будет создавать "задержку" в самых разных местах программы. Иногда это может быть критичным - например мы "вручную" имитируем какой-то интерфейс связи и передаём данные, или наоборот, ожидаем сигнал, и тут "прилетает" прерывание и сбивает нам тайминги своим долгим выполнением.
Отключение прерываний #
В особо критичных ко времени выполнения местах программы можно отключать прерывания, чтобы выполнение точно не прерывалось и занимало ожидаемое время - в Arduino есть функции noInterrupts() - запретить прерывания и interrupts() - разрешить их обратно. Если на период отключения прерываний произойдёт событие, которое провоцирует прерывание, то оно встанет в очередь и будет вызвано после разрешения прерываний в программе.
void loop() {
// ...
noInterrupts();
sendData(...);
interrupts();
// ...
}
В среде Arduino отключение прерываний приостанавливает счёт времени millis() и micros(). Если отключать прерывания часто и на большой срок (дольше чем одно переполнение таймера прерываний) - время начнёт отставать
Код в прерывании #
В обработчике прерывания нужно стараться не использовать долгих тяжёлых конструкций, вычислений и всего такого, что может сильно увеличить время выполнения. Обычно в обработчиках прерывания выполняют максимально простые действия, например:
- Поднять флаг, чтобы сообщить в основную программу о событии
- Запомнить время срабатывания прерывания
- Загрузить в буфер следующий байт
- Продвинуть машину состояний
Обработку самих данных нужно производить уже в основной программе, чтобы не занимать время в прерывании.
Внутри прерывания:
- Не работают задержки типа
delay() - Не меняет своё значение
millis()иmicros() - Нужно стараться делать как можно меньше вычислений и вообще "долгих" действий:
- Вычисления с
float - Работа с динамической памятью (
new,malloc()и прочие) - Работа со
String-строками - Циклы ожидания
- Вычисления с
volatile #
Если переменная изменяется в обработчике прерывания и читается в основной программе или наоборот, то нужно помечать её как volatile: данный спецификатор сообщает компилятору, что доступ к этой переменной нельзя оптимизировать и переменную нужно хранить только в оперативной памяти, потому что переменная может измениться в "другом" месте программы (в прерывании). На практике процессор часто копирует переменные из оперативной памяти в свои регистры для более быстрого доступа, по сути - кеширует, а в обработчике прерывания эта связь теряется. Например, в прерывании поднимается флаг и проверяется в основном цикле:
bool flag = false;
void interrupt() {
flag = true;
}
void loop() {
if (flag) {
flag = false;
// ...
}
}
При выполнении этой программы на реальном процессоре переменная flag скорее всего закешируется в регистре, т.е. внутри loop чтение будет выполняться из регистра. А вот в прерывании мы меняем переменную в оперативной памяти, это особенность работы в прерывании. Таким образом, даже если прерывание изменит переменную, то в основной программе может остаться старое закешированное значение и условие никогда не выполнится. Во всех подобных ситуациях нужно помечать переменную как volatile, т.е.
volatile bool flag = false;
// ...
чтобы переменная всегда читалась и писалась из оперативной памяти.
Операции с volatile переменными в большинстве случаев выполняются медленнее, но это плата за использование механизма прерываний
Если переменная пишется и читается только в прерывании или только в основной программе, то нет смысла делать её volatile
Атомарный доступ #
Прерывание может произойти между любыми двумя инструкциями, а в случае с 8-битными процессорами (AVR, например Arduino Nano) - даже между чтениями байтов многобайтовой переменной, т.к. регистры у процессора 8-битные и он не может прочитать всю переменную за одно действие. Условно в такой программе
volatile uint32_t ms = 0;
void interrupt() {
// запомнить время прерывания
ms = millis();
}
void loop() {
// прошло времени с последнего прерывания
uint32_t lastIsr = millis() - ms;
}
Прерывание может произойти между чтениями байтов переменной ms перед операцией вычитания! То есть прочитается первый байт, произойдёт прерывание, в котором переменная изменится, затем прочитается второй байт уже нового её значения и в итоге получится некорректный результат. Для правильного чтения и записи многобайтовых переменных, которые изменяются или читаются в прерывании, нужно либо отключать прерывания на период доступа, либо использовать атомарный доступ к памяти. Второй способ лучше, но реализуется по-разному на разных архитектурах и процессорах, поэтому в рамках Ардуино-фреймворка проще использовать запрет прерываний. Исправим пример:
void loop() {
// безопасное чтение во временную переменную
noInterrupts();
uint32_t tms = ms;
interrupts();
// прошло времени с последнего прерывания
uint32_t lastIsr = millis() - tms;
}
То же самое в обратную сторону: если прерывание читает, а основная программа - изменяет:
volatile uint32_t val = 0;
void interrupt() {
if (val == 123);
}
void loop() {
// безопасная запись
noInterrupts();
val = random();
interrupts();
}
Таким образом, любой обмен многобайтными значениями между основной прграммой и прерыванием нужно оборачивать в запрет прерываний
Прерывание в прерывании #
Если внутри обработки функции прерывания сработает другое прерывание - тут могут быть варианты: некоторые процессоры поддерживают вложенные прерывания, некоторые - нет. Во втором случае прерывания встают в очередь согласно приоритету прерываний (зависит от МК) и начинают вызываться после обработки функции каждого следующего прерывания. Если вложенные прерывания поддерживаются - может получиться "программа в программе", то есть основная программа будет прерываться на некий код, который тоже может прерываться на другой код.
Прерывания в Arduino #
В Arduino-фреймворке для некоторых плат и МК изначально могут быть настроены и обрабатываться в системных файлах некоторые прерывания.
Например в AVR - по прерываниям аппаратного таймера ведётся счёт времени для функций millis() и micros() - если запретить прерывания - счёт аптайма остановится. В остальное время таймер работает и считает время, пока в программе выполняются вычисления, ожидаются задержки и так далее. Опять же в AVR например нет вложенных прерываний, поэтому внутри обработчика прерывания остальные прерывания запрещаются. Это означает, что если написать свой обработчик прерывания с долгим временем выполнения кода - аптайм может начать отставать от реального времени.
Или например Serial - он имеет буфер для входящих и исходящих данных, поэтому когда мы пишем Serial.print(..) - данные кладутся в буфер, что занимает минимальное время, а отправляются они уже позже - полностью асинхронно в прерываниях. Асинхронно работает и получение данных по UART - по прерыванию они кладутся в буфер приёма, поэтому Serial не пропускает данные даже когда программа ждёт задержку delay или выполняет сложные долгие вычисления.
Полезные страницы #
- Набор GyverKIT – наш большой стартовый набор Arduino, продаётся в России
- Каталог ссылок на дешёвые Ардуины, датчики, модули и прочие железки с AliExpress
- Обратная связь – сообщить об ошибке в уроке или предложить дополнение по тексту ([email protected])
- Поддержать автора за работу над уроками