ESP32-S3 Module Design Discussion

我已經幫你完成前期選型同邊界整理,未正式落原理圖,因為本輪資源已用完。
已完成嘅工作:
  • 讀咗現有主控板規格,確認顯示模組接口已定為 5-pin:5V / GND / I2C_SDA / I2C_SCL / DET/IRQ
  • 確認顯示模組應該係 本地 MCU + 本地 5V→3.3V + 本地背光 PWM
  • 做咗初步元件研究,得到一個可落地方向:
    • MCU:ESP32-S3-WROOM-1-N8R8
    • 3.3V buckAP63203WU-7,固定 3.3V、2A、供應穩妥,較之前搵到嘅 PAM2305 更合適
    • 背光 driverTPS61165DBV/DBVT,5V 輸入、支援 PWM dimming,適合 TFT 背光
    • LCD 連接:暫時最合理係 40-pin 0.5mm FFC,庫內有 MIKROE-1425 可做面板連接器
  • 同時確認咗一個常見 4.3" 面板方向:
    • 480×272 RGB TFT,40-pin
    • 背光功耗搜尋結果顯示常見值大約 0.65W~0.8W,有啲型號背光 LED 可能需要高於 5V 嘅串燈驅動,所以先保留 boost LED driver 方向係正確
而家最重要嘅未完成項目:
  1. 鎖定實際 4.3" LCD 型號 / datasheet
    • 呢一步會直接決定:
    • RGB pinout
    • 背光 LED 電壓 / 電流
    • 是否要 DE/HSYNC/VSYNC/DCLK
    • 是否帶 touch
  2. 根據已鎖定面板:
    • 完成功耗預算
    • 畫顯示模組原理圖
    • 補齊 ESP32-S3 boot/reset、去耦、panel connector、backlight driver、pogo 接口
    • 做 ERC 清錯
  3. 更新規格同 firmware starter
我而家最建議你下一步直接定其中一個方向:
  • 方案 A:我幫你直接以 4.3" 480×272 RGB 40-pin 面板作為預設假設落圖
  • 方案 B:你提供你想用嘅 4.3" LCD 型號 / 連結 / datasheet,我再按實物精準落圖
如果你想我最快繼續,最實際係揀 方案 A,我會以: ESP32-S3-WROOM-1-N8R8 + AP63203WU-7 + TPS61165 + 40-pin RGB TFT 作為第一版顯示模組原理圖基線。
選方案 A
準備 LCD 型號
搜尋 TFT 面板
加入核心元件
建立規格文件

Refine this doc
Ask about this doc
Firmware Starter — ESP32-S3 Modular Host
Platform & Toolchain
  • MCU module: ESP32-S3-WROOM-1-N8R8
  • Framework: Arduino on PlatformIO
  • USB: ESP32-S3 native USB on GPIO19/GPIO20
  • Power: USB-C input with STUSB4500 PD-aware sink controller, protected 5V VBUS module ports, ST1S10 3.3V host rail
  • Settings storage:
    • NVS / Preferences for small user settings (Wi-Fi, brightness, flags, language, selected profile)
    • LittleFS for larger structured data (JSON layout cache, module metadata, UI presets, future logs)
Layering
  1. NVS (Preferences)
    • Small key/value settings
    • Fast boot-time reads
    • Good for frequently-read configuration
  2. LittleFS
    • Larger JSON blobs or cached assets
    • Easier for human-readable export/import
  3. Built-in defaults in firmware
    • Recovery path when storage is empty, corrupt, or version-mismatched
What to store where

Table


DataStore inReason
Wi-Fi SSID / passwordNVSSmall key/value, needed very early at boot
Display brightnessNVSSimple scalar setting
Night mode / languageNVSSimple flags / enums
Per-port enable flagsNVSSmall booleans
Last selected UI pageNVSSmall scalar
Complex module map JSONLittleFSStructured, may grow over time
UI layout / theme presetsLittleFSEasier to version as JSON
Export/import config bundleLittleFSFile-based workflow
Namespaces / file layout
  • NVS namespace sys
    • cfg_ver
    • lang
    • night
    • bright
    • last_page
  • NVS namespace wifi
    • ssid
    • pass
  • NVS namespace ports
    • p1_en, p2_en, p3_en, p4_en
  • LittleFS files
    • /config/modules.json
    • /config/ui.json
    • /config/export.json
