last updated: 2022-06-02
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.
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.
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.
To understand the difference between polling and interrupts, let's begin with an Arduino example. A simple zebra crossing along a road should help the pedestrians to cross and should stop the car drivers going too fast in a locality. For the cars the green light is on for 3 minutes. After this it goes to orange for 15 seconds and than to red for one minute. 15 seconds after the light going red for the cars, the pedestrian is allowed to cross for 30 seconds.
Write the program using delay()
(no push-button) and an test it. Divide the times by 15 to test. Document the program.
Now we want to enhance the program by adding a push-button for the pedestrians. The cars get red light 15 seconds after pressing the push-button. Write the program an test it (don't use millis()
, because millis()
is already based on interrupts). Document the program.
Imagine a bigger crossing with multiple buttons. What are the problems with such a program?
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.
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).
SREG
register).rjmp
or jmp
). If per example Timer0 generates an overflow interrupt request, the controller will look at the Flash address 0x0020
and find an jump to the interrupt handler (ISR) with the label TIM0_OVF
(an arbitrary name).reti
) instruction in the ISR).SREG
register is cleared, so that other interrupts may be treated.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.
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);
pinMode(PIN_INTERRUPT, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(PIN_INTERRUPT), get_edge, CHANGE);
}
void loop() {
switch (flag) {
case 0:
digitalWrite(PIN_LED1, LOW);
digitalWrite(PIN_LED2, LOW);
break;
case 1:
digitalWrite(PIN_LED1, HIGH);
delay(10);
flag = 0;
break;
case 2:
digitalWrite(PIN_LED2, HIGH);
delay(10);
flag = 0;
break;
}
}
// ISR
void get_edge() {
delayMicroseconds(1000);
flag = digitalRead(PIN_INTERRUPT);
if (flag == HIGH) {
flag = 1;
}
else {
flag = 2;
}
}
delay()
inside the ISR by allowing interrupts with one of the functions interrupts()
or sei()
. The functions noInterrupts()
or cli()
can be used to clear the general interrupt flag (forbid interrupts).Now try to improve the program further. We will use the following short ISR with no delays:
void ped_int() {
flag = HIGH;
}
Further you may use the millis()
-function in your main loop. The millis()
-function uses a timer interrupt (timer0) to count the milliseconds beginning from the start of the program.
HR-SC04
. Find the data sheet, and explain in detail the functioning of this sensor.echo_start
, echo_end
and echo_duration
. The echo signal should trigger a CHANGE
interrupt. Inside the ISR we use the Arduino function micros()
to get the values of echo_start
and echo_end
. We also calculate the difference (echo_duration
) and start a new measurement inside the ISR. Test the program with the microcontroller board MH-ET LIVE D1 mini ESP32 from our rover.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() {
pinMode(PIN_OUT_FREQ,OUTPUT);
pinMode(LED_BUILTIN,OUTPUT);
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();
}
yield();
}
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() {
pinMode(PIN_OUT_FREQ,OUTPUT);
pinMode(LED_BUILTIN,OUTPUT);
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
delay(250);
}
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
= 0
and 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.