Почему необходимо использовать ключевое слово volatile для глобальных переменных при обработке прерываний в ардуино?
Я знаком с ключевым словом Volatile
, используемым для объявления переменных, которые совместно используются несколькими потоками в программном приложении (в основном в многопоточном приложении).
Но почему мне нужно объявлять переменную как изменчивую
при выполнении кода на прерывании Arduino? Выполняется ли код в прерывании в другом потоке? Что-то еще происходит?
@, 👍9
Обсуждение2 ответа
volatile
не имеет ничего общего с многопоточностью как таковой, и, похоже, это одно из наиболее часто используемых ключевых слов в этой области. Это никогда не заменяет правильную синхронизацию.
В соответствии со стандартом C, volatile
вызывает в основном поведение, определяемое реализацией, указывающее компилятору не оптимизировать доступ к памяти, в которой находится переменная, даже если компилятор считает, что другого не может быть.
Это может быть полезно в следующих ситуациях, когда компилятор не знает, что внешние средства могут изменить переменную:
- аппаратные регистры, сопоставленные с памятью
- переменные, доступные из других потоков, но проверяемые в узком цикле
- переменные, доступные из обработчиков прерываний или сигналов, но проверяемые в узком цикле
Во-первых, он летучий
, а не Летучий
. Я освещаю эти концепции на своей странице о прерываниях, однако, чтобы избежать ответа только по ссылке, я повторю соответствующие фрагменты.
Что такое "изменчивые" переменные?
Переменные, разделяемые между функциями ISR и обычными функциями, должны быть объявлены изменчивыми
. Это говорит компилятору, что такие переменные могут измениться в любое время, и поэтому компилятор должен перезагружать переменную всякий раз, когда вы ссылаетесь на нее, вместо того, чтобы полагаться на копию, которую она может иметь в регистре процессора.
Например:
volatile boolean flag;
// Процедура обслуживания прерываний (ISR)
void isr ()
{
flag = true;
} // end of isr
void setup ()
{
attachInterrupt (0, isr, CHANGE); // присоединить обработчик прерываний
} // end of setup
void loop ()
{
if (flag)
{
// произошло прерывание
}
} // end of loop
Критические разделы ... доступ к изменчивым переменным
Существуют некоторые тонкие проблемы, касающиеся переменных, которые являются общими для подпрограмм обслуживания прерываний (ISR) и основного кода (то есть кода, не содержащегося в ISR).
Поскольку ISR может сработать в любое время, когда включены прерывания, вам нужно быть осторожным при доступе к таким общим переменным, так как они могут обновляться в тот самый момент, когда вы получаете к ним доступ.
Первый... когда вы используете "изменчивые" переменные?
Переменная должна быть помечена как изменчивая только в том случае, если она используется как внутри ISR, так и за его пределами.
- Переменные, используемые только за пределами ISR, не должны быть изменчивыми.
- Переменные, используемые только внутри ISR, не должны быть изменчивыми.
- Переменные, используемые как внутри, так и вне ISR, должны быть изменчивыми.
напр..
volatile int counter;
Пометка переменной как изменчивой указывает компилятору не "кэшировать" содержимое переменной в регистре процессора, а всегда считывать ее из памяти, когда это необходимо. Это может замедлить обработку, поэтому вы не просто делаете каждую переменную изменчивой, когда в этом нет необходимости.
Атомарный доступ
Рассмотрим этот код:
volatile byte count;
ISR (TIMER1_OVF_vect)
{
count = 10;
}
void setup ()
{
}
void loop ()
{
count++;
}
Код, сгенерированный для count++
(добавьте 1 к count), выглядит следующим образом:
14c: 80 91 00 02 lds r24, 0x0200
150: 8f 5f subi r24, 0xFF <<---- проблема, если прерывание происходит до его выполнения
152: 80 93 00 02 sts 0x0200, r24 <<---- проблема, если прерывание происходит до его выполнения
(Обратите внимание, что он добавляет 1, вычитая -1)
Здесь есть две опасные точки, как указано. Если прерывание срабатывает после lds (загрузить регистр 24 с количеством переменных), но до sts (сохранить обратно в число переменных
), то переменная может быть изменена ISR (TIMER1_OVF_vect), однако это изменение теперь потеряно, поскольку вместо этого использовалась переменная в регистре.
Нам нужно защитить общую переменную, на короткое время отключив прерывания, например:
volatile byte count;
ISR (TIMER1_OVF_vect)
{
count = 10;
} // end of TIMER1_OVF_vect
void setup ()
{
} // end of setup
void loop ()
{
noInterrupts ();
count++;
interrupts ();
} // end of loop
Теперь обновление подсчета
производится "атомарно"... то есть его нельзя прервать.
Многобайтовые переменные
Давайте сделаем count
2-байтовой переменной и рассмотрим другую проблему:
volatile unsigned int count;
ISR (TIMER1_OVF_vect)
{
count++;
} // end of TIMER1_OVF_vect
void setup ()
{
pinMode (13, OUTPUT);
} // end of setup
void loop ()
{
if (count > 20)
digitalWrite (13, HIGH);
} // end of loop
Хорошо, мы больше не меняем
количество, так что все еще есть проблема? К сожалению, да. Давайте посмотрим на сгенерированный код для оператора "если":
172: 80 91 10 02 lds r24, 0x0210
176: 90 91 11 02 lds r25, 0x0211 <<---- problem if interrupt occurs before this is executed
17a: 45 97 sbiw r24, 0x15
17c: 50 f0 brcs .+20
Представьте, что счетчик
был 0xFFFF и собирался "обернуться" обратно к нулю. Мы загружаем 0xFF в один регистр, но перед загрузкой второго 0xFF переменная изменяется на 0x00. Теперь мы думаем, что count
равен 0x00FF, что не соответствует значению, которое у него было раньше (0xFFFF) или сейчас (0x0000).
Поэтому мы снова должны "защитить" доступ к общей переменной, как это:
void loop ()
{
noInterrupts ();
if (count > 20)
digitalWrite (13, HIGH);
interrupts ();
} // end of loop
Что делать, если вы не уверены, включены или выключены прерывания?
Здесь есть последнее "попался". Что, если прерывания уже могут быть отключены? Затем включать их снова после этого-плохая идея.
В этом случае вам нужно сохранить регистр состояния процессора следующим образом:
unsigned int getCount ()
{
unsigned int c;
byte oldSREG = SREG; // запомнить, включены или выключены прерывания
noInterrupts (); // отключить прерывания
c = count; // доступ к общей переменной
SREG = oldSREG; // снова включить прерывания, если они были включены раньше
return c; // вернуть копию общей переменной
} // end of getCount
Этот "безопасный" код сохраняет текущее состояние прерываний, отключает их (возможно, они уже отключены), переводит общую переменную во временную переменную, снова включает прерывания - если они были включены, когда мы вводили функцию - и затем возвращает копию общей переменной.
Краткие сведения
Код может "показаться работающим", даже если вы не предпримете вышеуказанных мер предосторожности. Это связано с тем, что вероятность прерывания, возникающего в совершенно неподходящий момент, довольно мала (возможно, 1 из 100, в зависимости от размера кода). Но рано или поздно это произойдет в неподходящий момент, и ваш код либо выйдет из строя, либо время от времени будет возвращать неправильные результаты.
Поэтому для надежного кода обратите внимание на защиту доступа к общим переменным.
Я думал, что volatile будет обрабатывать доступ к многобайтовым типам для чтения и записи в основном цикле и ISR! Итак, я был совершенно неправ и должен отключить прерывания, прежде чем обращаться к ним в основном цикле, верно?! ;-(, @Shamim
Верно. изменчивость
просто говорит компилятору не оптимизировать и сохранять переменные в регистрах процессора. У этого нет никаких других побочных эффектов (кроме того, что компилятор знает, что эти переменные могут измениться в любое время, поэтому изменчивые переменные никогда не оптимизируются, потому что они "не используются"). Так что да, как показано выше, "защитите" доступ к многобайтовым переменным, отключив прерывания., @Nick Gammon
Безопасно ли удалять "изменчивость", если мы используем окончательную версию " getCount ()"?, @Kevin Yuan
Я не понимаю, почему в строке (sbiw r24, 0x15) используется значение 0x15 вместо значения 20 в разделе "Многобайтовая переменная". Может ли это быть ошибкой?, @ecc
@ecc Ну, один-шестнадцатеричный, а другой-десятичный. Конечно, 0x15-это 21 в десятичной дроби, а не 20. Однако компилятор, возможно, решил вычесть 21 и проверить, был ли установлен бит переноса, вместо сравнения с 20. Это было бы частью оптимизации компилятора. Другими словами, > 20-это то же самое, что >>= 21., @Nick Gammon
- Серийное прерывание
- Влияет ли `millis()` на длинные ISR?
- Как прервать функцию цикла и перезапустить ее?
- Какие Arduino поддерживают ATOMIC_BLOCK?
- Прерывания внутри класса, связанные с функцией класса
- Аппаратное прерывание срабатывает случайным образом
- Какой правильный способ запроса устройства I2C из процедуры обслуживания прерывания?
- Чтение квадратурного энкодера в реальном времени с полным разрешением только с одним прерыванием на ATmega328
"Я знаком с ключевым словом Volatile". Затем забудьте об этом и прочитайте еще раз., @Eugene Sh.
Фактически обработчик прерываний является отдельным потоком от основного. Без объявления volatile компилятор не знает, что main должен перечитывать глобальный (который, возможно, был изменен обработчиком прерываний)., @MarkU
`Является ли код, выполняемый в прерывании, выполняемым в другом потоке " - ну, в некотором роде, только наоборот. Прерывания-это механизм, техника, используемая программистами ОС для реализации потоков. Поэтому нам нужно использовать "изменчивость" в многопоточном коде, потому что их контекст был переключен операционной системой в прерывании. Нам нужно использовать "изменчивость" в прерываниях просто потому, что прерывания выполняются в другом контексте., @slebetman