Monitor humidity with the I2C bus
Tam Hanna feels moist and no one likes that, so he’s monitoring his environment.
Tam Hanna feels moist and no one likes that, so he’s monitoring his environment.
Living under the ground has benefits and disadvantages. While the person who treats himself to a subterranean living space starts to appreciate an absolutely noise-free sleeping experience, managing underground real estate challenges even experienced landladies such as my wife. The inspiration for this story struck when she had to travel to Germany for business: she wanted to keep an eye on the humidity and temperature levels of our headquarters.
The development of the semiconductor industry has led to smart sensors which combine a sensing element with conditioning logic. Output is handled via hardware buses such as I2C, the bus which we’ll use in the following tutorial. The Texas Instruments HDC2010 is an excellent temperature sensor; it not only takes care of temperature, but also keeps an eye on humidity. All that is done with a pretty impressive accuracy of within two per cent. While this might not sound like much, it’s the absolute high end of what is currently possible in affordable semiconductor sensors.
Sadly, Texas Instruments makes the HDC2010 in an extremely small package. Soldering this by hand is impossible, and reflowing it with the normal reflow oven is difficult at best. The official evaluation kit from Texas Instruments also is quite pricey, leaving unexperienced designers in a bit of a rut.
Temp&hum ahoy
Fortunately, Mikroelektronika recently started devoting resources to solving this problem. In the case of the HDC2010, the solution goes by the name of Temp&hum 3 Click and can be yours for about $13 from www.mikroe.com/temp-hum-3-click.
Freshly downloaded versions of Raspbian usually disable the Pi’s I2C interface. Fortunately, solving this problem is not hard – open raspi-config, switch to Interfacing options and enable the I2C interface. After the obligatory reboot, the I2C interfaces show up in the device tree: pi@raspberrypi:/dev $ ls | grep “i2” i2c-1
Scanning for the presence of devices is best accomplished via a small utility called i2cdetect. It’s not part of the standard distribution, so download it from the package repository as follows: pi@raspberrypi:/dev $ sudo apt-get install i2c-tools
After that, it’s time to assemble the actual circuit: see the diagram on page 56. Experienced electrical engineers might wonder why there is no pull-up resistor present. The answer is found in the schematics of the Mikroelektronika board (see page 57): the company put a set of pull-up resistors in place.
Let’s take a quick look at how I2C actually works. The bus, originally developed by Philips for various hi-fi applications, consists of the master and a set of slaves. The SCL line – short for serial clock – is toggled by the master. It is responsible for setting the speed at which the entity of the bus operates. SDA – short for serial data – is modulated by master or by slave in order to enable actual data transmission.
Master and slave can communicate over one line due to the use of the open drain principle. The abovementioned pull-up resistors lazily pull SCL and SDA to the positive voltage level. Both master and slaves can pull the line down via a transistor – the interesting bits of this technique are explained in the box on page 55.
For us, however, the next step involves finding out if the Raspberry Pi is able to detect the HDC2010. This is accomplished by i2cdetect - the parameter -y
eliminates the highly annoying warning about potential problems which might occur during the scan process: pi@raspberrypi:~ $ i2cdetect -y 1
0 1 2 3 4 5 6 7 8 9 a b c d e f
00: -- -- -- -- -- -- -- -- -- -- -- -- -10: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -20: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -30: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -40: 40 -- -- -- -- -- -- -- -- -- -- -- -- -- -- -50: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -60: -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -70: -- -- -- -- -- -- -- -
Should your i2cdetect run not show a slave presence at address 40, check the wiring. Furthermore, use an oscilloscope to verify the presence of waveforms on SCL; should a logic analyser be on hand, you can also use it to find out more.
Let’s code!
At this point, we need to start coding. In theory, opening the data sheet and taking a careful look at it is the action of choice. In practice, coding a driver from scratch tends to be unproductive; almost all semiconductor vendors provide example drivers. In the case of Texas Instruments, simply visit http://bit.ly/ LXF252I2C and look for the string ‘snac075’ to download a sample driver for the Arduino platform.
When dealing with manufacturer drivers, that process usually consists of adapting their logic to the API at hand and removing needed parts. For some reason, manufacturers’ application engineers insist on the implementation of every useless feature. Like most Arduino examples, the example code provided by Texas Instruments consists of three files. In addition to the header and a .cpp file, the semiconductor vendor provides a .ino file containing an example demonstrating the API. For convenience’s sake, open all three of these files in an editor.
Texas Instruments did an extraordinary job in encapsulating the Arduino-specific parts of the API from the rest of the logic. Because of that, we’ll reuse the class provided; in practice, cutting the logic out can often end up being quicker.
Start out by recreating the header file on Raspberry Pi’s memory. The contents can be taken one by one from the original file – just make sure to remove the two inclusions of Arduino-specific header files, as they will not be available when compiling C++ code for the Pi. Porting the body of the class is accomplished by using the process set out in the book Code Reading:
The Open Source Perspective by Diomidis Spinellis. He recommends that code should be considered compilable: make the compiler read it, and use the list of errors to find a working solution.
The first step for porting the .cpp file involves adjusting its inclusions. Obviously, the Arduino-related library include must be replaced with one intended for the Raspberry Pi:
#include “HDC2010.H”
#include
On the Arduino, all header files become part of the system library; in the case of our local compilation process, the file must instead be loaded from the current working path. We are ready to order the first compile run:
pi@raspberrypi:~ $ gcc Hdc2010.cpp -lwiringpi
. . .
HDC2010.H:98:3: error: ‘uint8_t’ does not name a type
When done, the compiler emits a list of more than a few dozen errors. Glance over them to find low-hanging fruit to eliminate first; eliminating simple problems reduces the length of the error list. We decided to go after those related to the various uint types first. Eliminating these first clears the ‘fog of war’ surrounding the codebase at hand.
A classic nuisance when moving code between platforms involves variable declarations. On the Arduino, a set of types for specific integers is declared to simplify interfacing hardware to code. The Raspberry Pi, obviously, does not have these types, as the system usually is not used for direct hardware interfacing.
Fixing the problem is easy. Open the header file, and add the following defines to bind the tags to variable declarations:
#ifndef HDC2010_H
#define HDC2010_H
#define uint8_t unsigned char
#define uint16_t unsigned short int
Running the new version of the code leads to a significantly reduced number of errors; another run of
GCC shows the problems that still need to be tackled. A careful look at the warning messages shows us that Texas Instruments implemented a set of functions to encapsulate the Wiring API used for I2C communications from the rest of the driver. We need to look at the functions responsible for hardware interaction, and the rest of the code can be used as-is.
Let’s start our work with the begin function that is responsible for establishing a connection. In case of the Wiring API, creating a connection to the I2C engine is
underground locations can have humidity problems: air, like all gases, can hold a specific amount of water depending on its temperature. as the temperature falls, it holds less humidity – pumping dry, hot air into a dry but cold space always leads to condensation.
simple. Sadly, the wiringpi API takes a different approach in that it returns a file descriptor for the interface. Due to that, we need to add a member to the class definition which holds the file descriptor: private: int myfd; int _addr; // Address of sensor
Once this is out of the way, the begin method can be adjusted so that connections can take place: void Hdc2010::begin(void)
{ myfd = wiringpii2csetup (0x40) ; } Purists might complain about our practice of not checking the return value carefully; while checking for -1 is recommended in production code, we omitted it here for clarity reasons.
While finding the best approach to navigate the compile errors is both art and science, we consider the reset() function to be the next valuable target: void Hdc2010::reset(void)
{ uint8_t configcontents; configcontents = READREG(CONFIG); configcontents = (configcontents | 0x80); writereg(config, configcontents); delay(50);
}
While reset() looks free of accesses to the Wire API, the use of the delay function – normally part of the wiringpi library – causes issues. We limited ourselves to including the header dedicated to the I2C library; add the following declaration to eliminate another problem: #include
A question of registers
While the I2C bus can, in theory, transmit just about any kind of information, most parts work on a register basis. The internals of the part to behave like a classic key value store, which can be addressed, written to and read from by the master.
On the Arduino’s I2C API, writing values is a two-step process. The register first needs to be opened for modification, after which the task at hand can be done. In the case of our driver, the design pattern is manifested in function openreg : void Hdc2010::openreg(uint8_t reg)
{ Wire.begintransmission(_addr); //Connect HDC2010 Wire.write(reg); // point to specified register Wire.endtransmission(); // Relinquish bus control }
While openreg is not needed on the Raspberry Pi, removing the function completely would require significant rewriting of our code. A more comfortable way involves removing the entity of the code and replacing it with no operation comment: void Hdc2010::openreg(uint8_t reg)
{
//NOP - No Operation on Raspbian
}
Readin’ and writin’
Now that the individual activation of the bus is out of the way, we need to proceed to reading and writing register information. The original writing function for the Arduino is complex, as the Wiring API requires you to send the register and the data bytes separately: void Hdc2010::writereg(uint8_t reg, uint8_t data)
{ Wire.begintransmission(_addr); // Open Device Wire.write(reg); // Point to register Wire.write(data); // Write data to register Wire.endtransmission(); // Relinquish bus control }
I2C bus transactions can involve either 8- or 16-bit values. A careful look at the types used in the methods of Texas Instruments’ example show us that the HDC2010 is purely an 8-bit device. Consulting the documentation of the wiringpi API reveals the presence of a dedicated function intended for writing 8-bit
register values. This allows us to create a replacement: void Hdc2010::writereg(uint8_t reg, uint8_t data)
{ wiringpii2cwritereg8(myfd, reg, data);
} Reading, in principle, is done along the same lines. The Arduino API shows itself to be extremely verbose: uint8_t Hdc2010::readreg(uint8_t reg)
{ openreg(reg); uint8_t reading; // holds byte of read data Wire.requestfrom(_addr, 1); // Request 1 byte Wire.endtransmission(); // Relinquish bus control if (1 <= Wire.available())
{ reading = (Wire.read()); // Read byte } return reading; } As we dealing with 8-bit values, the readreg function can also be greatly abbreviated: uint8_t Hdc2010::readreg(uint8_t reg)
{ return wiringpii2creadreg8(myfd, reg);
}
Save the changes to the file and order another compilation process – it will still fail, but with an extremely interesting message: pi@raspberrypi:~ $ gcc Hdc2010.cpp -lwiringpi (.text+0x34): undefined reference to `main’ collect2: error: ld returned 1 exit status
When compiling a C++ program not intended to be a library, the C++ standard requires the presence of the main function – so far, we’ve focused on simply porting the classes. It’s now time to create another file and start by including the various headers required: #include
#include “HDC2010.H”
#include
After that, the actual main function can be created: int main()
{ HDC2010 sensor(0x00); sensor.begin(); sensor.reset();
Adapting the example to an C++ environment is made difficult. Most importantly, C++ code living in the Arduino environment gets invoked in relatively complex ways. Due to this, our instance of the driver class is obliged to live on the stack.
After creating it, we invoke both the begin and reset methods. Invoking reset might look superfluous at first glance, but makes good sense – when interacting with sensors, ensuring clear start-up situations is a great design pattern.
In the next step, various parameters must be written to the sensor in order to inform it about the way we want our data provided: sensor.setmeasurementmode(temp_and_humid); sensor.setrate(one_hz); sensor.settempres(fourteen_bit); sensor.sethumidres(fourteen_bit); sensor.triggermeasurement();
After the configuration is complete, it is time for harvesting the actual values. As Texas Instruments’ example driver takes care of the conversion for us, the actual information collection can be done like this: float temperature = sensor.readtemp(); temperature = sensor.readtemp(); float humidity = sensor.readhumidity(); printf(“temp: %f | Humi: %f”, temperature, humidity); return 0; }
We invoke the readtemp method twice for a reason. The HDC2010 family has a small oddity: after being configured for the first time, the temperature sensor always returns a value of -40°C. This value gets discarded immediately after the first read operation; by invoking the method twice, we can ensure that valid values are available to our program.
At this point, another invocation of GCC is needed. This time, both the driver class and the main file need to be parsed in order to prevent linker errors: pi@raspberrypi:~ $ gcc Hdc2010.cpp test.cpp -lwiringpi
When done, temperature and humidity values can be harvested from the command line:
pi@raspberrypi:~ $ ./a.out
Temp: 21.170044 | Humi: 45.507812
Should you not believe our claims about the issue with the -40°C reset, feel free to remove the second invocation of the reading function. After ordering another recompilation, the temperature value returned will no longer be valid:
pi@raspberrypi:~ $ ./a.out
Temp: -40.000000 | Humi: 45.782471
What now?
Our Raspberry Pi is able to collect both temperature and humidity information. It can be shared across the internet in a variety of ways – in our case, we used a simple proprietary protocol based on the Berkeley Socket API.
This, however, is a topic better left to software engineers. For the electrical engineering team, another interesting problem arises: given that I2C is intended to cover multiple devices, we should be able to add multiple HDC2010 instances. This raises a set of new questions, the answers to which we will introduce in another issue.
For now, we hope that you enjoyed our little experiments with the world of sensor interfacing on the Raspberry Pi.
should you, for some reason, feel uncomfortable using the wiringpi library, you can also fall back to the i2c library contained in the linux kernel. More information on this programming approach can be found at http:// bit.ly/lxf252i2c.