PV198 Study Materials

Preliminaries

Theory

- Introduction

- GPIO

- Interrupts

- Timer

- PWM

- ADC

- Communication buses

- SPI

- I2C

- UART

Practical

Interrupts

Polling vs Interrupt

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 interrupts - 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.

Interrupts are a form of a parallelism

As a students of computer science, you should be aware of problems and intricacies with parallel computing, traditional in form of threads or processes. Interrupts 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;

Globals

Almost all MCUs have interrupts 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:

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.

Flags

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)

Priority

What happens if multiple interrupts occur at once?

That depends on the MCU, common pattern is that MCUs have a notion of priority for interrupts:

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…)

Performance

Traditionally it is required for interrupts to be as fast as possible, there are varying motivations for that:

  1. If the event for the interrupt happens frequently - let’s assume 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)
  2. In a lot of system, you want to react as fast as possible on the event: per-byte processing of communication line can imply that you have to process 1 byte before next one arrives; if safety button is pressed you want to disable some component as fast as possible (for example: safety button on an autonomous truck that disables the motor);

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.