IoTLabs

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

Series ESP32-S3 Dual-Core – Bài 6: DMA, LCD, Camera & Giao Tiếp Tốc Độ Cao

Vấn Đề: CPU Bận Truyền Data

Khi LCD update màn hình 30 lần/giây với 320×240 pixels, mỗi frame gồm 153.600 bytes cần truyền qua SPI. Nếu CPU tự ghi từng byte, nó phải làm công việc này:

320 × 240 × 2 bytes = 153.600 bytes × 30 fps = 4.608.000 bytes/giây

Ở tần số SPI 40 MHz, mỗi byte truyền ~0.2 µs → 30 ms/frame chỉ để truyền data. CPU bận toàn bộ thời gian đó, không làm được gì khác.

DMA (Direct Memory Access) giải quyết vấn đề này: một hardware controller chuyên dụng đảm nhận việc chuyển data từ bộ nhớ → SPI bus, trong khi CPU tự do làm việc khác.

DMA Là Gì?

DMA là hardware engine tích hợp trong SoC, có khả năng:

  • Copy data từ RAM → peripheral (SPI TX, I2S TX, UART TX)
  • Copy data từ peripheral → RAM (SPI RX, I2S RX, camera)
  • Không cần CPU can thiệp trong quá trình transfer

Trên ESP32-S3, DMA controller hỗ trợ:

  • SPI2 và SPI3: LCD, SD card, display modules
  • I2S0 và I2S1: camera (CSI-style qua I2S), audio
  • UART: DMA UART để không block CPU
  • ADC: DMA ADC cho sampling liên tục
Không DMA:    CPU ─── loop ghi từng byte ──→ SPI → LCD
Với DMA:      CPU ─── setup descriptor ──→ DMA controller ─── transfer ──→ SPI → LCD
              CPU tự do làm việc khác trong khi DMA chạy

SPI DMA Cho LCD

Thư viện ArduinoGFXTFTeSPI đều hỗ trợ DMA. Đây là cách dùng Arduino_GFX với DMA:

#include <Arduino_GFX_Library.h>

// Cấu hình SPI DMA cho ST7789 240×240
Arduino_DataBus* bus = create_default_Arduino_HWSPI(
    TFT_CS,   // Chip Select
    TFT_DC    // Data/Command
);
// Với DMA: dùng Arduino_ESP32SPI thay vì Arduino_HWSPI
// Arduino_DataBus* bus = new Arduino_ESP32SPI(TFT_DC, TFT_CS, SCK, MOSI, MISO,
//                                             VSPI, true);  // true = enable DMA

Arduino_GFX* gfx = new Arduino_ST7789(bus,
    TFT_RST,          // Reset pin
    0,                // Rotation
    true,             // IPS panel
    240, 240          // Width, height
);

void setup() {
    gfx->begin(40000000);  // 40 MHz SPI clock
}

void loop() {
    // DMA tự xử lý việc đẩy pixel — CPU không bị block
    gfx->fillScreen(BLACK);
    gfx->drawString("Hello ESP32-S3!", 10, 10, 2);
}

Framebuffer Approach

Cách hiệu quả hơn: render vào buffer PSRAM, sau đó DMA copy buffer lên LCD:

// Framebuffer trong PSRAM (320×240×2 = 150 KB)
uint16_t* framebuffer = (uint16_t*)ps_malloc(320 * 240 * 2);

void renderFrame() {
    // Render tất cả mọi thứ vào framebuffer trước
    for (int y = 0; y < 240; y++) {
        for (int x = 0; x < 320; x++) {
            framebuffer[y * 320 + x] = calculatePixelColor(x, y);
        }
    }
    // Rồi mới push lên LCD một lần qua DMA — nhanh, không bị tear
    gfx->draw16bitRGBBitmap(0, 0, framebuffer, 320, 240);
}

Double buffering (2 framebuffer) hoàn toàn loại bỏ screen tearing — trong khi DMA đang đẩy buffer A lên LCD, CPU render vào buffer B.

I2S DMA Cho Camera

ESP32-S3 hỗ trợ camera qua I2S parallel interface (8-bit hoặc 16-bit data bus). Thư viện esp32-camera từ Espressif sử dụng I2S DMA để capture frame không tốn CPU:

#include "esp_camera.h"

