Blog / Embedded

  • esp8266
  • arduino
  • deep-sleep
  • wifi
  • rtc-memory
  • low-power

Why an ESP8266 Forgets Its WiFi Channel Every Deep Sleep

An ESP8266 does not resume from deep sleep. It reboots. Your sketch starts at setup() with an empty RAM, no idea which channel the access point lives on, and no connection. Every wake pays for a full channel scan, and that scan is often the biggest slice of your battery budget.

The fix is small: remember the channel and BSSID somewhere that survives the sleep. That somewhere is RTC memory.

What actually survives deep sleep

Deep sleep powers down almost everything. What is left is the RTC block, which keeps ticking and holds a small amount of memory. Normal RAM is gone, so any variable you set before sleeping is back to its initial value on wake.

  • RAM: lost, the chip boots from scratch
  • Flash: kept, but slow to write and wears out
  • RTC user memory: kept across deep sleep, 512 bytes

One catch: RTC memory is only kept while the chip has power. On a cold power-up it holds garbage, so you must validate what you read.

Where the credentials went

By default the Arduino core stores your SSID and password in flash when you call WiFi.begin(), and the SDK can auto-connect from that on boot. So the credentials are usually not the missing piece. What is missing is everything the radio worked out last time: the channel, the BSSID, and the DHCP lease.

Many battery sketches also call WiFi.persistent(false) to stop flash writes on every boot. Sensible, but then nothing is cached in flash at all, and you must call WiFi.begin() with credentials each wake. Either way, without a channel hint the chip scans all channels.

Quick detour: why a scan costs so much

A scan means the radio sits on each channel in turn, listening for beacons. Channels 1 to 13 at roughly 100 ms or more of dwell each adds up to a second or more of transmit-and-receive current, on every wake. Then comes association, then DHCP. For a sensor that is awake for three seconds a minute, that is a large share of its energy use.

Handing WiFi.begin() a channel and BSSID lets it skip straight to the right frequency.

Caching the channel and BSSID in RTC memory

The struct below holds a checksum, the channel and the six-byte BSSID, padded to a multiple of four bytes because RTC memory is read in 32-bit blocks. The offset passed to the RTC functions counts 4-byte blocks from the start of the user area.

#include <ESP8266WiFi.h>

const char *SSID = "my-network";
const char *PASS = "my-password";

struct WifiCache {
  uint32_t check;
  uint8_t channel;
  uint8_t bssid[6];
  uint8_t pad;
};

// FNV-1a over everything after the checksum field.
uint32_t cacheSum(const WifiCache &c) {
  const uint8_t *p = &c.channel;
  uint32_t h = 2166136261u;
  for (size_t i = 0; i < sizeof(c) - sizeof(c.check); i++) {
    h ^= p[i];
    h *= 16777619u;
  }
  return h;
}

bool connectWifi() {
  WiFi.persistent(false);  // no flash writes on every wake
  WiFi.mode(WIFI_STA);

  WifiCache c;
  bool cached = ESP.rtcUserMemoryRead(0, (uint32_t *)&c, sizeof(c)) &&
                c.check == cacheSum(c);

  if (cached) {
    WiFi.begin(SSID, PASS, c.channel, c.bssid);
  } else {
    WiFi.begin(SSID, PASS);
  }

  uint32_t start = millis();
  while (WiFi.status() != WL_CONNECTED) {
    // Stale cache (router moved channel or was replaced)? Do a full connect.
    if (cached && millis() - start > 3000) {
      WiFi.disconnect();
      WiFi.begin(SSID, PASS);
      cached = false;
      start = millis();
    }
    if (!cached && millis() - start > 10000) return false;
    delay(10);
  }

  // Save what the radio learned, if it differs from what we had.
  WifiCache fresh = {};
  fresh.channel = WiFi.channel();
  memcpy(fresh.bssid, WiFi.BSSID(), 6);
  fresh.check = cacheSum(fresh);
  ESP.rtcUserMemoryWrite(0, (uint32_t *)&fresh, sizeof(fresh));
  return true;
}

The fallback matters. If you reboot your router and it picks a new channel, a cache with no escape hatch would fail forever. Three seconds of failed attempts, then a normal scan, then a corrected cache.

Sleeping properly

Deep sleep on the ESP8266 only wakes if GPIO16 is wired to RST. Without that wire the chip sleeps and never comes back, which looks exactly like a dead board. Then call it after your work is done:

void setup() {
  if (connectWifi()) {
    // read the sensor, send the reading
  }
  ESP.deepSleep(60e6);  // microseconds: 60 seconds
}

void loop() {}

If you see the board waking but staying stuck, check the wiring before anything else.

Further savings, with caveats

  • A static IP via WiFi.config() skips DHCP. Pick an address outside the router's pool, or you will eventually get a clash.
  • The second argument to ESP.deepSleep() controls RF calibration on wake. WAKE_NO_RFCAL wakes faster, at the cost of a less tuned radio, so test it on your own board.
  • Measure with a real current meter. Reconnect time varies a lot between routers.

The pattern is the same for any chip that reboots on wake: work out what the slow step learned, and park that in the one place that outlives the sleep.