Skip to content

feat(lilygo-t5-47): Add BSP for the LilyGo T5 4.7" ESP32-S3 e-paper board - #682

Open
finger563 wants to merge 1 commit into
mainfrom
feat/lilygo-t5-47-bsp
Open

feat(lilygo-t5-47): Add BSP for the LilyGo T5 4.7" ESP32-S3 e-paper board#682
finger563 wants to merge 1 commit into
mainfrom
feat/lilygo-t5-47-bsp

Conversation

@finger563

Copy link
Copy Markdown
Contributor

Description

Adds a Board Support Package for the LilyGo T5 4.7" (ESP32-S3) e-paper board
(espp::LilyGoT547), wrapping the epdiy
library for the ED047TC1 960×540 16-level-grayscale panel, plus two reusable
driver components it depends on.

BSP features (components/lilygo-t5-47/):

  • E-paper display via epdiy (LCD_CAM parallel bus): power on/off, clear, a
    grayscale framebuffer, and partial/full updates.
  • LVGL 9 integration — an L8 grayscale display whose flush batches each refresh
    cycle's dirty areas into a single panel update; selectable update mode
    (MODE_GC16 grayscale vs fast mono MODE_DU) and touch-aware display
    rotation
    .
  • GT911 capacitive touch as an LVGL input device (polled — the board's controller
    config is query-mode), plus the capacitive home button.
  • PCF8563 RTC (via the register-compatible espp::Bm8563 driver).
  • BQ27220 battery fuel gauge.
  • PCA9535 I/O expander (the same chip epdiy drives) incl. the "IO48" button.
  • Frontlight, BOOT button, qwiic bus accessor, microSD (SPI), and power-off via
    BQ25896 ship mode.
  • All on-board I2C peripherals share epdiy's bus (100 kHz), handed to epdiy via
    epd_init_with_config() so there is a single shared I2C master.

New reusable components:

  • components/bq27220/ — TI BQ27220 I2C battery fuel-gauge driver.
  • components/pca9535/ — PCA9535 / PCA9555 16-bit I2C GPIO-expander driver.

Example: an interactive LVGL GUI (Gui class) with a live RTC clock, a
toggle-able stats panel (battery / touch / IO48- and home-button state), and
buttons for frontlight, rotation, stats, full-refresh, and power-off; the update
mode follows the stats panel (GC16 while shown for legibility, DU while hidden
for a snappy clock).

Docs pages and CI example-build entries are included for all three components.

Motivation and Context

There was no espp BSP for the LilyGo T5 4.7" e-paper board. E-paper on the
ESP32-S3 (parallel LCD_CAM timing, waveforms, grayscale, partial updates) is
non-trivial, so this wraps the well-supported epdiy library and exposes the whole
board — display, LVGL, touch, RTC, battery, I/O expander, frontlight, microSD,
and power management — behind a single easy-to-use component, matching the other
espp BSPs. The bq27220 and pca9535 drivers are broadly useful on their own and
are factored out as standalone components.

How has this been tested?

  • Builds: all example projects compile cleanly with idf.py build — the BSP
    example (esp32s3), the bq27220 example (esp32), and the pca9535 example
    (esp32s3). Added to .github/workflows/build.yml.
  • Hardware: verified on a physical LilyGo T5 4.7" ePaper S3 PRO:
    • e-paper bring-up + LVGL grayscale UI, and display rotation (layout re-flows,
      touch stays aligned);
    • GT911 touch (coordinates, orientation, and the capacitive home button);
    • PCF8563 RTC (live clock), BQ27220 battery readings, PCA9535 IO48 button;
    • frontlight toggle and BQ25896 ship-mode power-off (wakes on the PWR button);
    • an on-boot I2C bus scan confirmed every device (0x20 expander, 0x51 RTC,
      0x55 gauge, 0x5D touch, 0x68 TPS, 0x6B PMIC).

Screenshots (if appropriate, e.g. schematic, board, console logs, lab pictures):

Console (I2C bus scan on boot):

Scanning internal I2C bus...
I2C device found at 0x20 (PCA9535 expander)
I2C device found at 0x51 (PCF8563 RTC)
I2C device found at 0x55 (BQ27220 gauge)
I2C device found at 0x5D (GT911 touch)
I2C device found at 0x68 (TPS65185)
I2C device found at 0x6B (BQ25896 PMIC)

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation Update
  • Hardware (schematic, board, system design) change
  • Software change

Checklist:

  • My change requires a change to the documentation.
  • I have added / updated the documentation related to this change via either README or WIKI

