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)
Display panels (HMI subboard). Both panels plug into the shared LCD adapter board via its FPC connector:
EK79007 (7”, 1024x600) — the panel Espressif ships/documents for this board:
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.
API Reference
Header File
Classes
-
class Esp32P4FunctionEvBoard : public espp::BaseComponent
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
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.
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<uint8_t> 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<std::recursive_mutex> 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<std::recursive_mutex> lock(lvgl_mutex); clear_circles(); static auto rotation = LV_DISPLAY_ROTATION_0; rotation = static_cast<lv_display_rotation_t>((static_cast<int>(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<std::recursive_mutex> 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<int> touch_x{0}, touch_y{0}, touch_n{0}; static std::atomic<bool> sd_card_mounted{false}; static std::atomic<uint32_t> sd_card_size_mb{0}; static std::atomic<bool> rtps_running{false}, rtps_has_peers{false}; static std::atomic<uint32_t> 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<int>((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<std::recursive_mutex> lock(lvgl_mutex); if (status_label) { lv_label_set_text(status_label, status.c_str()); } } std::unique_lock<std::mutex> 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<std::recursive_mutex> 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<std::recursive_mutex> lock(lvgl_mutex); lv_task_handler(); } std::unique_lock<std::mutex> lock(m); cv.wait_for(lock, 16ms); return false; }, .task_config = {.name = "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<uint32_t>(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<bool> 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<std::recursive_mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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();
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.
Public Types
-
enum class DisplayController
Enum for the display controller type (selected via Kconfig)
Values:
-
enumerator UNKNOWN
-
enumerator EK79007
-
enumerator ILI9881C
-
enumerator UNKNOWN
-
using button_callback_t = espp::Interrupt::event_callback_fn
Alias for the button callback function.
-
using Pixel = lv_color16_t
Alias for the pixel type used by the display.
-
using DisplayDriver = espp::display_drivers::Controller
Alias for the low-level display driver interface.
-
using TouchpadData = espp::TouchpadData
Alias for the touchpad data.
-
using touch_callback_t = std::function<void(const TouchpadData&)>
Alias for the touch callback when touch events are received.
-
using ethernet_link_callback_t = std::function<void(esp_ip4_addr_t ip)>
Callback invoked when the Ethernet link goes up (with the assigned IP)
Public Functions
-
inline I2c &internal_i2c()
Get a reference to the internal I2C bus
Note
Shared by the GT911 touch, ES8311 codec, and camera SCCB
- Returns:
A reference to the internal I2C bus
-
inline espp::Interrupt &interrupts()
Get a reference to the interrupts
- Returns:
A reference to the interrupts
-
inline DisplayController get_display_controller() const
Get the display controller type for the configured panel
- Returns:
The display controller type
-
inline const char *get_display_controller_name() const
Get a string name for the configured display controller
- Returns:
String name of the controller
-
bool initialize_lcd()
Initialize the LCD (MIPI-DSI + configured panel driver)
- Returns:
true if the LCD was successfully initialized, false otherwise
-
bool initialize_display(size_t pixel_buffer_size = 0)
Initialize the LVGL display
- Parameters:
pixel_buffer_size – The size of the pixel buffer, in pixels. If 0, a default based on the detected panel width is used.
- Returns:
true if the display was successfully initialized, false otherwise
-
bool initialize_touch(const touch_callback_t &callback = nullptr, gpio_num_t interrupt_pin = touch_interrupt_default)
Initialize the GT911 multi-touch controller
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.
- Parameters:
callback – The touchpad callback
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.
- Returns:
true if the touchpad was successfully initialized, false otherwise
-
inline size_t bytes_per_pixel() const
Get the number of bytes per pixel for the display
- Returns:
The number of bytes per pixel
-
inline std::shared_ptr<TouchpadInput> touchpad_input() const
Get the touchpad input
- Returns:
A shared pointer to the touchpad input
-
inline TouchpadData touchpad_data() const
Get the most recent touchpad data
- Returns:
The touchpad data
-
void touchpad_read(uint8_t *num_touch_points, uint16_t *x, uint16_t *y, uint8_t *btn_state)
Get the touchpad data for LVGL integration
- Parameters:
num_touch_points – The number of touch points
x – The x coordinate
y – The y coordinate
btn_state – The button state (0 = released, 1 = pressed)
-
TouchpadData touchpad_convert(const TouchpadData &data) const
Convert touchpad data from raw reading to display coordinates
- Parameters:
data – The touchpad data to convert
- Returns:
The converted touchpad data
-
void brightness(float brightness)
Set the display brightness
- Parameters:
brightness – The brightness as a percentage (0-100)
-
float brightness() const
Get the display brightness
- Returns:
The brightness as a percentage (0-100)
-
inline size_t display_width() const
Get the display width in pixels (of the detected/active panel)
Note
Valid after initialize_lcd() has detected the panel.
- Returns:
The display width in pixels
-
inline size_t display_height() const
Get the display height in pixels (of the detected/active panel)
Note
Valid after initialize_lcd() has detected the panel.
- Returns:
The display height in pixels
-
size_t rotated_display_width() const
Get the display width in pixels, according to the current orientation.
-
size_t rotated_display_height() const
Get the display height in pixels, according to the current orientation.
-
inline const std::shared_ptr<DisplayDriver> &display_driver() const
Get a shared pointer to the low-level display driver
- Returns:
A shared pointer to the display driver
-
void write_lcd_lines(int xs, int ys, int xe, int ye, const uint8_t *data, uint32_t user_data)
Write lines to the LCD
Note
This method queues the panel transfer asynchronously.
- 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})
Initialize the audio system (ES8311 codec)
- Parameters:
sample_rate – The audio sample rate in Hz (default 48kHz)
task_config – The task configuration for the audio task
- Returns:
true if the audio system was successfully initialized
-
void set_speaker_enabled(bool enable)
Enable or disable the speaker amplifier (NS4150B PA on GPIO53)
- Parameters:
enable – True to enable the amplifier, false to disable
-
void volume(float volume)
Set the audio volume
- Parameters:
volume – The volume as a percentage (0-100)
-
float volume() const
Get the audio volume
- Returns:
The volume as a percentage (0-100)
-
void mute(bool mute)
Mute or unmute the audio
- Parameters:
mute – True to mute, false to unmute
-
bool is_muted() const
Check if audio is muted
- Returns:
True if muted, false otherwise
-
uint32_t audio_sample_rate() const
Get the audio sample rate
- Returns:
The audio sample rate, in Hz
-
void audio_sample_rate(uint32_t sample_rate)
Set the audio sample rate
- Parameters:
sample_rate – The audio sample rate, in Hz
-
size_t audio_buffer_size() const
Get the audio buffer size, in bytes
- Returns:
The audio buffer size, in bytes
-
void play_audio(const uint8_t *data, uint32_t num_bytes)
Play audio data
- Parameters:
data – The audio data to play
num_bytes – The number of bytes to play
-
void play_audio(std::span<const uint8_t> data)
Play audio data
- Parameters:
data – The audio data to play
-
bool initialize_sdcard(const SdCardConfig &config)
Initialize microSD / uSD card
- Parameters:
config – Configuration for the uSD card
- Returns:
True if uSD card was successfully initialized
-
inline bool is_sd_card_available() const
Check if SD card is present and mounted
- Returns:
True if SD card is available
-
inline sdmmc_card_t *sdcard() const
Get the uSD card handle
- Returns:
A pointer to the uSD card, or nullptr if not initialized
-
bool get_sd_card_info(uint32_t *size_mb, uint32_t *free_mb) const
Get SD card info
- Parameters:
size_mb – Pointer to store size in MB
free_mb – Pointer to store free space in MB
- Returns:
True if info retrieved successfully
-
bool initialize_ethernet(const ethernet_link_callback_t &on_link_up = nullptr)
Initialize the Ethernet interface (EMAC + IP101 RMII PHY, DHCP client)
Note
Requires the ESP-IDF default event loop. The BSP creates it if needed.
- Parameters:
on_link_up – Optional callback invoked when an IP address is acquired
- Returns:
True if Ethernet was successfully initialized and started
-
inline bool is_ethernet_connected() const
Check whether the Ethernet link is up (cable connected + negotiated)
- Returns:
True if the link is up
-
inline esp_ip4_addr_t ethernet_ip() const
Get the most recently acquired IPv4 address (0 if none)
- Returns:
The IPv4 address
-
bool initialize_camera()
Initialize the MIPI-CSI camera (SC2336/OV5647).
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.
- Returns:
True if successful
-
bool initialize_button(const button_callback_t &callback = nullptr)
Initialize the BOOT button
- Parameters:
callback – The callback function to call when pressed/released
- Returns:
True if the button was successfully initialized
-
bool button_state() const
Get the button state
- Returns:
True if pressed, false otherwise
-
inline const std::string &get_name() const
Get the name of the component
Note
This is the tag of the logger
- Returns:
A const reference to the name of the component
-
inline void set_log_tag(const std::string_view &tag)
Set the tag for the logger
- Parameters:
tag – The tag to use for the logger
-
inline espp::Logger::Verbosity get_log_level() const
Get the log level for the logger
See also
See also
- Returns:
The verbosity level of the logger
-
inline void set_log_level(espp::Logger::Verbosity level)
Set the log level for the logger
See also
See also
- Parameters:
level – The verbosity level to use for the logger
-
inline void set_log_verbosity(espp::Logger::Verbosity level)
Set the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls set_log_level
- Parameters:
level – The verbosity level to use for the logger
-
inline espp::Logger::Verbosity get_log_verbosity() const
Get the log verbosity for the logger
See also
See also
See also
Note
This is a convenience method that calls get_log_level
- Returns:
The verbosity level of the logger
-
inline void set_log_rate_limit(std::chrono::duration<float> rate_limit)
Set the rate limit for the logger
See also
Note
Only calls to the logger that have _rate_limit suffix will be rate limited
- Parameters:
rate_limit – The rate limit to use for the logger
Public Static Functions
-
static inline Esp32P4FunctionEvBoard &get()
Access the singleton instance.
- Returns:
Reference to the singleton instance
Public Static Attributes
-
static constexpr char mount_point[] = "/sdcard"
Mount point for the uSD card.
-
static constexpr gpio_num_t touch_interrupt_default = GPIO_NUM_NC
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).
-
struct SdCardConfig
Configuration for the uSD card.