LVGL 入門使用教學

2022-09-15 15:00:50

一、準備資料

開發板:ESP32-S3
開發環境:VS Code + PlatformIO
串列埠屏驅動 TFT-eSPI:https://github.com/Bodmer/TFT_eSPI
觸控驅動 Arduino-FT6336U:https://github.com/aselectroworks/Arduino-FT6336U
GUI LVGL:https://github.com/lvgl/lvgl

二、專案搭建

  1. 資源庫下載
    這裡我使用的驅動都是從 GitHub 上下載,有經驗的小夥伴也可以自己寫驅動程式

  2. 工程檔案
    將下載的 TFT-eSPI、Arduino-FT6336U、LVGL,放在專案的lib檔案下,如下圖所示:

  3. 在 c_cpp_properties.json 檔案中的 includePath 和 path 中新增資源路徑

    "c:/Users/Administrator/Desktop/weather_clock_test/lib/TFT_eSPI",
    
    "c:/Users/Administrator/Desktop/weather_clock_test/lib/lvgl",
    "c:/Users/Administrator/Desktop/weather_clock_test/lib/lvgl/src",
    
    "c:/Users/Administrator/Desktop/weather_clock_test/lib/Arduino-FT6336U/src",
    

    注意:自己的專案路徑,我這裡只是舉例。

三、屏驅動 TFT-eSPI

  1. 設定顯示器驅動
    在路徑TFT_eSPI/User_Setup.h,中找到User_Setup.h檔案,設定顯示屏的驅動,不知道怎麼使用 TFT-eSPI 的小夥伴可以看我之前的筆記TFT-eSPI入門使用教學

  2. 建立物件

    TFT_eSPI tft = TFT_eSPI()
    TFT_eSPI tft = TFT_eSPI(320,240)        // 在建立物件的時候設定螢幕尺寸
    

    注意:記得載入標頭檔案 #include <TFT_eSPI.h>

  3. TFT-eSPI的初始化程式初始化

    /* ------------ 螢幕背光亮度 ------------*/
    /* 設定LED PWM通道屬性,PWD通道為 0,頻率為1KHz */
    ledcSetup(LCD_BL_PWM_CHANNEL, 1000, TFT_BL);
    /* 設定LED PWM通道屬性 */
    ledcAttachPin(LCD_BL_PIN, LCD_BL_PWM_CHANNEL);
    ledcWrite(LCD_BL_PWM_CHANNEL, (int)(1 * 255));
    
    /* 初始化顯示驅動 */
    tft.init(); 
    /* 旋轉角度 0、1、2、3 對應 0 、90度、180度、270 */
    tft.setRotation(0);
    /* 關閉顏色反轉 */
    tft.invertDisplay(0);
    

四. 觸控驅動 Arduino-FT6336U

觸控驅動比較簡單,不需要複雜的設定,只需要在初始化的時候傳入引腳即可

  1. 觸控引腳的宏定義

    #define I2C_SDA 4
    #define I2C_SCL 15
    #define RST_N_PIN 5
    #define INT_N_PIN 17
    
  2. 建立物件

    FT6336U ft6336u(I2C_SDA, I2C_SCL, RST_N_PIN, INT_N_PIN); 
    FT6336U_TouchPointType tp;
    
  3. 初始化

    ft6336u.begin();
    

五、LVGL使用

