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
MCUCR register, IVSEL bit
Interrupt processing
Sequence of interrupt processing
Response time to interrupt request
Keyword ISR
- Parameter ISR_NOBLOCK
- Parameter ISR_BLOCK
- Parameter ISR_NAKED
- Parameter ISR_ALIASOF
Prologue and epilogue of the interrupt handler function
Vector BADISR_vect
Keyword EMPTY_INTERRUPT
External interrupts INTx
Interrupts at Pin Change Interrupts (PCINT)
Watchdog Interrupts
Conclusion
MCUCR register, IVSEL bit
Interrupt processing
Sequence of interrupt processing
Response time to interrupt request
Keyword ISR
- Parameter ISR_NOBLOCK
- Parameter ISR_BLOCK
- Parameter ISR_NAKED
- Parameter ISR_ALIASOF
Prologue and epilogue of the interrupt handler function
Vector BADISR_vect
Keyword EMPTY_INTERRUPT
External interrupts INTx
Interrupts at Pin Change Interrupts (PCINT)
Watchdog Interrupts
Conclusion
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. | Address | Source | Description | Vector |
1 | 0x0000 | RESET | Reset vector | |
2 | 0x0002 | INT0 | External interrupt 0 | INT0_vect |
3 | 0x0004 | INT1 | External interrupt 1 | INT1_vect |
4 | 0x0006 | PCINT0 | Interrupt 0 for pin state changes | PCINT0_vect |
5 | 0x0008 | PCINT1 | Interrupt 1 for pin state changes | PCINT1_vect |
6 | 0x000A | PCINT2 | Interrupt 2 for pin state changes | PCINT2_vect |
7 | 0x000C | WDT | Watchdog timeout | WDT_vect |
8 | 0x000E | TIMER2_COMPA | Timer/Counter T2 Match A | TIMER2_COMPA_vect |
9 | 0x0010 | TIMER2_COMPB | Timer/Counter T2 Match B | TIMER2_COMPB_vect |
10 | 0x0012 | TIMER2_OVF | Timer/Counter T2 Overflow | TIMER2_OVF_vect |
eleven | 0x0014 | TIMER1_CAPT | Capture Timer/Counter T1 | TIMER1_CAPT_vect |
12 | 0x0016 | TIMER1_COMPA | Timer/Counter T1 Match A | TIMER1_COMPA_vect |
13 | 0x0018 | TIMER1_COMPB | Timer/Counter T1 Match B | TIMER1_COMPB_vect |
14 | 0x001A | TIMER1_OVF | Timer/Counter T1 Overflow | TIMER1_OVF_vect |
15 | 0x001C | TIMER0_COMPA | Timer/Counter T0 Match A | TIMER0_COMPA_vect |
16 | 0x001E | TIMER0_COMPB | Timer/Counter T0 Match B | TIMER0_COMPB_vect |
17 | 0x0020 | TIMER0_OVF | Timer/Counter T0 Overflow | TIMER0_OVF_vect |
18 | 0x0022 | SPI STC | SPI transfer completed | SPI_STC_vect |
19 | 0x0024 | USART_RX | USART reception completed | USART_RX_vect |
20 | 0x0026 | USART_UDRE | USART data register is empty | USART_UDRE_vect |
21 | 0x0028 | USART_TX | USART transfer completed | USART_TX_vect |
22 | 0x002A | ADC | ADC conversion completed | ADC_vect |
23 | 0x002C | EE READY | EEPROM Ready | EE_READY_vect |
24 | 0x002E | ANALOG COMP | Interrupt from analog comparator | ANALOG_COMP_vect |
25 | 0x0030 | TWI | Interrupt from TWI module (I2C) | TWI_vect |
26 | 0x0032 | SPM READY | SPM readiness | SPM_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 address | Address of the beginning of the interrupt vector table |
1 | 0 | 0x0000 | 0x0002 |
1 | 1 | 0x0000 | Starting address of the bootloader section + 0x0002 |
0 | 0 | Starting address of the bootloader section | 0x0002 |
0 | 1 | Starting address of the bootloader section | Starting 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.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.
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.
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.
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:
- 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.
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
. 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:
- Specify a handler using the ISR keyword .
- Determine the type of input events that generate an interrupt request ( EICRA register ).
- Enable external interrupt processing ( EIMSK register ).
- 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 :
- 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 :
- 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.
Bit | Pin designation (IDE Arduino) | Pin number (Microcontroller) | PCINTx |
---|---|---|---|
Register PCMSK0 | |||
0 | D8 | 14 | PCINT0 |
1 | D9 | 15 | PCINT1 |
2 | D10 | 16 | PCINT2 |
3 | D11 | 17 | PCINT3 |
4 | D12 | 18 | PCINT4 |
5 | D13 | 19 | PCINT5 |
6 | - | 9 | PCINT6 |
7 | - | 10 | PCINT7 |
Register PCMSK1 | |||
0 | A0 | 23 | PCINT8 |
1 | A1 | 24 | PCINT9 |
2 | A2 | 25 | PCINT10 |
3 | A3 | 26 | PCINT11 |
4 | A4 | 27 | PCINT12 |
5 | A5 | 28 | PCINT13 |
6 | - | 1 | PCINT14 |
PCMSK2 register | |||
0 | D0 | 2 | PCINT16 |
1 | D1 | 3 | PCINT17 |
2 | D2 | 4 | PCINT18 |
3 | D3 | 5 | PCINT19 |
4 | D4 | 6 | PCINT20 |
5 | D5 | eleven | PCINT21 |
6 | D6 | 12 | PCINT22 |
7 | D7 | 13 | PCINT23 |
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:
- Set a handler for the corresponding PCINT interrupt using the ISR macro .
- Allow interrupt generation by the microcontroller pin of interest ( PCMSKx group register ).
- Enable processing of the PCINT interrupt that generates the pin of interest ( PCICR register ).
- 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.
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:
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:
- 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:
WDP3 | WDP2 | WDP1 | WDP0 | Number of clock cycles of the WDT generator | Time interval |
0 | 0 | 0 | 0 | 2K (2048) | 16ms |
0 | 0 | 0 | 1 | 4K (4096) | 32ms |
0 | 0 | 1 | 0 | 8K (8192) | 64ms |
0 | 0 | 1 | 1 | 16K (16384) | 0.125s |
0 | 1 | 0 | 0 | 32K (32768) | 0.25s |
0 | 1 | 0 | 1 | 64K (65536) | 0.5s |
0 | 1 | 1 | 0 | 128K (131072) | 1s |
0 | 1 | 1 | 1 | 256K (262144) | 2s |
1 | 0 | 0 | 0 | 512K (524288) | 4s |
1 | 0 | 0 | 1 | 1024K (1048576) | 8s |
1 | 0 | 1 | 0 | Reserved | |
1 | 0 | 1 | 1 | ||
1 | 1 | 0 | 0 | ||
1 | 1 | 0 | 1 | ||
1 | 1 | 1 | 0 | ||
1 | 1 | 1 | 1 |
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:
- Set the WDCE and WDE bits (regardless of the previous WDE value ).
- 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.
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>
# 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
Post a Comment