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

Thanh 4 MB chia thành các phân vùng nvs, otadata, app0, app1, spiffs, coredump; chú thích phân vùng spiffs 1,375 MB là chỗ LittleFS dùng; bảng địa chỉ bắt đầu và kích thước từng phân vùng
Bảng phân vùng mặc định của ESP32 DevKit V1 (4 MB): LittleFS dùng phân vùng tên spiffs.

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)LittleFSThẻ nhớ microSD
Dạng dữ liệuCặp khóa – giá trị (số, chuỗi ngắn)File và thư mụcFile và thư mục (FAT)
Dung lượngVài KBKhoảng 1,4 MB (bảng mặc định)Hàng GB
Hợp vớiCấu hình, mật khẩu Wi-Fi, bộ đếmTrang web, file cấu hình JSON, log vừa phảiLog dài ngày, ảnh, dữ liệu lớn, đọc trên máy tính
Phần cứng thêmKhôngKhôngModule thẻ nhớ, 4 dây SPI

3. Đọc và ghi file

Bốn file dem.txt, index.html, log.csv, log_cu.csv với cách dùng: đọc rồi ghi đè, serveStatic gửi cho trình duyệt, ghi tiếp vào cuối, đổi tên khi quá 64 KB; bên dưới là vài dòng CSV lan_khoi_dong, giay, mV
Mỗi file một việc; chế độ mở quyết định ghi đè hay ghi tiếp (log là ví dụ).

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_WRITE tạo mới hoặc ghi đè; FILE_APPEND ghi 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ồi openNextFile() để 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.csv vượt 64 KB, chương trình xóa log_cu.csv rồi đổi tên log.csv thành log_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

Cửa sổ trình duyệt mở http://192.168.1.57/ hiển thị trang Log điện áp GPIO34 với đồ thị đường, dòng mới nhất lần khởi động 3 giây 1300 721 mV và liên kết Tải log.csv
Trình duyệt mở địa chỉ IP của ESP32: đồ thị 100 dòng log cuối (dữ liệu ví dụ).

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

Serial Monitor: LittleFS đã dùng 16384 trên 1441792 byte, khởi động lần 3; danh sách file dem.txt, index.html, log.csv với kích thước; địa chỉ http; gõ X in các dòng log
Khởi động lần thứ 3: các file từ lần trước vẫn còn; gõ X để in log (số liệu ví dụ).

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ượngNguyên nhân thường gặpCá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ốngQuê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ở đượcThiếu dấu / đầu đường dẫn; tên quá dàiLuôn bắt đầu bằng /; tên ngắn, không dấu
Trang web cũ, sửa TRANG_WEB không ănChươ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ầnLog không giới hạn kích thướcXoay vòng như trong bài
Chương trình quá lớn khi thêm Wi-Fi, webPhân vùng ứng dụng 1,25 MBChọn bảng phân vùng có app lớn hơn nhưng vẫn giữ spiffs

9. Xem thêm


Tin tức liên quan

Jack DC 5.5×2.1 và 5.5×2.5: phân biệt, cực tính, chọn jack gắn hộp và dây nguồn DC
Jack DC 5.5×2.1 và 5.5×2.5: phân biệt, cực tính, chọn jack gắn hộp và dây nguồn DC

Jack DC 5.5×2.1 và 5.5×2.5 khác nhau ở lỗ trong: cách phân biệt khi không có nhãn, ký hiệu tâm dương, đo cực tính adapter, lắp jack gắn hộp và nối nguồn vào mạch an toàn.

1-Wire là gì: khe thời gian, mã ROM và cách đọc DS18B20 bằng ESP32
1-Wire là gì: khe thời gian, mã ROM và cách đọc DS18B20 bằng ESP32

Giải thích giao tiếp 1-Wire: một dây dữ liệu, điện trở kéo lên, bốn kiểu khe thời gian, mã ROM 64 bit và CRC. Kèm hai chương trình ESP32 tự viết giao thức để đọc mã ROM và nhiệt độ DS18B20.

ESP32 không nhận cổng COM, lỗi Failed to connect, phải giữ nút BOOT: nguyên nhân và cách sửa
ESP32 không nhận cổng COM, lỗi Failed to connect, phải giữ nút BOOT: nguyên nhân và cách sửa

Cách sửa ESP32 không nhận cổng COM, lỗi Failed to connect, Wrong boot mode, phải giữ nút BOOT, nạp xong không chạy: dây USB, driver, chân khởi động, tụ 10 µF ở chân EN, bảng tra thông báo lỗi.

Cảm biến chuyển vị: biến trở, LVDT, điện dung, dòng xoáy, laser – cấu tạo, nguyên lý và chế tạo
Cảm biến chuyển vị: biến trở, LVDT, điện dung, dòng xoáy, laser – cấu tạo, nguyên lý và chế tạo

Cảm biến chuyển vị hoạt động thế nào: biến trở thẳng, LVDT, điện dung, dòng điện xoáy, laser tam giác lượng, từ giảo, cách chế tạo và đo vị trí, vận tốc bằng thước điện trở với ESP32.

Gá lắp gia tốc kế theo ISO 5348: tần số cộng hưởng, dải tần dùng được và các sai số khi đo rung
Gá lắp gia tốc kế theo ISO 5348: tần số cộng hưởng, dải tần dùng được và các sai số khi đo rung

Cách gá gia tốc kế theo ISO 5348: vít cấy, keo, sáp, nam châm, đầu dò cầm tay; tần số cộng hưởng lắp đặt, dải tần dùng được, ảnh hưởng khối lượng cảm biến và các điểm kỹ thuật khi gắn.

MOSFET: logic-level, điện trở gate, đóng cắt tải dòng lớn bằng PWM của ESP32
MOSFET: logic-level, điện trở gate, đóng cắt tải dòng lớn bằng PWM của ESP32

MOSFET kênh N, kênh P là gì, vì sao chọn MOSFET logic-level cho ESP32 3,3 V, VGS(th) và RDS(on), mạch đóng cắt phía thấp có điện trở gate, PWM và thí nghiệm chỉnh độ sáng dải LED 12 V.

Xúc xắc điện tử với ESP32 và OLED: thí nghiệm xác suất cho học sinh
Xúc xắc điện tử với ESP32 và OLED: thí nghiệm xác suất cho học sinh

Làm xúc xắc điện tử bằng ESP32 và màn hình OLED kèm 4 nút: tung một lần, tung nhanh 60 lần, vẽ biểu đồ; xác suất lý thuyết, thực nghiệm và luật số lớn cho học sinh.

Cảm biến độ ẩm: điện dung, điện trở, ẩm kế khô ướt – cấu tạo, nguyên lý, chế tạo và điểm sương
Cảm biến độ ẩm: điện dung, điện trở, ẩm kế khô ướt – cấu tạo, nguyên lý, chế tạo và điểm sương

Cảm biến độ ẩm hoạt động thế nào: độ ẩm tương đối, tuyệt đối, điểm sương; cảm biến điện dung polyme, điện trở, ẩm kế tóc, khô ướt, gương lạnh; chế tạo và đọc SHT31 bằng ESP32.


Bình luận
Đã thêm vào giỏ hàng