Как правильно использовать 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-переменных, о чем я должен был знать?
Обновление: вот весь код, если вы когда-нибудь захотите взглянуть. И он работает очень хорошо после изменения его на то, что было предложено в принятом ответе.
@daltonfury42, 👍6
Обсуждение2 ответа
Лучший ответ:
Вам нужно узнать о критических разделах.
Возможно, переменные изменяются подпрограммами прерывания в середине вычислений. Ваше «исправление» сокращает время, затрачиваемое на вычисления с 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
/* Привет всем и спасибо, что поделились. Из моего личного опыта вот что я нашел полезным
"Декодер 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
- Как сгенерировать аппаратное прерывание в mpu6050 для пробуждения Arduino из режима SLEEP_MODE_PWR_DOWN?
- Arduino непрерывно считывает значение АЦП с помощью прерывания
- Как прервать функцию цикла и перезапустить ее?
- 4-битный счетчик вверх и вниз
- Включить и отключить отдельные прерывания
- Управление функцией включения на драйвере микрошагового устройства
- Захват прерывания на обоих фронтах, когда он установлен на RISING или FALLING
- Распознавание монет с помощью Coin Accepter в Arduino Uno
В вашем вопросе говорится, что такое третий столбец вывода ... каковы другие столбцы? Пожалуйста, отредактируйте вопрос и добавьте заголовки столбцов, @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