ATOMIC-макросы для ARM

Поклонникам контроллеров AVR и компилятора WinAVR хорошо знаком макрос
ATOMIC_BLOCK(SOMETHING),
с помощью которого обычно реализуется атомарный доступ. При переходе на контроллеры ARM возникает вопрос: а где мой любимый ATOMIC_BLOCK? А нету. Восполним этот пробел.

Атомарность требуется при доступе к разделяемому ресурсу из нескольких потоков/процессов программы. Можно выделить два основных типа операций:

  • доступ к общей памяти (переменные/флаги/регистры);
  • доступ к общей периферии (на уровне драйвера).

С первым типом особых проблем нет. Ядро ARM в отличие от простых 8-биток имеет несколько специализированных команд, предназначенных для обеспечения атомарности передачи данных. Это bit-banding, LDREXH/STREXH. Есть полезные примочки компилятора в виде барьеров памяти. RTOS также предоставляет семафоры/мьютексы/флаги и прочие блокировки процессов и участков кода.

С периферией немного сложнее. В идеале каждый периферийный модуль (к примеру, UART) должен быть закреплён за одним процессом. Например, printf рекомендуется вызывать только из главного цикла. При использовании многозадачной RTOS эту функцию следует окружить мьютексами.

Но что делать, если к периферии нужно обращаться из главного цикла и из прерывания? Честно говоря, это неправильно. Если такая необходимость возникла, следует пересмотреть свои взгляды на жизнь и заодно переделать алгоритм работы программы. Но мы люди упрямые, проблему будем решать по старинке в лоб — запретом прерываний.

Уточняю задачу:

Хочу, чтобы было как в WinAVR!

Допустим, имеется гипотетическое (на самом деле вполне реальное) устройство под управлением микроконтроллера STM32:

В схеме есть общая шина SPI, к которой подключено несколько внешних устройств. Одно из них — однокристальный трансивер. Проблема в том, что он имеет внутренний буфер на 64 байта, а хочется передавать/принимать пакеты по 256. Кто в теме, тот знает, что придётся подгружать/читать данные на лету в процессе радиопередачи. А так как к сожалению продвинутые программеры не умеют использовать пустые циклы ожидания, то приходится работать по прерываниям. И вот здесь никак не обойтись без блокировки прерываний, так как SPI-передача может понадобиться как в прерывании, так и в основном потоке.

Глобальные прерывания запретить не проблема, CMSIS предоставляет соответствующие макросы __enable_irq()/__disable_irq(), на асме — это команды cpsie i и cpsid i. Прочитать статус, а также напрямую разрешить/запретить прерывания можно через регистр PRIMASK.

Каждую SPI-передачу приходится оформлять в виде
__disable_irq();
LE_LOW;
spi_send(data);
LE_HIGH;
__enable_irq();

А в WinAVR гораздо симпатичнее (конечно, это дело вкуса, но мне так больше нравится):
ATOMIC_BLOCK(ATOMIC_FORCEON)
{
   LE_LOW;
   spi_send(data);
   LE_HIGH;
}

А как сделать так, чтобы сюда "<-" компилятор сам вставил соответствующие команды?
{
   <-----------  __disable_irq();
   LE_LOW;
   spi_send(data);
   LE_HIGH;
   <-----------  __enable_irq();
}

На помощь приходит стандартный си-оператор for. Вспоминаем синтаксис:
for ( выражение 1; выражение 2; выражение 3 ) оператор
Выражение 1 вычисляется однократно перед выполнением цикла.
Выражение 2 — проверка условия повторения цикла, рассчитывается каждый раз перед выполнением оператора.
Если Выражение 2 = true, выполняется оператор, затем — Выражение 3. И цикл повторяется, начиная с расчёта Выражения 2.

Главная фишка в том, что в выражениях 1/2/3 можно использовать функции. Простые или inline. И всё, что мы там аккуратно натворим, компилятор может ужать до нескольких простых команд.

Итак, нам нужно:
— в выражении 1 запретить прерывания, инициализировать вспомогательный флаг;
— в выражении 2 — проверить вспомогательный флаг;
— в выражении 3 разрешить прерывания и сбросить вспомогательный флаг, чтобы завершить цикл.

ATOMIC_BLOCK_FORCEON

Пишем макрос и две инлайн-функции:
inline static int _irqDis(void)
{
    __ASM volatile ("cpsid i" : : : "memory");
    return 1;
}

inline static int _irqEn(void)
{
    __ASM volatile ("cpsie i" : : : "memory");
    return 0;
}

#define ATOMIC_BLOCK_FORCEON \
    for(int flag = _irqDis();\
        flag;\
        flag = _irqEn())
Применяем:
main()
{
    ATOMIC_BLOCK_FORCEON
    {
        do_something();
    }
}

Смотрим, что там насочиняли компилер с линкером (не забыть указать ключ Os либо O3):
ATOMIC_BLOCK_FORCEON { LED_GREEN_ON; }

// запретить прерывание
800b424: b672      cpsid	i
// выполнить некое действие
800b426: 4b02      ldr      r3, [pc, #8]
800b428: 2208      movs     r2, #8
800b42a: 611a      str      r2, [r3, #16]
// разрешить прерывание
800b42c: b662      cpsie    i
Отлично, то что доктор прописал.

ATOMIC_BLOCK_RESTORATE
Усложняем задачу: при входе в защищаемый блок сохранить флаг запрещения глобальных прерываний, при выходе — восстановить.
inline static int _iDisGetPrimask(void)
{
    int result;
    __ASM volatile ("MRS %0, primask" : "=r" (result) );
    __ASM volatile ("cpsid i" : : : "memory");
    return result;
}

inline static int _iSetPrimask(int priMask)
{
    __ASM volatile ("MSR primask, %0" : : "r" (priMask) : "memory");
    return 0;
}

#define ATOMIC_BLOCK_RESTORATE \
     for(int mask = _iDisGetPrimask(), flag = 1;\
         flag;\
         flag = _iSetPrimask(mask))

Здесь вместо восстановления прерывания командой cpsie i мы просто восстаналиваем состояние регистра PRIMARY (урезанный аналог SREG у AVR).

В результате имеем:
ATOMIC_BLOCK_RESTORATE { LED_GREEN_ON; }

// прочитать флаг i
800b430: f3ef 8310 mrs	r3, PRIMASK
// запретить прерывания
800b434: b672      cpsid	i
// выполнить некое действие
800b436: 4a03      ldr     r2, [pc, #12]
800b438: 2108      movs    r1, #8
800b43a: 6111      str     r1, [r2, #16]
// восстановить флаг i
800b43c: f383 8810 msr     PRIMASK, r3
Сойдёт, ничего лишнего.

условный ATOMIC_BLOCK_FORCEON
И на закуску — высший пилотаж. В программе потребовалась поддержка двух версий аппаратной платформы. После переподключения периферии в новой версии запрещать прерывания уже не требовалось…

Делаем макрос с условием: если true, прерывания должны запрещаться, иначе — прерывания не трогать. Просто добавим проверку условия, а компилятор дальше сам разберётся (при включенной оптимизации естественно):
inline static int _irqDis(int flag)
{
    if (flag)
        __ASM volatile ("cpsid i" : : : "memory");
    return 1;
}

inline static int _irqEn(int flag)
{
    if (flag)
        __ASM volatile ("cpsie i" : : : "memory");
    return 0;
}

#define ATOMIC_BLOCK_FORCEON_COND(condition) \
    for(int cond = condition, flag = __irqDis(cond);\
        flag;\
        flag = _irqEn(cond))
Здесь условие помещается в отдельную переменную cond, иначе проверку условия (а туда можно поместить и функцию) компилятор подставит дважды.

Результат:
ATOMIC_BLOCK_FORCEON_COND(var == 10) { LED_GREEN_ON; }

// проверка заданного условия
800b508: 4b06      ldr     r3, [pc, #24]
800b50a: 681b      ldr     r3, [r3, #0]
800b50c: f1b3 020a subs.w  r2, r3, #10
800b510: 4253      negs    r3, r2
800b512: 4153      adcs    r3, r2
// если условие не выполнено - пропустить команду
800b514: b103      cbz     r3, 800b518
800b516: b672      cpsid   i
// выполнить некое действие
800b518: 4a03      ldr     r2, [pc, #12]
800b51a: 2108      movs    r1, #8
800b51c: 6111      str     r1, [r2, #16]
// если условие не выполнено - пропустить команду
800b51e: b103      cbz     r3, 800b522
800b520: b662      cpsie   i
Тоже неплохо.

Аналогично:

условный ATOMIC_BLOCK_RESTORATE

static __inline int _irqDisGetPrimask(int flag)
{
    if (flag)
    {
        int result;
        __ASM volatile ("MRS %0, primask" : "=r" (result) );
        __ASM volatile ("cpsid i" : : : "memory");
        return result; 
    }
    else
        return 0;
}

static __inline int _irqSetPrimask(int priMask)
{
    __ASM volatile ("MSR primask, %0" : : "r" (priMask) : "memory");
    return 0;
}

#define ATOMIC_BLOCK_RESTORATE_COND(C) \
    for(int cond = c, mask = _irqDisGetPrimask(cond), flag = 1;\
        flag;\
        flag = cond ? _irqSetPrimask(mask):0)

Результат:
ATOMIC_BLOCK_RESTORATE_COND(ddd == 10) { LED_GREEN_ON; }

// проверить условие
800b524: 4b09      ldr     r3, [pc, #36]
800b526: 681b      ldr     r3, [r3, #0]
800b528: f1b3 020a subs.w  r2, r3, #10
800b52c: 4253      negs    r3, r2
800b52e: 4153      adcs    r3, r2
// пропустить, если условие не выполнено
800b530: b11b      cbz     r3, 800b53a
800b532: f3ef 8210 mrs     r2, PRIMASK
800b536: b672      cpsid   i
// а это зачем?
800b538: e000      b.n     800b53c
800b53a: 461a      mov     r2, r3
// выполнить некое действие 
800b53c: 4904      ldr     r1, [pc, #16]
800b53e: 2008      movs    r0, #8
800b540: 6108      str     r0, [r1, #16]
// пропустить, если условие не выполнено
800b542: b10b      cbz     r3, 800b548
800b544: f382 8810 msr     PRIMASK, r2

А здесь появились какие-то хитрые манипуляции с регистрами r2/r3. Таким образом компилятор пытается «протолкнуть» результат «0» из функции _irqDisGetPrimask(), который в общем-то никому и не нужен. Упрощаем эту функцию:
static __inline int _irqDisGetPrimask(int flag)
{
    int result;
    if (flag)
    {
        __ASM volatile ("MRS %0, primask" : "=r" (result) );
        __ASM volatile ("cpsid i" : : : "memory");
    }
    return result; 
}

Ну вот, совсем другое дело:
// проверить условие
800b508: 4b08      ldr     r3, [pc, #32]
800b50a: 681b      ldr     r3, [r3, #0]
800b50c: f1b3 010a subs.w  r1, r3, #10
800b510: 424b      negs    r3, r1
800b512: 414b      adcs    r3, r1
// если условие не выполнено - пропустить 2 команды
800b514: b113      cbz     r3, 800b51c
800b516: f3ef 8210 mrs     r2, PRIMASK
800b51a: b672      cpsid   i
// выполнить полезную работу
800b51c: 4904      ldr     r1, [pc, #16]
800b51e: 2008      movs    r0, #8
800b520: 6108      str     r0, [r1, #16]
// пропустить, если условие не выполнено
800b522: b10b      cbz     r3, 800b528
800b524: f382 8810 msr     PRIMASK, r2

Но теперь компилятор предупреждает, что «переменная-то может быть неинициализированной». Убеждаем его, что так и надо:
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wmaybe-uninitialized"
...  // здесь наши __inline функции
#pragma GCC diagnostic pop

Ещё один момент: в режиме Os компилятор GCC иногда решает, что наши inline-функции лучше не инлайнить, места меньше занимать будут. Говорим ему, что мы с ним не согласны, при помощи дополнительного атрибута
__attribute__((always_inline)).

В заключение повторю: запрет прерываний — не самый лучший способ решения проблем, аккуратнее планируйте свою программу. И почаще заглядывайте в ассемблерный листинг. Познавательно и интересно.

upd: из такого atomic-блока нельзя принудительно выходить (при помощи goto/break/return), иначе прерывание не будет восстановлено.
  • +14
  • 30 ноября 2013, 09:59
  • MikeSmith
  • 1
Файлы в топике: atomic.h.zip

Комментарии (30)

RSS свернуть / развернуть
Урок последовательности, ясности и логичности изложения. И достаточно коротко, за что аж завидую :)
Плюсую с радостью!

З.Ы. Даже не имеет значения, если сейчас настоящие знатоки (без иронии) начнут придираться к сути. Хотя, ИМХО, автор достаточно четко оговорился касательно некашерности самого подхода. Так что моей фантазии не хватает представить, за что же ЩАС начнут шпынять уважаемого автора :)
+1
А можете неграмотному объяснить синтаксис команд:

__ASM volatile ("MRS %0, primask" : "=r" (result) );
__ASM volatile ("cpsid i" : : : "memory");
__ASM volatile ("MSR primask, %0" : : "r" (priMask) : "memory");

Зачем там двоеточие и %0, что там вообще происходит и где про это можно подробнее почитать?
0
В двух словах это не объяснить, пока отсылаю к официальной документации или сюда. Где-то видел и переводной вариант… А по поводу ассемблерных команд вечером перед сном читаем официальную документацию на соответствующее ядро ARM.
0
Есть и в сообществе статья по этой теме.
0
Стал разбираться, оказывается эти же конструкции с двоеточими и в иаре работают, только Constraints отличаются.
0
здесь неплохо рассказано
0
… упс, выше уже есть ссылка
0
А можно просто взять эти функции из CMSIS
0
PRIMARY (аналог SREG у AVR)
… аналог SREG в Cortex-M xPSR, а в PRIMASK всего лишь один бит, при помощи которого можно отключить все прерывания (кроме NMI и HardFault)
PRIMASK = 1; // prevents the activation of all exceptions with configurable priority
0
… также в примере с ATOMIC_BLOCK_RESTORESTATE отсутствует cpsie i в конце блока
0
Состояние флага восстанавливается командой
msr PRIMASK, r2
0
… да, вы правы, хотя стоит отметить в статье что вы делаете вложенные критические секции, и вместо
i++;
__disable_irq();
...
i--;
__enable_irq();

вы используете
mrs r2, PRIMASK
cpsid i
...
msr PRIMASK, r2
0
… что-то в этом роде, для полноты картины
0
… упс, нашел, не внимательно читал
0
Угу, ваша правда. Аналогия заключается в том, что и там, и там управлять разрешением/запретом прерываний можно не только специализированными командами sei/cli (cpsid i/cpsie i), но и прямой записью требуемого состояния в указанные регистры. Только у AVR — это флаг разрешения прерываний, а у ARM — запрета.
0
не совсем понятен смысл.
Если на 32х-битке RTOS всё предоставляет, и при этом использовать периферию из юзерспейса — не правильно (по вашим же словам), то зачем на это идти? Почему не использовать периферию только в обработчике прерывания, окружая то, что надо, мутексами?

Кстати, вы хотели сказать bit-band
0
1. Я не использую в своих проектах RTOS. А использовать тот же SPI только в прерывании не всегда возможно и удобно. Особенно, когда к общей шине подключено много устройств.
2. Конечно, исправил. На будущее предлагаю об «очепятках» и прогих неграмотностях сообщать через личку, чтобы не захламлять топик.
0
Отличная статья, продолжайте в том же духе.
0
В CMSIS вообще-то прописано как квазикомпиляторонезависимо взять состояние PRIMASK и восстановить.
Получается
#define __enter_critical() {uint32_t flag; flag = __get_PRIMASK();
#define __exit_critical()  __set_PRIMASK(flag);}

из чего
#define _Atomic(X) __enter_critical(); {X}; __exit_critical();
+2
Любая задача имеет множество вариантов решения. Сложное и простое, красивое, и не очень. Главное, чтобы результат был правильный (работоспособный). «Квазикомпиляторонезависимые» решения — это тоже хорошо. Хотя, пользоваться ломаным Кейлом или Иаром совесть не позволяет (а демоверсии имеют известные ограничения), поэтому проблемы отличия компиляторов меня пока не волнуют.

Предложенный вариант тоже имеет право на жизнь (если добавить команду __disable_irq() в функцию __enter_critical()). Но лично мне не нравится, что критический блок нужно помещать в круглые скобки. Да, мелочь, но в си принято использовать {}.
И давайте посмотрим, что получится в итоге (-Os):
// считать PRIMASK
80002f6: f003 fc3b bl    8003b70 <__get_PRIMASK>
// запрет прерываний
80002fa: b672      cpsid i
// выполнить полезную работу
80002fc: 4c3a      ldr   r4, [pc, #232]
80002fe: f04f 5380 mov.w r3, #268435456
8000302: 6123      str   r3, [r4, #16]
// сохранить PRIMASK
8000304: f003 fc37 bl    8003b76 <__set_PRIMASK>
Вот так компилятор непринуждённо оформил две простые команды MRS/MSR в виде функций. Почему? Потому что так реализованы функции __get_PRIMASK()/__set_PRIMASK() например в CMSIS v1.30 для Cortex-M3. И компилятор ещё нужно постараться убедить «проинлайнить» эти простые функции.
0
Да, конечно забыл о главном:) — __disable_irq(). Насчет проинлайнить — действительно тяжело заставить — очень часто компиляторы считают inline просто рекомендацией — бывает приходится напрягать прагмами типа inline force и т.п. С другой стороны без барьера __ISB() даже при ассемблерных вставках можно получить не то, что написано — в этом деле запаковывание в функцию само по себе барьер. В принципе остальные способы (bitband, Set/Clear без RMW и прочая) позволяют уклониться от использования таких жестких способов обеспечения атомарности, как глобальное запрещение прерываний. Так что, ИМХО, лучше не трогать PRIMASK без крайней необходимости, а если уж нужно, тогда если без использования функций, то обкладываться барьерами.
0
Компилятор плевать хотел на __ISB, он не знает что это за инструкция.
Чтобы компилятор не мешал команды из разных блоков, надо ставить ассемблерному блоку флаг volatile, тогда инструкции из этого ассемблерного блока не будут смешиваться с инструкциями ни до, ни после. А простейший барьер выглядит как-то так:

__asm volatile ("");
0
Зачем-то есть в файле core_cmInstr.h и работает по назначению.
/** \brief  Instruction Synchronization Barrier

    Instruction Synchronization Barrier flushes the pipeline in the processor,
    so that all instructions following the ISB are fetched from cache or
    memory, after the instruction has been completed.
 */
#define __ISB()                           __isb(0xF)
0
Это был мой ответ на фразу «С другой стороны без барьера __ISB() даже при ассемблерных вставках можно получить не то, что написано».

Так вот, «не то, что написано» получается оттого, что если у ассемблерной вставки нет атрибута volatile, компилятор разбирает её на отдельные ассемблерные команды и может смешать с командами от сишного кода по своему усмотрению, как решит оптимизатор.

Если хочется предотвратить смешивание инструкций из блока asm(), надо этому блоку поставить атрибут volatile. В файле core_cmInstr.h кстати так оно и стоит:

__attribute__( ( always_inline ) ) __STATIC_INLINE void __ISB(void)
{
__ASM volatile («isb»);
}

Это сделано для того, чтобы компилятор не передвинул инструкцию isb куда-нибудь.
0
Вы утверждали, что компилятор не знает, что такое __ISB(). Далее Вы пытаетесь доказать, что ассемблерные инструкции, вписанные в определенном порядке(пусть прямо в код без участия компилятора), всегда выполнятся на Cortex-M в том же порядке. Это, к сожалению, не так. И инструкцию сброса конвейера придумали не просто чтобы она была.
0
Нет, это было недопонимание.
Я имею в виду, что компилятор понятия не имеет как _работает_ инструкция ISB. И поэтому он может её передвинуть ровно так же, как передвинул бы любую другую инструкцию — если не ставить атрибут volatile.

Про то, в каком порядке инструкции выполняются я не говорил ни слова. Я говорил лишь о порядке, в котором инструкции разместит компилятор в ассемблерном файле. Так вот, компилятор может инструкции менять местами по собственному разумению, причём и инструкции из asm() блока тоже, если на нём нет атрибута volatile.
0
Инструкции без всяких компиляторов всё-равно будут «перемешиваться» в рантайме. ISB() описано в файле core_cmInstr.h так, чтобы для выбранного компилятора установились все нужные атрибуты. А запись «простейшего барьера» __asm volatile (""); лишь конкретный компилятор может понять как некий барьер (они есть для данных, есть для инструкций).
0
Там еще FIQ есть. Причем емнип на них сидит прерывание систика.
0
Там еще FIQ есть
В ARM7 FIQ есть, но там нет SysTick.
Причем емнип на них сидит прерывание систика.
Скорее на FIQ висит прерывание Hard_Fault.
0
Вообще, синтаксис позволяет выделять инструкции через запятую, таким образом, #define можно без доп функций:

#define ATOMIC_BLOCK_FORCEON \
    for(int flag = 1, __disable_irq();\
        flag;\
        flag = 0, __enable_irq())
0
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.