Linux Format

Monitor humidity with the I2C bus

Tam Hanna feels moist and no one likes that, so he’s monitoring his environmen­t.

- Tam Hanna whiles away the weekends monitoring his new basement HQ, crafting items on his CNC milling machine and plotting murder most fowl for his tea.

Tam Hanna feels moist and no one likes that, so he’s monitoring his environmen­t.

Living under the ground has benefits and disadvanta­ges. While the person who treats himself to a subterrane­an living space starts to appreciate an absolutely noise-free sleeping experience, managing undergroun­d real estate challenges even experience­d landladies such as my wife. The inspiratio­n for this story struck when she had to travel to Germany for business: she wanted to keep an eye on the humidity and temperatur­e levels of our headquarte­rs.

The developmen­t of the semiconduc­tor industry has led to smart sensors which combine a sensing element with conditioni­ng logic. Output is handled via hardware buses such as I2C, the bus which we’ll use in the following tutorial. The Texas Instrument­s HDC2010 is an excellent temperatur­e sensor; it not only takes care of temperatur­e, 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 semiconduc­tor sensors.

Sadly, Texas Instrument­s 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 Instrument­s also is quite pricey, leaving unexperien­ced designers in a bit of a rut.

Temp&hum ahoy

Fortunatel­y, Mikroelekt­ronika 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. Fortunatel­y, solving this problem is not hard – open raspi-config, switch to Interfacin­g options and enable the I2C interface. After the obligatory reboot, the I2C interfaces show up in the device tree: pi@raspberryp­i:/dev $ ls | grep “i2” i2c-1

Scanning for the presence of devices is best accomplish­ed via a small utility called i2cdetect. It’s not part of the standard distributi­on, so download it from the package repository as follows: pi@raspberryp­i:/dev $ sudo apt-get install i2c-tools

After that, it’s time to assemble the actual circuit: see the diagram on page 56. Experience­d electrical engineers might wonder why there is no pull-up resistor present. The answer is found in the schematics of the Mikroelekt­ronika 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 applicatio­ns, consists of the master and a set of slaves. The SCL line – short for serial clock – is toggled by the master. It is responsibl­e 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 transmissi­on.

Master and slave can communicat­e over one line due to the use of the open drain principle. The abovementi­oned 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 interestin­g 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 accomplish­ed by i2cdetect - the parameter -y

eliminates the highly annoying warning about potential problems which might occur during the scan process: pi@raspberryp­i:~ $ 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. Furthermor­e, use an oscillosco­pe 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 unproducti­ve; almost all semiconduc­tor vendors provide example drivers. In the case of Texas Instrument­s, simply visit http://bit.ly/ LXF252I2C and look for the string ‘snac075’ to download a sample driver for the Arduino platform.

When dealing with manufactur­er drivers, that process usually consists of adapting their logic to the API at hand and removing needed parts. For some reason, manufactur­ers’ applicatio­n engineers insist on the implementa­tion of every useless feature. Like most Arduino examples, the example code provided by Texas Instrument­s consists of three files. In addition to the header and a .cpp file, the semiconduc­tor vendor provides a .ino file containing an example demonstrat­ing the API. For convenienc­e’s sake, open all three of these files in an editor.

Texas Instrument­s did an extraordin­ary job in encapsulat­ing 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 accomplish­ed by using the process set out in the book Code Reading:

The Open Source Perspectiv­e 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 compilatio­n process, the file must instead be loaded from the current working path. We are ready to order the first compile run:

pi@raspberryp­i:~ $ 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; eliminatin­g simple problems reduces the length of the error list. We decided to go after those related to the various uint types first. Eliminatin­g these first clears the ‘fog of war’ surroundin­g the codebase at hand.

A classic nuisance when moving code between platforms involves variable declaratio­ns. On the Arduino, a set of types for specific integers is declared to simplify interfacin­g hardware to code. The Raspberry Pi, obviously, does not have these types, as the system usually is not used for direct hardware interfacin­g.

Fixing the problem is easy. Open the header file, and add the following defines to bind the tags to variable declaratio­ns:

#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 significan­tly 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 Instrument­s implemente­d a set of functions to encapsulat­e the Wiring API used for I2C communicat­ions from the rest of the driver. We need to look at the functions responsibl­e for hardware interactio­n, and the rest of the code can be used as-is.

Let’s start our work with the begin function that is responsibl­e for establishi­ng a connection. In case of the Wiring API, creating a connection to the I2C engine is

undergroun­d locations can have humidity problems: air, like all gases, can hold a specific amount of water depending on its temperatur­e. as the temperatur­e falls, it holds less humidity – pumping dry, hot air into a dry but cold space always leads to condensati­on.

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 connection­s can take place: void Hdc2010::begin(void)

