Arduino and Timer&Interrupts

Before entering the logic of timer interrupts; The processors used on almost all ready-made boards collected under the name Arduino are avr-8-bit or 32-bit processors owned by atmega. Before I mention using timer on these processors, I would like to point out that all processors with this architecture can make similar adjustments.

What is Timer?

Timer is the structure that helps us to realize the events that we want to do at certain time intervals. Reminds the processor what to do when that time frame comes. This can be a specific feature that is supported by a series of your own commands or timers. For example, assuming that you are not using a timer when you want to burn an output-connected LEDat certain time intervals, which is a classic example, you can continuously check the time taken to perform this operation, or you can block the processor at that moment with commands that should be avoided, such as delay. Instead of doing these things, these structures that exist in the processor give us the time control we need. In doing so, the continuation of other parallel operations in the processor gives us the ability to multi-process.  

How Does Timer Work?

Timers periodically increase the counter register variable known as the counter register. This register can reach a certain value. The factor that determines this value is how many bits the timer we use. They usually appear as 8 bits or 16 bits. When this register reaches its maximum value (if it is 8 bits, this value starts at 255 because it starts at 0), it is reset to zero and a flag bit is set that the overflow is happening. Interrupt Service Routine (ISR) performs this process for us without us having to follow this bit, allowing us to be aware of the interrupt. ISR provides all the controls required for this flag bit. Well, we said timer increases the counter variable periodically. How does this period refer and increase? Timer needs a clock signal to work. Increases this variable one each time this signal occurs. This signal can be external or is available internally on most processors. Under normal circumstances, if you provide a 16MHz clock signal to a timer, the period for this timer will be : T = 1 / f ( Frequency Hz ) T= 1 / ( 16* 10^6 Hz ) T = 62.5 ns If it were 32Mhz, this value would be 31.25 ns. ( 1 sec = 10^9 ns) Above, we talked about the general working logic of timers. 8-bit AVR processors have multiple timers. Each timer is set by default for use in different operations for arduino. With unknowing changes we can make through timers, we may encounter unexpected results in time-dependent functions, or our codes may not work properly with conflicting libraries. (Engine, delay, millis, etc.)

Default Timer Settings

Timer0 – is an 8-bit timer. The Counter variable gets a maximum value of 255. The basic functions required for arduino, such as delay() and millis(), depend on this timer. It's best not to make changes unless you know what you're doing on this timer.

Timer1 – a 16-bit timer. The Counter variable gets a maximum value of 65535. The Servo library uses this timer.

Timer2 – is an 8-bit timer. The Counter variable gets a maximum value of 255.   The tone() function uses this timer.

The processor you are using may also have different timers such as Timern, Timern+1, Timern+2. You can access the necessary information through their pdfs as I mentioned in advance. In general, all of our adjustments will apply to each other. Setting Timer Summers There are registers for which to store the necessary settings for timers. You will soon have a better understanding of how to make the adjustments we mentioned above on these examples. We use two of these registers when setting timer settings. These are TCCRxA and TCCRxB. Each register stores 8-bit data. Here you can type the timer number that you want to set instead of x. The following examples include these registers for Timer1. When we set the period of timer interrupt, we provide the necessary clock signal with combinations of bits 0 1 2. For example, if we want to run Timer1 at 2MHz clock speed. So if we set the time for each count increase to be 0.5us, we need to write a block of code as follows.

void setup()
{
  pinMode(PIN, OUTPUT);
  Setting Timer1
  cli();         all interrupts are non-giant
  TCCR1A = 0;    resets the entire TCCR1A register
  TCCR1B = 0;    resets the entire TCCR1B register
 
opens overflow interrupt for timer1
  TIMSK1 |= (1 < TOIE1); //Kullandığınız işlemcide bu timer yoksa farklı bir timer ilede aynı ayarları set edebilirsiniz
 
  // CS11 bitini atadık vede timer şuanda clock hızında çalışacaktır.
  TCCR1B |= (1 < CS11); //  böleni 8 olarak ayarladık. So 16/8 = 2 MHZ
 
sei();//Run all interrupts.
 
}
 
void loop()
{
 
}

Overflow Interrupt

