list t=ON,c=132,n=80,p=16f84

title "An all software PWM with 256 states"

radix dec

;********************************************************************************

;

; T. Scott Dattalo - mailto:

;

;This software is totally free and released as is. I think it's pretty cool,

;but if it doesn't work for you then, well... sorry.

;

; 17JUN97 - Initial version

; AUG00 - Added support for 256 cycle wide pulses. Fixed a couple of bugs

;

;

; The purpose of this program is to generate a Pulse Width Modulated

;square wave with single instruction cycle resolution and 256 levels.

;Over half of the cycles are available every iteration for other tasks.

;This version has been simulated for both a 12C509 and a 16F84. However,

;it's only been tested on a 16F84

;

; HOW IT WORKS:

;

; The algorithm is a state machine based on indirect goto's and phase-shifting

;isochronous code. The 256 PWM levels are divided into 8 groups, or octants

;of 32 cycles each. A pulse in the first octant is considered a "short pulse"

;and one the last octant is a "long pulse". These two are special cases that

;are handled differently than the other six "normal pulse" cases. The upper

;three bits of the pulse width define the octant in which the pulse ends. And

;of course, the lower five bits define the cycle with in that octant.

;

;The (very) conceptual program flow for the normal case is:

;

; i) on cycle 0 turn on the PWM output

; ii) pw_octant = 0

; iii) if the pulse width ends in this octant then goto step v.

; iv) if (++pw_octant == 7) then goto step i else goto step iii.

; v) end the normal pulse width by delaying an amount equivalent to

; the lower five bits of the pulse width and turning off the PWM output.

;

;In the actual program the variable 'pw_octant' is actually a bit mask defined by:

;

; pw_octant = 1 < ((pw&11100000)>5)

;

;or in words, all bits except for one of them are zero in pw_octant. The bit that

;is a one corresponds to the octant in which the pulse terminates. For example,

;if the pulse is 40 (decimal) cycles wide then the upper three bits are 001 and

;the pw_octant mask equals 00000010 binary. This bitmasked approach allows the

;program to efficiently check the octant with BTF instructions.

;

; The variable 'next_start_state' contains the address at which the next PWM

;will begin executing. If the next PWM cycle is either a long or a normal

;pulse then next_start_state contains 'pulse_off2on+7' which happens to be a

;BCF PWM immediately followed by a BSF PWM. The BCF turns off the PWM output.

;(If the next PWM cycle is a short pulse then next_start_state will contain

;one of four possible starting points; discussed below). Here's how pulse_off2on

;works:

;

;pulse_off2on

; BCF PWM <--- cycle 248 of current pwm cycle

; BCF PWM

; BCF PWM These BCF's only get executed if the current

; BCF PWM pulse width is a long one, that is between

; BCF PWM 224 and 255 cycles.

; BCF PWM

; BCF PWM

; BCF PWM <--- cycle 255 of current pwm cycle

;

; BSF PWM <--- cycle 0 of next pwm cycle.

;

; MOVF state_1,W <--- Indirect branch to the next state which usually

; MOVWF PCL is the routine 'normal'. Short pulses that pass

; through here branch else where.

;

;The 8 BCF instructions between the start of pulse_off2on and the BSF are

;'BCF PWM' instructions. These are used to support long pulses, and in

;particularly very long pulses between 248 and 255 cycles. In this

;case, the complement of the lower three bits of the current PWM cycle are

;subtracted from 'next_start_state':

;

; if(pw_octant & 10000000b) // If the current pulse ends in the last octant

; next_start_state -= ( (pw & 00000111b) ^ 00000111b)

;

; So for example, if the current pulse width is 253 cycles wide, then pw equals

; 253 = 0xFD = 11111101b. The lower three bits are 101b and the complement of these

;is 010b. Thus next_start_state will be set to pulse_off2on+7-2 or pulse_off2on+5.

;

; Once 'next_start_state' is properly initialized it is used as an indirect goto:

;

; MOVF next_start_state,W

; MOVWF PCL

;

;more....

;#define family 0x12c ;12C508,12C509

;#define family 0x16c5 ;16C5x parts

#define family 0x16c6 ;Mid range core (16x84,C64,C71...)

if family == 0x12c

;; include "P12C509A.INC"

START_OF_RAM_LO EQU 0x07

;****************

;PWM PORT Equates

PWM_PORT EQU GPIO

