【立创·实战派ESP32-S3】文档教程
第 10 章 摄像头
当前,单片机或者 CPU 连接摄像头,一般分为 DVP 接口和 MIPI 接口。一般 500W 像素以下的摄像头模块,使用 DVP 接口,以上的使用 MIPI 接口。MIPI 接口速度要高于 DVP 接口。
ESP32-S3 有 DVP 接口,可用于连接 DVP 接口的摄像头,我们的开发板上,连接的摄像头型号是 GC0308,生产厂家是格科微,一家总部在上海的公司。
GC0308 摄像头最大分辨率 640480,30W 像素。单电源 2.8V 供电,工作电流 20mA,待机电流 10uA。输出格式有 8 位裸 RGB、RGB565\555、YUV\YCbCr 4:2:2 格式。工作在 24MHz 频率下,输出 240320 分辨率时,可达 30 帧。
本例程,我们实现将摄像头画面显示到液晶屏上,摄像头输出格式设置为 RGB565,正好与开发板液晶屏所需格式一致。
10.1 使用例程
把开发板提供的【07-lcd_camera】例程复制到你的实验文件夹当中,并使用 VSCode 打开工程。
连接开发板到电脑,在 VSCode 上选择串口号,选择目标芯片为 esp32s3,串口下载方式,然后点击“一键三联”按钮,等待编译下载打开终端。
开发板先显示一张图片,然后迅速进入摄像头采集到的画面。
10.2 例程讲解
本例程是在上一章例程【06-lcd】例程的基础上加了摄像头代码后修改而来,开发板运行后,先是图片显示,再进入摄像头画面。
点击打开 main.c 文件,找到 app_main 函数。
void app_main(void)
{
bsp_i2c_init(); // I2C初始化
pca9557_init(); // IO扩展芯片初始化
bsp_lcd_init(); // 液晶屏初始化
lcd_draw_pictrue(0, 0, 320, 240, gImage_yingwu); // 显示3只鹦鹉图片
vTaskDelay(500 / portTICK_PERIOD_MS); // 延时500毫秒
bsp_camera_init(); // 摄像头初始化
app_camera_lcd(); // 让摄像头画面显示到LCD上
}
2
3
4
5
6
7
8
9
10
前 4 条语句已经在上一章讲解过,这里只讲解后面两个,摄像头初始化函数和摄像头显示函数。
bsp_camera_init() 摄像头初始化函数
// 摄像头硬件初始化
void bsp_camera_init(void)
{
dvp_pwdn(0); // 打开摄像头
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_1; // LEDC通道选择 用于生成XCLK时钟 但是S3不用
config.ledc_timer = LEDC_TIMER_0; // LEDC timer选择 用于生成XCLK时钟 但是S3不用
config.pin_d0 = CAMERA_PIN_D0;
config.pin_d1 = CAMERA_PIN_D1;
config.pin_d2 = CAMERA_PIN_D2;
config.pin_d3 = CAMERA_PIN_D3;
config.pin_d4 = CAMERA_PIN_D4;
config.pin_d5 = CAMERA_PIN_D5;
config.pin_d6 = CAMERA_PIN_D6;
config.pin_d7 = CAMERA_PIN_D7;
config.pin_xclk = CAMERA_PIN_XCLK;
config.pin_pclk = CAMERA_PIN_PCLK;
config.pin_vsync = CAMERA_PIN_VSYNC;
config.pin_href = CAMERA_PIN_HREF;
config.pin_sccb_sda = -1; // 这里写-1 表示使用已经初始化的I2C接口
config.pin_sccb_scl = CAMERA_PIN_SIOC;
config.sccb_i2c_port = 0;
config.pin_pwdn = CAMERA_PIN_PWDN;
config.pin_reset = CAMERA_PIN_RESET;
config.xclk_freq_hz = XCLK_FREQ_HZ;
config.pixel_format = PIXFORMAT_RGB565;
config.frame_size = FRAMESIZE_QVGA;
config.jpeg_quality = 12;
config.fb_count = 2;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
// camera init
esp_err_t err = esp_camera_init(&config); // 配置上面定义的参数
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Camera init failed with error 0x%x", err);
return;
}
sensor_t *s = esp_camera_sensor_get(); // 获取摄像头型号
if (s->id.PID == GC0308_PID) {
s->set_hmirror(s, 1); // 这里控制摄像头镜像 写1镜像 写0不镜像
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
PWDN 引脚控制摄像头进入待机模式和工作模式,高电平进入待机模式,低电平进入工作模式,在 pca9557 初始化的时候,PWDN 引脚是高电平,现在使用 dvp_pwdn(0)让摄像头进入工作模式。
接下来定义了 camera_config_t 型结构体变量,并给其成员赋值。 这里面大部分内容看英文字面意思就可以知道它的作用,这里介绍两处比较模糊的地方。
第 1 处:
.ledc_channel
和 .ledc_timer
这两个成员,在上一章做液晶屏背光的时候,我们已经介绍过,LEDC 外设用来给某个引脚产生 PWM 信号,这里可以用来产生时钟信号给摄像头的 XCLK 引脚。但是 S3 芯片用不着,因为 S3 芯片的 CAM 外设会产生 XCLK 信号。关于这一点,看 ESP32-S3 的技术参考手册可以了解到,同时,在程序上也可以找到相关部分。
bsp_camera_init()函数中调用了 esp_camera_init()函数,esp_camera_init()函数中调用了 camera_probe()函数,在 camera_probe()函数中,我们看到如下语句:
if (config->pin_xclk >= 0) {
ESP_LOGD(TAG, "Enabling XCLK output");
CAMERA_ENABLE_OUT_CLOCK(config);
}
2
3
4
然后我们找到 CAMERA_ENABLE_OUT_CLOCK 的宏定义,如下代码所示:
#if CONFIG_IDF_TARGET_ESP32S3 // LCD_CAM module of ESP32-S3 will generate xclk
#define CAMERA_ENABLE_OUT_CLOCK(v)
#define CAMERA_DISABLE_OUT_CLOCK()
#else
#define CAMERA_ENABLE_OUT_CLOCK(v) camera_enable_out_clock((v))
#define CAMERA_DISABLE_OUT_CLOCK() camera_disable_out_clock()
#endif
2
3
4
5
6
7
上述代码的意思,如果目标芯片是 ESP32S3,CAMERA_ENABLE_OUT_CLOCK 就是个空定义,什么都不会执行。如果目标芯片不是 ESP32S3,CAMERA_ENABLE_OUT_CLOCK 就是 camera_enable_out_clock()函数,我们可以进入到 camera_enable_out_clock()函数里面,这个函数就是用来使用 LEDC 配置时钟的。
第 2 处:
再往下,.pin_sscb_sda
成员和 .pin_sccb_scl
成员,本来需要赋值为 I2C 的 SDA 引脚和 SCL 引脚。摄像头控制芯片的寄存器与 esp32 之间使用 sccb 通信,sccb 本质上就是 i2c 通信。这里没有把 .pin_sscb_sda
成员赋值为 SDA 引脚,而是赋值为了-1,这样可以告诉初始化程序使用已经初始化过的 i2c 接口就可以,不用重新初始化新的 i2c 接口。
这个是在哪看出来的?还是依照上面“第 1 处”中的路径,camera_probe()函数里面,有下面一段代码:
if (config->pin_sccb_sda != -1) {
ESP_LOGD(TAG, "Initializing SCCB");
ret = SCCB_Init(config->pin_sccb_sda, config->pin_sccb_scl);
} else {
ESP_LOGD(TAG, "Using existing I2C port");
ret = SCCB_Use_Port(config->sccb_i2c_port);
}
2
3
4
5
6
7
上面代码的意思为,如果 pin_sccb_sda 不等于-1,就初始化 sccb 接口,否则,使用已经存在的 i2c 接口。
app_camera_lcd()摄像头显示画面函数
// 让摄像头显示到LCD
void app_camera_lcd(void)
{
xQueueLCDFrame = xQueueCreate(2, sizeof(camera_fb_t *));
xTaskCreatePinnedToCore(task_process_camera, "task_process_camera", 3 * 1024, NULL, 5, NULL, 1);
xTaskCreatePinnedToCore(task_process_lcd, "task_process_lcd", 4 * 1024, NULL, 5, NULL, 0);
}
2
3
4
5
6
7
这里创建了两个任务,一个任务是摄像头获取画面的任务,一个是液晶屏显示画面的任务。其中,还创建了一个队列信号,摄像头获取到画面,发送队列信号通知 LCD 显示。ESP32S3 是双核处理器,这两个任务,一个定义在 CPU0 上运行,一个定义在 CPU1 上运行,这样可以提高运行速度。xTaskCreatePinnedToCore()函数的最后一个参数,用来定义在哪个 CPU 上运行。
两个处理任务函数定义如下所示:
// lcd处理任务
static void task_process_lcd(void *arg)
{
camera_fb_t *frame = NULL;
while (true)
{
if (xQueueReceive(xQueueLCDFrame, &frame, portMAX_DELAY))
{
esp_lcd_panel_draw_bitmap(panel_handle, 0, 0, frame->width, frame->height, (uint16_t *)frame->buf);
esp_camera_fb_return(frame);
}
}
}
// 摄像头处理任务
static void task_process_camera(void *arg)
{
while (true)
{
camera_fb_t *frame = esp_camera_fb_get();
if (frame)
xQueueSend(xQueueLCDFrame, &frame, portMAX_DELAY);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
esp_camera_fb_get()函数用来获取一帧摄像头图像,并把获取到的一帧信息返回,这些信息里面有哪些内容,看 camera_fb_t 的定义就知道了,如下所示:
/**
* @brief Data structure of camera frame buffer
*/
typedef struct {
uint8_t * buf; /*!< Pointer to the pixel data */
size_t len; /*!< Length of the buffer in bytes */
size_t width; /*!< Width of the buffer in pixels */
size_t height; /*!< Height of the buffer in pixels */
pixformat_t format; /*!< Format of the pixel data */
struct timeval timestamp; /*!< Timestamp since boot of the first DMA buffer of the frame */
} camera_fb_t;
2
3
4
5
6
7
8
9
10
11
第 1 个成员指向获取到的图片数据内存地址,第 2 个成员表示获取到的字节数,第 3、4 个成员表示获取到的图片宽和高,第 5 个成员表示图片的格式,第 6 个参数表示获取到的时间。
获取到图片后,把获取到的图片信息 frame 发送到队列,lcd 任务接收到队列数据后,使用 esp_lcd_panel_draw_bitmap()函数把图片显示出来,esp_lcd_panel_draw_bitmap()函数在上一张显示图片和刷屏函数中都使用过,这里又用到了它。在这里,用到了 frame 的参数 width、height、buf 成员。
显示完图片后,执行 esp_camera_fb_return()函数,它的作用是返回帧缓冲区以再次使用。
10.3 例程制作过程
本章的例程,我们直接在上一章液晶显示的例程上进行修改就可以,因为我们要把摄像头采集到的图像显示到液晶屏上,液晶屏的代码已经写好,本章写摄像头相关的代码就可以了。
把上一章的例程,复制粘贴到本文件夹,然后把名称修改为 07-lcd_camera,然后在硬盘上双击进入文件夹,把不需要的文件先删除。
如上图所示,把蓝底的文件都删除。最后剩下的文件如下图所示:
使用 VSCode 打开这个工程,先把一级目录下的 CMakeLists.txt 中的文件名称修改为 lcd_camera。
project(lcd_camera)
现在我们要参考的例程是官方 esp-who 中的 human_face_detection 的 lcd 例程。路径是:
esp-who\examples\human_face_detection\lcd
esp-who 开源地址:https://github.com/espressif/esp-who
我们使用 VSCode 打开 esp-who 整个工程,然后按照上面路径依次打开,在 main 中,点击打开 app_main.cpp 文件。
这个例程是一个人脸检测的例程,lcd 显示摄像头画面后,如果有人脸,会框选人脸,并标出眼睛鼻子和嘴巴的位置。
通过它的主函数,我们可以分析出实现过程是:建立一个队列,摄像头任务采集到图像后,把图片缓存指针等信息发送到队列,AI 任务检测到有队列信号,处理图片,进行人脸检测,然后把检测后的图片合成后,把合成图片缓存指针等信息发送到队列,液晶屏任务检测有合成图片队列信号后,把合成图片显示到液晶屏上,如此往复进行。
我们这里,先不做人脸检测,我们要实现的也就是,摄像头任务采集到图片后,把图片缓存指针等信息发送到队列,液晶屏任务检测到图片队列信号后,把图片显示到液晶屏上。省去了 AI 检测任务。
接下来,我们就开始参考它的代码。
在 register_camera 函数上单击右键,选择“转到定义”,来到该函数的定义处,位于 who_camera.c 文件中。
void register_camera(const pixformat_t pixel_fromat,
const framesize_t frame_size,
const uint8_t fb_count,
const QueueHandle_t frame_o)
{
ESP_LOGI(TAG, "Camera module is %s", CAMERA_MODULE_NAME);
#if CONFIG_CAMERA_MODULE_ESP_EYE || CONFIG_CAMERA_MODULE_ESP32_CAM_BOARD
/* IO13, IO14 is designed for JTAG by default,
* to use it as generalized input,
* firstly declair it as pullup input */
gpio_config_t conf;
conf.mode = GPIO_MODE_INPUT;
conf.pull_up_en = GPIO_PULLUP_ENABLE;
conf.pull_down_en = GPIO_PULLDOWN_DISABLE;
conf.intr_type = GPIO_INTR_DISABLE;
conf.pin_bit_mask = 1LL << 13;
gpio_config(&conf);
conf.pin_bit_mask = 1LL << 14;
gpio_config(&conf);
#endif
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = CAMERA_PIN_D0;
config.pin_d1 = CAMERA_PIN_D1;
config.pin_d2 = CAMERA_PIN_D2;
config.pin_d3 = CAMERA_PIN_D3;
config.pin_d4 = CAMERA_PIN_D4;
config.pin_d5 = CAMERA_PIN_D5;
config.pin_d6 = CAMERA_PIN_D6;
config.pin_d7 = CAMERA_PIN_D7;
config.pin_xclk = CAMERA_PIN_XCLK;
config.pin_pclk = CAMERA_PIN_PCLK;
config.pin_vsync = CAMERA_PIN_VSYNC;
config.pin_href = CAMERA_PIN_HREF;
config.pin_sscb_sda = CAMERA_PIN_SIOD;
config.pin_sscb_scl = CAMERA_PIN_SIOC;
config.pin_pwdn = CAMERA_PIN_PWDN;
config.pin_reset = CAMERA_PIN_RESET;
config.xclk_freq_hz = XCLK_FREQ_HZ;
config.pixel_format = pixel_fromat;
config.frame_size = frame_size;
config.jpeg_quality = 12;
config.fb_count = fb_count;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
// camera init
esp_err_t err = esp_camera_init(&config);
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Camera init failed with error 0x%x", err);
return;
}
sensor_t *s = esp_camera_sensor_get();
if (s->id.PID == OV3660_PID || s->id.PID == OV2640_PID) {
s->set_vflip(s, 1); //flip it back
} else if (s->id.PID == GC0308_PID) {
s->set_hmirror(s, 0);
} else if (s->id.PID == GC032A_PID) {
s->set_vflip(s, 1);
}
//initial sensors are flipped vertically and colors are a bit saturated
if (s->id.PID == OV3660_PID)
{
s->set_brightness(s, 1); //up the blightness just a bit
s->set_saturation(s, -2); //lower the saturation
}
xQueueFrameO = frame_o;
xTaskCreatePinnedToCore(task_process_handler, TAG, 3 * 1024, NULL, 5, NULL, 1);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
把这个函数复制到我们例程的 esp32_s3_szp.c 文件中,放到文件最底下就可以,然后进行修改。
第一条语句,打印摄像头模块名称,这个不需要,删除即可。
接着,下面是一个条件编译,是针对乐鑫官方开发板的,我们不使用乐鑫官方开发板,把条件编译删除,同时删除“如果是乐鑫开发板”的代码。
接下来,是摄像头引脚等参数的赋值,一会儿我们要把这些宏定义写到 esp32_s3_szp.h 文件中。
在赋值之前,先使用 dvp_pwdn(0)让摄像头由待机模式进入工作模式。
dvp_pwdn(0); // 打开摄像头电源
对 config 的成员变量赋值中,我们找到 pin_sscb_sda,以及 pin_sscb_scl,这里需要把 sscb 改成 sccb,不改的话,运行没有问题,但是编译的时候会有提示。在 camera_config_t 结构体的定义中,给出了原因,以下是相关代码片段:
union {
int pin_sccb_sda; /*!< GPIO pin for camera SDA line */
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; /*!< GPIO pin for camera SCL line */
int pin_sscb_scl __attribute__((deprecated("please use pin_sccb_scl instead"))); /*!< GPIO pin for camera SCL line (legacy name) */
};
2
3
4
5
6
7
8
然后把 pin_sccb_sda 的赋值,修改为-1,修改成-1 后,摄像头的配置,会使用前面已经初始化的 I2C 口(第 2 小节给出了这个原因)。
然后,在 pin_sscb_scl 赋值的下一条语句,加一条 sccb_i2c_port 的配置,赋值为 0。
接着,往下找到 esp_camera_sensor_get 这个函数,这个函数用来获取摄像头型号。把这个函数下面的 if 语句,是对不同型号摄像头进行的处理。set_vflip 函数是垂直方向翻转处理,set_hmirror 函数是水平方向的翻转处理,是否在水平和垂直方向翻转,取决于你的硬件设计,例如摄像头位置,液晶屏位置。在实战派 ESP32-S3 开发板上,垂直方向不用翻转,水平方向翻转就可以。
再往下,如果摄像头型号是 OV3660,设置它的亮度和饱和度。这个我们不需要,删除即可。
接下来,我们修改以下该函数的名称和参数。
为了和我们 lcd 的初始化函数相呼应,把函数名称改成 bsp_camera_init。
第 1 个参数,用来配置像素格式,这里我们直接给 config
的 .pixel_format
成员赋值为 PIXFORMAT_RGB565
,然后把第 1 个参数就删除。
第 2 个参数,用来配置帧大小,这里我们直接给 config
的 .frame_size
成员赋值为 FRAMESIZE_QVGA
,然后把第 2 个参数也删了。FRAMESIZE_QVGA
就是 320*240 大小。
第 3 个参数,用来分配多少个帧缓存,这里直接给 config
的 .fb_count
成员赋值为 2,然后把第 3 个参数也删除了。
第 4 个参数,用来传递队列句柄,这里不需要传递,也删除了。
然后把函数的最后两条语句也删除了,就是赋值队列句柄和创建任务的这两条。
最后,修改之后的代码如下所示:
// 摄像头硬件初始化
void bsp_camera_init(void)
{
dvp_pwdn(0); // 打开摄像头
camera_config_t config;
config.ledc_channel = LEDC_CHANNEL_1; // LEDC通道选择 用于生成XCLK时钟 但是S3不用
config.ledc_timer = LEDC_TIMER_1; // LEDC timer选择 用于生成XCLK时钟 但是S3不用
config.pin_d0 = CAMERA_PIN_D0;
config.pin_d1 = CAMERA_PIN_D1;
config.pin_d2 = CAMERA_PIN_D2;
config.pin_d3 = CAMERA_PIN_D3;
config.pin_d4 = CAMERA_PIN_D4;
config.pin_d5 = CAMERA_PIN_D5;
config.pin_d6 = CAMERA_PIN_D6;
config.pin_d7 = CAMERA_PIN_D7;
config.pin_xclk = CAMERA_PIN_XCLK;
config.pin_pclk = CAMERA_PIN_PCLK;
config.pin_vsync = CAMERA_PIN_VSYNC;
config.pin_href = CAMERA_PIN_HREF;
config.pin_sccb_sda = -1; // 这里写-1 表示使用已经初始化的I2C接口
config.pin_sccb_scl = CAMERA_PIN_SIOC;
config.sccb_i2c_port = 0;
config.pin_pwdn = CAMERA_PIN_PWDN;
config.pin_reset = CAMERA_PIN_RESET;
config.xclk_freq_hz = XCLK_FREQ_HZ;
config.pixel_format = PIXFORMAT_RGB565;
config.frame_size = FRAMESIZE_QVGA;
config.jpeg_quality = 12;
config.fb_count = 2;
config.fb_location = CAMERA_FB_IN_PSRAM;
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
// camera init
esp_err_t err = esp_camera_init(&config); // 配置上面定义的参数
if (err != ESP_OK)
{
ESP_LOGE(TAG, "Camera init failed with error 0x%x", err);
return;
}
sensor_t *s = esp_camera_sensor_get(); // 获取摄像头型号
if (s->id.PID == GC0308_PID) {
s->set_hmirror(s, 1); // 这里控制摄像头镜像 写1镜像 写0不镜像
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
人脸识别例程中建立了两个任务,分别位于 who_lcd.c 文件和 who_camera.c 文件中。现在我们把这两个任务分别复制到 esp32_s3_szp.c 文件中,放到刚才的摄像头初始化函数后面就行,如下所示:
static void task_process_handler(void *arg)
{
camera_fb_t *frame = NULL;
while (true)
{
if (xQueueReceive(xQueueFrameI, &frame, portMAX_DELAY))
{
esp_lcd_panel_draw_bitmap(panel_handle, 0, 0, frame->width, frame->height, (uint16_t *)frame->buf);
if (xQueueFrameO)
{
xQueueSend(xQueueFrameO, &frame, portMAX_DELAY);
}
else if (gReturnFB)
{
esp_camera_fb_return(frame);
}
else
{
free(frame);
}
}
}
}
static void task_process_handler(void *arg)
{
while (true)
{
camera_fb_t *frame = esp_camera_fb_get();
if (frame)
xQueueSend(xQueueFrameO, &frame, portMAX_DELAY);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
这两个函数名字都一样,我们把 lcd 的任务函数名称修改为 task_process_lcd,把 camera 的任务函数名称修改为 task_process_camera。
然后修改队列句柄名称,我们使用 xQueueLCDFrame 作为队列句柄名称。把 camera 的 xQueueFrameO 修改为 xQueueLCDFrame,camera 的任务函数就都修改完了。
再看 lcd 的任务函数,进入 while 的第 1 个 if 中,把 xQueueFrameI 修改为 xQueueLCDFrame。后面语句中的 esp_camera_fb_return(frame)留下,其余全部删除。
最后的结果就是:
// lcd处理任务
static void task_process_lcd(void *arg)
{
camera_fb_t *frame = NULL;
while (true)
{
if (xQueueReceive(xQueueLCDFrame, &frame, portMAX_DELAY))
{
esp_lcd_panel_draw_bitmap(panel_handle, 0, 0, frame->width, frame->height, (uint16_t *)frame->buf);
esp_camera_fb_return(frame);
}
}
}
// 摄像头处理任务
static void task_process_camera(void *arg)
{
while (true)
{
camera_fb_t *frame = esp_camera_fb_get();
if (frame)
xQueueSend(xQueueLCDFrame, &frame, portMAX_DELAY);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
任务函数写好了,创建任务的语句还没有写,现在再写一个创建任务的函数。
// 让摄像头显示到LCD
void app_camera_lcd(void)
{
xQueueLCDFrame = xQueueCreate(2, sizeof(camera_fb_t *));
xTaskCreatePinnedToCore(task_process_camera, "task_process_camera", 3 * 1024, NULL, 5, NULL, 1);
xTaskCreatePinnedToCore(task_process_lcd, "task_process_lcd", 4 * 1024, NULL, 5, NULL, 0);
}
2
3
4
5
6
7
这个函数中,先创建了队列句柄,然后分别创建了 2 个任务,且把两个任务分别分配到了 CPU0 和 CPU1 运行。
这个队列句柄,还需要定义一下,放到 esp32_s3_szp.c 文件中的 bsp_camera_init()函数前面就行。
// 定义lcd显示队列句柄
static QueueHandle_t xQueueLCDFrame = NULL;
2
点击打开 who_camera.h 文件,头文件下面,是各种乐鑫开发板的摄像头引脚配置定义,我们随便复制一组,粘贴到 esp32_s3_szp.h 文件中,然后依照原理图,修改一下引脚序号,修改后的引脚定义如下:
#define CAMERA_PIN_PWDN -1
#define CAMERA_PIN_RESET -1
#define CAMERA_PIN_XCLK 5
#define CAMERA_PIN_SIOD 1
#define CAMERA_PIN_SIOC 2
#define CAMERA_PIN_D7 9
#define CAMERA_PIN_D6 4
#define CAMERA_PIN_D5 6
#define CAMERA_PIN_D4 15
#define CAMERA_PIN_D3 17
#define CAMERA_PIN_D2 8
#define CAMERA_PIN_D1 18
#define CAMERA_PIN_D0 16
#define CAMERA_PIN_VSYNC 3
#define CAMERA_PIN_HREF 46
#define CAMERA_PIN_PCLK 7
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
接下来,who_camera.h 文件中的第 195 行定义摄像头主频大小,我们把这一行也复制到 esp32_s3_szp.h 文件中,然后修改频率为 24000000,让摄像头工作在最大主频状态下。
#define XCLK_FREQ_HZ 24000000
再接下来,把 bsp_camera_init()函数和 app_camera_lcd()函数声明到 esp32_s3_szp.h 文件中。
void bsp_camera_init(void);
void app_camera_lcd(void);
2
在 esp32_s3_szp.h 文件中,包含 esp_camera 头文件。
#include "esp_camera.h"
这个头文件,是 esp32-camera 组件的头文件。
我们打开乐鑫组件官网。
在搜索框输入 esp32-camera,点击搜索结果进入,在网页的右边,点击复制按钮,如下图所示:
回到我们的例程工程中,点击“终端”按钮,进入终端,在火焰按钮的旁边。
在终端中单击右键,刚才复制的命令,就自动粘贴到终端了,然后点击回车,执行命令。
这时,你会发现,main 文件夹下,自动增加了一个 idf_component.yml 文件,可以点击打开查看其内容。
回到 main.c 文件中,在主函数的后面增加调用 bsp_camera_init()函数和 app_camera_lcd()函数。
void app_main(void)
{
bsp_i2c_init(); // I2C初始化
pca9557_init(); // IO扩展芯片初始化
bsp_lcd_init(); // 液晶屏初始化
app_lcd_draw_yingwu(); // 显示图片
bsp_camera_init(); // 摄像头初始化
app_camera_lcd(); // 让摄像头画面显示到LCD上
}
2
3
4
5
6
7
8
9
10
现在就可以编译下载看结果了。
先选择目标芯片,再配置 menuconfig,下面说一下 menuconfig 中的配置。
FLASH 大小修改为 16MB,这里就不贴图了。
打开 PSRAM,并设置为 8 线,80MHz。
设置数据缓存指令。
设置 CPU 频率为 240MHz。
设置完后保存关闭文件。
选择好串口号、下载方式,就可以点击火焰图标一键三联了。正常情况下,不会有错误发生。下载后,即可看到液晶屏显示了摄像头画面。
最后使用 idf.py save-defconfig 命令生成 sdkconfig.defaults 文件,此文件保存了你在 menuconfig 中做的所有改动配置,不包含默认的配置。