Interfacing a PIC with a Microchip 24LC65 8K X 8 EEPROM

copyright, Nicole Ambrose, Dept of Electrical Engineering,
Morgan State University, Baltimore, MD 21239, August 3, '97

Introduction.

This discussion deals with interfacing a Microchip 24LC65 serial EEPROM with a PIC.

A data sheet for the 24LC65 is available from Microchip and devices are available from Digikey for about $3.50.

The 24LC65 is organized as 8192 8-bit bytes. Thus, valid addresses range from 0000H - 7FFF.

Typical applications include using EEPROM to store constants or calibration data specific to a particular installation. A specific example is saving the 64-bit serial numbers associated with addressing the Dallas 1-wire devices such as the DS1820 digital thermometers.

Another application is to perform calculations using the EEPROM as a lookup table. For example, an 12 bit A/D value might be mapped into one of 4096 addresses which contains a two byte value corresponding to some kind of real world quantity. Dedicating an EEPROM to avoid performing such functions as sin(x) or ln(x) may seem extravagant, but the cost of this device is but $3.50.

An obvious application is data logging. Various parameters may be logged with a 4-channel A/D such as the PCF8591 and the results of a measurement sequence might be written to EEPROM for later retrieval such as download to a PC for evaluation using such tools as spreadsheet analyis.

Program 24LC65_1.ASM.

This program illustrates how to write a single data byte to an address location and how to read the content of an address.

The address byte consists of the manufacturer's asssigned group code of 1010, followed by the users setting of the A2, A1 and A0 leads, followed by the R/W bit. The R/W bit is 0 for writes and 1 for reads.

In routine RANDOM_WRITE, the address is passed in variables ADR_HI and ADR_LO and the data is passed in variable DAT_VAL. The sequence begins with the START command, followed by sending the I2C address byte with the R/W bit set to zero, followed the high and low bytes of the address in the EEPROM, followed by the single data byte. Finally, the STOP command is sent. Note that a 25 msec delay is included to provide for the time required to burn the value into the EEPROM.

In routine RANDOM_READ, the address is passed in variables ADR_HI and ADR_LO. The sequence bgins with the START command followed by the two I2C address bytes with the R/W bit cleared to 0 (write). Initially, I found this is a bit confusing as we are performing a read operation. However, one might think of it as "writing" the two address bytes to the EEPROM. After the two address bytes have been written, another START command is initiated without any STOP command, followed by the address byte with the R/W bit set to a one (read), followed by reading the data using the IN_BYTE routine. This byte is returned to the calling program in the W register.

; Program 24LC65_1.ASM
; 
; Illustrates how to write a byte to an address and read a byte from an
; an address.
;
; Program writes the value 6CH to location 0341H and then reads the 
; location
;
;    PIC16C84				24LC65
;
; RB7 (term 13) ------------------- SCL (term 6) ----- To Other
; RB6 (term 12) ------------------- SDA (term 5) ----- I2C Devices
;
; Note that the slave address is determined by A2 (term 3), A1
; (term 2) and A0 (term 1) on the 24LC65.  The above SCL and SDA leads
; may be multipled to eight group "1010" devices, each strapped for a 
; unique A2 A1 A0 setting.
;
; 10K pullup resistors to +5VDC are required on both signal leads.
;
; copyright, Nicole L. Ambrose, MSU, 14 July, '97

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

	CONSTANT SDA=6
	CONSTANT SCL=7

	CONSTANT VARS=0CH

DAT_VAL	EQU VARS+0
NUM_VAL EQU VARS+1
ADR_HI	EQU VARS+2
ADR_LO 	EQU VARS+3

DEV_ADR EQU VARS+4	; A2, A1, A0 

_N	EQU VARS+5	; used for I2C routines
O_BYTE	EQU VARS+6
I_BYTE	EQU VARS+7

