In this tutorial, we’ll discuss what Pulse-Width Modulation (PWM) is and exactly how we can use it to control LED brightness, motor speed, and the intensity of many other output devices. Make sure you’ve gone through the GPIO Output, Input, and ADC tutorials before getting started.
Pulse Width Modu-what?
How can we create an analog output — such as a signal to vary our LED’s brightness — with a digital device? We could use an analog peripheral (such as our microcontroller’s DAC), but let’s forget about that for a minute.
Suppose I flip the light switch in my room on and off repeatedly. The light will blink, just as expected. If I flip the switch faster, the blinking speeds up. But what if I have robot hands?
If my bionic hands flipped the switch faster and faster, eventually the light would blink so fast you wouldn’t be able to see it blink. Instead, the “on”s and “off”s would blur together, and you’d just see a dim light. This is because of a phenomenon called the persistence of vision — our eyes effectively average the light intensity over time.
Similarly, if the same switch turned a motor on or off, and we rapidly turned it on and off, its inertia would keep it moving during the off periods, causing the speed to be averaged. The motor would run at half its fully-on speed.
Let’s go ahead and plot the voltage being applied to the device — either the light or the motor — over time.
In this waveform, we call the part that repeats the period (denoted with the letter T). The voltage is high for half of each period, and low for the other half.
What if instead of keeping this pattern, we flip the switch so that the voltage is only high for a quarter of each period? Then the light would be 1/4 of its maximum brightness, or the motor would run at 1/4 of its maximum speed.
The idea is that by keeping the period the same length, but varying the on-time, we can control the intensity of the device we’re powering. In other words, we modulate (change) the widths of our voltages pulses — hence the name for this waveform is Pulse-Width Modulation, or PWM. This allows us to control a device as if we’re outputting an analog voltage, using a digital signal.
When we’re talking about PWM waveforms, we call the ratio of the on-time to the period the waveform’s duty cycle. For example, in the above picture, our waveform has a duty cycle of 25%. If a waveform’s period lasts for 20ms, and the voltage is high for 16ms, its duty cycle would be 80%.
The duty cycle represents the intensity of the device we’re controlling.
A quick word on PWM frequency
According to our formula, if our voltage on-time was 1ms and our period was 3ms, we’d have a 33% duty cycle. If we doubled both the on-time and the period, our duty cycle would stay the same. Or, if we changed the on-time to 1us (one microsecond) and our period to 3us, the duty cycle wouldn’t change. Does that mean that all of these PWM signals are interchangeable? Does changing the PWM frequency matter?
The answer is yes and no. Obviously, if our frequency is too slow, it won’t work — if we blink the light too slowly, we’ll see it blink. However, higher-frequency signals also have problems. In the case of driving a motor, the motor won’t start if the PWM frequency is too high (since the pulses are too short to start the motor), and generating high frequency signals consumes more power. Also, there are many applications where we want to avoid PWM frequencies within the audio frequency range — between 20Hz and 20kHz — since the motor we’re driving will make an annoying whine. In general, the best frequency to use depends on what we’re driving.
The PWM module
You may have realized that we can already generate a PWM signal, using our knowledge of timers. Wouldn’t it be simpler to just do this, rather than learn a whole new peripheral? The advantage of using a dedicating PWM module is that we don’t need to keep executing instructions to turn the LED on and off. Instead, once we’ve set up the PWM module to generate a signal with our desired duty cycle and frequency, we don’t have to do anything to keep producing the signal. We could write an empty while loop and the LED would keep turning on and off! This frees up the processor to do other tasks without wasting cycles.
Writing the code
First, take a look at the datasheet’s table of contents and notice that our micro has two different types of PWM module: the “Capture/Compare/PWM” modules covered in section 28.0, and the dedicated PWM modules covered in section 29.0. Either one will do fine; we’ll use the dedicated modules for this tutorial.
Next, we’d like to figure out which pins the PWM modules output to — but we’ll quickly run into a problem. If you take a look at the pin allocation table near the start of the datasheet, as we usually do, you’ll notice that the 4 PWM modules aren’t connected to any pin! Instead, they’re simply listed as outputs.
Note (2) beneath the table gives a clue: it tells us that the I/O functions that aren’t listed with a pin connection are “PPS re-mappable”, and a scan of the table of contents tells us that PPS is the Peripheral Pin Select module. This module allows us to remap peripherals’ input and output pin connections, so if two peripherals use the same pin, we aren’t doomed — we can simply remap one of the connections to a different pin.
Jumping down to the PPS section (chapter 15.0), we learn that using it to remap pins is pretty straightforward — for inputs, we set a register for a given module that selects the pin that’s connected to it. Outputs work the opposite way: we set a register for a given pin that selects the peripheral module that’s connected to it.
PWM modules are output devices, so scroll past the input section to page 198, the “PPS Output Signal Routing Options” tables. The table on the right is for the PIC16(L)F15345, and looking through the table, we can see that the peripheral pin we’ll be using, PWM3OUT, is available on all ports A, B, and C. Checking this is always a good idea, in case the pin you’re planning to use isn’t available to connect to a given peripheral. We also see that PWM3OUT’s “register value” is listed as 0x0B — make a note of this.
Next we’ll take a look at which registers we need to mess with to route our PWM signal. The RxyPPS registers select the peripheral to connect to a pin. If we use the same pin we’ve been using for our first red LED (pin RC0), then we’ll write to register RC0PPS. The value we’ll write to it is the value of the desired register:
RC0PPS = 0x0B;
Calculating PWM time constants
Now that the PWM3 module is routed to pin RC0, let’s jump to the PWM section of the datasheet (29.0); go ahead and read through it.
First, we learn that we need timer2 to handle the PWM timing. We know how to configure timer modules from the previous tutorial:
T2CLKCON = 0b0001; //clock select is Fosc/4 T2CONbits.CKPS = 0b000; //for now, we won't mess with the prescaler T2CONbits.ON = 0b1; //turn timer2 on
Next, we’re given some formulas that we can use to calculate the values for some registers, given our desired PWM period and duty cycle. Let’s say we want our frequency to be 1kHz (1ms period) with a 50% duty cycle. We’ll use the PWM Period formula to figure out what value we should set the PR2 register to. Note that TOSC is just 1/FOSC, the system oscillator’s frequency, which is 32MHz.
We have to make sure that our prescaler ratio is big enough, or else our computed value won’t fit in PR2’s 8-bit width. Setting the prescaler to 1:128, we get a value of 61.5, which we can round to 61 (which, if you plug back into the original formula, gives us a frequency of 1.008kHz).
PR2 = 61;
Now we’ll use the Pulse Width formula to give us a 50% duty cycle. Since the pulse width duration should be half of the period (which is 1ms), we’re solving for a pulse width of 0.5ms.
The PWMxDC registers are 10-bit, so the value is split between a high and low register (just like the ADC result value). The upper 8 bits of our word go in PWMxDCH and the lower 2 bits go in the 2 MSBs of PWMxDCL. We’ll use bit shifting and bitmasking to split our 10-bit value:
PWM3DCH = pw>>2; //keep only the upper 8 bits PWM3DCL = (pw&0b11)<<6; //keep only the lower 2 bits, then shift them up
Lastly, we can use the Duty Cycle Ratio formula to verify our previous results.
Close enough! We have a 50% duty cycle. It’s also good to note that our resolution is only ~8 bits — we’re not using the full 10-bit range of duty cycles available to us. This means that with our current settings, we can select between around 256 different duty cycle ratios (corresponding to 256 different brightness levels) instead of the maximum available 1024 duty cycle ratios. If this was an issue, we’d take a look at the PWM resolution section for help on using the full 10 bits.
Writing the code
The datasheet’s step-by-step instructions for configuring the PWM3 module tell us that we need to:
- Set the time constant registers, PR2 and PWMxDCH/L
- Setup and enable timer 2
- Set the GPIO pin as an output (“enable the output driver”) and connect it to the PWM3 module using PPS
- Enable the PWM3 module
We’ve already seen how to do all of this, so let’s put it together!
void setupPWM()
{
//set up peripheral pin select
RC0PPS = 0x0B; //pin RC0 is connected to PWM3OUT
//set up TMR2
T2CLKCON = 0b0001; //clock select is Fosc/4
T2CONbits.CKPS = 0b111; //1:128 prescaler
T2CONbits.ON = 0b1; //turn TMR2 on
//set up PWM3
PR2 = 61; //set PR2 to give a 1ms pwm frequency, given FOSC=32MHz
unsigned short pw = 125; //use a 50% duty cycle
PWM3DCH = pw>>2; //keep only the upper 8 bits
PWM3DCL = (pw&0b11)<<6; //keep only the lower 2 bits, then shift them up
PWM3CONbits.EN = 0b1; //enable the module
}
The LED should be on, but dimmed. It might be hard to notice, so let’s drop the duty cycle even further. Go ahead and calculate the PWM3DC value that will give us a duty cycle of 10%, using the pulse width formula — you should get a value of 25. Run the updated code and the light should be just barely on.
Instead of using a hard-coded value for our duty cycle, let’s add a second function, setPWMduty(), that takes the desired duty cycle as an argument. Using the pulse width formula again, we see that if our pulse width is 1ms (giving us a 100% duty cycle), we’d have PWM3DC = 250. So setting PWM3DC to any value between 0 and 250 will give a proportional LED brightness.
bool setPWMduty(unsigned char duty) { if(duty > 250) return false; else { PWM3DCH = duty>>2; PWM3DCL = (duty&0b11)<<6; return true; } }
Now we can call setupPWM() once at the beginning of our program to configure and start the PWM module, and setPWMduty() whenever we want to change the LED brightness. For instance, we can use our potentiometer as a dimmer:
while(1)
{
//read the potentiometer
unsigned short potval = readADC();
potval /= 4.092; //convert the max. 10-bit ADC value (1023) to the max
//PWM duty cycle value (250)
//set the LED brightness
setPWMduty(potval);
}
Wrapping up
So far, we’ve learned how to control all of the simplest devices — digital and analog inputs and outputs — as well as to control the timing of our code and debug errors. Great job! Now we’ll move on to communication protocols, which we can use to talk with all kinds of sensors, computers (and other microcontrollers) and many other high-level devices. We’ll start with an overview of communication protocols.
Going further
Some things to try out:
- If you have a DC motor that you can drive using a MOSFET, use PWM to control the motor speed.
- Try controlling the LED brightness with different sensors, like a light-dependent resistor or a temperature sensor.
- As I mentioned above, we’re not using the full 10 bits of resolution available in the PWM module. See if you can change the values assigned to PR2, PWMxDC and the prescaler to use the full resolution.
- If you have access to an oscilloscope, scope the PWM signal. Is the signal’s period and pulse width what you expect?
Sample code