For many years I've been irritated by the relatively large amount of code needed to talk to I2C EEPROMS, especially on c5x parts where stack depth is limited. I swore one day I'd try to do some highly optimised code. I finally got around to it, and 30-odd sheets of paper later, below is the result
It's primarily targetted at c5x applications, and was designed with the following criteria & limitations in mind (based on several real applicati ons) :
The following limitation of the current code is considered acceptable for the applications targeted : The number of bytes to read/write, and the RAM address to read/write are likely to be fixed at assemble time, so it doesn't matter if strange offsets etc. are required to these values, as this costs no code space, and can be handled easily with macros.
The code will read or write up to 14 bytes per call, although the write cache size of small eeproms will usually limit writes to 8 bytes or fewer. READ THE DATASHEET CAREFULLY to find the cache size for the device you are using, and be aware that this can vary between different manufacturers of the same part, and part revisions (e.g. Microchip 24LC01 vs. 24LC01B).
The current code body uses 68 words (including return). The code used to set-up addresses etc. is not counted as this will be different in different applications. Three words can be saved if 'fast mode' eeproms are used (e.g. 24LC01B at 5V). 4 registers are used,(plus FSR), one of which is for the eeprom address, which is preserved and can be eliminated if only one address will be used. No internal CALLs are used.
Explanatory notes - the following notes describe the more subtle aspects of the code - you don't need to understand them if you just want to use the code 'as is', but you will if you want to modify or optimise it further!
The code runs in two 'phases' - a write phase and a read phase, the latter only being used for read operations. Each phase begins with a start condition, followed by the control byte.
The variable 'cnt' holds two counts, one per nibble, stored as negative numbers, i.e. counting up to zero.
The flags byte is used as follows :
Using the same byte for flags and the bit count doesn't actually take any more code - the extra cycles to increment the count it by adding 20h are saved by not having to initialise the count - it's done when the flags are set up.
When SCL is set high, if SDA is tri-state (input or '1' output), the SDA output register bit may get set high (which would prevent SDA going low) by read-modify-write bit operations on SCL. This problem is avoided by clearing both SDA and SCL bits together with ANDWF. This will not cause SDA glitches, as the only time this clearing will change the SDA output register state is when SDA is tri-stated.
FSR is incremented on every byte - there's no point doing it conditionally as all that's needed to compensate is an assembly-time offset.
Note that in a few cases you will need to add a clrwdt somewhere inside the retry loop, depending on choice of eeprom, WDT prescale setting, and the delay between a write and any subsequent read or write attempt. The worst-case write time of a 24LC01B is 10mS, and the PIC's worst-case undivided watchdog period is 9mS.
Everyone's application is different, and so there is scope for further optimisation depending on particular requirements - here are a few suggestions :
If power consumption is an issue, you may want to add a delay to the retry loop to reduce the number of retries that will be attempted when the eeprom is busy writing.
A 10K pullup resistor is required from SDA to Vdd. No pullup is required on SCL as open-drain SCL drive is not required for eeprom applications.
Note that this code will not work as-is with the 12CE51x devices, and although it could be modified, it may not be especially optimal - the CE devices don't (all?) have a write cache, so the multi-byte write capability will not be useful in its current form.
;*********************************************************************** ; compact I2C eeprom access code V1.0 ; (C) Mike Harrison 1998 ; example code for PIC12C508 ;------------------------------------------------------- workspace cnt equ 08 ; byte count register flags equ 09 ; flags and bit count : read equ 0 ; 1 to read bytes, 0 to write addr equ 1 ; 0 to send eeprom address byte rden equ 2 ; 1 to enable read next cycle ; b5-7 used as bit counter temp equ 0a ; read/write data byte eeadr equ 0b ; eeprom address iiport equ gpio ; port address sclbit equ 4 ; SCL port pin (no pullup required) sdabit equ 5 ; SDA port pin (10K pullup to Vdd required) lotris equ b'00101' ; TRIS setting with SCL and SDA outputs, other bits as required by application hitris equ lotris+(1<<sdabit) ; calling examples : ;to read 3 bytes from eeprom addr 10..12 to registers 14..16 ; movlw 10 ; movwf eeadr ; readee 3,14 ;to write 5 bytes from registers 19..1d to eeprom address 0 ; clrf eeadr ; writeee 5,19 ;--------------------------------------------------- calling macros ; to simplify parameter set-up, you can call the code using the following macros readee macro bytes,address ; usage : readee <no. of bytes to read>, <RAM address to read to> movlw address-3 ; FSR offset due to unconditional increment movwf fsr movlw 0ef - bytes ; 2 writes (control, address) + n+1 reads (control,data) call do_iic endm writeee macro bytes,address ; usage : writeee <no. of bytes to write>, <RAM address to write from> movlw address-1 movwf fsr movlw 0e0 - (bytes <<4) ; n+2 writes (control,address,data), no reads call do_iic endm ;-----------------------------------------------------------do_iic do_iic ; read/write byte(s) to I2C EEPROM ; W & FSR to be setup as follows : ; read : EF - nbytes FSR = RAM address-1 ; write : E0 - (nbytes<<4) FSR = RAM address-3 ; eeadr holds eeprom address (preserved on exit) ; on exit, FSR points to the byte after the last one read/written ; nbytes can be up to 14, but eeprom write cache may limit this movwf cnt retry_iic clrf flags ; initialise flags and bit count phaseloop movlw hitris tris iiport ; ensure SDA high bsf iiport,sclbit ; SCL high bcf iiport,sdabit ; ensure SDA o/p reg low movlw lotris goto $+1 ; ensure Tsu:sta - can be omitted in fast mode tris iiport ; sda low - start condition movlw iiadr ; IIC control byte (write) btfsc flags,rden movlw iiadr+1 ; .. or read control byte if read pending movwf temp ; IIC control byte bcf iiport,sclbit ; scl low - code above ensures Thd:sta byteloop ; ; start of byte read/write section movlw lotris btfss flags,read ; set SDA high (tri-state) if reading btfsc temp,7 ; set SDA low only if writing and data bit = 0 movlw hitris ; (sda o/p register bit will be low) tris iiport goto $+1 ; wait set-up time bsf iiport,sclbit ; clock high (may set SDA o/p reg bit high) clc ; used later - done here for timing movlw 0ff^(1<<sclbit)^(1<<sdabit) ; mask to clear SDA and SCL, " " btfsc iiport,sdabit ; test SDA input for read sec andwf iiport ; SCL low, SDA o/p reg bit low rlf temp ; shift read data in or write data out movlw 020 addwf flags ; increment bitcount in b5-7 skpc goto byteloop ; do 8 bits movlw 0f0 xorwf cnt,w ; last byte of read ? Z set if so movlw lotris ; ack low if reading to send ack to eeprom skpz ; ..but no ack on last byte of read btfss flags,read ; movlw hitris ; ack high for write to test ack from eeprom tris iiport bsf iiport,sclbit ; clock high to get or send ack bit goto $+1 ; wait ack pull-up time movlw 0ff^(1<<sclbit)^(1<<sdabit) ; SDA/SCL low mask, done here to add delay skpz ; last byte of read - skip retry btfss iiport,sdabit ; read ack bit state goto no_retry_iic goto retry_iic ; retry if ack high (will be forced low on reads, except last byte) no_retry _iic andwf iiport ; set scl and sda o/p register bit low ;..................... end of byte read/write section movf temp,w btfsc flags,read movwf indf ; store data if reading movf indf,w ; get write data incf fsr ; increment RAM pointer btfss flags,addr movf eeadr,w ; load eeprom address if not disabled movwf temp ; byte to send next loop - address or data bsf flags,addr ; disable address flag btfsc flags,rden ; read mode pending? bsf flags,read ; set read mode movlw 010 addwf cnt ; increment byte counter in B4..7 skpnz goto done ; both nibbles zero - all done skpc ; c set if b7-4 now clear - write phase done goto byteloop bsf flags,rden ; set 'read pending' flag swapf cnt ; load byte counter with read byte count goto phaseloop ; do second phase of command done ; do stop condition movlw lotris ; (SDA o/p bit will be low) tris iiport ; set SDA low bsf iiport,sclbit ; scl high goto $+1 ; ensure Tsu:sto goto $+1 ; both these can be omitted for fast mode movlw hitris tris iiport ; sda high retlw 0 #include <pic.h> // demo of Mike's optimised IIC code for eeprom access under hitech C // (C) Mike Harrison 2001 email mike -at- whitewing.co.uk // Note that this doesn't seem to work when local optimisation is enabled, as it // appears to 'optimise' the asm code by simply ignoring it! Global optimisation seems to be OK. // this is intended for inclusion within C source, not as a seperate 'extern' module // It could be converted to the latter form easily, and this would avoid the above optimisation // problem. (If anyone does this please let me have a copy!) // see notes on original assembler version for more details. // this version works under hitech C V7.86, and was tested on a PIC16C74 // if anyone who knows C better can suggest any improvements to the C side of this please let me know! //asm macros #define c 0 #define z 2 #define skpnc btfsc _STATUS,c #define skpc btfss _STATUS,c #define sec bsf _STATUS,c #define clc bcf _STATUS,c #define skpz btfss _STATUS,z #define skpnz btfsc _STATUS,z #define tris dw 0x60+ #define rp0 5 // define indf - missing from hitech static volatile unsigned char INDF @ 0x00; // eeprom port bits (PORT A) #define sclbit 2 #define sdabit 3 #define ee_scl _PORTA,sclbit #define ee_sda _PORTA,sdabit #define hitris 0x1a // PORT A TRIS for SDA high #define lotris hitris-(1<<sdabit) #define iiport _PORTA #define iiadr 0x0a0 void do_iic(char cnt,char eeadr,char fsrval) // flags #define rden 2 #define addr 1 #define read 0 { char flags; char temp; // the following assignments seem to stop the compiler optimising // out the temp vars because it thinks they are unused. // This is required when global optimisation is enabled. // if anyone knows a better way (i.e. one that doesn't waste ROM) // of telling the compiler that these vars are used please let me know! // one alternative would be to use global temp vars for temp & flags flags=flags; eeadr=eeadr; cnt=cnt; temp=temp; FSR=fsrval; // pointer to read/write data #asm do_iic ; read/write byte(s) to I2C EEPROM ; W & FSR to be setup as follows : ; read : cnt=EF - nbytes FSR = RAM address-1 ; write : cnt=E0 - (nbytes<<4) FSR = RAM address-3 ; eeadr holds eeprom address (preserved on exit) ; on exit, FSR points to the byte after the last one read/written ; nbytes can be up to 14, but eeprom write cache may limit this retry_iic clrf _do_iic$flags ; initialise flags and bit count phaseloop movlw hitris tris iiport ; tris iiportensure SDA high bsf ee_scl ; SCL high bcf ee_sda ; ensure SDA o/p reg low movlw lotris goto $+1 ; ensure Tsu:sta - can be omitted in fast mode tris iiport ; sda low - start condition movlw iiadr ; IIC control byte (write) btfsc _do_iic$flags,rden movlw iiadr+1 ; .. or read control byte if read pending movwf _do_iic$temp ; IIC control byte bcf ee_scl ; scl low - code above ensures Thd:sta byteloop ; ; start of byte read/write section movlw lotris btfss _do_iic$flags,read ; set SDA high (tri-state) if reading btfsc _do_iic$temp,7 ; set SDA low only if writing and data bit = 0 movlw hitris ; (sda o/p register bit will be low) tris iiport goto $+1 ; wait set-up time bsf ee_scl ; clock high (may set SDA o/p reg bit high) clc ; used later - done here for timing movlw 0xff^(1<<sclbit)^(1<<sdabit) ; mask to clear SDA and SCL, " " btfsc ee_sda ; test SDA input for read sec andwf iiport ; SCL low, SDA o/p reg bit low rlf _do_iic$temp ; shift read data in or write data out movlw 0x20 addwf _do_iic$flags ; increment bitcount in b5-7 skpc goto byteloop ; do 8 bits movlw 0xf0 xorwf _do_iic$cnt,w ; =f0 (last byte of read), result used later movlw lotris ; ack low if reading to send ack to eeprom skpz ; no ack on read of last byte btfss _do_iic$flags,read movlw hitris ; ack high for write to test ack from eeprom, or on last byte read tris iiport bsf ee_scl ; clock high to get or send ack bit goto $+1 ; wait ack pull-up time movlw 0xff^(1<<sclbit)^(1<<sdabit) ; SDA/SCL low mask, done here to add delay skpz ; last byte of read - skip retry btfss ee_sda ; read ack bit state goto no_retry_iic ; no retry if ack low, or on last byte of read goto retry_iic ; retry if ack high (will be forced low on reads, except last byte) no_retry_iic andwf iiport ; set scl and sda o/p register bit low ;..................... end of byte read/write section movf _do_iic$temp,w btfsc _do_iic$flags,read movwf indf ; store data if reading movf indf,w ; get write data incf fsr ; increment RAM pointer btfss _do_iic$flags,addr movf _do_iic$eeadr,w ; load eeprom address if not disabled movwf _do_iic$temp ; byte to send next loop - address or data bsf _do_iic$flags,addr ; disable address flag btfsc _do_iic$flags,rden ; read mode pending? bsf _do_iic$flags,read ; set read mode movlw 0x10 addwf _do_iic$cnt ; increment byte counter in B4..7 skpnz goto done ; both nibbles zero - all done skpc ; c set if b7-4 now clear - write phase done goto byteloop bsf _do_iic$flags,rden ; set 'read pending' flag swapf _do_iic$cnt ; load byte counter with read byte count goto phaseloop ; do second phase of command done ; do stop condition movlw lotris ; (SDA o/p bit will be low) tris iiport ; set SDA low bsf ee_scl ; scl high goto $+1 ; ensure Tsu:sto goto $+1 ; both these can be omitted for fast mode movlw hitris tris iiport ; sda high #endasm } //eeprom read/write routines. If only called once it would be more efficient to use macros instead // note upper limit of 14 bytes that can be written/read at a time // and for writes, #bytes may be limited by eeprom cache size void write_ee(char eeaddress,char nbytes,char source) // call with write_ee(eeadr,#bytes,(char)&varname); { do_iic(0xe0-(nbytes<<4),eeaddress,source-1); } void read_ee(char eeaddress,char nbytes,char dest) // call with read_ee(eeadr,#bytes,(char)&varname); { do_iic(0xef-nbytes,eeaddress,dest-3); } main() { long test1=0x33221100; int test2=0x5544; char test3=0x66; char buffer[8]; TRISA=hitris; ADCON1=7; // disable adc OPTION=0b10110000; write_ee(0x10,4,(char)&test1); // write long test1 to ee address 0x10 // data will be written LSB first, i.e. 00,11,22,33 in addresses 10..13 write_ee(0x14,2,(char)&test2); // write int test2 to ee address 0x14 write_ee(0x16,1,(char)&test3); // write char test3 to ee address 0x16 read_ee(0x10,4,(char)&test1); // read long test1 from ee address 0x10 read_ee(0x14,2,(char)&test2); // read int test2 from ee address 0x14 read_ee(0x16,1,(char)&test3); // read char test3 from ee address 0x16 read_ee(0x10,8,(char)&buffer[0]); // read 8 bytes from ee addresses 0x10..17 to buffer[0..7] do asm("clrwdt"); while(1); }
Questions:
Archive:
See also:
See: