ข้ามไปยังเนื้อหา

หน้าจอ LVGL แรก: โลโก้ หัวเรื่อง และคำบรรยาย

  1. build และ flash episode นี้ลง TESAIoT Dev Kit แล้วได้หน้าจอตรงกับภาพตัวอย่าง
  2. อธิบายว่า master template เรียก example_main(parent) เมื่อไร และทำไมเราไม่เขียน main เอง
  3. สร้าง object tree screen → image → label และจัดวางด้วย align ได้

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

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 ไปฟอนต์อื่นเงียบ ๆ

บอร์ดนี้ไม่มีไฟล์ระบบหรือ 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 ใด ๆ

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

  • ใช้ 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
Terminal window
# ในโฟลเดอร์ master template (ดูบทเรียน 1.1)
# 1) ลบไฟล์ของ episode เก่าใน proj_cm55/apps/
# 2) คัดลอกไฟล์ทั้งหมดของ episode นี้ลงใน proj_cm55/apps/
make build
make program # flash ผ่าน KitProg3

หรือเปิด ตัวอย่างนี้บน Developer Hub แล้ว flash เฟิร์มแวร์สำเร็จรูป

หน้าจอของ EP01 — Basic Label บน TESAIoT Dev Kit

ก่อนอ่านโค้ด ให้ทายว่าหน้าจอนี้มี object อะไรบ้าง และอะไรเปลี่ยนเมื่อผู้ใช้แตะหรือเมื่อค่าเซนเซอร์เปลี่ยน

  1. ทาย ก่อนแก้: เลือกค่าหนึ่งค่าที่ README ของตัวอย่างอธิบายไว้ในส่วน How แล้วเขียนว่าจะเห็นอะไรเปลี่ยนบนจอหรือใน log
  2. แก้และรัน build + flash แล้วเทียบกับที่ทายไว้ ถ้าไม่ตรง ให้หาว่าเข้าใจส่วนไหนผิด
  3. ทำเพิ่ม ต่อยอดหนึ่งอย่างที่ตัวอย่างยังไม่มี แล้วเก็บภาพหรือวิดีโอไว้ใน portfolio
  • master template ทำอะไรให้เราแล้วบ้างก่อนเรียก example_main()
  • ทำไมโลโก้ถูกฝังเป็น C array (APP_LOGO) แทนการโหลดจากไฟล์
  • ถ้าอยากย้าย title ลงอีก 20 px ต้องแก้ค่าใดในโค้ด

คำตอบอยู่ใน README ของตัวอย่างและในโค้ด ถ้าตอบข้อใดไม่ได้ ให้กลับไปอ่านส่วน Why / What / How อีกครั้ง

คำถามทบทวน

ลองตอบเองก่อน แล้วค่อยเปิดดูเฉลย

  1. master template เรียก example_main(parent) เมื่อไร (เป้าหมายข้อ 2)

    1. ทุกรอบของ lv_timer_handler() เพื่อให้ episode วาดจอใหม่
    2. ครั้งเดียว ภายใน cm55_gfx_task หลังเตรียม VGLite, LVGL, จอ และ touch เสร็จ
    3. ก่อนเริ่ม FreeRTOS scheduler เพื่อให้ episode ตั้งค่า clock เอง
    4. ทุกครั้งที่ผู้ใช้แตะจอ
    ดูเฉลย

    คำตอบ: 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

  2. ทำไม episode เขียนแค่ example_main() แทนการเขียน main() เอง (เป้าหมายข้อ 2)

    1. เพราะ ModusToolbox ไม่อนุญาตให้โปรแกรมภาษา C มี main()
    2. เพราะ main() ต้องรันบน CM33 ส่วน episode รันบน CM55
    3. เพราะ example_main() ทำงานเร็วกว่า main()
    4. เพราะ 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)

  3. ถ้าเปลี่ยน APP_LOGO เป็นรูปที่สูงกว่าเดิม 40 px โดยไม่แก้ตัวเลขอื่นในโค้ด หน้าจอจะเป็นอย่างไร (เป้าหมายข้อ 3)

    1. title และ subtitle เลื่อนลงตามโลโก้ ระยะห่าง 48 px และ 24 px ยังเท่าเดิม
    2. title ทับโลโก้ เพราะ title ถูกวางที่พิกัด y คงที่
    3. เฉพาะ title เลื่อนลง ส่วน subtitle อยู่ที่เดิมจึงทับ title
    4. โลโก้ถูกย่อให้พอดีพื้นที่เดิมโดยอัตโนมัติ
    ดูเฉลย

    คำตอบ: 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 ในโค้ดเขียนไว้ว่าทำเพื่อกันการทับเมื่อขนาดโลโก้เปลี่ยน

  4. ต้องการย้าย title ลงอีก 20 px จึงเปลี่ยน y offset ใน lv_obj_align_to ของ title จาก 48 เป็น 68 ข้อใดจะเกิดขึ้น (เลือกทุกข้อที่ถูก) (เป้าหมายข้อ 3)

    1. โลโก้เลื่อนลง 20 px ด้วย
    2. title เลื่อนลง 20 px
    3. subtitle เลื่อนลง 20 px ตาม title
    4. ระยะระหว่าง 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 จึงไม่ขยับ

  5. ลองเปลี่ยนฟอนต์ของ title เป็น lv_font_montserrat_48 แล้ว build ขึ้น error ว่า lv_font_montserrat_48 undeclared สาเหตุคืออะไร (เป้าหมายข้อ 1)

    1. ต้อง include ไฟล์ฟอนต์ใน main_example.c เอง
    2. ต้องรัน make getlibs ใหม่เพื่อดาวน์โหลดฟอนต์
    3. master เปิดฟอนต์ Montserrat ไว้เฉพาะบางขนาดระหว่าง 12–40 ขนาด 48 ต้องเปิด LV_FONT_MONTSERRAT_48 ใน lv_conf.h ก่อน
    4. ฟอนต์ที่ใหญ่กว่า 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 ฉบับเต็ม

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

เนื้อหาเผยแพร่ภายใต้ CC BY-NC 4.0 นำไปใช้ต่อในงานที่ไม่ใช่เพื่อการค้าได้ โปรดอ้างอิงสมาคมสมองกลฝังตัวไทย (TESA) ทุกครั้ง · วิธีอ้างอิง TESA