ElectronicZoologyfield notes from the garage
Memory • ESP32

How to log ESP32 boot
count and uptime
with LittleFS

Board: Any ESP32
Topic: NVS, Preferences, board history
ESP32 Memory Series - Step 6 of 9
✓ Confirmed

The Idea

Every ESP32 you own has a history. How many times it has booted, how many projects it has run, how long it has been running in total. By default none of that is recorded anywhere. When you reflash a board it is as if its past never happened.

This guide shows you how to change that. A dedicated NVS namespace called board that you never clear. Every sketch you flash writes to it on boot. Over time it accumulates:

  • A total boot count across all sketches ever flashed
  • A list of every unique sketch ever run on the board, newest last
  • Accumulated uptime across all sessions

Pull up the serial monitor on any board and immediately know its history.

Why this is useful: Once you have a few boards in rotation it gets hard to keep track of which is which. This gives every board its own record - how long it has been running, what sketch is currently on it, how many times it has booted. Pull up the serial monitor on any board and know exactly where it has been and what sketch it's running.

The Sketch

Add this to any project. Change the PROJECT_NAME string to match your sketch and the rest looks after itself.

/*
 * We stand on the shoulders of giants when we build
 * with knowledge gained from others' efforts.
 * That doesn't make us giants. Be humble.
 * Create with care. Open source is the way.
 *
 * ESP32 Board Health - Lifetime Tracking With NVS
 * ------------------------------------------------
 * Tracks total boot count, last sketch name, and
 * accumulated uptime across every sketch ever flashed.
 * Survives reflashing. Uses the Preferences library.
 *
 * Board:   Any ESP32
 * Library: Preferences (built into ESP32 Arduino core)
 *
 * Open source - MIT Licence
 * Electronic Zoology - field notes from the garage
 * https://electroniczoology.com/guides/how-to-log-esp32-boot-count-littlefs
 */

#include <Preferences.h>

#define PROJECT_NAME "my-project-name"
// Uptime save interval - uncomment one:
// #define UPTIME_SAVE_INTERVAL_MS (1UL * 60UL * 1000UL)   // 1 minute
// #define UPTIME_SAVE_INTERVAL_MS (30UL * 60UL * 1000UL)  // 30 minutes
#define UPTIME_SAVE_INTERVAL_MS   (60UL * 60UL * 1000UL)  // 1 hour

Preferences boardHealth;
uint32_t uptimeAtBoot = 0;

void logBoardHealth() {
    boardHealth.begin("board", false);

    uint32_t totalBoots = boardHealth.getUInt("totalBoots", 0) + 1;
    boardHealth.putUInt("totalBoots", totalBoots);

    // Unique list of every sketch ever flashed, newest last.
    // Format: "|sketch1|sketch2|sketch3|"
    String history = boardHealth.getString("sketchList", "|");
    String token = "|" PROJECT_NAME "|";
    if (history.indexOf(token) < 0) {
        history += String(PROJECT_NAME) + "|";
        boardHealth.putString("sketchList", history);
    }

    uptimeAtBoot = boardHealth.getUInt("totalUptime", 0);

    Serial.println("=== Board Health ===");
    Serial.printf("Total boots:  %lu\n", (unsigned long)totalBoots);
    Serial.printf("Total uptime: %lu seconds (%.1f hours)\n",
                  (unsigned long)uptimeAtBoot, uptimeAtBoot / 3600.0);
    Serial.print  ("Sketches:     ");
    Serial.println(history.substring(1, history.length() - 1));
    Serial.println("===================");

    boardHealth.end();
}

void saveUptime() {
    boardHealth.begin("board", false);
    boardHealth.putUInt("totalUptime", uptimeAtBoot + millis() / 1000);
    boardHealth.end();
}

void setup() {
    Serial.begin(115200);
    delay(1000);
    logBoardHealth();
}

void loop() {
    static uint32_t t0 = 0;
    if (millis() - t0 > UPTIME_SAVE_INTERVAL_MS) {
        t0 = millis();
        saveUptime();
    }
}
Arduino IDE serial monitor showing board health output with total boots, uptime and sketch list
Board health output in the serial monitor - boots, uptime and full sketch history

Adding to Your Sketch

The health tracker is four additions to any existing sketch. Here is the starfield animation sketch before and after adding it.

Before

/*
 * We stand on the shoulders of giants when we build
 * with knowledge gained from others' efforts.
 * That doesn't make us giants. Be humble.
 * Create with care. Open source is the way.
 *
 * GC9A01 Round TFT Display - Starfield
 * -------------------------------------
 * Renders a 150-star warp speed starfield on a
 * GC9A01 240x240 round IPS TFT display via SPI.
 *
 * Board:   ESP32 Dev Board
 * Library: Arduino_GFX_Library by Moon On Our Nation
 *
 * Wiring (ESP32 Dev Board):
 *   VCC -> 3.3V    GND -> GND
 *   SCL -> GPIO18  SDA -> GPIO23
 *   DC  -> GPIO27  CS  -> GPIO5
 *   RST -> GPIO4
 *
 * Open source - MIT Licence
 * Electronic Zoology - field notes from the garage
 * https://electroniczoology.com/guides/how-to-log-esp32-boot-count-littlefs
 */

