Break Points

Disabling Interrupts

Jack Ganssle

3/22/2010 11:48 AM EDT

I wish we lived in an atomic world.

No, I am not yearning for an Iranian bomb. Rather, I am referring to the fact that unavoidable non-atomic accesses to shared resources causes much grief and poor code.

I read a lot of code, and find much that handles non-atomic accesses in this manner:

long global_var;
void do_something(void)
{

// Handle a non-atomic access to "global_var"
#pragma disable interrupts somehow
// Do something non-atomic to global_var
#pragma enable interrupts somehow
}

This construct suffers from a number of problems, not the least of which is that it's not generally reuseable. If the function is called from some place with interrupts off, it returns with them on, disrupting the system's context.

Usual solutions involve saving and restoring the interrupt state. But that, too, is fraught with peril. Optimizers are very aggressive today, and in some cases can reorder statement execution to the point where interrupts aren't disabled at the right point. The result: all that atomic-work may not work.

I've asked several compiler vendors for their take, since they understand the optimizations the compilers do better than anyone. The most interesting and complete response came from Greg Davis of Green Hills Software, and he has graciously allowed me to reprint it here:

"What we recommend for Green Hills customers is to use intrinsic functions for disabling and restoring interrupts. What this looks like is:

#include
int global_var;
void foo(void)
{
    // Disable interrupts and return "key"
    // that expresses current interrupt state unsigned int key = __DIR();

    // Code that handles global_var in a non-atomic way

    // Restore interrupts to state expressed by "key"
    __RIR(key);
}"

These Green Hills intrinsics for __DIR() and __RIR() generate different assembly code depending on the architecture and CPU that you are compiling for, but their interface is the same. The compiler considers the system-instructions that these intrinsics generate to be non-swappable, so the code that manipulates global_var will not be swapped across them.

With GNU, people tend to prefer to use inline assembly. These assembly statements are typically embedded in inline functions or macros with GNU statement expressions. An implementation of something like this on an ARM7TDMI might look like:

static inline unsigned int
disable_interrupts_reentrant(void)
{
    unsigned int ret;
    asm volatile(
        "mrs %0,cpsr\n"
        "orr r1,%0,192\n"
        "msr cpsr_cxsf,r1\n"
        : /* output */ "=r" (ret)
        : /* input */
        : /* clobbers */ "r1", "memory"
        );
    return ret;
}

static inline void restore_interrupts(unsigned int state)
{
    asm volatile(
        "and r1,%0,192\n"
        "mrs r0,cpsr\n"
        "bic r0,r0,192\n"
        "orr r0,r0,r1\n"
        "msr cpsr_cxsf,r0\n"
        : /* output */
        : /* input */ "r" (state)
        : /* clobbers */ "r0", "r1", "memory"
        );
}

int global_var;
void foo(void)
{
    unsigned int key = disable_interrupts_reentrant();

    // Code that handles global_var in a non-atomic way

    restore_interrupts(key);
}

At least to my understanding, the combination of the declaring the assembly to be volatile and putting the "memory" in the clobbers list should ensure that memory accesses in the critical section stay in the critical section.

Both of the above approaches involve compiler-specific extensions. The best approach I'm aware of that isn't compiler specific is to move the code into another file so it just looks like a function call to the compiler:

extern unsigned int disable_interrupts_reentrant(void);
extern void restore_interrupts(unsigned int state);
int global_var;
void foo(void)
{
    unsigned int key = disable_interrupts_reentrant();

    // Code that handles global_var in a non-atomic way

    restore_interrupts(key);
}

and then to define the functions in a separate assembly file. The exact assembly syntax may vary between implementations, but it may look something like this on a traditional UNIX-style assembler.

    .text
    .globl disable_interrupts_reentrant
disable_interrupts_reentrant:
    ; Inputs: none
    ; Outputs: r0 (return register) contains a key for the
    ; current interrupt state
    mrs r0, cpsr
    orr r1, r0, 192
    msr cpsr_cxsf, r1
    bx lr
    .type disable_interrupts_reentrant,@function
    .size disable_interrupts_reentrant, .-
    disable_interrupts_reentrant

    .globl restore_interrupts
restore_interrupts:
    ; Inputs: r0: prior interrupt state ;
    Outputs: None
    and r1, r0, 192
    mrs r0, cpsr
    bic r0, r0, 192
    orr r0, r0, r1
    msr cpsr_cxsf,r0
    bx lr
    .type restore_interrupts,@function
    .size restore_interrupts,.-restore_interrupts

Since compilers need to assume that external functions read and write all global variables, there's no chance for the code that handles global_var to fall outside of the critical section."

Thanks, Greg, for the insight. I hope this information is useful to folks.

Jack G. Ganssle is a lecturer and consultant on embedded development issues. He conducts seminars on embedded systems and helps companies with their embedded challenges. Contact him at jack@ganssle.com. His website is www.ganssle.com.





Lundin

3/23/2010 3:42 AM EDT

How exactly will compiles assume that external functions access global variables? Since the globals aren't volatile in any of these examples, and since they are to be accessed by an ISR, there is just no way the compiler can "assume" anything.

You need to make all those global variables volatile or you are in for even more serious bugs than what non-atomic access would cause. Non-volatile globals optimized away, leading to random, mysterious intermittent bugs. Such bugs will in fact misbehave in exactly the same way as bugs caused by several threads/ISRs accessing the same resource, with the difference that the former will be much much harder to find.

Sign in to Reply



krwada

3/23/2010 5:51 PM EDT

Hoo boy!

This is one of the perils of embedded programming. There are so many ways to mess this up. And all of the ways use what appears to be reasonable code.

A few things that have worked for me in the past:
1. If you use an OS; use the pre-existing OS services to solve the problem
2. If you do not use an OS; use whatever resources the hardware vendor gives you to solve the problem.

So, in a nutshell:
- Read the user's manual, examples, appnotes, anything you can get your hands on
- See if the software user's manual has any instructions that will aid you in this. A good example is the swi instruction on the ARM7TDMI.
- Learn the assembly language and check to see what the compiler is generating.
- Make certain it works on paper before implementation begins
- If possible, use a debugger and step through the atomic section to see if it does what you expect.

Sign in to Reply



NevadaDave

3/24/2010 7:09 PM EDT

Hmmm... what I find interesting about this whole discussion is the light it sheds on the "how come we can't seem to write reliable software" question. It seems that no matter how hard you try, in the end, the specific language AND compiler implementation will find ways of messing you up. This forces me to a point I really don't like, and that is that software reliability and predictability will probably never happen until some effort is made to standardize architectures and software tool operation. Every time some bright young person adds a new "feature" or "enhancement" to (more-or-less) proven good hardware or software, you know as well as I that a multitude of opportunities for bugs develop.
The building industry has long had a variety of codes that specify materials and construction methods, and these have been proven over time to work out pretty well. However, at the same time, innovation in both areas has been very slow to become integrated, because of the standards set by the building code. I cannot imagine designers/developers agreeing to use a particular CPU & software package with the amount of chauvinism we all seem to have, so problems like unpredictable interrupt response & a whole host of things will never be resolved. We'll just have to do the best we can!

Sign in to Reply



knowlogic

3/25/2010 1:20 AM EDT

NevadaDave is right, "We'll just have to do our best", and that means knowing how to find bugs once they happen. That's not defeatist its reality, and these are hard bugs to find.

Sign in to Reply



Lundin

3/25/2010 9:28 AM EDT

The example with volatile globals is universal for all C compilers. The tools aren't the problem, they ARE standardized through ANSI/ISO C. Just stick to the standard. The main problem is lacking knowledge of how to write portable programs. If you don't write portable code, you are writing bad code.

For the poorly defined C language, this means that the programmer must have extensive knowledge of all details.
Engineering management must realize this. If you want to use the C language, you can't let some average Joe, or fresh out of school, or a hardware person develop the programs. You need at least one real C guru in the team, who knows exactly which pit falls that lead to latent bugs or non-portable code. The need for such knowledge will only increase when programs move over to multicore and need to be thread-safe and reentrant all over.

Sign in to Reply



NevadaDave

3/25/2010 4:27 PM EDT

Lundin,
"If you don't write portable code, you are writing bad code". I have to respectfully disagree. Most of my code is non-portable - it pretty much has to be, because the platform on which it runs has some hardware peculiarities that require platform-specific extensions/#pragmas/whatever you want to call them. I try not to use them if I can, but some things absolutely require them, because my company for which I work does not have the luxury of designing in and supporting a variety of processor types. If I ever had to port to a new device, much of the code would be portable - it's the I/O that would require the most changes. Every uC I know has a different way of designating I/O ports, A/D converters, timers, etc. and it would be pretty much impossible to write portable code that fit them all without having the additional overhead of some kind of RTOS or BIOS, which is just not feasible for an 8-bit micro with 2K of program memory and 128 bytes of RAM. OTOH, I have written a number of very successful C programs for this device, using the extensions the compiler maker provides.
As far a "real C guru in the team" - I AM the team! Kinda scary, huh?

Sign in to Reply



Peter House

3/25/2010 5:33 PM EDT

I've yet to see anyone mention the impact a compiler update might have on all of this.

Even though the vendor provides a list of fixes in a release - there is no good way to know what has really changed without a complete compile and test and compare the binary output before and after.

So goes/grows the test matrix . . .

Sign in to Reply



zanedp

3/27/2010 6:36 PM EDT

The compiler-agnostic suggestion made in the article assumes that the compiler does not do inter-module (rather than the "standard" intra-module) optimizations.

An optimizer that looks at the entire project--rather than a single module--when finding "inline" candidates will not be fooled. I have used a front-end to GCC that does preprocessing and concatenates all source files together into a single .c file before passing the source off to the compiler proper.

Sign in to Reply



Lundin

3/29/2010 9:02 AM EDT

> > "If you don't write portable code, you are writing bad code"
> I have to respectfully disagree. Most of my code is non-portable - it pretty much has to be, because the platform on which it runs has some hardware peculiarities that require platform-specific extensions/#pragmas/whatever you want to call them. I try not to use them if I can, but some things absolutely require them, because my company for which I work does not have the luxury of designing in and supporting a variety of processor types. If I ever had to port to a new device, much of the code would be portable - it's the I/O that would require the most changes. Every uC I know has a different way of designating I/O ports, A/D converters, timers, etc. and it would be pretty much impossible to write portable code that fit them all without having the additional overhead of some kind of RTOS or BIOS, which is just not feasible for an 8-bit micro with 2K of program memory and 128 bytes of RAM. OTOH, I have written a number of very successful C programs for this device, using the extensions the compiler maker provides.


Yeah of course you won't be able to port hardware I/O etc. What I meant with that statement is that you should be wary about for example integer sizes, integer signedness, data alignment, unspecified behavior and other such classic issues in the C language itself. Most of such issues will disappear if you follow MISRA-C.

Sign in to Reply



Lundin

3/29/2010 9:10 AM EDT

> The compiler-agnostic suggestion made in the article assumes that the compiler does not do inter-module (rather than the "standard" intra-module) optimizations.
> An optimizer that looks at the entire project--rather than a single module--when finding "inline" candidates will not be fooled. I have used a front-end to GCC that does preprocessing and concatenates all source files together into a single .c file before passing the source off to the compiler proper.

None of this compiler behavior is specified in ANSI/ISO C. Even if one persion is using a good compiler to get rid of such bugs, that doesn't mean that the next compiler used will have the same feature. That's why questions like this are better not directed to compiler vendors, they will no doubt tell you to use their super-smart compiler which solves the problem. Temporarily. Until the code is compiled on a crappier, but still ISO-compliant compiler.

Sign in to Reply



Please sign in to post comment

Navigate to related information

Datasheets.com Parts Search

185 million searchable parts
(please enter a part number or hit search to begin)
Jobs sponsored by

Feedback Form