Interrupts in Arduino. Part 2

 

Interrupts in Arduino. Part 2

 We continue the topic of using interrupts in Arduino. In the previous publication, we got acquainted with the functions of the Arduino environment for working with external interrupts. Today we’ll look at what other interrupts are present in AVR microcontrollers and look at several examples of their use. And we will start from the very beginning - with the interrupt vector table.

Content



Interrupt vector table

When an interrupt request appears, the microcontroller pauses execution of the main program and moves to the interrupt handler. But how does he find this very handler? For this purpose, the interrupt vector table is used. Each interrupt has its own vector, which is essentially a command to go to the handler function. This table is usually located in the low addresses of program memory: at address zero there is a reset vector, followed by interrupt vectors. Since interrupt sources are external and peripheral devices of the microcontroller, their set depends on the specific microcontroller model. For ATmega328/P the vector table looks like this:

No.AddressSourceDescriptionVector
10x0000RESETReset vector
20x0002INT0External interrupt 0INT0_vect
30x0004INT1External interrupt 1INT1_vect
40x0006PCINT0Interrupt 0 for pin state changesPCINT0_vect
50x0008PCINT1Interrupt 1 for pin state changesPCINT1_vect
60x000APCINT2Interrupt 2 for pin state changesPCINT2_vect
70x000CWDTWatchdog timeoutWDT_vect
80x000ETIMER2_COMPATimer/Counter T2 Match ATIMER2_COMPA_vect
90x0010TIMER2_COMPBTimer/Counter T2 Match BTIMER2_COMPB_vect
100x0012TIMER2_OVFTimer/Counter T2 OverflowTIMER2_OVF_vect
eleven0x0014TIMER1_CAPTCapture Timer/Counter T1TIMER1_CAPT_vect
120x0016TIMER1_COMPATimer/Counter T1 Match ATIMER1_COMPA_vect
130x0018TIMER1_COMPBTimer/Counter T1 Match BTIMER1_COMPB_vect
140x001ATIMER1_OVFTimer/Counter T1 OverflowTIMER1_OVF_vect
150x001CTIMER0_COMPATimer/Counter T0 Match ATIMER0_COMPA_vect
160x001ETIMER0_COMPBTimer/Counter T0 Match BTIMER0_COMPB_vect
170x0020TIMER0_OVFTimer/Counter T0 OverflowTIMER0_OVF_vect
180x0022SPI STCSPI transfer completedSPI_STC_vect
190x0024USART_RXUSART reception completedUSART_RX_vect
200x0026USART_UDREUSART data register is emptyUSART_UDRE_vect
210x0028USART_TXUSART transfer completedUSART_TX_vect
220x002AADCADC conversion completedADC_vect
230x002CEE READYEEPROM ReadyEE_READY_vect
240x002EANALOG COMPInterrupt from analog comparatorANALOG_COMP_vect
250x0030TWIInterrupt from TWI module (I2C)TWI_vect
260x0032SPM READYSPM readinessSPM_READY_vect


The position of the reset vector is determined by the fuse value  BOOTRST . When this fuse is not programmed (which is its default state), the reset vector is located at address 0x0000, where program execution begins. If the BOOTRST fuse is programmed, then after a reset the microcontroller will begin to execute the code located in the bootloader section. Arduino and similar boards work on this principle: after resetting the microcontroller, control is received by the bootloader, which waits for commands from the computer for some time. When receiving a command to write a new program, the bootloader accepts it and places it in program memory. After this, and also if there are no commands within the allotted time, the bootloader transfers control to the main program.

By analogy with the reset vector, you can specify the location of the interrupt vector table: in low addresses of program memory or in the bootloader section. The IVSEL bit of the MCUCR register is responsible for this . By default (after resetting the microcontroller), this bit is reset to 0 and interrupt vectors are located starting from address 0x0002. When the IVSEL bit is set to 1, interrupt vectors are “moved” to the bootloader section. The IVSEL bit will be discussed in more detail below. The following table shows the location of reset and interrupt vectors for various combinations of BOOTRST and IVSEL .

BOOTRST IVSEL Reset vector addressAddress of the beginning of the interrupt vector table
100x00000x0002
110x0000Starting address of the bootloader section + 0x0002 
00Starting address of the bootloader section 0x0002
01 Starting address of the bootloader sectionStarting address of the bootloader section + 0x0002 


Taking into account the above, the beginning of the program in assembly language for the ATmega328/P may look like this:

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

