Використання летючих у вбудованій 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, про який знає компілятор. - Реєстр обладнання, в якому АЦП надає результати, однак повинен бути мінливим, оскільки компілятор не може знати, чи / коли його значення зміниться (він змінюється, якщо / коли апаратне забезпечення АЦП закінчить конверсію.)
JimmyB

2
Чи порівнювали ви асемблер, створений обома версіями? Це повинно показати вам, що відбувається під кришкою
Mawg

3
@stark: BIOS? На мікроконтролері? Простір вводу / виводу, відображений на пам’яті, буде не кешованим (якщо архітектура навіть кеш даних не має, що не впевнене) за умови відповідності проекту між правилами кешування та картою пам'яті. Але летючі не мають нічого спільного з кешем контролера пам'яті.
Бен Войгт

1
@Davislor Загалом про мову не потрібно нічого більше говорити. Прочитане на летючий об'єкт виконає реальне навантаження (навіть якщо компілятор нещодавно зробив це і зазвичай знав, що таке значення), а запис на такий об’єкт виконає справжнє сховище (навіть якщо те саме значення було прочитане з об'єкта ). Тож у if(x==1) x=1;процесі запису може бути оптимізовано xвідсутнє значення для енергонезалежних і не може бути оптимізовано, якщо воно xє нестабільним. OTOH, якщо потрібні спеціальні вказівки для доступу до зовнішніх пристроїв, ви можете додати їх (f.ex., якщо діапазон пам'яті потрібно прописати).
цікавогут

Відповіді:


87

Визначення volatile

volatileповідомляє компілятору, що значення змінної може змінюватися, не знаючи компілятора. Отже, компілятор не може припустити, що значення не змінилося лише тому, що програма C, здається, не змінила його.

З іншого боку, це означає, що значення змінної може знадобитися (читати) десь ще, про який компілятор не знає, отже, він повинен переконатися, що кожне присвоєння змінній насправді виконується як операція запису.

Використовуйте випадки

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коду С вимагає від компілятора здійснити доступ до оперативної пам'яті (або порту вводу / виводу), що призводить до зниження продуктивності.

По-друге, компілятор може переупорядкувати операції над енергонезалежними змінними для продуктивності та / або розміру коду. Простий приклад:

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
Хороша дискусія про нестабільність для виступу :)
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 № C17 5.1.2.3 §6 визначає поведінку, що спостерігається : "Доступ до летючих об'єктів оцінюється строго за правилами абстрактної машини". Стандарт C не зовсім зрозумілий, де загалом потрібні бар'єри пам'яті. В кінці виразу, який використовується volatile, є точка послідовності, і все після нього повинно бути "послідовно послідовно". Це означає, що це вираження - своєрідний бар'єр пам’яті. Постачальники компіляторів вирішили поширити всі види міфів, щоб покласти відповідальність на бар'єри пам'яті перед програмістом, але це порушує правила "абстрактної машини".
Лундін

2
@JimmyB Місцевий непостійний, можливо, корисний для подібного коду volatile data_t data = {0}; set_mmio(&data); while (!data.ready);.
Мацей П'єхотка

13

Мінливе ключове слово повідомляє компілятору, що доступ до змінної має ефект, що спостерігається. Це означає, що кожен раз, коли ваш вихідний код використовує змінну, компілятор ПОВИНЕН створити доступ до змінної. Будь то доступ для читання чи запису.

Ефект цього полягає в тому, що будь-яка зміна змінної за межами нормального потоку коду також буде спостерігатися кодом. Наприклад, якщо обробник переривання змінює значення. Або якщо змінна - це фактично якийсь апаратний реєстр, який змінюється самостійно.

Ця велика користь також є його недоліком. Кожен окремий доступ до змінної проходить через змінну, і значення ніколи не зберігається в регістрі для швидшого доступу протягом будь-якого часу. Це означає, що мінлива зміна буде повільною. Величини повільніші. Тож використовуйте летючі лише там, де це фактично необхідно.

У вашому випадку, наскільки ви показали код, глобальна змінна змінюється лише тоді, коли ви її оновите самостійно adcValue = readADC();. Компілятор знає, коли це станеться, і ніколи не буде містити значення adcValue в регістрі через щось, що може викликати readFromADC()функцію. Або будь-яку функцію, про яку вона не знає. Або все, що буде маніпулювати покажчиками, які можуть вказувати на adcValueтаке і таке. Справді немає необхідності в мінливості, оскільки змінна ніколи не змінюється непередбачуваними способами.


6
Я погоджуюся з цією відповіддю, але "величина повільніше" звучить занадто жахливо.
kkrambo

6
До сучасного суперскалярного процесора можна отримати доступ до реєстру процесора за менший цикл процесора. З іншого боку, доступ до фактичної кешованої пам'яті (пам’ятайте, що деяке зовнішнє обладнання змінило б це, тому кеш процесора не дозволений) може знаходитися в межах 100-300 циклів процесора. Отже, так, величини. Не буде так погано на AVR або подібному мікроконтролері, але питання не вказує обладнання.
Госвін фон Бредерлоу

7
У вбудованих системах (мікроконтролерів) штраф за доступ до оперативної пам'яті часто набагато менший. Наприклад, AVR використовують лише два цикли процесора для зчитування або запису в оперативну пам'ять (переміщення регістру-реєстру займає один цикл), тому економія зберігання речей у регістрах наближається (але фактично ніколи не досягає) макс. 2 тактових цикли на доступ. - Звичайно, відносно кажучи, збереження значення з регістру X до оперативної пам’яті, то негайно перезавантаживши це значення в регістр X для подальших обчислень, буде потрібно 2х2 = 4 замість 0 циклів (при просто збереженні значення в X), а значить, нескінченно повільніше :)
JimmyB

1
Це "швидкість повільніше" в контексті "написання чи читання з певної змінної", так. Однак у контексті повноцінної програми, яка, ймовірно, робить значно більше, ніж читати з / записувати в одну змінну знову і знову, ні, не дуже. У цьому випадку загальна різниця, ймовірно, «мала до незначної». Потрібно бути обережним під час тверджень про ефективність, щоб уточнити, чи стосується твердження до певної програми чи до програми в цілому. Уповільнення нечасто використовуваного оператора на коефіцієнт ~ 300x майже ніколи не є великою справою.
1818

1
Ти маєш на увазі це останнє речення? Це означає набагато більше в сенсі "передчасна оптимізація - корінь усього зла". Очевидно, що ви не повинні використовувати volatileдля всього лише тому , що ви також не повинні ухилятися від цього в тих випадках, коли ви вважаєте, що це законно вимагається через передумови щодо продуктивності.
1818 року

9

Основне використання енергонезалежного ключового слова у вбудованих додатках C - це маркування глобальної змінної, записаної в обробник переривання. Це, звичайно, необов'язково.

Без цього компілятор не може довести, що значення коли-небудь записується після ініціалізації, оскільки він не може довести, що обробник переривання коли-небудь викликається. Тому він вважає, що може оптимізувати змінну поза існуванням.


2
Звичайно, існують інші практичні можливості, але це найбільш поширене.
vicatcu

1
Якщо значення зчитується лише в ISR (і змінюється з main ()), можливо, вам доведеться також використовувати летючі, щоб гарантувати доступ ATOMIC для багатобайтових змінних.
Rev1.0

15
@ Rev1.0 Ні, летючі речовини не гарантують ароматності. Цю проблему слід вирішувати окремо.
Кріс Страттон

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

3
"позначити глобальну змінну, записану в обробник переривання" nope. Це позначити змінну; глобальне чи інше; що це може бути змінено чимось поза розумінням компіляторів. Переривання не потрібно. Це може бути спільна пам'ять або хтось, що встромляє зонд в пам'ять (останнє не рекомендується для більш сучасних 40 років)
UKMonkey

9

Існує два випадки, коли потрібно використовувати volatileвбудовані системи.

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

    Це означає, що зареєстрований на пам'ять регістр, сам апаратний периферійний апарат всередині MCU. Це, ймовірно, матиме якусь криптовану назву на кшталт "ADC0DR". Цей реєстр повинен бути визначений у коді С, або через якусь карту реєстру, яку постачає постачальник інструментів, або сам. Щоб зробити це самостійно, ви зробите (якщо припустити 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 завжди декларується всередині драйвера обладнання. У цьому випадку ADR ISR повинен знаходитися всередині драйвера АЦП. Ніхто інший, крім водія, не повинен спілкуватися з ISR - все інше - це програвання спагетті.

  • Під час написання C вся комунікація між ISR та фоновою програмою повинна бути захищена від перегонів. Завжди , кожного разу, не виняток. Розмір шини даних MCU не має значення, оскільки навіть якщо ви робите одну 8-бітну копію на C, мова не може гарантувати атомність операцій. Якщо ви не використовуєте функцію C11 _Atomic. Якщо ця функція недоступна, потрібно використовувати певний спосіб семафору або відключити переривання під час зчитування тощо. Інший варіант асемблера - інший варіант. volatileне гарантує атомність.

    Що може статися, це таке:
    -Завантажити значення зі стека в регістр
    -Постає перерва
    -Використовувати значення з регістра

    І тоді не має значення, чи частина "використання значення" сама по собі є однією інструкцією. На жаль, значна частина всіх програмістів із вбудованими системами про це не звертає уваги, ймовірно, що робить його найпоширенішою помилкою з вбудованими системами. Завжди переривчастий, важко спровокувати, важко знайти.


Приклад правильно написаного драйвера АЦП виглядатиме так (припустимо, що 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;
  }
}
  • Цей код передбачає, що переривання не може бути перерване само по собі. У таких системах простий булевий може діяти як семафор, і він не повинен бути атомним, оскільки немає шкоди, якщо переривання відбудеться до встановлення булева. Недоліком вищезгаданого спрощеного методу є те, що він буде відкидати зчитування ADC, коли виникають умови перегонів, використовуючи замість цього попереднє значення. Цього можна також уникнути, але тоді код стає складнішим.

  • Тут volatileзахищено від помилок оптимізації. Це не має нічого спільного з даними, що походять з апаратного реєстру, лише те, що дані спільно використовуються з ISR.

  • staticзахищає від програмування спагетті та забруднення простору імен, роблячи змінну локальною для водія. (Це добре в одноядерних, однопотокових програмах, але не в багатопотокових.)


Важко відладку відносно, якщо код видалений, ви помітите, що ваш цінний код пішов - це досить сміливе твердження, що щось не так. Але я згоден, наслідки налагодження можуть бути дуже дивними і важкими.
Арсенал

@Arsenal Якщо у вас є гарний налагоджувач, який вбудовує асемблер на C, і ви хоч трохи знаєте асм, то так, це можна легко помітити. Але для більш складного коду, великий шматок машиногенерованого ASM не банальний. Або якщо ви не знаєте asm. Або якщо ваш налагоджувач лайно і не показує asm (cougheclipsecough).
Лундін

Можливо, я трохи зіпсований, використовуючи потім налагоджувачі Lauterbach. Якщо ви спробуєте встановити точку розриву в коді, який оптимізувався, він встановить його десь інше, і ви знаєте, що там щось відбувається.
Арсенал

@Arsenal Так, змішаний C / asm, який ви можете отримати в Lauterbach, аж ніяк не є стандартним. Більшість налагоджувачів відображає зом в окремому вікні, якщо воно взагалі є.
Лундін

semaphoreобов'язково має бути volatile! Насправді, це самий основний випадок використання вимагає яким : Сигнальний що - то з одного контексту виконання в інший. - У вашому прикладі компілятор може просто опустити, оскільки він "бачить", що його значення ніколи не читається, перш ніж його перезаписувати . volatilesemaphore = true;semaphore = false;
JimmyB

