Как использовать ключевое слово volatile

23/05/2010 - 16:30
  
   Если вы ответили «да» на одно из этих утверждений, скорее всего вы не используете ключевое слово volatile. Вы не одиноки. Использование этого  спецификатора плохо изучено многими программистами, поскольку большинство книг по программированию на языке Си не уделяют этой теме должного внимания.

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

Синтаксис


Ключевое слово volatile пишется до или после типа данных объявляемой переменной.

volatile int foo;
int volatile foo;

Указатели на volatile переменные объявляются так.
 
volatile int * pReg;
int volatile * pReg;

Volatile указатели на не volatile переменные встречаются крайне редко (думаю, я использовал их лишь один раз), но я на всякий случай дам их синтаксис:

int * volatile p;

И, для полноты, если вам понадобится volatile указатель на volatile переменную, следует написать:

int volatile * volatile p;

Если вы используете volatile для структуры (struct) или объединения (union), действие спецификатора будет распространяться на все содержимое структуры/объединения. Если вы не хотите этого, то можете применить спецификатор volatile к отдельным элементам структуры/объединения.

Правильное использование спецификатора VOLATILE


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

1.    Отображаемые в памяти периферийные регистры
2.    Глобальные переменные, изменяемые в обработчике прерывания
3.    Глобальные переменные, используемые в многопотоковом приложении

Далее мы поговорим о каждом из этих случаев.

Периферийные регистры

   Встраиваемые системы содержат оборудование со сложной периферией. В составе  периферии есть регистры, чьи значения могут изменяться асинхронно алгоритму программы. В качестве простого примера рассмотрим 8-битный регистр состояния, отображаемый в памяти по адресу 0х1234. Допустим, нам нужно опрашивать регистр состояния до тех пор, пока он не станет ненулевым. Простая и неправильная реализация может быть такой:

uint8_t * pReg = (uint8_t *) 0x1234;

// Wait for register to become non-zero
while (*pReg == 0) { } // Do something else

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

  mov ptr, #0x1234
  mov a, @ptr
loop:
  bz loop

   Логическое обоснование оптимизатора довольно простое: уже считав значение переменной в аккумулятор (вторую строка кода), нет необходимости считывать его заново, поскольку  значение всегда будет тем же. Таким образом, в третьей строке мы окажемся в бесконечном цикле. Чтобы заставить компилятор сделать то, что нам нужно, мы изменим описание на:

uint8_t volatile * pReg = (uint8_t volatile *) 0x1234;

Ассемблерный код теперь выглядит следующим образом:

  mov ptr, #0x1234
loop:
  mov a, @ptr
  bz loop

Требуемый ход процесса достигнут.

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

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

Программа обработки прерывания (ISR)

   Обработчики прерывания часто устанавливают значение переменных, которые проверяются в основной части кода. Например, прерывание последовательного порта может проверять каждый полученный символ, чтобы узнать, является ли он символом конца строки (предположительно обозначающего конец сообщения). Если символ является концом строки, обработчик прерываний может установить глобальный флаг. Неверная реализация в этом случае может быть такой:

int etx_rcvd = FALSE;

void main()
{
    ...
    while (!etx_rcvd)
    {
        // Wait
    }
    ...
}

interrupt void rx_isr(void)
{
    ...
    if (rx_char == ETX)
    {
        etx_rcvd = TRUE;
    }
    ...
}

   При выключенной оптимизации этот код мог бы работать. Однако включение даже начальных уровней оптимизации, скорее всего «сломает» код. Проблема в том, что компилятор не имеет понятия, что переменная etx_rcvd может быть изменена внутри обработчика прерываний. Для него выражение !ext_rcvd всегда истинно, а значит цикл while никогда не завершится. Следовательно, весь код после цикла while может быть попросту удален оптимизатором. Если вам повезет, компилятор вас об этом предупредит. Если вам не повезет (или вы не воспримете его предупреждения всерьез), ваш код выйдет из строя. Естественно, вина будет возложена на «никчемный оптимизатор».

   Решение проблемы заключается в объявлении переменной etx_rcvd с ключевым словом volatile. После этого все ваши проблемы (ну или некоторые из них) исчезнут.

Многопотоковые приложения

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

int cntr;

void task1(void)
{
    cntr = 0;
    
    while (cntr == 0)
    {
        sleep(1);
    }
    ...
}

void task2(void)
{
    ...
    cntr++;
    sleep(10);
    ...
}

   Скорее всего, код даст сбой, как только будет включена оптимизация. Верный способ решить проблему -  объявить переменную cntr с ключевым словом volatile.

Заключительные размышления


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

Jones, Nigel. "Introduction to the Volatile Keyword" Embedded Systems Programming, July 2001

Comments   

# Guest 2010-05-24 10:19
Можно еще упомянуть о порядке доступа к volatile-переме нным (что компилятор не имеет права менять местами подобные обращения) и вытекающем из этого предупреждении, гененрируемом компилятором при использовании нескольких volatile-переме нных в одном выражении (из-за неопределнности порядка вычисления подвыражений).

Из этого вытекает еще один случай, не рассмотренный вами в "На практике так ведут себя только три типа переменных". Есть еще четвертый - переменные, изменяемые в основном цикле и используемые в прерываниях.
Вот пример из вашего кода для UART и кольцевого буфера:
//кольцевой (циклический) буфер
unsigned char cycleBuf[SIZE_BUF];
unsigned char tail = 0;
unsigned char head = 0;
unsigned char count = 0;

