C++ уменьшение элемента однобайтового (volatile) массива не является атомарным! ЗАЧЕМ? (Также: как заставить атомарность в Atmel AVR mcus/Arduino)

Я просто потерял дни, буквально, ~25 часов работы, из-за попытки отладить мой код на чем-то простом, чего я не знал.

Оказывается, уменьшение элемента однобайтового массива в C++ на 8-разрядном микроконтроллере AVR ATmega328 (Arduino) не является атомарной операцией и требует элементарной защиты доступа (а именно, отключение прерываний). Почему это??? Кроме того, каковы все методы C для обеспечения атомарного доступа к переменным на микроконтроллере Atmel AVR?

Вот глупая версия того, что я сделал:

//global vars:
const uint8_t NUM_INPUT_PORTS = 3;
volatile uint8_t numElementsInBuf[NUM_INPUT_PORTS];

ISR(PCINT0_vect) //external pin change interrupt service routine on input port 0
{
  //do stuff here
  for (uint8_t i=0; i<NUM_INPUT_PORTS; i++)
    numElementsInBuf[i]++;
}

loop()
{
  for (uint8_t i=0; i<NUM_INPUT_PORTS; i++)
  {
    //do stuff here
    numElementsInBuf[i]--; //<--THIS CAUSES ERRORS!!!!! THE COUNTER GETS CORRUPTED.
  }
}

Вот версия цикла, которая в порядке:

loop()
{
  for (uint8_t i=0; i<NUM_INPUT_PORTS; i++)
  {
    //do stuff here
    noInterrupts(); //globally disable interrupts 
    numElementsInBuf[i]--; //now it's ok...30 hrs of debugging....
    interrupts(); //globally re-enable interrupts 
  }
}

Обратите внимание на "элементарные средства защиты доступа", т.е. отключение прерываний перед уменьшением, а затем повторное включение их после.

Поскольку я имел дело с одним байтом здесь, я не знал, что мне понадобятся атомные охранники доступа. Зачем они мне нужны для этого случая? Это типичное поведение? Я знаю, что они мне понадобятся, если это массив 2-байтовых значений, но почему для 1-байтовых значений???? Обычно для 1-байтовых значений здесь не требуются атомные средства защиты доступа...


Обновление: прочитайте раздел "Атомный доступ" здесь: http://www.gammon.com.au/interrupts. Это отличный источник.


Связанный (ответ для STM32 mcus):

Итак, мы знаем, что чтение или запись в любую однобайтовую переменную на AVR 8-битном mcus - это атомарная операция, но как насчет STM32 32-битного mcus? Какие переменные имеют автоматическое атомарное чтение и запись на STM32? Ответ здесь: Какие типы / размеры переменных являются атомарными на микроконтроллерах STM32?,

3 ответа

Решение

Хорошо, ответ на вопрос "Почему увеличение / уменьшение одной байтовой переменной НЕ атомарно?" здесь очень хорошо отвечают Ишмаэль и Майкл Берр.

Теперь, когда я получил свой ответ, что -- декремент и ++ инкрементные операции никогда не бывают атомарными, даже если они выполняются для байтовых значений (см. ответы выше и ссылку Ника Гаммона здесь), я хотел бы получить ответ на следующий вопрос о том, как заставить атомарность на микроконтроллерах Atmel AVR, также ответить на этот вопрос. становится чем-то вроде ресурса:

Вот все методы, которые мне известны, чтобы заставить атомарность в микроконтроллерах Atmel AVR, таких как Arduino:

1) Вариант 1 (предпочтительный метод):

uint8_t SREG_bak = SREG; //save global interrupt state
noInterrupts(); //disable interrupts (for Arduino only; this is an alias of AVR's "cli()")
//atomic variable-access code here
SREG = SREG_bak; //restore interrupt state

2) Вариант 2 (менее безопасный, не рекомендуемый метод, поскольку он может привести к непреднамеренному включению вложенных прерываний, если вы случайно используете этот подход в блоке кода или библиотеке, которая вызывается внутри ISR):

noInterrupts(); //disable interrupts (Arduino only; is an alias to AVR's "cli()" call)
//atomic variable-access code here
interrupts(); //enable interrupts (Arduino only; is an alias to AVR's "sei()" call)

Альтернативный вариант 2:

cli(); //clear (disable) interrupts flag; noInterrupts() is simply a macro for this
//atomic variable-access code here
sei(); //set (enable) interrupts flag; interrupts() is simply a macro for this

3) Вариант 3 (по сути тот же, что и вариант 1; просто вместо этого используется макрос, хранящийся в библиотеке avr-libc, и переменная область видимости применяется в фигурных скобках)
источник: http://www.nongnu.org/avr-libc/user-manual/group__util__atomic.html

#include <util/atomic.h> //(place at top of code)
ATOMIC_BLOCK(ATOMIC_RESTORESTATE)
{
  //atomic access code here
}

Лист данных ATmega328 указывает, что:

АЛУ поддерживает арифметические и логические операции между регистрами или между константой и регистром

Здесь не упоминается, что ALU может работать непосредственно с ячейками памяти. Таким образом, чтобы уменьшить значение, это означает, что процессор должен выполнить несколько операций:

  • загрузить значение в регистр
  • уменьшить регистр
  • сохранить значение обратно

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

Детали того, как операция может быть сделана атомарной, зависят от платформы. Более новые версии стандартов C и C++ имеют явную поддержку атомарных операций; Я понятия не имею, поддерживает ли набор инструментов для ATmega эти более новые стандарты.

Я не знаю много об Arduino и прерываниях, поэтому я не смогу ответить на ваш конкретный вопрос здесь, но в многопоточной среде, уменьшая и увеличивая с помощью -- а также ++ никогда не бывает атомным. Более того, volatile тоже не значит atomic в C++ в целом ( доказательство). Хотя я знаю что volatile имеет смысл, когда вы программируете микроконтроллеры, поэтому я подозреваю, что мой ответ может не относиться к вашему делу.

Это работает, если вы замените массив volatile uint8_tс тремя отдельными volatile uint8_ts?

Другие вопросы по тегам