“LittlevGL is a free and open-source graphics library providing everything you need to create embedded GUI with easy-to-use graphical elements, beautiful visual effects and low memory footprint.” -- https://littlevgl.com/#。
LittlevGL的移植工作可以说是异常简单,开发人员的工作做得很到位。具体的移植步骤和简易教程可以参考github上的LittlevGL工程的说明文档:https://github.com/littlevgl/lvgl。下面简单说明LittlevGL的移植步骤(参考官方文档):
1、下载(克隆)LittlevGL的源码到自己的工程目录下的lvgl目录。
2、将 lvgl/lv_conf_template.h 文件复制一份为 lv_conf.h 文件与 lvgl 目录并列存放,该文件用于配置LittlevGL,至少需要设置其中的LV_HOR_RES_MAX(水平分辨率)
、LV_VER_RES_MAX(垂直分辨率)
和LV_COLOR_DEPTH(像素深度)三个参数
。
3、在工程中包含 lvgl/lvgl.h ,该头文件中包含了使用LittlevGL所需要的函数定义。
4、每过 x 毫秒调用 lv_tick_inc(x) 函数一次(1 ≤ x ≤ 10),这个函数是LittlevGL运行所需的时钟源。如果定义LV_TICK_CUSTOM为1的话,就无须在应用程序中主动调用 lv_tick_inc(x) 函数,而是需要定义一个获取当前系统已运行时间的函数(例如uint32_t custom_tick_get(void);)并使用宏定义LV_TICK_CUSTOM_SYS_TIME_EXPR表示该函数,这个函数会在调用 lv_task_handler() 函数的时候自动调用并获取当前时间戳。
5、调用 lv_init() 函数初始化LittlevGL。
6、创建一个显示缓存区,该缓存区用于给LittlevGL进行绘制
:
static lv_disp_buf_t disp_buf;
static lv_color_t buf[LV_HOR_RES_MAX * 10]; /*Declare a buffer for 10 lines*/
lv_disp_buf_init(&disp_buf, buf, NULL, LV_HOR_RES_MAX * 10); /*Initialize the display buffer*/
7、创建并注册一个显示器驱动,这个驱动被注册到LittlevGL之后,LittlevGL就可以向该显示器输出数据:
lv_disp_drv_t disp_drv; /*Descriptor of a display driver*/
lv_disp_drv_init(&disp_drv); /*Basic initialization*/
disp_drv.flush_cb = my_disp_flush; /*Set your driver function*/
disp_drv.buffer = &disp_buf; /*Assign the buffer to the display*/
lv_disp_drv_register(&disp_drv); /*Finally register the driver*/
其中my_disp_flush函数是用于向显示器中写入数据的底层函数,在LittlevGL中,关于显示器的接口函数就这么一个,我们将这个函数实现得尽可能的高效率:
void my_disp_flush(lv_disp_t * disp, const lv_area_t * area, lv_color_t * color_p)
{
int32_t x, y;
for(y = area->y1; y <= area->y2; y++) {
for(x = area->x1; x <= area->x2; x++) {
set_pixel(x, y, *color_p); /* Put a pixel to the display.*/
color_p++;
}
}
lv_disp_flush_ready(disp); /* Indicate you are ready with the flushing*/
}
8、创建并注册输入设备(鼠标、键盘、触屏等),例如创建并注册一个触摸板的驱动:
lv_indev_drv_init(&indev_drv); /*Descriptor of a input device driver*/
indev_drv.type = LV_INDEV_TYPE_POINTER; /*Touch pad is a pointer-like device*/
indev_drv.read_cb = my_touchpad_read; /*Set your driver function*/
lv_indev_drv_register(&indev_drv); /*Finally register the driver*/
其中my_touchpad_read则是触摸板的底层操作函数,例如:
bool my_touchpad_read(lv_indev_t * indev, lv_indev_data_t * data)
{
static lv_coord_t last_x = 0;
static lv_coord_t last_y = 0;
/*Save the state and save the pressed coordinate*/
data->state = touchpad_is_pressed() ? LV_INDEV_STATE_PR : LV_INDEV_STATE_REL;
if(data->state == LV_INDEV_STATE_PR) touchpad_get_xy(&last_x, &last_y);
/*Set the coordinates (if released use the last pressed coordinates)*/
data->point.x = last_x;
data->point.y = last_y;
return false; /*Return `false` because we are not buffering and no more data to read*/
}
9、周期性调用 lv_task_handler() 函数,周期间隔为若干毫秒,在定时器中断或者一个线程中调用。这个函数会在适当的时候重绘屏幕以及读取输入设备的数据。
现在手头没有现成的可以一直LittlevGL得板子,但是LittlevGL官方给出的移植例程中有基于Linux系统下的FrameBuffer的移植例程:https://blog.littlevgl.com/2018-01-03/linux_fb。我们正好可以使用Ubuntu虚拟机对这个例程进行测试,从github上下载这个例程的源码:https://blog.littlevgl.com/2018-01-03/linux_fb,注意其中lv_drivers、lv_examples和lvgl是单独的git项目,需要单独下载。获取到完整的源码之后,在Linux下进行编译,直接执行make命令即可,得到一个 demo 可执行文件。先不要急着执行,先在Ubuntu下切换到字符终端模式(Ctrl + Alt + F1 and service lightdm stop
),然后执行demo文件:
执行效果直接出来了,就是这么简单。但是这个执行的效果和官网上的有一些区别,我们的demo执行结果中Write栏目中的键盘没有显示,下面查看demo工程的源码进行进一步的学习。
既然是基于Linux下的FrameBuffer移植测试的,我们需要先搞定Linux的FrameBuffer的驱动,下面是一个典型的Linux下FrameBuffer操作例程:
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
#include <fcntl.h>
#include <linux/fb.h>
#include <sys/mman.h>
#include <sys/ioctl.h>
int main()
{
int fbfd = 0;
struct fb_var_screeninfo vinfo;
struct fb_fix_screeninfo finfo;
long int screensize = 0;
char *fbp = 0;
int x = 0, y = 0;
long int location = 0;
// Open the file for reading and writing
fbfd = open("/dev/fb0", O_RDWR);
if (fbfd == -1) {
perror("Error: cannot open framebuffer device");
exit(1);
}
printf("The framebuffer device was opened successfully.\n");
// Get fixed screen information
if (ioctl(fbfd, FBIOGET_FSCREENINFO, &finfo) == -1) {
perror("Error reading fixed information");
exit(2);
}
// Get variable screen information
if (ioctl(fbfd, FBIOGET_VSCREENINFO, &vinfo) == -1) {
perror("Error reading variable information");
exit(3);
}
printf("%dx%d, %dbpp\n", vinfo.xres, vinfo.yres, vinfo.bits_per_pixel);
// Figure out the size of the screen in bytes
screensize = vinfo.xres * vinfo.yres * vinfo.bits_per_pixel / 8;
// Map the device to memory
fbp = (char *)mmap(0, screensize, PROT_READ | PROT_WRITE, MAP_SHARED, fbfd, 0);
if ((int)fbp == -1) {
perror("Error: failed to map framebuffer device to memory");
exit(4);
}
printf("The framebuffer device was mapped to memory successfully.\n");
x = 0; y = 0; // Where we are going to put the pixel
// Figure out where in memory to put the pixel
for (y = 0; y < vinfo.yres; y++)
for (x = 0; x < vinfo.xres; x++) {
location = (x+vinfo.xoffset) * (vinfo.bits_per_pixel/8) +
(y+vinfo.yoffset) * finfo.line_length;
*((unsigned int*)(fbp + location)) = 0xE81123;
}
for (y = 0; y < vinfo.yres / 2; y++)
for (x = 0; x < vinfo.xres / 2; x++) {
location = (x+vinfo.xoffset) * (vinfo.bits_per_pixel/8) +
(y+vinfo.yoffset) * finfo.line_length;
*((unsigned int*)(fbp + location)) = 0x746283;
}
munmap(fbp, screensize);
close(fbfd);
return 0;
}
该程序的执行效果如下:
红色填充满了全部的显示区域,灰色填充了1/2的宽度和1/2的高度的区域。现在我们拥有一个基于FrameBuffer的显示器驱动了。下面可以看看demo例程的源码了。main文件内容如下:
#include "lvgl/lvgl.h"
#include "lv_drivers/display/fbdev.h"
#include "lv_examples/lv_apps/demo/demo.h"
#include <unistd.h>
#include <pthread.h>
#include <time.h>
#include <sys/time.h>
#define DISP_BUF_SIZE (80*LV_HOR_RES_MAX)
int main(void)
{
/*LittlevGL init*/
lv_init();
/*Linux frame buffer device init*/
fbdev_init();
/*A small buffer for LittlevGL to draw the screen's content*/
static lv_color_t buf[DISP_BUF_SIZE];
/*Initialize a descriptor for the buffer*/
static lv_disp_buf_t disp_buf;
lv_disp_buf_init(&disp_buf, buf, NULL, DISP_BUF_SIZE);
/*Initialize and register a display driver*/
lv_disp_drv_t disp_drv;
lv_disp_drv_init(&disp_drv);
disp_drv.buffer = &disp_buf;
disp_drv.flush_cb = fbdev_flush;
lv_disp_drv_register(&disp_drv);
/*Create a Demo*/
demo_create();
/*Handle LitlevGL tasks (tickless mode)*/
while(1) {
lv_task_handler();
usleep(5000);
}
return 0;
}
/*Set in lv_conf.h as `LV_TICK_CUSTOM_SYS_TIME_EXPR`*/
uint32_t custom_tick_get(void)
{
static uint64_t start_ms = 0;
if(start_ms == 0) {
struct timeval tv_start;
gettimeofday(&tv_start, NULL);
start_ms = (tv_start.tv_sec * 1000000 + tv_start.tv_usec) / 1000;
}
struct timeval tv_now;
gettimeofday(&tv_now, NULL);
uint64_t now_ms;
now_ms = (tv_now.tv_sec * 1000000 + tv_now.tv_usec) / 1000;
uint32_t time_ms = now_ms - start_ms;
return time_ms;
}
该demo高度贴合前面说明的LittlevGL的移植步骤。其中 fbdev_init() 函数用于初始化系统的FrameBuffer显示驱动,和我们的FrameBuffer测试程序是相似的,然后还添加了一个 fbdev_flush() 函数,这个函数就是显示器驱动的底层绘制函数:
/**
* Flush a buffer to the marked area
* @param drv pointer to driver where this function belongs
* @param area an area where to copy `color_p`
* @param color_p an array of pixel to copy to the `area` part of the screen
*/
void fbdev_flush(lv_disp_drv_t * drv, const lv_area_t * area, lv_color_t * color_p)
{
if(fbp == NULL ||
area->x2 < 0 ||
area->y2 < 0 ||
area->x1 > (int32_t)vinfo.xres - 1 ||
area->y1 > (int32_t)vinfo.yres - 1) {
lv_disp_flush_ready(drv);
return;
}
/*Truncate the area to the screen*/
int32_t act_x1 = area->x1 < 0 ? 0 : area->x1;
int32_t act_y1 = area->y1 < 0 ? 0 : area->y1;
int32_t act_x2 = area->x2 > (int32_t)vinfo.xres - 1 ? (int32_t)vinfo.xres - 1 : area->x2;
int32_t act_y2 = area->y2 > (int32_t)vinfo.yres - 1 ? (int32_t)vinfo.yres - 1 : area->y2;
lv_coord_t w = lv_area_get_width(area);
long int location = 0;
long int byte_location = 0;
unsigned char bit_location = 0;
/*32 or 24 bit per pixel*/
if(vinfo.bits_per_pixel == 32 || vinfo.bits_per_pixel == 24) {
uint32_t * fbp32 = (uint32_t *)fbp;
int32_t y;
for(y = act_y1; y <= act_y2; y++) {
location = (act_x1 + vinfo.xoffset) + (y + vinfo.yoffset) * finfo.line_length / 4;
memcpy(&fbp32[location], (uint32_t *)color_p, (act_x2 - act_x1 + 1) * 4);
color_p += w;
}
}
/*16 bit per pixel*/
else if(vinfo.bits_per_pixel == 16) {
uint16_t * fbp16 = (uint16_t *)fbp;
int32_t y;
for(y = act_y1; y <= act_y2; y++) {
location = (act_x1 + vinfo.xoffset) + (y + vinfo.yoffset) * finfo.line_length / 2;
memcpy(&fbp16[location], (uint32_t *)color_p, (act_x2 - act_x1 + 1) * 2);
color_p += w;
}
}
/*8 bit per pixel*/
else if(vinfo.bits_per_pixel == 8) {
uint8_t * fbp8 = (uint8_t *)fbp;
int32_t y;
for(y = act_y1; y <= act_y2; y++) {
location = (act_x1 + vinfo.xoffset) + (y + vinfo.yoffset) * finfo.line_length;
memcpy(&fbp8[location], (uint32_t *)color_p, (act_x2 - act_x1 + 1));
color_p += w;
}
}
/*1 bit per pixel*/
else if(vinfo.bits_per_pixel == 1) {
uint8_t * fbp8 = (uint8_t *)fbp;
int32_t x;
int32_t y;
for(y = act_y1; y <= act_y2; y++) {
for(x = act_x1; x <= act_x2; x++) {
location = (x + vinfo.xoffset) + (y + vinfo.yoffset) * vinfo.xres;
byte_location = location / 8; /* find the byte we need to change */
bit_location = location % 8; /* inside the byte found, find the bit we need to change */
fbp8[byte_location] &= ~(((uint8_t)(1)) << bit_location);
fbp8[byte_location] |= ((uint8_t)(color_p->full)) << bit_location;
color_p++;
}
color_p += area->x2 - act_x2;
}
} else {
/*Not supported bit per pixel*/
}
//May be some direct update command is required
//ret = ioctl(state->fd, FBIO_UPDATE, (unsigned long)((uintptr_t)rect));
lv_disp_flush_ready(drv);
}
注册好显示器驱动之后调用 demo_create() 函数并周期性地执行 lv_task_handler() 函数。然后我们注意到有定义一个 custom_tick_get(void) 函数,但是主程序中没有调用该函数,细心点会注意到这个函数其实就是前面说过的,如果不使用 lv_tick_inc(x) 函数的话,需要定义一个获取当前程序已运行时间的函数,并以宏LV_TICK_CUSTOM_SYS_TIME_EXPR来表示该函数。到这里demo程序的框架就已经搞定了,具体的LittlevGL的使用也就是 demo_create() 函数中的实现后面再继续学习。