SDK สำหรับ TESAIoT Dev Kit
คู่มืออ้างอิง API และ Tutorial (ModusToolbox)
Loading...
Searching...
No Matches
B2 — การบูต CM55 จนถึงเฟรมแรก

เป้าหมายของหัวข้อนี้

GFX task เป็นเจ้าของทุกอย่าง IPC ขึ้นก่อนจอแสดงผล และมีหลักฐานที่ชัดเจนอยู่หนึ่งอย่างว่าเฟรมแรกส่งออกไปแล้วจริง เมื่อจบบทนี้จะบอกได้จาก LED เพียงอย่างเดียวว่าการบูตของ CM55 หยุดที่ขั้นใด และจะเข้าใจว่าเหตุใดความล้มเหลวของจอแสดงผลจึงไม่ดึงลิงก์ IPC ล้มไปด้วย

ลำดับการทำงานจริงของเฟิร์มแวร์

ในเทมเพลตมีไฟล์ที่อ่านได้ 2 ไฟล์ — proj_cm55/main.c และโค้ดของผู้ใช้ไลบรารีที่ไฟล์นั้นส่งงานต่อให้ ส่วนตัวควบคุมจอแสดงผลที่อยู่ระหว่างกลาง (tesaiot_display.c) คอมไพล์รวมอยู่ใน lib/cm55_core/…/libbento_cm55.a ซอร์สของไฟล์นั้นอยู่ที่ TESAIoT_KIT_PSE84_AI-Micropython-BentoClaw/proj_cm55/modules/lvgl_display/controller/ และอ้างอิงมาจากที่นั่น ลำดับภายใน archive (ไฟล์ไลบรารีแบบสแตติก .a) มีหลักฐานยืนยันจากซอร์สนั้น จาก lib/cm55_core/consumer_must_provide.txt:64-65 และจาก bento_libs/claw/APIs/08_IPC_API.md:714-716 — ขอระบุให้ชัด ผู้ใช้ไลบรารีที่มีเพียงเทมเพลตอ่านลำดับนี้เองไม่ได้

  1. main()cybsp_init(), __enable_irq() ความล้มเหลวคือ LED1 ที่ 50 ms (proj_cm55/main.c:180-188)
  2. tesaiot_display_init()เป็น xTaskCreate หนึ่งครั้ง ไม่ใช่การ init (tesaiot_display.c:210-213: xTaskCreate(tesaiot_display_task, GFX_TASK_NAME, GFX_TASK_STACK_SIZE, NULL, GFX_TASK_PRIORITY, &rtos_cm55_gfx_task_handle) ชื่อ "TESAIoT Gfx Task" priority configMAX_PRIORITIES - 1, tesaiot_display.h:41-43) ให้เทียบค่ากับ pdPASS ไม่ใช่ CY_RSLT_SUCCESS ความล้มเหลวคือ LED2 ที่ 50 ms:
