Skip to content

Adding your own model: three edits, a four-function contract and Vela

Module 7 — Under the hood and extending the firmware · Slides: slides.md · Module overview · Course page

Learn how to make the board know a model of your own. A shape-driven registry means only three edits are needed in the full firmware source — the Makefile, a ROW in s_models[], and the model file. Meet the four-function contract, AIM_ versus IMAI_, reusing an existing sensor’s feed, the flash-versus-arena memory budget, and the equivalent routes in the public SDK.

By the end of this lesson, you will:

  1. Explain why a shape-driven registry needs only three edits, with no change to MicroPython or the IPC, and name the equivalents in the public SDK (ai_engine_register(), filling an existing slot, or a staged IMU model).
  2. Write a model descriptor (name, sensor, class_labels with index 0 as the negative class, period_ms, four function pointers), and state the four-function contract with its return codes.
  3. Split a model’s memory into flash (weights + code) and RAM (tensor arena + buffers), and explain how an arena that’s too small makes select() fail.
  4. Explain why a model on an existing sensor can borrow the existing feed, and why feature parity is the number-one silent failure.

You’ve been through lessons 7.1–7.2, and understand the s_models[] registry, ai_model_desc_t, and the IPC model link. Keep the SDK’s Filling a model slot guide open alongside your screen.

  • Hardware: a TESAIoT Dev Kit board already flashed with BENTO’s MicroPython firmware, or the BENTO Emulator inside BENTO IDE — this is a concept lesson. Actually adding a model requires building the firmware with ModusToolbox; the full source containing ai_engine.c isn’t yet public. The public SDK provides a prebuilt engine and headers to use instead.
  • Prior lesson: lesson 7.2 — Hands-on: tracing the stack from MicroPython

Throughout the course, edge_ai.models() has answered with six models on the board. Today, ask the reverse question: how would a seventh model come to show up in that table? The full firmware source already has a FALL_ROW written as an example, waiting — it just needs to be switched on.

Neither MicroPython (edge_ai) nor the IPC model link knows any model’s name at all. count() counts the size of s_models[], model(n) copies the fields of row n, and select(n) has the task call that row’s function pointers. The registry is therefore shape-driven — data drives behaviour. In the full source, adding a model takes three edits: (1) add the name to AI_MODELS in the Makefile, which derives -DEDGE_AI_MODEL_<name> for you (watch out for extra spaces) (2) write a ROW wrapped in #if defined(...), then append it to s_models[] (the array’s order is the menu’s order), and (3) place the model file in proj_cm55/modules/ai_models/. In the public SDK, ai_engine comes as a prebuilt library, so a new row is added via ai_engine_register(&desc) from your own code at run time, or by filling an existing slot — and the current firmware version can also load a staged IMU model per ai_model_staged.h.

Every model must supply four functions: <PREFIX>_init(void), <PREFIX>_enqueue(const float *in), <PREFIX>_dequeue(float *out), and <PREFIX>_finalize(void), returning 0 on success, -1 (NODATA) while the window isn’t yet full, and -2 on error. A model built from DEEPCRAFT Studio uses the prefix AIM_<NAME>_, while a Ready-Model .a exports IMAI_*, with the same names on every one — so it needs objcopy to rename it to IMAI_<NAME>_* before it can coexist with others. The descriptor’s class_labels must have the negative class (idle, unlabelled, normal) at index 0, since the screen uses the highest score among class 1 and up as the confidence.

Memory splits into $M_{flash} = W_{weights} + C_{code}$ and $M_{RAM} = A_{arena} + B_{io}$, where the arena must be large enough for the largest number of tensors alive at once. If the arena isn’t big enough, init() returns a negative value, and select() raises OSError. Large weights in a combo image can hit the flash ceiling and need moving to the .ml_weights section. A model on an existing sensor can borrow the existing feed right away (Fall uses the IMU, so it borrows feed_imu); a new sensor needs its own feed written to convert raw values into the same units used during training. A self-trained model can be wrapped two ways: DEEPCRAFT’s converter, which generates C with a front end, or wrapping TFLite-Micro yourself with AddEthosU() and writing a matching front end. The real trap is feature parity: a .tflite graph has no FFT or mel filterbank inside it — if the front end is even slightly off, scores drift silently wrong.

This lesson’s slides also reference a file in another lesson or in shared/:

The same questions are in quiz.yaml for automated checking.

  1. Why doesn’t adding a model in the full source require editing MicroPython’s edge_ai module? (single choice · objective 1)

    • a) Because MicroPython already stores every model’s name
    • b) Because the registry is shape-driven — MicroPython asks count() and model(n) from s_models[] every time
    • c) Because IPC sends the model file itself
    • d) Because you’d edit the emulator instead
    Solution

    b — no model’s name is ever hard-coded on the Python side. Add a row at the far end, and the whole chain sees it on its own.

  2. If you’re using the public SDK, where ai_engine comes as a prebuilt library, how would you add a model that has no existing slot? (single choice · objective 1)

    • a) Edit ai_engine.c inside the archive
    • b) Call ai_engine_register(&desc) from your own code at run time
    • c) Add the name to edge_ai.py
    • d) It can’t be done at all
    Solution

    b — Route 0 in the SDK guide lets the engine add a row from your descriptor, with no need to rebuild the archive.

  3. AIM_FALL_dequeue returns −1 after only a few enqueue calls. What does that mean? (single choice · objective 2)

    • a) The model is broken
    • b) NODATA — the window isn’t full yet, keep enqueuing
    • c) The winning class is −1
    • d) The arena isn’t big enough
    Solution

    b — a streaming model needs a full window of L calls before dequeue can return 0 with scores.

  4. You add a model, and it shows up in models(), but select() raises OSError every time. What’s the most likely cause? (single choice · objective 3)

    • a) The model name is too long
    • b) init() returns a negative value, for example because the tensor arena is smaller than the graph needs
    • c) You forgot to add labels
    • d) WiFi isn’t connected
    Solution

    b — select confirms by observation. If the model’s init fails, the engine never switches, so Q_ACTIVE never equals the requested value.

  5. Why did the author choose Fall Detection as the first model to add? (single choice · objective 4)

    • a) Because it’s the most accurate
    • b) Because it uses the IMU, so it can borrow the existing feed_imu, and its front end is the lightest, with no FFT to get wrong
    • c) Because it doesn’t need to go through Vela
    • d) Because it’s float32
    Solution

    b — choose the path that’s already proven first. An audio model needs its FFT and mel filterbank to match training exactly, which is the most common silent failure point.

  • Write out Fall Detection’s ROW by hand in your learning log, pointing out which field ends up in edge_ai.models().
  • Open the SDK’s Filling a model slot guide, and summarise the difference between Route 0 (ai_engine_register) and Route 1 (filling an existing slot) in two lines.
  • Estimate the memory budget of a model with 40,000 bytes of weights and a 20 KB arena — which parts consume flash, and which consume RAM.

In lesson 7.4, we’ll use s19_extend_model.py to build a ROW from a spec, check the registry with count() and models(), then run the model we added.

Next lesson: lesson 7.4 — Hands-on: making a new model show up in edge_ai.models()

  • If you had to add a self-trained audio model, which front-end steps would need to match training exactly?
  • What are the trade-offs between adding a model at build time versus at run time, for a product that updates over OTA?

Review questions

