Search Code

Makerfabs MaTouch ESP32-S3 2.8 นิ้ว กล้องและหน้าจอสัมผัส: ภาพถ่าย 3MP ไปยังการ์ด SD

Makerfabs MaTouch ESP32-S3 2.8 นิ้ว กล้องและหน้าจอสัมผัส: ภาพถ่าย 3MP ไปยังการ์ด SD

วิวไฟน์เดอร์สด ถ่ายภาพ 3 เมกะพิกเซล บันทึกลง SD พร้อมการประทับเวลาจริง

แอปกล้องถ่ายรูปที่ใช้งานได้จริง: วิวไฟน์เดอร์สดที่ประมาณ 25 เฟรมต่อวินาที ปุ่มสัมผัสสามปุ่ม และภาพถ่ายที่บันทึกลงการ์ด microSD พร้อมการประทับวันที่และเวลาที่ถูกต้องจากนาฬิกาที่ใช้แบตเตอรี่สำรองของบอร์ด

Camera and Touchscreen running on the MaTouch AI ESP32-S3 board กล้องและหน้าจอสัมผัสทำงานบนบอร์ด MaTouch AI ESP32-S3

ปุ่มทั้งสาม

  • SNAP - ถ่ายภาพทันทีที่ความละเอียดวิวไฟน์เดอร์ รวดเร็วและทำงานได้เสมอ

  • 3 MP - ภาพถ่ายจริงขนาด 2048×1536 หน้าจอจะมืดลงประมาณสองวินาที นั่นคือไดรเวอร์กล้องกำลังรีสตาร์ท และจะอธิบายด้านล่าง

  • FLIP - สะท้อนภาพเมื่อคุณหันเลนส์ออกจากตัวเอง

ทำไมหน้าจอจึงมืดลงสำหรับการถ่ายภาพ 3 MP

รูปแบบพิกเซลของกล้องจะถูกกำหนดเมื่อไดรเวอร์เริ่มทำงาน คุณสามารถเปลี่ยนความละเอียดได้ทันที แต่คุณไม่สามารถสลับจาก RGB565 (สิ่งที่หน้าจอต้องการ) ไปเป็น JPEG (สิ่งที่ไฟล์ต้องการ) โดยไม่รีสตาร์ทไดรเวอร์ ดังนั้นเส้นทางความละเอียดสูงจึงเรียกใช้ esp_camera_deinit() รีสตาร์ทในโหมด JPEG ที่ความละเอียดเต็ม ถ่ายภาพ แล้วจึงคืนค่าวิวไฟน์เดอร์ การหยุดชั่วคราวนี้เป็นเรื่องปกติ ไม่ใช่บั๊ก

การประทับเวลาโดยไม่ต้องใช้อินเทอร์เน็ต

ภาพถ่ายจะถูกบันทึกเป็น /IMG_260730_143052.jpg - ปี เดือน วัน ชั่วโมง นาที วินาที ค่านั้นมาจากนาฬิกาเวลาจริง PCF8563T ซึ่งทำงานต่อเนื่องด้วยแบตเตอรี่ ดังนั้นเวลาจึงยังคงอยู่แม้ถอดปลั๊ก หากนาฬิกาหมดพลังงาน สเก็ตช์จะตั้งค่าเริ่มต้นจากช่วงเวลาที่สเก็ตช์ถูกคอมไพล์

การแก้ไขตัวอย่างจากผู้ผลิต

ตัวอย่างกล้องของผู้ขายเองกำหนดให้การ์ด SD อยู่บนพิน 2, 42 และ 41 หมายเลขพินเหล่านั้นคัดลอกมาจากบอร์ดอื่น - บนบอร์ดนี้它们是ไมโครโฟนพิน การ์ด SD จริงๆ แล้วอยู่บนบัส SPI ของจอแสดงผลพร้อมชิปเลือกของตัวเองที่ IO47 ซึ่งเป็นสิ่งที่โค้ดที่นี่ใช้ สิ่งนี้สำคัญหากคุณต้องการให้กล้องและไมโครโฟนทำงานพร้อมกันในสเก็ตช์เดียวกัน

การใช้บัส SPI ร่วมกัน

จอแสดงผลและการ์ด SD อยู่บนสายสามเส้นเดียวกัน ดังนั้นทั้งคู่ต้องผ่านไดรเวอร์ SPI เดียวกัน สเก็ตช์นี้ใช้ Arduino_HWSPI กับออบเจกต์ SPI มาตรฐานด้วยเหตุผลนั้นพอดี การใช้ไดรเวอร์ที่แตกต่างกันสำหรับแต่ละอุปกรณ์เป็นสาเหตุคลาสสิกของจอแสดงผลที่ค้างทันทีที่เมานต์การ์ด SD

เกี่ยวกับบอร์ด MaTouch AI ESP32-S3 2.8"

ทุกโปรเจกต์ในหน้านี้ทำงานบน MaTouch AI ESP32-S3 2.8" TFT ST7789V จาก Makerfabs เป็นบอร์ดแบบครบวงจร: หน้าจอสัมผัสสี กล้อง 3 เมกะพิกเซล ไมโครโฟนสองตัว และแอมพลิฟายเออร์ลำโพงจริง ทั้งหมดขับเคลื่อนด้วย ESP32-S3 พร้อม PSRAM 8 MB การผสมผสานนั้นทำให้โปรเจกต์ AI เหล่านี้เป็นไปได้บนบอร์ดเดียวโดยไม่ต้องเชื่อมต่ออุปกรณ์อื่นเพิ่มเติม

PSRAM 8 MB มีความสำคัญมากกว่าตัวเลขอื่นๆ ที่นี่ มันคือสิ่งที่ทำให้บอร์ดสามารถเก็บเฟรมกล้อง เสียงที่บันทึกไม่กี่วินาที หรือภาพที่เข้ารหัส base64 ในหน่วยความจำพร้อมกัน - ซึ่งไม่มีสิ่งใดพอดีกับ RAM ปกติของ ESP32

เอกสารผู้ผลิต: หน้า wiki ของ Makerfabs

ข้อมูลจำเพาะหลัก

  • โปรเซสเซอร์: ESP32-S3, dual core 240 MHz, WiFi 2.4 GHz + Bluetooth 5.0

  • หน่วยความจำ: แฟลช 16 MB, PSRAM 8 MB (จำเป็นสำหรับเกือบทุกโปรเจกต์ที่นี่)

  • จอแสดงผล: IPS 2.8", 320×240, ไดรเวอร์ ST7789V, SPI

  • ระบบสัมผัส: GT911 แบบ capacitive รองรับ 5 นิ้วพร้อมกัน

  • กล้อง: OV3660, 3 เมกะพิกเซล, สูงสุด 2048×1536

  • ไมโครโฟน: INMP441 I2S ดิจิทัลสองตัว (คู่สเตอริโอแท้)

  • ลำโพง: แอมพลิฟายเออร์คลาส D MAX98357A, 3.2 W ที่ 4 Ω

  • ที่เก็บข้อมูล: ช่องการ์ด microSD (โหมด SPI)

  • พลังงาน: USB-C, ขั้วต่อแบตเตอรี่ JST, เครื่องชาร์จ TP4056, สวิตช์เปิดปิด

  • อื่นๆ บนบอร์ด: ไฟ LED RGB WS2812B, นาฬิกาเวลาจริง PCF8563T ที่ใช้แบตเตอรี่สำรอง และเกจวัดแบตเตอรี่ MAX17048 ที่ไม่ได้ระบุไว้ในข้อมูลจำเพาะอย่างเป็นทางการ

พอร์ต USB-C ทั้งสองไม่เหมือนกัน ลำโพงของบอร์ดใช้พินสัญญาณร่วมกัน (IO19 และ IO20) กับพอร์ต USB เนทีฟ เนื่องจากพินเหล่านั้นคือสายข้อมูล USB ที่ถูกเดินสายตายตัวของ ESP32-S3 อัปโหลดและจ่ายไฟผ่าน พอร์ต USB-C CH340K เสมอ (พอร์ตที่อยู่ข้างปุ่ม RESET) และตั้งค่า USB CDC On Boot เป็น Disabled หากใช้พอร์ตผิด เสียงจะทำงานผิดปกติหรือการอัปโหลดจะล้มเหลว

การตั้งค่า Arduino IDE

การตั้งค่าเหล่านี้สำคัญ ปัญหาส่วนใหญ่ที่ผู้คนรายงานเกี่ยวกับบอร์ดนี้เกิดจากการตั้งค่าอย่างใดอย่างหนึ่งผิด และการตั้งค่าเหล่านี้จะรีเซ็ตเมื่อคุณเปลี่ยนเวอร์ชันคอร์ ดังนั้นควรตรวจสอบอีกครั้งหลังการเปลี่ยนแปลงใดๆ

การตั้งค่า

ค่า

บอร์ด

ESP32S3 Dev Module

เวอร์ชันคอร์ ESP32

2.0.17

PSRAM

OPI PSRAM

ขนาดแฟลช

16MB (128Mb)

รูปแบบพาร์ติชัน

16M Flash (3MB APP/9.9MB FATFS)

USB CDC On Boot

Disabled

ความเร็วอัปโหลด

921600

ลบแฟลชทั้งหมดก่อนอัปโหลด

Disabled

พอร์ต

พอร์ต USB-C CH340K

ใช้ ESP32 core 2.0.17 ไม่ใช่ 3.x Espressif ได้ลบโมเดลตรวจจับใบหน้าบนอุปกรณ์ออกใน core 3 ดังนั้นโปรเจกต์เกี่ยวกับใบหน้าจะไม่สามารถคอมไพล์ได้ที่นั่น การกำหนดให้ใช้ 2.0.17 จะทำให้ทุกโปรเจกต์ในหน้านี้ทำงานได้ด้วยการตั้งค่าเดียว ใน Boards Manager เมนูเลือกเวอร์ชันจะให้คุณสลับไปมาได้ตามต้องการ

