; ***************************************************************
; * Copyright (c) 2001, Embed Inc (http://www.embedinc.com) *
; * *
; * Permission to copy this file is granted as long as this *
; * copyright notice is included in its entirety at the *
; * beginning of the file, whether the file is copied in whole *
; * or in part and regardless of whether other information is *
; * added to the copy. *
; * *
; * The contents of this file may be used in any way, *
; * commercial or otherwise. This file is provided "as is", *
; * and Embed Inc makes no claims of suitability for a *
; * particular purpose nor assumes any liability resulting from *
; * its use. *
; ***************************************************************
;
; Interrupt service and related routines.
;
include "hal.inc"
extern_flags ;declare global flag bits EXTERN
;
;***********************************************************************
;
; Configuration constants.
;
save_fsr equ true ;indicate whether ISR must save/restore FSR
ccp1con_val equ b'00001100' ;CCP1CON value, PWM LSBs will be merged in later
; XX------ not implemented
; --00---- least significant bits of PWM period
; ----1100 select PWM operation mode
;
; Define the timer 2 setup. This timer is used for two purposes: to
; generate the PWM frequency, and to generate periodic interrupts that
; cause the PWM value to be reloaded. These periodic interrupts are
; also used as a time base for the rest of the sofware.
;
; The instruction clock is divided by the timer 2 prescaler and period
; register to yield the PWM period. This is then divided by the postscaler
; to yield the interrupt period.
;
; NOTE: The allowable ranges for these values MUST be observed. Out of
; range values are not checked but can cause system failure, not just
; an unexpected PWM slice period.
;
t2pre equ 1 ;timer 2 prescaler divide value (1, 4, or 16)
t2per equ 32 ;timer 2 period register divide value (1 - 256)
t2post equ 16 ;timer 2 postscaler divide value (1 - 16)
;
; The values above result in the following, assuming 8MHz oscillator
; which results in a 2MHz instruction clock (Finst).
;
; Finst / (t2pre * t2per) = 2MHz / (1 * 32) = 2MHz / 32
; = 62.5KHz PWM frequency (Fpwm)
; Fpwm / t2post = 62.5KHz / 16 = 3.91KHz interrupt rate
; = 256uS interrupt period (every 512 instructions)
;
; The PWM duty cycle has a granularity of one part in T2PER*4. This
; system stores 7 bits per audio sample, so T2PER is set to 32 so that
; the audio sample data can be directly loaded into the PWM hardware.
;
iadconv equ 4 ;do A/D conversion every this many interrupts
;
;**********
;
; Derived constants.
;
t2inst equ t2pre * t2per * t2post ;timer 2 period in instructions
t2ns equ t2inst * nsec_inst ;timer 2 period in nS
i100ms equ (100000000 + t2ns/2) / t2ns ;interrupts per 100mS
aud0 equ t2per / 2 ;CCPR1L value for 0 audio signal level
aud0sam equ t2per * 2 ;7 bit audio sample for 0 signal level
;
;***********************************************************************
;
; Global state.
;
; The following global state is in the normal register bank for global
; state. The bank is GBANK, and GBANKADR is an address guaranteed to
; be within this bank.
;
.bank#v(gbank) udata
temp1 res 1 ;temp scratch that may be trashed by any routine
global temp1
;
;***********************************************************************
;
; Local state. This is always in the same register bank as the global
; state.
;
audadr res 2 ;next audio sample adr * 4, low 2 bits are state
audlft res 2 ;number of audio samples left to play
cnt100ms res 2 ;counts down every interrupt, = 0 every 100mS
ccpcon_n res 1 ;next value to write to CCP1CON for PWM
ccprl_n res 1 ;next value to write to CCPR1L for PWM
audl res 1 ;audio sample from low half of prog memory word
audh res 1 ;audio sample from high half of prog memory word
cntad res 1 ;interrupts left until next A/D conversion
;
; The following state is private to the interrupt service routine, and
; must always be in bank 0.
;
if gbank != 0
.bank0 udata
endif
status_save res 1 ;saved copy of STATUS, nibbles swapped
if save_fsr
fsr_save res 1 ;saved copy of FSR (if FSR save enabled)
endif
if ncodepages > 1
pclath_save res 1 ;saved copy of PCLATH (if multiple code pages)
endif
itmp1 res 1 ;temp storage for use within interrupt routine
itmp2 res 1
;
; This state is private to the interrupt service routine and must always
; be accessible regardless of the current direct register bank setting.
;
udata_shr
w_save res 1 ;saved W during interrupt, mapped to all banks
.intr code
;
;***********************************************************************
;
; Subroutine INTR_INIT
;
; Initialize the interrupt system and other state managed by this module.
;
glbsub intr_init, noregs
;
; Initialize global state.
;
;
; Initialize local state.
;
dbankif gbankadr
loadk16 cnt100ms, i100ms ;init interrupts left until next 100mS tick
movlw aud0 ;init next audio output value to 0 level
movwf ccprl_n
movlw ccp1con_val
movwf ccpcon_n
movlw iadconv ;leave max time until next A/D conversion
movwf cntad
;
; Set up timer 2 to be the PWM time base and to produce the periodic
; interrupts.
;
dbankif pr2
movlw t2per - 1 ;set period register
movwf pr2
dbankif t2con
t2con_val set b'00000100' ;set static timer 2 control bits
; X------- unused
; -0000--- set postscaler to divide value - 1, set below
; -----1-- enable timer 2
; ------00 set prescaler to divide value ID, set below
t2con_val set t2con_val | ((t2post - 1) << 3) ;merge in postscaler divide select
if t2pre == 4 ;prescaler divide value is 4 ?
t2con_val set t2con_val | 1 ;merge in value for prescaler divide by 4
endif
if t2pre == 16 ;prescaler divide value is 16 ?
t2con_val set t2con_val | 2 ;merge in value for prescaler divide by 16
endif
movlw t2con_val ;set timer 2 control register
movwf t2con
dbankif tmr2
clrf tmr2 ;leave max time before first interrupt
dbankif pie1
bsf pie1, tmr2ie ;enable timer 2 interrupt
;
; Initialize the PWM hardware.
;
dbankif ccpr1l
movlw aud0 ;init PWM output to audio 0 level
movwf ccpr1l
dbankif ccp1con
movlw ccp1con_val ;configure the CCP1 module for the audio output PWM
movwf ccp1con
;
; Enable interrupts.
;
dbankif pir1 ;clear any peripheral interrupt conditions
clrf pir1
dbankif pir2
clrf pir2
bsf intcon, peie ;allow peripheral interrupts
bsf intcon, gie ;globally enable interrupts
leaverest
;
;***********************************************************************
;
; Subroutine SOUND
;
; Start playing a new audio sequence. Values are passed in as follows:
;
; REG1,REG0 - The number of samples in the sequence. Note that
; two samples are stored in each program memory word. This number
; may be odd, in which case the high sample in the last word is
; not used.
;
; REG3,REG2 - Starting memory address of the audio sequence. The
; sequence starts with the low sample at this address.
;
glbsub sound, regf2 | regf3
dbankif gbankadr
bcf flag_audon ;prevent interrupts from changing audio sequence state
;
; Set the number of output samples in this sequence. This is twice the
; number of samples stored in memory because interpolation is used to
; output the average value between two samples. This very first output
; sample will be the average of the first stored sample and the AUDH
; value.
;
dbankif gbankadr
bcf status, c ;set bit value to shift in
rlf reg0, w ;set number of output samples low byte
movwf audlft+0
rlf reg1, w ;set number of output samples high byte
movwf audlft+1
;
; Init AUDADR. This contains the source sample memory address in the high
; 14 bits, and a phase state value in the low 2 bits. The phase will be
; initialized to 0.
;
dbankif gbankadr
bcf status, c ;set bit value to shift in
rlf reg2 ;shift address left one bit
rlf reg3
bcf status, c ;set bit value to shift in
rlf reg2, w ;set AUDADR low byte
movwf audadr+0
rlf reg3, w ;set AUDADR high byte
movwf audadr+1
;
; Init AUDH. The first source sample will be averaged with this value
; to produce the first output sample. AUDH is initialized to the zero
; output level.
;
dbankif gbankadr
movlw aud0sam ;get the sample value for 0 output level
movwf audh
;
; Enable audio output if there is as least one sample in this sequence.
;
dbankif gbankadr
movf reg0, w ;set Z if audio sequence is empty
iorwf reg1, w
skip_z ;audio sequence is empty ?
bsf flag_audon ;sequence is not empty, enable audio output
leaverest
;
;***********************************************************************
;
; Interrupt service routine.
;
; The processor effectively executes a call to location 4 on an interrupt,
; except that global interrupts are also disabled. These are re-enabled
; at the end of the ISR by the RETFIE instruction.
;
; Note that subroutine calls must be minimized or avoided in the ISR.
; Since an interrupt can come at any time in the main code, any additional
; call stack locations used here are not available anywhere else.
;
.intr_svc code 4 ;start at interrupt vector location
movwf w_save ;save W
swapf status, w ;make copy of status without effecting status bits
clrf status ;select direct and indirect register banks 0
dbankis 0
ibankis 0
movwf status_save ;save old STATUS value with nibbles swapped
if save_fsr ;FSR needs to be saved ?
movf fsr, w ;save FSR
movwf fsr_save
endif
if ncodepages > 1 ;multiple code pages may be in use ?
movf pclath, w ;save PCLATH
movwf pclath_save
clrf pclath
endif
;
; W, STATUS, FSR (if SAVE_FSR set), and PCLATH (if multiple code pages)
; have been saved. Direct and indirect register banks 0 are selected, and
; the bank assumptions have been set accordingly. Program memory page 0
; is selected.
;
dbankif pir1
btfsc pir1, tmr2if ;check for timer 2 interrupt
goto intr_timer2
goto 0 ;unknown interrupt, reboot system (shouldn't happen)
;
;********************
;
; Timer 2 interrupt.
;
intr_timer2 dbankis pir1
bcf pir1, tmr2if ;clear the interrupt condition
;
; Update the audio output value. This means the next PWM period value is
; loaded into the PWM hardware. We are using a 7 bit maximum PWM period.
; These 7 bits are split between two registers. The low two bits are
; stuffed into the CCPR1CON register, and the upper bits go into the
; CCPR1L register. The direct values for these registers have been
; pre-computed and are in CCPCON_N and CCPRL_N. These value are
; pre-computed for the next interrupt at the end of the previous interrupt
; to eliminate just about all jitter in updating the audio output.
;
dbankif gbankadr ;update high 5 bits of new audio output level
movf ccprl_n, w
dbankif ccpr1l
movwf ccpr1l
dbankif gbankadr ;update low 2 bits of new audio output level
movf ccpcon_n, w
dbankif ccp1con
movwf ccp1con
;
; Update the audio output value for next time if audio output is enabled.
;
dbankif gbankadr
btfsc flag_audon ;audio output is disabled ?
goto intr_audon ;audio output is enabled
;
; The audio output is disabled. Load the audio 0 level into the values
; to use next time.
;
dbankif gbankadr
movlw aud0 ;init next audio output value to 0 level
movwf ccprl_n
movlw ccp1con_val
movwf ccpcon_n
goto done_audio ;all done dealing with audio output this interrupt
;
; The audio output is enabled. The high 14 bits of AUDADR is the address
; of the program memory location the next two samples will come from.
; The low 2 bits indicate the "phase" within the two samples. The phases
; are:
;
; 0 - The next sample is interpolated between the high 7 bits
; at the AUDADR address minus 1, and the low 7 bits at the
; AUDADR address. AUDH is the high 7 bits from the AUDADR
; address minus 1.
;
; 1 - The next sample is the low 7 bits at the address in AUDADR.
; AUDL and AUDH are the samples from the AUDADR address.
;
; 2 - The next sample is interpolated between the low and high
; 7 bits at the address in AUDADR. AUDL and AUDH are the samples
; from the AUDADR address.
;
; 3 - The next sample is the high 7 bits at the address in AUDADR.
; AUDH is the high 7 bits from the AUDADR address.
;
; The phase IDs are deliberately arranged so that AUDADR can be incremented
; as a single 16 bit number to advance thru the phases and the source
; address at the same time.
;
; Now jump to different code for each of these phase cases.
;
intr_audon unbank
movlw high jump_phase ;init PCLATH for start of jump table
movwf pclath
dbankif gbankadr
movf audadr, w ;get phase in two low bits
andlw 3 ;set all other bits to 0
addlw low jump_phase ;compute low 8 bits of table entry
skip_ncarr ;no carry into upper byte ?
incf pclath ;propagate the carry
movwf pcl ;jump to the selected table entry
;
; Phase jump table. The entries in this table jump to the specific routines
; for each audio sample phase. Each phase routine must do the following:
;
; 1 - Make sure AUDL and AUDH are set as required for the next phase.
;
; 2 - Set ITMP1 to the 7 bit audio sample for the next interrupt.
;
; 3 - Go to PH_DONE.
;
jump_phase
goto ph0
goto ph1
goto ph2
goto ph3
;
;**********
;
; Audio sample phase 0.
;
; The new sample is interpolated between the previous and the low sample
; at the AUDADR address. The program memory word at the AUDADR address
; is read in this phase.
;
ph0 dbankis gbankadr
;
; Set the EEPROM address to the address in the high 14 bits of AUDADR.
;
dbankif gbankadr
bcf status, c ;set bit to shift in
rrf audadr+1, w
movwf itmp1 ;temp save high byte, still shifted left 1
rrf audadr+0, w
dbankif eeadr ;save low byte still shifted left 1
movwf eeadr
dbankif gbankadr
bcf status, c ;set bit to shift in
rrf itmp1, w
dbankif eeadrh
movwf eeadrh ;make final address high byte
dbankif eeadr
rrf eeadr ;make final address low byte
;
; Read the program memory word at EEADRH,EEADR.
;
dbankif eecon1
bsf eecon1, eepgd ;select program memory, not data EEPROM access
bsf eecon1, rd ;start the program memory read
nop ;wait for the program memory read to complete
nop
;
; The program memory word value in EEDATH,EEDATA.
;
; Now compute the new sample and save the two samples at this program memory
; location. The new sample will be the average of the old value in AUDH
; and the low 7 bits of EEDATA. The sample value must be written to
; ITMP1.
;
dbankif eedata
movf eedata, w ;get second value in low 7 bits of W
andlw h'7F' ;mask in only the low sample
dbankif gbankadr
movwf audl ;save low sample at this address
addwf audh ;make sum of the two samples to interpolate between
rrf audh, w ;make average of the two samples in low 7 bits
andlw h'7F' ;mask in only the average value
movwf itmp1 ;set audio sample value to output next time
dbankif eedata
rlf eedata, w ;set C to bit to shift in
dbankif eedath
rlf eedath, w ;get high sample value into W
andlw h'7F' ;mask in only the valid sample bits
dbankif gbankadr
movwf audh ;save high sample at this address
goto ph_done ;done with unique processing for this sample phase
;
;**********
;
; Audio sample phase 1.
;
; The new sample is the low sample at the AUDADR address. This is already
; in AUDL.
;
ph1 dbankis gbankadr
movf audl, w ;get the new sample value
movwf itmp1 ;set audio sample value to output next time
goto ph_done ;done with unique processing for this sample phase
;
;**********
;
; Audio sample phase 2.
;
; The new audio sample is interpolated between the low and high samples
; at the AUDADR address. These low and high samples are already in
; AUDH, AUDL.
;
ph2 dbankis gbankadr
movf audl, w ;get low sample
addwf audh, w ;make sum of the two samples
movwf itmp1 ;temp save sum
bcf status, c ;set bit to shift in
rrf itmp1 ;make average of the two samples in ITMP1
goto ph_done ;done with unique processing for this sample phase
;
;**********
;
; Audio sample phase 3.
;
; The new sample is the high sample at the AUDADR address. This is already
; in AUDH.
;
ph3 dbankis gbankadr
movf audh, w ;get the new sample value
movwf itmp1 ;set audio sample value to output next time
;
;**********
;
; Done with the unique code for the specific audio sample phase.
;
; AUDH and AUDL have been updated as necessary by the phase code, and
; the new 7 bit output sample for next time is in ITMP1.
;
ph_done unbank
;
; Use the ITMP1 value to update CCPRL_N and CCPCON_N. These are the
; values to write to the PWM hardware next interrupt to set it to the
; new audio sample value.
;
dbankif gbankadr
swapf itmp1, w ;get low two bits in position for CCP1CON register
andlw h'30' ;mask in just the two data bits
iorlw ccp1con_val ;merge with static CCP1CON bits
movwf ccpcon_n ;set CCP1CON value for next audio sample
rrf itmp1 ;get high 5 sample bits in low W
rrf itmp1, w
andlw h'1F' ;mask in just the 5 valid bits
movwf ccprl_n ;save CCPR1L value for next audio sample
;
; Advance to the next audio output phase.
;
dbankif gbankadr
movlw 1 ;get increment value
addwf audadr+0 ;increment the low byte
skip_ncarr ;no carry into high byte ?
incf audadr+1 ;propagate the carry
;
; Decrement the number of samples left to play in this audio sequence.
; FLAG_AUDON is reset when this number reaches zero. This will cause
; the audio output to go to the 0 level after the next sample.
;
dbankif gbankadr
movlw 1 ;get the decrement value
subwf audlft+0 ;decrement the low byte
skip_nborr ;no borrow from high byte ?
decf audlft+1 ;propagate the borrow
movf audlft+0, w ;make OR of both counter bytes
iorwf audlft+1, w
skip_z ;no more samples to play after this next one ?
goto done_audlft ;there are more samples left to go
dbankif gbankadr
bcf flag_audon ;indicate audio output off
done_audlft unbank
done_audio unbank ;all done dealing with audio issues this interrupt
;
;**********
;
; Decrement the CNT100MS counter and set FLAG_100MS if this is another
; 100mS tick.
;
dbankif gbankadr
movlw 1 ;get decrement value into W
subwf cnt100ms+0 ;decrement the low byte
skip_nborr ;no borrow from high byte ?
decf cnt100ms+1 ;propagate the borrow
movf cnt100ms+0, w ;set Z if whole counter is now at zero
iorwf cnt100ms+1, w
skip_z ;counter was just decremented to 0 ?
goto done_tick100ms ;no, done with 100mS tick processing
bsf flag_100ms ;set flag to indicate 100mS tick occurred
loadk16 cnt100ms, i100ms ;reset counter for next 100mS tick
done_tick100ms dbankis gbankadr ;done with 100mS tick processing
;
; Check for time to set FLAG_ADCONV. This causes the foreground routine
; to start another A/D conversion.
;
dbankif gbankadr
decfsz cntad ;count one less interrupt until start A/D
goto done_adstart ;not time for a new conversion yet
bsf flag_adconv ;cause new A/D conversion to be started soon
movlw iadconv ;reset number of interrupt until next A/D conversion
movwf cntad
done_adstart
;
;********************
;
; Restore state to when the interrupt occurred and return from interrupt.
;
clrf status ;register bank settings are now 0
dbankis 0
ibankis 0
if ncodepages > 1 ;multiple code pages may be in use ?
movf pclath_save, w ;restore PCLATH
movwf pclath
endif
if save_fsr ;FSR needs to be restored ?
movf fsr_save, w ;restore FSR
movwf fsr
endif
swapf status_save, w ;get old STATUS with nibble order restored
movwf status ;restore STATUS, register banks now unknown
swapf w_save ;swap nibbles in saved copy of W
swapf w_save, w ;restore original W
retfie ;return from interrupt, re-enable global interrupts
end