Welcome to buchanan1.net

J.R. Buchanan

http://www.buchanan1.net

Making / building an Arduino based Temperature Logger

For a refrigerator

October 2016

Picture of Data Logger

The problem

Recently our refrigerator stopped working. It became intermittent and would work most of the time, but then later we'd find our food spoiled. We contacted a service company, but they told us that there was nothing they could do while it was working, call them when it stopped. How were we going to find out that it stopped working before our food went bad? We couldn't just stop using it, even though we have a second, smaller fridge in the garage, that just wasn't practical. Just replacing the whole unit on speculation was definitely out of the question.

What I did

It occurred to me that an alarm that went off when the temperature rose above a specified temperature would be useful. I'm sure that such devices are available for sale, but the budget wouldn't allow that. It occurred to me that I could build such a thing myself and it would be more challenging, and cheaper, I decided to do it only with parts on hand. I wanted an alarm (blinking light) to go off when a high end set-point was passed, a display of the current temperature, as well as the high and low temperature measured, and data logging to a file on an SD card so that the results could be loaded onto a spread sheet for analysis if I later felt the need.

The first thing that was needed was a temperature sensor. Four ideas came to mind, a temperature sensor IC such as an LM35, a thermocouple, a thermistor, and the BE junction of a transistor (a PN junction). The choice was simple, the only one I already had on hand was the transistor. I chose a 2N3904, they are common, and I have plenty of them. A PN junction drops something around 0.7 volts and has a linear response to temperature, the higher the temperature, the lower the voltage. The change is a little under 2mV per degree C. Of course, I didn't have this memorized, I looked it up.

I soldered the collector and base of the transistor together, this yields a more "ideal" diode junction than if the collector were left open (or if a small signal diode was used instead of the transistor). I twisted some solid core wire up to make a twisted pair and soldered one wire to the emitter and the other to collector-base junction of the transistor. This is where I intended to use silicon RTV to seal the connection, but I couldn't find any around the house. It was late at night, and I really wanted to do this without buying anything, so I substituted hot-melt glue. I was worried that this would be a problem when I calibrated the device with boiling water, but it worked out fine.

Now I needed to measure the voltage and display/log the results. I still had Arduino, a SEEED Studio SD card shield, and LCD display from my battery test pretty much still assembled as I'd used the display for other Arduino projects. So I made some modifications to the circuit and used it. Basically I replaced the battery and load with the transistor in parallel with a 1uF electrolytic capacitor (for noise suppression) and passed about 1mA of current through the junction with a 4.7K resistor connected to the 5V supply.

Diagram of Temperature Logger

Once I had the circuit built, I needed to know the voltage across the junction at both 0 degrees C and 100 degrees C. Since the junction voltage was around 0.7V and would only change over about a 0.2V range from 0 degrees C to 100 degrees C and the Arduino only has a 10 bit ADC (Analog to Digital Converter), I knew that I couldn't use the standard 5V reference for the ADC. There wouldn't be enough resolution between steps. Fortunately the ATmega328 used in the Arduino Uno provides an internal 1.1V reference to be selected in software with the "analogReference(INTERNAL)" statement. All looked good!

The next step was to collect some data about what the actual junction voltage versus temperature was. First I modified the same program I used for logging battery (well, cell) voltage in a previous project. I had it display the current voltage on the first line of the LCD display, and the minimum and maximum voltage recorded on the second line. I then immersed the transistor in ice water and boiling water (the hot melt glue softened a lot, but stayed in place - silicone RTV would be a much better choice) to record the voltages.

The code to read high and low voltages

The code starts with some comments describing the circuit. It's the same circuit displayed in the above diagram. Then some some header files are included, then the library for the liquid crystal display is set up. Then the A0 (Analog 0) pin on the Arduino is set to "read" so that it can be used to measure the voltage on from the sensor. Then the functions are declared. Then some constants are declared, see the comments for their descriptions.