int main(void)
{
cy_rslt_t result;
#if TESAIOT_ENABLE_FACE_RUNTIME && TESAIOT_ENABLE_FACE_RUNTIME_BOOT
/* Alternate boot mode: launch native Face-ID runtime. */
if (face_mode_runtime_requested()) {
face_mode_launch_runtime();
}
#endif
/* Initialize the device and board peripherals */
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 global interrupts */
__enable_irq();
/* GFX task: GFXSS/LVGL init + IPC + sensorhub UI */
BaseType_t xResult = tesaiot_display_init();
if (pdPASS != xResult) {
for (;;) {
Cy_GPIO_Inv(CYBSP_USER_LED2_PORT, CYBSP_USER_LED2_PIN);
Cy_SysLib_Delay(50);
}
}

ไม่มีอย่างอื่นเกิดขึ้นบน CM55 ก่อน scheduler อีก มีเพียงการสร้าง app_task (:217-224) แล้ว scheduler เริ่มทำงาน (:232)

  1. จุดเข้าของ GFX task: IPC pipe มาก่อนtesaiot_display.c:223-227: "IPC Pipe — initialize FIRST so IPC works even if display init fails. cm55_ipc_communication_setup() calls Cy_IPC_Pipe_Init()." ตัวการทำงานจริงส่งมอบมาในรูปซอร์ส:
void cm55_ipc_communication_setup(void)
{
/******************************************************/
/* IPC pipe endpoint-1 and endpoint-2. CM55 <--> CM33 */
/******************************************************/
/* clang-format off */
static const cy_stc_ipc_pipe_config_t cm55_ipc_pipe_config =
{
/* receiver endpoint CM55 */
{
.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
}
},
/* sender endpoint CM33 */
{
.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
};
/* clang-format on */
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)

  1. ไคลเอนต์ของ IPC ลงทะเบียน — ipc_sensorhub_init() (:228; ข้อกำหนดใน header ipc_sensorhub.h:58: "Must be called AFTER cm55_ipc_communication_setup()."), ipc_service_init() (:229; ipc_service.h:19) แล้วจึง deepcraft_task_init() ใต้ BENTO_HAS_MODEL_LINK (:231, "model-link peer — after pipe setup (ordering rule)") 5-10. 6 ขั้นของจอแสดงผลที่มีรหัส LED กำกับtesaiot_display.c:139 และ :248: 1=GFXSS 2=DC_IRQ 3=GPU_IRQ 4=I2C 5=Panel 6=VGLite ความล้มเหลวแต่ละครั้งตั้ง disp_debug_step = N แล้วกระโดดไปที่ ipc_only: (:270, :277, :285, :306/:313, :325, :345) เหตุผล ของขั้นที่ 5 (:295-297): "Panel controller at 0x45 only responds AFTER GFXSS/DSI powers the display."
  2. init ของ LVGL — lv_init(); lv_port_disp_init(); lv_port_indev_init(); (:349-357) 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-62tesaiot_display_tick()lv_tick_inc(1))
  3. เสียง แล้วจึงเป็น backlight ที่ลำดับแน่นอน — bento_audio_init() รอ PLL ของ codec นิ่ง 1000 ms แล้ว ws_panel_power_up(&bl_cfg) (:375-424) การเขียนไปยัง MCU ของพาเนลแล้วได้ NAK คือ LED2 กะพริบ 4 ครั้ง (:432)
  4. การสร้าง UI — จุดส่งงานต่อให้โค้ดของผู้ใช้ไลบรารี (:435-443): หน้าจอที่ทำงานอยู่ถมด้วยสีน้ำเงินเข้ม 0x003366 แล้วเรียก sensorhub_ui_init(scr)
  5. 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) ของหน้านั้น):
/* ...context: inside sensorhub_ui_init() page registration ... */
#if defined(BENTO_HAS_EDGE_AI) && (BENTO_HAS_EDGE_AI == 1)
/* Edge AI hub — ONE page hosting every compiled-in DEEPCRAFT model. */
{
page_def_t def = {
.name = "Edge AI",
.subtitle = "On-device inference",
.accent_color = UI_COLOR_ACCENT_PURPLE,
.create_cb = page_edge_ai_create,
.render_cb = page_edge_ai_render,
.destroy_cb = page_edge_ai_destroy,
};
pm_register(&s_pm, PAGE_ID_EDGE_AI, &def);
}
  1. สร้างและโหลดหน้า Home — page_home_create() แล้ว lv_screen_load(home_scr) (sensorhub_ui.c ท้ายฟังก์ชัน sensorhub_ui_init) การ์ดแต่ละใบผูกกับ page_id_t ของตัวเองที่ page_home.c:622-625
  2. ตัวจับเวลาเรนเดอร์ 33 ms — lv_timer_create(sensorhub_timer_cb, 33, NULL) callback ตัวนี้เก็บ snapshot หนึ่งชุดแล้วกระจายต่อ (บท B3)
  3. ป้าย ipc_only:, deferred binding (การผูกแบบเลื่อนเวลา), flag บอกความพร้อมtesaiot_display.c:470-479: เรียก (void)ipc_lcd_init(NULL); โดยไม่มีเงื่อนไข ส่วน (void)ipc_ui_init(NULL); เรียกเฉพาะเมื่อ if (display_ok) แล้วจึงตั้ง tesaiot_display_ready = 1 ทั้งสองรับ NULL เพราะหน้า Playground จะผูก container จริงในภายหลัง (บท B3) app_task รอ flag นั้นอยู่ โดยมีขอบเวลากำกับ:
