	;; Software to control my laser scanner.
	;; C. Scott Ananian (MAS863 assignment 3)
	;; (c) 2002
	
	processor p16f876
;	EXPAND
	
	include "p16f876.inc"
	;; DEBUGGING OPTIONS
;#define	DEBUG_FORCE_VERTICAL_LINES
;#define	DEBUG_NO_ALPHABET_LOOKUP
;#define	DEBUG_ALL_MESSAGE_CHARS_ZERO
;#define	DEBUG_DISABLE_SCROLL
;#define	DEBUG_8MHz_CLOCK
;#define	DEBUG_SERIAL
;#define	DEBUG_INCLUDE_KARAOKE
#define	STANDALONE

	;; define a macro to ensure safe table math (no overflows of PCL)
padtab	macro tablesize
	if (($ & 0xFF) > (0xFF-tablesize))
		nop
		padtab tablesize
	endif
	endm

	;; note that 70h-7fh is the same no matter what bank we're in.
W_TEMP		EQU	H'7F'	; W storage during interrupt.
STATUS_TEMP	EQU	H'7E'	; STATUS storage during interrupt
PCLATH_TEMP	EQU	H'75'	; PCLATH storage during interrupt

CUR_PERIOD_L	EQU	H'7C'	; the current rotation quarter-period
CUR_PERIOD_H	EQU	H'7D'	; " " (high byte)

SCAN_START_L	EQU	H'7A'	; the starting time of this scan
SCAN_START_H	EQU	H'7B'	; " " (high byte)

WHICH_SCAN	EQU	H'79'	; keep track of 1 of 4 rising/falling edges
SCAN_STATE	EQU	H'78'	; bit 0: "scan start" flag.
				; bit 1: temp for table lookup

LOOKUP_TEMP	EQU	H'77'	; temporary for use of table lookup routines
SCROLL_TEMP	EQU	H'76'	; temporary for use during do_scroll interrupt

SCROLL_SPEED	EQU	H'73'	; scroll speed.  1=fastest, 255=slowest
				;                0=no scroll
CURRENT_SCROLL	EQU	H'72'	; # of interrupts until next scroll event.

DEBUG_TEMP	EQU	H'71'	; used for serial debugging.
DEBUG_TEMP2	EQU	H'70'

	;; this stuff is only in bank 0.
CURCHARNUM	EQU	H'20'	; current character # in message.
CURLINENUM	EQU	H'21'	; current line # we're scanning
CURDOTS		EQU	H'22'	; a byte w/ the current pixels we're outputting
				; self-delimiting; bit 7 is the 'next' dot.
LINE_TIME_L	EQU	H'23'	; timer ticks for one line
LINE_TIME_H	EQU	H'24'	;   (1/6 of 4*CUR_PERIOD_H/L)
LINE_START_L	EQU	H'25'	; last line start time.
LINE_START_H	EQU	H'26'	; last line start time.
LINECHARNUM	EQU	H'27'	; which char starts the line
LINEDOTOFF	EQU	H'28'	; how many dots into that char do we start
NUM_PIXELS	EQU	H'29'	; number of pixels left in the line.
PIXEL_TIME	EQU	H'30'	; timer ticks for one pixel
TEMP_L		EQU	H'31'	; next start-of-bit time.
TEMP_H		EQU	H'32'

MESSAGE_LOW	EQU	H'33'	; starting address of current message
MESSAGE_HIGH	EQU	H'34'
MESSAGE_CHARS	EQU	H'35'	; number of characters in this message

	IFDEF STANDALONE
	org	0x0
	goto start_jump
	ENDIF
	
	org 0x4
	goto	tach_int	; tachometer edge interrupt.
	
	org 0x80
start_jump:
	goto start

	;; ** THIS ARE THE MESSAGES WE'RE GOING TO DISPLAY **
NUM_MESSAGES	EQU	D'1'	; how many of them do we have?
	
	;; this is a table of line offsets (in pixels) to compensate for
	;; the fact that the hexagonal mirrors aren't precisely hexagonal.
get_line_offset:
	movlw	HIGH line_offset_table
	movwf	PCLATH		; load PCLATH with MSB of adjust_table
	movf	CURLINENUM, W
	padtab	D'7'		; ensure that we're not too near page boundary.
	addwf	PCL, F
line_offset_table:
	dt D'7'			; for line 0
	dt D'5'			; for line 1
	dt D'3'			; for line 2
	dt D'2'			; for line 3
	dt D'1'			; for line 4
	dt D'0'			; for line 5

	;; This is where the code actually starts.
start:
	;; tachometer is hooked up to CCP1 and CCP2
	;; laser modulation is PB0
	;; pushbutton on RC5

	;; set up our state variables
	clrf	STATUS 		; bank 0
	clrf	WHICH_SCAN
	clrf	SCAN_STATE
	movlw	0x0c		; start twelve chars into message.
	movwf	LINECHARNUM
	clrf	LINEDOTOFF	; no dot offset.
	movlw	0x18		; medium-fast scroll speed
	movwf	SCROLL_SPEED
	movwf	CURRENT_SCROLL
			
	;; set up ports.
	clrf	PORTB
	bsf	STATUS, RP0	; bank 1
	movlw	B'11110000'
	movwf	TRISB		; PB0-3 are outputs; PB4-7 are inputs
	movlw	B'00100110'	; RC5/CCP1/CCP2 are inputs
	movwf	TRISC
	IFNDEF	DEBUG_DISABLE_SCROLL
	;; set up timer 0
	movlw	B'01010101'	; 1:64 prescaler; port b pullups enabled
	movwf	OPTION_REG
	ENDIF ; DEBUG_DISABLE_SCROLL 
	;; set up timer 1
	bcf	STATUS, RP0	; bank 0
	movlw	B'00000100'	; ccp1 captures on falling edges
	movwf	CCP1CON
	movlw	B'00000101'	; ccp2 captures on rising edges
	movwf	CCP2CON
	movlw	B'00010101'	; enables timer1 in timer mode.
	movwf	T1CON
	;; initialize starting message (also read port B before we set RBIE))
	call	read_port_and_setup_message
	;; enable interrupts
	bsf	STATUS, RP0	; bank 1
	movlw	B'00000100'
	movwf	PIE1
	movlw	B'00000001'
	movwf	PIE2	
	bcf	STATUS, RP0	; bank 0
	clrf	PIR1		; clear all pending interrupt flags
	clrf	PIR2
	clrf	INTCON
	IFNDEF	DEBUG_DISABLE_SCROLL
	bsf	INTCON, T0IE	; enable timer 0 (scroll) interrupt
	ENDIF ; DEBUG_DISABLE_SCROLL
	bsf	INTCON, PEIE	; enable peripheral interrupts
	bsf	INTCON, GIE	; set the global interrupt enable
		
	;; wait for one complete cycle to complete first.
	bcf	SCAN_STATE, 0
wait1:	
	btfss	SCAN_STATE, 0
	goto wait1

	bcf	SCAN_STATE, 0
	;; sweep starts when bit 0 of SCAN_STATE is *set*
frame_top:
wait3:	
	btfss	SCAN_STATE, 0
	goto wait3
	bcf	SCAN_STATE, 0	; reset so it can be set at start of next scan

	;; check RC5 -> if switch is depressed (RC5 low), then skip 1/4 frame.
	btfsc	PORTC, 5
	goto	no_pushbutton
	incf	WHICH_SCAN, F
	;; wait for switch to be unpressed (debounce)
wait4:	btfss	PORTC, 5
	goto	wait4
	;; now wait for start of next frame
	bcf	SCAN_STATE, 0
	goto	frame_top
no_pushbutton:
	IF NUM_MESSAGES > 1
	bcf	INTCON, GIE	; disable interrupts during message setup
	btfsc	INTCON,	RBIF	; change current message if PORTB has changed
	call	read_port_and_setup_message ; can't scroll during this!
	bsf	INTCON, GIE	; re-enable interrupts.
	ENDIF
	
	;; we're going to divide the rev time (4*CUR_PERIOD) by 1024 to get
	;; a per-pixel time, and divide by 6 to get a per-line time.
	;; this gives us 170 2/3 pixels per line; we're going to give
	;; ourselves timing margins by only emitting 160 pixels per line.

	;; give a sufficient offset to start at the beginning of a line.
	;; should be around 50 *pixel times* (can be up to 16 fewer)
	;; we're using 48:
	;; so -> add (CUR_PERIOD_H/L << 4 + CUR_PERIOD_H/L << 5) >> 8
	;; += CUR_PERIOD_H/L>>4  += CUR_PERIOD_H/L>>3
	;; also compute LINE_TIME_H/L = 2/3 * CUR_PERIOD_H/L
	;;   = (CUR_PERIOD<<1+CUR_PERIOD<<3+CUR_PERIOD<<5+CUR_PERIOD<<7)>>8
	;;   = CUR_PERIOD>>7 + CUR_PERIOD>>5 + CUR_PERIOD>>3 + CUR_PERIOD>>1
	movf	CUR_PERIOD_L, W
	movwf	TEMP_L
	movf	CUR_PERIOD_H, W
	movwf	TEMP_H
	;; TEMP_H/L = CUR_PERIOD
	bcf	STATUS, C
	rrf	TEMP_H, F
	rrf	TEMP_L, F
	;; TEMP_H/L = CUR_PERIOD>>1
	movf	TEMP_H, W
	movwf	LINE_TIME_H
	movf	TEMP_L, W
	movwf	LINE_TIME_L
	;; LINE_TIME = CUR_PERIOD>>1
	bcf	STATUS, C
	rrf	TEMP_H, F
	rrf	TEMP_L, F
	;; TEMP_H/L = CUR_PERIOD>>2
	bcf	STATUS, C
	rrf	TEMP_H, F
	rrf	TEMP_L, F
	;; TEMP_H/L = CUR_PERIOD>>3
	movf	TEMP_L, W
	addwf	SCAN_START_L, F
	btfsc	STATUS, C
	incf	SCAN_START_H, F
	movf	TEMP_H, W
	addwf	SCAN_START_H, F
	;; SCAN_START += CUR_PERIOD>>3
	movf	TEMP_L, W
	addwf	LINE_TIME_L, F
	btfsc	STATUS, C
	incf	LINE_TIME_H, F
	movf	TEMP_H, W
	addwf	LINE_TIME_H, F
	;; LINE_TIME += CUR_PERIOD>>3
	bcf	STATUS, C
	rrf	TEMP_H, F
	rrf	TEMP_L, F
	;; TEMP_H/L = CUR_PERIOD>>4
	movf	TEMP_L, W
	addwf	SCAN_START_L, F
	btfsc	STATUS, C
	incf	SCAN_START_H, F
	movf	TEMP_H, W
	addwf	SCAN_START_H, F
	;; SCAN_START += CUR_PERIOD>>4
	bcf	STATUS, C
	rrf	TEMP_H, F
	rrf	TEMP_L, F
	;; TEMP_H/L = CUR_PERIOD>>5
	movf	TEMP_L, W
	addwf	LINE_TIME_L, F
	btfsc	STATUS, C
	incf	LINE_TIME_H, F
	movf	TEMP_H, W
	addwf	LINE_TIME_H, F
	;; LINE_TIME += CUR_PERIOD>>5
	bcf	STATUS, C
	rrf	TEMP_H, F
	rrf	TEMP_L, F
	;; TEMP_H/L = CUR_PERIOD>>6
	bcf	STATUS, C
	rrf	TEMP_H, F
	rrf	TEMP_L, F
	;; TEMP_H/L = CUR_PERIOD>>7
	movf	TEMP_L, W
	addwf	LINE_TIME_L, F
	btfsc	STATUS, C
	incf	LINE_TIME_H, F
	movf	TEMP_H, W
	addwf	LINE_TIME_H, F
	;; LINE_TIME += CUR_PERIOD>>7
	movf	SCAN_START_L, W
	movwf	LINE_START_L
	movf	SCAN_START_H, W
	movwf	LINE_START_H
	;; LINE_START_H/L = OLD_SCAN_START + CUR_PERIOD>>3 + CUR_PERIOD>>5
	movf	CUR_PERIOD_H, W
	movwf	PIXEL_TIME
	;; PIXEL_TIME = CUR_PERIOD>>8

	IF (0) ;; XXX DEBUGGING: show the values OF LINE_TIME and LINE_START
	movf	LINE_TIME_H, W
	call	serial_debug
	movf	LINE_TIME_L, W
	call	serial_debug
	nop
	movf	LINE_START_H, W
	call	serial_debug
	movf	LINE_START_L, W
	call	serial_debug
	ENDIF ;; END DEBUGGING
	
	clrf	CURLINENUM	; we start scanning w/ line # 0
next_line:
	;; set up CURCHARNUM / CURDOTS
	movf	LINECHARNUM, W
	movwf	CURCHARNUM
	call	lookup_alphabet_data
	movwf	CURDOTS
	incf	LINEDOTOFF, W
	movwf	LOOKUP_TEMP	; use LOOKUP_TEMP to count rotations
	goto	test_rot_val
rotate_again:	
	bsf	STATUS, C
	rlf	CURDOTS, F
test_rot_val:
	decfsz	LOOKUP_TEMP, F
	goto	rotate_again
	;; ASSERT that CURDOTS!=0xFF at this point.
	;; initialize the number of bits/line
	IFDEF	DEBUG_8MHz_CLOCK
	movlw	0x70		; char lookup's too slow at 8MHz for all pixels
	ELSE
	movlw	0x90		; can put more pixels @ faster clock rate.
	ENDIF
	movwf	NUM_PIXELS	; number of bits/line.
	;; compute the starting time of this line.
	movf	LINE_START_L, W	; TEMP_H/L = LINE_START_H/L
	movwf	TEMP_L
	movf	LINE_START_H, W
	movwf	TEMP_H		;  (done)
	movf	LINE_TIME_L, W	; LINE_START += LINE_TIME
	addwf	LINE_START_L, F
	btfsc	STATUS, C
	incf	LINE_START_H, F
	movf	LINE_TIME_H, W
	addwf	LINE_START_H, F	;  (done)
	;; each line needs a little tweak in the offset, because the mirrors
	;; aren't perfectly hexagonal.
	call	get_line_offset
	addlw	0x1		; compensate for addition at start of next_bit
	movwf	LOOKUP_TEMP
subtract_another_pixel_offset:
	movf	PIXEL_TIME, W	; TEMP_H/L -= PIXEL_TIME
	subwf	TEMP_L, F
	btfss	STATUS, C	; carry=1 means borrow=0
	decf	TEMP_H, F
	decfsz	LOOKUP_TEMP, F
	goto	subtract_another_pixel_offset
	;; done with line offset.

	IF (0) ;; XXX DEBUGGING: show the values OF TEMP and TMR1
	movf	TEMP_H, W
	call	serial_debug
	movf	TEMP_L, W
	call	serial_debug
	movf	TMR1H, W
	call	serial_debug
	movf	TMR1L, W
	call	serial_debug
	ENDIF ;; END DEBUGGING
				
next_bit:
	;; when's the next bit time?
	movf	PIXEL_TIME, W	; TEMP_H/L += PIXEL_TIME
	addwf	TEMP_L, F
	btfsc	STATUS, C
	incf	TEMP_H, F
	;; do we need to wait for an overflow to occur?
	;;  (if TEMP_H is 0x0X and TMR1H bit 7 is 0xFX)
	movf	TEMP_H, W
	andlw	0xE0
	btfss	STATUS, Z
	goto	no_overflow
	comf	TMR1H, W
	andlw	0xE0
	btfss	STATUS, Z
	goto	no_overflow
	;; oops! overflow!
	;; wait until timer has overflowed.
	;; (until high bit of timer is zero)
wait_for_overflow:
	btfsc	TMR1H, 7
	goto	wait_for_overflow
no_overflow:
	;; wait until timer is greater than or equal to TEMP_H/TEMP_L
	;; three cases:	 TMR1H/L >= TEMP_H/TEMP_L (wait is over)
	;;               TMR1H/L <  TEMP_H/TEMP_L (keep waiting)
	;; TMR1H=00X..X and TEMP_H=11X..X (overflow; wait is over)
wait_for_timer:
	movf	TMR1H, W
	andlw	0xE0
	btfss	STATUS, Z
	goto	tmr1h_nonzero
	;; TMR1H==0 ---> if TEMP_H=0xFF, then we may have an overflow situation
	comf	TEMP_H, W
	andlw	0xE0
	btfsc	STATUS, Z
	goto	wait_done	; TMR1H=0X && TEMP_H=FX: overflow. wait over.
tmr1h_nonzero:
	movf	TMR1H, W
	subwf	TEMP_H, W	; W = TEMP_H - TMR1H
	btfsc	STATUS, Z
	goto	high_eq		; TEMP_H == TMR1H
	btfsc	STATUS, C	; borrow is !carry
	goto	wait_for_timer	; keep waiting if TEMP_H > TMR1H (no borrow)
	goto	wait_done	; the wait is over if TEMP_H < TMR1H (borrow)
high_eq: ; TMR1H==TEMP_H
	movf	TMR1L, W
	subwf	TEMP_L, W	; W = TEMP_L - TMR1L
	btfsc	STATUS, Z
	goto	wait_done	; TEMP_H/TEMP_L == TMR1H/TMR1L
	btfsc	STATUS, C
	goto	wait_for_timer	; keep waiting if TEMP_L > TMR1L (no borrow)
	;; wait is over! (TEMP_H==TMR1H && TEMP_L < TMR1L )
wait_done:
	;; OK! emit this pixel!
	IFDEF DEBUG_FORCE_VERTICAL_LINES
	bcf	PORTB, 0	; start by turning off the laser
	btfsc	NUM_PIXELS, 0	; use LSB of NUM_PIXELS to alternate
	bsf	PORTB, 0	; turn on laser if pixel is on.
	ELSE ; ! DEBUG_VERTICAL_LINES
	bcf	PORTB, 0	; start by turning off the laser
	btfsc	CURDOTS, 7	; current pixel is MSB of CURDOTS
	bsf	PORTB, 0	; turn on laser if pixel is on.
	;; now shift CURDOTS for the next go-round.
	bsf	STATUS, C
	rlf	CURDOTS, F
	;; test if CURDOTS==0xFF.  if so, time to advance to next char.
	incfsz	CURDOTS, W
	goto	no_advance	; no advance needed
	;; okay, advance to next char:
	movf	CURCHARNUM, W	; W = CURCHARNUM
	btfsc	STATUS, Z	; if (CURCHARNUM==0) W=MESSAGE_CHARS
	movf	MESSAGE_CHARS, W
	addlw	0xFF		; W = W - 1
	movwf	CURCHARNUM	; can we do this in less than five cycles?
	;; set CURDOTS
	call	lookup_alphabet_data
	movwf	CURDOTS
	;; okay, done w/ advance.
	ENDIF ;  DEBUG_VERTICAL_LINES
no_advance:
	decfsz	NUM_PIXELS, F	; NUM_PIXELS--
	goto	next_bit
	bcf	PORTB, 0	;  turn laser off for line retrace.
	incf	CURLINENUM, F	; CURLINENUM++
	btfsc	CURLINENUM, 2	;  (comment this line out to get only 1 line)
	btfss	CURLINENUM, 1
	goto	next_line	; CURLINENUM&6!=6, so keep doing lines.
	;; ooh, we've done all the lines.
	goto	frame_top 	; sync with scanner edge again.

	IFDEF DEBUG_SERIAL
serial_debug:			; debug: put the value of W out on PB2/3
	;; sync/clock on PB2, data on PB3
	movwf	DEBUG_TEMP
	movlw	0x8		; eight bits
	movwf	DEBUG_TEMP2
serloop1:
	bcf	PORTB, 2	; falling edge:	data invalid.
	btfsc	DEBUG_TEMP, 7	; msb
	bsf	PORTB, 3	; data
	btfss	DEBUG_TEMP, 7
	bcf	PORTB, 3
	bsf	PORTB, 2	; rising edge means data ready.
	nop			; give an extra two cycles
	nop			;  to make waveform easier to see.

	rlf	DEBUG_TEMP, F	; next bit.
	decfsz	DEBUG_TEMP2, F
	goto	serloop1

	bcf	PORTB, 2	; maintain low between words.
	bcf	PORTB, 3
	return		
	ENDIF

	padtab	D'10'
lookup_low_of_line:
	movlw	HIGH lookup_low_of_line
	movwf	PCLATH
	movf	CURLINENUM, W
	addwf	PCL, F
	dt	LOW (line1-D'32')
	dt	LOW (line4-D'32')
	dt	LOW (line5-D'32')
	dt	LOW (line6-D'32')
	dt	LOW (line3-D'32')
	dt	LOW (line2-D'32')

	;; lookup character CURCHARNUM in 'message'
	;;  CURCHARNUM = 0 to MESSAGE_CHARS-1 :	 which character we're doing
	;; returns byte in W