// Cấu hình camera (ESP32-S3-EYE hoặc board tương tự)
camera_config_t config = {
    .pin_pwdn     = -1,
    .pin_reset    = -1,
    .pin_xclk     = 15,
    .pin_sccb_sda = 4,
    .pin_sccb_scl = 5,

    // Data bus 8-bit
    .pin_d7 = 16, .pin_d6 = 17, .pin_d5 = 18, .pin_d4 = 12,
    .pin_d3 = 10, .pin_d2 = 8,  .pin_d1 = 9,  .pin_d0 = 11,

    .pin_vsync = 6,
    .pin_href  = 7,
    .pin_pclk  = 13,

    .xclk_freq_hz = 20000000,  // 20 MHz XCLK
    .ledc_timer   = LEDC_TIMER_0,
    .ledc_channel = LEDC_CHANNEL_0,

    .pixel_format = PIXFORMAT_JPEG,  // JPEG để tiết kiệm bandwidth
    .frame_size   = FRAMESIZE_VGA,   // 640×480

    // PSRAM cho frame buffer
    .jpeg_quality = 12,   // 0-63, thấp hơn = chất lượng cao hơn
    .fb_count     = 2,    // 2 frame buffer — double buffering
    .fb_location  = CAMERA_FB_IN_PSRAM,  // Frame buffer trong PSRAM
    .grab_mode    = CAMERA_GRAB_WHEN_EMPTY,
};

void setup() {
    esp_err_t err = esp_camera_init(&config);
    if (err != ESP_OK) {
        Serial.printf("Camera init failed: 0x%x\n", err);
        return;
    }
}

void captureAndProcess() {
    // Lấy frame — DMA đã capture xong, CPU chỉ cần lấy pointer
    camera_fb_t* fb = esp_camera_fb_get();
    if (!fb) {
        Serial.println("Frame capture failed");
        return;
    }

    Serial.printf("Frame: %zu bytes, %dx%d\n",
        fb->len, fb->width, fb->height);

    // Xử lý frame (gửi qua HTTP, phân tích AI, v.v.)
    processFrame(fb->buf, fb->len);

    // Trả frame buffer lại để DMA dùng lần tiếp
    esp_camera_fb_return(fb);
}

Pipeline Camera → LCD: Hiển Thị Real-Time

Pattern phổ biến: Camera capture → DMA → PSRAM buffer → DMA → LCD

// Task camera: chạy trên Core 0
void cameraTask(void* p) {
    while (true) {
        camera_fb_t* fb = esp_camera_fb_get();
        if (fb) {
            // Gửi pointer frame qua Queue (không copy data!)
            xQueueSend(frameQueue, &fb, pdMS_TO_TICKS(10));
        }
    }
}

// Task display: chạy trên Core 1
void displayTask(void* p) {
    camera_fb_t* fb;
    while (true) {
        if (xQueueReceive(frameQueue, &fb, pdMS_TO_TICKS(1000))) {
            // Convert JPEG → RGB và hiển thị
            // (dùng esp_jpg_decode hoặc thư viện tương tự)
            displayJpegFrame(fb->buf, fb->len);
            esp_camera_fb_return(fb);  // QUAN TRỌNG: trả buffer!
        }
    }
}

Chú ý: truyền pointer qua Queue, không copy frame data (hàng trăm KB) — tốn thời gian và bộ nhớ không cần thiết.

I2S DMA Cho Audio

ESP32-S3 có 2 I2S controller, cả 2 đều hỗ trợ DMA. Đây là cách capture audio từ microphone I2S MEMS:

#include <driver/i2s_std.h>

i2s_chan_handle_t rx_handle;

void setupMicI2S() {
    i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(
        I2S_NUM_0, I2S_ROLE_MASTER);
    i2s_new_channel(&chan_cfg, NULL, &rx_handle);

    i2s_std_config_t std_cfg = {
        .clk_cfg  = I2S_STD_CLK_DEFAULT_CONFIG(16000),  // 16 KHz sample rate
        .slot_cfg = I2S_STD_MSB_SLOT_DEFAULT_CONFIG(
            I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO),
        .gpio_cfg = {
            .mclk = I2S_GPIO_UNUSED,
            .bclk = GPIO_NUM_26,
            .ws   = GPIO_NUM_25,
            .dout = I2S_GPIO_UNUSED,
            .din  = GPIO_NUM_22,
        },
    };

    i2s_channel_init_std_mode(rx_handle, &std_cfg);
    i2s_channel_enable(rx_handle);
}

void captureAudio() {
    int16_t samples[512];
    size_t bytesRead;

    // DMA đã capture sẵn — CPU chỉ đọc từ buffer
    i2s_channel_read(rx_handle, samples, sizeof(samples), &bytesRead, pdMS_TO_TICKS(100));

    // Xử lý audio: FFT, VAD, wake word detection, v.v.
    processAudioSamples(samples, bytesRead / sizeof(int16_t));
}

Tổng Kết

Giao TiếpDMA SupportDùng ChoLưu Ý
SPI (VSPI/HSPI)LCD, SD cardBuffer phải DMA-capable
I2S parallelCamera OV2640/OV5640Cần 8 data pins
I2S serialMicrophone, DAC audio3 pins (BCLK, WS, DATA)
UARTHigh-speed serialÍt dùng DMA
I2CKhôngSensor, LCD nhỏKhông DMA, dùng interrupt

Bài tiếp theo: Bài 7 — USB OTG Trên ESP32-S3: Device, Host, CDC, HID và Ứng Dụng Thực Tế — ESP32-S3 làm USB device (CDC, HID) và USB host, phân biệt USB native vs USB-UART.