前言
在日常的开发中,涉及到用户显示界面控制的时候,如何快速、优美的设计一款用户UI,是攻城狮应该具备的能力(择偶优先权),如何把嵌入式UI设计像高级语言图形化设计一样简便、快捷,今天重磅介绍一款GUI设计开源库,LittleVgl,俗称LVGL,来吧,直接上干货,淦!
硬件环境
- STM32F407ZGT6(或者其他板子)
- 2.4寸TFT电阻式触摸屏
软件环境
- keil5
- lvgl软件源码
- 正点原子触摸屏例程
- 注:以上只是小飞哥使用的环境,其他也可以的,重要的是这种方法~
LVGL开源库是什么?
欢迎大家查看这篇文章,对LVGL有详细的介绍
移植过程是相对不复杂的,主要是添加文件到STM32工程,将LVGL源码添加进来,源码部分就没有什么多说的啦,所有的基本绘图函数都在其中,用户UI代码主要是用户自己设计的显示代码,可以导入一些官方例程之类的,也不多做介绍
LVGL与嵌入式平台显示接口
我们本次主要关注lv_port_disp.c和lv_port_indev.c,学习如何在LVGL开源库和嵌入式平台之间搭建一条桥梁,让两者联系起来,在lv_port_disp.c中调用打点函数或者画图形函数即可,小飞哥调用的是打点函数,显示部分只需要调用这一点即可。
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
//快速画点
//x,y:坐标
//color:颜色
void LCD_Fast_DrawPoint(u16 x, u16 y, u32 color)
{
if (lcddev.id == 0X9341 || lcddev.id == 0X5310)
{
LCD_WR_REG(lcddev.setxcmd);
LCD_WR_DATA(x >> 8);
LCD_WR_DATA(x & 0XFF);
LCD_WR_REG(lcddev.setycmd);
LCD_WR_DATA(y >> 8);
LCD_WR_DATA(y & 0XFF);
}
else if (lcddev.id == 0X5510)
{
LCD_WR_REG(lcddev.setxcmd);
LCD_WR_DATA(x >> 8);
LCD_WR_REG(lcddev.setxcmd + 1);
LCD_WR_DATA(x & 0XFF);
LCD_WR_REG(lcddev.setycmd);
LCD_WR_DATA(y >> 8);
LCD_WR_REG(lcddev.setycmd + 1);
LCD_WR_DATA(y & 0XFF);
}
else if (lcddev.id == 0X1963)
{
if (lcddev.dir == 0)
x = lcddev.width - 1 - x;
LCD_WR_REG(lcddev.setxcmd);
LCD_WR_DATA(x >> 8);
LCD_WR_DATA(x & 0XFF);
LCD_WR_DATA(x >> 8);
LCD_WR_DATA(x & 0XFF);
LCD_WR_REG(lcddev.setycmd);
LCD_WR_DATA(y >> 8);
LCD_WR_DATA(y & 0XFF);
LCD_WR_DATA(y >> 8);
LCD_WR_DATA(y & 0XFF);
}
LCD->LCD_REG = lcddev.wramcmd;
LCD->LCD_RAM = color;
}
void LCD_DrawFRONT_COLOR(u16 x, u16 y, u16 color)
{
LCD_Fast_DrawPoint(x,y,color);
}
/* Flush the content of the internal buffer the specific area on the display
* You can use DMA or any hardware acceleration to do this operation in the background but
* 'lv_disp_flush_ready()' has to be called when finished. */
static void disp_flush(lv_disp_drv_t * disp_drv, const lv_area_t * area, lv_color_t * color_p)
{
/*The most simple case (but also the slowest) to put all pixels to the screen one-by-one*/
int32_t x;
int32_t y;
for(y = area->y1; y <= area->y2; y++) {
for(x = area->x1; x <= area->x2; x++) {
/* Put a pixel to the display. For example: */
LCD_DrawFRONT_COLOR(x, y, color_p->full);
color_p++;
}
}
/* IMPORTANT!!!
* Inform the graphics library that you are ready with the flushing*/
lv_disp_flush_ready(disp_drv);
}
LVGL与嵌入式平台控制接口
然后是触摸屏控制部分,本文只介绍如何实现触摸屏结合LVGL控制,完成代替物理按键控制,关于触摸屏的驱动,可以查看原子哥的详细介绍,就不做赘述了。
lv_port_indev.c源文件中,我们可以看到定义了很多的接口,鼠标的,按键的,触摸的,等等,此次我们只用到触摸部分,我只保留了触摸的代码,其他的都删除了(主要是懒,删除最快捷),一下是修改后的代码,触摸部分的代码,只需要把原子哥的电阻屏驱动代码copy进来即可。
其实,lvgl的框架是不错的,这里为了省事,是直接改掉了tuoch_read函数,其他的获取坐标、按下确认等都没有使用,建议小伙伴们可以使用下完整的框架试试。
/* Will be called by the library to read the touchpad */
static bool touchpad_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *data)
{
static lv_coord_t last_x = 0;
static lv_coord_t last_y = 0;
tp_dev.scan(0);
if (tp_dev.sta & TP_PRES_DOWN) //
{
printf("x坐标:%d,Y坐标:%d\r\n", tp_dev.x[0], tp_dev.y[0]);
last_x = tp_dev.x[0];
last_y = tp_dev.y[0];
data->point.x = last_x;
data->point.y = last_y;
data->state = LV_INDEV_STATE_PR;
}
else
{
data->point.x = last_x;
data->point.y = last_y;
data->state = LV_INDEV_STATE_REL;
}
// /*Save the pressed coordinates and the state*/
// if(touchpad_is_pressed()) {
// touchpad_get_xy(&last_x, &last_y);
// data->state = LV_INDEV_STATE_PR;
// } else {
// data->state = LV_INDEV_STATE_REL;
// }
// /*Set the last pressed coordinates*/
// data->point.x = last_x;
// data->point.y = last_y;
/*Return `false` because we are not buffering and no more data to read*/
return false;
}
/**
* @file lv_port_indev_templ.c
*
*/
/*Copy this file as "lv_port_indev.c" and set this value to "1" to enable content*/
#if 1
/*********************
* INCLUDES
*********************/
#include "lv_port_indev.h"
#include "lv_hal_indev.h"
#include "touch.h"
/*********************
* DEFINES
*********************/
extern _m_tp_dev tp_dev; //锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷锟斤拷touch.c锟斤拷锟芥定锟斤拷
/**********************
* TYPEDEFS
**********************/
/**********************
* STATIC PROTOTYPES
**********************/
static void touchpad_init(void);
static bool touchpad_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *data);
static bool touchpad_is_pressed(void);
static void touchpad_get_xy(lv_coord_t *x, lv_coord_t *y);
/**********************
* STATIC VARIABLES
**********************/
lv_indev_t *indev_touchpad;
/**********************
* MACROS
**********************/
/**********************
* GLOBAL FUNCTIONS
**********************/
void lv_port_indev_init(void)
{
/* Here you will find example implementation of input devices supported by LittelvGL:
* - Touchpad
* - Mouse (with cursor support)
* - Keypad (supports GUI usage only with key)
* - Encoder (supports GUI usage only with: left, right, push)
* - Button (external buttons to press points on the screen)
*
* The `..._read()` function are only examples.
* You should shape them according to your hardware
*/
lv_indev_drv_t indev_drv;
/*------------------
* Touchpad
* -----------------*/
/*Initialize your touchpad if you have*/
touchpad_init();
/*Register a touchpad input device*/
lv_indev_drv_init(&indev_drv);
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = touchpad_read;
indev_touchpad = lv_indev_drv_register(&indev_drv);
}
/**********************
* STATIC FUNCTIONS
**********************/
/*------------------
* Touchpad
* -----------------*/
/*Initialize your touchpad*/
static void touchpad_init(void)
{
/*Your code comes here*/
}
/* Will be called by the library to read the touchpad */
static bool touchpad_read(lv_indev_drv_t *indev_drv, lv_indev_data_t *data)
{
static lv_coord_t last_x = 0;
static lv_coord_t last_y = 0;
tp_dev.scan(0);
if (tp_dev.sta & TP_PRES_DOWN) //
{
printf("x坐标:%d,Y坐标:%d\r\n", tp_dev.x[0], tp_dev.y[0]);
last_x = tp_dev.x[0];
last_y = tp_dev.y[0];
data->point.x = last_x;
data->point.y = last_y;
data->state = LV_INDEV_STATE_PR;
}
else
{
data->point.x = last_x;
data->point.y = last_y;
data->state = LV_INDEV_STATE_REL;
}
// /*Save the pressed coordinates and the state*/
// if(touchpad_is_pressed()) {
// touchpad_get_xy(&last_x, &last_y);
// data->state = LV_INDEV_STATE_PR;
// } else {
// data->state = LV_INDEV_STATE_REL;
// }
// /*Set the last pressed coordinates*/
// data->point.x = last_x;
// data->point.y = last_y;
/*Return `false` because we are not buffering and no more data to read*/
return false;
}
/*Return true is the touchpad is pressed*/
static bool touchpad_is_pressed(void)
{
/*Your code comes here*/
return false;
}
/*Get the x and y coordinates if the touchpad is pressed*/
static void touchpad_get_xy(lv_coord_t *x, lv_coord_t *y)
{
/*Your code comes here*/
(*x) = 0;
(*y) = 0;
}
#else /* Enable this file at the top */
/* This dummy typedef exists purely to silence -Wpedantic. */
typedef int keep_pedantic_happy;
#endif
LVGL配置文件
开发过程中,屏幕尺寸一般是根据外壳结构,产品形态等决定的,但是一般不会更换驱动芯片,所以对开发者来讲,底层驱动是不变的,但是随着屏幕尺寸的改变,苦逼的是还要一一修改尺寸,我们可以通过配置文件,对不同的屏幕做适配,可以参考LVGL官方配置文件,这里可以更改屏幕尺寸,配置文件里面还有很多配置项,很像我们做上位机界面时候的属性,通过修改不同的值,得到我们想要的效果,小伙伴们可以探索探索哈,实践出真知~
实例演示
群里有小伙伴最近有些疑惑,想使用slider做控制,不知道如何是用触摸屏,对如何如何获取触摸值,通过slider传输值目标器件,小飞哥一听,必须安排,粉丝的事无大小,干!!!
我们直接使用官方库,slider例程,例程是有两种slider,我使用的是第二种,添加到我们的工程中即可,需要再配置文件中开启方框中的选项。
void lv_ex_slider_2(void)
{
/* Create a slider in the center of the display */
lv_obj_t * slider = lv_slider_create(lv_scr_act(), NULL);
lv_obj_set_width(slider, LV_DPI * 2);
lv_obj_align(slider, NULL, LV_ALIGN_CENTER, 0, 0);
lv_obj_set_event_cb(slider, slider_event_cb);
lv_slider_set_range(slider, 0, 100); //修改此处,可以修改slider数值范围,目前是0-100
/* Create a label below the slider */
slider_label = lv_label_create(lv_scr_act(), NULL);
lv_label_set_text(slider_label, "0");
lv_obj_set_auto_realign(slider_label, true);
lv_obj_align(slider_label, slider, LV_ALIGN_OUT_BOTTOM_MID, 0, 10);
/* Create an informative label */
lv_obj_t * info = lv_label_create(lv_scr_act(), NULL);
lv_label_set_text(info, "Welcome to join in Embeded-Party\n");
lv_obj_align(info, NULL, LV_ALIGN_IN_TOP_LEFT, 10, 10);
}
上面顺利的话,此时我们的触摸屏驱动是OK的,无论按在哪一处,都是有坐标值的,来测试下,触摸任意位置时候,获取到的坐标值,可以发现,我们无论按在哪一位置,总是有坐标的,但是,显然是有很多我们不需要的值,那么如何处理呢?
对于slider来说,我们只需要进度条区域内的值作为我们输出的对象,这里就可以体会到LVGL的乐趣了,如何把坐标值转换为slider对应的数值,我们不需要关心如何实现的,直接按就完事了,黑盒子会帮我们实现,我们只需要在slider回调函数里面处理即可:
static void slider_event_cb(lv_obj_t * slider, lv_event_t event)
{
int16_t barvalue = 0;
if(event == LV_EVENT_VALUE_CHANGED) {
static char buf[4]; /* max 3 bytes for number plus 1 null terminating byte */
barvalue = lv_slider_get_value(slider);
printf("barvlue is %d\r\n",barvalue);
snprintf(buf, 4, "%u", lv_slider_get_value(slider));
lv_label_set_text(slider_label, buf);
}
}
main函数部分
int main(void)
{
HAL_Init(); //初始化HAL库
Stm32_Clock_Init(336, 8, 2, 7); //设置时钟,168Mhz
delay_init(168); //初始化延时函数
uart_init(115200); //初始化USART
LED_Init(); //初始化LED
KEY_Init(); //初始化KEY
LCD_Init(); //初始化LCD
tp_dev.init(); //触摸屏初始化
lv_init(); // 初始化lvgl
lv_port_disp_init(); // 显示初始化
lv_port_indev_init();
//lv_demo_widgets(); // 例子演示
lv_ex_slider_2();
//lv_ex_calendar_1();
//lv_demo_keypad_encoder();
// lv_demo_printer();
KEY_Init(); //初始化按键
TIM3_Init(1000 - 1, 84 - 1); //定时器3初始化,定时器时钟为84M,分频系数为8400-1,
//所以定时器3的频率为84M/8400=10K,自动重装载为100-1,那么定时器周期就是10ms
while (1)
{
lv_task_handler(); // 循环调用处理lvgl tasks
}
}
对比上面,小伙伴们是不是发现什么不同了呢,当我们触摸位置在slider有效区域内的时候,坐标值,坐标值是 已经转换为0-100内的数值了的,是不是很方便呢。
本次的介绍就到这里啦,后面有更精彩的内容,拥有自己的GUI,欢迎大家持续关注嵌入式实验基地,来这里还可以学习HAL库+cubemx的更多精彩内容哦!