Interfacing I2C Devices with a PIC16C84

(Interfacing with a Philips PCF8574 8-Bit I/O Expander)

copyright, Towanda Malone, Dept of Electrical Engineering
Morgan State University, Baltimore, MD, 21239, July 3, '97

Introduction.

The discussion focuses on the implementation of low level I2C routines which are common to interfacing with most I2C devices.

These routines are discussed in the context of interfacing with an Philips PCF8574 8-bit I/O device. The intent is to illustrate the use of the low level I2C routines. Other discussions focus on the Dallas DS1621 Digital Thermometer, Philips PCF8583 Real Time Clock, Philips PCF8591 4-channel A/D and single D/A and Microchip 24LC65 serial EEPROM. All of these use the same low level I2C routines presented in this discussion.

Throughout this discussion, please refer to program 8574_1.ASM which flashes an LED on 8574 I/O P0 when a switch on P7 is at a logic zero. When the switch is at a logic one, the LED is off. Clearly this is non too complex, but it is a simple platform to get across many points.

Note that as the purpose of the discussion is to help the reader with the low level I2C routines, the discussion begins with the lowest level routines and builds to higher level routines that were written specifically for the 8574.

Low Level Routines.

These are the building blocks for higher level functions.

HIGH_SDA. Causes a high impedance state on the SDA lead. This is accomplished by making the assigned bit an input by setting the appropriate bit on TRISB. Note that the required 10K pullup resistor on the SDA lead provides the required high impedance logic one to the slave device. This concept of a high impedance as a logic one as opposed to a hard TTL or CMOS logic one is important as it permits the slave device to pull the lead low when acknowldeging the receipt of a byte from the master.

LOW_SDA. Ouputs a logic zero on the SDA lead. The associated bit on PORTB is set to zero and the I/O lead is made an output by clearing the associated bit on TRISB.

HIGH_SCL and LOW_SCL. Same as HIGH_SDA and LOW_SDA, except the clock lead.

CLOCK_PULSE. Causes momentary logic one on SCL lead.

START. SDA lead is brought from high to low while SCL is high. This begins an exchange with a slave device.

STOP. SDA lead is brought from low to high while SCL is high. This concludes an exchange with a slave device.

OUT_BYTE. Serially sends the byte in o_byte, beginning with the most significant bit to the slave device. Note that SCL is normally low. In sending a bit, the SCA lead is brought to the appropriate state and then SCL is momentarily brought high using the CLOCK_PULSE routine. It is important that the SDA lead is set up prior to any transition on the SCL lead as a transistion on SDA while SCL is high would be interpretted by the slave as either a "start" or "stop" command.

This routine is implemented by rotating O_BYTE to the left causing the most significant bit to appear in the Carry bit of the STATUS register. This bit is then tested and either a logic zero or logic one is output. This is repeated for all eight bits.

IN_BYTE. Serially receives a byte from the slave, beginning with the most significant bit. The byte is returned to the calling program in variable I_BYTE. Throughout this routine, SDA is an input (which is really the same as an output logic one).

SCL is brought high and the SDA lead is then read and the Carry bit is either cleared or set. SCL is then brought low. I_BYTE is then shifted left, causing the Carry to appear in the least significant bit of I_BYTE. This is repeated eight times. Thus, the first bit which was received has been shifted into the most significant bit of I_BYTE.

NACK. SDA is brought high followed by a clock pulse. This routine is called after each transfer of a byte to the slave. During this time, the slave device pulls the SDA lead low to acknowledge receipt of the byte. In my implementation I do not verify this acknowledgment.

ACK. SDA is brought low followed by a clock pulse. When a slave device is sending more than a single byte to the master, it expects to receive an acknowledgement after each byte is received by the master. This only applies to multiple bytes transmitted by the slave. As multiple bytes are not transferred in interfacing with an 8574 I/O expander, there is no example of this in program 8574_1.ASM. Examples of the use of ACK appear in programs related to the PCF8591 A/D and Microchip 24LC65 where multiple bytes are received during the same exchange.

Timing.

