C – Access Other Data Types As Byte Array
I’m working on an Arduino project. Yes, I know I complained about them before, but that’s before I found out about the Espressif ESP32 and it’s smaller cousin, the ESP12 / ESP8266. These have built-in WiFi and Bluetooth, and have all sorts of cool features.
So, if you’ve visited this site in the last couple of years, you may have noticed that in the upper-right corner of the page, there is a widget called “Temperature at Casa de Parr”. This runs on a Raspberry Pi Zero W (like the Gate Controller). There is a DS18B20 temperature probe attached to the Pi, which is in a weather-proof case, and a second one connected via a wire, which sits just below the water level inside the pool.
At the time, the Pi was WAY overkill for this type of application, but for about $30 and some coding, I had a WiFi-accessible thermometer – very cool.
Well, I’ve been working on re-creating this using an ESP12, which is about $4, and I can use the same DS18B20 temperature probes.
Although the DS18B20 temperature probes are very consistent (precise), they are not very accurate, and therefore there is a need to store a calibration factor – basically an offset that you can add or subtract to the measured temperature, which may be two or three or even four degrees off, in order to calibrate it to the “real” temperature.
Of course, this assumes that the error is a constant at all temperature ranges – we could talk about two-point (linear-gradient) or three-point (curved) calibration, or even use an n-point bezier curve, which would be much more accurate, but for my needs, a constant is probably sufficient.
Unlike the Pi, which uses an SD card as its file system, the ESP can’t read or write any files unless you attach an SD card reader and write some code to read and write the files. Instead, they are designed to have a single, static program that gets stored in flash memory, which loads and runs automatically at boot. In addition, it has 512 bytes of EEPROM that’s accessible to the user, to store things like state or settings – 1/2 KB isn’t very much storage.
So to store my calibration factor, I need to write a float to EEPROM, and the method for writing to EEPROM is to write one byte at a time. A float is 4 bytes, and therefore I have to perform 4 write operations to 4 separate EEPROM addresses.
In order to accomplish this, I need to access my 4-byte float as a byte array. The easiest way to do this is to create a casted pointer, and I thought the concept behind this was interesting enough that you, dear reader, might be interested in it as well.
float f; char *c=(char *)&f; for(int i=0;i<4;i++) EEPROM.write(iBaseAddress+i,c[i]);
How This Works
The “char” (or byte) data type is one byte – normally, you would use an array of char as a string:
char myString[]="Hi There!";
At this point, you can access the individual bytes of myString as elements in the array:
myString[3]=='T'; //true
In addition, I can create a char pointer to my string, and access the pointer as if it was my original string:
char *c=&myString; //& means "Address of"
char *c=myString; //The compiler knows what you're trying to accomplish
c[3]=='T'; //true
Using the address operator (&) we can assign the pointer c to the address of our string &myString. However, if you just assign a pointer to a string, most compilers know what you’re trying to accomplish, and just directly assigning it results in the same thing – the pointer c holds the address of myString.
If we try to do this with our float, we get a type conversion error:
float f=3.14;
char *c=f; //Error: Can't convert float to char
char *c=&f; //Error: Can't convert float to char
However, if we cast our float as a char, we are telling the compiler to treat the variable f (a 4-byte float) as if it was a char. Using casting with the address operator returns a pointer to a char, which really points to our float:
char *c=(char *)&f;
Now, c[0] through c[3] are the four bytes of our float.
Conclusion
I wrote this out within my code, and I got to thinking about it – I don’t do very much c coding these days, and I should probably double check. I found NO GOOD answers, and the top answer seemed to be to use a union:
union { float f; char c[4]; } fl; fl.f=3.14; for(int i=0;i<4;i++) EEPROM.write(iBaseAddress+i,fl.c[i]);
This works, and I could certainly create a union type:
union fltype { float f; char c[4]; } union fltype fl;
This is certainly more readable, but the only time I need to read a float as a byte array is when reading or writing to EEPROM. Using type casting just seems like a cleaner solution.