Что может быть не так с этим кодом?
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 одного может быть вредным: необходимы дополнительные меры, такие как чтение после записи, барьеры памяти или отключение прерываний.
Счастливое Волатильное
В этом случае
«Атомный» означает, что доступ к переменной осуществляется «одним куском» и не может быть прерван или выполнен в несколько этапов, которые могут быть прерваны.