Simple and Low Cost Temperature and Humidity Sensor: Part 2

temp_humidity_pcb

Above you can see the primary circuitry for the simple temperature and humidity sensor I described last week.  The board shown above is part of a client’s design, so there is quite a bit more circuitry than is required for the sensor I described last week.

The code I’ll share today is simplified to just measuring the analog inputs from the sensors and one other input that I will use later for analog measurement calibration.  But first some housekeeping.

In last weeks blog I showed a basic schematic for connecting the temperature and relative humidity sensors to a PIC16F1789 microcontroller.  When I built the board up to run test code I had a short between 5V and ground.  It took me a bit of time to locate the culprit, which ended up being the relative humidity sensor (Honeywell HIH-5030-001).

The Honeywell datasheet showed no connection to the mounting tab of the sensor (left hand side of the part shown in the image above).  I assumed it was not electrically connected, or was connected to ground.  And so I connected it to ground.   It turns out that the tab must remain unconnected.  That requirement is listed in a document associated with installation instructions for the part.  Of additional interest is the fact that the part cannot be “washed” as most PCBs are after assembly by machines.  I was pretty sure this would be the case, as getting the sensor wet ruins them.

I’ve shown where last week’s schematic was in error below.

temp_humidity_schematic

There are a couple of other differences between the connections shown above and the firmware I wrote for the PIC16F1789 to test these sensors.  My design uses an 8MHz clock with a 4x phase lock loop (PLL).  This results in a clock instruction time of 0.125uS.  The microcontroller has in internal oscillator that would run this fast, but since my final design will be in an industrial setting I wanted to use an external oscillator and that is not shown in the schematic.

Code Description:
This test firmware is pretty simple.  I run a 50mS timer and once per second I toggle a heartbeat LED (the green one shown in the schematic).  Every 30 seconds I take a temperature and humidity measurement.  I compare the results to high thresholds, and set alarm bits and an alarm output (the red LED) accordingly.  I’ve got some hooks in their for calibration, and the some of the settings can be stored in EEPROM on the microcontroller.  But for the most part this code just takes the measurements.  The firmware was written using Microchip’s XC8 C compiler (version 1.12) and debugged with a PICkit3.  These development tools cost about under $100.

Update:  During testing of this design I had some pretty awful results using the internal voltage reference of the PIC16F1789.  The datasheet specified 4% accuracy, whish is pretty bad.  I found it was off even more than that.  In reviewing the Errata sheet for the part they have a notice stating that the fixed voltage reference accuracy is actually +/-8%.  You’re better off using VCC as the voltage reference.  With 5V as VCC each bit of the analog conversion is equal to 1.2mV, not 1mV. 

Code definitions:  This portion of the firmware has the includes, function prototypes, configuration bit setting, constants, variables, and pin i/o definitions.

/*
 * Temp_Humidity:  A simple temperature and humidity measuring design
 *
 * File:   main.c
 * Author: Lon
 *
 * Created on February, 2014,
 */

#include <stdio.h>
#include <stdlib.h>
#include <xc.h>            /* PIC hardware mapping */
#include <pic16f1789.h>    /*  SFR register and bit definitions */

/******************************************************************************/
/* Configuration bits                                                         */
/******************************************************************************/
#pragma config FOSC=ECH, WDTE=ON, MCLRE=ON, CP=OFF, CPD=OFF, BOREN=ON, CLKOUTEN=OFF, IESO=OFF, FCMEN=OFF
#pragma config WRT=OFF, PLLEN=ON, LVP=OFF

/******************************************************************************/
/* Variables-Constants-Function Prototypes                              */
/******************************************************************************/

#define SAMPLE_TIMER 15536                  // 50mS timer
#define SAMPLE_TIMER_BIG 600               // How often to sample ADC 600*50mS = 30 seconds
unsigned int SampleCounter;                 // Stores counter to establish sample time

#define LED_TIMER   20                      // How often to blink heartbeat LED
unsigned int BlinkyCounter;                 // Stores counter to for heartbeat LED

#define ALARM_MASK 3                        // Which indicator reg bits cause alarm output

unsigned char eeprom_read(unsigned char address);
void eeprom_write(unsigned char address, unsigned char value);

/**************************************************************
 Unions and other register definitions
 **************************************************************/
