===== PIC 16F84 Master and 16F88 Slave ===== This was my first attempt at doing I2C. I decided to get 2 PIC micros talking to each other. The 16F84 was the first chips i started using, which was easy to do for the Master. The slave however was a totally different beast, bit banging methods didn't work. The PIC16F88 comes with an inbuilt SSP module that allows I2C communcation to be handled via the firmware. This makes life a lot easier when detecting start and finish commands. ==== The Plan ==== Simple enough: Have the 16F84 send I2C data to the 16F88 and have it prove that its received it. FOr a simple test, a number will be incremented on the master, sent via I2C to the salve. It will then use a LED to show the highest bit set. This will happen when the incremented number is greater then 0x80. When the number carries over to 0x00, it will be reset. To jump ahead a little, here is the finished product on the prototyping board: {{ :electronics:pic:breadboard_image.jpg?direct&500 | Prototype}} This image show the finished product. The 16F84 'master' is on the left, the 16F88 'slave' on the right. The green LED on the master will flash for the duration of a send command (At 4Mhz, its a solid). I use this as the trigger on the logic analyser to get better detail about whats happening. The orange LED on the slave will only be lit when the master's counter is larger then 0x80. ==== 'Master' ==== Creating the master was the simple part. I borrow heavily from [[ http://www.phanderson.com/PIC/16C84/8574_1.html | here ]]. This guy has created a great example of how to read and write from a port expander. This will be my next project when the port expander arrives from [[ http://littlebirdelectronics.com | Little Bird Electronics ]]. I mostly re-wrote all the code while referencing the original. The 'SendCount' label is the guts of the program, sending the currently held number over the I2C bus. Another note here is the line //#define I2C_SLAVE1 0x27//. I have defined the address as 0x27, but on the slave it will be left-shifted, so the address on the slave side must be //0x43//. I'll get around to addressing that eventually. Here is the code verbatim list p=16F84A include "p16F84a.inc" ;***** CONFIGURATION __CONFIG _PWRTE_ON & _HS_OSC & _WDT_OFF ; pin assignments #define SDA 0 ; Pin 0 #define SCL 1 ; Pin 1 #define TRIS_SDA TRISB,SDA #define TRIS_SCL TRISB,SCL #define I2C_SDA PORTB,SDA #define I2C_SCL PORTB,SCL #define LED PORTB,2 #define I2C_SLAVE1 0x27 CBLOCK 0CH counter _I byte_send i2c_addr i2c_ret ; I2C Return Value dly_loop1 ; Loops for delays dly_loop2 ENDC ORG 0 goto start ; jump over to main routine ;***** Initialisation start ; configure ports clrw ; configure PORTB and PORTC as all outputs tris PORTB clrf PORTB movlw b'00000000' ; Init our counter movwf counter ;***** Main loop main_loop bsf LED call SendCount bcf LED goto main_loop SendCount movlw I2C_SLAVE1 ; 7-bit address of dev bcf STATUS,C ; Use Write mode ('0') call I2C_Start ; Start a transmission call ReadAck ; Wait for Ack from slave movf i2c_ret ; Check return value btfss STATUS,Z ; Goto SendEnd on NACK goto SendErr movfw counter ; Copy byte to write (counter) to w call I2C_Write ; Write the byte to the line call ReadAck ; Should read byte from bus here ; checking that its a Nack call I2C_End ; End the packet bcf LED incf counter,f call Delay_Short return SendErr call I2C_End ;call Delay_Long ; Wait a while before resend return I2C_Start movwf i2c_addr ; Save the address in W rlf i2c_addr,f ; Attach '0' to 8th bit call Start ; Start the Trans movfw i2c_addr call I2C_Write ; Write byte to port return I2C_End call Stop return I2C_Write movwf byte_send movlw .8 ; 8 bits to send movwf _I _WriteBit rlf byte_send,f ; Move highest bit to C btfss STATUS,C ; If 'C' is clear call SDA_Low ; -> Set data low btfsc STATUS,C ; If 'C' is set call SDA_High ; -> Set data high call SCL_Pulse decfsz _I,f ; Decrement counter, if not clear goto _WriteBit ; -> Send next bit call SDA_Low ; Set low to allow slave to write return Nack ; Clock a high SDA call SDA_High call SCL_Pulse return Ack ; Clock a high SDA call SDA_Low call SCL_Pulse return ReadAck call SCL_High clrf i2c_ret btfsc I2C_SDA bsf i2c_ret,0 call SCL_Low return Start call SDA_High call SCL_High call SDA_Low call SCL_Low return Stop ; Bring SDA high while Clock high call SCL_Low call SDA_Low call SCL_High call SDA_High return SCL_Pulse call SCL_High call SCL_Low return SDA_High bsf STATUS,RP0 ; Bank 1 bsf TRIS_SDA ; Make SDA pin input bcf STATUS,RP0 ; Back to bank 0 call Delay_Short return SDA_Low bcf I2C_SDA bsf STATUS,RP0 bcf TRIS_SDA ; Make SDA pin output bcf STATUS,RP0 call Delay_Short return SCL_High bsf STATUS,RP0 ; Bank 1 bsf TRIS_SCL ; Make SDA pin input bcf STATUS,RP0 ; Back to bank 0 call Delay_Short return SCL_Low bcf I2C_SCL bsf STATUS,RP0 bcf TRIS_SCL ; Make SDA pin output bcf STATUS,RP0 call Delay_Short return Delay_Short ; 25us delay movlw .5 movwf dly_loop2 Delay_Short_1 nop decfsz dly_loop2,f goto Delay_Short_1 return Delay_Long movlw .250 ; 250ms delay movwf dly_loop1 Outer movlw .110 ; Close to 1ms when set to .110 movwf dly_loop2 Inner nop nop nop nop nop nop decfsz dly_loop2,f goto Inner decfsz dly_loop1,f goto Outer return END ==== 'Slave' ==== The slave was a completely different beast to work with. Most code originated [[ http://ww1.microchip.com/downloads/en/appnotes/00734b.pdf | here ]], but its fairly bug ridden. It took a few hours of scouring forums to find the correct fixes. The biggest issues with the code itself where ANSEL not being cleared (leaving the pins in analogue mode), and the SDA/SCL pins not being declared as input. I'm happy to say that the code written here is completely working! list p=16F88 #include ; Change to device that you are using. __CONFIG _CONFIG1, _CP_OFF & _CCP1_RB0 & _DEBUG_OFF & _WRT_PROTECT_OFF & _CPD_OFF & _LVP_OFF & _BODEN_OFF & _MCLR_ON & _PWRTE_ON & _WDT_ON & _HS_OSC ERRORLEVEL -302 ;--------------------------------------------------------------------- ;Constant Definitions ;--------------------------------------------------------------------- #define NODE_ADDR 0x4e ; I2C address of this node ; Change this value to address that ; you wish to use. ;--------------------------------------------------------------------- ; Buffer Length Definition ;--------------------------------------------------------------------- #define RX_BUF_LEN 8 ; Length of receive buffer ;--------------------------------------------------------------------- ; Variable declarations ;--------------------------------------------------------------------- udata_shr WREGsave res 1 udata STATUSsave res 1 FSRsave res 1 PCLATHsave res 1 Index res 1 ; Index to receive buffer Temp res 1 ; RXBuffer res RX_BUF_LEN ; Holds rec'd bytes from master ; device. ;--------------------------------------------------------------------- ; Vectors ;--------------------------------------------------------------------- STARTUP code 0x00 nop goto Startup ; nop ; 0x0002 nop ; 0x0003 goto ISR ; 0x0004 PROG code ;--------------------------------------------------------------------- ; Macros ;--------------------------------------------------------------------- memset macro Buf_addr,Value,Length movlw Length ; This macro loads a range of data memory movwf Temp ; with a specified value. The starting movlw Buf_addr ; address and number of bytes are also movwf FSR ; specified. SetNext movlw Value movwf INDF incf FSR,F decfsz Temp,F goto SetNext endm LFSR macro Address,Offset ; This macro loads the correct value movlw Address ; into the FSR given an initial data movwf FSR ; memory address and offset value. movf Offset,W addwf FSR,F endm ;--------------------------------------------------------------------- ; Main Code ;--------------------------------------------------------------------- Startup bcf STATUS,RP1 bsf STATUS,RP0 call Setup banksel WREGsave Main clrwdt ; Clear the watchdog timer. btfsc RXBuffer,7 bsf PORTB,2 btfss RXBuffer,7 bcf PORTB,2 goto Main ; Loop forever. ;--------------------------------------------------------------------- ; Interrupt Code ;--------------------------------------------------------------------- ISR movwf WREGsave ; Save WREG movf STATUS,W ; Get STATUS register banksel STATUSsave ; Switch banks, if needed. movwf STATUSsave ; Save the STATUS register movf PCLATH,W ; movwf PCLATHsave ; Save PCLATH movf FSR,W ; movwf FSRsave ; Save FSR banksel PIR1 btfss PIR1,SSPIF ; Is this a SSP interrupt? goto $ ; No, just trap here. bcf PIR1,SSPIF call SSP_Handler ; Yes, service SSP interrupt. banksel FSRsave movf FSRsave,W ; movwf FSR ; Restore FSR movf PCLATHsave,W; movwf PCLATH ; Restore PCLATH movf STATUSsave,W; movwf STATUS ; Restore STATUS swapf WREGsave,F ; swapf WREGsave,W ; Restore WREG retfie ; Return from interrupt. ;--------------------------------------------------------------------- Setup ; ; Initializes program variables and peripheral registers. ;--------------------------------------------------------------------- banksel PCON bsf PCON,NOT_POR bsf PCON,NOT_BOR banksel ANSEL movlw 0x00 movwf ANSEL banksel Index ; Clear various program variables clrf Index clrf PORTB clrf PIR1 banksel TRISB ;clrf TRISB bsf TRISB,4 bsf TRISB,6 bsf TRISB,1 bcf TRISB,2 movlw 0x36 ; Setup SSP module for 7-bit banksel SSPCON movwf SSPCON ; address, slave mode movlw NODE_ADDR banksel SSPADD movwf SSPADD clrf SSPSTAT banksel PIE1 ; Enable interrupts bsf PIE1,SSPIE bsf INTCON,PEIE ; Enable all peripheral interrupts bsf INTCON,GIE ; Enable global interrupts bcf STATUS,RP0 return ;--------------------------------------------------------------------- SSP_Handler ;--------------------------------------------------------------------- ; The I2C code below checks for 5 states: ;--------------------------------------------------------------------- ; State 1: I2C write operation, last byte was an address byte. ; SSPSTAT bits: S = 1, D_A = 0, R_W = 0, BF = 1 ; ; State 2: I2C write operation, last byte was a data byte. ; SSPSTAT bits: S = 1, D_A = 1, R_W = 0, BF = 1 ; ; State 3: I2C read operation, last byte was an address byte. ; SSPSTAT bits: S = 1, D_A = 0, R_W = 1 (see Appendix C for more information) ; ; State 4: I2C read operation, last byte was a data byte. ; SSPSTAT bits: S = 1, D_A = 1, R_W = 1, BF = 0 ; ; State 5: Slave I2C logic reset by NACK from master. ; SSPSTAT bits: S = 1, D_A = 1, BF = 0 (see Appendix C for more information) ; ; For convenience, WriteI2C and ReadI2C functions have been used. ;---------------------------------------------------------------------- banksel SSPSTAT movf SSPSTAT,W ; Get the value of SSPSTAT andlw b'00101101' ; Mask out unimportant bits in SSPSTAT. banksel Temp ; Put masked value in Temp movwf Temp ; for comparision checking. State1: ; Write operation, last byte was an movlw b'00001001' ; address, buffer is full. xorwf Temp,W ; btfss STATUS,Z ; Are we in State1? goto State2 ; No, check for next state..... memset RXBuffer,0,RX_BUF_LEN ; Clear the receive buffer. clrf Index ; Clear the buffer index. banksel SSPBUF ; Do a dummy read of the SSPBUF. movf SSPBUF,W return State2: ; Write operation, last byte was data, movlw b'00101001' ; buffer is full. xorwf Temp,W btfss STATUS,Z ; Are we in State2? goto State3 ; No, check for next state..... LFSR RXBuffer,Index ; Point to the buffer. banksel SSPBUF ; Get the byte from the SSP. movf SSPBUF,W movwf INDF ; Put it in the buffer. incf Index,F ; Increment the buffer pointer. movf Index,W ; Get the current buffer index. sublw RX_BUF_LEN ; Subtract the buffer length. btfsc STATUS,Z ; Has the index exceeded the buffer length? clrf Index ; Yes, clear the buffer index. return State3: ; Read operation, last byte was an address, movf Temp,W ; andlw b'00101100' ; Mask BF bit in SSPSTAT xorlw b'00001100' btfss STATUS,Z ; Are we in State3? goto State4 ; No, check for next state..... clrf Index ; Clear the buffer index. LFSR RXBuffer,Index ; Point to the buffer movf INDF,W ; Get the byte from buffer. call WriteI2C ; Write the byte to SSPBUF incf Index,F ; Increment the buffer index. return State4: ; Read operation, last byte was data, banksel SSPCON ; buffer is empty. btfsc SSPCON, CKP goto State5 movlw b'00101100' xorwf Temp,W btfss STATUS,Z ; Are we in State4? goto State5 ; No, check for next state.... movf Index,W ; Get the current buffer index. sublw RX_BUF_LEN ; Subtract the buffer length. btfsc STATUS,Z ; Has the index exceeded the buffer length? clrf Index ; Yes, clear the buffer index. LFSR RXBuffer,Index ; Point to the buffer movf INDF,W ; Get the byte call WriteI2C ; Write to SSPBUF incf Index,F ; Increment the buffer index. return State5: movf Temp,W ; NACK received when sending data to the master andlw b'00101000' ; Mask RW bit in SSPSTAT xorlw b'00101000' ; btfss STATUS,Z ; goto I2CErr ; return ; If we aren’t in State5, then something is ; wrong. I2CErr nop banksel PORTB ; Something went wrong! Set LED bsf PORTB,2 ; and loop forever. WDT will reset goto $ ; device, if enabled. return ;--------------------------------------------------------------------- ; WriteI2C ;--------------------------------------------------------------------- WriteI2C banksel SSPSTAT btfsc SSPSTAT,BF ; Is the buffer full? goto WriteI2C ; Yes, keep waiting. banksel SSPCON ; No, continue. DoI2CWrite bcf SSPCON,WCOL ; Clear the WCOL flag. movwf SSPBUF ; Write the byte in WREG btfsc SSPCON,WCOL ; Was there a write collision? goto DoI2CWrite bsf SSPCON,CKP ; Release the clock. return end ==== Circuit ==== The test circuit here uses just a handful of components. The 16F88 could probably just use its own Internal 4MHz oscillator, but i havn't got around to testing that yet. Also waiting on my 10 & 20MHz crystals to arrive so i can test out higher speeds! The stabilising capacitors are really needed, but i've got a whole draw full of 29pF ceramics just sitting there so added them anyway. I also need to research if one crystal can be shared between two micro's. As every I2C forum will tell you, ensure the pull-up resistors are installed on the SDA and SCL lines. I'm using 2.2K 1/4W resistors in the picture, but 4.7K - 10K will probably work just as well. The diodes are there for when programming the chips using the PICKit 3. Its a fairly easy matter to run a few jump leads from the PICKit to the chips one at a time. The PICKit was even able to power the circuit while testing. This meant that I would program one chip, then swap 3 leads to the other one to program it. {{ :electronics:pic:pickit_iscp.jpg?direct&500 |}} Here is the schematic for the circuit, simple enough. Click for larger version {{ :electronics:pic:schematic.png?direct&500 |}} ==== Testing ==== Getting the timings right were critical to ensuring a good transmission. To debug it, nothing is better then using a [[ http://www.bitscope.com/ | BitScope ]]. The picture below shows a couple of packets of I2C transmission, and the analyses of the data on the right hand side {{ :electronics:pic:i2c_bus_logic00.png?direct&500 | BitScope Logic output}}