1

I am using an Arduino to read information from an EEPROM chip over I2C with the code I found here: https://playground.arduino.cc/Code/I2CEEPROM

It works great for int and char values but I also need to read short, long, and float values.

They are stored in this order: 1510543923
is stored as: 01011010 00001001 00010010 00110011

How can I read multiple bytes into a single variable of these types?

I tried the following but it gives me a 16 bit number:

i2c_eeprom_read_buffer(0x50, 25, (byte *)bytes, 4);
long j = (bytes[3] << 0) + (bytes[2] << 8) + (bytes[1] << 16) + (bytes[0] << 24);
2

4 Answers 4

3

1510543923 is stored as: 01011010 00001001 00010010 00110011

This is known as “big endian”, or “MSB first”, because the most significant byte (MSB, here 01011010) comes first.

I recommend against this order. If you can, swap all the bytes and store the value LSB first, i.e. with the least significant byte first, and the most significant byte last. This will make your life easier since it's the order used internally in the Arduino. If you can go little endian, then you will be able to read the data straight into the required variable, with no conversion needed, as in:

i2c_eeprom_read_buffer(0x50, 25, (byte *) &j, sizeof j);

and it will work identically with any data type.

If you cannot choose the byte order (maybe you cannot control what is inside the EEPROM), then you will have to reverse all the bytes. You can do this either with bit shifts or by explicitly moving bytes.

bytes[1] << 16

This will not work. Per the rules of the C++ language, bytes[1] is implicitly promoted to int type before the shift, then it is shifted as an int. But on your Arduino an int is 16-bits long, thus by shifting it 16 positions you are dropping off all of the bits. Actually, this is even worse: you are invoking what is known as undefined behavior, meaning that it is considered nonsense and the compiler is free to interpret however it wants.

To do this properly, you first have to explicitly cast bytes[i] to unsigned long. It has to be unsigned because changing the sign bit on a long by bit shifting is also undefined behavior. Thus, the proper way of reconstructing the number via bit shifts is:

long j = ((unsigned long) bytes[3] << 0)
       | ((unsigned long) bytes[2] << 8)
       | ((unsigned long) bytes[1] << 16)
       | ((unsigned long) bytes[0] << 24);

This is portable (it should also work on big-endian architectures) but only works with integral types, not with floats.

For reconstructing a float, my preferred option would be to use a union, in order to access the bytes of the variable explicitly. Then you just have to copy the bytes from the buffer to the union in reverse order:

union { float f; byte b[4]; } data;
data.b[0] = bytes[3];
data.b[1] = bytes[2];
data.b[2] = bytes[1];
data.b[3] = bytes[0];
float x = data.f;

One nice thing about this approach is that it works with any data type.

3
  • In reading the float, I correctly get 00110101-00111000-10101000-11001110 from the EEPROM but running it through the union snipit you show, gives 0.0. It should be 6.8790985E-7.
    – Alphy13
    Dec 7, 2017 at 3:19
  • @Alphy13: I am pretty sure you got the correct number. However, Serial.println(6.8790985e-7); prints “0.00”. Dec 7, 2017 at 8:28
  • Thanks for pointing that out. In case anyone has a similar issue: Serial.println(x, 10); gave the precision i needed to see a result.
    – Alphy13
    Dec 7, 2017 at 22:22
3

It gives you a 16-bit integer because the compiler automatically makes casts. In particular:

  • byte << int returns int
  • int + int returns int

So your (bytes[1] << 16) is converted to an int, and consequently gets zeroed.

I tried to simulate this code; the results are shown as comments

int a = 0x1234;
long b = a << 8;
long c = a << 8L;
long d = ((long)a) << 8;
Serial.println(a); // a is 0x1234
Serial.println(b); // b is 0x3400
Serial.println(c); // c is 0x3400
Serial.println(d); // d is 0x123400

Personally I'd bet that int << long would result in a long, but apparently I'm wrong.

In any case, you should write

long j= (bytes[3] << 0) + (bytes[2] << 8) + (((unsigned long)bytes[1]) << 16) + (((unsigned long)bytes[0]) << 24);

This should fix your error. In any case, you can also simply write

long write_value;
i2c_eeprom_write_page(0x50, 25, (byte *)&write_value, sizeof(write_value));
long read_value;
i2c_eeprom_read_buffer(0x50, 25, (byte *)&read_value, sizeof(read_value));

Please note that accessing directly the pointers, like in this case, will respect the endianness of the compiler. In the case of a big endian implementation, the long with value 0x12345678 will be saved as 12 34 56 78, while with a little endian implementation it will be saved as 78 56 34 12. This is not a problem if you always use the same platform, but if you are trying to transfer information over a network you will have to ensure that all the devices have the same endianness or manually correct it