LOOP1	EQU VARS+8	; timing
LOOP2	EQU VARS+9	; timing


	ORG 000H	        ;program code to start at 000H

	BSF STATUS, RP0		; RP1 = 0, RP0 = 1, BANK1
	CLRF TRISB		; make all PortB bits outputs
	BCF STATUS, RP0

	MOVLW 00H
	MOVWF DEV_ADR		; A2 A1 A0	

	MOVLW 03H        	; dummy up ADR_HI and ADR_LO
        MOVWF ADR_HI
        MOVLW 41H
        MOVWF ADR_LO
        MOVLW 6CH		; dummy data set to 6CH
        MOVWF DAT_VAL
        CALL RANDOM_WRITE	; write it	

	MOVLW .25		; 25 msec delay to allow for programming
				; EEPROM
        CALL DELAY_N_MS

        CALL RANDOM_READ	; now read it back	
        CALL DISPLAY

DONE:	GOTO DONE
; end main

RANDOM_WRITE:	; write DAT_VAL to 16-bit address ADR_HI and ADR_LO
	CALL START

	BCF STATUS, C		; send address byte
	RLF DEV_ADR, W
	IORLW 0A0H
        MOVWF O_BYTE
        CALL OUT_BYTE
        CALL NACK

        MOVF ADR_HI, W		; send high byte of address
        MOVWF O_BYTE
        CALL OUT_BYTE
        CALL NACK

        MOVF ADR_LO, W		; send low byte of address
        MOVWF O_BYTE
        CALL OUT_BYTE
        CALL NACK

        MOVF DAT_VAL, W		; send the actual data
        MOVWF O_BYTE
        CALL OUT_BYTE
        CALL NACK

        CALL STOP 
        RETURN

RANDOM_READ:	; reads data at location specified in ADR_HI & ADR_LO
		; returns result in W
	CALL START

	BCF STATUS, C		; send address byte - write
	RLF DEV_ADR, W
        IORLW 0A0H
        MOVWF O_BYTE
        CALL OUT_BYTE
        CALL NACK

        MOVF ADR_HI, W		; send high and low bytes of address
        MOVWF O_BYTE
        CALL OUT_BYTE
        CALL NACK

        MOVF ADR_LO, W
        MOVWF O_BYTE
        CALL OUT_BYTE
        CALL NACK

        CALL START		; note there is no STOP

        BCF STATUS, C
	RLF DEV_ADR, W
        IORLW 0A1H		; R/W set to one for read operation
        MOVWF O_BYTE
        CALL OUT_BYTE
        CALL NACK

        CALL IN_BYTE		; fetch the byte
        CALL NACK
        CALL STOP

	MOVF I_BYTE, W		; return the byte in W
        RETURN

DISPLAY:	; this is a dummy routine used as a convenient 
		; break point
	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:
	MOVLW .250	; 250 msec delay
DELAY_N_MS:
	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

Program 24LC65.ASM.

This program illustrates various tools that might be used in implementing a data logger.

A data buffer at variable location DAT_BUFF (18H) is used as a common interface point between modules. For example, the result of a measurment sequence might be written to the data buffer. The data may then be read from the data buffer and written to EEPROM. Data may later be retrieved from EEPROM and written to the data buffer for either display or serial transfer to a PC.

In this program, routine MAKE_MEAS_SEQ is used to dummy four values to the data buffer. This might be the result of four readings from a PCF8591 A/D converter, an eight byte serial number from a Dallas 1-wire device or a four byte counter value from a DS1602 elapsed time counter.

Routine SEQ_WRITE fetches each value from the data buffer and writes it to EEPROM. The address in the EEPROM is passed in variables ADR_HI and ADR_LO. A sequential write is the same command sequence as a write, except that after sending the EEPROM high and low addresses, the data bytes are send in sequence with a nack after each byte. Up to 64 bytes may be programmed in this manner and the bytes are stored sequentially beginning at the defined address.

Note that in sequentially writing bytes, a page boundary (64 bytes) must not be crossed.

Thus, when using the data buffer approach, it is important that each measurement consist of 1, 2, 4, 8, 16 or 32 bytes.

