ESP32电子相册

之前有一段时间想搞一搞电子吧唧这东西,后来想来想去也就是个电子相册,这里拿杜邦线接一接,写一写程序,当个DEMO验证没有什么问题

平台:ESP32S3R8N8 ST7789 PIO+Arduino


一、显示图片

这里使用最为广泛使用的TFT_eSPI库进行配置,直接在PIO HOME搜索装上完事

然后打开资源管理器的User_Setup.h

选驱动

选分辨率

改引脚

在main.c开启背光,进行初始化烧录程序,

cpp 复制代码
#include <SPI.h>
#include <TFT_eSPI.h>
#define BACKLIGHT_PIN 1  // 根据你的接线修改!
TFT_eSPI tft = TFT_eSPI();  // 创建TFT对象

void setup() {
  // 初始化屏幕
  pinMode(BACKLIGHT_PIN, OUTPUT);
  digitalWrite(BACKLIGHT_PIN, HIGH);  // 点亮背光
  tft.init();
  tft.setRotation(1);  // 设置屏幕的方向 0, 1, 2 or 3
  tft.fillScreen(TFT_BROWN);  // 填充背景
}

然后你就会发现,屏幕怎么还是黑的???

这是因为arduino驱动库和tft_espi库有冲突。

我最终在这位大佬的帖子中找到了解决办法esp32c3使用tft-espi屏幕不断重启原因及解决方法 -- 爱上生活

对于ESP32S3 而言,直接在User_Setup.h把这个开了就没问题了

如果你的颜色还有问题,那就把RGB顺序调一下,应该就没问题了

然后选择一个图片进行显示测试,可以通过下方网页生成C代码

File to C style array converter

最后调用这一个函数进行显示tft.pushImage(0, 0, 320, 240, pic);

测试没问题后进行下一步


二、图片压缩

在上面我们生成了一个320*240的数组,array size is 153600也就是153KB资源,如果你的屏幕分辨率更高,后续如果想放动图,或者更多图片,FLASH是不够用的,这时候就需要考虑压缩算法。

这里选择JPG算法,同时也方便后续的图片上传,在PIO搜索TJPG_decoder并导入到工程中,复制pandas数组并测试,这里给出的解码时间是70ms左右

cpp 复制代码
#include <SPI.h>
#include <TFT_eSPI.h>
#include <TJpg_Decoder.h>

#define BACKLIGHT_PIN 1  // 根据你的背光接线修改!

TFT_eSPI tft = TFT_eSPI();  

// ========== 解码回调函数==========
bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap) {
  // 边界检查,防止画到屏幕外
  if (y >= tft.height()) return 0;
  
  // 将解码后的像素块推送到屏幕
  tft.pushImage(x, y, w, h, bitmap);
  
  // 返回 1 继续解码,返回 0 停止解码
  return 1;
}

void setup() {

  Serial.begin(9600);
  delay(1000);
  // ========== 初始化屏幕==========
  pinMode(BACKLIGHT_PIN, OUTPUT);
  digitalWrite(BACKLIGHT_PIN, HIGH);  // 点亮背光
  tft.init();
  tft.setRotation(1);  // 设置屏幕的方向 0, 1, 2 or 3
  tft.fillScreen(TFT_BROWN);  // 填充背景

  // ========== 初始化解码器==========
  TJpgDec.setJpgScale(1);        // 缩放:1=原图, 2=1/2, 4=1/4, 8=1/8
  TJpgDec.setSwapBytes(true);    // ESP32 需要颜色字节交换(关键!)
  TJpgDec.setCallback(tft_output); // 设置绘制回调
}


void loop(){
  tft.fillScreen(TFT_RED);  // 红底清屏(观察覆盖情况)

  uint32_t t = millis();    // 开始计时

  // 【预检】不解码,只读取JPG头信息获取尺寸
  uint16_t w = 0, h = 0;
  TJpgDec.getJpgSize(&w, &h, panda, sizeof(panda));
  Serial.print("Width = "); Serial.print(w); 
  Serial.print(", height = "); Serial.println(h);

  // 【核心】解码并显示
  // 参数:x坐标, y坐标, 数据指针, 数据长度
  TJpgDec.drawJpg(0, 0, panda, sizeof(panda));

  // 输出耗时(性能测试)
  t = millis() - t;
  Serial.print(t); Serial.println(" ms");

  delay(5000);  // 暂停5秒再看
}

三、空间分配设置

ESP32S3N8 的默认分区表是这样的,但由于我们需要存储大量图片文件并且不需要OTA升级,那么可以稍微改一改;

cpp 复制代码
TEXT:=== 分区表信息 ===
Flash 总大小: 8 MB (8388608 bytes)
分区: nvs          类型: 0x01 子类型: 0x02 地址: 0x009000 大小: 0x005000 (20 KB)
分区: otadata      类型: 0x01 子类型: 0x00 地址: 0x00e000 大小: 0x002000 (8 KB)
分区: app0         类型: 0x00 子类型: 0x10 地址: 0x010000 大小: 0x330000 (3264 KB)
分区: app1         类型: 0x00 子类型: 0x11 地址: 0x340000 大小: 0x330000 (3264 KB)
分区: spiffs       类型: 0x01 子类型: 0x82 地址: 0x670000 大小: 0x180000 (1536 KB)
分区: coredump     类型: 0x01 子类型: 0x03 地址: 0x7f0000 大小: 0x010000 (64 KB)

在INI文件同级目录创建partitions_custom.csv

cpp 复制代码
nvs,        data, nvs,      0x9000,   0x6000,
phy_init,   data, phy,      0xf000,   0x1000,
factory,    app,  factory,  0x10000,  0x400000,
storage,    data, spiffs,   0x410000, 0x3D0000,
coredump,   data, coredump, 0x7E0000, 0x10000,

并在INI文件修改

cpp 复制代码
[env:esp32-s3-devkitc-1]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
lib_deps = 
	bodmer/TFT_eSPI@^2.5.43
	bodmer/TJpg_Decoder@^1.1.0
board_build.arduino.partitions = partitions_custom.csv
board_build.arduino.memory_type = qio_opi
build_flags = -DBOARD_HAS_PSRAM
board_upload.flash_size = 8MB

四、WIFI上传图片

添加WIFI账号密码后,路由器会自动分配IP地址,然后通过网页上传JPG,ESP32存储解码并显示

目前想到几个问题:

①图片上传前或者在网页端应该剪切,小于分辨率的照常发送,大于屏幕分辨率的自适应到屏幕分辨率,或许可以用微信小程序来做

②上传一般断网了怎么办,还需要一个垃圾空间的回收机制

总的来说,目前的demo达到了验证标准,距离实用还差了些距离

cpp 复制代码
#include <SPI.h>
#include <TFT_eSPI.h>
#include <TJpg_Decoder.h>
#include "SPIFFS.h" 
#include <WiFi.h>
#include <WebServer.h>

#define BACKLIGHT_PIN 1  // 根据你的背光接线修改!

const char* ssid = "";
const char* password = "";
const char* currentFile = "/upload.jpg";

WebServer server(80);  // HTTP端口
TFT_eSPI tft = TFT_eSPI();  

// ========== 解码回调函数==========
bool tft_output(int16_t x, int16_t y, uint16_t w, uint16_t h, uint16_t* bitmap) {
  // 边界检查,防止画到屏幕外
  if (y >= tft.height()) return 0;
  
  // 将解码后的像素块推送到屏幕
  tft.pushImage(x, y, w, h, bitmap);
  
  // 返回 1 继续解码,返回 0 停止解码
  return 1;
}

// ========== 智能显示(PSRAM 优化版)==========
void displayImageSmart(const char* filename) {
  tft.fillScreen(TFT_BLACK);
  
  if (!SPIFFS.exists(filename)) {
    tft.setTextColor(TFT_RED);
    tft.setTextDatum(MC_DATUM);
    tft.drawString("No Image", tft.width()/2, tft.height()/2);
    return;
  }
  
  File file = SPIFFS.open(filename, "r");
  size_t size = file.size();
  Serial.printf("文件大小: %d bytes (%.2f KB)\n", size, size/1024.0);

  uint8_t* buffer = NULL;
  
  // 尝试 PSRAM 分配
  if (psramFound() && size > 0) {
    buffer = (uint8_t*)ps_malloc(size);
    if (buffer) {
      Serial.println("使用 PSRAM");
    } else {
      Serial.println("PSRAM 分配失败");
    }
  }
  
  // 内存分配成功,读到内存处理
  if (buffer) {
    size_t read = file.read(buffer, size);
    file.close();
    
    if (read != size) {
      Serial.println("❌ 文件读取不完整");
      free(buffer);
      // 回退到直接文件解码
      TJpgDec.setJpgScale(1);
      TJpgDec.drawFsJpg(0, 0, filename, SPIFFS);
      return;
    }
    
    // 获取尺寸
    uint16_t w = 0, h = 0;
    JRESULT res = TJpgDec.getJpgSize(&w, &h, buffer, size);
    
    if (res == JDR_OK) {
      Serial.printf("图片尺寸: %dx%d\n", w, h);
      
      // 计算合适的缩放
      uint8_t scale = 1;
      while ((w/scale > tft.width() || h/scale > tft.height()) && scale < 8) {
        scale *= 2;
      }
      TJpgDec.setJpgScale(scale);
      
      // 居中显示
      int x = (tft.width() - w/scale) / 2;
      int y = (tft.height() - h/scale) / 2;
      
      Serial.printf("缩放: 1/%d, 位置: (%d, %d)\n", scale, x, y);
      
      // 使用内存方式显示
      TJpgDec.drawJpg(x, y, buffer, size);
      
      // 显示信息
      tft.setTextColor(TFT_WHITE, TFT_BLACK);
      tft.setTextDatum(TC_DATUM);
      String info = String(w) + "x" + String(h) + " @" + String(scale) + "x";
      tft.drawString(info, tft.width()/2, 5);
      
    } else {
      Serial.printf("❌ 无法获取尺寸: %d\n", res);
      // 尝试直接解码
      TJpgDec.setJpgScale(1);
      TJpgDec.drawJpg(0, 0, buffer, size);
    }
    
    free(buffer);  // PSRAM 和普通内存都用 free 释放
    
  } else {
    // 内存不足,使用流式解码
    file.close();
    Serial.println("内存不足,使用流式解码");
    TJpgDec.setJpgScale(1);
    TJpgDec.drawFsJpg(0, 0, filename, SPIFFS);
  }
}

