Я читал некоторые статьи и ответы на Stack Exchange об использовании volatile
ключевого слова для предотвращения применения компилятором каких-либо оптимизаций к объектам, которые могут изменяться способами, которые не могут быть определены компилятором.
Если я читаю из АЦП (давайте назовем переменную adcValue
) и объявляю эту переменную глобальной, следует ли мне использовать ключевое слово volatile
в этом случае?
Без использования
volatile
ключевого слова// Includes #include "adcDriver.h" // Global variables uint16_t adcValue; // Some code void readFromADC(void) { adcValue = readADC(); }
Используя
volatile
ключевое слово// Includes #include "adcDriver.h" // Global variables volatile uint16_t adcValue; // Some code void readFromADC(void) { adcValue = readADC(); }
Я задаю этот вопрос, потому что при отладке я не вижу различий между обоими подходами, хотя лучшие практики говорят, что в моем случае (глобальная переменная, которая изменяется непосредственно от аппаратного обеспечения), тогда использование volatile
обязательно.
источник
if(x==1) x=1;
запись может быть оптимизирована для энергонезависимойx
и не может быть оптимизирована, если онаx
является энергозависимой. OTOH, если для доступа к внешним устройствам требуются специальные инструкции, вы можете добавить их (например, если необходимо выполнить запись в диапазон памяти).Ответы:
Определение
volatile
volatile
сообщает компилятору, что значение переменной может измениться без ведома компилятора. Следовательно, компилятор не может предположить, что значение не изменилось только потому, что программа на Си, кажется, не изменила его.С другой стороны, это означает, что значение переменной может требоваться (считываться) где-то еще, о чем компилятор не знает, следовательно, он должен убедиться, что каждое присвоение переменной фактически выполняется как операция записи.
Сценарии использования
volatile
требуется, когдаЭффекты
volatile
Когда переменная объявлена,
volatile
компилятор должен убедиться, что каждое присвоение ей в программном коде отражается в фактической операции записи, и что каждое чтение в программном коде считывает значение из (mmapped) памяти.Для энергонезависимых переменных компилятор предполагает, что он знает, если / когда значение переменной изменяется, и может оптимизировать код различными способами.
Например, компилятор может уменьшить количество операций чтения / записи в память, сохраняя значение в регистрах ЦП.
Пример:
Здесь компилятор, вероятно, даже не выделит ОЗУ для
result
переменной и никогда не будет хранить промежуточные значения где-либо, кроме как в регистре процессора.Если бы оно
result
было изменчивым, то каждое вхождениеresult
в коде C требовало бы от компилятора доступа к ОЗУ (или к порту ввода / вывода), что приводило к снижению производительности.Во-вторых, компилятор может переупорядочивать операции над энергонезависимыми переменными для производительности и / или размера кода. Простой пример:
может быть переоформлен на
что может сохранить инструкцию на ассемблере, потому что значение
99
не нужно загружать дважды.Если
a
,b
иc
были неустойчивым компилятор должен излучать инструкции , которые присваивают значения в точном порядке , как они указаны в программе.Другой классический пример выглядит так:
Если бы в этом случае этого
signal
неvolatile
произошло, компилятор «подумал бы», что этоwhile( signal == 0 )
может быть бесконечный цикл (потому чтоsignal
он никогда не будет изменен кодом внутри цикла ) и может сгенерировать эквивалентРассмотрим обработку
volatile
значенийКак указано выше,
volatile
переменная может вводить снижение производительности, когда к ней обращаются чаще, чем фактически требуется. Чтобы смягчить эту проблему, вы можете «энергонезависимо» значение путем присвоения энергонезависимой переменной, напримерЭто может быть особенно полезно в ISR, где вы хотите быть как можно быстрее, не обращаясь к одному и тому же оборудованию или памяти несколько раз, когда вы знаете, что это не нужно, потому что значение не изменится во время работы вашего ISR. Это часто встречается, когда ISR является «источником» значений для переменной, как
sysTickCount
в приведенном выше примере. На AVR было бы особенно больно иметьdoSysTick()
доступ к тем же четырем байтам в памяти (четыре инструкции = 8 циклов ЦП на доступsysTickCount
) пять или шесть раз, а не только дважды, потому что программист знает, что значение не будет быть измененным из некоторого другого кода, пока его / ееdoSysTick()
работает.С помощью этого трюка вы, по сути, делаете то же самое, что делает компилятор для энергонезависимых переменных, то есть читаете их из памяти только тогда, когда это необходимо, сохраняете значение в регистре в течение некоторого времени и записываете обратно в память только тогда, когда это необходимо. ; но на этот раз вы знаете лучше, чем компилятор, если / когда должно произойти чтение / запись , поэтому вы освобождаете компилятор от этой задачи оптимизации и делаете это самостоятельно.
Ограничения
volatile
Неатомарный доступ
volatile
никак не обеспечивает атомарный доступ к переменным из нескольких слов. В этих случаях вам потребуется обеспечить взаимное исключение другими способами, помимо использованияvolatile
. На AVR вы можете использоватьATOMIC_BLOCK
от<util/atomic.h>
или простыхcli(); ... sei();
звонков. Соответствующие макросы также действуют как барьер памяти, что важно, когда речь идет о порядке доступа:Порядок исполнения
volatile
налагает строгий порядок выполнения только по отношению к другим переменным переменным. Это означает, что, например,гарантируется сначала присвоить 1 для,
i
а затем назначить 2 дляj
. Тем не менее, не гарантируется, чтоa
будет назначен между ними; компилятор может выполнить это назначение до или после фрагмента кода, в основном в любое время до первого (видимого) чтенияa
.Если бы не барьер памяти вышеупомянутых макросов, компилятору было бы разрешено переводить
в
или же
(Ради полноты я должен сказать, что барьеры памяти, подобные тем, которые подразумеваются макросами sei / cli, могут фактически исключить использование
volatile
, если все доступы заключены в скобки с этими барьерами.)источник
An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects.
больше людей должны читать его.cli
/sei
является слишком тяжелым решением, если ваша единственная цель - достичь барьера памяти, а не предотвратить прерывания. Эти макросы генерируют фактическиеcli
/sei
инструкции и дополнительно клоббер памяти, и именно этот клоббер приводит к барьеру. Чтобы иметь только барьер памяти без отключения прерываний, вы можете определить свой собственный макрос с похожим на тело текстом__asm__ __volatile__("":::"memory")
(т. Е. Пустой ассемблерный код с памятью).volatile
есть точка последовательности, и все после нее должно быть «упорядочено после». Это означает, что выражение является своего рода барьером памяти. Поставщики компиляторов решили распространять всевозможные мифы, чтобы возложить ответственность за барьеры памяти на программиста, но это нарушает правила «абстрактной машины».volatile data_t data = {0}; set_mmio(&data); while (!data.ready);
.Ключевое слово volatile сообщает компилятору, что доступ к переменной имеет наблюдаемый эффект. Это означает, что каждый раз, когда ваш исходный код использует переменную, компилятор ДОЛЖЕН создавать доступ к этой переменной. Будь то доступ для чтения или записи.
В результате этого любое изменение переменной вне нормального потока кода также будет отслеживаться кодом. Например, если обработчик прерывания меняет значение. Или, если переменная на самом деле является каким-то аппаратным регистром, который изменяется сам по себе.
Это большое преимущество также является его недостатком. Каждый отдельный доступ к переменной проходит через переменную, и значение никогда не сохраняется в регистре для более быстрого доступа в течение любого промежутка времени. Это означает, что переменная переменная будет медленной. Величины медленнее. Так что используйте volatile только там, где это действительно необходимо.
В вашем случае, насколько вы показали код, глобальная переменная изменяется только тогда, когда вы обновляете ее самостоятельно
adcValue = readADC();
. Компилятор знает, когда это произойдет, и никогда не будет хранить значение adcValue в регистре для чего-то, что может вызватьreadFromADC()
функцию. Или любую функцию, о которой он не знает. Или все, что будет манипулировать указателями, которые могут указыватьadcValue
и тому подобное. В действительности нет необходимости в volatile, поскольку переменная никогда не меняется непредсказуемым образом.источник
volatile
все только потому , что вы также не должны уклоняться от этого в тех случаях, когда вы считаете, что это обоснованно необходимо из-за упреждающих проблем с производительностью.Основное использование ключевого слова volatile во встроенных приложениях C - это пометка глобальной переменной, которая записывается в обработчик прерываний. Это, конечно, не обязательно в этом случае.
Без этого компилятор не может доказать, что значение когда-либо записывается после инициализации, потому что он не может доказать, что обработчик прерываний когда-либо вызывался. Поэтому он думает, что может оптимизировать переменную из существования.
источник
Существуют два случая, когда вы должны использовать
volatile
во встроенных системах.При чтении из аппаратного реестра.
Это означает, что отображаемый в памяти регистр сам является частью аппаратных периферийных устройств внутри MCU. Скорее всего, у него будет какое-то загадочное имя, например "ADC0DR". Этот регистр должен быть определен в коде C, либо через некоторую карту регистров, предоставленную поставщиком инструмента, либо самостоятельно. Чтобы сделать это самостоятельно, вы должны сделать (при условии 16-битного регистра):
где 0x1234 - это адрес, где MCU отобразил регистр. Так
volatile
как уже является частью вышеупомянутого макроса, любой доступ к нему будет волатильно-квалифицированным. Так что этот код в порядке:При совместном использовании переменной между ISR и соответствующим кодом используется результат ISR.
Если у вас есть что-то вроде этого:
Тогда компилятор может подумать: «adc_data всегда 0, потому что он нигде не обновляется. И эта функция ADC0_interrupt () никогда не вызывается, поэтому переменная не может быть изменена». Компилятор обычно не понимает, что прерывания вызываются аппаратным, а не программным обеспечением. Таким образом, компилятор отправляет и удаляет код,
if(adc_data > 0){ do_stuff(adc_data); }
поскольку считает, что он никогда не может быть правдивым, вызывая очень странную и трудную для отладки ошибку.Объявляя
adc_data
volatile
, компилятору не разрешается делать такие предположения, и ему не разрешается оптимизировать доступ к переменной.Важные заметки:
ISR всегда должен быть объявлен внутри драйвера оборудования. В этом случае АЦП АЦП должен находиться внутри драйвера АЦП. Никто, кроме водителя, не должен связываться с ISR - все остальное - программирование спагетти.
При написании C вся связь между ISR и фоновой программой должна быть защищена от условий гонки. Всегда , каждый раз, без исключений. Размер шины данных MCU не имеет значения, потому что даже если вы делаете одну 8-битную копию в C, язык не может гарантировать атомарность операций. Нет, если вы не используете функцию C11
_Atomic
. Если эта функция недоступна, вы должны использовать какой-либо семафор или отключить прерывание во время чтения и т. Д. Другой вариант - встроенный ассемблер.volatile
не гарантирует атомарность.Что может случиться так:
-load значение из стека в регистр
-Interrupt происходит
-use значение из регистра
И тогда не имеет значения, является ли часть «use value» отдельной инструкцией сама по себе. К сожалению, значительная часть всех программистов встраиваемых систем не замечает этого, вероятно, делая это самой распространенной ошибкой встраиваемых систем. Всегда непостоянно, трудно спровоцировать, трудно найти.
Пример правильно написанного драйвера ADC будет выглядеть следующим образом (при условии, что C11
_Atomic
недоступен):adc.h
adc.c
Этот код предполагает, что прерывание не может быть прервано само по себе. В таких системах простой логический тип может действовать как семафор, и он не должен быть атомарным, поскольку не будет никакого вреда, если прерывание произойдет до того, как логическое значение будет установлено. Недостатком вышеуказанного упрощенного метода является то, что он будет отбрасывать показания АЦП при возникновении условий гонки, используя вместо этого предыдущее значение. Этого также можно избежать, но тогда код становится более сложным.
Здесь
volatile
защищает от ошибок оптимизации. Это не имеет ничего общего с данными, полученными из аппаратного регистра, только то, что данные передаются ISR.static
защищает от программирования спагетти и загрязнения пространства имен, делая переменную локальной для драйвера. (Это хорошо в одноядерных, однопоточных приложениях, но не в многопоточных.)источник
semaphore
определенно должно бытьvolatile
! На самом деле, это самый основной случай использования требует которым : Сигнальный что - то из одного контекста исполнения в другой. - В вашем примере компилятор может просто пропустить, потому что он «видит», что его значение никогда не читается, пока не будет перезаписано .volatile
semaphore = true;
semaphore = false;
В фрагментах кода, представленных в вопросе, пока нет причин использовать volatile. Не имеет значения, что значение
adcValue
исходит от АЦП. АadcValue
глобальность должна вызывать у вас подозрения относительно того,adcValue
должна ли она быть нестабильной, но сама по себе это не причина.Быть глобальным - это ключ, потому что он открывает возможность
adcValue
доступа к более чем одному контексту программы., Программный контекст включает в себя обработчик прерываний и задачу RTOS. Если глобальная переменная изменяется одним контекстом, то другие контексты программы не могут предполагать, что они знают значение из предыдущего доступа. Каждый контекст должен перечитывать значение переменной каждый раз, когда они его используют, потому что значение могло быть изменено в другом программном контексте. Программный контекст не знает, когда происходит прерывание или переключение задач, поэтому он должен предполагать, что любые глобальные переменные, используемые несколькими контекстами, могут изменяться между любыми доступами к переменной из-за возможного переключения контекста. Это то, для чего изменчивая декларация. Он сообщает компилятору, что эта переменная может меняться вне вашего контекста, поэтому читайте ее при каждом доступе и не думайте, что вы уже знаете значение.Если переменная отображается в памяти на аппаратный адрес, то изменения, сделанные аппаратным обеспечением, фактически являются другим контекстом вне контекста вашей программы. Таким образом, отображение памяти также является ключом. Например, если ваша
readADC()
функция обращается к отображенному в памяти значению, чтобы получить значение АЦП, то эта отображаемая в памяти переменная, вероятно, должна быть энергозависимой.Итак, возвращаясь к вашему вопросу, если в вашем коде есть что-то еще и
adcValue
к нему обращается другой код, работающий в другом контексте, то да, онadcValue
должен быть изменчивым.источник
То, что значение поступает из какого-то аппаратного регистра АЦП, не означает, что оно «напрямую» изменяется аппаратно.
В вашем примере вы просто вызываете readADC (), который возвращает некоторое значение регистра ADC. Это нормально по отношению к компилятору, зная, что adcValue назначено новое значение в этой точке.
Было бы иначе, если бы вы использовали подпрограмму прерывания ADC для назначения нового значения, которое вызывается, когда новое значение ADC готово. В этом случае компилятор не будет знать, когда вызывается соответствующий ISR, и может решить, что adcValue не будет доступен таким образом. Здесь волатильность поможет.
источник
Поведение
volatile
аргумента во многом зависит от вашего кода, компилятора и выполненной оптимизации.Есть два варианта использования, где я лично использую
volatile
:Если есть переменная, которую я хочу просмотреть с помощью отладчика, но компилятор оптимизировал ее (значит, удалил ее, потому что обнаружил, что эта переменная не нужна), добавление
volatile
заставит компилятор сохранить ее и, следовательно, можно увидеть на отладке.Если переменная может измениться «вне кода», как правило, если у вас есть какое-то оборудование, обращающееся к ней, или если вы сопоставляете переменную непосредственно с адресом.
Кроме того, во встроенных системах в компиляторах иногда возникают некоторые ошибки, которые делают оптимизацию, которая на самом деле не работает, а иногда
volatile
может решить проблемы.Учитывая, что ваша переменная объявлена глобально, она, вероятно, не будет оптимизирована, пока переменная используется в коде, по крайней мере, написана и прочитана.
Пример:
В этом случае переменная, вероятно, будет оптимизирована для printf ("% i", 1);
не будет оптимизирован
Другой:
В этом случае компилятор может оптимизировать с помощью (если вы оптимизируете по скорости) и, таким образом, отбрасывать переменную
В вашем случае «это может зависеть» от остальной части вашего кода, от того, как
adcValue
он используется в другом месте и от версии / настроек оптимизации компилятора, которые вы используете.Иногда может быть неприятно иметь код, который работает без оптимизации, но ломается после оптимизации.
Это может быть оптимизировано для printf ("% i", readADC ());
-
Они, вероятно, не будут оптимизированы, но вы никогда не знаете, «насколько хорош компилятор», и могут измениться в зависимости от параметров компилятора. Обычно компиляторы с хорошей оптимизацией лицензируются.
источник
volatile
заставляет компилятор хранить переменную в ОЗУ и обновлять эту ОЗУ, как только значение будет присвоено переменной. В большинстве случаев компилятор не «удаляет» переменные, потому что мы обычно не пишем назначения без эффекта, но он может решить сохранить переменную в каком-либо регистре ЦП и может позже или никогда не записать значение этого регистра в ОЗУ. Отладчики часто не могут найти регистр ЦП, в котором хранится переменная, и, следовательно, не могут показать ее значение.Много технических объяснений, но я хочу сосредоточиться на практическом применении.
В
volatile
ключевых слов силы компилятор для чтения или записи значения переменной из памяти каждый раз , когда он используется. Обычно компилятор пытается оптимизировать, но не делать ненужных операций чтения и записи, например, сохраняя значение в регистре ЦП, а не обращаясь к памяти каждый раз.Это имеет два основных использования во встроенном коде. Во-первых, он используется для аппаратных регистров. Аппаратные регистры могут изменяться, например, регистр результата АЦП может записываться периферийным устройством АЦП. Аппаратные регистры также могут выполнять действия при доступе. Типичным примером является регистр данных UART, который часто очищает флаги прерываний при чтении.
Компилятор обычно пытается оптимизировать повторное чтение и запись в регистр при условии, что значение никогда не изменится, поэтому нет необходимости продолжать к нему доступ, но
volatile
ключевое слово заставит его выполнять операцию чтения каждый раз.Второе общее использование для переменных, используемых как кодом прерывания, так и кодом без прерываний. Прерывания не вызываются напрямую, поэтому компилятор не может определить, когда они будут выполняться, и, следовательно, предполагает, что никакого доступа внутри прерывания никогда не происходит. Поскольку
volatile
ключевое слово заставляет компилятор обращаться к переменной каждый раз, это предположение удалено.Важно отметить, что
volatile
ключевое слово не является полным решением этих проблем, и необходимо избегать их. Например, в 8-битной системе 16-битная переменная требует двух обращений к памяти для чтения или записи, и, таким образом, даже если компилятор вынужден делать эти обращения, они происходят последовательно, и аппаратные средства могут действовать при первом доступе или прерывание произойдет между ними.источник
В отсутствие
volatile
квалификатора значение объекта может храниться в нескольких местах в течение определенных частей кода. Рассмотрим, например, что-то вроде:В первые дни C компилятор обрабатывал бы оператор
через шаги:
Более сложные компиляторы, однако, признают, что если значение «foo» хранится в регистре во время цикла, его нужно будет загрузить только один раз перед циклом и сохранить один раз после. Однако во время цикла это будет означать, что значение «foo» хранится в двух местах - в глобальном хранилище и в регистре. Это не будет проблемой, если компилятор может увидеть все способы, которыми «foo» может быть доступен в цикле, но может вызвать проблемы, если значение «foo» доступно в каком-то механизме, о котором компилятор не знает ( такой как обработчик прерываний).
Возможно, авторы Стандарта могли бы добавить новый классификатор, который бы явным образом пригласил компилятор сделать такую оптимизацию, и сказать, что старомодная семантика будет применяться при его отсутствии, но случаи, когда оптимизации полезны, значительно превосходят по численности. в тех случаях, когда это было бы проблематично, поэтому стандарт вместо этого позволяет компиляторам предполагать, что такие оптимизации безопасны при отсутствии доказательств того, что это не так. Цель
volatile
ключевого слова - предоставить такие доказательства.Пара споров между некоторыми авторами компиляторов и программистами возникает в таких ситуациях:
Исторически сложилось так, что большинство компиляторов допускают возможность того, что запись места
volatile
хранения может вызвать произвольные побочные эффекты, и избегают кэширования любых значений в регистрах в таком хранилище, иначе они будут воздерживаться от кэширования значений в регистрах при вызовах функций, которые не квалифицированный как «встроенный», и, таким образом, будет записывать 0x1234 вoutput_buffer[0]
, настраивать параметры для вывода данных, ждать его завершения, затем записывать 0x2345output_buffer[0]
и продолжать оттуда. Стандарт не требует реализации для обработки акта хранения адресаoutput_buffer
вvolatile
квалифицированный указатель как признак того, что с ним что-то может произойти, означает, что компилятор не понимает, однако, потому что авторы думали, что компилятор, который авторы компиляторов, предназначенные для различных платформ и целей, распознают, будет выполнять эти задачи на этих платформах. без необходимости говорить. Следовательно, некоторые «умные» компиляторы, такие как gcc и clang, будут предполагать, что, хотя адресoutput_buffer
записывается в указатель с изменчивой квалификацией между двумя хранилищами вoutput_buffer[0]
, нет никаких оснований предполагать, что что-либо может заботиться о значении, которое содержится в этом объекте в то время.Кроме того, в то время как указатели, которые напрямую приводятся из целых чисел, редко используются для каких-либо целей, кроме манипулирования вещами способами, которые маловероятно понять компиляторам, стандарт снова не требует, чтобы компиляторы обрабатывали такие обращения как
volatile
. Следовательно,*((unsigned short*)0xC0001234)
«умные» компиляторы, такие как gcc и clang, могут пропустить первую запись в , потому что разработчики таких компиляторов скорее заявят, что код, который пренебрегает квалификацией таких вещей, какvolatile
«неработающие», чем признают, что совместимость с таким кодом полезна , Во многих заголовочных файлах, поставляемых поставщиком, не указываютсяvolatile
квалификаторы, а компилятор, совместимый с заголовочными файлами, поставляемыми поставщиком, полезнее, чем тот, который не является таковым.источник