Использование volatile в разработке встроенного C


44

Я читал некоторые статьи и ответы на Stack Exchange об использовании volatile ключевого слова для предотвращения применения компилятором каких-либо оптимизаций к объектам, которые могут изменяться способами, которые не могут быть определены компилятором.

Если я читаю из АЦП (давайте назовем переменную adcValue) и объявляю эту переменную глобальной, следует ли мне использовать ключевое слово volatileв этом случае?

  1. Без использования volatileключевого слова

    // Includes
    #include "adcDriver.h"
    
    // Global variables
    uint16_t adcValue;
    
    // Some code
    void readFromADC(void)
    {
       adcValue = readADC();
    }
    
  2. Используя volatileключевое слово

    // Includes
    #include "adcDriver.h"
    
    // Global variables
    volatile uint16_t adcValue;
    
    // Some code
    void readFromADC(void)
    {
       adcValue = readADC();
    }
    

Я задаю этот вопрос, потому что при отладке я не вижу различий между обоими подходами, хотя лучшие практики говорят, что в моем случае (глобальная переменная, которая изменяется непосредственно от аппаратного обеспечения), тогда использование volatileобязательно.


1
Ряд сред отладки (конечно, gcc) не применяют оптимизацию. Производственная сборка обычно будет (в зависимости от вашего выбора). Это может привести к «интересным» различиям между сборками. Глядя на карту вывода компоновщика является информативным.
Питер Смит

22
«в моем случае (глобальная переменная, которая изменяется напрямую от аппаратного обеспечения)» - ваша глобальная переменная изменяется не аппаратно, а только вашим кодом C, о котором знает компилятор. - Аппаратный регистр, в котором АЦП предоставляет свои результаты, однако, должен быть нестабильным, поскольку компилятор не может знать, изменится ли его значение (когда оно изменится (если / когда оборудование АЦП
завершит

2
Вы сравнили ассемблер, сгенерированный обеими версиями? Это должно показать вам, что происходит под капотом
Mawg

3
@stark: BIOS? На микроконтроллере? Отображаемое в память пространство ввода-вывода будет не кешируемым (если в архитектуре вообще есть кеш данных, что не гарантировано) из-за согласованности проекта между правилами кэширования и картой памяти. Но volatile не имеет ничего общего с кэшем контроллера памяти.
Бен Фойгт

1
@Davislor Стандарт языка вообще не должен ничего говорить. Чтение в энергозависимый объект будет выполнять реальную загрузку (даже если компилятор недавно ее сделал и обычно будет знать, каково значение), а запись в такой объект будет выполнять реальное сохранение (даже если то же значение было прочитано из объекта ). Таким образом, if(x==1) x=1;запись может быть оптимизирована для энергонезависимой xи не может быть оптимизирована, если она xявляется энергозависимой. OTOH, если для доступа к внешним устройствам требуются специальные инструкции, вы можете добавить их (например, если необходимо выполнить запись в диапазон памяти).
любопытный парень

Ответы:


87

Определение 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, если все доступы заключены в скобки с этими барьерами.)


7
Хорошее обсуждение un-volatiling для производительности :)
awjlogan

3
Я всегда хотел бы упомянуть определение летучих в ISO / IEC 9899: 1999 6.7.3 (6): An object that has volatile-qualified type may be modified in ways unknown to the implementation or have other unknown side effects. больше людей должны читать его.
Jeroen3

3
Возможно, стоит упомянуть, что cli/ seiявляется слишком тяжелым решением, если ваша единственная цель - достичь барьера памяти, а не предотвратить прерывания. Эти макросы генерируют фактические cli/ seiинструкции и дополнительно клоббер памяти, и именно этот клоббер приводит к барьеру. Чтобы иметь только барьер памяти без отключения прерываний, вы можете определить свой собственный макрос с похожим на тело текстом __asm__ __volatile__("":::"memory")(т. Е. Пустой ассемблерный код с памятью).
Руслан

