Sunday, October 18, 2009

Arduino Temperature Logger

Just like every other good engineer in life, I thrive on data. It doesn't even need to be solving a problem. Simply having the ability to say, "I know this," is awesome. Having some free time last month, I started playing with my Arduino again, and finally got around to ordering all the parts I needed for a project I've been thinking about for a long time.

This is my Arduino-based temperature logger. On the breadboard, from left to right, is a serial real time clock, a 1Mb EEPROM, and lastly the serial temperature sensor. Parts list:
Instead of all of the parts for the clock, you can also consider SparkFun's DS1307 module. You pay for it, but you get what looks like a very nice clock/crystal/battery package that you can just drop into your project. Disclaimer: I've never played with it personally. Only had it recommended to me. No way am I paying $20 for $3 in parts on a board.
Forget that. I was able to put together the functional equivalent with less than $10 worth of parts on a scrap piece of perf board.

Since pretty much everything uses I2C, I'll leave wiring it all up as an exercise for the student (Hint: connect everything to analog 4 & 5).

Source code.

Note that some of the features of the dumper (setting the clock, reading the clock), don't work. The main goal was to just get the D(ump) and Z(ero) commands working. I'll get the other commands working at some point if I mess up and the DS1307 loses power at some point (I originally set it using some sample code I found online).

I measured the current draw as 42.5mA, so ignoring the loss in the power supply, that's only half a watt. For this much data, I can pay for half a watt!

How It Works

On power-up, the Arduino checks the CH (Clock Halt) flag on the DS1307 clock chip. If this flag is set, it indicates that the clock has lost power, and that the date and data stored on it isn't valid. Without valid data, the Arduino displays an error message on the LCD and enters an infinite loop to do nothing.

If the clock is running, the controller then loads the first two bytes of general purpose RAM off of the clock. The DS1307 provides 56 bytes of RAM which can be backed by its battery in the case of power failure. I use the first two bytes of it to store the current index into the EEPROM, so that if the controller gets reset, as long as the clock has power, I'll never start writing over collected data by starting from the beginning again. It then prints the read count on the LCD for a second before changing to the current time and date. I found this useful for debugging, since I then only needed to hit the reset button to figure out where it was writing to on the EEPROM. It also sends the temperature sensor the signal to start taking temperature readings. The DS1631 is factory set to continuously take readings, as opposed to the other option where it only takes one and goes back into standby.

Once all of the initialization is finished, the Arduino enters a loop where every 100ms it reads the time off the DS1307 and the temperature off the DS1631 to display on the LCD. It then compares the minute value to the previous one, and if they're different, that means it's been a minute since the last temperature recorded in the EEPROM.

To record the temperature to the EEPROM, the current temperature reading is written to the EEPROM, and the address stored in the clock's RAM is incremented, so if the power goes out in the next minute, no data is lost or overwritten.

Conveniently, all three of the ICs use the I2C serial interface (by accident? Who knows...), so that leaves a ton of extra pins if I manage to think up anything else to add on.

Possible Extensions

Each temperature reading is stored as 2 bytes, one for the whole degrees, and the other to store 4 bits of fractional degrees. It wouldn't be unreasonable to assume my room is never going to swing outside of something like 20-30 degrees Celsius, and try and pack the reading into one byte to save room. I'm using a 1Mb EEPROM, which gives me enough room to take two byte readings every minute for 45 days. This is an incredibly high time resolution, and backing it off to something more reasonable, like once every 15 minutes, stretches this time out to 2 years. Changing the source is trivial; just add minute%15==0 to the if statement in loop().

Since the EEPROM is I2C, having the maximum 4 devices per bus is simply a question of paying the $4 per device. You would have to change how the tempreadcount variable is handled everywhere (store it in three bytes on the DS1307 instead of two, etc) so it doesn't roll over after the first chip is filled, but that would mean being able to take a temperature reading every minute for half a year, and that's just freakin awesome.

Even without the AT24C EEPROM, the Arduino comes with 512 or 1024 bytes of EEPROM on-board. This can get you a couple days of 15 minute readings, which is at least enough to play with, if you just wanted to buy the DS1631 chip.

