Proton BASIC Compiler - Another example of software interrupt driven PWM

  • Pic® Basic

  • An example of non distributed software interrupt driven PWM

    Here is another example of multiple PWM's being driven by an non distributed Interrupt routine

    Sometimes you need more PWM pins than a pic has in its hardware arsenal. Its then that you need to turn to software.

    Before we start off lets examine some term's.
    PWM has 2 components. Duty cycle and PWM period (Frequency)
    The duty cycle is defined as the percentage of digital ‘high’ to digital ‘low’ signals present during a PWM period.
    The PWM period is an arbitrarily time period in which PWM takes place. The Frequency usually is described in Hz or the no of cycles per second. You need to choose the best values to give best results for your particular application.

    A PWM signal

    The routine I'm going to describe is known as non distributed PWM. What's the difference? Well Non distributed as its name says does not distribute the on off times over the period, distributed PWM does. The advantage of distributed is that filtering is made smoother but the PWM Period varies with the Duty. Example below:- Non distributed and distributed

    I will post a distributed PWM version some other time

    Interrupt Period Calculations

    Ok on with the description of how the code works. First lets look at the maths. In this version we are going to produce a PWM period of 100hz (10ms) and in that period we will have a duty capability of 100 eg up to 100 steps. That takes our interrupt requirement to 100 hz / 100 that makes it every 100us. This by anybody's calculation is pretty heavy on the CPU so you need to think running at 20mhz at least. Our example is on a 18f452 at 20mhz and later I will show what looks like as a percentage of cpu time on a scope.

    Principals of operation

    For our system to work we need an interrupt every 100us. We could do it with Tmr0 but it would stop working as we went higher in our xtal value so we will use Tmr1. I will not go into a lot of description into how the interrupt is set up and loaded as its all documented in the code.

    With the interrupt set up we now need some timers. The first is the main PWM Period timer, in the example below its 0-99 (100 steps) which is stepped every 100us and is reloaded every 10ms (100hz). The next timers are the Duty timers. These timers step down every 100us and are re-loading every PWM Period with your desired duty. Every step the value if checked it's > 0 then the output of that PWM channel is set to the required value. If not then its the opposite is done.

    How the Setting of the PWM pin is done

    There are 2 basic principles that could be used

    If DutyTmr > 0 then
    dec DutyTmr
    Pin = Set
    Pin = Clear

    That system will address the port many times if there are a lot of channels

    My code

    pinvar = $FF
    If DutyTmr > 0 Then
    Dec DutyTmr
    pinvar = 0

    Port = pinvar

    The system in my code is to load a variable with the Set value and clear the var in the routine then write that value as a whole to the port. Its really horses for courses so it's up to you what you use. Mine is a little quicker if you have more than 4 PWM channels but they need to be all on the same port. You can mix and match with no ill effect.

    Important Considerations

    As was indicated before, the loading on the CPU is pretty heavy and as its an interrupt anything requiring timing will not work, that means RSin, Pulseout etc and Delay will be very slow. There is not much you can do about RSin etc but you can generate your own delay system as you now have those regular interrupts going. The periods are going to be limited to the min interrupt freq which is every 100us but in most cases you will find this more than adequate.

    Example of a 10ms timer tick and custom timer.

    If PWMGenCounter > 0 Then
        Dec PWMGenCounter
        TimerTick = 1
    That is from the main example and produces a tick every 10ms. Just look for it to be set then run your task code and clear the flag, next time you see it set 10ms will have passed.

    Custom delay

    If PWMGenCounter > 0 Then
        Dec PWMGenCounter
        If MyTimer > 0 Then
            Dec MyTimer
            Timerup = 1
    Here if say we wanted a 1 second delay we load the timer with 100 and clear Timerup. When we see it cleared 1 second will have elapsed. Note that when you make the timer bigger than a byte you need to turn of interrupts while you load the var or you will end up with odd results in places you never thought of.

    Altering the Freq and possible duty

    So you have the example code but you want to change it PWM Period. This is a matter of changing the interrupt frequency eg the reload value. But some simple back of the fag pack calculations will show that making the Period larger is fine but making it smaller is really going to stretch the CPU to breaking point were all it could end up doing is servicing the interrupt code. Increasing the Xtal freq will help.

    The Duty can increase but at the expense of the PWM Period. Play around with the no's to see the effect. Also note that changing to word vars for any timers will increase in the code overheads.

    The CPU Loading of 8 channels on a 18F at 20mhz

    As promised this is a pic of the CPU usage of the interrupt routine on a pic at 20mhz running 8 PWM channels

    The High is the time in the interrupt routine and the Low is the free time for the main code. As you can see the PWM routine is taking around 16% of the CPU time<

    The Code

        Device 18F452
        Xtal = 20
        ; Pic reg alias's
        Symbol GIE = INTCON.7       ' Global Interrupt Enable Bit
        Symbol PEIE = INTCON.6      ' Peripheral Interrupt Enable Bit
        Symbol TMR0IE = INTCON.5    ' TMR0 Overflow Interrupt Enable Bit
        Symbol INT0IE = INTCON.4    ' INT0 External Interrupt Enable Bit
        Symbol RABIE = INTCON.3     ' RA And RB Port Change Interrupt Enable Bit
        Symbol TMR0IF = INTCON.2    ' TMR0 Overflow Interrupt Flag Bit
        Symbol INT0IF = INTCON.1    ' INT0 External Interrupt Flag Bit
        Symbol RABIF =  INTCON.0    ' RA And RB Port Change Interrupt Flag Bit(1)
        Symbol TMR1IF = PIR1.0 ' TMR1 Overflow Interrupt Flag bit
        Symbol TMR2IF = PIR1.1 ' TMR2 to PR2 Match Interrupt Flag bit
        Symbol CCP1IF = PIR1.2 ' CCP1 Interrupt Flag bit
        Symbol SSPIF = PIR1.3  ' Master Synchronous Serial Port Interrupt Flag bit
        Symbol TXIF = PIR1.4   ' EUSART Transmit Interrupt Flag bit
        Symbol RCIF = PIR1.5   ' EUSART Receive Interrupt Flag bit
        Symbol ADIF = PIR1.6   ' A/D Converter Interrupt Flag bit
        Symbol TMR1ON = T1CON.0     ' Timer1 ON
        Symbol TMR1CS = T1CON.1     ' Timer1 Clock Source Select
        Symbol NOT_T1SYNC = T1CON.2 ' Timer1 External Clock Input Synchronization Control
        Symbol T1OSCEN = T1CON.3    ' Timer1 Oscillator Enable Control
        Symbol T1CKPS0 = T1CON.4    ' Timer1 Input Clock Prescale Select bits
        Symbol T1CKPS1 = T1CON.5    ' Timer1 Input Clock Prescale Select bits
        Symbol T1RUN = T1CON.6      ' Timer1 System Clock Status bit
        Symbol RD16 = T1CON.7       ' 16-bit Read/Write Mode Enable bit
        Symbol TMR1IE = PIE1.0   ' TMR1 Overflow Interrupt Enable bit
        Symbol TMR2IE = PIE1.1   ' TMR2 to PR2 Match Interrupt Enable bit
        Symbol CCP1IE = PIE1.2   ' CCP1 Interrupt Enable bit
        Symbol SSPIE = PIE1.3    ' Master Synchronous Serial Port Interrupt Enable bit
        Symbol TXIE = PIE1.4     ' EUSART Transmit Interrupt Enable bit
        Symbol RCIE = PIE1.5     ' EUSART Receive Interrupt Enable bit
        Symbol ADIE = PIE1.6     ' A/D Converter Interrupt Enable bit
        Symbol reserved = PIE1.7 ' Maintain this bit clear
        Dim TIMER1REG As TMR1L.Word
    ; This is our working port variable we use in the interrupt    
        Dim PWMPortAlias As Byte
    ; Alias's to the individual bits of the above var
        Dim PWMPortAlias_0 As PWMPortAlias.0
        Dim PWMPortAlias_1 As PWMPortAlias.1
        Dim PWMPortAlias_2 As PWMPortAlias.2
        Dim PWMPortAlias_3 As PWMPortAlias.3
        Dim PWMPortAlias_4 As PWMPortAlias.4
        Dim PWMPortAlias_5 As PWMPortAlias.5
        Dim PWMPortAlias_6 As PWMPortAlias.6
        Dim PWMPortAlias_7 As PWMPortAlias.7 
    ; These are our working counter for internal control of the pwm Duty, these are not written to by the main code    
        Dim PWM_0_LevelCntr As Byte
        Dim PWM_1_LevelCntr As Byte
        Dim PWM_2_LevelCntr As Byte
        Dim PWM_3_LevelCntr As Byte
        Dim PWM_4_LevelCntr As Byte
        Dim PWM_5_LevelCntr As Byte
        Dim PWM_6_LevelCntr As Byte
        Dim PWM_7_LevelCntr As Byte
    ; These variables are where you set the duty level you require 
        Dim PWMLevel_0 As Byte
        Dim PWMLevel_1 As Byte
        Dim PWMLevel_2 As Byte
        Dim PWMLevel_3 As Byte
        Dim PWMLevel_4 As Byte
        Dim PWMLevel_5 As Byte
        Dim PWMLevel_6 As Byte
        Dim PWMLevel_7 As Byte
    ; This is out main counter 
        Dim PWMGenCounter As Byte
    ; This is the reload value for the PWM eg here we are working on a 0 - 99 (100 step) duty level control
        Dim PWMGenCounterRelaodVal As 99
    ; On Off levels as reuired at the output    
        Dim PWMOn As 1
        Dim PWMOff As 0
    ; Same as above but for all the bits    
        Dim AllPWMOn As %11111111
        Dim PWMPicPort As PORTB
    ; This value was worked out using the Pic timer helper from the Mr E Multicalc plug in
    ; 20mhx 100us and a 5 instruction reload value
        Dim TMR1_VAL As 65041 
    ; Main code variables
        Dim TimerTick As Bit
        Dim Index As Byte
        GoTo MainCode
    ; Say what's going to happen on an interrupt
        On_Interrupt GoTo PWM_Driver   
        If TMR1IF = 1 Then
            Clear TMR1ON                                                ' STOP THE TIMER
            TIMER1REG = TIMER1REG + TMR1_VAL                            ' LOAD TMR1
            Set TMR1ON                                                  ' START THE TIMER AGAIN
            PWMPortAlias = AllPWMOn                                     ; Pre set all the leds on then turn them off as we go through the led PWM timers
        ; PWM for port 0
            If PWM_0_LevelCntr > 0 Then
                Dec PWM_0_LevelCntr
                PWMPortAlias_0 = PWMOff
        ; PWM for port  1
            If PWM_1_LevelCntr > 0 Then
                Dec PWM_1_LevelCntr
                PWMPortAlias_1 = PWMOff
        ; PWM for port 2    
            If PWM_2_LevelCntr > 0 Then
                Dec PWM_2_LevelCntr
                PWMPortAlias_2 = PWMOff
        ; PWM for port 3
            If PWM_3_LevelCntr > 0 Then
                Dec PWM_3_LevelCntr
                PWMPortAlias_3 = PWMOff
        ; PWM for port 4
            If PWM_4_LevelCntr > 0 Then
                Dec PWM_4_LevelCntr
                PWMPortAlias_4 = PWMOff
        ; PWM for port 5
            If PWM_5_LevelCntr > 0 Then
                Dec PWM_5_LevelCntr
                PWMPortAlias_5 = PWMOff
        ; PWM for port 6
            If PWM_6_LevelCntr > 0 Then
                Dec PWM_7_LevelCntr
                PWMPortAlias_6 = PWMOff
        ; PWM for port 7
            If PWM_7_LevelCntr > 0 Then
                Dec PWM_7_LevelCntr
                PWMPortAlias_7 = PWMOff
        ; Now write that Alias in one go to the port
            PWMPicPort = PWMPortAlias
        ; Were the port is on various ports or bits then use this type of method
        ;   Portx.x = PWMPortAlias_1
        ; Proton is very clever and writes the asm so it writes to port bits only once even though it takes 1 instruction more
        ; Every x counts we reload the PWM Counters with the desired PWM level as requested
            If PWMGenCounter > 0 Then
                Dec PWMGenCounter
            ; Every x reload the individual PWM counters with there required values
                PWM_0_LevelCntr = PWMLevel_0
                PWM_1_LevelCntr = PWMLevel_1
                PWM_2_LevelCntr = PWMLevel_2
                PWM_3_LevelCntr = PWMLevel_3
                PWM_4_LevelCntr = PWMLevel_4
                PWM_5_LevelCntr = PWMLevel_5
                PWM_6_LevelCntr = PWMLevel_6
                PWM_7_LevelCntr = PWMLevel_7
            ; and load up the main control counter again
                PWMGenCounter = PWMGenCounterRelaodVal
                ; This is a simple time indicator setting every time the PWM counters are reloaded eg every 100hz
                TimerTick = 1
        ; reset the interrupt flag
        TMR1IF = 0
        Retfie Fast  
        Clear                                                           ; clear all variables
        All_Digital = 1
        Output PWMPicPort                                               ; Set the entire port up as Output 
        '----- SET UP TIMER 1 INTERRUPT ----------------------------
        TIMER1REG = TMR1_VAL                                            ; Load Tmr1 with the reload value
        T1CON = %00000000                                               ' Set up Tmr1 to have 1:1 prescaler and act as a timer
        TMR1IF = 0                                                      ' Clear Tmr1 interrupt flag
        TMR1IE = 1                                                      ' Enable Tmr1 as peripheral interrupt source
        TMR1ON = 1
        PEIE = 1                             
        GIE = 1                                                         ; Global and Peripheral interrupts on     
        PWMGenCounter = PWMGenCounterRelaodVal
        DelayMS 50                                                      ; A short deley incase one is needed
        GoSub Initialise                                                ; Do the timer set up etc
        While 1 = 1                                                     ; Make a loop
            For Index = 0 To 99                                         ; Ramp the PWM up from 0 to 99
                While TimerTick = 0: Wend                               ; We wait in this loop until the interrupt has done on duty cycle
                PWMLevel_0 = Index
                PWMLevel_1 = Index
                PWMLevel_2 = Index
                PWMLevel_3 = Index
                PWMLevel_4 = Index
                PWMLevel_5 = Index
                PWMLevel_6 = Index
                PWMLevel_7 = Index
                TimerTick = 0            
    ; repeat the process in reverse
            For Index = 99 To 0  Step -1
                While TimerTick = 0: Wend
                PWMLevel_0 = Index
                PWMLevel_1 = Index
                PWMLevel_2 = Index
                PWMLevel_3 = Index
                PWMLevel_4 = Index
                PWMLevel_5 = Index
                PWMLevel_6 = Index
                PWMLevel_7 = Index
                TimerTick = 0