Hướng dẫn lưu dữ liệu vào bộ nhớ flash của ESP32 bằng thư viện Preferences

Hướng dẫn dùng thư viện Preferences trên ESP32 với Arduino IDE để lưu số đếm, trạng thái relay và thông số cài đặt vào bộ nhớ flash, không mất khi tắt nguồn.

Relay đang bật thì mất điện, có điện lại nó nên bật tiếp hay tắt? Cân đã hiệu chuẩn xong, tắt nguồn rồi có phải hiệu chuẩn lại không? Muốn thiết bị nhớ được những thông tin như vậy, dữ liệu phải được ghi vào bộ nhớ flash. Bài viết hướng dẫn dùng thư viện Preferences có sẵn của ESP32 trên Arduino IDE để lưu số đếm, trạng thái và thông số cài đặt sao cho không mất khi tắt nguồn.

1. Dữ liệu nào còn, dữ liệu nào mất?

Bảng so sánh biến thông thường, biến RTC_DATA_ATTR và dữ liệu Preferences: còn hay mất sau khi nhấn EN, sau Deep Sleep và khi mất nguồn
Ba nơi lưu dữ liệu trên ESP32 và khả năng giữ dữ liệu của từng nơi (hình vẽ minh họa).
  • Biến thông thường nằm trong RAM. Mỗi lần ESP32 khởi động lại, biến trở về giá trị ban đầu.
  • Biến khai báo với RTC_DATA_ATTR giữ được giá trị qua các lần Deep Sleep, nhưng vẫn mất khi mất nguồn hoặc khi nhấn nút EN.
  • Dữ liệu ghi vào bộ nhớ flash còn nguyên qua mọi lần khởi động lại và mất nguồn. Nạp chương trình mới cũng không làm mất dữ liệu này.

2. Thư viện Preferences là gì?

Preferences là thư viện đi kèm gói esp32, dùng để lưu các giá trị nhỏ vào một phân vùng riêng của bộ nhớ flash gọi là NVS. Dữ liệu được tổ chức theo ba khái niệm:

Cấu trúc lưu trữ của Preferences trên ESP32: ba vùng lưu trữ thongke, thietbi, caidat, mỗi vùng chứa khóa và giá trị
Vùng lưu trữ, khóa và giá trị trong Preferences (hình vẽ minh họa, số liệu ví dụ).
  • Vùng lưu trữ (namespace): một nhóm dữ liệu có tên riêng, giúp các phần của chương trình không ghi đè lên nhau.
  • Khóa (key): tên của một giá trị trong vùng. Tên vùng và tên khóa dài tối đa 15 ký tự.
  • Giá trị: có kiểu rõ ràng. Ghi bằng lệnh put kiểu nào thì phải đọc bằng lệnh get đúng kiểu đó.
Các cặp lệnh ghi và đọc thường dùng của Preferences
Kiểu dữ liệuLệnh ghiLệnh đọc
Đúng / saiputBool(khóa, giá trị)getBool(khóa, mặc định)
Số nguyên có dấu 32 bitputInt()getInt()
Số nguyên không dấu 32 bitputUInt()getUInt()
Số thựcputFloat()getFloat()
Chuỗi ký tựputString()getString()
Mảng byte, structputBytes()getBytes()

Preferences thay cho thư viện EEPROM quen thuộc trên Arduino Uno. Với dữ liệu lớn như file nhật ký, nên dùng thẻ nhớ theo bài dùng module thẻ nhớ Micro SD với ESP32.

3. Linh kiện cần chuẩn bị

Cả ba chương trình trong bài chỉ dùng LED và nút BOOT có sẵn trên mạch, không cần nối thêm linh kiện.

4. Chương trình 1: đếm số lần khởi động

Mỗi lần khởi động, chương trình đọc số đếm đã lưu, cộng thêm 1 rồi ghi lại:

/*
  PYWORLD.VN - ESP32 Preferences: đếm số lần khởi động, số đếm không mất khi tắt nguồn
  Chỉ cần mạch ESP32
  Thư viện Preferences có sẵn trong gói esp32, không cần cài thêm
*/

#include <Preferences.h>

Preferences boNho;

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

  // Mở vùng lưu trữ tên "thongke"; false = được phép đọc và ghi
  boNho.begin("thongke", false);

  // Đọc khóa "dem"; nếu chưa từng lưu thì nhận giá trị mặc định 0
  unsigned int soLan = boNho.getUInt("dem", 0);
  soLan++;

  boNho.putUInt("dem", soLan);   // Ghi giá trị mới vào bộ nhớ flash
  boNho.end();                   // Đóng vùng lưu trữ

  Serial.printf("\nESP32 da khoi dong %u lan.\n", soLan);
  Serial.println("Nhan nut EN hoac rut nguon cam lai de thay so dem tang.");
}

void loop() {
}

Chọn board DOIT ESP32 DEVKIT V1, nạp chương trình và mở Serial Monitor ở 115200 baud. Nhấn nút EN vài lần, sau đó rút cáp USB rồi cắm lại: số đếm vẫn tăng tiếp chứ không quay về 1.

Hình minh họa Serial Monitor của chương trình đếm số lần khởi động ESP32: số đếm tăng dần sau khi nhấn EN và sau khi rút nguồn
Kết quả mong đợi của chương trình đếm số lần khởi động (hình minh họa theo định dạng in của chương trình).

5. Giải thích các lệnh chính

  • boNho.begin("thongke", false): mở vùng lưu trữ tên thongke. Tham số false cho phép cả đọc và ghi; true là chỉ đọc. Nếu vùng chưa tồn tại, chế độ đọc và ghi sẽ tự tạo.
  • boNho.getUInt("dem", 0): đọc giá trị của khóa dem. Số 0 là giá trị trả về khi khóa chưa từng được lưu, ví dụ ở lần chạy đầu tiên.
  • boNho.putUInt("dem", soLan): ghi giá trị mới vào flash.
  • boNho.end(): đóng vùng lưu trữ khi không dùng nữa.
  • boNho.isKey("heso") (dùng ở chương trình 3): cho biết khóa đã tồn tại hay chưa.
  • boNho.remove("heso") xóa một khóa; boNho.clear() xóa mọi khóa trong vùng đang mở.

6. Chương trình 2: nhớ trạng thái LED hoặc relay sau khi mất điện

Nhấn nút BOOT để bật hoặc tắt LED trên mạch. Trạng thái được lưu lại ngay, và khi có điện trở lại ESP32 khôi phục đúng trạng thái đó. Cách làm với relay hoàn toàn giống vậy, chỉ cần đổi chân LED thành chân điều khiển relay.

/*
  PYWORLD.VN - ESP32 Preferences: nhớ trạng thái LED (hoặc relay) sau khi mất điện
  Nút BOOT có sẵn trên mạch (GPIO0): nhấn để bật, tắt LED
  LED có sẵn trên mạch DevKit V1: GPIO2
  Thư viện Preferences có sẵn trong gói esp32, không cần cài thêm
*/

#include <Preferences.h>

#define NUT_PIN  0
#define LED_PIN  2

Preferences boNho;
bool ledDangBat = false;
bool nutLanTruoc = HIGH;

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

  pinMode(NUT_PIN, INPUT_PULLUP);
  pinMode(LED_PIN, OUTPUT);

  // Khôi phục trạng thái đã lưu; lần chạy đầu tiên dùng giá trị mặc định false (tắt)
  boNho.begin("thietbi", false);
  ledDangBat = boNho.getBool("led", false);
  digitalWrite(LED_PIN, ledDangBat);

  Serial.printf("\nKhoi phuc trang thai: LED %s\n", ledDangBat ? "BAT" : "TAT");
}

void loop() {
  bool nut = digitalRead(NUT_PIN);

  // Phát hiện lúc vừa nhấn nút (mức cao chuyển xuống mức thấp)
  if (nutLanTruoc == HIGH && nut == LOW) {
    ledDangBat = !ledDangBat;
    digitalWrite(LED_PIN, ledDangBat);

    // Chỉ ghi vào flash khi trạng thái thay đổi, không ghi liên tục trong loop()
    boNho.putBool("led", ledDangBat);
    Serial.printf("Da luu: LED %s\n", ledDangBat ? "BAT" : "TAT");

    delay(50);   // Chống dội đơn giản
  }
  nutLanTruoc = nut;
}

Ví dụ kết quả khi nhấn nút một lần, rút nguồn rồi cắm lại (số minh họa):

Khoi phuc trang thai: LED TAT
Da luu: LED BAT

Khoi phuc trang thai: LED BAT

Chỉ nhấn nút BOOT khi chương trình đang chạy. Nếu giữ BOOT trong lúc cấp nguồn hoặc lúc nhấn nút EN, ESP32 sẽ vào chế độ chờ nạp chương trình.

7. Chương trình 3: lưu thông số cài đặt

Nhiều thiết bị cần nhớ một vài thông số do người dùng nhập: hệ số hiệu chuẩn của cân, ngưỡng cảnh báo, thời gian trễ. Chương trình sau nhận một số từ Serial Monitor và lưu lại; lần khởi động sau nó tự đọc lên.

/*
  PYWORLD.VN - ESP32 Preferences: lưu một thông số cài đặt nhập từ Serial Monitor
  Gõ một số (ví dụ 420.5) rồi nhấn Enter để lưu; gõ xoa rồi nhấn Enter để xóa
  Thư viện Preferences có sẵn trong gói esp32, không cần cài thêm
*/

#include <Preferences.h>

const float HE_SO_MAC_DINH = 1.0;

Preferences boNho;
float heSo = HE_SO_MAC_DINH;

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

  boNho.begin("caidat", false);

  // isKey() cho biết khóa đã từng được lưu hay chưa
  if (boNho.isKey("heso")) {
    heSo = boNho.getFloat("heso", HE_SO_MAC_DINH);
    Serial.printf("\nDa doc he so da luu: %.3f\n", heSo);
  } else {
    Serial.printf("\nChua co he so da luu, dung mac dinh: %.3f\n", heSo);
  }
  Serial.println("Go mot so roi nhan Enter de luu. Go xoa de xoa.");
}

void loop() {
  if (!Serial.available()) {
    return;
  }

  String dong = Serial.readStringUntil('\n');
  dong.trim();
  dong.replace(',', '.');
  if (dong.length() == 0) {
    return;
  }

  if (dong == "xoa") {
    boNho.remove("heso");
    heSo = HE_SO_MAC_DINH;
    Serial.println("Da xoa. He so quay ve mac dinh.");
    return;
  }

  float giaTri = dong.toFloat();
  if (giaTri == 0) {
    Serial.println("Gia tri khong hop le.");
    return;
  }

  // Chỉ ghi vào flash khi giá trị thay đổi
  if (giaTri != heSo) {
    heSo = giaTri;
    boNho.putFloat("heso", heSo);
  }
  Serial.printf("He so hien tai: %.3f (da luu)\n", heSo);
}

Trong Serial Monitor, chọn kiểu kết thúc dòng New Line, gõ 420.5 rồi nhấn Enter. Nhấn nút EN để khởi động lại và kiểm tra (số minh họa):

Chua co he so da luu, dung mac dinh: 1.000
Go mot so roi nhan Enter de luu. Go xoa de xoa.
He so hien tai: 420.500 (da luu)

Da doc he so da luu: 420.500
Go mot so roi nhan Enter de luu. Go xoa de xoa.

8. Ghi flash đúng cách để bộ nhớ bền

Mỗi ô nhớ flash chỉ chịu được một số lần xóa ghi giới hạn, cỡ một trăm nghìn lần. NVS tự xoay vòng vị trí ghi nên tuổi thọ thực tế cao hơn nhiều, nhưng vẫn cần ghi có chừng mực:

  • Chỉ ghi khi giá trị thay đổi. Cả ba chương trình trong bài đều ghi theo sự kiện, không ghi lặp lại trong loop().
  • Không lưu số đo thay đổi liên tục như nhiệt độ từng giây. Nếu cần, hãy gom lại và ghi thưa, ví dụ mỗi giờ một lần.
  • Số đếm thay đổi nhanh trong thiết bị dùng Deep Sleep nên để ở bộ nhớ RTC, thỉnh thoảng mới chép sang flash.
  • Dung lượng nhỏ: phân vùng NVS mặc định chỉ khoảng 20 KB, phù hợp với cài đặt và trạng thái, không dùng để lưu dữ liệu lớn.

9. Xóa dữ liệu đã lưu

  • Xóa một khóa: boNho.remove("tên khóa").
  • Xóa cả vùng đang mở: boNho.clear().
  • Xóa toàn bộ flash, kể cả dữ liệu Preferences: trong Arduino IDE chọn Tools > Erase All Flash Before Sketch Upload > Enabled rồi nạp lại chương trình. Sau đó nên đặt lại thành Disabled.

10. Xử lý lỗi thường gặp

  • Giá trị không được lưu: tên vùng hoặc tên khóa dài quá 15 ký tự, hoặc vùng đang mở ở chế độ chỉ đọc (begin(tên, true)).
  • Đọc ra luôn là giá trị mặc định: đọc sai kiểu so với lúc ghi (ví dụ ghi bằng putInt() nhưng đọc bằng getFloat()), hoặc gõ sai tên khóa. Tên khóa có phân biệt chữ hoa, chữ thường.
  • Mở vùng ở chế độ chỉ đọc mà không đọc được gì: vùng đó chưa từng được tạo, khi ấy begin() trả về false và mọi lệnh đọc đều ra giá trị mặc định. Mở vùng một lần ở chế độ đọc và ghi để tạo nó.
  • Dữ liệu biến mất sau khi nạp chương trình: tùy chọn Erase All Flash Before Sketch Upload đang bật.
  • Muốn lưu mật khẩu Wi-Fi: làm được bằng putString(), nhưng dữ liệu NVS mặc định không được mã hóa. Không dùng cách này cho thông tin thật sự nhạy cảm.
  • Không nạp được chương trình: dùng cáp USB có truyền dữ liệu, chọn đúng cổng COM và giữ nút BOOT khi IDE dừng ở dòng Connecting...

