Arduino "Wire" i2c not compatible with SMBus Block Read ... is it?

OK, I literally spent the last 4...6 hours tinkering with this low-level AVR code trying to understand why:

 聽Wire.beginTransmission(deviceaddress);
 聽Wire.send(command);
 聽Wire.endTransmission();
 聽Wire.requestFrom((uint8_t)deviceaddress,blockBufferLen);
 聽for (x = 0; x < blockBufferLen; x++ )
 聽 聽if (Wire.available()) blockBuffer[x] = Wire.receive();

... with the data in the device, "(4)NIMH" - 4 being the number of bytes following (SMBus spec) ... results in "(4)N(FF)(FF)"... or to be precise, "FF"'s all the way up 'til the buffer length, without cutting off.

Yuck. I've tried slowing the I2C bus speed, debugging it line by line, debugging the buffer transfers, debugging the register values, adding delays, you name it, I've tried it.

I noticed that the timing diagrams for i2c block read and SMBus block read don't exactly line up right... but yet, I can't find ANYWHERE online where anyone's ever even tried to do an SMBus block read with an Arduino.

Why, you might ask, would I need this functionality? Well... I'm using Wire (and Arduino's i2c controller) mostly for the occasional PC laptop battery hack. There's lots of amazing information in these packs that can be hacked using the Arduino i2c interface. And the block read is only used for a couple useless quirks; otherwise i2c overlays on SMBus pretty nicely; sending "commands" instead of "addresses" and doing reads the same way as an EEPROM. Take a look at this hacked-up sketch I wrote that interfaces with this old NiMH battery I'm trying to repair... (">>" indicates things I enter into the Serial Monitor window)

r0x9; (<-- {r}eceive word from command 0x9, semicolon-terminated strtoul())
Reading word using command: 9
Completed, received: 2E25 (hex), 11813 (dec) (<-- 11,813 mV; 11.813 volts)
r0xa;
Reading word using command: A
Completed, received: 3D9 (hex), 985 (dec) (<-- 985 mA; 0.985 A charge rate currently)

But here's where things get munged up...

Reading block using command: 22
Completed, received 17 bytes:
N每每每每每每每每每每每每每每每

... Nyyyyy, is right. It's supposed to read "NIMH" and end at 4 bytes (5, counting the byte count itself).

Is the i2c protocol hard-coded into the ATmega chip itself? There are just slight differences in handling sequential "ack"'s in order to extract a full sequence of bytes from an SMBus device, but I think the controller is sending a "refresh code" and knocking out the sequence. Problem is, there's no way to even retrieve those missing bytes without following protocol, since "bytes of a block" can't be directly addressed... one command of 11 characters is 0x20, the next command of 8 characters is 0x21. They just have to be read in bursts like this...

If I could get SMBus working properly, I could probably polish up this sketch and turn it into a much more useful "battery tool" :wink:

In this old forum entry: http://www.arduino.cc/cgi-bin/yabb2/YaBB.pl?num=1214872633 someone was having trouble with an SMBus device using the "Wire" library so they switched to an i2cmaster library and that worked. Perhaps that library will help with your problem.

Perfect! Works like a dream! Full control over ACK and NACK is exactly what I needed, and with the exception of a few "oops, didn't realize it didn't do that" - like turning on pull-ups and using i2c_start_wait() - it was a flawless conversion. Code blew up pretty big in size, though, to the tune of about 6.4kb, but meh, I'm not really going for efficiency :stuck_out_tongue:

This is such a sweet sight to see, coming from 2 wires poking into the connector of a laptop battery (and a 12v/2a switching power supply providing the manual "but assisted" charging):

