Если вы ответили «да» на одно из этих утверждений, скорее всего вы не используете ключевое слово 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
Из этого вытекает еще один случай, не рассмотренный вами в "На практике так ведут себя только три типа переменных". Есть еще четвертый - переменные, изменяемые в основном цикле и используемые в прерываниях.
Вот пример из вашего кода для 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++;
Неочевидно, но если cycleBuf, count и tail будут объявлены без volatile, то компилятор может как минимум переставить местами последние три строки Примерно так:
uint8_t idx = tail;
count++;
tail++;
cycleBuf[idx] = sym;
И если после count++ случится прерывание - оно считает из cycleBuf совсем не то, что мы ожидаем.
Я подозреваю, что тут еще другая проблема - одновременный доступ к "ресурсам общего доступа". В данном случае это структура кольцевого буфера. Может так случиться, что доступ к буферу захотят два прерывания. Например, сount будет 2-байтовым, и команда count++ превратится как минимум в 2 команды ассемблера. Если во время выполнения count++ сработает прерывание между этими двумя командами, в котором выполнится count-- то значение count может оказаться испорченным. Правильное решение в ассемблере - запретить прерывания, поменять "переменные общего доступа", разрешить прерывания. А как это на Си сделать? Так-же временным запретом прерывания? А есть ли статьи здесь на эту тему?
RSS feed for comments to this post