LittleFS trên ESP32: lưu file trong flash, ghi log CSV xoay vòng và phục vụ trang web qua Wi-Fi
LittleFS nằm ở đâu trong flash ESP32, khác Preferences và thẻ nhớ thế nào, đọc ghi file, ghi log CSV xoay vòng để flash bền, phục vụ trang web và file log từ LittleFS.
ESP32 DevKit có 4 MB bộ nhớ flash, nhưng chương trình thường chỉ dùng chưa tới một nửa. Phần còn lại có thể biến thành một "ổ đĩa" nhỏ bằng LittleFS: lưu file cấu hình, trang web, ảnh nhỏ, nhật ký đo đạc, và tất cả vẫn còn sau khi rút điện hay nạp lại chương trình. Bài này giải thích LittleFS nằm ở đâu trong flash, khác gì Preferences và thẻ nhớ, cách đọc ghi file, cách ghi log CSV có xoay vòng để không đầy bộ nhớ, và cách phục vụ trang web cùng file log qua Wi-Fi.
1. LittleFS nằm ở đâu trong flash

Flash được chia thành các phân vùng theo một bảng nạp cùng chương trình. Với bảng mặc định (Tools → Partition Scheme → Default 4MB with spiffs), chương trình nằm ở app0, app1 để dành cho cập nhật qua mạng (OTA), nvs chứa dữ liệu của Preferences, còn phân vùng tên spiffs rộng 1 408 KB là chỗ LittleFS dùng. Tên "spiffs" là do lịch sử: hệ thống file SPIFFS cũ cũng dùng phân vùng này; LittleFS thay thế SPIFFS vì có thư mục thật, chịu được mất điện giữa chừng tốt hơn và nhanh hơn.
Nạp lại chương trình chỉ ghi vào phân vùng ứng dụng, nên file trong LittleFS vẫn còn. Chỉ khi chọn "Erase All Flash Before Sketch Upload" hoặc đổi bảng phân vùng thì dữ liệu mới mất.
2. LittleFS, Preferences hay thẻ nhớ
| Preferences (NVS) | LittleFS | Thẻ nhớ microSD | |
|---|---|---|---|
| Dạng dữ liệu | Cặp khóa – giá trị (số, chuỗi ngắn) | File và thư mục | File và thư mục (FAT) |
| Dung lượng | Vài KB | Khoảng 1,4 MB (bảng mặc định) | Hàng GB |
| Hợp với | Cấu hình, mật khẩu Wi-Fi, bộ đếm | Trang web, file cấu hình JSON, log vừa phải | Log dài ngày, ảnh, dữ liệu lớn, đọc trên máy tính |
| Phần cứng thêm | Không | Không | Module thẻ nhớ, 4 dây SPI |
3. Đọc và ghi file

LittleFS.begin(true) gắn hệ thống file; tham số true cho phép định dạng phân vùng nếu nó chưa có hệ thống file (lần chạy đầu tiên). Sau đó dùng như file trên máy tính:
LittleFS.open(duongDan, FILE_READ)mở để đọc;FILE_WRITEtạo mới hoặc ghi đè;FILE_APPENDghi tiếp vào cuối.- File mở được dùng như
Serial:print,printf,readString,readStringUntil('\n'). - Luôn đóng file (
close()) sau khi ghi: dữ liệu chỉ chắc chắn nằm trong flash khi file đã đóng. Mặc định mở đồng thời tối đa 10 file. exists,remove,rename,mkdirđể kiểm tra, xóa, đổi tên, tạo thư mục;open("/")rồiopenNextFile()để liệt kê.
Chương trình trong bài dùng bốn file: /dem.txt giữ số lần khởi động (đọc, cộng 1, ghi đè), /index.html là trang web (ghi một lần ở lần chạy đầu), /log.csv nhận thêm một dòng mỗi 10 giây, và /log_cu.csv là log cũ sau khi xoay vòng.
4. Ghi log không làm hỏng flash
Mỗi ô nhớ flash chỉ chịu được khoảng 100 000 lần xóa. LittleFS tự rải các lần ghi ra khắp phân vùng để không ô nào bị ghi quá nhiều, nhưng chương trình vẫn nên:
- Ghi thưa: mỗi 10 giây một dòng như trong bài là thoải mái; ghi mỗi 100 ms thì nên gom nhiều dòng trong RAM rồi ghi một lần.
- Giới hạn kích thước: khi
log.csvvượt 64 KB, chương trình xóalog_cu.csvrồi đổi tênlog.csvthànhlog_cu.csv. Bộ nhớ không bao giờ đầy, luôn còn hai đoạn log gần nhất. - Ghi nhãn thời điểm: mỗi dòng có số lần khởi động và số giây từ lúc khởi động. Muốn có ngày giờ thật, lấy giờ qua mạng (NTP) hoặc dùng module DS3231.
Nếu mất điện đúng lúc đang ghi, LittleFS giữ phiên bản file trước đó còn nguyên; chỉ dòng đang ghi dở có thể mất. File CSV mở thẳng bằng Excel hay Google Sheets được.
5. Phục vụ trang web từ LittleFS

mayChu.serveStatic("/", LittleFS, "/index.html") gửi trang web cho trình duyệt; mayChu.streamFile(f, "text/csv") gửi file log theo từng đoạn nên file lớn cũng không chiếm hết RAM. Trang index.html có một đoạn JavaScript tải /log.csv rồi tự vẽ đồ thị: ESP32 chỉ gửi file, mọi việc tính toán do trình duyệt làm.
Chương trình tự ghi trang web vào flash ở lần chạy đầu, nên không cần công cụ gì thêm. Khi trang web có nhiều file (HTML, CSS, hình ảnh), có thể đặt chúng trong thư mục data cạnh sketch và dùng plugin tải file LittleFS cho Arduino IDE 2 để nạp cả thư mục lên phân vùng.
6. Chương trình
Chỉ cần ESP32 và mạng Wi-Fi; nối biến trở vào GPIO34 (hai đầu 3V3, GND) nếu muốn log có số thay đổi. Thay TEN_WIFI, MAT_KHAU_WIFI rồi nạp. Lệnh trên Serial Monitor: L liệt kê file, X in log, D xóa log, F xem dung lượng đã dùng.
// PYWORLD.VN - LittleFS trên ESP32: lưu file trong flash, đếm số lần khởi động, ghi log CSV có xoay vòng, phục vụ trang web và file log qua Wi-Fi
// Lệnh Serial: L = liệt kê file, X = in log, D = xóa log, F = dung lượng. Mở http://<địa chỉ IP>/ để xem đồ thị từ file log
// Thay TEN_WIFI, MAT_KHAU_WIFI. Chỉ dùng thư viện có sẵn trong gói esp32 (LittleFS, WiFi, WebServer)
#include <LittleFS.h>
#include <WiFi.h>
#include <WebServer.h>
const char *TEN_WIFI = "TEN_WIFI";
const char *MAT_KHAU_WIFI = "MAT_KHAU_WIFI";
const int CHAN_DO = 34; // biến trở hoặc cảm biến tương tự (có thể để trống)
const uint32_t CHU_KY_GHI_MS = 10000; // 10 giây ghi một dòng: đủ thưa để flash bền lâu
const size_t LOG_TOI_DA = 64 * 1024; // quá 64 KB thì đổi tên thành log_cu.csv, bắt đầu file mới
WebServer mayChu(80);
uint32_t lanKhoiDong = 0;
// Trang web nhỏ ghi sẵn vào flash ở lần chạy đầu: đọc /log.csv rồi vẽ 100 điểm cuối
const char TRANG_WEB[] = R"HTML(<!doctype html><html lang="vi"><meta charset="utf-8"><meta name="viewport" content="width=device-width">
<title>Log ESP32</title><body style="font-family:sans-serif;max-width:720px;margin:auto">
<h2>Log điện áp GPIO34</h2><svg id="d" viewBox="0 0 600 200" style="width:100%;background:#f4f6fa"></svg>
<p id="t"></p><p><a href="/log.csv">Tải log.csv</a></p>
<script>
fetch('/log.csv').then(r=>r.text()).then(s=>{
const h=s.trim().split('\n').slice(1).map(l=>l.split(',').map(Number)).slice(-100);
if(!h.length){document.getElementById('t').textContent='Chưa có dữ liệu';return;}
const p=h.map((r,i)=>(i*600/Math.max(1,h.length-1)).toFixed(1)+','+(200-r[2]/3300*200).toFixed(1)).join(' ');
document.getElementById('d').innerHTML='<polyline fill="none" stroke="#1f6fd1" stroke-width="2" points="'+p+'"/>';
const c=h[h.length-1];document.getElementById('t').textContent='Dòng mới nhất: lần khởi động '+c[0]+', giây '+c[1]+', '+c[2]+' mV';
});
</script></body></html>
)HTML";
String docFile(const char *duongDan) {
File f = LittleFS.open(duongDan, FILE_READ);
if (!f) return "";
String s = f.readString();
f.close();
return s;
}
bool ghiFile(const char *duongDan, const String &noiDung) {
File f = LittleFS.open(duongDan, FILE_WRITE); // FILE_WRITE: tạo mới hoặc ghi đè
if (!f) return false;
f.print(noiDung);
f.close(); // đóng file thì dữ liệu mới chắc chắn nằm trong flash
return true;
}
void ghiLog() {
File f = LittleFS.open("/log.csv", FILE_APPEND); // FILE_APPEND: ghi tiếp vào cuối
if (!f) return;
if (f.size() == 0) f.println("lan_khoi_dong,giay,mV");
f.printf("%lu,%lu,%lu\n", (unsigned long)lanKhoiDong, (unsigned long)(millis() / 1000), (unsigned long)analogReadMilliVolts(CHAN_DO));
size_t kichThuoc = f.size();
f.close();
if (kichThuoc > LOG_TOI_DA) { // xoay vòng: giữ tối đa hai file
LittleFS.remove("/log_cu.csv");
LittleFS.rename("/log.csv", "/log_cu.csv");
}
}
void lietKe() {
File thuMuc = LittleFS.open("/");
for (File f = thuMuc.openNextFile(); f; f = thuMuc.openNextFile()) {
Serial.printf(" %-14s %7u byte\n", f.name(), (unsigned)f.size());
}
}
void setup() {
Serial.begin(115200);
delay(300);
if (!LittleFS.begin(true)) { // true: chưa có hệ thống file thì định dạng phân vùng
Serial.println("Khong mo duoc LittleFS: kiem tra Partition Scheme co phan vung spiffs");
return;
}
lanKhoiDong = docFile("/dem.txt").toInt() + 1;
ghiFile("/dem.txt", String(lanKhoiDong));
if (!LittleFS.exists("/index.html")) ghiFile("/index.html", TRANG_WEB);
Serial.printf("LittleFS: da dung %u / %u byte. Khoi dong lan %lu\n", (unsigned)LittleFS.usedBytes(),
(unsigned)LittleFS.totalBytes(), (unsigned long)lanKhoiDong);
lietKe();
WiFi.begin(TEN_WIFI, MAT_KHAU_WIFI);
for (int i = 0; i < 40 && WiFi.status() != WL_CONNECTED; i++) delay(250);
if (WiFi.status() == WL_CONNECTED) {
mayChu.serveStatic("/", LittleFS, "/index.html");
mayChu.on("/log.csv", [] {
File f = LittleFS.open("/log.csv", FILE_READ);
if (!f) {
mayChu.send(404, "text/plain", "Chua co log");
return;
}
mayChu.streamFile(f, "text/csv"); // gửi dần từng đoạn, không cần đọc cả file vào RAM
f.close();
});
mayChu.begin();
Serial.printf("Mo http://%s/\n", WiFi.localIP().toString().c_str());
} else {
Serial.println("Chua ket noi Wi-Fi: van ghi log, dung lenh Serial de xem");
}
}
void loop() {
mayChu.handleClient();
static uint32_t lanGhi = 0;
if (millis() - lanGhi >= CHU_KY_GHI_MS) {
lanGhi = millis();
ghiLog();
}
if (!Serial.available()) return;
char c = toupper(Serial.read());
if (c == 'L') {
lietKe();
} else if (c == 'X') {
Serial.print(docFile("/log.csv"));
} else if (c == 'D') {
LittleFS.remove("/log.csv");
Serial.println("Da xoa log.csv");
} else if (c == 'F') {
Serial.printf("Da dung %u / %u byte\n", (unsigned)LittleFS.usedBytes(), (unsigned)LittleFS.totalBytes());
}
}
7. Kết quả mong đợi

Rút điện, cắm lại: số lần khởi động tăng tiếp và log cũ vẫn còn. Nạp lại chương trình (không xóa toàn bộ flash) cũng vậy. Dung lượng "đã dùng" tăng theo khối 4 KB, vì LittleFS cấp bộ nhớ theo khối chứ không theo từng byte.
8. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| "Khong mo duoc LittleFS" | Bảng phân vùng không có phân vùng spiffs (ví dụ "Huge APP") | Chọn Partition Scheme có spiffs; hoặc begin(true) để định dạng |
| File ghi xong mở lại thấy trống | Quên close(); mở bằng FILE_WRITE nên bị ghi đè | Đóng file; dùng FILE_APPEND khi ghi tiếp |
| Tên file không mở được | Thiếu dấu / đầu đường dẫn; tên quá dài | Luôn bắt đầu bằng /; tên ngắn, không dấu |
Trang web cũ, sửa TRANG_WEB không ăn | Chương trình chỉ ghi khi file chưa có | Xóa /index.html (thêm lệnh xóa) rồi khởi động lại |
| Flash đầy dần | Log không giới hạn kích thước | Xoay vòng như trong bài |
| Chương trình quá lớn khi thêm Wi-Fi, web | Phân vùng ứng dụng 1,25 MB | Chọn bảng phân vùng có app lớn hơn nhưng vẫn giữ spiffs |
9. Xem thêm
- Lưu dữ liệu bằng Preferences: khi chỉ cần lưu vài giá trị cấu hình.
- ESP32 làm web server điều khiển relay: thêm nút điều khiển vào trang web lưu trong LittleFS.
- Module đọc ghi thẻ nhớ micro SD: khi log cần lưu hàng tháng.
- Bài bảng phân vùng flash trong cùng loạt ESP32 nâng cao: chọn kích thước app, OTA, LittleFS cho phù hợp.
Xem thêm