Arduino 入门学习笔记(二十三):CAMERA 实验

开发板:正点原子ESP32S3
外设:OV2640或OV5640 LCD屏
例程源码在文章顶部可免费下载

1. 摄像头介绍

1.1 摄像头模块介绍

正点原子 OV2640 和 OV5640 摄像头如下图所示:
在这里插入图片描述
OV2640 和 OV5640 都是 OV(OmniVision)公司生产的 1/4 寸的 CMOS 图像传感器。摄像头模块除了采用 OV 公司的传感器外,还集成了有源晶振和 LDO,接口简单,使用非常方便。
OV2640 的特点有:

  • 支持各种尺寸输出。 QSXGA(OV5640 最大支持)、 UXGA(OV2640 最大支持)、 SXGA、SVGA、 VGA、 QVGA、 CIF 到 40*30 尺寸。
  • 支持多种格式输出。 YCbCr、 RGB(565/555/444)、 RawRGB 和压缩图像(JPEG)格式等。
  • 支持自动图像控制功能。 曝光控制、自动增益控制、自动白平衡、自动消除灯光条纹、自动黑电平校准等自动控制功能。
  • 支持图像质量控制。色饱和度调节、色调调节、 gamma 校准、锐度和镜头校准等。
  • 支持图像设置。图像缩放、平移和窗口设置,支持图像压缩,可输出 JPEG 图像数据。
  • 支持多种输出接口。 DVP 输出接口, OV5640 还支持 MIPI 接口。

摄像头内部工作流程简图如图所示:
在这里插入图片描述
摄像头模块对外接口引脚如下表所示:
在这里插入图片描述
结合图表进行查看,可知 OV_SCL 和 OV_SDA 是对摄像头传感器进行配置的,而 OV_D[7:0]、 OV_RCLK、 OV_VSYNC 和 OV_HREF 是摄像头图像输出相关引脚,而 OV_PWDN 和 OV_RESET 是摄像头执行某些操作固定引脚。
接下来,看一下摄像头行像素输出时序,如下图所示:
在这里插入图片描述
如上图所示, 当行同步信号 HREF 为高电平时,表示数据端口的数据有效。此时,每输出一个像素时钟 PCLK,就输出一个数据。 按照这个时序去读取摄像头数据,就可以得到图像数据。

1.2 ESP32S3 摄像头接口介绍

ESP32S3 的 LCD_CAM 控制器包含独立的 LCD 模块和 Camera 模块。 LCD 模块用于发送并行视频数据信号,其总线支持 RGB、 MOTO6800 和 8080 等接口时序。 Camera 模块用于接收并行视频数据信号,其总线支持 DVP 8-16bit 模式, 支持的时钟频率小于 40MHz, 支持 RGB565、YUV422、 YUV420、 YUV411 之间的相互转换。
本实验主要就是使用到 ESP32S3 的 LCD_CAM 控制器中的 Camera 模块。在 arduino-esp32库中有对应的函数接口供我们使用 Camera 模块了,在这里主要讲一下 camera 相关结构体和函数的使用方法。
首先介绍一下 camera_config_t 结构体类型,该结构体用于初始化 camera,代码如下:

