So far, our primary method of interacting with outside world is ability to read logical level of some signal pin of the MCU.
That is usually implemented with simple API
that just tells us whenever value was pressed:
while ( true ) {
if ( GetPin(PORT_A, PIN_42) == 1 ) {
// yay, button was pressed!
}
}
This should work, but what if we are solving some NP problem in the mean time?
while ( true ) {
SolveNPProblem();
if ( GetPin(PORT_A, PIN_42) == 1 ) {
// yay, button was pressed!
}
}
Let’s assume that it takes us up to 1s
to solve the NP problem, that means that we check whenever button was pressed only once per 1s
- it is almost guaranteed that we will miss some button presses done by the user.
Alternative to this is to block the device until we detect a button press and solve the NP problem after that:
while ( true ) {
while ( GetPin(PORT_A, PIN_42) != 1 ) {
asm ("nop");
}
// yay, button was pressed!
SolveNPProblem();
}
This way, we block until user presses the button and solve the NP problem only after that. This works slightly better, we can miss button press only if it happens too early after previous press. But we can assume that user would not press the button more than once per second.
This however is at great cost, as we can spend a huge amount of time waiting for the button press and can’t do anything else in the meantime. We could have solved so many NP problems…
Both approaches have polling
nature, we are repeatedly asking the pin it’s state, both approaches have clear disadvantages.
Alternative for this are interrupt
s - routines that are executed by the MCU
on events
at any point in time. One of the possible events is change of logical value on the pin of MCU
. We can for example configure interrupt to be called on event: logical level of pin 42 change from LOW to HIGH
.
Once that event happens, MCU
will finish processing actual instruction
and instantly switches to executing an interrupt handler
:
void ButtonHandler() {
// yay, button was pressed!
}
while ( true ) {
SolveNPProblem();
}
If all is configured properly, what happens is that once user presses the button, the MCU will cause execution of the ButtonHandler()
routine almost instantly. Once it finishes, the MCU will return to execution of code that was running previously.
This has obvious advantages, we can just keep solving NP problems forever and yet we will catch all presses of buttons done by the user. The cost for that is that the SolveNPProblem()
routine can be interrupted by the handler for a while, which we assume should not have undesired consequences here.
It may seem like interrupts are a silver bullet for us, but that is not true, there are consequences to using interrupts, mostly in the form of increased complexity and potential for nasty problems that are hard to debug, compared to plain simple polling.
We can have interrupt for various events and we will talk about them a lot in the course. However, think about interrupts as an approach to solve problems, always think properly whenever it is the right approach.
As a students of computer science, you should be aware of problems and intricacies with parallel computing, traditional in form of threads or processes. Interrupt
s are quite similar and share a lot of the problems.
You can model situation with main code
and interrupts
as two separated threads.
At most of time, T1
(thread with main code
) is running and at random occurences, the MCU switches to thread T2
(thread for specific interrupt
).
One of the common example of problems is concurrent access to data. Let’s assume we have following code pushing data to stack and popping data:
void stack_push( foo* stack, size_t* stack_size, foo item ) {
stack[*stack_size] = item;
*stack_size += 1;
}
foo stack_pop( foo* stack, size_t* stack_size ) {
*stack_size -= 1;
return stack[*stack_size];
}
What happens if we pop in the main code (T1
) and push in the interrupt handler
(T2
)?
In most situation the stack will work correctly, but what if the interrupt is executed at really wrong time? such as:
foo stack_pop( foo* stack, size_t* stack_size ) {
*stack_size -= 1;
// ------> interrupt is executed HERE <-------
return stack[*stack_size];
}
What happens is stack is lowered, after that new data are inserted into the stack in interrupt, and than last item in the stack array is returned, but is that the wanted value? (hint: no)
There are multiple problems caused by concurrent access or other phenomenons related to parallelism. Best way to avoid them is to have as simple interaction between interrupts/main code as possible.
volatile
Out of the parallelism context, we want to point out a frequently used tool: volatile
.
volatile
is a type qualifier in C-language, it affects the type of a variable and forces compiler to handle the variable differently. volatile
tells compiler that the variable should not be optimized away and that all writes and reads should be done properly.
Normally, compiler can for the sake of optimization reorder read/writes to a variable, skip the variable entirely (by simplifying code so it is not needed), or do other magical tricks with it.
This is generally desired effect, but it is really undesired for us in case we are using that variable for communication between interrupts and main code.
Let’s assume that we are using interrupt to detect whenever button was pressed:
bool button_was_pressed = false;
void ButtonHandler() {
button_was_pressed = true;
}
void Foo() {
if(button_was_pressed) { // A)
// ...
return;
}
ComputeNPProblem1();
if(button_was_pressed) { // B)
// ...
return;
}
ComputeNPProblem2();
if(button_was_pressed) { // C)
// ...
return;
}
ComputeNPProblem3();
}
In this case, if the compiler detects that ComputeNPProblem<X>
functions can’t affect button_was_pressed
, it is allowed to completely remove conditions B and C, because if variable button_was_pressed
would be true in this context, the function would already end in condition A).
By making the variable volatile
we prevent this optimization:
volatile bool button_was_pressed = false;
Almost all MCUs have interrupt
s with same behavior: their signature is void()
- they are without arguments.
Given that, we have to use global variables to implement any mechanics, common pattern is:
interrupt
has specific global variables used by it to transfer data into main code
main code
The simplest pattern is in a form of bool variable used previously:
volatile bool button_was_pressed = false;
void ButtonHandler() {
button_was_pressed = true;
}
void Foo() {
if(button_was_pressed) {
button_was_pressed = false;
// process button press
}
}
Note that this time we did it a bit differently, the variable was reset immediately, that is to make sure that we would not miss a button press during the phase in which we are processing the button.
To accomodate multiple different events that cause interrupt
, manufacturers also use the concept of flags
. For example: We can have one interrupt handler
for the peripheral I2C
that is called for multiple different events: error, data transmission finished, reading data finished, etc…
The peripheral contains registers with flags
that specifies the source - event that caused the interrupt.
What happens is that: once event appears, the peripheral sets the appropriate flag to 1
and notifies MCU to execute the adequate interrupt handler
.
It is up to the handler
to reset that flag back to 0
. That is, you have to manually write a code that reads all of the flags, decides which one is 1
and sets it back to 0
.
(Motivation for having this as a manual process is outside of the scope of these materials, but you can convince us to explain it)
What happens if multiple interrupts occur at once?
That depends on the MCU
, common pattern is that MCU
s have a notion of priority for interrupts:
A
is being processed and interrupt B
of higher priority appears, the MCU
switches to handling interrupt B
immediately and than goes back to interrupt A
A
is being processed and interrupt C
of lower priority appears, it is processed after A
Note that the side effect of situation B) is that lower priority interrupts have to be reentrable - the interrupt has to be implemented in a way that it won’t break anything if it is interrupted by higher level interrupt (including concurrency issues etc…)
Traditionally it is required for interrupts to be as fast as possible, there are varying motivations for that:
ethernet
peripheral would fire an interrupt for each 32 bytes of data from network, at 1 Mbit/s
. That is around 4000
interrupts per second. You really want for that interrupt to be as fas as possible. (And note that 1 Mbit/s
is slow in modern terms)Note that in case of 2) it’s not only about the performance of the specific interrupt itself, but about the sum performance of all of them. The maximum time between raising an event and interrupt ending, is the sum of maximum execution time of all interrupt handlers with higher or same priority. (Worst case scenario is that all others interrupts of the system fired at once and the one that you are interested is fired after them)
And practical point in life of developer: Out of all the possible problems, issues caused by interrupts being too slow are one of the hardest to even detect. In case of systems with multiple interrupts, the complexity for analysis grow worse than exponentially.
Given all of that it is best to develop a practice to write all interrupts as fast as possible.