A reference implementation for the Waveshare ESP32-S3-AMOLED-1.91 development board integrated with the Pimoroni Trackball Breakout. This project standardizes the drivers, power management, and UI architecture to serve as a foundation for future derived systems.
- Low Power Mode: Polled
esp_light_sleepimplementation for low idle consumption with instant wake-on-input. - Graphics: Dual-buffered LVGL 9 rendering on the RM67162 AMOLED controller using OPI PSRAM.
- Input Handling: Support for RGBW LED control and unified I2C bus driver with coordinate rotation.
- Build Configuration: Verified partition tables and build scripts to address common Xtensa/ARM assembly conflicts.
- Embedded Drivers: Custom implementations for the QSPI display and I2C trackball to support specific power states and peripheral configurations.
This project compiles into a complete interactive reference application that demonstrates:
- Smooth UI: A responsive LVGL interface running on the AMOLED screen.
- Trackball Navigation: Use the trackball to move focus between UI elements (rotated 90ยฐ to match the screen).
- Smart Power Saving:
- Uses a 4-state machine:
AWAKE->FADE_OUT->LIGHT_SLEEP->FADE_IN. - Idle: Dimming after 10s of inactivity, then entering Light Sleep.
- Sleep: Display & LED off. Polled wake-up every 100ms (Light Sleep).
- Wake: Instant wake-up by rolling or clicking the trackball.
- Uses a 4-state machine:
- Feedback: Click the on-screen color buttons to set the trackball's RGBW LED to the corresponding color.
- VSCode with PlatformIO extension.
- Python 3 (for build scripts).
- Clone the repository
- Verify
platformio.ini: Ensureboard_build.arduino.memory_type = qio_opiis set. - Build & Flash: Connect via USB-C (ensure you hold BOOT if it's the first flash).
| Component | Detail |
|---|---|
| MCU | ESP32-S3R8 (Dual Core 240MHz) |
| PSRAM | 8MB OPI (Octal SPI) |
| Flash | 16MB (External) |
| Display | 1.91" AMOLED (240x536) @ 60Hz |
| IMU | QMI8658 (6-Axis) |
| Input | Pimoroni Trackball Breakout (RGBW LED, Nuvoton MCU) |
| Function | Pin (GPIO) | Notes |
|---|---|---|
| QSPI SCK | 47 | Shared with SD Card CLK |
| QSPI CS | 6 | Display Chip Select |
| I2C SDA | 40 | Trackball & IMU Shared |
| I2C SCL | 39 | Trackball & IMU Shared |
| Display RST | - | Internal/Shared |
| Battery ADC | 1 | Voltage Monitor |
Driven via QSPI at 40MHz. Requires specific initialization for the AMOLED panel:
- Color Inversion:
INVON (0x21)is mandatory for correct black levels. - Orientation:
MADCTL (0x36)set to0x20 | 0x80for landscape. - Buffering: Uses two
MALLOC_CAP_SPIRAMbuffers in PSRAM for smooth partial rendering.
Uses a Polled Light Sleep loop:
- Enter
esp_light_sleep_start()for 100ms. - Wake & Poll Trackball I2C.
- If no activity -> Sleep.
- If activity -> Wake Display ->
lv_obj_invalidate()-> Fade In.
- App Partition: 3MB (via
partitions.csv) - Filesystem: ~9.9MB FATFS/LittleFS
- PSRAM: 8MB OPI (Critical:
qio_opimode)
The project uses custom-built drivers located in src/ to handle specific hardware requirements:
- RM67162 AMOLED Driver (
qspi_display.cpp/h):- Configures the ESP32-S3 QSPI peripheral at 40MHz.
- Uses manual memory-mapped addressing for frame data transfers.
- Implements the hardware-level sleep/wake commands for the display controller.
- Pimoroni Trackball Driver (
trackball.h):- Header-only I2C implementation for the Nuvoton-based breakout.
- Supports RGBW LED control and directional polling.
- LVGL Input Bridge (
input.cpp/h):- Maps the physical trackball to the LVGL
KEYPADinput system. - Implements coordinate rotation and software debouncing.
- Maps the physical trackball to the LVGL
- Display is Negative? -> You missed the
INVON (0x21)command in init. - Build Error (Neon/Helium)? -> Run the
fix_lvgl_9.pyscript to remove ARM assembly. - No Serial Output? -> Set
ARDUINO_USB_CDC_ON_BOOT=1inplatformio.ini. - Serial Stops after Sleep? -> This is normal behavior. It automatically reconnects 50ms after wake.
- Input Lag? -> Check
NAVIGATION_THRESHOLDininput.cpp.
# Name, Type, SubType, Offset, Size, Flags
nvs, data, nvs, 0x9000, 0x5000,
otadata, data, ota, 0xe000, 0x2000,
app0, app, ota_0, 0x10000, 0x300000,
app1, app, ota_1, 0x310000,0x300000,
spiffs, data, spiffs, 0x610000, 0x9E0000,// 1. SlpOut & Wait
writeCommand(0x11); delay(120);
// 2. Color Mode 16-bit
writeC8D8(0x3A, 0x55);
// 3. Orientation
writeC8D8(0x36, 0x20 | 0x80 | 0x00);
// 4. Brightness
writeC8D8(0x51, 0x00);
// 5. Display ON
writeCommand(0x29);
// 6. Color Inversion (CRITICAL)
writeCommand(0x21); delay(20);# Removes ARM assembly from LVGL which breaks ESP32 builds
import os
import shutil
Import("env")
try:
# Get the library dependencies directory
libdeps_dir = env.subst("$PROJECT_LIBDEPS_DIR")
env_name = env.subst("$PIOENV")
lvgl_dir = os.path.join(libdeps_dir, env_name, "lvgl")
if os.path.exists(lvgl_dir):
problematic_dirs = [
os.path.join(lvgl_dir, "src", "draw", "sw", "blend", "helium"),
os.path.join(lvgl_dir, "src", "draw", "sw", "blend", "neon"),
os.path.join(lvgl_dir, "src", "draw", "convert", "helium"),
os.path.join(lvgl_dir, "src", "draw", "convert", "neon"),
]
for d in problematic_dirs:
if os.path.exists(d):
print(f"Removing problematic LVGL 9 directory: {d}")
shutil.rmtree(d)
except Exception as e:
print(f"Error in fix_lvgl_9.py: {e}")