Question about Atomic Operation in relation to A timer Overflow Interrupt

I am using an ATTiny13 to read an incoming signal to then modulate a solenoid valve that works on a 29Hz pulse frequency with a variable pulse-width from 0% (=open) to 100% (=closed). Because the T13 has only a 8 bit timer (TCNT0), I extend the count of the 8-bit timer by counting overflow interrupts.

ISR(TIM0_OVF_vect) { //interrupt service routine when timer 0 overflows
  modulatedpulse_overflow_counter++;
}

In my main loop I use the TCNT0 timer value and the the overflow counter to calculate the required pulse length for the solenoid.

      cli();
      if (TCNT0 + (256 * modulatedpulse_overflow_counter) - modulatedpulse_start >= modulatedpulse_end) {
        PORTB &= ~_BV(solenoid); //solenoid off
       }
      sei();

Now my question:
modulatedpulse_overflow_counter is a volatile byte that is incremented in the ISR and reset in the main loop.
I turn interrupts off before I use this byte in combination with TCNT0 for my calculation, but does that do me any good? Or did I make it worse?
I now have prevented the Timer Overflow ISR to increment while I am calculating, but the timer TCNT0 is still counting. If it just went from 255 to 0 and gives me the "0" value, I have just blocked my TIM0_OVF ISR so I miss one overflow count.

How can I code in such way that I have the just overflown TCNT0 value AND the incremented overflow counter in my calculation. Should I only turn off interrupts when working with multi-byte fields and forget about using it elsewhere?

Hans

I am using an ATTiny13 to read an incoming signal to then modulate a solenoid valve that works on a 29Hz pulse frequency with a variable pulse-width from 0% (=open) to 100% (=closed).

What resolution of the duty cycle are you looking for?

I don't know all the constraints with an ATTiny 13, but using a hardware timer instead of a software timer with micros() to control a 29Hz pwm duty cycle seems to be making a simple task difficult.

For example, I would consider these variables and timers

currentTime = micros();
unsigned long period = 34483;//microseconds for 29 Hz
unsigned long onTime = (%dutyCycle * period);
unsigned long offTime = period - onTime;
if (outputState == HIGH && (currentTime - lastSwitchTime >= onTime))
  {
    lastSwitchTime = currentTime;
    outputState = LOW;
  }
  if (outputState == LOW && (currentTime - lastSwitchTime >= offTime))
  {
    lastSwitchTime = currentTime;
    outputState = HIGH;
  }
}

Thanks for your reply.
That's more or less how I did it when I coded this same thing on the Attiny85. But as a learning exercise I now want to move closer to the hardware with my programming and use as little memory as possible. Next step is to fit it into the Attiny10. Hence my question about atomic operation.

The resolution I now have is something between 10 and 100 microseconds. I started with one millisecond, but that was to coarse. (it is for the electronic fuel metering system from a Stihl chainsaw repurposed to model engines)

You actually helped me with my question as you wrote the word "micros()" in your response. So only then I got the idea to look how MCUDude coded micros() in the core for the Attiny13.

He takes a snapshot of TCNT0 and the overflow count and while doing so disabling interrupts.

  uint8_t oldSREG = SREG; // Preserve old SREG value
  t = TCNT0;              // Store timer0 counter value
  cli();                  // Disable global interrupts
  x = timer0_overflow;    // Store timer0 overflow count
  SREG = oldSREG;         // Restore SREG

I now need to study the datasheet and see what the meaning of the SREG register is.

And here is his whole micros() code.

/***** MICROS() *****/
// Enabling micros() will cause the processor to interrupt more often (every 2048th clock cycle if
// F_CPU < 4.8 MHz, every 16384th clock cycle if F_CPU >= 4.8 MHz. This will add some overhead when F_CPU is
// less than 4.8 MHz. It's disabled by default because it occupies precious flash space and loads the CPU with
// additional interrupts and calculations. Also note that micros() aren't very precise for frequencies that 64
// doesn't divide evenly by, such as 9.6 and 4.8 MHz.
#ifdef ENABLE_MICROS
// timer0 count variable
volatile uint32_t timer0_overflow = 0;
// This will cause an interrupt every 256*64 clock cycle
ISR(TIM0_OVF_vect)
{
  timer0_overflow++; // Increment counter by one
}

/**
 * @brief Returns the number of microseconds since the microcontroller
 *        began running the current program.
 *
 * @return uint32_t Number of microseconds passed since the program started
 */
