Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions firmware/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -177,3 +177,54 @@ lib_deps =
lvgl/lvgl@^9.2.0
bblanchon/ArduinoJson@^7.0.0
h2zero/NimBLE-Arduino@^2.1.1


[env:esp32_s3_4848s040]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38-1/platform-espressif32.zip
board = esp32-s3-devkitc-1
framework = arduino
board_build.arduino.memory_type = qio_opi
board_upload.flash_size = 16MB
board_upload.maximum_size = 16777216
board_build.partitions = default_16MB.csv
upload_speed = 921600
monitor_speed = 115200

build_src_filter =
+<*>
-<boards/>
+<boards/esp32_s3_4848s040/>

build_flags =
-DBOARD_4848S040
-DARDUINO_USB_CDC_ON_BOOT=1
-DBOARD_HAS_PSRAM
; NimBLE config — peripheral only, 2 connections (HID + data service)
-DCONFIG_BT_NIMBLE_ROLE_BROADCASTER=1
-DCONFIG_BT_NIMBLE_ROLE_PERIPHERAL=1
-DCONFIG_BT_NIMBLE_ROLE_CENTRAL=0
-DCONFIG_BT_NIMBLE_ROLE_OBSERVER=0
-DCONFIG_BT_NIMBLE_MAX_CONNECTIONS=2
; LVGL config via build flags
-DLV_CONF_SKIP
-DLV_COLOR_DEPTH=16
-DLV_USE_LOG=0
-DLV_USE_BAR=1
-DLV_USE_LABEL=1
-DLV_USE_ARC=1
-DLV_USE_BTN=1
-DLV_USE_LINE=1
-DLV_USE_OBJ=1
-DLV_USE_IMG=1
-DLV_USE_IMAGE=1
-DLV_USE_ANIMIMG=0
-DLV_TICK_CUSTOM=1
-DLV_USE_SNAPSHOT=1

lib_deps =
; 1.6.4+ includes Arduino_RGB_Display and Arduino_ESP32RGBPanel in mainline
moononournation/GFX Library for Arduino@^1.6.4
lvgl/lvgl@^9.2.0
bblanchon/ArduinoJson@^7.0.0
h2zero/NimBLE-Arduino@^2.1.1
; No SensorLib / XPowersLib — this board has no IMU or PMU
68 changes: 68 additions & 0 deletions firmware/src/boards/esp32_s3_4848s040/board.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
#pragma once

#define BOARD_NAME "ESP32-S3 4848S040"

// ---- Display geometry ----
#define LCD_WIDTH 480
#define LCD_HEIGHT 480

// ---- ST7701S: 3-wire SPI for init commands ----
#define TFT_CS 39
#define TFT_SCLK 48
#define TFT_MOSI 47

// ---- ST7701S: RGB parallel pixel interface ----
#define TFT_DE 18
#define TFT_VSYNC 17
#define TFT_HSYNC 16
#define TFT_PCLK 21
// Red channel (bit 0 = LSB)
#define TFT_R0 11
#define TFT_R1 12
#define TFT_R2 13
#define TFT_R3 14
#define TFT_R4 0
// Green channel
#define TFT_G0 8
#define TFT_G1 20
#define TFT_G2 3
#define TFT_G3 46
#define TFT_G4 9
#define TFT_G5 10
// Blue channel
#define TFT_B0 4
#define TFT_B1 5
#define TFT_B2 6
#define TFT_B3 7
#define TFT_B4 15

// ---- Sync timing (from Generic Guition datasheet / OpenHASP reference) ----
#define TFT_HSYNC_POLARITY 1
#define TFT_HSYNC_FRONT_PORCH 10
#define TFT_HSYNC_PULSE_WIDTH 8
#define TFT_HSYNC_BACK_PORCH 50
#define TFT_VSYNC_POLARITY 1
#define TFT_VSYNC_FRONT_PORCH 10
#define TFT_VSYNC_PULSE_WIDTH 8
#define TFT_VSYNC_BACK_PORCH 20
#define TFT_PCLK_ACTIVE_NEG 1
#define TFT_PREFER_SPEED 12000000L

// ---- Backlight (PWM, active-high) ----
#define TFT_BCKL 38

// ---- I2C bus (touch only on this board) ----
#define IIC_SDA 19
#define IIC_SCL 45

