Build Project

IoT Projects

ESP32 DHT22 microSD CSV Logger

Log DHT22 readings to a microSD CSV file every ten seconds, using UTC after NTP sync and explicit unset-time and sensor-error fields.

IntermediateParent Safe
ESP32 DHT22 microSD CSV Logger logical overview illustration, not a photograph or physical pin layout
Jump to a section

What You Will Build

A climate logger that appends one CSV row every ten seconds, including failed sensor samples. It works without a browser or broker. Wi-Fi is used only to obtain UTC from NTP; before the clock is set, rows retain uptime but leave UTC empty. No DHT11 page is replaced.

Before You Start

Classic ESP32-WROOM DevKit, Arduino-ESP32 3.3.2 review baseline and a documented 3.3 V-compatible DHT22. Use a short-wire SPI adapter explicitly rated for 3.3 V supply with 3.3 V signals. A common adapter with a regulator intended for 5 V input is not automatically equivalent. Start with a backed-up FAT32 card; this tutorial does not format it. A 2.4 GHz network with internet/NTP access is optional for calendar timestamps. Documentation and code-structure review only; compilation and physical hardware testing have not been performed.

Parts

Wiring

The card and DHT22 share GND and the specified 3.3 V supply, but their signals are separate. A bare card and an adapter have different pin layouts: use labels on a verified adapter, not physical positions in this logical illustration. SD write-current peaks require a suitable supply and module decoupling. No adapter signal may pull an ESP32 input above 3.3 V. For a bare DHT22 leave NC unconnected and add DATA pull-up to 3V3.

ESP32 DHT22 microSD CSV Logger logical wiring with labelled signals; verify actual package orientation separately

Open wiring diagram full size

  1. 1

    Back up the card on a computer; use a supported FAT32 filesystem and safely eject it.

  2. 2

    With USB unplugged, connect the adapter SPI signals to GPIO18/19/23/25 and its verified 3.3 V supply.

  3. 3

    Wire DHT22 DATA to GPIO4 with the required 10 kΩ pull-up; connect its VCC/GND.

  4. 4

    Check actual module labels and supply capacity, then reconnect USB. Never remove the card during a write.

Wiring Reference

SignalESP32 PinDirectionNotes
DHT22 DATAGPIO4Bidirectional10 kΩ pull-up to 3V3 for bare sensor
DHT22 VCC + SD adapter supply3V3PowerOnly documented 3.3 V supply adapters
DHT22 GND + SD GNDGNDGroundCommon ground; short wires
SD SCKGPIO18OutputSPI clock, 4 MHz request
SD MISO / DOGPIO19Input3.3 V signal output
SD MOSI / DIGPIO23Output3.3 V signal input
SD CSGPIO25OutputExplicit chip-select constant

Libraries and Setup

  • Built-in SPI, SD, WiFi and time/SNTP APIs from Arduino-ESP32 3.3.2. No external SD library is required.
  • Install Adafruit DHT sensor library 1.4.6 and Adafruit Unified Sensor 1.1.15 dependency through Library Manager. Select DHT22 in the constructor; the historical DHT11 quick-build uses another type.

Configure the Example

Replace YOUR_WIFI_SSID and YOUR_WIFI_PASSWORD with local credentials; never publish them in screenshots. Open Serial at 115200. The code appends /climate.csv and writes a header only when the file is empty. If that filename already contains another schema or a damaged final row, archive/repair it on a computer before this test. UTC is formatted with Z; no local daylight-saving offset is applied. To read the file, finish a sample, disconnect board power, remove the card and open the CSV on a computer.

Control and Recovery Behavior

ConditionResult
No Wi-Fi15-second connection windows at a minimum 30-second start cadence; no endless connect loop. Samples still run.
Clock unsetUTC field empty and time_status UNSET; uptime remains available.
Clock set this bootSET_THIS_BOOT means SNTP supplied time at least once. It does not promise current synchronization or accuracy after an outage.
DHT22 failureBlank temperature/humidity fields and sensor_error; no invented zero value.
Card failureStops writes for this boot, continues Serial samples and reports NOT SAVED. Fix hardware/filesystem with power off and reset. No offline replay.

Code

Install the libraries above and match the configuration and wiring before uploading.

climate-logger.ino
#include <Arduino.h>
#include <WiFi.h>
#include <SPI.h>
#include <SD.h>
#include <DHT.h>
#include <time.h>
#include <sys/time.h>
#include <esp_sntp.h>
#include <math.h>
#include <cstring>

const char* SSID = "YOUR_WIFI_SSID";
const char* PASSWORD = "YOUR_WIFI_PASSWORD";
constexpr uint8_t DHT_PIN = 4, SD_CS = 25;
constexpr uint32_t SAMPLE_MS = 10000, WIFI_WINDOW_MS = 15000, WIFI_RETRY_MS = 30000;
const char* FILE_NAME = "/climate.csv";
const char* HEADER = "uptime_ms,utc,time_status,temp_c,humidity_pct,sensor_status\n";
DHT dht(DHT_PIN, DHT22);
volatile bool clockSet = false;
bool cardReady = false, wifiTrying = false, ntpStarted = false;
uint32_t wifiStarted = 0, lastSample = 0;

void timeReceived(struct timeval*) { clockSet = true; }
void startWiFi() {
  wifiStarted = millis(); wifiTrying = true;
  WiFi.begin(SSID, PASSWORD);
  Serial.println("Starting bounded Wi-Fi attempt");
}
void networkTask() {
  uint32_t now = millis();
  if (WiFi.status() == WL_CONNECTED) {
    wifiTrying = false;
    if (!ntpStarted) {
      sntp_set_time_sync_notification_cb(timeReceived);
      configTime(0, 0, "pool.ntp.org", "time.nist.gov"); // UTC, asynchronous SNTP.
      ntpStarted = true;
    }
  } else {
    if (wifiTrying && now - wifiStarted >= WIFI_WINDOW_MS) {
      WiFi.disconnect(false); wifiTrying = false;
      Serial.println("Wi-Fi unavailable; sampling continues without waiting");
    }
    if (!wifiTrying && now - wifiStarted >= WIFI_RETRY_MS) startWiFi();
  }
}
bool initializeCard() {
  if (!SPI.begin(18, 19, 23, SD_CS)) return false;
  if (!SD.begin(SD_CS, SPI, 4000000) || SD.cardType() == CARD_NONE) return false;
  File file = SD.open(FILE_NAME, FILE_APPEND);
  if (!file) return false;
  bool ok = true;
  if (file.size() == 0) ok = file.print(HEADER) == strlen(HEADER);
  file.close();
  return ok;
}
void recordSample() {
  float h = dht.readHumidity(), t = dht.readTemperature();
  bool sensorOK = isfinite(h) && isfinite(t);
  char utc[25] = "";
  time_t stamp = time(nullptr);
  bool hasTime = clockSet && stamp >= 1704067200; // Reject an obviously unset clock.
  if (hasTime) {
    struct tm tmUTC;
    gmtime_r(&stamp, &tmUTC);
    strftime(utc, sizeof(utc), "%Y-%m-%dT%H:%M:%SZ", &tmUTC);
  }
  String row = String(millis()) + "," + utc + "," + (hasTime ? "SET_THIS_BOOT" : "UNSET") + ",";
  if (sensorOK) row += String(t, 1) + "," + String(h, 1) + ",ok\n";
  else row += ",,sensor_error\n";
  Serial.print(row);
  if (!cardReady) { Serial.println("NOT SAVED: card unavailable; no replay buffer"); return; }
  File file = SD.open(FILE_NAME, FILE_APPEND);
  if (!file) { cardReady = false; Serial.println("Open failed; fix card and reset"); return; }
  size_t written = file.print(row);
  file.close();
  if (written != row.length()) {
    cardReady = false;
    Serial.println("Short write; stop logging, inspect file and reset after repair");
  } else Serial.println("Row accepted by filesystem; not a power-loss durability guarantee");
}
void setup() {
  Serial.begin(115200);
  dht.begin();
  cardReady = initializeCard();
  Serial.println(cardReady ? "CSV card ready" : "SD init failed; Serial sampling still runs");
  WiFi.mode(WIFI_STA); WiFi.setAutoReconnect(false);
  startWiFi();
  lastSample = millis(); // First DHT22 sample after 10 seconds.
}
void loop() {
  networkTask();
  uint32_t now = millis();
  if (now - lastSample >= SAMPLE_MS) { lastSample = now; recordSample(); }
  delay(2);
}

How the Code Works

Samples are spaced ten seconds apart, slower than the DHT22 minimum interval and the library’s 2000 ms cache. Consecutive humidity/temperature calls share a sensor transaction through that cache. Each row includes six fields: uptime_ms, utc, time_status, temp_c, humidity_pct and sensor_status. millis() wraps after about 49.7 days and resets on reboot; uptime is not a permanent unique ID. The SNTP callback only marks clock state. Disk operations are synchronous and can delay loop, so this is not a real-time recorder. Missed intervals are not backfilled.

Expected Output

After CSV card ready, Serial prints a row approximately every ten seconds. Before NTP a row has an empty UTC field and UNSET. After a sync it contains an ISO-style UTC timestamp and SET_THIS_BOOT. The CSV begins with the documented header. A sensor error leaves both reading fields empty. Examples describe expected output, not physical measurements made by the author.

Troubleshooting

  • SD init failed Verify actual adapter supply requirements, 3.3 V logic, CS25/SCK18/MISO19/MOSI23, card format and wiring. Do not cure an incompatible adapter by applying 5 V to ESP32 signals.
  • UNSET persists Check Wi-Fi credentials, DNS and outbound NTP access. A network connection alone is not a time sync. The logger still records uptime.
  • sensor_error rows Check DHT22 identity, GPIO4, power and pull-up. Preserve DHT22 constructor and sample spacing.
  • CSV tail damaged or rows missing Power loss or card removal can interrupt filesystem writes. Inspect a backed-up copy on a computer. A returned byte count/close is not a durability guarantee.
  • Old columns mixed with new data The example appends without validating an existing header. Archive an incompatible climate.csv before using the sketch.

Checks to Perform on Your Build

  • Verify a standalone DHT22 reading before combining with SD.
  • Read the resulting file on a computer and check six fields per row and a single header.
  • Test without Wi-Fi: UTC should stay empty until a successful NTP sync.
  • After first sync, disconnect network and observe that SET_THIS_BOOT is not a fresh-sync claim.
  • With power off, remove card then restart; verify Serial still reports unsaved samples.

Practical Design Notes

Closing each row reduces the open-file window but cannot guarantee survival of sudden power loss. Keep backups and power down before card removal; there is no hot-swap detection, automatic repair, rotation or full-card recovery. Files grow until capacity or an error stops writes. UTC can be adjusted by NTP and is not guaranteed monotonic. No battery-backed RTC is fitted, so a cold boot without network loses calendar time. NTP here is not authenticated. This is environmental logging, not a certified recorder.

Review, Testing, and References

Author: Abdul Mubeen and the ESP32 Engine editorial team. Last updated: 2026-10-24. Reviewed: wiring logic, Arduino code structure, beginner safety, and learning sequence.

Educational level: Intermediate. Estimated completion time: Varies by build. This project is for learning and prototyping; production or unattended hardware needs additional engineering review.