diff --git a/components/display_drivers/include/class.hpp b/components/display_drivers/include/class.hpp new file mode 100644 index 000000000..88fc4e874 --- /dev/null +++ b/components/display_drivers/include/class.hpp @@ -0,0 +1,51 @@ + +/** + * @brief Base class for display drivers. + */ +class DisplayDriver { +public: + /// @brief Construct a DisplayDriver + /// @param config Configuration for the display driver + explicit DisplayDriver(const Config &config) + : config_(config) {} + + /// @brief Initialize the display driver + /// @param write Write function for sending commands to the display + /// @param send_lines Function for sending pixel data to the display + /// @param width Width of the display in pixels + /// @param height Height of the display in pixels + /// @return True if initialization was successful, false otherwise + virtual bool initialize(write_command_fn write, send_lines_fn send_lines, size_t width, + size_t height) { + write_command_ = write; + send_lines_ = send_lines; + width_ = width; + height_ = height; + return true; + } + + /// @brief Reset the display + virtual void reset() { + std::scoped_lock lk(config_mutex_); + if (config_.reset_pin != GPIO_NUM_NC) { + gpio_set_level(config_.reset_pin, config_.reset_value); + std::this_thread::sleep_for(std::chrono::milliseconds(10)); + gpio_set_level(config_.reset_pin, !config_.reset_value); + std::this_thread::sleep_for(std::chrono::milliseconds(120)); + } + } + + /// @brief Get the width of the display + /// @return Width in pixels + size_t width() const { return width_; } + + /// @brief Get the height of the display + /// @return Height in pixels + size_t height() const { return height_; } + +protected: + std::mutex config_mutex_; + Config config_; + size_t width_{0}; + size_t height_{0}; +}; diff --git a/components/display_drivers/include/st7123.hpp b/components/display_drivers/include/st7123.hpp new file mode 100644 index 000000000..b1ed6ea90 --- /dev/null +++ b/components/display_drivers/include/st7123.hpp @@ -0,0 +1,195 @@ +#pragma once + +#include +#include +#include + +#include "display_drivers.hpp" + +namespace espp { + +class St7123 { + // MADCTL bits (see Espressif driver) + static constexpr uint8_t GS_BIT = 1 << 0; // Row mirror (Y) + static constexpr uint8_t SS_BIT = 1 << 1; // Column mirror (X) + static constexpr uint8_t BGR_BIT = 1 << 3; + +public: + enum class Command : uint8_t { + NOP = 0x00, + SWRESET = 0x01, + RDDID = 0x04, + SLPIN = 0x10, + SLPOUT = 0x11, + NORON = 0x13, + INVOFF = 0x20, + INVON = 0x21, + DISPOFF = 0x28, + DISPON = 0x29, + CASET = 0x2A, + RASET = 0x2B, + RAMWR = 0x2C, + RAMRD = 0x2E, + MADCTL = 0x36, + COLMOD = 0x3A, + }; + + static bool initialize(const display_drivers::Config &config) { + write_command_ = config.write_command; + read_command_ = config.read_command; + lcd_send_lines_ = config.lcd_send_lines; + reset_pin_ = config.reset_pin; + dc_pin_ = config.data_command_pin; + offset_x_ = config.offset_x; + offset_y_ = config.offset_y; + mirror_x_ = config.mirror_x; + mirror_y_ = config.mirror_y; + mirror_portrait_ = config.mirror_portrait; + swap_xy_ = config.swap_xy; + swap_color_order_ = config.swap_color_order; + + // Initialize display pins + display_drivers::init_pins(reset_pin_, dc_pin_, config.reset_value); + + // MADCTL value + uint8_t madctl = 0; + if (mirror_x_) + madctl |= GS_BIT; + if (mirror_y_) + madctl |= SS_BIT; + if (swap_color_order_) + madctl |= BGR_BIT; + // Note: swap_xy_ not supported by ST7123 MADCTL + + // COLMOD value + uint8_t colmod = 0x55; // 16bpp default + switch (config.bits_per_pixel) { + case 16: + colmod = 0x55; + break; + case 18: + colmod = 0x66; + break; + case 24: + colmod = 0x77; + break; + default: + break; + } + // ST7123 vendor-specific init sequence (from Espressif driver) + using Cmd = display_drivers::DisplayInitCmd<>; + std::array init_cmds = {{ + {0x60, {0x71, 0x23, 0xa2}, 0}, + {0x60, {0x71, 0x23, 0xa3}, 0}, + {0x60, {0x71, 0x23, 0xa4}, 0}, + {0xA4, {0x31}, 0}, + {0xD7, {0x10, 0x0A, 0x10, 0x2A, 0x80, 0x80}, 0}, + {0x90, {0x71, 0x23, 0x5A, 0x20, 0x24, 0x09, 0x09}, 0}, + {0xA3, + {0x80, 0x01, 0x88, 0x30, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, + 0x1E, 0x5C, 0x1E, 0x80, 0x00, 0x4F, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, + 0x00, 0x00, 0x1E, 0x5C, 0x1E, 0x80, 0x00, 0x6F, 0x58, 0x00, 0x00, 0x00, 0xFF}, + 0}, + {0xA6, + {0x03, 0x00, 0x24, 0x55, 0x36, 0x00, 0x39, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x00, 0x24, + 0x55, 0x38, 0x00, 0x37, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x00, 0x24, 0x11, 0x00, 0x00, + 0x00, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x00, 0xEC, 0x11, 0x00, 0x03, 0x00, 0x03, 0x6E, + 0x6E, 0xFF, 0xFF, 0x00, 0x08, 0x80, 0x08, 0x80, 0x06, 0x00, 0x00, 0x00, 0x00}, + 0}, + {0xA7, + {0x19, 0x19, 0x80, 0x64, 0x40, 0x07, 0x16, 0x40, 0x00, 0x44, 0x03, 0x6E, 0x6E, 0x91, 0xFF, + 0x08, 0x80, 0x64, 0x40, 0x25, 0x34, 0x40, 0x00, 0x02, 0x01, 0x6E, 0x6E, 0x91, 0xFF, 0x08, + 0x80, 0x64, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x08, 0x80, + 0x64, 0x40, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x6E, 0x6E, 0x84, 0xFF, 0x08, 0x80, 0x44}, + 0}, + {0xAC, + {0x03, 0x19, 0x19, 0x18, 0x18, 0x06, 0x13, 0x13, 0x11, 0x11, 0x08, 0x08, 0x0A, 0x0A, 0x1C, + 0x1C, 0x07, 0x07, 0x00, 0x00, 0x02, 0x02, 0x01, 0x19, 0x19, 0x18, 0x18, 0x06, 0x12, 0x12, + 0x10, 0x10, 0x09, 0x09, 0x0B, 0x0B, 0x1C, 0x1C, 0x07, 0x07, 0x03, 0x03, 0x01, 0x01}, + 0}, + {0xAD, + {0xF0, 0x00, 0x46, 0x00, 0x03, 0x50, 0x50, 0xFF, 0xFF, 0xF0, 0x40, 0x06, 0x01, + 0x07, 0x42, 0x42, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF}, + 0}, + {0xAE, {0xFE, 0x3F, 0x3F, 0xFE, 0x3F, 0x3F, 0x00}, 0}, + {0xB2, + {0x15, 0x19, 0x05, 0x23, 0x49, 0xAF, 0x03, 0x2E, 0x5C, 0xD2, 0xFF, 0x10, 0x20, 0xFD, 0x20, + 0xC0, 0x00}, + 0}, + {0xE8, + {0x20, 0x6F, 0x04, 0x97, 0x97, 0x3E, 0x04, 0xDC, 0xDC, 0x3E, 0x06, 0xFA, 0x26, 0x3E}, + 0}, + {0x75, {0x03, 0x04}, 0}, + {0xE7, + {0x3B, 0x00, 0x00, 0x7C, 0xA1, 0x8C, 0x20, 0x1A, 0xF0, 0xB1, 0x50, 0x00, + 0x50, 0xB1, 0x50, 0xB1, 0x50, 0xD8, 0x00, 0x55, 0x00, 0xB1, 0x00, 0x45, + 0xC9, 0x6A, 0xFF, 0x5A, 0xD8, 0x18, 0x88, 0x15, 0xB1, 0x01, 0x01, 0x77}, + 0}, + {0xEA, {0x13, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x2C}, 0}, + {0xB0, {0x22, 0x43, 0x11, 0x61, 0x25, 0x43, 0x43}, 0}, + {0xB7, {0x00, 0x00, 0x73, 0x73}, 0}, + {0xBF, {0xA6, 0xAA}, 0}, + {0xA9, {0x00, 0x00, 0x73, 0xFF, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03}, 0}, + {0xC8, + {0x00, 0x00, 0x10, 0x1F, 0x36, 0x00, 0x5D, 0x04, 0x9D, 0x05, 0x10, 0xF2, 0x06, + 0x60, 0x03, 0x11, 0xAD, 0x00, 0xEF, 0x01, 0x22, 0x2E, 0x0E, 0x74, 0x08, 0x32, + 0xDC, 0x09, 0x33, 0x0F, 0xF3, 0x77, 0x0D, 0xB0, 0xDC, 0x03, 0xFF}, + 0}, + {0xC9, + {0x00, 0x00, 0x10, 0x1F, 0x36, 0x00, 0x5D, 0x04, 0x9D, 0x05, 0x10, 0xF2, 0x06, + 0x60, 0x03, 0x11, 0xAD, 0x00, 0xEF, 0x01, 0x22, 0x2E, 0x0E, 0x74, 0x08, 0x32, + 0xDC, 0x09, 0x33, 0x0F, 0xF3, 0x77, 0x0D, 0xB0, 0xDC, 0x03, 0xFF}, + 0}, + {0x11, {0x00}, 100}, + {0x29, {0x00}, 0}, + {0x35, {0x00}, 100}, + }}; + + // Send vendor-specific init sequence + for (const auto &cmd : init_cmds) { + write_command_(cmd.command, std::span(cmd.parameters), 0); + if (cmd.delay_ms > 0) { + std::this_thread::sleep_for(std::chrono::milliseconds(cmd.delay_ms)); + } + std::this_thread::sleep_for(std::chrono::milliseconds(5)); + } + + // Set MADCTL (mirror/color order) + write_command_(static_cast(Command::MADCTL), std::span(&madctl, 1), 0); + // Set COLMOD (color depth) + write_command_(static_cast(Command::COLMOD), std::span(&colmod, 1), 0); + + return true; + } + + static constexpr const char *id() { return "ST7123"; } + +protected: + static display_drivers::write_command_fn write_command_; + static display_drivers::read_command_fn read_command_; + static display_drivers::send_lines_fn lcd_send_lines_; + static gpio_num_t reset_pin_; + static gpio_num_t dc_pin_; + static int offset_x_; + static int offset_y_; + static bool swap_xy_; + static bool mirror_x_; + static bool mirror_y_; + static bool mirror_portrait_; + static bool swap_color_order_; +}; + +inline display_drivers::write_command_fn St7123::write_command_{nullptr}; +inline display_drivers::read_command_fn St7123::read_command_{nullptr}; +inline display_drivers::send_lines_fn St7123::lcd_send_lines_{nullptr}; +inline gpio_num_t St7123::reset_pin_{GPIO_NUM_NC}; +inline gpio_num_t St7123::dc_pin_{GPIO_NUM_NC}; +inline int St7123::offset_x_{0}; +inline int St7123::offset_y_{0}; +inline bool St7123::swap_xy_{false}; +inline bool St7123::mirror_x_{false}; +inline bool St7123::mirror_y_{false}; +inline bool St7123::mirror_portrait_{false}; +inline bool St7123::swap_color_order_{false}; + +} // namespace espp diff --git a/components/m5stack-tab5/README.md b/components/m5stack-tab5/README.md index 47de5a714..fa9ed6cba 100644 --- a/components/m5stack-tab5/README.md +++ b/components/m5stack-tab5/README.md @@ -10,6 +10,7 @@ The `espp::M5StackTab5` component provides a singleton hardware abstraction for ### Display & Touch - 5″ 1280 × 720 IPS TFT screen via MIPI-DSI +- **Automatic display controller detection** (supports ILI9881 or ST7123) - GT911 multi-touch controller (I²C) for smooth interaction - Adjustable backlight brightness control @@ -65,6 +66,28 @@ The `espp::M5StackTab5` component provides a singleton hardware abstraction for | Battery | NP-F550 2000mAh removable | | Expansion | Grove, M5-Bus, STAMP pads, GPIO headers | +## Display Controller Auto-Detection + +The M5Stack Tab5 hardware can be manufactured with one of two different MIPI-DSI display controllers: +- **ILI9881** (earlier hardware revisions) +- **ST7123** (newer hardware revisions) + +The BSP automatically detects which display controller is present during initialization by: +1. Attempting to initialize with the ILI9881 driver first +2. If ILI9881 detection fails, falling back to ST7123 initialization +3. Logging the detected controller type for debugging + +This means your application code works seamlessly across both hardware variants without any code changes. You can optionally query the detected controller type: + +```cpp +auto& tab5 = espp::M5StackTab5::get(); +tab5.initialize_lcd(); + +// Query the detected controller +auto controller_type = tab5.get_display_controller(); +const char* controller_name = tab5.get_display_controller_name(); +``` + ## Example The [example](./example) shows how to use the `espp::M5StackTab5` hardware abstraction component to initialize and use various subsystems of the Tab5. diff --git a/components/m5stack-tab5/example/main/m5stack_tab5_example.cpp b/components/m5stack-tab5/example/main/m5stack_tab5_example.cpp index 141f5c97c..281b7209e 100644 --- a/components/m5stack-tab5/example/main/m5stack_tab5_example.cpp +++ b/components/m5stack-tab5/example/main/m5stack_tab5_example.cpp @@ -307,17 +307,20 @@ extern "C" void app_main(void) { // start a simple thread to do the lv_task_handler every 16ms logger.info("Starting LVGL task..."); espp::Task lv_task({.callback = [](std::mutex &m, std::condition_variable &cv) -> bool { + auto start_time = std::chrono::high_resolution_clock::now(); { std::lock_guard lock(lvgl_mutex); lv_task_handler(); } std::unique_lock lock(m); - cv.wait_for(lock, 16ms); + cv.wait_until(lock, start_time + 16ms, []() { return false; }); return false; }, .task_config = { .name = "lv_task", .stack_size_bytes = 10 * 1024, + .priority = 20, + .core_id = 1, }}); lv_task.start(); @@ -348,7 +351,7 @@ extern "C" void app_main(void) { // sleep first in case we don't get IMU data and need to exit early { std::unique_lock lock(m); - cv.wait_for(lock, 10ms); + cv.wait_for(lock, 20ms); } static auto &tab5 = espp::M5StackTab5::get(); static auto imu = tab5.imu(); @@ -483,7 +486,7 @@ extern "C" void app_main(void) { .name = "Data Display Task", .stack_size_bytes = 6 * 1024, .priority = 10, - .core_id = 0, + .core_id = 1, }}); imu_task.start(); diff --git a/components/m5stack-tab5/include/m5stack-tab5.hpp b/components/m5stack-tab5/include/m5stack-tab5.hpp index df71d89df..06567fbc5 100644 --- a/components/m5stack-tab5/include/m5stack-tab5.hpp +++ b/components/m5stack-tab5/include/m5stack-tab5.hpp @@ -39,6 +39,7 @@ #include "led.hpp" #include "pi4ioe5v.hpp" #include "rx8130ce.hpp" +#include "st7123.hpp" #include "touchpad_input.hpp" // #include "wifi_ap.hpp" // #include "wifi_sta.hpp" @@ -75,8 +76,33 @@ class M5StackTab5 : public BaseComponent { /// Alias for the pixel type used by the Tab5 display using Pixel = lv_color16_t; - /// Alias for the display driver used by the Tab5 - using DisplayDriver = espp::Ili9881; + /// Enum for display controller type + enum class DisplayController { UNKNOWN, ILI9881, ST7123 }; + + DisplayController detect_display_controller(); + + /// Get the detected display controller type + /// \return The display controller type + DisplayController get_display_controller() const { return display_controller_; } + + /// Get a string name for the display controller + /// \return String name of the controller + const char *get_display_controller_name() const { + return get_display_controller_name(display_controller_); + } + + /// Get a string name for the display controller + /// \return String name of the controller + const char *get_display_controller_name(DisplayController controller) const { + switch (controller) { + case DisplayController::ILI9881: + return "ILI9881"; + case DisplayController::ST7123: + return "ST7123"; + default: + return "Unknown"; + } + } /// Alias for the GT911 touch controller used by the Tab5 using TouchDriver = espp::Gt911; @@ -710,6 +736,9 @@ class M5StackTab5 : public BaseComponent { esp_lcd_panel_handle_t panel{nullptr}; // color handle } lcd_handles_{}; + // Display controller detection + DisplayController display_controller_{DisplayController::UNKNOWN}; + // original function pointer for the panel del, init esp_err_t (*original_panel_del_)(esp_lcd_panel_t *panel){nullptr}; esp_err_t (*original_panel_init_)(esp_lcd_panel_t *panel){nullptr}; diff --git a/components/m5stack-tab5/src/video.cpp b/components/m5stack-tab5/src/video.cpp index 588bcc535..872c73b0a 100644 --- a/components/m5stack-tab5/src/video.cpp +++ b/components/m5stack-tab5/src/video.cpp @@ -12,9 +12,26 @@ using namespace std::chrono_literals; namespace espp { +M5StackTab5::DisplayController M5StackTab5::detect_display_controller() { + auto &i2c = internal_i2c(); + + // Probe for the GT911 touch controller, if it exists we have an ILI9881 display + bool exists = i2c.probe_device(0x14); + if (exists) { + return M5StackTab5::DisplayController::ILI9881; + } + + // Probe for the ST7123 display controller + exists = i2c.probe_device(0x55); + if (exists) { + return M5StackTab5::DisplayController::ST7123; + } + + // Unknown display controller + return M5StackTab5::DisplayController::UNKNOWN; +} bool M5StackTab5::initialize_lcd() { - logger_.info("Initializing M5Stack Tab5 LCD (MIPI-DSI, ILI9881C, {}x{})", display_width_, - display_height_); + logger_.info("Initializing M5Stack Tab5 LCD (MIPI-DSI, {}x{})", display_width_, display_height_); if (!ioexp_0x43_) { if (!initialize_io_expanders()) { @@ -66,6 +83,10 @@ bool M5StackTab5::initialize_lcd() { lcd_reset(false); // Release reset std::this_thread::sleep_for(120ms); + // Detect and initialize the appropriate display controller + logger_.info("Detecting display controller type"); + auto detected_controller = detect_display_controller(); + // create MIPI DSI bus first, it will initialize the DSI PHY as well if (lcd_handles_.mipi_dsi_bus == nullptr) { logger_.info("Creating MIPI DSI bus"); @@ -73,7 +94,8 @@ bool M5StackTab5::initialize_lcd() { .bus_id = 0, .num_data_lanes = 2, .phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT, - .lane_bit_rate_mbps = 730, + .lane_bit_rate_mbps = + (uint32_t)(detected_controller == DisplayController::ILI9881 ? 730 : 965), }; ret = esp_lcd_new_dsi_bus(&bus_config, &lcd_handles_.mipi_dsi_bus); if (ret != ESP_OK) { @@ -98,11 +120,18 @@ bool M5StackTab5::initialize_lcd() { } } - // Create DPI panel with M5Stack Tab5 official ILI9881 timing parameters - if (lcd_handles_.panel == nullptr) { + if (detected_controller == DisplayController::UNKNOWN) { + logger_.error("Unable to detect display controller"); + return false; + } + logger_.info("Detected display controller: {}", get_display_controller_name(detected_controller)); + + esp_lcd_dpi_panel_config_t dpi_cfg{}; + memset(&dpi_cfg, 0, sizeof(dpi_cfg)); + + if (detected_controller == DisplayController::ILI9881 && lcd_handles_.panel == nullptr) { + // Create DPI panel with M5Stack Tab5 official ILI9881 timing parameters logger_.info("Creating MIPI DSI DPI panel with M5Stack Tab5 ILI9881 configuration"); - esp_lcd_dpi_panel_config_t dpi_cfg{}; - memset(&dpi_cfg, 0, sizeof(dpi_cfg)); dpi_cfg.virtual_channel = 0; dpi_cfg.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; dpi_cfg.dpi_clock_freq_mhz = 60; @@ -118,6 +147,24 @@ bool M5StackTab5::initialize_lcd() { dpi_cfg.video_timing.vsync_front_porch = 20; dpi_cfg.flags.use_dma2d = true; + } else if (detected_controller == DisplayController::ST7123 && lcd_handles_.panel == nullptr) { + dpi_cfg.virtual_channel = 0; + dpi_cfg.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; + dpi_cfg.dpi_clock_freq_mhz = 100; + dpi_cfg.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; + dpi_cfg.num_fbs = 1; + dpi_cfg.video_timing.h_size = display_width_; + dpi_cfg.video_timing.v_size = display_height_; + dpi_cfg.video_timing.hsync_back_porch = 40; + dpi_cfg.video_timing.hsync_pulse_width = 2; + dpi_cfg.video_timing.hsync_front_porch = 40; + dpi_cfg.video_timing.vsync_back_porch = 8; + dpi_cfg.video_timing.vsync_pulse_width = 2; + dpi_cfg.video_timing.vsync_front_porch = 220; + dpi_cfg.flags.use_dma2d = true; + } + + if (lcd_handles_.panel == nullptr) { logger_.info("Creating DPI panel with resolution {}x{}", dpi_cfg.video_timing.h_size, dpi_cfg.video_timing.v_size); ret = esp_lcd_new_panel_dpi(lcd_handles_.mipi_dsi_bus, &dpi_cfg, &lcd_handles_.panel); @@ -127,10 +174,7 @@ bool M5StackTab5::initialize_lcd() { } } - // Now initialize DisplayDriver for any additional configuration - logger_.info("Initializing DisplayDriver with DSI configuration"); - using namespace std::placeholders; - DisplayDriver::initialize(espp::display_drivers::Config{ + espp::display_drivers::Config display_config{ .write_command = std::bind_front(&M5StackTab5::dsi_write_command, this), .read_command = std::bind_front(&M5StackTab5::dsi_read_command, this), .lcd_send_lines = nullptr, @@ -145,7 +189,26 @@ bool M5StackTab5::initialize_lcd() { .mirror_x = mirror_x, .mirror_y = mirror_y, .mirror_portrait = false, - }); + }; + + if (detected_controller == DisplayController::ILI9881) { + logger_.info("Initializing as ILI9881"); + if (espp::Ili9881::initialize(display_config)) { + logger_.info("Successfully initialized ILI9881 display controller"); + display_controller_ = DisplayController::ILI9881; + } + } else if (detected_controller == DisplayController::ST7123) { + logger_.info("Initializing as ST7123"); + if (espp::St7123::initialize(display_config)) { + logger_.info("Successfully initialized ST7123 display controller"); + display_controller_ = DisplayController::ST7123; + } + } else { + logger_.error("Failed to detect display controller"); + return false; + } + + logger_.info("Display controller: {}", get_display_controller_name()); // call init on the panel logger_.info("Calling low-level panel init");