Author Topic: AVR PWM in CTC mode?  (Read 1299 times)

0 Members and 1 Guest are viewing this topic.

Offline DmeadsTopic starter

  • Regular Contributor
  • *
  • Posts: 164
  • Country: us
  • who needs deep learning when you have 555 timers
AVR PWM in CTC mode?
« on: May 15, 2020, 08:47:54 pm »
New to AVR-C programming.

Im trying to make a PWM circuit that is 10KHz with 75% duty cycle. I got the normal modes and fast pwm modes to work on the atmega328p, but I cant seem to get CTC PWM Mode to work.

basically, what I am trying to do is update the top counter max which the counter resets on. Once the counter resets, the output will toggle. So I update the counter max in an interrupt. One counter max is 75, and the other is 25, so the output will be high for 75 counts, and low for 25 counts, resulting in 75% duty cycle.

But I am only getting 50% duty cycle at 3.8 KHz output. Here is my code and a timing diagram for CTC mode:

Code: [Select]

#define F_CPU = 16000000UL

#include <avr/io.h>
#include <avr/interrupt.h>  // include to update OCR0A

// high for 75 counts, low for 25 = 75% duty cycle
// total 100 counts (with pre-scalar) to make PWM 10 KHz
int duty = 75;   // global int for both main and ISR (between 0-100)
int next_duty = 25;  // global int to calculate next duty cycle
int duty_cycle;  // keeps track of current duty cycle


int main(void)
{
int count = 0;  // for counting the ticks

DDRD = 0x40;  // set OC0A (PD6) as output

TCCR0A = (1 << COM0A0) | (1 << WGM01);  // set to CTC mode, toggle OC0A at compare match
TCCR0B = (1 << CS01);  // pre-scaler = clk/8

OCR0A = duty_cycle - 1; 

TIMSK0 = (1 << TOIE0);  // enables overflow interrupt

sei();  // enable interrupts

    while (1)  // need to update OCR0A each time after reaches top
    {
// this counter counts
if(count < (100*8-1))  // need to multiply by 8 to account for pre-scalar
{
count++;
}
else
{
count = 0;
}

// if the count is high for a certain time, recalculate the duty cycle one count before overflow (and therefore interrupt occurs
if(count >= (duty - 2) * 8) 
{
duty_cycle = next_duty;
}
else if(count < (duty - 2) * 8)
{
duty_cycle = duty;
}
    }
}

ISR(TIMER0_OVF_vect)
{
OCR0A = duty_cycle - 1; // recalculate OCR0A top value
}


Thanks
 

Offline T3sl4co1l

  • Super Contributor
  • ***
  • Posts: 22274
  • Country: us
  • Expert, Analog Electronics, PCB Layout, EMC
    • Seven Transistor Labs
Re: AVR PWM in CTC mode?
« Reply #1 on: May 16, 2020, 12:51:30 am »
OCR0A is initialized to 65535..?  (That'll just delay the first cycle, you may not even notice it; but it's not good form -- imagine what would happen if you used a 24 or 32 bit timer!)

Wouldn't your code be doing a... some sort of asynchronous frequency modulation?  The main() loop runs flat out, incrementing count in a ramp, probably several times faster than the timer is running.  The timer only samples duty_cycle when it interrupts, and if it happens to read the same value twice in a row, you don't get any PWM.

You've also got a hazard that ints are two byte variables.  If the interrupt fires while main() is writing to duty_cycle, it can read one old byte and one new byte, getting some trash Frankensteined number.  (For values as shown, this won't ever happen because the high byte is always zero  -- duty and next_duty are both within the +127/-128 inclusive range of a 1-byte char.)

You've also got the problem that the compiler doesn't know anything else can read duty_cycle; since main() is only reading it in the loop, it might never bother to update the memory value, just changing around values in registers (or optimizing the whole thing down to nothing; I don't think avr-gcc is nearly smart enough to do that, but other flavors are).

The solution is to declare,
Code: [Select]
volatile int duty_cycle = duty;

which tells the compiler that, hey, other things want to read this value, so whenever you write to it, remember to save it to memory as well.  And then when accessing it, use:

Code: [Select]
#include <util/atomic.h>
...
ATOMIC_BLOCK(ATOMIC_FORCEON) {
duty_cycle = foo;
}

This is equivalent to putting cli() and sei() around the statement, it's just a more portable way of doing so.  (You can also use the parameter ATOMIC_RESTORESTATE instead, if you are in a function where you don't know whether interrupts are supposed to be on or off; it saves and restores the flag.)

An atomic operation is indivisible, as the name suggests.  By disabling interrupts, you guarantee TIMER0_OVF_vect never divides the writing process. :-+


So that's important, but it won't actually accomplish what you wanted to do!  As for that -- I'm not sure how you expected count to track the timer but it's just a variable in main(), it doesn't have any relation to device state.  Perhaps you meant to read TCNT0, the counter register; perhaps you meant to increment count (making it a global variable first) in the interrupt; perhaps you meant to set a flag in the interrupt (protip: 1-byte (char) variables are atomic so you don't need to use the ATOMIC macros!), and load duty or next_duty based on that, or poll that flag in the main() loop to the same end.  All are possibilities, with the interrupt handling itself being one of the best.

The best of course is to simply use the PWM mode, or use TC1 if you need the one bit extra resolution that your emulated counting mode provides (assuming of course, you aren't using it for anything else later; it is rather important after all, being the only 16-bit counter on the ATMEGA324).

In my last AVR project, I used three timers and have plans for a 4th, most of them literally just setting a flag and returning (the flag is then read by e.g. main() loop to trigger housekeeping functions, reading pushbuttons, updating display, that sort of stuff).  Helps that the XMEGA I'm using has tons of timers. :P

Tim
Seven Transistor Labs, LLC
Electronic design, from concept to prototype.
Bringing a project to life?  Send me a message!
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf