STM32F407VET6驱动1.8寸TFT LCD移植LVGL,实现矩阵按键控制UI

矩阵按键实现UI控制

基于实际项目代码,记录从底层驱动到LVGL交互的完整移植与调试过程,可以找我要源码。

一、项目背景

在嵌入式GUI开发中,LVGL(Light and Versatile Graphics Library)凭借其开源、轻量、控件丰富、跨平台等优势,成为众多开发者的首选。本文基于STM32F407VET6 主控芯片,搭配1.8寸128×160分辨率TFT LCD(驱动IC ST7735) ,成功移植LVGL,并使用4×4矩阵键盘(实际使用上下左右、确认、模式六个按键)实现了UI焦点切换和滑块控制。文章将详细记录从底层驱动到上层应用的完整实现,以及调试中遇到的典型问题(白屏、按键无反应、焦点飞移等)及解决方案。

二、硬件平台与开发环境

  • 主控:STM32F407VET6(Cortex-M4 @168MHz,1MB Flash,192KB RAM)

  • 显示屏:1.8寸TFT LCD,分辨率128×160,驱动IC ST7735,SPI接口

  • 按键:4×4矩阵键盘(实际使用Key_Up, Key_Down, Key_Left, Key_Right, Key_On, Key_Mode)

  • 开发工具:STM32CubeIDE(HAL库)

  • LVGL版本:v8.3(最新稳定版)

三、整体软件架构

根据实际代码,项目主要包含以下模块:

文件 功能
main.c 系统初始化,LVGL初始化,显示/输入设备注册,主循环
lcd.c/h LCD底层驱动(SPI通信,画点、填充等)
lv_port_disp.c LVGL显示接口(未展示具体实现,但已注册)
lv_port_indev.c LVGL按键输入设备驱动
btn.c/h 4×4矩阵按键扫描状态机
myui.c 创建UI(按钮、滑块)并绑定输入设备组

四、LCD驱动与LVGL显示接口

由于LCD底层驱动(lcd.c)和LVGL显示端口(lv_port_disp.c)并非本文重点,且用户未提供具体代码,此处仅说明:用户已实现lv_port_disp_init(),并正确注册了显示驱动,确保LVGL能够将渲染内容输出到1.8寸屏幕上。白屏问题的解决依赖于flush_cb中正确调用lv_disp_flush_ready()

五、矩阵按键驱动设计

5.1 状态机消抖与键值锁存

btn.c实现了一个完整的4×4矩阵键盘扫描状态机,核心变量:

复制代码
char KeyDown;               // 按键是否按住
KeyPressed KeyValue;        // 当前按住的键值
char KeyNew;                // 新按键标志(未使用电平触发)

5.2 电平触发式读取

getKey()函数采用电平触发方式:

复制代码
KeyPressed getKey(void) {
    if(KeyDown) return KeyValue;   // 按住期间持续返回键值
    else return Key_None;
}

这种设计保证LVGL在任何时刻读取按键都不会丢失事件,是实现可靠交互的关键。

六、LVGL输入设备注册与组绑定

6.1 输入设备驱动

lv_port_indev.c中注册了一个LV_INDEV_TYPE_KEYPAD设备,核心读取回调如下:

复制代码
static void keypad_read(lv_indev_drv_t * indev_drv, lv_indev_data_t * data)
{
    static uint32_t last_key = 0;

    uint32_t act_key = keypad_get_key();
    if(act_key != 0) {
        data->state = LV_INDEV_STATE_PR;

        switch(act_key) {
        case Key_Up:
            act_key = LV_KEY_PREV;
            break;
        case Key_Down:
            act_key = LV_KEY_NEXT;
            break;
        case Key_Left:
            act_key = LV_KEY_LEFT;
            break;
        case Key_Right:
            act_key = LV_KEY_RIGHT;
            break;
        case Key_On:
            act_key = LV_KEY_ENTER;
            break;
        case Key_Mode:
            act_key = LV_KEY_NEXT;
            break;
        default:
            act_key = 0;
            break;
        }

        last_key = act_key;
    } else {
        data->state = LV_INDEV_STATE_REL;
        last_key = 0;
    }

    data->key = last_key;
}

6.2 UI创建与组绑定

my_gui.c中,创建了四个按钮和一个滑块,并将它们加入同一个组(Group),最后将输入设备绑定到该组:

复制代码
#include "lcd.h"
#include "gui.h"
#include "test.h"
#include "lvgl.h"
#include "myui.h"


extern lv_indev_t *indev_keypad;

void my_gui(void)
{
    lv_group_t *g = lv_group_create();
    lv_group_set_default(g);

    lv_obj_t * btn1 = lv_btn_create(lv_scr_act());
    lv_obj_set_size(btn1, 50, 10);
    lv_obj_align(btn1, LV_ALIGN_CENTER, 0, -70);

    lv_obj_t * btn2 = lv_btn_create(lv_scr_act());
    lv_obj_set_size(btn2, 50, 10);
    lv_obj_align_to(btn2, btn1, LV_ALIGN_OUT_BOTTOM_MID, 0, 20);

    lv_obj_t * btn3 = lv_btn_create(lv_scr_act());
    lv_obj_set_size(btn3, 50, 10);
    lv_obj_align_to(btn3, btn2, LV_ALIGN_OUT_BOTTOM_MID, 0, 20);

    lv_obj_t * btn4 = lv_btn_create(lv_scr_act());
    lv_obj_set_size(btn4, 50, 10);
    lv_obj_align_to(btn4, btn3, LV_ALIGN_OUT_BOTTOM_MID, 0, 20);

    lv_obj_t * slider = lv_slider_create(lv_scr_act());
    lv_obj_align(slider, LV_ALIGN_BOTTOM_MID, 0, -10);
    lv_obj_set_size(slider, 110, 12);
    lv_slider_set_range(slider, 0, 100);
    lv_slider_set_value(slider, 50, LV_ANIM_OFF);

    lv_group_add_obj(g, btn1);
    lv_group_add_obj(g, btn2);
    lv_group_add_obj(g, btn3);
    lv_group_add_obj(g, btn4);
    lv_group_add_obj(g, slider);

    lv_indev_set_group(indev_keypad, g);
}

