序章
本文章是完成esp32s3驱动摄像头拍照储存sd卡,其代码逻辑是按键读取低电平(低电平驱动)拍一照片并存储进sd卡 下面是硬件和软件的实现
目录
1.硬件准备
1.1芯片选择
我使用的是esp32s3的芯片因为相比于8266来说,它具有8MPSRAM拍出分辨率更高的照片,用过8266的cam的都知道虽然它的生态好教程多但是模糊,因为它默认的分辨率是SVGA=800*600(当然是能支持一些应用如人脸识别)。
1.2摄像头
一、OV5640 摄像头介绍
OV5640_V5( V5 是版本号,下面均以 OV5640 表示该产品)是芯路恒科技推出的一款高性能 500W 像素高清摄像头模块。该模块采用 OmniVision 公司生产的一颗 1/4 英寸CMOS QSXGA( 25921944)图像传感器 OV5640,配合高质量的光学镜头及为实现更高性能而精心设计的 PCBA,使该模块拥有了尽可能高的成像质量。OV5640 模块的特点如下:
1.4μm1.4μm 像素大小,并且使用 OmniBSI 技术以达到更高性能(高灵敏度、
低串扰和低噪声)
自动图像控制功能:自动曝光( AEC)、自动白平衡( AWB)、自动消除灯光条纹、
自动黑电平校准( ABLC)和自动带通滤波器( ABF)等。
支持图像质量控制:色饱和度调节、色调调节、 gamma 校准、锐度和镜头校准等
标准的 SCCB 接口,兼容 IIC 接口
支持 RawRGB、 RGB(RGB565/RGB555/RGB444)、 CCIR656、 YUV(422/420)、 YCbCr
( 422)和压缩图像( JPEG)输出格式
支持 QSXGA( 500W)图像尺寸输出,以及按比例缩小到其他任何尺寸
支持图像缩放、平移和窗口设置
支持图像压缩,即可输出 JPEG 图像数据
支持数字视频接口( DVP)
1.3sd卡
2.代码
2.0开发软件和所使用的库
我使用的是vscode开发,因为里面有封装的espcam库和处理sd卡的软件抽象层的库
2.1头文件
2.1.1SD
2.1.1.1 sd头文件
这里需要注意的是我采用的是三线spi,也就是不要片选,其实大多时候使用spi是因为硬件本身需要用spi而非是真的用到了一对多的spi通讯,所以留出一个宝贵的的引脚还是很有必要的(没办法esp系列芯片的引脚确实不够用),当然如果你的pcb打开了cs引脚但不需要用到一对多那你只需要cs上拉处理即可(因为cs低电平有效)
#ifndef __SD_READ_WRITE_H
#define __SD_READ_WRITE_H
#include "Arduino.h"
#include "FS.h" //处理sd卡软件抽象层
#include "SD_MMC.h" //sd卡的读写库
//引脚定义这里用的是硬件spi 根据自己的具体所选的spi进行接线
#define SD_MMC_CMD 38 //据需求更改
#define SD_MMC_CLK 39 //据需求更改
#define SD_MMC_D0 40 //据需求更改
// 列出指定目录下的文件和子目录
// 参数 fs: 文件系统对象
// 参数 dirname: 要列出的目录名
// 参数 levels: 要递归列出的子目录层级
void listDir(fs::FS &fs, const char * dirname, uint8_t levels);
// 创建一个新的目录
// 参数 fs: 文件系统对象
// 参数 path: 要创建的目录路径
void createDir(fs::FS &fs, const char * path);
// 删除一个目录及其所有内容
// 参数 fs: 文件系统对象
// 参数 path: 要删除的目录路径
void removeDir(fs::FS &fs, const char * path);
// 读取并显示文件内容
// 参数 fs: 文件系统对象
// 参数 path: 要读取的文件路径
void readFile(fs::FS &fs, const char * path);
// 写入内容到文件,如果文件不存在则创建
// 参数 fs: 文件系统对象
// 参数 path: 要写入的文件路径
// 参数 message: 要写入文件的内容
void writeFile(fs::FS &fs, const char * path, const char * message);
// 在文件的末尾追加内容
// 参数 fs: 文件系统对象
// 参数 path: 要追加内容的文件路径
// 参数 message: 要追加的内容
void appendFile(fs::FS &fs, const char * path, const char * message);
// 重命名一个文件或目录
// 参数 fs: 文件系统对象
// 参数 path1: 原始文件或目录路径
// 参数 path2: 新的文件或目录路径
void renameFile(fs::FS &fs, const char * path1, const char * path2);
// 删除一个文件
// 参数 fs: 文件系统对象
// 参数 path: 要删除的文件路径
void deleteFile(fs::FS &fs, const char * path);
// 测试文件系统的读写性能
// 参数 fs: 文件系统对象
// 参数 path: 用于测试的文件路径
void testFileIO(fs::FS &fs, const char * path);
// 将JPEG图片数据写入文件
// 参数 fs: 文件系统对象
// 参数 path: 要写入的文件路径
// 参数 buf: JPEG图片数据的缓冲区
// 参数 size: JPEG图片数据的大小
void writejpg(fs::FS &fs, const char * path, const uint8_t *buf, size_t size);
// 读取指定目录下的文件数量
// 参数 fs: 文件系统对象
// 参数 dirname: 要读取文件数量的目录名
// 返回值: 目录下的文件数量
int readFileNum(fs::FS &fs, const char * dirname);
#endif
2.1.1.2 sdcpp文件
#include "sd_read_write.h"
//初始化sd卡
void sdmmcInit(void){
SD_MMC.setPins(SD_MMC_CLK, SD_MMC_CMD, SD_MMC_D0);
if (!SD_MMC.begin("/sdcard", true, true, SDMMC_FREQ_DEFAULT, 5)) {
Serial.println("Card Mount Failed");
return;
}
//获取SD卡的类型
uint8_t cardType = SD_MMC.cardType();
if(cardType == CARD_NONE){
Serial.println("No SD_MMC card attached");
return;
}
Serial.print("SD_MMC Card Type: ");
if(cardType == CARD_MMC){
Serial.println("MMC");
} else if(cardType == CARD_SD){
Serial.println("SDSC");
} else if(cardType == CARD_SDHC){
Serial.println("SDHC");
} else {
Serial.println("UNKNOWN");
}
// 打印SD卡大小
uint64_t cardSize = SD_MMC.cardSize() / (1024 * 1024);
Serial.printf("SD_MMC Card Size: %lluMB\n", cardSize);
Serial.printf("Total space: %lluMB\r\n", SD_MMC.totalBytes() / (1024 * 1024));
Serial.printf("Used space: %lluMB\r\n", SD_MMC.usedBytes() / (1024 * 1024));
}
// 列出指定目录下的文件和子目录
void listDir(fs::FS &fs, const char * dirname, uint8_t levels){
Serial.printf("Listing directory: %s\n", dirname);
// 打开目录
File root = fs.open(dirname);
if(!root){
Serial.println("Failed to open directory");
return;
}
// 检查是否为目录
if(!root.isDirectory()){
Serial.println("Not a directory");
return;
}
// 打开目录的下一个文件
File file = root.openNextFile();
while(file){
// 如果是目录,则打印目录名并递归列出子目录
if(file.isDirectory()){
Serial.print(" DIR : ");
Serial.println(file.name());
if(levels){
listDir(fs, file.path(), levels -1);
}
} else {
// 如果是文件,则打印文件名和大小
Serial.print(" FILE: ");
Serial.print(file.name());
Serial.print(" SIZE: ");
Serial.println(file.size());
}
file = root.openNextFile();
}
}
// 创建一个新的目录
void createDir(fs::FS &fs, const char * path){
// 打印正在创建的目录
Serial.printf("Creating Dir: %s\n", path);
if(fs.mkdir(path)){
// 尝试创建目录,如果成功则打印成功信息,否则打印失败信息
Serial.println("Dir created");
} else {
Serial.println("mkdir failed");
}
}
// 删除一个目录及其所有内容
void removeDir(fs::FS &fs, const char * path){
Serial.printf("Removing Dir: %s\n", path);
if(fs.rmdir(path)){
// 尝试删除目录,如果成功则打印成功信息,否则打印失败信息
Serial.println("Dir removed");
} else {
Serial.println("rmdir failed");
}
}
// 读取并显示文件内容
void readFile(fs::FS &fs, const char * path){
// 打印正在读取的文件
Serial.printf("Reading file: %s\n", path);
// 打开文件
File file = fs.open(path);
if(!file){
Serial.println("Failed to open file for reading");
return;
}
// 读取并打印文件内容
Serial.print("Read from file: ");
while(file.available()){
Serial.write(file.read());
}
}
// 写入内容到文件,如果文件不存在则创建
void writeFile(fs::FS &fs, const char * path, const char * message){
//打印正在写的文件
Serial.printf("Writing file: %s\n", path);
File file = fs.open(path, FILE_WRITE);
// 打开文件用于写入
if(!file){
Serial.println("Failed to open file for writing");
return;
}
if(file.print(message)){
Serial.println("File written");
} else {
Serial.println("Write failed");
}
}
// 向文件追加内容
void appendFile(fs::FS &fs, const char * path, const char * message){
// 打印正在追加内容的文件路径
Serial.printf("Appending to file: %s\n", path);
// 打开文件用于追加
File file = fs.open(path, FILE_APPEND);
if(!file){
// 如果文件打开失败,打印错误信息
Serial.println("Failed to open file for appending");
return;
}
// 追加内容到文件,并检查是否成功
if(file.print(message)){
Serial.println("Message appended");
} else {
// 如果追加失败,打印错误信息
Serial.println("Append failed");
}
}
// 重命名文件
void renameFile(fs::FS &fs, const char * path1, const char * path2){
// 打印重命名操作的信息
Serial.printf("Renaming file %s to %s\n", path1, path2);
// 尝试重命名文件,并检查是否成功
if (fs.rename(path1, path2)) {
Serial.println("File renamed");
} else {
// 如果重命名失败,打印错误信息
Serial.println("Rename failed");
}
}
// 删除文件
void deleteFile(fs::FS &fs, const char * path){
// 打印正在删除的文件路径
Serial.printf("Deleting file: %s\n", path);
// 尝试删除文件,并检查是否成功
if(fs.remove(path)){
Serial.println("File deleted");
} else {
// 如果删除失败,打印错误信息
Serial.println("Delete failed");
}
}
// 测试文件I/O性能
void testFileIO(fs::FS &fs, const char * path){
// 打开文件
File file = fs.open(path);
static uint8_t buf[512]; // 静态缓冲区
size_t len = 0; // 文件长度
uint32_t start = millis(); // 开始时间
uint32_t end = start; // 结束时间
if(file){
// 获取文件长度
len = file.size();
size_t flen = len; // 保存原始长度
start = millis(); // 重置开始时间
// 读取文件内容
while(len){
size_t toRead = len; // 要读取的字节数
if(toRead > 512){
toRead = 512; // 限制每次读取的字节数
}
file.read(buf, toRead); // 读取文件内容到缓冲区
len -= toRead; // 更新剩余字节数
}
end = millis() - start; // 计算读取时间
// 打印读取结果
Serial.printf("%u bytes read for %u ms\r\n", flen, end);
file.close(); // 关闭文件
} else {
// 如果文件打开失败,打印错误信息
Serial.println("Failed to open file for reading");
}
// 打开文件用于写入
file = fs.open(path, FILE_WRITE);
if(!file){
// 如果文件打开失败,打印错误信息
Serial.println("Failed to open file for writing");
return;
}
// 写入数据到文件
size_t i;
start = millis(); // 重置开始时间
for(i=0; i<2048; i++){
file.write(buf, 512); // 写入512字节到文件
}
end = millis() - start; // 计算写入时间
// 打印写入结果
Serial.printf("%u bytes written for %u ms\n", 2048 * 512, end);
file.close(); // 关闭文件
}
// 将JPEG图片数据写入文件
void writejpg(fs::FS &fs, const char * path, const uint8_t *buf, size_t size){
// 打开文件用于写入
File file = fs.open(path, FILE_WRITE);
if(!file){
// 如果文件打开失败,打印错误信息
Serial.println("Failed to open file for writing");
return;
}
// 写入数据到文件
file.write(buf, size);
// 打印文件保存成功信息
Serial.printf("Saved file to path: %s\r\n", path);
}
// 读取指定目录下的文件数量
int readFileNum(fs::FS &fs, const char * dirname){
File root = fs.open(dirname);
if(!root){
Serial.println("Failed to open directory");
return -1;
}
if(!root.isDirectory()){
Serial.println("Not a directory");
return -1;
}
File file = root.openNextFile();
int num=0;
while(file){
//遍历文件个数
file = root.openNextFile();
num++;
}
return num;
}
2.1.2 摄像头引脚的头文件
#ifndef __camera_pin_H
#define __camera_pin_H
#define PWDN_GPIO_NUM -1
#define RESET_GPIO_NUM -1
#define XCLK_GPIO_NUM 15
#define SIOD_GPIO_NUM 4
#define SIOC_GPIO_NUM 5
#define Y2_GPIO_NUM 11
#define Y3_GPIO_NUM 9
#define Y4_GPIO_NUM 8
#define Y5_GPIO_NUM 10
#define Y6_GPIO_NUM 12
#define Y7_GPIO_NUM 18
#define Y8_GPIO_NUM 17
#define Y9_GPIO_NUM 16
#define VSYNC_GPIO_NUM 6
#define HREF_GPIO_NUM 7
#define PCLK_GPIO_NUM 13
#endif
2.2CAM
2.2.1 摄像头初始化
1.下面是摄像头的结构体 可以根据具体的应用结合psram,芯片的算力还有摄像头的散热来修改下面的参数
SQCIF=12*96
QCIF=176*144
CIF=352*288
QVGA=320*240
VGA=640*480
SVGA=800*600
WSVGA=1024*600
XGA=1024*768
XVGA=1280*960
UXGA=1600*1200
2.亮度 饱和度 垂直翻转这些默认参数即可效果还是ok的但如果应用场景比较特殊的话也可以更改
// 创建并初始化摄像头配置结构体
camera_config_t config;
// 配置摄像头数据引脚
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
// 配置时钟和同步引脚
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
// 配置I2C引脚
config.pin_sccb_sda = SIOD_GPIO_NUM;
config.pin_sccb_scl = SIOC_GPIO_NUM;
// 配置电源和复位引脚
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
// 设置时钟频率
config.xclk_freq_hz = 20000000;
// 设置图像尺寸
config.frame_size = FRAMESIZE_UXGA;
// 设置像素格式
config.pixel_format = PIXFORMAT_JPEG; // JPEG格式用于流式传输
// 设置帧缓冲区获取模式
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
// 设置帧缓冲区位置
config.fb_location = CAMERA_FB_IN_PSRAM;
// 设置JPEG质量
config.jpeg_quality = 12;
// 设置帧缓冲区数量
config.fb_count = 1;
// 如果检测到PSRAM,则使用更高的分辨率和JPEG质量
if(psramFound()){
config.jpeg_quality = 10;
config.fb_count = 2;
config.grab_mode = CAMERA_GRAB_LATEST;
} else {
// 如果没有PSRAM,限制帧大小并使用DRAM
config.frame_size = FRAMESIZE_SVGA;
config.fb_location = CAMERA_FB_IN_DRAM;
}
// 初始化摄像头
esp_err_t err = esp_camera_init(&config);
// 检查摄像头是否初始化成功
if (err != ESP_OK) {
// 如果初始化失败,打印错误代码
Serial.printf("Camera init failed with error 0x%x", err);
return 0; // 返回0表示失败
}
// 获取传感器指针
sensor_t * s = esp_camera_sensor_get();
// 传感器初始设置:翻转图像、调整亮度和饱和度 这里可以不更改
s->set_vflip(s, 1); // 翻转图像垂直方向
s->set_brightness(s, 1); // 调整亮度
s->set_saturation(s, 0); // lower the saturation
2.2.2摄像头拍摄代码
while(digitalRead(BUTTON_PIN)==LOW);
// 按钮释放后,开始拍照流程
//初始化帧缓冲区的指针
camera_fb_t * fb = NULL;
// 获取一帧图像数据
fb = esp_camera_fb_get();
2.2.3摄像头存储进sd卡代码
fb = esp_camera_fb_get();
//检查是否获取帧缓冲区域
if (fb != NULL) {
// 读取sd卡上照片数量
int photo_index = readFileNum(SD_MMC, "/camera");
// 检查是否成功读取文件数量
if(photo_index!=-1)
{
// 构建照片存储路径
String path = "/camera/" + String(photo_index) +".jpg";
// 将照片写入SD卡
writejpg(SD_MMC, path.c_str(), fb->buf, fb->len);
}
// 释放帧缓冲区
esp_camera_fb_return(fb);
}
else {
// 如果获取帧缓冲区失败,打印错误信息
Serial.println("Camera capture failed.");
}
3.全部代码
#include "esp_camera.h"
#define CAMERA_MODEL_ESP32S3_EYE
#include "camera_pins.h"
#include "sd_read_write.h"
#define BUTTON_PIN 0 //定义按键引脚
int cameraSetup(void); //声明
void setup() {
Serial.begin(115200);
//关闭debug减少串口信息
Serial.setDebugOutput(false);
Serial.println();
//低电平驱动上拉电阻
pinMode(BUTTON_PIN, INPUT_PULLUP);
//sd卡初始化
sdmmcInit();
// removeDir(SD_MMC, "/camera"); //删除文件
createDir(SD_MMC, "/camera");
listDir(SD_MMC, "/camera", 0);
//摄像头连接判断
if(cameraSetup()==1){
Serial.println("camerasetup");
}
else{
Serial.println("cameradown");
return;
}
}
void loop() {
// 检查按钮是否被按下(低电平有效)
if(digitalRead(BUTTON_PIN)==LOW){
// 简单的消抖,延时20毫秒
delay(20);
// 再次检查按钮是否仍然被按下
if(digitalRead(BUTTON_PIN)==LOW){
// 当按钮持续被按下时,进入等待循环直到按钮释放
while(digitalRead(BUTTON_PIN)==LOW);
// 按钮释放后,开始拍照流程
//初始化帧缓冲区的指针
camera_fb_t * fb = NULL;
// 获取一帧图像数据
fb = esp_camera_fb_get();
//检查是否获取帧缓冲区域
if (fb != NULL) {
// 读取sd卡上照片数量
int photo_index = readFileNum(SD_MMC, "/camera");
// 检查是否成功读取文件数量
if(photo_index!=-1)
{
// 构建照片存储路径
String path = "/camera/" + String(photo_index) +".jpg";
// 将照片写入SD卡
writejpg(SD_MMC, path.c_str(), fb->buf, fb->len);
}
// 释放帧缓冲区
esp_camera_fb_return(fb);
}
else {
// 如果获取帧缓冲区失败,打印错误信息
Serial.println("Camera capture failed.");
}
}
}
}
// 摄像头设置函数
int cameraSetup(void) {
// 创建并初始化摄像头配置结构体
camera_config_t config;
// 配置摄像头数据引脚
config.pin_d0 = Y2_GPIO_NUM;
config.pin_d1 = Y3_GPIO_NUM;
config.pin_d2 = Y4_GPIO_NUM;
config.pin_d3 = Y5_GPIO_NUM;
config.pin_d4 = Y6_GPIO_NUM;
config.pin_d5 = Y7_GPIO_NUM;
config.pin_d6 = Y8_GPIO_NUM;
config.pin_d7 = Y9_GPIO_NUM;
// 配置时钟和同步引脚
config.pin_xclk = XCLK_GPIO_NUM;
config.pin_pclk = PCLK_GPIO_NUM;
config.pin_vsync = VSYNC_GPIO_NUM;
config.pin_href = HREF_GPIO_NUM;
// 配置I2C引脚
config.pin_sccb_sda = SIOD_GPIO_NUM;
config.pin_sccb_scl = SIOC_GPIO_NUM;
// 配置电源和复位引脚
config.pin_pwdn = PWDN_GPIO_NUM;
config.pin_reset = RESET_GPIO_NUM;
// 设置时钟频率
config.xclk_freq_hz = 20000000;
// 设置图像尺寸
config.frame_size = FRAMESIZE_UXGA;
// 设置像素格式
config.pixel_format = PIXFORMAT_JPEG; // JPEG格式用于流式传输
// 设置帧缓冲区获取模式
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
// 设置帧缓冲区位置
config.fb_location = CAMERA_FB_IN_PSRAM;
// 设置JPEG质量
config.jpeg_quality = 12;
// 设置帧缓冲区数量
config.fb_count = 1;
// 如果检测到PSRAM,则使用更高的分辨率和JPEG质量
if(psramFound()){
config.jpeg_quality = 10;
config.fb_count = 2;
config.grab_mode = CAMERA_GRAB_LATEST;
} else {
// 如果没有PSRAM,限制帧大小并使用DRAM
config.frame_size = FRAMESIZE_SVGA;
config.fb_location = CAMERA_FB_IN_DRAM;
}
// 初始化摄像头
esp_err_t err = esp_camera_init(&config);
// 检查摄像头是否初始化成功
if (err != ESP_OK) {
// 如果初始化失败,打印错误代码
Serial.printf("Camera init failed with error 0x%x", err);
return 0; // 返回0表示失败
}
// 获取传感器指针
sensor_t * s = esp_camera_sensor_get();
// 传感器初始设置:翻转图像、调整亮度和饱和度
s->set_vflip(s, 1); // 翻转图像垂直方向
s->set_brightness(s, 1); // 调整亮度
s->set_saturation(s, 0); // lower the saturation
Serial.println("Camera configuration complete!");
return 1;
}
4.效果展示
上课划水写的,后续发效果展示和硬件接线