Capture, Compare and Pulse Width Modulation (PWM) or CCP modules on Microchip’s microcontrollers have three modes:(1)
- Capture Mode
- In Capture mode, the value of Timer1 is captured/stored when an event ocurs on pin CCPx. The event is defined by a configuration setting and can be a falling edge, rising edge or every 4th or 16th rising edge.
- Capture mode is used to measure the length of time elapsed between two events (with 'event' meaning either rising or falling edge of a signal on the CCPx pin).
- Compare Mode
- In Compare mode, the 16-bit CCPRx register value is constantly compared against the TMR1 register pair values. When a match occurs, the CCPx pin is either: Driven high, Driven low, Remains unchanged, or Toggles based on the module’s control bit settings. A CCP interrupt is also generated when a match occurs.
- Compare mode is analogous to the timer function on a stopwatch, in that Compare mode counts from zero to a pre-set time and then performs the pre-set change to the CCPx pin. This mode is useful for
generating specific actions at precise intervals. A timer could be used to perform the same functionality, however, it would mean preloading the timer each time. Compare mode also has the added benefit of automatically altering the state of the CCPx pin based on the way the module is set up.
- Pulse Width Modulation (PWM) Mode
- The CCP module produce a 10-bit resolution Pulse-Width Modulated (PWM) waveform on the CCPx pin.
- Such applications are based on one basic principle of PWM signals – as the duty cycle of a PWM signal increases, the average voltage and power provided by the PWM increases linearly.
What is Pulse Width Modulation
The PIC microcontroller, as any microcontroller, has in-built capacity to handle digital inputs/outputs and analog inputs, but not analog outputs. PWM is the method for in effect enabling the PIC microcontroller to have an analog output. An analog output voltage is useful as this can be used to control DC motor speed, intensity of a light globe/LED, temperature of a heater element etc. Using PWM from a PIC microcontroller (or other IC's), as opposed to using a potentiometer or similar to vary voltage, not only allows easy digital control/interfacing, but is much more energy efficient as ohmic heating/voltage dissipation is not necessary.
The actual 'pulse' used in PWM is a square-wave, and with the PIC16F876 this has an amplitude of 0-5V. The 'on time' of the 5V square wave (i.e., when it is at the 5V level) is the 'pulse width'. Varying this 'pulse width' by using the PIC microcontroller settings is the 'modulation'(which just means to 'vary' something). The following diagram shows the relationship between 'on' and 'off' time, the period and the duty cycle.
To summarise, PWM is a square wave generated by the PIC microcontroller, which can have a varying 'on' and 'off' time within a cycle, but the sum of the 'on' and 'off' time remains constant for every cycle. The duty-cycle is simply the percentage of time 'on' versus the total cycle time (i.e., the period). Therefore, a 100% duty-cycle means 100% 'on' time, and since the amplitude of the square wave generated by the PIC microcontroller is 5V (for the PIC16F876 at least), this means 100% duty cycle = 5V. Similarly, a 50% duty-cycle means 50% 'on' time and would effectively produce an 2.5V output. So this shows PWM is a way of digitally encoding analog output. However, the PWM signal is still digital as at any point in time, the full DC supply at the output pin is either fully on or fully off. That is, the voltage or current is supplied to the analog load by means of a repeating series of on and off pulses, with the width of the pulses giving the value of the analog signal. This leads to what is the effect of the actual frequency of the PWM square wave?
Assume a duty cycle of 50% and a frequency of 1Hz output PWM signal from the PIC microcontroller, connected to a LED or light bulb. This would supply 5V to the LED for 0.5 seconds and then 0V for 0.5 seconds, an 'average' of 2.5V. However, the LED would not appear to be half as bright, but would appear to flicker on and off. In order to make the LED appear to be dimmer (that is, be always 'on' but not as bright), the 'on' and 'off' pulsing needs to be quicker than the human eye can register. This shows that the frequency of the PWM signal needs to be quicker than the output loads response time to the switching on and off of the signal. In the example of the LED or light bulb, the modulation frequency needs to be increased to more than about 60Hz (to enable a dimmer type effect to be produced). In terms of PWM control of motors a modulation frequency of greater than about 4kHz is used. While in a power supply/regulator type application, modulation frequency can be in the order of 100kHz. In effect, the higher the modulation frequency of the PWM signal, the greater the 'smoothing' of the pseudo-analog signal produced. This leads to the general requirement to filter the PWM signal before passing on to the analog load, by the use of a RC circuit or similar.
Generating/Using PWM with the PIC Microcontroller
Using the CSS C Compiler for PIC microcontrollers makes the generation of PWM very easy. The following code snippet is all that is required to produce a 50% duty cycle, 5kHz PWM on Pin C1 for example (the CSS C Compiler manual gives further details about the "#use PWM" library).
Code Snippet 1:
include "PWM_PIC16F876A.h"
#use PWM(PWM1, TIMER=2, OUTPUT=PIN_C1, FREQUENCY=5kHz, DUTY=50)
//PWM duty cycle 50%, period 200uS, frequency 5000Hz, resolution 9bits
void main()
{
int16 pulseWidth; //width set in "tenths" of a percent 1000 = 100%
PWM_ON();
pulseWidth=1000;
do {
pwm_set_duty_percent(pulseWidth);
delay_ms(20); // 20ms x 1000 'steps' ~ 20seconds ramp 5-0V
pulseWidth--;
if(pulseWidth==0) pulseWidth=1000;
} while (TRUE);
}
The following video shows the result of the previous code snippet running on a PIC16F876A. The DMM response to voltage change is much slower than the PWM frequency, and so the DMM records what is effectively a changing analog voltage.
While use of the CSS "#use PWM" library makes implementation of PWM with a PIC microcontroller easy, the downside is increased RAM/ROM usage. Available RAM/ROM is generally at a premium when using microcontrollers. So it is advantageous to know some details about the PIC part being used in order to make the best use of available RAM/ROM. The following code snippet performs the same function as the previous example, but decreases RAM/ROM usage from 7% and 3% respectively to less than 1%.
Code snippet 2:
include "PWM_PIC16F876A.h"
//PWM duty cycle 50%, period 200uS, frequency 5000Hz, resolution 9bits
void main()
{
int16 pulseWidth; //width set in "tenths" of a percent 1000 = 100%
setup_ccp1(CCP_PWM); // Select CCP1 mode as PWM
setup_timer_2(T2_DIV_BY_4,124,1);
// (1/10000000)*4*4*124 = 200 us or 5 kHz
// duty value = 0 to ((124+1)*4)-1=499 .: 499L = 100% duty
pulseWidth=499;
set_pwm1_duty(pulseWidth);
do {
delay_ms(40); // 40ms x 500 'steps' ~ 20seconds ramp 5-0V
pulseWidth--;
if(pulseWidth==0) pulseWidth=499;
set_pwm1_duty(pulseWidth);
} while (TRUE);
}
The trade-off with this alternative approach, which enables more effective use of available RAM/ROM, is the learning curve coming to grips with the "innards" of the PIC microcontroller, in this case, in regard to various registers required to implement PWM. Although the built-in functions of the CSS C Compiler still provide a level of abstraction and make the overall process relatively painless (as opposed to juggling registers/memory with asm for example!).
PIC16F876A PWM Module
The PIC16F876A has two CCP modules, which means two seperate PWM outputs are possible. Although from the data sheet, both PWM's will have the same frequency and update rate (as they both use TMR2). From the previous background discussion about PWM, a PWM output is just a square wave of a certain period (i.e. frequency, since 1/period = frequency) and the "on" part of the cycle being the "pulse width". This boils down to setting an output square wave to "low"(i.e. 0V) on a particular pin, counting for a certain time, then setting the output square wave to "high"(i.e. 5V), and then continue counting for a further amount of time equivalent to the total desired period, and then repeat "endlessly" (i.e. according to the PIC code loaded). So to implement this the PIC16F876A uses the PR2 register to hold the PWM period, the CCPRxL register for the pulse width (where x=1 or 2 for whichever PWM channel selected) and Timer2 to do the "counting". The following diagram summarises the process/implementation:
This means the PWM process on the PIC involves some initial setup in the T2CON and CCP1CON registers, setting the duty cycle via CCPRxL:CCP1CON<5:4> and the PWM period via the PR2 register. The built-in functions of the CSS C Compiler help with this.
- The function SETUP_CCP1(CCP_PWM) writes the correct bits to the CCP1CON register to select PWM mode for the PIC CCP module.
- The function SETUP_TIMER_2(mode,PWM_period,postscale) writes the correct bits to the T2CON and PR2 registers, where mode = a value for the pre-scaler (either T2_DIV_BY_1, T2_DIV_BY_4, or T2_DIV_BY_16); PWM_period = a integer value from 0 to 255 that is the number of "ticks" before the TMR2 is reset; and postscale always = 1 as the post-scaler is not used in PWM.
- The function SET_PWM1_DUTY(duty_value) writes the correct bits to CCPRxL:CCP1CON<5:4> where duty_value = a 8 or 16bit value representing the duty cycle
The 'PWM_period' value and the 'duty_value' are calculated as follows:
- (eq 1) PWM period = [(PR2) + 1] x 4 x Tosc x TMR2_prescaler_value, where
- PR2 = value from 0 to 255
- Tosc = 1/(PIC Oscillator frequency)
- TMR2_prescaler_value = value of either 1, 4, 16
- value of '4' is required because four oscillator "ticks" for each TMR2 increment
- (eq 2) duty_cycle = CCPRxL:CCP1CON x Tosc x TMR2_prescaler_value, where
- CCPRxL:CCP1CON<5:4> = value from 0 to xxx
- Tosc = 1/(PIC Oscillator frequency)
- TMR2_prescaler_value = value of either 1, 4, 16
For example, if the PIC oscillator is 10MHz and the desired PWM frequency is 5kHz with a 50% duty cycle required. Firstly, better to re-arrange the PWM period formula in terms of PR2, therefore:
PR2 = (PIC_oscillator_freq/(PWM_frequency x TMR2_prescaler_value x 4)) - 1
So for the example, PIC_oscillator_freq = 10,000,000; PWM_frequency= 5,000; and the allowable values for TMR2_pre_scaler_value are either 1, 4 or 16.
- For TMR2_prescaler_value = 1, calculated PR2 = 499; PR2 can be maximum of 255, so TRM2_prescaler = 1 cannot be used
- For TMR2_prescaler_value = 4, calculated PR2 = 124
- For TMR2_prescaler_value = 16, calculated PR2 = 30.25; PR2 must be integer therefore round value, means exact desired frequency cannot be achieved with this pre-scaler value.
So now knowing TMR2_prescaler value is equal to 4 for the desired PWM frequency (and the value of PR2 is 124), can then calculate the value of CCPRxL:CCP1CON<5:4> for the desired duty cycle. Remembering that the duty-cycle is simply the percentage of time 'on' versus the total cycle time, the equation given above for duty_cycle (eq 2) is really the 'on' time in seconds. So in order to calculate the value of CCPRxL:CCP1CON<5:4> for the desired duty cycle as a percentage the formula (duty_cycle/PWM_period)*100 = % duty cycle needs to be incorporated with eq 1 and eq 2 above and the rearranged in term of CCPRxL:CCP1CON<5:4> therefore:
CCPRxL:CCP1CON<5:4> = (duty cycle as percent x PWM_period)/(100 x Tosc x TMR2_prescaler_value)
So continuing with the above example, duty cycle as percent = 50, PWM_period = 0.0002 (calculated from eq 1), Tosc=0.0000001, TMR2_prescaler_value = 4 and therefore CCPRxL:CCP1CON<5:4> = 250. Notice the value of 250 'fits' within a 8-bit integer. If for alternative example, the duty cycle required was 75%, then the calculated value would have been 375. This would then require a 'long int' value within CSS C to be defined to hold the result of the calculation. There is a Excel spreadsheet available in the (see the Downloads table at the end of this section) that provides all the necessary calculations to give values for the various PIC microcontroller registers to enable PWM output.
Such calculation of 'raw' registers values required to implement PWM on the PIC leads to avoiding using CSS C PWM functions entirely, with the aim to save available RAM/ROM. The PIC16F876A datasheet states PWM operation requires the following steps:
- Set the PWM period by writing to the PR2 register
- Set the PWM duty cycle by writing to the CCPR1L register and CCP1CON<5:4> bits
- Make the CCP1 pin an output by clearing the TRISC<2> bit.
- Set the TMR2 prescale value and enable Timer2 by writing to T2CON.
- Set the CCP1 module for PWM operation.
This leads to the following Code Snippet 3:
include "PWM_PIC16F876A.h"
//PWM duty cycle 50%, period 200uS, frequency 5000Hz,
#byte T2CON = 0x12
#byte CCPR1L = 0x15
#byte CCP1CON = 0x17
#byte PR2 = 0x92
void main()
{
int16 dutyCycle; // note 16-bit variable, values from 0-1023
PR2 = 124; //(1/10000000)*4*4*124 = 200 us or 5 kHz
dutyCycle = 499; //duty value = 0 to ((124+1)*4)-1=499 .: 499L = 100%
CCPR1L = dutyCycle>>2; //8 MSB's of duty cycle into register
CCP1CON = 0b00001100; //select PWM mode
CCP1CON = CCP1CON | (dutyCycle ‹‹4); //2 LSB's of duty cycle into register
SET_TRIS_C(0); //set CCP1 pin as output, actually all Port C output
T2CON = 0b0000101; //select prescaler=4 and TMR2 set to on
do {
delay_ms(40); //40ms x 500 'steps' ~ 20seconds ramp 5-0V
dutyCycle--;
if(dutyCycle==0) dutyCycle=499;
CCPR1L = dutyCycle>>2; //update duty cycle MSB's
CCP1CON = CCP1CON | (dutyCycle ‹‹4); //update duty cycle 2 LSB's
} while (TRUE);
}
The following table reports ROM/RAM usage for the three code snippets (PWM implementation approaches on the PIC 16F876 using CSS C).
Code Snippet |
Description |
ROM Usage |
RAM Usage |
1. |
Using CSS C #USE PWM functions |
262 |
24 |
2. |
CSS C functions, but no #USE PWM |
116 |
9 |
3. |
Setting registers directly |
119 |
9 |
3a. |
Setting registers directly + not using 2 LSB's ie '8 bit mode' |
91 |
9 |
This shows not much benefit in accessing and setting the various special function registers on the PIC directly in terms of decreasing ROM use (although most likely my implementation of moving the 2 LSB's of the duty cycle into CCP1CON<5:4> can be improved). The code snippet "3a" references deleting the lines of code "CCP1CON = CCP1CON | (dutyCycle ‹‹4);" from code snippet 3. This in effect gives "8 bit" resolution of the pulse width. The use of the various CSS C functions (i.e., code snippet 2) does give more "readable" C code compared to code snippet 3 (and this has the benefit of likely decreasing coding/logic errors). The reference to "8 bit" pulse width resolution and not using the 2 LSB's of the duty cycle is probably worth a few extra comments.
As mentioned previously, the pulse width is determined by 8bits in the CCPR1L register concantenated with 2 bits from CCP1CON<5:4> therefore giving a maximum of 10bit resolution. However, the actual effective realisable resolution depends on the PWM frequency and PIC oscillator frequency as well. The PIC 16F876 datasheet gives more details about this. The upshot being that unless fine control is required (i.e. a possible 1024 different pulse widths are actually required) the extra overhead of maintaining the 2 LSB's in CCP1CON<5:4> is probably not warrented, and the PIC ROM saving could be more beneficial (i.e., the code fits in the PIC memory space or not!). Although, the pulse width value as calculated from equation 2 cannot "just" be put into CCPR1L and the 2 LSB's in CCP1CON<5:4> effectively ignored all together, as shown by the results in the following table.
duty cycle value | value as Binary 8bit/10bit | value (1st 8 bits) + "00 | value >> 2 + "00 | decimal value (1st 8 bits) + "00 | decimal value >> 2 + "00 | value(1st 8 bits)+ "00" pulse width (sec) | value (1st 8 bits) + "00" duty cycle % | value >> 2 + "00" pulse width (sec) | value >> 2+"00 duty cycle % |
0 | 00000000 | 0000000000 | 00000000 | 0 | 0 | 0.00000 | 0% | 0.00000 | 0% |
1 | 00000001 | 0000000100 | 00000000 | 4 | 0 | 0.00000 | 1% | 0.00000 | 0% |
2 | 00000010 | 0000001000 | 00000000 | 8 | 0 | 0.00000 | 2% | 0.00000 | 0% |
3 | 00000011 | 0000001100 | 00000000 | 12 | 0 | 0.00000 | 2% | 0.00000 | 0% |
4 | 00000100 | 0000010000 | 00000100 | 16 | 4 | 0.00001 | 3% | 0.00000 | 1% |
5 | 00000101 | 0000010100 | 00000100 | 20 | 4 | 0.00001 | 4% | 0.00000 | 1% |
6 | 00000110 | 0000011000 | 00000100 | 24 | 4 | 0.00001 | 5% | 0.00000 | 1% |
7 | 00000111 | 0000011100 | 00000100 | 28 | 4 | 0.00001 | 6% | 0.00000 | 1% |
8 | 00001000 | 0000100000 | 00001000 | 32 | 8 | 0.00001 | 6% | 0.00000 | 2% |
9 | 00001001 | 0000100100 | 00001000 | 36 | 8 | 0.00001 | 7% | 0.00000 | 2% |
10 | 00001010 | 0000101000 | 00001000 | 40 | 8 | 0.00002 | 8% | 0.00000 | 2% |
11 | 00001011 | 0000101100 | 00001000 | 44 | 8 | 0.00002 | 9% | 0.00000 | 2% |
50 | 00110010 | 0011001000 | 00110000 | 200 | 48 | 0.00008 | 40% | 0.00002 | 10% |
100 | 01100100 | 0110010000 | 01100100 | 400 | 100 | 0.00016 | 80% | 0.00004 | 20% |
125 | 01111101 | 0111110100 | 01111100 | 500 | 124 | 0.00020 | 100% | 0.00005 | 25% |
200 | 11001000 | 1100100000 | 11001000 | 800 | 200 | 0.00032 | 160% | 0.00008 | 40% |
255 | 11111111 | 1111111100 | 11111100 | 1020 | 252 | 0.00041 | 204% | 0.00010 | 50% |
300 | 0100101100 | 0010110000 | 0100101100 | 176 | 300 | 0.00007 | 35% | 0.00012 | 60% |
400 | 0110010000 | 1001000000 | 0110010000 | 576 | 400 | 0.00023 | 115% | 0.00016 | 80% |
494 | 0111101110 | 1110111000 | 0111101100 | 952 | 492 | 0.00038 | 190% | 0.00020 | 98% |
496 | 0111110000 | 1111000000 | 0111110000 | 960 | 496 | 0.00038 | 192% | 0.00020 | 99% |
500 | 0111110100 | 1111010000 | 0111110100 | 976 | 500 | 0.00039 | 195% | 0.00020 | 100% |
The results in the above table assume a 10MHz PIC oscillator, PR2=124 with timer2 prescaler = 4 (i.e., PWM period of 0.0002 sec, frequency 5kHz). Therefore, for a desired duty cycle of 50%, a value of 255 is needed within the effective 10bits of CCPR1L:CCP1CON<5:4>. As binary 255 = 11111111, and therefore requires "11" to be placed in CCP1CON<5:4> and the remaining "111111" in CCPR1L. If binary 255 is placed "just" within CCPR1L, this will be concatenated with "00"(or whatever else may be CCP1CON<5:4>), which in case of "00" would give an effective value of decimal 1020 (binary 1111111100), which gives a duty cycle greater than 100%, i.e. no PWM "square wave" just a constant "high" output.
However, if binary 255 is left shifted twice (the ">> 2" in the code) and then this result is placed "just" within CCPR1L. This in turn will be concatenated with "00" by the PIC giving an effective value of decimal 252 (binary 0011111100), which gives a duty cycle of 50% as desired (due to the decreased resolution). Therefore, left shifting twice the desired/calculated value of the pulse width (as per equation 2) and placing this result in CPR1L and ignoring the 2 bits in CCP1CON<5:4> "will work" just limiting the resolution of the pulse width available.
Downloads
Only Logged-In Members can add comments