ใช้ GFX Library for Arduino เวอร์ชัน 1.5.6 ไม่ใช่ 1.6.x เวอร์ชัน 1.6 ถูกสร้างสำหรับ ESP32 core 3 และอาจค้างตอนเริ่มต้นบน core 2.0.17 หากหน้าจอยังคงเป็นสีดำหลังอัปโหลด นี่คือสิ่งแรกที่ควรตรวจสอบ

ไลบรารีที่จำเป็น

ติดตั้งผ่าน Tools → Manage Libraries ใน Arduino IDE หมายเลขเวอร์ชันมีความสำคัญ - กรุณาใช้เวอร์ชันที่ระบุไว้

ไลบรารี

เวอร์ชัน

ผู้เขียน

GFX Library for Arduino

1.5.6

moononournation

bb_captouch

1.3.1

Larry Bank

การแก้ไขปัญหา

อาการ

สาเหตุและวิธีแก้ไข

หน้าจอยังคงเป็นสีดำ

เวอร์ชัน GFX library ผิด (ใช้ 1.5.6) หรือการตั้งค่าบอร์ดผิด

PSRAM alloc failed หรือข้อผิดพลาดกล้อง 0xffffffff

Tools → PSRAM ไม่ได้ตั้งค่าเป็น OPI PSRAM

ไม่สามารถอัปโหลดได้ / ไม่มีพอร์ต COM

พอร์ต USB-C ผิด หรือไม่ได้ติดตั้งไดรเวอร์ CH340

กล้องล้มเหลวและไม่สามารถกู้คืนได้

สายรีเซ็ตของกล้องเชื่อมต่อกับปุ่ม RESET ของบอร์ด ดังนั้นซอฟต์แวร์จึงไม่สามารถรีสตาร์ทได้ กด RESET หากยังล้มเหลว ให้เสียบสายแพของกล้องใหม่

ดาวน์โหลดโค้ด

สเก็ตช์ Arduino ที่สมบูรณ์สำหรับโปรเจกต์นี้ พร้อมด้วย pins.h และไฟล์อื่นๆ ที่จำเป็นทั้งหมด สามารถดาวน์โหลดได้ฟรี

ดาวน์โหลด 02_Camera_Touch

แตกไฟล์ เปิดไฟล์ .ino ใน Arduino IDE ตรวจสอบการตั้งค่าด้านบน แล้วอัปโหลดผ่านพอร์ต USB-C CH340K

สถานที่ซื้อบอร์ดนี้

ลิงก์เหล่านี้เป็นลิงก์พันธมิตร (affiliate) คุณไม่ต้องจ่ายเพิ่ม และช่วยสนับสนุนโค้ดฟรีและบทเรียนบนเว็บไซต์นี้ - ขอบคุณ

882-Arduin code for MaTouch AI ESP32S3 2.8in AI Camera: Camera and touch
ภาษา: C++
/*
 * ===========================================================================
 *  02_Camera_Touch  —  MaTouch AI ESP32-S3 2.8" TFT ST7789V
 * ===========================================================================
 *
 *  ---------------------------------------------------------------------------
----------
 *  ROBOJAX.COM  -  MaTouch AI ESP32-S3 2.8" project series
 *
 *    WATCH THE VIDEO
 *        https://youtu.be/6AL3g3tC_Hk
 *
 *    WRITTEN TUTORIALS - every project, with photos and full explanation
 *        Camera and touchscreen.... https://robojax.com/RTJ849
 *        Offline face recognition.. https://robojax.com/RTJ850
 *        AI voice assistant........ https://robojax.com/RTJ851
 *        AI vision................. https://robojax.com/RTJ852
 *
 *    GET THE BOARD - SAVE $5 with coupon code:  Robojax_Makerfab
 *        https://www.makerfabs.com/matouch-ai-esp32s3-2-8-tft-st7789v.html
 *        (enter the code at checkout)
 *
 *  All of this code is free. If it helped you, a subscribe on YouTube is
 *  the best way to support more of it.
 *  

 *  Watching the video first will save you time - it shows the Arduino IDE
 *  settings and the library versions being set up step by step.
 *  ---------------------------------------------------------------------------
 *
 *  Live camera viewfinder with a touch interface, saving real photos to the
 *  microSD card, each one stamped with the true date and time from the board's
 *  battery-backed RTC.
 *
 *  Three buttons down the right hand side:
 *
 *      SNAP    quick save. Converts the frame already on screen to JPEG.
 *              Instant, 240x240, always works.
 *
 *      3 MP    the real thing. Tears the camera down, brings it back up at
 *              2048x1536 JPEG, takes the shot, and restores the viewfinder.
 *              Takes a couple of seconds. This is the OV3660's full
 *              3 megapixels, not an upscale.
 *
 *      MIRROR  flips the image, for when you turn the board around to point
 *              the lens at someone else.
 *
 *  Photos land on the SD card as  /IMG_260729_143052.jpg
 *
 *  No WiFi, no API keys, no internet.
 *
 *  ---------------------------------------------------------------------------
 *  WHY THE 3 MP BUTTON HAS TO WORK SO HARD
 *  A camera's pixel format is fixed when the driver starts. You can change the
 *  resolution on the fly, but you cannot switch RGB565 (what the screen wants)
 *  to JPEG (what the SD card wants) without restarting the driver. So the
 *  hi-res path deinitialises and reinitialises. That is normal, and it is why
 *  the screen goes black for a moment.
 *
 *  ---------------------------------------------------------------------------
 *  BOARD SETTINGS - see pins.h. In short: ESP32S3 Dev Module, core 2.0.17,
 *  OPI PSRAM, 16MB flash, 16M partition scheme, upload via the CH340K port.
 *
 *  LIBRARIES
 *      GFX Library for Arduino   v1.5.6
 *      bb_captouch               v1.3.1
 *
 *  ---------------------------------------------------------------------------
 *  FUNCTIONS IN THIS SKETCH
 *      rtcRead() / rtcWrite(...)   read/set the PCF8563 battery-backed clock
 *      rtcSeedFromBuildTime()      set the clock from compile time if it lost power
 *      getTouch(&x,&y)       read the touch panel, mapped to screen coordinates
 *      inBox(...)            hit-test a touch against a button rectangle
 *      camStart(...)         start the camera in a given format/size
 *      camStartPreview()     start the 240x240 RGB565 live-view camera
 *      makeFilename(...)     /IMG_YYMMDD_HHMMSS.jpg from the RTC time
 *      writeToSD(...)        save a buffer to the SD card
 *      snapQuick()           SNAP button: re-encode the preview frame to JPEG
 *      snapHiRes()           3MP button: restart camera in JPEG mode, full res
 *      drawButton(...) / drawButtons() / drawStatus()   the touch UI
 *      setup() / loop()      boot / viewfinder + edge-detected button taps
 *
 *  Robojax.com
 * ===========================================================================
 */

#include <Arduino_GFX_Library.h>
#include <bb_captouch.h>
#include <Wire.h>
#include <SPI.h>
#include <SD.h>
#include "esp_camera.h"
#include "img_converters.h"      // frame2jpg()
#include "pins.h"

/* The TFT and SD share pins 13/12/48, so both must go through the SAME SPI
 * peripheral: Arduino_HWSPI wraps the standard SPI object the SD library
 * uses. (Arduino_ESP32SPI would grab a different peripheral, and the display
 * goes dark the moment the SD card claims the pins.) */
Arduino_HWSPI *bus = new Arduino_HWSPI(
    TFT_DC, TFT_CS, TFT_SCLK, TFT_MOSI, TFT_MISO, &SPI, true /* shared bus */);
Arduino_GFX *gfx = new Arduino_ST7789(bus, TFT_RES, 1, true);

BBCapTouch bbct;

/* --- layout: 240x240 viewfinder on the left, 80px button column on the right */
#define VIEW_W    240
#define BTN_X     242
#define BTN_W      78

#define BTN_SNAP_Y    2
#define BTN_HIRES_Y  62
#define BTN_MIRROR_Y 122
#define BTN_H        56
#define STATUS_Y    186

bool ok_sd     = false;
bool ok_rtc    = false;
bool mirrored  = true;      // the lens is upside down relative to the screen
int  shot_count = 0;
char status[48] = "ready";

/* --- the RTC time structure ------------------------------------------------
 * Defined up here, before any function, with explicit prototypes below.
 * WHY: the Arduino IDE auto-generates function prototypes and can insert them
 * ABOVE this struct - a function returning RtcTime then fails to compile with
 * "RtcTime does not name a type". Writing the prototypes ourselves stops the
 * IDE generating its own, so the order stays correct.
 * ------------------------------------------------------------------------- */
struct RtcTime {
  int year, mon, day, hour, min, sec;
  bool valid;
};

// explicit prototypes - do not remove (see comment above)
RtcTime rtcRead();
void    rtcWrite(int year, int mon, int day, int hour, int min, int sec);
void    rtcSeedFromBuildTime();
bool    getTouch(uint16_t *x, uint16_t *y);
bool    camStart(pixformat_t fmt, framesize_t size, int fb_count, int quality);
bool    camStartPreview();
void    makeFilename(char *out, size_t n);
bool    writeToSD(const char *path, const uint8_t *data, size_t len);
void    snapQuick();
void    snapHiRes();
void    drawButton(int y, const char *label, uint16_t colour);
void    drawButtons();
void    drawStatus();


/* ===========================================================================
 *  RTC  —  PCF8563T
 * =========================================================================== */
static inline uint8_t bcd2dec(uint8_t b) { return (uint8_t)((b >> 4) * 10 + (b & 0x0F)); }
static inline uint8_t dec2bcd(uint8_t d) { return (uint8_t)(((d / 10) << 4) | (d % 10)); }

RtcTime rtcRead() {
  RtcTime t = {2026, 1, 1, 0, 0, 0, false};
  Wire.beginTransmission(ADDR_PCF8563);
  Wire.write(0x02);
  if (Wire.endTransmission(false) != 0) return t;
  if (Wire.requestFrom((int)ADDR_PCF8563, 7) != 7) return t;

  uint8_t r[7];
  for (uint8_t i = 0; i < 7; i++) r[i] = Wire.read();

  t.valid = !(r[0] & 0x80);          // bit 7 = Voltage Low: time is unreliable
  t.sec  = bcd2dec(r[0] & 0x7F);
  t.min  = bcd2dec(r[1] & 0x7F);
  t.hour = bcd2dec(r[2] & 0x3F);
  t.day  = bcd2dec(r[3] & 0x3F);
  t.mon  = bcd2dec(r[5] & 0x1F);
  t.year = bcd2dec(r[6]) + 2000;
  return t;
}

void rtcWrite(int year, int mon, int day, int hour, int min, int sec) {
  Wire.beginTransmission(ADDR_PCF8563);
  Wire.write(0x02);
  Wire.write(dec2bcd(sec));          // writing clears the Voltage Low flag
  Wire.write(dec2bcd(min));
  Wire.write(dec2bcd(hour));
  Wire.write(dec2bcd(day));
  Wire.write(0x00);                  // weekday - we do not use it
  Wire.write(dec2bcd(mon));          // century bit left at 0 => 20xx
  Wire.write(dec2bcd(year % 100));
  Wire.endTransmission();
}

/* If the RTC has lost power, seed it from the moment this sketch was compiled.
 * Close enough for photo timestamps, and it means the clock is never blank. */
void rtcSeedFromBuildTime() {
  const char *mon_names = "JanFebMarAprMayJunJulAugSepOctNovDec";
  char mon_str[4] = {0};
  int day, year, hour, min, sec;

  sscanf(__DATE__, "%3s %d %d", mon_str, &day, &year);
  sscanf(__TIME__, "%d:%d:%d", &hour, &min, &sec);

  int mon = 1;
  for (int i = 0; i < 12; i++)
    if (strncmp(mon_str, mon_names + i * 3, 3) == 0) { mon = i + 1; break; }

  rtcWrite(year, mon, day, hour, min, sec);
  Serial.printf("RTC seeded from build time: %04d-%02d-%02d %02d:%02d:%02d\n",
                year, mon, day, hour, min, sec);
}


/* ===========================================================================
 *  Touch
 * =========================================================================== */
bool getTouch(uint16_t *x, uint16_t *y) {
  TOUCHINFO ti;
  if (!bbct.getSamples(&ti)) return false;
  if (ti.count < 1) return false;
  *x = ti.y[0];
  *y = (ti.x[0] > 240) ? 0 : (240 - ti.x[0]);
  return true;
}

static inline bool inBox(uint16_t x, uint16_t y, int bx, int by, int bw, int bh) {
  return (x >= bx && x < bx + bw && y >= by && y < by + bh);
}


/* ===========================================================================
 *  Camera
 * =========================================================================== */
bool camStart(pixformat_t fmt, framesize_t size, int fb_count, int quality) {
  camera_config_t c;
  c.ledc_channel = LEDC_CHANNEL_0;
  c.ledc_timer   = LEDC_TIMER_0;
  c.pin_d0 = CAM_PIN_D0;  c.pin_d1 = CAM_PIN_D1;
  c.pin_d2 = CAM_PIN_D2;  c.pin_d3 = CAM_PIN_D3;
  c.pin_d4 = CAM_PIN_D4;  c.pin_d5 = CAM_PIN_D5;
  c.pin_d6 = CAM_PIN_D6;  c.pin_d7 = CAM_PIN_D7;
  c.pin_xclk     = CAM_PIN_XCLK;
  c.pin_pclk     = CAM_PIN_PCLK;
  c.pin_vsync    = CAM_PIN_VSYNC;
  c.pin_href     = CAM_PIN_HREF;
  /* Share the I2C bus Wire already drives (touch/RTC live there too) instead
   * of letting the camera install a second driver on the same pins - that
   * kills touch and the RTC. Wire.begin() must run before this function. */
  c.pin_sccb_sda = -1;
  c.pin_sccb_scl = -1;
  c.sccb_i2c_port = 0;               // Wire = I2C port 0
  c.pin_pwdn     = CAM_PIN_PWDN;
  c.pin_reset    = CAM_PIN_RESET;
  c.xclk_freq_hz = 20000000;
  c.frame_size   = size;
  c.pixel_format = fmt;
  c.grab_mode    = CAMERA_GRAB_WHEN_EMPTY;
  c.fb_location  = CAMERA_FB_IN_PSRAM;
  c.jpeg_quality = quality;
  c.fb_count     = fb_count;

  esp_err_t err = esp_camera_init(&c);
  if (err != ESP_OK) {
    Serial.printf("esp_camera_init failed: 0x%x\n", err);
    return false;
  }

  sensor_t *s = esp_camera_sensor_get();
  if (s) {
    s->set_hmirror(s, mirrored ? 1 : 0);
    s->set_vflip(s,   mirrored ? 1 : 0);
    s->set_brightness(s, 1);
    s->set_saturation(s, 0);
  }
  return true;
}

bool camStartPreview() {
  if (camStart(PIXFORMAT_RGB565, FRAMESIZE_240X240, 2, 12)) return true;
  /* Retry with ONE frame buffer. The camera's DMA buffer must live in
   * internal RAM, and if something else grabbed it first this is the
   * difference between working and "cam_dma_config failed". */
  Serial.println("camera: retrying with a single frame buffer (low DMA RAM)");
  return camStart(PIXFORMAT_RGB565, FRAMESIZE_240X240, 1, 12);
}


/* ===========================================================================
 *  Saving
 * =========================================================================== */
void makeFilename(char *out, size_t n) {
  RtcTime t = rtcRead();
  snprintf(out, n, "/IMG_%02d%02d%02d_%02d%02d%02d.jpg",
           t.year % 100, t.mon, t.day, t.hour, t.min, t.sec);
}

bool writeToSD(const char *path, const uint8_t *data, size_t len) {
  if (!ok_sd) { snprintf(status, sizeof(status), "no SD card"); return false; }

  File f = SD.open(path, FILE_WRITE);
  if (!f) { snprintf(status, sizeof(status), "SD open failed"); return false; }

  size_t written = f.write(data, len);
  f.close();

  if (written != len) { snprintf(status, sizeof(status), "SD write short"); return false; }
  Serial.printf("Saved %s  (%u bytes)\n", path, (unsigned)len);
  return true;
}

/* Quick save: re-encode the RGB565 frame already in hand. */
void snapQuick() {
  snprintf(status, sizeof(status), "saving...");
  drawStatus();

  camera_fb_t *fb = esp_camera_fb_get();
  if (!fb) { snprintf(status, sizeof(status), "no frame"); return; }

  uint8_t *jpg = NULL;
  size_t   jpg_len = 0;
  bool ok = frame2jpg(fb, 90, &jpg, &jpg_len);
  esp_camera_fb_return(fb);

  if (!ok || !jpg) { snprintf(status, sizeof(status), "jpeg encode failed"); return; }

  char path[32];
  makeFilename(path, sizeof(path));
  if (writeToSD(path, jpg, jpg_len)) {
    shot_count++;
    snprintf(status, sizeof(status), "%s %uKB", path + 1, (unsigned)(jpg_len / 1024));
  }
  free(jpg);
}

/* Full resolution: restart the driver in JPEG mode at 2048x1536. */
void snapHiRes() {
  snprintf(status, sizeof(status), "3MP: restarting cam");
  drawStatus();
  gfx->fillRect(0, 0, VIEW_W, 240, BLACK);
  gfx->setTextColor(YELLOW);
  gfx->setCursor(60, 110);
  gfx->print("capturing 3 MP...");

  esp_camera_deinit();
  delay(120);

  if (!camStart(PIXFORMAT_JPEG, FRAMESIZE_QXGA, 1, 10)) {
    snprintf(status, sizeof(status), "3MP init failed");
    delay(120);
    if (!camStartPreview()) snprintf(status, sizeof(status), "CAM DEAD - press RESET");
    return;
  }

  // Throw away the first few frames so exposure and white balance settle,
  // otherwise the hi-res shot comes out darker than the viewfinder.
  for (int i = 0; i < 3; i++) {
    camera_fb_t *w = esp_camera_fb_get();
    if (w) esp_camera_fb_return(w);
    delay(120);
  }

  camera_fb_t *fb = esp_camera_fb_get();
  if (fb && fb->len > 0) {
    char path[32];
    makeFilename(path, sizeof(path));
    if (writeToSD(path, fb->buf, fb->len)) {
      shot_count++;
      snprintf(status, sizeof(status), "3MP %ux%u %uKB",
               fb->width, fb->height, (unsigned)(fb->len / 1024));
    }
  } else {
    snprintf(status, sizeof(status), "3MP capture failed");
  }
  if (fb) esp_camera_fb_return(fb);

  // Back to the viewfinder
  esp_camera_deinit();
  delay(120);
  if (!camStartPreview()) {
    snprintf(status, sizeof(status), "CAM DEAD - press RESET");
    gfx->fillRect(0, 0, VIEW_W, 240, RED);
  }
  drawButtons();
}