For example, when using a DS1621 (or DS1820 or DS1821) digital thermometer, each measurement might consist of T_C, the remaining count and the slope. As this is only three bytes, a page boundary will eventually be crossed when performing a sequential write. This can be avoided by dummying a fourth variable in the data buffer. Note, that even with this apparent "waste", a single 24LC65 has the ability to store over 2,000 4-byte data sample.

The SEQ_READ routine is similar to reading a single byte, except that multiple bytes are sequentially read, with the master sending an ACK to acknowledge the receipt of each byte.

In the program, this data is written to the data buffer.

It is then displayed on a serial LCD by reading from the data buffer.

; Program 24LC65_2.ASM
; 
; Illustrates how to sequentially write a series of bytes to serial
; EEPROM, beginning at a defined EEPROM address.  Also illustrates a 
; sequential read from EEPROM.
;
; Program calls MAKE_MEAS_SEQ which is a dummy routine to simulate
; the raw data obtained from a PCF8591, DS1621 or similar.  Raw data
; is written to the data location beginning at 18H.
;
; SEQ_WRITE then reads these values and writes them to the 24LC65 
; EEPROM.
;
; SEQ_READ then reads the data from the 24LC65 and writes it to the data
; locations beginning at 18H.  These values are then displayed.
;
; Doing this back to back is, of course, foolish.  But, the program is 
; intended to show how to transfer data from a measurement and save it to 
; EEPROM and how to read it.   
;
;    PIC16C84				24LC65
;
; RB7 (term 13) ------------------- SCL (term 6) ----- To Other
; RB6 (term 12) ------------------- SDA (term 5) ----- I2C Devices
;
; PORTA, Bit 1 (terminal 18) ------ TX ----------> to RX on Serial LCD
;
; Note that the slave address is determined by A2 (term 3), A1
; (term 2) and A0 (term 1) on the 24LC65.  The above SCL and SDA leads
; may be multipled to eight group "1010" devices, each strapped for a 
; unique A2 A1 A0 setting.
;
; 10K pullup resistors to +5VDC are required on both signal leads.
;
; Note that LCD_CTRL is included at the end of this program.
;
; copyright, Nicole L. Ambrose, MSU, 14 July, '97

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

	CONSTANT SDA=6
	CONSTANT SCL=7

	CONSTANT DATA_BUFF=18H	; measurements saved to 18, 19, 1A and 1B
	CONSTANT BUFF_SIZE=.4	; number of measurments

	CONSTANT VARS=0CH

N	EQU VARS+0
ADR_HI	EQU VARS+1
ADR_LO 	EQU VARS+2

DEV_ADR EQU VARS+3	; A2, A1, A0 

_N	EQU VARS+4	; used for I2C routines
O_BYTE	EQU VARS+5
I_BYTE	EQU VARS+6

LOOP1	EQU VARS+7	; timing
LOOP2	EQU VARS+8	; timing


	ORG 000H	        ;program code to start at 000H

	BSF STATUS, RP0		; RP1 = 0, RP0 = 1, BANK1
	CLRF TRISB		; make all PortB bits outputs
	BCF STATUS, RP0

	MOVLW 00H
	MOVWF DEV_ADR		; A2 A1 A0	
	
	CALL MAKE_MEAS_SEQ	; dummy some values to the data buffer

	MOVLW 03H        	; dummy up ADR_HI and ADR_LO
        MOVWF ADR_HI
        MOVLW 00H
        MOVWF ADR_LO

        CALL SEQ_WRITE		; write the values to the 24LC65

	MOVLW DATA_BUFF		; clear the data buffer just to be sure	
	MOVWF FSR		; the following seq read works
	MOVLW BUFF_SIZE
	MOVWF N
CLR:
	CLRF INDF		; clear each location in the data buffer
	INCF FSR, F
	DECFSZ N, F
	GOTO CLR

	MOVLW 03H        	; dummy up ADR_HI and ADR_LO
        MOVWF ADR_HI
        MOVLW 00H
        MOVWF ADR_LO
	
        CALL SEQ_READ		; now read data back from EEPROM
        CALL DISPLAY		; and display the values

DONE:	GOTO DONE
; end main

