If you are building a humidity sensor Arduino project, your first and most critical decision is which sensor module to buy. The ubiquitous DHT11 and DHT22 sensors are famous for throwing NaN (Not a Number) errors and failing when Wi-Fi interrupts fire on ESP boards. For any build requiring reliable, continuous data logging, the decision-forward choice is the Sensirion SHT31-D. It uses the I2C protocol, eliminating the microsecond-level timing vulnerabilities of the DHT series, and provides ±2% RH accuracy.

This guide walks through the hardware decision matrix, provides exact wiring for the Arduino Uno R3, delivers robust compilable code with error handling, and details the bench-level debugging steps for when your I2C bus throws NACK errors.

The Humidity Sensor Arduino Decision Matrix

Before wiring anything, use this decision tree to select the right sensor for your specific environmental constraints. We evaluate the four most common hobbyist modules based on protocol, accuracy, and interrupt sensitivity.

Sensor Module Protocol RH Accuracy Interrupt Safe? Typical Price (2026)
DHT11 Custom 1-Wire ±5% No (Fails with Wi-Fi) $1.50
DHT22 (AM2302) Custom 1-Wire ±2% No (Fails with Wi-Fi) $4.50
AHT20 I2C ±2% Yes $2.50
SHT31-D I2C ±2% (High Stability) Yes $14.95
The Concrete Pick: If your budget allows, buy the Adafruit SHT31-D Breakout (Product ID: 2857). The DHT series requires disabling interrupts for up to 5ms during reads, which starves Wi-Fi stacks on ESP8266/ESP32 boards and causes dropped packets. The SHT31-D uses standard I2C, meaning the microcontroller can handle network traffic while waiting for the sensor. If you need a sub-$3 I2C alternative, the AHT20 is acceptable, but the SHT31-D features superior long-term drift stability and a wider operating voltage range.

Parts List and Pin Mapping for the SHT31-D

This build targets the classic Arduino Uno R3 (ATmega328P) operating at 5V. The Adafruit SHT31-D breakout includes an onboard 3.3V LDO regulator and I2C level shifters, making it directly compatible with 5V logic without frying the silicon.

Required Hardware

  • Microcontroller: Arduino Uno R3 (or compatible ATmega328P clone)
  • Sensor: Adafruit SHT31-D Temperature & Humidity Breakout (PID 2857)
  • Wiring: 4x Male-to-Male jumper wires (keep under 30cm to avoid I2C capacitance issues)
  • Breadboard: Standard 830-point solderless breadboard

Pin Mapping Table

SHT31-D Breakout Pin Arduino Uno R3 Pin Function / Notes
VIN 5V Powers the onboard LDO and level shifters
GND GND Common ground reference
SDA A4 I2C Data (Includes onboard 10k pull-ups)
SCL A5 I2C Clock
ADR Not Connected Leave floating for default I2C address 0x44

Step-by-Step Wiring and Compilable Code

Difficulty: Beginner | Time to Build: 15 Minutes | Board Target: Arduino Uno R3 (AVR)

Wiring Procedure

  1. De-energize the board: Ensure the Arduino Uno is unplugged from USB before making I2C connections.
  2. Power the breakout: Connect the breakout VIN to the Arduino 5V pin, and GND to GND.
  3. Route the I2C bus: Connect SDA to A4 and SCL to A5. Keep these wires parallel and under 30cm (12 inches) to prevent bus capacitance from exceeding the 400pF I2C limit.
  4. Verify Address: Ensure the ADR (Address) pad on the breakout is not bridged with solder. This keeps the default I2C address at 0x44.

Compilable Arduino Code

You will need the Adafruit SHT31 and Adafruit Unified Sensor libraries installed via the Arduino Library Manager. This code includes robust initialization checks and read-state error handling to prevent the sketch from hanging if the sensor drops off the bus.

#include <Wire.h>
#include "Adafruit_SHT31.h"

// Target: Arduino Uno R3 (ATmega328P)
// I2C Pins: SDA = A4, SCL = A5

// Initialize sensor object
Adafruit_SHT31 sht31 = Adafruit_SHT31();

// Track last successful read to prevent serial spam on failure
unsigned long lastReadTime = 0;
const unsigned long READ_INTERVAL = 2000; // Read every 2 seconds

void setup() {
  Serial.begin(115200);
  // Wait for serial port to connect (useful for Leonardo/Micro, harmless on Uno)
  while (!Serial) delay(10);

  Serial.println("SHT31-D I2C Humidity Sensor Test");

  // Initialize I2C bus
  Wire.begin();

  // Attempt to initialize the sensor at default address 0x44
  if (!sht31.begin(0x44)) {
    Serial.println("CRITICAL ERROR: Couldn't find SHT31-D at 0x44");
    Serial.println("Check I2C wiring, pull-up resistors, and ADR pin state.");
    // Halt execution to prevent infinite error loops
    while (1) {
      delay(1000);
    }
  }
  
  Serial.println("Sensor initialized successfully.");
}

void loop() {
  unsigned long currentMillis = millis();

  if (currentMillis - lastReadTime >= READ_INTERVAL) {
    lastReadTime = currentMillis;

    // Read temperature and humidity
    float tempC = sht31.readTemperature();
    float humidity = sht31.readHumidity();

    // Error Handling: Check for NaN (Not a Number) returns
    if (isnan(tempC) || isnan(humidity)) {
      Serial.println("ERROR: Failed to read SHT31 data. I2C NACK received.");
      // Optional: Attempt I2C bus recovery by re-initializing Wire
      Wire.end();
      delay(50);
      Wire.begin();
    } else {
      // Calculate Fahrenheit for US users
      float tempF = (tempC * 1.8) + 32;
      
      Serial.print("Temp: ");
      Serial.print(tempC);
      Serial.print(" C (");
      Serial.print(tempF);
      Serial.print(" F) | Humidity: ");
      Serial.print(humidity);
      Serial.println(" %");
    }
  }
}

Debugging: Fixing Timeouts and "NaN" Errors

When working with environmental sensors, the serial monitor will inevitably throw errors. Here is how to diagnose the exact failure modes based on the error strings returned by the code or the hardware behavior.

Error 1: "ERROR: Failed to read SHT31 data. I2C NACK received."

This means the Arduino sent a clock pulse, but the SHT31-D did not acknowledge (NACK) the transaction. The sensor is physically connected but logically invisible.

  • Cause A (Most Likely): Missing or weak pull-up resistors. The Adafruit breakout includes 10kΩ pull-ups. If you are using a bare SHT31 chip or a cheap clone board without pull-ups, the I2C lines will float. Fix: Solder 4.7kΩ resistors between SDA-VCC and SCL-VCC.
  • Cause B: Bus Capacitance Overload. If your jumper wires exceed 30cm, the parasitic capacitance of the wire exceeds the 400pF I2C spec, rounding off the square clock waves into unreadable slopes. Fix: Shorten wires, or drop the I2C clock speed by adding Wire.setClock(50000); after Wire.begin();.

Error 2: Legacy DHT Sensors returning "NaN"

If you ignored the decision matrix and are using a DHT22, you will frequently see NaN in your serial output. The DHT protocol requires the MCU to time pulse widths with microsecond precision. If a hardware interrupt (like a timer or Wi-Fi stack) fires during the 40-bit read window, the timing is ruined, and the library returns NaN.

  • Cause A: Wi-Fi Interrupt Starvation. On ESP8266/ESP32 boards, background RF calibration interrupts block the DHT read. Fix: Switch to an I2C sensor (SHT31/AHT20), or use the yield() function aggressively between reads.
  • Cause B: Insufficient Power Delivery. The DHT22 draws up to 1.5mA during conversion. If powered from a weak 3.3V LDO on a clone ESP board, voltage sags cause bit-flips. Fix: Power the DHT22 from the 5V pin (if it has an onboard LDO) or add a 100nF decoupling capacitor directly across the VCC and GND pins of the sensor.

The First 3 Things to Check When Any Sensor Fails

  1. Run an I2C Scanner: Upload the standard Arduino "I2C Scanner" sketch. If the SHT31 doesn't show up at 0x44 (or 0x45), you have a physical wiring or pull-up issue, not a code issue.
  2. Measure VCC at the Breakout: Use a multimeter to probe the VIN and GND pins on the breadboard. You should read 4.8V to 5.2V. If it reads 3.3V, you are underpowering the onboard level shifters.
  3. Check for Address Collisions: If you have multiple I2C devices on the bus (e.g., an OLED display and the SHT31), ensure they don't share the same hex address. Use the ADR pad to shift the SHT31 to 0x45 if necessary.

Extending and Simplifying Your Build

Once your baseline humidity sensor Arduino circuit is logging reliably to the serial monitor, you can scale the project for real-world deployment.

Extension: Adding SD Card Data Logging

To log data for greenhouse or server-room monitoring, add a MicroSD card breakout. Wire the SD breakout using the SPI bus (Pins 10-13 on the Uno R3), leaving the I2C bus (A4/A5) exclusively for the SHT31-D. Use the SdFat library rather than the stock SD library for better memory management and faster write times, which prevents I2C read delays while the SPI bus is writing to the card.

Simplification: Migrating to ESP32 for MQTT

If your end goal is pushing humidity data to Home Assistant via MQTT, drop the Arduino Uno R3 and migrate to an ESP32-DevKitC V4. The ESP32 natively supports 3.3V logic, meaning you can bypass the level-shifters and wire a bare SHT31 chip or 3.3V breakout directly to GPIO 21 (SDA) and GPIO 22 (SCL). Use the PubSubClient library to publish the humidity and temperature floats as JSON payloads to your local Mosquitto broker.

Final Bench Tip: Never mount your humidity sensor directly above a heat-generating component like a voltage regulator or a microcontroller's CPU. The localized thermal plume will artificially lower the Relative Humidity (RH) reading. Mount the sensor on a remote pigtail or ensure at least 5cm of clearance above any PCB heat sources.

For deeper technical specifications on I2C timing and sensor calibration, refer to the Adafruit SHT31-D Guide and the official Arduino Wire Library Documentation. Understanding the physical layer of your I2C bus is the difference between a sensor that works on the bench and one that survives in the field.