Article Index

Processing the data

Our next task isn't really directly related to the problem of using the I2C bus but it is a very typical next step. The device returns the data in three bytes but the way that this data relates to the temperature isn't simple. 

If you read the data sheet you will discover that the temperature data is the 14-bit value that results by putting together the most and least significant byte and zeroing the bottom two bits. The bottom two bits are used as status bits - bit zero currently isn't used and bit one is a 1 if the data is a humidity measurement and a 0 if it is a temperature measurement. 

To put the two bytes together we use:

unsigned int data16=((unsigned int) msb << 8) | (unsigned int) (lsb & 0xFC);

This zeros the bottom two bits, shifts the msb up eight bits and Ors the two together. The result is a 16 bit temperature value with the bottom two bits zeroed. 

Now we have raw temperature value but we have still have to convert it to standard units. The datasheet gives the formula

Temp in C = -46.85 + 175.72 * data16 / 216

The only problem in implementing this is working out 216. You can work out 2x with the expression 1<<x i.e. shift 1 x places to the right. This gives:

float temp = (float)(-46.85 + (175.72 * data16 / (float)(1<<16))); 

Of course 216 is a constant that works out to 65536 so it is more efficient to write:

 float temp = (float)(-46.85 +   (175.72 * data16 / (float)65536));

Now all we have to do is print the temperature:

    printf("Temperature %f C \n\r", temp);

The final program is:

if (!bcm2835_init())
        return 1;
bcm2835_i2c_begin();
bcm2835_i2c_setClockDivider(BCM2835_I2C_CLOCK_DIVIDER_626);
setTimeout(40000);

bcm2835_i2c_setSlaveAddress(0x40);
char buf[4] = {0xE3};
uint8_t status = bcm2835_i2c_read_register_rs(buf, buf, 3);
printf("status=%d\n\r",status);
uint8_t msb = buf[0];
uint8_t lsb = buf[1];
uint8_t check = buf[2];
printf("msb %d \n\r lsb %d \n\r checksum %d \n\r", msb, lsb, check);

unsigned int data16 = ((unsigned int) msb << 8) | (unsigned int) (lsb & 0xFC);
float temp = (float) (-46.85 + (175.72 * data16 / (float) 65536));
printf("Temperature %f C \n\r", temp);

 

Reading the humidity

The nice thing about I2C and using a particular I2C device is that it gets easier. Once you have seen how to do it with one device the skill generalizes and once you know how to deal with a particular device other aspects of the device are usually similar. 

While clock stretching is the most simple it sometimes doesn't work with some slave and master combinations. It is worth knowing how the alternative polling method works. For this reason let's implement the humidity reading using polling. 

Finally we have to find out how to use the No hold master mode of reading the data - it is sometimes useful. 

In this case we can't use the read_register_ns  command to read and the master because we don't want to keep sending the 0xF5 command. We write it once to the slave and then repeatedly attempt to read the three byte response. If the slave isn't ready it simply replys with a NACK. 

   buf[0] = 0xF5;
    bcm2835_i2c_write(buf, 1);
    while (bcm2835_i2c_read(buf, 3) == BCM2835_I2C_REASON_ERROR_NACK) {
        bcm2835_delayMicroseconds(500);
    };

This polls repeatedly until the slave device returns an ACK when the data is loaded into the buffer. 

Once we have the data the formula to convert the 16-bit value to percentage humidity is:

RH= -6 + 125 * data16 / 2^16

Putting all this together and reusing some variables from the previous program we have:

 buf[0] = 0xF5;
 bcm2835_i2c_write(buf, 1);
 while (bcm2835_i2c_read(buf, 3) == BCM2835_I2C_REASON_ERROR_NACK) {
        bcm2835_delayMicroseconds(500);
    };
 msb = buf[0];
 lsb = buf[1];
 check = buf[2];
 printf("msb %d \n\r lsb %d \n\r checksum %d \n\r", msb, lsb, check);
 printf("crc = %d\n\r", crcCheck(msb, lsb, check));
 data16 = ((unsigned int) msb << 8) | (unsigned int) (lsb & 0xFC);
 float hum = -6 + (125.0 * (float) data16) / 65536;
 printf("Humidity %f %% \n\r", hum);
 bcm2835_i2c_end();

The only unusual part of the program is using %% to print a single % character - necessary because % means something in printf.

Checksum calculation

Although computing a checksum isn't specific to I2C, it is another common task.

The datasheet explains that the polynomial used is:

X8+X5+X4+1

Once you have this information you can work out the divisor by writing a binary number with a one in each location corresponding to a power of X in the polynomial - i.e. the 8th, 5th, 4th and 1st bit.

Hence the divisor is 0x0131. 

What you do next is roughly the same for all CRCs. First you put the data that was used to compute the checksum together with the checksum value as the low order bits:

uint32_t data32 = ((uint32_t)msb << 16)|
                    ((uint32_t) lsb <<8) |
                       (uint32_t) check;

 

Now you have three bytes, i.e 24 bits in a 32-bit variable.

Next you adjust the divisor so that its most significant non-zero bit aligns with the most significant bit of the three bytes. As this divisor has a one at bit eight it needs to be shifted 15 places to the right to move it to be the 24th bit:

uint32_t divisor = ((uint32_t) 0x0131) <<15;

Now that you have both the data and the divisor aligned, you step through the top-most 16 bits, i.e. you don't process the low order eight bits which is the received checksum. For each bit you check to see if it is a one - if it is you replace the data with the data XOR divisor. In either case you shift the divisor one place to the right:

for (int i = 0 ; i < 16 ; i++){
 if( data32 & (uint32_t)1<<(23 - i) )
                   data32 =data32 ^ divisor;
 divisor=divisor >> 1;
};

When the loop ends, if  there was no error, the data32 should be zeroed  and the received checksum is correct and as computed on the data received. 

A complete function to compute the checksum with some optimizations is:

uint8_t crcCheck(uint8_t msb, uint8_t lsb, uint8_t check){
 uint32_t data32 = ((uint32_t)msb << 16)|
         ((uint32_t) lsb <<8) |
                 (uint32_t) check;
 uint32_t divisor = 0x988000;
 for (int i = 0 ; i < 16 ; i++){
  if( data32 & (uint32_t)1<<(23 - i) )
                         data32 ^= divisor;
  divisor>>= 1;
 };
 return (uint8_t) data32;
}

It is rare to get a crc error on an I2C bus unless it is overloaded or subject to a lot of noise.