When writing microcontroller code in assembler, it's nice to be able to keep some commonly used variables in registers. The 8-bit AVR devices in particular have a nice big set of registers, most of which are rarely used in compiled code. Usually it's a waste of time to write all the code in assembler, though - it's much better to write the non time-critical bits in C, compile them with GCC and then link with the assembly code bits.
When accessing the register variables from the C code, the natural thing to do is just to make a global register variable:
register uint8_t frame __asm__ ("r3"); |
That has two effects - it allows you do access the variable as if it were a normal variable:
void initFrame() { frame = 0; } |
And it prevents the compiler from using r3
for something else (though one also has to be careful that the register isn't used by any other linked in libraries, including the C library and the compiler support functions in libgcc).
The trouble comes when you try to read those register variables. If optimizations are turned on, then the following code might just be an infinite loop:
void waitForFrame(int frameToWaitFor) { while (frame != frametoWaitFor); } |
The compiler hoists the read of frame
outside the loop, and never sees the updates. If frame
was a normal variable we could fix this just by adding volatile
, but using volatile
with a register variable doesn't work. This seems odd until we think about what volatile
actually means. A read from or write to a volatile
variable is considered an observable effect (like doing IO) so the compiler won't optimize it away. But the compiler has no concept of a "read from" or "write to" a register - registers are just used or not used and the optimizations around them are unaffected by the notion of volatile
.
There is a reasonably easy and not-too-invasive way to fix this, though, through the use of inline assembly. If you write:
#define getFrame() ({ \ __asm__ volatile ("" : "=r"(frame)); \ frame; \ }) void waitForFrame(int frameToWaitFor) { while (getFrame() != frametoWaitFor); } |
The compiler will treat the ""
as a block of assembly code which writes to r3
and which has unknown side effects (so that it can't be hoisted out of a loop for example). The code doesn't actually do anything (so the generated code won't be adversely affected) but it essentially provides a read barrier to the register. Unfortunately you can't use getFrame()
to write back to frame
, so to increment it for example you have to do frame = getFrame() + 1;
but that's actually kind of helpful because it makes the possibility of a race condition (for example by an interrupt routine also incrementing frame
at the same time) more obvious.