Wednesday 12 August 2015

Field Mate part 3 - Digital Humidity and Temperature

As a part of this project, I wanted to add in an environmental hygrometer as part of the environmental sensorium, and ordered a DHT-11.

The device is itself a hybrid, containing a chip, a capacitive humidity sensor and a thermistor. It is actually cheaper to order these things attached to a small circuit board (breakout board) intended for hobbyists - especially as they include the recommended resistor and capacitor, and are thus ready for plug in and go.

While the device sports four pins, only three of them are in use.

Unlike most devices which produce an analog output, or those that are accessed using I2C or SPI data buses, these little boxes of tricks have a single-wire serial interface that is, frankly, difficult to use.

The Internet maker community to the rescue! I was originally going to use a PIC microcontroller to turn the DHT-11 into an I2C device, but I found that there is a library available for the Arduino (since that is what I am currently using).

A few false starts, and I have access to the world of Relative Humidity and Temperature measurement. From that, it is possible to calculate Dew Point (the temperature at which that atmospheric moisture would begin to condense) and Heat Index (the apparent temperature of the atmosphere). Neither of these numbers are easy to convert using a calculator although they only take a few lines of code in a program.

Before I deal with the software, there are a couple of caveats regarding the DHT series of sensors -

The capacitive humidity sensor absorbs moisture rapidly, but will give it up fairly slowly, so breathing into the device, getting it wet or taking it into a steamy kitchen is going to have an effect that lasts a couple of hours.

