2 Star 3 Fork 1

稀风/LVGL

加入 Gitee
与超过 1200万 开发者一起发现、参与优秀开源项目,私有仓库也完全免费 :)
免费加入
该仓库未声明开源许可证文件(LICENSE),使用请关注具体项目描述及其代码上游依赖。
克隆/下载
LVGL无OS移植.md 15.10 KB
一键复制 编辑 原始数据 按行查看 历史
稀风 提交于 2024-03-16 12:48 . 工具配置添加FreeRTOS

认识 LVGL

  • LVGL(Light and Versatile Graphics Library)是一个免费的轻量级开源图形库,其主要特征有:
    • 丰富的部件:开关、按钮、图表、列表、滑块、图片,等等。
    • 高级图形属性:具有动画、抗锯齿、不透明度、平滑滚动等高级图形属性。
    • 支持多种输入设备:如触摸屏、鼠标、键盘、编码器等。
    • 支持多语言:UTF-8 编码。
    • 支持多显示器:它可以同时使用多个 TFT 或者单色显示器。
    • 支持多种样式属性:它具有类 CSS 的样式,支持自定义图形元素。
    • 独立于硬件之外:它可以与任何微控制器或显示器一起使用。
    • 可扩展性:它能够以小内存运行(最低 64 kB 闪存,16 kB RAM 的 MCU)。
    • 支持操作系统、外部存储器和 GPU(不是必需的)。
    • 具有高级图形效果:可进行单帧缓冲区操作。
    • 纯 C 编写: LVGL 基于 C 语言编写,以获得最大的兼容性。

LVGL 移植要求

  • 市面上拥有众多的微处理器(MCU),但并不是每一个 MCU 都适合移植 LVGL 图形库,例如传统的 51 单片机,它并不具备移植 LVGL 图形库的条件。下面我们来看看 LVGL 对硬件的要求:

MCU

  • LVGL 图形库对微处理器具有一定的要求,例如主频、内存等,具体要求如下表所示:
    要求 说明
    微控制器 16、32 、64 位的微控制器或处理器
    主控频率(Hz) > 16 MHz 时钟速度
    Flash/ROM > 64 kB ,如果使用非常多的部件,推荐 > 180 kB
    内存(RAM) 8kB(建议配置 24kB)
  • 从上表可知:微处理器至少需要 16 位以上,所以传统的 51、52 单片机无法移植 LVGL,它们都是 8 位的微处理器。

显示屏

  • LVGL 只需要一个简单的驱动程序函数即可将像素阵列复制到显示器的给定区域中,其对显示屏的兼容性很强,具体要求如下(满足其一即可):
    • 具有 8/16/24/32 位色深的显示屏。
    • HDMI 端口的显示器。
    • 小型单色显示器。
    • LED 矩阵。
    • 其他可以控制像素颜色/状态的显示器。

LVGL 源码下载

  • LVGL 相关的源码和工程都是存放在 GitHub 远程仓库中,该 GitHub 远程仓库地址为: https://github.com/lvgl/lvgl/
  • 以下实验中使用的是从 github 下载的最新版,版本为 9.0.1
  • 不同的版本之间可能会存在些许差异

LVGL 源码文件结构简介

  • 源码下载下来后我们先初步的把一些杂乱的无关紧要的东西删一删,如下图所示:

  • 余下的各文件夹和文件的功能如下表所示:

    文件/文件夹 说明
    demos LVGL 提供的综合演示源码
    docs LVGL 文献,主要说明 LVGL 每个部件的使用方法
    env_support 环境的支持(MDK、ESP、RTThread)
    examples LVGL 例程源码和 LVGL 输入设备驱动,显示屏驱动文件
    scripts LVGL 相关脚本工具
    src LVGL 源文件(LVGL 部件源码、第三方库)
    tests 官方人员的测试代码,该文件夹用户无需了解
    LICENCE.txt 定义了软件的使用权和限制
    lv_conf_template.h LVGL 的配置文件
    lvgl.h LVGL 包含的头文件
  • 上表中,与 LVGL 移植相关的有 examples 文件夹、src 文件夹、lv_conf_template.h 和 lvgl.h 文件,其他的部分均与移植无关,用户可以选择忽略。接下来我们分别看一下 examples、src 这两个文件夹的文件结构。

examples 文件夹

  • 该文件夹主要包含 LVGL 部件实例、动画实例、其他第三方库实例以及输入设备和显示器驱动文件等内容,具体如下图所示:

  • 其中只有 porting 文件夹与移植相关,其他文件夹中存放的是各种实例

src 文件夹

  • 该文件夹主要包含 LVGL 源文件(部件源码、多种解码库),具体如下表所示:
    文件 描述
    core LVGL 核心源码(事件、组、对象、坐标、样式、主题)
    draw LVGL 绘画驱动(图片、解码、DMA2D、圆、线、圆弧、和文本)
    extra LVGL 的拓展内容(布局、第三方库、其他测试、主题以及部件)
    font LVGL 字库
    gpu LVGL 针对图形加速
    hal 硬件抽象层(显示驱动程序、输入设备程序以及 LVGL 系统滴答)
    misc 主要描述 LVGL 其他定义(动画、内存管理、日志)
    widgets LVGL 基础部件
  • 上表中的内容都与移植相关。

移植准备工作

  • 移植 LVGL 前需要完成显示驱动和触摸驱动,这在前面的几个章节中我们已经完成了,除此之外我们还需要一个 1ms 的定时器中断,不过 CubeIDE 生成的代码中默认的 systick 就是 1ms 中断,直接使用 1ms 中断吧,省点事。

  • 继续把源码中无用的东西删一删,把 example 文件夹中的 porting 文件夹拷贝出来,然后把整个 example 文件夹全部删除,保留 src 文件夹,将 lv_conf_template.h 文件改名为 lv_conf.h, 其余文件或文件夹全部删掉。具体如下图所示:

  • porting 文件夹下的文件名也改一下,如下图所示:

LVGL无OS移植开始

  • 新建一个名为 Lvgl 的文件夹,将上面准备的代码拷贝其中。
  • 将该目录下所有的 .c 文件添加到项目工程中去,不同的 IDE 添加源文件方式可能不一样,这里就不具体说明了,这一步挺麻烦的。
  • 工程编译所需要添加的头文件路径只需要添加 lvgl.h 这一个头文件所在的路径和 porting 文件夹路径即可,我们使用 lvgl 中的功能时只需要包含 lvgl.h 这一头头文件即可,因为 lvgl.h 中已经包含了其它所有头文件的路径了
  • 打开 lv_conf.h 文件,将最上面的 '#if 0' 改为 '#if 1'
  • 编译一下,发现 lv_conf_internal.h 这个文件的第 59 行错误,提示找不到 “lv_conf.h”, 因为我们已经把 Lvgl 这个目录添加到工程的编译头文件路径中,所以直接 "#include "lv_conf.h" 即可,不需要相对路径。
  • lv_conf.h 中的 LV_MEM_SIZE 宏对编译也会有比较大的影响,如果编译提示内存空间不足,可以尝试将这个宏的数值改小。

为 LVGL 提供时基

  • 在 1ms 的中断处理函数中调用函数 'lv_tick_inc(1)' 为 LVGL 提供 1ms 时基,具体如下:
    void SysTick_Handler(void)
    {
      /* USER CODE BEGIN SysTick_IRQn 0 */
    
      /* USER CODE END SysTick_IRQn 0 */
      HAL_IncTick();
      /* USER CODE BEGIN SysTick_IRQn 1 */
      lv_tick_inc(1);
      /* USER CODE END SysTick_IRQn 1 */
    }

修改显示接口

  • lv_port_disp.c 和 lv_port_disp.h 这两个接口文件是 LVGL 的显示接口,可以将用户的底层显示驱动与 LVGL的显示驱动衔接起来。
  • 打开这两个文件,将最上面的 '#if 0' 改为 '#if 1'
  • 将 #include "lv_port_disp_template.h" 改为 #include "lv_port_disp.h"
  • 编译 22 行报错,又找不到头文件 lvgl.h, 这是因为这里有使用了相对路径,直接改为 #include "lvgl.h" 即可
  • 继续编译,警告:
    warning: #warning Please define or replace the macro MY_DISP_HOR_RES with the actual screen width, default value 320 is used for now. [-Wcpp]
  • 很显然,需要先定义一下这两个宏,这两个宏的值必须跟实际显示屏分辨率一致。
    /*********************
     *      DEFINES
    *********************/
    #define MY_DISP_HOR_RES    1024
    #define MY_DISP_VER_RES    600
  • lv_port_disp.c 文件中就 lv_port_disp_init 和 disp_flush 这两个接口函数需要改动,先看 lv_port_disp_init() 函数,在该函数中先调用了 disp_init() 函数,我们可以把 LCD 相关初始化代码放到这里面,但是目前我们的工程都是都过工具配置自动生成代码,不好改动,不过可以把点亮背光的代码放到 disp_init() 函数中,总得填点啥以示尊重嘛。再接下来 lvgl 提供了 3 个 Example,这 3 个 Example 只能保留其中的一个,具体要根据芯片 RAM 资源来定,理论上来说 3 个 Example 的 RAM 资源使用依次增加,对应的显示效果也是依次增加的。前两个 Example 的 buff 缓冲区大小也是可改的,这也是需要根据 RAM 资源大小而定的。
    /*Initialize your display and the required peripherals.*/
    static void disp_init(void)
    {
        /*You code here*/
        // 点亮背光
        HAL_GPIO_WritePin(LCD_BL_GPIO_Port, LCD_BL_Pin, GPIO_PIN_SET);
    }
    
    void lv_port_disp_init(void)
    {
        /*-------------------------
         * Initialize your display
         * -----------------------*/
        disp_init();
    
        /*------------------------------------
         * Create a display and set a flush_cb
         * -----------------------------------*/
        lv_display_t * disp = lv_display_create(MY_DISP_HOR_RES, MY_DISP_VER_RES);
        lv_display_set_flush_cb(disp, disp_flush);
    
        /* Example 1
         * One buffer for partial rendering*/
        static lv_color_t buf_1_1[MY_DISP_HOR_RES * 100];                          /*A buffer for 10 rows*/
        lv_display_set_buffers(disp, buf_1_1, NULL, sizeof(buf_1_1), LV_DISPLAY_RENDER_MODE_PARTIAL);
    
        // /* Example 2
        //  * Two buffers for partial rendering
        //  * In flush_cb DMA or similar hardware should be used to update the display in the background.*/
        // static lv_color_t buf_2_1[MY_DISP_HOR_RES * 50];
        // static lv_color_t buf_2_2[MY_DISP_HOR_RES * 50];
        // lv_display_set_buffers(disp, buf_2_1, buf_2_2, sizeof(buf_2_1), LV_DISPLAY_RENDER_MODE_PARTIAL);
    
        // /* Example 3
        //  * Two buffers screen sized buffer for double buffering.
        //  * Both LV_DISPLAY_RENDER_MODE_DIRECT and LV_DISPLAY_RENDER_MODE_FULL works, see their comments*/
        // static lv_color_t buf_3_1[MY_DISP_HOR_RES * MY_DISP_VER_RES];
        // static lv_color_t buf_3_2[MY_DISP_HOR_RES * MY_DISP_VER_RES];
        // lv_display_set_buffers(disp, buf_3_1, buf_3_2, sizeof(buf_3_1), LV_DISPLAY_RENDER_MODE_DIRECT);
    
    }
  • 接着看 disp_flush 函数,该函数需要调用 LCD 相关底层接口函数,将图像显示出来。为了提高刷屏速度,我们没有采用 LCD 的打点方式而是采用了 DMA2D 方式
    #include "lcd.h"
    lv_display_t* glv_display = NULL;
    static void disp_flush(lv_display_t * disp_drv, const lv_area_t * area, uint8_t * px_map)
    {
        if(disp_flush_enabled) 
        {
            glv_display = disp_drv;
            lcd_fill_area_with_pixmap(area->x1, area->y1, area->x2 - area->x1 + 1, area->y2 - area->y1 + 1, (uint16_t *)px_map);
        }
    
        /* IMPORTANT!!! 刷新完成通知放到了 LCD 区域填充完成中断(DMA2D 传输完成中断) 中 */
        // lv_display_flush_ready(disp_drv);
    }
  • 注意:刷新完成通知函数 lv_display_flush_ready 被放到了 DMA2D 传输完成中断中调用,具体如下所示,lcd_fill_finished 这个函数已经在 DMA2D 传输完成中断服务处理函数中调用了。
    /* LCD 区域填充完成回调 */
    #include "lvgl.h"
    extern lv_display_t* glv_display;
    void lcd_fill_finished(void)
    {
        lv_display_flush_ready(glv_display);
    }
  • 显示接口修改完成后,要在初始化中调用如下代码对 LVGL 进行初始化,注意 lv_port_disp_init 函数要放到 lv_init 函数之后:
    lv_init();                  /* lvgl init */
    lv_port_disp_init();        /* lvgl 显示接口初始化,放到 lv_init() 之后 */

LVGL主循环处理

  • LVGL 离正常工作就差最后一个循环处理了,lv_timer_handler 函数还需要被周期调用,大概 5ms 周期即可。
    while(1)
    {
      static uint32_t cnt_5ms;
      if(HAL_GetTick() >= cnt_5ms + 5)
      {
          cnt_5ms = HAL_GetTick();
          lv_timer_handler();
      }
    }

LVGL显示相关配置

  • LVGL 的相关配置信息都在 lv_conf.h 这个头文件中,具体如何配置现在先不详细介绍,以下的一些配置直接影响到移植后显示是否成功。
    /*Color depth: 8 (A8), 16 (RGB565), 24 (RGB888), 32 (XRGB8888)*/
    #define LV_COLOR_DEPTH 16
    
    /*For big endian systems set to 1*/
    #define LV_BIG_ENDIAN_SYSTEM 0

LVGL显示测试

  • 调用如下代码,测试一下显示接口是否修改成功,如果成功,屏幕中间应该能够显示 "Hello!!!" 字样。注意:下面测试代码只能执行一次,不可频繁调用。
    lv_obj_t *label = lv_label_create(lv_scr_act());
    lv_label_set_text(label,"Hello!!!");
    lv_obj_center(label);

修改输入接口

  • 打开 lv_port_indev.c 和 lv_port_indev.h 将最上面的 '#if 0' 改为 '#if 1'
  • 编译报错会跟前面类似,具体解决这里就不细说了。
  • LVGL 提供了 touchpad、mouse、keypad、encoder 和 button 这几种输入接口。
  • 因为输入设备是触摸屏,所以只保留 touchpad 相关代码,其他代码删除,这样子代码看起来简洁一点。
  • 一共就剩下 5 个函数,第一个函数 lv_port_indev_init 代码如下,其作用是初始化并注册输入设备。
    void lv_port_indev_init(void)
    {
        /*------------------
         * Touchpad
         * -----------------*/
    
        /*Initialize your touchpad if you have*/
        /* 初始化触摸屏 */
        touchpad_init();
    
        /*Register a touchpad input device*/
        /* 创建一个输入设备 */
        indev_touchpad = lv_indev_create();
        /* 设置输入设备类型:触摸屏 */
        lv_indev_set_type(indev_touchpad, LV_INDEV_TYPE_POINTER);
        /* 注册输入设备-绑定触摸坐标读取函数 */
        lv_indev_set_read_cb(indev_touchpad, touchpad_read);
    }
  • 初始化函数中调用了 touchpad_init 函数,其功能是初始化触摸屏,所以我们把触摸初始化 touch_init 函数放到该函数中调用即可
    static void touchpad_init(void)
    {
        /*Your code comes here*/
        touch_init();
    }
  • 在接下来是 touchpad_read 函数,该函数不需要改动,具体就不细说了。
  • 再往下是一个判断是否有触摸按下的函数
    static bool touchpad_is_pressed(void)
    {
        /*Your code comes here*/
        return  !!gTouchPoint.touch_num;
    }
  • 还有最后一个函数,其作用为读取触摸点坐标,从函数的表现形式来看 LVGL 貌似只支持单点触摸,不支持多点触摸。简单点处理,我们只上报第一个触摸点数据吧。
    static void touchpad_get_xy(int32_t * x, int32_t * y)
    {
        /*Your code comes here*/
        *x = gTouchPoint.tp[0].xPos;
        *y = gTouchPoint.tp[0].yPos;
    }

LVGL输入测试

  • 调用如下代码,测试一下输入接口是否修改成功,如果成功,屏幕中间会有一个开关按钮,点击后开关按钮会有反应。注意:下面测试代码只能执行一次,不可频繁调用。
    lv_obj_t* switch1 = lv_switch_create(lv_scr_act());
    lv_obj_center(switch1);
  • 关于 LVGL 无 OS 移植代码单独放到仓库 NO-OS 分支下,接下来的带 OS 的代码会继续在主分支下进行
马建仓 AI 助手
尝试更多
代码解读
代码找茬
代码优化
1
https://gitee.com/thin-wind/lvgl.git
git@gitee.com:thin-wind/lvgl.git
thin-wind
lvgl
LVGL
main

搜索帮助

246c6175 1850385 950819b3 1850385