Xem thêm: ESP32 làm web server điều khiển relay qua Wi-Fi, ESP32 điều khiển module relay 4 kênh và HX711 với load cell và LCD 1602 bằng MicroPython.


Tin tức liên quan

Hướng dẫn làm cân điện tử với ESP32, HX711 và load cell trên Arduino IDE
Hướng dẫn làm cân điện tử với ESP32, HX711 và load cell trên Arduino IDE

7 Lượt xem

Hướng dẫn nối load cell với module HX711 và ESP32 DevKit V1, gá load cell đúng cách, hiệu chuẩn bằng vật đã biết khối lượng và viết chương trình cân trên Arduino IDE.

Hướng dẫn làm đồng hồ Internet với ESP32, NTP và màn hình OLED 0.96 inch
Hướng dẫn làm đồng hồ Internet với ESP32, NTP và màn hình OLED 0.96 inch

5 Lượt xem

Hướng dẫn ESP32 lấy giờ Việt Nam từ Internet bằng NTP trên Arduino IDE, in ra Serial Monitor và làm đồng hồ hiển thị thứ, ngày, giờ trên màn hình OLED 0.96 inch SSD1306.

Hướng dẫn dùng MQTT với ESP32: gửi dữ liệu và điều khiển relay bằng PubSubClient
Hướng dẫn dùng MQTT với ESP32: gửi dữ liệu và điều khiển relay bằng PubSubClient

5 Lượt xem

Hướng dẫn dùng MQTT với ESP32 trên Arduino IDE: broker, topic, publish, subscribe, cài Mosquitto và PubSubClient, gửi dữ liệu và điều khiển module relay qua mạng nội bộ.

Hướng dẫn điều khiển relay bằng điện thoại qua Bluetooth với ESP32
Hướng dẫn điều khiển relay bằng điện thoại qua Bluetooth với ESP32

4 Lượt xem

Hướng dẫn dùng Bluetooth Classic của ESP32 trên Arduino IDE: ghép đôi với điện thoại Android, gửi nhận chữ bằng BluetoothSerial và điều khiển module relay bằng lệnh ON, OFF.

Hướng dẫn nạp chương trình cho ESP32 qua Wi-Fi (OTA) bằng Arduino IDE
Hướng dẫn nạp chương trình cho ESP32 qua Wi-Fi (OTA) bằng Arduino IDE

4 Lượt xem

Hướng dẫn bật OTA trên ESP32 bằng thư viện ArduinoOTA có sẵn, nạp chương trình qua Wi-Fi từ Arduino IDE 2.x, đặt mật khẩu và các quy tắc để không mất khả năng cập nhật từ xa.

Hướng dẫn dùng màn hình OLED 0.96 inch SSD1306 với Arduino Nano
Hướng dẫn dùng màn hình OLED 0.96 inch SSD1306 với Arduino Nano

5 Lượt xem

Hướng dẫn nối màn hình OLED 0.96 inch SSD1306 (I2C) với Arduino Nano, cài thư viện Adafruit, hiện chữ nhiều cỡ, vẽ thanh mức theo điện áp và lưu ý về bộ nhớ RAM.

Hướng dẫn đọc ADC trên ESP32 và đo điện áp pin 18650 bằng cầu phân áp
Hướng dẫn đọc ADC trên ESP32 và đo điện áp pin 18650 bằng cầu phân áp

5 Lượt xem

Hướng dẫn đọc điện áp analog bằng ADC của ESP32 trên Arduino IDE, dùng analogReadMilliVolts, làm cầu phân áp đo pin Li-ion 18650 và ước lượng phần trăm pin còn lại.

Hướng dẫn dùng Deep Sleep trên ESP32: tiết kiệm pin, đánh thức bằng timer và nút nhấn
Hướng dẫn dùng Deep Sleep trên ESP32: tiết kiệm pin, đánh thức bằng timer và nút nhấn

5 Lượt xem

Hướng dẫn đưa ESP32 vào Deep Sleep trên Arduino IDE để tiết kiệm pin: các chế độ nguồn, đánh thức bằng timer và nút nhấn, giữ biến trong bộ nhớ RTC và ước tính thời lượng pin.


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