PWM Operation

Designing the PWM circuit mostly involves selecting the output (pin number) you want to use and configuring the PIC’s various registers. Since we will be generating a “varying” pulse-width, we will need to determine how to change the pulse-width, how often to change the pulse-width, and when to change the pulse-width.

PWM Facilities

Looking at the 16F876 datasheet we see there are PWM outputs on PORTC RC1 and RC2. Since timer 2 is used for both PWM outputs, I have chosen to use CCP2 so I don’t confuse timer 1 with timer 2, i.e. CCP2 goes with TMR2. Other internal PWM support registers are the CCPR2L capture/comapare register, the timer 2 prescaler, PR2, and the TMR2 period register.

OK. So a further rerquirement for the PWM function is to sweep the duty cycle from approximately 1% to 99% of pulse-width. This needs to happen at a rate that allows a human to clearly observe the sweep and/or for a DMM to acquire and display a voltage measurement from an integrator circuit. One way to accomplish this is to generate an interrupt each time a full PWM period completes and count a sufficient number of those interrupts to provide the measurement or viewing delay required. (This method also gives us some experience with the interrupt system.) When that count has expired, the duty cycle will be modified (we’ll increment the period) and the process repeats. When the pulse-width period has reached its maximum value, it will be reset to its lowest value and the cycle continues.

Section 8.3 of the datasheet describes the PWM function. Let’s take a closer look at the period and duty-cycle calculations. The PWM circuit generates a constant frequency “low” period containing a variable width “high” time, or a “pulse”. The PWM period is determined by setting the PR2 register and the TMR2 prescaler. If there is a zero “pulse” time specified, the output would always be low. But, if there is a
pulse width specified, the pulse period begins with the match of PR2 and TMR2. When this match occurs, two things happen. First, the PWM output is set high to start the pulse period. Second, TMR2 is reset to zero and begins counting up immediately. The pulse remains high until a match of TMR2 with CCP2RL/CCP2CON[5:4]. When this match occurs, the PWM output is cleared (the pulse ends). TMR2 continues to count up until it again matches PR2 and the cycle repeats. If the TMR2IF (interrupt) is enabled, it occurs at the reset of TMR2. The calculations for period and duty cycle are:

    PWM period   =    [(PR2 + 1) * 4]    * Tosc * (TMR2 prescale value)
  PWM duty-cycle = [CCPR2L:CCP2CON[5:4]] * Tosc * (TMR2 prescale value)

Note that CCPR2L is an 8-bit register and is concatenated with CCP2CON[5:4] to create a 10-bit register giving a maximum count of 1024. PR2 is an 8-bit register but is “multiplied” by 4 internally. That essentially makes PR2 a 10-bit register also (remember, times-4 in binary is identical to 2 left shifts of a byte). This gives PR2 the same count length as CCPR2L+CCP2CON[5:4]; you just lack 2 LSBs of resolution in setting the value. These terms are highlighted in red above.

PWM Software Configuration

In the setup portion of the code, configure TRIS C to make pin RC1 an output for our CCP2 PWM signal. This is in addition to any other TRIS C setting required for display operation.

401
movlw
0x3c
; (assume Bank1 is active)
402
movwf
TRISC
; CCP2 is added as output

A GPR will be required to hold a variable for counting the delay between duty cycle updates (PdMod).

PdMod
EQU
0x34
; duty cycle update delay.

After all other set-up is finished the PWM registers are configured along with the interrupt system.

414
bsf
STATUS, 5
; Bank1
415
movlw
0x02
416
movwf
PIE1
; TMR2-PR2 match IE
417
movlw
0xff
418
movwf
PR2
419
bcf
STATUS, 5
; Bank0
420
movlw
0x0c
421
movwf
CCP2CON
; CCP2 On
422
movlw
0x06
423
movwf
T2CON
; set prescale1:16; timer on
424
movlw
0xc0
425
movwf
INTCON
; enable interrupts
426
call
delay
427
bcf
PIR1, 6
; clear ADC interrupt
428
bsf
ADCON0, GO
; start an acquisition

After enabling register bank 1 access the interrupt enable for timer 2 (TMR2IE) is set in PIE1 at line 416. This will generate an interrupt when the PWM period times-out.

Since we want to generate the maximum PWM period, the prescaler for timer 2 (PR2) is loaded with 0xff at line 418. PR2 retains its value unless it is written to again, so we can set-and-forget this register.

Back to register bank 0 and at line 421 CCP2CON bits CCP2X and CCP2Y are set to “00”. These are the 2 least significant bits of the CCPR2L register. The least significant 4 bits of this register are set to 0xc which selects PWM mode of operation.

Again, since we want the longest PWM period possible, we set the prescaler for timer 2 to 1:16 by writing b’10’ to T2CKPS0/1 in the T2CON register, line 423. TMR2ON turns TMR2 ‘ON’ when set to ‘1’. We do not use the postscaler for TMR2 so it is written with all ‘0’s. T2CON is therefore written with the value 0x06.

At line 425, 0xc0 sets GIE (the global interrupt enable) and PEIE (the peripheral enable) to their enabled state in INTCON. None of the other interrupts in this register are used, and the interrupt flags here are cleared.

Now let’s plug in some values. Assuming a 16MHz crystal, Tosc is 1/16Mhz = 62.5nS. We are generating the longest PWM period possible with PR2 at 0xff and the prescaler set to 1:16. This should work out to a period of (255 + 1) * 4 * 62.5nS * 16 = 1.024mS.

PWM Interrupt Service

We will use the interrupt service routine to periodically change the pulse width of the PWM from the shortest pulse width to the longest. When the pulse width reaches its maximum value, it will reset it to its minimum value. This is accomplished by just incrementing CCPR2L periodically. See below.

83
interrupt:
91
clrf
INTCON
92
bcf
PIR1, TMR2IF
; clear interrupt flag (timer2)
93
94
decfsz
PdMod0, f
; skif time for duty cycle modification
95
goto
fINT
96
bsf
PORTC, 0
97
movlw
0x60
98
movwf
PdMod0
; reset “mod-time” counter
99
incf
CCPR2L, f
; increment duty cycle counter
100
fINT:
107
bsf
INTCON, 6
; re-enable peripheral interrupts
108
bcf
PORTC, 0
109
retfie
; return from interupt

First, note that the STATUS and W register save and restore at the start and end of the interrupt service is not shown here (it’s in the downloadable code listing). But you should keep in mind that normally that functionality is required. See the Mid-Range MCU Family Reference Manual example 8-3 for more info.

The main thing to do in this interrupt service is to clear the interrupt flag and re-calculate the CCPR2L register value. In order to create the “scanning” duty cycle, the PdMod register counts 96 (0x60) interrupt cycles. During these 96 counts, the duty cycle remains constant. When this count finishes, it is reset and the CCPR2L register is incremented. Note that this generates 256 duty cycle steps which are 4 times the resolution of the PWM generator. If you want to generate all 1024 steps, you will have to manage CCP2CON[5:4] (the 2 LSBs of the duty cycle vallue).

Calculating time to sweep from 0% to 99% of the PWM range:

  Sweep time = (PW period) * PdMod * (duty-cycle steps)
             =   1.024mS   *  96   *       256
             =   25.17S

This should be enough time to observe the incrementing voltage on a scope, meter, or the display when we feed this output into a simple integrator and then into an analog-to-digital input port. To install the simple integrator, do the following:

  • Connect one end of a 10K resistor to PIC pin 12.
  • Connect the other end of the resistor to PIC pin 2.
  • Connect the “+” end of a 1uF cap to PIC pin 2.
  • Connect the other end of the cap to ground.
  • Measure volage at the resistor/capacitor junction (PIC pin 2).

I have written a python script to calculate the values for PR2 and CCPR registers based on the PIC clock frequency. If you have never looked into python, but were thinking about it, you can play with this code on a ‘real’ application. I have only tested it a little bit, so leave a comment on YouTube if you find a problem.

#!/usr/bin/python

#
#   pwm.py  --  calculate PR2, CCPR, & prescale values for PWM
#
#       Usage:  pwm.py {duty_cycle} {period} {sys_clock}
#               values can have a 'units' designation:
#                   S, mS, uS, Hz, kHz, MHz
#                   case insensitive, no spaces,
#                   missing unit assumes seconds.
#

import  sys, re

def compute( ):
pscale_values = [1, 4, 16]
fit = False

# ensure pd > dc
if pd < dc:
print "You specified a duty cycle greater than the PWM period."
usage( )

# calculate PR2 for the requested PWM period
for pscale in pscale_values:
ppdmax = (255 + 1) * 4 * Tosc * pscale
ppdmin =  (0 + 1)  * 4 * Tosc * pscale
if (pd <= ppdmax) & (pd >= ppdmin):
fit = True
break

if fit == False:
print "Can't fit requested parameters: "+ str( ppdmax ) +" <= "+ str( pd ) +" >= "+ str( ppdmin )
usage( )

PR2 = (pd / (4 * Tosc * pscale)) - 1

# calculate CCPR for requested duty cycle:
CCPR = dc / (Tosc * pscale) 

print "PR2: "+ str( int( round( PR2 ) ) ) +" ("+ hex( int( round( PR2 ) ) ) +")"
print "CCPR = "+ str( int( CCPR ) ) +" ("+ hex( int( round( CCPR ) ) ) +")"
print "prescale = 1:"+ str( pscale )

def conv_time( val ):
'''
Get value and units from each input argument.
Convert all values to proper "period" unit.
'''
units = ['hz', 'khz', 'mhz', 's', 'ms', 'us']

try:
unit = re.search( '[a-zA-Z]+', val ).group( ).lower( )
val = float( val[:-len( unit )] )
except AttributeError:
unit = ''
val = 1.0 / val

# value in time format:
if unit in units[3:]:
if unit[0]   == 'm':    val = val / 1000.0  
elif unit[0] == 'u':    val = val / 1000000.0   
else:                   val = val * 1.0
# value in frequency format:
if unit in units[:3]:
if unit[0]   == 'k':    val = 1 / (val * 1000.0);
elif unit[0] == 'm':    val = 1 / (val * 1000000.0)
else:                   val = 1 / (val * 1.0)

return val

def usage( ):
print "3 args required:"
print"\tPWM Duty Cycle"
print"\tPWM Period"
print"\tSystem Clock"
sys.exit( 1 )

if __name__ == "__main__":
# PWM period        =    [(PR2) + 1] * 4    * TOSC * (TMR2 prescale value)
# PWM duty cycle    = (CCPR1L:CCP1CON<5:4>) * TOSC * (TMR2 prescale value)

if len( sys.argv ) != 4:    usage( )

dc   = conv_time( sys.argv[1] )         # PWM duty cycle
pd   = conv_time( sys.argv[2] )         # PWM period
Tosc = conv_time( sys.argv[3] )         # Tosc
compute( )
    




©copyright 2014 pretzelogic LLC. All rights reserved.
No part of this page may be reproduced without permission.
Software, schematics, and text are presented as reference works only.
No claim as to useability, suitability, or correctness for any application is made.