Why Native AVR Sleep Beats Third-Party Libraries

When building battery-powered sensor nodes, dropping your microcontroller's current draw from 20mA to under 10µA is the difference between changing batteries every week and every year. While many tutorials default to the popular LowPower.h library, relying on native AVR headers (<avr/sleep.h> and <avr/power.h>) gives you granular control over peripheral clocks and avoids ISR (Interrupt Service Routine) vector conflicts.

This guide targets the Arduino Nano V3 (ATmega328P, 16MHz). We will build a soil/environmental monitor that reads a DHT22 sensor, transmits data over Serial, and enters SLEEP_MODE_PWR_DOWN until triggered by a physical pushbutton on the INT0 pin.

Target Board Variant: Arduino Nano V3 with the ATmega328P chip and the "Old Bootloader" or standard Optiboot. If you are using an ATmega168 variant, the sleep registers are identical, but the flash memory will limit larger sensor libraries.

Hardware Spec Sheet & Pin Mapping

To achieve microamp-level sleep currents, every component on the board must be accounted for. The standard Arduino Nano has a power LED and a 5V linear regulator that will parasitically drain your battery even when the ATmega328P is asleep. For true low-power operation, use a 3.3V Nano clone without the power LED, or physically desolder the LED resistor.

Component Exact Variant Role in Circuit Quiescent / Sleep Current
Microcontroller Arduino Nano V3 (ATmega328P) Main logic & sensor polling ~0.1mA (PWR_DOWN, LED removed)
Sensor DHT22 / AM2302 Temperature & Humidity reading ~0.5mA (idle), 1.5mA (measuring)
Wakeup Trigger 6x6mm Tactile Pushbutton Hardware interrupt on INT0 0mA (open), 2mA (pressed w/ pull-up)
Pull-up Resistor 10kΩ Carbon Film External pull-up for clean edge Negligible

Pin Mapping Table

Arduino Nano Pin ATmega328P Port/Pin Connected To Notes
D2 PD2 (INT0) Pushbutton (Normally Open to GND) Must use INT0 or INT1 for PWR_DOWN wakeup
D4 PD4 DHT22 Data Pin Requires 10kΩ pull-up to 3.3V/5V
D13 PB5 Onboard LED Explicitly forced LOW in code to save power
5V / 3V3 VCC DHT22 VCC, Pushbutton Pull-up Use 3.3V if running Nano at 8MHz
GND GND Common Ground Shared with battery negative terminal

Step-by-Step: Wiring and Native Sleep Implementation

Follow these steps to wire the breadboard. Ensure your multimeter is set to the µA range to verify sleep current after assembly.

  1. Prep the Nano: If your Nano has a red "ON" LED, carefully desolder the current-limiting resistor next to it (usually marked with an arrow or "LED"). This single step drops sleep current by ~10mA.
  2. Wire the Wakeup Button: Connect one leg of the tactile switch to GND. Connect the opposite leg to Digital Pin 2 (INT0).
  3. Configure the Pull-up: While the ATmega328P has internal pull-ups, they can be unstable during the sleep transition. Wire a 10kΩ external resistor from D2 to VCC (5V or 3.3V) to guarantee a clean HIGH state when the button is released.
  4. Wire the DHT22: Connect Pin 1 to VCC, Pin 2 to D4, and Pin 4 to GND. Place a 10kΩ resistor between Pin 1 and Pin 2. Leave Pin 3 unconnected.
  5. Verify Dead State: Before uploading code, use a multimeter to ensure there are no shorts between VCC and GND.

The Complete Compilable Code

This sketch uses native AVR registers. It disables the ADC and Timer0 before sleeping to eliminate peripheral bias currents. Board Selection: Select "Arduino Nano" and "ATmega328P" in the Arduino IDE. Requires the Adafruit DHT sensor library.

#include <avr/sleep.h>
#include <avr/power.h>
#include <DHT.h>

// --- PIN DEFINITIONS ---
#define WAKE_PIN 2       // Hardware INT0
#define DHT_PIN 4        // DHT22 Data
#define DHT_TYPE DHT22

DHT dht(DHT_PIN, DHT_TYPE);

void setup() {
  Serial.begin(9600);
  
  // Configure wakeup pin with external pull-up assumption
  pinMode(WAKE_PIN, INPUT);
  
  // Explicitly turn off onboard LED to prevent parasitic drain
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);
  
  dht.begin();
  Serial.println(F("System Initialized. Going to sleep..."));
  Serial.flush();
}

void loop() {
  // 1. Wake up sequence: Re-enable Timer0 for DHT bit-banging
  power_timer0_enable();
  delay(250); // DHT22 requires 2s between reads, but we just woke up
  
  // 2. Read Sensor
  float h = dht.readHumidity();
  float t = dht.readTemperature();

  // Error handling for sensor timeout/disconnect
  if (isnan(h) || isnan(t)) {
    Serial.println(F("ERROR: DHT22 read failed. Check data pin wiring."));
  } else {
    Serial.print(F("Temp: ")); Serial.print(t);
    Serial.print(F(" C | Hum: ")); Serial.print(h); Serial.println(F(" %"));
  }

  // 3. Prepare for sleep
  Serial.flush(); // CRITICAL: Wait for TX buffer to empty before killing UART
  power_all_disable(); // Shut down ADC, UART, Timers, SPI, I2C

  // 4. Configure Sleep Mode
  set_sleep_mode(SLEEP_MODE_PWR_DOWN);
  sleep_enable();
  
  // Disable Brown-out Detector (BOD) for maximum power saving (ATmega328P specific)
  MCUCR = bit(BODS) | bit(BODSE);
  MCUCR = bit(BODS);

  // 5. Attach Interrupt and Sleep
  noInterrupts(); // Prevent race conditions during ISR attachment
  attachInterrupt(digitalPinToInterrupt(WAKE_PIN), wakeUpISR, LOW);
  interrupts();
  
  // Enter sleep - CPU halts here
  sleep_cpu();

  // --- WAKES UP HERE ---
  sleep_disable(); // First thing: prevent immediate re-sleep
  detachInterrupt(digitalPinToInterrupt(WAKE_PIN));
}

// ISR must be as short as possible
void wakeUpISR() {
  // Empty. The interrupt vector simply breaks the sleep_cpu() halt.
}

Debugging: First Three Things to Check When It Fails

When an Arduino refuses to wake up, or the compiler throws vector errors, use this ranked decision path.

1. The "Immediate Wake" or "Never Sleeps" Loop

Symptom: The Serial monitor prints the sensor data continuously without stopping, or the multimeter never drops below 15mA.

Fix: Check the state of the WAKE_PIN. If you are using LOW as the interrupt trigger mode, and the pin is floating or pulled LOW by a miswired button, the ISR will fire continuously. Ensure your 10kΩ external pull-up is securely connected to VCC. Alternatively, change the trigger mode to FALLING in the attachInterrupt() call.

2. Compiler Error: Multiple Definition of Vector

Exact Error String: multiple definition of `__vector_16' (or __vector_5 depending on the timer/interrupt).

Ranked Causes:

  1. Third-Party Library Conflict: You are mixing native <avr/sleep.h> with the LowPower.h library or a library that defines its own ISR (like SoftwareSerial or PinChangeInterrupt). Fix: Remove LowPower.h and rely solely on native headers.
  2. Duplicate ISR Definitions: You have defined ISR(INT0_vect) manually elsewhere in the sketch while also using attachInterrupt(). Fix: Delete the manual ISR block and let the Arduino core handle the vector mapping.

3. Serial Monitor Prints Garbage After Waking

Symptom: The first character after wakeup is corrupted (e.g., ⸮Temp: 24.5).

Fix: The UART hardware takes a few microseconds to stabilize its baud rate generator after power_all_disable() is reversed. Insert a delay(10); immediately after waking up and re-enabling power, or ensure Serial.begin() is called again if you completely powered down the USART module.

Extending and Simplifying the Build

To Extend (Add Time-Based Wakeup): Pin interrupts are great for user interaction, but environmental loggers need to wake on a schedule. Replace the pushbutton with a DS3231 Real Time Clock (RTC). Wire the DS3231 SQW/INT pin to Nano D2. Configure the DS3231 to output a 1Hz square wave or a programmed alarm, and set the interrupt mode to FALLING. The DS3231 draws only ~1µA on battery backup, making it ideal for this topology.

To Simplify (Switching to ESP32): If you need WiFi telemetry, the ATmega328P is the wrong tool. Migrate to an ESP32-WROOM-32. The ESP32 uses a completely different architecture (esp_sleep_enable_ext0_wakeup() and esp_deep_sleep_start()). Note that ESP32 deep sleep resets the CPU state entirely, meaning you must store variables in RTC memory (RTC_DATA_ATTR) to persist data across sleep cycles.

Safety & Code Caveat: When running on raw lithium cells (e.g., 18650 at 4.2V fully charged), ensure your Nano's voltage regulator can handle the input. More importantly, never bypass the BMS (Battery Management System) on lithium packs to save quiescent current. A 10µA sleep savings is not worth a thermal runaway event.

FAQ: Arduino Sleep Long-Tail Questions

How much current does an Arduino Nano actually draw in sleep mode?

A stock Arduino Nano V3 draws roughly 15mA to 18mA in "sleep" mode because the onboard 5V linear regulator (usually an AMS1117 or similar) and the red power LED remain active. If you desolder the LED resistor and feed the board 3.3V directly to the 3V3 pin (bypassing the regulator), the ATmega328P alone in SLEEP_MODE_PWR_DOWN will draw between 0.1µA and 1.5µA at room temperature, as confirmed by the Microchip ATmega328P Datasheet.

Can I wake an Arduino from power-down sleep using a timer instead of a pin?

No. In SLEEP_MODE_PWR_DOWN, the system clock and all timers (including Timer0 and Timer1) are completely halted. The only valid wake sources are External Interrupts (INT0/INT1), Pin Change Interrupts (PCINT), the Watchdog Timer (WDT), or the TWI (I2C) address match. If you need timed wakeups without an external RTC, you must use SLEEP_MODE_IDLE (which keeps timers running but saves less power) or configure the native Watchdog Timer to generate an interrupt every 8 seconds.

Why does my serial monitor print garbage after the Arduino wakes up?

When you call power_all_disable(), the USART (UART) hardware is powered down. Upon waking, the hardware needs a few clock cycles to re-sync the baud rate generator. If you print immediately, the first byte is often corrupted. Always call Serial.flush() before sleeping to ensure the TX buffer is empty, and consider adding a delay(5) after waking before sending new Serial data. For more on UART timing, refer to the Arduino Serial Documentation.

Does the ESP32 use the same sleep commands as the ATmega328P?

Not at all. The ATmega328P uses AVR-Libc headers (<avr/sleep.h>) and manipulates CPU registers directly. The ESP32 uses the ESP-IDF framework via the Arduino core, relying on functions like esp_deep_sleep_start(). Furthermore, AVR sleep modes pause execution and resume on the next line of code, whereas ESP32 deep sleep triggers a full system reset, restarting the setup() function upon wakeup. You must use RTC_DATA_ATTR variables to retain state on the ESP32.