union
{
    struct UserRegs
    {
    int OneVolt;
    int ThreeVolt;
    int Temperature;
    int RHumidity;  
    int TempHigh; 
    int RHHigh;  
    }Ints;
    struct
    {
    int RegArray[6];
    };
}NV;

#define NV_ARRAY_SIZE 6

union {
    struct IndicatorReg {
        unsigned TEMPHIGH          :1;
        unsigned RHHIGH            :1;
        unsigned IR2               :1;
        unsigned IR3               :1;
        unsigned IR4               :1;
        unsigned IR5               :1;
        unsigned IR6              :1;
        unsigned IR7              :1;
    }Bits;
    unsigned char   Reg;
} INDICATOR;

union LongUnion
    {
    long LW;
    unsigned long uLW;
    int W[2];
    unsigned int uW[2];
    char B[4];
    unsigned char uB[4];
    };
union LongUnion
MyTemp;

/**************************************************************
 i/o pins
 **************************************************************/
// Port A Inputs
#define SWITCHIN      PORTAbits.RA4   // single DIP switch input

// Port B Inputs
#define IO1         PORTBbits.RB1   // i/o line
#define IO2         PORTBbits.RB2   // i/o line

// Port E Outputs
#define LED         LATEbits.LATE2  // green led
#define ALARM       LATEbits.LATE1  // alarm output and red LED

Setup code:  This portion of the firmware sets up the oscillator, timer, ports and peripherals.

 

/******************************************************************************/
/******************************************************************************/
/* Functions                                                               */
/******************************************************************************/
/******************************************************************************/
/* Configure_Oscillator:                                                      
   Configures the device to run with 8MHz and 4X PLL (32MHz) using the external
   oscillator. Instruction time will be 4X slower than clock, or 125ns per
   instruction.

   Sets WDT timout period.                                                    */
/******************************************************************************/
void Configure_Oscillator(void)
{
    OSCCONbits.IRCF=0x00;           // Oscillator determined by FOSC<2:0> in Configuation word 1
    WDTCONbits.WDTPS=0x7;           // set watch dog time for 128ms reset
}
/******************************************************************************/
/* Configure_Ports_ADC:
   Configures i/o direction and power on states and ADC                       */
/******************************************************************************/
void Configure_Ports_ADC(void)
{
    OPTION_REGbits.nWPUEN = 0;       // weak pullups enabled by individual latch values
    CM1CON0bits.C1OE = 0;            // disable comparator modules
    CM2CON0bits.C2ON = 0;
    OPA1CONbits.OPA1EN = 0;         // ensure op-amp modules disabled
    OPA2CONbits.OPA2EN = 0;
    OPA3CONbits.OPA3EN = 0;
    DAC1CON0bits.DAC1EN = 0;        // disable DAC module

    ANSELBbits.ANSELB = 0;          // all pins are digital inputs
    TRISB=0xFF;                     // PortB are inputs
    TRISC=0xFF;                     // PortC are inputs
    TRISD=0xFF;                     // PortD are inputs

    TRISE = 0xF9;                   // RE2 and RE1 outputs
    LED = 1;
    ALARM = 0;

    // Using 12-bit sign-magnitude result.  This is 12-bit left justified
    // so we can just use the ADRESH holds 8-bit result

    FVRCONbits.FVREN = 0x01;        // Enable fixed voltage reference
    FVRCONbits.ADFVR = 0x03;        // 4.096V
    ADCON0bits.ADRMD = 0;           // ADC is 12-bit, left justified
    ADCON1bits.ADFM = 0;            // use sign-magnitude result

    ADCON2bits.CHSN = 0x0F;         // Negative ADC is determined by ADCON1 bits
    ADCON1bits.ADCS = 0x03;         // use FOSC = RC
    ADCON1bits.ADNREF = 0x00;       // negative reference is ground
    ADCON1bits.ADPREF = 0x03;       // positive reference is FVR (Note: this has a +/-8% accuracy)
//    ADCON1bits.ADPREF = 0x00;       // positive reference is VCC (5V) (use VCC as positive reference for better accuracy)
    ADCON0bits.CHS = 1;

    ADCON0bits.ADON = 1;           // turn on ADC module

    ANSELAbits.ANSELA = 0x07;       // RA0, RA1, RA2 are ADC

    WPUAbits.WPUA = 0x10;            // RA4 pull-ups enabled
    TRISA=0x1F;                      // PortA are inputs
}

