Print this page

Как использовать ключевое слово 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

Related items