- Pre-Lab preparation
- Part 1: Polling and interrupts
- Part 2: Timer overflow
- Part 3: Extend the overflow
- Challenges
- References
- Arduino Uno board, USB cable
- Breadboard
- 2 LEDs or 1 two-color LED
- 2 resistors
- 1 push button
- Jumper wires
- Use
#define
compiler directives - Use internal microcontroller timers
- Understand overflow
- Combine different interrupts
Consider an n-bit number that we increment based on the clock signal. If we reach its maximum value and try to increase it, the value will be reset. We call this state an overflow. The overflow time depends on the frequency of the clock signal, the number of bits, and on the prescaler value:
Note: The equation was generated by Online LaTeX Equation Editor using the following code.
t_{OVF} = \frac{1}{f_{CPU}}\cdot 2^{nbit}\cdot prescaler
-
Complete the
GPIO_toggle
function ingpio.h
library from the previous lab. -
Calculate the overflow times for three Timer/Counter modules that contain ATmega328P if CPU clock frequency is 16 MHz. Complete the following table for given prescaler values. Note that, Timer/Counter2 is able to set 7 prescaler values, including 32 and 128 and other timers have only 5 prescaler values.
Module Number of bits 1 8 32 64 128 256 1024 Timer/Counter0 8 16u 128u -- -- Timer/Counter1 16 -- -- Timer/Counter2 8
The state of continuous monitoring of any parameter is called polling. The microcontroller keeps checking the status of other devices; and while doing so, it does no other operation and consumes all its processing time for monitoring.
While polling is a straightforward method for monitoring state changes, it comes with a trade-off. If the polling interval is too long, there may be a significant delay between the occurrence and detection of a state change, potentially leading to missing the change entirely if the state reverts before the next check. On the other hand, a shorter interval provides quicker and more dependable detection, but it also consumes considerably more processing time and power, as there are more unsuccessful checks.
An alternative approach is to employ interrupts. In this approach, a state change triggers an interrupt signal that prompts the CPU to pause its current operation (while preserving its current state), execute the interrupt-related processing, and subsequently restore its prior state before resuming from where it had been interrupted.
An interrupt is a fundamental feature of a microcontroller. It represents a signal sent to the processor by hardware or software, signifying an event that requires immediate attention. When an interrupt is triggered, the controller finishes executing the current instruction and proceeds to execute an Interrupt Service Routine (ISR) or Interrupt Handler. ISR tells the processor or controller what to do when the interrupt occurs. After the interrupt code is executed, the program continues exactly where it left off.
Interrupts can be set up for events such as a counter's value, a pin changing state, receiving data through serial communication, or when the Analog-to-Digital Converter has completed the conversion process. See the ATmega328P datasheet (section Interrupts > Interrupt Vectors in ATmega328 and ATmega328P) for sources of interruptions that can occur on ATmega328P.
All interrupts are disabled by default. If you want to use them, you must first enable them individually in specific control registers and then enable them centrally with the sei()
command (Set interrupt). You can also centrally disable all interrupts with the cli()
command (Clear interrupt).
A timer (or counter) is an integral hardware component in a microcontroller unit (MCU) designed for measuring time-based events. The ATmega328P MCU features three timers, designated as Timer/Counter0, Timer/Counter1, and Timer/Counter2. Timer0 and Timer2 are 8-bit timers, whereas Timer1 is a 16-bit timer.
The counter increments in alignment with the microcontroller clock, ranging from 0 to 255 for an 8-bit counter or 65,535 for a 16-bit counter. If counting continues, the timer value overflows to the default value of zero. Various clock sources can be designated for each timer by utilizing a CPU frequency divider equipped with predetermined prescaler values, including 8, 64, 256, 1024, and other options.
-
The timer modules can be configured with several special purpose registers. According to the ATmega328P datasheet (eg in the 8-bit Timer/Counter0 with PWM > Register Description section), which I/O registers and which bits configure the timer operations?
Module Operation I/O register(s) Bit(s) Timer/Counter0 Prescaler
8-bit data value
Overflow interrupt enableTimer/Counter1 Prescaler
16-bit data value
Overflow interrupt enableTCCR1B
TCNT1H, TCNT1L
TIMSK1CS12, CS11, CS10
(000: stopped, 001: 1, 010: 8, 011: 64, 100: 256, 101: 1024)
TCNT1[15:0]
TOIE1 (1: enable, 0: disable)Timer/Counter2 Prescaler
8-bit data value
Overflow interrupt enable -
In Visual Studio Code create a new PlatformIO project
lab3-timers
forArduino Uno
board and change project location to your local folder. -
IMPORTANT: Rename
LAB3-TIMERS > src > main.cpp
file tomain.c
, ie change the extension to.c
. -
In PlatformIO project, create a new folder
LAB3-TIMERS > lib > gpio
. Copy your GPIO library filesgpio.c
andgpio.h
from the previous lab to this folder. -
In PlatformIO project, create a new file
LAB3-TIMERS > include > timer.h
. Copy/paste header file totimer.h
. See the final project structure:LAB3-TIMERS // PlatfomIO project ├── include // Included file(s) │ └── timer.h ├── lib // Libraries │ └── gpio // Your GPIO library (from the previous Lab) │ ├── gpio.c │ └── gpio.h ├── src // Source file(s) │ └── main.c ├── test // No need this └── platformio.ini // Project Configuration File
To simplify the configuration of control registers, we defined Timer/Counter1 macros with meaningful names in the
timer.h
file. Because we only define macros and not function bodies, thetimer.c
source file is not needed this time! -
Copy/paste the following template to
LAB3-TIMERS > src > main.c
source file and complete the code to blink the on-board LED every 262 ms using Timer1 overflow interrupt.// -- Includes ------------------------------------------------------- #include <avr/io.h> // AVR device-specific IO definitions #include <avr/interrupt.h> // Interrupts standard C library for AVR-GCC #include <gpio.h> // GPIO library for AVR-GCC #include "timer.h" // Timer library for AVR-GCC int main(void) { ... // Enable overflow interrupt TIM1_ovf_enable(); ... // Enables interrupts by setting the global interrupt mask sei(); ... } // Interrupt service routines ISR(TIMER1_OVF_vect) { ... }
-
In
timer.h
header file, define similar macros also for Timer/Counter0. -
On the breadboard, connect an LED and a resistor to pin PB0. Alternatively, you can use a two-color LED (a 3-pin LED) and resistors. If you choose the two-color LED, connect its pins to PB0 and PB1. In
main.c
file, modify the code to use Timer0 and Timer1 interrupts to control all the LEDs. Compile the code and upload it to the ATmega328P microcontroller. Ensure that the LEDs operate correctly as per the modified code. -
(Optional:) Consider an active-low push button with internal pull-up resistor on the PD2 pin. Use Timer0 4-ms overflow to read button status. If the push button is pressed, turn on the on-board LED; turn the LED off after releasing the button. Note: Within the Timer0 interrupt service routine, use a read function from your GPIO library to get the button status.
-
Use Timer/Counter0 16-ms overflow and toggle external LED approximately every 100 ms (6 overflows x 16 ms = 100 ms).
Note: Use static variables declared in functions that use them for even better isolation or use volatile for all variables used in both Interrupt routines and main code loop. According to [7] the declaration line
static uint8_t n_ovfs = 0;
is only executed the first time, but the variable value is updated/stored each time the ISR is called.ISR(TIMER0_OVF_vect) { static uint8_t n_ovfs = 0; n_ovfs++; if (n_ovfs >= 6) { // Do this every 6 x 16 ms = 100 ms n_ovfs = 0; ... } // Else do nothing and exit the ISR }
-
Reduce the overflow time by storing a non-zero value in the Timer/Counter0 data register TCNT0 after each overflow.
ISR(TIMER0_OVF_vect) { static uint8_t n_ovfs = 0; // Change 8-bit timer value anytime it overflows TCNT0 = 128; n_ovfs++; if (n_ovfs >= 6) { n_ovfs = 0; ... } // Overflow time: t_ovf = 1/f_cpu * (2^bit-init) * prescaler // Normal counting: // TCNT0 = 0, 1, 2, ...., 128, 129, ...., 254, 255, 0, 1 // |---------------------------------------| // 16 ms // t_ovf = 1/16e6 * 256 * 1024 = 16 ms // // Shortened counting: // TCNT0 = 0, 128, 129, ...., 254, 255, 0, 128, .... // |---------------------------| // 8 ms // t_ovf = 1/16e6 * (256-128) * 1024 = 8 ms }
-
(Optional:) Use CTC (Clear Timer on Compare Match) mode of Timer0 and create a 1-millisecond time base with
TIMER0_COMPA_vect
interrupt vector. Toggle output pin using this interrupt and verify 1-millisecond duration by a logic analyzer.
-
In
timer.h
header file, complete macros for all three timers. -
Enhance the current application to control four LEDs in the Knight Rider style. Avoid using the delay library and instead, implement this functionality using a single Timer/Counter.
-
Use the ATmega328P datasheet (section 8-bit Timer/Counter0 with PWM > Modes of Operation) to find the main differences between:
- Normal mode,
- Clear Timer on Compare mode,
- Fast PWM mode, and
- Phase Correct PWM Mode.
-
Tomas Fryza. Schematic of Arduino Uno board
-
Microchip Technology Inc. ATmega328P datasheet
-
Renesas Electronics Corporation. Essentials of Microcontroller Use Learning about Peripherals: Interrupts
-
Tutorials Point. Embedded Systems - Interrupts
-
norwega. Knight Rider style chaser
-
StackOverflow. Static variables inside interrupts
-
Tutorials Point. Arduino - Pulse Width Modulation
-
Interrupts and 16-bit Timer/Counter 1: Atmel AVR Timers and Interrupts