http://www.phanderson.com/PIC/16C84/8574_1.html
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

|