Common Issues with STM32-FreeRTOS Adaptation for LVGL

Introduction

If you find reading text tiring, you can directly search for the account name “Embedded Crafter” (same on Bilibili / Douyin / Xiaohongshu), where the video combines explanations with practical operations, making it much more efficient than just reading the documentation.

Heartbeat Function

If you are using FreeRTOS configured with STM32Cubemx, you need to enable USE_TICK_HOOK first.

Common Issues with STM32-FreeRTOS Adaptation for LVGL

If you are porting it yourself, you need to enable this macro definition in FreeRTOSConfig for the vApplicationTickHook function to be effective.

Common Issues with STM32-FreeRTOS Adaptation for LVGL

LVGL has many internal timers and animations that rely on the concept of “time”. It does not generate time by itself; instead, the user needs to call lv_tick_inc(1) every 1 millisecond to notify it. vApplicationTickHook() is the FreeRTOS Tick Hook callback function that is triggered every 1ms by the system clock tick (SysTick).

Placing lv_tick_inc(1) here utilizes this “clock interrupt” to achieve millisecond-level timing for LVGL. ⚠️ If you do not call lv_tick_inc(1), LVGL’s animations, timers, long press, and other functions will malfunction (e.g., UI becomes unresponsive, timers do not execute).

void vApplicationTickHook(){    // Notify LVGL that 1 millisecond has passed    lv_tick_inc(1);}

Creating LVGL Task

lv_timer_handler() is the core processing function of LVGL, equivalent to the main loop, and you must call it periodically to keep the UI active. The recommended calling period is every 5 to 10 ms, so here we use osDelay(5) for periodic scheduling.

osThreadId_t lvglTaskHandle;const osThreadAttr_t lvglTask_attributes = {  .name = "defaultTask",  .priority = (osPriority_t) osPriorityHigh,  .stack_size = 128 * 8};lvglTaskHandle = osThreadNew(lvgl_Task, NULL, &defaultTask_attributes);void lvgl_Task(void *argument){  /* USER CODE BEGIN defaultTask */  /* Infinite loop */  for(;;)  {     lv_timer_handler();       osDelay(5);  }  /* USER CODE END defaultTask */}

Sometimes it may get stuck in HardFault_Handler(), and you need to check if the stack space for the LVGL task is too small; slightly increase the stack_size.

void HardFault_Handler(void){  /* USER CODE BEGIN HardFault_IRQn 0 */  /* USER CODE END HardFault_IRQn 0 */  while (1)  {    /* USER CODE BEGIN W1_HardFault_IRQn 0 */    /* USER CODE END W1_HardFault_IRQn 0 */  }}

Switching Interfaces

It is best to place the switching and displaying of LVGL interfaces in the same task. This is because LVGL’s API is not thread-safe; calling LVGL’s API from multiple tasks simultaneously can lead to unpredictable issues. This has caused the interface to frequently freeze on my end.

void Lvgl_Task(void *argument){    /* USER CODE BEGIN LvglTask */    const char* week_str[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};    const char* month_str[] = {        "JAN", "FEB", "MAR", "APR", "MAY", "JUN",        "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"    };    static lv_obj_t *last_screen = NULL;  // Used to record the "previous screen"                static uint16_t count = 0;    char buf[128];    ui_init();    user_event_bind();    //printf("UI Init OK\r\n");    for (;;)    {        count++;        lv_timer_handler();  // LVGL refresh processing        osDelay(5);          // Recommended 5~10ms, refresh frame rate about 100~200FPS        if(count % 40 == 0)        {            lv_obj_t *cur = lv_scr_act();            int16_t angle_minutes = (int16_t)((GetTime.Minutes * 1.0f / 60.0f) * 3600.0f);            int16_t angle_hours = (int16_t)((GetTime.Hours * 1.0f / 12.0f) * 3600.0f) + (int16_t)((GetTime.Minutes * 1.0f / 60.0f) * 300.0f);            if (cur == ui_Screen1)            {                //printf(">> Update Screen1\r\n");                lv_img_set_angle(ui_Screen1Hours, angle_hours);                lv_img_set_angle(ui_Screen1Minute, angle_minutes);                lv_img_set_angle(ui_Screen1Second, GetTime.Seconds * 60);                lv_label_set_text(ui_Screen1Week, week_str[GetData.WeekDay]);                if (GetData.Month >= 1 && GetData.Month <= 12)                    snprintf(buf, sizeof(buf), "%02d %s", GetData.Date, month_str[GetData.Month - 1]);                lv_label_set_text(ui_Screen1Month, buf);                snprintf(buf, sizeof(buf), "20%02d", GetData.Year);                lv_label_set_text(ui_Screen1Year, buf);                snprintf(buf, sizeof(buf), "%d", gSportStep);                lv_label_set_text(ui_Screen1Step, buf);            }            if (cur == ui_Screen2)            {                //printf(">> Update Screen2\r\n");                snprintf(buf, sizeof(buf), "%d", GetTime.Hours);                lv_label_set_text(ui_Screen2Hours, buf);                snprintf(buf, sizeof(buf), "%d", GetTime.Minutes);                lv_label_set_text(ui_Screen2Minute, buf);                lv_label_set_text(ui_Screen2Week, week_str[GetData.WeekDay]);                snprintf(buf, sizeof(buf), "%02d %s", GetData.Date, month_str[GetData.Month - 1]);                lv_label_set_text(ui_Screen2Month, buf);                snprintf(buf, sizeof(buf), "20%02d", GetData.Year);                lv_label_set_text(ui_Screen2Year, buf);                snprintf(buf, sizeof(buf), "%d", gSportStep);                lv_label_set_text(ui_Screen2Step, buf);            }                if (cur == ui_Screen3)            {                //printf(">> Update Screen3\r\n");                snprintf(buf, sizeof(buf), "%d", n_sp02);                lv_label_set_text(ui_blood_oxygen, buf);                snprintf(buf, sizeof(buf), "%d", n_heart_rate/4);                lv_label_set_text(ui_heartbeat, buf);            }                            if (cur == ui_Screen4)            {                //printf(">> Update Screen4\r\n");                snprintf(buf, sizeof(buf), "%d", n_heart_rate/4);                lv_label_set_text(ui_Screen4Heartbeat, buf);            }                if (cur == ui_ScreenMenu)            {                int light_level = lv_slider_get_value(ui_LightSlider);                Update_Backlight(light_level);            }        if (cur == ui_ScreenAlarm)             {                snprintf(buf, sizeof(buf), "%d", GetTime.Hours);                lv_label_set_text(ui_ScreenAlarmHour, buf);                snprintf(buf, sizeof(buf), "%d", GetTime.Minutes);                lv_label_set_text(ui_ScreenAlarmMin, buf);                int rollerHour_val = lv_roller_get_selected(ui_RollerHour);                int rollerMin_val = lv_roller_get_selected(ui_RollerMin);                bool switch_state = lv_obj_has_state(ui_Switch1, LV_STATE_CHECKED);                //printf("Alarm: %d:%d\r\n", rollerHour_val, rollerMin_val);                //printf("Current Switch State: %s\n", switch_state ? "ON" : "OFF");                Update_Alarm(rollerHour_val, rollerMin_val, switch_state);                            }        }        if(count % 200 == 0)        {            //printf("LVGL RUN FOR 1s\r\n");            if (osSemaphoreAcquire(menuPageChangeSem, 0) == osOK)            {                lv_obj_t *cur = lv_scr_act();  // Current screen                if (cur == ui_ScreenMenu)                {                    // Return to the previous screen (if it exists)                    if (last_screen != NULL && lv_obj_is_valid(last_screen))                    {                        lv_scr_load_anim(last_screen, LV_SCR_LOAD_ANIM_NONE, 300, 0, false);                        //printf("change to lastScreen\r\n");                    }                }                else                {                    // Record the current screen as the "previous screen"                    last_screen = cur;                    // Enter the menu screen                    if (lv_obj_is_valid(ui_ScreenMenu))                    {                        lv_scr_load_anim(ui_ScreenMenu, LV_SCR_LOAD_ANIM_NONE, 300, 0, false);                        //printf("change to ScreenMenu\r\n");                    }                }            }                }        if(count >= 1000)            count = 0;    }    /* USER CODE END LvglTask */}

Conclusion

Finally, I have prepared two sets of LVGL code: one is a standard library manually ported FreeRTOS-LVGL example, and the other is a FreeRTOS LVGL example generated by the HAL library STM32Cubemx. Friends in need can like and follow, and message me for a copy.

Leave a Comment