☰
ESP32-S3 LCD_CAM驱动实战:摄像头与屏幕并口共存方案
2026/10/15 17:46:36 网站建设 项目流程

简介:这是一套面向嵌入式开发者的LCD与摄像头驱动组件,基于C语言编写,可同时支持esp32、esp32s2、esp32c3、esp32s3四款芯片平台,解决多型号ESP32在显示与图像采集场景下驱动适配繁琐的问题。资源以组件形式提供,便于直接集成到ESP-IDF项目中,适合具备一定嵌入式基础、正在做屏幕显示或摄像头应用的开发者参考使用。压缩包共30个文件,约107KB,其中19个h头文件与5个c源文件构成核心驱动实现,另含3个txt说明、1个yml配置、1个makefile及1个md文档,目录按芯片型号分设lcd_cam.c与include,结构清晰。目前已有973人学习下载。读者可获得完整的驱动源码与头文件接口,参考lcd_cam_init等初始化调用方式,快速完成组件添加与工程配置,减少自行移植调试的时间成本。

1. 把摄像头和屏幕塞进同一颗芯片:lcd_cam 驱动到底解决了什么

如果你手上有 ESP32-S3 这类带 LCD_CAM 外设的芯片,又恰好想同时接一个并口摄像头和一块并口屏,大概率会遇到一个尴尬:摄像头占着 I80 总线,屏幕也想走 I80,两套时序互相打架,帧率掉得没法看。lcd_cam 驱动就是冲着这个场景来的——它把 ESP32-S3、ESP32-C3、ESP32-S2 以及基础款 ESP32 上那套 LCD 与 Camera 共用外设的底层能力封装出来,让摄像头采集和屏幕刷新能在同一颗芯片上协调运行。适合做小型视觉终端、扫码设备、低功耗图像采集面板的开发者。它不解决算法,只解决“数据怎么从 sensor 搬进内存、再从内存搬到屏”这条通路。

2. 先搞清 LCD_CAM 外设的硬件边界:哪些芯片能跑、跑多快

2.1 四款芯片的外设差异决定了你能接什么

标题里列了四款芯片,但它们的 LCD_CAM 能力并不对等,选型前必须把这张表看明白,否则代码写完发现引脚根本不够用。

芯片LCD 并口位宽Camera 并口位宽典型最高 PCLK是否支持 RGB 屏
ESP32-S38/16 bit8/16 bit40 MHz 级支持
ESP32-S28/16 bit8/16 bit40 MHz 级支持
ESP32-C3无独立 LCD_CAM无不适用不支持
ESP328 bit8 bit20 MHz 级部分支持

这里有个血泪经验:ESP32-C3 并没有独立的 LCD_CAM 外设,它走的是 SPI 屏方案,标题里把它列进来,更多是因为同一套驱动框架在 C3 上能通过 SPI 接口复用部分逻辑。如果你冲着“并口摄像头 + 并口屏”去买 C3,会直接翻车。ESP32 基础款虽然能跑,但 PCLK 上限低,接 OV2640 这类 sensor 时帧率会被压到 10 fps 以下,只适合做静态抓拍。

2.2 总线仲裁:摄像头和屏幕为什么不能同时满速

LCD_CAM 外设内部只有一套并口时序发生器,摄像头和屏幕分时复用。驱动层做的事,本质是在 DMA 描述符链上做时间片调度。摄像头一帧数据通过 DMA 写入 PSRAM,屏幕刷新再从 PSRAM 读出来送进 LCD FIFO。两者抢的是同一组 GPIO 矩阵和 DMA 通道。

常见做法是给摄像头分配更高的 DMA 优先级,屏幕刷新用双缓冲,在摄像头帧消隐期集中刷屏。我一般会把摄像头帧率锁在 15~20 fps,屏幕刷新率放到 30 Hz 以上,这样人眼看到的是流畅的屏,摄像头也不丢帧。参数上,cam_dma_burst设 16 或 32,lcd_dma_burst设 8,让摄像头一次搬更多数据,减少总线切换开销。

