Как правильно использовать volatile переменные в Arduino?

Я делал небольшой проект на Arduino Uno. Это включало прерывания, поскольку я использую энкодеры для измерения того, насколько система дифференциала перемещается вперед. Мой робот движется только вперед. Поэтому я использую только один канал от каждого энкодера. Вот две мои процедуры прерывания:

ISR (INT0_vect){

      encoderRPos = encoderRPos + 1;     

}

ISR (INT1_vect){

  encoderLPos = encoderLPos + 1;

}

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

Но то, что произошло в моем коде, было немного странным, и я не мог этого объяснить. Вот как я вычисляю расстояние, пройденное левым колесом:

  #define distancePerCount 0.056196868  
  float SR = distancePerCount * (encoderRPos - encoderRPosPrev);
  float SL = distancePerCount * (encoderLPos - encoderLPosPrev);

  encoderRPosPrev = encoderRPos;
  encoderLPosPrev = encoderLPos;

Но когда я печатаю на последовательном мониторе следующее, я замечаю аномалию:

Если вы посмотрите на третий столбец (SL), то его значение слишком велико только на какое-то время. Это расстраивает все мои расчеты.

Единственная подсказка, которую я могу получить, это если я возьму полученное значение SL (3682), которое всегда является константой, и вычислю обратно (encodeLPos - encoderLPosPrev), я получу 65519,66, что близко к максимальному значению unsigned int. Это означает, что (encoderLPos - encoderLPosPrev) вызывает переполнение, в то время как оба значения, разница между которыми принимается, составляют всего около 5000!

И мне удалось ее решить. Это было по счастливой случайности. Вот как я изменил код:

  static int encoderRPosPrev = 0;
  static int encoderLPosPrev = 0;

  int diffL = (encoderLPos - encoderLPosPrev);
  int diffR = (encoderRPos - encoderRPosPrev);

  float SR = distancePerCount * diffR;
  float SL = distancePerCount * diffL;

  encoderRPosPrev = encoderRPos;
  encoderLPosPrev = encoderLPos;

Я не могу понять, что произошло. Есть ли что-то о volatile-переменных, о чем я должен был знать?

Обновление: вот весь код, если вы когда-нибудь захотите взглянуть. И он работает очень хорошо после изменения его на то, что было предложено в принятом ответе.

, 👍6

Обсуждение

В вашем вопросе говорится, что такое третий столбец вывода ... каковы другие столбцы? Пожалуйста, отредактируйте вопрос и добавьте заголовки столбцов, @James Waldby - jwpat7

@jwpat7 jwpat7 Я намеренно удалил их, потому что это только запутает читателя. Но на этот вопрос уже хорошо ответил Маженко., @daltonfury42

По вашим фрагментам сложно дать развернутый ответ. не могли бы вы объяснить, почему это происходит не случайным образом, а в определенное время каждый раз, когда я запускаю код? Кроме того, почему это дает особое значение? - Я, наверное, смог бы это сделать, если бы увидел весь код. А пока прочитайте это: http://www.gammon.com.au/interrupts, @Nick Gammon

@NickGammon Вот, пожалуйста: http://paste.ubuntu.com/14085127/, @daltonfury42

3683 / .056196868 = 65537, так что похоже, что он увеличился в неправильный момент, да? Вы обращаетесь к переменной, которая может быть изменена в прерывании несколько раз в этом коде, поэтому получить локальную копию, когда прерывания отключены, было бы намного безопаснее., @Nick Gammon

@NickGammon Да, это то, что я в итоге сделал., @daltonfury42


2 ответа


Лучший ответ:

9

Вам нужно узнать о критических разделах.

Возможно, переменные изменяются подпрограммами прерывания в середине вычислений. Ваше «исправление» сокращает время, затрачиваемое на вычисления с volatile-переменными, что снижает вероятность коллизии.

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

cli();
int l = encoderLpos;
int r = encoderRpos;
sei();

Поскольку Arduino представляет собой 8-битный ЦП, для выполнения математических операций над 16-битными значениями требуется несколько инструкций по сборке. С плавающей запятой еще хуже, когда для простого сложения используется множество инструкций. Деление и умножение используют значительно больше. Прерывание имеет много возможностей сработать во время выполнения этого списка инструкций. Выполняя такое присваивание и затем используя новые локальные переменные в своих вычислениях, инструкции, необходимые для работы с изменчивыми переменными, сводятся к абсолютному минимуму. Отключая прерывания во время присваивания, вы гарантируете, что переменные не могут быть изменены, пока вы их используете. Этот фрагмент кода называется критическим разделом.

,

Это может быть как раз так. Просто интересно, не могли бы вы объяснить, почему это происходит не случайным образом, а в определенное время каждый раз, когда я запускаю код? Кроме того, почему это дает особое значение?, @daltonfury42