3
@NicHartley No. C17 5.1.2.3 §6 определяет наблюдаемое поведение : «Доступ к изменчивым объектам оценивается строго в соответствии с правилами абстрактной машины». Стандарт C не совсем ясно, где в целом необходимы барьеры памяти. В конце выражения, которое использует, volatileесть точка последовательности, и все после нее должно быть «упорядочено после». Это означает, что выражение является своего рода барьером памяти. Поставщики компиляторов решили распространять всевозможные мифы, чтобы возложить ответственность за барьеры памяти на программиста, но это нарушает правила «абстрактной машины».
Лундин

2
@JimmyB Локальный volatile может быть полезен для кода вроде volatile data_t data = {0}; set_mmio(&data); while (!data.ready);.
Мацей Пехотка

13

Ключевое слово volatile сообщает компилятору, что доступ к переменной имеет наблюдаемый эффект. Это означает, что каждый раз, когда ваш исходный код использует переменную, компилятор ДОЛЖЕН создавать доступ к этой переменной. Будь то доступ для чтения или записи.

В результате этого любое изменение переменной вне нормального потока кода также будет отслеживаться кодом. Например, если обработчик прерывания меняет значение. Или, если переменная на самом деле является каким-то аппаратным регистром, который изменяется сам по себе.

Это большое преимущество также является его недостатком. Каждый отдельный доступ к переменной проходит через переменную, и значение никогда не сохраняется в регистре для более быстрого доступа в течение любого промежутка времени. Это означает, что переменная переменная будет медленной. Величины медленнее. Так что используйте volatile только там, где это действительно необходимо.

В вашем случае, насколько вы показали код, глобальная переменная изменяется только тогда, когда вы обновляете ее самостоятельно adcValue = readADC();. Компилятор знает, когда это произойдет, и никогда не будет хранить значение adcValue в регистре для чего-то, что может вызвать readFromADC()функцию. Или любую функцию, о которой он не знает. Или все, что будет манипулировать указателями, которые могут указывать adcValueи тому подобное. В действительности нет необходимости в volatile, поскольку переменная никогда не меняется непредсказуемым образом.


6
Я согласен с этим ответом, но «медленнее» звучит слишком страшно.
Ккрамбо

6
В современных суперскалярных процессорах доступ к регистру ЦП возможен менее чем за один цикл процессора. С другой стороны, доступ к фактической не кэшированной памяти (помните, что некоторые внешние устройства могли бы изменить это, поэтому кэши ЦП не допускаются) может находиться в диапазоне 100-300 циклов ЦП. Так что да, величины. Не будет так плохо на AVR или подобном микроконтроллере, но вопрос не требует аппаратного обеспечения.
Госвин фон Бредерлоу

7
Во встроенных (микроконтроллерных) системах штраф за доступ к ОЗУ зачастую намного меньше. Например, AVR занимают только два цикла ЦП для чтения из или записи в ОЗУ (перемещение регистра-регистра занимает один цикл), поэтому экономия на хранении данных в регистрах приближается (но никогда не достигает) макс. 2 такта на доступ. - Конечно, условно говоря, сохранение значения из регистра X в RAM, а затем немедленная перезагрузка этого значения в регистр X для дальнейших вычислений займет 2x2 = 4 вместо 0 циклов (при простом сохранении значения в X) и, следовательно, будет бесконечно медленнее :)
JimmyB

1
Да, это «величины медленнее» в контексте «записи или чтения из определенной переменной». Однако в контексте полной программы, которая, вероятно, делает значительно больше, чем чтение / запись в одну переменную снова и снова, нет, не совсем. В этом случае общая разница, вероятно, «мала до незначительна». При утверждении о производительности следует проявлять осторожность, чтобы уточнить, относится ли это утверждение к одному конкретному оператору или к программе в целом. Замедление редко используемой операции с коэффициентом ~ 300х почти никогда не имеет большого значения.
Аромат

