Bruce Firmware – Build Guide for CachyOS / Arch Linux

Custom Board: ESP32_S3-16N8R_ili9341_xpt2046

Hardware:

  • ESP32-S3 QFN56 rev v0.2 · 16 MB Flash · 8 MB OPI PSRAM
  • Display: ILI9341 · Touch: XPT2046
  • Crystal: 40 MHz · Baud: 921600

1 · System Dependencies

BASH
sudo pacman -S --needed git python python-pip uv base-devel

Note: uv is available in the AUR if not already installed:

BASH
yay -S uv

2 · Python Virtual Environment (uv)

PlatformIO wird als globales Tool installiert – kein projektgebundenes .venv nötig.

BASH
# Install PlatformIO as a global uv tool
 uv tool install platformio --with littlefs-python --with fatfs-ng --with pyyaml

# Verify install and add to PATH (only once, if not already in ~/.local/bin)
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.zshrc
source ~/.zshrc

# Confirm
pio --version

Alternativ, falls ein projektgebundenes venv bevorzugt wird:

BASH
# Project-scoped venv
uv venv
uv pip install platformio
source .venv/bin/activate
pio --version

3 · Clone Bruce Firmware

BASH
git clone --recursive https://github.com/pr3y/Bruce.git
cd Bruce

--recursive ist wichtig – Bruce hat Git-Submodules.


4 · Custom Board Files erstellen

4.1 · Verzeichnisstruktur anlegen

BASH
mkdir -p boards/ESP32_S3-16N8R_ili9341_xpt2046

4.2 · pins_arduino.h

BASH
cat > boards/ESP32_S3-16N8R_ili9341_xpt2046/pins_arduino.h << 'EOF'
#ifndef Pins_Arduino_h
#define Pins_Arduino_h

#include "soc/soc_caps.h"
#include <stdint.h>

#ifndef DEVICE_NAME
#define DEVICE_NAME "ESP32-S3 16N8R ILI9341"
#endif

static const uint8_t TX   = 43;
static const uint8_t RX   = 44;
static const uint8_t SDA  = 47;
static const uint8_t SCL  = 48;
static const uint8_t SS   = 8;
static const uint8_t MOSI = 35;
static const uint8_t MISO = 37;
static const uint8_t SCK  = 36;

#define SERIAL_RX  44
#define SERIAL_TX  43
#define USB_as_HID 1

#define FP 1
#define FM 2
#define FG 3

// ── Display: ILI9341 via TFT_eSPI ───────────────────────────────────────────
#define HAS_SCREEN           1
#define ROTATION             2
#define MINBRIGHT            (uint8_t)1

#define USER_SETUP_LOADED    1
#define ILI9341_DRIVER       1
#define TFT_RGB_ORDER        TFT_BGR
#define TFT_BACKLIGHT_ON     1
#define TFT_CS               15
#define TFT_DC               16
#define TFT_RST              9
#define TFT_MOSI             18
#define TFT_SCLK             17
#define TFT_MISO             13
#define TFT_BL               21
#define SMOOTH_FONT          1
#define SPI_FREQUENCY        10000000
#define SPI_READ_FREQUENCY   8000000
#define SPI_TOUCH_FREQUENCY  2500000

// ── Touch: XPT2046 via CYD28_TouchR (bit-banged SPI) ────────────────────────
#define HAS_TOUCH          1
#define CYD28_TouchR_CS    4
#define CYD28_TouchR_IRQ   5
#define CYD28_TouchR_MOSI  11
#define CYD28_TouchR_MISO  10
#define CYD28_TouchR_CLK   12
#define TOUCH_CS           CYD28_TouchR_CS

// ── Main SPI bus (external modules) ─────────────────────────────────────────
// NOTE: CC1101_SS_PIN and SDCARD_CS are intentionally omitted.
//       Defining them as -1 causes ESP-HAL to map to GPIO 255 (0xFF),
//       which is not a valid pin → "Invalid pin: 255" boot errors.
#define SPI_SCK_PIN  36
#define SPI_MOSI_PIN 35
#define SPI_MISO_PIN 37
#define SPI_SS_PIN   8

// ── Grove I2C ────────────────────────────────────────────────────────────────
#define GROVE_SDA 47
#define GROVE_SCL 48

