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..5cada4f5b --- /dev/null +++ b/components/esp32-p4-function-ev-board/CMakeLists.txt @@ -0,0 +1,24 @@ +idf_component_register( + INCLUDE_DIRS "include" + SRC_DIRS "src" + 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..ecc834d42 --- /dev/null +++ b/components/esp32-p4-function-ev-board/Kconfig @@ -0,0 +1,69 @@ +menu "ESP32-P4 Function EV Board Configuration" + + choice ESP_P4_EV_BOARD_DISPLAY + prompt "HMI subboard display panel" + default ESP_P4_EV_BOARD_DISPLAY_EK79007 + help + 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)" + 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. 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)" + 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..55f30a1d6 --- /dev/null +++ b/components/esp32-p4-function-ev-board/README.md @@ -0,0 +1,124 @@ +# 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** — 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). +- **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 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 + +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** (`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 + 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..384e2a24d --- /dev/null +++ b/components/esp32-p4-function-ev-board/example/README.md @@ -0,0 +1,118 @@ +# 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. + +image + +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), 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 + +```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. + +## 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 + + 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 000000000..2183344a2 Binary files /dev/null and b/components/esp32-p4-function-ev-board/example/main/click.wav differ 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 new file mode 100644 index 000000000..fb0f1c7bd --- /dev/null +++ b/components/esp32-p4-function-ev-board/example/main/esp32_p4_function_ev_board_example.cpp @@ -0,0 +1,605 @@ +/** + * @file esp32_p4_function_ev_board_example.cpp + * @brief ESP32-P4 Function EV Board BSP example + * + * Demonstrates the BSP: MIPI-DSI display + GT911 touch (draw circles and play a + * click sound wherever you touch), microSD, audio (ES8311), Ethernet (IP101) + * with an RTPS publisher, and the BOOT button. Shows a live on-screen status + * read-out (panel, touch, SD, Ethernet, RTPS, and system memory/uptime). + */ + +#include +#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; + } + + // 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); + } + + // 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. + // + // 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; + board.initialize_touch([&](const auto &data) { + 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_td != td); + if (new_touch && !audio_bytes.empty()) { + board.play_audio(audio_bytes); // non-blocking, touch-down edge only + } + if (new_touch) { + std::lock_guard lock(lvgl_mutex); + draw_circle(td.x, td.y, kCircleRadius); + } + } + prev_td = td; + }); + + // 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(); + + // 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 + // => 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 + // 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; + 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; + + 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; + } + 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). + 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); + } + } + + // Publish the RTPS counter/peer state for the status task to render. + rtps_value = value; + rtps_has_peers = published; + + std::this_thread::sleep_for(loop_tick); + } +} + +////////////////////////////////////////////////////////////////////////////// +// 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..70af2f282 --- /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 + - ESP32P4 + - EVBoard + - 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..13bb47e1b --- /dev/null +++ b/components/esp32-p4-function-ev-board/include/esp32-p4-function-ev-board.hpp @@ -0,0 +1,580 @@ +#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"; + + /// 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() { + 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 + /// \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 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 + 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 || defined(_DOXYGEN_) + ///////////////////////////////////////////////////////////////////////////// + // 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 || defined(_DOXYGEN_) + + ///////////////////////////////////////////////////////////////////////////// + // 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; + // 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; + + 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..b3a7bf2e0 --- /dev/null +++ b/components/esp32-p4-function-ev-board/src/ethernet.cpp @@ -0,0 +1,196 @@ +#include "esp32-p4-function-ev-board.hpp" + +#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 +#include +#include + +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}}, +// 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 + + // 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..145c7ad65 --- /dev/null +++ b/components/esp32-p4-function-ev-board/src/touchpad.cpp @@ -0,0 +1,169 @@ +#include "esp32-p4-function-ev-board.hpp" + +using namespace std::chrono_literals; + +namespace espp { + +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; + } + + 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}); + + 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::FALLING_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(); + } + + 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..c8f4beb65 --- /dev/null +++ b/components/esp32-p4-function-ev-board/src/video.cpp @@ -0,0 +1,396 @@ +#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; + } + // 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), + // 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 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); + } + + 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/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/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 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..746ce08fb --- /dev/null +++ b/doc/en/esp32_p4_function_ev_board.rst @@ -0,0 +1,60 @@ +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. + +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) +- `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