Почему необходимо использовать ключевое слово volatile для глобальных переменных при обработке прерываний в ардуино?

Я знаком с ключевым словом Volatile, используемым для объявления переменных, которые совместно используются несколькими потоками в программном приложении (в основном в многопоточном приложении).

Но почему мне нужно объявлять переменную как изменчивую при выполнении кода на прерывании Arduino? Выполняется ли код в прерывании в другом потоке? Что-то еще происходит?

, 👍8

Обсуждение

"Я знаком с ключевым словом Volatile". Затем забудьте об этом и прочитайте еще раз., @Eugene Sh.

Фактически обработчик прерываний является отдельным потоком от основного. Без объявления volatile компилятор не знает, что main должен перечитывать глобальный (который, возможно, был изменен обработчиком прерываний)., @MarkU

`Является ли код, выполняемый в прерывании, выполняемым в другом потоке " - ну, в некотором роде, только наоборот. Прерывания-это механизм, техника, используемая программистами ОС для реализации потоков. Поэтому нам нужно использовать "изменчивость" в многопоточном коде, потому что их контекст был переключен операционной системой в прерывании. Нам нужно использовать "изменчивость" в прерываниях просто потому, что прерывания выполняются в другом контексте., @slebetman


2 ответа


7

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

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

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

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

16

Во-первых, он летучий, а не Летучий. Я освещаю эти концепции на своей странице о прерываниях, однако, чтобы избежать ответа только по ссылке, я повторю соответствующие фрагменты.


Что такое "изменчивые" переменные?

Переменные, разделяемые между функциями ISR и обычными функциями, должны быть объявлены изменчивыми. Это говорит компилятору, что такие переменные могут измениться в любое время, и поэтому компилятор должен перезагружать переменную всякий раз, когда вы ссылаетесь на нее, вместо того, чтобы полагаться на копию, которую она может иметь в регистре процессора.

Например:

volatile boolean flag;

// Процедура обслуживания прерываний (ISR)
void isr ()
{
 flag = true;
}  // end of isr

void setup ()
{
  attachInterrupt (0, isr, CHANGE);  // присоединить обработчик прерываний
}  // end of setup

void loop ()
{
  if (flag)
    {
    // произошло прерывание
    }
}  // end of loop

Критические разделы ... доступ к изменчивым переменным

Существуют некоторые тонкие проблемы, касающиеся переменных, которые являются общими для подпрограмм обслуживания прерываний (ISR) и основного кода (то есть кода, не содержащегося в ISR).

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


Первый... когда вы используете "изменчивые" переменные?

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

  • Переменные, используемые только за пределами ISR, не должны быть изменчивыми.
  • Переменные, используемые только внутри ISR, не должны быть изменчивыми.
  • Переменные, используемые как внутри, так и вне ISR, должны быть изменчивыми.

напр..

volatile int counter;

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


Атомарный доступ

Рассмотрим этот код:

volatile byte count;

ISR (TIMER1_OVF_vect)
  {
  count = 10;
  }

void setup ()
  {
  }

void loop () 
  {
  count++;
  }

Код, сгенерированный для count++ (добавьте 1 к count), выглядит следующим образом:

 14c:   80 91 00 02     lds r24, 0x0200
 150:   8f 5f           subi    r24, 0xFF     <<---- проблема, если прерывание происходит до его выполнения
 152:   80 93 00 02     sts 0x0200, r24   <<---- проблема, если прерывание происходит до его выполнения

(Обратите внимание, что он добавляет 1, вычитая -1)

Здесь есть две опасные точки, как указано. Если прерывание срабатывает после lds (загрузить регистр 24 с количеством переменных), но до sts (сохранить обратно в число переменных), то переменная может быть изменена ISR (TIMER1_OVF_vect), однако это изменение теперь потеряно, поскольку вместо этого использовалась переменная в регистре.

Нам нужно защитить общую переменную, на короткое время отключив прерывания, например:

volatile byte count;

ISR (TIMER1_OVF_vect)
  {
  count = 10;
  }  // end of TIMER1_OVF_vect

void setup ()
  {
  }  // end of setup

void loop () 
  {
  noInterrupts ();
  count++;
  interrupts ();
  } // end of loop

Теперь обновление подсчета производится "атомарно"... то есть его нельзя прервать.


Многобайтовые переменные

Давайте сделаем count 2-байтовой переменной и рассмотрим другую проблему:

volatile unsigned int count;

ISR (TIMER1_OVF_vect)
  {
  count++;
  } // end of TIMER1_OVF_vect

void setup ()
  {
  pinMode (13, OUTPUT);
  }  // end of setup

void loop () 
  {
  if (count > 20)
     digitalWrite (13, HIGH);
  } // end of loop

Хорошо, мы больше не меняем количество, так что все еще есть проблема? К сожалению, да. Давайте посмотрим на сгенерированный код для оператора "если":

 172:   80 91 10 02     lds r24, 0x0210
 176:   90 91 11 02     lds r25, 0x0211  <<---- problem if interrupt occurs before this is executed
 17a:   45 97           sbiw    r24, 0x15   
 17c:   50 f0           brcs    .+20        

Представьте, что счетчик был 0xFFFF и собирался "обернуться" обратно к нулю. Мы загружаем 0xFF в один регистр, но перед загрузкой второго 0xFF переменная изменяется на 0x00. Теперь мы думаем, что count равен 0x00FF, что не соответствует значению, которое у него было раньше (0xFFFF) или сейчас (0x0000).

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

void loop () 
  {
  noInterrupts ();
  if (count > 20)
     digitalWrite (13, HIGH);
  interrupts ();
  }  // end of loop

Что делать, если вы не уверены, включены или выключены прерывания?

Здесь есть последнее "попался". Что, если прерывания уже могут быть отключены? Затем включать их снова после этого-плохая идея.

В этом случае вам нужно сохранить регистр состояния процессора следующим образом:

unsigned int getCount ()
  {
  unsigned int c;
  byte oldSREG = SREG;   // запомнить, включены или выключены прерывания

  noInterrupts ();   // отключить прерывания
  c = count;         // доступ к общей переменной
  SREG = oldSREG;    // снова включить прерывания, если они были включены раньше

  return c;          // вернуть копию общей переменной
  }  // end of getCount

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


Краткие сведения

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

Поэтому для надежного кода обратите внимание на защиту доступа к общим переменным.

,

Я думал, что volatile будет обрабатывать доступ к многобайтовым типам для чтения и записи в основном цикле и ISR! Итак, я был совершенно неправ и должен отключить прерывания, прежде чем обращаться к ним в основном цикле, верно?! ;-(, @Shamim

Верно. изменчивость просто говорит компилятору не оптимизировать и сохранять переменные в регистрах процессора. У этого нет никаких других побочных эффектов (кроме того, что компилятор знает, что эти переменные могут измениться в любое время, поэтому изменчивые переменные никогда не оптимизируются, потому что они "не используются"). Так что да, как показано выше, "защитите" доступ к многобайтовым переменным, отключив прерывания., @Nick Gammon

Безопасно ли удалять "изменчивость", если мы используем окончательную версию " getCount ()"?, @Kevin Yuan

Я не понимаю, почему в строке (sbiw r24, 0x15) используется значение 0x15 вместо значения 20 в разделе "Многобайтовая переменная". Может ли это быть ошибкой?, @ecc

@ecc Ну, один-шестнадцатеричный, а другой-десятичный. Конечно, 0x15-это 21 в десятичной дроби, а не 20. Однако компилятор, возможно, решил вычесть 21 и проверить, был ли установлен бит переноса, вместо сравнения с 20. Это было бы частью оптимизации компилятора. Другими словами, > 20-это то же самое, что >>= 21., @Nick Gammon