Can't get 60 FPS using ESP32-P4 5" CrowPanel
The video was recorded from an iPhone at 240 FPS, to show the 60 FPS results of the app.
Every frame the "Hello: <N>" message shows how many times the widget-update callback has been called before 1 second has elapsed. This callback is not called 60 times per second, but instead slightly over 50 FPS.
Goal is a solid 60 FPS with no fluctuation. This is a simple real-time system, and seems like there should be no hidden tasks perturbing frame rate.
I'm only updating a tiny portion of the frame buffer, and LVGL's .full_refresh is set to false (see source code). DMA and double buffer are active.
Different Refresh Rates (see details in video of refresh rates):
- Parallel RGB LCD interface supports up to 40 MHz, but only setting to 25000000, with what seems like appropriate porch values
- LVGL Task (OS task) runs at 16 ms
- LVGL Display Callback (that updates the hello label) also runs at 16 ms.
No obvious bottle necks:
- Everything must be done within the LCD refresh rate of 16.7 msecs.
- LVGL task and display rates are both set to 16 msecs, so their processing is slightly faster to make sure always ready for the LCD.
- CPU jumps around between 3% and 50%, so it's mostly idle.
- LVGL task shows most of the time is in the transfer to the frame buffer and not processing, ranging from 3 to 7 msecs. So much faster than the 16 ms limit.
The system is mostly idle, with no other tasks running to perturb timing, yet the FPS is fluctuating as if something is getting in the way.
The device I'm testing on is the CrowPanel (CrowPanel Advanced 5inch |ESP32-P4 HMI AI Display 800x480 IPS Touch Screen):
It's supposedly capable of buttery smooth 60 FPS if the app is properly written.
I'm using ESP-IDF 6.0.2
Dependencies from main/idf_component.yml:
dependencies:
espressif/esp_lvgl_port: "^2.8.0"
lvgl/lvgl: "^9.5.0"
Does anyone see anything I'm doing wrong that stopping this app from getting a solid 60 FPS with no fluctuation?
Here's the source code (4 files):
sdkconfig.defaults
# Needed is using v1.x ESP32-P4 chip version
CONFIG_ESP32P4_SELECTS_REV_LESS_V3=y
CONFIG_IDF_EXPERIMENTAL_FEATURES=y
CONFIG_ESPTOOLPY_FLASHMODE_QIO=y
CONFIG_ESPTOOLPY_FLASH_SAMPLE_MODE_STR=y
CONFIG_ESPTOOLPY_FLASHFREQ_80M=y
CONFIG_ESPTOOLPY_FLASHSIZE_16MB=y
CONFIG_SPIRAM=y
CONFIG_SPIRAM_MODE_HEX=y
CONFIG_SPIRAM_SPEED_200M=y
# Extra Fonts
CONFIG_LV_FONT_MONTSERRAT_24=y
# LVGL Performance monitoring (will display results on screen, will show render and flush times)
CONFIG_LV_USE_SYSMON=y
CONFIG_LV_USE_PERF_MONITOR=y
CONFIG_LV_PERF_MONITOR_ALIGN_BOTTOM_RIGHT=y
CONFIG_LV_USE_PPA=y
CONFIG_LV_DRAW_BUF_ALIGN=64
lcd_panel_rgb_16bit.h
#pragma once
#include "esp_err.h"
esp_err_t lcd_display_init();
uint32_t lcd_pixel_update_freq();
float lcd_pixel_update_fps();
uint32_t lvgl_refresh_period_ms();
lcd_panel_rgb_16bit.c
#include "lcd_panel_rgb_16bit.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_log.h"
#include "esp_lcd_panel_rgb.h"
#include "esp_lvgl_port.h"
#include "math.h"
#define LCD_PANEL_TAG "LCD_PANEL"
#define LCD_PANEL_INFO(fmt, ...) ESP_LOGI(LCD_PANEL_TAG, fmt, ##__VA_ARGS__)
#define LCD_PANEL_DEBUG(fmt, ...) ESP_LOGD(LCD_PANEL_TAG, fmt, ##__VA_ARGS__)
#define LCD_PANEL_ERROR(fmt, ...) ESP_LOGE(LCD_PANEL_TAG, fmt, ##__VA_ARGS__)
// Screen info
#define RGB_LCD_H_RES 800
#define RGB_LCD_V_RES 480
#define BITS_PER_PIXEL 16
// Control pins
#define RGB_PIN_NUM_DISP_EN -1
#define RGB_PIN_NUM_HSYNC 40
#define RGB_PIN_NUM_VSYNC 41
#define RGB_PIN_NUM_DE 2
#define RGB_PIN_NUM_PCLK 3
// 16 data pins
#define RGB_PIN_NUM_DATA0 8
#define RGB_PIN_NUM_DATA1 7
#define RGB_PIN_NUM_DATA2 6
#define RGB_PIN_NUM_DATA3 5
#define RGB_PIN_NUM_DATA4 4
#define RGB_PIN_NUM_DATA5 14
#define RGB_PIN_NUM_DATA6 13
#define RGB_PIN_NUM_DATA7 12
#define RGB_PIN_NUM_DATA8 11
#define RGB_PIN_NUM_DATA9 10
#define RGB_PIN_NUM_DATA10 9
#define RGB_PIN_NUM_DATA11 19
#define RGB_PIN_NUM_DATA12 18
#define RGB_PIN_NUM_DATA13 17
#define RGB_PIN_NUM_DATA14 16
#define RGB_PIN_NUM_DATA15 15
// ----------------------------------------------------------------------------------
// LCD Panel refresh rate
// ----------------------------------------------------------------------------------
typedef struct {
uint32_t pclk_hz; // Pixel clock frequency
uint32_t hsync_pulse_width; // HSYNC pulse width (in PCLK cycles)
uint32_t hsync_back_porch; // Horizontal back porch (PCLK cycles between HSYNC and active data)
uint32_t hsync_front_porch; // Horizontal front porch (PCLK cycles between active data and next HSYNC)
uint32_t vsync_pulse_width; // VSYNC pulse width (in lines)
uint32_t vsync_back_porch; // Vertical back porch (blank lines between VSYNC and frame start)
uint32_t vsync_front_porch; // Vertical front porch (blank lines between frame end and next VSYNC)
} PanelFreqInfo;
/*+*/
// Goal is a solid 60 FPS with no fluctuation, when only updating a tiny portion of the frame buffer.
// Diffent Refresh Rates:
// - Parallel RGB LCD interface supports up to 40 MHz, but exceeding ~25000000 will start overclocking ESP32P4 internals
// Since it has no GRAM, it's needs at steady stream coming in from PSRAM if double buffering is active
// - LVGL Task (OS task), not sychronized to the LCD VSYNC
// - LVGL Display Callback. No synchronize to the LVGL Task, but LVGL task needs to be away to check if time for the callback. The callback synchronizes to the LCD VSYNC.
// No bottle necks
// - Everthing must be done within the LDC refresh rate of 16.7 msecs.
// - LVGL task and display rates are both set to 16 msecs.
// - CPU jumps around between 3% and 50%
// - LVGL task shows most of the time is in the transfer to the frame buffer and not processing, ranging from 3 to 7 msecs.
// Result
// - The system is mostly idle, with no other tasks running to purtub timing, yet the FPS is flutuating as if something is getting in the way.
// Partial frame_buffer data transfer is only about
// Doesn't make sense that a solid 60 FPS is not achieve since there should be no other tasks running
// FPS = pclk_hz / (RGB_LCD_H_RES + hsync_pulse_width + hsync_back_porch + hsync_front_porch) * (RGB_LCD_V_RES + vsync_pulse_width + vsync_back_porch + vsync_front_porch)
// pclk_hz HPW HBP HFP VPW VBP VFP
//static const PanelFreqInfo L_panel_freq_info = { 16600000, 4, 88, 164, 4, 32, 9}; // 29.9 FPS
//static const PanelFreqInfo L_panel_freq_info = { 18000000, 4, 8, 8, 4, 16, 16}; // 42.5 FPS (25.5 ms/frame)
//static const PanelFreqInfo L_panel_freq_info = { 25000000, 4, 40, 40, 4, 29, 13}; // 53.8 FPS (18.6 ms/frame)
//static const PanelFreqInfo L_panel_freq_info = { 25000000, 4, 12, 20, 4, 12, 14}; // 58.6 FPS (17.1 ms/frame)
static const PanelFreqInfo L_panel_freq_info = { 25000000, 4, 16, 20, 2, 6, 8}; // 60.0 FPS (16.7 ms/frame)
//static const PanelFreqInfo L_panel_freq_info = { 28000000, 4, 24, 32, 4, 8, 8}; // 65.1 FPS (15.4 ms/frame)
// NOTE: Anything greater overclocks the CrowPanel ESP32-P4 5" internals and is likely to cause screen glitching
uint32_t lcd_pixel_update_freq() {
return L_panel_freq_info.pclk_hz;
}
float lcd_pixel_update_fps() {
float h = RGB_LCD_H_RES + L_panel_freq_info.hsync_pulse_width + L_panel_freq_info.hsync_back_porch + L_panel_freq_info.hsync_front_porch;
float v = RGB_LCD_V_RES + L_panel_freq_info.vsync_pulse_width + L_panel_freq_info.vsync_back_porch + L_panel_freq_info.vsync_front_porch;
float fps = L_panel_freq_info.pclk_hz / h / v;
return fps;
}
static esp_err_t lcd_panel_init(esp_lcd_panel_handle_t *lcd_panel_handle_out) {
esp_err_t err = ESP_OK;
esp_lcd_rgb_panel_config_t panel_config = {
.data_width = BITS_PER_PIXEL,
.dma_burst_size = 64, // DMA burst size alignment for PSRAM buffer
.num_fbs = 2, // Number of frame buffers
.flags.fb_in_psram = true, // Allocate frame buffer in PSRAM
//.bounce_buffer_size_px = 20 * RGB_LCD_H_RES, // Set bounce buffer size if enabled
.clk_src = LCD_CLK_SRC_DEFAULT, // Clock source for RGB LCD peripheral
.timings = {
.h_res = RGB_LCD_H_RES,
.v_res = RGB_LCD_V_RES,
.pclk_hz = L_panel_freq_info.pclk_hz,
.hsync_pulse_width = L_panel_freq_info.hsync_pulse_width,
.hsync_back_porch = L_panel_freq_info.hsync_back_porch,
.hsync_front_porch = L_panel_freq_info.hsync_front_porch,
.vsync_pulse_width = L_panel_freq_info.vsync_pulse_width,
.vsync_back_porch = L_panel_freq_info.vsync_back_porch,
.vsync_front_porch = L_panel_freq_info.vsync_front_porch,
.flags = {
.hsync_idle_low = false, // HSYNC signal is low in IDLE state
.vsync_idle_low = false, // VSYNC signal is low in IDLE state
.de_idle_high = false, // DE signal is high in IDLE state
.pclk_active_neg = true, // RGB data is sampled on falling edge of PCLK
.pclk_idle_high = true, // PCLK remains high during idle periods
},
},
// Pins
.disp_gpio_num = RGB_PIN_NUM_DISP_EN, // Display enable control pin, -1 if unused
.pclk_gpio_num = RGB_PIN_NUM_PCLK,
.vsync_gpio_num = RGB_PIN_NUM_VSYNC,
.hsync_gpio_num = RGB_PIN_NUM_HSYNC,
.de_gpio_num = RGB_PIN_NUM_DE,
.data_gpio_nums = {
RGB_PIN_NUM_DATA0,
RGB_PIN_NUM_DATA1,
RGB_PIN_NUM_DATA2,
RGB_PIN_NUM_DATA3,
RGB_PIN_NUM_DATA4,
RGB_PIN_NUM_DATA5,
RGB_PIN_NUM_DATA6,
RGB_PIN_NUM_DATA7,
RGB_PIN_NUM_DATA8,
RGB_PIN_NUM_DATA9,
RGB_PIN_NUM_DATA10,
RGB_PIN_NUM_DATA11,
RGB_PIN_NUM_DATA12,
RGB_PIN_NUM_DATA13,
RGB_PIN_NUM_DATA14,
RGB_PIN_NUM_DATA15,
},
};
// Create the LCD panel instance
esp_lcd_panel_handle_t lcd_panel_handle;
err = esp_lcd_new_rgb_panel(&panel_config, &lcd_panel_handle);
if (err != ESP_OK) return err;
// Reset and initialize the display
ESP_ERROR_CHECK(esp_lcd_panel_reset(lcd_panel_handle));
ESP_ERROR_CHECK(esp_lcd_panel_init(lcd_panel_handle));
*lcd_panel_handle_out = lcd_panel_handle;
return err;
}
uint32_t lvgl_refresh_period_ms() {
// This is meant to be slightly faster than the LCD display rate, so the LCD always has something new to display.
uint32_t refresh_period = (uint32_t)(1000.0 / lcd_pixel_update_fps()); // Truncates, so is likely to be slighly faster than LCD display refresh rate
return refresh_period;
}
static esp_err_t lvgl_init(esp_lcd_panel_handle_t _lcd_panel_handle) {
esp_err_t err = ESP_OK;
// Create LVGL event handler task
uint32_t refresh_period_ms = lvgl_refresh_period_ms();
const lvgl_port_cfg_t lvgl_cfg = {
.task_priority = 4,
.task_stack = 8192*2,
.task_affinity = 1, // No CPU core affinity, or lock to core 1 (significantly improves CPU usage). Main task uses core 0.
.task_max_sleep_ms = 7, // This is the minimum time the LVGL task if forced to sleep, but does not cause the task to wake up early. Keep this less than timer_period_ms
.timer_period_ms = refresh_period_ms, // Tells FreeRTOS how often to call lv_timer_handler(). 16 ms is 62.5 FPS. This should take precedence over CONFIG_LV_DEF_REFR_PERIOD.
};
err = lvgl_port_init(&lvgl_cfg);
if (err != ESP_OK) {
LCD_PANEL_ERROR("LVGL port initialization failed"); // Log error if LVGL init fails
}
const lvgl_port_display_cfg_t disp_cfg = {
// .io_handle = mipi_dbi_io,
.panel_handle = _lcd_panel_handle,
.control_handle = _lcd_panel_handle,
.buffer_size = (RGB_LCD_H_RES * RGB_LCD_V_RES), // LVGL buffer size
.double_buffer = true, // True is double buffering, false is single buffer
.hres = RGB_LCD_H_RES, // Horizontal resolution
.vres = RGB_LCD_V_RES, // Vertical resolution
.monochrome = false, // Color display
.color_format = LV_COLOR_FORMAT_RGB565, // Color format for LVGL 9
.rotation = {
.swap_xy = false, // No XY swap
.mirror_x = false, // No X mirroring
.mirror_y = false, // No Y mirroring
},
.flags = {
.buff_dma = true, // Disable DMA buffer
.buff_spiram = true, // Allocate buffer in PSRAM
.sw_rotate = false, // Disable software rotation
.swap_bytes = false, // Swap byte order for RGB565
.full_refresh = false, // Disable full screen refresh
.direct_mode = true, // Enable direct rendering mode
},
};
// Sync with the VSYNC signal to avoid tearing. Also stop race conditions that will cause reboot.
const lvgl_port_display_rgb_cfg_t lvgl_rgb_cfg = {
.flags = {
.avoid_tearing = true,
},
};
// Inform LVGL of the display
lv_display_t *lvgl_display = lvgl_port_add_disp_rgb(&disp_cfg, &lvgl_rgb_cfg);
if (lvgl_display == NULL) {
err = ESP_FAIL;
LCD_PANEL_ERROR("LVGL rgb port add fail");
}
return err;
}
esp_err_t lcd_display_init() {
esp_err_t err = ESP_OK;
esp_lcd_panel_handle_t lcd_panel_handle = NULL;
err = lcd_panel_init(&lcd_panel_handle);
if (err != ESP_OK) {
LCD_PANEL_ERROR("lcd_panel_init() failed");
return err;
}
err = lvgl_init(lcd_panel_handle);
if (err != ESP_OK) {
LCD_PANEL_ERROR("lvgl_init() failed");
return err;
}
return err;
}
main.c
#include "lcd_panel_rgb_16bit.h"
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <lvgl.h>
#include <esp_lvgl_port.h>
#include <esp_private/esp_clk.h>
#include <esp_log.h>
#include <esp_timer.h>
#include <math.h>
#include <stdio.h>
#define ANIMATION_DURATION_SECS 1.0 // For the text label that moves around in a circle
static int32_t L_prev_progress_100X = 0;
static int32_t L_prev_frame = 1;
static int64_t L_anim_start_time_usecs = 0;
#define MAIN_TAG "MAIN"
#define MAIN_INFO(fmt, ...) ESP_LOGI(MAIN_TAG, fmt, ##__VA_ARGS__)
#define MAIN_DEBUG(fmt, ...) ESP_LOGD(MAIN_TAG, fmt, ##__VA_ARGS__)
#define MAIN_ERROR(fmt, ...) ESP_LOGE(MAIN_TAG, fmt, ##__VA_ARGS__)
static void titleLocationCallback(void *_widget, int32_t _progress_100X) {
float progress = _progress_100X / 100.0; // 0 .. 1
int32_t dx = 50 * cos(2.0 * M_PI * progress);
int32_t dy = 50 * sin(2.0 * M_PI * progress);
lv_obj_set_pos(_widget, dx, dy); // Relative to parent alignment which is currently centered
// Show frame number in text
if (_progress_100X < L_prev_progress_100X) {
L_prev_frame = 1;
} else {
L_prev_frame++;
}
L_prev_progress_100X = _progress_100X;
char text[20];
snprintf(text, 20, "Hello: %ld", L_prev_frame);
lv_label_set_text(_widget, text);
}
static void renderReadyCallback(lv_event_t *_event) {
// This runs on the LVGL task thread right after flushing finishes
lv_event_code_t code = lv_event_get_code(_event);
if (code == LV_EVENT_REFR_START) { // Sent before a refreshing cycle starts. Ideal for preparing/updating widget states right before rendering.
lv_obj_t *title_label = (lv_obj_t *)lv_event_get_user_data(_event);
// Get elapsed seconds
int64_t curr_time_usecs = esp_timer_get_time();
int64_t elapsed_usecs = curr_time_usecs - L_anim_start_time_usecs;
double remainder_secs = fmod(elapsed_usecs / 1000000.0, ANIMATION_DURATION_SECS);
double progress = remainder_secs / ANIMATION_DURATION_SECS;
int32_t progress_100X = (int32_t)(progress * 100.0);
// Update title
titleLocationCallback(title_label, progress_100X);
}
// LV_EVENT_REFR_READY: Sent after the refresh cycle completes (after rendering + flush callback). Could be used to verify render and idle time.
}
static void createUI(void) {
// IMPORTANT: If the lock stays active too long, the screen will shift
char mcu_freq_text[100];
snprintf(mcu_freq_text, 100, "MCU: %0.2f MHz", esp_clk_cpu_freq()/1000000.0);
char lcd_display_info_text[100];
snprintf(lcd_display_info_text, 100, "LCD: %0.2f MHz, %0.2f ms, %0.2f FPS", lcd_pixel_update_freq()/1000000.0, 1000.0/lcd_pixel_update_fps(), lcd_pixel_update_fps());
char lvgl_task_info_text[100];
uint32_t task_refresh_period_ms = lvgl_refresh_period_ms();
snprintf(lvgl_task_info_text, 100, "LVGL Task: %lu ms, %0.2f FPS", task_refresh_period_ms, 1000.0/task_refresh_period_ms);
uint32_t display_refresh_period_ms = task_refresh_period_ms;
char lvgl_display_info_text[100];
snprintf(lvgl_display_info_text, 100, "LVGL Display: %lu ms, %0.2f FPS", display_refresh_period_ms, 1000.0/display_refresh_period_ms);
// Lock the LVGL mutex before creating
lvgl_port_lock(portMAX_DELAY); // Wait max amount of time
// Create main screen
lv_obj_t *scr = lv_scr_act();
lv_obj_set_style_bg_color(scr, lv_color_hex(0xFFFFFF), LV_PART_MAIN); // Set white background
// Create title label
lv_obj_t *title_label = lv_label_create(scr);
lv_label_set_text(title_label, "Hello");
lv_obj_align(title_label, LV_ALIGN_CENTER, 0, 50);
lv_obj_set_style_text_font(title_label, &lv_font_montserrat_24, 0);
// Animate the title
{
// Let the LVGL task catch up
lvgl_port_unlock();
vTaskDelay(pdMS_TO_TICKS(300));
// IMPORTANT: This synchronizes with the LVGL display callback rate. Using an lv_anim would use a different timer and diverge from the callback frame rate.
L_anim_start_time_usecs = esp_timer_get_time();
void *user_data = title_label;
(void)lv_display_add_event_cb(lv_display_get_default(), renderReadyCallback, LV_EVENT_REFR_START, user_data);
lvgl_port_lock(portMAX_DELAY); // Wait max amount of time
}
// Let the LVGL task catch up
lvgl_port_unlock();
vTaskDelay(pdMS_TO_TICKS(300));
lvgl_port_lock(portMAX_DELAY); // Wait max amount of time
// Display some helpful stats
{
lv_obj_t *mcu_freq_label = lv_label_create(scr);
lv_label_set_text(mcu_freq_label, mcu_freq_text);
lv_obj_align(mcu_freq_label, LV_ALIGN_BOTTOM_MID, 0, -100);
lv_obj_set_style_text_font(mcu_freq_label, &lv_font_montserrat_24, 0);
}
{
lv_obj_t *lcd_display_info_label = lv_label_create(scr);
lv_label_set_text(lcd_display_info_label, lcd_display_info_text);
lv_obj_align(lcd_display_info_label, LV_ALIGN_BOTTOM_MID, 0, -70);
lv_obj_set_style_text_font(lcd_display_info_label, &lv_font_montserrat_24, 0);
}
{
lv_obj_t *lvgl_info_label = lv_label_create(scr);
lv_label_set_text(lvgl_info_label, lvgl_task_info_text);
lv_obj_align(lvgl_info_label, LV_ALIGN_BOTTOM_MID, 0, -40);
lv_obj_set_style_text_font(lvgl_info_label, &lv_font_montserrat_24, 0);
}
{
lv_obj_t *lvgl_info_label = lv_label_create(scr);
lv_label_set_text(lvgl_info_label, lvgl_display_info_text);
lv_obj_align(lvgl_info_label, LV_ALIGN_BOTTOM_MID, 0, -10);
lv_obj_set_style_text_font(lvgl_info_label, &lv_font_montserrat_24, 0);
}
// Let the LVGL task catch up
lvgl_port_unlock();
vTaskDelay(pdMS_TO_TICKS(100));
lvgl_port_lock(portMAX_DELAY); // Wait max amount of time
// IMPORTANT: This does not wake up the LVGL task; the wakeup time is defined by lvgl_port_init().
// When the task does wakeup, it will check to see if this timer is ready for the callback.
// The LVGL display_refresh_period_ms should <= the LVGL task_refresh_period_ms refresh time to make sure the display callback is always ready for an update
lv_timer_set_period(lv_display_get_refr_timer(lv_display_get_default()), display_refresh_period_ms);
lvgl_port_unlock();
}
void app_main(void) {
// Init
MAIN_INFO("Starting application...");
// Initialize LCD hardware and LVGL
esp_err_t err = lcd_display_init();
if (err != ESP_OK) {
while (1) {
MAIN_ERROR("[%s] init failed: %s", "Hello FPS App", esp_err_to_name(err));
vTaskDelay(pdMS_TO_TICKS(1000));
}
}
MAIN_INFO("LCD display initialized");
// Create UI
createUI();
MAIN_INFO("UI created");
// Allow LVGL background event handler to process events
MAIN_INFO("Main task going to sleep");
while (1) {
// Sleep and do nothing
vTaskDelay(pdMS_TO_TICKS(10000));
}
}