3
\$\begingroup\$

I am trying to turn on a LED for 5sec every 12 hours. I tried:

#include <mega16.h>
#include <delay.h>

void main(void)
{
    DDRC.0 = 1;
    PORTC.0 = 0;

    while(1)
    {      
       unsigned long i;
       unsigned long j;
       PORTC.0=1;

       for(i=1; i<432; i++)
       {   
          delay_ms(100);  
       }

       PORTC.0 = 0;

       for(j=1; j<100; j++)
       {
           delay_ms(50);  
       }
    }
}

but the LED was on for about 2 hour every week.

\$\endgroup\$
  • 2
    \$\begingroup\$ You need to know the frequency of the chip, and the program has to be aware of it also. The F_CPU macro is for that purpose. \$\endgroup\$ – Dzarda Jul 1 '15 at 11:50
  • 1
    \$\begingroup\$ Step one is looking at the clock speed of your chip. If the delay routine assumes it's 8MHz and you run it at 1MHz or even lower, your delays will multiply by that factor difference \$\endgroup\$ – Asmyldof Jul 1 '15 at 11:50
  • 2
    \$\begingroup\$ Have a look at this. But with a Timer more accurate delays could be achieved. \$\endgroup\$ – Bence Kaulics Jul 1 '15 at 11:52
  • 2
    \$\begingroup\$ As written, your code will turn the LED on for 500 milliseconds every 43.2 seconds. \$\endgroup\$ – Nick Johnson Jul 1 '15 at 12:41
  • 1
    \$\begingroup\$ I actually found this an interesting and thought-provoking question, and I don't think it deserves to be downvoted, if only for having procured @BenceKaulics detailed answer \$\endgroup\$ – Al Longley Jul 5 '15 at 11:55
20
\$\begingroup\$

I felt the need to give a non-blocking solution to the problem especially because we are talking about twelve hours of delay here.

The util/delay.h library and its _delay_ms() and _delay_us() functions are the software delay functions. They are convenient in small programs and for quick prototyping and experimentation. They are simple and the accuracy could be enough in many cases, but we have a disadvantage here. As they are software delays, they block the CPU. Actually we use the CPU as a counter/timer so we cannot do anything else in the meantime.

With hardware timers, it is possible to count in the background and handle events with interrupts, thus the CPU will be free to use while the timer runs independently.

The "disadvantage" is that we have to go through a little bit more documentation and we have to write a little bit more code, but in return we will get higher accuracy and a free CPU.


Set up a Timer with interrupts

I am going to use the Timer1 of Atmega16A which is a 16 bit counter. The clock frequency is \$ f = \small 8\,MHz \$ and I want the timer to overflow once in every seconds and I will count these seconds until 12 hours (43200 seconds) is reached.

Steps:

  1. Determine the clock-speed and the maximum value of the timer to get an overflow in every seconds.
  2. Set the corresponding registers (Timer mode, Clock select, Enable interrupt, Compare value (max value))
  3. Write our Interrupt Service Routine (ISR) to count the seconds and in this case to control the LED.

Execution:

  1. We begin with: \$f_{SystemClock} = \small 8\,MHz \$, if we use this frequency with no prescale the timer will have a tick in every:

    \$ \large T_{timer} = \frac{1}{f_{SystemClock}} = \frac{1}{8\,MHz} = 0.125\,\mu s \$

    Unforunately it is too fast so we will prescale the clock frequency by 256.

    \$ \large f_{timer} = \frac{f_{SystemClock}}{256} = 31250\,Hz \$

    \$ \large T_{timer} = \frac{1}{f_{timer}} = \frac{1}{31.250\,kHz} = 32\,\mu s \$

    For an 1 second overflow \$ \sum_{ticks} = \large \frac{1\,s}{32\,\mu s} = \small 31250 \$ ticks are needed, (In this 1 s case the frequency clearly gives this value.)
    With a 16 bit timer the maximum possible value is: \$ 2^{16} - 1 = 65535 \$, so our value will fit, not like with \$8\,MHz\$ where \$ \small \sum_{ticks} \$ would be too high.

  2. As we have determined the above mentioned values, it is time to configure the timer. We will use the CTC (Clear Timer on Compare match) mode of the timer, which allows to set a custom maximum value.

    In CTC mode the counter is cleared to zero when the counter value (TCNT1) matches the OCR1A (WGM13:0 = 4).

    • OCR1A =\$ \small \sum_{ticks} - 1\$ = 31250 - 1 = 31249, as the counter starts from 0.
    • WGM13:0 = 4,\$ \Rightarrow\$ so only WGM12 bit has to be set in the TCCR1B control register

    Next step is to set the prescaler to 256 by the Clock-select bits in the TCCR1B register. Note: the counting will begin as soon as the clock source is selected.

    • CS12:0 = 4, \$ \Rightarrow\$ so only CS12 bit has to be set.

    Next; enable the corresponding Timer1 interrupt in the TIMSK (Timer/Counter Interrupt Mask Register) register, which will be triggered upon reaching the maximum value.

    enter image description here

    • We need OCIE1A bit (Timer/Counter1, Output Compare A Match Interrupt Enable), which is when written to 1 the Timer/Counter1 Output Compare A match interrupt is enabled. (In normal mode TOIE1 bit ( Timer/Counter1, Overflow Interrupt Enable) should be used)
  3. Last step is to write the code of the ISR and the timer initialization.

#include <avr/io.h>
#include <avr/interrupt.h>

#define LED     PC0

// global variable to count the seconds
volatile uint16_t sec_cnt = 0; 

// Timer1 initializtion
void init_timer1() 
{
    // Timer Mode 4: Clear Timer on Compare match (CTC)
    TCCR1B |= (1<<WGM12); 
    // Initialize Timer staring value
    TCNT1 = 0;
    // Set Compare value for 1s overflow
    OCR1A = 31249;
    // Enable Timer Compare A Match interrupt
    TIMSK |= (1<<OCIE1A);
    // Start Timer & Clock Select: Prescale I/O clock by 256
    TCCR1B |= (1<<CS12);
}

// Timer1 output compare match A interrupt rutine
ISR(TIMER1_COMPA_vect)
{   
    if(sec_cnt < 5)
    {
        PORTC |= (1<<LED);  // turn on LED in first 5 sec
    }
    else
    {
        PORTC &= ~(1<<LED);
    }

    sec_cnt++;

    if(sec_cnt == 43200)    // 12*60*60 = 43200
    {
        sec_cnt = 0;        // restart counting
    }
}

void main(void)
{
    // init Timer1
    init_timer1();
    // enable global interrupts
    sei();                  

    while(1)
    {
         // Let the ISR handle the LED ...
         // and do other stuff
    }
}

This is only one way of writing the ISR, other could be using a flag indicating a 12 hours period, then handle the LED in the main function when this flag is set. For the 5 seconds delay either the _delay_ms(5000) (as it is not that long) or a separate flag could be used. The interrupt vectors (e.g:TIMER1_COMPA_vect) are listed in the datasheet.


Testing

I was toggling a pin inside the ISR to measure the 1 second timing of the interrupts and I have turned on the LED for 5 sec in every 30 sec. The result is in the image below:

enter image description here

A little error (~0.9%) could be observed on the 1 second value which is caused by the internal oscillator's inaccuracy. It is normal, according to the datasheet:

At 5V, 25°C and 1.0, 2.0, 4.0 or 8.0MHz Oscillator frequency selected, this calibration gives a frequency within ± 3% of the nominal frequency.


\$\endgroup\$
  • \$\begingroup\$ Awesome!you are the true teacher.thank you so much \$\endgroup\$ – user78933 Jul 7 '15 at 17:05
  • 1
    \$\begingroup\$ You are right that 31250 ticks are needed. But the counter star at 0. So OCR1A = 31249 isn't it? \$\endgroup\$ – G36 Jan 2 '18 at 16:18
  • \$\begingroup\$ @G36 Yes, you are correct. Thanks for spotting it, I will fix it. \$\endgroup\$ – Bence Kaulics Jan 2 '18 at 16:24
1
\$\begingroup\$

First you need to handle the clock speed masquerade that's discussed in the comments. When you make sure your clocks are right, I'd probably refactor the code a bit:

int32_t second;
for (second = 0; second < 60LL*60*12; ++second) {
    if (second < 5) {
        PORTC.0 = 1;
    } else {
        PORTC.0 = 0;
    }
    delay_ms(1000);
}

But note that this approach will be slightly inaccurate, because your overall period is longer than 1 second. Execution of the code takes time, be it very short compared to the 1000ms wait.


Yes, I too hate this 12-hour blocking loop.

\$\endgroup\$
  • 6
    \$\begingroup\$ "I dont have time to test it" ?!?!?!? \$\endgroup\$ – JimmyB Jul 1 '15 at 12:13
  • 3
    \$\begingroup\$ @LordDecapitary Read the darn webpage posted in the comments. \$\endgroup\$ – Dzarda Jul 1 '15 at 12:18
  • 3
    \$\begingroup\$ I hate that this is a 12 hour blocking function, but should do what the OP wants, unless the micro needs to do anything else in the idle time. \$\endgroup\$ – Matt Young Jul 1 '15 at 12:23
  • 1
    \$\begingroup\$ @LordDecapitary You get a 50 second delay because you STILL apparently haven't read the delay_ms documentation. \$\endgroup\$ – Matt Young Jul 1 '15 at 12:48
  • 3
    \$\begingroup\$ @LordDecapitary Stay with mechanical engineering. Peach engineering apparently isn't for you. \$\endgroup\$ – Dzarda Jul 1 '15 at 12:52
0
\$\begingroup\$
unsigned int seconds,mins,hours;

for(hours=0;hours<12;hours++)
{
    for(mins=0;mins<60;mins++)
    {
       for(seconds=0;seconds<60;seconds++)
       {
          delay_ms(1000);
       }
    }
}

led = 1;
delay_ms(5000);
led = 0;
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy