> 技术文档 > GOOUUU ESP32-S3-CAM 果云科技开发板开发指南(一)(超详细!)Vscode+espidf 通过摄像头拍摄照片并存取到SD卡中,文末附源码

GOOUUU ESP32-S3-CAM 果云科技开发板开发指南(一)(超详细!)Vscode+espidf 通过摄像头拍摄照片并存取到SD卡中,文末附源码

看到最近好玩的开源项目比较多,就想要学习一下esp32的开发,目前使用比较多的ide基本上是arduino、esp-idf和platformio,前者编译比较慢,后两者看到开源大佬的项目做的比较多,所以主要学习后两者。

本次使用的硬件是GOOUUU ESP32-S3-CAM,开发环境是espidf的5.4.1版本。

一:首先是环境配置

esp-idf和pio的环境都需要配置编译链,才能够更好的编译,我是在vscode里下载插件后,再去搭建好环境的,而由于espressif官方的库和依赖都在github上,所以在配环境上多少会遇到一些问题,大家可以自行去搜索环境配置,如果实在是解决不了的话,这里也给自己打一个小广告,咸鱼搜索flushddd,可以粉丝价帮助大家配置环境哈。

二:进入正题,本文从点灯开始,一步步深入到本文的最终目标

1.首先是放出最终的实验现象

最后的实验现象会在sd中找到我们拍到的照片,通过espidf的终端监视也可以看到拍摄到的图片被保存的信息。

三:首先开始点灯

首先是创建工程,通过idf的向导创建工程,我选择的框架是5.3.2的,虽然框架是5.3.2但是5.4.1的编译器也可以编译前者框架的代码,填写工程名称,地址,选择芯片目标,最后是选择基础框架,这里我们选择idf的sample-project

    

就是一个全新的工程,没有任何一个component的状态,只有main.c一个文件

开始点灯!

这里我们先看一下原理图,这里的led引脚是gpio43,记住这个数字,后面会用到。

如同stm32开发,想要编辑gpio口,也是要初始化一个结构体,创建代码led.c和led.h具体代码如下:

void led_init(void){ gpio_config_t led_pin_config;//定义结构体 led_pin_config.pin_bit_mask = 1<<LED_PIN; led_pin_config.mode = GPIO_MODE_OUTPUT; led_pin_config.pull_up_en = GPIO_PULLUP_DISABLE; led_pin_config.pull_down_en = GPIO_PULLDOWN_DISABLE; led_pin_config.intr_type=GPIO_INTR_DISABLE; gpio_config(&led_pin_config);}

上述为初始化代码,添加到led.c中,并且需要宏定义LED_PIN,并且添加到.h文件中。

#define LED_PIN 43

然后是控制LED的亮灭,以下就是控制LED亮灭的代码。

void led_on(void){ gpio_set_level(LED_PIN, 0);}void led_off(void){ gpio_set_level(LED_PIN, 1);}

和led.h的代码

#ifndef __LED_H#define __LED_H #include \"driver/gpio.h\"#define LED_PIN 43void led_init(void);void led_on(void);void led_off(void);#endif

做完这些后,就需要创建frertos的任务,由于espidf是使用frertos进行开发的,所以在这里我们也使用任务来点灯,当然了,在main里while循环也是可以的。

void led_blink_task(void * param){ while(1) { led_on(); vTaskDelay(pdMS_TO_TICKS(500)); led_off(); vTaskDelay(pdMS_TO_TICKS(500)); }}void app_main(void){ led_init(); xTaskCreatePinnedToCore(led_blink_task,\"led\",4096,NULL,3,NULL,1);}

以上代码就是在main.c中的 全部代码,然后点击编译和烧录,没有意外的话就会看到板子上的蓝色小灯以0.5s为间隔在一闪一闪。

 2.点灯实现了之后,接下来需要实现的是SD的读取和文件系统的挂载。

通过原理图我们可知,我们的sd卡是一个单线的,数据线只通过一个data信号线即可读取,然后是SD_CMD和SD_CLK两根线,即可完成sd卡的控制。

 具体初始化代码如下:

sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); slot_config.width=1; slot_config.clk=SD_PIN_CLK; slot_config.cmd=SD_PIN_CMD; slot_config.d0=SD_PIN_D0; slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP;

在这里面我们需要做的就是定义好引脚,通过引脚分配图我们可知,cmd是gpio38,clk是39以及Data是40,这里记得去完成一个宏定义即可。

除此之外,还需要做的是挂载文件系统,初始化代码如下:

 esp_vfs_fat_sdmmc_mount_config_t mount_config ={ .format_if_mount_failed = false, .max_files=5, .allocation_unit_size=16*1024, };

具体含义为,操作的最大文件数是5,分配的文件大小为16*1024字节,也就是16kb,

综上所述,完成初始化sd卡和挂载文件系统代码如下

 esp_vfs_fat_sdmmc_mount_config_t mount_config ={ .format_if_mount_failed = false, .max_files=5, .allocation_unit_size=16*1024, }; sdmmc_card_t *card; const char mount_point[] = MOUNT_POINT; ESP_LOGI(TAG, \"Initializing SD card\"); ESP_LOGI(TAG, \"Using SDMMC peripheral\"); sdmmc_host_t host = SDMMC_HOST_DEFAULT();sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); slot_config.width=1; slot_config.clk=SD_PIN_CLK; slot_config.cmd=SD_PIN_CMD; slot_config.d0=SD_PIN_D0; slot_config.flags |= SDMMC_SLOT_FLAG_INTERNAL_PULLUP; ESP_LOGI(TAG, \"Mounting filesystem\"); ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &card); if (ret != ESP_OK) { if (ret == ESP_FAIL) { ESP_LOGE(TAG, \"Failed to mount filesystem. \"  \"If you want the card to be formatted, set the EXAMPLE_FORMAT_IF_MOUNT_FAILED menuconfig option.\"); } else { ESP_LOGE(TAG, \"Failed to initialize the card (%s). \"  \"Make sure SD card lines have pull-up resistors in place.\", esp_err_to_name(ret)); } return; } ESP_LOGI(TAG, \"Filesystem mounted\"); sdmmc_card_print_info(stdout, card);

最后初始化完成了之后,会提示Filesystem mounted,如图所示。

 

在这里面我们也可以写一个测试代码,写入一个helloword.txt

 esp_err_t s_example_write_file(const char *path, char *data){ ESP_LOGI(TAG, \"Opening file %s\", path); FILE *f = fopen(path, \"w\"); if (f == NULL) { ESP_LOGE(TAG, \"Failed to open file for writing\"); return ESP_FAIL; } fprintf(f, data); fclose(f); ESP_LOGI(TAG, \"File written\"); return ESP_OK;} const char *file_hello = MOUNT_POINT\"/hello1.txt\"; char data[EXAMPLE_MAX_CHAR_SIZE]; snprintf(data, EXAMPLE_MAX_CHAR_SIZE, \"%s %s!\\n\", \"HelloWorld\", card->cid.name); ret = s_example_write_file(file_hello, data); if (ret != ESP_OK) { return; }

加入到app_main()函数里,编译烧录后,可以看到sd卡中出现了helloword.txt,具体内容为 helloword +你自己sd卡的名字。

3.最后,也就是camera

这里面我们需要把三者融合在一起,首先还是相机的初始化,这里先看原理图,我们这次使用的是OV2640型号的摄像头,camera的初始化一共需要14个引脚,其中有8根数据线,其余6根是信号线,然后在初始化之前,我们要打开esp32s3的psram,相机需要psram分配内存,将拍摄到的一帧图片存到psram中去。

这里我们先点开设置图样,或者通过espidf的cmd,定位到你创建工程的文件夹目录,通过命令

idf.py menuconfig也是可行的。在menuconfig里面找到ESP PSRAM并打开,勾选support for external ,SPI -connected RAM,spiram的模式就选择 quad,如果自己的板子后面在调试监视的时候报错,就更换成另外一种模式,自动检测使用SPIRAM类型,设置RAM的时钟速度为80MHZ,并且勾选Initialize SPI RAM during startup,这样就可以在初始化的过程中自己识别PSRAM的芯片类型并且在监视中显示出来了,这一步是初始化camera的关键,然后记得保存。

然后还有关键的一步,需要给现有的工程添加esp_camera的依赖库

通过idf的终端,输入指令:

idf.py add-dependency \"espressif/esp32-camera\"

即可在现有工程中添加相机库的依赖,引用esp_camera.h 

这里面为了确保psram确实已经得到了初始化,我们添加如下代码,显示如下:

 // 检测 PSRAM 存在性和大小 if(esp_psram_get_size() == 0) { ESP_LOGE(\"BOOT\", \"PSRAM NOT DETECTED! Check hardware connection.\"); vTaskDelay(pdMS_TO_TICKS(1000)); esp_restart(); // 自动重启 } ESP_LOGI(\"BOOT\", \"PSRAM Size: %d KB\", esp_psram_get_size() / 1024); // 初始化 PSRAM 缓存 esp_psram_init(); 

和芯片型号esp32s3n16r8一致,r8即代表PSRAM为8MB,

接下来就是camera的初始化,通过原理图中的引脚进行配置,一定不要把引脚输错了,代码如下:

void bsp_camera_init(void){camera_config_t cam_config = { .pin_pwdn = CAM_PIN_PWDN, .pin_reset = CAM_PIN_RESET, .pin_xclk = CAM_PIN_XCLK, .pin_sccb_scl = CAM_PIN_SIOC, .pin_sccb_sda = CAM_PIN_SIOD, .pin_d7=CAM_PIN_D7, .pin_d6=CAM_PIN_D6, .pin_d5=CAM_PIN_D5, .pin_d4=CAM_PIN_D4, .pin_d3=CAM_PIN_D3, .pin_d2=CAM_PIN_D2, .pin_d1=CAM_PIN_D1, .pin_d0=CAM_PIN_D0, .pin_vsync=CAM_PIN_VYSNC, .pin_href=CAM_PIN_HREF, .pin_pclk=CAM_PIN_PCLK, .ledc_timer = LEDC_TIMER_1, .ledc_channel = LEDC_CHANNEL_1, .xclk_freq_hz=10000000, .pixel_format = PIXFORMAT_JPEG, .frame_size = FRAMESIZE_QVGA, .jpeg_quality = 10, .fb_count = 2, .fb_location = CAMERA_FB_IN_PSRAM, .grab_mode = CAMERA_GRAB_WHEN_EMPTY, .sccb_i2c_port = 1, };esp_err_t err = esp_camera_init(&cam_config); if (err != ESP_OK) { ESP_LOGI(TAG,\"Camera init failed with error 0x%x\", err); } // 详细错误处理}

这里的初始化还需要注意一些参数分别是:.ledc_timer = LEDC_TIMER_1,
    .ledc_channel = LEDC_CHANNEL_1,
    .xclk_freq_hz=10000000,
    .pixel_format = PIXFORMAT_JPEG,
    .frame_size = FRAMESIZE_QVGA,
    .jpeg_quality = 10,
    .fb_count = 2,

分别指的是使用时钟的通道,camera的时钟频率,拍摄照片的分辨率是frame_size,照片的质量以及rgb格式等等,具体可以自己设置,最后完成初始化

在app_main进行初始化,初始化完成后,如果正确配置了这些参数, 通过监视器可以看到camera的一些信息,如图所示

做完这些后,我们就可以将前面的三者加起来,首先是初始化led作为测试用,然后是初始胡sd卡和相机,最后是拍照的代码,代码如下:

static uint32_t get_max_file_number(void){ DIR *dir = opendir(\"/sdcard\"); if (!dir) { ESP_LOGE(TAG, \"Failed to open directory\"); return 0; } uint32_t max_number = 0; struct dirent *entry; while ((entry = readdir(dir)) != NULL) { ESP_LOGI(TAG, \"Processing file: %s\", entry->d_name); // 检查文件名长度 size_t name_len = strlen(entry->d_name); if (name_len != 12) { // 改为12,因为PHOTO000.JPG是12个字符 ESP_LOGD(TAG, \"Skipping file with wrong length: %s (%d)\", entry->d_name, name_len); continue; } // 检查前缀 if (memcmp(entry->d_name, \"PHOTO\", 5) != 0) { ESP_LOGD(TAG, \"Skipping file without PHOTO prefix: %s\", entry->d_name); continue; } // 检查扩展名 if (memcmp(entry->d_name + 8, \".JPG\", 4) != 0) { ESP_LOGD(TAG, \"Skipping non-JPG file: %s\", entry->d_name); continue; } // 提取数字部分 char num_str[4] = {0}; memcpy(num_str, entry->d_name + 5, 3); uint32_t current_number; if (sscanf(num_str, \"%lu\", &current_number) == 1) { ESP_LOGI(TAG, \"Found valid photo number: %lu from file %s\", current_number, entry->d_name); if (current_number > max_number) { max_number = current_number; ESP_LOGI(TAG, \"New maximum number: %lu\", max_number); } } else { ESP_LOGW(TAG, \"Failed to parse number from: %s\", num_str); } } closedir(dir); ESP_LOGI(TAG, \"Final maximum file number found: %lu\", max_number); return max_number;}void camera_capture(){ char filename[32];  // 文件名缓冲区 camera_fb_t *fb = esp_camera_fb_get(); static uint32_t file_number = 0; // 文件编号 // 获取SD卡中最大的文件编号 file_number = get_max_file_number() + 1; ESP_LOGI(TAG, \"Starting file number: %lu\", file_number);if (!fb) { ESP_LOGE(\"CAM\", \"Failed to capture image\");} else { ESP_LOGI(\"CAM\", \"Image captured: %d bytes\", fb->len); if(fb) { snprintf(filename, sizeof(filename), \"/sdcard/PHOTO%03lu.JPG\",file_number++);  { // 分配JPEG缓冲区 uint8_t *jpeg_buf = NULL; size_t jpeg_len = 0; // 将frame转换为JPEG bool converted = frame2jpg(fb, // 输入frame  92, // JPEG质量(0-100)  &jpeg_buf, // 输出JPEG缓冲区  &jpeg_len); // 输出JPEG长度 if (!converted) {  ESP_LOGE(TAG, \"JPEG conversion failed\");  esp_camera_fb_return(fb); } else  {  FILE *file = fopen(filename, \"wb\");  if (file)  {  size_t written = fwrite(jpeg_buf, 1, jpeg_len, file);  fclose(file);  if (written == jpeg_len)  { ESP_LOGI(TAG, \"Saved %s (%d bytes)\", filename, jpeg_len);  }  else  { ESP_LOGE(TAG, \"File write failed: %d/%d bytes\", written, jpeg_len);  }  // 清理资源  free(jpeg_buf);  // 释放JPEG缓冲区  esp_camera_fb_return(fb); // 返回帧缓冲区  } } }} esp_camera_fb_return(fb);}}

 上述代码实现了首先查找sd卡中的文件数量,然后添加照片的命名,拍摄到图片后将其转成jpeg的格式,当将照片添加到sd卡中后,最后释放内存。其中拍照是通过函数esp_camera_fb_get();实现的,可惜的是板子没有可以使用的按键,否则检测按键,我们可以通过按按键来实现拍照。

最后将每个函数依次实现,编译烧录就大功告成了!

你可以看到自己拍摄到的图片出现在sd卡中了!

全部的工程如下:

通过网盘分享的文件:esp32-s3-cam
链接: https://pan.baidu.com/s/1MWgmBimOhGoUzEfFXVIDTQ?pwd=came 提取码: came 
--来自百度网盘超级会员v4的分享