; PIC 16F688 code for sending IR signals to a Vizio TV
;
; ### license
;
; Note: This is for the Vizio E422AR in particular. It is likely that
;       the code will work for other Vizio TVs. This implements the
;       NEC protocol ("pulse distance modulation"), so tweaking it for
;       other Japanese TVs should work (they mostly use the NEC proto)
;
; 38 kHz == 26.3 uS per cycle. Our instruction cycle is 0.5 uS (assuming
; that the internal 8 MHz oscillator is used). This maps to 52.6
; instructions per signal-cycle. Rounding down to an even count (helps
; when we use the prescaler) gives us 52 instructions per signal-cycle.
; The duty cycle of HIGH should be between 25% and 33%, so we choose
; 16 HIGH, 36 LOW, for a 30.7% duty.
;
; The START HIGH burst is 9 ms in duration, which implies 342 cycles
; of 38 kHz signal (and 18000 instruction cycles). However, we run
; at 38.46 kHz due to rounding down to 52 instruction cycles. Thus,
; we will run 346 HIGH/LOW signal-cycles during the START HIGH period.
;
; Then we spend 4.5 ms in the LOW condition, before starting data transfer.
;
; All other HIGH/LOW signal-cycles specified with a 560 uS duration
; (followed by a LOW duration to signal 0 or 1). We will execute
; 21 HIGH/LOW signal-cycles, each lasting 26 uS (rounding!), for a total
; duration of 546 uS.
;
; The external build script should set the following:
;
;   LED: the PortC pin to use for driving the IR emitter (default RC3)
;   VIZIO_VAR_BASE: the address for the code's working state
;                   this must be in Bank 0. (default 0x20)
;
; References:
;   http://www.sbprojects.com/knowledge/ir/nec.php
;

        list p = 16f688
        include p16f688.inc
        include ../include/handy.inc

vizio:  code                    ; relocatable


; The IR emitter is attached to this pin.
 ifndef LED
   constant LED = 3             ; pin RC3
 endif

; figure out where we're going to place our variables
; ### assumed to be in bank0 !!
 ifndef VIZIO_VAR_BASE
   constant VIZIO_VAR_BASE = 0x20
 endif


; These are the number of TIMER0 interrupts for the HIGH/LOW duty cycle.
; Since TIMER0 is operating on a 1:2 prescaler against the instruction
; clock, these are half of the values determined above (16/36). Also note
; their subtraction from the 0xFF -> 0x00 rollover which triggers the
; interrupt.
 constant HIGH_DUTY = 0xF8      ; 256 - 8
 constant LOW_DUTY = 0xEE       ; 256 - 18

; The START state needs to run for 346 cycles of 38 kHz. This does not
; fit into our single CYCLE byte. Thus, we split the START state into
; two, each running for 173 cycles.
 constant START_CYCLE = 0xAD

; The code uses several state machines. This state determines what to do
; at the next interrupt.
ISTATE  equ     VIZIO_VAR_BASE

; And the Interrupt State values
#define IS_DUTY_HIGH    istate_duty_high
#define IS_DUTY_LOW     istate_duty_low
#define IS_START_LOW    istate_start_low
#define IS_BIT_LOW      istate_bit_low

; Another state describes where we are in the overall message. This
; state is examined when we end a HIGH/LOW pulse sequence, and will
; determine how long the following LOW will last.
MSTATE  equ     VIZIO_VAR_BASE + 0x01

; And the Message State values
#define MS_START_HIGH1  mstate_start_high1
#define MS_START_HIGH2  mstate_start_high2
#define MS_SEND_DATA    mstate_send_data
#define MS_SEND_TRAILER mstate_send_trailer

; This records both HIGH/LOW cycles, and interrupt counts during a
; long-duration LOW event.
CYCLE   equ     VIZIO_VAR_BASE + 0x02

; The actual code that we need to send
IRCODE  equ     VIZIO_VAR_BASE + 0x03

; The current byte we are sending. It is shifted out to the LED. MSB first.
; NOTE: it appears the NEC specifies LSB first, but our codes are all
;       recorded as if it was MSB first. So we just send that way.
SHIFT   equ     VIZIO_VAR_BASE + 0x04

