The AVR microcontrollers at the heart of most Arduino boards (ATmega328P on Uno/Nano, ATmega32U4 on Leonardo, ATtiny85) have a sleep system that predates the ESP32 by decades but is well-understood, reliable, and capable of very low quiescent current — down to 0.1 µA in power-down mode. This chapter covers the AVR sleep hierarchy and how to use it from Arduino code.
| Mode | CPU | ADC | Timers | SPI/UART/I2C | Ext. Interrupts | Typical Current (3.3 V) |
|---|---|---|---|---|---|---|
| Idle | Off | On | On | On | Yes | 1–5 mA |
| ADC Noise Reduction | Off | On | Timer2 only | Off | Yes | ~1 mA |
| Power-save | Off | Off | Timer2 only | Off | Yes | ~1 µA + Timer2 |
| Power-down | Off | Off | Off | Off | Yes (async) | 0.1–1 µA |
| Standby | Off | Off | Off | Off | Yes | ~0.1 µA |
For battery-powered projects where the AVR only needs to do something periodically, power-down is the target mode. The microcontroller draws under 1 µA and can only be woken by:
The watchdog timer (WDT) can generate an interrupt at intervals from 16 ms to 8 s. This makes it the primary wake source for duty-cycle applications (e.g., take a sensor reading every 8 seconds).
The AVR avr/sleep.h and avr/wdt.h headers provide direct access to these modes from Arduino sketches:
#include <avr/sleep.h>
#include <avr/wdt.h>
volatile bool wdt_fired = false;
ISR(WDT_vect) { wdt_fired = true; } // watchdog interrupt handler
void sleepSeconds(uint8_t wdp) {
// wdp: WDTO_1S, WDTO_2S, WDTO_4S, WDTO_8S (from avr/wdt.h)
noInterrupts();
MCUSR &= ~(1 << WDRF); // clear watchdog reset flag
WDTCSR = (1 << WDCE) | (1 << WDE);
WDTCSR = (1 << WDIE) | wdp; // interrupt mode, not reset
interrupts();
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
sleep_enable();
sleep_cpu(); // actually sleep here
sleep_disable();
}
void loop() {
// ... take measurement, transmit ...
sleepSeconds(WDTO_8S); // sleep ~8 seconds
}
The full example with sensor reading and serial output is in code/avr_sleep_demo.ino.
Even in power-down mode, certain peripherals can add unwanted current. On the ATmega328P:
Brown-out Detection (BOD): The BOD circuit monitors supply voltage and resets the MCU if it drops too low. It draws ~20 µA continuously. Disable it for lowest power:
// Disable BOD before sleep (must be done in timed sequence)
MCUCR |= (1 << BODS) | (1 << BODSE);
MCUCR &= ~(1 << BODSE);
sleep_cpu();
ADC: The ADC draws ~300 µA when enabled. Disable it before sleep:
ADCSRA &= ~(1 << ADEN); // disable ADC
// ... sleep ...
ADCSRA |= (1 << ADEN); // re-enable before any analogRead()
Power Reduction Register (PRR): Clocks entire peripheral blocks off. Disabling SPI, USART, TWI, and Timer1 saves ~0.5–1 mA at active voltage:
PRR = (1 << PRTWI) | (1 << PRTIM1) | (1 << PRSPI) | (1 << PRUSART0);
Just as on the ESP32, floating GPIO pins cause leakage current on AVR. Every unconnected pin in input mode with no pull-up enabled can leak through internal protection diodes. The fix: configure all unused pins as output low, or enable the internal pull-up.
// In setup(): make all unused pins output low
for (int i = 0; i < 20; i++) {
pinMode(i, OUTPUT);
digitalWrite(i, LOW);
}
The ATtiny85 and its family members are optimized for minimal power consumption. The ATtiny85 in power-down mode at 1.8 V draws about 0.1 µA — one tenth of the ATmega328P. For fixed-function sensor nodes where the Arduino IDE convenience is not needed, an ATtiny with a single job (e.g., pulse counting, temperature sensing) and a power-down sleep loop can run on a coin cell for years.
Key differences from ATmega:
Using the same duty-cycle formula from Chapter 4:
Example: ATmega328P reading a DHT22 sensor every 60 seconds, transmitting via a radio module.
I_avg = (15 mA × 0.25 s + 0.001 mA × 59.75 s) / 60 s
= (3.75 + 0.06) / 60
= 0.0635 mA
On 3× AA NiMH (2000 mAh at 1.2 V × 3 = 3.6 V, ~70% usable = 1400 mAh):
Runtime = 1400 mAh / 0.0635 mA ≈ 22,047 hours ≈ 918 days ≈ 2.5 years
| ← Chapter 4: ESP32 Deep Sleep and Wake Sources | Table of Contents | Chapter 6: Embedded Peripheral Management → |