diff --git a/docs/porting/capability-flags.md b/docs/porting/capability-flags.md index 76c2d63..191e900 100644 --- a/docs/porting/capability-flags.md +++ b/docs/porting/capability-flags.md @@ -24,6 +24,10 @@ Keep the two in sync. The pattern in `caps.cpp` does this for you: | `BOARD_HAS_BATTERY` | 0 | Whether PMU battery measurement is meaningful on this board. UI hides the battery indicator when false. | | `BOARD_HAS_IO_EXPANDER` | 0 | Whether an IO expander gates display / touch reset lines. Doesn't directly gate any code path — but signals to the porter that `board_init()` must release the expander before `display_hal_init()`. | +## Build-flag macros + +`BOARD_HAS_PSRAM` is set as a `-D` build flag in `platformio.ini` (not in `board.h`) on chips with external PSRAM wired up. Shared code (`main.cpp`, `splash.cpp`) and per-board display drivers use it to choose between `MALLOC_CAP_SPIRAM` (large buffers) and `MALLOC_CAP_INTERNAL` (small buffers, partial-render LVGL, splash canvas capped at ~80 KB, screenshot capture disabled). New ESP32-C6 / ESP32-C3 ports must leave this undefined. + ## Future capabilities Add a new flag when: diff --git a/firmware/platformio.ini b/firmware/platformio.ini index fc23263..560cbd3 100644 --- a/firmware/platformio.ini +++ b/firmware/platformio.ini @@ -109,3 +109,71 @@ lib_deps = lvgl/lvgl@^9.2.0 bblanchon/ArduinoJson@^7.0.0 h2zero/NimBLE-Arduino@^2.1.1 + + +[env:waveshare_amoled_216_c6] +; C6 sibling of the AMOLED-2.16. Same panel/touch/PMU/IMU, no PSRAM, +; single-core RISC-V, BLE 5.3 only (no classic BT). If the build fails +; resolving the C6 board, bump pioarduino to a newer 55.x release that +; ships esp32-c6-devkitc-1. +platform = https://github.com/pioarduino/platform-espressif32/releases/download/55.03.38-1/platform-espressif32.zip +board = esp32-c6-devkitc-1 +framework = arduino +upload_speed = 921600 +monitor_speed = 115200 +; Waveshare's C6-AMOLED-2.16 ships with 16 MB flash. The default 1.25 MB +; app partition can't hold this firmware (~1.43 MB) — switch to the +; 16 MB layout that gives the app ~6.5 MB, matching the AMOLED-1.8 env. +board_upload.flash_size = 16MB +board_upload.maximum_size = 16777216 +board_build.partitions = default_16MB.csv + +build_src_filter = + +<*> + - + + + +build_flags = + -DBOARD_AMOLED_216_C6 + ; C6 has only USB-Serial-JTAG (HWCDC), no native USB-OTG. The Arduino + ; core maps Serial → HWCDCSerial only when BOTH flags below are set; + ; CDC_ON_BOOT alone selects USBSerial (TinyUSB) which doesn't exist on + ; C6 and breaks the build. Without either flag, Serial routes to UART0 + ; on physical pins and nothing reaches the USB host. + -DARDUINO_USB_CDC_ON_BOOT=1 + -DARDUINO_USB_MODE=1 + ; No -DBOARD_HAS_PSRAM — C6 has no external PSRAM. Shared code + ; (main.cpp, splash.cpp) gates on this to pick MALLOC_CAP_INTERNAL + ; and shrink LVGL buffers + splash canvas accordingly. + -DXPOWERS_CHIP_AXP2101 + -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 + -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 + ; Screenshot capture needs a full-frame RGB565 buffer (~460 KB on + ; 480×480) which doesn't fit in C6 internal SRAM. send_screenshot + ; prints SCREENSHOT_UNSUPPORTED on this board. + -DLV_USE_SNAPSHOT=0 + +lib_deps = + ; 1.6.4+ required for Arduino_SH8601 (this board uses SH8601, not CO5300) + moononournation/GFX Library for Arduino@^1.6.4 + lewisxhe/SensorLib@^0.2.6 + lewisxhe/XPowersLib@^0.2.7 + lvgl/lvgl@^9.2.0 + bblanchon/ArduinoJson@^7.0.0 + h2zero/NimBLE-Arduino@^2.1.1 diff --git a/firmware/src/boards/waveshare_amoled_216_c6/board.h b/firmware/src/boards/waveshare_amoled_216_c6/board.h new file mode 100644 index 0000000..83fec56 --- /dev/null +++ b/firmware/src/boards/waveshare_amoled_216_c6/board.h @@ -0,0 +1,54 @@ +#pragma once + +// Waveshare ESP32-C6-Touch-AMOLED-2.16. +// +// Despite the matching "2.16" model number, this board is hardware- +// distinct from the S3 AMOLED-2.16: it uses an SH8601 panel (same driver +// family as the AMOLED-1.8), a CST9217 touch controller, and the C6 SoC's +// own GPIO map. There is no PSRAM. AXP2101 PMU and QMI8658 IMU carry over. +// +// Pin assignments verified against the official Waveshare XiaoZhi BSP at +// waveshareteam/ESP32-C6-Touch-AMOLED-2.16 (XiaoZhi config.h) and the +// ESP-IDF example user_config.h files in the same repo. + +#define BOARD_NAME "Waveshare AMOLED 2.16 (C6)" + +#define LCD_WIDTH 480 +#define LCD_HEIGHT 480 + +// ---- QSPI display pins (SH8601) ---- +#define LCD_CS 15 +#define LCD_SCLK 0 +#define LCD_SDIO0 1 +#define LCD_SDIO1 2 +#define LCD_SDIO2 3 +#define LCD_SDIO3 4 +// LCD reset is not wired to a MCU GPIO on this board — the SH8601 relies +// on its internal power-on reset. The Arduino_GFX driver gets +// GFX_NOT_DEFINED for reset. + +// ---- I2C bus (touch + PMU + IMU all share one bus) ---- +#define IIC_SDA 8 +#define IIC_SCL 7 + +// ---- Touch (CST9217 via TouchDrvCST92xx library) ---- +// CST9217 is register-compatible with CST9220 in the relevant subset; the +// SensorLib CST92xx driver works against both. I2C address is the same. +#define TP_INT 5 +#define TP_RST 11 +#define CST9220_ADDR 0x5A + +// ---- PMU ---- +#define AXP2101_ADDR 0x34 + +// ---- Buttons ---- +// Only one user GPIO button (BOOT). The "PWR" side button is the AXP2101 +// PKEY input — already serviced by power.cpp via the PKEY_SHORT_IRQ path. +#define BTN_BACK_GPIO 9 // BOOT — primary, Space (PTT) + +// ---- Capability flags ---- +#define BOARD_HAS_SECONDARY_BUTTON 0 +#define BOARD_HAS_ROTATION 0 // C6 has no PSRAM headroom for the rotation strip +#define BOARD_HAS_IMU 1 // present + initialized for I2C bus health +#define BOARD_HAS_BATTERY 1 +#define BOARD_HAS_IO_EXPANDER 0 // TCA9554 exists on board but only services audio diff --git a/firmware/src/boards/waveshare_amoled_216_c6/board_init.cpp b/firmware/src/boards/waveshare_amoled_216_c6/board_init.cpp new file mode 100644 index 0000000..c72a5d0 --- /dev/null +++ b/firmware/src/boards/waveshare_amoled_216_c6/board_init.cpp @@ -0,0 +1,51 @@ +#include "board.h" +#include +#include +#include + +// On this C6 board the SH8601 LCD + CST9217 touch are powered from the +// AXP2101's ALDO rails, not directly from 3V3. The display init must run +// AFTER the rails are up, so we bring the PMU up here in board_init() +// (before display_hal_init) instead of in power_hal_init() which runs +// later. power.cpp re-uses the same XPowersPMU handle for battery polling. +// +// Rail config mirrors the Waveshare XiaoZhi BSP for this board: +// DC1 = 3.3V (system rail) +// ALDO1..4 = 3.3V, all enabled (LCD, touch, sensors) + +XPowersPMU board_pmu; // shared instance — power.cpp uses extern reference + +extern "C" void board_init(void) { + Wire.begin(IIC_SDA, IIC_SCL); + + if (!board_pmu.begin(Wire, AXP2101_ADDR, IIC_SDA, IIC_SCL)) { + Serial.println("AXP2101 init failed (board_init)"); + return; + } + + // Set all four ALDOs to 3.3V. + board_pmu.setALDO1Voltage(3300); + board_pmu.setALDO2Voltage(3300); + board_pmu.setALDO3Voltage(3300); + board_pmu.setALDO4Voltage(3300); + + // ALDO1, 2, 4 just need to be enabled. + board_pmu.enableALDO1(); + board_pmu.enableALDO2(); + board_pmu.enableALDO4(); + + // ALDO3 doubles as the LCD reset line on this board (the SH8601's RST + // pin isn't wired to a MCU GPIO). The Waveshare BSP pulses it + // HIGH → LOW → HIGH with 100 ms holds to issue a proper reset before + // the panel sees its first SPI command. Without this pulse the SH8601 + // stays in an indeterminate state and the screen is black even though + // QSPI init and brightness writes succeed. + board_pmu.enableALDO3(); + delay(100); + board_pmu.disableALDO3(); + delay(100); + board_pmu.enableALDO3(); + delay(100); + + Serial.println("AXP2101 rails up (ALDO1-4 @ 3.3V, LCD reset pulsed)"); +} diff --git a/firmware/src/boards/waveshare_amoled_216_c6/caps.cpp b/firmware/src/boards/waveshare_amoled_216_c6/caps.cpp new file mode 100644 index 0000000..5295955 --- /dev/null +++ b/firmware/src/boards/waveshare_amoled_216_c6/caps.cpp @@ -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 = 1, // BOOT only; PWR is via AXP PKEY IRQ + .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; } diff --git a/firmware/src/boards/waveshare_amoled_216_c6/display.cpp b/firmware/src/boards/waveshare_amoled_216_c6/display.cpp new file mode 100644 index 0000000..dfdc9ee --- /dev/null +++ b/firmware/src/boards/waveshare_amoled_216_c6/display.cpp @@ -0,0 +1,77 @@ +#include "../../hal/display_hal.h" +#include "board.h" +#include +#include + +// C6 AMOLED-2.16 uses an SH8601 panel — same driver family as the +// AMOLED-1.8 port. LCD reset is not wired to any MCU GPIO; the SH8601 +// boots from its internal POR. Rotation is disabled (no PSRAM headroom +// for the strip buffer). + +static Arduino_DataBus* bus = nullptr; +static Arduino_SH8601* gfx = nullptr; + +void display_hal_init(void) { + bus = new Arduino_ESP32QSPI( + LCD_CS, LCD_SCLK, LCD_SDIO0, LCD_SDIO1, LCD_SDIO2, LCD_SDIO3); + gfx = new Arduino_SH8601( + bus, GFX_NOT_DEFINED, 0, LCD_WIDTH, LCD_HEIGHT); +} + +// Vendor-specific init commands from the Waveshare C6-2.16 BSP +// (02_Example/Arduino-v3.3.3/09_LVGL_V9_Test/bsp_lvgl_port.cpp in the +// waveshareteam/ESP32-C6-Touch-AMOLED-2.16 repo). The stock Arduino_GFX +// SH8601 init does SLPOUT + NORON + INVOFF + PIXFMT + DISPON + brightness, +// which is enough for the AMOLED-1.8 panel but leaves this 2.16 panel +// dark. The page-switch sequence (0xFE 0x20 ... 0xFE 0x00) writes two +// panel-specific manufacturer registers (0x19 and 0x1C) that gate the +// driving voltages — without them the panel stays black even with the +// rails up and the reset pulse applied. +static void send_vendor_init(Arduino_DataBus* b) { + b->beginWrite(); + b->writeC8D8(0xFE, 0x20); // enter manufacturer command page 0x20 + b->writeC8D8(0x19, 0x10); // panel driving + b->writeC8D8(0x1C, 0xA0); // panel driving + b->writeC8D8(0xFE, 0x00); // back to user command page + b->writeC8D8(0xC4, 0x80); // SPI mode control + b->writeC8D8(0x36, 0x30); // MADCTL (BSP value) + b->writeC8D8(0x53, 0x20); // CTRL display 1 (brightness control on) + b->writeC8D8(0x51, 0xFF); // brightness = max + b->writeC8D8(0x63, 0xFF); // HBM brightness = max + b->writeCommand(0x29); // DISPON (idempotent — stock init already did this) + b->endWrite(); + delay(20); +} + +void display_hal_begin(void) { + gfx->begin(); + send_vendor_init(bus); // patch up panel-specific regs the stock init misses + 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) gfx->draw16bitRGBBitmap(x, y, (uint16_t*)pixels, w, h); +} + +void display_hal_tick(void) { + // No rotation cycle on this board. +} + +// Mirrors the CO5300/SH8601 even-alignment pattern from the other ports. +// Harmless on SH8601, kept for consistency. +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; +} diff --git a/firmware/src/boards/waveshare_amoled_216_c6/imu.cpp b/firmware/src/boards/waveshare_amoled_216_c6/imu.cpp new file mode 100644 index 0000000..2b24d57 --- /dev/null +++ b/firmware/src/boards/waveshare_amoled_216_c6/imu.cpp @@ -0,0 +1,26 @@ +#include "../../hal/imu_hal.h" +#include "board.h" +#include +#include +#include + +// QMI8658 is populated on the 2.16 carrier PCB but rotation is disabled +// on the C6 build (BOARD_HAS_ROTATION=0). We still initialize the device +// so the shared I2C bus stays healthy and so a future build can flip +// rotation on without changing this file. Always reports quadrant 0. + +static SensorQMI8658 imu; + +void imu_hal_init(void) { + if (!imu.begin(Wire, QMI8658_L_SLAVE_ADDRESS, IIC_SDA, IIC_SCL)) { + Serial.println("QMI8658 init failed"); + return; + } + Serial.println("QMI8658 init OK (rotation disabled on this board)"); +} + +void imu_hal_tick(void) { + // No-op — rotation is disabled. +} + +uint8_t imu_hal_rotation_quadrant(void) { return 0; } diff --git a/firmware/src/boards/waveshare_amoled_216_c6/input.cpp b/firmware/src/boards/waveshare_amoled_216_c6/input.cpp new file mode 100644 index 0000000..b9f7fd4 --- /dev/null +++ b/firmware/src/boards/waveshare_amoled_216_c6/input.cpp @@ -0,0 +1,17 @@ +#include "../../hal/input_hal.h" +#include "board.h" +#include + +// Only BOOT is wired to a MCU GPIO. The "PWR" side button is the AXP2101 +// PKEY, handled in power.cpp. + +void input_hal_init(void) { + pinMode(BTN_BACK_GPIO, INPUT_PULLUP); +} + +bool input_hal_is_held(InputButton btn) { + if (btn == INPUT_BTN_PRIMARY) { + return digitalRead(BTN_BACK_GPIO) == LOW; + } + return false; +} diff --git a/firmware/src/boards/waveshare_amoled_216_c6/power.cpp b/firmware/src/boards/waveshare_amoled_216_c6/power.cpp new file mode 100644 index 0000000..a7aa277 --- /dev/null +++ b/firmware/src/boards/waveshare_amoled_216_c6/power.cpp @@ -0,0 +1,82 @@ +#include "../../hal/power_hal.h" +#include "board.h" +#include +#include +#include + +// AXP2101 PMU — identical chip and protocol to the S3 variant. + +#define BATTERY_POLL_MS 2000 +#define CHARGING_POLL_MS 500 +#define PWR_POLL_MS 50 + +// The PMU instance is owned by board_init.cpp on this board — it has to +// come up before display_hal_init() to enable the LCD power rails. We +// reuse the same handle here for battery polling and PKEY IRQ wiring. +extern XPowersPMU board_pmu; +#define pmu board_pmu + +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; + +void power_hal_init(void) { + // pmu.begin() already ran in board_init(); just configure battery + + // IRQ wiring here. + pmu.enableBattDetection(); + pmu.enableBattVoltageMeasure(); + + // Mirror the Waveshare XiaoZhi BSP charging config so the on-chip + // fuel gauge has the right reference numbers. Without these, + // getBatteryPercent() returns -1 / shows "---" on the UI. + pmu.setChargeTargetVoltage(XPOWERS_AXP2101_CHG_VOL_4V1); + pmu.setChargerConstantCurr(XPOWERS_AXP2101_CHG_CUR_400MA); + pmu.setChargerTerminationCurr(XPOWERS_AXP2101_CHG_ITERM_25MA); + pmu.setPrechargeCurr(XPOWERS_AXP2101_PRECHARGE_50MA); + + pmu.disableIRQ(XPOWERS_AXP2101_ALL_IRQ); + pmu.clearIrqStatus(); + pmu.enableIRQ(XPOWERS_AXP2101_PKEY_SHORT_IRQ); + + cached_charging = pmu.isCharging(); + cached_vbus = pmu.isVbusIn(); + cached_pct = pmu.getBatteryPercent(); +} + +void power_hal_tick(void) { + uint32_t now = millis(); + + 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 = pmu.getBatteryPercent(); + } + if (now - last_pwr_ms >= PWR_POLL_MS) { + last_pwr_ms = now; + pmu.getIrqStatus(); + if (pmu.isPekeyShortPressIrq()) { + pwr_pressed_flag = true; + } + pmu.clearIrqStatus(); + } +} + +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; +} diff --git a/firmware/src/boards/waveshare_amoled_216_c6/touch.cpp b/firmware/src/boards/waveshare_amoled_216_c6/touch.cpp new file mode 100644 index 0000000..8fdeef2 --- /dev/null +++ b/firmware/src/boards/waveshare_amoled_216_c6/touch.cpp @@ -0,0 +1,51 @@ +#include "../../hal/touch_hal.h" +#include "board.h" +#include +#include +#include + +static TouchDrvCST92xx 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, CST9220_ADDR, IIC_SDA, IIC_SCL)) { + Serial.println("Touch init failed"); + return; + } + touch.setMaxCoordinates(LCD_WIDTH, LCD_HEIGHT); + // C6 2.16 panel mapping (verified empirically): the CST9217's raw + // axes are swapped relative to the SH8601 raster AND X is mirrored. + // Matches the Waveshare BSP, which reads y = raw_byte1, x = W - raw_byte2. + touch.setSwapXY(true); + touch.setMirrorXY(true, false); + pinMode(TP_INT, INPUT_PULLUP); + attachInterrupt(TP_INT, touch_isr, FALLING); + Serial.println("Touch 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; +} diff --git a/firmware/src/main.cpp b/firmware/src/main.cpp index 3d0fae5..08a5cb2 100644 --- a/firmware/src/main.cpp +++ b/firmware/src/main.cpp @@ -21,8 +21,17 @@ static UsageData usage = {}; -// ---- LVGL draw buffers (PSRAM, partial render mode) ---- +// ---- LVGL draw buffers (partial render mode) ---- +// PSRAM-equipped boards (S3) can comfortably hold larger strips. PSRAM-free +// boards (e.g. ESP32-C6) allocate from internal SRAM, so we shrink the strip +// — 480×20 RGB565 = 19 KB × 2 buffers = 38 KB, fits beside everything else. +#ifdef BOARD_HAS_PSRAM #define BUF_LINES 40 +#define LV_BUF_CAPS (MALLOC_CAP_SPIRAM) +#else +#define BUF_LINES 20 +#define LV_BUF_CAPS (MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT) +#endif static uint16_t* buf1 = nullptr; static uint16_t* buf2 = nullptr; @@ -111,6 +120,12 @@ static char cmd_buf[CMD_BUF_SIZE]; static int cmd_pos = 0; static void send_screenshot() { +#ifndef BOARD_HAS_PSRAM + // A full RGB565 framebuffer doesn't fit in internal SRAM on PSRAM-free + // boards (e.g. 480×480×2 = 460 KB). Capture is unsupported there. + Serial.println("SCREENSHOT_UNSUPPORTED"); + return; +#else const uint32_t w = board_caps().width; const uint32_t h = board_caps().height; const uint32_t row_bytes = w * 2; @@ -139,6 +154,7 @@ static void send_screenshot() { Serial.println(); Serial.println("SCREENSHOT_END"); heap_caps_free(sbuf); +#endif } static void check_serial_cmd() { @@ -182,8 +198,8 @@ void setup() { lv_init(); lv_tick_set_cb(my_tick); - buf1 = (uint16_t*)heap_caps_malloc(W * BUF_LINES * 2, MALLOC_CAP_SPIRAM); - buf2 = (uint16_t*)heap_caps_malloc(W * BUF_LINES * 2, MALLOC_CAP_SPIRAM); + buf1 = (uint16_t*)heap_caps_malloc(W * BUF_LINES * 2, LV_BUF_CAPS); + buf2 = (uint16_t*)heap_caps_malloc(W * BUF_LINES * 2, LV_BUF_CAPS); lv_display_t* disp = lv_display_create(W, H); lv_display_set_color_format(disp, LV_COLOR_FORMAT_RGB565); diff --git a/firmware/src/splash.cpp b/firmware/src/splash.cpp index ad42463..c6e4fe6 100644 --- a/firmware/src/splash.cpp +++ b/firmware/src/splash.cpp @@ -103,11 +103,25 @@ void splash_init(lv_obj_t *parent) { int min_dim = (c.width < c.height) ? c.width : c.height; cell = min_dim / GRID; // fits within the smaller display dimension if (cell < 4) cell = 4; + +#ifdef BOARD_HAS_PSRAM + const uint32_t canvas_caps = MALLOC_CAP_SPIRAM; +#else + // Without PSRAM the full 480×480 RGB565 canvas (460 KB) won't fit. Cap + // the canvas so the buffer stays under ~80 KB, leaving the rest of + // internal SRAM free for LVGL, NimBLE, and the audio/PMU stacks. The + // canvas is centered, so the cost is extra black border around the + // pixel art — not cropping. + const uint32_t canvas_caps = MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT; + const int MAX_CELL_NO_PSRAM = 10; // 10*20=200; 200*200*2=78 KB + if (cell > MAX_CELL_NO_PSRAM) cell = MAX_CELL_NO_PSRAM; +#endif + canvas_w = GRID * cell; canvas_h = GRID * cell; - canvas_buf = (uint16_t*)heap_caps_malloc(canvas_w * canvas_h * 2, MALLOC_CAP_SPIRAM); - row_buf = (uint16_t*)heap_caps_malloc(canvas_w * 2, MALLOC_CAP_SPIRAM); + canvas_buf = (uint16_t*)heap_caps_malloc(canvas_w * canvas_h * 2, canvas_caps); + row_buf = (uint16_t*)heap_caps_malloc(canvas_w * 2, canvas_caps); if (!canvas_buf || !row_buf) { Serial.println("splash: failed to alloc canvas buffer"); return;