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.
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;
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.
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