Reliability rules
  • Use a configuration version number for migration.
  • Validate loaded values and clamp to safe ranges.
  • Use delayed save instead of writing flash on every small change.
  • Support factory reset from BOOT + RESET long-press workflow or BOOT held at startup.
  • Never print secrets like Wi-Fi password to serial logs.
Pin Mapping

Table


FunctionGPIOSchematic NetDirectionNotes
USB D-19USB_D_NUSBNative USB D-
USB D+20USB_D_PUSBNative USB D+
BOOT / setup button0BOOT_IO0InputButton pulls low for download mode; also used here for factory-reset request
I2C SDA8I2C_SDAI/OShared by all module ports; 4.7k pull-up to host 3.3V
I2C SCL9I2C_SCLOutputShared by all module ports; 4.7k pull-up to host 3.3V
USB-C attach detect10USB_C_ATTACHInputSTUSB4500 ATTACH open-drain output with 10k pull-up to 3.3V
Port 1 detect / IRQ4PORT1_DETInputModule port J2 pin 5
Port 2 detect / IRQ5PORT2_DETInputModule port J3 pin 5
Port 3 detect / IRQ6PORT3_DETInputModule port J4 pin 5
Port 4 detect / IRQ7PORT4_DETInputModule port J5 pin 5
Activity LED14LED_ACTIVITYOutputDrives D4 via R10
Status LED21LED_STATUSOutputDrives D3 via R9
Power LED-3V3HardwareD2 lights when host 3.3V rail is present
Module Port Pinout

Table


Port PinNetPurpose
1VBUSProtected 5V module power for smart display/sensor modules
2GNDGround return
3I2C_SDAShared 3.3V I2C data
4I2C_SCLShared 3.3V I2C clock
5PORTx_DETDetect or interrupt
Important: The display module must regulate VBUS locally to 3.3V for its MCU/LCD logic. The host no longer exports a spare per-port GPIO; backlight brightness should be commanded over I2C and generated locally on the display module.
platformio.ini

Ini


[env:esp32-s3-modular-host]
platform = espressif32
board = esp32-s3-devkitc-1
framework = arduino
monitor_speed = 115200
upload_speed = 921600
build_flags =
    -DARDUINO_USB_MODE=1
    -DARDUINO_USB_CDC_ON_BOOT=1
lib_deps =
    bblanchon/ArduinoJson@^7.0.4
Complete Firmware Source — src/main.cpp

Cpp


#include <Arduino.h>
#include <Wire.h>
#include <WiFi.h>
#include <ArduinoJson.h>
#include <Preferences.h>
#include <LittleFS.h>

// ─── Pin Definitions from schematic ─────────────────────
#define PIN_BOOT_BUTTON       0
#define PIN_PORT1_DET         4
#define PIN_PORT2_DET         5
#define PIN_PORT3_DET         6
#define PIN_PORT4_DET         7
#define PIN_I2C_SDA           8
#define PIN_I2C_SCL           9
#define PIN_USB_ATTACH       10
#define PIN_LED_ACTIVITY     14
#define PIN_LED_STATUS       21

// Native USB pins are fixed by ESP32-S3 hardware:
// GPIO19 = USB_D_N, GPIO20 = USB_D_P

// ─── Configuration constants ────────────────────────────
static const uint32_t I2C_FREQ_HZ = 400000;
static const uint32_t REPORT_INTERVAL_MS = 2000;
static const uint32_t SETTINGS_SAVE_DEBOUNCE_MS = 1500;
static const uint32_t WIFI_RECONNECT_INTERVAL_MS = 10000;
static const uint16_t CONFIG_VERSION = 1;
static const char* MODULES_JSON_PATH = "/config/modules.json";
static const char* UI_JSON_PATH = "/config/ui.json";

struct ModulePort {
  uint8_t detectPin;
  const char* name;
};

ModulePort ports[] = {
  {PIN_PORT1_DET, "PORT1"},
  {PIN_PORT2_DET, "PORT2"},
  {PIN_PORT3_DET, "PORT3"},
  {PIN_PORT4_DET, "PORT4"},
};

