STM32实战:基于LVGL的嵌入式GUI界面开发(智能手表UI)

AI2天前发布 beixibaobao
2 0 0

文章目录

    • 一、开发环境搭建
      • 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 软件安装步骤

  1. 安装STM32CubeMX:
    • 下载地址:https://www.st.com/en/development-tools/stm32cubemx.html
    • 安装完成后,打开软件并安装对应STM32H7系列的固件库(STM32CubeH7)
  2. 安装MDK-ARM Keil:
    • 下载地址:https://www.keil.com/demo/eval/arm.htm
    • 安装完成后,添加STM32H7的器件库(需要注册Keil账号)
  3. 下载LVGL源码:
    • 下载地址:https://github.com/lvgl/lvgl/releases/tag/v8.3.10
    • 解压到本地目录,备用

二、STM32CubeMX工程配置

2.1 新建工程

  1. 打开STM32CubeMX,点击"New Project"
  2. 在搜索框输入"STM32H743IIT6",选择对应芯片型号
  3. 点击"Start Project"进入配置界面

2.2 核心外设配置

2.2.1 时钟配置

时钟配置

选择外部晶振HSE=25MHz

配置PLL锁相环

PLL1=400MHz(系统时钟)

PLL2=200MHz(外设时钟)

AHB=400MHz

APB1=100MHz, APB2=200MHz

  1. 点击"Clock Configuration"标签页
  2. 选择HSE为Crystal/Ceramic Resonator
  3. 配置PLL参数:
    • PLL_M: 5
    • PLL_N: 160
    • PLL_P: 2
    • PLL_Q: 4
    • PLL_R: 2
  4. 将系统时钟源设置为PLL1
  5. 确保AHB/APB分频后时钟频率符合芯片规格
2.2.2 LCD屏SPI接口配置
  1. 点击"Pinout & Configuration"标签页
  2. 启用SPI5(也可选择其他SPI外设):
    • Mode: Full-Duplex Master
    • Prescaler: 8 (时钟频率=50MHz)
    • First Bit: MSB
    • CPOL: Low
    • CPHA: 1 Edge
  3. 配置LCD控制引脚:
    • DC(数据/命令): PA8 (GPIO_Output)
    • RST(复位): PA9 (GPIO_Output)
    • CS(片选): PA10 (GPIO_Output)
2.2.3 触摸芯片I2C配置
  1. 启用I2C1:
    • Mode: I2C Master
    • Speed: Fast Mode (400kHz)
    • Addressing Mode: 7-bit
  2. 配置I2C1引脚:
    • SCL: PB8 (I2C1_SCL)
    • SDA: PB9 (I2C1_SDA)
2.2.4 定时器配置(LVGL刷新)
  1. 启用TIM6:
    • Prescaler: 399 (分频后1MHz)
    • Counter Mode: Up
    • Period: 10 (定时10us,用于LVGL刷新)
  2. 启用TIM6中断,优先级设置为1
2.2.5 NVIC配置
  1. 启用TIM6全局中断
  2. 配置中断优先级分组为4(4位抢占优先级,0位响应优先级)

2.3 工程生成

  1. 点击"Project Manager"标签页
  2. 设置工程名称:STM32_LVGL_Watch
  3. 设置工程路径:非中文路径
  4. 工具链/IDE选择:MDK-ARM V5
  5. 点击"Code Generator",勾选:
    • Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral
    • Copy all used libraries into the project folder
  6. 点击"Generate Code"生成工程

三、LVGL库移植

3.1 文件复制

  1. 将LVGL源码中的以下文件/文件夹复制到工程目录:
    • lvgl-8.3.10/lvgl 文件夹 → 工程目录/Core/Src/lvgl
    • lvgl-8.3.10/lv_conf_template.h → 工程目录/Core/Inc/lv_conf.h
  2. 在工程目录/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工程配置

  1. 打开生成的MDK工程(STM32_LVGL_Watch.uvprojx)
  2. 添加LVGL源文件到工程:
    • 在工程中新建"lvgl"分组
    • 添加lvgl/src目录下的所有.c文件
    • 添加lv_port_disp.c、lv_port_indev.c、watch_ui.c文件
  3. 添加头文件路径:
    • 点击魔术棒 → C/C++ → Include Paths
    • 添加lvgl、lvgl/src、Core/Inc等目录
  4. 配置编译器优化等级为-O0(调试)或-O2(发布)

6.2 编译工程

  1. 点击"Build"按钮编译工程
  2. 检查是否有编译错误,根据提示修正
  3. 编译成功后,生成.axf和.hex文件

6.3 程序下载

  1. 连接ST-Link调试器到开发板
  2. 点击"Download"按钮下载程序
  3. 下载完成后,复位开发板,即可看到智能手表UI界面

七、功能扩展建议

  1. RTC实时时钟:集成STM32内置RTC,实现精准的时间显示和计时
  2. 电量监测:通过ADC读取锂电池电压,实现真实的电量显示
  3. 计步器功能:集成三轴加速度传感器(如ADXL345),实现计步功能
  4. 心率监测:集成心率传感器(如MAX30102),实现心率监测
  5. 蓝牙通信:集成蓝牙模块(如HC-08),实现与手机的数据同步
  6. 闹钟功能:利用RTC闹钟中断,实现闹钟提醒
  7. 自定义字体:使用FontCreator制作中文字体,支持中文显示
  8. 多语言支持:添加语言切换功能,支持中英文显示

总结

  1. 本教程完整实现了基于STM32和LVGL的智能手表UI开发,涵盖了环境搭建、LVGL移植、UI界面开发、工程集成等全流程,代码可直接落地使用。
  2. 核心步骤包括STM32CubeMX工程配置、LVGL显示/输入驱动移植、智能手表UI界面设计与实现,重点关注了硬件驱动与LVGL的适配。
  3. 教程提供了完整的代码实现,包含主界面、菜单界面、时间显示、触摸交互等核心功能,零基础小白可按照步骤逐步实现智能手表GUI开发。
© 版权声明

相关文章