Tutorials: Microcontroller systems (MICSY)


last updated: 2022-06-02

Quick links to the subchapters

Song of this chapter: The Hollies > Hollies Sing Dylan > I want you


Often we use microcontroller boards, not only because they are cheaper than single-board computer like the Raspberry Pi, but because they have no operating system and so can react in real-time. This is often needed in an industrial environment.

The mechanism built in microcontroller to handle real-time events is called an interrupt. It's job is to make sure that the processor responds quickly to important events, by interrupting whatever the controller or processor is doing, and execute some code to handle the important event. After this (normally very short) interrupt, the controller goes back to whatever it was originally doing (as if nothing happened :)).

You can compare interrupts with an important phone call, that interrupts your doing. After reacting to the call (e.g. a short glimpse to get some information) you resume to your preoccupation.

Interrupts structure the system to react quickly and efficiently to important events. Additionally it frees up your controller for doing other things while waiting on an event.

Interrupts (wiki)

Hardware and software interrupts

So an interrupt is a signal sent to the processor or controller to interrupt the current process. The signal may be generated by a hardware device or a software program.

Hardware interrupts are used by internal or external devices (e.g. timer, mouse), to communicate, that they require attention from the controller (or operating system). Hardware interrupts are asynchronous! They can occur in the middle of instruction execution. The controller needs to react promptly and so additional care in programming is required.

The service initiating a hardware interrupt sends an Interrupt ReQuest (IRQ) to the controller. This prevents conflicts and ensures that interrupts are prioritized (see interrupt vector table).

Hardware interrupts are often maskable, meaning they can be allowed or forbidden (switched on and off) individually by setting bits in the special register (SPR) (often using a mask). Non-Maskable Interrupt (NMI) can not be switched off. They must be handled.

The AVR controller have only one non-maskable interrupt. This is the RESET interrupt on address 0x0000. It has the highest priority. An IRQ is generated at power up or by using the RESET pin. There is no normal service routine, but the main program is executed.

Software interrupts are used to handle errors and exceptions that may occur during runtime of a program. These interrupts may, per example, prevent the program from crashing by allowing the program to handle the error before continuing. Microcontroller without operating system seldom use software interrupts.

Hardware and software interrupts have interrupt handlers, called Interrupt Service Routines (ISR). Each interrupt has its own interrupt handler. An interrupt handler is called after an interrupt request. The ISR handles the event and after this the program resumes. Interrupts should be as brief as possible and are often processed in less than a millisecond.

Polling versus Interrupts

Waiting in a loop on an event by simply checking for a condition periodically is called polling. It's not very elegant, but in programs with few tasks this method is appropriate, because the programming stays simple and clearly pre-visible.

Interrupts are more complex to use and prone to errors. But they are the first choice if events occur that request immediate attention.

The microcontroller has many integrated hardware modules (timers, serial interfaces, analog to digital converter ...), that can work parallel to the controller. They can interact with the running program more effectively using interrupts. As an example, the AVR Analog to Digital Converter (ADC) needs 13 µs to convert an analog voltage to a 10 bit digital value. Polling would block the controller during this time. An interrupt that passes the value will occur automatically generated by the hardware and needs only about 1 µs to pass the converted value to the controller.

"Just do it" Interrupts 1:

traffic lights

Interrupt vector table (wiki)

To understand the underlying mechanisms we will again use the ATmega328 microcontroller of the Arduino Uno. Let's begin with the interrupt vector table. As seen it resides at the beginning of the Flash.

memory atmega328 Flash

The number of hardware interrupts is limited by the number of interrupt request (IRQ) lines to the controller. For the ATmega328 we get 26 hardware interrupts 25 from them maskable. Each interrupt vector occupies two instruction words in the table, because a jump (not relative) needs two instructions. The table determines the priority levels of the different interrupts. The lower the address the higher is the priority level. RESET has the highest priority. Next is the external interrupt INT0 (Arduino Uno Pin 2).

Vector # Flash address Definition Vector name
26 0x0032 Store Program Memory Read SPM_READY_vect
25 0x0030 Two-wire Serial Interface TWI_vect
24 0x002E Analog Comparator ANALOG_COMP_vect
23 0x002C EEPROM Ready EE_READY_vect
22 0x002A ADC Conversion Complete ADC_vect
21 0x0028 USART Tx Complete USART_TX_vect
20 0x0026 USART Data Register Empty USART_UDRE_vect
19 0x0024 USART Rx Complete USART_RX_vect
18 0x0022 SPI Serial Transfer Complete SPI_STC_vect
17 0x0020 Timer/Counter0 Overflow TIMER0_OVF_vect
16 0x001E Timer/Counter0 Compare Match B TIMER0_COMPB_vect
15 0x001C Timer/Counter0 Compare Match A TIMER0_COMPA_vect
14 0x001A Timer/Counter1 Overflow TIMER1_OVF_vect
13 0x0018 Timer/Counter1 Compare Match B TIMER1_COMPB_vect
12 0x0016 Timer/Counter1 Compare Match A TIMER1_COMPA_vect
11 0x0014 Timer/Counter1 Capture Event TIMER1_CAPT_vect
10 0x0012 Timer/Counter2 Overflow TIMER2_OVF_vect
9 0x0010 Timer/Counter2 Compare Match B TIMER2_COMPB_vect
8 0x000E Timer/Counter2 Compare Match A TIMER2_COMPA_vect
7 0x000C Watchdog Time-out Interrupt WDT_vect
6 0x000A Pin Change Interrupt Request 2 PCINT2_vect
5 0x0008 Pin Change Interrupt Request 1 PCINT1_vect
4 0x0006 Pin Change Interrupt Request 0 PCINT0_vect
3 0x0004 External Interrupt Request 1 INT1_vect
2 0x0002 External Interrupt Request 0 INT0_vect
1 0x0000 Reset

Typical and general program setup in assembler for ATmega328P (including the address; taken from the data sheet):

   0x0000         jmp    RESET            ; Reset Handler
   0x0002         jmp    EXT_INT0         ; IRQ0 Handler
   0x0004         jmp    EXT_INT1         ; IRQ1 Handler
   0x0006         jmp    PCINT0           ; PCINT0 Handler
   0x0008         jmp    PCINT1           ; PCINT1 Handler
   0x000A         jmp    PCINT2           ; PCINT2 Handler
   0x000C         jmp    WDT              ; Watchdog Timer Handler
   0x000E         jmp    TIM2_COMPA       ; Timer2 Compare A Handler
   0x0010         jmp    TIM2_COMPB       ; Timer2 Compare B Handler
   0x0012         jmp    TIM2_OVF         ; Timer2 Overflow Handler
   0x0014         jmp    TIM1_CAPT        ; Timer1 Capture Handler
   0x0016         jmp    TIM1_COMPA       ; Timer1 Compare A Handler
   0x0018         jmp    TIM1_COMPB       ; Timer1 Compare B Handler
   0x001A         jmp    TIM1_OVF         ; Timer1 Overflow Handler
   0x001C         jmp    TIM0_COMPA       ; Timer0 Compare A Handler
   0x001E         jmp    TIM0_COMPB       ; Timer0 Compare B Handler
   0x0020         jmp    TIM0_OVF         ; Timer0 Overflow Handler
   0x0022         jmp    SPI_STC          ; SPI Transfer Complete Handler
   0x0024         jmp    USART_RXC        ; USART, RX Complete Handler
   0x0026         jmp    USART_UDRE       ; USART, UDR Empty Handler
   0x0028         jmp    USART_TXC        ; USART, TX Complete Handler
   0x002A         jmp    ADC              ; ADC Conversion Complete Handler
   0x002C         jmp    EE_RDY           ; EEPROM Ready Handler
   0x002E         jmp    ANA_COMP         ; Analog Comparator Handler
   0x0030         jmp    TWI              ; 2-wire Serial Interface Handler
   0x0032         jmp    SPM_RDY          ; Store Program Memory Ready Handler
   0x0033  RESET: ldi    r16,high(RAMEND) ; Main program start
   0x0034         out    SPH,r16          ; Set Stack Pointer to top of RAM
   0x0035         ldi    r16,low(RAMEND)
   0x0036         out    SPL,r16
   0x0037         sei                     ; Enable interrupts
   0x0038         ...                     ; next instruction of Main
   ...            ...    ...          ...

The labels (names) in the jmp instructions used for the ISRs (e.g. EXT_INT0) are of course arbitrary.

Interrupts must be activated globally (Interrupt flag in SREG) and specifically by setting a bit in a special function register for this interrupt (like the main fuse and the individual fuse of a socket in a house fuse box).

How does interrupts work?

Interrupt service routine (ISR)

The interrupt handler is an external function called an Interrupts Service Routine (ISR). Interrupts service routines do have specific constrains and do not behave exactly like some other functions.

All register that will be used locally in the ISR must be saved to the stack! This includes the SREG special function register! Look at the following instructions in an assembler program:

    dec     r16
    brne    LOOP1

If an interrupt will occur during the decrement instruction, this instruction will be executed. Th following instruction (branch if not equal) relies on the Zero flag from the SREG register. If the flags are changed by operations inside the ISR the program will not work properly.