2
  • 2
    Please note that (((long)bytes[0]) << 24) is undefined behavior if bytes[0] happens to be larger than 127. You should use unsigned long instead. Dec 6, 2017 at 9:40
  • 1
    @EdgarBonet totally agree, my bad. I fixed it in the answer. Thank you
    – frarugi87
    Dec 6, 2017 at 10:52
0

Using template functions this could be written as:

template<class T> void i2c_read(uint16_t addr, T& x)
{
  i2c_eeprom_read_buffer(0x50, addr, &x, sizeof(T));
}

template<class T> void i2c_write(uint16_t addr, T& x)
{
  i2c_eeprom_write_buffer(0x50, addr, &x, sizeof(T));
}

These two functions take the data type as template parameter. Some examples:

  long x = 0x12345;
  i2c_write<long>(0, x);

  long y = 0;
  i2c_read<long>(0, y);

With auto the functions could be written as:

void i2c_read(uint16_t addr, auto& x)
{
  i2c_eeprom_read_buffer(0x50, addr, &x, sizeof(x));
}

void i2c_write(uint16_t addr, auto& x)
{
  i2c_eeprom_write_buffer(0x50, addr, &x, sizeof(x));
}

With auto the compiler will determine the data type.

Cheers!

-1

How can I read multiple bytes into a single variable of these types?

use a pointer to read each byte of a multi-byte type and save it into eeprom;

then do the same in reading them back: use a pointer to re-assemble the data (in bytes).

sizeof() comes in handy here.

Edit: lots of wonderfully complex answers for something this simple. When I get sometime I will pin my approach down.

Btw, many i2c libraries allow blocked read or write. So take that into your coding consideration.

edit2:

so here is what I have, to demonstrate how pointers can be used to write / read data.

//simulated write to buffer
//write n bytes of data from *ptr to address starting with addr
void mem_write(char addr, char *ptr, char n) {
    do {buffer[addr++] = *ptr++;} while (n--);
}

//simulated read from buffer
void mem_read(char addr, char *ptr, char n) {
    do {*ptr++ = buffer[addr++];} while (n--);
}

in this particular example, mem_write() writes n bytes of data, from source pointer ptr, into buffer[] starting at address addr. the read does the same.

you can port in your i2c byte read / write routines here, or use block read/write routines for more efficiency.

to confirm that it works, I wrote the following:

void setup() {
    Serial.begin(9600);         //initialize serial transmission

    dat.x = 1; dat.z = 3.0;     //initialize dat
    dat_ptr = (uint8_t *)&dat;  //initialize pointers
    dat_ptr1= (uint8_t *)&dat1;

    //print serial
    Serial.print("line 0: size = "); Serial.print(sizeof dat); Serial.print(", x = "); Serial.print(dat.x); Serial.print(", z = "); Serial.println(dat.z);

    //round 1: similated eeprom write/read
    mem_write(0, dat_ptr, sizeof dat);                  //write to buffer
    dat1.x = 0; dat1.z = 0;                             //initialize dat1
    mem_read(0, dat_ptr1, sizeof dat1);                 //read from buffer
    //now, dat1 contains the same data as dat
    //print dat1 on serial -> it should read identical to line 0
    Serial.print("line 1: size = "); Serial.print(sizeof dat1); Serial.print(", x = "); Serial.print(dat1.x); Serial.print(", z = "); Serial.println(dat1.z);

    //round 2: similated eeprom write/read
    //slightly different way of doing the same
    dat.x +=1; dat.z +=10.0;                            //x=2, z=13.0
    mem_write(0, (char *)&dat, sizeof dat);             //write to buffer
    dat1.x = 0; dat1.z = 0;                             //initialize dat1
    mem_read(0, (char *)&dat1, sizeof dat1);            //read from buffer
    //now dat1.x = 1+1=2, dat1.z = 3.0+10.0 = 13.0
    //print dat1 on serial
    Serial.print("line 2: size = "); Serial.print(sizeof dat1); Serial.print(", x = "); Serial.print(dat1.x); Serial.print(", z = "); Serial.println(dat1.z);

}

it essentially writes one data element "dat" into the buffer, and read it back into "dat1"; both items are printed over serial to confirm that the write / read are successful.

then I incremented on dat, and wrote it to buffer and read it back to dat1. again, printed dat1 on the serial monitor to confirm that we have the right result.

here is what I got: enter image description here

so we have the expected result.

the approach here is fairly standard. Obviously, experts usually come up with far more complex ways of doing simple things, as shown earlier answers, :)

1
  • 1
    About your simple/complex dichotomy: Both @frarugi87's answer and mine cover both the simple case (same endianness on the EEPROM and on the CPU), and the case when you have to swap the bytes. Your answer is simpler because you only covered the simple case. Notice that I explicitly recommended using the same endianness, precisely because it makes things simpler. But the “experts” know that you don't always have the choice. Dec 7, 2017 at 8:49

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.