這裡我整理了一些 LVGL 的學習資料,需要的可以瞭解一下 LVGL學習資料

  1. 下載 LVGL
    從GitHub 中下載 或者克隆 LVGL 資源庫

  2. 在專案中新增 LVGL 資源庫
    將下載的庫檔案複製到專案的lib路徑下,建議將資源的檔名改為 lvgl
    注意:名字不一樣時,c_cpp_properties.json檔案中新增的路徑也會變化

  3. 重新命名 lv_conf_template.h 檔案

    • lvgl/lv_conf_template.h 檔案重新命名為 lv_conf.h
    • 將檔案中的第一個 #if 0 改為 #if 1
    • 通過設定 LV_COLOR_DEPTH 宏,設定顯示屏的顏色深度
  4. 設定 LVGL 的心跳時間
    在計時器或任務重每 x 毫秒呼叫一次 lv_tick_inc(x) 函數( x 應該在 1 ~ 10 之間)。
    當然使用 Arduino 環境的可以直接設定 lv_conf.h 檔案中的宏 LV_TICK_CUSTOM ,達到目的,原理如下圖所示:

  5. LVGL 庫的使用
    在需要使用 LVGL 庫相關函數的檔案中新增 #include <lvgl.h> 標頭檔案即可

  6. 初始化 LVGL 庫
    只需要在使用 LVGL 之前 呼叫 lv_init() 函數即可

  7. 建立繪製緩衝區
    LVGL 將在緩衝區中渲染影象,然後通過顯示驅動的函數將影象傳送到顯示器
    緩衝區大小可以自由設定,但是建議緩衝區最小為螢幕大小的 1/10,程式如下所示:

    /*------------ 通過靜態空間建立緩衝區 ------------*/
    #define DISP_BUF_SIZE ((240*320)/10)
    static lv_disp_draw_buf_t draw_buf;            // 繪製緩衝區的內部圖形緩衝區
    static lv_color_t buf_1[DISP_BUF_SIZE];        // 緩衝區為螢幕大小的1/10
    
    /* 初始化顯示緩衝區 */
    lv_disp_draw_buf_init(&draw_buf, buf_1, NULL, DISP_BUF_SIZE);
    
    /*------------ 通過堆空間建立緩衝區 ------------*/
    #define DISP_BUF_SIZE ((240*320)/10)
    static lv_disp_draw_buf_t draw_buf;        // 繪製緩衝區的內部圖形緩衝區
    static lv_color_t *buf1;                   // 緩衝區1
    static lv_color_t *buf2;                   // 緩衝區2
    
    buf1 = (lv_color_t*)heap_caps_malloc(DISP_BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA);
    assert(buf1 != NULL);
    
    buf2 = (lv_color_t*)heap_caps_malloc(DISP_BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA);
    assert(buf2 != NULL);
    
    lv_disp_draw_buf_init(&draw_buf, buf1, buf2, DISP_BUF_SIZE);
    

    注意:
    必須保證繪製緩衝區的宣告週期,方式可以是全域性變數、靜態空間、堆空間。
    如果是黑白屏建立一個緩衝區即可,也就是 buf2 = NULL ,彩色屏建議建立兩個繪製緩衝區

  8. 註冊顯示驅動
    通過註冊的回撥函數,將繪製好的圖形通過顯示屏驅動進行繪製顯示。回撥函數會在重新整理顯示的時候呼叫

    /* 設定LVGL的顯示驅動的結構屬性 */
    static lv_disp_drv_t disp_drv;              // 顯示驅動程式的描述符
    lv_disp_drv_init(&disp_drv);                // 初始化控制程式碼,確保所有引數都是預設值
    disp_drv.hor_res = MY_DISP_HOR_RES;         // 設定顯示器的水平解析度
    disp_drv.ver_res = MY_DISP_VER_RES;         // 設定顯示器的垂直解析度
    disp_drv.flush_cb = my_disp_flush;          // 顯示驅動的回撥函數
    disp_drv.draw_buf = &draw_buf;              // 將緩衝區分配給顯示器
    lv_disp_drv_register(&disp_drv);            // 註冊驅動
    
    /**
     * @brief 顯示回撥函數,通過此回撥函數將繪製空間的圖形傳遞給顯示驅動程式
     * @param disp 顯示驅動程式的描述符
     * @param area 影象需要顯示的區域
     * @param color_p 描繪後的圖形
     */
    void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p)
    {
        uint32_t w = (area->x2 - area->x1 + 1);
        uint32_t h = (area->y2 - area->y1 + 1);
    
        tft.startWrite();
        tft.setAddrWindow(area->x1, area->y1, w, h);
        tft.pushColors(&color_p->full, w * h, true);
        tft.endWrite();
    
        /* 反饋顯示結果*/
        lv_disp_flush_ready(disp);
    }
    
  9. 輸入裝置驅動
    通過註冊的回撥函數,將觸控獲取的座標值傳遞給 LVGL 。此回撥函數是由 LVGL 的時間管理進行定時呼叫的,能否通過終端的形式進行獲取我目前還不知道,有了解的朋友望告知一下。

    static lv_indev_drv_t indev_drv;                     // 輸入驅動程式的描述符
    lv_indev_drv_init(&indev_drv);                       // 初始化
    indev_drv.type = LV_INDEV_TYPE_POINTER;              // 設定裝置型別
    indev_drv.read_cb = touch_read;                      // 輸入裝置的回撥函數
    lv_indev_drv_register(&indev_drv);                   // 建立輸入裝置
    
    /**
     * @brief 觸控回撥函數,通過此回撥函數將觸控獲取的座標傳遞給 LVGL
     * @param indev_driver 
     * @param data 輸入裝置的資料
     */
    void touch_read(lv_indev_drv_t * indev_driver, lv_indev_data_t * data)
    {
        tp = ft6336u.scan(); 
        static int16_t last_x = 0;
        static int16_t last_y = 0;
    
        /* 判斷螢幕是否被按下 */
        bool touched = tp.touch_count;
        if (touched)
        {
            last_x = tp.tp[0].x;
            last_y = tp.tp[0].y;
            data->state = LV_INDEV_STATE_PRESSED; 
        }
        else {
            data->state = LV_INDEV_STATE_RELEASED;
        }
    
        /* 將獲取的座標傳入 LVGL */
        data->point.x = last_x;
        data->point.y = last_y;
    }
    
  10. 呼叫 lv_timer_handler()
    在主while(1) 迴圈或作業系統任務中每隔幾毫秒定期呼叫lv_timer_handler()。它將重繪螢幕、處理輸入裝置、動畫等

