Interrupts in Arduino. Part 1

 

Interrupts in Arduino. Part 1

 I put this topic off for a long time. And yet, it’s time to consider working with interrupts in Arduino: what they are, why they are needed and how to use them. There is quite a lot of information, so in this article I will talk about external interruptions without delving into the theory. And I will devote the next publication to a more detailed consideration of the interrupt system of AVR microcontrollers.

Content



What is an interrupt

An interrupt is a signal from hardware indicating the occurrence of some priority event that requires immediate attention. In response to an interrupt request, the processor pauses the execution of the current code and calls a special function - the Interrupt Service Routine (ISR). This function reacts to an event by performing certain actions, after which it returns control to the interrupted code.

The interrupt system was introduced into processor architecture in order to free them from the need to constantly poll peripheral devices in anticipation of some event. This is especially true when the processor interacts with slow devices. When interrupts are used, the device itself notifies the processor about the occurrence of an event, and until that moment the processor can perform other work. This approach made it possible to significantly increase the performance of processor systems.

Unlike personal computers, in which interrupts can be hardware and software, in AVR microcontrollers there are only hardware interrupts - their sources are peripheral devices, internal or external. Therefore, interruptions themselves are divided into internal and external.


External interrupts

It should be immediately noted that AVR microcontrollers have two types of external interrupts. Interrupts of the first type, which are the subject of this article, are generated when a specified signal appears at the input of an external interrupt . As a rule, when talking about external interrupts, this is the type that is meant. Interrupts of the second type are generated whenever the state of certain pins of the microcontroller changes, hence their name - Pin Change Interrupt. They will be discussed in the next publication.

Most Arduino boards (specifically those built on the ATmega328/P) have two external interrupt inputs: INT0 and INT1. In order for the microcontroller to begin responding to interrupt requests received from them, the following steps must be performed:
  1. Write an interrupt handler function. It must be parameterless and not return any values.
  2. Enable an external interrupt and set a handler for it. The attachInterrupt function is used for these purposes .


attachInterrupt

The attachInterrupt function  tells the microcontroller what events it should respond to and what handler it should respond to. The function has the following syntax:
attachInterrupt (interrupt, function, mode)
  • interrupt - external interrupt number. It can be specified explicitly (the table of correspondence between interrupt numbers and Arduino pins is given below) or use the digitalPinToInterrupt(pin) function - it will return the interrupt number by pin number.
  • function - interrupt handler function. This function will be called every time an interrupt request occurs.
  • mode - determines what type of events will be considered as an interrupt request. The following values ​​are available for this parameter:
    • LOW - generate an interrupt request when there is a low level signal;
    • RISING - generate an interrupt request when the signal level changes from low to high;
    • FALLING - generate an interrupt request when the signal level changes from high to low;
    • CHANGE - generate an interrupt request for any change in signal level;
    • HIGH is also available for DUE and Zero .

Table of correspondence between interrupt numbers and Arduino pins:

PayINT0INT1INT2INT3INT4INT5
UNO and others based on ATmega328/P23



Leonardo32017
Mega25602321201916

The Arduino DUE board supports interrupts on all I/O lines. For this, you don’t have to use the  digitalPinToInterrupt function and specify the pin number directly in the attachInterrupt parameters .

Each new call to the attachInterrupt function  binds a new interrupt handler, that is, if another handler function was previously attached, it will no longer be called. The situation is similar with the mode parameter , which determines the type of events to which the microcontroller should respond. In other words, you cannot, for example, set separate handlers for FALLING and RISING for one external interrupt input.

Let's look at an example of working with external interrupts using the described functions:

# define ledPin 13 
# define interruptPin 2 // Button between digital pin 2 and GND
 volatile  byte state = LOW ;

void  setup () {
   pinMode (ledPin, OUTPUT );
   pinMode (interruptPin, INPUT_PULLUP );
   attachInterrupt (digitalPinToInterrupt(interruptPin), blink , FALLING);
}

void  loop () {
   digitalWrite (ledPin, state);
}