5

У фрагментах коду, представлених у запитанні, ще немає причини використовувати летючі. Неважливо, що значення adcValueпоходить від АЦП. А adcValueглобальність повинна adcValueвикликати у вас підозри щодо того, чи має бути мінливою, але це не є причиною сама по собі.

Бути глобальним є поняттям, оскільки це відкриває можливість adcValueотримати доступ до більш ніж одного програмного контексту. Контекст програми включає обробник переривання і завдання RTOS. Якщо глобальна змінна змінюється одним контекстом, то інші контексти програми не можуть припустити, що вони знають значення попереднього доступу. Кожен контекст повинен перечитувати значення змінної кожного разу, коли вони використовують його, оскільки це значення, можливо, було змінено в іншому контексті програми. Контекст програми не усвідомлюється, коли відбувається переривання або переключення завдань, тому він повинен припускати, що будь-які глобальні змінні, що використовуються у декількох контекстах, можуть змінюватися між будь-яким доступом до змінної через можливий контекстний комутатор. Саме для цього є мінливою декларацією. Він повідомляє компілятору, що ця змінна може змінюватися поза вашим контекстом, тому читайте її кожен доступ і не припускайте, що ви вже знаєте значення.

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

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


4

"Глобальна змінна, яка змінюється безпосередньо з обладнання"

Тільки тому, що значення надходить з якогось апаратного реєстру АЦП, не означає, що воно "безпосередньо" змінюється апаратним забезпеченням.

У вашому прикладі ви просто викликаєте readADC (), який повертає деяке значення регістра ADC. Це добре стосовно компілятора, знаючи, що adcValue присвоюється нове значення в цій точці.

Було б інакше, якби ви використовували процедуру переривання ADC, щоб призначити нове значення, яке викликається тоді, коли готове нове значення АЦП. У цьому випадку компілятор не матиме поняття про те, коли викликається відповідна ISR, і може вирішити, що таким чином не буде доступний adcValue. Саме тут міг би допомогти мінливий.


1
Оскільки ваш код ніколи не "викликає" функцію ISR, Компілер бачить, що змінна оновлюється лише у функції, яку ніхто не викликає. Тож компілятор оптимізує це.
Swanand

1
Це залежить від решти коду, якщо adcValue не читається ніде (як-от читання лише через відладчик) або якщо він читається лише один раз в одному місці, компілятор, ймовірно, оптимізує його.
Демієн

2
@Damien: Це завжди "залежить", але я мав на меті вирішити власне питання "Чи слід використовувати в цьому випадку ключове слово летюче?" якомога коротше.
Rev1.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
Наприклад a = 1; b = a; і c = b; компілятор може подумати, почекайте хвилину, a і b марні, давайте просто поставимо 1 до c безпосередньо. Звичайно, ви не зробите цього у своєму коді, але компілятор кращий за вас при пошуку, також якщо ви спробуєте написати оптимізований код відразу, це було б нечитабельно.
Демієн

2
Правильний код з правильним компілятором не порушиться з увімкненими оптимізаціями. Правильність компілятора - це чимала проблема, але принаймні з IAR я не стикався з ситуацією, коли оптимізація призводить до порушення коду там, де він не повинен.
Арсенал

5
Дуже багато випадків, коли оптимізація порушує код, коли ви йдете на територію UB ..
труба

2
Так, побічним ефектом летючого є те, що він може сприяти налагодженню. Але це не вагомий привід використовувати летючі. Ви, ймовірно, повинні вимкнути оптимізацію, якщо вашою метою є проста налагодження. Ця відповідь навіть не згадує про переривання.
kkrambo

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в avolatile-кваліфікований покажчик як знак того, що щось може статися з ним через засоби, що компілятор не розуміє, однак, оскільки автори думали, що компілятор автори компіляторів, призначені для різних платформ і цілей, визнають, коли це робитимуть ці цілі на цих платформах без того, щоб говорити. Отже, деякі "розумні" компілятори, такі як 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.