Community project
Ai-thinker-exp32-cam With Ili9341 Only Batt Powe
Generated with AIThis project builds a battery-powered display controller around the ESP32 platform, using an ILI9341 TFT screen as the primary interface. The system integrates a LiPo battery with charging and protection circuitry, a boost converter for stable 5V supply, and a push button for interactive control. Builders will receive a complete wiring diagram, parts list, step-by-step assembly instructions, and firmware that manages power delivery, display rendering, and user input handling.
The firmware demonstrates SPI communication with the ILI9341 display, button debouncing logic, and display inversion toggling. This guide is ideal for makers looking to create a portable, battery-operated display project with proper power management and a responsive user interface.
Wiring diagram
Interactive · read-only
Pan and zoom to explore the wiring. Remix the project to edit it in your own workspace.
Parts list
Bill of materials| Component | Qty | Notes |
|---|---|---|
| ILI9341 240x320 SPI TFT display module (display-only)240 × 320, 3.3 V logic | 1 | 3.3 V ILI9341 display-only SPI TFT module. ESP32 firmware explicitly remaps serial data, clock, and chip select; touchscreen, MISO, and microSD features are not used. |
| LiPo 3.7V 1000mAh Battery3.7 V, 1000 mAh | 1 | Single-cell LiPo pack, nominal 3.7 V, 1000 mAh. Default rechargeable choice for portable ESP32 / Pico projects. Pair with a TP4056 charger for safe USB recharging. |
| TP4056 Li-Ion/LiPo charger module with protection1-cell protected charger | 1 | TP4056 single-cell Li-Ion/LiPo linear charger module, 5V USB input, 1A charge current (programmable). Common variants ship with DW01 protection. Pair with battery_lipo_storage for the cell. |
| Boost Converteradjusted to 5.0 V, ≥1 A | 1 | Small adjustable MT3608-style DC-DC boost converter module for stepping a lower DC input up to a higher rail such as 5V, 9V, or 12V. It is a power-path module with VIN/VOUT terminals, not a GPIO peripheral. |
| Push ButtonMomentary, normally open | 1 | Momentary push button switch |
Assembly
6 stepsConnect the protected battery charger
With every power source disconnected, connect lipo_1 positive (+V) to charger_1 B+ and lipo_1 negative (GND) to charger_1 B-. Use a TP4056 module with separate OUT+/OUT- pads, not a four-pad charger-only board.
- Tip: Check the battery connector polarity before soldering.
- Tip: Insulate all battery solder joints.
- ⚠ A LiPo battery can be damaged or become hazardous if reversed or short-circuited.
- ⚠ Do not connect the ESP32-CAM directly to the battery.
Set up the 5 V boost supply
Connect charger_1 OUT+ to boost_1 VIN+ and charger_1 OUT- to boost_1 VIN-. Power the charger from its 5 V USB input and, before attaching the ESP32-CAM, adjust or verify boost_1 is exactly 5.0 V. Then connect boost_1 VOUT+ to the ESP32-CAM 5V/VIN pin and boost_1 VOUT- to an ESP32-CAM GND pin.
- Tip: Use a multimeter to check the boost output.
- Tip: Choose a boost module rated for at least 1 A peak current.
- ⚠ Never adjust the boost converter with the ESP32-CAM connected.
- ⚠ Do not use the ESP32-CAM 3.3 V pin as a 5 V input.
Connect TFT power
Connect ili9341_1 VCC and TFT_BL (sometimes labelled LED) to the ESP32-CAM 3.3V pin. Connect ili9341_1 GND to ESP32-CAM GND.
- Tip: The ESP32-CAM, display, charger, and boost supply must share ground.
- Tip: Keep the display supply wires short.
- ⚠ Use a 3.3 V logic ILI9341 module. Do not apply the boost converter's 5 V output to display logic pins.
Wire the display-only SPI signals
Connect ili9341_1 MOSI to ESP32-CAM GPIO13, SCK to GPIO14, TFT_CS to GPIO15, TFT_DC to GPIO2, and TFT_RST to GPIO4. Leave the ILI9341 MISO and any touchscreen pins unconnected; this project uses only the display. Do not install or use a microSD card because these header pins are shared with the ESP32-CAM microSD interface.
- Tip: Keep SPI leads short to reduce display noise.
- Tip: The ILI9341 module may label MOSI as SDI or DIN, and SCK as CLK.
- ⚠ GPIO2 and GPIO15 are boot-strapping pins. Do not add pull-up or pull-down resistors to them.
- ⚠ If external wiring prevents booting, disconnect TFT power while entering programming mode, then restore it after reset.
Add the display-invert button
Connect one terminal of invert_button_1 to ESP32-CAM GPIO12 and the other terminal to GND. The firmware enables the ESP32’s internal pull-up, so no external resistor is needed. A press pulls GPIO12 low and toggles inverted display colors; the display line changes between “Button: invert OFF” and “Button: invert ON.”
- Tip: Use the two terminals that are disconnected until the button is pressed; on a four-leg tactile switch these are usually opposite sides.
- Tip: Keep the button wiring short.
- ⚠ GPIO12 is a boot-strap pin. Do not add an external pull-up resistor and do not hold the button while resetting or powering the ESP32-CAM.
- ⚠ If the board fails to boot, release the button and power-cycle it.
Charge and run the unit
Charge through charger_1's USB input only. The display will show the OV2640 camera status and count a test capture every 10 seconds; captures are checked and released immediately, not saved because microSD is unused. Press invert_button_1 briefly to invert or restore the TFT colors.
- Tip: Secure the battery away from sharp joints and the camera lens.
- Tip: Use Schematik’s Deploy button to flash the project.
- ⚠ Do not leave a LiPo battery unattended while charging.
- ⚠ A weak boost converter or thin power wiring can cause ESP32-CAM brownouts during camera captures.
Pin assignments
Board wiring reference| Pin | Connection | Type |
|---|---|---|
| 3V3 | ili9341_1 VCC | power |
| GND | ili9341_1 GND | ground |
| 3V3 | ili9341_1 TFT_BL | power |
| EXT | lipo_1 +V → TP4056 Li-Ion/LiPo charger module with protection B+ | power |
| EXT | lipo_1 GND → TP4056 Li-Ion/LiPo charger module with protection B- | ground |
| EXT | charger_1 IN+ → 5 V USB charging connector positive | power |
| EXT | charger_1 IN- → 5 V USB charging connector ground | ground |
| EXT | charger_1 OUT+ → Boost Converter VIN+ | power |
| EXT | charger_1 OUT- → Boost Converter VIN- | ground |
| VIN | boost_1 VOUT+ | power |
| GND | boost_1 VOUT- | ground |
| GPIO 13 | ili9341_1 DISPLAY_DATA | digital |
| GPIO 14 | ili9341_1 DISPLAY_CLOCK | digital |
| GPIO 15 | ili9341_1 DISPLAY_SELECT | digital |
| GPIO 2 | ili9341_1 DISPLAY_DC | digital |
| GPIO 4 | ili9341_1 DISPLAY_RESET | digital |
| GND | invert_button_1 GND | ground |
| GPIO 12 | invert_button_1 SIGNAL | digital |
Firmware
ESP32#include <Arduino.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ILI9341.h>
#include "esp_camera.h"
// Forward declarations
void showInvertState();
bool initCamera();
void drawStaticScreen();
void updateStatus();
void captureAndCount();
constexpr int TFT_MOSI = 13;
constexpr int TFT_SCK = 14;
constexpr int TFT_CS = 15;
constexpr int TFT_DC = 2;
constexpr int TFT_RST = 4;
constexpr int INVERT_BUTTON_PIN = 12;
constexpr uint32_t CAPTURE_INTERVAL_MS = 10000;
constexpr uint32_t BUTTON_DEBOUNCE_MS = 35;
// RGB565 dark gray, equivalent to approximately RGB(123, 125, 123).
constexpr uint16_t COLOR_DARK_GRAY = 0x7BEF;
// Use the portable hardware-SPI constructor; SPI.begin() below supplies the ESP32-CAM pin routing.
Adafruit_ILI9341 tft(TFT_CS, TFT_DC, TFT_RST);
bool cameraReady = false;
uint32_t lastCaptureAt = 0;
uint32_t captureCount = 0;
uint32_t failedCaptures = 0;
bool displayInverted = false;
bool lastButtonReading = HIGH;
bool stableButtonState = HIGH;
uint32_t lastButtonChangeAt = 0;
bool initCamera() {
camera_config_t config = {};
config.ledc_channel = LEDC_CHANNEL_0;
config.ledc_timer = LEDC_TIMER_0;
config.pin_d0 = 5;
config.pin_d1 = 18;
config.pin_d2 = 19;
config.pin_d3 = 21;
config.pin_d4 = 36;
config.pin_d5 = 39;
config.pin_d6 = 34;
config.pin_d7 = 35;
config.pin_xclk = 0;
config.pin_pclk = 22;
config.pin_vsync = 25;
config.pin_href = 23;
config.pin_sccb_sda = 26;
config.pin_sccb_scl = 27;
config.pin_pwdn = 32;
config.pin_reset = -1;
config.xclk_freq_hz = 20000000;
config.pixel_format = PIXFORMAT_JPEG;
config.frame_size = FRAMESIZE_QVGA;
config.jpeg_quality = 12;
config.fb_count = 1;
config.grab_mode = CAMERA_GRAB_WHEN_EMPTY;
return esp_camera_init(&config) == ESP_OK;
}
void drawStaticScreen() {
tft.fillScreen(ILI9341_BLACK);
tft.setTextWrap(false);
tft.setTextColor(ILI9341_CYAN, ILI9341_BLACK);
tft.setTextSize(2);
tft.setCursor(12, 12);
tft.print("ESP32-CAM");
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.setTextSize(1);
tft.setCursor(12, 45);
tft.print("OV2640 capture monitor");
tft.drawFastHLine(12, 62, 216, COLOR_DARK_GRAY);
tft.setCursor(12, 80);
tft.print("Camera:");
tft.setCursor(12, 112);
tft.print("Captures:");
tft.setCursor(12, 144);
tft.print("Failures:");
tft.setTextColor(ILI9341_YELLOW, ILI9341_BLACK);
tft.setCursor(12, 192);
tft.print("microSD not used");
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.setCursor(12, 216);
tft.print("Button: invert OFF");
}
void updateStatus() {
tft.fillRect(95, 76, 125, 18, ILI9341_BLACK);
tft.setCursor(95, 80);
tft.setTextSize(2);
tft.setTextColor(cameraReady ? ILI9341_GREEN : ILI9341_RED, ILI9341_BLACK);
tft.print(cameraReady ? "READY" : "ERROR");
tft.fillRect(95, 108, 125, 18, ILI9341_BLACK);
tft.setCursor(95, 112);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.print(captureCount);
tft.fillRect(95, 140, 125, 18, ILI9341_BLACK);
tft.setCursor(95, 144);
tft.setTextColor(failedCaptures ? ILI9341_RED : ILI9341_WHITE, ILI9341_BLACK);
tft.print(failedCaptures);
}
void showInvertState() {
tft.fillRect(12, 212, 216, 14, ILI9341_BLACK);
tft.setTextSize(1);
tft.setTextColor(ILI9341_WHITE, ILI9341_BLACK);
tft.setCursor(12, 216);
tft.print("Button: invert ");
tft.print(displayInverted ? "ON" : "OFF");
}
void captureAndCount() {
if (!cameraReady) return;
camera_fb_t *frame = esp_camera_fb_get();
if (frame != nullptr) {
++captureCount;
esp_camera_fb_return(frame);
} else {
++failedCaptures;
}
updateStatus();
}
void setup() {
Serial.begin(115200);
pinMode(INVERT_BUTTON_PIN, INPUT_PULLUP);
SPI.begin(TFT_SCK, -1, TFT_MOSI, TFT_CS);
tft.begin(40000000);
tft.setRotation(1);
drawStaticScreen();
cameraReady = initCamera();
updateStatus();
if (cameraReady) captureAndCount();
lastCaptureAt = millis();
}
void loop() {
const uint32_t now = millis();
const bool buttonReading = digitalRead(INVERT_BUTTON_PIN);
if (buttonReading != lastButtonReading) {
lastButtonChangeAt = now;
lastButtonReading = buttonReading;
}
if (now - lastButtonChangeAt >= BUTTON_DEBOUNCE_MS && buttonReading != stableButtonState) {
stableButtonState = buttonReading;
if (stableButtonState == LOW) {
displayInverted = !displayInverted;
tft.invertDisplay(displayInverted);
showInvertState();
}
}
if (now - lastCaptureAt >= CAPTURE_INTERVAL_MS) {
lastCaptureAt = now;
captureAndCount();
}
}“Deploy to device” opens this project in Schematik, where you can flash it to your board over USB.
Remix this project
Make it yours in one click
Open a full copy of this project in your own Schematik workspace — diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.