uint32_t micros()
{
  uint32_t x;
  uint8_t t;

  uint8_t oldSREG = SREG; // Preserve old SREG value
  t = TCNT0;              // Store timer0 counter value
  cli();                  // Disable global interrupts
  x = timer0_overflow;    // Store timer0 overflow count
  SREG = oldSREG;         // Restore SREG

  #if F_CPU == 20000000L
    // Each timer tick is 1/(16MHz/64) = 3.2us long. We multiply the timer0_overflow variable
    // by 256 (bitshift 8 times) and we add the current timer count TCNT0. Since each tick is 3.2us long,
    // we multiply by 3 at the end
    return ((x << 8) + t) * 3;
  #elif F_CPU == 16000000L
    // Each timer tick is 1/(16MHz/64) = 4us long. We multiply the timer0_overflow variable
    // by 256 (bitshift 8 times) and we add the current timer count TCNT0. Since each tick is 4us long,
    // we multiply by 4 at the end
    return ((x << 8) + t) * 4;
  #elif F_CPU == 12000000L
    // Each timer tick is 1/(12MHz/64) = 5.333us long. We multiply the timer0_overflow variable
    // by 256 (bitshift 8 times) and we add the current timer count TCNT0. Since each tick is 5.333us long,
    // we multiply by 5 at the end
    return ((x << 8) + t) * 5;
  #elif F_CPU == 9600000L
    // Each timer tick is 1/(9.6MHz/64) = 6.666us long. We multiply the timer0_overflow variable
    // by 256 (bitshift 8 times) and we add the current timer count TCNT0. Since each tick is 6.666us long,
    // we multiply by 7 at the end
    return ((x << 8) + t) * 7;
  #elif F_CPU == 8000000L
    // Each timer tick is 1/(8MHz/64) = 8us long. We multiply the timer0_overflow variable
    // by 256 (bitshift 8 times) and we add the current timer count TCNT0. Since each tick is 8us long,
    // we multiply by 8 at the end
    return ((x << 8) + t) * 8;
  #elif F_CPU == 4800000L
    // Each timer tick is 1/(4.8MHz/64) = 13.333us long. We multiply the timer0_overflow variable
    // by 256 (bitshift 8 times) and we add the current timer count TCNT0. Since each tick is 13.333us long,
    // we multiply by 13 at the end
    return ((x << 8) + t) * 13;
  #elif F_CPU == 1200000L
    // Each timer tick is 1/(1.2MHz/8) = 6.666us long. We multiply the timer0_overflow variable
    // by 256 (bitshift 8 times) and we add the current timer count TCNT0. Since each tick is 6.666us long,
    // we multiply by 7 at the end
    return ((x << 8) + t) * 7;
  #elif F_CPU == 1000000L
    // Each timer tick is 1/(1MHz/8) = 8us long. We multiply the timer0_overflow variable
    // by 256 (bitshift 8 times) and we add the current timer count TCNT0. Since each tick is 8us long,
    // we multiply by 8 at the end
    return ((x << 8) + t) * 8;
  #elif F_CPU == 600000L
    // Each timer tick is 1/(600kHz/8) = 13.333us long. We multiply the timer0_overflow variable
    // by 256 (bitshift 8 times) and we add the current timer count TCNT0. Since each tick is 13.333us long,
    // we multiply by 13 at the end
    return ((x << 8) + t) * 13;
  #elif F_CPU == 128000L
    // Each timer tick is 1/(128kHz/8) = 62.5us long. We multiply the timer0_overflow variable
    // by 256 (bitshift 8 times) and we add the current timer count TCNT0. Since each tick is 62.5us long,
    // we multiply by 62 at the end
    return ((x << 8) + t) * 62;
 #endif

}
#endif // ENABLE_MICROS

Notice that interrupts were disabled for dealing with a 32-bit variable. This should be unnecessary for an 8-bit variable.

I looked up what the maeaning of
SREG = oldSREG; // Restore SREG"
is.
It is a more elegant solution of returning the "interrupt enable" status to whatever it was before the "cli()" command that disabled interrupts.

Because I have a one byte overflow counter, I decided to do away with disabling interrupts as per your suggestion.

Except when it is.

When does it become necessary to disable interrupts to access a byte?

I don't intend to start an argument; I am truly interested.

@vaj4088, if you want me (anyone) to be notified of your reply you need to reply to the actual post instead of replying to the thread. Or, you can quote from the post.

Both sides write (mostly true; there is an exception). That includes setting a variable to zero.

For example, if @hmeijdam sets modulatedpulse_overflow_counter to zero on the loop side then there is a non-zero chance of data loss. That pattern occurs often with people trying to use encoders.

When it comes to potential race conditions it is nearly always better to err on the side of caution. In @hmeijdam's case we're discussing three machine instructions (3 * 62.5µs @ 16 MHz). It's unlikely the application will suffer from being that amount slower. But, getting the critical section wrong often results in nearly impossible to fix bugs.

Thank you for telling me about replies and notifications. I think that I did it correctly this time but I won't know for sure until AFTER I post. I can see that it is going to take me a while to get accustomed to the "New and Improved" forum software.

I don't see how just setting modulatedpulse_overflow_counter to zero (or any other value) can cause data loss. Of course, if there is a check-then-act race condition (e.g. Test whether modulatedpulse_overflow_counter is greater than or equal to some value, and if so then set it back to zero), then I agree that "there is a non-zero chance of data loss." The combination of testing and acting MUST be handled by disabling interrupts OR in some other way handling the whole thing as a critical section.

I agree that optimization is to be avoided unless measurements are used before and afterwards to show that the optimization was needed and was useful.

I think that you meant that three machine instructions at 16 MHz is 3 * 62.5 nanoseconds. It is going to be better to get the critical section correct and not worry about the speed at first.

1 Like

Yup. (Hint: Select some text from my post. A " Quote button should appear.)

That's the exception. But, a written value never read is useless.

In the case of a critical section, never worry about the speed. Correct is paramount. Three machine instructions are irrelevant.

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