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