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