การเขียนโค้ด LVGL 8.x เทียบ 9.x บนบอร์ด ATD3.5-S3

Supachai Vorapojpisut
5 min readMay 6, 2024

--

LVGL (Light and Versatile Graphics Library) เป็นไลบรารีแบบโอเพนซอร์สสำหรับพัฒนา GUI ให้กับอุปกรณ์แบบ embedded ที่มาแรงมากในช่วง 2–3 ปีนี้ ซึ่งอาจเป็นเพราะเทคโนโลยีฝั่งฮาร์ดแวร์ที่ประสิทธิภาพสูงขึ้นแต่ต้นทุนกลับถูกลงทั้งในส่วนไมโครคอนโทรลเลอร์ (ESP32, STM32, …) และจอภาพขนาดใหญ่ (> 3 นิ้วที่เหมาะกับงาน HMI) นอกจากนี้ การเป็นซอฟต์แวร์แบบโอเพนซอร์สที่มีการพัฒนาอย่างต่อเนื่องรวมถึงความร่วมมือกับทั้งชุมชนนักพัฒนาและภาคธุรกิจ ยังเป็นปัจจัยที่กระตุ้นให้มีการนำ LVGL ไปใช้งานมากขึ้นเรื่อยๆ

ตัวอย่าง GUI ที่สร้างด้วย LVGL

ช่วงนี้ไปรับปากว่าจะสร้างอุปกรณ์ต้นแบบไปใช้ในโรงพยาบาล โดยเงื่อนไขตอนคุยกับทีมพยาบาลที่จะเป็นผู้ใช้อุปกรณ์คือ การมีหน้าจอขนาดพอสมควรแสดงสถานะการเฝ้าระวัง แต่ไม่อยากให้เสียบปลั๊กและไม่อยากใช้จอแบบแยก ดังนั้นสามารถตัดการใช้ Raspberry Pi ต่อจอภาพได้เลย และมาลองดูบอร์ดในสต็อคก็มีแต่รุ่นที่จอเล็กไป (M5Stack, ESP32-S3 Box, LilyGo T-Display-S3) พอดีใน Facebook เห็น post ของ AstronShop แนะนำบอร์ด ATD3.5-S3 ที่ออกมาใหม่ มีฮาร์ดแวร์ที่ตอบโจทย์ได้ครบพอดีทั้งในส่วนจอสัมผัสขนาด 3.5 นิ้ว มีช่อง speaker และขา I/O ที่เปิดให้ต่อวงจร ดังนั้นจึงสั่งมาทดลองใช้ดู 1 ชุด ซึ่งก็ค่อนข้างประทับใจของที่ส่งมา เพราะมีเตรียมมาให้ครบพอสมควรทั้งสาย USB เสาอากาศพร้อมหัวต่อ และตัวยึดบอร์ด แต่ขอบ่นหน่อยว่าน่าจะมีตัวเลือกให้สั่งกล่องที่ขนาดพอดีมาด้วย จะได้ยึดอุปกรณ์ลงในบอร์ดพร้อมเอาไปส่งมอบ

บอร์ด ATD3.5-S3 ของ AstronShop

ทีมงานของ AstronShop เตรียมตัวดีมากในการปล่อยบอร์ด ATD3.5-S3 เข้าตลาด เพราะมี clip ในหน้าเว็บที่สอนการใช้งานบอร์ดร่วมกับโปรแกรม SquareLine Studio สำหรับสร้างโค้ดเรียกใช้ LVGL มาสร้าง GUI เตรียมโค้ดตัวอย่างใน GitHub ทำให้เริ่มพัฒนาแอพพลิเคชันได้ง่ายขึ้น และยังมีการปล่อยไฟล์สไลด์ (PPT/PDF) ที่ใช้อบรม ซึ่งน่าจะเป็น PLUS มากสำหรับคนที่จะเอาไปใช้สอน/อบรม ผมลองศึกษาโครงสร้างของโค้ดตัวอย่างพบว่ามีการเรียกใช้ไลบรารี ATD3.5-S3 ที่หุ้มการทำงานของจอภาพ (ST7796S สำหรับแสดงผล FT6336U สำหรับตรวจการสัมผัส) ส่วน SD-card (ผ่าน SPI) และส่วน speaker (MAX98357 เชื่อม I2S) โดยมีแทรกฟังก์ชันสำหรับเชื่อมการทำงานของ LVGL ไว้ด้วย ซึ่งน่าจะช่วยให้พัฒนาได้ง่ายขึ้น

การศึกษาส่วนโค้ดของไลบรารี ATD3.5-S3 พบว่าส่วนโค้ดถูกพัฒนาขึ้นด้วยการผสมระหว่างการเรียกใช้ API ของ Arduino และ ESP-IDF (ส่วนการเข้าถึงฮาร์ดแวร์) การเชื่อมการทำงานเข้ากับไลบรารี LVGL จะมี method คือ useLVGL() เช่น ในกรณีคลาส LCD สำหรับคุมจอแสดงผลจะทำหน้าที่จองหน่วยความจำบัฟเฟอร์แสดงผล และประกาศส่วน callback ในการอัพเดทหน้าจอ ดังนั้นอาจกล่าวได้ว่าไลบรารีนี้น่าจะทำงานได้อย่างมีประสิทธิภาพเพราะเหลือเฉพาะส่วนโค้ดที่เกี่ยวข้องกับบอร์ดนี้

โค้ดในการเชื่อมต่อ ST7796S ผ่าน SPI

ซอฟต์แวร์ที่ทาง AstronShop เตรียมไว้ถือได้ว่าตอบโจทย์การพัฒนาส่วนใหญ่ … แต่ด้วยความอยากรู้ว่าหากไม่ใช้ SquareLine Studio ในการสร้างโค้ด GUI แล้วเขียนโค้ดเองจากการศึกษาโค้ดตัวอย่างของ LVGL บน GitHub กับบอร์ด ATD3.5-S3 จะวุ่นวายแค่ไหน เลยเป็นแรงบันดาลใจในการทดลองจนได้เนื้อหามาเขียนบทความนี้

ด่านแรก TFT_eSPI

ด่านแรกที่ต้องทดลองก่อนคือ ไลบรารี TFT_eSPI ซึ่งเป็นตัวเลือกหลักในการใช้งานจอแสดงผล เนื่องจากรองรับหน่วยประมวลผลหลากตระกูล (RPi RP2040, ESP32, STM32) รวมทั้งมีไดรเวอร์สำหรับชิพรุ่นต่างๆ (ILI9xxx, SSD1351, ST77xx) ที่ทำหน้าที่คุมจอแสดงผล การใช้ไลบรารี TFT_eSPI กับบอร์ด ATD3.5-S3 จะรองรับส่วนหน่วยประมวลผล ESP32-S3 และชิพ ST7796S ที่คุมหน้าจอ แต่ต้องเตรียมส่วนของชิพ FT6636U ที่ตรวจจับการสัมผัสเอง ความวุ่นวายในส่วนนี้เกิดจากเงื่อนไขในการตั้งค่าขาสัญญาณของบัส SPI ที่จะไปเชื่อมต่อกับชิพ ST7796 ซึ่ง TFT_eSPI แนะนำมา 3 วิธีคือ

  1. แก้เนื้อหาในไฟล์ User_Setup.h ที่อยู่ในโฟลเดอร์หลักของไลบรารี TFT_eSPI
  2. แก้เนื้อหาในไฟล์ Setup27_RPi_ST7796_ESP32.h ในโฟลเดอร์ User_Setups แล้วทำการ include แบบเจาะจง
  3. ประกาศค่าคงที่ต่างๆที่เกี่ยวข้อง เช่น ST7796_DRIVER TFT_MISO