When the power is turned on and a reset signal is generated by the Power-on Reset circuit, the microcontroller will execute the command located at address 0x0000 (or it will be switched to from the bootloader, as is the case with Arduino). This command is an unconditional transition to the RESET label; the initial initialization and execution of the user program begins with it. If, during program operation, a request for external interrupt INT1 is received, for example, and its processing is enabled, the microcontroller will move to the INT1 vector (to address 0x0004), which in turn will redirect the microcontroller to the handler of this interrupt.

If the program does not use interrupts, it can be located immediately from address 0x0000.

And, returning to Arduino boards, I’ll add that the user does not have to create a vector table and fill it with current addresses. This work is performed by the AVR-GCC compiler.


MCUCR register, IVSEL bit

As mentioned, the IVSEL bit of the MCUCR register (MicroController Unit Control Register) is responsible for the position of the interrupt vector table. It contains the following bits: The IVSEL (Interrupt Vector Select) bit is cleared by default and the interrupt vector table starts at address 0x0002. To reassign it to the bootloader section, you need to write a 1 to this bit. To do this, you first need to enable its change by setting the IVCE (Interrupt Vector Change Enable) bit. Then write the new value to IVSEL within 4 clock cycles . Setting IVCE automatically disables all interrupt processing. Their processing will be enabled again after the IVSEL bit is changed or after 4 clock cycles have elapsed. Of course, changing IVSEL does not physically transfer the interrupt vector table; we only tell the microcontroller where it should look for it: in the program section or in the bootloader section. Why this is needed, I think, is clear: if the bootloader uses interrupts, then it must have corresponding handlers and a vector table. Upon completion of its work, the boot loader resets IVSEL so that interrupts are serviced by the main program handlers. The remaining bits of the MCUCR register are of no interest to us now. These are the BODS and BODSE bits , which prohibit the operation of the BOD circuit when the microcontroller goes to sleep. And the PUD bit , which is used to globally disable the pull-up resistors.

MCUCR register structure (ATmega328P)






Interrupt handling

So, we found out how the microcontroller finds the required handler when a particular interrupt occurs. Let us consider in more detail the order of their processing. Bit I of the SREG register

is used to globally enable/disable interrupts . To enable interrupts, it must be set to 1, to disable it, it must be reset to 0. It is this bit that is manipulated by the sei , cli and interrupts , noInterrupts functions discussed in the previous publication . In addition, for each interrupt there is an individual bit that allows its processing.


All interruptions can be divided into two types. Interrupts of the first type are generated when some event occurs, as a result of which the interrupt flag is set. Then, if the interrupt is enabled and the I bit of the SREG register is set, the program counter is loaded with the address of that interrupt's vector. In this case, the flag of this interrupt is reset by hardware (with the exception of the TWI interface flag, which is reset only by software). It can also be reset programmatically by writing the value 1 to it. It is impossible to set the interrupt flag programmatically (for example, in order to emulate the occurrence of an interrupt).

If an interrupt request arrives at a time when its processing is disabled (globally by the I bit or by an individual bit), the corresponding flag will still be set. This way, we will not miss the monitored event and when the interrupt is resolved, its handler will be executed. This is how the considered interrupt type differs from the second type.

Interrupts of the second type do not have flags; they are generated as long as the conditions for their generation are present. Accordingly, if an interrupt request arrives at a time when its processing is prohibited and disappears before it is resolved, the handler will not be executed and we will “lose” this request. If, after executing the handler, the condition for generating an interrupt is still present, it will be executed again. An example of this type of interrupt is external low-level interrupts.

When an interrupt is processed, the I bit of the SREG register is automatically cleared, thereby disabling the processing of other interrupts. When returning from the handler to the main program, the I bit is set again. This behavior is usually preferable for the programmer, since it eliminates the recursive call of handlers, which threatens memory loss. But if the program logic requires nested interrupt processing, then it can be enabled by setting the I bit when entering the handler.


Interrupt processing order

The interrupt flags mentioned earlier log interrupt requests even when their processing is disabled. When subsequently enabled, interrupts will be serviced one at a time according to their priority. The same thing happens when several requests arrive at the same time. The priority of an interrupt is determined by its position in the vector table. In the vector table above, external interrupt INT0 has the highest priority, then INT1, and so on until SPM READY.

If a queue of interrupt requests is formed, then after the execution of the next handler, control always returns to the main program. The next interrupt is processed after one instruction of the main program is executed.


