Learning timer interrupts to produce a stable frequency on ATtiny85-20PU - edited title

Hello!
I've been working on a program for an ATTINY85 that supplies a 20kHz pulse through the main loop by toggling a pin's state at a predefined time interval. That part works well.

This program also contains a modified SPI routine that was inspired by one of nickgammon's old posts (thank you for being awesome!). This is for sending two bytes over to a pair of series shift registers that controls 4 segment displays.

When the program is busy sending over the serial bits it holds up the pulse routine in the main loop causing my 20kHz signal to go down to ~6.5kHz.

So my question is, what is the best way for my code to pause what's it's doing in the SPI routine, toggle the pulse pin on time and then continue the SPI routine where it left off.

I can probably get around this by updating the timer variable and copying the if statement & toggle routine in between each command outside of the main loop but find this "are we there yet" method rather counterintuitive.

Figured I should post a topic as google tends to be a gauntlet of misdirection.

Thanks, everyone!

when you call a routine from a routine then one will paused and control goes to called one.

Can't you program Timer1 to produce a 20 kHz signal on the OC1A or OC1B pins? That will keep time regardless of what the software is doing.

johnwasser,
First off thanks for chiming in. You've helped me a couple of times over the years posting here.

2nd, you tell me? lol
Can I set a pin as a stable clock source all the while running other code simply by using Timer1?
Right now it simply toggles a pin's state by updating a time variable in the main loop.

Also, would I be able to count pulses from the simulated clock source if implemented through timer1? That is something I cannot lose for this project.

You can enable one of the timer interrupts and use that to count cycles.

Okay thank you! That gives me enough direction to refine my searches.
Very much appreciated.

I found references to the compare match function for timer0 and read through the datasheet however the formula on page 72 was not providing predicable results - I must be missing something. I did adjust the prescaler and play with the OCR0A value and was able to get close to my target frequency.

Now I have a stable clock source while running the software SPI with no problem.

Thanks again!

Warning: I expect that Timer0 is used by the Arduino code to maintain the millis() and micros() timers. If you muck with it then any Arduino code, like libraries, that use millis() or micros() for timing will not work properly. That is why I recommended using Timer1.

I appreciate the warning. I did consider that implication but changing the clock to a compare match routine I no longer need to track time - which was my previous method of toggling a pin state for a specified frequency.

Here's a simple version of my code - leaving out the parts for the intended circuit. This outputs a ~10kHz signal on PB0 while controlling shift registers (in series) to drive 4 common anode segment displays. I've included an interrupt to count the compare match actuations - which will be for the intended use of this project.
I know it's not helpful without the schematic of what's happening on the breadboard but figured it would help others searching the web.

This is my first time playing with the registers as well as utilizing interrupts. So far I'm quite impressed with what this ATTINY85 can do.

int clickCounter = 0;
byte digitIndex[4];
byte segmentMap[10]
{
  0x88, // 0
  0xEB, // 1
  0x4C, // 2
  0x49, // 3
  0x2B, // 4
  0x19, // 5
  0x18, // 6
  0xCB, // 7
  0x08, // 8
  0x0B  // 9
};
byte commonMap[4]
{
  0xF7, // digit 1
  0xFB, // digit 2
  0xFD, // digit 3
  0xFE  // digit 4
};

byte sendData(const byte data)
{
  USIDR = data; // byte to the USI Data Register
  for (byte b = 0; b < 8; b++)
  {
    USICR = (1 << USIWM0) | (1 << USICS1) | (1 << USITC);
    USICR = (1 << USIWM0) | (1 << USICS1) | (1 << USITC) | (1 << USICLK);
  }
  return USIDR;
}

void setup()
{
  noInterrupts();
  DDRB |= (1 << PB0); // Clock pin as output
  DDRB |= (1 << PB1);  // Data Out pin as output
  DDRB |= (1 << PB2); // SCK pin as output
  DDRB |= (1 << PB4); // Latch pin as output

  PORTB |= (1 << PB4); // PB4 HIGH

  USICR = (1 << USIWM0);

  TCCR0A = 0;
  TCCR0B = 0;
  TCNT0 = 0;
  TCCR0A |= (1 << COM0A0);
  TCCR0B |= (1 << CS02); // prescaler of 256 - page 80, table 11-6
  TCCR0A |= (1 << WGM01);
  OCR0A = 2;

  TIMSK |= (1 << OCIE0A);
  interrupts();

}

ISR (TIMER0_COMPA_vect)
{
  clickCounter++;
}

void loop()
{
  int i = 1234; // just a number to display
  
  // multiplexing segment displays
  for (byte b = 0; b < 4; b++)
  {
    digitIndex[b] = i % 10;
    i = i / 10;
    sendData(commonMap[b]);
    sendData(segmentMap[digitIndex[b]]);
    PINB = PINB | (1 << PB4);
    PINB = PINB | (1 << PB4);
  }
}

did you undestand: using timer0 for your own pruposes will DISable the function of millis() and micros()

There a lot of libraries that make use of millis() or micros() inside the library and these libraies would malfunction or not work anymore. What would be the problem to simply use timer1 or timer2??

StefanL38, yes... as John had already implied.
You may also see in my code that I'm not using any libraries - let alone anything that uses timer0 outside of the compare & match.
If I'm mistaken please let me know. I am still learning and may be unaware of things - I'm still confused on why the formula on page 72 of the datasheet isn't producing the expecting frequency on PB0 when I use the suggested values & prescaler.
In effort to better understanding choosing prescalers & a value for OCR0A I had searched around and found an answer by nickgammon where he provided this formula;
MCUfreq / prescaler / OCR0A / 2 = desired frequency. Or as he wrote it on this page; "8000000 / 1024 / 128 / 2 = 30.5"

So in that respect, at 16MHz with a prescaler of 256 and OCR0A set to 2
16000000 / 256 / 2 / 2 = 15,265.
I'm getting ~10.6kHz and do not know why.
Am I including a function in my code that is also using timer0?
I suppose I could try to switch it to timer1 and see what happens.

Also, there is no timer2 on the ATtiny85. But if you look at the datasheet you may see that timer1 is a bit different than timer0. I suppose that's why I didn't think to change it after finding a solution.

here is a demo-code that uses timer2 on unos
I don't know if the atTiny85-timer1 uses the same control-registers as an an Uno Timer1
anyway here it is
It calculates the register settings for a given frequency

// this demo-code belongs to the public domain
// this code demonstrates how to blink the  onboard-LED
// of an Arduino-Uno (connected to IO-pin 13)
// in a nonblocking way using timing based on function millis()
// and creating a pwm-signal "in the backround"
// through setting up a timer-interrupt through configuring timer2

// A T T E N T I O N !    R E M A R K
// some other libraries that make use of timer2 may conflict with this


