Proton BASIC Compiler - Another example of software interrupt driven PWM

• # 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
Else
Pin = Clear
endif

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
Else
pinvar = 0
EndIf

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.

Code:
```If PWMGenCounter > 0 Then
Dec PWMGenCounter
Else
TimerTick = 1
EndIf```
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

Code:
```If PWMGenCounter > 0 Then
Dec PWMGenCounter
Else
If MyTimer > 0 Then
Dec MyTimer
Else
Timerup = 1
EndIf
EndIf```
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.

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

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

'-------------------------------------------------------------------------------------------
'
' TIMER INTERRUPT TO DRIVE THE PWM
'
'
'-------------------------------------------------------------------------------------------
PWM_Driver:
Reset_Bank

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
Else
PWMPortAlias_0 = PWMOff
EndIf

; PWM for port  1
If PWM_1_LevelCntr > 0 Then
Dec PWM_1_LevelCntr
Else
PWMPortAlias_1 = PWMOff
EndIf

; PWM for port 2
If PWM_2_LevelCntr > 0 Then
Dec PWM_2_LevelCntr
Else
PWMPortAlias_2 = PWMOff
EndIf
; PWM for port 3
If PWM_3_LevelCntr > 0 Then
Dec PWM_3_LevelCntr
Else
PWMPortAlias_3 = PWMOff
EndIf
; PWM for port 4
If PWM_4_LevelCntr > 0 Then
Dec PWM_4_LevelCntr
Else
PWMPortAlias_4 = PWMOff
EndIf
; PWM for port 5
If PWM_5_LevelCntr > 0 Then
Dec PWM_5_LevelCntr
Else
PWMPortAlias_5 = PWMOff
EndIf
; PWM for port 6
If PWM_6_LevelCntr > 0 Then
Dec PWM_7_LevelCntr
Else
PWMPortAlias_6 = PWMOff
EndIf
; PWM for port 7
If PWM_7_LevelCntr > 0 Then
Dec PWM_7_LevelCntr
Else
PWMPortAlias_7 = PWMOff
EndIf

; 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
Else
; 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

EndIf

EndIf
; reset the interrupt flag
TMR1IF = 0

Retfie Fast
'---------------------------

Initialise:

Clear                                                           ; clear all variables

All_Digital = 1

Output PWMPicPort                                               ; Set the entire port up as Output

'----- SET UP TIMER 1 INTERRUPT ----------------------------

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

Return

MainCode:

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
Next

; 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
Next
Wend```