void  blink () {
  state = !state;
}

In the setup function , we configure the thirteenth pin to be a pin to drive the built-in LED. We connect the second pin to the power supply, this will provide a high-level signal on it. Next, using the attachInterrupt function, we set the blink handler function , which should be called when the signal on the second pin changes from high to low ( FALLING ). Inside the  blink function  , we change the value of the state variable , which subsequently turns the LED on and off in the  loop function . This way you can control the LED without having to poll the button in the main program.

Upload this sketch to Arduino and test it by adding a button between the second digital pin and ground (or simply connecting them with a wire). Overall the sketch will work as intended, but you will notice that sometimes the LED will not respond to a button press. This behavior is caused by bouncing of the button contacts: repeated changes in the signal at digital input 2 lead to repeated calls to the handler. As a result, the value of  the state variable  may remain unchanged. We will return to this problem a little later, but for now we will continue to analyze the example. There is one more point that needs clarification - the volatile keyword .


volatile

volatile is a variable type qualifier that tells the compiler that the variable's value can change at any time. The compiler takes this fact into account when constructing and optimizing executable code. In order not to explain the purpose  of volatile  in the abstract, let's look at the work of the compiler using the following code fragment:

byte A = 0 ;
 byte B;

void  loop () {
  A++;
  B = A + 1 ;
}

Variables A and B are cells in the microcontroller's memory. In order for the microcontroller to do something with them (change, compare, etc.), their values ​​must be loaded from memory into internal registers. Therefore, when compiling this fragment, code like this will be generated:

  1. Load value A from memory into register P1
  2. Load constant 1 into register P2
  3. Add the value of P2 with P1 (result in P2)
  4. Store the value of register P2 in memory at address A
  5. Add the contents of register P1 with constant 2
  6. Store the value of register P1 in memory at address B

The value of variable A is read into the register at the very beginning of the code. If at one of the above steps an interrupt request is received, during the processing of which the value of variable A (memory cell) is changed, then after returning to the loop function , the microcontroller will work with its irrelevant value remaining in the register. Using the volatile qualifier  allows you to avoid such situations. When accessing a variable declared as volatile , the microcontroller will always read its current value from memory, and not use the previously read one (that is, the compiler will generate such code). Of course, the absence of volatile

does not always lead to an error; it all depends on the logic of the program. So the above example of using interrupts to control an LED will work without volatile . But in other cases, using this qualifier will help avoid errors that are difficult to detect. Therefore, it is better to simply make a rule: always use volatile when declaring variables that the interrupt handler shares with other functions. Let's look at another interesting example:



# define interruptPin 2 
volatile  byte f = 0 ;

void  setup () {
   pinMode (interruptPin, INPUT_PULLUP );
   attachInterrupt (digitalPinToInterrupt(interruptPin), buttonPressed, FALLING);
}

void  loop () {
   while (f == 0 ) {
     // Do something while waiting for the button to be pressed
  }
  // Button pressed
}

void buttonPressed() {
  f = 1 ;
}

The loop inside the loop function must run as long as the value of f is zero. And it should change in the interrupt handler when the button is pressed. If we declared the variable f without the volatile qualifier , then the compiler, “seeing” that the value of the variable inside the loop does not change and the loop exit condition remains false, would replace the loop with an infinite one. This is how code optimization works during compilation. Having entered such a cycle, the microcontroller will simply freeze. Declaring a variable with the volatile qualifier ensures that the variable will not receive any optimized access type.

When working with interrupts and sharing variables between the handler and the main program, there is a very important point to remember: AVR microcontrollers are 8-bit and several separate operations are required to load a 16- or 32-bit value from memory. Accordingly, a situation is possible when the microcontroller reads the low byte of a variable from memory, after which an interrupt request will be received and the value of this variable will be changed by the handler. After returning to the main program, the microcontroller reads the high byte from memory, receiving not just the old value, but a completely different one, which can lead to a program error. To avoid such situations, you can either disable interrupt processing while accessing shared variables using the  interrupts  and  noInterrupts functions , or place access to such a variable in an atomically executed block of code .


