ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于STM32与LVGL的智能手表开发实战:从移植到UI设计完整指南

基于STM32与LVGL的智能手表开发实战:从移植到UI设计完整指南 先问一个问题你是不是也在网上收藏了不少 STM32 和 LVGL 的教程但真到自己想做一个“智能手表”项目时却发现资料零散有的讲移植、有的讲控件、有的讲 UI 设计很难串成一条完整链路这篇文章我会从零开始带你拆解一个基于 STM32 LVGL 的智能手表项目。内容覆盖硬件选型、开发环境搭建、LVGL 移植、表盘与菜单 UI 设计、RTC 时间显示、低功耗与内存优化、常见报错排查等。整个项目不一定用到很高端的芯片但会把嵌入式 GUI 开发的核心路径走通。如果你正准备做课设、毕设或者想在开发板上复刻一个“手腕上的小产品”这篇文章应该能帮你节省大量找资料的时间。1. 项目概览与整体架构一个智能手表项目看起来“小”实际上涉及到的技术点并不少。我们从整体上先梳理清楚再做分步实现。1.1 智能手表的功能定位市面上常见的智能手表核心功能无非是这几类时间显示数字表盘、指针表盘、日期星期。健康监测心率、计步、血氧依赖传感器硬件。消息提醒蓝牙连接手机后推送通知。交互操作触摸滑动、点击切换页面。系统设置亮度、音量、主题切换、关机重启。低功耗待机熄屏、按键点亮、定时唤醒。在 STM32 LVGL 方案中我们通常先做最核心的“时间显示 菜单切换 页面联动”再根据手里的硬件传感器逐步扩展健康监测等功能。下面给出一个典型的手表功能脑图式拆解智能手表STM32 LVGL ├── UI 显示层 │ ├── 表盘界面时钟、日期、电量 │ ├── 菜单界面应用图标网格 │ ├── 设置界面亮度、主题、关于 │ └── 二级页面心率、计步、消息等 ├── 驱动层 │ ├── LCD 显示屏驱动SPI/并口 │ ├── 触摸屏驱动I2C/SPI │ ├── RTC 时钟芯片/内部 RTC │ ├── 传感器驱动心率、加速度计等 │ └── 电源管理电池电量检测 └── 中间层 ├── LVGL 图形库 ├── FreeRTOS 实时操作系统可选 └── 业务逻辑任务时间刷新、传感器读取、事件处理1.2 为什么选择 STM32 LVGLLVGLLight and Versatile Graphics Library是目前嵌入式领域非常流行的开源图形库它专门为资源受限的 MCU 设计。相比直接操作 framebuffer 或自己写 UI 控件LVGL 提供了丰富的内置控件按钮、标签、列表、弧线、仪表盘、开关等。自带布局系统Flex 布局和 Grid 布局可以像写前端一样排列控件。完善的事件机制点击、长按、滑动、释放等。内存占用可控通过配置项可以裁剪功能。官方模拟器可以在 PC 上先调试 UI再移植到开发板。STM32 作为主控芯片生态成熟、资料多、价格选择范围广。无论你是用 STM32F103 这种“入门神芯”还是用 STM32F407、STM32H750 这类性能更强的芯片LVGL 都能找到合适的配置方案。2. 硬件选型与材料准备做智能手表项目硬件选型直接影响后续开发难度。我建议根据你自己的板子情况做选择下面列出常见方案。2.1 主控芯片选择芯片型号内核主频内存Flash适合场景STM32F103C8T6Cortex-M372MHz20KB64KB入门、简单表盘、低分辨率屏STM32F407VET6Cortex-M4F168MHz192KB512KB动画流畅、支持复杂控件STM32H750VBT6Cortex-M7480MHz512KB128KB可外扩高性能表盘、大量图片素材如果你手里的板子是 STM32F103C8T6常见的“蓝色药丸”开发板也能做但需要严格控制 LVGL 内存占用尽量使用 240x240 或 128x160 分辨率的屏幕。如果你有 STM32F407 或更高配的板子操作空间就大很多可以上 320x240 甚至更高的分辨率动画切换也更跟手。2.2 显示屏选择智能手表项目最关键的硬件外设就是屏幕。常见选择ST77351.8 寸、128x160SPI 接口价格便宜适合入门。ST77891.3 寸 / 1.54 寸 / 1.69 寸、240x240 或 240x280SPI 接口是目前 DIY 手表的热门选择。ILI93412.4 寸 / 2.8 寸、320x240SPI 或并口适合做开发板演示。驱动芯片不同初始化代码略有差异但 LVGL 上层不用改只需要适配底层flush函数。2.3 触摸屏选择如果你希望手表支持滑动、点击交互就需要触摸屏。常见两种电阻触摸XPT2046SPI 接口价格便宜但需要校准。电容触摸GT911、FT6236I2C 接口支持多点触摸体验更好适合手表。如果暂时没有触摸屏也可以先用按键模拟 LVGL 的点击和选择事件项目主体逻辑不受影响。2.4 RTC 与电源手表必须显示时间所以需要一个可靠的时间来源STM32 内部 RTC简单但掉电需要后备电池VBAT。外部 RTC 芯片如 DS3231、PCF8563走时精准带电池座。电源方面如果是开发板阶段直接用 USB 供电即可不需要做电池管理。如果要做得更接近产品可以加锂电池充放电电路和电量检测 ADC。3. 开发环境搭建3.1 STM32 开发环境目前常用的方案有两种Keil MDK老牌工具适合大多数 STM32 工程很多教程默认使用。STM32CubeIDEST 官方免费 IDE内置 CubeMX 配置适合新项目。如果你之前用的是 Keil可以继续用如果是从头开始我更推荐 STM32CubeIDE因为 CubeMX 图形化配置时钟、引脚、外设非常方便能减少低级错误。无论选择哪个 IDE都建议先安装 STM32CubeMX 来生成初始化代码再手动加入 LVGL 源码。3.2 LVGL 版本选择LVGL 目前有两个主流大版本LVGL v8.x目前资料最多、教程最多API 相对稳定。绝大多数网上教程都是基于 v8 写的遇到问题容易搜索到答案。LVGL v9.x较新版本API 有一些调整比如初始化方式、输入设备注册方式变化。本文示例以 LVGL v8.3 为主这也是很多开发板和开源项目默认使用的版本。如果你用的是 v9注意个别 API 名称和参数需要对照官方文档调整。3.3 LVGL 源码获取LVGL 官方源码托管在 GitHub仓库名是lvgl同时还有一个lv_drivers仓库存放显示器、触摸屏等驱动抽象层。如果你访问 GitHub 不方便也可以从国内镜像、Gitee 或开发板厂商提供的 SDK 中获取。建议直接下载官方 release 包例如v8.3.x的源码包。下载后LVGL 源码目录结构大致如下lvgl/ ├── examples/ ├── src/ │ ├── core/ │ ├── draw/ │ ├── extra/ │ ├── fonts/ │ ├── hal/ │ ├── misc/ │ └── widgets/ ├── demos/ ├── lv_conf_template.h ├── lvgl.h └── ...我们需要将src目录加入编译同时把lv_conf_template.h复制一份改名为lv_conf.h放在工程根目录下。4. LVGL 移植到 STM32LVGL 移植的核心工作可以概括为四件事把 LVGL 源码加入工程。配置lv_conf.h。实现显示刷新函数flush。实现触摸输入读取函数如果有触摸屏。下面逐步说明。4.1 工程目录结构这里给出一个示例目录结构方便你对照组织代码smart_watch/ ├── Core/ │ ├── Inc/ │ │ ├── main.h │ │ ├── lcd.h │ │ └── touch.h │ └── Src/ │ ├── main.c │ ├── lcd.c │ └── touch.c ├── Drivers/ │ ├── CMSIS/ │ └── STM32F1xx_HAL_Driver/ ├── LVGL/ │ ├── lv_conf.h │ ├── lvgl/ │ │ ├── src/ │ │ ├── examples/ │ │ └── ... │ └── lv_port_disp.c │ └── lv_port_indev.c ├── Middlewares/ └── smart_watch.ioc其中lv_port_disp.c和lv_port_indev.c是 LVGL 官方提供的移植示例文件位于lvgl/examples/porting/目录下你把它复制到自己的工程中修改即可。4.2 修改 lv_conf.hlv_conf.h是 LVGL 的全局配置文件。你需要把文件开头的#if 0改为#if 1来启用配置。关键配置项如下/* 颜色深度RGB565 占 2 字节RGB888 占 3 字节 */ #define LV_COLOR_DEPTH 16 /* 内存池大小视你的 MCU 而定 */ #define LV_MEM_SIZE (64U * 1024U) /* 开启 PNG、GIF 等图片解码按需 */ #define LV_USE_PNG 1 #define LV_USE_GIF 1 /* 开启字体如果需要中文需要额外添加中文字库 */ #define LV_FONT_MONTSERRAT_14 1 #define LV_FONT_MONTSERRAT_20 1 #define LV_FONT_MONTSERRAT_30 1 /* 开启常用控件 */ #define LV_USE_BTN 1 #define LV_USE_LABEL 1 #define LV_USE_ARC 1 #define LV_USE_LINE 1 #define LV_USE_LIST 1 #define LV_USE_SWITCH 1 /* 使用内存动态分配 */ #define LV_MEM_CUSTOM 1如果你的 MCU 内存比较小比如 STM32F103C8T6 只有 20KB RAMLV_MEM_SIZE建议设置为16 * 1024左右同时关闭不需要的控件和图片解码功能否则编译后运行会直接卡死或进入 HardFault。4.3 显示接口移植LVGL 的显示驱动核心是一个flush回调函数作用是把 LVGL 绘制好的图像数据写入到 LCD 屏幕。以常见的 ST7789 SPI 屏幕为例伪代码如下// 文件路径lv_port_disp.c 中修改 static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p) { uint32_t w (area-x2 - area-x1 1); uint32_t h (area-y2 - area-y1 1); lcd_set_window(area-x1, area-y1, area-x2, area-y2); lcd_write_data((uint8_t *)color_p, w * h * 2); lv_disp_flush_ready(disp_drv); }lcd_set_window和lcd_write_data需要根据你的屏幕驱动芯片自行实现。ST7789 的初始化代码网上非常多这里不展开但核心步骤是发送软件复位命令。设置屏幕方向、颜色格式。设置扫描方向和显存访问控制。退出睡眠模式。打开显示。重要LVGL 在lv_init()之后会调用lv_disp_drv_register注册显示驱动。如果你使用的是 RGB565 格式确保 LCD 初始化时也设置为 RGB565否则颜色会紊乱。4.4 触摸输入移植如果你的屏幕带有电容触摸屏需要实现indev_read回调// 文件路径lv_port_indev.c 中修改 static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data) { uint16_t x, y; uint8_t touched touch_read(x, y); if (touched) { >x touch_x * 240 / 480; y touch_y * 240 / 480;4.5 测试最小工程完成移植后先写一个最简单的测试代码不要急着做 UI。#include lvgl.h #include lcd.h #include touch.h void lvgl_test_init(void) { lv_init(); lv_port_disp_init(); lv_port_indev_init(); lv_obj_t * label lv_label_create(lv_scr_act()); lv_label_set_text(label, Welcome to Smart Watch); lv_obj_center(label); } void lvgl_test_loop(void) { lv_timer_handler(); delay_ms(5); }如果屏幕能正常显示文字说明 LVGL 已经成功跑在了你的 STM32 上。5. 手表 UI 设计智能手表 UI 是整个项目的门面。下面我以“表盘、菜单、设置”三个核心页面为例演示如何用 LVGL 搭建一个可用的手表界面。5.1 表盘页面设计表盘是手表默认显示的页面需要显示时间、日期、电量。LVGL 中最直接的方式是把屏幕当作一个容器在上面叠加lv_label和lv_arc。下面是一个简单的数字表盘示例// 文件路径ui_face.c #include ui_face.h static lv_obj_t * time_label; static lv_obj_t * date_label; static lv_obj_t * battery_arc; void ui_face_create(void) { lv_obj_t * scr lv_scr_act(); lv_obj_set_style_bg_color(scr, lv_color_hex(0x000000), 0); // 时间标签 time_label lv_label_create(scr); lv_label_set_text(time_label, 12:00); lv_obj_set_style_text_color(time_label, lv_color_hex(0xFFFFFF), 0); lv_obj_set_style_text_font(time_label, lv_font_montserrat_30, 0); lv_obj_align(time_label, LV_ALIGN_CENTER, 0, -20); // 日期标签 date_label lv_label_create(scr); lv_label_set_text(date_label, 2025-01-01 Mon); lv_obj_set_style_text_color(date_label, lv_color_hex(0x888888), 0); lv_obj_align(date_label, LV_ALIGN_CENTER, 0, 20); // 电池电量弧线 battery_arc lv_arc_create(scr); lv_obj_set_size(battery_arc, 60, 60); lv_arc_set_rotation(battery_arc, 135); lv_arc_set_bg_angles(battery_arc, 0, 270); lv_arc_set_value(battery_arc, 80); lv_obj_remove_style(battery_arc, NULL, LV_PART_KNOB); lv_obj_align(battery_arc, LV_ALIGN_BOTTOM_RIGHT, -10, -10); }这里有两个细节时间我们使用lv_font_montserrat_30这是 LVGL 自带的数字字体。电量弧线通过lv_arc实现并移除了中间可拖动的旋钮让它只起到进度指示作用。时间刷新逻辑可以放在一个定时器里void ui_face_update_time(uint8_t hour, uint8_t min, uint8_t sec) { char buf[16]; snprintf(buf, sizeof(buf), %02d:%02d:%02d, hour, min, sec); lv_label_set_text(time_label, buf); }然后在主循环或 RTOS 任务中每秒调用一次即可。5.2 菜单页面设计手表菜单通常是一组图标。如果你的屏幕是 240x240可以一行放 3 个图标显示 2 到 3 行。LVGL 中可以使用lv_btnmatrix快速生成网格按钮// 文件路径ui_menu.c #include ui_menu.h static const char * menu_map[] { Heart, Step, Msg, Music, Clock, Set, }; void ui_menu_create(lv_obj_t * parent) { lv_obj_t * btnm lv_btnmatrix_create(parent); lv_btnmatrix_set_map(btnm, menu_map); lv_obj_set_size(btnm, 220, 180); lv_obj_align(btnm, LV_ALIGN_CENTER, 0, 10); lv_obj_add_event_cb(btnm, menu_event_cb, LV_EVENT_VALUE_CHANGED, NULL); } static void menu_event_cb(lv_event_t * e) { lv_event_code_t code lv_event_get_code(e); lv_obj_t * obj lv_event_get_target(e); if (code LV_EVENT_VALUE_CHANGED) { uint32_t id lv_btnmatrix_get_selected_btn(obj); const char * txt lv_btnmatrix_get_btn_text(obj, id); // 根据 txt 跳转到不同页面例如切换到心率页 if (strcmp(txt, Heart) 0) { // 页面切换逻辑 } } }如果你的 UI 素材比较丰富想要每个菜单项带图标可以考虑使用lv_obj自定义“图标 文字”的组合lv_obj_t * app_item lv_obj_create(parent); lv_obj_set_size(app_item, 60, 60); lv_obj_set_layout(app_item, LV_LAYOUT_FLEX); lv_obj_set_flex_flow(app_item, LV_FLEX_FLOW_COLUMN); lv_obj_set_style_pad_row(app_item, 4, 0); lv_obj_t * icon lv_label_create(app_item); lv_label_set_text(icon, LV_SYMBOL_HEART); lv_obj_center(icon); lv_obj_t * name lv_label_create(app_item); lv_label_set_text(name, 心率);注意LV_SYMBOL_HEART这类内置符号是字体文件里的一串 Unicode 映射LVGL 基础字体自带了一部分符号可以在lv_symbol_def.h中查看。如果要显示中文“心率”则需要额外准备中文字库。5.3 页面切换与动画手表页面切换最好带一点过渡动画否则会显得生硬。LVGL v8 中可以使用lv_scr_load_anim实现void ui_switch_to_page(lv_obj_t * new_scr) { lv_scr_load_anim(new_scr, LV_SCR_LOAD_ANIM_MOVE_LEFT, 300, 0, false); }这里LV_SCR_LOAD_ANIM_MOVE_LEFT表示新页面从右侧滑入动画持续 300ms。这个功能在页面较多时非常有用推荐优先实现。5.4 中文字库处理LVGL 默认字体只支持 ASCII 字符不包含中文。如果你需要显示“设置”“心率”等中文有几种方案使用字体转换工具LVGL 官方提供了在线字体转换工具把需要的汉字和字体文件上传生成 C 数组或字体文件。使用 LVGL 内置中文字体LVGL 有lv_font_simsun_16_cjk之类的字库但体积较大只适合内存充足的芯片。只保留常用字在转换工具中只勾选你用到的汉字生成的字体文件会小很多。在线字体转换工具的使用流程大致是上传 TTF 或 WOFF 字体文件。设置字体大小和像素范围。输入需要包含的字符页面中出现的所有中文。选择输出格式为 C 数组。下载生成的文件替换 LVGL 工程中对应字体文件。注意中文字体随字号变大体积膨胀非常明显建议在嵌入式项目中严格控制使用中文字符的数量。一个 16 号全角中文汉字大约占 32 字节左右100 个汉字就约 3200 字节再加上字体表结构总体积并不小。6. 时间获取与电源管理6.1 使用 STM32 RTCSTM32 内部 RTC 模块可以在单片机运行时走时。CubeMX 中配置 RTC 非常方便启用 RTC。选择时钟源为 LSE 或 LSI。设置初始日期和时间。生成代码后RTC 的基本读写接口已就绪。你可以使用 HAL 库函数RTC_TimeTypeDef sTime; RTC_DateTypeDef sDate; HAL_RTC_GetTime(hrtc, sTime, RTC_FORMAT_BIN); HAL_RTC_GetDate(hrtc, sDate, RTC_FORMAT_BIN); printf(Time: %02d:%02d:%02d\n, sTime.Hours, sTime.Minutes, sTime.Seconds);注意在 HAL 库中必须先调用HAL_RTC_GetTime再调用HAL_RTC_GetDate否则日期数据不会更新。这是一个容易踩的坑。6.2 电池电量检测电量检测一般用 ADC 采集电池电压HAL_ADC_Start(hadc); HAL_ADC_PollForConversion(hadc, 100); uint16_t adc_val HAL_ADC_GetValue(hadc); float voltage adc_val * 3.3f / 4096.0f;然后根据电压估算电量百分比比如uint8_t battery_percent (uint8_t)((voltage - 3.0f) / (4.2f - 3.0f) * 100);这只是一个简单的线性估算实际产品中还要考虑电池放电曲线和温度补偿。做开发板演示时能实时反映电压变化即可。6.3 低功耗策略LVGL 本身不负责低功耗管理但我们可以配合 STM32 的低功耗模式来延长续航。常见做法屏幕自动熄屏无操作 10 秒后关闭 LCD 背光。进入 STOP 模式按键唤醒或使用 RTC 闹钟定时唤醒刷新表盘。降低刷新率LVGL 在lv_timer_handler()中会周期性处理任务如果界面没有变化可以降低调用频率。下面是一个简单的背光超时处理示例uint32_t last_active_time 0; void app_loop(void) { // 每次触摸或按键时更新 last_active_time if (is_touch_active()) { last_active_time HAL_GetTick(); lcd_backlight_on(); } if (HAL_GetTick() - last_active_time 10000) { lcd_backlight_off(); } lv_timer_handler(); delay_ms(5); }注意进入低功耗模式前必须确保 LVGL 已经完成了当前帧的绘制否则屏幕可能留下残影或脏数据。7. 内存优化与性能调优7.1 LVGL 内存配置STM32 的 RAM 资源有限LVGL 的动态内存池大小决定了能创建的控件数量和复杂性。对于 STM32F103C8T620KB RAM建议配置为#define LV_MEM_SIZE (16U * 1024U)对于 STM32F407192KB RAM可以放宽到#define LV_MEM_SIZE (64U * 1024U)一个经验法则是内存池大小不要超过可用 RAM 的一半否则留给栈和 HAL 库的内存会不足。如果运行过程中出现控件丢失、绘制异常或 HardFault优先检查LV_MEM_SIZE。7.2 使用 LVGL 内存分析LVGL 提供了内存监控接口可以在日志中输出内存状态lv_mem_monitor_t mon; lv_mem_monitor(mon); printf(total: %d, free: %d, used: %d\n, mon.total_size, mon.free_size, mon.used_size);在开发阶段建议在lv_timer_handler()循环外周期打印内存信息观察是否有内存泄漏或分配失败。7.3 图片资源优化手表 UI 中经常会用到图标图片。如果直接使用 RGB565 位图一张 60x60 的图标就要60 * 60 * 2 7200 字节一张 240x240 的全屏背景图则是240 * 240 * 2 115200 字节 ≈ 112.5KB这还不算图片在内存中的加载开销。要优化图片资源有几种方式使用 LVGL 内置图片转换器把图片转为 C 数组并以LV_IMG_CF_TRUE_COLOR_ALPHA格式存储支持透明通道。使用索引色图片颜色较少时使用LV_IMG_CF_INDEXED_1BIT或LV_IMG_CF_INDEXED_2BIT体积大幅下降。压缩图片尺寸手表屏幕本身不大不需要用超高清图片素材。按需加载使用 LVGL 的图片缓存机制避免所有图片同时加载。8. 常见问题与排查思路这里整理了我开发 STM32 LVGL 项目时遇到频率比较高的问题希望能帮你省一些排查时间。问题现象常见原因解决思路屏幕白屏LVGL 初始化失败或 LCD 初始化未完成检查 SPI 引脚配置、复位时序、LVGL 初始化顺序屏幕有显示但花屏颜色格式不匹配确认 LV_COLOR_DEPTH 与 LCD 初始化颜色格式一致触摸无反应输入设备未正确注册 / 坐标范围错误检查 indev 注册代码触摸坐标是否越界卡顿严重LVGL 刷新率低、SPI 速率低提高 SPI 频率、开启 DMA 传输、降低屏幕分辨率进入 HardFault内存溢出 / 空指针 / 栈溢出减少 LV_MEM_SIZE、检查代码逻辑、使用调试器定位界面元素消失内存分配失败将LV_MEM_SIZE调大或裁剪功能中文显示乱码/空白字体文件不支持中文字符使用包含目标中文字符的字库文件8.1 花屏问题花屏是新人最容易遇到的问题。通常原因是颜色格式不一致。LVGL 默认LV_COLOR_DEPTH为 16也就是 RGB565。如果屏幕初始化时设置的是 RGB888那么 LVGL 把 2 字节颜色当作 3 字节发送给屏幕画面当然是花的。8.2 HardFault 定位思路STM32 进 HardFault 后第一步不是瞎改代码而是先用调试器查看调用栈和 fault 状态寄存器。常见做法在调试器中打开 Fault Reports 窗口。查看 PC程序计数器和 LR链接寄存器的值判断是哪一行代码触发。查看是访问了非法地址还是栈溢出。如果是栈溢出可以在 CubeMX 中将main线程栈调大比如从 128 字节改为 512 字节同时在 RTOS 任务中为 LVGL 单独分配任务栈。8.3 LVGL 任务卡死原因LVGL 的lv_timer_handler()必须周期调用否则界面无法刷新。但调用频率不宜过快否则 CPU 占用过高。一般建议while (1) { lv_timer_handler(); HAL_Delay(5); // 5ms 调用一次即 200Hz }如果你在主循环中执行了耗时操作比如传感器读取、Flash 写入LVGL 界面会出现明显卡顿。这时最好将耗时长、阻塞型的操作放在独立 RTOS 任务或中断中保证 LVGL 刷新不被长时间阻塞。9. 完整工程示例流程为了让大家有更直观的印象我把从新建工程到屏幕亮起的完整流程再串一遍。9.1 步骤一CubeMX 生成基础工程选择你的 MCU 型号例如 STM32F103C8T6。配置时钟树SPI 外设引脚。如果使用内部 RTC使能 RTC。如果使用 ADC 检测电量使能 ADC。配置调试接口为 SWD注意有些板子调试口和 SPI 冲突需要关闭 JTAG。CubeMX 生成代码时注意在 Project Manager 中选择合适的工具链比如 MDK-ARM 或 STM32CubeIDE。9.2 步骤二添加 LVGL 源码在工程根目录新建LVGL文件夹。将下载的 LVGL 源码src目录和lv_conf_template.h复制进去。在 IDE 中添加src目录下所有.c文件。在头文件搜索路径中添加上级目录和src目录。将lv_conf_template.h改名为lv_conf.h并修改配置项。9.3 步骤三实现 LCD 驱动在lcd.c中实现屏幕初始化和画点函数。如果只是测试可以用一个简易画点函数验证屏幕void lcd_draw_pixel(uint16_t x, uint16_t y, uint16_t color) { lcd_set_window(x, y, x, y); uint8_t data[2] { color 8, color 0xFF }; lcd_write_data(data, 2); }但 LVGL 实际使用的是批量刷新因此lcd_fill或lcd_write_data才是性能关键。建议直接从第一步就使用批量写入函数。9.4 步骤四运行 LVGL demoLVGL 官方带了几个 demo 程序比如demos/widgets可以用来快速验证移植是否成功#include demos/lv_demos.h lv_demo_widgets();如果你的屏幕能正常显示 demo 界面说明 LVGL 移植已经成功后续就可以开始编写手表 UI 了。10. 最佳实践与工程建议10.1 代码分层设计不要把所有 UI 代码都写在main.c里。建议按功能拆分文件ui/ ├── ui_face.c // 表盘界面 ├── ui_menu.c // 菜单界面 ├── ui_settings.c // 设置界面 ├── ui_heart_rate.c // 心率界面 └── ui_common.c // 公共样式、页面切换辅助函数这样每个文件职责明确后续增加新页面时更方便维护。10.2 合理管理 LVGL 对象生命周期LVGL v8 中控件对象是通过引用计数和父子关系管理的。页面切换时如果你使用lv_scr_load而非lv_scr_load_anim旧屏幕不会自动删除。要注意在切换页面时决定是保留返回时恢复状态还是删除重新创建。// 返回表盘时如果表盘屏幕对象已删除则重新创建 if (face_scr NULL) { ui_face_create(); } lv_scr_load(face_scr);10.3 谨慎使用动态创建和删除频繁创建、删除控件会带来内存碎片。LVGL 内部使用内存池管理虽然自带碎片整理机制但如果长时间运行后控件创建失败可以考虑减少动态创建次数比如固定页面在初始化时一次性创建。使用lv_obj_clean清除子对象而不是删掉父对象。定期打印内存监控信息提前发现碎片问题。10.4 样式管理LVGL 支持全局样式和局部样式。建议把颜色、圆角、边距等公共样式抽取为lv_style_tstatic lv_style_t style_card; lv_style_init(style_card); lv_style_set_bg_color(style_card, lv_color_hex(0x222222)); lv_style_set_radius(style_card, 12); lv_style_set_pad_all(style_card, 8);然后多个页面复用时直接lv_obj_add_style(obj, style_card, 0);这样界面统一修改也方便。10.5 日志与调试开发阶段强烈建议开启串口日志。LVGL 内部有日志输出开关#define LV_USE_LOG 1 #define LV_LOG_LEVEL LV_LOG_LEVEL_WARN配合串口重定向可以在系统运行过程中看到 LVGL 的警告和错误信息比如内存不足、对象创建失败等。这在定位界面异常时非常有用。10.6 安全与稳定性如果智能手表项目后续连接了蓝牙、网络等模块需要特别注意数据安全和权限检查。嵌入式设备上尤其要注意蓝牙配对必须经过用户授权不能自动无条件连接。健康数据属于敏感数据如果上传到手机或云端必须加密传输。本地存储的传感器数据要考虑清理策略避免长期累积。涉及固件升级功能时必须先做版本校验、完整性和校验和验证防止升级过程中断电变砖。对于开发者个人项目至少要做到不采集非必要数据不上传明文隐私信息对外设操作遵循最小权限原则。11. 扩展方向项目的骨架完成之后你可以往以下几个方向继续扩展11.1 增加传感器心率传感器MAX30102通过 I2C 读取配合算法显示心率。加速度计MPU6050 或 LIS3DH实现计步和抬手亮屏。温湿度传感器SHT30显示环境温度。11.2 增加蓝牙通信使用 HC-05 蓝牙串口模块或低功耗蓝牙 BLE 模块实现手机消息推送。LVGL 界面中做一个通知列表每收到一条消息就弹出一个卡片提醒。11.3 增加 FreeRTOSLVGL 和传感器读取、蓝牙数据处理可以放在不同任务中。LVGL 任务优先级不宜过高一般与传感器任务同级或略低即可。注意在任务中访问 LVGL API 时要加互斥锁避免多任务同时操作 LVGL 导致崩溃。11.4 表盘主题把表盘设计成可切换模式数字时钟、指针时钟、极简表盘。每种表盘对应一个lv_obj_t *页面通过设置菜单切换。到这里一个基于 STM32 LVGL 的智能手表项目主体框架已经搭建完成。从硬件选型、环境搭建、LVGL 移植到表盘 UI 设计、时间获取、内存优化和常见问题排查整条链路都是可以在你的开发板上实际跑通的。不同芯片和屏幕驱动的具体初始化时序会有差异所以如果你在某个环节卡住了优先检查底层的 SPI 通信、屏幕初始化顺序和lv_conf.h配置这三块是大多数问题的高发区。我建议你先跑通最简流程再逐步增加页面和功能。把“屏幕亮起来”和“时间能刷新”这两个基础环节做扎实后面加再多的 UI 功能都会顺畅很多。第一次做智能手表项目的朋友直接在桌面上放一个lv_mem_monitor的打印能帮你省下大量排查内存问题的时间。如果这篇实战笔记对你有帮助可以收藏备用。等你做完第一版手表欢迎回来聊聊你在移植时踩过哪些坑。
返回列表