1
Вы имеете в виду, что последнее предложение? Это означает гораздо больше в том смысле, что «преждевременная оптимизация - корень зла». Очевидно, что вы не должны использовать volatileвсе только потому , что вы также не должны уклоняться от этого в тех случаях, когда вы считаете, что это обоснованно необходимо из-за упреждающих проблем с производительностью.
Аромат

9

Основное использование ключевого слова volatile во встроенных приложениях C - это пометка глобальной переменной, которая записывается в обработчик прерываний. Это, конечно, не обязательно в этом случае.

Без этого компилятор не может доказать, что значение когда-либо записывается после инициализации, потому что он не может доказать, что обработчик прерываний когда-либо вызывался. Поэтому он думает, что может оптимизировать переменную из существования.


2
Конечно, существуют и другие практические применения, но imho это наиболее распространенное.
Викачу

1
Если значение читается только в ISR (и изменяется от main ()), вам, возможно, придется также использовать volatile, чтобы гарантировать ATOMIC-доступ для многобайтовых переменных.
Rev 1.0

15
@ Rev1.0 Нет, volatile не гарантирует схожесть. Эта проблема должна рассматриваться отдельно.
Крис Страттон

1
В размещенном коде нет ни чтения с аппаратного обеспечения, ни прерываний. Вы предполагаете вещи из вопроса, которых там нет. На него нельзя ответить в его нынешнем виде.
Лундин

3
msgstr "пометить глобальную переменную, которая записывается в обработчик прерываний" Это помечать переменную; глобальный или иной; что это может быть изменено чем-то за пределами понимания компиляторов. Прерывание не требуется. Это может быть общая память или кто-то вставит в память зонд (последний не рекомендуется для чего-то более современного, чем 40 лет)
UKMonkey

9

Существуют два случая, когда вы должны использовать volatileво встроенных системах.

  • При чтении из аппаратного реестра.

    Это означает, что отображаемый в памяти регистр сам является частью аппаратных периферийных устройств внутри MCU. Скорее всего, у него будет какое-то загадочное имя, например "ADC0DR". Этот регистр должен быть определен в коде C, либо через некоторую карту регистров, предоставленную поставщиком инструмента, либо самостоятельно. Чтобы сделать это самостоятельно, вы должны сделать (при условии 16-битного регистра):

    #define ADC0DR (*(volatile uint16_t*)0x1234)

    где 0x1234 - это адрес, где MCU отобразил регистр. Так volatileкак уже является частью вышеупомянутого макроса, любой доступ к нему будет волатильно-квалифицированным. Так что этот код в порядке:

    uint16_t adc_data;
    adc_data = ADC0DR;
  • При совместном использовании переменной между ISR и соответствующим кодом используется результат ISR.

    Если у вас есть что-то вроде этого:

    uint16_t adc_data = 0;
    
    void adc_stuff (void)
    {
      if(adc_data > 0)
      {
        do_stuff(adc_data);
      } 
    }
    
    interrupt void ADC0_interrupt (void)
    {
      adc_data = ADC0DR;
    }

    Тогда компилятор может подумать: «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.h
#ifndef ADC_H
#define ADC_H

/* misc init routines here */

uint16_t adc_get_val (void);

#endif

adc.c

// adc.c
#include "adc.h"

#define ADC0DR (*(volatile uint16_t*)0x1234)

static volatile bool semaphore = false;
static volatile uint16_t adc_val = 0;

uint16_t adc_get_val (void)
{
  uint16_t result;
  semaphore = true;
    result = adc_val;
  semaphore = false;
  return result;
}