Software

  • I have added tests to cover my changes.
  • I have updated the .github/workflows/build.yml file to add my new test to the automated cloud build github action.
  • All new and existing tests passed.
  • My code follows the code style of this project.

Copilot AI review requested due to automatic review settings July 30, 2026 01:06
@github-actions

Copy link
Copy Markdown

✅Static analysis result - no issues found! ✅

…oard

Board support package (espp::LilyGoT547 singleton) for the LilyGo T5 4.7"
(ESP32-S3) e-paper board, wrapping the epdiy library for the ED047TC1 960x540
16-level-grayscale panel.

Features:
- E-paper display via epdiy (LCD_CAM parallel bus): power on/off, clear, a
  grayscale framebuffer, and partial/full updates.
- LVGL 9 integration: an L8 grayscale display whose flush batches each refresh
  cycle's dirty areas into a single panel update; selectable update mode
  (GC16 grayscale vs fast mono DU) and touch-aware display rotation.
- GT911 capacitive touch (polled - the panel's controller config is query-mode)
  as an LVGL input device, plus the capacitive home button.
- PCF8563 RTC (via the register-compatible espp::Bm8563 driver).
- BQ27220 battery fuel gauge.
- PCA9535 I/O expander (the same chip epdiy drives) including the IO48 button.
- Frontlight, BOOT button, qwiic bus accessor, microSD (SPI), and power-off via
  BQ25896 ship mode.
- All on-board I2C peripherals share epdiy's bus (100 kHz), handed to epdiy via
  epd_init_with_config so there is a single shared I2C master.

Adds two reusable espp components used by this BSP:
- bq27220: TI BQ27220 I2C battery fuel-gauge driver.
- pca9535: PCA9535 / PCA9555 16-bit I2C GPIO-expander driver.

The example is an interactive LVGL GUI (Gui class): a live RTC clock, a
toggle-able stats panel (battery / touch / IO48- and home-button state), and
buttons for frontlight, rotation, stats, full-refresh, and power-off. The panel
update mode follows the stats panel (GC16 while shown for legibility, DU while
hidden for a snappy clock).

Docs pages and CI example-build entries added for all three components.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@finger563
finger563 force-pushed the feat/lilygo-t5-47-bsp branch from d8b7b92 to 1763406 Compare July 30, 2026 01:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new ESPP Board Support Package (BSP) for the LilyGo T5 4.7" ESP32-S3 e-paper board, plus two new reusable I2C driver components (BQ27220 fuel gauge and PCA9535/9555 I/O expander). This integrates epdiy for the ED047TC1 panel, provides LVGL 9 support, and documents/build-tests the new components in CI.

Changes:

  • Added espp::LilyGoT547 BSP component (display via epdiy, LVGL flush integration, touch, RTC, battery gauge, IO expander, frontlight, SD card, and ship-mode shutdown).
  • Added new standalone components: bq27220 (fuel gauge) and pca9535 (I/O expander), each with examples and docs.
  • Updated Sphinx/Doxygen docs and GitHub Actions CI build matrix to include the new examples.

Reviewed changes

Copilot reviewed 52 out of 52 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
doc/en/io_expander/pca9535.rst Adds PCA9535/9555 documentation page.
doc/en/io_expander/pca9535_example.md Adds docs include page for the PCA9535 example.
doc/en/io_expander/index.rst Adds PCA9535 to IO expander docs index.
doc/en/dev_boards/lilygo/lilygo_t5_47.rst Adds BSP documentation page for LilyGo T5 4.7".
doc/en/dev_boards/lilygo/lilygo_t5_47_example.md Adds docs include page for the LilyGo T5 4.7" example.
doc/en/dev_boards/lilygo/index.rst Adds the new LilyGo T5 4.7" page to the LilyGo boards index.
doc/en/battery/index.rst Adds BQ27220 to battery docs index.
doc/en/battery/bq27220.rst Adds BQ27220 documentation page.
doc/en/battery/bq27220_example.md Adds docs include page for the BQ27220 example.
doc/Doxyfile Adds new components’ headers and examples to Doxygen inputs/paths.
components/pca9535/README.md PCA9535 component README.
components/pca9535/include/pca9535.hpp Implements the PCA9535/9555 I2C expander driver.
components/pca9535/idf_component.yml Component Manager manifest for pca9535.
components/pca9535/example/sdkconfig.defaults Example default sdkconfig for pca9535.
components/pca9535/example/README.md PCA9535 example README.
components/pca9535/example/main/pca9535_example.cpp PCA9535 example application.
components/pca9535/example/main/Kconfig.projbuild Kconfig options for PCA9535 example I2C pins/hardware.
components/pca9535/example/main/CMakeLists.txt Example main component registration.
components/pca9535/example/CMakeLists.txt Example project CMake (local components).
components/pca9535/CMakeLists.txt pca9535 component build definition.
components/lilygo-t5-47/src/touch.cpp GT911 bring-up and polling task + LVGL input integration.
components/lilygo-t5-47/src/sdcard.cpp SPI bus init + FAT mount for microSD.
components/lilygo-t5-47/src/rtc.cpp RTC bring-up using Bm8563 driver for PCF8563-compatible chip.
components/lilygo-t5-47/src/power.cpp Ship-mode shutdown via PMIC register write.
components/lilygo-t5-47/src/lvgl.cpp LVGL initialization + flush that writes to epdiy framebuffer and batches updates.
components/lilygo-t5-47/src/lilygo-t5-47.cpp Core display initialization and basic display operations.
components/lilygo-t5-47/src/io_expander.cpp PCA9535 bring-up (shared with epdiy) + IO48 button helper.
components/lilygo-t5-47/src/frontlight.cpp Frontlight GPIO control.
components/lilygo-t5-47/src/buttons.cpp BOOT button interrupt wiring (shared ISR service).
components/lilygo-t5-47/src/battery.cpp BQ27220 bring-up on internal I2C bus.
components/lilygo-t5-47/README.md BSP README including features and pin mapping.
components/lilygo-t5-47/include/lilygo-t5-47.hpp BSP public API header and singleton interface.
components/lilygo-t5-47/idf_component.yml Component Manager manifest for lilygo-t5-47 (includes epdiy git dependency).
components/lilygo-t5-47/example/sdkconfig.defaults Example default sdkconfig (PSRAM/cache line/fonts/partition).
components/lilygo-t5-47/example/README.md BSP example README describing the LVGL GUI behavior and hardware expectations.
components/lilygo-t5-47/example/main/lilygo-t5-47_example.cpp BSP example app wiring up display, LVGL GUI, peripherals, and polling loop.
components/lilygo-t5-47/example/main/gui.hpp GUI class interface + LVGL update task structure.
components/lilygo-t5-47/example/main/gui.cpp GUI implementation (layout, buttons, update loop, refresh behavior).
components/lilygo-t5-47/example/main/CMakeLists.txt Example main component registration.
components/lilygo-t5-47/example/CMakeLists.txt Example project CMake (component manager enabled to fetch epdiy).
components/lilygo-t5-47/CMakeLists.txt BSP component build definition and requirements.
components/bq27220/README.md BQ27220 component README.
components/bq27220/include/bq27220.hpp Implements the BQ27220 fuel gauge driver (little-endian standard commands).
components/bq27220/idf_component.yml Component Manager manifest for bq27220.
components/bq27220/example/sdkconfig.defaults Example default sdkconfig for bq27220.
components/bq27220/example/README.md BQ27220 example README.
components/bq27220/example/main/Kconfig.projbuild Kconfig options for BQ27220 example I2C pins/hardware.
components/bq27220/example/main/CMakeLists.txt Example main component registration.
components/bq27220/example/main/bq27220_example.cpp BQ27220 example application.
components/bq27220/example/CMakeLists.txt Example project CMake (local components).
components/bq27220/CMakeLists.txt bq27220 component build definition.
.github/workflows/build.yml Adds CI build matrix entries for the new examples.
Comments suppressed due to low confidence (2)

doc/Doxyfile:146

  • The new pca9535 example entry is out of order in EXAMPLE_PATH (it appears between mcp23x17 and mt6701), despite the file’s note to keep entries alphabetical. Please move it to the correct location in the list to keep maintenance simple.

This issue also appears on line 340 of the same file.
doc/Doxyfile:343

  • The new pca9535 header entry is out of order in Doxyfile INPUT (it is listed before mt6701/odrive_ascii). The header list is intended to be alphabetical; please reorder the local block so pca9535 is placed correctly.

Comment on lines +26 to +51
lv_init();
lv_tick_set_cb(lvgl_tick_ms);

const int w = width();
const int h = height();
if (buffer_lines <= 0 || buffer_lines > h) {
buffer_lines = h;
}
// L8 = 1 byte per pixel. Two partial draw buffers in PSRAM.
const size_t buffer_bytes = static_cast<size_t>(w) * buffer_lines;
lvgl_buffer0_ =
static_cast<uint8_t *>(heap_caps_malloc(buffer_bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
lvgl_buffer1_ =
static_cast<uint8_t *>(heap_caps_malloc(buffer_bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
if (lvgl_buffer0_ == nullptr || lvgl_buffer1_ == nullptr) {
logger_.error("Could not allocate LVGL draw buffers ({} bytes each)", buffer_bytes);
return false;
}

lvgl_display_ = lv_display_create(w, h);
lv_display_set_color_format(lvgl_display_, LV_COLOR_FORMAT_L8);
lv_display_set_buffers(lvgl_display_, lvgl_buffer0_, lvgl_buffer1_, buffer_bytes,
LV_DISPLAY_RENDER_MODE_PARTIAL);
lv_display_set_user_data(lvgl_display_, this);
lv_display_set_flush_cb(lvgl_display_, &LilyGoT547::lvgl_flush_cb);
lv_display_set_default(lvgl_display_);
Comment on lines +49 to +69
auto p0_pins = pca9535.get_pins(espp::Pca9535::Port::PORT0, ec);
if (ec) {
fmt::print("get_pins failed: {}\n", ec.message());
return false;
}
// toggle the output on port 1
output = ~output;
pca9535.set_pins(espp::Pca9535::Port::PORT1, output, ec);
if (ec) {
fmt::print("set_pins failed: {}\n", ec.message());
return false;
}
fmt::print("{:.3f}, {:#x}, {:#x}\n", seconds, p0_pins, output);
// NOTE: sleeping in this way allows the sleep to exit early when the
// task is being stopped / destroyed
{
std::unique_lock<std::mutex> lk(m);
cv.wait_for(lk, 500ms);
}
// don't want to stop the task
return false;
Copilot AI review requested due to automatic review settings July 30, 2026 01:13

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 52 out of 52 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (3)

components/lilygo-t5-47/src/lvgl.cpp:43

  • If either LVGL draw-buffer allocation fails, the function returns without freeing the other buffer, leaking PSRAM and leaving the object in a partially-initialized state for subsequent calls.
  lvgl_buffer0_ =
      static_cast<uint8_t *>(heap_caps_malloc(buffer_bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
  lvgl_buffer1_ =
      static_cast<uint8_t *>(heap_caps_malloc(buffer_bytes, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
  if (lvgl_buffer0_ == nullptr || lvgl_buffer1_ == nullptr) {
    logger_.error("Could not allocate LVGL draw buffers ({} bytes each)", buffer_bytes);
    return false;
  }

doc/en/dev_boards/lilygo/lilygo_t5_47_example.md:1

  • The docs include path is incorrect for this file's location (doc/en/dev_boards/lilygo/...). As written it resolves under doc/en/, so the rendered page will fail to include the example README.
    components/lilygo-t5-47/include/lilygo-t5-47.hpp:207
  • The header comment says the touch callback is invoked from an interrupt, but the implementation polls GT911 from a Task and calls the callback from that polling task. This matters for callback expectations (blocking/logging is OK in a task but not in an ISR).
  /// Initialize the GT911 capacitive touch controller on the internal I2C bus,
  /// and register an LVGL input device for it.
  /// \param callback Called (from the touch interrupt) with each new touch state
  /// \return true on success. initialize_display() must have been called first
  ///         (it creates the shared I2C bus).

Comment on lines +143 to +153
void Gui::set_label_if_changed(lv_obj_t *label, std::string &cache, std::string_view text) {
if (!label) {
return;
}
if (cache == text) {
return; // no change - avoid a needless e-paper refresh
}
cache = std::string(text);
std::lock_guard<std::recursive_mutex> lock(mutex_);
lv_label_set_text(label, cache.c_str());
}
Comment on lines +21 to +32
internal_i2c_ = std::make_unique<espp::I2c>(espp::I2c::Config{
.port = I2C_NUM_0,
.sda_io_num = internal_i2c_sda,
.scl_io_num = internal_i2c_scl,
.sda_pullup_en = GPIO_PULLUP_ENABLE,
.scl_pullup_en = GPIO_PULLUP_ENABLE,
.clk_speed = 100 * 1000, // the board (and epdiy) run this bus at 100 kHz
.log_level = espp::Logger::Verbosity::WARN,
});
EpdI2cConfig epd_i2c_config = {.bus_handle = internal_i2c_->native_bus_handle()};
EpdInitConfig epd_init_config = {.i2c = &epd_i2c_config};

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants