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.