Arduino-based 1-Wire LCD thermometer
Published in July 2013
This article describes a quick-and-dirty project I did mostly for
fun. The background is that many years ago (in 1991 I think)
I built a six-channel thermometer for the ventilation/heat exchanger system
at my parent's house. It was an almost completely analog design based
on AD590 temperature-to-current sensors, some opamp-based signal
conditioning circuitry, a rotary switch and a home-made LCD voltmeter
based on an ICL7126 ADC with LCD driving
capability. This thermometer had started to become a bit unreliable
over the years (probably due to corrosion in the switch and/or
connectors) and I realized that it would be quite easy (and fun!) to
build a more modern version of it without any custom PCBs or analog
signal conditioning but with a display that could show all the
temperatures simultaneously.
Circuitry
To make the thermometer maximally easy to build and install, I based
it on an Arduino
Nano,
a 4x20
character alphanumeric LCD display and
1-Wire
temperature sensors, DS18B20.
The connection between the Arduino and the external hardware is
defined at the top of the source code. Basically I chose the following
connections:
Arduino pin
| Connects to
|
D2
| LCD RS (pin 4)
|
D3
| LCD E (pin 6)
|
D4
| LCD D4 (pin 11)
|
D5
| LCD D5 (pin 12)
|
D6
| LCD D6 (pin 13)
|
D7
| LCD D7 (pin 14)
|
D8
| 1-wire DQ and 4k7 pull-up to +5V.
(All sensors are connected in parallel to this pin.)
|
In addition to the connections between the Arduino and the external
circuitry, there are a few other connections to make:
- LCD pin 1 (VSS) has to be grounded.
- LCD pin 2 (VDD) has to be connected to 5V (make sure you
have a 5V tolerant LCD).
- LCD pin 3 (V0) needs a contrast bias voltage. Connect a 10k
potentiometer between 5V and GND with the center tap connected to this
pin.
- LCD pin 5 (R/W) has to be grounded.
- The Arduino needs 5V. I supply it via its USB port from an old
phone charger.
- The temperature sensors need (of course) GND in addition to DQ.
- If you are using DS18B20 instead of DS18B20-PAR as temperature
sensors, you need to connect the VDD pin of each sensor to GND (a bit
inconvenient since they are not next to each other). So it is more
convenient to use DS18B20-PAR.
The bill of materials (BOM) of the project is shown below:
Quantity
| Component
|
1
| Arduino Nano 3.0
|
1
| 4x20 alphanumeric LCD display, e.g. Digikey
73-1249-ND
|
6
| DS18B20 or DS18B20-PAR 1-wire temperature sensors
from Maxim Integrated
|
1
| 10 kΩ potentiometer
|
1
| 4.7 kΩ resistor
|
1
| pin header, 16 pins, 2.54 mm pitch (for LCD)
|
2
| 15-pin socket strip, 2.54 mm pitch (for Arduino)
|
1
| 5V power supply, ~100 mA or more
|
1
| small piece of bread board
|
?
| 2-conductor cables to connect to the remote sensors
|
Pictures
Below are some pictures of the thermometer.
The display and sensors
(click to enlarge)
|
The Arduino Nano
(click to enlarge)
|
The bread board and contrast pot
(click to enlarge)
|
The display
(click to enlarge)
|
Source code
Below is the Arduino source code for the thermometer. In order to
make it work for you, you will have to make some modifications:
- The sensor addresses are hard-coded and you need to change them to
match the unique addresses of your sensors. To figure out the
addresses of your devices, enable serial debug output and look at the
output using the Arduino serial monitor.
- You most likely also want to change the title text and the labels
shown on the display at each temperature.
- Maybe you also want to remove the easter egg...
You also need the 1-Wire library from here: http://www.pjrc.com/teensy/td_libs_OneWire.html. Download the
OneWire library and place its OneWire folder in C:\Program Files\Arduino\libraries. Then you
should be able to compile the code.
Link to source file: thermometer.ino
////////////////////////////////////////////////////
// LCD multi-thermometer based on an Arduino Nano //
////////////////////////////////////////////////////
// Measures the temperature using several one-wire sensors and displays
// the results on a 4x20 alphanumeric LCD.
//
// Written in 2013 by Per Magnusson, Axotron, http://axotron.se/index_en.php
// v 1.3
//
// Part of the code was copied from:
// https://github.com/pbrook/arduino-onewire/blob/master/examples/DS18x20_Temperature/DS18x20_Temperature.pde
// The OneWire library was downloaded from:
// http://www.pjrc.com/teensy/td_libs_OneWire.html
// Download the OneWire lib and place its OneWire folder in
// C:\Program Files\Arduino\libraries
// Then restart the Arduino development environment.
//
// (c) 2013, Per Magnusson, Axotron
//
// License:
// I make this code available in the hope that it might be educational and/or
// inspirational. Feel free to use it, change it, include it in your own commercial
// or non-commercial projects as you see fit.
// I give no guarantee that it will be good for anything.
// Below is some legalese I copied from a version of the BSD license:
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
// ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include <OneWire.h>
#include <LiquidCrystal.h>
// Specification of hardware connection:
LiquidCrystal lcd(2, 3, 4, 5, 6, 7);
OneWire ds(8); // on pin 8 (a 4.7K pull-up resistor is necessary)
// Constants
const byte N_SENSORS = 6; // Number of sensors
const byte allow_serial = 0; // Set to 1 to enable serial port debug output
const char title[] = " Termometer v 1.3 ";
// Sensor addresses
// To figure out the address of new sensors: set allow_serial to 1 and check with a
// serial console what addresses the new sensors have
const byte sensor_addr[N_SENSORS][8] = {
{0x28, 0x92, 0xFD, 0xE2, 0x03, 0x00, 0x00, 0x21}, // ok
{0x28, 0x01, 0x0c, 0xE3, 0x03, 0x00, 0x00, 0x2D}, // ok
{0x28, 0xC5, 0x55, 0x95, 0x04, 0x00, 0x00, 0xD3}, // ok
{0x28, 0x4E, 0x89, 0x95, 0x04, 0x00, 0x00, 0xBE}, // ok
{0x28, 0x91, 0xA9, 0x95, 0x04, 0x00, 0x00, 0xA6}, // ok
{0x28, 0xA9, 0xB1, 0x95, 0x04, 0x00, 0x00, 0xA8} // ok
};
// Test position on the LCD, column - row
const byte text_pos[N_SENSORS][2] = {
{0, 1},
{10, 1},
{0, 2},
{10, 2},
{0, 3},
{10, 3}};
// Labels for the different sensor positions
const char *labels[] = {"NG", "SG", "FX", "EX", "FL", "AL"};
// Norra gaveln, södra gaveln, ...
const char *labels_long[] = {"Norra gaveln", "Sodra gaveln", "Tilluft fore VVX", "Tilluft efter VVX",
"Franluft", "Avluft"};
// Counters for easter eggs
byte brr_cnt = 15;
byte puh_cnt = 15;
byte sense_idx = 0; // Index to the sensor we would like to talk to
byte miss_cnt = 0; // Number of times we have failed to find the desired sensor
void setup(void) {
lcd.begin(20, 4);
lcd.setCursor(0,0);
lcd.write(title);
delay(3000);
// To allow programmer to program a new sketch.
// This is an attempt at a workaround for the:
// 'avrdude: usbdev_open(): did not find any USB device "usb"' bug
// But this workaround is probably ineffective...
// It seems quite random when the bug hits.
if(allow_serial) {
Serial.begin(9600);
}
}
void loop(void) {
byte ii;
byte present = 0;
byte type_s;
byte data[12];
byte addr[8];
byte crc_calc;
double celsius;
char sbuf[20];
if(sense_idx >= N_SENSORS) {
// Wrap around if sense_idx is too high
sense_idx = 0;
}
// Look for next 1-wire device
if ( !ds.search(addr)) {
// No more addresses
if(allow_serial) {
Serial.println("No more addresses.");
Serial.println();
}
ds.reset_search();
delay(250);
return;
}
// Are we talking to the one we want to talk to?
if(!addr_comp(sensor_addr[sense_idx], addr)) {
// We are not talking to the sensor we would like to talk to, try the next one
miss_cnt++;
if(miss_cnt > N_SENSORS) {
// The desired sensor does not seem to reply!
if(allow_serial) {
Serial.print("Cannot find sensor ");
Serial.println(labels[sense_idx]);
}
// Show error on LCD
lcd.setCursor(text_pos[sense_idx][0], text_pos[sense_idx][1]);
lcd.write(labels[sense_idx]);
lcd.write(":");
lcd.write(" Err! ");
// Go to next sensor index
miss_cnt = 0;
sense_idx++;
}
// Try to search again
return;
}
// Found the desired sensor, its index is in sense_idx
miss_cnt = 0;
// Write label explanation
lcd.setCursor(0,0);
lcd.write(" "); // Clear top line of LCD
lcd.setCursor(0,0);
lcd.write(labels[sense_idx]);
lcd.write(" ");
lcd.write(labels_long[sense_idx]);
if(allow_serial) {
Serial.print("ROM =");
for(ii = 0; ii < 8; ii++) {
Serial.write(' ');
Serial.print(addr[ii], HEX);
}
}
crc_calc = OneWire::crc8(addr, 7);
if (crc_calc != addr[7]) {
if(allow_serial) {
Serial.println();
Serial.print("CRC is not valid!");
Serial.println(crc_calc, HEX);
}
return;
}
// the first ROM byte indicates which chip
switch (addr[0]) {
case 0x10:
if(allow_serial) {
Serial.println(" Chip = DS18S20"); // or old DS1820
}
type_s = 1;
break;
case 0x28:
if(allow_serial) {
Serial.println(" Chip = DS18B20");
}
type_s = 0;
break;
case 0x22:
if(allow_serial) {
Serial.println(" Chip = DS1822");
}
type_s = 0;
break;
default:
if(allow_serial) {
Serial.println("Device is not a DS18x20 family device.");
}
return;
}
ds.reset();
ds.select(addr);
ds.write(0x44, 1); // start conversion, with parasite power on at the end
// Turn off degree sign to show that a conversion is in progress
lcd.setCursor(text_pos[sense_idx][0]+8, text_pos[sense_idx][1]);
lcd.write(" ");
delay(1000); // maybe 750ms is enough, maybe not
// we might do a ds.depower() here, but the reset will take care of it.
present = ds.reset();
ds.select(addr);
ds.write(0xBE); // Read Scratchpad
if(allow_serial) {
Serial.print(" Data = ");
Serial.print(present, HEX);
Serial.print(" ");
}
for (ii = 0; ii < 9; ii++) { // we need 9 bytes
data[ii] = ds.read();
if(allow_serial) {
Serial.print(data[ii], HEX);
Serial.print(" ");
}
}
if(allow_serial) {
Serial.print(" CRC=");
Serial.print(OneWire::crc8(data, 8), HEX);
Serial.println();
}
// Convert the data to actual temperature
// because the result is a 16 bit signed integer, it should
// be stored to an "int16_t" type, which is always 16 bits
// even when compiled on a 32 bit processor.
int16_t raw = (data[1] << 8) | data[0];
if (type_s) {
raw = raw << 3; // 9 bit resolution default
if (data[7] == 0x10) {
// "count remain" gives full 12 bit resolution
raw = (raw & 0xFFF0) + 12 - data[6];
}
} else {
byte cfg = (data[4] & 0x60);
// at lower res, the low bits are undefined, so let's zero them
if (cfg == 0x00) raw = raw & ~7; // 9 bit resolution, 93.75 ms
else if (cfg == 0x20) raw = raw & ~3; // 10 bit res, 187.5 ms
else if (cfg == 0x40) raw = raw & ~1; // 11 bit res, 375 ms
//// default is 12 bit resolution, 750 ms conversion time
}
celsius = (float)raw / 16.0;
if(allow_serial) {
Serial.print(" Temperature = ");
Serial.print(celsius);
Serial.println(" C");
}
dtostrf(celsius, 5, 1, sbuf);
if(celsius < -10.0) {
// Brr! easter egg shown occasionally at cold temperatures
brr_cnt++;
if(brr_cnt > 17) {
brr_cnt = 0;
sbuf[0] = ' ';
sbuf[1] = 'B';
sbuf[2] = 'r';
sbuf[3] = 'r';
sbuf[4] = '!';
sbuf[5] = ' ';
sbuf[6] = '\0';
}
} else if(celsius > 27.0) {
// Puh! easter egg shown occasionally at hot temperatures
puh_cnt++;
if(puh_cnt > 17) {
puh_cnt = 0;
sbuf[0] = ' ';
sbuf[1] = 'P';
sbuf[2] = 'u';
sbuf[3] = 'h';
sbuf[4] = '!';
sbuf[5] = ' ';
sbuf[6] = '\0';
}
}
//lcd.setCursor(0,0); // Not any more since we started writing the explanations
//lcd.write(title);
lcd.setCursor(text_pos[sense_idx][0], text_pos[sense_idx][1]);
lcd.write(labels[sense_idx]);
lcd.write(":");
lcd.write(sbuf);
if((sbuf[1] != 'B') && (sbuf[1] != 'P')){
// Show degree sign when not showing easter eggs Brr! or Puh!
lcd.write(0xDF); // degree sign
lcd.write(' '); // space in case something else was written here
}
// Update to next sensor
sense_idx++;
}
byte addr_comp(const byte *a1, const byte *a2) {
// Compare two OneWire addresses and return true iff they are equal
byte ii;
byte res = 1;
for(ii=0; ii<8; ii++) {
if(a1[ii] != a2[ii]) {
res = 0;
break;
}
}
return res;
}