Storing a float as a byte

I am using an NTC to read temperature. I use the Steinhart-Hart formula to calculate the temperature from the Resistance of the NTC. The output of that formula is a float (double integer, 4 bytes).
I also want to log the results in memory. For that I have a couple of choices
Store the floating point variable : that will cost me 32 bits of memory for every temperature value logged
Store the original NTC reading and just pull that through the Steinhart-Hart formula again when needed, that will cost me 16 bits for every temperature logged.
As I really don’t need the high decimal precision that a floating point value can give and as I really didn’t expect my temperature range to go outside -15 to +30 degrees Celcius, both 16 bits and 32 bits seemed serious overkill.

So I was wondering if I just couldn’t store it in in 1 Byte. As the Steinhart-Hart formula gives an approximation of the temperature, one can wonder how precise of a reading one needs, but I decided I wanted a solution of 0.25 degrees, though no doubt 0.5 degrees would have been enough already.
I figured I would use the MSB for the sign bit, that left 7 bits
decimal number 31 is equal to 0b00011111, so to cover my range I needed 5 bits for the temperature that left 2 bits
In those two bits I can store numbers 0-3, that I could use for the decimal fraction.
Extracting the essential bits from the binary level of the float seemed to much of a problem so I mainly worked with the decimal values.
In the function floatToByte I do a couple of essential things: I first check if it is a positive or a negative number as that determines the MSB I need to set in my byte. Then Itake the absolute value of the float so I get rid of o potential minus value, which would complicate calculations.
Then I turn the float into an integer and subtract that from the float. That leaves the decimal fraction. In order to keep at least some precision in that I have to put that in a double. Initially it was my idea to turn the values 0-9 into 0-3, but a simple division by 3 didnt have the desired effect as ‘9’ was sometimes 0.89 which by division would give a 2, So I decided on some simple ‘if’ conditions to convert the double fraction into half a nibble.
I then would shift the 5 bit temperature 2 bits to the right bitwise OR it with the fraction (on the two least significant bits and bitwise OR it with the sign bit.
That gave me temperature values that I can store in 1 Byte of EEPROM.

The way back – from memory byte to sensible temperature. Goes the other way around

byte floatToByte(float temp2) 
{
   byte sign = 128;
  if (temp2 >= 0) {
    sign = 0;
  }
  double temp = abs(temp2);
 
  int heel = int(temp);
  double fractie = temp - heel;
  
  fractie = (temp - heel);
  fractie = fractie * 100;
  byte fractie2 = 0;
  if (fractie >= 25) {
    fractie2 = 1;
  }
  if (fractie >= 50) {
    fractie2 = 2;
  }
  if (fractie >= 75) {
    fractie2 = 3;
  }
  byte temperatuur = (((heel << 2) | fractie2) | sign); 
  return temperatuur; 
  } 

void getTempByte(byte Temp) 
{ 
float temperatuurCelcius; 
if (Temp & 0b10000000) // check negative 
{
 Temp = Temp ^ 0b10000000; // knock out MSB byte 
 double fractie = (Temp & 0b00000011); 
 byte body = (Temp >> 2);
    Serial.println(-1 * (body + (fractie * 0.25)));
  }
  else
  {
    byte fractie = (Temp & 0b00000011);
    byte body = (Temp >> 2);
    Serial.println(body + fractie * 0.25);
  }


}
void setup() {
  Serial.begin(9600);
  byte x = floatToByte(+31.10);
  Serial.println(x, BIN);
  x = floatToByte(+31.25);
  Serial.println(x, BIN);
  x = floatToByte(31.50);
  Serial.println(x, BIN);
  x = floatToByte(+31.75);
  Serial.println(x, BIN);
  x = floatToByte(-31.75);
  Serial.println(x, BIN);
  x = floatToByte(+0.15);
  Serial.println(x, BIN);
  Serial.println("temperatuur ");
  getTempByte(0b1111101);

}
void loop() {
  // put your main code here, to run repeatedly:

}
Advertisement

Checking for odd and even numbers on the Arduino

Trying to determine if a number is odd or even on an Arduino is rather simple. There are several ways, but the easiest ways are to either take the modulo of the number to be tested, or do a bitwise AND on the Least Significant Bit.
If x % 2 is ‘0’ that means that x is even. If it not zero, it is an uneven number.
Similar for the bitwise test
If x & 1 is ‘1’ it must be an odd number. If it is ‘0’ it must be an even number.

If you want a specific action in your program to happen every so many hours or say just on the odd or even hours, that is rather simple.

Suppose I have a hydroponics system that I want to irrigate 6 times a day.
I have an Arduino +RTC and  the hours are stored in variable ‘hour’.

6 times a day is  every  4 hours.
I could of course specify the action 6 times, but there is  a simpler way: I can just check if the hour can be divided by 4. How do I do that? Well, a number  can be divided by 4 if it can be divided  by two, and then still would be even. (ofcourse you could check for x%4==0 but this is just to illustrate).
So I would have the condition:

if(((hour / 2) & 1) == 0) 
  {
   // then it must be 0,4,8,12,16,20 hr
  }

Because:
0/2=0  0&1=0
4/2=2  2&1=0
etc…

Sadly of course that may work on paper arithmetic, but it doesn’t work on a microcontroller like the Arduino, for the simple reason that bytes and integers don’t know fractions. So if ‘hour’ would be ‘3’,  ‘hour/2’ would not be 1.5 but 1. Well, no problem there, but if it would be ‘5’, hour / 2 would not be 2.5 but ‘2’. That is still an even number and that would make the computer think ‘5’ is in fact even.

This is what numbers 0-23 in bytes or integers look like when divided by 2

0=>  0 – Computer thinks its dividable by 4
1=>  0 – Computer thinks its dividable by 4  and is wrong
2=>  1
3=>  1
4=>  2 – Computer thinks its dividable by 4
5=>  2 – Computer thinks its dividable by 4 and is wrong
6=>  3
7=>  3
8=>  4 – Computer thinks its dividable by 4
9=>  4 – Computer thinks its dividable by 4 and is wrong
10=> 5
11=> 5
12=> 6 – Computer thinks its dividable by 4
13=> 6 – Computer thinks its dividable by 4 and is wrong
14=> 7
15=> 7
16=> 8 – Computer thinks its dividable by 4
17=> 8 – Computer thinks its dividable by 4 and is wrong
18=> 9
19=> 9
20=> 10 – Computer thinks its dividable by 4
21=> 10 – Computer thinks its dividable by 4 and is wrong
22=> 11
23=> 11

So instead of 6 actions, I get 12 actions.
There are several solutions for this. One solution could be to use floats, but they consume 4 bytes, where we prior could use 1 byte  for the hour. Also, as they are stored differently, the ‘& 1’ would give unpredictable results and the modulo function also doesn’t work on floats. So that would probably create more problems than it would solve.
The better solutions becomes immediately clear from the the table: if we first filter out the odd numbers, then divide by 2 and then check if the number is still even, then we have obtained our goal.

Condition becomes:

if ((hour & 1) == 0)  // filter out/discard odd numbers
   {
     if(((hour / 2) & 1) == 0)  // divide by two and check for LSB
       {
        //then it must be 0,4,8,12,16,20 hr
        //take action
       }
   }

The ‘x & 1’ method works for the standard arduino negative integers as well. Try:

void setup()
{
	Serial.begin(9600);
	Serial.println
	("Begin");
	for (int x = -10; x < 10; x++)
	{
		Serial.print(x);

		if((x & 1) == 0)

			Serial.print(" Even");
		else
		{
			Serial.print(" odd");
		}
		Serial.println();
	}
}

void loop()
{
}

it will not work on floats

However, as said a much quicker way would be: if(x % 4==0), but this is just to clear the principle

 

The binary storage of floating point numbers

Floating point numbers are positive or negative numbers with a decimal fraction: numbers such as 1.0 or 17.11 or –3.12 are all floating point numbers.
On the Arduino and other microprocessors are stored in ‘floats’.
Whereas the concept of a byte or an integer is quite straightforward, a float in binary form is a bit more challenging.
A byte is simple, it is just 2⁷+2⁶+2⁵+2⁴+2³ + 2²+2¹ +2⁰.

An integer is similar, be it that it extends to 215.

But how do you store a number like –1.5 ?

Well, floats are stored on the concept that practically every number can be expressed as the multiplication of a power of 2 times  a number between 1 and 2. Take for instance 7, or 7.00 for that matter, that can be expressed as 4 * 1.75 (or 2² * 1.75).

the number  -1.5 is in fact 1 * 1.5 which is 2⁰ * 1.5, preceded by a ‘-‘ sign.
The same goes for higher numbers, say 20.5= 16 * 1.28125  (=2⁴ * 1.28125).
Sure 20.5 can also be expressed by 2*10.25, but the last number must be between 1 and 2.

So in fact every number can be represented by:
sign * 2x  * y  (y being a number between 1 and 2).

According to agreement we call ‘x’ the ‘exponent’ and ‘y’  the ‘mantissa’ though the word mantissa is also used for the fractional part of a logarithm. The IEEE standard for floating point numbers therefore encourages to use the word ‘fraction’  instead of ‘mantissa’, so we can write the above as:
floating point number= sign * 2exponent * fraction

Lets get back to the  fraction part of the number 20.5 which is 1.28125 (remember? 20.5=2⁴ * 1.28125). If we look at that a bit deeper, we can see that that is actually 1+ 1/4 + 1/32. That makes sense coz 16* (1+1/4+1/32)= 16+4+0.5=20.5.
If we would break this down again we can see that  the ‘fraction’ or mantissa is actually a sum of  fractions that all are  again 1/(a power of 2). It is probably clear by now that for 20.5 that would be 1/2⁰ + 1/2² +1/2⁵

Anyway, back to the binary storage.
As said, on the Arduino and many other processors, the floating number is stored in 32 bits and the protocol to store that follows from the notation we have learned above.
The most left bit, bit 32, stores the ‘sign’ if it is a ‘1’  the number is negative, if it is  a ‘0’ it is positive.
The next 8 bits, bits 31-24 store the exponent. as we want  values between 2128  and 2-127, we store 2¹ as 10000000 (decimal 128), 2² as 10000001 (decimal 129), 2³ as  10000010 (decimal 130)  etc… The exponent thus follows from subtracting 127 from the decimal number that is stored in bits 31-24.

The fraction or mantissa is stored in bits 23-1. However, since we know that the fraction is always between 1 an 2, we do not store  the ‘1’ as we know it is always there. We refer to that as the ‘hidden’ bit, although it is not  hidden, it is just not stored. We use bits 23-1 to indicate a sum of  the fractions 1/2, 1/4, 1/8, 1/16 etc.
So, the binary storage of a float is as follows:

sign exponent hidden fraction
20.5 0 10000011 01001000000000000000000
+ 4(131-127) 1+ 1/4 +1/32
-7 1 10000001 11000000000000000000000
2(129-127) 1+ 1/2+1/4

The DS3231 RTC temperature sensor

The DS3231 RTC is a rather accurate RTC that has an internal temperature sensor that is used to calibrate the oscillator. The sensor is however also readable from external software. It has a 10 bit solution and  uses two registers: 0x11 and 0x12. The information in the upper byte is stored in 2-complement notation. The lowerbyte is there for the fractional part and has a solution of 0.25 degrees Celsius.
Two’s complement notation means that with positive numbers  it just follows the regular binary storage, but with negative numbers it does it a bit different. Knowing whether a number is negative or positive is indicated by the MSB in the Upper byte. If that is  a ‘1’, then the number is negative.
ds3231 temp sensorAny reading of the registers therefore needs to include a check to see if the  number is positive or negative.  As the Lower byte only indicates the fraction with an accuracy of 0.25 degrees it only needs to count to 4 (0.0. 0.25, 0.50, 0.75), hence two bits are enough
So suppose we have  retrieved the number:
0b0001100101 => +25.25°C. We can easily see it is 25.25°C because the top 8 bits are 00011001, which is 25, while the lower two bits 0b01, mean 1×0.25.
As the  lower byte, only uses the top 2 bits, it may need to be rightshifted 6 positions for calculations. So how about negative numbers, say -18 degrees.
Well -18 is 0b11101110  (=238 in twos complement notatie).
We can see that the highest bit is a 1, indicating a negative number. In order to make a check, we do the following:
0b11101110 &   0b10000000 => 0b10000000  So we know it is negative
Then we need to convert the 2 complement notation
0b11101110 XOR 0b11111111 => 0b00010001 (=17) // first XOR it
17+1= 18   // and  add a ‘1’
18*-1 = -18 // and then we turn it negative
So, how does that look in a program?

float getTemperature()
{
    int   temperatureCelsius;
    float fTemperatureCelsius;

     uint8_t UBYTE  = readRegister(REG_TEMPM); //Two's complement form
     uint8_t LRBYTE = readRegister(REG_TEMPL); //Fractional part

	if (UBYTE & 0b10000000 !=0) //check if -ve number
	{
		UBYTE  ^= 0b11111111;
		UBYTE  += 0x1;
		fTemperatureCelsius = UBYTE + ((LRBYTE >> 6) * 0.25);
		fTemperatureCelsius = fTemperatureCelsius * -1;
	}
	else
	{
		fTemperatureCelsius = UBYTE + ((LRBYTE >> 6) * 0.25);
	}

	return (fTemperatureCelsius);

}

Obvously this isnt a full program but just a function. You still need to define REG_TEMPM (0h11) and REG_TEMPL (0x12), and ‘readRegister’ is another function that just reads the specified registers (using the ‘Wire’library)

Measuring temperature with NTC-The Steinhart-Hart Formula

I know, this subject can be found all over the web, but the calculations sometimes are   presented a bit cryptic as if the author  wanted to put as many nested calculations on one line, so I just wanted to write something that explains it step by step, especially where the actual calculations are concerned. Lady Ada does a good job explaining the use of the thermistor, but I added some more explicit calculations.
The Arduino has several ADC ports that we can use to read a voltage, or rather an ‘ADC value’. If the Analog port is connected to Vcc, the max value one  reads is 1023 and of course when connected to ground it is 0.
spanningsdelerNow if we make a voltage divider which is typically two resistors in series between Vcc and Ground and the analogport in the middle, the reading will depend on the ratio of the two resistors: If they are equal, the reading will be 512, halfway to 1023. If one of the resistors, say the bottom one is an NTC, then the readings on the analogport will vary with the temperature:
If the temperature goes down, the value of the resistor increases and so will the reading on the analog port.

Suppose we have a 10k Seriesresistor and an NTC that for now we call ‘R’.
Then the voltage that can be measured in the middle is

Vo=R/(R+10K) * Vcc

The analogPort readings however don’t give a voltage but an ADC value that can easily be calculated
ADC value= Vo*1023/Vcc  // if for instance the Vo=4Volt the ADC = 818
or
ADC value= 1023 *(Vo/Vcc)

If we now combining the two formulas or as it is called ‘substitute’ Vo in the formula for ADC we get the following:
ADC value= (R/(R+10K))*Vcc*1023/Vcc
As we multiply by Vcc but also divide by Vcc we can take that out of the equation and end up with
ADC value= (R/(R+10k))*1023
ADC value=  1023*R/(10+R)
if we want to get the value of R out of that equation, that becomes
R=10k/(1023/ADC-1)
If that goes a bit too fast, here is the equation worked out. I prefer pictures over the ‘in line’ formulas as some people have problems understanding the PEMDAS / BODMAS order of operation.

CodeCogsEqn(1)

This becomes
CodeCogsEqn(7)
subtraction of R
CodeCogsEqn(2)Work  -R into the multiplication
CodeCogsEqn(3)As we are interested in R, divide both sides by the enclosed fracture
CodeCogsEqn(4)
The ’10’ stood for ’10k’CodeCogsEqn(5)
and as we don’t always use a 10k we just make it more general:
CodeCogsEqn(6)So, as long as we know the value of the series resistor, we can calculate the value of the NTC from the measured ADC value. Now remember, this is valid voor a pull-up configuration. If it is a pull down configuration, the calculation of the ADC to resistor value is  the inverse.

Rntc = Rseries*(1023/ADC-1);// for pull down
Rntc = Rseries/(1023/ADC – 1)); // for pull-up configuration

So what would that look like in a  program?

//Measure NTC value
byte NTCPin = A0;
const int SERIESRESISTOR = 10000;
void setup()
{
	Serial.begin(9600);
}
void loop()
{
	float ADCvalue;
	float Resistance;
	ADCvalue = analogRead(NTCPin);
	Serial.print("Analog value ");
	Serial.print(ADCvalue);
	Serial.print(" = ");
//convert value to resistance
	Resistance = (1023 / ADCvalue) - 1;
	Resistance = SERIESRESISTOR / Resistance;
	Serial.print(Resistance);
	Serial.println(" Ohm");
	delay(1000);
}
//end program

Knowing the resistance of the NTC is nice but it doesn’t tell us much about the temperature… or does it?
Well many NTC’s have a nominal value that is measured at 25 degrees Centigrade, so if you have a 10k NTC and you measure it to be 10k, that means it is 25 degrees at that moment. That doesn’t help you much when the measurement is different.
You could keep a table in which every resistance value stands for a temperature. Those tables are very accurate but require a lot of work and memory space.

However, there is a formula, the Steinhart-Hart equation, that does a good approximation of converting resistance values of an NTC to temperature. Its not as exact as the thermistor table ( after all it is an approximation) but its fairly accurate.

The Steinhart-Hart equation looks like this:
CodeCogsEqn(8)That is a rather complex equation that requires several parameters (A, B, C) that we normally do not have for the run of the mill NTC.There are two things we can do. We can take 3 readings with a calibrated temperature and then work out the A, B and C parameters.

CodeCogsEqn

but fortunately there is a simplification of this formula, called the B-parameter Equation. That one looks as follows:
CodeCogsEqn(9) To is the nominal temperature, 25 °C in Kelvin (= 298.15 K). B is the coefficient of the thermistor (3950 is a common value). Ro is the nominal resistance of the NTC  (thus at 25 degrees). Let’s say we have a 10Kohm NTC. We only need to plug in R (the resistance measured) to get T (temperature in Kelvin) which we then convert to °C.

The program looks as follows:

//---------------
byte NTCPin = A0;
#define SERIESRESISTOR 10000
#define NOMINAL_RESISTANCE 10000
#define NOMINAL_TEMPERATURE 25
#define BCOEFFICIENT 3950

void setup()
{
Serial.begin(9600);
}
void loop()
{
float ADCvalue;
float Resistance;
ADCvalue = analogRead(NTCPin);
Serial.print("Analog value ");
Serial.print(ADCvalue);
Serial.print(" = ");
//convert value to resistance
Resistance = (1023 / ADCvalue) - 1;
Resistance = SERIESRESISTOR / Resistance;
Serial.print(Resistance);
Serial.println(" Ohm");

float steinhart;
steinhart = Resistance / NOMINAL_RESISTANCE; // (R/Ro)
steinhart = log(steinhart); // ln(R/Ro)
steinhart /= BCOEFFICIENT; // 1/B * ln(R/Ro)
steinhart += 1.0 / (NOMINAL_TEMPERATURE + 273.15); // + (1/To)
steinhart = 1.0 / steinhart; // Invert
steinhart -= 273.15; // convert to C

Serial.print("Temperature ");
Serial.print(steinhart);
Serial.println(" oC");
delay(1000);
}
//-------------