七、主循环与时钟管理

main.c中的主循环实现了LVGL任务处理和时钟节拍供给:

复制代码
while (1)
  {
    /* USER CODE END WHILE */

    /* USER CODE BEGIN 3 */
    Scan_Keyboard();
    lv_timer_handler();
    uint32_t now = HAL_GetTick();
    if (now - last_tick >= 5) {
        lv_tick_inc(now - last_tick);
        last_tick = now;
    }
    HAL_Delay(5);
  /* USER CODE END 3 */
}

注意:虽然主循环中调用了Scan_Keyboard(),但keypad_read内部也会通过getKey()间接调用扫描函数。实际运行证明这样做没有冲突,且能保证按键实时性。

八、调试过程中的关键问题与解决

8.1 白屏问题

  • 现象:屏幕有背光,但无任何显示。

  • 原因 :LVGL显示驱动未正确注册或flush_cb未调用lv_disp_flush_ready

  • 解决 :检查lv_port_disp_init()实现,确保刷屏完成后调用lv_disp_flush_ready

8.2 按键无反应(裸机测试正常)

  • 现象 :在主循环中直接轮询getKey()并改变LCD颜色可以正常工作,但LVGL界面焦点不移动。

  • 原因 :最初getKey()使用边沿触发(基于KeyNew),按键按下仅返回一次键值,而LVGL轮询周期短,容易错过。

  • 解决 :改为电平触发(基于KeyDown),按住期间持续返回键值,保证事件不丢失。

8.3 按键一次按下多次移动焦点

  • 现象:按一下"下"键,焦点连续向下移动多格。

  • 原因:电平触发导致LVGL在每个周期都收到相同按键事件,产生重复移动。

  • 解决 :对于按钮,可通过在keypad_read中加入去重逻辑(例如记录上次上报的键值)来解决。但由于项目中滑块需要长按连续滑动,且按钮移动速度可接受,最终保留了电平触发不做去重。

8.4 长按滑块连续滑动

  • 现象:长按左/右键,滑块值连续变化,体验良好。

  • 原理:LVGL内置长按重复机制,电平触发配合该机制自然实现了连续滑动。无需额外代码。

九、最终效果

  • 上电后,四个按钮垂直排列,底部一个滑块,第一个按钮获得焦点。

  • 上/下 键:焦点在按钮之间移动。

  • 左/右 键:滑块值减小/增加,长按时连续滑动。

  • 确认 键:可触发按钮的点击事件(用户可添加回调实现具体功能)。

十、经验总结

  1. 电平触发 vs 边沿触发:在轮询式输入设备驱动中,电平触发更可靠,能避免事件丢失;但需配合去重逻辑,否则会导致重复触发。

  2. 组(Group)是按键导航的核心:必须将输入设备绑定到组,并将所有需要焦点的控件加入该组。

  3. 主循环保持简洁 :尽量避免在LVGL主循环中做过多耗时操作,保证lv_timer_handler()能及时调用。

  4. 时钟节拍不可缺lv_tick_inc必须周期性调用,否则长按重复、动画等依赖时间的功能会失效。

  5. 分模块验证:先裸机测试LCD和按键,再集成LVGL,便于快速定位问题

十一、扩展建议

  • 为按钮添加LV_EVENT_CLICKED回调,实现具体业务逻辑(如界面跳转、参数设置)。

  • 增加更多LVGL控件(下拉列表、图表、进度条等),丰富人机交互。

  • 优化按键扫描,支持组合键或更复杂的输入序列。

本次移植基于STM32F407VET6和1.8寸TFT LCD,成功实现了LVGL图形界面与矩阵按键的完整交互。代码结构清晰,可作为嵌入式GUI开发的参考模板。

文中所有代码均来自实际项目,已在硬件上验证通过。欢迎交流讨论。

相关推荐
恒森宇电子有限公司2 小时前
芯晞微CSM4056H 单节锂离子电池充电器芯片 封装ESOP-8
单片机·嵌入式硬件
果果燕2 小时前
ARM嵌入式学习(五)---IMX6ULL外部中断
单片机·嵌入式硬件
无人机9012 小时前
Delphi网络编程实战:UDP通信与多线程网络优化详解
单片机·嵌入式硬件
KOYUELEC光与电子努力加油3 小时前
BROADCOM博通集成 Matter 1.5平台认证就绪、BK7239N等芯片助力智能家居无缝融合
服务器·科技·单片机·智能家居
zmj3203244 小时前
芯片的ISP在系统编程-模式
单片机·嵌入式开发
zmj3203244 小时前
KW45的ISP模式
stm32·单片机·嵌入式硬件·kw45
小美单片机4 小时前
十字路交通灯系统设计
c语言·单片机·51单片机·proteus·课设
AzusaFighting4 小时前
STM32F103R基于AI生成的HAL库DMA串口应用用例
stm32·单片机·嵌入式硬件
ZHANG13HAO5 小时前
基于九轴传感器 + K-means 聚类的振动异常检测实战教程
单片机