The interrupts and noInterrupts functions

These functions are used to enable and disable interrupt processing, respectively. They can be useful when accessing a variable whose value is changed by an interrupt handler (the situation described above). Or if the code is runtime sensitive and therefore needs to run without interruption. In this case, the code should be surrounded by the specified functions:

  noInterrupts (); // Disable interrupt processing 
  // Time-critical code 
  interrupts (); // Enable interrupt processing

Just keep in mind that interrupts are used to operate many modules: timers-counters, UART, I2C, SPI, ADC and others. By disabling interrupt handling, you will not be able, for example, to use the Serial class or the millis and micros functions. Therefore, avoid blocking interrupts for long periods of time. In addition to the interrupts and noInterrupts

functions , you can use the sei  and cli functions for the same purposes  - enable and disable interruptions, respectively. There is no difference between them, it’s just that the latter are part of the AVR Libc set of libraries, while others were introduced by the developers of the Arduino IDE as an alternative that is easier to remember and understand in the program. Agree, noInterrupts is a more descriptive name than cli .


Atomically executable code blocks

Constructs for working with atomically and non-atomically executed code blocks are part of the AVR Libc. They allow you to define a block that is guaranteed to be executed atomically (that is, without interruptions) or non-atomically. Below is an example of using an atomically executable block:

# include  <util/atomic.h>

# define interruptPin 2 
volatile  unsigned  int counter = 0 ;

void  setup () {
   pinMode (interruptPin, INPUT_PULLUP );
   attachInterrupt (digitalPinToInterrupt(interruptPin), myISR, FALLING);
}

void myISR() {
  counter++;
}

void  loop () {
   unsigned  int counter_copy;  
  ATOMIC_BLOCK(ATOMIC_FORCEON){
    // Atomic access to the 2-byte variable counter:
    counter_copy = counter;
  }
  // For further work, use the counter_copy value 
  // if (counter_copy == ... 
}

This example starts by including the  atomic.h header file , which contains macros to support atomicity. The loop function then accesses the two-byte counter variable . The call is placed in the ATOMIC_BLOCK block , which guarantees its atomicity. As in the case of using the interrupts  and noInterrupts functions , atomicity is achieved by globally disabling interrupt processing before entering such a block. After exiting the block, interrupt processing is enabled, which is controlled by the  ATOMIC_FORCEON parameter . If for some reason we do not know before entering the block whether interrupts are enabled or not, then forcing them to be enabled after exiting the block would be a bad decision. In such cases, the ATOMIC_BLOCK macro  must use the  ATOMIC_RESTORESTATE parameter  - it will restore the previous value of the global interrupt enable flag:

ATOMIC_BLOCK(ATOMIC_RESTORESTATE){
   // ... 
}

To define non-atomically executable code blocks, the macro  NONATOMIC_BLOCK is used , and its parameter can be one of the values:  NONATOMIC_FORCEOFF  NONATOMIC_RESTORESTATE . The purpose of such a block is the opposite of what was discussed earlier: it defines a block that should not be executed atomically. This can be useful when inside a large atomically executed block there are fragments that do not require atomicity.


detachInterrupt

If your program no longer needs to monitor external interrupts, you can cancel their processing with the detachInterrupt function . Its only parameter is the interrupt number specified earlier in the attachInterrupt function:

detachInterrupt (digitalPinToInterrupt(interruptPin));


Contact bounce

Obviously, when a signal distorted by contact bounce is applied to the external interrupt input, the interrupt handler will be executed several times. In some cases, bouncing is not a problem, for example, if we are waiting for a button to be pressed to perform some action in the program: it is enough to disable the tracking of this interrupt when entering the handler, and turn it on again after executing our code (implying that the execution time of our actions exceeds the duration of contact bounce). This way we will skip the bounce and the handler will be executed only once. It's another matter when interrupts are used to register events. A good example is a mechanical rotary encoder. In this case, we cannot disable interrupt tracking because we risk missing the event (pulse from the encoder). The processor cannot decide whether the impulse is false or not, and this is simply not the task that it should be doing. Therefore, the only rational solution is to use hardware debouncers. Several of my publications are devoted to this topic, links to them are below:


Using interrupts to wake up from sleep mode

The previously discussed examples, in which button presses were monitored, can be implemented without interruptions by regularly polling the pins. But there is a task that cannot be done without using interrupts. We are talking about waking up the microcontroller from sleep mode.

When entering sleep mode (energy saving mode), program execution is suspended. But a number of microcontroller nodes, in particular the interrupt processing subsystem, continue to work, monitoring the receipt of interrupt requests. If processing of an incoming interrupt request is allowed, the microcontroller will wake up and proceed to execute the corresponding handler, after which it will continue executing the main program.

In the article  Arduino Power Saving Modes,  I described the available (for boards based on ATmega328/P) modes and types of interrupts that can wake up the microcontroller. You can also find this information in the Sleep Modes section of the datasheet :

Power saving and interrupt modes ATmega328/P

External interrupts (both INTx, discussed in this article, and level change interrupts) can bring the microcontroller out of any power saving mode. Here I want to draw attention to the inaccuracy in the datasheet on the ATmega328/P, supposedly in power saving modes, when the clocking of the I/O subsystem is suspended (and these are all modes except Idle), detection of external INTx interrupts on the edge is not available and an interrupt should be used to wake up the microcontroller at a low level. This is wrong. Detecting an interrupt request and waking up the microcontroller is possible for all (LOW, RISING, FALLING, CHANGE) event types on the INTx inputs. Well, to confirm my words, a simple example with going to sleep and waking up by an external interruption via FALLING:

# include  <avr/sleep.h>
 # define ledPin 13 
# define interruptPin 2 // Button between digital pin 2 and GND

void  setup () {
   pinMode (ledPin, OUTPUT );
   pinMode (interruptPin, INPUT_PULLUP );
}

void  loop () {
   // Turn off the LED, enable the FALLING interrupt and go to sleep: 
  digitalWrite (ledPin, 0 );
   attachInterrupt (digitalPinToInterrupt(interruptPin), myISR, FALLING);
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  sleep_mode();
  
  // After waking up, the handler will be executed first. Then we'll come back here. 
  // Disable the interrupt, turn on the LED to indicate wakeup 
  detachInterrupt (digitalPinToInterrupt(interruptPin));
   digitalWrite (ledPin, 1 );
   // And here can be the code that should be executed when the button is pressed 
  delay ( 2000 );
}

void myISR() {
   //This is empty
}



Request an interrupt while a handler is executing

I will leave a detailed discussion of the topic of receiving interrupt requests and the order of their servicing for the next part of the publication. But now I consider it necessary to voice an important point. When moving to the interrupt handler, processing of all other interrupts is disabled. As if we called the  noInterrupts function . After the handler has executed and exited, interrupt processing is enabled again. Thus, interruption of one handler by another is excluded (unless specifically allowed). For this reason, handlers cannot access functions whose operation is based on interrupts.


General guidelines for writing interrupt handlers

In conclusion, I would like to give a few recommendations for writing interrupt handlers.
  • First, keep your handlers as short as possible. After all, they interrupt the execution of the main program and also block the processing of other interrupts. If possible, the handler should record only the fact that the event occurred by changing the value of the variable. And the reaction to the event itself must be performed in the main program when analyzing this variable.
  • As already mentioned, when entering the handler, a global ban on processing other interrupts is established. And this in turn affects the operation of functions that use interrupts. Be careful with them. If you are not sure about the safety of calling them, then it is better not to use them in the handler.
  • Make it a rule to declare variables shared between the main program and the handler as  volatile . And do not forget that this qualifier is not enough in the case of multibyte variables - use atomically executed blocks or interrupts/noInterrupts when working with them
With this I will finish the first part of the publication; it has already turned out to be quite lengthy. In the next part, I will show how to work with interrupts without using Arduino functions, and also explain the order in which the microcontroller processes them.

Post a Comment

أحدث أقدم