Using PWM to Dim an LED

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

Overview

In this video:

  • Introduce Pulse Width Modulation (PWM) and the importance of high frequency and resolution.
  • Set a prescaler of 1 to get maximum PWM frequency.
  • Set up two output compare IRQs: one to control the period, the other to control the duty cycle.

In the previous lesson, we used a counter/timer interrupt to toggle an LED at a specific frequency.  If, however, we were more interested in the specific time the LED was ON vs. OFF we would look at Pulse Width Modulation (PWM).  This is based on the principle that the current through a pin is directly proportional to the percentage of time it is on vs. off.  PWM can be used to finely control current to not only LEDs but other current-controlled devices like DC motors.

For Pulse Width Modulation, you want a very high base frequency which will allow you to finely control the amount of current flowing through the pin.  We are going to use our ATMega328PB timers to generate PWM to control the brightness of our LED by modifying the code from our previous lesson.  We will modify the code to use one Output Compare interrupt to turn on the LED and another to turn it off.

Back to top

Procedure

Initialize the PWM

Create a Function to Initialize the PWM.

Copy the Timer_Frequency function and paste it immediately below it and name it PWM_Init.  Since this function takes no arguments and returns no values, it will be void as shown below.

void PWM_Init(void)
{
    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);
}

We want to modify it so that we have the highest frequency possible by removing the 256 prescaler. To make this happen with our code, we refer to the datasheet where we can see that we need to write to CS10 instead of CS12.

No Prescaling

Make the change to our code as shown below...

void PWM_Init(void)
{
    TCCR1B |= (1 << CS10) | (1 << WGM12);  //No Prescaler & set mode to CTC
   TIMSK1 |= (1 << OCIE1A); //Enable the CTC interrupt
   OCR1A = (F_CPU/(freq*2*256)-1);
}

Now we change the value of the prescaler in the equation from 256 to 1 and add a channel B CTC interrupt.  Our function now looks like below...

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 = (F_CPU/(freq*2*1)-1);
}

Now we go back to the frequency equation that we rearranged in the previous lesson to calculate a value for our output compare register (0CR1A) to set it up for 10kHz.

The equation looks like below with our values substituted in...

OCR1A = (16000000/(10000*2*1)-1)

This gives us an 800 for our OCR1A.  This value is the counter value for how long it will take until the counter reaches the point where it turns the LED on.  We will use 100 for OCR1B  This value is how long it will take to turn the LED off.  Our updated function now looks like below...

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 = 100;
}

Back to top


Interrupt Vectors

Set up the Interrupt Vectors

We need to do some work with the interrupt vectors.  We will use our original ISR to turn on the LED and add another ISR for channel B to turn it off.  We also added our PWM_Init call to out main and removed the Timer_Frequency call from the previous lesson because we have now recalculated it.  The complete code is listed below...

#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;
}

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;
}

int main(void) {
    DDRB |= (1 << PB5); // set PB5 as output pin
   DDRB &= ~(1<<DDB7); //set PB7 as an input pin
   
       
    PWM_Init();
    sei();                  //Enable global interrupts
   
   
   while (1) {
              
    }
}

Back to top


Program the device

Press the Make and Program Device Main Project button at the top of the MPLAB X GUI.

Program Device

Use should observe the LED coming on but rather dimly.  It is running at a duty cycle of 12%.  Try changing OCR1B to 400.  This will change the duty cycle to 50% and the LED will be much brighter.

Back to top


Learn More

Back to top