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:
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
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
}
3 Answers 3
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...)
.
-
\$\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– Pouria P2018年07月24日 11:24:40 +00:00Commented Jul 24, 2018 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– Pouria P2018年07月24日 11:27:39 +00:00Commented Jul 24, 2018 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– JimmyB2018年07月24日 12:55:57 +00:00Commented Jul 24, 2018 at 12:55
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.
Even after 4 years perhaps still useful:
I'm not sure with what registers and setting you tried for hardware PWM, but perhaps the code in my FastPwmPin library is useful (see Github).
I have tested hardware PWM on the ATtiny13A using Timer0 on pins 0 and 1. Note however that Timer0, pin 0 only supports toggle mode (50% PWM). Pin 1 does support other duty cycles with 8-bit resolution.
When testing my library on an ATtiny13A clocked at 9.6 MHz, I was able to generate frequencies starting at 38 Hz up to 1.6 MHz. So the frequency you mention should not be an issue for hardware PWM. (But note that at very high frequencies the PWM resolution drops).
OCR0A
is used by the IR code so I only haveOCR0B
. I'm trying to use it to generate software PWM on 3 non-PWM pins. \$\endgroup\$