lookup_char_in_message:
	movf	CURCHARNUM, W
lookup_char_in_message2a:	; alternate entry point for use of 'do_scroll'
	IFDEF	DEBUG_ALL_MESSAGE_CHARS_ZERO
	retlw	H'00'		; use the test character.
	ENDIF ;; DEBUG_ALL_MESSAGE_CHARS_ZERO
	addwf	MESSAGE_LOW, W
	movwf	LOOKUP_TEMP
	movf	MESSAGE_HIGH, W
lookup_char_in_message2b:	; alternate entry point for use of 'do_scroll'
	btfsc	STATUS, C
	addlw	0x1
	movwf	PCLATH		; setup PCLATH
	movf	LOOKUP_TEMP, W
	movwf	PCL		; does return to caller of 'lookup_char'
	;; NOTE we don't have to restore PCLATH as long as our program
	;; fits entirely in bank 0 (i.e. we don't have to use top two
	;; bits of address)
			
	;; lookup alphabet data.
	;; CURLINENUM = 0 to 5, which line we're on.
	;; (from call to lookup_char_in_message:
	;;  CURCHARNUM = 0 to MESSAGE_CHARS-1, which character we're doing)
	;; returns byte in W
lookup_alphabet_data:
	IFDEF	DEBUG_NO_ALPHABET_LOOKUP
	swapf	CURLINENUM, W	; don't actually look up character
	iorlw	0x87
	return
	ENDIF
	call	lookup_char_in_message
	movwf	LOOKUP_TEMP	; lookup temp = value of char # CURCHARNUM
	call	lookup_low_of_line
	addwf	LOOKUP_TEMP, F	; LOOKUP_TEMP now has LSB of table addr.
	;; now deal with carry from the add
	bcf	SCAN_STATE, 1
	btfsc	STATUS, C
	bsf	SCAN_STATE, 1	; store carry bit in bit 1 of SCAN_STATE
	;; okay, now compute MSB.
	call	lookup_high_of_line
	btfsc	SCAN_STATE, 1
	addlw	0x1		; do the carry
	movwf	PCLATH		; setup PCLATH with MSB of table addr.
	movf	LOOKUP_TEMP, W	; W=LSB of table addr.
	movwf	PCL	; does return to caller of 'lookup_alphabet_data'
	;; NOTE we don't have to restore PCLATH as long as our program
	;; fits entirely in bank 0 (i.e. we don't have to use top two
	;; bits of address)

	IFDEF	DEBUG_INCLUDE_KARAOKE
MESSAGE1_CHARS	EQU	D'1'*D'12'	; number of characters in this message.
message1:
	dt 'k', 'a', 'r', 'a', 'o', 'k', 'e', ' ', ' ', ' ', ' ', ' '
	ELSE
MESSAGE1_CHARS	EQU	D'4'*D'12'	; number of characters in this message.
message1:
	;;  12 characters per line.
	dt 'V', 'N', 'S', 'U', ' ', 'F', 'G', 'H', 'I', ';', ' ', 'C'
	dt 'W', 'Q', 'J', 'R', 'S', ' ', 'K', 'X', 'L', 'U', 'X', 'I'
	dt ' ', 'L', 'K', ' ', '\"', 'O', 'U', 'G', 'L', '\"', ' ', 'D'
	dt 'H', ' ', 'T', 'Y', 'A', 'E', 'Z', 'Z', 'N', ' ', ' ', ' '
	ENDIF

	;; use PB4-7 to setup MESSAGE_HIGH, MESSAGE_LOW, and MESSAGE_CHARS
	;; also reset CURCHARNUM, LINECHARNUM, and LINEDOTOFF to be safe
	;; (because # of characters in the message can change)
	IF NUM_MESSAGES > 1
	padtab	(((0x8*D'15')+0x1)+0x9)
	ENDIF
read_port_and_setup_message:
	clrf	LINECHARNUM
	clrf	LINEDOTOFF
	IF NUM_MESSAGES > 1
	movlw	HIGH	message_table
	movwf	PCLATH
	rrf	PORTB, W	; read port B and shift, W=(PB7-PB4)*8
	bcf	INTCON,	RBIF	; reset "port B changed" flag
	xorlw	0xFF		; flip sense of bits, so that 'on' is 1 (not 0)
	andlw	0x78		; mask out irrelevant bits.
	addwf	PCL, F		; jump!
	ENDIF
message_table_entry	macro	num
	movlw	LOW message#v(num)
	movwf	MESSAGE_LOW
	movlw	HIGH message#v(num)
	movwf	MESSAGE_HIGH
	movlw	MESSAGE#v(num)_CHARS
	movwf	MESSAGE_CHARS
	return
	nop			; pad out to exactly 8 instructions.
	endm
do_entries	macro numleft,curnum
	if (numleft>0)
mentry#v(numleft)		; make a label for this entry
		message_table_entry	(curnum+0x1)
		do_entries	(numleft-0x1), ((curnum+0x1)%NUM_MESSAGES)
	endif
	endm
	IF NUM_MESSAGES > 1
message_table:
	do_entries	0x10, 0x00 ; 16 entries
	ELSE
	message_table_entry 1
	ENDIF

do_scroll:
	bcf	INTCON, T0IF	; clear the timer 0 overflow interrupt flag
	decfsz	CURRENT_SCROLL, F
	goto	tach_done	; only continue 1 in SCROLL_SPEED times.
	movf	SCROLL_SPEED, W
	btfsc	STATUS, Z
	goto	tach_done	; never scroll if SCROLL_SPEED==0
	;; 
	IF (0) ;; DEBUGGING: indicate that we're servicing an interrupt
	movlw	0x10
	xorwf	PORTB, F
	ENDIF ;; END DEBUGGING
	;; okay, really do the scroll.
	decf	LINEDOTOFF, F	; LINEDOTOFF--
	incfsz	LINEDOTOFF, W	; if LINEDOTOFF>=0
	goto	skip_char_inc	;   we're done with the scroll!
	;; else increment LINECHARNUM
	incf	LINECHARNUM, F	; LINECHARNUM++
	movf	MESSAGE_CHARS, W
	subwf	LINECHARNUM, W
	btfsc	STATUS, Z	; if LINECHARNUM==MESSAGE_CHARS
	clrf	LINECHARNUM	;   LINECHARNUM=0
	;; now we need to set LINEDOTOFF properly.
	movf	LOOKUP_TEMP, W
	movwf	CURRENT_SCROLL	; use CUR..SCROLL as temp storage for LOOKUP..
	movf	PCLATH, W
	movwf	PCLATH_TEMP	; store away PCLATH value
	movf	LINECHARNUM, W
	call	lookup_char_in_message2a ; W = message[W]
	addlw	LOW line1
	movwf	LOOKUP_TEMP
	movlw	HIGH line1
	call	lookup_char_in_message2b; a little more efficient than has_high
	;; now W has CURDOTS for the first character in the scan line.
	movwf	SCROLL_TEMP
	;; increment LINEDOTOFF until we've reached the delimiter
	;; note that LINEDOTOFF==-1 at this point.
rotate_again2:
	incf	LINEDOTOFF, F
	bsf	STATUS, C
	rlf	SCROLL_TEMP, F
	incfsz	SCROLL_TEMP, W
	goto	rotate_again2
	;; restore PCLATH
	movf	PCLATH_TEMP, W
	movwf	PCLATH
	;; restore LOOKUP_TEMP
	movf	CURRENT_SCROLL, W
	movwf	LOOKUP_TEMP
skip_char_inc:
	;; reset CURRENT_SCROLL to SCROLL_SPEED
	movf	SCROLL_SPEED, W
	movwf	CURRENT_SCROLL
	;; done!
	goto	tach_done
	
	padtab D'10'
lookup_high_of_line:
	movlw	HIGH lookup_high_of_line
	movwf	PCLATH
	movf	CURLINENUM, W
	addwf	PCL, F
	dt	HIGH (line1-D'32')
	dt	HIGH (line4-D'32')
	dt	HIGH (line5-D'32')
	dt	HIGH (line6-D'32')
	dt	HIGH (line3-D'32')
	dt	HIGH (line2-D'32')

tach_int:			; tachometer interrupt.
	;; save state.
	movwf	W_TEMP		; copy W to a temp register in whatever
				; bank we're currently in.
	swapf	STATUS,W	; swap STATUS nibbles and place in W
	movwf	STATUS_TEMP	; store away STATUS in common ram.

	;; okay, now figure out what interrupt this was.
	bcf	STATUS, RP0	; bank 0
	bcf	STATUS, RP1	; bank 0

	IFNDEF	DEBUG_DISABLE_SCROLL
	btfsc	INTCON, T0IF
	goto	do_scroll	; scroll on timer0 overflow
	ENDIF ; DEBUG_DISABLE_SCROLL

	IF (0) ;; DEBUGGING: indicate that we're servicing an interrupt
	movlw	0x02
	xorwf	PORTB, F
	ENDIF ;; END DEBUGGING
	
	incf	WHICH_SCAN, F	; increment the 'edges I've seen' counter.
	btfsc	PIR1, CCP1IF	; was this ccp1?
	goto	falling_edge
rising_edge:			; this was ccp2.
	bcf	PIR2, CCP2IF	; clear the flag.
	movf	CCPR1L, W	; last capture is sitting in CCPR1
	subwf	CCPR2L, W	; W=CCPR2L-CCPR1L
	movwf	CUR_PERIOD_L
	movf	CCPR1H, W	; last capture was from CCPR1
	btfss	STATUS, C	; borrow is !carry
	addlw	0x1		; add one to W
	subwf	CCPR2H, W	; W=CCPR2H-CCPR1H
	movwf	CUR_PERIOD_H
	;; is this the 'start scan' edge?
	btfss	WHICH_SCAN, 0
	btfsc	WHICH_SCAN, 1
	goto	tach_done	; if WHICH_SCAN&3!=0, then not the 'start scan'
	;; ELSE (if which_scan&3==0), record this as SCAN_START and
	;; set bit 0 of SCAN_STATE
	movf	CCPR2L, W
	movwf	SCAN_START_L
	movf	CCPR2H, W
	movwf	SCAN_START_H
	goto	start_scan_edge
falling_edge:			; like rising_edge, but reversed.
	bcf	PIR1, CCP1IF	; clear the flag.
	movf	CCPR2L, W	; last capture is sitting in CCPR2
	subwf	CCPR1L, W	; W=CCPR1L-CCPR2L
	movwf	CUR_PERIOD_L
	movf	CCPR2H, W	; last capture was from CCPR2
	btfss	STATUS, C	; borrow is !carry
	addlw	0x1		; add one to W
	subwf	CCPR1H, W	; W=CCPR1H-CCPR2H
	movwf	CUR_PERIOD_H
	;; is this the 'start scan' edge?
	btfss	WHICH_SCAN, 0
	btfsc	WHICH_SCAN, 1
	goto	tach_done	; if WHICH_SCAN&3!=0, then not the 'start scan'
	;; ELSE (if which_scan&3==0), record this as SCAN_START and
	;; set bit 0 of SCAN_STATE
	movf	CCPR1L, W
	movwf	SCAN_START_L
	movf	CCPR1H, W
	movwf	SCAN_START_H
start_scan_edge:
	bsf	SCAN_STATE, 0
	;; fall through to tach_done
tach_done:
	;; end of interrupt service routine.
	swapf	STATUS_TEMP,W	; swap status into W
	movwf	STATUS		; restore status register.
	swapf	W_TEMP,F	; swap W_TEMP nibbles and place back in W_TEMP
	swapf	W_TEMP,W	; restore W_TEMP to W without affecting STATUS
	retfie			; done!
	
	;; now include the lookup tables for alphabetic characters.
	IFNDEF	DEBUG_ALL_MESSAGE_CHARS_ZERO
include alphabet.asm
	ELSE ; DEBUG_ALL_MESSAGE_CHARS_ZERO
line1:	retlw	H'57'
line2:	retlw	H'17'
line3:	retlw	H'27'
line4:	retlw	H'47'
line5:	retlw	H'87'
line6:	retlw	H'A7'
	ENDIF ; DEBUG_ALL_MESSAGE_CHARS_ZERO
			
	END
