矩阵按键实现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内置长按重复机制,电平触发配合该机制自然实现了连续滑动。无需额外代码。
九、最终效果
-
上电后,四个按钮垂直排列,底部一个滑块,第一个按钮获得焦点。
-
按 上/下 键:焦点在按钮之间移动。
-
按 左/右 键:滑块值减小/增加,长按时连续滑动。
-
按 确认 键:可触发按钮的点击事件(用户可添加回调实现具体功能)。
十、经验总结
-
电平触发 vs 边沿触发:在轮询式输入设备驱动中,电平触发更可靠,能避免事件丢失;但需配合去重逻辑,否则会导致重复触发。
-
组(Group)是按键导航的核心:必须将输入设备绑定到组,并将所有需要焦点的控件加入该组。
-
主循环保持简洁 :尽量避免在LVGL主循环中做过多耗时操作,保证
lv_timer_handler()能及时调用。 -
时钟节拍不可缺 :
lv_tick_inc必须周期性调用,否则长按重复、动画等依赖时间的功能会失效。 -
分模块验证:先裸机测试LCD和按键,再集成LVGL,便于快速定位问题
十一、扩展建议
-
为按钮添加
LV_EVENT_CLICKED回调,实现具体业务逻辑(如界面跳转、参数设置)。 -
增加更多LVGL控件(下拉列表、图表、进度条等),丰富人机交互。
-
优化按键扫描,支持组合键或更复杂的输入序列。
本次移植基于STM32F407VET6和1.8寸TFT LCD,成功实现了LVGL图形界面与矩阵按键的完整交互。代码结构清晰,可作为嵌入式GUI开发的参考模板。
文中所有代码均来自实际项目,已在硬件上验证通过。欢迎交流讨论。