Often when a microcontroller is being used, we want it to know when certain things occur, and then have something happen in response. A simple example is to have a pushbutton determine when an LED turns on.
One implementation that would satisfy this is called “polling”. Inside of the main while loop we could test the state of an input pin every loop iteration and turn on an LED depending on its state.
The above psuedocode checks the pin state of an input pin connected to a pushbutton and depending on the state turns on or off an LED.
Polling a pin state becomes tricky when you have to do other things in your event loop, especially things that take a lot of time. If for instance you needed to send or interpret some kind of serial data, doing so would take many clock cycles. This would essentially add latency to the response time of the LED to the pushbutton, as checking the pin state would have the same priority as every other sequential task in the while loop.
Hardware interrupts allow for asynchronous handling of system events. If an interrupt is set for a pin, when the pin state changes the code execution in the main loop halts and the code inside an Interrupt Service Routine (ISR) function is executed.
Let’s consider how we can use an interrupt to turn on 2 LEDs (green, red), while another two LEDs (blue, orange) are independently blinking. We will implement the blinking LEDs through the main while loop using a delay function, which would add a lot of latency to our pushbutton controlled LEDs if we controlled them by polling.
Keep in mind that while we could implement the blinking LEDs using timer hardware, we will use the delay implementation in place of more involved code that adds latency between code execution in the main while loop.
Above is the circuit diagram for our system and picture of an implementation on a tiny breadboard.
Above is the pinout for the ATtiny85, for reference.
Let’s consider the code implementation in chunks:
We define the CPU frequency as the default 1MHz, so that our delay function delays for the right time. We include, io.h for our pin and register macros (i.e. PB0, PORTB, etc.), delay.h for our delay function, and interrupt.h for our interrupt macros.
We will initialize our interrupt in a function and use a pin change interrupt to allow our pushbutton to act as a hardware interrupt. A pin change interrupt on the ATtiny85 will look for a logical change on a PCINT pin (PCINT0-PCINT5), and if it finds one will set the pin change interrupt flag and call the PCINT0 interrupt vector. The PCINT0 interrupt vector will halt the execution of code in the main while loop and begin execution of the corresponding Interrupt Service Routine code which will act to turn on our green and red LEDs. Once the ISR toggles the LEDs on or off, the execution will return to where it left off in the main loop, keeping the other LEDs blinking away.
Chapter 9 of the datasheet covers interrupts, explaining how to enable them and use them. To turn on the pin change interrupt and enable it for the correct pin (PCINT4) we will need to set some bits in specific registers.
We start by setting the Pin Change Interrupt Enable (PCIE) bit in the General Interrupt Mask Register (GIMSK).
Next we set the PCINT4 bit in the Pin Change Mask Register (PCMSK) to enable the PCINT4 (PB4) pin as a pin change interrupt.
Finally we call the function sei() from interrupt.h, which enables the global interrupt flag, thereby enabling interrupts for our system.
Next we write our Interrupt Service Routine (ISR) for the Pin Change Interrupt vector (PCINT0_vect), which is the code that will be executed after a pin change interrupt occurs. When the pushbutton is pressed, the state of the PCINT4 pin will change, and the PCINT0 vector will be called, executing the code inside our ISR. Likewise, when the pushbutton is released, the pin state of PCINT4 will change again, and the code in the ISR will be executed. Inside our ISR we use XOR to toggle the state of PB0 and PB2 on PORTB, either turning on the green and red LED if off, or vice versa.
Keep in mind that ISR is a special function from interrupt.h that gets called when an interrupt flag is set and the corresponding interrupt vector is called.
Inside our main loop we first set PB0-PB3 as outputs for our LEDs. To use our pushbutton with one end connected to PB4 and the other to ground, we will have to enable the internal pullup resistor. The internal pullup resistor is set by not enabling PB4 as an output, while setting the corresponding bit on PORTB.
We call our interrupt initialization function and enter our main while loop, which delays 250 ms between toggling PB1 and PB3 on and off.
Above is a short video demonstrating the behavior of the circuit.
One problem to keep in mind with using pushbuttons for hardware interrupts is that unlike with a polling implementation, there really isn’t a way to debounce the input. This can lead to the springy/bouncy nature of the button introducing unintended interrupt triggers. Hardware debounced inputs can help with this, but typically for pushbutton inputs it is best to poll them and debounce the input.
While pin change interrupts don’t work so well with pushbuttons, they do work well to interface with hardware peripherals that change one of their output lines to communicate something, perhaps that it is ready to receive data from the microcontroller, or that it is done processing some received data. These sort of implementations would not need a debounced input on the microcontroller and could be used with a pin change interrupt.
There are also interrupt vectors for when a timer has an compare match or when a timer overflows, i.e. it goes from 255 to 0. These can be useful when a system needs to do something regularly that is more involved than toggling an IO line.
We will use timer/counter 1 along with it’s compare match interrupt to count from 0-15 in binary using the same circuit with 4 LEDs, having a 1 second delay between each increment. The previous post titled “ATtiny85: Blinking Without Clock Cycles” introduces timer/counter hardware, so make sure to reference it if you want to learn more.
Let’s look at the code:
We will be using an 8-bit variable ‘i’ to store the current value displayed on the LED counter. The volatile keyword makes sure that our compiler doesn’t optimize out (remove) our variable i. Since i will only be changed in an Interrupt Service Routine, which may never be executed, the compiler thinks that this variable is not going to be used and will remove it. By adding the volatile keyword, we prevent this from happening.
If you plan on altering a variable inside of an ISR make sure to give it the volatile keyword. Also, since ISR only takes an interrupt vector as an argument, any variable that it needs to change within it must be made global in scope.
We initialize our timer and corresponding interrupt in a function that again sets the appropriate bits in the specific register detailed in the datasheet in chapter 12.
We set the Clear Timer on Compare match (CTC1) Bit in the Timer/Counter 1 Control Register (TCCR1) to clear our counter to 0 on each match with our compare value.
We next set the bits that set the prescaler that will divide down our system clock frequency to be used by the timer/counter to increment the counter. Our system clock runs at 1 MHz, so if we set the prescaler to 8192, the clock used for the timer will have a period of 1/(1E6/8192) = 8.192 ms. So we will need to count 122 of these adjusted clock cycles to amount to nearly 1 second (122*8.192ms = 999.424 ms).
We consult the timer/counter1 prescale select table for the TCCR1 register, and see that we need to set bits CS13, CS12, and CS11 to set the 8192 clock prescaler.
Next we set the compare match value OCR1C to 122, such that the counter will count from 0 to 122 in around 1 second, at which point the compare match interrupt will be activated, the counter will be cleared, and the value i will be incremented.
Finally we set the Output Compare Interrupt Enable (OCIE1A) bit in the Timer/Counter Interrupt Mask Register (TIMSK), to enable the timer compare match interrupt we will use.
Next we define the ISR that receives the TIMER1_COMPA vector that is called when the timer compare value is matched. In this we increment i from 0 to 15 and then set PORTB to i, which takes the lower 4 bits of i that count up to 15 and sets pins PB0-PB3 to their values. This will display the value of i on the 4 LEDS.
Inside main we set pins PB0-PB3 as outputs, enable the pullup on the pushbutton that is still attached, just in case, initialize the timer and timer interrupts with initTimer1(), and then enable all interrupts with sei(). This time our while loop is empty, left to take care of some other task, only to be briefly interrupted when it is time to update i and PORTB.
Above is a short video demonstrating the circuit behavior.
There are many different interrupt vectors to use on the ATtiny85.
Above is a table of all the interrupt vectors available, in order of priority. Priority makes it so that if two interrupt vectors are triggered at the same time, the one with higher priority is taken care of first.
The interrupt for INT0 is an external interrupt that is tied to pin PB2 that has a higher priority than a pin change interrupt, but essentially works the same with a few extra features, such as not only being able to trigger on a logical change but also a positive or negative edge change. The higher priority of INT0 over PCINT makes it useful for using for an external hardware interrupt that you want to be served before another that is set to PCINT. Getting your priorities straight is key.
To wrap it all up, interrupts make it possible to asynchronously handle system events. Being able to utilize the power of interrupts may seem intimidating at first, but taking the time to learn and master them will allow you to build more responsive, scheduled, and robust systems.