/******************************************************************************/
/* Configure_Timers:
   Sets up timers used by the firmware.                          */
/******************************************************************************/
void Configure_Timers(void)
{
    // configure timer1
    T1CONbits.T1OSCEN = 0x00;       // dedicated clock enabled for TMR1
    T1CONbits.T1CKPS = 0x03;        // 1:8 prescaler
    T1CONbits.TMR1CS = 0x00;        // internal clock source on T1CKI
    T1CONbits.TMR1ON = 0x01;        // turn on timer1, 16-bit
}
/******************************************************************************/
/* Configure_Interrupts:
   Modify interrupt related registers at power up                             */
/******************************************************************************/
void Configure_Interrupts(void)
{  
    // Enable interrupts
    INTCONbits.PEIE = 0;            // disable peripheral interrupts
    INTCONbits.GIE = 0;             // disable global interrupts
}

Main code:  This is the main code loop.

 

/******************************************************************************/
/******************************************************************************/
/* Main Program                                                               */
/******************************************************************************/
/******************************************************************************/
void main(void)
{
    Configure_Oscillator();     // setup internal oscillator
    Configure_Ports_ADC();      // get i/o ports sorted
    Configure_Timers();         // configures other timers including servo related
    Configure_Registers();      // makes sure registers are loaded at start

    while(1)
    {
    CLRWDT();                   // clear the watch dog timer

    if (PIR1bits.TMR1IF == 1)   // check for 50mS timer elapsing
        {
        PIR1bits.TMR1IF = 0;    // clear timer flag
        TMR1 = SAMPLE_TIMER;    // reset 50mS timer

        SampleCounter++;        // check for 30 second sample timer
        if (SampleCounter > SAMPLE_TIMER_BIG)
            {
            SampleCounter = 0;  // reset counter
            Read_ADC();         // accepts and responds to communication
            }
        BlinkyCounter++;        // check for heartbeat LED blink
        if (BlinkyCounter > LED_TIMER)
            {
            BlinkyCounter = 0;  // reset LED blink counter
            LED = ~LED;         // toggle LED state
            }
        }
    }
}

Function subroutines:  This section of code sows the subroutines that are called in the main loop, as well as some called by the various configuration routines.  Four of the five subroutines are just handling registers at power up or storing their contents in EEPROM.  They are more or less in place because of other things I’ll be doing with the code later.  The Read_ADC() function does the actual reading of the sensors to determine if there is an error condition caused by either sensor exceeding its high setting.

Obviously you could average the readings or calibrate for temperature or humidity against a known/calibrated sensor.  Doing that requires a controlled environment and I won’t go into that process here.  Later I might calibrate for the errors in the microcontroller’s analog converter.  The third reading analog channel reading is included for that.  Essentially you would read 1VDC and 3VDC and create a y = mX+b line and then fit the sensor measurements to that line.

/******************************************************************************/
/* Store_Registers:
   Stores registers in EEPROM.
/******************************************************************************/
void Store_Registers(void)
{
    unsigned char i;
    unsigned char j = 0;
    unsigned char EEWrite = 0;

    NV.Ints.Temperature = 0;
    NV.Ints.RHumidity = 0;

    for(i=0;i<NV_ARRAY_SIZE;i++)
    {
        EEWrite = (char)(NV.RegArray[i]&0xff);
        eeprom_write(j,EEWrite);  // write array to eeprom
        while(EECON1bits.WR)    // wait for write to finish before continuing
            CLRWDT();           // avoid killing program
        j++;
        EEWrite = (char)(NV.RegArray[i]>>8);
        eeprom_write(j,EEWrite);  // write array to eeprom
        while(EECON1bits.WR)    // wait for write to finish before continuing
            CLRWDT();           // avoid killing program
        j++;
    }
}
/******************************************************************************/
/* Restore_Register_Defaults:
   Loads register default settings when called and stores them in EEPROM.
/******************************************************************************/
void Restore_Register_Defaults(void)
{
    unsigned char i;
    unsigned char j = 0;
    unsigned char EEWrite = 0;

    for(i=0;i<NV_ARRAY_SIZE;i++)
        NV.RegArray[i] = 0x00;  // load all variables with 0

    NV.Ints.TempHigh = 500;
    NV.Ints.RHHigh = 2500;
    NV.Ints.OneVolt = 1000;
    NV.Ints.ThreeVolt = 3000;

    for(i=0;i<NV_ARRAY_SIZE;i++)
    {
        EEWrite = (char)(NV.RegArray[i]&0xff);
        eeprom_write(j,EEWrite);  // write array to eeprom
        while(EECON1bits.WR)    // wait for write to finish before continuing
            CLRWDT();           // avoid killing program
        j++;
        EEWrite = (char)(NV.RegArray[i]>>8);
        eeprom_write(j,EEWrite);  // write array to eeprom
        while(EECON1bits.WR)    // wait for write to finish before continuing
            CLRWDT();           // avoid killing program
        j++;
    }
}
/******************************************************************************/
/* Configure_Registers:
   Loads registers from EEPROM on power-up, and checks to see if default settings
   should be applied (first time power up).                                                         */