PIC processors may be too fast for many I2C devices. Thus, in all of my routines, a 25 usec delay was added in subroutines HIGH_SDA, LOW_SDA, HIGH_SCL and LOW_SCL. This deserves further research as to how much delay, if any, is neccesary. My goal in preparing this material was to get the devices working and I have yet to return to timing questions.

The following discussion is devoted to higher level functions which were developed specifically for the 8574.

OUT_PATT. Causes the content of variable O_PATT to appear on the outputs of the 8574. This should not be confused with OUT_BYTE which is limited to transfering a byte on the I2C bus.

First, a START command is issued. This calls all devices on the I2C bus to attention.

This is followed by an address byte which consists of the manufacturer's assigned "group code" (4-bits), the specific three bit address assigned by the user using the A2, A1 and A0 terminals on the device and whether the operation is a Read or a Write (one bit).

For example, the manufacturer's assigned group code for the 8574 is 0100. Assume the user has strapped the A2, A1 and A0 terminals at 010, respectively. As OUT_PATT is a write operation the R/W bit is a zero.

Thus, the address byte is;

	0100 010 0

On receipt of this, all slave devices on the bus other than the addressed are inactive. All subsequent data, until another START command, is assummed to be for the addreesed device.

[This all reminds me of the Army style of instruction. The drill sargent asks the question which gets everyones' attention, much like the START command. All the students panic. I doubt the I2C devices do, but you get the point.

After a torturing pause, the sargent then looks at his roll sheet and calls on Lt Anderson, much like the address byte. Everyone else breathes a sigh of relief as for the next few minutes the dialog is with Anderson.]

This is followed by a NACK to permit the addressed device to acknowledge receipt of the byte. .

[Now for the dialog with Lt Anderson]. This is than followed by the data to be output on outputs of the 8574.

Then, another NACK.

The session closes with the STOP command.

For example, if the desired pattern to appear at the output of the 8574 is;

	0110 1011

the full sequence is;

	START 0100 0100 N 0110 1011 N STOP

	       address	   data

Thus, the implementation of OUT_PATT is to send the START command, followed by the address byte using the OUT_BYTE command, a Nack, then outputting O_PATT using OUT_BYTE, another Nack and finally STOP.

The astute reader might note that O_PATT is first logically ored with DIRS and the result is then output. The reason for this is discussed below.

IN_PATT. This subroutine reads the inputs on the 8574 and returns the result in I_PATT.

This begins with the START command followed by the address byte. The address byte is the same as for outputting except the R/W bit is set to logic one as this is a read operation. This is followed by a NACK.

The data is then read using the IN_BYTE subroutine. This is followed by a NACK from the master and the exchange is terminated with the STOP command.

From the example above;

	START  0100 0101 N RRRR RRRR N STOP
                       ^
		       note that logic one indicates a read operation

Where R, indicates a bit received by the master from the slave.

Outputs of 8574.

The outputs of the 8574 are open drain. That is, the output states are either a logic zero (close to ground) or a logic one (open). This is significant for two reasons.

When interfacing devices with the 8574, pullup resistors will be required if the interfacing logic does not interpret an open as a logic one.

The open drain configuration permits use of an I/O bit as an input. If the I/O bit is at a logic one, the output is a high impedance state which permits external signals to be forced on the terminal. However, if the I/O bit is at a logic zero, the output is at a hard ground and externally applied signals will be read as a logic zero. (This is analogous to the open collector bits on the Control Port associated with the PC Parallel Port).

Thus, when using a I/O bit on the 8574 as an input, it is important that the bit be set to a logic one. This is implemented in program 8574_1.ASM by using a variable DIRS which identifies the bits to be used as inputs. Bits which are to be used as inputs are set to a logic one. To assure the bits which are identified as inputs are consistently set to output logic ones, DIRS is logically ored with O_PATT in OUT_PATT.

Note that when reading from the 8574, the bits which are used as outputs will be read as the same state.

The Program.

In program 8574_1.ASM, the user assigned A2 A1 A0 address is copied into DEVICE_ADR. Bit 7 of the 8574 is identified as an input by setting bit 7 of DIRS to a 1.

The state of the switch on P7 is continually read using the IN_PATT routine. If the state is a logic one, the LED is turned off by setting P0 to a logic one. Otherwise, the LED on P0 is flashed by bringing P0 low for 250 msecs and then high for 250 msecs.


