title "PIC16F870 Unilink(R) Interface by Werner Johansson, wj@yodel.net" subtitl "Definitions" list c=150,P=16F870,R=DEC,F=inhx8m include "p16f870.inc" ; Standard equates & Macros ERRORLEVEL 1,-302 ; Get rid of those annoying 302 msgs! ;****************************************************************************** ; ; This program is free software; you can redistribute it and/or modify ; it under the terms of the GNU General Public License as published by ; the Free Software Foundation; either version 2 of the License, or ; (at your option) any later version. ; ; This program is distributed in the hope that it will be useful, ; but WITHOUT ANY WARRANTY; without even the implied warranty of ; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ; GNU General Public License for more details. ; ; You should have received a copy of the GNU General Public License ; along with this program; if not, write to the Free Software ; Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA ; ; Author: Werner Johansson (wj@yodel.net), with some code and ideas from ; Simon Woods' GNUnilink, radix conversion utilities from piclist.com ; and of course the reverse-engineered Unilink(R) command list ; ;****************************************************************************** ;---------------------------------------------------------------- ; The Configuration Word __CONFIG _HS_OSC&_WDT_OFF&_PWRTE_ON&_BODEN_ON&_LVP_OFF&_CPD_OFF&_WRT_ENABLE_ON&_DEBUG_OFF&_CP_OFF ;---------------------------------------------------------------- ; TODO ;---------------------------------------------------------------- ; Check how the slave should sense "power off" de-selection. Now it continues to play, as the slave doesn't stop requesting updates! ; Investigate whether I actually have to save PCLATH in ISH, maybe save FSR? - Not saving any of them for now ; Check Overrun errors from the UART ; Implement the Watchdog Timer (might be useful even though I haven't seen it hang yet..) ; Make the bit shift routine at the beginning of the ISR timeout if the clock suddenly stops (in the middle of a byte) ; (will keep it from hanging until the next bit gets clocked out, just ignore the faulty bits and carry on) ; Implement command b0 0x (change CD to x (1-a))? ; Implement command 08 10 (Tel Mute on) and 08 18 (Tel Mute off)? ;---------------------------------------------------------------- ; HISTORY ;---------------------------------------------------------------- ; Version ; ; 0.9 Interrupt driven UART RX for status updates, using raw unilink packets for everything else, dynamic text ; 0.8 Some text commands implemented, only static text for now though ; 0.7 Debug Serial TX in ISR now, checksum check for incoming packets in place, A/D works, solved the master reset prob ; (by calling the INT handler from TMR2 ISR code (too much interrupt latency when transmitting) ; 0.6 Some more LCD info and clean-up of the Unilink recovery code, some problems with master resetting :( ; 0.5 Issues slave breaks seemingly without hickups (!) ; 0.4 Some fixups in the bootstrap code (I actually had to put the PIC in my PICSTART Plus programmer again :)) ; 0.3 Implementing more Unilink commands and RingIndicator control (to wake the computer from sleep) ; 0.2 First attempt at responding to the Anyone command ; 0.1 Receives Unilink data OK, relays it to serial ; 0.0 Very first "F**king No Work!" version ;---------------------------------------------------------------- ; I/O LAYOUT ;---------------------------------------------------------------- ; Unilink BUSON IN (blue) connected to RC2/CCP1 ; Unilink DATA (green) connected to RC3 ; Unilink BUSON OUT (blue) connected to RC4 (this is for daisy-chaining) ; Unilink CLK (yellow) connected to RB0/INT (Interrupt pin) ; Unilink RST (lilac) connected to RA4 ; LCD RS connected to pin RB1 (The LCD is a standard 16x1 char HD44780 compatible unit) ; LCD RW connected to pin RB2 ; LCD E connected to pin RB3 ; LCD DB4-DB7 connected to RB4-RB7 ; RS-232 TX from computer connected to RC7/RX ; RS-232 RX to computer connected to RC6/TX ; RS-232 RI to computer connected to RC5 ; B+ connected via trimmer and resistors to AN0 (divider approx 20k5/5k to give 20.48V maximum scale) ; ; This leaves RC0, RC1 and four analog inputs (AN1-AN4) free for now... #define BUSON_IN_BIT PORTC,2 #define DATA_BIT PORTC,3 #define BUSON_OUT_BIT PORTC,4 #define CLK_BIT PORTB,0 #define RST_BIT PORTA,4 #define LCD_RS_BIT PORTB,1 #define LCD_RW_BIT PORTB,2 #define LCD_E_BIT PORTB,3 #define LCD_DB4_BIT PORTB,4 #define LCD_DB5_BIT PORTB,5 #define LCD_DB6_BIT PORTB,6 #define LCD_DB7_BIT PORTB,7 #define RS232_RI_BIT PORTC,5 ;---------------------------------------------------------------- ; FILE REGISTER USAGE ;---------------------------------------------------------------- ; Free from 20h-48h TrackName equ 20h TracknameEnd equ 3eh DiscName equ 3fh DiscNameEnd equ 48h CurDisc equ 49h CurTrack equ 4ah CurMins equ 4bh CurSecs equ 4ch RecvType equ 4dh TargetPos equ 4eh CurRecvPos equ 4fh UnilinkRAD equ 50h ; Beginning of Unilink packet - the Receiving Address UnilinkTAD equ 51h ; Transmitter address UnilinkCMD1 equ 52h ; CMD1 byte UnilinkCMD2 equ 53h ; CMD2 byte UnilinkParity1 equ 54h ; First or only parity byte for short packets (6 bytes) UnilinkData1 equ 55h ; Extra data for medium/large packets, or zero for short packets UnilinkData2 equ 56h ; UnilinkData3 equ 57h ; UnilinkData4 equ 58h ; UnilinkData5 equ 59h ; Data5 if this is a large packet UnilinkParity2M equ 59h ; Parity2 shares the same byte if it's a medium sized packet UnilinkData6 equ 5ah ; Extra data for large packets, or zero for medium packets UnilinkData7 equ 5bh ; UnilinkData8 equ 5ch ; UnilinkData9 equ 5dh ; UnilinkParity2 equ 5eh ; Parity byte for large packets UnilinkZero equ 5fh ; Should always be zero (possibly used to signal corrupt packets from slave to master?) UnilinkTimeout equ 60h ; Counts up every 0.5ms to "age out" faulty bytes clocked in UnilinkSelected equ 61h ; High bit is set when selected UnilinkBit equ 62h ; This is my "bitmask" to be used for requests UnilinkID equ 63h ; This is my Bus ID UnilinkCmdLen equ 64h ; This gets updated with the actual packet length after CMD1 has been received UnilinkTXRX equ 65h ; This is a pointer to the Unilink packet above, used with indirect addressing SlaveBreakState equ 66h ; Hold state and time-out information about slave break, indicates when it can happen DisplayStatus equ 67h ; What information will be put on the display next, bit 7 cleared if nothing Icount equ 68h ; Offset of string to print TxTemp equ 69h ; blahblah TxTemp2 equ 6ah ; Blahblah2 LCDWTmp equ 6bh Dcount2 equ 6ch temp equ 6dh Dcount equ 6eh e_LEN equ 6fh Counter equ 70h DataCount equ 71h ; Temp storage for the bit counter used during bit shifts (Unilink TX/RX) UnilinkCurID equ 72h ; This is a kludge DisplayCounter equ 73h UnilinkAttenuation equ 74h ; The amount of attenuation the volume control is currently set to NumH equ 75h NumL equ 76h TenK equ 77h Thou equ 78h Hund equ 79h Tens equ 7ah Ones equ 7bh UnilinkReInits equ 7ch IRQPCLATH equ 7dh ; ISH storage IRQSTATUS equ 7eh ; Needs to be located in a shared area accessible from all register banks IRQW equ 7fh ; RecvBuf equ 0a0h ; Buffer for received data from PC (32 bytes) RecvBufEnd equ 0bfh ; subtitl "Startup" page ;---------------------------------------------------------------- ; Power up/Reset starting point org 0 ; Start at the beginning of memory (the reset vector) call Bootstrap ; Call Flash Load routine call LCDInit ; Initialize LCD I/F call IRQInit ; Set up and start the IRQ handler goto Main ; Run the main program loop (skip the IRQ handler) subtitl "IRQ Handler" ;---------------------------------------------------------------- ; Interrupt handler always starts at addr 4 ; In order to reduce the INT latency the actual code is put here directly instead of using a goto instruction. ; Also because of the real-time requirements for clocking data onto the Unilink bus the first check in the ISR ; is to see whether the Unilink clock rise was the reason for the interrupt. This results in a "clock rise to ; bit ready" time of less than 30 instruction cycles, should be plenty of spare time waiting for clock to go low ; again after that. Other interrupts might introduce latencies, but let's see how this works.. org 4 ; ISR vector is at address 4 movwf IRQW ; Save W swapf STATUS,w ; Get the status register into w clrf STATUS ; Zero out the status reg, gives Reg Bank0 movwf IRQSTATUS ; Store the STATUS reg ; Not using PCLATH for anything in the ISR right now ; movf PCLATH,w ; Get the PCLATH reg ; movwf IRQPCLATH ; And store it ; clrf PCLATH ; Go to low memory ; Maybe save FSR here as well (if there's a need for it in the non-ISR code) call IRQCheckINT ; Implemented as a subroutine as there's a need to call it repeatedly from the other ISRs btfss PIR1,TMR2IF ; Check if it's the TMR2 interrupt (0.5ms timing) goto IRQNotTMR2 ; No it's not, check the other sources incf Counter,f ; Increment the general purpose counter (increments every 0.5ms) ; Slave break opportunity detection here - the logic works as follows: ; Look for a data low period of at least 5 ms (10 loops) ; Look for a data high period of at least 2 ms (4 loops) ; If the Slave Break request bit has been set, issue a slave break by holding the data line low for 4ms (8 loops) ; If a bit would be received (CLK activates) the packet handler automatically clears out the SlaveBreakState, which means start all over call IRQCheckINT ; Check the Unilink INT as well btfsc SlaveBreakState,5 ; Check if already pulling the data line low goto IRQTMR2SlaveBreak btfsc SlaveBreakState,7 ; Looking for low or high data goto IRQTMR2HighData btfss DATA_BIT ; Looking for a low data line, if it's low, increment state, if it's high, reset state goto IRQTMR2LowDataOK clrf SlaveBreakState ; Got a high data line while waiting for a low one, reset state call IRQCheckINT ; Check the Unilink INT as well goto IRQAfterTMR2 ; Leave ISR IRQTMR2HighData call IRQCheckINT ; Check the Unilink INT as well btfsc DATA_BIT ; Looking for a high data line, if it's high - increment state, otherwise wait goto IRQTMR2HighDataOK movlw 080h btfsc SlaveBreakState,6 ; Test the "first time around" bit clrw ; Not the beginning of the state, have to restart the entire thing now, not just this state andwf SlaveBreakState,f ; Mask out the 1 upper control bits and restart this state goto IRQAfterTMR2 IRQTMR2HighDataOK IRQTMR2LowDataOK call IRQCheckINT ; Check the Unilink INT as well bsf SlaveBreakState,6 ; Set the "first time around" bit movf SlaveBreakState,w andlw 1fh btfss SlaveBreakState,7 ; Checking whether it's low or high goto IRQTMR2FoundLow xorlw 4 ; It's high now, and if 4 periods have passed we can activate slave break skpz goto IRQAfterTMR2 ; Issue slave break here clrf SlaveBreakState ; incf Counter,f : Debug! btfss DisplayStatus,7 ; Only do this if high bit is set goto IRQAfterTMR2 movlw 20h movwf SlaveBreakState bcf DATA_BIT bsf STATUS,RP0 bcf DATA_BIT bcf STATUS,RP0 goto IRQAfterTMR2 IRQTMR2FoundLow xorlw 10 skpz goto IRQAfterTMR2 call IRQCheckINT ; Check the Unilink INT as well movlw 80h ; Prepare for state 2, looking for data line high movwf SlaveBreakState goto IRQAfterTMR2 IRQTMR2SlaveBreak call IRQCheckINT ; Check the Unilink INT as well movf SlaveBreakState,w andlw 01fh xorlw 8 skpz goto IRQAfterTMR2 bsf STATUS,RP0 bsf DATA_BIT bcf STATUS,RP0 clrf SlaveBreakState IRQAfterTMR2 btfss SlaveBreakState,4 ; Only increment to 0x10 incf SlaveBreakState,f bcf PIR1,TMR2IF ; Clear the IRQ source bit to re-enable TMR2 interrupts again IRQNotTMR2 call IRQCheckINT ; Check the Unilink INT as well btfss PIR1,RCIF ; Check if it's the UART RX interrupt goto IRQNotRCIF ; No it's not, check the other sources ;---------------------------------------------------------------- ; This is the UART RX routine, this should be control info from the connected PC we're receiving movf CurRecvPos,w ; Load fsr movwf FSR movf RCREG,w ; Get the byte bcf PIR1,RCIF ; Clear current interrupt condition movwf INDF ; Store the recv byte at the position waiting movlw RecvType xorwf FSR,w ; Check if it's the recvtype coming in bnz NotRecvType call IRQCheckINT ; Check the Unilink INT as well movf RecvType,w ; Load type bz RecvTimeUpdate ; Position update xorlw 1 ; Track Name perhaps? bz RecvTrackName xorlw 3 ; this is xor 1 and then xor 2, disc name? bz RecvDiscName call IRQCheckINT ; Check the Unilink INT as well btfss RecvType,7 ; Check Hi byte goto IRQNotRCIF movf RecvType,w xorlw 81h bnz NotText call IRQCheckINT ; Check the Unilink INT as well movf RecvType,w goto TextUpdate NotText call IRQCheckINT ; Check the Unilink INT as well movf RecvType,w btfss DisplayStatus,7 ; Only if not updating already! TextUpdate movwf DisplayStatus ; Force it into DisplayStatus goto Finished RecvTimeUpdate movlw CurDisc ; Load DL start here movwf CurRecvPos movlw CurSecs+1 ; And stop here movwf TargetPos goto IRQNotRCIF RecvTrackName movlw TrackName ; Load DL start here movwf CurRecvPos movlw TracknameEnd+1 ; And stop here movwf TargetPos goto IRQNotRCIF RecvDiscName movlw DiscName ; Load DL start here movwf CurRecvPos movlw DiscNameEnd+1 ; And stop here movwf TargetPos goto IRQNotRCIF NotRecvType incf CurRecvPos,f call IRQCheckINT ; Check the Unilink INT as well movf CurRecvPos,w ; Check if we're done xorwf TargetPos,w bnz NotFinished Finished call IRQCheckINT ; Check the Unilink INT as well ; movlw 81h ; Assume we do it from the beginning (text and pos) ; movf RecvType,f ; Just check for time-update cmd ; skpnz ; No adjustment if another cmd ; movlw 85h ; Skip the first 4 display cmds ; movwf DisplayStatus ; call IRQCheckINT ; Check the Unilink INT as well movlw RecvType ; restore for another go movwf CurRecvPos NotFinished IRQNotRCIF call IRQCheckINT ; Check the Unilink INT as well btfss PIR1,TXIF ; Check if it's the UART TX interrupt goto IRQNotTXIF ; No it's not, check the other sources bsf STATUS,RP0 ; Reg bank 1 btfss PIE1,TXIE ; As TXIF is set as long as nothing gets sent, is the interrupt actually enabled? goto IRQTXIFDisabled ;---------------------------------------------------------------- ; This is the UART TX routine, gets called when TXIE has been set and the TX load register is empty IRQTXIFDisabled bcf STATUS,RP0 ; Make sure we're going back to reg bank 0 IRQNotTXIF ; Finally restore CPU state and return from the ISR ; If I have to save the FSR in the beginning I also need to restore it here... ; movf IRQPCLATH,w ; movwf PCLATH ; Restore PCLATH swapf IRQSTATUS,w movwf STATUS ; Restore STATUS swapf IRQW,f swapf IRQW,w ; Restore W retfie ; Interrupt return ;---------------------------------------------------------------- ; IRQCheckINT - This part is the actual Unilink tranceiver, have to call it often as there are only ~20 spare cycles ; (which is only a problem if we're going to transmit, but the check can be done anyway, it's cheap if no bit is there) IRQCheckINT btfss INTCON,INTF ; Check if it's the INT edge interrupt (Unilink CLK) return ; No it's not, return again after only four cycles ; If there's activity on the clock line (the clock goes high) the CPU will stay in here until eight bits have been clocked in ; - this reduces context switching (and it's just a few hundred cpu cycles after all (20us*8 bits=160us=800 instruction ; cycles (5 MIPS @ 20MHz), not even a problem for serial input if it's not receiving more than 6250 bytes per second, and the ; 2-byte FIFO somehow fills up (this should be impossible even @ 115200 as this blocking INT handler only runs a maximum of ; 1000 times per second, halting INT's for 1/6250 of a second - this gives the CPU ample of time to deal with all bytes from ; the USART. I should check the OERR (Serial Overrun) bit to catch this though.. Note that this piece of code does both TX ; and RX at the same time (in order to receive packets one has to make sure that the packet buffer is zeroed out before entering ; here, otherwise collisions will occur.. ; According to my logic analyzer this implementation is pretty decent when it comes to timing, even though it's an ; interrupt driven "USART" implemented in software - by trigging the interrupt on the rising edge there's some extra margin here ; (the clock goes high 10us before the master clocks the bit in (on the falling edge), that should be plenty of time..) movlw 8 ; Loop through the 8 bits movwf DataCount movf UnilinkTXRX,w ; Get the pointer movwf FSR ; Store it to make use of indirect addressing IRQINTBitSet btfss INDF,7 ; Test high bit of data (that's the first bit to be clocked out) goto IRQINTTristate ; Bit is low, we should tristate bit bcf PORTC,3 ; Otherwise set DATA bit low bsf STATUS,RP0 ; Select high regs bcf TRISC,3 ; And pull low (now it's an output) bcf STATUS,RP0 ; Back to regbank 0 goto IRQINTCLKWaitLow ; Wait for master to actually clock this bit in IRQINTTristate bsf STATUS,RP0 ; Select high regs bsf TRISC,3 ; Force the bit to be tristated bcf STATUS,RP0 ; Back to regbank 0 IRQINTCLKWaitLow btfss PORTC,2 ; Check for BUSON goto IRQAfterINT btfsc PORTB,0 ; Wait for clock to go low goto IRQINTCLKWaitLow clrc ; Clear carry btfss PORTC,3 ; Test DATA setc ; Set carry if data is LOW (data is inverted!) rlf INDF,f ; Shift it into the "accumulator" decfsz DataCount,f ; Loop once more perhaps? goto IRQINTCLKWaitHigh ; Yes, again! goto IRQINTRecvDone ; No it's done, don't check for clock to go high again IRQINTCLKWaitHigh btfss PORTC,2 ; Check for BUSON goto IRQAfterINT btfss PORTB,0 ; Wait for clock to go high goto IRQINTCLKWaitHigh goto IRQINTBitSet ; Loop again ; Successfully received a byte here, run it through a state machine to figure out what to do ; (several possibilites exists here): ;;;;;; If more than 1.1ms has passed since last receive, reset receive counter to zero ; If receive counter is zero and the received byte is a zero byte, discard it ; Otherwise store the byte in our receive buffer and increment receive counter ; If the receive counter is 3 check the two upper bits of recv'd byte (CMD1) - this tells us the length of the packet ; 00 = short 6 byte packet ; 10 = medium 11 byte packet ; 11 = long 16 byte packet ; Update the receive length byte accordingly ; Check whether receive length and receive count are equal, that means that we're finished and we can carry on parsing ; the packet and take appropriate action. IRQINTRecvDone clrf SlaveBreakState ; First of all, clear the break state - this got in the way, restart detection.. movf INDF,w call BootTXB ; Send the byte to the serial port movf UnilinkTXRX,w ; Find out which byte # that was received andlw 0fh ; Mask bnz IRQINTRecvNotFirst ; Not the first byte movf UnilinkRAD,w ; Get the first byte received bz IRQINTRecvNullByte ; Null byte received, ignore this, don't increment counter IRQINTRecvNotFirst incf UnilinkTXRX,f ; Increment address movf UnilinkTXRX,w ; Get the byte position again andlw 0fh ; Only lower 4 bits of interest xorlw 03h ; Well, is it the third byte? (CMD1, telling us the length of the packet) bnz IRQINTRecvNotCMD1 ; No, skip the length code for now movlw 6 ; Assume it's a short packet btfss INDF,7 ; INDF still points to received byte, test high bit for medium/long goto IRQINTRecvShort ; Nope, it's a short packet addlw 5 ; OK, it's long or medium at least btfsc INDF,6 ; Test for long addlw 5 ; Yep, it's a long packet IRQINTRecvShort movwf UnilinkCmdLen ; Store the length IRQINTRecvNotCMD1 movf UnilinkTXRX,w ; Get the byte position xorwf UnilinkCmdLen,w ; XOR with the calculated command length andlw 0fh ; and mask - this results in a zero result when finished receiving bnz IRQINTRecvIncomplete ; Packet not ready yet ; Here a packet is actually received, should check the checksum(s) now movf UnilinkRAD,w ; QnD checksum check addwf UnilinkTAD,w addwf UnilinkCMD1,w addwf UnilinkCMD2,w xorwf UnilinkParity1,w ; This should be zero bnz IRQINTParseComplete ; Don't allow packet parsing on corrupt packets btfss UnilinkCMD1,7 ; Test whether there's more parity to check (medium or long packet) goto IRQINTParser ; No, skip directly to parsing logic movf UnilinkParity1,w ; QnD checksum check for the remaining part of the packet addwf UnilinkData1,w addwf UnilinkData2,w addwf UnilinkData3,w addwf UnilinkData4,w btfss UnilinkCMD1,6 ; Test for a long packet goto IRQINTBypassLongPacket xorwf UnilinkParity2M,w ; Fix for the medium packet parity check a few lines down... addwf UnilinkData5,w addwf UnilinkData6,w addwf UnilinkData7,w addwf UnilinkData8,w addwf UnilinkData9,w xorwf UnilinkParity2,w ; This should be zero when xor:ed with the Parity2M IRQINTBypassLongPacket xorwf UnilinkParity2M,w ; This should be zero for valid medium and long packets bnz IRQINTParseComplete IRQINTParser ; This is inefficient, I know, I'll improve it later... (Not that it matters, there's plenty of time here ; (there won't be any more communication for at least another 4.8ms)) ; Unilink command parser: ; Check for CMD1 = 01h (System bus commands) movf UnilinkCMD1,w xorlw 01h bnz IRQINTParseNot01 ; Check for 01 00 (Bus Re-Initialization) movf UnilinkCMD2,w bnz IRQINTParseNot0100 call ClearUnilinkStatus ; Clear everything Unilink (ID, BUSON_OUT) incf UnilinkReInits,f ; increment the debug counter goto IRQINTParseComplete ; Don't send any reply to this (clear the packet buffer though) IRQINTParseNot0100 ; Check for 01 02 (Anyone) movf UnilinkCMD2,w xorlw 02h bnz IRQINTParseNot0102 movf UnilinkID,w ; Do I have an ID already? bnz IRQINTParseComplete ; Yep, I don't want another one! call ClearUnilinkBuffer ; Zero it out completely movlw 10h ; Sending to Master addwf UnilinkParity1,f movwf UnilinkRAD movlw 0d0h ; I'm in the MD changer group addwf UnilinkParity1,f movwf UnilinkTAD movlw 8ch ; Device discovery command reply addwf UnilinkParity1,f movwf UnilinkCMD1 movlw 10h ; 00?? addwf UnilinkParity1,f movwf UnilinkCMD2 movf UnilinkParity1,w movwf UnilinkParity2M movlw 24h ; My internal MD sends 25 here first time, and then 24 when appointed!?? addwf UnilinkParity2M,f movwf UnilinkData1 movlw 0a8h ; 2c?? addwf UnilinkParity2M,f movwf UnilinkData2 movlw 17h ; 22?? addwf UnilinkParity2M,f movwf UnilinkData3 movlw 0a0h ; 00?? 0a0=10 disc? addwf UnilinkParity2M,f movwf UnilinkData4 goto IRQINTParseBypassClear ; Don't clear the data, the buffer will be sent as the next packet IRQINTParseNot0102 ; Check for 01 12 (Time poll) movf UnilinkCMD2,w xorlw 12h bnz IRQINTParseNot0112 movf UnilinkRAD,w xorwf UnilinkID,w ; Is it for me? bnz IRQINTParseNot0112 ; Nope call ClearUnilinkBuffer movlw 10h ; Sending to Master addwf UnilinkParity1,f movwf UnilinkRAD movf UnilinkID,w ; This is my ID addwf UnilinkParity1,f movwf UnilinkTAD movlw 00h addwf UnilinkParity1,f movwf UnilinkCMD1 movlw 80h ; Idle unless selected btfsc UnilinkSelected,7 clrw addwf UnilinkParity1,f movwf UnilinkCMD2 goto IRQINTParseBypassClear ; Don't clear the data, the buffer will be sent as the next packet IRQINTParseNot0112 ; Check for 01 13 (Request Time poll) movf UnilinkCMD2,w xorlw 13h bnz IRQINTParseNot0113 movf UnilinkRAD,w xorwf UnilinkID,w ; Is it for me? bnz IRQINTParseNot0113 ; Nope btfss DisplayStatus,7 ; If not displaying, skip this goto IRQINTParseComplete call ClearUnilinkBuffer movlw 70h ; Sending to Display Group addwf UnilinkParity1,f movwf UnilinkRAD movf UnilinkID,w ; This is my ID addwf UnilinkParity1,f movwf UnilinkTAD movf DisplayStatus,w xorlw 80h ; First slave break? bnz IRQINTParse0113Not80 movlw 90h addwf UnilinkParity1,f movwf UnilinkCMD1 movlw 50h addwf UnilinkParity1,f movwf UnilinkCMD2 movf UnilinkParity1,w ; Carry the parity forward movwf UnilinkParity2M movf CurTrack,w addwf UnilinkParity2M,f movwf UnilinkData1 movf CurMins,w addwf UnilinkParity2M,f movwf UnilinkData2 movf CurSecs,w addwf UnilinkParity2M,f movwf UnilinkData3 ; movlw 0c0h movf CurDisc,w ; andlw 0f0h ; iorlw 0eh addwf UnilinkParity2M,f movwf UnilinkData4 ; clrf DisplayStatus ; goto IRQINTParseBypassClear ; Don't clear the data, the buffer will be sent as the next packet goto IRQINTParse0113Complete IRQINTParse0113Not80 movf DisplayStatus,w xorlw 81h ; Second slave break? bnz IRQINTParse0113Not81 movlw 0cdh ; Disc name addwf UnilinkParity1,f movwf UnilinkCMD1 movf DiscName,w addwf UnilinkParity1,f movwf UnilinkCMD2 movf UnilinkParity1,w ; Carry the parity forward movwf UnilinkParity2 movf DiscName+1,w addwf UnilinkParity2,f movwf UnilinkData1 movf DiscName+2,w addwf UnilinkParity2,f movwf UnilinkData2 movf DiscName+3,w addwf UnilinkParity2,f movwf UnilinkData3 movf DiscName+4,w addwf UnilinkParity2,f movwf UnilinkData4 movf DiscName+5,w addwf UnilinkParity2,f movwf UnilinkData5 movf DiscName+6,w addwf UnilinkParity2,f movwf UnilinkData6 movf DiscName+7,w addwf UnilinkParity2,f movwf UnilinkData7 movlw 00h addwf UnilinkParity2,f movwf UnilinkData8 movlw 0eh addwf UnilinkParity2,f movwf UnilinkData9 goto IRQINTParse0113Complete IRQINTParse0113Not81 movf DisplayStatus,w xorlw 82h ; Third slave break? bnz IRQINTParse0113Not82 movlw 0cdh ; Disc name addwf UnilinkParity1,f movwf UnilinkCMD1 movf DiscName+8,w addwf UnilinkParity1,f movwf UnilinkCMD2 movf UnilinkParity1,w ; Carry the parity forward movwf UnilinkParity2 movf DiscName+9,w addwf UnilinkParity2,f movwf UnilinkData1 movlw 0 addwf UnilinkParity2,f movwf UnilinkData2 movlw 0 addwf UnilinkParity2,f movwf UnilinkData3 movlw 0 addwf UnilinkParity2,f movwf UnilinkData4 movlw 0 addwf UnilinkParity2,f movwf UnilinkData5 movlw 0 addwf UnilinkParity2,f movwf UnilinkData6 movlw 0 addwf UnilinkParity2,f movwf UnilinkData7 movlw 00h addwf UnilinkParity2,f movwf UnilinkData8 movlw 1eh addwf UnilinkParity2,f movwf UnilinkData9 goto IRQINTParse0113Complete IRQINTParse0113Not82 movf DisplayStatus,w xorlw 83h ; Fourth slave break? bnz IRQINTParse0113Not83 movlw 0c9h ; Track name 1 addwf UnilinkParity1,f movwf UnilinkCMD1 movf TrackName,w addwf UnilinkParity1,f movwf UnilinkCMD2 movf UnilinkParity1,w ; Carry the parity forward movwf UnilinkParity2 movf TrackName+1,w addwf UnilinkParity2,f movwf UnilinkData1 movf TrackName+2,w addwf UnilinkParity2,f movwf UnilinkData2 movf TrackName+3,w addwf UnilinkParity2,f movwf UnilinkData3 movf TrackName+4,w addwf UnilinkParity2,f movwf UnilinkData4 movf TrackName+5,w addwf UnilinkParity2,f movwf UnilinkData5 movf TrackName+6,w addwf UnilinkParity2,f movwf UnilinkData6 movf TrackName+7,w addwf UnilinkParity2,f movwf UnilinkData7 movlw 00h addwf UnilinkParity2,f movwf UnilinkData8 movlw 0eh addwf UnilinkParity2,f movwf UnilinkData9 goto IRQINTParse0113Complete IRQINTParse0113Not83 movf DisplayStatus,w xorlw 84h ; Fifth slave break? bnz IRQINTParse0113Not84 movlw 0c9h ; Track name (2) addwf UnilinkParity1,f movwf UnilinkCMD1 movf TrackName+8,w addwf UnilinkParity1,f movwf UnilinkCMD2 movf UnilinkParity1,w ; Carry the parity forward movwf UnilinkParity2 movf TrackName+9,w addwf UnilinkParity2,f movwf UnilinkData1 movf TrackName+10,w addwf UnilinkParity2,f movwf UnilinkData2 movf TrackName+11,w addwf UnilinkParity2,f movwf UnilinkData3 movf TrackName+12,w addwf UnilinkParity2,f movwf UnilinkData4 movf TrackName+13,w addwf UnilinkParity2,f movwf UnilinkData5 movf TrackName+14,w addwf UnilinkParity2,f movwf UnilinkData6 movf TrackName+15,w addwf UnilinkParity2,f movwf UnilinkData7 movlw 00h addwf UnilinkParity2,f movwf UnilinkData8 movlw 1eh addwf UnilinkParity2,f movwf UnilinkData9 goto IRQINTParse0113Complete IRQINTParse0113Not84 movf DisplayStatus,w xorlw 85h ; Sixth slave break? bnz IRQINTParse0113Not85 movlw 90h addwf UnilinkParity1,f movwf UnilinkCMD1 movlw 50h addwf UnilinkParity1,f movwf UnilinkCMD2 movf UnilinkParity1,w ; Carry the parity forward movwf UnilinkParity2M movf CurTrack,w addwf UnilinkParity2M,f movwf UnilinkData1 movf CurMins,w addwf UnilinkParity2M,f movwf UnilinkData2 movf CurSecs,w addwf UnilinkParity2M,f movwf UnilinkData3 ; movlw 0c0h movf CurDisc,w ; andlw 0f0h ; iorlw 0eh addwf UnilinkParity2M,f movwf UnilinkData4 clrf DisplayStatus ; for now! goto IRQINTParse0113Complete IRQINTParse0113Not85 clrf DisplayStatus incf DisplayStatus,f ; Skip step one for now goto IRQINTParseComplete IRQINTParse0113Complete incf DisplayStatus,f ; Increment display state counter ; bsf DisplayStatus,7 goto IRQINTParseBypassClear ; Don't clear the data, the buffer will be sent as the next packet IRQINTParseNot0113 ; Check for 01 15 (Who sent the slave break?) movf UnilinkCMD2,w xorlw 15h bnz IRQINTParseNot0115 btfss DisplayStatus,7 ; First of all check if there should be anything displayed goto IRQINTParseComplete ; No, not at this time call ClearUnilinkBuffer movlw 10h ; Sending to Master addwf UnilinkParity1,f movwf UnilinkRAD movlw 18h ; Broadcast address sending in this special case addwf UnilinkParity1,f movwf UnilinkTAD movlw 82h ; Who wants to talk reply command addwf UnilinkParity1,f movwf UnilinkCMD1 clrw call Bit_Frig addwf UnilinkParity1,f movwf UnilinkCMD2 movf UnilinkParity1,w ; Carry the parity forward movwf UnilinkParity2M movlw 20h call Bit_Frig addwf UnilinkParity2M,f movwf UnilinkData1 movlw 40h call Bit_Frig addwf UnilinkParity2M,f movwf UnilinkData2 movlw 60h call Bit_Frig addwf UnilinkParity2M,f movwf UnilinkData3 movlw 80h call Bit_Frig addwf UnilinkParity2M,f movwf UnilinkData4 goto IRQINTParseBypassClear ; Don't clear the data, the buffer will be sent as the next packet ;****************************************************************************** ; Bit frig - works out which bit to set in the response to Master Poll ; This is taken more or less verbatim from Simon Woods' GNUnilink code! ; ; W register is input of which stage you are on (0x00, 0x20, 0x40 etc) ; and is returned with the byte to write (0x00 if wrong stage). Bit_Frig: xorwf UnilinkBit, 0 andlw 0xe0 ; Strip off low bits btfsc STATUS, Z ; Do we have a hit? goto Bit_Frig_Hit movlw 0x00 return Bit_Frig_Hit: btfss UnilinkBit, 4 ; Do we need to swap nybbles? goto Bit_Frig_Swap movf UnilinkBit, 0 andlw 0x0F return Bit_Frig_Swap: swapf UnilinkBit, 0 andlw 0xF0 return IRQINTParseNot0115 IRQINTParseNot01 ; Check for CMD1 = 02h (Appoint) movf UnilinkCMD1,w xorlw 02h bnz IRQINTParseNot02 movf UnilinkID,w ; Do I have an ID already? bnz IRQINTParseComplete ; Yep, I don't want another one! movf UnilinkRAD,w ; So I don't have any ID yet, see what the master is trying to set andlw 0f0h ; Check the device group xorlw 0d0h ; Verify it's a MD changer bnz IRQINTParseComplete ; No, something else, skip this movf UnilinkRAD,w ; Get the ID the master has given me movwf UnilinkID ; Store my id movf UnilinkCMD2,w ; Get the bitmask movwf UnilinkBit ; And store it (this is needed when doing slave breaks and actually responding) call ClearUnilinkBuffer movlw 10h ; Sending to Master addwf UnilinkParity1,f movwf UnilinkRAD movf UnilinkID,w ; This is my ID addwf UnilinkParity1,f movwf UnilinkTAD movlw 8ch ; Device discovery command again addwf UnilinkParity1,f movwf UnilinkCMD1 movlw 10h addwf UnilinkParity1,f movwf UnilinkCMD2 movf UnilinkParity1,w movwf UnilinkParity2M ; That's the parity when sending medium messages movlw 24h addwf UnilinkParity2M,f movwf UnilinkData1 movlw 0a8h ; My internal MD sends 1c here... (external/internal difference) addwf UnilinkParity2M,f movwf UnilinkData2 movlw 17h addwf UnilinkParity2M,f movwf UnilinkData3 movlw 0a0h ; 0a0=10disc addwf UnilinkParity2M,f movwf UnilinkData4 bsf BUSON_OUT_BIT ; Now activate the cascade BUSON pin, to allow others after us to be discovered goto IRQINTParseBypassClear ; Don't clear the data, the buffer will be sent as the next packet IRQINTParseNot02 ; Check for CMD1 = 80h (Display button) movf UnilinkCMD1,w xorlw 080h bnz IRQINTParseNot80 movf UnilinkID,w ; Check if I'm currently selected xorwf UnilinkCurID,w skpnz ; No, skip this command ; bsf DisplayStatus,7 ; Make sure we update the display again movlw 81h movwf DisplayStatus goto IRQINTParseComplete IRQINTParseNot80 ; Check for CMD1 = 87h (Power control) movf UnilinkCMD1,w xorlw 087h bnz IRQINTParseNot87 ; This part could use some more packet sniffing (really), it's sketchy to say the least.. :( ; The idea here is that the 18 10 87 2a PP 12 00 80 00 PP ZZ command is sent on power-up from my headunit if green color and ; if amber it looks like 18 10 87 2a PP 02 00 80 00 PP ZZ, giving away that high nibble (or bit 4) of D1 sets the color ; Also interesting is that the exact same commands gets sent when pressing the power-off button, this makes slaves pause ; playing, and then a few seconds later the unit shuts down with the following command: ; 18 10 87 22 PP 02 00 80 00 PP ZZ or ; 18 10 87 22 PP 12 00 80 00 PP ZZ depending on the color of the backlight ; This makes me think that bit 3 in CMD2 reflects the actual power state of the headunit (actually sleeping or bus active) ; Anyway I use this to set/clear the RI pin used for WakeOnRing on my laptop ; Also I de-select to make everything pause and clear the display status (if we're doing slave breaks after power status ; the headunit will never enter sleep!) ; From what I have gathered the bit mapping of DATA1 is as follows: ; 7 6 5 4 3 2 1 0 ; X - Backlight color changed if 1 ; ? ? ; X - Backlight color, 0=Amber, 1=Green ; X - Dimmer setting changed if 1 ; X X - Dimmer setting, 01=Dimmer Auto, 10=Dimmer On, 00=Dimmer Off, 11=??? ; X - Beep setting, 0=Beep on(!), 1=Beep off ; ; Also bit field of CMD2 for now: ; 7 6 5 4 3 2 1 0 ; X X - These two bits are set when changing color, beep etc, but not when actually powering the system on or off??? ; X - Always set to 1 on my headunit ; X - Always set to 0 on my headunit ; X - Set to 1 when power is on, 0 when powering off (last command sent before bus dies is 87 22 on my unit) ; X - Always set to 0 on my headunit ; X - Always set to 1 on my headunit ; X - Always set to 0 on my headunit ; Test for power-on bit (it seems like bit 3 (0x08h) of CMD2 is set when the power is on) btfsc UnilinkCMD2,3 goto IRQINTParse87PowerOn bsf RS232_RI_BIT ; Set this to make RI pin go low (after RS-232 levels) goto IRQINTParseComplete IRQINTParse87PowerOn bcf RS232_RI_BIT ; Clear this to make RI pin go high (waking the computer) btfsc UnilinkCMD2,7 ; Test high bit if it's just a "set" command, if yes don't clear status goto IRQINTParseComplete bcf UnilinkSelected,7 ; Also de-select us (this gets sent when powering off but before the actual power down) clrf DisplayStatus goto IRQINTParseComplete IRQINTParseNot87 ; Check for CMD1 = 90h (Display/DSP info, volume etc.) movf UnilinkCMD1,w xorlw 090h bnz IRQINTParseNot90 ; Check for 90 10 (Current Volume) movf UnilinkCMD2,w xorlw 010h bnz IRQINTParseNot9010 movf UnilinkData1,w ; Store current volume setting movwf UnilinkAttenuation goto IRQINTParseComplete ; Don't send any reply to this (clear the packet buffer though) IRQINTParseNot9010 IRQINTParseNot90 ; Check for CMD1 = f0h (Source Select) movf UnilinkCMD1,w xorlw 0f0h bnz IRQINTParseNotF0 movf UnilinkCMD2,w movwf UnilinkCurID ; Store it for display and debugging xorwf UnilinkID,w ; Check if it's selecting me bnz IRQINTParseF0Deselect bsf UnilinkSelected,7 ; Now we're selected ; bsf DisplayStatus,7 movlw 81h movwf DisplayStatus goto IRQINTParseComplete IRQINTParseF0Deselect bcf UnilinkSelected,7 ; Now we're de-selected bcf DisplayStatus,7 goto IRQINTParseComplete IRQINTParseNotF0 IRQINTParseComplete ; The code ends up here when parsing is complete and it's not interested in sending any reply back to the master ; (that's why we clear out all the packet buffer bytes) call ClearUnilinkBuffer IRQINTParseBypassClear movlw UnilinkRAD ; Get the pointer to the first byte in the receive buffer movwf UnilinkTXRX ; Store it - this way the next byte that gets received goes into RAD clrf UnilinkCmdLen ; No command length while waiting for a new packet IRQINTRecvIncomplete IRQINTRecvNullByte ; movf INDF,w ; movwf DataStore ; Store it so the non-irq code can snoop IRQAfterINT bcf INTCON,INTF ; Clear the IRQ source bit to re-enable INT interrupts again IRQNotINT return ;---------------------------------------------------------------- ; ClearUnilinkStatus - Zeroes out the Unilink state (used when initializing) ClearUnilinkStatus clrf UnilinkID ; Clear the existing Unilink ID, if any clrf UnilinkCurID ; Clear the currently selected ID as well bcf BUSON_OUT_BIT ; Clear the cascade BUSON pin, not activated again until we have a new ID clrf DisplayStatus ; No crazy display updates when resetting.. :) clrf UnilinkSelected ; We're not selected anymore bsf STATUS,RP0 ; Reg bank 1 bsf DATA_BIT ; Make sure data is tristated bcf STATUS,RP0 ; Reg bank 0 movlw UnilinkRAD ; Get the pointer to the first byte in the receive buffer movwf UnilinkTXRX ; Store it - this way the next byte that gets received goes into RAD clrf UnilinkCmdLen ; No command length while waiting for a new packet clrf SlaveBreakState ; Slave Break Processing has to start all over return ;---------------------------------------------------------------- ; ClearUnilinkBuffer - Zeroes out the Unilink packet buffer ClearUnilinkBuffer ; TODO: Replace this with an FSR access to save space and make the code neater clrf UnilinkRAD clrf UnilinkTAD clrf UnilinkCMD1 clrf UnilinkCMD2 clrf UnilinkParity1 clrf UnilinkData1 clrf UnilinkData2 clrf UnilinkData3 clrf UnilinkData4 clrf UnilinkData5 clrf UnilinkData6 clrf UnilinkData7 clrf UnilinkData8 clrf UnilinkData9 clrf UnilinkParity2 clrf UnilinkZero return subtitl "Main loop" page ;---------------------------------------------------------------- ; Main program begins here. [Called after bootloader, lcdinit and irqinit...] ; Here all other house keeping tasks are performed, like displaying info on the LCD.. Main movlw high LookUp ; Set the high PC bits to indicate data lookup page movwf PCLATH movlw 0ffh ; Set infinite attenuation to begin with movwf UnilinkAttenuation clrf RecvType movlw RecvType movwf CurRecvPos clrf UnilinkReInits ; Clear the bus re-initialization counter bsf STATUS,RP0 ; Reg bank 1 movlw 080h ; Right adjusted A/D, all analog inputs, no Vrefs movwf ADCON1 bcf STATUS,RP0 movlw 081h ; Activate A/D, ch 0, Fosc/32 (for 20MHz operation) movwf ADCON0 bsf ADCON0,2 ; Start the first A/D operation movlw 8 ; Display page timing (approx 8/sec) movwf DisplayCounter bcf LCD_RS_BIT ; LCD Command mode movlw 80h ; DisplayRam 0 call TxLCDB bsf LCD_RS_BIT movlw low DefaultText1 movwf Icount movlw 80 movwf e_LEN call TxLCD8BLoop ; Send 80 bytes to the LCD MainLoop bcf LCD_RS_BIT ; LCD Command mode movlw 80h ; DisplayRam 0 call TxLCDB bsf LCD_RS_BIT ; movlw '0' movf Counter,w ; Debug timer btfsc PORTA,4 ; Test RST movlw 'R' call TxLCDB ; movlw '0' movf SlaveBreakState,w andlw 80h btfsc PORTB,0 ; Test CLK movlw 'C' call TxLCDB movlw '0' btfsc PORTC,2 ; Test BUSON-IN movlw 'B' call TxLCDB movlw '0' btfsc PORTC,3 ; Test DATA movlw 'D' call TxLCDB movf UnilinkCmdLen,w bz MainDontPrintCmd addlw '0' call TxLCDB MainDontPrintCmd ; Default text ; UnilinkID @ 13-14 ; UnilinkAttenuation @ 16-17 ; UnilinkSelected @ 28-29 ; UnilinkReInits @ 38,39 ; UnilinkCurID @ 54-55 ; DisplayStatus @ 62-63 ; BattVoltage @ 66,67,69,70 (thou,hund,tens,unit) bcf LCD_RS_BIT ; LCD Command mode movlw 80h+13 ; DisplayRam 13 call TxLCDB bsf LCD_RS_BIT movf UnilinkID,w call TxLCDHEX bcf LCD_RS_BIT ; LCD Command mode movlw 80h+16 ; DisplayRam 16 call TxLCDB bsf LCD_RS_BIT movf UnilinkAttenuation,w call TxLCDHEX bcf LCD_RS_BIT ; LCD Command mode movlw 80h+28 ; DisplayRam 28 call TxLCDB bsf LCD_RS_BIT movf UnilinkSelected,w call TxLCDHEX bcf LCD_RS_BIT ; LCD Command mode movlw 80h+38 ; DisplayRam 38 call TxLCDB bsf LCD_RS_BIT movf UnilinkReInits,w call TxLCDHEX bcf LCD_RS_BIT ; LCD Command mode movlw 80h+40h+14 ; DisplayRam 54 call TxLCDB bsf LCD_RS_BIT movf UnilinkCurID,w call TxLCDHEX bcf LCD_RS_BIT ; LCD Command mode movlw 80h+40h+22 ; DisplayRam 62 call TxLCDB bsf LCD_RS_BIT movf DisplayStatus,w call TxLCDHEX btfsc ADCON0,2 ; Test if A/D is ready goto MainADNotReady bsf STATUS,RP0 movf ADRESL,w ; Add to our result bcf STATUS,RP0 addwf NumL,f skpnc incf NumH,f movf ADRESL,w addwf NumH,f ; And the high byte movlw 20h addwf NumH,f skpc ; When this overflows we know there are 8 samples collected goto MainADStartAD ; Now shift the added results two steps down (/4) as there are 8 added samples here, and filter high bits movlw 1fh andwf NumH,f clrc rrf NumH,f rrf NumL,f clrc rrf NumH,f rrf NumL,f movf Counter,w bnz MainADSkipDisplay bcf LCD_RS_BIT ; LCD Command mode movlw 80h+40h+26 ; DisplayRam 66 call TxLCDB bsf LCD_RS_BIT call BCDConvert movf Thou,w addlw 30h call TxLCDB movf Hund,w addlw 30h call TxLCDB movlw '.' call TxLCDB movf Tens,w addlw 30h call TxLCDB movf Ones,w addlw 30h call TxLCDB MainADSkipDisplay clrf NumL clrf NumH MainADStartAD bsf ADCON0,2 ; Start a new conversion MainADNotReady ; This part handles display "scroll" by shifting one screen at a time btfss Counter,7 ; Test high bit goto MainCounterLow ; So bit is high, set high bit of displaycounter as well... bsf DisplayCounter,7 goto MainSkipScroll MainCounterLow ; OK, bit is low, now figure out whether it was high or low last time -> check high bit of DisplayCounter btfss DisplayCounter,7 goto MainSkipScroll bcf DisplayCounter,7 ; Clear the high bit to allow countdown to commence movf Counter,w ; Load it skpz goto MainSkipScroll decfsz DisplayCounter,f goto MainSkipScroll movlw 8 movwf DisplayCounter bcf LCD_RS_BIT ; LCD Command mode movlw 18h ; Display shift Left call TxLCDB ; Shift it 8 positions call TxLCDB call TxLCDB call TxLCDB call TxLCDB call TxLCDB call TxLCDB call TxLCDB bsf LCD_RS_BIT MainSkipScroll ; Display scroll part ends here... goto MainLoop ;---------------------------------------------------------------- ; IRQInit - Sets up the IRQ Handler ; Set up Timer2 to generate 2000 interrupts per second, used for timing - 1/16 prescaler and a PR2 reg of 156 (0x9c) is set ; Also enable INT interrupts for Unilink CLK processing IRQInit call ClearUnilinkStatus call ClearUnilinkBuffer ; Fix the output state of RI and BUSON_OUT to a safe default bsf RS232_RI_BIT ; RS232 RI should be inactive (inverted logic, a set bit here gives a negative output) bcf BUSON_OUT_BIT ; BUSON_OUT should be disabled for now, must be appointed first movlw 06h ; Timer2 enabled + 1/16 prescaler movwf T2CON bsf STATUS,RP0 ; Reg bank 1 movlw 09ch ; Timer PR2 reg giving 2000 interrupts per second movwf PR2 bcf RS232_RI_BIT ; Both bits should be outputs bcf BUSON_OUT_BIT ; ; The default behavior of RB0/INT is to interrupt on the rising edge, that's what we use... ; bcf OPTION_REG,INTEDG ; We want RB0 to give us an IRQ on the falling edge bsf INTCON,INTE ; Enable the RB0/INT bsf INTCON,PEIE ; Enable the peripheral interrupts bsf PIE1,TMR2IE ; Enable the Timer2 peripheral interrupt bsf PIE1,RCIE ; Enable the UART receive interrupt bsf INTCON,GIE ; Enable global interrupts bsf TXSTA,TXEN ; Enable UART TX bcf STATUS,RP0 ; Back to bank 0 bsf RCSTA,SPEN ; Enable serial port bsf RCSTA,CREN ; Enable UART RX return ;---------------------------------------------------------------- ; Initialize LCD Controller... LCDInit clrf PORTB ; First clear PortB data register bsf STATUS,RP0 ; Reg bank 1 movlw 001h ; All but RB0 are outputs. movwf TRISB ; bcf OPTION_REG,NOT_RBPU ; Turn on port B pull-up bcf STATUS,RP0 ; Restore Reg bank 0 ; This is a standard reset sequence for the LCD controller movlw 170 ; Need to delay for at least 15ms, let's go for 17ms delay call DelayW movlw 3 ; Write 3 to the LCD call TxLCD ; Send to LCD movlw 60 ; Need to delay for at least 4.1ms, let's go for 6ms delay call DelayW movlw 3 ; Write 3 to the LCD call TxLCD movlw 10 ; Need to delay for at least 100us, let's go for 1ms delay call DelayW movlw 3 ; Write 3 to the LCD call TxLCD movlw 10 ; Need to delay for at least 40us, let's go for 1ms delay call DelayW movlw 2 ; 4-bit interface requested call TxLCD ; movlw 10 ; Need to delay for at least 40us, let's go for 1ms delay call DelayW ; ; Reset sequence ends here ; From this point no delays are needed, now the BUSY bit is valid and the bus I/F is 4 bits movlw 28h ; Function Select + 4-bit bus + 2-line display call TxLCDB movlw 0ch ; Display Control + LCD On (No cursor) call TxLCDB movlw 01h ; Clear Display call TxLCDB movlw 06h ; Auto Increment cursor position call TxLCDB bsf LCD_RS_BIT ; Accept data return ;---------------------------------------------------------------- ; TxLCDHEX ; Sends two characters hex to the LCD TxLCDHEX ; Original binary to 2-digit hex conversion from piclist.com, modified to fit here movwf Icount swapf Icount,w andlw 0x0f addlw 6 skpndc addlw 'A'-('9'+1) addlw '0'-6 xorwf Icount,w xorwf Icount,f xorwf Icount,w andlw 0x0f addlw 6 skpndc addlw 'A'-('9'+1) addlw '0'-6 movwf e_LEN movf Icount,w call TxLCDB movf e_LEN,w call TxLCDB return ;---------------------------------------------------------------- ; ; Binary-to-BCD. Written by John Payson. ; Taken from piclist.com - why re-invent the wheel when writing open-sourced code? ; ; Enter with 16-bit binary number in NumH:NumL. ; Exits with BCD equivalent in TenK:Thou:Hund:Tens:Ones. ; BCDConvert: ; Takes number in NumH:NumL ; Returns decimal in ; TenK:Thou:Hund:Tens:Ones swapf NumH,w andlw 0Fh ;*** PERSONALLY, I'D REPLACE THESE 2 addlw 0F0h ;*** LINES WITH "IORLW 11110000B" -AW movwf Thou addwf Thou,f addlw 0E2h movwf Hund addlw 032h movwf Ones movf NumH,w andlw 0Fh addwf Hund,f addwf Hund,f addwf Ones,f addlw 0E9h movwf Tens addwf Tens,f addwf Tens,f swapf NumL,w andlw 0Fh addwf Tens,f addwf Ones,f rlf Tens,f rlf Ones,f comf Ones,f rlf Ones,f movf NumL,w andlw 0Fh addwf Ones,f rlf Thou,f movlw 07h movwf TenK ; At this point, the original number is ; equal to TenK*10000+Thou*1000+Hund*100+Tens*10+Ones ; if those entities are regarded as two's compliment ; binary. To be precise, all of them are negative ; except TenK. Now the number needs to be normal- ; ized, but this can all be done with simple byte ; arithmetic. movlw 0Ah ; Ten Lb1: addwf Ones,f decf Tens,f btfss 3,0 goto Lb1 Lb2: addwf Tens,f decf Hund,f btfss 3,0 goto Lb2 Lb3: addwf Hund,f decf Thou,f btfss 3,0 goto Lb3 Lb4: addwf Thou,f decf TenK,f btfss 3,0 goto Lb4 retlw 0 ;---------------------------------------------------------------- ; TxLCD16B ; Send a string to the LCD. TxLCD16B movwf Icount bcf LCD_RS_BIT movlw 80h ; DisplayRam 0 call TxLCDB bsf LCD_RS_BIT call TxLCD8B bcf LCD_RS_BIT movlw 80h+40 ; DisplayRam 40 (row 2) call TxLCDB bsf LCD_RS_BIT call TxLCD8B return ;---------------------------------------------------------------- ; TxLCD8B ; Send a string to the LCD. TxLCD8B ; movwf Icount ; Icount = W movlw 8 movwf e_LEN ; Move to e_LEN TxLCD8BLoop movf Icount,w ; get the byte call LookUp incf Icount,f ; ...else ++Icount (table index) call TxLCDB ; Send out the byte decfsz e_LEN,f goto TxLCD8BLoop return ;---------------------------------------------------------------- ; TxLCDB - send a byte to the LCD TxLCDB movwf TxTemp ; Store byte to send for a while... bcf temp,0 ; Clear my temp bit btfss LCD_RS_BIT ; Check if we try the correct reg goto RxNoProb bcf LCD_RS_BIT bsf temp,0 ; Indicate RS change RxNoProb NotReady call RxLCDB ; Receive byte from LCD, status reg andlw 80h skpz ; If the bit was set, the zero flag is not goto NotReady btfsc temp,0 ; If we had to clear RS reset it now bsf LCD_RS_BIT swapf TxTemp,w ; Hi nibble of data to send in lo w bits call TxLCD ; Send them first... movf TxTemp,w ; Then we have the low nibble in low w bits call TxLCD ; And send that one as well return ;---------------------------------------------------------------- ; RxLCDB - recv a byte from the LCD RxLCDB call RxLCD ; Receive the high nibble movwf LCDWTmp swapf LCDWTmp,f ; Swap it back to file call RxLCD ; Receive the low nibble addwf LCDWTmp,w ; Put the nibbles together and return in W return ;---------------------------------------------------------------- ; TxLCD - send a nibble to the LCD TxLCD movwf LCDWTmp ; Write nibble to tmp bcf LCD_DB4_BIT ; Clear previous data bcf LCD_DB5_BIT ; bcf LCD_DB6_BIT ; bcf LCD_DB7_BIT ; btfsc LCDWTmp,0 ; Test bit 0, transfer a set bit to LCD PORT bsf LCD_DB4_BIT btfsc LCDWTmp,1 ; Test bit 1, transfer a set bit to LCD PORT bsf LCD_DB5_BIT btfsc LCDWTmp,2 ; Test bit 2, transfer a set bit to LCD PORT bsf LCD_DB6_BIT btfsc LCDWTmp,3 ; Test bit 3, transfer a set bit to LCD PORT bsf LCD_DB7_BIT bsf LCD_E_BIT ; And set E to clock the data into the LCD module nop ; Let it settle bcf LCD_E_BIT ; And clear the Enable again. return ; Returns without modifying W ;---------------------------------------------------------------- ; RxLCD - recv a nibble from the LCD RxLCD clrw ; Clear W register, return data in lower 4 bits bsf STATUS,RP0 ; Select 2nd reg bank, now TRIS regs can be accessed bsf LCD_DB4_BIT ; This sets the port bit as an input bsf LCD_DB5_BIT bsf LCD_DB6_BIT bsf LCD_DB7_BIT bcf STATUS,RP0 ; Back at reg bank 0 bsf LCD_RW_BIT ; Set Read mode for the LCD bsf LCD_E_BIT ; And set E to clock the data out of the LCD module nop ; Let the bus settle btfsc LCD_DB4_BIT ; Transfer a set port bit into W addlw 1 btfsc LCD_DB5_BIT ; Transfer a set port bit into W addlw 2 btfsc LCD_DB6_BIT ; Transfer a set port bit into W addlw 4 btfsc LCD_DB7_BIT ; Transfer a set port bit into W addlw 8 bcf LCD_E_BIT ; And clear the Enable again. bcf LCD_RW_BIT ; Set Write mode for the LCD bsf STATUS,RP0 ; Select 2nd reg bank, now TRIS regs can be accessed bcf LCD_DB4_BIT ; Set the port as an output again bcf LCD_DB5_BIT ; bcf LCD_DB6_BIT ; bcf LCD_DB7_BIT ; bcf STATUS,RP0 ; Back at reg bank 0 return ; Returns with data in W ;---------------------------------------------------------------------- ; Delay routines (non-interrupt based, therefore not even close to reliable) ; W=10 gives ~ 1ms of delay ; 1ms=5000 instructions wasted, 100us=500 cycles ; Maximum time waited will be 256*100us=25.6ms DelayW movwf Dcount ; Set delay counter, number of 100us periods to wait DelayOuter movlw 0a5h ; This gives 165 iterations of the inner loop, wastes 495 cycles + these two + one more movwf Dcount2 ; exiting the loop + 3 more for the outer loop = 501 cycles for every Dcount DelayInner decfsz Dcount2,f ; 1 cycle (or two when exiting the loop) goto DelayInner ; 2 cycles decfsz Dcount,f ; Now decrement number of 100us periods and loop again goto DelayOuter return ;---------------------------------------------------------------- ; Data can be stored between 600 and 6ffh... org 600h ; Default text ; UnilinkID @ 13-14 ; UnilinkAttenuation @ 16-17 ; UnilinkSelected @ 28-29 ; UnilinkReInits @ 38,39 ; UnilinkCurID @ 54-55 ; DisplayStatus @ 62-63 ; BattVoltage @ 66,67,69,70 (thou,hund,tens,unit) DefaultText1 DT "----- WJ", "MyID:xx ", "xx dB at", "Sel:xx B", "Inits:xx" DT " Unilink", "CurID:xx", "t Dsp:xx", "atxx.xxV", " " PCWaitText0 ; DT ">Waiting" ; DT "Wait->PC" DT "Booting." PCWaitText1 ; DT " for PC<" ; DT " Booting" DT "..",0,0,0,0,0,0 LookUp movwf PCL ; Go to it (this assumes PCLATH == 06h) subtitl "Bootstrap/Bootloader code" page ;---------------------------------------------------------------------- ; Bootstrap code - Allows PIC to flash itself with data from the async port. ; Accepts a standard INHX8 encoded file as input, the only caveat is that the code is slow when writing to memory ; (we have to wait for the flash to complete), and therefore care has to be taken not to overflow the RS232 receiver ; (one good way of solving that is to wait for the echo from the PIC before sending anything else) ; Both program memory and Data EEPROM memory can be programmed, but due to hardware contraints the configuration ; register can't be programmed. That means that any references to the config register in the hex file will be ignored. ; ; Startup @9600bps ; RAM usage for the bootstrap code BootBits equ 7eh ; bit0 1=write 0=read, bit1 1=PGM 0=EE, bit2 0=normal 1=no-op when prog BootAddrL equ 7dh BootAddrH equ 7ch BootDataL equ 7bh BootDataH equ 7ah BootTimerL equ 79h BootTimerM equ 78h BootTimerH equ 77h BootNumBytes equ 76h BootDataVL equ 75h BootDataVH equ 74h BootHEXTemp equ 73h org 738h ; Place the boot code at the top of memory (currently the loader is exactly 200 bytes) Bootstrap bsf STATUS,RP0 ; Access bank 1 bsf TXSTA,TXEN ; Enable UART TX movlw 31 ; Divisor for 9k6 @ 20MHz Fosc movwf SPBRG ; Store bcf STATUS,RP0 ; Back to bank 0 bsf RCSTA,SPEN ; Enable serial port bsf RCSTA,CREN ; Enable UART RX movlw low BootStartText ; Send boot banner to the serial port call BootTXStr ; movlw 0e8h ; Initialize timeout timer (e8 is about 3 secs) movlw 0fdh ; Initialize timeout timer (fd is short enough to get the headunit to recognize us) movwf BootTimerL movwf BootTimerM movwf BootTimerH BootTimeout incf BootTimerL,f ; A 24-bit counter skpnz incf BootTimerM,f skpnz incf BootTimerH,f skpnz ; When overflowing here.. goto BootReturn ; ..Exit boot loader, no keypress within timeout period, resume program btfss PIR1,RCIF ; Wait for RX to complete goto BootTimeout call BootRXB xorlw 27 ; ESC skpz goto BootTimeout ; If it wasn't ESC, wait for another key BootFlash movlw low BootFlashText ; OK, flashing it is, send "start" text to serial port call BootTXStr bsf BootBits,1 clrf BootAddrL clrf BootAddrH BootLoop call BootRXB ; First find the ':' xorlw ':' skpz goto BootLoop ; Loop until we find it! call BootRXHEX ; Get one ASCII encoded byte (two chars) movwf BootNumBytes ; This is the number of bytes to be programmed on the line ; Maybe clear cary here? rrf BootNumBytes,f ; Right shift because we're double addressing this 8-bit format ; Note carry should be clear here as there cannot be odd number of bytes in this format call BootRXHEX ; Receive AddrH movwf BootAddrH call BootRXHEX ; Receive AddrL movwf BootAddrL rrf BootAddrH,f ; Fix the addressing again rrf BootAddrL,f bcf BootBits,2 ; Assume we should program bsf BootBits,1 ; And assume we should program flash not ee movf BootAddrH,w xorlw 020h ; Check if it's configuration, which we can't program skpnz ; Skip the bit set if it was false alarm bsf BootBits,2 ; No programming for this line xorlw 001h ; Also check if it's EEPROM memory (first xor 20h then 1 =21h) skpnz ; Skip the bit set instr if not EE data address bcf BootBits,1 ; We should program EE, will ignore the AddrH call BootRXHEX ; Receive Record Type (must be 0 for real records) skpz ; Check if zero goto BootFlashComplete BootLineLoop call BootRXHEX ; Receive low-byte of data word movwf BootDataVL call BootRXHEX ; Receive high-byte of data word movwf BootDataVH btfsc BootBits,2 ; Check whether this line should be programmed at all goto BootWriteSkip bcf BootBits,0 ; Read mode first, verify if we actually have to write call BootEE movf BootDataVL,w xorwf BootDataL,f ; Compare and destroy DataL movwf BootDataL ; Write new data to DataL skpz ; Skip if no difference, have to check high byte as well goto BootWrite ; Jump directly to write movf BootDataVH,w xorwf BootDataH,f ; Compare skpnz ; Skip if no difference, no programming necessary goto BootWriteSkip BootWrite movf BootDataVH,w movwf BootDataH ; Have to put the new H byte data in as well bsf BootBits,0 call BootEE ; Write directly into program mem ; Here a verify can take place, the read-back results are now in DataL/H BootWriteSkip incf BootAddrL,f ; Advance counter to next addr skpnz incf BootAddrH,f ; And add to high byte if needed decfsz BootNumBytes,f goto BootLineLoop goto BootLoop BootFlashComplete BootReturn movlw low BootRunText call BootTXStr bsf STATUS,RP0 ; Reg bank 1 BootReturnWait btfss TXSTA,TRMT ; Wait for last things to flush goto BootReturnWait bcf TXSTA,TXEN ; Disable UART TX bcf STATUS,RP0 ; Back to bank 0 bcf RCSTA,SPEN ; Disable serial port bcf RCSTA,CREN ; Disable UART RX return ; Return to code ;---------------------------------------------------------------------- ; BootTXB - Sends one byte to the UART, waits for transmitter to become ; free before sending BootTXB BootTXW1 btfss PIR1,TXIF ; Wait for TX to empty goto BootTXW1 movwf TXREG ; Send the byte return ;---------------------------------------------------------------------- ; BootTXStr - Sends ASCII string pointed to by W, zero terminated BootTXStr movwf BootAddrL ; Store LSB of text pointer movlw 07h ; MSB of pointer to the text (0700h in this boot loader) movwf BootAddrH movlw 02h ; Select "Read Program Memory" operation movwf BootBits BootTXStrLoop call BootEE ; Lookup char (actually two packed into one word) rlf BootDataL,w ; Shift the MSB out into carry (that's the 2nd char LSB) rlf BootDataH,w ; Shift it into 2nd char call BootTXB ; Send the high byte first movf BootDataL,w ; Get the low byte andlw 07fh ; Mask of the highest bit skpnz ; Stop if zero return call BootTXB ; Send char incf BootAddrL,f ; Increment pointer goto BootTXStrLoop ;---------------------------------------------------------------------- ; BootRXB - Receives one byte from the UART, waits if nothing available BootRXB BootRXW1 btfss PIR1,RCIF ; Wait for RX to complete goto BootRXW1 movf RCREG,w ; Get the recvd byte call BootTXB ; Echo to terminal return ;---------------------------------------------------------------------- ; BootRXHEXNibble - Receives one byte and converts it from ASCII HEX to binary BootRXHEXNibble call BootRXB ; Receive nibble ; This code is from piclist.com, really neat! addlw -'A' ; Convert from BCD to binary nibble skpc ; Test if if was 0-9 or A-F, skip if A-F addlw 'A' - 10 - '0' ; It was numeric '0' addlw 10 ; Add 10 (A get to be 0ah etc.) return ;---------------------------------------------------------------------- ; BootRXHEX - Receives two bytes from the UART, waits if nothing available ; Decodes the bytes as ASCII hex and returns a single byte in W BootRXHEX call BootRXHEXNibble movwf BootHEXTemp swapf BootHEXTemp,f ; Swap it up to the high nibble call BootRXHEXNibble addwf BootHEXTemp,w ; And add the two nibbles together return ;---------------------------------------------------------------------- ; BootEE - Reads or writes EE or Flash memory, BootBits specify the ; exact action to take. BootAddrL and BootAddrH has to be initialized ; to the address of choice (0000-003fh for EE and 0000h-07ffh for flash ; The data to be written has to be put in BootDataL and BootDataH, and ; data will be to the same place when read back BootEE bsf STATUS,RP1 ; Select bank 2 (RP0 must be 0) movf BootAddrH,w ; Load desired address movwf EEADRH movf BootAddrL,w movwf EEADR movf BootDataH,w ; And load the data (only used when writing) movwf EEDATH movf BootDataL,w movwf EEDATA bsf STATUS,RP0 ; Go to bank 3 bsf EECON1,EEPGD ; Point to Program Flash mem btfss BootBits,1 ; Test if that was correct or if we have to clear again bcf EECON1,EEPGD ; Point to EE DATA mem btfss BootBits,0 ; Check from read or write goto BootEERD ; Skip the WR if we were going for a read bsf EECON1,WREN ; Enable writes movlw 55h movwf EECON2 movlw 0AAh movwf EECON2 ; Unlock write operation bsf EECON1,WR ; And start a write cycle BootWRLoop btfsc EECON1,WR ; This executes for EE only not flash, waits for WR to finish goto BootWRLoop ; These two instructions gets NOPed when flashing bcf EECON1,WREN ; Finally disable writes again ; Here we read the data back again, can be used as verify BootEERD bsf EECON1,RD ; Start a read cycle nop ; Only necessary for flash read, same thing as when writing above nop ; Except I could use the two words for something useful there.. :) BootEEX bcf STATUS,RP0 ; Back to bank 2 movf EEDATA,w ; Store our EE-data movwf BootDataL movf EEDATH,w movwf BootDataH bcf STATUS,RP1 ; And finally back to bank 0 return ; To produce compact code the end zero byte has to be in the LSB (that means an even number of chars in every string) BootStartText DA "WJBoot - press ESC to flash\x00" BootFlashText DA "\r\nSend INHX8 file now...\r\x00" BootRunText DA "\r\nExiting loader\r\x00" ;---------------------------------------------------------------------- ; EE Data (64 bytes), located at 2100h org 2100h ; de 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh ; de 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh ; de 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh ; de 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh ; de 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh ; de 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh ; de 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh ; de 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh, 0ffh END