/******************************************************************************/
void Configure_Registers(void)
{
    unsigned char i;
    INDICATOR.Reg = 0;

    for(i=0;i<NV_ARRAY_SIZE;i++)
    {
        NV.RegArray[i] = 0;
        NV.RegArray[i] = eeprom_read(2*i);  // copy data eeprom to program var. array
        NV.RegArray[i] += (eeprom_read((2*i)+1))*256;  // copy data eeprom to program var. array
    }

//    NV.Ints.OneVolt = 0xFFFF;   // used in testing to force storage of defaults
    if(NV.Ints.OneVolt==0xFFFF)   // if this reg = 0xffff eeprom is blank
        Restore_Register_Defaults();

    SampleCounter = 0;      // clear ADC sample timer counter
    BlinkyCounter = 0;      // clear LED blink timer counter
    TMR1 = SAMPLE_TIMER;
    PIR1bits.TMR1IF = 0;

}
/******************************************************************************/
/* SampleTime:
  5 instructions + about 3 instructions * loop
 /******************************************************************************/
void sample_time(unsigned char loop)
{
    char i=0;
    for(i=0;i<loop;i++);
}
/******************************************************************************/
/* Read_ADC:
 Reads analog signals at RA0, RA1, and RA2.  ADC is 12-bit, 4.096V reference,
 and left justified.  Shifting the result 4 places to the right gives a
 1mV per bit result.
 /******************************************************************************/
void Read_ADC(void)
{
    // Measure relative humidity
    ADCON0bits.CHS = 0x00;          // select ADC channel
    sample_time(10);                // wait for sample timer, ~4uS
    ADCON0bits.ADGO = 1;            // start conversions
    while(ADCON0bits.ADGO == 1);    // wait for completion
    NV.Ints.RHumidity = ADRES>>4;   // move result to register

        // Measure temperature
    ADCON0bits.CHS = 0x01;
    sample_time(10);
    ADCON0bits.ADGO = 1;
    while(ADCON0bits.ADGO == 1);
    NV.Ints.Temperature = ADRES>>4;

        // Measure calibration voltage
    ADCON0bits.CHS = 0x02;
    sample_time(10);
    ADCON0bits.ADGO = 1;
    while(ADCON0bits.ADGO == 1);

    if (IO1 == 1) // if this i/o is set 1V is at RA2
        NV.Ints.OneVolt = ADRES>>4;    
    if (IO2 == 1)// if this i/o is set 3V is at RA2
        NV.Ints.ThreeVolt = ADRES>>4;  

    // Check for temp and humidity high levels
    INDICATOR.Bits.RHHIGH = 0;
    if(NV.Ints.RHumidity > NV.Ints.RHHigh)
        INDICATOR.Bits.RHHIGH = 1;

    INDICATOR.Bits.TEMPHIGH = 0;
    if(NV.Ints.Temperature > NV.Ints.TempHigh)
        INDICATOR.Bits.TEMPHIGH = 1;

    // check for alarm output due to high levels
    if ((ALARM_MASK & INDICATOR.Reg) != 0)
        ALARM = 1;
    else
        ALARM = 0;
}

Speak Your Mind

*