After this is the setup() function. In an Arduino, this function is called once at power-up or reset, and then control is passed to the loop() function, which loops forever doing most of the work. The first thing done in setup() is the declaration of the local variables. Then the lcd library is told how many lines and characters are in the display. Then the reference voltage for the ADC (analog to Digital Converter) in the Arduino is switched from 5V to 1.1V. Then the voltage on A0 is read 10 times. I'm not sure why, but the first few times it's read, some spurious low voltages are returned, this way they're dumped.

Then the loop() function is called. Again, the first step is to declare the local variables. The "minimum_voltage" and "maximum_voltage" variables are declared as static, meaning that the values are stored between calls to the function. It's sort of like being a global variable, but better because only the loop() function can see these variables. Less chance for errors that way. The sense voltage is then read with the function read_A0(), more on that later. The reading, 0 to 1023 is then converted to a voltage reading taking into account the 1.1V reference. After this, the lowest and highest values are stored. Then the first line of the LCD display is cleared with the lcd_clear function, again, more on this later. The voltage is displayed on the first line. Then this is repeated for the low and high readings on the second line. Then we have a delay and the loop is repeated indefinitely.

The lcd_clear() function creates a string using malloc() with the number of characters equal to the characters in one line of the display, all spaces, ' '. The string is terminated with a NULL, '\0'. On a bigger system I'd check to see if the malloc() was successful, but we don't have a ton of room on the Arduino, especially with all the space the SD library hogs. Then the resulting blank string is written to each line of the display to clear them and the memory allocated with malloc() is freed. A very important step, the Arduino will shortly run out of memory if it is not freed.

In the read_A0() function, the measurement at the A0 (Analog 0) pin is read 10 times and summed in the variable "sense_value". The result is divided by 10 and returned. This averaging of 10 readings damps some bobble that sometimes resulted from a single read.

High/Low code


/*

Used to read high and low voltages dropped across a PN (base-emitter) junction of a transistor for
use as constants in the temoperature_logger.ino program

The circuit:
 * SD card attached to SPI bus as follows:
 ** MOSI - pin 11
 ** MISO - pin 12
 ** CLK - pin 13
 ** CS - pin 4
 Pin 10 is unused with the seeed SD card shield, but must remain an unused I/O for library to work right
 
 RS, Enable, D4, D5, D6, D7     <- names of lcd pins
 13, 11, 6, 5, 4, 3             <- pin number on lcd
 8,   7, 6, 5, 3, 2             <- pin numbers on Arduino (digital I/O)
 
 LED in series with a 560ohm resistor from digital 0 to ground
 
 Analog 0 (A0) connected to +5V with a 4.7K resistor and ground through the BE junction of a 2N3904 transistor
 (with base and collector tied together) A 1uF electrolytic connected across the BE junction of the transistor.
 
 */
 
#include  // needed as of IDE 1.6
#include 
#include 

// Which pins are used on the Arduino and the order they're hooked to lcd
// RS, Enable, D4, D5, D6, D7             <- names of lcd pins
// 13, 11, 6, 5, 4, 3                     <- pin number on lcd
LiquidCrystal lcd(8, 7, 6, 5, 3, 2);   // <- matching pins on Arduino (digital)

// A locally written clear function
// line is 0 for first line, 1 for second line
// no_char is the number of characters your display supports per line
void lcd_clear (int line, int no_char);

// Reads the Analog 0 pin 10 times 10uS apart, then returns the average value
int read_A0 (void);

#define NO_CHAR 16               // number of characters per line on your display
#define TEST_INTERVAL 900        // Time between temperature test, also blink rate of LED
#define MEASUREMENT_INTERVAL 100 // Time of temperature test, total of these two lines should de 1000mS
#define START_VALUE_LOW 1.1      // Starting volts for min/max measurments equal to the reference voltage for the ADC
#define START_VALUE_HIGH 0       // Starting volts for min/max measurments

