Arduino I2C with the MCP9808 Temperature Sensor
[latexpage]The Microchip MCP9808 is a high precision temperature sensor with an I2C interface – making it easy to interface with microcontroller or embedded Linux electronics. The chip has a very small form-factor: available as either an 8-pin DFN package – or an 8-pin MSOP package. For hobbiest purposes DFN is almost impossible to use due to the complexity of having to reflow solder under the contacts; and MSOP is quite challenging (though it can be done with care). If you want something easy to use with a breadboard however, the good folks at Adafruit can come to the rescue – as they sell the device pre-soldered to a SIP breakout board. (Also available from Makersify in the UK).
If all you want to do is get something up and running quickly – then you can’t do better than to use the Adafruit Aurduino library (or if you prefer a Python library for BeagleBone or RPi).
Adafruit have done a great job with these libraries and most users will be delighted that “it just works”; but I want to explore how it all works: how to actually communicate with chip directly – using I2C. This guide written from the point of view of an Arduino; but you should be able to follow along from any other device if you’re reasonably familiar with I2C and low-level coding.
The circuit itself is very straightforward: we just need to supply power to the chip, and connect the two I2C lines: the clock – SCL, and the data line SDA. On an Arduino Uno these are pins A5 (SCL) and A4 (SDA). Different boards use different pins – but (for other Arduino boards) there’s a helpful list in the documentation for the Arduino I2C library.
The MCP9808 isn’t too fussy about the voltage it takes as supply – either 3.3v or 5v is fine; but it needs to be the same voltage as the microcontroller or embedded system is using for I2C. For an Arduino this means 5v – but most ARM-based embedded systems will use 3.3v.
Note that we don’t need to connect the other four pins – unless we want to change the default I2C address of the sensor chip, or use the alert functionality.
On Arduino the first thing that we need to do in include the arduino I2C library – called wire.h (or if you want even more details, see here) which enables us to use I2C.
Remember that Arduino’s (like most microcontroller systems) use a super-loop paradigm (that is to say that the main body of the program consists of an infinite loop – which is entered in to after a set up phase): so if we establish that we’re using I2C communciations in setup() – then we can handle individual communications as and when we need them in the main loop().
Wire.begin();
If we want to get the output in a serial console – then we need to initialize serial too
Serial.begin(9600);
And that’s all of the setup code that we need.
On to the main loop. The basis idea here couldn’t be simpler: we’ll connect to the I2C device, and read the value of the internal register corresponding to the current temperature. From the data sheet we can see that to do this, we write 0x05 to the device – and then read back a 16-bit value corresponding to the temperature: unfortunately I2C libraries generally work by handling individual bytes – so to get a 16-bit value, we’ll need to read two bytes and fix it ourselves.
Strictly speaking the wire library works with values unsigned 8-bit integer values (uint8_t) but the library will convert from regular data types for us. To begin we initialize communication with the chip. The chip’s I2C address is determined by the state of the three additional pins (A0-A2): the 7-bit address is 0011 A2 A1 A0 (e.g. if A0, A1 & A2 are all pulled to GND (and if you use the Adafruit breakout board this is done by default) then the chip will be addressable at 0x18); so all we need to do is:
Wire.beginTransmission(0x18);
Next we send 0x05 to signal that we’re looking to obtain the current temperature – and lastly we end the transmission.
Wire.write(0x05);
Wire.endTransmission();
The chip will then respond with our two bytes. To hold this 16-bit value – we’ll use a variable of type uint16_t (an unsigned 16-bit integer). We’ll tell the Wire library that we’re expecting two bytes from our device…
uint16_t t;
Wire.requestFrom(0x18, 2);
Then we’ll call the Wire.read() function to read the first byte:
t = Wire.read();
As the data sheet tells us this first byte is the “upper” or most significant byte. If we just read it in to our 16-bit variable it would be the least significant 8-bits: so we need to shift it 8 places left…
t <<= 8;
Then to get the second 8-bits we simply OR the old value and the new value.
t |= Wire.read();
But what is the data that’s returned? Again the data sheet tells us everything we need. Essentially the temperature is returned as a signed fixed-point (rather than floating point) number – with three additional flag bits.
Bits 15-13 – the first (most significant) 3-bits of the first (most significant) byte, are flags identifying whether or not the temperature is greater-than or equal to the critical value, greater-than the upper alert or less-than the lower alert. Given that we haven’t specified these yet – we can safely disregard the values. The next bit (bit 12) is the sign bit (0 for +ve temperature or 1 or -ve). The remaining 4-bits (bits 11-8) of the first byte are the most significant 4-bits of the 8-bit integer component of the temperature.
The most significant bits of the second (least significant byte) – bits 7-4 – are the least significant four bits of the 8-bit integer portion; and the remaining 4-bits (bits 3-0) are a fixed-point representation of the fractional component of the temperature (with the bits referring to $2^{-1}$, $2^{-2}$, $2^{-3}$ & $2^{-4}$).
For example if the temperature is 25.25ºC – then the temperature value returned would be:
0000 0001 1001 0100
(00011001 = 25, and 0100 gives us 0.25).
Given that we want to discard the first 4-bits (the three flag bits and the sign – which we’ll deal with after we’ve converted it to a “regular” float
) we can simply AND it with a bit mask of 0FFF
float temp = t & 0x0FFF;
For a positive temperature, if we treat the value that we now have as a 16-bit integer it would be equal to 404 – exactly 16x larger than the correct value of 25.25… So we can simply divide by 16.0.
temp /= 16.0;
Lastly the sign bit. The chip uses two’s complement for negative values. The easiest way to think about two’s complement numbers is that it’s like running a car’s odometer backwards – so one less than 0000 would be (for a binary odometer!) 1111: which is -1 as a 4-bit two’s complement number. The idea of two’s complement is that if you can perform subtraction by addition. For example we know that 1 + -1 = 0. In 4-bit two’s complement arithmetic that’s: 0001 + 1111 = 10000. Discarding the overflow, the least significant four bits are 0000 – which of course is 0.
So for our temperature value, if bit 12 is a 1 – then we just need to (as the data sheet helpfully tells us!) subtract the number we have from 256.
For example if the temperature was -5.0ºC then the data would be:
0001 1111 1011 0000
In decimal this is 4016 – if we shift it to the right by 4-bits (diving by 16) gives us 251.0. If we now subtract 256 we get -5.0 : the correct answer.
We can do both of these steps in one – if we AND the binary value (t) with a mask of 0x1000 – if that equals 1 then subtract 256…
if (t & 0x1000) temp -= 256;
And for simple use that’s all there is.
Here’s the complete code:
#include <Wire.h> void setup() { Serial.begin(9600); Wire.begin(); } void loop() { uint16_t t; // Get the raw temperature Wire.beginTransmission(0x18); Wire.write(0x05); Wire.endTransmission(); Wire.requestFrom(0x18, 2); t = Wire.read(); t <<= 8; t |= Wire.read(); // Now convert into degrees C float temp = t & 0x0FFF; temp /= 16.0; if (t & 0x1000) temp -= 256; // Lastly write the value to serial Serial.println(temp); delay(300); }
That’s all for now. In part two we’ll look at what else we can do with the device.
3 thoughts on “Arduino I2C with the MCP9808 Temperature Sensor”
Leave a Reply Cancel reply
This site uses Akismet to reduce spam. Learn how your comment data is processed.
Filed under: Arduino,Electronics - @ May 9, 2015 10:40
Hi,
Thanks für the MCP9808 sketch. Using it, the readings keep always the same. Are there setup registers that need to be set, like wakeup, precision or timing? Is there a way of waiting for the next available temperature reading? I can imagine the sensor is not the fastest one.
Also, in your appended code, the << smybols did get mixed up with some html markup error, so direct copying it does not work. I corrected this for my sketch, though, so this should be unrelated to the error.
Best regards,
Dennis
Solved it: Add a delay as follows:
…
Wire.endTransmission();
delay(100); // Works reliably with this additional delay
Wire.requestFrom(0x18, 2);
…
250 ms delay is more appropriate since this is the fastest acquisition rate at high precision of the mcp9808. Quoting the original data sheet:
“Resolution bits
00 = +0.5°C (tCONV = 30 ms typical)
01 = +0.25°C (tCONV = 65 ms typical)
10 = +0.125°C (tCONV = 130 ms typical)
11 = +0.0625°C (power-up default, tCONV = 250 ms typical)”