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);
-
By treating the variable as a sequence of bytes.Ignacio Vazquez-Abrams– Ignacio Vazquez-Abrams2017年12月06日 02:02:13 +00:00Commented Dec 6, 2017 at 2:02
-
Or use a template function. You could modify this for I2C EEPROM playground.arduino.cc/Code/EEPROMWriteAnythingMikael Patel– Mikael Patel2017年12月06日 07:05:12 +00:00Commented Dec 6, 2017 at 7:05
4 Answers 4
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.
-
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– Alphy132017年12月07日 03:19:08 +00:00Commented 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".Edgar Bonet– Edgar Bonet2017年12月07日 08:28:50 +00:00Commented 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– Alphy132017年12月07日 22:22:27 +00:00Commented Dec 7, 2017 at 22:22
It gives you a 16-bit integer because the compiler automatically makes casts. In particular:
byte << int
returnsint
int + int
returnsint
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
-
2Please note that
(((long)bytes[0]) << 24)
is undefined behavior ifbytes[0]
happens to be larger than 127. You should useunsigned long
instead.Edgar Bonet– Edgar Bonet2017年12月06日 09:40:44 +00:00Commented Dec 6, 2017 at 9:40 -
1@EdgarBonet totally agree, my bad. I fixed it in the answer. Thank youfrarugi87– frarugi872017年12月06日 10:52:03 +00:00Commented Dec 6, 2017 at 10:52
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!
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, :)
-
1About 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.Edgar Bonet– Edgar Bonet2017年12月07日 08:49:27 +00:00Commented Dec 7, 2017 at 8:49