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 included in this package. 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 CM55 code produces nothing by default, and how to change that.

Firmware sequence

Event plane — the packed heartbeat. Inside a set no per-model intent event is sent: the intent byte could report only the set, not the member that fired (read members with ai_engine_snapshot_model()). The 1 s heartbeat sent while a model runs therefore carries packed counters in one 32-bit value:

Bits Value Meaning
[31:28] ai_engine_init_calls() & 0xF 0 = the task never reached the model's init()
[27:20] ai_engine_last_init_rc() & 0xFF 0 = ok; otherwise the model's failure code
[19:16] ai_engine_feeds() & 0xF low nibble of the model-intake counter
[15:12] ai_engine_inits() & 0xF completed cold loads
[11:0] ai_engine_dq_ok() & 0xFFF verdicts published

Read the feeds nibble across consecutive beats: 0 on every beat = no new IMU sample reaches the model (the CM33 sensor push is not advancing); non-zero but frozen = the push stopped mid-run; changing with dq_ok climbing = feed and inference healthy, so if result() is still None the break is in the result read path; changing with dq_ok == 0 = samples arrive but no verdict is published, for one of two reasons — the model's window never completes (model, rate or geometry mismatch), or every inference ran past the Ethos-U wait bound and its result was withheld. stale_drops tells those two apart.

Pull plane — MODEL_LINK_Q_DIAG. Answered even while inference is stalled, and surfaced as edge_ai.diag(). Its keys map to the getters in the field guide below.

Screen plane — the stats row. The Edge AI page renders push/s feed/s flush/s … init N/M rc R stale S. Feed rate is a delta over at least one second:

static uint32_t last_feeds, last_ms;
if ((now_ms - last_ms) >= 1000u) {
const uint32_t feeds = ai_engine_feeds();
const uint32_t feed_hz = ((feeds - last_feeds) * 1000u) / (now_ms - last_ms);
last_feeds = feeds;
last_ms = now_ms;
}
uint32_t ai_engine_feeds(void)
Samples accepted by the model; take the delta over at least 1 s.

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.
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 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(): 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 no-op by default. CM55 has no console: CM33_NS owns the UART, and newlib stdio on CM55 would take a mutex that is never initialised and abort the core. So proj_cm55/bento_cm55_stdio.c — open source you can read and change — defines printf and puts as weak** no-ops. libbento_edge_ai.a defines neither (they are not in lib/edge_ai/api.txt, and overridable.txt is empty). Your options: define your own printf, which is strong and wins the link, or route CM55 diagnostics over IPC to the CM33_NS console. That file's header has the details.
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