欢迎光临
我们一直在努力

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 时钟配置

    #mermaid-svg-aiR97byGi3ycZCyP{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-aiR97byGi3ycZCyP .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-aiR97byGi3ycZCyP .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-aiR97byGi3ycZCyP .error-icon{fill:#552222;}#mermaid-svg-aiR97byGi3ycZCyP .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-aiR97byGi3ycZCyP .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-aiR97byGi3ycZCyP .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-aiR97byGi3ycZCyP .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-aiR97byGi3ycZCyP .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-aiR97byGi3ycZCyP .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-aiR97byGi3ycZCyP .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-aiR97byGi3ycZCyP .marker{fill:#333333;stroke:#333333;}#mermaid-svg-aiR97byGi3ycZCyP .marker.cross{stroke:#333333;}#mermaid-svg-aiR97byGi3ycZCyP svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-aiR97byGi3ycZCyP p{margin:0;}#mermaid-svg-aiR97byGi3ycZCyP .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-aiR97byGi3ycZCyP .cluster-label text{fill:#333;}#mermaid-svg-aiR97byGi3ycZCyP .cluster-label span{color:#333;}#mermaid-svg-aiR97byGi3ycZCyP .cluster-label span p{background-color:transparent;}#mermaid-svg-aiR97byGi3ycZCyP .label text,#mermaid-svg-aiR97byGi3ycZCyP span{fill:#333;color:#333;}#mermaid-svg-aiR97byGi3ycZCyP .node rect,#mermaid-svg-aiR97byGi3ycZCyP .node circle,#mermaid-svg-aiR97byGi3ycZCyP .node ellipse,#mermaid-svg-aiR97byGi3ycZCyP .node polygon,#mermaid-svg-aiR97byGi3ycZCyP .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-aiR97byGi3ycZCyP .rough-node .label text,#mermaid-svg-aiR97byGi3ycZCyP .node .label text,#mermaid-svg-aiR97byGi3ycZCyP .image-shape .label,#mermaid-svg-aiR97byGi3ycZCyP .icon-shape .label{text-anchor:middle;}#mermaid-svg-aiR97byGi3ycZCyP .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-aiR97byGi3ycZCyP .rough-node .label,#mermaid-svg-aiR97byGi3ycZCyP .node .label,#mermaid-svg-aiR97byGi3ycZCyP .image-shape .label,#mermaid-svg-aiR97byGi3ycZCyP .icon-shape .label{text-align:center;}#mermaid-svg-aiR97byGi3ycZCyP .node.clickable{cursor:pointer;}#mermaid-svg-aiR97byGi3ycZCyP .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-aiR97byGi3ycZCyP .arrowheadPath{fill:#333333;}#mermaid-svg-aiR97byGi3ycZCyP .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-aiR97byGi3ycZCyP .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-aiR97byGi3ycZCyP .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aiR97byGi3ycZCyP .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-aiR97byGi3ycZCyP .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aiR97byGi3ycZCyP .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-aiR97byGi3ycZCyP .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-aiR97byGi3ycZCyP .cluster text{fill:#333;}#mermaid-svg-aiR97byGi3ycZCyP .cluster span{color:#333;}#mermaid-svg-aiR97byGi3ycZCyP div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-aiR97byGi3ycZCyP .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-aiR97byGi3ycZCyP rect.text{fill:none;stroke-width:0;}#mermaid-svg-aiR97byGi3ycZCyP .icon-shape,#mermaid-svg-aiR97byGi3ycZCyP .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-aiR97byGi3ycZCyP .icon-shape p,#mermaid-svg-aiR97byGi3ycZCyP .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-aiR97byGi3ycZCyP .icon-shape rect,#mermaid-svg-aiR97byGi3ycZCyP .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-aiR97byGi3ycZCyP .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-aiR97byGi3ycZCyP .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-aiR97byGi3ycZCyP :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}#mermaid-svg-aiR97byGi3ycZCyP .default>*{fill:#2c3e50!important;color:#ffffff!important;font-size:14px!important;}#mermaid-svg-aiR97byGi3ycZCyP .default span{fill:#2c3e50!important;color:#ffffff!important;font-size:14px!important;}#mermaid-svg-aiR97byGi3ycZCyP .default tspan{fill:#ffffff!important;}

    时钟配置

    选择外部晶振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架构

    #mermaid-svg-1wAJWUEWFfCiR3sS{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-1wAJWUEWFfCiR3sS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-1wAJWUEWFfCiR3sS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-1wAJWUEWFfCiR3sS .error-icon{fill:#552222;}#mermaid-svg-1wAJWUEWFfCiR3sS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-1wAJWUEWFfCiR3sS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-1wAJWUEWFfCiR3sS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-1wAJWUEWFfCiR3sS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-1wAJWUEWFfCiR3sS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-1wAJWUEWFfCiR3sS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-1wAJWUEWFfCiR3sS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-1wAJWUEWFfCiR3sS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-1wAJWUEWFfCiR3sS .marker.cross{stroke:#333333;}#mermaid-svg-1wAJWUEWFfCiR3sS svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-1wAJWUEWFfCiR3sS p{margin:0;}#mermaid-svg-1wAJWUEWFfCiR3sS .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-1wAJWUEWFfCiR3sS .cluster-label text{fill:#333;}#mermaid-svg-1wAJWUEWFfCiR3sS .cluster-label span{color:#333;}#mermaid-svg-1wAJWUEWFfCiR3sS .cluster-label span p{background-color:transparent;}#mermaid-svg-1wAJWUEWFfCiR3sS .label text,#mermaid-svg-1wAJWUEWFfCiR3sS span{fill:#333;color:#333;}#mermaid-svg-1wAJWUEWFfCiR3sS .node rect,#mermaid-svg-1wAJWUEWFfCiR3sS .node circle,#mermaid-svg-1wAJWUEWFfCiR3sS .node ellipse,#mermaid-svg-1wAJWUEWFfCiR3sS .node polygon,#mermaid-svg-1wAJWUEWFfCiR3sS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-1wAJWUEWFfCiR3sS .rough-node .label text,#mermaid-svg-1wAJWUEWFfCiR3sS .node .label text,#mermaid-svg-1wAJWUEWFfCiR3sS .image-shape .label,#mermaid-svg-1wAJWUEWFfCiR3sS .icon-shape .label{text-anchor:middle;}#mermaid-svg-1wAJWUEWFfCiR3sS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-1wAJWUEWFfCiR3sS .rough-node .label,#mermaid-svg-1wAJWUEWFfCiR3sS .node .label,#mermaid-svg-1wAJWUEWFfCiR3sS .image-shape .label,#mermaid-svg-1wAJWUEWFfCiR3sS .icon-shape .label{text-align:center;}#mermaid-svg-1wAJWUEWFfCiR3sS .node.clickable{cursor:pointer;}#mermaid-svg-1wAJWUEWFfCiR3sS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-1wAJWUEWFfCiR3sS .arrowheadPath{fill:#333333;}#mermaid-svg-1wAJWUEWFfCiR3sS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-1wAJWUEWFfCiR3sS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-1wAJWUEWFfCiR3sS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1wAJWUEWFfCiR3sS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-1wAJWUEWFfCiR3sS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1wAJWUEWFfCiR3sS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-1wAJWUEWFfCiR3sS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-1wAJWUEWFfCiR3sS .cluster text{fill:#333;}#mermaid-svg-1wAJWUEWFfCiR3sS .cluster span{color:#333;}#mermaid-svg-1wAJWUEWFfCiR3sS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-1wAJWUEWFfCiR3sS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-1wAJWUEWFfCiR3sS rect.text{fill:none;stroke-width:0;}#mermaid-svg-1wAJWUEWFfCiR3sS .icon-shape,#mermaid-svg-1wAJWUEWFfCiR3sS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-1wAJWUEWFfCiR3sS .icon-shape p,#mermaid-svg-1wAJWUEWFfCiR3sS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-1wAJWUEWFfCiR3sS .icon-shape rect,#mermaid-svg-1wAJWUEWFfCiR3sS .image-shape rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-1wAJWUEWFfCiR3sS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-1wAJWUEWFfCiR3sS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-1wAJWUEWFfCiR3sS :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}#mermaid-svg-1wAJWUEWFfCiR3sS .default>*{fill:#2c3e50!important;color:#ffffff!important;font-size:14px!important;}#mermaid-svg-1wAJWUEWFfCiR3sS .default span{fill:#2c3e50!important;color:#ffffff!important;font-size:14px!important;}#mermaid-svg-1wAJWUEWFfCiR3sS .default tspan{fill:#ffffff!important;}

    智能手表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开发。
  • 赞(0)
    未经允许不得转载:171主机测评 » STM32实战:基于LVGL的嵌入式GUI界面开发(智能手表UI)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址