View Categories

Прерывания

При программировании МК доступен один очень мощный инструмент - прерывания (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 или выполняет сложные долгие вычисления.

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

Подписаться
Уведомить о
guest

0 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Прокрутить вверх