I'm encountering an issue with my ESP32 project using LVGL for UI rendering. After running for about 30 minutes, the entire UI freezes. After some debugging, it seems that the freeze is due to my lv_tick_task not running anymore—it appears to be stuck after calling hte lv_task_handler and also it had the mutex now and not releasing it.
Here’s a summary of my setup:
LVGL Task (lv_tick_task): This FreeRTOS task is created with xTaskCreate() and periodically calls lv_task_handler() to refresh the UI. It acquires a mutex (implemented with a FreeRTOS semaphore) before calling the handler and releases it immediately after.
Other UI Update Tasks:
periodic_ui_update_task: Runs every 1 second to update various parts of the UI. CAN Task: Processes incoming CAN messages and updates the UI accordingly. Timers: Some UI updates are also scheduled via ESP timers.
The critical part of the code looks like this:
static SemaphoreHandle_t lvgl_mutex_handle;
void lvgl_lock_init(void) {
lvgl_mutex_handle = xSemaphoreCreateMutex();
}
bool lvgl_lock(uint32_t ticks_to_wait) {
return (xSemaphoreTake(lvgl_mutex_handle, (TickType_t)ticks_to_wait) == pdTRUE);
}
void lvgl_unlock(void) {
xSemaphoreGive(lvgl_mutex_handle);
}
_Noreturn void lv_tick_task(void *arg) {
static const uint16_t lvgl_max_delay_ms = 50;
static const uint16_t lvgl_min_delay_ms = 10;
uint32_t next_delay_ms = lvgl_min_delay_ms;
while(1)
{
PORT_TIMER_LOG(__func__, "Waiting for lock");
if (lvgl_lock(portMAX_DELAY)) {
PORT_TIMER_LOG(__func__, "Got lock");
next_delay_ms = lv_task_handler();
lvgl_unlock();
PORT_TIMER_LOG(__func__, "Released lock");
}
if (next_delay_ms > lvgl_max_delay_ms) {
next_delay_ms = lvgl_max_delay_ms;
} else if (next_delay_ms < lvgl_min_delay_ms) {
next_delay_ms = lvgl_min_delay_ms;
}
vTaskDelay(next_delay_ms / portTICK_PERIOD_MS);
}
}
esp_err_t lv_port_tick_init(void)
{
static const uint32_t tick_inc_period_ms = 5;
const esp_timer_create_args_t periodic_timer_args = {
.callback = lv_tick_inc_cb,
.arg = (void *) &tick_inc_period_ms,
.dispatch_method = ESP_TIMER_TASK,
.name = "",
.skip_unhandled_events = true,
};
esp_timer_handle_t periodic_timer;
ESP_ERROR_CHECK(esp_timer_create(&periodic_timer_args, &periodic_timer));
ESP_ERROR_CHECK(esp_timer_start_periodic(periodic_timer, tick_inc_period_ms * 1000));
return ESP_OK;
}
static void lv_tick_inc_cb(void *data)
{
uint32_t tick_inc_period_ms = *((uint32_t *) data);
lv_tick_inc(tick_inc_period_ms);
}
xTaskCreate(lv_tick_task, "lv_tick_task", 4096, NULL, LV_TASK_HANDLE_PRIORITY, NULL);
The Problem: After some time, the UI freezes and my debugging indicates that lv_tick_task is no longer executing because it’s stuck. It seems that another part of my code (possibly one of the UI update tasks or a timer callback) might be acquiring the same mutex and not releasing it properly, causing a deadlock.
My Questions:
Mutex Usage:
Could there be potential pitfalls with this approach of using the mutex, such as priority inversion or deadlock scenarios? Best Practices:
How should one architect the interaction between LVGL’s lv_task_handler and other UI update functions? Should UI updates (from CAN messages, periodic tasks, timers, etc.) be coordinated differently (e.g., using separate mutexes, event queues, or a dedicated UI thread) to avoid conflicts? Debugging Steps:
What are the recommended debugging steps or tools to pinpoint which part of the code is holding the mutex and not releasing it?
Any insights or recommendations would be greatly appreciated. I’m looking to understand if my architecture needs adjustments or if there’s a known issue with this kind of setup.