嫌文字看着累的话,直接搜账号名嵌入式crafter(小破站 / 某音 / 某红书同号),视频里把讲解的内容和实战操作揉在一起了,比对着文档啃效率高多了。
心跳函数
如果使用STM32Cubemx配置的FreeRTOS,需要先打开USE_TICK_HOOK。
添加图片注释,不超过 140 字(可选)
如果是自己移植的需要在FreeRTOSConfig中打开这个宏定义,vApplicationTickHook这个函数才会有效。
添加图片注释,不超过 140 字(可选)
LVGL 内部有很多定时器和动画需要依赖“时间”的概念。它自己不产生时间,而是需要用户每过 1 毫秒调用 lv_tick_inc(1) 通知它。vApplicationTickHook() 是 FreeRTOS 的 Tick Hook 回调函数,每 1ms 系统时钟节拍(SysTick)就会触发一次。
把 lv_tick_inc(1) 放在这里,就是利用了这个“时钟中断”,实现毫秒级 LVGL 计时。⚠️ 如果你不调用 lv_tick_inc(1),LVGL 的动画、定时器、长按等功能会出问题(比如 UI 没响应、定时器不执行)。
void vApplicationTickHook()
{
//告诉lvgl已经过去了1毫秒
lv_tick_inc(1);
}
创建LVGL任务
lv_timer_handler() 是 LVGL 的核心处理函数,相当于主循环(loop),你必须周期性调用它,才能让 UI 动起来。推荐的调用周期是每 5~10ms 一次,所以这里配合了 osDelay(5) 来做周期调度。
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 */
}
界面的切换
LVGL界面的切换和显示API的调用,最好都是放在同一个任务中。因为LVGL的API并不是线程安全的,如果你在多个任务同时去调用LVGL的API,会产生不可预料的问题。我这边是导致界面经常会卡死。
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; // 用于记录“上一个界面”
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 刷新处理
osDelay(5); // 建议 5~10ms,刷新帧率约 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(); // 当前界面
if (cur == ui_ScreenMenu)
{
// 返回之前的界面(如果存在)
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
{
// 记录当前界面为“上一个界面”
last_screen = cur;
// 进入菜单界面
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 */
}
结尾
最后我也是准备了两份LVGL的代码,一份是标准库手工移植FreeRTOS-LVGL的例程,一份是HAL库STM32Cubemx生成的FreeRTOS的LVGL例程。有需要的朋友可以点赞关注下,私信我发给你。