Статьи

«Летучий» может быть вредным…

Что может быть не так с этим кодом?

volatile uint16_t myDelay;
 
void wait(uint16_t time) {
  myDelay = 0;
  while (myDelay<time) {
    /* wait .... */
  }
}
 
void TimerInterrupt(void) {
  myDelay++;
}

Очевидно, что, учитывая заголовок этой статьи и использование volatileв приведенном выше источнике, речь идет о ключевом слове C / C ++, которое очень важно для каждого программиста встроенных систем.

Изменчивое ключевое слово и классификатор типов

volatile является зарезервированным словом («ключевое слово») в C / C ++. Это «квалификатор типа», что означает, что он добавляется как дополнительный атрибут или классификатор к типу, аналогичному const. Ниже приведен типичный пример:

volatile int flags;

Это переменная с именем «flags» типа «int», и она помечена как «volatile».

Компилятор, не будь умным!

«Volatile» означает, что переменная может измениться в любой момент времени. И это именно то, что он говорит компилятору:

  1. Переменная или место в памяти могут меняться даже без доступа из программного кода.
  2. Чтение или запись в эту область памяти может вызвать побочные эффекты — это может вызвать изменения в других областях памяти или изменить саму переменную.

Следовательно, это говорит компилятору не быть умным и не оптимизировать доступ к переменной.

Летучие для периферийных регистров

Типичное использование этого для  volatile периферийных регистров, например, для переменной, которая отображается в регистр таймера:

extern volatile uint16_t TIMER_A; /* Timer A counter register */

Другой способ использовать такой регистр таймера:

#define TIMER_A (*((volatile uint16_t*)0xAA)) /* Timer A counter register at address 0xAA */

Это приводит к целому числу 0xAA в качестве указателя на изменчивое 16-битное значение.

Используя это так:

timerVal = TIMER_A;

будет читать содержимое регистра таймера.

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

TIMER_A = 0;
while (TIMER_A == 0) {
  /* wait until timer gets incremented */
}

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

Volatile приводит к тому, что он не оптимизирует доступ к переменной «только для чтения»:

TIMER_A; /* read access to register, not optimized because volatile */

Без volatile в типе, компилятор может удалить доступ.

:!: Хотя использование языка C для такого доступа может быть вполне подходящим, я рекомендую использовать Assembly или встроенный ассемблер, если для оборудования требуется очень специфический метод доступа.

Имейте в виду, что volatile является определителем типа. Так

volatile int *ptr;

указатель на изменчивый int , в то время как

int *volatile ptr;

является изменчивым указателем на (нормальный) int.

Volatile для локальных переменных

volatile также может быть полезен для локальных переменных или параметров:

void foo(volatile int param) {
  ...
}

Или как в этой функции:

void bar(int flags) {
  volatile int tmp;
  ...
}

Как указано выше, компилятор не будет оптимизировать этот код. Для этого есть два варианта использования:

  1. Упрощение отладки кода: если вы не уверены в том, что делает компилятор, с помощью команды  volatileубедитесь, что компилятор генерирует простой код, так что может быть проще следовать последовательности кода и отлаживать проблему (пользовательский код) , Конечно, вы можете захотеть удалить volatileпотом.
  2. В качестве обходного пути для проблемы компилятора: компиляторы могут оптимизировать вещи настолько, что код будет неправильным. :-(  В этом случае volatileследует обмануть оптимизацию и можно использовать в качестве обходного пути.:-)

Летучие и прерывания

Поскольку volatileинформирует компилятор о том, что переменная может быть изменена, это отличный способ пометить общие переменные между функциями прерывания и основной программой:

static volatile bool dataSentFlag = FALSE;
 
void myTxInterrupt(void) {
  ...
  dataSentFlag = TRUE;
  ...
}
 
void main(void) {
  ...
  dataSentFlag = FALSE;
  TxData(); /* send data, will raise myTxInterrupt() */
  while(!dataSentFlag) {
    /* wait until interrupt sets flag */
  }
  ...
}