Вот отличная ссылка на файл cli/sei. http://www.nongnu.org/avr-libc/user-manual/optimization.html#optim_code_reorder. С барьером памяти объявление volatile в приведенном выше коде на самом деле не нужно. Вот интересное чтение на эту тему. https://www.kernel.org/doc/Documentation/volatile-considered-harmful.txt, @Mikael Patel

@MikaelPatel Хорошо, но не так актуально для MCU. В этой ситуации требуется Volatile, чтобы компилятор не оптимизировал экземпляры, где, по его мнению, он не используется (значение никогда не меняется). cli/sei предназначен для того, чтобы сделать операцию атомарной WRT единственным другим потоком (прерыванием), который выполняется., @Majenko

Вы пытались скомпилировать код с volatile и без него? Но с критической секцией (cli/sei). То, что я пытаюсь обсудить, - это концепция барьера памяти и то, как он обеспечивает доступ к volatile (и правильное упорядочение) от компилятора с необходимостью объявлять переменные как volatile. Большинство программистов учат, что любая переменная, доступ к которой осуществляется в ISR, должна быть объявлена изменчивой, но в этой истории гораздо больше., @Mikael Patel

Я не думаю, что компилятор имеет представление о том, что делают cli() и sei() и как это повлияет на такие вещи, как оптимизация переменных, которые не должны быть оптимизированы. Все, что делают sei() и cli(), — это манипулируют глобальным флагом разрешения прерывания в своем регистре. Они ничего не делают для потока кода., @Majenko

В конце концов, они просто сопоставляются с ассемблерной инструкцией: # define sei() __asm__ __volatile__ ("sei" ::: "memory"), @Majenko

Это волшебное слово «память» сообщает компилятору, что это также барьер памяти. https://gcc.gnu.org/onlinedocs/gcc/Volatiles.html, @Mikael Patel

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

@MikaelPatel Вот экстремальный пример (экстремальный, поэтому он вызывает оптимизацию, которую я после демонстрации): http://pastebin.com/u9bfiN4R, @Majenko

@Majenko Это был крайний пример. Вы пробовали и volatile, и cli/sei? Противоположным примером является Cosa/RTT https://github.com/mikaelpatel/Cosa/blob/master/cores/cosa/Cosa/RTT.cpp, в нем нет volatile. Вместо этого все доступы синхронизируются., @Mikael Patel


-1

/* Привет всем и спасибо, что поделились. Из моего личного опыта вот что я нашел полезным

"Декодер DTMF сохраняет последний код DTMF в байтах в своей памяти. На выводе STQ устанавливается высокий уровень каждый раз, когда обнаруживается новый DTMF. Это только часть кода, ориентированного на ISR (прерывание) "

   Tips when using ISR with Arduino nano and IDE 18.19

1: только HIGH или LOW, RISING или FALLING не сработает должным образом, если Nano использует внешнее цифровое прерывание.

2: Будьте осторожны при использовании LOW. В соответствии с использованием HIGH в примере вам необходимо использовать флаги (байты событий) и возможные счетчики.

3: Когда вы набираете attachInterrupt, не используйте скобки для обозначения пустоты функции, которую вы вызываете, иначе каждый раз, когда запускается ISR, он будет переходить от повторной инициализации/сброса кода всех глобальных переменных независимо от того если вы объявите их изменчивыми или статическими!

4: Задержка не работает, только delayMicroseconds()

5. Будьте краткими, переменные размера в байтах и, по возможности, без сложной математики

6: внешние цифровые контакты ISR Arduino nano — 2,3

7. Не запускайте void, который вы вызываете в ISR, в цикле().

8: Учитывайте все остальные комментарии, оставленные другими программистами.

*/

///////////////// Пример //////////////////////////// ///

byte CFlag;     // ГЛОБАЛЬНЫЙ

byte Counter;   // ГЛОБАЛЬНЫЙ

#define STQ  3  // ГЛОБАЛЬНО

setup();
{
Serial.begin(9600);

pinMode(STQ, INPUT);                    

attachInterrupt(digitalPinToInterrupt(), DragonBalls, HIGH);  
//Когда вывод STQ СТАНОВИТСЯ ВЫСОКИМ, он запускает функцию void, называемую DragonBalls()

}//конец настройки


void DragonBalls()      

{
    Dpin = digitalRead(STQ);
    delayMicroseconds(10000); 
    if      (Dpin == HIGH &&  CFlag ==LOW) {  
                                                CFlag=HIGH;
                                                Counter++ ; 
                                            }//конец, если
  
    else if  (Dpin == LOW  &&  CFlag == HIGH) { 
                                                CFlag=LOW;
                                                DragonPrint();// Персонал Serial.print
                                                
                                                }//конец иначе, если
                                   

}// конец DragonBalls(void)


void DragonPrint()   

{
    Serial.print(CFlag);  
    Serial.print(" = CFlag, Counter = "); 
    Serial.println(Counter);
                                                          
}//конец пустоты DragonPrint
,

на какой вопрос это отвечает? Я уверен, что не тот, что выше, @Juraj