Problems
  • For some reason, I could not get the LiquidCrystal library and Serial to both work at the same time. This means that to dump the EEPROM or reset the clock, I have to flash in a different program, then reflash the logging program.
  • I was originally using a DS1306 clock chip, but it drifted badly (a few minutes a day). Once I switched to the DS1307, which calls for the 12pF crystal I had, instead of a 6pF crystal, it hasn't noticeably drifted in a few days. Lesson learned: make sure you have the right crystal for your clock.
  • Don't copy paste big chunks of code that almost do the same thing without stopping and really thinking about it. I spent a long time trying to figure out why the data dump just read the last measurement for all of them. Was using the read number count instead of the loop variable... I was afraid I had somehow smoked my $4 EEPROM.
  • I think finding I2C addresses in data sheets is some kind of right of passage. You'd think that they would be something they're put rather prominently on the first page, or refer to it several times, but instead, every time they do refer to it, they refer to it as, "that address as noted previously," etc. Freakin pain in the ass, digging through not only the text, but all the I2C spec diagrams to find where they happened to show the waveform for the device's address, which you then get to decode into the 7 bit number you needed in the first place. I finally wrote up one page of notes with all the I2C addresses and register numbers so I could actually write some code instead of spend two hours digging through the data sheets every time I try to implement something.

Results

After running for one day, it came time to dump the data into Excel. I did this by writing the second program, which just prints each reading number, and the two bytes of the reading, all tab seperated so they will go into seperate cells nicely. To calculate the temperature was simply a matter of =(first cell)+(second cell/256). A little bit of good old graphing magic later, I got this:
Click to enlarge!
It is pretty clear that a one minute time resolution on a 12 bit temperature measurement is a little overkill. Most values are repeated for 10 minutes before drifting down another 1/16th of a degree to the next possible value. I've been playing with massaging the data after the fact to get a smoother line, but the logical next step is to just record the average reading for 5-15 minute periods to ink out two more bits, while saving a tremendous amount of EEPROM (45 days per chip to 1.8 years for 15 minute intervals). Note that the spikes in the afternoon were me playing with it by putting my finger on it and watching the temperature go up and fall back down to room temp.


Update 10/24/09: So after running the logger for a whole week, I've dumped the 10,000 some odd data points, and graphed them in Excel, just for your enjoyment (I'm lying, mine too).
Click to enlarge.
The ticks on the x axis are about where I believe midnight to be (I'm planning on patching the firmware to log a 0xFFFF temperature at midnight, since it knows what time it is). It's pretty clear that I left my window open over night two of the seven nights (Monday morning, Friday morning). What I did find surprising was how late it was before it started getting cooler. Most nights you can see it at least holding steady until I finally went to bed around 12:30am (Sunday night being the exception, due to the open window). On Thursday (second to last day) it's most dramatic, but when I leave for class, etc, the temperature markedly stops rising. I expected being home, awake, and active had an effect on my room's temperature, but certainly not to the degree shown on these graphs.


I also implemented a histogram feature on the last of the unused LCD space using the eight custom glyphs feature on the HD44780 controller. Essentially what I did was every hour (every half hour in the picture), stored the current temperature in an array of the last 40 (5 columns * 8 glyphs) such readings. I then calculated the maximum and minimum values for this set, and used that to normalize the values so they ranged between zero and eight, to calculate how many pixels high that column should be. The code ended up being pretty ugly, so I'll leave the specifics as a challenge for the student (don't you hate it when they say that?). The general idea is:
Bar height = (temperature - minimum temperature) * 8 / (maximum - minimum)
Storing the hourly temp readings in an array was just me being lazy. The I2C bus is pretty freakin fast, so the better solution would be to just read the values back off the EEPROM, since this only needs to be done once an hour, and that has the added advantage of not having an empty graph on reset.


Update 10/25/09: Data collected in my refrigerator.

16 comments:

  1. Hey Kenneth,

    I've came here many times to read about your setup. Good work!

    Would you be kind enough to post the circuit diagram so one can reproduce what you did?

    Best regards,

    Etienne

    ReplyDelete
  2. Thanks. Not to be a jerk, but I think you should try and figure out the wiring yourself. It's only 3 I2C devices, all with relatively readable datasheets. On the Arduino, analog 4 is SDA and analog 5 is SCL; connect all the SDA and SCL pins together. Refer to the LiquidCrystal library and search around for the pinout of your LCD module, if you have one.

    ReplyDelete
  3. I'm doing pretty much the same thing (building a weather logger) using an ATMega328, DS1307 RTC, AT24C1024B EEPROMs, and DS1631 temp sensor. I'm also using a Freescale MPXH6115 to get barometric pressure and a Honeywell HIH-4030 to get a humidity reading, with both of those being sampled via ADC ports 0 and 1. I have a 20x4 line LCD giving me date, time, temp, barometric pressure, and relative humidity, all updated four times a second.

    Next steps for me are gonna be adding a user interface and some buttons, followed by making the whole thing write data to the EEPROMs on user-selectable intervals. That part should put the F-U in fun...

    I2C can definitely be annoying to get working on a breadboard - bump a wrong wire and everything locks up. Short, low wires and decoupling caps are your friends!

    ReplyDelete
  4. Sounds pretty cool. I had considered moving in that direction, but the cost of the extra sensors turned me off, since I really have no real use for any of that information.

    Good to know about I2C. I've just happened to always use the short breadboard wires, so I haven't had a problem yet. Thanks for the comment.

    ReplyDelete
  5. Cool application. I'm also starting such kind of data logger but will use a sd card instead and a ds1621.
    For you graphic, excel is good, but have a look at gnuplot because it's commandline graph tool and i find it easier to quickly convert data into graph

    ReplyDelete
  6. hello keneth,
    how if it is PIC16F684?
    can u find out the code in c to store temperature data into this eeprom PIC16f684? I js want to save around 200 cycle (js an examples)

    tq,
    lizzy

    ReplyDelete
  7. @lizzy: Programming PICs is entirely different than AVRs, and I've never done it. Getting a PIC to talk to I2C chips shouldn't be that difficult, which is most of what you'd have to change to port this code to a PIC.

    ReplyDelete
  8. I have programmed my ATmega32 and connect it to the DS1307 based on typicial and standard connections to get a 1 Hz wave, but no sinal is appeared on the crystal pins and no signals on the DS1307 output.
    What Should I do?

    ReplyDelete
  9. The DS1307 doesn't start the oscillator until you clear the clock halt flag in the second register. You will have to use the I2C interface.

    ReplyDelete
  10. Could you use ds18s20?

    ReplyDelete
  11. You could, but that would require the addition of a 1 Wire library and changing out all of the temperature calls. Good luck.

    ReplyDelete
  12. Hi there!
    I have a similar setup right now, just that I am using an SD to store the data, but I am thinking about using an EEPROM instead (the SD library is way too big and it's giving me trouble plus I don't need that much storage anyway) and thus I came across your page.
    I got a bit concerned about the flashing/dumping requiring re-flashing, did you ever get around that issue?

    Great job btw!
    -rod

    ReplyDelete
  13. I never got around to it, but solving that issue wouldn't be a big deal. Just have the logging script check the serial port for commands to dump the logs and reset the address to the beginning.

    ReplyDelete
  14. Nice project - I just spotted a slight mistake? On your first picture you have no i2c pullup resistors. Adding these make i2c more reliable. 4k7 should do the trick.

    ReplyDelete
    Replies
    1. ATMega series controllers allow you to enable the internal pull-up resistors while enabling the I2C hardware module, so no, for short I2C buses with little load, you don't need pull-up resistors. You unfortunately can't play the same trick when using ATTiny chips...

      Delete
  15. Nice work man... can u plz tell me i2c address of at24c1024b chip? I know its address can be changed using A1 A2 pins on IC but what is the address if I left all this pins open..
    Wire.beginTransmission(address chip for example 0x67);

    What address u used for eeprom.. plz reply fast

    ReplyDelete