;**************************************************************************
; FILE:      theremin.asm                                                 *
; CONTENTS:  Junior Theremin                                              *
; COPYRIGHT: MadLab Ltd. 2001-2003                                        *
; AUTHOR:    James Hutchby                                                *
; UPDATED:   10/11/03                                                     *
;**************************************************************************

     list p=12C508A
;    list p=12C509A

     ifdef __12C508A
     include "p12c508a.inc"
     endif
     ifdef __12C509A
     include "p12c509a.inc"
     endif

;    __config _IntRC_OSC & _WDT_OFF & _MCLRE_OFF & _CP_OFF
     __config _IntRC_OSC & _WDT_OFF & _MCLRE_OFF & _CP_ON

     __idlocs h'CD10'

     errorlevel -302,-305


;**************************************************************************
;                                                                         *
; Specification                                                           *
;                                                                         *
;**************************************************************************

; power-up self-test - all LEDs flash twice
; double beep then self-calibrates

; re-calibrates every 5s or less

; tone frequency = ~500Hz - ~4kHz

; button1 decreases range by an octave
; button2 increases range by an octave
; both buttons toggles slide/discrete mode

; sleeps after 5 minutes of inactivity, either button to wake


;**************************************************************************
;                                                                         *
; Port assignments                                                        *
;                                                                         *
;**************************************************************************

GPIO_IN        equ  b'011111'      ; GPIO IN status
GPIO_OUT       equ  b'011100'      ; GPIO OUT status

SPEAKER_PORT   equ  GPIO           ; speaker port
SPEAKER1       equ  4              ; speaker output1
SPEAKER2       equ  5              ; speaker output2
SPEAKER_MASK   equ  b'110000'      ; speaker mask

SPEAKER_ON     equ  GPIO_OUT&~(1<<SPEAKER1)
SPEAKER_OFF    equ  GPIO_OUT|(1<<SPEAKER1)

BUTTON_PORT    equ  GPIO           ; button port
BUTTON1        equ  1              ; button1
BUTTON2        equ  0              ; button2
BUTTON_MASK    equ  b'000011'      ; button mask

LED_PORT       equ  GPIO           ; LED port
LED1           equ  1              ; LED #4
LED2           equ  4+1            ; LED #3
LED3           equ  0              ; LED #2
LED4           equ  4+0            ; LED #1
MUX            equ  5              ; LED multiplexer
LED_MASK       equ  b'000011'      ; LED mask


;**************************************************************************
;                                                                         *
; Constants and timings                                                   *
;                                                                         *
;**************************************************************************

CLOCK     equ  d'4000000'          ; processor clock frequency in Hz

SAMPLE1   equ  d'10'               ; slide sample period in ms
SAMPLE2   equ  d'125'              ; discrete sample period in ms

SLIDE_THRESHOLD     equ  d'6'
SLIDE_UPPER         equ  d'250'
SLIDE_LOWER         equ  d'50'

RECALIBRATE    equ  d'5000'        ; recalibration time in ms

BEEP_PITCH     equ  d'75'          ; beep pitch
BEEP_PERIOD    equ  d'250'         ; beep period

TIMEOUT   equ  d'300'*d'100'       ; sleep timeout period in 1/100s


;**************************************************************************
;                                                                         *
; File register usage                                                     *
;                                                                         *
;**************************************************************************

RAM  equ  h'07'

     cblock RAM
     LEDS                     ; multiplexed LEDs
     buttons                  ; buttons pressed
     mode                     ; mode (0 = slide, 1 = discrete)
     ndx                      ; index
     note                     ; current note, -1 if silent
     last                     ; last note, -1 if silent
     period                   ; note period in cycles/16
     sample:2                 ; sample period in cycles/16
     pulses:2                 ; pulse counter
     base:2                   ; baseline pulse count
     highest:2                ; highest pulse count
     toggle                   ; toggle time
     recal                    ; recalibration timer
     timer:2                  ; sleep timer
     count                    ; scratch counter
     work1, work2             ; work registers
     endc


;**************************************************************************
;                                                                         *
; Macros                                                                  *
;                                                                         *
;**************************************************************************

routine   macro label              ; routine
label
          endm

table     macro label              ; define lookup table
label     addwf PCL
          endm

entry     macro value              ; define table entry
          retlw value
          endm

index     macro label              ; index lookup table
          call label
          endm

jump      macro label              ; jump through table
          goto label
          endm

tstw      macro                    ; test w register
          iorlw 0
          endm

movff     macro f1,f2              ; move file to file
          movfw f1
          movwf f2
          endm

movlf     macro n,f                ; move literal to file
          movlw n
          movwf f
          endm


;--------------------------------------------------------------------------
; reset vector
;--------------------------------------------------------------------------

          org 0

          movwf OSCCAL

          goto main_entry


;**************************************************************************
;                                                                         *
; Lookup tables                                                           *
;                                                                         *
;**************************************************************************

          table pulse_table

          entry d'50'
          entry d'55'
          entry d'60'
          entry d'65'
          entry d'70'
          entry d'75'
          entry d'80'
          entry d'85'

          entry 0


          table period_table

C1_FREQ        equ  d'523'              ; ~523.2 Hz
D1_FREQ        equ  d'587'              ; ~587.3 Hz
E1_FREQ        equ  d'659'              ; ~659.2 Hz
F1_FREQ        equ  d'698'              ; ~698.4 Hz
G1_FREQ        equ  d'784'              ; ~783.9 Hz
A2_FREQ        equ  d'880'              ; ~879.9 Hz
B2_FREQ        equ  d'988'              ; ~987.7 Hz
C2_FREQ        equ  d'1046'             ; ~1046.4 Hz
D2_FREQ        equ  d'1175'             ; ~1174.5 Hz
E2_FREQ        equ  d'1318'             ; ~1318.4 Hz
F2_FREQ        equ  d'1397'             ; ~1396.8 Hz
G2_FREQ        equ  d'1568'             ; ~1567.8 Hz
A3_FREQ        equ  d'1760'             ; ~1759.8 Hz
B3_FREQ        equ  d'1975'             ; ~1975.3 Hz
C3_FREQ        equ  d'2093'             ; ~2092.8 Hz
D3_FREQ        equ  d'2349'             ; ~2349.1 Hz
E3_FREQ        equ  d'2637'             ; ~2636.8 Hz
F3_FREQ        equ  d'2794'             ; ~2793.6 Hz
G3_FREQ        equ  d'3136'             ; ~3135.7 Hz
A4_FREQ        equ  d'3520'             ; ~3519.7 Hz
B4_FREQ        equ  d'3951'             ; ~3950.7 Hz
C4_FREQ        equ  d'4186'             ; ~4185.6 Hz

note_     macro freq
          entry (CLOCK/(freq*2))/d'16'
          endm

          entry d'250'

          note_ C1_FREQ
          note_ D1_FREQ
          note_ E1_FREQ
          note_ F1_FREQ
          note_ G1_FREQ
          note_ A2_FREQ
          note_ B2_FREQ
          note_ C2_FREQ
          note_ D2_FREQ
          note_ E2_FREQ
          note_ F2_FREQ
          note_ G2_FREQ
          note_ A3_FREQ
          note_ B3_FREQ
          note_ C3_FREQ
          note_ D3_FREQ
          note_ E3_FREQ
          note_ F3_FREQ
          note_ G3_FREQ
          note_ A4_FREQ
          note_ B4_FREQ
          note_ C4_FREQ


          table patterns_table

pattern_  macro leds,repeat
          variable i = repeat
          while i > 0
          entry leds
i         set i-1
          endw
          endm

          pattern_ b'0000',1
          pattern_ b'0001',1
          pattern_ b'0010',1
          pattern_ b'0100',1
          pattern_ b'1000',1
          pattern_ b'1001',1
          pattern_ b'1010',1
          pattern_ b'1100',2
          pattern_ b'1101',2
          pattern_ b'1110',2
          pattern_ b'1111',3


;**************************************************************************
;                                                                         *
; Procedures                                                              *
;                                                                         *
;**************************************************************************

;--------------------------------------------------------------------------
; polls the pushbuttons, returns NZ flag set if either pushbutton pressed
;--------------------------------------------------------------------------

          routine poll

          movff GPIO,work1

          movlw GPIO_IN                 ; input mode
          tris GPIO
          bcf LED_PORT,MUX

          iorwf GPIO                    ; poll the buttons
          clrwdt
          comf GPIO,w
          movwf work2

          movff work1,GPIO              ; re-initialise port
          incf note,w
          movlw GPIO_OUT
          skpz
          andlw ~(1<<SPEAKER1)
          tris GPIO

          movfw work2
          andlw BUTTON_MASK
          movwf buttons

          retlw 0


;--------------------------------------------------------------------------
; multiplexes the LEDs
;--------------------------------------------------------------------------

          routine get_mux

          movwf LEDS

do_bit    macro bit,led
          btfsc LEDS,bit
          if led < 4
          iorlw 1<<led
          else
          andlw ~(1<<led)
          endif
          endm

          movlw LED_MASK<<4             ; determine port I/O data
          do_bit 0,LED1
          do_bit 1,LED2
          do_bit 2,LED3
          do_bit 3,LED4
          movwf LEDS

          retlw 0


;--------------------------------------------------------------------------
; toggles the speaker and sets the LEDs
;--------------------------------------------------------------------------

do_speaker     macro                    ; [28]

          movfw LEDS                    ; set LEDs [4]
          btfss LED_PORT,MUX            ; [4/8]
          swapf LEDS,w                  ; [4]
          xorwf LED_PORT,w              ; [4]
          andlw LED_MASK                ; [4]

          iorlw SPEAKER_MASK            ; toggle speaker output [4]
          xorwf SPEAKER_PORT            ; [4]

          endm


;--------------------------------------------------------------------------
; waits, fed with the wait in 1/100s in the w reg
;--------------------------------------------------------------------------

          routine wait

          movwf count

          movlw SPEAKER_OFF             ; speaker off
          tris GPIO
          bsf SPEAKER_PORT,SPEAKER1
          bcf SPEAKER_PORT,SPEAKER2

wait1     movlf CLOCK/(d'100'*d'16'*d'256'),work1

wait2     do_speaker

          clrf work2
wait3     clrwdt                        ; [4]
          decfsz work2                  ; [4]
          goto wait3                    ; [8]

          decfsz work1
          goto wait2

          decfsz count
          goto wait1

          clrf GPIO

          retlw 0


;--------------------------------------------------------------------------
; beeps
;--------------------------------------------------------------------------

          routine beep

          movlw SPEAKER_ON              ; speaker on
          tris GPIO
          bsf SPEAKER_PORT,SPEAKER1
          bcf SPEAKER_PORT,SPEAKER2

          movlf BEEP_PERIOD,work1

beep1     do_speaker                    ; toggle speaker output

          movlf BEEP_PITCH,work2        ; half-cycle delay
beep2     clrwdt
          decfsz work2
          goto beep2

          decfsz work1
          goto beep1

          movlw SPEAKER_OFF             ; speaker off
          tris GPIO
          clrf GPIO

          retlw 0


;--------------------------------------------------------------------------
; counts pulses while playing a note
;--------------------------------------------------------------------------

do_timing      macro f                  ; timing loop [16 * f]

          local dot1

dot1      clrwdt                        ; [4]
          decfsz f                      ; [4/8]
          goto dot1                     ; [8]
          nop                           ; [4]

          endm


do_count       macro                    ; [32]

          movfw TMR0                    ; low byte of pulse counter [4]
          xorwf pulses+1,w              ; [4]
          xorwf pulses+1                ; pulses+1 <= TMR0 [4]

          xorlw h'80'                   ; determine if TMR0 has rolled over [4]
          iorwf pulses+1,w              ; [4]
          andlw h'80'                   ; [4]
          skpnz                         ; [8/4]
          incf pulses+0                 ; increment high byte if yes [4]

          endm


wait_speaker   macro

          local spk1

          clrwdt                        ; wait for toggle time
spk1      movfw TMR0
          subwf toggle,w
          andlw ~1
          bnz spk1

          do_speaker

          movfw period                  ; next toggle time
          addwf toggle

          endm


          routine count_pulses

          incf note,w                   ; get note period
          skpz
          btfsc mode,0
          index period_table
          movwf period

PERIOD1   set  (SAMPLE1*CLOCK)/(d'1000'*d'16')
PERIOD2   set  (SAMPLE2*CLOCK)/(d'1000'*d'16')

          movlw high PERIOD1            ; initialise sample period
          btfsc mode,0
          movlw high PERIOD2
          movwf sample+0
          movlw low PERIOD1
          btfsc mode,0
          movlw low PERIOD2
          movwf sample+1

          movfw period                  ; initial subtraction
          subwf sample+1
          skpc
          decf sample+0

          clrf pulses+0                 ; clear pulse counter
          clrf pulses+1

          incf last,w                   ; note playing ?
          bz count2                     ; branch if not

          wait_speaker

          movlw d'5'                    ; adjust toggle time
          subwf toggle

          clrwdt                        ; wait for toggle time
count1    movfw TMR0
          subwf toggle,w
          andlw ~1
          bnz count1

count2    incf last,w                   ; speaker change of state ? [4]
          bz count3                     ; [8]
          incf note,w                   ; [4]
          bnz count4                    ; [12]
count3    movfw last
          andwf note,w
          xorlw h'ff'
          bz count4                     ; branch if not

          incf note,w                   ; speaker on or off
          movlw SPEAKER_ON
          skpnz
          movlw SPEAKER_OFF
          tris GPIO
          bsf SPEAKER_PORT,SPEAKER1
          bcf SPEAKER_PORT,SPEAKER2

count4    movff note,last               ; [8]

          movlw b'00101111'             ; count low-to-high transitions on RTCC pin [4]
          clrwdt                        ; no prescaling, weak pull-ups enabled [4]
          clrf TMR0                     ; wake on pin change [4]
          option                        ; [4]
          clrwdt                        ; [4]
          nop                           ; [4]

          clrf TMR0                     ; initialise TMR0 [4]

          nop                           ; 2 instruction cycle delay [4]
          nop                           ; after writing to TMR0 [4]

; -- start of pulse counting --

CYCLES1   equ  d'112'

count5    do_speaker                    ; toggle speaker output [28]

          movlw CYCLES1/d'16'           ; initialise timer [4]
          subwf period,w                ; [4]
          movwf work1                   ; [4]

          do_timing work1               ; timing loop [16 * work1]

          do_count                      ; get pulses [32]

          nop                           ; [4]
          nop                           ; [4]
          nop                           ; [4]

          movfw period                  ; decrement sample period [4]
          subwf sample+1                ; [4]
          skpc                          ; [8/4]
          decf sample+0                 ; [4]

          btfss sample+0,7              ; finished ? [8/4]
          goto count5                   ; loop if not [8]
          nop                           ; [4]

; -- last iteration --

CYCLES2   equ  d'112'

          do_speaker                    ; toggle speaker output [28]

          movfw period                  ; remainder [4]
          addwf sample+1                ; [4]

          incf sample+1,w               ; initialise timer [4]
          movwf work1                   ; ensure not zero [4]

          do_timing work1               ; timing loop [16 * work1]

; -- end of pulse counting --

          do_count                      ; get final pulses [32]

          nop                           ; [4]

          movfw sample+1                ; [4]
          subwf period,w                ; [4]
          movwf work1                   ; [4]
          movlw CYCLES2/d'16'           ; [4]
          subwf work1                   ; [4]
          skpz                          ; [4]
          skpc                          ; [8]
          goto count6

          do_timing work1               ; timing loop [16 * work1]

count6    do_speaker                    ; toggle speaker output [28]

          incf note,w                   ; note playing ? [4]
          bz count7                     ; exit if not [8]

          clrwdt                        ; count instructions, prescale RTCC by 4 [4]
          movlw b'00000001'             ; weak pull-ups enabled, wake on pin change [4]
          option                        ; [4]
          nop                           ; [4]
          nop                           ; [4]

          clrf TMR0                     ; initialise TMR0 [4]

          movff period,toggle           ; toggle time
          movlw d'4'
          subwf toggle

count7    retlw 0


;--------------------------------------------------------------------------
; power down
;--------------------------------------------------------------------------

          routine power_down

          movlw b'111111'               ; read port
          tris GPIO
          movwf GPIO
          nop
          movfw GPIO

          sleep                         ; standby mode


;--------------------------------------------------------------------------
; main entry point
;--------------------------------------------------------------------------

          routine main_entry

          clrf GPIO                     ; initialise port
          movlw GPIO_OUT
          tris GPIO

          clrwdt
          movlw b'00000000'             ; weak pull-ups enabled, wake on pin change
          option

          movlw b'1111'                 ; flash LEDs twice
          call get_mux
          movlw d'25'
          call wait
          movlw b'0000'
          call get_mux
          movlw d'25'
          call wait
          movlw b'1111'
          call get_mux
          movlw d'25'
          call wait
          clrf LED_PORT

          clrf mode                     ; slide mode
          decf mode


;--------------------------------------------------------------------------
; next mode
;--------------------------------------------------------------------------

          routine next_mode

          movlw -1
          movwf last
          movwf note

          movlw b'0000'
          call get_mux

          call beep                     ; double beep
          movlw d'10'
          call wait
          call beep

next1     call poll                     ; wait for buttons to be released
          bnz next1

          incf mode                     ; next mode
          bcf mode,1

          call count_pulses             ; baseline pulse count
          movff pulses+0,base+0
          movff pulses+1,base+1

          clrf highest+0
          clrf highest+1

          movlw h'ff'                   ; initialise recalibration timer
          btfsc mode,0
          movlw RECALIBRATE/SAMPLE2
          movwf recal


