Перейти к содержанию

Чтение-модификация-запись (read-modify-write)

Чтобы включить один светодиод, не трогая остальные выводы порта, программа читает регистр, ставит нужный бит и записывает всё обратно. Это три отдельных шага, и между ними может случиться прерывание или поменяться флаг в самом регистре. Отсюда целый класс трудноуловимых ошибок.

Обновлено Редакция GAW

Чтение-модификация-запись (read-modify-write, RMW) — способ изменить часть регистра или ячейки памяти в три шага: прочитать текущее значение, поменять нужные биты, записать результат обратно. Так компилируются привычные строки PORTB |= (1 << 5); и GPIOA->ODR &= ~mask;. Опасность в том, что эти шаги не неделимы.

Две классические ошибки

Гонка с прерыванием. Основная программа прочитала регистр порта, а до записи сработало прерывание и изменило другой бит того же регистра. Затем программа записывает своё — и затирает изменение прерывания. Это состояние гонки, которое проявляется редко и зависит от тайминга.

Флаги, сбрасываемые единицей. Во многих регистрах состояния флаг сбрасывается записью 1 — такие биты описаны в статье о типах доступа к регистрам. Если прочитать регистр, где флаг уже взведён, изменить соседний бит настройки и записать всё обратно, единица флага уйдёт в регистр и сбросит его. Пример — регистр UCSR0A у ATmega328P: бит настройки MPCM0 соседствует с флагом TXC0, который сбрасывается записью единицы. Даташит (разд. 20.9.1) предупреждает, что менять MPCMn командами SBI и CBI нельзя — TXCn может случайно сброситься. Сами SBI и CBI до UCSR0A не дотягиваются: он лежит по адресу 0xC0 в расширенном пространстве ввода-вывода, — так что на деле опасна строка на C вроде UCSR0A |= (1 << MPCM0);, которая компилируется в чтение, изменение и запись.

Как избежать

СпособГде
Запретить прерывания на время операциивезде, см. атомарный доступ
Побитовые команды SBI и CBIAVR, регистры ввода-вывода с адресами 0x00–0x1F
Запись 1 в PINxn для переключения выводаAVR: переключает PORTxn
Регистр GPIOx_BSRRSTM32: установка и сброс выводов без чтения
Bit-bandingCortex-M3/M4, если изготовитель его включил (функция необязательная): запись в алиас изменяет один бит

У ATmega328P, как отмечает даташит, команды SBI и CBI действуют только на указанный бит, в отличие от многих других AVR, поэтому их безопасно применять и к регистрам с флагами в пространстве ввода-вывода 0x00–0x1F. На STM32 регистр BSRR создан, по словам RM0090, именно для атомарного изменения выводов без риска, что прерывание вклинится между чтением и записью.

Работу с битами и масками в C разбирает статья о битовых операциях, а на практике с портами AVR — урок курса AVR с нуля о портах и регистрах.

Частые вопросы

Почему PORTB |= (1 << 5) может испортить соседний вывод?

Потому что это три шага: прочитать PORTB, установить бит, записать обратно. Если между чтением и записью сработает прерывание и изменит другой бит того же порта, основная программа запишет старое значение и затрёт это изменение. Защита — запрет прерываний на время операции или атомарная команда. На ATmega328P для регистров ввода-вывода с адресами 0x00–0x1F есть побитовые команды SBI и CBI, которые изменяют один бит без такого риска.

Чем опасна чтение-модификация-запись для регистров флагов?

Многие флаги сбрасываются записью единицы. Если прочитать регистр, где уже взведён флаг, поменять другой бит и записать всё обратно, единица флага запишется вместе с ним и сбросит его, и событие будет потеряно. На ATmega328P так бывает с регистром UCSR0A: в нём рядом лежат бит настройки MPCM0 и флаг TXC0, который сбрасывается записью единицы. Строка UCSR0A |= (1 << MPCM0) прочитает взведённый TXC0 и сбросит его. Даташит предупреждает об этом же для команд SBI и CBI, но UCSR0A находится по адресу 0xC0, вне их досягаемости, поэтому на практике риск создаёт именно чтение-модификация-запись в коде на C. Такие флаги сбрасывают прямой записью маски, без чтения.

Как установить бит атомарно на STM32?

Записью в регистр GPIOx_BSRR: единица в младших 16 битах устанавливает соответствующий вывод, в старших — сбрасывает, нули ничего не меняют. RM0090 прямо называет назначение BSRR — атомарное изменение без риска, что прерывание вклинится между чтением и записью. Для произвольных битов памяти и периферии на Cortex-M3 и M4 может быть bit-banding — это необязательная функция ядра, и у STM32F4 она есть.

Источники

  1. Microchip. ATmega48A/PA/88A/PA/168A/PA/328/P Data Sheet, DS40002061B, 2020 — разд. 8.5: регистры ввода-вывода с адресами 0x00–0x1F доступны побитово командами SBI и CBI; некоторые флаги состояния сбрасываются записью единицы, и, в отличие от большинства других AVR, у этих микроконтроллеров CBI и SBI действуют только на указанный бит, поэтому их можно применять к регистрам с такими флагами; разд. 14.2.2: запись единицы в PINxn переключает PORTxn; регистр UCSR0A расположен по адресу 0xC0 (Register Summary); разд. 20.9.1: не использовать команды чтения-модификации-записи SBI и CBI для бита MPCMn — он в одном регистре с флагом TXCn, который может случайно сброситься; разд. 27.8: заполнение буфера страницы до стирания позволяет сделать «чтение-изменение-запись» страницы флеш-памяти
  2. STMicroelectronics. RM0090 Reference manual STM32F405/415, STM32F407/417, STM32F427/437, STM32F429/439, Rev 21 — разд. 8.3: назначение регистра GPIOx_BSRR — атомарные операции чтения-изменения над регистрами GPIO, чтобы прерывание не могло вклиниться между чтением и записью; разд. 8.4.7: запись 1 в BSRR(i) устанавливает бит ODR(i), в BSRR(i+16) — сбрасывает, запись 0 не действует; разд. 2.3.3: запись слова в область алиасов bit-band равносильна операции чтения-изменения-записи целевого бита
  3. Arm. Cortex-M4 Devices Generic User Guide, DUI 0553B — разд. 2.2.5, Optional bit-banding: области bit-band в карте памяти SRAM и периферии — необязательная возможность ядра; запись в слово алиаса атомарно меняет один бит

Нашли ошибку?