volatile

volatile запрещает компилятору кешировать переменную — она всегда читается из памяти, что критично для флагов и счётчиков, изменяемых в обработчиках прерываний.

Полная статья

Описание

volatile — это квалификатор переменной, который говорит компилятору: «не оптимизируй обращения к этой переменной, каждый раз читай её прямо из памяти». Без него компилятор может решить, что раз переменная нигде в основном коде не меняется, то и перечитывать её незачем — и «запомнить» значение в регистре процессора. В результате основной код будет работать с устаревшей копией, даже если прерывание давно обновило реальное значение.

В Arduino volatile особенно важен для переменных-флагов и счётчиков, которые изменяются внутри обработчиков прерываний. Типичная схема: прерывание устанавливает флаг, а loop() проверяет его и реагирует. Если флаг объявлен без volatile, компилятор может полностью убрать проверку как «всегда ложную» — и программа перестанет реагировать на события.

Пример кода

volatile гарантирует, что изменение переменной changed внутри toggle() будет видно в loop().

    // Мигает светодиодом 1 с, если входной сигнал изменился
    // за предыдущую секунду.

    volatile byte changed = 0;

    void setup() {
      pinMode(LED_BUILTIN, OUTPUT);
      attachInterrupt(digitalPinToInterrupt(2), toggle, CHANGE);
    }

    void loop() {
      if (changed == 1) {
        // toggle() вызывается из прерываний!

        // Сбрасываем changed в 0
        changed = 0;

        // Мигаем светодиодом 200 мс
        digitalWrite(LED_BUILTIN, HIGH);
        delay(200);
        digitalWrite(LED_BUILTIN, LOW);
      }
    }

    void toggle() {
      changed = 1;
    }

Многобайтные переменные и прерывания

На 8-битных AVR-микроконтроллерах операция чтения переменной, занимающей больше одного байта, выполняется несколькими машинными командами. Например, int занимает 2 байта, а long — 4 байта. Если прерывание изменит такую переменную прямо между двумя командами чтения, основной код получит «склеенное» значение: старшие байты — от старого значения, младшие — от нового. Это трудноуловимая ошибка, которая проявляется редко и непредсказуемо.

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

    #include <util/atomic.h> // эта библиотека включает макрос ATOMIC_BLOCK.
    volatile int input_from_interrupt;

    // Где-то в коде, например внутри loop()
      ATOMIC_BLOCK(ATOMIC_RESTORESTATE) {
        // код с заблокированными прерываниями (последовательные атомарные операции не будут прерваны)
        int result = input_from_interrupt;
      }

Где применяется

  • Флаги событий от прерываний — кнопка нажата, пришёл байт по UART, сработал таймер.
  • Счётчики импульсов — энкодер, датчик потока, тахометр: прерывание инкрементирует счётчик, основной цикл его читает.
  • Флаги от внешних прерываний — детектирование фронта сигнала на цифровом входе через attachInterrupt().
  • Передача одного байта данных между обработчиком прерывания и основным кодом (для большего объёма нужна атомарная защита).

Практические замечания

  • volatile не делает операцию атомарной. Квалификатор только заставляет компилятор каждый раз обращаться к памяти, но не защищает от прерывания в середине многобайтного чтения или записи.
  • Для простых флагов из прерываний хорошо подходят однобайтные типы: volatile bool или volatile byte — они читаются и пишутся за одну машинную команду на AVR.
  • Обработчик прерывания должен быть максимально коротким: установить флаг, сохранить минимум данных и немедленно вернуться. Тяжёлую логику выносите в loop().
  • Для надёжного обмена многобайтными данными между основным кодом и прерыванием используйте атомарные блоки или временно отключайте прерывания через noInterrupts().
  • Если программа «не видит» изменений, сделанных в прерывании, первым делом проверьте, объявлена ли переменная как volatile.

Полезные материалы