petemate, the approach or pattern or paradigm is called
event-driven programming.
If you've done ordinary Arduino sketches, that one is
imperative programming.
The difference is that in imperative programming, you the programmer are in complete control of what the microcontroller does at any point in time; in event-driven programming, you write code that
reacts to different events. With hardware interrupts (and e.g. POSIX signals in Linux/Mac/BSD OSes), those events are usually different functions, and while the event (function) is being executed, nothing else happens. The event can occur anywhere except in critical sections (where interrupts, or POSIX signals in Linux/Mac/BSD OSes, are blocked). Because of this, it is crucial that the event handler does not
sleep or dilly-dally.
Exactly how to plan or structure your code, depends on whether you have BOTH interrupt (event-based) actions AND continuously-running "work".
Let's say you have a simple sketch that generates a pulse stream, say for LEDs or some such, and some buttons that start, stop, and change the stream being output.
For the three buttons, you could install interrupt functions (with software debouncing), that simply change
static volatile bool or
static volatile int flags. (Remember, Arduino uses GNU C++.
static in the file scope means the variable or function it is only visible in this file scope; you can omit it if it weirds you out.)
The loop() function would generate the next pulse, except that first it would check the flags, and change its operating mode accordingly. (The loop() function would basically be the iterative part of a simple
finite state machine.)
Okay, but why would anyone use an interrupt for that, instead of just checking the button states at the beginning?Because with the interrupt approach, you can detect keypresses much shorter than the duration of a single iteration of the loop.
Essentially, this sketch would have a continuously running pulse generator, which regularly checks certain volatile flags that modify its operation. Those flags are updated by the interrupt functions.
There is one nasty detail: Interrupts can be interrupted by higher-priority ones, too – you can usually change the interrupt priorities, though – and "atomicity" is an issue. What "atomicity" means, is that when changing some variable, it may actually require more than one underlying machine instruction, and if interrupted in mid-modification, with the interrupt handling function seeing a partially modified value. If
that also modifies the value, and execution returns to the earlier interrupt which only modifies the rest of the variable value, the value becomes corrupted.
If the interrupt only examines the flags, and does not change their values, there can be no problem. In the case where interrupt functions only set a flag, and normal (loop()) code only reads the flags, you do not need to worry about atomicity. The atomicity issues only occur when both normal code and interrupt code can modify the same variable.
Different environments provide different tools to work with this. The Arduino environment provides
ATOMIC_BLOCK(ATOMIC_RESTORESTATE) { critical operations } macro when
<util/atomic.h> is included. The GCC compiler provides
__atomic_ built-in functions (functions provided by the GCC compiler, not by any library or environment, for a given hardware architecture). In pseudocode, the typical sequence is
Let intsEnabled = (state of interrupts enabled status flag)
If intsEnabled:
Disable_Interrupts
End If
Critical operations
If intsEnabled:
Enable_Interrupts
End If
(This is what the Arduino macro on AVRs does, expanding into a single-iteration
for loop.)
The idea is that interrupts are disabled for the duration of the atomic or critical operations, and if enabled before, re-enabled afterwards. (If these critical operations happen to be part of a larger set of critical operations, interrupts could be already disabled – even if we are in an interrupt handler. That's the reason why simply disabling and re-enabling interrupts may not always work the way intended!)
The GCC atomic operations use simple hardware instructions for variable sizes that are known to be atomic on that hardware, and some other code for larger sizes.
Regardless of which environment – or even bare-metal – you use, the above concepts do apply. Only the details vary between environments (hardware and libraries).