PWM_BIT EQU 0

endif

if family == 0x16C6

include "p16c84.inc"

__CONFIG _CP_OFF & _PWRTE_ON & _WDT_OFF& _HS_OSC

START_OF_RAM_LO EQU 0x0C

;****************

;PWM PORT Equates

PWM_PORT EQU PORTB

PWM_BIT EQU 0

endif

#define PWM PWM_PORT,PWM_BIT

cblock START_OF_RAM_LO

;

pw ;pulse width - this is the width of the pulse

; currently being driven. Note that this should

; not be directly written to.

pw_octant ;The octant when the pulse goes inactive

t1 ;A temporary variable used in calculating delays

next_pw ;The width of the next pulse. This is where the

;the user writes the desired pulse width

next_pw_octant ;The octant the next pulse goes inactive.

;The user should not write to this.

state_1 ;Internal variable for the state machine

next_start_state ;Internal variable for the state machine

all_high ;The user will set the lsb of all_high if the

;next pulse is to stay high for all 256 cycles

; application variables:

buf1

pulse_width_lo,pulse_width_hi

phase_accum

endc

;***********************************************************************

ORG 0 ;Reset Vector

GOTO Main

;***********************************************************************

;This segment will drive the PWM output high (or keep it high if it

;was driven earlier) and then 1 to 8 cycles later drive it low. It then

;branches to the address contained in state_1.

pulse_on2off

BSF PWM

BSF PWM

BSF PWM

BSF PWM

BSF PWM

BSF PWM

BSF PWM

BCF PWM

MOVLW pulse_on2off+8

SUBWF state_1,W

XORLW B'11111111'

goto variable_delay

short_vd

MOVF pw,F

SKPZ

goto finish_short2+1

finish_short1

CALL delay20

finish_short2

NOP

CALL delay16

CALL delay13

goto normal

goto finish_short1

short_12_15

goto $+1

CALL ep2

CALL delay13

goto normal

short_16_31

goto $+1

CALL ep1

CALL delay13

goto normal

;This segment will drive the PWM output low (or keep it low if it

;was driven earlier) and then 1 to 8 cycles later drive it high for

;one cycle and finally drives it low. This state is only executed for

;a single-cycle pulse. The exact entry depends on wide the previous

;pulse was. If it is between 248-255 cycles, execution begins at one

;of the BCF's.

pulse_off2on_1

BCF PWM

BCF PWM

BCF PWM

BCF PWM

BCF PWM

BCF PWM

BCF PWM

BCF PWM ;255 - last state of pulse

BSF PWM ;0 - first state of next pulse

BCF PWM ;1

goto pfn_2_3 ;2,3

;This segment will drive the PWM output low (or keep it low if it

;was driven earlier) and then 1 to 8 cycles later drive it high for

;two or three cycles and finally drives it low. This state is only

;executed for a two or three cycle pulse.

pulse_off2on_2_3

BCF PWM

BCF PWM

BCF PWM

BCF PWM

BCF PWM

BCF PWM

BCF PWM

BCF PWM ;255 - last state of pulse

BSF PWM ;0 - first state of next pulse

BTFSS pw,0 ;1 If the pulse is two cycles wide

BCF PWM ;2 then turn it off now

BCF PWM ;3

pfn_2_3

GOTO finish_short1

;This segment will drive the PWM output low (or keep it low if it

;was driven earlier) and then 1 to 8 cycles later drive it high. It then

;falls through to normal processing.

;This section is executed for 'long pulses' - ones that are between 248 and

;and 255 cycles long.

pulse_off2on

BCF PWM

BCF PWM

BCF PWM

BCF PWM

BCF PWM

BCF PWM

BCF PWM

BCF PWM ;255 - last state of pulse

BSF PWM ;0 - first state of next pulse

MOVF state_1,W

MOVWF PCL

;; If the next pulse is a zero (which means there is NO pulse)

;; then we'll pass through here. The exact entry depends upon the

;; width of the pulse prior to this one.

pulse_zero

BCF PWM

BCF PWM

BCF PWM

BCF PWM

BCF PWM

BCF PWM

BCF PWM

BCF PWM ;255 - last state of pulse

BCF PWM ;0 - first state of next pulse

gotoshort_vd

;------

;normal - Every cycle passes through this portion of the code.

;

; The purpose of this routine is to off the PWM output when ever

;the pulse's terminating octant is reached. 145 cycles are available