This ofcourse is not an ideal program. It is always good to take a few samples and to average them.
The following function can do that for you:

float sample(byte z)
/* This function will read the Pin 'z' 5 times and take an average.
 */
{
	byte i;
	float sval = 0;
	for (i = 0; i < 5; i++)
	{
	sval = sval + analogRead(z);// sensor on analog pin 'z'
	}
	sval = sval / 5.0;    // average
	return sval;
}

Feeding the series resistor and NTC from the 5 Volt supply from the Arduino is possible. The Arduino power line does have glitches though. For accurate measurements it is better to use the 3.3Volt line as analog reference and feed the resistor from there.
for that add the following code in the setup
// connect AREF to 3.3V and use that as VCC for the resistor and NTC!
analogReference(EXTERNAL);

Simple EEPROM Module for Arduino or other microcontroller

256 EEPROMs come in handy if you want to store some data without losing it. Many microcontrollers come with some internal EEPROM, but as EEPROMs have a finite (though very large) number of writes before they start becoming faulty, my anal retentive character always had a problem with using that internal EEPROM.
There are other reasons too why you might want to use an external EEPROM: data logging in which you just want to swap an EEPROM rather than having to read out your microcontroller in the field.
Anyway, I just wanted to share a simple 5 minute build EEPROM module that is a bit simpler than a prior one i published.
BOM
1x 24LC256 EEPROM (or other size)
2x 4k7 resistors
stripboard 5 strips of 8
8 pin DIL IC socket (optional)