The reset signal is not an interrupt; it is processed out of turn.


Interrupt response time

The shortest response time for any interrupt is 4 clock cycles. During this time, the program counter is stored on the stack and the I bit is reset. The program counter is then loaded with the vector address. As a rule, at this address there is a command to go to the handler, the execution of which takes another 3 clock cycles. If a command lasting several clock cycles is in progress when an interrupt is detected, interrupt processing will begin after the command completes. If an interrupt request is received while the microcontroller is in sleep mode, then in addition to the wake-up time, which depends on the mode and fuse settings, the response time increases by another 4 clock cycles.

Returning from the handler to the main program takes 4 clock cycles, during which the program counter is restored from the stack and bit I of the SREG register is set .


ISR Keyword

It was noted earlier that the compiler is responsible for organizing the interrupt vector table. In AVR-GCC, for each type of microcontroller, a table with the necessary set of vectors is already predefined. We just have to tell the compiler which interrupt handler corresponds to which interrupt, so that it enters the current address of the handler into the table. The ISR macro is used for this . Its syntax is as follows:
ISR (vector, attributes)

The vector parameter specifies the interrupt for which we want to create a handler. Possible parameter values ​​for ATmega328/P are given in the interrupt table in the Vector column. Information about all valid values ​​of this parameter can be found in the AVR Libc documentation:   https://www.nongnu.org/avr-libc/user-manual/group__avr__interrupts.html The optional attributes

parameter can take the following values:  ISR_BLOCK , ISR_NOBLOCK , ISR_NAKED and ISR_ALIASOF(vect) . It is allowed to combine values ​​by specifying them separated by a space.


As an example of using the ISR keyword , consider defining a handler function for a watchdog timer:

ISR (WDT_vect) {
   //Our code is here 
}

Everything is extremely simple: in brackets we indicate the interrupt vector for which this handler is intended; The handler code is given inside curly braces. And, of course, in the main program you need to enable the WDT and configure it to generate interrupts.

Putting addresses in the vector table is not the only work that the compiler does when processing the ISR macro . It also ensures that the contents of the registers used in the handler are saved upon entry and then restored when the handler exits. A stack is used for these purposes. For example, when compiling a program with the above handler for WDT, I got the following code:

push     r1 
push     r0 
in       r0 , SREG 
push     r0 
clr      r1 
; Our code here 
pop      r0 
out      SREG , r0 
pop      r0 
pop      r1 
reti

In this case, the push commands save the values ​​of registers r0, r1 and the status register SREG on the stack (I will explain why this is done in paragraph Prologue and epilogue of the interrupt handler function ). In addition to them, the stack already contains the address for returning to the main program. And this despite the fact that the handler does nothing. If it were to execute instructions using general-purpose registers, their contents would also be stored on the stack. Now it should be clear what kind of memory loss I talked about earlier in the case of enabling nested interrupts. However, if you control the situation and excessive memory consumption is excluded, nothing prevents you from allowing nested interrupt processing. And here we smoothly approached the assignment of ISR_NOBLOCK in the ISR macro .


ISR_NOBLOCK parameter

When you specify the value ISR_NOBLOCK as the second parameter of the ISR macro , the compiler will add a command to set the I bit to the handler , thereby allowing nested interrupts to be processed. Of course, you can insert the interrupts or sei function at the beginning of the handler yourself , but, as we have seen, the compiler supplements the handler with code to save the contents of registers on the stack and interrupts will be enabled only after this code is executed. The ISR_NOBLOCK option   instructs the compiler to insert a  sei command  at the very beginning of the handler before saving registers. This will reduce the response time to an interrupt request in cases where it is necessary. An example of use and the code generated by the compiler are given below.

ISR (WDT_vect, ISR_NOBLOCK) {
   //Our code is here 
}

sei 
push     r1 
push     r0 
in       r0 , SREG 
push     r0 
clr      r1 
; Our code here 
pop      r0 
out      SREG , r0 
pop      r0 
pop      r1 
reti

Here we can mention the sei command , which we see both in the code generated by the compiler and in Arduino sketches. sei and cli are assembly language commands for AVR microcontrollers. These instructions respectively set and clear the I bit of the SREG register . The sei and cli functions that we use in the sketches are macros declared in the file interrupt.h (part of the AVR Libc), which in turn are compiled into one of the given commands. The interrupts and noInterrupts functions are part of the Arduino IDE and are declared in the Arduino.h file as follows:


# define interrupts() sei() 
# define noInterrupts() cli()

These are the same sei and cli for which more convenient names have been defined.


ISR_BLOCK parameter

The ISR_BLOCK value   instructs the compiler not to allow nested interrupts, which is its default behavior. Therefore, the description of a handler with the  ISR_BLOCK parameter  is equivalent to its description without it.


ISR_NAKED parameter

In some cases, the code generated by the compiler to save and restore register values ​​within a handler may not be optimal. For example, the above handler for WDT does nothing at all, but the values ​​of the three registers are nevertheless stored on the stack. If we are not satisfied with the code generated by the compiler, then we can suppress its addition to the handler by specifying the value ISR_NAKED in the second parameter of the ISR macro . In this case, neither code for saving registers nor even a command to return to the main program  reti will be added to the handler ; responsibility for the correct operation of the handler falls on us. Example of using ISR_NAKED :

ISR(TIMER1_OVF_vect, ISR_NAKED)
{
  PORTB |= _BV( 0 );
  reti();
}

reti is an assembly command to return from a handler to the main program. But in the above fragment, the call to reti() is a call to a macro; it is declared in the same interrupt.h file and is compiled into the microcontroller command of the same name.


ISR_ALIASOF parameter

Using  ISR_ALIASOF allows you to tell the compiler that a given interrupt shares a handler with another interrupt. This is useful in cases where the handlers of two or more interrupts are completely identical (or can be brought to a common form). A good example is a common handler for multiple PCINT interrupts:

ISR(PCINT0_vect){
   // Our code is here
}
ISR(PCINT1_vect, ISR_ALIASOF(PCINT0_vect));
ISR(PCINT2_vect, ISR_ALIASOF(PCINT0_vect));

For the code above, the compiler will associate all 3 PCINT vectors with one common handler.


Prologue and epilogue of the interrupt handler function

Since we've touched on the topic of code generated by the compiler for interrupt handlers, let's look at its purpose. The set of instructions that the compiler adds before the handler code is called a prologue; after the handler - an epilogue. The function prologue prepares registers for use: stores their contents on the stack; epilog restores registers before exiting so that the calling/interrupted program can continue working with them. Let's look at the saved registers from the examples above.

SREG - status register. This register contains flags whose values ​​change when various commands are executed. For example, the zero flag (Zero flag) is set to one if 0 is obtained as a result of performing a logical or arithmetic operation. Other flags signal the fact of a transfer, or a negative result, and so on. Preserving the contents of this register when entering the handler is a golden rule, which is also observed in AVR-GCC. However, SREG cannot be directly pushed onto the stack using the push command . Therefore, its contents are first read into register r 0 .

r0 is a general purpose register. AVR-GCC uses it as an intermediate cell in cases like the one described above. Therefore, before reading SREG, the contents of r0 are also pushed onto the stack.

r1 is a general purpose register, in the context of AVR-GCC it is used as a zero register ("zero register") - it must always contain 0. Implying this, this register is used, for example, when it is necessary to compare with 0 or when writing 0 to a memory cell . That is why in the prologue there is a command to clear register r1:
clr      r1
to be sure that it contains 0. Why then store it on the stack if it always contains 0? The fact of the matter is that it is not always the case: multiplication commands place the high byte of the result in register r1. If the interrupt occurred at a time when the result of the multiplication had not yet been processed by the main program, then register r1 may contain a non-zero value. Therefore, its contents are also pushed onto the stack.


Vector BADISR_vect

The situation when a handler is not specified for an enabled interrupt is an error. AVR-GCC, when generating a vector table for all interrupts that do not have their own handler, sets the “default” handler. This handler contains a single command - going to the reset vector. If necessary, you can redefine this handler; for this, the  BADISR_vect vector is used :

ISR(BADISR_vect) {
   //Our code is here
}

Thus, instead of a typical reset, you can specify a different behavior for unexpected interrupts.


Keyword EMPTY_INTERRUPT

In rare cases, the handler is not required to perform any action at all. For example, if we use an interrupt only to wake up the microcontroller from sleep mode. In this case, you can define an empty handler for it (using ISR ), but a better solution would be to use the  EMPTY_INTERRUPT macro . Unlike the ISR macro , it will not add a prologue and epilogue to the handler, and its only command will be to return to the main program - reti . Below is an example of using this macro to interrupt from WDT:

EMPTY_INTERRUPT(WDT_vect);


External interrupts INTx

Having understood the logic of interrupt handling and the syntax of the ISR macro , you can apply your new knowledge in practice. In the previous article, we introduced the attachInterrupt and  detachInterrupt functions for working with external interrupts. Let's now try to do without the IDE functions and perform all the necessary steps to handle external interrupts ourselves.

As already noted, in addition to the I bit , which enables interrupt processing globally, there are bits that enable interrupt processing individually. For external interrupts, these are the two least significant bits of the  EIMSK register (External Interrupt Mask Register): In order to enable interrupt processing at the INT0 input, you must set the same bit of the EIMSK register . To enable interrupts from INT1, the INT1 bit of the register should be set accordingly . By default (after resetting the microcontroller), both bits are cleared and external interrupt processing is disabled. To set the type of monitored events at the INTx inputs, the  EICRA (External Interrupt Control Register A) register is used. The purpose of its bits is as follows: The values ​​of bits  ISC00 and  ISC01 determine what type of events lead to the generation of the INT0 interrupt:

EIMSK register structure (ATmega328P)





EICRA register structure (ATmega328P)


  • 00 - in the presence of a low level signal;
  • 01 - when the signal changes from high level to low and vice versa;
  • 10 - when the signal changes from high level to low;
  • 11 - when the signal changes from low to high.
The purpose of the ISC10 and ISC11 bits  is similar to ISC0x with the difference that they determine the type of events for the INT1 interrupt.

And the last, third, register related to the processing of external interrupts is the  EIFR flag register (External Interrupt Flag Register): The INTF0 bit is set to 1 when the interrupt generation condition is met at the INT0 input in accordance with the configuration of the ISC0x bits of the EICRA register

EIFR register structure (ATmega328P)
. Then, if interrupt handling from INT0 is enabled and the I bit is set to 1, the corresponding handler will be executed and the bit value will be reset. The value of the INTF0 bit can also be reset programmatically by writing the value “1” to it. If the configuration of the ISC0x bits of the EICRA register determines to generate interrupts when there is a low level at the INT0 input, then this flag is not used in processing and will have the value “0”. The INTF1

bit is similar to INTF0 , but signals the detection of an interrupt request at the INT1 input. So, to enable external interrupts and set their processing mode, you must perform the following steps:



  1. Specify a handler using the ISR keyword .
  2. Determine the type of input events that generate an interrupt request ( EICRA register ).
  3. Enable external interrupt processing ( EIMSK register ).
  4. Set the I bit , which enables interrupt processing globally ( SREG register ).

All these registers and bits are defined in the header files included in the AVR Libc and can be accessed by name in the Arduino development environment. As in the previous publication, as an example of using external interrupts, consider the code for controlling the built-in Arduino LED:

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

void  setup () {
   pinMode (ledPin, OUTPUT );
   pinMode (interruptPin, INPUT_PULLUP ); // Connect the second pin to the power supply 
  EICRA &= ~( 1 << ISC00); //Reset ISC00 
  EICRA |= ( 1 << ISC01); // Set ISC01 - track FALLING to INT0 
  EIMSK |= ( 1 << INT0); // Enable interrupt INT0
}

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

ISR(INT0_vect) {
  state = !state;
}

In the setup function, after setting the operating mode of the pins, the ISC00 bit is reset   and the EICRA  register  ISC01 is set . Thus, their combination will ensure that the signal at the INT0 input changes from high to low. Next, we enable INT0 interrupt processing; bit I of the SREG register is already set. The handler is defined using the ISR macro, with the value INT0_vect specified as a parameter (interrupt vector)  . The logic of the program is the same as in the previous publication using attachInterrupt: in the handler we change the value of the variable, and in the loop function we use this variable to control the LED. To check the operation of the sketch, install the button between the second pin and the ground.


Pin Change Interrupts (PCINT)

As the name suggests, this type of interrupt is generated whenever the state of the pin changes. And even though low-level interrupts are not available to us, only on the rising or only on the falling edge of the signal, as is the case with external INTx interrupts, we are no longer limited to two inputs: interrupts for changing the output state are available on almost all Arduino pins. For Arduino UNO (and other boards based on ATmega328/P), these pins are:

  • D8 .. D13 - generate PCINT0 interrupt request
  • A0 .. A5 - generate PCINT1 interrupt request
  • D0 .. D7 - generate a PCINT2 interrupt request

Thus, the interrupt source inputs are combined into groups; each group has its own vector and handler. If we, for example, enable interrupts on all pins of the first group (PCINT0), then the same handler will be called for all interrupt requests coming from them. The microcontroller does not have special tools for determining the specific pin from which the interrupt request came.

Due to the lack of functions in the Arduino IDE that make it easier to use pin change interrupts (as is the case with external INTx interrupts), they are less known and less commonly used by Arduino enthusiasts. In fact, there is nothing complicated in using them, as we will now see.

To work with PCINT, the PCICR , PCIFR and three PCMSKx registers are provided . Let's look at each of them. PCICR  (Pin Change Interrupt Control Register)

bit assignments  :

PCICR register structure (ATmega328P)




  • PCIE0  - the value "1" in this bit enables interrupt processing of the PCINT0 group.
  • PCIE1  - the value "1" in this bit enables interrupt processing of the PCINT1 group.
  • PCIE2  - the value "1" in this bit enables processing of PCINT2 group interrupts.

PCIFR  (Pin Change Interrupt Flag Register) bit assignments  :

PCIFR Register Structure (ATmega328P)



  • PCIF0 - A value of 1 in this bit signals that a PCINT0 interrupt request has been detected.
  • PCIF1 - A value of "1" in this bit signals that a PCINT1 interrupt request has been detected.
  • PCIF2 - A value of "1" in this bit signals that a PCINT2 interrupt request has been detected.


Three registers PCMSK0 , PCMSK1 and PCMSK2 (Pin Change Mask Register) are used to specify the inputs that are allowed to generate an interrupt request signal. The correspondence between the bits of the PCMSKx registers and the pins of the ATmega328/P microcontroller (for a 28-pin DIP package) and their designations in the Arduino IDE is given in the following table:

Bit Pin designation (IDE Arduino)Pin number (Microcontroller)PCINTx
Register PCMSK0
0D814PCINT0
1D915PCINT1
2D1016PCINT2
3D1117PCINT3
4D1218PCINT4
5D1319PCINT5
6-9PCINT6
7-10PCINT7
Register PCMSK1
0A023PCINT8
1A124PCINT9
2A225PCINT10
3A326PCINT11
4A427PCINT12
5A528PCINT13
6-1PCINT14
PCMSK2 register
0D02PCINT16
1D13PCINT17
2D24PCINT18
3D35PCINT19
4D46PCINT20
5D5elevenPCINT21
6D612PCINT22
7D713PCINT23

Pins of the ATmega328/P microcontroller numbered 9 and 10 are used in Arduino to connect the resonator; pin 1 is the Reset input. Therefore, we will have to abandon their use as interrupt inputs. There is no PCINT15 bit  in the ATmega328/P at all.

To enable interrupts when a pin changes state, do the following:
  1. Set a handler for the corresponding PCINT interrupt using the ISR macro .
  2. Allow interrupt generation by the microcontroller pin of interest (  PCMSKx group register ).
  3. Enable processing of the PCINT interrupt that generates the pin of interest (  PCICR register ).
  4. Set the  I bit , which enables interrupt processing globally (  SREG register ).

And for example, another sketch for controlling the built-in LED. This time, separate buttons will be used to turn the LED on and off. This will help you understand the principle of finding the interrupt source pin.

# define ledPin 13       // Pin for the LED
 # define setLedOnPin 8   // Pin of the LED on button
 # define setLedOffPin 9 // Pin of the LED off button

volatile uint8_t state = 0 ;
uint8_t oldPINB = 0xFF ;

void pciSetup( byte pin) {
  *digitalPinToPCMSK(pin) |= bit (digitalPinToPCMSKbit(pin));   // Enable PCINT for the specified pin 
  PCIFR |= bit (digitalPinToPCICRbit(pin)); // Clear the interrupt request flag for the corresponding group of 
  PCICR pins |= bit (digitalPinToPCICRbit(pin)); // Enable PCINT for the corresponding group of pins
}

ISR(PCINT0_vect){ // Interrupt request handler from pins D8..D13
  uint8_t changedbits = PINB ^ oldPINB;
  oldPINB = PINB;

  if (changedbits & ( 1 << PB0)) { // Changed D8 
    state = 1 ; // Light up the LED
  }

  if (changedbits & ( 1 << PB1)) { // Changed D9 
    state = 0 ; // Turn off the LED
  }

  //if (changedbits & (1 << PB2)) { ... } - similar conditions for other pins
}

ISR (PCINT1_vect) { // Interrupt request handler from pins A0..A5 
  // Processing is similar to PCINT0_vect, only change to PINC, oldPINC, PCx
}

ISR (PCINT2_vect) { // Interrupt request handler from pins D0..D7 
  // Processing is similar to PCINT0_vect, only change to PIND, oldPIND, PDx
}

void  setup () {
   pinMode (ledPin, OUTPUT );
   pinMode (setLedOnPin,   INPUT_PULLUP ); // Pull up the PCINT source pins to the power supply 
  pinMode (setLedOffPin, INPUT_PULLUP );
  pciSetup(setLedOnPin); // And enable interrupts for them
  pciSetup(setLedOffPin);
}


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

The previously described manipulations with microcontroller registers in this example are transferred to the  pciSetup function . In addition to setting the desired bits in the PCMSKx and PCICR registers  , the function also resets the interrupt request detection flag in the PCIFR register . This will help avoid calling the handler function unintentionally. After the  pciSetup function  there are three interrupt handlers PCINT0, PCINT1 and PCINT2, but only the first of them is used in the sketch. The rest I added to show how they are described, they can be safely removed. To determine the interrupt source, the handler stores the previous value of pins D8..D13 in the  oldPINB variable  and compares it with the current one. If the value of a pin has changed, then we execute the corresponding block of code. In this case, we change the value of  the state variable to control the LED. The setup function   uses the built-in pull-up resistors and enables interrupts when the state of pins D8 and D9 changes.

To test the sketch, install buttons between the indicated pins and ground. Since the interrupt request is generated when the state of the pin changes, the handler will be called both when the button is pressed and released. But this is not important here.


Watchdog interrupts

Let's look at another example of working with interrupts, this time from a Watchdog Timer (WDT). We have already used WDT interrupts to wake up from sleep mode, now we can study them in more detail.

In general, generating an interrupt to wake up the microcontroller is not the only function of a watchdog timer. It can be used as a simple timer if precision of measured time intervals is not required. In the reset signal generation mode, the watchdog timer is used when the system may freeze: during normal operation, the program regularly resets the watchdog timer, preventing the microcontroller from being reset; If the program hangs after a specified time, the watchdog timer generates a reset signal. This improves the reliability of the microcontroller system. It is also possible to combine interrupt generation and reset modes; in this case, the interrupt handler will first be called, which will save important data, and then at the next WDT timeout, a microcontroller reset signal will be generated.

The watchdog timer is controlled using the WDTCSR register . The purpose of the WDTCSR (Watchdog Timer Control Register)

register bits  is:

WDTCSR Register Structure (ATmega328P)


  • WDIF (Watchdog Interrupt Flag)  - interrupt request flag. Set to 1 after a specified period of time has elapsed when the watchdog timer is configured to generate interrupts. The flag is reset by hardware when the handler is executed or by programmatically writing the value "1" to it.
  • WDIE (Watchdog Interrupt Enable) - a bit that allows interrupt processing from WDT. When this bit is set and WDE is cleared, the watchdog timer is configured to generate interrupts. With WDIE and WDE set , the watchdog timer will first generate an interrupt request, resetting the WDIE , then the next timer timeout will trigger the microcontroller reset signal.
  • WDP[3] (Watchdog Timer Prescaler 3) is the third bit of the WDT frequency divider.
  • WDCE (Watchdog Change Enable) - a bit that allows changing the WDE bit and the divisor value (WDP[3:0]). To change them, WDCE must be set to "1". After four clock cycles, this bit is automatically reset, so it should be set immediately before changing WDE and WDP[3:0].
  • WDE (Watchdog System Reset Enable) - this bit enables the generation of a reset signal by the watchdog timer. Its change is controlled by the WDCE bit, in addition, it cannot be reset as long as the WDRF bit of the MCUSR register is set.
  • WDP[2:0] (Watchdog Timer Prescaler 2, 1, and 0) - the least significant three bits of the WDT clock divider. Valid WDP[3:0] bit combinations and their corresponding time intervals are shown in the table:
WDP3WDP2WDP1WDP0Number of clock cycles of the WDT generatorTime interval
00002K (2048)16ms
00014K (4096)32ms
00108K (8192)64ms
001116K (16384)0.125s
010032K (32768)0.25s
010164K (65536)0.5s
0110128K (131072)1s
0111256K (262144)2s
1000512K (524288)4s
10011024K (1048576)8s
1010Reserved
1011
1100
1101
1110
1111

I also remind you about the  WDTON configuration bit , which affects the operation of the watchdog timer.

To change the WDE and WDP[3:0] values , do the following:
  1. Set the WDCE and WDE bits (regardless of the previous WDE value ).
  2. Over the next four clock cycles, set the desired value of the WDE , WDP[3:0] bits and reset the WDCE bit . This must be done within one team.
To the described sequence, it remains to add the setting of the WDIE bit and the disabling of interrupts while WDE and WDP[3:0] are changing . As a result, we get code for generating interrupts at a given time interval:

volatile  bool f = 0 ;

void  setup () {
   Serial . begin ( 9600 );
  cli(); // Disable interrupts while WDE and WDP are changing 
  asm ( "wdr" ); // Reset WDT 
  // Allow changing the value of the WDT prescaler: 
  WDTCSR |= ( 1 << WDCE) | ( 1 << WDE);
   // Set the WDP3 bit to select the 4s interval and enable interrupts from the WDT: 
  WDTCSR = ( 1 << WDP3) | ( 1 << WDIE );
  sei(); // Enable interrupts
}

void  loop () {
   if (f) {
     Serial . print ( millis ());
     Serial . println ( "WDT!" );
    f = 0 ;
  }
}

ISR(WDT_vect){
  f = 1 ;
}

If you upload the above sketch to Arduino, you will see that after starting the watchdog timer, interrupts are generated approximately every 4 seconds. There is no need to restart the timer. The sketch contains the wdr assembly command ; it resets the timer. It is recommended to reset the watchdog timer before changing the WDP bits, otherwise changing them down may result in a WDT timeout.

The AVR Libc package includes the wdt.h header file. With it, working with WDT is reduced to calling the functions to start, stop and reset the timer. In the following sketch, Arduino goes to sleep and is awakened by a watchdog interrupt. To work with the timer, the functions of the wdt.h file are used.

# include  <avr/wdt.h>
 # include  <avr/sleep.h>

# define ledPin 13 // Pin for LED
 bool f = 0 ;

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

void  loop () {
   // There may be a condition for entering sleep mode 
  wdt_enable(WDTO_4S); // Allow the timer to operate and set the interval 
  // Values ​​defined in wdt.h: WDTO_15MS, WDTO_30MS, WDTO_60MS, WDTO_120MS, 
  // WDTO_250MS, WDTO_500MS, WDTO_1S, WDTO_2S, WDTO_4S, WDTO_8S 
  WDTCSR |= ( 1 << WDIE); // Enable watchdog timer interrupts 
  set_sleep_mode(SLEEP_MODE_PWR_DOWN); //Set the mode we are interested in 
  sleep_mode(); // Put the MK into sleep mode. Wake up on WDT 
  // After the WDT handler is executed, the program will continue from here 
  wdt_disable(); // Stop WDT, we don't need it anymore 
  // Then we perform some actions and go to sleep again 
  digitalWrite (ledPin, (f=!f));
}

EMPTY_INTERRUPT(WDT_vect);

What's interesting about the code above? First, we successfully used the EMPTY_INTERRUPT macro discussed earlier. In addition, the use of macros from the wdt.h header file made it possible to make the sketch shorter. But there is a nuance here: calling  wdt_enable sets the WDE bit , which we do not need (we are only interested in the interrupt). Therefore, we set the WDIE bit ourselves , so the timer is configured to generate an interrupt and reset. This means that the  WDIE bit will be automatically reset each time the handler is called and, if it is not set again, the next WDT timeout will already reset the microcontroller. According to the logic of the program, we do not need WDT after waking up, so we stop it by calling  wdt_disable and we are not in danger of resetting the microcontroller. But this feature must be kept in mind when working with a watchdog timer.

And finally, a piece of code that can be used to reset the microcontroller when the watchdog timeout expires:

# include  <avr/wdt.h>
...
wdt_enable(WDTO_15MS); // Reset via WDT after ~16ms 
while ( 1 ); // Waiting for reset


Conclusion

So, we got acquainted with the interrupt processing logic in AVR microcontrollers and the AVR Libc tools for using them. The material turned out to be difficult, but, it seems to me, quite interesting. I hope it helped you find answers to your questions. If anything remains unclear, ask questions in the comments, I will try to answer them.

Post a Comment

Previous Post Next Post