PIC Microcontoller Input / Output Memory Method

for I2C to EEPROM Memorys

by Mike Harrison [mike at whitewing.co.uk]

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: