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 + + 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 + + + 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 +```