Timer 2 Delay Library Feature
π Overview
An interface is required to handle program delays for time-sensitive operations such as AC voltage sensing and serial communication, among other tasks.
π βWhy not use the Arduino core functions
delay(),millis(), ormicros()to accomplish this?β
Although that might seem like the most straightforward approach, the ATMEGA328P Timer0 β used by these core functions β is also responsible for PWM generation in our hardware. This is described in detail in the PWM Inversion section.
If we attempt to use the core delay functions (which reconfigure Timer0 registers), while simultaneously using Timer0 to handle PWM interrupts, we risk introducing conflicting register states. This also translates to erratic behavior in our PWM output which although may not be catastrophic, it is certainly not desired π .
π βWhy not create a software-based delay program with an incrementer?β
unsigned long counter = 0;
while (counter < SOME_VALUE) {
counter++;
}
To implement delay2() and millis2(), itβs not practical to only rely on a simple software incrementer (e.g., counter++) inside a loop to create timing. This would be an approach that requires trial-and-error tuning of a maximum value to achieve a desired delay. In addition, this is inefficient, non-deterministic, and highly sensitive to compiler optimizations or clock speed changes.
π‘ The Workaround
To resolve this, we developed custom timing functions β delay2() and millis2() β based on Timer2 from the ATMEGA328P. These serve as effective substitutes for the Arduino core library timing functions, providing a millisecond-based timing mechanism that operates independently of Timer0.
β±οΈ Understanding the Timers on the ATMEGA328P
The ATMEGA328P includes three timers, each with different bit widths:
- Timer0: 8-bit
- Timer1: 16-bit
- Timer2: 8-bit
π βWhat do the βbitsβ actually mean?β
The number of bits determines how high a timer can count before it overflows and restarts.
Think of it like a clock:
- An 8-bit timer can count from
0to255($2^8 - 1$) - A 16-bit timer can count from
0to65,535($2^{16} - 1$)
A higher bit-width means higher resolution is possible β but for a simple 1ms tick, an 8-bit timer like Timer2 is more than sufficient.
π Header File Definitions
Implementation in Atinverter.h:
// Timer2 Increment
static volatile unsigned long timer2Millis;
// Methods
void initTimer2Delay();
void delay2(unsigned long ms);
unsigned long millis2();
π Method Descriptions
void initTimer2Delay()
Purpose: Initializes Timer 2 to generate an interrupt every 1 millisecond.
Pseudocode:
- Disable global interrupts
- Reset all Timer 2 related registers
- Set compare match value for 1ms at 16MHz with 64 prescaler (249)
- Set Clear Timer on Compare (CTC) mode
- Set pre-scaler to 64
- Enable interrupts on compare match
- Enable global interrupts
Implementation in Atinverter.cpp:
void Atinverter::initTimer2Delay() {
cli();
TCCR2A = 0;
TCCR2B = 0;
TCNT2 = 0;
OCR2A = 249;
TCCR2A |= (1 << WGM21);
TCCR2B |= (1 << CS22);
TIMSK2 = (1 << OCIE2A);
sei();
}
void delay2(unsigned long ms)
Purpose: Pauses execution for a specified duration in milliseconds.
Pseudocode:
- Record the current millisecond count from timer2Millis
- Continuously check if the elapsed time has reached the specified delay
- Exit once the condition is met
Implementation in Atinverter.cpp:
void Atinverter::delay2(unsigned long ms) {
unsigned long start = timer2Millis;
while ((timer2Millis - start) < ms) {
}
}
unsigned long millis2()
Purpose: Returns the number of milliseconds elapsed since Timer 2 was initialized.
Pseudocode:
- Return the current value of timer2Millis
Implementation in Atinverter.cpp:
unsigned long Atinverter::millis2() {
return timer2Millis;
}
βΉοΈ ISR Description
ISR(TIMER2_COMPA_vect)
Purpose: Interrupt Service Routine (ISR) for Timer 2 Compare Match A. It increments a global millisecond counter each time Timer 2 reaches its compare value (every 1ms).
Pseudocode:
- Increment the timer2Millis counter
Implementation in Atinverter.cpp:
ISR(TIMER2_COMPA_vect) {
Atinverter::timer2Millis++;
}
βοΈ Timer2 Delay Initialization
A thorough analysis on how to configure Timer 2 such that its interrupts fire every 1ms.
π βWhy trigger the interrupts every 1ms? Canβt you use another time frame?β
Yes, you can definitely use another time frame, but why would you reasonably do that? Consider these reasons why 1ms interrupts are most appropriate:
- βοΈ Arduinoβs built-in
millis()anddelay()functions are based on 1ms resolution. - π Simpler math and high enough resolution (1:1 mapping)
- 500 ticks $\rightarrow$ count to 500 ticks where each counter tick is 1ms instead of 2000 ticks $\rightarrow$ count to 2000 ticks where each tick is 0.25ms
- βοΈ Low CPU Overhead
- Operating the CPU at higher frequency can overwhelm the CPU and be more power intensive
Okay now that I hopefully convinced you of why we need 1ms interrupts, here is how we do it: As per the registers described in pages 127-134 of the ATMEGA328P datasheet, these are what we will need to configure. There are 9 registers in total, but we do not need to configure all of them.
The registers of interest and what is needed to do is as follows:
π§ TCCR2A β Timer/Counter Control Register A
Reset TCCR2A:
Avoids unexpected behavior from previously set states and allows for a known clean slate.
TCCR2A = 0; // Reset Timer2 TCCR2A control register
What It Controls:
- Compare Output Modes
COM2A[1:0], COM2B[1:0]β what happens to OC2A/OC2B (digital 11/3) pins on compare match with TCNT2 - Waveform Generation Mode
WGM2[1:0]β selects the timerβs counting behavior (Normal, PWM/Phase Correct, CTC, or Fast PWM)
What To Set:
- Clear Timer on Compare Match (CTC) mode using WGM21 bit
TCCR2A |= (1 << WGM21); // Set CTC mode
π§ TCCR2B β Timer/Counter Control Register B
Reset TCCR2B:
Avoids unexpected behavior from previously set states and allows for a known clean slate.
TCCR2B = 0; // Reset Timer2 TCCR2B control register
What It Controls:
- Force Output Compare
FOC2B, FOC2Aβ forcing a compare match on OC2B/OC2A (digital 3/11) pins - Waveform Generation Mode
WGM22β Used with TCCR2A for CTC or fast PWM - Clock Select
CS2[2:0]β Sets the pre-scaler for the timer clock source
What To Set:
- Prescaler of 64 using CS22 bit
TCCR2B |= (1 << CS22); // Set prescaler = 64
π§ TCNT2 β Timer/Counter Register
Reset TCNT2:
Starts counter at 0, why would you start anywhere else? π
TCNT2 = 0; // Reset Timer2 TCNT2 control register
What It Controls:
- Timer Counter 2
TCNT2[7:0]β The current value of Timer2βs counter (0-255)
What To Set:
- Nothing, the timer runs autonomously
π§ TIMSK2 β Timer/Counter2 Interrupt Mask Register
Reset TIMSK2:
Avoids unexpected behavior from previously set states and allows for a known clean slate.
TIMSK2 = 0; // Reset Timer2 TIMSK2 control register
What It Controls:
- Output Compare Match Interrupt Enables
OCIE2B, OCIE2Aβ Enables an interrupt when the timer valueTCNT2matches the value inOCR2BorOCR2A. - Waveform Generation Mode
TOIE2β Enables an interrupt when timer overflows
What To Set:
- Enable interrupt on Compare Match A using the OCIE2A bit.
TIMSK2 = (1 << OCIE2A); // enable compare interrupt
π§ OCR2A β Output Compare Register A
What It Controls:
- Holds the top value the TCNT2 counts up to in CTC mode.
- When
TCNT2 == OCR2A, an interrupt is fired (if enabled), and the counter resets to 0 on the next tick.
What To Set:
- OCR2A register to 249
OCR2A = 249;
Why Set it To 249:
- This value can be computed based on the equation provided in page 121 of the
ATMEGA328P datasheet.
(1) General CTC equation (when toggling pin):
\[f_{OCnx} = \frac{f_{clk_{I/O}}}{2 \times N \times (1 + OCRnx)}\]where:
- $OCRnx$ is the output compare register value
- $f_{OCnx}$ is the output frequency of the timer/counter
- $f_{clk_{I/O}}$ Clock input frequency (16 MHz for ATmega328P)
- N is the timer prescale factor (1, 8, 32, 64, 128, 256, or 1024)
- β2β in denominator only needed when toggling a pin to generate a waveform
πImportant Note: In interrupt-only CTC mode (no pin toggling), the β2β in the denominator is not included because the interrupt occurs once per timer cycle. Hence, this constant can be ignored.
(2) CTC Interrupt-Only Frequency Equation:
\[f_{OCnx} = \frac{f_{clk_{I/O}}}{ N \times (1 + OCRnx)}\]Now, we can solve for the OCR2A value that produces an interrupt at a frequency of 1kHz (1ms period) by restructuring this equation.
(3) CTC Interrupt-Only OCR2A Equation:
\[OCRnx = \frac{f_{clk_{I/O}}}{N \times f_{OCnx}} - 1\]π’ Solving for OCR2A:
\[OCRnx = \frac{16*10^6}{64 \times 1000} - 1 = 249\]β Result:
Setting OCR2A = 249 results in 1ms timer interrupts using a 16 MHz clock and a prescaler of 64.
And we are done! Those are all the registers you need to set. Now when you look at the Timer 2 register initialization, it should now make more sense π