ESP32-CAM 與 OV2640 攝影機
ESP32-CAM 是 AI-Thinker 推出的低成本攝影機模組,內建 ESP32 晶片與 OV2640 200 萬像素 CMOS 感測器,支援 Wi-Fi 和藍牙,廣泛應用於視覺 IoT、人臉辨識、遠端監控與影像串流。本文將深入探討 OV2640 的硬體介面(DVP + SCCB)、JPEG 編碼管線、ESP32 的 Camera Driver 程式設計,以及常見問題排查。

OV2640 與 DVP 並列介面
OV2640 是 OmniVision 生產的 1/4 吋 CMOS 影像感測器,最大解析度 1632x1232(UXGA),支援自動曝光控制(AEC)、自動白平衡(AWB)、自動增益控制(AGC)與內建 JPEG 壓縮引擎。
與 ESP32 的連接採用 DVP(Digital Video Port)並列介面:
| 訊號 | 方向 | 說明 |
|---|---|---|
| PCLK | OV2640 → ESP32 | 像素時鐘,通常 12~24 MHz |
| VSYNC | OV2640 → ESP32 | 垂直同步(LOW 表示幀開始) |
| HREF | OV2640 → ESP32 | 水平參考(HIGH 表示一行資料有效) |
| D[7:0] | OV2640 → ESP32 | 8 位元像素資料匯流排 |
| XCLK | ESP32 → OV2640 | 主時鐘(ESP32 提供,常見 10~20 MHz) |
| SIOC | ESP32 → OV2640 | SCCB 時鐘(類似 I2C SCL) |
| SIOD | 雙向 | SCCB 資料(類似 I2C SDA) |
DVP 時序解析:
- VSYNC 從 HIGH 變 LOW 表示一幀開始(某些配置為正邏輯,視暫存器設定)
- HREF 為 HIGH 時,D[7:0] 上每 PCLK 週期輸出一個像素
- VGA(640x480)一行有 640 個像素,HREF 在 PCLK 下維持 640 個週期
- 一幀有 480 行(加上 blanking 約 510 行),VSYNC 在行空白期間觸發
- PCLK 頻率由 XCLK 和 OV2640 內部 PLL 決定:PCLK = XCLK x PLL_mult / PLL_div
OV2640 SCCB 暫存器配置
OV2640 使用 SCCB(Serial Camera Control Bus)協定進行暫存器存取,與標準 I2C 高度相容但略有差異:
- 寫入:Start + 裝置位址 0x42 (W) + 暫存器位址 + 資料 + Stop
- 讀取:Start + 0x42 (W) + 暫存器位址 + Restart + 0x43 (R) + 資料 + Stop
- 注意:SCCB 使用 Restart 條件,不是 I2C 的 Repeated Start(雖然多數實作相容)
- 預設位址:0x60(7-bit)/ 0x30(8-bit, OV2640 使用)
關鍵暫存器速查
| 暫存器 | 位址 | 預設值 | 功能 |
|---|---|---|---|
| PID | 0x0A | 0x26 | Product ID Number(辨識 OV2640) |
| VER | 0x0B | 0x42 | Revision Number |
| CLKRC | 0x08 | 0x80 | 內部時鐘分頻 |
| COM3 | 0x0C | 0x00 | JPEG 輸出啟用(Bit 5: SWAP_YUV) |
| COM7 | 0x12 | 0x00 | 格式設定:Bit 6=1 RAW, Bit 5=1 RGB, Bit 4=1 YUV, Bit 3=1 JPEG |
| COM4 | 0x0D | 0x40 | PLL 倍頻(Bit 1: PLL enable) |
| DM_LNL | 0x2D | 0x00 | DVP 驅動強度調整 |
| AEC | 0x10 | 0x00 | 曝光時間低位元組 |
| AGC | 0x00 | 0x04 | 增益控制高位元組 |
JPEG 編碼管線
OV2640 最大的優勢是 內建硬體 JPEG 編碼器,不需佔用 ESP32 的 CPU 資源進行壓縮。
管線流程:
- Bayer RAW10 數據從感測器陣列讀出
- 透過 ISP 管線(Bayer interpolation、AWB、AEC、AGC、CSC)
- 以 YUV422 格式送入 JPEG 編碼器
- JPEG 編碼器以 8x8 區塊進行 DCT + Huffman 壓縮
- 壓縮後的 JPEG 資料存入 OV2640 內部 FIFO
- ESP32 透過 SPI 或 DVP + FIFO_RD 訊號讀取 JPEG 資料
- 讀取的資料可存入 SD 卡、透過 Wi-Fi 串流出去,或直接傳送到瀏覽器