ผมไม่เลือกทั้ง 2 วิธีแรกจากการที่ช่วงหลังใช้แต่ Platform.io เป็นเครื่องมือพัฒนา เพราะอาจเกิดปัญหาขึ้นเมื่อมีการส่งต่อ project ไปคนอื่น เช่น การ clean แล้ว push ขึ้น GitHub ปัญหาจะเกิดจากไฟล์ที่แก้แล้วจะหายไป ยกเว้นแต่การดาวน์โหลดไฟล์ไลบรารีมาขยายลงในโฟลเดอร์ lib เอง ส่วนวิธีที่ 3 ทำได้ง่ายด้วยการกำหนดส่วน DEFINE ในขั้นตอนการคอมไพล์ไว้ในไฟล์ platformio.ini ตามตัวอย่าง

[env:atd35-s3]
platform = espressif32
board = atd35-s3
board_build.partitions = 8m_ota_app.csv
framework = arduino
build_flags =
-DCORE_DEBUG_LEVEL=5
-DUSER_SETUP_LOADED=1 ; Disable User_Setup.h
-DST7796_DRIVER=1 ; Select ST7796 driver
-DTFT_MISO=13 ; Define SPI pins
-DTFT_MOSI=11
-DTFT_SCLK=12
-DTFT_CS=10
-DSPI_FREQUENCY=40000000 ; Set SPI frequency
-DTFT_DC=21 ; Data/Comand pin
-DTFT_RST=14 ; Reset pin
-DTFT_BL=3 ; Backlight pin
-DUSE_HSPI_PORT=1 ; Use HSPI port
-DTFT_BACKLIGHT_ON=1 ; Backlight control
-DLOAD_GLCD=1 ; Load Fonts
monitor_speed = 115200
lib_deps =
bodmer/TFT_eSPI@^2.5.43

การทดสอบใช้โค้ดตัวอย่าง TFT_String_Align ในโฟลเดอร์ 480 x 320 เพื่อวาดเส้นและตัวอักษรบนหน้าจอ

หน้าจอแสดงผลตัวอักษรที่ย้ายตำแหน่งไปเรื่อยๆ

ด่านสอง LVGL 8.4.0

รูปแบบ API ของ LVGL จนถึงรุ่น 8.x จะดำเนินการในระดับองค์ประกอบ widget บนหน้าจอเป็นหลัก โดยส่วนโค้ดที่นักพัฒนาจะต้องปรับแต่งให้เข้ากับฮาร์ดแวร์ตัวเองแบ่งออกเป็น 3 ส่วน ได้แก่

  • ส่วนการโอนย้ายข้อมูลระหว่างหน่วยความจำบัฟเฟอร์และชิพควบคุมจอภาพ
  • ส่วนการจัดการภาครับสถานะอินพุต เช่น จอสัมผัส ปุ่มกด
  • ส่วนการคุมจังหวะเวลา

รายละเอียดของ API ที่ขึ้นกับฮาร์ดแวร์ศึกษาได้จากเอกสารบนเว็บของ LVGL

การเริ่มใช้งาน API ของ LVGL รุ่น 8.x จะมีรูปแบบเดียวกับ TFT_eSPI โดยนักพัฒนาจะต้องไปปรับแต่งเงื่อนไขต่างๆในไฟล์ lv_conf.h ที่อยู่ในส่วนโค้ดของไลบรารี LVGL แต่ก็เหมือนกับไลบรารี TFT_eSPI ที่วิธีนี้จะเกิดปัญหาเวลาส่ง project ให้กับนักพัฒนาคนอื่น ผมจึงเลือกจะใช้การสร้าง lv_conf.h ในโฟลเดอร์ของเราเอง โดยใช้ build_flags กำหนดค่า LV_CONF_INCLUDE_SIMPLE ในไฟล์ platformio.ini

build_flags = 
-I./include
-DLV_CONF_INCLUDE_SIMPLE=1

สำหรับหน่วยประมวลผล ESP32 ที่ใช้ Arduino Core มีโค้ดตัวอย่างบน GitHub ที่ใช้ไลบรารี TFT_eSPI ในการส่งผ่านข้อมูลจุดภาพต่างๆไปยังชิพควบคุมจอภาพ รวมถึงการจัดการในส่วนตรวจจับการสัมผัส ส่วนโค้ดอัพเดทหน้าจออยู่ในรูปของฟังก์ชัน callback ที่ทาง LVGL จะกระตุ้นให้อัพเดทเฉพาะพื้นที่ของหน้าจอ โดยเรียกใช้ API ของ TFT_eSPI เพื่อส่งข้อมูลไปยังชิพควบคุมจอภาพ ทั้งนี้รายละเอียดในระดับฮาร์ดแวร์ เช่น ส่งผ่าน SPI อย่างไร จะเป็นการดำเนินการในระดับไลบรารี TFT_eSPI ที่ทาง LVGL จะไม่เข้ามาก้าวก่าย

static lv_disp_draw_buf_t draw_buf;
static lv_color_t buf[ screenWidth * screenHeight / 10 ];

TFT_eSPI tft = TFT_eSPI(screenWidth, screenHeight); /* TFT instance */

void setup() {
...
/*Initialize the display*/
static lv_disp_drv_t disp_drv;
lv_disp_drv_init( &disp_drv );
/*Change the following line to your display resolution*/
disp_drv.hor_res = screenWidth;
disp_drv.ver_res = screenHeight;
disp_drv.flush_cb = my_disp_flush;
disp_drv.draw_buf = &draw_buf;
lv_disp_drv_register( &disp_drv );
,

/*callback function to update screen pixels*/
void my_disp_flush( lv_disp_drv_t *disp_drv, const lv_area_t *area, lv_color_t *color_p ) {
/*draw pixels into specified area*/
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( ( uint16_t * )&color_p->full, w * h, true );
tft.endWrite();
lv_disp_flush_ready( disp_drv );
}

ส่วนโค้ดที่จัดการเรื่องภาคสั่งการผ่านจอสัมผัสจะอยู่ในรูปแบบเดียวกัน เริ่มจากการลงทะเบียนฟังก์ชัน callback สำหรับรับภาคอินพุต โค้ดตัวอย่างเป็นกรณีที่จอภาพมีส่วนชิพตรวจจับการสัมผัสที่สื่อสารผ่านบัส SPI จึงถูกจัดการด้วย API ของไลบรารี TFT_eSPI ด้วย ดังนั้นโค้ดตัวอย่างจึงทำหน้าที่แค่อ่านค่าตำแหน่งสัมผัสแล้วเขียนค่าลงในตัวแปรส่งกลับไปให้ LVGL ที่เรียกใช้ฟังก์ชัน callback นั้น

void setup() {
...
/*Initialize the (dummy) input device driver*/
static lv_indev_drv_t indev_drv;
lv_indev_drv_init( &indev_drv );
indev_drv.type = LV_INDEV_TYPE_POINTER;
indev_drv.read_cb = my_touchpad_read;
lv_indev_drv_register( &indev_drv );
}

/*Read the touchpad*/
void my_touchpad_read( lv_indev_drv_t * indev_drv, lv_indev_data_t * data ) {
uint16_t touchX, touchY;
bool touched = tft.getTouch( &touchX, &touchY, 600 );
if( !touched ) {
data->state = LV_INDEV_STATE_REL;
} else {
data->state = LV_INDEV_STATE_PR;
/*Set the coordinates*/
data->point.x = touchX;
data->point.y = touchY;
}

ส่วนโค้ดจัดการฐานเวลาของ LVGL ในกรณี ESP32 ที่ทำงานด้วย Arduino Core จะใช้การกำหนดเงื่อนไข TICK_CUSTOM ในไฟล์ lv_conf.h และวนเรียกใช้คำสั่ง lv_timer_handler() เพื่อกระตุ้นให้ LVGL อัพเดทสถานะการแสดงผล

ในกรณีบอร์ด ATD3.5-S3 มีข้อแตกต่างที่ไม่สามารถใช้โค้ดตัวอย่างของ LVGL ได้ตรงๆ เนื่องจากชิพตรวจจับการสัมผัสคือ FT6336U เชื่อมต่อผ่าน I2C จึงไม่สามารถเรียกใช้ API ของ TFT_eSPI ได้ ผมเลือกติดตั้งไลบรารี Adafruit_FT6206 แทน เพื่อใช้อ่านค่าตำแหน่งการสัมผัส แต่ก็ต้องปรับแต่งระบบแกนอ้างอิงให้สอดคล้องกับ LVGL ขั้นตอนในการขยาย project ของ TFT_eSPI ในหัวข้อก่อนหน้าให้รองรับ LVGL มีดังนี้

  1. ปรับปรุงไฟล์ platformio.ini โดยเพิ่มไลบรารี LVGL และ Adafruit_FT6206 และเพิ่ม LV_CONF_INCLUDE_SIMPLE ในส่วน build_flags
  2. สร้างไฟล์ lv_conf.h โดยใช้ต้นแบบจากไฟล์ lv_conf_template.h จากนั้นแก้ไขเงื่อนไข TICK_CUSTOM
  3. แก้ไขโค้ดตัวอย่างในส่วนของฟังก์ชัน callback ของการตรวจจับการสัมผัสให้อ่านค่าด้วย API ของไลบรารี Adafruit_FT6206 และแปลงพิกัดให้สอดคล้องกับ LVGL
Adafruit_FT6206 touch_ctrler = Adafruit_FT6206();

void setup() {
...
// initialize touch screen
Wire.begin(SDA_PIN, SCL_PIN);
touch_ctrler.begin();
}

// Callback function for LVGL to read touchpad
void lcd_touchpad_read( lv_indev_drv_t * indev_drv, lv_indev_data_t * data ) {
if( touch_ctrler.touched() ) {
TS_Point point = touch_ctrler.getPoint(0);
data->state = LV_INDEV_STATE_PRESSED;
data->point.x = point.y; // Convert coordinate system
data->point.y = 320 - point.x;
} else {
data->state = LV_INDEV_STATE_RELEASED;
}
}

การทดสอบฟีเจอร์ของไลบรารี LVGL รุ่น 8.4.0 ใช้การสร้างปุ่มกดโดยผูกฟังก์ชัน callback ให้แสดงข้อความเมื่อปุ่มถูกกด การทดสอบก็ทำงานได้

void setup() {
...
// Create a button
lv_obj_t *btn = lv_btn_create(lv_scr_act()); /*Add a button to the current screen*/
lv_obj_set_pos(btn, 50, 50); /*Set its position*/
lv_obj_set_size(btn, 100, 50);
lv_obj_add_event_cb(btn, btn_click_handler, LV_EVENT_ALL, NULL);
lv_obj_t *label = lv_label_create(btn); /*Add a label to the button*/
lv_label_set_text(label, "Button"); /*Set the labels text*/
lv_obj_center(label); /*Align the label to the center*/
}

// Callback function for button click
void btn_click_handler(lv_event_t * ev) {
if (ev->code == LV_EVENT_CLICKED) {
ESP_LOGI(TAG, "Button clicked");
}
}
สถานะปุ่มกดที่ถูกกด

ด่านสาม LVGL 9.1.0

TBD

終わりに (ในตอนท้าย)

TBD

--

--

Supachai Vorapojpisut
Supachai Vorapojpisut

Written by Supachai Vorapojpisut

Assistant Professor at Thammasat University

No responses yet