Electrical – Replace loop with timers in this ATtiny code

attinyavrtimer

How can I rewrite the following to use timers? Note that this is an ATtiny which has just Timer0 and Timer1

Essentially I'm trying to software PWM on many output pins, more than the built-in PWM pins. The code below is slightly simplified but the real code uses direct port manipulation and does almost nothing else, so the result should be almost as good as built-in PWM.

The intended application is intended to run on a small battery, so power saving is a major design goal. Also given that this is ATtiny, every byte of program space matters.

Or, convince me that it's not worth trying to use timers in this case.

void loop() {
  static int8_t procession = 0;
  static uint32_t lastTime = millis();
  static int8_t pos = 0;

  if (lastTime > millis() + 200) {
    lastTime = millis();
    if (++procession == 9) {
      procession = 0;
    }
  }

  const uint32_t ON_CYCLE = 5;
  const uint32_t OFF_CYCLE = 5;

  digitalWrite(procession, HIGH);

  delayMicroseconds(ON_CYCLE);

  if (OFF_CYCLE > 0) {
    digitalWrite(procession, LOW);
    delayMicroseconds(OFF_CYCLE);
  }
}

Best Answer

Of course you can replace any sort of looping and delays with the hardware timers. This will almost always result in more efficient and predictable operation. You haven't specified the number of PWM channels you want to use, or the frequency / duty cycles you want for any of them. For "bit-banging" PWM, you are limited to the number of available I/O lines (although, you could always add external shift registers to greatly increase this limit).

You are also limited on the CPU frequency in use. The fast the clock is ticking, the more things you can do in the same amount of time. This will become more important as you add PWM channels. The default AVR clock speed is 8MHz, scaled down to 1MHz internally. Arduino typically uses an external 16MHz crystal. This detail is important!

Each timer will have various "compare match" registers for use in generated interrupt service requests. Basically, the timer will count until it hits the value in a compare match register, then might trigger an ISR, reset the count, etc. Off the top of my head, there are numerous ways to create multiple PWMs using a single timer:

Multiple PWMs of the same frequency and duty cycle

This case is fairly trivial, but using a single timer, you can set any number of output pins on the "Compare Match A" (which also resets the count), and clear these pins on "Compare Match B". The value for "B" would be something from 1 to the value of "A", depending on your desired duty cycle.

Multiple PWMs of the same frequency with different duty cycles

This is a bit trickier, but still definitely doable. Similar to the first case, we will use "A" to set all of the PWM pins and reset the count. The "B" ISR is a bit more complex, but here is the gist of it:

  1. create a global array of time values, representing the "off time" of each PWM pin.
  2. Set the compare match B register to the smallest of these "off times"
  3. In the "B" ISR, check if the value of the register is equal to the "off time" of each PWM line in the time array (it should always be equal to one of them when it triggers the ISR) then turn off that pin(s).
  4. Loop through the time array for the NEXT Off time, and set the "B" register to that value, which will be used to trigger the next ISR.

This process will give you as many PWM pins as you want with different (variable) duty cycles (on times) but they all must have the same frequency.

Multiple PWMs with different frequencies and different Duty cycles

This final case I'll present builds on the ideas of the previous ones, but will add in a count variable that increments when the Timer ISR triggers. Every time this ISR triggers, an exact, known amount of time has passed, so you are free to use counter variables to decide when an event takes place. For example, you could could use one variable to count to 100 before setting a pin, then count to 100 before clearing it, all while another variable is counting to 200 to do the same things. That's 2 variables for 2 independent PWM channels.

Of course, this method is the messiest in terms of code, but it should seem familiar - it's essentially what your code was doing in the Main program loop, it has just moved this functionality to the hardware timers and generated ISRs without the hard delays, freeing your Main loop and CPU to do more important things than stare at ASM "NOP"s for thousands of clock cycles.

Additional Notes

I haven't used any code here, but I can add some in if you are more specific with your requirements, or give a hint as to what you already know how to do.

Also, I assume you're using an 8-bit AVR here. If so, I highly encourage you to pay attention to your required variable types. Using a 32-bit integer for a constant value of 5 is silly... use uint8_t for anything less than 256.

If you are really concerned with power and code size, drop the Arduino stuff and needless layers of abstraction.