diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 26dd83b21..4b9e4832b 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -37,12 +37,14 @@ jobs:
           target: esp32
         - path: 'components/button/example'
           target: esp32
-        - path: 'components/controller/example'
+        - path: 'components/chsc6x/example'
           target: esp32s3
         - path: 'components/cli/example'
           target: esp32
         - path: 'components/color/example'
           target: esp32
+        - path: 'components/controller/example'
+          target: esp32s3
         - path: 'components/cst816/example'
           target: esp32s3
         - path: 'components/csv/example'
@@ -111,6 +113,8 @@ jobs:
           target: esp32s3
         - path: 'components/rtsp/example'
           target: esp32
+        - path: 'components/seeed-studio-round-display/example'
+          target: esp32s3
         - path: 'components/serialization/example'
           target: esp32
         - path: 'components/socket/example'
diff --git a/components/chsc6x/CMakeLists.txt b/components/chsc6x/CMakeLists.txt
new file mode 100644
index 000000000..d43ade275
--- /dev/null
+++ b/components/chsc6x/CMakeLists.txt
@@ -0,0 +1,4 @@
+idf_component_register(
+  INCLUDE_DIRS "include"
+  REQUIRES "base_peripheral"
+  )
diff --git a/components/chsc6x/example/CMakeLists.txt b/components/chsc6x/example/CMakeLists.txt
new file mode 100644
index 000000000..e57c4873f
--- /dev/null
+++ b/components/chsc6x/example/CMakeLists.txt
@@ -0,0 +1,21 @@
+# The following lines of boilerplate have to be in your project's CMakeLists
+# in this exact order for cmake to work correctly
+cmake_minimum_required(VERSION 3.5)
+
+include($ENV{IDF_PATH}/tools/cmake/project.cmake)
+
+# add the component directories that we want to use
+set(EXTRA_COMPONENT_DIRS
+  "../../../components/"
+)
+
+set(
+  COMPONENTS
+  "main esptool_py task chsc6x i2c"
+  CACHE STRING
+  "List of components to include"
+  )
+
+project(chsc6x_example)
+
+set(CMAKE_CXX_STANDARD 20)
diff --git a/components/chsc6x/example/README.md b/components/chsc6x/example/README.md
new file mode 100644
index 000000000..7836e0137
--- /dev/null
+++ b/components/chsc6x/example/README.md
@@ -0,0 +1,37 @@
+# CHSC6X Example
+
+This example shows how to use the CHSC6X touch controller with ESP32. It is
+designed to run on a Seeed Studio Round Display.
+
+## How to use example
+
+### Hardware Required
+
+Seeed Studio Round Display (or any other dev board with a CHSC6X touch
+controller)
+
+### Configure
+
+``` 
+idf.py menuconfig
+```
+
+Set the hardware configuration for the example.
+
+### Build and Flash
+
+Build the project and flash it to the board, then run monitor tool to view serial output:
+
+```
+idf.py -p PORT flash monitor
+```
+
+(Replace PORT with the name of the serial port to use.)
+
+(To exit the serial monitor, type ``Ctrl-]``.)
+
+See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.
+
+## Example Output
+
+![CleanShot 2024-08-26 at 08 21 47](https://github.com/user-attachments/assets/44ed3371-0c8f-44d1-992d-01ee1c983efc)
diff --git a/components/chsc6x/example/main/CMakeLists.txt b/components/chsc6x/example/main/CMakeLists.txt
new file mode 100644
index 000000000..a941e22ba
--- /dev/null
+++ b/components/chsc6x/example/main/CMakeLists.txt
@@ -0,0 +1,2 @@
+idf_component_register(SRC_DIRS "."
+                       INCLUDE_DIRS ".")
diff --git a/components/chsc6x/example/main/Kconfig.projbuild b/components/chsc6x/example/main/Kconfig.projbuild
new file mode 100644
index 000000000..cb70b2a00
--- /dev/null
+++ b/components/chsc6x/example/main/Kconfig.projbuild
@@ -0,0 +1,39 @@
+menu "Example Configuration"
+
+    choice EXAMPLE_HARDWARE
+        prompt "Hardware"
+        default EXAMPLE_HARDWARE_XIAOS3
+        help
+            Select the hardware to run this example on.
+
+        config EXAMPLE_HARDWARE_XIAOS3
+            depends on IDF_TARGET_ESP32S3
+            bool "XIAO-S3"
+
+        config EXAMPLE_HARDWARE_QTPYS3
+            depends on IDF_TARGET_ESP32S3
+            bool "QTPY ESP32 S3"
+
+        config EXAMPLE_HARDWARE_CUSTOM
+            bool "Custom"
+    endchoice
+
+    config EXAMPLE_I2C_SCL_GPIO
+        int "SCL GPIO Num"
+        range 0 50
+        default 6 if EXAMPLE_HARDWARE_XIAOS3
+        default 6 if EXAMPLE_HARDWARE_QTPYS3
+        default 19 if EXAMPLE_HARDWARE_CUSTOM
+        help
+            GPIO number for I2C Master clock line.
+
+    config EXAMPLE_I2C_SDA_GPIO
+        int "SDA GPIO Num"
+        range 0 50
+        default 5 if EXAMPLE_HARDWARE_XIAOS3
+        default 7 if EXAMPLE_HARDWARE_QTPYS3
+        default 22 if EXAMPLE_HARDWARE_CUSTOM
+        help
+            GPIO number for I2C Master data line.
+
+endmenu
diff --git a/components/chsc6x/example/main/chsc6x_example.cpp b/components/chsc6x/example/main/chsc6x_example.cpp
new file mode 100644
index 000000000..fc174dd37
--- /dev/null
+++ b/components/chsc6x/example/main/chsc6x_example.cpp
@@ -0,0 +1,81 @@
+#include <chrono>
+#include <sdkconfig.h>
+#include <vector>
+
+#include "chsc6x.hpp"
+#include "i2c.hpp"
+#include "task.hpp"
+
+using namespace std::chrono_literals;
+
+extern "C" void app_main(void) {
+  {
+    std::atomic<bool> quit_test = false;
+    fmt::print("Starting chsc6x example\n");
+    //! [chsc6x example]
+    // make the I2C that we'll use to communicate
+    espp::I2c i2c({
+        .port = I2C_NUM_0,
+        .sda_io_num = (gpio_num_t)CONFIG_EXAMPLE_I2C_SDA_GPIO,
+        .scl_io_num = (gpio_num_t)CONFIG_EXAMPLE_I2C_SCL_GPIO,
+        .sda_pullup_en = GPIO_PULLUP_ENABLE,
+        .scl_pullup_en = GPIO_PULLUP_ENABLE,
+        .timeout_ms = 100,
+        .clk_speed = 400 * 1000,
+    });
+
+    bool has_chsc6x = i2c.probe_device(espp::Chsc6x::DEFAULT_ADDRESS);
+    fmt::print("Touchpad probe: {}\n", has_chsc6x);
+
+    // now make the chsc6x which decodes the data
+    espp::Chsc6x chsc6x({.write = std::bind(&espp::I2c::write, &i2c, std::placeholders::_1,
+                                            std::placeholders::_2, std::placeholders::_3),
+                         .read = std::bind(&espp::I2c::read, &i2c, std::placeholders::_1,
+                                           std::placeholders::_2, std::placeholders::_3),
+                         .log_level = espp::Logger::Verbosity::WARN});
+
+    // and finally, make the task to periodically poll the chsc6x and print
+    // the state
+    auto task_fn = [&chsc6x](std::mutex &m, std::condition_variable &cv) {
+      std::error_code ec;
+      // update the state
+      bool new_data = chsc6x.update(ec);
+      if (ec) {
+        fmt::print("Could not update state\n");
+        return false;
+      }
+      if (!new_data) {
+        return false; // don't stop the task
+      }
+      // get the state
+      uint8_t num_touch_points = 0;
+      uint16_t x = 0, y = 0;
+      chsc6x.get_touch_point(&num_touch_points, &x, &y);
+      if (ec) {
+        fmt::print("Could not get touch point\n");
+        return false;
+      }
+      fmt::print("num_touch_points: {}, x: {}, y: {}\n", num_touch_points, x, y);
+      // 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, 50ms);
+      }
+      return false; // don't stop the task
+    };
+    auto task = espp::Task(
+        {.name = "Chsc6x Task", .callback = task_fn, .log_level = espp::Logger::Verbosity::WARN});
+    task.start();
+    //! [chsc6x example]
+    while (true) {
+      std::this_thread::sleep_for(100ms);
+    }
+  }
+
+  fmt::print("Chsc6x example complete!\n");
+
+  while (true) {
+    std::this_thread::sleep_for(1s);
+  }
+}
diff --git a/components/chsc6x/example/sdkconfig.defaults b/components/chsc6x/example/sdkconfig.defaults
new file mode 100644
index 000000000..482d3bb8c
--- /dev/null
+++ b/components/chsc6x/example/sdkconfig.defaults
@@ -0,0 +1,6 @@
+CONFIG_IDF_TARGET="esp32s3"
+
+# Common ESP-related
+#
+CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096
+CONFIG_ESP_MAIN_TASK_STACK_SIZE=8192
diff --git a/components/chsc6x/include/chsc6x.hpp b/components/chsc6x/include/chsc6x.hpp
new file mode 100644
index 000000000..d1028f9e3
--- /dev/null
+++ b/components/chsc6x/include/chsc6x.hpp
@@ -0,0 +1,80 @@
+#pragma once
+
+#include <atomic>
+#include <functional>
+
+#include "base_peripheral.hpp"
+
+namespace espp {
+/// @brief Driver for the Chsc6x touch controller
+///
+/// \section Example
+/// \snippet chsc6x_example.cpp chsc6x example
+class Chsc6x : public BasePeripheral<> {
+public:
+  /// Default address for the CHSC6X chip
+  static constexpr uint8_t DEFAULT_ADDRESS = 0x2E;
+
+  /// @brief Configuration for the CHSC6X driver
+  struct Config {
+    BasePeripheral::write_fn write;    ///< Function for writing to the CHSC6X chip
+    BasePeripheral::read_fn read;      ///< Function for reading from the CHSC6X chip
+    uint8_t address = DEFAULT_ADDRESS; ///< Which address to use for this chip?
+    espp::Logger::Verbosity log_level{
+        espp::Logger::Verbosity::WARN}; ///< Log verbosity for the input driver.
+  };
+
+  /// @brief Constructor for the CHSC6X driver
+  /// @param config The configuration for the driver
+  explicit Chsc6x(const Config &config)
+      : BasePeripheral({.address = config.address, .write = config.write, .read = config.read},
+                       "Chsc6x", config.log_level) {}
+
+  /// @brief Update the state of the CHSC6X driver
+  /// @param ec Error code to set if an error occurs
+  /// @return True if the CHSC6X has new data, false otherwise
+  bool update(std::error_code &ec) {
+    static constexpr size_t DATA_LEN = 5;
+    static uint8_t data[DATA_LEN];
+    read_many_from_register(0, data, DATA_LEN, ec);
+    if (ec)
+      return false;
+
+    // first byte is non-zero when touched, 3rd byte is x, 5th byte is y
+    if (data[0] == 0) {
+      x_ = 0;
+      y_ = 0;
+      num_touch_points_ = 0;
+      return true;
+    }
+    x_ = data[2];
+    y_ = data[4];
+    num_touch_points_ = 1;
+    logger_.debug("Touch at ({}, {})", x_, y_);
+    return true;
+  }
+
+  /// @brief Get the number of touch points
+  /// @return The number of touch points as of the last update
+  /// @note This is a cached value from the last update() call
+  uint8_t get_num_touch_points() const { return num_touch_points_; }
+
+  /// @brief Get the touch point data
+  /// @param num_touch_points The number of touch points as of the last update
+  /// @param x The x coordinate of the touch point
+  /// @param y The y coordinate of the touch point
+  /// @note This is a cached value from the last update() call
+  void get_touch_point(uint8_t *num_touch_points, uint16_t *x, uint16_t *y) const {
+    *num_touch_points = get_num_touch_points();
+    if (*num_touch_points != 0) {
+      *x = x_;
+      *y = y_;
+    }
+  }
+
+protected:
+  std::atomic<uint8_t> num_touch_points_;
+  std::atomic<uint16_t> x_;
+  std::atomic<uint16_t> y_;
+}; // class Chsc6x
+} // namespace espp
diff --git a/components/display_drivers/CMakeLists.txt b/components/display_drivers/CMakeLists.txt
index 8a849e2b8..fbe01a1bf 100644
--- a/components/display_drivers/CMakeLists.txt
+++ b/components/display_drivers/CMakeLists.txt
@@ -1,6 +1,5 @@
 idf_component_register(
   INCLUDE_DIRS "include"
   SRC_DIRS "src"
-  PRIV_REQUIRES display driver esp_lcd
-  REQUIRES led
+  REQUIRES display driver esp_lcd led
 )
diff --git a/components/esp-box/CMakeLists.txt b/components/esp-box/CMakeLists.txt
index 49214c7ae..c6d777765 100644
--- a/components/esp-box/CMakeLists.txt
+++ b/components/esp-box/CMakeLists.txt
@@ -1,6 +1,6 @@
 idf_component_register(
   INCLUDE_DIRS "include"
   SRC_DIRS "src"
-  REQUIRES driver esp_lcd base_component codec display display_drivers i2c input_drivers interrupt gt911 task tt21100
+  REQUIRES driver base_component codec display display_drivers i2c input_drivers interrupt gt911 task tt21100
   REQUIRED_IDF_TARGETS "esp32s3"
   )
diff --git a/components/esp-box/example/sdkconfig.defaults b/components/esp-box/example/sdkconfig.defaults
index 81e7ff2c5..be4a2ecad 100644
--- a/components/esp-box/example/sdkconfig.defaults
+++ b/components/esp-box/example/sdkconfig.defaults
@@ -25,10 +25,12 @@ CONFIG_ESP_TIMER_TASK_STACK_SIZE=6144
 # set the functions into IRAM
 CONFIG_SPI_MASTER_IN_IRAM=y
 
+CONFIG_LV_DEF_REFR_PERIOD=16
+
 #
 # LVGL configuration - # Themes
 #
 CONFIG_LV_USE_THEME_DEFAULT=y
 CONFIG_LV_THEME_DEFAULT_DARK=y
 CONFIG_LV_THEME_DEFAULT_GROW=y
-CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=80
+CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=30
diff --git a/components/matouch-rotary-display/CMakeLists.txt b/components/matouch-rotary-display/CMakeLists.txt
index 362e8c02a..a85486c6e 100644
--- a/components/matouch-rotary-display/CMakeLists.txt
+++ b/components/matouch-rotary-display/CMakeLists.txt
@@ -2,6 +2,6 @@
 idf_component_register(
   INCLUDE_DIRS "include"
   SRC_DIRS "src"
-  REQUIRES driver esp_lcd base_component cst816 encoder display display_drivers i2c input_drivers interrupt task
+  REQUIRES driver base_component cst816 encoder display display_drivers i2c input_drivers interrupt task
   REQUIRED_IDF_TARGETS "esp32s3"
   )
diff --git a/components/matouch-rotary-display/example/sdkconfig.defaults b/components/matouch-rotary-display/example/sdkconfig.defaults
index 81e7ff2c5..be4a2ecad 100644
--- a/components/matouch-rotary-display/example/sdkconfig.defaults
+++ b/components/matouch-rotary-display/example/sdkconfig.defaults
@@ -25,10 +25,12 @@ CONFIG_ESP_TIMER_TASK_STACK_SIZE=6144
 # set the functions into IRAM
 CONFIG_SPI_MASTER_IN_IRAM=y
 
+CONFIG_LV_DEF_REFR_PERIOD=16
+
 #
 # LVGL configuration - # Themes
 #
 CONFIG_LV_USE_THEME_DEFAULT=y
 CONFIG_LV_THEME_DEFAULT_DARK=y
 CONFIG_LV_THEME_DEFAULT_GROW=y
-CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=80
+CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=30
diff --git a/components/seeed-studio-round-display/CMakeLists.txt b/components/seeed-studio-round-display/CMakeLists.txt
new file mode 100644
index 000000000..13aac6378
--- /dev/null
+++ b/components/seeed-studio-round-display/CMakeLists.txt
@@ -0,0 +1,5 @@
+idf_component_register(
+  INCLUDE_DIRS "include"
+  SRC_DIRS "src"
+  REQUIRES driver base_component display display_drivers i2c input_drivers interrupt task chsc6x
+  )
diff --git a/components/seeed-studio-round-display/Kconfig b/components/seeed-studio-round-display/Kconfig
new file mode 100644
index 000000000..210f10979
--- /dev/null
+++ b/components/seeed-studio-round-display/Kconfig
@@ -0,0 +1,8 @@
+menu "Seeed Studio Round Display Configuration"
+    config SEEED_STUDIO_ROUND_DISPLAY_INTERRUPT_STACK_SIZE
+        int "Interrupt stack size"
+        default 4096
+        help
+            Size of the stack used for the interrupt handler. Used by the touch
+            callback.
+endmenu
diff --git a/components/seeed-studio-round-display/example/CMakeLists.txt b/components/seeed-studio-round-display/example/CMakeLists.txt
new file mode 100644
index 000000000..16a44cf2a
--- /dev/null
+++ b/components/seeed-studio-round-display/example/CMakeLists.txt
@@ -0,0 +1,21 @@
+# The following lines of boilerplate have to be in your project's CMakeLists
+# in this exact order for cmake to work correctly
+cmake_minimum_required(VERSION 3.5)
+
+include($ENV{IDF_PATH}/tools/cmake/project.cmake)
+
+# add the component directories that we want to use
+set(EXTRA_COMPONENT_DIRS
+  "../../../components/"
+)
+
+set(
+  COMPONENTS
+  "main esptool_py seeed-studio-round-display"
+  CACHE STRING
+  "List of components to include"
+  )
+
+project(seeed_studio_roudn_display_example)
+
+set(CMAKE_CXX_STANDARD 20)
diff --git a/components/seeed-studio-round-display/example/README.md b/components/seeed-studio-round-display/example/README.md
new file mode 100644
index 000000000..ddcf5cb55
--- /dev/null
+++ b/components/seeed-studio-round-display/example/README.md
@@ -0,0 +1,40 @@
+# Seeed Studio Round Display Example
+
+This example shows how to use the `espp::SsRoundDisplay` hardware abstraction
+component to initialize the hardware components on the [Seeed Studio Round
+Display](https://wiki.seeedstudio.com/get_start_round_display/) when used with
+either the XIAO S3 or the QtPy ESP32S3.
+
+It initializes the touch and display subsystems. It reads the touchpad state and
+each time you touch the scren it uses LVGL to draw a circle where you touch.
+
+
+https://github.com/user-attachments/assets/4cac7882-2a79-4c7c-a139-d36e39322660
+
+
+## How to use example
+
+### Hardware Required
+
+This example is designed to run on either the QtPy ESP32S3 or the XIAO S3,
+though the `espp::SsRoundDisplay` can be used with custom hardware.
+
+### Build and Flash
+
+Build the project and flash it to the board, then run monitor tool to view
+serial output:
+
+```
+idf.py -p PORT flash monitor
+```
+
+(Replace PORT with the name of the serial port to use.)
+
+(To exit the serial monitor, type ``Ctrl-]``.)
+
+See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects.
+
+## Example Output
+
+![CleanShot 2024-08-26 at 08 24 22](https://github.com/user-attachments/assets/8ae7df9b-913b-4084-a7b7-1b17d5e491d3)
+
diff --git a/components/seeed-studio-round-display/example/main/CMakeLists.txt b/components/seeed-studio-round-display/example/main/CMakeLists.txt
new file mode 100644
index 000000000..a941e22ba
--- /dev/null
+++ b/components/seeed-studio-round-display/example/main/CMakeLists.txt
@@ -0,0 +1,2 @@
+idf_component_register(SRC_DIRS "."
+                       INCLUDE_DIRS ".")
diff --git a/components/seeed-studio-round-display/example/main/Kconfig.projbuild b/components/seeed-studio-round-display/example/main/Kconfig.projbuild
new file mode 100644
index 000000000..e6a3402b0
--- /dev/null
+++ b/components/seeed-studio-round-display/example/main/Kconfig.projbuild
@@ -0,0 +1,18 @@
+menu "Seeed Studio Round Display Example Config"
+
+    choice EXAMPLE_HARDWARE
+        prompt "Hardware"
+        default EXAMPLE_HARDWARE_XIAOS3
+        help
+            Select the hardware to run this example on.
+
+        config EXAMPLE_HARDWARE_QTPYS3
+            depends on IDF_TARGET_ESP32S3
+            bool "Qt Py ESP32 S3"
+
+        config EXAMPLE_HARDWARE_XIAOS3
+            depends on IDF_TARGET_ESP32S3
+            bool "XIAO Esp32 S3"
+    endchoice
+
+endmenu
diff --git a/components/seeed-studio-round-display/example/main/seeed_studio_round_display_example.cpp b/components/seeed-studio-round-display/example/main/seeed_studio_round_display_example.cpp
new file mode 100644
index 000000000..22f16b9c7
--- /dev/null
+++ b/components/seeed-studio-round-display/example/main/seeed_studio_round_display_example.cpp
@@ -0,0 +1,180 @@
+#include <chrono>
+#include <deque>
+#include <stdlib.h>
+#include <vector>
+
+#include "seeed-studio-round-display.hpp"
+
+using namespace std::chrono_literals;
+
+static constexpr size_t MAX_CIRCLES = 100;
+static std::deque<lv_obj_t *> circles;
+
+static std::recursive_mutex lvgl_mutex;
+static void draw_circle(int x0, int y0, int radius);
+static void clear_circles();
+static void on_rotate_pressed(lv_event_t *event);
+static void on_clear_pressed(lv_event_t *event);
+
+extern "C" void app_main(void) {
+  espp::Logger logger(
+      {.tag = "Seeed Studio Round Display Example", .level = espp::Logger::Verbosity::INFO});
+  logger.info("Starting example!");
+
+  //! [seeed studio round display example]
+#if CONFIG_EXAMPLE_HARDWARE_XIAOS3
+  logger.info("Using XiaoS3 hardware configuration");
+  espp::SsRoundDisplay::set_pin_config(espp::SsRoundDisplay::XiaoS3Config);
+#elif CONFIG_EXAMPLE_HARDWARE_QTPYS3
+  logger.info("Using QtpyS3 hardware configuration");
+  espp::SsRoundDisplay::set_pin_config(espp::SsRoundDisplay::QtpyS3Config);
+#else
+#error "Please select a hardware configuration"
+#endif
+  espp::SsRoundDisplay &round_display = espp::SsRoundDisplay::get();
+
+  auto touch_callback = [&](const auto &touch) {
+    // NOTE: since we're directly using the touchpad data, and not using the
+    // TouchpadInput + LVGL, we'll need to ensure the touchpad data is
+    // converted into proper screen coordinates instead of simply using the
+    // raw values.
+    static auto previous_touchpad_data = round_display.touchpad_convert(touch);
+    auto touchpad_data = round_display.touchpad_convert(touch);
+    if (touchpad_data != previous_touchpad_data) {
+      logger.info("Touch: {}", touchpad_data);
+      previous_touchpad_data = touchpad_data;
+      // if the button is pressed, clear the circles
+      if (touchpad_data.btn_state) {
+        clear_circles();
+      }
+      // if there is a touch point, draw a circle
+      if (touchpad_data.num_touch_points > 0) {
+        draw_circle(touchpad_data.x, touchpad_data.y, 10);
+      }
+    }
+  };
+
+  // initialize the LCD
+  if (!round_display.initialize_lcd()) {
+    logger.error("Failed to initialize LCD!");
+    return;
+  }
+  // set the pixel buffer to be 50 lines high
+  static constexpr size_t pixel_buffer_size = round_display.lcd_width() * 50;
+  espp::Task::BaseConfig display_task_config = {
+      .name = "Display",
+      .stack_size_bytes = 6 * 1024,
+      .priority = 10,
+      .core_id = 0,
+  };
+  // initialize the LVGL display for the seeed-studio-round-display
+  if (!round_display.initialize_display(pixel_buffer_size, display_task_config)) {
+    logger.error("Failed to initialize display!");
+    return;
+  }
+  // initialize the touchpad
+  if (!round_display.initialize_touch(touch_callback)) {
+    logger.error("Failed to initialize touchpad!");
+    return;
+  }
+
+  // set the background color to black
+  lv_obj_t *bg = lv_obj_create(lv_screen_active());
+  lv_obj_set_size(bg, round_display.lcd_width(), round_display.lcd_height());
+  lv_obj_set_style_bg_color(bg, lv_color_make(0, 0, 0), 0);
+
+  // add text in the center of the screen
+  lv_obj_t *label = lv_label_create(lv_screen_active());
+  lv_label_set_text(label, "Touch the screen!");
+  lv_obj_align(label, LV_ALIGN_CENTER, 0, 0);
+  lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0);
+
+  // add a button in the top middel which (when pressed) will rotate the display
+  // through 0, 90, 180, 270 degrees
+  lv_obj_t *btn = lv_btn_create(lv_screen_active());
+  lv_obj_set_size(btn, 50, 50);
+  lv_obj_align(btn, LV_ALIGN_TOP_MID, 0, 0);
+  lv_obj_t *label_btn = lv_label_create(btn);
+  lv_label_set_text(label_btn, LV_SYMBOL_REFRESH);
+  lv_obj_align(label_btn, LV_ALIGN_CENTER, 0, 0);
+  lv_obj_add_event_cb(btn, on_rotate_pressed, LV_EVENT_PRESSED, nullptr);
+
+  // add a button in the bottom middle which (when pressed) will clear the
+  // circles
+  lv_obj_t *btn_clear = lv_btn_create(lv_screen_active());
+  lv_obj_set_size(btn_clear, 50, 50);
+  lv_obj_align(btn_clear, LV_ALIGN_BOTTOM_MID, 0, 0);
+  lv_obj_add_state(btn_clear, LV_STATE_CHECKED); // make the button red
+  lv_obj_t *label_btn_clear = lv_label_create(btn_clear);
+  lv_label_set_text(label_btn_clear, LV_SYMBOL_TRASH);
+  lv_obj_align(label_btn_clear, LV_ALIGN_CENTER, 0, 0);
+  lv_obj_add_event_cb(btn_clear, on_clear_pressed, LV_EVENT_PRESSED, nullptr);
+
+  // disable scrolling on the screen (so that it doesn't behave weirdly when
+  // rotated and drawing with your finger)
+  lv_obj_set_scrollbar_mode(lv_screen_active(), LV_SCROLLBAR_MODE_OFF);
+  lv_obj_clear_flag(lv_screen_active(), LV_OBJ_FLAG_SCROLLABLE);
+
+  // start a simple thread to do the lv_task_handler every 16ms
+  espp::Task lv_task({.callback = [](std::mutex &m, std::condition_variable &cv) -> bool {
+                        {
+                          std::lock_guard<std::recursive_mutex> lock(lvgl_mutex);
+                          lv_task_handler();
+                        }
+                        std::unique_lock<std::mutex> lock(m);
+                        cv.wait_for(lock, 16ms);
+                        return false;
+                      },
+                      .task_config = {
+                          .name = "lv_task",
+                      }});
+  lv_task.start();
+
+  // set the display brightness to be 75%
+  round_display.brightness(75.0f);
+
+  // loop forever
+  while (true) {
+    std::this_thread::sleep_for(1s);
+  }
+  //! [seeed studio round display example]
+}
+
+static void on_rotate_pressed(lv_event_t *event) {
+  clear_circles();
+  std::lock_guard<std::recursive_mutex> lock(lvgl_mutex);
+  static auto rotation = LV_DISPLAY_ROTATION_0;
+  rotation = static_cast<lv_display_rotation_t>((static_cast<int>(rotation) + 1) % 4);
+  lv_display_t *disp = _lv_refr_get_disp_refreshing();
+  lv_disp_set_rotation(disp, rotation);
+}
+
+static void on_clear_pressed(lv_event_t *event) { clear_circles(); }
+
+static void draw_circle(int x0, int y0, int radius) {
+  std::lock_guard<std::recursive_mutex> lock(lvgl_mutex);
+  // if the number of circles is greater than the max, remove the oldest circle
+  if (circles.size() > MAX_CIRCLES) {
+    lv_obj_delete(circles.front());
+    circles.pop_front();
+  }
+  lv_obj_t *my_Cir = lv_obj_create(lv_screen_active());
+  lv_obj_set_scrollbar_mode(my_Cir, LV_SCROLLBAR_MODE_OFF);
+  lv_obj_set_size(my_Cir, radius * 2, radius * 2);
+  lv_obj_set_pos(my_Cir, x0 - radius, y0 - radius);
+  lv_obj_set_style_radius(my_Cir, LV_RADIUS_CIRCLE, 0);
+  // ensure the circle ignores touch events (so things behind it can still be
+  // interacted with)
+  lv_obj_clear_flag(my_Cir, LV_OBJ_FLAG_CLICKABLE);
+  circles.push_back(my_Cir);
+}
+
+static void clear_circles() {
+  std::lock_guard<std::recursive_mutex> lock(lvgl_mutex);
+  // remove the circles from lvgl
+  for (auto circle : circles) {
+    lv_obj_delete(circle);
+  }
+  // clear the vector
+  circles.clear();
+}
diff --git a/components/seeed-studio-round-display/example/sdkconfig.defaults b/components/seeed-studio-round-display/example/sdkconfig.defaults
new file mode 100644
index 000000000..65e28dce3
--- /dev/null
+++ b/components/seeed-studio-round-display/example/sdkconfig.defaults
@@ -0,0 +1,35 @@
+CONFIG_IDF_TARGET="esp32s3"
+
+CONFIG_FREERTOS_HZ=1000
+
+# set compiler optimization level to -O2 (compile for performance)
+CONFIG_COMPILER_OPTIMIZATION_PERF=y
+
+CONFIG_ESPTOOLPY_FLASHSIZE_8MB=y
+CONFIG_ESPTOOLPY_FLASHSIZE="8MB"
+CONFIG_ESPTOOLPY_FLASHMODE_QIO=y
+
+# ESP32-specific
+#
+CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ_240=y
+CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ=240
+
+# Common ESP-related
+#
+CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096
+CONFIG_ESP_MAIN_TASK_STACK_SIZE=16384
+
+# Set esp-timer task stack size to 6KB
+CONFIG_ESP_TIMER_TASK_STACK_SIZE=6144
+
+# set the functions into IRAM
+CONFIG_SPI_MASTER_IN_IRAM=y
+
+CONFIG_LV_DEF_REFR_PERIOD=16
+#
+# LVGL configuration - # Themes
+#
+CONFIG_LV_USE_THEME_DEFAULT=y
+CONFIG_LV_THEME_DEFAULT_DARK=y
+CONFIG_LV_THEME_DEFAULT_GROW=y
+CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=30
diff --git a/components/seeed-studio-round-display/include/seeed-studio-round-display.hpp b/components/seeed-studio-round-display/include/seeed-studio-round-display.hpp
new file mode 100644
index 000000000..ad2c2dc72
--- /dev/null
+++ b/components/seeed-studio-round-display/include/seeed-studio-round-display.hpp
@@ -0,0 +1,325 @@
+#pragma once
+
+#include "base_component.hpp"
+
+#include <driver/gpio.h>
+#include <driver/spi_master.h>
+#include <hal/spi_types.h>
+
+#include "chsc6x.hpp"
+#include "gc9a01.hpp"
+#include "i2c.hpp"
+#include "interrupt.hpp"
+#include "touchpad_input.hpp"
+
+namespace espp {
+/// The SsRoundDisplay class provides an interface to the Seeed Studio Round
+/// Display development board.
+///
+/// The class provides access to the following features:
+/// - Touchpad
+/// - Display
+///
+/// The class is a singleton and can be accessed using the get() method.
+///
+/// \note You must call set_pin_config() before calling get() for the first
+/// time, in order to provide the appropriate pin configuration for the
+/// controller board connected to the display. Some pin configuration structures
+/// are provided for convenience.
+///
+/// \section seeed_studio_round_display_example Example
+/// \snippet seeed_studio_round_display_example.cpp seeed studio round display example
+class SsRoundDisplay : public espp::BaseComponent {
+public:
+  using Pixel = lv_color16_t;              ///< Alias for the type of pixel the display uses.
+  using DisplayDriver = espp::Gc9a01;      ///< Alias for the display driver.
+  using TouchDriver = espp::Chsc6x;        ///< Alias for the touch driver.
+  using TouchpadData = espp::TouchpadData; ///< Alias for the touchpad data.
+
+  /// The touch callback function type
+  /// \param data The touchpad data
+  using touch_callback_t = std::function<void(const TouchpadData &)>;
+
+  /// The pin configuration structure
+  struct PinConfig {
+    gpio_num_t sda = GPIO_NUM_NC;             ///< I2C data
+    gpio_num_t scl = GPIO_NUM_NC;             ///< I2C clock
+    gpio_num_t usd_cs = GPIO_NUM_NC;          ///< uSD card chip select
+    gpio_num_t lcd_cs = GPIO_NUM_NC;          ///< LCD chip select
+    gpio_num_t lcd_dc = GPIO_NUM_NC;          ///< LCD data/command
+    gpio_num_t lcd_backlight = GPIO_NUM_NC;   ///< LCD backlight
+    gpio_num_t miso = GPIO_NUM_NC;            ///< SPI MISO
+    gpio_num_t mosi = GPIO_NUM_NC;            ///< SPI MOSI
+    gpio_num_t sck = GPIO_NUM_NC;             ///< SPI SCK
+    gpio_num_t touch_interrupt = GPIO_NUM_NC; ///< Touch interrupt
+
+    bool operator==(const PinConfig &rhs) const = default;
+    bool operator!=(const PinConfig &rhs) const = default;
+  };
+
+  /// @brief The default pin configuration for the Seeed Studio Round Display
+  ///        connected to the Xiao ESP32-S3
+  static constexpr PinConfig XiaoS3Config = {
+      .sda = GPIO_NUM_5,              ///< I2C data. D4 on the Xiao
+      .scl = GPIO_NUM_6,              ///< I2C clock. D5 on the Xiao
+      .usd_cs = GPIO_NUM_3,           ///< uSD card chip select. D2 on the Xiao
+      .lcd_cs = GPIO_NUM_2,           ///< LCD chip select. D1 on the Xiao
+      .lcd_dc = GPIO_NUM_4,           ///< LCD data/command. D3 on the Xiao
+      .lcd_backlight = GPIO_NUM_43,   ///< LCD backlight. D6/TX on the Xiao
+      .miso = GPIO_NUM_8,             ///< SPI MISO. D9 / MISO on the Xiao
+      .mosi = GPIO_NUM_9,             ///< SPI MOSI. D10 / MOSI on the Xiao
+      .sck = GPIO_NUM_7,              ///< SPI SCK. D8 / SCK on the Xiao
+      .touch_interrupt = GPIO_NUM_44, ///< Touch interrupt. D7/RX on the Xiao
+  };
+
+  /// @brief The default pin configuration for the Seeed Studio Round Display
+  ///        connected to the Qtpy Esp32S3
+  static constexpr PinConfig QtpyS3Config = {
+      .sda = GPIO_NUM_7,              ///< I2C data. SDA on the Qtpy
+      .scl = GPIO_NUM_6,              ///< I2C clock. SCL on the Qtpy
+      .usd_cs = GPIO_NUM_9,           ///< uSD card chip select. A2 on the Qtpy
+      .lcd_cs = GPIO_NUM_17,          ///< LCD chip select. A1 on the Qtpy
+      .lcd_dc = GPIO_NUM_8,           ///< LCD data/command. A3 on the Qtpy
+      .lcd_backlight = GPIO_NUM_5,    ///< LCD backlight. TX on the Qtpy
+      .miso = GPIO_NUM_37,            ///< SPI MISO. MISO on the Qtpy
+      .mosi = GPIO_NUM_35,            ///< SPI MOSI. MOSI on the Qtpy
+      .sck = GPIO_NUM_36,             ///< SPI SCK. SCK on the Qtpy
+      .touch_interrupt = GPIO_NUM_16, ///< Touch interrupt. RX on the Qtpy
+  };
+
+  /// @brief Set the pin configuration for the controller board connected to the
+  ///        display
+  /// @param pin_config The pin configuration for the controller board
+  static void set_pin_config(const PinConfig &pin_config) { pin_config_ = pin_config; }
+
+  /// @brief Access the singleton instance of the SsRoundDisplay class
+  /// @return Reference to the singleton instance of the SsRoundDisplay class
+  /// @note This method must be called after set_pin_config() has been called
+  static SsRoundDisplay &get() {
+    static SsRoundDisplay instance;
+    return instance;
+  }
+
+  SsRoundDisplay(const SsRoundDisplay &) = delete;
+  SsRoundDisplay &operator=(const SsRoundDisplay &) = delete;
+  SsRoundDisplay(SsRoundDisplay &&) = delete;
+  SsRoundDisplay &operator=(SsRoundDisplay &&) = delete;
+
+  /// Get a reference to the internal I2C bus
+  /// \return A reference to the internal I2C bus
+  espp::I2c &internal_i2c();
+
+  /// Get a reference to the interrupts
+  /// \return A reference to the interrupts
+  espp::Interrupt &interrupts();
+
+  /////////////////////////////////////////////////////////////////////////////
+  // Touchpad
+  /////////////////////////////////////////////////////////////////////////////
+
+  /// Initialize the touchpad
+  /// \param callback The touchpad callback
+  /// \return true if the touchpad was successfully initialized, false otherwise
+  /// \warning This method should be called after the display has been
+  ///          initialized if you want the touchpad to be recognized and used
+  ///          with LVGL and its objects.
+  /// \note This will configure the touchpad interrupt pin which will
+  ///       automatically call the touch callback function when the touchpad is
+  ///       touched
+  bool initialize_touch(const touch_callback_t &callback = nullptr);
+
+  /// Get the touchpad input
+  /// \return A shared pointer to the touchpad input
+  std::shared_ptr<TouchpadInput> touchpad_input() const;
+
+  /// Get the most recent touchpad data
+  /// \return The touchpad data
+  TouchpadData touchpad_data() const;
+
+  /// Get the most recent touchpad data
+  /// \param num_touch_points The number of touch points
+  /// \param x The x coordinate
+  /// \param y The y coordinate
+  /// \param btn_state The button state (0 = button released, 1 = button pressed)
+  /// \note This method is a convenience method for integrating with LVGL, the
+  ///       data it returns is identical to the data returned by the
+  ///       touchpad_data() method
+  /// \see touchpad_data()
+  void touchpad_read(uint8_t *num_touch_points, uint16_t *x, uint16_t *y, uint8_t *btn_state);
+
+  /// Convert touchpad data from raw reading to display coordinates
+  /// \param data The touchpad data to convert
+  /// \return The converted touchpad data
+  /// \note Uses the touch_invert_x and touch_invert_y settings to determine
+  ///       if the x and y coordinates should be inverted
+  TouchpadData touchpad_convert(const TouchpadData &data) const;
+
+  /////////////////////////////////////////////////////////////////////////////
+  // Display
+  /////////////////////////////////////////////////////////////////////////////
+
+  /// Initialize the LCD (low level display driver)
+  /// \return true if the LCD was successfully initialized, false otherwise
+  bool initialize_lcd();
+
+  /// Initialize the display (lvgl display driver)
+  /// \param pixel_buffer_size The size of the pixel buffer
+  /// \param task_config The task configuration for the display task
+  /// \param update_period_ms The update period of the display task
+  /// \return true if the display was successfully initialized, false otherwise
+  /// \note This will also allocate two full frame buffers in the SPIRAM
+  bool initialize_display(size_t pixel_buffer_size,
+                          const espp::Task::BaseConfig &task_config = {.name = "Display",
+                                                                       .stack_size_bytes = 4096,
+                                                                       .priority = 10,
+                                                                       .core_id = 0},
+                          int update_period_ms = 16);
+
+  /// Get the width of the LCD in pixels
+  /// \return The width of the LCD in pixels
+  static constexpr size_t lcd_width() { return lcd_width_; }
+
+  /// Get the height of the LCD in pixels
+  /// \return The height of the LCD in pixels
+  static constexpr size_t lcd_height() { return lcd_height_; }
+
+  /// Get the GPIO pin for the LCD data/command signal
+  /// \return The GPIO pin for the LCD data/command signal
+  static constexpr auto get_lcd_dc_gpio() { return pin_config_.lcd_dc; }
+
+  /// Get a shared pointer to the display
+  /// \return A shared pointer to the display
+  std::shared_ptr<Display<Pixel>> display() const;
+
+  /// Set the brightness of the backlight
+  /// \param brightness The brightness of the backlight as a percentage (0 - 100)
+  void brightness(float brightness);
+
+  /// Get the brightness of the backlight
+  /// \return The brightness of the backlight as a percentage (0 - 100)
+  float brightness() const;
+
+  /// Get the VRAM 0 pointer (DMA memory used by LVGL)
+  /// \return The VRAM 0 pointer
+  /// \note This is the memory used by LVGL for rendering
+  /// \note This is null unless initialize_display() has been called
+  Pixel *vram0() const;
+
+  /// Get the VRAM 1 pointer (DMA memory used by LVGL)
+  /// \return The VRAM 1 pointer
+  /// \note This is the memory used by LVGL for rendering
+  /// \note This is null unless initialize_display() has been called
+  Pixel *vram1() const;
+
+  /// Get the frame buffer 0 pointer
+  /// \return The frame buffer 0 pointer
+  /// \note This memory is designed to be used by the application developer and
+  ///       is provided as a convenience. It is not used by the display driver.
+  /// \note This is null unless initialize_display() has been called
+  uint8_t *frame_buffer0() const;
+
+  /// Get the frame buffer 1 pointer
+  /// \return The frame buffer 1 pointer
+  /// \note This memory is designed to be used by the application developer and
+  ///       is provided as a convenience. It is not used by the display driver.
+  /// \note This is null unless initialize_display() has been called
+  uint8_t *frame_buffer1() const;
+
+  /// Write data to the LCD
+  /// \param data The data to write
+  /// \param length The length of the data
+  /// \param user_data User data to pass to the spi transaction callback
+  /// \note This method is designed to be used by the display driver
+  /// \note This method queues the data to be written to the LCD, only blocking
+  ///      if there is an ongoing SPI transaction
+  void write_lcd(const uint8_t *data, size_t length, uint32_t user_data);
+
+  /// Write a frame to the LCD
+  /// \param x The x coordinate
+  /// \param y The y coordinate
+  /// \param width The width of the frame, in pixels
+  /// \param height The height of the frame, in pixels
+  /// \param data The data to write
+  /// \note This method queues the data to be written to the LCD, only blocking
+  ///      if there is an ongoing SPI transaction
+  void write_lcd_frame(const uint16_t x, const uint16_t y, const uint16_t width,
+                       const uint16_t height, uint8_t *data);
+
+  /// Write lines to the LCD
+  /// \param xs The x start coordinate
+  /// \param ys The y start coordinate
+  /// \param xe The x end coordinate
+  /// \param ye The y end coordinate
+  /// \param data The data to write
+  /// \param user_data User data to pass to the spi transaction callback
+  /// \note This method queues the data to be written to the LCD, only blocking
+  ///      if there is an ongoing SPI transaction
+  void write_lcd_lines(int xs, int ys, int xe, int ye, const uint8_t *data, uint32_t user_data);
+
+protected:
+  SsRoundDisplay();
+  void touch_interrupt_handler(const espp::Interrupt::Event &event);
+  bool update_touch();
+  void lcd_wait_lines();
+
+  // common:
+  // internal i2c (touchscreen)
+  static constexpr auto internal_i2c_port = I2C_NUM_0;
+  static constexpr auto internal_i2c_clock_speed = 400 * 1000;
+
+  // LCD
+  static constexpr size_t lcd_width_ = 240;
+  static constexpr size_t lcd_height_ = 240;
+  static constexpr size_t lcd_bytes_per_pixel = 2;
+  static constexpr size_t frame_buffer_size = (((lcd_width_)*lcd_bytes_per_pixel) * lcd_height_);
+  static constexpr int lcd_clock_speed = 20 * 1000 * 1000;
+  static constexpr auto lcd_spi_num = SPI2_HOST;
+  static constexpr gpio_num_t lcd_reset_io = GPIO_NUM_NC;
+  static constexpr bool backlight_value = true;
+  static constexpr bool reset_value = false;
+  static constexpr bool invert_colors = true;
+  static constexpr auto rotation = espp::DisplayRotation::LANDSCAPE;
+  static constexpr bool mirror_x = false;
+  static constexpr bool mirror_y = false;
+  static constexpr bool mirror_portrait = false;
+  static constexpr bool swap_xy = false;
+  static constexpr bool swap_color_order = true;
+
+  // touch
+  static constexpr bool touch_swap_xy = false;
+  static constexpr bool touch_invert_x = false;
+  static constexpr bool touch_invert_y = false;
+  static constexpr auto touch_interrupt_level = espp::Interrupt::ActiveLevel::LOW;
+
+  static PinConfig pin_config_;
+  I2c internal_i2c_;
+  espp::Interrupt::PinConfig touch_interrupt_pin_;
+
+  // we'll only add each interrupt pin if the initialize method is called
+  espp::Interrupt interrupts_{
+      {.interrupts = {},
+       .task_config = {.name = "round display interrupts",
+                       .stack_size_bytes =
+                           CONFIG_SEEED_STUDIO_ROUND_DISPLAY_INTERRUPT_STACK_SIZE}}};
+
+  // touch
+  std::shared_ptr<TouchDriver> touch_;
+  std::shared_ptr<TouchpadInput> touchpad_input_;
+  std::recursive_mutex touchpad_data_mutex_;
+  TouchpadData touchpad_data_;
+  touch_callback_t touch_callback_{nullptr};
+
+  // display
+  std::shared_ptr<Display<Pixel>> display_;
+  /// SPI bus for communication with the LCD
+  spi_bus_config_t lcd_spi_bus_config_;
+  spi_device_interface_config_t lcd_config_;
+  spi_device_handle_t lcd_handle_{nullptr};
+  static constexpr int spi_queue_size = 6;
+  spi_transaction_t trans[spi_queue_size];
+  std::atomic<int> num_queued_trans = 0;
+  uint8_t *frame_buffer0_{nullptr};
+  uint8_t *frame_buffer1_{nullptr};
+
+}; // class SsRoundDisplay
+} // namespace espp
diff --git a/components/seeed-studio-round-display/src/seeed-studio-round-display.cpp b/components/seeed-studio-round-display/src/seeed-studio-round-display.cpp
new file mode 100644
index 000000000..6485eb74d
--- /dev/null
+++ b/components/seeed-studio-round-display/src/seeed-studio-round-display.cpp
@@ -0,0 +1,424 @@
+#include "seeed-studio-round-display.hpp"
+
+using namespace espp;
+
+SsRoundDisplay::PinConfig SsRoundDisplay::pin_config_;
+
+SsRoundDisplay::SsRoundDisplay()
+    : BaseComponent("SsRoundDisplay")
+    , internal_i2c_({.port = internal_i2c_port,
+                     .sda_io_num = pin_config_.sda,
+                     .scl_io_num = pin_config_.scl,
+                     .sda_pullup_en = GPIO_PULLUP_ENABLE,
+                     .scl_pullup_en = GPIO_PULLUP_ENABLE})
+    , touch_interrupt_pin_({
+          .gpio_num = pin_config_.touch_interrupt,
+          .callback =
+              std::bind(&SsRoundDisplay::touch_interrupt_handler, this, std::placeholders::_1),
+          .active_level = touch_interrupt_level,
+          .interrupt_type = espp::Interrupt::Type::FALLING_EDGE,
+      }) {
+  if (pin_config_ == PinConfig{}) {
+    logger_.error("PinConfig not set, you must call set_pin_config() before initializing the "
+                  "SsRoundDisplay! Hardware will not work properly!");
+  }
+}
+
+espp::I2c &SsRoundDisplay::internal_i2c() { return internal_i2c_; }
+
+espp::Interrupt &SsRoundDisplay::interrupts() { return interrupts_; }
+
+////////////////////////
+// Touchpad Functions //
+////////////////////////
+
+bool SsRoundDisplay::initialize_touch(const SsRoundDisplay::touch_callback_t &callback) {
+  if (touchpad_input_) {
+    logger_.warn("Touchpad already initialized, not initializing again!");
+    return false;
+  }
+
+  if (!display_) {
+    logger_.warn("You should call initialize_display() before initialize_touch(), otherwise lvgl "
+                 "will not properly handle the touchpad input!");
+  }
+
+  logger_.info("Initializing Touch Driver");
+  touch_ = std::make_unique<TouchDriver>(TouchDriver::Config{
+      .write = std::bind(&espp::I2c::write, &internal_i2c_, std::placeholders::_1,
+                         std::placeholders::_2, std::placeholders::_3),
+      .read = std::bind(&espp::I2c::read, &internal_i2c_, std::placeholders::_1,
+                        std::placeholders::_2, std::placeholders::_3),
+      .log_level = espp::Logger::Verbosity::WARN});
+
+  touchpad_input_ = std::make_shared<espp::TouchpadInput>(espp::TouchpadInput::Config{
+      .touchpad_read =
+          std::bind(&SsRoundDisplay::touchpad_read, this, std::placeholders::_1,
+                    std::placeholders::_2, std::placeholders::_3, std::placeholders::_4),
+      .swap_xy = touch_swap_xy,
+      .invert_x = touch_invert_x,
+      .invert_y = touch_invert_y,
+      .log_level = espp::Logger::Verbosity::WARN});
+
+  // store the callback
+  touch_callback_ = callback;
+
+  // add the touch interrupt pin
+  interrupts_.add_interrupt(touch_interrupt_pin_);
+
+  return true;
+}
+
+void SsRoundDisplay::touch_interrupt_handler(const espp::Interrupt::Event &event) {
+  if (update_touch()) {
+    if (touch_callback_) {
+      touch_callback_(touchpad_data());
+    }
+  }
+}
+
+bool SsRoundDisplay::update_touch() {
+  // ensure the touch is initialized
+  if (!touch_) {
+    return false;
+  }
+  // get the latest data from the device
+  std::error_code ec;
+  bool new_data = touch_->update(ec);
+  if (ec) {
+    logger_.error("could not update touch driver: {}\n", ec.message());
+    std::lock_guard<std::recursive_mutex> lock(touchpad_data_mutex_);
+    touchpad_data_ = {};
+    return false;
+  }
+  if (!new_data) {
+    return false;
+  }
+  // get the latest data from the touchpad
+  TouchpadData temp_data;
+  touch_->get_touch_point(&temp_data.num_touch_points, &temp_data.x, &temp_data.y);
+  // update the touchpad data
+  std::lock_guard<std::recursive_mutex> lock(touchpad_data_mutex_);
+  touchpad_data_ = temp_data;
+  return true;
+}
+
+std::shared_ptr<espp::TouchpadInput> SsRoundDisplay::touchpad_input() const {
+  return touchpad_input_;
+}
+
+TouchpadData SsRoundDisplay::touchpad_data() const { return touchpad_data_; }
+
+void SsRoundDisplay::touchpad_read(uint8_t *num_touch_points, uint16_t *x, uint16_t *y,
+                                   uint8_t *btn_state) {
+  std::lock_guard<std::recursive_mutex> lock(touchpad_data_mutex_);
+  *num_touch_points = touchpad_data_.num_touch_points;
+  *x = touchpad_data_.x;
+  *y = touchpad_data_.y;
+  *btn_state = touchpad_data_.btn_state;
+}
+
+TouchpadData SsRoundDisplay::touchpad_convert(const TouchpadData &data) const {
+  TouchpadData temp_data;
+  temp_data.num_touch_points = data.num_touch_points;
+  temp_data.x = data.x;
+  temp_data.y = data.y;
+  temp_data.btn_state = data.btn_state;
+  if (temp_data.num_touch_points == 0) {
+    return temp_data;
+  }
+  if (touch_swap_xy) {
+    std::swap(temp_data.x, temp_data.y);
+  }
+  if (touch_invert_x) {
+    temp_data.x = lcd_width_ - (temp_data.x + 1);
+  }
+  if (touch_invert_y) {
+    temp_data.y = lcd_height_ - (temp_data.y + 1);
+  }
+  // get the orientation of the display
+  auto rotation = lv_display_get_rotation(lv_display_get_default());
+  switch (rotation) {
+  case LV_DISPLAY_ROTATION_0:
+    break;
+  case LV_DISPLAY_ROTATION_90:
+    temp_data.y = lcd_height_ - (temp_data.y + 1);
+    std::swap(temp_data.x, temp_data.y);
+    break;
+  case LV_DISPLAY_ROTATION_180:
+    temp_data.x = lcd_width_ - (temp_data.x + 1);
+    temp_data.y = lcd_height_ - (temp_data.y + 1);
+    break;
+  case LV_DISPLAY_ROTATION_270:
+    temp_data.x = lcd_width_ - (temp_data.x + 1);
+    std::swap(temp_data.x, temp_data.y);
+    break;
+  default:
+    break;
+  }
+  return temp_data;
+}
+
+////////////////////////
+// Display Functions //
+////////////////////////
+
+// the user flag for the callbacks does two things:
+// 1. Provides the GPIO level for the data/command pin, and
+// 2. Sets some bits for other signaling (such as LVGL FLUSH)
+static constexpr int FLUSH_BIT = (1 << (int)espp::display_drivers::Flags::FLUSH_BIT);
+static constexpr int DC_LEVEL_BIT = (1 << (int)espp::display_drivers::Flags::DC_LEVEL_BIT);
+
+// This function is called (in irq context!) just before a transmission starts.
+// It will set the D/C line to the value indicated in the user field
+// (DC_LEVEL_BIT).
+static void IRAM_ATTR lcd_spi_pre_transfer_callback(spi_transaction_t *t) {
+  static auto lcd_dc_io = SsRoundDisplay::get_lcd_dc_gpio();
+  uint32_t user_flags = (uint32_t)(t->user);
+  bool dc_level = user_flags & DC_LEVEL_BIT;
+  gpio_set_level(lcd_dc_io, dc_level);
+}
+
+// This function is called (in irq context!) just after a transmission ends. It
+// will indicate to lvgl that the next flush is ready to be done if the
+// FLUSH_BIT is set.
+static void IRAM_ATTR lcd_spi_post_transfer_callback(spi_transaction_t *t) {
+  uint16_t user_flags = (uint32_t)(t->user);
+  bool should_flush = user_flags & FLUSH_BIT;
+  if (should_flush) {
+    lv_display_t *disp = _lv_refr_get_disp_refreshing();
+    lv_display_flush_ready(disp);
+  }
+}
+
+bool SsRoundDisplay::initialize_lcd() {
+  if (lcd_handle_) {
+    logger_.warn("LCD already initialized, not initializing again!");
+    return false;
+  }
+
+  esp_err_t ret;
+
+  memset(&lcd_spi_bus_config_, 0, sizeof(lcd_spi_bus_config_));
+  lcd_spi_bus_config_.mosi_io_num = pin_config_.mosi;
+  lcd_spi_bus_config_.miso_io_num = -1;
+  lcd_spi_bus_config_.sclk_io_num = pin_config_.sck;
+  lcd_spi_bus_config_.quadwp_io_num = -1;
+  lcd_spi_bus_config_.quadhd_io_num = -1;
+  lcd_spi_bus_config_.max_transfer_sz = frame_buffer_size * sizeof(lv_color_t) + 100;
+
+  memset(&lcd_config_, 0, sizeof(lcd_config_));
+  lcd_config_.mode = 0;
+  // lcd_config_.flags = SPI_DEVICE_NO_RETURN_RESULT;
+  lcd_config_.clock_speed_hz = lcd_clock_speed;
+  lcd_config_.input_delay_ns = 0;
+  lcd_config_.spics_io_num = pin_config_.lcd_cs;
+  lcd_config_.queue_size = spi_queue_size;
+  lcd_config_.pre_cb = lcd_spi_pre_transfer_callback;
+  lcd_config_.post_cb = lcd_spi_post_transfer_callback;
+
+  // Initialize the SPI bus
+  ret = spi_bus_initialize(lcd_spi_num, &lcd_spi_bus_config_, SPI_DMA_CH_AUTO);
+  ESP_ERROR_CHECK(ret);
+  // Attach the LCD to the SPI bus
+  ret = spi_bus_add_device(lcd_spi_num, &lcd_config_, &lcd_handle_);
+  ESP_ERROR_CHECK(ret);
+  // initialize the controller
+  using namespace std::placeholders;
+  DisplayDriver::initialize(espp::display_drivers::Config{
+      .lcd_write = std::bind(&SsRoundDisplay::write_lcd, this, _1, _2, _3),
+      .lcd_send_lines = std::bind(&SsRoundDisplay::write_lcd_lines, this, _1, _2, _3, _4, _5, _6),
+      .reset_pin = lcd_reset_io,
+      .data_command_pin = pin_config_.lcd_dc,
+      .reset_value = reset_value,
+      .invert_colors = invert_colors,
+      .swap_color_order = swap_color_order,
+      .swap_xy = swap_xy,
+      .mirror_x = mirror_x,
+      .mirror_y = mirror_y});
+  return true;
+}
+
+bool SsRoundDisplay::initialize_display(size_t pixel_buffer_size,
+                                        const espp::Task::BaseConfig &task_config,
+                                        int update_period_ms) {
+  if (!lcd_handle_) {
+    logger_.error(
+        "LCD not initialized, you must call initialize_lcd() before initialize_display()!");
+    return false;
+  }
+  if (display_) {
+    logger_.warn("Display already initialized, not initializing again!");
+    return false;
+  }
+  // initialize the display / lvgl
+  using namespace std::chrono_literals;
+  display_ = std::make_shared<espp::Display<Pixel>>(espp::Display<Pixel>::AllocatingConfig{
+      .width = lcd_width_,
+      .height = lcd_height_,
+      .pixel_buffer_size = pixel_buffer_size,
+      .flush_callback = DisplayDriver::flush,
+      .rotation_callback = DisplayDriver::rotate,
+      .backlight_pin = pin_config_.lcd_backlight,
+      .backlight_on_value = backlight_value,
+      .task_config = task_config,
+      .update_period = 1ms * update_period_ms,
+      .double_buffered = true,
+      .allocation_flags = MALLOC_CAP_8BIT | MALLOC_CAP_DMA,
+      .rotation = rotation,
+      .software_rotation_enabled = true,
+  });
+
+  frame_buffer0_ =
+      (uint8_t *)heap_caps_malloc(frame_buffer_size, MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM);
+  frame_buffer1_ =
+      (uint8_t *)heap_caps_malloc(frame_buffer_size, MALLOC_CAP_8BIT | MALLOC_CAP_SPIRAM);
+  return true;
+}
+
+std::shared_ptr<espp::Display<SsRoundDisplay::Pixel>> SsRoundDisplay::display() const {
+  return display_;
+}
+
+void IRAM_ATTR SsRoundDisplay::lcd_wait_lines() {
+  spi_transaction_t *rtrans;
+  esp_err_t ret;
+  // logger_.debug("Waiting for {} queued transactions", num_queued_trans);
+  // Wait for all transactions to be done and get back the results.
+  while (num_queued_trans) {
+    ret = spi_device_get_trans_result(lcd_handle_, &rtrans, 10 / portTICK_PERIOD_MS);
+    if (ret != ESP_OK) {
+      logger_.error("Display: Could not get spi trans result: {} '{}'", ret, esp_err_to_name(ret));
+    }
+    num_queued_trans--;
+    // We could inspect rtrans now if we received any info back. The LCD is treated as write-only,
+    // though.
+  }
+}
+
+void IRAM_ATTR SsRoundDisplay::write_lcd(const uint8_t *data, size_t length, uint32_t user_data) {
+  if (length == 0) {
+    return;
+  }
+  lcd_wait_lines();
+  esp_err_t ret;
+  memset(&trans[0], 0, sizeof(spi_transaction_t));
+  trans[0].length = length * 8;
+  trans[0].user = (void *)user_data;
+  // look at the length of the data and use tx_data if it is <= 32 bits
+  if (length <= 4) {
+    // copy the data pointer to trans[0].tx_data
+    memcpy(trans[0].tx_data, data, length);
+    trans[0].flags = SPI_TRANS_USE_TXDATA;
+  } else {
+    trans[0].tx_buffer = data;
+    trans[0].flags = 0;
+  }
+  ret = spi_device_queue_trans(lcd_handle_, &trans[0], 10 / portTICK_PERIOD_MS);
+  if (ret != ESP_OK) {
+    logger_.error("Couldn't queue spi trans for display: {} '{}'", ret, esp_err_to_name(ret));
+  } else {
+    num_queued_trans++;
+  }
+}
+
+void IRAM_ATTR SsRoundDisplay::write_lcd_lines(int xs, int ys, int xe, int ye, const uint8_t *data,
+                                               uint32_t user_data) {
+  // if we haven't waited by now, wait here...
+  lcd_wait_lines();
+  esp_err_t ret;
+  size_t length = (xe - xs + 1) * (ye - ys + 1) * 2;
+  if (length == 0) {
+    logger_.error("lcd_send_lines: Bad length: ({},{}) to ({},{})", xs, ys, xe, ye);
+  }
+  // initialize the spi transactions
+  for (int i = 0; i < 6; i++) {
+    memset(&trans[i], 0, sizeof(spi_transaction_t));
+    if ((i & 1) == 0) {
+      // Even transfers are commands
+      trans[i].length = 8;
+      trans[i].user = (void *)0;
+    } else {
+      // Odd transfers are data
+      trans[i].length = 8 * 4;
+      trans[i].user = (void *)DC_LEVEL_BIT;
+    }
+    trans[i].flags = SPI_TRANS_USE_TXDATA;
+  }
+  trans[0].tx_data[0] = (uint8_t)DisplayDriver::Command::caset;
+  trans[1].tx_data[0] = (xs) >> 8;
+  trans[1].tx_data[1] = (xs)&0xff;
+  trans[1].tx_data[2] = (xe) >> 8;
+  trans[1].tx_data[3] = (xe)&0xff;
+  trans[2].tx_data[0] = (uint8_t)DisplayDriver::Command::raset;
+  trans[3].tx_data[0] = (ys) >> 8;
+  trans[3].tx_data[1] = (ys)&0xff;
+  trans[3].tx_data[2] = (ye) >> 8;
+  trans[3].tx_data[3] = (ye)&0xff;
+  trans[4].tx_data[0] = (uint8_t)DisplayDriver::Command::ramwr;
+  trans[5].tx_buffer = data;
+  trans[5].length = length * 8;
+  // undo SPI_TRANS_USE_TXDATA flag
+  trans[5].flags = SPI_TRANS_DMA_BUFFER_ALIGN_MANUAL;
+  // we need to keep the dc bit set, but also add our flags
+  trans[5].user = (void *)(DC_LEVEL_BIT | user_data);
+  // Queue all transactions.
+  for (int i = 0; i < 6; i++) {
+    ret = spi_device_queue_trans(lcd_handle_, &trans[i], 10 / portTICK_PERIOD_MS);
+    if (ret != ESP_OK) {
+      logger_.error("Couldn't queue spi trans for display: {} '{}'", ret, esp_err_to_name(ret));
+    } else {
+      num_queued_trans++;
+    }
+  }
+  // When we are here, the SPI driver is busy (in the background) getting the
+  // transactions sent. That happens mostly using DMA, so the CPU doesn't have
+  // much to do here. We're not going to wait for the transaction to finish
+  // because we may as well spend the time calculating the next line. When that
+  // is done, we can call lcd_wait_lines, which will wait for the transfers
+  // to be done and check their status.
+}
+
+void SsRoundDisplay::write_lcd_frame(const uint16_t xs, const uint16_t ys, const uint16_t width,
+                                     const uint16_t height, uint8_t *data) {
+  if (data) {
+    // have data, fill the area with the color data
+    lv_area_t area{.x1 = (lv_coord_t)(xs),
+                   .y1 = (lv_coord_t)(ys),
+                   .x2 = (lv_coord_t)(xs + width - 1),
+                   .y2 = (lv_coord_t)(ys + height - 1)};
+    DisplayDriver::fill(nullptr, &area, data);
+  } else {
+    // don't have data, so clear the area (set to 0)
+    DisplayDriver::clear(xs, ys, width, height);
+  }
+}
+
+SsRoundDisplay::Pixel *SsRoundDisplay::vram0() const {
+  if (!display_) {
+    return nullptr;
+  }
+  return display_->vram0();
+}
+
+SsRoundDisplay::Pixel *SsRoundDisplay::vram1() const {
+  if (!display_) {
+    return nullptr;
+  }
+  return display_->vram1();
+}
+
+uint8_t *SsRoundDisplay::frame_buffer0() const { return frame_buffer0_; }
+
+uint8_t *SsRoundDisplay::frame_buffer1() const { return frame_buffer1_; }
+
+void SsRoundDisplay::brightness(float brightness) {
+  brightness = std::clamp(brightness, 0.0f, 100.0f) / 100.0f;
+  // display expects a value between 0 and 1
+  display_->set_brightness(brightness);
+}
+
+float SsRoundDisplay::brightness() const {
+  // display returns a value between 0 and 1
+  return display_->get_brightness() * 100.0f;
+}
diff --git a/components/t-deck/CMakeLists.txt b/components/t-deck/CMakeLists.txt
index 74fe4bc32..7b8090727 100644
--- a/components/t-deck/CMakeLists.txt
+++ b/components/t-deck/CMakeLists.txt
@@ -2,6 +2,6 @@
 idf_component_register(
   INCLUDE_DIRS "include"
   SRC_DIRS "src"
-  REQUIRES driver esp_lcd base_component display display_drivers i2c input_drivers interrupt gt911 task t_keyboard
+  REQUIRES driver base_component display display_drivers i2c input_drivers interrupt gt911 task t_keyboard
   REQUIRED_IDF_TARGETS "esp32s3"
   )
diff --git a/components/t-deck/example/sdkconfig.defaults b/components/t-deck/example/sdkconfig.defaults
index 7a52b0f9d..be4a2ecad 100644
--- a/components/t-deck/example/sdkconfig.defaults
+++ b/components/t-deck/example/sdkconfig.defaults
@@ -33,4 +33,4 @@ CONFIG_LV_DEF_REFR_PERIOD=16
 CONFIG_LV_USE_THEME_DEFAULT=y
 CONFIG_LV_THEME_DEFAULT_DARK=y
 CONFIG_LV_THEME_DEFAULT_GROW=y
-CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=80
+CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=30
diff --git a/components/t-dongle-s3/CMakeLists.txt b/components/t-dongle-s3/CMakeLists.txt
index 90f8e2645..b008a901b 100644
--- a/components/t-dongle-s3/CMakeLists.txt
+++ b/components/t-dongle-s3/CMakeLists.txt
@@ -2,6 +2,6 @@
 idf_component_register(
   INCLUDE_DIRS "include"
   SRC_DIRS "src"
-  REQUIRES driver esp_lcd base_component display display_drivers i2c led_strip task
+  REQUIRES driver base_component display display_drivers i2c led_strip task
   REQUIRED_IDF_TARGETS "esp32s3"
   )
