News:

Let's find out together what makes a PIC Tick!

Main Menu

Positron8 - HPWM Library for PIC18FxxK40 devices

Started by top204, Jul 12, 2023, 05:18 PM

Previous topic - Next topic

top204

I was creating a project that needed a few PWM waveforms of different frequencies to run in the background, and two of them had to be the opposite polarity to each other, so I wrote a library that allows the two CCP and the two PWM peripherals on a PIC18FxxK40 device to work at multiple frequencies using the three compatible timers, which are Timer2, Timer4 and Timer6. Hopefully, the library file will come in useful for others.

The library works very well, and has several procedures to alter the timer used for a PWM channel, the frequency of a PWM channel, and the duty cycle of a PWM channel. The duty cycle operates in an 8-bit mode, so a value of 0 is minimum duty, and 255 is maximum duty, so a value of 127 is a 50% duty cycle.

With the three suitable timers on a PIC18FxxK40 device, three independent frequencies can be generated, and a PWM channel that shares a timer will operate at the same frequency as the other channel using the same timer.

The library uses 32-bit maths, so very high frequencies can be calculated and generated, and I have had it producing a 1.2MHz waveform on a PIC18F26K40 device operating at 64MHz!.

A simple demonstration program is listed below using the "HPWM_K40.inc" library file:

'
'   /\\\\\\\\\
'  /\\\///////\\\
'  \/\\\     \/\\\                                                 /\\\          /\\\
'   \/\\\\\\\\\\\/        /\\\\\     /\\\\\\\\\\     /\\\\\\\\   /\\\\\\\\\\\  /\\\\\\\\\\\  /\\\\\\\\\
'    \/\\\//////\\\      /\\\///\\\  \/\\\//////    /\\\/////\\\ \////\\\////  \////\\\////  \////////\\\
'     \/\\\    \//\\\    /\\\  \//\\\ \/\\\\\\\\\\  /\\\\\\\\\\\     \/\\\         \/\\\        /\\\\\\\\\\
'      \/\\\     \//\\\  \//\\\  /\\\  \////////\\\ \//\\///////      \/\\\ /\\     \/\\\ /\\   /\\\/////\\\
'       \/\\\      \//\\\  \///\\\\\/    /\\\\\\\\\\  \//\\\\\\\\\\    \//\\\\\      \//\\\\\   \//\\\\\\\\/\\
'        \///        \///     \/////     \//////////    \//////////      \/////        \/////     \////////\//
'                                  Let's find out together what makes a PIC Tick!
'
' Hardware PWM routines that make use of the CCP and PWM peripherals on the PIC18FxxK40 devices.
' It allows frequencies up to 1.2MHz
' Low end frequencies and high end frequencies depend on the oscillator value used.
' Frequencies may be approximate depending on the oscillator value used and the frequency required.
' This is not due to a limitation of the compiler, but of the CCP and HPWM peripherals and the device's operating frequency.
'
' Written for the Positron8 BASIC Compiler by Les Johnson.
'
    Device = 18F26K40                                   ' Tell the compiler what device to compile for
    Declare Xtal = 64                                   ' Tell the compiler what frequency the device is operating at (in MHz)
    Declare Create_Coff = True                          ' Create a COF file for debugging in Proteus
'
' Setup which pins are used by the CCP and PWM peripherals
'
    Declare HPWM1_Pin = PORTC.0
    Declare HPWM2_Pin = PORTC.1
    Declare HPWM3_Pin = PORTC.2
    Declare HPWM4_Pin = PORTC.3

    Include "HPWM_K40.inc"                              ' Load the 18FxxK40 PWM library into the program

'---------------------------------------------------------------------------
' The main program starts here
' Set the timers used by the HPWM peripherals and the frequencies and duty cycle values
'
Main:
    Setup()                                             ' Setup the program and the peripherals

    HPWM1(HPWM_cTimer2, 2000, 127)                      ' Set PWM channel 1 to use Timer2 at 2KHz, and a 50% duty cycle
    HPWM2(HPWM_cTimer4, 10000, 127)                     ' Set PWM channel 2 to use Timer4 at 10KHz, and a 50% duty cycle
    HPWM3(HPWM_cTimer6, 100000, 127)                    ' Set PWM channel 3 to use Timer6 at 100KHz, and a 50% duty cycle
    HPWM4(HPWM_cTimer6, 100000, 127)                    ' Set PWM channel 4 to use Timer6 at 100KHz, and a 50% duty cycle (same timer used, so same frequency)

    HPWM4_PolInvert()                                   ' Invert the polarity of the HPWM4 waveform

'---------------------------------------------------------------------------
' Setup the program and the peripherals
' Input     : None
' Output    : None
' Notes     : None
'
Proc Setup()
    Oscillator_64MHz()                                  ' Set the device to operate at 64MHz with its internal oscillator

    PPS_Unlock()                                        ' Unlock the PPS mechanism
    RC0PPS = PPS_Fn_PWM1                                ' Configure the PPS for the pin to use for PWM channel 1
    RC1PPS = PPS_Fn_PWM2                                ' Configure the PPS for the pin to use for PWM channel 2
    RC2PPS = PPS_Fn_PWM3                                ' Configure the PPS for the pin to use for PWM channel 3
    RC3PPS = PPS_Fn_PWM4                                ' Configure the PPS for the pin to use for PWM channel 4
EndProc

'------------------------------------------------------------------------------------------------
' Set the microcontroller to internal 64MHz operation with an HFINTOSC_1MHZ fuse
' Input     : None
' Output    : None
' Notes     : None
'
Proc Oscillator_64MHz()
    OSCCON1 = %01100000
    OSCCON3 = %00000000
    OSCEN   = %00000000
    OSCFRQ  = %00001000
    OSCTUNE = %00000000
    DelayMS 100
EndProc