// start of macros dbg and dbgi
#define dbg(myFixedText, variableName) \
  Serial.print( F(#myFixedText " "  #variableName"=") ); \
  Serial.println(variableName);
// usage: dbg("1:my fixed text",myVariable);
// myVariable can be any variable or expression that is defined in scope

#define dbgi(myFixedText, variableName,timeInterval) \
  do { \
    static unsigned long intervalStartTime; \
    if ( millis() - intervalStartTime >= timeInterval ){ \
      intervalStartTime = millis(); \
      Serial.print( F(#myFixedText " "  #variableName"=") ); \
      Serial.println(variableName); \
    } \
  } while (false);
// usage: dbgi("2:my fixed text",myVariable,1000);
// myVariable can be any variable or expression that is defined in scope
// third parameter is the time in milliseconds that must pass by until the next time a
// Serial.print is executed
// end of macros dbg and dbgi

void PrintFileNameDateTime() {
  Serial.println( F("Code running comes from file ") );
  Serial.println( F(__FILE__));
  Serial.print( F("  compiled ") );
  Serial.print(F(__DATE__));
  Serial.print( F(" ") );
  Serial.println(F(__TIME__));
}


boolean TimePeriodIsOver (unsigned long &periodStartTime, unsigned long TimePeriod) {
  unsigned long currentMillis  = millis();
  if ( currentMillis - periodStartTime >= TimePeriod )
  {
    periodStartTime = currentMillis; // set new expireTime
    return true;                // more time than TimePeriod) has elapsed since last time if-condition was true
  }
  else return false;            // not expired
}

unsigned long MyTestTimer =  0;                   // variables MUST be of type unsigned long
const byte    OnBoard_LED = 13;


void BlinkHeartBeatLED(int IO_Pin, int BlinkPeriod) {
  static unsigned long MyBlinkTimer;
  pinMode(IO_Pin, OUTPUT);

  if ( TimePeriodIsOver(MyBlinkTimer, BlinkPeriod) ) {
    digitalWrite(IO_Pin, !digitalRead(IO_Pin) );
  }
}


const unsigned long pwmBaseFrequency = 16800;
const unsigned long pulseFreq = 280;

volatile unsigned long pwmCounter   = 0;
volatile unsigned long periodCount  = pwmBaseFrequency / pulseFreq;
volatile unsigned long duty80    = (periodCount * 4) / 5;
volatile unsigned long duty25    = (periodCount * 1) / 4;
volatile unsigned long duty50    = (periodCount * 1) / 2 ;


void setupTimerInterrupt(unsigned long ISR_call_frequency) {
  const byte Prescaler___8 = (1 << CS21);
  const byte Prescaler__32 = (1 << CS21) + (1 << CS20);
  const byte Prescaler__64 = (1 << CS22);
  const byte Prescaler_128 = (1 << CS22) + (1 << CS20);
  const byte Prescaler_256 = (1 << CS22) + (1 << CS21);
  const byte Prescaler1024 = (1 << CS22) + (1 << CS21) + (1 << CS20);

  const unsigned long CPU_Clock = 16000000;
  const byte toggleFactor = 1;

  unsigned long OCR2A_value;

  cli();//stop interrupts

  TCCR2A = 0;// set entire TCCR2A register to 0
  TCCR2B = 0;// same for TCCR2B
  TCNT2  = 0;//initialize counter value to 0

  TCCR2A |= (1 << WGM21); // turn on CTC mode
  TIMSK2 |= (1 << OCIE2A); // enable timer compare interrupt

  // the prescaler must be setup to a value that the calculation
  // of the value for OCR2A is below 256
  TCCR2B = Prescaler___8;
  OCR2A_value = (CPU_Clock / ( 8 * ISR_call_frequency * toggleFactor) )  - 1;
  dbg("1 setup: timer ", OCR2A_value);

  if (OCR2A_value > 256) {  // if value too big
    TCCR2B = Prescaler__32; // set higher prescaler
    OCR2A_value = (CPU_Clock / ( 32 * ISR_call_frequency * toggleFactor) )  - 1;
    dbg("setup: prescaler 32", OCR2A_value);
  }

  if (OCR2A_value > 256) { // if value too big
    TCCR2B = Prescaler__64;// set higher prescaler
    OCR2A_value = (CPU_Clock / ( 64 * ISR_call_frequency * toggleFactor) )  - 1;
    dbg("setup: prescaler 64", OCR2A_value);
  }

  if (OCR2A_value > 256) { // if value too big
    TCCR2B = Prescaler_128;// set higher prescaler
    OCR2A_value = (CPU_Clock / ( 128 * ISR_call_frequency * toggleFactor) )  - 1;
    dbg("setup: prescaler 128", OCR2A_value);
  }

  if (OCR2A_value > 256) {  // if value too big
    TCCR2B = Prescaler_256; // set higher prescaler
    OCR2A_value = (CPU_Clock / ( 256 * ISR_call_frequency * toggleFactor) )  - 1;
    dbg("setup: prescaler 256", OCR2A_value);
  }

  if (OCR2A_value > 256) {   // if value too big
    TCCR2B = Prescaler1024;  // set higher prescaler
    OCR2A_value = (CPU_Clock / ( 1024 * ISR_call_frequency * toggleFactor) )  - 1;
    dbg("setup: prescaler 1024", OCR2A_value);
  }

  OCR2A = OCR2A_value; // finally set the value of OCR2A

  sei();//allow interrupts
  dbg("setup: timer done", OCR2A_value);

  dbg("setup",periodCount);
  dbg("setup",duty50);
  dbg("setup",duty80);
  dbg("setup",duty25);
}

unsigned long pwmCount = 0;
const byte PWM_Pin     = 4;
const byte Clock_Pin   = 5;


void setup() {
  Serial.begin(115200);
  Serial.println( F("Setup-Start") );
  PrintFileNameDateTime();
  pinMode(PWM_Pin, OUTPUT);
  pinMode(Clock_Pin, OUTPUT);

  setupTimerInterrupt(pwmBaseFrequency);
  pwmCount = 0;
}

const byte sc_50percentDuty = 1;
const byte sc_80percentDuty = 2;
const byte sc_25percentDuty = 3;

byte dutyState = sc_50percentDuty;
byte pulseCount = 0;
volatile byte pwmState;

void flap() {
  pwmCount++;

  digitalWrite(Clock_Pin, !digitalRead(Clock_Pin) );

  if (pwmCount == periodCount) {
    pwmCount = 0;
    digitalWrite(PWM_Pin, !digitalRead(PWM_Pin) );
  }

  switch (dutyState) {

    case sc_50percentDuty:

      if (pwmCount == duty50) { // if end of HIGH-time reached
        digitalWrite(PWM_Pin, LOW); // set LOW
        pulseCount++;

        if (pulseCount == 11) {
          pulseCount = 0;
          dutyState = sc_80percentDuty;
        }
      }
      break;

    case sc_80percentDuty:

      if (pwmCount == duty80) { // if end of HIGH-time reached
        digitalWrite(PWM_Pin, LOW); // set LOW
        pulseCount++;

        if (pulseCount == 11) {
          pulseCount = 0;
          dutyState = sc_25percentDuty;
        }
      }
      break;

    case sc_25percentDuty:

      if (pwmCount == duty25) { // if end of HIGH-time reached
        digitalWrite(PWM_Pin, LOW); // set LOW
        pulseCount++;

        if (pulseCount == 10) {
          pulseCount = 0;
          dutyState = sc_50percentDuty;
        }
      }
      break;
  }
}


ISR(TIMER2_COMPA_vect) {
  flap();
}


void loop() {
  BlinkHeartBeatLED(OnBoard_LED, 200);

}

Why does your example seem overcomplicated?
I appreciate it anyways. I'll scan through it later - however at a quick glance it doesn't help my current situation at all.

I've updated the title of my thread according to the direction things are going.

it uses a timer-interrupt. it is setting up a timer-interrupt and with calling the isr at a frequency of 40 kHz and toggling an IO-pin would produce a 20 kHz clock-signal

As long as you don't disable interrupts the ISR will be called regardless of what the rest of the code is doing

Note: If you set TOP to 2, the frequency is divided by 3 since the counter goes 0, 1, 2, 0, 1, 2...

16 MHz / 256 (precale) / 2 (phase correct) / 3 (TOP+1) = 10416.666 Hz, very near 10.6 kHz.

Ah you're totally right, John. I forgot about the +1 for the OCRA0 value when using the formula - it was a late night.
Back at it looking at my scope & frequency counter(multimeter) now, I'm actually getting 10.85kHz.
I would assume it's simply a matter of a poorly tuned internal oscillator but when I was using the time data within the loop (before using compare match) I was getting more accurate results. So I'm wondering if something else is going on that is causing the discrepancy.

Isn't the internal oscillator 8 MHz, like on an ATmega328P?

Section 6.2.2 of the datasheet

High Frequency PLL Clock
There is an internal PLL that provides nominally 64 MHz clock rate locked to the RC Oscillator for the use of the Peripheral Timer/Counter1 and for the system clock source. When selected as a system clock source, by programming the CKSEL fuses to ‘0001’, it is divided by four like shown in Table 6-2

I have the option of using a 1MHz, 8MHz or 16MHz internal oscillator. Out of curiousity I did burn the bootloader at 8MHz and it did as expected - halfing the frequency.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.