Cortex-M3 был разработан для тяжелой многозадачности с малой задержкой и малым джиттером, то есть контроллер прерываний взаимодействует с ядром, чтобы сохранить гарантии на количество циклов с момента запуска прерывания до обработки прерывания. Ldrex / strex был реализован как способ взаимодействия со всем этим (под всем, что я имею в виду маскирование прерываний и другие детали, такие как установка атомарного бита с помощью псевдонимов битовых полос), в качестве одноядерного, не MMU, некэш-кэша µC иначе мало пользы для этого. Если бы он не реализовал это, задача с низким приоритетом должна была бы удерживать блокировку, и это могло бы генерировать инверсии с небольшим приоритетом, генерируя задержки и джиттер, что является сложной системой реального времени (это дизайн для этого, хотя концепция слишком широка) не может справиться, по крайней мере, не в порядке, допустимом семантикой «повторных попыток», которой обладает отказавший ldrex / strex.
Если учесть, что, строго говоря, с точки зрения синхронизации и джиттера, Cortex-M0 имеет более традиционный профиль синхронизации прерываний (то есть он не будет прерывать команды на ядре при поступлении прерывания), поскольку подвергается большему воздействию джиттер и латентность. По этому вопросу (опять же, строго по времени) он более сопоставим со старыми моделями (например, arm7tdmi), в которых также отсутствует атомная загрузка / изменение / сохранение, а также атомарные приращения и уменьшения и другие совместные инструкции с малой задержкой, требующие отключения / прерывания. включить чаще.
Я использую что-то подобное в Cortex-M3:
#define unlikely(x) __builtin_expect((long)(x),0)
static inline int atomic_LL(volatile void *addr) {
int dest;
__asm__ __volatile__("ldrex %0, [%1]" : "=r" (dest) : "r" (addr));
return dest;
}
static inline int atomic_SC(volatile void *addr, int32_t value) {
int dest;
__asm__ __volatile__("strex %0, %2, [%1]" :
"=&r" (dest) : "r" (addr), "r" (value) : "memory");
return dest;
}
/**
* atomic Compare And Swap
* @param addr Address
* @param expected Expected value in *addr
* @param store Value to be stored, if (*addr == expected).
* @return 0 ok, 1 failure.
*/
static inline int atomic_CAS(volatile void *addr, int32_t expected,
int32_t store) {
int ret;
do {
if (unlikely(atomic_LL(addr) != expected))
return 1;
} while (unlikely((ret = atomic_SC(addr, store))));
return ret;
}
Другими словами, он переводит ldrex / strex в хорошо известное условие Linked-Load и Store Conditional, а также реализует семантику сравнения и замены.
Если ваш код работает нормально только с помощью сравнения и замены, вы можете реализовать его для cortex-m0 следующим образом:
static inline int atomic_CAS(volatile void *addr, int32_t expected,
int32_t store) {
int ret = 1;
__interrupt_disable();
if (*(volatile uint32_t *)addr) == expected) {
*addr = store;
ret = 0;
}
__interrupt_enable();
return ret;
}
Это наиболее часто используемый шаблон, потому что он есть у некоторых архитектур (на ум приходит x86). Реализация эмуляции шаблона LL / SC с помощью CAS кажется мне ужасной. Особенно, когда SC более чем на несколько инструкций отделен от LL, но хотя и очень распространен, ARM не рекомендует его специально в случае Cortex-M3, потому что при любые прерывания приведут к сбою strex, если вы запустите если вы слишком долго переходите между ldrex / strex, ваш код будет тратить много времени на повторение цикла strex. Это злоупотребляет шаблоном, а не использует его.
Что касается вашего дополнительного вопроса, в случае cortex-m3 возвращение strex в регистр, потому что семантика уже была определена высокоуровневыми архитектурами (strex / ldrex существует в многоядерных ветвях, которые были реализованы до того, как armv7-m был определяется, и после него контроллеры кеша фактически проверяют адреса ldrex / strex, т.е. strex дает сбой только тогда, когда кеш не может доказать, что точка адреса не была изменена).
Если бы я размышлял, я бы сказал, что это было потому, что в ранние годы этот тип атомной системы разрабатывался в библиотеках: вы возвращали бы успех / неудачу в функциях, реализованных в ассемблере, и это должно было бы уважать ABI и большинство из них (все, что я знаю) использует регистр или стек, а не флаги для возврата значений. Это также может быть связано с тем, что компиляторы лучше используют раскраску регистров, чем сжимают флаги в случае, если их использует какая-то другая инструкция, т.е. рассматривают сложную операцию, которая генерирует флаги, и в середине у вас есть последовательность ldrex / strex и операция для этого нужны флаги: компилятору все равно придется перемещать флаги в регистр.