diff --git a/components/wrover-kit/CMakeLists.txt b/components/wrover-kit/CMakeLists.txt
index 1bdea3c45..f1015d027 100644
--- a/components/wrover-kit/CMakeLists.txt
+++ b/components/wrover-kit/CMakeLists.txt
@@ -1,6 +1,6 @@
 idf_component_register(
   INCLUDE_DIRS "include"
   SRC_DIRS "src"
-  REQUIRES driver esp_lcd base_component display display_drivers task
+  REQUIRES driver base_component display display_drivers task
   REQUIRED_IDF_TARGETS "esp32"
   )
diff --git a/doc/Doxyfile b/doc/Doxyfile
index 706c404c9..5db582615 100644
--- a/doc/Doxyfile
+++ b/doc/Doxyfile
@@ -30,9 +30,10 @@ EXAMPLE_PATH += $(PROJECT_PATH)/components/bldc_motor/example/main/bldc_motor_ex
 EXAMPLE_PATH += $(PROJECT_PATH)/components/bldc_haptics/example/main/bldc_haptics_example.cpp
 EXAMPLE_PATH += $(PROJECT_PATH)/components/bm8563/example/main/bm8563_example.cpp
 EXAMPLE_PATH += $(PROJECT_PATH)/components/button/example/main/button_example.cpp
-EXAMPLE_PATH += $(PROJECT_PATH)/components/controller/example/main/controller_example.cpp
+EXAMPLE_PATH += $(PROJECT_PATH)/components/chsc6x/example/main/chsc6x_example.cpp
 EXAMPLE_PATH += $(PROJECT_PATH)/components/cli/example/main/cli_example.cpp
 EXAMPLE_PATH += $(PROJECT_PATH)/components/color/example/main/color_example.cpp
+EXAMPLE_PATH += $(PROJECT_PATH)/components/controller/example/main/controller_example.cpp
 EXAMPLE_PATH += $(PROJECT_PATH)/components/cst816/example/main/cst816_example.cpp
 EXAMPLE_PATH += $(PROJECT_PATH)/components/csv/example/main/csv_example.cpp
 EXAMPLE_PATH += $(PROJECT_PATH)/components/display_drivers/example/main/display_drivers_example.cpp
@@ -68,6 +69,7 @@ EXAMPLE_PATH += $(PROJECT_PATH)/components/qwiicnes/example/main/qwiicnes_exampl
 EXAMPLE_PATH += $(PROJECT_PATH)/components/rmt/example/main/rmt_example.cpp
 EXAMPLE_PATH += $(PROJECT_PATH)/components/rtsp/example/main/rtsp_example.cpp
 EXAMPLE_PATH += $(PROJECT_PATH)/components/serialization/example/main/serialization_example.cpp
+EXAMPLE_PATH += $(PROJECT_PATH)/components/seeed-studio-round-display/example/main/seeed_studio_round_display_example.cpp
 EXAMPLE_PATH += $(PROJECT_PATH)/components/socket/example/main/socket_example.cpp
 EXAMPLE_PATH += $(PROJECT_PATH)/components/st25dv/example/main/st25dv_example.cpp
 EXAMPLE_PATH += $(PROJECT_PATH)/components/state_machine/example/main/hfsm_example.cpp
@@ -115,10 +117,11 @@ INPUT += $(PROJECT_PATH)/components/bldc_motor/include/bldc_types.hpp
 INPUT += $(PROJECT_PATH)/components/bldc_motor/include/sensor_direction.hpp
 INPUT += $(PROJECT_PATH)/components/bm8563/include/bm8563.hpp
 INPUT += $(PROJECT_PATH)/components/button/include/button.hpp
-INPUT += $(PROJECT_PATH)/components/controller/include/controller.hpp
+INPUT += $(PROJECT_PATH)/components/chsc6x/include/chsc6x.hpp
 INPUT += $(PROJECT_PATH)/components/cli/include/cli.hpp
 INPUT += $(PROJECT_PATH)/components/cli/include/line_input.hpp
 INPUT += $(PROJECT_PATH)/components/color/include/color.hpp
+INPUT += $(PROJECT_PATH)/components/controller/include/controller.hpp
 INPUT += $(PROJECT_PATH)/components/cst816/include/cst816.hpp
 INPUT += $(PROJECT_PATH)/components/csv/include/csv.hpp
 INPUT += $(PROJECT_PATH)/components/display/include/display.hpp
@@ -186,6 +189,7 @@ INPUT += $(PROJECT_PATH)/components/rtsp/include/rtp_jpeg_packet.hpp
 INPUT += $(PROJECT_PATH)/components/rtsp/include/jpeg_frame.hpp
 INPUT += $(PROJECT_PATH)/components/rtsp/include/jpeg_header.hpp
 INPUT += $(PROJECT_PATH)/components/serialization/include/serialization.hpp
+INPUT += $(PROJECT_PATH)/components/seeed-studio-round-display/include/seeed-studio-round-display.hpp
 INPUT += $(PROJECT_PATH)/components/socket/include/socket.hpp
 INPUT += $(PROJECT_PATH)/components/socket/include/udp_socket.hpp
 INPUT += $(PROJECT_PATH)/components/socket/include/tcp_socket.hpp
diff --git a/doc/en/index.rst b/doc/en/index.rst
index 6ea379744..fff55bdab 100644
--- a/doc/en/index.rst
+++ b/doc/en/index.rst
@@ -50,6 +50,7 @@ This is the documentation for esp-idf c++ components, ESPP (`espp <https://githu
    rmt
    rtc/index
    rtsp
+   seeed_studio_round_display
    serialization
    state_machine
    t_deck
diff --git a/doc/en/input/chsc6x.rst b/doc/en/input/chsc6x.rst
new file mode 100644
index 000000000..00aab8b02
--- /dev/null
+++ b/doc/en/input/chsc6x.rst
@@ -0,0 +1,17 @@
+CHSC6X Touch Controller
+**********************
+
+The `Chsc6x` class provides an interface to the CHSC6X touch controller.
+
+.. ------------------------------- Example -------------------------------------
+
+.. toctree::
+
+   chsc6x_example
+
+.. ---------------------------- API Reference ----------------------------------
+
+API Reference
+-------------
+
+.. include-build-file:: inc/chsc6x.inc
diff --git a/doc/en/input/chsc6x_example.md b/doc/en/input/chsc6x_example.md
new file mode 100644
index 000000000..d948489bc
--- /dev/null
+++ b/doc/en/input/chsc6x_example.md
@@ -0,0 +1,2 @@
+```{include} ../../../components/chsc6x/example/README.md
+```
diff --git a/doc/en/input/cst816_example.rst b/doc/en/input/cst816_example.md
similarity index 100%
rename from doc/en/input/cst816_example.rst
rename to doc/en/input/cst816_example.md
diff --git a/doc/en/input/index.rst b/doc/en/input/index.rst
index 07b3a7faa..acbeefa9b 100644
--- a/doc/en/input/index.rst
+++ b/doc/en/input/index.rst
@@ -4,6 +4,7 @@ Input APIs
 .. toctree::
     :maxdepth: 1
 
+    chsc6x
     cst816
     ft5x06
     gt911
diff --git a/doc/en/seeed_studio_round_display.rst b/doc/en/seeed_studio_round_display.rst
new file mode 100644
index 000000000..775523a44
--- /dev/null
+++ b/doc/en/seeed_studio_round_display.rst
@@ -0,0 +1,26 @@
+Seeed Studio Round Display
+**************************
+
+SsRoundDisplay
+--------------
+
+The Seeed Studio Round Display is a development board containing a 240x240 round
+display with touchscreen, RTC, battery charger, and uSD card slot. It support
+connection to a QtyPy or a XIAO module.
+
+The `espp::SsRoundDisplay` component provides a singleton hardware abstraction
+for initializing the display and touchscreen components for the Seeed Studio
+Round Display.
+
+.. ------------------------------ Example  -------------------------------------
+
+.. toctree::
+
+   seeed_studio_round_display_example.md
+
+.. ---------------------------- API Reference ----------------------------------
+
+API Reference
+-------------
+
+.. include-build-file:: inc/seeed-studio-round-display.inc
diff --git a/doc/en/seeed_studio_round_display_example.md b/doc/en/seeed_studio_round_display_example.md
new file mode 100644
index 000000000..7e4b2b028
--- /dev/null
+++ b/doc/en/seeed_studio_round_display_example.md
@@ -0,0 +1,2 @@
+```{include} ../../components/seeed-studio-round-display/example/README.md
+```