IoTLabs

Nghiên cứu, Sáng tạo và Thử nghiệm

Series ESP32-S3 Dual-Core – Bài 5: Memory Architecture – SRAM, PSRAM, Flash, Heap

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ùngKích thướcDùng Cho
IRAM~400 KBCode chạy nhanh (ISR, time-critical)
DRAM~320 KBBiến, heap, stack
RTC FAST RAM8 KBGiữ data qua deep sleep
RTC SLOW RAM8 KBGiữ 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 DataBộ Nhớ Phù HợpAPI
ISR, time-critical codeIRAMIRAM_ATTR
Biến local nhỏ trong taskStack (SRAM tự động)Khai báo local
Buffer nhỏ < 50 KBSRAM heapmalloc()
Buffer lớn > 50 KB (LCD, camera)PSRAMps_malloc()
DMA buffer (SPI, I2S)SRAM DMA-capableheapcapsmalloc(MALLOCCAPDMA)
Config lưu qua resetNVSPreferences library
File, assetsFlash SPIFFS/LittleFSSPIFFS.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.