From f69bf042709f3b0e565336b2503cd519b052a292 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Wed, 24 Jun 2026 14:01:57 -0500 Subject: [PATCH 1/9] feat(esp32p4-function-ev-board): Add new BSP component for `ESP32P4 Function Evaluation Board` and the associated LCD board --- .github/workflows/build.yml | 2 + .github/workflows/upload_components.yml | 1 + .../display_drivers/include/ek79007.hpp | 93 +++ .../esp32-p4-function-ev-board/CMakeLists.txt | 24 + components/esp32-p4-function-ev-board/Kconfig | 51 ++ .../esp32-p4-function-ev-board/README.md | 97 +++ .../example/CMakeLists.txt | 22 + .../example/README.md | 44 ++ .../example/main/CMakeLists.txt | 3 + .../example/main/click.wav | Bin 0 -> 35918 bytes .../esp32_p4_function_ev_board_example.cpp | 560 ++++++++++++++++++ .../example/partitions.csv | 5 + .../example/sdkconfig.defaults | 56 ++ .../idf_component.yml | 31 + .../include/esp32-p4-function-ev-board.hpp | 560 ++++++++++++++++++ .../esp32-p4-function-ev-board/src/audio.cpp | 178 ++++++ .../src/esp32-p4-function-ev-board.cpp | 56 ++ .../src/ethernet.cpp | 187 ++++++ .../esp32-p4-function-ev-board/src/sdcard.cpp | 89 +++ .../src/touchpad.cpp | 147 +++++ .../esp32-p4-function-ev-board/src/video.cpp | 398 +++++++++++++ .../third_party/LICENSE | 201 +++++++ .../third_party/README.md | 18 + .../third_party/esp_eth_phy_ip101.c | 222 +++++++ .../third_party/esp_eth_phy_ip101.h | 28 + doc/Doxyfile | 3 + doc/en/esp32_p4_function_ev_board.rst | 54 ++ doc/en/esp32_p4_function_ev_board_example.md | 2 + doc/en/index.rst | 1 + 29 files changed, 3133 insertions(+) create mode 100644 components/display_drivers/include/ek79007.hpp create mode 100644 components/esp32-p4-function-ev-board/CMakeLists.txt create mode 100644 components/esp32-p4-function-ev-board/Kconfig create mode 100644 components/esp32-p4-function-ev-board/README.md create mode 100644 components/esp32-p4-function-ev-board/example/CMakeLists.txt create mode 100644 components/esp32-p4-function-ev-board/example/README.md create mode 100644 components/esp32-p4-function-ev-board/example/main/CMakeLists.txt create mode 100644 components/esp32-p4-function-ev-board/example/main/click.wav create mode 100644 components/esp32-p4-function-ev-board/example/main/esp32_p4_function_ev_board_example.cpp create mode 100644 components/esp32-p4-function-ev-board/example/partitions.csv create mode 100644 components/esp32-p4-function-ev-board/example/sdkconfig.defaults create mode 100644 components/esp32-p4-function-ev-board/idf_component.yml create mode 100644 components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp create mode 100644 components/esp32-p4-function-ev-board/src/audio.cpp create mode 100644 components/esp32-p4-function-ev-board/src/esp32-p4-function-ev-board.cpp create mode 100644 components/esp32-p4-function-ev-board/src/ethernet.cpp create mode 100644 components/esp32-p4-function-ev-board/src/sdcard.cpp create mode 100644 components/esp32-p4-function-ev-board/src/touchpad.cpp create mode 100644 components/esp32-p4-function-ev-board/src/video.cpp create mode 100644 components/esp32-p4-function-ev-board/third_party/LICENSE create mode 100644 components/esp32-p4-function-ev-board/third_party/README.md create mode 100644 components/esp32-p4-function-ev-board/third_party/esp_eth_phy_ip101.c create mode 100644 components/esp32-p4-function-ev-board/third_party/esp_eth_phy_ip101.h create mode 100644 doc/en/esp32_p4_function_ev_board.rst create mode 100644 doc/en/esp32_p4_function_ev_board_example.md diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 06b8cce8a..18c2219e2 100755 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -79,6 +79,8 @@ jobs: target: esp32 - path: 'components/esp32-timer-cam/example' target: esp32 + - path: 'components/esp32-p4-function-ev-board/example' + target: esp32p4 - path: 'components/esp-box/example' target: esp32s3 - path: 'components/event_manager/example' diff --git a/.github/workflows/upload_components.yml b/.github/workflows/upload_components.yml index bfa0e014f..fbd2c4487 100755 --- a/.github/workflows/upload_components.yml +++ b/.github/workflows/upload_components.yml @@ -62,6 +62,7 @@ jobs: components/drv2605 components/encoder components/esp-box + components/esp32-p4-function-ev-board components/esp32-timer-cam components/event_manager components/expressive_eyes diff --git a/components/display_drivers/include/ek79007.hpp b/components/display_drivers/include/ek79007.hpp new file mode 100644 index 000000000..3dbcdefab --- /dev/null +++ b/components/display_drivers/include/ek79007.hpp @@ -0,0 +1,93 @@ +#pragma once + +#include + +#include "display_drivers.hpp" + +namespace espp { +/** + * @brief Display driver for the EK79007 MIPI-DSI display controller. + * + * The EK79007 (e.g. the 7-inch 1024x600 panel on the ESP32-P4-HMI-Subboard) is + * largely driven by its DPI video timing; it only needs a short vendor command + * sequence plus a DSI lane-count command and Sleep-Out. This follows the same + * interface as the other espp display drivers and relies on a lower-level + * transport (MIPI-DSI DBI) to execute write_command. + * + * The initialization sequence here mirrors Espressif's esp_lcd_ek79007 driver. + */ +class Ek79007 : public display_drivers::MipiDbiDisplayDriver { + // EK79007 MADCTL mirror bits (non-standard layout) + static constexpr uint8_t SHLR_BIT = 1 << 0; ///< Source (horizontal) flip -> mirror x + static constexpr uint8_t UPDN_BIT = 1 << 1; ///< Gate (vertical) flip -> 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 + 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 + pad_control = 0xB2, ///< DSI lane configuration + }; + + /// DSI lane configuration values for the pad_control (0xB2) command + static constexpr uint8_t DSI_2_LANE = 0x10; + static constexpr uint8_t DSI_4_LANE = 0x00; + + explicit Ek79007(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 { + display_drivers::init_pins(config_.reset_pin, config_.data_command_pin, config_.reset_value); + + // This must match Espressif's esp_lcd_ek79007 vendor_specific_init_default + // exactly: the DSI lane-config command, the 0x80-0x86 vendor registers, then + // Sleep-Out. Notably it does NOT send MADCTL or Display-On here (the EK79007 + // uses its power-on defaults + the DPI video stream); sending those extra + // commands can leave the panel showing black. + auto init_commands = std::to_array>({ + // Configure DSI for 2 data lanes + {static_cast(Command::pad_control), {DSI_2_LANE}, 0}, + // Vendor-specific power/timing setup + {0x80, {0x8B}, 0}, + {0x81, {0x78}, 0}, + {0x82, {0x84}, 0}, + {0x83, {0x88}, 0}, + {0x84, {0xA8}, 0}, + {0x85, {0xE3}, 0}, + {0x86, {0x88}, 0}, + // Exit sleep (requires >=120ms before sending further commands) + {static_cast(Command::sleep_out), {}, 120}, + }); + + send_commands(init_commands); + return true; + } + + void set_rotation(const DisplayRotation &rotation) override { + Controller::set_rotation(rotation); + auto data = std::array{make_madctl()}; + std::scoped_lock lock(io_mutex_); + write_command(static_cast(Command::madctl), data, 0); + } + +private: + uint8_t make_madctl() const { + uint8_t value = 0; + if (config_.mirror_x) + value |= SHLR_BIT; + if (config_.mirror_y) + value |= UPDN_BIT; + return value; + } +}; +} // namespace espp diff --git a/components/esp32-p4-function-ev-board/CMakeLists.txt b/components/esp32-p4-function-ev-board/CMakeLists.txt new file mode 100644 index 000000000..1712fa844 --- /dev/null +++ b/components/esp32-p4-function-ev-board/CMakeLists.txt @@ -0,0 +1,24 @@ +idf_component_register( + INCLUDE_DIRS "include" "third_party" + SRC_DIRS "src" "third_party" + REQUIRES + "base_component" + "codec" + "display" + "display_drivers" + "gt911" + "i2c" + "input_drivers" + "interrupt" + "led" + "task" + "esp_driver_gpio" + "esp_lcd" + "esp_driver_i2s" + "esp_eth" + "esp_netif" + "esp_event" + "fatfs" + "esp_driver_sdmmc" + "sdmmc" + ) diff --git a/components/esp32-p4-function-ev-board/Kconfig b/components/esp32-p4-function-ev-board/Kconfig new file mode 100644 index 000000000..029621eca --- /dev/null +++ b/components/esp32-p4-function-ev-board/Kconfig @@ -0,0 +1,51 @@ +menu "ESP32-P4 Function EV Board Configuration" + + choice ESP_P4_EV_BOARD_DISPLAY + prompt "HMI subboard display panel (fallback/default)" + default ESP_P4_EV_BOARD_DISPLAY_EK79007 + help + The BSP probes the attached MIPI-DSI panel at runtime (it reads the + ILI9881C ID over DSI and otherwise assumes EK79007), so this choice is only + the fallback used if probing is inconclusive. Set it to the panel you + normally use. + + config ESP_P4_EV_BOARD_DISPLAY_EK79007 + bool "EK79007 (7-inch, 1024x600)" + help + The 7-inch 1024x600 panel that ships with the standard + ESP32-P4-Function-EV-Board kit. Backlight on GPIO26, reset on GPIO27. + + config ESP_P4_EV_BOARD_DISPLAY_ILI9881C + bool "ILI9881C (10.1-inch, 800x1280)" + help + The 10.1-inch 800x1280 panel variant. Backlight on GPIO23, reset is not + connected (handled over DSI). + endchoice + + config ESP_P4_EV_BOARD_INTERRUPT_STACK_SIZE + int "Interrupt task stack size (bytes)" + default 4096 + help + Size of the stack used for the GPIO interrupt handler task (button, etc.). + + config ESP_P4_EV_BOARD_TOUCH_TASK_STACK_SIZE + int "Touch polling task stack size (bytes)" + default 4096 + help + Size of the stack used for the GT911 touch polling task. The HMI subboard + does not route the touch interrupt to the ESP32-P4, so touch is polled. + + config ESP_P4_EV_BOARD_AUDIO_TASK_STACK_SIZE + int "Audio task stack size (bytes)" + default 8192 + help + Size of the stack used for the audio processing task. + + config ESP_P4_EV_BOARD_ETHERNET + bool "Enable Ethernet (IP101 RMII) support" + default y + help + Build the Ethernet (EMAC + IP101 RMII PHY) support in the BSP. Disable to + drop the esp_eth dependency if you do not need wired networking. + +endmenu diff --git a/components/esp32-p4-function-ev-board/README.md b/components/esp32-p4-function-ev-board/README.md new file mode 100644 index 000000000..104a5588d --- /dev/null +++ b/components/esp32-p4-function-ev-board/README.md @@ -0,0 +1,97 @@ +# ESP32-P4 Function EV Board + +[![Badge](https://components.espressif.com/components/espp/esp32-p4-function-ev-board/badge.svg)](https://components.espressif.com/components/espp/esp32-p4-function-ev-board) + +Board Support Package (BSP) for the Espressif **ESP32-P4 Function EV Board** used +together with the **ESP32-P4-HMI-Subboard**. + +> [!IMPORTANT] +> **Display jumpers.** On the LCD adapter board, the `RST_LCD` and `PWM` signals +> are broken out on a header and must be wired to the ESP32-P4 main board for the +> display to work: connect **`RST_LCD` → J1 GPIO27** and **`PWM` → J1 GPIO26** +> (these are the LCD reset and backlight-PWM pins this BSP drives). Without these +> jumpers the panel never gets reset and the backlight PWM is unconnected, so the +> screen stays black even though everything initializes. See the +> [user guide](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32p4/esp32-p4-function-ev-board/user_guide.html) +> for the header pinout. + +## Official board documentation + +- [ESP32-P4-Function-EV-Board User Guide](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32p4/esp32-p4-function-ev-board/user_guide.html) + (includes the ESP32-P4-HMI-Subboard) +- [ESP32-P4-Function-EV-Board overview page](https://docs.espressif.com/projects/esp-dev-kits/en/latest/esp32p4/esp32-p4-function-ev-board/index.html) +- [Board schematic (PDF)](https://dl.espressif.com/dl/schematics/esp32-p4-function-ev-board-schematics_v1.52.pdf) +- [Espressif reference BSP (`esp-bsp`)](https://github.com/espressif/esp-bsp/tree/master/bsp/esp32_p4_function_ev_board) +- [ESP32-P4 Get Started (ESP-IDF)](https://docs.espressif.com/projects/esp-idf/en/stable/esp32p4/get-started/index.html) + +### Display panels (HMI subboard) + +Both panels plug into the shared LCD adapter board via its FPC connector: + +- [LCD Adapter Board Schematic (PDF)](https://dl.espressif.com/dl/schematics/esp32-p4-function-ev-board-lcd-subboard-schematics.pdf) +- [LCD Adapter Board PCB Layout (PDF)](https://dl.espressif.com/dl/schematics/esp32-p4-function-ev-board-lcd-subboard-pcb-layout.pdf) + +**EK79007 — 7", 1024x600** (the panel Espressif ships/documents for this board): + +- [Display Datasheet (PDF)](https://dl.espressif.com/dl/schematics/display_datasheet.pdf) +- [EK79007AD display driver chip datasheet (PDF)](https://dl.espressif.com/dl/schematics/display_driver_chip_EK79007AD_datasheet.pdf) +- [EK73217BCGA display driver chip datasheet (PDF)](https://dl.espressif.com/dl/schematics/display_driver_chip_EK73217BCGA_datasheet.pdf) + +**ILI9881C — 10.1", 800x1280**: a panel option supported by the BSP. Espressif +does not publish a dedicated LCD-subboard schematic/datasheet for this panel on +this board; refer to the +[esp-bsp `esp_lcd_ili9881c` driver](https://github.com/espressif/esp-bsp/tree/master/components/lcd/esp_lcd_ili9881c) +for the panel/timing details. + +The `espp::Esp32P4FunctionEvBoard` class is a singleton hardware abstraction that +initializes and exposes the board's peripherals: + +- **MIPI-DSI display** — Kconfig-selectable **EK79007 (7", 1024x600)** or + **ILI9881C (10.1", 800x1280)** — with LVGL integration and PWM backlight. +- **GT911 capacitive multi-touch** (the touch interrupt is not routed on this + board, so touch is polled). +- **ES8311 audio codec** (+ NS4150B speaker amplifier) over I2S for playback. +- **10/100 Ethernet** (EMAC + IP101 RMII PHY) with DHCP. +- **microSD card** (4-bit SDMMC, powered via the on-chip LDO). +- **MIPI-CSI camera** (SC2336/OV5647) — pins/SCCB wired, capture pipeline is a + stub (see Camera below). +- **BOOT button**. + +All on-board control lines are direct ESP32-P4 GPIOs (this board has no I/O +expander). The display, touch, and audio codec share a single I2C bus +(`SDA=GPIO7`, `SCL=GPIO8`). + +## Display panel detection + +`initialize_lcd()` **probes the attached panel at runtime**: it brings up the +MIPI-DSI bus, reads the ILI9881C ID over DSI, and selects **ILI9881C** if it +matches or **EK79007** otherwise. It then applies that panel's resolution, DPI +timing, backlight GPIO (26 for EK79007, 23 for ILI9881C), and reset GPIO, and +`get_display_controller()` / `display_width()` / `display_height()` reflect the +detected panel. The probed ID bytes are logged (`Panel probe ID: ...`). + +The Kconfig choice under *ESP32-P4 Function EV Board Configuration* is only the +**fallback** used if probing is inconclusive. + +## Example + +See the [example](./example). It initializes the display + touch, shows a live +on-screen status read-out (panel, touch coordinates, SD, Ethernet IP), and brings +up the SD card, audio, Ethernet, and BOOT button. + +## Peripheral status / notes + +- **Ethernet**: uses the ESP-IDF internal EMAC with the generic 802.3 PHY driver + for the IP101 (the dedicated `esp_eth_phy_ip101` driver is a separate managed + component in newer ESP-IDF; the generic driver works for this board). The RMII + pinout is the ESP-IDF ESP32-P4 default (MDC=31, MDIO=52, REF_CLK in=50, + TX_EN=49, TXD0=34, TXD1=35, CRS_DV=28, RXD0=29, RXD1=30; PHY reset=51, addr=1). +- **Camera**: the SC2336/OV5647 MIPI-CSI sensor's pins are documented (SCCB on + the internal I2C bus at 0x30, reset/XCLK not connected), but the `esp_video` + capture pipeline is **not yet implemented**; `initialize_camera()` returns + false. Contributions welcome. +- This BSP requires PSRAM for the MIPI-DSI frame buffers (see the example's + `sdkconfig.defaults`). +- The Ethernet RMII pinout comes from the ESP-IDF ESP32-P4 software defaults + (corroborated by the board docs) rather than a parsed schematic — verify + against your board revision if Ethernet does not link. diff --git a/components/esp32-p4-function-ev-board/example/CMakeLists.txt b/components/esp32-p4-function-ev-board/example/CMakeLists.txt new file mode 100644 index 000000000..d2e9e6fc2 --- /dev/null +++ b/components/esp32-p4-function-ev-board/example/CMakeLists.txt @@ -0,0 +1,22 @@ +# The following lines of boilerplate have to be in your project's CMakeLists +# in this exact order for cmake to work correctly +cmake_minimum_required(VERSION 3.20) + +set(ENV{IDF_COMPONENT_MANAGER} "0") +include($ENV{IDF_PATH}/tools/cmake/project.cmake) + +# add the component directories that we want to use +set(EXTRA_COMPONENT_DIRS + "../../../components/" +) + +set( + COMPONENTS + "main esptool_py esp32-p4-function-ev-board display lvgl cdr rtps ping" + CACHE STRING + "List of components to include" + ) + +project(esp32_p4_function_ev_board_example) + +set(CMAKE_CXX_STANDARD 20) diff --git a/components/esp32-p4-function-ev-board/example/README.md b/components/esp32-p4-function-ev-board/example/README.md new file mode 100644 index 000000000..3e4597c81 --- /dev/null +++ b/components/esp32-p4-function-ev-board/example/README.md @@ -0,0 +1,44 @@ +# ESP32-P4 Function EV Board Example + +This example demonstrates the `espp::Esp32P4FunctionEvBoard` BSP for the +Espressif ESP32-P4 Function EV Board + ESP32-P4-HMI-Subboard. + +It: + +- initializes the MIPI-DSI display (EK79007 or ILI9881C, per Kconfig) and the + GT911 touch controller, +- draws an LVGL UI that lets you **draw circles wherever you touch** and plays a + **click sound** on each touch (press the BOOT button to clear the drawing), +- shows a live on-screen status read-out: display panel, touch coordinates, SD + card size, Ethernet IP, **RTPS publisher** status, and **system** info (free + internal/PSRAM heap and uptime), +- mounts the microSD card (if inserted), +- initializes the ES8311 audio codec (and loads an embedded `click.wav`), +- brings up Ethernet (IP101) with DHCP and, once it has an IP, starts an + **RTPS participant** that publishes a counter on `espp/test/counter`, and +- wires the BOOT button. + +> [!IMPORTANT] +> The LCD adapter board's `RST_LCD` and `PWM` signals must be jumpered to the +> ESP32-P4 main board (`RST_LCD` → J1 GPIO27, `PWM` → J1 GPIO26) or the screen +> stays black. See the component README. + +## Configuration + +Use `idf.py menuconfig` → *ESP32-P4 Function EV Board Configuration* to select +the display panel (EK79007 1024x600 by default, or ILI9881C 800x1280) and adjust +task stack sizes / enable Ethernet. + +## Build and Flash + +```sh +idf.py set-target esp32p4 +idf.py build flash monitor +``` + +## Notes + +- PSRAM is required (enabled in `sdkconfig.defaults`) for the MIPI-DSI frame + buffers. +- Plug an Ethernet cable to see the DHCP-assigned IP appear on screen and in the + log. diff --git a/components/esp32-p4-function-ev-board/example/main/CMakeLists.txt b/components/esp32-p4-function-ev-board/example/main/CMakeLists.txt new file mode 100644 index 000000000..a3e606e36 --- /dev/null +++ b/components/esp32-p4-function-ev-board/example/main/CMakeLists.txt @@ -0,0 +1,3 @@ +idf_component_register(SRC_DIRS "." + INCLUDE_DIRS "." + EMBED_TXTFILES click.wav) diff --git a/components/esp32-p4-function-ev-board/example/main/click.wav b/components/esp32-p4-function-ev-board/example/main/click.wav new file mode 100644 index 0000000000000000000000000000000000000000..2183344a236219ea90c0b6d27b792c93542eda0f GIT binary patch literal 35918 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 +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "cdr.hpp" +#include "ping.hpp" +#include "rtps.hpp" + +#include "esp32-p4-function-ev-board.hpp" + +using namespace std::chrono_literals; +using Board = espp::Esp32P4FunctionEvBoard; + +// Address of the most recently discovered RTPS peer (filled by the participant's +// on_participant_discovered callback, read by the ping self-test). +static std::mutex g_peer_mutex; +static std::string g_peer_addr; + +// Ping a target host a few times and log the result (uses the espp Ping helper). +static void ping_target(espp::Logger &logger, const char *name, const std::string &ip) { + if (ip.empty() || ip == "0.0.0.0") { + logger.warn("Ping {}: no address to ping", name); + return; + } + logger.info("Pinging {} ({})...", name, ip); + std::error_code ec; + espp::Ping ping(espp::Ping::Config{ + .session = {.target_host = ip, .count = 4, .interval_ms = 500, .timeout_ms = 1000}, + .callbacks = { + .on_session_start = nullptr, + .on_reply = + [&logger, name](uint32_t seq, uint32_t ttl, uint32_t time_ms, uint32_t bytes) { + logger.info(" {}: seq={} ttl={} time={}ms ({} bytes)", name, seq, ttl, time_ms, + bytes); + }, + .on_timeout = [&logger, name]() { logger.warn(" {}: request timed out", name); }, + .on_end = + [&logger, name, ip](const espp::Ping::Stats &s) { + logger.info("Ping {} ({}): {}/{} received, {:.0f}% loss, avg {} ms", name, ip, + s.received, s.transmitted, s.loss_pct, s.avg_ms); + }, + }, + .log_level = espp::Logger::Verbosity::WARN, + }); + ping.run(ec); + if (ec) { + logger.error("Ping {} failed to start: {}", name, ec.message()); + } +} + +////////////////////////////////////////////////////////////////////////////// +// Touch-to-draw circle state (a ring buffer of recent touch points) +////////////////////////////////////////////////////////////////////////////// +static constexpr size_t MAX_CIRCLES = 100; +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 std::vector audio_bytes; + +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 bool load_audio(size_t &out_size, size_t &out_sample_rate); + +namespace { +/// Serialize a uint32 as an encapsulated little-endian CDR payload (matches std_msgs/msg/UInt32). +inline std::vector serialize_uint32(uint32_t value) { + espp::CdrWriter writer; // defaults: CDR_LE with a 4-byte encapsulation header + writer.write(value); + return writer.take_buffer(); +} + +/// Parse a uint32 from an encapsulated CDR payload, or std::nullopt if invalid. +inline std::optional deserialize_uint32(std::span cdr) { + espp::CdrReader reader(cdr); + uint32_t value = 0; + if (!reader.valid() || !reader.read(value)) { + return std::nullopt; + } + return value; +} +} // namespace + +extern "C" void app_main(void) { + espp::Logger logger( + {.tag = "ESP32-P4 Function EV Board Example", .level = espp::Logger::Verbosity::INFO}); + logger.info("Starting example!"); + + //! [esp32 p4 function ev board example] + auto &board = Board::get(); + board.set_log_level(espp::Logger::Verbosity::INFO); + logger.info("Display panel: {}", board.get_display_controller_name()); + + // Probe the internal I2C bus + auto &i2c = board.internal_i2c(); + std::vector found; + for (uint8_t addr = 1; addr < 128; addr++) { + if (i2c.probe_device(addr)) { + found.push_back(addr); + } + } + logger.info("Found {} I2C device(s)", found.size()); + + // Display + if (!board.initialize_lcd()) { + logger.error("Failed to initialize LCD!"); + return; + } + size_t pixel_buffer_size = board.display_width() * 50; + if (!board.initialize_display(pixel_buffer_size)) { + logger.error("Failed to initialize display!"); + return; + } + + // microSD (optional — only present if a card is inserted) + bool sd_ok = board.initialize_sdcard({.format_if_mount_failed = false}); + uint32_t sd_size_mb = 0, sd_free_mb = 0; + if (sd_ok) { + board.get_sd_card_info(&sd_size_mb, &sd_free_mb); + logger.info("SD card: {} MB total, {} MB free", sd_size_mb, sd_free_mb); + } else { + logger.warn("No SD card mounted"); + } + + // Audio (ES8311) — load the embedded click sound first so we can initialize + // the codec directly at the clip's sample rate (changing the sample rate after + // the audio task is running is racy, so we avoid it here). + size_t wav_size = 0, wav_sample_rate = 0; + bool have_audio = load_audio(wav_size, wav_sample_rate); + uint32_t audio_rate = have_audio ? static_cast(wav_sample_rate) : 48000; + if (board.initialize_audio(audio_rate)) { + board.mute(false); + board.volume(60.0f); + if (have_audio) { + logger.info("Loaded {} bytes of click audio @ {} Hz", wav_size, wav_sample_rate); + } + } + + // Ethernet (IP101) — DHCP; the callback fires once an IP is acquired + static std::atomic have_ip{false}; + static std::string ip_str{"(no link)"}; + board.initialize_ethernet([&](esp_ip4_addr_t ip) { + char buf[16]; + esp_ip4addr_ntoa(&ip, buf, sizeof(buf)); + ip_str = buf; + have_ip = true; + logger.info("Ethernet IP: {}", ip_str); + }); + + // BOOT button — clears the drawn circles + // + // NOTE: GPIO35 is shared with Ethernet RMII TXD1 on this board, so the BOOT + // button can't be used as a runtime input while Ethernet is active + // (initialize_button() refuses to run when Ethernet is up). It is + // disabled here since this example uses Ethernet. + bool button_initialized = board.initialize_button([&](const auto &event) { + if (event.active) { + std::lock_guard lock(lvgl_mutex); + clear_circles(); + } + }); + if (button_initialized) { + logger.error("BOOT button incorrectly initialized while Ethernet is active!"); + } else { + logger.info("BOOT button not initialized (shared with Ethernet RMII TXD1 pin)"); + } + + // Build the LVGL UI: a title, a status label, a rotate button, and a + // transparent layer that the touch handler draws circles onto. + lv_obj_t *bg = nullptr; + lv_obj_t *title = nullptr; + static lv_obj_t *status_label = nullptr; + { + std::lock_guard lock(lvgl_mutex); + bg = lv_obj_create(lv_screen_active()); + lv_obj_set_size(bg, board.display_width(), board.display_height()); + lv_obj_set_style_bg_color(bg, lv_color_make(0, 0, 0), 0); + + title = lv_label_create(lv_screen_active()); + lv_label_set_text(title, "ESP32-P4 Function EV Board - touch to draw!"); + lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 8); + + status_label = lv_label_create(lv_screen_active()); + lv_obj_set_style_text_align(status_label, LV_TEXT_ALIGN_LEFT, 0); + lv_obj_align(status_label, LV_ALIGN_TOP_LEFT, 8, 40); + + initialize_circle_layer(board.display_width(), board.display_height()); + lv_obj_set_scrollbar_mode(lv_screen_active(), LV_SCROLLBAR_MODE_OFF); + lv_obj_clear_flag(lv_screen_active(), LV_OBJ_FLAG_SCROLLABLE); + if (circle_layer) { + lv_obj_move_foreground(circle_layer); + } + } + + // Cycle the display rotation (0 -> 90 -> 180 -> 270) and resize the background + // and circle layers to match the new orientation. Static so the (non-capturing) + // button event callback below can call it; it captures app_main locals by + // reference, which is safe because app_main never returns. + 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_set_rotation(lv_display_get_default(), rotation); + lv_obj_set_size(bg, board.rotated_display_width(), board.rotated_display_height()); + 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); + } + // re-align the labels to the (now reoriented) screen edges + lv_obj_align(title, LV_ALIGN_TOP_MID, 0, 8); + if (status_label) { + lv_obj_align(status_label, LV_ALIGN_TOP_LEFT, 8, 40); + } + }; + + { + std::lock_guard lock(lvgl_mutex); + lv_obj_t *rotate_btn = lv_btn_create(lv_screen_active()); + lv_obj_set_size(rotate_btn, 50, 50); + lv_obj_align(rotate_btn, LV_ALIGN_TOP_RIGHT, -8, 8); + lv_obj_t *btn_label = lv_label_create(rotate_btn); + lv_label_set_text(btn_label, LV_SYMBOL_REFRESH); + lv_obj_align(btn_label, LV_ALIGN_CENTER, 0, 0); + lv_obj_add_event_cb( + rotate_btn, [](lv_event_t *) { rotate_display(); }, LV_EVENT_CLICKED, nullptr); + } + + // Touch: draw a circle wherever the screen is touched, and play a click on + // each new touch-down. play_audio() is non-blocking, and the click is gated to + // the touch-down edge so it doesn't retrigger every poll while held/dragging. + static std::atomic touch_x{0}, touch_y{0}, touch_n{0}; + board.initialize_touch([&](const auto &data) { + static int prev_touch_n = 0; + auto td = board.touchpad_convert(data); + touch_n = td.num_touch_points; + touch_x = td.x; + touch_y = td.y; + if (td.num_touch_points > 0) { + if (prev_touch_n == 0 && !audio_bytes.empty()) { + board.play_audio(audio_bytes); // non-blocking + } + std::lock_guard lock(lvgl_mutex); + draw_circle(td.x, td.y, 10); + } + prev_touch_n = td.num_touch_points; + }); + + // Run the LVGL task handler periodically + espp::Task lv_task({.callback = [](std::mutex &m, std::condition_variable &cv) -> bool { + { + std::lock_guard lock(lvgl_mutex); + lv_task_handler(); + } + std::unique_lock lock(m); + cv.wait_for(lock, 16ms); + return false; + }, + .task_config = {.name = "lvgl", .stack_size_bytes = 8192}}); + lv_task.start(); + + // Connectivity self-test: once we have an IP (and a moment for RTPS discovery), + // ping the gateway and the discovered peer once, then stop. This makes it easy + // to tell board-vs-network problems apart (e.g. gateway reachable but peer not + // => client isolation / L2 reachability problem, not the board). + espp::Task ping_task( + espp::Task::Config{.callback = [&logger](std::mutex &m, std::condition_variable &cv) -> bool { + if (!have_ip) { + std::unique_lock lk(m); + cv.wait_for(lk, 250ms); + return false; // keep waiting for an IP + } + // give RTPS discovery a few seconds to find the peer + { + std::unique_lock lk(m); + cv.wait_for(lk, 4s); + } + logger.info("=== Connectivity self-test (ping) ==="); + esp_netif_ip_info_t ip_info{}; + if (auto *netif = esp_netif_get_default_netif()) { + esp_netif_get_ip_info(netif, &ip_info); + } + char gw[16] = {0}; + esp_ip4addr_ntoa(&ip_info.gw, gw, sizeof(gw)); + ping_target(logger, "gateway", gw); + std::string peer; + { + std::lock_guard lk(g_peer_mutex); + peer = g_peer_addr; + } + if (!peer.empty()) { + ping_target(logger, "peer", peer); + } else { + logger.warn("Ping self-test: no RTPS peer discovered yet to ping"); + } + logger.info("=== Connectivity self-test done ==="); + return true; // one-shot + }, + .task_config = {.name = "ping-test", .stack_size_bytes = 8192}}); + ping_task.start(); + //! [esp32 p4 function ev board example] + + // Once we have an IP, start an RTPS participant that publishes a counter. + // The display status is refreshed at 50 Hz while RTPS publishes at 2 Hz. + static bool did_have_ip = false; + std::shared_ptr participant = nullptr; + const std::string topic = "espp/test/counter"; + const std::string rtps_type = "std_msgs::msg::dds_::UInt32_"; + uint32_t value = 0; + bool published = false; + const int64_t start_us = esp_timer_get_time(); + static constexpr auto status_period = 20ms; // 50 Hz display status update + static constexpr int64_t publish_period_us = 500'000; // 2 Hz RTPS publish + int64_t last_publish_us = 0; + + while (true) { + const int64_t now_us = esp_timer_get_time(); + + // (Re)start the RTPS participant when the Ethernet link comes up. + if (!did_have_ip && have_ip) { + did_have_ip = true; + std::string address = ip_str; + logger.info("Got IP {}, starting RTPS participant", address); + participant = std::make_shared(espp::RtpsParticipant::Config{ + .node_name = "espp_publisher", + .participant_id = 10, + .advertised_address = address, + .announce_period = 500ms, + .on_participant_discovered = + [&logger](const auto &p) { + { + std::lock_guard lk(g_peer_mutex); + if (g_peer_addr.empty()) { + g_peer_addr = p.address; + } + } + logger.info("discovered participant at {}", p.address); + }, + .on_endpoint_discovered = + [&logger](const auto &endpoint) { + logger.info("discovered {} '{}'", endpoint.is_reader ? "reader" : "writer", + endpoint.topic_name); + }, + }); + participant->add_writer({ + .topic_name = topic, + .type_name = rtps_type, + }); + if (!participant->start()) { + logger.error("Failed to start participant (is multicast networking available?)"); + participant.reset(); + } + value = 0; + last_publish_us = now_us; + } else if (did_have_ip && !have_ip) { + logger.warn("Lost IP, stopping RTPS participant"); + participant.reset(); + did_have_ip = false; + } + + // Publish the next counter value at 2 Hz (independent of the status refresh). + // Only publish if there is a discovered peer (otherwise the publish() call will return false). + bool publish_period_elapsed = (now_us - last_publish_us) >= publish_period_us; + bool can_publish = + participant && !participant->discovered_participants().empty() && publish_period_elapsed; + if (can_publish) { + last_publish_us = now_us; + published = participant->publish(topic, serialize_uint32(value)); + if (published) { + logger.info("published {}", value); + ++value; + } else { + logger.warn("publish {} failed (no discovered peers)", value); + } + } + + // Build and show the on-screen status at 50 Hz. + size_t free_internal = heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024; + size_t free_psram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM) / 1024; + int uptime_s = static_cast((now_us - start_us) / 1'000'000); + std::string rtps_text = + participant ? ("publishing '" + topic + "' #" + std::to_string(value) + + (published ? "" : " (no peers)")) + : (have_ip ? std::string("not started") : std::string("waiting for network")); + std::string status = + "Panel: " + std::string(board.get_display_controller_name()) + " (" + + std::to_string(board.display_width()) + "x" + std::to_string(board.display_height()) + + ")\n" + "Touch: " + std::to_string(touch_n.load()) + " pts (" + + std::to_string(touch_x.load()) + ", " + std::to_string(touch_y.load()) + ")\n" + + "SD card: " + (sd_ok ? std::to_string(sd_size_mb) + " MB" : "none") + "\n" + + "Ethernet: " + (have_ip ? ip_str : std::string("(no link)")) + "\n" + + "RTPS: " + rtps_text + "\n" + "System: " + std::to_string(free_internal) + + " KB int, " + std::to_string(free_psram) + " KB psram free, up " + + std::to_string(uptime_s) + " s"; + + { + std::lock_guard lock(lvgl_mutex); + if (status_label) { + lv_label_set_text(status_label, status.c_str()); + } + } + + std::this_thread::sleep_for(status_period); + } +} + +////////////////////////////////////////////////////////////////////////////// +// LVGL circle-drawing helpers (a transparent full-screen layer with a custom +// draw callback that renders the ring buffer of recent touch points). +////////////////////////////////////////////////////////////////////////////// +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 (!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) { + if (circle.visible) { + invalidate_circle_area(circle); + } + circle.visible = false; + } + next_circle_index = 0; + visible_circle_count = 0; +} + +////////////////////////////////////////////////////////////////////////////// +// Load the embedded click.wav (stripping the 44-byte WAV header) and report its +// size and sample rate. +////////////////////////////////////////////////////////////////////////////// +static bool load_audio(size_t &out_size, size_t &out_sample_rate) { + if (!audio_bytes.empty()) { + out_size = audio_bytes.size(); + return true; + } + extern const uint8_t click_wav_start[] asm("_binary_click_wav_start"); + extern const uint8_t click_wav_end[] asm("_binary_click_wav_end"); + audio_bytes = std::vector(click_wav_start, click_wav_end); + if (audio_bytes.size() < 44) { + audio_bytes.clear(); + return false; + } + uint32_t sample_rate = *(reinterpret_cast(&audio_bytes[24])); + audio_bytes.erase(audio_bytes.begin(), audio_bytes.begin() + 44); + out_size = audio_bytes.size(); + out_sample_rate = sample_rate; + return true; +} diff --git a/components/esp32-p4-function-ev-board/example/partitions.csv b/components/esp32-p4-function-ev-board/example/partitions.csv new file mode 100644 index 000000000..ff7429511 --- /dev/null +++ b/components/esp32-p4-function-ev-board/example/partitions.csv @@ -0,0 +1,5 @@ +# Name, Type, SubType, Offset, Size +nvs, data, nvs, 0xa000, 0x6000 +phy_init, data, phy, , 0x1000 +factory, app, factory, , 4M +littlefs, data, littlefs, , 4M diff --git a/components/esp32-p4-function-ev-board/example/sdkconfig.defaults b/components/esp32-p4-function-ev-board/example/sdkconfig.defaults new file mode 100644 index 000000000..6d3f1dcf8 --- /dev/null +++ b/components/esp32-p4-function-ev-board/example/sdkconfig.defaults @@ -0,0 +1,56 @@ +# ESP32-P4 specific configuration +CONFIG_IDF_TARGET="esp32p4" +# for ESP-IDF >= v6.0, we have to set this to allow older boards (e.g. this dev +# board) to work with the newer ESP-IDF since it's using an older p4 chip +# revision +CONFIG_ESP32P4_SELECTS_REV_LESS_V3=y + +# CONFIG_COMPILER_OPTIMIZATION_PERF=y + +CONFIG_ESPTOOLPY_FLASHMODE_QIO=y +CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y +CONFIG_ESPTOOLPY_FLASHSIZE="16MB" + +# Memory configuration +CONFIG_ESP_MAIN_TASK_STACK_SIZE=32768 +CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 +CONFIG_ESP_TIMER_TASK_STACK_SIZE=6144 + +# Ensure eth is enabled +CONFIG_ETH_ENABLED=y +CONFIG_ETH_USE_ESP32_EMAC=y + +# +# Partition Table +# +CONFIG_PARTITION_TABLE_CUSTOM=y +CONFIG_PARTITION_TABLE_OFFSET=0x9000 +CONFIG_PARTITION_TABLE_CUSTOM_FILENAME="partitions.csv" + +# FreeRTOS +CONFIG_FREERTOS_HZ=1000 + +# Copy received Ethernet frames into a fresh lwip pbuf instead of referencing the +# EMAC DMA buffer directly. This avoids cache-coherency issues with the EMAC RX +# DMA on the ESP32-P4 (which can silently drop/corrupt unicast frames while +# broadcast/multicast appear to work, e.g. DHCP succeeds but ping fails). +# CONFIG_LWIP_L2_TO_L3_COPY=y + +# PSRAM (required for the MIPI-DSI frame buffers) +CONFIG_SPIRAM=y +CONFIG_SPIRAM_SPEED_200M=y +CONFIG_SPIRAM_XIP_FROM_PSRAM=y +CONFIG_CACHE_L2_CACHE_256KB=y +CONFIG_CACHE_L2_CACHE_LINE_128B=y + +# LVGL configuration +CONFIG_LV_DEF_REFR_PERIOD=16 +CONFIG_LV_USE_THEME_DEFAULT=y +CONFIG_LV_THEME_DEFAULT_DARK=y +CONFIG_LV_THEME_DEFAULT_GROW=y +CONFIG_LV_THEME_DEFAULT_TRANSITION_TIME=30 + +# ESP32-P4 Function EV Board BSP configuration +CONFIG_ESP_P4_EV_BOARD_INTERRUPT_STACK_SIZE=4096 +CONFIG_ESP_P4_EV_BOARD_TOUCH_TASK_STACK_SIZE=4096 +CONFIG_ESP_P4_EV_BOARD_AUDIO_TASK_STACK_SIZE=8192 diff --git a/components/esp32-p4-function-ev-board/idf_component.yml b/components/esp32-p4-function-ev-board/idf_component.yml new file mode 100644 index 000000000..932470db4 --- /dev/null +++ b/components/esp32-p4-function-ev-board/idf_component.yml @@ -0,0 +1,31 @@ +## IDF Component Manager Manifest File +license: "MIT" +description: "ESP32-P4 Function EV Board (+ ESP32-P4-HMI-Subboard) Board Support Package (BSP) component in C++" +url: "https://github.com/esp-cpp/espp/tree/main/components/esp32-p4-function-ev-board" +repository: "git://github.com/esp-cpp/espp.git" +maintainers: + - William Emfinger +documentation: "https://esp-cpp.github.io/espp/esp32_p4_function_ev_board.html" +examples: + - path: example +tags: + - cpp + - Component + - BSP + - ESP32-P4 + - EV-Board + - HMI +dependencies: + idf: ">=5.3" + espp/base_component: ">=1.0" + espp/codec: ">=1.0" + espp/display: ">=1.0" + espp/display_drivers: ">=1.0" + espp/gt911: ">=1.0" + espp/i2c: ">=1.0" + espp/input_drivers: ">=1.0" + espp/interrupt: ">=1.0" + espp/led: ">=1.0" + espp/task: ">=1.0" +targets: + - esp32p4 diff --git a/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp b/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp new file mode 100644 index 000000000..5ceb5edf4 --- /dev/null +++ b/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp @@ -0,0 +1,560 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +#include +#include +#include + +#if CONFIG_ESP_P4_EV_BOARD_ETHERNET +#include +#include +#endif + +#include +#include + +#include "base_component.hpp" +#include "display.hpp" +#include "display_drivers.hpp" +#include "ek79007.hpp" +#include "es8311.hpp" +#include "gt911.hpp" +#include "i2c.hpp" +#include "ili9881.hpp" +#include "interrupt.hpp" +#include "led.hpp" +#include "task.hpp" +#include "touchpad_input.hpp" + +namespace espp { +/// @brief Board Support Package (BSP) for the Espressif ESP32-P4 Function EV +/// Board used with the ESP32-P4-HMI-Subboard. +/// +/// This class provides a singleton interface to the board's peripherals: +/// - MIPI-DSI display (Kconfig-selectable EK79007 1024x600 or ILI9881C 800x1280) +/// with a GT911 capacitive multi-touch controller +/// - ES8311 audio codec (+ NS4150B speaker amplifier) over I2S +/// - 10/100 Ethernet (EMAC + IP101 RMII PHY) +/// - microSD card (4-bit SDMMC) +/// - MIPI-CSI camera (SC2336/OV5647) — pins wired, capture pipeline is a stub +/// +/// \note The BOOT button cannot be used simultaneously with the ethernet PHY, +/// since the BOOT button is connected to the PHY's RMII_TXD1 pin. If you +/// need to use the BOOT button, you must disable the ethernet PHY. +/// +/// All on-board control lines are direct ESP32-P4 GPIOs (this board has no I/O +/// expander). The display, touch, and audio codec share a single I2C bus +/// (SDA=GPIO7, SCL=GPIO8). The touch interrupt is not routed to the ESP32-P4 on +/// this board, so touch is polled. +/// +/// The class is a singleton and can be accessed using the get() method. +/// +/// \section esp32_p4_function_ev_board_example Example +/// \snippet esp32_p4_function_ev_board_example.cpp esp32 p4 function ev board example +class Esp32P4FunctionEvBoard : public BaseComponent { +public: + /// Alias for the button callback function + using button_callback_t = espp::Interrupt::event_callback_fn; + + /// Alias for the pixel type used by the display + using Pixel = lv_color16_t; + + /// Alias for the low-level display driver interface + using DisplayDriver = espp::display_drivers::Controller; + + /// Alias for the GT911 touch controller + using TouchDriver = espp::Gt911; + + /// Alias for the touchpad data + using TouchpadData = espp::TouchpadData; + + /// Alias for the touch callback when touch events are received + using touch_callback_t = std::function; + + /// Enum for the display controller type (selected via Kconfig) + enum class DisplayController { UNKNOWN, EK79007, ILI9881C }; + + /// Mount point for the uSD card + static constexpr char mount_point[] = "/sdcard"; + + /// @brief Access the singleton instance + /// @return Reference to the singleton instance + static Esp32P4FunctionEvBoard &get() { + static Esp32P4FunctionEvBoard instance; + return instance; + } + + Esp32P4FunctionEvBoard(const Esp32P4FunctionEvBoard &) = delete; + Esp32P4FunctionEvBoard &operator=(const Esp32P4FunctionEvBoard &) = delete; + Esp32P4FunctionEvBoard(Esp32P4FunctionEvBoard &&) = delete; + Esp32P4FunctionEvBoard &operator=(Esp32P4FunctionEvBoard &&) = delete; + + /// Get a reference to the internal I2C bus + /// \return A reference to the internal I2C bus + /// \note Shared by the GT911 touch, ES8311 codec, and camera SCCB + I2c &internal_i2c() { return internal_i2c_; } + + /// Get a reference to the interrupts + /// \return A reference to the interrupts + espp::Interrupt &interrupts() { return interrupts_; } + + /// Get the display controller type for the configured panel + /// \return The display controller type + DisplayController get_display_controller() const { return display_controller_; } + + /// Get a string name for the configured display controller + /// \return String name of the controller + const char *get_display_controller_name() const { + switch (display_controller_) { + case DisplayController::EK79007: + return "EK79007"; + case DisplayController::ILI9881C: + return "ILI9881C"; + default: + return "Unknown"; + } + } + + ///////////////////////////////////////////////////////////////////////////// + // Display & Touchpad + ///////////////////////////////////////////////////////////////////////////// + + /// Initialize the LCD (MIPI-DSI + configured panel driver) + /// \return true if the LCD was successfully initialized, false otherwise + bool initialize_lcd(); + + /// Initialize the LVGL display + /// \param pixel_buffer_size The size of the pixel buffer, in pixels. If 0, a + /// default based on the detected panel width is used. + /// \return true if the display was successfully initialized, false otherwise + bool initialize_display(size_t pixel_buffer_size = 0); + + /// Initialize the GT911 multi-touch controller + /// \param callback The touchpad callback + /// \return true if the touchpad was successfully initialized, false otherwise + /// \note The touch interrupt is not routed on this board, so a polling task is + /// used to read the GT911 and invoke the callback. + bool initialize_touch(const touch_callback_t &callback = nullptr); + + /// Get the number of bytes per pixel for the display + /// \return The number of bytes per pixel + size_t bytes_per_pixel() const { return sizeof(Pixel); } + + /// Get the touchpad input + /// \return A shared pointer to the touchpad input + std::shared_ptr touchpad_input() const { return touchpad_input_; } + + /// Get the most recent touchpad data + /// \return The touchpad data + TouchpadData touchpad_data() const { return touchpad_data_; } + + /// Get the touchpad data for LVGL integration + /// \param num_touch_points The number of touch points + /// \param x The x coordinate + /// \param y The y coordinate + /// \param btn_state The button state (0 = released, 1 = pressed) + void touchpad_read(uint8_t *num_touch_points, uint16_t *x, uint16_t *y, uint8_t *btn_state); + + /// Convert touchpad data from raw reading to display coordinates + /// \param data The touchpad data to convert + /// \return The converted touchpad data + TouchpadData touchpad_convert(const TouchpadData &data) const; + + /// Set the display brightness + /// \param brightness The brightness as a percentage (0-100) + void brightness(float brightness); + + /// Get the display brightness + /// \return The brightness as a percentage (0-100) + float brightness() const; + + /// Get the display width in pixels (of the detected/active panel) + /// \return The display width in pixels + /// \note Valid after initialize_lcd() has detected the panel. + size_t display_width() const { return display_width_; } + + /// Get the display height in pixels (of the detected/active panel) + /// \return The display height in pixels + /// \note Valid after initialize_lcd() has detected the panel. + size_t display_height() const { return display_height_; } + + /// Get 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 + size_t rotated_display_height() const; + + /// Get a shared pointer to the low-level display driver + /// \return A shared pointer to the display driver + const std::shared_ptr &display_driver() const { return display_driver_; } + + /// Write lines to the LCD + /// \note This method queues the panel transfer asynchronously. + void write_lcd_lines(int xs, int ys, int xe, int ye, const uint8_t *data, uint32_t user_data); + + ///////////////////////////////////////////////////////////////////////////// + // Audio System (ES8311 + NS4150B) + ///////////////////////////////////////////////////////////////////////////// + + /// Initialize the audio system (ES8311 codec) + /// \param sample_rate The audio sample rate in Hz (default 48kHz) + /// \param task_config The task configuration for the audio task + /// \return true if the audio system was successfully initialized + bool initialize_audio(uint32_t sample_rate = 48000, + const espp::Task::BaseConfig &task_config = { + .name = "p4_ev_audio", + .stack_size_bytes = CONFIG_ESP_P4_EV_BOARD_AUDIO_TASK_STACK_SIZE, + .priority = 20, + .core_id = 0}); + + /// Enable or disable the speaker amplifier (NS4150B PA on GPIO53) + /// \param enable True to enable the amplifier, false to disable + void set_speaker_enabled(bool enable); + + /// Set the audio volume + /// \param volume The volume as a percentage (0-100) + void volume(float volume); + + /// Get the audio volume + /// \return The volume as a percentage (0-100) + float volume() const; + + /// Mute or unmute the audio + /// \param mute True to mute, false to unmute + void mute(bool mute); + + /// Check if audio is muted + /// \return True if muted, false otherwise + bool is_muted() const; + + /// Get the audio sample rate + /// \return The audio sample rate, in Hz + uint32_t audio_sample_rate() const; + + /// Set the audio sample rate + /// \param sample_rate The audio sample rate, in Hz + void audio_sample_rate(uint32_t sample_rate); + + /// Get the audio buffer size, in bytes + /// \return The audio buffer size, in bytes + size_t audio_buffer_size() const; + + /// Play audio data + /// \param data The audio data to play + /// \param num_bytes The number of bytes to play + void play_audio(const uint8_t *data, uint32_t num_bytes); + + /// Play audio data + /// \param data The audio data to play + void play_audio(std::span data); + + ///////////////////////////////////////////////////////////////////////////// + // uSD Card (4-bit SDMMC) + ///////////////////////////////////////////////////////////////////////////// + + /// Configuration for the uSD card + struct SdCardConfig { + bool format_if_mount_failed = false; ///< Format the uSD card if mount failed + int max_files = 5; ///< The maximum number of files to open at once + size_t allocation_unit_size = 2 * 1024; ///< The allocation unit size in bytes + }; + + /// Initialize microSD / uSD card + /// \param config Configuration for the uSD card + /// \return True if uSD card was successfully initialized + bool initialize_sdcard(const SdCardConfig &config); + + /// Check if SD card is present and mounted + /// \return True if SD card is available + bool is_sd_card_available() const { return sd_card_initialized_; } + + /// Get the uSD card handle + /// \return A pointer to the uSD card, or nullptr if not initialized + sdmmc_card_t *sdcard() const { return sdcard_; } + + /// Get SD card info + /// \param size_mb Pointer to store size in MB + /// \param free_mb Pointer to store free space in MB + /// \return True if info retrieved successfully + bool get_sd_card_info(uint32_t *size_mb, uint32_t *free_mb) const; + +#if CONFIG_ESP_P4_EV_BOARD_ETHERNET + ///////////////////////////////////////////////////////////////////////////// + // Ethernet (EMAC + IP101 RMII PHY) + ///////////////////////////////////////////////////////////////////////////// + + /// Callback invoked when the Ethernet link goes up (with the assigned IP) + using ethernet_link_callback_t = std::function; + + /// Initialize the Ethernet interface (EMAC + IP101 RMII PHY, DHCP client) + /// \param on_link_up Optional callback invoked when an IP address is acquired + /// \return True if Ethernet was successfully initialized and started + /// \note Requires the ESP-IDF default event loop. The BSP creates it if needed. + bool initialize_ethernet(const ethernet_link_callback_t &on_link_up = nullptr); + + /// Check whether the Ethernet link is up (cable connected + negotiated) + /// \return True if the link is up + bool is_ethernet_connected() const { return ethernet_connected_; } + + /// Get the most recently acquired IPv4 address (0 if none) + /// \return The IPv4 address + esp_ip4_addr_t ethernet_ip() const { return ethernet_ip_; } +#endif // CONFIG_ESP_P4_EV_BOARD_ETHERNET + + ///////////////////////////////////////////////////////////////////////////// + // Camera (MIPI-CSI) — pins wired, capture pipeline is a stub + ///////////////////////////////////////////////////////////////////////////// + + /// Initialize the MIPI-CSI camera (SC2336/OV5647). + /// \return True if successful + /// \note Not yet implemented — the camera pins/SCCB are documented in this + /// BSP but the esp_video capture pipeline is not wired up. This always + /// returns false for now. + bool initialize_camera(); + + ///////////////////////////////////////////////////////////////////////////// + // Button (BOOT) + ///////////////////////////////////////////////////////////////////////////// + + /// Initialize the BOOT button + /// \param callback The callback function to call when pressed/released + /// \return True if the button was successfully initialized + bool initialize_button(const button_callback_t &callback = nullptr); + + /// Get the button state + /// \return True if pressed, false otherwise + bool button_state() const; + +protected: + Esp32P4FunctionEvBoard(); + + bool update_touch(); + bool audio_task_callback(std::mutex &m, std::condition_variable &cv, bool &task_notified); + + ///////////////////////////////////////////////////////////////////////////// + // Display geometry / per-panel parameters + ///////////////////////////////////////////////////////////////////////////// + // Per-panel parameters (geometry, DPI clock, backlight + reset GPIOs, and the + // DPI video timing porches). The active panel is detected at runtime (see + // detect_display_controller()), falling back to the Kconfig-selected default. + struct PanelParams { + size_t width; + size_t height; + int dpi_clock_freq_mhz; + 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 + static constexpr PanelParams EK79007_PARAMS{1024, 600, 52, GPIO_NUM_26, GPIO_NUM_27, 10, + 160, 160, 1, 23, 12}; + // ILI9881C 10.1" 800x1280 (hsync: pulse=40, back=140, front=40) + static constexpr PanelParams ILI9881C_PARAMS{800, 1280, 80, GPIO_NUM_23, GPIO_NUM_NC, 40, + 140, 40, 4, 16, 16}; + +#if CONFIG_ESP_P4_EV_BOARD_DISPLAY_ILI9881C + static constexpr DisplayController default_controller_ = DisplayController::ILI9881C; +#else + static constexpr DisplayController default_controller_ = DisplayController::EK79007; +#endif + + // Runtime display geometry, set from the detected/configured panel. + PanelParams panel_params_{default_controller_ == DisplayController::ILI9881C ? ILI9881C_PARAMS + : EK79007_PARAMS}; + size_t display_width_{panel_params_.width}; + size_t display_height_{panel_params_.height}; + + /// Apply the parameters (geometry/timing/GPIOs) for the given controller. + /// \note The panel is selected via Kconfig; the ESP32-P4-HMI-Subboard does not + /// provide a way to safely auto-probe it (the EK79007 does not answer + /// DSI reads), matching Espressif's own esp-bsp which is Kconfig-driven. + void apply_panel_params(DisplayController controller); + + // MIPI-DSI common parameters + static constexpr int mipi_dsi_lanes = 2; + static constexpr int mipi_dsi_lane_bitrate_mbps = 1000; + 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; + + static constexpr bool backlight_value = true; + static constexpr bool invert_colors = false; + static constexpr auto rotation = espp::DisplayRotation::LANDSCAPE; + static constexpr bool swap_color_order = false; + // Panel is used in its native orientation; no display mirror/swap. + static constexpr bool mirror_x = false; + static constexpr bool mirror_y = false; + static constexpr bool swap_xy = false; + // touch -> display coordinate conversion. The esp-bsp GT911 config on this + // board mirrors x and y, so invert both axes here. 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; + + ///////////////////////////////////////////////////////////////////////////// + // Internal I2C bus (GT911 touch 0x5D/0x14, ES8311 codec 0x18, camera SCCB) + ///////////////////////////////////////////////////////////////////////////// + static constexpr auto internal_i2c_port = I2C_NUM_0; + static constexpr auto internal_i2c_clock_speed = 400 * 1000; + static constexpr gpio_num_t internal_i2c_sda = GPIO_NUM_7; + static constexpr gpio_num_t internal_i2c_scl = GPIO_NUM_8; + + // Touch (GT911) — interrupt/reset are NOT connected on this board + static constexpr uint8_t gt911_default_address = 0x5D; + static constexpr uint8_t gt911_backup_address = 0x14; + + ///////////////////////////////////////////////////////////////////////////// + // Audio (ES8311 + NS4150B), I2S peripheral + ///////////////////////////////////////////////////////////////////////////// + static constexpr uint8_t es8311_i2c_address = 0x18; + static constexpr auto audio_i2s_port = I2S_NUM_0; + static constexpr gpio_num_t audio_mclk_io = GPIO_NUM_13; // MCLK + static constexpr gpio_num_t audio_sclk_io = GPIO_NUM_12; // BCLK + static constexpr gpio_num_t audio_lrck_io = GPIO_NUM_10; // WS/LRCK + static constexpr gpio_num_t audio_dout_io = GPIO_NUM_9; // P4 -> codec DSDIN + static constexpr gpio_num_t audio_din_io = GPIO_NUM_11; // codec ASDOUT -> P4 + static constexpr gpio_num_t audio_pa_enable_io = GPIO_NUM_53; // NS4150B enable + + ///////////////////////////////////////////////////////////////////////////// + // microSD (4-bit SDMMC, slot 0, fixed IO-MUX pins). Powered via on-chip LDO_VO4. + ///////////////////////////////////////////////////////////////////////////// + static constexpr int sd_ldo_channel = 4; + static constexpr gpio_num_t sd_clk_io = GPIO_NUM_43; + static constexpr gpio_num_t sd_cmd_io = GPIO_NUM_44; + static constexpr gpio_num_t sd_d0_io = GPIO_NUM_39; + static constexpr gpio_num_t sd_d1_io = GPIO_NUM_40; + static constexpr gpio_num_t sd_d2_io = GPIO_NUM_41; + static constexpr gpio_num_t sd_d3_io = GPIO_NUM_42; + + ///////////////////////////////////////////////////////////////////////////// + // Camera (MIPI-CSI) — SCCB shares the internal I2C bus; reset/xclk not connected + ///////////////////////////////////////////////////////////////////////////// + static constexpr gpio_num_t camera_reset_io = GPIO_NUM_NC; + static constexpr gpio_num_t camera_xclk_io = GPIO_NUM_NC; + + // USB OTG (documented; USB stack not initialized by the BSP) + static constexpr gpio_num_t usb_dp_io = GPIO_NUM_20; + static constexpr gpio_num_t usb_dn_io = GPIO_NUM_19; + + // BOOT button (strapping pin). NOTE: GPIO35 is also Ethernet RMII TXD1, so the + // button cannot be used as a runtime input while Ethernet is enabled (claiming + // it as a GPIO would take down Ethernet TX). initialize_button() refuses to run + // while Ethernet is up. + static constexpr gpio_num_t button_io = GPIO_NUM_35; + + // Audio buffer sizing + static constexpr int NUM_CHANNELS = 2; + static constexpr int NUM_BYTES_PER_CHANNEL = 2; + static constexpr int UPDATE_FREQUENCY = 60; + static constexpr int calc_audio_buffer_size(int sample_rate) { + return sample_rate * NUM_CHANNELS * NUM_BYTES_PER_CHANNEL / UPDATE_FREQUENCY; + } + + ///////////////////////////////////////////////////////////////////////////// + // Member variables + ///////////////////////////////////////////////////////////////////////////// + I2c internal_i2c_{{.port = internal_i2c_port, + .sda_io_num = internal_i2c_sda, + .scl_io_num = internal_i2c_scl, + .sda_pullup_en = GPIO_PULLUP_ENABLE, + .scl_pullup_en = GPIO_PULLUP_ENABLE, + .clk_speed = internal_i2c_clock_speed}}; + + // Interrupts (button) + espp::Interrupt::PinConfig button_interrupt_pin_{ + .gpio_num = button_io, + .callback = + [this](const auto &event) { + if (button_callback_) { + button_callback_(event); + } + }, + .active_level = espp::Interrupt::ActiveLevel::LOW, + .interrupt_type = espp::Interrupt::Type::ANY_EDGE, + .pullup_enabled = true}; + + espp::Interrupt interrupts_{ + {.interrupts = {}, + .task_config = {.name = "p4-ev interrupts", + .stack_size_bytes = CONFIG_ESP_P4_EV_BOARD_INTERRUPT_STACK_SIZE}}}; + + // Touch + std::shared_ptr> touch_i2c_device_; + std::shared_ptr touch_driver_; + std::shared_ptr touchpad_input_; + std::recursive_mutex touchpad_data_mutex_; + TouchpadData touchpad_data_; + touch_callback_t touch_callback_{nullptr}; + std::unique_ptr touch_task_{nullptr}; + + // Button + button_callback_t button_callback_{nullptr}; + + // Audio + std::atomic audio_initialized_{false}; + std::atomic volume_{50.0f}; + std::atomic mute_{false}; + std::shared_ptr> es8311_i2c_device_; + std::unique_ptr audio_task_{nullptr}; + i2s_chan_handle_t audio_tx_handle{nullptr}; + i2s_std_config_t audio_std_cfg{}; + i2s_event_callbacks_t audio_tx_callbacks_{}; + std::vector audio_tx_buffer; + StreamBufferHandle_t audio_tx_stream{nullptr}; + std::atomic has_sound{false}; + + // uSD card + std::atomic sd_card_initialized_{false}; + sdmmc_card_t *sdcard_{nullptr}; + void *sd_pwr_ctrl_handle_{nullptr}; + +#if CONFIG_ESP_P4_EV_BOARD_ETHERNET + // Ethernet + std::atomic ethernet_initialized_{false}; + std::atomic ethernet_connected_{false}; + esp_ip4_addr_t ethernet_ip_{}; + ethernet_link_callback_t ethernet_link_callback_{nullptr}; + esp_eth_handle_t eth_handle_{nullptr}; + void *eth_glue_{nullptr}; // esp_eth_netif_glue_handle_t + esp_netif_t *eth_netif_{nullptr}; + static void ethernet_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, + void *event_data); + static void ethernet_got_ip_handler(void *arg, esp_event_base_t event_base, int32_t event_id, + void *event_data); +#endif + + // Display state + std::shared_ptr> display_; + std::shared_ptr display_driver_{static_cast(nullptr)}; + std::shared_ptr backlight_; + std::vector backlight_channel_configs_; + struct LcdHandles { + esp_lcd_dsi_bus_handle_t mipi_dsi_bus{nullptr}; + esp_lcd_panel_io_handle_t io{nullptr}; + esp_lcd_panel_handle_t panel{nullptr}; + } lcd_handles_{}; + DisplayController display_controller_{DisplayController::UNKNOWN}; + + void flush(lv_display_t *disp, const lv_area_t *area, uint8_t *px_map); + static bool notify_lvgl_flush_ready(esp_lcd_panel_handle_t panel, + esp_lcd_dpi_panel_event_data_t *edata, void *user_ctx); + + // DSI command helpers (used by the panel drivers) + void dsi_write_command(uint8_t cmd, std::span params, uint32_t flags); + void dsi_read_command(uint8_t cmd, std::span data, uint32_t flags); +}; // class Esp32P4FunctionEvBoard +} // namespace espp diff --git a/components/esp32-p4-function-ev-board/src/audio.cpp b/components/esp32-p4-function-ev-board/src/audio.cpp new file mode 100644 index 000000000..1f9dd137e --- /dev/null +++ b/components/esp32-p4-function-ev-board/src/audio.cpp @@ -0,0 +1,178 @@ +#include "esp32-p4-function-ev-board.hpp" + +#include +#include + +#include + +#include "es8311.hpp" + +namespace espp { + +bool Esp32P4FunctionEvBoard::initialize_audio(uint32_t sample_rate, + const espp::Task::BaseConfig &task_config) { + logger_.info("Initializing audio (ES8311) at {} Hz", sample_rate); + + if (audio_initialized_) { + logger_.warn("Audio already initialized"); + return true; + } + + // Configure the speaker-amplifier (NS4150B) enable GPIO + gpio_config_t pa_cfg{}; + pa_cfg.pin_bit_mask = 1ULL << static_cast(audio_pa_enable_io); + pa_cfg.mode = GPIO_MODE_OUTPUT; + gpio_config(&pa_cfg); + set_speaker_enabled(false); + + std::error_code ec; + es8311_i2c_device_ = internal_i2c_.add_device( + { + .device_address = es8311_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 (!es8311_i2c_device_) { + logger_.error("Could not initialize ES8311 I2C device: {}", ec.message()); + return false; + } + + // Wire codec register access over the internal I2C bus + set_es8311_write(espp::make_i2c_addressed_write(es8311_i2c_device_)); + set_es8311_read(espp::make_i2c_addressed_read_register(es8311_i2c_device_)); + + // Create the I2S standard channel for playback (TX). MCLK = 256 * fs (default). + i2s_chan_config_t chan_cfg = I2S_CHANNEL_DEFAULT_CONFIG(audio_i2s_port, I2S_ROLE_MASTER); + chan_cfg.auto_clear = true; + if (i2s_new_channel(&chan_cfg, &audio_tx_handle, nullptr) != ESP_OK) { + logger_.error("Failed to create I2S channel"); + return false; + } + + audio_std_cfg = { + .clk_cfg = I2S_STD_CLK_DEFAULT_CONFIG(sample_rate), + .slot_cfg = I2S_STD_PHILIP_SLOT_DEFAULT_CONFIG(I2S_DATA_BIT_WIDTH_16BIT, I2S_SLOT_MODE_MONO), + .gpio_cfg = {.mclk = audio_mclk_io, + .bclk = audio_sclk_io, + .ws = audio_lrck_io, + .dout = audio_dout_io, + .din = audio_din_io, + .invert_flags = {.mclk_inv = false, .bclk_inv = false, .ws_inv = false}}, + }; + if (i2s_channel_init_std_mode(audio_tx_handle, &audio_std_cfg) != ESP_OK) { + logger_.error("Failed to init I2S std mode"); + return false; + } + + // Initialize the ES8311 codec for playback (codec is an I2S slave) + audio_hal_codec_config_t es8311_cfg{}; + es8311_cfg.codec_mode = AUDIO_HAL_CODEC_MODE_DECODE; + es8311_cfg.dac_output = AUDIO_HAL_DAC_OUTPUT_ALL; + es8311_cfg.i2s_iface.bits = AUDIO_HAL_BIT_LENGTH_16BITS; + es8311_cfg.i2s_iface.fmt = AUDIO_HAL_I2S_NORMAL; + es8311_cfg.i2s_iface.mode = AUDIO_HAL_MODE_SLAVE; + es8311_cfg.i2s_iface.samples = AUDIO_HAL_48K_SAMPLES; + if (es8311_codec_init(&es8311_cfg) != ESP_OK) { + logger_.error("ES8311 init failed"); + return false; + } + es8311_codec_set_sample_rate(sample_rate); + es8311_codec_set_voice_volume(static_cast(volume_)); + es8311_set_voice_mute(false); + es8311_codec_ctrl_state(AUDIO_HAL_CODEC_MODE_DECODE, AUDIO_HAL_CTRL_START); + + if (i2s_channel_enable(audio_tx_handle) != ESP_OK) { + logger_.error("Failed to enable I2S channel"); + return false; + } + + // The audio task drains this stream buffer to I2S. Size it generously so a + // whole UI sound clip fits and play_audio() can enqueue it without blocking. + auto tx_buf_size = calc_audio_buffer_size(sample_rate); + audio_tx_buffer.resize(tx_buf_size); + audio_tx_stream = xStreamBufferCreate(std::max(tx_buf_size * 4, 64 * 1024), 0); + xStreamBufferReset(audio_tx_stream); + + using namespace std::placeholders; + audio_task_ = espp::Task::make_unique( + {.callback = std::bind(&Esp32P4FunctionEvBoard::audio_task_callback, this, _1, _2, _3), + .task_config = task_config}); + + set_speaker_enabled(true); + audio_initialized_ = true; + return audio_task_->start(); +} + +void Esp32P4FunctionEvBoard::set_speaker_enabled(bool enable) { + gpio_set_level(audio_pa_enable_io, enable ? 1 : 0); +} + +void Esp32P4FunctionEvBoard::volume(float volume) { + volume = std::clamp(volume, 0.0f, 100.0f); + volume_ = volume; + es8311_codec_set_voice_volume(static_cast(volume_)); +} + +float Esp32P4FunctionEvBoard::volume() const { return volume_; } + +void Esp32P4FunctionEvBoard::mute(bool mute) { + mute_ = mute; + es8311_set_voice_mute(mute_); +} + +bool Esp32P4FunctionEvBoard::is_muted() const { return mute_; } + +void Esp32P4FunctionEvBoard::play_audio(const uint8_t *data, uint32_t num_bytes) { + if (!audio_initialized_ || !data || num_bytes == 0) { + return; + } + // Non-blocking: enqueue as much as currently fits in the TX stream buffer for + // the audio task to drain to I2S. This never blocks the caller, so it is safe + // to call from a touch callback / any task. If the buffer is full the excess + // is dropped (the clip is truncated) rather than stalling the caller. + xStreamBufferSend(audio_tx_stream, data, num_bytes, 0); +} + +void Esp32P4FunctionEvBoard::play_audio(std::span data) { + play_audio(data.data(), data.size()); +} + +bool Esp32P4FunctionEvBoard::audio_task_callback(std::mutex &m, std::condition_variable &cv, + bool &task_notified) { + uint16_t available = xStreamBufferBytesAvailable(audio_tx_stream); + int buffer_size = audio_tx_buffer.size(); + available = std::min(available, buffer_size); + uint8_t *tx_buf = audio_tx_buffer.data(); + memset(tx_buf, 0, buffer_size); + if (available == 0) { + 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); + } + return false; +} + +uint32_t Esp32P4FunctionEvBoard::audio_sample_rate() const { + return audio_std_cfg.clk_cfg.sample_rate_hz; +} + +size_t Esp32P4FunctionEvBoard::audio_buffer_size() const { return audio_tx_buffer.size(); } + +void Esp32P4FunctionEvBoard::audio_sample_rate(uint32_t sample_rate) { + // NOTE: this reconfigures the running I2S channel. It is best called when the + // audio task is not actively streaming (e.g. right after initialize_audio, or + // while no audio is playing). To avoid a runtime change entirely, pass the + // desired sample rate to initialize_audio(). The ES8311 is an I2S slave and + // follows the I2S clock, so it does not need a separate codec reconfigure. + logger_.info("Setting audio sample rate to {} Hz", sample_rate); + i2s_channel_disable(audio_tx_handle); + audio_std_cfg.clk_cfg.sample_rate_hz = sample_rate; + i2s_channel_reconfig_std_clock(audio_tx_handle, &audio_std_cfg.clk_cfg); + xStreamBufferReset(audio_tx_stream); + i2s_channel_enable(audio_tx_handle); +} + +} // namespace espp diff --git a/components/esp32-p4-function-ev-board/src/esp32-p4-function-ev-board.cpp b/components/esp32-p4-function-ev-board/src/esp32-p4-function-ev-board.cpp new file mode 100644 index 000000000..7941a9292 --- /dev/null +++ b/components/esp32-p4-function-ev-board/src/esp32-p4-function-ev-board.cpp @@ -0,0 +1,56 @@ +#include "esp32-p4-function-ev-board.hpp" + +#include + +// Peripheral subsystems are split into separate translation units: +// video.cpp - MIPI-DSI display + LVGL +// touchpad.cpp - GT911 touch (polled) +// audio.cpp - ES8311 codec + I2S +// sdcard.cpp - 4-bit SDMMC +// ethernet.cpp - EMAC + IP101 RMII + +namespace espp { + +Esp32P4FunctionEvBoard::Esp32P4FunctionEvBoard() + : BaseComponent("Esp32P4FunctionEvBoard") { + // The display panel is selected at compile time via Kconfig. +#if CONFIG_ESP_P4_EV_BOARD_DISPLAY_ILI9881C + display_controller_ = DisplayController::ILI9881C; +#else + display_controller_ = DisplayController::EK79007; +#endif +} + +bool Esp32P4FunctionEvBoard::initialize_button(const button_callback_t &callback) { + // GPIO35 (button_io) is shared with Ethernet RMII TXD1 on this board. Claiming + // it as a GPIO input would take down Ethernet TX, so refuse rather than break a + // live network: Ethernet (system connectivity) outranks the UI button. Disable + // Ethernet if you need the BOOT button. + if (ethernet_initialized_) { + logger_.error("BOOT button shares GPIO{} with Ethernet RMII TXD1; refusing to " + "initialize it while Ethernet is up (it would kill Ethernet TX).", + static_cast(button_io)); + return false; + } + + logger_.info("Initializing BOOT button on GPIO{}", static_cast(button_io)); + button_callback_ = callback; + interrupts_.add_interrupt(button_interrupt_pin_); + + return true; +} + +bool Esp32P4FunctionEvBoard::button_state() const { + // BOOT button is active-low + return gpio_get_level(button_io) == 0; +} + +bool Esp32P4FunctionEvBoard::initialize_camera() { + // The MIPI-CSI camera (SC2336/OV5647, SCCB on the internal I2C bus, RST/XCLK + // not connected) is wired in this BSP's pin map, but the esp_video capture + // pipeline is not implemented yet. See the README for the camera status. + logger_.warn("Camera (MIPI-CSI) is not yet implemented in this BSP"); + return false; +} + +} // namespace espp diff --git a/components/esp32-p4-function-ev-board/src/ethernet.cpp b/components/esp32-p4-function-ev-board/src/ethernet.cpp new file mode 100644 index 000000000..d6cf12b16 --- /dev/null +++ b/components/esp32-p4-function-ev-board/src/ethernet.cpp @@ -0,0 +1,187 @@ +#include "esp32-p4-function-ev-board.hpp" + +#if CONFIG_ESP_P4_EV_BOARD_ETHERNET + +#include +#include +#include +#include +#include + +#include "esp_eth_phy_ip101.h" // vendored IP101 PHY driver (third_party/) + +namespace espp { + +// IP101 PHY: reset GPIO and address. The RMII data/clock/MDIO pins for the +// ESP32-P4 Function EV Board are the ESP-IDF ESP32-P4 defaults +// (ETH_ESP32_EMAC_DEFAULT_CONFIG): MDC=31, MDIO=52, REF_CLK in=50, TX_EN=49, +// TXD0=34, TXD1=35, CRS_DV=28, RXD0=29, RXD1=30. +static constexpr int kPhyResetGpio = 51; +static constexpr int kPhyAddr = 1; + +void Esp32P4FunctionEvBoard::ethernet_event_handler(void *arg, esp_event_base_t /*event_base*/, + int32_t event_id, void * /*event_data*/) { + auto *self = static_cast(arg); + switch (event_id) { + case ETHERNET_EVENT_CONNECTED: { + // Log the negotiated speed/duplex. A 10 Mbps or half-duplex result usually + // indicates an autonegotiation/duplex mismatch, which presents as "DHCP works + // but unicast (ping) fails". + eth_speed_t speed = ETH_SPEED_10M; + eth_duplex_t duplex = ETH_DUPLEX_HALF; + if (self->eth_handle_) { + esp_eth_ioctl(self->eth_handle_, ETH_CMD_G_SPEED, &speed); + esp_eth_ioctl(self->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex); + } + self->logger_.info("Ethernet link up: {} Mbps, {} duplex", speed == ETH_SPEED_100M ? 100 : 10, + duplex == ETH_DUPLEX_FULL ? "full" : "half"); + break; + } + case ETHERNET_EVENT_DISCONNECTED: + self->logger_.info("Ethernet link down"); + self->ethernet_connected_ = false; + self->ethernet_ip_ = {}; + break; + case ETHERNET_EVENT_START: + self->logger_.info("Ethernet started"); + break; + case ETHERNET_EVENT_STOP: + self->logger_.info("Ethernet stopped"); + break; + default: + break; + } +} + +void Esp32P4FunctionEvBoard::ethernet_got_ip_handler(void *arg, esp_event_base_t /*event_base*/, + int32_t /*event_id*/, void *event_data) { + auto *self = static_cast(arg); + auto *event = static_cast(event_data); + self->ethernet_ip_ = event->ip_info.ip; + self->ethernet_connected_ = true; + // Note: the espp logger uses fmt-style ({}) formatting, not printf-style, so + // format the IPv4 octets explicitly rather than using IPSTR/IP2STR. + self->logger_.info("Ethernet got IP: {}.{}.{}.{}", esp_ip4_addr1_16(&event->ip_info.ip), + esp_ip4_addr2_16(&event->ip_info.ip), esp_ip4_addr3_16(&event->ip_info.ip), + esp_ip4_addr4_16(&event->ip_info.ip)); + if (self->ethernet_link_callback_) { + self->ethernet_link_callback_(event->ip_info.ip); + } +} + +bool Esp32P4FunctionEvBoard::initialize_ethernet(const ethernet_link_callback_t &on_link_up) { + if (ethernet_initialized_) { + logger_.warn("Ethernet already initialized"); + return true; + } + + // warn the user if they are initializing ethernet after initializing the boot + // button, since they share a pin. + if (button_callback_) { + logger_.warn( + "Initializing Ethernet while BOOT button is initialized. The BOOT button is connected to " + "the PHY's RMII_TXD1 pin, so the boot button will not work while Ethernet is enabled!"); + } + + logger_.info("Initializing Ethernet (EMAC + IP101 EMAC)"); + ethernet_link_callback_ = on_link_up; + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG(); + eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG(); + // NOTE: we can't use the ETH_ESP32_EMAC_DEFAULT_CONFIG macro because it's out + // of order which is a hard error in c++20 and above. + eth_esp32_emac_config_t esp32_emac_config = { + .smi_gpio = {.mdc_num = 31, .mdio_num = 52}, + .interface = EMAC_DATA_INTERFACE_RMII, + .clock_config = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = 50}}, + .dma_burst_len = ETH_DMA_BURST_LEN_32, + .intr_priority = 0, + .emac_dataif_gpio = {.rmii = {.tx_en_num = 49, + .txd0_num = 34, + .txd1_num = 35, + .crs_dv_num = 28, + .rxd0_num = 29, + .rxd1_num = 30}}, + .clock_config_out_in = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = -1}}, + .mdc_freq_hz = 0, + }; +#pragma GCC diagnostic pop + + // Update PHY config based on board specific configuration + phy_config.phy_addr = 1; + phy_config.reset_gpio_num = 51; + + // Update vendor specific MAC config based on board configuration + esp32_emac_config.smi_gpio.mdc_num = 31; + esp32_emac_config.smi_gpio.mdio_num = 52; + + logger_.info("Creating ESP32 EMAC"); + esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config); + if (!mac) { + logger_.error("Failed to create EMAC"); + return false; + } + + logger_.info("Creating generic PHY (IP101)"); + esp_eth_phy_t *phy = esp_eth_phy_new_generic(&phy_config); + if (!phy) { + logger_.error("Failed to create generic PHY"); + return false; + } + +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-field-initializers" + esp_eth_config_t config = ETH_DEFAULT_CONFIG(mac, phy); +#pragma GCC diagnostic pop + eth_handle_ = nullptr; + logger_.info("Installing Ethernet driver"); + esp_err_t ret = esp_eth_driver_install(&config, ð_handle_); + if (ret != ESP_OK) { + logger_.error("esp_eth_driver_install failed: {}", esp_err_to_name(ret)); + return false; + } + + ret = esp_netif_init(); + if (ret != ESP_OK) { + logger_.error("esp_netif_init failed: {}", esp_err_to_name(ret)); + return false; + } + ret = esp_event_loop_create_default(); + if (ret != ESP_OK && ret != ESP_ERR_INVALID_STATE) { + logger_.error("esp_event_loop_create_default failed: {}", esp_err_to_name(ret)); + return false; + } + + esp_netif_config_t netif_cfg = ESP_NETIF_DEFAULT_ETH(); + eth_netif_ = esp_netif_new(&netif_cfg); + if (!eth_netif_) { + logger_.error("Failed to create Ethernet netif"); + return false; + } + + eth_glue_ = esp_eth_new_netif_glue(eth_handle_); + ret = esp_netif_attach(eth_netif_, static_cast(eth_glue_)); + if (ret != ESP_OK) { + logger_.error("esp_netif_attach failed: {}", esp_err_to_name(ret)); + return false; + } + + esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, ðernet_event_handler, this); + esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, ðernet_got_ip_handler, this); + + ret = esp_eth_start(eth_handle_); + if (ret != ESP_OK) { + logger_.error("esp_eth_start failed: {}", esp_err_to_name(ret)); + return false; + } + + ethernet_initialized_ = true; + logger_.info("Ethernet initialized; waiting for link/DHCP"); + return true; +} + +} // namespace espp + +#endif // CONFIG_ESP_P4_EV_BOARD_ETHERNET diff --git a/components/esp32-p4-function-ev-board/src/sdcard.cpp b/components/esp32-p4-function-ev-board/src/sdcard.cpp new file mode 100644 index 000000000..9e405212b --- /dev/null +++ b/components/esp32-p4-function-ev-board/src/sdcard.cpp @@ -0,0 +1,89 @@ +#include "esp32-p4-function-ev-board.hpp" + +#include +#include +#include + +namespace espp { + +bool Esp32P4FunctionEvBoard::initialize_sdcard(const SdCardConfig &config) { + if (sdcard_) { + logger_.error("SD card already initialized!"); + return false; + } + + logger_.info("Initializing SD card (4-bit SDMMC)"); + + esp_vfs_fat_sdmmc_mount_config_t mount_config{}; + mount_config.format_if_mount_failed = config.format_if_mount_failed; + mount_config.max_files = config.max_files; + mount_config.allocation_unit_size = config.allocation_unit_size; + + sdmmc_host_t host = SDMMC_HOST_DEFAULT(); + host.max_freq_khz = SDMMC_FREQ_HIGHSPEED; + host.slot = SDMMC_HOST_SLOT_0; + + // The ESP32-P4 powers the SD card via an internal LDO (LDO_VO4). Configure the + // power control handle so the host can enable that rail. + sd_pwr_ctrl_ldo_config_t ldo_config{}; + ldo_config.ldo_chan_id = sd_ldo_channel; + sd_pwr_ctrl_handle_t pwr_ctrl_handle = nullptr; + esp_err_t ret = sd_pwr_ctrl_new_on_chip_ldo(&ldo_config, &pwr_ctrl_handle); + if (ret != ESP_OK) { + logger_.error("Failed to create SD power control driver: {}", esp_err_to_name(ret)); + return false; + } + host.pwr_ctrl_handle = pwr_ctrl_handle; + sd_pwr_ctrl_handle_ = pwr_ctrl_handle; + + sdmmc_slot_config_t slot_config = SDMMC_SLOT_CONFIG_DEFAULT(); + slot_config.width = 4; + slot_config.clk = sd_clk_io; + slot_config.cmd = sd_cmd_io; + slot_config.d0 = sd_d0_io; + slot_config.d1 = sd_d1_io; + slot_config.d2 = sd_d2_io; + slot_config.d3 = sd_d3_io; + + logger_.debug("Mounting filesystem"); + ret = esp_vfs_fat_sdmmc_mount(mount_point, &host, &slot_config, &mount_config, &sdcard_); + + if (ret != ESP_OK) { + if (ret == ESP_FAIL) { + logger_.error("Failed to mount filesystem."); + } else { + logger_.warn("Failed to initialize the card ({}). " + "Make sure an SD card is inserted.", + esp_err_to_name(ret)); + } + sd_pwr_ctrl_del_on_chip_ldo(pwr_ctrl_handle); + sd_pwr_ctrl_handle_ = nullptr; + return false; + } + + logger_.info("Filesystem mounted"); + sdmmc_card_print_info(stdout, sdcard_); + sd_card_initialized_ = true; + return true; +} + +bool Esp32P4FunctionEvBoard::get_sd_card_info(uint32_t *size_mb, uint32_t *free_mb) const { + if (!sd_card_initialized_) { + return false; + } + uint64_t total_bytes = 0, free_bytes = 0; + esp_err_t ret = esp_vfs_fat_info(mount_point, &total_bytes, &free_bytes); + if (ret != ESP_OK) { + logger_.error("Failed to get SD card information ({})", esp_err_to_name(ret)); + return false; + } + if (size_mb) { + *size_mb = total_bytes / (1024 * 1024); + } + if (free_mb) { + *free_mb = free_bytes / (1024 * 1024); + } + return true; +} + +} // namespace espp diff --git a/components/esp32-p4-function-ev-board/src/touchpad.cpp b/components/esp32-p4-function-ev-board/src/touchpad.cpp new file mode 100644 index 000000000..17e6dcc37 --- /dev/null +++ b/components/esp32-p4-function-ev-board/src/touchpad.cpp @@ -0,0 +1,147 @@ +#include "esp32-p4-function-ev-board.hpp" + +using namespace std::chrono_literals; + +namespace espp { + +bool Esp32P4FunctionEvBoard::initialize_touch(const touch_callback_t &callback) { + if (touch_driver_) { + logger_.warn("Touch driver already initialized"); + return true; + } + + logger_.info("Initializing GT911 multi-touch controller"); + touch_callback_ = callback; + + // The HMI subboard does not route the GT911 reset; the address is fixed at + // power-on. Probe the primary (0x5D) then the backup (0x14) address. + uint8_t address = gt911_default_address; + if (!internal_i2c_.probe_device(gt911_default_address)) { + if (internal_i2c_.probe_device(gt911_backup_address)) { + address = gt911_backup_address; + } else { + logger_.warn("GT911 not found at 0x{:02X} or 0x{:02X}; continuing with 0x{:02X}", + gt911_default_address, gt911_backup_address, address); + } + } + logger_.info("Using GT911 at address 0x{:02X}", address); + + std::error_code ec; + touch_i2c_device_ = internal_i2c_.add_device( + { + .device_address = 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 (!touch_i2c_device_) { + logger_.error("Could not initialize touch I2C device: {}", ec.message()); + return false; + } + + touch_driver_ = std::make_shared( + TouchDriver::Config{.write = espp::make_i2c_addressed_write(touch_i2c_device_), + .read = espp::make_i2c_addressed_read(touch_i2c_device_), + .address = address, + .log_level = espp::Logger::Verbosity::WARN}); + + touchpad_input_ = std::make_shared(TouchpadInput::Config{ + .touchpad_read = std::bind_front(&Esp32P4FunctionEvBoard::touchpad_read, this), + .swap_xy = touch_swap_xy, + .invert_x = touch_invert_x, + .invert_y = touch_invert_y, + .log_level = espp::Logger::Verbosity::WARN}); + + // The touch interrupt is not routed to the ESP32-P4 on this board, so poll the + // GT911 in a task and invoke the user callback on new data. + touch_task_ = std::make_unique(espp::Task::Config{ + .callback = [this](std::mutex &m, std::condition_variable &cv) -> bool { + if (update_touch()) { + if (touch_callback_) { + touch_callback_(touchpad_data()); + } + } + std::unique_lock lock(m); + cv.wait_for(lock, 16ms); + return false; // don't stop + }, + .task_config = {.name = "p4-ev touch", + .stack_size_bytes = CONFIG_ESP_P4_EV_BOARD_TOUCH_TASK_STACK_SIZE}}); + touch_task_->start(); + + logger_.info("Touch controller initialized"); + return true; +} + +bool Esp32P4FunctionEvBoard::update_touch() { + if (!touch_driver_) { + return false; + } + std::error_code ec; + bool new_data = touch_driver_->update(ec); + if (ec) { + logger_.error("could not update touch driver: {}", ec.message()); + std::lock_guard lock(touchpad_data_mutex_); + touchpad_data_ = {}; + return false; + } + if (!new_data) { + return false; + } + TouchpadData temp_data; + touch_driver_->get_touch_point(&temp_data.num_touch_points, &temp_data.x, &temp_data.y); + temp_data.btn_state = touch_driver_->get_home_button_state(); + std::lock_guard lock(touchpad_data_mutex_); + touchpad_data_ = temp_data; + return true; +} + +void Esp32P4FunctionEvBoard::touchpad_read(uint8_t *num_touch_points, uint16_t *x, uint16_t *y, + uint8_t *btn_state) { + std::lock_guard lock(touchpad_data_mutex_); + *num_touch_points = touchpad_data_.num_touch_points; + *x = touchpad_data_.x; + *y = touchpad_data_.y; + *btn_state = touchpad_data_.btn_state; +} + +Esp32P4FunctionEvBoard::TouchpadData +Esp32P4FunctionEvBoard::touchpad_convert(const TouchpadData &data) const { + TouchpadData temp_data = data; + if (temp_data.num_touch_points == 0) { + return temp_data; + } + if (touch_swap_xy) { + std::swap(temp_data.x, temp_data.y); + } + if (touch_invert_x) { + temp_data.x = display_width_ - (temp_data.x + 1); + } + if (touch_invert_y) { + temp_data.y = display_height_ - (temp_data.y + 1); + } + // Map the (panel-native) touch point into the current LVGL display rotation so + // it lines up with what is drawn on screen. + auto rotation = lv_display_get_rotation(lv_display_get_default()); + switch (rotation) { + case LV_DISPLAY_ROTATION_90: + temp_data.y = display_height_ - (temp_data.y + 1); + std::swap(temp_data.x, temp_data.y); + break; + case LV_DISPLAY_ROTATION_180: + temp_data.x = display_width_ - (temp_data.x + 1); + temp_data.y = display_height_ - (temp_data.y + 1); + break; + case LV_DISPLAY_ROTATION_270: + temp_data.x = display_width_ - (temp_data.x + 1); + std::swap(temp_data.x, temp_data.y); + break; + case LV_DISPLAY_ROTATION_0: + default: + break; + } + return temp_data; +} + +} // namespace espp diff --git a/components/esp32-p4-function-ev-board/src/video.cpp b/components/esp32-p4-function-ev-board/src/video.cpp new file mode 100644 index 000000000..16830ef97 --- /dev/null +++ b/components/esp32-p4-function-ev-board/src/video.cpp @@ -0,0 +1,398 @@ +#include "esp32-p4-function-ev-board.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 +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace std::chrono_literals; + +namespace espp { + +bool Esp32P4FunctionEvBoard::initialize_lcd() { + logger_.info("Initializing LCD (MIPI-DSI)"); + + esp_err_t ret = ESP_OK; + + // Enable the MIPI DSI PHY power LDO (on-chip LDO_VO3 -> VDD_MIPI_DPHY) + static esp_ldo_channel_handle_t phy_pwr_chan = nullptr; + if (phy_pwr_chan == nullptr) { + esp_ldo_channel_config_t phy_pwr_cfg{}; + phy_pwr_cfg.chan_id = mipi_dsi_phy_ldo_channel; + phy_pwr_cfg.voltage_mv = mipi_dsi_phy_ldo_voltage_mv; + ret = esp_ldo_acquire_channel(&phy_pwr_cfg, &phy_pwr_chan); + if (ret != ESP_OK) { + logger_.error("Failed to acquire MIPI DSI PHY power LDO channel: {}", esp_err_to_name(ret)); + return false; + } + } + + // Hardware reset on the EK79007 reset GPIO (active-low) before detection. This + // is harmless if an ILI9881C is attached (its reset line is not connected on + // this board; it is reset over DSI during init). + { + constexpr gpio_num_t pre_reset_io = GPIO_NUM_27; + logger_.info("Performing LCD hardware reset on GPIO{}", static_cast(pre_reset_io)); + gpio_config_t rst_cfg{}; + rst_cfg.pin_bit_mask = 1ULL << static_cast(pre_reset_io); + rst_cfg.mode = GPIO_MODE_OUTPUT; + gpio_config(&rst_cfg); + gpio_set_level(pre_reset_io, 0); // assert reset + std::this_thread::sleep_for(10ms); + gpio_set_level(pre_reset_io, 1); // release reset + std::this_thread::sleep_for(120ms); + } + + // 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); + 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; + 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)); + return false; + } + } + + // Install the DBI panel IO (used to send DCS commands/parameters) + if (lcd_handles_.io == nullptr) { + logger_.info("Installing MIPI DSI DBI panel IO"); + esp_lcd_dbi_io_config_t dbi_config = {}; + dbi_config.virtual_channel = 0; + dbi_config.lcd_cmd_bits = 8; + dbi_config.lcd_param_bits = 8; + ret = esp_lcd_new_panel_io_dbi(lcd_handles_.mipi_dsi_bus, &dbi_config, &lcd_handles_.io); + if (ret != ESP_OK) { + logger_.error("New panel IO failed: {}", esp_err_to_name(ret)); + return false; + } + } + + // Select the panel (Kconfig-driven) and apply its parameters (geometry, DPI + // timing, backlight/reset GPIOs). + apply_panel_params(default_controller_); + logger_.info("Using display panel: {} ({}x{})", get_display_controller_name(), display_width_, + display_height_); + + // Configure the backlight PWM on the detected panel's backlight GPIO + if (!backlight_) { + backlight_channel_configs_.push_back({.gpio = static_cast(panel_params_.backlight_io), + .channel = LEDC_CHANNEL_0, + .timer = LEDC_TIMER_0, + .duty = 0.0f, + .speed_mode = LEDC_LOW_SPEED_MODE, + .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); + + // Create the DPI (video) panel with the detected panel's timing. This matches + // Espressif's esp_lcd_ek79007/ili9881c flow: the DPI panel is created first, + // then the vendor init commands are sent (command mode), and finally the DPI + // panel is started (panel->init()). + 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; + } +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + // On ESP-IDF >= 6.0 the DPI panel needs DMA2D explicitly enabled for + // esp_lcd_panel_draw_bitmap() (the LVGL flush path) to copy into the + // framebuffer; without it the screen stays blank. + ret = esp_lcd_dpi_panel_enable_dma2d(lcd_handles_.panel); + if (ret != ESP_OK) { + logger_.error("Failed to enable DMA2D on DPI panel: {}", esp_err_to_name(ret)); + return false; + } +#endif + } + + // 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(&Esp32P4FunctionEvBoard::dsi_write_command, this), + .read_command = std::bind_front(&Esp32P4FunctionEvBoard::dsi_read_command, this), + .lcd_send_lines = nullptr, + .reset_pin = GPIO_NUM_NC, + .data_command_pin = GPIO_NUM_NC, + .reset_value = false, + .invert_colors = invert_colors, + .swap_color_order = swap_color_order, + .offset_x = 0, + .offset_y = 0, + .swap_xy = swap_xy, + .mirror_x = mirror_x, + .mirror_y = mirror_y, + .mirror_portrait = false, + }; + + display_driver_.reset(); + if (display_controller_ == DisplayController::ILI9881C) { + auto driver = std::make_shared(display_config); + if (driver->initialize()) { + display_driver_ = std::move(driver); + } + } else { + auto driver = std::make_shared(display_config); + if (driver->initialize()) { + display_driver_ = std::move(driver); + } + } + if (!display_driver_) { + logger_.error("Failed to initialize {} display controller", get_display_controller_name()); + return false; + } + + // Low-level panel init (starts the DPI video stream) + ret = lcd_handles_.panel->init(lcd_handles_.panel); + if (ret != ESP_OK) { + logger_.error("Low-level panel init failed: {}", esp_err_to_name(ret)); + return false; + } + + // Note: the raw MIPI-DSI DPI panel does not implement disp_on_off (the panel + // is driven on by its vendor init + the DPI video stream), so we don't call + // esp_lcd_panel_disp_on_off() here — it would just log an unsupported error. + + // Register the DPI "color transfer done" callback so LVGL flush completes + esp_lcd_dpi_panel_event_callbacks_t cbs = { + .on_color_trans_done = &Esp32P4FunctionEvBoard::notify_lvgl_flush_ready, + .on_refresh_done = nullptr, + }; + ret = esp_lcd_dpi_panel_register_event_callbacks(lcd_handles_.panel, &cbs, this); + if (ret != ESP_OK) { + logger_.error("Failed to register panel event callback: {}", esp_err_to_name(ret)); + return false; + } + + logger_.info("LCD initialization completed ({})", get_display_controller_name()); + return true; +} + +void Esp32P4FunctionEvBoard::apply_panel_params(DisplayController controller) { + display_controller_ = + (controller == DisplayController::UNKNOWN) ? default_controller_ : controller; + panel_params_ = + (display_controller_ == DisplayController::ILI9881C) ? ILI9881C_PARAMS : EK79007_PARAMS; + display_width_ = panel_params_.width; + display_height_ = panel_params_.height; +} + +static uint16_t *third_buffer = nullptr; + +bool Esp32P4FunctionEvBoard::initialize_display(size_t pixel_buffer_size) { + if (pixel_buffer_size == 0) { + pixel_buffer_size = display_width_ * 50; + } + logger_.info("Initializing LVGL display with pixel buffer size: {} pixels", pixel_buffer_size); + if (!display_) { + display_ = std::make_shared>( + Display::LvglConfig{.width = display_width_, + .height = display_height_, + .flush_callback = + std::bind_front(&Esp32P4FunctionEvBoard::flush, this), + .rotation_callback = nullptr, + .rotation = rotation}, + Display::OledConfig{ + .set_brightness_callback = + [this](float brightness) { this->brightness(brightness * 100.0f); }, + .get_brightness_callback = [this]() { return this->brightness() / 100.0f; }}, + Display::DynamicMemoryConfig{ + .pixel_buffer_size = pixel_buffer_size, + .double_buffered = true, + // Allocate the LVGL draw buffers in PSRAM, not internal RAM. The + // MIPI-DSI DMA2D path can read PSRAM, and keeping these large buffers + // out of internal SRAM leaves room for FreeRTOS/pthread task stacks + // (which must be internal). + .allocation_flags = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, + }, + Logger::Verbosity::WARN); + } + + third_buffer = (uint16_t *)heap_caps_malloc(pixel_buffer_size * sizeof(uint16_t), + MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT); + + logger_.info("LVGL display initialized"); + return true; +} + +size_t Esp32P4FunctionEvBoard::rotated_display_width() const { + auto rot = lv_display_get_rotation(lv_display_get_default()); + switch (rot) { + case LV_DISPLAY_ROTATION_90: + case LV_DISPLAY_ROTATION_270: + return display_height_; + default: + return display_width_; + } +} + +size_t Esp32P4FunctionEvBoard::rotated_display_height() const { + auto rot = lv_display_get_rotation(lv_display_get_default()); + switch (rot) { + case LV_DISPLAY_ROTATION_90: + case LV_DISPLAY_ROTATION_270: + return display_width_; + default: + return display_height_; + } +} + +void Esp32P4FunctionEvBoard::write_lcd_lines(int xs, int ys, int xe, int ye, const uint8_t *data, + uint32_t user_data) { + (void)user_data; + if (lcd_handles_.panel == nullptr || data == nullptr) { + return; + } + if (xs < 0 || ys < 0 || xe < xs || ye < ys) { + logger_.error("write_lcd_lines: Bad region: ({},{}) to ({},{})", xs, ys, xe, ye); + return; + } + esp_lcd_panel_draw_bitmap(lcd_handles_.panel, xs, ys, xe + 1, ye + 1, data); +} + +void Esp32P4FunctionEvBoard::brightness(float brightness) { + brightness = std::clamp(brightness, 0.0f, 100.0f); + if (backlight_) { + backlight_->set_duty(LEDC_CHANNEL_0, brightness); + } else { + gpio_set_level(panel_params_.backlight_io, brightness > 0.0f ? 1 : 0); + } +} + +float Esp32P4FunctionEvBoard::brightness() const { + if (backlight_) { + auto maybe_duty = backlight_->get_duty(LEDC_CHANNEL_0); + if (maybe_duty.has_value()) + return maybe_duty.value(); + } + return gpio_get_level(panel_params_.backlight_io) ? 100.0f : 0.0f; +} + +void IRAM_ATTR Esp32P4FunctionEvBoard::flush(lv_display_t *disp, const lv_area_t *area, + uint8_t *px_map) { + if (lcd_handles_.panel == nullptr) { + lv_display_flush_ready(disp); + return; + } + + int offsetx1 = area->x1; + int offsetx2 = area->x2; + int offsety1 = area->y1; + int offsety2 = area->y2; + + auto rot = lv_display_get_rotation(lv_display_get_default()); + if (rot > LV_DISPLAY_ROTATION_0 && third_buffer != nullptr) { + int32_t ww = lv_area_get_width(area); + int32_t hh = lv_area_get_height(area); + lv_color_format_t cf = lv_display_get_color_format(disp); + uint32_t w_stride = lv_draw_buf_width_to_stride(ww, cf); + uint32_t h_stride = lv_draw_buf_width_to_stride(hh, cf); + if (rot == LV_DISPLAY_ROTATION_180) { + lv_draw_sw_rotate(px_map, third_buffer, hh, ww, h_stride, h_stride, LV_DISPLAY_ROTATION_180, + cf); + } else if (rot == LV_DISPLAY_ROTATION_90) { + lv_draw_sw_rotate(px_map, third_buffer, ww, hh, w_stride, h_stride, LV_DISPLAY_ROTATION_90, + cf); + } else if (rot == LV_DISPLAY_ROTATION_270) { + lv_draw_sw_rotate(px_map, third_buffer, ww, hh, w_stride, h_stride, LV_DISPLAY_ROTATION_270, + cf); + } + px_map = reinterpret_cast(third_buffer); + lv_display_rotate_area(disp, const_cast(area)); + offsetx1 = area->x1; + offsetx2 = area->x2; + offsety1 = area->y1; + offsety2 = area->y2; + } + + esp_lcd_panel_draw_bitmap(lcd_handles_.panel, offsetx1, offsety1, offsetx2 + 1, offsety2 + 1, + px_map); +} + +bool IRAM_ATTR Esp32P4FunctionEvBoard::notify_lvgl_flush_ready( + esp_lcd_panel_handle_t panel, esp_lcd_dpi_panel_event_data_t *edata, void *user_ctx) { + (void)panel; + (void)edata; + auto *board = static_cast(user_ctx); + if (board && board->display_) { + board->display_->notify_flush_ready(); + } + return false; +} + +void Esp32P4FunctionEvBoard::dsi_write_command(uint8_t cmd, std::span params, + uint32_t /*flags*/) { + if (!lcd_handles_.io) { + logger_.error("DSI write_command does not have a valid IO handle"); + return; + } + esp_err_t err = + esp_lcd_panel_io_tx_param(lcd_handles_.io, (int)cmd, params.data(), params.size()); + if (err != ESP_OK) { + logger_.error("DSI write_command 0x{:02X} failed: {}", cmd, esp_err_to_name(err)); + } +} + +void Esp32P4FunctionEvBoard::dsi_read_command(uint8_t cmd, std::span data, + uint32_t /*flags*/) { + if (!lcd_handles_.io) { + logger_.error("DSI read_command does not have a valid IO handle"); + return; + } + esp_err_t err = esp_lcd_panel_io_rx_param(lcd_handles_.io, (int)cmd, data.data(), data.size()); + if (err != ESP_OK) { + logger_.error("DSI read_command 0x{:02X} failed: {}", cmd, esp_err_to_name(err)); + } +} + +} // namespace espp diff --git a/components/esp32-p4-function-ev-board/third_party/LICENSE b/components/esp32-p4-function-ev-board/third_party/LICENSE new file mode 100644 index 000000000..261eeb9e9 --- /dev/null +++ b/components/esp32-p4-function-ev-board/third_party/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/components/esp32-p4-function-ev-board/third_party/README.md b/components/esp32-p4-function-ev-board/third_party/README.md new file mode 100644 index 000000000..469bc563f --- /dev/null +++ b/components/esp32-p4-function-ev-board/third_party/README.md @@ -0,0 +1,18 @@ +# Vendored third-party sources + +## esp_eth_phy_ip101 (IP101 Ethernet PHY driver) + +`esp_eth_phy_ip101.c` / `esp_eth_phy_ip101.h` are vendored verbatim from +Espressif's [`esp-eth-drivers`](https://github.com/espressif/esp-eth-drivers) +(`ip101/` component), licensed **Apache-2.0** (see `LICENSE`). + +They are bundled here because, on ESP-IDF v6+, the vendor-specific Ethernet PHY +drivers (IP101, etc.) were moved out of the built-in `esp_eth` component into +separate registry components. The ESP32-P4 Function EV Board uses an **IP101** +PHY, and this BSP builds with the ESP-IDF component manager disabled (it consumes +the local `espp/*` components directly), so the driver is vendored rather than +fetched. It depends only on `esp_eth_phy_802_3.h`, which is part of the IDF's +`esp_eth` component. + +To update: replace the two files from the upstream `ip101/` component (keeping +their SPDX license headers) and update this note. diff --git a/components/esp32-p4-function-ev-board/third_party/esp_eth_phy_ip101.c b/components/esp32-p4-function-ev-board/third_party/esp_eth_phy_ip101.c new file mode 100644 index 000000000..c8e15cf41 --- /dev/null +++ b/components/esp32-p4-function-ev-board/third_party/esp_eth_phy_ip101.c @@ -0,0 +1,222 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ +#include "esp_check.h" +#include "esp_eth_phy_802_3.h" +#include "esp_log.h" +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" +#include +#include +#include + +static const char *TAG = "ip101"; + +#define IP101_PHY_RESET_ASSERTION_TIME_US 10000 +#define IP101_PHY_POST_RESET_INIT_TIME_MS 10 + +/***************Vendor Specific Register***************/ + +/** + * @brief PCR(Page Control Register) + * + */ +typedef union { + struct { + uint32_t register_page_select : 5; /* Select register page, default is 16 */ + uint32_t reserved : 11; /* Reserved */ + }; + uint32_t val; +} pcr_reg_t; +#define ETH_PHY_PCR_REG_ADDR (0x14) + +/** + * @brief ISR(Interrupt Status Register), Page 16 + * + */ +typedef union { + struct { + uint32_t link_changed : 1; /* Flag to indicate link status change interrupt */ + uint32_t duplex_changed : 1; /* Flag to indicate duplex change interrupt */ + uint32_t speed_changed : 1; /* Flag to indicate speed change interrupt */ + uint32_t intr_status : 1; /* Flag to indicate interrupt status */ + uint32_t reserved1 : 4; /* Reserved */ + uint32_t link_mask : 1; /* Mask link change interrupt */ + uint32_t duplex_mask : 1; /* Mask duplex change interrupt */ + uint32_t speed_mask : 1; /* Mask speed change interrupt */ + uint32_t all_mask : 1; /* Mask all interrupt */ + uint32_t reserved2 : 3; /* Reserved */ + uint32_t use_intr_pin : 1; /* Set high to use INTR and INTR_32 as an interrupt pin */ + }; + uint32_t val; +} isr_reg_t; +#define ETH_PHY_ISR_REG_ADDR (0x11) + +/** + * @brief PHY MDI/MDIX Control and Specific Status Register, Page 16 + * + */ +typedef union { + struct { + uint32_t op_mode : 3; /* Operation Mode Indicator */ + uint32_t force_mdix : 1; /* Force the MDIX channel to be selected */ + uint32_t reserved1 : 4; /* Reserved */ + uint32_t link_up : 1; /* Indicate the link status is OK or FAIL */ + uint32_t reserved2 : 7; /* Reserved */ + }; + uint32_t val; +} cssr_reg_t; +#define ETH_PHY_CSSR_REG_ADDR (0x1E) + +/** + * @brief PSCR(PHY Specific Control Register), Page 1 + * + */ +typedef union { + struct { + uint32_t reserved1 : 7; /* Reserved */ + uint32_t force_link_100 : 1; /* Force Link 100 */ + uint32_t force_link_10 : 1; /* Force Link 10 */ + uint32_t reserved2 : 7; /* Reserved */ + }; + uint32_t val; +} pscr_reg_t; +#define ETH_PHY_PSCR_REG_ADDR (0x11) + +typedef struct { + phy_802_3_t phy_802_3; +} phy_ip101_t; + +static esp_err_t ip101_page_select(phy_ip101_t *ip101, uint32_t page) { + esp_err_t ret = ESP_OK; + esp_eth_mediator_t *eth = ip101->phy_802_3.eth; + pcr_reg_t pcr = {.register_page_select = page}; + ESP_GOTO_ON_ERROR(eth->phy_reg_write(eth, ip101->phy_802_3.addr, ETH_PHY_PCR_REG_ADDR, pcr.val), + err, TAG, "write PCR failed"); + return ESP_OK; +err: + return ret; +} + +static esp_err_t ip101_update_link_duplex_speed(phy_ip101_t *ip101) { + esp_err_t ret = ESP_OK; + esp_eth_mediator_t *eth = ip101->phy_802_3.eth; + uint32_t addr = ip101->phy_802_3.addr; + eth_speed_t speed = ETH_SPEED_10M; + eth_duplex_t duplex = ETH_DUPLEX_HALF; + uint32_t peer_pause_ability = false; + cssr_reg_t cssr; + anlpar_reg_t anlpar; + + ESP_GOTO_ON_ERROR(ip101_page_select(ip101, 16), err, TAG, "select page 16 failed"); + ESP_GOTO_ON_ERROR(eth->phy_reg_read(eth, addr, ETH_PHY_CSSR_REG_ADDR, &(cssr.val)), err, TAG, + "read CSSR failed"); + ESP_GOTO_ON_ERROR(eth->phy_reg_read(eth, addr, ETH_PHY_ANLPAR_REG_ADDR, &(anlpar.val)), err, TAG, + "read ANLPAR failed"); + eth_link_t link = cssr.link_up ? ETH_LINK_UP : ETH_LINK_DOWN; + /* check if link status changed */ + if (ip101->phy_802_3.link_status != link) { + /* when link up, read negotiation result */ + if (link == ETH_LINK_UP) { + switch (cssr.op_mode) { + case 1: // 10M Half + speed = ETH_SPEED_10M; + duplex = ETH_DUPLEX_HALF; + break; + case 2: // 100M Half + speed = ETH_SPEED_100M; + duplex = ETH_DUPLEX_HALF; + break; + case 5: // 10M Full + speed = ETH_SPEED_10M; + duplex = ETH_DUPLEX_FULL; + break; + case 6: // 100M Full + speed = ETH_SPEED_100M; + duplex = ETH_DUPLEX_FULL; + break; + default: + break; + } + ESP_GOTO_ON_ERROR(eth->on_state_changed(eth, ETH_STATE_SPEED, (void *)speed), err, TAG, + "change speed failed"); + ESP_GOTO_ON_ERROR(eth->on_state_changed(eth, ETH_STATE_DUPLEX, (void *)duplex), err, TAG, + "change duplex failed"); + /* if we're in duplex mode, and peer has the flow control ability */ + if (duplex == ETH_DUPLEX_FULL && anlpar.symmetric_pause) { + peer_pause_ability = 1; + } else { + peer_pause_ability = 0; + } + ESP_GOTO_ON_ERROR(eth->on_state_changed(eth, ETH_STATE_PAUSE, (void *)peer_pause_ability), + err, TAG, "change pause ability failed"); + } + ESP_GOTO_ON_ERROR(eth->on_state_changed(eth, ETH_STATE_LINK, (void *)link), err, TAG, + "change link failed"); + ip101->phy_802_3.link_status = link; + } + return ESP_OK; +err: + return ret; +} + +static esp_err_t ip101_get_link(esp_eth_phy_t *phy) { + esp_err_t ret = ESP_OK; + phy_ip101_t *ip101 = __containerof(esp_eth_phy_into_phy_802_3(phy), phy_ip101_t, phy_802_3); + + /* Update information about link, speed, duplex */ + ESP_GOTO_ON_ERROR(ip101_update_link_duplex_speed(ip101), err, TAG, + "update link duplex speed failed"); + return ESP_OK; +err: + return ret; +} + +static esp_err_t ip101_init(esp_eth_phy_t *phy) { + esp_err_t ret = ESP_OK; + phy_802_3_t *phy_802_3 = esp_eth_phy_into_phy_802_3(phy); + + /* Basic PHY init */ + ESP_GOTO_ON_ERROR(esp_eth_phy_802_3_basic_phy_init(phy_802_3), err, TAG, "failed to init PHY"); + + /* Check PHY ID */ + uint32_t oui; + uint8_t model; + ESP_GOTO_ON_ERROR(esp_eth_phy_802_3_read_oui(phy_802_3, &oui), err, TAG, "read OUI failed"); + ESP_GOTO_ON_ERROR(esp_eth_phy_802_3_read_manufac_info(phy_802_3, &model, NULL), err, TAG, + "read manufacturer's info failed"); + ESP_GOTO_ON_FALSE(oui == 0x90C3 && model == 0x5, ESP_FAIL, err, TAG, "wrong chip ID"); + + return ESP_OK; +err: + return ret; +} + +esp_eth_phy_t *esp_eth_phy_new_ip101(const eth_phy_config_t *config) { + esp_eth_phy_t *ret = NULL; + phy_ip101_t *ip101 = calloc(1, sizeof(phy_ip101_t)); + ESP_GOTO_ON_FALSE(ip101, NULL, err, TAG, "calloc ip101 failed"); + eth_phy_config_t ip101_config = *config; + // default chip specific configuration + if (config->hw_reset_assert_time_us == 0) { + ip101_config.hw_reset_assert_time_us = IP101_PHY_RESET_ASSERTION_TIME_US; + } + if (config->post_hw_reset_delay_ms == 0) { + ip101_config.post_hw_reset_delay_ms = IP101_PHY_POST_RESET_INIT_TIME_MS; + } + ESP_GOTO_ON_FALSE(esp_eth_phy_802_3_obj_config_init(&ip101->phy_802_3, &ip101_config) == ESP_OK, + NULL, err, TAG, "configuration initialization of PHY 802.3 failed"); + + // redefine functions which need to be customized for sake of IP101 + ip101->phy_802_3.parent.init = ip101_init; + ip101->phy_802_3.parent.get_link = ip101_get_link; + + return &ip101->phy_802_3.parent; +err: + if (ip101 != NULL) { + free(ip101); + } + return ret; +} diff --git a/components/esp32-p4-function-ev-board/third_party/esp_eth_phy_ip101.h b/components/esp32-p4-function-ev-board/third_party/esp_eth_phy_ip101.h new file mode 100644 index 000000000..8363bbedf --- /dev/null +++ b/components/esp32-p4-function-ev-board/third_party/esp_eth_phy_ip101.h @@ -0,0 +1,28 @@ +/* + * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD + * + * SPDX-License-Identifier: Apache-2.0 + */ + +#pragma once + +#include "esp_eth_phy.h" + +#ifdef __cplusplus +extern "C" { +#endif + +/** + * @brief Create a PHY instance of IP101 + * + * @param[in] config: configuration of PHY + * + * @return + * - instance: create PHY instance successfully + * - NULL: create PHY instance failed because some error occurred + */ +esp_eth_phy_t *esp_eth_phy_new_ip101(const eth_phy_config_t *config); + +#ifdef __cplusplus +} +#endif diff --git a/doc/Doxyfile b/doc/Doxyfile index 9c97c3f3a..e48aee3b7 100755 --- a/doc/Doxyfile +++ b/doc/Doxyfile @@ -103,6 +103,7 @@ EXAMPLE_PATH = \ $(PROJECT_PATH)/components/drv2605/example/main/drv2605_example.cpp \ $(PROJECT_PATH)/components/encoder/example/main/encoder_example.cpp \ $(PROJECT_PATH)/components/esp32-timer-cam/example/main/esp_timer_cam_example.cpp \ + $(PROJECT_PATH)/components/esp32-p4-function-ev-board/example/main/esp32_p4_function_ev_board_example.cpp \ $(PROJECT_PATH)/components/esp-box/example/main/esp_box_example.cpp \ $(PROJECT_PATH)/components/event_manager/example/main/event_manager_example.cpp \ $(PROJECT_PATH)/components/expressive_eyes/example/main/expressive_eyes_example.cpp \ @@ -231,6 +232,7 @@ INPUT = \ $(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/ek79007.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 \ @@ -245,6 +247,7 @@ INPUT = \ $(PROJECT_PATH)/components/encoder/include/abi_encoder.hpp \ $(PROJECT_PATH)/components/encoder/include/encoder_types.hpp \ $(PROJECT_PATH)/components/esp32-timer-cam/include/esp32-timer-cam.hpp \ + $(PROJECT_PATH)/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp \ $(PROJECT_PATH)/components/esp-box/include/esp-box.hpp \ $(PROJECT_PATH)/components/event_manager/include/event_manager.hpp \ $(PROJECT_PATH)/components/expressive_eyes/include/expressive_eyes.hpp \ diff --git a/doc/en/esp32_p4_function_ev_board.rst b/doc/en/esp32_p4_function_ev_board.rst new file mode 100644 index 000000000..22ae4a4b2 --- /dev/null +++ b/doc/en/esp32_p4_function_ev_board.rst @@ -0,0 +1,54 @@ +ESP32-P4 Function EV Board +************************** + +ESP32-P4-Function-EV-Board +-------------------------- + +The ESP32-P4 Function EV Board is an Espressif development board for the ESP32-P4 +microprocessor. Together with the ESP32-P4-HMI-Subboard it provides a MIPI-DSI +touchscreen display, an ES8311 audio codec with speaker amplifier, 10/100 +Ethernet, a MIPI-CSI camera interface, a microSD card slot, and USB. + +The `espp::Esp32P4FunctionEvBoard` component provides a singleton hardware +abstraction for initializing the display, touch, audio, Ethernet, and SD card +subsystems. The display panel (EK79007 1024x600 or ILI9881C 800x1280) is +selectable via Kconfig. + +Official board documentation: + +- `ESP32-P4-Function-EV-Board User Guide `_ (includes the ESP32-P4-HMI-Subboard) +- `ESP32-P4-Function-EV-Board overview page `_ +- `Board schematic (PDF) `_ +- `Espressif reference BSP (esp-bsp) `_ +- `ESP32-P4 Get Started (ESP-IDF) `_ + +Display panels (HMI subboard). Both panels plug into the shared LCD adapter +board via its FPC connector: + +- `LCD Adapter Board Schematic (PDF) `_ +- `LCD Adapter Board PCB Layout (PDF) `_ + +EK79007 (7", 1024x600) — the panel Espressif ships/documents for this board: + +- `Display Datasheet (PDF) `_ +- `EK79007AD display driver chip datasheet (PDF) `_ +- `EK73217BCGA display driver chip datasheet (PDF) `_ + +ILI9881C (10.1", 800x1280) — a panel option supported by the BSP. Espressif does +not publish a dedicated LCD-subboard schematic/datasheet for this panel on this +board; refer to the `esp-bsp esp_lcd_ili9881c driver +`_ +for the panel/timing details. + +.. ------------------------------- Example ------------------------------------- + +.. toctree:: + + esp32_p4_function_ev_board_example + +.. ---------------------------- API Reference ---------------------------------- + +API Reference +------------- + +.. include-build-file:: inc/esp32-p4-function-ev-board.inc diff --git a/doc/en/esp32_p4_function_ev_board_example.md b/doc/en/esp32_p4_function_ev_board_example.md new file mode 100644 index 000000000..29af32b07 --- /dev/null +++ b/doc/en/esp32_p4_function_ev_board_example.md @@ -0,0 +1,2 @@ +```{include} ../../components/esp32-p4-function-ev-board/example/README.md +``` diff --git a/doc/en/index.rst b/doc/en/index.rst index 24e00bd8e..14a360e56 100755 --- a/doc/en/index.rst +++ b/doc/en/index.rst @@ -28,6 +28,7 @@ This is the documentation for esp-idf c++ components, ESPP (`espp Date: Wed, 24 Jun 2026 14:09:42 -0500 Subject: [PATCH 2/9] remove unused vendored ip101 driver --- .../esp32-p4-function-ev-board/CMakeLists.txt | 4 +- .../src/ethernet.cpp | 2 - .../third_party/LICENSE | 201 ---------------- .../third_party/README.md | 18 -- .../third_party/esp_eth_phy_ip101.c | 222 ------------------ .../third_party/esp_eth_phy_ip101.h | 28 --- 6 files changed, 2 insertions(+), 473 deletions(-) delete mode 100644 components/esp32-p4-function-ev-board/third_party/LICENSE delete mode 100644 components/esp32-p4-function-ev-board/third_party/README.md delete mode 100644 components/esp32-p4-function-ev-board/third_party/esp_eth_phy_ip101.c delete mode 100644 components/esp32-p4-function-ev-board/third_party/esp_eth_phy_ip101.h diff --git a/components/esp32-p4-function-ev-board/CMakeLists.txt b/components/esp32-p4-function-ev-board/CMakeLists.txt index 1712fa844..5cada4f5b 100644 --- a/components/esp32-p4-function-ev-board/CMakeLists.txt +++ b/components/esp32-p4-function-ev-board/CMakeLists.txt @@ -1,6 +1,6 @@ idf_component_register( - INCLUDE_DIRS "include" "third_party" - SRC_DIRS "src" "third_party" + INCLUDE_DIRS "include" + SRC_DIRS "src" REQUIRES "base_component" "codec" diff --git a/components/esp32-p4-function-ev-board/src/ethernet.cpp b/components/esp32-p4-function-ev-board/src/ethernet.cpp index d6cf12b16..aac90952c 100644 --- a/components/esp32-p4-function-ev-board/src/ethernet.cpp +++ b/components/esp32-p4-function-ev-board/src/ethernet.cpp @@ -8,8 +8,6 @@ #include #include -#include "esp_eth_phy_ip101.h" // vendored IP101 PHY driver (third_party/) - namespace espp { // IP101 PHY: reset GPIO and address. The RMII data/clock/MDIO pins for the diff --git a/components/esp32-p4-function-ev-board/third_party/LICENSE b/components/esp32-p4-function-ev-board/third_party/LICENSE deleted file mode 100644 index 261eeb9e9..000000000 --- a/components/esp32-p4-function-ev-board/third_party/LICENSE +++ /dev/null @@ -1,201 +0,0 @@ - Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "[]" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright [yyyy] [name of copyright owner] - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. diff --git a/components/esp32-p4-function-ev-board/third_party/README.md b/components/esp32-p4-function-ev-board/third_party/README.md deleted file mode 100644 index 469bc563f..000000000 --- a/components/esp32-p4-function-ev-board/third_party/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Vendored third-party sources - -## esp_eth_phy_ip101 (IP101 Ethernet PHY driver) - -`esp_eth_phy_ip101.c` / `esp_eth_phy_ip101.h` are vendored verbatim from -Espressif's [`esp-eth-drivers`](https://github.com/espressif/esp-eth-drivers) -(`ip101/` component), licensed **Apache-2.0** (see `LICENSE`). - -They are bundled here because, on ESP-IDF v6+, the vendor-specific Ethernet PHY -drivers (IP101, etc.) were moved out of the built-in `esp_eth` component into -separate registry components. The ESP32-P4 Function EV Board uses an **IP101** -PHY, and this BSP builds with the ESP-IDF component manager disabled (it consumes -the local `espp/*` components directly), so the driver is vendored rather than -fetched. It depends only on `esp_eth_phy_802_3.h`, which is part of the IDF's -`esp_eth` component. - -To update: replace the two files from the upstream `ip101/` component (keeping -their SPDX license headers) and update this note. diff --git a/components/esp32-p4-function-ev-board/third_party/esp_eth_phy_ip101.c b/components/esp32-p4-function-ev-board/third_party/esp_eth_phy_ip101.c deleted file mode 100644 index c8e15cf41..000000000 --- a/components/esp32-p4-function-ev-board/third_party/esp_eth_phy_ip101.c +++ /dev/null @@ -1,222 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ -#include "esp_check.h" -#include "esp_eth_phy_802_3.h" -#include "esp_log.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" -#include -#include -#include - -static const char *TAG = "ip101"; - -#define IP101_PHY_RESET_ASSERTION_TIME_US 10000 -#define IP101_PHY_POST_RESET_INIT_TIME_MS 10 - -/***************Vendor Specific Register***************/ - -/** - * @brief PCR(Page Control Register) - * - */ -typedef union { - struct { - uint32_t register_page_select : 5; /* Select register page, default is 16 */ - uint32_t reserved : 11; /* Reserved */ - }; - uint32_t val; -} pcr_reg_t; -#define ETH_PHY_PCR_REG_ADDR (0x14) - -/** - * @brief ISR(Interrupt Status Register), Page 16 - * - */ -typedef union { - struct { - uint32_t link_changed : 1; /* Flag to indicate link status change interrupt */ - uint32_t duplex_changed : 1; /* Flag to indicate duplex change interrupt */ - uint32_t speed_changed : 1; /* Flag to indicate speed change interrupt */ - uint32_t intr_status : 1; /* Flag to indicate interrupt status */ - uint32_t reserved1 : 4; /* Reserved */ - uint32_t link_mask : 1; /* Mask link change interrupt */ - uint32_t duplex_mask : 1; /* Mask duplex change interrupt */ - uint32_t speed_mask : 1; /* Mask speed change interrupt */ - uint32_t all_mask : 1; /* Mask all interrupt */ - uint32_t reserved2 : 3; /* Reserved */ - uint32_t use_intr_pin : 1; /* Set high to use INTR and INTR_32 as an interrupt pin */ - }; - uint32_t val; -} isr_reg_t; -#define ETH_PHY_ISR_REG_ADDR (0x11) - -/** - * @brief PHY MDI/MDIX Control and Specific Status Register, Page 16 - * - */ -typedef union { - struct { - uint32_t op_mode : 3; /* Operation Mode Indicator */ - uint32_t force_mdix : 1; /* Force the MDIX channel to be selected */ - uint32_t reserved1 : 4; /* Reserved */ - uint32_t link_up : 1; /* Indicate the link status is OK or FAIL */ - uint32_t reserved2 : 7; /* Reserved */ - }; - uint32_t val; -} cssr_reg_t; -#define ETH_PHY_CSSR_REG_ADDR (0x1E) - -/** - * @brief PSCR(PHY Specific Control Register), Page 1 - * - */ -typedef union { - struct { - uint32_t reserved1 : 7; /* Reserved */ - uint32_t force_link_100 : 1; /* Force Link 100 */ - uint32_t force_link_10 : 1; /* Force Link 10 */ - uint32_t reserved2 : 7; /* Reserved */ - }; - uint32_t val; -} pscr_reg_t; -#define ETH_PHY_PSCR_REG_ADDR (0x11) - -typedef struct { - phy_802_3_t phy_802_3; -} phy_ip101_t; - -static esp_err_t ip101_page_select(phy_ip101_t *ip101, uint32_t page) { - esp_err_t ret = ESP_OK; - esp_eth_mediator_t *eth = ip101->phy_802_3.eth; - pcr_reg_t pcr = {.register_page_select = page}; - ESP_GOTO_ON_ERROR(eth->phy_reg_write(eth, ip101->phy_802_3.addr, ETH_PHY_PCR_REG_ADDR, pcr.val), - err, TAG, "write PCR failed"); - return ESP_OK; -err: - return ret; -} - -static esp_err_t ip101_update_link_duplex_speed(phy_ip101_t *ip101) { - esp_err_t ret = ESP_OK; - esp_eth_mediator_t *eth = ip101->phy_802_3.eth; - uint32_t addr = ip101->phy_802_3.addr; - eth_speed_t speed = ETH_SPEED_10M; - eth_duplex_t duplex = ETH_DUPLEX_HALF; - uint32_t peer_pause_ability = false; - cssr_reg_t cssr; - anlpar_reg_t anlpar; - - ESP_GOTO_ON_ERROR(ip101_page_select(ip101, 16), err, TAG, "select page 16 failed"); - ESP_GOTO_ON_ERROR(eth->phy_reg_read(eth, addr, ETH_PHY_CSSR_REG_ADDR, &(cssr.val)), err, TAG, - "read CSSR failed"); - ESP_GOTO_ON_ERROR(eth->phy_reg_read(eth, addr, ETH_PHY_ANLPAR_REG_ADDR, &(anlpar.val)), err, TAG, - "read ANLPAR failed"); - eth_link_t link = cssr.link_up ? ETH_LINK_UP : ETH_LINK_DOWN; - /* check if link status changed */ - if (ip101->phy_802_3.link_status != link) { - /* when link up, read negotiation result */ - if (link == ETH_LINK_UP) { - switch (cssr.op_mode) { - case 1: // 10M Half - speed = ETH_SPEED_10M; - duplex = ETH_DUPLEX_HALF; - break; - case 2: // 100M Half - speed = ETH_SPEED_100M; - duplex = ETH_DUPLEX_HALF; - break; - case 5: // 10M Full - speed = ETH_SPEED_10M; - duplex = ETH_DUPLEX_FULL; - break; - case 6: // 100M Full - speed = ETH_SPEED_100M; - duplex = ETH_DUPLEX_FULL; - break; - default: - break; - } - ESP_GOTO_ON_ERROR(eth->on_state_changed(eth, ETH_STATE_SPEED, (void *)speed), err, TAG, - "change speed failed"); - ESP_GOTO_ON_ERROR(eth->on_state_changed(eth, ETH_STATE_DUPLEX, (void *)duplex), err, TAG, - "change duplex failed"); - /* if we're in duplex mode, and peer has the flow control ability */ - if (duplex == ETH_DUPLEX_FULL && anlpar.symmetric_pause) { - peer_pause_ability = 1; - } else { - peer_pause_ability = 0; - } - ESP_GOTO_ON_ERROR(eth->on_state_changed(eth, ETH_STATE_PAUSE, (void *)peer_pause_ability), - err, TAG, "change pause ability failed"); - } - ESP_GOTO_ON_ERROR(eth->on_state_changed(eth, ETH_STATE_LINK, (void *)link), err, TAG, - "change link failed"); - ip101->phy_802_3.link_status = link; - } - return ESP_OK; -err: - return ret; -} - -static esp_err_t ip101_get_link(esp_eth_phy_t *phy) { - esp_err_t ret = ESP_OK; - phy_ip101_t *ip101 = __containerof(esp_eth_phy_into_phy_802_3(phy), phy_ip101_t, phy_802_3); - - /* Update information about link, speed, duplex */ - ESP_GOTO_ON_ERROR(ip101_update_link_duplex_speed(ip101), err, TAG, - "update link duplex speed failed"); - return ESP_OK; -err: - return ret; -} - -static esp_err_t ip101_init(esp_eth_phy_t *phy) { - esp_err_t ret = ESP_OK; - phy_802_3_t *phy_802_3 = esp_eth_phy_into_phy_802_3(phy); - - /* Basic PHY init */ - ESP_GOTO_ON_ERROR(esp_eth_phy_802_3_basic_phy_init(phy_802_3), err, TAG, "failed to init PHY"); - - /* Check PHY ID */ - uint32_t oui; - uint8_t model; - ESP_GOTO_ON_ERROR(esp_eth_phy_802_3_read_oui(phy_802_3, &oui), err, TAG, "read OUI failed"); - ESP_GOTO_ON_ERROR(esp_eth_phy_802_3_read_manufac_info(phy_802_3, &model, NULL), err, TAG, - "read manufacturer's info failed"); - ESP_GOTO_ON_FALSE(oui == 0x90C3 && model == 0x5, ESP_FAIL, err, TAG, "wrong chip ID"); - - return ESP_OK; -err: - return ret; -} - -esp_eth_phy_t *esp_eth_phy_new_ip101(const eth_phy_config_t *config) { - esp_eth_phy_t *ret = NULL; - phy_ip101_t *ip101 = calloc(1, sizeof(phy_ip101_t)); - ESP_GOTO_ON_FALSE(ip101, NULL, err, TAG, "calloc ip101 failed"); - eth_phy_config_t ip101_config = *config; - // default chip specific configuration - if (config->hw_reset_assert_time_us == 0) { - ip101_config.hw_reset_assert_time_us = IP101_PHY_RESET_ASSERTION_TIME_US; - } - if (config->post_hw_reset_delay_ms == 0) { - ip101_config.post_hw_reset_delay_ms = IP101_PHY_POST_RESET_INIT_TIME_MS; - } - ESP_GOTO_ON_FALSE(esp_eth_phy_802_3_obj_config_init(&ip101->phy_802_3, &ip101_config) == ESP_OK, - NULL, err, TAG, "configuration initialization of PHY 802.3 failed"); - - // redefine functions which need to be customized for sake of IP101 - ip101->phy_802_3.parent.init = ip101_init; - ip101->phy_802_3.parent.get_link = ip101_get_link; - - return &ip101->phy_802_3.parent; -err: - if (ip101 != NULL) { - free(ip101); - } - return ret; -} diff --git a/components/esp32-p4-function-ev-board/third_party/esp_eth_phy_ip101.h b/components/esp32-p4-function-ev-board/third_party/esp_eth_phy_ip101.h deleted file mode 100644 index 8363bbedf..000000000 --- a/components/esp32-p4-function-ev-board/third_party/esp_eth_phy_ip101.h +++ /dev/null @@ -1,28 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2025 Espressif Systems (Shanghai) CO LTD - * - * SPDX-License-Identifier: Apache-2.0 - */ - -#pragma once - -#include "esp_eth_phy.h" - -#ifdef __cplusplus -extern "C" { -#endif - -/** - * @brief Create a PHY instance of IP101 - * - * @param[in] config: configuration of PHY - * - * @return - * - instance: create PHY instance successfully - * - NULL: create PHY instance failed because some error occurred - */ -esp_eth_phy_t *esp_eth_phy_new_ip101(const eth_phy_config_t *config); - -#ifdef __cplusplus -} -#endif From 9194da6c3f22712bcfe9738ec0381fc61ef22c9a Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Wed, 24 Jun 2026 21:58:58 -0500 Subject: [PATCH 3/9] fixed some video issues in esp32-p4 config --- .../esp32_p4_function_ev_board_example.cpp | 24 ++++++++++++++--- .../include/esp32-p4-function-ev-board.hpp | 6 ++++- .../esp32-p4-function-ev-board/src/video.cpp | 26 +++++++++---------- 3 files changed, 37 insertions(+), 19 deletions(-) diff --git a/components/esp32-p4-function-ev-board/example/main/esp32_p4_function_ev_board_example.cpp b/components/esp32-p4-function-ev-board/example/main/esp32_p4_function_ev_board_example.cpp index ac6933566..3cb90d694 100644 --- a/components/esp32-p4-function-ev-board/example/main/esp32_p4_function_ev_board_example.cpp +++ b/components/esp32-p4-function-ev-board/example/main/esp32_p4_function_ev_board_example.cpp @@ -260,19 +260,35 @@ extern "C" void app_main(void) { // Touch: draw a circle wherever the screen is touched, and play a click on // each new touch-down. play_audio() is non-blocking, and the click is gated to // the touch-down edge so it doesn't retrigger every poll while held/dragging. + // + // The touch task polls at ~16 ms, so we must NOT draw a circle on every poll: + // a held/stationary finger would stack many translucent (LV_OPA_70) circles at + // the same point and they'd composite to look fully opaque. Draw only on a new + // touch-down or once the point has moved at least one radius, so a stationary + // touch draws a single circle and a drag leaves a spaced trail. + static constexpr int kCircleRadius = 10; static std::atomic touch_x{0}, touch_y{0}, touch_n{0}; board.initialize_touch([&](const auto &data) { static int prev_touch_n = 0; + static int last_drawn_x = 0, last_drawn_y = 0; auto td = board.touchpad_convert(data); touch_n = td.num_touch_points; touch_x = td.x; touch_y = td.y; if (td.num_touch_points > 0) { - if (prev_touch_n == 0 && !audio_bytes.empty()) { - board.play_audio(audio_bytes); // non-blocking + const bool new_touch = (prev_touch_n == 0); + if (new_touch && !audio_bytes.empty()) { + board.play_audio(audio_bytes); // non-blocking, touch-down edge only + } + const int dx = static_cast(td.x) - last_drawn_x; + const int dy = static_cast(td.y) - last_drawn_y; + const bool moved = (dx * dx + dy * dy) >= (kCircleRadius * kCircleRadius); + if (new_touch || moved) { + std::lock_guard lock(lvgl_mutex); + draw_circle(td.x, td.y, kCircleRadius); + last_drawn_x = td.x; + last_drawn_y = td.y; } - std::lock_guard lock(lvgl_mutex); - draw_circle(td.x, td.y, 10); } prev_touch_n = td.num_touch_points; }); diff --git a/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp b/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp index 5ceb5edf4..df5e0fe09 100644 --- a/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp +++ b/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp @@ -387,7 +387,11 @@ class Esp32P4FunctionEvBoard : public BaseComponent { // MIPI-DSI common parameters static constexpr int mipi_dsi_lanes = 2; - static constexpr int mipi_dsi_lane_bitrate_mbps = 1000; + // DSI HS lane bit rate. 900 Mbps matches Espressif's official + // EK79007_PANEL_BUS_DSI_2CH_CONFIG; using the wrong rate (e.g. 1000) mis-packs + // the pixel bits on the link, which shows up as shifted/oversaturated colors + // (notably on mid-tones / alpha-blended content) while white/black look fine. + 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; diff --git a/components/esp32-p4-function-ev-board/src/video.cpp b/components/esp32-p4-function-ev-board/src/video.cpp index 16830ef97..c8f4beb65 100644 --- a/components/esp32-p4-function-ev-board/src/video.cpp +++ b/components/esp32-p4-function-ev-board/src/video.cpp @@ -143,16 +143,14 @@ bool Esp32P4FunctionEvBoard::initialize_lcd() { logger_.error("Failed to create MIPI DSI DPI panel: {}", esp_err_to_name(ret)); return false; } -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) - // On ESP-IDF >= 6.0 the DPI panel needs DMA2D explicitly enabled for - // esp_lcd_panel_draw_bitmap() (the LVGL flush path) to copy into the - // framebuffer; without it the screen stays blank. - ret = esp_lcd_dpi_panel_enable_dma2d(lcd_handles_.panel); - if (ret != ESP_OK) { - logger_.error("Failed to enable DMA2D on DPI panel: {}", esp_err_to_name(ret)); - return false; - } -#endif + // 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 on + // this board — colors come out brighter/greener and alpha blends render + // wrong, while the bytes in the frame buffer are correct. The plain CPU copy + // path renders correctly and matches the m5stack-tab5 BSP, which also does + // not enable DMA2D on IDF >= 6. (An earlier "blank screen without DMA2D" was + // actually the RST_LCD/PWM jumper wiring, not DMA2D.) } // Send the panel controller's vendor init sequence over DBI (command mode), @@ -249,10 +247,10 @@ bool Esp32P4FunctionEvBoard::initialize_display(size_t pixel_buffer_size) { Display::DynamicMemoryConfig{ .pixel_buffer_size = pixel_buffer_size, .double_buffered = true, - // Allocate the LVGL draw buffers in PSRAM, not internal RAM. The - // MIPI-DSI DMA2D path can read PSRAM, and keeping these large buffers - // out of internal SRAM leaves room for FreeRTOS/pthread task stacks - // (which must be internal). + // Allocate the LVGL draw buffers in PSRAM to keep these large buffers + // out of internal SRAM, leaving room for the FreeRTOS/pthread task + // stacks that must be internal (e.g. the RTPS receive/announce tasks). + // The CPU-copy flush reads them coherently. .allocation_flags = MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT, }, Logger::Verbosity::WARN); From 87e22c5cadcec83f2fac803a2afa5cb7299b7671 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Wed, 24 Jun 2026 22:26:01 -0500 Subject: [PATCH 4/9] fix component compile on esp-idf <6.0; improve touch system to support interrupt driven touch --- components/esp32-p4-function-ev-board/Kconfig | 23 +++++++- .../esp32_p4_function_ev_board_example.cpp | 14 ++--- .../include/esp32-p4-function-ev-board.hpp | 22 ++++++-- .../src/ethernet.cpp | 37 ++++++++----- .../src/touchpad.cpp | 54 +++++++++++++------ 5 files changed, 106 insertions(+), 44 deletions(-) diff --git a/components/esp32-p4-function-ev-board/Kconfig b/components/esp32-p4-function-ev-board/Kconfig index 029621eca..b09729aa0 100644 --- a/components/esp32-p4-function-ev-board/Kconfig +++ b/components/esp32-p4-function-ev-board/Kconfig @@ -32,8 +32,27 @@ menu "ESP32-P4 Function EV Board Configuration" int "Touch polling task stack size (bytes)" default 4096 help - Size of the stack used for the GT911 touch polling task. The HMI subboard - does not route the touch interrupt to the ESP32-P4, so touch is polled. + Size of the stack used for the GT911 touch polling task. Used only in + polling mode (see ESP_P4_EV_BOARD_TOUCH_INTERRUPT). + + config ESP_P4_EV_BOARD_TOUCH_INTERRUPT + bool "Use interrupt-driven touch instead of polling" + default n + help + By default the ESP32-P4-HMI-Subboard does not route the GT911 touch INT pin + to the ESP32-P4, so touch is polled in a task. The LCD expansion header does + expose the touch INT pin: if you wire it to a free ESP32-P4 GPIO, enable this + to read the GT911 from a GPIO interrupt instead of polling (lower CPU usage + and latency). The GPIO is set below, and can also be overridden at runtime + via initialize_touch(). + + config ESP_P4_EV_BOARD_TOUCH_INTERRUPT_GPIO + int "Touch interrupt GPIO" + depends on ESP_P4_EV_BOARD_TOUCH_INTERRUPT + default 33 + help + GPIO that the GT911 touch INT pin (from the LCD expansion header) is wired + to. Must be a free GPIO not used by another on-board peripheral. config ESP_P4_EV_BOARD_AUDIO_TASK_STACK_SIZE int "Audio task stack size (bytes)" diff --git a/components/esp32-p4-function-ev-board/example/main/esp32_p4_function_ev_board_example.cpp b/components/esp32-p4-function-ev-board/example/main/esp32_p4_function_ev_board_example.cpp index 3cb90d694..916b951c5 100644 --- a/components/esp32-p4-function-ev-board/example/main/esp32_p4_function_ev_board_example.cpp +++ b/components/esp32-p4-function-ev-board/example/main/esp32_p4_function_ev_board_example.cpp @@ -269,28 +269,22 @@ extern "C" void app_main(void) { static constexpr int kCircleRadius = 10; static std::atomic touch_x{0}, touch_y{0}, touch_n{0}; board.initialize_touch([&](const auto &data) { - static int prev_touch_n = 0; - static int last_drawn_x = 0, last_drawn_y = 0; auto td = board.touchpad_convert(data); + static Board::TouchpadData prev_td = {}; touch_n = td.num_touch_points; touch_x = td.x; touch_y = td.y; if (td.num_touch_points > 0) { - const bool new_touch = (prev_touch_n == 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 } - const int dx = static_cast(td.x) - last_drawn_x; - const int dy = static_cast(td.y) - last_drawn_y; - const bool moved = (dx * dx + dy * dy) >= (kCircleRadius * kCircleRadius); - if (new_touch || moved) { + if (new_touch) { std::lock_guard lock(lvgl_mutex); draw_circle(td.x, td.y, kCircleRadius); - last_drawn_x = td.x; - last_drawn_y = td.y; } } - prev_touch_n = td.num_touch_points; + prev_td = td; }); // Run the LVGL task handler periodically diff --git a/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp b/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp index df5e0fe09..d16695503 100644 --- a/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp +++ b/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp @@ -91,6 +91,16 @@ class Esp32P4FunctionEvBoard : public BaseComponent { /// Mount point for the uSD card static constexpr char mount_point[] = "/sdcard"; + /// 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 + /// the configured GPIO (CONFIG_ESP_P4_EV_BOARD_TOUCH_INTERRUPT_GPIO). +#if CONFIG_ESP_P4_EV_BOARD_TOUCH_INTERRUPT + static constexpr gpio_num_t touch_interrupt_default = + static_cast(CONFIG_ESP_P4_EV_BOARD_TOUCH_INTERRUPT_GPIO); +#else + static constexpr gpio_num_t touch_interrupt_default = GPIO_NUM_NC; +#endif + /// @brief Access the singleton instance /// @return Reference to the singleton instance static Esp32P4FunctionEvBoard &get() { @@ -145,10 +155,16 @@ class Esp32P4FunctionEvBoard : public BaseComponent { /// Initialize the GT911 multi-touch controller /// \param callback The touchpad callback + /// \param interrupt_pin GPIO wired to the GT911 touch INT pin. If GPIO_NUM_NC + /// (the default, unless interrupt-driven touch is enabled via Kconfig), + /// the GT911 is polled in a task. If a valid GPIO is provided, touch is + /// read from a GPIO interrupt on that pin instead of polling. /// \return true if the touchpad was successfully initialized, false otherwise - /// \note The touch interrupt is not routed on this board, so a polling task is - /// used to read the GT911 and invoke the callback. - bool initialize_touch(const touch_callback_t &callback = nullptr); + /// \note The HMI subboard does not route the GT911 INT pin to the ESP32-P4 by + /// default (hence polling); the LCD expansion header exposes it, so wiring + /// it to a free GPIO enables the interrupt-driven path. + bool initialize_touch(const touch_callback_t &callback = nullptr, + gpio_num_t interrupt_pin = touch_interrupt_default); /// Get the number of bytes per pixel for the display /// \return The number of bytes per pixel diff --git a/components/esp32-p4-function-ev-board/src/ethernet.cpp b/components/esp32-p4-function-ev-board/src/ethernet.cpp index aac90952c..b3a7bf2e0 100644 --- a/components/esp32-p4-function-ev-board/src/ethernet.cpp +++ b/components/esp32-p4-function-ev-board/src/ethernet.cpp @@ -2,6 +2,14 @@ #if CONFIG_ESP_P4_EV_BOARD_ETHERNET +#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 @@ -91,19 +99,22 @@ bool Esp32P4FunctionEvBoard::initialize_ethernet(const ethernet_link_callback_t // NOTE: we can't use the ETH_ESP32_EMAC_DEFAULT_CONFIG macro because it's out // of order which is a hard error in c++20 and above. eth_esp32_emac_config_t esp32_emac_config = { - .smi_gpio = {.mdc_num = 31, .mdio_num = 52}, - .interface = EMAC_DATA_INTERFACE_RMII, - .clock_config = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = 50}}, - .dma_burst_len = ETH_DMA_BURST_LEN_32, - .intr_priority = 0, - .emac_dataif_gpio = {.rmii = {.tx_en_num = 49, - .txd0_num = 34, - .txd1_num = 35, - .crs_dv_num = 28, - .rxd0_num = 29, - .rxd1_num = 30}}, - .clock_config_out_in = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = -1}}, - .mdc_freq_hz = 0, + .smi_gpio = {.mdc_num = 31, .mdio_num = 52}, + .interface = EMAC_DATA_INTERFACE_RMII, + .clock_config = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = 50}}, + .dma_burst_len = ETH_DMA_BURST_LEN_32, + .intr_priority = 0, + .emac_dataif_gpio = {.rmii = {.tx_en_num = 49, + .txd0_num = 34, + .txd1_num = 35, + .crs_dv_num = 28, + .rxd0_num = 29, + .rxd1_num = 30}}, + .clock_config_out_in = {.rmii = {.clock_mode = EMAC_CLK_EXT_IN, .clock_gpio = -1}}, +// The below only exists in esp-idf >= v6.0 +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) + .mdc_freq_hz = 0, +#endif }; #pragma GCC diagnostic pop diff --git a/components/esp32-p4-function-ev-board/src/touchpad.cpp b/components/esp32-p4-function-ev-board/src/touchpad.cpp index 17e6dcc37..5ed6ece35 100644 --- a/components/esp32-p4-function-ev-board/src/touchpad.cpp +++ b/components/esp32-p4-function-ev-board/src/touchpad.cpp @@ -4,7 +4,8 @@ using namespace std::chrono_literals; namespace espp { -bool Esp32P4FunctionEvBoard::initialize_touch(const touch_callback_t &callback) { +bool Esp32P4FunctionEvBoard::initialize_touch(const touch_callback_t &callback, + gpio_num_t interrupt_pin) { if (touch_driver_) { logger_.warn("Touch driver already initialized"); return true; @@ -53,22 +54,43 @@ bool Esp32P4FunctionEvBoard::initialize_touch(const touch_callback_t &callback) .invert_y = touch_invert_y, .log_level = espp::Logger::Verbosity::WARN}); - // The touch interrupt is not routed to the ESP32-P4 on this board, so poll the - // GT911 in a task and invoke the user callback on new data. - touch_task_ = std::make_unique(espp::Task::Config{ - .callback = [this](std::mutex &m, std::condition_variable &cv) -> bool { - if (update_touch()) { - if (touch_callback_) { - touch_callback_(touchpad_data()); + if (interrupt_pin != GPIO_NUM_NC) { + // Interrupt-driven: read the GT911 only when its INT pin signals new data, + // instead of polling. update_touch() reads the touch point(s) and clears the + // GT911's data-ready flag (which de-asserts INT). We use ANY_EDGE so this + // works regardless of the GT911's configured INT polarity; a spurious edge + // just finds no new data and invokes nothing. + logger_.info("Touch in interrupt mode (GT911 INT on GPIO{})", static_cast(interrupt_pin)); + interrupts_.add_interrupt( + espp::Interrupt::PinConfig{.gpio_num = interrupt_pin, + .callback = + [this](const auto &) { + if (update_touch() && touch_callback_) { + touch_callback_(touchpad_data()); + } + }, + .active_level = espp::Interrupt::ActiveLevel::LOW, + .interrupt_type = espp::Interrupt::Type::ANY_EDGE, + .pullup_enabled = true}); + } else { + // The touch INT pin is not wired to a GPIO, so poll the GT911 in a task and + // invoke the user callback on new data. + logger_.info("Touch in polling mode (GT911 INT not wired)"); + touch_task_ = std::make_unique(espp::Task::Config{ + .callback = [this](std::mutex &m, std::condition_variable &cv) -> bool { + if (update_touch()) { + if (touch_callback_) { + touch_callback_(touchpad_data()); + } } - } - std::unique_lock lock(m); - cv.wait_for(lock, 16ms); - return false; // don't stop - }, - .task_config = {.name = "p4-ev touch", - .stack_size_bytes = CONFIG_ESP_P4_EV_BOARD_TOUCH_TASK_STACK_SIZE}}); - touch_task_->start(); + std::unique_lock lock(m); + cv.wait_for(lock, 16ms); + return false; // don't stop + }, + .task_config = {.name = "p4-ev touch", + .stack_size_bytes = CONFIG_ESP_P4_EV_BOARD_TOUCH_TASK_STACK_SIZE}}); + touch_task_->start(); + } logger_.info("Touch controller initialized"); return true; From d2b6d419eb0a66a81ac284ece627120497f4af44 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 25 Jun 2026 09:40:33 -0500 Subject: [PATCH 5/9] update docs and improve display initialization in example; tested and validated interrupt touch --- components/esp32-p4-function-ev-board/Kconfig | 9 +- .../esp32-p4-function-ev-board/README.md | 63 ++++-- .../example/README.md | 10 +- .../esp32_p4_function_ev_board_example.cpp | 203 ++++++++++-------- .../src/touchpad.cpp | 2 +- doc/en/esp32_p4_function_ev_board.rst | 6 + 6 files changed, 183 insertions(+), 110 deletions(-) diff --git a/components/esp32-p4-function-ev-board/Kconfig b/components/esp32-p4-function-ev-board/Kconfig index b09729aa0..ecc834d42 100644 --- a/components/esp32-p4-function-ev-board/Kconfig +++ b/components/esp32-p4-function-ev-board/Kconfig @@ -1,13 +1,12 @@ menu "ESP32-P4 Function EV Board Configuration" choice ESP_P4_EV_BOARD_DISPLAY - prompt "HMI subboard display panel (fallback/default)" + prompt "HMI subboard display panel" default ESP_P4_EV_BOARD_DISPLAY_EK79007 help - The BSP probes the attached MIPI-DSI panel at runtime (it reads the - ILI9881C ID over DSI and otherwise assumes EK79007), so this choice is only - the fallback used if probing is inconclusive. Set it to the panel you - normally use. + Select the MIPI-DSI panel attached to the HMI subboard. The BSP does not + auto-detect the panel (runtime DSI-ID probing was removed because it hangs + the boot watchdog on the EK79007), so set this to the panel you have. config ESP_P4_EV_BOARD_DISPLAY_EK79007 bool "EK79007 (7-inch, 1024x600)" diff --git a/components/esp32-p4-function-ev-board/README.md b/components/esp32-p4-function-ev-board/README.md index 104a5588d..55f30a1d6 100644 --- a/components/esp32-p4-function-ev-board/README.md +++ b/components/esp32-p4-function-ev-board/README.md @@ -48,8 +48,9 @@ initializes and exposes the board's peripherals: - **MIPI-DSI display** — Kconfig-selectable **EK79007 (7", 1024x600)** or **ILI9881C (10.1", 800x1280)** — with LVGL integration and PWM backlight. -- **GT911 capacitive multi-touch** (the touch interrupt is not routed on this - board, so touch is polled). +- **GT911 capacitive multi-touch** — polled by default (the touch INT pin is not + routed to the ESP32-P4 on this board), or **interrupt-driven** if you wire the + INT pin from the LCD expansion header to a GPIO (see [Touch mode](#touch-mode-polling-vs-interrupt)). - **ES8311 audio codec** (+ NS4150B speaker amplifier) over I2S for playback. - **10/100 Ethernet** (EMAC + IP101 RMII PHY) with DHCP. - **microSD card** (4-bit SDMMC, powered via the on-chip LDO). @@ -61,17 +62,37 @@ All on-board control lines are direct ESP32-P4 GPIOs (this board has no I/O expander). The display, touch, and audio codec share a single I2C bus (`SDA=GPIO7`, `SCL=GPIO8`). -## Display panel detection - -`initialize_lcd()` **probes the attached panel at runtime**: it brings up the -MIPI-DSI bus, reads the ILI9881C ID over DSI, and selects **ILI9881C** if it -matches or **EK79007** otherwise. It then applies that panel's resolution, DPI -timing, backlight GPIO (26 for EK79007, 23 for ILI9881C), and reset GPIO, and -`get_display_controller()` / `display_width()` / `display_height()` reflect the -detected panel. The probed ID bytes are logged (`Panel probe ID: ...`). - -The Kconfig choice under *ESP32-P4 Function EV Board Configuration* is only the -**fallback** used if probing is inconclusive. +## Display panel selection + +The active panel is selected at **compile time via Kconfig** +(*ESP32-P4 Function EV Board Configuration* → display panel: EK79007 1024x600 by +default, or ILI9881C 800x1280). `initialize_lcd()` applies that panel's +resolution, DPI timing, backlight GPIO (26 for EK79007, 23 for ILI9881C), and +reset GPIO, and `get_display_controller()` / `display_width()` / +`display_height()` reflect the configured panel. + +> [!NOTE] +> The BSP does **not** auto-detect the attached panel. Runtime probing (reading +> the panel ID over DSI) was tried but removed: the EK79007 does not answer DSI +> reads and the read poll has no timeout, which hung the boot watchdog. This +> matches Espressif's esp-bsp, which also selects the panel via Kconfig. Set the +> Kconfig choice to the panel you have. + +## Touch mode (polling vs interrupt) + +By default the GT911 is **polled** in a task, because the ESP32-P4-HMI-Subboard +does not route the touch INT pin to the ESP32-P4. The LCD expansion header *does* +expose the INT pin, so you can wire it to a free GPIO and switch to +**interrupt-driven** touch (lower CPU usage and latency): + +- **Kconfig** (*ESP32-P4 Function EV Board Configuration*): + - `ESP_P4_EV_BOARD_TOUCH_INTERRUPT` — use interrupt-driven touch instead of polling. + - `ESP_P4_EV_BOARD_TOUCH_INTERRUPT_GPIO` — the GPIO the INT pin is wired to. +- **API**: `initialize_touch(callback, interrupt_pin)`. Pass `GPIO_NUM_NC` to + poll, or a valid GPIO to read the GT911 from an interrupt on that pin. The + default `interrupt_pin` (`touch_interrupt_default`) follows the Kconfig setting, + so an unchanged `initialize_touch(cb)` call uses whichever mode you selected in + Kconfig; pass an explicit GPIO to override at runtime. ## Example @@ -81,11 +102,17 @@ up the SD card, audio, Ethernet, and BOOT button. ## Peripheral status / notes -- **Ethernet**: uses the ESP-IDF internal EMAC with the generic 802.3 PHY driver - for the IP101 (the dedicated `esp_eth_phy_ip101` driver is a separate managed - component in newer ESP-IDF; the generic driver works for this board). The RMII - pinout is the ESP-IDF ESP32-P4 default (MDC=31, MDIO=52, REF_CLK in=50, - TX_EN=49, TXD0=34, TXD1=35, CRS_DV=28, RXD0=29, RXD1=30; PHY reset=51, addr=1). +- **Ethernet**: uses the ESP-IDF internal EMAC with the **generic 802.3 PHY + driver** (`esp_eth_phy_new_generic`) for the IP101. The dedicated + `esp_eth_phy_ip101` driver is a separate registry component on ESP-IDF v6+, and + this BSP builds with the component manager disabled, so the generic 802.3 driver + is used instead — it drives the IP101 on this board. The RMII pinout is the + ESP-IDF ESP32-P4 default (MDC=31, MDIO=52, REF_CLK in=50, TX_EN=49, TXD0=34, + TXD1=35, CRS_DV=28, RXD0=29, RXD1=30; PHY reset=51, addr=1). +- **BOOT button**: GPIO35 is shared with Ethernet **RMII TXD1**, so the button + cannot be used as a runtime input while Ethernet is enabled — claiming the pin + would take down Ethernet TX. `initialize_button()` refuses (returns `false`) + while Ethernet is active. - **Camera**: the SC2336/OV5647 MIPI-CSI sensor's pins are documented (SCCB on the internal I2C bus at 0x30, reset/XCLK not connected), but the `esp_video` capture pipeline is **not yet implemented**; `initialize_camera()` returns diff --git a/components/esp32-p4-function-ev-board/example/README.md b/components/esp32-p4-function-ev-board/example/README.md index 3e4597c81..15c23e048 100644 --- a/components/esp32-p4-function-ev-board/example/README.md +++ b/components/esp32-p4-function-ev-board/example/README.md @@ -26,8 +26,14 @@ It: ## Configuration Use `idf.py menuconfig` → *ESP32-P4 Function EV Board Configuration* to select -the display panel (EK79007 1024x600 by default, or ILI9881C 800x1280) and adjust -task stack sizes / enable Ethernet. +the display panel (EK79007 1024x600 by default, or ILI9881C 800x1280), adjust +task stack sizes, and enable Ethernet. + +By default touch is **polled**. If you wire the GT911 touch INT pin (from the LCD +expansion header) to a free GPIO, enable **`Use interrupt-driven touch instead of +polling`** and set **`Touch interrupt GPIO`** to read touch from an interrupt +instead. The example needs no code change — `initialize_touch()` follows the +Kconfig setting. ## Build and Flash diff --git a/components/esp32-p4-function-ev-board/example/main/esp32_p4_function_ev_board_example.cpp b/components/esp32-p4-function-ev-board/example/main/esp32_p4_function_ev_board_example.cpp index 916b951c5..fb0f1c7bd 100644 --- a/components/esp32-p4-function-ev-board/example/main/esp32_p4_function_ev_board_example.cpp +++ b/components/esp32-p4-function-ev-board/example/main/esp32_p4_function_ev_board_example.cpp @@ -141,59 +141,6 @@ extern "C" void app_main(void) { return; } - // microSD (optional — only present if a card is inserted) - bool sd_ok = board.initialize_sdcard({.format_if_mount_failed = false}); - uint32_t sd_size_mb = 0, sd_free_mb = 0; - if (sd_ok) { - board.get_sd_card_info(&sd_size_mb, &sd_free_mb); - logger.info("SD card: {} MB total, {} MB free", sd_size_mb, sd_free_mb); - } else { - logger.warn("No SD card mounted"); - } - - // Audio (ES8311) — load the embedded click sound first so we can initialize - // the codec directly at the clip's sample rate (changing the sample rate after - // the audio task is running is racy, so we avoid it here). - size_t wav_size = 0, wav_sample_rate = 0; - bool have_audio = load_audio(wav_size, wav_sample_rate); - uint32_t audio_rate = have_audio ? static_cast(wav_sample_rate) : 48000; - if (board.initialize_audio(audio_rate)) { - board.mute(false); - board.volume(60.0f); - if (have_audio) { - logger.info("Loaded {} bytes of click audio @ {} Hz", wav_size, wav_sample_rate); - } - } - - // Ethernet (IP101) — DHCP; the callback fires once an IP is acquired - static std::atomic have_ip{false}; - static std::string ip_str{"(no link)"}; - board.initialize_ethernet([&](esp_ip4_addr_t ip) { - char buf[16]; - esp_ip4addr_ntoa(&ip, buf, sizeof(buf)); - ip_str = buf; - have_ip = true; - logger.info("Ethernet IP: {}", ip_str); - }); - - // BOOT button — clears the drawn circles - // - // NOTE: GPIO35 is shared with Ethernet RMII TXD1 on this board, so the BOOT - // button can't be used as a runtime input while Ethernet is active - // (initialize_button() refuses to run when Ethernet is up). It is - // disabled here since this example uses Ethernet. - bool button_initialized = board.initialize_button([&](const auto &event) { - if (event.active) { - std::lock_guard lock(lvgl_mutex); - clear_circles(); - } - }); - if (button_initialized) { - logger.error("BOOT button incorrectly initialized while Ethernet is active!"); - } else { - logger.info("BOOT button not initialized (shared with Ethernet RMII TXD1 pin)"); - } - // Build the LVGL UI: a title, a status label, a rotate button, and a // transparent layer that the touch handler draws circles onto. lv_obj_t *bg = nullptr; @@ -257,6 +204,61 @@ extern "C" void app_main(void) { rotate_btn, [](lv_event_t *) { rotate_display(); }, LV_EVENT_CLICKED, nullptr); } + // On-screen status state. These are filled in as each subsystem initializes + // below, and rendered immediately by the status task, so the display shows SD / + // Ethernet / RTPS coming online live instead of staying blank until the whole + // bring-up finishes. + static std::atomic touch_x{0}, touch_y{0}, touch_n{0}; + static std::atomic sd_card_mounted{false}; + static std::atomic sd_card_size_mb{0}; + static std::atomic rtps_running{false}, rtps_has_peers{false}; + static std::atomic rtps_value{0}; + static int64_t status_start_us = esp_timer_get_time(); + + // Status updater: starts now (right after the display is up) and refreshes the + // on-screen status ~10x/s. Ethernet state is read live from the board; SD and + // RTPS state are published into the atomics above as those subsystems come up. + espp::Task status_task(espp::Task::Config{ + .callback = [&board](std::mutex &m, std::condition_variable &cv) -> bool { + const size_t free_internal = heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024; + const size_t free_psram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM) / 1024; + const int uptime_s = static_cast((esp_timer_get_time() - status_start_us) / 1'000'000); + std::string eth_text = "(no link)"; + if (board.is_ethernet_connected()) { + auto ip = board.ethernet_ip(); + eth_text = std::to_string(esp_ip4_addr1_16(&ip)) + "." + + std::to_string(esp_ip4_addr2_16(&ip)) + "." + + std::to_string(esp_ip4_addr3_16(&ip)) + "." + + std::to_string(esp_ip4_addr4_16(&ip)); + } + std::string rtps_text = + rtps_running ? ("publishing #" + std::to_string(rtps_value.load()) + + (rtps_has_peers ? "" : " (no peers)")) + : (board.is_ethernet_connected() ? std::string("not started") + : std::string("waiting for network")); + std::string status = + "Panel: " + std::string(board.get_display_controller_name()) + " (" + + std::to_string(board.display_width()) + "x" + std::to_string(board.display_height()) + + ")\n" + "Touch: " + std::to_string(touch_n.load()) + " pts (" + + std::to_string(touch_x.load()) + ", " + std::to_string(touch_y.load()) + ")\n" + + "SD card: " + + (sd_card_mounted ? std::to_string(sd_card_size_mb.load()) + " MB" : "none") + "\n" + + "Ethernet: " + eth_text + "\n" + "RTPS: " + rtps_text + "\n" + + "System: " + std::to_string(free_internal) + " KB int, " + + std::to_string(free_psram) + " KB psram free, up " + std::to_string(uptime_s) + " s"; + { + std::lock_guard lock(lvgl_mutex); + if (status_label) { + lv_label_set_text(status_label, status.c_str()); + } + } + std::unique_lock lock(m); + cv.wait_for(lock, 100ms); + return false; + }, + .task_config = {.name = "p4-ev status", .stack_size_bytes = 6144}}); + status_task.start(); + // Touch: draw a circle wherever the screen is touched, and play a click on // each new touch-down. play_audio() is non-blocking, and the click is gated to // the touch-down edge so it doesn't retrigger every poll while held/dragging. @@ -267,7 +269,6 @@ extern "C" void app_main(void) { // touch-down or once the point has moved at least one radius, so a stationary // touch draws a single circle and a drag leaves a spaced trail. static constexpr int kCircleRadius = 10; - static std::atomic touch_x{0}, touch_y{0}, touch_n{0}; board.initialize_touch([&](const auto &data) { auto td = board.touchpad_convert(data); static Board::TouchpadData prev_td = {}; @@ -300,6 +301,61 @@ extern "C" void app_main(void) { .task_config = {.name = "lvgl", .stack_size_bytes = 8192}}); lv_task.start(); + // microSD (optional — only present if a card is inserted) + bool sd_ok = board.initialize_sdcard({.format_if_mount_failed = false}); + uint32_t sd_size_mb = 0, sd_free_mb = 0; + if (sd_ok) { + board.get_sd_card_info(&sd_size_mb, &sd_free_mb); + logger.info("SD card: {} MB total, {} MB free", sd_size_mb, sd_free_mb); + } else { + logger.warn("No SD card mounted"); + } + sd_card_mounted = sd_ok; + sd_card_size_mb = sd_size_mb; // published to the status task + + // Audio (ES8311) — load the embedded click sound first so we can initialize + // the codec directly at the clip's sample rate (changing the sample rate after + // the audio task is running is racy, so we avoid it here). + size_t wav_size = 0, wav_sample_rate = 0; + bool have_audio = load_audio(wav_size, wav_sample_rate); + uint32_t audio_rate = have_audio ? static_cast(wav_sample_rate) : 48000; + if (board.initialize_audio(audio_rate)) { + board.mute(false); + board.volume(60.0f); + if (have_audio) { + logger.info("Loaded {} bytes of click audio @ {} Hz", wav_size, wav_sample_rate); + } + } + + // Ethernet (IP101) — DHCP; the callback fires once an IP is acquired + static std::atomic have_ip{false}; + static std::string ip_str{"(no link)"}; + board.initialize_ethernet([&](esp_ip4_addr_t ip) { + char buf[16]; + esp_ip4addr_ntoa(&ip, buf, sizeof(buf)); + ip_str = buf; + have_ip = true; + logger.info("Ethernet IP: {}", ip_str); + }); + + // BOOT button — clears the drawn circles + // + // NOTE: GPIO35 is shared with Ethernet RMII TXD1 on this board, so the BOOT + // button can't be used as a runtime input while Ethernet is active + // (initialize_button() refuses to run when Ethernet is up). It is + // disabled here since this example uses Ethernet. + bool button_initialized = board.initialize_button([&](const auto &event) { + if (event.active) { + std::lock_guard lock(lvgl_mutex); + clear_circles(); + } + }); + if (button_initialized) { + logger.error("BOOT button incorrectly initialized while Ethernet is active!"); + } else { + logger.info("BOOT button not initialized (shared with Ethernet RMII TXD1 pin)"); + } + // Connectivity self-test: once we have an IP (and a moment for RTPS discovery), // ping the gateway and the discovered peer once, then stop. This makes it easy // to tell board-vs-network problems apart (e.g. gateway reachable but peer not @@ -341,16 +397,16 @@ extern "C" void app_main(void) { ping_task.start(); //! [esp32 p4 function ev board example] - // Once we have an IP, start an RTPS participant that publishes a counter. - // The display status is refreshed at 50 Hz while RTPS publishes at 2 Hz. + // Once we have an IP, start an RTPS participant that publishes a counter. The + // on-screen status is rendered separately by status_task above; this loop drives + // RTPS and publishes its state into the rtps_* atomics for the status task. static bool did_have_ip = false; std::shared_ptr participant = nullptr; const std::string topic = "espp/test/counter"; const std::string rtps_type = "std_msgs::msg::dds_::UInt32_"; uint32_t value = 0; bool published = false; - const int64_t start_us = esp_timer_get_time(); - static constexpr auto status_period = 20ms; // 50 Hz display status update + static constexpr auto loop_tick = 20ms; // RTPS loop tick static constexpr int64_t publish_period_us = 500'000; // 2 Hz RTPS publish int64_t last_publish_us = 0; @@ -398,6 +454,7 @@ extern "C" void app_main(void) { participant.reset(); did_have_ip = false; } + rtps_running = (participant != nullptr); // Publish the next counter value at 2 Hz (independent of the status refresh). // Only publish if there is a discovered peer (otherwise the publish() call will return false). @@ -415,33 +472,11 @@ extern "C" void app_main(void) { } } - // Build and show the on-screen status at 50 Hz. - size_t free_internal = heap_caps_get_free_size(MALLOC_CAP_INTERNAL) / 1024; - size_t free_psram = heap_caps_get_free_size(MALLOC_CAP_SPIRAM) / 1024; - int uptime_s = static_cast((now_us - start_us) / 1'000'000); - std::string rtps_text = - participant ? ("publishing '" + topic + "' #" + std::to_string(value) + - (published ? "" : " (no peers)")) - : (have_ip ? std::string("not started") : std::string("waiting for network")); - std::string status = - "Panel: " + std::string(board.get_display_controller_name()) + " (" + - std::to_string(board.display_width()) + "x" + std::to_string(board.display_height()) + - ")\n" + "Touch: " + std::to_string(touch_n.load()) + " pts (" + - std::to_string(touch_x.load()) + ", " + std::to_string(touch_y.load()) + ")\n" + - "SD card: " + (sd_ok ? std::to_string(sd_size_mb) + " MB" : "none") + "\n" + - "Ethernet: " + (have_ip ? ip_str : std::string("(no link)")) + "\n" + - "RTPS: " + rtps_text + "\n" + "System: " + std::to_string(free_internal) + - " KB int, " + std::to_string(free_psram) + " KB psram free, up " + - std::to_string(uptime_s) + " s"; - - { - std::lock_guard lock(lvgl_mutex); - if (status_label) { - lv_label_set_text(status_label, status.c_str()); - } - } + // Publish the RTPS counter/peer state for the status task to render. + rtps_value = value; + rtps_has_peers = published; - std::this_thread::sleep_for(status_period); + std::this_thread::sleep_for(loop_tick); } } diff --git a/components/esp32-p4-function-ev-board/src/touchpad.cpp b/components/esp32-p4-function-ev-board/src/touchpad.cpp index 5ed6ece35..145c7ad65 100644 --- a/components/esp32-p4-function-ev-board/src/touchpad.cpp +++ b/components/esp32-p4-function-ev-board/src/touchpad.cpp @@ -70,7 +70,7 @@ bool Esp32P4FunctionEvBoard::initialize_touch(const touch_callback_t &callback, } }, .active_level = espp::Interrupt::ActiveLevel::LOW, - .interrupt_type = espp::Interrupt::Type::ANY_EDGE, + .interrupt_type = espp::Interrupt::Type::FALLING_EDGE, .pullup_enabled = true}); } else { // The touch INT pin is not wired to a GPIO, so poll the GT911 in a task and diff --git a/doc/en/esp32_p4_function_ev_board.rst b/doc/en/esp32_p4_function_ev_board.rst index 22ae4a4b2..746ce08fb 100644 --- a/doc/en/esp32_p4_function_ev_board.rst +++ b/doc/en/esp32_p4_function_ev_board.rst @@ -14,6 +14,12 @@ abstraction for initializing the display, touch, audio, Ethernet, and SD card subsystems. The display panel (EK79007 1024x600 or ILI9881C 800x1280) is selectable via Kconfig. +Touch is **polled** by default (the GT911 INT pin is not routed to the ESP32-P4 +on this board). If you wire the INT pin from the LCD expansion header to a free +GPIO, you can switch to **interrupt-driven** touch via Kconfig +(``ESP_P4_EV_BOARD_TOUCH_INTERRUPT`` / ``..._GPIO``) or by passing the GPIO to +``initialize_touch()``. + Official board documentation: - `ESP32-P4-Function-EV-Board User Guide `_ (includes the ESP32-P4-HMI-Subboard) From 3aacc764ec238c0a32228c7950b63f679dc3b395 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 25 Jun 2026 10:11:12 -0500 Subject: [PATCH 6/9] update readme --- .../example/README.md | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/components/esp32-p4-function-ev-board/example/README.md b/components/esp32-p4-function-ev-board/example/README.md index 15c23e048..384e2a24d 100644 --- a/components/esp32-p4-function-ev-board/example/README.md +++ b/components/esp32-p4-function-ev-board/example/README.md @@ -3,6 +3,8 @@ This example demonstrates the `espp::Esp32P4FunctionEvBoard` BSP for the Espressif ESP32-P4 Function EV Board + ESP32-P4-HMI-Subboard. +image + It: - initializes the MIPI-DSI display (EK79007 or ILI9881C, per Kconfig) and the @@ -48,3 +50,69 @@ idf.py build flash monitor buffers. - Plug an Ethernet cable to see the DHCP-assigned IP appear on screen and in the log. + +## Example Output + + ``` +I (1352) main_task: Calling app_main() +[ESP32-P4 Function EV Board Example/I][1.355]: Starting example! +[ESP32-P4 Function EV Board Example/I][1.363]: Display panel: EK79007 +[ESP32-P4 Function EV Board Example/I][1.388]: Found 2 I2C device(s) +[Esp32P4FunctionEvBoard/I][1.388]: Initializing LCD (MIPI-DSI) +[Esp32P4FunctionEvBoard/I][1.390]: Performing LCD hardware reset on GPIO27 +[Esp32P4FunctionEvBoard/I][1.529]: Creating MIPI DSI bus (2 lanes, 900 Mbps/lane) +[Esp32P4FunctionEvBoard/I][1.530]: Installing MIPI DSI DBI panel IO +[Esp32P4FunctionEvBoard/I][1.532]: Using display panel: EK79007 (1024x600) +[Esp32P4FunctionEvBoard/I][1.540]: Creating DPI panel (1024x600 @ 52 MHz) +[Esp32P4FunctionEvBoard/I][1.675]: LCD initialization completed (EK79007) +[Esp32P4FunctionEvBoard/I][1.675]: Initializing LVGL display with pixel buffer size: 51200 pixels +[Display/W][1.680]: No rotation callback provided, resolution changed event will not automatically update the display hardware rotation. +[Esp32P4FunctionEvBoard/I][1.695]: LVGL display initialized +[Esp32P4FunctionEvBoard/I][1.703]: Initializing GT911 multi-touch controller +[Esp32P4FunctionEvBoard/I][1.707]: Using GT911 at address 0x5D +[Esp32P4FunctionEvBoard/I][1.713]: Touch in interrupt mode (GT911 INT on GPIO33) +[Esp32P4FunctionEvBoard/I][1.721]: Touch controller initialized +[Esp32P4FunctionEvBoard/I][1.762]: Initializing SD card (4-bit SDMMC) +E (1790) sdmmc_common: sdmmc_init_ocr: send_op_cond (1) returned 0x107 +HINT: Please reboot the board and then try again +E (1790) vfs_fat_sdmmc: sdmmc_card_init failed (0x107). +HINT: Please verify if there is an SD card inserted into the SD slot. Then, try rebooting the board. +E (1791) vfs_fat_sdmmc: esp_vfs_fat_sdmmc_sdcard_init failed (0x107). +[Esp32P4FunctionEvBoard/W][1.797]: Failed to initialize the card (ESP_ERR_TIMEOUT). Make sure an SD card is inserted. +[ESP32-P4 Function EV Board Example/W][1.808]: No SD card mounted +[Esp32P4FunctionEvBoard/I][1.819]: Initializing audio (ES8311) at 44100 Hz +W (1822) i2s_common: dma frame num is adjusted to 256 to align the dma buffer with 64, bufsize = 512 +I (1833) DRV8311: ES8311 in Slave mode +[ESP32-P4 Function EV Board Example/I][1.843]: Loaded 35875 bytes of click audio @ 44100 Hz +[Esp32P4FunctionEvBoard/I][1.843]: Initializing Ethernet (EMAC + IP101 EMAC) +[Esp32P4FunctionEvBoard/I][1.850]: Creating ESP32 EMAC +[Esp32P4FunctionEvBoard/I][1.857]: Creating generic PHY (IP101) +[Esp32P4FunctionEvBoard/I][1.862]: Installing Ethernet driver +I (2410) esp_eth.netif.netif_glue: 60:55:f9:f9:49:a1 +I (2410) esp_eth.netif.netif_glue: ethernet attached to netif +[Esp32P4FunctionEvBoard/I][4.610]: Ethernet started +[Esp32P4FunctionEvBoard/I][4.610]: Ethernet initialized; waiting for link/DHCP +[Esp32P4FunctionEvBoard/I][4.612]: Ethernet link up: 100 Mbps, full duplex +[Esp32P4FunctionEvBoard/E][4.619]: BOOT button shares GPIO35 with Ethernet RMII TXD1; refusing to initialize it while Ethernet is up (it would kill Ethernet TX). +[ESP32-P4 Function EV Board Example/I][4.637]: BOOT button not initialized (shared with Ethernet RMII TXD1 pin) +I (6758) esp_netif_handlers: eth ip: 192.168.1.31, mask: 255.255.255.0, gw: 192.168.1.1 +[Esp32P4FunctionEvBoard/I][6.758]: Ethernet got IP: 192.168.1.31 +[ESP32-P4 Function EV Board Example/I][6.761]: Ethernet IP: 192.168.1.31 +[ESP32-P4 Function EV Board Example/I][6.770]: Got IP 192.168.1.31, starting RTPS participant +[ESP32-P4 Function EV Board Example/I][10.905]: === Connectivity self-test (ping) === +[ESP32-P4 Function EV Board Example/I][10.905]: Pinging gateway (192.168.1.1)... +[ESP32-P4 Function EV Board Example/I][10.912]: gateway: seq=1207959553 ttl=1341597504 time=0ms (64 bytes) +[ESP32-P4 Function EV Board Example/I][11.411]: gateway: seq=1207959554 ttl=1341597504 time=0ms (64 bytes) +[ESP32-P4 Function EV Board Example/I][11.910]: gateway: seq=1207959555 ttl=1341597504 time=0ms (64 bytes) +[ESP32-P4 Function EV Board Example/I][12.410]: gateway: seq=1207959556 ttl=1341597504 time=0ms (64 bytes) +[ESP32-P4 Function EV Board Example/I][12.910]: Ping gateway (192.168.1.1): 4/4 received, 0% loss, avg 0 ms +[ESP32-P4 Function EV Board Example/W][12.910]: Ping self-test: no RTPS peer discovered yet to ping +[ESP32-P4 Function EV Board Example/I][12.918]: === Connectivity self-test done === + ``` + +image + + +https://github.com/user-attachments/assets/b6fe4516-af14-455f-9c21-8f5d4323dd56 + + From 4ecc00a4b97961868c2b1d009bf4dd85a9f0cce4 Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 25 Jun 2026 10:15:45 -0500 Subject: [PATCH 7/9] ensure ethernet is included in docs --- .../include/esp32-p4-function-ev-board.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp b/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp index d16695503..13bb47e1b 100644 --- a/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp +++ b/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp @@ -308,7 +308,7 @@ class Esp32P4FunctionEvBoard : public BaseComponent { /// \return True if info retrieved successfully bool get_sd_card_info(uint32_t *size_mb, uint32_t *free_mb) const; -#if CONFIG_ESP_P4_EV_BOARD_ETHERNET +#if CONFIG_ESP_P4_EV_BOARD_ETHERNET || defined(_DOXYGEN_) ///////////////////////////////////////////////////////////////////////////// // Ethernet (EMAC + IP101 RMII PHY) ///////////////////////////////////////////////////////////////////////////// @@ -329,7 +329,7 @@ class Esp32P4FunctionEvBoard : public BaseComponent { /// Get the most recently acquired IPv4 address (0 if none) /// \return The IPv4 address esp_ip4_addr_t ethernet_ip() const { return ethernet_ip_; } -#endif // CONFIG_ESP_P4_EV_BOARD_ETHERNET +#endif // CONFIG_ESP_P4_EV_BOARD_ETHERNET || defined(_DOXYGEN_) ///////////////////////////////////////////////////////////////////////////// // Camera (MIPI-CSI) — pins wired, capture pipeline is a stub From 1cb3bacf60679924187025f13e4b12a270f0a86e Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 25 Jun 2026 10:20:24 -0500 Subject: [PATCH 8/9] fix comp manifest --- components/esp32-p4-function-ev-board/idf_component.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/esp32-p4-function-ev-board/idf_component.yml b/components/esp32-p4-function-ev-board/idf_component.yml index 932470db4..70af2f282 100644 --- a/components/esp32-p4-function-ev-board/idf_component.yml +++ b/components/esp32-p4-function-ev-board/idf_component.yml @@ -12,8 +12,8 @@ tags: - cpp - Component - BSP - - ESP32-P4 - - EV-Board + - ESP32P4 + - EVBoard - HMI dependencies: idf: ">=5.3" From 87563e74d1c28947887258b0e359eb2c9bfa5a4d Mon Sep 17 00:00:00 2001 From: William Emfinger Date: Thu, 25 Jun 2026 10:22:48 -0500 Subject: [PATCH 9/9] ensure display driver is added as well --- doc/en/display/display_drivers.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/en/display/display_drivers.rst b/doc/en/display/display_drivers.rst index 1f3c7b4f5..5230fc445 100755 --- a/doc/en/display/display_drivers.rst +++ b/doc/en/display/display_drivers.rst @@ -25,6 +25,7 @@ API Reference ------------- .. include-build-file:: inc/display_drivers.inc +.. include-build-file:: inc/ek79007.inc .. include-build-file:: inc/gc9a01.inc .. include-build-file:: inc/ili9341.inc .. include-build-file:: inc/ili9881.inc