LowPowerLab Forum

Hardware support => Moteino => Topic started by: ChemE on September 17, 2016, 05:42:15 PM

Title: Lightweight I2C Library [SOLVED]
Post by: ChemE on September 17, 2016, 05:42:15 PM
EDIT: See my last post below.  My code is as expected much faster and smaller than Wire which lets me get back to sleep much faster.

I'm wondering if anyone has a better/faster/smaller library for I2C communications on a Moteino.  I've tried several times to get Peter Fleury's I2Cmaster working and I've even gotten it to compile...but when I upload any sketch that includes his header, my radio light stays solidly on and the whole sketch hangs.  The unholy union of Wire/Stream/twi does work but it is very bloated.  Interrupts and state machines are not at all my strong suit, so every time I sit down to write my own from the Atmel datasheets or pare down the working example I have, I get lost.
Title: Re: Lightweight I2C Library
Post by: TomWS on September 17, 2016, 06:58:06 PM
Quote from: ChemE on September 17, 2016, 05:42:15 PM
I'm wondering if anyone has a better/faster/smaller library for I2C communications on a Moteino.  I've tried several times to get Peter Fleury's I2Cmaster working and I've even gotten it to compile...but when I upload any sketch that includes his header, my radio light stays solidly on and the whole sketch hangs.  The unholy union of Wire/Stream/twi does work but it is very bloated.  Interrupts and state machines are not at all my strong suit, so every time I sit down to write my own from the Atmel datasheets or pare down the working example I have, I get lost.
Hmmmm, define 'very bloated'.  If I build a TH Mote with Si7021 & Wire support, the code size is ~20.6K.  If I build without Si7021 & Wire support the code size is ~18.2K.  If I build with the Wire library included, but not the Si7021 library (stealing a few lines of code from one of the Wire examples), the code size is ~19.6K, meaning the Wire library and 'fixings' is about 1.4K of code.   The Wire library may not be the most efficient, but it works...

Don't get me wrong, if there is a working, more efficient library it makes sense to use it, but, since TWI on AVR is not exactly performance efficient anyway, I'm not sure there is much room for improvement.

Tom
Title: Re: Lightweight I2C Library
Post by: ChemE on September 17, 2016, 07:07:44 PM
Well, my little sketch that grabs a temperature from my HTU21D and sends it to the UART (which I implement way smaller and simpler than Serial) compiles to 3.9k.  But still, it is just bit twiddling.  I can do the same thing using the OneWire protocol with a DS18B20 in only 800 bytes.
Title: Re: Lightweight I2C Library
Post by: ChemE on September 20, 2016, 09:21:57 PM
Guess no one but me has much/any interest in this.  I've made some progress and have working code which can get a temperature and humidity measurement from my HTU21D.  I need to polish it up a fair bit and add some functionality but at least it happily runs at 400kHz and probably will be significantly smaller than the Wire library.  This is master transmit and master receive only and I'm not planning on adding support for arbitration or multiple masters.
Title: Re: Lightweight I2C Library
Post by: ChemE on September 22, 2016, 02:45:35 PM
Got some more good progress done today and am getting very promising results that have serious battery longevity repercussions.  I've now implemented functions to adjust the resolution of the readings and am now able to spend just 300 microseconds talking to the sensor (could be a Si7021 or HTU21D) and 32 ms sleeping while the measurements occur.  Rather than the default 100kHz baud rate I have totally reliable communications at 800kHz and this bus speed difference easily shows up when clocking the whole exchange with micros().  Thus I can spend a fraction of time awake/active that the default library requires!  Setting TWBR to 2 is effectively what I'm doing.

I've also made small tweaks to the code which prevents infinite loops while the interrupt flag is set which does not rely on millis() and sped the whole exchange up another 10%.  I need to polish and compact the code more still but it now appears to be rock solid and blisteringly fast compared to Wire.
Title: Re: Lightweight I2C Library
Post by: ChemE on September 23, 2016, 04:02:17 PM
I'm declaring victory.  I will no doubt compress and polish this code a little more over the next weeks but I've made 99% of the progress that I will make.  Taking out the bloated serial code below and shoving the temp and RH into DDRD so the compiler cannot ignore my code results in a program size of 1,856 bytes and 10 bytes of SRAM.  This also executes dramatically faster than Wire which means I can get back to sleep much faster and save a lot of power.  This code spends 300 microseconds awake and bitbanging the TWI and 30 milliseconds in deep sleep.  The sensor is measuring for 7.5 of those 30 milliseconds.

My Tiny and Fast I2C Functions for HTU21D

#define BAUD_RATE                     800000ul
#define TRIGGER_TEMP_MEASURE_NOHOLD   0xF3
#define TRIGGER_HUMD_MEASURE_NOHOLD   0xF5
#define WRITE_USER_REGISTER           0xE6
#define ELEVEN_BIT_TEMP               B10000011
#define EIGHT_BIT_RH                  B00000011
#define address                       0x40
#define SLA_W                         (address << 1)
#define SLA_R                         ((address << 1) + 0x01)
#define TWI_STATUS                    (TWSR & 0xF8)

static inline void initTWI() {
  DDRC |= _BV(PC3) | _BV(PC2);
  PORTC |= _BV(PC2) | _BV(PC4) | _BV(PC5);
  TWBR = ((F_CPU / BAUD_RATE) - 16) / 2; 
}

static inline void issueCommand(uint8_t comm, uint8_t res) {
  uint16_t counter;
 
  TWCR = (1<<TWINT)|(1<<TWSTA)|(1<<TWEN); 
  while (!(TWCR & (1<<TWINT)) && ++counter){}
  TWDR = SLA_W;
  TWCR = (1<<TWINT) | (1<<TWEN);
  while (!(TWCR & (1<<TWINT)) && ++counter){}
  TWDR = comm;    // Send the command
  TWCR = (1<<TWINT) | (1<<TWEN);
  while (!(TWCR & (1<<TWINT)) && ++counter){}
  if(comm==WRITE_USER_REGISTER) {  // Send the new resolution
    TWDR = res;
    TWCR = (1<<TWINT) | (1<<TWEN);
    while (!(TWCR & (1<<TWINT)) && ++counter){}
  } else {    // Issue a stop on the I2C bus so we can enter sleep
    TWCR = (1<<TWINT)|(1<<TWEN)| (1<<TWSTO);
    while ((TWCR & (1<<TWSTO)) && ++counter);
  }
}

inline uint16_t readRaw(void) {
  uint8_t msb, lsb;
  uint16_t counter;
 
  do {  // Start + SLA(R) until we get an ACK
    TWCR = (1<<TWINT)|(1<<TWSTA)|(1<<TWEN);
    while (!(TWCR & (1<<TWINT)) && ++counter);
    TWDR = SLA_R;
    TWCR = (1<<TWINT) | (1<<TWEN);
    while (!(TWCR & (1<<TWINT)) && ++counter);
  } while (TWI_STATUS == 0x48 && ++counter);

  // Measurement is ready, read back 2 bytes
  TWCR = (1<<TWINT) | (1<<TWEA)| (1<<TWEN);    // Set the ACK bit to let the transmitter know we need another byte
  while (!(TWCR & (1<<TWINT)) && ++counter);
  msb = TWDR;
  TWCR = (1<<TWINT) | (1<<TWEN);    // Set the NACK bit to let the transmitter know we are done
  while (!(TWCR & (1<<TWINT)) && ++counter);
  lsb = TWDR;
  return((msb<<8|lsb)&0xFFFC);
}


Example Code Using I2C Functions

void setup() {
  Serial.begin(115200);
  initTWI();
}

void loop() {
  uint16_t startT, elapsed, rawHumd, rawTemp;
 
  // put your main code here, to run repeatedly:
  startT = micros();           // ==================== START THE CLOCK ====================
  issueCommand(WRITE_USER_REGISTER, ELEVEN_BIT_TEMP);
  issueCommand(TRIGGER_TEMP_MEASURE_NOHOLD,0);
  LowPower.powerDown(SLEEP_15MS, ADC_OFF, BOD_OFF);
  rawTemp = readRaw();
  float temp = (rawTemp * (316.296 / 65536.0) - 52.33);
 
  issueCommand(WRITE_USER_REGISTER, EIGHT_BIT_RH);
  issueCommand(TRIGGER_HUMD_MEASURE_NOHOLD,0);
  LowPower.powerDown(SLEEP_15MS, ADC_OFF, BOD_OFF); 
  rawHumd = readRaw();
  float humd = (rawHumd * (125.0 / 65536.0) - 6.0);
  elapsed = micros()-startT;  // ==================== STOP THE CLOCK ====================
 
  Serial.print("Temperature: ");
  Serial.print(temp,1);
  Serial.print("\tHumidity: ");
  Serial.print(humd,1);
  Serial.print("\tLoop took: ");
  Serial.print(elapsed);
  Serial.println(" microseconds");

  _delay_ms(5000);
}
Title: Re: Lightweight I2C Library [SOLVED]
Post by: ChemE on September 23, 2016, 04:12:34 PM
Taking out the floating point math and calls to Felix's LowPower drops the program size to 810 bytes and 9 bytes of SRAM.
Title: Re: Lightweight I2C Library [SOLVED]
Post by: captcha on September 23, 2016, 06:09:02 PM
oh, I like what you're doing. :-)

I've been running into large sketch sizes here and there and being able to shave off a few kB's sound like music to my ears.

I mainly use the library for communicating to a RTC (MCP79410) to get power consumption down (predictable length sleep times) and a custom 2x16 LCD I once made.

Looking forward to testing your library.