2.3 引脚矩阵配置:哪些 GPIO 能复用、哪些是死路

ESP32-S3 的 LCD_CAM 信号通过 GPIO 矩阵几乎可以映射到任意引脚,但有几个坑:

  • PCLK、HSYNC、VSYNC 这类高频信号尽量放在同一组 IO_MUX 支持的引脚上,否则会引入额外延迟。
  • 摄像头 XCLK 输出引脚必须选支持时钟输出的 GPIO,不是所有脚都能出 20 MHz 时钟。
  • 屏幕的 DE 信号和摄像头的 PCLK 不要放在相邻引脚,串扰会让图像出现斜纹。

配置时用esp_rom_gpio_connect_out_signal把信号绑到具体 GPIO,再用gpio_set_drive_capability把驱动能力调到 3 档,长排线场景下能明显减少花屏。

3. 从零搭一个摄像头采集 + 屏幕预览的最小工程

3.1 环境准备与依赖组件拉取

假设你已经装好 ESP-IDF,版本建议 v5.1 以上,因为 lcd_cam 驱动对 DMA 描述符链的 API 在 v5.0 之后才稳定。新建工程后,在idf_component.yml里声明依赖:

dependencies: idf: ">=5.1" espressif/esp32-camera: "^2.0.0"

然后在CMakeLists.txt里把驱动源文件加进来。如果你拿到的是一份独立的 lcd_cam 驱动包,通常包含lcd_cam.c、lcd_cam.h和几个寄存器定义头文件,直接放进components/lcd_cam/目录即可。

idf.py create-component lcd_cam # 把驱动源码拷进 components/lcd_cam/ idf.py set-target esp32s3 idf.py menuconfig

在 menuconfig 里要开三处:Component config → ESP32S3-Specific → Support for external, SPI-connected RAM打开 PSRAM;Camera configuration里选对 sensor 型号;LCD configuration里把并口位宽设成 16 bit。

3.2 摄像头初始化:sensor 参数与 DMA 缓冲怎么配

摄像头初始化分两步:先配 sensor 寄存器,再配 LCD_CAM 外设的接收通道。下面这段代码是 OV2640 在 800x600 分辨率下的典型配置:

#include "lcd_cam.h" #include "esp_camera.h" static camera_config_t cam_cfg = { .pin_pclk = 15, .pin_xclk = 16, .pin_vsync = 17, .pin_href = 18, .pin_d0 = 4, .pin_d1 = 5, .pin_d2 = 6, .pin_d3 = 7, .pin_d4 = 8, .pin_d5 = 9, .pin_d6 = 10, .pin_d7 = 11, .xclk_freq_hz = 20000000, // XCLK 20MHz,OV2640 稳定上限 .ledc_timer = LEDC_TIMER_0, .ledc_channel = LEDC_CHANNEL_0, .pixel_format = PIXFORMAT_RGB565, // 直接出 RGB565,省一次转换 .frame_size = FRAMESIZE_SVGA, // 800x600 .jpeg_quality = 12, .fb_count = 2, // 双缓冲,一帧采集一帧处理 .fb_location = CAMERA_FB_IN_PSRAM, .grab_mode = CAMERA_GRAB_LATEST, // 取最新帧,避免旧帧堆积 }; esp_err_t cam_ret = esp_camera_init(&cam_cfg); if (cam_ret != ESP_OK) { ESP_LOGE(TAG, "camera init failed: %s", esp_err_to_name(cam_ret)); return; }

逻辑说明:xclk_freq_hz设 20 MHz 是 OV2640 的稳妥值,设 24 MHz 部分批次会出噪点。pixel_format选 RGB565 是因为后面要直接推屏,如果选 JPEG 还得软解,S3 的算力扛不住实时。fb_count设 2 时,驱动会分配两块 PSRAM 帧缓冲,DMA 写满一块就切下一块,CPU 处理上一块,这是能跑满 20 fps 的关键。grab_mode选CAMERA_GRAB_LATEST而不是CAMERA_GRAB_WHEN_EMPTY,前者在屏刷新慢的时候会丢旧帧,后者会阻塞采集。

