開發板: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
資源庫下載
這裡我使用的驅動都是從 GitHub 上下載,有經驗的小夥伴也可以自己寫驅動程式
工程檔案
將下載的 TFT-eSPI、Arduino-FT6336U、LVGL,放在專案的lib檔案下,如下圖所示:
在 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/User_Setup.h,中找到User_Setup.h檔案,設定顯示屏的驅動,不知道怎麼使用 TFT-eSPI 的小夥伴可以看我之前的筆記TFT-eSPI入門使用教學。
建立物件
TFT_eSPI tft = TFT_eSPI()
TFT_eSPI tft = TFT_eSPI(320,240) // 在建立物件的時候設定螢幕尺寸
注意:記得載入標頭檔案 #include <TFT_eSPI.h>
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);
觸控驅動比較簡單,不需要複雜的設定,只需要在初始化的時候傳入引腳即可
觸控引腳的宏定義
#define I2C_SDA 4
#define I2C_SCL 15
#define RST_N_PIN 5
#define INT_N_PIN 17
建立物件
FT6336U ft6336u(I2C_SDA, I2C_SCL, RST_N_PIN, INT_N_PIN);
FT6336U_TouchPointType tp;
初始化
ft6336u.begin();
這裡我整理了一些 LVGL 的學習資料,需要的可以瞭解一下 LVGL學習資料
下載 LVGL
從GitHub 中下載 或者克隆 LVGL 資源庫
在專案中新增 LVGL 資源庫
將下載的庫檔案複製到專案的lib路徑下,建議將資源的檔名改為 lvgl
注意:名字不一樣時,c_cpp_properties.json檔案中新增的路徑也會變化
重新命名 lv_conf_template.h 檔案
lvgl/lv_conf_template.h
檔案重新命名為 lv_conf.h
#if 0
改為 #if 1
LV_COLOR_DEPTH
宏,設定顯示屏的顏色深度設定 LVGL 的心跳時間
在計時器或任務重每 x
毫秒呼叫一次 lv_tick_inc(x)
函數( x
應該在 1 ~ 10 之間)。
當然使用 Arduino 環境的可以直接設定 lv_conf.h
檔案中的宏 LV_TICK_CUSTOM
,達到目的,原理如下圖所示:
LVGL 庫的使用
在需要使用 LVGL 庫相關函數的檔案中新增 #include <lvgl.h>
標頭檔案即可
初始化 LVGL 庫
只需要在使用 LVGL 之前 呼叫 lv_init()
函數即可
建立繪製緩衝區
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
,彩色屏建議建立兩個繪製緩衝區
註冊顯示驅動
通過註冊的回撥函數,將繪製好的圖形通過顯示屏驅動進行繪製顯示。回撥函數會在重新整理顯示的時候呼叫
/* 設定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);
}
輸入裝置驅動
通過註冊的回撥函數,將觸控獲取的座標值傳遞給 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;
}
呼叫 lv_timer_handler()
在主while(1) 迴圈或作業系統任務中每隔幾毫秒定期呼叫lv_timer_handler()。它將重繪螢幕、處理輸入裝置、動畫等
建立介面
方式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());
建立標籤
/**
* @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
}
建立按鈕
/**
* @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); // 物件居中顯示
}
}
}
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);
}