;; filename:	renard-fast-20080307.asm
;; Copyright (c) 2006-2008 Phil Short
;;
;;  This program is provided free for you to use in any way that you wish,
;;  subject to the laws and regulations where you are using it.  Due diligence
;;  is strongly suggested before using this code.
;;
;;  The Author makes no warranty of any kind, express or implied, with regard
;;  to this program or the documentation contained in this document.  The
;;  Author shall not be liable in any event for incidental or consequential
;;  damages in connection with, or arising out of, the furnishing, performance
;;  or use of these programs.
;;
;;  --------------------------------------------------------------------------
;;  NOTE:
;;  This code is configured for regular use (the firmware generates 30 uS (at
;;  60 Hz) pulses to drive the SSRs, synchronized to the zero-crossing input
;;  pin).  Here are the DEFINE statements to change for PWM use (generates a
;;  variable width pulse sync'd to the zero-crossing signal) or DC use
;;  generates a variable-width pulse, not sync'd to anything).

;; PWM_map determines which bits should be 'PWM' (normally 0, for all non-PWM).
;; It is binary-encoded, with bit 0 corresponding to channel 0 through bit 7
;; channel 7.  As an example, the value 67 = 64 + 2 + 1 would have three
;; channels (0,1 and 7) as PWM and the rest as non-PWM.
	
#define PWM_map 0xFF
	
;; DC_build should be either 0 (for AC operation) or 1 (for DC operation).  It
;;   should remain 0 if PWM_map is anything other than 255

 #define DC_build 0

 #define CLOCKRATE 18432000
 #define BAUDRATE 250000

;; CTR_LOCKOUT should be 0 for the DC build, can be a somewhat larger number
;; for non-PWM or non-DC builds.

 #define CTR_LOCKOUT 40

;; Bitmap of outputs which sink current (normally 0xFF, for all 8 outputs).
;; This is binary-encoded the same way as the PWM_map 'variable'.  A '1' bit
;; means that the output sinks current (is active low), a '0' bit means that
;; output sources current (active high).

 #define SINK_map 0xFF

 #define DEBUG_DMX 0

;; The DMX start address should be between 1 and 505 inclusive.

 #define DMX_START_ADDRESS 1	
;;  --------------------------------------------------------------------------
;;
;; Revision History.
;; 20071216 PJS Restart for DMX variant.
;;
;;  Hardware mappings
;;  PIN 13 (RA0) - triac driver 1 (output)
;;  PIN 12 (RA1) - triac driver 2 (output)
;;  PIN 11 (RA2) - triac driver 3 (output)
;;  PIN 4  (RA3) - zero-crossing (input only)
;;  PIN 3  (RA4) - triac driver 0 (output)
;;  PIN 10 (RC0) - triac driver 4 (output)
;;  PIN 9  (RC1) - triac driver 5 (output)
;;  PIN 8  (RC2) - triac driver 6 (output)
;;  PIN 7  (RC3) - triac driver 7 (output)
;;  PIN 6  (RC4/TX) - uart_out (output)
;;  PIN 5  (RC5/RX) - uart_in (input)
;;  PIN 2  (RA5) - clock input
;;
;;  The interrupt used by this program is timer0, which is intended to create
;;  a periodic (32 us) clock.
;;

 LIST R=DEC
 INCLUDE "p16f688.inc"

;; Use the internal clock of the PIC

;; __CONFIG _FCMEN_OFF & _IESO_OFF & _BOD_ON & _CPD_OFF & _CP_OFF & _MCLRE_OFF & _PWRTE_ON & _WDT_OFF & _INTOSCIO

;; Use external clock
	
 __CONFIG _FCMEN_OFF & _IESO_OFF & _BOD_ON & _CPD_OFF & _CP_OFF & _MCLRE_OFF & _PWRTE_ON & _WDT_OFF & _EC_OSC

 CBLOCK 0x020
;; the slot_id_cktn variables must be on a multiple of 0x10 address.

  slot_id_ckt0, slot_id_ckt1, slot_id_ckt2, slot_id_ckt3	
  slot_id_ckt4, slot_id_ckt5, slot_id_ckt6, slot_id_ckt7	

  ctr_ckt0, ctr_ckt1, ctr_ckt2, ctr_ckt3	
  ctr_ckt4, ctr_ckt5, ctr_ckt6, ctr_ckt7	

  _w, _status			; isr context save registers.

  now_on_map, tmp, always_on_map

  rx_char, rx_state, rx_count_lsb, rx_count_msb	

  zc_counter

  timer0_preset,timer0_preset_updates

  timeslot_id

  wdt_counter

  our_addr_lsb,our_addr_msb

  ts_state			; timer_stopped state

 ENDC	

;; assigned numbers:

 #define DEFAULT_LINERATE 60
 #define MAX_LINERATE 59
	
;; derived numbers:

;; initializer for baudrate generator (based on formula from datasheet)
	
 #define BAUD_INIT (CLOCKRATE/BAUDRATE/4 - 1)

;; initializer for cell timer (replaces TIMER_MAGIC0/1).  This assumes 50 Hz,
;; but it will self-adjust after 1/2 AC cycle (better too slow than too fast).

;; Number used to prevent overly large values from being stored in the
;; timer0_preset variable register.

 #define MIN_TMR1H  ((CLOCKRATE/4)/DEFAULT_LINERATE/256/2)
	 	
;; I/O bit definition

 IF (!DC_build)
   #define ZC_BIT   3
   #define ZC       PORTA,(ZC_BIT)
   #define ZC_MASK  (1<<(ZC_BIT))
 ENDIF

 #define TEST_STROBE PORTA,0

 #define PWM_remap ((PWM_map & 0xF0) | ((PWM_map >> 1) & 7) | ((PWM_map << 3) & 8)) 
 #define SINK_remap ((SINK_map & 0xF0) | ((SINK_map >> 1) & 7) | ((SINK_map << 3) & 8)) 
;; rx_state machine bit definitions - see description of rx_state usage later
;; in this program (after mainloop label)

 #DEFINE ANY_BIT 2
 #DEFINE DISCARD_BIT 1
 #DEFINE CMD_BIT 0
		
;; ---------------------------------------------------------------------------

 org 0
	nop
	goto initialize

 org 4
	;; interrupt routine - currently 45 clocks in the most common case
	;; Timer interrupt and interrupt on change (zero-crossing)
Int
	movwf	_w		; save 'w'
	movf	STATUS, w	; src, dst
	movwf	_status		; save f to _status

;; Prepare for timer updating
	
	movf	timer0_preset,w	; update timer0 for next timer interrupt
;;	movlw	2-MIN_TMR1H	; test code for now

;;
;;  Check for special cases
;;
	incfsz	timeslot_id,f
	goto	isr_normal_case

;; two possibilities: in lockout phase (i.e. very last timeslot allowed), or
;; interrupt on change (ZC) has just occurred.
	
	btfss	INTCON,RAIE
	goto	isr_in_lockout

	movwf	TMR0		; interrupt on change just occurred - in first
				; timeslot

	movlw	(1<<GIE) | (1<<T0IE)
	movwf	INTCON

	movlw	CTR_LOCKOUT
	movwf	timeslot_id

	movf	always_on_map,w
	goto	isr_reenter
		
isr_in_lockout:			; last allowed timeslot -> turn off all outputs
	clrf	INTCON		; disable interrupts and wait for foreground
	decf	timeslot_id,f	; code to enable 'interrupt on change'

	clrf	ts_state
	
	movf	always_on_map,w	; turn all outputs off (except those that must
	goto	isr_reenter

isr_normal_case:
;;
;; update timer0 (usually determines when the next interrupt will occur).
;;
	addwf	TMR0,f

	bcf	INTCON,T0IF	; re-enable interrupts if not lockout period

isr_continue:

;;
;; determine which outputs should (potentially) turn on
;;
	movlw	1

	addwf	ctr_ckt1,f
	rrf	tmp,f
	addwf	ctr_ckt2,f
	rrf	tmp,f
	addwf	ctr_ckt3,f
	rrf	tmp,f
	addwf	ctr_ckt0,f
	rrf	tmp,f
	addwf	ctr_ckt4,f
	rrf	tmp,f
	addwf	ctr_ckt5,f
	rrf	tmp,f
	addwf	ctr_ckt6,f
	rrf	tmp,f
	addwf	ctr_ckt7,f
	rrf	tmp,w
;;
;; turn selected outputs on
;;
	iorwf	now_on_map,w	; output new assertions

isr_reenter:
	movwf	now_on_map


;; invert the 'active' low outputs, and output to the i/o port

	xorlw	SINK_remap
	movwf	tmp

	andlw	0x0F
	addlw	0x8
	movwf	PORTA

	swapf	tmp,w	;
	movwf	PORTC

;; turn off the non-pwm outputs

	movlw	PWM_remap
	andwf	now_on_map,f

;;
;; The usual isr epilogue
;;
isr_epilogue:
	
	movf	_status, w
	movwf	STATUS
	swapf	_w,f
	swapf	_w,w
	retfie

;;
;; End of Interrupt Routine, start of Foreground code.
;;

initialize:
;;
;; initialize some registers
;;
	clrf	slot_id_ckt0	; set all 'lamps' to lowest intensity
	clrf	slot_id_ckt1
	clrf	slot_id_ckt2
	clrf	slot_id_ckt3
	clrf	slot_id_ckt4
	clrf	slot_id_ckt5
	clrf	slot_id_ckt6
	clrf	slot_id_ckt7

	clrf	timer0_preset_updates
	
	movlw	(1 << ANY_BIT) | (1 << DISCARD_BIT)
	movwf	rx_state

	clrf	ts_state	; timer_stopped state
	clrf	zc_counter	; zero-crossing counter

;; configure the I/O ports

	bcf	STATUS,RP0	; select bank 0
	movlw	7
	movwf	CMCON0		; disable the comparators
	clrf	now_on_map

	bsf	STATUS,RP0

	movlw	0x74
	movwf	OSCCON ^ 0x80
	
	movlw	0x20		; bit 5 is the clock input bit
	movwf	TRISA ^ 0x80	; h/w ignores bit 3 of this register

	movlw	0xF0		; upper four bits are tri-stated for uart op.
	movwf	TRISC ^ 0x80		; 
	clrf	ANSEL ^ 0x80
	bcf	OPTION_REG ^ 0x80,7	; global weak-pullup enable
	movlw	0x20		; turn on weak pullup for unused input (RA5)
	movwf	WPUA ^ 0x80

#IF !DC_build
	movlw	1<<ZC_BIT
	movwf	IOCA ^ 0x80
#ENDIF
	
;; these lines will cause some assembler warnings, but they are expected and OK
	movlw	0x00
	movwf	EEADR ^ 0x80
	movwf	EEADRH ^ 0x80
	movwf	EECON1 ^ 0x80

	bsf	EECON1 ^ 0x80,RD
	movf	EEDAT ^ 0x80,w

	bcf	STATUS,RP0
	movwf	our_addr_msb
	bsf	STATUS,RP0

	incf	EEADR ^ 0x80,f
	
	bsf	EECON1 ^ 0x80,RD; initiate read operation
	movf	EEDAT ^ 0x80,w

	bcf	STATUS,RP0
	movwf	our_addr_lsb

	movlw	0x01		; convert 1-based value from EEPROM into
	subwf	our_addr_lsb,f	; 0-based value used in rx routine.
	btfss	STATUS,C
	subwf	our_addr_msb,f
		
;; set the outputs to the passive state

	movlw	SINK_remap >> 4
	movwf	PORTC
	movlw	(SINK_remap & 7) | ((SINK_remap & 8) << 1)
	movwf	PORTA

;; configure the UART

	movlw	(1<<BRG16)
	movwf	BAUDCTL
	movlw	BAUD_INIT;
	movwf	SPBRG
	clrf	SPBRGH
		
	movlw	(1<<TXEN) | (1<<BRGH); enable 8-bit transmitter in async mode
				; (x16 baud divisor)
	movwf	TXSTA
	movlw	(1<<SPEN) | (1<<CREN); enable serial port, continuous 8-bit rx
	movwf	RCSTA
	
;; Set up timer0 - generate periodic interrupt at 512 times powerline freq

	bsf	STATUS,RP0
	movlw	0x08		; assign prescaler to WDT (i.e. not used)
	movwf	OPTION_REG ^ 0x80; clear all bits -> timer mode, clk/2
	bcf	STATUS,RP0

;; Next three wait loops ensure that we are on the rising edge of ZC when we
;; first enter the mainloop, so that we don't put an overly small value in the
;; timer0_preset register.

	clrf	T1CON
	clrf	TMR1L
	clrf	TMR1H
	
	movlw	2 - MIN_TMR1H	; assume 60 Hz 
	movwf	timer0_preset

  IF DC_build
	movlw	(1<<GIE) | (1<<T0IE)
	movwf	INTCON
  ELSE	

wait_zc:
	btfss	ZC
	goto	wait_zc

wait_nzc:
	btfsc	ZC
	goto	wait_nzc

	bsf	T1CON,TMR1ON

wait_zc1:
	btfss	ZC
	goto	wait_zc1

	movlw	MIN_TMR1H
	subwf	TMR1H,w		; TMR1H - MIN_TMR1H - no borrow if OK.
	comf	TMR1H,w

	btfss	STATUS,Z
	btfss	STATUS,C	;
	goto	mainloop	; skip preset update if out of range

	addlw	3		; compensate for difference between 2's compl
				; and 1's complement inversion and for two lost
				; clocks when writing to TMR0

	movwf	timer0_preset

  ENDIF

;; With DMX we expect to receive a character every 44 uS.  Timer interrupts
;; occur every 32 uS, and take 23 uS to process, so the mainloop needs to be
;; kept under about 10 uS, or 20 instructions.
;;
;; To be more precise, the LCM of 32 and 44 is 352.  During this time there
;; are 11 timer interrupts, occupying 253 uS, leaving for 99 uS for the 8
;; receive character interrupts.  Thus, each rx_char event must take 12 uS or
;; less, which translates to 24 instructions.
	
mainloop:
	btfss	INTCON,T0IE
	call	timer_stopped

;; ----------------- Serial Rx Data Processing (DMX512) -----------------------
;; rx_state:
;; xxxxx110 -> discard data (non-zero start code or uart error)
;; xxxxx101 -> process START_CODE byte (just after rx Break)
;; xxxxx100 -> copy data to transmitter (START_CODE <> 0, after last byte for
;;               this PIC).
;; xxxxx000 -> process user data (START_CODE through last byte for this PIC)

	btfss	PIR1,RCIF	; skip if UART has a rx character to process
	goto	mainloop	; no character available

	btfsc	RCSTA,FERR	; read framing error before reading char
	goto	frame_err

	movf	RCREG,w		; read the character
;;
;; must preserve w until the movwf INDF below
;;
	btfsc	RCSTA,OERR	; and check the overrun error bit.
	goto	overrun_err

	btfsc	rx_state,ANY_BIT
	goto	not_data_octet
;;
;; we are expecting data for this node or data between the START_CODE and the
;; data for this code.  This data is not re-transmitted in either case.
;; 
	movf	rx_count_lsb,f
	btfss	rx_count_msb,0	; if msb is still set, goto 'still_waiting'
	btfss	STATUS,Z	; if lsb is non-zero, goto 'still_waiting'
	goto	still_waiting

	movwf	INDF
	incf	FSR,f

	btfsc	FSR,3
	bsf	rx_state,ANY_BIT; go into copy state
	clrf	wdt_counter
	goto	mainloop

still_waiting:
	movlw	0x01
	subwf	rx_count_lsb,f	; borrow -> C flag clear
	btfss	STATUS,C
	subwf	rx_count_msb,f

	goto	mainloop

;; ANY_BIT was set (in copy, discard, or process_start_code state)
not_data_octet:
	btfss	rx_state,DISCARD_BIT
	movwf	TXREG		; re-transmit start code or packet trailer

	btfss	rx_state,CMD_BIT; exit if this is not the start code byte
	goto	mainloop
	
	addlw	0		; START_CODE - check for 0.

	bcf	rx_state,CMD_BIT; now in copy_state (only ANY_BIT is set)
	btfsc	STATUS,Z
	bcf	rx_state,ANY_BIT

	goto	mainloop
;;
;; could be either the sync byte (RCREG == 0) or true framing error
;;
frame_err:

	movf	RCREG,w		; read the character

	btfss	STATUS,Z
	goto	true_frame_err

;; A break character, not a framing error.  This path takes longer than the
;; norm, but that's ok because it only happens at the beginning of a packet,
;; although a lot of minimum length (44 uS) breaks in a row could wreak havoc.

tx_loop:
	btfss	TXSTA,TRMT
	goto	tx_loop

	movlw	~10
tx_loop_loop:
	addlw	1
	btfss	STATUS,Z
	goto	tx_loop_loop
	
	bsf	TXSTA,SENDB
	movwf	TXREG

	movlw	(1<<ANY_BIT) | (1 << CMD_BIT)
	movwf	rx_state

	movlw	slot_id_ckt0
	movwf	FSR

	movf	our_addr_msb,w
	movwf	rx_count_msb

	movf	our_addr_lsb,w
	movwf	rx_count_lsb

	goto	mainloop

true_frame_err:
	movlw	(1 << ANY_BIT) | (1<<DISCARD_BIT)
	movwf	rx_state
		
	goto	mainloop
	
;;
;; overrun is a true error ... should not happen in real life, is either a
;; coding error or a PIC hardware error (oscillator is mis-functioning, some
;; other hardware fault in the PIC).
;;
overrun_err:
	bcf	RCSTA,CREN
	bsf	RCSTA,CREN
	bsf	rx_state,DISCARD_BIT
	goto	mainloop

;;
;; This routine is called if the timer interrupts are stopped.  It keeps a
;; bitmap of the state of the zero-crossing the last eight times that it was
;; called.
;; 
;; If it detects that the zero-crossing was set the most recent four
;; times, and cleared the four times before that, it decides that this is the
;; rising edge of the zero-crossing signal, and recalculates the timer0_preset
;; variable (based on the timer1), and possibly shifts the dimmer values right
;; by one bit if no data has been received recently (thereby fading to off if
;; it has stopped receiving data updates).
;;
;; If it detects that the zero-crossing was set for the eight most recent times
;; that it was called, it updates the dimmer value counters used in the ISR,
;; and enables the interrupt-on-change bit.
;;

timer_stopped:

	btfsc	ts_state,7	; all timer_stop work done, waiting for !ZC
	return

	btfsc	ts_state,0
	goto	ts_state1

#IF !DC_build
	btfsc	ZC
	goto	fast_ts_exit
#ENDIF

;; first portion of timer_stop condition
	bsf	ts_state,0
	clrf	zc_counter

	movf	slot_id_ckt1,w
	movwf	ctr_ckt1
	addlw	1
	rrf	always_on_map,f
			
	movf	slot_id_ckt2,w
	movwf	ctr_ckt2
	addlw	1
	rrf	always_on_map,f
	
	movf	slot_id_ckt3,w
	movwf	ctr_ckt3
	addlw	1
	rrf	always_on_map,f
	
	movf	slot_id_ckt0,w
	movwf	ctr_ckt0
	addlw	1
	rrf	always_on_map,f

	swapf	always_on_map,w
	andlw	PWM_remap
	movwf	always_on_map
	movwf	now_on_map

	return

ts_state1:			; second portion of timer-stop work
	btfsc	ts_state,1
	goto	ts_state2
	bsf	ts_state,1

	swapf	always_on_map,f
		
	movf	slot_id_ckt4,w
	movwf	ctr_ckt4
	addlw	1
	rrf	always_on_map,f
	
	movf	slot_id_ckt5,w
	movwf	ctr_ckt5
	addlw	1
	rrf	always_on_map,f
	
	movf	slot_id_ckt6,w
	movwf	ctr_ckt6
	addlw	1
	rrf	always_on_map,f
	
	movf	slot_id_ckt7,w
	movwf	ctr_ckt7
	addlw	1
	rrf	always_on_map,w
	andlw	PWM_remap
	movwf	always_on_map
	movwf	now_on_map

  #IF (!DC_build)
	btfsc	ZC		; skip state 2 if zc already on
	bsf	ts_state,3
  #ENDIF
	return

ts_state2:			; state waiting for zc
	btfsc	ts_state,3
	goto	ts_state3

  #IF (!DC_build)
	btfss	ZC		; no ZC ... stay in this state
	return
  #ENDIF
	
	bsf	ts_state,3
update_wdt_ctr:

	incfsz	wdt_counter,f
	goto	zc_filter_restart_timer

	;; 'wdt_timer' overflowed - dim to 'OFF'
	
	addlw	0		; clear C flag
	rrf	slot_id_ckt0,f	; fade to '0'
	addlw	0
	rrf	slot_id_ckt1,f
	addlw	0
	rrf	slot_id_ckt2,f
	addlw	0
	rrf	slot_id_ckt3,f
	addlw	0
	rrf	slot_id_ckt4,f
	addlw	0
	rrf	slot_id_ckt5,f
	addlw	0
	rrf	slot_id_ckt6,f
	addlw	0
	rrf	slot_id_ckt7,f
	
zc_filter_restart_timer:
	return

ts_state3:			; ZC rising edge was detected at least once
	btfsc	ts_state,4
	goto	ts_state4

  #IF (!DC_build)
	incf	zc_counter,f
	btfss	ZC
	clrf	zc_counter

	btfss	zc_counter,3	; wait until zc has been detected 8x in a row
	return

	movf	PORTA,w		; zc is stable high - enable falling-edge int.
	movlw	(1<<GIE) | (1<<RAIE);
  #ELSE
	movlw	(1<<GIE) | (1<<T0IE) | (1<<RAIE)
  #ENDIF
	movwf	INTCON
	bsf	ts_state,4
ts_state4:			; terminal state...waiting for interrupt
	return
  #IF (!DC_build)
fast_ts_exit:
	movlw	0xFF
	movwf	timeslot_id

	movf	slot_id_ckt1,w
	movwf	ctr_ckt1
	movf	slot_id_ckt2,w
	movwf	ctr_ckt2
	movf	slot_id_ckt3,w
	movwf	ctr_ckt3
	movf	slot_id_ckt0,w
	movwf	ctr_ckt0
	movf	slot_id_ckt4,w
	movwf	ctr_ckt4
	movf	slot_id_ckt5,w
	movwf	ctr_ckt5
	movf	slot_id_ckt6,w
	movwf	ctr_ckt6
	movf	slot_id_ckt7,w
	movwf	ctr_ckt7

	bsf	ts_state,7

	movf	PORTA,w
	movlw	(1<<GIE) | (1<<RAIE)
	movwf	INTCON
	return
  #ENDIF
;; data stored in the eeprom.  First byte is MSB of our address (0/1), second
;; byte is the LSB of our address (0-255).

  ORG 0x2100
   de (DMX_START_ADDRESS / 256)
   de (DMX_START_ADDRESS & 255)
  
  END
