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
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ jobs:
{ id: m5stack-cores3, arch: esp32s3 },
{ id: m5stack-stickc-plus, arch: esp32 },
{ id: m5stack-stickc-plus2, arch: esp32 },
{ id: m5stack-tab5, arch: esp32p4 },
{ id: unphone, arch: esp32s3 },
{ id: waveshare-esp32-s3-geek, arch: esp32s3 },
{ id: waveshare-s3-lcd-13, arch: esp32s3 },
Expand Down
2 changes: 1 addition & 1 deletion Devices/guition-jc1060p470ciwy/Source/devices/Display.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -61,5 +61,5 @@ std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay() {
});

const auto display = std::make_shared<Jd9165Display>(configuration);
return std::reinterpret_pointer_cast<tt::hal::display::DisplayDevice>(display);
return std::static_pointer_cast<tt::hal::display::DisplayDevice>(display);
}
2 changes: 1 addition & 1 deletion Devices/guition-jc1060p470ciwy/device.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[general]
vendor=Guition
name=JC1060P470CIWY
name=JC1060P470C-I-W-Y

[hardware]
target=ESP32P4
Expand Down
7 changes: 7 additions & 0 deletions Devices/m5stack-tab5/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
file(GLOB_RECURSE SOURCE_FILES Source/*.c*)

idf_component_register(
SRCS ${SOURCE_FILES}
INCLUDE_DIRS "Source"
REQUIRES Tactility esp_lvgl_port esp_lcd EspLcdCompat esp_lcd_ili9881c GT911 PwmBacklight driver vfs fatfs
)
126 changes: 126 additions & 0 deletions Devices/m5stack-tab5/Source/Configuration.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
#include "devices/Display.h"
#include "devices/SdCard.h"

#include <Tactility/hal/Configuration.h>

using namespace tt::hal;

static const auto LOGGER = tt::Logger("Tab5");

static DeviceVector createDevices() {
return {
createDisplay(),
createSdCard(),
};
}

static bool initBoot() {
/*
PI4IOE5V6408-1 (0x43)
- Bit 0: RF internal/external switch
- Bit 1: Speaker enable
- Bit 2: External 5V bus enable
- Bit 3: /
- Bit 4: LCD reset
- Bit 5: Touch reset
- Bit 6: Camera reset
- Bit 7: Headphone detect

PI4IOE5V6408-2 (0x44)
- Bit 0: C6 WLAN enable
- Bit 1: /
- Bit 2: /
- Bit 3: USB-A 5V enable
- Bit 4: Device power: PWROFF_PLUSE
- Bit 5: IP2326: nCHG_QC_EN
- Bit 6: IP2326: CHG_STAT_LED
- Bit 7: IP2326: CHG_EN
*/

// Init byte arrays adapted from https://github.com/m5stack/M5GFX/blob/03565ccc96cb0b73c8b157f5ec3fbde439b034ad/src/M5GFX.cpp
static constexpr uint8_t reg_data_io1_1[] = {
0x03, 0b01111111, // PI4IO_REG_IO_DIR
0x05, 0b01000110, // PI4IO_REG_OUT_SET (bit4=LCD Reset, bit5=GT911 TouchReset -> LOW)
0x07, 0b00000000, // PI4IO_REG_OUT_H_IM
0x0D, 0b01111111, // PI4IO_REG_PULL_SEL
0x0B, 0b01111111, // PI4IO_REG_PULL_EN
};

static constexpr uint8_t reg_data_io1_2[] = {
0x05, 0b01110110, // PI4IO_REG_OUT_SET (bit4=LCD Reset, bit5=GT911 TouchReset -> HIGH)
};

static constexpr uint8_t reg_data_io2[] = {
0x03, 0b10111001, // PI4IO_REG_IO_DIR
0x07, 0b00000110, // PI4IO_REG_OUT_H_IM
0x0D, 0b10111001, // PI4IO_REG_PULL_SEL
0x0B, 0b11111001, // PI4IO_REG_PULL_EN
0x09, 0b01000000, // PI4IO_REG_IN_DEF_STA
0x11, 0b10111111, // PI4IO_REG_INT_MASK
0x05, 0b10001001, // PI4IO_REG_OUT_SET (enable WiFi, USB-A 5V and CHG_EN)
};

constexpr auto IO_EXPANDER1_ADDRESS = 0x43;
if (!i2c::masterWriteRegisterArray(I2C_NUM_0, IO_EXPANDER1_ADDRESS, reg_data_io1_1, sizeof(reg_data_io1_1))) {
LOGGER.error("IO expander 1 init failed in phase 1");
return false;
}

constexpr auto IO_EXPANDER2_ADDRESS = 0x44;
if (!i2c::masterWriteRegisterArray(I2C_NUM_0, IO_EXPANDER2_ADDRESS, reg_data_io2, sizeof(reg_data_io2))) {
LOGGER.error("IO expander 2 init failed");
return false;
}

// The M5Stack code applies this, but it's not known why
// TODO: Remove and test it extensively
tt::kernel::delayTicks(10);

if (!i2c::masterWriteRegisterArray(I2C_NUM_0, IO_EXPANDER1_ADDRESS, reg_data_io1_2, sizeof(reg_data_io1_2))) {
LOGGER.error("IO expander 1 init failed in phase 2");
return false;
}

return true;
}

extern const Configuration hardwareConfiguration = {
.initBoot = initBoot,
.createDevices = createDevices,
.i2c = {
i2c::Configuration {
.name = "Internal",
.port = I2C_NUM_0,
.initMode = i2c::InitMode::ByTactility,
.isMutable = false,
.config = (i2c_config_t) {
.mode = I2C_MODE_MASTER,
.sda_io_num = GPIO_NUM_31,
.scl_io_num = GPIO_NUM_32,
.sda_pullup_en = true,
.scl_pullup_en = true,
.master = {
.clk_speed = 400000
},
.clk_flags = 0
}
},
i2c::Configuration {
.name = "Port A",
.port = I2C_NUM_1,
.initMode = i2c::InitMode::ByTactility,
.isMutable = false,
.config = (i2c_config_t) {
.mode = I2C_MODE_MASTER,
.sda_io_num = GPIO_NUM_53,
.scl_io_num = GPIO_NUM_54,
.sda_pullup_en = true,
.scl_pullup_en = true,
.master = {
.clk_speed = 400000
},
.clk_flags = 0
}
}
}
};
64 changes: 64 additions & 0 deletions Devices/m5stack-tab5/Source/devices/Display.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
#include "Display.h"
#include "Ili9881cDisplay.h"

#include <Gt911Touch.h>
#include <PwmBacklight.h>
#include <Tactility/Logger.h>
#include <Tactility/Mutex.h>
#include <Tactility/hal/gpio/Gpio.h>

constexpr auto LCD_PIN_RESET = GPIO_NUM_0; // Match P4 EV board reset line
constexpr auto LCD_PIN_BACKLIGHT = GPIO_NUM_22;

static std::shared_ptr<tt::hal::touch::TouchDevice> createTouch() {
auto configuration = std::make_unique<Gt911Touch::Configuration>(
I2C_NUM_0,
720,
1280,
false, // swapXY
false, // mirrorX
false, // mirrorY
GPIO_NUM_NC, // reset pin
GPIO_NUM_NC // "GPIO_NUM_23 cannot be used due to resistor to 3V3" https://github.com/espressif/esp-bsp/blob/ad668c765cbad177495a122181df0a70ff9f8f61/bsp/m5stack_tab5/src/m5stack_tab5.c#L76234
);

return std::make_shared<Gt911Touch>(std::move(configuration));
}

std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay() {
// Initialize PWM backlight
if (!driver::pwmbacklight::init(LCD_PIN_BACKLIGHT, 5000, LEDC_TIMER_1, LEDC_CHANNEL_0)) {
tt::Logger("Tab5").warn("Failed to initialize backlight");
}

auto touch = createTouch();

// Work-around to init touch : interrupt pin must be set to low
// Note: There is a resistor to 3V3 on interrupt pin which is blocking GT911 touch
// See https://github.com/espressif/esp-bsp/blob/ad668c765cbad177495a122181df0a70ff9f8f61/bsp/m5stack_tab5/src/m5stack_tab5.c#L777
tt::hal::gpio::configure(23, tt::hal::gpio::Mode::Output, true, false);
tt::hal::gpio::setLevel(23, false);

auto configuration = std::make_shared<EspLcdConfiguration>(EspLcdConfiguration {
.horizontalResolution = 720,
.verticalResolution = 1280,
.gapX = 0,
.gapY = 0,
.monochrome = false,
.swapXY = false,
.mirrorX = false,
.mirrorY = false,
.invertColor = false,
.bufferSize = 0, // 0 = default (1/10 of screen)
.touch = touch,
.backlightDutyFunction = driver::pwmbacklight::setBacklightDuty,
.resetPin = LCD_PIN_RESET,
.lvglColorFormat = LV_COLOR_FORMAT_RGB565,
.lvglSwapBytes = false,
.rgbElementOrder = LCD_RGB_ELEMENT_ORDER_RGB,
.bitsPerPixel = 16
});

const auto display = std::make_shared<Ili9881cDisplay>(configuration);
return std::static_pointer_cast<tt::hal::display::DisplayDevice>(display);
}
6 changes: 6 additions & 0 deletions Devices/m5stack-tab5/Source/devices/Display.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#pragma once

#include <Tactility/hal/display/DisplayDevice.h>
#include <memory>

std::shared_ptr<tt::hal::display::DisplayDevice> createDisplay();
149 changes: 149 additions & 0 deletions Devices/m5stack-tab5/Source/devices/Ili9881cDisplay.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
#include "Ili9881cDisplay.h"
#include "disp_init_data.h"

#include <Tactility/Logger.h>
#include <esp_lcd_ili9881c.h>

static const auto LOGGER = tt::Logger("ILI9881C");

Ili9881cDisplay::~Ili9881cDisplay() {
// TODO: This should happen during ::stop(), but this isn't currently exposed
if (mipiDsiBus != nullptr) {
esp_lcd_del_dsi_bus(mipiDsiBus);
mipiDsiBus = nullptr;
}
if (ldoChannel != nullptr) {
esp_ldo_release_channel(ldoChannel);
ldoChannel = nullptr;
}
}

bool Ili9881cDisplay::createMipiDsiBus() {
esp_ldo_channel_config_t ldo_mipi_phy_config = {
.chan_id = 3,
.voltage_mv = 2500,
.flags = {
.adjustable = 0,
.owned_by_hw = 0,
.bypass = 0
}
};

if (esp_ldo_acquire_channel(&ldo_mipi_phy_config, &ldoChannel) != ESP_OK) {
LOGGER.error("Failed to acquire LDO channel for MIPI DSI PHY");
return false;
}

LOGGER.info("Powered on");

// Create bus
// TODO: use MIPI_DSI_PHY_CLK_SRC_DEFAULT() in future ESP-IDF 6.0.0 update with esp_lcd_jd9165 library version 2.x
const esp_lcd_dsi_bus_config_t bus_config = {
.bus_id = 0,
.num_data_lanes = 2,
.phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT,
.lane_bit_rate_mbps = 1000
};

if (esp_lcd_new_dsi_bus(&bus_config, &mipiDsiBus) != ESP_OK) {
LOGGER.error("Failed to create bus");
return false;
}

LOGGER.info("Bus created");
return true;
}

bool Ili9881cDisplay::createIoHandle(esp_lcd_panel_io_handle_t& ioHandle) {
// Initialize MIPI DSI bus if not already done
if (mipiDsiBus == nullptr) {
if (!createMipiDsiBus()) {
return false;
}
}

// Use DBI interface to send LCD commands and parameters
esp_lcd_dbi_io_config_t dbi_config = ILI9881C_PANEL_IO_DBI_CONFIG();

if (esp_lcd_new_panel_io_dbi(mipiDsiBus, &dbi_config, &ioHandle) != ESP_OK) {
LOGGER.error("Failed to create panel IO");
return false;
}

return true;
}

esp_lcd_panel_dev_config_t Ili9881cDisplay::createPanelConfig(std::shared_ptr<EspLcdConfiguration> espLcdConfiguration, gpio_num_t resetPin) {
return {
.reset_gpio_num = resetPin,
.rgb_ele_order = espLcdConfiguration->rgbElementOrder,
.data_endian = LCD_RGB_DATA_ENDIAN_LITTLE,
.bits_per_pixel = static_cast<uint8_t>(espLcdConfiguration->bitsPerPixel),
.flags = {
.reset_active_high = 0
},
.vendor_config = nullptr // Will be set in createPanelHandle
};
}