#include <Arduino_GFX_Library.h>

// ESP32 Dev Board pins
#define TFT_DC  27
#define TFT_CS   5
#define TFT_RST  4
#define TFT_SCK 18
#define TFT_MOSI 23

Arduino_DataBus *bus = new Arduino_HWSPI(TFT_DC, TFT_CS, TFT_SCK, TFT_MOSI);
Arduino_GFX *gfx = new Arduino_GC9A01(bus, TFT_RST, 0, true);

#define CX        120
#define CY        120
#define NUM_STARS 150
#define SPEED     3.5f

struct Star { float x, y, z; int px, py; };
Star stars[NUM_STARS];

void resetStar(Star &s) {
  s.x = random(-120, 120);
  s.y = random(-120, 120);
  s.z = random(60, 240);
  s.px = -1; s.py = -1;
}

uint16_t grey(uint8_t v) {
  return ((v >> 3) << 11) | ((v >> 2) << 5) | (v >> 3);
}

void drawTitle() {
  gfx->setTextColor(0xFFFF);
  gfx->setTextSize(2);
  gfx->setCursor(60, 100); gfx->print("Electronic");
  gfx->setCursor(54, 122); gfx->print("Zoology.com");
}

void setup() {
  pinMode(4, OUTPUT);
  digitalWrite(4, LOW);
  Serial.begin(115200);
  randomSeed(analogRead(0));
  gfx->begin();
  gfx->fillScreen(0x0000);
  for (int i = 0; i < NUM_STARS; i++) {
    resetStar(stars[i]);
    stars[i].z = random(1, 240);
  }
}

void loop() {
  for (int i = 0; i < NUM_STARS; i++) {
    Star &s = stars[i];
    if (s.px >= 0) gfx->fillCircle(s.px, s.py, s.z > 120 ? 1 : 2, 0x0000);
    s.z -= SPEED;
    if (s.z <= 1) { resetStar(s); continue; }
    int sx = CX + (int)(s.x * 120.0f / s.z);
    int sy = CY + (int)(s.y * 120.0f / s.z);
    if (sx < 0 || sx >= 240 || sy < 0 || sy >= 240) { resetStar(s); continue; }
    uint8_t brightness = (uint8_t)((1.0f - s.z / 240.0f) * 220 + 35);
    int radius = s.z < 80 ? 2 : 1;
    gfx->fillCircle(sx, sy, radius, grey(brightness));
    s.px = sx; s.py = sy;
  }
  drawTitle();
}

After - board health tracking added

/*
 * We stand on the shoulders of giants when we build
 * with knowledge gained from others' efforts.
 * That doesn't make us giants. Be humble.
 * Create with care. Open source is the way.
 *
 * GC9A01 Round TFT Display - Starfield with Board Health Tracking
 * ----------------------------------------------------------------
 * Starfield animation with NVS board health tracking added.
 * Tracks total boots, accumulated uptime, and a running list
 * of every sketch ever flashed to the board.
 *
 * Board:   ESP32 Dev Board
 * Library: Arduino_GFX_Library by Moon On Our Nation
 *          Preferences (built into ESP32 Arduino core)
 *
 * Wiring (ESP32 Dev Board):
 *   VCC -> 3.3V    GND -> GND
 *   SCL -> GPIO18  SDA -> GPIO23
 *   DC  -> GPIO27  CS  -> GPIO5
 *   RST -> GPIO4
 *
 * Open source - MIT Licence
 * Electronic Zoology - field notes from the garage
 * https://electroniczoology.com/guides/how-to-log-esp32-boot-count-littlefs
 */

#include <Arduino_GFX_Library.h>
#include <Preferences.h>

#define PROJECT_NAME "starfield"
// Uptime save interval - uncomment one:
// #define UPTIME_SAVE_INTERVAL_MS (1UL * 60UL * 1000UL)   // 1 minute
// #define UPTIME_SAVE_INTERVAL_MS (30UL * 60UL * 1000UL)  // 30 minutes
#define UPTIME_SAVE_INTERVAL_MS   (60UL * 60UL * 1000UL)  // 1 hour

// ESP32 Dev Board pins
#define TFT_DC  27
#define TFT_CS   5
#define TFT_RST  4
#define TFT_SCK 18
#define TFT_MOSI 23

Arduino_DataBus *bus = new Arduino_HWSPI(TFT_DC, TFT_CS, TFT_SCK, TFT_MOSI);
Arduino_GFX *gfx = new Arduino_GC9A01(bus, TFT_RST, 0, true);

#define CX        120
#define CY        120
#define NUM_STARS 150
#define SPEED     3.5f

struct Star { float x, y, z; int px, py; };
Star stars[NUM_STARS];
Preferences boardHealth;
uint32_t uptimeAtBoot = 0;

void resetStar(Star &s) {
  s.x = random(-120, 120);
  s.y = random(-120, 120);
  s.z = random(60, 240);
  s.px = -1; s.py = -1;
}

uint16_t grey(uint8_t v) {
  return ((v >> 3) << 11) | ((v >> 2) << 5) | (v >> 3);
}

void drawTitle() {
  gfx->setTextColor(0xFFFF);
  gfx->setTextSize(2);
  gfx->setCursor(60, 100); gfx->print("Electronic");
  gfx->setCursor(54, 122); gfx->print("Zoology.com");
}

void logBoardHealth() {
  boardHealth.begin("board", false);

  uint32_t totalBoots = boardHealth.getUInt("totalBoots", 0) + 1;
  boardHealth.putUInt("totalBoots", totalBoots);

  // Unique list of every sketch ever flashed, newest last.
  // Format: "|sketch1|sketch2|sketch3|"
  String history = boardHealth.getString("sketchList", "|");
  String token = "|" PROJECT_NAME "|";
  if (history.indexOf(token) < 0) {
    history += String(PROJECT_NAME) + "|";
    boardHealth.putString("sketchList", history);
  }

  uptimeAtBoot = boardHealth.getUInt("totalUptime", 0);

  Serial.println("=== Board Health ===");
  Serial.printf("Total boots:  %lu\n", (unsigned long)totalBoots);
  Serial.printf("Total uptime: %lu seconds (%.1f hours)\n",
                (unsigned long)uptimeAtBoot, uptimeAtBoot / 3600.0);
  Serial.print  ("Sketches:     ");
  Serial.println(history.substring(1, history.length() - 1));
  Serial.println("===================");

  boardHealth.end();
}

void saveUptime() {
  boardHealth.begin("board", false);
  boardHealth.putUInt("totalUptime", uptimeAtBoot + millis() / 1000);
  boardHealth.end();
}

void setup() {
  Serial.begin(115200);
  delay(1000);
  logBoardHealth();
  randomSeed(analogRead(0));
  gfx->begin();
  gfx->fillScreen(0x0000);
  for (int i = 0; i < NUM_STARS; i++) {
    resetStar(stars[i]);
    stars[i].z = random(1, 240);
  }
}

void loop() {
  for (int i = 0; i < NUM_STARS; i++) {
    Star &s = stars[i];
    if (s.px >= 0) gfx->fillCircle(s.px, s.py, s.z > 120 ? 1 : 2, 0x0000);
    s.z -= SPEED;
    if (s.z <= 1) { resetStar(s); continue; }
    int sx = CX + (int)(s.x * 120.0f / s.z);
    int sy = CY + (int)(s.y * 120.0f / s.z);
    if (sx < 0 || sx >= 240 || sy < 0 || sy >= 240) { resetStar(s); continue; }
    uint8_t brightness = (uint8_t)((1.0f - s.z / 240.0f) * 220 + 35);
    int radius = s.z < 80 ? 2 : 1;
    gfx->fillCircle(sx, sy, radius, grey(brightness));
    s.px = sx; s.py = sy;
  }
  drawTitle();

  static uint32_t t0 = 0;
  if (millis() - t0 > UPTIME_SAVE_INTERVAL_MS) {
    t0 = millis();
    saveUptime();
  }
}

Change PROJECT_NAME for each new sketch and the rest is identical every time.

What You Get

Every board you own builds its own record. A board you have had for two years and run twenty projects on will tell you that. A board you just pulled from a bag will show zero boots and no project history.

Notes

The one thing that resets it

NVS persists across reflashing but not across a full flash erase. If you use Tools in the Arduino IDE to erase all flash, or use esptool to wipe the chip, the board health partition goes with it. That is the only way to reset it to zero, which means it only happens when you choose it to.

Arduino IDE Tools menu showing Erase All Flash Before Sketch Upload set to Disabled
Tools - Erase All Flash Before Sketch Upload. Leave this Disabled to preserve NVS data across reflashing.

Write cycles

NVS uses wear levelling to spread writes across the flash cells. Saving uptime every 60 minutes is not going to wear out your flash in any realistic timeframe. If you are concerned, increase the save interval.

Next in series How to use LittleFS on ESP32 → - a full filing system inside flash.
NVS basics first? How to use ESP32 NVS for persistent storage → - the foundation this guide builds on.
See board health in a real project How to wire the GC9A01 round display with ESP32 → - add boot tracking to any display project.