Bảng phân vùng flash ESP32: app, OTA, NVS, LittleFS, chọn Partition Scheme và tự đặt partitions.csv
Bảng phân vùng flash ESP32 gồm những gì, so sánh các Partition Scheme có sẵn, vì sao OTA cần hai ô app, tự đặt partitions.csv đúng quy tắc, chương trình đọc bảng đang nạp.
Khi chương trình ESP32 lớn dần (Wi-Fi, web server, HTTPS, Bluetooth), sẽ tới lúc gặp lỗi "Sketch too big" dù chip có tới 4 MB flash. Lý do là flash được chia thành nhiều phân vùng, và phần dành cho chương trình mặc định chỉ có 1,25 MB. Bài này giải thích bảng phân vùng gồm những gì, các bảng có sẵn trong Arduino khác nhau ra sao, OTA cần hai ô chương trình như thế nào, cách tự đặt bảng bằng file partitions.csv, và một chương trình đọc lại bảng đang nạp trong chip.
1. Bảng phân vùng là gì
Bảng phân vùng là một danh sách nhỏ nạp ở địa chỉ 0x8000 của flash, cho biết mỗi vùng tên gì, dùng làm gì, bắt đầu ở đâu, dài bao nhiêu. Bảng mặc định của ESP32 DevKit V1 có:
| Phân vùng | Kích thước | Dùng cho |
|---|---|---|
| nvs | 20 KB | Preferences, cấu hình Wi-Fi đã lưu, dữ liệu của thư viện |
| otadata | 8 KB | Ghi nhớ lần khởi động sau chạy ô chương trình nào |
| app0 | 1,25 MB | Chương trình (ô thứ nhất) |
| app1 | 1,25 MB | Chương trình (ô thứ hai, dùng khi cập nhật qua mạng) |
| spiffs | 1,375 MB | Hệ thống file LittleFS (hoặc SPIFFS) |
| coredump | 64 KB | Lưu thông tin lỗi khi chip bị panic, để đọc lại sau |
36 KB đầu tiên dành cho bootloader và chính bảng phân vùng. Nạp chương trình chỉ ghi vào ô app, nên dữ liệu trong nvs và spiffs vẫn còn qua các lần nạp.
2. Các bảng có sẵn

- Default: cân bằng, có OTA, có LittleFS 1,375 MB. Chương trình tối đa 1,25 MB.
- Minimal SPIFFS: giữ OTA, mỗi ô chương trình 1,875 MB, LittleFS chỉ còn 128 KB. Hợp với chương trình lớn có cập nhật qua mạng.
- No OTA: một ô chương trình 2 MB, LittleFS 1,875 MB, không cập nhật qua mạng được.
- Huge APP: một ô 3 MB cho chương trình rất lớn (ví dụ Wi-Fi và Bluetooth cùng lúc), LittleFS 896 KB, không OTA.
Lưu ý với bo DOIT ESP32 DevKit V1: trong Arduino IDE, bo này không có menu Partition Scheme. Có hai cách đổi bảng: chọn bo "ESP32 Dev Module" (cùng loại chip, có đủ menu Partition Scheme), hoặc đặt file partitions.csv trong thư mục sketch như mục 4.
3. Vì sao OTA cần hai ô chương trình