bool Ili9881cDisplay::createPanelHandle(esp_lcd_panel_io_handle_t ioHandle, const esp_lcd_panel_dev_config_t& panelConfig, esp_lcd_panel_handle_t& panelHandle) {
// Based on BSP: https://github.com/espressif/esp-bsp/blob/master/bsp/m5stack_tab5/README.md
// TODO: undo static
static esp_lcd_dpi_panel_config_t dpi_config = {
.virtual_channel = 0,
.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT,
.dpi_clock_freq_mhz = 60,
.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565,
.in_color_format = LCD_COLOR_FMT_RGB565,
.out_color_format = LCD_COLOR_FMT_RGB565,
.num_fbs = 1, // TODO: 2?
.video_timing =
{
.h_size = 720,
.v_size = 1280,
.hsync_pulse_width = 40,
.hsync_back_porch = 140,
.hsync_front_porch = 40,
.vsync_pulse_width = 4,
.vsync_back_porch = 20,
.vsync_front_porch = 20,
},
.flags {
.use_dma2d = 1, // TODO: true?
.disable_lp = 0,
}
};

// TODO: undo static
static ili9881c_vendor_config_t vendor_config = {
.init_cmds = disp_init_data,
.init_cmds_size = std::size(disp_init_data),
.mipi_config = {
.dsi_bus = mipiDsiBus,
.dpi_config = &dpi_config,
.lane_num = 2,
},
};
Comment on lines +117 to +126
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Static vendor_config captures instance member mipiDsiBus - potential correctness issue.

The static vendor_config at line 118 references mipiDsiBus (line 122), which is a per-instance member. This creates a subtle bug: if Ili9881cDisplay is ever destroyed and recreated (or if multiple instances exist), the static vendor_config would reference stale or incorrect bus handles.

While the TODO suggests this is known, the current implementation is unsafe for object lifecycle scenarios. Consider either:

  1. Making vendor_config non-static (preferred)
  2. Adding a static assertion or runtime guard to ensure single-instance usage
🐛 Proposed fix - remove static
-    // TODO: undo static
-    static esp_lcd_dpi_panel_config_t dpi_config = {
+    esp_lcd_dpi_panel_config_t dpi_config = {
         .virtual_channel = 0,
         .dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT,
         .dpi_clock_freq_mhz = 60,
         ...
     };

-    // TODO: undo static
-    static ili9881c_vendor_config_t vendor_config = {
+    ili9881c_vendor_config_t vendor_config = {
         .init_cmds = disp_init_data,
         .init_cmds_size = std::size(disp_init_data),
         .mipi_config = {
             .dsi_bus = mipiDsiBus,
             .dpi_config = &dpi_config,
             .lane_num = 2,
         },
     };

Note: If removing static causes issues with the library expecting these configs to persist beyond the function call, store them as class members instead.

🤖 Prompt for AI Agents
In @Devices/m5stack-tab5/Source/devices/Ili9881cDisplay.cpp around lines 117 -
126, The static ili9881c_vendor_config_t vendor_config captures the instance
member mipiDsiBus which can become stale if Ili9881cDisplay instances are
recreated or multiple instances exist; make vendor_config an instance-owned
object (remove the static and move vendor_config into the Ili9881cDisplay class
as a member initialized with .mipi_config.dsi_bus = mipiDsiBus and other fields
set from disp_init_data/dpi_config) so its lifetime matches the instance, or if
you must keep it static, add a clear runtime guard or static assertion enforcing
single-instance usage and document the requirement.


// Create a mutable copy of panelConfig to set vendor_config
esp_lcd_panel_dev_config_t mutable_panel_config = panelConfig;
mutable_panel_config.vendor_config = &vendor_config;

if (esp_lcd_new_panel_ili9881c(ioHandle, &mutable_panel_config, &panelHandle) != ESP_OK) {
LOGGER.error("Failed to create panel");
return false;
}

LOGGER.info("Panel created successfully");
// Defer reset/init to base class applyConfiguration to avoid double initialization
return true;
}

lvgl_port_display_dsi_cfg_t Ili9881cDisplay::getLvglPortDisplayDsiConfig(esp_lcd_panel_io_handle_t /*ioHandle*/, esp_lcd_panel_handle_t /*panelHandle*/) {
// Disable avoid_tearing to prevent stalls/blank flashes when other tasks (e.g. flash writes) block timing
return lvgl_port_display_dsi_cfg_t{
.flags = {
.avoid_tearing = 0,
},
};
}
Loading