;for other tasks. There are other free cycles available else where,

;however they are state-dependent. In other words, depending on the

;pulse width, they may or may not be executed.

normal

MOVLW normal ;Re-initialize state_1 to point back to here.

MOVWF state_1 ;(Short pulses will over write this.)

;

CALL delay13 ;13 free cycles

;

BTFSC pw_octant,1 ;If bit 1 is set then it's time to end the pulse.

CALL end_pulse ;Note, that bit 0 is not checked because it

;corresponds to a 'short pulse'.

;

; CALL delay30 ;30 free cycles

calladjust_pulse

;

BTFSC pw_octant,2 ;

CALL end_pulse ;

;

CALL delay30 ;30 free cycles

;

BTFSC pw_octant,3 ;

CALL end_pulse ;

;

CALL delay30 ;30 free cycles

;

BTFSC pw_octant,4 ;

CALL end_pulse ;

;

CALL delay30 ;30 free cycles

;

BTFSC pw_octant,5 ;

CALL end_pulse ;

;

CALL setup_next_pulse;This time, the 30 cycles are used to set up

;the state machine for the next pulse.

BTFSC pw_octant,6 ;

CALL end_pulse ;

;

CALL delay8 ;8 free cycles

;

clrc ;The lsb of all_high indicates if the next

rlf all_high,f ;pulse is to be all high (256 cycles)

;

BTFSC pw_octant,7 ;If bit 7 is set then this pulse is a long one

goto long_pulse ;

;

MOVF next_pw,W ;Otherwise, it's a short or a normal one.

MOVWF pw ;

MOVF next_pw_octant,W

MOVWF pw_octant

NOP

INCF next_start_state,W

BTFSSall_high,2 ;If the current pulse is not 256 cycles wide

MOVWF PCL ;then start the next pulse cycle now.

;; We get here for the special case of an 'all high' pulse.

;;

calldelay30

calldelay17

calldelay6

movwfPCL

;------

;long_pulse

long_pulse

CALL delay12 ;12 free cycles for long pulses.

;

COMF pw,W ;Negate the lower

ANDLW B'00000111' ;three bits of pw

;This segment provides a variable delay of 0 to 7 cycles.

;Note that normally this code is only used by the "long_pulse" routine.

;However, it is also used if the next pulse is zero cycles wide

variable_delay

ADDWF PCL,F

NOP

NOP

NOP

NOP

NOP

NOP

NOP

NOP

BTFSS pw_octant,7 ;If this is a zero-cycle wide pulse then

goto short_vd ;this is a special case of a short pulse.

SUBWF next_start_state,F

MOVF pw,W

MOVWF t1

MOVF next_pw,W

MOVWF pw

MOVF next_pw_octant,W

MOVWF pw_octant

;Now execute an isochronous phase shift

BTFSC t1,4 ;If the pulse is between 240 and 255 cycles

goto lp2 ;then that's handled below

;

BTFSC t1,3 ;If the pulse is between 232 and 239 cycles

CALL delay9 ;then delay a little

;

lp1 ;

BCF PWM ;Turn it off now!

;

BTFSC t1,4 ;If the pulse is between 240 and 247 cycles

goto lp3 ;then it's time to go to the next state

;

BTFSS t1,3 ;If the pulse is between 224 and 231 cycles

CALL delay9 ;then delay a little.

;

CALL delay5 ;

goto lp3 ;

lp2 ;

CALL delay14 ;

;

BTFSS t1,3 ;If the pulse is between 240 and 247 cycles

goto lp1 ;then go turn it off.

;

CALL delay5 ;Otherwise, the pulse is between 248 and 255

;cycles. Delay a little and branch to the next

lp3 ;start state where it will be turned off.

NOP ;

MOVF next_start_state,W

MOVWF PCL ;

;------

;short_pulse

; The purpose of this section is to handle "short pulse widths". In

;this case, short means that the pulse is between 12 and 32 cycles.

;The two columns of numbers in the comments correspond to the shortest

;(12) and longest (31) paths. Regardless of the path, the execution

;time is a constant 37 cycles.

;

short_pulse

BTFSC pw,4

CALL delay9

BTFSC pw,3

CALL delay5

COMF pw,W

ANDLW 7

ADDWF PCL,F

BCF PWM

BCF PWM

BCF PWM

BCF PWM

BCF PWM

BCF PWM

BCF PWM

BCF PWM

CALL ep3

CALL delay5

goto normal

;------

;setup_next_pulse

;

; The purpose of this routine is to prepare the PWM state machine

;for the next pulse (after the one we are current controlling).

;INPUTS:

; next_pw - The width of the next pulse (time high)

;OUTPUTS:

; next_pw_octant - A bit mask that indicates in which octant the

; next pulse will appear. Algorithmically:

; next_pw_octant = 1 < ((next_pw & 11100000b)>5)

; state_1 - If the next pulse is not a short one (less than 32 cycles)

; then this will be initialized to point to the routine

; 'normal'. Otherwise, this will point to one of

; several places depending upon the pulse width.

setup_next_pulse

btfscall_high,0

gotonext_is_all_high

RRF next_pw,W ;Move bits 7,6,5 down one position

ANDLW B'01110000'

SKPZ ;If this is not a short pulse, then

goto next_is_normal ;it's a normal (or maybe long) one.

;Next pulse is a short one

CLRF next_pw_octant

INCF next_pw_octant,F

MOVLW pulse_off2on+7 ;

MOVWF next_start_state ;

BTFSC next_pw,4

goto next_ge_16

MOVLW -4 ;If the next pulse is < 4 cycles, then

ADDWF next_pw,W ;this will be handled by the special routines

SKPNC ;'pulse_off2on_1' or 'pulse_off2on_2_3'

goto next_ge_4 ;otherwise...

;

; MOVLW variable_delay+8 ;Assume next pulse is 0 cycles

MOVLWpulse_zero+7;Assume the next pulse is 0 cycles

BTFSC next_pw,0 ;If bit 0 is set then the assumption was wrong

MOVLW pulse_off2on_1+7 ;Assume next pulse is 1 cycle

;

BTFSC next_pw,1 ;If bit 1 is set then

MOVLW pulse_off2on_2_3+7 ;the next pulse is 2 or 3 cycles wide

;

MOVWF next_start_state ;And this is where it'll begin