256pin I am using a 24LC256. That is a 256 kiloBIT EEPROM so in fact only a 32kiloBYTE EEPROM in my traditional way of thinking. These are not to expensive. Can be had for around 80 ct USD.
Although the board can also be used for smaller EEPROMS, like a 24C32, I would advise against that. If that is what you need, buy a 50 ct DS1307 RTC module that includes a 24C32 (which is actually 4kByte).

Anyway, the 24LC256 is an I2C EEPROM, which is pretty standard and which makes use easy
Pins A0, A1 and A2 select the I2C addresses, (A0=pin1, A1=pin1, A2=pin3).
The addressing is as follows

1 0 1 0 A2 A1 A0 x

So if you connect pins A0, A1, A2 to GND the I2C address will be 1010000 = 0x50 in hexadecimal.
If you connect them all to Vcc it will be 1010111=0x57.
As 0x50 is an address that is often used, I decided to connect A2 and A1 to ground and A0 to Vcc, which gives 0x51. It also made the design a tadd simpler.
Pin 7 is the ‘WriteProtect’ pin that needs to be pulled HIGH for ReadOnly and LOW for Read/Write. Obviously I connected it to ground.
The pull up resistors are both 4k7
You will find many EEPROM libraries in the Arduino Playground I tend to use the following code to read and write:

void writeData(int device, unsigned int addr, byte data)
// writes a byte of data 'data' to the chip at I2C address 'device', 
// in memory location 'add'
{
    if ( addr > 65535 )
        addr = addr | B00001000;
    Wire.beginTransmission(device);
    Wire.write((int)(addr >> 8));   // left-part of pointer address
    Wire.write((int)(addr & 0xFF)); // and the right
    Wire.write(data);
    Wire.endTransmission();
    delay(10);
}
byte readData(int device, unsigned int add)
// reads a byte of data from memory location 'add' in chip at I2C address 'device'
{
    byte result; // returned value
    Wire.beginTransmission(device); // these three lines set the pointer 
				    // position in the EEPROM
    Wire.write((int)(add >> 8));    // left-part of pointer address
    Wire.write((int)(add & 0xFF));  // and the right
    Wire.endTransmission();
    Wire.requestFrom(device, 1);    // now get the byte of data...
    result = Wire.read();
    return result; // and return it as a result of the function readData
}

Special Notes if using the 24LC1025

1025

This board can also be used for larger EEPROMS, but……… if you use it for the 24LC1025, you need to make a small adaptation. With this chip A2 MUST be tied to Vcc for it to operate.

The 24LC1025 has an internal addressing boundary limitation that is divided into two segments of 512K bits. Block select bit ‘B0’ to control access to each segment.

The addressing is determined by

1 0 1 0 B0 A1 A0 R/-W  

in which the ‘Block select bit is used to address the lower or higher 512k Block The chip thus has 2 different addresses. with the A0A1 selection as in my board those are:

1010101 =0x55

1010001 =0x51

For memory access from 0..65535 the I2C address for the Memory Chip is 0x51. If you want to access memory between 65536..131071 the Chip address is 0x55

You cannot write across the 65535..65536 byte boundary with breaking the operation into two write() calls. One to chip 0x51, the other to chip 0x55.