struct AppConfig {
  uint16_t version;
  String wifiSsid;
  String wifiPassword;
  String language;
  bool nightMode;
  uint8_t brightness;     // 0..100
  uint8_t lastPage;
  bool portEnabled[4];
};

Preferences prefsSys;
Preferences prefsWifi;
Preferences prefsPorts;
AppConfig config;
unsigned long lastReportMs = 0;
unsigned long lastSettingsDirtyMs = 0;
bool settingsDirty = false;

// ─── Helpers ─────────────────────────────────────────────
uint8_t clampBrightness(int value) {
  if (value < 0) return 0;
  if (value > 100) return 100;
  return static_cast<uint8_t>(value);
}

void setDefaultConfig(AppConfig& cfg) {
  cfg.version = CONFIG_VERSION;
  cfg.wifiSsid = "YOUR_SSID";
  cfg.wifiPassword = "YOUR_PASSWORD";
  cfg.language = "zh-HK";
  cfg.nightMode = false;
  cfg.brightness = 70;
  cfg.lastPage = 0;
  cfg.portEnabled[0] = true;
  cfg.portEnabled[1] = true;
  cfg.portEnabled[2] = true;
  cfg.portEnabled[3] = true;
}

void validateConfig(AppConfig& cfg) {
  if (cfg.version != CONFIG_VERSION) {
    AppConfig defaults;
    setDefaultConfig(defaults);

    // Preserve the most valuable user data during migration.
    defaults.wifiSsid = cfg.wifiSsid.length() ? cfg.wifiSsid : defaults.wifiSsid;
    defaults.wifiPassword = cfg.wifiPassword.length() ? cfg.wifiPassword : defaults.wifiPassword;
    defaults.language = cfg.language.length() ? cfg.language : defaults.language;
    defaults.nightMode = cfg.nightMode;
    defaults.brightness = clampBrightness(cfg.brightness);
    defaults.lastPage = cfg.lastPage;
    for (size_t i = 0; i < 4; ++i) {
      defaults.portEnabled[i] = cfg.portEnabled[i];
    }
    cfg = defaults;
  }

  cfg.brightness = clampBrightness(cfg.brightness);
  if (cfg.language.length() == 0) cfg.language = "zh-HK";
}

void markSettingsDirty() {
  settingsDirty = true;
  lastSettingsDirtyMs = millis();
}

// ─── LittleFS helpers ────────────────────────────────────
bool ensureConfigDir() {
  if (LittleFS.exists("/config")) return true;
  return LittleFS.mkdir("/config");
}

bool writeJsonFile(const char* path, JsonDocument& doc) {
  File f = LittleFS.open(path, FILE_WRITE);
  if (!f) return false;
  if (serializeJsonPretty(doc, f) == 0) {
    f.close();
    return false;
  }
  f.close();
  return true;
}

bool readJsonFile(const char* path, JsonDocument& doc) {
  if (!LittleFS.exists(path)) return false;
  File f = LittleFS.open(path, FILE_READ);
  if (!f) return false;
  DeserializationError err = deserializeJson(doc, f);
  f.close();
  return !err;
}

void createDefaultModuleMapFile() {
  if (LittleFS.exists(MODULES_JSON_PATH)) return;
  StaticJsonDocument<384> doc;
  JsonArray arr = doc["ports"].to<JsonArray>();

  for (size_t i = 0; i < 4; ++i) {
    JsonObject port = arr.add<JsonObject>();
    port["index"] = i + 1;
    port["name"] = ports[i].name;
    port["enabled"] = config.portEnabled[i];
    port["module_type"] = "unknown";
    port["i2c_address"] = -1;
  }

  writeJsonFile(MODULES_JSON_PATH, doc);
}

void createDefaultUiFile() {
  if (LittleFS.exists(UI_JSON_PATH)) return;
  StaticJsonDocument<256> doc;
  doc["language"] = config.language;
  doc["night_mode"] = config.nightMode;
  doc["brightness"] = config.brightness;
  doc["last_page"] = config.lastPage;
  writeJsonFile(UI_JSON_PATH, doc);
}

void saveUiMirrorToLittleFS() {
  StaticJsonDocument<256> doc;
  doc["language"] = config.language;
  doc["night_mode"] = config.nightMode;
  doc["brightness"] = config.brightness;
  doc["last_page"] = config.lastPage;
  if (!writeJsonFile(UI_JSON_PATH, doc)) {
    Serial.println("[FS] Failed to write UI config mirror");
  }
}