Answer on your own first, then open the answer.

  1. Why does adding a model in the full source need no change to the MicroPython edge_ai module? (Objective 1)

    1. เพราะ MicroPython เก็บชื่อโมเดลไว้ทุกตัวอยู่แล้ว
    2. เพราะทะเบียนเป็น shape-driven MicroPython ถาม count() และ model(n) จาก s_models[] ทุกครั้ง
    3. เพราะ IPC ส่งไฟล์โมเดลให้เอง
    4. เพราะต้องแก้ Emulator แทน
    Show answer

    Answer: B. เพราะทะเบียนเป็น shape-driven MicroPython ถาม count() และ model(n) จาก s_models[] ทุกครั้ง

    ไม่มีชื่อโมเดลใดถูก hard-code ฝั่ง Python เพิ่มแถวที่ปลายทาง ทั้งสายเห็นเอง

  2. With the public SDK, where ai_engine is a prebuilt library, how do you add a model that has no existing slot? (Objective 1)

    1. แก้ ai_engine.c ใน archive
    2. เรียก ai_engine_register(&desc) จากโค้ดของเราตอนรัน
    3. เพิ่มชื่อใน edge_ai.py
    4. ทำไม่ได้เลย
    Show answer

    Answer: B. เรียก ai_engine_register(&desc) จากโค้ดของเราตอนรัน

    Route 0 ของคู่มือ SDK ให้ engine เพิ่มแถวจาก descriptor ของเรา โดยไม่ต้อง rebuild archive

  3. AIM_FALL_dequeue returns −1 after only a few enqueues. What does it mean? (Objective 2)

    1. โมเดลพัง
    2. NODATA หน้าต่างยังไม่เต็ม ต้อง enqueue ต่อ
    3. คลาสที่ชนะคือ −1
    4. arena ไม่พอ
    Show answer

    Answer: B. NODATA หน้าต่างยังไม่เต็ม ต้อง enqueue ต่อ

    โมเดลแบบ streaming ต้องได้ข้อมูลครบหน้าต่าง L ครั้งก่อน dequeue จึงคืน 0 พร้อมคะแนน

  4. The new model appears in models() but select() always raises OSError. What is a likely cause? (Objective 3)

    1. ชื่อโมเดลยาวเกินไป
    2. init() คืนค่าติดลบ เช่น tensor arena เล็กเกินกว่าที่กราฟต้องใช้
    3. ลืมใส่ labels
    4. Wi-Fi ไม่ติด
    Show answer

    Answer: B. init() คืนค่าติดลบ เช่น tensor arena เล็กเกินกว่าที่กราฟต้องใช้

    select ยืนยันด้วยการสังเกต ถ้าโมเดล init ไม่ผ่าน engine ไม่สลับ Q_ACTIVE จึงไม่เคยเท่ากับที่ขอ

  5. Why does the author pick Fall Detection as the first model to add? (Objective 4)

    1. เพราะแม่นที่สุด
    2. เพราะใช้ IMU จึงยืม feed_imu เดิมได้ และ front-end เบาที่สุด ไม่มี FFT ให้พลาด
    3. เพราะไม่ต้องผ่าน Vela
    4. เพราะเป็น float32
    Show answer

    Answer: B. เพราะใช้ IMU จึงยืม feed_imu เดิมได้ และ front-end เบาที่สุด ไม่มี FFT ให้พลาด

    เลือกเส้นทางที่พิสูจน์แล้วก่อน โมเดลเสียงต้องทำ FFT และ mel ให้ตรงกับตอนฝึกซึ่งเป็นจุดพังเงียบที่พบบ่อยที่สุด

Cite this lesson

If you teach from this lesson or reuse it in slides or documents, credit it with the text below. If you changed it, add (adapted) after the title.

"Adding your own model: three edits, a four-function contract and Vela" from TESA Open Knowledge by the Thai Embedded Systems Association (TESA), https://github.com/tesaiot/tesa-qualification-program, licensed under CC BY-NC 4.0

Thai attribution: "เพิ่มโมเดลของเราเอง: สามการแก้ สัญญาสี่ฟังก์ชัน และ Vela" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0

Lesson link: https://tesaiot.github.io/tesa-qualification-program/en/courses/edge-ai-developer/m07-under-the-hood/l03-add-your-own-model/

Full guide: how to cite TESA

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0

Content is licensed CC BY-NC 4.0. Reuse it non-commercially and credit the Thai Embedded Systems Association (TESA) every time. · How to cite TESA