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:

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.

Thermometer
The display and sensors
(click to enlarge)
Thermometer
The Arduino Nano
(click to enlarge)
Thermometer
The bread board and contrast pot
(click to enlarge)
Thermometer
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:

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; }