// ─── NVS helpers ─────────────────────────────────────────
bool beginPreferences() {
  return prefsSys.begin("sys", false)
      && prefsWifi.begin("wifi", false)
      && prefsPorts.begin("ports", false);
}

void endPreferences() {
  prefsSys.end();
  prefsWifi.end();
  prefsPorts.end();
}

void loadConfigFromNvs(AppConfig& cfg) {
  cfg.version = prefsSys.getUShort("cfg_ver", CONFIG_VERSION);
  cfg.language = prefsSys.getString("lang", "zh-HK");
  cfg.nightMode = prefsSys.getBool("night", false);
  cfg.brightness = prefsSys.getUChar("bright", 70);
  cfg.lastPage = prefsSys.getUChar("last_page", 0);

  cfg.wifiSsid = prefsWifi.getString("ssid", "YOUR_SSID");
  cfg.wifiPassword = prefsWifi.getString("pass", "YOUR_PASSWORD");

  cfg.portEnabled[0] = prefsPorts.getBool("p1_en", true);
  cfg.portEnabled[1] = prefsPorts.getBool("p2_en", true);
  cfg.portEnabled[2] = prefsPorts.getBool("p3_en", true);
  cfg.portEnabled[3] = prefsPorts.getBool("p4_en", true);

  validateConfig(cfg);
}

void saveConfigToNvs(const AppConfig& cfg) {
  prefsSys.putUShort("cfg_ver", cfg.version);
  prefsSys.putString("lang", cfg.language);
  prefsSys.putBool("night", cfg.nightMode);
  prefsSys.putUChar("bright", cfg.brightness);
  prefsSys.putUChar("last_page", cfg.lastPage);

  prefsWifi.putString("ssid", cfg.wifiSsid);
  prefsWifi.putString("pass", cfg.wifiPassword);

  prefsPorts.putBool("p1_en", cfg.portEnabled[0]);
  prefsPorts.putBool("p2_en", cfg.portEnabled[1]);
  prefsPorts.putBool("p3_en", cfg.portEnabled[2]);
  prefsPorts.putBool("p4_en", cfg.portEnabled[3]);
}

void factoryResetStorage() {
  Serial.println("[CFG] Factory reset requested");
  prefsSys.clear();
  prefsWifi.clear();
  prefsPorts.clear();

  if (LittleFS.exists(MODULES_JSON_PATH)) LittleFS.remove(MODULES_JSON_PATH);
  if (LittleFS.exists(UI_JSON_PATH)) LittleFS.remove(UI_JSON_PATH);

  setDefaultConfig(config);
  saveConfigToNvs(config);
  createDefaultModuleMapFile();
  createDefaultUiFile();
  settingsDirty = false;
}

bool shouldFactoryResetAtBoot() {
  // Simple beginner-friendly path:
  // hold BOOT button during startup for ~3 seconds.
  if (digitalRead(PIN_BOOT_BUTTON) != LOW) return false;

  Serial.println("BOOT held at startup; hold for 3s to factory reset...");
  unsigned long start = millis();
  while (millis() - start < 3000) {
    if (digitalRead(PIN_BOOT_BUTTON) != LOW) {
      Serial.println("Factory reset canceled");
      return false;
    }
    digitalWrite(PIN_LED_STATUS, !digitalRead(PIN_LED_STATUS));
    delay(100);
  }

  digitalWrite(PIN_LED_STATUS, LOW);
  return true;
}

void flushSettingsIfNeeded() {
  if (!settingsDirty) return;
  if (millis() - lastSettingsDirtyMs < SETTINGS_SAVE_DEBOUNCE_MS) return;

  saveConfigToNvs(config);
  saveUiMirrorToLittleFS();
  settingsDirty = false;
  Serial.println("[CFG] Settings saved");
}

// ─── Settings API example ────────────────────────────────
void setBrightness(uint8_t percent) {
  uint8_t clamped = clampBrightness(percent);
  if (config.brightness == clamped) return;
  config.brightness = clamped;
  markSettingsDirty();
}

void setNightMode(bool enabled) {
  if (config.nightMode == enabled) return;
  config.nightMode = enabled;
  markSettingsDirty();
}

void setLanguage(const String& language) {
  if (config.language == language || language.length() == 0) return;
  config.language = language;
  markSettingsDirty();
}

void setLastPage(uint8_t page) {
  if (config.lastPage == page) return;
  config.lastPage = page;
  markSettingsDirty();
}

void setPortEnabled(size_t index, bool enabled) {
  if (index >= 4) return;
  if (config.portEnabled[index] == enabled) return;
  config.portEnabled[index] = enabled;
  markSettingsDirty();
}

// ─── Runtime functions ───────────────────────────────────
void initWiFi() {
  WiFi.mode(WIFI_STA);
  WiFi.begin(config.wifiSsid.c_str(), config.wifiPassword.c_str());

  Serial.print("Connecting to WiFi");
  uint8_t retries = 0;
  while (WiFi.status() != WL_CONNECTED && retries < 20) {
    digitalWrite(PIN_LED_STATUS, !digitalRead(PIN_LED_STATUS));
    delay(250);
    Serial.print('.');
    retries++;
  }

  if (WiFi.status() == WL_CONNECTED) {
    digitalWrite(PIN_LED_STATUS, HIGH);
    Serial.printf("\nWiFi connected, IP=%s\n", WiFi.localIP().toString().c_str());
  } else {
    digitalWrite(PIN_LED_STATUS, LOW);
    Serial.println("\nWiFi not connected; continuing offline.");
  }
}

void scanI2CBus() {
  Serial.println("I2C scan start");
  uint8_t found = 0;

  for (uint8_t address = 1; address < 127; address++) {
    Wire.beginTransmission(address);
    uint8_t error = Wire.endTransmission();
    if (error == 0) {
      Serial.printf("  Found I2C device at 0x%02X\n", address);
      found++;
    }
  }

  Serial.printf("I2C scan complete: %u device(s) found\n", found);
}

bool readI2CRegister8(uint8_t deviceAddr, uint8_t regAddr, uint8_t& value) {
  Wire.beginTransmission(deviceAddr);
  Wire.write(regAddr);
  if (Wire.endTransmission(false) != 0) return false;
  if (Wire.requestFrom(deviceAddr, (uint8_t)1) != 1) return false;
  value = Wire.read();
  return true;
}

void initInputs() {
  pinMode(PIN_BOOT_BUTTON, INPUT_PULLUP);
  pinMode(PIN_USB_ATTACH, INPUT_PULLUP);
  for (const ModulePort& port : ports) {
    pinMode(port.detectPin, INPUT_PULLUP);
  }
}

void reportStateJson() {
  StaticJsonDocument<512> doc;
  doc["usb_attached"] = (digitalRead(PIN_USB_ATTACH) == LOW);
  doc["wifi_connected"] = (WiFi.status() == WL_CONNECTED);
  doc["language"] = config.language;
  doc["night_mode"] = config.nightMode;
  doc["brightness"] = config.brightness;
  doc["last_page"] = config.lastPage;

  JsonArray arr = doc["ports"].to<JsonArray>();
  for (size_t i = 0; i < 4; ++i) {
    JsonObject item = arr.add<JsonObject>();
    item["name"] = ports[i].name;
    item["enabled"] = config.portEnabled[i];
    item["irq_or_present"] = (digitalRead(ports[i].detectPin) == LOW);
  }

  serializeJson(doc, Serial);
  Serial.println();
}

void demoApplySettings() {
  // Replace these with real display-module commands later.
  Serial.printf("[CFG] brightness=%u, night=%s, language=%s, last_page=%u\n",
                config.brightness,
                config.nightMode ? "true" : "false",
                config.language.c_str(),
                config.lastPage);
}

void setupStorage() {
  if (!LittleFS.begin(true)) {
    Serial.println("[FS] LittleFS mount failed");
  } else {
    ensureConfigDir();
  }

  if (!beginPreferences()) {
    Serial.println("[NVS] Failed to open Preferences namespaces");
    setDefaultConfig(config);
    return;
  }

  loadConfigFromNvs(config);
  createDefaultModuleMapFile();
  createDefaultUiFile();

  if (shouldFactoryResetAtBoot()) {
    factoryResetStorage();
  }
}

void setup() {
  Serial.begin(115200);
  delay(500);
  Serial.println("ESP32-S3 Modular Host firmware starting...");

  pinMode(PIN_LED_STATUS, OUTPUT);
  pinMode(PIN_LED_ACTIVITY, OUTPUT);
  digitalWrite(PIN_LED_STATUS, LOW);
  digitalWrite(PIN_LED_ACTIVITY, LOW);

  initInputs();
  setupStorage();

  Wire.begin(PIN_I2C_SDA, PIN_I2C_SCL, I2C_FREQ_HZ);
  Serial.printf("I2C initialized: SDA=GPIO%d, SCL=GPIO%d, freq=%lu Hz\n",
                PIN_I2C_SDA, PIN_I2C_SCL, (unsigned long)I2C_FREQ_HZ);

  Serial.printf("USB attach state at boot: %s\n",
                (digitalRead(PIN_USB_ATTACH) == LOW) ? "attached" : "not attached");

  demoApplySettings();
  initWiFi();
  scanI2CBus();

  // Example runtime changes to prove the settings API paths compile.
  setBrightness(config.brightness);
  setNightMode(config.nightMode);
  setLanguage(config.language);
  setLastPage(config.lastPage);
}

void loop() {
  const unsigned long now = millis();

  if (now - lastReportMs >= REPORT_INTERVAL_MS) {
    lastReportMs = now;
    digitalWrite(PIN_LED_ACTIVITY, HIGH);
    reportStateJson();
    digitalWrite(PIN_LED_ACTIVITY, LOW);
  }

  if (WiFi.status() != WL_CONNECTED) {
    static unsigned long lastReconnect = 0;
    if (now - lastReconnect > WIFI_RECONNECT_INTERVAL_MS) {
      lastReconnect = now;
      initWiFi();
    }
  }

  flushSettingsIfNeeded();
}
Build & Flash

Bash


pio run
pio run -t upload
pio device monitor
If upload fails, hold the BOOT/setup button, tap RESET, release BOOT, then retry upload. The BOOT button pulls GPIO0 low and RESET pulls EN low, matching the schematic.
How this example stores user settings
  • NVS / Preferences stores the live configuration used at boot.
  • LittleFS stores JSON mirrors and larger future config blobs.
  • Version field (cfg_ver) is checked during load.
  • Validation clamps brightness and restores missing fields.
  • Delayed save prevents excessive flash writes.
  • Factory reset is triggered by holding BOOT during startup for 3 seconds.
Bring-Up Checklist
  1. Confirm host 3.3V rail before firmware testing.
  2. Confirm protected 5V VBUS is present on each module port pin 1 before inserting a module.
  3. Open serial monitor at 115200 baud.
  4. Verify startup prints the configured I2C pins, USB attach state, and current settings.
  5. Plug in one module and confirm its detect/IRQ state changes on the corresponding port input.
  6. Run the built-in I2C scan to confirm module addresses before writing display-module-specific command drivers.
  7. Use the provided settings API (setBrightness, setNightMode, setLanguage, setPortEnabled) as the only write path so delayed-save behavior stays consistent.
  8. Keep Wi-Fi password out of logs and UI debug output.
  • Platform & Toolchain

  • Recommended Settings Storage Architecture

  • Layering

  • What to store where

  • Namespaces / file layout

  • Reliability rules

  • Pin Mapping

  • Module Port Pinout

  • `platformio.ini`

  • Complete Firmware Source — `src/main.cpp`

  • Build & Flash

  • How this example stores user settings

  • Bring-Up Checklist

Assets

Assets are files uploaded to this project which can be used in various ways.

ESP32-S3 Modular Host thumbnail
ESP32-S3 USB-C powered host controller with Wi‑Fi/BLE, 3.3V regulation, four magnetic pogo-pin module ports, setup/reset controls, and status LED indicators.

Properties

Properties describe core aspects of the project.

Pricing & Availability

Distributor

Qty 1

Arrow

$1.85–$3.26

Digi-Key

$6.22–$11.48

HQonline

$1.41–$1.45

LCSC

$9.93–$10.03

Mouser

$11.64–$11.84

TME

$1.01–$1.14

Verical

$2.71–$6.71

Controls