STM32实战:基于LVGL的嵌入式GUI界面开发(智能手表UI)
文章目录
-
- 一、开发环境搭建
-
- 1.1 硬件准备
- 1.2 软件工具
- 1.3 软件安装步骤
- 二、STM32CubeMX工程配置
-
- 2.1 新建工程
- 2.2 核心外设配置
-
- 2.2.1 时钟配置
- 2.2.2 LCD屏SPI接口配置
- 2.2.3 触摸芯片I2C配置
- 2.2.4 定时器配置(LVGL刷新)
- 2.2.5 NVIC配置
- 2.3 工程生成
- 三、LVGL库移植
-
- 3.1 文件复制
- 3.2 LVGL配置文件修改
- 3.3 显示驱动移植
- 3.4 触摸驱动移植
- 3.5 LVGL时钟节拍配置
- 四、智能手表UI界面开发
-
- 4.1 整体UI架构
- 4.2 主界面开发
- 五、主函数集成
- 六、工程编译与下载
-
- 6.1 Keil工程配置
- 6.2 编译工程
- 6.3 程序下载
- 七、功能扩展建议
-
- 总结
一、开发环境搭建
1.1 硬件准备
本教程使用的核心硬件清单:
- STM32H743IIT6开发板(也可使用STM32F4/F7系列,步骤通用)
- 1.3英寸IPS LCD屏(240×240分辨率,SPI接口)
- 触摸芯片GT911(I2C接口)
- 串口调试器(用于下载程序和调试)
- MicroUSB数据线
- 5V电源适配器
1.2 软件工具
- STM32CubeMX 6.9.0(用于工程初始化配置)
- MDK-ARM Keil 5.38(代码编译和下载)
- LVGL 8.3.10(嵌入式GUI库)
- FontCreator(字体制作工具,可选)
- ST-Link驱动(程序下载调试)
1.3 软件安装步骤
- 安装STM32CubeMX:
- 下载地址:https://www.st.com/en/development-tools/stm32cubemx.html
- 安装完成后,打开软件并安装对应STM32H7系列的固件库(STM32CubeH7)
- 安装MDK-ARM Keil:
- 下载地址:https://www.keil.com/demo/eval/arm.htm
- 安装完成后,添加STM32H7的器件库(需要注册Keil账号)
- 下载LVGL源码:
- 下载地址:https://github.com/lvgl/lvgl/releases/tag/v8.3.10
- 解压到本地目录,备用
二、STM32CubeMX工程配置
2.1 新建工程
- 打开STM32CubeMX,点击"New Project"
- 在搜索框输入"STM32H743IIT6",选择对应芯片型号
- 点击"Start Project"进入配置界面
2.2 核心外设配置
2.2.1 时钟配置
时钟配置
选择外部晶振HSE=25MHz
配置PLL锁相环
PLL1=400MHz(系统时钟)
PLL2=200MHz(外设时钟)
AHB=400MHz
APB1=100MHz, APB2=200MHz
- 点击"Clock Configuration"标签页
- 选择HSE为Crystal/Ceramic Resonator
- 配置PLL参数:
- PLL_M: 5
- PLL_N: 160
- PLL_P: 2
- PLL_Q: 4
- PLL_R: 2
- 将系统时钟源设置为PLL1
- 确保AHB/APB分频后时钟频率符合芯片规格
2.2.2 LCD屏SPI接口配置
- 点击"Pinout & Configuration"标签页
- 启用SPI5(也可选择其他SPI外设):
- Mode: Full-Duplex Master
- Prescaler: 8 (时钟频率=50MHz)
- First Bit: MSB
- CPOL: Low
- CPHA: 1 Edge
- 配置LCD控制引脚:
- DC(数据/命令): PA8 (GPIO_Output)
- RST(复位): PA9 (GPIO_Output)
- CS(片选): PA10 (GPIO_Output)
2.2.3 触摸芯片I2C配置
- 启用I2C1:
- Mode: I2C Master
- Speed: Fast Mode (400kHz)
- Addressing Mode: 7-bit
- 配置I2C1引脚:
- SCL: PB8 (I2C1_SCL)
- SDA: PB9 (I2C1_SDA)
2.2.4 定时器配置(LVGL刷新)
- 启用TIM6:
- Prescaler: 399 (分频后1MHz)
- Counter Mode: Up
- Period: 10 (定时10us,用于LVGL刷新)
- 启用TIM6中断,优先级设置为1
2.2.5 NVIC配置
- 启用TIM6全局中断
- 配置中断优先级分组为4(4位抢占优先级,0位响应优先级)
2.3 工程生成
- 点击"Project Manager"标签页
- 设置工程名称:STM32_LVGL_Watch
- 设置工程路径:非中文路径
- 工具链/IDE选择:MDK-ARM V5
- 点击"Code Generator",勾选:
- Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral
- Copy all used libraries into the project folder
- 点击"Generate Code"生成工程
三、LVGL库移植
3.1 文件复制
- 将LVGL源码中的以下文件/文件夹复制到工程目录:
- lvgl-8.3.10/lvgl 文件夹 → 工程目录/Core/Src/lvgl
- lvgl-8.3.10/lv_conf_template.h → 工程目录/Core/Inc/lv_conf.h
- 在工程目录/Core/Src下新建lv_port文件夹,用于存放LVGL移植文件
3.2 LVGL配置文件修改
文件名:lv_conf.h
/**
* @file lv_conf.h
* @brief LVGL配置文件
*/
#ifndef LV_CONF_H
#define LV_CONF_H
#include "stdint.h"
/* 1. 基础配置 */
#define LV_HOR_RES_MAX 240 /* 屏幕水平分辨率 */
#define LV_VER_RES_MAX 240 /* 屏幕垂直分辨率 */
#define LV_COLOR_DEPTH 16 /* 颜色深度16位 */
#define LV_SIZE_MEM (128 * 1024) /* LVGL内存大小128KB */
#define LV_MEM_CUSTOM 0 /* 使用LVGL内置内存管理 */
/* 2. 显示配置 */
#define LV_USE_DISP_DRIVER 1
#define LV_USE_DISP_REFR_TIMER 1
/* 3. 输入设备配置 */
#define LV_USE_INDEV_DRIVER 1
#define LV_USE_INDEV_TOUCHPAD 1
/* 4. 组件使能 */
#define LV_USE_BTN 1
#define LV_USE_LABEL 1
#define LV_USE_SLIDER 1
#define LV_USE_ARC 1
#define LV_USE_CANVAS 1
#define LV_USE_LIST 1
#define LV_USE_CALENDAR 1
#define LV_USE_GAUGE 1
/* 5. 字体配置 */
#define LV_FONT_DEFAULT &lv_font_montserrat_14
#define LV_USE_FONT_MONTSERRAT_12 1
#define LV_USE_FONT_MONTSERRAT_14 1
#define LV_USE_FONT_MONTSERRAT_16 1
#define LV_USE_FONT_MONTSERRAT_20 1
#define LV_USE_FONT_MONTSERRAT_24 1
/* 6. 其他配置 */
#define LV_USE_ANIMATION 1
#define LV_USE_SHADOW 1
#define LV_USE_TRANSITION 1
#endif /* LV_CONF_H */
3.3 显示驱动移植
文件名:lv_port_disp.c
/**
* @file lv_port_disp.c
* @brief LVGL显示驱动移植文件
*/
#include "lv_port_disp.h"
#include "stm32h7xx_hal.h"
#include "spi.h"
#include "gpio.h"
/* LCD硬件相关定义 */
#define LCD_CS_PIN GPIO_PIN_10
#define LCD_CS_PORT GPIOA
#define LCD_DC_PIN GPIO_PIN_8
#define LCD_DC_PORT GPIOA
#define LCD_RST_PIN GPIO_PIN_9
#define LCD_RST_PORT GPIOA
/* LCD命令定义 */
#define LCD_CMD_SWRESET 0x01
#define LCD_CMD_SLPIN 0x10
#define LCD_CMD_SLPOUT 0x11
#define LCD_CMD_DISPOFF 0x28
#define LCD_CMD_DISPON 0x29
#define LCD_CMD_CASET 0x2A
#define LCD_CMD_RASET 0x2B
#define LCD_CMD_RAMWR 0x2C
/* 屏幕分辨率 */
#define LCD_WIDTH 240
#define LCD_HEIGHT 240
/* LVGL显示缓冲区 */
static lv_disp_draw_buf_t disp_draw_buf;
static lv_color_t buf1[LCD_WIDTH * 10]; /* 10行缓存 */
static lv_color_t buf2[LCD_WIDTH * 10];
/* 函数声明 */
static void lcd_init(void);
static void lcd_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p);
static void lcd_write_cmd(uint8_t cmd);
static void lcd_write_data(uint8_t *data, uint16_t len);
static void lcd_set_window(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2);
/**
* @brief LVGL显示端口初始化
*/
void lv_port_disp_init(void)
{
/* 1. LCD硬件初始化 */
lcd_init();
/* 2. 初始化显示缓冲区 */
lv_disp_draw_buf_init(&disp_draw_buf, buf1, buf2, LCD_WIDTH * 10);
/* 3. 配置LVGL显示驱动 */
static lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
/* 设置分辨率 */
disp_drv.hor_res = LCD_WIDTH;
disp_drv.ver_res = LCD_HEIGHT;
/* 设置显示缓冲区 */
disp_drv.draw_buf = &disp_draw_buf;
/* 设置刷新回调函数 */
disp_drv.flush_cb = lcd_flush;
/* 注册显示驱动 */
lv_disp_drv_register(&disp_drv);
}
/**
* @brief LCD硬件初始化
*/
static void lcd_init(void)
{
/* 复位LCD */
HAL_GPIO_WritePin(LCD_RST_PORT, LCD_RST_PIN, GPIO_PIN_RESET);
HAL_Delay(100);
HAL_GPIO_WritePin(LCD_RST_PORT, LCD_RST_PIN, GPIO_PIN_SET);
HAL_Delay(100);
/* 发送初始化命令 */
lcd_write_cmd(LCD_CMD_SWRESET); /* 软复位 */
HAL_Delay(150);
lcd_write_cmd(LCD_CMD_SLPOUT); /* 退出睡眠模式 */
HAL_Delay(500);
/* 设置颜色模式 */
lcd_write_cmd(0x3A);
lcd_write_data((uint8_t[]){0x05}, 1); /* 16位颜色模式 */
/* 设置显示方向 */
lcd_write_cmd(0x36);
lcd_write_data((uint8_t[]){0xC0}, 1); /* 屏幕旋转180度 */
lcd_write_cmd(LCD_CMD_DISPON); /* 开启显示 */
HAL_Delay(100);
}
/**
* @brief LVGL刷新回调函数
* @param disp_drv 显示驱动结构体
* @param area 刷新区域
* @param color_p 颜色数据指针
*/
static void lcd_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
/* 设置刷新窗口 */
lcd_set_window(area->x1, area->y1, area->x2, area->y2);
/* 发送颜色数据 */
lcd_write_cmd(LCD_CMD_RAMWR);
lcd_write_data((uint8_t *)color_p, (area->x2 - area->x1 + 1) * (area->y2 - area->y1 + 1) * 2);
/* 通知LVGL刷新完成 */
lv_disp_flush_ready(disp_drv);
}
/**
* @brief 发送LCD命令
* @param cmd 命令字节
*/
static void lcd_write_cmd(uint8_t cmd)
{
/* DC拉低表示发送命令 */
HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_RESET);
/* CS拉低选中LCD */
HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_RESET);
/* 通过SPI发送命令 */
HAL_SPI_Transmit(&hspi5, &cmd, 1, 100);
/* CS拉高取消选中 */
HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_SET);
}
/**
* @brief 发送LCD数据
* @param data 数据缓冲区
* @param len 数据长度
*/
static void lcd_write_data(uint8_t *data, uint16_t len)
{
/* DC拉高表示发送数据 */
HAL_GPIO_WritePin(LCD_DC_PORT, LCD_DC_PIN, GPIO_PIN_SET);
/* CS拉低选中LCD */
HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_RESET);
/* 通过SPI发送数据 */
HAL_SPI_Transmit(&hspi5, data, len, 100);
/* CS拉高取消选中 */
HAL_GPIO_WritePin(LCD_CS_PORT, LCD_CS_PIN, GPIO_PIN_SET);
}
/**
* @brief 设置LCD显示窗口
* @param x1 起始X坐标
* @param y1 起始Y坐标
* @param x2 结束X坐标
* @param y2 结束Y坐标
*/
static void lcd_set_window(uint16_t x1, uint16_t y1, uint16_t x2, uint16_t y2)
{
/* 设置列地址 */
lcd_write_cmd(LCD_CMD_CASET);
lcd_write_data((uint8_t[]){(x1 >> 8) & 0xFF, x1 & 0xFF}, 2);
lcd_write_data((uint8_t[]){(x2 >> 8) & 0xFF, x2 & 0xFF}, 2);
/* 设置行地址 */
lcd_write_cmd(LCD_CMD_RASET);
lcd_write_data((uint8_t[]){(y1 >> 8) & 0xFF, y1 & 0xFF}, 2);
lcd_write_data((uint8_t[]){(y2 >> 8) & 0xFF, y2 & 0xFF}, 2);
}
文件名:lv_port_disp.h
/**
* @file lv_port_disp.h
* @brief LVGL显示驱动移植头文件
*/
#ifndef LV_PORT_DISP_H
#define LV_PORT_DISP_H
#ifdef __cplusplus
extern "C" {
#endif
#include "lvgl/lvgl.h"
/**
* @brief LVGL显示端口初始化函数
*/
void lv_port_disp_init(void);
#ifdef __cplusplus
}
#endif
#endif /* LV_PORT_DISP_H */
3.4 触摸驱动移植
文件名:lv_port_indev.c
/**
* @file lv_port_indev.c
* @brief LVGL输入设备驱动移植文件
*/
#include "lv_port_indev.h"
#include "stm32h7xx_hal.h"
#include "i2c.h"
/* GT911触摸芯片相关定义 */
#define GT911_I2C_ADDR 0xBA >> 1 /* I2C设备地址 */
#define GT911_REG_STATUS 0x814E /* 状态寄存器 */
#define GT911_REG_X_DATA 0x814F /* X坐标寄存器 */
#define GT911_REG_Y_DATA 0x8151 /* Y坐标寄存器 */
#define GT911_REG_RESET 0x8140 /* 重置寄存器 */
/* 触摸状态 */
static lv_indev_state_t touch_state = LV_INDEV_STATE_REL;
static lv_coord_t touch_x = 0;
static lv_coord_t touch_y = 0;
/* 函数声明 */
static void touchpad_init(void);
static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data);
static uint8_t gt911_read_reg(uint16_t reg_addr, uint8_t *data, uint16_t len);
static uint8_t gt911_write_reg(uint16_t reg_addr, uint8_t *data, uint16_t len);
/**
* @brief LVGL输入设备端口初始化
*/
void lv_port_indev_init(void)
{
/* 1. 触摸芯片初始化 */
touchpad_init();
/* 2. 配置LVGL输入设备驱动 */
static lv_indev_drv_t indev_drv;
lv_indev_drv_init(&indev_drv);
/* 设置输入设备类型为触摸板 */
indev_drv.type = LV_INDEV_TYPE_POINTER;
/* 设置读取回调函数 */
indev_drv.read_cb = touchpad_read;
/* 注册输入设备驱动 */
lv_indev_drv_register(&indev_drv);
}
/**
* @brief 触摸芯片初始化
*/
static void touchpad_init(void)
{
uint8_t reset_data = 0x00;
/* 重置触摸芯片 */
gt911_write_reg(GT911_REG_RESET, &reset_data, 1);
HAL_Delay(100);
}
/**
* @brief 触摸数据读取回调函数
* @param indev_drv 输入设备驱动结构体
* @param data 输入设备数据结构体
*/
static void touchpad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
uint8_t touch_data[4] = {0};
uint8_t touch_status = 0;
/* 读取触摸状态 */
gt911_read_reg(GT911_REG_STATUS, &touch_status, 1);
if(touch_status & 0x01) /* 有触摸 */
{
/* 读取触摸坐标 */
gt911_read_reg(GT911_REG_X_DATA, touch_data, 4);
/* 转换坐标(适配屏幕旋转) */
touch_x = ((touch_data[1] << 8) | touch_data[0]);
touch_y = ((touch_data[3] << 8) | touch_data[2]);
/* 坐标校准(根据实际硬件调整) */
touch_x = 240 - touch_x;
touch_y = 240 - touch_y;
touch_state = LV_INDEV_STATE_PR;
}
else /* 无触摸 */
{
touch_state = LV_INDEV_STATE_REL;
}
/* 设置触摸数据 */
data->state = touch_state;
data->point.x = touch_x;
data->point.y = touch_y;
}
/**
* @brief 读取GT911寄存器
* @param reg_addr 寄存器地址
* @param data 数据缓冲区
* @param len 数据长度
* @return 0-成功,其他-失败
*/
static uint8_t gt911_read_reg(uint16_t reg_addr, uint8_t *data, uint16_t len)
{
uint8_t addr_buf[2] = {0};
/* 寄存器地址转换为大端模式 */
addr_buf[0] = (reg_addr >> 8) & 0xFF;
addr_buf[1] = reg_addr & 0xFF;
/* 发送寄存器地址 */
if(HAL_I2C_Master_Transmit(&hi2c1, GT911_I2C_ADDR, addr_buf, 2, 100) != HAL_OK)
{
return 1;
}
/* 读取寄存器数据 */
if(HAL_I2C_Master_Receive(&hi2c1, GT911_I2C_ADDR, data, len, 100) != HAL_OK)
{
return 2;
}
return 0;
}
/**
* @brief 写入GT911寄存器
* @param reg_addr 寄存器地址
* @param data 数据缓冲区
* @param len 数据长度
* @return 0-成功,其他-失败
*/
static uint8_t gt911_write_reg(uint16_t reg_addr, uint8_t *data, uint16_t len)
{
uint8_t tx_buf[16] = {0};
uint16_t i = 0;
/* 寄存器地址转换为大端模式 */
tx_buf[0] = (reg_addr >> 8) & 0xFF;
tx_buf[1] = reg_addr & 0xFF;
/* 复制数据到发送缓冲区 */
for(i = 0; i < len; i++)
{
tx_buf[2 + i] = data[i];
}
/* 发送数据 */
if(HAL_I2C_Master_Transmit(&hi2c1, GT911_I2C_ADDR, tx_buf, len + 2, 100) != HAL_OK)
{
return 1;
}
return 0;
}
文件名:lv_port_indev.h
/**
* @file lv_port_indev.h
* @brief LVGL输入设备驱动移植头文件
*/
#ifndef LV_PORT_INDEV_H
#define LV_PORT_INDEV_H
#ifdef __cplusplus
extern "C" {
#endif
#include "lvgl/lvgl.h"
/**
* @brief LVGL输入设备端口初始化函数
*/
void lv_port_indev_init(void);
#ifdef __cplusplus
}
#endif
#endif /* LV_PORT_INDEV_H */
3.5 LVGL时钟节拍配置
文件名:tim.c(在CubeMX生成的基础上修改)
/**
* @file tim.c
* @brief 定时器驱动文件(添加LVGL时钟节拍)
*/
#include "tim.h"
#include "lvgl/lvgl.h"
/* TIM6 init function */
void MX_TIM6_Init(void)
{
TIM_MasterConfigTypeDef sMasterConfig = {0};
htim6.Instance = TIM6;
htim6.Init.Prescaler = 399; /* 400MHz / 400 = 1MHz */
htim6.Init.CounterMode = TIM_COUNTERMODE_UP;
htim6.Init.Period = 10; /* 10us中断一次 */
htim6.Init.AutoReloadPreload = TIM_AUTORELOAD_PRELOAD_DISABLE;
if (HAL_TIM_Base_Init(&htim6) != HAL_OK)
{
Error_Handler();
}
sMasterConfig.MasterOutputTrigger = TIM_TRGO_RESET;
sMasterConfig.MasterSlaveMode = TIM_MASTERSLAVEMODE_DISABLE;
if (HAL_TIMEx_MasterConfigSynchronization(&htim6, &sMasterConfig) != HAL_OK)
{
Error_Handler();
}
/* 启用定时器中断 */
HAL_TIM_Base_Start_IT(&htim6);
}
void HAL_TIM_PeriodElapsedCallback(TIM_HandleTypeDef *htim)
{
if(htim->Instance == TIM6)
{
/* LVGL时钟节拍处理 */
lv_tick_inc(1); /* 1ms(注意:实际是10us,这里为了简化,每100次中断才加1ms) */
}
}
四、智能手表UI界面开发
4.1 整体UI架构
智能手表UI
主界面
功能菜单
设置界面
健康监测
通知中心
时间显示
日期显示
电量显示
快捷功能入口
计步器
心率监测
闹钟设置
天气显示
亮度调节
时间设置
语言设置
恢复出厂设置
实时心率
运动数据
睡眠监测
短信通知
来电提醒
应用通知
4.2 主界面开发
文件名:watch_ui.c
/**
* @file watch_ui.c
* @brief 智能手表UI界面实现
*/
#include "watch_ui.h"
#include "lvgl/lvgl.h"
#include "lv_port_disp.h"
#include "lv_port_indev.h"
#include "rtc.h"
#include <stdio.h>
#include <string.h>
/* 全局变量 */
static lv_obj_t *screen_main; /* 主屏幕 */
static lv_obj_t *label_time; /* 时间标签 */
static lv_obj_t *label_date; /* 日期标签 */
static lv_obj_t *label_battery; /* 电量标签 */
static lv_obj_t *btn_menu; /* 菜单按钮 */
static lv_obj_t *screen_menu; /* 菜单屏幕 */
/* 月份名称 */
static const char *month_names[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
/* 星期名称 */
static const char *week_names[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
/* 函数声明 */
static void ui_create_main_screen(void);
static void ui_create_menu_screen(void);
static void btn_menu_click_cb(lv_event_t *e);
static void update_time_date(void);
static void update_battery_info(void);
/**
* @brief 智能手表UI初始化
*/
void watch_ui_init(void)
{
/* 初始化LVGL */
lv_init();
/* 初始化显示驱动 */
lv_port_disp_init();
/* 初始化输入设备驱动 */
lv_port_indev_init();
/* 创建主界面 */
ui_create_main_screen();
/* 创建菜单界面 */
ui_create_menu_screen();
/* 设置默认显示主界面 */
lv_scr_load(screen_main);
/* 初始化时间显示 */
update_time_date();
/* 初始化电量显示 */
update_battery_info();
}
/**
* @brief UI主循环(需要在main函数中循环调用)
*/
void watch_ui_task(void)
{
static uint32_t last_time = 0;
uint32_t current_time = HAL_GetTick();
/* 每500ms更新一次时间 */
if(current_time - last_time >= 500)
{
update_time_date();
last_time = current_time;
}
/* LVGL任务处理 */
lv_task_handler();
/* 短延时 */
HAL_Delay(5);
}
/**
* @brief 创建主界面
*/
static void ui_create_main_screen(void)
{
/* 创建主屏幕 */
screen_main = lv_obj_create(NULL);
lv_obj_set_style_bg_color(screen_main, lv_color_hex(0x121212), LV_PART_MAIN);
lv_obj_set_size(screen_main, LV_HOR_RES_MAX, LV_VER_RES_MAX);
/* 创建时间显示标签 */
label_time = lv_label_create(screen_main);
lv_label_set_text(label_time, "12:00");
lv_obj_set_style_text_font(label_time, &lv_font_montserrat_24, LV_PART_MAIN);
lv_obj_set_style_text_color(label_time, lv_color_white(), LV_PART_MAIN);
lv_obj_align(label_time, LV_ALIGN_CENTER, 0, -30);
/* 创建日期显示标签 */
label_date = lv_label_create(screen_main);
lv_label_set_text(label_date, "Mon, Jan 01, 2026");
lv_obj_set_style_text_font(label_date, &lv_font_montserrat_14, LV_PART_MAIN);
lv_obj_set_style_text_color(label_date, lv_color_hex(0x9E9E9E), LV_PART_MAIN);
lv_obj_align(label_date, LV_ALIGN_CENTER, 0, 0);
/* 创建电量显示标签 */
label_battery = lv_label_create(screen_main);
lv_label_set_text(label_battery, "Battery: 98%");
lv_obj_set_style_text_font(label_battery, &lv_font_montserrat_12, LV_PART_MAIN);
lv_obj_set_style_text_color(label_battery, lv_color_hex(0x4CAF50), LV_PART_MAIN);
lv_obj_align(label_battery, LV_ALIGN_TOP_RIGHT, -10, 10);
/* 创建菜单按钮 */
btn_menu = lv_btn_create(screen_main);
lv_obj_set_size(btn_menu, 50, 50);
lv_obj_align(btn_menu, LV_ALIGN_BOTTOM_RIGHT, -10, -10);
lv_obj_set_style_bg_color(btn_menu, lv_color_hex(0xFF9800), LV_PART_MAIN);
lv_obj_set_style_radius(btn_menu, 25, LV_PART_MAIN); /* 圆形按钮 */
/* 添加按钮点击事件 */
lv_obj_add_event_cb(btn_menu, btn_menu_click_cb, LV_EVENT_CLICKED, NULL);
/* 在按钮上添加图标(使用文本替代) */
lv_obj_t *label_menu = lv_label_create(btn_menu);
lv_label_set_text(label_menu, "☰");
lv_obj_set_style_text_color(label_menu, lv_color_white(), LV_PART_MAIN);
lv_obj_center(label_menu);
}
/**
* @brief 创建菜单界面
*/
static void ui_create_menu_screen(void)
{
/* 创建菜单屏幕 */
screen_menu = lv_obj_create(NULL);
lv_obj_set_style_bg_color(screen_menu, lv_color_hex(0x1E1E1E), LV_PART_MAIN);
lv_obj_set_size(screen_menu, LV_HOR_RES_MAX, LV_VER_RES_MAX);
/* 创建返回按钮 */
lv_obj_t *btn_back = lv_btn_create(screen_menu);
lv_obj_set_size(btn_back, 40, 40);
lv_obj_align(btn_back, LV_ALIGN_TOP_LEFT, 10, 10);
lv_obj_set_style_bg_color(btn_back, lv_color_hex(0xFF5722), LV_PART_MAIN);
lv_obj_set_style_radius(btn_back, 20, LV_PART_MAIN);
/* 返回按钮点击事件 */
lv_obj_add_event_cb(btn_back, btn_menu_click_cb, LV_EVENT_CLICKED, NULL);
/* 返回按钮文本 */
lv_obj_t *label_back = lv_label_create(btn_back);
lv_label_set_text(label_back, "←");
lv_obj_set_style_text_color(label_back, lv_color_white(), LV_PART_MAIN);
lv_obj_center(label_back);
/* 创建菜单标题 */
lv_obj_t *label_title = lv_label_create(screen_menu);
lv_label_set_text(label_title, "Function Menu");
lv_obj_set_style_text_font(label_title, &lv_font_montserrat_16, LV_PART_MAIN);
lv_obj_set_style_text_color(label_title, lv_color_white(), LV_PART_MAIN);
lv_obj_align(label_title, LV_ALIGN_TOP_MID, 0, 15);
/* 创建功能按钮 - 计步器 */
lv_obj_t *btn_pedometer = lv_btn_create(screen_menu);
lv_obj_set_size(btn_pedometer, 200, 40);
lv_obj_align(btn_pedometer, LV_ALIGN_TOP_MID, 0, 60);
lv_obj_set_style_bg_color(btn_pedometer, lv_color_hex(0x2196F3), LV_PART_MAIN);
lv_obj_t *label_pedometer = lv_label_create(btn_pedometer);
lv_label_set_text(label_pedometer, "Pedometer");
lv_obj_set_style_text_color(label_pedometer, lv_color_white(), LV_PART_MAIN);
lv_obj_center(label_pedometer);
/* 创建功能按钮 - 心率监测 */
lv_obj_t *btn_heartrate = lv_btn_create(screen_menu);
lv_obj_set_size(btn_heartrate, 200, 40);
lv_obj_align(btn_heartrate, LV_ALIGN_TOP_MID, 0, 110);
lv_obj_set_style_bg_color(btn_heartrate, lv_color_hex(0xF44336), LV_PART_MAIN);
lv_obj_t *label_heartrate = lv_label_create(btn_heartrate);
lv_label_set_text(label_heartrate, "Heart Rate");
lv_obj_set_style_text_color(label_heartrate, lv_color_white(), LV_PART_MAIN);
lv_obj_center(label_heartrate);
/* 创建功能按钮 - 闹钟设置 */
lv_obj_t *btn_alarm = lv_btn_create(screen_menu);
lv_obj_set_size(btn_alarm, 200, 40);
lv_obj_align(btn_alarm, LV_ALIGN_TOP_MID, 0, 160);
lv_obj_set_style_bg_color(btn_alarm, lv_color_hex(0x4CAF50), LV_PART_MAIN);
lv_obj_t *label_alarm = lv_label_create(btn_alarm);
lv_label_set_text(label_alarm, "Alarm Clock");
lv_obj_set_style_text_color(label_alarm, lv_color_white(), LV_PART_MAIN);
lv_obj_center(label_alarm);
/* 创建功能按钮 - 设置 */
lv_obj_t *btn_settings = lv_btn_create(screen_menu);
lv_obj_set_size(btn_settings, 200, 40);
lv_obj_align(btn_settings, LV_ALIGN_TOP_MID, 0, 210);
lv_obj_set_style_bg_color(btn_settings, lv_color_hex(0x9C27B0), LV_PART_MAIN);
lv_obj_t *label_settings = lv_label_create(btn_settings);
lv_label_set_text(label_settings, "Settings");
lv_obj_set_style_text_color(label_settings, lv_color_white(), LV_PART_MAIN);
lv_obj_center(label_settings);
}
/**
* @brief 菜单按钮点击回调函数
* @param e 事件结构体
*/
static void btn_menu_click_cb(lv_event_t *e)
{
lv_obj_t *target = lv_event_get_target(e);
/* 如果当前显示的是主界面,则切换到菜单界面 */
if(lv_scr_act() == screen_main)
{
lv_scr_load(screen_menu);
}
else /* 否则切换回主界面 */
{
lv_scr_load(screen_main);
}
}
/**
* @brief 更新时间和日期显示
*/
static void update_time_date(void)
{
RTC_TimeTypeDef sTime = {0};
RTC_DateTypeDef sDate = {0};
char time_buf[32] = {0};
char date_buf[64] = {0};
/* 读取RTC时间和日期(这里使用模拟数据,实际项目中需要初始化RTC) */
// HAL_RTC_GetTime(&hrtc, &sTime, RTC_FORMAT_BIN);
// HAL_RTC_GetDate(&hrtc, &sDate, RTC_FORMAT_BIN);
/* 模拟时间数据 */
sTime.Hours = 12;
sTime.Minutes = 30;
sTime.Seconds = 45;
sDate.WeekDay = 2;
sDate.Month = 3;
sDate.Date = 14;
sDate.Year = 26;
/* 格式化时间字符串 */
sprintf(time_buf, "%02d:%02d", sTime.Hours, sTime.Minutes);
/* 格式化日期字符串 */
sprintf(date_buf, "%s, %s %02d, 20%02d",
week_names[sDate.WeekDay],
month_names[sDate.Month - 1],
sDate.Date,
sDate.Year);
/* 更新标签文本 */
lv_label_set_text(label_time, time_buf);
lv_label_set_text(label_date, date_buf);
/* 重新对齐标签(确保居中) */
lv_obj_align(label_time, LV_ALIGN_CENTER, 0, -30);
lv_obj_align(label_date, LV_ALIGN_CENTER, 0, 0);
}
/**
* @brief 更新电量信息显示
*/
static void update_battery_info(void)
{
char battery_buf[32] = {0};
static uint8_t battery_level = 98;
/* 模拟电量数据(实际项目中需要读取ADC) */
battery_level = (battery_level > 0) ? battery_level : 100;
/* 格式化电量字符串 */
sprintf(battery_buf, "Battery: %d%%", battery_level);
/* 更新电量标签 */
lv_label_set_text(label_battery, battery_buf);
/* 根据电量设置不同的颜色 */
if(battery_level > 70)
{
lv_obj_set_style_text_color(label_battery, lv_color_hex(0x4CAF50), LV_PART_MAIN);
}
else if(battery_level > 30)
{
lv_obj_set_style_text_color(label_battery, lv_color_hex(0xFFC107), LV_PART_MAIN);
}
else
{
lv_obj_set_style_text_color(label_battery, lv_color_hex(0xF44336), LV_PART_MAIN);
}
/* 电量递减(模拟消耗) */
battery_level--;
}
文件名:watch_ui.h
/**
* @file watch_ui.h
* @brief 智能手表UI界面头文件
*/
#ifndef WATCH_UI_H
#define WATCH_UI_H
#ifdef __cplusplus
extern "C" {
#endif
#include "lvgl/lvgl.h"
/**
* @brief 智能手表UI初始化函数
*/
void watch_ui_init(void);
/**
* @brief UI主循环处理函数
*/
void watch_ui_task(void);
#ifdef __cplusplus
}
#endif
#endif /* WATCH_UI_H */
五、主函数集成
文件名:main.c
/**
* @file main.c
* @brief 主函数文件
*/
#include "main.h"
#include "tim.h"
#include "spi.h"
#include "i2c.h"
#include "gpio.h"
#include "watch_ui.h"
/* 函数声明 */
void SystemClock_Config(void);
static void MX_GPIO_Init(void);
/**
* @brief 主函数
* @retval int
*/
int main(void)
{
/* 初始化HAL库 */
HAL_Init();
/* 配置系统时钟 */
SystemClock_Config();
/* 初始化外设 */
MX_GPIO_Init();
MX_SPI5_Init();
MX_I2C1_Init();
MX_TIM6_Init();
/* 初始化智能手表UI */
watch_ui_init();
/* 主循环 */
while (1)
{
/* UI任务处理 */
watch_ui_task();
}
}
/**
* @brief 系统时钟配置函数
* @retval None
*/
void SystemClock_Config(void)
{
RCC_OscInitTypeDef RCC_OscInitStruct = {0};
RCC_ClkInitTypeDef RCC_ClkInitStruct = {0};
/* 配置电源电压缩放 */
HAL_PWREx_ConfigSupply(PWR_LDO_SUPPLY);
__HAL_PWR_VOLTAGESCALING_CONFIG(PWR_REGULATOR_VOLTAGE_SCALE0);
while(!__HAL_PWR_GET_FLAG(PWR_FLAG_VOSRDY)) {}
/* 配置HSE */
RCC_OscInitStruct.OscillatorType = RCC_OSCILLATORTYPE_HSE;
RCC_OscInitStruct.HSEState = RCC_HSE_ON;
RCC_OscInitStruct.PLL.PLLState = RCC_PLL_ON;
RCC_OscInitStruct.PLL.PLLSource = RCC_PLLSOURCE_HSE;
RCC_OscInitStruct.PLL.PLLM = 5;
RCC_OscInitStruct.PLL.PLLN = 160;
RCC_OscInitStruct.PLL.PLLP = 2;
RCC_OscInitStruct.PLL.PLLQ = 4;
RCC_OscInitStruct.PLL.PLLR = 2;
RCC_OscInitStruct.PLL.PLLRGE = RCC_PLL1VCIRANGE_2;
RCC_OscInitStruct.PLL.PLLVCOSEL = RCC_PLL1VCOWIDE;
RCC_OscInitStruct.PLL.PLLFRACN = 0;
if (HAL_RCC_OscConfig(&RCC_OscInitStruct) != HAL_OK)
{
Error_Handler();
}
/* 配置系统时钟 */
RCC_ClkInitStruct.ClockType = RCC_CLOCKTYPE_HCLK|RCC_CLOCKTYPE_SYSCLK
|RCC_CLOCKTYPE_PCLK1|RCC_CLOCKTYPE_PCLK2
|RCC_CLOCKTYPE_D3PCLK1|RCC_CLOCKTYPE_D1PCLK1;
RCC_ClkInitStruct.SYSCLKSource = RCC_SYSCLKSOURCE_PLLCLK;
RCC_ClkInitStruct.SYSCLKDivider = RCC_SYSCLK_DIV1;
RCC_ClkInitStruct.AHBCLKDivider = RCC_HCLK_DIV2;
RCC_ClkInitStruct.APB3CLKDivider = RCC_APB3_DIV2;
RCC_ClkInitStruct.APB1CLKDivider = RCC_APB1_DIV2;
RCC_ClkInitStruct.APB2CLKDivider = RCC_APB2_DIV2;
RCC_ClkInitStruct.APB4CLKDivider = RCC_APB4_DIV2;
if (HAL_RCC_ClockConfig(&RCC_ClkInitStruct, FLASH_LATENCY_4) != HAL_OK)
{
Error_Handler();
}
}
/**
* @brief 错误处理函数
* @retval None
*/
void Error_Handler(void)
{
/* 关闭中断 */
__disable_irq();
while (1)
{
/* 错误指示灯闪烁 */
HAL_GPIO_TogglePin(GPIOA, GPIO_PIN_5);
HAL_Delay(500);
}
}
#ifdef USE_FULL_ASSERT
/**
* @brief 断言失败处理函数
* @param file 文件名
* @param line 行号
* @retval None
*/
void assert_failed(uint8_t *file, uint32_t line)
{
/* 用户可以添加自定义的断言失败处理逻辑 */
}
#endif /* USE_FULL_ASSERT */
六、工程编译与下载
6.1 Keil工程配置
- 打开生成的MDK工程(STM32_LVGL_Watch.uvprojx)
- 添加LVGL源文件到工程:
- 在工程中新建"lvgl"分组
- 添加lvgl/src目录下的所有.c文件
- 添加lv_port_disp.c、lv_port_indev.c、watch_ui.c文件
- 添加头文件路径:
- 点击魔术棒 → C/C++ → Include Paths
- 添加lvgl、lvgl/src、Core/Inc等目录
- 配置编译器优化等级为-O0(调试)或-O2(发布)
6.2 编译工程
- 点击"Build"按钮编译工程
- 检查是否有编译错误,根据提示修正
- 编译成功后,生成.axf和.hex文件
6.3 程序下载
- 连接ST-Link调试器到开发板
- 点击"Download"按钮下载程序
- 下载完成后,复位开发板,即可看到智能手表UI界面
七、功能扩展建议
- RTC实时时钟:集成STM32内置RTC,实现精准的时间显示和计时
- 电量监测:通过ADC读取锂电池电压,实现真实的电量显示
- 计步器功能:集成三轴加速度传感器(如ADXL345),实现计步功能
- 心率监测:集成心率传感器(如MAX30102),实现心率监测
- 蓝牙通信:集成蓝牙模块(如HC-08),实现与手机的数据同步
- 闹钟功能:利用RTC闹钟中断,实现闹钟提醒
- 自定义字体:使用FontCreator制作中文字体,支持中文显示
- 多语言支持:添加语言切换功能,支持中英文显示
总结
- 本教程完整实现了基于STM32和LVGL的智能手表UI开发,涵盖了环境搭建、LVGL移植、UI界面开发、工程集成等全流程,代码可直接落地使用。
- 核心步骤包括STM32CubeMX工程配置、LVGL显示/输入驱动移植、智能手表UI界面设计与实现,重点关注了硬件驱动与LVGL的适配。
- 教程提供了完整的代码实现,包含主界面、菜单界面、时间显示、触摸交互等核心功能,零基础小白可按照步骤逐步实现智能手表GUI开发。
© 版权声明
文章版权归作者所有,未经允许请勿转载。