// ---- Touch: GT911 ----
// No IRQ or RST pins connected; probe both possible addresses at init.
#define GT911_ADDR_PRIMARY 0x5D
#define GT911_ADDR_ALT 0x14

// ---- Capability flags ----
#define BOARD_HAS_SECONDARY_BUTTON 0
#define BOARD_HAS_ROTATION 0
#define BOARD_HAS_IMU 0
#define BOARD_HAS_BATTERY 0
#define BOARD_HAS_IO_EXPANDER 0
7 changes: 7 additions & 0 deletions firmware/src/boards/esp32_s3_4848s040/board_init.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#include "board.h"
#include <Arduino.h>
#include <Wire.h>

extern "C" void board_init(void) {
Wire.begin(IIC_SDA, IIC_SCL, 400000UL);
}
14 changes: 14 additions & 0 deletions firmware/src/boards/esp32_s3_4848s040/caps.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
#include "../../hal/board_caps.h"
#include "board.h"

static const BoardCaps caps = {
.name = BOARD_NAME,
.width = LCD_WIDTH,
.height = LCD_HEIGHT,
.button_count = 0,
.has_rotation = false,
.has_battery = false,
.has_imu = false,
};

const BoardCaps& board_caps(void) { return caps; }
203 changes: 203 additions & 0 deletions firmware/src/boards/esp32_s3_4848s040/display.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
#include "../../hal/display_hal.h"
#include "board.h"
#include <Arduino.h>
#include <Arduino_GFX_Library.h>

// ST7701S uses 3-wire SPI (CS/SCLK/MOSI) for the register init sequence and
// a 16-bit RGB parallel interface (DE/HSYNC/VSYNC/PCLK + R[4:0]/G[5:0]/B[4:0])
// for pixel data. The ESP32-S3 LCD DMA controller drives the RGB side via a
// full-screen framebuffer in PSRAM — it refreshes the panel continuously, so
// partial LVGL flush writes go straight into that framebuffer and are visible
// on the very next DMA cycle. No explicit flush management needed.
//
// setBrightness() is not available on RGB displays; backlight is PWM on GPIO 38.

static Arduino_DataBus* spi_bus = nullptr;
static Arduino_ESP32RGBPanel* panel = nullptr;
static Arduino_RGB_Display* gfx = nullptr;

