Extreme and fast i2c/TWI functions

I needed i2c for a 24LC512 EEPROM for an emulator project I'm working on.

I got sick of the Wire library. It uses lots of flash/SRAM space, is slow and can't behave well using sequential R/W.

So I wrote my own low level routines for i2C access.

I don't think it uses more than 200bytes of flash. It can read out the entire 64Kb in one pass and write 64bytes per write access.

The wire libray doesn't even come close.

Anyway, here it is.

// This is the hardware and bitstate definitions for the i2c functions
// They are defined for PORTB on an ATmega
// If you use any other pins than PORTB3-5, change these to reflect the pins used
// It is written by Jan Ostman in 2014 but free for non commercial use

#define SCLoutput     asm volatile("sbi 0x04,4\n");  //define SCL as output PORTB bit4
#define SDAoutput     asm volatile("sbi 0x04,5\n");  //define SDA as output PORTB bit5
#define SDAinput      asm volatile("cbi 0x04,5\n");  //define SDA as input
#define setSCLhigh    asm volatile("sbi 0x05,4\n");  //set SCL high
#define setSCLlow     asm volatile("cbi 0x05,4\n");  //set SCL low
#define setSDAhigh    asm volatile("sbi 0x05,5\n");  //set SCL high
#define setSDAlow     asm volatile("cbi 0x05,5\n");  //set SCL low
#define powerPIN      asm volatile("sbi 0x04,3\n");  //set flash power pin as output 
#define powerON       asm volatile("sbi 0x05,3\n");  //Turn on flash power
#define powerOFF      asm volatile("cbi 0x05,3\n");  //Turn off flash power
#define delayHALF     delayMicroseconds(3);          //Constant delay for half bit time

// These are the basic low level i2c functions 

void initTWI() {  //Initialize the port pins to run i2c
    powerPIN   //Set the pin for i2c power as output
    powerON    //Turn on i2c power
    SCLoutput  //Set SCL as output
    SDAoutput  //Set SDA as output
    setSCLhigh  //Set SCL high     
    setSDAlow  //Set SDA low
    delayHALF  //Wait half bit time  
    setSDAhigh  //Set SDA high for stop condition
    delayMicroseconds(20);  //Wait for the starting TWI bus to settle  
}
  
void sendTWIstart() {  //Send TWI start condition    
    setSDAlow  //Set SDA low for start 
    delayHALF  //Wait half bit time 
    setSCLlow  //Set SCL low for first bit 
}

void sendTWIdata(char data) {  //Send a byte on the TWI bus
    SDAoutput  //Set SDA as output
    for (uint8_t shift=128;shift;shift>>=1) { //Shift out 8 bits
      setSDAlow  //Set SDA low for a 0 bit
     if (data&shift) setSDAhigh  //Set SDA high for a 1 bit
      delayHALF  //Wait half bit time
      setSCLhigh  //Set SCL low 
      delayHALF  //Wait half bit time
      setSCLlow  //Set SCL low 
    }
    setSDAlow  //Set SDA low for ACK 
    delayHALF  //Wait half bit time
    setSCLhigh  //Set SCL high to clock in ACK
    delayHALF   //Wait half bit time
    setSCLlow  //Set SCL low 
}
  
uint8_t getTWIdata() {   //Recieve a byte from the TWI bus
    uint8_t data=0;
    SDAinput  //Set SDA as input
    setSDAhigh  //Set SDA with pullup
    for (uint8_t shift=128;shift;shift>>=1) {  //Shift in 8 bits
      delayHALF //Wait half bit time 
      setSCLhigh  //Set SCL high 
      delayHALF //Wait half bit time
      if (PINB&32) data|=shift; //Get the bit  
      setSCLlow  //Set SCL low 
    }
    SDAoutput  //Set SDA as output
    setSDAlow  //Set SDA low for ACK
    delayHALF  //Wait half bit time
    setSCLhigh  //Set SCL high 
    delayHALF  //Wait half bit time
    setSCLlow  //Set SCL low
    return data;  //Return the byte
}
  
