Avoiding global variables when using interrupts in embedded systems

There is a de facto standard way to do this (assuming C programming):

  • Interrupts/ISRs are low-level and should therefore only be implemented inside the driver related to the hardware that generates the interrupt. They should not be located anywhere else but inside that driver.
  • All communication with the ISR is done by the driver and the driver only. If other parts of the program needs access to that information, it has to request it from the driver through setter/getter functions or similar.
  • You should not declare "global" variables. Global meaning file scope variables with external linkage. That is: variables that could be called upon with extern keyword or simply by mistake.
  • Instead, to force private encapsulation inside the driver, all such variables shared between the driver and the ISR shall be declared static. Such a variable is not global but restricted to the file where it is declared.
  • To prevent compiler optimization issues, such variabels should also be declared as volatile. Note: this does not give atomic access or solve re-entrancy!
  • Some manner of re-entrancy mechanism is often needed in the driver, in case the ISR writes to the variable. Examples: interrupt disable, global interrupt mask, semaphore/mutex or guaranteed atomic reads.

this use of global variables goes against the grain to me

This is the real problem. Get over it.

Now before the knee-jerkers immediately rant about how this is unclean, let me qualify that a bit. There is certainly danger in using global variables to excess. But, they can also increase efficiency, which sometimes matters in small resource-limited systems.

The key is to think about when you can reasonably use them and are unlikely to get yourself into trouble, versus a bug just waiting to happen. There are always tradeoffs. While generally avoiding global variables for communicating between interrupt and foreground code is a understandable guideline, taking it, like most other guidelines, to a religions extreme is counter-productive.

Some examples where I sometimes use global variables to pass information between interrupt and foreground code are:

  1. Clock tick counters managed by the system clock interrupt. I usually have a periodic clock interrupt that runs every 1 ms. That is often useful for various timing in the system. One way to get this information out of the interrupt routine to where the rest of the system can use it is to keep a global clock tick counter. The interrupt routine increments the counter every clock tick. Foreground code can read the counter at any time. Often I do this for 10 ms, 100 ms, and even 1 second ticks.

    I make sure the 1 ms, 10 ms, and 100 ms ticks are of a word size that can be read in a single atomic operation. If using a high level language, make sure to tell the compiler that these variables can change asynchronously. In C, you declare them extern volatile, for example. Of course this is something that goes into a canned include file, so you don't need to remember that for every project.

    I sometimes make the 1 s tick counter the total elapsed up time counter, so make that 32 bits wide. That can't be read in a single atomic operation on many of the small micro I use, so that isn't made global. Instead, a routine is provided that reads the multi-word value, deals with possible updates between reads, and returns the result.

    Of course there could have been routines to get the smaller 1 ms, 10 ms, etc, tick counters too. However, that really does very little for you, adds a lot of instructions in place of reading a single word, and uses up another call stack location.

    What's the downside? I suppose someone could make a typo that accidentally writes to one of the counters, which then could mess up other timing in the system. Writing to a counter deliberately would make no sense, so this kind of bug would need to be something unintentional like a typo. Seems very unlikely. I don't recall that ever happening in well over 100 small microcontroller projects.

  2. Final filtered and adjusted A/D values. A common thing to do is to have a interrupt routine handling readings from a A/D. I usually read analog values faster than necessary, then apply a little low-pass filtering. There is often also scaling and offseting that get applied.

    For example, the A/D may be reading the 0 to 3 V output of a voltage divider to measure the 24 V supply. The many readings are run through some filtering, then scaled so that the final value is in millivolts. If the supply is at 24.015 V, then the final value is 24015.

    The rest of the system just sees a live updated value indicating the supply voltage. It doesn't know nor need to care when exactly that is updated, especially since it is updated much more often than the low pass filter settling time.

    Again, a interface routine could be used, but you get very little benefit from that. Just using the global variable whenever you need the power supply voltage is much simpler. Remember that simplicity isn't just for the machine, but that simpler also means less chance of human error.


Any particular interrupt will be a global resource. Sometimes, however, it may be useful to have several interrupts share the same code. For example, a system might have several UARTs, all of which should use similar send/receive logic.

A nice approach to handle that is to place the things used by the interrupt handler, or pointers to them, in a structure object, and then have the actual hardware interrupt handlers be something like:

void UART1_handler(void) { uart_handler(&uart1_info); }
void UART2_handler(void) { uart_handler(&uart2_info); }
void UART3_handler(void) { uart_handler(&uart3_info); }

The objects uart1_info, uart2_info, etc. would be global variables, but they would be the only global variables used by the interrupt handlers. Everything else that the handlers are going to touch would be handled within those.

Note that anything which is accessed both by the interrupt handler and by the main-line code must be qualified volatile. It may be simplest to just declare as volatile everything that will be used at all by the interrupt handler, but if performance is important one may want to write code that copies information to temporary values, operates upon them, and then writes them back. For example, instead of writing:

if (foo->timer)
  foo->timer--;

write:

uint32_t was_timer;
was_timer = foo->timer;
if (was_timer)
{
  was_timer--;
  foo->timer = was_timer;
}

The former approach may be easier to read and understand, but will be less efficient than the latter. Whether that is a concern would depend upon the application.