Глобальная изменчивая переменная не обновляется в ISR
При работе на atmega168p у меня возникло интересное поведение, которое я не могу объяснить. Глобальная изменчивая переменная, которая здесь называется pos
, не обновляется должным образом. Как только я добавлю код под назначением, он начнет работать.
#define ENCODER_DO_NOT_USE_INTERRUPTS
#include <Encoder.h>
Encoder myEnc(A2, A3);
void setup() {
Serial.begin(115200);
while (!Serial); Serial.println("Started.");
PCMSK1 |= bit(PCINT10) | bit(PCINT11) | bit(PCINT12);
PCIFR |= bit (PCIF1); // очищаем все необработанные прерывания
PCICR |= bit(PCIE1);
}
volatile int pos = 0;
int oldpos = 0;
void loop() {
if (oldpos != pos) {
oldpos = pos;
Serial.println(pos);
}
delay(100);
}
// PCINT10, PCINT11
ISR(PCINT1_vect) {
pos = myEnc.read();
// добавление digitalRead(A0)
// или даже Serial.println(pos) корректно обновляет переменную
}
Переключение с int
на byte
ничего не меняет. Библиотека кодировщика: https://github.com/PaulStoffregen/Encoder. Есть подсказки?
@kotique, 👍0
Обсуждение3 ответа
Лучший ответ:
Что ж, долгожданный ответ на этот вопрос прост: библиотека, которую я пытался использовать в своем коде, чрезмерно оптимизировала вещи, переходя на полный ASM и вмешиваясь в мои вещи. Использование версии C все исправило.
Всем, кто сталкивается со странным поведением, я рекомендую бросить быстрый взгляд на код библиотеки, которую вы используете, возможно, это не ваша вина.
Звучит как хитрая библиотека, если она мешает коду C++. Возможно, он вмешивается в регистры, не сообщая об этом компилятору., @Nick Gammon
Вы используете pos вне прерывания, не выключая прерывание. pos — это два байта, поэтому для чтения этой переменной Arduino требуется более одного цикла, независимо от того, как она обрабатывается. Если в это время произойдет прерывание, вы получите искаженное значение.
Вам нужно иметь cli(), затем скопировать pos в другую переменную, а затем sei(), а затем использовать эту копию в своих вычислениях в цикле.
Или используйте int8_t
, если это возможно., @RubberDuck
Вы должны были прочитать до конца вопроса, в котором говорится: «Переключение с int на byte ничего не меняет»., @kotique
Как распознать и исправить условия гонки в Arduino с помощью атомарных средств защиты доступа:
Переключение с int на byte ничего не меняет. Библиотека кодировщика: https://github.com/PaulStoffregen/Encoder. Есть подсказки?
Ваш код здесь:
void loop() {
if (oldpos != pos) {
oldpos = pos;
Serial.println(pos);
}
delay(100);
}
состояние неустраненной гонки приводит к неопределенному поведению и повреждению данных. Преобразование oldpos
в uint8_t
или int8_t
, каждый из которых равен 1 байту, вместо int
, который равен 2 байт, это не исправит, потому что вам нужно, чтобы весь раздел от if (oldpos != pos) {
до Serial.println(pos);
был атомарным, чтобы значение pos
, которое вы используете для входа в блок if
, не изменяется прерыванием, пока вы находитесь в блоке if
. Я объясню больше позже. Это напоминает мне о проблеме, с которой я столкнулся несколько лет назад: С++ уменьшение элемента однобайтового (изменчивого) массива не является атомарным! ПОЧЕМУ? (Также: как принудительно реализовать атомарность в Atmel AVR mcus/Arduino).
Итак, давайте рассмотрим случай, когда преобразование 2-байтовой переменной в 1-байтовую переменную БУДЕТ решить проблему, во-первых, чтобы убедиться, что мы понимаем, почему можно подумать, что преобразование 1-байтовой переменной является решением.< /p>
Давайте рассмотрим этот пример. Есть состояние гонки и неопределенное поведение. Сможете заметить?
volatile uint16_t isr_counter = 0;
void setup()
{
// здесь включить ISR
}
// Любой ISR
ISR()
{
my_var++;
}
void loop()
{
Serial.println(isr_counter); // <== состояние гонки!
}
Ответ заключается в том, что isr_counter
— это 2-байтовая переменная, а микроконтроллеры AVR имеют 8-битные (1-байтовые) процессоры, поэтому возможно, что isr_counter
считывается в loop()
для печати, 1 из 2 байтов правильно считывается для печати, затем срабатывает ISR, изменяет переменную и возвращает управление основному циклу. Считывается 2-й байт, теперь не тот, что должен был быть, что приводит к повреждению переменной! Пример: предположим, что isr_counter
был 0xFFFF или 65535, в тот момент, когда он должен быть напечатан, и допустим, что младший байт считывается правильно как 0xFF, затем срабатывает ISR, переменная увеличивается на 0x0000, так как он переполняется, то управление возвращается в основной цикл, и считывается 2-й байт, теперь он поврежден. То, что будет напечатано, будет содержимым переменной, содержащей 0x00FF, или 255, тогда как должно быть распечатано 65535, а следующее значение после этого, которое будет распечатано, будет 0. И что, если старший байт был прочитан правильно перед младшим байтом? (я не уверен, в каком порядке AVR mcus считывал байты), тогда ваше поврежденное значение будет 0xFF00 или 65280, тогда как должно быть распечатано 65535 (0xFFFF), а следующее значение после этого для печати будет 0 (0x0000).
Итак, простое преобразование переменной в uint8_t
волшебным образом решает эту проблему гонки для этого конкретного случая, поскольку 8-битные переменные на 8-битных процессорах считываются и записываются атомарно. , что означает «как атомарное или целое, единое целое, не разбитое и не поврежденное прерываниями или другими потоками». Все операции, занимающие 1 цикл команд ЦП, автоматически являются атомарными, поскольку их нельзя прервать. Для любого блока кода с несколькими циклами команд, который нам нужен, чтобы быть атомарным, мы должны принудительно выполнять его быть атомарным, используя то, что я называю «атомарными средствами защиты доступа». Чтение 1-байтовой переменной на 8-битном ЦП или запись 1-байтовой переменной на 8-битном ЦП — это 1 инструкция, поэтому это естественно атомарно. Однако увеличение или уменьшение 1-байтовой переменной НЕ является атомарной операцией, поскольку переменная должна быть прочитана, изменена и записана. Чтение переменной размером 2 или более байта на 8-битном ЦП или запись переменной размером 2 или более байта на 8-битном ЦП также НЕ является атомарным, поскольку для каждого байта требуется одна инструкция. Это означает, что простое чтение 2-байтовой переменной или запись 2-байтовой переменной могут быть прерваны ISR в середине операции, что приводит к повреждению данных, если вы не используете атомарные защиты доступа!
Теперь ваш случай: решит ли преобразование pos
1-байтовую переменную, поскольку volatile int8_t pos = 0;
, состояние гонки? Нет, совсем нет. Давайте еще раз посмотрим на ваш код, на этот раз представив, что pos
— это int8_t
, а не int
(то есть int16_t
на AVR Arduino):
void loop() {
if (oldpos != pos) {
oldpos = pos;
Serial.println(pos);
}
delay(100);
}
Давайте рассмотрим возможный сценарий: pos
равно 7. oldpos
равно 6. Таким образом, oldpos != pos
верно, и вы введите оператор if
. Теперь, как только вы входите, срабатывает ISR, ISR изменяет pos
на 6 (или на любое другое число — это все еще состояние гонки и поведение undefined), oldpos
теперь установлен на 6, как это уже было, и у вас есть повреждение данных, потому что он должен был быть установлен на 7! Обратите внимание: когда я говорю, что в данном случае это поведение undefined, я просто означает, что мы не можем предсказать, что произойдет. Это глючный код. Код должен всегда быть предсказуемым. Неопределенное поведение — это ошибка! Обратите также внимание на то, что, говоря о «неопределенном поведении»; в соответствии со стандартами C или C++ мы имеем в виду, что «стандарт не определяет, что произойдет», что означает, что ваш код может действовать одним образом на одной архитектуре или с одним компилятором и по-разному на другой архитектуре или с другим компилятором. Я говорю не об этом. Здесь, когда я говорю "неопределенное поведение" Я не имею в виду "согласно стандарту C или C++", я имею в виду "согласно правилам логики".
Решение проблемы состязания заключается в принудительном атомарном доступе с помощью того, что я называю "атомарными средствами защиты доступа". В многопоточных системах это означает использование блокировок, мьютексов и семафоров. В однопоточных системах, таких как эта, это означает запретить любое прерывание, которое может помешать и вызвать состояние гонки (вы можете думать о прерываниях, взаимодействующих с основным циклом, как о параллельном взаимодействии одного потока с другой поток, так что это как если бы у вас была многопоточная система, просто имея однопоточную систему + прерывания). Для микроконтроллеров AVR отключение прерываний в качестве защиты атомарного доступа обычно выполняется путем отключения глобальных (или всех) прерываний. НО, отключение прерываний вносит джиттер из-за задержки обработки ISR, поэтому это следует делать на как можно более короткий период времени! Это означает, что вы должны просто отключить прерывания, сделать копии volatile-переменные и повторно разрешать прерывания. Однако это важный момент: иногда, в случае использования 1-байтовой volatile-переменной, используется копия вашей переменной, которая может НЕ может быть изменено ISR в любой момент, а также само по себе действует как форма «атомарной защиты доступа»; и решает проблему сам по себе. Ниже я опубликую пример этого.
Но: ОПАСНОСТЬ ОПАСНОСТЬ! Это "лучшая практика" чтобы всегда создавать резервную копию состояния прерывания, отключать прерывания, а затем восстанавливать состояние прерывания, а не просто отключать прерывания, а затем снова включать их все время. Почему? Потому что что, если вы пишете функцию, которая иногда вызывается из самого ISR? Если вы разрешаете прерывания внутри ISR, вы просто разрешаете вложенные прерывания, что приводит к дополнительные условия гонки. Если вы точно знаете, что делаете, и вам нужны вложенные прерывания, это нормально! Но, скорее всего, кто-то просто совершает здесь ошибку и случайно включает вложенные прерывания, даже не осознавая этого, что приводит к действительно трудно отслеживаемому повреждению данных из-за вложенных прерываний. Критически важные для безопасности системы могут дать сбой и причинить вред людям, поэтому мы не можем допустить неуправляемых условий гонки.
Итак, сделайте это следующим образом:
- Принудительная атомарность в
loop()
путем резервного копирования состояния прерывания.
- Создайте копии изменчивых переменных.
- Затем восстановите глобальное состояние прерывания.
- Используйте только копию переменной в остальной части кода. Это (использование копии) также является формой "атомарной защиты доступа". все само по себе для более поздних блоков кода, использующих переменную, потому что использование копии вместо изменчивой переменной означает, что переменная НЕ может быть изменена в любой момент с помощью ISR. Это также обеспечивает атомарность!
Создание копии volatile-переменной и ничего больше при отключенных прерываниях минимизирует время, в течение которого прерывания отключены, делая ваш код как можно лучше! Теперь это должно выглядеть так:
1 из 3: ЛУЧШИЙ СПОСОБ!:
volatile int pos = 0; // предположим, что эта переменная по-прежнему `int`, то есть 2 байта
void loop() {
uint8_t SREG_bak = SREG; // резервное копирование глобального состояния прерывания
noInterrupts(); // отключаем прерывания
// делаем копии ваших volatile-переменных здесь
int pos_copy = pos;
// восстановить состояние прерывания, снова включив их, если они были включены до этого,
// или оставить их ВЫКЛЮЧЕННЫМИ, если они были ВЫКЛЮЧЕНЫ ранее.
SREG = SREG_bak;
// Теперь делайте с копией что хотите, используя ТОЛЬКО pos_copy,
// чтобы избежать состояния гонки! (то есть: использование этой копии здесь обеспечивает
// весь этот блок теперь должен быть естественно атомарным и сам по себе
// является формой "атомарных средств защиты доступа", как объяснялось выше.)
if (oldpos != pos_copy) {
oldpos = pos_copy;
Serial.println(pos_copy);
}
delay(100);
}
Решено!
Тем не менее, для полноты картины давайте рассмотрим еще 2 примера.
Здесь мы делаем весь блок операторов if
атомарным. На первый взгляд это выглядит правильно, но есть две проблемы: 1) [настоящая ошибка] Serial.println
будет блокироваться навсегда, если исходящий последовательный буфер заполнен, так как он использует прерывания для его очистки, и если прерывания отключены, это когда-либо произойдет, и 2) [нежелательный эффект, приводящий к проблемам с производительностью, но не ошибка] прерывания отключены в течение более длительного периода времени, чем необходимо. Вышеприведенный код был лучшим: он удерживал прерывания за минимально возможное время, просто создавая копии volatile-переменных, и гарантировал, что весь этот блок операторов if
был атомарным, просто используя его копию, которая автоматически действует как «атомарная защита доступа»; сам по себе и обеспечивает атомарность в блоке операторов if
.
2 из 3: у этого все еще есть 2 проблемы: одна ошибка, а другая проблема производительности, но не ошибка:
volatile int pos = 0; // предположим, что эта переменная по-прежнему `int`, то есть 2 байта
void loop() {
uint8_t SREG_bak = SREG; // резервное копирование глобального состояния прерывания
noInterrupts(); // отключаем прерывания
if (oldpos != pos) {
oldpos = pos;
Serial.println(pos);
}
// восстановить состояние прерывания, снова включив их, если они были включены до этого,
// или оставить их ВЫКЛЮЧЕННЫМИ, если они были ВЫКЛЮЧЕНЫ ранее.
SREG = SREG_bak;
delay(100);
}
И, наконец, что, если переменная volatile была бы 1-байтовой? Ну, тогда вы могли бы упростить до этого, так как чтение 1-байтовой переменной, естественно, является атомарной операцией!
3 из 3: это тоже работает (без ошибок и без проблем с производительностью), но ТОЛЬКО если ваша изменчивая переменная здесь 1-байтовая!:
// эта переменная теперь является 1-байтовой переменной, которая имеет естественное атомарное чтение и
// операции записи! (но не увеличение или уменьшение)
volatile int8_t pos = 0;
void loop() {
// Сделайте копии ваших volatile-переменных здесь.
// Будучи 1-байтовой переменной в 8-битной (1-байтовой) архитектуре ЦП, эта операция
// уже является атомарным по своей природе, поэтому нет необходимости в "защите прерываний атомарного доступа".
int8_t pos_copy = pos;
// Теперь делайте с копией что хотите, используя ТОЛЬКО pos_copy,
// чтобы избежать состояния гонки! (то есть: использование этой копии здесь обеспечивает
// весь этот блок теперь должен быть естественно атомарным и сам по себе
// является формой "атомарных средств защиты доступа", как объяснялось выше.)
if (oldpos != pos_copy) {
oldpos = pos_copy;
Serial.println(pos_copy);
}
delay(100);
}
О 3 других способах создания атомарных средств защиты доступа в 8-битных Arduino на основе AVR см. мой ответ от 2016 года здесь: https://stackoverflow.com/questions/36381932/c-decrementing -элемент-однобайтового-изменчивого-массива-не-атомарный-почему/39693278#39693278.
Последний комментарий: Пол Стоффреген — один из самых опытных специалистов по Arduino в наши дни. Я уверен, что он ошибается, но я по умолчанию доверяю всему, что он пишет. Он превосходнейший разработчик программного обеспечения и инженер: один из лучших во всем мире. Ищите ошибки в том, что он пишет, и дайте ему знать, если вы их найдете, но у вас здесь неустраненные условия гонки, так что я бы сначала посмотрел на это.
Ссылки:
- Моя собственная проблема состояния гонки: https://stackoverflow.com/questions/36381932/c-decrementing-an-element-of-a-single-byte-volatile-array-is-not-atomic-why
- Мое собственное решение условия гонки, демонстрирующее 3 (или 4, в зависимости от того, как вы на это смотрите) способа принудительного применения «атомарных средств защиты доступа» на 8-разрядных платах Arduino на базе AVR: https: //stackoverflow.com/questions/36381932/c-decrementing-an-element-of-a-single-byte-volatile-array-is-not-atomic-why/39693278#39693278
Я согласен с созданием копии (как в вашем примере с использованием pos_copy
), однако для однобайтовой переменной вам не нужно отключать прерывания, поскольку копирование из одного байта в «копию» будет атомарным. Однако это может быть незаметная ожидающая ошибка, если кто-то однажды решит сделать pos двумя байтами, не понимая, почему вы **не** поместили туда атомарную защиту доступа. Возможно, комментарий поможет в этом случае., @Nick Gammon
В вашем примере delay(100)
не будет работать, если прерывания будут отключены, поэтому, возможно, сохранение и восстановление SREG будет излишним. Можно просто снова включить прерывания. Если они каким-то образом отключатся в другом месте, задержка не сработает, и вам нужно будет решить эту проблему., @Nick Gammon
Вы правы во втором комментарии, но я думаю, что это просто лучшая практика, поскольку часто я оборачиваю этот материал в функции, которые я намереваюсь вызывать в ISR или из них., @Gabriel Staples
Что касается вашего 1-го комментария: «однако для однобайтовой переменной вам не нужно отключать прерывания, так как копирование из одного байта в «копию» будет атомарным». Я согласен с вами в этом пункте: «как копирование из один байт в «копии» будет атомарным.`, но не согласен с вами в этом: ‘для однобайтовой переменной вам не нужно отключать прерывания’, потому что то, что должно быть атомарным, взято из оператора ‘if’ вниз по серийной печати включительно. Весь этот блок должен быть атомарным, иначе вы оцениваете оператор if для одного состояния переменной, но делаете что-то с ним (и печатаете) для другого., @Gabriel Staples
Другими словами, атомарная защита доступа для 1-байтовой переменной по-прежнему необходима, чтобы гарантировать, что то, что вы используете внутри блока if, также является тем, что вы использовали, чтобы решить войти в блок if в первую очередь, не так ли?, @Gabriel Staples
@NickGammon, меня интересует ваш ответ на мои последние два комментария здесь, если у вас будет возможность., @Gabriel Staples
Да, но что ты можешь сделать? Если вы сделаете его охраняемым, он все равно может измениться, но вы этого не заметите. Сделать копию и сравнить с другим байтом — лучшее, что вы можете сделать. «Весь этот блок должен быть атомарным, иначе вы оцениваете оператор if для одного состояния переменной, но делаете что-то с ним (и печатаете) для другого». — Вовсе нет. Вы оцениваете копию и печатаете копию., @Nick Gammon
@NickGammon, ваш ответ здесь показывает, что мы на самом деле согласны и говорим одно и то же, просто не понимая этого. `вовсе нет. Вы оцениваете копию и печатаете копию». Я говорю о том, что создание копии **является** добавлением атомарных охранников доступа вокруг всего этого блока. Мы согласны. Я понимаю, что мы согласны и с 1-байтовой переменной. Я говорю, что его исходный код не будет работать, даже если он использует 1-байтовую переменную. Я думал, ты не согласен со мной, но ты не согласен с чем-то другим. Я обновлю свой ответ, чтобы сделать то, что я говорю, более ясным. Я почти уверен, что мы согласны., @Gabriel Staples
Ответ обновлен. Этот ответ смехотворно тщательен для этого вопроса. Я думаю, что когда-нибудь мне следует переместить его в вопрос в стиле вики, где я и задаю вопрос, и даю ответ. Эта тема условий гонки, атомарного доступа и прочего возникает *все время* и требует подробного, канонического ответа, такого как этот, чтобы обратиться к нему, и на который мы можем указать людям, когда у них возникнет вопрос или проблема, которые требуют этого, и вот что Я пытался сделать этот ответ., @Gabriel Staples
- C++ против языка Arduino?
- avrdude ser_open() can't set com-state
- Как читать и записывать EEPROM в ESP8266
- Float печатается только 2 десятичных знака после запятой
- устаревшее преобразование из строковой константы в 'char*'
- Запрограммировать ATMega328P и использовать его без платы Arduino.
- Разница между print() и println()
- Как исправить: Invalid conversion from 'const char*' to 'char*' [-fpermissive]
Похоже, что ISR запускается только один раз. облом., @kotique
Я думаю, что было бы лучше использовать библиотеку, которая поддерживает прерывания PCINT, вместо того, чтобы взламывать ее на библиотеке, которая этого не делает. Например, https://github.com/kr4fty/EncoderPCI выглядит так, будто поддерживает PCINT., @Gerben
Связано: 3 способа обеспечения атомарного доступа к микроконтроллерам Arduino/AVR: https://stackoverflow.com/questions/36381932/c-decrementing-an-element-of-a-single-byte-volatile-array-is-not- атомно-почему/39693278#39693278, @Gabriel Staples
@kotique, у вас в коде есть условия неуправляемой гонки. Я обратился к ним и дал подробное объяснение здесь: https://arduinoprosto.ru/q/77512/7727. Я не могу сказать, что это единственные проблемы, но у вас определенно есть условия для гонок. Создание volatile переменной
int8_t
не исправит их, они все еще там. Попробуйте с моими исправлениями, которые я описываю в своем действии, и посмотрите, что произойдет сейчас., @Gabriel Staples