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

Критическая секция

Когда основная программа и обработчик прерывания работают с одной переменной, некоторые участки кода надо выполнять без перерыва. Такой участок называют критической секцией: на время его выполнения прерывания запрещают, а потом восстанавливают как было.

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

Содержание

Критическая секция (critical section) — участок программы, который должен выполниться целиком, без вмешательства обработчика прерывания или другой задачи. В микроконтроллерах без операционной системы её почти всегда делают запретом прерываний: секция начинается с их выключения и заканчивается восстановлением прежнего состояния. Нужна она там, где иначе возникает состояние гонки: чтение многобайтной переменной, которую меняет обработчик, или изменение общей структуры данных.

AVR

Команда CLI запрещает прерывания сразу, но регистр SREG с флагом разрешения аппаратно не сохраняется — это делает программа. avr-libc даёт макрос ATOMIC_BLOCK(ATOMIC_RESTORESTATE): он сохраняет SREG, запрещает прерывания и при выходе из блока — в том числе через break или return — возвращает SREG как было. Вариант ATOMIC_FORCEON в конце всегда включает прерывания. Есть тонкость: cli() и sei() — барьер памяти для компилятора, но не для вычислений, и avr-libc показывает, как деление может «переехать» внутрь секции и удлинить её.

Cortex-M

РегистрЧто запрещает
PRIMASK = 1все исключения с настраиваемым приоритетом
FAULTMASK = 1все, кроме NMI
BASEPRI = N (байт приоритета: у STM32F1 уровень 5 → 0x50)исключения с численным значением приоритета ≥ N; BASEPRI = 0 — маска выключена

CMSIS даёт функции __disable_irq() (команда CPSID I), __get_PRIMASK(), __set_PRIMASK(), __set_BASEPRI(). Arm в AN321 отмечает, что после CPSID отдельный барьер не нужен. BASEPRI позволяет оставить работать самые срочные прерывания — приоритеты задаёт NVIC. Порог записывается сдвинутым, как байт приоритета: у STM32F1 реализованы только 4 старших бита, поэтому уровень 5 — это __set_BASEPRI(5 << (8 - __NVIC_PRIO_BITS)), то есть 0x50. Вызов __set_BASEPRI(5) запишет значение, младшие биты которого не реализованы, — получится 0, и маска не включится.

Правила

  • Секция как можно короче: пока прерывания запрещены, растёт задержка их обслуживания, а повторные события одного прерывания могут потеряться.
  • Сохранять и восстанавливать состояние, а не включать прерывания безусловно.
  • Если операция и так неделима — одно чтение выровненного слова на Cortex-M или байта на AVR, — секция не нужна, см. атомарная операция.

Не путать с мьютексом операционной системы: он блокирует только задачи, которые его ждут, а прерывания продолжают работать. Примеры с общим счётчиком, кольцевым буфером и сном без гонки — в статье «Прерывания и общие данные»; значения порогов BASEPRI посчитает калькулятор приоритетов NVIC.

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

Почему нельзя просто писать cli() и sei() вокруг кода?

Потому что sei() в конце включит прерывания, даже если до входа они были выключены — например, если функцию вызвали из другой критической секции или из обработчика. Правильно сохранять состояние и восстанавливать его: на AVR это делает ATOMIC_BLOCK(ATOMIC_RESTORESTATE), который возвращает SREG, на Cortex-M — пара __get_PRIMASK и __set_PRIMASK.

Пропадёт ли прерывание, пришедшее во время критической секции?

Нет, если оно одно: флаг прерывания запоминается, и обработчик вызовется сразу после включения прерываний. Но если за время запрета одно и то же событие произойдёт дважды, флаг один, и обработчик вызовется один раз — второе событие потеряется. Поэтому секции держат короткими.

Зачем BASEPRI, если есть PRIMASK?

PRIMASK запрещает все настраиваемые прерывания, а BASEPRI — только с приоритетом, равным порогу и ниже. Самые срочные прерывания продолжают работать, пока основной код защищает данные от менее важных. Порог пишут в формате байта приоритета: у STM32F1 уровень 5 — это 5 << (8 − __NVIC_PRIO_BITS) = 0x50, а __set_BASEPRI(5) запишет 0, и маска не включится. У Cortex-M0 регистра BASEPRI нет.

Источники

  1. AVR-LibC 2.3.2: <avr/interrupt.h>, <util/atomic.h>, <avr/sleep.h>, Interrupts (interrupts.dox), Problems with Reordering Code (reordering-code.dox) — util/atomic.h: ATOMIC_BLOCK(ATOMIC_RESTORESTATE) восстанавливает SREG, ATOMIC_FORCEON всегда включает прерывания; sei()/cli() — одна команда и барьер памяти; reordering-code.dox: компилятор может перенести вычисления внутрь участка между cli и sei
  2. Microchip. ATmega48A/PA/88A/PA/168A/PA/328/P Data Sheet, DS40002061B, 2020 — SREG аппаратно не сохраняется; CLI действует сразу; пример сохранения SREG и CLI; флаги прерываний, пришедших при запрете, запоминаются
  3. Arm. Cortex-M3 Devices Generic User Guide, DUI 0552A, 2010 — PRIMASK запрещает все исключения с настраиваемым приоритетом, FAULTMASK — все, кроме NMI, BASEPRI = N — исключения с численным значением приоритета ≥ N, BASEPRI = 0 — маска выключена; функции CMSIS __disable_irq (CPSID I), __get/__set_PRIMASK, __set_BASEPRI
  4. Arm. Application Note 321: ARM Cortex-M Programming Guide to Memory Barrier Instructions, DAI 0321A, 2012 — после CPSID барьер не нужен

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