1-Wire Humidity Monitoring on the Raspberry Pi

First published: 18th July 2013

After setting up 1-wire temperature monitoring and I2C humidity monitoring on the RPi, it was time to do 1-wire humidity monitoring.

Hardware

A scrap of matrix board was used as a base. The DS2438 is a surface-mount chip, a bit bigger than the BAT54S I found a challenge to solder for the 1-wire interface, but it has 8 pins. Fortunately, only the four at the corners are required for this circuit.

The HIH-5030-001 reports the humidity as a voltage, and the DS2438 converts that to a digital value, and also provides a temperature reading. R1 and C1 form a low-pass filter to keep the reading steady. C2 is a 10µF tantalum to keep the power steady on long cable runs.

The circuit is wired to a 3.5mm jack socket, the same as the temperature probe, but these components won't fit inside the plug body. Instead, I used an empty dental floss container, with holes drilled so that there would be free airflow, wired to the plug with about 10cm of telephone flex.

Software

Loading the required kernel modules (w1-gpio and w1-therm) was discussed for the temperature probe, but it is easier to add them to /etc/modules so that they are loaded at boot time. The probe gets recognised when it is plugged in, and can be found in the filesystem:

cd /sys/bus/w1/devices
ls
10-0008029dbea3  10-000802a49270  10-000802a5390d  w1_bus_master1
10-000802a46697  10-000802a4fe3d  26-0000016027b3

Here, you can see I have five DS18S20 temperature sensors (family code 10) and one DS2438 (family code 26), which is my humidity sensor.

Reading the Data

The 1-Wire driver creates files for accessing the device:

cd /sys/bus/w1/devices/26-0000016027b3
ls -l
total 0
lrwxrwxrwx 1 root root    0 Jul 18 10:42 driver -> ../../../bus/w1/drivers/w1_slave_driver
-r--r--r-- 1 root root 4096 Jul 18 10:42 id
-r--r--r-- 1 root root 4096 Jul 18 10:42 name
drwxr-xr-x 2 root root    0 Jul 18 10:42 power
-rw-r--r-- 1 root root 4096 Jul 18 16:08 rw
lrwxrwxrwx 1 root root    0 Jul 18 10:42 subsystem -> ../../../bus/w1
-rw-r--r-- 1 root root 4096 Jul 18 10:42 uevent

The Linux kernel documentation for the w1.generic driver explains that the critical file is rw which is "created for slave devices which do not have appropriate family driver. Allows to read/write binary data." I did not find any further explanation. Could it be as simple as writing commands to the file, and then reading the data I expect. Not quite.

The Dallas datasheets explain the 1-wire protocol in detail. To talk to a particular device, the controller must issue a reset pulse, wait for a presence pulse, issue a Match ROM command and the device serial number, and then issue a Memory/Control command and perhaps read or write data. I assumed that the w1.generic driver would take care of the device selection, because, if I write to the (for example) 26-0000016027b3/rw file, I have told it which device I am trying to access.

The DS2438 has 8 pages of memory, each of 8 bytes. For this application, everything I want is on page 0:

ByteContentR/W
0Status / ConfigurationR/W
1Temperature LSBR
2Temperature MSBR
3Voltage LSBR
4Voltage MSBR
5Current LSBR
6Current MSBR
7ThresholdR/W

The data I need to calculate humidity is the supply voltage (VDD), the voltage output of the HIH-5030-001 (VAD), and the temperature. Before reading the data, it is necessary to tell the DS2438 to do an analogue to digital conversion. There are separate commands to start a temperature and a voltage conversion. Also, the status / configuration byte controls which voltage to read and reports when the conversions are finished:

BitAbbr.ValuesDescription
0IAD1 = enabledCurrent A/D Control
1CA1 = enabledCurrent Accumulator configuration
2EE1 = shadowCurrent Accumulator Shadow Selector
3AD1 = VDD, 0 = VADVoltage A/D Input Select
4TB1 = busyTemperature Busy Flag
5NVB1 = busyNonvolatile Memory Busy
6ADB1 = busyA/D Converter Busy
7X not used

As another complication, the memory cannot be read from or written to directly. There are scratch pads, and commands to copy a scratchpad to memory, and recall memory to a scratchpad. My initial attempts to communicate with the DS2438 failed. The problem seemed to be knowing when I could issue the next command. I hit on the idea of, after sending each command, reading from the file until it returns a non-zero bit. The DS2438 with a 0 is it is busy, and 1 if it is idle. My working Perl fragment is:

use Digest::CRC;

sub DS2438reader {
  my ($devf) = @_;
  my $rtime= time;
  print STDERR "Reading DS2438 / HIH-5030-001 $devf\n" if $opt_d;
  unless (open( DS2438, "+>$devf")) {
    print STDERR "Unable to open $devf $!\n";
    return;
  }
  my $oldfh = select DS2438; $|= 1; select $oldfh; # autoflush
  my ($status, $vad, $vdd, $temp, $humid);
  print DS2438 "\x4e\x00\x00";
  for (my $b="\x00"; $b eq "\x00"; ) { read DS2438, $b, 1; }
  print DS2438 "\x48\x00";
  for (my $b="\x00"; $b eq "\x00"; ) { read DS2438, $b, 1; }
  print DS2438 "\x44";
  for (my $b="\x00"; $b eq "\x00"; ) { read DS2438, $b, 1; }
  print DS2438 "\xb4";
  for (my $b="\x00"; $b eq "\x00"; ) { read DS2438, $b, 1; }
  print DS2438 "\xb8\x00";
  for (my $b="\x00"; $b eq "\x00"; ) { read DS2438, $b, 1; }
  print DS2438 "\xbe\x00";
  ($status, $temp, $vad)= readTVC();
  return unless defined($vad);
  if ($status) {
    print STDERR "DS2438 incorrect status reading VAD: " . sprintf( "%02d", $status);
    return;
  }
  print DS2438 "\x4e\x00\x08";
  for (my $b="\x00"; $b eq "\x00"; ) { read DS2438, $b, 1; }
  print DS2438 "\x48\x00";
  for (my $b="\x00"; $b eq "\x00"; ) { read DS2438, $b, 1; }
  print DS2438 "\xb4";
  for (my $b="\x00"; $b eq "\x00"; ) { read DS2438, $b, 1; }
  print DS2438 "\xb8\x00";
  for (my $b="\x00"; $b eq "\x00"; ) { read DS2438, $b, 1; }
  print DS2438 "\xbe\x00";
  ($status, $temp, $vdd)= readTVC();
  return unless defined($vdd);
  if ($status ^ 0x08) {
    print STDERR "DS2438 incorrect status reading VDD: " . sprintf( "%02d", $status);
    return;
  }
  if ($vad == $vdd) {
    print STDERR "Error D008: DS2438 voltage match $vad";
    return;
  }
  print "H read ", (($vad / $vdd) - 0.1515) / 0.00636, "\tComp ",
        1.0546 - 0.00216 * $temp, "\n" if $opt_d;
  $humid= ($vad / $vdd - 0.1515) / 0.00636 / (1.0546 - 0.00216 * $temp);
  print localtime() . " Temperature $temp\tHumidity $humid\n" if $opt_d;

  return { temp => $temp, humid => $humid, time => (time - $rtime) };
}

sub readTVC {
  my $data;
  read DS2438, $data, 9; # and CRC
  my ($status, $t, $v, $c, $th, $crc) = unpack('CvvvCC', $data);
  printf "TVC raw %02x %02x %02x %02x %02x %02x %02x %02x %02x\n",
        unpack('C*', $data) if $opt_d;
  print "TVC $status, $t, $v, $c, $th, $crc\n" if $opt_d;
  unless (crcCheck(substr($data, 0, -1), substr($data, -1))) {
    print STDERR "Error D009: DS2438 CRC error $status $t $v $c $th $crc";
    return;
  }
  my $temp = ($t >> 3) * 0.03125;
  print "T $temp\tV $v\n" if $opt_d;
  return $status, $temp, $v;
}

sub crcCheck {
  # Dallas/Maxim CRC8
  my ($r, $c) = @_;
  my $ctx = Digest::CRC->new(width => 8, poly => 0x31, init => 0x00, xorout => 0x00, refin => 1, refout => 1);
  $ctx->add($r);
  my $crc= $ctx->digest;
  if ($opt_d) {
    my @data = unpack('C*', $r);
    printf "crcCheck %d %d | %02x %02x %02x %02x %02x %02x %02x %02x |\n",
        ord($c), $crc, @data;
  }
  print "CRC ", (ord($c) == $crc), " ", ord($c), " ", $crc, "\n" if $opt_d;
  return (ord($c) == $crc);
}

(This can be used to replace the dummy DS2438reader() routine in my temperature reader)

The script does various error checking, and, if something is wrong, the subroutine returns nothing. crcCheck() implements Dallas' CRC, using the Digest::CRC module. Initially, I found that the humidity would occasionally be reported as about 131%, I found this was because the change of the status register to select VDD or VAD had failed, so the two voltages were from the same input and therefore the same (or very close), so I added the checks to throw away the data if the voltages were the same, or if the status register showed the wrong input selected.

The voltages are read in mV, but the calculation of humidity replies on their ratio, so I did not bother converting both to volts. There are four constants in the humidity calculation, these are from the HIH-5030-001 datasheet. Other Honeywell humidity sensors have different constants, ideally, these would be stored in a database table indexed by the DS2438 serial number.

The script also returns the elapsed time in seconds, this is the big problem with this method, getting one reading takes about 26 seconds! It turns out that each time I use my idea of reading from the file until I get non-zero it takes 2 seconds. I would like to use a more intelligent way of getting the 1-wire bus to do what I want, I imagine there might be some way of using ioctl() to issue reset pulses and other low-level events, but I have not found any documentation describing it.

So, the humidity sensor is working (slowly), but I would like to improve the read time. Either by finding out how to use the rw file interface correctly, or by taking the w1-therm module source and rewriting it.

Updated: 20th September 2016

Well, I did say that the code here was untested. Thanks to Gerd Mevissen for getting me to check it. The errors were:

  1. Use of an undeclared function log_incident(), replaced with print STDERR

  2. Unnecessary second close parenthesis before the semi-colon:

    print STDERR "DS2438 incorrect status reading VDD: " . sprintf( "%02d", $status);

Updated: 13th March 2017

Mariusz Białończyk has written a kernel module for the DS2438, find out more about it on his blog. It fixes the big problem with my perl script: slow read time.


Gallery

1-wire humidity probe schematic1-wire humidity probe schematic
Humidity probe, case openHumidity probe, case open
Humidity probe in dental floss caseHumidity probe in dental floss case
Share