Arduino Timer/Counter

There is no function in Arduino’s library to manipulate the Timer/Counter directly. After some searching on the Internet and reading the datasheet of ATmega 328/P I got some ideas on how to code in arduino’s IDE to talk to the timers.

Some codes first.

/*
 * Timer/Counter 1 normal mode demo
 * 
 * 这个版本使用中断服务程序 This version uses Interrupt Service Routine
 */

#define TCNT1_RLD 34286
typedef unsigned char uchar;

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);
  noInterrupts();
  TCCR1A = 0;
  TCCR1B = 0x04;   // normal mode, using clk_io/256, rate is 2Hz
  TCNT1 = TCNT1_RLD;
  TIMSK1 |= (1 << TOIE1);   // enable TC1 int
  interrupts();
}

ISR(TIMER1_OVF_vect) {
  TCNT1 = TCNT1_RLD;
  digitalWrite(LED_BUILTIN, ! digitalRead(LED_BUILTIN));
}

void loop() {
}

Generally speaking, to control Timer/Counter, you have to grab some knowledge of related registers. Timer/Counter 1 in ATmega 328/P has 5 modes of operation and diving into the description of its registers will be way too horrible. To be simple and clear, this post (and the codes pasted) merely deals with the Normal Mode, which is the simplest.

The main scheme

In Normal Mode, you set the timer and its value will increase automatically. When it overflows (ie the value of the timer reaches 65535 and roll over to 0), an interrupt will fire. A function known as Interrupt Service Routine (ISR) will be called “under your permit”.

The codes

Facing the registers

 TCCR1A = 0;
 TCCR1B = 0x04; // normal mode, using clk_io/256, rate is 2Hz
 TCNT1 = TCNT1_RLD;
 TIMSK1 |= (1 << TOIE1); // enable TC1 int

Thanks to avr-libc, we are able to manipulate the registers in our C/C++ sketch. As you see, TCCR1A and TCCR1B are TC1 Control Registers. TCNT1, TC1 Counter, is actually two 8-bit registers that stores the value of the timer. It is as is for convenience. TIMSK1 is the Timer/Counter 1 Interrupt Mask Register which you set its lowest bit TOIE1 to logic one to allow the call of ISR.

In Normal Mode, you don’t have to care every bit of these registers. Instead, watch CS1[2:0] in TCCR1B. Setting TOIE in TCCR1B (known as TOIE1 in the world of avr-libc) and do the right calculation to set TCNT1. Leave every other bit to be its value on reset (that is, 0) and everything will be fine. There is even a TCCR1C but it has nothing to do with Normal Mode, so just leave it there!

In order to prevent data corruption, a pair of noInterrupts()  and interrupts() is employed.

There is a problem still. If I have ever commented TCCR1A = 0 out, the led would stay on instead of blinking. It should not have been since the datasheet says that TCCR1A is cleared to zero upon resetting. I will update if one day I figure this out.

Interrupt Service Routine in Arduino/avr-libc

ISR(TIMER1_OVF_vect) {
 TCNT1 = TCNT1_RLD;
 digitalWrite(LED_BUILTIN, ! digitalRead(LED_BUILTIN));
}

Normally, you use the function attachInterrupt() to turn an ordinary function into an ISR. However, attachInterrupt() is intended for interrupts caused by external pins. It cannot handle “internal” interrupt source like timer-overflow.

In such case, you can use the ISR macro (yes this is a macro in avr-libc) to tell the compiler you are coding an Interrupt Service Routine. In the brackets of ISR is the interrupt vector. Of course you will be going to use the macro TIMER1_OVF_vect for convenience.  You can substitute _VECTOR(13) for TIMER1_OVF_vect as well (13 is the vector number of Timer/Counter1 Overflow), but it’s harder to read.

In the code above, the ISR reloads TCNT1 and toggles the state of the built-in LED on the Arduino board.

Keep in mind that ISR should be short. Because when in an ISR, all interrupts are masked by hardware unless you explicitly call interrupts() in the Arduino library or manipulate interrupt mask registers. Please note that some functions in the Arduino library do utilize Timer/Counter and interrupts to work. Therefore you are unable to use them in your ISR. Refer to this page.

How long should you wait before the interrupt fire?

This kind of calculation is extremely similar to what you do with the timers of MCS-51. Except that you now have an extra frequency divider, known as Timer/Counter 0, 1 Prescalers, to help you control the resolution of timer more accurately.

Most Arduino boards equip a 16 MHz oscillator. Let f_clk_IO = 16 MHz.

CS12, CS11, CS10, or CS1[2:0], are respectively bit 2, 1, 0 of the TCCR1A Register. Refer to the table below to understand what do they do.

CS1[2:0] Description f_clk @f_clk_IO=16MHz
0x00 No clock source (Timer/Counter stopped). 0
0x01 f_clk_IO /1 (No prescaling) 16MHz
0x02 f_clk_IO /8 (No prescaling) 2MHz
0x03 f_clk_IO /64 (No prescaling) 250kHz
0x04 f_clk_IO /256 (No prescaling) 62.5kHz
0x05 f_clk_IO /1024 (No prescaling) 15.625kH
0x06 External clock source on T1 pin. Clock on falling edge.
0x07 External clock source on T1 pin. Clock on rising edge.

The f_clk is the clock fed to Timer/Counter 1, whose value will increase by 1 in TCNT1 at each edge of f_clk (either rising or falling, I don’t care at the time being).

Suppose you want the LED to blink at the frequency of f_blink = 2Hz, you will need the clock to tick f_clk/f_blink times. That is 7812.5 times for f_clk=15.625kHz, 31250 times for f_clk=62.5kHz, 125000 times for f_clk=250kHz , 1000000 times for f_clk=2Mhz and 8000000 times for f_clk=16MHz. Since the MAX of TCNT1 is 65535, you may choose f_clk=15.625kHz or 62.5kHz. To be more accurate this example picks 62.5kHz. You may want to set an counter variable and try the 16MHz approach. It’s OK but not necessary in this case.

Therefore, TCNT1 is set to 65536-31250=34286 and CS1[2:0] 0x04. Please note that TCCR1B is set to 0x04 because Input Capture is not used in this example. If you don’t want to touch the higher bits of TCCR1B, you should write

TCCR1B |= 0x04;

Ending notes

Some people may not want to mess up with ISR but still have interrupt work in their programs. They will check the interrupt flag in interrupt flag registers and clear the flag themselves. If you are one of them, pay attention that you need to write logic one rather than zero to clear one particular interrupt flag in TC1 Interrupt Flag Register (TIFR1).

发表评论

电子邮件地址不会被公开。