;*******************************************************************
;
;  Altair Cassette Interface
;
;    Data 1 is represented by a 2400hz carrier. Data 0 is represented
;    by an 1850hz carrier. Earliest boards used 2225hz and 2025hz. This
;    is still a 2125hz center frequency. The Kansas City Standard (KCS)
;    used 2400hz and 1200hz. This program reads any of these standards
;    but always writes using the 2400/1850hz frequencies.
;
;    Author: Mike Douglas
;
;  Revision: 1.2 (10-1-23)
;          - Allow use of the RS-232 port for data in/out from/to the
;	 cassette in addition to the normal connection to the Altair
;	 Clone (through the six pin header J1). This allows use of
;	 the board as a modem for an RS-232 connection. The RS-232
;	 option is the default. For use in an Altair Clone, install
;	 a jumper between pins 2&4 on the programming header.
;
;          - Eliminated the two cycle filter for reading data and went
;	 with an input capture ignore period of 50us to allow any
;	 oscillations of the comparator to settle out.
;	
;	 Checksum 8407
;
;  Revision: 1.1 (12-30-13), Update input frequencies to better match
;	 the actual 2125hz center frequency used for both the 
;	 early 2225/2025hz boards and the later 2400/1850hz boards.
;	 Drop the lowest frequency accepted from 1562hz to 1000hz
;	 so KCS tapes can be read (2400/1200hz). 
;	 Checksum: D833
;
;  Revision: 1.0 (9-20-13),  Initial release 
;	 Checksum: D771
;
;*******************************************************************
;
;  Processor is a 16F1823, Internal Oscillator w/o PLL at 8 Mhz
;
	list p=16f1823,b=12,r=dec
	include <p16f1823.inc>

_BORV_25	EQU	H'FBFF'	;Brown-out Reset Voltage (Vbor), high trip point selected.

	__config _CONFIG1, _FCMEN_OFF & _IESO_OFF & _CLKOUTEN_OFF & _BOREN_ON & _CPD_OFF & _CP_OFF & _MCLRE_ON & _PWRTE_ON & _WDTE_OFF & _FOSC_INTOSC
	__config _CONFIG2, _LVP_OFF & _BORV_25 & _STVREN_OFF & _PLLEN_OFF & _WRT_ALL
	
;  Misc Equates 

DATA1_PER	equ	104	;208us at 500khz (1/2 cycle of 2400hz)
DATA0_PER	equ	135	;270us at 500khz (1/2 cycle of 1850hz)
MIN_PERIOD	equ	632	;minimum valid input period in us * 2 (3165hz)
MID_PERIOD	equ	941	;midpoint input period in us * 2 (2125hz)
MAX_PERIOD	equ	2000	;maximum valid input period in us * 2 (1000hz)
GOOD_DATA	equ	20	;twenty good cassette in cycles in a row

;  Port A bit definitions

FROM_ALTAIR	equ	4	;serial data from Altair board
TO_ALTAIR	equ	5	;serial data to Altair board

;  Port C bit definitions

AUDIO_OUT	equ	0	;audio waveform out to cassette
RS232_OUT	equ	3	;serial data out to the RS-232 port
RS232_IN	equ	1	;seria data in from the RS-232 port

;  Data reception variables

	cblock	0x70	;common RAM all pages
captLo			;this capture low byte
captHi			;this capture high byte	
prevLo			;previous counter low byte
prevHi			;previous counter high byte
deltaLo			;difference low byte
deltaHi			;difference high byte
goodCnt			;count of good pulses in a row
flags			;flags byte
	endc

;  Flags variable equates

F_CASS_IN	equ	0	;1 = cassette input is active
F_CLONE	equ	1	;1 = running inside an Altair Clone

;************************************************************************
;
;  Program Space
;
;************************************************************************

;  Reset and interrupt vectors

	org	0		;page zero for jump sbrs
	goto	reset_pwr		;reset vector location zero
	goto	$		;loop here and die
	goto	$
	goto	$
	goto	tmr2_int		;interrupt vector location 0x04

;  Start of code. Reset entry point

reset_pwr	equ	$
	banksel	INTCON
	clrf	INTCON		;disable all interrupts just in case
	banksel	OSCCON
	movlw	b'01110000'		;8 Mhz internal clock
	movwf	OSCCON
	banksel	OPTION_REG
	movlw	b'01010110'		;weak pullups enabled, /128 on TMR0
	movwf	OPTION_REG
	
; Initialize PORT A

	banksel	PORTA
	movlw	b'00100000'		;serial output to Altair idle at 1
	movwf	PORTA
	banksel	LATA
	movwf	LATA		;init Port A latch = Port A
	banksel	ANSELA
	clrf	ANSELA		;all digital - no analog
	banksel	TRISA
	movlw	b'00011001'		;Pgmming bits input, bit 4 in from Altair
	movwf	TRISA		;  (bit 1 temporarily output for now)
	banksel	WPUA
	movlw	b'00011011'		;pullup on all input bits
	movwf	WPUA

; Initialize PORT C

	banksel	PORTC
	movlw	b'00001000'		;TX out to DB25 idle at 1
	movwf	PORTC		;no output bits really matter
	banksel	LATC
	movwf	LATC		;init Port C latch = Port C
	banksel	ANSELC
	movlw	b'00000100'		;RC2 used as analog comparator input
	movwf	ANSELC
	banksel	TRISC
	movlw	b'00100110'		;RC5 IC, RC2 audio in, RC1 RX DB25 in 
	movwf	TRISC
	banksel	WPUC
	movlw	b'00000000'		;no pullups used
	movwf	WPUC

; If jumper installed across programming header pins 2&4, then set flag for CLONE,
;    else, we're running with RS-232 host input

	clrf	flags		;all flags false
	banksel	PORTA
	btfss	PORTA,0		;header pin 2 shorted to pin 4?
	bsf	flags,F_CLONE	;yes, running inside the Altair Clone
	banksel	TRISA
	movlw	b'00011011'		;idle header pin 2 as input
	movwf	TRISA
	clrf	goodCnt		;no good cassette input cycles yet

; Initialize voltage reference, DAC and comparator for processing the audio input. 
;    The audio input comes in on C2- where it is compared to 0.25v on the + input.
;    The output of the comparator goes is connected externally to CCP1 (the input
;    capture pin). The comparator output is inverted so it goes high when audio
;    in is > 0.25 volts and goes low when audio input is < 0.25 volts.

	banksel	FVRCON		;fixed voltage reference
	movlw	b'10000100'		;FVR on, 1.024v output
	movwf	FVRCON

	banksel	DACCON0		;DAC control register
	movlw	8		;DAC 8/32 = .25v 
	movwf	DACCON1
	movlw	b'10001000'		;DAC on, FVR provides 1.024v
	movwf	DACCON0

	banksel	CM2CON0		;comparator 2 control register
	movlw	b'00010010'		;IN+ is DAC, IN- is C12IN2
	movwf	CM2CON1
	movlw	b'10110110'		;cmp on, output enabled, hysteresis on, inverted
	movwf	CM2CON0

;  Setup Timer 1 and input capture to measure the cycle length of the audio input.
;     The timer free-runs at 2 MHz. Input is captured on rising edges.

	banksel	T1CON		;timer 1 for input capture
	movlw	b'00000101'		;FOSC/4, no prescale, timer on
	movwf	T1CON

	banksel	CCP1CON		;capture control register
	movlw	b'00000101'		;capture mode, every rising edge
	movwf	CCP1CON

;  Setup Timer 2 to generate a compare every 1/2 cycle for 2400hz (one) or
;     1850hz (zero). The timer is run at 500khz. Set up to interrupt
;     on each compare match.

	banksel	TMR2		;timer 2 for audio out generation
	movlw	b'00000101'		;timer 2 on, F-Inst/4 = 500Khz
	movwf	T2CON
	clrf	TMR2		;start timer at zero
	movlw	DATA1_PER		;assume data out is 1
	movwf	PR2
	clrf	PIR1		;clear any pending interrupts 
	banksel	PIE1		;enable TMR2 interrupts
	movlw	b'00000010'		;enable TMR2 interrupts
	movwf	PIE1
	clrf	PIE2
	banksel	INTCON		;back to page zero registers
	movlw	b'11000000'		;global and peripheral interrupts enabled
	movwf	INTCON		;let things run

;---------------------------------------------------------------------------
;  Main loop
;    1) Copy data line from Altair directly to RS-232 output. The audio
;       output of the Altair data is done by the tmr2_int interrupt.
;
;    2) While cassette audio input is not present, copy data from the
;       RS-232 input directly to the Altair data input.
;
;    3) While cassette audio is being received, copy data from the
;       cassette input directly to the Altair data input.
;----------------------------------------------------------------------------
main	btfss	flags,F_CLONE	;running inside an Altair Clone?
	goto	chk_cass		;no

	btfss	PORTA,FROM_ALTAIR	;receiving one or zero?
	goto	set_rs232L		;data is zero
	bsf	PORTC,RS232_OUT	;set data out to one
	goto	chk_data_in
set_rs232L	bcf	PORTC,RS232_OUT	;set data out to zero

; chk_data_in - If not receiving from cassette, then copy RS-232 in to the Altair

chk_data_in	btfsc	flags,F_CASS_IN	;presently receiving audio?
	goto	chk_cass		;yes
	btfss	PORTC,RS232_IN	;copy RS-232 in to the Altair
	goto	set_altairL		;data is zero
	bsf	PORTA,TO_ALTAIR	;set data to Altair high
	goto	chk_cass		;try to detect audio
set_altairL	bcf	PORTA,TO_ALTAIR	;set data to Altair low
				;fall through to chk_cass

; chk_cass - process cassette audio input. 
;    If not presently receiving cassette data (F_CASS_IN false), then GOOD_DATA
;    pulses in a row from the cassette will set the F_CASS_IN flag true and data to
;    the Altair will then come from the cassette.
;
;    If presently receiving cassette data (F_CASS_IN true), then data to the Altair
;    comes from the cassette input. If TMR0 bit 7 becomes set, (8.2ms without a 
;    valid audio pulse), then F_CASS_IN is set back to false.

chk_cass	btfsc	PIR1,CCP1IF		;capture occur?
	goto	have_pulse		;yes

; No pulse occurred. If TMR0 bit 7 is set, then we've had 8.2ms of idle time.
;    Set F_CASS_IN false.

	btfss	TMR0,7
	goto	main		;not 8.2ms yet
	bcf	flags,F_CASS_IN	;no longer getting cassette data
	goto	main

; Capture occured, save the value capture in captHi/captLo

have_pulse	equ	$
	banksel	CCPR1L		;save the capture value
	movfw	CCPR1H
	movwf	captHi
	movfw	CCPR1L
	movwf	captLo

; Ignore captures for 50us to let possible comparator oscillations cease

	movlw	33		;33 loops = 50us
delay50	decfsz	WREG,F
	goto	delay50

; Compute the time since last capture in deltaHi/deltaLo

	banksel	PORTA
	bcf	PIR1,CCP1IF		;clear the capture flag
	movfw	prevLo		;subtract previous value
	subwf	captLo,W		;lsb first
	movwf	deltaLo
	movfw	prevHi		;ms byte	
	subwfb	captHi,W	
	movwf	deltaHi

; Save current capture time as the previous capture time

	movfw	captLo		;save current timer as previous
	movwf	prevLo
	movfw	captHi
	movwf	prevHi

; See if period is shorter than MIN_PERIOD. If so, treat as invalid pulse
;    but a data one.

	movlw	low(MIN_PERIOD)
	subwf	deltaLo,W
	movlw	high(MIN_PERIOD)
	subwfb	deltaHi,W
	btfsc	STATUS,C		;carry clear if < MIN_PERIOD
	goto	chk_mid
	clrf	goodCnt		;invalid pulse
	goto	cass_in1		;treat as data 1

; See if period is shorter than MID_PERIOD. If so, treat as a valid pulse
;     and a data one (2400hz)

chk_mid	movlw	low(MID_PERIOD)
	subwf	deltaLo,W
	movlw	high(MID_PERIOD)
	subwfb	deltaHi,W
	btfsc	STATUS,C		;carry clear if < MID_PERIOD
	goto	chk_max
	incf	goodCnt,F		;have a good pulse
	clrf	TMR0		;reset bad data timeout
	goto	cass_in1		;cassette data is a one

; See if period is shorter than MAX_PERIOD. If so, treat as a valid pulse
;     and a data zero (1850hz)

chk_max	movlw	low(MAX_PERIOD)
	subwf	deltaLo,W
	movlw	high(MAX_PERIOD)
	subwfb	deltaHi,W
	btfsc	STATUS,C		;carry clear if < MAX_PERIOD
	goto	bad_zero
	incf	goodCnt,F		;have a good pulse
	clrf	TMR0		;reset bad data timeout
	goto	cass_in0		;cassette data is a zero

; Period is longer than MAX_PERIOD. Treat as an invalid pulse and data zero

bad_zero	clrf	goodCnt		;invalid pulse
				;fall through to data zero
;  Received a 0 from the cassette

cass_in0	btfss	flags,F_CASS_IN	;are we accepting cassette input data?
	goto	chk_goodCnt		;no

	btfss	flags,F_CLONE	;skip RS-232 out if in an Altair Clone
	bcf	PORTC,RS232_OUT	;output data via RS-232
	bcf	PORTA,TO_ALTAIR	;output to Altair now zero
	goto	main

;  Received a 1 from the cassette

cass_in1	btfss	flags,F_CASS_IN	;are we accepting cassette input data?
	goto	chk_goodCnt		;no

	btfss	flags,F_CLONE	;skip RS-232 out if in an Altair Clone
	bsf	PORTC,RS232_OUT	;output data via RS-232
	bsf	PORTA,TO_ALTAIR	;output to Altair now 1
	goto	main

; chk_goodCnt - if goodCnt reaches GOOD_DATA then set F_CASS_IN true;

chk_goodCnt	movlw	GOOD_DATA
	subwf	goodCnt,W		;reach GOOD_DATA in a row?
	btfss	STATUS,C		;carry set if goodCnt >= GOOD_DATA	
	goto	main
	bsf	flags,F_CASS_IN	;now accepting cassette data
	goto	main

;----------------------------------------------------------------------------
;
; tmr2_int
;    Timer 2 interrupt occurs every 1/2 cycle of the audio output waveform.
;    This is 208us for 2400hz (one) or 270us for 1850 hz (zero). Clear
;    the interrupt, toggle the output state and set the period register
;    based on the current data input from the Altair.
;
;----------------------------------------------------------------------------
tmr2_int	equ	$
	banksel	PORTA		;select bank 0
	bcf	PIR1,TMR2IF		;clear the interrupt
	btfss	PORTC,AUDIO_OUT	;presently high or low?
	goto	set_audioH		;is low, go set high
	bcf	PORTC,AUDIO_OUT	;was high, set low
	goto	tmr2_next		;set next wakeup
set_audioH	bsf	PORTC,AUDIO_OUT	;was low, set high

tmr2_next	movlw	DATA1_PER		;assume data is one
	btfss	flags,F_CLONE	;inside the Altair Clone?
	goto	rs232_host		;no

	btfss	PORTA,FROM_ALTAIR	;receiving one or zero from Altair?
 	movlw	DATA0_PER		;receiving zero
	movwf	PR2		;set next wakeup
	retfie			

rs232_host	btfss	PORTC,RS232_IN	;receiving one or zero from host?
	movlw	DATA0_PER		;receiving zero
	movwf	PR2		;set next wakeup
	retfie			

	end