Embedded Systems / Laboratory
LABORATORY 02

Consumption States and the Autonomy of an ESP32-C6 Node

Duration: 3 hours Reference: Chapters 4, 5 and 10 Platform: ESP32-C6 DevKitC-1 Book reference chapter RO versiunea română

A sensor that must last a year on a battery works for under a second per hour. The rest of the time it sleeps. In this lab we measure what each state of an ESP32-C6 consumes, build the energy budget of a real node, and discover that the biggest saving does not come from sleeping deeper, but from waking up for less time.

1Objectives of the lab

  • Identifying the consumption states of a modern microcontroller and what stays on in each of them
  • Measuring the current in each state, from hundreds of milliamps down to a few microamps
  • Programming deep sleep and its wakeup sources: timer and input pin
  • Preserving information across reboots using memory in the always-powered domain
  • Calculating the average current, the energy budget and the battery autonomy
  • Reducing the duration of the active phase - by far the most effective optimization
  • Understanding the difference between the chip's consumption and the development board's consumption

2About the project

We build an autonomous sensor node: it wakes up at a fixed interval, reads the temperature and humidity, sends the values over Wi-Fi and goes back to sleep. Then we optimize it step by step and watch the calculated autonomy grow from a few days to over a year - without changing the battery and without changing the chip.

microcontroller
ESP32-C6
32-bit RISC-V, up to 160 MHz
special feature
low-power core
a second core at 20 MHz, which can work while the main one sleeps
communication
Wi-Fi 6 · BLE 5 · 802.15.4
Thread and Zigbee, besides Wi-Fi
always-powered domain
memory and timer
survive deep sleep
100 mA 1 mA 10 µA active deep sleep active deep sleep cycle period - this is where the autonomy is decided 3 s …
Fig. 1 - Current profile of a battery-powered node, with a logarithmic vertical axis. The short spikes are the Wi-Fi transmissions. The area under the curve is the charge consumed - and, despite appearances, most of it sits in the narrow spikes, not in the long plateau below.

3Materials needed

  • 1 ESP32-C6-DevKitC-1
  • 1 USB-C data cable
  • 1 INA219 module
  • 1 Multimeter with a µA range
  • 1 16×2 LCD with I²C adapter (PCF8574)
  • 1 I²C level shifter 3.3 V ↔ 5 V
  • 1 5 V bench supply
  • 1 BME280 sensor (I²C) or DHT22
  • 1 Push button + 10 kΩ resistor
  • 1 Breadboard
  • 20 Jumper wires
  • 1 2×AA holder or LiPo battery (optional)
About measuring very small currents An ordinary multimeter, on its µA range, has an internal resistance large enough to cause a voltage drop that resets the microcontroller at every current spike. If the board reboots strangely during measurements, this is the cause. The INA219 does not have this problem, because its shunt is only 0.1 Ω.

Wiring diagram

bench supply 5 V max 1 A + − INA219 Vin+ → shunt → Vin− 0x40 · VCC ← 3V3 SDA→GPIO6 SCL→GPIO7 common GND LCD 16×2 PCF8574 · 0x27 VCC ← 5 V supply common GND I²C level shifter LV 3.3 V · HV 5 V SDA→GPIO6 SCL→GPIO7 ESP32-C6 DevKitC-1 5V GND 3V3 GPIO6 · SDA GPIO7 · SCL GND GPIO4 USB disconnected BME280 0x76 VCC SDA SCL GND 10 kΩ button
Fig. 2 - The wiring. The 5 V supply enters the board's 5V pin through the INA219 shunt (red): all of the board's current, including its regulator's, flows through the module. The LCD is powered directly from the supply, before the shunt, so its consumption (and its backlight's) stays out of the measurement. The BME280, the INA219 and the LCD share the board's I²C bus (GPIO6 = SDA, GPIO7 = SCL) at different addresses; the LCD goes through a level shifter because it runs at 5 V. The button pulls GPIO4 to ground, and the 10 kΩ resistor keeps the pin high, also during deep sleep.
Before switching the power on 1. Disconnect the USB cable. The board is never powered from two sources at the same time.
2. Set the supply to 5 V and limit the current to 1 A before connecting it. The 5V pin goes through the board's regulator, so the INA219 sees the board's consumption as it is, not just the chip's.
3. Do not connect the LCD directly to GPIO6/GPIO7. The PCF8574 adapter has pull-up resistors to 5 V, and the ESP32-C6 pins are not 5 V tolerant. Use the level shifter (LV side to the board's 3V3, HV side to 5 V) or remove the two pull-up resistors from the adapter.
4. Ground is common: the supply's minus and the GND of the board, the INA219, the shifter and the LCD.

For the sleep states, the multimeter on its µA range goes in series on the same red wire, between the supply and the INA219.

4Consumption states

A modern microcontroller does not have two states (on / off), but a whole ladder of them. Each step turns off something extra and, in exchange, needs more time to get back to work.

StateWhat stays onOrder of magnitudeWakeup
Active, radio onprocessor, memory, Wi-Fi transmittingtens-hundreds of mA-
Modem-sleepprocessor and memory; the radio turns off between framesa few tens of mAimmediate
Light-sleepmemory keeps its contents, clocks stophundreds of µAmicroseconds, the program continues where it left off
Deep-sleeponly the always-powered domain: the timer and a small memorya few µAmilliseconds, but the program starts over
Hibernationpractically nothing besides the wakeup circuitunder 1 µAlike a full power-on
The difference that matters most In light-sleep, execution continues from the point where it was interrupted: variables are intact, the Wi-Fi connection can be kept. In deep-sleep, the chip reboots: setup() runs again, and all ordinary variables are lost. In exchange, the consumption is about a hundred times lower. Choosing between them is the first design trade-off of any battery-powered node.

The development-board trap

Read this paragraph before you are surprised by the results The datasheet promises a few microamps in deep sleep. A development board will most likely consume a thousand times more. The culprits are on the board, not in the chip:
  • the USB-to-serial converter - stays powered and draws current permanently
  • the voltage regulator - has its own idle current, sometimes on the order of milliamps
  • the power LED - 2-5 mA, i.e. by itself a thousand times the sleeping chip
  • the pull-up/pull-down resistors on the programming lines
A real product uses the bare module, not the development board. Still measure the board as it is: the difference between the datasheet figure and the measured figure is the most valuable lesson of this lab.

5The energy budget calculator

Enter the measured currents and the desired period. The calculator shows the average current, the split of the charge between the two phases, and the estimated autonomy.

Average current, charge split and autonomy
The thought experiment that changes everything Start from the default values (80 mA for 4 s, every 15 minutes) and try two optimizations:
  • Ten-times-better sleep: lower the sleep current from 15 µA to 1.5 µA. Look at the autonomy.
  • Four-times-shorter wakeup: put the sleep current back to 15 µA and lower the active duration from 4 s to 1 s.
The second optimization wins decisively, even though it looks more modest. The reason: at these values, the active phase dominates the budget. That is why engineering effort must go into waking up for less time - and that is exactly what we do in section 9.

6Setting up the work environment

  1. Install ESP32 support in the Arduino IDE

    In File → Preferences → Additional Boards Manager URLs add:

    address
    https://espressif.github.io/arduino-esp32/package_esp32_index.json

    Then Tools → Board → Boards Manager, search for "esp32" and install the package esp32 by Espressif Systems, version 3.0 or newer. The 2.x versions do not know about the ESP32-C6.

  2. Select the board and the port

    Tools → Board → ESP32 Arduino → ESP32C6 Dev Module. For Port, pick the port that appears after connecting the board.

    If no port appearsThe USB-C cable is, in many cases, charge-only. Try another cable before suspecting the drivers. The C6 board has two USB connectors - use the one marked USB (not UART) for the native serial port.
  3. Verify the toolchain with a minimal program
    test.ino
    void setup() {
      Serial.begin(115200);
      delay(1000);                      // let the native serial port initialize
      Serial.println("ESP32-C6 working");
    }
    
    void loop() {
      Serial.printf("time: %lu ms\n", millis());
      delay(1000);
    }

    Upload it and open the serial monitor at 115200 baud. If you see the messages, the environment is ready.

  4. Install the sensor library

    Tools → Manage Libraries, search for Adafruit BME280 and install it along with the suggested dependencies (Adafruit Unified Sensor).

7Deep sleep and the memory that survives it

The first program demonstrates the behavior that surprises everyone: after deep sleep, the microcontroller does not continue, it reboots. The only thing that survives is what we put in the memory of the always-powered domain.

sleep_01_basic.ino
#include <esp_sleep.h>

#define uS_PER_S   1000000ULL
#define SLEEP_S    10             // how many seconds we sleep

// The RTC_DATA_ATTR attribute places the variable in the always-powered
// domain's memory. It survives deep sleep; an ordinary variable does not.
RTC_DATA_ATTR int wakeup_count = 0;
RTC_DATA_ATTR uint32_t total_active_time_ms = 0;

int ordinary_counter = 0;          // this will always be 0 after wakeup

void print_wakeup_reason() {
  switch (esp_sleep_get_wakeup_cause()) {
    case ESP_SLEEP_WAKEUP_TIMER:  Serial.println("woken by the timer");  break;
    case ESP_SLEEP_WAKEUP_GPIO:   Serial.println("woken by a pin");      break;
    case ESP_SLEEP_WAKEUP_EXT1:   Serial.println("woken by EXT1");       break;
    default:                      Serial.println("normal power-on (not from sleep)");
  }
}

void setup() {
  uint32_t t0 = millis();
  Serial.begin(115200);
  delay(200);                     // without this pause we miss the first messages

  wakeup_count++;
  ordinary_counter++;

  Serial.println("-----------------------------");
  print_wakeup_reason();
  Serial.printf("wakeups (RTC memory) : %d\n", wakeup_count);
  Serial.printf("ordinary counter     : %d   <- always 1\n", ordinary_counter);
  Serial.printf("cumulative active time : %lu ms\n", total_active_time_ms);

  // The actual work would go here: sensor reading, transmission.
  delay(500);

  total_active_time_ms += millis() - t0;

  Serial.printf("sleeping for %d seconds...\n", SLEEP_S);
  Serial.flush();                 // flush the serial buffer BEFORE sleeping

  esp_sleep_enable_timer_wakeup(SLEEP_S * uS_PER_S);
  esp_deep_sleep_start();         // nothing executes past this point
}

void loop() {
  // Never reached: esp_deep_sleep_start() does not return.
}
Three things to notice
  1. wakeup_count grows every cycle, but ordinary_counter always stays 1 - ordinary memory is lost.
  2. The statements after esp_deep_sleep_start() never execute; the function does not return.
  3. Without Serial.flush() the last characters are lost: the chip falls asleep before they get a chance to go out over the wire. It is a mistake that produces truncated messages and a lot of confusion.
RTC memory is not unlimited It is a few kilobytes. Put only the strictly necessary things there: counters, the last reading, the Wi-Fi channel. Do not try to store the history of the measurements.

8Wakeup sources

The timer is not the only way out of sleep. A real node must also react to external events: a button, a motion sensor, a door contact.

sleep_02_wakeup.ino
#include <esp_sleep.h>

#define uS_PER_S    1000000ULL
#define SLEEP_S     30
#define BUTTON_PIN  GPIO_NUM_4    // on the C6, pin wakeup requires GPIO0..GPIO7

RTC_DATA_ATTR int timer_wakeups = 0;
RTC_DATA_ATTR int button_wakeups = 0;

void setup() {
  Serial.begin(115200);
  delay(200);

  esp_sleep_wakeup_cause_t reason = esp_sleep_get_wakeup_cause();
  if (reason == ESP_SLEEP_WAKEUP_TIMER) {
    timer_wakeups++;
    Serial.println("timer - scheduled measurement");
  } else if (reason == ESP_SLEEP_WAKEUP_GPIO) {
    button_wakeups++;
    Serial.println("BUTTON - unexpected event!");
  } else {
    Serial.println("cold start");
  }
  Serial.printf("timer: %d   button: %d\n",
                timer_wakeups, button_wakeups);

  // The button ties the pin to ground when pressed, so we hold it high with
  // an internal resistor and wake up on the falling edge.
  pinMode(BUTTON_PIN, INPUT_PULLUP);

  // Two wakeup sources active at the same time.
  esp_sleep_enable_timer_wakeup(SLEEP_S * uS_PER_S);
  esp_deep_sleep_enable_gpio_wakeup(BIT(BUTTON_PIN), ESP_GPIO_WAKEUP_GPIO_LOW);

  Serial.println("sleeping - press the button or wait 30 s");
  Serial.flush();
  esp_deep_sleep_start();
}

void loop() {}
The wakeup API differs between ESP32 variants This is one of the most frequent sources of errors when moving from one chip to another:
ChipPin wakeup functionAllowed pins
Classic ESP32esp_sleep_enable_ext0_wakeup() / ext1the RTC pins
ESP32-C3 / C6 / H2esp_deep_sleep_enable_gpio_wakeup()GPIO0-GPIO7
ESP32-S3both variantsthe RTC pins
Always check the documentation for the specific chip. A program written for the classic ESP32 will not compile on the C6, and the error message does not clearly say why.

Waking up from light-sleep

When you need the program to continue where it left off - for example to keep a connection open - use light sleep. The difference in code is minimal, but the behavior is completely different:

light sleep - the program continues
esp_sleep_enable_timer_wakeup(5 * uS_PER_S);
esp_light_sleep_start();          // this function DOES RETURN

Serial.println("woke up and continuing from here");
Serial.printf("variables are intact: %d\n", ordinary_counter);

9Shortening the active phase

The calculator in section 5 showed us where the energy goes. Now we attack the active phase. An unoptimized node spends most of its awake time waiting to connect to Wi-Fi, not reading the sensor.

What a connection takes

StageTypical durationCan it be eliminated?
Scanning all channels1-3 syes - if we remember the channel and the access point's address
Association and authentication100-300 mspartially
Getting an address via DHCP0.5-2 syes - with a fixed IP address
The actual transmission10-50 msno, but it is negligible anyway
The central observation The useful data takes up under 5% of the awake time. The rest is preparation - and almost all of it can be remembered from the previous cycle in RTC memory.
wifi_fast.ino - connecting with remembered data
#include <WiFi.h>
#include <esp_wifi.h>

const char* SSID = "lab_network";
const char* PASSWORD = "password";

// Everything worth remembering from the previous cycle, so we do not redo it.
RTC_DATA_ATTR uint8_t  saved_channel = 0;
RTC_DATA_ATTR uint8_t  saved_bssid[6];
RTC_DATA_ATTR bool     have_saved_data = false;
RTC_DATA_ATTR uint32_t consecutive_failures = 0;

// A fixed address completely removes the DHCP negotiation.
IPAddress local_ip(192, 168, 1, 77);
IPAddress gateway  (192, 168, 1, 1);
IPAddress subnet   (255, 255, 255, 0);
IPAddress dns      (192, 168, 1, 1);

bool connect(uint32_t limit_ms = 8000) {
  uint32_t t0 = millis();

  WiFi.mode(WIFI_STA);
  WiFi.config(local_ip, gateway, subnet, dns);   // no DHCP
  WiFi.setSleep(true);                            // the radio turns off between frames

  if (have_saved_data) {
    // We know exactly which channel and access point: no scanning.
    WiFi.begin(SSID, PASSWORD, saved_channel, saved_bssid);
  } else {
    WiFi.begin(SSID, PASSWORD);                   // the first time, we search
  }

  while (WiFi.status() != WL_CONNECTED && millis() - t0 < limit_ms) {
    delay(10);
  }

  if (WiFi.status() != WL_CONNECTED) {
    consecutive_failures++;
    // After three failures we suspect the access point moved to another
    // channel and drop the saved data, to redo a full scan.
    if (consecutive_failures >= 3) have_saved_data = false;
    return false;
  }

  saved_channel = WiFi.channel();
  memcpy(saved_bssid, WiFi.BSSID(), 6);
  have_saved_data = true;
  consecutive_failures = 0;

  Serial.printf("connected in %lu ms  (channel %d, IP %s)\n",
                millis() - t0, saved_channel, WiFi.localIP().toString().c_str());
  return true;
}
Why the failure counter is mandatory Access points change their channel on their own, to avoid interference. If the node insists on the remembered channel, it stays disconnected forever. Three failed attempts trigger a full scan. Without this safety net, the optimization turns the node into a device that dies silently after a few weeks - a type of failure that is very hard to diagnose in the field.

Measure the gain

Run the connection ten times with have_saved_data = false forced, then ten times normally. Record the average time in each case and recalculate the autonomy with the widget from section 5. The difference is usually a factor of three to five.

10The complete node

We put everything together: wake up, read the sensor, transmit quickly, go to sleep. The program measures the duration of its own active phase and reports it, so we can track the effect of the optimizations.

autonomous_node.ino
#include <WiFi.h>
#include <HTTPClient.h>
#include <Wire.h>
#include <Adafruit_BME280.h>
#include <esp_sleep.h>

#define uS_PER_S      1000000ULL
#define PERIOD_S      900              // 15 minutes
#define SDA_PIN       6
#define SCL_PIN       7

const char* SSID     = "lab_network";
const char* PASSWORD = "password";
const char* SERVER   = "http://192.168.1.10:8000/readings";

RTC_DATA_ATTR uint32_t cycle = 0;
RTC_DATA_ATTR uint32_t total_active_time_ms = 0;
RTC_DATA_ATTR uint8_t  saved_channel = 0;
RTC_DATA_ATTR uint8_t  saved_bssid[6];
RTC_DATA_ATTR bool     have_saved_data = false;

Adafruit_BME280 bme;

void go_to_sleep(const char* reason) {
  total_active_time_ms += millis();
  Serial.printf("[%s] active %lu ms  (cumulative %lu ms over %lu cycles)\n",
                reason, millis(), total_active_time_ms, cycle);
  Serial.flush();

  WiFi.disconnect(true);               // explicitly turn off the radio
  WiFi.mode(WIFI_OFF);

  esp_sleep_enable_timer_wakeup((uint64_t) PERIOD_S * uS_PER_S);
  esp_deep_sleep_start();
}

void setup() {
  Serial.begin(115200);
  delay(150);
  cycle++;

  // --- 1. the sensor, as early as possible: it can be read without Wi-Fi on ---
  Wire.begin(SDA_PIN, SCL_PIN);
  if (!bme.begin(0x76)) {              // some modules respond at 0x77
    Serial.println("the sensor is not responding");
    go_to_sleep("no sensor");
  }

  // Forced mode: the sensor takes a single measurement on request and then stops.
  // This is exactly what we need - a sensor that measures continuously wastes energy.
  bme.setSampling(Adafruit_BME280::MODE_FORCED,
                  Adafruit_BME280::SAMPLING_X1,     // temperature
                  Adafruit_BME280::SAMPLING_X1,     // pressure
                  Adafruit_BME280::SAMPLING_X1,     // humidity
                  Adafruit_BME280::FILTER_OFF);
  bme.takeForcedMeasurement();

  float temperature = bme.readTemperature();
  float humidity    = bme.readHumidity();
  float pressure    = bme.readPressure() / 100.0F;

  Serial.printf("cycle %lu: %.2f degC  %.1f%%  %.1f hPa\n",
                cycle, temperature, humidity, pressure);

  // --- 2. the network, as late and as briefly as possible ---
  WiFi.mode(WIFI_STA);
  WiFi.setSleep(true);
  if (have_saved_data) WiFi.begin(SSID, PASSWORD, saved_channel, saved_bssid);
  else                 WiFi.begin(SSID, PASSWORD);

  uint32_t t0 = millis();
  while (WiFi.status() != WL_CONNECTED && millis() - t0 < 8000) delay(10);
  if (WiFi.status() != WL_CONNECTED) {
    have_saved_data = false;           // redo a scan next time
    go_to_sleep("no network");
  }
  saved_channel = WiFi.channel();
  memcpy(saved_bssid, WiFi.BSSID(), 6);
  have_saved_data = true;

  // --- 3. the transmission ---
  char body[160];
  snprintf(body, sizeof(body),
           "{\"cycle\":%lu,\"temp\":%.2f,\"humid\":%.1f,\"pres\":%.1f,\"active_ms\":%lu}",
           cycle, temperature, humidity, pressure, millis());

  HTTPClient http;
  http.begin(SERVER);
  http.addHeader("Content-Type", "application/json");
  int code = http.POST(body);
  Serial.printf("server: %d\n", code);
  http.end();

  go_to_sleep("ok");
}

void loop() {}
The order of operations is not accidental The sensor is read before turning on Wi-Fi. If we did the opposite, the radio would sit on for nothing during the 10-20 ms of the measurement - a small waste on its own, but one that adds up tens of thousands of times over the battery's lifetime. The same logic explains the sensor's "forced" mode: it measures once, on command, then turns itself off.

11Measurement protocol

Fill in this table with your own values. The right-hand column is the one that matters for the report.

Measured stateHow to trigger itMeasured current
Active, Wi-Fi connected and transmittingduring the http.POST() call………… mA
Active, Wi-Fi on but idleafter connecting, in a delay() loop………… mA
Active, Wi-Fi offWiFi.mode(WIFI_OFF) then an empty loop………… mA
Light-sleepesp_light_sleep_start() with a 30 s wakeup………… µA
Deep-sleepesp_deep_sleep_start()………… µA
Deep-sleep, power LED desolderedoptional, only with the instructor's approval………… µA

Reading on the LCD

The program below steps through the active states on its own, holds each state for 10 seconds and shows on the LCD the average current measured by the INA219. The board needs no computer: after uploading the program, unplug the USB cable and switch the supply on. Required libraries, from the Library Manager: Adafruit INA219 and LiquidCrystal I2C.

masurare_lcd.ino
#include <WiFi.h>
#include <WiFiUdp.h>
#include <Wire.h>
#include <Adafruit_INA219.h>
#include <LiquidCrystal_I2C.h>
#include <esp_sleep.h>

#define SDA_PIN     6
#define SCL_PIN     7
#define DURATION_MS 10000UL            // how long each state is held
#define SLEEP_S     20                 // how long each sleep state lasts
#define uS_PER_S    1000000ULL

const char* SSID     = "lab_network";
const char* PASSWORD = "password";

Adafruit_INA219   ina(0x40);
LiquidCrystal_I2C lcd(0x27, 16, 2);    // some adapters answer at 0x3F
WiFiUDP           udp;

// Continuous transmission: UDP broadcast packets, no server needed.
void transmit() {
  static uint8_t buf[1000];
  udp.beginPacket(IPAddress(255, 255, 255, 255), 9999);
  udp.write(buf, sizeof(buf));
  udp.endPacket();
}

// Holds the state for DURATION_MS, averages the current, shows it every second.
float measure(const char* name, void (*work)() = nullptr) {
  lcd.clear();
  lcd.print(name);
  double sum = 0;
  uint32_t n = 0;
  unsigned long t0 = millis(), shown = t0;
  while (millis() - t0 < DURATION_MS) {
    if (work) work(); else delay(2);
    sum += ina.getCurrent_mA();
    n++;
    if (millis() - shown >= 1000) {
      shown = millis();
      lcd.setCursor(0, 1);
      lcd.printf("I=%7.1f mA    ", sum / n);
    }
  }
  return sum / n;
}

// The LCD is powered before the shunt: the text stays while the board sleeps.
void announceSleep(const char* name) {
  lcd.clear();
  lcd.print(name);
  lcd.setCursor(0, 1);
  lcd.print("read multimeter");
  ina.powerSave(true);                 // INA219 drops to a few uA
  delay(50);
}

void setup() {
  Wire.begin(SDA_PIN, SCL_PIN);
  lcd.init();
  lcd.backlight();
  if (!ina.begin(&Wire)) {
    lcd.print("no INA219!");
    while (true) delay(1000);
  }
  ina.setCalibration_32V_1A();         // 0.04 mA step, handles the Wi-Fi peaks

  WiFi.mode(WIFI_OFF);
  measure("WiFi off");

  lcd.clear();
  lcd.print("connecting...");
  WiFi.begin(SSID, PASSWORD);
  while (WiFi.status() != WL_CONNECTED) delay(100);
  measure("WiFi idle");
  measure("WiFi transmit", transmit);

  WiFi.mode(WIFI_OFF);
  announceSleep("Light-sleep 20 s");
  esp_sleep_enable_timer_wakeup(SLEEP_S * uS_PER_S);
  esp_light_sleep_start();             // the program continues from here
  ina.powerSave(false);

  announceSleep("Deep-sleep 20 s");
  esp_deep_sleep_start();              // on wakeup, setup() starts over
}

void loop() {}
Why the sleep states are read on the multimeter While the board sleeps, no program is running to read the INA219. Besides, an INA219 with a 0.1 Ω shunt has a step of about 0.1 mA, too coarse for a few microamperes. So before each sleep the program writes on the LCD which state comes next and puts the INA219 in power-save mode. Since the LCD is powered before the shunt, the message stays on the screen, and you read the current on the multimeter placed in series. After 20 seconds of deep-sleep the board restarts and the cycle repeats.

How to get a correct measurement

  • Power the board from a battery or a separate source, not through USB from the computer
  • Disconnect the programming cable during the measurement - the USB converter adds its own consumption
  • Wait at least 5 seconds after entering sleep before reading: the first interval contains transients
  • For the active phase use the average value over the whole duration, not the peak
  • Repeat every measurement three times and note the spread
Measuring current spikes The Wi-Fi transmission lasts a few milliseconds and can exceed 300 mA. An ordinary multimeter, which takes a reading every few hundred milliseconds, will never see this spike. If you have access to an oscilloscope, measure the voltage drop across a 1 Ω resistor placed in series - the picture of the spikes is dramatic and explains why some batteries with high internal resistance cannot power a Wi-Fi node, even though they have enough capacity.

12Assignments

  1. Upload sleep_01_basic.ino and let it run for ten cycles. Explain in the report why wakeup_count increases while ordinary_counter does not.
  2. Fill in the whole measurement table from section 11.
  3. Using the widget from section 5, calculate the node's autonomy on two AA batteries (about 2600 mAh) for periods of 1, 15 and 60 minutes. Produce an autonomy-versus-period chart.
  4. Measure the duration of the active phase before and after the optimizations from section 9 (remembered channel and fixed IP address). Recalculate the autonomy in both cases and express the gain as a percentage.
  5. Add button wakeup to the complete node, so that pressing it triggers an immediate measurement, outside the regular schedule. Verify, through the counter in RTC memory, that both sources work.
  6. Compare light-sleep with deep-sleep for a 5-second period. At what period does deep-sleep become more advantageous, taking into account that a full reboot also takes time and consumes energy?

13Deeper-dive challenge

The node that picks its own pace A temperature sensor that reports every 15 minutes wastes energy when the temperature is not changing and loses information when it changes suddenly. Implement an adaptive interval:
  • keep the last transmitted value in RTC memory;
  • if the new value differs by less than 0.3 °C, do not transmit at all and double the sleep interval (up to a maximum of one hour);
  • if it differs by more than 1 °C, transmit and lower the interval to the minimum (one minute);
  • still transmit at least once an hour regardless, so the server knows the node is alive.
Measure the average consumption over an hour, once in a room with a stable temperature and once with the sensor held in your hand. Discuss in the report the trade-off between autonomy and the temporal resolution of the data, and why the "at least once an hour" rule is mandatory in a real system.

14Self-check questions

15Resources