interrupt void ADC0_interrupt (void)
{
  if(!semaphore)
  {
    adc_val = ADC0DR;
  }
}
  • Этот код предполагает, что прерывание не может быть прервано само по себе. В таких системах простой логический тип может действовать как семафор, и он не должен быть атомарным, поскольку не будет никакого вреда, если прерывание произойдет до того, как логическое значение будет установлено. Недостатком вышеуказанного упрощенного метода является то, что он будет отбрасывать показания АЦП при возникновении условий гонки, используя вместо этого предыдущее значение. Этого также можно избежать, но тогда код становится более сложным.

  • Здесь volatileзащищает от ошибок оптимизации. Это не имеет ничего общего с данными, полученными из аппаратного регистра, только то, что данные передаются ISR.

  • staticзащищает от программирования спагетти и загрязнения пространства имен, делая переменную локальной для драйвера. (Это хорошо в одноядерных, однопоточных приложениях, но не в многопоточных.)


Трудно отлаживать относительно, если удалить код, вы заметите, что ваш ценный код исчез - это довольно смелое утверждение, что что-то не так. Но я согласен, могут быть очень странные и трудно отлаживаемые эффекты.
Арсенал

@Arsenal Если у вас есть хороший отладчик, который встроен в ассемблер с C, и вы знаете хотя бы немного asm, то да, это может быть легко обнаружить. Но для более сложного кода большая часть сгенерированного машиной asm не является простой задачей. Или если ты не знаешь asm. Или если ваш отладчик дерьмовый и не показывает asm (cougheclipsecough).
Лундин

Может быть, я немного избалован использованием отладчиков Lauterbach тогда. Если вы попытаетесь установить точку останова в оптимизированном коде, он установит ее где-то иначе, и вы знаете, что там что-то происходит.
Арсенал

@ Arsenal Да, тип смешанного C / Asm, который вы можете получить в Лаутербахе, ни в коем случае не является стандартным. Большинство отладчиков отображают asm в отдельном окне, если оно вообще есть.
Лундин

semaphoreопределенно должно быть volatile! На самом деле, это самый основной случай использования требует которым : Сигнальный что - то из одного контекста исполнения в другой. - В вашем примере компилятор может просто пропустить, потому что он «видит», что его значение никогда не читается, пока не будет перезаписано . volatilesemaphore = true;semaphore = false;
JimmyB

5

В фрагментах кода, представленных в вопросе, пока нет причин использовать volatile. Не имеет значения, что значение adcValueисходит от АЦП. А adcValueглобальность должна вызывать у вас подозрения относительно того, adcValueдолжна ли она быть нестабильной, но сама по себе это не причина.

Быть глобальным - это ключ, потому что он открывает возможность adcValueдоступа к более чем одному контексту программы., Программный контекст включает в себя обработчик прерываний и задачу RTOS. Если глобальная переменная изменяется одним контекстом, то другие контексты программы не могут предполагать, что они знают значение из предыдущего доступа. Каждый контекст должен перечитывать значение переменной каждый раз, когда они его используют, потому что значение могло быть изменено в другом программном контексте. Программный контекст не знает, когда происходит прерывание или переключение задач, поэтому он должен предполагать, что любые глобальные переменные, используемые несколькими контекстами, могут изменяться между любыми доступами к переменной из-за возможного переключения контекста. Это то, для чего изменчивая декларация. Он сообщает компилятору, что эта переменная может меняться вне вашего контекста, поэтому читайте ее при каждом доступе и не думайте, что вы уже знаете значение.

Если переменная отображается в памяти на аппаратный адрес, то изменения, сделанные аппаратным обеспечением, фактически являются другим контекстом вне контекста вашей программы. Таким образом, отображение памяти также является ключом. Например, если ваша readADC()функция обращается к отображенному в памяти значению, чтобы получить значение АЦП, то эта отображаемая в памяти переменная, вероятно, должна быть энергозависимой.

Итак, возвращаясь к вашему вопросу, если в вашем коде есть что-то еще и adcValueк нему обращается другой код, работающий в другом контексте, то да, он adcValueдолжен быть изменчивым.


4