// ========== 处理上传 ==========
void handleUpload() {
  HTTPUpload& upload = server.upload();
  
  if (upload.status == UPLOAD_FILE_START) {
    Serial.printf("开始上传: %s\n", upload.filename.c_str());
    
    // 删除旧文件
    if (SPIFFS.exists(currentFile)) {
      SPIFFS.remove(currentFile);
    }
    
    File file = SPIFFS.open(currentFile, "w");
    if (!file) {
      server.send(500, "text/plain", "无法创建文件");
      return;
    }
    file.close();
    
  } else if (upload.status == UPLOAD_FILE_WRITE) {
    File file = SPIFFS.open(currentFile, "a");
    if (file) {
      file.write(upload.buf, upload.currentSize);
      file.close();
    }
    
  } else if (upload.status == UPLOAD_FILE_END) {
    Serial.printf("上传完成: %d bytes\n", upload.totalSize);
    
    // 显示图片
    displayImageSmart(currentFile);
    
    // 返回成功页面
    String html = R"(
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <title>上传成功</title>
  <style>
    body { font-family: Arial; text-align: center; padding: 50px; background: #f0f0f0; }
    .success { color: #4CAF50; font-size: 48px; margin: 20px; }
    .info { color: #666; margin: 20px; }
    .btn { display: inline-block; padding: 15px 30px; background: #2196F3; color: white; 
           text-decoration: none; border-radius: 5px; margin-top: 20px; }
  </style>
</head>
<body>
  <div class="success">✓</div>
  <h1>上传成功!</h1>
  <div class="info">文件大小: )" + String(upload.totalSize) + R"( bytes</div>
  <div class="info">已显示在 ESP32 屏幕上</div>
  <a href="/" class="btn">继续上传</a>
</body>
</html>
)";
    server.send(200, "text/html", html);
  }
}

// ========== 主页 ==========
void handleRoot() {
  String html = R"(
<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>ESP32 图片上传</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; }
    body {
      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      min-height: 100vh;
      display: flex;
      justify-content: center;
      align-items: center;
      padding: 20px;
    }
    .container {
      background: white;
      border-radius: 20px;
      padding: 40px;
      box-shadow: 0 20px 60px rgba(0,0,0,0.3);
      max-width: 500px;
      width: 100%;
      text-align: center;
    }
    h1 { color: #333; margin-bottom: 10px; }
    .subtitle { color: #666; margin-bottom: 30px; font-size: 14px; }
    .upload-area {
      border: 3px dashed #ddd;
      border-radius: 15px;
      padding: 40px;
      margin-bottom: 20px;
      transition: all 0.3s;
      position: relative;
    }
    .upload-area:hover { border-color: #667eea; background: #f8f9ff; }
    input[type="file"] {
      position: absolute; top: 0; left: 0; right: 0; bottom: 0;
      opacity: 0; cursor: pointer; width: 100%;
    }
    .upload-icon { font-size: 48px; margin-bottom: 10px; }
    .btn {
      background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
      color: white; border: none; padding: 15px 40px;
      border-radius: 25px; font-size: 16px; cursor: pointer;
      width: 100%;
    }
    .info { margin-top: 30px; color: #666; font-size: 12px; }
  </style>
</head>
<body>
  <div class="container">
    <h1>📷 图片上传</h1>
    <div class="subtitle">发送到 ESP32 显示屏</div>
    <form action="/upload" method="POST" enctype="multipart/form-data">
      <div class="upload-area">
        <input type="file" name="image" accept=".jpg,.jpeg" required>
        <div class="upload-icon">📁</div>
        <div>点击选择 JPG 图片</div>
      </div>
      <button type="submit" class="btn">上传并显示</button>
    </form>
    <div class="info">
      IP: )" + WiFi.localIP().toString() + R"(<br>
      建议尺寸: 240x320 像素
    </div>
  </div>
</body>
</html>
)";
  server.send(200, "text/html", html);
}

void setup() {

  // ========== 初始化串口==========
  Serial.begin(9600);
  delay(1000);

  // ========== 初始化文件系统==========
  if (!SPIFFS.begin(true)) {
    Serial.println("SPIFFS.begin(true) 失败,尝试强制格式化...");
    Serial.println("正在格式化 SPIFFS...");
    if (SPIFFS.format()) {
      Serial.println("格式化成功,重新挂载...");
      if (!SPIFFS.begin()) {
        Serial.println("格式化后仍然挂载失败!");
        while(1) delay(1000);
      }
    } else {
      Serial.println("格式化失败!");
      while(1) delay(1000);
    }
  }
  Serial.println("SPIFFS 挂载成功!");
  Serial.printf("总空间: %d bytes (%.2f MB)\n", 
    SPIFFS.totalBytes(), SPIFFS.totalBytes() / 1024.0 / 1024.0);
  Serial.printf("已用空间: %d bytes\n", SPIFFS.usedBytes());
  Serial.printf("可用空间: %d bytes\n", 
    SPIFFS.totalBytes() - SPIFFS.usedBytes());

  // ========== 初始化屏幕==========
  pinMode(BACKLIGHT_PIN, OUTPUT);
  digitalWrite(BACKLIGHT_PIN, HIGH);  // 点亮背光
  tft.init();
  tft.setRotation(1);  // 设置屏幕的方向 0, 1, 2 or 3
  tft.fillScreen(TFT_BLACK);  // 填充背景

  // ========== 初始化解码器==========
  TJpgDec.setJpgScale(1);        // 缩放:1=原图, 2=1/2, 4=1/4, 8=1/8
  TJpgDec.setSwapBytes(true);    // ESP32 需要颜色字节交换(关键!)
  TJpgDec.setCallback(tft_output); // 设置绘制回调

  // ========== 显示启动画面==========
  tft.setTextColor(TFT_WHITE, TFT_BLACK);
  tft.setTextDatum(MC_DATUM);
  tft.drawString("Connecting...", tft.width()/2, tft.height()/2);

  // ========== 连接WIFI=========
  WiFi.begin(ssid, password); 
  while (WiFi.status() != WL_CONNECTED) {
    delay(500);
    Serial.print(".");
  } 
  Serial.println();
  Serial.print("IP: ");
  Serial.println(WiFi.localIP());
  tft.fillScreen(TFT_BLACK);
  tft.drawString(WiFi.localIP().toString(), tft.width()/2, tft.height()/2 - 20);
  tft.drawString("Waiting for upload...", tft.width()/2, tft.height()/2 + 20);
  
  // ========== 设置路由========
  server.on("/", handleRoot);
  server.on("/upload", HTTP_POST, [](){}, handleUpload);
  server.begin();
  Serial.println("HTTP server started");
}


void loop(){
  server.handleClient();
}

相关推荐
tokepson1 天前
关于 MicroPython + ESP32-S3 的使用流程
嵌入式·esp32·micropython·技术
小灰灰搞电子2 天前
ESP32 使用ESP-IDF驱动SPI接口OLED12864源码分享
esp32·oled12864
小灰灰搞电子3 天前
ESP32 使用ESP-IDF驱动IIC接口OLED源码分享
esp32·oled
小灰灰搞电子4 天前
ESP32 使用ESP-IDF驱动DHT11温湿度计源码分享
esp32·dht11
小灰灰搞电子6 天前
ESP32 使用ESP-IDF WiFi一键配网 源码分享
esp32·一键配网
风痕天际6 天前
ESP32-S3开发教程6:硬件定时器
单片机·嵌入式硬件·嵌入式·esp32·freertos·esp32s3
小灰灰搞电子6 天前
ESP32+ESP-IDF 使用MQTT协议连接阿里云物联网平台源码分享
物联网·阿里云·esp32
小灰灰搞电子7 天前
ESP32 使用ESP-IDF 驱动红外遥控器源码分享
esp32·红外遥控
勇敢牛牛_7 天前
ESP32 + Rust 开发的简易语音助手
rust·嵌入式·esp32·语音助手