SDK for TESAIoT Dev Kit
API reference & tutorials (ModusToolbox)
Loading...
Searching...
No Matches
B2 — CM55 boot to first frame

Learning goal

The GFX task owns everything; IPC comes up before the display; and there is one clean proof that the first frame was flushed. By the end you can name the step at which a CM55 boot stopped from the LED alone, and you know why a display failure does not take the IPC link with it.

The real firmware sequence

Two files are readable in the template — proj_cm55/main.c and the consumer code it hands off to. The display controller between them (tesaiot_display.c) is compiled into lib/cm55_core/…/libbento_cm55.a; its source lives in TESAIoT_KIT_PSE84_AI-Micropython-BentoClaw/proj_cm55/modules/lvgl_display/controller/ and is cited from there. The internal order inside the archive is attested by that source, by lib/cm55_core/consumer_must_provide.txt:64-65, and by bento_libs/claw/APIs/08_IPC_API.md:714-716 — state this plainly: a template consumer cannot read it.

  1. main() — cybsp_init(), __enable_irq(); failure is LED1 at 50 ms (proj_cm55/main.c:180-188).
  2. tesaiot_display_init() — one xTaskCreate, not an 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); name "TESAIoT Gfx Task", priority configMAX_PRIORITIES - 1, tesaiot_display.h:41-43). Compare against pdPASS, not CY_RSLT_SUCCESS; failure is LED2 at 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);
}
}

Nothing else happens on CM55 before the scheduler. app_task is created (:217-224) and the scheduler starts (:232).

  1. GFX task entry: the IPC pipe FIRST — 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()."* The implementation ships as source:
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);

The rule, verbatim from 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." The fault table at :765-771 gives the symptom and the fix: "Always call cm55_ipc_communication_setup() first in GFX task."* This function is consumer-provided (consumer_must_provide.txt) — you own the call. Never call it twice (wifi_manager.c:65).

  1. IPC clients register — ipc_sensorhub_init() (:228; header contract ipc_sensorhub.h:58: "Must be called AFTER cm55_ipc_communication_setup()."), ipc_service_init() (:229; ipc_service.h:19), then deepcraft_task_init() under BENTO_HAS_MODEL_LINK (:231, "model-link peer — after pipe setup (ordering rule)"). 5-10. Six LED-coded display steps — tesaiot_display.c:139 and :248: 1=GFXSS 2=DC_IRQ 3=GPU_IRQ 4=I2C 5=Panel 6=VGLite. Each failure sets disp_debug_step = N and jumps to ipc_only: (:270, :277, :285, :306/:313, :325, :345). Step 5's why (:295-297): "Panel controller at 0x45 only responds AFTER GFXSS/DSI powers the display."
  2. LVGL init — lv_init(); lv_port_disp_init(); lv_port_indev_init(); (:349-357). lv_port_disp_init is template source (proj_cm55/modules/lvgl_display/core/lv_port_disp.c:267-285): one display, full render mode, disp_flush as the flush callback. The LVGL tick comes from the FreeRTOS tick hook (proj_cm55/main.c:53-62 → tesaiot_display_tick() → lv_tick_inc(1)).
  3. Audio, then the deterministic backlight — bento_audio_init(), a 1000 ms codec PLL settle, ws_panel_power_up(&bl_cfg) (:375-424). A NAKed panel-MCU write is 4 LED2 blinks (:432).
  4. UI creation — the handoff to consumer code (:435-443): the active screen is filled dark blue 0x003366 and sensorhub_ui_init(scr) is called.
  5. Page manager init + registration — template source, proj_cm55/modules/page-components/_core/sensorhub_ui.c: tesaiot_ui_styles_init(); pm_init(&s_pm); pm_set_instance(&s_pm); then the pm_register calls, one per page. The Edge AI registration is the exemplar (guarded BENTO_HAS_EDGE_AI; the page manager owns the 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 created and loaded — page_home_create() then lv_screen_load(home_scr) (sensorhub_ui.c, end of sensorhub_ui_init); each card is bound to its page_id_t at page_home.c:622-625.
  2. 33 ms render timer — lv_timer_create(sensorhub_timer_cb, 33, NULL); the callback takes one snapshot and fans it out (Chapter B3).
  3. ipc_only: label, deferred binding, ready flag — tesaiot_display.c:470-479: (void)ipc_lcd_init(NULL); unconditionally, (void)ipc_ui_init(NULL); only if (display_ok), then tesaiot_display_ready = 1. Both take NULL — the Playground page binds the real containers later (Chapter B3). app_task waits on that flag, bounded:
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. First frame — the GFX loop calls lv_timer_handler(), which invokes disp_flush; the DC interrupt completes the frame and notifies the 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);

with a 500 ms missed-IRQ safety net that force-completes a 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);

Step-by-step

Step 1 — Watch LED2 through bring-up

Power-cycle and watch LED2 only.

What you should observe
One blink then solid ON while Cy_GFXSS_Init runs (step 1, tesaiot_display.c:262-263), OFF when it returns; then OFF for good once display_ok = true; debug_led_off(); (:459-468, "LED2 OFF = display init fully succeeded"). If instead LED2 settles into N quick blinks every 2 s, bring-up stopped at step N and the task is in the ipc_only loop (:581-582). Four blinks is the backlight NAK (:432).

Step 2 — Watch the screen

What you should observe
A solid dark-blue 0x003366 fill (:440) replaced within a frame or two by the Home card grid on its dark shell (page_home.c:366-373), with the version badge. On a board that stops at step 13 you would see only the blue fill; that state has not been observed on shipped firmware and has no recipe.

Step 3 — Prove the first frame (per variant)

What you should observe — both variants
LED2 OFF and the Home grid visible. That is the human-eye proof.
What you should observe — mtb-mpy only
At the REPL, ui._diag() (modui.c:1470) returns the GFX task's own counters via ipc_ui_platform_diag() (tesaiot_display.c:593-609): DC IRQ status, flush_start_count, flush_ready_count, flush_timeout_count, GFX stack high-water mark, idle percent. flush_start_count incrementing between two calls is the cleanest "first frame worked" assertion on this variant — it is disp_flush being entered (lv_port_disp.c:157). A non-zero flush_timeout_count means the 500 ms safety net fired: the DC interrupt was missed at least once.
What you should observe — mtb-only
There is no REPL, so no ui._diag(). The programmatic equivalent is a C-side call to ipc_ui_platform_diag(out, max_words) with max_words >= 10 from your own CM55 code (it returns the same ten words). Otherwise: LED2 OFF (:459-468) plus the Home grid.

Step 4 — Prove IPC survives a display failure

Do not induce one. Reason from the code instead: ipc_lcd_init(NULL) sits after the ipc_only: label and is not gated on display_ok; ipc_ui_init(NULL) is gated because it creates an LVGL timer (ipc_ui.c:911). tesaiot_display_ready therefore becomes 1 even when the panel never came up, and app_task proceeds.

What you should observe
On mtb-mpy, on a board with a display step failure (LED2 N-blinks), the console still works and import ui still succeeds — the pipe was set up at step 3, before anything display-related. tesaiot_display_ready's tri-state (proj_cm55/main.c:138: 0=pending, 1=display+IPC, 2=IPC-only (headless)) is why consumers test == 0, never == 1.

Traps

  • HardFault if step 3 is reversed. RegisterCallback before cm55_ipc_communication_setup() — 08_IPC_API.md:708, :765-771. The observable is LED1+LED2 in groups of three (HardFault, proj_cm55/main.c:246-254) and 0xDEAD0003 at 0x28000000.
  • 08_IPC_API.md:711-717 prescribes an order the code does not follow. It lists ipc_lcd_init(parent) third and ipc_service_init() fourth with real parents; the shipped task calls ipc_service_init() at step 4 and both ipc_lcd_init(NULL)/ipc_ui_init(NULL) at step 17 with NULL — deliberate deferred binding (tesaiot_display.c:471-476). Only rule #1 (setup first) is load-bearing.
  • Appendix X #1 — CM55 printf is a silent no-op once libbento_edge_ai.a is linked (ai_engine.c:276-288). Do not add prints to trace this sequence; use LED2 or ui._diag().
  • Appendix X #13 — unbounded I2C timeouts on the shared display bus. The GFX task runs at configMAX_PRIORITIES - 1; a wedged clock-stretching device on the touch/panel bus with a block-forever timeout starves every lower task (cm55_sensor_poll.c:49-53).
  • Appendix X #14 — PAGE_ID ordinals are ABI. Step 14's registrations use page_id_t values baked into libbento_ipc.a; renumbering compiles and misbehaves (page_id_ordinal_assert.c:22-25).
  • Console ban. CM33_NS: Booting CM55... is BOOT_VERBOSE. Do not use it as the "CM55 was released" marker; the Home screen is.
  • ui._diag() on mtb-only. It does not exist there. A tutorial step that requires it is an mtb-mpy step.

Variant applicability

Variant
mtb-mpy and mtb-only. CM55 is variant-agnostic — steps 1-18 are the same image. The Verify split is in Step 3: ui._diag() is a MicroPython REPL call and exists only on mtb-mpy; mtb-only verifies with LED2 OFF, the Home grid, or a C-side ipc_ui_platform_diag() call.