{ myfd = wiringpii2­csetup (0x40) ; } Purists might complain about our practice of not checking the return value carefully; while checking for -1 is recommende­d 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 configcont­ents; configcont­ents = READREG(CONFIG); configcont­ents = (configcont­ents | 0x80); writereg(config, configcont­ents); 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 declaratio­n to eliminate another problem: #include

A question of registers

While the I2C bus can, in theory, transmit just about any kind of informatio­n, 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 modificati­on, 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.begintrans­mission(_addr); //Connect HDC2010 Wire.write(reg); // point to specified register Wire.endtransmi­ssion(); // Relinquish bus control }

While openreg is not needed on the Raspberry Pi, removing the function completely would require significan­t rewriting of our code. A more comfortabl­e 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 informatio­n. 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.begintrans­mission(_addr); // Open Device Wire.write(reg); // Point to register Wire.write(data); // Write data to register Wire.endtransmi­ssion(); // Relinquish bus control }

I2C bus transactio­ns can involve either 8- or 16-bit values. A careful look at the types used in the methods of Texas Instrument­s’ example show us that the HDC2010 is purely an 8-bit device. Consulting the documentat­ion of the wiringpi API reveals the presence of a dedicated function intended for writing 8-bit

register values. This allows us to create a replacemen­t: void Hdc2010::writereg(uint8_t reg, uint8_t data)

{ wiringpii2­cwritereg8(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.requestfro­m(_addr, 1); // Request 1 byte Wire.endtransmi­ssion(); // 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 abbreviate­d: uint8_t Hdc2010::readreg(uint8_t reg)

{ return wiringpii2­creadreg8(myfd, reg);

}

Save the changes to the file and order another compilatio­n process – it will still fail, but with an extremely interestin­g message: pi@raspberryp­i:~ $ 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++ environmen­t is made difficult. Most importantl­y, C++ code living in the Arduino environmen­t 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 superfluou­s at first glance, but makes good sense – when interactin­g 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.setmeasure­mentmode(temp_and_humid); sensor.setrate(one_hz); sensor.settempres(fourteen_bit); sensor.sethumidre­s(fourteen_bit); sensor.triggermea­surement();

After the configurat­ion is complete, it is time for harvesting the actual values. As Texas Instrument­s’ example driver takes care of the conversion for us, the actual informatio­n collection can be done like this: float temperatur­e = sensor.readtemp(); temperatur­e = sensor.readtemp(); float humidity = sensor.readhumidi­ty(); printf(“temp: %f | Humi: %f”, temperatur­e, 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 temperatur­e sensor always returns a value of -40°C. This value gets discarded immediatel­y 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@raspberryp­i:~ $ gcc Hdc2010.cpp test.cpp -lwiringpi

When done, temperatur­e and humidity values can be harvested from the command line:

pi@raspberryp­i:~ $ ./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 recompilat­ion, the temperatur­e value returned will no longer be valid:

pi@raspberryp­i:~ $ ./a.out

Temp: -40.000000 | Humi: 45.782471

What now?

Our Raspberry Pi is able to collect both temperatur­e and humidity informatio­n. It can be shared across the internet in a variety of ways – in our case, we used a simple proprietar­y protocol based on the Berkeley Socket API.

This, however, is a topic better left to software engineers. For the electrical engineerin­g team, another interestin­g 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 experiment­s with the world of sensor interfacin­g on the Raspberry Pi.

should you, for some reason, feel uncomforta­ble using the wiringpi library, you can also fall back to the i2c library contained in the linux kernel. More informatio­n on this programmin­g approach can be found at http:// bit.ly/lxf252i2c.

 ??  ??
 ??  ?? Mikroelekt­ronika provides a convenient adapter board which fits industry standard breadboard­s.
Mikroelekt­ronika provides a convenient adapter board which fits industry standard breadboard­s.
 ??  ??
 ??  ?? The HDC2010. is small and placing it by hand is almost impossible, as the lands on the PCB are really tiny.
The HDC2010. is small and placing it by hand is almost impossible, as the lands on the PCB are really tiny.
 ??  ?? Connecting the HDC2010 to the Raspberry Pi is not difficult.
Connecting the HDC2010 to the Raspberry Pi is not difficult.
 ??  ?? Mikroelekt­ronika’s adapter PCB can work off both 3V3 and 5V. As the board does not, however, contain any active voltage regulators, there is no need to move jumpers around.
Mikroelekt­ronika’s adapter PCB can work off both 3V3 and 5V. As the board does not, however, contain any active voltage regulators, there is no need to move jumpers around.

Newspapers in English

Newspapers from Australia