เป้าหมายของหัวข้อนี้
GFX task เป็นเจ้าของทุกอย่าง IPC ขึ้นก่อนจอแสดงผล และมีหลักฐานที่ชัดเจนอยู่หนึ่งอย่างว่าเฟรมแรกส่งออกไปแล้วจริง เมื่อจบบทนี้จะบอกได้จาก LED เพียงอย่างเดียวว่าการบูตของ CM55 หยุดที่ขั้นใด และจะเข้าใจว่าเหตุใดความล้มเหลวของจอแสดงผลจึงไม่ดึงลิงก์ IPC ล้มไปด้วย
ลำดับการทำงานจริงของเฟิร์มแวร์
ในเทมเพลตมีไฟล์ที่อ่านได้ 2 ไฟล์ — proj_cm55/main.c และโค้ดของผู้ใช้ไลบรารีที่ไฟล์นั้นส่งงานต่อให้ ส่วนตัวควบคุมจอแสดงผลที่อยู่ระหว่างกลางคอมไพล์รวมอยู่ใน lib/cm55_core/…/libbento_cm55.a และส่งมอบโดยไม่มีซอร์ส ส่วนที่เปิดให้ใช้คือ tesaiot_display.h ลำดับภายใน archive (ไฟล์ไลบรารีแบบสแตติก .a) ที่อธิบายด้านล่างคือข้อกำหนดที่ archive ยึดถือ และระบุไว้ใน lib/cm55_core/consumer_must_provide.txt:64-65 และ bento_libs/claw/APIs/08_IPC_API.md:714-716 ด้วย — ขอระบุให้ชัด ผู้ใช้ไลบรารีที่มีเพียงเทมเพลตอ่านโค้ดเบื้องหลังลำดับนี้เองไม่ได้
- main() — cybsp_init(), __enable_irq() ความล้มเหลวคือ LED1 ที่ 50 ms (proj_cm55/main.c:180-188)
- tesaiot_display_init() — สร้าง GFX task เท่านั้น ไม่ใช่การ init task นั้นรัน tesaiot_display_task() ด้วยชื่อ ขนาด stack และ priority ที่ header กำหนด (GFX_TASK_NAME = "TESAIoT Gfx Task", GFX_TASK_PRIORITY = configMAX_PRIORITIES - 1, tesaiot_display.h:42-44) และเก็บ handle ไว้ใน rtos_cm55_gfx_task_handle ค่าที่คืนคือผลของ xTaskCreate จึงต้องเทียบกับ pdPASS ไม่ใช่ CY_RSLT_SUCCESS ความล้มเหลวคือ LED2 ที่ 50 ms:
int main(void)
{
cy_rslt_t result;
#if TESAIOT_ENABLE_FACE_RUNTIME && TESAIOT_ENABLE_FACE_RUNTIME_BOOT
if (face_mode_runtime_requested()) {
face_mode_launch_runtime();
}
#endif
result = cybsp_init();
if (CY_RSLT_SUCCESS != result)
{
for (;;) {
Cy_GPIO_Inv(CYBSP_USER_LED1_PORT, CYBSP_USER_LED1_PIN);
Cy_SysLib_Delay(50);
}
}
__enable_irq();
s_scb5_boot_held = ipc_scb5_lock_wait_us(SCB5_BOOT_LOCK_WAIT_US);
if (pdPASS != xTaskCreate(scb5_boot_release_task, "scb5_boot",
configMINIMAL_STACK_SIZE * 2U, NULL,
tskIDLE_PRIORITY + 1U, NULL)) {
if (s_scb5_boot_held) {
ipc_scb5_unlock();
s_scb5_boot_held = false;
}
}
if (s_scb5_boot_held) {
disp_i2c_bus_clear();
}
if (pdPASS != xResult) {
for (;;) {
Cy_GPIO_Inv(CYBSP_USER_LED2_PORT, CYBSP_USER_LED2_PIN);
Cy_SysLib_Delay(50);
}
}
ก่อน scheduler มีอีกสามขั้นตอน คือเคลียร์บัส I2C ของจอที่อาจค้างจากก่อน reset (disp_i2c_bus_clear()), จองตัวล็อก SCB5 ข้ามคอร์ไว้จนจอพร้อม (ipc_scb5_lock.h, task scb5_boot คืนให้) และสร้าง app_task แล้ว scheduler จึงเริ่มทำงาน
- จุดเข้าของ GFX task: IPC pipe มาก่อน — สิ่งแรกที่ GFX task ทำคือเรียก cm55_ipc_communication_setup() ซึ่งเรียก Cy_IPC_Pipe_Init() อีกต่อหนึ่ง IPC จึงทำงานได้แม้ขั้นของจอแสดงผลที่ตามมาจะล้มเหลวทุกขั้น ตัวการทำงานจริงของฟังก์ชันนี้ส่งมอบมาในรูปซอร์ส:
void cm55_ipc_communication_setup(void)
{
static const cy_stc_ipc_pipe_config_t cm55_ipc_pipe_config =
{
{
.ipcNotifierNumber = CY_IPC_INTR_CYPIPE_EP2,
.ipcNotifierPriority = CY_IPC_INTR_CYPIPE_PRIOR_EP2,
.ipcNotifierMuxNumber = CY_IPC_INTR_CYPIPE_MUX_EP2,
.epAddress = CM55_IPC_PIPE_EP_ADDR,
{
.epChannel = CY_IPC_CHAN_CYPIPE_EP2,
.epIntr = CY_IPC_INTR_CYPIPE_EP2,
.epIntrmask = CY_IPC_CYPIPE_INTR_MASK
}
},
{
.ipcNotifierNumber = CY_IPC_INTR_CYPIPE_EP1,
.ipcNotifierPriority = CY_IPC_INTR_CYPIPE_PRIOR_EP1,
.ipcNotifierMuxNumber = CY_IPC_INTR_CYPIPE_MUX_EP1,
.epAddress = CM33_IPC_PIPE_EP_ADDR,
{
.epChannel = CY_IPC_CHAN_CYPIPE_EP1,
.epIntr = CY_IPC_INTR_CYPIPE_EP1,
.epIntrmask = CY_IPC_CYPIPE_INTR_MASK
}
},
.endpointClientsCount = CY_IPC_CYPIPE_CLIENT_CNT,
.endpointsCallbacksArray = ep2_cb_array,
.userPipeIsrHandler = &Cy_SysIpcPipeIsrCm55
};
Cy_IPC_Pipe_Config(cm55_ipc_pipe_array);
Cy_IPC_Pipe_Init(&cm55_ipc_pipe_config);
{
cy_stc_sysint_t ep2_intr_cfg = {
.intrSrc = (IRQn_Type)CY_IPC_INTR_CYPIPE_MUX_EP2,
.intrPriority = (uint32_t)CY_IPC_INTR_CYPIPE_PRIOR_EP2
};
(void)Cy_SysInt_Init(&ep2_intr_cfg, &Cy_SysIpcPipeIsrCm55);
NVIC_EnableIRQ((IRQn_Type)CY_IPC_INTR_CYPIPE_MUX_EP2);
กฎที่คัดมาตามตัวอักษรจาก 08_IPC_API.md:708: "cm55_ipc_communication_setup() must be called before any Cy_IPC_Pipe_RegisterCallback(). Without this, RegisterCallback accesses uninitialized pipe endpoints, causing a HardFault at boot." ตารางความผิดพลาดที่ :765-771 ให้อาการและวิธีแก้ไว้: "Always call cm55_ipc_communication_setup() first in GFX task." ฟังก์ชันนี้เป็น ของที่ผู้ใช้ไลบรารีต้องจัดหาเอง (consumer_must_provide.txt) — การเรียกเป็นความรับผิดชอบของผู้ใช้ไลบรารี ห้ามเรียกซ้ำ 2 ครั้ง (wifi_manager.c:65)
- ไคลเอนต์ของ IPC ลงทะเบียน — ipc_sensorhub_init() (ข้อกำหนดใน header ipc_sensorhub.h:58: "Must be called AFTER cm55_ipc_communication_setup()."), ipc_service_init() (ipc_service.h:19) แล้วจึง deepcraft_task_init() ใต้ BENTO_HAS_MODEL_LINK ซึ่งเป็นคู่สื่อสารของ model link และต้องลงทะเบียนหลังการตั้งค่า pipe เช่นกัน (deepcraft_task.h:24) 5-10. 6 ขั้นของจอแสดงผลที่มีรหัส LED กำกับ ตามลำดับ: 1 GFXSS, 2 อินเทอร์รัปต์ของ DC, 3 อินเทอร์รัปต์ของ GPU, 4 ตัวควบคุม I2C ของจอแสดงผล, 5 ตัวควบคุมพาเนล, 6 GPU ของ VGLite หากขั้นที่ N ของจอแสดงผลล้มเหลว task จะจำเลข N ไว้ ข้ามงานของจอแสดงผลที่เหลือทั้งหมด แล้วไปที่เส้นทาง IPC-only (ขั้นที่ 17) ทันที จากนั้น LED2 จะกะพริบ N ครั้งซ้ำไปเรื่อย ๆ (ดูขั้นที่ 1 ของหัวข้อทีละขั้น) ขั้นที่ 5 ของจอแสดงผล (พาเนล) ต้องมาหลังขั้นที่ 1 (GFXSS) เพราะตัวควบคุมพาเนลที่แอดเดรส I2C 0x45 จะตอบก็ต่อเมื่อ GFXSS/DSI จ่ายไฟให้จอแสดงผลแล้วเท่านั้น
- init ของ LVGL — lv_init() แล้วตามด้วยพอร์ตของจอแสดงผล lv_port_disp_init() และพอร์ตอินพุตของการสัมผัส lv_port_indev_init() โดย lv_port_disp_init เป็นซอร์สในเทมเพลต (proj_cm55/modules/lvgl_display/core/lv_port_disp.c:267-285): จอแสดงผลหนึ่งจอ โหมดเรนเดอร์แบบเต็ม และใช้ disp_flush เป็น callback ของการ flush ส่วน tick ของ LVGL มาจาก tick hook ของ FreeRTOS (proj_cm55/main.c:53-62 → tesaiot_display_tick() → lv_tick_inc(1))
- เสียง แล้วจึงเป็น backlight ที่ลำดับแน่นอน — bento_audio_init() (bento_audio.h) รอ PLL ของ codec นิ่ง 1000 ms แล้วเปิดไฟพาเนลหนึ่งรอบด้วย ws_panel_power_up() (ws_panel_power.h) การเขียนไปยัง MCU ของพาเนลแล้วได้ NAK คือ LED2 กะพริบ 4 ครั้ง
- การสร้าง UI — จุดส่งงานต่อให้โค้ดของผู้ใช้ไลบรารี: หน้าจอที่ทำงานอยู่ถมด้วยสีน้ำเงินเข้ม 0x003366 แล้วส่งหน้าจอนั้นให้ sensorhub_ui_init()
- init ของ page manager และการลงทะเบียนหน้า — เป็นซอร์สในเทมเพลต proj_cm55/modules/page-components/_core/sensorhub_ui.c: tesaiot_ui_styles_init(); pm_init(&s_pm); pm_set_instance(&s_pm); แล้วตามด้วยการเรียก pm_register หน้าละหนึ่งครั้ง การลงทะเบียนหน้า Edge AI เป็นตัวอย่างต้นแบบ (มีตัวกัน BENTO_HAS_EDGE_AI และ page manager เป็นเจ้าของวงจรชีวิต (lifecycle) ของหน้านั้น):
#if defined(BENTO_HAS_EDGE_AI) && (BENTO_HAS_EDGE_AI == 1)
{
page_def_t def = {
.name = "Edge AI",
.subtitle = "On-device inference",
.accent_color = UI_COLOR_ACCENT_PURPLE,
};
pm_register(&s_pm, PAGE_ID_EDGE_AI, &def);
}
- สร้างและโหลดหน้า Home — page_home_create() แล้ว lv_screen_load(home_scr) (sensorhub_ui.c ท้ายฟังก์ชัน sensorhub_ui_init) การ์ดแต่ละใบผูกกับ page_id_t ของตัวเองที่ page_home.c:622-625
- ตัวจับเวลาเรนเดอร์ 33 ms — lv_timer_create(sensorhub_timer_cb, 33, NULL) callback ตัวนี้เก็บ snapshot หนึ่งชุดแล้วกระจายต่อ (บท B3)
- เส้นทาง IPC-only, deferred binding (การผูกแบบเลื่อนเวลา), flag บอกความพร้อม — ทุกเส้นทางมาจบที่จุดนี้ รวมถึงกรณีที่ขั้นใดขั้นหนึ่งของจอแสดงผลล้มเหลว: เรียก ipc_lcd_init(NULL) โดยไม่มีเงื่อนไข ส่วน ipc_ui_init(NULL) เรียกเฉพาะเมื่อจอแสดงผลขึ้นสำเร็จ แล้วจึงตั้ง tesaiot_display_ready เป็น 1 ทั้งสองรับ NULL เพราะหน้า Playground จะผูก container จริงในภายหลังด้วย ipc_lcd_set_container() / ipc_ui_set_container() (ipc_lcd.h, ipc_ui.h; บท B3) app_task รอ flag นั้นอยู่ โดยมีขอบเวลากำกับ:
static void app_task(void *arg)
{
CY_UNUSED_PARAMETER(arg);
uint32_t wait_count = 0;
{
vTaskDelay(pdMS_TO_TICKS(100));
wait_count++;
if (wait_count > 150) {
break;
}
}
- เฟรมแรก — ลูปของ GFX เรียก lv_timer_handler() ซึ่งไปเรียก disp_flush ต่อ อินเทอร์รัปต์ของ DC ปิดงานเฟรมนั้นแล้วแจ้ง GFX task:
static void LV_ATTRIBUTE_FAST_MEM disp_flush(lv_display_t *disp_drv,
const lv_area_t *area,
uint8_t *color_p)
{
CY_UNUSED_PARAMETER(area);
s_flush_disp = disp_drv;
s_flush_start_tick = xTaskGetTickCount();
s_flush_start_count++;
Cy_GFXSS_Set_FrameBuffer((GFXSS_Type*) GFXSS, (uint32_t*) color_p,
&gfx_context);
}
void lv_port_disp_flush_ready(void)
{
lv_display_t *d = (lv_display_t *)s_flush_disp;
s_flush_disp = NULL;
if (d != NULL) {
s_flush_ready_count++;
lv_display_flush_ready(d);
พร้อมกลไกสำรองกันพลาดสำหรับ IRQ ที่หลุด ซึ่งตั้งไว้ที่ 500 ms และจะบังคับปิดงาน flush ให้:
void lv_port_disp_check_flush_timeout(void)
{
lv_display_t *d = (lv_display_t *)s_flush_disp;
if (d != NULL) {
TickType_t elapsed = xTaskGetTickCount() - s_flush_start_tick;
if (elapsed >= FLUSH_TIMEOUT_TICKS) {
s_flush_disp = NULL;
lv_display_flush_ready(d);
ทีละขั้น
ขั้นที่ 1 — เฝ้าดู LED2 ตลอดการเริ่มระบบ (bring-up)
ตัดไฟแล้วจ่ายไฟใหม่ และเฝ้าดูเฉพาะ LED2
- สิ่งที่ควรสังเกต
- กะพริบหนึ่งครั้งแล้วติดค้างขณะ Cy_GFXSS_Init ทำงาน (ขั้นที่ 1 ของจอแสดงผล) แล้วดับเมื่อฟังก์ชันคืนค่า จากนั้นดับถาวรเมื่อการ init จอแสดงผลสำเร็จครบถ้วน แต่หาก LED2 ลงตัวเป็นการกะพริบเร็ว N ครั้งทุก 2 s แสดงว่า bring-up หยุดที่ขั้นที่ N และ task อยู่ในลูป IPC-only ซึ่งยังให้บริการ IPC ต่อไป การกะพริบ 4 ครั้งคือ NAK ของ backlight (ขั้นที่ 12)
ขั้นที่ 2 — เฝ้าดูหน้าจอ
- สิ่งที่ควรสังเกต
- พื้นสีน้ำเงินเข้ม 0x003366 เต็มจอ (ขั้นที่ 13) แล้วภายในหนึ่งถึง 2 เฟรมจะเปลี่ยนเป็นตารางการ์ดของหน้า Home บนพื้นหลังสีเข้ม (page_home.c:366-373) พร้อมป้ายเวอร์ชัน บนบอร์ดที่หยุดที่ขั้นที่ 13 จะเห็นเพียงพื้นสีน้ำเงิน สถานะนั้นยังไม่เคยพบบนเฟิร์มแวร์ที่ส่งมอบจริง และไม่มีวิธีทำซ้ำ
ขั้นที่ 3 — พิสูจน์เฟรมแรก (แยกตาม variant)
- สิ่งที่ควรสังเกต — ทั้งสอง variant
- LED2 ดับ และเห็นตารางหน้า Home นั่นคือหลักฐานที่ยืนยันได้ด้วยตาเปล่า
- สิ่งที่ควรสังเกต — mtb-mpy เท่านั้น
- ที่ REPL ui._diag() (modui.c:1470) คืนค่าตัวนับของ GFX task เองผ่าน ipc_ui_platform_diag() ซึ่งตัวควบคุมจอแสดงผลใน libbento_cm55.a เป็นผู้ implement ได้แก่ สถานะ IRQ ของ DC, flush_start_count, flush_ready_count, flush_timeout_count, ระดับสูงสุดที่ stack ของ GFX เคยใช้ และเปอร์เซ็นต์เวลาว่าง การที่ flush_start_count เพิ่มขึ้นระหว่างการเรียก 2 ครั้ง คือข้อยืนยันว่า "เฟรมแรกทำงานแล้ว" ที่ชัดที่สุดบน variant นี้ เพราะนั่นคือการเข้าสู่ disp_flush (lv_port_disp.c:157) ส่วน flush_timeout_count ที่ไม่เป็นศูนย์หมายความว่ากลไกสำรองกันพลาด 500 ms ทำงานไปแล้ว กล่าวคืออินเทอร์รัปต์ของ DC หลุดไปอย่างน้อยหนึ่งครั้ง
- สิ่งที่ควรสังเกต — mtb-only
- ไม่มี REPL จึงไม่มี ui._diag() สิ่งเทียบเท่าในเชิงโปรแกรมคือการเรียก ipc_ui_platform_diag(out, max_words) จากฝั่ง C ด้วย max_words >= 10 ในโค้ด CM55 ของตนเอง (คืนค่าเป็น 10 เวิร์ดชุดเดียวกัน) นอกนั้นคือ LED2 ดับ พร้อมตารางหน้า Home
ขั้นที่ 4 — พิสูจน์ว่า IPC รอดแม้จอแสดงผลล้มเหลว
ห้ามทำให้เกิดขึ้นเอง ให้ใช้การให้เหตุผลจากลำดับข้างต้นแทน: ipc_lcd_init(NULL) อยู่บนเส้นทาง IPC-only และ ไม่ได้ ขึ้นกับว่าจอแสดงผลขึ้นสำเร็จหรือไม่ ส่วน ipc_ui_init(NULL) ขึ้นกับเงื่อนไขนั้น เพราะสร้างตัวจับเวลาของ LVGL (ipc_ui.h:20-21) tesaiot_display_ready จึงกลายเป็น 1 แม้พาเนลจะไม่เคยขึ้นเลย และ app_task เดินหน้าต่อ
- สิ่งที่ควรสังเกต
- บน mtb-mpy บนบอร์ดที่จอแสดงผลล้มเหลวที่ขั้นใดขั้นหนึ่ง (LED2 กะพริบ N ครั้ง) คอนโซลยังทำงานได้ และ import ui ยังสำเร็จ เพราะ pipe ตั้งขึ้นตั้งแต่ขั้นที่ 3 ก่อนสิ่งใดที่เกี่ยวกับจอแสดงผล การที่ tesaiot_display_ready มี 3 สถานะ (proj_cm55/main.c:138: 0=pending, 1=display+IPC, 2=IPC-only (headless)) คือเหตุผลที่ผู้ใช้ไลบรารีต้องทดสอบด้วย == 0 ห้ามทดสอบด้วย == 1
กับดัก
- HardFault หากสลับลำดับในขั้นที่ 3 คือเรียก RegisterCallback ก่อน cm55_ipc_communication_setup() — 08_IPC_API.md:708, :765-771 สิ่งที่สังเกตได้คือ LED1+LED2 กะพริบเป็นชุดละ 3 ครั้ง (HardFault, proj_cm55/main.c:246-254) และค่า 0xDEAD0003 ที่ 0x28000000
- 08_IPC_API.md:711-717 กำหนดลำดับที่โค้ดจริงไม่ได้ทำตาม เอกสารนั้นให้ ipc_lcd_init(parent) เป็นลำดับที่สามและ ipc_service_init() เป็นลำดับที่สี่โดยส่ง parent จริง แต่ task ที่ส่งมอบจริงเรียก ipc_service_init() ที่ขั้นที่ 4 และเรียกทั้ง ipc_lcd_init(NULL)/ipc_ui_init(NULL) ที่ขั้นที่ 17 ด้วยค่า NULL ซึ่งเป็น deferred binding โดยเจตนา มีเพียงกฎข้อ #1 (ตั้งค่าก่อน) เท่านั้นที่รับน้ำหนักไว้จริง
- ภาคผนวก X #1 — printf บน CM55 เป็น no-op โดยค่าตั้งต้น (weak ใน proj_cm55/bento_cm55_stdio.c) ห้ามใส่ข้อความพิมพ์เพื่อไล่ดูลำดับนี้ ให้ใช้ LED2 หรือ ui._diag() แทน
- ภาคผนวก X #13 — timeout ของ I2C ที่ไม่มีขอบเวลาบนบัสจอแสดงผลที่ใช้ร่วมกัน GFX task ทำงานที่ configMAX_PRIORITIES - 1 อุปกรณ์ที่ค้างและยืดสัญญาณนาฬิกาบนบัสของการสัมผัส (touch) หรือของพาเนล เมื่อมาพร้อม timeout แบบบล็อกตลอดกาล จะทำให้ task ที่ priority ต่ำกว่าทั้งหมดอดทำงาน (cm55_sensor_poll.c:49-53)
- ภาคผนวก X #14 — ลำดับเลข (ordinal) ของ PAGE_ID เป็น ABI การลงทะเบียนในขั้นที่ 14 ใช้ค่า page_id_t ที่ฝังอยู่ใน libbento_ipc.a การเปลี่ยนเลขลำดับยังคอมไพล์ผ่านแต่ทำงานผิด (page_id_ordinal_assert.c:22-25)
- ข้อห้ามเรื่องคอนโซล CM33_NS: Booting CM55... เป็น BOOT_VERBOSE ห้ามใช้เป็นเครื่องหมายว่า "ปล่อย CM55 แล้ว" สิ่งที่เป็นเครื่องหมายคือหน้าจอ Home
- ui._diag() บน mtb-only ไม่มีอยู่บน variant นั้น ขั้นใดใน Tutorial ที่ต้องใช้ฟังก์ชันนี้ คือขั้นของ mtb-mpy
ขอบเขตการใช้กับแต่ละ variant
- variant ที่ใช้ได้
- mtb-mpy และ mtb-only CM55 ไม่ขึ้นกับ variant — ขั้นที่ 1-18 เป็นอิมเมจเดียวกัน จุดที่การตรวจสอบแยกกันอยู่ที่ขั้นที่ 3 ของหัวข้อทีละขั้น: ui._diag() เป็นการเรียกจาก REPL ของ MicroPython จึงมีเฉพาะบน mtb-mpy ส่วน mtb-only ตรวจสอบด้วย LED2 ที่ดับ ตารางหน้า Home หรือการเรียก ipc_ui_platform_diag() จากฝั่ง C