Посмотр рубрик

Прерывания

При программировании МК доступен очень мощный инструмент - прерывания (interrupts). Прерывание - это событие, вызываемое периферией МК. Их может быть много разных - например изменение состояния пина, завершение отправки или получение байта данных по интерфейсу связи, срабатывание аппаратного таймера и так далее. Список прерываний и информация как их настроить есть в документации на конкретный МК.

В стандартном API Arduino напрямую вынесены внешние прерывания с пинов (attachInterrupt()), про них есть отдельный урок. Остальные прерывания - таймеров, интерфейсов связи и другой периферии - обычно использует само ядро и библиотеки

Механизм прерываний позволяет "подключить" функцию-обработчик, которая будет вызываться при наступлении соответствующего события. Эта функция вызывается непосредственно в тот момент, когда сработало прерывание - выполнение текущей инструкции прерывается (неважно, что это было - задержка, вычисление...), выполняется функция-обработчик прерывания, затем процессор возвращается к основному коду программы с того места, где прервался.

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

Обработчик прерывания #

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

Отключение прерываний #

В особо критичных ко времени выполнения местах программы можно отключать прерывания, чтобы выполнение точно не прерывалось - в Arduino есть функции noInterrupts() и interrupts(). Событие, произошедшее в это время, может оставить аппаратный флаг и обработаться позже, но полноценной бесконечной очереди обычно нет: повторные события могут склеиться или потеряться, а буферы - переполниться.

interrupts() безусловно разрешает прерывания, а не восстанавливает предыдущее состояние. Если функция может быть вызвана при уже запрещённых прерываниях, состояние нужно сохранять и восстанавливать средствами конкретной платформы

void loop() {
    // ...
    noInterrupts();
    sendData(...);
    interrupts();
    // ...
}

На классических AVR счётчик millis() не обновляется при запрещённых прерываниях. micros() некоторое время учитывает аппаратный счётчик таймера, но при пропущенных переполнениях тоже начнёт ошибаться. На других платформах реализация может отличаться

Код в прерывании #

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

  • Поднять флаг, чтобы сообщить в основную программу о событии
  • Запомнить время срабатывания прерывания
  • Загрузить в буфер следующий байт
  • Продвинуть машину состояний

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

Внутри прерывания:

  • Не работают задержки типа delay()
  • На AVR не обновляется millis(), а показания micros() при долгой обработке становятся ненадёжными
  • Нужно стараться делать как можно меньше вычислений и вообще "долгих" действий:
    • Вычисления с float
    • Работа с динамической памятью (new, malloc() и прочие)
    • Работа со String-строками
    • Циклы ожидания

volatile #

Если переменная изменяется в обработчике прерывания и читается в основной программе или наоборот, то обычно нужно помечать её как volatile: данный спецификатор сообщает компилятору, что обращения к переменной являются наблюдаемыми и её значение может измениться в "другом" месте программы. volatile не делает операции атомарными и не заменяет синхронизацию. Например, в прерывании поднимается флаг и проверяется в основном цикле:

bool flag = false;

void interrupt() {
    flag = true;
}

void loop() {
    if (flag) {
        flag = false;
        // ...
    }
}

Без volatile компилятор не обязан заново читать flag при каждой проверке и может считать, что значение не изменяется неожиданно. Поэтому основная программа рискует не заметить изменение из обработчика. Во всех подобных ситуациях нужно помечать переменную как 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();
}

Таким образом, обмен многобайтными значениями между основной программой и прерыванием на 8-битном МК нужно защищать критической секцией

Прерывание в прерывании #

Если внутри обработки функции прерывания сработает другое прерывание - тут могут быть варианты: некоторые процессоры поддерживают вложенные прерывания, некоторые - нет. Во втором случае аппаратные флаги ожидают завершения текущего обработчика, после чего процессор выбирает следующее прерывание по правилам приоритета конкретного МК. Несколько одинаковых событий могут объединиться в один флаг и потеряться. Если вложенные прерывания поддерживаются - может получиться "программа в программе", то есть основная программа будет прерываться на некий код, который тоже может прерываться на другой код.

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

Прерывания в Arduino #

В Arduino-фреймворке для некоторых плат и МК изначально могут быть настроены и обрабатываться в системных файлах некоторые прерывания.

Например в AVR - по прерываниям аппаратного таймера ведётся счёт времени для функций millis() и micros(). Если надолго запретить прерывания или написать долгий обработчик - переполнения таймера могут быть пропущены и аптайм начнёт отставать от реального времени.

Или например Serial - он имеет буфер для входящих и исходящих данных, поэтому при наличии свободного места Serial.print(..) быстро кладёт данные в буфер, а отправляются они уже позже в прерываниях. Если буфер передачи заполнится, Serial.print() будет ждать. Принятые по UART данные тоже кладутся в буфер из прерывания, но при долгой задержке основная программа может не успеть их прочитать и переполненный буфер начнёт терять новые байты.

Дополнительно #

Дополнительный контент доступен владельцам набора GyverKIT и по подписке, подробнее читай здесь. Блок содержит:

  • Вопросы, Задания

Полезные страницы #


(4 голоса)
Подписаться
Уведомить о
guest

6 комментариев
Старые
Новые Популярные
Прокрутить вверх