Updating PWM Duty Cycle Using a Millisecond Timer

Last modified by Microchip on 2023/11/10 11:09

In the previous lesson, we were able to get basic PWM working.  In order to do something interesting with PWM, like gradually fading in or out the brightness of an LED or increasing the speed of a motor, we need to regularly update our duty cycle.  For this, we are going to add a second timer.  We will use the 8-bit timer/counter0 which we learned in a previous lesson that it can count up to about 30ms if we use the maximum prescaler of 1024.

In This Video

  • Setting up a millisecond timer to update the duty cycle
  • Using the 8-bit timer-counter 0 as a millisecond timer.
  • Incrementing the duty cycle every 2 ms, resetting the duty cycle to 0 when it equals the period.

Procedure

TC0 - 8-bit Timer/Counter0

  • Review the Register Description for TC0 - 8-bit Timer/Counter0

Look for the Waveform Generation Modes table in the datasheet.  We will be using CTC mode which means that we need to set the WGM01 bit in the TCCR0A register.

WGM01 bit

TCCR0A Register Bit Descriptions

Back to Top


Prescaler Selector Bits

Determine the 1024 Prescaler selector bits

From this table, we see that we need to set CS02 and CS00 in the TCCR0B register.

Prescaler Bits

TCCR0B Bit Descriptions

Back to Top


CTC Mode

  • Select CTC mode and set the Prescaler in our Code

Here is the start of the new function for our millisecond timer

void millis_timer(uint8_t milliS)
{
    TCCR0A |= (1 << WGM01);               //set to CTC mode
   TCCR0B |= (1 << CS02) |= (1 << CS00);  //set prescaler to 1024

}

 

Back to Top


OCR0A Value

Get a value for the OCR0A. 

This register is associated with the CTC mode from the table shown in Step 2.  This value will determine how long our timer will run.

Recall the formula...

Frequency Formula

We will rearrange it to solve for OCRnX (OCR0A). include our milliS variable, and add it to our function.

OCR0A = milliS * 7.8125 - 1

void millis_timer(uint8_t milliS)
{
    TCCR0A |= (1 << WGM01);                //set to CTC mode
   TCCR0B |= (1 << CS02) | (1 << CS00);   //set prescaler to 1024

    OCR0A = milliS * 7.8125 - 1;
}

Back to Top


Enable the interrupt

In the register description section for the 8-bit Timer/Counter0 there is the register for TC0 Interrupt Mask Register.

TMSK0 Register

OCIE0A BIt

Bit 1 needs to be set in our milliS_timer function which is OCIE0A

void milliS_timer(uint8_t milliS)
{
    TCCR0A |= (1 << WGM01);                //set to CTC mode
   TCCR0B |= (1 << CS02) | (1 << CS00);   //set prescaler to 1024

    OCR0A = milliS * 7.8125 - 1;
    TIMSK0 |= (1 << OCIE0A);               //enable the interrupt  
}

Back to Top


Call the millS_timer function

  • Recall that our period resolution was set at 800 so if we wait 2ms before we increment each we should see about 1.6 seconds before we reach the top. Our function call will look like this...
milliS_timer(2);

Back to Top


Increase the Duty Cycle

  • Finally, we will write the Counter/Timer interrupt routine with some logic to increase our duty cycle and if the duty value has reached the period value, the duty value is reset.
ISR(TIMER0_COMPA_vect)
{
   uint16_t period = OCR1A;
   uint16_t duty =  OCR1B;
   
   if (duty < period)
    {
        duty++;
    }
   else
    {
        duty = 0;
    }
    OCR1B = duty;
}

Back to Top


Program the device

The full code is here: 

#define F_CPU 16000000UL
#include
<avr/io.h>
#include
<util/delay.h>
#include
<avr/interrupt.h>

#define LED_ON  PORTB |= (1<<PORTB5)
#define LED_OFF PORTB &= ~(1<<PORTB5)
#define LED_TOGGLE  PINB |= (1<<PINB5)

ISR(TIMER1_COMPA_vect)
{
   LED_ON;
}

ISR(TIMER1_COMPB_vect)
{
   LED_OFF;
}

ISR(TIMER0_COMPA_vect)
{
   uint16_t period = OCR1A;
   uint16_t duty =  OCR1B;
   
   if (duty < period)
    {
        duty++;
    }
   else
    {
        duty = 0;
    }
    OCR1B = duty;
}



void Timer_Frequency(uint8_t freq)
{
    TCCR1B |= (1 << CS12) | (1 << WGM12);  //Set clock source & set mode to CTC
   TIMSK1 |= (1 << OCIE1A); //Enable the CTC interrupt
   OCR1A = (F_CPU/(freq*2*256)-1);
}

void PWM_Init(void)
{
    TCCR1B |= (1 << CS10) | (1 << WGM12);  //No Prescaler & set mode to CTC
   TIMSK1 |= (1 << OCIE1A) | (1 << OCIE1B); //Enable the CTC interrupt
   OCR1A = 800;
    OCR1B = 400;
}

void milliS_timer(uint8_t milliS)
{
    TCCR0A |= (1 << WGM01);                //set to CTC mode
   TCCR0B |= (1 << CS02) | (1 << CS00);   //set prescaler to 1024

    OCR0A = milliS * 7.8125 - 1;
    TIMSK0 |= (1 << OCIE0A);               //enable the interrupt  
}
          
          
int main(void) {
    DDRB |= (1 << PB5); // set PB5 as output pin
   DDRB &= ~(1<<DDB7); //set PB7 as an input pin
   
    milliS_timer(2);    

    PWM_Init();
   
    sei();                  //Enable global interrupts
   
   while (1) {
              
    }
}

Select the Make and Program Target Main Project Button at the top of the MPLAB X GUI.

Program the Device

You should see the LED increasing in brightness and resetting.

Learn More

Back to Top