Audio Storage (LittleFS)
No physical wiring is needed for audio storage. Alarm audio files are stored in a dedicated LittleFS partition in the ESP32's flash memory — no SD card module, no extra wires, no separate connector. This page explains the partition layout, how to write an audio file to flash during development, and how the OTA audio update mechanism works.
LittleFS vs. SD Card
| LittleFS (this project) | SD card | |
|---|---|---|
| Physical connection | None — uses built-in flash | SPI wiring, CS pin, module board |
| Power-fail safety | Yes — copy-on-write filesystem | No — FAT32 can corrupt on power loss |
| Update without reflash | Yes — HTTP OTA upload | Yes — remove card, copy file, reinsert |
| Swap audio without reflash | Yes — HTTP upload | Yes — copy file to card |
| Cost | $0 (uses existing flash) | $3–5 (module + card) |
Partition Layout
The partition table lives in partitions.csv in the firmware project. A 4 MB LittleFS
partition alongside a 3 MB app partition is comfortable in the 32 MB flash:
# Name, Type, SubType, Offset, Size
nvs, data, nvs, 0x9000, 0x5000
phy_init, data, phy, 0xe000, 0x1000
factory, app, factory, 0x10000, 3M
lfs, data, spiffs, , 4M
The spiffs subtype is how ESP-IDF's partition tool identifies a data partition that
will be used with either SPIFFS or LittleFS — the filesystem choice is made in the
driver, not the partition table.
The lfs label must match the partition_label field in the firmware driver
configuration (see the LittleFS driver doc).
Writing Audio Files During Development
Step 1 — Prepare the audio directory
Create a local directory to mirror the LittleFS filesystem layout:
audio/
└── alarms/
└── alarm.wav
Place your 16-bit PCM WAV file (mono or stereo, 22–44 kHz) at audio/alarms/alarm.wav.
Step 2 — Build the LittleFS image
Use mklittlefs to package the directory into a binary image:
mklittlefs -c audio/ -b 4096 -p 256 -s 0x400000 lfs.bin
| Flag | Value | Meaning |
|---|---|---|
-c audio/ | — | Source directory to package |
-b 4096 | 4096 bytes | Block size (match your flash page size) |
-p 256 | 256 bytes | Page size |
-s 0x400000 | 4 MB | Image size (match the lfs partition size) |
Install mklittlefs:
pip install mklittlefs # or download from GitHub
Step 3 — Flash the image
Look up the lfs partition's offset in partitions.csv (or run
idf.py partition-table to confirm). Then flash:
esptool.py --port /dev/ttyUSB0 write_flash 0x310000 lfs.bin
Replace 0x310000 with the actual offset of the lfs partition in your partition table.
idf.py flash flashes the firmware binary only, not the LittleFS image. You must flash
the LittleFS image separately with esptool.py whenever you update the audio file during
development.
Mounting LittleFS in Firmware
#include "esp_littlefs.h"
esp_vfs_littlefs_conf_t conf = {
.base_path = "/lfs",
.partition_label = "lfs",
.format_if_mount_failed = false, // use true during initial development only
.dont_mount = false,
};
esp_err_t err = esp_vfs_littlefs_register(&conf);
if (err != ESP_OK) {
ESP_LOGE("lfs", "Failed to mount LittleFS: %s", esp_err_to_name(err));
return;
}
ESP_LOGI("lfs", "LittleFS mounted at /lfs");
After mounting, files are accessible via standard POSIX calls:
FILE *f = fopen("/lfs/alarms/alarm.wav", "rb");
if (!f) {
ESP_LOGE("lfs", "alarm.wav not found");
return;
}
// ... read and play ...
fclose(f);
OTA Audio Update
When the clock is connected to WiFi, you can update the alarm audio file over HTTP
without reflashing the firmware. The firmware receives the new WAV file via an HTTP POST
to /upload, writes it to a temporary path, then atomically renames it to the live path:
esp_err_t lfs_update_alarm_wav(const uint8_t *data, size_t len) {
// Write to temp file first — if power is lost mid-write, the original is intact
FILE *tmp = fopen("/lfs/alarms/alarm_tmp.wav", "wb");
if (!tmp) return ESP_FAIL;
size_t written = fwrite(data, 1, len, tmp);
fclose(tmp);
if (written != len) {
unlink("/lfs/alarms/alarm_tmp.wav");
return ESP_FAIL;
}
// Atomic rename — LittleFS guarantees this is power-fail safe
if (rename("/lfs/alarms/alarm_tmp.wav", "/lfs/alarms/alarm.wav") != 0) {
return ESP_FAIL;
}
ESP_LOGI("lfs", "alarm.wav updated successfully");
return ESP_OK;
}
Why temp-then-rename? If power is lost while writing the new file directly to
/lfs/alarms/alarm.wav, the file would be partially written and the alarm would fail on
next boot. By writing to a temp file and renaming only on success, the original file
remains intact. LittleFS's copy-on-write design makes rename() atomic — either the
whole rename completes or neither file is modified.
Verification
Add this check to your bring-up sequence to confirm the partition mounted and the audio file is present:
FILE *f = fopen("/lfs/alarms/alarm.wav", "rb");
if (f) {
fseek(f, 0, SEEK_END);
long size = ftell(f);
fclose(f);
ESP_LOGI("lfs", "alarm.wav found, size %ld bytes", size);
} else {
ESP_LOGE("lfs", "alarm.wav not found — check partition flash and path");
}
Expected serial output:
I (xxx) lfs: LittleFS mounted at /lfs
I (xxx) lfs: alarm.wav found, size 1234567 bytes
Gotchas
format_if_mount_failed = false in production — if set to true, a mount failure
silently reformats the partition and loses all data. Use false in production; the error
log will tell you if the partition image is missing or corrupted.
Flash offset must match partition table — if esptool.py writes to the wrong offset,
the partition label will not match and esp_vfs_littlefs_register will return ESP_FAIL.
Run idf.py partition-table to confirm the offset before flashing.
WAV format — the audio driver expects 16-bit PCM WAV. Verify your WAV file is not
compressed (MP3 headers inside a WAV container will not play). On macOS, afinfo alarm.wav
confirms the format.