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_t
s?