Using PWM with Swordfish

The great thing about a PICs hardware PWM module is that it can run continuously in the background while a program is executing other code. The not so good thing is that when reading a datasheet, it can be a little confusing with respect to setting it all up - although I have to say that reading the datasheet is always a very good idea, as configuration requirements can vary from device to device.

I'm assuming the reader knowns what PWM is as I don't intend to discuss the ins and outs of PWM on a PIC but rather the basic steps to get PWM working when using Swordfish. The two methods I will discuss are fixed frequency PWM and variable frequency PWM. One of the first things I would recommend you do is download PIC MultiCalc. This is a very useful utility that enables you to calculate Timers, PWM, USART or EUSART register values, you can see a screenshot to the left. If your eyes glaze over when reading the datasheet, then this utility is just what you need!

The fixed frequency PWM approach produces a very small code footprint. If all you want to do is vary the duty cycle when you code is executing then this is probably the best approach to use. The variable frequency approach, as the name suggests, enables you to change both frequency and duty when the program is executing. This requires more work by the MCU to calculate the correct PR2 and prescale values and so the code footprint is much larger. However, if you need to change frequency 'on the fly' then the variable frequency approach is the one you should look at. In the following code samples I am using an 18F452 running at 20MHz. Just connect an LED to PORTC.2 to see PWM in action!

Fixed Frequency PWM

The steps in configuring PWM basically break down to (1) set the PWM period by writing to the PR2 register (2) set the PWM duty cycle (3) make the PWM pin an output (4) set the TMR2 prescale value and enable Timer2 and (5) configure the CCP module for PWM operation. This is much easier to understand with some code. Lets assume we want a 5KHz frequency. First, start the PIC MultiCalc utility and press the 'PWM calc' button. Just enter values for

  • OSC - we are using 20
  • Frequency - we want 5KHz

and press enter. The fields we are interested in are prescaler, PR2 and max duty value. If more than one field is displayed (which there will be using our values) then pick the values which are closest to your target frequency. In this example it's: prescale = 4; PR2 = 249; max duty value = 1000.

Now all you need to do is type in the following code:

// set clock and osc...
Device = 18F452
Clock = 20

// **************************************************************************
// * download PIC MultiCalc
// * and then change to following constants to match...
// **************************************************************************
#option PRESCALER = 4        // prescale value [1, 4 or 16]
#option PR2_VALUE = 249      // prescale (PR2 register value)
#option MAX_DUTY = 1000      // max duty for this configuration

// bring options into the main program...
Const
   #if PRESCALER = 1 
   PrescaleValue = $00,
   #elseif PRESCALER = 4
   PrescaleValue = $01,   
   #elseif PRESCALER = 16
   PrescaleValue = $03,   
   #endif
   PR2Value = PR2_VALUE, 
   MaxDutyValue = MAX_DUTY 

// set duty routine...
Sub SetDuty(pDuty As Word)
   CCP1CON.5 = pDuty.1	
   CCP1CON.4 = pDuty.0	
   CCPR1L = pDuty >> 2
End Sub

// local duty variable...
Dim Duty As Word

// program start...
Output(PORTC.2)               // make PORTC.2 an output
CCP1CON = %00001100           // set CCP1 to PWM
T2CON = PrescaleValue Or $04  // set prescale, turn on TMR2 ($04)
PR2 = PR2Value                // set PR2 to get 5KHz out @ 20MHz	

// loop forever...
While true
   Duty = 0
   Repeat
      SetDuty(Duty)
      Inc(Duty, 10)
      DelayMS(20)	
   Until Duty > MaxDutyValue
Wend

All you need to do is transfer the values from PIC MultiCalc into the #options section in the above code and that's it!

Variable Frequency PWM

To support variable frequency PWM we need a little routine that calculates the prescale, PR2 and max duty value when the program is actually running. This takes a little bit more work but it's very easy to use. Given the amount of code, I decided to put it in a module called (not surprisingly) PWM. I'll show that later, but here is some code that actually calls the module

// import PWM module...
Include "PWM.bas"

// local duty variable...
Dim Duty As Word

// main program...
If PWM.SetFreq(5000) Then
   While true
      Duty = 0
      Repeat
         PWM.SetDuty(Duty)
         Inc(Duty,10)
         DelayMS(10)	
      Until Duty > PWM.MaxDuty
   Wend
EndIf

As you can see with the above code, it's very easy to use. When you call SetFreq(), the prescale, PR2 and max duty value are automatically calculated. Note that SetFreq() returns true if the frequency could be set, false otherwise. If it returns false, you need to check your datasheet (or use PIC MultiCalc to verify). The chances are the frequency you have entered is not supported at the clock frequency you are using.

The call to SetFreq() also has an optional percentage parameter to set the initial duty. For example,

PWM.SetFreq(5000, 50)

will set the PWM module at 5KHz with an initial duty cycle of 50%. In the code example above, the repeat...until loop iterates through until Duty > PWM.MaxDuty. However, rather than call PWM.SetDuty() you can use PWM.SetDutyPercent() to range the duty cycle from 0 to 100. For example,

// import PWM module...
Include "PWM.bas"

// local duty variable...
Dim Duty As Byte

// main program...
SetFreq(5000)
While true
   Duty = 0
   Repeat
      SetDutyPercent(Duty)
      Inc(Duty)
      DelayMS(10)	
   Until Duty > 100
Wend

Here is the module code for the above variable frequency examples. Just copy and paste into the Swordfish IDE and save in you UserLibrary folder as PWM.bas

Posted on the forum 17-09-2007 by Warren Schroeder. I would like to add that you can simplify the math in your PWM.SetFreq()routine for determining MAX_DUTY. For example,

MAX_DUTY = Word((PR2 + 1) << 2) 
{
*****************************************************************************
*  Name    : PWM.BAS                                                        *
*  Author  : David John Barker                                              *
*  Notice  : Copyright (c) 2007 Mecanique                                   *
*          : All Rights Reserved                                            *
*  Date    : 23/08/2007                                                     *
*  Version : 1.0                                                            *
*  Notes   : From an idea found at http://www.eng-serve.net/pic             *                    
*          :                                                                *
*****************************************************************************
}
Module PWM
Dim
   FMaxDuty As Word,
   FTMR2ON As T2CON.2

#if _device in (18F1220, 18F1320)
Dim FPWMPin As PORTB.3
#else
Dim FPWMPin As PORTC.2
#endif
{
****************************************************************************
* Name    : Start                                                          *
* Purpose :                                                                *
****************************************************************************
}  
Public Sub Start()
    CCP1CON = $0C
    Output(FPWMPin) 
    FTMR2ON = 1   
End Sub
{
****************************************************************************
* Name    : Stop                                                           *
* Purpose :                                                                *
****************************************************************************
}  
Public Sub Stop()
   Input(FPWMPin)
   CCP1CON = $00
End Sub
{
****************************************************************************
* Name    : SetDuty                                                        *
* Purpose : The CCPR1L contains the eight MSbs And the CCP1CON<5:4>        *
*         : contains the two LSbs. This 10-Bit value is represented by     *
*         : CCPR1L:CCP1CON<5:4>.                                           *
****************************************************************************
} 	
Public Sub SetDuty(pDuty As Word)
   CCP1CON.5 = pDuty.1
   CCP1CON.4 = pDuty.0	
   CCPR1L = pDuty >> 2
End Sub
{
****************************************************************************
* Name    : SetDutyPercent                                                 *
* Purpose : Set the duty as a percentage                                   *
****************************************************************************
} 	
Public Sub SetDutyPercent(pPercent As Byte)
   SetDuty(FMaxDuty * pPercent / 100)
End Sub
{
****************************************************************************
* Name    : MaxDuty                                                        *
* Purpose :                                                                *
****************************************************************************
}   
Public Inline Function MaxDuty() As FMaxDuty
End Function
{
****************************************************************************
* Name    : SetFreq                                                        *
* Purpose :                                                                *
****************************************************************************
} 
Public Function SetFreq(pFrequency As Word, pInitialDuty As Byte = 0) As Boolean
   Const Fosc As LongWord = _clock * 1000000
   Dim Prescale As Byte
   Dim PR2Value, PRConst As Word

   // loop through all the valid prescalers...
   PRConst = Fosc / pFrequency / 4
   Prescale = 1
   Result = false
   Repeat
      PR2Value = PRConst / Prescale - 1            // calculate a PR2 value
      If (PR2Value < 256) And (PR2Value > 1) Then  // if it is a valid value, then...
         Result = true                             // function return true (success)
         Select Prescale                           // configure T2CON prescale
            Case 1  : T2CON = %00000000            // prescale 1
	        Case 4  : T2CON = %00000001            // prescale 4
	        Case 16 : T2CON = %00000011            // prescale 16
         End Select
	     PR2 = PR2Value                            // initialise PR2
         FMaxDuty = Fosc / (pFrequency * Prescale) // determine maximum duty...
         SetDutyPercent(pInitialDuty)              // set to initial duty
         Start                                     // start PWM
         Exit                                      // exit the sub
      EndIf   
      Prescale = Prescale * 4
   Until Prescale > 16
End Function

// initialise the module
FMaxDuty = 0