Skip to content
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ Boards supported out of the box:
- [Waveshare ESP32-S3-Touch-AMOLED-1.8](https://www.waveshare.com/esp32-s3-touch-amoled-1.8.htm?&aff_id=149786)
- [Waveshare ESP32-C6-Touch-AMOLED-1.8](https://www.waveshare.com/esp32-c6-touch-amoled-1.8.htm?&aff_id=149786)
- [Waveshare ESP32-S3-Touch-AMOLED-2.06](https://www.waveshare.com/esp32-s3-touch-amoled-2.06.htm?&aff_id=149786)
- [Waveshare ESP32-S3-Touch-LCD-1.54](https://www.waveshare.com/esp32-s3-lcd-1.54.htm?sku=33869) (240x240 SPI TFT, not AMOLED)

> Please check if a pull request exists for your alternative hardware port before opening a new one, providing QA feedback and testing on the same hardware is more valuable than duplicate pull requests.

Expand Down
Binary file added assets/icon_battery-charging_24.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/icon_battery-full_24.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/icon_battery-low_24.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/icon_battery-medium_24.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/icon_battery_24.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/logo_40.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
56 changes: 56 additions & 0 deletions firmware/platformio.ini
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,62 @@ lib_deps =
h2zero/NimBLE-Arduino@^2.1.1


[env:waveshare_lcd_154]
; Waveshare ESP32-S3-Touch-LCD-1.54 — 240x240 ST7789 over plain 4-wire SPI
; (first non-QSPI/TFT port), CST816T touch, battery ADC (no PMU), ES8311
; speaker. Module is ESP32-S3R8: 16 MB quad flash + 8 MB octal PSRAM
; (esptool-verified: "Embedded PSRAM 8MB (AP_3v3)", eFuse flash type quad),
; hence qio_opi and the 16 MB partition layout.
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/waveshare_lcd_154/>

build_flags =
-DBOARD_LCD_154
-DARDUINO_USB_CDC_ON_BOOT=1
-DBOARD_HAS_PSRAM
; 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 =
moononournation/GFX Library for Arduino@^1.6.4
lvgl/lvgl@^9.2.0
bblanchon/ArduinoJson@^7.0.0
h2zero/NimBLE-Arduino@^2.1.1


[env:waveshare_amoled_18]
platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38-1/platform-espressif32.zip
board = esp32-s3-devkitc-1
Expand Down
66 changes: 66 additions & 0 deletions firmware/src/boards/waveshare_lcd_154/board.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
#pragma once

// Waveshare ESP32-S3-Touch-LCD-1.54 — 1.54" square TFT kit.
// 240x240 ST7789 (plain 4-wire SPI, not QSPI) + CST816T touch + battery ADC
// (no PMU — the ETA6098 charger is standalone) + ES8311 codec + speaker.
// QMI8658 IMU is populated but unused (fixed enclosure orientation).
//
// Pin map taken from the BambuHelper project's ws_lcd_154 configuration,
// which is hardware-tested on this exact kit (display, touch, buttons,
// battery ADC and ES8311 all exercised there).

#define BOARD_NAME "Waveshare LCD 1.54"

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

// ---- SPI display pins (ST7789, 4-wire SPI) ----
#define LCD_CS 21
#define LCD_SCLK 38
#define LCD_MOSI 39
#define LCD_DC 45
#define LCD_RST 40
#define LCD_BL 46 // backlight, LEDC PWM (TFT has no brightness cmd)

// ---- I2C bus (touch + codec + IMU share one bus) ----
#define IIC_SDA 42
#define IIC_SCL 41

// ---- Touch (CST816T, minimal inline I2C reader) ----
#define TP_INT 48
#define TP_RST 47
#define CST816_ADDR 0x15

// ---- Battery (ADC divider, no I2C-readable PMU) ----
// VBAT → 3.0x divider → GPIO1. BAT_EN (GPIO2) is the power-hold line: must be
// driven HIGH early in board_init() or the board browns out on battery power.
#define BAT_EN 2
#define BAT_ADC_PIN 1
#define BAT_VOLT_DIVIDER 3.0f

// ---- Audio (ES8311 mono codec + speaker, I2S) ----
// Pins from Waveshare's factory ES8311 example. No mic path wired (the ES7210
// mic ADC is a separate chip) — DIN unused.
#define SND_I2S_MCLK 8
#define SND_I2S_BCLK 9
#define SND_I2S_WS 10 // LRCK
#define SND_I2S_DOUT 12 // ESP → ES8311 (speaker)
#define SND_I2S_DIN -1 // unused
#define SND_PA_PIN 7 // power-amp enable (HIGH = on)
#define SND_SAMPLE_RATE 44100 // must match the embedded PCM (bell_pcm.h)
#define SND_ES8311_ADDR 0x18

// ---- Buttons (active-LOW GPIOs, from the Waveshare button example) ----
#define BTN_BACK_GPIO 0 // BOOT — primary, Space (PTT)
#define BTN_FWD_GPIO 5 // secondary, Shift+Tab (mode toggle)
#define BTN_PWR_GPIO 4 // PWR-role button — screens/brightness/pairing;
// hold 8s = power off (see power.cpp)

// ---- Capability flags ----
#define BOARD_HAS_SECONDARY_BUTTON 1
#define BOARD_HAS_ROTATION 0
#define BOARD_HAS_IMU 0 // QMI8658 present but unused
#define BOARD_HAS_BATTERY 1
#define BOARD_HAS_IO_EXPANDER 0
#define BOARD_HAS_SOUND 1
19 changes: 19 additions & 0 deletions firmware/src/boards/waveshare_lcd_154/board_init.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#include "board.h"
#include <Arduino.h>
#include <Wire.h>
#include <driver/gpio.h>

// Bring up the shared I2C bus and assert the battery power-hold line. BAT_EN
// must go HIGH before anything power-hungry runs or the board browns out when
// running from battery (it is harmless on USB power). No IO expander on this
// kit; the touch reset GPIO is pulsed in touch_hal_init().
extern "C" void board_init(void) {
// Release the pad hold a power-off left behind (power.cpp latches BAT_EN
// low with gpio_hold_en before deep sleep) — without this the HIGH write
// below would not reach the pin after a USB pseudo-off wake.
gpio_hold_dis((gpio_num_t)BAT_EN);
pinMode(BAT_EN, OUTPUT);
digitalWrite(BAT_EN, HIGH);

Wire.begin(IIC_SDA, IIC_SCL);
}
16 changes: 16 additions & 0 deletions firmware/src/boards/waveshare_lcd_154/caps.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#include "../../hal/board_caps.h"
#include "board.h"

static const BoardCaps caps = {
.name = BOARD_NAME,
.width = LCD_WIDTH,
.height = LCD_HEIGHT,
// BOOT (primary) + GPIO5 (secondary). The PWR-role button on GPIO4 is
// handled in power.cpp and not counted here.
.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; }
52 changes: 52 additions & 0 deletions firmware/src/boards/waveshare_lcd_154/display.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#include "../../hal/display_hal.h"
#include "board.h"
#include <Arduino.h>
#include <Arduino_GFX_Library.h>

// ST7789 over plain 4-wire SPI — the first non-QSPI panel in the tree, but
// Arduino_GFX abstracts the bus so the HAL surface is identical. The ST7789
// has no in-panel brightness command; brightness is a LEDC PWM duty on the
// backlight GPIO instead, which keeps the idle fade working unchanged.

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

void display_hal_init(void) {
bus = new Arduino_ESP32SPI(LCD_DC, LCD_CS, LCD_SCLK, LCD_MOSI,
GFX_NOT_DEFINED /* no MISO */);
// 240x240 window of the 240x320 GRAM: rows 0-239 at rotation 0, so
// offsets are 0,0 (the 80-row offset only matters for rotations 2/3).
// ips=true — this module needs color inversion (matches the
// hardware-tested BambuHelper config's USE_ST7789_INVERT).
gfx = new Arduino_ST7789(bus, LCD_RST, 0 /* rotation */, true /* ips */,
LCD_WIDTH, LCD_HEIGHT, 0, 0, 0, 80);
}

void display_hal_begin(void) {
gfx->begin(80000000); // 80 MHz write clock, hardware-tested on this panel
gfx->fillScreen(0x0000);
ledcAttach(LCD_BL, 5000 /* Hz */, 8 /* bits */);
ledcWrite(LCD_BL, 200);
}

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

void display_hal_tick(void) {
// No rotation cycle on this board.
}

// ST7789 over SPI has no flush-region alignment requirement.
void display_hal_round_area(int32_t* x1, int32_t* y1, int32_t* x2, int32_t* y2) {
(void)x1; (void)y1; (void)x2; (void)y2;
}
9 changes: 9 additions & 0 deletions firmware/src/boards/waveshare_lcd_154/imu.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#include "../../hal/imu_hal.h"

// QMI8658 is populated on the kit (I2C 0x6B) but the enclosure mounts the
// panel at a fixed orientation, so rotation is off and the IMU stays
// uninitialized.

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

void input_hal_init(void) {
pinMode(BTN_BACK_GPIO, INPUT_PULLUP);
#if BOARD_HAS_SECONDARY_BUTTON
pinMode(BTN_FWD_GPIO, INPUT_PULLUP);
#endif
}

bool input_hal_is_held(InputButton btn) {
switch (btn) {
case INPUT_BTN_PRIMARY:
return digitalRead(BTN_BACK_GPIO) == LOW;
case INPUT_BTN_SECONDARY:
#if BOARD_HAS_SECONDARY_BUTTON
return digitalRead(BTN_FWD_GPIO) == LOW;
#else
return false;
#endif
}
return false;
}
130 changes: 130 additions & 0 deletions firmware/src/boards/waveshare_lcd_154/power.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#include "../../hal/power_hal.h"
#include "../../hal/display_hal.h"
#include "board.h"
#include <Arduino.h>
#include <driver/gpio.h>
#include <esp_sleep.h>

// No PMU on this kit — the ETA6098 charger is standalone and exposes nothing
// over I2C. Battery percentage comes from the VBAT divider on BAT_ADC_PIN;
// charging / VBUS state is unknowable, so both report false.
//
// The PWR-role button is a plain active-LOW GPIO. Same software edge
// synthesis as the AMOLED-1.8 port (which polls an IO expander instead):
// short — fired on release if the hold was shorter than PWR_LONG_MS
// long — fired once when a hold crosses PWR_LONG_MS
// release — fired on every release edge
// This keeps the hold-to-pair gesture logic in main.cpp board-agnostic.
//
// Power OFF mirrors the AXP boards' 8-second hardware shutdown, synthesized
// in software since there is no PMU: holding the PWR button to 8 s drops the
// BAT_EN latch (hard power cut on battery) and deep-sleeps (pseudo-off on
// USB, where the rail can't be cut). The pairing gesture in main.cpp already
// disarms at 6 s precisely to leave this window free, so the timings compose
// without any shared-code change. Power back ON: center PWR button (battery,
// re-raises the latched rail) or BOOT press / reset (USB deep sleep, via the
// ext0 wake armed below).

#define BATTERY_POLL_MS 2000
#define PWR_POLL_MS 50
#define PWR_LONG_MS 1500
#define PWR_OFF_HOLD_MS 8000 // mirrors the AXP2101 PKEY 8s shutdown

static int cached_pct = -1;
static bool pwr_pressed_flag = false;
static bool pwr_long_flag = false;
static bool pwr_released_flag = false;
static bool last_pwr_state = false;
static uint32_t pwr_press_started_ms = 0;
static bool pwr_long_fired = false;
static uint32_t last_battery_ms = 0;
static uint32_t last_pwr_ms = 0;

static void sample_battery(void) {
// Average a few reads — the divider is high-impedance and single ADC
// samples on the S3 are noisy.
uint32_t mv = 0;
for (int i = 0; i < 4; i++) mv += analogReadMilliVolts(BAT_ADC_PIN);
float vbat = (mv / 4) * BAT_VOLT_DIVIDER / 1000.0f;

if (vbat < 3.0f) { // divider floating — no battery connected
cached_pct = -1;
return;
}
// Linear 3.3 V → 0%, 4.2 V → 100%. Crude but serviceable for a
// four-state indicator icon.
int pct = (int)((vbat - 3.3f) * (100.0f / 0.9f) + 0.5f);
cached_pct = pct < 0 ? 0 : pct > 100 ? 100 : pct;
}

static void power_off(void) {
Serial.println("PWR held 8s — powering off");
Serial.flush();
display_hal_set_brightness(0);
delay(50);

// Drop the battery power-hold latch and keep it low through deep sleep.
// On battery this cuts the rail outright; on USB the chip deep-sleeps.
digitalWrite(BAT_EN, LOW);
gpio_hold_en((gpio_num_t)BAT_EN);
esp_sleep_pd_config(ESP_PD_DOMAIN_RTC_PERIPH, ESP_PD_OPTION_ON);
// On USB power, let a BOOT press wake the pseudo-off state (GPIO0 is
// RTC-capable on the S3). Irrelevant on battery — the rail is gone.
esp_sleep_enable_ext0_wakeup(GPIO_NUM_0, 0);
delay(200);
esp_deep_sleep_start();
}

void power_hal_init(void) {
pinMode(BTN_PWR_GPIO, INPUT_PULLUP);
analogReadResolution(12);
sample_battery();
}

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

if (now - last_battery_ms >= BATTERY_POLL_MS) {
last_battery_ms = now;
sample_battery();
}
if (now - last_pwr_ms >= PWR_POLL_MS) {
last_pwr_ms = now;
bool pwr_now = (digitalRead(BTN_PWR_GPIO) == LOW); // active LOW
if (pwr_now && !last_pwr_state) { // press edge — hold begins
pwr_press_started_ms = now;
pwr_long_fired = false;
} else if (pwr_now && last_pwr_state) { // held
if (!pwr_long_fired && (now - pwr_press_started_ms >= PWR_LONG_MS)) {
pwr_long_flag = true;
pwr_long_fired = true;
}
if (now - pwr_press_started_ms >= PWR_OFF_HOLD_MS) {
power_off(); // does not return
}
} else if (!pwr_now && last_pwr_state) { // release edge
pwr_released_flag = true;
if (!pwr_long_fired) pwr_pressed_flag = true; // short press
}
last_pwr_state = pwr_now;
}
}

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

bool power_hal_pwr_pressed(void) {
if (pwr_pressed_flag) { pwr_pressed_flag = false; return true; }
return false;
}

bool power_hal_pwr_long_pressed(void) {
if (pwr_long_flag) { pwr_long_flag = false; return true; }
return false;
}

bool power_hal_pwr_released(void) {
if (pwr_released_flag) { pwr_released_flag = false; return true; }
return false;
}
Loading