Positron8 - Low Frequency, Interrupt Driven PWM Generator

Started by top204, Today at 10:42 AM

Previous topic - Next topic

top204

When using the CCP or PWM peripherals for generating a very low frequency PWM signal, I have found them to have a limit on the frequency that they will drop too, however, there are bound to be some methods to do it in the newer devices with the CLC acting as a divider, or the NCO operating at a low frequency etc.... However, whenever I have needed a very slow PWM signal, I have reverted back to the good old pin toggling within an interrupt method, and it works everytime, and on all devices that have an interrupt capability, and with all device operating frequencies.

Below is a code listing I created, quite, a few years ago for a PIC18F25K20 device, that generates a PWM signal in the 10s to 100s of Hz range, and uses a simple 'Special Event' interrupt based upon the CCP2 and Timer3 peripherals. However, these can be changed to suit a project's requirements, and can also be changed for other devices, because it is a standard interrupt method.

It can also be accomplished with a, simple, standard Timer overflow interrupt, but at the time of writing the code, I was showing a user how to implement a 'Special Event' interrupt. I've used the standard Timer overflow method of PWM generation a few times for altering the voltage to LEDs, when the CCP and PWM peripherals were busy being used as an audio DAC.

'
'   /\\\\\\\\\
'  /\\\///////\\\
'  \/\\\     \/\\\                                                 /\\\          /\\\
'   \/\\\\\\\\\\\/        /\\\\\     /\\\\\\\\\\     /\\\\\\\\   /\\\\\\\\\\\  /\\\\\\\\\\\  /\\\\\\\\\
'    \/\\\//////\\\      /\\\///\\\  \/\\\//////    /\\\/////\\\ \////\\\////  \////\\\////  \////////\\\
'     \/\\\    \//\\\    /\\\  \//\\\ \/\\\\\\\\\\  /\\\\\\\\\\\     \/\\\         \/\\\        /\\\\\\\\\\
'      \/\\\     \//\\\  \//\\\  /\\\  \////////\\\ \//\\///////      \/\\\ /\\     \/\\\ /\\   /\\\/////\\\
'       \/\\\      \//\\\  \///\\\\\/    /\\\\\\\\\\  \//\\\\\\\\\\    \//\\\\\      \//\\\\\   \//\\\\\\\\/\\
'        \///        \///     \/////     \//////////    \//////////      \/////        \/////     \////////\//
'                                  Let's find out together what makes a PIC Tick!
'
' Within an interrupt, create a simple PWM routine with variable frequency and duty cycle
' The 8-bit relative frequency of the PWM can be altered by changing the value held in PWM_bRelFrequency
' The 8-bit duty cycle of the PWM can be altered by changing the value held in PWM_bDutyCycle
' The duty cycle is not duly effected by relative frequency
'
' Uses a Compare Interrupt using the Special Event mechanism
' Compares the 16-bit value of Timer3 with the contents of 16-bit registers CCPR2L\H
' Triggers the interrupt when Timer3 reaches the value held in CCPR2L\H
' Timer3 is automatically reset in hardware
' The rate of the interrupt can be altered by changing the constant value held in cMicroSeconds
'
' Written by Les Johnson for the Positron8 BASIC compiler.
' https://sites.google.com/view/rosetta-tech/home
'
    Device = 18F25K20                                   ' Tell the compiler what device to compile for
    Declare Xtal = 64                                   ' Tell the compiler what frequency the device is operating at (in MHz)
    On_Hardware_Interrupt GoTo ISR_Handler              ' Point the interrupt to the handler routine
   
    Declare Auto_Heap_Strings = On                      ' Make all Strings created in high RAM, above standard variables
    Declare Auto_Variable_Bank_Cross = On               ' Make sure multi-byte variables stay within a single RAM bank
'
' Setup USART1 for debugging
'
    Declare HSerial1_Baud = 9600
    Declare HRsout1_Pin   = PORTC.6

$define PWM_Pin PORTB.0                                 ' Set the pin for PWM output
'
' Timer3 configuration masks to be 'Anded' together
'
$define T3_INT_OFF      %01111111                      ' Interrupts disabled
$define T3_INT_ON       %11111111                      ' Interrupts enabled

$define T3_8BIT_RW      %11111110                      ' 8-bit mode
$define T3_16BIT_RW     %11111111                      ' 16-bit mode

$define T3_PS_1_1       %11001111                      ' 1:1 prescale value
$define T3_PS_1_2       %11011111                      ' 1:2 prescale value
$define T3_PS_1_4       %11101111                      ' 1:4 prescale value
$define T3_PS_1_8       %11111111                      ' 1:8 prescale value

$define T3_SYNC_EXT_ON  %11111011                      ' Synchronise external clock input
$define T3_SYNC_EXT_OFF %11111111                      ' Do not synchronise external clock input

$define T3_SOURCE_INT   %11111101                      ' Internal clock source
$define T3_SOURCE_EXT   %11111111                      ' External clock source

$define T3_SOURCE_CCP   %11111111                      ' T3 is source for CCP
$define T1_CCP1_T3_CCP2 %10111111                      ' T1 is source for CCP1 and T3 is source for CCP2
$define T1_SOURCE_CCP   %10110111                      ' T1 is source for CCP
'
' Create some variables
'
    Dim PWM_bFreqAccum As Byte Access                   ' Frequency accumulator for the interrupt handler
    Dim PWM_bDutyAccum As Byte Access                   ' Duty Cycle accumulator for the interrupt handler

    Dim PWM_bDutyCycle As Byte Access                   ' PWM Duty Cycle variable
    Dim PWM_bRelFrequency As Byte Access                ' PWM relative frequency variable

    Dim wCCPR2_SFR As CCPR2L.Word                       ' Combine CCPR2L\H into a 16-bit SFR

'--------------------------------------------------------------------------------------------------
' The main program loop starts here
' Alter the frequency, then the duty cycle of the low frequency PWM signal
'
Main:
    Setup()                                             ' Setup the program and any peripherals
'
' Create a loop to alter the relative frequency of the PWM signal
' The higher the value of PWM_bRelFrequency, the lower the relative frequency
' With the device running at 64MHz, a PWM_bRelFrequency value of 0 will operate the PWM at 488Hz
'
    'PWM_bDutyCycle = 127                                ' 50% duty cycle
    'For PWM_bRelFrequency = 0 To 255                    ' Decrease the frequency
    '    HRsoutLn "RelFreq: ", Dec PWM_bRelFrequency     ' Transmit the Relative Frequency value for debugging
    '    DelayMS 50                                      ' Create a delay slower than the rate of change
    'Next
'
' Create a loop to alter the duty cycle of the PWM signal
' The higher the value of PWM_bDutyCycle, the higher the duty cycle
'
    PWM_bRelFrequency = 4                               ' Choose a relative frequency of 122Hz
    Do                                                  ' Create a loop
        For PWM_bDutyCycle = 0 To 255                   ' Increase the duty cycle
            HRsoutLn "Duty: ", Dec PWM_bDutyCycle       ' Transmit the duty value for debugging
            DelayMS 20                                  ' Create a delay slower than the rate of change
        Next
    Loop                                                ' Do it forever

'--------------------------------------------------------------------------------------------------
' Setup the program and any peripherals
' Input     : None
' Output    : None
' Notes     : None
'
Proc Setup()
    PWM_Init()                                          ' Initialise the software PWM and its interrupt
EndProc

'--------------------------------------------------------------------------------------------------
' Initialise the CCP2 Special Event interrupt and the PWM variables
' Input     : None
' Output    : None
' Notes     : None
'
Proc PWM_Init()
    PWM_bFreqAccum = 1                                  ' Reset the ISR relative frequency accumulator
    PWM_bDutyAccum = 0                                  ' Reset the ISR duty cycle accumulator
    PWM_bDutyCycle = 0                                  ' Reset the duty cycle
    PWM_bRelFrequency = 0                               ' Reset the relative frequency
    PinLow PWM_Pin                                      ' Make the pin output low

    CCP2CON = %00001011                                 ' Compare mode: Trigger is Special Event, Reset timer

    Timer3_Open(T3_INT_OFF & T3_16BIT_RW & T3_PS_1_8 & T3_SYNC_EXT_OFF & T3_SOURCE_INT & T3_SOURCE_CCP)
    $define cPrescalerValue 8                           ' Alter this to match the prescaler parameter above. i.e. 4 for T3_PS_1_4, 8 for T3_PS_1_8
    $define cMicroSeconds 10                            ' Interrupt rate (in uS)
'
' Calculate the value to place into the CCPRx register in order to achieve a certain interrupt rate (in us)
'
    $define cCCPRx_Value $eval (cMicroSeconds / cPrescalerValue) * (_xtal / 4)

$if cCCPRx_Value > 65535
    $error "Value too large for interrupt duration"
$elseif cCCPRx_Value = 0
    $error "Value too small for interrupt duration"
$endif

    wCCPR2_SFR = cCCPRx_Value                           ' Load CCPR2L\H with the value to trigger an interrupt at a certain duration
    PIE2bits_CCP2IE = 1                                 ' Enable the Special Event Interrupt on CCP2
    INTCONbits_PEIE = 1                                 ' Enable Peripheral Interrupts
    INTCONbits_GIE = 1                                  ' Enable Global Interrupts
EndProc

'---------------------------------------------------------------------------------
' Configure Timer3
' Input         : pConfig holds the bit definitions to configure Timer3
' Output        : None
' Notes         : This routine first resets the Timer3 regs to the POR state and then configures the interrupt, clock source.
'               : The bit definitions for pConfig can be found at the top of this file
'
Proc Timer3_Open(pConfig As Byte)
    T3CON = pConfig & %01111110                         ' Clear and set the bits required
    TMR3H = 0                                           ' \ Clear the Timer3 registers
    TMR3L = 0                                           ' /
    PIR2bits_TMR3IF = 0                                 ' Clear the interrupt flag
    PIE2bits_TMR3IE = pConfig.7                         ' Enable/Disable Timer3 interrupt
    T1CONbits_T1OSCEN = pConfig.1                       ' Enable/Disable Timer1 oscillator
    T3CONbits_T3RD16 = ~pConfig.0                       ' Select between 8-bit and 16-bit modes
    T3CONbits_TMR3ON = 1                                ' Turn on Timer3