static void app_task(void *arg)
{
CY_UNUSED_PARAMETER(arg);
/* Wait for display + IPC initialization to complete
* tesaiot_display_ready: 0=pending, 1=display+IPC, 2=IPC-only (headless) */
extern volatile uint8_t tesaiot_display_ready;
uint32_t wait_count = 0;
while (tesaiot_display_ready == 0)
{
vTaskDelay(pdMS_TO_TICKS(100));
wait_count++;
if (wait_count > 150) {
break;
}
}
  1. เฟรมแรก — ลูปของ 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);
/* Store display pointer so the DC ISR can call flush_ready.
* Non-blocking: the GFX task remains free to run LVGL timers
* (spinner animation, IPC UI commands) while waiting for the
* DC hardware to finish the frame buffer swap. */
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);
}
/*******************************************************************************
* Function Name: lv_port_disp_flush_ready
********************************************************************************
* Summary:
* Called from the DC interrupt handler to signal LVGL that the frame buffer
* swap is complete. This is ISR-safe — lv_display_flush_ready() only sets
* internal flags (no mutex, no allocation).
*
* Parameters:
* void
*
* Return:
* void
*
*******************************************************************************/
void lv_port_disp_flush_ready(void)
{
/* Clear pointer FIRST to prevent double-call if watchdog fires
* simultaneously. 32-bit pointer write is atomic on ARM Cortex-M. */
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 ให้:

/* ...context: lv_port_disp_check_flush_timeout() and its rationale ... */
/*******************************************************************************
* Function Name: lv_port_disp_check_flush_timeout
********************************************************************************
* Summary:
* Called from the GFX task main loop to detect a stuck display flush.
* If disp_flush() was called but the DC interrupt hasn't fired within
* FLUSH_TIMEOUT_TICKS (500ms), force-complete the flush.
*
* This handles missed DC interrupts — the primary suspected cause of
* the stochastic display freeze (both GPU and SW rendering affected).
*
* Cost: one dropped/stale frame (imperceptible at 30fps).
* The display immediately resumes rendering on the next loop iteration.
*
*******************************************************************************/
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, tesaiot_display.c:262-263) แล้วดับเมื่อฟังก์ชันคืนค่า จากนั้นดับถาวรเมื่อถึง display_ok = true; debug_led_off(); (:459-468, "LED2 OFF = display init fully succeeded") แต่หาก LED2 ลงตัวเป็นการกะพริบเร็ว N ครั้งทุก 2 s แสดงว่า bring-up หยุดที่ขั้นที่ N และ task อยู่ในลูป ipc_only (:581-582) การกะพริบ 4 ครั้งคือ NAK ของ backlight (:432)

ขั้นที่ 2 — เฝ้าดูหน้าจอ

สิ่งที่ควรสังเกต
พื้นสีน้ำเงินเข้ม 0x003366 เต็มจอ (:440) แล้วภายในหนึ่งถึง 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() (tesaiot_display.c:593-609) ได้แก่ สถานะ 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 ดับ (:459-468) พร้อมตารางหน้า Home

ขั้นที่ 4 — พิสูจน์ว่า IPC รอดแม้จอแสดงผลล้มเหลว

ห้ามทำให้เกิดขึ้นเอง ให้ใช้การให้เหตุผลจากโค้ดแทน: ipc_lcd_init(NULL) อยู่หลังป้าย ipc_only: และ ไม่ได้ กั้นด้วย display_ok ส่วน ipc_ui_init(NULL) มีการกั้นไว้เพราะสร้างตัวจับเวลาของ LVGL (ipc_ui.c:911) 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 โดยเจตนา (tesaiot_display.c:471-476) มีเพียงกฎข้อ #1 (ตั้งค่าก่อน) เท่านั้นที่รับน้ำหนักไว้จริง
  • ภาคผนวก X #1 — printf บน CM55 กลายเป็น no-op เงียบ ๆ ทันทีที่ลิงก์ libbento_edge_ai.a (ai_engine.c:276-288) ห้ามใส่ข้อความพิมพ์เพื่อไล่ดูลำดับนี้ ให้ใช้ 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