#endif
EOF

4.3 · interface.cpp

BASH
cat > boards/ESP32_S3-16N8R_ili9341_xpt2046/interface.cpp << 'EOF'
#include "core/powerSave.h"
#include "core/utils.h"
#include <CYD28_TouchscreenR.h>
#include <interface.h>

CYD28_TouchR touch(320, 240);

void _setup_gpio() {
    bruceConfig.colorInverted = 0;
    bruceConfigPins.rotation  = ROTATION;
    pinMode(TFT_BL, OUTPUT);
    ledcAttach(TFT_BL, 5000, 8);
    ledcWrite(TFT_BL, 255);
}

void _post_setup_gpio() {
    if (!touch.begin()) {
        Serial.println("Touch IC not Started");
        log_i("Touch IC not Started");
    } else {
        Serial.println("Touch IC Started");
    }
}

int getBattery() { return 0; }

void _setBrightness(uint8_t brightval) {
    int dutyCycle;
    if      (brightval == 100) dutyCycle = 255;
    else if (brightval == 75)  dutyCycle = 130;
    else if (brightval == 50)  dutyCycle = 70;
    else if (brightval == 25)  dutyCycle = 20;
    else if (brightval == 0)   dutyCycle = 0;
    else                       dutyCycle = ((brightval * 255) / 100);
    ledcWrite(TFT_BL, dutyCycle);
}

void InputHandler(void) {
    static unsigned long tm = millis();
    if (millis() - tm > 200 || LongPress) {
        if (touch.touched()) {
            auto t = touch.getPointScaled();
            t  = touch.getPointScaled();
            tm = millis();

            if (bruceConfigPins.rotation == 3) {
                t.y = (tftHeight + 20) - t.y;
                t.x = tftWidth - t.x;
            }
            if (bruceConfigPins.rotation == 0) {
                int tmp = t.x;
                t.x = tftWidth - t.y;
                t.y = tmp;
            }
            if (bruceConfigPins.rotation == 2) {
                int tmp = t.x;
                t.x = t.y;
                t.y = (tftHeight + 20) - tmp;
            }

            if (!wakeUpScreen()) AnyKeyPress = true;
            else return;

            touchPoint.x       = t.x;
            touchPoint.y       = t.y;
            touchPoint.pressed = true;
            touchHeatMap(touchPoint);
        } else {
            touchPoint.pressed = false;
        }
    }
}

void powerOff() {
    esp_sleep_enable_ext0_wakeup(GPIO_NUM_0, LOW);
    esp_deep_sleep_start();
}

void checkReboot() {}
EOF

4.4 · PlatformIO Environment (.ini)

Die Environment-Konfiguration wird direkt in die Haupt-platformio.ini eingetragen.
Öffne platformio.ini und füge am Ende den folgenden Block hinzu:

INI
[env:ESP32_S3-16N8R_ili9341_xpt2046]
board                        = esp32-s3-devkitc-1
board_build.partitions       = custom_16Mb.csv
board_build.arduino.memory_type = qio_opi
board_build.flash_mode       = qio
board_build.f_flash          = 80000000L
board_upload.flash_size      = 16MB
board_upload.maximum_size    = 16777216
build_src_filter             = ${env.build_src_filter} +<../boards/ESP32_S3-16N8R_ili9341_xpt2046>
build_flags =
    ${env.build_flags}
    -Iboards/ESP32_S3-16N8R_ili9341_xpt2046
    -Os
    -DCORE_DEBUG_LEVEL=1
    -DBOARD_HAS_PSRAM
    -DUSE_HSPI_PORT=1
    -DDEVICE_NAME='"ESP32-S3 16N8R ILI9341"'
upload_speed                 = 921600
monitor_speed                = 115200
lib_deps =
    ${env.lib_deps}

Warum esp32-s3-devkitc-1 statt Custom-JSON?
Der Standard-ESP32-S3-Devkit ist im PlatformIO-Board-Registry vorhanden und passt für
den QFN56 ES32-S3. Die eigentliche Hardware-Konfiguration erfolgt über die build_flags
und board_build.*-Optionen. Ein eigenes Board-JSON wäre nur nötig bei abweichenden
Upload-Protokollen oder ungewöhnlichen MCU-Varianten.


5 · Partition Table prüfen

Bruce liefert custom_16Mb.csv bereits mit. Prüfen ob vorhanden:

BASH
ls partitions/custom_16Mb.csv
# oder im Root-Verzeichnis:
ls custom_16Mb.csv

Falls nicht vorhanden, anlegen:

BASH
cat > custom_16Mb.csv << 'EOF'
# Name,   Type, SubType,  Offset,    Size,    Flags
nvs,      data, nvs,      0x9000,    0x5000,
otadata,  data, ota,      0xe000,    0x2000,
app0,     app,  ota_0,    0x10000,   0x650000,
app1,     app,  ota_1,    0x660000,  0x650000,
spiffs,   data, spiffs,   0xCB0000,  0x340000,
coredump, data, coredump, 0xFF0000,  0x10000,
EOF

6 · PlatformIO initialisieren

Beim ersten Start werden Toolchain, ESP32-Arduino-Core und alle Libraries
automatisch heruntergeladen (~500 MB, nur einmalig):

BASH
# Activate venv if you used the project-scoped approach
# source .venv/bin/activate

pio pkg update

7 · Build

BASH
pio run -e ESP32_S3-16N8R_ili9341_xpt2046

Ausgabe bei Erfolg:

TXT
RAM:   [=         ]   x.x% (used xxxxx bytes from 327680 bytes)
Flash: [===       ]  xx.x% (used xxxxxxx bytes from 16777216 bytes)
================================ [SUCCESS] ================================

Paralleles Kompilieren beschleunigen (nutzt alle CPU-Kerne):

BASH
pio run -e ESP32_S3-16N8R_ili9341_xpt2046 -j $(nproc)

8 · Flash

8.1 · Port ermitteln

BASH
# ESP32-S3 via USB-JTAG/Serial (Espressif 0x1001) erscheint als:
ls /dev/ttyACM*   # USB-JTAG bridge (häufigster Fall)
ls /dev/ttyUSB*   # Falls separater USB-Serial-Chip verbaut

8.2 · User zur dialout-Gruppe hinzufügen (einmalig)

BASH
sudo usermod -aG uucp $USER   # Arch verwendet 'uucp', nicht 'dialout'
# Logout/Login erforderlich, oder:
newgrp uucp

8.3 · Flash-Befehl

BASH
pio run -e ESP32_S3-16N8R_ili9341_xpt2046 --target upload

Mit explizitem Port (falls automatische Erkennung fehlschlägt):

BASH
pio run -e ESP32_S3-16N8R_ili9341_xpt2046 --target upload --upload-port /dev/ttyACM0

8.4 · Serial Monitor

BASH
pio device monitor -e ESP32_S3-16N8R_ili9341_xpt2046 -b 115200

Beenden: Ctrl+C


9 · ESP32-S3 Boot Mode (falls Upload fehlschlägt)

Der ESP32-S3 DevKit unterstützt automatisches Reset via DTR/RTS.
Falls das nicht funktioniert, manuell in den Download-Modus:

  1. BOOT-Taste gedrückt halten
  2. RST-Taste kurz drücken
  3. BOOT loslassen
  4. Upload starten

10 · Troubleshooting

Symptom Ursache Fix
Invalid pin: 255 -1 in pin defines CC1101_SS_PIN / SDCARD_CS auskommentiert lassen
TG1WDT_SYS_RST Boot-Loop GPIO 255 Exception Obiges Fix, neu flashen
SPIWP:0xee · mode:DIO statt QIO Flash-Mode-Mismatch board_build.flash_mode = qio in ini prüfen
Upload schlägt fehl mit Permission denied Fehlende Gruppe sudo usermod -aG uucp $USER + neu einloggen
esptool.py: command not found PlatformIO noch nicht initialisiert pio pkg update ausführen
PSRAM nicht erkannt qio_opi nicht gesetzt board_build.arduino.memory_type = qio_opi prüfen

Zusammenfassung der Dateipfade

TXT
Bruce/
├── platformio.ini                              ← env-Block hier einfügen
├── custom_16Mb.csv                             ← Partition table
└── boards/
    └── ESP32_S3-16N8R_ili9341_xpt2046/
        ├── pins_arduino.h                      ← Pin-Definitionen
        └── interface.cpp                       ← GPIO / Touch / Brightness