When you search for example code for Arduino, you usually find fragmented snippets that assume perfect hardware conditions. In the real world, I2C buses hang, SD cards fail to mount, and microcontrollers brown out. A robust data logger needs explicit pin definitions, hardware fault detection, and a watchdog timer to recover from fatal locks.

This guide provides a complete, production-ready architecture for logging temperature, humidity, and barometric pressure. We are pairing an I2C environmental sensor with an SPI-based storage module to avoid bus contention, wrapping it all in a C++ sketch that handles initialization failures gracefully.

The Decision Path: Which Arduino Board and Sensor Combo?

Before writing a single line of code, you must select hardware that matches your power and memory constraints. Here is the decision framework for choosing your microcontroller and sensor breakout for a logging project.

Scenario / Requirement Recommended Board Why It Wins
Bench prototyping & 5V logic tolerance Arduino Uno R3 (ATmega328P) Massive physical footprint makes wiring easy; 5V I/O matches standard SD modules.
Battery-powered remote deployment Arduino Nano 33 IoT (SAMD21) Native 3.3V logic, deep sleep modes, and built-in crypto/WiFi for MQTT offloading.
Compact, low-cost robust logging (Our Pick) Arduino Nano V3.0 (ATmega328P) Small breadboard footprint, 32KB flash, native AVR watchdog timer support, and cheap to replace if you fry a clone.
Default Recommendation: For this specific example code, we are targeting the Arduino Nano V3.0 (ATmega328P). It provides the perfect balance of 5V SPI compatibility for standard SD card modules and a compact physical profile. If you are using a Nano Every (ATmega4809), note that the AVR watchdog library syntax differs slightly.

Hardware Spec Sheet and Pin Mapping

Mixing I2C and SPI on the same ATmega328P is highly reliable because they use entirely separate hardware peripherals and pins. This prevents the MISO/MOSI bus contention issues that plague dual-SPI designs.

Bill of Materials (BOM)

  • Microcontroller: Arduino Nano V3.0 (Official ABX00028 or reputable ATmega328P clone) - ~$12.00
  • Sensor: Adafruit BME280 I2C Breakout (Product ID 2652) - ~$19.95. Note: Generic $3 clones often lack onboard 3.3V regulators and pull-up resistors, leading to I2C bus noise.
  • Storage: Adafruit MicroSD Breakout Board+ (Product ID 254) - ~$7.50. Includes a 3.3V logic level shifter, which is mandatory when driving from the Nano's 5V SPI pins.
  • Media: 8GB or 16GB MicroSDHC card (Formatted strictly to FAT32).

Wiring Pinout Table

Module Module Pin Arduino Nano Pin Wire Color (Suggested)
BME280 VIN 5V Red
BME280 GND GND Black
BME280 SCK (SCL) A5 Blue
BME280 SDI (SDA) A4 Green
MicroSD 5V 5V Red
MicroSD GND GND Black
MicroSD CS D10 Orange
MicroSD DI (MOSI) D11 Yellow
MicroSD DO (MISO) D12 Purple
MicroSD CLK (SCK) D13 White

The Complete, Compilable Example Code for Arduino

This sketch targets the ATmega328P architecture. It utilizes the hardware Wire library for I2C and includes the AVR Watchdog Timer (WDT) to automatically reboot the Nano if the main loop hangs for more than 8 seconds—a common failure mode when an SD card write operation blocks indefinitely.

#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include <Adafruit_Sensor.h>
#include <Adafruit_BME280.h>
#include <avr/wdt.h>

// --- PIN DEFINITIONS ---
#define SD_CS_PIN      10
#define STATUS_LED_PIN 13
#define BME_I2C_ADDR   0x77 // Adafruit breakouts use 0x77; some clones use 0x76

// --- OBJECT INSTANTIATION ---
Adafruit_BME280 bme;
File dataFile;

// --- FAULT HANDLING ---
void haltWithError(const char* errorMsg) {
  Serial.println(errorMsg);
  // Blink LED rapidly to indicate hardware fault
  while (1) {
    digitalWrite(STATUS_LED_PIN, HIGH);
    delay(100);
    digitalWrite(STATUS_LED_PIN, LOW);
    delay(100);
    wdt_reset(); // Keep watchdog alive so we don't reboot in a tight fault loop
  }
}

void setup() {
  pinMode(STATUS_LED_PIN, OUTPUT);
  digitalWrite(STATUS_LED_PIN, HIGH); // Solid ON during setup
  
  Serial.begin(115200);
  while (!Serial) { delay(10); } // Wait for native USB serial (ignored on Nano V3)
  
  // Enable 8-second Watchdog Timer
  wdt_enable(WDTO_8S);
  
  Serial.println(F("Initializing BME280 and SD Logger..."));

  // 1. Initialize I2C Sensor
  if (!bme.begin(BME_I2C_ADDR)) {
    haltWithError("FATAL: Could not find a valid BME280 sensor, check wiring or I2C address!");
  }
  
  // Configure BME280 for forced mode to save power between reads
  bme.setSampling(Adafruit_BME280::MODE_FORCED,
                  Adafruit_BME280::SAMPLING_X1, // Temp
                  Adafruit_BME280::SAMPLING_X1, // Pressure
                  Adafruit_BME280::SAMPLING_X1, // Humidity
                  Adafruit_BME280::FILTER_OFF);

  // 2. Initialize SPI SD Card
  if (!SD.begin(SD_CS_PIN)) {
    haltWithError("FATAL: SD initialization failed! Check CS pin and FAT32 format.");
  }
  
  digitalWrite(STATUS_LED_PIN, LOW); // Setup complete, LED OFF
  Serial.println(F("Setup complete. Logging data..."));
}

