RTT-GUI: เฟรมเวิร์กกราฟิกประสิทธิภาพสูงสำหรับระบบฝังตัวบน Arduino
RTT-GUI เป็นโอเพนซอร์ซ GUI Framework ที่ออกแบบมาเพื่อระบบฝังตัว (Embedded Systems) โดยเฉพาะ โปรเจกต์นี้ได้รับการพัฒนาต่อยอด (Fork) มาจาก RT-Thread Package -- GUI Engine และผ่านการปรับปรุงโครงสร้างใหม่ (Refactoring) อย่างหนักเพื่อให้สามารถใช้งานร่วมกับสภาพแวดล้อมของ Arduino ได้อย่างมีประสิทธิภาพสูงสุด โดยเน้นที่ความยืดหยุ่นและการจัดการทรัพยากรที่มีจำกัดบนไมโครคอนโทรลเลอร์
สถาปัตยกรรมของระบบ (Architecture)

หัวใจสำคัญของ RTT-GUI คือการทำงานอยู่บนเลเยอร์ของ Arduino RT-Thread library ซึ่งเป็นระบบปฏิบัติการแบบเรียลไทม์ (RTOS) โดย RT-Thread จะทำหน้าที่จัดการคิวงานและทรัพยากรพื้นฐาน ในขณะที่ RTT-GUI จะรับหน้าที่จัดการส่วนติดต่อผู้ใช้ผ่านทาง Low-level Device Drivers ที่เชื่อมโยงกับฮาร์ดแวร์กราฟิก
โครงสร้างนี้ช่วยให้การพอร์ต (Porting) ไปยังหน้าจอหรือบอร์ดรุ่นต่างๆ ทำได้ง่ายผ่านการนิยามฟังก์ชันมาตรฐานสองส่วนหลัก คือ Common Device Operations และ Graphic Device Operations:
/* common device operations (RT-Thread: "rtdef.h") */
struct rt_device_ops {
rt_err_t (*init)(rt_device_t dev);
rt_err_t (*open)(rt_device_t dev, rt_uint16_t oflag);
rt_err_t (*close)(rt_device_t dev);
rt_size_t (*read)(rt_device_t dev, rt_off_t pos, void *buffer, rt_size_t size);
rt_size_t (*write)(rt_device_t dev, rt_off_t pos, const void *buffer, rt_size_t size);
rt_err_t (*control)(rt_device_t dev, int cmd, void *args);
};
ในส่วนของ rt_device_ops จะเป็นอินเตอร์เฟซมาตรฐานของ RT-Thread สำหรับการเข้าถึงทรัพยากร เช่น การเปิด/ปิดหน้าจอ หรือการเขียนข้อมูลลงหน่วยความจำวิดีโอ (Frame Buffer)
/* graphic device operations (RTT-GUI: "driver.h") */
struct rtgui_graphic_driver_ops {
void (*set_pixel)(rtgui_color_t *c, int x, int y);
void (*get_pixel)(rtgui_color_t *c, int x, int y);
void (*draw_hline)(rtgui_color_t *c, int x1, int x2, int y);
void (*draw_vline)(rtgui_color_t *c, int x , int y1, int y2);
void (*draw_raw_hline)(rt_uint8_t *pixels, int x1, int x2, int y);
};
ส่วน rtgui_graphic_driver_ops จะเน้นไปที่การควบคุมพิกเซลบนหน้าจอโดยเฉพาะ เช่น การวาดจุด (set_pixel) หรือการวาดเส้นตรงในแนวตั้งและแนวนอน ซึ่งเป็นพื้นฐานสำคัญในการเรนเดอร์รูปทรงที่ซับซ้อนขึ้น
ฟังก์ชันกราฟิกพื้นฐาน (Graphic Functions)
RTT-GUI มาพร้อมกับไลบรารีการวาดภาพที่ครอบคลุม ตั้งแต่จุดพื้นฐานไปจนถึงรูปทรงเรขาคณิตที่ซับซ้อน:
/* graphic functions (RTT-GUI: "dc.h") */
void rtgui_dc_draw_point(rtgui_dc_t *dc, int x, int y);
void rtgui_dc_draw_vline(rtgui_dc_t *dc, int x, int y1, int y2);
void rtgui_dc_draw_hline(rtgui_dc_t *dc, int x1, int x2, int y);
void rtgui_dc_draw_line(rtgui_dc_t *dc, int x1, int y1, int x2, int y2);
void rtgui_dc_draw_rect(rtgui_dc_t *dc, rtgui_rect_t *rect);
void rtgui_dc_fill_rect(rtgui_dc_t *dc, rtgui_rect_t *rect);
void rtgui_dc_draw_round_rect(rtgui_dc_t *dc, rtgui_rect_t *rect, int r);
void rtgui_dc_fill_round_rect(rtgui_dc_t *dc, rtgui_rect_t *rect, int r);
void rtgui_dc_draw_annulus(rtgui_dc_t *dc, rt_int16_t x, rt_int16_t y, rt_int16_t r1, rt_int16_t r2, rt_int16_t start, rt_int16_t end);
void rtgui_dc_draw_pie(rtgui_dc_t *dc, rt_int16_t x, rt_int16_t y, rt_int16_t r, rt_int16_t start, rt_int16_t end);
void rtgui_dc_fill_pie(rtgui_dc_t *dc, rt_int16_t x, rt_int16_t y, rt_int16_t r, rt_int16_t start, rt_int16_t end);
void rtgui_dc_draw_polygon(rtgui_dc_t *dc, const int *vx, const int *vy, int count);
void rtgui_dc_fill_polygon(rtgui_dc_t *dc, const int *vx, const int *vy, int count);
void rtgui_dc_draw_circle(rtgui_dc_t *dc, int x, int y, int r);
void rtgui_dc_fill_circle(rtgui_dc_t *dc, rt_int16_t x, rt_int16_t y, rt_int16_t r);
void rtgui_dc_draw_arc(rtgui_dc_t *dc, rt_int16_t x, rt_int16_t y, rt_int16_t r, rt_int16_t start, rt_int16_t end);
void rtgui_dc_draw_ellipse(rtgui_dc_t *dc, rt_int16_t x, rt_int16_t y, rt_int16_t rx, rt_int16_t ry);
void rtgui_dc_fill_ellipse(rtgui_dc_t *dc, rt_int16_t x, rt_int16_t y, rt_int16_t rx, rt_int16_t ry);
ในการใช้งานฟังก์ชันเหล่านี้ พารามิเตอร์ตัวแรกคือ Device Context (DC) ซึ่งเปรียบเสมือน "บริบทของการวาด" ที่เก็บข้อมูลสถานะปัจจุบัน เช่น สี และขอบเขตการวาด คุณสามารถเริ่มการวาดได้ด้วย rtgui_dc_begin_drawing()
สำหรับการกำหนดสี สามารถทำได้ผ่านมาโคร RTGUI_DC_FC() ซึ่งเป็นการเปลี่ยนสี Foreground (สีหลัก) ของวิดเจ็ตเป้าหมาย:
RTGUI_DC_FC(dc) = GREEN;
สิ่งสำคัญที่สุดคือเมื่อทำการวาดเสร็จสิ้น จะต้องเรียกใช้ rtgui_dc_end_drawing() เพื่อคืนทรัพยากรและสั่งให้ระบบทำการเรนเดอร์ (Render) ข้อมูลลงหน้าจอจริง
/* device context functions (RTT-GUI: "dc.h") */
rtgui_dc_t *rtgui_dc_begin_drawing(rtgui_widget_t *owner);
void rtgui_dc_end_drawing(rtgui_dc_t *dc, rt_bool_t update);
- rtgui_dc_begin_drawing: รับ Pointer ของวิดเจ็ตที่ต้องการวาดลงไป เพื่อสร้าง Context
- rtgui_dc_end_drawing: รับ Pointer ของ DC และพารามิเตอร์ Boolean เพื่อระบุว่าต้องการ Force Update (บังคับรีเฟรชหน้าจอทันที) หรือไม่
การจัดการข้อความ (Text Functions)
RTT-GUI รองรับการแสดงผลตัวอักษร ASCII และอักษรจีนตัวย่อ (Unicode) โดยมีขนาดฟอนต์ให้เลือกสองขนาดคือ 12 และ 16 พิกเซล (asc12, asc16, hz12 และ hz16)
เพื่อให้การใช้งานหน่วยความจำ Flash ของไมโครคอนโทรลเลอร์เป็นไปอย่างคุ้มค่า หากโปรเจกต์ของคุณมีการเชื่อมต่อ SD Card แนะนำให้เก็บไฟล์ฟอนต์ไว้ใน SD Card (โดยการตั้งค่า CONFIG_USING_FONT_FILE == 1) โดยปกติ RTT-GUI จะมองหาฟอนต์ในโฟลเดอร์ /font/ โดยอัตโนมัติ
/* font config (RTT-GUI: "guiconfig.h") */
#define CONFIG_USING_FONT_12 (0)
#define CONFIG_USING_FONT_16 (1)
#define CONFIG_USING_FONT_HZ (0)
#define CONFIG_USING_FONT_FILE (1)
การวาดข้อความทำได้ง่ายๆ ผ่านฟังก์ชัน:
void rtgui_dc_draw_text(rtgui_dc_t *dc, const char *text, rtgui_rect_t *rect);
การจัดการรูปภาพ (Image Functions)
หนึ่งในจุดเด่นของ RTT-GUI คือความสามารถในการถอดรหัสรูปภาพยอดนิยมได้หลากหลายรูปแบบ โดยใช้ไลบรารีประสิทธิภาพสูงที่ปรับแต่งมาเพื่อ Embedded System:
- BMP: รูปภาพบิตแมปมาตรฐาน
- JPEG: รองรับผ่านโปรเจกต์ ChaN's TJpgDec ซึ่งใช้แรมในการถอดรหัสน้อยมาก
- PNG: รองรับในขั้นทดลอง (Experimental) ผ่าน Lode Vandevenne's LodePNG
- XPM: รูปแบบภายในของ RTT-GUI สำหรับไอคอนขนาดเล็ก
คุณสามารถเลือกเปิดหรือปิดการใช้งานตัวถอดรหัส (Decoder) แต่ละตัวได้ในไฟล์คอนฟิกเพื่อประหยัดพื้นที่โค้ด:
/* image decoder config (RTT-GUI: "guiconfig.h") */
#define CONFIG_USING_IMAGE_XPM (1)
#define CONFIG_USING_IMAGE_BMP (1)
#define CONFIG_USING_IMAGE_JPEG (1)
#define CONFIG_USING_IMAGE_PNG (0)
สำหรับการโหลดภาพ สามารถเลือกโหลดจากไฟล์ใน SD Card หรือโหลดจากหน่วยความจำโดยตรง (MemBuffer):
rtgui_image_t *rtgui_image_create_from_file(const char *type, const char *fn, rt_int32_t scale, rt_bool_t load);
rtgui_image_t *rtgui_image_create_from_mem(const char *type, const rt_uint8_t *data, rt_size_t size, rt_int32_t scale, rt_bool_t load);
void rtgui_image_blit(rtgui_image_t *image, rtgui_dc_t *dc, rtgui_rect_t *rect);
กลไกการทำงานของ GUI (GUI Functions)
RTT-GUI ใช้สถาปัตยกรรมแบบ Client-Server โดย Server Thread จะทำงานอยู่เบื้องหลังเพื่อทำหน้าที่ส่งต่อเหตุการณ์ (Dispatch Events) ไปยัง Client ซึ่งก็คือบรรดาวิดเจ็ตต่างๆ ระบบการทำงานจะเหมือนกับ UI Framework ระดับสูง ที่เหตุการณ์จะถูกส่งจากวิดเจ็ตชั้นบนสุด (เช่น Main Window) ลงไปยังวิดเจ็ตลูกที่ลึกที่สุด จนกว่าจะมีตัวใดตัวหนึ่งจัดการเหตุการณ์นั้น (Event Handling)
ขั้นตอนการสร้างแอปพลิเคชัน GUI เริ่มจากการประกาศอินสแตนซ์ของแอป:
CREATE_APP_INSTANCE(app, RT_NULL, "Demo App");
ฟังก์ชันนี้จะเชื่อมโยงแอปพลิเคชันเข้ากับ Thread ปัจจุบัน ข้อควรระวังคือ 1 Thread สามารถมีแอป GUI ได้เพียง 1 อินสแตนซ์เท่านั้น
จากนั้นสร้างหน้าต่างหลัก (Main Window):
main_win = CREATE_MAIN_WIN(win_event_handler, "Demo Window", RTGUI_WIN_STYLE_DEFAULT);
ในฟังก์ชัน win_event_handler เราจะจัดการเหตุการณ์ที่สนใจ เช่น PAINT (การวาดหน้าจอ) และปล่อยให้เหตุการณ์อื่นๆ ถูกจัดการโดย Default Handler:
rt_bool_t win_event_handler(void *obj, rtgui_evt_generic_t *evt) {
rt_bool_t done = RT_FALSE;
if (DEFAULT_HANDLER(obj)) {
done = DEFAULT_HANDLER(obj)(obj, evt);
}
if (IS_EVENT_TYPE(evt, PAINT)) {
done = show_demo(TO_WIN(obj));
}
return done;
}
การเพิ่มวิดเจ็ตอย่าง Label หรือ Button สามารถทำได้ด้วยฟังก์ชันตระกูล CREATE_..._INSTANCE:
label = CREATE_LABEL_INSTANCE(main_win, RT_NULL, RT_NULL, "Label Demo");
WIDGET_TEXTALIGN_SET(label, CENTER_HORIZONTAL);
btn = CREATE_BUTTON_INSTANCE(main_win, RT_NULL, NORMAL, "Button Demo");
เพื่อให้การจัดวางองค์ประกอบง่ายขึ้น RTT-GUI มีระบบ Sizer (Box) เพื่อช่วยจัดตำแหน่งอัตโนมัติ ไม่ต้องกำหนดพิกเซลเองทุกตัว:
sizer = CREATE_BOX_INSTANCE(main_win, RTGUI_HORIZONTAL, 0);
WIDGET_ALIGN_SET(label, STRETCH);
rtgui_box_layout(sizer);
ขั้นตอนสุดท้ายคือการรันแอปพลิเคชัน:
rtgui_win_show(main_win, RT_FALSE);
rtgui_app_run(app);
ฟังก์ชัน rtgui_app_run จะรัน Event Loop และไม่ส่งคืนค่า (Block) จนกว่าหน้าต่างหลักจะถูกปิด
การออกแบบหน้าจอแบบไดนามิก (Dynamic Design)
นอกจากการเขียนโค้ดเพื่อสร้าง UI แล้ว RTT-GUI ยังรองรับการใช้สคริปต์ลักษณะคล้าย JSON เพื่อกำหนดโครงสร้างหน้าจอ ช่วยให้การแยกส่วน Logic และ Design ทำได้ง่ายขึ้น:
APP: {
PARAM: { "Demo App", NULL },
MWIN: {
PARAM: { "Demo Window", hdl },
BOX: { PARAM: { 1, 0 }, ID: 0 },
LABEL: {
PARAM: { NULL, NULL, "Label Demo" },
TEXTALIGN: 0x01, ALIGN: 0x20
}
}
}
ในฝั่งของ Arduino Sketch เราสามารถใช้ rtgui_design_parse() เพื่อแปลงสคริปต์นี้ (ไม่ว่าจะเก็บใน String หรือไฟล์ใน SD Card) ให้กลายเป็นอ็อบเจกต์ GUI จริงๆ ซึ่งช่วยประหยัดเวลาในการแก้โค้ดและคอมไพล์ใหม่เมื่อต้องการเปลี่ยนเพียงแค่ตำแหน่งวิดเจ็ต
ฟังก์ชันการจับภาพหน้าจอ (Capture Screen)
หากคุณเปิดใช้งานการสนับสนุน BMP (CONFIG_USING_IMAGE_BMP == 1) คุณจะสามารถใช้ฟังก์ชัน screenshot() เพื่อบันทึกภาพหน้าจอที่แสดงผลอยู่ปัจจุบันลงใน SD Card ได้ทันที ซึ่งมีประโยชน์มากสำหรับการทำคู่มือหรือการดีบัก โดยสามารถสั่งการผ่าน Shell (FinSH หรือ MSH) ได้โดยตรง:
msh />prtscn demo.bmp
ตัวอย่างภาพที่ได้จากการบันทึกหน้าจอ:
ฮาร์ดแวร์ที่รองรับ (Supported Hardware)
ปัจจุบัน RTT-GUI รองรับฮาร์ดแวร์มาตรฐานที่นิยมใช้ในวงการ Maker ดังนี้:
1. Adafruit 2.8" TFT Touch Shield v2
- หน้าจอ ILI9341 (SPI) และระบบสัมผัส FT6206 (I2C)
#define CONFIG_USING_ADAFRUIT_TFT_CAPACITIVE
#define CONFIG_GUI_DEVICE_NAME "ILI9341"
#define CONFIG_TOUCH_DEVICE_NAME "FT6206"
2. TinyCircuits Pocket Arcade
- หน้าจอ SSD1331 OLED (SPI) พร้อมปุ่มกดในตัว
#define CONFIG_USING_TINYSCREEN
#define CONFIG_GUI_DEVICE_NAME "SSD1331"
#define CONFIG_KEY_DEVICE_NAME "BTN"
3. Generic 0.96" OLED module
- หน้าจอ SSD1306 (SPI) สำหรับการแสดงผลขาวดำ (Monochrome)
#define CONFIG_USING_SSD1306_SPI4
#define CONFIG_GUI_DEVICE_NAME "SSD1306"
#define CONFIG_USING_MONO (1)
ตัวอย่างการใช้งาน (Examples)
นอกจากเดโมพื้นฐานแล้ว ยังมีโปรเจกต์ตัวอย่างที่ซับซ้อนขึ้น เช่น "Pic Show" สำหรับเปิดดูรูปภาพ และ "File Browser" สำหรับสำรวจไฟล์ใน SD Card ซึ่งมีให้ศึกษาทั้งแบบเขียนโค้ดสดและแบบใช้สคริปต์ Dynamic Design
ขั้นตอนต่อไป (Next Steps)
เพื่อความเข้าใจที่ลึกซึ้งยิ่งขึ้นเกี่ยวกับการทำงานของระบบพื้นฐาน คุณสามารถศึกษาเพิ่มเติมได้จากหัวข้อเหล่านี้:
- Multitasking on Arduino - เรียนรู้ระบบ Threading ที่ขับเคลื่อน GUI
- A better SD library with RT-Thread - การจัดการไฟล์รูปภาพและฟอนต์
- Arduino App with RT-Thread - การสร้างแอปพลิเคชันแบบเต็มรูปแบบบน RTOS