EndProc

'--------------------------------------------------------------------------------------------------
' Interrupt Handler
' Input     : PWM_bDutyCycle holds the duty cycle of the PWM waveform (0 to 255)
'           : PWM_bRelFrequency holds the relative frequency of the PWM waveform
' Output    : None
' Notes     : Creates a very slow PWM signal with variable frequency and duty
'
'
ISR_Handler:
    Context Save

    If PIR2bits_CCP2IF = 1 Then                         ' Was it a Compare Special Event on CCP2 that triggered the interrupt?
        Inc PWM_bFreqAccum                              ' Yes. So increase the relative frequency accumulator
        If PWM_bFreqAccum >= PWM_bRelFrequency Then     ' Has the relative frequency reached the desired value?
            PWM_bFreqAccum = 0                          ' Yes. So reset the relative frequency accumulator
            Inc PWM_bDutyAccum                          ' Increment the PWM duty cycle accumulator
            If PWM_bDutyAccum >= PWM_bDutyCycle Then    ' Has the duty cycle accumulator reached the required duty cycle?
                PinClear PWM_Pin                        ' Yes. So make the PWM pin low
            Else                                        ' Otherwise...
                PinSet PWM_Pin                          ' Make the PWM pin high
            EndIf
        EndIf
        PIR2bits_CCP2IF = 0                             ' Clear the CCP2 interrupt flag
    EndIf
'
' << More Interrupt Conditions and Code Here >>
'
    Context Restore                                     ' Exit the interrupt

'-------------------------------------------------------------
' Setup the config fuses for 4x PLL with an external crystal on a PIC18F25K20 device
'
Config_Start
    FOSC = HSPLL        ' HS oscillator, PLL enabled and under software control
    HFOFST = Off        ' The system clock is held Off until the HF-INTOSC is stable
    FCMEN = Off         ' Fail-Safe Clock Monitor disabled
    IESO = Off          ' Two-Speed Start-up disabled
    WDTPS = 128         ' Watchdog is 1:128
    BOREN = Off         ' Brown-out Reset disabled in hardware and software
    BORV = 18           ' VBOR set to 1.8 V nominal
    MCLRE = On          ' MCLR pin enabled, RE3 input pin disabled
    LPT1OSC = Off       ' T1 operates in standard power mode
    PBADEN = Off        ' PORTB<4:0> pins are configured as digital I/O on Reset
    CCP2MX = PORTC      ' CCP2 input/output is multiplexed with RC1
    STVREN = Off        ' Reset on stack overflow/underflow disabled
    WDTEN = Off         ' WDT disabled (control is placed on SWDTEN bit)
    Debug = Off         ' Background debugger disabled' RB6 and RB7 configured as general purpose I/O pins
    XINST = Off         ' Instruction set extension and Indexed Addressing mode disabled (Legacy mode)
    LVP = Off           ' Single-Supply ICSP disabled
    Cp0 = Off           ' Block 0 (000800-001FFF) not code-protected
    CP1 = Off           ' Block 1 (002000-003FFF) not code-protected
    CPB = Off           ' Boot block (000000-0007FF) not code-protected
    CPD = Off           ' Data EEPROM not code-protected
    WRT0 = Off          ' Block 0 (000800-001FFF) not write-protected
    WRT1 = Off          ' Block 1 (002000-003FFF) not write-protected
    WRTB = Off          ' Boot block (000000-0007FF) not write-protected
    WRTC = Off          ' Configuration registers (300000-3000FF) not write-protected
    WRTD = Off          ' Data EEPROM not write-protected
    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 'Relative Frequency' loop has been commented out of the demo code listing, so that the PWM duty can be seen operating more clearly.

Below is a screenshot of the above code listing working in the Proteus simulator. It can be seen that the PWM signal is operating at 122Hz, and this can be changed by altering the value within the 'PWM_bRelFrequency' (Relative Frequency), variable, and/or the interrupt event time, which is every 10uS in the above code listing ($define cMicroSeconds 10):

Low Frequency PWM.jpg

The actual PWM generation is within the interrupt, that gets fired every x microseconds or milliseconds, depending on the frequency of the PWM waveform required:

If PWM_bDutyAccum >= PWM_bDutyCycle Then    ' Has the duty cycle accumulator reached the required duty cycle?
    PinClear PWM_Pin                        ' Yes. So make the PWM pin low
Else                                        ' Otherwise...
    PinSet PWM_Pin                          ' Make the PWM pin high
EndIf