Определение volatile
volatile
сообщает компилятору, что значение переменной может измениться без ведома компилятора. Следовательно, компилятор не может предположить, что значение не изменилось только потому, что программа на Си, кажется, не изменила его.
С другой стороны, это означает, что значение переменной может требоваться (считываться) где-то еще, о чем компилятор не знает, следовательно, он должен убедиться, что каждое присвоение переменной фактически выполняется как операция записи.
Сценарии использования
volatile
требуется, когда
- представление аппаратных регистров (или ввод-вывод с отображением в память) в качестве переменных - даже если регистр никогда не будет прочитан, компилятор не должен просто пропустить операцию записи, думая: «Глупый программист. Пытается сохранить значение в переменной, которую он / она никогда не будет читать назад. Он / она даже не заметит, если мы пропустим запись ". И наоборот, даже если программа никогда не записывает значение в переменную, ее значение все равно может быть изменено аппаратно.
- разделение переменных между контекстами выполнения (например, ISR / основная программа) (см. ответ @ kkramo)
Эффекты volatile
Когда переменная объявлена, volatile
компилятор должен убедиться, что каждое присвоение ей в программном коде отражается в фактической операции записи, и что каждое чтение в программном коде считывает значение из (mmapped) памяти.
Для энергонезависимых переменных компилятор предполагает, что он знает, если / когда значение переменной изменяется, и может оптимизировать код различными способами.
Например, компилятор может уменьшить количество операций чтения / записи в память, сохраняя значение в регистрах ЦП.
Пример:
void uint8_t compute(uint8_t input) {
uint8_t result = input + 2;
result = result * 2;
if ( result > 100 ) {
result -= 100;
}
return result;
}
Здесь компилятор, вероятно, даже не выделит ОЗУ для result
переменной и никогда не будет хранить промежуточные значения где-либо, кроме как в регистре процессора.
Если бы оно result
было изменчивым, то каждое вхождение result
в коде C требовало бы от компилятора доступа к ОЗУ (или к порту ввода / вывода), что приводило к снижению производительности.
Во-вторых, компилятор может переупорядочивать операции над энергонезависимыми переменными для производительности и / или размера кода. Простой пример:
int a = 99;
int b = 1;
int c = 99;
может быть переоформлен на
int a = 99;
int c = 99;
int b = 1;
что может сохранить инструкцию на ассемблере, потому что значение 99
не нужно загружать дважды.
Если a
, b
и c
были неустойчивым компилятор должен излучать инструкции , которые присваивают значения в точном порядке , как они указаны в программе.
Другой классический пример выглядит так:
volatile uint8_t signal;
void waitForSignal() {
while ( signal == 0 ) {
// Do nothing.
}
}
Если бы в этом случае этого signal
не volatile
произошло, компилятор «подумал бы», что это while( signal == 0 )
может быть бесконечный цикл (потому что signal
он никогда не будет изменен кодом внутри цикла ) и может сгенерировать эквивалент
void waitForSignal() {
if ( signal != 0 ) {
return;
} else {
while(true) { // <-- Endless loop!
// do nothing.
}
}
}
Рассмотрим обработку volatile
значений
Как указано выше, volatile
переменная может вводить снижение производительности, когда к ней обращаются чаще, чем фактически требуется. Чтобы смягчить эту проблему, вы можете «энергонезависимо» значение путем присвоения энергонезависимой переменной, например
volatile uint32_t sysTickCount;
void doSysTick() {
uint32_t ticks = sysTickCount; // A single read access to sysTickCount
ticks = ticks + 1;
setLEDState( ticks < 500000L );
if ( ticks >= 1000000L ) {
ticks = 0;
}
sysTickCount = ticks; // A single write access to volatile sysTickCount
}
Это может быть особенно полезно в ISR, где вы хотите быть как можно быстрее, не обращаясь к одному и тому же оборудованию или памяти несколько раз, когда вы знаете, что это не нужно, потому что значение не изменится во время работы вашего ISR. Это часто встречается, когда ISR является «источником» значений для переменной, как sysTickCount
в приведенном выше примере. На AVR было бы особенно больно иметь doSysTick()
доступ к тем же четырем байтам в памяти (четыре инструкции = 8 циклов ЦП на доступ sysTickCount
) пять или шесть раз, а не только дважды, потому что программист знает, что значение не будет быть измененным из некоторого другого кода, пока его / ее doSysTick()
работает.
С помощью этого трюка вы, по сути, делаете то же самое, что делает компилятор для энергонезависимых переменных, то есть читаете их из памяти только тогда, когда это необходимо, сохраняете значение в регистре в течение некоторого времени и записываете обратно в память только тогда, когда это необходимо. ; но на этот раз вы знаете лучше, чем компилятор, если / когда должно произойти чтение / запись , поэтому вы освобождаете компилятор от этой задачи оптимизации и делаете это самостоятельно.
Ограничения volatile
Неатомарный доступ
volatile
никак не обеспечивает атомарный доступ к переменным из нескольких слов. В этих случаях вам потребуется обеспечить взаимное исключение другими способами, помимо использования volatile
. На AVR вы можете использовать ATOMIC_BLOCK
от <util/atomic.h>
или простых cli(); ... sei();
звонков. Соответствующие макросы также действуют как барьер памяти, что важно, когда речь идет о порядке доступа:
Порядок исполнения
volatile
налагает строгий порядок выполнения только по отношению к другим переменным переменным. Это означает, что, например,
volatile int i;
volatile int j;
int a;
...
i = 1;
a = 99;
j = 2;
гарантируется сначала присвоить 1 для, i
а затем назначить 2 для j
. Тем не менее, не гарантируется, что a
будет назначен между ними; компилятор может выполнить это назначение до или после фрагмента кода, в основном в любое время до первого (видимого) чтения a
.
Если бы не барьер памяти вышеупомянутых макросов, компилятору было бы разрешено переводить
uint32_t x;
cli();
x = volatileVar;
sei();
в
x = volatileVar;
cli();
sei();
или же
cli();
sei();
x = volatileVar;
(Ради полноты я должен сказать, что барьеры памяти, подобные тем, которые подразумеваются макросами sei / cli, могут фактически исключить использование volatile
, если все доступы заключены в скобки с этими барьерами.)