IoTLabs

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

Series ESP32-S3 Dual-Core – Bài 9: Project Camera + LCD + Dual-Core

Mục Tiêu Project

Xây dựng một camera viewer hiển thị live feed từ OV2640 lên màn hình LCD ST7789 (240×240), dùng kiến trúc Dual-Core FreeRTOS và PSRAM framebuffer. Project này áp dụng tất cả kiến thức từ các bài trước: DMA, PSRAM, Queue, Dual-Core, Memory management.

Phần cứng:

  • ESP32-S3-DevKitC-1 (8 MB PSRAM, 16 MB Flash)
  • Camera OV2640 (module với connector 24-pin)
  • LCD ST7789 240×240 SPI (ví dụ: 1.3 inch round display)
  • Breadboard + dây jumper

Kiến Trúc Phần Mềm

Core 0 (PRO_CPU):
├── WiFi stack (tự động)
├── cameraTask: capture frame → PSRAM → frameQueue
└── networkTask: stream JPEG qua HTTP (tuỳ chọn)

Core 1 (APP_CPU):
├── displayTask: nhận frame từ queue → DMA SPI → LCD
└── statsTask: in FPS, memory mỗi 5 giây

Shared:
├── frameQueue: QueueHandle_t, chứa pointer frame
├── displayMutex: bảo vệ SPI bus
└── PSRAM: frame buffer pool (double buffering)

Sơ Đồ Kết Nối

Camera OV2640 → ESP32-S3

Camera PinESP32-S3 GPIOGhi Chú
PWDN-1Không dùng
RESET-1Không dùng
XCLKGPIO1520 MHz clock
SIOD (SDA)GPIO4I2C data
SIOC (SCL)GPIO5I2C clock
D7–D0GPIO16,17,18,12,10,8,9,11Parallel data
VSYNCGPIO6Frame sync
HREFGPIO7Line sync
PCLKGPIO13Pixel clock

LCD ST7789 → ESP32-S3 (SPI)

LCD PinESP32-S3 GPIOGhi Chú
SCKGPIO36SPI Clock
MOSIGPIO35SPI Data
CSGPIO34Chip Select
DCGPIO33Data/Command
RSTGPIO37Reset
BLGPIO38Backlight

Code Đầy Đủ

#include <Arduino.h>
#include "esp_camera.h"
#include <Arduino_GFX_Library.h>
#include <JPEGDecoder.h>

// ── Cấu Hình LCD ─────────────────────────────────────────────────────────────

#define LCD_SCK   36
#define LCD_MOSI  35
#define LCD_CS    34
#define LCD_DC    33
#define LCD_RST   37
#define LCD_BL    38
#define LCD_W     240
#define LCD_H     240

Arduino_DataBus* bus = new Arduino_ESP32SPI(LCD_DC, LCD_CS, LCD_SCK, LCD_MOSI,
                                             GFX_NOT_DEFINED, VSPI, true);  // DMA
Arduino_GFX* gfx = new Arduino_ST7789(bus, LCD_RST, 0, true, LCD_W, LCD_H);

// ── Camera Config ─────────────────────────────────────────────────────────────

camera_config_t cam_config = {
    .pin_pwdn     = -1,     .pin_reset = -1,
    .pin_xclk     = 15,
    .pin_sccb_sda = 4,      .pin_sccb_scl = 5,
    .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,
    .ledc_timer    = LEDC_TIMER_0,
    .ledc_channel  = LEDC_CHANNEL_0,
    .pixel_format  = PIXFORMAT_JPEG,
    .frame_size    = FRAMESIZE_240X240,  // 240×240 khớp với LCD
    .jpeg_quality  = 15,                 // 0-63: thấp hơn = tốt hơn
    .fb_count      = 2,                  // Double buffering
    .fb_location   = CAMERA_FB_IN_PSRAM, // Frame buffer trong PSRAM
    .grab_mode     = CAMERA_GRAB_WHEN_EMPTY,
};

// ── Queue và Stats ────────────────────────────────────────────────────────────

QueueHandle_t frameQueue;
volatile uint32_t captureCount = 0;
volatile uint32_t displayCount = 0;

// ── Task: Camera Capture (Core 0) ─────────────────────────────────────────────

void cameraTask(void* p) {
    camera_fb_t* fb;
    camera_fb_t* prevFb = nullptr;

    while (true) {
        fb = esp_camera_fb_get();
        if (!fb) {
            vTaskDelay(pdMS_TO_TICKS(10));
            continue;
        }

        // Gửi frame vào queue, không chờ nếu queue đầy (drop frame)
        if (xQueueSend(frameQueue, &fb, 0) != pdPASS) {
            // Queue đầy → display task chậm hơn capture → drop frame
            esp_camera_fb_return(fb);
        } else {
            captureCount++;
        }

        vTaskDelay(pdMS_TO_TICKS(1));  // Nhường CPU một chút
    }
}

// ── Task: LCD Display (Core 1) ────────────────────────────────────────────────

// Buffer JPEG decode trong PSRAM
uint16_t* jpegDecodeBuffer = nullptr;

void displayTask(void* p) {
    // Cấp phát decode buffer 1 lần
    jpegDecodeBuffer = (uint16_t*)ps_malloc(LCD_W * LCD_H * 2);
    if (!jpegDecodeBuffer) {
        Serial.println("FATAL: Không đủ PSRAM cho decode buffer!");
        vTaskDelete(NULL);
        return;
    }

    camera_fb_t* fb;
    while (true) {
        // Chờ frame từ camera task
        if (xQueueReceive(frameQueue, &fb, pdMS_TO_TICKS(1000)) == pdPASS) {
            // Decode JPEG → RGB565
            if (JpegDec.decodeArray(fb->buf, fb->len)) {
                uint16_t* pImg = jpegDecodeBuffer;
                uint32_t pixelCount = 0;

                while (JpegDec.read()) {
                    uint16_t* pSrc = JpegDec.pImage;
                    for (int i = 0; i < JpegDec.MCUWidth * JpegDec.MCUHeight; i++) {
                        if (pixelCount < (uint32_t)(LCD_W * LCD_H)) {
                            // Convert RGB888 → RGB565
                            uint16_t pixel = *pSrc++;
                            pImg[pixelCount++] = ((pixel & 0xF800) >> 0) |
                                                  ((pixel & 0x07E0) << 0) |
                                                  ((pixel & 0x001F) << 0);
                        }
                    }
                }

                // Đẩy toàn bộ framebuffer lên LCD qua DMA SPI
                gfx->draw16bitRGBBitmap(0, 0, jpegDecodeBuffer, LCD_W, LCD_H);
                displayCount++;
            }

            // Trả frame buffer cho camera
            esp_camera_fb_return(fb);
        }
    }
}

// ── Task: Stats mỗi 5 giây (Core 1, priority thấp) ──────────────────────────

void statsTask(void* p) {
    uint32_t lastCapture = 0, lastDisplay = 0;
    uint32_t lastTime = millis();

    while (true) {
        vTaskDelay(pdMS_TO_TICKS(5000));

        uint32_t now     = millis();
        uint32_t elapsed = (now - lastTime) / 1000;

        float captureFPS = (float)(captureCount - lastCapture) / elapsed;
        float displayFPS = (float)(displayCount - lastDisplay) / elapsed;

        Serial.printf("[Stats] Capture: %.1f fps | Display: %.1f fps\n",
            captureFPS, displayFPS);
        Serial.printf("[Mem] Heap: %lu KB | PSRAM: %lu KB\n",
            ESP.getFreeHeap() / 1024,
            ESP.getFreePsram() / 1024);

        lastCapture = captureCount;
        lastDisplay = displayCount;
        lastTime    = now;
    }
}

// ── Setup ─────────────────────────────────────────────────────────────────────

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

    // Kiểm tra PSRAM
    if (!psramFound()) {
        Serial.println("FATAL: Không có PSRAM! Cần board có PSRAM.");
        while (1) delay(1000);
    }
    Serial.printf("PSRAM: %lu MB\n", ESP.getPsramSize() / (1024 * 1024));

    // Khởi tạo LCD
    pinMode(LCD_BL, OUTPUT);
    digitalWrite(LCD_BL, HIGH);
    gfx->begin(40000000);  // 40 MHz SPI
    gfx->fillScreen(BLACK);
    gfx->setTextColor(WHITE);
    gfx->setTextSize(2);
    gfx->setCursor(20, 100);
    gfx->print("Initializing...");

    // Khởi tạo camera
    esp_err_t err = esp_camera_init(&cam_config);
    if (err != ESP_OK) {
        Serial.printf("Camera init failed: 0x%x\n", err);
        gfx->fillScreen(RED);
        gfx->setCursor(10, 100);
        gfx->printf("Camera fail: %x", err);
        while (1) delay(1000);
    }

    // Điều chỉnh cài đặt camera
    sensor_t* s = esp_camera_sensor_get();
    s->set_vflip(s, 1);       // Lật dọc nếu ảnh ngược
    s->set_hmirror(s, 0);     // Mirror ngang
    s->set_quality(s, 15);    // JPEG quality

    // Queue chứa tối đa 2 frame pointer
    frameQueue = xQueueCreate(2, sizeof(camera_fb_t*));

    // Tạo tasks
    xTaskCreatePinnedToCore(cameraTask,  "Camera",  4096,  NULL, 4, NULL, 0);
    xTaskCreatePinnedToCore(displayTask, "Display", 8192,  NULL, 3, NULL, 1);
    xTaskCreatePinnedToCore(statsTask,   "Stats",   2048,  NULL, 1, NULL, 1);

    Serial.println("Setup complete!");
}

void loop() {
    vTaskDelay(portMAX_DELAY);
}

Kết Quả và Tối Ưu

Với cấu hình trên, bạn sẽ đạt được khoảng:

  • Capture: 15–20 fps (JPEG 240×240 quality 15)
  • Display: 12–18 fps (giới hạn bởi JPEG decode time)
  • Heap free: ~180 KB (hầu hết PSRAM)

Tăng FPS:

  • Tăng JPEG quality (số nhỏ hơn) → file nhỏ hơn → decode nhanh hơn
  • Dùng FRAMESIZE_QVGA (320×240) thay vì 240×240 nếu LCD hỗ trợ
  • Thử thư viện JPEG decode nhanh hơn (libjpeg-turbo port)

Tổng Kết

Thành PhầnVai TròCore
cameraTaskI2S DMA capture → frameQueue0
displayTaskJPEG decode → SPI DMA → LCD1
statsTaskMonitor FPS và memory1
PSRAMFrame buffer + decode buffer
frameQueueTách producer và consumer

Bài tiếp theo: Bài 10 — Firmware IoT Production Cho ESP32-S3: OTA, Health Monitoring và State Machine — Thiết kế firmware production-ready: OTA update, watchdog, error recovery, logging và device state.