WTF Volatile?

Опубликовано в рубрике "Статьи", 01.10.2009.
Тэги: , , , , автор:

Если вы пишите прошивки для микроконтроллеров на C/C++, но наверняка встречались с ситуациями, когда:

  • Программа работает хорошо до тех пор, пока вы не включите оптимизацию
  • Программа работает хорошо пока запрещены прерывания
  • Драйвера аппаратуры прекрасно работают у ваших друзей/коллег, а у вас – глючат
  • В многозадачных приложениях задачи по отдельности работают хорошо, а если запустить их вместе – происходит что-то страшное.

bug

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

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

Синтаксис

Для того, чтобы описать переменную, как volatile, достаточно просто добавить это ключевое слово до или после типа. К примеру:

volatile int var1;
int volatile var2;

Эти объявления совершенно равнозначны. Теперь объявим указатели на volatile-переменные

volatile int * pVar1;
int volatile * pVar2;

Эти указатели также совершенно равнозначны.

Существуют так-же volatile – указатели на не volatile-переменные и volatile – указатель на volatile  — переменную. Правда они представляют в основном академический интерес и используются очень редко. Пример:

int * volatile pVar2;
int volatile * volatile pVar4;

Если вы применяете volatile к структуре или классу, все внутренние переменные получают этот квалификатор.

 

Использование

Квалификатор volatile необходимо применять ко всем переменным, чье значение может неожиданно (для компилятора) поменяться. В основном, это

  • Регистры периферийных устройств
  • Глобальные переменные, которые изменяются в прерываниях
  • Глобальные переменные в многозадачных приложениях, которые служат для обмена данными между задачами.

Рассмотрим каждый вид подробнее.

 

Регистры периферийных устройств

Обычно, в микроконтроллеры встроено большое количеств разнообразной периферии. Регистры этой периферии могут изменяться совершенно неожиданно для компилятора. К примеру, у нас есть таймер и мы ждем, пока его счетчик не станет равным нулю. Пусть, регистр счетчика располагается по адресу 0x12. Большинство программистов написало бы так:

unsigned char *tmr = (unsigned char *)0x12;
while ( *tmr ) {};

Такое решение будет работать только до тех пор, пока вы не включите оптимизатор. К примеру, для архитектуры AVR при включенном оптимизаторе этот код может скомпилироваться так:

LDS R16, 0x12    // загружаем переменную
TST R16 // проверяем на равенство нулю
loop: BRNE loop // если переменная не равна нулю, прыгаем на метку loop.

Таким образом, мы получаем бесконечный цикл в таком довольно безобидном коде.

Если-же мы перепишем наш код так

volatile unsigned char *tmr = (volatile unsigned char *)0x12;
while ( *tmr ) {};

компилятор выдаст вот такое

loop: LDS R16, 0x12    // загружаем переменную
TST R16 // проверяем на равенство нулю
BRNE loop // если переменная не равна нулю, прыгаем на метку loop.

Таким образом, мы получили то, чего хотели.

В современном мире ситуация сильно упростилась, так как компиляторы поставляются с заголовочными файлами с правильно описанными регистрами периферии.

 

Прерывания

Часто в прерываниях заносят данные в переменные, а обрабатывают их в основном потоке программы. Рассмотрим, к примеру, таймер по переполнению которого устанавливается флаг doImportantThings

   1: unsigned char doImportantThings = 0;

   2:  

   3: void timer_int()

   4: {

   5:    doImportantThings = 1;

   6: }

   7:  

   8: int main( void )

   9: {

  10:      ...

  11:    while ( !doImportantThings )  /* ожидаем события */

  12:    {

  13:          ...

  14:    }

  15:     ...

  16: }

 

С выключенной оптимизацией этот код, может и заработает, однако любой приличный оптимизатор превратит этот код в один большой глюк.

Проблема в том, что компилятор ничего не знает о переменной doImportantThings, и о том, что она может измениться в прерывании, и будет считать этот флаг всегда равным нулю. Таким образом, мы опять получим бесконечный цикл. Более того, весь код который содержится после while будет удален, так как ему никогда не будет передано управление. В таких случаях народ начинает винить “долбанный оптимизатор” в непонятных багах.

Решение проблемы – обьявить doImportantThings как volatile:

volatile unsigned char doImportantThings = 0;

 

Многозадачные приложения

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

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

 

Выводы

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

 

Дополнительно можно почитать

https://alenacpp.blogspot.com/2006/04/volatile.html

Комментарии
  1. Роман написал(а) 13th Декабрь, 2009 в 15:31

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

  2. BSVi написал(а) 13th Декабрь, 2009 в 15:41

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

  3. Panda_Y2K написал(а) 23rd Январь, 2012 в 21:01

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

  4. Panda_Y2K написал(а) 23rd Январь, 2012 в 21:01

    ЗЫ оптимизация почти всегда есть, и чаще всего максимальная.

  5. BSVi написал(а) 23rd Январь, 2012 в 22:17

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

  6. vinrom написал(а) 21st Январь, 2013 в 14:14

    ошибка же
    компилятор выдаст вот такое

    loop: LDS R16, 0×12 // загружаем переменную
    TST R16 // проверяем на равенство нулю
    BRNE loop // если переменная не равна нулю, прыгаем на метку loop.

    =>
    компилятор выдаст вот такое

    LDS R16, 0×12 // загружаем переменную
    loop: TST R16 // проверяем на равенство нулю
    BRNE loop // если переменная не равна нулю, прыгаем на метку loop.

  7. BSVi написал(а) 23rd Январь, 2013 в 14:51

    Нет, не ошибка. Специально проверил: https://bsvi.me/pp2230113ae579a

  8. vinrom написал(а) 23rd Январь, 2013 в 18:22

    да, это я ошибся — нужно же загрузить сначала содержимое переменной в регистр, перед проверкой.
    можно удалить эти мои комменты 🙂

  9. bea написал(а) 15th Июнь, 2013 в 15:45

    часть статьи — перевод отсюда https://www.embedded.com/electronics-blogs/beginner-s-corner/4023801/Introduction-to-the-Volatile-Keyword в любом случае будет полезно почитать

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Этот сайт защищен reCAPTCHA и применяются Политика конфиденциальности и Условия обслуживания применять.