In our adjustment here, we did not specify what kind of interrupt timer would use. The Timer/Counter1 Interrupt Mask Register timsk1 provides control of this. By assigning TOIE1 to this register, we set the timer to use overflow interrupt. A little later, I will also mention a different type of cutting, CTC (Clear Timer on Compare Match). If we continue where we left off now, isr (TIMER1_OVF_vect) is called when overflow occurs;

void setup()
{
  pinMode(PIN, OUTPUT);
  Setting Timer1
  cli();         all interrupts are non-giant
  TCCR1A = 0;    resets the entire TCCR1A register
  TCCR1B = 0;    resets the entire TCCR1B register
 
opens overflow interrupt for timer1
  TIMSK1 |= (1 < TOIE1); //Kullandığınız işlemcide bu timer yoksa farklı bir timer ilede aynı ayarları set edebilirsiniz
 
  // CS11 bitini atadık vede timer şuanda clock hızında çalışacaktır.
  TCCR1B |= (1 < CS11); //  böleni 8 olarak ayarladık. So 16/8 = 2 MHZ
  TIMSK1 | = (1) < TOIE1); // Overflow kesmesini açarız
  sei();//Tüm interruptları çalıştır.
 
}
 
void loop()
{
 
}
ISR(TIMER1_OVF_vect)
{
 
digitalWrite(PIN, !digitalRead(PIN));
 
}

In the section up to this, we've created the cut definition. If we always cut the frequency:

  • We set our Clock signal using 8 segments (CS11), which means we got a 2MHz clock signal
  • The time it takes to increase the value of a count is 0.5us.
  • Counter increases to 0 – 65535 and increases the value by 65536.
  • T = 65536*0.5 =32.77ms
  • f=1/T = 30.51 Hz

So our ISR function will be activated approximately 30.51 times per second. One of the drawbacks of overflow cutting is that it is difficult to fully adjust the frequency values we want. Because each time the counter is increased to the end and the cutting is expected to occur. Instead, I'd like to talk about CTC mode, the mode where we can make interrupts occur after timer reaches a value that we set so that we can make more precise frequency plays.  

Clear Timer on Compare Match Cutting ( CTC )

Instead of waiting for timer's counter to overflow, it is a break that occurs when it equals a count value that we have already set. Here is register ACRxA, to which we can assign this value. Here x varies according to the timer we choose. At the same time, the maximum value we will assign depends on how many bits this selected timer is. Again, if we use timer1 as an example, the values we will assign should be values less than unsigned 65536. If we use timer0, this value is less than 256. Another advantage of using this mode is that if we use default prescale (64 segments) when using timer0, other basic functions will continue to work correctly as there is no change in the overflow interrupt. So we can also use the timer0 for cuttings with this mode. However, due to the fact that it is 8 bits, the frequency ranges that we can adapt will be limited. Here I want to give you a formul where you can get the frequency you want. With this formul, you can easily calculate the number of steps required to reach the desired frequency. The frequency value in the formula is the frequency we want to achieve. Dividing is the value at which we set the clock time of the processor that we mentioned earlier. The reason we're removing 1 is because it starts at 0. Now let's create a 16KHz cutting frequency with an example.

void setup()
{
  pinMode(PIN, OUTPUT);
  Setting Timer1
  cli();         all interrupts are non-giant
  TCCR1A = 0;    resets the entire TCCR1A register
  TCCR1B = 0;    resets the entire TCCR1B register
 
TCCR1A |= (1 < WGM21); // CTC modunu aktif hale getirir
  // CS11 bitini atadık vede timer şuanda clock hızında çalışacaktır.
  OCR2A = 124;// ((16*10^6)/(16000*8))-1 Provides us with a cutting frequency of 16KHz
  TCCR1B |= (1 < CS11); //  böleni 8 olarak ayarladık. So 16/8 = 2 MHZ clock signal
  TIMSK1 |= ( 1 < OCIE1A); // Timer Compare kesmesini aktif hale getir.
  sei();//Run all interrupts.
 
}
 
void loop()
{
 
}
ISR(TIMER1_COMPA_vect) // has a frequency of 16 KHz.
{
 
digitalWrite(PIN, !digitalRead(PIN)); Since the on/off 2 cut-off holds, the value you will measure at the output will be 8KHz.
 
}

An important point to consider when using interrupts and variables is the volatile value. With this definition, we ensure that different interrupts in variables perform sequential operations on the variable and make a more stable reliable storage. At very high frequency values, this statement is not sufficient and can be applied to disable cutting and read and reopen the variable.