PWM is one of those things that we’ve always done in software. It allows us to set the frequency and duty cycle easily but at a cost of cpu cycles. In fact, until recently we didn’t actually know what frequency and duty cycle meant and because our PWM experience was mostly with servo- and motor-control, trying to understand them in these terms was more confusing than it needed to be.
Consider how a servo works.
We have to send a continuous series of pulses between 1ms and 2ms wide (in practice, between 0.6ms and 2.4ms) at least every 20ms. This is where frequency, duty cycle and comparing to hardware pwm gets confused.
With a servo, you need send only one pulse, up to 2ms wide, every 20ms.
This means that for 18 out of 20 milliseconds, the output doesn’t actually need to do anything:
So the first thing we need to do is to forget all about servo control.
With that out of the way, we need to understand how PWM is produced inside our microcontroller. If we were doing it in software, this is probably how we’d do it:
First, set up a timer. Every time the timer rolls over, switch on an output pin.
Secondly, set up a CCP (compare, capture, pwm) interrupt.
Put simply, in compare mode, we give this register a value. Whenever the timer value reaches this “milestone” or “waypoint” or whatever term you want to give it, an interrupt occurs. When this interrupt occurs, we turn the output pin off.
The end result is a PWM square wave.
The frequency is determined by the timer1 interrupt. In this example, we’re assuming a 16-bit timer1 so the timer counts up to 65,535 and raises the interrupt every time the timer rolls over to zero.
If we pre-loaded timer1 with a value each time, we could reduce the number that timer1 counts up to (actually, we’d start it from a higher value; it always raises an interrupt on rollover to zero, but the principle is the same). If timer1 is only counting up to, say, 20,000 instead of 65,535 (so we’d start the timer1 off at 65535-20000 = 45,535 to get it to count up to 20,000) then the frequency (number of times something occurs) is increased.
Duty cycle is a whole other thing and is dependent on which “value we’re counting up to”.
In terms of servo control, duty cycle doesn’t actually mean anything – we’re dealing with definite lengths of time. Turn on a pwm pin, then after 1.5ms, turn it off again. This 1.5ms isn’t actually a percentage of a frequency, it’s just an amount of time to leave a pin on for.
But if we’re creating a continuous, repeating square wave, duty cycle (as a percentage) depends on which value we’re counting up to.
If we wanted a 50% duty cycle (half the square wave is high, half low) we need to know what value we count up to, in order to create the timer1/reset interrupt. Then we set our CCP (turn-output-pin-off) interrupt to half this value.
It seems that this is where confusion creeps in when working with hardware PWM.
The datasheets talk about frequency, duty cycle, resolution and all this sort of crazy stuff that just gets confusing – especially when you’re trying to load a value to 25% into a register to set the duty cycle.
In order to create a 25% duty cycle, you need to first know the “timebase” for the PWM signal. This is the same as the number the timer is counting up to. Take 25% of this and put that value into the duty cycle register. So if we have an 8-bit PWM cycle (timer is counting from 0-255) in order to create a 25% duty cycle, we need to set the CCP value to 256*25% = 256/4 = 64.
If we’re using a 16-bit timer (counting from 0-65,535) then a 25% duty cycle would need a CCP value of 65536*25% = 65535/4 = 16,384
So in summary –
To increase the frequency, we need our PWM module to count up to a lower value.
To create a duty cycle (in percentage terms) we need to know what number we’re counting up to, calculate the number to count up to, in order to create the % based duty cycle and put that value into the duty cycle register.
This is nothing like the way we’ve been creating PWM for servos in code but does provide us with a really quick and easy way of playing PCM based wav files. Here’s a snippet from the datasheet for a PIC16F1825
So by loading the value 0x1F into our PWM register, the hardware will automatically reset the PWM output pin 250,000 times per second. Because of this, the maximum “resolution” – maximum number we can set the CCP interrupt to is a 7-bit value (0-127). In order to create a 250khz carrier wave, the timer1 “count-to-this” value (or hardware equivalent) has to be less than 255 to ensure it can be reset enough times during one second. Because “timer1” is counting up to a value less than 255, the CCP interrupt point cannot be an 8-bit value (0-255) so the maximum resolution is a 7-bit value (0-128).
For our audio playback example, we can set the PWM output “carrier wave” to 250khz – an inaudible frequency except for small animals – and then set the “duty cycle” using the value of each sample in the wav file. It needs a little adjustment, since we’re reading 8-bit values from the wav file. Luckily these eight-bit values represent duty cycles in binary (so a 50% amplitude in the sound wav is recorded as 128 in the wav file – i.e. 50% of the maximum 256). So if we could convert an 8-bit value to a 7-bit value, we could just load this straight into our CCP value register in PWM hardware.
To convert 8-bits to 7-bits is quite easy.
We simply bit-shift the entire value to the right by one place.
This is the same as dividing the value by two (but bit-shifting is a quicker operation to do).
What this does is makes all max values of 256 a new value 128.
All minimum values of 1 are discarded (or treated as zero) and the new minimum value becomes 2 (which when bit-shifted or divided by two is one). The overall result is a tiny loss of definition in the sound – but given the low-quality hardware we’ll be playing it back on, is of little concern.
So there we have it.
A quick round up of how to play sounds from a 22khz using PWM hardware:
- Set up the “carrier wave” frequency to be 250khz by loading PR1 with the value 0x1F.
- Every 1/22050th of a second, load another byte from the wav file, bitshift right by one position (divide by two) and set CCPR1L.