Project, a Standalone Data Logger
| Difficulty | Intermediate |
|---|---|
| Category | Projects |
| Supported boards | Arduino Uno R3 and compatible ATmega328P boards |
| Time needed | 120 minutes |
| Prerequisites | Logging Data to an SD Card |
| Tested board | Not yet verified |
| Tested on | Not yet verified |
| Arduino IDE version | Not yet verified |
| Library | Not yet verified |
What you will build
A box you leave somewhere for a week. It records a temperature every minute with a real date and time against it, and when you come back the card opens in a spreadsheet and draws a graph.
No laptop, no network, no watching. That is the whole point: this is the project that lets you find out what actually happened to the greenhouse overnight, or how cold the store room gets before dawn.
It is also the project where the failures matter. A logger that stops on Tuesday and tells nobody has wasted a week, so a good part of this page is about noticing and surviving.
Before you begin
Parts
| Part | Siratek product | Its own tutorial |
|---|---|---|
| Arduino Uno | 2219 | Getting Started with Arduino Uno |
| Micro SD card module | 6573 | Logging Data to an SD Card |
| DS3231 real time clock | 2522 | Keeping Time with a DS3231 |
| DS18B20 waterproof probe | 49704 | Waterproof Temperature with DS18B20 |
| 4.7 kilohm resistor | Required by the probe | |
| Micro SD card, 32 GB or smaller, FAT32 | ||
| Breadboard and jumper wires | Using a Breadboard |
What shares what
Three devices, three different ways of talking:
| Device | Bus | Pins |
|---|---|---|
| SD card | SPI | D10 to D13 |
| DS3231 | I2C | A4, A5 |
| DS18B20 | 1-Wire | D2 |
They do not conflict, which is pleasant and worth noticing: SPI, I2C and 1-Wire are three separate arrangements and can all run at once. Between them they take six pins, leaving most of the board free.
The DS3231 is at address 0x68. Nothing else here is on I2C, so there is no clash — but if you add an MPU-6050 later there will be, and Keeping Time with a DS3231 explains the fix.
The power budget
| Part | Typical | Peak |
|---|---|---|
| Uno | 50 mA | |
| SD card | a few mA idle | up to ~100 mA while writing |
| DS3231 | under 1 mA | |
| DS18B20 | about 1 mA |
On USB that is comfortable. The peak matters because a card write is exactly when a brownout is most damaging: a reset mid-write can leave the file system, not just the line, in a bad state.
The sketch below opens, writes and closes the file every single line for that reason. It is slower, and at one reading a minute the speed is irrelevant.
Choosing the interval
Worth a moment’s arithmetic before you start.
| Interval | Lines per day | A week |
|---|---|---|
| 1 second | 86,400 | 600,000 |
| 1 minute | 1,440 | 10,080 |
| 10 minutes | 144 | 1,008 |
A week at one minute is ten thousand lines, which is about 300 KB and opens instantly in a spreadsheet. A week at one second is six hundred thousand lines, which most spreadsheets will refuse or struggle with, and tells you nothing a minute’s average would not.
One minute is the right default for temperature. Faster only if the thing you are measuring genuinely changes faster.
How it works
Four things happen, at three different rates:
| Job | How often |
|---|---|
| Read the probe and the clock | Every minute |
| Write a line to the card | Every minute |
| Write a heartbeat line | Every hour, whether or not anything changed |
| Blink the LED | Every reading |
The heartbeat is the part people leave out and then regret. A log that only records events cannot distinguish “nothing happened” from “the logger stopped”. An hourly line, even an uninteresting one, means a gap in the file tells you exactly when something went wrong.
Pin reference
| Device | Pin | Connect to |
|---|---|---|
| SD module | GND |
Uno GND |
| SD module | VCC |
Uno 5V |
| SD module | MISO |
Uno D12 |
| SD module | MOSI |
Uno D11 |
| SD module | SCK |
Uno D13 |
| SD module | CS |
Uno D10 |
| DS3231 | GND, VCC |
Uno GND, 5V |
| DS3231 | SDA, SCL |
Uno A4, A5 |
| DS18B20 red | Uno 5V |
|
| DS18B20 black | Uno GND |
|
| DS18B20 yellow | Uno D2, and 4.7 k to 5V |
Build it in stages
Stage 1: each part alone
Run the minimal example from each of the three component tutorials, one at a time. Specifically, confirm:
- The card initialises and lists its contents.
- The clock returns a sensible time, with
lostPower()handled so it is not reset on every boot. - The probe reports a device count of 1, not 0, and a plausible temperature.
A device count of 0 means the 4.7 k resistor is missing. A -127.00 means the same thing.
Stage 2: all three, to serial
Combine them with no card writing at all: read the clock and the probe once a minute and print a CSV line to the serial monitor.
Watch it for five minutes. Then unplug the probe and watch what happens, because that is a rehearsal for the thing that will go wrong in the field.
Stage 3: write to the card
Add the file. The complete sketch below.
The complete sketch
// Siratek: a standalone temperature logger.
// Writes one CSV line a minute, with a real timestamp, and keeps going
// when a part fails.
#include <SPI.h>
#include <SD.h>
#include <Wire.h>
#include <RTClib.h>
#include <OneWire.h>
#include <DallasTemperature.h>
const int chipSelect = 10;
const int ledPin = LED_BUILTIN;
const char* FILENAME = "LOG.CSV"; // 8.3 names only
RTC_DS3231 rtc;
OneWire oneWire(2);
DallasTemperature probe(&oneWire);
const unsigned long INTERVAL = 60000UL; // one minute
const unsigned long HEARTBEAT = 3600000UL; // one hour
unsigned long lastReading = 0;
unsigned long lastHeartbeat = 0;
bool cardReady = false;
bool clockReady = false;
unsigned long linesWritten = 0;
unsigned long writeFailures = 0;
void setup() {
pinMode(ledPin, OUTPUT);
Serial.begin(9600);
Serial.println(F("Siratek: data logger starting."));
clockReady = rtc.begin();
if (!clockReady) {
Serial.println(F(" no clock: timestamps will be elapsed minutes"));
} else if (rtc.lostPower()) {
Serial.println(F(" clock had lost power, setting from compile time"));
rtc.adjust(DateTime(F(__DATE__), F(__TIME__)));
}
probe.begin();
Serial.print(F(" probes found: "));
Serial.println(probe.getDeviceCount());
if (probe.getDeviceCount() == 0) {
Serial.println(F(" check the 4.7k between data and 5V"));
}
cardReady = SD.begin(chipSelect);
if (!cardReady) {
Serial.println(F(" no card: logging disabled, serial only"));
} else if (!SD.exists(FILENAME)) {
File f = SD.open(FILENAME, FILE_WRITE);
if (f) {
f.println(F("timestamp,temperature_c,note"));
f.close();
Serial.println(F(" created LOG.CSV with a header"));
}
}
// Take the first reading straight away rather than waiting a minute.
lastReading = millis() - INTERVAL;
lastHeartbeat = millis();
}
// Build a timestamp, or an elapsed count if the clock is missing.
void appendTimestamp(String& line) {
if (!clockReady) {
line += F("elapsed_");
line += String(millis() / 60000UL);
return;
}
DateTime now = rtc.now();
char buf[20];
snprintf(buf, sizeof(buf), "%04d-%02d-%02d %02d:%02d:%02d",
now.year(), now.month(), now.day(),
now.hour(), now.minute(), now.second());
line += buf;
}
void writeLine(const String& line) {
Serial.println(line);
if (!cardReady) {
return;
}
// Open, write, close, every time: a closed file survives a power cut.
File f = SD.open(FILENAME, FILE_WRITE);
if (f) {
f.println(line);
f.close();
linesWritten++;
} else {
writeFailures++;
Serial.println(F(" could not open the file"));
// The card may have been removed. Try to pick it up again next time.
if (writeFailures > 3) {
cardReady = SD.begin(chipSelect);
writeFailures = 0;
}
}
}
void takeReading() {
digitalWrite(ledPin, HIGH);
probe.requestTemperatures();
float c = probe.getTempCByIndex(0);
String line = "";
appendTimestamp(line);
line += ",";
if (c == DEVICE_DISCONNECTED_C) {
line += ",probe not responding";
} else {
line += String(c, 1);
line += ",";
}
writeLine(line);
digitalWrite(ledPin, LOW);
}
void heartbeat() {
String line = "";
appendTimestamp(line);
line += ",,alive: ";
line += String(linesWritten);
line += " lines";
writeLine(line);
}
void loop() {
if (millis() - lastReading >= INTERVAL) {
lastReading = millis();
takeReading();
}
if (millis() - lastHeartbeat >= HEARTBEAT) {
lastHeartbeat = millis();
heartbeat();
}
}
Four decisions that make this a logger rather than a demonstration
It runs without any of its parts. No card, it logs to serial. No clock, it timestamps with elapsed minutes. No probe, it writes a line saying so. A logger that refuses to start because one thing is missing has thrown away everything else it could have recorded.
The probe failure is recorded, not skipped. A missing line is ambiguous; a line saying “probe not responding” is evidence. When you open the file in a week, the difference matters.
It tries to recover the card. Three failures in a row and it calls SD.begin() again, which picks up a card that was removed and put back, or one that glitched. It is four lines and it turns a permanent failure into a gap.
The heartbeat proves it is alive. An hourly line means a gap in the file has a start time and an end time. Without it you know only that the file stops.
A note on String
This sketch uses String to build each line, and at one line a minute that is fine: the heap has time to settle between allocations.
If you log every second, replace it with snprintf() into a fixed char buffer. Logging Data to an SD Card explains why, and the symptom of ignoring it is a sketch that runs for hours and then freezes.
Testing it
Before leaving it anywhere, do these five. Each one rehearses a real failure.
| Try this | It should |
|---|---|
| Run it for five minutes | Five lines, a minute apart, with sensible times |
| Pull the probe out | Keep logging, with “probe not responding” |
| Put the probe back | Resume recording temperatures |
| Pull the card out and put it back | Report failures, then recover within a few minutes |
| Unplug the power mid-run | On restart, continue appending to the same file |
The last is the important one. Unplug it, plug it back in, and open the card: the header should appear once, not twice, and the earlier lines should still be there.
Then take the card to a computer, open LOG.CSV in a spreadsheet, and chart the temperature column. That is the project working.
Making it yours
- More sensors. A second DS18B20 on the same pin and the same resistor, which is the whole point of 1-Wire. Add a column per probe.
- Humidity. A DHT22 on a spare pin. The weather station project has the reading code.
- Run it on batteries. This is the real upgrade, and it leads straight to Running an ATmega328P Without the Board: the Uno’s regulator and USB chip draw far more than the logging does. A bare chip asleep between readings runs for months.
- A display. An OLED showing the current reading and the line count, so you can tell at a glance that it is working without pulling the card.
- A watchdog. Making a Project Survive Unattended covers it, and this is exactly the kind of project it exists for.
Troubleshooting
| Symptom | Likely cause | Check or fix |
|---|---|---|
| Card will not initialise | Too large, or not FAT32 | 32 GB or smaller, FAT32 |
| Card fails intermittently | Long jumper leads, or a cheap card | Shorten them; try another card |
| File never appears | Filename longer than 8.3 | LOG.CSV, not templog.csv |
| Header appears twice | Written unconditionally | Guard it with SD.exists() |
| Temperature reads -127 | No 4.7 k pull-up | Between the data wire and 5V |
| Probe count is 0 | Same cause | Fit the resistor |
| Timestamps all identical | Clock not set | Check lostPower() handling |
| Timestamps reset on every boot | adjust() called unconditionally |
Guard it with lostPower() |
Timestamps say elapsed_ |
Clock not found | Scan the bus; expect 0x68 |
| File corrupted after a power cut | File left open | Open, write, close each line |
| Logger stopped and you cannot tell when | No heartbeat | An hourly line gives you the gap |
| Runs for hours then freezes | String fragmenting the heap |
snprintf() into a fixed buffer |
Sources and attribution
All text, wiring guidance and code in this project are original Siratek work.
- Component behaviour is as described in Logging Data to an SD Card, Keeping Time with a DS3231 and Waterproof Temperature with DS18B20, including the 8.3 filename limit, the
lostPower()behaviour and the probe’s required pull-up resistor.
Current figures are typical and vary with the card. No third-party tutorial text, diagrams, photographs or code has been reproduced here.

