I see several potential problems with those critical sections. There are caveats and solutions to all of these, but as a summary:
- There's nothing preventing the compiler from moving code across these macros, for optimization or random other reasons.
- They save and restore some parts of the processor state the compiler expects inline assembly to leave alone (unless it's told otherwise).
- There's nothing preventing an interrupt from occurring in the middle of the sequence and changing the state between when it's read and when it's written.
First off, you definitely need some compiler memory barriers. GCC implements these as clobbers. Basically, this is a way to tell the compiler "No, you can't move memory accesses across this piece of inline assembly because it might affect the result of the memory accesses." Specifically, you need both "memory"
and "cc"
clobbers, on both the begin and end macros. These will prevent other things (like function calls) from being reordered relative to the inline assembly too, because the compiler knows they might have memory accesses. I have seen GCC for ARM hold state in condition code registers across inline assembly with "memory"
clobbers, so you definitely do need the "cc"
clobber.
Secondly, these critical sections are saving and restoring a lot more than just whether interrupts are enabled. Specifically, they're saving and restoring most of the CPSR (Current Program Status Register) (the link is for Cortex-R4 because I couldn't find a nice diagram for an A9, but it should be identical). There are subtle restrictions around which pieces of state can actually be modified, but it's more than necessary here.
Among other things, this includes the condition codes (where the results of instructions like cmp
are stored so subsequent conditional instructions can act on the result). The compiler will definitely be confused by this. This is easily solvable using the "cc"
clobber as mentioned above. However, this will make code fail every time, so it doesn't sound like what you're seeing problems with. Somewhat of a ticking time bomb though, in that modifying random other code might cause the compiler to do something a little different which will be broken by this.
This will also attempt to save/restore the IT bits, which are used to implement Thumb conditional execution. Note that if you never execute Thumb code, this doesn't matter. I've never figured out how GCC's inline assembly deals with the IT bits, other than concluding it doesn't, meaning the compiler must never put inline assembly in an IT block and always expects the assembly to end outside of an IT block. I've never seen GCC generate code violating these assumptions, and I've done some fairly intricate inline assembly with heavy optimization, so I'm reasonably sure they hold. This means it probably won't actually attempt to change the IT bits, in which case everything is fine. Attempting to modify these bits is classified as "architecturally unpredictable", so it could do all kinds of bad things, but probably won't do anything at all.
The last category of bits which will be saved/restored (besides the ones to actually disable interrupts) are the mode bits. These probably won't change, so it probably won't matter, but if you have any code that deliberately changes modes these interrupt sections could cause problems. Changing between privileged and user mode is the only case of doing this I would expect.
Third, there's nothing preventing an interrupt from changing other parts of CPSR between the MRS
and MSR
in ARM_INT_LOCK
. Any such changes could be overwritten. In most reasonable systems, asynchronous interrupts don't change the state of the code they're interrupt (including CPSR). If they do, it becomes very hard to reason about what code will do. However, it is possible (changing the FIQ disable bit seems most likely to me), so you should consider if your system does this.
Here's how I would implement these in a way which addresses all the potential issues I pointed out:
#define ARM_INT_KEY_TYPE unsigned int
#define ARM_INT_LOCK(key_) \
asm volatile(\
"mrs %[key], cpsr\n\t"\
"ands %[key], %[key], #0xC0\n\t"\
"cpsid if\n\t" : [key]"=r"(key_) :: "memory", "cc" );
#define ARM_INT_UNLOCK(key_) asm volatile (\
"tst %[key], #0x40\n\t"\
"beq 0f\n\t"\
"cpsie f\n\t"\
"0: tst %[key], #0x80\n\t"\
"beq 1f\n\t"\
"cpsie i\n\t"
"1:\n\t" :: [key]"r" (key_) : "memory", "cc")
Make sure to compile with -mcpu=cortex-a9
because at least some GCC versions (like mine) default to an older ARM CPU which doesn't support cpsie
and cpsid
.
I used ands
instead of just and
in ARM_INT_LOCK
so it's a 16-bit instruction if this is used in Thumb code. The "cc"
clobber is necessary anyways, so it's strictly a performance/code size benefit.
0
and 1
are local labels, for reference.
These should be usable in all the same ways as your versions. The ARM_INT_LOCK
is just as fast/small as your original one. Unfortunately, I couldn't come up with a way to do ARM_INT_UNLOCK
safely in anywhere near as few instructions.
If your system has constraints on when IRQs and FIQs are disabled, this could be simplified. For example, if they're always disabled together, you could combine into one cbz
+ cpsie if
like this:
#define ARM_INT_UNLOCK(key_) asm volatile (\
"cbz %[key], 0f\n\t"\
"cpsie if\n\t"\
"0:\n\t" :: [key]"r" (key_) : "memory", "cc")
Alternatively, if you don't care about FIQs at all then it's similar to just drop enabling/disabling them completely.
If you know that nothing else ever changes any of the other state bits in CPSR between the lock and unlock, then you could also use continue with something very similar to your original code, except with both "memory"
and "cc"
clobbers in both ARM_INT_LOCK
and ARM_INT_UNLOCK
ldrex
andstrex
to do it properly. Here's a webpage showing you how to useldrex
andstrex
to implement a spinlock. \$\endgroup\$