Close

clock, delay function, timer interrupt, and PWM

A project log for ATTiny9 blinky

Simple project to demonstrate how to implement a blinking LED with an ATTiny and an Arduino as a programmer.

frank-bussFrank Buss 10/28/2017 at 04:283 Comments

The ATtiny9 has an internal oscillator of 8 MHz, which is enabled by default on reset, with a prescaler of 8, resulting in a CPU frequency of 1 MHz. The GNU compiler has a builtin delay function _delay_ms. To use this function, you have to define the CPU frequency with the F_CPU macro and then include util/delay.h. For 1 MHz it can delay up to 4.2 seconds.

Example for a 1 Hz blinker:

#define F_CPU 1000000UL
#include <avr/io.h>
#include <util/delay.h>

int main(void)
{
    DDRB |= 1 << PINB2;
    while (1) {
        PORTB &= ~(1 << PINB2);
        _delay_ms(500);
        PORTB |= 1 << PINB2;
        _delay_ms(500);
    }
}

 Internally it uses delay loops, so you can still do other things with the internal timers. 

The accuracy of the 8 MHz oscillator is guaranteed to be within +/-10% with factory calibration, says the full datasheet, see chapter 17.4.1. Accuracy of Calibrated Internal Oscillator. It can be calibrated to +/-1% manually with the OSCCAL value. Unlike with the PIC microcontrollers, looks like the IDE has no integrated support to do the calibration and to save it in a protected flash location, but there are instructions how to do it, like this one. When I tested it at 20°C room temperature and with 5V power supply, the factory calibration was already better than 1%.

The _delay_ms function uses a delay loop, which has some disadvantages, e.g. if you need an exact time interval, you have to adjust the delay value depending on the rest of the program. 

A better way to do the delay is by using a timer. The following program uses the 16 bit Timer0 and the capture compare functionality to generate an 1 kHz interrupt. The CPU clock is also changed to 8 MHz, so that you can do more things in the interrupt, if necessary:

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

volatile static uint16_t millis = 0;

// interrupt for compare match
SIGNAL (TIM0_COMPA_vect)
{
    if (millis < 500) {
        PORTB &= ~(1 << PINB2);
    } else {
        PORTB |= 1 << PINB2;
    }
    millis++;
    if (millis == 1000) millis = 0;
}

int main(void)
{
    // unprotect the CLKPSR register
    CCP = 0xd8;

    // set prescaler to 1 for 8 MHz CPU clock
    CLKPSR = 0;

    // configure pin B2 as output
    DDRB |= 1 << PINB2;
    
    // count up to 1000 for 1 kHz interrupts
    OCR0A = 1000;

    // clear timer on compare match (CTC) mode
    // clock source clk/8
    TCCR0A = 0;
    TCCR0B = (1 << WGM02) | (1 << CS01);

    // enable interrupt on OCR0A match
    TIMSK0 = 1 << OCF0A;

    // clear all interrupt flags
    TIFR0 = 0xff;

    // global interrupt enable
    sei();              
    
    // the rest is done in the interrupt
    while(1) {
    }
}

This needs about 3 mA (without the LED connected).

Finally you can use the PWM output for the blinking LED. For this connect it to PB0 and use this code:

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

int main(void)
{
    // unprotect the CLKPSR register
    CCP = 0xd8;

    // set prescaler to 1 for 8 MHz CPU clock
    CLKPSR = 0;

    // configure pin B0 as output
    DDRB |= 1 << DDB0;

    // 1 HZ PWM frequency (8 MHz / 1024 / 2 - 1)
    OCR0A = 3905;

    // toggle OC0A on compare match
    // clear timer on compare match (CTC) mode
    // clock source clk/1024
    TCCR0A = 1 << COM0A0;
    TCCR0B = (1 << WGM02) | (1 << CS00) | (1 << CS02);

    // go to sleep for the CPU
    set_sleep_mode(SLEEP_MODE_IDLE);
    sleep_enable();
    sleep_mode();
}

Enabling the sleep mode at the end reduces the power consumption to about 1.3 mA (without the LED).

With a different prescaler this could be used as a simple high resolution, fixed frequency square wave generator. For example 1 kHz:

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

int main(void)
{
    // unprotect the CLKPSR register
    CCP = 0xd8;

    // set prescaler to 1 for 8 MHz CPU clock
    CLKPSR = 0;

    // configure pin B0 as output
    DDRB |= 1 << DDB0;

    // 1 kHZ PWM frequency (8 MHz / 2 / 1000 - 1)
    OCR0A = 3999;

    // toggle OC0A on compare match
    // clear timer on compare match (CTC) mode
    // clock source clk/1 (no prescaler)
    TCCR0A = 1 << COM0A0;
    TCCR0B = (1 << WGM02) | (1 << CS00);

    // go to sleep for the CPU
    set_sleep_mode(SLEEP_MODE_IDLE);
    sleep_enable();
    sleep_mode();
}

Discussions

Frank Buss wrote 10/28/2017 at 10:58 point

Not in this program, because millis is only changed in the interrupt and there are no reentrant interrupts on the ATtiny. But in theory it might be possible to change it outside, so your variation would be a bit more safe. To be 100% safe, maybe it should be done like the Arduino does it, using a millis() function, because then you can only read it, and you could hide the millis variable with a static variable in an extra file, which makes it even impossible to use it from other files with "extern uint16_t millis". But this is just a hack, so no need for it :-)

  Are you sure? yes | no

davedarko wrote 10/28/2017 at 11:34 point

alright :) I only glanced at the code and overlooked the increment, but the millis == 1000 rang internal alarm bells ;) 

  Are you sure? yes | no

davedarko wrote 10/28/2017 at 07:56 point

isn't there a chance you run past 1000 in a loop? 

if (millis == 1000) millis = 0;

Just in case I'd rather write:

if (millis >= 1000) millis = 0;

  Are you sure? yes | no