'------------------------------------------------------------------------------------------------
' Setup the fuses to use the internal oscillator on a PIC18F26K40
'
Config_Start
    RSTOSC = HFINTOSC_1MHZ                              ' With HFFRQ = 4MHz and CDIV = 4:1
    FEXTOSC = Off                                       ' External Oscillator not enabled
    CLKOUTEN = Off                                      ' CLKOUT function is disabled. Pins RA6 and RA7 are standrad I/O pins
    WDTE = Off                                          ' WDT disabled
    CSWEN = On                                          ' Writing to NOSC and NDIV is allowed
    FCMEN = Off                                         ' Fail-Safe Clock Monitor disabled
    MCLRE = EXTMCLR                                     ' MCLR pin is reset pin
    PWRTE = On                                          ' Power up timer enabled
    LPBOREN = Off                                       ' LPBOREN disabled
    BOREN = On                                          ' Brown-out turned on
    BORV = VBOR_285                                     ' Brown-out Reset Voltage set to 2.85V
    ZCD = Off                                           ' ZCD disabled. ZCD can be enabled by setting the ZCDSEN bit of ZCDCON
    PPS1WAY = Off                                       ' PPSLOCK bit can be set and cleared repeatedly (subject to the unlock sequence)
    STVREN = Off                                        ' Stack full/underflow will not cause Reset
    Debug = Off                                         ' Background debugger disabled
    XINST = Off                                         ' Extended Instruction Set and Indexed Addressing Mode disabled
    SCANE = Off                                         ' Scanner module is not available for use. SCANMD bit is ignored
    LVP = Off                                           ' Low Voltage programming disabled
    WDTCPS = WDTCPS_13                                  ' Watchdog Divider ratio 1:x (8 seconds)
    WDTCWS = WDTCWS_7                                   ' Window always open (100%). Software control. Keyed access not required
    WDTCCS = LFINTOSC                                   ' WDT input clock selector->WDT reference clock is the 31.2kHz HFINTOSC output
    WRT0 = Off                                          ' Block 0 (000800-001FFF) not write-protected
    WRT1 = Off                                          ' Block 1 (002000-003FFF) not write-protected
    WRTC = Off                                          ' Configuration registers (300000-30000B) not write-protected
    WRTB = Off                                          ' Boot Block (000000-0007FF) write-protected
    WRTD = Off                                          ' Data EEPROM not write-protected
    Cp = Off                                            ' UserNVM code protection disabled
    CPD = Off                                           ' DataNVM code protection disabled
    EBTR0 = Off                                         ' Block 0 (000800-001FFF) not protected from table reads executed in other blocks
    EBTR1 = Off                                         ' Block 1 (002000-003FFF) not protected from table reads executed in other blocks
    EBTRB = Off                                         ' Boot Block (000000-0007FF) not protected from table reads executed in other blocks
Config_End

The "HPWM_K40.inc" library's code listing is shown below:

$ifndef _HPWM_K40_INC_
$define _HPWM_K40_INC_
'
'   /\\\\\\\\\
'  /\\\///////\\\
'  \/\\\     \/\\\                                                 /\\\          /\\\
'   \/\\\\\\\\\\\/        /\\\\\     /\\\\\\\\\\     /\\\\\\\\   /\\\\\\\\\\\  /\\\\\\\\\\\  /\\\\\\\\\
'    \/\\\//////\\\      /\\\///\\\  \/\\\//////    /\\\/////\\\ \////\\\////  \////\\\////  \////////\\\
'     \/\\\    \//\\\    /\\\  \//\\\ \/\\\\\\\\\\  /\\\\\\\\\\\     \/\\\         \/\\\        /\\\\\\\\\\
'      \/\\\     \//\\\  \//\\\  /\\\  \////////\\\ \//\\///////      \/\\\ /\\     \/\\\ /\\   /\\\/////\\\
'       \/\\\      \//\\\  \///\\\\\/    /\\\\\\\\\\  \//\\\\\\\\\\    \//\\\\\      \//\\\\\   \//\\\\\\\\/\\
'        \///        \///     \/////     \//////////    \//////////      \/////        \/////     \////////\//
'                                  Let's find out together what makes a PIC Tick!
'
' Hardware PWM library for the PIC18FxxK40 devices.
' The library uses the CCP and the PWM peripherals, and allows them to use three timers independently.
' Timer2 and Timer4 and Timer6 are available for use with the CCP and PWM peripherals.
'
' This means, three independent frequency waveforms can be generated.
'
' It also allows frequencies up to 1.2MHz.
' Low end frequencies and high end frequencies depend on the oscillator value used.
' Frequencies may be approximate depending on the oscillator value used and the frequency required.
' This is not due to a limitation of the compiler, but of the CCP and HPWM peripherals and the device's operating frequency.
'
' Note that the library uses the, still experimental, "Shared" variable directive, so that some global variables can be re-declared and re-used if they are the same type,
' without generating a "Variable Already Declared", type error message.
'
' Written for the Positron8 BASIC Compiler, version 4.0.3.4 onwards, by Les Johnson.
'
$if (_device <> _18F26K40) And (_device <> _18F27K40) And (_device <> _18F46K40) And (_device <> _18F47K40)
    $error "The HPWM_K40 library is only tested on 18F26K40 or 18F27K40 or 18F46K40 or 18F47K40 devices"
$endif
'-------------------------------------
' Procedures:
' HPWM1(pTimer, pFrequency, pDuty)
' HPWM2(pTimer, pFrequency, pDuty)
' HPWM3(pTimer, pFrequency, pDuty)
' HPWM4(pTimer, pFrequency, pDuty)
'
' Alters the Timer used, and the frequency of a PWM waveform, and the duty cycle then starts it.
'
' pTimer chooses the timer to use for the PWM clock: 2 is Timer2, 4 is Timer4 and 6 is Timer6
' pFrequency alters the frequency (in Hz) of a CCP or PWM peripheral and can be any value from 0 to 1000000,
' however, lower or upper frequencies depend on the microcontroller's oscillator.
' pDuty alters the duty cycle of a PWM waveform and can be any value from 0 to 255.
'
' On completion, the Carry flag (STATUSbits_C) will hold 1 if the frequency is achievable.
' Use this as a guide indication only and not as an absolute indicator.
'
'-------------------------------------
' Procedures:
' HPWM1_Duty(pDuty)
' HPWM2_Duty(pDuty)
' HPWM3_Duty(pDuty)
' HPWM4_Duty(pDuty)
'
' Alters the duty of the PWM waveform
'
' pDuty alters the duty cycle of a waveform and can be any value from 0 to 255.
'
'-------------------------------------
' Procedures:
' HPWM1_Start()
' HPWM2_Start()
' HPWM3_Start()
' HPWM4_Start()
'
' Enables a PWM peripheral
'
'-------------------------------------
' Procedures:
' HPWM1_Stop()
' HPWM2_Stop()
' HPWM3_Stop()
' HPWM4_Stop()
'
' Disables a PWM peripheral
'
'-------------------------------------------------------------------------------------------------------

    Declare Onboard_HPWM = 4                            ' Set the device to have 4 PWM channels (regardless what is in the device's .PPI file)

'-------------------------------------------------------------------------------------------------------
'
' Create some PPS defines to make the CCP types look like PWM types
'
$define PPS_Fn_PWM1 PPS_Fn_CCP1
$define PPS_Fn_PWM2 PPS_Fn_CCP2
'
' Create defines for the valid timers to use for the PWM generators
'
$define HPWM_cTimer2 2                                          ' Use Timer2 for the PWM channel's timer
$define HPWM_cTimer4 4                                          ' Use Timer4 for the PWM channel's timer
$define HPWM_cTimer6 6                                          ' Use Timer6 for the PWM channel's timer

$define HPWM_cFosc $eval (_xtal * 1000)                         ' Calculate the oscillator frequency in MHz
'
' Create global variables used by the library routines
'
    Dim HPWM_dCalcDuty       As Dword                           ' Used to calculate the duty cycle value
    Dim HPWM_wCalcDuty       As HPWM_dCalcDuty.Word0            ' Used to pass the duty cycle value to other procedures
    Dim HPWM_wTxCON_Addr     As Word                            ' Global shared variable used to hold the address of a timer's TxCON SFR. i.e. T2CON, T4CON or T6CON
    Dim HPWM_wPR_Addr        As Word                            ' Global shared variable used to hold the address of a timer's PR SFR. i.e. T2PR, T4PR or T6PR

'-----------------------------------------------------------------------------
' A helper routine to calculate a duty cycle required for a given PWM frequency
' Input     : HPWM_dFrequency holds the 32-bit frequency of the PWM
'           : HPWM_bPrescaleSelect holds the 8-bit prescale value
'           : HPWM_bDuty holds the 8-bit duty cycle (0 to 255)
' Output    : HPWM_wCalcDuty holds the 16-bit duty cycle value
' Notes     : None
'

Proc HPWM_hCalculateDuty()
Global Dim HPWM_dFrequency As Dword Shared                      ' Global shared variable used to pass the 32-bit frequency value as a parameter
Global Dim HPWM_bDuty      As Byte Shared                       ' Global shared variable used to pass the 8-bit duty cycle as a parameter
Global Dim HPWM_bPrescaleSelect As Byte Shared                  ' Global shared variable used to Hold the timer's prescaler chosen for a particular frequency
    Dim dMaxDuty As Dword

    HPWM_dCalcDuty = HPWM_dFrequency * HPWM_bPrescaleSelect     ' \
    HPWM_wCalcDuty = HPWM_cFosc / HPWM_dCalcDuty                ' / Determine the maximum duty
    dMaxDuty       = HPWM_wCalcDuty * HPWM_bDuty                ' \
    HPWM_wCalcDuty = dMaxDuty / 256                             ' / Calculate the duty value
EndProc

'-----------------------------------------------------------------------------
' A helper routine to calculate the values required for a given timer, frequency and duty cycle
' Input     : pFrequency holds the 32-bit frequency (in Hz)
'           : Global variable HPWM_bDuty holds the 8-bit duty cycle (0 to 255)
'           : Global Variable HPWM_wTxCON_Addr holds the address of the timer's TxCON SFR. i.e. T2CON, T4CON or T6CON
'           : Global Variable HPWM_wPR_Addr holds the address of the timer's PR SFR. i.e. T2PR, T4PR or T6PR
' Output    : Returns 1 if the frequency is possibly achievable, Otherwise, returns a 0
'           : Global variable HPWM_bPrescaleSelect holds the timer's prescaler value for the required frequency
' Notes     : None
'
Proc HPWM_hCalcFreq(pFrequency As HPWM_dFrequency), STATUSbits_C
Global Dim HPWM_dFrequency As Dword Shared                      ' Global shared variable used to pass the 32-bit frequency value as a parameter
Global Dim HPWM_bDuty      As Byte Shared                       ' Global shared variable used to pass the 8-bit duty cycle as a parameter
Global Dim HPWM_bPrescaleSelect As Byte Shared                  ' Global shared variable used to hold the timer's prescaler chosen for a particular frequency
    Dim wPRxTemp   As Word                                      ' A temporary variable for calculations
    Dim bTConValue As Byte                                      ' Holds the value to place into a timer's TxCON SFR
    Dim wPRxValue  As Word                                      ' Holds the value that is placed into a timer's PR SFR

    pFrequency = pFrequency / 1000                              ' \
    wPRxTemp = HPWM_cFosc / pFrequency                          ' | Calculate the value for the timer's PRx SFR
    wPRxTemp = wPRxTemp / 4                                     ' /
'
' Loop through all the valid prescalers
'
    HPWM_bPrescaleSelect = 1
    Repeat
        wPRxValue = wPRxTemp / HPWM_bPrescaleSelect             ' \
        wPRxValue = wPRxValue - 1                               ' / Calculate a timer's PRx value
        If wPRxValue <= 255 Then                                ' \ Is the value to place into the timer's PRx valid?
            If wPRxValue > 1 Then                               ' /
                bTConValue = %10000000                          ' Yes. So Default to TxCON loaded with 0 (1:1 Prescaler) and Enable the timer
                If HPWM_bPrescaleSelect = 2 Then                ' Is HPWM_bPrescaleSelect 2?
                    bTConValue = %10010000                      ' Yes. So load bTConValue for a 1:2 Prescaler and Enable the timer
                ElseIf HPWM_bPrescaleSelect = 4 Then            ' Is HPWM_bPrescaleSelect 4?
                    bTConValue = %10100000                      ' Yes. So load bTConValue for a 1:4 Prescaler and Enable the timer
                Else If HPWM_bPrescaleSelect = 8 Then           ' Is HPWM_bPrescaleSelect 8?
                    bTConValue = %10110000                      ' Yes. So load bTConValue for a 1:8 Prescaler and Enable the timer
                Else If HPWM_bPrescaleSelect = 16 Then          ' Is HPWM_bPrescaleSelect 16?
                    bTConValue = %11000000                      ' Yes. So load bTConValue for a 1:16 Prescaler and Enable the timer
                Else If HPWM_bPrescaleSelect = 32 Then          ' Is HPWM_bPrescaleSelect 32?
                    bTConValue = %11010000                      ' Yes. So load bTConValue for a 1:32 Prescaler and Enable the timer
                Else If HPWM_bPrescaleSelect = 64 Then          ' Is HPWM_bPrescaleSelect 64?
                    bTConValue = %11100000                      ' Yes. So load bTConValue for a 1:64 Prescaler and Enable the timer
                Else If HPWM_bPrescaleSelect = 128 Then         ' Is HPWM_bPrescaleSelect 128?
                    bTConValue = %11110000                      ' Yes. So adjust TxCON for a 1:128 Prescaler and Enable the timer
                EndIf
                HPWM_hCalculateDuty()                           ' Calculate the duty value based upon global variable HPWM_bDuty

                Ptr8(HPWM_wPR_Addr)    = wPRxValue.Byte0        ' Load the appropriate timer's PRx register. T2PR, T4PR or T6PR
                Ptr8(HPWM_wTxCON_Addr) = bTConValue             ' Load the appropriate timer's TxCON register

                Result = 1                                      ' Return the Carry flag holding 1 if successful
                ExitProc                                        ' Exit the procedure
            EndIf
        EndIf
        HPWM_bPrescaleSelect = HPWM_bPrescaleSelect * 2         ' Move to the prescaler select value
    Until HPWM_bPrescaleSelect > 128
    Result = 0                                                  ' Return the Carry flag holding 0 if unsuccesful
EndProc

'-----------------------------------------------------------------------------
' Select the Timer to use for a CCP peripheral operating as PWM, or a PWM peripheral
' Input     : pChannel holds the PWM channel (1 to 8)
'           : pTimer holds the timer to use: HPWM_cTimer2, HPWM_cTimer4 or HPWM_cTimer6
' Output    : HPWM_wTxCON_Addr holds the address of the timer's TxCON SFR. i.e. T2CON, T4CON or T6CON
'           : HPWM_wPR_Addr holds the address of the timer's PR SFR. i.e. T2PR, T4PR or T6PR
' Notes     : The timer chosen has its clock set to fOsc/4
'
Proc HPWM_SelectTimer(pChannel As Byte, pTimer As HPWM_bTimerReq)
Global Dim HPWM_bTimerReq As Byte Shared                        ' Global shared variable used to pass the timer required as a parameter
    Dim bCCPT_BitsValue As Byte                                 ' Holds the bit values required for a particular timer

    If pChannel = 0 Then ExitProc

    If pTimer = HPWM_cTimer2 Then                               ' Is Timer2 being used for the PWM?
        bCCPT_BitsValue.0 = 1                                   ' \ Yes. So select Timer2
        bCCPT_BitsValue.1 = 0                                   ' /
        T2CLK = %00000001                                       ' T2CS is fOsc/4
        HPWM_wTxCON_Addr = AddressOf(T2CON)                     ' Load the address of T2CON into HPWM_wTxCON_Addr
        HPWM_wPR_Addr    = AddressOf(T2PR)                      ' Load the address of T2PR into HPWM_wPR_Addr

    ElseIf pTimer = HPWM_cTimer4 Then                           ' Is Timer4 being used for the PWM?
        bCCPT_BitsValue.0 = 0                                   ' \ Yes. So select Timer4
        bCCPT_BitsValue.1 = 1                                   ' /
        T4CLK = %00000001                                       ' T4CS is fOsc/4
        HPWM_wTxCON_Addr = AddressOf(T4CON)                     ' Load the address of T4CON into HPWM_wTxCON_Addr
        HPWM_wPR_Addr    = AddressOf(T4PR)                      ' Load the address of T4PR into HPWM_wPR_Addr

    ElseIf pTimer = HPWM_cTimer6 Then                           ' Is Timer6 being used for the PWM?
        bCCPT_BitsValue.0 = 1                                   ' \ Yes. So select Timer6
        bCCPT_BitsValue.1 = 1                                   ' /
        T6CLK = %00000001                                       ' T6CS is fOsc/4
        HPWM_wTxCON_Addr = AddressOf(T6CON)                     ' Load the address of T6CON into HPWM_wTxCON_Addr
        HPWM_wPR_Addr    = AddressOf(T6PR)                      ' Load the address of T6PR into HPWM_wPR_Addr
    Else                                                        ' Otherwise... Not a valid timer
        ExitProc                                                ' So exit the procedure
    EndIf

    If pChannel = 1 Then
        CCPTMRSbits_C1TSEL0 = bCCPT_BitsValue.0                  ' \ Select the timer for the CCP1 peripheral acting as PWM
        CCPTMRSbits_C1TSEL1 = bCCPT_BitsValue.1                  ' /

    ElseIf pChannel = 2 Then
        CCPTMRSbits_C2TSEL0 = bCCPT_BitsValue.0                  ' \ Select the timer for the CCP2 peripheral acting as PWM
        CCPTMRSbits_C2TSEL1 = bCCPT_BitsValue.1                  ' /

    ElseIf pChannel = 3 Then
        CCPTMRSbits_P3TSEL0 = bCCPT_BitsValue.0                  ' \ Select the timer for the PWM3 peripheral
        CCPTMRSbits_P3TSEL1 = bCCPT_BitsValue.1                  ' /

    ElseIf pChannel = 4 Then
        CCPTMRSbits_P4TSEL0 = bCCPT_BitsValue.0                  ' \ Select the timer for the PWM4 peripheral
        CCPTMRSbits_P4TSEL1 = bCCPT_BitsValue.1                  ' /
    EndIf
EndProc

'-----------------------------------------------------------------------------
' Set the frequency of all the PWM channels
' Input     : pFrequency holds the 32-bit frequency (in Hz)
' Output    : Returns 1 if the frequency is possibly achievable, Otherwise, returns a 0
' Notes     : Uses Timer2 for the clock source for the CCP and PWM peripherals
'
Proc HPWM_SetFreq(pFrequency As HPWM_dFrequency), STATUSbits_C
Global Dim HPWM_dFrequency As Dword Shared                      ' Global shared variable used to pass the 32-bit frequency value as a parameter
Global Dim HPWM_bDuty      As Byte Shared                       ' Global shared variable used to pass the 8-bit duty cycle as a parameter
Global Dim HPWM_bPrescaleSelect As Byte Shared                  ' Global shared variable used to Hold the timer's prescaler chosen for a particular frequency
    Dim wPRxTemp  As Word
    Dim wPRxValue As Word

    HPWM_bDuty = 0                                              ' Set the duty value to 0
    T2CLK = %00000001                                           ' T2CS is FOSC/4
    CCPTMRS = %01010101                                         ' Select Timer2 for the PWM and CCP peripherals acting as PWM

    pFrequency = pFrequency / 1000                              ' \
    wPRxTemp = HPWM_cFosc / pFrequency                          ' | Calculate the value for the timer's PRx SFR
    wPRxTemp = wPRxTemp / 4                                     ' /
'
' Loop through all the valid prescalers
'
    HPWM_bPrescaleSelect = 1
    Repeat
        wPRxValue = wPRxTemp / HPWM_bPrescaleSelect             ' \
        wPRxValue = wPRxValue - 1                               ' / Calculate a PR2 value
        If wPRxValue <= 255 Then                                ' \ Is the value to place into PR2 valid?
            If wPRxValue > 1 Then                               ' /
                T2CON = %10000000                               ' Yes. So Default to T2CON loaded with 0 (1:1 Prescaler)
                If HPWM_bPrescaleSelect = 2 Then                ' Is HPWM_bPrescaleSelect 2?
                    T2CON = %10010000                           ' Yes. So adjust T2CON for a 1:2 Prescaler
                ElseIf HPWM_bPrescaleSelect = 4 Then            ' Is HPWM_bPrescaleSelect 4?
                    T2CON = %10100000                           ' Yes. So adjust T2CON for a 1:4 Prescaler
                Else If HPWM_bPrescaleSelect = 8 Then           ' Is HPWM_bPrescaleSelect 8?
                    T2CON = %10110000                           ' Yes. So adjust T2CON for a 1:8 Prescaler
                Else If HPWM_bPrescaleSelect = 16 Then          ' Is HPWM_bPrescaleSelect 16?
                    T2CON = %11000000                           ' Yes. So adjust T2CON for a 1:16 Prescaler
                Else If HPWM_bPrescaleSelect = 32 Then          ' Is HPWM_bPrescaleSelect 32?
                    T2CON = %11010000                           ' Yes. So adjust T2CON for a 1:32 Prescaler
                Else If HPWM_bPrescaleSelect = 64 Then          ' Is HPWM_bPrescaleSelect 64?
                    T2CON = %01100000                           ' Yes. So adjust T2CON for a 1:64 Prescaler
                Else If HPWM_bPrescaleSelect = 128 Then         ' Is HPWM_bPrescaleSelect 128?
                    T2CON = %11110000                           ' Yes. So adjust T2CON for a 1:128 Prescaler
                EndIf
                T2PR = wPRxValue                                ' Load T2PR
                HPWM_hCalculateDuty()                           ' Calculate the duty value based upon global variable HPWM_bDuty
                Result = 1                                      ' Return Carry flag holding 1 if successful
                ExitProc                                        ' Exit the procedure
            EndIf
        EndIf
        HPWM_bPrescaleSelect = HPWM_bPrescaleSelect * 2         ' Move to the prescaler select value
    Until HPWM_bPrescaleSelect > 128
    Result = 0                                                  ' Return Carry flag holding 0 if unsuccesful
EndProc

'*****************************************************************************
$ifdef _CCP1CON
'-----------------------------------------------------------------------------
' Start CCP1 operation
' Input     : None
' Output    : None
' Notes     : None
'
Proc HPWM1_Start()
    CCP1CON = %10001100
#ifSym __CCP1_PIN
    PinOutput __CCP1_PORT_PIN
#endIfSym
EndProc

'-----------------------------------------------------------------------------
' Stop CCP1 operation
' Input     : None
' Output    : None
' Notes     : None
'
Proc HPWM1_Stop()
#ifSym __CCP1_PIN
    PinInput __CCP1_PORT_PIN
#endIfSym
   CCP1CON = %00001100
EndProc

'-----------------------------------------------------------------------------
' Helper routine to load the duty cycle value into the CCPR1L and CCPR1H SFRs
' Input     : HPWM_wCalcDuty holds the duty cycle value
' Output    : None
' Notes     : None
'
$define HPWM1_hSetDuty()                                        '
    HPWM_wCalcDuty.Byte1 = HPWM_wCalcDuty.Byte1 & %00000011     '
    CCPR1H = HPWM_wCalcDuty.Byte1                               '
    CCPR1L = HPWM_wCalcDuty.Byte0

'-----------------------------------------------------------------------------
' Adjust the duty cycle of CCP1
' Input     : pDuty (HPWM_bDuty) holds the 8-bit duty cycle (0 to 255)
' Output    : None
' Notes     : None
'
Proc HPWM1_Duty(pDuty As HPWM_bDuty)
Global Dim HPWM_bDuty As Byte Shared                            ' Global shared variable used to pass the 8-bit duty cycle as a parameter

    HPWM_hCalculateDuty()                                       ' Calculate the duty value into HPWM_wCalcDuty
    HPWM1_hSetDuty()                                            ' Set the duty cycle of the waveform
EndProc

'-----------------------------------------------------------------------------
' Calculate the values required for a required frequency and duty cycle for CCP1
' Input     : pFrequency (HPWM_dFrequency) holds the 32-bit frequency (in Hz)
'           : pDuty (HPWM_bDuty) holds the 8-bit duty cycle (0 to 255)
' Output    : Returns 1 if the frequency is possibly achievable, Otherwise, returns a 0
' Notes     : Also starts the PWM for CCP1
'
Proc HPWM1(pTimer As HPWM_bTimerReq, pFrequency As HPWM_dFrequency, pDuty As HPWM_bDuty), STATUSbits_C
Global Dim HPWM_dFrequency As Dword Shared                      ' Global shared variable used to pass the 32-bit frequency value as a parameter
Global Dim HPWM_bDuty      As Byte Shared                       ' Global shared variable used to pass the 8-bit duty cycle as a parameter
Global Dim HPWM_bTimerReq  As Byte Shared                       ' Global shared variable used to pass the timer required as a parameter

    HPWM_SelectTimer(1, pTimer)                                 ' Choose which timer to use for the channel
    HPWM_hCalcFreq(pFrequency)                                  ' Calculate the values for a given frequency and duty cycle
    If STATUSbits_C = 1 Then                                    ' Was the frequency accepted?
        HPWM1_hSetDuty()                                        ' Yes. So load the CCP1 registers with the appropriate value
        HPWM1_Start()                                           ' Start CCP1
        Result = 1                                              ' Indicate everything OK
    EndIf
EndProc

'-----------------------------------------------------------------------------
' HPWM1 polarity
'
$define HPWM1_PolInvert() $error "Cannot invert the polarity of the CCP1 peripheral operating as PWM. Only suitable with the PWM3 and PWM4 peripherals"
$define HPWM1_PolNormal() HPWM_tPolarity1 = 0                   ' Set the polarity to normal

$endif ' _CCP1CON

'*****************************************************************************
$ifdef _CCP2CON
'-----------------------------------------------------------------------------
' Start CCP2 operation
' Input     : None
' Output    : None
' Notes     : None
'
Proc HPWM2_Start()
    CCP2CON = %10001100                                     ' So. Clear the FMT bit
#ifSym __CCP2_PIN
    PinOutput __CCP2_PORT_PIN
#endIfSym
EndProc

'-----------------------------------------------------------------------------
' Stop CCP2 operation
' Input     : None
' Output    : None
' Notes     : None
'
Proc HPWM2_Stop()
#ifSym __CCP2_PIN
    PinInput __CCP2_PORT_PIN
#endIfSym
   CCP2CON = %00001100
EndProc

'-----------------------------------------------------------------------------
' Helper routine to load the duty cycle value into the CCPR2L and CCPR2H SFRs
' Input     : HPWM_dCalcDuty holds the duty cycle value
' Output    : None
' Notes     : None
'
$define HPWM2_hSetDuty()                                    '
    HPWM_wCalcDuty.Byte1 = HPWM_wCalcDuty.Byte1 & %00000011 '
    CCPR2H = HPWM_wCalcDuty.Byte1                           '
    CCPR2L = HPWM_wCalcDuty.Byte0

'-----------------------------------------------------------------------------
' Adjust the duty cycle of CCP2
' Input     : pDuty (HPWM_bDuty) holds the 8-bit duty cycle (0 to 255)
' Output    : None
' Notes     : None
'
Proc HPWM2_Duty(pDuty As HPWM_bDuty)
Global Dim HPWM_bDuty As Byte Shared                            ' Global shared variable used to pass the 8-bit duty cycle as a parameter

    HPWM_hCalculateDuty()                                       ' Calculate the duty value into HPWM_wCalcDuty
    HPWM2_hSetDuty()                                            ' Set the duty cycle of the waveform
EndProc

'-----------------------------------------------------------------------------
' Calculate the values required for a required frequency and duty cycle for CCP2
' Input     : pFrequency holds the 32-bit frequency (in Hz)
'           : pDuty (HPWM_bDuty) holds the 8-bit duty cycle (0 to 255)
' Output    : Returns 1 if the frequency is possibly achievable, Otherwise, returns a 0
' Notes     : Also starts the PWM for CCP2
'
Proc HPWM2(pTimer As HPWM_bTimerReq, pFrequency As HPWM_dFrequency, pDuty As HPWM_bDuty), STATUSbits_C
Global Dim HPWM_dFrequency As Dword Shared                      ' Global shared variable used to pass the 32-bit frequency value as a parameter
Global Dim HPWM_bDuty      As Byte Shared                       ' Global shared variable used to pass the 8-bit duty cycle as a parameter
Global Dim HPWM_bTimerReq  As Byte Shared                       ' Global shared variable used to pass the timer required as a parameter

    HPWM_SelectTimer(2, pTimer)                                 ' Choose which timer to use for the channel
    HPWM_hCalcFreq(pFrequency)                                  ' Calculate the values for a given frequency and duty cycle
    If STATUSbits_C = 1 Then                                    ' Was the frequency accepted?
        HPWM2_hSetDuty()                                        ' Yes. So load the CCP2 registers with the appropriate value
        HPWM2_Start()                                           ' Start CCP2
        Result = 1                                              ' Indicate everything OK
    EndIf
EndProc

'------------------------------------------------------------------------
$define HPWM2_PolInvert() $error "Cannot invert the polarity of the CCP2 peripheral operating as PWM. Only suitable with the PWM3 and PWM4 peripherals"
$define HPWM2_PolNormal() CCP2CONbits_FMT = 0                   ' Set the HPWM2 polarity to normal

$endif ' _CCP2CON

'*****************************************************************************
$ifdef _PWM3CON
    Dim HPWM_tPolarity3 As Bit = 0                              ' Holds 1 if the PWM3 waveform is to be inverted

'-----------------------------------------------------------------------------
' Start PWM3 operation
' Input     : None
' Output    : None
' Notes     : None
'
Proc HPWM3_Start()
    If HPWM_tPolarity3 = 1 Then                                 ' Is the PWM to be inverted?
        PWM3CON = %10010000                                     ' Yes. So set the PWM3POL bit
    Else                                                        ' Otherwise... Not inverted...
        PWM3CON = %10000000                                     ' So... Clear the PWM3POL bit
    EndIf
#ifSym __HPWM3_PIN
    PinOutput __HPWM3_PORT_PIN
#endIfSym
EndProc

'-----------------------------------------------------------------------------
' Stop PWM3 operation
' Input     : None
' Output    : None
' Notes     : None
'
Proc HPWM3_Stop()
#ifSym __HPWM3_PIN
   PinInput __HPWM3_PORT_PIN
#endIfSym
   PWM3CON = %00000000
EndProc

'-----------------------------------------------------------------------------
' Helper routine to load the duty cycle value into the PWM3DCL and PWM3DCH SFRs
' Input     : HPWM_wCalcDuty holds the duty cycle value
' Output    : None
' Notes     : None
'
Proc HPWM3_hSetDuty()
    WREG = 0                                                    ' Clear WREG before the shifts
    Ror HPWM_wCalcDuty                                          ' Rotate the Least Significant bit into the Carry flag
    Ror WREG                                                    ' Rotate the Carry flag into the Most Significant bit
    Ror HPWM_wCalcDuty                                          ' Rotate the Least Significant bit into the Carry flag
    Ror WREG                                                    ' Rotate the Carry flag into the Most Significant bit
    PWM3DCL = WREG                                              ' Place WREG into the low duty SFR
    PWM3DCH = HPWM_wCalcDuty.Byte0                              ' Place HPWM_wCalcDuty.Byte0 into the high duty SFR
EndProc

'-----------------------------------------------------------------------------
' Adjust the duty cycle of PWM3
' Input     : pDuty (HPWM_bDuty) holds the 8-bit duty cycle (0 to 255)
' Output    : None
' Notes     : None
'
Proc HPWM3_Duty(pDuty As HPWM_bDuty)
Global Dim HPWM_bDuty As Byte Shared                            ' Global shared variable used to pass the 8-bit duty cycle as a parameter

    HPWM_hCalculateDuty()                                       ' Calculate the duty value into HPWM_wCalcDuty
    HPWM3_hSetDuty()                                            ' Set the duty cycle of the waveform
EndProc

'-----------------------------------------------------------------------------
' Calculate the values required for a required frequency and duty cycle for PWM3
' Input     : pFrequency holds the 16-bit frequency (in Hz)
'           : pDuty (HPWM_bDuty) holds the 8-bit duty cycle (0 to 255)
' Output    : Returns 1 if the frequency is possibly achievable, otherwise, returns a 0
' Notes     : Also starts the PWM for PWM3
'
Proc HPWM3(pTimer As HPWM_bTimerReq, pFrequency As HPWM_dFrequency, pDuty As HPWM_bDuty), STATUSbits_C
Global Dim HPWM_dFrequency As Dword Shared                      ' Global shared variable used to pass the 32-bit frequency value as a parameter
Global Dim HPWM_bDuty      As Byte Shared                       ' Global shared variable used to pass the 8-bit duty cycle as a parameter
Global Dim HPWM_bTimerReq  As Byte Shared                       ' Global shared variable used to pass the timer required as a parameter

    HPWM_SelectTimer(3, pTimer)                                 ' Choose which timer to use for the channel
    HPWM_hCalcFreq(pFrequency)                                  ' Calculate the values for a given frequency and duty cycle
    If STATUSbits_C = 1 Then                                    ' Was the frequency accepted?
        HPWM3_hSetDuty()                                        ' Yes. So load the PWM3DC registers with the appropriate value
        HPWM3_Start()                                           ' Start PWM3
        Result = 1                                              ' Indicate everything OK
    EndIf
EndProc

'-----------------------------------------------------------------------------
' Invert the polarity for HPWM3
' Input     : None
' Output    : HPWM_tPolarity3 holds 1 for inverted
' Notes     : Also sets the POL bit of PWM3CON
'
Proc HPWM3_PolInvert()
    PWM3CONbits_POL = 1
    HPWM_tPolarity3 = 1
EndProc

'-----------------------------------------------------------------------------
' Do not Invert the polarity for HPWM3
' Input     : None
' Output    : HPWM_tPolarity3 holds 0 for not inverted
' Notes     : Also clears the POL bit of PWM3CON
'
Proc HPWM3_PolNormal()
    PWM3CONbits_POL = 0
    HPWM_tPolarity3 = 0
EndProc

$endif  ' _PWM3CON

'*****************************************************************************
$ifdef _PWM4CON
    Dim HPWM_tPolarity4 As Bit = 0                              ' Holds 1 if the PWM4 waveform is to be inverted
'-----------------------------------------------------------------------------
' Start PWM4 operation
' Input     : None
' Output    : None
' Notes     : None
'
Proc HPWM4_Start()
    If HPWM_tPolarity4 = 1 Then                                 ' Is the PWM to be inverted?
        PWM4CON = %10010000                                     ' Yes. So set the PWM4POL bit
    Else                                                        ' Otherwise... Not inverted...
        PWM4CON = %10000000                                     ' So... Clear the PWM4POL bit
    EndIf
#ifSym __HPWM4_PIN
    PinOutput __HPWM4_PORT_PIN
#endIfSym
EndProc

'-----------------------------------------------------------------------------
' Stop PWM4 operation
' Input     : None
' Output    : None
' Notes     : None
'
Proc HPWM4_Stop()
#ifSym __HPWM4_PIN
   PinInput __HPWM4_PORT_PIN
#endIfSym
   PWM4CON = %00000000
EndProc

'-----------------------------------------------------------------------------
' Helper routine to load the duty cycle value into the PWM4DCL and PWM4DCH SFRs
' Input     : HPWM_wCalcDuty holds the duty cycle value
' Output    : None
' Notes     : None
'
Proc HPWM4_hSetDuty()
    WREG = 0                                                    ' Clear WREG before the shifts
    Ror HPWM_wCalcDuty                                          ' Rotate the Least Significant bit into the Carry flag
    Ror WREG                                                    ' Rotate the Carry flag into the Most Significant bit
    Ror HPWM_wCalcDuty                                          ' Rotate the Least Significant bit into the Carry flag
    Ror WREG                                                    ' Rotate the Carry flag into the Most Significant bit
    PWM4DCL = WREG                                              ' Place WREG into the low duty SFR
    PWM4DCH = HPWM_wCalcDuty.Byte0                              ' Place HPWM_wCalcDuty.Byte0 into the high duty SFR
EndProc

'-----------------------------------------------------------------------------
' Adjust the duty cycle of PWM4
' Input     : pDuty (HPWM_bDuty) holds the 8-bit duty cycle (0 to 255)
' Output    : None
' Notes     : None
'
Proc HPWM4_Duty(pDuty As HPWM_bDuty)
Global Dim HPWM_bDuty As Byte Shared                            ' Global shared variable used to pass the 8-bit duty cycle as a parameter

    HPWM_hCalculateDuty()                                       ' Calculate the duty value into HPWM_wCalcDuty
    HPWM4_hSetDuty()                                            ' Set the duty cycle of the waveform
EndProc

'-----------------------------------------------------------------------------
' Calculate the values required for a required frequency and duty cycle for PWM4
' Input     : pFrequency holds the 32-bit frequency (in Hz)
'           : pDuty (HPWM_bDuty) holds the 8-bit duty cycle (0 to 255)
' Output    : Returns 1 if the frequency is possibly achievable, otherwise, returns a 0
' Notes     : Also starts the PWM for PWM4
'
Proc HPWM4(pTimer As HPWM_bTimerReq, pFrequency As HPWM_dFrequency, pDuty As HPWM_bDuty), STATUSbits_C
Global Dim HPWM_dFrequency As Dword Shared                      ' Global shared variable used to pass the 32-bit frequency value as a parameter
Global Dim HPWM_bDuty      As Byte Shared                       ' Global shared variable used to pass the 8-bit duty cycle as a parameter
Global Dim HPWM_bTimerReq  As Byte Shared                       ' Global shared variable used to pass the timer required as a parameter

    HPWM_SelectTimer(4, pTimer)                                 ' Choose which timer to use for the channel
    HPWM_hCalcFreq(pFrequency)                                  ' Calculate the values for a given frequency and duty cycle
    If STATUSbits_C = 1 Then                                    ' Was the frequency accepted?
        HPWM4_hSetDuty()                                        ' Yes. So load the PWM4DC registers with the appropriate value
        HPWM4_Start()                                           ' Start PWM4
        Result = 1                                              ' Indicate everything OK
    EndIf
EndProc

'-----------------------------------------------------------------------------
' Invert the polarity for HPWM4
' Input     : None
' Output    : HPWM_tPolarity4 holds 1 for inverted
' Notes     : Also sets the POL bit of PWM4CON
'
Proc HPWM4_PolInvert()
    PWM4CONbits_POL = 1
    HPWM_tPolarity4 = 1
EndProc

'-----------------------------------------------------------------------------
' Do not Invert the polarity for HPWM4
' Input     : None
' Output    : HPWM_tPolarity4 holds 0 for not inverted
' Notes     : Also clears the POL bit of PWM4CON
'
Proc HPWM4_PolNormal()
    PWM4CONbits_POL = 0
    HPWM_tPolarity4 = 0
EndProc

$endif  ' _PWM4CON

$endif  ' _HPWM_K40_INC_

In order to have all programs see the "HPWM_K40.inc" library file, place it in the "PDS\Includes\" folder, which is located at: "C:\Users\User Name\PDS\Includes\", and is created when the compilers are installed, and contains many more library include files.

The library code's mechanism can be changed for a multitude of PIC devices that have CCP or PWM peripherals, or both. And the changes required will normally be SFR names and bit names, and positions of the bits within the SFRs, because microchip continuously change things on devices, but the frequency and duty cycle calculations should be re-usable, as long as the PIC device chosen has the appropriate timers and the timer prescalers.

The library file and three demo programs are attached below in a zip file named "HPWM_Library_18F26K40.zip", and the zip file also has the Proteus simulation project in it. A screenshot of the demo program listed above, is shown below:
18FxxK40_HPWM_Library_Screenshot.jpg


Craig

Thank you Les, that is going to come in very handy in the future.