MyTetra Share
Делитесь знаниями!
Volatile
Время создания: 24.04.2017 12:15
Раздел: Electronics - Microcontrollers - AVR8 - AVR GCC

[Правильное использование ключевого слова volatile]

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

1. Отображенные на память регистры процессора или его периферийного устройства (memory-mapped peripheral registers).
2. Глобальные переменные, модифицируемые в коде обработчика прерывания (interrupt service routine, ISR).
3. Глобальные переменные, к которым обращаются разные задачи в многопоточного приложения (обычно имеются в виду разные потоки RTOS).

Рассмотрим подробнее каждый из этих трех случаев.


Регистры периферийных устройств процессора. Встраиваемые системы на микроконтроллерах содержат реальное аппаратное оборудование, обычно это различные интерфейсы для обмена данными с внешним миром (UART, SPI, I2C и т. д.). Это так называемые периферийные устройства, и они содержат регистры, значение которых может асинхронно поменяться (асинхронно по отношению к выполняемому коду) в любой момент времени. Как очень простой пример, рассмотрим 8-битный регистр статуса, отображенный на адрес памяти 0x1234. Программа должна опрашивать этот регистр в ожидании, когда он станет не равным 0. Наивная и некорректная реализация будет такой:


uint8_t * pReg = (uint8_t *) 0x1234;

// Ожидание, когда регистр станет не равным 0:

while (*pReg == 0) { } // (в цикле ожидания могут быть и другие действия)


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


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

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


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


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


int etx_rcvd = FALSE;
 
void main() 
{
    ... 
    while (!ext_rcvd) 
    {
        // Wait
    } 
    ...
}
 
interrupt void rx_isr(void) 
{
    ... 
    if (ETX == rx_char) 
    {
        etx_rcvd = TRUE;
    } 
    ...
}

Когда оптимизация компилятора выключена (так обычно поступают при отладке), этот код может работать. Однако практически любой более-менее достойный оптимизатор "поломает" этот код. Проблема в том, что компилятора нет никакой информации о том, что переменная etx_rcvd может быть изменена в ISR. Пока компилятор считает так, то для него выражение !ext_rcvd всегда истина, так что цикл никогда не завершится. Следовательно, весь код после тела цикла оптимизатором может быть просто удален оптимизатором. Если Вам повезет, то компилятор сообщит об этом. Если не повезет (или Вы еще не научились обращать внимание на сообщения компилятора), то работа кода будет нарушена. Естественно, вина сразу будет возложена на "паршивый оптимизатор".

Решением в этой ситуации будет объявить переменную etx_rcvd как volatile. Тогда все Ваши проблемы (или по крайней мере проблемы, связанные с этим случаем) исчезнут.

Многопоточные приложения. Несмотря на наличие очередей (queues), каналов (pipes), и других механизмов обмена в многопоточном окружении (RTOS), все равно обычной практикой остается обмен информацией между двумя потоками с помощью общей памяти (глобальной переменной). Даже когда Вы добавите вытесняющий планировщик в свой код, у компилятора все равно нет никаких предположений, что сработает переключение контекста, и что это может произойти. В этом случае, когда другая задача модифицировала общую глобальную переменную, может произойти то же самое, как и с обработчиком прерывания, что уже обсуждалось выше. Как и в предыдущем примере, общая переменная должна быть объявлена как volatile. Например, здесь могут быть проблемы в коде при доступе к общей переменной cntr из разных задач:

int cntr;
 
void task1(void) 
{
    cntr = 0; 
    
    while (cntr == 0) 
    {
        sleep(1);
    } 
    ...
}
 
void task2(void) 
{
    ...
    cntr++; 
    sleep(10); 
    ...
}

Этот код скорее всего не будет работать, когда оптимизация будет разрешена. Декларирование cntr как volatile будет правильным путем решения проблемы.

[Дополнительные замечания]

Некоторые компиляторы позволяют Вам неявно декларировать все переменные как volatile. Не поддавайтесь на это искушение, потому что оно по сути отучает думать (как впрочем, и отключение оптимизации). Это также потенциально приводит к менее оптимизированному коду.

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

 
MyTetra Share v.0.53
Яндекс индекс цитирования