diff --git a/components/byte90/CMakeLists.txt b/components/byte90/CMakeLists.txt index 0eed46e30..e1540e7a1 100644 --- a/components/byte90/CMakeLists.txt +++ b/components/byte90/CMakeLists.txt @@ -2,6 +2,6 @@ idf_component_register( INCLUDE_DIRS "include" SRC_DIRS "src" - REQUIRES driver adxl345 base_component display display_drivers i2c interrupt task + REQUIRES driver adxl345 base_component display display_drivers i2c interrupt spi task REQUIRED_IDF_TARGETS "esp32s3" ) diff --git a/components/byte90/example/main/byte90_example.cpp b/components/byte90/example/main/byte90_example.cpp index 7c354c4a6..4e81236bf 100644 --- a/components/byte90/example/main/byte90_example.cpp +++ b/components/byte90/example/main/byte90_example.cpp @@ -1,15 +1,27 @@ +#include #include -#include #include #include "byte90.hpp" using namespace std::chrono_literals; -static constexpr size_t MAX_CIRCLES = 100; -static std::deque circles; +static constexpr size_t MAX_CIRCLES = 10; +struct Circle { + int x{0}; + int y{0}; + int radius{0}; + bool visible{false}; +}; +static std::array circles; +static size_t next_circle_index = 0; +static size_t visible_circle_count = 0; +static lv_obj_t *circle_layer = nullptr; static std::recursive_mutex lvgl_mutex; +static bool initialize_circle_layer(int width, int height); +static void draw_circle_layer(lv_event_t *event); +static void invalidate_circle_area(const Circle &circle); static void draw_circle(int x0, int y0, int radius); static void clear_circles(); @@ -39,6 +51,20 @@ extern "C" void app_main(void) { } // initialize the button, which we'll use to cycle the rotation of the display logger.info("Initializing the button"); + lv_obj_t *bg = nullptr; + lv_obj_t *label = nullptr; + static auto update_layout = [&]() { + int width = byte90.rotated_display_width(); + int height = byte90.rotated_display_height(); + lv_obj_set_size(bg, width, height); + lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); + if (circle_layer) { + lv_obj_set_size(circle_layer, width, height); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_move_foreground(circle_layer); + lv_obj_invalidate(circle_layer); + } + }; auto on_button_pressed = [&](const auto &event) { if (event.active) { // increment the brightness by 10%, looping back to 0% after 100% @@ -50,22 +76,30 @@ extern "C" void app_main(void) { std::lock_guard lock(lvgl_mutex); static auto rotation = LV_DISPLAY_ROTATION_0; rotation = static_cast((static_cast(rotation) + 1) % 4); - lv_display_t *disp = lv_disp_get_default(); + lv_display_t *disp = lv_display_get_default(); lv_disp_set_rotation(disp, rotation); + update_layout(); } }; byte90.initialize_button(on_button_pressed); // set the background color to black - lv_obj_t *bg = lv_obj_create(lv_screen_active()); - lv_obj_set_size(bg, byte90.lcd_width(), byte90.lcd_height()); + bg = lv_obj_create(lv_screen_active()); + lv_obj_set_size(bg, byte90.rotated_display_width(), byte90.rotated_display_height()); lv_obj_set_style_bg_color(bg, lv_color_make(0, 0, 0), 0); + if (!initialize_circle_layer(byte90.rotated_display_width(), byte90.rotated_display_height())) { + logger.error("Failed to initialize circle layer!"); + return; + } // add text in the center of the screen - lv_obj_t *label = lv_label_create(lv_screen_active()); + label = lv_label_create(lv_screen_active()); lv_label_set_text(label, "Drawing circles\nto the screen."); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0); + update_layout(); + + lv_obj_move_foreground(circle_layer); // 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 { @@ -88,17 +122,16 @@ extern "C" void app_main(void) { while (true) { auto start = esp_timer_get_time(); // if there are 10 circles on the screen, clear them - static constexpr int max_circles = 10; - if (circles.size() >= max_circles) { + if (visible_circle_count >= MAX_CIRCLES) { // lock the lvgl mutex std::lock_guard lock(lvgl_mutex); clear_circles(); } else { // draw a circle of circles on the screen (just draw the next circle) - static constexpr int middle_x = byte90.lcd_width() / 2; - static constexpr int middle_y = byte90.lcd_height() / 2; + int middle_x = byte90.rotated_display_width() / 2; + int middle_y = byte90.rotated_display_height() / 2; static constexpr int radius = 30; - float angle = circles.size() * 2.0f * M_PI / max_circles; + float angle = visible_circle_count * 2.0f * M_PI / MAX_CIRCLES; int x = middle_x + radius * cos(angle); int y = middle_y + radius * sin(angle); // lock the lvgl mutex @@ -112,28 +145,100 @@ extern "C" void app_main(void) { //! [byte90 example] } +static bool initialize_circle_layer(int width, int height) { + if (circle_layer) { + return true; + } + circle_layer = lv_obj_create(lv_screen_active()); + if (!circle_layer) { + return false; + } + lv_obj_remove_style_all(circle_layer); + lv_obj_set_size(circle_layer, width, height); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_CLICKABLE); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(circle_layer, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(circle_layer, 0, 0); + lv_obj_set_style_outline_width(circle_layer, 0, 0); + lv_obj_set_style_shadow_width(circle_layer, 0, 0); + lv_obj_add_event_cb(circle_layer, draw_circle_layer, LV_EVENT_DRAW_MAIN, nullptr); + return true; +} + +static void draw_circle_layer(lv_event_t *event) { + if (visible_circle_count == 0) { + return; + } + auto *obj = static_cast(lv_event_get_current_target(event)); + auto *layer = lv_event_get_layer(event); + lv_area_t obj_coords; + lv_obj_get_coords(obj, &obj_coords); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.base.layer = layer; + rect_dsc.radius = LV_RADIUS_CIRCLE; + rect_dsc.bg_opa = LV_OPA_70; + rect_dsc.bg_color = lv_color_make(0, 255, 255); + rect_dsc.border_width = 0; + rect_dsc.outline_width = 0; + rect_dsc.shadow_width = 0; + + for (const auto &circle : circles) { + if (!circle.visible) { + continue; + } + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_draw_rect(layer, &rect_dsc, &coords); + } +} + +static void invalidate_circle_area(const Circle &circle) { + if (!circle_layer || circle.radius <= 0) { + return; + } + + lv_area_t obj_coords; + lv_obj_get_coords(circle_layer, &obj_coords); + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_obj_invalidate_area(circle_layer, &coords); +} + static void draw_circle(int x0, int y0, int radius) { - // if we have too many circles, remove the oldest one - if (circles.size() >= MAX_CIRCLES) { - lv_obj_delete(circles.front()); - circles.pop_front(); + if (!circle_layer) { + return; } - 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); + lv_obj_move_foreground(circle_layer); + Circle previous_circle = circles[next_circle_index]; + circles[next_circle_index] = {.x = x0, .y = y0, .radius = radius, .visible = true}; + next_circle_index = (next_circle_index + 1) % circles.size(); + if (visible_circle_count < circles.size()) { + visible_circle_count++; + } + if (previous_circle.visible) { + invalidate_circle_area(previous_circle); + } + invalidate_circle_area(circles[(next_circle_index + circles.size() - 1) % circles.size()]); } static void clear_circles() { - // remove the circles from lvgl - for (auto circle : circles) { - lv_obj_delete(circle); + for (auto &circle : circles) { + if (circle.visible) { + invalidate_circle_area(circle); + } + circle.visible = false; } - // clear the vector - circles.clear(); + next_circle_index = 0; + visible_circle_count = 0; } diff --git a/components/byte90/idf_component.yml b/components/byte90/idf_component.yml index 0d928476f..9397a3369 100644 --- a/components/byte90/idf_component.yml +++ b/components/byte90/idf_component.yml @@ -23,6 +23,7 @@ dependencies: espp/display_drivers: '>=1.0' espp/i2c: '>=1.0' espp/interrupt: '>=1.0' + espp/spi: '>=1.0' espp/task: '>=1.0' targets: - esp32s3 diff --git a/components/byte90/include/byte90.hpp b/components/byte90/include/byte90.hpp index 37a2ba4ff..44075307a 100644 --- a/components/byte90/include/byte90.hpp +++ b/components/byte90/include/byte90.hpp @@ -5,7 +5,6 @@ #include #include -#include #include #include @@ -13,6 +12,7 @@ #include "base_component.hpp" #include "i2c.hpp" #include "interrupt.hpp" +#include "spi.hpp" #include "ssd1351.hpp" namespace espp { @@ -154,6 +154,14 @@ class Byte90 : public BaseComponent { /// \return The height of the LCD in pixels static constexpr size_t lcd_height() { return lcd_height_; } + /// Get the display width in pixels, according to the current orientation + /// \return The display width in pixels, according to the current orientation + size_t rotated_display_width() const; + + /// Get the display height in pixels, according to the current orientation + /// \return The display height in pixels, according to the current orientation + size_t rotated_display_height() const; + /// 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 lcd_dc_io; } @@ -196,15 +204,6 @@ class Byte90 : public BaseComponent { /// \note This is null unless initialize_display() has been called uint8_t *frame_buffer1() const; - /// Write command and optional parameters to the LCD - /// \param command The command to write - /// \param parameters The command parameters to write - /// \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_command(uint8_t command, std::span parameters, uint32_t user_data); - /// Write a frame to the LCD /// \param x The x coordinate /// \param y The y coordinate @@ -216,21 +215,8 @@ class Byte90 : public BaseComponent { 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: Byte90(); - bool init_spi_bus(); - void lcd_wait_lines(); // common: // internal i2c (adxl345) @@ -275,9 +261,6 @@ class Byte90 : public BaseComponent { .sda_pullup_en = GPIO_PULLUP_ENABLE, .scl_pullup_en = GPIO_PULLUP_ENABLE}}; - // spi bus shared between sdcard and lcd - std::atomic spi_bus_initialized_{false}; - espp::Interrupt::PinConfig button_interrupt_pin_{ .gpio_num = button_io, .callback = @@ -346,12 +329,10 @@ class Byte90 : public BaseComponent { // display std::shared_ptr> display_; - /// SPI bus for communication with the LCD - spi_device_interface_config_t lcd_config_; - spi_device_handle_t lcd_handle_{nullptr}; + std::unique_ptr display_driver_; static constexpr int spi_queue_size = 6; - spi_transaction_t trans[spi_queue_size]; - std::atomic num_queued_trans = 0; + std::unique_ptr lcd_spi_; + std::unique_ptr lcd_; uint8_t *frame_buffer0_{nullptr}; uint8_t *frame_buffer1_{nullptr}; }; // class Byte90 diff --git a/components/byte90/src/byte90.cpp b/components/byte90/src/byte90.cpp index 6e3cce203..e6c3d8200 100644 --- a/components/byte90/src/byte90.cpp +++ b/components/byte90/src/byte90.cpp @@ -4,32 +4,3 @@ using namespace espp; Byte90::Byte90() : BaseComponent("Byte90") {} - -//////////////////////// -// SPI Functions // -//////////////////////// - -bool Byte90::init_spi_bus() { - if (spi_bus_initialized_) { - return true; - } - - spi_bus_config_t bus_cfg; - memset(&bus_cfg, 0, sizeof(bus_cfg)); - bus_cfg.mosi_io_num = spi_mosi_io; - bus_cfg.miso_io_num = -1; - bus_cfg.sclk_io_num = spi_sclk_io; - bus_cfg.quadwp_io_num = -1; - bus_cfg.quadhd_io_num = -1; - bus_cfg.max_transfer_sz = SPI_MAX_TRANSFER_BYTES; - auto ret = spi_bus_initialize(spi_num, &bus_cfg, SPI_DMA_CH_AUTO); - if (ret != ESP_OK) { - logger_.error("Failed to initialize bus."); - return false; - } - - logger_.info("SPI bus initialized"); - spi_bus_initialized_ = true; - - return true; -} diff --git a/components/byte90/src/display.cpp b/components/byte90/src/display.cpp index 7743bbcbd..54d256d12 100644 --- a/components/byte90/src/display.cpp +++ b/components/byte90/src/display.cpp @@ -12,80 +12,75 @@ using namespace espp; 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). -// -// cppcheck-suppress constParameterCallback -static void IRAM_ATTR lcd_spi_pre_transfer_callback(spi_transaction_t *t) { - static auto lcd_dc_io = Byte90::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. -// -// cppcheck-suppress constParameterCallback -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_display_get_default(); - lv_display_flush_ready(disp); - } +static void IRAM_ATTR lcd_spi_flush_ready(uint32_t) { + lv_display_t *disp = lv_display_get_default(); + lv_display_flush_ready(disp); } bool Byte90::initialize_lcd() { - if (lcd_handle_) { + if (lcd_ || lcd_spi_) { logger_.warn("LCD already initialized, not initializing again!"); return false; } - if (!init_spi_bus()) { - logger_.error("Failed to initialize SPI bus for LCD."); - return false; - } - logger_.info("Initializing LCD"); - esp_err_t ret; - memset(&lcd_config_, 0, sizeof(lcd_config_)); - lcd_config_.mode = 0; - lcd_config_.flags = SPI_DEVICE_NO_DUMMY | SPI_DEVICE_3WIRE; - lcd_config_.clock_speed_hz = lcd_clock_speed; - lcd_config_.input_delay_ns = 0; - lcd_config_.spics_io_num = lcd_cs_io; - 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; - // lcd_config_.cs_ena_pretrans = 16; - // lcd_config_.cs_ena_posttrans = 16; + lcd_spi_ = std::make_unique(Spi::Config{ + .host = spi_num, + .sclk_io_num = spi_sclk_io, + .mosi_io_num = spi_mosi_io, + .miso_io_num = GPIO_NUM_NC, + .max_transfer_sz = SPI_MAX_TRANSFER_BYTES, + .log_level = get_log_level(), + }); + lcd_ = std::make_unique(SpiPanelIo::Config{ + .spi = lcd_spi_.get(), + .device_config = + { + .mode = 0, + .clock_speed_hz = lcd_clock_speed, + .input_delay_ns = 0, + .cs_io_num = lcd_cs_io, + .queue_size = spi_queue_size, + .flags = SPI_DEVICE_NO_DUMMY | SPI_DEVICE_3WIRE, + }, + .data_command_io = lcd_dc_io, + .data_command_bit_mask = DC_LEVEL_BIT, + .post_transaction_callback_bit_mask = FLUSH_BIT, + .post_transaction_callback = lcd_spi_flush_ready, + .log_level = get_log_level(), + }); + if (!lcd_->initialized()) { + lcd_.reset(); + lcd_spi_.reset(); + return false; + } - // Attach the LCD to the SPI bus - ret = spi_bus_add_device(spi_num, &lcd_config_, &lcd_handle_); - ESP_ERROR_CHECK(ret); - // initialize the controller - using namespace std::placeholders; - DisplayDriver::initialize(espp::display_drivers::Config{ - .write_command = std::bind(&Byte90::write_command, this, _1, _2, _3), - .lcd_send_lines = std::bind(&Byte90::write_lcd_lines, this, _1, _2, _3, _4, _5, _6), - .reset_pin = lcd_reset_io, - .data_command_pin = lcd_dc_io, - .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, - .mirror_portrait = mirror_portrait}); + display_driver_ = std::make_unique( + espp::display_drivers::Config{.panel_io = lcd_.get(), + .write_command = nullptr, + .read_command = nullptr, + .lcd_send_lines = nullptr, + .reset_pin = lcd_reset_io, + .data_command_pin = lcd_dc_io, + .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, + .mirror_portrait = mirror_portrait}); + if (!display_driver_ || !display_driver_->initialize()) { + display_driver_.reset(); + lcd_.reset(); + lcd_spi_.reset(); + return false; + } return true; } bool Byte90::initialize_display(size_t pixel_buffer_size) { - if (!lcd_handle_) { + if (!lcd_) { logger_.error( "LCD not initialized, you must call initialize_lcd() before initialize_display()!"); return false; @@ -100,13 +95,31 @@ bool Byte90::initialize_display(size_t pixel_buffer_size) { // initialize the display / lvgl using namespace std::chrono_literals; display_ = std::make_shared>( - Display::LvglConfig{.width = lcd_width_, - .height = lcd_height_, - .flush_callback = DisplayDriver::flush, - .rotation_callback = DisplayDriver::rotate, - .rotation = rotation}, - Display::OledConfig{.set_brightness_callback = DisplayDriver::set_brightness, - .get_brightness_callback = DisplayDriver::get_brightness}, + Display::LvglConfig{ + .width = lcd_width_, + .height = lcd_height_, + .flush_callback = + [this](lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { + if (display_driver_) { + display_driver_->flush(disp, area, color_map); + } + }, + .rotation_callback = + [this](const DisplayRotation &new_rotation) { + if (display_driver_) { + display_driver_->set_rotation(new_rotation); + } + }, + .rotation = rotation}, + Display::OledConfig{ + .set_brightness_callback = + [this](float brightness) { + if (display_driver_) { + display_driver_->set_brightness(brightness); + } + }, + .get_brightness_callback = + [this]() { return display_driver_ ? display_driver_->get_brightness() : 0.0f; }}, Display::DynamicMemoryConfig{ .pixel_buffer_size = pixel_buffer_size, .double_buffered = true, @@ -122,138 +135,21 @@ bool Byte90::initialize_display(size_t pixel_buffer_size) { std::shared_ptr> Byte90::display() const { return display_; } -void IRAM_ATTR Byte90::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 Byte90::write_command(uint8_t command, std::span parameters, - uint32_t user_data) { - lcd_wait_lines(); - memset(&trans[0], 0, sizeof(spi_transaction_t)); - memset(&trans[1], 0, sizeof(spi_transaction_t)); - - trans[0].length = 8; - trans[0].user = reinterpret_cast(user_data); - trans[0].flags = SPI_TRANS_USE_TXDATA; - trans[0].tx_data[0] = command; - - trans[1].length = parameters.size() * 8; - if (parameters.size() <= 4) { - // copy the data pointer to trans[0].tx_data - memcpy(trans[1].tx_data, parameters.data(), parameters.size()); - trans[1].flags = SPI_TRANS_USE_TXDATA; - } else if (!parameters.empty()) { - trans[1].tx_buffer = parameters.data(); - trans[1].flags = 0; - } - trans[1].user = reinterpret_cast( - user_data | (1 << static_cast(display_drivers::Flags::DC_LEVEL_BIT))); - - esp_err_t ret = spi_device_queue_trans(lcd_handle_, &trans[0], 10 / portTICK_PERIOD_MS); - if (ret != ESP_OK) { - logger_.error("Couldn't queue spi command trans for display: {} '{}'", ret, - esp_err_to_name(ret)); - } else { - ++num_queued_trans; - if (!parameters.empty()) { - ret = spi_device_queue_trans(lcd_handle_, &trans[1], 10 / portTICK_PERIOD_MS); - if (ret != ESP_OK) { - logger_.error("Couldn't queue spi data trans for display: {} '{}'", ret, - esp_err_to_name(ret)); - } else { - ++num_queued_trans; - } - } - } -} - -void IRAM_ATTR Byte90::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 * 2; - trans[i].user = (void *)DC_LEVEL_BIT; - } - trans[i].flags = SPI_TRANS_USE_TXDATA; - } - - lv_display_t *disp = lv_disp_get_default(); - auto rotation = lv_disp_get_rotation(disp); - if (rotation == lv_display_rotation_t::LV_DISPLAY_ROTATION_90 || - rotation == lv_display_rotation_t::LV_DISPLAY_ROTATION_270) { - // swap x and y coordinates for 90/270 degree rotation - std::swap(xs, ys); - std::swap(xe, ye); - } - - trans[0].tx_data[0] = (uint8_t)DisplayDriver::Command::caset; - trans[1].tx_data[0] = (xs)&0xff; - trans[1].tx_data[1] = (xe)&0xff; - trans[2].tx_data[0] = (uint8_t)DisplayDriver::Command::raset; - trans[3].tx_data[0] = (ys)&0xff; - trans[3].tx_data[1] = (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 Byte90::write_lcd_frame(const uint16_t xs, const uint16_t ys, const uint16_t width, const uint16_t height, uint8_t *data) { + if (!display_driver_) { + return; + } 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); + display_driver_->fill(nullptr, &area, data); } else { // don't have data, so clear the area (set to 0) - DisplayDriver::clear(xs, ys, width, height); + display_driver_->clear(xs, ys, width, height); } } @@ -285,3 +181,31 @@ float Byte90::brightness() const { // display returns a value between 0 and 1 return display_->get_brightness() * 100.0f; } + +size_t Byte90::rotated_display_width() const { + auto *display = lv_display_get_default(); + auto rotation = display ? lv_display_get_rotation(display) : LV_DISPLAY_ROTATION_0; + switch (rotation) { + case LV_DISPLAY_ROTATION_90: + case LV_DISPLAY_ROTATION_270: + return lcd_height_; + case LV_DISPLAY_ROTATION_0: + case LV_DISPLAY_ROTATION_180: + default: + return lcd_width_; + } +} + +size_t Byte90::rotated_display_height() const { + auto *display = lv_display_get_default(); + auto rotation = display ? lv_display_get_rotation(display) : LV_DISPLAY_ROTATION_0; + switch (rotation) { + case LV_DISPLAY_ROTATION_90: + case LV_DISPLAY_ROTATION_270: + return lcd_width_; + case LV_DISPLAY_ROTATION_0: + case LV_DISPLAY_ROTATION_180: + default: + return lcd_height_; + } +} diff --git a/components/display_drivers/CMakeLists.txt b/components/display_drivers/CMakeLists.txt index d3453d4fc..943bff222 100644 --- a/components/display_drivers/CMakeLists.txt +++ b/components/display_drivers/CMakeLists.txt @@ -1,4 +1,5 @@ idf_component_register( INCLUDE_DIRS "include" - REQUIRES display driver esp_lcd led + SRC_DIRS "src" + REQUIRES display driver esp_lcd led spi ) diff --git a/components/display_drivers/README.md b/components/display_drivers/README.md index ee38e0c02..ad22b4a54 100644 --- a/components/display_drivers/README.md +++ b/components/display_drivers/README.md @@ -2,10 +2,29 @@ [![Badge](https://components.espressif.com/components/espp/display_drivers/badge.svg)](https://components.espressif.com/components/espp/display_drivers) -This component contains a few different display drivers, corresponding to common -display drivers on espressif development boards. These display drivers are +This component contains a set of reusable display-controller implementations designed to be used with the `display` component. +The drivers now follow the shared object-style +`display_drivers::Controller` / `display_drivers::MipiDbiDisplayDriver` +architecture, with controller state owned per instance instead of hidden behind +static globals. + +It also contains the `SpiPanelIo` transport helper in +`include/spi_panel_io.hpp` (also aliased as `SpiCommandData`) for SPI-connected +command/data panels built on top of the shared `spi` component. + +## Included Drivers + +- `Gc9a01` +- `Ili9341` +- `Ili9881` +- `Sh8601` +- `Ssd1351` +- `St7123` +- `St7789` +- `St7796` + ## Example The [example](./example) is designed to show how the `display_drivers` component diff --git a/components/display_drivers/example/CMakeLists.txt b/components/display_drivers/example/CMakeLists.txt index 14acf7a4f..8a6e11820 100644 --- a/components/display_drivers/example/CMakeLists.txt +++ b/components/display_drivers/example/CMakeLists.txt @@ -12,7 +12,7 @@ set(EXTRA_COMPONENT_DIRS set( COMPONENTS - "main esptool_py driver task format display display_drivers" + "main esptool_py driver task format display display_drivers spi" CACHE STRING "List of components to include" ) diff --git a/components/display_drivers/example/README.md b/components/display_drivers/example/README.md index 86b938c5f..91f510d4f 100644 --- a/components/display_drivers/example/README.md +++ b/components/display_drivers/example/README.md @@ -2,7 +2,9 @@ This example is designed to show how the `display_drivers` component can be used to drive various different displays with LVGL and a simple GUI (that is -contained within the example: `main/gui.hpp`). +contained within the example: `main/gui.hpp`). It demonstrates both the +object-style display-controller API and the shared SPI transport helpers used by +the BSPs in this repository. ## Demo @@ -36,7 +38,7 @@ idf.py menuconfig ``` When configuring the project, select the `Display Drivers Example Configuration` -value that matches the board you've selected (must be one of the 4 boards +value that matches the board you've selected (must be one of the boards mentioned above.) ### Build and Flash @@ -56,10 +58,10 @@ See the Getting Started Guide for full steps to configure and use ESP-IDF to bui ## Example Breakdown The example has the following functionality: -* SPI pre and post transfer callbacks for handling the data/command (DC_LEVEL) - GPIO for the screens and the lvgl flush flag management -* `lcd_write` for polling (blocking) transmit example -* `lcd_send_lines` and `lcd_wait_lines` for queued (non-blocking) transmit example +* Uses the shared `espp::Spi` wrapper for the display bus on every supported board +* Uses `espp::SpiPanelIo` for standard command/data SPI panels +* Keeps the T-Encoder Pro quad-SPI path on `espp::Spi::Device`, since that panel needs custom + multi-line transactions +* Exercises the object-style controller classes from the `display_drivers` component * `Gui` class (contained in `main/gui.hpp`) which encapsulates some very basic LVGL components into an object that manages gui update task and synchronization. - diff --git a/components/display_drivers/example/main/CMakeLists.txt b/components/display_drivers/example/main/CMakeLists.txt index a941e22ba..9381e4e06 100644 --- a/components/display_drivers/example/main/CMakeLists.txt +++ b/components/display_drivers/example/main/CMakeLists.txt @@ -1,2 +1,3 @@ idf_component_register(SRC_DIRS "." - INCLUDE_DIRS ".") + INCLUDE_DIRS "." + REQUIRES display display_drivers spi) diff --git a/components/display_drivers/example/main/display_drivers_example.cpp b/components/display_drivers/example/main/display_drivers_example.cpp index f948fa509..843979d3e 100644 --- a/components/display_drivers/example/main/display_drivers_example.cpp +++ b/components/display_drivers/example/main/display_drivers_example.cpp @@ -1,16 +1,14 @@ +#include #include +#include #include #include -#include #include -#include #include "display.hpp" +#include "spi.hpp" -// default, most displays use 16-bit coordinates -#define DISPLAY_COORDINATES_16BIT 1 -#define DISPLAY_COORDINATES_8BIT 0 #define DISPLAY_IS_OLED 0 // most displays are not OLEDs #if CONFIG_HARDWARE_WROVER_KIT @@ -35,13 +33,13 @@ using Display = espp::Display; using DisplayDriver = espp::Gc9a01; #elif CONFIG_T_ENCODER_PRO #include "sh8601.hpp" +#undef DISPLAY_IS_OLED #define DISPLAY_IS_OLED 1 // T-Encoder Pro uses an OLED display using Display = espp::Display; using DisplayDriver = espp::Sh8601; #elif CONFIG_HARDWARE_BYTE90 #include "ssd1351.hpp" -#define DISPLAY_COORDINATES_8BIT 1 // ssd1351 only supports 8-bit coordinates -#define DISPLAY_COORDINATES_16BIT 0 +#undef DISPLAY_IS_OLED #define DISPLAY_IS_OLED 1 // Byte90 uses an OLED display static constexpr int DC_PIN_NUM = 43; using Display = espp::Display; @@ -55,10 +53,16 @@ using DisplayDriver = espp::Ssd1351; using namespace std::chrono_literals; -static spi_device_handle_t spi; -static const int spi_queue_size = 7; +static std::unique_ptr spi_bus; +static constexpr auto spi_num = SPI2_HOST; +#ifdef CONFIG_DISPLAY_QUAD_SPI +static std::shared_ptr spi_device; +static constexpr int spi_queue_size = 7; static size_t num_queued_trans = 0; -static auto spi_num = SPI2_HOST; +#else +static std::unique_ptr panel_io; +static constexpr int spi_queue_size = 6; +#endif // the user flag for the callbacks does two things: // 1. Provides the GPIO level for the data/command pin, and @@ -66,108 +70,80 @@ static auto spi_num = SPI2_HOST; 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); -//! [pre_transfer_callback example] -// 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). -// Except for the T-Encoder Pro, which does not have a D/C line. +static void IRAM_ATTR lcd_spi_flush_ready(uint32_t) { + lv_display_t *disp = lv_display_get_default(); + lv_display_flush_ready(disp); +} + +#ifdef CONFIG_DISPLAY_QUAD_SPI #ifndef CONFIG_T_ENCODER_PRO // cppcheck-suppress constParameterCallback static void IRAM_ATTR lcd_spi_pre_transfer_callback(spi_transaction_t *t) { - uint32_t user_flags = (uint32_t)(t->user); - bool dc_level = user_flags & DC_LEVEL_BIT; + auto user_flags = static_cast(reinterpret_cast(t->user)); + bool dc_level = (user_flags & DC_LEVEL_BIT) != 0; gpio_set_level((gpio_num_t)DC_PIN_NUM, dc_level); } #endif -//! [pre_transfer_callback example] -//! [post_transfer_callback example] -// 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. -// // cppcheck-suppress constParameterCallback 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_display_get_default(); - lv_display_flush_ready(disp); + auto user_flags = static_cast(reinterpret_cast(t->user)); + if ((user_flags & FLUSH_BIT) != 0) { + lcd_spi_flush_ready(user_flags); } } -//! [post_transfer_callback example] -//! [polling_transmit example] -#ifdef CONFIG_DISPLAY_QUAD_SPI extern "C" void IRAM_ATTR write_command(uint8_t command, std::span parameters, uint32_t user_data) { static spi_transaction_t t = {}; + if (!spi_device) { + return; + } + std::memset(&t, 0, sizeof(t)); t.cmd = static_cast(DisplayDriver::TransferMode::SINGLE_LINE); t.addr = static_cast(command) << 8; t.flags = SPI_TRANS_MULTILINE_CMD | SPI_TRANS_MULTILINE_ADDR; t.length = parameters.size() * 8; - t.user = reinterpret_cast(user_data); + t.user = reinterpret_cast(static_cast(user_data)); if (!parameters.empty() && parameters.size() <= 4) { - memcpy(t.tx_data, parameters.data(), parameters.size()); + std::memcpy(t.tx_data, parameters.data(), parameters.size()); t.flags |= SPI_TRANS_USE_TXDATA; } else if (!parameters.empty()) { t.tx_buffer = parameters.data(); } - auto ret = spi_device_acquire_bus(spi, portMAX_DELAY); - if (ret != ESP_OK) { - fmt::print("Failed to acquire bus: {}\n", esp_err_to_name(ret)); + std::error_code ec; + auto lock = spi_device->acquire_bus(portMAX_DELAY, ec); + if (ec || !lock) { + fmt::print("Failed to acquire bus: {}\n", ec.message()); return; } - ret = spi_device_polling_transmit(spi, &t); - if (ret != ESP_OK) { - fmt::print("Failed to send command: {}\n", esp_err_to_name(ret)); - } - spi_device_release_bus(spi); -} -#else -extern "C" void IRAM_ATTR write_command(uint8_t command, std::span parameters, - uint32_t user_data) { - static spi_transaction_t t = {}; - t.length = 8; - t.tx_buffer = &command; - t.user = reinterpret_cast(user_data); - if (!parameters.empty()) { - spi_device_polling_transmit(spi, &t); - t.length = parameters.size() * 8; - t.tx_buffer = parameters.data(); - t.user = reinterpret_cast( - user_data | (1 << static_cast(espp::display_drivers::Flags::DC_LEVEL_BIT))); + if (!spi_device->polling_transmit(t, ec)) { + fmt::print("Failed to send command: {}\n", ec.message()); } - spi_device_polling_transmit(spi, &t); } -#endif -//! [polling_transmit example] -//! [queued_transmit example] static void lcd_wait_lines() { - spi_transaction_t *rtrans; - esp_err_t ret; - // Wait for all transactions to be done and get back the results. + if (!spi_device) { + return; + } + spi_transaction_t *rtrans = nullptr; while (num_queued_trans) { - // fmt::print("Waiting for {} lines\n", num_queued_trans); - ret = spi_device_get_trans_result(spi, &rtrans, portMAX_DELAY); - if (ret != ESP_OK) { - fmt::print("Could not get trans result: {} '{}'\n", ret, esp_err_to_name(ret)); + std::error_code ec; + if (!spi_device->get_transaction_result(&rtrans, portMAX_DELAY, ec)) { + fmt::print("Could not get trans result: {}\n", ec.message()); + return; } num_queued_trans--; - // We could inspect rtrans now if we received any info back. The LCD is treated as write-only, - // though. } } -#ifdef CONFIG_DISPLAY_QUAD_SPI void IRAM_ATTR lcd_send_lines(const int xStart, const int yStart, const int xEnd, const int yEnd, const uint8_t *data, const uint32_t user_data) { - if (data == nullptr) { + if (!spi_device || data == nullptr) { return; } @@ -241,126 +217,33 @@ void IRAM_ATTR lcd_send_lines(const int xStart, const int yStart, const int xEnd SPI_TRANS_VARIABLE_CMD | SPI_TRANS_VARIABLE_ADDR | SPI_TRANS_VARIABLE_DUMMY; } + transactions[index].user = nullptr; remaining -= transfer_size; index++; } // Set the flush bit on the last transaction, index - 1 as index is already incremented - transactions[index - 1].user = reinterpret_cast(user_data); + transactions[index - 1].user = reinterpret_cast(static_cast(user_data)); // Have the final pixel transaction stop asserting the CS line transactions[index - 1].flags &= ~SPI_TRANS_CS_KEEP_ACTIVE; // Acquire the SPI bus, required for the SPI_TRANS_CS_KEEP_ACTIVE flag - auto ret = spi_device_acquire_bus(spi, portMAX_DELAY); - if (ret != ESP_OK) { - fmt::print("Couldn't acquire bus: {}", esp_err_to_name(ret)); + std::error_code ec; + auto lock = spi_device->acquire_bus(portMAX_DELAY, ec); + if (ec || !lock) { + fmt::print("Couldn't acquire bus: {}\n", ec.message()); return; } - // Queue all used transactions for (int i = 0; i < index; i++) { - esp_err_t ret = spi_device_queue_trans(spi, &transactions[i], portMAX_DELAY); - if (ret != ESP_OK) { - fmt::print("Couldn't queue transaction: {}", esp_err_to_name(ret)); - } else { - num_queued_trans++; - } - } - spi_device_release_bus(spi); - // 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 send_line_finish, which will wait for the transfers - // to be done and check their status. -} -#else -void IRAM_ATTR lcd_send_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; - // Transaction descriptors. Declared static so they're not allocated on the stack; we need this - // memory even when this function is finished because the SPI driver needs access to it even while - // we're already calculating the next line. - static spi_transaction_t trans[6]; - // In theory, it's better to initialize trans and data only once and hang on to the initialized - // variables. We allocate them on the stack, so we need to re-init them each call. - 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 -#if DISPLAY_COORDINATES_8BIT - trans[i].length = 8 * 2; // byte90 only has 2 byte per pixel address (1 byte for each axis) -#else // other displays support 16-bit coordinates - trans[i].length = 8 * 4; -#endif - trans[i].user = (void *)DC_LEVEL_BIT; - } - trans[i].flags = SPI_TRANS_USE_TXDATA; - } - -#ifdef CONFIG_HARDWARE_BYTE90 - lv_display_t *disp = lv_disp_get_default(); - auto rotation = lv_disp_get_rotation(disp); - if (rotation == lv_display_rotation_t::LV_DISPLAY_ROTATION_90 || - rotation == lv_display_rotation_t::LV_DISPLAY_ROTATION_270) { - // swap x and y coordinates for 90/270 degree rotation - std::swap(xs, ys); - std::swap(xe, ye); - } -#endif - - size_t length = (xe - xs + 1) * (ye - ys + 1) * 2; - trans[0].tx_data[0] = (uint8_t)DisplayDriver::Command::caset; -#if DISPLAY_COORDINATES_8BIT - trans[1].tx_data[0] = (xs)&0xff; - trans[1].tx_data[1] = (xe)&0xff; -#else // other displays support 16-bit coordinates - 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; -#endif - trans[2].tx_data[0] = (uint8_t)DisplayDriver::Command::raset; -#if DISPLAY_COORDINATES_8BIT - trans[3].tx_data[0] = (ys)&0xff; - trans[3].tx_data[1] = (ye)&0xff; -#else // other displays support 16-bit coordinates - 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; -#endif - 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 = 0; - // 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(spi, &trans[i], portMAX_DELAY); - if (ret != ESP_OK) { - fmt::print("Couldn't queue trans: {} '{}'\n", ret, esp_err_to_name(ret)); + if (!spi_device->queue_transaction(transactions[i], portMAX_DELAY, ec)) { + fmt::print("Couldn't queue transaction: {}\n", ec.message()); } 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 send_line_finish, which will wait for the transfers - // to be done and check their status. } -//! [queued_transmit example] #endif extern "C" void app_main(void) { @@ -527,50 +410,86 @@ extern "C" void app_main(void) { gpio_set_level(enable, 1); #endif //! [display_drivers example] - // create the spi host - spi_bus_config_t buscfg; - memset(&buscfg, 0, sizeof(buscfg)); - buscfg.mosi_io_num = mosi; + spi_bus = std::make_unique(espp::Spi::Config{ + .host = spi_num, + .sclk_io_num = sclk, + .mosi_io_num = mosi, #ifdef CONFIG_DISPLAY_QUAD_SPI - buscfg.miso_io_num = miso; - buscfg.data2_io_num = data2; - buscfg.data3_io_num = data3; + .miso_io_num = miso, + .quadwp_io_num = data2, + .quadhd_io_num = data3, #else - buscfg.miso_io_num = -1; - buscfg.quadwp_io_num = -1; - buscfg.quadhd_io_num = -1; + .miso_io_num = GPIO_NUM_NC, #endif - buscfg.sclk_io_num = sclk; - buscfg.max_transfer_sz = SPI_MAX_TRANSFER_BYTES; - // create the spi device - spi_device_interface_config_t devcfg; - memset(&devcfg, 0, sizeof(devcfg)); - devcfg.mode = 0; - devcfg.clock_speed_hz = clock_speed; - devcfg.input_delay_ns = 0; - devcfg.spics_io_num = spics; - devcfg.queue_size = spi_queue_size; + .max_transfer_sz = SPI_MAX_TRANSFER_BYTES, + }); + if (!spi_bus->initialized()) { + fmt::print("Failed to initialize SPI bus\n"); + return; + } + #ifdef CONFIG_DISPLAY_QUAD_SPI - devcfg.flags = SPI_DEVICE_HALFDUPLEX; - devcfg.command_bits = 8; - devcfg.address_bits = 24; -#endif + std::error_code ec; + spi_device = spi_bus->add_device( + espp::Spi::DeviceConfig{ + .command_bits = 8, + .address_bits = 24, + .mode = 0, + .clock_speed_hz = clock_speed, + .input_delay_ns = 0, + .cs_io_num = spics, + .queue_size = spi_queue_size, + .flags = SPI_DEVICE_HALFDUPLEX, #ifndef CONFIG_T_ENCODER_PRO - devcfg.pre_cb = lcd_spi_pre_transfer_callback; + .pre_cb = lcd_spi_pre_transfer_callback, +#endif + .post_cb = lcd_spi_post_transfer_callback, + }, + ec); + if (ec || !spi_device) { + fmt::print("Failed to initialize SPI device: {}\n", ec.message()); + return; + } +#else + panel_io = std::make_unique(espp::SpiPanelIo::Config{ + .spi = spi_bus.get(), + .device_config = + { + .mode = 0, + .clock_speed_hz = clock_speed, + .input_delay_ns = 0, + .cs_io_num = spics, + .queue_size = spi_queue_size, +#ifdef CONFIG_HARDWARE_BYTE90 + .flags = SPI_DEVICE_NO_DUMMY | SPI_DEVICE_3WIRE, +#endif + }, + .data_command_io = dc_pin, + .data_command_bit_mask = DC_LEVEL_BIT, + .post_transaction_callback_bit_mask = FLUSH_BIT, + .post_transaction_callback = lcd_spi_flush_ready, + }); + if (!panel_io->initialized()) { + fmt::print("Failed to initialize SPI panel I/O\n"); + return; + } #endif - devcfg.post_cb = lcd_spi_post_transfer_callback; - esp_err_t ret; - // Initialize the SPI bus - ret = spi_bus_initialize(spi_num, &buscfg, SPI_DMA_CH_AUTO); - ESP_ERROR_CHECK(ret); - // Attach the LCD to the SPI bus - ret = spi_bus_add_device(spi_num, &devcfg, &spi); - ESP_ERROR_CHECK(ret); - - // initialize the controller - DisplayDriver::initialize(espp::display_drivers::Config{ + + auto display_driver = std::make_shared(espp::display_drivers::Config{ + .panel_io = +#ifdef CONFIG_DISPLAY_QUAD_SPI + nullptr, .write_command = write_command, +#else + panel_io.get(), + .write_command = nullptr, +#endif + .read_command = nullptr, +#ifdef CONFIG_DISPLAY_QUAD_SPI .lcd_send_lines = lcd_send_lines, +#else + .lcd_send_lines = nullptr, +#endif .reset_pin = reset, #ifndef CONFIG_T_ENCODER_PRO .data_command_pin = dc_pin, @@ -582,16 +501,27 @@ extern "C" void app_main(void) { .mirror_x = mirror_x, .mirror_y = mirror_y, }); + display_driver->initialize(); // initialize the display / lvgl auto display = std::make_shared( - Display::LvglConfig{.width = width, - .height = height, - .flush_callback = DisplayDriver::flush, - .rotation_callback = DisplayDriver::rotate, - .rotation = rotation}, + Display::LvglConfig{ + .width = width, + .height = height, + .flush_callback = + [display_driver](lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { + display_driver->flush(disp, area, color_map); + }, + .rotation_callback = + [display_driver](const espp::DisplayRotation &rotation) { + display_driver->set_rotation(rotation); + }, + .rotation = rotation}, #if DISPLAY_IS_OLED - Display::OledConfig{.set_brightness_callback = DisplayDriver::set_brightness, - .get_brightness_callback = DisplayDriver::get_brightness}, + Display::OledConfig{ + .set_brightness_callback = + [display_driver](float brightness) { display_driver->set_brightness(brightness); }, + .get_brightness_callback = + [display_driver]() { return display_driver->get_brightness(); }}, #else Display::LcdConfig{.backlight_pin = backlight, .backlight_on_value = backlight_on_value}, #endif diff --git a/components/display_drivers/idf_component.yml b/components/display_drivers/idf_component.yml index 8a0bf68b6..653c6d6d5 100755 --- a/components/display_drivers/idf_component.yml +++ b/components/display_drivers/idf_component.yml @@ -13,6 +13,7 @@ tags: - Component - Display - Drivers + - SPI - GC9A01 - ILI9341 - SH8601 @@ -23,3 +24,4 @@ dependencies: version: '>=5.0' espp/display: '>=1.0' espp/led: '>=1.0' + espp/spi: '>=1.0' diff --git a/components/display_drivers/include/class.hpp b/components/display_drivers/include/class.hpp deleted file mode 100644 index 88fc4e874..000000000 --- a/components/display_drivers/include/class.hpp +++ /dev/null @@ -1,51 +0,0 @@ - -/** - * @brief Base class for display drivers. - */ -class DisplayDriver { -public: - /// @brief Construct a DisplayDriver - /// @param config Configuration for the display driver - explicit DisplayDriver(const Config &config) - : config_(config) {} - - /// @brief Initialize the display driver - /// @param write Write function for sending commands to the display - /// @param send_lines Function for sending pixel data to the display - /// @param width Width of the display in pixels - /// @param height Height of the display in pixels - /// @return True if initialization was successful, false otherwise - virtual bool initialize(write_command_fn write, send_lines_fn send_lines, size_t width, - size_t height) { - write_command_ = write; - send_lines_ = send_lines; - width_ = width; - height_ = height; - return true; - } - - /// @brief Reset the display - virtual void reset() { - std::scoped_lock lk(config_mutex_); - if (config_.reset_pin != GPIO_NUM_NC) { - gpio_set_level(config_.reset_pin, config_.reset_value); - std::this_thread::sleep_for(std::chrono::milliseconds(10)); - gpio_set_level(config_.reset_pin, !config_.reset_value); - std::this_thread::sleep_for(std::chrono::milliseconds(120)); - } - } - - /// @brief Get the width of the display - /// @return Width in pixels - size_t width() const { return width_; } - - /// @brief Get the height of the display - /// @return Height in pixels - size_t height() const { return height_; } - -protected: - std::mutex config_mutex_; - Config config_; - size_t width_{0}; - size_t height_{0}; -}; diff --git a/components/display_drivers/include/display_drivers.hpp b/components/display_drivers/include/display_drivers.hpp index 6a7f51b93..51d3ea7ad 100644 --- a/components/display_drivers/include/display_drivers.hpp +++ b/components/display_drivers/include/display_drivers.hpp @@ -1,7 +1,18 @@ #pragma once +#include +#include +#include +#include +#include +#include +#include +#include #include +#include #include +#include +#include #include #include @@ -42,21 +53,63 @@ typedef std::function send_lines_fn; +/** + * @brief SPI/parallel/DSI panel transport interface for object-style display + * controllers. + */ +class PanelIo { +public: + /// @brief Virtual destructor. + virtual ~PanelIo() = default; + + /// @brief Check whether the transport has been initialized successfully. + /// @return True if the transport is ready for use. + virtual bool initialized() const = 0; + + /// @brief Send a command and optional parameter payload immediately. + /// @param command Command byte to transmit. + /// @param parameters Optional command payload bytes. + /// @param flags Optional transport-specific user flags. + virtual void write_command(uint8_t command, std::span parameters, + uint32_t flags = 0) = 0; + + /// @brief Queue a command byte for asynchronous transmission. + /// @param command Command byte to transmit. + /// @param flags Optional transport-specific user flags. + virtual void queue_command(uint8_t command, uint32_t flags = 0) = 0; + + /// @brief Queue a non-pixel data payload for asynchronous transmission. + /// @param data Payload bytes to transmit. + /// @param flags Optional transport-specific user flags. + virtual void queue_data(std::span data, uint32_t flags = 0) = 0; + + /// @brief Queue a pixel payload for asynchronous transmission. + /// @param data Pointer to the pixel payload. + /// @param size Payload size in bytes. + /// @param flags Optional transport-specific user flags. + /// @param transaction_flags Optional low-level transaction flags. + virtual void queue_pixels(const uint8_t *data, size_t size, uint32_t flags = 0, + uint32_t transaction_flags = 0) = 0; + + /// @brief Wait for all queued transfers to complete. + virtual void wait() = 0; +}; + /** * @brief Config structure for all display drivers. */ struct Config { - write_command_fn write_command; /**< Function which the display driver uses to write commands to - the display. */ + PanelIo *panel_io{nullptr}; /**< Optional object-style transport used by the controller. */ + write_command_fn write_command; /**< Legacy low-level function used by the display driver to write + commands to the display. */ read_command_fn read_command{ - nullptr}; /**< Function which the display driver uses to read commands from - the display. Optional, may be nullptr if not supported. */ - send_lines_fn lcd_send_lines; /**< Function which the display driver uses to send bulk (color) - data (non-blocking) to be written to the display. Optional, may - be nullptr. */ + nullptr}; /**< Legacy low-level function used by the display driver to read commands from + the display. Optional, may be nullptr if not supported. */ + send_lines_fn lcd_send_lines; /**< Legacy low-level function used by the display driver to send + bulk color data asynchronously. Optional, may be nullptr. */ gpio_num_t reset_pin{GPIO_NUM_NC}; /**< Optional GPIO used for resetting the display. */ gpio_num_t data_command_pin{GPIO_NUM_NC}; /**< Optional GPIO used for indicating to the LCD - whether the bits are data or command bits. */ + whether the bits are data or command bits. */ bool reset_value{false}; /**< The value to set the reset pin to when resetting the display (low to reset default). */ uint8_t bits_per_pixel{16}; /**< How many bits per pixel, e.g. [1, 8, 16, 18, 24, 32]*/ @@ -98,13 +151,23 @@ template struct DisplayInitCmd { size_t delay_ms = 0; /**< Delay in milliseconds after sending the command. */ }; +/** + * @brief Common rectangle used by the reusable display-driver base classes. + */ +struct Region { + int xs{0}; + int ys{0}; + int xe{0}; + int ye{0}; +}; + /** * @brief Initialize the display pins. * @param reset GPIO pin used for resetting the display. * @param data_command GPIO pin used for indicating to the LCD whether the bits are data or command * @param reset_value The value to set the reset pin to when resetting the display. */ -static void init_pins(gpio_num_t reset, gpio_num_t data_command, uint8_t reset_value) { +inline void init_pins(gpio_num_t reset, gpio_num_t data_command, uint8_t reset_value) { // Initialize display pins if (reset == GPIO_NUM_NC && data_command == GPIO_NUM_NC) { return; @@ -136,5 +199,381 @@ static void init_pins(gpio_num_t reset, gpio_num_t data_command, uint8_t reset_v std::this_thread::sleep_for(100ms); } } + +/** + * @brief Build a controller-specific MADCTL base value from the shared config. + * @param config Display configuration. + * @param color_order_bit Bit used to swap RGB/BGR ordering. + * @param mirror_x_bit Bit used to mirror the X axis. + * @param mirror_y_bit Bit used to mirror the Y axis. + * @param swap_xy_bit Bit used to swap X/Y addressing, or 0 if unsupported. + * @return Base MADCTL value before runtime rotation is applied. + */ +inline uint8_t make_madctl_base(const Config &config, uint8_t color_order_bit, uint8_t mirror_x_bit, + uint8_t mirror_y_bit, uint8_t swap_xy_bit) { + uint8_t value = 0; + if (config.swap_color_order) { + value |= color_order_bit; + } + if (config.mirror_x) { + value |= mirror_x_bit; + } + if (config.mirror_y) { + value |= mirror_y_bit; + } + if (config.swap_xy && swap_xy_bit != 0) { + value |= swap_xy_bit; + } + return value; +} + +/** + * @brief Apply the standard four-orientation transform to a MADCTL value. + * @param value Base MADCTL value. + * @param config Display configuration. + * @param rotation Desired display rotation. + * @param mirror_x_bit Bit used to mirror the X axis. + * @param mirror_y_bit Bit used to mirror the Y axis. + * @param swap_xy_bit Bit used to swap X/Y addressing, or 0 if unsupported. + * @return Rotated MADCTL value. + */ +inline uint8_t apply_standard_rotation(uint8_t value, const Config &config, + DisplayRotation rotation, uint8_t mirror_x_bit, + uint8_t mirror_y_bit, uint8_t swap_xy_bit) { + switch (rotation) { + case DisplayRotation::LANDSCAPE: + break; + case DisplayRotation::PORTRAIT: + if (config.mirror_portrait) { + value ^= (mirror_x_bit | swap_xy_bit); + } else { + value ^= (mirror_y_bit | swap_xy_bit); + } + break; + case DisplayRotation::LANDSCAPE_INVERTED: + value ^= (mirror_y_bit | mirror_x_bit); + break; + case DisplayRotation::PORTRAIT_INVERTED: + if (config.mirror_portrait) { + value ^= (mirror_y_bit | swap_xy_bit); + } else { + value ^= (mirror_x_bit | swap_xy_bit); + } + break; + } + return value; +} + +/** + * @brief Object-style base class for display controllers. + */ +class Controller { +public: + /// @brief Construct a controller from the shared display configuration. + /// @param config Display configuration. + explicit Controller(const Config &config) + : config_(config) {} + + /// @brief Virtual destructor. + virtual ~Controller() = default; + + /// @brief Initialize the concrete display controller. + /// @return True on success. + virtual bool initialize() = 0; + + /// @brief Update the controller rotation state. + /// @param rotation New display rotation. + virtual void set_rotation(const DisplayRotation &rotation) { rotation_ = rotation; } + + /// @brief Write an area of pixel data without implicitly notifying LVGL. + /// @param disp LVGL display pointer, used when synchronous flush completion is needed. + /// @param area Area to update. + /// @param color_map Pixel buffer for the area. + /// @param flags Transport/user flags passed through to the write path. + virtual void fill(lv_display_t *disp, const lv_area_t *area, uint8_t *color_map, + uint32_t flags = 0) { + auto pixel_count = lv_area_get_width(area) * lv_area_get_height(area); + preprocess_color_map(color_map, pixel_count); + write_region({.xs = area->x1, .ys = area->y1, .xe = area->x2, .ye = area->y2}, color_map, + pixel_count * bytes_per_pixel(), flags); + if ((flags & flush_flag()) != 0 && !uses_async_flush() && disp != nullptr) { + lv_display_flush_ready(disp); + } + } + + /// @brief Flush an area of pixel data and notify LVGL when appropriate. + /// @param disp LVGL display pointer. + /// @param area Area to flush. + /// @param color_map Pixel buffer for the area. + virtual void flush(lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { + fill(disp, area, color_map, flush_flag()); + } + + /// @brief Clear a rectangular region to a solid color. + /// @param x Starting X coordinate. + /// @param y Starting Y coordinate. + /// @param width Width in pixels. + /// @param height Height in pixels. + /// @param color RGB565 color value. + virtual void clear(size_t x, size_t y, size_t width, size_t height, uint16_t color = 0x0000) = 0; + + /// @brief Update the controller's logical top-left pixel offset. + /// @param x X offset in pixels. + /// @param y Y offset in pixels. + virtual void set_offset(int x, int y) { + config_.offset_x = x; + config_.offset_y = y; + } + + /// @brief Read the controller's logical top-left pixel offset. + /// @param x Filled with the current X offset. + /// @param y Filled with the current Y offset. + virtual void get_offset(int &x, int &y) const { + x = config_.offset_x; + y = config_.offset_y; + } + +protected: + /// @brief Flag used to signal LVGL flush completion through low-level transports. + /// @return Bitmask containing the flush flag. + static constexpr uint32_t flush_flag() { + return 1u << static_cast(display_drivers::Flags::FLUSH_BIT); + } + + /// @brief Get bytes-per-pixel from the configured bit depth. + /// @return Number of bytes per pixel. + size_t bytes_per_pixel() const { return std::max(1, config_.bits_per_pixel / 8); } + + /// @brief Check whether this controller is using a `PanelIo` transport. + /// @return True if `panel_io` is configured and initialized. + bool uses_panel_io() const { + return config_.panel_io != nullptr && config_.panel_io->initialized(); + } + + /// @brief Check whether the active transport completes flushes asynchronously. + /// @return True if the transport signals completion later. + bool uses_async_flush() const { return uses_panel_io() || config_.lcd_send_lines != nullptr; } + + /// @brief Get the configured X/Y offset after applying the current rotation convention. + /// @return Rotated `(x, y)` offset pair. + std::pair get_rotated_offset() const { + switch (rotation_) { + case DisplayRotation::PORTRAIT: + case DisplayRotation::PORTRAIT_INVERTED: + return {config_.offset_y, config_.offset_x}; + case DisplayRotation::LANDSCAPE: + case DisplayRotation::LANDSCAPE_INVERTED: + default: + return {config_.offset_x, config_.offset_y}; + } + } + + /// @brief Apply the rotated offset to a region. + /// @param region Region in logical display coordinates. + /// @return Offset-adjusted region. + Region apply_offset(Region region) const { + auto [offset_x, offset_y] = get_rotated_offset(); + region.xs += offset_x; + region.xe += offset_x; + region.ys += offset_y; + region.ye += offset_y; + return region; + } + + /// @brief Allow concrete controllers to transform a region before writing it. + /// @param region Region after offsets have been applied. + /// @return Controller-specific transformed region. + virtual Region transform_region(Region region) const { return region; } + + /// @brief Send a list of initialization commands. + /// @tparam Command Command enum or byte type. + /// @param commands Sequence of initialization commands. + template + void send_commands(std::span> commands) { + using namespace std::chrono_literals; + for (const auto &[command, parameters, delay_ms] : commands) { + write_command(static_cast(command), parameters, 0); + if (delay_ms > 0) { + std::this_thread::sleep_for(delay_ms * 1ms); + } + } + } + + /// @brief Send a fixed-size array of initialization commands. + /// @tparam Command Command enum or byte type. + /// @tparam N Number of commands. + /// @param commands Sequence of initialization commands. + template + void send_commands(const std::array, N> &commands) { + send_commands(std::span>(commands.data(), commands.size())); + } + + /// @brief Write a command through the active transport. + /// @param command Command byte. + /// @param parameters Optional payload bytes. + /// @param flags Optional transport/user flags. + void write_command(uint8_t command, std::span parameters = {}, + uint32_t flags = 0) { + if (uses_panel_io()) { + config_.panel_io->write_command(command, parameters, flags); + return; + } + if (config_.write_command) { + config_.write_command(command, parameters, flags); + } + } + + /// @brief Read command data through the legacy callback path. + /// @param command Command byte. + /// @param data Buffer to fill with returned data. + /// @param flags Optional transport/user flags. + void read_command(uint8_t command, std::span data, uint32_t flags = 0) { + if (config_.read_command) { + config_.read_command(command, data, flags); + } + } + + /// @brief Perform any controller-specific preprocessing on a pixel buffer before writeout. + /// @param color_map Pixel buffer. + /// @param pixel_count Number of pixels in the buffer. + virtual void preprocess_color_map(uint8_t *color_map, size_t pixel_count) const { + if (config_.bits_per_pixel == 16) { + lv_draw_sw_rgb565_swap(color_map, pixel_count); + } + } + + /// @brief Write a transformed region to the active transport. + /// @param region Region to write. + /// @param data Pixel payload bytes. + /// @param size Payload size in bytes. + /// @param flags Optional transport/user flags. + virtual void write_region(Region region, const uint8_t *data, size_t size, uint32_t flags) = 0; + + Config config_; + std::mutex io_mutex_; + DisplayRotation rotation_{DisplayRotation::LANDSCAPE}; +}; + +/** + * @brief Shared base for command/data/window-based MIPI DBI panels. + */ +class MipiDbiDisplayDriver : public Controller { +public: + /// @brief DBI protocol commands used by a concrete controller. + struct Protocol { + uint8_t column_address_command{0}; ///< CASET-equivalent command. + uint8_t row_address_command{0}; ///< RASET-equivalent command. + uint8_t memory_write_command{0}; ///< RAMWR-equivalent command. + bool use_8bit_coordinates{false}; ///< Whether region coordinates are encoded as 8-bit values. + }; + + /// @brief Construct a shared MIPI DBI-style controller base. + /// @param config Shared display configuration. + /// @param protocol Controller-specific command mapping. + explicit MipiDbiDisplayDriver(const Config &config, const Protocol &protocol) + : Controller(config) + , protocol_(protocol) {} + + /// @brief Clear a rectangular region to a solid color. + /// @param x Starting X coordinate. + /// @param y Starting Y coordinate. + /// @param width Width in pixels. + /// @param height Height in pixels. + /// @param color RGB565 color value. + void clear(size_t x, size_t y, size_t width, size_t height, uint16_t color = 0x0000) override { + auto region = transform_region(apply_offset({.xs = static_cast(x), + .ys = static_cast(y), + .xe = static_cast(x + width), + .ye = static_cast(y + height)})); + + std::array color_words; + color_words.fill(color); + + std::scoped_lock lock(io_mutex_); + if (uses_panel_io()) { + config_.panel_io->wait(); + } + write_window(region); + + auto total_pixels = width * height; + for (size_t written = 0; written < total_pixels; written += color_words.size()) { + auto chunk_pixels = std::min(total_pixels - written, color_words.size()); + write_command(protocol_.memory_write_command, + {reinterpret_cast(color_words.data()), chunk_pixels * 2}, 0); + } + } + +protected: + /// @brief Write a pixel region using either `PanelIo`, legacy async callbacks, or blocking + /// writes. + /// @param region Region to update. + /// @param data Pixel payload bytes. + /// @param size Payload size in bytes. + /// @param flags Optional transport/user flags. + void write_region(Region region, const uint8_t *data, size_t size, uint32_t flags) override { + region = transform_region(apply_offset(region)); + + if (uses_panel_io()) { + std::scoped_lock lock(io_mutex_); + config_.panel_io->wait(); + queue_window(region); + config_.panel_io->queue_command(protocol_.memory_write_command); + config_.panel_io->queue_pixels(data, size, flags); + return; + } + + if (config_.lcd_send_lines) { + config_.lcd_send_lines(region.xs, region.ys, region.xe, region.ye, data, flags); + return; + } + + std::scoped_lock lock(io_mutex_); + write_window(region); + write_command(protocol_.memory_write_command, {data, size}, flags); + } + + /// @brief Write the column/row window registers synchronously. + /// @param region Region to encode as the active drawing window. + void write_window(const Region ®ion) { + auto [column_bytes, column_size] = encode_range(region.xs, region.xe); + auto [row_bytes, row_size] = encode_range(region.ys, region.ye); + write_command(protocol_.column_address_command, {column_bytes.data(), column_size}, 0); + write_command(protocol_.row_address_command, {row_bytes.data(), row_size}, 0); + } + + /// @brief Queue the column/row window registers for asynchronous transmission. + /// @param region Region to encode as the active drawing window. + void queue_window(const Region ®ion) { + auto [column_bytes, column_size] = encode_range(region.xs, region.xe); + auto [row_bytes, row_size] = encode_range(region.ys, region.ye); + config_.panel_io->queue_command(protocol_.column_address_command); + config_.panel_io->queue_data({column_bytes.data(), column_size}); + config_.panel_io->queue_command(protocol_.row_address_command); + config_.panel_io->queue_data({row_bytes.data(), row_size}); + } + + /// @brief Encode a start/end coordinate range for the controller protocol. + /// @param start Starting coordinate. + /// @param end Ending coordinate. + /// @return Encoded byte buffer and number of valid bytes. + std::pair, size_t> encode_range(int start, int end) const { + std::array bytes{}; + if (protocol_.use_8bit_coordinates) { + bytes[0] = start & 0xff; + bytes[1] = end & 0xff; + return {bytes, 2}; + } + + bytes[0] = (start >> 8) & 0xff; + bytes[1] = start & 0xff; + bytes[2] = (end >> 8) & 0xff; + bytes[3] = end & 0xff; + return {bytes, 4}; + } + + Protocol protocol_; +}; } // namespace display_drivers } // namespace espp + +#include "spi_panel_io.hpp" diff --git a/components/display_drivers/include/gc9a01.hpp b/components/display_drivers/include/gc9a01.hpp index 3deebbe34..d0cf77654 100644 --- a/components/display_drivers/include/gc9a01.hpp +++ b/components/display_drivers/include/gc9a01.hpp @@ -1,130 +1,80 @@ #pragma once -#include - #include "display_drivers.hpp" namespace espp { /** * @brief Display driver for the GC9A01 display controller. * - * This code is modified from - * https://github.com/lvgl/lvgl_esp32_drivers/blob/master/lvgl_tft/GC9A01.c - * - * See also: - * https://github.com/espressif/esp-bsp/blob/master/components/lcd/esp_lcd_gc9a01/esp_lcd_gc9a01.c - * * \section smartknob_ha_cfg SmartKnob Config * \snippet display_drivers_example.cpp smartknob_config example * \section gc9a01_ex1 Gc9a01 Example * \snippet display_drivers_example.cpp display_drivers example */ -class Gc9a01 { +class Gc9a01 : public display_drivers::MipiDbiDisplayDriver { public: enum class Command : uint8_t { - nop = 0x00, // no operation - swreset = 0x01, // software reset - rddid = 0x04, // read display id - rddst = 0x09, // read display status - - slpin = 0x10, // sleep in - slpout = 0x11, // sleep out - ptlon = 0x12, // partial mode on - noron = 0x13, // normal display mode on - - invoff = 0x20, // display inversion off - invon = 0x21, // display inversion on - - dispoff = 0x28, // display off - dispon = 0x29, // display on - - caset = 0x2a, // column address set - raset = 0x2b, // row address set - ramwr = 0x2c, // ram write - - ptlar = 0x30, // partial area - vscrdef = 0x33, // vertical scrolling definition - teoff = 0x34, // tearing effect line off - teon = 0x35, // tearing effect line on - madctl = 0x36, // memory access control - idmoff = 0x38, // idle mode off - idmon = 0x39, // idle mode on - colmod = 0x3a, // color mode - pixel format - ramwrc = 0x3c, // memory write continue - - settes = 0x44, // set tear scanline - gettes = 0x45, // get tear scanline - - wrdpbr = 0x51, // write display brightness - - wrctrldp = 0x53, // write CTRL display - - readid1 = 0xDA, // read ID 1 - readid2 = 0xDB, // read ID 2 - readid3 = 0xDC, // read ID 3 - - rgbctrl = 0xb0, // rgb control - porctrl = 0xb5, // porch control - dpfuctrl = 0xb6, // display function control - - tectrl = 0xba, // ram control - - intrctrl = 0xf6, // interface control - - frctrl = 0xe8, // frame rate control - spi2dctrl = 0xe9, // spi 2data control - - pwrctrl1 = 0xc1, // power control 1 - pwrctrl2 = 0xc3, // power control 2 - pwrctrl3 = 0xc4, // power control 3 - pwrctrl4 = 0xc9, // power control 4 - pwrctrl7 = 0xa7, // power control 7 - - intren1 = 0xfe, // inter register enable 1 - intren2 = 0xef, // inter register enable 2 - - stgamma1 = 0xf0, // set gamma 1 - stgamma2 = 0xf1, // set gamma 2 - stgamma3 = 0xf2, // set gamma 3 - stgamma4 = 0xf3, // set gamma 4 + nop = 0x00, + swreset = 0x01, + rddid = 0x04, + rddst = 0x09, + slpin = 0x10, + slpout = 0x11, + ptlon = 0x12, + noron = 0x13, + invoff = 0x20, + invon = 0x21, + dispoff = 0x28, + dispon = 0x29, + caset = 0x2a, + raset = 0x2b, + ramwr = 0x2c, + ptlar = 0x30, + vscrdef = 0x33, + teoff = 0x34, + teon = 0x35, + madctl = 0x36, + idmoff = 0x38, + idmon = 0x39, + colmod = 0x3a, + ramwrc = 0x3c, + settes = 0x44, + gettes = 0x45, + wrdpbr = 0x51, + wrctrldp = 0x53, + readid1 = 0xDA, + readid2 = 0xDB, + readid3 = 0xDC, + rgbctrl = 0xb0, + porctrl = 0xb5, + dpfuctrl = 0xb6, + tectrl = 0xba, + intrctrl = 0xf6, + frctrl = 0xe8, + spi2dctrl = 0xe9, + pwrctrl1 = 0xc1, + pwrctrl2 = 0xc3, + pwrctrl3 = 0xc4, + pwrctrl4 = 0xc9, + pwrctrl7 = 0xa7, + intren1 = 0xfe, + intren2 = 0xef, + stgamma1 = 0xf0, + stgamma2 = 0xf1, + stgamma3 = 0xf2, + stgamma4 = 0xf3, }; - /** - * @brief Store the config data and send the initialization commands to the - * display controller. - * @param config display_drivers::Config structure - */ - static void initialize(const display_drivers::Config &config) { - // update the static members - write_command_ = config.write_command; - lcd_send_lines_ = config.lcd_send_lines; - reset_pin_ = config.reset_pin; - dc_pin_ = config.data_command_pin; - offset_x_ = config.offset_x; - offset_y_ = config.offset_y; - mirror_x_ = config.mirror_x; - mirror_y_ = config.mirror_y; - mirror_portrait_ = config.mirror_portrait; - swap_xy_ = config.swap_xy; - swap_color_order_ = config.swap_color_order; - // Initialize display pins - display_drivers::init_pins(reset_pin_, dc_pin_, config.reset_value); + explicit Gc9a01(const display_drivers::Config &config) + : MipiDbiDisplayDriver(config, + {.column_address_command = static_cast(Command::caset), + .row_address_command = static_cast(Command::raset), + .memory_write_command = static_cast(Command::ramwr)}) {} - uint8_t madctl = 0; - if (swap_color_order_) { - madctl |= LCD_CMD_BGR_BIT; - } - if (mirror_x_) { - madctl |= LCD_CMD_MX_BIT; - } - if (mirror_y_) { - madctl |= LCD_CMD_MY_BIT; - } - if (swap_xy_) { - madctl |= LCD_CMD_MV_BIT; - } + bool initialize() override { + display_drivers::init_pins(config_.reset_pin, config_.data_command_pin, config_.reset_value); - // init the display + auto madctl = make_madctl(DisplayRotation::LANDSCAPE); auto init_commands = std::to_array>({ {0xEF}, {0xEB, {0x14}}, @@ -144,7 +94,6 @@ class Gc9a01 { {0x8E, {0xFF}}, {0x8F, {0xFF}}, {0xB6, {0x00, 0x20}}, - // call orientation {0x36, {madctl}}, {0x3A, {0x05}}, {0x90, {0x08, 0x08, 0X08, 0X08}}, @@ -179,253 +128,25 @@ class Gc9a01 { {0x29, {}, 100}, }); - // send the init commands send_commands(init_commands); - - // configure the display color configuration - if (config.invert_colors) { - write_command_(static_cast(Command::invon), {}, 0); - } else { - write_command_(static_cast(Command::invoff), {}, 0); - } - } - - /** - * @brief Set the display rotation. - * @param rotation New display rotation. - */ - static void rotate(const DisplayRotation &rotation) { - uint8_t data = 0; - if (swap_color_order_) { - data |= LCD_CMD_BGR_BIT; - } - if (mirror_x_) { - data |= LCD_CMD_MX_BIT; - } - if (mirror_y_) { - data |= LCD_CMD_MY_BIT; - } - if (swap_xy_) { - data |= LCD_CMD_MV_BIT; - } - switch (rotation) { - case DisplayRotation::LANDSCAPE: - break; - case DisplayRotation::PORTRAIT: - // flip the mx and mv bits (xor) - if (mirror_portrait_) { - data ^= (LCD_CMD_MX_BIT | LCD_CMD_MV_BIT); - } else { - data ^= (LCD_CMD_MY_BIT | LCD_CMD_MV_BIT); - } - break; - case DisplayRotation::LANDSCAPE_INVERTED: - // flip the my and mx bits (xor) - data ^= (LCD_CMD_MY_BIT | LCD_CMD_MX_BIT); - break; - case DisplayRotation::PORTRAIT_INVERTED: - // flip the my and mv bits (xor) - if (mirror_portrait_) { - data ^= (LCD_CMD_MY_BIT | LCD_CMD_MV_BIT); - } else { - data ^= (LCD_CMD_MX_BIT | LCD_CMD_MV_BIT); - } - break; - } - std::scoped_lock lock{spi_mutex_}; - write_command_(static_cast(Command::madctl), {&data, 1}, 0); + write_command(static_cast(config_.invert_colors ? Command::invon : Command::invoff), + {}, 0); + return true; } - /** - * @brief Flush the pixel data for the provided area to the display. - * @param *disp Pointer to the LVGL display. - * @param *area Pointer to the structure describing the pixel area. - * @param *color_map Pointer to array of colors to flush to the display. - */ - static void flush(lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { - fill(disp, area, color_map, (1 << (int)display_drivers::Flags::FLUSH_BIT)); + void set_rotation(const DisplayRotation &rotation) override { + Controller::set_rotation(rotation); + auto data = std::array{make_madctl(rotation)}; + std::scoped_lock lock(io_mutex_); + write_command(static_cast(Command::madctl), data, 0); } - /** - * @brief Set the drawing area for the display, resets the cursor to the - * starting position of the area. - * @param *area Pointer to lv_area_t strcuture with start/end x/y - * coordinates. - */ - static void set_drawing_area(const lv_area_t *area) { - set_drawing_area(area->x1, area->y1, area->x2, area->y2); +private: + uint8_t make_madctl(DisplayRotation rotation) const { + auto value = display_drivers::make_madctl_base(config_, LCD_CMD_BGR_BIT, LCD_CMD_MX_BIT, + LCD_CMD_MY_BIT, LCD_CMD_MV_BIT); + return display_drivers::apply_standard_rotation(value, config_, rotation, LCD_CMD_MX_BIT, + LCD_CMD_MY_BIT, LCD_CMD_MV_BIT); } - - /** - * @brief Set the drawing area for the display, resets the cursor to the - * starting position of the area. - * @param xs Starting x coordinate of the area. - * @param ys Starting y coordinate of the area. - * @param xe Ending x coordinate of the area. - * @param ye Ending y coordinate of the area. - */ - static void set_drawing_area(size_t xs, size_t ys, size_t xe, size_t ye) { - std::array data; - - int offset_x = 0; - int offset_y = 0; - get_offset_rotated(offset_x, offset_y); - - uint16_t start_x = xs + offset_x; - uint16_t end_x = xe + offset_x; - uint16_t start_y = ys + offset_y; - uint16_t end_y = ye + offset_y; - - // Set the column (x) start / end addresses - data[0] = (start_x >> 8) & 0xFF; - data[1] = start_x & 0xFF; - data[2] = (end_x >> 8) & 0xFF; - data[3] = end_x & 0xFF; - write_command_(static_cast(Command::caset), data, 0); - - // Set the row (y) start / end addresses - data[0] = (start_y >> 8) & 0xFF; - data[1] = start_y & 0xFF; - data[2] = (end_y >> 8) & 0xFF; - data[3] = end_y & 0xFF; - write_command_(static_cast(Command::raset), data, 0); - } - - /** - * @brief Fill the display area with the provided color map. - * @param *disp Pointer to the LVGL display. - * @param *area Pointer to the structure describing the pixel area. - * @param *color_map Pointer to array of colors to fill the display with. - * @param flags uint32_t user data / flags to pass to the lcd_write transfer function. - */ - static void fill(lv_display_t *disp, const lv_area_t *area, uint8_t *color_map, - uint32_t flags = 0) { - std::scoped_lock lock{spi_mutex_}; - lv_draw_sw_rgb565_swap(color_map, lv_area_get_width(area) * lv_area_get_height(area)); - if (lcd_send_lines_) { - int offset_x = 0; - int offset_y = 0; - get_offset_rotated(offset_x, offset_y); - lcd_send_lines_(area->x1 + offset_x, area->y1 + offset_y, area->x2 + offset_x, - area->y2 + offset_y, color_map, flags); - } else { - set_drawing_area(area); - uint32_t size = lv_area_get_width(area) * lv_area_get_height(area); - write_command_(static_cast(Command::ramwr), {color_map, size * 2}, flags); - } - } - - /** - * @brief Clear the display area, filling it with the provided color. - * @param x X coordinate of the upper left corner of the display area. - * @param y Y coordinate of the upper left corner of the display area. - * @param width Width of the display area to clear. - * @param height Height of the display area to clear. - * @param color 16 bit color (default 0x0000) to fill with. - */ - static void clear(size_t x, size_t y, size_t width, size_t height, uint16_t color = 0x0000) { - set_drawing_area(x, y, x + width, y + height); - - // Write the color data to controller RAM - uint32_t size = width * height; - static constexpr int max_bytes_to_send = 1024 * 2; - uint16_t color_data[max_bytes_to_send]; - memset(color_data, color, max_bytes_to_send * sizeof(uint16_t)); - for (int i = 0; i < size; i += max_bytes_to_send) { - size_t num_bytes = std::min(static_cast(size - i), (int)(max_bytes_to_send)); - write_command_(static_cast(Command::ramwr), - {reinterpret_cast(color_data), num_bytes * 2}, 0); - } - } - - /** - * @brief Send the provided commands to the display controller. - * @param commands Array of display_drivers::LcdInitCmd structures. - */ - static void send_commands(std::span> commands) { - using namespace std::chrono_literals; - - for (const auto &[command, parameters, delay_ms] : commands) { - std::scoped_lock lock{spi_mutex_}; - write_command_(command, parameters, 0); - std::this_thread::sleep_for(delay_ms * 1ms); - } - } - - /** - * @brief Set the offset (upper left starting coordinate) of the display. - * @note This modifies internal variables that are used when sending - * coordinates / filling parts of the display. - * @param x New starting x coordinate (so writing to x address 0 later will - * actually write to this offset). - * @param y New starting y coordinate (so writing to y address 0 later will - * actually write to this offset). - */ - static void set_offset(int x, int y) { - offset_x_ = x; - offset_y_ = y; - } - - /** - * @brief Get the offset (upper left starting coordinate) of the display. - * @note This returns internal variables that are used when sending - * coordinates / filling parts of the display. - * @param x Reference variable that will be filled with the currently - * configured starting x coordinate that was provided in the config - * or set by set_offset(). - * @param y Reference variable that will be filled with the currently - * configured starting y coordinate that was provided in the config - * or set by set_offset(). - */ - static void get_offset(int &x, int &y) { - x = offset_x_; - y = offset_y_; - } - - /** - * @brief Get the offset (upper left starting coordinate) of the display - * after rotation. - * @note This returns internal variables that are used when sending - * coordinates / filling parts of the display. - * @param x Reference variable that will be filled with the currently - * configured starting x coordinate that was provided in the config - * or set by set_offset(), updated for the current rotation. - * @param y Reference variable that will be filled with the currently - * configured starting y coordinate that was provided in the config - * or set by set_offset(), updated for the current rotation. - */ - static void get_offset_rotated(int &x, int &y) { - auto rotation = lv_display_get_rotation(lv_display_get_default()); - switch (rotation) { - case LV_DISPLAY_ROTATION_90: - // intentional fallthrough - case LV_DISPLAY_ROTATION_270: - x = offset_y_; - y = offset_x_; - break; - case LV_DISPLAY_ROTATION_0: - // intentional fallthrough - case LV_DISPLAY_ROTATION_180: - // intentional fallthrough - default: - x = offset_x_; - y = offset_y_; - break; - } - } - -protected: - static inline display_drivers::write_command_fn write_command_; - static inline display_drivers::send_lines_fn lcd_send_lines_; - static inline gpio_num_t reset_pin_; - static inline gpio_num_t dc_pin_; - static inline int offset_x_; - static inline int offset_y_; - static inline bool mirror_x_; - static inline bool mirror_y_; - static inline bool mirror_portrait_; - static inline bool swap_xy_; - static inline bool swap_color_order_; - static inline std::mutex spi_mutex_; }; } // namespace espp diff --git a/components/display_drivers/include/ili9341.hpp b/components/display_drivers/include/ili9341.hpp index ce38c93b8..8da3e0586 100644 --- a/components/display_drivers/include/ili9341.hpp +++ b/components/display_drivers/include/ili9341.hpp @@ -1,83 +1,47 @@ #pragma once -#include - #include "display_drivers.hpp" namespace espp { /** * @brief Display driver for the ILI9341 display controller. * - * This code is modified from - * https://github.com/lvgl/lvgl_esp32_drivers/blob/master/lvgl_tft/ili9341.c - * and - * https://github.com/espressif/esp-dev-kits/blob/master/esp32-s2-hmi-devkit-1/components/screen/controller_driver/ili9341/ili9341.c - * - * See also: - * https://github.com/espressif/esp-bsp/blob/master/components/lcd/esp_lcd_ili9341/esp_lcd_ili9341.c - * * \section ili9341_wrover_cfg WROVER-KIT ILI9341 Config * \snippet display_drivers_example.cpp wrover_kit_config example * \section ili9341_ex1 ili9341 Example * \snippet display_drivers_example.cpp display_drivers example */ -class Ili9341 { +class Ili9341 : public display_drivers::MipiDbiDisplayDriver { public: enum class Command : uint8_t { - invoff = 0x20, // display inversion off - invon = 0x21, // display inversion on - gamset = 0x26, // gamma set - dispoff = 0x28, // display off - dispon = 0x29, // display on - caset = 0x2a, // column address set - raset = 0x2b, // row address set - ramwr = 0x2c, // ram write - rgbset = 0x2d, // color setting for 4096, 64k and 262k colors - ramrd = 0x2e, // ram read - madctl = 0x36, // memory data access control - idmoff = 0x38, // idle mode off - idmon = 0x39, // idle mode on - ramwrc = 0x3c, // memory write continue (st7789v) - ramrdc = 0x3e, // memory read continue (st7789v) - colmod = 0x3a, // color mode - pixel format + invoff = 0x20, + invon = 0x21, + gamset = 0x26, + dispoff = 0x28, + dispon = 0x29, + caset = 0x2a, + raset = 0x2b, + ramwr = 0x2c, + rgbset = 0x2d, + ramrd = 0x2e, + madctl = 0x36, + idmoff = 0x38, + idmon = 0x39, + ramwrc = 0x3c, + ramrdc = 0x3e, + colmod = 0x3a, }; - /** - * @brief Store the config data and send the initialization commands to the - * display controller. - * @param config display_drivers::Config structure - */ - static void initialize(const display_drivers::Config &config) { - // update the static members - write_command_ = config.write_command; - lcd_send_lines_ = config.lcd_send_lines; - reset_pin_ = config.reset_pin; - dc_pin_ = config.data_command_pin; - offset_x_ = config.offset_x; - offset_y_ = config.offset_y; - mirror_x_ = config.mirror_x; - mirror_y_ = config.mirror_y; - mirror_portrait_ = config.mirror_portrait; - swap_xy_ = config.swap_xy; - swap_color_order_ = config.swap_color_order; - // Initialize display pins - display_drivers::init_pins(reset_pin_, dc_pin_, config.reset_value); + explicit Ili9341(const display_drivers::Config &config) + : MipiDbiDisplayDriver(config, + {.column_address_command = static_cast(Command::caset), + .row_address_command = static_cast(Command::raset), + .memory_write_command = static_cast(Command::ramwr)}) {} - uint8_t madctl = 0x00; - if (swap_color_order_) { - madctl |= LCD_CMD_BGR_BIT; - } - if (mirror_x_) { - madctl |= LCD_CMD_MX_BIT; - } - if (mirror_y_) { - madctl |= LCD_CMD_MY_BIT; - } - if (swap_xy_) { - madctl |= LCD_CMD_MV_BIT; - } + bool initialize() override { + display_drivers::init_pins(config_.reset_pin, config_.data_command_pin, config_.reset_value); - // init the display + auto madctl = make_madctl(DisplayRotation::LANDSCAPE); auto init_commands = std::to_array>({ {0xCF, {0x00, 0x83, 0X30}}, {0xED, {0x64, 0x03, 0X12, 0X81}}, @@ -109,253 +73,25 @@ class Ili9341 { {0x29, {}, 100}, }); - // send the init commands send_commands(init_commands); - - // configure the display color configuration - if (config.invert_colors) { - write_command_(static_cast(Command::invon), {}, 0); - } else { - write_command_(static_cast(Command::invoff), {}, 0); - } - } - - /** - * @brief Set the display rotation. - * @param rotation New display rotation. - */ - static void rotate(const DisplayRotation &rotation) { - uint8_t data = 0x00; - if (swap_color_order_) { - data |= LCD_CMD_BGR_BIT; - } - if (mirror_x_) { - data |= LCD_CMD_MX_BIT; - } - if (mirror_y_) { - data |= LCD_CMD_MY_BIT; - } - if (swap_xy_) { - data |= LCD_CMD_MV_BIT; - } - switch (rotation) { - case DisplayRotation::LANDSCAPE: - break; - case DisplayRotation::PORTRAIT: - // flip the mx and mv bits (xor) - if (mirror_portrait_) { - data ^= (LCD_CMD_MX_BIT | LCD_CMD_MV_BIT); - } else { - data ^= (LCD_CMD_MY_BIT | LCD_CMD_MV_BIT); - } - break; - case DisplayRotation::LANDSCAPE_INVERTED: - // flip the my and mx bits (xor) - data ^= (LCD_CMD_MY_BIT | LCD_CMD_MX_BIT); - break; - case DisplayRotation::PORTRAIT_INVERTED: - // flip the my and mv bits (xor) - if (mirror_portrait_) { - data ^= (LCD_CMD_MY_BIT | LCD_CMD_MV_BIT); - } else { - data ^= (LCD_CMD_MX_BIT | LCD_CMD_MV_BIT); - } - break; - } - std::scoped_lock lock{spi_mutex_}; - write_command_(static_cast(Command::madctl), {&data, 1}, 0); - } - - /** - * @brief Flush the pixel data for the provided area to the display. - * @param *disp Pointer to the LVGL display. - * @param *area Pointer to the structure describing the pixel area. - * @param *color_map Pointer to array of colors to flush to the display. - */ - static void flush(lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { - fill(disp, area, color_map, (1 << (int)display_drivers::Flags::FLUSH_BIT)); - } - - /** - * @brief Set the drawing area for the display, resets the cursor to the - * starting position of the area. - * @param *area Pointer to lv_area_t strcuture with start/end x/y - * coordinates. - */ - static void set_drawing_area(const lv_area_t *area) { - set_drawing_area(area->x1, area->y1, area->x2, area->y2); + write_command(static_cast(config_.invert_colors ? Command::invon : Command::invoff), + {}, 0); + return true; } - /** - * @brief Set the drawing area for the display, resets the cursor to the - * starting position of the area. - * @param xs Starting x coordinate of the area. - * @param ys Starting y coordinate of the area. - * @param xe Ending x coordinate of the area. - * @param ye Ending y coordinate of the area. - */ - static void set_drawing_area(size_t xs, size_t ys, size_t xe, size_t ye) { - std::array data; - - int offset_x = 0; - int offset_y = 0; - get_offset_rotated(offset_x, offset_y); - - uint16_t start_x = xs + offset_x; - uint16_t end_x = xe + offset_x; - uint16_t start_y = ys + offset_y; - uint16_t end_y = ye + offset_y; - - // Set the column (x) start / end addresses - data[0] = (start_x >> 8) & 0xFF; - data[1] = start_x & 0xFF; - data[2] = (end_x >> 8) & 0xFF; - data[3] = end_x & 0xFF; - write_command_(static_cast(Command::caset), data, 0); - - // Set the row (y) start / end addresses - data[0] = (start_y >> 8) & 0xFF; - data[1] = start_y & 0xFF; - data[2] = (end_y >> 8) & 0xFF; - data[3] = end_y & 0xFF; - write_command_(static_cast(Command::raset), data, 0); + void set_rotation(const DisplayRotation &rotation) override { + Controller::set_rotation(rotation); + auto data = std::array{make_madctl(rotation)}; + std::scoped_lock lock(io_mutex_); + write_command(static_cast(Command::madctl), data, 0); } - /** - * @brief Fill the display area with the provided color map. - * @param *disp Pointer to the LVGL display. - * @param *area Pointer to the structure describing the pixel area. - * @param *color_map Pointer to array of colors to flush to the display. - * @param flags uint32_t user data / flags to pass to the lcd_write transfer function. - */ - static void fill(lv_display_t *disp, const lv_area_t *area, uint8_t *color_map, - uint32_t flags = 0) { - std::scoped_lock lock{spi_mutex_}; - lv_draw_sw_rgb565_swap(color_map, lv_area_get_width(area) * lv_area_get_height(area)); - if (lcd_send_lines_) { - int offset_x = 0; - int offset_y = 0; - get_offset_rotated(offset_x, offset_y); - lcd_send_lines_(area->x1 + offset_x, area->y1 + offset_y, area->x2 + offset_x, - area->y2 + offset_y, color_map, flags); - } else { - set_drawing_area(area); - uint32_t size = lv_area_get_width(area) * lv_area_get_height(area); - write_command_(static_cast(Command::ramwr), {color_map, size * 2}, flags); - } +private: + uint8_t make_madctl(DisplayRotation rotation) const { + auto value = display_drivers::make_madctl_base(config_, LCD_CMD_BGR_BIT, LCD_CMD_MX_BIT, + LCD_CMD_MY_BIT, LCD_CMD_MV_BIT); + return display_drivers::apply_standard_rotation(value, config_, rotation, LCD_CMD_MX_BIT, + LCD_CMD_MY_BIT, LCD_CMD_MV_BIT); } - - /** - * @brief Clear the display area, filling it with the provided color. - * @param x X coordinate of the upper left corner of the display area. - * @param y Y coordinate of the upper left corner of the display area. - * @param width Width of the display area to clear. - * @param height Height of the display area to clear. - * @param color 16 bit color (default 0x0000) to fill with. - */ - static void clear(size_t x, size_t y, size_t width, size_t height, uint16_t color = 0x0000) { - set_drawing_area(x, y, x + width, y + height); - - // Write the color data to controller RAM - uint32_t size = width * height; - static constexpr int max_bytes_to_send = 1024 * 2; - uint16_t color_data[max_bytes_to_send]; - memset(color_data, color, max_bytes_to_send * sizeof(uint16_t)); - for (int i = 0; i < size; i += max_bytes_to_send) { - size_t num_bytes = std::min((int)(size - i), (int)(max_bytes_to_send)); - write_command_(static_cast(Command::ramwr), - {reinterpret_cast(color_data), num_bytes * 2}, 0); - } - } - - /** - * @brief Send the provided commands to the display controller. - * @param commands Array of display_drivers::LcdInitCmd structures. - */ - static void send_commands(std::span> commands) { - using namespace std::chrono_literals; - - for (const auto &[command, parameters, delay_ms] : commands) { - std::scoped_lock lock{spi_mutex_}; - write_command_(command, parameters, 0); - std::this_thread::sleep_for(delay_ms * 1ms); - } - } - - /** - * @brief Set the offset (upper left starting coordinate) of the display. - * @note This modifies internal variables that are used when sending - * coordinates / filling parts of the display. - * @param x New starting x coordinate (so writing to x address 0 later will - * actually write to this offset). - * @param y New starting y coordinate (so writing to y address 0 later will - * actually write to this offset). - */ - static void set_offset(int x, int y) { - offset_x_ = x; - offset_y_ = y; - } - - /** - * @brief Get the offset (upper left starting coordinate) of the display. - * @note This returns internal variables that are used when sending - * coordinates / filling parts of the display. - * @param x Reference variable that will be filled with the currently - * configured starting x coordinate that was provided in the config - * or set by set_offset(). - * @param y Reference variable that will be filled with the currently - * configured starting y coordinate that was provided in the config - * or set by set_offset(). - */ - static void get_offset(int &x, int &y) { - x = offset_x_; - y = offset_y_; - } - - /** - * @brief Get the offset (upper left starting coordinate) of the display - * after rotation. - * @note This returns internal variables that are used when sending - * coordinates / filling parts of the display. - * @param x Reference variable that will be filled with the currently - * configured starting x coordinate that was provided in the config - * or set by set_offset(), updated for the current rotation. - * @param y Reference variable that will be filled with the currently - * configured starting y coordinate that was provided in the config - * or set by set_offset(), updated for the current rotation. - */ - static void get_offset_rotated(int &x, int &y) { - auto rotation = lv_display_get_rotation(lv_display_get_default()); - switch (rotation) { - case LV_DISPLAY_ROTATION_90: - // intentional fallthrough - case LV_DISPLAY_ROTATION_270: - x = offset_y_; - y = offset_x_; - break; - case LV_DISPLAY_ROTATION_0: - // intentional fallthrough - case LV_DISPLAY_ROTATION_180: - // intentional fallthrough - default: - x = offset_x_; - y = offset_y_; - break; - } - } - -protected: - static inline display_drivers::write_command_fn write_command_; - static inline display_drivers::send_lines_fn lcd_send_lines_; - static inline gpio_num_t reset_pin_; - static inline gpio_num_t dc_pin_; - static inline int offset_x_; - static inline int offset_y_; - static inline bool mirror_x_; - static inline bool mirror_y_; - static inline bool mirror_portrait_; - static inline bool swap_xy_; - static inline bool swap_color_order_; - static inline std::mutex spi_mutex_; }; } // namespace espp diff --git a/components/display_drivers/include/ili9881.hpp b/components/display_drivers/include/ili9881.hpp index eaa7333c7..5e10ca21c 100644 --- a/components/display_drivers/include/ili9881.hpp +++ b/components/display_drivers/include/ili9881.hpp @@ -15,7 +15,7 @@ namespace espp { * with GIP (Gate In Panel) timing control, power management, and gamma correction * for optimal display quality. */ -class Ili9881 { +class Ili9881 : public display_drivers::MipiDbiDisplayDriver { static constexpr uint8_t GS_BIT = 1 << 0; static constexpr uint8_t SS_BIT = 1 << 1; @@ -91,73 +91,45 @@ class Ili9881 { nop_extended = 0xFE, ///< Extended NOP Command }; - /** - * @brief Store config and send initialization commands to the controller. - * @param config display_drivers::Config - */ - static bool initialize(const display_drivers::Config &config) { - write_command_ = config.write_command; - read_command_ = config.read_command; - lcd_send_lines_ = config.lcd_send_lines; - reset_pin_ = config.reset_pin; - dc_pin_ = config.data_command_pin; - offset_x_ = config.offset_x; - offset_y_ = config.offset_y; - mirror_x_ = config.mirror_x; - mirror_y_ = config.mirror_y; - mirror_portrait_ = config.mirror_portrait; - swap_xy_ = config.swap_xy; - swap_color_order_ = config.swap_color_order; + explicit Ili9881(const display_drivers::Config &config) + : MipiDbiDisplayDriver(config, + {.column_address_command = static_cast(Command::caset), + .row_address_command = static_cast(Command::raset), + .memory_write_command = static_cast(Command::ramwr)}) {} - // Initialize display pins - display_drivers::init_pins(reset_pin_, dc_pin_, config.reset_value); + bool initialize() override { + display_drivers::init_pins(config_.reset_pin, config_.data_command_pin, config_.reset_value); - uint8_t madctl = 0x00; - if (swap_color_order_) { - madctl |= LCD_CMD_BGR_BIT; - } - if (mirror_x_) { - madctl |= GS_BIT; // LCD_CMD_MX_BIT; - } - if (mirror_y_) { - madctl |= SS_BIT; // LCD_CMD_MY_BIT; - } - if (swap_xy_) { - madctl |= 0; // LCD_CMD_MV_BIT; - } + auto madctl = make_madctl(DisplayRotation::LANDSCAPE); - uint8_t colmod = 0x55; // default to 16 bits per pixel - switch (config.bits_per_pixel) { - case 16: // RGB565 + uint8_t colmod = 0x55; + switch (config_.bits_per_pixel) { + case 16: colmod = 0x55; break; - case 18: // RGB666 + case 18: colmod = 0x66; break; - case 24: // RGB888 + case 24: colmod = 0x77; break; default: break; } - // first let's read the ID if we have a read_command function - if (config.read_command) { - uint8_t id[3] = {0}; - // select cmd page 1 - write_command_(static_cast(Command::page_select), - std::span{{0x98, 0x81, 0x01}}, 0); - // read ID registers - read_command_(static_cast(0x00), {&id[0], 1}, 0); // ID1 - read_command_(static_cast(0x01), {&id[1], 1}, 0); // ID2 - read_command_(static_cast(0x02), {&id[2], 1}, 0); // ID3 + if (config_.read_command) { + std::array page_1{0x98, 0x81, 0x01}; + std::array id{0}; + write_command(static_cast(Command::page_select), page_1, 0); + read_command(static_cast(0x00), {&id[0], 1}, 0); + read_command(static_cast(0x01), {&id[1], 1}, 0); + read_command(static_cast(0x02), {&id[2], 1}, 0); if (id[0] != 0x98 || id[1] != 0x81 || id[2] != 0x5C) { return false; } } - // Comprehensive ILI9881C initialization sequence (M5Stack Tab5 specific) auto init_commands = std::to_array>({ // CMD_Page 1 - DSI and Basic Setup {static_cast(Command::page_select), @@ -393,207 +365,27 @@ class Ili9881 { // Final DCS commands {static_cast(Command::sleep_out), {}, 120}, // Sleep Out (120ms delay) {static_cast(Command::madctl), {madctl}, 0}, // Memory access control - {static_cast(Command::colmod), {0x55}, 0}, // 16-bit/pixel (RGB565) - {static_cast(Command::display_on), {}, 20}, // Display ON (20ms delay) + {static_cast(Command::colmod), {colmod}, 0}, + {static_cast(Command::display_on), {}, 20}, // Display ON (20ms delay) }); send_commands(init_commands); - return true; } - /** - * @brief Set the display rotation. - */ - static void rotate(const DisplayRotation &rotation) { - uint8_t data = 0x00; - if (swap_color_order_) { - data |= LCD_CMD_BGR_BIT; - } - if (mirror_x_) { - data |= GS_BIT; // LCD_CMD_MX_BIT; - } - if (mirror_y_) { - data |= SS_BIT; // LCD_CMD_MY_BIT; - } - if (swap_xy_) { - data |= 0; // LCD_CMD_MV_BIT; - } - switch (rotation) { - case DisplayRotation::LANDSCAPE: - break; - case DisplayRotation::PORTRAIT: - if (mirror_portrait_) { - data ^= GS_BIT; // (LCD_CMD_MX_BIT | LCD_CMD_MV_BIT); - } else { - data ^= SS_BIT; // (LCD_CMD_MY_BIT | LCD_CMD_MV_BIT); - } - break; - case DisplayRotation::LANDSCAPE_INVERTED: - data ^= GS_BIT | SS_BIT; // (LCD_CMD_MY_BIT | LCD_CMD_MX_BIT); - break; - case DisplayRotation::PORTRAIT_INVERTED: - if (mirror_portrait_) { - data ^= SS_BIT; // (LCD_CMD_MY_BIT | LCD_CMD_MV_BIT); - } else { - data ^= GS_BIT; // (LCD_CMD_MX_BIT | LCD_CMD_MV_BIT); - } - break; - } - - auto lcd_commands = std::to_array>({ - // CMD_Page 0 - User Commands - {static_cast(Command::page_select), - {0x98, 0x81, 0x00}, - 0}, // Switch to Command Page 0 - {static_cast(Command::madctl), {data}, 0}, // Memory access control - }); - send_commands(lcd_commands); - } - - /** - * @brief Flush LVGL area to display. - */ - static void flush(lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { - fill(disp, area, color_map, (1u << (int)display_drivers::Flags::FLUSH_BIT)); - } - - /** - * @brief Set drawing area using an lv_area_t. - */ - static void set_drawing_area(const lv_area_t *area) { - set_drawing_area(area->x1, area->y1, area->x2, area->y2); + void set_rotation(const DisplayRotation &rotation) override { + Controller::set_rotation(rotation); + auto data = std::array{make_madctl(rotation)}; + auto page_0 = std::array{0x98, 0x81, 0x00}; + std::scoped_lock lock(io_mutex_); + write_command(static_cast(Command::page_select), page_0, 0); + write_command(static_cast(Command::madctl), data, 0); } - /** - * @brief Set drawing area using coordinates. - */ - static void set_drawing_area(size_t xs, size_t ys, size_t xe, size_t ye) { - std::array data; - - int offset_x = 0; - int offset_y = 0; - get_offset_rotated(offset_x, offset_y); - - uint16_t start_x = xs + offset_x; - uint16_t end_x = xe + offset_x; - uint16_t start_y = ys + offset_y; - uint16_t end_y = ye + offset_y; - - // column (x) - data[0] = (start_x >> 8) & 0xFF; - data[1] = start_x & 0xFF; - data[2] = (end_x >> 8) & 0xFF; - data[3] = end_x & 0xFF; - write_command_(static_cast(Command::caset), data, 0); - - // row (y) - data[0] = (start_y >> 8) & 0xFF; - data[1] = start_y & 0xFF; - data[2] = (end_y >> 8) & 0xFF; - data[3] = end_y & 0xFF; - write_command_(static_cast(Command::raset), data, 0); - } - - /** - * @brief Fill an area with a color map. - */ - static void fill(lv_display_t *disp, const lv_area_t *area, uint8_t *color_map, - uint32_t flags = 0) { - std::scoped_lock lock{spi_mutex_}; - lv_draw_sw_rgb565_swap(color_map, lv_area_get_width(area) * lv_area_get_height(area)); - if (lcd_send_lines_) { - int offset_x = 0; - int offset_y = 0; - get_offset_rotated(offset_x, offset_y); - lcd_send_lines_(area->x1 + offset_x, area->y1 + offset_y, area->x2 + offset_x, - area->y2 + offset_y, color_map, flags); - } else { - set_drawing_area(area); - uint32_t size = lv_area_get_width(area) * lv_area_get_height(area); - write_command_(static_cast(Command::ramwr), {color_map, size * 2}, flags); - } +private: + uint8_t make_madctl(DisplayRotation rotation) const { + auto value = display_drivers::make_madctl_base(config_, LCD_CMD_BGR_BIT, GS_BIT, SS_BIT, 0); + return display_drivers::apply_standard_rotation(value, config_, rotation, GS_BIT, SS_BIT, 0); } - - /** - * @brief Clear a rectangular region to a color. - */ - static void clear(size_t x, size_t y, size_t width, size_t height, uint16_t color = 0x0000) { - set_drawing_area(x, y, x + width, y + height); - - uint32_t size = width * height; - static constexpr int max_words = 1024; - uint16_t color_words[max_words]; - for (int i = 0; i < max_words; i++) - color_words[i] = color; - for (uint32_t i = 0; i < size; i += max_words) { - uint32_t chunk = std::min(size - i, max_words); - write_command_(static_cast(Command::ramwr), - {reinterpret_cast(color_words), chunk * 2}, 0); - } - } - - /** - * @brief Send a list of initialization/display commands. - */ - static void send_commands(std::span> commands) { - using namespace std::chrono_literals; - for (const auto &[cmd, params, delay_ms] : commands) { - std::scoped_lock lock{spi_mutex_}; - write_command_(cmd, params, 0); - std::this_thread::sleep_for(delay_ms * 1ms); - } - } - - /** - * @brief Set top-left pixel offset. - */ - static void set_offset(int x, int y) { - offset_x_ = x; - offset_y_ = y; - } - - /** - * @brief Get offset. - */ - static void get_offset(int &x, int &y) { - x = offset_x_; - y = offset_y_; - } - - /** - * @brief Get offset, adjusted for rotation. - */ - static void get_offset_rotated(int &x, int &y) { - auto rotation = lv_display_get_rotation(lv_display_get_default()); - switch (rotation) { - case LV_DISPLAY_ROTATION_90: - case LV_DISPLAY_ROTATION_270: - x = offset_y_; - y = offset_x_; - break; - case LV_DISPLAY_ROTATION_0: - case LV_DISPLAY_ROTATION_180: - default: - x = offset_x_; - y = offset_y_; - break; - } - } - -protected: - static inline display_drivers::write_command_fn write_command_; - static inline display_drivers::read_command_fn read_command_; - static inline display_drivers::send_lines_fn lcd_send_lines_; - static inline gpio_num_t reset_pin_; - static inline gpio_num_t dc_pin_; - static inline int offset_x_; - static inline int offset_y_; - static inline bool mirror_x_; - static inline bool mirror_y_; - static inline bool mirror_portrait_; - static inline bool swap_xy_; - static inline bool swap_color_order_; - static inline std::mutex spi_mutex_; }; } // namespace espp diff --git a/components/display_drivers/include/sh8601.hpp b/components/display_drivers/include/sh8601.hpp index f54cad14e..61ff23484 100644 --- a/components/display_drivers/include/sh8601.hpp +++ b/components/display_drivers/include/sh8601.hpp @@ -1,348 +1,125 @@ #pragma once -#include - #include "display_drivers.hpp" namespace espp { /** * @brief Display driver for the SH8601 display controller. * - * This code is based off this datasheet: - * https://dl.espressif.com/AE/esp-iot-solution/SH8601A0_DataSheet_Preliminary_V0.0_UCS__191107_1_.pdf - * * \section t_encoder_pro_cfg SmartKnob Config * \snippet display_drivers_example.cpp t_encoder_pro_config example * \section sh8601_ex1 Sh8601 Example * \snippet display_drivers_example.cpp display_drivers example */ -class Sh8601 { +class Sh8601 : public display_drivers::MipiDbiDisplayDriver { public: - // Initial bytes for all transactions, indicating whether the data is being sent over just MOSI or - // all QSPI lines. enum class TransferMode : uint8_t { SINGLE_LINE = 0x02, MULTI_LINE = 0x32, }; enum class Command : uint8_t { - nop = 0x00, // no operation - swreset = 0x01, // software reset - rddid = 0x04, // read display id - rddst = 0x09, // read display status - - slpin = 0x10, // sleep in - slpout = 0x11, // sleep out - ptlon = 0x12, // partial mode on - noron = 0x13, // normal display mode on - - invoff = 0x20, // display inversion off - invon = 0x21, // display inversion on - - dispoff = 0x28, // display off - dispon = 0x29, // display on - - caset = 0x2a, // column address set - paset = 0x2b, // page address set - ramwr = 0x2c, // ram write - - ptlar = 0x30, // partial area - vscrdef = 0x33, // vertical scrolling definition - teoff = 0x34, // tearing effect line off - teon = 0x35, // tearing effect line on - madctl = 0x36, // memory access control - idmoff = 0x38, // idle mode off - idmon = 0x39, // idle mode on - colmod = 0x3a, // color mode - pixel format - ramwrc = 0x3c, // memory write continue - - settes = 0x44, // set tear scanline - gettes = 0x45, // get tear scanline - - wrdpbr = 0x51, // write display brightness - - wrctrldp = 0x53, // write CTRL display - - readid1 = 0xDA, // read ID 1 - readid2 = 0xDB, // read ID 2 - readid3 = 0xDC, // read ID 3 - - rgbctrl = 0xb0, // rgb control - porctrl = 0xb5, // porch control - dpfuctrl = 0xb6, // display function control - - tectrl = 0xba, // ram control - - intrctrl = 0xf6, // interface control - - frctrl = 0xe8, // frame rate control - spi2dctrl = 0xe9, // spi 2data control - - pwrctrl1 = 0xc1, // power control 1 - pwrctrl2 = 0xc3, // power control 2 - pwrctrl3 = 0xc4, // power control 3 - pwrctrl4 = 0xc9, // power control 4 - pwrctrl7 = 0xa7, // power control 7 - - intren1 = 0xfe, // inter register enable 1 - intren2 = 0xef, // inter register enable 2 - - stgamma1 = 0xf0, // set gamma 1 - stgamma2 = 0xf1, // set gamma 2 - stgamma3 = 0xf2, // set gamma 3 - stgamma4 = 0xf3, // set gamma 4 + nop = 0x00, + swreset = 0x01, + rddid = 0x04, + rddst = 0x09, + slpin = 0x10, + slpout = 0x11, + ptlon = 0x12, + noron = 0x13, + invoff = 0x20, + invon = 0x21, + dispoff = 0x28, + dispon = 0x29, + caset = 0x2a, + paset = 0x2b, + ramwr = 0x2c, + ptlar = 0x30, + vscrdef = 0x33, + teoff = 0x34, + teon = 0x35, + madctl = 0x36, + idmoff = 0x38, + idmon = 0x39, + colmod = 0x3a, + ramwrc = 0x3c, + settes = 0x44, + gettes = 0x45, + wrdpbr = 0x51, + wrctrldp = 0x53, + readid1 = 0xDA, + readid2 = 0xDB, + readid3 = 0xDC, + rgbctrl = 0xb0, + porctrl = 0xb5, + dpfuctrl = 0xb6, + tectrl = 0xba, + intrctrl = 0xf6, + frctrl = 0xe8, + spi2dctrl = 0xe9, + pwrctrl1 = 0xc1, + pwrctrl2 = 0xc3, + pwrctrl3 = 0xc4, + pwrctrl4 = 0xc9, + pwrctrl7 = 0xa7, + intren1 = 0xfe, + intren2 = 0xef, + stgamma1 = 0xf0, + stgamma2 = 0xf1, + stgamma3 = 0xf2, + stgamma4 = 0xf3, }; - /** - * @brief Store the config data and send the initialization commands to the - * display controller. - * @param config display_drivers::Config structure - */ - static void initialize(const display_drivers::Config &config) { - // update the static members - write_command_ = config.write_command; - lcd_send_lines_ = config.lcd_send_lines; - reset_pin_ = config.reset_pin; - dc_pin_ = config.data_command_pin; - offset_x_ = config.offset_x; - offset_y_ = config.offset_y; - mirror_x_ = config.mirror_x; - mirror_y_ = config.mirror_y; - mirror_portrait_ = config.mirror_portrait; - swap_xy_ = config.swap_xy; - swap_color_order_ = config.swap_color_order; - // Initialize display pins - display_drivers::init_pins(reset_pin_, dc_pin_, config.reset_value); + explicit Sh8601(const display_drivers::Config &config) + : MipiDbiDisplayDriver(config, + {.column_address_command = static_cast(Command::caset), + .row_address_command = static_cast(Command::paset), + .memory_write_command = static_cast(Command::ramwr)}) {} + bool initialize() override { + display_drivers::init_pins(config_.reset_pin, config_.data_command_pin, config_.reset_value); auto init_cmds = std::to_array>({ - {Command::slpout, {}, 120}, // sleep out - {Command::noron}, // normal mode - {config.invert_colors ? Command::invon : Command::invoff}, // inversion + {Command::slpout, {}, 120}, + {Command::noron}, + {config_.invert_colors ? Command::invon : Command::invoff}, #ifdef CONFIG_LV_COLOR_DEPTH_16 - {Command::colmod, {0x05}}, // color mode 16 bit + {Command::colmod, + { 0x05 }}, #else - {Command::colmod, {0x07}}, // color mode 24 bit + {Command::colmod, {0x07}}, #endif - - {Command::dispon}, // display on - {Command::wrctrldp, {0x28}}, // write CTRL display - {Command::wrdpbr, {0xFF}, 10}, // brightness normal mode // end of commands + {Command::dispon}, + {Command::wrctrldp, {0x28}}, + {Command::wrdpbr, {0xFF}, 10}, }); - - // send the init commands send_commands(init_cmds); + return true; } - /** - * @brief Set the display rotation. - * @param rotation New display rotation. - */ - static void rotate(const DisplayRotation &rotation) { - uint8_t data = 0; - if (swap_color_order_) { // cppcheck-suppress knownConditionTrueFalse - data |= LCD_CMD_BGR_BIT; - } - if (mirror_x_) { // cppcheck-suppress knownConditionTrueFalse - data |= LCD_CMD_MX_BIT; - } - if (mirror_y_) { // cppcheck-suppress knownConditionTrueFalse - data |= LCD_CMD_MY_BIT; - } - if (swap_xy_) { // cppcheck-suppress knownConditionTrueFalse - data |= LCD_CMD_MV_BIT; - } - - switch (rotation) { - case DisplayRotation::LANDSCAPE: - break; - case DisplayRotation::PORTRAIT: - // flip the mx and mv bits (xor) - if (mirror_portrait_) { // cppcheck-suppress knownConditionTrueFalse - data ^= (LCD_CMD_MX_BIT | LCD_CMD_MV_BIT); - } else { - data ^= (LCD_CMD_MY_BIT | LCD_CMD_MV_BIT); - } - break; - case DisplayRotation::LANDSCAPE_INVERTED: - // flip the my and mx bits (xor) - data ^= (LCD_CMD_MY_BIT | LCD_CMD_MX_BIT); - break; - case DisplayRotation::PORTRAIT_INVERTED: - // flip the my and mv bits (xor) - if (mirror_portrait_) { // cppcheck-suppress knownConditionTrueFalse - data ^= (LCD_CMD_MY_BIT | LCD_CMD_MV_BIT); - } else { - data ^= (LCD_CMD_MX_BIT | LCD_CMD_MV_BIT); - } - break; - } - std::scoped_lock lock{spi_mutex_}; - write_command_(static_cast(Command::madctl), {&data, 1}, 0); + void set_rotation(const DisplayRotation &rotation) override { + Controller::set_rotation(rotation); + auto data = std::array{make_madctl(rotation)}; + std::scoped_lock lock(io_mutex_); + write_command(static_cast(Command::madctl), data, 0); } - /** - * @brief Flush the pixel data for the provided area to the display. - * @param *disp Pointer to the LVGL display. - * @param *area Pointer to the structure describing the pixel area. - * @param *color_map Pointer to array of colors to flush to the display. - */ - static void flush(lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { - // No need to swap the colors when in RGB888 mode -#if LV_COLOR_DEPTH == 16 - lv_draw_sw_rgb565_swap(color_map, lv_area_get_width(area) * lv_area_get_height(area)); -#endif - - int offset_x = 0; - int offset_y = 0; - get_offset_rotated(offset_x, offset_y); - - lcd_send_lines_(area->x1 + offset_x, area->y1 + offset_y, area->x2 + offset_x, - area->y2 + offset_y, color_map, - (1 << static_cast(display_drivers::Flags::FLUSH_BIT))); - } - - /** - * @brief Set the drawing area for the display, resets the cursor to the - * starting position of the area. - * @param area Pointer to lv_area_t strcuture with start/end x/y - * coordinates. - */ - static void set_drawing_area(const lv_area_t *area) { - set_drawing_area(area->x1, area->y1, area->x2, area->y2); - } - - /** - * @brief Set the drawing area for the display, resets the cursor to the - * starting position of the area. - * @param xs Starting x coordinate of the area. - * @param ys Starting y coordinate of the area. - * @param xe Ending x coordinate of the area. - * @param ye Ending y coordinate of the area. - */ - static void set_drawing_area(size_t xs, size_t ys, size_t xe, size_t ye) { - std::array data; - - int offset_x = 0; - int offset_y = 0; - get_offset_rotated(offset_x, offset_y); - - const uint16_t start_x = xs + offset_x; - const uint16_t end_x = xe + offset_x; - const uint16_t start_y = ys + offset_y; - const uint16_t end_y = ye + offset_y; - - // Set the column (x) start / end addresses - data[0] = (start_x >> 8) & 0xFF; - data[1] = start_x & 0xFF; - data[2] = (end_x >> 8) & 0xFF; - data[3] = end_x & 0xFF; - std::scoped_lock lock{spi_mutex_}; - write_command_(static_cast(Command::caset), data, 0); - - // Set the row (y) start / end addresses - data[0] = (start_y >> 8) & 0xFF; - data[1] = start_y & 0xFF; - data[2] = (end_y >> 8) & 0xFF; - data[3] = end_y & 0xFF; - write_command_(static_cast(Command::paset), data, 0); - } - - static void send_commands(std::span> commands) { - using namespace std::chrono_literals; - for (const auto &[command, parameters, delay_ms] : commands) { - std::scoped_lock lock{spi_mutex_}; - write_command_(static_cast(command), parameters, 0); - std::this_thread::sleep_for(delay_ms * 1ms); - } - } - - /** - * @brief Set the offset (upper left starting coordinate) of the display. - * @note This modifies internal variables that are used when sending - * coordinates / filling parts of the display. - * @param x New starting x coordinate (so writing to x address 0 later will - * actually write to this offset). - * @param y New starting y coordinate (so writing to y address 0 later will - * actually write to this offset). - */ - static void set_offset(int x, int y) { - offset_x_ = x; - offset_y_ = y; + void set_brightness(float brightness) { + brightness_ = std::clamp(brightness, 0.0f, 1.0f); + uint16_t value = brightness_ * 1023; + write_command(static_cast(Command::wrdpbr), + {reinterpret_cast(&value), 2}, 0); } - /** - * @brief Get the offset (upper left starting coordinate) of the display. - * @note This returns internal variables that are used when sending - * coordinates / filling parts of the display. - * @param x Reference variable that will be filled with the currently - * configured starting x coordinate that was provided in the config - * or set by set_offset(). - * @param y Reference variable that will be filled with the currently - * configured starting y coordinate that was provided in the config - * or set by set_offset(). - */ - static void get_offset(int &x, int &y) { - x = offset_x_; - y = offset_y_; - } + float get_brightness() const { return brightness_; } - /** - * @brief Get the offset (upper left starting coordinate) of the display - * after rotation. - * @note This returns internal variables that are used when sending - * coordinates / filling parts of the display. - * @param x Reference variable that will be filled with the currently - * configured starting x coordinate that was provided in the config - * or set by set_offset(), updated for the current rotation. - * @param y Reference variable that will be filled with the currently - * configured starting y coordinate that was provided in the config - * or set by set_offset(), updated for the current rotation. - */ - static void get_offset_rotated(int &x, int &y) { - auto rotation = lv_display_get_rotation(lv_display_get_default()); - switch (rotation) { - case LV_DISPLAY_ROTATION_90: - // intentional fallthrough - case LV_DISPLAY_ROTATION_270: - x = offset_y_; - y = offset_x_; - break; - case LV_DISPLAY_ROTATION_0: - // intentional fallthrough - case LV_DISPLAY_ROTATION_180: - // intentional fallthrough - default: - x = offset_x_; - y = offset_y_; - break; - } +private: + uint8_t make_madctl(DisplayRotation rotation) const { + auto value = display_drivers::make_madctl_base(config_, LCD_CMD_BGR_BIT, LCD_CMD_MX_BIT, + LCD_CMD_MY_BIT, LCD_CMD_MV_BIT); + return display_drivers::apply_standard_rotation(value, config_, rotation, LCD_CMD_MX_BIT, + LCD_CMD_MY_BIT, LCD_CMD_MV_BIT); } - static void set_brightness(const float brightness) { - // Update the local brightness value - brightness_ = brightness; - - // This display has a 10-bit brightness control - uint16_t data = brightness * 1023; - write_command_(static_cast(Command::wrdpbr), {reinterpret_cast(&data), 2}, - 0); - } - - static float get_brightness() { return brightness_; } - -protected: - static inline display_drivers::write_command_fn write_command_; - static inline display_drivers::send_lines_fn lcd_send_lines_; - static inline gpio_num_t reset_pin_; - static inline gpio_num_t dc_pin_; - static inline int offset_x_; - static inline int offset_y_; - static inline bool mirror_x_ = false; - static inline bool mirror_y_ = false; - static inline bool mirror_portrait_ = false; - static inline bool swap_xy_ = false; - static inline bool swap_color_order_ = false; - static inline std::mutex spi_mutex_; - static inline float brightness_ = 0; + float brightness_{0.0f}; }; } // namespace espp diff --git a/components/display_drivers/include/spi_panel_io.hpp b/components/display_drivers/include/spi_panel_io.hpp new file mode 100644 index 000000000..6cc423226 --- /dev/null +++ b/components/display_drivers/include/spi_panel_io.hpp @@ -0,0 +1,115 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "base_component.hpp" +#include "display_drivers.hpp" +#include "spi.hpp" + +namespace espp { +/// @brief LCD-style command/data helper built on top of `Spi`. +/// @details +/// `queue_command()` transmits with D/C low. `queue_data()` and +/// `queue_pixels()` always transmit with D/C high, so callers should treat them +/// as payload helpers rather than manually setting the D/C bit for normal panel +/// data transactions. +class SpiPanelIo : public display_drivers::PanelIo, public BaseComponent { +public: + /// @brief IRQ-safe callback invoked after matching queued transactions finish. + using post_transaction_callback_t = void (*)(uint32_t user_flags); + + /// @brief Configuration for `SpiPanelIo`. + struct Config { + Spi *spi = nullptr; ///< Bus on which to register the display device. + Spi::DeviceConfig device_config{}; ///< SPI device configuration. + gpio_num_t data_command_io = GPIO_NUM_NC; ///< D/C GPIO pin. + uint32_t data_command_bit_mask = + 1u << static_cast(display_drivers::Flags::DC_LEVEL_BIT); ///< User bit that + ///< selects data mode. + uint32_t post_transaction_callback_bit_mask = 0; ///< User bit mask that triggers the callback. + post_transaction_callback_t post_transaction_callback = + nullptr; ///< Optional IRQ-safe callback. + uint32_t pixel_data_flags = + SPI_TRANS_DMA_BUFFER_ALIGN_MANUAL; ///< Flags used for pixel payload transfers. + uint32_t timeout_ms = 10; ///< Queue/result timeout in milliseconds. + Logger::Verbosity log_level = Logger::Verbosity::WARN; ///< Logger verbosity. + }; + + /// @brief Construct a panel-I/O helper for SPI command/data displays. + /// @param config Panel transport configuration. + explicit SpiPanelIo(const Config &config); + + /// @brief Check whether the helper has a registered SPI device. + /// @return True if initialization succeeded. + bool initialized() const override; + + /// @brief Get the underlying SPI device wrapper. + /// @return Shared pointer to the attached SPI device. + std::shared_ptr device() const; + + /// @brief Wait for all queued transactions to complete. + void wait() override; + + /// @brief Send a command byte and optional parameter payload. + /// @param command Command byte sent with D/C low. + /// @param parameters Optional payload bytes sent with D/C high. + /// @param user_flags Additional user-defined flags passed to callbacks. + void write_command(uint8_t command, std::span parameters, + uint32_t user_flags = 0) override; + + /// @brief Queue a command transaction with D/C low. + /// @param command Command byte to transmit. + /// @param user_flags Additional user-defined flags passed to callbacks. + void queue_command(uint8_t command, uint32_t user_flags = 0) override; + + /// @brief Queue a non-pixel data payload with D/C high. + /// @param data Data bytes to transmit. + /// @param user_flags Additional user-defined flags passed to callbacks. + void queue_data(std::span data, uint32_t user_flags = 0) override; + + /// @brief Queue a pixel payload with D/C high. + /// @param data Pointer to the pixel buffer. + /// @param size Pixel payload size in bytes. + /// @param user_flags Additional user-defined flags passed to callbacks. + /// @param transaction_flags Optional SPI transaction flags overriding the default pixel flags. + void queue_pixels(const uint8_t *data, size_t size, uint32_t user_flags = 0, + uint32_t transaction_flags = 0) override; + +private: + struct TransactionContext { + SpiPanelIo *helper{nullptr}; + uint32_t user_flags{0}; + }; + + static void pre_transfer_callback(spi_transaction_t *transaction); + static void post_transfer_callback(spi_transaction_t *transaction); + + TickType_t timeout_ticks() const; + size_t prepare_transaction(uint32_t user_flags); + void queue_command_locked(uint8_t command, uint32_t user_flags); + void queue_data_locked(std::span data, uint32_t user_flags); + void queue_pixels_locked(const uint8_t *data, size_t size, uint32_t user_flags, + uint32_t transaction_flags); + void queue_transaction_locked(size_t index); + void wait_locked(); + + Config config_; + std::shared_ptr device_{}; + std::mutex mutex_; + std::vector transactions_{}; + std::vector contexts_{}; + int queued_transactions_{0}; +}; + +/// @brief Backward-compatible alias for the older SPI display transport name. +using SpiCommandData = SpiPanelIo; +} // namespace espp diff --git a/components/display_drivers/include/ssd1351.hpp b/components/display_drivers/include/ssd1351.hpp index 9c6392d76..7a02f084a 100644 --- a/components/display_drivers/include/ssd1351.hpp +++ b/components/display_drivers/include/ssd1351.hpp @@ -1,461 +1,184 @@ #pragma once -#include -#include - #include "display_drivers.hpp" namespace espp { /** * @brief Display driver for the SSD1351 128x128 RGB OLED display controller. * - * This code is modified from - * https://github.com/adafruit/Adafruit-SSD1351-library/blob/master/Adafruit_SSD1351.cpp - * and - * https://github.com/lvgl/lvgl_esp32_drivers/blob/master/lvgl_tft/ssd1306.c - * and - * https://github.com/rdagger/micropython-ssd1351/blob/master/ssd1351.py - * and - * https://github.com/Bodmer/TFT_eSPI/blob/master/TFT_Drivers/SSD1351_Defines.h - * - * Datasheet can be found here: - * https://cdn-shop.adafruit.com/datasheets/SSD1351-Revision+1.3.pdf - * * \section ssd1351_byte90_cfg Byte90 Ssd1351 Config * \snippet display_drivers_example.cpp byte90_config example * \section ssd1351_ex1 ssd1351 Example * \snippet display_drivers_example.cpp display_drivers example */ -class Ssd1351 { +class Ssd1351 : public display_drivers::MipiDbiDisplayDriver { public: - /** - * @brief Enum for Command values used by the SSD1351 display controller. - */ enum class Command : uint8_t { - caset = 0x15, ///< Column address set - raset = 0x75, ///< Row address set - ramwr = 0x5C, ///< RAM write - ramrd = 0x5D, ///< RAM read - - madctl = 0xA0, ///< Memory data access control - - STARTLINE = 0xA1, ///< Vertical Scroll by RAM - - DISPLAYOFFSET = 0xA2, ///< Vertical Scroll by row (locked) - DISPLAYALLOFF = 0xA4, ///< All pixels off - DISPLAYALLON = 0xA5, ///< All pixels on (all pixels have GS63) - NORMALDISPLAY = 0xA6, ///< Normal display mode - INVERTDISPLAY = 0xA7, ///< Inverted display mode - - FUNCTIONSELECT = 0xAB, ///< See datasheet - - DISPLAYOFF = 0xAE, ///< Sleep Mode On - DISPLAYON = 0xAF, ///< Sleep Mode Off - - PRECHARGE = 0xB1, ///< See datasheet - DISPLAYENHANCE = 0xB2, ///< Not currently used - CLOCKDIV = 0xB3, ///< See datasheet - SETVSL = 0xB4, ///< See datasheet - SETGPIO = 0xB5, ///< See datasheet - PRECHARGE2 = 0xB6, ///< See datasheet - SETGRAY = 0xB8, ///< Not currently used - USELUT = 0xB9, ///< Not currently used - PRECHARGELEVEL = 0xBB, ///< Not currently used - VCOMH = 0xBE, ///< See datasheet - CONTRASTABC = 0xC1, ///< See datasheet - CONTRASTMASTER = 0xC7, ///< See datasheet - MUXRATIO = 0xCA, ///< See datasheet - HORIZSCROLL = 0x96, ///< Not currently used - STOPSCROLL = 0x9E, ///< Not currently used - STARTSCROLL = 0x9F, ///< Not currently used - - nop0 = 0xD1, ///< No operation (0) - nop1 = 0xE3, ///< No operation (1) - - COMMANDLOCK = 0xFD, ///< Command lock and MCU protection status + caset = 0x15, + raset = 0x75, + ramwr = 0x5C, + ramrd = 0x5D, + madctl = 0xA0, + STARTLINE = 0xA1, + DISPLAYOFFSET = 0xA2, + DISPLAYALLOFF = 0xA4, + DISPLAYALLON = 0xA5, + NORMALDISPLAY = 0xA6, + INVERTDISPLAY = 0xA7, + FUNCTIONSELECT = 0xAB, + DISPLAYOFF = 0xAE, + DISPLAYON = 0xAF, + PRECHARGE = 0xB1, + DISPLAYENHANCE = 0xB2, + CLOCKDIV = 0xB3, + SETVSL = 0xB4, + SETGPIO = 0xB5, + PRECHARGE2 = 0xB6, + SETGRAY = 0xB8, + USELUT = 0xB9, + PRECHARGELEVEL = 0xBB, + VCOMH = 0xBE, + CONTRASTABC = 0xC1, + CONTRASTMASTER = 0xC7, + MUXRATIO = 0xCA, + HORIZSCROLL = 0x96, + STOPSCROLL = 0x9E, + STARTSCROLL = 0x9F, + nop0 = 0xD1, + nop1 = 0xE3, + COMMANDLOCK = 0xFD, }; - // madctl bits: - // 6,7 Color depth (01 = 64K) - // 5 Odd/even split COM (0: disable, 1: enable) - split COM - // 4 Scan direction (0: top-down, 1: bottom-up) - mirror Y - // 3 Reserved - always 0 - // 2 Color remap (0: A->B->C, 1: C->B->A) - BGR color order - // 1 Column remap (0: 0-127, 1: 127-0) - mirror X - // 0 Address increment (0: horizontal, 1: vertical) - swap X and Y - static constexpr int OLED_CMD_COLOR_DEPTH_65K1 = 0b00 << 6; // 65K color mode - static constexpr int OLED_CMD_COLOR_DEPTH_65K2 = 0b01 << 6; // 65K color mode - static constexpr int OLED_CMD_COLOR_DEPTH_262K1 = 0b10 << 6; // 262K color mode - static constexpr int OLED_CMD_COLOR_DEPTH_262K2 = 0b11 << 6; // 262K color mode, 16-bit format 2 - static constexpr int OLED_CMD_COM_SPLIT = 0b1 << 5; // Odd/even split COM - static constexpr int OLED_CMD_MY_BIT = 0b1 << 4; // Mirror Y - static constexpr int OLED_CMD_BGR_BIT = 0b1 << 2; // BGR color order - static constexpr int OLED_CMD_MX_BIT = 0b1 << 1; // Mirror X - static constexpr int OLED_CMD_MV_BIT = 0b1 << 0; // Swap X and Y - - static constexpr int DEFAULT_MADCTL = - OLED_CMD_COLOR_DEPTH_65K2 | OLED_CMD_COM_SPLIT; ///< Default MADCTL value - - /** - * @brief Store the config data and send the initialization commands to the - * display controller. - * @param config display_drivers::Config structure - */ - static void initialize(const display_drivers::Config &config) { - // update the static members - write_command_ = config.write_command; - lcd_send_lines_ = config.lcd_send_lines; - reset_pin_ = config.reset_pin; - dc_pin_ = config.data_command_pin; - offset_x_ = config.offset_x; - offset_y_ = config.offset_y; - mirror_x_ = config.mirror_x; - mirror_y_ = config.mirror_y; - mirror_portrait_ = config.mirror_portrait; - swap_xy_ = config.swap_xy; - swap_color_order_ = config.swap_color_order; - - // Initialize display pins - display_drivers::init_pins(reset_pin_, dc_pin_, config.reset_value); - - uint8_t madctl = DEFAULT_MADCTL; // Default MADCTL value - if (swap_color_order_) { - madctl |= OLED_CMD_BGR_BIT; - } - if (mirror_x_) { - madctl |= OLED_CMD_MX_BIT; - } - if (mirror_y_) { - madctl |= OLED_CMD_MY_BIT; - } - if (swap_xy_) { - madctl |= OLED_CMD_MV_BIT; - } - - // set up the init commands + static constexpr int OLED_CMD_COLOR_DEPTH_65K2 = 0b01 << 6; + static constexpr int OLED_CMD_COM_SPLIT = 0b1 << 5; + static constexpr int OLED_CMD_MY_BIT = 0b1 << 4; + static constexpr int OLED_CMD_BGR_BIT = 0b1 << 2; + static constexpr int OLED_CMD_MX_BIT = 0b1 << 1; + static constexpr int OLED_CMD_MV_BIT = 0b1 << 0; + static constexpr int DEFAULT_MADCTL = OLED_CMD_COLOR_DEPTH_65K2 | OLED_CMD_COM_SPLIT; + + explicit Ssd1351(const display_drivers::Config &config) + : MipiDbiDisplayDriver(config, + {.column_address_command = static_cast(Command::caset), + .row_address_command = static_cast(Command::raset), + .memory_write_command = static_cast(Command::ramwr), + .use_8bit_coordinates = true}) {} + + bool initialize() override { + display_drivers::init_pins(config_.reset_pin, config_.data_command_pin, config_.reset_value); + + auto madctl = make_madctl(DisplayRotation::LANDSCAPE); auto init_commands = std::to_array>({ - // Command lock - unlock OLED driver IC MCU interface - {(uint8_t)Command::COMMANDLOCK, {0x12}, 0}, // Unlock commands - {(uint8_t)Command::COMMANDLOCK, {0xB1}, 0}, // Make commands A2,B1,B3,BB,BE,C1 accessible - - // Display off - {(uint8_t)Command::DISPLAYOFF, {}, 0}, - - // Clock divider and oscillator frequency - {(uint8_t)Command::CLOCKDIV, {0xF1}, 0}, // 7:4 = Oscillator Frequency, 3:0 = CLK Div Ratio - - // Multiplex ratio (128 lines) - {(uint8_t)Command::MUXRATIO, {0x7F}, 0}, // 127 (128-1) - - {(uint8_t)Command::DISPLAYOFFSET, {0x00}, 0}, // Set display offset to 0 - {(uint8_t)Command::SETGPIO, {0x00}, 0}, // Set GPIO to 0 (disable) - {(uint8_t)Command::FUNCTIONSELECT, {0x01}, 0}, // Internal VDD regulator - {(uint8_t)Command::PRECHARGE, {0x32}, 0}, // Set precharge speed - {(uint8_t)Command::VCOMH, {0x05}, 0}, // Set VCOMH voltage - - // // Normal or inverted display - {config.invert_colors ? (uint8_t)Command::INVERTDISPLAY : (uint8_t)Command::NORMALDISPLAY, + {static_cast(Command::COMMANDLOCK), {0x12}, 0}, + {static_cast(Command::COMMANDLOCK), {0xB1}, 0}, + {static_cast(Command::DISPLAYOFF), {}, 0}, + {static_cast(Command::CLOCKDIV), {0xF1}, 0}, + {static_cast(Command::MUXRATIO), {0x7F}, 0}, + {static_cast(Command::DISPLAYOFFSET), {0x00}, 0}, + {static_cast(Command::SETGPIO), {0x00}, 0}, + {static_cast(Command::FUNCTIONSELECT), {0x01}, 0}, + {static_cast(Command::PRECHARGE), {0x32}, 0}, + {static_cast(Command::VCOMH), {0x05}, 0}, + {config_.invert_colors ? static_cast(Command::INVERTDISPLAY) + : static_cast(Command::NORMALDISPLAY), {}, 0}, - - {(uint8_t)Command::CONTRASTABC, {0xC8, 0x80, 0xC8}, 0}, // Set contrast for colors A, B, C - {(uint8_t)Command::CONTRASTMASTER, {0x0F}, 0}, // Set master contrast - {(uint8_t)Command::SETVSL, {0xA0, 0xB5, 0x55}, 0}, // Set VSL voltage - {(uint8_t)Command::PRECHARGE2, {0x01}, 0}, // Set precharge 2 speed - {(uint8_t)Command::PRECHARGELEVEL, {0x1C, 0x1C, 0x1C}, 0}, // Set precharge level - - // Display on - {(uint8_t)Command::DISPLAYON, {}, 0}, - - // Set remap and color depth - {(uint8_t)Command::madctl, {madctl}, 0}, - - // Set display start line - {(uint8_t)Command::STARTLINE, {0x00}, 0}, + {static_cast(Command::CONTRASTABC), {0xC8, 0x80, 0xC8}, 0}, + {static_cast(Command::CONTRASTMASTER), {0x0F}, 0}, + {static_cast(Command::SETVSL), {0xA0, 0xB5, 0x55}, 0}, + {static_cast(Command::PRECHARGE2), {0x01}, 0}, + {static_cast(Command::PRECHARGELEVEL), {0x1C, 0x1C, 0x1C}, 0}, + {static_cast(Command::DISPLAYON), {}, 0}, + {static_cast(Command::madctl), {madctl}, 0}, + {static_cast(Command::STARTLINE), {0x00}, 0}, }); - // send the init commands send_commands(init_commands); + return true; } - /** - * @brief Set the display rotation. - * @param rotation New display rotation. - */ - static void rotate(const DisplayRotation &rotation) { - uint8_t madctl = DEFAULT_MADCTL; // Default MADCTL value + void set_rotation(const DisplayRotation &rotation) override { + Controller::set_rotation(rotation); + + auto madctl = make_madctl(rotation); uint8_t startline = 0; - if (swap_color_order_) { - madctl |= OLED_CMD_BGR_BIT; - } - if (mirror_x_) { - madctl |= OLED_CMD_MX_BIT; - } - if (mirror_y_) { - madctl |= OLED_CMD_MY_BIT; - } - if (swap_xy_) { - madctl |= OLED_CMD_MV_BIT; - } switch (rotation) { - case DisplayRotation::LANDSCAPE: { - // set startline to HEIGHT - startline = 127; - std::scoped_lock lock{spi_mutex_}; - write_command_(static_cast(Command::STARTLINE), {&startline, 1}, 0); - } break; - case DisplayRotation::PORTRAIT: { - // flip the mv bit (xor) - madctl ^= (OLED_CMD_MV_BIT); - if (mirror_portrait_) { - // flip the my bit (xor) - madctl ^= (OLED_CMD_MY_BIT); - } else { - // flip the mx bit (xor) - madctl ^= OLED_CMD_MX_BIT; - } - // set startline to WIDTH + case DisplayRotation::LANDSCAPE: + case DisplayRotation::PORTRAIT: startline = 127; - std::scoped_lock lock{spi_mutex_}; - write_command_(static_cast(Command::STARTLINE), {&startline, 1}, 0); - } break; - case DisplayRotation::LANDSCAPE_INVERTED: { - // flip the my and mx bits (xor) - madctl ^= (OLED_CMD_MY_BIT | OLED_CMD_MX_BIT); - // set startline to 0 - startline = 0; - std::scoped_lock lock{spi_mutex_}; - write_command_(static_cast(Command::STARTLINE), {&startline, 1}, 0); - } break; - case DisplayRotation::PORTRAIT_INVERTED: { - // flip the mv bit (xor) - madctl ^= (OLED_CMD_MV_BIT); - if (mirror_portrait_) { - // flip the mx bit (xor) - madctl ^= OLED_CMD_MX_BIT; - } else { - // flip the my bit (xor) - madctl ^= (OLED_CMD_MY_BIT); - } - // set startline to 0 + break; + case DisplayRotation::LANDSCAPE_INVERTED: + case DisplayRotation::PORTRAIT_INVERTED: startline = 0; - std::scoped_lock lock{spi_mutex_}; - write_command_(static_cast(Command::STARTLINE), {&startline, 1}, 0); - } break; + break; } - auto madctl_data = std::array{madctl}; - std::scoped_lock lock{spi_mutex_}; - write_command_(static_cast(Command::madctl), madctl_data, 0); - } - /** - * @brief Flush the pixel data for the provided area to the display. - * @param *disp Pointer to the LVGL display. - * @param *area Pointer to the structure describing the pixel area. - * @param *color_map Pointer to array of colors to flush to the display. - */ - static void flush(lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { - fill(disp, area, color_map, (1 << (int)display_drivers::Flags::FLUSH_BIT)); + auto startline_data = std::array{startline}; + auto madctl_data = std::array{madctl}; + std::scoped_lock lock(io_mutex_); + write_command(static_cast(Command::STARTLINE), startline_data, 0); + write_command(static_cast(Command::madctl), madctl_data, 0); } - /** - * @brief Set the drawing area for the display, resets the cursor to the - * starting position of the area. - * @param *area Pointer to lv_area_t strcuture with start/end x/y - * coordinates. - */ - static void set_drawing_area(const lv_area_t *area) { - set_drawing_area(area->x1, area->y1, area->x2, area->y2); + void set_brightness(float brightness) { + brightness_ = std::clamp(brightness, 0.0f, 1.0f); + auto data = std::array{static_cast(brightness_ * 15.0f)}; + std::scoped_lock lock(io_mutex_); + write_command(static_cast(Command::CONTRASTMASTER), data, 0); } - /** - * @brief Set the drawing area for the display, resets the cursor to the - * starting position of the area. - * @param xs Starting x coordinate of the area. - * @param ys Starting y coordinate of the area. - * @param xe Ending x coordinate of the area. - * @param ye Ending y coordinate of the area. - */ - static void set_drawing_area(size_t xs, size_t ys, size_t xe, size_t ye) { - std::array data; - - int offset_x = 0; - int offset_y = 0; - get_offset_rotated(offset_x, offset_y); - - uint16_t start_x = xs + offset_x; - uint16_t end_x = xe + offset_x; - uint16_t start_y = ys + offset_y; - uint16_t end_y = ye + offset_y; - - // Set the column (x) start / end addresses - data[0] = start_x & 0xFF; - data[1] = end_x & 0xFF; - write_command_(static_cast(Command::caset), data, 0); + float get_brightness() const { return brightness_; } - // Set the row (y) start / end addresses - data[0] = start_y & 0xFF; - data[1] = end_y & 0xFF; - write_command_(static_cast(Command::raset), data, 0); - } - - /** - * @brief Fill the display area with the provided color map. - * @param *disp Pointer to the LVGL display. - * @param *area Pointer to the structure describing the pixel area. - * @param *color_map Pointer to array of colors to flush to the display. - * @param flags uint32_t user data / flags to pass to the lcd_write transfer function. - */ - static void fill(lv_display_t *disp, const lv_area_t *area, uint8_t *color_map, - uint32_t flags = 0) { - std::scoped_lock lock{spi_mutex_}; - lv_draw_sw_rgb565_swap(color_map, lv_area_get_width(area) * lv_area_get_height(area)); - if (lcd_send_lines_) { - int offset_x = 0; - int offset_y = 0; - get_offset_rotated(offset_x, offset_y); - lcd_send_lines_(area->x1 + offset_x, area->y1 + offset_y, area->x2 + offset_x, - area->y2 + offset_y, color_map, flags); - } else { - set_drawing_area(area); - uint32_t size = lv_area_get_width(area) * lv_area_get_height(area); - write_command_(static_cast(Command::ramwr), {color_map, size * 2}, flags); +protected: + display_drivers::Region transform_region(display_drivers::Region region) const override { + switch (rotation_) { + case DisplayRotation::PORTRAIT: + case DisplayRotation::PORTRAIT_INVERTED: + std::swap(region.xs, region.ys); + std::swap(region.xe, region.ye); + break; + case DisplayRotation::LANDSCAPE: + case DisplayRotation::LANDSCAPE_INVERTED: + default: + break; } + return region; } - /** - * @brief Clear the display area, filling it with the provided color. - * @param x X coordinate of the upper left corner of the display area. - * @param y Y coordinate of the upper left corner of the display area. - * @param width Width of the display area to clear. - * @param height Height of the display area to clear. - * @param color 16 bit color (default 0x0000) to fill with. - */ - static void clear(size_t x, size_t y, size_t width, size_t height, uint16_t color = 0x0000) { - set_drawing_area(x, y, x + width, y + height); - - // Write the color data to controller RAM - uint32_t size = width * height; - static constexpr int max_bytes_to_send = 1024 * 2; - static uint16_t color_data[max_bytes_to_send]; - memset(color_data, color, max_bytes_to_send * sizeof(uint16_t)); - for (int i = 0; i < size; i += max_bytes_to_send) { - size_t num_bytes = std::min(static_cast(size - i), (int)(max_bytes_to_send)); - write_command_(static_cast(Command::ramwr), - {reinterpret_cast(color_data), num_bytes * 2}, 0); +private: + uint8_t make_madctl(DisplayRotation rotation) const { + auto value = DEFAULT_MADCTL; + if (config_.swap_color_order) { + value |= OLED_CMD_BGR_BIT; } - } - - /** - * @brief Send the provided commands to the display controller. - * @param commands Array of display_drivers::LcdInitCmd structures. - */ - static void send_commands(std::span> commands) { - using namespace std::chrono_literals; - - for (const auto &[command, parameters, delay_ms] : commands) { - std::scoped_lock lock{spi_mutex_}; - write_command_(command, parameters, 0); - if (delay_ms) { - std::this_thread::sleep_for(delay_ms * 1ms); - } + if (config_.mirror_x) { + value |= OLED_CMD_MX_BIT; + } + if (config_.mirror_y) { + value |= OLED_CMD_MY_BIT; + } + if (config_.swap_xy) { + value |= OLED_CMD_MV_BIT; } - } - - /** - * @brief Set the offset (upper left starting coordinate) of the display. - * @note This modifies internal variables that are used when sending - * coordinates / filling parts of the display. - * @param x New starting x coordinate (so writing to x address 0 later will - * actually write to this offset). - * @param y New starting y coordinate (so writing to y address 0 later will - * actually write to this offset). - */ - static void set_offset(int x, int y) { - offset_x_ = x; - offset_y_ = y; - } - - /** - * @brief Get the offset (upper left starting coordinate) of the display. - * @note This returns internal variables that are used when sending - * coordinates / filling parts of the display. - * @param x Reference variable that will be filled with the currently - * configured starting x coordinate that was provided in the config - * or set by set_offset(). - * @param y Reference variable that will be filled with the currently - * configured starting y coordinate that was provided in the config - * or set by set_offset(). - */ - static void get_offset(int &x, int &y) { - x = offset_x_; - y = offset_y_; - } - /** - * @brief Get the offset (upper left starting coordinate) of the display - * after rotation. - * @note This returns internal variables that are used when sending - * coordinates / filling parts of the display. - * @param x Reference variable that will be filled with the currently - * configured starting x coordinate that was provided in the config - * or set by set_offset(), updated for the current rotation. - * @param y Reference variable that will be filled with the currently - * configured starting y coordinate that was provided in the config - * or set by set_offset(), updated for the current rotation. - */ - static void get_offset_rotated(int &x, int &y) { - auto rotation = lv_display_get_rotation(lv_display_get_default()); switch (rotation) { - case LV_DISPLAY_ROTATION_90: - // intentional fallthrough - case LV_DISPLAY_ROTATION_270: - x = offset_y_; - y = offset_x_; + case DisplayRotation::LANDSCAPE: break; - case LV_DISPLAY_ROTATION_0: - // intentional fallthrough - case LV_DISPLAY_ROTATION_180: - // intentional fallthrough - default: - x = offset_x_; - y = offset_y_; + case DisplayRotation::PORTRAIT: + value ^= OLED_CMD_MV_BIT; + value ^= config_.mirror_portrait ? OLED_CMD_MY_BIT : OLED_CMD_MX_BIT; + break; + case DisplayRotation::LANDSCAPE_INVERTED: + value ^= (OLED_CMD_MY_BIT | OLED_CMD_MX_BIT); + break; + case DisplayRotation::PORTRAIT_INVERTED: + value ^= OLED_CMD_MV_BIT; + value ^= config_.mirror_portrait ? OLED_CMD_MX_BIT : OLED_CMD_MY_BIT; break; } + return value; } - /** - * @brief Set the display brightness. - * @param brightness Brightness value in range [0.0, 1.0]. - */ - static void set_brightness(const float brightness) { - // Update the local brightness value - brightness_ = brightness; - - // This display has a 4-bit brightness control - uint8_t data = brightness * 15.0f; // Scale to 0-15 range - write_command_(static_cast(Command::CONTRASTMASTER), {&data, 1}, 0); - } - - /** - * @brief Get the current display brightness. - * @return Current brightness value in range [0.0, 1.0]. - */ - static float get_brightness() { return brightness_; } - -protected: - static inline display_drivers::write_command_fn write_command_; - static inline display_drivers::send_lines_fn lcd_send_lines_; - static inline gpio_num_t reset_pin_; - static inline gpio_num_t dc_pin_; - static inline int offset_x_; - static inline int offset_y_; - static inline bool mirror_x_; - static inline bool mirror_y_; - static inline bool mirror_portrait_; - static inline bool swap_xy_; - static inline bool swap_color_order_; - static inline std::mutex spi_mutex_; - static inline float brightness_ = 1.0f; + float brightness_{1.0f}; }; } // namespace espp diff --git a/components/display_drivers/include/st7123.hpp b/components/display_drivers/include/st7123.hpp index b1ed6ea90..0e9697d3a 100644 --- a/components/display_drivers/include/st7123.hpp +++ b/components/display_drivers/include/st7123.hpp @@ -1,17 +1,14 @@ #pragma once #include -#include -#include #include "display_drivers.hpp" namespace espp { -class St7123 { - // MADCTL bits (see Espressif driver) - static constexpr uint8_t GS_BIT = 1 << 0; // Row mirror (Y) - static constexpr uint8_t SS_BIT = 1 << 1; // Column mirror (X) +class St7123 : public display_drivers::MipiDbiDisplayDriver { + static constexpr uint8_t GS_BIT = 1 << 0; + static constexpr uint8_t SS_BIT = 1 << 1; static constexpr uint8_t BGR_BIT = 1 << 3; public: @@ -34,36 +31,21 @@ class St7123 { COLMOD = 0x3A, }; - static bool initialize(const display_drivers::Config &config) { - write_command_ = config.write_command; - read_command_ = config.read_command; - lcd_send_lines_ = config.lcd_send_lines; - reset_pin_ = config.reset_pin; - dc_pin_ = config.data_command_pin; - offset_x_ = config.offset_x; - offset_y_ = config.offset_y; - mirror_x_ = config.mirror_x; - mirror_y_ = config.mirror_y; - mirror_portrait_ = config.mirror_portrait; - swap_xy_ = config.swap_xy; - swap_color_order_ = config.swap_color_order; + explicit St7123(const display_drivers::Config &config) + : MipiDbiDisplayDriver(config, + {.column_address_command = static_cast(Command::CASET), + .row_address_command = static_cast(Command::RASET), + .memory_write_command = static_cast(Command::RAMWR)}) {} - // Initialize display pins - display_drivers::init_pins(reset_pin_, dc_pin_, config.reset_value); + static constexpr const char *id() { return "ST7123"; } + + bool initialize() override { + display_drivers::init_pins(config_.reset_pin, config_.data_command_pin, config_.reset_value); - // MADCTL value - uint8_t madctl = 0; - if (mirror_x_) - madctl |= GS_BIT; - if (mirror_y_) - madctl |= SS_BIT; - if (swap_color_order_) - madctl |= BGR_BIT; - // Note: swap_xy_ not supported by ST7123 MADCTL + auto madctl = make_madctl(DisplayRotation::LANDSCAPE); - // COLMOD value - uint8_t colmod = 0x55; // 16bpp default - switch (config.bits_per_pixel) { + uint8_t colmod = 0x55; + switch (config_.bits_per_pixel) { case 16: colmod = 0x55; break; @@ -76,9 +58,8 @@ class St7123 { default: break; } - // ST7123 vendor-specific init sequence (from Espressif driver) - using Cmd = display_drivers::DisplayInitCmd<>; - std::array init_cmds = {{ + + auto init_commands = std::to_array>({ {0x60, {0x71, 0x23, 0xa2}, 0}, {0x60, {0x71, 0x23, 0xa3}, 0}, {0x60, {0x71, 0x23, 0xa4}, 0}, @@ -143,53 +124,26 @@ class St7123 { {0x11, {0x00}, 100}, {0x29, {0x00}, 0}, {0x35, {0x00}, 100}, - }}; - - // Send vendor-specific init sequence - for (const auto &cmd : init_cmds) { - write_command_(cmd.command, std::span(cmd.parameters), 0); - if (cmd.delay_ms > 0) { - std::this_thread::sleep_for(std::chrono::milliseconds(cmd.delay_ms)); - } - std::this_thread::sleep_for(std::chrono::milliseconds(5)); - } - - // Set MADCTL (mirror/color order) - write_command_(static_cast(Command::MADCTL), std::span(&madctl, 1), 0); - // Set COLMOD (color depth) - write_command_(static_cast(Command::COLMOD), std::span(&colmod, 1), 0); + {static_cast(Command::MADCTL), {madctl}, 0}, + {static_cast(Command::COLMOD), {colmod}, 0}, + }); + send_commands(init_commands); return true; } - static constexpr const char *id() { return "ST7123"; } + void set_rotation(const DisplayRotation &rotation) override { + Controller::set_rotation(rotation); + auto data = std::array{make_madctl(rotation)}; + std::scoped_lock lock(io_mutex_); + write_command(static_cast(Command::MADCTL), data, 0); + } -protected: - static display_drivers::write_command_fn write_command_; - static display_drivers::read_command_fn read_command_; - static display_drivers::send_lines_fn lcd_send_lines_; - static gpio_num_t reset_pin_; - static gpio_num_t dc_pin_; - static int offset_x_; - static int offset_y_; - static bool swap_xy_; - static bool mirror_x_; - static bool mirror_y_; - static bool mirror_portrait_; - static bool swap_color_order_; +private: + uint8_t make_madctl(DisplayRotation rotation) const { + auto value = display_drivers::make_madctl_base(config_, BGR_BIT, GS_BIT, SS_BIT, 0); + return display_drivers::apply_standard_rotation(value, config_, rotation, GS_BIT, SS_BIT, 0); + } }; -inline display_drivers::write_command_fn St7123::write_command_{nullptr}; -inline display_drivers::read_command_fn St7123::read_command_{nullptr}; -inline display_drivers::send_lines_fn St7123::lcd_send_lines_{nullptr}; -inline gpio_num_t St7123::reset_pin_{GPIO_NUM_NC}; -inline gpio_num_t St7123::dc_pin_{GPIO_NUM_NC}; -inline int St7123::offset_x_{0}; -inline int St7123::offset_y_{0}; -inline bool St7123::swap_xy_{false}; -inline bool St7123::mirror_x_{false}; -inline bool St7123::mirror_y_{false}; -inline bool St7123::mirror_portrait_{false}; -inline bool St7123::swap_color_order_{false}; - } // namespace espp diff --git a/components/display_drivers/include/st7789.hpp b/components/display_drivers/include/st7789.hpp index cccf9f5aa..0515027f4 100644 --- a/components/display_drivers/include/st7789.hpp +++ b/components/display_drivers/include/st7789.hpp @@ -1,8 +1,5 @@ #pragma once -#include -#include - #include "display_drivers.hpp" namespace espp { @@ -14,15 +11,6 @@ namespace espp { * and * https://github.com/Bodmer/TFT_eSPI/blob/master/TFT_Drivers/ST7789_Defines.h * - * See also: - * https://github.com/espressif/esp-who/blob/master/components/screen/controller_driver/st7789/st7789.c - * or - * https://github.com/espressif/tflite-micro-esp-examples/blob/master/components/screen/controller_driver/st7789/st7789.c - * or https://esphome.io/api/st7789v_8h_source.html or - * https://github.com/mireq/esp32-st7789-demo/blob/master/components/st7789/include/st7789.h - * or - * https://github.com/mireq/esp32-st7789-demo/blob/master/components/st7789/st7789.c - * * \section st7789_ttgo_cfg TTGO St7789 Config * \snippet display_drivers_example.cpp ttgo_config example * \section st7789_box_cfg ESP32-S3-BOX St7789 Config @@ -30,396 +18,137 @@ namespace espp { * \section st7789_ex1 st7789 Example * \snippet display_drivers_example.cpp display_drivers example */ -class St7789 { +class St7789 : public display_drivers::MipiDbiDisplayDriver { public: enum class Command : uint8_t { - nop = 0x00, // no operation - swreset = 0x01, // software reset - rddid = 0x04, // read display id - rddst = 0x09, // read display status - - rddpm = 0x0a, // read display power mode - rdd_madctl = 0x0b, // read display madctl - rdd_colmod = 0x0c, // read display pixel format - rddim = 0x0d, // read display image mode - rddsm = 0x0e, // read display signal mode - rddsr = 0x0f, // read display self-diagnostic result (st7789v) - - slpin = 0x10, // sleep in - slpout = 0x11, // sleep out - ptlon = 0x12, // partial mode on - noron = 0x13, // normal display mode on - - invoff = 0x20, // display inversion off - invon = 0x21, // display inversion on - gamset = 0x26, // gamma set - dispoff = 0x28, // display off - dispon = 0x29, // display on - caset = 0x2a, // column address set - raset = 0x2b, // row address set - ramwr = 0x2c, // ram write - rgbset = 0x2d, // color setting for 4096, 64k and 262k colors - ramrd = 0x2e, // ram read - + nop = 0x00, + swreset = 0x01, + rddid = 0x04, + rddst = 0x09, + rddpm = 0x0a, + rdd_madctl = 0x0b, + rdd_colmod = 0x0c, + rddim = 0x0d, + rddsm = 0x0e, + rddsr = 0x0f, + slpin = 0x10, + slpout = 0x11, + ptlon = 0x12, + noron = 0x13, + invoff = 0x20, + invon = 0x21, + gamset = 0x26, + dispoff = 0x28, + dispon = 0x29, + caset = 0x2a, + raset = 0x2b, + ramwr = 0x2c, + rgbset = 0x2d, + ramrd = 0x2e, ptlar = 0x30, - vscrdef = 0x33, // vertical scrolling definition (st7789v) - teoff = 0x34, // tearing effect line off - teon = 0x35, // tearing effect line on - madctl = 0x36, // memory data access control - idmoff = 0x38, // idle mode off - idmon = 0x39, // idle mode on - ramwrc = 0x3c, // memory write continue (st7789v) - ramrdc = 0x3e, // memory read continue (st7789v) - colmod = 0x3a, // color mode - pixel format - - ramctrl = 0xb0, // ram control - rgbctrl = 0xb1, // rgb control - porctrl = 0xb2, // porch control - frctrl1 = 0xb3, // frame rate control - parctrl = 0xb5, // partial mode control - gctrl = 0xb7, // gate control - gtadj = 0xb8, // gate on timing adjustment - dgmen = 0xba, // digital gamma enable - vcoms = 0xbb, // vcoms setting - lcmctrl = 0xc0, // lcm control - idset = 0xc1, // id setting - vdvvrhen = 0xc2, // vdv and vrh command enable - vrhs = 0xc3, // vrh set - vdvset = 0xc4, // vdv setting - vcmofset = 0xc5, // vcoms offset set - frctr2 = 0xc6, // fr control 2 - cabcctrl = 0xc7, // cabc control - regsel1 = 0xc8, // register value section 1 - regsel2 = 0xca, // register value section 2 - pwmfrsel = 0xcc, // pwm frequency selection - pwctrl1 = 0xd0, // power control 1 - vapvanen = 0xd2, // enable vap/van signal output - cmd2en = 0xdf, // command 2 enable - pvgamctrl = 0xe0, // positive voltage gamma control - nvgamctrl = 0xe1, // negative voltage gamma control - dgmlutr = 0xe2, // digital gamma look-up table for red - dgmlutb = 0xe3, // digital gamma look-up table for blue - gatectrl = 0xe4, // gate control - spi2en = 0xe7, // spi2 enable - pwctrl2 = 0xe8, // power control 2 - eqctrl = 0xe9, // equalize time control - promctrl = 0xec, // program control - promen = 0xfa, // program mode enable - nvmset = 0xfc, // nvm setting - promact = 0xfe, // program action + vscrdef = 0x33, + teoff = 0x34, + teon = 0x35, + madctl = 0x36, + idmoff = 0x38, + idmon = 0x39, + ramwrc = 0x3c, + ramrdc = 0x3e, + colmod = 0x3a, + ramctrl = 0xb0, + rgbctrl = 0xb1, + porctrl = 0xb2, + frctrl1 = 0xb3, + parctrl = 0xb5, + gctrl = 0xb7, + gtadj = 0xb8, + dgmen = 0xba, + vcoms = 0xbb, + lcmctrl = 0xc0, + idset = 0xc1, + vdvvrhen = 0xc2, + vrhs = 0xc3, + vdvset = 0xc4, + vcmofset = 0xc5, + frctr2 = 0xc6, + cabcctrl = 0xc7, + regsel1 = 0xc8, + regsel2 = 0xca, + pwmfrsel = 0xcc, + pwctrl1 = 0xd0, + vapvanen = 0xd2, + cmd2en = 0xdf, + pvgamctrl = 0xe0, + nvgamctrl = 0xe1, + dgmlutr = 0xe2, + dgmlutb = 0xe3, + gatectrl = 0xe4, + spi2en = 0xe7, + pwctrl2 = 0xe8, + eqctrl = 0xe9, + promctrl = 0xec, + promen = 0xfa, + nvmset = 0xfc, + promact = 0xfe, }; - /** - * @brief Store the config data and send the initialization commands to the - * display controller. - * @param config display_drivers::Config structure - */ - static void initialize(const display_drivers::Config &config) { - // update the static members - write_command_ = config.write_command; - lcd_send_lines_ = config.lcd_send_lines; - reset_pin_ = config.reset_pin; - dc_pin_ = config.data_command_pin; - offset_x_ = config.offset_x; - offset_y_ = config.offset_y; - mirror_x_ = config.mirror_x; - mirror_y_ = config.mirror_y; - mirror_portrait_ = config.mirror_portrait; - swap_xy_ = config.swap_xy; - swap_color_order_ = config.swap_color_order; - - // Initialize display pins - display_drivers::init_pins(reset_pin_, dc_pin_, config.reset_value); + explicit St7789(const display_drivers::Config &config) + : MipiDbiDisplayDriver(config, + {.column_address_command = static_cast(Command::caset), + .row_address_command = static_cast(Command::raset), + .memory_write_command = static_cast(Command::ramwr)}) {} - uint8_t madctl = 0; - if (swap_color_order_) { - madctl |= LCD_CMD_BGR_BIT; - } - if (mirror_x_) { - madctl |= LCD_CMD_MX_BIT; - } - if (mirror_y_) { - madctl |= LCD_CMD_MY_BIT; - } - if (swap_xy_) { - madctl |= LCD_CMD_MV_BIT; - } + bool initialize() override { + display_drivers::init_pins(config_.reset_pin, config_.data_command_pin, config_.reset_value); - // set up the init commands + auto madctl = make_madctl(DisplayRotation::LANDSCAPE); auto init_commands = std::to_array>({ {0xCF, {0x00, 0x83, 0X30}}, {0xED, {0x64, 0x03, 0X12, 0X81}}, - {(uint8_t)Command::pwctrl2, {0x85, 0x01, 0x79}}, + {static_cast(Command::pwctrl2), {0x85, 0x01, 0x79}}, {0xCB, {0x39, 0x2C, 0x00, 0x34, 0x02}}, {0xF7, {0x20}}, {0xEA, {0x00, 0x00}}, - {(uint8_t)Command::lcmctrl, {0x26}}, - {(uint8_t)Command::idset, {0x11}}, - {(uint8_t)Command::vcmofset, {0x35, 0x3E}}, - {(uint8_t)Command::cabcctrl, {0xBE}}, - {(uint8_t)Command::madctl, {madctl}}, - {(uint8_t)Command::colmod, {0x55}}, - {(uint8_t)Command::invon}, - {(uint8_t)Command::rgbctrl, {0x00, 0x1B}}, + {static_cast(Command::lcmctrl), {0x26}}, + {static_cast(Command::idset), {0x11}}, + {static_cast(Command::vcmofset), {0x35, 0x3E}}, + {static_cast(Command::cabcctrl), {0xBE}}, + {static_cast(Command::madctl), {madctl}}, + {static_cast(Command::colmod), {0x55}}, + {static_cast(Command::invon)}, + {static_cast(Command::rgbctrl), {0x00, 0x1B}}, {0xF2, {0x08}}, - {(uint8_t)Command::gamset, {0x01}}, - {(uint8_t)Command::caset, {0x00, 0x00, 0x00, 0xEF}}, - {(uint8_t)Command::raset, {0x00, 0x00, 0x01, 0x3f}}, - {(uint8_t)Command::ramwr}, - {(uint8_t)Command::gctrl, {0x07}}, + {static_cast(Command::gamset), {0x01}}, + {static_cast(Command::caset), {0x00, 0x00, 0x00, 0xEF}}, + {static_cast(Command::raset), {0x00, 0x00, 0x01, 0x3f}}, + {static_cast(Command::ramwr)}, + {static_cast(Command::gctrl), {0x07}}, {0xB6, {0x0A, 0x82, 0x27, 0x00}}, - {(uint8_t)Command::slpout, {0}, 100}, - {(uint8_t)Command::dispon, {0}, 100}, + {static_cast(Command::slpout), {0}, 100}, + {static_cast(Command::dispon), {0}, 100}, }); - // NOTE: ST7789 setting the reverse color is the normal color so we inver - // the logic here. - if (config.invert_colors) { - init_commands[12].command = (uint8_t)Command::invoff; - } else { - init_commands[12].command = (uint8_t)Command::invon; + if (config_.invert_colors) { + init_commands[12].command = static_cast(Command::invoff); } - // send the init commands send_commands(init_commands); + return true; } - /** - * @brief Set the display rotation. - * @param rotation New display rotation. - */ - static void rotate(const DisplayRotation &rotation) { - uint8_t data = 0; - if (swap_color_order_) { - data |= LCD_CMD_BGR_BIT; - } - if (mirror_x_) { - data |= LCD_CMD_MX_BIT; - } - if (mirror_y_) { - data |= LCD_CMD_MY_BIT; - } - if (swap_xy_) { - data |= LCD_CMD_MV_BIT; - } - switch (rotation) { - case DisplayRotation::LANDSCAPE: - break; - case DisplayRotation::PORTRAIT: - // flip the mx and mv bits (xor) - if (mirror_portrait_) { - data ^= (LCD_CMD_MX_BIT | LCD_CMD_MV_BIT); - } else { - data ^= (LCD_CMD_MY_BIT | LCD_CMD_MV_BIT); - } - break; - case DisplayRotation::LANDSCAPE_INVERTED: - // flip the my and mx bits (xor) - data ^= (LCD_CMD_MY_BIT | LCD_CMD_MX_BIT); - break; - case DisplayRotation::PORTRAIT_INVERTED: - // flip the my and mv bits (xor) - if (mirror_portrait_) { - data ^= (LCD_CMD_MY_BIT | LCD_CMD_MV_BIT); - } else { - data ^= (LCD_CMD_MX_BIT | LCD_CMD_MV_BIT); - } - break; - } - std::scoped_lock lock{spi_mutex_}; - write_command_(static_cast(Command::madctl), {&data, 1}, 0); - } - - /** - * @brief Flush the pixel data for the provided area to the display. - * @param *disp Pointer to the LVGL display. - * @param *area Pointer to the structure describing the pixel area. - * @param *color_map Pointer to array of colors to flush to the display. - */ - static void flush(lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { - fill(disp, area, color_map, (1 << (int)display_drivers::Flags::FLUSH_BIT)); - } - - /** - * @brief Set the drawing area for the display, resets the cursor to the - * starting position of the area. - * @param *area Pointer to lv_area_t strcuture with start/end x/y - * coordinates. - */ - static void set_drawing_area(const lv_area_t *area) { - set_drawing_area(area->x1, area->y1, area->x2, area->y2); - } - - /** - * @brief Set the drawing area for the display, resets the cursor to the - * starting position of the area. - * @param xs Starting x coordinate of the area. - * @param ys Starting y coordinate of the area. - * @param xe Ending x coordinate of the area. - * @param ye Ending y coordinate of the area. - */ - static void set_drawing_area(size_t xs, size_t ys, size_t xe, size_t ye) { - std::array data; - - int offset_x = 0; - int offset_y = 0; - get_offset_rotated(offset_x, offset_y); - - uint16_t start_x = xs + offset_x; - uint16_t end_x = xe + offset_x; - uint16_t start_y = ys + offset_y; - uint16_t end_y = ye + offset_y; - - // Set the column (x) start / end addresses - data[0] = (start_x >> 8) & 0xFF; - data[1] = start_x & 0xFF; - data[2] = (end_x >> 8) & 0xFF; - data[3] = end_x & 0xFF; - write_command_(static_cast(Command::caset), data, 0); - - // Set the row (y) start / end addresses - data[0] = (start_y >> 8) & 0xFF; - data[1] = start_y & 0xFF; - data[2] = (end_y >> 8) & 0xFF; - data[3] = end_y & 0xFF; - write_command_(static_cast(Command::raset), data, 0); - } - - /** - * @brief Fill the display area with the provided color map. - * @param *disp Pointer to the LVGL display. - * @param *area Pointer to the structure describing the pixel area. - * @param *color_map Pointer to array of colors to flush to the display. - * @param flags uint32_t user data / flags to pass to the lcd_write transfer function. - */ - static void fill(lv_display_t *disp, const lv_area_t *area, uint8_t *color_map, - uint32_t flags = 0) { - std::scoped_lock lock{spi_mutex_}; - lv_draw_sw_rgb565_swap(color_map, lv_area_get_width(area) * lv_area_get_height(area)); - if (lcd_send_lines_) { - int offset_x = 0; - int offset_y = 0; - get_offset_rotated(offset_x, offset_y); - lcd_send_lines_(area->x1 + offset_x, area->y1 + offset_y, area->x2 + offset_x, - area->y2 + offset_y, color_map, flags); - } else { - set_drawing_area(area); - uint32_t size = lv_area_get_width(area) * lv_area_get_height(area); - write_command_(static_cast(Command::ramwr), {color_map, size * 2}, flags); - } - } - - /** - * @brief Clear the display area, filling it with the provided color. - * @param x X coordinate of the upper left corner of the display area. - * @param y Y coordinate of the upper left corner of the display area. - * @param width Width of the display area to clear. - * @param height Height of the display area to clear. - * @param color 16 bit color (default 0x0000) to fill with. - */ - static void clear(size_t x, size_t y, size_t width, size_t height, uint16_t color = 0x0000) { - set_drawing_area(x, y, x + width, y + height); - - // Write the color data to controller RAM - uint32_t size = width * height; - static constexpr int max_bytes_to_send = 1024 * 2; - static uint16_t color_data[max_bytes_to_send]; - memset(color_data, color, max_bytes_to_send * sizeof(uint16_t)); - for (int i = 0; i < size; i += max_bytes_to_send) { - size_t num_bytes = std::min(static_cast(size - i), (int)(max_bytes_to_send)); - write_command_(static_cast(Command::ramwr), - {reinterpret_cast(color_data), num_bytes * 2}, 0); - } - } - - /** - * @brief Send the provided commands to the display controller. - * @param commands Array of display_drivers::LcdInitCmd structures. - */ - static void send_commands(std::span> commands) { - using namespace std::chrono_literals; - - for (const auto &[command, parameters, delay_ms] : commands) { - std::scoped_lock lock{spi_mutex_}; - write_command_(command, parameters, 0); - std::this_thread::sleep_for(delay_ms * 1ms); - } + void set_rotation(const DisplayRotation &rotation) override { + Controller::set_rotation(rotation); + auto data = std::array{make_madctl(rotation)}; + std::scoped_lock lock(io_mutex_); + write_command(static_cast(Command::madctl), data, 0); } - /** - * @brief Set the offset (upper left starting coordinate) of the display. - * @note This modifies internal variables that are used when sending - * coordinates / filling parts of the display. - * @param x New starting x coordinate (so writing to x address 0 later will - * actually write to this offset). - * @param y New starting y coordinate (so writing to y address 0 later will - * actually write to this offset). - */ - static void set_offset(int x, int y) { - offset_x_ = x; - offset_y_ = y; +private: + uint8_t make_madctl(DisplayRotation rotation) const { + auto value = display_drivers::make_madctl_base(config_, LCD_CMD_BGR_BIT, LCD_CMD_MX_BIT, + LCD_CMD_MY_BIT, LCD_CMD_MV_BIT); + return display_drivers::apply_standard_rotation(value, config_, rotation, LCD_CMD_MX_BIT, + LCD_CMD_MY_BIT, LCD_CMD_MV_BIT); } - - /** - * @brief Get the offset (upper left starting coordinate) of the display. - * @note This returns internal variables that are used when sending - * coordinates / filling parts of the display. - * @param x Reference variable that will be filled with the currently - * configured starting x coordinate that was provided in the config - * or set by set_offset(). - * @param y Reference variable that will be filled with the currently - * configured starting y coordinate that was provided in the config - * or set by set_offset(). - */ - static void get_offset(int &x, int &y) { - x = offset_x_; - y = offset_y_; - } - - /** - * @brief Get the offset (upper left starting coordinate) of the display - * after rotation. - * @note This returns internal variables that are used when sending - * coordinates / filling parts of the display. - * @param x Reference variable that will be filled with the currently - * configured starting x coordinate that was provided in the config - * or set by set_offset(), updated for the current rotation. - * @param y Reference variable that will be filled with the currently - * configured starting y coordinate that was provided in the config - * or set by set_offset(), updated for the current rotation. - */ - static void get_offset_rotated(int &x, int &y) { - auto rotation = lv_display_get_rotation(lv_display_get_default()); - switch (rotation) { - case LV_DISPLAY_ROTATION_90: - // intentional fallthrough - case LV_DISPLAY_ROTATION_270: - x = offset_y_; - y = offset_x_; - break; - case LV_DISPLAY_ROTATION_0: - // intentional fallthrough - case LV_DISPLAY_ROTATION_180: - // intentional fallthrough - default: - x = offset_x_; - y = offset_y_; - break; - } - } - -protected: - static inline display_drivers::write_command_fn write_command_; - static inline display_drivers::send_lines_fn lcd_send_lines_; - static inline gpio_num_t reset_pin_; - static inline gpio_num_t dc_pin_; - static inline int offset_x_; - static inline int offset_y_; - static inline bool mirror_x_; - static inline bool mirror_y_; - static inline bool mirror_portrait_; - static inline bool swap_xy_; - static inline bool swap_color_order_; - static inline std::mutex spi_mutex_; }; } // namespace espp diff --git a/components/display_drivers/include/st7796.hpp b/components/display_drivers/include/st7796.hpp old mode 100755 new mode 100644 index d6c0cc9c3..b4d96bbc3 --- a/components/display_drivers/include/st7796.hpp +++ b/components/display_drivers/include/st7796.hpp @@ -10,12 +10,8 @@ namespace espp { /** * @brief Display driver for the ST7796 display controller. - * - * This implementation follows the same lightweight callback-based structure as - * the other ESPP display drivers, with initialization values adapted from the - * Smart Panlee SC01 Plus board configuration. */ -class St7796 { +class St7796 : public display_drivers::MipiDbiDisplayDriver { public: /// Supported command values used by this driver wrapper. enum class Command : uint8_t { @@ -30,37 +26,16 @@ class St7796 { colmod = 0x3a, ///< Configure color depth / pixel format. }; - /// Initialize the display controller state and send the power-on sequence. - /// \param config Driver transport and orientation configuration. - static void initialize(const display_drivers::Config &config) { - write_command_ = config.write_command; - lcd_send_lines_ = config.lcd_send_lines; - reset_pin_ = config.reset_pin; - dc_pin_ = config.data_command_pin; - offset_x_ = config.offset_x; - offset_y_ = config.offset_y; - mirror_x_ = config.mirror_x; - mirror_y_ = config.mirror_y; - mirror_portrait_ = config.mirror_portrait; - swap_xy_ = config.swap_xy; - swap_color_order_ = config.swap_color_order; + explicit St7796(const display_drivers::Config &config) + : MipiDbiDisplayDriver(config, + {.column_address_command = static_cast(Command::caset), + .row_address_command = static_cast(Command::raset), + .memory_write_command = static_cast(Command::ramwr)}) {} - display_drivers::init_pins(reset_pin_, dc_pin_, config.reset_value); - - uint8_t madctl = 0; - if (swap_color_order_) { - madctl |= LCD_CMD_BGR_BIT; - } - if (mirror_x_) { - madctl |= LCD_CMD_MX_BIT; - } - if (mirror_y_) { - madctl |= LCD_CMD_MY_BIT; - } - if (swap_xy_) { - madctl |= LCD_CMD_MV_BIT; - } + bool initialize() override { + display_drivers::init_pins(config_.reset_pin, config_.data_command_pin, config_.reset_value); + auto madctl = make_madctl(DisplayRotation::LANDSCAPE); auto init_commands = std::to_array>({ {(uint8_t)Command::slpout, {}, 120}, {(uint8_t)Command::madctl, {madctl}}, @@ -88,205 +63,27 @@ class St7796 { send_commands(init_commands); - if (config.invert_colors) { - write_command_(static_cast(Command::invon), {}, 0); - } else { - write_command_(static_cast(Command::invoff), {}, 0); - } - } - - /// Update the controller addressing mode for a new display rotation. - /// \param rotation New display rotation. - static void rotate(const DisplayRotation &rotation) { - uint8_t data = 0; - if (swap_color_order_) { - data |= LCD_CMD_BGR_BIT; - } - if (mirror_x_) { - data |= LCD_CMD_MX_BIT; - } - if (mirror_y_) { - data |= LCD_CMD_MY_BIT; - } - if (swap_xy_) { - data |= LCD_CMD_MV_BIT; - } - switch (rotation) { - case DisplayRotation::LANDSCAPE: - break; - case DisplayRotation::PORTRAIT: - if (mirror_portrait_) { - data ^= (LCD_CMD_MX_BIT | LCD_CMD_MV_BIT); - } else { - data ^= (LCD_CMD_MY_BIT | LCD_CMD_MV_BIT); - } - break; - case DisplayRotation::LANDSCAPE_INVERTED: - data ^= (LCD_CMD_MY_BIT | LCD_CMD_MX_BIT); - break; - case DisplayRotation::PORTRAIT_INVERTED: - if (mirror_portrait_) { - data ^= (LCD_CMD_MY_BIT | LCD_CMD_MV_BIT); - } else { - data ^= (LCD_CMD_MX_BIT | LCD_CMD_MV_BIT); - } - break; - } - std::scoped_lock lock{spi_mutex_}; - write_command_(static_cast(Command::madctl), {&data, 1}, 0); - } - - /// Flush an LVGL drawing area to the display. - /// \param disp LVGL display pointer. - /// \param area Area to flush. - /// \param color_map RGB565 pixel data for the area. - static void flush(lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { - fill(disp, area, color_map, (1 << (int)display_drivers::Flags::FLUSH_BIT)); - } - - /// Set the active drawing area using an LVGL area. - /// \param area Area to make active on the controller. - static void set_drawing_area(const lv_area_t *area) { - set_drawing_area(area->x1, area->y1, area->x2, area->y2); - } - - /// Set the active drawing area using raw coordinates. - /// \param xs Starting x coordinate. - /// \param ys Starting y coordinate. - /// \param xe Ending x coordinate. - /// \param ye Ending y coordinate. - static void set_drawing_area(size_t xs, size_t ys, size_t xe, size_t ye) { - std::array data; - - int offset_x = 0; - int offset_y = 0; - get_offset_rotated(offset_x, offset_y); - - uint16_t start_x = xs + offset_x; - uint16_t end_x = xe + offset_x; - uint16_t start_y = ys + offset_y; - uint16_t end_y = ye + offset_y; - - data[0] = (start_x >> 8) & 0xFF; - data[1] = start_x & 0xFF; - data[2] = (end_x >> 8) & 0xFF; - data[3] = end_x & 0xFF; - write_command_(static_cast(Command::caset), data, 0); - - data[0] = (start_y >> 8) & 0xFF; - data[1] = start_y & 0xFF; - data[2] = (end_y >> 8) & 0xFF; - data[3] = end_y & 0xFF; - write_command_(static_cast(Command::raset), data, 0); - } - - /// Fill an LVGL area using the supplied color buffer. - /// \param disp LVGL display pointer. - /// \param area Area to fill. - /// \param color_map RGB565 pixel data for the area. - /// \param flags Optional transport flags. - static void fill(lv_display_t *disp, const lv_area_t *area, uint8_t *color_map, - uint32_t flags = 0) { - std::scoped_lock lock{spi_mutex_}; - lv_draw_sw_rgb565_swap(color_map, lv_area_get_width(area) * lv_area_get_height(area)); - if (lcd_send_lines_) { - int offset_x = 0; - int offset_y = 0; - get_offset_rotated(offset_x, offset_y); - lcd_send_lines_(area->x1 + offset_x, area->y1 + offset_y, area->x2 + offset_x, - area->y2 + offset_y, color_map, flags); + if (config_.invert_colors) { + write_command(static_cast(Command::invon), {}, 0); } else { - set_drawing_area(area); - uint32_t size = lv_area_get_width(area) * lv_area_get_height(area); - write_command_(static_cast(Command::ramwr), {color_map, size * 2}, flags); - } - } - - /// Clear a rectangular region to a solid color. - /// \param x Starting x coordinate. - /// \param y Starting y coordinate. - /// \param width Width in pixels. - /// \param height Height in pixels. - /// \param color RGB565 fill color. - static void clear(size_t x, size_t y, size_t width, size_t height, uint16_t color = 0x0000) { - if (width == 0 || height == 0) { - return; - } - - set_drawing_area(x, y, x + width - 1, y + height - 1); - - uint32_t size = width * height; - static constexpr int max_pixels_to_send = 1024; - std::array color_data; - std::fill(color_data.begin(), color_data.end(), color); - for (int i = 0; i < size; i += max_pixels_to_send) { - size_t num_pixels = std::min((int)(size - i), max_pixels_to_send); - write_command_(static_cast(Command::ramwr), - {reinterpret_cast(color_data.data()), num_pixels * 2}, 0); - } - } - - /// Send a sequence of initialization commands to the controller. - /// \param commands Command list to send in order. - static void send_commands(std::span> commands) { - using namespace std::chrono_literals; - - for (const auto &[command, parameters, delay_ms] : commands) { - std::scoped_lock lock{spi_mutex_}; - write_command_(command, parameters, 0); - std::this_thread::sleep_for(delay_ms * 1ms); + write_command(static_cast(Command::invoff), {}, 0); } + return true; } - /// Set the controller coordinate offset used by this driver. - /// \param x X offset in pixels. - /// \param y Y offset in pixels. - static void set_offset(int x, int y) { - offset_x_ = x; - offset_y_ = y; + void set_rotation(const DisplayRotation &rotation) override { + Controller::set_rotation(rotation); + auto data = std::array{make_madctl(rotation)}; + std::scoped_lock lock(io_mutex_); + write_command(static_cast(Command::madctl), data, 0); } - /// Get the configured controller coordinate offset. - /// \param x Filled with the x offset. - /// \param y Filled with the y offset. - static void get_offset(int &x, int &y) { - x = offset_x_; - y = offset_y_; +private: + uint8_t make_madctl(DisplayRotation rotation) const { + auto value = display_drivers::make_madctl_base(config_, LCD_CMD_BGR_BIT, LCD_CMD_MX_BIT, + LCD_CMD_MY_BIT, LCD_CMD_MV_BIT); + return display_drivers::apply_standard_rotation(value, config_, rotation, LCD_CMD_MX_BIT, + LCD_CMD_MY_BIT, LCD_CMD_MV_BIT); } - - /// Get the coordinate offset after accounting for the current LVGL rotation. - /// \param x Filled with the rotated x offset. - /// \param y Filled with the rotated y offset. - static void get_offset_rotated(int &x, int &y) { - auto *display = lv_display_get_default(); - auto rotation = display ? lv_display_get_rotation(display) : LV_DISPLAY_ROTATION_0; - switch (rotation) { - case LV_DISPLAY_ROTATION_90: - case LV_DISPLAY_ROTATION_270: - x = offset_y_; - y = offset_x_; - break; - case LV_DISPLAY_ROTATION_0: - case LV_DISPLAY_ROTATION_180: - default: - x = offset_x_; - y = offset_y_; - break; - } - } - -protected: - static inline display_drivers::write_command_fn write_command_; - static inline display_drivers::send_lines_fn lcd_send_lines_; - static inline gpio_num_t reset_pin_; - static inline gpio_num_t dc_pin_; - static inline int offset_x_; - static inline int offset_y_; - static inline bool mirror_x_; - static inline bool mirror_y_; - static inline bool mirror_portrait_; - static inline bool swap_xy_; - static inline bool swap_color_order_; - static inline std::mutex spi_mutex_; }; } // namespace espp diff --git a/components/display_drivers/src/spi_panel_io.cpp b/components/display_drivers/src/spi_panel_io.cpp new file mode 100644 index 000000000..17c0c3d2d --- /dev/null +++ b/components/display_drivers/src/spi_panel_io.cpp @@ -0,0 +1,195 @@ +#include "spi_panel_io.hpp" + +#include +#include + +namespace espp { +SpiPanelIo::SpiPanelIo(const Config &config) + : BaseComponent("SPI Panel IO", config.log_level) + , config_(config) { + if (config_.device_config.queue_size < 1) { + logger_.error("SPI panel device queue_size must be at least 1"); + return; + } + if (config_.data_command_io != GPIO_NUM_NC && config_.data_command_bit_mask == 0) { + logger_.error("SPI panel D/C bit mask must be non-zero when a D/C GPIO is configured"); + return; + } + + auto queue_depth = static_cast(config_.device_config.queue_size); + transactions_.resize(queue_depth); + contexts_.resize(queue_depth); + if (!config_.spi) { + logger_.error("missing SPI bus"); + return; + } + if (config_.data_command_io != GPIO_NUM_NC) { + gpio_set_direction(config_.data_command_io, GPIO_MODE_OUTPUT); + gpio_set_level(config_.data_command_io, 0); + } + auto device_config = config_.device_config; + device_config.pre_cb = &SpiPanelIo::pre_transfer_callback; + device_config.post_cb = &SpiPanelIo::post_transfer_callback; + std::error_code ec; + device_ = config_.spi->add_device(device_config, ec); + if (ec || !device_) { + logger_.error("could not initialize SPI command/data device"); + } +} + +bool SpiPanelIo::initialized() const { return static_cast(device_); } + +std::shared_ptr SpiPanelIo::device() const { return device_; } + +void SpiPanelIo::wait() { + std::lock_guard lock(mutex_); + wait_locked(); +} + +void SpiPanelIo::write_command(uint8_t command, std::span parameters, + uint32_t user_flags) { + std::lock_guard lock(mutex_); + if (!device_) { + logger_.error("device not initialized"); + return; + } + wait_locked(); + + queue_command_locked(command, user_flags); + + if (parameters.empty()) { + wait_locked(); + return; + } + + queue_data_locked(parameters, user_flags | config_.data_command_bit_mask); + wait_locked(); +} + +void SpiPanelIo::queue_command(uint8_t command, uint32_t user_flags) { + std::lock_guard lock(mutex_); + if (!device_) { + logger_.error("device not initialized"); + return; + } + queue_command_locked(command, user_flags); +} + +void SpiPanelIo::queue_data(std::span data, uint32_t user_flags) { + std::lock_guard lock(mutex_); + if (!device_) { + logger_.error("device not initialized"); + return; + } + if (data.empty()) { + return; + } + queue_data_locked(data, user_flags | config_.data_command_bit_mask); +} + +void SpiPanelIo::queue_pixels(const uint8_t *data, size_t size, uint32_t user_flags, + uint32_t transaction_flags) { + std::lock_guard lock(mutex_); + if (!device_) { + logger_.error("device not initialized"); + return; + } + if (!data || size == 0) { + logger_.error("cannot queue null or empty pixel data"); + return; + } + queue_pixels_locked(data, size, config_.data_command_bit_mask | user_flags, transaction_flags); +} + +void SpiPanelIo::pre_transfer_callback(spi_transaction_t *transaction) { + auto *context = static_cast(transaction->user); + if (!context || !context->helper) { + return; + } + if (context->helper->config_.data_command_io == GPIO_NUM_NC) { + return; + } + bool dc_level = (context->user_flags & context->helper->config_.data_command_bit_mask) != 0; + gpio_set_level(context->helper->config_.data_command_io, dc_level); +} + +void SpiPanelIo::post_transfer_callback(spi_transaction_t *transaction) { + auto *context = static_cast(transaction->user); + if (!context || !context->helper) { + return; + } + if (!context->helper->config_.post_transaction_callback) { + return; + } + if (context->helper->config_.post_transaction_callback_bit_mask != 0 && + (context->user_flags & context->helper->config_.post_transaction_callback_bit_mask) == 0) { + return; + } + context->helper->config_.post_transaction_callback(context->user_flags); +} + +TickType_t SpiPanelIo::timeout_ticks() const { return pdMS_TO_TICKS(config_.timeout_ms); } + +size_t SpiPanelIo::prepare_transaction(uint32_t user_flags) { + if (queued_transactions_ >= static_cast(transactions_.size())) { + wait_locked(); + } + auto index = static_cast(queued_transactions_); + std::memset(&transactions_[index], 0, sizeof(transactions_[index])); + contexts_[index] = {.helper = this, .user_flags = user_flags}; + transactions_[index].user = &contexts_[index]; + return index; +} + +void SpiPanelIo::queue_command_locked(uint8_t command, uint32_t user_flags) { + auto index = prepare_transaction(user_flags); + transactions_[index].length = 8; + transactions_[index].flags = SPI_TRANS_USE_TXDATA; + transactions_[index].tx_data[0] = command; + queue_transaction_locked(index); +} + +void SpiPanelIo::queue_data_locked(std::span data, uint32_t user_flags) { + auto index = prepare_transaction(user_flags); + transactions_[index].length = data.size() * 8; + if (data.size() <= sizeof(transactions_[index].tx_data)) { + std::memcpy(transactions_[index].tx_data, data.data(), data.size()); + transactions_[index].flags = SPI_TRANS_USE_TXDATA; + } else { + transactions_[index].tx_buffer = data.data(); + } + queue_transaction_locked(index); +} + +void SpiPanelIo::queue_pixels_locked(const uint8_t *data, size_t size, uint32_t user_flags, + uint32_t transaction_flags) { + auto index = prepare_transaction(user_flags); + transactions_[index].length = size * 8; + transactions_[index].flags = + transaction_flags != 0 ? transaction_flags : config_.pixel_data_flags; + transactions_[index].tx_buffer = data; + queue_transaction_locked(index); +} + +void SpiPanelIo::queue_transaction_locked(size_t index) { + std::error_code ec; + if (device_->queue_transaction(transactions_[index], timeout_ticks(), ec)) { + queued_transactions_++; + return; + } + logger_.error("could not queue SPI command/data transaction"); +} + +void SpiPanelIo::wait_locked() { + spi_transaction_t *completed = nullptr; + while (queued_transactions_ > 0) { + std::error_code ec; + if (!device_->get_transaction_result(&completed, timeout_ticks(), ec)) { + logger_.error("could not get queued SPI transaction result"); + continue; + } + (void)completed; + queued_transactions_--; + } +} +} // namespace espp diff --git a/components/esp-box/CMakeLists.txt b/components/esp-box/CMakeLists.txt index 09c5f20d8..8cb4c5a20 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_driver_i2s base_component codec display display_drivers i2c input_drivers interrupt gt911 task tt21100 icm42607 + REQUIRES driver esp_driver_i2s base_component codec display display_drivers i2c input_drivers interrupt gt911 spi task tt21100 icm42607 REQUIRED_IDF_TARGETS "esp32s3" ) diff --git a/components/esp-box/example/main/esp_box_example.cpp b/components/esp-box/example/main/esp_box_example.cpp index 451e2f0cf..f6b3bdbee 100644 --- a/components/esp-box/example/main/esp_box_example.cpp +++ b/components/esp-box/example/main/esp_box_example.cpp @@ -1,5 +1,5 @@ +#include #include -#include #include #include @@ -11,10 +11,22 @@ using namespace std::chrono_literals; static constexpr size_t MAX_CIRCLES = 100; -static std::deque circles; +struct Circle { + int x{0}; + int y{0}; + int radius{0}; + bool visible{false}; +}; +static std::array circles; +static size_t next_circle_index = 0; +static size_t visible_circle_count = 0; static std::vector audio_bytes; +static lv_obj_t *circle_layer = nullptr; static std::recursive_mutex lvgl_mutex; +static bool initialize_circle_layer(int width, int height); +static void draw_circle_layer(lv_event_t *event); +static void invalidate_circle_area(const Circle &circle); static void draw_circle(int x0, int y0, int radius); static void clear_circles(); @@ -71,12 +83,6 @@ extern "C" void app_main(void) { logger.error("Failed to initialize display!"); return; } - // initialize the touchpad - if (!box.initialize_touch(touch_callback)) { - logger.error("Failed to initialize touchpad!"); - return; - } - // make the filter we'll use for the IMU to compute the orientation static constexpr float angle_noise = 0.001f; static constexpr float rate_noise = 0.1f; @@ -130,6 +136,11 @@ extern "C" void app_main(void) { lv_obj_set_size(bg, box.lcd_width(), box.lcd_height()); lv_obj_set_style_bg_color(bg, lv_color_make(0, 0, 0), 0); + if (!initialize_circle_layer(box.lcd_width(), box.lcd_height())) { + logger.error("Failed to initialize circle layer!"); + return; + } + // add text in the center of the screen lv_obj_t *label = lv_label_create(lv_screen_active()); static std::string label_text = @@ -173,6 +184,11 @@ extern "C" void app_main(void) { lv_disp_set_rotation(disp, rotation); // update the size of the screen lv_obj_set_size(bg, box.rotated_display_width(), box.rotated_display_height()); + if (circle_layer) { + lv_obj_set_size(circle_layer, box.rotated_display_width(), box.rotated_display_height()); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_invalidate(circle_layer); + } }; // add a button in the top left which (when pressed) will rotate the display @@ -192,6 +208,14 @@ extern "C" void app_main(void) { lv_obj_set_scrollbar_mode(lv_screen_active(), LV_SCROLLBAR_MODE_OFF); lv_obj_clear_flag(lv_screen_active(), LV_OBJ_FLAG_SCROLLABLE); + // initialize the touchpad after the circle canvas exists so touch events can + // update the trail immediately. + if (!box.initialize_touch(touch_callback)) { + logger.error("Failed to initialize touchpad!"); + return; + } + lv_obj_move_foreground(circle_layer); + // 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 { { @@ -356,30 +380,104 @@ extern "C" void app_main(void) { //! [esp box example] } +static bool initialize_circle_layer(int width, int height) { + if (circle_layer) { + return true; + } + + circle_layer = lv_obj_create(lv_screen_active()); + if (!circle_layer) { + return false; + } + lv_obj_remove_style_all(circle_layer); + lv_obj_set_size(circle_layer, width, height); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_CLICKABLE); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(circle_layer, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(circle_layer, 0, 0); + lv_obj_set_style_outline_width(circle_layer, 0, 0); + lv_obj_set_style_shadow_width(circle_layer, 0, 0); + lv_obj_add_event_cb(circle_layer, draw_circle_layer, LV_EVENT_DRAW_MAIN, nullptr); + return true; +} + +static void draw_circle_layer(lv_event_t *event) { + if (visible_circle_count == 0) { + return; + } + + auto *obj = static_cast(lv_event_get_current_target(event)); + auto *layer = lv_event_get_layer(event); + lv_area_t obj_coords; + lv_obj_get_coords(obj, &obj_coords); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.base.layer = layer; + rect_dsc.radius = LV_RADIUS_CIRCLE; + rect_dsc.bg_opa = LV_OPA_70; + rect_dsc.bg_color = lv_color_make(0, 255, 255); + rect_dsc.border_width = 0; + rect_dsc.outline_width = 0; + rect_dsc.shadow_width = 0; + + for (const auto &circle : circles) { + if (!circle.visible) { + continue; + } + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_draw_rect(layer, &rect_dsc, &coords); + } +} + +static void invalidate_circle_area(const Circle &circle) { + if (!circle_layer || circle.radius <= 0) { + return; + } + + lv_area_t obj_coords; + lv_obj_get_coords(circle_layer, &obj_coords); + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_obj_invalidate_area(circle_layer, &coords); +} + static void draw_circle(int x0, int y0, int radius) { - // 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(); + if (!circle_layer) { + return; + } + lv_obj_move_foreground(circle_layer); + Circle previous_circle = circles[next_circle_index]; + circles[next_circle_index] = {.x = x0, .y = y0, .radius = radius, .visible = true}; + next_circle_index = (next_circle_index + 1) % circles.size(); + if (visible_circle_count < circles.size()) { + visible_circle_count++; } - 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); + if (previous_circle.visible) { + invalidate_circle_area(previous_circle); + } + invalidate_circle_area(circles[(next_circle_index + circles.size() - 1) % circles.size()]); } static void clear_circles() { - // remove the circles from lvgl - for (auto circle : circles) { - lv_obj_delete(circle); + for (auto &circle : circles) { + if (circle.visible) { + invalidate_circle_area(circle); + } + circle.visible = false; } - // clear the vector - circles.clear(); + next_circle_index = 0; + visible_circle_count = 0; } static bool load_audio(size_t &out_size, size_t &out_sample_rate) { diff --git a/components/esp-box/idf_component.yml b/components/esp-box/idf_component.yml index af876f5d6..7c74a8942 100644 --- a/components/esp-box/idf_component.yml +++ b/components/esp-box/idf_component.yml @@ -25,6 +25,7 @@ dependencies: espp/input_drivers: '>=1.0' espp/interrupt: '>=1.0' espp/gt911: '>=1.0' + espp/spi: '>=1.0' espp/task: '>=1.0' espp/tt21100: '>=1.0' espp/icm42607: '>=1.0' diff --git a/components/esp-box/include/esp-box.hpp b/components/esp-box/include/esp-box.hpp index 02e8cdece..be7b1b87c 100644 --- a/components/esp-box/include/esp-box.hpp +++ b/components/esp-box/include/esp-box.hpp @@ -22,6 +22,7 @@ #include "i2c.hpp" #include "icm42607.hpp" #include "interrupt.hpp" +#include "spi.hpp" #include "st7789.hpp" #include "touchpad_input.hpp" #include "tt21100.hpp" @@ -222,15 +223,6 @@ class EspBox : public BaseComponent { /// \note This is null unless initialize_display() has been called uint8_t *frame_buffer1() const; - /// Write command and optional parameters to the LCD - /// \param command The command to write - /// \param parameters The command parameters to write - /// \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_command(uint8_t command, std::span parameters, uint32_t user_data); - /// Write a frame to the LCD /// \param x The x coordinate /// \param y The y coordinate @@ -242,17 +234,6 @@ class EspBox : public BaseComponent { 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); - ///////////////////////////////////////////////////////////////////////////// // Button ///////////////////////////////////////////////////////////////////////////// @@ -520,13 +501,9 @@ class EspBox : public BaseComponent { std::vector backlight_channel_configs_; std::shared_ptr backlight_; std::shared_ptr> 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 num_queued_trans = 0; + std::unique_ptr display_driver_; + std::unique_ptr lcd_spi_; + std::unique_ptr lcd_; uint8_t *frame_buffer0_{nullptr}; uint8_t *frame_buffer1_{nullptr}; diff --git a/components/esp-box/src/video.cpp b/components/esp-box/src/video.cpp index affe37ab2..4a2a3ece2 100644 --- a/components/esp-box/src/video.cpp +++ b/components/esp-box/src/video.cpp @@ -12,34 +12,13 @@ using namespace espp; 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). -// -// cppcheck-suppress constParameterCallback -static void lcd_spi_pre_transfer_callback(spi_transaction_t *t) { - static auto lcd_dc_io = EspBox::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. -// -// cppcheck-suppress constParameterCallback -static void 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_display_get_default(); - lv_display_flush_ready(disp); - } +static void IRAM_ATTR lcd_spi_flush_ready(uint32_t) { + lv_display_t *disp = lv_display_get_default(); + lv_display_flush_ready(disp); } bool EspBox::initialize_lcd() { - if (lcd_handle_ || backlight_) { + if (lcd_ || backlight_) { logger_.warn("LCD already initialized, not initializing again!"); return false; } @@ -57,51 +36,59 @@ bool EspBox::initialize_lcd() { // set the brightness to 100% brightness(100.0f); - esp_err_t ret; - - memset(&lcd_spi_bus_config_, 0, sizeof(lcd_spi_bus_config_)); - lcd_spi_bus_config_.mosi_io_num = lcd_mosi_io; - lcd_spi_bus_config_.miso_io_num = -1; - lcd_spi_bus_config_.sclk_io_num = lcd_sclk_io; - lcd_spi_bus_config_.quadwp_io_num = -1; - lcd_spi_bus_config_.quadhd_io_num = -1; - // 32k on s3; we can use this because we enable DMA below - lcd_spi_bus_config_.max_transfer_sz = SPI_MAX_TRANSFER_BYTES; - - 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 = lcd_cs_io; - 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{ - .write_command = std::bind(&EspBox::write_command, this, _1, _2, _3), - .lcd_send_lines = std::bind(&EspBox::write_lcd_lines, this, _1, _2, _3, _4, _5, _6), - .reset_pin = lcd_reset_io, - .data_command_pin = lcd_dc_io, - .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}); + lcd_spi_ = std::make_unique(Spi::Config{ + .host = lcd_spi_num, + .sclk_io_num = lcd_sclk_io, + .mosi_io_num = lcd_mosi_io, + .miso_io_num = GPIO_NUM_NC, + .max_transfer_sz = SPI_MAX_TRANSFER_BYTES, + .log_level = get_log_level(), + }); + lcd_ = std::make_unique(SpiPanelIo::Config{ + .spi = lcd_spi_.get(), + .device_config = + { + .mode = 0, + .clock_speed_hz = lcd_clock_speed, + .input_delay_ns = 0, + .cs_io_num = lcd_cs_io, + .queue_size = 6, + }, + .data_command_io = lcd_dc_io, + .data_command_bit_mask = DC_LEVEL_BIT, + .post_transaction_callback_bit_mask = FLUSH_BIT, + .post_transaction_callback = lcd_spi_flush_ready, + .log_level = get_log_level(), + }); + if (!lcd_->initialized()) { + lcd_.reset(); + lcd_spi_.reset(); + return false; + } + display_driver_ = std::make_unique( + espp::display_drivers::Config{.panel_io = lcd_.get(), + .write_command = nullptr, + .read_command = nullptr, + .lcd_send_lines = nullptr, + .reset_pin = lcd_reset_io, + .data_command_pin = lcd_dc_io, + .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}); + if (!display_driver_ || !display_driver_->initialize()) { + display_driver_.reset(); + lcd_.reset(); + lcd_spi_.reset(); + return false; + } return true; } bool EspBox::initialize_display(size_t pixel_buffer_size) { - if (!lcd_handle_) { + if (!lcd_) { logger_.error( "LCD not initialized, you must call initialize_lcd() before initialize_display()!"); return false; @@ -113,11 +100,22 @@ bool EspBox::initialize_display(size_t pixel_buffer_size) { // initialize the display / lvgl using namespace std::chrono_literals; display_ = std::make_shared>( - Display::LvglConfig{.width = lcd_width_, - .height = lcd_height_, - .flush_callback = DisplayDriver::flush, - .rotation_callback = DisplayDriver::rotate, - .rotation = rotation}, + Display::LvglConfig{ + .width = lcd_width_, + .height = lcd_height_, + .flush_callback = + [this](lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { + if (display_driver_) { + display_driver_->flush(disp, area, color_map); + } + }, + .rotation_callback = + [this](const DisplayRotation &new_rotation) { + if (display_driver_) { + display_driver_->set_rotation(new_rotation); + } + }, + .rotation = rotation}, Display::OledConfig{ .set_brightness_callback = [this](float brightness) { this->brightness(brightness * 100.0f); }, @@ -146,131 +144,26 @@ bool EspBox::initialize_display(size_t pixel_buffer_size) { } void EspBox::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 EspBox::write_command(uint8_t command, std::span parameters, - uint32_t user_data) { - lcd_wait_lines(); - memset(&trans[0], 0, sizeof(spi_transaction_t)); - memset(&trans[1], 0, sizeof(spi_transaction_t)); - - trans[0].length = 8; - trans[0].user = reinterpret_cast(user_data); - trans[0].flags = SPI_TRANS_USE_TXDATA; - trans[0].tx_data[0] = command; - - trans[1].length = parameters.size() * 8; - if (parameters.size() <= 4) { - // copy the data pointer to trans[1].tx_data - memcpy(trans[1].tx_data, parameters.data(), parameters.size()); - trans[1].flags = SPI_TRANS_USE_TXDATA; - } else if (!parameters.empty()) { - trans[1].tx_buffer = parameters.data(); - trans[1].flags = 0; - } - trans[1].user = reinterpret_cast( - user_data | (1 << static_cast(display_drivers::Flags::DC_LEVEL_BIT))); - - esp_err_t ret = spi_device_queue_trans(lcd_handle_, &trans[0], 10 / portTICK_PERIOD_MS); - if (ret != ESP_OK) { - logger_.error("Couldn't queue spi command trans for display: {} '{}'", ret, - esp_err_to_name(ret)); - } else { - ++num_queued_trans; - if (!parameters.empty()) { - ret = spi_device_queue_trans(lcd_handle_, &trans[1], 10 / portTICK_PERIOD_MS); - if (ret != ESP_OK) { - logger_.error("Couldn't queue spi data trans for display: {} '{}'", ret, - esp_err_to_name(ret)); - } else { - ++num_queued_trans; - } - } - } -} - -void EspBox::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; + if (lcd_) { + lcd_->wait(); } - 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 EspBox::write_lcd_frame(const uint16_t xs, const uint16_t ys, const uint16_t width, const uint16_t height, uint8_t *data) { + if (!display_driver_) { + return; + } 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); + display_driver_->fill(nullptr, &area, data); } else { // don't have data, so clear the area (set to 0) - DisplayDriver::clear(xs, ys, width, height); + display_driver_->clear(xs, ys, width, height); } } diff --git a/components/m5stack-tab5/example/CMakeLists.txt b/components/m5stack-tab5/example/CMakeLists.txt index c60031698..51a119d09 100644 --- a/components/m5stack-tab5/example/CMakeLists.txt +++ b/components/m5stack-tab5/example/CMakeLists.txt @@ -31,6 +31,7 @@ set(EXTRA_COMPONENT_DIRS "../../../components/math" "../../../components/rx8130ce" "../../../components/pi4ioe5v" + "../../../components/spi" "../../../components/task" ) diff --git a/components/m5stack-tab5/example/main/m5stack_tab5_example.cpp b/components/m5stack-tab5/example/main/m5stack_tab5_example.cpp index d5034a3c8..fcadfe6fb 100644 --- a/components/m5stack-tab5/example/main/m5stack_tab5_example.cpp +++ b/components/m5stack-tab5/example/main/m5stack_tab5_example.cpp @@ -7,8 +7,8 @@ * and communication interfaces. */ +#include #include -#include #include #include @@ -20,10 +20,22 @@ using namespace std::chrono_literals; static constexpr size_t MAX_CIRCLES = 100; -static std::deque circles; +struct Circle { + int x{0}; + int y{0}; + int radius{0}; + bool visible{false}; +}; +static std::array circles; +static size_t next_circle_index = 0; +static size_t visible_circle_count = 0; static std::vector audio_bytes; +static lv_obj_t *circle_layer = nullptr; static std::recursive_mutex lvgl_mutex; +static bool initialize_circle_layer(int width, int height); +static void draw_circle_layer(lv_event_t *event); +static void invalidate_circle_area(const Circle &circle); static void draw_circle(int x0, int y0, int radius); static void clear_circles(); @@ -96,12 +108,6 @@ extern "C" void app_main(void) { } }; - logger.info("Initializing touch..."); - if (!tab5.initialize_touch(touch_callback)) { - logger.error("Failed to initialize touch!"); - return; - } - // make the filter we'll use for the IMU to compute the orientation static constexpr float angle_noise = 0.001f; static constexpr float rate_noise = 0.1f; @@ -239,6 +245,10 @@ extern "C" void app_main(void) { lv_obj_t *bg = lv_obj_create(lv_screen_active()); lv_obj_set_size(bg, tab5.display_width(), tab5.display_height()); lv_obj_set_style_bg_color(bg, lv_color_make(0, 0, 0), 0); + if (!initialize_circle_layer(tab5.display_width(), tab5.display_height())) { + logger.error("Failed to initialize circle layer!"); + return; + } // add text in the center of the screen lv_obj_t *label = lv_label_create(lv_screen_active()); @@ -284,7 +294,12 @@ extern "C" void app_main(void) { lv_disp_set_rotation(disp, rotation); // update the size of the screen lv_obj_set_size(bg, tab5.rotated_display_width(), tab5.rotated_display_height()); - // refresh the display + if (circle_layer) { + lv_obj_set_size(circle_layer, tab5.rotated_display_width(), tab5.rotated_display_height()); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_move_foreground(circle_layer); + lv_obj_invalidate(circle_layer); + } }; // add a button in the top left which (when pressed) will rotate the display @@ -303,6 +318,13 @@ extern "C" void app_main(void) { // 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); + lv_obj_move_foreground(circle_layer); + + logger.info("Initializing touch..."); + if (!tab5.initialize_touch(touch_callback)) { + logger.error("Failed to initialize touch!"); + return; + } // start a simple thread to do the lv_task_handler every 16ms logger.info("Starting LVGL task..."); @@ -497,29 +519,102 @@ extern "C" void app_main(void) { //! [m5stack tab5 example] } +static bool initialize_circle_layer(int width, int height) { + if (circle_layer) { + return true; + } + circle_layer = lv_obj_create(lv_screen_active()); + if (!circle_layer) { + return false; + } + lv_obj_remove_style_all(circle_layer); + lv_obj_set_size(circle_layer, width, height); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_CLICKABLE); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(circle_layer, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(circle_layer, 0, 0); + lv_obj_set_style_outline_width(circle_layer, 0, 0); + lv_obj_set_style_shadow_width(circle_layer, 0, 0); + lv_obj_add_event_cb(circle_layer, draw_circle_layer, LV_EVENT_DRAW_MAIN, nullptr); + return true; +} + +static void draw_circle_layer(lv_event_t *event) { + if (visible_circle_count == 0) { + return; + } + auto *obj = static_cast(lv_event_get_current_target(event)); + auto *layer = lv_event_get_layer(event); + lv_area_t obj_coords; + lv_obj_get_coords(obj, &obj_coords); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.base.layer = layer; + rect_dsc.radius = LV_RADIUS_CIRCLE; + rect_dsc.bg_opa = LV_OPA_70; + rect_dsc.bg_color = lv_color_make(0, 255, 255); + rect_dsc.border_width = 0; + rect_dsc.outline_width = 0; + rect_dsc.shadow_width = 0; + + for (const auto &circle : circles) { + if (!circle.visible) { + continue; + } + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_draw_rect(layer, &rect_dsc, &coords); + } +} + +static void invalidate_circle_area(const Circle &circle) { + if (!circle_layer || circle.radius <= 0) { + return; + } + + lv_area_t obj_coords; + lv_obj_get_coords(circle_layer, &obj_coords); + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_obj_invalidate_area(circle_layer, &coords); +} + static void draw_circle(int x0, int y0, int radius) { - lv_obj_t *circle = lv_obj_create(lv_scr_act()); - lv_obj_set_size(circle, radius * 2, radius * 2); - lv_obj_set_pos(circle, x0 - radius, y0 - radius); - lv_obj_set_style_radius(circle, radius, 0); - lv_obj_set_style_bg_opa(circle, LV_OPA_50, 0); - lv_obj_set_style_border_width(circle, 0, 0); - lv_obj_set_style_bg_color(circle, lv_color_hex(0xFF0000), 0); // Red color - - circles.push_back(circle); - - // Limit the number of circles to prevent memory issues - if (circles.size() > MAX_CIRCLES) { - lv_obj_del(circles.front()); - circles.pop_front(); + if (!circle_layer) { + return; + } + lv_obj_move_foreground(circle_layer); + Circle previous_circle = circles[next_circle_index]; + circles[next_circle_index] = {.x = x0, .y = y0, .radius = radius, .visible = true}; + next_circle_index = (next_circle_index + 1) % circles.size(); + if (visible_circle_count < circles.size()) { + visible_circle_count++; } + if (previous_circle.visible) { + invalidate_circle_area(previous_circle); + } + invalidate_circle_area(circles[(next_circle_index + circles.size() - 1) % circles.size()]); } static void clear_circles() { - for (auto circle : circles) { - lv_obj_del(circle); + for (auto &circle : circles) { + if (circle.visible) { + invalidate_circle_area(circle); + } + circle.visible = false; } - circles.clear(); + next_circle_index = 0; + visible_circle_count = 0; } static bool load_audio(size_t &out_size, size_t &out_sample_rate) { diff --git a/components/m5stack-tab5/example/sdkconfig.defaults b/components/m5stack-tab5/example/sdkconfig.defaults index 417f896fd..e69fe299a 100644 --- a/components/m5stack-tab5/example/sdkconfig.defaults +++ b/components/m5stack-tab5/example/sdkconfig.defaults @@ -7,6 +7,11 @@ CONFIG_ESPTOOLPY_FLASHMODE_QIO=y CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y CONFIG_ESPTOOLPY_FLASHSIZE="16MB" +# for ESP-IDF >= v6.0, we have to set this to allow older boards (e.g. m5stack +# tab5) to work with the newer ESP-IDF since it's using an older p4 chip +# revision +CONFIG_ESP32P4_SELECTS_REV_LESS_V3=y + # Memory configuration CONFIG_ESP_MAIN_TASK_STACK_SIZE=32768 CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 diff --git a/components/m5stack-tab5/include/m5stack-tab5.hpp b/components/m5stack-tab5/include/m5stack-tab5.hpp index 06567fbc5..47daa1d4d 100644 --- a/components/m5stack-tab5/include/m5stack-tab5.hpp +++ b/components/m5stack-tab5/include/m5stack-tab5.hpp @@ -728,6 +728,7 @@ class M5StackTab5 : public BaseComponent { // Display state std::shared_ptr> display_; + std::unique_ptr display_driver_; std::shared_ptr backlight_; std::vector backlight_channel_configs_; struct LcdHandles { diff --git a/components/m5stack-tab5/src/sdcard.cpp b/components/m5stack-tab5/src/sdcard.cpp index 7c89c44fa..19fd74715 100644 --- a/components/m5stack-tab5/src/sdcard.cpp +++ b/components/m5stack-tab5/src/sdcard.cpp @@ -54,12 +54,10 @@ bool M5StackTab5::initialize_sdcard(const M5StackTab5::SdCardConfig &config) { if (ret != ESP_OK) { if (ret == ESP_FAIL) { logger_.error("Failed to mount filesystem. "); - return false; } else { - logger_.error("Failed to initialize the card ({}). " - "Make sure SD card lines have pull-up resistors in place.", - esp_err_to_name(ret)); - return false; + logger_.warn("Failed to initialize the card ({}). " + "Make sure SD card is present and lines have pull-up resistors in place.", + esp_err_to_name(ret)); } return false; } diff --git a/components/m5stack-tab5/src/video.cpp b/components/m5stack-tab5/src/video.cpp index e42b8aeaf..47393be5b 100644 --- a/components/m5stack-tab5/src/video.cpp +++ b/components/m5stack-tab5/src/video.cpp @@ -201,6 +201,7 @@ bool M5StackTab5::initialize_lcd() { } espp::display_drivers::Config display_config{ + .panel_io = nullptr, .write_command = std::bind_front(&M5StackTab5::dsi_write_command, this), .read_command = std::bind_front(&M5StackTab5::dsi_read_command, this), .lcd_send_lines = nullptr, @@ -217,16 +218,21 @@ bool M5StackTab5::initialize_lcd() { .mirror_portrait = false, }; + display_driver_.reset(); if (detected_controller == DisplayController::ILI9881) { logger_.info("Initializing as ILI9881"); - if (espp::Ili9881::initialize(display_config)) { + auto display_driver = std::make_unique(display_config); + if (display_driver->initialize()) { logger_.info("Successfully initialized ILI9881 display controller"); + display_driver_ = std::move(display_driver); display_controller_ = DisplayController::ILI9881; } } else if (detected_controller == DisplayController::ST7123) { logger_.info("Initializing as ST7123"); - if (espp::St7123::initialize(display_config)) { + auto display_driver = std::make_unique(display_config); + if (display_driver->initialize()) { logger_.info("Successfully initialized ST7123 display controller"); + display_driver_ = std::move(display_driver); display_controller_ = DisplayController::ST7123; } } else { @@ -234,6 +240,12 @@ bool M5StackTab5::initialize_lcd() { return false; } + if (!display_driver_) { + logger_.error("Failed to initialize {} display controller", + get_display_controller_name(detected_controller)); + return false; + } + logger_.info("Display controller: {}", get_display_controller_name()); // call init on the panel diff --git a/components/matouch-rotary-display/CMakeLists.txt b/components/matouch-rotary-display/CMakeLists.txt index a85486c6e..3a3f3a7dc 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 base_component cst816 encoder display display_drivers i2c input_drivers interrupt task + REQUIRES driver base_component cst816 display display_drivers encoder i2c input_drivers interrupt spi task REQUIRED_IDF_TARGETS "esp32s3" ) diff --git a/components/matouch-rotary-display/example/main/matouch_rotary_display_example.cpp b/components/matouch-rotary-display/example/main/matouch_rotary_display_example.cpp index 7b7185bee..e33f41ca2 100644 --- a/components/matouch-rotary-display/example/main/matouch_rotary_display_example.cpp +++ b/components/matouch-rotary-display/example/main/matouch_rotary_display_example.cpp @@ -1,5 +1,5 @@ +#include #include -#include #include #include "matouch-rotary-display.hpp" @@ -7,10 +7,22 @@ using namespace std::chrono_literals; static constexpr size_t MAX_CIRCLES = 100; -static std::deque circles; +struct Circle { + int x{0}; + int y{0}; + int radius{0}; + bool visible{false}; +}; +static std::array circles; +static size_t next_circle_index = 0; +static size_t visible_circle_count = 0; +static lv_obj_t *circle_layer = nullptr; static std::recursive_mutex lvgl_mutex; +static bool initialize_circle_layer(int width, int height); +static void draw_circle_layer(lv_event_t *event); +static void invalidate_circle_area(const Circle &circle); static void draw_circle(int x0, int y0, int radius); static void clear_circles(); @@ -65,11 +77,6 @@ extern "C" void app_main(void) { logger.error("Failed to initialize display!"); return; } - // initialize the touchpad - if (!mt_display.initialize_touch(on_touch)) { - logger.error("Failed to initialize touchpad!"); - return; - } // initialize the rotary encoder if (!mt_display.initialize_encoder()) { logger.error("Failed to initialize rotary encoder!"); @@ -83,8 +90,13 @@ extern "C" void app_main(void) { // set the background color to black lv_obj_t *bg = lv_obj_create(lv_screen_active()); - lv_obj_set_size(bg, mt_display.lcd_width(), mt_display.lcd_height()); + lv_obj_set_size(bg, mt_display.rotated_display_width(), mt_display.rotated_display_height()); lv_obj_set_style_bg_color(bg, lv_color_make(0, 0, 0), 0); + if (!initialize_circle_layer(mt_display.rotated_display_width(), + mt_display.rotated_display_height())) { + logger.error("Failed to initialize circle layer!"); + return; + } // add text in the center of the screen lv_obj_t *label = lv_label_create(lv_screen_active()); @@ -101,22 +113,44 @@ extern "C" void app_main(void) { lv_label_set_text(label_btn, LV_SYMBOL_REFRESH); // center the text in the button lv_obj_align(label_btn, LV_ALIGN_CENTER, 0, 0); + static auto update_layout = [&]() { + int width = mt_display.rotated_display_width(); + int height = mt_display.rotated_display_height(); + lv_obj_set_size(bg, width, height); + lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); + lv_obj_align(btn, LV_ALIGN_TOP_MID, 0, 0); + if (circle_layer) { + lv_obj_set_size(circle_layer, width, height); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_move_foreground(circle_layer); + lv_obj_invalidate(circle_layer); + } + }; + static auto rotate_display = [&]() { + std::lock_guard lock(lvgl_mutex); + clear_circles(); + static auto rotation = LV_DISPLAY_ROTATION_0; + rotation = static_cast((static_cast(rotation) + 1) % 4); + lv_display_t *disp = lv_display_get_default(); + lv_disp_set_rotation(disp, rotation); + update_layout(); + }; lv_obj_add_event_cb( - btn, - [](auto event) { - std::lock_guard lock(lvgl_mutex); - clear_circles(); - static auto rotation = LV_DISPLAY_ROTATION_0; - rotation = static_cast((static_cast(rotation) + 1) % 4); - lv_display_t *disp = lv_display_get_default(); - lv_disp_set_rotation(disp, rotation); - }, - LV_EVENT_PRESSED, nullptr); + btn, [](auto event) { rotate_display(); }, LV_EVENT_PRESSED, nullptr); + update_layout(); // 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); + lv_obj_move_foreground(circle_layer); + + // initialize the touchpad after the circle layer exists so touch events can + // update it immediately. + if (!mt_display.initialize_touch(on_touch)) { + logger.error("Failed to initialize touchpad!"); + return; + } // 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 { @@ -154,28 +188,100 @@ extern "C" void app_main(void) { //! [matouch-rotary-display example] } +static bool initialize_circle_layer(int width, int height) { + if (circle_layer) { + return true; + } + circle_layer = lv_obj_create(lv_screen_active()); + if (!circle_layer) { + return false; + } + lv_obj_remove_style_all(circle_layer); + lv_obj_set_size(circle_layer, width, height); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_CLICKABLE); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(circle_layer, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(circle_layer, 0, 0); + lv_obj_set_style_outline_width(circle_layer, 0, 0); + lv_obj_set_style_shadow_width(circle_layer, 0, 0); + lv_obj_add_event_cb(circle_layer, draw_circle_layer, LV_EVENT_DRAW_MAIN, nullptr); + return true; +} + +static void draw_circle_layer(lv_event_t *event) { + if (visible_circle_count == 0) { + return; + } + auto *obj = static_cast(lv_event_get_current_target(event)); + auto *layer = lv_event_get_layer(event); + lv_area_t obj_coords; + lv_obj_get_coords(obj, &obj_coords); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.base.layer = layer; + rect_dsc.radius = LV_RADIUS_CIRCLE; + rect_dsc.bg_opa = LV_OPA_70; + rect_dsc.bg_color = lv_color_make(0, 255, 255); + rect_dsc.border_width = 0; + rect_dsc.outline_width = 0; + rect_dsc.shadow_width = 0; + + for (const auto &circle : circles) { + if (!circle.visible) { + continue; + } + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_draw_rect(layer, &rect_dsc, &coords); + } +} + +static void invalidate_circle_area(const Circle &circle) { + if (!circle_layer || circle.radius <= 0) { + return; + } + + lv_area_t obj_coords; + lv_obj_get_coords(circle_layer, &obj_coords); + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_obj_invalidate_area(circle_layer, &coords); +} + static void draw_circle(int x0, int y0, int radius) { - // if the number of circles exceeds 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); + if (!circle_layer) { + return; + } + lv_obj_move_foreground(circle_layer); + Circle previous_circle = circles[next_circle_index]; + circles[next_circle_index] = {.x = x0, .y = y0, .radius = radius, .visible = true}; + next_circle_index = (next_circle_index + 1) % circles.size(); + if (visible_circle_count < circles.size()) { + visible_circle_count++; + } + if (previous_circle.visible) { + invalidate_circle_area(previous_circle); + } + invalidate_circle_area(circles[(next_circle_index + circles.size() - 1) % circles.size()]); } static void clear_circles() { - // remove the circles from lvgl - for (auto circle : circles) { - lv_obj_delete(circle); + for (auto &circle : circles) { + if (circle.visible) { + invalidate_circle_area(circle); + } + circle.visible = false; } - // clear the vector - circles.clear(); + next_circle_index = 0; + visible_circle_count = 0; } diff --git a/components/matouch-rotary-display/idf_component.yml b/components/matouch-rotary-display/idf_component.yml index 60e8cd2cc..35edcbe6c 100644 --- a/components/matouch-rotary-display/idf_component.yml +++ b/components/matouch-rotary-display/idf_component.yml @@ -26,6 +26,7 @@ dependencies: espp/i2c: '>=1.0' espp/input_drivers: '>=1.0' espp/interrupt: '>=1.0' + espp/spi: '>=1.0' espp/task: '>=1.0' targets: - esp32s3 diff --git a/components/matouch-rotary-display/include/matouch-rotary-display.hpp b/components/matouch-rotary-display/include/matouch-rotary-display.hpp index 00197c1f1..683b93814 100644 --- a/components/matouch-rotary-display/include/matouch-rotary-display.hpp +++ b/components/matouch-rotary-display/include/matouch-rotary-display.hpp @@ -16,6 +16,7 @@ #include "i2c.hpp" #include "interrupt.hpp" #include "led.hpp" +#include "spi.hpp" #include "touchpad_input.hpp" namespace espp { @@ -172,6 +173,14 @@ class MatouchRotaryDisplay : public BaseComponent { /// \return The height of the LCD in pixels static constexpr size_t lcd_height() { return lcd_height_; } + /// Get the display width in pixels, according to the current orientation + /// \return The display width in pixels, according to the current orientation + size_t rotated_display_width() const; + + /// Get the display height in pixels, according to the current orientation + /// \return The display height in pixels, according to the current orientation + size_t rotated_display_height() const; + /// 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 lcd_dc_io; } @@ -216,15 +225,6 @@ class MatouchRotaryDisplay : public BaseComponent { /// \note This is null unless initialize_display() has been called uint8_t *frame_buffer1() const; - /// Write command and optional parameters to the LCD - /// \param command The command to write - /// \param parameters The command parameters to write - /// \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_command(uint8_t command, std::span parameters, uint32_t user_data); - /// Write a frame to the LCD /// \param x The x coordinate /// \param y The y coordinate @@ -236,17 +236,6 @@ class MatouchRotaryDisplay : public BaseComponent { 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: MatouchRotaryDisplay(); bool update_cst816(); @@ -349,13 +338,9 @@ class MatouchRotaryDisplay : public BaseComponent { std::shared_ptr> display_; std::vector backlight_channel_configs_{}; std::shared_ptr backlight_{}; - /// 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 num_queued_trans = 0; + std::unique_ptr display_driver_; + std::unique_ptr lcd_spi_; + std::unique_ptr lcd_; uint8_t *frame_buffer0_{nullptr}; uint8_t *frame_buffer1_{nullptr}; }; // class MatouchRotaryDisplay diff --git a/components/matouch-rotary-display/src/matouch-rotary-display.cpp b/components/matouch-rotary-display/src/matouch-rotary-display.cpp index 709aff31e..f9f1ce875 100644 --- a/components/matouch-rotary-display/src/matouch-rotary-display.cpp +++ b/components/matouch-rotary-display/src/matouch-rotary-display.cpp @@ -196,77 +196,64 @@ MatouchRotaryDisplay::touchpad_convert(const MatouchRotaryDisplay::TouchpadData 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). -// -// cppcheck-suppress constParameterCallback -static void IRAM_ATTR lcd_spi_pre_transfer_callback(spi_transaction_t *t) { - static auto lcd_dc_io = MatouchRotaryDisplay::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. -// -// cppcheck-suppress constParameterCallback -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_display_get_default(); - lv_display_flush_ready(disp); - } +static void lcd_spi_flush_ready(uint32_t) { + lv_display_t *disp = lv_display_get_default(); + lv_display_flush_ready(disp); } bool MatouchRotaryDisplay::initialize_lcd() { - if (lcd_handle_ || backlight_) { + if (lcd_ || backlight_) { 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 = lcd_mosi_io; - lcd_spi_bus_config_.miso_io_num = -1; - lcd_spi_bus_config_.sclk_io_num = lcd_sclk_io; - lcd_spi_bus_config_.quadwp_io_num = -1; - lcd_spi_bus_config_.quadhd_io_num = -1; - lcd_spi_bus_config_.max_transfer_sz = SPI_MAX_TRANSFER_BYTES; - - 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 = lcd_cs_io; - 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{ - .write_command = std::bind(&MatouchRotaryDisplay::write_command, this, _1, _2, _3), - .lcd_send_lines = - std::bind(&MatouchRotaryDisplay::write_lcd_lines, this, _1, _2, _3, _4, _5, _6), - .reset_pin = lcd_reset_io, - .data_command_pin = lcd_dc_io, - .reset_value = reset_value, - .invert_colors = invert_colors, - .swap_color_order = swap_color_order, - .mirror_x = mirror_x, - .mirror_y = mirror_y}); + lcd_spi_ = std::make_unique(Spi::Config{ + .host = lcd_spi_num, + .sclk_io_num = lcd_sclk_io, + .mosi_io_num = lcd_mosi_io, + .miso_io_num = GPIO_NUM_NC, + .max_transfer_sz = SPI_MAX_TRANSFER_BYTES, + .log_level = get_log_level(), + }); + lcd_ = std::make_unique(SpiPanelIo::Config{ + .spi = lcd_spi_.get(), + .device_config = + { + .mode = 0, + .clock_speed_hz = lcd_clock_speed, + .input_delay_ns = 0, + .cs_io_num = lcd_cs_io, + .queue_size = 6, + }, + .data_command_io = lcd_dc_io, + .data_command_bit_mask = DC_LEVEL_BIT, + .post_transaction_callback_bit_mask = FLUSH_BIT, + .post_transaction_callback = lcd_spi_flush_ready, + .log_level = get_log_level(), + }); + if (!lcd_->initialized()) { + lcd_.reset(); + lcd_spi_.reset(); + return false; + } + display_driver_ = std::make_unique( + espp::display_drivers::Config{.panel_io = lcd_.get(), + .write_command = nullptr, + .read_command = nullptr, + .lcd_send_lines = nullptr, + .reset_pin = lcd_reset_io, + .data_command_pin = lcd_dc_io, + .reset_value = reset_value, + .invert_colors = invert_colors, + .swap_color_order = swap_color_order, + .mirror_x = mirror_x, + .mirror_y = mirror_y}); + if (!display_driver_ || !display_driver_->initialize()) { + display_driver_.reset(); + lcd_.reset(); + lcd_spi_.reset(); + return false; + } // Initialize backlight PWM (moved out of Display) backlight_channel_configs_.push_back({.gpio = static_cast(backlight_io), .channel = LEDC_CHANNEL_0, @@ -282,7 +269,7 @@ bool MatouchRotaryDisplay::initialize_lcd() { } bool MatouchRotaryDisplay::initialize_display(size_t pixel_buffer_size) { - if (!lcd_handle_) { + if (!lcd_) { logger_.error( "LCD not initialized, you must call initialize_lcd() before initialize_display()!"); return false; @@ -294,11 +281,22 @@ bool MatouchRotaryDisplay::initialize_display(size_t pixel_buffer_size) { // initialize the display / lvgl using namespace std::chrono_literals; display_ = std::make_shared>( - Display::LvglConfig{.width = lcd_width_, - .height = lcd_height_, - .flush_callback = DisplayDriver::flush, - .rotation_callback = DisplayDriver::rotate, - .rotation = rotation}, + Display::LvglConfig{ + .width = lcd_width_, + .height = lcd_height_, + .flush_callback = + [this](lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { + if (display_driver_) { + display_driver_->flush(disp, area, color_map); + } + }, + .rotation_callback = + [this](const DisplayRotation &new_rotation) { + if (display_driver_) { + display_driver_->set_rotation(new_rotation); + } + }, + .rotation = rotation}, Display::OledConfig{ .set_brightness_callback = [this](float brightness) { this->brightness(brightness * 100.0f); }, @@ -329,133 +327,28 @@ std::shared_ptr> MatouchRotaryDisplay return display_; } -void IRAM_ATTR MatouchRotaryDisplay::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 MatouchRotaryDisplay::lcd_wait_lines() { + if (lcd_) { + lcd_->wait(); } } -void MatouchRotaryDisplay::write_command(uint8_t command, std::span parameters, - uint32_t user_data) { - lcd_wait_lines(); - memset(&trans[0], 0, sizeof(spi_transaction_t)); - memset(&trans[1], 0, sizeof(spi_transaction_t)); - - trans[0].length = 8; - trans[0].user = reinterpret_cast(user_data); - trans[0].flags = SPI_TRANS_USE_TXDATA; - trans[0].tx_data[0] = command; - - trans[1].length = parameters.size() * 8; - if (parameters.size() <= 4) { - // copy the data pointer to trans[1].tx_data - memcpy(trans[1].tx_data, parameters.data(), parameters.size()); - trans[1].flags = SPI_TRANS_USE_TXDATA; - } else if (!parameters.empty()) { - trans[1].tx_buffer = parameters.data(); - trans[1].flags = 0; - } - trans[1].user = reinterpret_cast( - user_data | (1 << static_cast(display_drivers::Flags::DC_LEVEL_BIT))); - - esp_err_t ret = spi_device_queue_trans(lcd_handle_, &trans[0], 10 / portTICK_PERIOD_MS); - if (ret != ESP_OK) { - logger_.error("Couldn't queue spi command trans for display: {} '{}'", ret, - esp_err_to_name(ret)); - } else { - ++num_queued_trans; - if (!parameters.empty()) { - ret = spi_device_queue_trans(lcd_handle_, &trans[1], 10 / portTICK_PERIOD_MS); - if (ret != ESP_OK) { - logger_.error("Couldn't queue spi data trans for display: {} '{}'", ret, - esp_err_to_name(ret)); - } else { - ++num_queued_trans; - } - } - } -} - -void IRAM_ATTR MatouchRotaryDisplay::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 MatouchRotaryDisplay::write_lcd_frame(const uint16_t xs, const uint16_t ys, const uint16_t width, const uint16_t height, uint8_t *data) { + if (!display_driver_) { + return; + } 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); + display_driver_->fill(nullptr, &area, data); } else { // don't have data, so clear the area (set to 0) - DisplayDriver::clear(xs, ys, width, height); + display_driver_->clear(xs, ys, width, height); } } @@ -493,3 +386,31 @@ float MatouchRotaryDisplay::brightness() const { } return 0.0f; // if no backlight, return 0 } + +size_t MatouchRotaryDisplay::rotated_display_width() const { + auto *display = lv_display_get_default(); + auto rotation = display ? lv_display_get_rotation(display) : LV_DISPLAY_ROTATION_0; + switch (rotation) { + case LV_DISPLAY_ROTATION_90: + case LV_DISPLAY_ROTATION_270: + return lcd_height_; + case LV_DISPLAY_ROTATION_0: + case LV_DISPLAY_ROTATION_180: + default: + return lcd_width_; + } +} + +size_t MatouchRotaryDisplay::rotated_display_height() const { + auto *display = lv_display_get_default(); + auto rotation = display ? lv_display_get_rotation(display) : LV_DISPLAY_ROTATION_0; + switch (rotation) { + case LV_DISPLAY_ROTATION_90: + case LV_DISPLAY_ROTATION_270: + return lcd_width_; + case LV_DISPLAY_ROTATION_0: + case LV_DISPLAY_ROTATION_180: + default: + return lcd_height_; + } +} diff --git a/components/motorgo-mini/CMakeLists.txt b/components/motorgo-mini/CMakeLists.txt index 68fb249ca..8b2ee2230 100644 --- a/components/motorgo-mini/CMakeLists.txt +++ b/components/motorgo-mini/CMakeLists.txt @@ -1,6 +1,6 @@ idf_component_register( INCLUDE_DIRS "include" SRC_DIRS "src" - REQUIRES base_component interrupt filters led math mt6701 pid task bldc_driver bldc_motor i2c adc esp_driver_spi + REQUIRES adc base_component bldc_driver bldc_motor esp_driver_spi filters i2c interrupt led math mt6701 pid spi task REQUIRED_IDF_TARGETS "esp32s3" ) diff --git a/components/motorgo-mini/idf_component.yml b/components/motorgo-mini/idf_component.yml index 844db8d9c..711ed6c8e 100644 --- a/components/motorgo-mini/idf_component.yml +++ b/components/motorgo-mini/idf_component.yml @@ -28,6 +28,7 @@ dependencies: espp/math: '>=1.0' espp/mt6701: '>=1.0' espp/pid: '>=1.0' + espp/spi: '>=1.0' espp/task: '>=1.0' targets: - esp32s3 diff --git a/components/motorgo-mini/include/motorgo-mini.hpp b/components/motorgo-mini/include/motorgo-mini.hpp index 3c696594c..609cb634c 100644 --- a/components/motorgo-mini/include/motorgo-mini.hpp +++ b/components/motorgo-mini/include/motorgo-mini.hpp @@ -15,6 +15,7 @@ #include "mt6701.hpp" #include "oneshot_adc.hpp" #include "simple_lowpass_filter.hpp" +#include "spi.hpp" namespace espp { /// This class acts as a board support component for the MotorGo-Mini board. @@ -373,7 +374,7 @@ class MotorGoMini : public BaseComponent { float breathe(float breathing_period, uint64_t start_us, bool restart = false); - bool read_encoder(const auto &encoder_handle, uint8_t *data, size_t size); + bool read_encoder(const std::shared_ptr &encoder_device, uint8_t *data, size_t size); /// I2C bus for external communication I2c external_i2c_{{.port = I2C_PORT, @@ -382,24 +383,19 @@ class MotorGoMini : public BaseComponent { .sda_pullup_en = GPIO_PULLUP_ENABLE, .scl_pullup_en = GPIO_PULLUP_ENABLE}}; - /// SPI bus for communication with the Encoders - spi_bus_config_t encoder_spi_bus_config_; - - // SPI handles for the encoders - spi_device_interface_config_t encoder1_config; - spi_device_handle_t encoder1_handle_; - spi_device_interface_config_t encoder2_config; - spi_device_handle_t encoder2_handle_; + std::unique_ptr encoder_spi_; + std::shared_ptr encoder1_spi_device_; + std::shared_ptr encoder2_spi_device_; // Encoders Encoder::Config encoder1_config_{.read = [this](uint8_t *data, size_t size) -> bool { - return read_encoder(encoder1_handle_, data, size); + return read_encoder(encoder1_spi_device_, data, size); }, .update_period = std::chrono::duration(core_update_period_us / 1e6f), .log_level = get_log_level()}; Encoder::Config encoder2_config_{.read = [this](uint8_t *data, size_t size) -> bool { - return read_encoder(encoder2_handle_, data, size); + return read_encoder(encoder2_spi_device_, data, size); }, .update_period = std::chrono::duration(core_update_period_us / 1e6f), diff --git a/components/motorgo-mini/src/motorgo-mini.cpp b/components/motorgo-mini/src/motorgo-mini.cpp index 1efcbe936..905b39a35 100644 --- a/components/motorgo-mini/src/motorgo-mini.cpp +++ b/components/motorgo-mini/src/motorgo-mini.cpp @@ -150,47 +150,37 @@ void MotorGoMini::always_init() { } void MotorGoMini::init_spi() { - // Initialize the SPI bus for the encoders - memset(&encoder_spi_bus_config_, 0, sizeof(encoder_spi_bus_config_)); - encoder_spi_bus_config_.mosi_io_num = -1; - encoder_spi_bus_config_.miso_io_num = ENCODER_SPI_MISO_PIN; - encoder_spi_bus_config_.sclk_io_num = ENCODER_SPI_SCLK_PIN; - encoder_spi_bus_config_.quadwp_io_num = -1; - encoder_spi_bus_config_.quadhd_io_num = -1; - encoder_spi_bus_config_.max_transfer_sz = 100; - // encoder_spi_bus_config_.isr_cpu_id = 0; // set to the same core as the esp-timer task (which - // runs the encoders) - auto err = spi_bus_initialize(ENCODER_SPI_HOST, &encoder_spi_bus_config_, SPI_DMA_CH_AUTO); - if (err != ESP_OK) { - logger_.error("Failed to initialize SPI bus for encoders: {}", esp_err_to_name(err)); - return; - } - - // Initialize the encoder 1 - memset(&encoder1_config, 0, sizeof(encoder1_config)); - encoder1_config.mode = 0; - encoder1_config.clock_speed_hz = ENCODER_SPI_CLK_SPEED; - encoder1_config.queue_size = 1; - encoder1_config.spics_io_num = ENCODER_1_CS_PIN; - // encoder1_config.cs_ena_pretrans = 2; - // encoder1_config.input_delay_ns = 30; - err = spi_bus_add_device(ENCODER_SPI_HOST, &encoder1_config, &encoder1_handle_); - if (err != ESP_OK) { - logger_.error("Failed to initialize Encoder 1: {}", esp_err_to_name(err)); + encoder_spi_ = std::make_unique(Spi::Config{ + .host = ENCODER_SPI_HOST, + .sclk_io_num = ENCODER_SPI_SCLK_PIN, + .mosi_io_num = GPIO_NUM_NC, + .miso_io_num = ENCODER_SPI_MISO_PIN, + .max_transfer_sz = 100, + .log_level = get_log_level(), + }); + std::error_code ec; + encoder1_spi_device_ = encoder_spi_->add_device( + Spi::DeviceConfig{ + .mode = 0, + .clock_speed_hz = ENCODER_SPI_CLK_SPEED, + .cs_io_num = ENCODER_1_CS_PIN, + .queue_size = 1, + }, + ec); + if (ec || !encoder1_spi_device_) { + logger_.error("Failed to initialize Encoder 1 SPI device: {}", ec.message()); return; } - - // Initialize the encoder 2 - memset(&encoder2_config, 0, sizeof(encoder2_config)); - encoder2_config.mode = 0; - encoder2_config.clock_speed_hz = ENCODER_SPI_CLK_SPEED; - encoder2_config.queue_size = 1; - encoder2_config.spics_io_num = ENCODER_2_CS_PIN; - // encoder2_config.cs_ena_pretrans = 2; - // encoder2_config.input_delay_ns = 30; - err = spi_bus_add_device(ENCODER_SPI_HOST, &encoder2_config, &encoder2_handle_); - if (err != ESP_OK) { - logger_.error("Failed to initialize Encoder 2: {}", esp_err_to_name(err)); + encoder2_spi_device_ = encoder_spi_->add_device( + Spi::DeviceConfig{ + .mode = 0, + .clock_speed_hz = ENCODER_SPI_CLK_SPEED, + .cs_io_num = ENCODER_2_CS_PIN, + .queue_size = 1, + }, + ec); + if (ec || !encoder2_spi_device_) { + logger_.error("Failed to initialize Encoder 2 SPI device: {}", ec.message()); return; } } @@ -206,26 +196,11 @@ float MotorGoMini::breathe(float breathing_period, uint64_t start_us, bool resta return gaussian_(t); } -bool IRAM_ATTR MotorGoMini::read_encoder(const auto &encoder_handle, uint8_t *data, size_t size) { - static constexpr uint8_t SPIBUS_READ = 0x80; - spi_transaction_t t{}; - t.addr = SPIBUS_READ; - t.length = size * 8; - t.rxlength = size * 8; - t.rx_buffer = data; - if (size <= 4) { - t.flags = SPI_TRANS_USE_RXDATA; - t.rx_buffer = nullptr; - } - esp_err_t err = spi_device_transmit(encoder_handle, &t); - if (err != ESP_OK) { +bool IRAM_ATTR MotorGoMini::read_encoder(const std::shared_ptr &encoder_device, + uint8_t *data, size_t size) { + if (!encoder_device) { return false; } - if (size <= 4) { - // copy the data from the rx_data field - for (size_t i = 0; i < size; i++) { - data[i] = t.rx_data[i]; - } - } - return true; + std::error_code ec; + return encoder_device->read(std::span(data, size), {}, ec); } diff --git a/components/mt6701/example/CMakeLists.txt b/components/mt6701/example/CMakeLists.txt index 041db1485..f4bb4333f 100644 --- a/components/mt6701/example/CMakeLists.txt +++ b/components/mt6701/example/CMakeLists.txt @@ -12,7 +12,7 @@ set(EXTRA_COMPONENT_DIRS set( COMPONENTS - "main esptool_py driver esp_driver_spi filters i2c task mt6701" + "main esptool_py driver esp_driver_spi filters i2c spi task mt6701" CACHE STRING "List of components to include" ) diff --git a/components/mt6701/example/README.md b/components/mt6701/example/README.md index cb57140a8..e84f4da6a 100644 --- a/components/mt6701/example/README.md +++ b/components/mt6701/example/README.md @@ -12,9 +12,9 @@ the `format` component to print the data to the console in CSV format. ### Hardware Required -This requires a MT6701 magnetic encoder dev board with an diametric magnet and -the MT6701 dev board should be connected to the ESP dev board you choose via -I2C. +This requires a MT6701 magnetic encoder dev board with an diametric magnet. The +MT6701 dev board can be connected to the ESP dev board you choose via either +I2C or SSI / SPI, depending on the example configuration you select. ### Build and Flash diff --git a/components/mt6701/example/main/CMakeLists.txt b/components/mt6701/example/main/CMakeLists.txt index a941e22ba..93680b765 100644 --- a/components/mt6701/example/main/CMakeLists.txt +++ b/components/mt6701/example/main/CMakeLists.txt @@ -1,2 +1,3 @@ idf_component_register(SRC_DIRS "." - INCLUDE_DIRS ".") + INCLUDE_DIRS "." + REQUIRES filters i2c mt6701 spi task) diff --git a/components/mt6701/example/main/mt6701_example.cpp b/components/mt6701/example/main/mt6701_example.cpp index 88f8c5e7a..0e0d33aca 100644 --- a/components/mt6701/example/main/mt6701_example.cpp +++ b/components/mt6701/example/main/mt6701_example.cpp @@ -2,11 +2,10 @@ #include #include -#include - #include "butterworth_filter.hpp" #include "i2c.hpp" #include "mt6701.hpp" +#include "spi.hpp" #include "task.hpp" using namespace std::chrono_literals; @@ -95,36 +94,30 @@ extern "C" void app_main(void) { //! [mt6701 ssi example] std::atomic quit_test = false; - // make the SSI (SPI) that we'll use to communicate - - // create the spi host - spi_device_handle_t encoder_spi_handle; - spi_bus_config_t buscfg; - memset(&buscfg, 0, sizeof(buscfg)); - buscfg.mosi_io_num = -1; - buscfg.miso_io_num = CONFIG_EXAMPLE_SPI_MISO_GPIO; - buscfg.sclk_io_num = CONFIG_EXAMPLE_SPI_SCLK_GPIO; - buscfg.quadwp_io_num = -1; - buscfg.quadhd_io_num = -1; - buscfg.max_transfer_sz = 32; - - // create the spi device - spi_device_interface_config_t devcfg; - memset(&devcfg, 0, sizeof(devcfg)); - devcfg.mode = 0; - devcfg.clock_speed_hz = CONFIG_EXAMPLE_SPI_CLOCK_SPEED; // Supports 64ns clock period, 15.625MHz - devcfg.input_delay_ns = 0; - devcfg.spics_io_num = CONFIG_EXAMPLE_SPI_CS_GPIO; - devcfg.queue_size = 1; - - esp_err_t ret; - // Initialize the SPI bus - auto spi_num = SPI2_HOST; - ret = spi_bus_initialize(spi_num, &buscfg, SPI_DMA_CH_AUTO); - ESP_ERROR_CHECK(ret); - // Attach the LCD to the SPI bus - ret = spi_bus_add_device(spi_num, &devcfg, &encoder_spi_handle); - ESP_ERROR_CHECK(ret); + // make the SPI bus and SSI device that we'll use to communicate + espp::Spi spi({ + .host = SPI2_HOST, + .sclk_io_num = static_cast(CONFIG_EXAMPLE_SPI_SCLK_GPIO), + .mosi_io_num = GPIO_NUM_NC, + .miso_io_num = static_cast(CONFIG_EXAMPLE_SPI_MISO_GPIO), + .max_transfer_sz = 32, + .log_level = espp::Logger::Verbosity::WARN, + }); + std::error_code ec; + auto encoder_spi_device = spi.add_device( + { + .mode = 0, + .clock_speed_hz = CONFIG_EXAMPLE_SPI_CLOCK_SPEED, // Supports 64ns clock period, + // 15.625MHz + .input_delay_ns = 0, + .cs_io_num = static_cast(CONFIG_EXAMPLE_SPI_CS_GPIO), + .queue_size = 1, + }, + ec); + if (ec || !encoder_spi_device) { + fmt::print("Failed to initialize SPI device: {}\n", ec.message()); + return; + } // make the velocity filter static constexpr float filter_cutoff_hz = 10.0f; @@ -136,34 +129,8 @@ extern "C" void app_main(void) { // now make the mt6701 which decodes the data using Mt6701 = espp::Mt6701; Mt6701 mt6701({.read = [&](uint8_t *data, size_t len) -> bool { - // we can use the SPI_TRANS_USE_RXDATA since our length is <= 4 bytes (32 - // bits), this means we can directly use the tarnsaction's rx_data field - static constexpr uint8_t SPIBUS_READ = 0x80; - spi_transaction_t t = { - .flags = 0, - .cmd = 0, - .addr = SPIBUS_READ, - .length = len * 8, - .rxlength = len * 8, - .user = nullptr, - .tx_buffer = nullptr, - .rx_buffer = data, - }; - if (len <= 4) { - t.flags = SPI_TRANS_USE_RXDATA; - t.rx_buffer = nullptr; - } - esp_err_t err = spi_device_transmit(encoder_spi_handle, &t); - if (err != ESP_OK) { - return false; - } - if (len <= 4) { - // copy the data from the rx_data field - for (size_t i = 0; i < len; i++) { - data[i] = t.rx_data[i]; - } - } - return true; + std::error_code read_ec; + return encoder_spi_device->read(std::span(data, len), {}, read_ec); }, .velocity_filter = filter_fn, .update_period = std::chrono::duration(encoder_update_period), diff --git a/components/seeed-studio-round-display/CMakeLists.txt b/components/seeed-studio-round-display/CMakeLists.txt index ecaf29151..cbb83778f 100644 --- a/components/seeed-studio-round-display/CMakeLists.txt +++ b/components/seeed-studio-round-display/CMakeLists.txt @@ -1,6 +1,6 @@ idf_component_register( INCLUDE_DIRS "include" SRC_DIRS "src" - REQUIRES driver base_component display display_drivers i2c input_drivers interrupt task chsc6x + REQUIRES driver base_component chsc6x display display_drivers i2c input_drivers interrupt spi task REQUIRED_IDF_TARGETS "esp32s3" ) 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 index 2190170c0..0bc476958 100644 --- 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 @@ -1,20 +1,29 @@ +#include #include -#include #include -#include #include "seeed-studio-round-display.hpp" using namespace std::chrono_literals; static constexpr size_t MAX_CIRCLES = 100; -static std::deque circles; +struct Circle { + int x{0}; + int y{0}; + int radius{0}; + bool visible{false}; +}; +static std::array circles; +static size_t next_circle_index = 0; +static size_t visible_circle_count = 0; +static lv_obj_t *circle_layer = nullptr; static std::recursive_mutex lvgl_mutex; +static bool initialize_circle_layer(int width, int height); +static void draw_circle_layer(lv_event_t *event); +static void invalidate_circle_area(const Circle &circle); 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( @@ -45,10 +54,12 @@ extern "C" void app_main(void) { previous_touchpad_data = touchpad_data; // if the button is pressed, clear the circles if (touchpad_data.btn_state) { + std::lock_guard lock(lvgl_mutex); clear_circles(); } // if there is a touch point, draw a circle if (touchpad_data.num_touch_points > 0) { + std::lock_guard lock(lvgl_mutex); draw_circle(touchpad_data.x, touchpad_data.y, 10); } } @@ -66,16 +77,17 @@ extern "C" void app_main(void) { 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_size(bg, round_display.rotated_display_width(), + round_display.rotated_display_height()); lv_obj_set_style_bg_color(bg, lv_color_make(0, 0, 0), 0); + if (!initialize_circle_layer(round_display.rotated_display_width(), + round_display.rotated_display_height())) { + logger.error("Failed to initialize circle layer!"); + return; + } // add text in the center of the screen lv_obj_t *label = lv_label_create(lv_screen_active()); @@ -91,7 +103,6 @@ extern "C" void app_main(void) { 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 @@ -102,12 +113,52 @@ extern "C" void app_main(void) { 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); + static auto update_layout = [&]() { + int width = round_display.rotated_display_width(); + int height = round_display.rotated_display_height(); + lv_obj_set_size(bg, width, height); + lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); + lv_obj_align(btn, LV_ALIGN_TOP_MID, 0, 0); + lv_obj_align(btn_clear, LV_ALIGN_BOTTOM_MID, 0, 0); + if (circle_layer) { + lv_obj_set_size(circle_layer, width, height); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_move_foreground(circle_layer); + lv_obj_invalidate(circle_layer); + } + }; + static auto rotate_display = [&]() { + std::lock_guard lock(lvgl_mutex); + clear_circles(); + static auto rotation = LV_DISPLAY_ROTATION_0; + rotation = static_cast((static_cast(rotation) + 1) % 4); + lv_display_t *disp = lv_display_get_default(); + lv_disp_set_rotation(disp, rotation); + update_layout(); + }; + lv_obj_add_event_cb( + btn, [](auto event) { rotate_display(); }, LV_EVENT_PRESSED, nullptr); + lv_obj_add_event_cb( + btn_clear, + [](auto event) { + std::lock_guard lock(lvgl_mutex); + clear_circles(); + }, + LV_EVENT_PRESSED, nullptr); + update_layout(); // 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); + lv_obj_move_foreground(circle_layer); + + // initialize the touchpad after the circle layer exists so touch events can + // update it immediately. + if (!round_display.initialize_touch(touch_callback)) { + logger.error("Failed to initialize touchpad!"); + return; + } // 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 { @@ -134,42 +185,100 @@ extern "C" void app_main(void) { //! [seeed studio round display example] } -static void on_rotate_pressed(lv_event_t *event) { - clear_circles(); - std::lock_guard lock(lvgl_mutex); - static auto rotation = LV_DISPLAY_ROTATION_0; - rotation = static_cast((static_cast(rotation) + 1) % 4); - lv_display_t *disp = lv_display_get_default(); - lv_disp_set_rotation(disp, rotation); +static bool initialize_circle_layer(int width, int height) { + if (circle_layer) { + return true; + } + circle_layer = lv_obj_create(lv_screen_active()); + if (!circle_layer) { + return false; + } + lv_obj_remove_style_all(circle_layer); + lv_obj_set_size(circle_layer, width, height); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_CLICKABLE); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(circle_layer, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(circle_layer, 0, 0); + lv_obj_set_style_outline_width(circle_layer, 0, 0); + lv_obj_set_style_shadow_width(circle_layer, 0, 0); + lv_obj_add_event_cb(circle_layer, draw_circle_layer, LV_EVENT_DRAW_MAIN, nullptr); + return true; +} + +static void draw_circle_layer(lv_event_t *event) { + if (visible_circle_count == 0) { + return; + } + auto *obj = static_cast(lv_event_get_current_target(event)); + auto *layer = lv_event_get_layer(event); + lv_area_t obj_coords; + lv_obj_get_coords(obj, &obj_coords); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.base.layer = layer; + rect_dsc.radius = LV_RADIUS_CIRCLE; + rect_dsc.bg_opa = LV_OPA_70; + rect_dsc.bg_color = lv_color_make(0, 255, 255); + rect_dsc.border_width = 0; + rect_dsc.outline_width = 0; + rect_dsc.shadow_width = 0; + + for (const auto &circle : circles) { + if (!circle.visible) { + continue; + } + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_draw_rect(layer, &rect_dsc, &coords); + } } -// cppcheck-suppress constParameterCallback -static void on_clear_pressed(lv_event_t *event) { clear_circles(); } +static void invalidate_circle_area(const Circle &circle) { + if (!circle_layer || circle.radius <= 0) { + return; + } + + lv_area_t obj_coords; + lv_obj_get_coords(circle_layer, &obj_coords); + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_obj_invalidate_area(circle_layer, &coords); +} static void draw_circle(int x0, int y0, int radius) { - std::lock_guard 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(); + if (!circle_layer) { + return; + } + lv_obj_move_foreground(circle_layer); + Circle previous_circle = circles[next_circle_index]; + circles[next_circle_index] = {.x = x0, .y = y0, .radius = radius, .visible = true}; + next_circle_index = (next_circle_index + 1) % circles.size(); + if (visible_circle_count < circles.size()) { + visible_circle_count++; } - 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); + if (previous_circle.visible) { + invalidate_circle_area(previous_circle); + } + invalidate_circle_area(circles[(next_circle_index + circles.size() - 1) % circles.size()]); } static void clear_circles() { - std::lock_guard lock(lvgl_mutex); - // remove the circles from lvgl - for (auto circle : circles) { - lv_obj_delete(circle); + for (auto &circle : circles) { + if (circle.visible) { + invalidate_circle_area(circle); + } + circle.visible = false; } - // clear the vector - circles.clear(); + next_circle_index = 0; + visible_circle_count = 0; } diff --git a/components/seeed-studio-round-display/idf_component.yml b/components/seeed-studio-round-display/idf_component.yml index da418e915..b1f430f4f 100644 --- a/components/seeed-studio-round-display/idf_component.yml +++ b/components/seeed-studio-round-display/idf_component.yml @@ -25,6 +25,7 @@ dependencies: espp/i2c: '>=1.0' espp/input_drivers: '>=1.0' espp/interrupt: '>=1.0' + espp/spi: '>=1.0' espp/task: '>=1.0' targets: - esp32s3 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 index cbc3684ec..b1d8c87bf 100644 --- a/components/seeed-studio-round-display/include/seeed-studio-round-display.hpp +++ b/components/seeed-studio-round-display/include/seeed-studio-round-display.hpp @@ -12,6 +12,7 @@ #include "i2c.hpp" #include "interrupt.hpp" #include "led.hpp" +#include "spi.hpp" #include "touchpad_input.hpp" namespace espp { @@ -181,6 +182,14 @@ class SsRoundDisplay : public espp::BaseComponent { /// \return The height of the LCD in pixels static constexpr size_t lcd_height() { return lcd_height_; } + /// Get the display width in pixels, according to the current orientation + /// \return The display width in pixels, according to the current orientation + size_t rotated_display_width() const; + + /// Get the display height in pixels, according to the current orientation + /// \return The display height in pixels, according to the current orientation + size_t rotated_display_height() const; + /// 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; } @@ -225,15 +234,6 @@ class SsRoundDisplay : public espp::BaseComponent { /// \note This is null unless initialize_display() has been called uint8_t *frame_buffer1() const; - /// Write command and optional parameters to the LCD - /// \param command The command to write - /// \param parameters The command parameters to write - /// \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_command(uint8_t command, std::span parameters, uint32_t user_data); - /// Write a frame to the LCD /// \param x The x coordinate /// \param y The y coordinate @@ -245,17 +245,6 @@ class SsRoundDisplay : public espp::BaseComponent { 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); @@ -313,13 +302,9 @@ class SsRoundDisplay : public espp::BaseComponent { std::shared_ptr> display_; std::vector backlight_channel_configs_{}; std::shared_ptr backlight_{}; - /// 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 num_queued_trans = 0; + std::unique_ptr display_driver_; + std::unique_ptr lcd_spi_; + std::unique_ptr lcd_; uint8_t *frame_buffer0_{nullptr}; uint8_t *frame_buffer1_{nullptr}; 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 index 6e38bbad1..33b3d480a 100644 --- a/components/seeed-studio-round-display/src/seeed-studio-round-display.cpp +++ b/components/seeed-studio-round-display/src/seeed-studio-round-display.cpp @@ -155,77 +155,65 @@ TouchpadData SsRoundDisplay::touchpad_convert(const TouchpadData &data) const { 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). -// -// cppcheck-suppress constParameterCallback -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. -// -// cppcheck-suppress constParameterCallback -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_display_get_default(); - lv_display_flush_ready(disp); - } +static void lcd_spi_flush_ready(uint32_t) { + lv_display_t *disp = lv_display_get_default(); + lv_display_flush_ready(disp); } bool SsRoundDisplay::initialize_lcd() { - if (lcd_handle_ || backlight_) { + if (lcd_ || backlight_) { 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 = SPI_MAX_TRANSFER_BYTES; - - 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{ - .write_command = std::bind(&SsRoundDisplay::write_command, 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}); + lcd_spi_ = std::make_unique(Spi::Config{ + .host = lcd_spi_num, + .sclk_io_num = pin_config_.sck, + .mosi_io_num = pin_config_.mosi, + .miso_io_num = GPIO_NUM_NC, + .max_transfer_sz = SPI_MAX_TRANSFER_BYTES, + .log_level = get_log_level(), + }); + lcd_ = std::make_unique(SpiPanelIo::Config{ + .spi = lcd_spi_.get(), + .device_config = + { + .mode = 0, + .clock_speed_hz = lcd_clock_speed, + .input_delay_ns = 0, + .cs_io_num = pin_config_.lcd_cs, + .queue_size = 6, + }, + .data_command_io = pin_config_.lcd_dc, + .data_command_bit_mask = DC_LEVEL_BIT, + .post_transaction_callback_bit_mask = FLUSH_BIT, + .post_transaction_callback = lcd_spi_flush_ready, + .log_level = get_log_level(), + }); + if (!lcd_->initialized()) { + lcd_.reset(); + lcd_spi_.reset(); + return false; + } + display_driver_ = std::make_unique( + espp::display_drivers::Config{.panel_io = lcd_.get(), + .write_command = nullptr, + .read_command = nullptr, + .lcd_send_lines = nullptr, + .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}); + if (!display_driver_ || !display_driver_->initialize()) { + display_driver_.reset(); + lcd_.reset(); + lcd_spi_.reset(); + return false; + } // Initialize backlight PWM (moved out of Display) backlight_channel_configs_.push_back({.gpio = static_cast(pin_config_.lcd_backlight), .channel = LEDC_CHANNEL_0, @@ -241,7 +229,7 @@ bool SsRoundDisplay::initialize_lcd() { } bool SsRoundDisplay::initialize_display(size_t pixel_buffer_size) { - if (!lcd_handle_) { + if (!lcd_) { logger_.error( "LCD not initialized, you must call initialize_lcd() before initialize_display()!"); return false; @@ -253,11 +241,22 @@ bool SsRoundDisplay::initialize_display(size_t pixel_buffer_size) { // initialize the display / lvgl using namespace std::chrono_literals; display_ = std::make_shared>( - Display::LvglConfig{.width = lcd_width_, - .height = lcd_height_, - .flush_callback = DisplayDriver::flush, - .rotation_callback = DisplayDriver::rotate, - .rotation = rotation}, + Display::LvglConfig{ + .width = lcd_width_, + .height = lcd_height_, + .flush_callback = + [this](lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { + if (display_driver_) { + display_driver_->flush(disp, area, color_map); + } + }, + .rotation_callback = + [this](const DisplayRotation &new_rotation) { + if (display_driver_) { + display_driver_->set_rotation(new_rotation); + } + }, + .rotation = rotation}, Display::OledConfig{ .set_brightness_callback = [this](float brightness) { this->brightness(brightness * 100.0f); }, @@ -288,132 +287,27 @@ std::shared_ptr> SsRoundDisplay::display() 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 SsRoundDisplay::lcd_wait_lines() { + if (lcd_) { + lcd_->wait(); } } -void IRAM_ATTR SsRoundDisplay::write_command(uint8_t command, std::span parameters, - uint32_t user_data) { - lcd_wait_lines(); - memset(&trans[0], 0, sizeof(spi_transaction_t)); - memset(&trans[1], 0, sizeof(spi_transaction_t)); - - trans[0].length = 8; - trans[0].user = reinterpret_cast(user_data); - trans[0].flags = SPI_TRANS_USE_TXDATA; - trans[0].tx_data[0] = command; - - trans[1].length = parameters.size() * 8; - if (parameters.size() <= 4) { - // copy the data pointer to trans[1].tx_data - memcpy(trans[1].tx_data, parameters.data(), parameters.size()); - trans[1].flags = SPI_TRANS_USE_TXDATA; - } else if (!parameters.empty()) { - trans[1].tx_buffer = parameters.data(); - trans[1].flags = 0; - } - trans[1].user = reinterpret_cast( - user_data | (1 << static_cast(display_drivers::Flags::DC_LEVEL_BIT))); - - esp_err_t ret = spi_device_queue_trans(lcd_handle_, &trans[0], 10 / portTICK_PERIOD_MS); - if (ret != ESP_OK) { - logger_.error("Couldn't queue spi command trans for display: {} '{}'", ret, - esp_err_to_name(ret)); - } else { - ++num_queued_trans; - if (!parameters.empty()) { - ret = spi_device_queue_trans(lcd_handle_, &trans[1], 10 / portTICK_PERIOD_MS); - if (ret != ESP_OK) { - logger_.error("Couldn't queue spi data 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 (!display_driver_) { + return; + } 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); + display_driver_->fill(nullptr, &area, data); } else { // don't have data, so clear the area (set to 0) - DisplayDriver::clear(xs, ys, width, height); + display_driver_->clear(xs, ys, width, height); } } @@ -451,3 +345,31 @@ float SsRoundDisplay::brightness() const { } return 0.0f; // if no backlight, return 0 } + +size_t SsRoundDisplay::rotated_display_width() const { + auto *display = lv_display_get_default(); + auto rotation = display ? lv_display_get_rotation(display) : LV_DISPLAY_ROTATION_0; + switch (rotation) { + case LV_DISPLAY_ROTATION_90: + case LV_DISPLAY_ROTATION_270: + return lcd_height_; + case LV_DISPLAY_ROTATION_0: + case LV_DISPLAY_ROTATION_180: + default: + return lcd_width_; + } +} + +size_t SsRoundDisplay::rotated_display_height() const { + auto *display = lv_display_get_default(); + auto rotation = display ? lv_display_get_rotation(display) : LV_DISPLAY_ROTATION_0; + switch (rotation) { + case LV_DISPLAY_ROTATION_90: + case LV_DISPLAY_ROTATION_270: + return lcd_width_; + case LV_DISPLAY_ROTATION_0: + case LV_DISPLAY_ROTATION_180: + default: + return lcd_height_; + } +} diff --git a/components/smartpanlee-sc01-plus/README.md b/components/smartpanlee-sc01-plus/README.md index d3cd90859..ddae02e39 100755 --- a/components/smartpanlee-sc01-plus/README.md +++ b/components/smartpanlee-sc01-plus/README.md @@ -19,6 +19,7 @@ path, microSD card, and exposed peripheral pin mappings. - I2S speaker playback with software volume, mute, and sample-rate control - SPI microSD card mounting helpers - Exposed I2S, RS-485, and external GPIO pin maps for application use +- Object-style `espp::St7796` display-controller integration for the LCD path ## Notes @@ -34,4 +35,4 @@ path, microSD card, and exposed peripheral pin mappings. The [example](./example) shows how to initialize the display, register touch input, play a click sound through the speaker path, adjust backlight -brightness, and optionally mount the microSD card. +brightness, rotate the display, and optionally mount the microSD card. diff --git a/components/smartpanlee-sc01-plus/example/README.md b/components/smartpanlee-sc01-plus/example/README.md index 6338ffe00..caca7ca07 100755 --- a/components/smartpanlee-sc01-plus/example/README.md +++ b/components/smartpanlee-sc01-plus/example/README.md @@ -9,10 +9,12 @@ BSP component on the Smart Panlee SC01 Plus touchscreen display module. - ST7796 display initialization over the ESP32-S3 8-bit parallel LCD bus - FT5x06 touch handling with LVGL integration +- Foreground custom-drawn touch trail with bounded invalidation for smooth redraws - I2S speaker playback with a bundled touch-click sound - Backlight brightness control - Optional microSD mounting and filesystem inspection - Published peripheral pin maps for I2S, RS-485, and external GPIOs +- Rotation-aware UI layout with wrapped instructions that stay on-screen in portrait/landscape ## Hardware Required @@ -24,7 +26,8 @@ BSP component on the Smart Panlee SC01 Plus touchscreen display module. Build the project and flash it to the board, then run monitor tool to view serial output and verify that touching the screen draws circles and plays the -embedded click sound: +embedded click sound. The refresh button rotates the display and the label will +reflow to stay within the visible screen width: ``` idf.py -p PORT flash monitor diff --git a/components/smartpanlee-sc01-plus/example/main/smartpanlee_sc01_plus_example.cpp b/components/smartpanlee-sc01-plus/example/main/smartpanlee_sc01_plus_example.cpp index ef1e4fe14..37bb89eaf 100755 --- a/components/smartpanlee-sc01-plus/example/main/smartpanlee_sc01_plus_example.cpp +++ b/components/smartpanlee-sc01-plus/example/main/smartpanlee_sc01_plus_example.cpp @@ -3,6 +3,8 @@ * @brief Smart Panlee SC01 Plus BSP Example */ +#include +#include #include #include #include @@ -14,14 +16,26 @@ using namespace std::chrono_literals; static constexpr size_t MAX_CIRCLES = 100; -static std::vector circles; +struct Circle { + int x{0}; + int y{0}; + int radius{0}; + bool visible{false}; +}; +static std::array circles; static size_t next_circle_index = 0; +static size_t visible_circle_count = 0; static std::vector audio_bytes; static std::recursive_mutex lvgl_mutex; static lv_obj_t *background = nullptr; +static lv_obj_t *circle_layer = nullptr; +static lv_obj_t *info_label = nullptr; +static bool initialize_circle_layer(int width, int height); +static void draw_circle_layer(lv_event_t *event); +static void invalidate_circle_area(const Circle &circle); static void draw_circle(int x0, int y0, int radius); static void clear_circles(); -static void initialize_circles(); +static void update_label_layout(int width); static void rotate_display(); static bool load_audio(size_t &out_size, size_t &out_sample_rate); static void play_click(espp::SmartPanleeSc01Plus &board); @@ -29,7 +43,6 @@ static void play_click(espp::SmartPanleeSc01Plus &board); extern "C" void app_main(void) { espp::Logger logger({.tag = "SC01 Plus Example", .level = espp::Logger::Verbosity::INFO}); logger.info("Starting example!"); - circles.reserve(MAX_CIRCLES); //! [smartpanlee sc01 plus example] auto &board = espp::SmartPanleeSc01Plus::get(); @@ -57,10 +70,6 @@ extern "C" void app_main(void) { } }; - if (!board.initialize_touch(touch_callback)) { - logger.warn("Touch initialization did not complete cleanly"); - } - if (!board.initialize_audio()) { logger.warn("Audio initialization did not complete cleanly"); } else { @@ -91,12 +100,13 @@ extern "C" void app_main(void) { lv_obj_set_size(bg, board.display_width(), board.display_height()); lv_obj_set_style_bg_color(bg, lv_color_make(8, 12, 24), 0); - lv_obj_t *label = lv_label_create(lv_screen_active()); - lv_label_set_text(label, "Smart Panlee SC01 Plus\n\nTouch the screen to draw and play a click.\n" - "Press refresh to rotate.\nCheck serial output for SD card, pin, and " - "audio info."); - lv_obj_align(label, LV_ALIGN_TOP_LEFT, 16, 16); - lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_LEFT, 0); + info_label = lv_label_create(lv_screen_active()); + lv_label_set_text(info_label, "Smart Panlee SC01 Plus\n\nTouch the screen to draw and play a " + "click.\nPress refresh to rotate.\nCheck serial output for SD " + "card, pin, and audio info."); + lv_label_set_long_mode(info_label, LV_LABEL_LONG_WRAP); + lv_obj_set_style_text_align(info_label, LV_TEXT_ALIGN_LEFT, 0); + update_label_layout(static_cast(board.display_width())); lv_obj_t *btn = lv_btn_create(lv_screen_active()); lv_obj_set_size(btn, 56, 56); @@ -105,7 +115,10 @@ extern "C" void app_main(void) { lv_label_set_text(btn_label, LV_SYMBOL_REFRESH); lv_obj_align(btn_label, LV_ALIGN_CENTER, 0, 0); background = bg; - initialize_circles(); + if (!initialize_circle_layer(board.display_width(), board.display_height())) { + logger.error("Failed to initialize circle layer!"); + return; + } lv_obj_add_event_cb( btn, [](lv_event_t *event) { @@ -116,6 +129,11 @@ extern "C" void app_main(void) { lv_obj_set_scrollbar_mode(lv_screen_active(), LV_SCROLLBAR_MODE_OFF); lv_obj_clear_flag(lv_screen_active(), LV_OBJ_FLAG_SCROLLABLE); + lv_obj_move_foreground(circle_layer); + + if (!board.initialize_touch(touch_callback)) { + logger.warn("Touch initialization did not complete cleanly"); + } espp::Task lv_task({.callback = [](std::mutex &m, std::condition_variable &cv) -> bool { { @@ -145,44 +163,113 @@ extern "C" void app_main(void) { } } -static void draw_circle(int x0, int y0, int radius) { - if (circles.empty()) { +static bool initialize_circle_layer(int width, int height) { + if (circle_layer) { + return true; + } + circle_layer = lv_obj_create(lv_screen_active()); + if (!circle_layer) { + return false; + } + lv_obj_remove_style_all(circle_layer); + lv_obj_set_size(circle_layer, width, height); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_CLICKABLE); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(circle_layer, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(circle_layer, 0, 0); + lv_obj_set_style_outline_width(circle_layer, 0, 0); + lv_obj_set_style_shadow_width(circle_layer, 0, 0); + lv_obj_add_event_cb(circle_layer, draw_circle_layer, LV_EVENT_DRAW_MAIN, nullptr); + return true; +} + +static void draw_circle_layer(lv_event_t *event) { + if (visible_circle_count == 0) { return; } - auto *circle = circles[next_circle_index]; - lv_obj_set_size(circle, radius * 2, radius * 2); - lv_obj_align(circle, LV_ALIGN_TOP_LEFT, x0 - radius, y0 - radius); - lv_obj_clear_flag(circle, LV_OBJ_FLAG_HIDDEN); + auto *obj = static_cast(lv_event_get_current_target(event)); + auto *layer = lv_event_get_layer(event); + lv_area_t obj_coords; + lv_obj_get_coords(obj, &obj_coords); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.base.layer = layer; + rect_dsc.radius = LV_RADIUS_CIRCLE; + rect_dsc.bg_opa = LV_OPA_70; + rect_dsc.bg_color = lv_color_make(0, 255, 255); + rect_dsc.border_width = 0; + rect_dsc.outline_width = 0; + rect_dsc.shadow_width = 0; + + for (const auto &circle : circles) { + if (!circle.visible) { + continue; + } + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_draw_rect(layer, &rect_dsc, &coords); + } +} + +static void invalidate_circle_area(const Circle &circle) { + if (!circle_layer || circle.radius <= 0) { + return; + } + + lv_area_t obj_coords; + lv_obj_get_coords(circle_layer, &obj_coords); + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_obj_invalidate_area(circle_layer, &coords); +} +static void draw_circle(int x0, int y0, int radius) { + if (!circle_layer) { + return; + } + lv_obj_move_foreground(circle_layer); + Circle previous_circle = circles[next_circle_index]; + circles[next_circle_index] = {.x = x0, .y = y0, .radius = radius, .visible = true}; next_circle_index = (next_circle_index + 1) % circles.size(); + if (visible_circle_count < circles.size()) { + visible_circle_count++; + } + if (previous_circle.visible) { + invalidate_circle_area(previous_circle); + } + invalidate_circle_area(circles[(next_circle_index + circles.size() - 1) % circles.size()]); } static void clear_circles() { - for (auto *circle : circles) { - lv_obj_add_flag(circle, LV_OBJ_FLAG_HIDDEN); + for (auto &circle : circles) { + if (circle.visible) { + invalidate_circle_area(circle); + } + circle.visible = false; } next_circle_index = 0; + visible_circle_count = 0; } -static void initialize_circles() { - if (!circles.empty()) { +static void update_label_layout(int width) { + if (!info_label) { return; } - circles.reserve(MAX_CIRCLES); - for (size_t i = 0; i < MAX_CIRCLES; ++i) { - auto *circle = lv_obj_create(lv_screen_active()); - lv_obj_remove_style_all(circle); - lv_obj_set_size(circle, 20, 20); - lv_obj_set_style_radius(circle, LV_RADIUS_CIRCLE, 0); - lv_obj_set_style_bg_color(circle, lv_palette_main(LV_PALETTE_CYAN), 0); - lv_obj_set_style_bg_opa(circle, LV_OPA_70, 0); - lv_obj_set_style_border_width(circle, 0, 0); - lv_obj_clear_flag(circle, LV_OBJ_FLAG_CLICKABLE); - lv_obj_add_flag(circle, LV_OBJ_FLAG_HIDDEN); - circles.push_back(circle); - } + auto label_width = std::max(width - 96, 120); + lv_obj_set_width(info_label, label_width); + lv_obj_align(info_label, LV_ALIGN_TOP_LEFT, 16, 16); } static void rotate_display() { @@ -196,6 +283,13 @@ static void rotate_display() { if (background) { lv_obj_set_size(background, board.rotated_display_width(), board.rotated_display_height()); } + update_label_layout(static_cast(board.rotated_display_width())); + if (circle_layer) { + lv_obj_set_size(circle_layer, board.rotated_display_width(), board.rotated_display_height()); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_move_foreground(circle_layer); + lv_obj_invalidate(circle_layer); + } } static bool load_audio(size_t &out_size, size_t &out_sample_rate) { diff --git a/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp b/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp index 9d9bb0928..aa2582ad3 100755 --- a/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp +++ b/components/smartpanlee-sc01-plus/include/smartpanlee-sc01-plus.hpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include #include @@ -344,6 +345,7 @@ class SmartPanleeSc01Plus : public BaseComponent { touch_callback_t touch_callback_{nullptr}; std::shared_ptr> display_; + std::unique_ptr display_driver_; std::shared_ptr backlight_; std::vector backlight_channel_configs_; esp_lcd_i80_bus_handle_t lcd_bus_{nullptr}; diff --git a/components/smartpanlee-sc01-plus/src/smartpanlee-sc01-plus.cpp b/components/smartpanlee-sc01-plus/src/smartpanlee-sc01-plus.cpp old mode 100755 new mode 100644 index 82b446c8b..92799e4e9 --- a/components/smartpanlee-sc01-plus/src/smartpanlee-sc01-plus.cpp +++ b/components/smartpanlee-sc01-plus/src/smartpanlee-sc01-plus.cpp @@ -1,5 +1,13 @@ #include "smartpanlee-sc01-plus.hpp" +#include "esp_idf_version.h" +#ifndef ESP_IDF_VERSION_VAL +#define ESP_IDF_VERSION_VAL(major, minor, patch) (((major) << 16) | ((minor) << 8) | (patch)) +#endif +#ifndef ESP_IDF_VERSION +#define ESP_IDF_VERSION ESP_IDF_VERSION_VAL(0, 0, 0) +#endif + #include #include #include @@ -167,46 +175,43 @@ bool SmartPanleeSc01Plus::initialize_lcd() { } esp_lcd_i80_bus_config_t bus_config = { - .dc_gpio_num = lcd_dc_io, - .wr_gpio_num = lcd_wr_io, - .clk_src = LCD_CLK_SRC_DEFAULT, - .data_gpio_nums = {lcd_d0_io, lcd_d1_io, lcd_d2_io, lcd_d3_io, lcd_d4_io, lcd_d5_io, - lcd_d6_io, lcd_d7_io}, - .bus_width = 8, - .max_transfer_bytes = lcd_max_transfer_bytes, - .dma_burst_size = 64, + .dc_gpio_num = lcd_dc_io, + .wr_gpio_num = lcd_wr_io, + .clk_src = LCD_CLK_SRC_DEFAULT, + .data_gpio_nums = {lcd_d0_io, lcd_d1_io, lcd_d2_io, lcd_d3_io, lcd_d4_io, lcd_d5_io, lcd_d6_io, + lcd_d7_io}, + .bus_width = 8, + .max_transfer_bytes = lcd_max_transfer_bytes, + .dma_burst_size = 64, +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + .flags = {}, +#endif }; ESP_ERROR_CHECK(esp_lcd_new_i80_bus(&bus_config, &lcd_bus_)); esp_lcd_panel_io_i80_config_t io_config = { - .cs_gpio_num = lcd_cs_io, - .pclk_hz = lcd_clock_speed_hz, - .trans_queue_depth = 10, - .on_color_trans_done = nullptr, - .user_ctx = nullptr, - .lcd_cmd_bits = 8, - .lcd_param_bits = 8, - .dc_levels = - { - .dc_idle_level = 0, - .dc_cmd_level = 0, - .dc_dummy_level = 0, - .dc_data_level = 1, - }, + .cs_gpio_num = lcd_cs_io, + .pclk_hz = lcd_clock_speed_hz, + .trans_queue_depth = 10, + .on_color_trans_done = nullptr, + .user_ctx = nullptr, + .lcd_cmd_bits = 8, + .lcd_param_bits = 8, + .dc_levels = + { + .dc_idle_level = 0, + .dc_cmd_level = 0, + .dc_dummy_level = 0, + .dc_data_level = 1, + }, +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + .flags = {}, +#endif }; ESP_ERROR_CHECK(esp_lcd_new_panel_io_i80(lcd_bus_, &io_config, &panel_io_)); - backlight_channel_configs_.push_back({.gpio = static_cast(lcd_backlight_io), - .channel = LEDC_CHANNEL_0, - .timer = LEDC_TIMER_0, - .output_invert = !backlight_value}); - backlight_ = std::make_shared(Led::Config{.timer = LEDC_TIMER_0, - .frequency_hz = 5000, - .channels = backlight_channel_configs_, - .duty_resolution = LEDC_TIMER_10_BIT}); - using namespace std::placeholders; - DisplayDriver::initialize(espp::display_drivers::Config{ + display_driver_ = std::make_unique(espp::display_drivers::Config{ .write_command = std::bind(&SmartPanleeSc01Plus::write_command, this, _1, _2, _3), .lcd_send_lines = std::bind(&SmartPanleeSc01Plus::write_lcd_lines, this, _1, _2, _3, _4, _5, _6), @@ -218,13 +223,26 @@ bool SmartPanleeSc01Plus::initialize_lcd() { .swap_xy = swap_xy, .mirror_x = mirror_x, .mirror_y = mirror_y}); + if (!display_driver_ || !display_driver_->initialize()) { + display_driver_.reset(); + return false; + } + + backlight_channel_configs_.push_back({.gpio = static_cast(lcd_backlight_io), + .channel = LEDC_CHANNEL_0, + .timer = LEDC_TIMER_0, + .output_invert = !backlight_value}); + backlight_ = std::make_shared(Led::Config{.timer = LEDC_TIMER_0, + .frequency_hz = 5000, + .channels = backlight_channel_configs_, + .duty_resolution = LEDC_TIMER_10_BIT}); brightness(100.0f); return true; } bool SmartPanleeSc01Plus::initialize_display(size_t pixel_buffer_size) { - if (!panel_io_) { + if (!panel_io_ || !display_driver_) { logger_.error( "LCD not initialized, you must call initialize_lcd() before initialize_display()!"); return false; @@ -235,11 +253,22 @@ bool SmartPanleeSc01Plus::initialize_display(size_t pixel_buffer_size) { } display_ = std::make_shared>( - Display::LvglConfig{.width = lcd_width_, - .height = lcd_height_, - .flush_callback = DisplayDriver::flush, - .rotation_callback = DisplayDriver::rotate, - .rotation = rotation}, + Display::LvglConfig{ + .width = lcd_width_, + .height = lcd_height_, + .flush_callback = + [this](lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { + if (display_driver_) { + display_driver_->flush(disp, area, color_map); + } + }, + .rotation_callback = + [this](const DisplayRotation &new_rotation) { + if (display_driver_) { + display_driver_->set_rotation(new_rotation); + } + }, + .rotation = rotation}, Display::OledConfig{ .set_brightness_callback = [this](float brightness) { this->brightness(brightness * 100); }, diff --git a/components/t-deck/CMakeLists.txt b/components/t-deck/CMakeLists.txt index 1a70a27c3..e0311d718 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_driver_i2s esp_driver_spi base_component display display_drivers fatfs i2c input_drivers interrupt gt911 task t_keyboard + REQUIRES driver esp_driver_i2s esp_driver_spi base_component display display_drivers fatfs i2c input_drivers interrupt gt911 spi task t_keyboard REQUIRED_IDF_TARGETS "esp32s3" ) diff --git a/components/t-deck/README.md b/components/t-deck/README.md index 346f8d4db..09875a694 100644 --- a/components/t-deck/README.md +++ b/components/t-deck/README.md @@ -1,15 +1,26 @@ -# LilyGo T-Deck Board Support Package (BSP) Component +# LilyGo T-Deck Board Support Package (BSP) Component [![Badge](https://components.espressif.com/components/espp/t-deck/badge.svg)](https://components.espressif.com/components/espp/t-deck) The LilyGo T-Deck is a development board for the ESP32-S3 module. It features a -nice touchscreen display and expansion headers. +touchscreen display, keyboard, trackball, audio, and expansion headers. The `espp::TDeck` component provides a singleton hardware abstraction for -initializing the touch, display, audio, and micro-SD card subsystems. +initializing the touch, display, keyboard, trackball, audio, and micro-SD card +subsystems. + +## Supported Features + +- ST7789 LCD driven through the shared `espp::Spi` + `SpiPanelIo` path +- GT911 capacitive touch input with LVGL integration helpers +- T-Keyboard input and trackball pointer input +- I2S speaker output with software volume / mute control +- microSD mounting over SDSPI on the shared SPI host +- Backlight brightness control and exposed peripheral helpers ## Example The [example](./example) shows how to use the `espp::TDeck` hardware abstraction -component initialize the components on the LilyGo T-Deck. - +component to initialize the major subsystems on the LilyGo T-Deck and interact +with the display, touch panel, keyboard, trackball, audio path, and optional +microSD card. diff --git a/components/t-deck/example/README.md b/components/t-deck/example/README.md index f64d97e97..3c20445f6 100644 --- a/components/t-deck/example/README.md +++ b/components/t-deck/example/README.md @@ -1,12 +1,12 @@ # T-Deck Example This example shows how to use the `espp::TDeck` hardware abstraction component -initialize the components on the LilyGo T-Deck. +to initialize the components on the LilyGo T-Deck. -It initializes the touch, display, and t-keyboard subsystems. It reads the -touchpad state and each time you touch the screen it uses LVGL to draw a circle -where you touch. If you press the home button on the display, it will clear the -circles. +It initializes the touch panel, display, keyboard, trackball, sound output, and +optional microSD card support. Touching the screen draws a cyan trail overlay, +the delete key clears the trail, the space key or on-screen refresh button +rotates the display, and the keyboard also exposes simple mute/volume controls. https://github.com/user-attachments/assets/5d7e7086-fc2c-4477-8948-07b5bab3e51f @@ -35,6 +35,11 @@ idf.py -p PORT flash monitor See the Getting Started Guide for full steps to configure and use ESP-IDF to build projects. -## Example Output +## Features Demonstrated -![CleanShot 2024-07-05 at 23 43 27@2x](https://github.com/esp-cpp/espp/assets/213467/03d1dad5-e9fa-461c-9eb2-1e5d314dcfdb) +- Shared SPI LCD transport via `espp::Spi` and `SpiPanelIo` +- Touch drawing with a rotation-aware LVGL overlay +- T-Keyboard input for clearing / rotating and audio control +- Trackball initialization and callback wiring +- Optional uSD card mounting over SDSPI on the same SPI host +- WAV playback through the onboard audio path diff --git a/components/t-deck/example/main/t_deck_example.cpp b/components/t-deck/example/main/t_deck_example.cpp index 6d4cf6a80..0ebe1128e 100644 --- a/components/t-deck/example/main/t_deck_example.cpp +++ b/components/t-deck/example/main/t_deck_example.cpp @@ -1,5 +1,5 @@ +#include #include -#include #include #include "t-deck.hpp" @@ -7,10 +7,22 @@ using namespace std::chrono_literals; static constexpr size_t MAX_CIRCLES = 100; -static std::deque circles; +struct Circle { + int x{0}; + int y{0}; + int radius{0}; + bool visible{false}; +}; +static std::array circles; +static size_t next_circle_index = 0; +static size_t visible_circle_count = 0; static std::vector audio_bytes; +static lv_obj_t *circle_layer = nullptr; static std::recursive_mutex lvgl_mutex; +static bool initialize_circle_layer(int width, int height); +static void draw_circle_layer(lv_event_t *event); +static void invalidate_circle_area(const Circle &circle); static void draw_circle(int x0, int y0, int radius); static void clear_circles(); @@ -38,6 +50,12 @@ extern "C" void app_main(void) { lv_disp_set_rotation(disp, rotation); // update the size of the screen lv_obj_set_size(bg, tdeck.rotated_display_width(), tdeck.rotated_display_height()); + if (circle_layer) { + lv_obj_set_size(circle_layer, tdeck.rotated_display_width(), tdeck.rotated_display_height()); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_move_foreground(circle_layer); + lv_obj_invalidate(circle_layer); + } }; auto keypress_callback = [&](uint8_t key) { @@ -123,11 +141,6 @@ extern "C" void app_main(void) { logger.error("Failed to initialize display!"); return; } - // initialize the touchpad - if (!tdeck.initialize_touch(touch_callback)) { - logger.error("Failed to initialize touchpad!"); - return; - } // initialize the trackball if (!tdeck.initialize_trackball(trackball_callback)) { logger.error("Failed to initialize trackball!"); @@ -138,6 +151,10 @@ extern "C" void app_main(void) { bg = lv_obj_create(lv_screen_active()); lv_obj_set_size(bg, tdeck.lcd_width(), tdeck.lcd_height()); lv_obj_set_style_bg_color(bg, lv_color_make(0, 0, 0), 0); + if (!initialize_circle_layer(tdeck.lcd_width(), tdeck.lcd_height())) { + logger.error("Failed to initialize circle layer!"); + return; + } // add text in the center of the screen lv_obj_t *label = lv_label_create(lv_screen_active()); @@ -162,6 +179,14 @@ extern "C" void app_main(void) { // 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); + lv_obj_move_foreground(circle_layer); + + // initialize the touchpad after the circle layer exists so touch events can + // update it immediately. + if (!tdeck.initialize_touch(touch_callback)) { + logger.error("Failed to initialize touchpad!"); + return; + } // 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 { @@ -205,30 +230,102 @@ extern "C" void app_main(void) { //! [t-deck example] } +static bool initialize_circle_layer(int width, int height) { + if (circle_layer) { + return true; + } + circle_layer = lv_obj_create(lv_screen_active()); + if (!circle_layer) { + return false; + } + lv_obj_remove_style_all(circle_layer); + lv_obj_set_size(circle_layer, width, height); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_CLICKABLE); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(circle_layer, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(circle_layer, 0, 0); + lv_obj_set_style_outline_width(circle_layer, 0, 0); + lv_obj_set_style_shadow_width(circle_layer, 0, 0); + lv_obj_add_event_cb(circle_layer, draw_circle_layer, LV_EVENT_DRAW_MAIN, nullptr); + return true; +} + +static void draw_circle_layer(lv_event_t *event) { + if (visible_circle_count == 0) { + return; + } + auto *obj = static_cast(lv_event_get_current_target(event)); + auto *layer = lv_event_get_layer(event); + lv_area_t obj_coords; + lv_obj_get_coords(obj, &obj_coords); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.base.layer = layer; + rect_dsc.radius = LV_RADIUS_CIRCLE; + rect_dsc.bg_opa = LV_OPA_70; + rect_dsc.bg_color = lv_color_make(0, 255, 255); + rect_dsc.border_width = 0; + rect_dsc.outline_width = 0; + rect_dsc.shadow_width = 0; + + for (const auto &circle : circles) { + if (!circle.visible) { + continue; + } + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_draw_rect(layer, &rect_dsc, &coords); + } +} + +static void invalidate_circle_area(const Circle &circle) { + if (!circle_layer || circle.radius <= 0) { + return; + } + + lv_area_t obj_coords; + lv_obj_get_coords(circle_layer, &obj_coords); + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_obj_invalidate_area(circle_layer, &coords); +} + static void draw_circle(int x0, int y0, int radius) { - // if we have too many circles, remove the oldest one - if (circles.size() >= MAX_CIRCLES) { - lv_obj_delete(circles.front()); - circles.pop_front(); + if (!circle_layer) { + return; + } + lv_obj_move_foreground(circle_layer); + Circle previous_circle = circles[next_circle_index]; + circles[next_circle_index] = {.x = x0, .y = y0, .radius = radius, .visible = true}; + next_circle_index = (next_circle_index + 1) % circles.size(); + if (visible_circle_count < circles.size()) { + visible_circle_count++; } - 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); + if (previous_circle.visible) { + invalidate_circle_area(previous_circle); + } + invalidate_circle_area(circles[(next_circle_index + circles.size() - 1) % circles.size()]); } static void clear_circles() { - // remove the circles from lvgl - for (auto circle : circles) { - lv_obj_delete(circle); + for (auto &circle : circles) { + if (circle.visible) { + invalidate_circle_area(circle); + } + circle.visible = false; } - // clear the vector - circles.clear(); + next_circle_index = 0; + visible_circle_count = 0; } static bool load_audio(size_t &out_size, size_t &out_sample_rate) { diff --git a/components/t-deck/idf_component.yml b/components/t-deck/idf_component.yml index 10440229d..7f6c16eaf 100644 --- a/components/t-deck/idf_component.yml +++ b/components/t-deck/idf_component.yml @@ -24,6 +24,7 @@ dependencies: espp/i2c: '>=1.0' espp/input_drivers: '>=1.0' espp/interrupt: '>=1.0' + espp/spi: '>=1.0' espp/task: '>=1.0' espp/t_keyboard: '>=1.0' targets: diff --git a/components/t-deck/include/t-deck.hpp b/components/t-deck/include/t-deck.hpp index 2687a225f..f5e3374c1 100644 --- a/components/t-deck/include/t-deck.hpp +++ b/components/t-deck/include/t-deck.hpp @@ -26,6 +26,7 @@ #include "interrupt.hpp" #include "led.hpp" #include "pointer_input.hpp" +#include "spi.hpp" #include "st7789.hpp" #include "t_keyboard.hpp" #include "touchpad_input.hpp" @@ -562,9 +563,6 @@ class TDeck : public BaseComponent { .sda_pullup_en = GPIO_PULLUP_ENABLE, .scl_pullup_en = GPIO_PULLUP_ENABLE}}; - // spi bus shared between sdcard and lcd - std::atomic spi_bus_initialized_{false}; - // sdcard sdmmc_card_t *sdcard_{nullptr}; @@ -646,14 +644,12 @@ class TDeck : public BaseComponent { // display std::shared_ptr> display_; + std::unique_ptr display_driver_; std::vector backlight_channel_configs_{}; std::shared_ptr backlight_{}; - /// SPI bus for communication with the LCD - 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 num_queued_trans = 0; + std::unique_ptr lcd_spi_; + std::unique_ptr lcd_; uint8_t *frame_buffer0_{nullptr}; uint8_t *frame_buffer1_{nullptr}; diff --git a/components/t-deck/src/t-deck.cpp b/components/t-deck/src/t-deck.cpp index 682985ce8..b74cbac5f 100644 --- a/components/t-deck/src/t-deck.cpp +++ b/components/t-deck/src/t-deck.cpp @@ -25,28 +25,24 @@ bool TDeck::peripheral_power() const { return gpio_get_level(peripheral_power_pi //////////////////////// bool TDeck::init_spi_bus() { - if (spi_bus_initialized_) { - return true; - } - - spi_bus_config_t bus_cfg; - memset(&bus_cfg, 0, sizeof(bus_cfg)); - bus_cfg.mosi_io_num = spi_mosi_io; - bus_cfg.miso_io_num = spi_miso_io; - bus_cfg.sclk_io_num = spi_sclk_io; - bus_cfg.quadwp_io_num = -1; - bus_cfg.quadhd_io_num = -1; - bus_cfg.max_transfer_sz = SPI_MAX_TRANSFER_BYTES; - auto ret = spi_bus_initialize(spi_num, &bus_cfg, - SDSPI_DEFAULT_DMA); // SPI_DMA_CH_AUTO); // SDSPI_DEFAULT_DMA); - if (ret != ESP_OK) { + if (lcd_spi_) { + return lcd_spi_->initialized(); + } + + lcd_spi_ = std::make_unique(Spi::Config{ + .host = spi_num, + .sclk_io_num = spi_sclk_io, + .mosi_io_num = spi_mosi_io, + .miso_io_num = spi_miso_io, + .max_transfer_sz = SPI_MAX_TRANSFER_BYTES, + .dma_channel = static_cast(SDSPI_DEFAULT_DMA), + .log_level = get_log_level(), + }); + if (!lcd_spi_->initialized()) { logger_.error("Failed to initialize bus."); + lcd_spi_.reset(); return false; } - - logger_.info("SPI bus initialized"); - spi_bus_initialized_ = true; - return true; } @@ -259,25 +255,7 @@ espp::TouchpadData TDeck::touchpad_convert(const espp::TouchpadData &data) const 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). -// -// cppcheck-suppress constParameterCallback -static void IRAM_ATTR lcd_spi_pre_transfer_callback(spi_transaction_t *t) { - static auto lcd_dc_io = TDeck::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. -// -// cppcheck-suppress constParameterCallback -static void IRAM_ATTR lcd_spi_post_transfer_callback(spi_transaction_t *t) { - uint16_t user_flags = (uint32_t)(t->user); +static void IRAM_ATTR lcd_spi_flush_ready(uint32_t user_flags) { bool should_flush = user_flags & FLUSH_BIT; if (should_flush) { lv_display_t *disp = lv_display_get_default(); @@ -286,11 +264,55 @@ static void IRAM_ATTR lcd_spi_post_transfer_callback(spi_transaction_t *t) { } bool TDeck::initialize_lcd() { - if (lcd_handle_ || backlight_) { + if (lcd_ || backlight_) { logger_.warn("LCD already initialized, not initializing again!"); return false; } - // Initialize backlight PWM + if (!init_spi_bus()) { + logger_.error("Failed to initialize SPI bus for LCD."); + return false; + } + + lcd_ = std::make_unique(SpiPanelIo::Config{ + .spi = lcd_spi_.get(), + .device_config = + { + .mode = 0, + .clock_speed_hz = lcd_clock_speed, + .input_delay_ns = 0, + .cs_io_num = lcd_cs_io, + .queue_size = spi_queue_size, + }, + .data_command_io = lcd_dc_io, + .data_command_bit_mask = DC_LEVEL_BIT, + .post_transaction_callback_bit_mask = FLUSH_BIT, + .post_transaction_callback = lcd_spi_flush_ready, + .log_level = get_log_level(), + }); + if (!lcd_->initialized()) { + lcd_.reset(); + return false; + } + + display_driver_ = std::make_unique( + espp::display_drivers::Config{.panel_io = lcd_.get(), + .write_command = nullptr, + .read_command = nullptr, + .lcd_send_lines = nullptr, + .reset_pin = lcd_reset_io, + .data_command_pin = lcd_dc_io, + .reset_value = reset_value, + .invert_colors = invert_colors, + .swap_xy = swap_xy, + .mirror_x = mirror_x, + .mirror_y = mirror_y, + .mirror_portrait = mirror_portrait}); + if (!display_driver_ || !display_driver_->initialize()) { + display_driver_.reset(); + lcd_.reset(); + return false; + } + backlight_channel_configs_.push_back({.gpio = static_cast(backlight_io), .channel = LEDC_CHANNEL_0, .timer = LEDC_TIMER_0, @@ -300,44 +322,11 @@ bool TDeck::initialize_lcd() { .channels = backlight_channel_configs_, .duty_resolution = LEDC_TIMER_10_BIT})); brightness(100.0f); - - if (!init_spi_bus()) { - logger_.error("Failed to initialize SPI bus for LCD."); - return false; - } - - esp_err_t ret; - 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 = lcd_cs_io; - 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; - - // Attach the LCD to the SPI bus - ret = spi_bus_add_device(spi_num, &lcd_config_, &lcd_handle_); - ESP_ERROR_CHECK(ret); - // initialize the controller - using namespace std::placeholders; - DisplayDriver::initialize(espp::display_drivers::Config{ - .write_command = std::bind(&TDeck::write_command, this, _1, _2, _3), - .lcd_send_lines = std::bind(&TDeck::write_lcd_lines, this, _1, _2, _3, _4, _5, _6), - .reset_pin = lcd_reset_io, - .data_command_pin = lcd_dc_io, - .reset_value = reset_value, - .invert_colors = invert_colors, - .swap_xy = swap_xy, - .mirror_x = mirror_x, - .mirror_y = mirror_y, - .mirror_portrait = mirror_portrait}); return true; } bool TDeck::initialize_display(size_t pixel_buffer_size) { - if (!lcd_handle_) { + if (!lcd_) { logger_.error( "LCD not initialized, you must call initialize_lcd() before initialize_display()!"); return false; @@ -349,11 +338,22 @@ bool TDeck::initialize_display(size_t pixel_buffer_size) { // initialize the display / lvgl using namespace std::chrono_literals; display_ = std::make_shared>( - Display::LvglConfig{.width = lcd_width_, - .height = lcd_height_, - .flush_callback = DisplayDriver::flush, - .rotation_callback = DisplayDriver::rotate, - .rotation = rotation}, + Display::LvglConfig{ + .width = lcd_width_, + .height = lcd_height_, + .flush_callback = + [this](lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { + if (display_driver_) { + display_driver_->flush(disp, area, color_map); + } + }, + .rotation_callback = + [this](const DisplayRotation &new_rotation) { + if (display_driver_) { + display_driver_->set_rotation(new_rotation); + } + }, + .rotation = rotation}, Display::OledConfig{ .set_brightness_callback = [this](float brightness) { this->brightness(brightness * 100.0f); }, @@ -383,131 +383,64 @@ bool TDeck::initialize_display(size_t pixel_buffer_size) { std::shared_ptr> TDeck::display() const { return display_; } void IRAM_ATTR TDeck::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. + if (lcd_) { + lcd_->wait(); } } void IRAM_ATTR TDeck::write_command(uint8_t command, std::span parameters, uint32_t user_data) { - lcd_wait_lines(); - memset(&trans[0], 0, sizeof(spi_transaction_t)); - memset(&trans[1], 0, sizeof(spi_transaction_t)); - - trans[0].length = 8; - trans[0].user = reinterpret_cast(user_data); - trans[0].flags = SPI_TRANS_USE_TXDATA; - trans[0].tx_data[0] = command; - - trans[1].length = parameters.size() * 8; - if (parameters.size() <= 4) { - // copy the data pointer to trans[0].tx_data - memcpy(trans[1].tx_data, parameters.data(), parameters.size()); - trans[1].flags = SPI_TRANS_USE_TXDATA; - } else if (!parameters.empty()) { - trans[1].tx_buffer = parameters.data(); - trans[1].flags = 0; - } - trans[1].user = reinterpret_cast( - user_data | (1 << static_cast(display_drivers::Flags::DC_LEVEL_BIT))); - - esp_err_t ret = spi_device_queue_trans(lcd_handle_, &trans[0], 10 / portTICK_PERIOD_MS); - if (ret != ESP_OK) { - logger_.error("Couldn't queue spi command trans for display: {} '{}'", ret, - esp_err_to_name(ret)); - } else { - ++num_queued_trans; - if (!parameters.empty()) { - ret = spi_device_queue_trans(lcd_handle_, &trans[1], 10 / portTICK_PERIOD_MS); - if (ret != ESP_OK) { - logger_.error("Couldn't queue spi data trans for display: {} '{}'", ret, - esp_err_to_name(ret)); - } else { - ++num_queued_trans; - } - } + if (lcd_) { + lcd_->write_command(command, parameters, user_data); } } void IRAM_ATTR TDeck::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; + if (!lcd_) { + return; + } 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. + return; + } + lcd_->wait(); + std::array window = { + static_cast((xs >> 8) & 0xff), + static_cast(xs & 0xff), + static_cast((xe >> 8) & 0xff), + static_cast(xe & 0xff), + }; + lcd_->queue_command(static_cast(DisplayDriver::Command::caset)); + lcd_->queue_data(window); + window = { + static_cast((ys >> 8) & 0xff), + static_cast(ys & 0xff), + static_cast((ye >> 8) & 0xff), + static_cast(ye & 0xff), + }; + lcd_->queue_command(static_cast(DisplayDriver::Command::raset)); + lcd_->queue_data(window); + lcd_->queue_command(static_cast(DisplayDriver::Command::ramwr)); + lcd_->queue_pixels(data, length, user_data); } void TDeck::write_lcd_frame(const uint16_t xs, const uint16_t ys, const uint16_t width, const uint16_t height, uint8_t *data) { + if (!display_driver_) { + return; + } 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); + display_driver_->fill(nullptr, &area, data); } else { // don't have data, so clear the area (set to 0) - DisplayDriver::clear(xs, ys, width, height); + display_driver_->clear(xs, ys, width, height); } } diff --git a/components/t-dongle-s3/CMakeLists.txt b/components/t-dongle-s3/CMakeLists.txt index 8f15ce9e3..db14222ad 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 fatfs base_component display display_drivers i2c interrupt led_strip task + REQUIRES driver fatfs base_component display display_drivers i2c interrupt led_strip spi task REQUIRED_IDF_TARGETS "esp32s3" ) diff --git a/components/t-dongle-s3/example/main/t_dongle_s3_example.cpp b/components/t-dongle-s3/example/main/t_dongle_s3_example.cpp index 473d1da5a..c251db8d5 100644 --- a/components/t-dongle-s3/example/main/t_dongle_s3_example.cpp +++ b/components/t-dongle-s3/example/main/t_dongle_s3_example.cpp @@ -1,14 +1,27 @@ +#include #include #include -#include #include "t-dongle-s3.hpp" using namespace std::chrono_literals; -static std::vector circles; +static constexpr size_t MAX_CIRCLES = 10; +struct Circle { + int x{0}; + int y{0}; + int radius{0}; + bool visible{false}; +}; +static std::array circles; +static size_t next_circle_index = 0; +static size_t visible_circle_count = 0; +static lv_obj_t *circle_layer = nullptr; static std::mutex lvgl_mutex; +static bool initialize_circle_layer(int width, int height); +static void draw_circle_layer(lv_event_t *event); +static void invalidate_circle_area(const Circle &circle); static void draw_circle(int x0, int y0, int radius); static void clear_circles(); @@ -47,6 +60,20 @@ extern "C" void app_main(void) { // initialize the button, which we'll use to cycle the rotation of the display logger.info("Initializing the button"); + lv_obj_t *bg = nullptr; + lv_obj_t *label = nullptr; + static auto update_layout = [&]() { + int width = tdongle.rotated_display_width(); + int height = tdongle.rotated_display_height(); + lv_obj_set_size(bg, width, height); + lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); + if (circle_layer) { + lv_obj_set_size(circle_layer, width, height); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_move_foreground(circle_layer); + lv_obj_invalidate(circle_layer); + } + }; auto on_button_pressed = [&](const auto &event) { if (event.active) { // lock the display mutex @@ -54,8 +81,9 @@ extern "C" void app_main(void) { static auto rotation = LV_DISPLAY_ROTATION_0; rotation = static_cast((static_cast(rotation) + 1) % 4); fmt::print("Setting rotation to {}\n", (int)rotation); - lv_display_t *disp = lv_disp_get_default(); + lv_display_t *disp = lv_display_get_default(); lv_disp_set_rotation(disp, rotation); + update_layout(); } }; tdongle.initialize_button(on_button_pressed); @@ -66,15 +94,22 @@ extern "C" void app_main(void) { tdongle.led(hsv, brightness); // set the background color to black - lv_obj_t *bg = lv_obj_create(lv_screen_active()); - lv_obj_set_size(bg, tdongle.lcd_width(), tdongle.lcd_height()); + bg = lv_obj_create(lv_screen_active()); + lv_obj_set_size(bg, tdongle.rotated_display_width(), tdongle.rotated_display_height()); lv_obj_set_style_bg_color(bg, lv_color_make(0, 0, 0), 0); + if (!initialize_circle_layer(tdongle.rotated_display_width(), tdongle.rotated_display_height())) { + logger.error("Failed to initialize circle layer!"); + return; + } // add text in the center of the screen - lv_obj_t *label = lv_label_create(lv_screen_active()); + label = lv_label_create(lv_screen_active()); lv_label_set_text(label, "Drawing circles\nto the screen."); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0); + update_layout(); + + lv_obj_move_foreground(circle_layer); // 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 { @@ -117,17 +152,16 @@ extern "C" void app_main(void) { while (true) { auto start = esp_timer_get_time(); // if there are 10 circles on the screen, clear them - static constexpr int max_circles = 10; - if (circles.size() >= max_circles) { + if (visible_circle_count >= MAX_CIRCLES) { // lock the lvgl mutex std::lock_guard lock(lvgl_mutex); clear_circles(); } else { // draw a circle of circles on the screen (just draw the next circle) - static constexpr int middle_x = tdongle.lcd_width() / 2; - static constexpr int middle_y = tdongle.lcd_height() / 2; + int middle_x = tdongle.rotated_display_width() / 2; + int middle_y = tdongle.rotated_display_height() / 2; static constexpr int radius = 30; - float angle = circles.size() * 2.0f * M_PI / max_circles; + float angle = visible_circle_count * 2.0f * M_PI / MAX_CIRCLES; int x = middle_x + radius * cos(angle); int y = middle_y + radius * sin(angle); // lock the lvgl mutex @@ -141,20 +175,100 @@ extern "C" void app_main(void) { //! [t-dongle-s3 example] } +static bool initialize_circle_layer(int width, int height) { + if (circle_layer) { + return true; + } + circle_layer = lv_obj_create(lv_screen_active()); + if (!circle_layer) { + return false; + } + lv_obj_remove_style_all(circle_layer); + lv_obj_set_size(circle_layer, width, height); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_CLICKABLE); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(circle_layer, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(circle_layer, 0, 0); + lv_obj_set_style_outline_width(circle_layer, 0, 0); + lv_obj_set_style_shadow_width(circle_layer, 0, 0); + lv_obj_add_event_cb(circle_layer, draw_circle_layer, LV_EVENT_DRAW_MAIN, nullptr); + return true; +} + +static void draw_circle_layer(lv_event_t *event) { + if (visible_circle_count == 0) { + return; + } + auto *obj = static_cast(lv_event_get_current_target(event)); + auto *layer = lv_event_get_layer(event); + lv_area_t obj_coords; + lv_obj_get_coords(obj, &obj_coords); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.base.layer = layer; + rect_dsc.radius = LV_RADIUS_CIRCLE; + rect_dsc.bg_opa = LV_OPA_70; + rect_dsc.bg_color = lv_color_make(0, 255, 255); + rect_dsc.border_width = 0; + rect_dsc.outline_width = 0; + rect_dsc.shadow_width = 0; + + for (const auto &circle : circles) { + if (!circle.visible) { + continue; + } + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_draw_rect(layer, &rect_dsc, &coords); + } +} + +static void invalidate_circle_area(const Circle &circle) { + if (!circle_layer || circle.radius <= 0) { + return; + } + + lv_area_t obj_coords; + lv_obj_get_coords(circle_layer, &obj_coords); + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_obj_invalidate_area(circle_layer, &coords); +} + static void draw_circle(int x0, int y0, int radius) { - 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); - circles.push_back(my_Cir); + if (!circle_layer) { + return; + } + lv_obj_move_foreground(circle_layer); + Circle previous_circle = circles[next_circle_index]; + circles[next_circle_index] = {.x = x0, .y = y0, .radius = radius, .visible = true}; + next_circle_index = (next_circle_index + 1) % circles.size(); + if (visible_circle_count < circles.size()) { + visible_circle_count++; + } + if (previous_circle.visible) { + invalidate_circle_area(previous_circle); + } + invalidate_circle_area(circles[(next_circle_index + circles.size() - 1) % circles.size()]); } static void clear_circles() { - // remove the circles from lvgl - for (auto circle : circles) { - lv_obj_delete(circle); + for (auto &circle : circles) { + if (circle.visible) { + invalidate_circle_area(circle); + } + circle.visible = false; } - // clear the vector - circles.clear(); + next_circle_index = 0; + visible_circle_count = 0; } diff --git a/components/t-dongle-s3/idf_component.yml b/components/t-dongle-s3/idf_component.yml index 124f275c8..19c3fe28f 100644 --- a/components/t-dongle-s3/idf_component.yml +++ b/components/t-dongle-s3/idf_component.yml @@ -23,6 +23,7 @@ dependencies: espp/i2c: '>=1.0' espp/interrupt: '>=1.0' espp/led_strip: '>=1.0' + espp/spi: '>=1.0' espp/task: '>=1.0' targets: - esp32s3 diff --git a/components/t-dongle-s3/include/t-dongle-s3.hpp b/components/t-dongle-s3/include/t-dongle-s3.hpp index a25fe1671..3f0e59fa0 100644 --- a/components/t-dongle-s3/include/t-dongle-s3.hpp +++ b/components/t-dongle-s3/include/t-dongle-s3.hpp @@ -19,6 +19,7 @@ #include "interrupt.hpp" #include "led.hpp" #include "led_strip.hpp" +#include "spi.hpp" #include "st7789.hpp" namespace espp { @@ -129,6 +130,14 @@ class TDongleS3 : public BaseComponent { /// \return The height of the LCD in pixels static constexpr size_t lcd_height() { return lcd_height_; } + /// Get the display width in pixels, according to the current orientation + /// \return The display width in pixels, according to the current orientation + size_t rotated_display_width() const; + + /// Get the display height in pixels, according to the current orientation + /// \return The display height in pixels, according to the current orientation + size_t rotated_display_height() const; + /// 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 lcd_dc_io; } @@ -173,15 +182,6 @@ class TDongleS3 : public BaseComponent { /// \note This is null unless initialize_display() has been called uint8_t *frame_buffer1() const; - /// Write command and optional parameters to the LCD - /// \param command The command to write - /// \param parameters The command parameters to write - /// \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_command(uint8_t command, std::span parameters, uint32_t user_data); - /// Write a frame to the LCD /// \param x The x coordinate /// \param y The y coordinate @@ -193,17 +193,6 @@ class TDongleS3 : public BaseComponent { 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); - ///////////////////////////////////////////////////////////////////////////// // uSD Card ///////////////////////////////////////////////////////////////////////////// @@ -309,15 +298,12 @@ class TDongleS3 : public BaseComponent { // display std::shared_ptr> display_; + std::unique_ptr display_driver_; std::vector backlight_channel_configs_{}; std::shared_ptr backlight_{}; - /// 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 num_queued_trans = 0; + std::unique_ptr lcd_spi_; + std::unique_ptr lcd_; uint8_t *frame_buffer0_{nullptr}; uint8_t *frame_buffer1_{nullptr}; }; // class TDongleS3 diff --git a/components/t-dongle-s3/src/t-dongle-s3.cpp b/components/t-dongle-s3/src/t-dongle-s3.cpp index 0b01f2b4b..0920eeac8 100644 --- a/components/t-dongle-s3/src/t-dongle-s3.cpp +++ b/components/t-dongle-s3/src/t-dongle-s3.cpp @@ -122,34 +122,13 @@ bool TDongleS3::led(const Rgb &rgb, float brightness) { return led(rgb.hsv(), br 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). -// -// cppcheck-suppress constParameterCallback -static void IRAM_ATTR lcd_spi_pre_transfer_callback(spi_transaction_t *t) { - static auto lcd_dc_io = TDongleS3::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. -// -// cppcheck-suppress constParameterCallback -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_disp_get_default(); - lv_display_flush_ready(disp); - } +static void IRAM_ATTR lcd_spi_flush_ready(uint32_t) { + lv_display_t *disp = lv_display_get_default(); + lv_display_flush_ready(disp); } bool TDongleS3::initialize_lcd() { - if (lcd_handle_ || backlight_) { + if (lcd_ || backlight_) { logger_.warn("LCD already initialized, not initializing again!"); return false; } @@ -164,52 +143,61 @@ bool TDongleS3::initialize_lcd() { .duty_resolution = LEDC_TIMER_10_BIT})); brightness(100.0f); - esp_err_t ret; - - memset(&lcd_spi_bus_config_, 0, sizeof(lcd_spi_bus_config_)); - lcd_spi_bus_config_.mosi_io_num = lcd_mosi_io; - lcd_spi_bus_config_.miso_io_num = -1; - lcd_spi_bus_config_.sclk_io_num = lcd_sclk_io; - lcd_spi_bus_config_.quadwp_io_num = -1; - lcd_spi_bus_config_.quadhd_io_num = -1; - lcd_spi_bus_config_.max_transfer_sz = SPI_MAX_TRANSFER_BYTES; - - 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 = lcd_cs_io; - 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{ - .write_command = std::bind(&TDongleS3::write_command, this, _1, _2, _3), - .lcd_send_lines = std::bind(&TDongleS3::write_lcd_lines, this, _1, _2, _3, _4, _5, _6), - .reset_pin = lcd_reset_io, - .data_command_pin = lcd_dc_io, - .reset_value = reset_value, - .invert_colors = invert_colors, - .swap_color_order = swap_color_order, - .offset_x = lcd_offset_x, - .offset_y = lcd_offset_y, - .swap_xy = swap_xy, - .mirror_x = mirror_x, - .mirror_y = mirror_y}); + lcd_spi_ = std::make_unique(Spi::Config{ + .host = lcd_spi_num, + .sclk_io_num = lcd_sclk_io, + .mosi_io_num = lcd_mosi_io, + .miso_io_num = GPIO_NUM_NC, + .max_transfer_sz = SPI_MAX_TRANSFER_BYTES, + .log_level = get_log_level(), + }); + lcd_ = std::make_unique(SpiPanelIo::Config{ + .spi = lcd_spi_.get(), + .device_config = + { + .mode = 0, + .clock_speed_hz = lcd_clock_speed, + .input_delay_ns = 0, + .cs_io_num = lcd_cs_io, + .queue_size = spi_queue_size, + }, + .data_command_io = lcd_dc_io, + .data_command_bit_mask = DC_LEVEL_BIT, + .post_transaction_callback_bit_mask = FLUSH_BIT, + .post_transaction_callback = lcd_spi_flush_ready, + .log_level = get_log_level(), + }); + if (!lcd_->initialized()) { + lcd_.reset(); + lcd_spi_.reset(); + return false; + } + display_driver_ = std::make_unique( + espp::display_drivers::Config{.panel_io = lcd_.get(), + .write_command = nullptr, + .read_command = nullptr, + .lcd_send_lines = nullptr, + .reset_pin = lcd_reset_io, + .data_command_pin = lcd_dc_io, + .reset_value = reset_value, + .invert_colors = invert_colors, + .swap_color_order = swap_color_order, + .offset_x = lcd_offset_x, + .offset_y = lcd_offset_y, + .swap_xy = swap_xy, + .mirror_x = mirror_x, + .mirror_y = mirror_y}); + if (!display_driver_ || !display_driver_->initialize()) { + display_driver_.reset(); + lcd_.reset(); + lcd_spi_.reset(); + return false; + } return true; } bool TDongleS3::initialize_display(size_t pixel_buffer_size) { - if (!lcd_handle_) { + if (!lcd_) { logger_.error( "LCD not initialized, you must call initialize_lcd() before initialize_display()!"); return false; @@ -222,11 +210,22 @@ bool TDongleS3::initialize_display(size_t pixel_buffer_size) { using namespace std::chrono_literals; display_ = std::make_shared>( // NOTE: for some reason, we have to swap the width and height here - Display::LvglConfig{.width = lcd_height_, - .height = lcd_width_, - .flush_callback = DisplayDriver::flush, - .rotation_callback = DisplayDriver::rotate, - .rotation = rotation}, + Display::LvglConfig{ + .width = lcd_height_, + .height = lcd_width_, + .flush_callback = + [this](lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { + if (display_driver_) { + display_driver_->flush(disp, area, color_map); + } + }, + .rotation_callback = + [this](const DisplayRotation &new_rotation) { + if (display_driver_) { + display_driver_->set_rotation(new_rotation); + } + }, + .rotation = rotation}, Display::OledConfig{ .set_brightness_callback = [this](float brightness) { this->brightness(brightness * 100.0f); }, @@ -247,131 +246,26 @@ bool TDongleS3::initialize_display(size_t pixel_buffer_size) { std::shared_ptr> TDongleS3::display() const { return display_; } void IRAM_ATTR TDongleS3::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. + if (lcd_) { + lcd_->wait(); } } -void TDongleS3::write_command(uint8_t command, std::span parameters, - uint32_t user_data) { - lcd_wait_lines(); - memset(&trans[0], 0, sizeof(spi_transaction_t)); - memset(&trans[1], 0, sizeof(spi_transaction_t)); - - trans[0].length = 8; - trans[0].user = reinterpret_cast(user_data); - trans[0].flags = SPI_TRANS_USE_TXDATA; - trans[0].tx_data[0] = command; - - trans[1].length = parameters.size() * 8; - if (parameters.size() <= 4) { - // copy the data pointer to trans[1].tx_data - memcpy(trans[1].tx_data, parameters.data(), parameters.size()); - trans[1].flags = SPI_TRANS_USE_TXDATA; - } else if (!parameters.empty()) { - trans[1].tx_buffer = parameters.data(); - trans[1].flags = 0; - } - trans[1].user = reinterpret_cast( - user_data | (1 << static_cast(display_drivers::Flags::DC_LEVEL_BIT))); - - esp_err_t ret = spi_device_queue_trans(lcd_handle_, &trans[0], 10 / portTICK_PERIOD_MS); - if (ret != ESP_OK) { - logger_.error("Couldn't queue spi command trans for display: {} '{}'", ret, - esp_err_to_name(ret)); - } else { - ++num_queued_trans; - if (!parameters.empty()) { - ret = spi_device_queue_trans(lcd_handle_, &trans[1], 10 / portTICK_PERIOD_MS); - if (ret != ESP_OK) { - logger_.error("Couldn't queue spi data trans for display: {} '{}'", ret, - esp_err_to_name(ret)); - } else { - ++num_queued_trans; - } - } - } -} - -void IRAM_ATTR TDongleS3::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 TDongleS3::write_lcd_frame(const uint16_t xs, const uint16_t ys, const uint16_t width, const uint16_t height, uint8_t *data) { + if (!display_driver_) { + return; + } 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); + display_driver_->fill(nullptr, &area, data); } else { // don't have data, so clear the area (set to 0) - DisplayDriver::clear(xs, ys, width, height); + display_driver_->clear(xs, ys, width, height); } } @@ -407,3 +301,31 @@ float TDongleS3::brightness() const { } return 0.0f; } + +size_t TDongleS3::rotated_display_width() const { + auto *display = lv_display_get_default(); + auto rotation = display ? lv_display_get_rotation(display) : LV_DISPLAY_ROTATION_0; + switch (rotation) { + case LV_DISPLAY_ROTATION_90: + case LV_DISPLAY_ROTATION_270: + return lcd_height_; + case LV_DISPLAY_ROTATION_0: + case LV_DISPLAY_ROTATION_180: + default: + return lcd_width_; + } +} + +size_t TDongleS3::rotated_display_height() const { + auto *display = lv_display_get_default(); + auto rotation = display ? lv_display_get_rotation(display) : LV_DISPLAY_ROTATION_0; + switch (rotation) { + case LV_DISPLAY_ROTATION_90: + case LV_DISPLAY_ROTATION_270: + return lcd_width_; + case LV_DISPLAY_ROTATION_0: + case LV_DISPLAY_ROTATION_180: + default: + return lcd_height_; + } +} diff --git a/components/wrover-kit/CMakeLists.txt b/components/wrover-kit/CMakeLists.txt index f1015d027..b28eeeeda 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 base_component display display_drivers task + REQUIRES driver base_component display display_drivers spi task REQUIRED_IDF_TARGETS "esp32" ) diff --git a/components/wrover-kit/example/main/wrover_kit_example.cpp b/components/wrover-kit/example/main/wrover_kit_example.cpp index d9be099f1..58e01bd15 100644 --- a/components/wrover-kit/example/main/wrover_kit_example.cpp +++ b/components/wrover-kit/example/main/wrover_kit_example.cpp @@ -1,15 +1,28 @@ +#include #include #include -#include #include "button.hpp" #include "wrover-kit.hpp" using namespace std::chrono_literals; -static std::vector circles; +static constexpr size_t MAX_CIRCLES = 10; +struct Circle { + int x{0}; + int y{0}; + int radius{0}; + bool visible{false}; +}; +static std::array circles; +static size_t next_circle_index = 0; +static size_t visible_circle_count = 0; +static lv_obj_t *circle_layer = nullptr; static std::mutex lvgl_mutex; +static bool initialize_circle_layer(int width, int height); +static void draw_circle_layer(lv_event_t *event); +static void invalidate_circle_area(const Circle &circle); static void draw_circle(int x0, int y0, int radius); static void clear_circles(); @@ -35,6 +48,20 @@ extern "C" void app_main(void) { } logger.info("Adding LVGL objects to the screen."); + lv_obj_t *bg = nullptr; + lv_obj_t *label = nullptr; + static auto update_layout = [&]() { + int width = wrover.rotated_display_width(); + int height = wrover.rotated_display_height(); + lv_obj_set_size(bg, width, height); + lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); + if (circle_layer) { + lv_obj_set_size(circle_layer, width, height); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_move_foreground(circle_layer); + lv_obj_invalidate(circle_layer); + } + }; // initialize the button, which we'll use to cycle the rotation of the display espp::Button button(espp::Button::Config{ @@ -52,6 +79,7 @@ extern "C" void app_main(void) { fmt::print("Setting rotation to {}\n", (int)rotation); lv_display_t *disp = lv_display_get_default(); lv_disp_set_rotation(disp, rotation); + update_layout(); } }, .active_level = espp::Interrupt::ActiveLevel::LOW, @@ -61,15 +89,22 @@ extern "C" void app_main(void) { }); // set the background color to black - lv_obj_t *bg = lv_obj_create(lv_screen_active()); - lv_obj_set_size(bg, wrover.lcd_width(), wrover.lcd_height()); + bg = lv_obj_create(lv_screen_active()); + lv_obj_set_size(bg, wrover.rotated_display_width(), wrover.rotated_display_height()); lv_obj_set_style_bg_color(bg, lv_color_make(0, 0, 0), 0); + if (!initialize_circle_layer(wrover.rotated_display_width(), wrover.rotated_display_height())) { + logger.error("Failed to initialize circle layer!"); + return; + } // add text in the center of the screen - lv_obj_t *label = lv_label_create(lv_screen_active()); + label = lv_label_create(lv_screen_active()); lv_label_set_text(label, "Drawing circles to the screen."); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0); + update_layout(); + + lv_obj_move_foreground(circle_layer); // 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 { @@ -92,16 +127,15 @@ extern "C" void app_main(void) { while (true) { auto start = esp_timer_get_time(); // if there are 10 circles on the screen, clear them - static constexpr int max_circles = 10; - if (circles.size() >= max_circles) { + if (visible_circle_count >= MAX_CIRCLES) { std::lock_guard lock(lvgl_mutex); clear_circles(); } else { // draw a circle of circles on the screen (just draw the next circle) - static constexpr int middle_x = wrover.lcd_width() / 2; - static constexpr int middle_y = wrover.lcd_height() / 2; + int middle_x = wrover.rotated_display_width() / 2; + int middle_y = wrover.rotated_display_height() / 2; static constexpr int radius = 50; - float angle = circles.size() * 2.0f * M_PI / max_circles; + float angle = visible_circle_count * 2.0f * M_PI / MAX_CIRCLES; int x = middle_x + radius * cos(angle); int y = middle_y + radius * sin(angle); std::lock_guard lock(lvgl_mutex); @@ -114,20 +148,100 @@ extern "C" void app_main(void) { //! [wrover-kit example] } +static bool initialize_circle_layer(int width, int height) { + if (circle_layer) { + return true; + } + circle_layer = lv_obj_create(lv_screen_active()); + if (!circle_layer) { + return false; + } + lv_obj_remove_style_all(circle_layer); + lv_obj_set_size(circle_layer, width, height); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_CLICKABLE); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(circle_layer, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(circle_layer, 0, 0); + lv_obj_set_style_outline_width(circle_layer, 0, 0); + lv_obj_set_style_shadow_width(circle_layer, 0, 0); + lv_obj_add_event_cb(circle_layer, draw_circle_layer, LV_EVENT_DRAW_MAIN, nullptr); + return true; +} + +static void draw_circle_layer(lv_event_t *event) { + if (visible_circle_count == 0) { + return; + } + auto *obj = static_cast(lv_event_get_current_target(event)); + auto *layer = lv_event_get_layer(event); + lv_area_t obj_coords; + lv_obj_get_coords(obj, &obj_coords); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.base.layer = layer; + rect_dsc.radius = LV_RADIUS_CIRCLE; + rect_dsc.bg_opa = LV_OPA_70; + rect_dsc.bg_color = lv_color_make(0, 255, 255); + rect_dsc.border_width = 0; + rect_dsc.outline_width = 0; + rect_dsc.shadow_width = 0; + + for (const auto &circle : circles) { + if (!circle.visible) { + continue; + } + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_draw_rect(layer, &rect_dsc, &coords); + } +} + +static void invalidate_circle_area(const Circle &circle) { + if (!circle_layer || circle.radius <= 0) { + return; + } + + lv_area_t obj_coords; + lv_obj_get_coords(circle_layer, &obj_coords); + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_obj_invalidate_area(circle_layer, &coords); +} + static void draw_circle(int x0, int y0, int radius) { - 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); - circles.push_back(my_Cir); + if (!circle_layer) { + return; + } + lv_obj_move_foreground(circle_layer); + Circle previous_circle = circles[next_circle_index]; + circles[next_circle_index] = {.x = x0, .y = y0, .radius = radius, .visible = true}; + next_circle_index = (next_circle_index + 1) % circles.size(); + if (visible_circle_count < circles.size()) { + visible_circle_count++; + } + if (previous_circle.visible) { + invalidate_circle_area(previous_circle); + } + invalidate_circle_area(circles[(next_circle_index + circles.size() - 1) % circles.size()]); } static void clear_circles() { - // remove the circles from lvgl - for (auto circle : circles) { - lv_obj_delete(circle); + for (auto &circle : circles) { + if (circle.visible) { + invalidate_circle_area(circle); + } + circle.visible = false; } - // clear the vector - circles.clear(); + next_circle_index = 0; + visible_circle_count = 0; } diff --git a/components/wrover-kit/idf_component.yml b/components/wrover-kit/idf_component.yml index 7d93222b5..c7ab04f9d 100644 --- a/components/wrover-kit/idf_component.yml +++ b/components/wrover-kit/idf_component.yml @@ -19,6 +19,7 @@ dependencies: espp/base_component: '>=1.0' espp/display: '>=1.0' espp/display_drivers: '>=1.0' + espp/spi: '>=1.0' espp/task: '>=1.0' targets: - esp32 diff --git a/components/wrover-kit/include/wrover-kit.hpp b/components/wrover-kit/include/wrover-kit.hpp index e2d4cd48b..983da77e5 100644 --- a/components/wrover-kit/include/wrover-kit.hpp +++ b/components/wrover-kit/include/wrover-kit.hpp @@ -12,6 +12,7 @@ #include "base_component.hpp" #include "ili9341.hpp" #include "led.hpp" +#include "spi.hpp" namespace espp { /// The WroverKit class provides an interface to the ESP32-WROVER-KIT ESP32 @@ -80,6 +81,14 @@ class WroverKit : public BaseComponent { /// \return The height of the LCD in pixels static constexpr size_t lcd_height() { return lcd_height_; } + /// Get the display width in pixels, according to the current orientation + /// \return The display width in pixels, according to the current orientation + size_t rotated_display_width() const; + + /// Get the display height in pixels, according to the current orientation + /// \return The display height in pixels, according to the current orientation + size_t rotated_display_height() const; + /// 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 lcd_dc_io; } @@ -124,15 +133,6 @@ class WroverKit : public BaseComponent { /// \note This is null unless initialize_display() has been called uint8_t *frame_buffer1() const; - /// Write command and optional parameters to the LCD - /// \param command The command to write - /// \param parameters The command parameters to write - /// \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_command(uint8_t command, std::span parameters, uint32_t user_data); - /// Write a frame to the LCD /// \param x The x coordinate /// \param y The y coordinate @@ -144,17 +144,6 @@ class WroverKit : public BaseComponent { 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: WroverKit(); void lcd_wait_lines(); @@ -186,13 +175,9 @@ class WroverKit : public BaseComponent { std::shared_ptr> display_; std::vector backlight_channel_configs_{}; std::shared_ptr backlight_{}; - /// 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 num_queued_trans = 0; + std::unique_ptr display_driver_; + std::unique_ptr lcd_spi_; + std::unique_ptr lcd_; uint8_t *frame_buffer0_{nullptr}; uint8_t *frame_buffer1_{nullptr}; }; // class WroverKit diff --git a/components/wrover-kit/src/wrover-kit.cpp b/components/wrover-kit/src/wrover-kit.cpp index 63b4fa1a4..4e76172b7 100644 --- a/components/wrover-kit/src/wrover-kit.cpp +++ b/components/wrover-kit/src/wrover-kit.cpp @@ -15,34 +15,13 @@ WroverKit::WroverKit() 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). -// -// cppcheck-suppress constParameterCallback -static void IRAM_ATTR lcd_spi_pre_transfer_callback(spi_transaction_t *t) { - static auto lcd_dc_io = WroverKit::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. -// -// cppcheck-suppress constParameterCallback -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_display_get_default(); - lv_display_flush_ready(disp); - } +static void lcd_spi_flush_ready(uint32_t) { + lv_display_t *disp = lv_display_get_default(); + lv_display_flush_ready(disp); } bool WroverKit::initialize_lcd() { - if (lcd_handle_ || backlight_) { + if (lcd_ || backlight_) { logger_.warn("LCD already initialized, not initializing again!"); return false; } @@ -59,49 +38,58 @@ bool WroverKit::initialize_lcd() { .duty_resolution = LEDC_TIMER_10_BIT})); brightness(100.0f); - esp_err_t ret; - - memset(&lcd_spi_bus_config_, 0, sizeof(lcd_spi_bus_config_)); - lcd_spi_bus_config_.mosi_io_num = lcd_mosi_io; - lcd_spi_bus_config_.miso_io_num = -1; - lcd_spi_bus_config_.sclk_io_num = lcd_sclk_io; - lcd_spi_bus_config_.quadwp_io_num = -1; - lcd_spi_bus_config_.quadhd_io_num = -1; - lcd_spi_bus_config_.max_transfer_sz = SPI_MAX_TRANSFER_BYTES; - - 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 = lcd_cs_io; - 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{ - .write_command = std::bind(&WroverKit::write_command, this, _1, _2, _3), - .lcd_send_lines = std::bind(&WroverKit::write_lcd_lines, this, _1, _2, _3, _4, _5, _6), - .reset_pin = lcd_reset_io, - .data_command_pin = lcd_dc_io, - .reset_value = reset_value, - .invert_colors = invert_colors, - .swap_xy = swap_xy, - .mirror_x = mirror_x, - .mirror_y = mirror_y}); + lcd_spi_ = std::make_unique(Spi::Config{ + .host = lcd_spi_num, + .sclk_io_num = lcd_sclk_io, + .mosi_io_num = lcd_mosi_io, + .miso_io_num = GPIO_NUM_NC, + .max_transfer_sz = SPI_MAX_TRANSFER_BYTES, + .log_level = get_log_level(), + }); + lcd_ = std::make_unique(SpiPanelIo::Config{ + .spi = lcd_spi_.get(), + .device_config = + { + .mode = 0, + .clock_speed_hz = lcd_clock_speed, + .input_delay_ns = 0, + .cs_io_num = lcd_cs_io, + .queue_size = 6, + }, + .data_command_io = lcd_dc_io, + .data_command_bit_mask = DC_LEVEL_BIT, + .post_transaction_callback_bit_mask = FLUSH_BIT, + .post_transaction_callback = lcd_spi_flush_ready, + .log_level = get_log_level(), + }); + if (!lcd_->initialized()) { + lcd_.reset(); + lcd_spi_.reset(); + return false; + } + display_driver_ = + std::make_unique(espp::display_drivers::Config{.panel_io = lcd_.get(), + .write_command = nullptr, + .read_command = nullptr, + .lcd_send_lines = nullptr, + .reset_pin = lcd_reset_io, + .data_command_pin = lcd_dc_io, + .reset_value = reset_value, + .invert_colors = invert_colors, + .swap_xy = swap_xy, + .mirror_x = mirror_x, + .mirror_y = mirror_y}); + if (!display_driver_ || !display_driver_->initialize()) { + display_driver_.reset(); + lcd_.reset(); + lcd_spi_.reset(); + return false; + } return true; } bool WroverKit::initialize_display(size_t pixel_buffer_size) { - if (!lcd_handle_) { + if (!lcd_) { logger_.error( "LCD not initialized, you must call initialize_lcd() before initialize_display()!"); return false; @@ -114,11 +102,22 @@ bool WroverKit::initialize_display(size_t pixel_buffer_size) { // initialize the display / lvgl using namespace std::chrono_literals; display_ = std::make_shared>( - Display::LvglConfig{.width = lcd_width_, - .height = lcd_height_, - .flush_callback = DisplayDriver::flush, - .rotation_callback = DisplayDriver::rotate, - .rotation = rotation}, + Display::LvglConfig{ + .width = lcd_width_, + .height = lcd_height_, + .flush_callback = + [this](lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { + if (display_driver_) { + display_driver_->flush(disp, area, color_map); + } + }, + .rotation_callback = + [this](const DisplayRotation &new_rotation) { + if (display_driver_) { + display_driver_->set_rotation(new_rotation); + } + }, + .rotation = rotation}, Display::OledConfig{ .set_brightness_callback = [this](float brightness) { this->brightness(brightness * 100.0f); }, @@ -138,132 +137,27 @@ bool WroverKit::initialize_display(size_t pixel_buffer_size) { std::shared_ptr> WroverKit::display() const { return display_; } -void IRAM_ATTR WroverKit::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 WroverKit::write_command(uint8_t command, std::span parameters, - uint32_t user_data) { - lcd_wait_lines(); - memset(&trans[0], 0, sizeof(spi_transaction_t)); - memset(&trans[1], 0, sizeof(spi_transaction_t)); - - trans[0].length = 8; - trans[0].user = reinterpret_cast(user_data); - trans[0].flags = SPI_TRANS_USE_TXDATA; - trans[0].tx_data[0] = command; - - trans[1].length = parameters.size() * 8; - if (parameters.size() <= 4) { - // copy the data pointer to trans[1].tx_data - memcpy(trans[1].tx_data, parameters.data(), parameters.size()); - trans[1].flags = SPI_TRANS_USE_TXDATA; - } else if (!parameters.empty()) { - trans[1].tx_buffer = parameters.data(); - trans[1].flags = 0; +void WroverKit::lcd_wait_lines() { + if (lcd_) { + lcd_->wait(); } - trans[1].user = reinterpret_cast( - user_data | (1 << static_cast(display_drivers::Flags::DC_LEVEL_BIT))); - - esp_err_t ret = spi_device_queue_trans(lcd_handle_, &trans[0], 10 / portTICK_PERIOD_MS); - if (ret != ESP_OK) { - logger_.error("Couldn't queue spi command trans for display: {} '{}'", ret, - esp_err_to_name(ret)); - } else { - ++num_queued_trans; - if (!parameters.empty()) { - ret = spi_device_queue_trans(lcd_handle_, &trans[1], 10 / portTICK_PERIOD_MS); - if (ret != ESP_OK) { - logger_.error("Couldn't queue spi data trans for display: {} '{}'", ret, - esp_err_to_name(ret)); - } else { - ++num_queued_trans; - } - } - } -} - -void IRAM_ATTR WroverKit::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 WroverKit::write_lcd_frame(const uint16_t xs, const uint16_t ys, const uint16_t width, const uint16_t height, uint8_t *data) { + if (!display_driver_) { + return; + } 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); + display_driver_->fill(nullptr, &area, data); } else { // don't have data, so clear the area (set to 0) - DisplayDriver::clear(xs, ys, width, height); + display_driver_->clear(xs, ys, width, height); } } @@ -299,3 +193,31 @@ float WroverKit::brightness() const { } return 0.0f; } + +size_t WroverKit::rotated_display_width() const { + auto *display = lv_display_get_default(); + auto rotation = display ? lv_display_get_rotation(display) : LV_DISPLAY_ROTATION_0; + switch (rotation) { + case LV_DISPLAY_ROTATION_90: + case LV_DISPLAY_ROTATION_270: + return lcd_height_; + case LV_DISPLAY_ROTATION_0: + case LV_DISPLAY_ROTATION_180: + default: + return lcd_width_; + } +} + +size_t WroverKit::rotated_display_height() const { + auto *display = lv_display_get_default(); + auto rotation = display ? lv_display_get_rotation(display) : LV_DISPLAY_ROTATION_0; + switch (rotation) { + case LV_DISPLAY_ROTATION_90: + case LV_DISPLAY_ROTATION_270: + return lcd_width_; + case LV_DISPLAY_ROTATION_0: + case LV_DISPLAY_ROTATION_180: + default: + return lcd_height_; + } +} diff --git a/components/ws-s3-geek/CMakeLists.txt b/components/ws-s3-geek/CMakeLists.txt index 7bdf9251b..395b2df18 100644 --- a/components/ws-s3-geek/CMakeLists.txt +++ b/components/ws-s3-geek/CMakeLists.txt @@ -2,6 +2,6 @@ idf_component_register( INCLUDE_DIRS "include" SRC_DIRS "src" - REQUIRES driver fatfs base_component display display_drivers i2c interrupt task + REQUIRES driver fatfs base_component display display_drivers i2c interrupt spi task REQUIRED_IDF_TARGETS "esp32s3" ) diff --git a/components/ws-s3-geek/example/main/ws_s3_geek_example.cpp b/components/ws-s3-geek/example/main/ws_s3_geek_example.cpp index 09cce5924..dd636a869 100644 --- a/components/ws-s3-geek/example/main/ws_s3_geek_example.cpp +++ b/components/ws-s3-geek/example/main/ws_s3_geek_example.cpp @@ -1,14 +1,27 @@ +#include #include #include -#include #include "ws-s3-geek.hpp" using namespace std::chrono_literals; -static std::vector circles; +static constexpr size_t MAX_CIRCLES = 10; +struct Circle { + int x{0}; + int y{0}; + int radius{0}; + bool visible{false}; +}; +static std::array circles; +static size_t next_circle_index = 0; +static size_t visible_circle_count = 0; +static lv_obj_t *circle_layer = nullptr; static std::mutex lvgl_mutex; +static bool initialize_circle_layer(int width, int height); +static void draw_circle_layer(lv_event_t *event); +static void invalidate_circle_area(const Circle &circle); static void draw_circle(int x0, int y0, int radius); static void clear_circles(); @@ -42,6 +55,20 @@ extern "C" void app_main(void) { // initialize the button, which we'll use to cycle the rotation of the display logger.info("Initializing the button"); + lv_obj_t *bg = nullptr; + lv_obj_t *label = nullptr; + static auto update_layout = [&]() { + int width = board.rotated_display_width(); + int height = board.rotated_display_height(); + lv_obj_set_size(bg, width, height); + lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); + if (circle_layer) { + lv_obj_set_size(circle_layer, width, height); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_move_foreground(circle_layer); + lv_obj_invalidate(circle_layer); + } + }; auto on_button_pressed = [&](const auto &event) { if (event.active) { // lock the display mutex @@ -49,22 +76,30 @@ extern "C" void app_main(void) { static auto rotation = LV_DISPLAY_ROTATION_0; rotation = static_cast((static_cast(rotation) + 1) % 4); fmt::print("Setting rotation to {}\n", (int)rotation); - lv_display_t *disp = lv_disp_get_default(); + lv_display_t *disp = lv_display_get_default(); lv_disp_set_rotation(disp, rotation); + update_layout(); } }; board.initialize_button(on_button_pressed); // set the background color to black - lv_obj_t *bg = lv_obj_create(lv_screen_active()); - lv_obj_set_size(bg, board.lcd_width(), board.lcd_height()); + bg = lv_obj_create(lv_screen_active()); + lv_obj_set_size(bg, board.rotated_display_width(), board.rotated_display_height()); lv_obj_set_style_bg_color(bg, lv_color_make(0, 0, 0), 0); + if (!initialize_circle_layer(board.rotated_display_width(), board.rotated_display_height())) { + logger.error("Failed to initialize circle layer!"); + return; + } // add text in the center of the screen - lv_obj_t *label = lv_label_create(lv_screen_active()); + label = lv_label_create(lv_screen_active()); lv_label_set_text(label, "Drawing circles\nto the screen."); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0); + update_layout(); + + lv_obj_move_foreground(circle_layer); // 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 { @@ -89,33 +124,19 @@ extern "C" void app_main(void) { while (true) { auto start = esp_timer_get_time(); // if there are 10 circles on the screen, clear them - static constexpr int max_circles = 10; - if (circles.size() >= max_circles) { + if (visible_circle_count >= MAX_CIRCLES) { // lock the lvgl mutex std::lock_guard lock(lvgl_mutex); clear_circles(); } else { // draw a circle of circles on the screen (just draw the next circle) - static constexpr int middle_x = espp::WsS3Geek::lcd_width() / 2; - static constexpr int middle_y = espp::WsS3Geek::lcd_height() / 2; + int middle_x = board.rotated_display_width() / 2; + int middle_y = board.rotated_display_height() / 2; static constexpr int radius = 30; - float angle = circles.size() * 2.0f * M_PI / max_circles; + float angle = visible_circle_count * 2.0f * M_PI / MAX_CIRCLES; int x = middle_x + radius * cos(angle); int y = middle_y + radius * sin(angle); - // handle the rotation of the display to ensure the circles are centered - auto rotation = lv_display_get_rotation(lv_disp_get_default()); - switch (rotation) { - default: - case LV_DISPLAY_ROTATION_0: - case LV_DISPLAY_ROTATION_180: - break; - case LV_DISPLAY_ROTATION_90: - case LV_DISPLAY_ROTATION_270: - std::swap(x, y); - break; - } - // lock the lvgl mutex std::lock_guard lock(lvgl_mutex); draw_circle(x, y, 5); @@ -127,20 +148,100 @@ extern "C" void app_main(void) { //! [ws-s3-geek example] } +static bool initialize_circle_layer(int width, int height) { + if (circle_layer) { + return true; + } + circle_layer = lv_obj_create(lv_screen_active()); + if (!circle_layer) { + return false; + } + lv_obj_remove_style_all(circle_layer); + lv_obj_set_size(circle_layer, width, height); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_CLICKABLE); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(circle_layer, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(circle_layer, 0, 0); + lv_obj_set_style_outline_width(circle_layer, 0, 0); + lv_obj_set_style_shadow_width(circle_layer, 0, 0); + lv_obj_add_event_cb(circle_layer, draw_circle_layer, LV_EVENT_DRAW_MAIN, nullptr); + return true; +} + +static void draw_circle_layer(lv_event_t *event) { + if (visible_circle_count == 0) { + return; + } + auto *obj = static_cast(lv_event_get_current_target(event)); + auto *layer = lv_event_get_layer(event); + lv_area_t obj_coords; + lv_obj_get_coords(obj, &obj_coords); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.base.layer = layer; + rect_dsc.radius = LV_RADIUS_CIRCLE; + rect_dsc.bg_opa = LV_OPA_70; + rect_dsc.bg_color = lv_color_make(0, 255, 255); + rect_dsc.border_width = 0; + rect_dsc.outline_width = 0; + rect_dsc.shadow_width = 0; + + for (const auto &circle : circles) { + if (!circle.visible) { + continue; + } + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_draw_rect(layer, &rect_dsc, &coords); + } +} + +static void invalidate_circle_area(const Circle &circle) { + if (!circle_layer || circle.radius <= 0) { + return; + } + + lv_area_t obj_coords; + lv_obj_get_coords(circle_layer, &obj_coords); + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_obj_invalidate_area(circle_layer, &coords); +} + static void draw_circle(int x0, int y0, int radius) { - 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); - circles.push_back(my_Cir); + if (!circle_layer) { + return; + } + lv_obj_move_foreground(circle_layer); + Circle previous_circle = circles[next_circle_index]; + circles[next_circle_index] = {.x = x0, .y = y0, .radius = radius, .visible = true}; + next_circle_index = (next_circle_index + 1) % circles.size(); + if (visible_circle_count < circles.size()) { + visible_circle_count++; + } + if (previous_circle.visible) { + invalidate_circle_area(previous_circle); + } + invalidate_circle_area(circles[(next_circle_index + circles.size() - 1) % circles.size()]); } static void clear_circles() { - // remove the circles from lvgl - for (auto circle : circles) { - lv_obj_delete(circle); + for (auto &circle : circles) { + if (circle.visible) { + invalidate_circle_area(circle); + } + circle.visible = false; } - // clear the vector - circles.clear(); + next_circle_index = 0; + visible_circle_count = 0; } diff --git a/components/ws-s3-geek/idf_component.yml b/components/ws-s3-geek/idf_component.yml index e1496ff3d..885363a5b 100644 --- a/components/ws-s3-geek/idf_component.yml +++ b/components/ws-s3-geek/idf_component.yml @@ -23,6 +23,7 @@ dependencies: espp/display_drivers: '>=1.0' espp/i2c: '>=1.0' espp/interrupt: '>=1.0' + espp/spi: '>=1.0' espp/task: '>=1.0' targets: - esp32s3 diff --git a/components/ws-s3-geek/include/ws-s3-geek.hpp b/components/ws-s3-geek/include/ws-s3-geek.hpp index 7d418d52e..81183d074 100644 --- a/components/ws-s3-geek/include/ws-s3-geek.hpp +++ b/components/ws-s3-geek/include/ws-s3-geek.hpp @@ -18,6 +18,7 @@ #include "base_component.hpp" #include "interrupt.hpp" #include "led.hpp" +#include "spi.hpp" #include "st7789.hpp" namespace espp { @@ -99,6 +100,14 @@ class WsS3Geek : public BaseComponent { /// \return The height of the LCD in pixels static constexpr size_t lcd_height() { return lcd_height_; } + /// Get the display width in pixels, according to the current orientation + /// \return The display width in pixels, according to the current orientation + size_t rotated_display_width() const; + + /// Get the display height in pixels, according to the current orientation + /// \return The display height in pixels, according to the current orientation + size_t rotated_display_height() const; + /// 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 lcd_dc_io; } @@ -129,15 +138,6 @@ class WsS3Geek : public BaseComponent { /// \note This is null unless initialize_display() has been called Pixel *vram1() const; - /// Write command and optional parameters to the LCD - /// \param command The command to write - /// \param parameters The command parameters to write - /// \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_command(uint8_t command, std::span parameters, uint32_t user_data); - /// Write a frame to the LCD /// \param x The x coordinate /// \param y The y coordinate @@ -149,17 +149,6 @@ class WsS3Geek : public BaseComponent { 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); - ///////////////////////////////////////////////////////////////////////////// // uSD Card ///////////////////////////////////////////////////////////////////////////// @@ -255,14 +244,11 @@ class WsS3Geek : public BaseComponent { // display std::shared_ptr> display_; + std::unique_ptr display_driver_; std::vector backlight_channel_configs_{}; std::shared_ptr backlight_{}; - /// 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 num_queued_trans = 0; + std::unique_ptr lcd_spi_; + std::unique_ptr lcd_; }; // class WsS3Geek } // namespace espp diff --git a/components/ws-s3-geek/src/ws-s3-geek.cpp b/components/ws-s3-geek/src/ws-s3-geek.cpp index e03218b9d..903576720 100644 --- a/components/ws-s3-geek/src/ws-s3-geek.cpp +++ b/components/ws-s3-geek/src/ws-s3-geek.cpp @@ -45,34 +45,13 @@ bool WsS3Geek::button_state() const { 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). -// -// cppcheck-suppress constParameterCallback -static void IRAM_ATTR lcd_spi_pre_transfer_callback(spi_transaction_t *t) { - static auto lcd_dc_io = WsS3Geek::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. -// -// cppcheck-suppress constParameterCallback -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_disp_get_default(); - lv_display_flush_ready(disp); - } +static void IRAM_ATTR lcd_spi_flush_ready(uint32_t) { + lv_display_t *disp = lv_display_get_default(); + lv_display_flush_ready(disp); } bool WsS3Geek::initialize_lcd() { - if (lcd_handle_ || backlight_) { + if (lcd_ || backlight_) { logger_.warn("LCD already initialized, not initializing again!"); return false; } @@ -90,52 +69,61 @@ bool WsS3Geek::initialize_lcd() { .duty_resolution = LEDC_TIMER_10_BIT})); brightness(100.0f); - esp_err_t ret; - - memset(&lcd_spi_bus_config_, 0, sizeof(lcd_spi_bus_config_)); - lcd_spi_bus_config_.mosi_io_num = lcd_mosi_io; - lcd_spi_bus_config_.miso_io_num = -1; - lcd_spi_bus_config_.sclk_io_num = lcd_sclk_io; - lcd_spi_bus_config_.quadwp_io_num = -1; - lcd_spi_bus_config_.quadhd_io_num = -1; - lcd_spi_bus_config_.max_transfer_sz = SPI_MAX_TRANSFER_BYTES; - - 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 = lcd_cs_io; - 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{ - .write_command = std::bind(&WsS3Geek::write_command, this, _1, _2, _3), - .lcd_send_lines = std::bind(&WsS3Geek::write_lcd_lines, this, _1, _2, _3, _4, _5, _6), - .reset_pin = lcd_reset_io, - .data_command_pin = lcd_dc_io, - .reset_value = reset_value, - .invert_colors = invert_colors, - .swap_color_order = swap_color_order, - .offset_x = lcd_offset_x, - .offset_y = lcd_offset_y, - .swap_xy = swap_xy, - .mirror_x = mirror_x, - .mirror_y = mirror_y}); + lcd_spi_ = std::make_unique(Spi::Config{ + .host = lcd_spi_num, + .sclk_io_num = lcd_sclk_io, + .mosi_io_num = lcd_mosi_io, + .miso_io_num = GPIO_NUM_NC, + .max_transfer_sz = SPI_MAX_TRANSFER_BYTES, + .log_level = get_log_level(), + }); + lcd_ = std::make_unique(SpiPanelIo::Config{ + .spi = lcd_spi_.get(), + .device_config = + { + .mode = 0, + .clock_speed_hz = lcd_clock_speed, + .input_delay_ns = 0, + .cs_io_num = lcd_cs_io, + .queue_size = spi_queue_size, + }, + .data_command_io = lcd_dc_io, + .data_command_bit_mask = DC_LEVEL_BIT, + .post_transaction_callback_bit_mask = FLUSH_BIT, + .post_transaction_callback = lcd_spi_flush_ready, + .log_level = get_log_level(), + }); + if (!lcd_->initialized()) { + lcd_.reset(); + lcd_spi_.reset(); + return false; + } + display_driver_ = std::make_unique( + espp::display_drivers::Config{.panel_io = lcd_.get(), + .write_command = nullptr, + .read_command = nullptr, + .lcd_send_lines = nullptr, + .reset_pin = lcd_reset_io, + .data_command_pin = lcd_dc_io, + .reset_value = reset_value, + .invert_colors = invert_colors, + .swap_color_order = swap_color_order, + .offset_x = lcd_offset_x, + .offset_y = lcd_offset_y, + .swap_xy = swap_xy, + .mirror_x = mirror_x, + .mirror_y = mirror_y}); + if (!display_driver_ || !display_driver_->initialize()) { + display_driver_.reset(); + lcd_.reset(); + lcd_spi_.reset(); + return false; + } return true; } bool WsS3Geek::initialize_display(size_t pixel_buffer_size) { - if (!lcd_handle_) { + if (!lcd_) { logger_.error( "LCD not initialized, you must call initialize_lcd() before initialize_display()!"); return false; @@ -148,11 +136,22 @@ bool WsS3Geek::initialize_display(size_t pixel_buffer_size) { // initialize the display / lvgl using namespace std::chrono_literals; display_ = std::make_shared>( - Display::LvglConfig{.width = lcd_width_, - .height = lcd_height_, - .flush_callback = DisplayDriver::flush, - .rotation_callback = DisplayDriver::rotate, - .rotation = rotation}, + Display::LvglConfig{ + .width = lcd_width_, + .height = lcd_height_, + .flush_callback = + [this](lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { + if (display_driver_) { + display_driver_->flush(disp, area, color_map); + } + }, + .rotation_callback = + [this](const DisplayRotation &new_rotation) { + if (display_driver_) { + display_driver_->set_rotation(new_rotation); + } + }, + .rotation = rotation}, Display::OledConfig{ .set_brightness_callback = [this](float brightness) { this->brightness(brightness * 100.0f); }, @@ -168,131 +167,26 @@ bool WsS3Geek::initialize_display(size_t pixel_buffer_size) { std::shared_ptr> WsS3Geek::display() const { return display_; } void IRAM_ATTR WsS3Geek::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 WsS3Geek::write_command(uint8_t command, std::span parameters, - uint32_t user_data) { - lcd_wait_lines(); - memset(&trans[0], 0, sizeof(spi_transaction_t)); - memset(&trans[1], 0, sizeof(spi_transaction_t)); - - trans[0].length = 8; - trans[0].user = reinterpret_cast(user_data); - trans[0].flags = SPI_TRANS_USE_TXDATA; - trans[0].tx_data[0] = command; - - trans[1].length = parameters.size() * 8; - if (parameters.size() <= 4) { - // copy the data pointer to trans[1].tx_data - memcpy(trans[1].tx_data, parameters.data(), parameters.size()); - trans[1].flags = SPI_TRANS_USE_TXDATA; - } else if (!parameters.empty()) { - trans[1].tx_buffer = parameters.data(); - trans[1].flags = 0; + if (lcd_) { + lcd_->wait(); } - trans[1].user = reinterpret_cast( - user_data | (1 << static_cast(display_drivers::Flags::DC_LEVEL_BIT))); - - esp_err_t ret = spi_device_queue_trans(lcd_handle_, &trans[0], 10 / portTICK_PERIOD_MS); - if (ret != ESP_OK) { - logger_.error("Couldn't queue spi command trans for display: {} '{}'", ret, - esp_err_to_name(ret)); - } else { - ++num_queued_trans; - if (!parameters.empty()) { - ret = spi_device_queue_trans(lcd_handle_, &trans[1], 10 / portTICK_PERIOD_MS); - if (ret != ESP_OK) { - logger_.error("Couldn't queue spi data trans for display: {} '{}'", ret, - esp_err_to_name(ret)); - } else { - ++num_queued_trans; - } - } - } -} - -void IRAM_ATTR WsS3Geek::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 WsS3Geek::write_lcd_frame(const uint16_t xs, const uint16_t ys, const uint16_t width, const uint16_t height, uint8_t *data) { + if (!display_driver_) { + return; + } 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); + display_driver_->fill(nullptr, &area, data); } else { // don't have data, so clear the area (set to 0) - DisplayDriver::clear(xs, ys, width, height); + display_driver_->clear(xs, ys, width, height); } } @@ -324,3 +218,31 @@ float WsS3Geek::brightness() const { } return 0.0f; } + +size_t WsS3Geek::rotated_display_width() const { + auto *display = lv_display_get_default(); + auto rotation = display ? lv_display_get_rotation(display) : LV_DISPLAY_ROTATION_0; + switch (rotation) { + case LV_DISPLAY_ROTATION_90: + case LV_DISPLAY_ROTATION_270: + return lcd_height_; + case LV_DISPLAY_ROTATION_0: + case LV_DISPLAY_ROTATION_180: + default: + return lcd_width_; + } +} + +size_t WsS3Geek::rotated_display_height() const { + auto *display = lv_display_get_default(); + auto rotation = display ? lv_display_get_rotation(display) : LV_DISPLAY_ROTATION_0; + switch (rotation) { + case LV_DISPLAY_ROTATION_90: + case LV_DISPLAY_ROTATION_270: + return lcd_width_; + case LV_DISPLAY_ROTATION_0: + case LV_DISPLAY_ROTATION_180: + default: + return lcd_height_; + } +} diff --git a/components/ws-s3-lcd-1-47/CMakeLists.txt b/components/ws-s3-lcd-1-47/CMakeLists.txt index 6a5d20ee5..3d6ce662f 100644 --- a/components/ws-s3-lcd-1-47/CMakeLists.txt +++ b/components/ws-s3-lcd-1-47/CMakeLists.txt @@ -2,6 +2,6 @@ idf_component_register( INCLUDE_DIRS "include" SRC_DIRS "src" - REQUIRES driver fatfs base_component display display_drivers i2c interrupt task neopixel + REQUIRES driver fatfs base_component display display_drivers i2c interrupt spi task neopixel REQUIRED_IDF_TARGETS "esp32s3" ) diff --git a/components/ws-s3-lcd-1-47/example/main/ws_s3_lcd_1_47_example.cpp b/components/ws-s3-lcd-1-47/example/main/ws_s3_lcd_1_47_example.cpp index b11290a56..ab73e39f7 100644 --- a/components/ws-s3-lcd-1-47/example/main/ws_s3_lcd_1_47_example.cpp +++ b/components/ws-s3-lcd-1-47/example/main/ws_s3_lcd_1_47_example.cpp @@ -1,13 +1,26 @@ +#include #include -#include #include "ws-s3-lcd-1-47.hpp" using namespace std::chrono_literals; -static std::vector circles; +static constexpr size_t MAX_CIRCLES = 10; +struct Circle { + int x{0}; + int y{0}; + int radius{0}; + bool visible{false}; +}; +static std::array circles; +static size_t next_circle_index = 0; +static size_t visible_circle_count = 0; +static lv_obj_t *circle_layer = nullptr; static std::mutex lvgl_mutex; +static bool initialize_circle_layer(int width, int height); +static void draw_circle_layer(lv_event_t *event); +static void invalidate_circle_area(const Circle &circle); static void draw_circle(int x0, int y0, int radius); static void clear_circles(); @@ -48,6 +61,20 @@ extern "C" void app_main(void) { // initialize the button, which we'll use to cycle the rotation of the display logger.info("Initializing the button"); + lv_obj_t *bg = nullptr; + lv_obj_t *label = nullptr; + static auto update_layout = [&]() { + int width = board.rotated_display_width(); + int height = board.rotated_display_height(); + lv_obj_set_size(bg, width, height); + lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); + if (circle_layer) { + lv_obj_set_size(circle_layer, width, height); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_move_foreground(circle_layer); + lv_obj_invalidate(circle_layer); + } + }; auto on_button_pressed = [&](const auto &event) { if (event.active) { // lock the display mutex @@ -55,22 +82,30 @@ extern "C" void app_main(void) { static auto rotation = LV_DISPLAY_ROTATION_0; rotation = static_cast((static_cast(rotation) + 1) % 4); fmt::print("Setting rotation to {}\n", (int)rotation); - lv_display_t *disp = lv_disp_get_default(); + lv_display_t *disp = lv_display_get_default(); lv_disp_set_rotation(disp, rotation); + update_layout(); } }; board.initialize_button(on_button_pressed); // set the background color to black - lv_obj_t *bg = lv_obj_create(lv_screen_active()); - lv_obj_set_size(bg, board.lcd_width(), board.lcd_height()); + bg = lv_obj_create(lv_screen_active()); + lv_obj_set_size(bg, board.rotated_display_width(), board.rotated_display_height()); lv_obj_set_style_bg_color(bg, lv_color_make(0, 0, 0), 0); + if (!initialize_circle_layer(board.rotated_display_width(), board.rotated_display_height())) { + logger.error("Failed to initialize circle layer!"); + return; + } // add text in the center of the screen - lv_obj_t *label = lv_label_create(lv_screen_active()); + label = lv_label_create(lv_screen_active()); lv_label_set_text(label, "Drawing circles\nto the screen."); lv_obj_align(label, LV_ALIGN_CENTER, 0, 0); lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_CENTER, 0); + update_layout(); + + lv_obj_move_foreground(circle_layer); // 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 { @@ -113,33 +148,19 @@ extern "C" void app_main(void) { while (true) { auto start = esp_timer_get_time(); // if there are 10 circles on the screen, clear them - static constexpr int max_circles = 10; - if (circles.size() >= max_circles) { + if (visible_circle_count >= MAX_CIRCLES) { // lock the lvgl mutex std::lock_guard lock(lvgl_mutex); clear_circles(); } else { // draw a circle of circles on the screen (just draw the next circle) - static constexpr int middle_x = Bsp::lcd_width() / 2; - static constexpr int middle_y = Bsp::lcd_height() / 2; + int middle_x = board.rotated_display_width() / 2; + int middle_y = board.rotated_display_height() / 2; static constexpr int radius = 50; - float angle = circles.size() * 2.0f * M_PI / max_circles; + float angle = visible_circle_count * 2.0f * M_PI / MAX_CIRCLES; int x = middle_x + radius * cos(angle); int y = middle_y + radius * sin(angle); - // handle the rotation of the display to ensure the circles are centered - auto rotation = lv_display_get_rotation(lv_disp_get_default()); - switch (rotation) { - default: - case LV_DISPLAY_ROTATION_0: - case LV_DISPLAY_ROTATION_180: - break; - case LV_DISPLAY_ROTATION_90: - case LV_DISPLAY_ROTATION_270: - std::swap(x, y); - break; - } - // lock the lvgl mutex std::lock_guard lock(lvgl_mutex); draw_circle(x, y, 10); @@ -151,20 +172,100 @@ extern "C" void app_main(void) { //! [ws-s3-lcd-1-47 example] } +static bool initialize_circle_layer(int width, int height) { + if (circle_layer) { + return true; + } + circle_layer = lv_obj_create(lv_screen_active()); + if (!circle_layer) { + return false; + } + lv_obj_remove_style_all(circle_layer); + lv_obj_set_size(circle_layer, width, height); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_CLICKABLE); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(circle_layer, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(circle_layer, 0, 0); + lv_obj_set_style_outline_width(circle_layer, 0, 0); + lv_obj_set_style_shadow_width(circle_layer, 0, 0); + lv_obj_add_event_cb(circle_layer, draw_circle_layer, LV_EVENT_DRAW_MAIN, nullptr); + return true; +} + +static void draw_circle_layer(lv_event_t *event) { + if (visible_circle_count == 0) { + return; + } + auto *obj = static_cast(lv_event_get_current_target(event)); + auto *layer = lv_event_get_layer(event); + lv_area_t obj_coords; + lv_obj_get_coords(obj, &obj_coords); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.base.layer = layer; + rect_dsc.radius = LV_RADIUS_CIRCLE; + rect_dsc.bg_opa = LV_OPA_70; + rect_dsc.bg_color = lv_color_make(0, 255, 255); + rect_dsc.border_width = 0; + rect_dsc.outline_width = 0; + rect_dsc.shadow_width = 0; + + for (const auto &circle : circles) { + if (!circle.visible) { + continue; + } + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_draw_rect(layer, &rect_dsc, &coords); + } +} + +static void invalidate_circle_area(const Circle &circle) { + if (!circle_layer || circle.radius <= 0) { + return; + } + + lv_area_t obj_coords; + lv_obj_get_coords(circle_layer, &obj_coords); + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_obj_invalidate_area(circle_layer, &coords); +} + static void draw_circle(int x0, int y0, int radius) { - 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); - circles.push_back(my_Cir); + if (!circle_layer) { + return; + } + lv_obj_move_foreground(circle_layer); + Circle previous_circle = circles[next_circle_index]; + circles[next_circle_index] = {.x = x0, .y = y0, .radius = radius, .visible = true}; + next_circle_index = (next_circle_index + 1) % circles.size(); + if (visible_circle_count < circles.size()) { + visible_circle_count++; + } + if (previous_circle.visible) { + invalidate_circle_area(previous_circle); + } + invalidate_circle_area(circles[(next_circle_index + circles.size() - 1) % circles.size()]); } static void clear_circles() { - // remove the circles from lvgl - for (auto circle : circles) { - lv_obj_delete(circle); + for (auto &circle : circles) { + if (circle.visible) { + invalidate_circle_area(circle); + } + circle.visible = false; } - // clear the vector - circles.clear(); + next_circle_index = 0; + visible_circle_count = 0; } diff --git a/components/ws-s3-lcd-1-47/idf_component.yml b/components/ws-s3-lcd-1-47/idf_component.yml index c1051cef2..5fe13147c 100644 --- a/components/ws-s3-lcd-1-47/idf_component.yml +++ b/components/ws-s3-lcd-1-47/idf_component.yml @@ -23,6 +23,7 @@ dependencies: espp/display_drivers: '>=1.0' espp/i2c: '>=1.0' espp/interrupt: '>=1.0' + espp/spi: '>=1.0' espp/task: '>=1.0' targets: - esp32s3 diff --git a/components/ws-s3-lcd-1-47/include/ws-s3-lcd-1-47.hpp b/components/ws-s3-lcd-1-47/include/ws-s3-lcd-1-47.hpp index 14b3c62fe..924a17a0d 100644 --- a/components/ws-s3-lcd-1-47/include/ws-s3-lcd-1-47.hpp +++ b/components/ws-s3-lcd-1-47/include/ws-s3-lcd-1-47.hpp @@ -19,6 +19,7 @@ #include "interrupt.hpp" #include "led.hpp" #include "neopixel.hpp" +#include "spi.hpp" #include "st7789.hpp" namespace espp { @@ -101,6 +102,14 @@ class WsS3Lcd147 : public BaseComponent { /// \return The height of the LCD in pixels static constexpr size_t lcd_height() { return lcd_height_; } + /// Get the display width in pixels, according to the current orientation + /// \return The display width in pixels, according to the current orientation + size_t rotated_display_width() const; + + /// Get the display height in pixels, according to the current orientation + /// \return The display height in pixels, according to the current orientation + size_t rotated_display_height() const; + /// 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 lcd_dc_io; } @@ -145,15 +154,6 @@ class WsS3Lcd147 : public BaseComponent { /// \note This is null unless initialize_display() has been called uint8_t *frame_buffer1() const; - /// Write command and optional parameters to the LCD - /// \param command The command to write - /// \param parameters The command parameters to write - /// \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_command(uint8_t command, std::span parameters, uint32_t user_data); - /// Write a frame to the LCD /// \param x The x coordinate /// \param y The y coordinate @@ -165,17 +165,6 @@ class WsS3Lcd147 : public BaseComponent { 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); - ///////////////////////////////////////////////////////////////////////////// // uSD Card ///////////////////////////////////////////////////////////////////////////// @@ -292,15 +281,12 @@ class WsS3Lcd147 : public BaseComponent { // display std::shared_ptr> display_; + std::unique_ptr display_driver_; std::vector backlight_channel_configs_{}; std::shared_ptr backlight_{}; - /// 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 num_queued_trans = 0; + std::unique_ptr lcd_spi_; + std::unique_ptr lcd_; uint8_t *frame_buffer0_{nullptr}; uint8_t *frame_buffer1_{nullptr}; }; // class WsS3Lcd147 diff --git a/components/ws-s3-lcd-1-47/src/ws-s3-lcd-1-47.cpp b/components/ws-s3-lcd-1-47/src/ws-s3-lcd-1-47.cpp index 0bd1217c5..7530cd39d 100644 --- a/components/ws-s3-lcd-1-47/src/ws-s3-lcd-1-47.cpp +++ b/components/ws-s3-lcd-1-47/src/ws-s3-lcd-1-47.cpp @@ -42,34 +42,13 @@ bool WsS3Lcd147::button_state() const { 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). -// -// cppcheck-suppress constParameterCallback -static void IRAM_ATTR lcd_spi_pre_transfer_callback(spi_transaction_t *t) { - static auto lcd_dc_io = WsS3Lcd147::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. -// -// cppcheck-suppress constParameterCallback -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_disp_get_default(); - lv_display_flush_ready(disp); - } +static void IRAM_ATTR lcd_spi_flush_ready(uint32_t) { + lv_display_t *disp = lv_display_get_default(); + lv_display_flush_ready(disp); } bool WsS3Lcd147::initialize_lcd() { - if (lcd_handle_ || backlight_) { + if (lcd_ || backlight_) { logger_.warn("LCD already initialized, not initializing again!"); return false; } @@ -89,52 +68,61 @@ bool WsS3Lcd147::initialize_lcd() { // default 100% brightness(100.0f); - esp_err_t ret; - - memset(&lcd_spi_bus_config_, 0, sizeof(lcd_spi_bus_config_)); - lcd_spi_bus_config_.mosi_io_num = lcd_mosi_io; - lcd_spi_bus_config_.miso_io_num = -1; - lcd_spi_bus_config_.sclk_io_num = lcd_sclk_io; - lcd_spi_bus_config_.quadwp_io_num = -1; - lcd_spi_bus_config_.quadhd_io_num = -1; - lcd_spi_bus_config_.max_transfer_sz = SPI_MAX_TRANSFER_BYTES; - - 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 = lcd_cs_io; - 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{ - .write_command = std::bind(&WsS3Lcd147::write_command, this, _1, _2, _3), - .lcd_send_lines = std::bind(&WsS3Lcd147::write_lcd_lines, this, _1, _2, _3, _4, _5, _6), - .reset_pin = lcd_reset_io, - .data_command_pin = lcd_dc_io, - .reset_value = reset_value, - .invert_colors = invert_colors, - .swap_color_order = swap_color_order, - .offset_x = lcd_offset_x, - .offset_y = lcd_offset_y, - .swap_xy = swap_xy, - .mirror_x = mirror_x, - .mirror_y = mirror_y}); + lcd_spi_ = std::make_unique(Spi::Config{ + .host = lcd_spi_num, + .sclk_io_num = lcd_sclk_io, + .mosi_io_num = lcd_mosi_io, + .miso_io_num = GPIO_NUM_NC, + .max_transfer_sz = SPI_MAX_TRANSFER_BYTES, + .log_level = get_log_level(), + }); + lcd_ = std::make_unique(SpiPanelIo::Config{ + .spi = lcd_spi_.get(), + .device_config = + { + .mode = 0, + .clock_speed_hz = lcd_clock_speed, + .input_delay_ns = 0, + .cs_io_num = lcd_cs_io, + .queue_size = spi_queue_size, + }, + .data_command_io = lcd_dc_io, + .data_command_bit_mask = DC_LEVEL_BIT, + .post_transaction_callback_bit_mask = FLUSH_BIT, + .post_transaction_callback = lcd_spi_flush_ready, + .log_level = get_log_level(), + }); + if (!lcd_->initialized()) { + lcd_.reset(); + lcd_spi_.reset(); + return false; + } + display_driver_ = std::make_unique( + espp::display_drivers::Config{.panel_io = lcd_.get(), + .write_command = nullptr, + .read_command = nullptr, + .lcd_send_lines = nullptr, + .reset_pin = lcd_reset_io, + .data_command_pin = lcd_dc_io, + .reset_value = reset_value, + .invert_colors = invert_colors, + .swap_color_order = swap_color_order, + .offset_x = lcd_offset_x, + .offset_y = lcd_offset_y, + .swap_xy = swap_xy, + .mirror_x = mirror_x, + .mirror_y = mirror_y}); + if (!display_driver_ || !display_driver_->initialize()) { + display_driver_.reset(); + lcd_.reset(); + lcd_spi_.reset(); + return false; + } return true; } bool WsS3Lcd147::initialize_display(size_t pixel_buffer_size) { - if (!lcd_handle_) { + if (!lcd_) { logger_.error( "LCD not initialized, you must call initialize_lcd() before initialize_display()!"); return false; @@ -147,11 +135,22 @@ bool WsS3Lcd147::initialize_display(size_t pixel_buffer_size) { // initialize the display / lvgl using namespace std::chrono_literals; display_ = std::make_shared>( - Display::LvglConfig{.width = lcd_width_, - .height = lcd_height_, - .flush_callback = DisplayDriver::flush, - .rotation_callback = DisplayDriver::rotate, - .rotation = rotation}, + Display::LvglConfig{ + .width = lcd_width_, + .height = lcd_height_, + .flush_callback = + [this](lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { + if (display_driver_) { + display_driver_->flush(disp, area, color_map); + } + }, + .rotation_callback = + [this](const DisplayRotation &new_rotation) { + if (display_driver_) { + display_driver_->set_rotation(new_rotation); + } + }, + .rotation = rotation}, Display::OledConfig{ .set_brightness_callback = [this](float brightness) { this->brightness(brightness * 100.0f); }, @@ -172,131 +171,26 @@ bool WsS3Lcd147::initialize_display(size_t pixel_buffer_size) { std::shared_ptr> WsS3Lcd147::display() const { return display_; } void IRAM_ATTR WsS3Lcd147::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 WsS3Lcd147::write_command(uint8_t command, std::span parameters, - uint32_t user_data) { - lcd_wait_lines(); - memset(&trans[0], 0, sizeof(spi_transaction_t)); - memset(&trans[1], 0, sizeof(spi_transaction_t)); - - trans[0].length = 8; - trans[0].user = reinterpret_cast(user_data); - trans[0].flags = SPI_TRANS_USE_TXDATA; - trans[0].tx_data[0] = command; - - trans[1].length = parameters.size() * 8; - if (parameters.size() <= 4) { - // copy the data pointer to trans[1].tx_data - memcpy(trans[1].tx_data, parameters.data(), parameters.size()); - trans[1].flags = SPI_TRANS_USE_TXDATA; - } else if (!parameters.empty()) { - trans[1].tx_buffer = parameters.data(); - trans[1].flags = 0; + if (lcd_) { + lcd_->wait(); } - trans[1].user = reinterpret_cast( - user_data | (1 << static_cast(display_drivers::Flags::DC_LEVEL_BIT))); - - esp_err_t ret = spi_device_queue_trans(lcd_handle_, &trans[0], 10 / portTICK_PERIOD_MS); - if (ret != ESP_OK) { - logger_.error("Couldn't queue spi command trans for display: {} '{}'", ret, - esp_err_to_name(ret)); - } else { - ++num_queued_trans; - if (!parameters.empty()) { - ret = spi_device_queue_trans(lcd_handle_, &trans[1], 10 / portTICK_PERIOD_MS); - if (ret != ESP_OK) { - logger_.error("Couldn't queue spi data trans for display: {} '{}'", ret, - esp_err_to_name(ret)); - } else { - ++num_queued_trans; - } - } - } -} - -void IRAM_ATTR WsS3Lcd147::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 WsS3Lcd147::write_lcd_frame(const uint16_t xs, const uint16_t ys, const uint16_t width, const uint16_t height, uint8_t *data) { + if (!display_driver_) { + return; + } 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); + display_driver_->fill(nullptr, &area, data); } else { // don't have data, so clear the area (set to 0) - DisplayDriver::clear(xs, ys, width, height); + display_driver_->clear(xs, ys, width, height); } } @@ -333,6 +227,34 @@ float WsS3Lcd147::brightness() const { return 0.0f; } +size_t WsS3Lcd147::rotated_display_width() const { + auto *display = lv_display_get_default(); + auto rotation = display ? lv_display_get_rotation(display) : LV_DISPLAY_ROTATION_0; + switch (rotation) { + case LV_DISPLAY_ROTATION_90: + case LV_DISPLAY_ROTATION_270: + return lcd_height_; + case LV_DISPLAY_ROTATION_0: + case LV_DISPLAY_ROTATION_180: + default: + return lcd_width_; + } +} + +size_t WsS3Lcd147::rotated_display_height() const { + auto *display = lv_display_get_default(); + auto rotation = display ? lv_display_get_rotation(display) : LV_DISPLAY_ROTATION_0; + switch (rotation) { + case LV_DISPLAY_ROTATION_90: + case LV_DISPLAY_ROTATION_270: + return lcd_width_; + case LV_DISPLAY_ROTATION_0: + case LV_DISPLAY_ROTATION_180: + default: + return lcd_height_; + } +} + ///////////////////////////////////////////////////////////////////////////// // LED ///////////////////////////////////////////////////////////////////////////// diff --git a/components/ws-s3-touch/CMakeLists.txt b/components/ws-s3-touch/CMakeLists.txt index 465bfe3a2..c53594f48 100644 --- a/components/ws-s3-touch/CMakeLists.txt +++ b/components/ws-s3-touch/CMakeLists.txt @@ -1,6 +1,6 @@ idf_component_register( INCLUDE_DIRS "include" SRC_DIRS "src" - REQUIRES driver base_component display display_drivers i2c input_drivers interrupt led cst816 task qmi8658 pcf85063 + REQUIRES driver base_component display display_drivers i2c input_drivers interrupt led cst816 qmi8658 pcf85063 spi task REQUIRED_IDF_TARGETS "esp32s3" ) diff --git a/components/ws-s3-touch/example/main/ws_s3_touch_example.cpp b/components/ws-s3-touch/example/main/ws_s3_touch_example.cpp index 4f9bf85f5..98e7ae4e2 100644 --- a/components/ws-s3-touch/example/main/ws_s3_touch_example.cpp +++ b/components/ws-s3-touch/example/main/ws_s3_touch_example.cpp @@ -1,5 +1,5 @@ +#include #include -#include #include #include @@ -11,9 +11,21 @@ using namespace std::chrono_literals; static constexpr size_t MAX_CIRCLES = 100; -static std::deque circles; +struct Circle { + int x{0}; + int y{0}; + int radius{0}; + bool visible{false}; +}; +static std::array circles; +static size_t next_circle_index = 0; +static size_t visible_circle_count = 0; +static lv_obj_t *circle_layer = nullptr; static std::recursive_mutex lvgl_mutex; +static bool initialize_circle_layer(int width, int height); +static void draw_circle_layer(lv_event_t *event); +static void invalidate_circle_area(const Circle &circle); static void draw_circle(int x0, int y0, int radius); static void clear_circles(); @@ -103,11 +115,6 @@ extern "C" void app_main(void) { logger.error("Failed to initialize display!"); return; } - // initialize the touchpad - if (!bsp.initialize_touch(touch_callback)) { - logger.error("Failed to initialize touchpad!"); - return; - } // initialize the RTC if (!bsp.initialize_rtc()) { @@ -183,14 +190,20 @@ extern "C" void app_main(void) { // set the background color to black lv_obj_t *bg = lv_obj_create(lv_screen_active()); - lv_obj_set_size(bg, bsp.lcd_width(), bsp.lcd_height()); + lv_obj_set_size(bg, bsp.rotated_display_width(), bsp.rotated_display_height()); lv_obj_set_style_bg_color(bg, lv_color_make(0, 0, 0), 0); + if (!initialize_circle_layer(bsp.rotated_display_width(), bsp.rotated_display_height())) { + logger.error("Failed to initialize circle layer!"); + return; + } // add text in the center of the screen lv_obj_t *label = lv_label_create(lv_screen_active()); static std::string label_text = "\n\n\n\nTouch the screen!\nPress the home button to clear circles."; lv_label_set_text(label, label_text.c_str()); + lv_label_set_long_mode(label, LV_LABEL_LONG_WRAP); + lv_obj_set_width(label, bsp.rotated_display_width()); lv_obj_align(label, LV_ALIGN_TOP_LEFT, 0, 0); lv_obj_set_style_text_align(label, LV_TEXT_ALIGN_LEFT, 0); @@ -235,22 +248,46 @@ extern "C" void app_main(void) { lv_label_set_text(label_btn, LV_SYMBOL_REFRESH); // center the text in the button lv_obj_align(label_btn, LV_ALIGN_CENTER, 0, 0); + static auto update_layout = [&]() { + int width = bsp.rotated_display_width(); + int height = bsp.rotated_display_height(); + lv_obj_set_size(bg, width, height); + lv_obj_set_width(label, width); + lv_obj_align(label, LV_ALIGN_TOP_LEFT, 0, 0); + lv_obj_align(rtc_label, LV_ALIGN_TOP_MID, 0, 20); + lv_obj_align(btn, LV_ALIGN_TOP_LEFT, 0, 0); + if (circle_layer) { + lv_obj_set_size(circle_layer, width, height); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_move_foreground(circle_layer); + lv_obj_invalidate(circle_layer); + } + }; + static auto rotate_display = [&]() { + std::lock_guard lock(lvgl_mutex); + clear_circles(); + static auto rotation = LV_DISPLAY_ROTATION_0; + rotation = static_cast((static_cast(rotation) + 1) % 4); + lv_display_t *disp = lv_display_get_default(); + lv_disp_set_rotation(disp, rotation); + update_layout(); + }; lv_obj_add_event_cb( - btn, - [](auto event) { - std::lock_guard lock(lvgl_mutex); - clear_circles(); - static auto rotation = LV_DISPLAY_ROTATION_0; - rotation = static_cast((static_cast(rotation) + 1) % 4); - lv_display_t *disp = lv_display_get_default(); - lv_disp_set_rotation(disp, rotation); - }, - LV_EVENT_PRESSED, nullptr); + btn, [](auto event) { rotate_display(); }, LV_EVENT_PRESSED, nullptr); + update_layout(); // 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); + lv_obj_move_foreground(circle_layer); + + // initialize the touchpad after the circle layer exists so touch events can + // update it immediately. + if (!bsp.initialize_touch(touch_callback)) { + logger.error("Failed to initialize touchpad!"); + return; + } logger.info("Starting LVGL task handler!"); @@ -358,13 +395,15 @@ extern "C" void app_main(void) { // use the pitch to to draw a line on the screen indiating the // direction from the center of the screen to "down" - int x0 = bsp.lcd_width() / 2; - int y0 = bsp.lcd_height() / 2; + int x0 = bsp.rotated_display_width() / 2; + int y0 = bsp.rotated_display_height() / 2; int x1 = x0 + 50 * gravity_vector.x; int y1 = y0 + 50 * gravity_vector.y; static lv_point_precise_t line_points0[] = {{x0, y0}, {x1, y1}}; + line_points0[0].x = x0; + line_points0[0].y = y0; line_points0[1].x = x1; line_points0[1].y = y1; @@ -399,6 +438,8 @@ extern "C" void app_main(void) { y1 = y0 + 50 * vy; static lv_point_precise_t line_points1[] = {{x0, y0}, {x1, y1}}; + line_points1[0].x = x0; + line_points1[0].y = y0; line_points1[1].x = x1; line_points1[1].y = y1; @@ -426,28 +467,100 @@ extern "C" void app_main(void) { //! [ws-s3-touch example] } +static bool initialize_circle_layer(int width, int height) { + if (circle_layer) { + return true; + } + circle_layer = lv_obj_create(lv_screen_active()); + if (!circle_layer) { + return false; + } + lv_obj_remove_style_all(circle_layer); + lv_obj_set_size(circle_layer, width, height); + lv_obj_align(circle_layer, LV_ALIGN_CENTER, 0, 0); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_CLICKABLE); + lv_obj_clear_flag(circle_layer, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_set_style_bg_opa(circle_layer, LV_OPA_TRANSP, 0); + lv_obj_set_style_border_width(circle_layer, 0, 0); + lv_obj_set_style_outline_width(circle_layer, 0, 0); + lv_obj_set_style_shadow_width(circle_layer, 0, 0); + lv_obj_add_event_cb(circle_layer, draw_circle_layer, LV_EVENT_DRAW_MAIN, nullptr); + return true; +} + +static void draw_circle_layer(lv_event_t *event) { + if (visible_circle_count == 0) { + return; + } + auto *obj = static_cast(lv_event_get_current_target(event)); + auto *layer = lv_event_get_layer(event); + lv_area_t obj_coords; + lv_obj_get_coords(obj, &obj_coords); + + lv_draw_rect_dsc_t rect_dsc; + lv_draw_rect_dsc_init(&rect_dsc); + rect_dsc.base.layer = layer; + rect_dsc.radius = LV_RADIUS_CIRCLE; + rect_dsc.bg_opa = LV_OPA_70; + rect_dsc.bg_color = lv_color_make(0, 255, 255); + rect_dsc.border_width = 0; + rect_dsc.outline_width = 0; + rect_dsc.shadow_width = 0; + + for (const auto &circle : circles) { + if (!circle.visible) { + continue; + } + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_draw_rect(layer, &rect_dsc, &coords); + } +} + +static void invalidate_circle_area(const Circle &circle) { + if (!circle_layer || circle.radius <= 0) { + return; + } + + lv_area_t obj_coords; + lv_obj_get_coords(circle_layer, &obj_coords); + lv_area_t coords = { + .x1 = static_cast(obj_coords.x1 + circle.x - circle.radius), + .y1 = static_cast(obj_coords.y1 + circle.y - circle.radius), + .x2 = static_cast(obj_coords.x1 + circle.x + circle.radius - 1), + .y2 = static_cast(obj_coords.y1 + circle.y + circle.radius - 1), + }; + lv_obj_invalidate_area(circle_layer, &coords); +} + static void draw_circle(int x0, int y0, int radius) { - // 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(); + if (!circle_layer) { + return; + } + lv_obj_move_foreground(circle_layer); + Circle previous_circle = circles[next_circle_index]; + circles[next_circle_index] = {.x = x0, .y = y0, .radius = radius, .visible = true}; + next_circle_index = (next_circle_index + 1) % circles.size(); + if (visible_circle_count < circles.size()) { + visible_circle_count++; } - 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); + if (previous_circle.visible) { + invalidate_circle_area(previous_circle); + } + invalidate_circle_area(circles[(next_circle_index + circles.size() - 1) % circles.size()]); } static void clear_circles() { - // remove the circles from lvgl - for (auto circle : circles) { - lv_obj_delete(circle); + for (auto &circle : circles) { + if (circle.visible) { + invalidate_circle_area(circle); + } + circle.visible = false; } - // clear the vector - circles.clear(); + next_circle_index = 0; + visible_circle_count = 0; } diff --git a/components/ws-s3-touch/idf_component.yml b/components/ws-s3-touch/idf_component.yml index 11630909d..4d53e0c75 100644 --- a/components/ws-s3-touch/idf_component.yml +++ b/components/ws-s3-touch/idf_component.yml @@ -25,6 +25,7 @@ dependencies: espp/interrupt: '>=1.0' espp/led: '>=1.0' espp/cst816: '>=1.0' + espp/spi: '>=1.0' espp/task: '>=1.0' espp/qmi8658: '>=1.0' espp/pcf85063: '>=1.0' diff --git a/components/ws-s3-touch/include/ws-s3-touch.hpp b/components/ws-s3-touch/include/ws-s3-touch.hpp index c237bc7f1..e505fbbbf 100644 --- a/components/ws-s3-touch/include/ws-s3-touch.hpp +++ b/components/ws-s3-touch/include/ws-s3-touch.hpp @@ -16,6 +16,7 @@ #include "led.hpp" #include "pcf85063.hpp" #include "qmi8658.hpp" +#include "spi.hpp" #include "st7789.hpp" #include "touchpad_input.hpp" @@ -159,6 +160,14 @@ class WsS3Touch : public BaseComponent { /// \return The height of the LCD in pixels static constexpr size_t lcd_height() { return lcd_height_; } + /// Get the display width in pixels, according to the current orientation + /// \return The display width in pixels, according to the current orientation + size_t rotated_display_width() const; + + /// Get the display height in pixels, according to the current orientation + /// \return The display height in pixels, according to the current orientation + size_t rotated_display_height() const; + /// 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 lcd_dc_io; } @@ -189,15 +198,6 @@ class WsS3Touch : public BaseComponent { /// \note This is null unless initialize_display() has been called Pixel *vram1() const; - /// Write command and optional parameters to the LCD - /// \param command The command to write - /// \param parameters The command parameters to write - /// \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_command(uint8_t command, std::span parameters, uint32_t user_data); - /// Write a frame to the LCD /// \param x The x coordinate /// \param y The y coordinate @@ -209,17 +209,6 @@ class WsS3Touch : public BaseComponent { 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); - ///////////////////////////////////////////////////////////////////////////// // Button ///////////////////////////////////////////////////////////////////////////// @@ -413,13 +402,9 @@ class WsS3Touch : public BaseComponent { std::shared_ptr> display_; std::vector backlight_channel_configs_{}; std::shared_ptr backlight_{}; - /// 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 num_queued_trans = 0; + std::unique_ptr display_driver_; + std::unique_ptr lcd_spi_; + std::unique_ptr lcd_; // sound std::shared_ptr buzzer_; diff --git a/components/ws-s3-touch/src/video.cpp b/components/ws-s3-touch/src/video.cpp index 38c51b4c5..8e3f54cf7 100644 --- a/components/ws-s3-touch/src/video.cpp +++ b/components/ws-s3-touch/src/video.cpp @@ -12,85 +12,71 @@ using namespace espp; 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). -// -// cppcheck-suppress constParameterCallback -static void lcd_spi_pre_transfer_callback(spi_transaction_t *t) { - static auto lcd_dc_io = WsS3Touch::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. -// -// cppcheck-suppress constParameterCallback -static void 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_display_get_default(); - lv_display_flush_ready(disp); - } +static void IRAM_ATTR lcd_spi_flush_ready(uint32_t) { + lv_display_t *disp = lv_display_get_default(); + lv_display_flush_ready(disp); } bool WsS3Touch::initialize_lcd() { - if (lcd_handle_ || backlight_) { + if (lcd_ || backlight_) { logger_.warn("LCD already initialized, not initializing again!"); return false; } logger_.info("Initializing LCD..."); - esp_err_t ret; - - memset(&lcd_spi_bus_config_, 0, sizeof(lcd_spi_bus_config_)); - lcd_spi_bus_config_.mosi_io_num = lcd_mosi_io; - lcd_spi_bus_config_.miso_io_num = -1; - lcd_spi_bus_config_.sclk_io_num = lcd_sclk_io; - lcd_spi_bus_config_.quadwp_io_num = -1; - lcd_spi_bus_config_.quadhd_io_num = -1; - lcd_spi_bus_config_.max_transfer_sz = SPI_MAX_TRANSFER_BYTES; - - 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 = lcd_cs_io; - 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; - logger_.info("Initializing SPI..."); - - // Initialize the SPI bus - ret = spi_bus_initialize(lcd_spi_num, &lcd_spi_bus_config_, SPI_DMA_CH_AUTO); - ESP_ERROR_CHECK(ret); - logger_.info("Adding device to SPI bus..."); - // 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 + lcd_spi_ = std::make_unique(Spi::Config{ + .host = lcd_spi_num, + .sclk_io_num = lcd_sclk_io, + .mosi_io_num = lcd_mosi_io, + .miso_io_num = GPIO_NUM_NC, + .max_transfer_sz = SPI_MAX_TRANSFER_BYTES, + .log_level = get_log_level(), + }); + lcd_ = std::make_unique(SpiPanelIo::Config{ + .spi = lcd_spi_.get(), + .device_config = + { + .mode = 0, + .clock_speed_hz = lcd_clock_speed, + .input_delay_ns = 0, + .cs_io_num = lcd_cs_io, + .queue_size = 6, + }, + .data_command_io = lcd_dc_io, + .data_command_bit_mask = DC_LEVEL_BIT, + .post_transaction_callback_bit_mask = FLUSH_BIT, + .post_transaction_callback = lcd_spi_flush_ready, + .log_level = get_log_level(), + }); + if (!lcd_->initialized()) { + lcd_.reset(); + lcd_spi_.reset(); + return false; + } logger_.info("Initializing the display driver..."); - using namespace std::placeholders; - DisplayDriver::initialize(espp::display_drivers::Config{ - .write_command = std::bind(&WsS3Touch::write_command, this, _1, _2, _3), - .lcd_send_lines = std::bind(&WsS3Touch::write_lcd_lines, this, _1, _2, _3, _4, _5, _6), - .reset_pin = lcd_reset_io, - .data_command_pin = lcd_dc_io, - .reset_value = reset_value, - .invert_colors = invert_colors, - .swap_color_order = swap_color_order, - .offset_x = offset_x, - .offset_y = offset_y, - .swap_xy = swap_xy, - .mirror_x = mirror_x, - .mirror_y = mirror_y}); + display_driver_ = std::make_unique( + espp::display_drivers::Config{.panel_io = lcd_.get(), + .write_command = nullptr, + .read_command = nullptr, + .lcd_send_lines = nullptr, + .reset_pin = lcd_reset_io, + .data_command_pin = lcd_dc_io, + .reset_value = reset_value, + .invert_colors = invert_colors, + .swap_color_order = swap_color_order, + .offset_x = offset_x, + .offset_y = offset_y, + .swap_xy = swap_xy, + .mirror_x = mirror_x, + .mirror_y = mirror_y}); + if (!display_driver_ || !display_driver_->initialize()) { + display_driver_.reset(); + lcd_.reset(); + lcd_spi_.reset(); + return false; + } logger_.info("Display driver initialized successfully!"); // Initialize backlight PWM (moved out of Display) @@ -108,7 +94,7 @@ bool WsS3Touch::initialize_lcd() { } bool WsS3Touch::initialize_display(size_t pixel_buffer_size) { - if (!lcd_handle_) { + if (!lcd_) { logger_.error( "LCD not initialized, you must call initialize_lcd() before initialize_display()!"); return false; @@ -121,11 +107,22 @@ bool WsS3Touch::initialize_display(size_t pixel_buffer_size) { // initialize the display / lvgl using namespace std::chrono_literals; display_ = std::make_shared>( - Display::LvglConfig{.width = lcd_width_, - .height = lcd_height_, - .flush_callback = DisplayDriver::flush, - .rotation_callback = DisplayDriver::rotate, - .rotation = rotation}, + Display::LvglConfig{ + .width = lcd_width_, + .height = lcd_height_, + .flush_callback = + [this](lv_display_t *disp, const lv_area_t *area, uint8_t *color_map) { + if (display_driver_) { + display_driver_->flush(disp, area, color_map); + } + }, + .rotation_callback = + [this](const DisplayRotation &new_rotation) { + if (display_driver_) { + display_driver_->set_rotation(new_rotation); + } + }, + .rotation = rotation}, Display::OledConfig{ .set_brightness_callback = [this](float brightness) { this->brightness(brightness * 100.0f); }, @@ -151,131 +148,26 @@ bool WsS3Touch::initialize_display(size_t pixel_buffer_size) { } void WsS3Touch::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. + if (lcd_) { + lcd_->wait(); } } -void WsS3Touch::write_command(uint8_t command, std::span parameters, - uint32_t user_data) { - lcd_wait_lines(); - memset(&trans[0], 0, sizeof(spi_transaction_t)); - memset(&trans[1], 0, sizeof(spi_transaction_t)); - - trans[0].length = 8; - trans[0].user = reinterpret_cast(user_data); - trans[0].flags = SPI_TRANS_USE_TXDATA; - trans[0].tx_data[0] = command; - - trans[1].length = parameters.size() * 8; - if (parameters.size() <= 4) { - // copy the data pointer to trans[1].tx_data - memcpy(trans[1].tx_data, parameters.data(), parameters.size()); - trans[1].flags = SPI_TRANS_USE_TXDATA; - } else if (!parameters.empty()) { - trans[1].tx_buffer = parameters.data(); - trans[1].flags = 0; - } - trans[1].user = reinterpret_cast( - user_data | (1 << static_cast(display_drivers::Flags::DC_LEVEL_BIT))); - - esp_err_t ret = spi_device_queue_trans(lcd_handle_, &trans[0], 10 / portTICK_PERIOD_MS); - if (ret != ESP_OK) { - logger_.error("Couldn't queue spi command trans for display: {} '{}'", ret, - esp_err_to_name(ret)); - } else { - ++num_queued_trans; - if (!parameters.empty()) { - ret = spi_device_queue_trans(lcd_handle_, &trans[1], 10 / portTICK_PERIOD_MS); - if (ret != ESP_OK) { - logger_.error("Couldn't queue spi data trans for display: {} '{}'", ret, - esp_err_to_name(ret)); - } else { - ++num_queued_trans; - } - } - } -} - -void WsS3Touch::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 WsS3Touch::write_lcd_frame(const uint16_t xs, const uint16_t ys, const uint16_t width, const uint16_t height, uint8_t *data) { + if (!display_driver_) { + return; + } 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); + display_driver_->fill(nullptr, &area, data); } else { // don't have data, so clear the area (set to 0) - DisplayDriver::clear(xs, ys, width, height); + display_driver_->clear(xs, ys, width, height); } } @@ -309,3 +201,31 @@ float WsS3Touch::brightness() const { } return 0.0f; // if no backlight, return 0 } + +size_t WsS3Touch::rotated_display_width() const { + auto *display = lv_display_get_default(); + auto rotation = display ? lv_display_get_rotation(display) : LV_DISPLAY_ROTATION_0; + switch (rotation) { + case LV_DISPLAY_ROTATION_90: + case LV_DISPLAY_ROTATION_270: + return lcd_height_; + case LV_DISPLAY_ROTATION_0: + case LV_DISPLAY_ROTATION_180: + default: + return lcd_width_; + } +} + +size_t WsS3Touch::rotated_display_height() const { + auto *display = lv_display_get_default(); + auto rotation = display ? lv_display_get_rotation(display) : LV_DISPLAY_ROTATION_0; + switch (rotation) { + case LV_DISPLAY_ROTATION_90: + case LV_DISPLAY_ROTATION_270: + return lcd_width_; + case LV_DISPLAY_ROTATION_0: + case LV_DISPLAY_ROTATION_180: + default: + return lcd_height_; + } +} diff --git a/doc/Doxyfile b/doc/Doxyfile index a1f9156be..367a91594 100755 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -213,10 +213,14 @@ INPUT = \ $(PROJECT_PATH)/components/cst816/include/cst816.hpp \ $(PROJECT_PATH)/components/csv/include/csv.hpp \ $(PROJECT_PATH)/components/display/include/display.hpp \ + $(PROJECT_PATH)/components/display_drivers/include/display_drivers.hpp \ $(PROJECT_PATH)/components/display_drivers/include/gc9a01.hpp \ $(PROJECT_PATH)/components/display_drivers/include/ili9341.hpp \ + $(PROJECT_PATH)/components/display_drivers/include/ili9881.hpp \ $(PROJECT_PATH)/components/display_drivers/include/sh8601.hpp \ + $(PROJECT_PATH)/components/display_drivers/include/spi_panel_io.hpp \ $(PROJECT_PATH)/components/display_drivers/include/ssd1351.hpp \ + $(PROJECT_PATH)/components/display_drivers/include/st7123.hpp \ $(PROJECT_PATH)/components/display_drivers/include/st7789.hpp \ $(PROJECT_PATH)/components/display_drivers/include/st7796.hpp \ $(PROJECT_PATH)/components/dns_server/include/dns_server.hpp \ diff --git a/doc/en/display/display_drivers.rst b/doc/en/display/display_drivers.rst index b7311753a..1f3c7b4f5 100755 --- a/doc/en/display/display_drivers.rst +++ b/doc/en/display/display_drivers.rst @@ -1,9 +1,17 @@ Display Drivers *************** -ESPP contains a few different display drivers in the `display_drivers` -component, corresponding to common display drivers on espressif development -boards. +ESPP contains a reusable set of display-controller implementations in the +`display_drivers` component. + +The drivers use the shared object-style +`display_drivers::Controller` / `display_drivers::MipiDbiDisplayDriver` +architecture so controller state is owned per instance instead of hidden behind +static helpers. + +The component also provides `SpiPanelIo` / `SpiCommandData`, a SPI-backed +implementation of the shared `display_drivers::PanelIo` transport interface for +command/data display panels, in ``components/display_drivers/include/spi_panel_io.hpp``. .. ------------------------------- Example ------------------------------------- @@ -16,9 +24,13 @@ boards. API Reference ------------- +.. include-build-file:: inc/display_drivers.inc .. include-build-file:: inc/gc9a01.inc .. include-build-file:: inc/ili9341.inc +.. include-build-file:: inc/ili9881.inc .. include-build-file:: inc/sh8601.inc +.. include-build-file:: inc/spi_panel_io.inc .. include-build-file:: inc/ssd1351.inc +.. include-build-file:: inc/st7123.inc .. include-build-file:: inc/st7789.inc .. include-build-file:: inc/st7796.inc diff --git a/doc/en/smartpanlee_sc01_plus.rst b/doc/en/smartpanlee_sc01_plus.rst index 2209755ab..fde622af6 100755 --- a/doc/en/smartpanlee_sc01_plus.rst +++ b/doc/en/smartpanlee_sc01_plus.rst @@ -12,6 +12,7 @@ maps. The `espp::SmartPanleeSc01Plus` component provides a singleton hardware abstraction for the display, touch, backlight, audio output, and microSD card, while also exposing the board's documented peripheral pins for application use. +Its LCD path uses the object-style `espp::St7796` controller integration. .. toctree:: diff --git a/doc/en/t_deck.rst b/doc/en/t_deck.rst index b8b12354a..939e84bf7 100644 --- a/doc/en/t_deck.rst +++ b/doc/en/t_deck.rst @@ -4,11 +4,13 @@ LilyGo T-Deck T-Deck ------ -The LilyGo T-Deck is a development board for the ESP32-S3 module. It features a -nice touchscreen display and expansion headers. +The LilyGo T-Deck is an ESP32-S3 development board with a touchscreen display, +keyboard, trackball, audio output, and expansion headers. The `espp::TDeck` component provides a singleton hardware abstraction for -initializing the touch, display, audio, and micro-SD card subsystems. +initializing the touch, display, keyboard, trackball, audio, and micro-SD card +subsystems. The LCD now uses the shared `espp::Spi` / `SpiPanelIo` transport +path while SDSPI-mounted microSD access stays on the same SPI host. .. ------------------------------- Example -------------------------------------