«Глобальная переменная, которая меняется напрямую от оборудования»

То, что значение поступает из какого-то аппаратного регистра АЦП, не означает, что оно «напрямую» изменяется аппаратно.

В вашем примере вы просто вызываете readADC (), который возвращает некоторое значение регистра ADC. Это нормально по отношению к компилятору, зная, что adcValue назначено новое значение в этой точке.

Было бы иначе, если бы вы использовали подпрограмму прерывания ADC для назначения нового значения, которое вызывается, когда новое значение ADC готово. В этом случае компилятор не будет знать, когда вызывается соответствующий ISR, и может решить, что adcValue не будет доступен таким образом. Здесь волатильность поможет.


1
Поскольку ваш код никогда не «вызывает» функцию ISR, компилятор видит, что переменная обновляется только в функции, которую никто не вызывает. Таким образом, компилятор оптимизирует его.
Swanand

1
Это зависит от остальной части кода, если adcValue нигде не читается (например, читается только через отладчик), или если он читается только один раз в одном месте, компилятор, вероятно, оптимизирует его.
Дэмиен

2
@Damien: Это всегда "зависит", но я стремился ответить на реальный вопрос "Должен ли я использовать ключевое слово volatile в этом случае?" как можно короче.
Rev 1.0

4

Поведение volatileаргумента во многом зависит от вашего кода, компилятора и выполненной оптимизации.

Есть два варианта использования, где я лично использую volatile:

  • Если есть переменная, которую я хочу просмотреть с помощью отладчика, но компилятор оптимизировал ее (значит, удалил ее, потому что обнаружил, что эта переменная не нужна), добавление volatileзаставит компилятор сохранить ее и, следовательно, можно увидеть на отладке.

  • Если переменная может измениться «вне кода», как правило, если у вас есть какое-то оборудование, обращающееся к ней, или если вы сопоставляете переменную непосредственно с адресом.

Кроме того, во встроенных системах в компиляторах иногда возникают некоторые ошибки, которые делают оптимизацию, которая на самом деле не работает, а иногда volatileможет решить проблемы.

Учитывая, что ваша переменная объявлена ​​глобально, она, вероятно, не будет оптимизирована, пока переменная используется в коде, по крайней мере, написана и прочитана.

Пример:

void test()
{
    int a = 1;
    printf("%i", a);
}

В этом случае переменная, вероятно, будет оптимизирована для printf ("% i", 1);

void test()
{
    volatile int a = 1;
    printf("%i", a);
}

не будет оптимизирован

Другой:

void delay1Ms()
{
    unsigned int i;
    for (i=0; i<10; i++)
    {
        delay10us( 10);
    }
}

В этом случае компилятор может оптимизировать с помощью (если вы оптимизируете по скорости) и, таким образом, отбрасывать переменную

void delay1Ms()
{
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
       delay10us( 10);
}

В вашем случае «это может зависеть» от остальной части вашего кода, от того, как adcValueон используется в другом месте и от версии / настроек оптимизации компилятора, которые вы используете.

Иногда может быть неприятно иметь код, который работает без оптимизации, но ломается после оптимизации.

uint16_t adcValue;
void readFromADC(void)
{
  adcValue = readADC();
  printf("%i", adcValue);
}

Это может быть оптимизировано для printf ("% i", readADC ());

uint16_t adcValue;
void readFromADC(void)
{
  adcValue = readADC();
  printf("%i", adcValue);
  callAnotherFunction(adcValue);
}

-

uint16_t adcValue;
void readFromADC(void)
{
  adcValue = readADC();
  printf("%i", adcValue);
}

void anotherFunction()
{
   // Do something with adcValue
}

Они, вероятно, не будут оптимизированы, но вы никогда не знаете, «насколько хорош компилятор», и могут измениться в зависимости от параметров компилятора. Обычно компиляторы с хорошей оптимизацией лицензируются.


1
Например, а = 1; Ь = а; и с = b; компилятор может подумать, подождите минуту, a и b бесполезны, давайте просто поместим 1 в c напрямую. Конечно, вы не будете делать это в своем коде, но компилятор лучше, чем вы, обнаруживает их, даже если вы попытаетесь написать оптимизированный код сразу, это будет нечитаемо.
Дэмиен

2
Правильный код с правильным компилятором не сломается с включенными оптимизациями. Корректность компилятора - небольшая проблема, но по крайней мере с IAR я не сталкивался с ситуацией, когда оптимизация приводила к взлому кода, где это не должно происходить.
Арсенал

5
Во многих случаях, когда оптимизация нарушает код, вы тоже рискуете на территорию UB ..
pipe

2
Да, побочный эффект volatile заключается в том, что он может помочь при отладке. Но это не веская причина использовать изменчивый. Вероятно, вам следует отключить оптимизацию, если вашей целью является легкая отладка. Этот ответ даже не упоминает прерывания.
Ккрамбо

2
Добавление к аргументу отладки volatileзаставляет компилятор хранить переменную в ОЗУ и обновлять эту ОЗУ, как только значение будет присвоено переменной. В большинстве случаев компилятор не «удаляет» переменные, потому что мы обычно не пишем назначения без эффекта, но он может решить сохранить переменную в каком-либо регистре ЦП и может позже или никогда не записать значение этого регистра в ОЗУ. Отладчики часто не могут найти регистр ЦП, в котором хранится переменная, и, следовательно, не могут показать ее значение.
JimmyB

1

Много технических объяснений, но я хочу сосредоточиться на практическом применении.

В volatileключевых слов силы компилятор для чтения или записи значения переменной из памяти каждый раз , когда он используется. Обычно компилятор пытается оптимизировать, но не делать ненужных операций чтения и записи, например, сохраняя значение в регистре ЦП, а не обращаясь к памяти каждый раз.

Это имеет два основных использования во встроенном коде. Во-первых, он используется для аппаратных регистров. Аппаратные регистры могут изменяться, например, регистр результата АЦП может записываться периферийным устройством АЦП. Аппаратные регистры также могут выполнять действия при доступе. Типичным примером является регистр данных UART, который часто очищает флаги прерываний при чтении.

Компилятор обычно пытается оптимизировать повторное чтение и запись в регистр при условии, что значение никогда не изменится, поэтому нет необходимости продолжать к нему доступ, но volatileключевое слово заставит его выполнять операцию чтения каждый раз.

Второе общее использование для переменных, используемых как кодом прерывания, так и кодом без прерываний. Прерывания не вызываются напрямую, поэтому компилятор не может определить, когда они будут выполняться, и, следовательно, предполагает, что никакого доступа внутри прерывания никогда не происходит. Поскольку volatileключевое слово заставляет компилятор обращаться к переменной каждый раз, это предположение удалено.

Важно отметить, что volatileключевое слово не является полным решением этих проблем, и необходимо избегать их. Например, в 8-битной системе 16-битная переменная требует двух обращений к памяти для чтения или записи, и, таким образом, даже если компилятор вынужден делать эти обращения, они происходят последовательно, и аппаратные средства могут действовать при первом доступе или прерывание произойдет между ними.


0

В отсутствие volatileквалификатора значение объекта может храниться в нескольких местах в течение определенных частей кода. Рассмотрим, например, что-то вроде:

int foo;
int someArray[64];
void test(void)
{
  int i;
  foo = 0;
  for (i=0; i<64; i++)
    if (someArray[i] > 0)
      foo++;
}

В первые дни C компилятор обрабатывал бы оператор

foo++;

через шаги:

load foo into a register
increment that register
store that register back to foo