Cập nhật qua mạng (OTA) không thể ghi đè lên chính chương trình đang chạy. Chương trình mới được tải về và ghi vào ô còn lại, kiểm tra toàn vẹn, rồi phân vùng otadata được sửa để lần khởi động sau chạy ô mới. Nếu tải dở, mất điện, hay bản mới lỗi, ô cũ vẫn còn nguyên để chạy tiếp. Cái giá là mỗi ô chỉ được một nửa: muốn OTA, chương trình phải vừa một ô.
4. Tự đặt bảng bằng partitions.csv
Đặt một file tên partitions.csv vào cùng thư mục với file .ino; gói esp32 sẽ dùng file đó thay cho bảng mặc định khi biên dịch và nạp. Ví dụ bảng giữ OTA, mỗi ô 1,75 MB, LittleFS 384 KB:
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x1C0000,
app1, app, ota_1, 0x1D0000,0x1C0000,
spiffs, data, spiffs, 0x390000,0x60000,
coredump, data, coredump,0x3F0000,0x10000,
Quy tắc khi tự đặt:
- Phân vùng sau bắt đầu đúng chỗ phân vùng trước kết thúc (địa chỉ + kích thước), không chồng lên nhau, không vượt dung lượng flash.
- Phân vùng loại
appphải bắt đầu ở địa chỉ chia hết cho 0x10000 (64 KB). - Giữ tên
spiffscho phân vùng file:LittleFS.begin()mặc định tìm phân vùng tên này. - Đổi bảng có thể làm mất dữ liệu cũ trong nvs và spiffs nếu địa chỉ của chúng thay đổi.
5. Chương trình đọc bảng đang nạp
Sau khi đổi Partition Scheme hay thêm partitions.csv, chương trình này cho biết bảng thật sự đang nằm trong chip: từng phân vùng, ô chương trình đang chạy, chương trình chiếm bao nhiêu phần trăm ô, OTA sẽ ghi vào đâu, và bộ nhớ NVS đã dùng bao nhiêu.
// PYWORLD.VN - Đọc bảng phân vùng flash đang nạp trong ESP32: tên, loại, địa chỉ, kích thước, phân vùng chương trình đang chạy, chỗ trống cho OTA
// Dùng để kiểm tra Partition Scheme hoặc file partitions.csv trong thư mục sketch đã có tác dụng chưa
// Chỉ cần ESP32, không cần thư viện ngoài (hàm esp_partition, esp_ota, nvs của ESP-IDF có sẵn trong gói esp32)
#include <esp_partition.h>
#include <esp_ota_ops.h>
#include <nvs.h>
const char *tenKieu(const esp_partition_t *p) {
if (p->type == ESP_PARTITION_TYPE_APP) {
if (p->subtype == ESP_PARTITION_SUBTYPE_APP_FACTORY) return "factory";
if (p->subtype >= ESP_PARTITION_SUBTYPE_APP_OTA_MIN && p->subtype <= ESP_PARTITION_SUBTYPE_APP_OTA_MAX) return "ota_n";
return "app?";
}
switch (p->subtype) {
case ESP_PARTITION_SUBTYPE_DATA_OTA: return "ota";
case ESP_PARTITION_SUBTYPE_DATA_NVS: return "nvs";
case ESP_PARTITION_SUBTYPE_DATA_COREDUMP: return "coredump";
case ESP_PARTITION_SUBTYPE_DATA_FAT: return "fat";
case ESP_PARTITION_SUBTYPE_DATA_SPIFFS: return "spiffs";
default: return "khac";
}
}
void inLoai(esp_partition_type_t loai, const esp_partition_t *dangChay) {
// esp_partition_next() trả về NULL và tự giải phóng iterator khi hết danh sách
for (esp_partition_iterator_t it = esp_partition_find(loai, ESP_PARTITION_SUBTYPE_ANY, NULL); it; it = esp_partition_next(it)) {
const esp_partition_t *p = esp_partition_get(it);
Serial.printf("%-10s %-5s %-9s 0x%06lx %8lu %s\n", p->label, loai == ESP_PARTITION_TYPE_APP ? "app" : "data", tenKieu(p),
(unsigned long)p->address, (unsigned long)p->size, p == dangChay ? "<- dang chay" : "");
}
}
void setup() {
Serial.begin(115200);
delay(500);
const esp_partition_t *dangChay = esp_ota_get_running_partition();
const esp_partition_t *otaKeTiep = esp_ota_get_next_update_partition(NULL);
Serial.printf("Flash: %lu byte (%lu MB)\n", (unsigned long)ESP.getFlashChipSize(), (unsigned long)(ESP.getFlashChipSize() >> 20));
Serial.println("Ten Loai Kieu Dia chi Kich thuoc");
inLoai(ESP_PARTITION_TYPE_APP, dangChay);
inLoai(ESP_PARTITION_TYPE_DATA, dangChay);
Serial.printf("Chuong trinh dang dung %lu byte (%lu%% phan vung %s)\n", (unsigned long)ESP.getSketchSize(),
(unsigned long)(100ULL * ESP.getSketchSize() / dangChay->size), dangChay->label);
if (otaKeTiep) Serial.printf("Cap nhat OTA se ghi vao: %s (%lu byte)\n", otaKeTiep->label, (unsigned long)otaKeTiep->size);
else Serial.println("Bang phan vung khong co cho cho OTA");
nvs_stats_t thongKe;
if (nvs_get_stats(NULL, &thongKe) == ESP_OK) {
Serial.printf("NVS (Preferences): da dung %u / %u muc\n", (unsigned)thongKe.used_entries, (unsigned)thongKe.total_entries);
}
}
void loop() {}
6. Kết quả mong đợi

Con số phần trăm trùng với dòng "Sketch uses … (21%) of program storage space" mà Arduino IDE in ra khi biên dịch: đó chính là tỉ lệ so với một ô app. Khi con số này vượt khoảng 90%, nên tính trước việc đổi bảng phân vùng.
7. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| "Sketch too big" / "text section exceeds available space" | Chương trình lớn hơn ô app 1,25 MB | Chọn Minimal SPIFFS, No OTA hoặc Huge APP; hoặc tự đặt bảng |
| Không thấy menu Partition Scheme | Đang chọn bo DOIT ESP32 DevKit V1 | Chọn "ESP32 Dev Module" hoặc dùng partitions.csv |
| LittleFS không mở được sau khi đổi bảng | Bảng mới không có phân vùng spiffs, hoặc đổi tên | Giữ phân vùng tên spiffs; begin(true) để định dạng |
| Mất cấu hình đã lưu sau khi đổi bảng | Địa chỉ nvs, spiffs thay đổi | Giữ nguyên nvs ở 0x9000; sao lưu dữ liệu trước khi đổi |
| OTA báo không đủ chỗ | Bảng không có app1, hoặc chương trình mới lớn hơn một ô | Dùng bảng có hai ô app đủ lớn |
| Bảng tự đặt nạp xong chip không khởi động | Phân vùng app không căn 64 KB, chồng nhau, vượt dung lượng flash | Kiểm tra lại địa chỉ, kích thước theo quy tắc ở mục 4 |
8. Xem thêm
- Lưu dữ liệu bằng Preferences: dữ liệu nằm trong phân vùng nvs.
- ESP32 DevKit V1 38 chân và 30 chân: cùng flash 4 MB, cùng bảng mặc định.
- Bài LittleFS và bài watchdog (coredump, lý do khởi động lại) trong cùng loạt ESP32 nâng cao.
Xem thêm