简介:这份资源围绕ESP32驱动OV7670摄像头展开,面向物联网与嵌入式开发的初学者及DIY爱好者,帮助解决从硬件连线到图像采集的入门难题。压缩包共55个文件,约7.74MB,包含8个h头文件、3个cpp源文件、1个ino主程序及若干o、d编译产物,另有bin固件、xml工程配置与png预览图,覆盖I2C、I2S、OV7670、XClk、DMABuffer等模块,结构完整便于直接编译调试。资源重点涉及引脚连接、寄存器初始化、像素格式与帧率配置、SPI/I2S数据传输以及Wi-Fi实时图传等关键知识点,并附带README说明与Arduino示例代码,可帮助读者快速理解ESP32与CMOS传感器的交互流程。目前已有5787人学习下载,适合作为智能视觉应用的实践起点。
1. 从一块二手 OV7670 说起:ESP32 到底能不能带得动它
手里攒了几块 OV7670 摄像头模块,又刚好有几片 ESP32 开发板,很多人第一反应就是把这俩凑一起做个图传或者拍照的小玩意。但真上手就会发现,事情没那么简单。OV7670 是并口输出的老式传感器,没有 FIFO 的版本需要主控提供稳定的 XCLK、PCLK、VSYNC、HREF 以及 8 位数据线,还要在像素时钟的节拍里把数据读走。ESP32 虽然有 I2S 外设可以模拟部分并口时序,但它的 GPIO 矩阵和中断延迟决定了这不是一个“接上线就能跑”的组合。网上能搜到的例程大多停留在“能读出花屏”或者“偶尔出一帧”的状态,真正稳定出图、能调参数、能换分辨率的完整方案并不多。这篇笔记围绕一份 ESP32 驱动 OV7670 的实战代码包展开,把接线、时钟配置、I2S 采集、SCCB 初始化、常见花屏和丢帧的排查路径都拆一遍。适合已经玩过 ESP32、想低成本搞视觉采集的开发者,也适合被并口时序折磨过、想看看别人怎么绕坑的人。
2. 硬件连线与时钟树:先把 XCLK 和 PCLK 的关系理清楚
2.1 为什么 OV7670 不能直接插在 ESP32 的 I2C 上
OV7670 有两套接口,一套是 SCCB,用来读写寄存器,电气特性跟 I2C 几乎一样,但 OV7670 的 SCCB 不响应重复起始条件,也不支持时钟拉伸,所以用 ESP32 的硬件 I2C 去驱动它,经常在扫描地址那一步就卡住。常见做法是用软件模拟 SCCB,把 SCL 和 SDA 当成普通 GPIO 来翻转,时序自己控。另一套是并口数据输出,8 位数据线加 PCLK、VSYNC、HREF,这组线才是图像数据的通道。很多人第一次接线只连了 SCCB,发现能读到厂商 ID,就以为通了,结果图像数据线没接对,读出来全是 0 或者 0xFF。所以第一步不是写代码,是把两组线分开理清楚:SCCB 负责配置,并口负责搬运。
代码包里通常会把 SCCB 的 GPIO 和并口的 GPIO 分开定义,方便换板子的时候改引脚。以常见的 ESP32-WROOM 为例,SCCB 可以用 GPIO21/22,并口数据线用 GPIO12 到 GPIO19 这一组,但要注意 GPIO12 到 GPIO15 是 strapping 引脚,上电时的电平会影响启动模式,如果 OV7670 的数据线在这些脚上输出高电平,可能导致 ESP32 进不了正常启动流程。血泪经验是,尽量把数据线避开 GPIO0、GPIO2、GPIO12、GPIO15,实在避不开就在传感器端加电平隔离或者上电顺序上做文章。
2.2 XCLK 从哪来,PCLK 又该怎么接
OV7670 需要外部给它一个主时钟,也就是 XCLK,典型值是 24MHz 或者 8MHz。ESP32 可以用 LEDC 或者 MCPWM 输出这个时钟,代码包里一般用 LEDC 的 24MHz 配置。XCLK 的频率直接决定了 PCLK 的上限,OV7670 内部会分频产生 PCLK,默认配置下 PCLK 大概是 XCLK 的一半或者四分之一。如果 XCLK 给 24MHz,PCLK 可能跑到 12MHz,这个速度对 ESP32 的 I2S 采样来说已经偏快,容易丢像素。所以很多稳定方案会把 XCLK 降到 8MHz 到 10MHz,牺牲帧率换稳定性。
接线的时候,XCLK 从 ESP32 的 LEDC 输出脚接到 OV7670 的 XCLK 输入脚,PCLK 从 OV7670 输出接到 ESP32 的一个支持 I2S 采样的 GPIO 上。VSYNC 和 HREF 也各占一个 GPIO。数据线 D0 到 D7 按顺序接到 ESP32 的八个 GPIO。这里有个容易翻车的点:PCLK 的极性。OV7670 默认是在 PCLK 上升沿输出数据,但有些模块因为走线或者电平转换,实际有效沿可能反了,导致读出来的像素错位。代码里一般会提供一个pclk_polarity的配置项,如果出图是斜的或者颜色错乱,先试着把这个值翻转一下。
// SCCB 初始化,软件模拟,SCL/SDA 可配 #define SCCB_SCL_GPIO 21 #define SCCB_SDA_GPIO 22 // 并口数据线,避开 strapping 引脚 #define CAM_D0_GPIO 18 #define CAM_D1_GPIO 19 #define CAM_D2_GPIO 23 #define CAM_D3_GPIO 25 #define CAM_D4_GPIO 26 #define CAM_D5_GPIO 27 #define CAM_D6_GPIO 32 #define CAM_D7_GPIO 33 // 同步信号 #define CAM_PCLK_GPIO 34 // 输入,只读引脚 #define CAM_VSYNC_GPIO 35 // 输入 #define CAM_HREF_GPIO 36 // 输入,部分板子需注意 36/39 无内部上拉 // XCLK 输出 #define CAM_XCLK_GPIO 4上面这段引脚定义是代码包里常见的结构,把 SCCB、数据、同步、时钟分开。参数说明:CAM_PCLK_GPIO必须选输入专用引脚或者普通 GPIO,但要注意 GPIO34 到 GPIO39 是只读的,不能做输出,拿来做 PCLK 输入没问题,但别拿来做 XCLK 输出。CAM_XCLK_GPIO要选支持 LEDC 输出的引脚,GPIO4 在多数 ESP32 模块上都可以。改板子的时候,先确认这些引脚在目标板上有没有被 Flash 或者 PSRAM 占用,尤其是 GPIO16 和 GPIO17 在带 PSRAM 的模块上通常不可用。
2.3 电源和去耦,别等花屏了才想起来
OV7670 模块对电源纹波比较敏感,尤其是模拟供电部分。如果直接从 ESP32 的 3.3V 引脚拉线,而 ESP32 又在跑 WiFi 或者蓝牙,电源上的噪声很容易串到图像数据里,表现为随机噪点或者横向条纹。常见做法是在模块的 VCC 和 GND 之间并一个 10uF 电解电容加一个 0.1uF 陶瓷电容,位置尽量靠近模块引脚。另外,OV7670 的 I/O 电平是 2.8V 左右,有些模块自带电平转换,有些没有。如果买到的是没有电平转换的裸模块,直接接 ESP32 的 3.3V GPIO 长期跑可能损伤传感器,最好加一个电平转换芯片或者至少串一个 100 欧姆电阻限流。这个细节在代码包里不会体现,但硬件上省掉这一步,后面调软件会一直怀疑是时序问题,其实是电平在打架。
3. I2S 并口采集:把 8 位数据流塞进 DMA 缓冲区
3.1 为什么选 I2S 而不是 GPIO 中断
ESP32 读取并口数据有两种常见思路,一种是给 PCLK 接中断,在中断里读 8 个 GPIO 的状态,拼成一个字节。这种方法在低分辨率下勉强能用,但 PCLK 频率一高,中断响应跟不上,丢像素是必然的。另一种是用 I2S 外设的 LCD 模式或者摄像头模式,把 PCLK 当作位时钟,VSYNC 当作帧同步,HREF 当作行有效,数据线接到 I2S 的数据输入引脚上,让 DMA 自动搬运。代码包里用的就是第二种,I2S 配置成从模式,接收 OV7670 送来的数据流。
I2S 的配置有几个关键参数:采样位宽、通道模式、DMA 缓冲区数量和大小。OV7670 输出的是 8 位并行数据,但 I2S 通常按字节或者半字接收,所以要把数据线映射到 I2S 的数据输入引脚上。ESP32 的 I2S 支持 8 位、16 位、24 位、32 位,这里选 8 位或者 16 位都可以,选 16 位的话每个像素会占两个字节,高位补零,处理起来稍微浪费一点内存,但 DMA 对齐更友好。DMA 缓冲区数量一般设 4 到 8 个,每个缓冲区大小根据一行像素的字节数来定。比如 QVGA 是 320x240,一行 320 个像素,8 位模式下就是 320 字节,缓冲区可以设成 320 的整数倍,避免一行数据跨两个缓冲区导致拼接逻辑变复杂。
// I2S 配置片段,从模式,接收并口数据 i2s_config_t i2s_config = { .mode = I2S_MODE_SLAVE | I2S_MODE_RX | I2S_MODE_PDM, .sample_rate = 1000000, // 初始值,实际由 PCLK 决定 .bits_per_sample = I2S_BITS_PER_SAMPLE_8BIT, .channel_format = I2S_CHANNEL_FMT_ONLY_LEFT, .communication_format = I2S_COMM_FORMAT_STAND_I2S, .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, .dma_buf_count = 8, .dma_buf_len = 320, // 一行 QVGA 像素数 .use_apll = false, .tx_desc_auto_clear = false, .fixed_mclk = 0 }; i2s_pin_config_t pin_config = { .bck_io_num = CAM_PCLK_GPIO, // PCLK 作为位时钟 .ws_io_num = CAM_HREF_GPIO, // HREF 作为左右声道选择 .data_in_num = CAM_D0_GPIO, // 数据线起始引脚 .data_out_num = I2S_PIN_NO_CHANGE };这段配置里,.mode选了从模式加接收,因为 PCLK 是 OV7670 给的,ESP32 只能跟着走。.dma_buf_len设成 320,对应 QVGA 一行,这样 DMA 每次填满一个缓冲区就是完整一行,应用层读的时候不用做跨缓冲区拼接。.bck_io_num接 PCLK,.ws_io_num接 HREF,这两个信号的极性如果反了,读出来的数据会整体偏移。参数说明:.sample_rate在从模式下其实不起决定作用,实际速率由 PCLK 决定,但填一个合理值可以避免驱动内部计算出奇怪的时钟分频。.use_apll设 false,因为从模式不需要 APLL。如果换成 VGA 分辨率,一行 640 像素,.dma_buf_len要相应改成 640,同时dma_buf_count可能要加大,否则 DMA 中断太频繁,CPU 来不及处理。
3.2 VSYNC 中断和帧同步怎么配合
I2S 的 DMA 只管搬数据,它不知道一帧从哪里开始、到哪里结束。所以还需要一个 VSYNC 中断来标记帧边界。常见做法是把 VSYNC 接到一个普通 GPIO 上,配置成下降沿或者上升沿触发中断。在中断服务函数里,把当前 DMA 缓冲区的写指针重置,或者设置一个帧开始标志,让应用层知道接下来读到的数据是新的一帧。代码包里一般会用一个环形缓冲区来存图像数据,VSYNC 中断负责把写索引拨回开头,HREF 的有效电平则用来判断一行是否有效。
这里有个容易忽略的点:VSYNC 中断和 I2S DMA 中断的优先级。如果 VSYNC 中断优先级太低,可能在帧已经开始之后才响应,导致第一行数据丢失。如果优先级太高,又可能打断 I2S 的 DMA 搬运,造成数据错位。一般把 VSYNC 中断设成中等优先级,并且在中断里只做最简单的标志位操作,不做数据拷贝。数据拷贝放在主循环或者单独的任务里,从 DMA 缓冲区读走。
// VSYNC 中断服务函数,只做帧同步标记 static void IRAM_ATTR vsync_isr_handler(void* arg) { portBASE_TYPE high_task_awoken = pdFALSE; frame_ready = true; // 通知应用层一帧开始 write_index = 0; // 环形缓冲区写指针归零 xTaskNotifyFromISR(cam_task_handle, 0x01, eSetBits, &high_task_awoken); if (high_task_awoken == pdTRUE) { portYIELD_FROM_ISR(); } }这段中断函数里,frame_ready是一个 volatile 标志,write_index是环形缓冲区的写位置。xTaskNotifyFromISR用来唤醒摄像头任务,让它开始从 I2S 读数据。注意IRAM_ATTR修饰,因为中断服务函数在 Flash 操作期间也可能被调用,放在 IRAM 里可以避免缓存失效导致的崩溃。参数说明:0x01是通知位,应用层任务里用xTaskNotifyWait等这个位。如果发现帧率不稳定,可以在中断里翻转一个 GPIO,用示波器看 VSYNC 的实际频率,跟预期对比。
3.3 从 DMA 缓冲区到图像数组的搬运逻辑
I2S 读出来的数据是连续的字节流,但图像在内存里通常按行存放,每行有固定的像素数。如果 DMA 缓冲区大小刚好等于一行,那么每次i2s_read返回的就是一行数据,直接拷到图像数组的对应行就行。如果缓冲区大小是一行的整数倍,就要按行拆分。代码包里一般会维护一个frame_buffer二维数组或者一维数组加行偏移,i2s_read读到的数据按顺序填入。
搬运的时候要注意字节序和像素格式。OV7670 默认输出 RGB565,每个像素两个字节,低字节在前还是高字节在前取决于寄存器配置。如果读出来的图像颜色偏蓝或者偏红,先检查 RGB565 的字节顺序。常见做法是在 SCCB 初始化的时候把COM7或者COM13寄存器里的位设对,然后在应用层按同样的顺序解析。如果用的是灰度模式,每个像素一个字节,搬运逻辑更简单,但分辨率不能设太高,否则一帧数据量超过内存。
// 从 I2S 读取一行数据并存入帧缓冲区 size_t bytes_read = 0; uint8_t line_buf[320]; // QVGA 一行 320 字节,灰度模式 esp_err_t ret = i2s_read(I2S_NUM_0, line_buf, sizeof(line_buf), &bytes_read, portMAX_DELAY); if (ret == ESP_OK && bytes_read == sizeof(line_buf)) { // 按行号写入帧缓冲区,line_count 由 HREF 计数维护 memcpy(&frame_buffer[line_count * 320], line_buf, 320); line_count++; if (line_count >= 240) { line_count = 0; frame_ready = true; // 一帧收满 } }这段代码里,i2s_read阻塞等待一行数据,portMAX_DELAY表示一直等到有数据为止。line_count由 HREF 的有效边沿计数得到,每来一个 HREF 脉冲就是一行。frame_buffer的大小是 320x240 字节,灰度模式下一帧 76800 字节,ESP32 的内部 RAM 够用,但如果开 WiFi 或者蓝牙,内存会紧张,可能需要用 PSRAM 或者降低分辨率。参数说明:bytes_read返回实际读到的字节数,如果小于预期,说明 PCLK 太快或者 DMA 缓冲区不够,需要降 XCLK 频率或者加大dma_buf_count。
4. SCCB 寄存器配置:别抄错一个字节就花屏
4.1 初始化序列里哪些寄存器不能省
OV7670 的寄存器有上百个,但真正影响出图的核心就那么十几个。代码包里通常会带一个初始化数组,按顺序写寄存器。第一个要写的是COM7,用来复位或者选择分辨率。写0x80是复位所有寄存器,写0x00是进入正常模式。复位之后要等至少 1ms 再写其他寄存器,否则配置不生效。接着是CLKRC,用来设置内部时钟分频,影响 PCLK 频率。如果 XCLK 给 24MHz,CLKRC设成0x80可能分频出 12MHz 的 PCLK,设成0xC0分频出 6MHz。这个值要根据实际接线和 I2S 能承受的速率来调。
然后是COM3到COM13这一组,控制输出格式、镜像、翻转、白平衡、曝光等。其中COM7的 bit2 决定输出 RGB 还是 YUV,bit4 决定分辨率。COM13的 bit0 控制 RGB565 的字节顺序。如果图像颜色不对,先查这两个寄存器。还有TSLB和COM13配合,影响 YUV 的 UV 顺序。这些寄存器在初始化数组里的顺序不能乱,有些寄存器必须在其他寄存器之前写,否则会被后续写入覆盖。
// OV7670 初始化寄存器序列,QVGA RGB565 const uint8_t ov7670_init_regs[][2] = { {0x12, 0x80}, // COM7 复位 {0xFF, 0x01}, // 延时标记,实际代码里用 vTaskDelay {0x12, 0x00}, // COM7 正常模式 {0x0C, 0x00}, // COM3 关闭缩放 {0x3E, 0x00}, // COM14 关闭缩放 {0x40, 0xD0}, // COM15 RGB565 输出范围 {0x13, 0xE0}, // COM13 使能 RGB565,字节顺序调整 {0x1E, 0x30}, // MVFP 垂直翻转和镜像 {0x72, 0x11}, // SCALING_PCLK_DIV {0x73, 0x00}, // SCALING_PCLK_DELAY {0x11, 0x80}, // CLKRC 分频 {0x0D, 0x40}, // COM4 {0x3D, 0x80}, // COM13 再次确认 {0x17, 0x14}, // HSTART {0x18, 0x03}, // HSTOP {0x19, 0x03}, // VSTART {0x1A, 0x7B}, // VSTOP {0x32, 0x80}, // HREF 控制 {0x03, 0x0A}, // VREF {0x0A, 0x76}, // PID {0x0B, 0x73}, // VER {0x00, 0x00} // 结束标记 };这个数组里,{0xFF, 0x01}是一个占位标记,实际代码里遇到这个标记会插入一个延时,让复位生效。COM7写0x80之后必须等,否则后面的配置全部无效。COM15的0xD0是设置 RGB565 的输出范围,如果设成0xF0可能颜色过曝。CLKRC的0x80是分频值,改这个值可以调 PCLK 频率。HSTART、HSTOP、VSTART、VSTOP控制有效像素窗口,如果图像边缘有黑边或者偏移,调这四个寄存器。参数说明:每个寄存器写完之后,SCCB 需要一点时间生效,连续写太快可能导致某些寄存器被跳过,常见做法是每写几个寄存器加一个短延时。
4.2 用 SCCB 读回寄存器验证写入
写完初始化序列之后,最好把关键寄存器读回来对比,确认写入成功。SCCB 的读时序比写时序多一个重复起始条件,但 OV7670 对重复起始不敏感,所以软件模拟的时候可以直接发两次起始。代码包里一般会提供一个sccb_read函数,传入寄存器地址,返回读到的值。如果读回来的值和写入的不一样,说明 SCCB 通信有问题,可能是上拉电阻没接、线太长、或者 GPIO 配置错了。
// SCCB 读寄存器,返回读到的字节 uint8_t sccb_read(uint8_t reg_addr) { uint8_t data = 0; sccb_start(); sccb_write_byte(OV7670_WRITE_ADDR); // 写地址 sccb_write_byte(reg_addr); // 寄存器地址 sccb_stop(); sccb_start(); sccb_write_byte(OV7670_READ_ADDR); // 读地址 data = sccb_read_byte(); sccb_nack(); sccb_stop(); return data; }这段代码里,OV7670_WRITE_ADDR通常是0x42,OV7670_READ_ADDR是0x43。先写寄存器地址,再发起读,读到一个字节后发 NACK 再停止。如果读回来一直是0xFF或者0x00,先检查 SDA 和 SCL 有没有上拉电阻,OV7670 模块上一般自带 4.7k 上拉,但有些裸模块没有。另外,SCCB 的时钟频率不能太高,软件模拟的时候延时循环要够,一般 SCL 高电平时间和低电平时间各保持 5us 左右比较稳。
4.3 曝光和白平衡的寄存器怎么调
OV7670 的自动曝光和自动白平衡由COM8、COM9、AEW、AEB等寄存器控制。默认初始化序列里一般会打开 AEC 和 AGC,让传感器根据环境光自动调整。但如果光线变化剧烈,自动曝光可能会来回震荡,导致图像忽明忽暗。常见做法是先把自动曝光关掉,手动设一个固定值,等图像稳定了再决定要不要开。AECH和AECL是曝光的高字节和低字节,GAIN是增益。手动曝光的时候,先设COM8的 bit0 为 0 关闭 AEC,然后写AECH和AECL。增益同理,关掉 AGC 之后写GAIN。
白平衡方面,COM8的 bit1 控制 AWB,关掉之后可以手动写BLUE和RED增益。如果图像偏绿,调GREEN增益。这些寄存器没有固定的“正确值”,跟具体的光源和镜头有关,一般是在实际环境里一边看图像一边调。代码包里如果带了调参工具,可以通过串口发命令实时改寄存器,不用每次重新烧录。这个功能在调试阶段非常省时间,建议优先确认代码包里有没有。
5. 避坑与排查:花屏、丢帧、颜色错乱怎么定位
5.1 图像全是噪点或者横向条纹
现象是读出来的图像没有有效内容,全是随机噪点,或者有规律的横向条纹。原因通常是 PCLK 太快,I2S 采样跟不上,或者电源噪声串进了数据线。解决方法是先把 XCLK 降到 8MHz,看噪点是否减少。如果减少,说明是速率问题,可以逐步提高 XCLK 找到稳定上限。如果没变化,检查电源去耦电容有没有焊,数据线有没有跟 WiFi 天线或者时钟线走得太近。另外,PCLK 的极性如果反了,也会表现为条纹,试着在 I2S 配置里翻转 PCLK 的有效沿。
5.2 图像斜切或者只有半帧
现象是图像上半部分正常,下半部分错位,或者整体斜切。原因一般是 VSYNC 和 HREF 的时序没对齐,或者 DMA 缓冲区大小跟一行像素数不匹配。先确认dma_buf_len是不是等于一行像素的字节数,如果不等,改成相等。然后检查 VSYNC 中断里有没有正确重置write_index,如果重置晚了,新帧的数据会覆盖旧帧的尾部。还有一种可能是 HREF 的极性反了,导致行计数多算或者少算,图像垂直方向被压缩。
5.3 颜色偏蓝、偏红或者整体发绿
现象是图像能出,但颜色明显不对。原因通常是 RGB565 的字节顺序错了,或者白平衡没调好。先查COM13的 bit0,翻转它看颜色是否恢复正常。如果翻转后颜色对了但图像镜像了,再查MVFP寄存器的镜像位。如果颜色还是偏,关掉 AWB,手动调BLUE和RED增益。注意 OV7670 的 RGB565 输出有两种字节顺序,一种是小端,一种是大端,代码里解析的时候要跟寄存器配置一致。
5.4 SCCB 写不进去,读回来全是 0xFF
现象是初始化的时候读厂商 ID 失败,或者写寄存器之后读回来没变化。原因可能是 SCCB 的上拉电阻没接,或者 SCL/SDA 的 GPIO 配置成了输出但没开漏。软件模拟 SCCB 的时候,SDA 在输出和输入之间切换,如果没正确切换方向,读回来的永远是高电平。解决方法是确认 SDA 在读之前设成输入模式,并且使能内部上拉(如果外部没有上拉)。另外,SCCB 的起始条件里,SDA 拉低的时候 SCL 必须是高电平,如果顺序反了,OV7670 不会响应。
5.5 帧率忽高忽低,偶尔卡死
现象是图像能出,但帧率不稳定,跑一段时间之后任务看门狗复位。原因可能是 DMA 缓冲区数量不够,I2S 中断太频繁,挤占了其他任务的 CPU 时间。先把dma_buf_count从 4 加到 8 或者 16,看是否改善。如果还卡,检查 VSYNC 中断里有没有调用阻塞函数,中断服务函数必须尽快返回。另外,如果开了 WiFi,WiFi 任务会占用大量 CPU,摄像头任务优先级要设得比 WiFi 低,但又要保证能及时读走 DMA 数据,这个平衡需要根据实际帧率调。
6. 进阶:把 JPEG 编码和 WiFi 传输串起来
6.1 为什么要在 ESP32 上做 JPEG 编码
OV7670 输出的是原始 RGB565 或者 YUV 数据,一帧 QVGA 的 RGB565 是 320x240x2 等于 153600 字节,直接通过 WiFi 发出去,带宽压力很大,而且很多上位机不认这种裸数据。常见做法是在 ESP32 上先做 JPEG 编码,把一帧压到 10KB 到 30KB,再通过 TCP 或者 UDP 发出去。ESP32 的 ROM 里带了一个 JPEG 编码器,但只能处理灰度或者特定格式,彩色 JPEG 编码需要自己移植或者用第三方库。代码包里如果带了 JPEG 编码,一般会提供一个frame_to_jpeg的函数,输入 RGB565 数组,输出 JPEG 字节流。
// 调用 JPEG 编码器,把 RGB565 帧压成 JPEG size_t jpeg_size = 0; uint8_t *jpeg_buf = malloc(320 * 240); // 压缩后一般不超过 30KB if (jpeg_buf != NULL) { jpeg_size = frame_to_jpeg(frame_buffer, 320, 240, jpeg_buf, 320 * 240); if (jpeg_size > 0) { // 通过 TCP 发送 jpeg_buf 和 jpeg_size send(sock, jpeg_buf, jpeg_size, 0); } free(jpeg_buf); }这段代码里,frame_to_jpeg是编码函数,输入帧缓冲区、宽、高,输出 JPEG 缓冲区。jpeg_buf的大小按一帧原始数据的一半来分配,一般够用。如果编码失败返回 0,可能是内存不够或者输入格式不对。参数说明:JPEG 质量可以在编码函数里设,质量越高,压缩后越大,传输越慢。QVGA 下质量设 12 到 15 比较平衡,再高就超过 UDP 包大小了。
6.2 用 UDP 还是 TCP,怎么选
UDP 延迟低,但丢包会导致图像花屏或者缺行。TCP 可靠,但重传会引入延迟,帧率不稳定。常见做法是局域网内用 UDP,每帧分片发送,接收端按帧号重组,丢一帧就丢一帧,不重传。如果对完整性要求高,用 TCP,但要把发送缓冲区设大一点,避免阻塞摄像头任务。代码包里一般会提供两种传输方式的示例,选哪种看实际场景。如果只是调试看图像,UDP 更简单,接收端用 Python 或者 ffmpeg 都能直接显示。
6.3 内存不够的时候怎么降级
ESP32 的内部 RAM 在开 WiFi 之后只剩 100KB 左右,一帧 QVGA RGB565 就占 150KB,肯定放不下。所以要么用 PSRAM,要么降分辨率到 QQVGA 或者用灰度模式。如果板子带 PSRAM,把frame_buffer分配到 PSRAM 里,用heap_caps_malloc指定MALLOC_CAP_SPIRAM。如果没有 PSRAM,把分辨率降到 160x120,灰度模式,一帧 19200 字节,内部 RAM 勉强够。JPEG 编码的时候也要注意,编码器的临时缓冲区可能不小,如果内存不够,先编码成灰度 JPEG,再传。
从那以后我每次调摄像头,都先把 XCLK 降到 8MHz,确认能出稳定图像,再逐步往上加,绝不一开始就冲 24MHz。寄存器配置也是,写完初始化序列一定读回关键寄存器对比,不省这一步。希望帮到你。
本文还有配套的精品资源,点击获取