// ST7701S init sequence for the Generic Guition 4848S040, sourced from the
// OpenHASP project (lib/Arduino_RPi_DPI_RGBPanel_mod/Arduino_RGB_Display_mod.h).
// Enum values (BEGIN_WRITE, WRITE_COMMAND_8, …) come from Arduino_DataBus.h.
static const uint8_t st7701_init_ops[] = {
BEGIN_WRITE,
WRITE_COMMAND_8, 0xFF,
WRITE_BYTES, 5, 0x77, 0x01, 0x00, 0x00, 0x10,

WRITE_C8_D16, 0xC0, 0x3B, 0x00,
WRITE_C8_D16, 0xC1, 0x0D, 0x02,
WRITE_C8_D16, 0xC2, 0x31, 0x05,
WRITE_C8_D8, 0xCD, 0x00,

WRITE_COMMAND_8, 0xB0, // Positive Voltage Gamma
WRITE_BYTES, 16,
0x00, 0x11, 0x18, 0x0E,
0x11, 0x06, 0x07, 0x08,
0x07, 0x22, 0x04, 0x12,
0x0F, 0xAA, 0x31, 0x18,

WRITE_COMMAND_8, 0xB1, // Negative Voltage Gamma
WRITE_BYTES, 16,
0x00, 0x11, 0x19, 0x0E,
0x12, 0x07, 0x08, 0x08,
0x08, 0x22, 0x04, 0x11,
0x11, 0xA9, 0x32, 0x18,

// PAGE1
WRITE_COMMAND_8, 0xFF,
WRITE_BYTES, 5, 0x77, 0x01, 0x00, 0x00, 0x11,

WRITE_C8_D8, 0xB0, 0x60, // Vop = 4.7375 V
WRITE_C8_D8, 0xB1, 0x32, // VCOM
WRITE_C8_D8, 0xB2, 0x07, // VGH = 15 V
WRITE_C8_D8, 0xB3, 0x80,
WRITE_C8_D8, 0xB5, 0x49, // VGL = −10.17 V
WRITE_C8_D8, 0xB7, 0x85,
WRITE_C8_D8, 0xB8, 0x21, // AVDD = 6.6 V, AVCL = −4.6 V
WRITE_C8_D8, 0xC1, 0x78,
WRITE_C8_D8, 0xC2, 0x78,

WRITE_COMMAND_8, 0xE0,
WRITE_BYTES, 3, 0x00, 0x1B, 0x02,

WRITE_COMMAND_8, 0xE1,
WRITE_BYTES, 11,
0x08, 0xA0, 0x00, 0x00,
0x07, 0xA0, 0x00, 0x00,
0x00, 0x44, 0x44,

WRITE_COMMAND_8, 0xE2,
WRITE_BYTES, 12,
0x11, 0x11, 0x44, 0x44,
0xED, 0xA0, 0x00, 0x00,
0xEC, 0xA0, 0x00, 0x00,

WRITE_COMMAND_8, 0xE3,
WRITE_BYTES, 4, 0x00, 0x00, 0x11, 0x11,

WRITE_C8_D16, 0xE4, 0x44, 0x44,

WRITE_COMMAND_8, 0xE5,
WRITE_BYTES, 16,
0x0A, 0xE9, 0xD8, 0xA0,
0x0C, 0xEB, 0xD8, 0xA0,
0x0E, 0xED, 0xD8, 0xA0,
0x10, 0xEF, 0xD8, 0xA0,

WRITE_COMMAND_8, 0xE6,
WRITE_BYTES, 4, 0x00, 0x00, 0x11, 0x11,

WRITE_C8_D16, 0xE7, 0x44, 0x44,

WRITE_COMMAND_8, 0xE8,
WRITE_BYTES, 16,
0x09, 0xE8, 0xD8, 0xA0,
0x0B, 0xEA, 0xD8, 0xA0,
0x0D, 0xEC, 0xD8, 0xA0,
0x0F, 0xEE, 0xD8, 0xA0,

WRITE_COMMAND_8, 0xEB,
WRITE_BYTES, 7,
0x02, 0x00, 0xE4, 0xE4,
0x88, 0x00, 0x40,

WRITE_C8_D16, 0xEC, 0x3C, 0x00,

WRITE_COMMAND_8, 0xED,
WRITE_BYTES, 16,
0xAB, 0x89, 0x76, 0x54,
0x02, 0xFF, 0xFF, 0xFF,
0xFF, 0xFF, 0xFF, 0x20,
0x45, 0x67, 0x98, 0xBA,

// VAP / VAN bank
WRITE_COMMAND_8, 0xFF,
WRITE_BYTES, 5, 0x77, 0x01, 0x00, 0x00, 0x13,
WRITE_C8_D8, 0xE5, 0xE4,

WRITE_COMMAND_8, 0xFF,
WRITE_BYTES, 5, 0x77, 0x01, 0x00, 0x00, 0x00,

WRITE_COMMAND_8, 0x21, // IPS mode
WRITE_C8_D8, 0x3A, 0x60, // RGB666 pixel format

WRITE_COMMAND_8, 0x11, // Sleep Out
END_WRITE,

DELAY, 120,

BEGIN_WRITE,
WRITE_COMMAND_8, 0x29, // Display On
END_WRITE,

BEGIN_WRITE,
WRITE_COMMAND_8, 0x20, // Display Inversion Off (normal)
END_WRITE,
};

void display_hal_init(void) {
// 3-wire SPI bus: DC=-1 (not used for RGB init protocol), no MISO
spi_bus = new Arduino_SWSPI(
GFX_NOT_DEFINED /* DC */, TFT_CS, TFT_SCLK, TFT_MOSI, GFX_NOT_DEFINED /* MISO */);

panel = new Arduino_ESP32RGBPanel(
TFT_DE, TFT_VSYNC, TFT_HSYNC, TFT_PCLK,
TFT_R0, TFT_R1, TFT_R2, TFT_R3, TFT_R4,
TFT_G0, TFT_G1, TFT_G2, TFT_G3, TFT_G4, TFT_G5,
TFT_B0, TFT_B1, TFT_B2, TFT_B3, TFT_B4,
TFT_HSYNC_POLARITY, TFT_HSYNC_FRONT_PORCH, TFT_HSYNC_PULSE_WIDTH, TFT_HSYNC_BACK_PORCH,
TFT_VSYNC_POLARITY, TFT_VSYNC_FRONT_PORCH, TFT_VSYNC_PULSE_WIDTH, TFT_VSYNC_BACK_PORCH,
TFT_PCLK_ACTIVE_NEG, TFT_PREFER_SPEED);

gfx = new Arduino_RGB_Display(
LCD_WIDTH, LCD_HEIGHT, panel,
0 /* rotation */, false /* auto_flush — LVGL drives flushing */,
spi_bus, GFX_NOT_DEFINED /* RST */,
st7701_init_ops, sizeof(st7701_init_ops));
}