Более сложные компиляторы, однако, признают, что если значение «foo» хранится в регистре во время цикла, его нужно будет загрузить только один раз перед циклом и сохранить один раз после. Однако во время цикла это будет означать, что значение «foo» хранится в двух местах - в глобальном хранилище и в регистре. Это не будет проблемой, если компилятор может увидеть все способы, которыми «foo» может быть доступен в цикле, но может вызвать проблемы, если значение «foo» доступно в каком-то механизме, о котором компилятор не знает ( такой как обработчик прерываний).

Возможно, авторы Стандарта могли бы добавить новый классификатор, который бы явным образом пригласил компилятор сделать такую ​​оптимизацию, и сказать, что старомодная семантика будет применяться при его отсутствии, но случаи, когда оптимизации полезны, значительно превосходят по численности. в тех случаях, когда это было бы проблематично, поэтому стандарт вместо этого позволяет компиляторам предполагать, что такие оптимизации безопасны при отсутствии доказательств того, что это не так. Цель volatileключевого слова - предоставить такие доказательства.

Пара споров между некоторыми авторами компиляторов и программистами возникает в таких ситуациях:

unsigned short volatile *volatile output_ptr;
unsigned volatile output_count;

void interrupt_handler(void)
{
  if (output_count)
  {
    *((unsigned short*)0xC0001230) = *output_ptr; // Hardware I/O register
    *((unsigned short*)0xC0001234) = 1; // Hardware I/O register
    *((unsigned short*)0xC0001234) = 0; // Hardware I/O register
    output_ptr++;
    output_count--;
  }
}

void output_data_via_interrupt(unsigned short *dat, unsigned count)
{
  output_ptr = dat;
  output_count = count;
  while(output_count)
     ; // Wait for interrupt to output the data
}

unsigned short output_buffer[10];

void test(void)
{
  output_buffer[0] = 0x1234;
  output_data_via_interrupt(output_buffer, 1);
  output_buffer[0] = 0x2345;
  output_buffer[1] = 0x6789;
  output_data_via_interrupt(output_buffer,2);
}

Исторически сложилось так, что большинство компиляторов допускают возможность того, что запись места volatileхранения может вызвать произвольные побочные эффекты, и избегают кэширования любых значений в регистрах в таком хранилище, иначе они будут воздерживаться от кэширования значений в регистрах при вызовах функций, которые не квалифицированный как «встроенный», и, таким образом, будет записывать 0x1234 в output_buffer[0], настраивать параметры для вывода данных, ждать его завершения, затем записывать 0x2345 output_buffer[0]и продолжать оттуда. Стандарт не требует реализации для обработки акта хранения адресаoutput_buffer вvolatileквалифицированный указатель как признак того, что с ним что-то может произойти, означает, что компилятор не понимает, однако, потому что авторы думали, что компилятор, который авторы компиляторов, предназначенные для различных платформ и целей, распознают, будет выполнять эти задачи на этих платформах. без необходимости говорить. Следовательно, некоторые «умные» компиляторы, такие как gcc и clang, будут предполагать, что, хотя адрес output_bufferзаписывается в указатель с изменчивой квалификацией между двумя хранилищами в output_buffer[0], нет никаких оснований предполагать, что что-либо может заботиться о значении, которое содержится в этом объекте в то время.

Кроме того, в то время как указатели, которые напрямую приводятся из целых чисел, редко используются для каких-либо целей, кроме манипулирования вещами способами, которые маловероятно понять компиляторам, стандарт снова не требует, чтобы компиляторы обрабатывали такие обращения как volatile. Следовательно, *((unsigned short*)0xC0001234)«умные» компиляторы, такие как gcc и clang, могут пропустить первую запись в , потому что разработчики таких компиляторов скорее заявят, что код, который пренебрегает квалификацией таких вещей, как volatile«неработающие», чем признают, что совместимость с таким кодом полезна , Во многих заголовочных файлах, поставляемых поставщиком, не указываются volatileквалификаторы, а компилятор, совместимый с заголовочными файлами, поставляемыми поставщиком, полезнее, чем тот, который не является таковым.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.