AVR Sleep Modes and Watchdog Timer: Low-Power Design and Crash Recovery
Two features separate a classroom demo from a product that runs unattended for months. The first is sleep: a battery-powered sensor node that wakes for a few milliseconds every few seconds can last far longer than one that runs its CPU continuously. The second is the watchdog timer: if a bug, electrical noise or a stuck I2C bus hangs the program, the watchdog resets the chip automatically.
On AVR these two features are closely related, because the watchdog has its own 128 kHz oscillator that keeps running in the deepest sleep mode and can wake the CPU. This tutorial covers the ATmega328P sleep modes, a periodic wake-up using the watchdog interrupt, wake-up on a button with INT0, and using the watchdog as a crash-recovery reset.
ATmega328P Sleep Modes
Sleep is entered with the SLEEP instruction after setting the mode bits SM2..SM0 and the enable bit SE in the SMCR register. avr-libc wraps this in <avr/sleep.h>. Each mode stops a different set of clocks:
| Mode | What keeps running | Typical wake-up sources |
|---|---|---|
| Idle | All peripherals; only the CPU clock stops | Any interrupt |
| ADC Noise Reduction | ADC, Timer2 asynchronous, TWI address match, watchdog | ADC complete, external interrupts, watchdog |
| Power-save | Timer2 (useful with a 32.768 kHz watch crystal), watchdog | Timer2, external interrupts, watchdog |
| Standby | Main crystal oscillator keeps running for fast wake-up | Same as power-down |
| Extended Standby | Oscillator and Timer2 | Same as power-save |
| Power-down | Only the watchdog, external interrupts and TWI address match logic | INT0/INT1 level, pin change, watchdog, TWI address match |
In power-down mode the external interrupts INT0 and INT1 can only wake the chip on a low level, not on an edge, because detecting edges needs the I/O clock. Pin change interrupts do work in power-down.
Reducing current further
- Turn off the ADC before sleeping by clearing ADEN in ADCSRA; it keeps drawing current otherwise.
- Use the Power Reduction Register (PRR), through
power_all_disable()in<avr/power.h>, to stop clocks to unused peripherals while awake. - Never leave inputs floating; enable pull-ups or drive unused pins.
- Use
sleep_bod_disable()just before sleeping to switch off the brown-out detector during sleep on the ATmega328P.
How the Watchdog Works
The watchdog counts pulses from its separate 128 kHz oscillator. The prescaler bits WDP3..WDP0 in WDTCSR choose a timeout from about 16 ms to about 8 s. If the program does not reset the counter with the WDR instruction (wdt_reset() in C) before the timeout, the watchdog acts. Two control bits decide what it does: WDE causes a system reset, and WDIE causes an interrupt (WDT_vect). With both set, the first timeout gives an interrupt and the next one a reset.
Changing the watchdog configuration needs a timed sequence: write WDCE and WDE together, then within four clock cycles write the new value. Interrupts must be disabled around this sequence. After a watchdog reset, the WDRF flag in MCUSR is set and the watchdog stays enabled with the shortest timeout, so startup code must clear WDRF and disable the watchdog immediately, or the chip will keep resetting.
Program 1: Wake Every 8 Seconds or on a Button
The board sleeps in power-down mode, wakes every 8 seconds via the watchdog interrupt or immediately when a button on PD2 (INT0) is pressed, blinks the LED on PB5 and goes back to sleep.
#define F_CPU 16000000UL
#include <avr/io.h>
#include <avr/interrupt.h>
#include <avr/sleep.h>
#include <avr/wdt.h>
#include <util/delay.h>
volatile uint8_t woke_by_button = 0;
ISR(WDT_vect)
{
/* Watchdog interrupt: nothing to do, waking is enough */
}
ISR(INT0_vect)
{
EIMSK &= ~(1 << INT0); /* disable: level interrupt would repeat */
woke_by_button = 1;
}
/* Watchdog in interrupt-only mode with an 8 s period */
static void wdt_interrupt_8s(void)
{
cli();
wdt_reset();
MCUSR &= ~(1 << WDRF); /* required before clearing WDE */
WDTCSR = (1 << WDCE) | (1 << WDE); /* start timed sequence */
WDTCSR = (1 << WDIE) | (1 << WDP3) | (1 << WDP0); /* WDP = 1001: 8 s */
sei();
}
static void go_to_sleep(void)
{
ADCSRA &= ~(1 << ADEN); /* ADC off */
EIFR = (1 << INTF0);
EIMSK |= (1 << INT0); /* low level on INT0 wakes us */
set_sleep_mode(SLEEP_MODE_PWR_DOWN);
cli();
sleep_enable();
sleep_bod_disable(); /* BOD off during sleep */
sei(); /* must be right before sleep */
sleep_cpu(); /* ...sleeping here... */
sleep_disable(); /* execution resumes here */
}
int main(void)
{
DDRB |= (1 << DDB5); /* LED */
DDRD &= ~(1 << DDD2);
PORTD |= (1 << PORTD2); /* button with pull-up */
EICRA = 0; /* INT0 on low level */
wdt_interrupt_8s();
while (1) {
go_to_sleep();
uint8_t blinks = woke_by_button ? 3 : 1;
woke_by_button = 0;
for (uint8_t i = 0; i < blinks; i++) {
PORTB |= (1 << PORTB5);
_delay_ms(50);
PORTB &= ~(1 << PORTB5);
_delay_ms(150);
}
while (!(PIND & (1 << PIND2))) /* wait for button release */
;
}
}
The sequence cli(); sleep_enable(); sei(); sleep_cpu(); avoids a race condition. The AVR always executes the instruction after sei before handling a pending interrupt, so the CPU is guaranteed to reach SLEEP; any interrupt that arrives then wakes it instead of being lost. The INT0 ISR disables itself because a low-level interrupt fires continuously while the button is held.
Program 2: Watchdog Reset for Crash Recovery
#include <avr/io.h>
#include <avr/wdt.h>
/* Runs before main(): clear WDRF and stop the watchdog so a
watchdog reset does not lead to an endless reset loop. */
uint8_t mcusr_copy __attribute__((section(".noinit")));
void early_wdt_off(void) __attribute__((naked, used, section(".init3")));
void early_wdt_off(void)
{
mcusr_copy = MCUSR; /* keep the reset cause */
MCUSR = 0;
wdt_disable();
}
int main(void)
{
DDRB |= (1 << DDB5);
if (mcusr_copy & (1 << WDRF))
PORTB |= (1 << PORTB5); /* LED on: last reset was the watchdog */
wdt_enable(WDTO_1S); /* reset if not kicked within ~1 s */
while (1) {
/* ... normal work that must finish well within 1 s ... */
wdt_reset(); /* kick the watchdog */
}
}
Kick the watchdog in one place only, at the end of a complete pass of the main loop. Calling wdt_reset() inside an inner wait loop or a timer ISR defeats its purpose, because the watchdog keeps being satisfied even while the main program is stuck.
Troubleshooting
- Chip resets endlessly after the first watchdog timeout: the startup code does not clear WDRF and disable the watchdog. Add the
.init3function shown above. - The board never wakes from power-down on a button: INT0 is configured for a falling or rising edge. Only a low level (or a pin change interrupt) wakes the chip from power-down.
- The watchdog interrupt never fires: WDIE was written without the WDCE timed sequence, or interrupts are disabled when
sleep_cpu()runs. - Sleep current is still in milliamps on a bare chip: the ADC is enabled, an input is floating, an LED is lit, or the USB-serial adapter is back-powering the chip through the RX pin.
- The watchdog period is not exactly 8 s: the 128 kHz watchdog oscillator varies with voltage and temperature, so do not use it for accurate timekeeping. Use a DS1307 or Timer2 with a watch crystal instead.
Summary
Use set_sleep_mode(), sleep_enable() and sleep_cpu() from <avr/sleep.h> to enter sleep, choosing power-down for the lowest current. Wake from power-down with the watchdog interrupt, a low level on INT0/INT1 or a pin change. Configure WDTCSR with the WDCE timed sequence, clear WDRF and disable the watchdog early after any reset, and use wdt_enable() with a single wdt_reset() in the main loop to recover automatically from hangs.