六、介面繪製

  1. 建立介面

    • 方式1
      建立一個空的介面

      lv_obj_t *view_test = lv_btn_create(NULL);
      

      注意:新的介面在顯示的時候需要通過載入函數,將介面載入到顯示器上

      lv_scr_load(view_test);
      
    • 方式2:
      在當前活動介面上建立介面,建立完成後會自動載入到顯示器上

      lv_obj_t * text_t = lv_btn_create(lv_scr_act());
      
  2. 建立標籤

    /**
     * @brief 建立一個標籤
     */
    lv_obj_t *label = lv_label_create(lv_scr_act());
    if (NULL != label)
    {
        // lv_obj_set_x(label, 90);                         // 設定控制元件的X座標
        // lv_obj_set_y(label, 100);                        // 設定控制元件的Y座標
        // lv_obj_set_size(label, 60, 20);                  // 設定控制元件大小
        lv_label_set_text(label, "Counter");                // 初始顯示 0
        // lv_obj_center(label);                            // 居中顯示
        lv_obj_align(label, LV_ALIGN_CENTER, 0, -50);       // 居中顯示後,向上偏移50
    }
    
  3. 建立按鈕

    /**
     * @brief 按鈕事件回撥函數
     */
    static void btn_event_callback(lv_event_t* event)
    {
        static uint32_t counter = 1;
     
        lv_obj_t* btn = lv_event_get_target(event);                 //獲取事件物件
        if (btn != NULL)
        {
            lv_label_set_text_fmt(label, "%d", counter);            //設定顯示內容
            lv_obj_align(label, LV_ALIGN_CENTER, 0, -50);           // 居中顯示後,向上偏移50
            counter++;
        }
    }
    
    /**
     * @brief 建立按鈕
     */
    void lvgl_button_test(){
        /* 在當前介面中建立一個按鈕 */
        lv_obj_t* btn = lv_btn_create(lv_scr_act());                                        // 建立Button物件
        if (btn != NULL)
        {
            lv_obj_set_size(btn, 80, 20);                                                   // 設定物件寬度和高度
            // lv_obj_set_pos(btn, 90, 200);                                                // 設定按鈕的X和Y座標
            lv_obj_add_event_cb(btn, btn_event_callback, LV_EVENT_CLICKED, NULL);           // 給物件新增CLICK事件和事件處理回撥函數
            lv_obj_align(btn, LV_ALIGN_CENTER, 0, 50);                                      // 居中顯示後,向下偏移50
     
            lv_obj_t* btn_label = lv_label_create(btn);                                     // 基於Button物件建立Label物件
            if (btn_label != NULL)
            {
                lv_label_set_text(btn_label, "button");                                     // 設定顯示內容
                lv_obj_center(btn_label);                                                   // 物件居中顯示
            }
        }    
    }
    
  4. LGVL 的API
    因為官網並沒有詳細的 API 檔案,所以想找查詢詳細的 API 只能通過每個元件的標頭檔案進行檢視

七、測試程式

main.cpp

#include <Arduino.h>
#include <lvgl.h>
#include <TFT_eSPI.h>
#include <FT6336U.h>

/*------------ 觸控引腳 ------------*/
#define I2C_SDA 4
#define I2C_SCL 15
#define RST_N_PIN 5
#define INT_N_PIN 17

/*------------ 背光通道 ------------*/
#define LCD_BL_PIN 6				// PWD 的 IO 引腳
#define LCD_BL_PWM_CHANNEL 0		// Channel  通道, 0 ~ 16,高速通道(0 ~ 7)由80MHz時鐘驅動,低速通道(8 ~ 15)由 1MHz 時鐘驅動

/*------------ LVGL ------------*/
#define MY_DISP_HOR_RES 240                                     // 顯示屏的寬畫素
#define MY_DISP_VER_RES 320                                     // 顯示屏的高畫素
#define DISP_BUF_SIZE ((MY_DISP_HOR_RES*MY_DISP_VER_RES)/10)

static lv_disp_draw_buf_t draw_buf;                             // 繪製緩衝區的內部圖形緩衝區
static lv_color_t buf_1[DISP_BUF_SIZE];                         // 緩衝區為螢幕大小的1/10
static lv_color_t *buf1;                                        // 緩衝區為螢幕大小的1/10
static lv_color_t *buf2;                                        // 緩衝區為螢幕大小的1/10

/*------------ 顯示驅動物件 ------------*/
TFT_eSPI tft = TFT_eSPI();

/*------------ 觸控驅動物件 ------------*/
FT6336U ft6336u(I2C_SDA, I2C_SCL, RST_N_PIN, INT_N_PIN); 
FT6336U_TouchPointType tp;

/*------------ 測試介面物件 ------------*/
lv_obj_t *label; 

/**
 * @brief 觸控回撥函數,通過此回撥函數將觸控獲取的座標傳遞給 LVGL
 * @param indev_driver 
 * @param data 輸入裝置的資料
 */
void touch_read(lv_indev_drv_t * indev_driver, lv_indev_data_t * data)
{
    tp = ft6336u.scan(); 
    static int16_t last_x = 0;
    static int16_t last_y = 0;

    /* 判斷螢幕是否被按下 */
    bool touched = tp.touch_count;
    if (touched)
    {
        last_x = tp.tp[0].x;
        last_y = tp.tp[0].y;
        data->state = LV_INDEV_STATE_PRESSED; 
    }
    else {
        data->state = LV_INDEV_STATE_RELEASED;
    }

    /* 將獲取的座標傳入 LVGL */
    data->point.x = last_x;
    data->point.y = last_y;
}

/**
 * @brief 顯示回撥函數,通過此回撥函數將繪製空間的圖形傳遞給顯示驅動程式
 * @param disp 顯示驅動程式的描述符
 * @param area 影象需要顯示的區域
 * @param color_p 描繪後的圖形
 */
void my_disp_flush(lv_disp_drv_t *disp, const lv_area_t *area, lv_color_t *color_p)
{
    uint32_t w = (area->x2 - area->x1 + 1);
    uint32_t h = (area->y2 - area->y1 + 1);

    tft.startWrite();
    tft.setAddrWindow(area->x1, area->y1, w, h);
    tft.pushColors(&color_p->full, w * h, true);
    tft.endWrite();

    /* 反饋顯示結果*/
    lv_disp_flush_ready(disp);
}

/**
 * @brief 初始化顯示屏驅動
 */
void disp_drv_init(){
    /* ------------ 螢幕背光亮度 ------------*/
    /* 設定LED PWM通道屬性,PWD通道為 0,頻率為1KHz */
    ledcSetup(LCD_BL_PWM_CHANNEL, 1000, TFT_BL);
	/* 設定LED PWM通道屬性 */
    ledcAttachPin(LCD_BL_PIN, LCD_BL_PWM_CHANNEL);
    ledcWrite(LCD_BL_PWM_CHANNEL, (int)(1 * 255));

    /* 初始化顯示驅動 */
    tft.init(); 
    /* 旋轉角度 0、1、2、3 對應 0 、90度、180度、270 */
    tft.setRotation(0);
    /* 關閉顏色反轉 */
    tft.invertDisplay(0);
}

/**
 * @brief 初始化觸控驅動
 */
void touch_drv_init(){
    ft6336u.begin();
}