MAKE_MEAS_SEQ:			; dummy up the data buffer with 4 values
	MOVLW DATA_BUFF		; presummable this would be the result
	MOVWF FSR		; of a measurement process

	MOVLW 11H		; data is 11H, 22H, 33H, 44H
	MOVWF INDF

	INCF FSR, F
	MOVLW 22H
	MOVWF INDF

	INCF FSR, F
	MOVLW 33H
	MOVWF INDF

	INCF FSR, F
	MOVLW 44H
	MOVWF INDF

	RETURN

SEQ_WRITE:	; write DAT_VAL to 16-bit address ADR_HI and ADR_LO
	CALL START

	BCF STATUS, C		; send address byte
	RLF DEV_ADR, W
	IORLW 0A0H
        MOVWF O_BYTE
        CALL OUT_BYTE
        CALL NACK

        MOVF ADR_HI, W		; send high byte of address
        MOVWF O_BYTE
        CALL OUT_BYTE
        CALL NACK

        MOVF ADR_LO, W		; send low byte of address
        MOVWF O_BYTE
        CALL OUT_BYTE
        CALL NACK

	MOVLW DATA_BUFF		; now write BUFF_SIZE bytes to EEPROM
	MOVWF FSR		
	MOVLW BUFF_SIZE
	MOVWF N
SEQ_WRITE_1:
	MOVF INDF, W		; fetch the data byte from the buffer
	MOVWF O_BYTE		; and save it to EEPROM
        CALL OUT_BYTE
        CALL NACK

	MOVLW .25		; 25 msec delay for EEPROM to burn
	CALL DELAY_N_MS

	INCF FSR, F
	DECFSZ N, F
	GOTO SEQ_WRITE_1
      
        CALL STOP 
        RETURN

SEQ_READ:	; reads data bytes beginning at location specified 
		; in ADR_HI & ADR_LO and places in DATA_BUFF
		
	CALL START

	BCF STATUS, C		; send address byte - write
	RLF DEV_ADR, W
        IORLW 0A0H
        MOVWF O_BYTE
        CALL OUT_BYTE
        CALL NACK

        MOVF ADR_HI, W		; send high and low bytes of address
        MOVWF O_BYTE
        CALL OUT_BYTE
        CALL NACK

        MOVF ADR_HI, W
        MOVWF O_BYTE
        CALL OUT_BYTE
        CALL NACK

        CALL START		; note there is no STOP

        BCF STATUS, C
	RLF DEV_ADR, W
        IORLW 0A1H		; R/W set to one for read operation
        MOVWF O_BYTE
        CALL OUT_BYTE
        CALL NACK

	MOVLW DATA_BUFF		; now sequentially read bytes from EEPROM
	MOVWF FSR		
	MOVLW BUFF_SIZE
	MOVWF N
SEQ_READ_1:
	CALL IN_BYTE		; fetch each byte from EEPROM
	MOVF I_BYTE, W
	MOVWF INDF		; and save in data buffer
	INCF FSR, F
	DECFSZ N, F
	GOTO SEQ_READ_2		; not done
	GOTO SEQ_READ_3

SEQ_READ_2:
	CALL ACK		; if not done, send an ACK and continue
	GOTO SEQ_READ_1
SEQ_READ_3:
        CALL NACK		; if done, send a NACK
        CALL STOP
	
        RETURN

DISPLAY:	; display BUFF_SIZE bytes on Line 1 of serial LCD	
	CALL LCD_CLR
	CALL LCD_LINE1

	MOVLW DATA_BUFF		; now read from buffer and display
	MOVWF FSR		
	MOVLW BUFF_SIZE
	MOVWF N
DISPLAY_1:
	MOVF INDF, W		; fetch the data byte from the buffer
	CALL LCD_VAL		; display it
	MOVLW " "			
	CALL LCD_CHAR		; send two spaces
	CALL LCD_CHAR        
	INCF FSR, F
	DECFSZ N, F
	GOTO DISPLAY_1
      
	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
	MOVLW .250	; 250 msec delay
	MOVWF LOOP1
DELAY_N_MS:
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

#include <a:\lcd\lcd_ctrl.asm>

	END