; How many bits of SHIFT do we still need to send?
BCOUNT  equ     VIZIO_VAR_BASE + 0x05

; Which byte of data are we sending?
; NOTE: this counts backwards from 4 to 0.
BYTE_N  equ     VIZIO_VAR_BASE + 0x06


; Move to a new state, defined by magical PC offsets.
set_istate: macro WHICH
        ; assume Bank 0
        movlw   WHICH - ISTATE_BASE
        movwf   ISTATE
        endm

; Move to a new state, defined by magical PC offsets.
set_mstate: macro WHICH
        ; assume Bank 0
        movlw   WHICH - MSTATE_BASE
        movwf   MSTATE
        endm


; Send a code to the television.
;
; INPUTS: W should contain the code to send.
;
send_code:
        ; NOTE: assuming Bank 0.

        ; Stash the code we're supposed to send.
        movwf   IRCODE

        ; Initial Message State. The 'duty' code will set up the
        ; Interrupt State.
        set_mstate MS_START_HIGH1

        ; Zero the bit count. This simplifies initializing it later.
        clrf    BCOUNT

        ; Initialize the count of data bytes to send.
        movlw   0x03
        movwf   BYTE_N

        ; Set up the cycle count for the first half of the START HIGH.
        movlw   START_CYCLE
        movwf   CYCLE

        ; Good to go. Let's light up the START burst.
        goto    prepare_duty


; Application should call this when T0 interrupts. W and STATUS should
; be saved/restored by the app.
timer_interrupt:
        ; ### copy over some comments from slave.asm if we keep this
        bank0

        movfw   ISTATE
        addwf   PCL, F
        ; NOTREACHED

; The ADDWF above adjusts the PC relative to this location:
ISTATE_BASE:
; The label has no associated code. kthxbai.


istate_duty_high:
        ; NOTE: assuming Bank 0.

        ; We're in the HIGH state during a START or pulse. Simply
        ; transition to LOW. We only count cycles when the LOW state
        ; has "completed" its duration.

        ; Turn off the LED, and load the timer with the LOW duration.
        bcf     PORTC, LED
        movlw   LOW_DUTY
        movwf   TMR0

        set_istate IS_DUTY_LOW

        return


istate_start_low:
        ; NOTE: assuming Bank 0.

        ; We're holding the LED LOW for 4.5 ms.
        decfsz  CYCLE, F
        goto    start_completed
        return


istate_bit_low:
        ; NOTE: assuming Bank 0.

        decfsz  CYCLE, F
        goto    next_bit        ; Done with LOW. Go for next bit.

        ; Keep holding it LOW.
        return


istate_duty_low:
        ; NOTE: assuming Bank 0.

        ; We're in the LOW state of the duty cycle. We need to continue
        ; cycling, or move into a prolonged LOW state. Decrement the
        ; CYCLE count and determine.

        decfsz  CYCLE, F
        goto    continue_duty

        ; Prolonged LOW state is needed. The duration is determined
        ; by the Message State.
        movfw   MSTATE
        addwf   PCL, F
        ; NOTREACHED

; The ADDWF above adjusts the PC relative to this location:
MSTATE_BASE:
; The label has no associated code. kthxbai.


mstate_start_high1:
        ; NOTE: assuming Bank 0.

        ; Finished the first half of the START signal. Keep going.

        set_mstate MS_START_HIGH2

        ; Continue for another START_CYCLE cycles.
        movlw   START_CYCLE
        movwf   CYCLE

        ; ### maybe FALLTHRU?
        goto    continue_duty


mstate_start_high2:
        ; NOTE: assuming Bank 0.

        ; Finished the START signal. Hold LOW for a while.

        set_istate IS_START_LOW

        ; NOTE: we could maybe stick with the normal prescaling, but
        ;       it is nicer to slow down the interrupts and let the
        ;       main "process" go do its thing.
        ; NOTE: we could use 1:256 prescaling, but a 0-bit rounds off
        ;       poorly for the 0-bit duration (560 uS). Using 1:128
        ;       gets it closer to target.

        ; Keep LED set to LOW for 70 TIMER0 interrupts (4.5 ms). This
        ; maps to 4480 uS.
        movlw   0x46

        goto    enter_low_state


