SDK for TESAIoT Dev Kit
API reference & tutorials (ModusToolbox)
Loading...
Searching...
No Matches
B1 — CM33_NS boot walk-through

Learning goal

What runs before the scheduler on CM33_NS, and why the order is load-bearing. Fourteen steps in proj_cm33_ns/main.c, traced against the shipped file; the one step where the two variants fork is shown as side-by-side panels. By the end you can say, for any of the fourteen, what breaks if it moves.

The real firmware sequence

proj_cm33_ns/main.c, main() from :209. Steps 1-6 and 8-14 are identical in both variants; step 7 is the fork.

  1. cybsp_init() — :214-217; CY_ASSERT(0) on failure.
  2. __enable_irq() — :220.
  3. init_potentiometer_adc() under #if BSP_HAS_POTENTIOMETER — :222-225. The reason it sits here, from the comment at :132-137: "CM33_NS must initialise + start the SAR ADC before CM55's cm55_sensor_poll can read VR1-4." CM55 is released at step 12; the ADC must already be running.
  4. init_retarget_io() — :228. The UART exists from here on. Nothing before this line can print, which is why steps 1-3 have no observable.
  5. PSA: register the OPTIGA SE driver FIRST, then psa_crypto_init() — :230-247:
/* ...context: inside main(), before any TLS use ... */
/* Phase G: PSA Crypto + OPTIGA SE driver init.
* MUST be called BEFORE any TLS operations (WiFi, MQTT, HTTPS).
* Order: register SE driver FIRST, then init PSA crypto subsystem.
* psa_crypto_init() makes PSA hash functions available for x509 cert parsing
* (required when MBEDTLS_USE_PSA_CRYPTO is enabled). */
{
psa_status_t psa_ret = optiga_psa_register();
if (psa_ret != PSA_SUCCESS) {
printf("[BOOT] optiga_psa_register failed: %d\n", (int)psa_ret);
}
psa_ret = psa_crypto_init();
if (psa_ret != PSA_SUCCESS) {
printf("[BOOT] psa_crypto_init failed: %d\n", (int)psa_ret);
}
}

The comment is the contract: register the secure-element driver, then initialise PSA, before any TLS use. Both failures print; success prints nothing.

  1. Boot banner — #ifdef BOOT_VERBOSE, compiled out (:249-254).
  2. The variant fork — :256-315:
/* ...context: inside main() ... */
#if BENTO_HAS_MPY
/* Create MicroPython REPL task */
BaseType_t xResult = xTaskCreate(
mpy_task_entry,
"MicroPython",
MPY_TASK_STACK_SIZE / sizeof(StackType_t),
NULL,
MPY_TASK_PRIORITY,
NULL
);
if (pdPASS != xResult) {
printf("ERROR: Failed to create MicroPython task\r\n");
CY_ASSERT(0);
}
#else
/* mtb-only. Three jobs the MicroPython task performed at boot have to
* happen here, or they happen nowhere — and all three fail silently:
*
* mount / vfs_mount_script -> bento_storage_init()
* load config mpy_main was the only caller of
* tesaiot_config_init()
* configure IPC pipe sensor_auto_task_create() was the ONLY boot-path
* caller of cm33_ipc_communication_setup(); the two
* handler inits below RegisterCallback on that pipe
* and do not set it up themselves. Without this the
* link is clean, the boot is quiet, and every CM55
* page that talks to CM33 is dead.
*
* (Boot WiFi credentials are read inside sensor_auto_task's C path.)
*
* A failed mount is reported and NOT formatted — the mpy variant formats
* on any mount error, but wiping /main.py and the config to recover a
* transient SMIF fault is the wrong trade on a board people work on. */
/* ...context: inside main(), mtb-only branch ... */
/* Liveness heartbeat over UART, every 10 s.
*
* This variant has no REPL, so with the boot prints muted the only sign of
* life used to be the debugger — and attaching the debugger to a running
* board parks CM33 in a boot-ROM loop (measured 2026-08-28; the mpy
* variant tolerates the same attach, cause not established). One line
* every ten seconds is what let that be discovered at all: a heartbeat
* that kept beating for six minutes detached, after an hour of postmortems
* that all blamed the firmware. Keep it until the variant has an
* instrument that is not also the murder weapon. */
xTaskCreate(bento_heartbeat_task, "HB", 256, NULL, 1, NULL);
if (!bento_storage_init()) {
printf("ERROR: storage unavailable — config and WiFi credentials "
"will use defaults\r\n");
}
if (!tesaiot_config_init()) {
printf("ERROR: tesaiot_config_init failed\r\n");
}
cm33_ipc_communication_setup();
#endif
  • mtb-mpy panel: one xTaskCreate(mpy_task_entry, "MicroPython", …) (8 KB stack, priority 3, :117-118). Storage mount, config load and the IPC pipe do not happen here — they happen inside that task, after the scheduler starts: mpy_main.c:592 mounts the VFS, :559 tesaiot_config_init(), :561 ipc_tesaiot_refresh_status().
  • mtb-only panel: four calls in order — the heartbeat task (:303), bento_storage_init() (:306-309), tesaiot_config_init() (:310-312), cm33_ipc_communication_setup() (:313). The comment at :273-290 says why these must be here: "Three jobs the MicroPython task performed at boot have to happen here, or they happen nowhere — and all three fail silently" — and for the pipe: "Without this the link is clean, the boot is quiet, and every CM55 page that talks to CM33 is dead."

