8
\$\begingroup\$

I'm trying to make a remote control RGB LED light using an ATtiny13A.

I know the ATtiny85 is better suited for this purpose, and I know I might not eventually be able to fit the whole code, but for now my main concern is to generate a software PWM using interrupts in CTC mode.

I cannot operate in any other mode (except for fast PWM with OCR0A as TOP which is basically the same thing) because the IR receiver code I am using needs a 38 kHz frequency which it generates using CTC and OCR0A=122.

So I'm trying to (and I've seen people mention this on the Internet) use the Output Compare A and Output Compare B interrupts to generate a software PWM.

OCR0A, which is also used by the IR code, determines the frequency, which I don't care about. And OCR0B, determines the duty cycle of the PWM which I'll be using for changing the LED colors.

I'm expecting to be able to get a PWM with 0-100% duty cycle by changing the OCR0B value from 0 to OCR0A. This is my understanding of what should happen:

The wave form

But what actually is happening is this (this is from Proteus ISIS simulation):

As you can see below, I'm able to get about 25%-75% duty cycle but for ~0-25% and ~75-100% the wave form is just stuck and doesn't change.

YELLOW line: Hardware PWM

RED line: Software PWM with fixed duty cycle

GREEN line: Software PWM with varying duty cycle

Oscilloscope results

And here is my code:

#ifndef        F_CPU
    #define        F_CPU        (9600000UL) // 9.6 MHz
#endif

#include <avr/io.h>
#include <avr/interrupt.h>
#include <util/delay.h>

int main(void)
{
    cli();

    TCCR0A = 0x00;                        // Init to zero
    TCCR0B = 0x00;

    TCCR0A |= (1<<WGM01);                 // CTC mode
    TCCR0A |= (1<<COM0A0);                // Toggle OC0A on compare match (50% PWM on PINB0)
                                          // => YELLOW line on oscilloscope

    TIMSK0 |= (1<<OCIE0A) | (1<<OCIE0B);  // Compare match A and compare match B interrupt enabled

    TCCR0B |= (1<<CS00);                  // Prescalar 1

    sei();

    DDRB = 0xFF;                          // All ports output


    while (1)
    {
        OCR0A = 122;                      // This is the value I'll be using in my main program
        for(int i=0; i<OCR0A; i++)
        {
            OCR0B = i;                    // Should change the duty cycle
            _delay_ms(2);
        }
    }
}


ISR(TIM0_COMPA_vect){
    PORTB ^= (1<<PINB3);                  // Toggle PINB3 on compare match (50% <SOFTWARE> PWM on PINB3)
                                          // =>RED line on oscilloscope
    PORTB &= ~(1<<PINB4);                 // PINB4 LOW
                                          // =>GREEN line on oscilloscope
}

ISR(TIM0_COMPB_vect){
    PORTB |= (1<<PINB4);                  // PINB4 HIGH
}
\$\endgroup\$
  • \$\begingroup\$ May I ask why you cannot use hardware PWM? The reason you are giving does not make any sense. The only reason not to use hardware is if you need SPI interface or external interrupt. \$\endgroup\$ – Maple Jul 24 '18 at 9:25
  • \$\begingroup\$ @Maple I'm trying to control an RGB LED so I need 3 PWM signals, one for each color. OCR0A is used by the IR code so I only have OCR0B. I'm trying to use it to generate software PWM on 3 non-PWM pins. \$\endgroup\$ – Pouria P Jul 24 '18 at 9:35
  • \$\begingroup\$ 38kHz software PWM won't work. That's too fast for the MCU. \$\endgroup\$ – JimmyB Jul 24 '18 at 9:35
  • 1
    \$\begingroup\$ You can (and have done so) run an ISR @ 38kHz. But for any duty cycle other than 50% you will need a higher frequency. Example: For 25% @ 38kHz you need to be able to handle two successive interrupts within a 38kHz/25%=152kHz time frame. That leaves only about 63 CPU clock cycles (9600kHz/152kHz) for the ISR. At 10% duty cycle you have one 25 CPU clocks left for the ISR. \$\endgroup\$ – JimmyB Jul 24 '18 at 9:52
  • 3
    \$\begingroup\$ You did not specify the desired PWM frequency. For brightness control you won't need to be anywhere near 38kHz. 100Hz may be sufficient. I suggest you use the 38kHz (IR) frequency as the lowest duty cycle for your software PWM and implement the PWM as some multiple of that, e.g. 256, so that the lowest duty cycle is 1/256 (one 38kHz clock period) and the highest (below 100%) is (255/256), equal to 255 38kHz clock periods. This gives you an 8-bit PWM at (38000/256)~148Hz. \$\endgroup\$ – JimmyB Jul 24 '18 at 10:00
8
\$\begingroup\$

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...).

\$\endgroup\$
  • \$\begingroup\$ Wow you're awesome. I was wondering if my understanding of what you told me to do was correct and now there's this highly informative answer with examples and all. Thanks again. \$\endgroup\$ – Pouria P Jul 24 '18 at 11:24
  • \$\begingroup\$ Just one more thing, did I understand this correctly: If I were to do the PWM every other timer overflow I'd put an if in the interrupt routine to only execute the PWM code every other time. By doing this if my PWM code takes too long and the next overflow interrupt is missed then my program will be fine because the next interrupt was not going to do anything anyway. Is that what you meant? \$\endgroup\$ – Pouria P Jul 24 '18 at 11:27
  • \$\begingroup\$ Yes, this is what I meant, sorry for being so brief about it. The ISR should be quick enough not to miss any interrupt in the first place, but even when it is, spending 90% of the CPU time in one ISR may not be good either, so you could cut that almost by half by skipping the 'complex' logic every other interrupt leaving more time for other tasks. \$\endgroup\$ – JimmyB Jul 24 '18 at 12:55
2
\$\begingroup\$

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.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.