;--------------------------------------------------------------------------
; main loop
;--------------------------------------------------------------------------

          routine main_loop

          movlf high TIMEOUT,timer+0    ; initialise sleep timer
          movlf low TIMEOUT,timer+1

loop0     clrwdt

          call count_pulses             ; count pulses

          movfw pulses+0                ; store highest pulse count
          subwf highest+0,w
          movwf work1
          movfw pulses+1
          subwf highest+1,w
          skpc
          decf work1

          btfss work1,7
          goto loop1

          movff pulses+0,highest+0
          movff pulses+1,highest+1

loop1     decfsz recal                  ; re-calibrate ?
          goto loop2                    ; branch if not

          movff highest+0,base+0        ; new baseline
          movff highest+1,base+1

          clrf highest+0
          clrf highest+1

          movlw h'ff'                   ; recharge recalibration timer
          btfsc mode,0
          movlw RECALIBRATE/SAMPLE2
          movwf recal

loop2     movfw pulses+0                ; determine pulse count delta
          subwf base+0,w
          movwf pulses+0
          movfw pulses+1
          subwf base+1,w
          movwf pulses+1
          skpc
          decf pulses+0

          btfss pulses+0,7              ; negative delta ?
          goto loop3                    ; branch if not

          movfw pulses+0                ; adjust baseline
          subwf base+0
          movfw pulses+1
          subwf base+1
          skpc
          decf base+0

          clrf pulses+0
          clrf pulses+1

loop3     movlw b'0000'
          call get_mux

          btfsc mode,0                  ; slide mode ?
          goto loop4                    ; branch if not

          movlf -1,note

          movlw SLIDE_THRESHOLD         ; threshold reached ?
          subwf pulses+1
          skpc
          decf pulses+0
          btfsc pulses+0,7
          goto loop7                    ; branch if not

          clrc
          rlf pulses+1
          rlf pulses+0

          tstf pulses+0                 ; limit to single byte
          movlw h'ff'
          skpz
          movwf pulses+1

          swapf pulses+1,w              ; more LEDS on as frequency
          movwf work1                   ; increases
          rlf work1,w
          rlf work1
          movfw work1
          btfsc work1,4
          movlw h'0f'
          andlw h'0f'
          index patterns_table
          call get_mux                  ; multiplex LEDs

          movlf SLIDE_UPPER-SLIDE_LOWER,note
          movfw pulses+1
          subwf note
          skpc
          clrf note
          movlw SLIDE_LOWER
          addwf note

          goto loop7

loop4     clrf ndx                      ; determine the note
          clrf work1

loop5     movfw ndx
          index pulse_table
          tstw
          bz loop6

          subwf pulses+1
          skpc
          decf pulses+0

          btfsc pulses+0,7
          goto loop6

          incf ndx

          clrc
          tstf work1
          skpnz
          setc
          rlf work1

          incf last,w                   ; note playing ?
          bz loop5                      ; branch if not

          wait_speaker

          goto loop5

loop6     swapf work1,w
          iorwf work1,w
          call get_mux                  ; multiplex LEDs

          decf ndx,w                    ; note
          movwf note

          incf note,w                   ; middle octave if not silent
          movlw d'7'
          skpz
          addwf note

loop7     call poll                     ; both pushbuttons pressed ?
          movfw buttons
          xorlw BUTTON_MASK
          bz next_mode                  ; branch if yes

          btfss mode,0                  ; discrete mode ?
          goto loop8                    ; branch if not

          incf note,w                   ; silent ?
          bz loop8                      ; branch if yes

          movlw d'7'                    ; octave -
          btfsc buttons,BUTTON1
          subwf note

          movlw d'7'                    ; octave +
          btfsc buttons,BUTTON2
          addwf note

loop8     incf note,w                   ; silent ?
          bnz main_loop                 ; branch if not

          movlw SAMPLE1/d'10'
          btfsc mode,0
          movlw SAMPLE2/d'10'
          subwf timer+1
          skpc
          decf timer+0

          btfsc timer+0,7               ; sleep if timeout
          goto power_down

          goto loop0


          ifdef __12C508A
;         org h'1ff'                    ; *** comment for OTP part ***
;         goto main_entry
          endif
          ifdef __12C509A
;         org h'3ff'                    ; *** comment for OTP part ***
;         goto main_entry
          endif


          end