typedef struct {
	int pin_pwdn; /* 断电引脚 */
	int pin_reset; /* 复位引脚 */
	int pin_xclk; /* 外部时钟脚 */
	union {
		int pin_sccb_sda; /* SCCB 数据线 */
		int pin_sscb_sda __attribute__((deprecated("please use pin_sccb_sda
		instead"))); /*!< GPIO pin for camera SDA line (legacy name) */
	};
	union {
		int pin_sccb_scl; /* SCCB 时钟线 */
		int pin_sscb_scl __attribute__((deprecated("please use pin_sccb_scl
		instead"))); /*!< GPIO pin for camera SCL line (legacy name) */
	};
	int pin_d7; /* 数据线 7 */
	int pin_d6; /* 数据线 6 */
	int pin_d5; /* 数据线 5 */
	int pin_d4; /* 数据线 4 */
	int pin_d3; /* 数据线 3 */
	int pin_d2; /* 数据线 2 */
	int pin_d1; /* 数据线 1 */
	int pin_d0; /* 数据线 0 */
	int pin_vsync; /* 垂直同步脚 */
	int pin_href; /* 水平同步脚 */
	int pin_pclk; /* 像素时钟脚 */
	int xclk_freq_hz; /* 外部时钟频率 */
	ledc_timer_t ledc_timer; /* 产生 XCLK 时钟的定时器 */
	ledc_channel_t ledc_channel; /* 产生 XCLK 时钟通道 */
	pixformat_t pixel_format; /* 图像格式 */
	framesize_t frame_size; /* 图像大小 */
	int jpeg_quality; /* JPEG 图像画质 */
	size_t fb_count; /* 图像缓冲区数量 */
	camera_fb_location_t fb_location; /* 摄像头图像缓冲区存放位置 */
	camera_grab_mode_t grab_mode; /* 相机初始化的配置结构 */
#if CONFIG_CAMERA_CONVERTER_ENABLED
	camera_conv_mode_t conv_mode; /*!< RGB<->YUV Conversion mode */
#endif
	int sccb_i2c_port; /* SCCB 接口用到 IIC 端口 */
} camera_config_t;

定义一个 camera_config_t 结构体类型的变量,结合硬件连接情况以及摄像头情况,对其结构体成员赋值,然后调用 esp_camera_init 函数进行初始化即可。
接下来,介绍一下 esp_camera_init 函数,定义如下:

esp_err_t esp_camera_init(const camera_config_t* config);

参数为 config 即摄像头配置结构体指针;
返回值: 0 为成功, 1 为失败。
初始化摄像头成功后,就可以通过 esp_camera_fb_get 函数获取图像数据,定义如下:

camera_fb_t* esp_camera_fb_get(void);

无参数;
返回值为 camera_fb_t 结构体类型指针,定义如下:

typedef struct {
	uint8_t * buf; /* 指向像素数据指针 */
	size_t len; /* 缓冲数据长度 */
	size_t width; /* 像素数据的宽度 */
	size_t height; /* 像素数据的高度 */
	pixformat_t format; /* 像素数据的格式 */
	struct timeval timestamp; /* 启动第一个 DMA 缓冲区开始的时间戳 */
} camera_fb_t;

在这里,我们主要使用到的是 buf 和 len 两个结构体成员,结合这两者,便可以从 buf 缓冲
区中取出 len 个字节数据,这就是摄像头的图像数据。
清除缓冲区的函数为 esp_camera_fb_return,该函数比较简单,可以直接看源码,这里就不多说了。
在实验中,还用到了 sensor_t 结构体类型以及 esp_camera_sensor_get 函数,这两个主要是获取摄像头信息以及可配置摄像头参数作用。

2. 硬件设计

2.1 例程功能

程序下载完成,摄像头的图像数据在 LCD 显示屏上显示。

2.2 硬件资源

  • LED 灯
    LED-IO1
  • USART0
    U0TXD-IO43
    U0RXD-IO44
  • XL9555
    IIC_SDA-IO41
    IIC_SCL-IO42
  • SPILCD
    CS-IO21
    SCK-IO12
    SDA-IO11
    DC-IO40(在 P5 端口,使用跳线帽将 IO_SET 和 LCD_DC 相连)
    PWR- IO1_3(XL9555)
    RST- IO1_2(XL9555)
  • CAMERA
    OV_SCL-IO38
    OV_SDA- IO39
    VSYNC- IO47
    HREF- IO48
    PCLK- IO45
    D0- IO4
    D1- IO5
    D2- IO6
    D3- IO7
    D4- IO15
    D5- IO16
    D6- IO17
    D7- IO18
    RESET-IO0_5(XL9555)
    PWDN-IO0_4(XL9555)

2.3 原理图

摄像头原理图,如下图所示:
在这里插入图片描述

3. 软件设计

3.1 程序流程图

下面看看本实验的程序流程图:
在这里插入图片描述

3.2 程序解析

CAMERA 驱动代码
这里我们只讲解核心代码,详细的源码请大家参考光盘本实验对应源码。 CAMERA 驱动源码包括两个文件: camera.cpp 和 camera.h。
下面我们先解析 camera.h 的程序。 对 CAMERA 的相关引脚做了定义。

在这里插入代码片
#define OV_SCL_PIN 38
#define OV_SDA_PIN 39
#define OV_D0_PIN 4
#define OV_D1_PIN 5
#define OV_D2_PIN 6
#define OV_D3_PIN 7
#define OV_D4_PIN 15
#define OV_D5_PIN 16
#define OV_D6_PIN 17
#define OV_D7_PIN 18
#define OV_VSYNC_PIN 47
#define OV_HREF_PIN 48
#define OV_PCLK_PIN 45
#define OV_XCLK_PIN -1
#define OV_RESET_PIN -1
#define OV_PWDN_PIN -1

这里查看一下原理图就清楚了引脚的具体 IO 口了, OV_RESET 和OV_PWDN是在 xl9555.h中已经有定义了,所以这里不再定义。
下面我们解析 camera.cpp 的程序,首先先来看一下初始化函数 camera_init,代码如下:

/**
* @brief 摄像头(OV5640 / OV2640)初始化
* @param 无
* @retval 0:表示初始化成功 / 1:表示失败
*/
uint8_t camera_init(void)
{
	camera_config_t camera_config;
	camera_config.ledc_channel = LEDC_CHANNEL_0; /* 产生 XCLK 时钟通道 */
	camera_config.ledc_timer = LEDC_TIMER_0; /* 产生 XCLK 时钟的定时器 */
	camera_config.xclk_freq_hz = 20000000; /* 设定外部时钟频率 20M */
	camera_config.pin_d7 = OV_D7_PIN; /* 数据线 7 */
	camera_config.pin_d6 = OV_D6_PIN; /* 数据线 6 */
	camera_config.pin_d5 = OV_D5_PIN; /* 数据线 5 */
	camera_config.pin_d4 = OV_D4_PIN; /* 数据线 4 */
	camera_config.pin_d3 = OV_D3_PIN; /* 数据线 3 */
	camera_config.pin_d2 = OV_D2_PIN; /* 数据线 2 */
	camera_config.pin_d1 = OV_D1_PIN; /* 数据线 1 */
	camera_config.pin_d0 = OV_D0_PIN; /* 数据线 0 */
	camera_config.pin_xclk = OV_XCLK_PIN; /* 外部时钟脚 */
	camera_config.pin_pclk = OV_PCLK_PIN; /* 像素时钟脚 */
	camera_config.pin_vsync = OV_VSYNC_PIN; /* 垂直同步脚 */
	camera_config.pin_href = OV_HREF_PIN; /* 水平同步脚 */
	camera_config.pin_sscb_sda = OV_SDA_PIN; /* SCCB 数据线 */
	camera_config.pin_sscb_scl = OV_SCL_PIN; /* SCCB 时钟线 */
	camera_config.pin_pwdn = OV_PWDN_PIN; /* 断电引脚 */
	camera_config.pin_reset = OV_RESET_PIN; /* 复位引脚 */
	camera_config.frame_size = FRAMESIZE_QVGA; /* 图像大小 */
	camera_config.pixel_format = PIXFORMAT_RGB565; /* 设置图像格式 */
	camera_config.grab_mode = CAMERA_GRAB_WHEN_EMPTY; /* 相机初始化的配置结构 */
	camera_config.fb_location = CAMERA_FB_IN_PSRAM; /* 摄像头图像缓冲存放位置 */
	camera_config.jpeg_quality = 12; /* 设置 JPEG 图像画质(0~63,数字越低画质越高)*/
	camera_config.fb_count = 1; /* 图像缓冲区数量 */
	if (spilcd_dir == 0) /* 竖屏情况下,只能显示 240*240 分辨率的图像 */
	{
		camera_config.frame_size = FRAMESIZE_240X240;
	}
	if (OV_PWDN_PIN == -1) /* 摄像头上电 */
	{
		xl9555_io_config(OV_PWDN, IO_SET_OUTPUT); /* PWDN 引脚用了 XL9555 的 IO */
		xl9555_pin_set(OV_PWDN, IO_SET_LOW); /* 上电 */
	}
	if (OV_RESET_PIN == -1) /* 硬件复位 */
	{
		xl9555_io_config(OV_RESET, IO_SET_OUTPUT); /* RESET 引脚用了 XL9555 的 IO */
		xl9555_pin_set(OV_RESET, IO_SET_LOW);
		delay(20);
		xl9555_pin_set(OV_RESET, IO_SET_HIGH);
		delay(20);
	}
	esp_err_t err = esp_camera_init(&camera_config); /* 摄像头初始化 */
	if (err != ESP_OK)
	{
		Serial.printf("摄像头初始化失败,错误码:0x%x", err);
		return 1;
	}
	sensor_t * s = esp_camera_sensor_get(); /* 获取摄像头信息 */
	Serial.printf("摄像头 ID:%#x \r\n", s->id.PID); /* 打印摄像头 ID */
	s->set_brightness(s, 0); /* 设置亮度 (-2 ~ 2) */
	s->set_contrast(s, 0); /* 设置对比度 (-2 ~ 2) */
	s->set_saturation(s, 0); /* 设置饱和度 (-2 ~ 2) */
	s->set_hmirror(s, 0); /* 不设置水平方向翻转 */
	s->set_vflip(s, 1); /* 设置垂直方向翻转 */
	if (s->id.PID == OV2640_PID)
	{
		s->set_vflip(s, 0); /* 不设置垂直方向翻转 */
	}
	return 0;
}

在 CAMERA 初始化函数中, 要对摄像头配置结构体 camera_config 的成员进行赋值,然后调用 esp_camera_init 函数进行初始化,后面再调用 esp_camera_sensor_get 获取摄像头信息。
注意:由于摄像头输出的图像方向与 LCD 的方向有点差异,所以通过 vflip 函数设置垂直方向翻转。
下面介绍的是 LCD 显示摄像头捕获数据的函数,定义如下:

/**
* @brief LCD 显示摄像头捕获数据
* @param 无
* @retval 0:成功 / 1:画面获取有问题
*/
uint8_t camera_capture_show(void)
{
	fb = esp_camera_fb_get(); /* 捕获一帧图像数据 */
	if (!fb)
	{
		Serial.printf("无法获得图像数据 \r\n");
		return 1;
	}
	if (spilcd_dir == 1) /* 横屏情况下,显示 320*240 分辨率的图像 */
	{
		lcd_show_pic(0, 0, 320, 240, fb->buf); /* 画面全屏显示 */
	}
	else /* 竖屏情况下,显示 240*240 分辨率的图像 */
	{
		lcd_show_pic(0, 39, 240, 240, fb->buf); /* 画面居中显示 */
	}
	esp_camera_fb_return(fb); /* 清除摄像头缓存 */
	fb = NULL;
	return 0;
}

该函数通过调用 esp_camera_fb_get 函数获取一帧图像数据,然后调用 lcd_show_pic 函数进行显示。
20_camera.ino 代码
在 20_camera.ino 里面编写如下代码:

#include "uart.h"
#include "led.h"
#include "led.h"
#include "uart.h"
#include "xl9555.h"
#include "spilcd.h"
#include "camera.h
/**
* @brief 当程序开始执行时,将调用 setup()函数,通常用来初始化变量、函数等
* @param 无
* @retval 无
*/
void setup()
{
	led_init(); /* LED 初始化 */
	uart_init(0, 115200); /* 串口 0 初始化 */
	xl9555_init(); /* IO 扩展芯片初始化 */
	lcd_init(); /* LCD 初始化 */
	lcd_show_string(30, 50, 200, 16, LCD_FONT_16, "ESP32-S3", RED);
	lcd_show_string(30, 70, 200, 16, LCD_FONT_16, "CAMERA TEST", RED);
	lcd_show_string(30, 90, 200, 16, LCD_FONT_16, "ATOM@ALIENTEK", RED);
	while (camera_init()) /* 摄像头初始化 */
	{
		lcd_show_string(30,110,200,16, LCD_FONT_16, "Please check camera!", RED);
		delay(200);
		lcd_fill(30, 110, 200, 126, WHITE);
		delay(200);
	}
}
/**
* @brief 循环函数,通常放程序的主体或者需要不断刷新的语句
* @param 无
* @retval 无
*/
void loop()
{
	camera_capture_show(); /* 在 LCD 显示摄像头捕获的数据 */
}

在 setup 函数中,调用 led_init 函数完成 LED 初始化,调用 uart_init 函数完成串口初始化,调用 xl9555_init 函数完成 XL9555 初始化,调用 lcd_init 函数完成 LCD 屏初始化, LCD 显示实验信息,调用 camera_init 函数完成摄像头初始化。若调用 camera_init 函数无法完成初始化,
LCD 显示检测信息。在 loop 函数中,调用 camera_capture_show 函数实现 LCD 显示摄像头数据。

4. 下载验证

假定摄像头已经接上去正确的位置,将程序下载到开发板后, LCD 显示当前摄像头拍摄的内容如下图所示:
在这里插入图片描述

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