ESP32 驱动 OV7670 摄像头实现简易照相机系统
本文介绍了基于 ESP32 微控制器和 OV7670 摄像头模块构建简易照相机系统的完整方案。系统采用 QQVGA 分辨率,利用 WebSocket 协议实现网页端实时视频流传输与拍照功能。内容涵盖硬件接线、ESP32 固件代码解析、摄像头寄存器配置原理、图像数据时序处理及常见问题排查。重点解决了内存分配优化和实时视频流压缩传输问题,适合嵌入式开发与物联网应用学习。

本文介绍了基于 ESP32 微控制器和 OV7670 摄像头模块构建简易照相机系统的完整方案。系统采用 QQVGA 分辨率,利用 WebSocket 协议实现网页端实时视频流传输与拍照功能。内容涵盖硬件接线、ESP32 固件代码解析、摄像头寄存器配置原理、图像数据时序处理及常见问题排查。重点解决了内存分配优化和实时视频流压缩传输问题,适合嵌入式开发与物联网应用学习。

本项目基于 ESP32 开发板和 OV7670 摄像头模块,实现了一个功能完整的简易照相机系统。系统采用 QQVGA(160×120) 分辨率,RGB565 色彩格式,在保证图像质量的同时控制数据传输量,确保 ESP32 能够稳定处理。通过优化的 WebSocket 传输协议,实现了在网页端实时显示摄像头画面和拍照功能。
解决方案: 采用预分配内存策略,分配好图像传输缓冲区,避免运行时动态内存分配。
解决方案: 将每帧图像分成多行传输,减少单次数据传输量。
| 组件名称 | 规格型号 | 数量 | 备注 |
|---|---|---|---|
| 主控板 | ESP32 WROOM | 1 | 核心处理单元 |
| 摄像头模块 | OV7670 | 1 | 30 万像素,支持 RGB565 输出 |
| 连接线 | 杜邦线 | 若干 | 用于各模块间连接 |
| 电源 | USB 数据线 | 1 | 5V 供电 |
| 电阻 | 10kΩ | 2 | 用于 I2C 上拉 |
模块使用 3.3V 供电,OV7670 摄像头与 ESP32 的连接按照以下的引脚定义进行:
const camera_config_t cam_conf = {
.D0 = 36, // 数据位 0
.D1 = 39, // 数据位 1
.D2 = 34, // 数据位 2
.D3 = 35, // 数据位 3
.D4 = 32, // 数据位 4
.D5 = 33, // 数据位 5
.D6 = 25, // 数据位 6
.D7 = 26, // 数据位 7
.XCLK = 15, // 时钟信号
.PCLK = 14, // 像素时钟
.VSYNC = 13, // 垂直同步信号
.xclk_freq_hz = 10000000, // 10MHz 时钟
.ledc_timer = LEDC_TIMER_0,
.ledc_channel = LEDC_CHANNEL_0
};
注意:SCCB 通信还需要连接 I2C 总线,SDA 接 ESP32 的 GPIO21、SCL 接 GPIO22。RET 复位接 3.3V,PWDN 接 GND。

ps:模块上的 LDO 可以将 3.3V 转换为 2.8V 和 1.8V 供摄像头使用

// 摄像头配置结构体
const camera_config_t cam_conf = {
.D0 = 36, .D1 = 39, // ... 其他引脚配置
.xclk_freq_hz = 10000000, // 10MHz 时钟
.ledc_timer = LEDC_TIMER_0,
.ledc_channel = LEDC_CHANNEL_0
};
// 摄像头初始化
esp_err_t err = cam.init(&cam_conf, CAM_RES, RGB565);
if(err != ESP_OK){
Serial.println(F("cam.init ERROR"));
while(1); // 初始化失败时停止程序
}
cam_conf:摄像头配置结构体,包含引脚定义和时钟配置CAM_RES:分辨率设置,本项目使用 QQVGA(160x120)RGB565:色彩格式,每个像素占用 2 字节// 初始化图像传输头部
bool setImgHeader(uint16_t w, uint16_t h){
line_h = h;
line_size = w * 2; // RGB565 格式,每个像素 2 字节
data_size = 2 + line_size * h; // 行号 (2 字节) + 图像数据
// 分配内存
WScamData = (uint8_t*)malloc(data_size + 4); // +4 字节 WebSocket 头部
// 设置 WebSocket 帧头部
WScamData[0] = OP_BIN; // 二进制数据
WScamData[1] = 126; // 数据长度标识
WScamData[2] = (uint8_t)(data_size / 256); // 数据长度高字节
WScamData[3] = (uint8_t)(data_size % 256); // 数据长度低字节
return true;
}
// 发送图像数据
void WS_sendImg(uint16_t lineNo){
// 设置行号
WScamData[4] = (uint8_t)(lineNo % 256);
WScamData[5] = (uint8_t)(lineNo / 256);
// 发送数据
uint16_t len = data_size + 4;
uint8_t *pData = WScamData;
while(len){
uint16_t send_size = (len > UNIT_SIZE) ? UNIT_SIZE : len;
WSclient.(pData, send_size);
len -= send_size;
pData += send_size;
}
}
// 主循环中的图像采集
void loop(void){
uint16_t y, dy;
dy = CAM_HEIGHT / CAM_DIV; // 每次处理的行数
while(1){
for(y = 0; y < CAM_HEIGHT; y += dy){
// 获取 dy 行图像数据
cam.getLines(y+1, &WScamData[6], dy);
if(WS_on && !snapshotInProgress){
if(WSclient){
WS_sendImg(y); // 发送图像数据
}
}
}
if(!WS_on){
Ini_HTTP_Response(); // 处理 HTTP 请求
}
}
}
//*************************************************************************
// OV7670 (non FIFO) Simple Web streamer for ESP32
// Optimized version for QQVGA (160x120) resolution
// Added snapshot functionality with dropdown menu
//*************************************************************************
#include <Wire.h>
#include <SPI.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"
#include "esp_log.h"
#include <WiFi.h>
#include <WiFiMulti.h>
#include "hwcrypto/sha.h"
#include "base64.h"
#include <OV7670.h>
// Network configuration
IPAddress myIP = IPAddress(192, 168, 3, 78); // Static IP address
IPAddress myGateway = IPAddress(192, 168, 3, 1);
// Camera pin configuration
const camera_config_t cam_conf = {
.D0 = , .D1 = , .D2 = , .D3 = ,
.D4 = , .D5 = , .D6 = , .D7 = ,
.XCLK = , .PCLK = , .VSYNC = ,
.xclk_freq_hz = ,
.ledc_timer = LEDC_TIMER_0,
.ledc_channel = LEDC_CHANNEL_0
};
OV7670 cam;
;
WiFiClient WSclient;
boolean WS_on = ;
WiFiMulti wifiMulti;
*html_head =
;
*html_body =
;
*html_script =
;
*WScamData = ;
data_size = ;
line_size = ;
line_h = ;
snapshotRequested = ;
snapshotInProgress = ;
snapshotBuffer[CAM_WIDTH * CAM_HEIGHT];
snapshotIndex = ;
{
wifiMulti.(, );
Serial.(());
(wifiMulti.() == WL_CONNECTED) {
WiFi.(myIP, myGateway, (,,,));
Serial.(());
Serial.(());
Serial.(WiFi.());
Serial.(());
Serial.(WiFi.());
Serial.(());
Serial.(WiFi.());
Serial.(());
;
} ;
}
{
Serial.();
client.(html_head);
Serial.();
client.(html_body);
client.(WiFi.());
client.(());
Serial.();
client.(html_script);
Serial.();
}
{
hash[];
String str = h_req_key + ;
(SHA1, ( *)str.(), str.(), hash);
str = base64::(hash, );
str;
}
{
String req;
String hash_req_key;
Serial.(());
{
req = client.();
Serial.(req);
(req.() >= ){
hash_req_key = req.(req.(), req.());
Serial.();
Serial.(());
Serial.(hash_req_key);
}
}(req.() != );
();
Serial.(());
String str = ;
str += ;
str += ;
str += ;
str += (hash_req_key);
str += ;
Serial.(str);
client.(str);
WSclient = client;
}
{
(len >= && ((*)data, , ) == ) {
Serial.();
snapshotRequested = ;
}
}
{
(len < ) ;
opcode = data[] & ;
isMasked = (data[] & ) != ;
payloadLength = data[] & ;
maskIndex = ;
(payloadLength == ) {
(len < ) ;
payloadLength = (data[] << ) | data[];
maskIndex = ;
} (payloadLength == ) {
(len < ) ;
;
}
(isMasked) {
(len < maskIndex + ) ;
maskingKey[] = {data[maskIndex], data[maskIndex], data[maskIndex], data[maskIndex]};
maskIndex += ;
( i = ; i < payloadLength && i + maskIndex < len; i++) {
data[maskIndex + i] ^= maskingKey[i % ];
}
}
(opcode == OP_TEXT) {
(data + maskIndex, payloadLength);
}
}
{
(!WSclient.()) ;
wsBuffer[];
wsBufferIndex = ;
(WSclient.()) {
b = WSclient.();
wsBuffer[wsBufferIndex++] = b;
(wsBufferIndex >= (wsBuffer)) {
(wsBuffer, wsBufferIndex);
wsBufferIndex = ;
}
}
(wsBufferIndex > ) {
(wsBuffer, wsBufferIndex);
wsBufferIndex = ;
}
}
{
String req;
WiFiClient client = server.();
(!client) ;
(client.()){
(!client.()) ;
Serial.(());
req = client.();
(req.() != ){
(req.() != ){
req = client.();
Serial.(req);
(req.() != ){
Serial.(());
(client);
WS_on = ;
;
}
}
();
Serial.(());
(client);
Serial.(());
} {
Serial.(());
Serial.(req);
(client.()){ Serial.(client.()); }
}
(!WS_on){
();
client.();
();
Serial.(());
}
}
}
{
Serial.();
Serial.(());
Wire.();
Wire.();
WS_on = ;
(()){
server.();
}
Serial.(());
err = cam.(&cam_conf, CAM_RES, RGB565);
(err != ESP_OK){
Serial.(());
();
}
cam.();
Serial.(, cam.());
Serial.(, cam.());
Serial.(());
(!(CAM_WIDTH, CAM_HEIGHT / CAM_DIV)){
Serial.(());
();
}
}
{
y, dy;
dy = CAM_HEIGHT / CAM_DIV;
(){
(WS_on && WSclient){
();
}
(snapshotRequested){
snapshotRequested = ;
snapshotInProgress = ;
snapshotIndex = ;
Serial.();
(y = ; y < CAM_HEIGHT; y += dy){
cam.(y, (*)&snapshotBuffer[snapshotIndex], dy);
snapshotIndex += CAM_WIDTH * dy;
(WS_on && WSclient){
String progress = + (y * / CAM_HEIGHT) + ;
WSclient.(progress);
}
}
(WS_on && WSclient){
(y = ; y < CAM_HEIGHT; y += dy){
(y, &snapshotBuffer[y * CAM_WIDTH]);
}
WSclient.();
}
snapshotInProgress = ;
Serial.();
}
(y = ; y < CAM_HEIGHT; y += dy){
cam.(y, &WScamData[], dy);
(WS_on && !snapshotInProgress){
(WSclient){
(y);
} {
WSclient.();
WS_on = ;
Serial.(());
}
}
}
(!WS_on){
();
}
}
}
OV7670 摄像头需要通过 SCCB(Serial Camera Control Bus)接口配置内部寄存器才能正常工作。
{0x11, 0x80}, // CLKRC:内部时钟控制,使用外部时钟源
{0x6b, 0x40}, // PLL 控制寄存器,设置 PLL 倍频
寄存器中的【5:0】控制我们输入时钟分频,通过'寄存器特定位写值→按'值 + 1'算分频系数→输入时钟除以系数'的逻辑,实现对设备工作时钟的精准控制。
{0x12, 0x14}, // COM7:选择 QVGA 分辨率和 RGB 输出
{0x40, 0x10}, // COM15:RGB565 格式,全范围输出
{0x0C, 0x04}, // COM3:启用缩放功能
{0x3E, 0x19}, // COM14:缩放参数
本项目使用 QQVGA 分辨率,通过 bit4 选择 QVGA(320,240)作为基础分辨率,再结合 0x32、0x17~0x1A 等寄存器进一步裁剪画面尺寸至 160x120。
{0x55, 0x00}, // 亮度控制
{0x56, 0x60}, // 对比度控制
{0x57, 0x80}, // 对比度中心
{0x13, 0xE7}, // COM8:启用 AGC、AEC 和白平衡
{0x6F, 0x40}, // AWB 蓝色增益
{0x70, 0x40}, // AWB 红色增益
调整图像的亮度、对比度、白平衡等参数,优化图像质量。
一个 VS 周期(一帧)内,HS 的一个完整周期对应一行图像数据。
配置的图像分辨率是 160×120(宽 160 像素、高 120 行):
PCLK 像素时钟控制'单个字节数据'的读取,始终规律跳变,但仅在 HS 高电平(行传输期间)的信号有效。
PCLK 下降沿:OV7670 更新 D0D7 引脚的字节数据;PCLK 上升沿:单片机读取 D0D7 的字节数据。
static const struct regval_list qqvga_OV7670[] PROGMEM = {
{REG_COM3, COM3_DCWEN}, // Enable format scaling
{REG_COM14, COM14_DCWEN | COM14_MANUAL | COM14_PCLKDIV_4}, // divide by 4
{REG_SCALING_XSC, 0x3a}, // Horizontal scale factor
{REG_SCALING_YSC, 0x35}, // Vertical scale factor
{REG_SCALING_DCWCTR, SCALING_DCWCTR_VDS_by_4 | SCALING_DCWCTR_HDS_by_4}, // down sampling by 4
{REG_SCALING_PCLK_DIV, SCALING_PCLK_DIV_RSVD | SCALING_PCLK_DIV_4}, // DSP scale control Clock divide by 4
{REG_SCALING_PCLK_DELAY,0x02},
{0xff, 0xff} // END MARKER
};
在本项目中,获取 QQVGA 分辨率(160x120)是通过硬件配置实现的,直接配置 OV7670 摄像头的内部寄存器,使其直接输出目标分辨率:
串口打印输出摄像头设备号,cam MID = 7FA2、cam PID = 7673,说明初始化成功
拍照效果示例:点击拍摄并保存可以通过下方的下拉栏选中查看并删除照片

OV7670 摄像头实现简易照相机系统
展示实时视频流和拍照功能
A: 可能的原因是网络带宽不足或 ESP32 处理能力达到极限:
A: 这是白平衡或色彩矩阵配置不当导致的:
A: 可以尝试以下方法:

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML 转 Markdown 互为补充。 在线工具,Markdown 转 HTML在线工具,online