เป้าหมายของหัวข้อนี้
ข้อกำหนดการเรียกใช้ของ IPC 3 ข้อที่ทุกหน้าบน CM55 พึ่งพา ได้แก่ ลำดับการตั้งค่า (จากบท B2), deferred binding (การผูกแบบเลื่อนเวลา) ของ container ที่ทำให้ IPC handler เขียนลงในหน้าที่มีอยู่จริง และ snapshot แบบมีผู้อ่านรายเดียวซึ่งป้อนข้อมูลให้ทุกหน้าทุก 33 ms พร้อมด้วยสำนวนการเขียน 2 แบบที่สร้างบน 3 ข้อนั้น คือสถานะ topbar ที่ตรวจจับด้วยขอบสัญญาณ และการสลับระหว่างคอนโซลกับ widget เมื่อจบบทนี้จะเขียนหน้าที่ผูกและยกเลิกการผูกได้อย่างถูกต้อง และรู้ว่าการเรียกแต่ละอย่างเรียกจากที่ใดได้บ้าง
ลำดับการทำงานจริงของเฟิร์มแวร์
ข้อกำหนดที่ 1 — บทนำเรื่องลำดับ
tesaiot_display.c:223-238 (คอมไพล์รวมอยู่ใน libbento_cm55.a ซอร์สอยู่ในต้นไม้ไฟล์ของชุดคิท): cm55_ipc_communication_setup() → ipc_sensorhub_init() → ipc_service_init() → (deepcraft_task_init() หากมี model-link) → … ขั้นต่าง ๆ ของจอแสดงผล … → ipc_only: → (void)ipc_lcd_init(NULL) → if (display_ok) (void)ipc_ui_init(NULL) บท B2 เดินตามลำดับนี้มาแล้ว ประเด็นตรงนี้คือ NULL 2 ตัว: handler ทั้ง 2 ตัว init ขึ้นมา โดยไม่มี container
ข้อกำหนดที่ 2 — deferred binding ของ container
หน้า Playground เป็นผู้ใช้คอนโซล LCD ของ IPC และตัวจัดการ widget ของ IPC หน้านี้ผูกตอนสร้าง โดยเรียงลำดับ ui ก่อนแล้วจึง lcd:
s_uxui_container = content;
s_terminal_container = content;
และ ต้องตั้งทั้งสองเป็น NULL ตอนทำลายหน้า โดยเรียงลำดับเดียวกัน:
void page_playground_destroy(void)
{
s_uxui_container = NULL;
s_terminal_container = NULL;
ipc_ui_set_container() เป็นการส่งต่อบรรทัดเดียวไปยัง ui_widget_mgr_set_parent() (ipc_ui.c:918-921) เหตุที่ NULL สำคัญ: หน้านั้นถูกรื้อทิ้งด้วย lv_screen_load_anim(..., auto_del) หาก IPC handler ยังถือ container เก่าไว้ คำสั่งถัดไปจาก CM33_NS จะเขียนลงในอ็อบเจ็กต์ LVGL ที่ปลดหน่วยความจำไปแล้ว — เป็น use-after-free ที่จะโผล่เป็น HardFault ในเวลาต่อมา ตอนที่มีคำสั่งอื่นซึ่งไม่เกี่ยวกันเข้ามา ui_widget_mgr_get_parent() เป็นค่าที่เป็น NULL ได้ก็เพื่อช่วงเวลานี้พอดี (ระหว่าง set_container(NULL) กับการผูกของหน้าถัดไป) และผู้เรียกทั้ง 5 จุดของฟังก์ชันนี้ตรวจ NULL ทุกจุด (ipc_ui.c:443,516-524,560,859)
สำนวนที่เกี่ยวข้องกันคือการรื้อก่อนสั่งเริ่มใหม่: ล้าง widget ทั้งหมด ก่อนที่คำสั่งเริ่มใหม่จะไปถึง CM33_NS เพื่อไม่ให้ฝั่งโน้นดันข้อมูลเข้ามาในตารางที่ล้างไปได้ครึ่งเดียว:
if (cmd == UXUI_CTRL_RESTART) {
send_ipc_cmd(IPC_CMD_RESTART_SCRIPT);
}
ข้อกำหนดที่ 3 — snapshot แบบมีผู้อ่านรายเดียว
ตัวจับเวลา 33 ms (ตั้งไว้ท้ายฟังก์ชัน sensorhub_ui_init ขั้นที่ 16 ของบท B2) เก็บ snapshot หนึ่งชุด ลงใน sensorhub_snapshot_t ที่อยู่บน stack แล้วกระจายต่อผ่าน pm_render():
static void sensorhub_timer_cb(lv_timer_t *timer)
{
(void)timer;
pm_render(&s_pm, &snap);
if (!s_pm.animating) {
pm_update_topbar(&s_pm);
}
}
ipc_sensorhub.h:66-67 ระบุว่า "Safe to call from any task context" — แต่ "Clears the 'changed' flags after reading." ดังนั้นต่อหนึ่ง tick จึงมี ผู้อ่านได้รายเดียว เท่านั้น: หน้าใดที่ไปเก็บ snapshot ของตัวเองภายใน render_cb จะแย่ง changed flag ไปจากทุกหน้าที่เหลือ หน้าต่าง ๆ รับ snapshot มาเป็นอาร์กิวเมนต์ ไม่ได้เรียก ipc_sensorhub_snapshot() เอง
สำนวน — สถานะ topbar ที่ตรวจจับด้วยขอบสัญญาณ
ipc_sensorhub_wifi_connected() ใช้การวนถาม (polling) ไม่มี API แบบ callback ให้ใช้ topbar เทียบกับ flag ปัจจุบันของ LVGL แล้วเพิ่มหรือล้างเฉพาะเมื่อมีการเปลี่ยนแปลง เพราะการเพิ่มหรือล้างทุก tick โดยไม่มีเงื่อนไขทำให้เห็นภาพกะพริบ:
if (pm->wifi_lbl) {
bool hidden = lv_obj_has_flag(pm->wifi_lbl, LV_OBJ_FLAG_HIDDEN);
if (connected && hidden) {
lv_obj_clear_flag(pm->wifi_lbl, LV_OBJ_FLAG_HIDDEN);
} else if (!connected && !hidden) {
lv_obj_add_flag(pm->wifi_lbl, LV_OBJ_FLAG_HIDDEN);
}
}
นาฬิกามีการกั้นด้วย ipc_sensorhub_ntp_synced() ก่อนจะอ่าน RTC เลยด้วยซ้ำ (ipc_sensorhub.h:99: เรียก Cy_RTC_GetDateAndTime ได้หลังการแจ้งเรื่อง NTP จาก CM33_NS เท่านั้น) ค่าจริงเป็นเงื่อนไขจำเป็นแต่ยังไม่เพียงพอ — ยังต้องตรวจช่วงค่าของแต่ละฟิลด์ และตรวจป้ายด้วย strcmp ก่อนเรียก lv_label_set_text เพื่อเลี่ยงการกะพริบของแถวการ์ด โดยทำที่ราว 6 fps และข้ามไปขณะกำลังเล่นภาพเคลื่อนไหว:
static const char * const dow[] = {
"", "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"
};
static const char * const mon[] = {
"", "Jan", "Feb", "Mar", "Apr", "May", "Jun",
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
};
cy_stc_rtc_config_t rtc;
Cy_RTC_GetDateAndTime(&rtc);
if (rtc.month >= 1 && rtc.month <= 12 &&
rtc.dayOfWeek >= 1 && rtc.dayOfWeek <= 7) {
char buf[32];
snprintf(buf, sizeof(buf), "%s %d %s %02d:%02d",
dow[rtc.dayOfWeek], (int)rtc.date,
mon[rtc.month], (int)rtc.hour, (int)rtc.min);
if (strcmp(lv_label_get_text(pm->time_lbl), buf) != 0) {
lv_label_set_text(pm->time_lbl, buf);
}
สำนวน — การสลับระหว่างคอนโซลกับ widget
ipc_lcd_toggle_panel() เป็น การสลับแบบตาบอด ไม่มีรูปแบบ set ที่เป็น idempotent ให้ใช้ ต้องกันด้วย ipc_lcd_is_panel_visible() เสมอ คอนโซลกับ widget ใช้ร่วมกันไม่ได้ ต้องเลือกอย่างใดอย่างหนึ่ง ลำดับของสองทางไม่สมมาตรกัน (ซ่อน widget แล้วแสดงคอนโซล แต่ซ่อนคอนโซลแล้วแสดง widget) ส่วนตัวนับข้อความที่ยังไม่อ่านจะล้างอย่างชัดแจ้งเฉพาะตอนเปลี่ยนเข้าสู่โหมดคอนโซลเท่านั้น:
static void console_toggle_cb(lv_event_t *e)
{
(void)e;
s_console_mode = !s_console_mode;
if (s_console_mode) {
if (s_console_badge) {
lv_obj_add_flag(s_console_badge, LV_OBJ_FLAG_HIDDEN);
}
}
} else {
}
}
if (s_console_btn_lbl) {
lv_label_set_text(s_console_btn_lbl,
s_console_mode ? LV_SYMBOL_EYE_OPEN : LV_SYMBOL_LIST);
}
}
ipc_lcd_has_unread() เป็นการอ่านล้วน ไม่ล้างค่า และมีความหมายเฉพาะขณะที่คอนโซลซ่อนอยู่ โดยวนถามจาก render hook ที่ทำงานทุก tick:
{
(void)snap;
if (s_console_badge && !s_console_mode) {
lv_obj_remove_flag(s_console_badge, LV_OBJ_FLAG_HIDDEN);
}
}
}
ทั้งหมดข้างต้นทำงานใน GFX-task context (callback ของเหตุการณ์ LVGL และตัวจับเวลาของ LVGL) ฝั่ง ISR ของ IPC ทำเพียงเข้าคิวเท่านั้น งานของ LVGL เกิดบนตัวจับเวลา 50 ms (ipc_ui.h:7-8)
ทีละขั้น
ขั้นที่ 1 — ดูไอคอน WiFi บน topbar เลิกซ่อนเมื่อเชื่อมต่อสำเร็จ
ต่อบอร์ดเข้ากับ WiFi ด้วยเส้นทางใดก็ตามที่มีอยู่ (หน้า UI ในบท C2 หรือบน mtb-mpy ใช้ wifi.connect(...) ที่ REPL ในบท C1)
- สิ่งที่ควรสังเกต
- ไอคอน WiFi บน topbar ปรากฏ หนึ่งครั้ง ที่จังหวะเปลี่ยนสถานะ แล้วคงอยู่ ไม่กะพริบ เมื่อหลุดการเชื่อมต่อก็หายไปหนึ่งครั้ง บน mtb-mpy เส้นทาง REPL พิมพ์ [WiFi] Connecting… / [WiFi] Connected! จาก modwifi.c:240-282 ส่วนเส้นทาง UI ไม่พิมพ์อะไรเลย — ตัวไอคอนเองคือสิ่งที่สังเกตได้ ห้ามรอบรรทัด [wifi-glue] เพราะบรรทัดเหล่านั้นพิมพ์เฉพาะภายใน app_wifi_connect_direct() (wifi_init.c:225-242) ซึ่งไม่มีสิ่งใดบนเส้นทาง REPL, UI หรือการบูตเรียกเลย ผู้เรียกรายเดียวคือตัวจัดคิววิทยุ BLE ที่อยู่ใน archive ซึ่งเชื่อมเข้ามาภายใต้ ENABLE_PAGE_BENTO_BUDDY=1 (บท I1) ส่วน [WiFi-Boot] และ [WiFiIPC] ปิดเสียงไว้และจะไม่ปรากฏ
ขั้นที่ 2 — ดูนาฬิกาปรากฏหลัง NTP เท่านั้น
- สิ่งที่ควรสังเกต
- จะไม่มีข้อความนาฬิกาบน topbar จนกว่า CM33_NS จะซิงก์ NTP เสร็จและแจ้ง CM55 แล้ว จากนั้นเวลาจะปรากฏและอัปเดตราว 6 ครั้งต่อวินาที (ไม่ใช่ทุก tick ที่ 33 ms) หากบอร์ดไม่มีเส้นทางไปยังเซิร์ฟเวอร์ NTP นาฬิกาจะไม่ปรากฏเลย นั่นคือ gate ทำงานถูกต้อง ไม่ใช่ความผิดพลาด
ขั้นที่ 3 — ทดลองตัวบอกข้อความที่ยังไม่อ่านบนหน้า Playground
เปิดหน้า Playground แล้วรันอะไรบางอย่างบน REPL ของ mtb-mpy ที่พิมพ์ออกคอนโซล IPC (ใช้ print() อะไรก็ได้ขณะที่แผงคอนโซลซ่อนอยู่)
- สิ่งที่ควรสังเกต
- ขณะที่คอนโซลซ่อนอยู่ ตัวบอกข้อความที่ยังไม่อ่านจะปรากฏใน tick การเรนเดอร์ถัดไป (ipc_lcd_has_unread() วนถามจาก render hook) เมื่อสลับไปที่คอนโซล ตัวบอกนั้นจะล้างที่จังหวะเปลี่ยนเข้าสู่โหมดคอนโซล (ipc_lcd_clear_unread() เรียกอย่างชัดแจ้งและแยกจากการอ่าน) เมื่อสลับกลับ widget จะกลับมาและคอนโซลซ่อนไป ตามลำดับนั้น
บน mtb-only ไม่มี REPL ให้พิมพ์ออกมา แผงคอนโซลและการสลับยังมีอยู่และกลไกยังใช้ได้เหมือนกัน แต่ขั้นนี้ไม่มีตัวขับบน variant นั้น
ขั้นที่ 4 — อ่านกรณีที่ผิดพลาด แต่ห้ามลงมือทำ
อ่าน snippet ของการยกเลิกการผูกอีกครั้ง แล้วนึกภาพว่าไม่มี snippet นั้น
- สิ่งที่ควรสังเกต
- ไม่มีอะไร — ข้อนี้เป็นความล้มเหลวที่บรรยายไว้ ไม่ใช่ความล้มเหลวที่ลงมือทำ สิ่งที่ จะเกิดขึ้นคือ ออกจากหน้า Playground แล้วส่งคำสั่ง widget ใด ๆ จาก CM33_NS handler จะเขียนผ่านพอยน์เตอร์ container ไปยังอ็อบเจ็กต์ LVGL ที่ lv_screen_load_anim(auto_del) ปลดหน่วยความจำไปแล้ว ความผิดพลาดจะมาถึงในภายหลังและที่อื่น ในรูป LED1+LED2 กะพริบเป็นชุดละ 3 ครั้ง และค่า 0xDEAD0003 ที่ 0x28000000 (บท A3) ไม่มีวิธีทำซ้ำที่ปลอดภัยสำหรับกรณีนี้ และจะไม่มีการให้ไว้
กับดัก
- ภาคผนวก X #7 — การเรียก ipc_lcd_toggle_panel() แบบตาบอดโดยไม่มี is_panel_visible() ทำให้สถานะกลับด้าน ไม่มีรูปแบบ set ให้ใช้ ต้องกันการสลับทุกครั้ง
- ภาคผนวก X #8 — การลืม set_container(NULL) ตอนทำลายหน้า เท่ากับ use-after-free ต้องเรียกทั้ง ipc_ui_set_container(NULL) และ ipc_lcd_set_container(NULL) ใน callback ของการทำลายหน้า เรียงลำดับ ui ก่อนแล้วจึง lcd เหมือนตอนผูก
- ภาคผนวก X #9 — ipc_sensorhub_snapshot ล้าง changed flag ต่อหนึ่ง tick มีผู้อ่านได้รายเดียว แต่ละหน้าใช้ snapshot ที่ส่งมาให้เท่านั้น
- ภาคผนวก X #14 — ลำดับเลข (ordinal) ของ PAGE_ID เป็น ABI archive ฝังค่าลำดับของ PAGE_ID_PLAYGROUND ไว้ตายตัว (dist/ipc_core/PROVENANCE.txt) โปรเจกต์ที่เรียงลำดับ page_id_t ต่างออกไปจะเชื่อม (link) ผ่านโดยไม่มีข้อความเตือนใด ๆ แล้วไปเทียบกับหน้าที่ผิด
- การเรียก ui_widget_mgr_init() เอง ipc_ui_init() เรียกให้อยู่แล้ว (ipc_ui.c:890) การเรียกซ้ำครั้งที่สองทำให้ตาราง widget init ซ้ำ 2 รอบ
- การมองฟังก์ชันพื้นฐาน ui_widget_mgr_* ว่าเป็น API ของภาษา C ผู้เรียกรายเดียวคือสวิตช์คำสั่ง IPC process_ui_command() ส่วนผู้เรียกจริงคือสคริปต์ MicroPython ที่ข้ามมาทาง IPC (บทกลุ่ม F บท F2)
- การเรียก cm55_ipc_communication_setup() 2 ครั้ง (wifi_manager.c:65)
- การเรียก LVGL จาก task ที่ไม่ใช่ ทุกการเรียกในบทนี้อยู่ใน GFX-task context การสร้าง widget จาก ISR ของ IPC หรือจาก task อื่นใด คือ HardFault
ขอบเขตการใช้กับแต่ละ variant
- variant ที่ใช้ได้
- mtb-mpy และ mtb-only ทุกฟังก์ชันในบทนี้อยู่ใน libbento_ipc.a และ CM55 ไม่ขึ้นกับ variant ส่วนตัวขับของขั้นที่ 3 ในหัวข้อทีละขั้น (การพิมพ์ออกคอนโซล IPC) เป็นการกระทำฝั่ง MicroPython จึงไม่มีสิ่งเทียบเท่าบน mtb-only แต่กลไกฝั่ง CM55 เหมือนกันทุกประการ