当前位置: 首页 > news >正文

esp32s3 + ov2640,给摄像头加上拍照功能,存储到sd卡

前面已经做了从过网络访问摄像头的实验:esp32s3 通过wifi查看正点原子摄像头-CSDN博客

现在的目标是添加一张sd卡(其实是手机上用的tf卡),没有现成的,就从玩具中拆了一张出来,一看才128M,这也太小了,玩具厂可真会节省成本^_^!

功能上,就添加了sd卡的读写功能,还在页面上添加了一个查看sd卡中存储的照片的功能。

就直接在main.cpp上添加代码了,实际项目中的话,最好还是拆出来,这样代码比较清晰。

代码如下:

#include <Arduino.h>
#include "esp_camera.h"
#include "camera.h"
#include "xl9555.h"
#include <WiFi.h>
#include <WebServer.h>
#include <SD.h>          // 使用SPI模式需要包含SD库
#include <SPI.h>         // SPI库
#include "FS.h"          // 文件系统支持// SD卡引脚定义(自定义以避免与摄像头冲突)
#define SD_CS_PIN         2   
#define SD_MISO_PIN       13 
#define SD_MOSI_PIN       11
#define SD_SCK_PIN        12// SD卡相关变量
bool sdCardAvailable = false;
uint32_t imageCounter = 0;  // 图片计数器// WiFi配置
const char* ssid = "改为你家的wifi";
const char* password = "改为你家的wifi密码";// HTTP服务器
WebServer server(80);camera_fb_t *fb = NULL;// 提前声明HTTP处理函数
void handleRoot();
void handleStream();
void handleJPG();
void handleListImages();  // 查看SD卡照片
void handleImageFile();   // 显示单张图片// 函数声明
bool saveImageToSD(camera_fb_t *fb);
bool initSDCard();void setup() {Serial.begin(115200);// 初始化SD卡sdCardAvailable = initSDCard();// 初始化XL9555扩展IOxl9555_init();// 摄像头初始化if(camera_init() != 0) {Serial.println("摄像头初始化失败!");while(1) delay(100);}// 连接WiFiWiFi.begin(ssid, password);while (WiFi.status() != WL_CONNECTED) {delay(500);Serial.print(".");}Serial.println("\nWiFi已连接");Serial.print("IP地址: ");Serial.println(WiFi.localIP());// 设置HTTP路由server.on("/", HTTP_GET, handleRoot);server.on("/stream", HTTP_GET, handleStream);server.on("/jpg", HTTP_GET, handleJPG);server.on("/list", HTTP_GET, handleListImages); // 查看照片列表server.on("/image", HTTP_GET, handleImageFile); // 显示单张图片server.begin();Serial.println("HTTP服务器已启动");
}void loop() {server.handleClient();
}/*** @brief       摄像头初始化* @param       无* @retval      0:成功 / 1:失败 */
uint8_t camera_init(void) {camera_config_t camera_config;// 引脚配置camera_config.pin_d0 = OV_D0_PIN;camera_config.pin_d1 = OV_D1_PIN;camera_config.pin_d2 = OV_D2_PIN;camera_config.pin_d3 = OV_D3_PIN;camera_config.pin_d4 = OV_D4_PIN;camera_config.pin_d5 = OV_D5_PIN;camera_config.pin_d6 = OV_D6_PIN;camera_config.pin_d7 = OV_D7_PIN;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_sccb_sda = OV_SDA_PIN;camera_config.pin_sccb_scl = OV_SCL_PIN;camera_config.pin_pwdn = OV_PWDN_PIN;camera_config.pin_reset = OV_RESET_PIN;// 其他配置camera_config.ledc_channel = LEDC_CHANNEL_0;camera_config.ledc_timer = LEDC_TIMER_0;camera_config.xclk_freq_hz = 20000000;camera_config.pixel_format = PIXFORMAT_JPEG;  // 使用JPEG格式// 优先使用PSRAMif(psramFound()){camera_config.frame_size = FRAMESIZE_SVGA;  // 800x600camera_config.jpeg_quality = 12;camera_config.fb_count = 2;camera_config.grab_mode = CAMERA_GRAB_LATEST;camera_config.fb_location = CAMERA_FB_IN_PSRAM;} else {camera_config.frame_size = FRAMESIZE_QVGA;  // 320x240camera_config.jpeg_quality = 15;camera_config.fb_count = 1;}// XL9555引脚控制if (OV_PWDN_PIN == -1) {xl9555_io_config(OV_PWDN, IO_SET_OUTPUT);xl9555_pin_set(OV_PWDN, IO_SET_LOW);}if (OV_RESET_PIN == -1) {xl9555_io_config(OV_RESET, IO_SET_OUTPUT);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();// 根据摄像头型号设置方向if (s->id.PID == OV2640_PID) {s->set_vflip(s, 0);       // OV2640不需要垂直翻转} else {s->set_vflip(s, 1);       // 其他摄像头垂直翻转}// 图像参数调整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);       // 水平镜像Serial.println("摄像头初始化成功");return 0;
}// 根页面处理
void handleRoot() {String html = "<html>\n""<head>\n""<meta charset=\"UTF-8\">\n"  // 添加UTF-8编码声明"<title>ESP32-CAM 监控</title>\n""<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n""<style>\n""body { font-family: Arial; text-align: center; margin: 0; padding: 20px; background-color: #f5f5f5; }\n""h1 { color: #333; }\n"".container { max-width: 800px; margin: 0 auto; }\n""img { max-width: 100%; border: 1px solid #ddd; border-radius: 4px; box-shadow: 0 0 10px rgba(0,0,0,0.1); }\n"".controls { margin: 20px 0; }\n""a { display: inline-block; margin: 10px; padding: 10px 20px; background: #4CAF50; color: white; text-decoration: none; border-radius: 4px; }\n"".gallery { display: flex; flex-wrap: wrap; justify-content: center; }\n"".gallery img { width: 150px; height: 150px; object-fit: cover; margin: 5px; }\n""</style>\n""</head>\n""<body>\n""<div class=\"container\">\n""<h1>ESP32-S3 摄像头监控</h1>\n""<img src=\"/stream\" id=\"video\" alt=\"实时视频流\">\n""<div class=\"controls\">\n""<a href=\"/jpg\">拍照</a>\n""<a href=\"/list\">查看保存的照片</a>\n""<a href=\"javascript:location.reload()\">刷新</a>\n""</div>\n""<p>IP地址: " + WiFi.localIP().toString() + "</p>\n""</div>\n""<script>\n""// Auto-refresh image\n""setInterval(function() {\n""  document.getElementById('video').src = '/stream?' + Date.now();\n""}, 100);\n""</script>\n""</body>\n""</html>\n";server.send(200, "text/html", html);
}// 视频流处理
void handleStream() {WiFiClient client = server.client();// 发送HTTP头String response = "HTTP/1.1 200 OK\r\n";response += "Content-Type: multipart/x-mixed-replace; boundary=frame\r\n\r\n";client.print(response);while (client.connected()) {fb = esp_camera_fb_get();if (!fb) {Serial.println("摄像头捕获失败");delay(100);continue;}// 发送图像边界String header = "--frame\r\n";header += "Content-Type: image/jpeg\r\n";header += "Content-Length: " + String(fb->len) + "\r\n\r\n";client.print(header);// 发送图像数据client.write(fb->buf, fb->len);esp_camera_fb_return(fb);fb = NULL;// 短暂延迟以控制帧率delay(50);// 检查连接状态if (!client.connected()) {break;}}
}// 单张图片处理
void handleJPG() {fb = esp_camera_fb_get();if (!fb) {server.send(500, "text/plain", "摄像头捕获失败");return;}// 保存图片到SD卡if (sdCardAvailable) {saveImageToSD(fb);}// 发送图片到客户端server.send_P(200, "image/jpeg", reinterpret_cast<const char*>(fb->buf), fb->len);esp_camera_fb_return(fb);fb = NULL;
}// 保存图片到SD卡
bool saveImageToSD(camera_fb_t *fb) {if (!sdCardAvailable) {Serial.println("SD卡不可用,无法保存图像");return false;}// 创建目录(如果不存在)if (!SD.exists("/images")) {if (!SD.mkdir("/images")) {Serial.println("创建/images目录失败");return false;}Serial.println("已创建/images目录");}// 生成带时间戳的文件名struct timeval tv;gettimeofday(&tv, NULL);char filename[64];snprintf(filename, sizeof(filename), "/images/img_%lu_%lu.jpg", tv.tv_sec, imageCounter++);// 打开文件File file = SD.open(filename, FILE_WRITE);if (!file) {Serial.printf("无法创建文件: %s\n", filename);return false;}// 写入图像数据size_t bytesWritten = file.write(fb->buf, fb->len);file.close();if (bytesWritten != fb->len) {Serial.printf("写入不完整: %d/%d 字节\n", bytesWritten, fb->len);return false;}Serial.printf("图片已保存: %s (%d字节)\n", filename, bytesWritten);return true;
}// 初始化SD卡 (SPI模式)
bool initSDCard() {Serial.println("初始化SD卡(SPI模式)...");// 初始化SPI引脚SPI.begin(SD_SCK_PIN, SD_MISO_PIN, SD_MOSI_PIN, SD_CS_PIN);if (!SD.begin(SD_CS_PIN)) {Serial.println("SD卡初始化失败");return false;}// 检查SD卡类型uint8_t cardType = SD.cardType();if (cardType == CARD_NONE) {Serial.println("未检测到SD卡");return false;}Serial.print("SD卡类型: ");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("未知");}// 显示SD卡大小uint64_t cardSize = SD.cardSize() / (1024 * 1024);Serial.printf("SD卡大小: %lluMB\n", cardSize);// 检查可用空间uint64_t freeBytes = SD.totalBytes() - SD.usedBytes();Serial.printf("可用空间: %lluMB\n", freeBytes / (1024 * 1024));// 确保/images目录存在if (!SD.exists("/images")) {if (!SD.mkdir("/images")) {Serial.println("无法创建/images目录");} else {Serial.println("已创建/images目录");}}return true;
}// 查看SD卡照片列表
void handleListImages() {if (!sdCardAvailable) {server.send(200, "text/plain", "SD卡不可用");return;}String html = "<html><head><meta charset=\"UTF-8\"><title>已保存的照片</title><style>";html += "body { text-align: center; font-family: Arial; }";html += "h1 { color: #333; }";html += ".gallery { display: flex; flex-wrap: wrap; justify-content: center; }";html += ".gallery a { margin: 10px; text-decoration: none; color: #333; }";html += ".gallery img { width: 200px; height: 150px; object-fit: cover; border: 1px solid #ddd; border-radius: 4px; }";html += ".back-btn { display: block; margin: 20px auto; padding: 10px 20px; background: #4CAF50; color: white; border-radius: 4px; width: fit-content; }";html += "</style></head><body>";html += "<h1>已保存的照片</h1>";html += "<div class='gallery'>";// 列出/images目录下的文件File root = SD.open("/images");if (!root) {html += "<p>无法打开目录</p>";} else if (!root.isDirectory()) {html += "<p>不是一个目录</p>";} else {File file = root.openNextFile();int count = 0;while (file) {if (!file.isDirectory()) {String path = file.path();if (path.endsWith(".jpg") || path.endsWith(".jpeg")) {// 移除"/sd"前缀,因为SD库会添加这个前缀if (path.startsWith("/sd")) {path = path.substring(3);}html += "<a href='/image?path=" + path + "'>";html += "<img src='/image?path=" + path + "' alt='照片'>";html += "<br>" + path.substring(8) + "</a>"; // 显示文件名(去掉/images/前缀)count++;}}file = root.openNextFile();}if (count == 0) {html += "<p>没有找到照片</p>";}}root.close();html += "</div>";html += "<a class='back-btn' href='/'>返回主页</a>";html += "</body></html>";server.send(200, "text/html", html);
}// 显示单张图片
void handleImageFile() {String path = server.arg("path");if (path.length() == 0) {server.send(400, "text/plain", "缺少路径参数");return;}// 确保路径以/开头if (!path.startsWith("/")) {path = "/" + path;}// 检查文件是否存在if (!SD.exists(path)) {server.send(404, "text/plain", "文件未找到: " + path);return;}File file = SD.open(path, FILE_READ);if (!file) {server.send(500, "text/plain", "无法打开文件: " + path);return;}// 设置正确的Content-Typeserver.setContentLength(file.size());server.send(200, "image/jpeg");// 流式传输文件内容uint8_t buffer[1024];size_t bytesRead;while ((bytesRead = file.read(buffer, sizeof(buffer))) > 0) {server.sendContent_P((const char*)buffer, bytesRead);}file.close();
}

platformio.ini

[env:dnesp32s3]
platform = espressif32
board = dnesp32s3
framework = arduino
monitor_speed = 115200
test_speed = 115200
upload_speed=115200
debug_speed = 115200lib_deps = esp32-camera@^2.0.0https://github.com/me-no-dev/AsyncTCP.githttps://github.com/me-no-dev/ESPAsyncWebServer.git; 启用PSRAM支持
board_build.psram = opi
board_build.psram_mode = opi; 分区方案
board_build.partitions = default_16MB.csv; 优化设置
build_flags = -DBOARD_HAS_PSRAM-mfix-esp32-psram-cache-issue

 

烧录成功:

 从页面上查看实际效果:

 尝试拍照:

 点击“查看保存的照片”:

不过页面刷了一会才出来,需要耐心等几秒钟。

 

http://www.dtcms.com/a/308440.html

相关文章:

  • 109㎡中古风家装:北京业之峰在朝阳区绘就温馨画卷
  • 【实际项目1.2-西门子PLC的报警监控思路】
  • Java多线程详解(1)
  • C#反射的概念与实战
  • [2025CVPR-小样本方向]ImagineFSL:基于VLM的少样本学习的想象基集上的自监督预训练很重要
  • 三方支付详解
  • SQL 中 WHERE 与 HAVING 的用法详解:分组聚合场景下的混用指南
  • 大数据平台数仓数湖hive之拉链表高效实现
  • 深度学习入门:用pytorch跑通GitHub的UNET-ZOO项目
  • 云服务器数据库
  • Camx-查看sensor mode 和效果参数
  • (LeetCode 每日一题) 2683. 相邻值的按位异或 (位运算)
  • 网络操作系统与应用服务器-1
  • SIwave 中 SIwizard 的 500 多个标准列表
  • 代码详细注释:演示多线程如何安全操作共享变量,使用互斥锁避免数据竞争。
  • Linux 文件系统基本管理
  • minidocx: 在C++11环境下运行的解决方案(二)
  • 网络攻击新态势企业级安全防御指南
  • Git分支管理:每个分支为什么这么命名?
  • Acrobat DC 应用安全配置:沙箱防护、数字签名
  • 了解微前端和SSO单点登录
  • Linux/Ubuntu 系统中打开火狐firefox、chromium浏览器失败
  • (三)从零搭建unity3d机器人仿真:使用WheelCollider实现turtlebot轮子差速运动
  • Linux系统编程-gcc(黑马笔记)
  • 译 | 用于具有外生特征的时间序列预测模型TimeXer
  • JavaScript 大数运算!
  • Abp+ShardingCore+EFCore.BulkExtensions使用案例
  • MCU中的DAC(数字模拟转换器)是什么?
  • 动态挑战-响应机制和密钥轮换
  • 算法练习:JZ32 从上往下打印二叉树