Tuning ATtiny internal oscillator

square wavesThe internal oscillator of an ATtiny can be inaccurate and might require tuning.

This post discusses how to do that if you have access to an oscilloscope or a frequency counter.

An accurate clock speed is important if you are doing timing critical operations such as serial UART communication.

The internal oscillator

ATtiny microprocessors can use an internal RC oscillator or an external crystal oscillator. External crystal oscillators are more accurate, but require two pins. For these low pin count devices it can be beneficial to use the internal oscillator.

Atmel claim the internal oscillator is factory calibrated to +/-10% at 25 degrees centigrade and three volts. In my experience they are actually calibrated to +/- 1%.

Higher temperatures will increase the clock rate and higher voltages will decrease the clock rate. My ATtiny85 microcontrollers run about 1.1% slower at 5V.

For serial UART you’ll probably need to be within 5% (less than half a bit width drift over one start and 8 data bits), so even a factory calibrated ATtiny running at 9600 baud will work fine even at 5V. However there are times when you might want the internal oscillator more finely tuned.

The oscillator calibration register, OSCCAL

Tuning of the internal oscillator is controlled by the oscillator calibration register, OSCCAL. When the microcontroller starts the factory calibration value is automatically loaded into the OSCCAL register. You cannot modify the factory calibration value, but your program can change the OSCCAL register at runtime.

So tuning the oscillator is as simple as adjusting OSCCAL at the beginning of your program. For example I added the following to my code for an ATtiny85 running at 5V.

OSCCAL += 3;

Using a delta adjustment of the factory calibrated value can be better than baking in the actual OSCCAL value if you don’t want to tune each chip individually.

For example I know that Atmel did a good job calibrating the oscillator at 3V, but I will be running at 5V so the oscillator will run slightly slower at that voltage. If adding 3 to OSCCAL works for one chip, it will probably work reasonably well for every chip.

Using EEPROM

For most hobbyists it is sufficient to add an OSCCAL adjustment to our code. Even if you have to change that line of code from chip to chip, you are only working with a handful of chips.

However this would be impractical in a production environment where we could be working with hundreds or thousands of chips.

For a production run, we would want to write the value into EEPROM during calibration and copy it from EEPROM to the OSCCAL register at the beginning of the program.

In Arduino we can use the EEPROM library:

#include <EEPROM.h>
OSCCAL = EEPROM.read(0);

In avr-libc we can use the avr/eeprom.h routines:

#include <avr/eeprom.h>
OSCCAL = eeprom_read_byte((uint8_t*)0x00);

Measuring oscillator accuracy

XMEGA XprotolabThere are approaches that involve comparing the clock speed to an external clock source, but if you have access to an oscilloscope or a frequency counter then its simpler to just measure the clock speed directly.

I have a tiny 1″ $49 Gabotronic XMEGA Xprotolab oscilloscope that does the job nicely.

So then we just need to generate a specific frequency on one of the pins, measure it, adjust OSCCAL up or down accordingly and try again.

Using the CKOUT fuse

If you are comfortable setting fuses, then the simplest approach is to set the CKOUT fuse (bit 6 of the LOW fuse register) which will send the clock signal to PB4. Then you can use your oscilloscope or frequency counter to see how close you are to the selected frequency.

I use Atmel Studio and an Atmel-ICE Programmer/Debugger to set this fuse.

Atmel Stdion CKOUT fuse

WARNING: You can “brick” your ATtiny by setting the fuses incorrectly, for example by selecting an external crystal when you don’t have one connected. In the worst case you could make it impossible to reprogram your ATtiny without a special high voltage programmer.

A safer approach

If you don’t want to mess with fuses, then an alternative is to load a small program that oscillates a pin at a measureable frequency. I like to use 10Khz as it’s a nice round number and fast enough for calibration.

We can do this with a PWM output or with a Timer/Counter compare match. The PWM approach is slightly less code, but I am going to use the Timer/Counter approach as I think the code is slightly easier to understand.

The following program uses Timer/Counter0 Comparator A to generate a 10KHz 50% load on pin PB4 for an ATtiny25/45/85. It generates 10KHz whether the ATtiny is running directly off the internal oscillator at 8Mhz or is running at 1MHz because the CKDIV8 fuse is set.

Using Arduino:

// Timer/Counter0 Compare Match A interrupt handler 
ISR (TIMER0_COMPA_vect) {
   PORTB ^= 1 << PINB4;        // Invert pin PB4
}
 
void setup() {
    OSCCAL += 3;                // User calibration
    pinMode(4,OUTPUT);          // Set PB4 to output
    TCNT0 = 0;                  // Count up from 0
    TCCR0A = 2 << WGM00;        // CTC mode
    if (CLKPR == 3)             // If clock set to 1MHz
        TCCR0B = (1<<CS00);     // Set prescaler to /1 (1uS at 1Mhz)
    else                        // Otherwise clock set to 8MHz
        TCCR0B = (2<<CS00);     // Set prescaler to /8 (1uS at 8Mhz)
    GTCCR |= 1 << PSR0;         // Reset prescaler
    OCR0A = 49;                 // 49 + 1 = 50 microseconds (10KHz)
    TIFR = 1 << OCF0A;          // Clear output compare interrupt flag
    TIMSK |= 1 << OCIE0A;       // Enable output compare interrupt
}
 
void loop() {}

Using avr-libc:

 #include <avr/interrupt.h>
 
 // Timer/Counter0 Comparator A interrupt vector
 ISR (TIMER0_COMPA_vect) {
     PORTB ^= 1<< PINB4;         // Invert pin PB4
 }
 
 int main(void)
 {
     OSCCAL += 3;                // User calibration
     DDRB = 1 << PINB4;          // Set PB4 to output
     TCNT0 = 0;                  // Count up from 0
     TCCR0A = 2<<WGM00;          // CTC mode
     if (CLKPR == 3)             // If clock set to 1MHz
         TCCR0B = (1<<CS00);     // Set prescaler to /1 (1uS at 1Mhz)
     else                        // Otherwise clock set to 8MHz
         TCCR0B = (2<<CS00);     // Set prescaler to /8 (1uS at 8Mhz)
     GTCCR |= 1 << PSR0;         // Reset prescaler
     OCR0A = 49;                 // 49 + 1 = 50 microseconds (10KHz)
     TIFR = 1 << OCF0A;          // Clear output compare interrupt flag
     TIMSK |= 1<<OCIE0A;         // Enable output compare interrupt
     sei();                      // Enable global interrupts
     
     while (1) {}
}