17

Is there a good way of implementing communication between an ISR and the rest of the program for an embedded system which avoids global variables?

It seems that the general pattern is to have a global variable which is shared between the ISR and the rest of the program and used as a flag, but this use of global variables goes against the grain to me. I've included a simple example using avr-libc style ISRs:

volatile uint8_t flag;

int main() {
    ...

    if (flag == 1) {
        ...
    }
    ...
}

ISR(...) {
    ...
    flag = 1;
    ...
}

I can't see away around what is essentially a scoping issue; any variables accessible by both the ISR and the rest of the program must inherently be global, surely? Despite this, I've often seen people say things along the lines of "global variables are one way of implementing communication between ISRs and the rest of the program" (emphasis mine), which seems to imply that there are other methods; if there are other methods, what are they?

  • 1
    related: [Use of global variables in Embedded Systems](https://electronics.stackexchange.com/a/97242/7036) – Nick Alexeev Sep 14 '17 at 13:13
  • 1
    It's not necessarily true that ALL the rest of the program would have access; if you declared the variable as static, only the file in which the variable was declared would see it. It's not at all hard to have variables that are visible within the whole of one file, but not the rest of the program and that can help. – DiBosco Sep 14 '17 at 13:13
  • 1
    beside, flag must be declared volatile, because you're using/changing it outside the normal program flow. This forces the compiler not to optimize any read/write to flag, and perform the actual read/write operation. – next-hack Sep 14 '17 at 14:13
  • @next-hack Yes that's absolutely correct, sorry I was just trying to come up with an example quickly. –  Sep 14 '17 at 14:21

6 Answers6

21

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.
Lundin
  • 17,577
  • 1
  • 24
  • 67
  • Note: you may have to expose the ISR function prototype through a header, in order to place it in a vector table located in another file. But that's not an issue as long as you document that it is an interrupt and should not get called by the program. – Lundin Sep 18 '17 at 14:45
  • What would you say, if the counter-argument was the increased overhead (and extra code) of using the setter/getting functions? I've been going over this myself, thinking about code standards for our 8 bit embedded devices. – Leroy105 Jan 12 '18 at 19:36
  • 2
    @Leroy105 The C language has supported inline functions for an eternity by now. Although even the use of `inline` is becoming obsolete, since compilers get smarter and smarter at optimizing code. I'd say that worrying about the overhead is "pre-mature optimization" - in most cases the overhead does not matter, if it's at all even present in the machine code. – Lundin Jan 15 '18 at 07:45
  • 2
    That being said, in the case of writing ISR drivers, some 80-90% of all programmers (not exaggerating here) always get something in them wrong. The outcome is subtle bugs: incorrectly cleared flags, incorrect compiler optimization from missing volatile, race conditions, lousy real-time performance, stack overflows etc etc. In case the ISR is not properly encapsulated inside the driver, the chance of such subtle bugs is further increased. Focus on writing a bug free-driver before worrying about things of peripheral interest, such as if setter/getters introduce a tiny bit of overhead. – Lundin Jan 15 '18 at 07:50
10
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.

RMAAlmeida
  • 1,997
  • 15
  • 24
Olin Lathrop
  • 310,974
  • 36
  • 428
  • 915
  • I have been going to therapy, in a slow week, really trying to nitpick my code. I see Lundin's point on restricting variable access, but I look at my actual systems and think it is such a remote possibility ANY PERSON would actually jank a system critical global variable. The Getter/Setter functions end up costing you overhead versus just using a global and accepting these are pretty simple programs... – Leroy105 Jan 12 '18 at 19:33
  • 3
    @Leroy105 The problem isn't "terrorists" intentionally abusing the global variable. Namespace pollution could be a problem in larger projects, but that can be solved with good naming. No, the true problem is the programmer trying to use the global variable as intended, but failing to do so correctly. Either because they don't realize the race condition issue that exists with all ISRs, or because they mess up the implementation of the mandatory protection mechanism, or simply because they spew out use of the global variable all over the code, creating tight coupling and unreadable code. – Lundin Jan 15 '18 at 07:57
  • Your points are valid Olin, but even in these examples, replacing `extern int ticks10ms` with `inline int getTicks10ms()` will make absolutely no difference in the compiled assembly, while on the other hand it will make hard to accidentally change its value in other parts of the program, and also allow you to a way to "hook" to this call (e.g. to mock the time during unit testing, to log access to this variable, or whatever). Even if you argue that the chance of a san programmer changing this variable to zero, there is no cost of an inline getter. – vgru Mar 09 '18 at 20:13
  • @Groo: That is only true if you are using a language that supports inlining functions, and it means the definition of the getter function needs to be visible to all. Actually when using a high level language, I use getter functions more and global variables less. In assembly, it's just a lot easier to grab the value of a global variable than to bother with a getter function. – Olin Lathrop Mar 09 '18 at 21:32
  • Of course, if you cannot inline, then the choice is not so simple. I wanted to say that with inlined functions (and many pre C99 compilers already supported inlining extensions), performance cannot be an argument against getters. With a reasonable optimizing compiler, you should end up with the same produced assembly. – vgru Mar 10 '18 at 10:13
  • For simple programs, a global variable isn't going to cause the end of the world. But simple programs sometimes grow into larger ones. *Not* using global variables makes it easier to write correct unit tests. Although a single global variable is manageable, it's not scalable. And future developers may be more inclined to add a new global if they already see at least one. Try to write tests for, or debug code, with 50+ global variables - it's completely counterproductive. Therefore I argue that one should strive to avoid globals, even just one. It's worth thinking about. – davidA Nov 15 '19 at 00:15
3

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.

supercat
  • 45,939
  • 2
  • 84
  • 143
1

Here's three ideas:

Declare the flag variable as static to limit the scope to a single file.

Make the flag variable private and use getter and setter functions to access the flag value.

Use a signalling object such as a semaphore instead of a flag variable. The ISR would set/post the semaphore.

kkrambo
  • 2,338
  • 12
  • 18
1

I'm coding for Cortex M0/M4 at the moment and the approach we are using in C++ (there is no C++ tag, so this answer might be off-topic) is the following:

We use a class CInterruptVectorTable which contains all the interrupt service routines which are stored in the actual interrupt vector of the controller:

#pragma location = ".intvec"
extern "C" const intvec_elem __vector_table[] =
{
  { .__ptr = __sfe( "CSTACK" ) },           // 0x00
  __iar_program_start,                      // 0x04

  CInterruptVectorTable::IsrNMI,            // 0x08
  CInterruptVectorTable::IsrHardFault,      // 0x0C
  //[...]
}

The class CInterruptVectorTable implements an abstraction of the interrupt vectors, so you can bind different functions to the interrupt vectors during runtime.

The interface of that class looks like this:

class CInterruptVectorTable  {
public :
    typedef void (*IsrCallbackfunction_t)(void);                      

    enum InterruptId_t {
        INTERRUPT_ID_NMI,
        INTERRUPT_ID_HARDFAULT,
        //[...]
    };

    typedef struct InterruptVectorTable_t {
        IsrCallbackfunction_t IsrNMI;
        IsrCallbackfunction_t IsrHardFault;
        //[...]
    } InterruptVectorTable_t;

    typedef InterruptVectorTable_t* PinterruptVectorTable_t;


public :
    CInterruptVectorTable(void);
    void SetIsrCallbackfunction(const InterruptId_t& interruptID, const IsrCallbackfunction_t& isrCallbackFunction);

private :

    static void IsrStandard(void);

public :
    static void IsrNMI(void);
    static void IsrHardFault(void);
    //[...]

private :

    volatile InterruptVectorTable_t virtualVectorTable;
    static volatile CInterruptVectorTable* pThis;
};

You need to make the functions which are stored in the vector table static because the controller cannot provide a this-pointer as the vector table is not an object. So to get around that problem we have the static pThis-pointer inside the CInterruptVectorTable. Upon entering one of the static interrupt functions, it can access the pThis-pointer to gain access to members of the one object of CInterruptVectorTable.


Now in the program, you can use the SetIsrCallbackfunction to provide a function pointer to a static function which is to be called when an interrupt happens. The pointers are stored in the InterruptVectorTable_t virtualVectorTable.

And the implementation of an interrupt function looks like this:

void CInterruptVectorTable::IsrNMI(void) {
    pThis->virtualVectorTable.IsrNMI(); 
}

So that will call a static method of another class (which can be private), which then can contain another static this-pointer to gain access to member-variables of that object (only one).

I guess you could build and interface like IInterruptHandler and store pointers to objects, so you don't need the static this-pointer in all those classes. (maybe we try that in the next iteration of our architecture)

The other approach works fine for us, as the only objects allowed to implement an interrupt handler are those inside the hardware abstraction layer, and we usually only have one object for each hardware block, so it's fine working with static this-pointers. And the hardware abstraction layer provides yet another abstraction to interrupts, called ICallback which is then implemented in the device layer above the hardware.


Do you access global data? Sure you do, but you can make most of the needed global data private like the this-pointers and the interrupt functions.

It's not bulletproof, and it adds overhead. You'll struggle to implement an IO-Link stack using this approach. But if you're not extremely tight with timings, this works quite well to get a flexible abstraction of interrupts and communication in modules without using global variables which are accessible from everywhere.

Arsenal
  • 17,464
  • 1
  • 32
  • 59
  • 1
    "so you can bind different functions to the interrupt vectors during runtime" This sounds like a bad idea. The "cyclomatic complexity" of the program would just go through the roof. All use-case combinations would have to be tested so that there are neither timing nor stack use conflicts. Lots of head ache for a feature with very limited usefulness IMO. (Unless you have a bootloader case, that's another story) Overall this smells meta programming. – Lundin Sep 18 '17 at 14:48
  • @Lundin I don't really see your point. We use it to bind for example the DMA interrupt to the SPI interrupt handler if the DMA is in use for the SPI and to the UART interrupt handler if it is in use for the UART. Both handlers have to be tested, sure, but not a problem. And it surely has nothing to do with meta programming. – Arsenal Sep 18 '17 at 16:10
  • DMA is one thing, run-time assignment of interrupt vectors is something else entirely. It makes sense to let a DMA driver setup be variable, in run-time. A vector table, not so much. – Lundin Sep 19 '17 at 06:43
  • @Lundin I guess we have different views on that, we could start a chat about it, because I still don't see your problem with it - so it might be my answer is so badly written, that the whole concept is misunderstood. – Arsenal Sep 19 '17 at 07:02
0

An interrupt (i.e., the vector that points to your handler) is a global resource. So even if you use some variable on the stack or on the heap:

volatile bool *flag;  // must be initialized before the interrupt is enabled

ISR(...) {
    *flag = true;
}

or object-oriented code with a 'virtual' function:

HandlerObject *obj;

ISR(...) {
    obj->handler_function(obj);
}

… the first step must involve an actual global (or at least static) variable to reach that other data.

All these mechanisms add an indirection, so this is usually not done if you want to squeeze the last cycle out of the interrupt handler.

CL.
  • 18,161
  • 5
  • 40
  • 67