void setup()
{
  int sense_value;
  int ktr;
    
  // init lcd
  lcd.begin(NO_CHAR, 2);
  
  // Switch the ADC reference voltage from 5.0V to 1.1V
  analogReference(INTERNAL);

  // wait for input to settle down
  // I really don't know why, but the first few voltage reading come out low if
  // I don't get rid of them here
  for (ktr = 0; ktr < 10; ktr++) {
    sense_value =analogRead(A0);
  }
  
}

  
void loop()
{
  int sense_value;
  double voltage;
  static double minimum_voltage = START_VALUE_LOW;
  static double maximum_voltage = START_VALUE_HIGH;
  
  sense_value = read_A0 ();               // read value from pin A0
  voltage = sense_value * (1.1 / 1023.0); // calculate the voltage

  // log the high and low values for the voltage
  if (voltage < minimum_voltage){
    minimum_voltage = voltage;
  }
  if (voltage > maximum_voltage){
    maximum_voltage = voltage;
  }

  // clear the first line of the display
  lcd_clear (0, NO_CHAR);

  // display the current voltage on the first line
  lcd.print (voltage, 3);

  // clear the second line of the displsy
  lcd_clear (1, NO_CHAR);

  // display the min and max voltages on the second line of the display
  lcd.print (minimum_voltage, 3);
  lcd.print ("V");
  lcd.print (" ");
  lcd.print (maximum_voltage, 3);
  lcd.print ("V");
  
  // wait for next iteration
  delay (TEST_INTERVAL);

}

// clears specified line on lcd display
void lcd_clear (int line, int no_char)
{
  char *printable_string;
  int ktr;
 
  // displays typically have 2 lines
  line = constrain (line, 0, 1);

  // make a string with no_char spaces    
  printable_string = (char *) malloc (no_char + 1); // daangerous assumption that
                                                    // malloc succeeds
  for (ktr = 0; ktr < no_char; ktr++)
  {
    printable_string[ktr] = ' ';
  }  
  printable_string[no_char] = '\0';
  
  // set cursor and print blanking string
  lcd.setCursor (0, line); // position cursor to begining of specified line
  lcd.print (printable_string);
  lcd.setCursor (0, line); // position cursor to begining of specified line
  
  free (printable_string); // spectacular memory corruption on ktr equal 96 w/o this! :-)

}

// read A0 ten times and return the average value
// A little simplistic, but it gets rid of some bobble in the readings
int read_A0 (void)
  {
  int sense_value = 0;
  int ktr;

  for (ktr = 0; ktr < 10; ktr++)
    {
    sense_value += analogRead(A0);
    delay (MEASUREMENT_INTERVAL / 10);
    }

  return (sense_value / 10);

  }

The measured voltages were 0.547 volts at 100 degrees C and 0.735 V at 0 degrees C. That comes to 1.88mV per degree C or 0.89mV per degree F. Since the 10 bit ADC with a 1.1V reference can read in increments of 0.98mV, that comes to about a resolution of about 1.1 degree F. Not too impressive, but good enough to log the temperature in a refrigerator.

What if we used a temperature sensor IC? An LM35 temperature sensor provides a change of 10mV per degree C, about 5 times the change that the PN junction provides. Unlike the PN junction, the LM35 has a positive temperature coefficient. This increased sensitivity could increase the resolution greatly. Interestingly, it doesn't increase the accuracy too much, as the LM35 has an accuracy of about 1 degree F. You can measure roughly 0.2 degree F increments, but the actual accuracy is roughly +/- 1 degree F. Not that impressive either. Another problem is that to read negative temperatures, you need a positive reference voltage, so that the output can drop below that. What to use for a reference? Many projects I see on the 'net use a pair of 1N4148 diodes in series to provide a roughly 1.3V reference. But as we've seen above, the voltage across a PN junction varies with temperature. So if the temperature at the actual logging unit changes, so does the indicated temperature. One solution is to use a second ADC to measure the reference voltage. That adds complexity. It also adds another point for the resolution of the ADC to affect the measured temperature. Also, to do this, we'd need a higher reference voltage for the ADC. This would increase the space between each step of the ADC, resulting in even lower resolution. Not ideal. Note that the above numbers are approximate.

I then wrote the final program, with a high temperature limit set to 42 degrees F. A little research showed that 40 degrees F is considered the high limit of a safe fridge, and due to measurement uncertainty, I used 3 degrees higher as the point the alarm light goes on..

Temperature logging code

This program is based on the program used to measure the voltages at high and low temperatures. The voltage is converted to degrees C by subtracting it from the voltage at 0 degrees C and multiplying it by number of degrees between the low and high voltages at 0 and 100 degrees C. The subtraction from the voltage at 0 degrees is done because the voltage from the sensor is inversely proportional to the temperature. The temperature is then converted to degrees F by multiplying by 1.8 and adding 32.

A flag is set if the temperature goes above the threshold (42 degrees F). Each time through the loop, this flag is tested and if it's set, the status of the led_state is toggled and if it is high, the warning light is turned on for the next time through the loop, and off for the next time after that through the loop. This causes the LED to flash once a second if the temperature goes over the threshold. The light stays blinking even if the temperature drops below the threshold.

The other difference in this program is that the temperature is logged once a minute to the SD card in a CSV (Comma Separated Value) file, suitable for reading into a spread sheet.

The header for the SD card library is included in the same manner as the header for the LCD library. A file handle is set up after this, which is used when writing to the SD card. In the setup() function, the SD library is initialized with SD.begin, if this fails (likely due to no card being present), a message is written to the LCD display and the light is blinked at a 500mS rate until the Arduino is reset. After the SD card init, the log file is either created or opened for append to previous data if the file is already there. A string, "-----\n" is written to the file to indicate that a logging session has begun.

Then in the loop() function, the number of seconds is incremented each time through the loop and if it is evenly divided by 60 (approximately once a minute) the number of minutes, a comma, and the temperature is written to the file.

Since there is never a graceful shutdown of the Arduino, power is simply removed, the file handle is flushed each time a write to the file is made, so that no data in the buffer is lost at power off, and the file is always up to date.

The minute count is not precise because the commands executed in the loop take a finite time. A real time clock (RTC) could be used to solve this problem, but I didn't have one on hand, and the goal was to build this only with parts already on hand. Another solution/improvement would be to have the program toggle an otherwise unused pin every time the loop is executed, then measure the time period with an oscilloscope so that the loop timing could be adjusted to be closer to one second.

Temperature reading code


/*

Used to log temperature in a refrigerator, keep max and min
temperatures on LCD and blink LED wheneveer a high limit is passed

The circuit:
 * SD card attached to SPI bus as follows:
 ** MOSI - pin 11
 ** MISO - pin 12
 ** CLK - pin 13
 ** CS - pin 4
 Pin 10 is unused with the seeed SD card shield, but must remain an unused I/O for library to work right
 
 RS, Enable, D4, D5, D6, D7     <- names of lcd pins
 13, 11, 6, 5, 4, 3             <- pin number on lcd
 8,   7, 6, 5, 3, 2             <- pin numbers on Arduino (digital I/O)
 
 LED in series with a 560ohm resistor from digital 0 to ground
 
 Analog 0 (A0) connected to +5V with a 4.7K resistor and ground through the BE junction of a 2N3904 transistor
 (with base and collector tied together) A 1uF electrolytic connected across the BE junction of the transistor.
 
 */
 
#include  // needed as of IDE 1.6
#include 
#include 

File myFile;

// Which pins are used on the Arduino and the order they're hooked to lcd
// RS, Enable, D4, D5, D6, D7             <- names of lcd pins
// 13, 11, 6, 5, 4, 3                     <- pin number on lcd
LiquidCrystal lcd(8, 7, 6, 5, 3, 2);   // <- matching pins on Arduino (digital)