Ready!
------
Mfg: OEM | Dev: - | Chem: NIMH
Date: 2/22/2000 | Cycles: 1
11.92v, 1.28 A, 87.89 F, 57%, 87 min remaining.
11.91v, 1.28 A, 87.89 F, 57%, 87 min remaining.
11.90v, 1.28 A, 87.89 F, 57%, 87 min remaining.
11.92v, 1.28 A, 87.89 F, 57%, 85 min remaining.
11.92v, 1.28 A, 87.89 F, 57%, 85 min remaining.
11.91v, 1.28 A, 87.89 F, 57%, 85 min remaining.
>>b0x20;
Reading block using command: 20
Completed, received 3 bytes:
OEM
11.92v, 1.28 A, 87.89 F, 57%, 82 min remaining.
11.92v, 1.28 A, 87.89 F, 57%, 82 min remaining.
11.91v, 1.28 A, 88.07 F, 57%, 82 min remaining.

Yep, it grabs the "block read" flawlessly! Through this little thing, I now have full control over all the functions of the battery as documented in the datasheet :smiley: I just send a read command with, e.g. "r0x0c;", for "read command 0x0C", and it confirms the interpreted input, sends the command, and returns the result. Woo!

Here's the full code, now that it's working pretty well... this is based on a bq2040 "Gas Gauge IC with SMBus Interface". By tweaking the command parameters (sorry, I didn't #define them), it's pretty easy to adapt it to other batteries... all batteries that I know of use this SMBus interface :slight_smile:

#include <i2cmaster.h>

#define readBufferLen 17
char readBuffer[readBufferLen];
uint8_t i2cBuffer[readBufferLen];
#define deviceaddress B00010110
uint8_t serialCommand, loopCount;
unsigned int serialData;

void setup() {
聽 i2c_init();
聽 PORTC = (1 << PORTC4) | (1 << PORTC5); //enable pullups
聽 Serial.begin(9600);
聽 Serial.println("Ready!");
聽 Serial.println("------");
聽 Serial.print("Mfg: ");
聽 i2c_smbus_read_block(0x20,i2cBuffer,readBufferLen);
聽 Serial.print((char*)i2cBuffer);
聽 Serial.print(" | Dev: ");
聽 i2c_smbus_read_block(0x21,i2cBuffer,readBufferLen);
聽 Serial.print((char*)i2cBuffer);
聽 Serial.print(" | Chem: ");
聽 i2c_smbus_read_block(0x22,i2cBuffer,readBufferLen);
聽 Serial.println((char*)i2cBuffer);
聽 serialData = i2c_smbus_read_word(0x1b);
聽 Serial.print("Date: ");
聽 Serial.print((uint8_t)(serialData >> 5) & B00001111,DEC);
聽 Serial.print('/');
聽 Serial.print((uint8_t)serialData & B00011111,DEC);
聽 Serial.print('/');
聽 Serial.print((serialData >> 9) + 1980,DEC);
聽 Serial.print(" | Cycles: ");
聽 Serial.println(i2c_smbus_read_word(0x17));
}

void loop() {
聽 if (loopCount > 10) {
聽 聽 // gather things
聽 聽 int currVoltage, currAmps, estPercent, currTemp, timeLeft;
聽 聽 currVoltage = i2c_smbus_read_word(0x9);
聽 聽 currAmps = i2c_smbus_read_word(0xA);
聽 聽 estPercent = i2c_smbus_read_word(0xD);
聽 聽 currTemp = i2c_smbus_read_word(0x8);
聽 聽 timeLeft = i2c_smbus_read_word(0x13);
聽 
聽 聽 Serial.print((float)currVoltage / 1000, 2);
聽 聽 Serial.print("v, ");
聽 聽 Serial.print((float)currAmps / 1000, 2);
聽 聽 Serial.print(" A, ");
聽 聽 Serial.print(((float)currTemp/10 - 273.15) * 1.8 + 32, 2);
聽 聽 Serial.print(" F, ");
聽 聽 Serial.print(estPercent,DEC);
聽 聽 Serial.print("%, ");
聽 聽 Serial.print(timeLeft,DEC);
聽 聽 Serial.println(" min remaining.");
聽 聽 loopCount = 0;
聽 }
聽 loopCount++;

聽 if (Serial.available()) {
聽 聽 delay(500);
聽 聽 uint8_t x = 0;
聽 聽 if (Serial.peek() == 'w') {
聽 聽 聽 // write
聽 聽 聽 Serial.read(); // get the state out of the buffer
聽 聽 聽 while (Serial.peek() != ';') {
聽 聽 聽 聽 readBuffer[x] = Serial.read();
聽 聽 聽 聽 x++;
聽 聽 聽 聽 if (x > readBufferLen) return;
聽 聽 聽 }
聽 聽 聽 readBuffer[x] = 0; // null the buffer
聽 聽 聽 Serial.read(); // get the semicolon out of there
聽 聽 聽 serialCommand = strtoul(readBuffer, NULL, 0);
聽 聽 聽 Serial.print("Writing word using command: ");
聽 聽 聽 Serial.print(serialCommand,HEX);
聽 聽 聽 x = 0;
聽 聽 聽 while (Serial.peek() != ';') {
聽 聽 聽 聽 readBuffer[x] = Serial.read();
聽 聽 聽 聽 x++;
聽 聽 聽 聽 if (x > readBufferLen) return;
聽 聽 聽 }
聽 聽 聽 readBuffer[x] = 0; // null the buffer
聽 聽 聽 Serial.read(); // clear the semicolon
聽 聽 聽 serialData = strtoul(readBuffer, NULL, 0);
聽 聽 聽 Serial.print(", data: ");
聽 聽 聽 Serial.println(serialData,HEX);
聽 聽 聽 i2c_smbus_write_word(serialCommand,serialData);
聽 聽 聽 Serial.println("Completed.");
聽 聽 聽 
聽 聽 } else if (Serial.peek() == 'r') {
聽 聽 聽 // read word
聽 聽 聽 Serial.read(); // get the state out of the buffer
聽 聽 聽 while (Serial.peek() != ';') {
聽 聽 聽 聽 readBuffer[x] = Serial.read();
聽 聽 聽 聽 x++;
聽 聽 聽 聽 if (x > readBufferLen) return;
聽 聽 聽 }
聽 聽 聽 readBuffer[x] = 0; // null the buffer
聽 聽 聽 Serial.read(); // clear the semicolon
聽 聽 聽 serialCommand = strtoul(readBuffer, NULL, 0);
聽 聽 聽 Serial.print("Reading word using command: ");
聽 聽 聽 Serial.println(serialCommand,HEX);
聽 聽 聽 serialData = i2c_smbus_read_word(serialCommand);
聽 聽 聽 Serial.print("Completed, received: ");
聽 聽 聽 Serial.print(serialData,HEX);
聽 聽 聽 Serial.print(" (hex), ");
聽 聽 聽 Serial.print(serialData,DEC);
聽 聽 聽 Serial.println(" (dec)");

聽 聽 } else if (Serial.peek() == 'b') {
聽 聽 聽 // read block
聽 聽 聽 Serial.read(); // get the state out of the buffer
聽 聽 聽 while (Serial.peek() != ';') {
聽 聽 聽 聽 readBuffer[x] = Serial.read();
聽 聽 聽 聽 x++;
聽 聽 聽 聽 if (x > readBufferLen) return;
聽 聽 聽 }
聽 聽 聽 Serial.read(); // clear the semicolon
聽 聽 聽 readBuffer[x] = 0;
聽 聽 聽 serialCommand = strtoul(readBuffer, NULL, 0);
聽 聽 聽 Serial.print("Reading block using command: ");
聽 聽 聽 Serial.println(serialCommand,HEX);
聽 聽 聽 x = i2c_smbus_read_block(serialCommand,i2cBuffer,readBufferLen);
聽 聽 聽 Serial.print("Completed, received ");
聽 聽 聽 Serial.print(x,DEC);
聽 聽 聽 Serial.println(" bytes:");
聽 聽 聽 x = 0;
聽 聽 聽 for (x=0; x<readBufferLen; x++) {
聽 聽 聽 聽 Serial.print(i2cBuffer[x]);
聽 聽 聽 }
聽 聽 聽 Serial.println();
聽 聽 } else {
聽 聽 聽 Serial.print("Junk: ");
聽 聽 聽 Serial.println(Serial.read(),HEX);
聽 聽 }
聽 }
聽 delay(500);
}

void i2c_smbus_write_word ( uint8_t command, unsigned int data ) {
聽 i2c_start_wait(deviceaddress + I2C_WRITE);
聽 i2c_write(command);
聽 i2c_write((uint8_t)data);
聽 i2c_write((uint8_t)(data>>8));
聽 i2c_stop();
聽 return;
}

unsigned int i2c_smbus_read_word ( uint8_t command ) {
聽 unsigned int buffer = 0;
聽 i2c_start_wait(deviceaddress + I2C_WRITE);
聽 i2c_write(command);
聽 i2c_rep_start(deviceaddress + I2C_READ);
聽 buffer = i2c_readAck();
聽 buffer += i2c_readNak() << 8;
聽 return buffer;
}

uint8_t i2c_smbus_read_block ( uint8_t command, uint8_t* blockBuffer, uint8_t blockBufferLen ) {
聽 uint8_t x = 0;
聽 uint8_t y = 0;
聽 i2c_start_wait(deviceaddress + I2C_WRITE);
聽 i2c_write(command);
聽 i2c_rep_start(deviceaddress + I2C_READ);
聽 y = i2c_readAck();
聽 for (x=0; x<y-1; x++) {
聽 聽 blockBuffer[x] = i2c_readAck();
聽 }
聽 blockBuffer[x] = i2c_readNak();
聽 blockBuffer[x+1] = 0;
聽 return y;
}

Also, it was mostly designed for my use, but since I had to ask for help in the forum (and got it, thanks!), I figured I'd return my work to the community :slight_smile:

FalconFour:
Also, it was mostly designed for my use, but since I had to ask for help in the forum (and got it, thanks!), I figured I'd return my work to the community :slight_smile:

Glad I could help and thanks for the code.

Hey FalconFour,

I'm trying to get battery monitor bq2060 working with arduino fio. I noticed there is huge similarity between my project and what you have done here (Relief!) Initially i started out with wire library but could not get my ic working. I have tried i2c sensors before so my coding wasnt a problem. While searching for alternative library, i found this page and i must thank you for making your code public.

I have few doubts and would appreciate your help. I downloaded i2c master library from here( http://homepage.hispeed.ch/peterfleury/avr-software.html) is this the one you're referring to? When i copy paste this folder in 'library' folder of arduino and import the library in arduino IDE , it isnt working.. Do you have any idea why this must be happening?

Priya.

The ic2master library won't work for me neither.

I have installed the library in Arduino.app/Contents/Resources/Java/Libraries/i2cmaster (I am on a Mac).
I have renamed twimaster.c as twimaster.cpp and test_i2cmaster.c as test_i2cmaster.cpp.
I have modified the F_CPU with value 16000000UL and the SCL_CLOCK with value 50000L in twimaster.cpp.
I have created a keywords file (see it at the bottom of this message) and put it within the library's folder.

However there is something wrong because the code doesn't run. Even my basic Serial.print() instructions are not executed.

What am I doing wrong ???

Here's the sketch:

/* 

聽 i2cmaster library test with an AD7746 
聽 
*/

#include <i2cmaster.h>

#define AD7746聽 0x48聽 聽 // device address of AD7746 without 8th bit
unsigned char ret;


void setup() {聽 聽 
聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽  
聽 聽  Serial.begin(9600);
聽 聽  Serial.println("Hello!"); // just to see if any code is executed
聽 聽  i2c_init();聽  // initialize I2C library
}
 
void loop() {

聽 聽  // read
聽 聽  i2c_start_wait(AD7746+I2C_WRITE);聽 聽  // set device address and write mode

聽 聽  i2c_write(0x00);聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 // read address = 0
聽 聽  i2c_rep_start(AD7746+I2C_READ);聽 聽 聽  // set device address and read mode

聽 聽  ret = i2c_readNak();聽 聽 聽 聽 // read one byte
聽 聽  i2c_stop();
聽 聽  Serial.print("RET = ");
聽 聽  Serial.println(ret);
 }
#######################################
# Syntax Coloring Map For i2cmaster
#######################################

#######################################
# Datatypes (KEYWORD1)
#######################################

#######################################
# Methods and Functions (KEYWORD2)
#######################################

i2c_init	KEYWORD2
i2c_start_wait	KEYWORD2
i2c_write	KEYWORD2
i2c_stop	KEYWORD2
i2c_rep_start	KEYWORD2
i2c_cursor	KEYWORD2
i2c_readNak	KEYWORD2
i2c_readAck	KEYWORD2
i2c_read	KEYWORD2

#######################################
# Constants (LITERAL1)
#######################################

I2C_WRITE	LITERAL1
I2C_READ	LITERAL1

if I remove the test_i2cmaster.cpp (previously test_i2cmaster.c) the code doesn't hang immediately but still does very soon.

Trough the insertion of Serial.print("dummy text") instructions, I could find where it hangs. It's just after this line:
ret = i2c_readNak();聽 聽 聽 聽 // read one byte

So, is it my code, the installation of the library or the library itself ?

It seems the point where the code hangs depends on the I2D device address used.

With address 0x00 the code hangs after this:

ret = i2c_readNak();聽 聽 聽 聽 // read one byte

With a random address it hangs after this:

i2c_write(0x00);聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 // read address = 0

And with the supposed correct address (0x90) almost all the code is executed excepted the last line:

i2c_stop();

Also, no value is returned by the I2C slave anyway (the ret variable remains without value).

#define AD7746聽 0x48聽 聽 // device address of AD7746 without 8th bit
...
i2c_start_wait(AD7746+I2C_WRITE);聽 聽  // set device address and write mode

That doesn't look right. According to the datasheet for the AD7746:

The start byte address for the AD7745/AD7746 is 0x90 for a write and 0x91 for a read.

But you are doing 0x48 for a write and 0x49 for a read.

i2c_start_wait does not appear to shift the address left 1 bit, and if it did, then when you use:

 i2c_rep_start(AD7746+I2C_READ)

... then the 1 which is I2C_READ would be doubled to make it 2, which would be wrong.

#define I2C_READ聽 聽 1

Nick Gammon wrote :

That doesn't look right. According to the datasheet for the AD7746:

The start byte address for the AD7745/AD7746 is 0x90 for a write and 0x91 for a read.

You're right but I have tried 0x48 as well as 0x90 because 0x48 = 0x90 >> 1 and I wasn't sure 0x90 included the read/write bit or not. Finally 0x90 seems indeed the right address because 0x48 hangs the program and 0x90 doesn't.

However the problem was indeed that I2C_WRITE and I2C_READ were not defined. I thought they were constants defined by the library hence I didn't define them myself.

Now the program doesnt' hang anymore but nothing seems to be read.

If I code like this

 聽 聽 ret = i2c_readNak(); 聽 聽 聽 聽// read one byte
 聽 聽 Serial.println(ret);

nothing appears on the terminal window

but if I convert the value supposed to be returned on the I2C bus into another format like this

 聽 聽 ret = i2c_readNak(); 聽 聽 聽 聽// read one byte
 聽 聽 Serial.println(ret, DEC);

the printed value is always 144 whatever the registrer address chosen.

Anyway, thanks for the help Nick !

Here is the complete sketch:

/* 

 聽i2cmaster library test with an AD7746 
 聽
*/

#include <i2cmaster.h>

#define AD7746 聽0x90 聽 聽// device address of AD7746
#define I2C_READ 0
#define I2C_WRITE 1
unsigned char ret = 0;


void setup() { 聽 聽
 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 
 聽 聽 Serial.begin(9600);
 聽 聽 Serial.println();
 聽 聽 i2c_init(); 聽 // initialize I2C library
}
 
void loop() {

 聽 聽 // read
 聽 聽 i2c_start_wait(AD7746+I2C_WRITE); 聽 聽 // set device address and write mode
 聽 聽 i2c_write(0x00); 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽// read address = 0
 聽 聽 i2c_rep_start(AD7746+I2C_READ); 聽 聽 聽 // set device address and read mode

 聽 聽 ret = i2c_readNak(); 聽 聽 聽 聽// read one byte
 聽 聽 Serial.print("RET = ");
 聽 聽 Serial.println(ret, DEC);
 聽 聽 i2c_stop();
 聽 聽 delay(500);
 }

You have:

#define I2C_READ 0
#define I2C_WRITE 1

However i2cmaster.h has:

/** defines the data direction (reading from I2C device) in i2c_start(),i2c_rep_start() */
#define I2C_READ聽 聽 1

/** defines the data direction (writing to I2C device) in i2c_start(),i2c_rep_start() */
#define I2C_WRITE聽  0

You have reversed the sense of read/write. You need to get this right, or you will have a lot of problems.

Oops !

OK, now it seems to work properly. Thanks Nick !

I am not sure about my reset command but it doesn't stop the program at least.

Still one strange thing: when I do not choose to read from address 0x00, the program hangs after one reading.

Also, it seems I cannot read bytes within a "for" loop. Does this kind of loop slow down the reading too much ?

/* 

聽 i2cmaster library test with an AD7746 
聽 
*/
//#include <avr/io.h>
#include <i2cmaster.h>

#define AD7746聽 0x90聽 聽 // device address of AD7746 without 8th bit
#define I2C_READ 1
#define I2C_WRITE 0
#define RESET_ADDRESS 0xBF
unsigned char ret[10] ;


void setup() {聽 聽 
聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽  
聽 聽  Serial.begin(9600);
聽 聽  Serial.println();
聽 聽  i2c_init();聽  // initialize I2C library
}
 
void loop() {
聽 聽 聽 
聽 聽  // Reset AD7746
聽 聽  Serial.println("Initializing");
聽 聽  i2c_start_wait(AD7746+I2C_WRITE);聽 聽  // set device address and write mode
聽 聽  i2c_write(RESET_ADDRESS);聽 聽 聽 聽 聽 聽  
聽 聽  i2c_stop();聽 // set stop conditon = release bus
聽 聽  delay (1); // let AD7746 a short time to reset (max 200 碌s)

聽 聽  // read
聽 聽  i2c_start_wait(AD7746+I2C_WRITE);聽  // set device address and write mode
聽 聽  i2c_write(0x00);聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 聽 // read address = 0
聽 聽  i2c_rep_start(AD7746+I2C_READ);聽 聽 聽  // set device address and read mode
聽 聽  ret[0] = i2c_readAck();聽 聽 聽 聽 // read one byte
聽 聽  ret[1] = i2c_readAck();聽 聽 聽 聽 // read one byte
聽 聽  ret[2] = i2c_readAck();聽 聽 聽 聽 // read one byte
聽 聽  ret[3] = i2c_readAck();聽 聽 聽 聽 // read one byte
聽 聽  ret[4] = i2c_readAck();聽 聽 聽 聽 // read one byte
聽 聽  ret[5] = i2c_readAck();聽 聽 聽 聽 // read one byte
聽 聽  ret[6] = i2c_readAck();聽 聽 聽 聽 // read one byte
聽 聽  ret[7] = i2c_readAck();聽 聽 聽 聽 // read one byte
聽 聽  ret[8] = i2c_readAck();聽 聽 聽 聽 // read one byte
聽 聽  ret[9] = i2c_readAck();聽 聽 聽 聽 // read one byte
聽 聽  ret[10] = i2c_readNak();聽 聽 聽 聽 // read one byte
聽 聽  for(int i = 0; i < 10 ;聽 i++) {聽 聽 聽 
聽 聽 聽  Serial.print("RET["+String(i)+"] : ");
聽 聽 聽  Serial.println(ret[i], DEC);
聽 聽  }
聽 聽  i2c_stop();
聽 聽  delay(500);
}

Not at all. Can you show the code with the for loop in it?

Well, now the reading of several bytes within a loop is functioning correctly. Now that I understand how to use the the i2cmaster library, I'll try to communicate with the Si1143 IC for light sensing.