Using PWM to Dim an LED
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.
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.
{
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.
Make the change to our code as shown below...
{
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...
{
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...
{
TCCR1B |= (1 << CS10) | (1 << WGM12); //No Prescaler & set mode to CTC
TIMSK1 |= (1 << OCIE1A) | (1 << OCIE1B); //Enable the CTC interrupt
OCR1A = 800;
OCR1B = 100;
}
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...
#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) {
}
}
Program the device
Press the Make and Program Device Main Project button at the top of the MPLAB X GUI.
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.