Este tutorial es parte de: Makerfabs MaTouch AI ESP32S3 2.8" Camera
La última placa MaTouch AI integra entrada de voz I2S/altavoz I2S/cámara de 3 millones de píxeles OV3660/pantalla de resolución 320*240, con el potente procesador ESP32S3 y capacidad WiFi, lo que convierte a esta placa en una buena herramienta/plataforma para el desarrollo de IA con ESP32.
Makerfabs MaTouch ESP32-S3 Pantalla táctil y cámara de 2.8": Fotos de 3MP a la tarjeta SD
Visor en vivo, fotos de 3 megapíxeles, guardadas en la SD con marca de tiempo real
Una aplicación de cámara funcional: un visor en vivo a aproximadamente 25 fotogramas por segundo, tres botones táctiles y fotos guardadas en la tarjeta microSD con la fecha y hora correctas del reloj alimentado por batería de la placa.
Cámara y pantalla táctil funcionando en la placa MaTouch AI ESP32-S3
Los tres botones
SNAP: foto instantánea a la resolución del visor. Rápido y siempre funciona.
3 MP: una foto real de 2048×1536. La pantalla se oscurece durante unos dos segundos; es el reinicio del controlador de la cámara, y se explica a continuación.
FLIP: refleja la imagen para cuando apuntas la lente en dirección opuesta a ti.
Por qué la pantalla se oscurece para la foto de 3 MP
El formato de píxeles de una cámara se fija cuando se inicia el controlador. Puedes cambiar la resolución sobre la marcha, pero no puedes cambiar de RGB565 (lo que necesita la pantalla) a JPEG (lo que necesita el archivo) sin reiniciar el controlador. Así que la ruta de alta resolución llama a esp_camera_deinit(), se reinicia en modo JPEG a resolución completa, toma la foto y luego restaura el visor. La pausa es honesta, no un error.
Marcas de tiempo sin internet
Las fotos se guardan como /IMG_260730_143052.jpg: año, mes, día, hora, minuto, segundo. Eso proviene del reloj en tiempo real PCF8563T, que se mantiene en funcionamiento con la batería, por lo que la hora sobrevive al desconectarlo. Si el reloj ha perdido energía, el sketch lo inicializa desde el momento en que se compiló.
Una corrección al ejemplo del fabricante
El ejemplo de cámara del propio proveedor define la tarjeta SD en los pines 2, 42 y 41. Esos números de pin están copiados de una placa diferente; en esta, son los pines del micrófono. La tarjeta SD está realmente en el bus SPI de la pantalla con su propia selección de chip en IO47, que es lo que usa el código aquí. Esto importa si alguna vez quieres que la cámara y los micrófonos funcionen en el mismo sketch.
Compartiendo un bus SPI
La pantalla y la tarjeta SD están en los mismos tres cables, por lo que ambas deben pasar por el mismo controlador SPI. Este sketch usa Arduino_HWSPI con el objeto SPI estándar precisamente por esa razón. Usar un controlador diferente para cada una es la causa clásica de una pantalla que se congela en el momento en que se monta la tarjeta SD.
Acerca de la placa MaTouch AI ESP32-S3 2.8"
Cada proyecto en esta página se ejecuta en la MaTouch AI ESP32-S3 2.8" TFT ST7789V de Makerfabs. Es una placa todo en uno: una pantalla táctil a color, una cámara de 3 megapíxeles, dos micrófonos y un amplificador de altavoz real, todo impulsado por un ESP32-S3 con 8 MB de PSRAM. Esa combinación es lo que hace posibles estos proyectos de IA en una sola placa sin nada más conectado.
Los 8 MB de PSRAM importan más que cualquier otro número aquí. Es lo que permite que la placa tenga en memoria un fotograma de cámara, unos segundos de audio grabado o una foto codificada en base64 al mismo tiempo; nada de eso cabe en la RAM normal del ESP32.
Documentación del fabricante: página wiki de Makerfabs.
Especificaciones clave
Procesador: ESP32-S3, doble núcleo a 240 MHz, WiFi 2.4 GHz + Bluetooth 5.0
Memoria: 16 MB de flash, 8 MB de PSRAM (requerido por casi todos los proyectos aquí)
Pantalla: IPS de 2.8", 320×240, controlador ST7789V, SPI
Táctil: GT911 capacitivo, rastrea 5 dedos a la vez
Cámara: OV3660, 3 megapíxeles, hasta 2048×1536
Micrófonos: dos micrófonos digitales I2S INMP441 (un par estéreo genuino)
Altavoz: amplificador clase D MAX98357A, 3.2 W en 4 Ω
Almacenamiento: ranura para tarjeta microSD (modo SPI)
Alimentación: USB-C, conector de batería JST, cargador TP4056, interruptor de encendido
También en la placa: LED RGB WS2812B, reloj en tiempo real PCF8563T con respaldo de batería y un medidor de batería MAX17048 que no está listado en las especificaciones oficiales
Los dos puertos USB-C no son iguales. El altavoz de la placa comparte sus pines de señal (IO19 e IO20) con el puerto USB nativo, porque esos pines son las líneas de datos USB cableadas del ESP32-S3. Siempre carga y alimenta a través del puerto USB-C CH340K (el que está junto al botón RESET) y configura USB CDC On Boot en Disabled. Usa el puerto equivocado y el audio se comportará mal o las cargas fallarán.
Configuración del IDE de Arduino
Estas configuraciones importan. La mayoría de los problemas que la gente reporta con esta placa son uno de estos ajustes incorrectos, y se restablecen cuando cambias la versión del núcleo, así que revísalos de nuevo después de cualquier cambio.
Configuración | Valor |
|---|---|
Placa | ESP32S3 Dev Module |
Versión del núcleo ESP32 | 2.0.17 |
PSRAM | OPI PSRAM |
Tamaño de flash | 16MB (128Mb) |
Esquema de particiones | 16M Flash (3MB APP/9.9MB FATFS) |
USB CDC On Boot | Disabled |
Velocidad de carga | 921600 |
Borrar todo el flash antes de cargar | Disabled |
Puerto | el puerto USB-C CH340K |
Use el núcleo ESP32 2.0.17, no el 3.x. Espressif eliminó los modelos de detección de rostros en el dispositivo en el núcleo 3, por lo que los proyectos de rostros no compilarán allí. Fijar la versión 2.0.17 mantiene todos los proyectos de esta página funcionando con una sola configuración. En el Administrador de placas, el menú desplegable de versiones le permite cambiar de una a otra cuando lo desee.
Use la versión 1.5.6 de la Biblioteca GFX para Arduino, no la 1.6.x. Las versiones 1.6 están diseñadas para el núcleo ESP32 3 y pueden colgarse al inicio en el núcleo 2.0.17. Si su pantalla permanece negra después de cargar, esto es lo primero que debe verificar.
Bibliotecas requeridas
Instálelas a través de Herramientas → Administrar bibliotecas en el IDE de Arduino. Los números de versión importan: por favor, use los que se enumeran.
Biblioteca | Versión | Autor |
|---|---|---|
Biblioteca GFX para Arduino | 1.5.6 | moononournation |
bb_captouch | 1.3.1 | Larry Bank |
Solución de problemas
Síntoma | Causa y solución |
|---|---|
La pantalla permanece negra | Versión incorrecta de la biblioteca GFX (use 1.5.6) o configuración incorrecta de la placa. |
|
|
No se carga nada / no hay puerto COM | Puerto USB-C incorrecto, o el controlador CH340 no está instalado. |
La cámara falla y nunca se recupera | La línea de reinicio de la cámara está conectada al botón RESET de la placa, por lo que el software no puede reiniciarla. Presione RESET. Si aún falla, vuelva a colocar el cable plano de la cámara. |
Descargar el código
El boceto completo de Arduino para este proyecto, junto con pins.h y todo lo demás que necesita, se puede descargar de forma gratuita.
Descomprímalo, abra el archivo .ino en el IDE de Arduino, verifique la configuración anterior y cárguelo a través del puerto USB-C CH340K.
Dónde comprar esta placa
Estos son enlaces de afiliado. No le cuestan nada extra y ayudan a apoyar el código gratuito y los tutoriales en este sitio: gracias.
Este tutorial es parte de: Makerfabs MaTouch AI ESP32S3 2.8" Camera
- Makerfabs MaTouch ESP32-S3 2.8" cámara Detección facial sin conexión en ESP32-S3 - Sin Internet, sin clave API
- Fabricante Fabs MaTouch ESP32-S3 cámara de 2.8 pulgadas Construye un asistente de voz con IA en ESP32-S3 (Azure + DeepSeek)
- Makerfabs MaTouch ESP32-S3 2.8" cámara ESP32-S3 Visión IA: Apunta la cámara y pregunta qué ve
/*
* ===========================================================================
* 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(); }
}
Cosas que podrías necesitar
-
Amazonas
-
OtroProduct page for MaTouch AI ESP32S3 2.8" TFT ST7789Vmakerfabs.com
Recursos y referencias
-
DocumentaciónMakerfabs MaTouch ESP32-S3 2.8" Camera and Touchscreen: User's Manualwiki.makerfabs.com
-
Documentación
-
DocumentaciónProduct page for MaTouch AI ESP32S3 2.8" TFT ST7789Vmakerfabs.com
-
DescargarArduino GFX Library on GitHubgithub.com
Archivos📁
Archivo Requerido (.h)
Otros Archivos
Esquemático
-
MaTouch_AI 2.8" MaTouch AI ESP32S3 2.8" TFT ST7789V esquemáticoLa última placa MaTouch AI integra entrada de voz I2S / altavoz I2S / cámara de 3 millones OV3660 / pantalla de resolución 320*240, con el potente procesador ESP32S3 y capacidad Wi-Fi, para hacer de esta placa una buena herramienta/plataforma para el desarrollo de IA con ESP32.
MaTouch_AI 2.8“ SPI TFT ST7789V V1.1.PDF0.15 MB