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

Комментарии   

# Guest 24.05.2010 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 24.05.2010 10:22
"очень длинный комментарий", бью на два сообщения:

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

Я подозреваю, что тут еще другая проблема - одновременный доступ к "ресурсам общего доступа". В данном случае это структура кольцевого буфера. Может так случиться, что доступ к буферу захотят два прерывания. Например, сount будет 2-байтовым, и команда count++ превратится как минимум в 2 команды ассемблера. Если во время выполнения count++ сработает прерывание между этими двумя командами, в котором выполнится count-- то значение count может оказаться испорченным. Правильное решение в ассемблере - запретить прерывания, поменять "переменные общего доступа", разрешить прерывания. А как это на Си сделать? Так-же временным запретом прерывания? А есть ли статьи здесь на эту тему?
Ответить | Ответить с цитатой | Цитировать
# САБ 12.11.2013 10:40
Это вопрос атомарного доступа. Он несколько перпендикулярен к volatile. Да, и на Си и на ассемблере этого можно достичь запретом прерываний. При чтении многобайтовой переменной можно обойтись без запрета - считать старший байт, затем считать младший, снова считать старший и сравнить с результатом предыдущего чтения. Если они равны - можно использовать результат. Если не равны - снова считать младший, старший, сравнить. Это несколько сложнее, чем запрещать прерывания, потому и используется очень редко, когда прерывания запрещать ну никак нельзя. В процессорах Cortex есть специальные инструкции LDREX/STREX для организации атомарного доступа, а в компиляторах Си для этих ядер - спец. функции для работы с этими инструкциями.
Ответить | Ответить с цитатой | Цитировать
# Crystaly 14.11.2013 16:38
Да согласен атомарный доступ и volatile это разные вещи. Но я бы сказал, что они параллельны, а не перпендикулярны . Часто они взаимосвязаны и используются одновременно. Например, в вашем примере переменную count если она много-байтная, надо обеспечить не только volatile, но и атомарный доступ. Вот тут http://chipenable.ru/index.php/programming-avr/item/16-atomarnyy-dostup-k-peremennym нашел статью на эту тему. А если быть точнее, надо говорить не об атомарном доступе к переменным, а об атомарном исполнении (блоков) команд. Например, однобайтовая переменная может изменяться за три команды чтение-модифика ция-запись, и эти три команды должны быть выполнены все сразу, без прерывания.
Ответить | Ответить с цитатой | Цитировать
# Pashgan 24.05.2010 16:00
Спасибо за дельный комментарий. Каждый раз от вас узнаю что-нибудь новое.
Ответить | Ответить с цитатой | Цитировать
# Guest 30.05.2010 10:04
Отличный сайт у вас, спасибо! За один вечер освоил написание простеньких программ (решил сделать восьмиразрядный счетчик с динамической индикацией для начала). Не планируете ли вы сделать описание для бут лоадера через com порт ПК? Было бы очень хорошим дополнением к статье про UART.
Ответить | Ответить с цитатой | Цитировать
# Pashgan 31.05.2010 19:27
Планирую. Вас интересует создание бутлоадера с нуля или использование готового?
Ответить | Ответить с цитатой | Цитировать
# Guest 08.06.2010 13:48
Создание с нуля было бы довольно интересно, например под AVRProg.
Ответить | Ответить с цитатой | Цитировать
# Guest 19.06.2010 23:23
Добрый день, не смотря на ваши похвалы оптимизаторам, я считаю, что программист - сам должен оптимизировать код. А тупое игнорирование инструкции оптимизатором - это действительно тупо. Оптимизаторы - гамно. Простите за выражение - за то правдивое
Ответить | Ответить с цитатой | Цитировать
# Pashgan 20.06.2010 05:57
Это переводная статья и не мои похвалы. Я тоже считаю, что программист должен полагаться на свое знание языка и компилятора.
Ответить | Ответить с цитатой | Цитировать
# Guest 28.06.2010 13:15
Цитирую Innos:
А тупое игнорирование инструкции оптимизатором - это действительно тупо. Оптимизаторы - гамно.
Приводите пример, обсудим. Обычно этим самым гамном оказывается уровень знаний программиста, у которого оптимизатор "тупо игнорирует". А если оптимизатор чуть-чуть не доработал, то он сразу превращается в "тупой компилятор, который не может соптимизировать такую простую конструкцию". А написать гамнокод можно хоть на ассемблере, хоть в кодах.
Ответить | Ответить с цитатой | Цитировать
# Guest 13.07.2010 17:00
Пашганчик,здрав ия желаю!А про шину i2c не планируете ли начать повествование?
Ответить | Ответить с цитатой | Цитировать
# 037 19.10.2010 14:51
Ни разу не использовал volotile, в программе много функций, есть прерывание, использую максимальную оптимизацию, а программы все равно работают, блин, хотя есть много глобальных переменных, которые используются то там, то тут.
Ответить | Ответить с цитатой | Цитировать
# Saxel 28.02.2011 09:14
Спасибо за статью, при написании проектов на AVR с таким не сталкивался ни разу, а вот пришлось писать под PIC, возник затык. Код, который работает в эмуляторе, абсолютно не работает в процессоре. Volatile сильная штука, которая позволяет заставить код работать в железе.
Ответить | Ответить с цитатой | Цитировать
# DIMA 22.06.2011 14:28
ВО_ВО моя проблема. Когда разрешаю глобально прерывания прога при определенной комбинации мигания диодами останавливается , даже на ресет не сбрасывается. Хотя локально все прерывания отключены и обработчики прерываний отсутствуют пока еще. И volatile не помогает. Разная оптимизация тоже не помогает. Не знаю куда еще рыть?
Ответить | Ответить с цитатой | Цитировать

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

Защитный код
Обновить