// A locally written clear function
// line is 0 for first line, 1 for second line
// no_char is the number of characters your display supports per line
void lcd_clear (int line, int no_char);

// Reads the Analog 0 pin 10 times 10uS apart, then returns the average value
int read_A0 (void);

#define NO_CHAR 16               // number of characters per line on your display
#define LOG_FILE "LOG.TXT"       // Name of log file
#define TEST_INTERVAL 900        // Time between temperature test, also blink rate of LED
#define MEASUREMENT_INTERVAL 100 // Time of temperature test, total of these two lines should de 1000mS
#define START_VALUE_LOW 1.1      // Starting volts for min/max measurments equal to the reference voltage for the ADC
#define START_VALUE_HIGH 0       // Starting volts for min/max measurments
#define VOLTAGE_AT_0 0.735       // sense voltage at 0 degrees C
#define VOLTAGE_AT_100 0.547     // sense voltage at 100 degrees C
#define ALARM_TEMPERATURE 42     // Turn on the LED if the temperature goes over this (in F)

void setup()
{
  int sense_value;
  int ktr;
    
  // init lcd
  lcd.begin(NO_CHAR, 2);
  
  // On the SD Shield, CS is pin 4. It's set as an output by default.
  // Note that even if it's not used as the CS pin, the hardware SS pin
  // (10 on most Arduino boards, 53 on the Mega) must be left as an output
  // or the SD library functions will not work.
  pinMode(10, OUTPUT);

  // for overtemp LED
  pinMode(0, OUTPUT);
  digitalWrite (0, LOW);

  // init SD library, blink LED and print error if it fails
  // failure is most likely caused by no SD card being present in SD shield
  if (!SD.begin(4)) {
    lcd_clear (0, NO_CHAR);
    lcd.print("SD init failed");
    while (1){
      digitalWrite(0, HIGH);
      delay(500);
      digitalWrite(0, LOW);
      delay(500); 
    }
  }

  // Create log file, append if it already exists
  myFile = SD.open(LOG_FILE, FILE_WRITE);
  myFile.print ("-----\n");
  myFile.flush (); // flush so that everything will have been written when power is removed or SD
                   // card is ejected -not elegant, but it works

  // Switch the ADC reference voltage from 5.0V to 1.1V
  analogReference(INTERNAL);

  // wait for input to settle down
  // I really don't know why, but the first few voltage reading come out low if
  // I don't get rid of them here
  for (ktr = 0; ktr < 10; ktr++) {
    sense_value =analogRead(A0);
  }
  
}

  
void loop()
{
  int sense_value;
  double voltage;
  static unsigned long no_seconds = 0;
  static double minimum_voltage = START_VALUE_LOW;
  static double maximum_voltage = START_VALUE_HIGH;
  double temperature;
  double minimum_temperature;
  double maximum_temperature;
  static int led_state = 0;
  static int overtemp_flag = 0; // set if overtemp is detected, won't go back to 0 unless Arduino is reset
  
  sense_value = read_A0 ();               // read value from pin A0
  voltage = sense_value * (1.1 / 1023.0); // calculate the voltage

  // log the high and low values for the voltage
  if (voltage < minimum_voltage){
    minimum_voltage = voltage;
  }
  if (voltage > maximum_voltage){
    maximum_voltage = voltage;
  }

  // find the temperatues in degrees C
  // voltage is subtracted from VOLTAGE_AT_0 because voltage is inversely proportional to temperature
  // voltage is then multiplied the number of degrees per volt
  temperature = (VOLTAGE_AT_0 - voltage) * (100 / (VOLTAGE_AT_0 - VOLTAGE_AT_100));
  minimum_temperature = (VOLTAGE_AT_0 - maximum_voltage) * (100 / (VOLTAGE_AT_0 - VOLTAGE_AT_100));
  maximum_temperature = (VOLTAGE_AT_0 - minimum_voltage) * (100 / (VOLTAGE_AT_0 - VOLTAGE_AT_100));

  // convert to degrees F
  temperature = round (temperature * 1.8 + 32);
  minimum_temperature = round (minimum_temperature * 1.8 + 32);
  maximum_temperature = round (maximum_temperature * 1.8 + 32);

  // set flag if temperature goes over limit
  if (temperature > ALARM_TEMPERATURE)
  {
    overtemp_flag = 1;
  }

  // clear the first line of the display
  lcd_clear (0, NO_CHAR);

  // display the current temperature on the first line
  lcd.print (temperature, 0);
  lcd.print ("F");

  // clear the second line of the displsy
  lcd_clear (1, NO_CHAR);

  // display the min and max temperatures on the second line of the display
  lcd.print ("mn");
  lcd.print (minimum_temperature, 0);
  lcd.print ("F");
  lcd.print (" mx");
  lcd.print (maximum_temperature, 0);
  lcd.print ("F");
  
  
  // Write data to log file once a minute, when no_seconds divided by 60 has no remainder
  // File format CSV (comma seperated value) for easy import into spread sheet, an RTC would make this nicer:
  // -----
  // 0,33
  // 1,34
  // [...]
  // 112,37
  if (! (no_seconds % 60))
  {
    myFile.print (no_seconds / 60);
    myFile.print (",");
    myFile.print (temperature, 0);
    myFile.print ("\n");
    myFile.flush ();               // flush so that we always have the latest readings on the card so that we
                                   // don't lose anything at power off
  }

  // wait for next iteration
  delay (TEST_INTERVAL);

  // blink LED if over temperature flag is set
  if (overtemp_flag == 1)
  {
    led_state = ! led_state;
    digitalWrite (0, led_state);
  }

  // Not super accurate, as tests take finite time
  no_seconds++;
  
}

// clears specified line on lcd display
void lcd_clear (int line, int no_char)
{
  char *printable_string;
  int ktr;
 
  // displays typically have 2 lines
  line = constrain (line, 0, 1);

  // make a string with no_char spaces    
  printable_string = (char *) malloc (no_char + 1); // daangerous assumption that
                                                    // malloc succeeds
  for (ktr = 0; ktr < no_char; ktr++)
  {
    printable_string[ktr] = ' ';
  }  
  printable_string[no_char] = '\0';
  
  // set cursor and print blanking string
  lcd.setCursor (0, line); // position cursor to begining of specified line
  lcd.print (printable_string);
  lcd.setCursor (0, line); // position cursor to begining of specified line
  
  free (printable_string); // spectacular memory corruption on ktr equal 96 w/o this! :-)

}

// read A0 ten times and return the average value
// A little simplistic, but it gets rid of some bobble in the readings
int read_A0 (void)
  {
  int sense_value = 0;
  int ktr;

  for (ktr = 0; ktr < 10; ktr++)
    {
    sense_value += analogRead(A0);
    delay (MEASUREMENT_INTERVAL / 10);
    }

  return (sense_value / 10);

  }

The results

Once the code was written, I tested for sanity by measuring room temperature and the temperature on my forehead. I got plausible numbers. I knew that the numbers in ice water and boiling water would be fine too, as I already knew the voltage produced and the formula used to determine temperature from that.

Then I put the sensor in the refrigerator. Wow, was I surprised. With an empty Fridge and the sensor on the top shelf, I saw fairly fast (in a few minutes) temperature fluctuations of about 15 degrees F! I had faith in the temperature reading, but could this for real? I Googled it and learned that other people were surprised by the same thing when they put temperature probes in their refrigerators. The solution was to use a lower shelf where there was less of a change in internal air flow as the fan turned on and off and to load some food or cups of water around the sensor. This really damped down the rapid fluctuations.

Has the fridge died again? No, of course not, it's been working perfectly.

Here is an example of part of the resulting data file. I have not yet graphed this. If I did, a real time clock (RTC) would help make a lot more sense of it.


-----
0,34
1,35
2,34
3,35
[...]
143,35
144,35
145,35
146,35