mstate_send_data:
        ; ###


mstate_send_trailer:
        ; NOTE: assuming Bank 0.

        ; ### return to idle
        bcf     INTCON, T0IE

        return


start_completed:
        ; Done with the START LOW. Start sending data.
        set_mstate MS_SEND_DATA

        ; BYTE_N was initialized to 3. Select 0x20 and send.

        ; FALLTHRU

more_bytes:

        movlw   0x20

        ; Select the address, or the command code.
        btfss   BYTE_N, 1
        movfw   IRCODE

        ; Possibly complement it, for the checksum value.
        btfss   BYTE_N, 0
        xorlw   0xFF

        ; Store the value we need to send.
        movwf   SHIFT

        ; We know that BCOUNT is zero. Setting bit 3... makes it 8.
        bsf     BCOUNT, 3

        ; FALLTHRU

begin_560:
        ; We need 21 HIGH/LOW cycles.
        movlw   0x15
        movwf   CYCLE

        ; Go back to the 1:2 prescaler, as we start HIGH_DUTY below.

        ; FALLTHRU

prepare_duty:
        ; Set up the prescalar at 1:2.

        ; This will clear all the lower bits: TIMER0 uses instruction
        ; cycle clock, and prescaler is assigned to TIMER0 with 1:2 rate.
        ; We also happen to clear a few other bits that don't matter
        ; since we're using the internal clock.
        bank1s
        movlw   0x80
        andwf   OPTION_REG, F
        bank0s

        ; FALLTHRU

continue_duty:
        ; NOTE: assuming Bank 0.

        ; We're in the LOW state, and need to keep cycling.

        ; Turn on the LED, and load the timer with the HIGH duration.
        bsf     PORTC, LED
        movlw   HIGH_DUTY
        movwf   TMR0

        ; Strictly speaking, this is only needed at startup. But we
        ; cannot enable interrupts before we set a bunch of params.
        ; At this point, all params are set, so we enable interrupts,
        ; even if it is redundant mid-message. (the alternative is
        ; a second copy of timer-setup code)

        ; ### think more on this...
        bsf     INTCON, T0IE

        set_istate IS_DUTY_HIGH

        return


next_bit:
        decfsz  BCOUNT, F
        goto    more_bits       ; non-zero

        ; BCOUNT reached zero. This byte is done. Queue up the next
        ; byte to send, or deliver a trailer.

        decfsz  BYTE_N, F
        goto    more_bytes

        ; Ran out of bytes to send. Send a trailer pulse of 560 uS
        ; to mark the end of transmission.

        ; ### do something
        set_mstate MS_SEND_TRAILER
        goto    begin_560


more_bits:

        ; Sending a 0 bit means we want the LOW state for 560 uS.
        ; With a 1:128 prescaler against the instruction cycle, we
        ; wait for 9 interrupts. This maps to 576 uS.
        movlw   0x09

        ; Send in MSB order.
        rlf     SHIFT, F
        skpnc                   ; skip the 1-bit loading

        ; Sending a 1 bit means we want the LOW state for 1680 uS.
        ; With a 1:128 prescaler against the instruction cycle, we
        ; wait for 26 interrupts. This maps to 1664 uS.
        movlw   0x1A

        ; W has the number of interrupts to hold the LOW state.

        set_istate IS_BIT_LOW

        ; FALLTHRU

enter_low_state:
        ; NOTE: assuming Bank 0.
        ; NOTE: W must contain the number of 1:128 cycles that we
        ;       will remain in the LOW output state.
        ; NOTE: the LED is LOW.
        ; NOTE: ISTATE should be set on entry.

        movwf   CYCLE

        ; Switch to 1:128 prescaling.
        bank1s
        movlw   0x06
        iorwf   OPTION_REG, F

        return


; stupid assemblers don't realize EOF means *end*
        end