One ordering difference to notice: in mtb-only the config is loaded (:310) before ipc_tesaiot_handler_init() (step 10), so that handler's first read of tls_mode sees real values; in mtb-mpy the config loads later, inside the VM task, which is why mpy_main.c:601 calls ipc_tesaiot_refresh_status() afterwards.

  1. sensor_auto_task_create() — :318; body at bento_libs/claw/common/mpy/sensor_auto_task.c:1487. Creates the WiFi request queue, sets up the IPC pipe if not already done (idempotent — cm55_ipc_communication.c's CM33 twin early-returns on a flag, so the mtb-only call at step 7 is not duplicated work), creates the WiFi IPC worker before registering its IPC callback (comment at :1503-1505: the worker blocks on the queue; priority 2 lets the TCP/IP, WCM and WHD threads pre-empt it during cy_wcm_init()), registers sensor_auto_ctrl_callback, then creates the sensor task.
void sensor_auto_task_create(void) {
if (s_auto_task_handle != NULL) return;
if (s_wifi_req_queue == NULL) {
s_wifi_req_queue = xQueueCreate(AUTO_WIFI_REQ_QUEUE_LEN, sizeof(wifi_ipc_req_t));
if (s_wifi_req_queue == NULL) {
printf("ERROR: Failed to create SensorAuto WiFi queue\r\n");
}
}
if (!s_ipc_initialized) {
cm33_ipc_communication_setup();
Cy_SysLib_Delay(50);
s_ipc_initialized = true;
}
/* Create WiFiIPC worker task BEFORE registering IPC callback.
* Worker blocks on queue — zero CPU when idle. Priority 2 ensures
* TCPIP(4)/WCM(4)/WHD(5) threads can preempt during cy_wcm_init(). */
if (s_wifi_ipc_task_handle == NULL) {
BaseType_t wres = xTaskCreate(
wifi_ipc_worker_task,
AUTO_WIFI_IPC_TASK_NAME,
AUTO_WIFI_IPC_STACK_WORDS,
NULL,
AUTO_WIFI_IPC_TASK_PRIORITY,
&s_wifi_ipc_task_handle
);
if (wres != pdPASS) {
printf("ERROR: Failed to create WiFiIPC task\r\n");
}
}
if (!s_ctrl_cb_registered) {
cy_en_ipc_pipe_status_t st = Cy_IPC_Pipe_RegisterCallback(
CM33_IPC_PIPE_EP_ADDR,
sensor_auto_ctrl_callback,
(uint32_t)CM33_IPC_SENSOR_CTRL_CLIENT_ID);
s_ctrl_cb_registered = (st == CY_IPC_PIPE_SUCCESS);
if (!s_ctrl_cb_registered) {
printf("WARN: SensorAuto IPC ctrl callback registration failed (%lu)\r\n",
(unsigned long)st);
}
}
/* SensorAuto task: reads sensors and pushes data via IPC to CM55.
*
* Eva Kit (USE_KIT_PSE84_EVAL_EPC2):
* CM55 cm55_sensor_poll owns SCB0 I2C (P8[0]/P8[1]) for BMI270,
* CapSense, Potentiometer. But BMM350 is on the separate I3C bus
* (P3[0]/P3[1]) — no contention. So we create the task on Eva Kit
* too, but restrict the enabled mask to BMM350 only. */
#if defined(USE_KIT_PSE84_EVAL_EPC2) && !defined(CM33_OWNS_I2C_SENSORS)
/* Eva Kit AI-Core: CM55 cm55_sensor_poll owns SCB0 I2C — only BMM350 (I3C) */
s_enabled_mask = SENSOR_AUTO_BMM350;
#endif
BaseType_t res = xTaskCreate(
sensor_auto_task_body,
AUTO_TASK_NAME,
AUTO_TASK_STACK_SIZE,
NULL,
AUTO_TASK_PRIORITY,
&s_auto_task_handle
);
  1. init_hsm_optiga_security() — :321 → ipc_hsm_handler_init() (proj_cm33_ns/ipc_hsm_handler.c:2496): two semaphores, two static tasks (HSM_IPC, HsmProv), one Cy_IPC_Pipe_RegisterCallback. What is deliberately absent: optiga_manager_init(). It is the first thing hsm_task_func does after the scheduler starts (ipc_hsm_handler.c:1364), because from main() it would reach xTimerCreate() inside a critical section with no scheduler (:1356-1363). Calling it here once looked harmless and was not.
  2. ipc_tesaiot_handler_init() — :324 (modules/tesaiot_config/ipc_tesaiot_handler.c:370): reads tesaiot_config_get_ptr()->tls_mode, creates the TESAIOT_CFG task, registers its callback. Its [TESAIOT_IPC] handler initialized print is muted by :26 — it will never appear.
  3. init_gfxss_clocks() — three Cy_SysClk_PeriGroupSlaveInit calls (GPU, DC, MIPI-DSI):
/*******************************************************************************
* GFXSS Clock Init (GPU + Display Controller + MIPI-DSI)
*
* Our BSP design.modus does NOT include GFXSS, so init_cycfg_peripherals()
* never enables these peripheral clocks. CM33_NS must enable them before
* CM55 can initialise the display pipeline (DCNano + VGLite + MIPI-DSI).
******************************************************************************/
static void init_gfxss_clocks(void)
{
Cy_SysClk_PeriGroupSlaveInit(CY_MMIO_GFXSS_GPU_PERI_NR,
CY_MMIO_GFXSS_GPU_GROUP_NR,
CY_MMIO_GFXSS_GPU_SLAVE_NR,
CY_MMIO_GFXSS_GPU_CLK_HF_NR);
Cy_SysClk_PeriGroupSlaveInit(CY_MMIO_GFXSS_DC_PERI_NR,
CY_MMIO_GFXSS_DC_GROUP_NR,
CY_MMIO_GFXSS_DC_SLAVE_NR,
CY_MMIO_GFXSS_DC_CLK_HF_NR);
Cy_SysClk_PeriGroupSlaveInit(CY_MMIO_GFXSS_MIPIDSI_PERI_NR,
CY_MMIO_GFXSS_MIPIDSI_GROUP_NR,
CY_MMIO_GFXSS_MIPIDSI_SLAVE_NR,
CY_MMIO_GFXSS_MIPIDSI_CLK_HF_NR);
}

The BSP design.modus does not include GFXSS; CM33_NS must enable these clocks before CM55 can initialise the display pipeline.

  1. init_cm55_boot() → Cy_SysEnableCM55(MXCM55, CM55_APP_BOOT_ADDR, CM55_BOOT_WAIT_TIME_USEC):
static void init_cm55_boot(void)
{
Cy_SysEnableCM55(MXCM55, CM55_APP_BOOT_ADDR, CM55_BOOT_WAIT_TIME_USEC);
}

Why IPC before this line: the three callbacks from steps 8-10 are registered on a pipe configured before CM55 starts executing. The code does not state it in one sentence; assemble it from the registrations (sensor_auto_task.c:1521, ipc_hsm_handler.c:2519, ipc_tesaiot_handler.c:400), this release, and the failure mode at :284-288.

  1. BLE block — #if ENABLE_PAGE_BENTO_BUDDY (:342-376). Independent of BENTO_HAS_MPY, but compiled out by default (Makefile:64,:305 ENABLE_PAGE_BENTO_BUDDY ?= 0). Group I.
  2. vTaskStartScheduler() — :379; never returns; CY_ASSERT(0) after.

Step-by-step

Step 1 — Boot with the console open, and read the PSA step

Press reset with the terminal from A0 attached.

What you should observe
Nothing from steps 1-4 (no UART yet) and nothing from step 5 on success. Only the failure forms exist: [BOOT] optiga_psa_register failed: d or [BOOT] psa_crypto_init failed: d (main.c:238, :242). Success is silent. If you see either line, every TLS operation in Groups C and D will fail later; fix this first.

Step 2 — Watch the fork resolve (per variant)

What you should observe — mtb-only
Any storage or config failure prints immediately, in this order: storage: SMIF setup failed 0x%08lx or storage: mount failed (d); volume left untouched (bento_storage.c:108, :131), then ERROR: storage unavailable — config and WiFi credentials will use defaults (main.c:306-308), then possibly ERROR: tesaiot_config_init failed (main.c:310-312). On a healthy board none of these appear, and ten seconds after the scheduler starts the first [HB] t=lus tasks=u arrives (main.c:110-112).
What you should observe — mtb-mpy
[MPY] GC heap u KB @ p in s (mpy_main.c:552-554) — the VM task started. The VFS mount, config load and credential read happen after this line, inside the task, and are silent on success. Note the divergence: the mpy mount script formats the volume on any mount error (mpy_main.c:487-490, bare except: → mkfs); mtb-only never formats (Chapter G1).

Step 3 — Confirm CM55 was released with IPC ready

What you should observe
The Home screen renders (Chapter B2). CM55's own failures are LED codes, not text — LED2 toggling every 50 ms means tesaiot_display_init() failed (proj_cm55/main.c:193-199); LED1 every 50 ms means CM55's cybsp_init failed (:180-188); both at 100 ms means app_task creation failed (:217-224). See Chapter A3. On mtb-mpy, ui._diag() at the REPL answering at all proves the pipe from steps 8-12 is up.

Step 4 — Say what moves and what breaks

For each of the following, state the consequence before reading the answer:

  • Move step 3 after step 12 → CM55's cm55_sensor_poll reads a SAR ADC that is not running (main.c:132-137).
  • Swap the two calls in step 5 → PSA has no SE driver when TLS first signs; the failure surfaces in Chapter C4, not here.
  • In mtb-only, drop cm33_ipc_communication_setup() from step 7 → the handler inits in steps 9-10 RegisterCallback on an unconfigured pipe; "the link is clean, the boot is quiet, and every CM55 page that talks to CM33 is dead"* (main.c:284-288).
  • Call optiga_manager_init() in step 9 → xTimerCreate() inside a critical section with no scheduler (ipc_hsm_handler.c:1356-1363).
  • Move step 11 after step 12 → CM55 display bring-up fails at step 1 (Cy_GFXSS_Init); LED2 blinks once, repeating.
What you should observe
Your answers match the citations. Nothing is flashed in this step.

Traps

  • Expecting boot text. CM33_NS: Booting CM55..., CM33_NS: CM55 boot initiated, [BOOT] HSM OPTIGA handler OK and the banner are all BOOT_VERBOSE — compiled out. [TESAIOT_IPC] handler initialized is macro-muted (ipc_tesaiot_handler.c:26).
  • Appendix X #11 — mtb-mpy credential flush never runs under a looping /main.py (mpy_main.c:709/:686). The boot read in step 7's mpy panel is fine; the write back is deferred to a REPL-idle flusher. Chapter C1.
  • Appendix X #19 — weak-consumed HSM symbols. Step 9's handler calls publish_csr and friends through weak pointers that are NULL unless ENABLE_OPTIGA_CLM=1; Chapter D3.
  • Appendix X #16 — debugger attach on mtb-only. If you attach to trace these steps on a running mtb-only board, CM33 parks in a boot-ROM loop (main.c:292-302). Halt-on-reset from a fresh flash instead.
  • The pot-ADC step is conditional. BSP_HAS_POTENTIOMETER is 1 on the Eva Kit and the TESAIoT Dev Kit base, 0 on the bare AI Kit; do not look for it on a board that does not have it.

Variant applicability

Variant
mtb-mpy and mtb-only — with the fork at step 7 shown as two panels. Steps 1-6 and 8-14 are the same code on both. The mpy_secure calls on this path (tacp_init, lfs_wifi_creds_* from mpy_main.c:570, :589-604) exist only under BENTO_HAS_MPY=1 (proj_cm33_ns/Makefile:458-462).