void lvgl_init(){
    /*------------- 初始化LVGL庫 -------------*/
    lv_init();

    /* 初始化顯示緩衝區 */
    // lv_disp_draw_buf_init(&draw_buf, buf_1, NULL, DISP_BUF_SIZE);

    /*------------- 建立圖形繪製緩衝區 -------------*/
    buf1 = (lv_color_t*)heap_caps_malloc(DISP_BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA);
    assert(buf1 != NULL);

    buf2 = (lv_color_t*)heap_caps_malloc(DISP_BUF_SIZE * sizeof(lv_color_t), MALLOC_CAP_DMA);
    assert(buf2 != NULL);

    lv_disp_draw_buf_init(&draw_buf, buf1, buf2, DISP_BUF_SIZE);
 
    /*------------- 設定LVGL的顯示裝置 -------------*/
    static lv_disp_drv_t disp_drv;              // 顯示驅動程式的描述符
    lv_disp_drv_init(&disp_drv);                // 初始化控制程式碼,確保所有引數都是預設值
    disp_drv.hor_res = MY_DISP_HOR_RES;             // 設定顯示器的水平解析度
    disp_drv.ver_res = MY_DISP_VER_RES;            // 設定顯示器的垂直解析度
    disp_drv.flush_cb = my_disp_flush;          // 顯示驅動的回撥函數
    disp_drv.draw_buf = &draw_buf;              // 將緩衝區分配給顯示器
    lv_disp_drv_register(&disp_drv);            // 註冊驅動

    /*------------- 設定LVGL的輸入裝置 -------------*/
    // static lv_indev_t *indev_cor;
    static lv_indev_drv_t indev_drv;                     // 輸入驅動程式的描述符
    lv_indev_drv_init(&indev_drv);                       // 初始化
    indev_drv.type = LV_INDEV_TYPE_POINTER;              // 設定裝置型別
    indev_drv.read_cb = touch_read;                      // 輸入裝置的回撥函數
    lv_indev_drv_register(&indev_drv);                   // 建立輸入裝置
}

/**
 * @brief 建立標籤
 */
void lvgl_lable_test(){
    /* 建立一個標籤 */
    label = lv_label_create(lv_scr_act());
    if (NULL != label)
    {
        // lv_obj_set_x(label, 90);                         // 設定控制元件的X座標
        // lv_obj_set_y(label, 100);                        // 設定控制元件的Y座標
        // lv_obj_set_size(label, 60, 20);                  // 設定控制元件大小
        lv_label_set_text(label, "Counter");                // 初始顯示 0
        // lv_obj_center(label);                            // 居中顯示
        lv_obj_align(label, LV_ALIGN_CENTER, 0, -50);       // 居中顯示後,向上偏移50
    }
}

/**
 * @brief 按鈕事件回撥函數
 */
static void btn_event_callback(lv_event_t* event)
{
    static uint32_t counter = 1;
 
    lv_obj_t* btn = lv_event_get_target(event);                 //獲取事件物件
    if (btn != NULL)
    {
        lv_label_set_text_fmt(label, "%d", counter);            //設定顯示內容
        lv_obj_align(label, LV_ALIGN_CENTER, 0, -50);           // 居中顯示後,向上偏移50
        counter++;
    }
}

/**
 * @brief 建立按鈕
 */
void lvgl_button_test(){
    /* 在當前介面中建立一個按鈕 */
    lv_obj_t* btn = lv_btn_create(lv_scr_act());                                        // 建立Button物件
    if (btn != NULL)
    {
        lv_obj_set_size(btn, 80, 20);                                                   // 設定物件寬度和高度
        // lv_obj_set_pos(btn, 90, 200);                                                // 設定按鈕的X和Y座標
        lv_obj_add_event_cb(btn, btn_event_callback, LV_EVENT_CLICKED, NULL);           // 給物件新增CLICK事件和事件處理回撥函數
        lv_obj_align(btn, LV_ALIGN_CENTER, 0, 50);                                      // 居中顯示後,向下偏移50
 
        lv_obj_t* btn_label = lv_label_create(btn);                                     // 基於Button物件建立Label物件
        if (btn_label != NULL)
        {
            lv_label_set_text(btn_label, "button");                                     // 設定顯示內容
            lv_obj_center(btn_label);                                                   // 物件居中顯示
        }
    }    
}

void setup() {
	Serial.begin(115200);
    Serial.println("mian.cpp-> 程式初始化......");

    /* 初始化顯示驅動 */
	disp_drv_init();

    /* 初始化觸控驅動 */
    touch_drv_init();

	/* lvgl 初始化 */
    lvgl_init();

    /* 載入標籤 */
    lvgl_lable_test();
    /* 載入按鈕 */
    lvgl_button_test();
}

void loop() {
    lv_timer_handler();
	delay(5);
}

八、測試