void sendTWIstop() {   //Send TWI stop condition
    SDAoutput  //Set SDA as output  
    setSDAlow  //Set SDA low for stop 
    setSCLhigh  //Set SCL high for stop 
    delayHALF   //Wait half bit time
    setSDAhigh  //Set SDA high for stop 
}

void setup() {
  initTWI();
  sendTWIstart();
  sendTWIdata(0xA0);  //Write 0xAA to location 0x0000 in a 24LC256/512 EEPROM
  sendTWIdata(0x00);
  sendTWIdata(0x00);
  sendTWIdata(0xAA);
  sendTWIstop();
  Serial.begin (9600);
}

void loop() {
  sendTWIstart();
  sendTWIdata(0xA0);
  sendTWIdata(0x00);
  sendTWIdata(0x00);
  sendTWIstop();
  sendTWIstart();
  sendTWIdata(0xA1);
  uint8_t temp=getTWIdata();
  sendTWIstop();
  Serial.print(temp,HEX);  //Print back the 0xAA byte
  Serial.println();
  
}

a) You can change the state of a pin much more quickly using the "PIN" register than with SBI/CBI :) (you have to know its current state, obviously, but that's not a problem in functions like your "sendTWIdata()"...)

b) All AVR chips have hardware for this, don't they? Even ATtinys.

c) You're not supposed to actively drive I2C lines HIGH, you're supposed to let the external pullup resistor do that. This is why I2C cocuments talk about "releasing" lines, not setting them HIGH.

The way to achieve this on an AVR chip is to toggle the port direction bit (DDRB) instead of the data bit (PORTB), ie. you switch between OUTPUT(LOW) and INPUT(NOPULLUP).

c) You're supposed to wait for SCL to go high every time you release it. I2C devices are allowed to hold the line low to slow you down if you're going too fast.

eg. On ATtinys the USI hardware can be configured to automatically hold the SCL line low whenever they detect a start condition, this gives you time to respond to it and prepare to receive data - the master is supposed to wait until you manually release the line.

Apart from that, not bad.

The TWI/USI hardware on the Mega/Tiny is made to behave well or comply.

I have cut a few corners in my routines like knowing that an ACK is active low and just made the port active low at the same time.

Why do I need to read an ACK if I know its true? Waste of time.

It will never be false or the hardware broke?

janost: Why do I need to read an ACK if I know its true?

Because you're comparing your library to another one that does it properly, that's why.

I needed the speed so I might aswell cut some corners.

I put the code up for others that want to cut corners.

Thanks for sharing! (bookmarked)

The wire libray doesn't even come close.

What is the speed of your implementation? Do you have measurements?

It would be nice to create a library from it (newWire) that has the same interface as (Wire) that could replace the existing one flawlessly.

What might be missing is disabling / enabling interrupts as that currently may disrupt the communication..

It can read out the entire 64Kb in one pass and write 64bytes per write access

Note for writing to 24LCxxx EEPROM there is a latency of 5 millis() per write operation. See datasheet As the pagesize = 64 that will be the max write in once, [the wire lib has internal a 32byte buffer which is limiting factor]

The speedlimit is on the chip. That cant be changed. Its 400KHz if the chip is.

At least my code isn't depent of any interrupts.

The second thing is the buffers. The wire library allocates 3 buffers, 32bytes each, and you can use 30 bytes and why 3 of them? I have no idea and its useless.

Most of us want to use the full buffer capability in the chip. For a 24lcxxx that's 64 bytes and the wire library does not support that.

Its ok if you want to access some sensor but not for memory.

I think that what I wrote is more useful for any kind of TWI device?

janost: I think that what I wrote is more useful for any kind of TWI device?

It probably works for addressing a single device with a guaranteed response time.

As a general purpose library? Definitely not. It breaks the I2C standard in multiples ways, both electronically and in the protocol - see above.

janost: The wire library allocates 3 buffers, 32bytes each, and you can use 30 bytes and why 3 of them? I have no idea and its useless.

Yes, buffering seems a bit pointless.

I'm definitely not saying the Arduino Wire library is optimal.