The DHT-11 is integer only (which I didn't realise), but the data capture library is compatible with the whole of the DHT range (DHT-11, -21, -22, -33 & -44), which means that they are plug compatible with both software and hardware. Upgrade to the higher precision devices is therefore quite simple.

The DHT-xx series are rather slow devices, and so should not be interrogated too frequently - this is taken care of quite naturally by the device library, as it delays other program activity until the data has been read (blocking device).


The library and more information is available from the Arduino Playground -

 The latest edition of the library is available from Rob Tillaart on GitHub -

The source code for the wrapper which I am using, based on Rob's examples and other contributions, follows:

// dhtwrap.h
//
// ####################################################################
// ####################################################################
// ##
// ## 
dhtwrap.ino (dhtwrap.h)
// ##  version 1.00.00
// ##  date    12-Aug-2015
// ##  author  Alysson Rowan (AlyssonR)
// ##   BASED on the work of Ron Tillaart
// ## 
// ##   alyssonrowan @ gmail.com
// ##
// ####################################################################
// ####################################################################
// ##
// ##  This program is provided on an as-is basis without warranty.
// ##  No claim is made as toward operability or fitness for purpose
// ##  whatsoever. No liability can be accepted for any loss or damages
// ##  howsoever caused in respect of this software.
// ##
// ##  This software is made freely available under the terms of the
// ##  GNU General Public License V2. In short, you are allowed to do
// ##  anything with this program except sell it and hide the
// ##  source code.
// ##
// ##  In addition, this program is "postcard ware" – if you find it
// ##  useful then please send me an e-mail and tell me about your
// ##  application.
// ##
// ##  Your comments and suggestions are much appreciated.
// ##
// ####################################################################
// ####################################################################

// ####################################################################
// ####################################################################
// ####################################################################
// ####################################################################
// ##
// ##  DEPENDENCIES:
// ##  -------------
// ##
// ## DHTlibrary by Rob Tillaart -
// ## Latest version is available at:
// ##                   https://github.com/RobTillaart/Arduino
// ##
// ## Library files at:
// ##                   https://github.com/RobTillaart/Arduino/tree/master/libraries/DHTlib
// ##
// ## File tested with DHT library version 0.1.20




#include
#define DHTLIB_ERROR_DEVICE     -9  // - additional error code

dht DHT;

double DHTtemperature;  // Return values from DHTxx device.
double DHThumidity;
double DHTdewpoint;
double DHTheatindex;
int DHTerror;

void DHTGet(int DHTmodel, int DHTpin ) {
// Exit error codes:
// DHTerror =    0 :  No error
// DHTerror =   -1 :  Checksum error
// DHTerror =   -2 :  Timeout
// DHTerror =   -3 :  Connection error
// DHTerror =   -4 :  ACK-L error
// DHTerror =   -5 :  ACK-H error
// DHTerror =   -9 :  Invalid device selection
// DHTerror = -999 :  Unspecified Error

  int chk;
  switch (DHTmodel) { // Get data from the DHTxx module - using the appropriate read function.
 
    case 11:
                chk = DHT.read11(DHTpin);
                break;
    case 21:
                chk = DHT.read21(DHTpin);
                break;
    case 22:
                chk = DHT.read11(DHTpin);
                break;
    case 33:
                chk = DHT.read33(DHTpin);
                break;
    case 44:
                chk = DHT.read44(DHTpin);
                break;
    default:
                chk = DHTLIB_ERROR_DEVICE;
                break;
  }           
 
  DHTtemperature = -999.0; // Set default (ERROR values, rather than end up with NaN
  DHThumidity    = -999.0;
  DHTdewpoint    = -999.0;
  DHTheatindex   = -999.0;

  switch (chk) { // Error test and set the return data

    case DHTLIB_OK: 
                DHTtemperature = DHT.temperature;
                DHThumidity = DHT.humidity;
                DHTdewpoint = dewPoint(DHT.temperature, DHT.humidity);
                DHTheatindex = heatIndex(DHT.temperature, DHT.humidity);
                DHTerror = DHTLIB_OK;
        break;
    case DHTLIB_ERROR_CHECKSUM:
                DHTerror = DHTLIB_ERROR_CHECKSUM;   
        break;
    case DHTLIB_ERROR_TIMEOUT:
                DHTerror = DHTLIB_ERROR_TIMEOUT;
        break;
    case DHTLIB_ERROR_DEVICE:
                DHTerror = DHTLIB_ERROR_DEVICE;
                break;   
    default:
               DHTerror = chk;
        break;
  }
}


// dewPoint function NOAA
// reference (1) : http://wahiduddin.net/calc/density_algorithms.htm
// reference (2) : http://www.colorado.edu/geography/weather_station/Geog_site/about.htm
// Thanks to Rob Tillaart who came up with this.

double dewPoint(double celsius, double humidity)
{
    // (1) Saturation Vapor Pressure = ESGG(T)
    double RATIO = 373.15 / (273.15 + celsius);
    double RHS = -7.90298 * (RATIO - 1);
    RHS += 5.02808 * log10(RATIO);
    RHS += -1.3816e-7 * (pow(10, (11.344 * (1 - 1/RATIO ))) - 1) ;
    RHS += 8.1328e-3 * (pow(10, (-3.49149 * (RATIO - 1))) - 1) ;
    RHS += log10(1013.246);

        // factor -3 is to adjust units - Vapor Pressure SVP * humidity
    double VP = pow(10, RHS - 3) * humidity;

        // (2) DEWPOINT = F(Vapor Pressure)
    double T = log(VP/0.61078);   // temp var
    return (241.88 * T) / (17.558 - T);
}



// heatIndex function NOAA
// reference (1) : http://www.colorado.edu/geography/weather_station/Geog_site/about.htm
// Thanks to Rob Tillaart who came up with this faster version.

double heatIndex(double celsius, double humidity)
{
  double farenheit = (celsius * 1.8) + 32;
  double farenheitsq = farenheit * farenheit;
  double humiditysq = humidity * humidity;
  double heatindex = celsius;
  if ((farenheit >= 80) and (humidity >= 40)) {
    //Heat index is only valid for temperatures 80F upwards AND for R.H. 40% upwards
   
    double c1 = -42.379, c2 = 2.049015, c3 = 10.14333, c4 = -0.224755, c5 = -6.83783e-3 ;
    double c6 = -5.481717e-2, c7 = 1.22874e-3, c8 = 8.5282e-4, c9 = -1.99e-6 ;

    double A = (( c5 * farenheit) + c2) * farenheit + c1;
    double B = (((c7 * farenheit) + c4) * farenheit + c3) * humidity;
    double C = (((c9 * farenheit) + c8) * farenheit + c6) * humidity * humidity;
    heatindex = A + B + C;
   
    heatindex = (heatindex - 32) /1.8; // We are using Celsius throughout.
  }
  return heatindex;

While it is usually considered most improper, I tend to use global variables for communicating blocks of related data between parts of the program. This makes things simpler for beginning users. I could have used a structured variable, but that makes the wrapper more complicated for a novice user. By calling

DHTGet(deviceSuffix, DigitalPinNumber);

then the temperature, relative humidity, dew point and heat index will be placed in  the variables

DHTtemperature, DHThumidity, DHTdewpoint and DHTheatindex respectively. The error status will be in DHTerror, and will be zero if there is no error.

All temperatures are expressed in degrees Celsius, the relative humidity is expressed as a percentage.

Thursday 6 August 2015

Field Mate Part 2 - Open Your Eyes to an Open Sensorium

A few posts past, I addressed the idea of Field Buddy Field Mate - a portable device for capturing environmental data while in the field. Having had a chance to play with a bit of magnetometry (amongst other toys), and finding that I need to build an intelligent thermostat, I have come to the conclusion that there is something lacking in the world of hobby/amateur electronics - and that is a properly integrated set of devices and software for microcontroller use.

I have decided, therefore, that it is time to formalise the Field Mate concept into a more general project.

To this end, I have decided to commence work on what I am calling The Open Sensorium Project.

The aim is to produce a series of software and hardware modules that can be built up and put together in order to build custom instrumentation. Based around Data Capture using sensors (i.e. a sensorium), the modules will provide both sensing, primary processing and export of data via displays, data streaming and data caching (using SD cards).

It will not only provide an open sensorium, in the sense that the data system's "eyes" are wide open, but will also be an open project - open source software, open source hardware (albeit, using a lot of off-the-shelf modules) and free to use and modify.

Given the relatively low cost of microcontrollers, it would be possible for each major module to be separately intelligent.

I hope that there will be a lot of cross-pollination with various other projects, and between the developers using a range of microcontroller systems including, but not limited to, Microchip's PIC, Parallax Propeller, Atmel AVC / Arduino and Raspberry Pi,

As an open project, the material will be released into the wild with few restrictions, and subject to the Gnu General Public Licence.

The rules will be simple - open source may not form a part of a closed source project unless those portions that are open source remain open source. Derivative works are brilliant. Respect and acknowledge the intellectual rights of those whose work you are building upon - and retain any copyright notices that form a part of the source that you are using.

Finally, there will be standards for various parts of the project - standards for quality of product, for quality of documentation and for communication protocols between modules - and to the outside world.


I hope that others will want to get involved in various ways - even if it is only through an eMail saying that you found it useful.

I look forward to hearing from you. I will pass on the web site address once I have settled upon one. I have set up a project page on Sourceforge at: https://sourceforge.net/p/open-sensorium/



For those who care about these things - the font is Neuropolis, the logos were made in MS Word 2013 and either screen-captured or copied to Inkscape.

Sunday 26 July 2015

Photodiodes as illuminometers

Continuing with the theme of experimenting with simple, home-brewed sensors, I was wondering about how to produce a wide-range illuminometer (or a Luxmeter) with the speed of silicon and the broad range of the human eye.

By and large, electronic detectors are incapable of making meaningful measurements of physical quantities over many orders of magnitude - they tend to either be insensitive at low levels or easily swamped at high levels ... or they break when you try to measure something far too big (I think that everyone has burned out a multimeter or two this way).

One of the guiding principles of electronics is to attempt to design a measuring device that uses a linear characteristic. This is fine for many instruments - and it works for a photodiode luxmeter, provided that you know that the range of measurement is within the compass of your device.

But, what about using a logarithmic characteristic - this will extend the measurement range of the device, but at the cost of having to perform mathematical work on the raw numbers.

As we saw in my previous post on thermistors as thermometers, the use of software simplifies what would have been a pretty horrible piece of electronic design, especially where the equation relating some measured value with the quantity we want to measure is well known.

Unfortunately, no one seems to share their thoughts on using the open-circuit, photovoltaic mode for photodiodes - they prefer to use transimpedance amplifiers, and their additional circuitry.

So, to the simplest passive circuit imaginable...
Vout is connected to an analog input of the microcontroller. The circuit approximates an open-circuit since the analog input of most modern sensor chips (including microcontrollers) is an extremely high impedance CMOS (or similar) device.

According to my understanding, the relevant equation is:


Not an easy one, but certainly not as nasty as the Steinhart-Hart equation for thermistors.

V0 (dark voltage) can be easily determined by putting a piece of foil over the photodiode; a and d, two arbitrary constants, can be left (for now) as 1 and zero respectively - they can be used to calibrate the photodiode later.

More problematical are k and c, two more arbitrary constants. It is possible to determine these experimentally, but a bit of creative guesswork with trial and error will do almost as well (at least for now).

In actual fact, for the sake of ease of tweaking, there is another constant that I have used, which allows for the slope of the equation to be altered after adding in any offset. This trick only works because there is no negative illuminance possible for this system and the illuminance value should pass through the origin. The new equation is therefore:


The relevant line in the code is the assignment to the variable, luxmeterLux, near the end of the code.

As previously, the code is presented as a header file luxmeter.h, and the constants #defined in the preamble. I am keen for anyone with a better understanding of the physics (or the maths) to poke holes or make improvements in the mathematics of this code snippet.

Please see the notes below the code for a couple of caveats for more advanced users ...





// luxmeter.h



// ####################################################################

// ####################################################################

// ##

// ##  luxmeter.ino (luxmeter.h)

// ##  version 1.00.00

// ##  date    26-Jul-2015

// ##  author  Alysson Rowan (AlyssonR)

// ##

// ##  Copyright (C) 2015, Alysson Rowan

// ##   alyssonrowan @ gmail.com

// ##

// ####################################################################

// ####################################################################

// ##

// ##  This program is provided on an as-is basis without warranty.

// ##  No claim is made as toward operability or fitness for purpose

// ##  whatsoever. No liability can be accepted for any loss or damages

// ##  howsoever caused in respect of this software.

// ##

// ##  This software is made freely available under the terms of the

// ##  GNU General Public License V2. In short, you are allowed to do

// ##  anything with this program except sell it and hide the

// ##  source code.

// ##

// ##  In addition, this program is "postcard ware" – if you find it

// ##  useful then please send me an e-mail and tell me about your

// ##  application.

// ##

// ##  Your comments and suggestions are much appreciated.

// ##

// ####################################################################

// ####################################################################



// ***************************\           /****************************

// ***************************** WARNING ******************************

// ***************************/           \****************************

//

// The function analogLuxmeterSacaled returns a value in units AIU

// Arbitrary Illimination Units – which may approximate Lux.

//

// This is because I have limited resources for calibrating a luxmeter

// of this (or any other) type. Linearity and scaling is not guaranteed

// (or even terribly likely).





// The following constants were determined mainly by trial and error ...



#define luxmeterPort A0          // analog input pin assignment

#define luxmeterVdrk 0.02        // Dark voltage on photodiode

#define luxmeterCorr 1.0         // Calibration scaling factor

#define luxmeterOffs -0.292      // Calibration offset

#define luxmeterSlope .1573      // a slope correction



// Photovoltaic constants coefficients

// – these magic numbers are for an S1226 (Hammamatsu) photodiode

//   your mileage may vary!



#define luxmeterConA 23.791    // coefficient a

#define luxmeterConB -1.0     // coefficient b



// set up analog luxmeter

void analogLuxmeterEnable()

    {

    analogReference(DEFAULT);

    pinMode(luxmeterPort, OUTPUT);

    digitalWrite(luxmeterPort, LOW);

    pinMode(luxmeterPort, INPUT);

    analogRead(luxmeterPort);

    }



// read analog luxmeter

float analogLuxmeterRead()

    {

    int luxmeterRaw;

    float luxmeterScaled;



    luxmeterRaw = analogRead(luxmeterPort);

    // integer value in range 0 – 1023



    luxmeterScaled = luxmeterRaw * 0.00488759;

    // magic number converts integer reading to millivolts

    return luxmeterScaled;

    }



// convert millivolts to Lux (or something similar)

float analogLuxmeterConvert( float luxmeterScaled)

    {

    float luxmeterLux;

    luxmeterLux = (luxmeterCorr * (exp(luxmeterConA * (luxmeterScaled - luxmeterVdrk) + luxmeterConB)) + luxmeterOffs) * luxmeterSlope;

    return luxmeterLux;

    }

The caveats (cautions) ... which could also provide interesting further development ...

The value V0, or the voltage generated by the photodiode when in the dark, is temperature dependent. Indeed, this fact was used in  several machines to measure temperature before accurate thermoresistive (thermistor) devices were readily available. A matching photodiode with a foil cover could be used to provide temperature compensation for this sensor.

Photometer photodiodes can be damaged by prolonged exposure to excessive light levels - please don't invest in an expensive device unless you are certain that it is what you need. Mine were taken from scrap equipment, and are therefore, effectively, disposable .

Take care to ensure that the photodiode is correctly oriented - reverse polarity could damage your analog input.

Saturday 25 July 2015

Thermistors as thermometers


 

I am taking a break from work of my microscope eyepiece project, and have been playing around with sensors using the Arduino embedded systems.

The device that is the most idiotically complicated to get meaningful information from is the humble thermistor.


Negative Coefficient (NTC) thermistors are used because of their large resistance change with temperature - as the temperature of the device increases, the resistance of the device decreases.

Unfortunately, the relationship between resistance and temperature is far from linear.

Indeed, the Steinhart-Hart Thermistor Equation is the kind of thing that causes nightmares in undergraduate students. Three coefficients of uncertain origin, natural logs of resistances and complicated denominators do not make for a happy user.

Happily, there is a solution.

For the Arduino user, there is a simple circuit to build (right) which is a thermistor analog breakout, and can be made on a thumbnail sized scrap of stripboard. You will need to select a resistor for R2 that matches (approximately, at least) the room-temperature resistance of the thermistor.

The 0V and +5V connections go to the power rails of the microcontroller circuit, and the Vout connection to one of the analog inputs (I use A1 for this).

What you will be doing is measuring the resistance of the thermistor by measuring the voltage drop across R2.


The real work is done in code. I built a simple test program around the code below, and ended up with a thermometer that is both fairly accurate and fairly stable.

Electrical noise throws out a series of fluctuations in the recorded value of Vout  - noise that may be masked by averaging a number of readings, and then using a time-weighted average for the output. This has the effect of making the thermometer slow to react to sudden changes in temperature, but overall, I was happy with the results.

A common mistake with using the Arduino analog ports is due to the nature of the way in which the port reports the voltage - as an integer from zero to 1023. Many users use the formula  -


V = AnalogValue *  Vref  / 1024

This is, of course, incorrect, and should read:


V = AnalogValue *  Vref  / 1023

I have added a pair of calibration constants into the program in order to allow for small shifts in the calibration of the thermistor - slope and offset. The linearity of the output is beyond the average user's facility to correct. The three coefficients in the Steinhart-Hart Equation are represented by three magic numbers that are typical for most modern NTC thermistors - mucking about with these (or using atypical components) will certainly lead to frustration and inevitably to tears before bedtime.



The code, which is probably what you are interested in follows ...


// thermistor.h


// ####################################################################

// ####################################################################

// ##

// ##  thermistor.ino (thermistor.h)

// ##  version 1.00.00

// ##  date    25-Jul-2015

// ##  author  Alysson Rowan (AlyssonR)

// ##

// ##  Copyright (C) 2015, Alysson Rowan

// ##   alyssonrowan @ gmail.com

// ##

// ####################################################################

// ####################################################################

// ##

// ##  This program is provided on an as-is basis without warranty.

// ##  No claim is made as toward operability or fitness for purpose

// ##  whatsoever. No liability can be accepted for any loss or damages

// ##  howsoever caused in respect of this software.

// ##

// ##  This software is made freely available under the terms of the

// ##  GNU General Public License V2. In short, you are allowed to do

// ##  anything with this program except sell it and hide the

// ##  source code.

// ##

// ##  In addition, this program is "postcard ware" – if you find it

// ##  useful then please send me an e-mail and tell me about your

// ##  application.

// ##

// ##  Your comments and suggestions are much appreciated.

// ##

// ####################################################################

// ####################################################################
 




#define thermistorPort A1          // analog input pin assignment

#define thermistorRefR 9630        // resistance of buffer resistor

#define thermistorNRes 10000       // Nominal resistance of thermistor

#define thermistorCorr 1           // Calibration scaling factor

#define thermistorOffs 0           // Calibration offset



// Steinhart-Hart coefficients

// – these magic numbers are fairly close for most modern NTC Thermistors

#define thermistorConA 0.001129148     // coefficient a

#define thermistorConB 0.000234125     // coefficient b

#define thermistorConC 0.0000000876741 // coefficient c



float tempAvge; // Temperature averaged over time







// Temperature Conversions:

//

// To convert Kelvin to Celsius

// Celsius = Kelvin – 273.15;

//

// To convert Celsius to Farenheit

// Farenheit = (Celsius * 1.8000) + 32;



// set up analog thermistor

void analogThermistorEnable()

    {

    analogReference(DEFAULT);

    pinMode(thermistorPort, OUTPUT);

    digitalWrite(thermistorPort, LOW);

    pinMode(thermistorPort, INPUT);

    analogRead(thermistorPort);

    }



// Noise reduction using time-weighted averaging

void thermistorGetTemp(int tempWeight)

  // tempWeight controls the weight that the current  reading has

  // in tempAvge.

  //    tempAvge = 0 produces "current temperature only"

  //    tempAvge = 1 => 50% weighting (standard weighting)

  //    tempAvge = 2 => 33.33% weighting etc.

  // NB: at tempAvge = 4, it takes about 30 readings for the temperature to

  //     stabilise on initialisation and after a sudden change in temperature

  {

  float temperature;

  temperature = (analogTemp(analogThermistorRead())-273.15);

  tempAvge = ((tempAvge * tempWeight) + temperature) / (tempWeight+1);

  }



// read analog thermistor

float analogThermistorRead()

    {

    float thermistorRaw;

    thermistorRaw = analogRead(thermistorPort); 
                   // integer value in range 0 – 1023

    // NB: noise results in periodic variation of reading of up to 0.75 degrees

    // noise is reduced markedly by averaging a number of readings

    for( int count = 1; count < 8; count++)

      {

      thermistorRaw = (thermistorRaw + analogRead(thermistorPort))/2;

      }

    return thermistorRaw;

    }



//  calculate temperature from measured voltage

float analogTemp(float voltageRaw)

    {

    float tempLn; // Natural log of temperature

    float tempKelvin

    tempLn = log(ThermistorRefR * ((1023.0/voltageRaw) - 1); 
             // 1023 intervals not 1024 counts on ADC!!!



    tempKelvin = 1 / (thermistorConA + (thermistorConB * tempLn) + (thermistorConC * tempLn * tempLn * tempLn));

    return tempKelvin;

    }


Note: There is one line that breaks over the width of the column (tempKelvin assignment), so please don't be tempted to put in a line-break. The code is presented as a header file, but it can be as easily copy-pasted into your own code rather than #included