ATtiny13A - Can't generate software PWM with CTC mode

A minimal software PWM could look like this:

volatile uint16_t dutyCycle;


uint8_t currentPwmCount;

ISR(TIM0_COMPA_vect){
  const uint8_t cnt = currentPwmCount + 1; // will overflow from 255 to 0
  currentPwmCount = cnt;
  if ( cnt <= dutyCyle ) {
    // Output 0 to pin
  } else {
    // Output 1 to pin
  }
}

Your program sets dutyCycle to the desired value and the ISR outputs the corresponding PWM signal. dutyCycle is a uint16_t to allow for values between 0 and 256 inclusive; 256 is bigger than any possible value of currentPwmCount and thus provides full 100% duty cycle.

If you don't need 0% (or 100%) you can shave off some cycles by using a uint8_t so that either 0 results in a duty cycle of 1/256 and 255 is 100% or 0 is 0% and 255 is a duty cycle of 255/256.

You still don't have much time in a 38kHz ISR; using a little inline assembler you can probably cut the cycle count of the ISR by 1/3 to 1/2. Alternative: Run your PWM code only every other timer overflow, halving the PWM frequency.

If you have multiple PWM channels and the pins you're PMW-ing are all on the same PORT you can also collect all pins' states in a variable and finally output them to the port in one step which then only needs the read-from-port, and-with-mask, or-with-new-state, write-to-port once instead of once per pin/channel.

Example:

volatile uint8_t dutyCycleRed;
volatile uint8_t dutyCycleGreen;
volatile uint8_t dutyCycleBlue;

#define PIN_RED (0) // Example: Red on Pin 0
#define PIN_GREEN (4) // Green on pin 4
#define PIN_BLUE (7) // Blue on pin 7

#define BIT_RED (1<<PIN_RED)
#define BIT_GREEN (1<<PIN_GREEN)
#define BIT_BLUE (1<<PIN_BLUE)

#define RGB_PORT_MASK ((uint8_t)(~(BIT_RED | BIT_GREEN | BIT_BLUE)))

uint8_t currentPwmCount;

ISR(TIM0_COMPA_vect){
  uint8_t cnt = currentPwmCount + 1;
  if ( cnt > 254 ) {
    /* Let the counter overflow from 254 -> 0, so that 255 is never reached
       -> duty cycle 255 = 100% */
    cnt = 0;
  }
  currentPwmCount = cnt;
  uint8_t output = 0;
  if ( cnt < dutyCycleRed ) {
    output |= BIT_RED;
  }
  if ( cnt < dutyCycleGreen ) {
    output |= BIT_GREEN;
  }
  if ( cnt < dutyCycleBlue ) {
    output |= BIT_BLUE;
  }

  PORTx = (PORTx & RGB_PORT_MASK) | output;
}

This code maps the duty cycle to a logical 1 output on the pins; if your LEDs have 'negative logic' (LED on when pin is low), you can invert the polarity of the PWM signal by simply changing if (cnt < dutyCycle...) to if (cnt >= dutyCycle...).


As @JimmyB commented the PWM frequency is too high.

It seems that the interrupts have a total latency of one quarter of the PWM cycle.

When overlapping, the duty cycle is fixed given by the total latency, since the second interrupt is queued and executed after the first is exited.

The minimum PWM duty cycle is given by the total interrupt latency percentage in the PWM period. The same logic applies to the maximum PWM duty cycle.

Looking at the graphs the minimum duty cycle is around 25%, and then the total latency must be ~ 1/(38000*4) = 6.7 µs.

As consequence the minimum PWM period is 256*6.7 µs = 1715 µs and 583 Hz maximum frequency.

Some more explanations about possible patches at a high frequency:

The interrupt has two blind windows when nothing can be done, entering end exiting the interrupt when the context is saved and recovered. Since your code is pretty simple I suspect that this takes a good portion of the latency.

A solution to skip the low values will still have a latency at least as exiting the interrupt and entering the next interrupt so the minimum duty cycle will not be as expected.

As long as this is not less than a PWM step, the PWM duty cycle will begin at a higher value. Just a slight improvement from what you have now.

I see you already use 25% of the processor time in interrupts, so why don't you use 50% or more of it, leave the second interrupt and just pool for the compare flag. If you use values only up to 128 you will have only up to 50% duty cycle, but with the latency of two instructions that could be optimized in assembler.