; 8574.ASM
;
; Illustrates control of 8574.  Flashes LED on P0 of 8574 if switch at
; P7 is at zero.
;
;    PIC16C84				PCF8574
;
; RB7 (term 13) ------------------- SCL (term 14) ----- To Other
; RB6 (term 12) ------------------- SDA (term 15) ----- I2C Devices
;
; Note that the slave address is determined by A2 (term 3), A1
; (term2) and A0 (term 1) on the 8574.  The above SCL and SDA leads
; may be multipled to eight devices, each strapped for a unique A2 
; A1 A0 setting.
;
; 10K pullup resistors to +5VDC are required on both signal leads.
;
; copyright, Towanda Malone, MSU, July 3, '97

	LIST p=16c84
#include <c:\mplab\p16c84.inc>	
	__CONFIG 11h

	CONSTANT SCL=7
	CONSTANT SDA=6		; bits on Portb defined

	CONSTANT VARS=0CH


DIRS	EQU VARS+0	; identifies input bits on 8574
DEVICE_ADR EQU VARS+1	; A2 A1 A0 address

O_PATT	EQU VARS+2	; byte to be output on 8574
I_PATT	EQU VARS+3	; input from 8574
O_BYTE	EQU VARS+4	; byte sent on I2C bus
I_BYTE	EQU VARS+5	; byte received on I2C bus
_N	EQU VARS+6	; index
LOOP1	EQU VARS+7	; timing
LOOP2	EQU VARS+8	; timing

	ORG 000H

MAIN:
	MOVLW 00H		; A2 A1 A0 address of 8574
	MOVWF DEVICE_ADR	
	MOVLW 80H		; define bit 7 of 8574 as an input
	MOVWF DIRS
	MOVLW 0FFH		; initialize all outputs to one
	MOVWF O_PATT
	CALL OUT_PATT
READ_SWITCH:
	CALL IN_PATT		; fetch from 8574
	BTFSS I_PATT, 7		; test switch on p7 of 8574
	GOTO FLASH		; flash the LED once

LED_OFF:
	MOVLW 01H		; otherwise, turn it off
	MOVWF O_PATT
	CALL OUT_PATT
	GOTO READ_SWITCH	; continue to read switch

FLASH:				; wink LED on for 250 ms and then off
	MOVLW 00H		; LED on
	MOVWF O_PATT
	CALL OUT_PATT
	CALL DELAY_LONG
	MOVLW 01H		; LED off
	MOVWF O_PATT
	CALL OUT_PATT
	CALL DELAY_LONG
	GOTO READ_SWITCH

; end of main

IN_PATT:  	; reads inputs on specified 8574.  returns result in
i_patt

	CALL START
	BCF STATUS, C		; clear carry
	RLF DEVICE_ADR, W	; left shift DEVICE_ADR, result to W
	IORLW 41H		
	MOVWF O_BYTE		; output 0100AAA1 for read
	CALL OUT_BYTE		;            210
	CALL NACK

	
	CALL IN_BYTE		; fetch the byte on i2c bus
	CALL NACK
	CALL STOP

	MOVF I_BYTE, W
	MOVWF I_PATT		; return result in i_patt
	RETURN
	
OUT_PATT: 			; outputs o_patt on addressed 8574

	CALL START
	BCF STATUS, C		; clear carry
	RLF DEVICE_ADR, W	; left shift DEVICE_ADR, result to W
	IORLW 40H		
	MOVWF O_BYTE		; output 0100AAA0 for write
	CALL OUT_BYTE		;	     210
	CALL NACK
	MOVF O_PATT, W
	IORWF DIRS, W
	MOVWF O_BYTE		; output o_patt | dirs
	CALL OUT_BYTE
	CALL NACK
	CALL STOP
	RETURN

; The following routines are low level I2C routines applicable to most
; interfaces with I2C devices.

IN_BYTE				; read byte on i2c bus
	CLRF I_BYTE
	MOVLW .8
	MOVWF _N		; set index to 8	
	CALL HIGH_SDA		; be sure SDA is configured as input
IN_BIT
	CALL HIGH_SCL		; clock high
	BTFSS PORTB, SDA	; test SDA bit
	GOTO IN_ZERO
	GOTO IN_ONE

IN_ZERO
	BCF STATUS, C		; clear any carry
	RLF I_BYTE, F		; i_byte = i_byte << 1 | 0
	GOTO CONT_IN

IN_ONE
	BCF STATUS, C		; clear any carry
	RLF I_BYTE, F		
	INCF I_BYTE, F		; i_byte = (i_byte << 1) | 1
	GOTO CONT_IN

CONT_IN
	CALL LOW_SCL		; bring clock low
	DECFSZ _N, F		; decrement index
	GOTO IN_BIT
	RETURN

;;;;;;

OUT_BYTE:			; send o_byte on I2C bus
	MOVLW .8
	MOVWF _N
OUT_BIT:
	BCF STATUS,C		; clear carry
	RLF O_BYTE, F		; left shift, most sig bit is now in carry
	BTFSS STATUS, C		; if one, send a one
	GOTO OUT_ZERO
	GOTO OUT_ONE

OUT_ZERO:
	CALL LOW_SDA		; SDA at zero
	CALL CLOCK_PULSE	
	CALL HIGH_SDA
	GOTO OUT_CONT

OUT_ONE:
	CALL HIGH_SDA		; SDA at logic one
	CALL CLOCK_PULSE
	GOTO OUT_CONT

OUT_CONT:
	DECFSZ _N, F		; decrement index
	GOTO OUT_BIT
	RETURN	

;;;;;;
		
NACK:				; bring SDA high and clock
	CALL HIGH_SDA
	CALL CLOCK_PULSE
	RETURN

ACK:
	CALL LOW_SDA
	CALL CLOCK_PULSE
	RETURN

START:				
	CALL LOW_SCL
	CALL HIGH_SDA
	CALL HIGH_SCL
	CALL LOW_SDA		; bring SDA low while SCL is high
	CALL LOW_SCL
	RETURN

STOP:
	CALL LOW_SCL
	CALL LOW_SDA
	CALL HIGH_SCL
	CALL HIGH_SDA		; bring SDA high while SCL is high
	CALL LOW_SCL
	RETURN

CLOCK_PULSE:			; SCL momentarily to logic one
	CALL HIGH_SCL
	CALL LOW_SCL
	RETURN		

HIGH_SDA:			; high impedance by making SDA an input
	BSF STATUS, RP0		; bank 1
	BSF TRISB, SDA		; make SDA pin an input
	BCF STATUS, RP0		; back to bank 0
	CALL DELAY_SHORT
	RETURN

LOW_SDA:
	BCF PORTB, SDA	
	BSF STATUS, RP0		; bank 1
	BCF TRISB, SDA		; make SDA pin an output
	BCF STATUS, RP0		; back to bank 0
	CALL DELAY_SHORT
	RETURN

HIGH_SCL:
	BSF STATUS, RP0		; bank 1
	BSF TRISB, SCL		; make SCL pin an input
	BCF STATUS, RP0		; back to bank 0
	CALL DELAY_SHORT
	RETURN

LOW_SCL:
	BCF PORTB, SCL
	BSF STATUS, RP0		; bank 1
	BCF TRISB, SCL		; make SCL pin an output
	BCF STATUS, RP0		; back to bank 0
	CALL DELAY_SHORT
	RETURN

DELAY_SHORT:			; provides nominal 25 usec delay
	MOVLW .5
	MOVWF LOOP2
DELAY_SHORT_1:
	NOP
	DECFSZ LOOP2, F
	GOTO DELAY_SHORT_1
	RETURN 	

DELAY_LONG:			; provide 250 ms delay
	MOVLW .250
	MOVWF LOOP1
OUTTER:
	MOVLW	.110		; close to 1.0 msec delay when set to .110
	MOVWF 	LOOP2
INNER:
	NOP
	NOP
	NOP
	NOP
	NOP
	NOP
	DECFSZ	LOOP2, F	; decrement and leave result in LOOP2 
				; skip next statement if zero
	GOTO INNER
	DECFSZ 	LOOP1, F
	GOTO OUTTER
	RETURN

	END