void loop() {
  wdt_reset(); // Reset watchdog timer at the start of every loop

  // Trigger a forced reading
  bme.takeForcedMeasurement();
  
  float tempC = bme.readTemperature();
  float humidity = bme.readHumidity();
  float pressurePa = bme.readPressure();
  unsigned long runtimeMs = millis();

  // Open file in append mode
  dataFile = SD.open("datalog.csv", FILE_WRITE);
  
  if (dataFile) {
    // Write CSV formatted data
    dataFile.print(runtimeMs);
    dataFile.print(",");
    dataFile.print(tempC);
    dataFile.print(",");
    dataFile.print(humidity);
    dataFile.print(",");
    dataFile.println(pressurePa);
    
    dataFile.close(); // Crucial: flushes buffer to physical card
    
    // Echo to serial for bench debugging
    Serial.print("Logged: ");
    Serial.print(tempC); Serial.print(" C, ");
    Serial.print(humidity); Serial.print(" %, ");
    Serial.print(pressurePa); Serial.println(" Pa");
  } else {
    Serial.println("ERROR: Failed to open datalog.csv for writing.");
  }

  // Wait 10 seconds before next reading (non-blocking delay preferred in advanced builds)
  for (unsigned long start = millis(); millis() - start < 10000; ) {
    wdt_reset(); // Pet the dog during the delay
    delay(100);
  }
}

Debugging: First Three Things to Check When It Fails

When your compile fails or the hardware hangs, do not start rewriting the logic. 90% of embedded failures are physical layer or environment issues. Here are the exact error strings and how to fix them.

1. Exact Error: SD initialization failed!

If the serial monitor prints this and the LED starts rapid-blinking, the ATmega328P cannot mount the filesystem.

  • Cause A (Most Likely): The SD card is formatted as exFAT or NTFS. The standard Arduino SD.h library only supports FAT16 and FAT32. Use the official SD Card Formatter tool to format cards 32GB or smaller to FAT32.
  • Cause B: You are using a 64GB+ SDXC card. The SD.h library struggles with SDHC/SDXC high-capacity addressing. Stick to 16GB or 32GB MicroSDHC cards.
  • Cause C: SPI bus contention. Ensure no other SPI devices are sharing the bus without proper Chip Select (CS) management. If you have an SPI display connected, its CS pin must be held HIGH when not in use.

2. Exact Error: Could not find a valid BME280 sensor!

The I2C bus scanned the address space and found nothing at 0x77.

  • Cause A: I2C Address mismatch. Many cheap clone BME280 boards route the SDO pin to GND internally, shifting the address to 0x76. Change #define BME_I2C_ADDR 0x77 to 0x76 in the code and re-upload.
  • Cause B: Missing pull-up resistors. The I2C specification requires pull-ups on SDA and SCL. While the Adafruit BME280 breakout includes 10kΩ pull-ups onboard, generic bare-bones modules do not. If using a clone, solder 4.7kΩ resistors between the SDA/SCL lines and the 3.3V VCC pin.

3. Exact Error: fatal error: Adafruit_BME280.h: No such file or directory

This is an IDE-level compilation failure before the code ever reaches the board.

  • Cause A: You installed the wrong library. Open the Arduino IDE Library Manager (Ctrl+Shift+I), search for "Adafruit BME280", and install the official library by Adafruit. It will prompt you to install dependencies; click "Install All" to ensure Adafruit Unified Sensor is also present.

How to Extend or Simplify the Build

Depending on your deployment environment, you may need to strip this project down to its bare essentials or scale it up for remote telemetry.

To Simplify (The Bench Test Build)

If you are just testing the sensor on your desk and don't need persistent storage:

  1. Remove the #include <SD.h> and all SD.begin() / dataFile logic.
  2. Remove the avr/wdt.h watchdog includes. Watchdogs are for unattended deployments; they are a nuisance when you are actively tweaking code on the bench.
  3. Rely purely on Serial.print() and view the output in the Serial Plotter.

To Extend (The Remote IoT Build)

If this logger is going inside a weatherproof enclosure on a pole:

  1. Add Real-Time: The ATmega328P millis() counter rolls over after ~49 days and doesn't know the actual date. Add a DS3231 RTC module on the same I2C bus (it uses address 0x68, so it won't conflict with the BME280).
  2. Add Telemetry: Swap the Arduino Nano V3 for an ESP32-WROOM-32. You will need to update the pin definitions (ESP32 uses different GPIO mappings for hardware I2C/SPI) and replace the SD logging block with an MQTT publish payload via WiFi.
  3. Power Management: Put the BME280 into deep sleep and use the ESP32's native touch-wake or RTC-wake features to draw microamps between 15-minute logging intervals.
Pro-Tip on I2C Wire Length: The I2C bus was designed for chips on the same PCB, not devices across a room. The I2C specification limits bus capacitance to 400pF. Standard breadboard jumper wires add roughly 10pF to 15pF per foot. If your BME280 is more than 2 feet away from the Arduino Nano, the signal edges will degrade, causing silent data corruption. Keep I2C wires under 12 inches, or switch to an RS-485 transceiver for long-distance sensor runs.