For low power applications it is essential to use the sleep modes of the processor to achieve the lowest power consumption. Most code will be triggered by interrupts that wake up the processor. It is therefore critical that the processor can enter the sleep mode in a safe way that ensures no interrupt effects are lost. An example will serve to clarify.
Imagine that we need to process some data every time an interrupt arrives. We can code the following:
brush: c
volatile int process_data = 0;
void interrupt() {
// Clear interrupt
...
// Signal main loop
process_data = 1;
}
int main() {
while (1) {
if (process_data) {
process_data = 0;
// Do something with the data
...
}
sleep(); // Translated to a single instruction
}
}
The problem with the code above is that in some cases the signal from the interrupt service routine (isr) can be lost by the main loop. Imagine that a new interrupt is received while the data is being processed. The code will enter sleep and will only come from sleep on the next interrupt. It might then be too late to save a life.
We could think that the above can be solved by checking the variable process_data before entering sleep mode.
brush: c
int main() {
while (1) {
if (process_data) {
process_data = 0;
// Do something with the data
...
}
else {
// Now we only sleep immediately after checking process_data is not set
sleep();
}
}
}
The problem is still there but minimized. Now it will be a much harder bug to find. The window where the interrupt is received and the processor goes to sleep is a handful of instructions. I'm sure this would not pass a safety review.
Another problem in all the above implementations is the the shared globsal variable is manipulated in two contexts that can be threaded. Hence atomocity is not guaranteed. We can improve by disabling interrupts:
brush: c
int main() {
while (1) {
disableInterrupts()
if (process_data) {
process_data = 0;
enableInterrupts();
// Do something with the data
...
}
else {
// process_data is checked/modified with interrupts disabled
enableInterrupts();
sleep();
}
}
}
This code still sufers from the same condition but now is just limited to consecutive instructions: the instruction to enable interrupts and the instruction to enter the sleep mode. Unfortunately, this is still not 100% safe.
The correct way to solve this is to add a mechanism in the processor to execute this two in an atomic way. This can be either an instruction that does both, or a mechanism to ensure two instructions are executed atomically (as in the Atmel AVR microcontroller family link).
Once this is in place, we can finally get safe code:
brush: c
int main() {
while (1) {
disableInterrupts()
if (process_data) {
process_data = 0;
enableInterrupts();
// Do something with the data
...
}
else {
// Now we only sleep immediately after checking process_data
enableInterruptsAndSleep();
}
}
}
Some reader might have frowned at the use of the process_data and might prefer a version where no variable is written from more than one thread. This can be achieved as follows:
brush: c
volatile int interrupts_received = 0;
volatile int interrupts_processed = 0;
void interrupt() {
// Clear interrupt
...
// Signal main loop
interrupts_received++;
}
int main() {
while (1) {
disableInterrupts()
if (interrupts_received != interrupts_processed) {
interrupts_processed++;
enableInterrupts();
// Do something with the data
...
}
else {
// Now we only sleep immediately after the check
enableInterruptsAndSleep();
}
}
}
The code above still requires the atomic sleep and interrupt enable. Otherwise it is possible that the interrupts_received variable is updated betwen the two.
Hence, all lower processors will need a mechanism to sleep and enable interrupts in an atomic way.
Side note: the discussion above is only valid for single processor systems. In a multiprocessor system disabling interrupts will not guarantee that the access to the control variables is atomic. This will be the topic for a post some other day.