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
58 changes: 58 additions & 0 deletions firmware/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -109,3 +109,61 @@ lib_deps =
lvgl/lvgl@^9.2.0
bblanchon/ArduinoJson@^7.0.0
h2zero/NimBLE-Arduino@^2.1.1


[env:lilygo_t4s3]
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
upload_speed = 921600
monitor_speed = 115200

; Compile shared code + this board's per-board folder; exclude all other
; boards. New ports add a `+<boards/<name>/>` line in their own env block.
build_src_filter =
+<*>
-<boards/>
+<boards/lilygo_t4s3/>

build_flags =
-DBOARD_LILYGO_T4S3
-DARDUINO_USB_CDC_ON_BOOT=1
-DBOARD_HAS_PSRAM
; SY6970 charger (not AXP) — selects the right driver inside XPowersLib
-DXPOWERS_CHIP_SY6970
; NimBLE config — peripheral, 2 simultaneous connections so the OS can
; hold the HID link (Space key from BOOT button) while the daemon holds
; its own connection for the custom 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 =
; RM690B0 has been in mainline since well before 1.5.6 — pin to the
; same baseline as the 2.16 env.
moononournation/GFX Library for Arduino@^1.5.6
; SensorLib supplies TouchClassCST226 (CST226SE driver)
lewisxhe/SensorLib@^0.2.6
; XPowersLib supplies PowersSY6970 when XPOWERS_CHIP_SY6970 is defined
lewisxhe/XPowersLib@^0.2.7
lvgl/lvgl@^9.2.0
bblanchon/ArduinoJson@^7.0.0
h2zero/NimBLE-Arduino@^2.1.1
56 changes: 56 additions & 0 deletions firmware/src/boards/lilygo_t4s3/board.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
#pragma once

// LilyGO T4-S3 — 450x600 portrait AMOLED kit.
// RM690B0 + CST226SE touch + SY6970 charger. No IMU, no PMU push-button,
// no second GPIO button (GPIO 18 is the panel TE line). Single physical
// input is the BOOT button; a long hold synthesizes a PWR press so the
// shared screen-cycling path still works.

#define BOARD_NAME "LilyGO T4-S3"

// ---- Display geometry ----
#define LCD_WIDTH 450
#define LCD_HEIGHT 600

// The RM690B0 controller addresses a 482-wide RAM but only the middle
// 450 columns are wired to glass — every draw needs col_offset = 16 so
// pixels land inside the visible viewport.
#define LCD_COL_OFFSET 16

// ---- QSPI display pins (RM690B0) ----
#define LCD_CS 11
#define LCD_SCLK 15
#define LCD_SDIO0 14
#define LCD_SDIO1 10
#define LCD_SDIO2 16
#define LCD_SDIO3 12
#define LCD_RESET 13
// GPIO 18 is the panel's Tearing-Effect line — reserved, do not reuse.

// ---- I2C bus (touch + PMU share one bus) ----
#define IIC_SDA 6
#define IIC_SCL 7

// ---- Touch (CST226SE via SensorLib TouchClassCST226) ----
#define TP_INT 8
#define TP_RST 17
#define CST226_ADDR 0x5A

// ---- PMU (SY6970 charger; not a fuel gauge) ----
#define SY6970_ADDR 0x6A
#define PMU_IRQ 5
#define PMICEN_GPIO 9 // drive HIGH to power the PMU's LDO rails

// ---- Buttons ----
#define BTN_BACK_GPIO 0 // BOOT — primary, Space (PTT)
// No second GPIO button on this board. The SY6970 has no user push-button
// either, so screen cycling is mapped to a long press of BOOT — see
// power.cpp. Keep this threshold above a comfortable voice-mode PTT hold.
#define PWR_LONGPRESS_MS 1500

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

// The panel and touch controller are powered by an LDO rail gated by
// PMICEN. The display init touches I2C-PMU registers and the QSPI lines,
// both of which sit behind that rail — so PMICEN must go HIGH before
// display_hal_init() runs. The 50 ms settling time matches what worked
// reliably during the original bring-up; shorter delays produced
// intermittent panel-init failures from boot.
extern "C" void board_init(void) {
pinMode(PMICEN_GPIO, OUTPUT);
digitalWrite(PMICEN_GPIO, HIGH);
delay(50);

Wire.begin(IIC_SDA, IIC_SCL);
}
14 changes: 14 additions & 0 deletions firmware/src/boards/lilygo_t4s3/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 = (uint8_t)(1 + BOARD_HAS_SECONDARY_BUTTON),
.has_rotation = (bool)BOARD_HAS_ROTATION,
.has_battery = (bool)BOARD_HAS_BATTERY,
.has_imu = (bool)BOARD_HAS_IMU,
};

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

// RM690B0 native portrait — no rotation buffer needed.

static Arduino_DataBus* bus = nullptr;
static Arduino_RM690B0* gfx = nullptr;

void display_hal_init(void) {
bus = new Arduino_ESP32QSPI(
LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3);
// The visible viewport is the middle 450 columns of a 482-wide controller
// RAM, so every transfer needs col_offset = LCD_COL_OFFSET on both
// sides. Row offset is 0.
gfx = new Arduino_RM690B0(
bus, LCD_RESET, 0 /* rotation */,
LCD_WIDTH, LCD_HEIGHT,
LCD_COL_OFFSET, 0, LCD_COL_OFFSET, 0);
}

void display_hal_begin(void) {
gfx->begin();
gfx->fillScreen(0x0000);
gfx->setBrightness(200);
}

void display_hal_set_brightness(uint8_t level) {
if (gfx) gfx->setBrightness(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;
gfx->draw16bitRGBBitmap(x, y, (uint16_t*)pixels, w, h);
}

void display_hal_tick(void) {
// No-op: no IMU-driven rotation on this board.
}

// RM690B0 expects even-aligned flush regions like the other QSPI AMOLEDs.
void display_hal_round_area(int32_t* x1, int32_t* y1, int32_t* x2, int32_t* y2) {
*x1 = *x1 & ~1;
*y1 = *y1 & ~1;
*x2 = *x2 | 1;
*y2 = *y2 | 1;
}
7 changes: 7 additions & 0 deletions firmware/src/boards/lilygo_t4s3/imu.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#include "../../hal/imu_hal.h"

// No IMU on the LilyGO T4-S3. Rotation is fixed at 0.

void imu_hal_init(void) {}
void imu_hal_tick(void) {}
uint8_t imu_hal_rotation_quadrant(void) { return 0; }
22 changes: 22 additions & 0 deletions firmware/src/boards/lilygo_t4s3/input.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#include "../../hal/input_hal.h"
#include "board.h"
#include <Arduino.h>

// Only the BOOT button is wired as a user input — GPIO 18 carries the
// panel TE signal so the second-button slot stays empty. Screen cycling
// piggybacks on a long press of the same GPIO, synthesized as a PWR
// press inside power.cpp.

void input_hal_init(void) {
pinMode(BTN_BACK_GPIO, INPUT_PULLUP);
}

bool input_hal_is_held(InputButton btn) {
switch (btn) {
case INPUT_BTN_PRIMARY:
return digitalRead(BTN_BACK_GPIO) == LOW;
case INPUT_BTN_SECONDARY:
return false;
}
return false;
}
102 changes: 102 additions & 0 deletions firmware/src/boards/lilygo_t4s3/power.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
#include "../../hal/power_hal.h"
#include "board.h"
#include <Arduino.h>
#include <Wire.h>
#include <XPowersLib.h>

// The SY6970 is a charger IC, not a fuel gauge — no native percent
// reading is available. We approximate from the battery voltage, which is
// rough but acceptable for a desk indicator. Range is calibrated for a
// single-cell LiPo: 4.20 V → 100 %, 3.30 V → 0 %.
//
// PWR-button synthesis: the board has no PMU push-button and no second
// GPIO button, so screen cycling is mapped to a long press of GPIO 0
// (the same button input.cpp reports as PRIMARY). The two HALs observe
// the same GPIO independently — a brief press is just a Space PTT; a
// hold past PWR_LONGPRESS_MS fires one PWR press event (edge on release)
// without disturbing the in-flight HID press/release semantics.

#define BATTERY_POLL_MS 2000
#define CHARGING_POLL_MS 500
#define PWR_POLL_MS 20 // tight enough to catch a deliberate long press

static PowersSY6970 pmu;
static bool pmu_ok = false;

static int cached_pct = -1;
static bool cached_charging = false;
static bool cached_vbus = false;
static bool pwr_pressed_flag = false;
static uint32_t last_battery_ms = 0;
static uint32_t last_charging_ms = 0;
static uint32_t last_pwr_ms = 0;

// Long-press tracker for GPIO 0.
static bool btn_was_down = false;
static uint32_t btn_press_start_ms = 0;
static bool btn_long_consumed = false;

static int approx_pct_from_mv(uint16_t mv) {
if (mv == 0) return -1; // ADC not yet scanned / no battery
if (mv >= 4200) return 100;
if (mv <= 3300) return 0;
return (int)((mv - 3300) * 100UL / (4200 - 3300));
}

void power_hal_init(void) {
// PMICEN is already HIGH from board_init(); the SY6970 talks I2C
// straight away. PowersSY6970 uses init() rather than begin().
if (!pmu.init(Wire, IIC_SDA, IIC_SCL, SY6970_ADDR)) {
Serial.println("SY6970 init failed");
return;
}
pmu.enableADCMeasure(); // otherwise getBattVoltage() returns 0
pmu_ok = true;
Serial.println("SY6970 init OK");

cached_charging = pmu.isCharging();
cached_vbus = pmu.isVbusIn();
cached_pct = approx_pct_from_mv(pmu.getBattVoltage());
}

void power_hal_tick(void) {
uint32_t now = millis();

if (pmu_ok) {
if (now - last_charging_ms >= CHARGING_POLL_MS) {
last_charging_ms = now;
cached_charging = pmu.isCharging();
cached_vbus = pmu.isVbusIn();
}
if (now - last_battery_ms >= BATTERY_POLL_MS) {
last_battery_ms = now;
cached_pct = approx_pct_from_mv(pmu.getBattVoltage());
}
}

if (now - last_pwr_ms >= PWR_POLL_MS) {
last_pwr_ms = now;
bool down = (digitalRead(BTN_BACK_GPIO) == LOW);
if (down && !btn_was_down) {
btn_press_start_ms = now;
btn_long_consumed = false;
} else if (down && !btn_long_consumed &&
now - btn_press_start_ms >= PWR_LONGPRESS_MS) {
pwr_pressed_flag = true;
btn_long_consumed = true;
}
btn_was_down = down;
}
}

int power_hal_battery_pct(void) { return cached_pct; }
bool power_hal_is_charging(void) { return cached_charging; }
bool power_hal_is_vbus_in(void) { return cached_vbus; }

bool power_hal_pwr_pressed(void) {
if (pwr_pressed_flag) {
pwr_pressed_flag = false;
return true;
}
return false;
}
51 changes: 51 additions & 0 deletions firmware/src/boards/lilygo_t4s3/touch.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
#include "../../hal/touch_hal.h"
#include "board.h"
#include <Arduino.h>
#include <Wire.h>
#include <TouchDrvCSTXXX.hpp>

static TouchClassCST226 touch;

static volatile bool touch_data_ready = false;
static volatile bool touch_pressed = false;
static volatile uint16_t touch_x = 0;
static volatile uint16_t touch_y = 0;

static void IRAM_ATTR touch_isr(void) {
touch_data_ready = true;
}

void touch_hal_init(void) {
touch.setPins(TP_RST, TP_INT);
if (!touch.begin(Wire, CST226_ADDR, IIC_SDA, IIC_SCL)) {
Serial.println("CST226 init failed");
return;
}
touch.setMaxCoordinates(LCD_WIDTH, LCD_HEIGHT);
// Empirical swap/mirror for the CST226SE as mounted on the LilyGO
// T4-S3 — touch X is the panel's long axis, Y is the short axis, and
// X is mirrored relative to the display.
touch.setSwapXY(true);
touch.setMirrorXY(true, false);
pinMode(TP_INT, INPUT_PULLUP);
attachInterrupt(TP_INT, touch_isr, FALLING);
Serial.println("CST226 init OK");
}

void touch_hal_read(uint16_t* x, uint16_t* y, bool* pressed) {
if (touch_data_ready) {
touch_data_ready = false;
int16_t tx[5], ty[5];
uint8_t n = touch.getPoint(tx, ty, touch.getSupportTouchPoint());
if (n > 0) {
touch_pressed = true;
touch_x = (uint16_t)tx[0];
touch_y = (uint16_t)ty[0];
} else {
touch_pressed = false;
}
}
*x = touch_x;
*y = touch_y;
*pressed = touch_pressed;
}
Loading