;(note, state_1 doesn't need initialization)

goto delay6 ;

next_ge_4

MOVWF state_1 ;

ANDLW B'11111000' ;If any of the upper 5 bits of W are set

SKPZ ;then W is greater than 7, which means that

goto next_ge_12 ;the next pulse is greater than 4+7 or 11

COMF state_1,F ;

MOVLW pulse_on2off + 7 + 1 ;

ADDWF state_1,F ;

goto delay4

next_ge_12

MOVLW short_12_15 ;

MOVWF state_1 ;

gotodelay4

;; ; RETURN

next_ge_16

MOVLW short_16_31

MOVWF state_1 ;

goto delay13

next_is_normal

;Compute 2^n where n is the upper three bits of 'pw'. Note, Mike Keitz

;deserves partial credit for this code.

ANDLW B'00110000' ; We only want 6 & 5 right now

MOVWF next_pw_octant ;

SWAPF next_pw_octant,F ;Put bits 6&5 into positions 1 & 0.

INCF next_pw_octant,W ;00 -> 01, 01 -> 10, 10 -> 11, 11->100

BTFSC next_pw_octant,1 ;If bit6 is a 1 then

IORWF next_pw_octant,F ;the IORWF will set the lower two bits

;At this point, here's what we've got

;bits 6&5 next_pw_octant

; 0 0 00000000

; 0 1 00000001

; 1 0 00000011

; 1 1 00000111

INCF next_pw_octant,F ;1 < bits 6&5

BTFSC next_pw,7 ;If bit 7 is set, then we are dealing

SWAPF next_pw_octant,F ;with the upper half of the octants.

MOVLW pulse_off2on+7 ;

MOVWF next_start_state ;

goto delay10

next_is_all_high:

;The next pulse is going to remain high for the entire 256 cycles. This

;is a special case.

;

clrfnext_pw_octant

MOVLW pulse_off2on+7 ;

MOVWF next_start_state ;

gotodelay22

;------

;end_pulse

; The purpose of this isochronous routine is to turn off the

;PWM output. Only the lower 5 bits of 'pw' are used.

;

;Input: pw

;Output

;Execution: 52 cycles

;

end_pulse

BTFSC pw,4 ;01;01;

CALL delay17 ;02;02

ep1 BTFSC pw,3 ;03;19

CALL delay9 ;04;20

BTFSC pw,2 ;05;29

CALL delay5 ;06;30

ep2 BTFSS pw,1 ;07;35

goto $+3 ;08;36

NOP ; ;37

GOTO $+1 ; ;38

ep3 BTFSS pw,0 ;10;40

BCF PWM ;11;sk

BCF PWM ;12;42

BTFSC pw,1 ;13;43

goto $+3 ;14;44

NOP ;15;

GOTO $+1 ;16;

BTFSS pw,2 ;18;46

CALL delay5 ;19;47

BTFSS pw,3 ;24;48

CALL delay9 ;25;49

BTFSS pw,4 ;34;50

CALL delay17 ;35;51

return ;52;52

;------

; various delays:

delay30

goto $+1

goto $+1

delay26 goto $+1

delay24 goto $+1

delay22 goto $+1

delay20 NOP

delay19 NOP

delay18 NOP

delay17 NOP

delay16 NOP

delay15 NOP

delay14 NOP

delay13 NOP

delay12 NOP

delay11 NOP

delay10 NOP

delay9 NOP

delay8 NOP

delay7 NOP

delay6 NOP

delay5 NOP

delay4 RETURN

;------

;; We've got exactly 28 cycles to determine what the next

;; pulse's width is to be.

adjust_pulse:

;;

;; 8-bit saw-tooth

;;

;; This example illustrates how to generate a 'saw tooth'

;; wave form. The way it works, is the pulse width is incremented

;; by 1 each time we are called. When 255 is reached, the count

;; will roll over to zero.

incfnext_pw,f

nop

gotodelay26

;;

;; 12/16-bit saw-tooth

;;

;; This is hard to describe...

;; To get 12-bit resolution from an 8-bit PWM, you have to average

;; 16 consecutive outputs. For example, suppose you wanted to generate

;; the level 1000/4096 with an 8-bit PWM. There are several ways

;; to approach. You could for example, try concatenating the first three

;; 8-bit outputs to get 256*3 = 768 and then driving the fourth with

;; a pulse that is 1000-768 = 232 cycles. Then the 5th through the 16th

;; 'pulses' would be driven low. This would generate a pwm waveform

;; that is high for 1000 out of 4096 cycles.

;;

;; The only drawback with this approach is that the output frequency

;; is rather low and consequently more difficult to filter. Another

;; approach would be to split the 12-bit pulse over 16 cycles. For

;; example, 1000/16 = 62.5. So we could we could drive 8 pulses with

;; 62 cycle wide pulses and 8 others with 63 cycles (8*62 + 8*63 =1000).

;; This method implemented below.

;;

;; The way it works is first the 12-bit value is treated as a 16-bit

;; value. This way, instead of dividing by 16 we can divide by 256,

;; which is trivial: just take the high order byte. This is used

;; for the width of the next PWM cycle. The low order byte is part

;; of a 'phase accumulator'. It is repeatedly added to 'phase_accum'.

;; Everytime this addition rolls over, the width for the next PWM cycle

;; is incremented by 1.

;;

;; If the next pulse is incremented to 256, the range of the 8-bit PWM

;; is exceeded. To compensate, the lsb of the variable 'all_high' is

;; set indicating that the next pulse will be driven high for the whole

;; PWM cycle.

;;

;adjust_pulse:

movfpulse_width_hi,w;The high byte of the 16-bit pulse

movwfnext_pw;is the width of the next pulse

movfpulse_width_lo,w;Unless, the phase-accumulated low

addwfphase_accum,f;byte rolls over

bsfall_high,0;Assume 1) rollover 2) next_pw = 256

skpnc;skip if phase accumalator didn't roll

incfsz next_pw,f;inc next if it did roll over

bcfall_high,0;assumptions 1 and 2 were wrong.

;

; Counter

;

decfsz buf1,f

gotodelay19

;

; set buf1 = 16

;

bsfbuf1,4

;

; Every 16 cycles, increase the 16-bit wide pulse a little:

;

movlw0x10

addwfpulse_width_lo,f

skpnc

incfpulse_width_hi,f

gotodelay13

;------

Main

BCF STATUS,RP0 ;Point to BANK 0

clrw

TRIS PWM_PORT ;

clrfPWM_PORT

clrfall_high

MOVLW normal

MOVWF state_1

MOVLW pulse_off2on+7

MOVWF next_start_state

MOVLW 232

MOVWF pw

MOVWF next_pw

goto normal

END