Electronic – AVR setting certain PWM frequency and duty cycle is not working right

avrpwm

I am prototyping a simple LED dimmer on an Arduino Uno (Atmega328p) and I have issues with the PWM frequency and duty cycle I am trying to adjust.
To get a frequency of 200Hz flat I am using the CTC mode and I used the formula in the datasheet (OCR2A = (F_CPU/2N200Hz)-1) to calculate the upper output compare register (OCR2A). It's 38 in my case.
The lower output compare register (OCR2B) is being controlled by an ADC input.

The ADC is working fine. I checked it with the serial monitor. So that part of the code can be ignored.

My main gripe is the frequency. According to my cheap handheld oscilloscope the frequency is stuck at 60Hz even if I change OCR2A. I can't explain why.

Then there's a problem with the duty cycle. If OCR2B is at 6 the duty cycle is at ca. 13%. If OCR2B is at 37 the duty cycle is close to 0%. I expected the duty cycle to be much higher the lower OCR2B is. Why is this not the case?

#define F_CPU 16000000UL       

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

void setup() {
    
    sei();
    DDRC = 0;                      // Input for ADC
    DDRD |= (1<<PD7);         // PWM Output
    ADMUX = 0;                  // use ADC0
    ADMUX |= (1 << REFS0);    // use AVcc as the reference
    ADMUX |= (1 << ADLAR);    // Right adjust for 8 bit resolution
    ADCSRA |= (1 << ADEN)|(1<<ADPS2)|(1<<ADPS1)|(1<<ADPS0);  // Enable the ADC, 128 prescale
   
    TIMSK2 |= (1<<OCIE2B)|(1<<OCIE2A)|(1<<TOIE2); // enable Timer/Counter Output Compare Match B Interrupt, enable Timer/Counter Output Compare Match A Interrupt, Ebable Timer/Counter Overflow Interrupt
    TCCR2A |= (1<<WGM01); //CTC Mode enabled
    TCCR2B |= (1 << CS22)|(1<<CS21)|(1<<CS20); //prescale: N=1024
    OCR2A = 38; // 200Hz PWM (upper threshold of timer compare register), OCR2A = (F_CPU/2*N*200Hz)-1
}

void loop() {


  ADCSRA |= (1 << ADSC)|(1 << ADIF);    // Start the ADC conversion -> ADSCRA = 0101 0000
     
     
  while(ADCSRA & (1 << ADIF) == 0); //ADSCRA = 0101 0000 & 0001 0000 = 1 ---- 0100 0000 & 0001 0000 = 0 -> conversion finished
 
   
  OCR2B = ADCH*38/255; //scaling of OC2RB, since OC2RB (lower threshold of timer compare register) must not be higher than OC2RA!

 

}

ISR(TIMER2_COMPA_vect) {
   PORTD &= ~(1<<PD7);
}

ISR(TIMER2_COMPB_vect) {
  if (OCR2B < 5){
    PORTD &= ~(1<<PD7);
  } else {
   
    PORTD |= (1<<PD7);
  }
}

Best Answer

You are probably also seeing is that the two interrupts can't execute simultaneously (they take a bit of time each) meaning that you either start missing some, or they start toggling the output out of sync with each other.

In practice this isn't the best way to generate a PWM signal using the timers, ideally you should use the PWM waveform generator modes.

Timer 2 has two dedicated outputs, OC2A and OC2B, which correspond to PB3 (digital pin 11 in Arduino speak) and PD3 (digital pin 3). These timer outputs allow direct PWM generation with various controls over the paramters. Each of these outputs has a special compare register OCR2A and OCR2B.

Because you want accurate frequency control, you need to be using the special modes which use both compare registers to generate a single PWM signal. The timer is configured such that the counter resets each time it matches OCR2A (the same as CTC mode) to set the period of the counter, leaving OCR2B to allow setting of the duty cycle.

This means you'd need to use OC2B (drives PD3) for your output. You've mentioned in the discussion we had that this is no issue, so you're good to go with the hardware method.

Based on the datasheet timer information, this means you'll need the following settings.

Firstly, you'll want to set the generator into either Fast PWM mode, or Phase Correct PWM mode. The former will allow twice the timing resolution, the latter results in a symmetric waveform which can help with harmonic suppression. In your case a higher resolution for the duty cycle would be better, so lets go for Fast PWM.

//First reset the counter to clear out any Arduino setup
TIMSK2 = 0; //Don't want any interrupts.
TCCR2B = 0; //Disable clock source
TCCR2A = 0;

Looking up the Waveform Generator Mode setting for the timer (WGM2[2:0]) on Table 18-8, we see for Fast PWM with OCR2A as the top value (to set frequency), mode 7 is required. This means all bits in the WGM2 setting need to be a 1. This gives:

// Set Fast PWM mode with OCR2A as period.
TCCR2A = (1<<WGM21) | (1<<WGM20); //Set lower bits [1:0] of WGM both to 1
TCCR2B = (1<<WGM22); //Set bit [2] of WGM to 1.

Next the comparison mode for the output pin - this allows you to tie PD3 directly into the timers PWM generation. You can either have non-inverting PWM or inverting PWM. Lets assume non-inverting, this gives a Compare Output Mode for OC2B output (register COM2B[1:0]) of 2. So we need to set COM2B1 and leave COM2B0 clear.

//Configure compare output for OC2B (PD3) to be non-inverting PWM
TCCR2A |= (1<<COM2B1);
OCR2B = 0; //Default to 0% duty cycle.

Now we need to work out the prescalar value for the timer, along with the OCR2A value such that you get a PWM frequency of 200Hz. For some insight into where this equation comes from, the period of the PWM signal is set by the number of count values (OCR2A+1 in Fast PWM mode) before the timer overflows, divided by the frequency of the counter. The frequency of the counter is simply the CPU frequency divided by a prescalar. This gives for Fast PWM:

$$f_{pwm} = \frac{f_{cpu}}{\mathrm{Prescalar} \times (\mathrm{OCR2A} + 1)}$$

For completeness for Phase Correct PWM, the period of the counter would be \$(2\times\mathrm{OCR2A})\$ instead of \$(\mathrm{OCR2A} + 1)\$ because it counts up from 0 to OCR2A then back down to 0.

Now we have an \$f_{cpu}\$ of 16MHz, and an \$f_{pwm}\$ of 200Hz. Rearranging a bit we get:

$$\mathrm{OCR2A} = \frac{16000000}{\mathrm{Prescalar} \times 200} - 1 = \frac{80000}{\mathrm{Prescalar}} - 1$$

The smallest prescalar value which results in an OCR2A value of less than 256 (8-bit) is the 1024 setting, which gives:

//Set counter top value
OCR2A = 77; //80000/1024 - 1

Then we want to enable the timer to count at \$f_{cpu}/1024\$ which corresponds to a Clock Select (CS2[2:0]) value of 7.

//Enable counter with clock source of Fcpu/1024
TCCR2B |= (1 << CS22)|(1<<CS21)|(1<<CS20);

And we are good to go. All that is left is to enable the PWM output pin itself:

DDRD |= (1 << PD3); //Set PD3 as an output.

Now you can change the duty cycle between 0% and 100% by setting the duty cycle register OCR2B to any value between 0 (0%) and OCR2A=77 (100%) inclusive, giving you 78 possible duty cycles and a fixed 200Hz period.