diff --git a/components/display_drivers/include/jd9365.hpp b/components/display_drivers/include/jd9365.hpp new file mode 100644 index 000000000..1e8af842f --- /dev/null +++ b/components/display_drivers/include/jd9365.hpp @@ -0,0 +1,351 @@ +#pragma once + +#include + +#include "display_drivers.hpp" + +namespace espp { +/** + * @brief Display driver for the JD9365 MIPI-DSI display controller. + * + * The JD9365 (e.g. the Waveshare 10.1" 800x1280 DSI panel used with the + * ESP32-P4-ETH / ESP32-P4-NANO boards) is configured over the MIPI-DSI DBI + * (command) channel and then driven by the DPI video stream. This follows the + * same interface as the other espp display drivers and relies on a lower-level + * transport to execute write_command. + * + * The vendor initialization sequence (the page-selected register writes below) + * is taken verbatim from the default init table + * (`vendor_specific_init_default[]`) of Waveshare's esp_lcd_jd9365 v2.0.0 + * managed component (Apache-2.0, Copyright 2024 Espressif Systems (Shanghai) + * CO LTD), as is the command ordering around it (software reset; user page / + * MADCTL / COLMOD / DSI lane config; vendor table ending in Sleep-Out + + * Display-On). + */ +class Jd9365 : public display_drivers::MipiDbiDisplayDriver { + // JD9365 MADCTL mirror bits (gate scan / source scan direction) + static constexpr uint8_t GS_BIT = 1 << 0; ///< Gate scan direction -> mirror x + static constexpr uint8_t SS_BIT = 1 << 1; ///< Source scan direction -> mirror y + +public: + enum class Command : uint8_t { + nop = 0x00, ///< No Operation + swreset = 0x01, ///< Software Reset + sleep_in = 0x10, ///< Sleep In + sleep_out = 0x11, ///< Sleep Out + invert_off = 0x20, ///< Display Inversion Off + invert_on = 0x21, ///< Display Inversion On + display_off = 0x28, ///< Display Off + display_on = 0x29, ///< Display On + caset = 0x2A, ///< Column Address Set + raset = 0x2B, ///< Row Address Set + ramwr = 0x2C, ///< Memory Write + madctl = 0x36, ///< Memory Data Access Control + colmod = 0x3A, ///< Pixel Format Set + dsi_int0 = 0x80, ///< DSI interface config (lane count), on the user page + page_select = 0xE0, ///< Command page select (0x00 = user page) + }; + + /// Values for the dsi_int0 (0x80) DSI lane-count command + static constexpr uint8_t DSI_1_LANE = 0x00; + static constexpr uint8_t DSI_2_LANE = 0x01; + static constexpr uint8_t DSI_3_LANE = 0x10; + static constexpr uint8_t DSI_4_LANE = 0x11; + + /// Value for page_select (0xE0) selecting the user command page + static constexpr uint8_t PAGE_USER = 0x00; + + explicit Jd9365(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)}) {} + + bool initialize() override { + using namespace std::chrono_literals; + display_drivers::init_pins(config_.reset_pin, config_.data_command_pin, config_.reset_value); + + // Neither supported board routes a panel reset GPIO, so do what the vendor + // component's panel_jd9365_reset() does in that case: a DCS software reset + // followed by a 120 ms delay. + if (config_.reset_pin == GPIO_NUM_NC) { + write_command(static_cast(Command::swreset), {}, 0); + std::this_thread::sleep_for(120ms); + } + + auto madctl = make_madctl(DisplayRotation::LANDSCAPE); + + uint8_t colmod = 0x55; + switch (config_.bits_per_pixel) { + case 16: // RGB565 + colmod = 0x55; + break; + case 18: // RGB666 + colmod = 0x66; + break; + case 24: // RGB888 + colmod = 0x77; + break; + default: + break; + } + + auto init_commands = std::to_array>({ + // Select the user command page, then MADCTL / COLMOD / DSI lane count + // (2 data lanes), in the same order the vendor component's + // panel_jd9365_init() sends them before the vendor init table. + {static_cast(Command::page_select), {PAGE_USER}, 0}, + {static_cast(Command::madctl), {madctl}, 0}, + {static_cast(Command::colmod), {colmod}, 0}, + {static_cast(Command::dsi_int0), {DSI_2_LANE}, 0}, + + // Vendor init table (verbatim from Waveshare's esp_lcd_jd9365 v2.0.0 + // vendor_specific_init_default[], default 800x1280 10.1" panel table). + {0xE0, {0x00}, 0}, + {0xE1, {0x93}, 0}, + {0xE2, {0x65}, 0}, + {0xE3, {0xF8}, 0}, + {0x80, {0x01}, 0}, + + // Select command page 1 + {0xE0, {0x01}, 0}, + {0x00, {0x00}, 0}, + {0x01, {0x38}, 0}, + {0x03, {0x10}, 0}, + {0x04, {0x38}, 0}, + + {0x0C, {0x74}, 0}, + + {0x17, {0x00}, 0}, + {0x18, {0xAF}, 0}, + {0x19, {0x00}, 0}, + {0x1A, {0x00}, 0}, + {0x1B, {0xAF}, 0}, + {0x1C, {0x00}, 0}, + + {0x35, {0x26}, 0}, + + {0x37, {0x09}, 0}, + + {0x38, {0x04}, 0}, + {0x39, {0x00}, 0}, + {0x3A, {0x01}, 0}, + {0x3C, {0x78}, 0}, + {0x3D, {0xFF}, 0}, + {0x3E, {0xFF}, 0}, + {0x3F, {0x7F}, 0}, + + {0x40, {0x06}, 0}, + {0x41, {0xA0}, 0}, + {0x42, {0x81}, 0}, + {0x43, {0x1E}, 0}, + {0x44, {0x0D}, 0}, + {0x45, {0x28}, 0}, + // SKIP: //{0x4A, (uint8_t[]){0x35}, 1, 0},//bist + + {0x55, {0x02}, 0}, + {0x57, {0x69}, 0}, + {0x59, {0x0A}, 0}, + {0x5A, {0x2A}, 0}, + {0x5B, {0x17}, 0}, + + {0x5D, {0x7F}, 0}, + {0x5E, {0x6A}, 0}, + {0x5F, {0x5B}, 0}, + {0x60, {0x4F}, 0}, + {0x61, {0x4A}, 0}, + {0x62, {0x3D}, 0}, + {0x63, {0x41}, 0}, + {0x64, {0x2A}, 0}, + {0x65, {0x44}, 0}, + {0x66, {0x43}, 0}, + {0x67, {0x44}, 0}, + {0x68, {0x62}, 0}, + {0x69, {0x52}, 0}, + {0x6A, {0x59}, 0}, + {0x6B, {0x4C}, 0}, + {0x6C, {0x48}, 0}, + {0x6D, {0x3A}, 0}, + {0x6E, {0x26}, 0}, + {0x6F, {0x00}, 0}, + {0x70, {0x7F}, 0}, + {0x71, {0x6A}, 0}, + {0x72, {0x5B}, 0}, + {0x73, {0x4F}, 0}, + {0x74, {0x4A}, 0}, + {0x75, {0x3D}, 0}, + {0x76, {0x41}, 0}, + {0x77, {0x2A}, 0}, + {0x78, {0x44}, 0}, + {0x79, {0x43}, 0}, + {0x7A, {0x44}, 0}, + {0x7B, {0x62}, 0}, + {0x7C, {0x52}, 0}, + {0x7D, {0x59}, 0}, + {0x7E, {0x4C}, 0}, + {0x7F, {0x48}, 0}, + {0x80, {0x3A}, 0}, + {0x81, {0x26}, 0}, + {0x82, {0x00}, 0}, + + // Select command page 2 + {0xE0, {0x02}, 0}, + {0x00, {0x42}, 0}, + {0x01, {0x42}, 0}, + {0x02, {0x40}, 0}, + {0x03, {0x40}, 0}, + {0x04, {0x5E}, 0}, + {0x05, {0x5E}, 0}, + {0x06, {0x5F}, 0}, + {0x07, {0x5F}, 0}, + {0x08, {0x5F}, 0}, + {0x09, {0x57}, 0}, + {0x0A, {0x57}, 0}, + {0x0B, {0x77}, 0}, + {0x0C, {0x77}, 0}, + {0x0D, {0x47}, 0}, + {0x0E, {0x47}, 0}, + {0x0F, {0x45}, 0}, + {0x10, {0x45}, 0}, + {0x11, {0x4B}, 0}, + {0x12, {0x4B}, 0}, + {0x13, {0x49}, 0}, + {0x14, {0x49}, 0}, + {0x15, {0x5F}, 0}, + + {0x16, {0x41}, 0}, + {0x17, {0x41}, 0}, + {0x18, {0x40}, 0}, + {0x19, {0x40}, 0}, + {0x1A, {0x5E}, 0}, + {0x1B, {0x5E}, 0}, + {0x1C, {0x5F}, 0}, + {0x1D, {0x5F}, 0}, + {0x1E, {0x5F}, 0}, + {0x1F, {0x57}, 0}, + {0x20, {0x57}, 0}, + {0x21, {0x77}, 0}, + {0x22, {0x77}, 0}, + {0x23, {0x46}, 0}, + {0x24, {0x46}, 0}, + {0x25, {0x44}, 0}, + {0x26, {0x44}, 0}, + {0x27, {0x4A}, 0}, + {0x28, {0x4A}, 0}, + {0x29, {0x48}, 0}, + {0x2A, {0x48}, 0}, + {0x2B, {0x5F}, 0}, + + {0x2C, {0x01}, 0}, + {0x2D, {0x01}, 0}, + {0x2E, {0x00}, 0}, + {0x2F, {0x00}, 0}, + {0x30, {0x1F}, 0}, + {0x31, {0x1F}, 0}, + {0x32, {0x1E}, 0}, + {0x33, {0x1E}, 0}, + {0x34, {0x1F}, 0}, + {0x35, {0x17}, 0}, + {0x36, {0x17}, 0}, + {0x37, {0x37}, 0}, + {0x38, {0x37}, 0}, + {0x39, {0x08}, 0}, + {0x3A, {0x08}, 0}, + {0x3B, {0x0A}, 0}, + {0x3C, {0x0A}, 0}, + {0x3D, {0x04}, 0}, + {0x3E, {0x04}, 0}, + {0x3F, {0x06}, 0}, + {0x40, {0x06}, 0}, + {0x41, {0x1F}, 0}, + + {0x42, {0x02}, 0}, + {0x43, {0x02}, 0}, + {0x44, {0x00}, 0}, + {0x45, {0x00}, 0}, + {0x46, {0x1F}, 0}, + {0x47, {0x1F}, 0}, + {0x48, {0x1E}, 0}, + {0x49, {0x1E}, 0}, + {0x4A, {0x1F}, 0}, + {0x4B, {0x17}, 0}, + {0x4C, {0x17}, 0}, + {0x4D, {0x37}, 0}, + {0x4E, {0x37}, 0}, + {0x4F, {0x09}, 0}, + {0x50, {0x09}, 0}, + {0x51, {0x0B}, 0}, + {0x52, {0x0B}, 0}, + {0x53, {0x05}, 0}, + {0x54, {0x05}, 0}, + {0x55, {0x07}, 0}, + {0x56, {0x07}, 0}, + {0x57, {0x1F}, 0}, + + {0x58, {0x40}, 0}, + {0x5B, {0x30}, 0}, + {0x5C, {0x00}, 0}, + {0x5D, {0x34}, 0}, + {0x5E, {0x05}, 0}, + {0x5F, {0x02}, 0}, + {0x63, {0x00}, 0}, + {0x64, {0x6A}, 0}, + {0x67, {0x73}, 0}, + {0x68, {0x07}, 0}, + {0x69, {0x08}, 0}, + {0x6A, {0x6A}, 0}, + {0x6B, {0x08}, 0}, + + {0x6C, {0x00}, 0}, + {0x6D, {0x00}, 0}, + {0x6E, {0x00}, 0}, + {0x6F, {0x88}, 0}, + + {0x75, {0xFF}, 0}, + {0x77, {0xDD}, 0}, + {0x78, {0x2C}, 0}, + {0x79, {0x15}, 0}, + {0x7A, {0x17}, 0}, + {0x7D, {0x14}, 0}, + {0x7E, {0x82}, 0}, + + // Select command page 4 + {0xE0, {0x04}, 0}, + {0x00, {0x0E}, 0}, + {0x02, {0xB3}, 0}, + {0x09, {0x61}, 0}, + {0x0E, {0x48}, 0}, + {0x37, {0x58}, 0}, + {0x2B, {0x0F}, 0}, + + // Select command page 0 (user page) + {0xE0, {0x00}, 0}, + + {0xE6, {0x02}, 0}, + {0xE7, {0x0C}, 0}, + + {static_cast(Command::sleep_out), {0x00}, 120}, // Sleep-Out (+120 ms) + + {static_cast(Command::display_on), {0x00}, 20}, // Display-On (+20 ms) + }); + + send_commands(init_commands); + return true; + } + + void set_rotation(const DisplayRotation &rotation) override { + Controller::set_rotation(rotation); + auto data = std::array{make_madctl(rotation)}; + auto page_user = std::array{PAGE_USER}; + std::scoped_lock lock(io_mutex_); + write_command(static_cast(Command::page_select), page_user, 0); + write_command(static_cast(Command::madctl), data, 0); + } + +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); + } +}; +} // namespace espp diff --git a/components/esp32-p4-eth/Kconfig.projbuild b/components/esp32-p4-eth/Kconfig.projbuild index f3706ce36..559c9c4f7 100644 --- a/components/esp32-p4-eth/Kconfig.projbuild +++ b/components/esp32-p4-eth/Kconfig.projbuild @@ -2,18 +2,30 @@ menu "ESP32-P4-ETH Configuration" choice ESP32_P4_ETH_DISPLAY prompt "MIPI-DSI display panel" - default ESP32_P4_ETH_DISPLAY_ILI9881C + default ESP32_P4_ETH_DISPLAY_JD9365_10_1 help Select the MIPI-DSI panel attached to the Waveshare ESP32-P4-ETH. The BSP does not auto-detect the panel (runtime DSI-ID probing hangs the boot watchdog on some panels), so set this to the panel you have. + config ESP32_P4_ETH_DISPLAY_JD9365_10_1 + bool "10.1\" 800x1280 (JD9365) - the panel Waveshare sells for this board" + help + The Waveshare 10.1-inch 800x1280 DSI panel, which uses a JD9365 + controller, driven by the espp::Jd9365 display driver (vendor init + over DSI/DBI + DPI panel). Reset is handled over DSI (no reset GPIO). + The backlight is driven by an on-board I2C controller (addr 0x45), so + there is no backlight GPIO; brightness() writes the controller. + config ESP32_P4_ETH_DISPLAY_ILI9881C bool "ILI9881C (10.1-inch, 800x1280)" help - The 10.1-inch 800x1280 ILI9881C panel. Reset is handled over DSI (no - reset GPIO). The backlight is driven by an on-board I2C controller, so - there is no backlight GPIO. + A 10.1-inch 800x1280 ILI9881C panel. EXPERIMENTAL on this board: the + panel Waveshare sells for this board is a JD9365, and the ILI9881C + init sequence wedges the DSI link on it (task-watchdog hang). Only + select this if you have actually attached an ILI9881C panel. Reset is + handled over DSI (no reset GPIO). The backlight is driven by an + on-board I2C controller, so there is no backlight GPIO. config ESP32_P4_ETH_DISPLAY_EK79007 bool "EK79007 (7-inch, 1024x600)" diff --git a/components/esp32-p4-eth/example/main/esp32_p4_eth_example.cpp b/components/esp32-p4-eth/example/main/esp32_p4_eth_example.cpp index 8813dbabf..0daa16277 100644 --- a/components/esp32-p4-eth/example/main/esp32_p4_eth_example.cpp +++ b/components/esp32-p4-eth/example/main/esp32_p4_eth_example.cpp @@ -139,11 +139,28 @@ extern "C" void app_main(void) { touch_y = td.y; if (td.num_touch_points > 0) { const bool new_touch = (prev_td != td); - if (new_touch && !audio_bytes.empty()) { - board.play_audio(audio_bytes); // non-blocking, touch-down edge only - } - if (new_touch && gui.draw_page_active()) { - gui.draw_circle(td.x, td.y, kCircleRadius); + const bool touch_down_edge = (prev_td.num_touch_points == 0); + // Touch feedback (click + circle) only applies on the draw/status page; + // touches on the other tabs (buttons, sliders) stay silent. + if (gui.draw_page_active()) { + // Click feedback: instant on the touch-DOWN edge, and retriggered while + // dragging - each retrigger restarts (clips) the click so drawing gives + // a stream of overlapping-feel clicks. The retrigger interval keeps a + // fast drag from restarting the click every poll (16 ms), which would + // reduce it to a buzz of its first few milliseconds. + static constexpr auto kClickRetriggerInterval = std::chrono::milliseconds(100); + static auto last_click_time = std::chrono::steady_clock::time_point{}; + const auto now = std::chrono::steady_clock::now(); + const bool click_due = + touch_down_edge || (now - last_click_time >= kClickRetriggerInterval); + if (new_touch && click_due && !audio_bytes.empty()) { + board.clear_audio(); // drop any queued tail (restart) + board.play_audio(audio_bytes); // non-blocking + last_click_time = now; + } + if (new_touch) { + gui.draw_circle(td.x, td.y, kCircleRadius); + } } } prev_td = td; @@ -330,7 +347,27 @@ static bool load_audio(size_t &out_size, size_t &out_sample_rate) { } uint32_t sample_rate = 0; std::memcpy(&sample_rate, &audio_bytes[24], sizeof(sample_rate)); - audio_bytes.erase(audio_bytes.begin(), audio_bytes.begin() + 44); + // Walk the RIFF chunks to find the 'data' chunk and keep exactly its payload. + // A fixed 44-byte strip is wrong for files with trailing metadata chunks + // (cue/LIST/bext): those bytes would be played as audio, producing a pop at + // the end of playback. + size_t data_off = 0, data_len = 0; + for (size_t off = 12; off + 8 <= audio_bytes.size();) { + uint32_t chunk_size = 0; + std::memcpy(&chunk_size, &audio_bytes[off + 4], sizeof(chunk_size)); + if (std::memcmp(&audio_bytes[off], "data", 4) == 0) { + data_off = off + 8; + data_len = std::min(chunk_size, audio_bytes.size() - data_off); + break; + } + off += 8 + chunk_size + (chunk_size & 1); // chunks are word-aligned + } + if (data_len == 0) { + audio_bytes.clear(); + return false; + } + audio_bytes.erase(audio_bytes.begin() + data_off + data_len, audio_bytes.end()); + audio_bytes.erase(audio_bytes.begin(), audio_bytes.begin() + data_off); out_size = audio_bytes.size(); out_sample_rate = sample_rate; return true; diff --git a/components/esp32-p4-eth/example/main/gui.cpp b/components/esp32-p4-eth/example/main/gui.cpp index 86dcf64e0..90842420c 100644 --- a/components/esp32-p4-eth/example/main/gui.cpp +++ b/components/esp32-p4-eth/example/main/gui.cpp @@ -30,6 +30,12 @@ void Gui::init_tabview() { status_tab_ = lv_tabview_add_tab(tabview_, "Status"); audio_tab_ = lv_tabview_add_tab(tabview_, "Audio"); camera_tab_ = lv_tabview_add_tab(tabview_, "Camera"); + // The tab pages themselves are scrollable by default; disable that so drags + // inside a page don't rubber-band/scroll the content (matches the other BSP + // example GUIs). + lv_obj_clear_flag(status_tab_, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_clear_flag(audio_tab_, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_clear_flag(camera_tab_, LV_OBJ_FLAG_SCROLLABLE); // switching tabs is done with the tab buttons only: disable swipe // scrolling of the content so drawing on the Status tab cannot accidentally // change pages @@ -266,6 +272,17 @@ void Gui::init_circle_layer() { bool Gui::update(std::mutex &m, std::condition_variable &cv) { { std::lock_guard lock(mutex_); + // drain any touch points queued since the last cycle + { + std::vector points; + { + std::lock_guard plock(pending_points_mutex_); + points.swap(pending_points_); + } + for (const auto &c : points) { + draw_circle_pending(c); + } + } lv_task_handler(); // keep the audio volume label in sync with the live BSP state, so the // first press of a volume button doesn't appear to jump from a stale @@ -380,8 +397,18 @@ void Gui::next_rotation() { } void Gui::draw_circle(int x, int y, int radius) { - std::lock_guard lock(mutex_); + // Only queue the point here; the GUI update task drains the queue under the + // LVGL mutex. Taking mutex_ directly would block the caller (the touch poll + // task) for the duration of lv_task_handler() rendering, collapsing the + // touch sample rate. + std::lock_guard lock(pending_points_mutex_); + pending_points_.push_back({.x = x, .y = y, .radius = radius, .visible = true}); +} + +void Gui::draw_circle_pending(const Circle &c) { + // caller holds mutex_ lv_obj_move_foreground(circle_layer_); + int x = c.x, y = c.y, radius = c.radius; Circle previous_circle = circles_[next_circle_index_]; circles_[next_circle_index_] = {.x = x, .y = y, .radius = radius, .visible = true}; next_circle_index_ = (next_circle_index_ + 1) % circles_.size(); diff --git a/components/esp32-p4-eth/example/main/gui.hpp b/components/esp32-p4-eth/example/main/gui.hpp index 864bbda54..f0f535ad6 100644 --- a/components/esp32-p4-eth/example/main/gui.hpp +++ b/components/esp32-p4-eth/example/main/gui.hpp @@ -75,6 +75,10 @@ class Gui { /// @param x The x coordinate (screen space) /// @param y The y coordinate (screen space) /// @param radius The radius of the circle + /// Queue a circle to draw at (x, y). Thread-safe and non-blocking: the point + /// is queued under a small lock and rendered by the GUI update task on its + /// next cycle, so callers (e.g. the touch poll task) never wait on LVGL + /// rendering. void draw_circle(int x, int y, int radius); /// Clear all circles from the screen. Thread-safe. @@ -156,6 +160,7 @@ class Gui { // unlocked implementations, called with the mutex held void clear_circles_impl(); + void draw_circle_pending(const Circle &c); // LVGL objects lv_obj_t *tabview_{nullptr}; @@ -203,6 +208,11 @@ class Gui { .task_config = {.name = "gui", .stack_size_bytes = 12 * 1024}}}; espp::Logger logger_; std::recursive_mutex mutex_; + // Pending draw_circle() points, queued by (fast) producers and drained under + // mutex_ by the GUI update task; keeps the touch poll task from blocking on + // LVGL rendering. + std::mutex pending_points_mutex_; + std::vector pending_points_; // True between init_ui() and deinit_ui(). Guards set_camera_frame() (called // from the camera task) against touching the LVGL tree after teardown. bool ui_ready_{false}; diff --git a/components/esp32-p4-eth/include/esp32-p4-eth.hpp b/components/esp32-p4-eth/include/esp32-p4-eth.hpp index 359cb962a..b606fa80a 100644 --- a/components/esp32-p4-eth/include/esp32-p4-eth.hpp +++ b/components/esp32-p4-eth/include/esp32-p4-eth.hpp @@ -32,6 +32,7 @@ #include "i2c.hpp" #include "ili9881.hpp" #include "interrupt.hpp" +#include "jd9365.hpp" #include "task.hpp" #include "touchpad_input.hpp" @@ -40,8 +41,8 @@ namespace espp { /// /// This class provides a singleton interface to the board's peripherals: /// - 10/100 Ethernet via the ESP32-P4 internal EMAC and an IP101GRI RMII PHY. -/// - MIPI-DSI display (ILI9881C 10.1" or EK79007 7", selected via Kconfig) with -/// a GT911 capacitive-touch controller. +/// - MIPI-DSI display (JD9365 10.1" by default, or ILI9881C 10.1" / EK79007 7", +/// selected via Kconfig) with a GT911 capacitive-touch controller. /// - MIPI-CSI camera (esp_video / V4L2 capture pipeline; OV5647 by default). /// - microSD / TF card over 4-bit SDMMC. /// - ES8311 audio codec (+ NS4150B amplifier) for speaker output and microphone @@ -140,7 +141,7 @@ class Esp32P4Eth : public BaseComponent { using touch_callback_t = std::function; /// Enum for the display controller type (selected via Kconfig) - enum class DisplayController { UNKNOWN, EK79007, ILI9881C }; + enum class DisplayController { UNKNOWN, EK79007, ILI9881C, JD9365 }; /// Default touch INT GPIO used by initialize_touch(). GPIO_NUM_NC means the /// GT911 is polled; if interrupt-driven touch is enabled via Kconfig this is @@ -174,6 +175,8 @@ class Esp32P4Eth : public BaseComponent { return "EK79007"; case DisplayController::ILI9881C: return "ILI9881C"; + case DisplayController::JD9365: + return "JD9365"; default: return "Unknown"; } @@ -233,8 +236,10 @@ class Esp32P4Eth : public BaseComponent { /// Set the display brightness /// \param brightness The brightness as a percentage (0-100) - /// \note The ESP32-P4-ETH has no backlight GPIO; brightness is stored but not - /// applied to hardware (see the source for details). + /// \note The ESP32-P4-ETH has no backlight GPIO. On the 10.1" JD9365 panel + /// the backlight is driven by an on-board I2C controller (addr 0x45) + /// and this call writes it; on other panels the value is stored but + /// not applied to hardware (see the source for details). void brightness(float brightness); /// Get the display brightness @@ -346,6 +351,12 @@ class Esp32P4Eth : public BaseComponent { /// \note Must be called from task context, not from an ISR. size_t play_audio(std::span data); + /// Drop any queued (not yet played) audio so a subsequent play_audio() starts + /// immediately instead of waiting behind previously queued sound. Useful for + /// UI sounds where a new event should restart the sound for maximum + /// responsiveness. + void clear_audio(); + ///////////////////////////////////////////////////////////////////////////// // Microphone ///////////////////////////////////////////////////////////////////////////// @@ -476,27 +487,36 @@ class Esp32P4Eth : public BaseComponent { size_t width; size_t height; int dpi_clock_freq_mhz; + int lane_bitrate_mbps; gpio_num_t backlight_io; gpio_num_t reset_io; int hsync_pulse_width, hsync_back_porch, hsync_front_porch; int vsync_pulse_width, vsync_back_porch, vsync_front_porch; }; // EK79007 7" 1024x600 (reset over DSI, no backlight GPIO) - static constexpr PanelParams EK79007_PARAMS{1024, 600, 52, GPIO_NUM_NC, GPIO_NUM_NC, 10, - 160, 160, 1, 23, 12}; + static constexpr PanelParams EK79007_PARAMS{1024, 600, 52, 900, GPIO_NUM_NC, GPIO_NUM_NC, + 10, 160, 160, 1, 23, 12}; // ILI9881C 10.1" 800x1280 (hsync: pulse=40, back=140, front=40; reset over DSI) - static constexpr PanelParams ILI9881C_PARAMS{800, 1280, 80, GPIO_NUM_NC, GPIO_NUM_NC, 40, - 140, 40, 4, 16, 16}; + static constexpr PanelParams ILI9881C_PARAMS{800, 1280, 80, 1500, GPIO_NUM_NC, GPIO_NUM_NC, + 40, 140, 40, 4, 16, 16}; + // JD9365 10.1" 800x1280 (the panel Waveshare sells for this board; reset over + // DSI, no backlight GPIO). Timing matches Waveshare's + // JD9365_800_1280_PANEL_60HZ_DPI_CONFIG vendor timing. + static constexpr PanelParams JD9365_PARAMS{800, 1280, 80, 1500, GPIO_NUM_NC, GPIO_NUM_NC, + 20, 20, 40, 4, 10, 30}; #if CONFIG_ESP32_P4_ETH_DISPLAY_EK79007 static constexpr DisplayController default_controller_ = DisplayController::EK79007; -#else +#elif CONFIG_ESP32_P4_ETH_DISPLAY_ILI9881C static constexpr DisplayController default_controller_ = DisplayController::ILI9881C; +#else + static constexpr DisplayController default_controller_ = DisplayController::JD9365; #endif // Runtime display geometry, set from the configured panel. - PanelParams panel_params_{default_controller_ == DisplayController::ILI9881C ? ILI9881C_PARAMS - : EK79007_PARAMS}; + PanelParams panel_params_{default_controller_ == DisplayController::ILI9881C ? ILI9881C_PARAMS + : default_controller_ == DisplayController::EK79007 ? EK79007_PARAMS + : JD9365_PARAMS}; size_t display_width_{panel_params_.width}; size_t display_height_{panel_params_.height}; @@ -508,7 +528,6 @@ class Esp32P4Eth : public BaseComponent { static constexpr int mipi_dsi_lanes = 2; // DSI HS lane bit rate (Mbps/lane). 900 Mbps matches Espressif's official // panel bus configs; using the wrong rate mis-packs the pixel bits on the link. - static constexpr int mipi_dsi_lane_bitrate_mbps = 900; static constexpr int mipi_dsi_phy_ldo_channel = 3; // on-chip LDO_VO3 -> VDD_MIPI_DPHY static constexpr int mipi_dsi_phy_ldo_voltage_mv = 2500; @@ -521,8 +540,8 @@ class Esp32P4Eth : public BaseComponent { static constexpr bool swap_xy = false; // touch -> display coordinate conversion. May need tuning per panel. static constexpr bool touch_swap_xy = false; - static constexpr bool touch_invert_x = true; - static constexpr bool touch_invert_y = true; + static constexpr bool touch_invert_x = false; + static constexpr bool touch_invert_y = false; // Touch (GT911) - interrupt/reset are NOT connected on this board static constexpr uint8_t gt911_default_address = 0x5D; @@ -638,9 +657,14 @@ class Esp32P4Eth : public BaseComponent { ///////////////////////////////////////////////////////////////////////////// // Display state (MIPI-DSI). NOTE: there is no backlight GPIO / espp::Led on - // this board; the backlight is driven by an on-board I2C controller, so the - // stored brightness is best-effort only (see src/video.cpp). + // this board; the backlight is driven by an on-board I2C controller. On the + // 10.1" JD9365 panel brightness() writes that controller (addr 0x45, reg + // 0x96); on other panels the stored brightness is best-effort only (see + // src/video.cpp). ///////////////////////////////////////////////////////////////////////////// + // On-board I2C backlight controller (10.1" JD9365 panel) + static constexpr uint8_t backlight_i2c_address = 0x45; + std::shared_ptr> backlight_i2c_device_; std::atomic brightness_{100.0f}; std::shared_ptr> display_; std::shared_ptr display_driver_{static_cast(nullptr)}; diff --git a/components/esp32-p4-eth/src/audio.cpp b/components/esp32-p4-eth/src/audio.cpp index 8825aaa35..1669e1258 100644 --- a/components/esp32-p4-eth/src/audio.cpp +++ b/components/esp32-p4-eth/src/audio.cpp @@ -171,6 +171,16 @@ size_t Esp32P4Eth::play_audio(const uint8_t *data, uint32_t num_bytes) { return xStreamBufferSend(audio_tx_stream, data, sendable, 0); } +void Esp32P4Eth::clear_audio() { + if (!audio_initialized_ || audio_tx_stream == nullptr) { + return; + } + // Drop everything queued but not yet handed to the I2S DMA. The drain task's + // current (at most one) frame still finishes, so this cuts over on the next + // ~16 ms frame boundary. + xStreamBufferReset(audio_tx_stream); +} + size_t Esp32P4Eth::play_audio(std::span data) { return play_audio(data.data(), data.size()); } @@ -301,7 +311,12 @@ bool Esp32P4Eth::audio_task_callback(std::mutex &m, std::condition_variable &cv, i2s_channel_write(audio_tx_handle, tx_buf, buffer_size, NULL, pdMS_TO_TICKS(100)); } else { xStreamBufferReceive(audio_tx_stream, tx_buf, available, 0); - i2s_channel_write(audio_tx_handle, tx_buf, available, NULL, pdMS_TO_TICKS(100)); + // Always write a full, frame-aligned buffer (queued samples zero-padded to + // buffer_size) so the I2S DMA is fed at a constant cadence - matching the + // esp-box / t-deck / m5stack-tab5 playback path. Writing only `available` + // bytes makes the drain cadence variable and interleaves whole frames of + // silence into bursty streams, which sounds choppy/glitchy. + i2s_channel_write(audio_tx_handle, tx_buf, buffer_size, NULL, pdMS_TO_TICKS(100)); } // honor a stop request per the Task contract: check/clear notified under m std::unique_lock lock(m); diff --git a/components/esp32-p4-eth/src/camera.cpp b/components/esp32-p4-eth/src/camera.cpp index a68271c2f..12f59ff70 100644 --- a/components/esp32-p4-eth/src/camera.cpp +++ b/components/esp32-p4-eth/src/camera.cpp @@ -51,12 +51,11 @@ bool Esp32P4Eth::initialize_camera(const camera_frame_callback_t &callback, esp_video_init_csi_config_t csi_config = {}; csi_config.sccb_config.init_sccb = false; csi_config.sccb_config.i2c_handle = internal_i2c_.native_bus_handle(); - // Run the sensor SCCB at 400 kHz rather than 100 kHz. The SCCB shares the - // internal I2C bus with the touch controller and audio codec, and the ISP's - // auto-exposure writes the sensor over SCCB every frame; at 100 kHz each of - // those writes holds the shared bus ~4x longer than needed. 400 kHz is a safe - // SCCB speed for this sensor and matches the bus's configured clock. - csi_config.sccb_config.freq = 400000; + // Run the sensor SCCB at 100 kHz, matching Waveshare's own camera demos for + // these boards (their Kconfig floor is 100 kHz). Some OV sensors are + // unreliable at higher SCCB rates during probe, and esp_video applies this + // freq to the SCCB device even when reusing an external I2C bus handle. + csi_config.sccb_config.freq = 100000; csi_config.reset_pin = GPIO_NUM_NC; csi_config.pwdn_pin = GPIO_NUM_NC; csi_config.dont_init_ldo = false; diff --git a/components/esp32-p4-eth/src/video.cpp b/components/esp32-p4-eth/src/video.cpp index 569b986f3..796e0de30 100644 --- a/components/esp32-p4-eth/src/video.cpp +++ b/components/esp32-p4-eth/src/video.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -28,6 +29,9 @@ namespace espp { bool Esp32P4Eth::initialize_lcd() { logger_.info("Initializing LCD (MIPI-DSI)"); + // Select the panel params first: the DSI lane bit rate is per-panel. + apply_panel_params(default_controller_); + esp_err_t ret = ESP_OK; // Enable the MIPI DSI PHY power LDO (on-chip LDO_VO3 -> VDD_MIPI_DPHY, @@ -48,15 +52,56 @@ bool Esp32P4Eth::initialize_lcd() { // the panel is reset over DSI during its vendor init sequence, so there is no // hardware GPIO reset step here (unlike the ESP32-P4-Function-EV-Board). + // The 10.1" JD9365 panel is powered/reset and backlit by an on-board I2C + // controller (addr 0x45) on the BSP's internal I2C bus: register 0x95 is the + // panel power/reset control and 0x96 the backlight level (0-255) on this + // panel's controller. This power-on sequence (values and delays) matches + // Waveshare's vendor panel component and must run before any DSI traffic + // so the panel is powered and out of reset when the vendor init sequence is + // sent. + if (display_controller_ == DisplayController::JD9365) { + // Lazily create the backlight/panel-power I2C device on the internal bus + // (shared with brightness()). + if (!backlight_i2c_device_) { + std::error_code ec; + backlight_i2c_device_ = internal_i2c_.add_device( + { + .device_address = backlight_i2c_address, + .timeout_ms = static_cast(internal_i2c_.config().timeout_ms), + .scl_speed_hz = internal_i2c_.config().clk_speed, + .log_level = espp::Logger::Verbosity::WARN, + }, + ec); + if (!backlight_i2c_device_) { + logger_.error("Could not initialize panel power/backlight I2C device (0x{:02X}): {}", + backlight_i2c_address, ec.message()); + return false; + } + } + auto write_panel_reg = [this](uint8_t reg, uint8_t value) { + const uint8_t data[2] = {reg, value}; + std::error_code ec; + if (!backlight_i2c_device_->write(data, sizeof(data), ec)) { + logger_.error("Failed to write panel power controller reg 0x{:02X}: {}", reg, ec.message()); + } + }; + write_panel_reg(0x95, 0x11); // panel power/reset control + write_panel_reg(0x95, 0x17); // panel power/reset control + write_panel_reg(0x96, 0x00); // backlight off while powering up + std::this_thread::sleep_for(100ms); + write_panel_reg(0x96, 0xFF); // backlight full on + std::this_thread::sleep_for(1000ms); + } + // Create the MIPI DSI bus (also initializes the DSI PHY) if (lcd_handles_.mipi_dsi_bus == nullptr) { logger_.info("Creating MIPI DSI bus ({} lanes, {} Mbps/lane)", mipi_dsi_lanes, - mipi_dsi_lane_bitrate_mbps); + panel_params_.lane_bitrate_mbps); esp_lcd_dsi_bus_config_t bus_config = {}; bus_config.bus_id = 0; bus_config.num_data_lanes = mipi_dsi_lanes; bus_config.phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT; - bus_config.lane_bit_rate_mbps = mipi_dsi_lane_bitrate_mbps; + bus_config.lane_bit_rate_mbps = panel_params_.lane_bitrate_mbps; ret = esp_lcd_new_dsi_bus(&bus_config, &lcd_handles_.mipi_dsi_bus); if (ret != ESP_OK) { logger_.error("New DSI bus init failed: {}", esp_err_to_name(ret)); @@ -80,59 +125,27 @@ bool Esp32P4Eth::initialize_lcd() { // Select the panel (Kconfig-driven) and apply its parameters (geometry, DPI // timing). - apply_panel_params(default_controller_); logger_.info("Using display panel: {} ({}x{})", get_display_controller_name(), display_width_, display_height_); // NOTE: The ESP32-P4-ETH has no backlight GPIO. The backlight is driven by an - // on-board I2C controller (addr ~0x45/0x95, chip/protocol not yet identified); - // brightness control is a TODO pending hardware identification. The panel powers - // up with the backlight on, so no espp::Led / PWM backlight is instantiated here - // and the display fully initializes and shows pixels without any backlight code. + // on-board I2C controller (addr 0x45). On the 10.1" JD9365 panel brightness() + // writes that controller (reg 0x96); on the other panels the panel powers up + // with the backlight on and brightness() only stores the value. No espp::Led / + // PWM backlight is instantiated here. brightness(100.0f); - // Create the DPI (video) panel with the configured panel's timing. - if (lcd_handles_.panel == nullptr) { - esp_lcd_dpi_panel_config_t dpi_cfg{}; - memset(&dpi_cfg, 0, sizeof(dpi_cfg)); - dpi_cfg.virtual_channel = 0; - dpi_cfg.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; - dpi_cfg.dpi_clock_freq_mhz = panel_params_.dpi_clock_freq_mhz; -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) - dpi_cfg.in_color_format = LCD_COLOR_FMT_RGB565; - dpi_cfg.out_color_format = LCD_COLOR_FMT_RGB565; -#else - dpi_cfg.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; - dpi_cfg.flags.use_dma2d = true; -#endif - dpi_cfg.num_fbs = 1; - dpi_cfg.video_timing.h_size = display_width_; - dpi_cfg.video_timing.v_size = display_height_; - dpi_cfg.video_timing.hsync_pulse_width = panel_params_.hsync_pulse_width; - dpi_cfg.video_timing.hsync_back_porch = panel_params_.hsync_back_porch; - dpi_cfg.video_timing.hsync_front_porch = panel_params_.hsync_front_porch; - dpi_cfg.video_timing.vsync_pulse_width = panel_params_.vsync_pulse_width; - dpi_cfg.video_timing.vsync_back_porch = panel_params_.vsync_back_porch; - dpi_cfg.video_timing.vsync_front_porch = panel_params_.vsync_front_porch; - logger_.info("Creating DPI panel ({}x{} @ {} MHz)", dpi_cfg.video_timing.h_size, - dpi_cfg.video_timing.v_size, dpi_cfg.dpi_clock_freq_mhz); - ret = esp_lcd_new_panel_dpi(lcd_handles_.mipi_dsi_bus, &dpi_cfg, &lcd_handles_.panel); - if (ret != ESP_OK) { - logger_.error("Failed to create MIPI DSI DPI panel: {}", esp_err_to_name(ret)); - return false; - } - // NOTE: deliberately do NOT enable DMA2D for the DPI panel. DMA2D is a - // color-processing engine, not a plain copy: routing the LVGL flush - // (esp_lcd_panel_draw_bitmap) through it corrupts the RGB565 channel order, - // while the plain CPU copy path renders correctly. - } - - // Send the panel controller's vendor init sequence over DBI (command mode), - // before starting the DPI video stream. + // espp-driver path (JD9365 / ILI9881C / EK79007): send the panel + // controller's vendor init sequence over DBI (command mode), before starting + // the DPI video stream. espp::display_drivers::Config display_config{ .panel_io = nullptr, .write_command = std::bind_front(&Esp32P4Eth::dsi_write_command, this), - .read_command = std::bind_front(&Esp32P4Eth::dsi_read_command, this), + // NOTE: the Waveshare ESP32-P4 panels do not reliably support MIPI-DSI DCS + // reads (bus turn-around); the ESP-IDF HAL busy-waits on the read, which + // hangs panel init and trips the task watchdog. Do not provide a + // read_command so the driver skips the optional panel-ID read. + .read_command = nullptr, .lcd_send_lines = nullptr, .reset_pin = GPIO_NUM_NC, .data_command_pin = GPIO_NUM_NC, @@ -148,7 +161,15 @@ bool Esp32P4Eth::initialize_lcd() { }; display_driver_.reset(); - if (display_controller_ == DisplayController::ILI9881C) { + if (display_controller_ == DisplayController::JD9365) { + // The Waveshare 10.1" 800x1280 panel is a JD9365. espp::Jd9365 performs + // the DCS software reset and sends the vendor init sequence (taken from + // Waveshare's vendor panel component; see jd9365.hpp) over the DBI IO. + auto driver = std::make_shared(display_config); + if (driver->initialize()) { + display_driver_ = std::move(driver); + } + } else if (display_controller_ == DisplayController::ILI9881C) { auto driver = std::make_shared(display_config); if (driver->initialize()) { display_driver_ = std::move(driver); @@ -164,6 +185,58 @@ bool Esp32P4Eth::initialize_lcd() { return false; } + // Create the DPI (video) panel with the configured panel's timing. This must + // come AFTER the vendor init sequence above: esp_lcd_new_panel_dpi() starts the + // HS video stream, and once it is running the DSI cannot drain the low-power + // command FIFO, so a long init sequence (e.g. ILI9881C's 202 commands) would + // overflow it and hang. + if (lcd_handles_.panel == nullptr) { + esp_lcd_dpi_panel_config_t dpi_cfg{}; + memset(&dpi_cfg, 0, sizeof(dpi_cfg)); + dpi_cfg.virtual_channel = 0; + dpi_cfg.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; + dpi_cfg.dpi_clock_freq_mhz = panel_params_.dpi_clock_freq_mhz; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + dpi_cfg.in_color_format = LCD_COLOR_FMT_RGB565; + dpi_cfg.out_color_format = LCD_COLOR_FMT_RGB565; +#else + dpi_cfg.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; + dpi_cfg.flags.use_dma2d = true; +#endif + dpi_cfg.num_fbs = 1; + dpi_cfg.video_timing.h_size = display_width_; + dpi_cfg.video_timing.v_size = display_height_; + dpi_cfg.video_timing.hsync_pulse_width = panel_params_.hsync_pulse_width; + dpi_cfg.video_timing.hsync_back_porch = panel_params_.hsync_back_porch; + dpi_cfg.video_timing.hsync_front_porch = panel_params_.hsync_front_porch; + dpi_cfg.video_timing.vsync_pulse_width = panel_params_.vsync_pulse_width; + dpi_cfg.video_timing.vsync_back_porch = panel_params_.vsync_back_porch; + dpi_cfg.video_timing.vsync_front_porch = panel_params_.vsync_front_porch; + logger_.info("Creating DPI panel ({}x{} @ {} MHz)", dpi_cfg.video_timing.h_size, + dpi_cfg.video_timing.v_size, dpi_cfg.dpi_clock_freq_mhz); + ret = esp_lcd_new_panel_dpi(lcd_handles_.mipi_dsi_bus, &dpi_cfg, &lcd_handles_.panel); + if (ret != ESP_OK) { + logger_.error("Failed to create MIPI DSI DPI panel: {}", esp_err_to_name(ret)); + return false; + } + // NOTE: for the ILI9881C / EK79007 panels we deliberately do NOT enable + // DMA2D for the DPI panel. DMA2D is a color-processing engine, not a plain + // copy: routing the LVGL flush (esp_lcd_panel_draw_bitmap) through it + // corrupts the RGB565 channel order on those panels, while the plain CPU + // copy path renders correctly. The JD9365 panel renders correctly WITH + // DMA2D (and Waveshare's vendor panel component enables it), so keep it + // enabled on that path. +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + if (display_controller_ == DisplayController::JD9365) { + ret = esp_lcd_dpi_panel_enable_dma2d(lcd_handles_.panel); + if (ret != ESP_OK) { + logger_.error("Failed to enable DMA2D for the DPI panel: {}", esp_err_to_name(ret)); + return false; + } + } +#endif + } + // Low-level panel init (starts the DPI video stream) ret = lcd_handles_.panel->init(lcd_handles_.panel); if (ret != ESP_OK) { @@ -193,8 +266,17 @@ bool Esp32P4Eth::initialize_lcd() { void Esp32P4Eth::apply_panel_params(DisplayController controller) { display_controller_ = (controller == DisplayController::UNKNOWN) ? default_controller_ : controller; - panel_params_ = - (display_controller_ == DisplayController::ILI9881C) ? ILI9881C_PARAMS : EK79007_PARAMS; + switch (display_controller_) { + case DisplayController::JD9365: + panel_params_ = JD9365_PARAMS; + break; + case DisplayController::ILI9881C: + panel_params_ = ILI9881C_PARAMS; + break; + default: + panel_params_ = EK79007_PARAMS; + break; + } display_width_ = panel_params_.width; display_height_ = panel_params_.height; } @@ -287,15 +369,44 @@ void Esp32P4Eth::write_lcd_lines(int xs, int ys, int xe, int ye, const uint8_t * void Esp32P4Eth::brightness(float brightness) { // The ESP32-P4-ETH has NO backlight GPIO. The backlight is driven by an - // on-board I2C controller (addr ~0x45/0x95, chip/protocol not yet identified); - // brightness control is a TODO pending hardware identification. The panel powers - // up with the backlight on. For now this is best-effort: store the requested - // value so brightness() reads back what was set, and log it. + // on-board I2C controller at address 0x45. On the 10.1" JD9365 panel the + // brightness register is 0x96 (value 0-255); Waveshare's own BSP writes 0x86 + // instead for some panel revisions. For panels other than the JD9365 the + // chip/protocol has not been verified, so this remains best-effort: store + // the requested value so brightness() reads back what was set, and log it. brightness = std::clamp(brightness, 0.0f, 100.0f); brightness_ = brightness; - logger_.debug("brightness({}) requested; no backlight GPIO on this board (on-board I2C " - "controller not yet identified), value stored only", - brightness); + if (display_controller_ != DisplayController::JD9365) { + logger_.debug("brightness({}) requested; no backlight GPIO on this board (on-board I2C " + "backlight protocol not verified for this panel), value stored only", + brightness); + return; + } + // Lazily create the backlight I2C device on the internal bus. + if (!backlight_i2c_device_) { + std::error_code ec; + backlight_i2c_device_ = internal_i2c_.add_device( + { + .device_address = backlight_i2c_address, + .timeout_ms = static_cast(internal_i2c_.config().timeout_ms), + .scl_speed_hz = internal_i2c_.config().clk_speed, + .log_level = espp::Logger::Verbosity::WARN, + }, + ec); + if (!backlight_i2c_device_) { + logger_.error("Could not initialize backlight I2C device (0x{:02X}): {}", + backlight_i2c_address, ec.message()); + return; + } + } + // Register 0x96 is the brightness register (0-255) on the 10.1" JD9365 + // panel's power/backlight controller (Waveshare's BSP uses 0x86 for some + // panel revisions). + const uint8_t data[2] = {0x96, static_cast(255.0f * brightness / 100.0f)}; + std::error_code ec; + if (!backlight_i2c_device_->write(data, sizeof(data), ec)) { + logger_.error("Failed to write backlight brightness: {}", ec.message()); + } } float Esp32P4Eth::brightness() const { return brightness_.load(); } diff --git a/components/esp32-p4-function-ev-board/src/audio.cpp b/components/esp32-p4-function-ev-board/src/audio.cpp index fa1832704..511082d4f 100644 --- a/components/esp32-p4-function-ev-board/src/audio.cpp +++ b/components/esp32-p4-function-ev-board/src/audio.cpp @@ -273,7 +273,12 @@ bool Esp32P4FunctionEvBoard::audio_task_callback(std::mutex &m, std::condition_v i2s_channel_write(audio_tx_handle, tx_buf, buffer_size, NULL, portMAX_DELAY); } else { xStreamBufferReceive(audio_tx_stream, tx_buf, available, 0); - i2s_channel_write(audio_tx_handle, tx_buf, available, NULL, portMAX_DELAY); + // Always write a full, frame-aligned buffer (queued samples zero-padded to + // buffer_size) so the I2S DMA is fed at a constant cadence - matching the + // esp-box / t-deck / m5stack-tab5 playback path. Writing only `available` + // bytes makes the drain cadence variable and interleaves whole frames of + // silence into bursty streams, which sounds choppy/glitchy. + i2s_channel_write(audio_tx_handle, tx_buf, buffer_size, NULL, portMAX_DELAY); } return false; } diff --git a/components/esp32-p4-nano/Kconfig.projbuild b/components/esp32-p4-nano/Kconfig.projbuild index 1d896f437..ae60e3381 100644 --- a/components/esp32-p4-nano/Kconfig.projbuild +++ b/components/esp32-p4-nano/Kconfig.projbuild @@ -2,18 +2,30 @@ menu "ESP32-P4-NANO Configuration" choice ESP32_P4_NANO_DISPLAY prompt "MIPI-DSI display panel" - default ESP32_P4_NANO_DISPLAY_ILI9881C + default ESP32_P4_NANO_DISPLAY_JD9365_10_1 help Select the MIPI-DSI panel attached to the Waveshare ESP32-P4-NANO. The BSP does not auto-detect the panel (runtime DSI-ID probing hangs the boot watchdog on some panels), so set this to the panel you have. + config ESP32_P4_NANO_DISPLAY_JD9365_10_1 + bool "10.1\" 800x1280 (JD9365) - the panel Waveshare sells for this board" + help + The Waveshare 10.1-inch 800x1280 DSI panel, which uses a JD9365 + controller, driven by the espp::Jd9365 display driver (vendor init + over DSI/DBI + DPI panel). Reset is handled over DSI (no reset GPIO). + The backlight is driven by an on-board I2C controller (addr 0x45), so + there is no backlight GPIO; brightness() writes the controller. + config ESP32_P4_NANO_DISPLAY_ILI9881C bool "ILI9881C (10.1-inch, 800x1280)" help - The 10.1-inch 800x1280 ILI9881C panel. Reset is handled over DSI (no - reset GPIO). The backlight is driven by an on-board I2C controller, so - there is no backlight GPIO. + A 10.1-inch 800x1280 ILI9881C panel. EXPERIMENTAL on this board: the + panel Waveshare sells for this board is a JD9365, and the ILI9881C + init sequence wedges the DSI link on it (task-watchdog hang). Only + select this if you have actually attached an ILI9881C panel. Reset is + handled over DSI (no reset GPIO). The backlight is driven by an + on-board I2C controller, so there is no backlight GPIO. config ESP32_P4_NANO_DISPLAY_EK79007 bool "EK79007 (7-inch, 1024x600)" diff --git a/components/esp32-p4-nano/example/main/esp32_p4_nano_example.cpp b/components/esp32-p4-nano/example/main/esp32_p4_nano_example.cpp index b29eeaf3e..eb1de2e5e 100644 --- a/components/esp32-p4-nano/example/main/esp32_p4_nano_example.cpp +++ b/components/esp32-p4-nano/example/main/esp32_p4_nano_example.cpp @@ -139,11 +139,28 @@ extern "C" void app_main(void) { touch_y = td.y; if (td.num_touch_points > 0) { const bool new_touch = (prev_td != td); - if (new_touch && !audio_bytes.empty()) { - board.play_audio(audio_bytes); // non-blocking, touch-down edge only - } - if (new_touch && gui.draw_page_active()) { - gui.draw_circle(td.x, td.y, kCircleRadius); + const bool touch_down_edge = (prev_td.num_touch_points == 0); + // Touch feedback (click + circle) only applies on the draw/status page; + // touches on the other tabs (buttons, sliders) stay silent. + if (gui.draw_page_active()) { + // Click feedback: instant on the touch-DOWN edge, and retriggered while + // dragging - each retrigger restarts (clips) the click so drawing gives + // a stream of overlapping-feel clicks. The retrigger interval keeps a + // fast drag from restarting the click every poll (16 ms), which would + // reduce it to a buzz of its first few milliseconds. + static constexpr auto kClickRetriggerInterval = std::chrono::milliseconds(100); + static auto last_click_time = std::chrono::steady_clock::time_point{}; + const auto now = std::chrono::steady_clock::now(); + const bool click_due = + touch_down_edge || (now - last_click_time >= kClickRetriggerInterval); + if (new_touch && click_due && !audio_bytes.empty()) { + board.clear_audio(); // drop any queued tail (restart) + board.play_audio(audio_bytes); // non-blocking + last_click_time = now; + } + if (new_touch) { + gui.draw_circle(td.x, td.y, kCircleRadius); + } } } prev_td = td; @@ -330,7 +347,27 @@ static bool load_audio(size_t &out_size, size_t &out_sample_rate) { } uint32_t sample_rate = 0; std::memcpy(&sample_rate, &audio_bytes[24], sizeof(sample_rate)); - audio_bytes.erase(audio_bytes.begin(), audio_bytes.begin() + 44); + // Walk the RIFF chunks to find the 'data' chunk and keep exactly its payload. + // A fixed 44-byte strip is wrong for files with trailing metadata chunks + // (cue/LIST/bext): those bytes would be played as audio, producing a pop at + // the end of playback. + size_t data_off = 0, data_len = 0; + for (size_t off = 12; off + 8 <= audio_bytes.size();) { + uint32_t chunk_size = 0; + std::memcpy(&chunk_size, &audio_bytes[off + 4], sizeof(chunk_size)); + if (std::memcmp(&audio_bytes[off], "data", 4) == 0) { + data_off = off + 8; + data_len = std::min(chunk_size, audio_bytes.size() - data_off); + break; + } + off += 8 + chunk_size + (chunk_size & 1); // chunks are word-aligned + } + if (data_len == 0) { + audio_bytes.clear(); + return false; + } + audio_bytes.erase(audio_bytes.begin() + data_off + data_len, audio_bytes.end()); + audio_bytes.erase(audio_bytes.begin(), audio_bytes.begin() + data_off); out_size = audio_bytes.size(); out_sample_rate = sample_rate; return true; diff --git a/components/esp32-p4-nano/example/main/gui.cpp b/components/esp32-p4-nano/example/main/gui.cpp index 57fc9ebea..3a4a50021 100644 --- a/components/esp32-p4-nano/example/main/gui.cpp +++ b/components/esp32-p4-nano/example/main/gui.cpp @@ -30,6 +30,12 @@ void Gui::init_tabview() { status_tab_ = lv_tabview_add_tab(tabview_, "Status"); audio_tab_ = lv_tabview_add_tab(tabview_, "Audio"); camera_tab_ = lv_tabview_add_tab(tabview_, "Camera"); + // The tab pages themselves are scrollable by default; disable that so drags + // inside a page don't rubber-band/scroll the content (matches the other BSP + // example GUIs). + lv_obj_clear_flag(status_tab_, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_clear_flag(audio_tab_, LV_OBJ_FLAG_SCROLLABLE); + lv_obj_clear_flag(camera_tab_, LV_OBJ_FLAG_SCROLLABLE); // switching tabs is done with the tab buttons only: disable swipe // scrolling of the content so drawing on the Status tab cannot accidentally // change pages @@ -266,6 +272,17 @@ void Gui::init_circle_layer() { bool Gui::update(std::mutex &m, std::condition_variable &cv) { { std::lock_guard lock(mutex_); + // drain any touch points queued since the last cycle + { + std::vector points; + { + std::lock_guard plock(pending_points_mutex_); + points.swap(pending_points_); + } + for (const auto &c : points) { + draw_circle_pending(c); + } + } lv_task_handler(); // keep the audio volume label in sync with the live BSP state, so the // first press of a volume button doesn't appear to jump from a stale @@ -380,8 +397,18 @@ void Gui::next_rotation() { } void Gui::draw_circle(int x, int y, int radius) { - std::lock_guard lock(mutex_); + // Only queue the point here; the GUI update task drains the queue under the + // LVGL mutex. Taking mutex_ directly would block the caller (the touch poll + // task) for the duration of lv_task_handler() rendering, collapsing the + // touch sample rate. + std::lock_guard lock(pending_points_mutex_); + pending_points_.push_back({.x = x, .y = y, .radius = radius, .visible = true}); +} + +void Gui::draw_circle_pending(const Circle &c) { + // caller holds mutex_ lv_obj_move_foreground(circle_layer_); + int x = c.x, y = c.y, radius = c.radius; Circle previous_circle = circles_[next_circle_index_]; circles_[next_circle_index_] = {.x = x, .y = y, .radius = radius, .visible = true}; next_circle_index_ = (next_circle_index_ + 1) % circles_.size(); diff --git a/components/esp32-p4-nano/example/main/gui.hpp b/components/esp32-p4-nano/example/main/gui.hpp index 36944c786..d90d23eae 100644 --- a/components/esp32-p4-nano/example/main/gui.hpp +++ b/components/esp32-p4-nano/example/main/gui.hpp @@ -75,6 +75,10 @@ class Gui { /// @param x The x coordinate (screen space) /// @param y The y coordinate (screen space) /// @param radius The radius of the circle + /// Queue a circle to draw at (x, y). Thread-safe and non-blocking: the point + /// is queued under a small lock and rendered by the GUI update task on its + /// next cycle, so callers (e.g. the touch poll task) never wait on LVGL + /// rendering. void draw_circle(int x, int y, int radius); /// Clear all circles from the screen. Thread-safe. @@ -156,6 +160,7 @@ class Gui { // unlocked implementations, called with the mutex held void clear_circles_impl(); + void draw_circle_pending(const Circle &c); // LVGL objects lv_obj_t *tabview_{nullptr}; @@ -203,6 +208,11 @@ class Gui { .task_config = {.name = "gui", .stack_size_bytes = 12 * 1024}}}; espp::Logger logger_; std::recursive_mutex mutex_; + // Pending draw_circle() points, queued by (fast) producers and drained under + // mutex_ by the GUI update task; keeps the touch poll task from blocking on + // LVGL rendering. + std::mutex pending_points_mutex_; + std::vector pending_points_; // True between init_ui() and deinit_ui(). Guards set_camera_frame() (called // from the camera task) against touching the LVGL tree after teardown. bool ui_ready_{false}; diff --git a/components/esp32-p4-nano/include/esp32-p4-nano.hpp b/components/esp32-p4-nano/include/esp32-p4-nano.hpp index 875948d36..ca03c084f 100644 --- a/components/esp32-p4-nano/include/esp32-p4-nano.hpp +++ b/components/esp32-p4-nano/include/esp32-p4-nano.hpp @@ -32,6 +32,7 @@ #include "i2c.hpp" #include "ili9881.hpp" #include "interrupt.hpp" +#include "jd9365.hpp" #include "task.hpp" #include "touchpad_input.hpp" @@ -40,8 +41,8 @@ namespace espp { /// /// This class provides a singleton interface to the board's peripherals: /// - 10/100 Ethernet via the ESP32-P4 internal EMAC and an IP101GRI RMII PHY. -/// - MIPI-DSI display (ILI9881C 10.1" or EK79007 7", selected via Kconfig) with -/// a GT911 capacitive-touch controller. +/// - MIPI-DSI display (JD9365 10.1" by default, or ILI9881C 10.1" / EK79007 7", +/// selected via Kconfig) with a GT911 capacitive-touch controller. /// - MIPI-CSI camera (esp_video / V4L2 capture pipeline; OV5647 by default). /// - microSD / TF card over 4-bit SDMMC. /// - ES8311 audio codec (+ NS4150B amplifier) for speaker output and microphone @@ -141,7 +142,7 @@ class Esp32P4Nano : public BaseComponent { using touch_callback_t = std::function; /// Enum for the display controller type (selected via Kconfig) - enum class DisplayController { UNKNOWN, EK79007, ILI9881C }; + enum class DisplayController { UNKNOWN, EK79007, ILI9881C, JD9365 }; /// Default touch INT GPIO used by initialize_touch(). GPIO_NUM_NC means the /// GT911 is polled; if interrupt-driven touch is enabled via Kconfig this is @@ -175,6 +176,8 @@ class Esp32P4Nano : public BaseComponent { return "EK79007"; case DisplayController::ILI9881C: return "ILI9881C"; + case DisplayController::JD9365: + return "JD9365"; default: return "Unknown"; } @@ -234,8 +237,10 @@ class Esp32P4Nano : public BaseComponent { /// Set the display brightness /// \param brightness The brightness as a percentage (0-100) - /// \note The ESP32-P4-NANO has no backlight GPIO; brightness is stored but not - /// applied to hardware (see the source for details). + /// \note The ESP32-P4-NANO has no backlight GPIO. On the 10.1" JD9365 panel + /// the backlight is driven by an on-board I2C controller (addr 0x45) + /// and this call writes it; on other panels the value is stored but + /// not applied to hardware (see the source for details). void brightness(float brightness); /// Get the display brightness @@ -347,6 +352,12 @@ class Esp32P4Nano : public BaseComponent { /// \note Must be called from task context, not from an ISR. size_t play_audio(std::span data); + /// Drop any queued (not yet played) audio so a subsequent play_audio() starts + /// immediately instead of waiting behind previously queued sound. Useful for + /// UI sounds where a new event should restart the sound for maximum + /// responsiveness. + void clear_audio(); + ///////////////////////////////////////////////////////////////////////////// // Microphone ///////////////////////////////////////////////////////////////////////////// @@ -477,27 +488,36 @@ class Esp32P4Nano : public BaseComponent { size_t width; size_t height; int dpi_clock_freq_mhz; + int lane_bitrate_mbps; gpio_num_t backlight_io; gpio_num_t reset_io; int hsync_pulse_width, hsync_back_porch, hsync_front_porch; int vsync_pulse_width, vsync_back_porch, vsync_front_porch; }; // EK79007 7" 1024x600 (reset over DSI, no backlight GPIO) - static constexpr PanelParams EK79007_PARAMS{1024, 600, 52, GPIO_NUM_NC, GPIO_NUM_NC, 10, - 160, 160, 1, 23, 12}; + static constexpr PanelParams EK79007_PARAMS{1024, 600, 52, 900, GPIO_NUM_NC, GPIO_NUM_NC, + 10, 160, 160, 1, 23, 12}; // ILI9881C 10.1" 800x1280 (hsync: pulse=40, back=140, front=40; reset over DSI) - static constexpr PanelParams ILI9881C_PARAMS{800, 1280, 80, GPIO_NUM_NC, GPIO_NUM_NC, 40, - 140, 40, 4, 16, 16}; + static constexpr PanelParams ILI9881C_PARAMS{800, 1280, 80, 1500, GPIO_NUM_NC, GPIO_NUM_NC, + 40, 140, 40, 4, 16, 16}; + // JD9365 10.1" 800x1280 (the panel Waveshare sells for this board; reset over + // DSI, no backlight GPIO). Timing matches Waveshare's + // JD9365_800_1280_PANEL_60HZ_DPI_CONFIG vendor timing. + static constexpr PanelParams JD9365_PARAMS{800, 1280, 80, 1500, GPIO_NUM_NC, GPIO_NUM_NC, + 20, 20, 40, 4, 10, 30}; #if CONFIG_ESP32_P4_NANO_DISPLAY_EK79007 static constexpr DisplayController default_controller_ = DisplayController::EK79007; -#else +#elif CONFIG_ESP32_P4_NANO_DISPLAY_ILI9881C static constexpr DisplayController default_controller_ = DisplayController::ILI9881C; +#else + static constexpr DisplayController default_controller_ = DisplayController::JD9365; #endif // Runtime display geometry, set from the configured panel. - PanelParams panel_params_{default_controller_ == DisplayController::ILI9881C ? ILI9881C_PARAMS - : EK79007_PARAMS}; + PanelParams panel_params_{default_controller_ == DisplayController::ILI9881C ? ILI9881C_PARAMS + : default_controller_ == DisplayController::EK79007 ? EK79007_PARAMS + : JD9365_PARAMS}; size_t display_width_{panel_params_.width}; size_t display_height_{panel_params_.height}; @@ -509,7 +529,6 @@ class Esp32P4Nano : public BaseComponent { static constexpr int mipi_dsi_lanes = 2; // DSI HS lane bit rate (Mbps/lane). 900 Mbps matches Espressif's official // panel bus configs; using the wrong rate mis-packs the pixel bits on the link. - static constexpr int mipi_dsi_lane_bitrate_mbps = 900; static constexpr int mipi_dsi_phy_ldo_channel = 3; // on-chip LDO_VO3 -> VDD_MIPI_DPHY static constexpr int mipi_dsi_phy_ldo_voltage_mv = 2500; @@ -522,8 +541,8 @@ class Esp32P4Nano : public BaseComponent { static constexpr bool swap_xy = false; // touch -> display coordinate conversion. May need tuning per panel. static constexpr bool touch_swap_xy = false; - static constexpr bool touch_invert_x = true; - static constexpr bool touch_invert_y = true; + static constexpr bool touch_invert_x = false; + static constexpr bool touch_invert_y = false; // Touch (GT911) - interrupt/reset are NOT connected on this board static constexpr uint8_t gt911_default_address = 0x5D; @@ -639,9 +658,14 @@ class Esp32P4Nano : public BaseComponent { ///////////////////////////////////////////////////////////////////////////// // Display state (MIPI-DSI). NOTE: there is no backlight GPIO / espp::Led on - // this board; the backlight is driven by an on-board I2C controller, so the - // stored brightness is best-effort only (see src/video.cpp). + // this board; the backlight is driven by an on-board I2C controller. On the + // 10.1" JD9365 panel brightness() writes that controller (addr 0x45, reg + // 0x96); on other panels the stored brightness is best-effort only (see + // src/video.cpp). ///////////////////////////////////////////////////////////////////////////// + // On-board I2C backlight controller (10.1" JD9365 panel) + static constexpr uint8_t backlight_i2c_address = 0x45; + std::shared_ptr> backlight_i2c_device_; std::atomic brightness_{100.0f}; std::shared_ptr> display_; std::shared_ptr display_driver_{static_cast(nullptr)}; diff --git a/components/esp32-p4-nano/src/audio.cpp b/components/esp32-p4-nano/src/audio.cpp index 903cb626d..4c9d0da15 100644 --- a/components/esp32-p4-nano/src/audio.cpp +++ b/components/esp32-p4-nano/src/audio.cpp @@ -172,6 +172,16 @@ size_t Esp32P4Nano::play_audio(const uint8_t *data, uint32_t num_bytes) { return xStreamBufferSend(audio_tx_stream, data, sendable, 0); } +void Esp32P4Nano::clear_audio() { + if (!audio_initialized_ || audio_tx_stream == nullptr) { + return; + } + // Drop everything queued but not yet handed to the I2S DMA. The drain task's + // current (at most one) frame still finishes, so this cuts over on the next + // ~16 ms frame boundary. + xStreamBufferReset(audio_tx_stream); +} + size_t Esp32P4Nano::play_audio(std::span data) { return play_audio(data.data(), data.size()); } @@ -302,7 +312,12 @@ bool Esp32P4Nano::audio_task_callback(std::mutex &m, std::condition_variable &cv i2s_channel_write(audio_tx_handle, tx_buf, buffer_size, NULL, pdMS_TO_TICKS(100)); } else { xStreamBufferReceive(audio_tx_stream, tx_buf, available, 0); - i2s_channel_write(audio_tx_handle, tx_buf, available, NULL, pdMS_TO_TICKS(100)); + // Always write a full, frame-aligned buffer (queued samples zero-padded to + // buffer_size) so the I2S DMA is fed at a constant cadence - matching the + // esp-box / t-deck / m5stack-tab5 playback path. Writing only `available` + // bytes makes the drain cadence variable and interleaves whole frames of + // silence into bursty streams, which sounds choppy/glitchy. + i2s_channel_write(audio_tx_handle, tx_buf, buffer_size, NULL, pdMS_TO_TICKS(100)); } // honor a stop request per the Task contract: check/clear notified under m std::unique_lock lock(m); diff --git a/components/esp32-p4-nano/src/camera.cpp b/components/esp32-p4-nano/src/camera.cpp index 817868f93..3e5ddbf36 100644 --- a/components/esp32-p4-nano/src/camera.cpp +++ b/components/esp32-p4-nano/src/camera.cpp @@ -51,12 +51,11 @@ bool Esp32P4Nano::initialize_camera(const camera_frame_callback_t &callback, esp_video_init_csi_config_t csi_config = {}; csi_config.sccb_config.init_sccb = false; csi_config.sccb_config.i2c_handle = internal_i2c_.native_bus_handle(); - // Run the sensor SCCB at 400 kHz rather than 100 kHz. The SCCB shares the - // internal I2C bus with the touch controller and audio codec, and the ISP's - // auto-exposure writes the sensor over SCCB every frame; at 100 kHz each of - // those writes holds the shared bus ~4x longer than needed. 400 kHz is a safe - // SCCB speed for this sensor and matches the bus's configured clock. - csi_config.sccb_config.freq = 400000; + // Run the sensor SCCB at 100 kHz, matching Waveshare's own camera demos for + // these boards (their Kconfig floor is 100 kHz). Some OV sensors are + // unreliable at higher SCCB rates during probe, and esp_video applies this + // freq to the SCCB device even when reusing an external I2C bus handle. + csi_config.sccb_config.freq = 100000; csi_config.reset_pin = GPIO_NUM_NC; csi_config.pwdn_pin = GPIO_NUM_NC; csi_config.dont_init_ldo = false; diff --git a/components/esp32-p4-nano/src/video.cpp b/components/esp32-p4-nano/src/video.cpp index 1a8c94de6..bf9f45ef5 100644 --- a/components/esp32-p4-nano/src/video.cpp +++ b/components/esp32-p4-nano/src/video.cpp @@ -12,6 +12,7 @@ #include #include #include +#include #include #include @@ -28,6 +29,9 @@ namespace espp { bool Esp32P4Nano::initialize_lcd() { logger_.info("Initializing LCD (MIPI-DSI)"); + // Select the panel params first: the DSI lane bit rate is per-panel. + apply_panel_params(default_controller_); + esp_err_t ret = ESP_OK; // Enable the MIPI DSI PHY power LDO (on-chip LDO_VO3 -> VDD_MIPI_DPHY, @@ -48,15 +52,56 @@ bool Esp32P4Nano::initialize_lcd() { // the panel is reset over DSI during its vendor init sequence, so there is no // hardware GPIO reset step here (unlike the ESP32-P4-Function-EV-Board). + // The 10.1" JD9365 panel is powered/reset and backlit by an on-board I2C + // controller (addr 0x45) on the BSP's internal I2C bus: register 0x95 is the + // panel power/reset control and 0x96 the backlight level (0-255) on this + // panel's controller. This power-on sequence (values and delays) matches + // Waveshare's vendor panel component and must run before any DSI traffic + // so the panel is powered and out of reset when the vendor init sequence is + // sent. + if (display_controller_ == DisplayController::JD9365) { + // Lazily create the backlight/panel-power I2C device on the internal bus + // (shared with brightness()). + if (!backlight_i2c_device_) { + std::error_code ec; + backlight_i2c_device_ = internal_i2c_.add_device( + { + .device_address = backlight_i2c_address, + .timeout_ms = static_cast(internal_i2c_.config().timeout_ms), + .scl_speed_hz = internal_i2c_.config().clk_speed, + .log_level = espp::Logger::Verbosity::WARN, + }, + ec); + if (!backlight_i2c_device_) { + logger_.error("Could not initialize panel power/backlight I2C device (0x{:02X}): {}", + backlight_i2c_address, ec.message()); + return false; + } + } + auto write_panel_reg = [this](uint8_t reg, uint8_t value) { + const uint8_t data[2] = {reg, value}; + std::error_code ec; + if (!backlight_i2c_device_->write(data, sizeof(data), ec)) { + logger_.error("Failed to write panel power controller reg 0x{:02X}: {}", reg, ec.message()); + } + }; + write_panel_reg(0x95, 0x11); // panel power/reset control + write_panel_reg(0x95, 0x17); // panel power/reset control + write_panel_reg(0x96, 0x00); // backlight off while powering up + std::this_thread::sleep_for(100ms); + write_panel_reg(0x96, 0xFF); // backlight full on + std::this_thread::sleep_for(1000ms); + } + // Create the MIPI DSI bus (also initializes the DSI PHY) if (lcd_handles_.mipi_dsi_bus == nullptr) { logger_.info("Creating MIPI DSI bus ({} lanes, {} Mbps/lane)", mipi_dsi_lanes, - mipi_dsi_lane_bitrate_mbps); + panel_params_.lane_bitrate_mbps); esp_lcd_dsi_bus_config_t bus_config = {}; bus_config.bus_id = 0; bus_config.num_data_lanes = mipi_dsi_lanes; bus_config.phy_clk_src = MIPI_DSI_PHY_CLK_SRC_DEFAULT; - bus_config.lane_bit_rate_mbps = mipi_dsi_lane_bitrate_mbps; + bus_config.lane_bit_rate_mbps = panel_params_.lane_bitrate_mbps; ret = esp_lcd_new_dsi_bus(&bus_config, &lcd_handles_.mipi_dsi_bus); if (ret != ESP_OK) { logger_.error("New DSI bus init failed: {}", esp_err_to_name(ret)); @@ -80,59 +125,27 @@ bool Esp32P4Nano::initialize_lcd() { // Select the panel (Kconfig-driven) and apply its parameters (geometry, DPI // timing). - apply_panel_params(default_controller_); logger_.info("Using display panel: {} ({}x{})", get_display_controller_name(), display_width_, display_height_); // NOTE: The ESP32-P4-NANO has no backlight GPIO. The backlight is driven by an - // on-board I2C controller (addr ~0x45/0x95, chip/protocol not yet identified); - // brightness control is a TODO pending hardware identification. The panel powers - // up with the backlight on, so no espp::Led / PWM backlight is instantiated here - // and the display fully initializes and shows pixels without any backlight code. + // on-board I2C controller (addr 0x45). On the 10.1" JD9365 panel brightness() + // writes that controller (reg 0x96); on the other panels the panel powers up + // with the backlight on and brightness() only stores the value. No espp::Led / + // PWM backlight is instantiated here. brightness(100.0f); - // Create the DPI (video) panel with the configured panel's timing. - if (lcd_handles_.panel == nullptr) { - esp_lcd_dpi_panel_config_t dpi_cfg{}; - memset(&dpi_cfg, 0, sizeof(dpi_cfg)); - dpi_cfg.virtual_channel = 0; - dpi_cfg.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; - dpi_cfg.dpi_clock_freq_mhz = panel_params_.dpi_clock_freq_mhz; -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) - dpi_cfg.in_color_format = LCD_COLOR_FMT_RGB565; - dpi_cfg.out_color_format = LCD_COLOR_FMT_RGB565; -#else - dpi_cfg.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; - dpi_cfg.flags.use_dma2d = true; -#endif - dpi_cfg.num_fbs = 1; - dpi_cfg.video_timing.h_size = display_width_; - dpi_cfg.video_timing.v_size = display_height_; - dpi_cfg.video_timing.hsync_pulse_width = panel_params_.hsync_pulse_width; - dpi_cfg.video_timing.hsync_back_porch = panel_params_.hsync_back_porch; - dpi_cfg.video_timing.hsync_front_porch = panel_params_.hsync_front_porch; - dpi_cfg.video_timing.vsync_pulse_width = panel_params_.vsync_pulse_width; - dpi_cfg.video_timing.vsync_back_porch = panel_params_.vsync_back_porch; - dpi_cfg.video_timing.vsync_front_porch = panel_params_.vsync_front_porch; - logger_.info("Creating DPI panel ({}x{} @ {} MHz)", dpi_cfg.video_timing.h_size, - dpi_cfg.video_timing.v_size, dpi_cfg.dpi_clock_freq_mhz); - ret = esp_lcd_new_panel_dpi(lcd_handles_.mipi_dsi_bus, &dpi_cfg, &lcd_handles_.panel); - if (ret != ESP_OK) { - logger_.error("Failed to create MIPI DSI DPI panel: {}", esp_err_to_name(ret)); - return false; - } - // NOTE: deliberately do NOT enable DMA2D for the DPI panel. DMA2D is a - // color-processing engine, not a plain copy: routing the LVGL flush - // (esp_lcd_panel_draw_bitmap) through it corrupts the RGB565 channel order, - // while the plain CPU copy path renders correctly. - } - - // Send the panel controller's vendor init sequence over DBI (command mode), - // before starting the DPI video stream. + // espp-driver path (JD9365 / ILI9881C / EK79007): send the panel + // controller's vendor init sequence over DBI (command mode), before starting + // the DPI video stream. espp::display_drivers::Config display_config{ .panel_io = nullptr, .write_command = std::bind_front(&Esp32P4Nano::dsi_write_command, this), - .read_command = std::bind_front(&Esp32P4Nano::dsi_read_command, this), + // NOTE: the Waveshare ESP32-P4 panels do not reliably support MIPI-DSI DCS + // reads (bus turn-around); the ESP-IDF HAL busy-waits on the read, which + // hangs panel init and trips the task watchdog. Do not provide a + // read_command so the driver skips the optional panel-ID read. + .read_command = nullptr, .lcd_send_lines = nullptr, .reset_pin = GPIO_NUM_NC, .data_command_pin = GPIO_NUM_NC, @@ -148,7 +161,15 @@ bool Esp32P4Nano::initialize_lcd() { }; display_driver_.reset(); - if (display_controller_ == DisplayController::ILI9881C) { + if (display_controller_ == DisplayController::JD9365) { + // The Waveshare 10.1" 800x1280 panel is a JD9365. espp::Jd9365 performs + // the DCS software reset and sends the vendor init sequence (taken from + // Waveshare's vendor panel component; see jd9365.hpp) over the DBI IO. + auto driver = std::make_shared(display_config); + if (driver->initialize()) { + display_driver_ = std::move(driver); + } + } else if (display_controller_ == DisplayController::ILI9881C) { auto driver = std::make_shared(display_config); if (driver->initialize()) { display_driver_ = std::move(driver); @@ -164,6 +185,58 @@ bool Esp32P4Nano::initialize_lcd() { return false; } + // Create the DPI (video) panel with the configured panel's timing. This must + // come AFTER the vendor init sequence above: esp_lcd_new_panel_dpi() starts the + // HS video stream, and once it is running the DSI cannot drain the low-power + // command FIFO, so a long init sequence (e.g. ILI9881C's 202 commands) would + // overflow it and hang. + if (lcd_handles_.panel == nullptr) { + esp_lcd_dpi_panel_config_t dpi_cfg{}; + memset(&dpi_cfg, 0, sizeof(dpi_cfg)); + dpi_cfg.virtual_channel = 0; + dpi_cfg.dpi_clk_src = MIPI_DSI_DPI_CLK_SRC_DEFAULT; + dpi_cfg.dpi_clock_freq_mhz = panel_params_.dpi_clock_freq_mhz; +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + dpi_cfg.in_color_format = LCD_COLOR_FMT_RGB565; + dpi_cfg.out_color_format = LCD_COLOR_FMT_RGB565; +#else + dpi_cfg.pixel_format = LCD_COLOR_PIXEL_FORMAT_RGB565; + dpi_cfg.flags.use_dma2d = true; +#endif + dpi_cfg.num_fbs = 1; + dpi_cfg.video_timing.h_size = display_width_; + dpi_cfg.video_timing.v_size = display_height_; + dpi_cfg.video_timing.hsync_pulse_width = panel_params_.hsync_pulse_width; + dpi_cfg.video_timing.hsync_back_porch = panel_params_.hsync_back_porch; + dpi_cfg.video_timing.hsync_front_porch = panel_params_.hsync_front_porch; + dpi_cfg.video_timing.vsync_pulse_width = panel_params_.vsync_pulse_width; + dpi_cfg.video_timing.vsync_back_porch = panel_params_.vsync_back_porch; + dpi_cfg.video_timing.vsync_front_porch = panel_params_.vsync_front_porch; + logger_.info("Creating DPI panel ({}x{} @ {} MHz)", dpi_cfg.video_timing.h_size, + dpi_cfg.video_timing.v_size, dpi_cfg.dpi_clock_freq_mhz); + ret = esp_lcd_new_panel_dpi(lcd_handles_.mipi_dsi_bus, &dpi_cfg, &lcd_handles_.panel); + if (ret != ESP_OK) { + logger_.error("Failed to create MIPI DSI DPI panel: {}", esp_err_to_name(ret)); + return false; + } + // NOTE: for the ILI9881C / EK79007 panels we deliberately do NOT enable + // DMA2D for the DPI panel. DMA2D is a color-processing engine, not a plain + // copy: routing the LVGL flush (esp_lcd_panel_draw_bitmap) through it + // corrupts the RGB565 channel order on those panels, while the plain CPU + // copy path renders correctly. The JD9365 panel renders correctly WITH + // DMA2D (and Waveshare's vendor panel component enables it), so keep it + // enabled on that path. +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + if (display_controller_ == DisplayController::JD9365) { + ret = esp_lcd_dpi_panel_enable_dma2d(lcd_handles_.panel); + if (ret != ESP_OK) { + logger_.error("Failed to enable DMA2D for the DPI panel: {}", esp_err_to_name(ret)); + return false; + } + } +#endif + } + // Low-level panel init (starts the DPI video stream) ret = lcd_handles_.panel->init(lcd_handles_.panel); if (ret != ESP_OK) { @@ -193,8 +266,17 @@ bool Esp32P4Nano::initialize_lcd() { void Esp32P4Nano::apply_panel_params(DisplayController controller) { display_controller_ = (controller == DisplayController::UNKNOWN) ? default_controller_ : controller; - panel_params_ = - (display_controller_ == DisplayController::ILI9881C) ? ILI9881C_PARAMS : EK79007_PARAMS; + switch (display_controller_) { + case DisplayController::JD9365: + panel_params_ = JD9365_PARAMS; + break; + case DisplayController::ILI9881C: + panel_params_ = ILI9881C_PARAMS; + break; + default: + panel_params_ = EK79007_PARAMS; + break; + } display_width_ = panel_params_.width; display_height_ = panel_params_.height; } @@ -287,15 +369,44 @@ void Esp32P4Nano::write_lcd_lines(int xs, int ys, int xe, int ye, const uint8_t void Esp32P4Nano::brightness(float brightness) { // The ESP32-P4-NANO has NO backlight GPIO. The backlight is driven by an - // on-board I2C controller (addr ~0x45/0x95, chip/protocol not yet identified); - // brightness control is a TODO pending hardware identification. The panel powers - // up with the backlight on. For now this is best-effort: store the requested - // value so brightness() reads back what was set, and log it. + // on-board I2C controller at address 0x45. On the 10.1" JD9365 panel the + // brightness register is 0x96 (value 0-255); Waveshare's own BSP writes 0x86 + // instead for some panel revisions. For panels other than the JD9365 the + // chip/protocol has not been verified, so this remains best-effort: store + // the requested value so brightness() reads back what was set, and log it. brightness = std::clamp(brightness, 0.0f, 100.0f); brightness_ = brightness; - logger_.debug("brightness({}) requested; no backlight GPIO on this board (on-board I2C " - "controller not yet identified), value stored only", - brightness); + if (display_controller_ != DisplayController::JD9365) { + logger_.debug("brightness({}) requested; no backlight GPIO on this board (on-board I2C " + "backlight protocol not verified for this panel), value stored only", + brightness); + return; + } + // Lazily create the backlight I2C device on the internal bus. + if (!backlight_i2c_device_) { + std::error_code ec; + backlight_i2c_device_ = internal_i2c_.add_device( + { + .device_address = backlight_i2c_address, + .timeout_ms = static_cast(internal_i2c_.config().timeout_ms), + .scl_speed_hz = internal_i2c_.config().clk_speed, + .log_level = espp::Logger::Verbosity::WARN, + }, + ec); + if (!backlight_i2c_device_) { + logger_.error("Could not initialize backlight I2C device (0x{:02X}): {}", + backlight_i2c_address, ec.message()); + return; + } + } + // Register 0x96 is the brightness register (0-255) on the 10.1" JD9365 + // panel's power/backlight controller (Waveshare's BSP uses 0x86 for some + // panel revisions). + const uint8_t data[2] = {0x96, static_cast(255.0f * brightness / 100.0f)}; + std::error_code ec; + if (!backlight_i2c_device_->write(data, sizeof(data), ec)) { + logger_.error("Failed to write backlight brightness: {}", ec.message()); + } } float Esp32P4Nano::brightness() const { return brightness_.load(); } diff --git a/doc/Doxyfile b/doc/Doxyfile index 33f097b46..6200383ba 100755 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -255,6 +255,7 @@ INPUT = \ $(PROJECT_PATH)/components/display_drivers/include/ek79007.hpp \ $(PROJECT_PATH)/components/display_drivers/include/ili9341.hpp \ $(PROJECT_PATH)/components/display_drivers/include/ili9881.hpp \ + $(PROJECT_PATH)/components/display_drivers/include/jd9365.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 \