Tại Sao Cần Hiểu Memory?
Bạn đã gặp những lỗi này chưa?
malloc()trả về NULL dùESP.getFreeHeap()vẫn còn vài chục KB- Firmware crash ngẫu nhiên sau vài giờ, không có error rõ ràng
- LVGL hoặc camera buffer fail khi khởi tạo
- Task bị stack overflow dù stack size trông đủ lớn
Tất cả đều liên quan đến việc không hiểu rõ bộ nhớ đang được dùng thế nào. ESP32-S3 có nhiều loại bộ nhớ với đặc tính khác nhau — biết khi nào dùng cái nào là kỹ năng quan trọng.
Bản Đồ Bộ Nhớ ESP32-S3
Địa chỉ Tên Kích thước Đặc điểm
─────────────────────────────────────────────────────────
0x3FC88000 DRAM (data) 320 KB Đọc/ghi, DMA-capable
0x3FCF0000 DRAM (bss) ~192 KB BSS, zero-init
0x40370000 IRAM 384 KB Code nhanh, không cache
0x40000000 ROM 448 KB Bootloader, Espressif code
0x3C000000 Flash (mapped) 16 MB Code, data, filesystem
0x3D000000 PSRAM (ext) 8 MB External, cần enable
Thực tế firmware thấy ít hơn vì:
- WiFi stack chiếm ~100 KB DRAM
- FreeRTOS kernel ~30 KB
- Bluetooth ~80 KB (nếu bật)
- Còn lại cho application: ~180–220 KB DRAM
SRAM: Nhanh Nhưng Ít
SRAM nội của ESP32-S3 là 512 KB, chia thành:
| Vùng | Kích thước | Dùng Cho |
|---|---|---|
| IRAM | ~400 KB | Code chạy nhanh (ISR, time-critical) |
| DRAM | ~320 KB | Biến, heap, stack |
| RTC FAST RAM | 8 KB | Giữ data qua deep sleep |
| RTC SLOW RAM | 8 KB | Giữ data qua deep sleep (chậm hơn) |
SRAM được truy cập trong 1 chu kỳ clock — nhanh nhất có thể. Khi code chạy từ Flash, CPU phải chờ Flash bus (100–200 ns mỗi cache miss). Khi code chạy từ SRAM (IRAM), không có delay.
// Kiểm tra bộ nhớ nội khả dụng
Serial.printf("Free internal heap: %lu bytes\n",
heap_caps_get_free_size(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT));
// Largest contiguous block (quan trọng hơn total free!)
Serial.printf("Largest free block: %lu bytes\n",
heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT));
PSRAM: Nhiều Nhưng Chậm Hơn
Board ESP32-S3 có PSRAM (Pseudo Static RAM) kết nối qua Octal SPI ở 80 MHz. Kích thước thường là 8 MB.
PSRAM chậm hơn SRAM nội khoảng 5–10 lần vì phải qua bus SPI. Tuy nhiên vẫn nhanh hơn nhiều so với Flash và đủ cho:
- Frame buffer LCD (320×240×2 = 150 KB)
- Camera frame buffer (640×480 = 614 KB raw)
- LVGL display buffer
- Audio buffer
- String/JSON buffer lớn
// Cấp phát từ PSRAM
void* psramPtr = ps_malloc(1024 * 1024); // 1 MB từ PSRAM
if (!psramPtr) {
Serial.println("PSRAM alloc fail!");
return;
}
// Hoặc dùng heap_caps để kiểm soát chi tiết hơn
uint8_t* buf = (uint8_t*)heap_caps_malloc(
640 * 480 * 2, // 614 KB
MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT
);
Khi nào PSRAM tự động được dùng:
Nếu bật CONFIGSPIRAMUSE_MALLOC=y trong menuconfig (hoặc board config Arduino), malloc() thông thường sẽ tự chọn PSRAM khi SRAM nội không đủ. Trong Arduino, hành vi này phụ thuộc vào board definition.
Heap: Bộ Nhớ Động
malloc() / new / free() làm việc với heap — vùng bộ nhớ được quản lý động. ESP-IDF dùng heap manager riêng hỗ trợ nhiều capability:
// Cấp phát từ SRAM nội, DMA-capable (cho SPI, I2S DMA)
uint8_t* dmaBuffer = (uint8_t*)heap_caps_malloc(
4096,
MALLOC_CAP_INTERNAL | MALLOC_CAP_DMA
);
// Cấp phát từ PSRAM
uint8_t* largeBuffer = (uint8_t*)heap_caps_malloc(
512 * 1024,
MALLOC_CAP_SPIRAM
);
// Cấp phát từ bất kỳ đâu còn đủ (ưu tiên SRAM)
uint8_t* anyBuffer = (uint8_t*)heap_caps_malloc(
1024,
MALLOC_CAP_DEFAULT
);
Memory Fragmentation
Vấn đề phổ biến với firmware chạy lâu:
Sau 24 giờ chạy:
Total free heap: 40 KB
Largest free block: 8 KB ← CHỈ có thể malloc tối đa 8 KB!
Heap bị phân mảnh — nhiều mảnh nhỏ rải rác thay vì một khối liên tục. malloc(20 * 1024) sẽ fail dù free heap còn 40 KB.
Giải pháp:
- Cấp phát buffer lớn một lần trong
setup(), không trong loop - Dùng pool allocator cho buffer cùng kích thước
- Buffer lớn → PSRAM (ít bị fragment vì riêng biệt)
// ĐÚNG: cấp phát lớn một lần
uint8_t* framebuffer;
void setup() {
framebuffer = (uint8_t*)ps_malloc(320 * 240 * 2); // Cấp phát 1 lần
// Dùng suốt vòng đời firmware, không free/malloc lại
}
// SAI: cấp phát trong loop
void loop() {
uint8_t* buf = (uint8_t*)malloc(1024); // malloc mỗi loop → fragmentation!
processData(buf);
free(buf);
}
Stack: Bộ Nhớ Task
Mỗi FreeRTOS task có stack riêng — vùng bộ nhớ cho biến local, call stack, và context switch:
// Stack 4096 bytes — đủ cho logic đơn giản
xTaskCreatePinnedToCore(simpleTask, "Simple", 4096, NULL, 1, NULL, 1);
// Stack 16384 bytes — cần cho LCD, JSON, HTTP
xTaskCreatePinnedToCore(heavyTask, "Heavy", 16384, NULL, 1, NULL, 1);
Stack của task nằm trong SRAM nội (không phải PSRAM). Đây là lý do stack size tổng cộng bị giới hạn bởi SRAM.
Đo stack usage thực tế:
void monitorTask(void* p) {
while (true) {
// In stack usage của task hiện tại
Serial.printf("[%s] Stack còn: %u bytes\n",
pcTaskGetName(NULL),
uxTaskGetStackHighWaterMark(NULL) * sizeof(StackType_t));
vTaskDelay(pdMS_TO_TICKS(5000));
}
}
Flash: Code và Filesystem
Flash 16 MB trên ESP32-S3 được phân vùng (partition):
nvs 0x9000 0x6000 (24 KB) — WiFi, preferences storage
otadata 0xF000 0x2000 (8 KB) — OTA partition info
app0 0x10000 0x640000 (6.25 MB) — Firmware slot 1
app1 0x650000 0x640000 (6.25 MB) — Firmware slot 2 (OTA)
spiffs 0xC90000 0x370000 (3.5 MB) — Filesystem
Code của bạn chạy từ Flash qua MMU (Memory Management Unit) — Flash được map vào địa chỉ 0x3C000000. CPU đọc code như đọc RAM, cache giúp ẩn latency Flash.
// Đọc thông tin partition
#include "esp_partition.h"
void printPartitions() {
esp_partition_iterator_t it = esp_partition_find(
ESP_PARTITION_TYPE_ANY, ESP_PARTITION_SUBTYPE_ANY, NULL);
while (it != NULL) {
const esp_partition_t* p = esp_partition_get(it);
Serial.printf("%-16s 0x%06x %6lu KB\n",
p->label, p->address, p->size / 1024);
it = esp_partition_next(it);
}
esp_partition_iterator_release(it);
}
IRAM_ATTR: Code Nhanh Không Cache
Macro IRAM_ATTR yêu cầu linker đặt hàm vào IRAM:
// ISR phải ở IRAM — không được phép cache miss trong ISR
void IRAM_ATTR criticalISR() {
gpio_set_level(LED_PIN, 1); // Set pin nhanh nhất có thể
}
// Hàm time-critical cũng nên ở IRAM
void IRAM_ATTR fastSignalProcess(int16_t* samples, int count) {
for (int i = 0; i < count; i++) {
samples[i] = samples[i] * 2; // Xử lý không qua cache
}
}
IRAM có khoảng 400 KB tổng, nhưng phần lớn dành cho WiFi và FreeRTOS. Application chỉ nên dùng IRAM_ATTR cho ISR và hàm tối quan trọng về thời gian — không đặt hàm thông thường vào IRAM vì sẽ hết nhanh.
Kiểm Tra Memory Health
void printMemoryStatus() {
Serial.println("\n=== Memory Status ===");
// Heap nội
Serial.printf("Internal heap: free=%lu KB, largest=%lu KB\n",
heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024,
heap_caps_get_largest_free_block(MALLOC_CAP_INTERNAL) / 1024);
// PSRAM
if (psramFound()) {
Serial.printf("PSRAM: free=%lu KB, largest=%lu KB\n",
heap_caps_get_free_size(MALLOC_CAP_SPIRAM) / 1024,
heap_caps_get_largest_free_block(MALLOC_CAP_SPIRAM) / 1024);
}
// Tổng heap
Serial.printf("Total heap: free=%lu KB\n",
ESP.getFreeHeap() / 1024);
Serial.println("===================\n");
}
Tổng Kết: Chọn Bộ Nhớ Đúng
| Loại Data | Bộ Nhớ Phù Hợp | API |
|---|---|---|
| ISR, time-critical code | IRAM | IRAM_ATTR |
| Biến local nhỏ trong task | Stack (SRAM tự động) | Khai báo local |
| Buffer nhỏ < 50 KB | SRAM heap | malloc() |
| Buffer lớn > 50 KB (LCD, camera) | PSRAM | ps_malloc() |
| DMA buffer (SPI, I2S) | SRAM DMA-capable | heapcapsmalloc(MALLOCCAPDMA) |
| Config lưu qua reset | NVS | Preferences library |
| File, assets | Flash SPIFFS/LittleFS | SPIFFS.open() |
Bài tiếp theo: Bài 6 — DMA, LCD, Camera và Giao Tiếp Tốc Độ Cao Trên ESP32-S3 — DMA giải phóng CPU thế nào, LCD framebuffer pipeline và camera capture với DMA.
📚 Series: Sức Mạnh ESP32-S3 Dual-Core
⬅️ Bài trước: S3 Dual-Core – Bài 4: Thiết Kế Firmware Dual-Core Đúng Cách
➡️ Bài tiếp theo: S3 Dual-Core – Bài 6: DMA, LCD, Camera Giao Tiếp Tốc Độ Cao


