SDK for TESAIoT Dev Kit
API reference & tutorials (ModusToolbox)
Loading...
Searching...
No Matches
E4 — Reading the Edge AI diagnostics
Variant
mtb-mpy and mtb-only

The screen diagnostics row is common to both variants. edge_ai.diag() and ui._diag() are MicroPython REPL calls and exist on mtb-mpy only; the mtb-only equivalent is the screen row plus a C-side call to ipc_ui_platform_diag().

Note
Credit: the Edge AI models are Infineon's, not TESAIoT's. The motion, audio and radar models this firmware ships (proj_cm55/modules/ai_models/model_*.c) are DEEPCRAFT™ Studio exports, copyright Imagimob AB, an Infineon Technologies company. The Siren, Cough and Factory Alarm models are DEEPCRAFT™ Ready Models by the same author, published by Infineon under the Imagimob AI Model Evaluation License Agreement, and are not redistributed here. TESAIoT trained none of them and owns none of them — what is ours is the engine around them. The shipped model sources carry a bare "All Rights Reserved" reservation and no grant of any kind, so they are credited here rather than licensed on. Our use is research and teaching, and not commercial; a reader who needs a grant must obtain it from Infineon and Imagimob. Train your own at https://www.infineon.com/design-resources/embedded-software/deepcraft-edge-ai-solutions/deepcraft-studio Full credit and the clause citations: THIRD_PARTY_NOTICES.md §2.2, §2.4 and §4.3.

Learning goal

Read the three diagnostic surfaces — the packed heartbeat word, the on-screen stats row, and the edge_ai.diag() dictionary — and apply the one pass condition that matters: npu_cycles advancing while stale_drops stays flat. You will also learn why printf in your own CM55 code produces nothing.

Real firmware sequence

Event plane — the packed heartbeat. Inside a set the intent-event branch is skipped entirely (deepcraft_task.c:831-836, :853-866: every member writes the same slot and the intent byte cannot say which model fired), so the heartbeat carries packed counters instead:

Origin
Lifted from deepcraft_task.c:880-917 (compiled into the prebuilt archive; not shipped as source).

Pull plane — MODEL_LINK_Q_DIAG. Answered in the IPC callback (deepcraft_task.c:356-395) so it still arrives when ai_task is wedged; surfaced as edge_ai.diag() (modedgeai.c:438-489). The getters it reads:

Origin
Lifted from deepcraft_task.c:371-382 (compiled into the prebuilt archive; not shipped as source).

Screen plane — the stats row. page_edge_ai.c:1455-1479 renders push/s feed/s flush/s … init N/M rc R stale S:

Origin
Lifted from page_edge_ai.c:1454-1477 (compiled into the prebuilt archive; not shipped as source).

Feed rate is a delta over at least one second:

Origin
Lifted from page_edge_ai.c:1319-1328 (compiled into the prebuilt archive; not shipped as source).

Field guide

Field Getter How to read it
npu_cycles ai_engine_npu_cycles() u64 split hi/lo over the wire. Advancing = the NPU is being driven. Not proof an inference completed.
stale_drops ai_engine_stale_drops() Must stay flat while npu_cycles advances — the pass condition. ml_state alone is untrustworthy (modedgeai.c:432-437).
feeds ai_engine_feeds() Only the diag "feed" field reports model intake. Derive Hz over ≥1 s.
dq_ok / dq_calls ai_engine_dq_ok() / ai_engine_dq_calls() dq_ok frozen while dq_calls climbs = NPU stall. dq_calls has no shipped caller — authored pairing below.
init_calls / init_returns ai_engine_init_calls() / ai_engine_init_returns() Read together. "2/1" = entered init and never left.
last_init_rc ai_engine_last_init_rc() 0x7FFFFFFF = never called; 0 = ok.
inits ai_engine_inits() Packed nibble in the heartbeat word.
stack_words / stack_free_words ai_engine_stack_words() / ai_engine_stack_free_words() 0 words = engine never started. stack_free_words is an O(stack) scan — ~1 Hz, never per frame.

The authored dq_calls pairing:

static void bento_ex_ai_engine_dq_calls(void)
{
static uint32_t last_calls, last_ok;
if (ai_engine_stack_words() == 0u) {
return; /* engine never started — no data */
}
/* Call this block at >= 1 s intervals (e.g. the same 1 Hz slot as
* the watchdog); the deltas are the signal, not the totals. */
uint32_t calls = ai_engine_dq_calls();
uint32_t ok = ai_engine_dq_ok();
uint32_t d_calls = calls - last_calls;
uint32_t d_ok = ok - last_ok;
last_calls = calls;
last_ok = ok;
if (d_calls > 0u && d_ok == 0u) {
/* NPU stall signature: dequeues attempted, none succeeded over
* the whole interval. Surface it — do not silently keep polling.
* (ml_state alone is untrustworthy for this; the counters are
* the evidence.) */
}
}

Step by step

Step 1 — Read the screen row (both variants)

Select any model on the Edge AI page and wait for CHIP_RUNNING.

What you should observe. The stats row updates about once per second. feed/s is non-zero, the init N/M pair has N == M, rc 0, and stale holds a constant value while the model runs.

Step 2 — Apply the pass condition (mtb-mpy)

>>> a = edge_ai.diag(); import time; time.sleep(2); b = edge_ai.diag()
>>> b['npu_cycles'] > a['npu_cycles'], b['stale_drops'] == a['stale_drops']
(True, True)

What you should observe. (True, True). Any other combination is a stall or a starved feed; see the field guide.

Step 3 — Read the GFX task's numbers (per variant)

mtb-mpy:

>>> ui._diag()

What you should observe. The ten words returned by ipc_ui_platform_diag() (tesaiot_display.c:593-609): DC IRQ status, flush counters (flush_start_count incrementing = frames are flowing), GFX stack HWM, idle %. ui._diag() is modui.c:1470.

mtb-only: there is no REPL. Call ipc_ui_platform_diag(out, max_words) from your own CM55 code with max_words >= 10; it returns 10 words. That accessor — not the g_tesaiot_display_diag struct — is the supported path.

Step 4 — Confirm nothing on the UART is Edge AI

What you should observe. On mtb-mpy the console shows [MPY] at boot and nothing per inference; on mtb-only it shows [HB] t=lus tasks=u every 10 s and nothing else. Do not wait for an Edge AI line — none exists. CM55 does not own a UART (proj_cm55/main.c:9-10).

Traps

Warning
CM55 printf is a silent no-op once libbento_edge_ai.a is linked. The archive defines printf and puts as deliberate no-op stubs (ai_engine.c:276-288; nm -S shows 8- and 4-byte T symbols). Rationale at ai_engine.c:270-275: CM33_NS owns the UART, and newlib stdio on CM55 would take a foreign lock and panic. Any printf in your own CM55 code produces nothing, and you cannot link newlib stdio into this core. These two symbols are an ABI hazard, not API.
ml_state is not a health signal. Use the cycles/stale pair.
Do not call stack_free_words() per frame.

Variant box

mtb-mpy mtb-only
Engine diag edge_ai.diag() + screen row Screen row; C-side getters from your own CM55 code
GFX diag ui._diag() C-side ipc_ui_platform_diag()
Console [MPY] only [HB] only