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