Что может быть не так с этим кодом?
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» означает, что переменная может измениться в любой момент времени. И это именно то, что он говорит компилятору:
- Переменная или место в памяти могут меняться даже без доступа из программного кода.
- Чтение или запись в эту область памяти может вызвать побочные эффекты — это может вызвать изменения в других областях памяти или изменить саму переменную.
Следовательно, это говорит компилятору не быть умным и не оптимизировать доступ к переменной.
Летучие для периферийных регистров
Типичное использование этого для 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; ... }
Как указано выше, компилятор не будет оптимизировать этот код. Для этого есть два варианта использования:
- Упрощение отладки кода: если вы не уверены в том, что делает компилятор, с помощью команды
volatile
убедитесь, что компилятор генерирует простой код, так что может быть проще следовать последовательности кода и отлаживать проблему (пользовательский код) , Конечно, вы можете захотеть удалитьvolatile
потом. - В качестве обходного пути для проблемы компилятора: компиляторы могут оптимизировать вещи настолько, что код будет неправильным. В этом случае
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 () не могут быть вложенными!
Сериализация доступа к памяти
Все может стать еще сложнее. Иногда необходимо понимать доступ к основной шине микроконтроллера. Недавно я читал статью о сериализации операций и событий в памяти : просто выполнение записи в моем коде не означает, что запись вступает в силу немедленно! Из-за того, как работает шина, а также из-за состояния кэширования и ожидания, запись может произойти намного позже, чем я ожидал. Поэтому, если у меня есть запись в регистр, и на основе этой записи мне нужно прочитать что-то еще (зависимость от записи для чтения), я не смогу получить ожидаемый результат из-за циклов шины. Вместо этого мне нужно выполнить чтение регистра, чтобы принудительно записать («сериализовать доступ к памяти»):
- Написать в реестр
- Сразу же прочитайте регистр еще раз, чтобы сериализовать доступ к памяти
Без этого могут возникнуть тонкие проблемы со временем.
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++; }
Предполагается, что:
EnterCritical()
иExitCritcal()
построить критическую секцию, например, с отключением и повторным включением прерываний.TimerInterrupt()
само по себе не прерывается (имеет наивысший приоритет или не имеет вложенных прерываний).- Прерывание не такое быстрое, чтобы оно сразу переполнило счетчик.
Резюме
volatile
Ключевое слово указывает компилятору не быть умным о доступе к памяти. По сути, это позволяет избежать оптимизации компилятора и используется для обозначения периферийных регистров, имеющих побочные эффекты. На современных микропроцессорах volatile
одного недостаточно, чтобы гарантировать сериализацию или обеспечить повторный доступ к памяти. Только использование volatile
одного может быть вредным: необходимы дополнительные меры, такие как чтение после записи, барьеры памяти или отключение прерываний.
Счастливое Волатильное