/* ===========================================================================
 *  UI
 * =========================================================================== */
void drawButton(int y, const char *label, uint16_t colour) {
  gfx->fillRoundRect(BTN_X, y, BTN_W, BTN_H, 6, colour);
  gfx->drawRoundRect(BTN_X, y, BTN_W, BTN_H, 6, WHITE);
  gfx->setTextSize(2);
  gfx->setTextColor(WHITE);
  int len = strlen(label);
  gfx->setCursor(BTN_X + (BTN_W - len * 12) / 2, y + (BTN_H - 16) / 2);
  gfx->print(label);
  gfx->setTextSize(1);
}

void drawButtons() {
  drawButton(BTN_SNAP_Y,   "SNAP",  gfx->color565(0, 90, 160));
  drawButton(BTN_HIRES_Y,  "3 MP",  gfx->color565(140, 70, 0));
  drawButton(BTN_MIRROR_Y, "FLIP",  gfx->color565(80, 80, 90));
}

void drawStatus() {
  gfx->fillRect(BTN_X, STATUS_Y, BTN_W, 240 - STATUS_Y, BLACK);
  gfx->setTextColor(CYAN);
  gfx->setTextSize(1);

  gfx->setCursor(BTN_X + 2, STATUS_Y + 2);
  gfx->printf("shots:%d", shot_count);

  RtcTime t = rtcRead();
  gfx->setCursor(BTN_X + 2, STATUS_Y + 14);
  gfx->setTextColor(ok_rtc ? GREEN : RED);
  gfx->printf("%02d:%02d:%02d", t.hour, t.min, t.sec);

  // Status text wraps across the narrow column, 13 characters at a time
  gfx->setTextColor(YELLOW);
  int row = 0;
  for (size_t i = 0; i < strlen(status) && row < 3; i += 13, row++) {
    char chunk[14] = {0};
    strncpy(chunk, status + i, 13);
    gfx->setCursor(BTN_X + 2, STATUS_Y + 26 + row * 10);
    gfx->print(chunk);
  }
}


/* ===========================================================================
 *  SETUP
 * =========================================================================== */
void setup() {
  Serial.begin(115200);
  delay(400);
  Serial.println("\n=== 02 Camera + Touch  |  Robojax.com ===");

  pinMode(TFT_BLK, OUTPUT);
  digitalWrite(TFT_BLK, LOW);
  pinMode(SD_CS, OUTPUT);
  digitalWrite(SD_CS, HIGH);

  // one shared SPI bus for TFT + SD, started before either device
  SPI.begin(TFT_SCLK, TFT_MISO, TFT_MOSI);

  gfx->begin();
  gfx->fillScreen(BLACK);
  gfx->setTextSize(1);
  digitalWrite(TFT_BLK, HIGH);

  gfx->setTextColor(CYAN);
  gfx->setCursor(4, 4);
  gfx->print("MaTouch AI - camera demo (Robojax.com)");

  // Touch. Pulses IO18, which is also the LCD reset - harmless at this point.
  bbct.init(TOUCH_SDA, TOUCH_SCL, TOUCH_RST, TOUCH_INT);
  delay(50);

  // RTC on the shared I2C bus (must be up before the camera - shared SCCB)
  Wire.begin(I2C_SDA, I2C_SCL, 100000);
  delay(20);
  RtcTime t = rtcRead();
  ok_rtc = (t.year >= 2020);
  if (ok_rtc && !t.valid) {
    Serial.println("RTC lost power - seeding from build time");
    rtcSeedFromBuildTime();
  }
  gfx->setCursor(4, 16);
  gfx->setTextColor(ok_rtc ? GREEN : RED);
  gfx->print(ok_rtc ? "RTC ok" : "RTC not found");

  /* ---- CAMERA BEFORE SD. This order matters. ----
   * The camera's DMA buffer must come from internal (not PSRAM) memory, and
   * mounting the SD card first can take enough of it that the camera driver
   * fails with "cam_dma_config failed / 0xffffffff". Camera first, SD second,
   * and both fit. (On ESP32 core 3.x the allocation is looser and the old
   * order happened to work - which is why this only appeared on 2.0.17.) */
  gfx->setCursor(4, 28);
  gfx->setTextColor(YELLOW);
  gfx->print("starting camera...");
  Serial.printf("free internal RAM before camera: %u bytes\n",
                (unsigned)heap_caps_get_free_size(MALLOC_CAP_INTERNAL));

  if (!camStartPreview()) {
    gfx->fillScreen(RED);
    gfx->setTextColor(WHITE);
    gfx->setTextSize(2);
    gfx->setCursor(20, 90);
    gfx->print("CAMERA FAILED");
    gfx->setTextSize(1);
    gfx->setCursor(20, 120);
    gfx->print("0xffffffff / cam_dma_config = out of DMA memory:");
    gfx->setCursor(20, 132);
    gfx->print("  check Tools > PSRAM = OPI PSRAM, then press RESET.");
    gfx->setCursor(20, 150);
    gfx->print("Any other error = reseat the camera FPC cable.");
    gfx->setCursor(20, 162);
    gfx->print("The camera reset line is tied to board RESET, so it");
    gfx->setCursor(20, 174);
    gfx->print("cannot be restarted from software - use the button.");
    while (1) delay(1000);
  }

  // SD card LAST. Same three wires as the display, its own chip select on IO47.
  const uint32_t speeds[] = {40000000, 20000000, 10000000, 4000000};
  for (uint8_t i = 0; i < 4 && !ok_sd; i++)
    if (SD.begin(SD_CS, SPI, speeds[i])) ok_sd = true;
  digitalWrite(SD_CS, HIGH);

  gfx->setCursor(4, 40);
  gfx->setTextColor(ok_sd ? GREEN : RED);
  gfx->print(ok_sd ? "SD ok" : "SD FAILED - photos will not save");
  Serial.println(ok_sd ? "SD ok" : "SD failed");

  delay(600);
  gfx->fillScreen(BLACK);
  drawButtons();
  drawStatus();
  Serial.println("Running. SNAP = quick, 3 MP = full resolution, FLIP = mirror.");
}


/* ===========================================================================
 *  LOOP
 * =========================================================================== */
void loop() {
  static uint32_t last_status = 0;

  /* touch edge detection: a button fires exactly once per physical tap.
   * touch_down latches on the first touched frame and only releases after
   * several consecutive untouched frames (the GT911 occasionally skips a
   * sample mid-press, which must not count as a release). */
  static bool    touch_down = false;
  static uint8_t release_count = 0;

  /* FPS: averaged over half a second and redrawn only then - redrawing it
   * every frame just makes it flicker unreadably. */
  static uint32_t fps_window_start = 0;
  static uint16_t fps_frames = 0;

  // --- viewfinder ---
  camera_fb_t *fb = esp_camera_fb_get();
  if (fb) {
    gfx->draw16bitBeRGBBitmap(0, 0, (uint16_t *)fb->buf, fb->width, fb->height);
    esp_camera_fb_return(fb);
    fps_frames++;
  }

  // --- touch: act only on the moment the finger first lands ---
  uint16_t x, y;
  if (getTouch(&x, &y)) {
    release_count = 0;
    if (!touch_down) {                       // new tap, not a held finger
      touch_down = true;
      if (inBox(x, y, BTN_X, BTN_SNAP_Y, BTN_W, BTN_H)) {
        snapQuick();
        drawStatus();
      } else if (inBox(x, y, BTN_X, BTN_HIRES_Y, BTN_W, BTN_H)) {
        snapHiRes();
        drawStatus();
      } else if (inBox(x, y, BTN_X, BTN_MIRROR_Y, BTN_W, BTN_H)) {
        mirrored = !mirrored;
        sensor_t *s = esp_camera_sensor_get();
        if (s) { s->set_hmirror(s, mirrored ? 1 : 0); s->set_vflip(s, mirrored ? 1 : 0); }
        snprintf(status, sizeof(status), mirrored ? "mirrored" : "normal");
        drawStatus();
      }
    }
  } else if (touch_down) {
    // require ~4 consecutive empty reads (~130ms) before declaring release
    if (++release_count >= 4) { touch_down = false; release_count = 0; }
  }

  // --- FPS, averaged, top left corner of the viewfinder ---
  uint32_t now = millis();
  if (now - fps_window_start >= 500) {
    float fps = fps_frames * 1000.0f / (now - fps_window_start);
    fps_window_start = now;
    fps_frames = 0;
    gfx->fillRect(2, 2, 56, 10, BLACK);
    gfx->setTextColor(GREEN);
    gfx->setTextSize(1);
    gfx->setCursor(4, 3);
    gfx->printf("FPS %.1f", fps);
  }

  if (now - last_status > 1000) { last_status = now; drawStatus(); }
}

สิ่งที่คุณอาจต้องการ

แหล่งข้อมูลและเอกสารอ้างอิง

ไฟล์📁

ไฟล์ที่ต้องการ (.h)

  • secrets.h
    file for Makerfabs MaTouch AI ESP32S3 2.8" TFT Camera module
    secrets.h 0.01 MB

ไฟล์อื่น ๆ

  • pins.h
    pins file for MaTouch AI ESP32S3 2.8" camera LCD touch screen.
    pins.h 0.01 MB

แผนภาพวงจร

  • MaTouch_AI 2.8“ MaTouch AI ESP32S3 2.8" TFT ST7789V schematic
    The latest MaTouch AI board integrate I2S voice input/I2S speaker/ 3 million camera OV3660/ 320*240 resolution display, with ESP32S3 strong processor& Wifi ability, to make this board a good tool/platform for AI development with ESP32.
    MaTouch_AI 2.8“ SPI TFT ST7789V V1.1.PDF 0.15 MB