void PutChar(unsigne d char sym)
{
if (count < SIZE_BUF){
cycleBuf[tail] = sym;
count++;
tail++;
# Guest 2010-05-24 10:22
"очень длинный комментарий", бью на два сообщения:

Неочевидно, но если cycleBuf, count и tail будут объявлены без volatile, то компилятор может как минимум переставить местами последние три строки Примерно так:
uint8_t idx = tail;
count++;
tail++;
cycleBuf[idx] = sym;
И если после count++ случится прерывание - оно считает из cycleBuf совсем не то, что мы ожидаем.
# Crystaly 2013-11-11 11:21
Quoting Guest:
"...Неочевидно, но если cycleBuf, count и tail будут объявлены без volatile, то компилятор может как минимум переставить местами последние три строки...

Я подозреваю, что тут еще другая проблема - одновременный доступ к "ресурсам общего доступа". В данном случае это структура кольцевого буфера. Может так случиться, что доступ к буферу захотят два прерывания. Например, сount будет 2-байтовым, и команда count++ превратится как минимум в 2 команды ассемблера. Если во время выполнения count++ сработает прерывание между этими двумя командами, в котором выполнится count-- то значение count может оказаться испорченным. Правильное решение в ассемблере - запретить прерывания, поменять "переменные общего доступа", разрешить прерывания. А как это на Си сделать? Так-же временным запретом прерывания? А есть ли статьи здесь на эту тему?
# САБ 2013-11-12 10:40
Это вопрос атомарного доступа. Он несколько перпендикулярен к volatile. Да, и на Си и на ассемблере этого можно достичь запретом прерываний. При чтении многобайтовой переменной можно обойтись без запрета - считать старший байт, затем считать младший, снова считать старший и сравнить с результатом предыдущего чтения. Если они равны - можно использовать результат. Если не равны - снова считать младший, старший, сравнить. Это несколько сложнее, чем запрещать прерывания, потому и используется очень редко, когда прерывания запрещать ну никак нельзя. В процессорах Cortex есть специальные инструкции LDREX/STREX для организации атомарного доступа, а в компиляторах Си для этих ядер - спец. функции для работы с этими инструкциями.
# Crystaly 2013-11-14 16:38
Да согласен атомарный доступ и volatile это разные вещи. Но я бы сказал, что они параллельны, а не перпендикулярны . Часто они взаимосвязаны и используются одновременно. Например, в вашем примере переменную count если она много-байтная, надо обеспечить не только volatile, но и атомарный доступ. Вот тут http://chipenable.ru/index.php/programming-avr/item/16-atomarnyy-dostup-k-peremennym нашел статью на эту тему. А если быть точнее, надо говорить не об атомарном доступе к переменным, а об атомарном исполнении (блоков) команд. Например, однобайтовая переменная может изменяться за три команды чтение-модифика ция-запись, и эти три команды должны быть выполнены все сразу, без прерывания.
# Pashgan 2010-05-24 16:00
Спасибо за дельный комментарий. Каждый раз от вас узнаю что-нибудь новое.
# Guest 2010-05-30 10:04
Отличный сайт у вас, спасибо! За один вечер освоил написание простеньких программ (решил сделать восьмиразрядный счетчик с динамической индикацией для начала). Не планируете ли вы сделать описание для бут лоадера через com порт ПК? Было бы очень хорошим дополнением к статье про UART.
# Pashgan 2010-05-31 19:27
Планирую. Вас интересует создание бутлоадера с нуля или использование готового?
# Guest 2010-06-08 13:48
Создание с нуля было бы довольно интересно, например под AVRProg.
# Guest 2010-06-19 23:23
Добрый день, не смотря на ваши похвалы оптимизаторам, я считаю, что программист - сам должен оптимизировать код. А тупое игнорирование инструкции оптимизатором - это действительно тупо. Оптимизаторы - гамно. Простите за выражение - за то правдивое
# Pashgan 2010-06-20 05:57
Это переводная статья и не мои похвалы. Я тоже считаю, что программист должен полагаться на свое знание языка и компилятора.
# Guest 2010-06-28 13:15
Quoting Innos:
А тупое игнорирование инструкции оптимизатором - это действительно тупо. Оптимизаторы - гамно.
Приводите пример, обсудим. Обычно этим самым гамном оказывается уровень знаний программиста, у которого оптимизатор "тупо игнорирует". А если оптимизатор чуть-чуть не доработал, то он сразу превращается в "тупой компилятор, который не может соптимизировать такую простую конструкцию". А написать гамнокод можно хоть на ассемблере, хоть в кодах.
# Guest 2010-07-13 17:00
Пашганчик,здрав ия желаю!А про шину i2c не планируете ли начать повествование?
# 037 2010-10-19 14:51
Ни разу не использовал volotile, в программе много функций, есть прерывание, использую максимальную оптимизацию, а программы все равно работают, блин, хотя есть много глобальных переменных, которые используются то там, то тут.
# Saxel 2011-02-28 09:14
Спасибо за статью, при написании проектов на AVR с таким не сталкивался ни разу, а вот пришлось писать под PIC, возник затык. Код, который работает в эмуляторе, абсолютно не работает в процессоре. Volatile сильная штука, которая позволяет заставить код работать в железе.
# DIMA 2011-06-22 14:28
ВО_ВО моя проблема. Когда разрешаю глобально прерывания прога при определенной комбинации мигания диодами останавливается , даже на ресет не сбрасывается. Хотя локально все прерывания отключены и обработчики прерываний отсутствуют пока еще. И volatile не помогает. Разная оптимизация тоже не помогает. Не знаю куда еще рыть?

У вас недостаточно прав для комментирования.