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.
- cybsp_init() — :214-217; CY_ASSERT(0) on failure.
- __enable_irq() — :220.
- 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.
- 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.
- PSA: register the OPTIGA SE driver FIRST, then psa_crypto_init() — :230-247:
{
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.
- Boot banner — #ifdef BOOT_VERBOSE, compiled out (:249-254).
- The variant fork — :256-315:
#if BENTO_HAS_MPY
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
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.
- 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;
}
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);
}
}
#if defined(USE_KIT_PSE84_EVAL_EPC2) && !defined(CM33_OWNS_I2C_SENSORS)
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
);
- 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.
- 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.
- init_gfxss_clocks() — three Cy_SysClk_PeriGroupSlaveInit calls (GPU, DC, 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.
- 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.
- 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.
- 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).