ESP32 Camera Driver 程式設計
Arduino IDE 設定(ESP32-CAM)
首先在 Arduino IDE 的 "開發板管理員" 安裝 ESP32 支援套件,然後選取 "AI Thinker ESP32-CAM" 開發板。
基礎程式:攝影與 Wi-Fi 串流
// ESP32-CAM Wi-Fi 影像串流伺服器
#include <WiFi.h>
#include "esp_camera.h"
#include "esp_timer.h"
// Wi-Fi 設定
const char* ssid = "你的WiFi名稱";
const char* password = "你的WiFi密碼";
// AI-Thinker ESP32-CAM 腳位定義
#define PWDN_GPIO_NUM 32
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 0
#define SIOD_GPIO_NUM 26
#define SIOC_GPIO_NUM 27
#define Y9_GPIO_NUM 35
#define Y8_GPIO_NUM 34
#define Y7_GPIO_NUM 39
#define Y6_GPIO_NUM 36
#define Y5_GPIO_NUM 21
#define Y4_GPIO_NUM 19
#define Y3_GPIO_NUM 18
#define Y2_GPIO_NUM 5
#define VSYNC_GPIO_NUM 25
#define HREF_GPIO_NUM 23
#define PCLK_GPIO_NUM 22
// 簡易 HTTP 伺服器
WiFiServer server(80);
unsigned long lastFrame = 0;
bool streamActive = false;
void startCameraServer();
void handleCameraClient();
frame_size_t currentResolution = FRAMESIZE_VGA; // 640x480
pixformat_t currentFormat = PIXFORMAT_JPEG; // JPEG 輸出
void setup() {
Serial.begin(115200);
Serial.println("ESP32-CAM 啟動中...");
// 設定攝影機參數
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
config.pin_sscb_sda = SIOD_GPIO_NUM;
config.pin_sscb_scl = SIOC_GPIO_NUM;
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
config.xclk_freq_hz = 20000000; // XCLK = 20 MHz
config.pixel_format = currentFormat;
config.frame_size = currentResolution;
config.jpeg_quality = 12; // JPEG 品質 0~63(越小越好)
config.fb_count = 1; // 單一 frame buffer
// 初始化攝影機
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK) {
Serial.printf("攝影機初始化失敗: 0x%x
", err);
return;
}
感測器參數調校
// 取得感測器控制物件
sensor_t *s = esp_camera_sensor_get();
// 常見調校參數
s->set_brightness(s, 0); // 亮度: -2~2
s->set_contrast(s, 0); // 對比度: -2~2
s->set_saturation(s, 0); // 飽和度: -2~2
s->set_special_effect(s, 0);// 特效: 0~6
s->set_whitebal(s, 1); // 自動白平衡
s->set_awb_gain(s, 1); // AWB 增益
s->set_wb_mode(s, 0); // 白平衡模式: 0~4
s->set_exposure_ctrl(s, 1); // 自動曝光控制
s->set_aec2(s, 0); // AEC2 啟用
s->set_ae_level(s, 0); // AE 補償: -2~2
s->set_aec_value(s, 300); // 手動曝光時間: 0~1200
s->set_gain_ctrl(s, 1); // 自動增益控制
s->set_agc_gain(s, 0); // AGC 增益: 0~30
s->set_gainceiling(s, 0); // 增益上限: 0~6
s->set_bpc(s, 0); // Black Pixel Correction
s->set_wpc(s, 1); // White Pixel Correction
s->set_raw_gma(s, 1); // 伽瑪校正
s->set_lenc(s, 1); // 鏡頭像差校正
s->set_hmirror(s, 0); // 水平翻轉
s->set_vflip(s, 0); // 垂直翻轉
s->set_dcw(s, 1); // Downsize EN
Serial.println("感測器初始化完成");
拍攝靜態照片並透過序列埠輸出
// ESP32-CAM 拍攝照片範例
void capturePhoto() {
// 擷取一張照片
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
Serial.println("擷取失敗!");
return;
}
Serial.printf("照片尺寸: %d x %d, 格式: %d, 大小: %zu bytes
",
fb->width, fb->height, fb->format, fb->len);
// JPEG 二進位資料在 fb->buf, 長度在 fb->len
// 範例:計算平均亮度(僅 JPEG 頭部粗略估算)
uint32_t sum = 0;
for (size_t i = 0; i < min(fb->len, (size_t)1000); i++) {
sum += fb->buf[i];
}
Serial.printf("平均亮度 (前 1KB): %d
", sum / min(fb->len, (size_t)1000));
// 歸還 frame buffer(必須!)
esp_camera_fb_return(fb);
}
MJPEG 串流伺服器
// ESP32-CAM簡易 MJPEG 串流
void streamJPEG(WiFiClient &client) {
String header = "HTTP/1.1 200 OK
";
header += "Content-Type: multipart/x-mixed-replace; boundary=frame
";
header += "Cache-Control: no-cache
";
header += "Connection: close
";
client.print(header);
while (client.connected()) {
camera_fb_t *fb = esp_camera_fb_get();
if (!fb) {
Serial.println("取得 frame 失敗");
break;
}
client.printf("--frame
");
client.printf("Content-Type: image/jpeg
");
client.printf("Content-Length: %zu
", fb->len);
client.write(fb->buf, fb->len);
client.printf("
");
esp_camera_fb_return(fb);
}
}
動態切換解析度
// 動態改變攝影機解析度
bool setResolution(framesize_t newSize) {
sensor_t *s = esp_camera_sensor_get();
if (s == NULL) return false;
// 暫停串流後切換
int ret = s->set_framesize(s, newSize);
if (ret == 0) {
Serial.printf("解析度切換成功: %d
", newSize);
return true;
}
Serial.printf("解析度切換失敗: %d
", ret);
return false;
}
// 支援的解析度列舉
// FRAMESIZE_96X96, FRAMESIZE_QQVGA (160x120),
// FRAMESIZE_QVGA (320x240), FRAMESIZE_VGA (640x480),
// FRAMESIZE_SVGA (800x600), FRAMESIZE_XGA (1024x768),
// FRAMESIZE_SXGA (1280x1024), FRAMESIZE_UXGA (1600x1200)
常見問題與排查
| 問題 | 原因 | 解法 |
|---|---|---|
| 攝影機初始化失敗 (0x101) | PSRAM 未啟用 | Tools → Partition Scheme → Huge APP (3MB NO OTA / 1MB SPIFFS) |
| 畫面全黑 | PWDN 腳位未正確設置 | 確認 PWDN_GPIO_NUM = 32 且為 LOW |
| 影像雜訊/條紋 | 電源供應不足 | ESP32-CAM 需要 5V / 2A 以上電源,勿用 USB 線供電 |
| FPS 過低 | JPEG 品質太高或 XCLK 太低 | 降低 xclk_freq_hz 至 10MHz 或提高 jpeg_quality 值 |
| Wi-Fi 連線後崩潰 | 記憶體不足 | 降低解析度至 VGA,關閉 OTA,使用 PSRAM |
| 顏色偏綠/偏藍 | 白平衡設定不當 | 啟用 AWB 或手動設定 wb_mode |
| 影像過曝/過暗 | 曝光或增益設定不當 | 調整 aec_value 或 ae_level |
| 無法辨識 OV2640 PID | SCCB 時序不匹配 | 確認 SIOC/SIOD 上拉電阻,降低 SCCB 頻率 |
| ESP32-CAM 過熱 | 長時間運轉無散熱 | 添加散熱片,降低解析度,或啟用睡眠模式 |
ESP-IDF vs Arduino
ESP32 Camera Driver 底層使用 ESP-IDF 框架,Arduino 的 esp_camera.h 是對 ESP-IDF driver 的封裝。如果需要更深度的控制,可以直接使用 ESP-IDF:
// ESP-IDF 直接操作 OV2640 SCCB 暫存器(不透過 Camera Driver)
#include "driver/i2c.h"
#include "soc/i2c_reg.h"
#include "hal/i2c_hal.h"
#define OV2640_ADDR 0x30 // SCCB 7-bit 地址左移一位
#define I2C_MASTER_NUM I2C_NUM_0
esp_err_t ov2640_write_reg(uint8_t reg, uint8_t val) {
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, OV2640_ADDR | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, reg, true);
i2c_master_write_byte(cmd, val, true);
i2c_master_stop(cmd);
esp_err_t ret = i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, 1000 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
return ret;
}
uint8_t ov2640_read_reg(uint8_t reg) {
uint8_t val = 0;
i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd);
i2c_master_write_byte(cmd, OV2640_ADDR | I2C_MASTER_WRITE, true);
i2c_master_write_byte(cmd, reg, true);
i2c_master_start(cmd); // SCCB Restart!
i2c_master_write_byte(cmd, OV2640_ADDR | I2C_MASTER_READ, true);
i2c_master_read_byte(cmd, &val, I2C_MASTER_NACK);
i2c_master_stop(cmd);
i2c_master_cmd_begin(I2C_MASTER_NUM, cmd, 1000 / portTICK_PERIOD_MS);
i2c_cmd_link_delete(cmd);
return val;
}
// 驗證感測器
void check_ov2640() {
uint8_t pid = ov2640_read_reg(0x0A); // Product ID
uint8_t ver = ov2640_read_reg(0x0B); // Revision
printf("OV2640 PID=0x%02X VER=0x%02X
", pid, ver);
// PID 應為 0x26 (OV2640)
}
投影機應用:ESP32-CAM 低功耗定時拍照
// ESP32-CAM 低功耗定時拍照 + Deep Sleep
// 每 30 分鐘拍照一次,發送 HTTP POST 到伺服器
#include <WiFi.h>
#include "esp_camera.h"
#include "esp_sleep.h"
#define uS_TO_S_FACTOR 1000000ULL
#define TIME_TO_SLEEP 1800 // 30 分鐘
void setup() {
Serial.begin(115200);
esp_sleep_enable_timer_wakeup(TIME_TO_SLEEP * uS_TO_S_FACTOR);
// 初始化攝影機(同上)
// ...
// 連線 Wi-Fi
WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
// 拍照
camera_fb_t *fb = esp_camera_fb_get();
if (fb) {
// 上傳照片到伺服器
WiFiClient client;
// HTTP POST ...
esp_camera_fb_return(fb);
}
// 關閉 Wi-Fi 進入深睡
WiFi.disconnect(true);
esp_deep_sleep_start();
}
void loop() {} // 永不執行
總結
ESP32-CAM 搭配 OV2640 是建構視覺 IoT 最經濟實惠的方案之一。透過 DVP 並列介面和內建 JPEG 編碼器,可在 ESP32 上實現即時 MJPEG 串流、定時拍照上傳、人臉辨識等應用。
選型參考:
- VGA 即時串流(30 FPS):OV2640 + ESP32-CAM + JPEG + Wi-Fi 串流
- 高解析度拍照(UXGA):OV2640 + SD 卡儲存 + 事後讀取
- 低功耗定時監控:OV2640 + ESP32 Deep Sleep + HTTP 上傳
- 人臉/物件辨識:OV2640 VGA + ESP32 + TensorFlow Lite(幀率約 1~3 FPS)
- 更高畫質(5MP+):換用 OV5640 或支援 MIPI CSI 的 ESP32-S3
文章評論