void display_hal_begin(void) {
gfx->begin(GFX_NOT_DEFINED); // speed ignored for RGB panel
gfx->fillScreen(0x0000);

// Backlight: Arduino 3.x LEDC API (pioarduino / ESP-IDF 5.x)
// NOTE: 100 Hz with 8-bit resolution overflows the 18-bit ESP32-S3 LEDC
// clock_divider register (needs div=800,000; max is 262,143 at 80 MHz APB).
// Minimum safe frequency at 8-bit/80 MHz is ~305 Hz; use 1 kHz.
ledcAttach(TFT_BCKL, 1000, 8); // 1 kHz, 8-bit resolution
ledcWrite(TFT_BCKL, 200); // default brightness (~78 %)
}

void display_hal_set_brightness(uint8_t level) {
ledcWrite(TFT_BCKL, level);
}

void display_hal_fill_screen(uint16_t color) {
if (gfx) gfx->fillScreen(color);
}

void display_hal_draw_bitmap(int32_t x, int32_t y, int32_t w, int32_t h,
const uint16_t* pixels) {
if (!gfx) return;
// Use memcpy row-by-row directly into the DMA framebuffer instead of the
// GFX library's word-loop. gfx_draw_bitmap_to_framebuffer copies 2 pixels
// per iteration (~1.9 ms for a 480x40 strip); memcpy uses the compiler's
// optimised path (~0.5 ms), halving the window during which the DMA and
// CPU race over the same PSRAM region and reducing visible tearing.
uint16_t* fb = gfx->getFramebuffer();
if (!fb) return;
for (int32_t row = 0; row < h; row++) {
memcpy(&fb[(y + row) * LCD_WIDTH + x],
&pixels[row * w],
(size_t)w * sizeof(uint16_t));
}
}

void display_hal_tick(void) {
// No rotation, no transition handling needed.
}

void display_hal_round_area(int32_t* x1, int32_t* y1, int32_t* x2, int32_t* y2) {
// RGB parallel panel has no alignment constraint; this is a no-op.
(void)x1; (void)y1; (void)x2; (void)y2;
}
6 changes: 6 additions & 0 deletions firmware/src/boards/esp32_s3_4848s040/imu.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#include "../../hal/imu_hal.h"

// No IMU on this board; rotation is fixed at 0°.
void imu_hal_init(void) {}
void imu_hal_tick(void) {}
uint8_t imu_hal_rotation_quadrant(void) { return 0; }
4 changes: 4 additions & 0 deletions firmware/src/boards/esp32_s3_4848s040/input.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
#include "../../hal/input_hal.h"

void input_hal_init(void) {}
bool input_hal_is_held(InputButton btn) { (void)btn; return false; }
18 changes: 18 additions & 0 deletions firmware/src/boards/esp32_s3_4848s040/power.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
#include "../../hal/power_hal.h"
#include <Arduino.h>

// No PMU, no battery, no dedicated power button on this board.
// IDLE_SLEEP_WHEN_CHARGING=false in idle_cfg.h means idle.cpp will call
// power_hal_is_vbus_in() to keep the display on while USB is connected.
// Return true unconditionally here so the device never auto-sleeps while
// plugged in — it has no way to detect VBUS without a PMU.

void power_hal_init(void) {}
void power_hal_tick(void) {}

int power_hal_battery_pct(void) { return -1; }
bool power_hal_is_charging(void) { return false; }
bool power_hal_is_vbus_in(void) { return true; } // always-on, no PMU
bool power_hal_pwr_pressed(void) { return false; }
bool power_hal_pwr_long_pressed(void) { return false; }
bool power_hal_pwr_released(void) { return false; }
Loading