Хотя использование volatileздесь идеально, существует общее заблуждение volatile: оно  НЕ:!: гарантирует повторный доступ к переменной!

Приведенный выше пример можно рассматривать как не представляющий проблему, если микроконтроллер читает и записывает общую переменную атомарным способом.

: Идея: «Атомный» означает, что доступ к переменной осуществляется «одним куском» и не может быть прерван или выполнен в несколько этапов, которые могут быть прерваны.

Это может быть не так для следующего примера, который подсчитывает количество переданных байтов:

static volatile uint32_t nofTxBytes = 0;
 
void myTxInterrupt(void) {
  ...
  nofTxBytes++;
  ...
}
 
void main(void) {
  ...
  nofTxBytes = 0;
  TxData(); /* send data, will raise myTxInterrupt() */
  while(nofTxBytes < nofDataSent) { /* compare against how much we sent */
    /* wait until transaction is done */
  }
  ...
}

Теперь все зависит от микроконтроллера и архитектуры шины, чтобы определить, что происходит. Доступ и использование nofTxBytes больше не являются атомарными, что может привести к неправильному поведению во время выполнения. Чтобы избежать состояния гонки , доступ к общей переменной должен быть защищен критическим разделом .

: Идея:Критические секции могут быть легко реализованы путем отключения и повторного включения прерываний. Processor Expert создает макросы EnterCritical () и ExitCritical (), которые отключают и повторно активируют прерывания. Эти макросы имеют преимущество в том, что состояние прерывания сохраняется. Помните, что макросы EnterCritical () и ExitCritical () не могут быть вложенными!

Сериализация доступа к памяти

Все может стать еще сложнее. Иногда необходимо понимать доступ к основной шине микроконтроллера. Недавно я читал статью о сериализации операций и событий в памяти : просто выполнение записи в моем коде не означает, что запись вступает в силу немедленно! Из-за того, как работает шина, а также из-за состояния кэширования и ожидания, запись может произойти намного позже, чем я ожидал. Поэтому, если у меня есть запись в регистр, и на основе этой записи мне нужно прочитать что-то еще (зависимость от записи для чтения), я не смогу получить ожидаемый результат из-за циклов шины. Вместо этого мне нужно выполнить чтение регистра, чтобы принудительно записать («сериализовать доступ к памяти»):

  1. Написать в реестр
  2. Сразу же прочитайте регистр еще раз, чтобы сериализовать доступ к памяти

Без этого могут возникнуть тонкие проблемы со временем.

ARM Память Барьер Инструкции

Ниже приведен источник FreeRTOS, который создает переключение контекста в подпрограмме обслуживания прерываний:

void vPortYieldFromISR(void) {
  /* Set a PendSV to request a context switch. */
  *(portNVIC_INT_CTRL) = portNVIC_PENDSVSET_BIT;
  /* Barriers are normally not required but do ensure the code is completely
     within the specified behavior for the architecture. */
  __asm volatile("dsb");
  __asm volatile("isb");
}

Обратите внимание на две инструкции по сборке в конце: DSB ( д ата с ynchronization б arrier) и ИМК ( я nstruction сек ynchronization б arrier). Они обеспечивают сериализацию данных и инструкций. Смотрите эту статью ARM Infocenter для деталей.

Критический раздел

Возвращаясь к примеру в начале, что может быть одним из решений это:

volatile uint16_t myDelay;
 
void wait(uint16_t time) {
  uint16_t tmp;
 
  EnterCritical();
  myDelay = 0;
  ExitCritical();
  do {
    EnterCritical();
    tmp = myDelay();
    ExitCritical();
  } while(tmp<time);
}
 
void TimerInterrupt(void) {
  myDelay++;
}

Предполагается, что:

  1. EnterCritical()и ExitCritcal()построить критическую секцию, например, с отключением и повторным включением прерываний.
  2. TimerInterrupt() само по себе не прерывается (имеет наивысший приоритет или не имеет вложенных прерываний).
  3. Прерывание не такое быстрое, чтобы оно сразу переполнило счетчик.

Резюме

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

Счастливое Волатильное :-)