|
第三十七章 MIPICAMERA实验
1)实验平台:正点原子DNESP32P4开发板
2)章节摘自【正点原子】ESP32-P4开发指南— V1.0
3)购买链接:https://detail.tmall.com/item.htm?id=873309579825
4)全套实验源码+手册+视频下载地址:http://www.openedv.com/docs/boards/esp32/ATK-DNESP32P4.html
5)正点原子官方B站:https://space.bilibili.com/394620890
6)正点原子DNESP32S3开发板技术交流群:132780729
本章,我们将介绍MIPI摄像头OV5645,学习如何在ESP32-P4上驱动MIPI摄像头,实现摄像头图像数据采集并进行显示。
本章分为如下几个小节:
37.1 OV5645摄像头介绍及ESP32-P4的CAM相关外设介绍
37.2 硬件设计
37.3 程序设计
37.4 下载验证
37.1 OV5645摄像头介绍及ESP32-P4的MIPI_CSI介绍
37.1.1 OV5645摄像头介绍
OV5645是正点原子推出的一款高性能超清2K摄像头模块。该模块具有一颗1/4英寸CMOS 2592x1944图像传感器OV5645。同时,模块集成了有源晶振和LDO芯片。模块接口有间距2mm的2*11pin排针和间距为0.5mm的22pin的FPC接口,可供用户根据不同的场景选择使用。
模块实物图如下图所示。
图37.1.1.1 OV5645摄像头实物图
OV5645模块具有以下特点:
●像素大小:1.4微米 x 1.4微米
●500万像素(2592x1944)
●可编程控制帧速率,镜像和翻转,裁剪和窗口
●支持图像大小:2K(2560x1440)、1080P(1920x1080)、720P(1280x720)等
●双线串行总线控制(SCCB)
●双通道MIPI串行输出
●集成有源晶振,无需外部提供时钟
●集成LDO,仅需提供3.3V电源即可正常工作
●自动图像控制功能:自动曝光控制(AEC)、自动白平衡(AWB)、自动带通滤波器(ABF)、自动黑电平校准(ABLC)、自动50/60Hz亮度检测
●可编程控制:帧率、图像大小、水平反射镜、垂直翻转、裁剪和平移
●支持RawRGB、RGB565、RGB555、RGB444、YUV422、YUV420、YCbCr422和压缩图像(JPEG)输出格式
●电源核心:1.5±5%;模拟量:2.6V ~ 3.0V;输入/输出:1.7V ~ 3.0V
●工作温度范围:-30℃ ~ 70℃,稳定成像温度范围:0 ~ 50℃
OV5645的内部结构如下图所示:
图 错误!文档中没有指定样式的文字。.1.2 OV5645内部结构图
由上图可知,时序发生器timing generator控制着感光阵列image array、放大器AMP、AD转换以及输出外部时序信号(VSYNC、HREF和PCLK),外部时钟XVCLK经过PLL锁相环后输出的时钟作为系统的控制时钟;感光阵列将光信号转化成模拟信号,经过增益放大器之后进入10位AD转换器;AD 转换器将模拟信号转化成数字信号,并且经过ISP进行相关图像处理,最终输出所配置格式视频数据流。增益放大器控制以及 ISP 等都可以通过寄存器(registers)来配置,配置寄存器的接口就是SCCB接口,该接口协议兼容IIC协议。
SCCB(Serial Camera Control Bus,串行摄像头控制总线)是由OV(OmniVision的简称)公司定义和发展的三线式串行总线,该总线控制着摄像头大部分的功能,包括图像数据格式、分辨率以及图像处理参数等。OV公司为了减少传感器引脚的封装,现在 SCCB 总线大多采用两线式接口总线。
OV5645使用的是两线式SCCB接口总线,该接口总线包括SIO_C串行时钟输入线和SIO_D 串行双向数据线,分别相当于IIC协议的SCL信号线和SDA信号线。我们在前面提到过SCCB协议兼容IIC协议,是因为SCCB协议和IIC协议非常相似,有关IIC协议的详细介绍请大家参考“IIC_EXIO”章节。
OV5645是用16位(两个字节)表示寄存器地址。OV5645 SCCB的写传输协议如下图所示:
图 错误!文档中没有指定样式的文字。.3 SCCB 写传输协议
上图中的ID Address(W)由7位器件地址和1位读写控制位构成(0:写;1:读),而OV5645的器件地址为0x3C,所以在写传输协议中,ID Address(W)= 0x78(器件地址左移1位,低位补0);Sub-address(H)为高8位寄存器地址,Sub-address(L)为低 8 位寄存器地址,在OV5645众多寄存器中,有些寄存器是可改写的,有些是只读的,只有可改写的寄存器才能正确写入;Write Data为8位写数据,每一个寄存器地址对应8位的配置数据。上图中的第9位X表示Don’t Care(不必关心位),该位是由从机(此处指OV5645)发出应答信号来响应主机表示当前ID Address、Sub-address和Write Data是否传输完成,但是从机有可能不发出应答信号,因此主机可不用判断此处是否有应答,直接默认当前传输完成即可。
我们可以发现,SCCB和IIC写传输协议是极为相似的,只是在SCCB写传输协议中,第9位为不必关心位,而IIC写传输协议为应答位。SCCB的读传输协议和IIC有些差异,在IIC读传输协议中,写完寄存器地址后会有restart即重复开始的操作;而SCCB读传输协议中没有重复开始的概念,在写完寄存器地址后,发起总线停止信号,下图为SCCB的读传输协议。
图 错误!文档中没有指定样式的文字。.1.4 SCCB 读传输协议
SCCB读传输协议分为两个部分。第一部分是写器件地址和寄存器地址,即先进行一次虚写操作,通过这种虚写操作使地址指针指向虚写操作中寄存器地址的位置,当然虚写操作也可以通过前面介绍的写传输协议来完成。第二部分是读器件地址和读数据,此时读取到的数据才是寄存器地址对应的数据,注意ID Address(R) = 0x43(器件地址左移1位,低位补1)。上图中的NA位由主机产生,由于SCCB总线不支持连续读写,因此NA位必须为高电平。
在OV5645正常工作之前,必须先对传感器进行初始化,即通过配置寄存器使其工作在预期的工作模式,以及得到较好画质的图像。因为SCCB的写传输协议和IIC几乎相同,因此我们可以直接使用IIC的驱动程序来配置摄像头。当然这么多寄存器也并非都需要配置,很多寄存器可以采用默认的值。下表是本程序用到的关键寄存器的配置说明。
由于OV5645摄像头的寄存器非常多,并且大多数的寄存器都可以使用默认设置,因此上表只列出了一些关键寄存器,更多寄存器设置大家可以参考官方数据手册。
表37.1.1.1 OV5645寄存器表
由于OV5645摄像头的寄存器非常多,并且大多数的寄存器都可以使用默认设置,因此上表只列出了一些关键寄存器,更多寄存器设置大家可以参考官方数据手册。
时钟配置
在上表中列出了0x3035、0x3036和0x3037三个时钟配置寄存器,这三个寄存器在整个时钟链路当中的作用如下图所示:
图 错误!文档中没有指定样式的文字。.5 时钟路径
从图中可以看到OV5645外部输入时钟范围是6~27Mhz(板载24Mhz),外部输入时钟进入摄像头内部PLL,先经过pre-divider,根据0x3037寄存器bit0~bit3的值将时钟降为4~27Mhz(本节实验降低到8Mhz);之后再经过multiplier,判断寄存器0x3036的bit7位是否为1,如果是1则将时钟倍频到0x3036[7:1]*2倍的值,如果不为1则倍频到0x3036[6:0]倍的值(本节实验倍频到0x3036[6:0]倍的值,即112倍,最终输出值为896Mhz),最终输出时钟范围是500~1000Mhz;再之后时钟会经过SYS divider0和MIPI divider,前者是判断0x3035寄存器bit4~bit7的值是否为0,如果为0则时钟分频16倍,否则分频0x3035[7:4]倍,后者是判断0x3035寄存器bit0~bit3的值是否为0,如果为0则时钟分频16倍,否则分频0x3035[3:0]倍(本节实验先分频2倍,再分频1倍,最终输出448Mhz);最后时钟到达MIPI PHY,在这个模块之中直接对时钟进行2分频,最终输出MIPI CLK(本节实验最终的MIPI CLK为224Mhz)。
图像输出开窗设置
OV5645摄像头输出的图像大小是可以调整的,整体的开窗设置如下图所示:
图 错误!文档中没有指定样式的文字。.6 开窗示意图
从上图中可以看出OV5645的开窗大小是用(“开窗水平结束位置”减去“开窗水平起始位置”)*(“开窗垂直结束位置”减去“开窗垂直起始位置”),通过寄存器0x3800~0x3807来控制。
开完窗后还可以通过水平方向和垂直方向的偏移寄存器(0x3810~0x3813)来进一步调整大小,最终输出的图像就上图中白色区域部分。这里大家需要注意一下,在整体图像分辨率水平:垂直=4:3的情况下按上图去配置是可以的,如果是其他尺寸比例那么按上图配置不一定能成功,具体的配置需要原厂协助,我们也没有详细的配置方法或者尺寸计算公式。
OV5645输出图像大小不仅可以通过开窗来调整,还可以通过压缩功能来调整,如下图所示:
图 错误!文档中没有指定样式的文字。.7 图像压缩
当寄存器0x5001的bit5压缩使能打开(0关闭,1开启),就可以在开窗的基础上输出压缩后的图像,压缩图像等于开窗图像减去2倍水平垂直偏移量。
输出像素格式
从寄存器的配置表中就可以看到OV5645摄像头支持的像素格式非常多,可以通过配置寄存器0x4300,让摄像头输出不同格式的像素数据。本节实验使用的是RAW格式数据,所以接下来只介绍RAW格式的像素数据输出,如下图所示:
图 错误!文档中没有指定样式的文字。.8原始像素格式
从上图中可以看到整个传感器拥有水平2624垂直1964共计5153536个感光像素点,其中有效感光像素点是水平2592垂直1944共计5038848个像素,也就是图中active区域。在有效显示区域中像素数据是按照Bayer阵列排布的。所谓的Bayer阵列其实就是在上图active区域中像素按照2*2的方格排列,如下图所示:
图 错误!文档中没有指定样式的文字。.9 Byer阵列
上图中红色方框框出来的就是一个2*2的Byer阵列,可以看到它的组合方式是BGGR,这是摄像头的原始配置,当寄存器0x4300配置为0x00时输出的就是这种组合,第一行数据全部是BGBG,第二行数据全部是GRGR,组成Byer阵列就是BGGR。当然也可以通过改变寄存器0x4300的配置值来改变输出的像素格式,具体的格式配置在错误!未找到引用源。中已经列出。这种Byer阵列数据可以通过插值法来进一步转换成RGB数据,最终在显示屏幕上显示出来。
彩条测试模式
图像传感器配置成彩条测试模式后,会输出彩色的条纹,方便测试图像传感器是否正常工作,通过配置寄存器 0x503d 的Bit[7]位打开和关闭彩条模式。当需要打开彩条模式时,寄存器 0x503d 配置成 0x80,关闭时配置成0x00,下图为打开彩条模式后图像输出的条纹(8个颜色条)。
图 错误!文档中没有指定样式的文字。.1.10 测试彩条
注意彩条最左边是一个白色长条,颜色与背景一致,所以看不清楚。
关于OV5645的基础介绍,我们就介绍到这。OV5645的详细资料请参考光盘:7,硬件资料→2,芯片资料→ OV5645_CSP3_DS_2.01.pdf这个文档,供大家参考学习。
37.1.2 ESP32-P4的CAMERA模块介绍
ESP32-P4的LCD_CAM控制器包括独立的LCD控制模块和Camera(摄像头)控制模块。在前面已经介绍过了LCD模块,本章节主要介绍Camera模块。
首先通过分析LCD_CAM控制器的结构框图,了解一下其工作过程。结构框图如下图所示。
图37.1.2.1 LCD_CAM控制器结构框图
Camera控制模块(红色虚线以上)包括独立的接收控制单元(Camera_Ctrl)、接收异步FIFO(Async RX FIFO)、时钟生成模块(CAM_Clock Generator)和格式转换模块(RGB/YUV Converter)。
接收控制单元:用于控制CAM数据的接收。CAM数据从上图可知,可由GDMA取自内部或外部存储器。
接收异步FIFO:用于与外部设备进行交互。
CAM时钟生成模块:用于生成CAM_CLK时钟。时钟源经过时钟生成模块处理后,生成Camera模块所需要的时钟CAM_CLK,这一过程如下图所示。
图37.1.2.2 CAM模块时钟生成过程
Camera模块时钟由三个时钟源提供,分别是XTAL_CLK、PLL_F160M_CLK和APLL_CLK。可由HP_SYS_CLKRST_PERI_CLK_CTRL19_REG的HP_SYS_CLKRST_CAM_CLK_SRC_SEL位决定,0选择使用XTAL_CLK,1选择使用PLL_F160M_CLK,2选择使用APLL_CLK。
选择好时钟源后(即得到CAM_CLK_SRC),经过降频得到CAM_CLK,后面再经过分频最终得到CAM_CLK,这三者的关系如下:
Note:
① 第一条公式中的N、b和a都是通过HP_SYS_CLKRST_PERI_CLK_CTRL120_REG寄存器进行配置的。N的取值范围为2≤N≤256,通过HP_SYS_CLKRST_CAM_CLK_DIV_NUM位进行配置。b是通过HP_SYS_CLKRST_CAM_CLK_DIV_NUMERATOR位进行配置,而a是通过HP_SYS_CLKRST_CAM_CLK_DIV_DENONMINATOR位进行配置。
格式转换模块:用于各种格式的视频数据互相转换。在上图中,该部分是虚线框,也就是可不使用该模块。
Camera-LCD控制器的CAM和LCD接口都可通过GPIO交换矩阵灵活配置使用任意GPIO引脚。对于CAM模块的信号,即图37.1.2.1右上角部分,在这里也简单介绍一下,如下表所示。
表37.1.2.1 CAM模块信号描述
37.1.3 ESP32-P4 MIPI_CSI接口介绍
ESP32-P4带有一个MIPI CSI接口,用于连接MIPI接口的摄像头,具有如下特性:
● 符合MIPI CSI-2协议。
● 使用DPHY v1.1版本。
● 2-lane x 1.5 Gbps。
● 输入格式支持RGB888、RGB666、RGB565、YUV422、YUV420、RAW8、RAW10、RAW12。
MIPI CSI接口使用专用数字管脚,管脚序号为42~48。
37.1.4 图像信号处理器ISP介绍
ESP32-P4带有一个图像信号处理器(ISP),具有如下特性:
最大分辨率 1920 x 1080
三个输入通道:MIPI CSI、DVP、DW-GDMA
输入格式:RAW8、RAW10、RAW12
输出格式:RAW8、RGB888、RGB565、YUV422、YUV420
pipeline结构:拜尔域降噪(BF)、去马赛克(Demosaic)、色彩矩阵(CCM)、伽马矫正(Gamma Correction)、锐化(Edge)、对比度色相饱和度亮度调节(Contrast/Hue/Saturation/Brightness)、自动曝光统计(AE)、自动对焦统计(AF)、白平衡统计(AWB)、直方图统计(HIST)。
37.1.5 像素处理加速器PPA外设介绍
ESP32-P4带有像素处理加速器(PPA),可实现SRM与BLEND两大功能,具有如下特性:
SRM可实现图像旋转、缩放、镜像
- 输入格式支持ARGB8888、RGB888、RGB565、YUV420
- 输出格式支持ARGB8888、RGB888、RGB565、YUV420
- 支持逆时针90°、180°、270°旋转
- 支持水平、垂直方向缩放,缩放系数的整数部分为8-bit,小数部分为4-bit
- 支持水平、垂直方向镜像
BLEND可实现两个相同尺寸的图层叠加
- 输入格式支持ARGB8888、RGB888、RGB565、L4、L8、A4、A8
- 输出格式支持ARGB8888、RGB888、RGB565
- 支持两个图层基于Alpha通道叠加。若图层不包含Alpha通道信息,通过寄存器配置补全
- 通过设置前景以及背景color-key范围实现特殊颜色过滤
37.1.6 CAMERA驱动框架介绍
CAMERA驱动框架主要用到的是V4L2框架,即Video for Linux 2,是Linux系统中用于处理视频设备的内核驱动程序接口。
V4L2提供了一种标准化的方式,使用户空间程序能够与视频设备(如摄像头、视频采集卡等)进行通信和交互。它屏蔽了底层摄像头的不同驱动实现,为用户空间提供了统一的接口调用。
37.2 硬件设计
37.2.1 例程功能
开机的时候,初始化OV5645摄像头。初始化成功后直接在LCD上输出图像。 LED0用来指示程序正在运行。
37.2.2 硬件资源
1)LED灯
LED 0 - IO51
2)RGBLCD / MIPILCD(引脚太多,不罗列出来)
3)MIPICAMERA
CSI_D0_P(固定引脚) CSI_D0_N(固定引脚)
CSI_D1_P(固定引脚) CSI_D1_N(固定引脚)
CSI_CK_P(固定引脚) CSI_CK_N(固定引脚)
CSI_RST - IO34 CSI_PWDN - IO54
IIC_SCL - IO32 IIC_SDA - IO33
37.2.3 原理图
CAMERA相关原理图,如下图所示。
图37.2.3.1 摄像头实验原理图
这里说明一下,与OV5645通信的引脚都是1.8V的,所以通过CJ3134芯片进行转换。
37.3 程序设计
37.3.1 CAMERA的IDF驱动
esp_video、esp_sccb_intf、esp_cam_sensor组件驱动位于ESP-IDF在线组件注册表中。如果需要将该组件添加到项目工程中,可按照以下步骤操作:
1)打开ESP-IDF注册表。
2)搜索 “esp_video”、“esp_sccb_intf”、“esp_cam_sensor”组件。
3)将组件安装到项目中。
组件安装完成后,系统会自动更新main文件夹中的特殊组件清单文件idf_component.yml在项目编译时,系统会根据清单文件从注册表中下载并集成该组件到工程中。关于上述操作流程,可参考本书籍第八章的内容。
注意:在进行ESP32摄像头模块(如OV5645)开发时,必须对电源管理进行一些关键的调整。首先,在esp_cam_sensor组件下的ov5645.c文件中的ov5645_power_on函数中,我们需要确保pwdn(Power Down)管脚在初始化时先拉低再拉高。这一修改是根据OV5645摄像头的数据手册要求的,因为不按照这个时序操作,摄像头可能无法正常启动或工作不稳定。接着,在esp_video组件中的esp_video_csi_device.c文件里,我们将CSI_LDO_CFG_VOL_MV的电压值设置为1900mV。这是因为LDO3的电压在LCD初始化时已经被定义为1900mV,为了确保系统电源的一致性,必须将此电压值与LCD初始化时的配置保持一致。若电压不一致,可能会导致系统不稳定或出现运行错误。通过这些调整,我们可以确保摄像头模块和视频系统的电源管理符合硬件要求,并提高系统的稳定性,避免因电压不匹配或初始化顺序错误导致的潜在问题。这里的组件过多嵌套,暂时不做说明,请参考应用层代码即可。
37.3.2 程序流程图
图37.3.2.1 摄像头实验程序流程图
37.3.3 程序解析
在28_mipicamera例程中,作者在28_mipicamera\main\APP路径下新建了1个文件夹MIPI_CAM,并且需要更改CMakeLists.txt内容,以便在其他文件上调用。
1. MIPI_CAM驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。MIPI_CAM驱动源码包括四个文件:mipi_cam.c、mipi_cam.h、app_video.c和app_video.h。
其中mipi_cam.c和mipi_cam.h主要是MIPI摄像头的驱动文件,而app_video.c和app_video.h主要是关于MIPI摄像头调用相对下层接口实现。
在mipi_cam.h中主要是函数声明,大家自行查看源码即可。
下面我们再解析mipi_cam.c的程序,首先先来看一下初始化函数mipi_cam_init,代码如下:
- <font size="3">/**</font>
- <font size="3"> * @brief mipi_cam初始化</font>
- <font size="3"> * [url=home.php?mod=space&uid=271674]@param[/url] 无</font>
- <font size="3"> * @retval ESP_OK:开启成功; ESP_FAIL:开启失败</font>
- <font size="3"> */</font>
- <font size="3">esp_err_t mipi_cam_init(void)</font>
- <font size="3">{</font>
- <font size="3"> esp_err_t ret = ESP_OK;</font>
- <font size="3"> ret = app_video_main(bus_handle); /* 初始化摄像头硬件和软件接口 */</font>
- <font size="3"> if (ret != ESP_OK)</font>
- <font size="3"> {</font>
- <font size="3"> ESP_LOGE(mipi_cam_tag, "video main init failed with error 0x%x", ret);</font>
- <font size="3"> return ESP_FAIL;</font>
- <font size="3"> }</font>
- <font size="3"> int video_cam_fd0 = app_video_open(0); /* 打开摄像头设备 */</font>
- <font size="3"> if (video_cam_fd0 < 0)</font>
- <font size="3"> {</font>
- <font size="3"> ESP_LOGE(mipi_cam_tag, "video cam open failed");</font>
- <font size="3"> return ESP_FAIL;</font>
- <font size="3"> }</font>
- <font size="3"> /* 初始化设备并设置捕获视频格式 */</font>
- <font size="3"> ret = app_video_init(video_cam_fd0, APP_VIDEO_FMT_RGB565); </font>
- <font size="3"> if (ret != ESP_OK)</font>
- <font size="3"> {</font>
- <font size="3"> ESP_LOGE(mipi_cam_tag, "Video cam init failed with error 0x%x", ret);</font>
- <font size="3"> return ESP_FAIL;</font>
- <font size="3"> }</font>
- <font size="3"> xTaskCreatePinnedToCore(lcd_cam_task, "lcd cam display", 4096, &video_cam_fd0, 4, NULL, 0); /* 新建摄像头显示任务 */</font>
- <font size="3"> return ESP_OK;</font>
- <font size="3">}</font>
复制代码 在MIPI摄像头初始化函数中,首先调用app_video_main函数初始化摄像头硬件和软件接口接口,然后调用app_video_open函数打开摄像头设备,随后调用app_video_init函数初始化设备并设置捕获视频格式。最后新建摄像头显示任务lcd_cam_task。
接下来,看一下前面提及到的摄像头显示任务函数lcd_cam_task,代码如下。
- <font size="3">/**</font>
- <font size="3"> * @brief 摄像头显示任务</font>
- <font size="3"> * @param arg: 传入参数(未用到)</font>
- <font size="3"> * @retval 无</font>
- <font size="3"> */</font>
- <font size="3">static void lcd_cam_task(void *arg)</font>
- <font size="3">{</font>
- <font size="3"> int video_fd = *((int *)arg); /* 任务参数 */</font>
- <font size="3"> int res = 0;</font>
- <font size="3"> struct v4l2_buffer buf; /* 视频缓冲信息 */</font>
- <font size="3"> int type = V4L2_BUF_TYPE_VIDEO_CAPTURE;</font>
- <font size="3"> struct v4l2_format format = {0}; /* 数据流格式 */</font>
- <font size="3"> format.type = type; </font>
- <font size="3"> /* 获取LCD BUF(双buffer) */</font>
- <font size="3"> void *lcd_buffer[2];</font>
- <font size="3"> void *draw_buffer = NULL;</font>
- <font size="3"> size_t data_cache_line_size = 0;</font>
- <font size="3"> if (lcddev.id <= 0x7084) /* RGB屏触摸屏 */</font>
- <font size="3"> {</font>
- <font size="3"> ESP_ERROR_CHECK(esp_lcd_rgb_panel_get_frame_buffer(</font>
- <font size="3"> lcddev.lcd_panel_handle, 2, &lcd_buffer[0], &lcd_buffer[1]));</font>
- <font size="3"> }</font>
- <font size="3"> else</font>
- <font size="3"> {</font>
- <font size="3"> ESP_ERROR_CHECK(esp_lcd_dpi_panel_get_frame_buffer(</font>
- <font size="3"> lcddev.lcd_panel_handle, 2, &lcd_buffer[0], &lcd_buffer[1]));</font>
- <font size="3"> }</font>
- <font size="3"> draw_buffer = lcd_buffer[1];</font>
- <font size="3">ESP_ERROR_CHECK(esp_cache_get_alignment(MALLOC_CAP_SPIRAM,</font>
- <font size="3"> &data_cache_line_size));</font>
- <font size="3"> /* CAM BUF设置两个,使用MMAP */</font>
- <font size="3"> void *camera_outbuf[2];</font>
- <font size="3"> ESP_ERROR_CHECK(camera_set_bufs(video_fd, 2, NULL)); /* 设置帧缓存数量 */</font>
- <font size="3"> ESP_ERROR_CHECK(camera_get_bufs(2, &camera_outbuf[0])); /* 获取帧缓冲 */</font>
- <font size="3"> /*---------------PPA的设置-SRM------------------*/</font>
- <font size="3"> ppa_client_handle_t ppa_srm_handle = NULL;</font>
- <font size="3"> ppa_client_config_t ppa_srm_config = { /* 设置客户端的属性 */</font>
- <font size="3"> .oper_type = PPA_OPERATION_SRM, /* PPA操作类型 */</font>
- <font size="3"> .max_pending_trans_num = 1, /* 客户端可以持有的最大PPA事务挂起数量 */</font>
- <font size="3">};</font>
- <font size="3"> /* 注册PPA客户端 */</font>
- <font size="3"> ESP_ERROR_CHECK(ppa_register_client(&ppa_srm_config, &ppa_srm_handle)); </font>
- <font size="3"> ESP_ERROR_CHECK(camera_stream_start(video_fd)); /* 开启视频流 */</font>
- <font size="3">#if FPS_FUNC_ON == 1 /* FPS_FUNC_ON该宏 打开帧率显示功能 */</font>
- <font size="3"> int fps_count = 0;</font>
- <font size="3"> int64_t start_time = esp_timer_get_time();</font>
- <font size="3">#endif</font>
- <font size="3"> if (ioctl(video_fd, VIDIOC_G_FMT, &format) != 0) /* 获取设置支持的视频格式 */</font>
- <font size="3"> {</font>
- <font size="3"> ESP_LOGE(mipi_cam_tag, "get fmt failed");</font>
- <font size="3"> }</font>
- <font size="3"> while (1)</font>
- <font size="3">{ /* 双缓冲情况下,切换buffer */</font>
- <font size="3"> draw_buffer = lcd_buffer[0] == draw_buffer ?lcd_buffer[1]:lcd_buffer[0];</font>
- <font size="3">#if FPS_FUNC_ON == 1 /* FPS_FUNC_ON该宏 打开帧率显示功能 */</font>
- <font size="3"> fps_count++;</font>
- <font size="3"> if (fps_count == 50) {</font>
- <font size="3"> int64_t end_time = esp_timer_get_time();</font>
- <font size="3"> ESP_LOGI(mipi_cam_tag, "fps: %f", 1000000.0 / </font>
- <font size="3">((end_time - start_time) / 50.0));</font>
- <font size="3"> start_time = end_time;</font>
- <font size="3"> fps_count = 0;</font>
- <font size="3"> }</font>
- <font size="3">#endif</font>
- <font size="3"> memset(&buf, 0, sizeof(buf));</font>
- <font size="3"> buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE; /* 与前面format.type要一致 */</font>
- <font size="3"> buf.memory = V4L2_MEMORY_MMAP; /* 内存使用mmap方式 */</font>
- <font size="3"> </font>
- <font size="3">/* 将已经捕获好视频的内存拉出已捕获视频的队列 */</font>
- <font size="3"> res = ioctl(video_fd, VIDIOC_DQBUF, &buf); </font>
- <font size="3"> if (res != 0)</font>
- <font size="3"> {</font>
- <font size="3"> ESP_LOGE(mipi_cam_tag, "failed to receive video frame");</font>
- <font size="3"> }</font>
- <font size="3"> if (mipidev.id == 0x8399) /* 1080p */</font>
- <font size="3"> {</font>
- <font size="3"> ppa_srm_oper_config_t oper_config = {</font>
- <font size="3"> .in.buffer = camera_outbuf[buf.index], </font>
- <font size="3"> .in.pic_w = format.fmt.pix.width, </font>
- <font size="3"> .in.pic_h = format.fmt.pix.height, </font>
- <font size="3"> .in.block_w = format.fmt.pix.width, </font>
- <font size="3"> .in.block_h = format.fmt.pix.height, </font>
- <font size="3"> .in.block_offset_x = 0, </font>
- <font size="3"> .in.block_offset_y = 0, </font>
- <font size="3"> .in.srm_cm = PPA_SRM_COLOR_MODE_RGB565, </font>
- <font size="3"> .out.buffer = draw_buffer, </font>
- <font size="3"> .out.buffer_size = ALIGN_UP_BY(lcddev.height * lcddev.width *</font>
- <font size="3"> 16 / 8, data_cache_line_size), </font>
- <font size="3"> .out.pic_w = lcddev.width, </font>
- <font size="3"> .out.pic_h = lcddev.height, </font>
- <font size="3"> .out.block_offset_x = 60, </font>
- <font size="3"> .out.block_offset_y = 0, </font>
- <font size="3"> .out.srm_cm = PPA_SRM_COLOR_MODE_RGB565, </font>
- <font size="3"> .rotation_angle = PPA_SRM_ROTATION_ANGLE_90, </font>
- <font size="3"> .scale_x = 1.5, </font>
- <font size="3"> .scale_y = 1, </font>
- <font size="3"> .mirror_x = true, </font>
- <font size="3"> .mode = PPA_TRANS_MODE_BLOCKING, </font>
- <font size="3"> };</font>
- <font size="3"> ppa_do_scale_rotate_mirror(ppa_srm_handle, &oper_config); </font>
- <font size="3"> }</font>
- <font size="3"> else if (mipidev.id == 0x8394) /* 720p */</font>
- <font size="3"> {</font>
- <font size="3"> ppa_srm_oper_config_t oper_config = {</font>
- <font size="3"> .in.buffer = camera_outbuf[buf.index],</font>
- <font size="3"> .in.pic_w = format.fmt.pix.width, </font>
- <font size="3"> .in.pic_h = format.fmt.pix.height,</font>
- <font size="3"> .in.block_w = format.fmt.pix.width / 2,</font>
- <font size="3"> .in.block_h = format.fmt.pix.height / 2,</font>
- <font size="3"> .in.block_offset_x = 0,</font>
- <font size="3"> .in.block_offset_y = 0,</font>
- <font size="3"> .in.srm_cm = PPA_SRM_COLOR_MODE_RGB565,</font>
- <font size="3"> .out.buffer = draw_buffer,</font>
- <font size="3"> .out.buffer_size = ALIGN_UP_BY(lcddev.height * lcddev.width * </font>
- <font size="3">16 / 8, data_cache_line_size),</font>
- <font size="3"> .out.pic_w = lcddev.width,</font>
- <font size="3"> .out.pic_h = lcddev.height,</font>
- <font size="3"> .out.block_offset_x = 0,</font>
- <font size="3"> .out.block_offset_y = 0,</font>
- <font size="3"> .out.srm_cm = PPA_SRM_COLOR_MODE_RGB565,</font>
- <font size="3"> .rotation_angle = PPA_SRM_ROTATION_ANGLE_90,</font>
- <font size="3"> .scale_x = 2,</font>
- <font size="3"> .scale_y = 1.5,</font>
- <font size="3"> .mirror_x = true,</font>
- <font size="3"> .mode = PPA_TRANS_MODE_BLOCKING,</font>
- <font size="3"> };</font>
- <font size="3"> ppa_do_scale_rotate_mirror(ppa_srm_handle, &oper_config); </font>
- <font size="3"> }</font>
- <font size="3"> else if (mipidev.id == 0x9881) /* 800p */</font>
- <font size="3"> {</font>
- <font size="3"> ppa_srm_oper_config_t oper_config = {</font>
- <font size="3"> .in.buffer = camera_outbuf[buf.index],</font>
- <font size="3"> .in.pic_w = format.fmt.pix.width, </font>
- <font size="3"> .in.pic_h = format.fmt.pix.height,</font>
- <font size="3"> .in.block_w = format.fmt.pix.width,</font>
- <font size="3"> .in.block_h = format.fmt.pix.height / 2,</font>
- <font size="3"> .in.block_offset_x = 0,</font>
- <font size="3"> .in.block_offset_y = 0,</font>
- <font size="3"> .in.srm_cm = PPA_SRM_COLOR_MODE_RGB565,</font>
- <font size="3"> .out.buffer = draw_buffer,</font>
- <font size="3"> .out.buffer_size = ALIGN_UP_BY(lcddev.height * lcddev.width *</font>
- <font size="3"> 16 / 8, data_cache_line_size),</font>
- <font size="3"> .out.pic_w = lcddev.width,</font>
- <font size="3"> .out.pic_h = lcddev.height,</font>
- <font size="3"> .out.block_offset_x = 40,</font>
- <font size="3"> .out.block_offset_y = 0,</font>
- <font size="3"> .out.srm_cm = PPA_SRM_COLOR_MODE_RGB565,</font>
- <font size="3"> .rotation_angle = PPA_SRM_ROTATION_ANGLE_90,</font>
- <font size="3"> .scale_x = 1,</font>
- <font size="3"> .scale_y = 1.5,</font>
- <font size="3"> .mirror_x = true,</font>
- <font size="3"> .mode = PPA_TRANS_MODE_BLOCKING,</font>
- <font size="3"> };</font>
- <font size="3"> ppa_do_scale_rotate_mirror(ppa_srm_handle, &oper_config); </font>
- <font size="3"> }</font>
- <font size="3"> else if (lcddev.id == 0X4342)</font>
- <font size="3"> {</font>
- <font size="3"> ppa_srm_oper_config_t oper_config = {</font>
- <font size="3"> .in.buffer = camera_outbuf[buf.index],</font>
- <font size="3"> .in.pic_w = format.fmt.pix.width, </font>
- <font size="3"> .in.pic_h = format.fmt.pix.height,</font>
- <font size="3"> .in.block_w = format.fmt.pix.width / 4,</font>
- <font size="3"> .in.block_h = format.fmt.pix.height / 4, </font>
- <font size="3"> .in.block_offset_x = 0,</font>
- <font size="3"> .in.block_offset_y = 0,</font>
- <font size="3"> .in.srm_cm = PPA_SRM_COLOR_MODE_RGB565,</font>
- <font size="3"> .out.buffer = draw_buffer,</font>
- <font size="3"> .out.buffer_size = ALIGN_UP_BY(lcddev.height * lcddev.width *</font>
- <font size="3"> 16 / 8, data_cache_line_size),</font>
- <font size="3"> .out.pic_w = lcddev.width,</font>
- <font size="3"> .out.pic_h = lcddev.height,</font>
- <font size="3"> .out.block_offset_x = 0,</font>
- <font size="3"> .out.block_offset_y = 16,</font>
- <font size="3"> .out.srm_cm = PPA_SRM_COLOR_MODE_RGB565,</font>
- <font size="3"> .rotation_angle = PPA_SRM_ROTATION_ANGLE_0,</font>
- <font size="3"> .scale_x = 1.5,</font>
- <font size="3"> .scale_y = 1,</font>
- <font size="3"> .mirror_x = true,</font>
- <font size="3"> .mirror_y = true,</font>
- <font size="3"> .mode = PPA_TRANS_MODE_BLOCKING,</font>
- <font size="3"> };</font>
- <font size="3"> ppa_do_scale_rotate_mirror(ppa_srm_handle, &oper_config); </font>
- <font size="3"> }</font>
- <font size="3"> else if (lcddev.id == 0X4384) /* PCLK时钟要调整为20MHz */</font>
- <font size="3"> {</font>
- <font size="3"> ppa_srm_oper_config_t oper_config = {</font>
- <font size="3"> .in.buffer = camera_outbuf[buf.index],</font>
- <font size="3"> .in.pic_w = format.fmt.pix.width, </font>
- <font size="3"> .in.pic_h = format.fmt.pix.height,</font>
- <font size="3"> .in.block_w = format.fmt.pix.width / 2,</font>
- <font size="3"> .in.block_h = format.fmt.pix.height / 2,</font>
- <font size="3"> .in.block_offset_x = 0,</font>
- <font size="3"> .in.block_offset_y = 0,</font>
- <font size="3"> .in.srm_cm = PPA_SRM_COLOR_MODE_RGB565,</font>
- <font size="3"> .out.buffer = draw_buffer,</font>
- <font size="3"> .out.buffer_size = ALIGN_UP_BY(lcddev.height * lcddev.width * </font>
- <font size="3">16 / 8, data_cache_line_size),</font>
- <font size="3"> .out.pic_w = lcddev.width,</font>
- <font size="3"> .out.pic_h = lcddev.height,</font>
- <font size="3"> .out.block_offset_x = 0,</font>
- <font size="3"> .out.block_offset_y = 0,</font>
- <font size="3"> .out.srm_cm = PPA_SRM_COLOR_MODE_RGB565,</font>
- <font size="3"> .rotation_angle = PPA_SRM_ROTATION_ANGLE_0,</font>
- <font size="3"> .scale_x = 1.25,</font>
- <font size="3"> .scale_y = 1,</font>
- <font size="3"> .mirror_x = true,</font>
- <font size="3"> .mirror_y = true,</font>
- <font size="3"> .mode = PPA_TRANS_MODE_BLOCKING,</font>
- <font size="3"> };</font>
- <font size="3"> ppa_do_scale_rotate_mirror(ppa_srm_handle, &oper_config); </font>
- <font size="3"> }</font>
- <font size="3"> else if (lcddev.id == 0X7084) /* PCLK时钟要调整为20MHz */</font>
- <font size="3"> {</font>
- <font size="3"> ppa_srm_oper_config_t oper_config = {</font>
- <font size="3"> .in.buffer = camera_outbuf[buf.index],</font>
- <font size="3"> .in.pic_w = format.fmt.pix.width, </font>
- <font size="3"> .in.pic_h = format.fmt.pix.height,</font>
- <font size="3"> .in.block_w = format.fmt.pix.width / 2,</font>
- <font size="3"> .in.block_h = format.fmt.pix.height / 2,</font>
- <font size="3"> .in.block_offset_x = 0,</font>
- <font size="3"> .in.block_offset_y = 0,</font>
- <font size="3"> .in.srm_cm = PPA_SRM_COLOR_MODE_RGB565,</font>
- <font size="3"> .out.buffer = draw_buffer,</font>
- <font size="3"> .out.buffer_size = ALIGN_UP_BY(lcddev.height * lcddev.width * </font>
- <font size="3">16 / 8, data_cache_line_size),</font>
- <font size="3"> .out.pic_w = lcddev.width,</font>
- <font size="3"> .out.pic_h = lcddev.height,</font>
- <font size="3"> .out.block_offset_x = 0,</font>
- <font size="3"> .out.block_offset_y = 0,</font>
- <font size="3"> .out.srm_cm = PPA_SRM_COLOR_MODE_RGB565,</font>
- <font size="3"> .rotation_angle = PPA_SRM_ROTATION_ANGLE_0,</font>
- <font size="3"> .scale_x = 1.25,</font>
- <font size="3"> .scale_y = 1,</font>
- <font size="3"> .mirror_x = true,</font>
- <font size="3"> .mirror_y = true,</font>
- <font size="3"> .mode = PPA_TRANS_MODE_BLOCKING,</font>
- <font size="3"> };</font>
- <font size="3"> ppa_do_scale_rotate_mirror(ppa_srm_handle, &oper_config); </font>
- <font size="3"> }</font>
- <font size="3"> else if (lcddev.id == 0X7016) /* PCLK时钟要调整为18MHz */</font>
- <font size="3"> {</font>
- <font size="3"> ppa_srm_oper_config_t oper_config = {</font>
- <font size="3"> .in.buffer = camera_outbuf[buf.index],</font>
- <font size="3"> .in.pic_w = format.fmt.pix.width, </font>
- <font size="3"> .in.pic_h = format.fmt.pix.height,</font>
- <font size="3"> .in.block_w = format.fmt.pix.width / 2,</font>
- <font size="3"> .in.block_h = format.fmt.pix.height / 2,</font>
- <font size="3"> .in.block_offset_x = 0,</font>
- <font size="3"> .in.block_offset_y = 0,</font>
- <font size="3"> .in.srm_cm = PPA_SRM_COLOR_MODE_RGB565,</font>
- <font size="3"> .out.buffer = draw_buffer,</font>
- <font size="3"> .out.buffer_size = ALIGN_UP_BY(lcddev.height * lcddev.width * </font>
- <font size="3">16 / 8, data_cache_line_size),</font>
- <font size="3"> .out.pic_w = lcddev.width,</font>
- <font size="3"> .out.pic_h = lcddev.height,</font>
- <font size="3"> .out.block_offset_x = 32,</font>
- <font size="3"> .out.block_offset_y = 12,</font>
- <font size="3"> .out.srm_cm = PPA_SRM_COLOR_MODE_RGB565,</font>
- <font size="3"> .rotation_angle = PPA_SRM_ROTATION_ANGLE_0,</font>
- <font size="3"> .scale_x = 1.5,</font>
- <font size="3"> .scale_y = 1.2,</font>
- <font size="3"> .mirror_x = true,</font>
- <font size="3"> .mirror_y = true,</font>
- <font size="3"> .mode = PPA_TRANS_MODE_BLOCKING,</font>
- <font size="3"> };</font>
- <font size="3"> ppa_do_scale_rotate_mirror(ppa_srm_handle, &oper_config); </font>
- <font size="3"> }</font>
- <font size="3">/* 将空闲的内存加入可捕获视频的队列 */</font>
- <font size="3"> if (ioctl(video_fd, VIDIOC_QBUF, &buf) != 0) </font>
- <font size="3"> {</font>
- <font size="3"> ESP_LOGE(mipi_cam_tag, "failed to free video frame");</font>
- <font size="3"> }</font>
- <font size="3"> }</font>
- <font size="3">}</font>
复制代码 该函数的实现,主要先把LCD的缓冲区获取到,然后设置CAM的缓冲区,图像数据直接传输到LCD的缓冲区中。由于摄像头的分辨率跟屏幕分辨率不一致问题,所以得使用PPA外设进行裁剪,具体操作如上代码所示。在while循环中,便可通过调用ppa_do_scale_rotate_mirror函数去把图像直接显示出来。特别注意:若使用的是RGB屏,其PCLK需要调整,才能正常运行起来。
2. CMakeLists.txt文件
本例程的功能实现主要依靠MIPI驱动。要在main函数中,成功调用MIPI_CAM文件中的内容,就得需要修改main文件夹下的CMakeLists.txt文件,修改如下:
- <font size="3">idf_component_register(</font>
- <font size="3"> SRC_DIRS </font>
- <font size="3"> "."</font>
- <font size="3"> "APP/MIPI_CAM" </font>
- <font size="3"> INCLUDE_DIRS </font>
- <font size="3"> "."</font>
- <font size="3"> "APP/MIPI_CAM" </font>
- <font size="3"> )</font>
复制代码 3. main.c驱动代码
在main.c里面编写如下代码。
- <font size="3">void app_main(void)</font>
- <font size="3">{</font>
- <font size="3"> esp_err_t ret;</font>
- <font size="3"> ret = nvs_flash_init(); /* 初始化NVS */</font>
- <font size="3"> if(ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND)</font>
- <font size="3"> {</font>
- <font size="3"> ESP_ERROR_CHECK(nvs_flash_erase());</font>
- <font size="3"> ESP_ERROR_CHECK(nvs_flash_init());</font>
- <font size="3"> }</font>
- <font size="3"> led_init(); /* LED初始化 */</font>
- <font size="3"> myiic_init(); /* MYIIC初始化 */</font>
- <font size="3"> lcd_init(); /* LCD屏初始化 */</font>
- <font size="3"> lcd_show_string(30, 50, 200, 16, 16, "ESP32-P4", RED);</font>
- <font size="3"> lcd_show_string(30, 70, 200, 16, 16, "CAMERA TEST", RED);</font>
- <font size="3"> lcd_show_string(30, 90, 200, 16, 16, "ATOM@ALIENTEK", RED);</font>
- <font size="3"> vTaskDelay(pdMS_TO_TICKS(500));</font>
- <font size="3"> lcd_clear(WHITE);</font>
- <font size="3"> mipi_cam_init(); /* 摄像头初始化 */</font>
- <font size="3"> while (1)</font>
- <font size="3"> {</font>
- <font size="3"> LED0_TOGGLE();</font>
- <font size="3"> vTaskDelay(pdMS_TO_TICKS(500));</font>
- <font size="3"> }</font>
- <font size="3">}</font>
复制代码 app_main函数比较简单了,主要就是初始化所需外设后,直接调用mipi_cam_init函数,该函数内部会新建一个摄像头显示任务,直接就可以把摄像头数据显示在LCD上了。
37.4 下载验证
将程序下载到开发板后,可以看到LED0不停的闪烁,提示程序已经在运行了。LCD显示的内容如下图所示:
图37.4.1 摄像头实验程序运行效果图
屏幕显示了当前摄像头的图像数据。
摄像头实验我们就讲解到这里。 |