3.3 屏幕并口初始化:时序参数一个都不能错

屏幕这边以 ST7789 并口模式为例,初始化时最容易被忽略的是时序参数。下面这段配置里,lcd_cam_config_t的每个字段都对应屏幕手册上的一个时间要求:

static lcd_cam_config_t lcd_cfg = { .data_width = 16, .pin_data = {12, 13, 14, 21, 38, 39, 40, 41, 42, 45, 46, 47, 48, 1, 2, 3}, .pin_wr = 19, .pin_rd = 20, .pin_cs = 43, .pin_dc = 44, .pin_rst = 0, .pin_backlight = 45, .wr_active_edge = 0, // 写信号下降沿采样 .rd_active_edge = 0, .cs_active_low = 1, .dc_cmd_level = 0, // DC 低电平为命令 .dc_data_level = 1, .pclk_hz = 20000000, // 并口时钟 20MHz .max_transfer_bytes = 800 * 600 * 2, // 一帧 RGB565 字节数 }; esp_err_t lcd_ret = lcd_cam_init(&lcd_cfg); if (lcd_ret != ESP_OK) { ESP_LOGE(TAG, "lcd init failed: %s", esp_err_to_name(lcd_ret)); return; }

逻辑说明:wr_active_edge设 0 表示下降沿写数据,ST7789 手册要求 WR 下降沿采样,设反了会出全白或全黑。pclk_hz设 20 MHz 是并口屏的常见上限,再高需要缩短排线并加匹配电阻。max_transfer_bytes必须大于等于一帧数据量,否则 DMA 描述符链会截断,屏幕只显示上半部分。pin_backlight如果接的是 PWM 调光,这里先给高电平,后面再用 LEDC 单独控制。

3.4 主循环:采集一帧、推一帧、不撕裂

主循环里要做的是把摄像头帧缓冲直接交给屏幕 DMA,中间不做像素格式转换,这是能跑流畅的核心:

while (1) { camera_fb_t *fb = esp_camera_fb_get(); if (!fb) { vTaskDelay(pdMS_TO_TICKS(10)); continue; } // 直接把 RGB565 帧缓冲推给 LCD esp_err_t ret = lcd_cam_send_frame(fb->buf, fb->len); if (ret != ESP_OK) { ESP_LOGW(TAG, "lcd send frame failed"); } esp_camera_fb_return(fb); // 归还缓冲,让 DMA 继续采集 vTaskDelay(pdMS_TO_TICKS(1)); }

逻辑说明:esp_camera_fb_get返回的fb->buf已经在 PSRAM 里,lcd_cam_send_frame内部会启动 LCD DMA 从同一块内存读数据。这里有个关键点:屏幕 DMA 读的时候,摄像头 DMA 不能写同一块缓冲,所以fb_count必须大于等于 2。vTaskDelay(1)是给 RTOS 调度留时间,不加的话看门狗会咬。如果屏幕出现横向撕裂,说明屏幕刷新和摄像头采集撞在同一块缓冲上了,把fb_count加到 3 能缓解。

4. 避坑与排查:调不通时先看这五个地方

4.1 现象:屏幕全白,摄像头能出图

原因:并口屏的 WR 信号极性配反了,或者 CS 一直没拉低。ST7789 在 WR 上升沿采样数据,如果驱动里设成下降沿,数据全错。

解决:把wr_active_edge从 0 改成 1 试一次,同时用逻辑分析仪抓 WR 和 CS,确认 CS 在整帧传输期间保持低电平。如果 CS 是 GPIO 控制的,检查cs_active_low是否和硬件一致。

4.2 现象:图像有斜纹或颜色错位

原因:数据线顺序接错了。并口 RGB565 的 D0~D15 对应关系是固定的,如果 D0 和 D1 接反,颜色会整体偏移。

解决:写一个测试图案,让屏幕显示红、绿、蓝、白四色竖条,然后逐根调换数据线,直到颜色正确。更省事的办法是在驱动里加一个data_pin_swap映射表,软件层面纠正。

4.3 现象:帧率只有 5 fps,远低于预期

原因:PSRAM 带宽被占满,或者 DMA 描述符链太短导致频繁中断。ESP32-S3 的 PSRAM 在 80 MHz 下理论带宽约 80 MB/s,800x600 RGB565 一帧是 960 KB,20 fps 需要 19 MB/s,看似够用,但摄像头写和屏幕读同时发生时会抢带宽。

解决:把cam_dma_burst和lcd_dma_burst都调到 32,减少总线仲裁次数。另外确认 PSRAM 模式是 Octal 而不是 Quad,Octal 带宽翻倍。如果还不行,把分辨率降到 640x480。

4.4 现象:运行几分钟后花屏或重启

原因:DMA 描述符链耗尽,或者 PSRAM 热稳定性问题。有些模组的 PSRAM 在高温下会出位翻转。

解决:在menuconfig里把 PSRAM 速度从 80 MHz 降到 60 MHz,牺牲一点带宽换稳定。同时检查fb_count是否设得太大导致内存不足,S3 的 PSRAM 通常 8 MB,fb_count=3时 800x600 要占 2.8 MB,加上屏幕缓冲和程序本身,余量不多。

4.5 现象:ESP32-C3 上编译报错找不到 LCD_CAM 寄存器

原因:C3 没有 LCD_CAM 外设,驱动里的寄存器定义头文件在 C3 目标下不存在。

解决:如果一定要用 C3,走 SPI 屏方案,把 lcd_cam 驱动里的并口部分用宏隔离掉,只保留 SPI 接口层。标题里列 C3 更多是框架兼容性,不是硬件能力。

5. 进阶:用双缓冲 + 局部刷新把屏刷新率再提一档

前面跑通的是全屏刷新,每帧都要把 960 KB 从 PSRAM 搬到屏幕,PSRAM 带宽吃紧。实际做产品时,我一般会改成局部刷新:摄像头只采集变化区域,屏幕只刷对应矩形。lcd_cam 驱动支持lcd_cam_send_region(x, y, w, h, buf)这样的接口,配合摄像头的 ROI 裁剪,能把数据量降到全屏的 1/4 甚至更低。

具体做法是:在摄像头配置里把frame_size设成FRAMESIZE_SVGA,但通过set_roi只取中间 400x300 区域;屏幕端用 ST7789 的CASET和RASET命令限定刷新窗口。这样一帧数据量从 960 KB 降到 240 KB,PSRAM 带宽压力骤降,帧率能从 20 fps 提到 35 fps 以上。

另一个技巧是错开摄像头和屏幕的 DMA 突发窗口。在lcd_cam_send_region调用前,先esp_camera_fb_return把当前帧还回去,让摄像头 DMA 先跑,等屏幕 DMA 启动后再取下一帧。这个顺序调整能减少总线冲突,实测帧率波动从 ±5 fps 降到 ±1 fps。

验证方法很简单:在屏幕四角画四个小方块,用手机慢动作拍屏幕,看方块有没有闪烁或撕裂。如果方块稳定,说明双缓冲和局部刷新配合到位了。我自己的习惯是每改一次 DMA 参数就跑一次这个测试,比看日志靠谱。

最后说个教训:别在 PSRAM 里同时放摄像头帧、屏幕帧和 JPEG 编码缓冲,S3 的 8 MB 看着大,三块 800x600 的 RGB565 就吃掉 2.8 MB,再加 WiFi 协议栈和 LVGL 的显存,很容易 OOM。我一般会把屏幕缓冲放到内部 SRAM,虽然只有 512 KB,但放一块 400x300 的局部刷新缓冲绰绰有余,带宽还比 PSRAM 高一个数量级。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询