;       ISR with two internal variables
ISR_I1: push    r16             ;save r16 to stack (needed as temporary reg.)
        in      r16,SREG        ;read status register
        push    r16             ;save SREG to stack
        push    r17             ;save r17 to stack (needed in ISR)

        ;ISR code

        pop     r17             ;recover r17
        pop     r16             ;recover SREG
        out     SREG,r16        ;
        pop     r16             ;recover r16
        reti                    ;return from interrupt (takes return address
                                ;from stack and saves it to the program conter)

ISRs don't get parameters, and they don't return anything. Generally the ISR will use volatile global variables to communicate with the main program. An ISR should be very short (best less than a millisecond), because it blocks the main program and also other ISRs.

Using external interrupts in Arduino

External interrupts are interrupts generated by signals not coming from the internal microcontroller hardware, but from external devices through input pins.

Depending on the board, we have only few pins that can be used unrestricted for external interrupts. Arduino Uno has only two external interrupts INT0 (pin 2) and INT1 (pin 3). They can be triggered by a falling or rising edge or a low level. Arduino Leonardo has 5 pins (pins 0-3 and pin 7) same as the Teensy 2.0 (pins 5-8 and INT6 (not connected to the header)).

Fore other boards look here.

Most newer controller have supplementary pin change interrupts that react on the positive and the negative edge, but can be used on all pins. To use these interrupts we need a special library in Arduino or we must set the different SPRs ourself.

The pins can trigger an interrupt on different states, like: the pin is low (mode LOW), the pin is high (mode HIGH e.g. Arduino DUE), the pin changes value (pos. or neg. edge: mode CHANGE), the pin goes from low to high (mode RISING positive edge) or from high to low (mode FALLING negative edge).

The mode used has to be passed as argument when attaching the Interrupt Service Routine (ISR) to the interrupt.

The syntax for the command is:

    attachInterrupt(digitalPinToInterrupt(pin), ISR_name, mode);

The first parameter to attachInterrupt() is an interrupt number. For Arduino Uno we have Int0 (number 0) on pin 2 and Int1 (number 1) on pin 3. To simplify this we use the digitalPinToInterrupt(pin)-function that gets the interrupt number for the used board from the pin used.

The second parameter is the name of the ISR (without parentheses!).

The third parameter is the mode (LOW, CHANGE, RISING or FALLING).

To pass parameter (data) to and from an ISR global variables are used. To prevent the compiler to eliminate the variables and to make sure the variables are updated correctly, we have to declare them as volatile.

When an ISR is running it blocks not only the main program but also prevents other interrupts from occurring. Only one interrupt can run at a time. Other interrupts occurring during a running ISR will be executed after the current one finishes in an order that depends on their priority (see vector table).

millis() and delay() rely on interrupts so they can't be used inside an ISR. Only micros() works for very short times (less than 1  ms) and delayMicroseconds() will work as normal.

Here a little program, using a global variable named flag to pass information to the main loop. Connect a push-button to pin 2 (Arduino Uno) and two LEDs (with series resistor) to pin 0 and 1.

    // show changes on interrupt pin with two LEDs (Arduino Uno!)

    const byte PIN_LED1 = 0;
    const byte PIN_LED2 = 1;
    const byte PIN_INTERRUPT = 2; // Interrupt Numer 0 INT0 
    volatile byte flag = 0;       // 0 = no interrupt, 1 = rising, 2 = falling 

    void setup() {
    pinMode(PIN_LED1, OUTPUT);
    pinMode(PIN_LED2, OUTPUT);
    attachInterrupt(digitalPinToInterrupt(PIN_INTERRUPT), get_edge, CHANGE);

    void loop() {
    switch (flag) {
        case 0:
        digitalWrite(PIN_LED1, LOW);
        digitalWrite(PIN_LED2, LOW);
        case 1:  
        digitalWrite(PIN_LED1, HIGH);
        flag = 0;
    case 2:  
        digitalWrite(PIN_LED2, HIGH);
        flag = 0;

    // ISR
    void get_edge() {
    flag = digitalRead(PIN_INTERRUPT);
    if (flag == HIGH) {
        flag = 1;
    else {
        flag = 2;  
"Just do it" Interrupts 2:
"Just do it" Interrupts 3:
"Just do it" Interrupts 4:
"Just do it" Interrupts 5:

Using timer interrupts in Arduino

The millis() function is a big help when programming time critical sketches. It uses the overflow interrupt of Timer0 on Arduino Uno (Arduino Uno has 3 timers: Timer0, Timer1 and Timer2). Here a little sketch how to use millis() to get a 100 Hz square wave and simultaneously let blink an LED with a slow frequency.

    // square wave generator 100 Hz (LED blinks with 2 Hz)

    const byte PIN_OUT_FREQ = 2;      // frequeny output
    unsigned long prev_millis_1,prev_millis_2;

    void setup() {  
    prev_millis_1 = millis();
    prev_millis_2 = millis();

    void loop() {
    if ((millis()-prev_millis_1) >= 5){
    digitalWrite(PIN_OUT_FREQ, !digitalRead(PIN_OUT_FREQ)); // toggle freq. pin  
    prev_millis_1 = millis();    
    if ((millis()-prev_millis_2) >= 250){
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));   // toggle LED pin  
    prev_millis_2 = millis();    

Using millis() requests that we call millis() every time through the loop to see if it was time to do something. Calling millis() some hundred times a millisecond, only to find out that the time hasn't changed is a kind of waste. If we look at our 100 Hz signal on an oscilloscope, we see that it is not very stable.

This can be ameliorated with a Timer interrupt. So let's grab the data sheet of the ATmega328p, read the section about Timer, and do it the hard way. This will not work for other controller!, so the better way is to use a timer library for the corresponding board if available.

As Timer0 is already set up to generate a millisecond interrupt to update the millisecond counter from millis(), we will use the 8 bit Timer2 to generate the 100 Hz signal.

Timers are simple counters that count at some frequency derived from the 16 MHz system clock. A prescaler allows us to divide the clock frequency by 1, 8, 32, 64, 128, 256 or 1024. The overflow interrupt creates an interrupt when the timer register TCNT2 reaches it's maximum (255 for Timer2). The formula to calculate the frequency for Timer2 would be:

fI = 16 MHz/(prescaler·256)

256 because the timer counts from 0 to 255 = 256 steps.

This gives us the following possible frequencies:

prescaler frequency
1 62500
8 7812.5
32 1953.13
64 976.5625
128 488.284
256 244.14
1024 61.04

The prescaler 64 is used for the Timer0 overflow interrupt of millis(). This explains in parts the fluctuating signal.

We need an interrupt frequency of 200 Hz, the double of the output frequency, because of the toggeling.

To get the desired frequency we have two possibilities: In the Overflow ISR we preload the timer count register TCNT2 with a number to reduce the counting steps. The second possibility is cleaner. We use the CTC mode (clear timer on compare match) interrupt. Here a second register, the compare register OCR2A is loaded with a value, and if both register TCNT2 and OCR2A are equal an interrupt is generated.

fI = 16 MHz/(prescaler·(OCR2A+1))
OCR2A = 16 MHz/(prescaler·fI)-1

Because the count and compare register are only 8 bit register, the prescaler has to be 1024. So we get:

OCR2A = (16 MHz/1024·200 Hz)-1 = 77

Because we have to round, the real frequency will be:

f = fI/2 = 16 MHz/1024·(77+1))/2 = 100.16 Hz

Here the corresponding sketch:

    // square wave generator 100 Hz (LED blinks with 2 Hz)

    const byte PIN_OUT_FREQ = 2; // frequeny output

    void setup() {  
    TCCR2A = 0b00000010;           // turn on CTC mode (WGM21 = 1)
    TCCR2B = 0b00000111;           // prescaler = 1024 (CS20 = CS21 = CS22 = 1)
    OCR2A = 77;                    // (16*10^6/(1024*200))-1 (must be <256)
    TIMSK2 = 0b00000010;           // enable timer compare interrupt (OCIE2A = 1) 

    ISR(TIMER2_COMPA_vect) {         //timer2 CTC interrupt (ISR)
    digitalWrite(PIN_OUT_FREQ, !digitalRead(PIN_OUT_FREQ)); // toggle freq. pin

    void loop() {
    digitalWrite(LED_BUILTIN, !digitalRead(LED_BUILTIN));    // toggle LED pin  

The registers TCCR2A and TCCR2B define the modes and the prescaler. We use here the normal mode, so we can choose an arbitrary digital pin. The pins COM2A1, COM2A0, COM2B1 and COM2B0 are set to 0. The waveform generation mode is 2 (CTC), so pins WGM20 = WGM22 = 0 WGM22 = 0and WGM21 = 1 (in TCCR2A). No output compare is forced, so the pins FOC2A = FOC2B are 0. To get a prescaler the bits CS20-CS22 are 1 (TCCR2B). With the bit OCIE2A = 1 in the TIMSK2 SPR, the specific interrupt is allowed.

"Just do it" Interrupts 6:

Interesting links