หน้าจอ LVGL แรก: โลโก้ หัวเรื่อง และคำบรรยาย
เป้าหมาย
หัวข้อที่มีชื่อว่า “เป้าหมาย”- build และ flash episode นี้ลง TESAIoT Dev Kit แล้วได้หน้าจอตรงกับภาพตัวอย่าง
- อธิบายว่า master template เรียก example_main(parent) เมื่อไร และทำไมเราไม่เขียน main เอง
- สร้าง object tree screen → image → label และจัดวางด้วย align ได้
LVGL widget คือ lv_obj_t * ทั้งหมด
หัวข้อที่มีชื่อว่า “LVGL widget คือ lv_obj_t * ทั้งหมด”LVGL (Light and Versatile Graphics Library) แทน widget ทุกชนิด — screen, image, label, ปุ่ม, เมนู — ด้วย struct
เดียวกันคือ lv_obj_t เราจึงสร้าง เปลี่ยน style และจัดวาง widget ทุกชนิดด้วย API ชุดเดียวกันไม่ว่าจะเป็น object
แบบไหน lv_screen_active() (หรือชื่อเดิม lv_scr_act()) คืนค่า lv_obj_t * ของหน้าจอที่กำลังแสดงอยู่ ใช้เป็น
parent เมื่อสร้าง widget ตัวแรกของ episode
object tree: screen → image → label
หัวข้อที่มีชื่อว่า “object tree: screen → image → label”EP01 สร้าง widget สามชิ้นเรียงกันเป็นต้นไม้ที่มี screen เป็นราก คือ screen (พื้นหลัง) → logo (image, ลูกของ
screen) → title และ subtitle (label สองตัว) การจัดวางใช้สองฟังก์ชันที่ความหมายต่างกัน — lv_obj_align(obj, align, x, y) จัดวาง obj เทียบกับ parent ของมันเอง ส่วน lv_obj_align_to(obj, target, align, x, y) จัดวาง
obj เทียบกับ widget อื่น (target) เป็น anchor เช่น title ถูกวางใต้ logo ด้วย LV_ALIGN_OUT_BOTTOM_MID
ไม่ใช่ใต้ screen ตรง ๆ — ถ้าขนาดโลโก้เปลี่ยน title ก็จะเลื่อนตามโดยอัตโนมัติ เพราะ anchor คือ widget ไม่ใช่ตำแหน่ง
พิกัดตายตัว ระยะ offset ที่ใช้คือ 24 px (โลโก้ห่างขอบบน), 48 px (title ใต้โลโก้) และ 24 px (subtitle ใต้ title)
สีและฟอนต์: หน่วยที่โค้ดใช้จริง
หัวข้อที่มีชื่อว่า “สีและฟอนต์: หน่วยที่โค้ดใช้จริง”สีทุกจุดตั้งด้วย lv_color_hex(0xRRGGBB) แปลงเลขฐานสิบหกเป็น lv_color_t — พื้นหลังใช้ 0x0F172A (slate-900
จาก Tailwind palette) กับ lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, LV_PART_MAIN) เพื่อให้พื้นทึบแสงเต็ม
(LV_OPA_COVER = ค่าความทึบสูงสุด) LV_PART_MAIN หมายถึงส่วนหลักของ widget ที่ style นี้จะไปลง ฟอนต์ต้องเป็น
Montserrat ที่ถูกเปิดไว้ใน lv_conf.h ของ master แล้วเท่านั้น (episode นี้ใช้ 30 px กับ title และ 20 px กับ
subtitle) เลือกขนาดที่ไม่ได้เปิดไว้จะ compile ไม่ผ่านหรือ fallback ไปฟอนต์อื่นเงียบ ๆ
ทำไมโลโก้ต้องฝังเป็น C array ไม่ใช่โหลดจากไฟล์
หัวข้อที่มีชื่อว่า “ทำไมโลโก้ต้องฝังเป็น C array ไม่ใช่โหลดจากไฟล์”บอร์ดนี้ไม่มีไฟล์ระบบหรือ SD card ให้ LVGL เปิดไฟล์ภาพตอน runtime รูปโลโก้จึงถูกแปลงล่วงหน้าเป็น
lv_image_dsc_t APP_LOGO — struct ที่เก็บทั้ง header (ขนาด, color format) และ pixel data ดิบ — คอมไพล์ฝังรวมเข้า
ไปใน flash image เดียวกับโปรแกรม lv_image_set_src(logo, &APP_LOGO) จึงรับ pointer ไปยังข้อมูลใน flash ตรง ๆ
ไม่มีการอ่านไฟล์ ไม่มี latency ของ I/O และไม่มี dependency กับ filesystem ใด ๆ
สัญญาการเข้า-ออกของ episode
หัวข้อที่มีชื่อว่า “สัญญาการเข้า-ออกของ episode”master template (ดูบทเรียน 1.1) เรียก example_main(lv_scr_act()) เพียงครั้งเดียวหลัง FreeRTOS, display driver,
VGLite GPU และ LVGL พร้อมหมดแล้ว main_example.c ของ episode นี้ implement example_main() แบบ strong แล้ว
forward ต่อไปที่ ui_ep01_basic_label_create() ทันที โดยไม่ใช้ค่า parent ที่ได้รับมาตรง ๆ (มี (void)parent;
กำกับไว้) เพราะฟังก์ชันสร้าง UI เรียก lv_screen_active() เองภายใน — ออกแบบไว้แบบนี้เพื่อให้ episode อื่น copy
โค้ดสร้าง UI ไปใช้ได้โดยไม่ต้องแก้ signature นอกจากนี้ main_example.c ยังเรียก
tesaiot_add_thai_support_badge() ก่อนสร้างหน้าจอของ episode — เป็นฟังก์ชันช่วยของ master ที่ยืนยันว่าไฟล์ฟอนต์
Noto Sans Thai ถูก bundle มาพร้อมแล้ว ไม่ได้เป็นส่วนหนึ่งของ UI tree ที่ EP01 สอน หลัง example_main() return
master จะวน lv_timer_handler() ให้ตลอดไปเพื่อ redraw — EP01 ไม่มี event หรือ timer ของตัวเอง จึงวาดครั้งเดียว
แล้วค้างภาพนั้นไว้
ตัวอย่างสมบูรณ์
หัวข้อที่มีชื่อว่า “ตัวอย่างสมบูรณ์”โค้ดของ episode นี้อยู่ใน Developer Hub (อ้างอิงที่ commit 9a8e3ed) อ่าน Why / What / How ฉบับเต็มก่อนใน README ของ episode โค้ดตัวอย่างด้านล่างคัดลอกจาก tesaiot/developer-hub (Apache-2.0) ที่ commit เดียวกัน
main_example.c — จุดที่ master ส่ง control เข้ามา:
void example_main(lv_obj_t *parent){ /* Master template bundles Noto Sans Thai fonts — this badge * confirms to the developer that Thai rendering is available. */ tesaiot_add_thai_support_badge();
(void)parent; /* The episode manages its own screen composition via lv_screen_active(). */
ui_ep01_basic_label_create();}ui_ep01_basic_label.c — สร้าง object tree ทั้งหมดของ episode:
lv_obj_t *screen = lv_screen_active();lv_obj_set_style_bg_color(screen, lv_color_hex(0x0F172A), LV_PART_MAIN);lv_obj_set_style_bg_opa(screen, LV_OPA_COVER, LV_PART_MAIN);
lv_obj_t *logo = lv_image_create(screen);lv_image_set_src(logo, &APP_LOGO);lv_obj_align(logo, LV_ALIGN_TOP_MID, 0, 24);
lv_obj_t *title = lv_label_create(screen);lv_label_set_text(title, "EP01 - Basic Label");lv_obj_set_style_text_color(title, lv_color_hex(0xF8FAFC), LV_PART_MAIN);lv_obj_set_style_text_font(title, &lv_font_montserrat_30, LV_PART_MAIN);lv_obj_align_to(title, logo, LV_ALIGN_OUT_BOTTOM_MID, 0, 48);สังเกต API ที่สำคัญ — lv_image_create(parent) สร้าง image widget ที่มี screen เป็น parent, lv_image_set_src()
รับ pointer ไปยัง lv_image_dsc_t ที่ฝังใน flash (ไม่ใช่ path ไฟล์), lv_obj_align() จัดวางเทียบกับ parent ส่วน
lv_obj_align_to() จัดวางเทียบกับ widget อื่นเป็น anchor — ไฟล์เต็มยังมี subtitle อีกหนึ่ง label ที่วางด้วย
รูปแบบเดียวกัน ให้ดูใน ไฟล์เต็มบน Developer Hub
ui_ep01_basic_label.h— ประกาศui_ep01_basic_label_create(void)ตัวเดียว
จุดที่มักพลาด
หัวข้อที่มีชื่อว่า “จุดที่มักพลาด”- ใช้
lv_obj_align()แทนlv_obj_align_to()— ถ้าอยากวาง title เทียบกับโลโก้ (ซึ่งอาจเปลี่ยนขนาด) ต้องใช้lv_obj_align_to(title, logo, ...)ไม่ใช่lv_obj_align(title, ...)ซึ่งจะจัดวางเทียบกับscreenแทน - ลืมเปิดขนาดฟอนต์ใน
lv_conf.h—lv_font_montserrat_30/_20ต้องถูกเปิดไว้ในโปรเจกต์ master ก่อน ถ้า episode เลือกขนาดที่ไม่ได้เปิดจะ build ไม่ผ่านหรือได้ฟอนต์อื่นแทนแบบไม่มี error ชัดเจน - คิดว่าต้องใช้
parentที่ได้รับมาตรง ๆ — โค้ดในmain_example.cใส่(void)parent;เพราะฟังก์ชันสร้าง UI เรียกlv_screen_active()เองข้างใน ทั้งสองค่าคือ active screen เดียวกัน แต่การไม่ใช้parentตรง ๆ ทำให้ episode พกไปวางในโปรเจกต์อื่นได้โดยไม่ต้องแก้ signature ของฟังก์ชันสร้าง UI
build และ flash
หัวข้อที่มีชื่อว่า “build และ flash”# ในโฟลเดอร์ master template (ดูบทเรียน 1.1)# 1) ลบไฟล์ของ episode เก่าใน proj_cm55/apps/# 2) คัดลอกไฟล์ทั้งหมดของ episode นี้ลงใน proj_cm55/apps/make buildmake program # flash ผ่าน KitProg3หรือเปิด ตัวอย่างนี้บน Developer Hub แล้ว flash เฟิร์มแวร์สำเร็จรูป
ดูของจริงก่อน
หัวข้อที่มีชื่อว่า “ดูของจริงก่อน”
ก่อนอ่านโค้ด ให้ทายว่าหน้าจอนี้มี object อะไรบ้าง และอะไรเปลี่ยนเมื่อผู้ใช้แตะหรือเมื่อค่าเซนเซอร์เปลี่ยน
- ทาย ก่อนแก้: เลือกค่าหนึ่งค่าที่ README ของตัวอย่างอธิบายไว้ในส่วน How แล้วเขียนว่าจะเห็นอะไรเปลี่ยนบนจอหรือใน log
- แก้และรัน build + flash แล้วเทียบกับที่ทายไว้ ถ้าไม่ตรง ให้หาว่าเข้าใจส่วนไหนผิด
- ทำเพิ่ม ต่อยอดหนึ่งอย่างที่ตัวอย่างยังไม่มี แล้วเก็บภาพหรือวิดีโอไว้ใน portfolio
เช็กความเข้าใจ
หัวข้อที่มีชื่อว่า “เช็กความเข้าใจ”- master template ทำอะไรให้เราแล้วบ้างก่อนเรียก example_main()
- ทำไมโลโก้ถูกฝังเป็น C array (APP_LOGO) แทนการโหลดจากไฟล์
- ถ้าอยากย้าย title ลงอีก 20 px ต้องแก้ค่าใดในโค้ด
คำตอบอยู่ใน README ของตัวอย่างและในโค้ด ถ้าตอบข้อใดไม่ได้ ให้กลับไปอ่านส่วน Why / What / How อีกครั้ง
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- README ของ episode · โฟลเดอร์โค้ด · commit
9a8e3ed - เปิดตัวอย่างนี้บน Developer Hub
- โค้ดเป็นของ Developer Hub และอ้างอิงด้วยลิงก์ ไม่ได้คัดลอกเข้าคลังนี้
คำถามทบทวน
ลองตอบเองก่อน แล้วค่อยเปิดดูเฉลย
-
master template เรียก example_main(parent) เมื่อไร (เป้าหมายข้อ 2)
- ทุกรอบของ lv_timer_handler() เพื่อให้ episode วาดจอใหม่
- ครั้งเดียว ภายใน cm55_gfx_task หลังเตรียม VGLite, LVGL, จอ และ touch เสร็จ
- ก่อนเริ่ม FreeRTOS scheduler เพื่อให้ episode ตั้งค่า clock เอง
- ทุกครั้งที่ผู้ใช้แตะจอ
ดูเฉลย
คำตอบ: B. ครั้งเดียว ภายใน cm55_gfx_task หลังเตรียม VGLite, LVGL, จอ และ touch เสร็จ
main.c ของ master เรียก lv_init(), lv_port_disp_init(), lv_port_indev_init() แล้วเรียก example_main(lv_scr_act()) ครั้งเดียว จากนั้นวนเรียก lv_timer_handler() ให้เอง episode จึงแค่สร้าง object tree แล้ว return
-
ทำไม episode เขียนแค่ example_main() แทนการเขียน main() เอง (เป้าหมายข้อ 2)
- เพราะ ModusToolbox ไม่อนุญาตให้โปรแกรมภาษา C มี main()
- เพราะ main() ต้องรันบน CM33 ส่วน episode รันบน CM55
- เพราะ example_main() ทำงานเร็วกว่า main()
- เพราะ main.c ของ master เตรียมบอร์ด FreeRTOS GPU LVGL จอ และ touch ไว้แล้ว และไม่เปลี่ยนเลยระหว่าง episode ทุก episode จึงเสียบเข้าที่จุดเดียวกันได้
ดูเฉลย
คำตอบ: D. เพราะ main.c ของ master เตรียมบอร์ด FreeRTOS GPU LVGL จอ และ touch ไว้แล้ว และไม่เปลี่ยนเลยระหว่าง episode ทุก episode จึงเสียบเข้าที่จุดเดียวกันได้
comment ใน main.c เขียนว่า main.c NEVER changes per episode และ episode ให้แค่ example_main(parent) ถ้าทุก episode ต้องเขียนการเตรียมฮาร์ดแวร์เองจะซ้ำและพังง่าย อีกทั้ง proj_cm55 มี main() ของ master อยู่แล้ว (CM55 มี main() ของตัวเอง ไม่ได้รันบน CM33)
-
ถ้าเปลี่ยน APP_LOGO เป็นรูปที่สูงกว่าเดิม 40 px โดยไม่แก้ตัวเลขอื่นในโค้ด หน้าจอจะเป็นอย่างไร (เป้าหมายข้อ 3)
- title และ subtitle เลื่อนลงตามโลโก้ ระยะห่าง 48 px และ 24 px ยังเท่าเดิม
- title ทับโลโก้ เพราะ title ถูกวางที่พิกัด y คงที่
- เฉพาะ title เลื่อนลง ส่วน subtitle อยู่ที่เดิมจึงทับ title
- โลโก้ถูกย่อให้พอดีพื้นที่เดิมโดยอัตโนมัติ
ดูเฉลย
คำตอบ: A. title และ subtitle เลื่อนลงตามโลโก้ ระยะห่าง 48 px และ 24 px ยังเท่าเดิม
title ใช้ lv_obj_align_to(title, logo, LV_ALIGN_OUT_BOTTOM_MID, 0, 48) และ subtitle ใช้ align_to กับ title อีกทอด ทั้งคู่จึงยึดตำแหน่งกับ object ที่อยู่เหนือตัวเอง ไม่ใช่พิกัดตายตัว comment ในโค้ดเขียนไว้ว่าทำเพื่อกันการทับเมื่อขนาดโลโก้เปลี่ยน
-
ต้องการย้าย title ลงอีก 20 px จึงเปลี่ยน y offset ใน lv_obj_align_to ของ title จาก 48 เป็น 68 ข้อใดจะเกิดขึ้น (เลือกทุกข้อที่ถูก) (เป้าหมายข้อ 3)
- โลโก้เลื่อนลง 20 px ด้วย
- title เลื่อนลง 20 px
- subtitle เลื่อนลง 20 px ตาม title
- ระยะระหว่าง title กับ subtitle ยังเป็น 24 px
ดูเฉลย
คำตอบ: B. title เลื่อนลง 20 px · C. subtitle เลื่อนลง 20 px ตาม title · D. ระยะระหว่าง title กับ subtitle ยังเป็น 24 px
offset 48 วัดจากขอบล่างของโลโก้ การเพิ่มเป็น 68 ย้ายเฉพาะ title ส่วน subtitle ถูก align_to กับ title จึงเลื่อนตามด้วยระยะ 24 px เดิม โลโก้ยึดกับ screen ด้วย LV_ALIGN_TOP_MID จึงไม่ขยับ
-
ลองเปลี่ยนฟอนต์ของ title เป็น lv_font_montserrat_48 แล้ว build ขึ้น error ว่า lv_font_montserrat_48 undeclared สาเหตุคืออะไร (เป้าหมายข้อ 1)
- ต้อง include ไฟล์ฟอนต์ใน main_example.c เอง
- ต้องรัน make getlibs ใหม่เพื่อดาวน์โหลดฟอนต์
- master เปิดฟอนต์ Montserrat ไว้เฉพาะบางขนาดระหว่าง 12–40 ขนาด 48 ต้องเปิด LV_FONT_MONTSERRAT_48 ใน lv_conf.h ก่อน
- ฟอนต์ที่ใหญ่กว่า 40 px ใช้กับจอ 4.3 นิ้วไม่ได้
ดูเฉลย
คำตอบ: C. master เปิดฟอนต์ Montserrat ไว้เฉพาะบางขนาดระหว่าง 12–40 ขนาด 48 ต้องเปิด LV_FONT_MONTSERRAT_48 ใน lv_conf.h ก่อน
README ของ master ระบุฟอนต์ที่เปิดไว้แล้วคือ 12, 14, 16, 18, 20, 22, 24, 28, 30, 40 และ EXTENDING.md ข้อ 4.1 บอกว่าขนาดอื่นต้องเปิดเพิ่มด้วย #define LV_FONT_MONTSERRAT_XX 1 ใน lv_conf.h README ของ EP01 เองก็เตือนไว้ในหัวข้อทดลองเปลี่ยนฟอนต์
อ้างอิงบทเรียนนี้
ถ้านำบทเรียนนี้ไปสอน ทำสไลด์ หรือทำเอกสารต่อ ให้อ้างอิงด้วยข้อความนี้ ถ้าดัดแปลงเนื้อหา ให้เติม (ดัดแปลง)ต่อท้ายชื่อบทเรียน
"หน้าจอ LVGL แรก: โลโก้ หัวเรื่อง และคำบรรยาย" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0
ข้อความอ้างอิงภาษาอังกฤษ: "First LVGL screen: logo, title and subtitle" from TESA Open Knowledge by the Thai Embedded Systems Association (TESA), https://github.com/tesaiot/tesa-qualification-program, licensed under CC BY-NC 4.0
ลิงก์บทเรียน: https://tesaiot.github.io/tesa-qualification-program/courses/tesaiot-firmware-stack/m02-hmi-menu-setting/l01-basic-label/
บทเรียนนี้ดัดแปลงจากต้นฉบับด้านล่าง เมื่ออ้างอิงให้คงเครดิตต้นฉบับไว้ด้วย
https://github.com/tesaiot/developer-hub/blob/9a8e3ed1d813bfd67fabf6b7ac15c6ff9750b465/hmi_ep01_basic_label · Code stays in the Developer Hub and is linked at pinned commits, never copied: the episodes, practice codes and main-branch examples are Apache-2.0; the master template and the OPTIGA client carry Infineon/Cypress EULAs.
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0
เนื้อหาเผยแพร่ภายใต้ CC BY-NC 4.0 นำไปใช้ต่อในงานที่ไม่ใช่เพื่อการค้าได้ โปรดอ้างอิงสมาคมสมองกลฝังตัวไทย (TESA) ทุกครั้ง · วิธีอ้างอิง TESA