บทเรียน 7.3 — เพิ่มโมเดลของเราเอง: สามการแก้ สัญญาสี่ฟังก์ชัน และ Vela

เพิ่มโมเดลของเราเองเข้าเฟิร์มแวร์

โมดูล 7 — ใต้ฝากระโปรงและการต่อเติม

โมดูล 7–8 · Researcher — ลงไปแก้เครื่องยนต์เอง

คาถาประจำบทเรียน: "การเพิ่มโมเดลใหม่ให้บอร์ดรู้จัก ใช้แค่ 3 การแก้ — Makefile หนึ่งบรรทัด, C ROW หนึ่งก้อน, ไฟล์โมเดลหนึ่งไฟล์ แล้วมันจะโผล่ใน edge_ai.models() เอง"

MicroPython + C บนบอร์ด BENTO (PSoC Edge · Cortex-M55 + Ethos-U55 NPU)

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

เปิดบทเรียนด้วยของจริงก่อน

ทั้งคอร์สเราเรียก edge_ai.models() แล้วได้ตาราง 6 โมเดลกลับมา วันนี้เรากลับด้านอีกครั้ง — ไม่ถามว่า "มีโมเดลอะไร" แต่ถามว่า "ทำยังไงโมเดลถึงโผล่ในตารางนั้น" แล้วลงมือเพิ่มตัวที่ 7 ด้วยมือเราเอง

โมเดลที่มีอยู่ 6 ตัวในทะเบียน แกะทะเบียน ROW เข้ามายังไง เพิ่มของเรา 3 การแก้ โผล่ในเมนู models() +1

น่าสนใจตรงที่เฟิร์มแวร์จริง มีสล็อตของโมเดลตัวที่ 7 รออยู่แล้ว — FALL_ROW, GESTURE_ROW, KEYWORD_ROW ถูกเขียนไว้เป็นคอมเมนต์ในซอร์ส เป็นตัวอย่างที่พร้อมปลุกให้ทำงาน เราจะเริ่มจากตรงนั้น

ชุดบทเรียนนี้เป็นสาย Researcher — เราไม่ได้แค่เรียก API แล้ว วันนี้เราเปิดฝากระโปรง เพิ่มโมเดลเข้าเครื่องยนต์เอง แล้วยืนยันว่ามันโผล่จริง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

เป้าหมายของชุดบทเรียนนี้

จบชุดบทเรียนนี้เราจะเดินครบ แล้วปิดท้ายด้วยโมเดลของเราเองรันบนบอร์ด:

  1. "3 การแก้" ที่ทำให้โมเดลใหม่โผล่ในทะเบียน (Makefile · C ROW · ไฟล์โมเดล)
  2. สัญญา 4 ฟังก์ชัน ที่ทุกโมเดลต้องมี — init / enqueue / dequeue / finalize
  3. AIM_ vs IMAI_ — สองทางได้โมเดลมา (gen จาก DEEPCRAFT vs ready-model .a)
  4. feed คืออะไร ทำไมโมเดลที่ใช้เซนเซอร์เดิม ไม่ต้องเขียน feed ใหม่
  5. Vela — ทำไม MCU ต้องคอมไพล์เพิ่มก่อนโมเดลรันบน NPU ได้
  6. ลงมือ: เพิ่ม Fall Detection ครบ 3 การแก้ แล้วยืนยันด้วย edge_ai.count() / models()

ปลายทางของวันนี้: โมเดลตัวที่ 7 (Fall Detection) โผล่ใน edge_ai.models() เลือกรันแล้วอ่าน verdict ได้จริง

วันนี้เราแตะ C จริง เป็นครั้งแรกของคอร์ส แต่ไม่ต้องเขียนโมเดลเอง — เราแค่ "ต่อสาย" โมเดลที่มีอยู่เข้าทะเบียน แล้วให้ MicroPython มองเห็น

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

ย้อนดู: โมเดลโผล่ใน models() ได้ยังไง (จาก บทเรียน 7.1–7.2)

ชุดบทเรียนก่อนหน้า (บทเรียน 7.1–7.2) เราไล่ stack จากปลายถึงต้น วันนี้เราสนใจแค่ท่อนบน: จาก edge_ai.models() ลงไปถึง s_models[]

edge_ai.models() MicroPython · CM33 IPC model-link Q_COUNT / Q_MODEL ai_engine (CM55) อ่านจาก s_models[] s_models[] ทะเบียน ROW ต่อกัน เราเติมที่นี่วันนี้ ทั้งสายอ่านจาก s_models[] — เพิ่ม ROW ที่ปลายทาง ทั้งสายเห็นเอง

จุดสำคัญ: MicroPython ไม่ได้ hard-code ชื่อโมเดลไว้เลย มันถามลงไปที่ s_models[] ทุกครั้ง เพราะงั้นเราแก้ที่ s_models[] ที่เดียว ทั้งสาย (IPC + Python + จอ) ปรับตามเอง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

หัวใจของชุดบทเรียน — เพิ่มโมเดล = "3 การแก้" เท่านั้น

เอกสารภายในของเฟิร์มแวร์สรุปการเพิ่มโมเดลไว้สั้นมาก มีแค่สามที่ที่ต้องแตะ (ต้องมีซอร์สเฟิร์มแวร์ตัวเต็มซึ่งยังไม่เปิดเผย ใน SDK สาธารณะ ai_engine มาเป็นไลบรารี prebuilt จึงเพิ่มแถวใหม่ด้วย ai_engine_register() ตอนรัน ใส่โมเดลแทนช่องเดิม หรือโหลดโมเดล IMU แบบ staged ตาม ai_model_staged.h แทน Edit 2):

Edit 1 · Makefile เติมชื่อใน AI_MODELS หนึ่งคำ Edit 2 · ai_engine.c เพิ่ม ROW + ต่อ s_models[] หนึ่งก้อน C Edit 3 · ไฟล์โมเดล model_x.c/.h หรือ .a วางลงโฟลเดอร์ แล้ว build+flash — ไม่ต้องแตะ MicroPython หรือ IPC เลย

สังเกตว่า ไม่มี "Edit 4: แก้ MicroPython" — นั่นคือความงามของ shape-driven registry ที่เราจะอธิบายหน้าถัดไป

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

ทำไมแค่ 3 การแก้ถึงพอ — shape-driven

โค้ด MicroPython (modedgeai.c) กับ IPC model-link ไม่รู้จักชื่อโมเดลใดๆ เลย มันแค่ส่งต่อ "รูปร่าง" ที่ s_models[] บอก:

  • ถาม count() → ตอบ sizeof(s_models)/sizeof(...) — เพิ่ม ROW หนึ่งตัว เลขนี้ขึ้นเอง
  • ถาม model(n) → คัดลอก s_models[n].name / sensor / class_labels ส่งกลับ — ไม่มีชื่อไหน hard-code
  • select(n) → บอก ai_task ให้เรียก s_models[n].init/enqueue/dequeue — ผูกด้วย pointer ไม่ใช่ชื่อ
// registry เป็นแค่ array ของ descriptor — เพิ่มสมาชิกก็พอ
static const ai_model_desc_t s_models[] = {
    MOTION_ROW AUDIO_ROW RADAR_ROW COUGH_ROW ALARM_ROW SIREN_ROW
    FALL_ROW          // <- เติมของเราตรงนี้ ทั้งสายเห็นเอง
};
#define MODEL_COUNT ((uint32_t)(sizeof(s_models)/sizeof(s_models[0])))

นี่คือบทเรียนออกแบบซอฟต์แวร์ที่ใช้ได้ทุกที่: ให้ข้อมูล (data) ขับพฤติกรรม อย่าให้ชื่อ (name) ขับ พอทะเบียนเป็น data ล้วน การเพิ่มของใหม่ก็แค่เพิ่มแถวข้อมูล ไม่ต้องไล่แก้โค้ดหลายที่

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

Edit 1 — Makefile: เติมชื่อใน AI_MODELS

การแก้แรกง่ายสุด บอก build system ว่า image นี้จะบรรจุโมเดลอะไรบ้าง:

# proj_cm55/Makefile
ifeq ($(EDGE_AI_MODEL),combo)
AI_MODELS := motion audio radar cough alarm siren fall
endif                                            # ^ เติม fall
  • Makefile จะ auto-derive ให้เองจากคำว่า fall:
    • นิยาม -DEDGE_AI_MODEL_fall (ตัวที่ #if defined(...) ในซอร์สเช็ก)
    • ดึง ML component + ตั้ง CY_ML_MODEL_MEM
    • (ถ้าเป็น .a) สร้าง LDLIBS guard ให้
  • combo = image ที่บรรจุหลายโมเดลแล้วสลับตอนรันได้ (ที่เราใช้ทั้งคอร์ส)

ข้อควรระวังจากบันทึกจริงของโปรเจกต์: flag ใน .mk ต้องเป็นคำเปล่าๆ เว้นวรรคหลังคำเกินมาจะทำ ifeq พัง — พิมพ์ fall ให้สะอาด อย่ามี trailing space

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

Edit 2 — ai_engine.c: กายวิภาคของ ROW

ROW คือ "บัตรประจำตัวโมเดล" หนึ่งใบ ห่อด้วย #if defined เพื่อให้ image ที่ไม่บรรจุโมเดลนี้ ROW หายไปเฉยๆ (ขยายเป็นค่าว่าง):

#if defined(EDGE_AI_MODEL_fall)
#  include "model_fall.h"
#  define FALL_ROW { .name = "Fall Detection",              \
        .description = "Detects a fall from the IMU",       \
        .sensor = AI_SENSOR_IMU, .class_count = 2,          \
        .class_labels = { "normal", "fall" },               \
        .flash_bytes = 40000u, .period_ms = 200u,           \
        .init = AIM_FALL_init, .enqueue = AIM_FALL_enqueue, \
        .dequeue = AIM_FALL_dequeue, .finalize = AIM_FALL_finalize },
#else
#  define FALL_ROW                       // image ไม่มี fall -> ROW เป็นค่าว่าง
#endif
  • .name / .class_labels = สิ่งที่ edge_ai.models() ส่งกลับไปโชว์บนจอ
  • .sensor เลือกว่าใช้ feed ตัวไหน · .period_ms = จังหวะป้อนข้อมูล
  • สี่ pointer ล่างคือ สัญญา 4 ฟังก์ชัน (หน้าถัดไป)

ก้อนนี้เป็นของจริงจาก ai_engine.c — FALL_ROW เขียนไว้ให้แล้วในซอร์ส เพียงแต่ EDGE_AI_MODEL_fall ยังไม่ถูกนิยาม (จนกว่าเราจะทำ Edit 1)

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

Edit 2 (ต่อ) — ต่อ ROW เข้า s_models[]

เขียน ROW ไว้อย่างเดียวยังไม่พอ ต้อง "เสียบ" มันเข้าทะเบียนด้วย มิฉะนั้นมันลอยอยู่เฉยๆ:

static const ai_model_desc_t s_models[] = {
    MOTION_ROW
    AUDIO_ROW
    RADAR_ROW
    COUGH_ROW
    ALARM_ROW
    SIREN_ROW
    FALL_ROW        // <- เติมบรรทัดนี้ (ลำดับที่นี่ = ลำดับในเมนู)
};
  • ไม่ต้องมีลูกน้ำเพราะแต่ละ _ROW จบด้วย }, ในตัวมาโครเองแล้ว
  • ลำดับใน array นี้ = ลำดับที่โผล่ในเมนู/dropdown ตรงๆ
  • ถ้า image นี้ไม่บรรจุ fall → FALL_ROW ขยายเป็นว่าง → ทะเบียนสั้นลงเองอย่างปลอดภัย

จำ pattern สองจังหวะนี้: ประกาศ ROW (define) แล้ว เสียบเข้า array (ต่อท้าย) เหมือน UI page ในคอร์สเกมที่ต้อง register แล้วต้องใส่การ์ด — ลืมข้อใดข้อหนึ่งแล้ว "มีแต่ไม่โผล่" หรือ "โผล่แต่พัง"

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

Edit 3 — วางไฟล์โมเดล (source vs ready-model)

การแก้ที่สามคือเอา "ตัวโมเดล" มาวางในโฟลเดอร์ proj_cm55/modules/ai_models/ มีสองแบบ:

source-generated (AIM_) model_fall.c + model_fall.h export AIM_FALL_init / enqueue / ... มาจาก DEEPCRAFT converter มี DSP front-end ในตัว · แก้/อ่านได้ ready-model .a (IMAI_) fall_lib_eval.a (binary อย่างเดียว) export IMAI_FALL_init / enqueue / ... objcopy เปลี่ยนชื่อกันชน (prefix) Infineon ส่งมาแบบ eval · ไม่มีซอร์ส
  • ทั้งสองแบบเสียบเข้า ROW ได้เหมือนกัน เพราะ ลายเซ็นฟังก์ชันเหมือนกันเป๊ะ ต่างแค่คำนำหน้า
  • Cough / Alarm / Siren ในเฟิร์มแวร์จริง = ready-model .a ทั้งสาม (แต่ละตัว objcopy prefix ให้ไม่ชนกัน)

ปัญหาใหญ่ของ ready-model คือ สัญลักษณ์ชนกัน (.a หลายตัว export IMAI_init เหมือนกัน) วิธีแก้คือ objcopy เปลี่ยนชื่อเป็น IMAI_COUGH_* / IMAI_ALARM_* ให้ nm เห็นแยกกันสนิท

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

สัญญา 4 ฟังก์ชัน — หัวใจที่ทุกโมเดลต้องมี

ไม่ว่าโมเดลมาจากทางไหน มันต้องให้ครบสี่ฟังก์ชันนี้ (ลายเซ็นตรงเป๊ะ) ROW ถึงจะเสียบได้:

int  <PREFIX>_init(void);                // 0 = ok, <0 = fail (เตรียมโมเดล/arena)
int  <PREFIX>_enqueue(const float *in);  // ป้อน 1 sample/หน้าต่าง เข้าโมเดล
int  <PREFIX>_dequeue(float *out);       // 0 = มี verdict (เติม out[]), <0 = ยังไม่มี/จบ
void <PREFIX>_finalize(void);            // (ไม่ถูกเรียกตอน runtime — มีไว้ครบสัญญา)
  • ai_task บน CM55 วนเรียก enqueue (ป้อนข้อมูลจากเซนเซอร์) แล้ว dequeue (ถามว่ามีคำตอบยัง)
  • return codes: SUCCESS(0) / NODATA(-1) / ERROR(-2) / STREAMEND(-3)
  • <PREFIX> = AIM_FALL (source) หรือ IMAI_FALL (ready .a) — เลือกให้ตรงกับไฟล์ที่วาง

นี่คือ "interface" แบบเดียวกับที่ ROW ผูกด้วย function pointer — ตราบใดที่โมเดลทำตามสัญญานี้ ai_engine ไม่สนใจว่าข้างในเป็น TFLite-Micro, DEEPCRAFT หรืออะไร มันเรียกผ่าน 4 ช่องนี้เท่านั้น

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

คณิตเบื้องหลัง (1) — โมเดลหนึ่งตัวกินหน่วยความจำเท่าไร

โมเดลไม่ได้อยู่ที่เดียว มันแยกร่างลงสองหน่วยความจำ — weights อยู่ใน flash, arena อยู่ใน RAM เขียนเป็นสูตรง่ายๆ ได้แบบนี้:

Mflash=Wweights+Ccode,MRAM=Aarena+BioM_{\text{flash}} = W_{\text{weights}} + C_{\text{code}}, \qquad M_{\text{RAM}} = A_{\text{arena}} + B_{\text{io}}

อ่านทีละตัว (ภาษาคน):

  • MflashM_{\text{flash}} = ที่ที่โมเดลกินใน flash · WweightsW_{\text{weights}} = ก้อนพารามิเตอร์ int8 ของโมเดล (ตัวนี้แหละคือ .flash_bytes = 40000u ที่เราประกาศใน FALL_ROW) · CcodeC_{\text{code}} = โค้ด kernel ที่รันโมเดล
  • MRAMM_{\text{RAM}} = ที่ที่โมเดลกินใน RAM ตอนรัน · AarenaA_{\text{arena}} = tensor arena ที่ TFLite-Micro ใช้พัก activation ระหว่างชั้น · BioB_{\text{io}} = บัฟเฟอร์ input/output (หน้าต่าง feed)

arena ต้องใหญ่แค่ไหน? ใหญ่พอสำหรับ "จังหวะที่ tensor มีชีวิตพร้อมกันเยอะสุด" ในกราฟ:

Aarena  ≥  max⁡t ∑τ ∈ live(t)size(τ)A_{\text{arena}} \;\ge\; \max_{t}\ \sum_{\tau\,\in\,\mathrm{live}(t)} \mathrm{size}(\tau)

ทำไมเรื่องนี้สำคัญ วันนี้: ถ้าเราประเมิน .flash_bytes ต่ำไป weights ก้อนใหญ่จะทะลุ flash wall (0x60900000) — นี่คือเหตุผลที่กฎ checklist บอกให้ย้าย weights ก้อนโตไป section .ml_weights · และถ้า arena ไม่พอ init() จะคืนค่า <0 ทำให้ select() โยน OSError (จำได้ไหมว่าเราห่อ try/except ไว้ทำไม)

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

คณิตเบื้องหลัง (2) — สัญญา 4 ฟังก์ชันในภาษาคณิต

หน้าที่แล้วเราเห็นสี่ฟังก์ชันเป็นโค้ด ทีนี้มองมันเป็นคณิตสั้นๆ — โมเดลหนึ่งตัวคือ ทูเพิลของฟังก์ชันสี่ตัว:

M  ≡  ⟨ init, enqueue, dequeue, finalize ⟩\mathcal{M} \;\equiv\; \langle\, \texttt{init},\ \texttt{enqueue},\ \texttt{dequeue},\ \texttt{finalize} \,\rangle

มันเป็นแบบ streaming — ป้อนทีละ sample สะสมจนเต็มหน้าต่างยาว LL ก่อน ถึงจะมี verdict โผล่:

enqueue(x1), …, enqueue(xL)⏟L ครั้ง  ⟹  dequeue(y)=0\underbrace{\texttt{enqueue}(x_1),\ \dots,\ \texttt{enqueue}(x_L)}_{L\ \text{ครั้ง}} \;\Longrightarrow\; \texttt{dequeue}(y) = 0

พอ dequeue คืน 0 ตัว yy คือคะแนนของแต่ละคลาส เราเลือกคลาสที่คะแนนสูงสุดเป็นคำตอบ:

c^  =  arg⁡max⁡c pc,∑cpc=1\hat{c} \;=\; \arg\max_{c}\, p_c, \qquad \sum_{c} p_c = 1

อ่านทีละตัว: LL = ความยาวหน้าต่าง (ผูกกับ period_ms × sample rate) · pcp_c = ความมั่นใจของคลาส cc (รวมกันได้ 1) · c^\hat{c} = คลาสที่ชนะ คือ label ที่ edge_ai.result() คืนกลับมา

ทำไมเรื่องนี้สำคัญ วันนี้: สี่ฟังก์ชันนี้คือ "หน้าตา" ที่ ROW ผูกด้วย function pointer — โมเดลของเราจะต่อสายเข้าทะเบียนได้ก็ต่อเมื่อมันครบทั้งสี่และลายเซ็นตรงเป๊ะ · และ c^\hat{c} นี่แหละคือเลขที่ฉบับเต็มเอาไปเทียบกับ CONF_FLOOR ก่อนจะเชื่อว่า "ล้มจริง" ไม่ใช่สัญญาณรบกวน

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

feed — ทำไม Fall ไม่ต้องเขียน feed ใหม่

enqueue ต้องการ "ข้อมูลในหน่วยที่โมเดลฝึกมา" ตัวที่แปลงข้อมูลดิบจากเซนเซอร์ให้อยู่ในหน่วยนั้นเรียกว่า feed ในเฟิร์มแวร์มี feed อยู่แล้ว 3 ตัว ตามเซนเซอร์:

feed_imu Motion · Fall feed_radar Push · Gesture feed_audio Baby Cry · Cough · Alarm · Siren enqueue() ของโมเดล Fall ใช้ IMU = ยืม feed_imu
  • .sensor = AI_SENSOR_IMU บอกให้ ai_task ใช้ feed_imu ที่มีอยู่แล้ว — เราไม่ต้องเขียนบรรทัด feed เลย
  • นี่คือเหตุผลที่เราเลือก Fall เป็นตัวอย่างแรก: มันใช้ IMU เหมือน Motion ทุกอย่างต่อสายให้อัตโนมัติ

เอกสารระบุชัด: FALL ยืม feed ของ IMU, GESTURE ยืม radar, KEYWORD ยืม mic — โมเดลที่ใช้เซนเซอร์เดิม ไม่ต้องมี feed ใหม่ เลือกเซนเซอร์ให้ตรงกับที่มีอยู่ งานจะเบาที่สุด

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

ถ้าเซนเซอร์ใหม่จริงๆ — ต้องเขียน feed เอง

ถ้าโมเดลของคุณใช้เซนเซอร์ที่ยังไม่มี feed (ไม่ใช่ IMU/radar/mic) จะมีงานเพิ่มสองที่:

// 1) เขียน feed ใหม่ ลอกโครงจาก feed_imu / feed_radar
static void feed_<sensor>(const ai_model_desc_t *m) {
    // ดึง sample ใหม่สุดจากเซนเซอร์
    // แปลงเป็นหน่วยที่โมเดลฝึกมา (สำคัญ! ต้องตรงกับตอน train)
    // m->enqueue(sample);
}

// 2) เพิ่ม case ใน ai_task dispatch
switch (m->sensor) {
    case AI_SENSOR_IMU:   feed_imu(m);   break;
    case AI_SENSOR_RADAR: feed_radar(m); break;
    case AI_SENSOR_<S>:   feed_<sensor>(m); break;   // <- เพิ่มตรงนี้
}
  • งานหลักของ feed คือ แปลงหน่วย — ดึงค่าดิบแล้วทำให้เหมือนที่โมเดลเห็นตอนฝึก
  • ชุดบทเรียนนี้เราเลี่ยงงานนี้โดยเลือกโมเดลที่ใช้เซนเซอร์เดิม แต่ต้องรู้ว่ามันอยู่ตรงไหนเผื่อวันหน้า

การเลือก "เซนเซอร์เดิม" ไม่ใช่การขี้เกียจ — เป็นการตัดสินใจเชิงวิศวกรรมที่ฉลาด: เริ่มจากเส้นทางที่พิสูจน์แล้วว่าเดินได้ ค่อยขยายทีหลัง (ตรงกับกฎ "reuse proven path, don't guess")

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

เลือกตัวอย่าง — Fall Detection (มีในเฟิร์มแวร์จริง)

ทำไมเราเลือก Fall เป็นโมเดลตัวที่ 7 ที่จะเพิ่ม? เพราะมันเป็น worked example ที่เขียนรออยู่แล้ว ในซอร์ส:

ประเด็น Fall Detection
เซนเซอร์ IMU → ยืม feed_imu (ไม่ต้องเขียน feed)
คลาส normal, fall (2 คลาส)
ROW FALL_ROW เขียนไว้แล้วใน ai_engine.c (เป็นคอมเมนต์รอ)
งานเรา ทำ Edit 1 (Makefile) + ปลดล็อก Edit 2 + วางไฟล์โมเดล
ยืนยัน edge_ai.count() เพิ่มขึ้น, Fall Detection โผล่ใน models()
  • ถ้ายังไม่มีไฟล์ model_fall.c/.h จริง คุณสามารถ retrain ใน DEEPCRAFT Studio แล้ว export หรือห่อโมเดล IMU ที่ฝึกเองในโมดูล 5 ตามสัญญา 4 ฟังก์ชัน
  • โครงงาน "ต่อสาย" ทั้งหมดเหมือนกันหมด ไม่ว่าโมเดลจริงข้างในจะเป็นอะไร

เริ่มจากตัวที่เฟิร์มแวร์ "เกือบพร้อม" อยู่แล้ว ทำให้เราโฟกัสที่ กลไกการเพิ่ม ไม่ใช่ไปติดเรื่องเทรนโมเดล ซึ่งเราทำไปแล้วใน โมดูล 5 (Training)

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

Vela — ทำไม MCU ต้องคอมไพล์เพิ่ม

จำสเปกตรัมเป้าหมายจาก บทเรียน 1.1–1.3 ได้ไหม? Web/Cortex-A ใช้ .tflite เดิมได้เลย แต่ MCU ตัวเดียวที่ต้องคอมไพล์เพิ่ม ก่อนรันบน NPU:

model_int8.tflite จาก Training (5.8–5.9) vela คอมไพล์ให้ Ethos-U55 _vela.tflite NPU อ่านออก · ฝังใน .a/.c Vela แปลง op ให้เป็นคำสั่งที่ Ethos-U55 รันได้ — ที่เหลือ CPU ทำ (fallback)
  • Vela คือ compiler ของ Arm ที่แปลงกราฟ .tflite int8 → คำสั่งเฉพาะของ Ethos-U55 NPU
  • เราทำขั้นนี้ไปแล้วใน บทเรียน 5.8–5.9 (quantize_vela.sh) — ผลลัพธ์ _vela.tflite คือสิ่งที่ฝังในไฟล์โมเดล (Edit 3)
  • Path B (hand-wrap) ต้องใส่ Ethos-U custom op ใน op resolver (AddEthosU()) โมเดลถึงเรียก NPU ได้

int8 คือ "ตัวหารร่วม" ที่ MCU บังคับ — Vela รับเฉพาะ int8 นี่คือเหตุผลที่ โมดูล 5 (Training) เน้น quantization ไม่ใช่แค่ความแม่น แต่เพื่อให้ผ่าน Vela ลง NPU ได้

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

โมเดลที่คุณฝึกเอง — สองเส้นทางสู่ AIM_/IMAI_

ถ้าเป็น .tflite ที่คุณเทรนเองใน โมดูล 5 (Training) จะทำให้บอร์ดรันได้ยังไง? มีสองทาง:

Path A · DEEPCRAFT converter ป้อน .tflite/.keras เข้า converter มันสร้าง model_x.c/.h (AIM_*) ให้ รวม DSP front-end (FFT/mel) ให้ด้วย แนะนำ · เรียบง่ายสุด Path B · hand-wrap TFLite-Micro xxd -i ฝัง _vela.tflite เป็น bytes ตั้ง MicroInterpreter + AddEthosU() เขียน front-end เอง (graph ไม่ทำ FFT) คุมได้เต็ม · งานสาย Researcher

Path A เหมาะกับคอร์สทั่วไป Path B คือสิ่งที่สาย extension/research สอน — คุมทุกอย่างเองแลกกับงานที่มากขึ้น ชุดบทเรียนนี้เราเข้าใจทั้งสอง แล้วเลือก Path ที่เหมาะกับโมเดลของเรา

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0

กับดักตัวจริง — feature parity ไม่ใช่ตัว graph

จุดที่ทำให้โมเดลที่ฝึกดีๆ "ใบ้สนิท" บนบอร์ด มักไม่ใช่กราฟผิด แต่เป็น front-end ไม่ตรงกับตอนเทรน:

  • โมเดลเสียง/เรดาร์ คาดหวัง feature vector เฉพาะ — เช่น Baby Cry = FFT 512 จุด Hann window → mel 20 band → clip → log ต่อหน้าต่าง 60 เฟรม
  • ตัว .tflite ไม่มี ขั้น FFT/mel อยู่ในกราฟ — feed/enqueue ของคุณต้องทำเอง ให้ตรงเป๊ะกับตอนฝึก
  • window/hop/mel/normalization ผิดนิดเดียว = คะแนนเพี้ยนเงียบๆ ไม่มี error ให้จับ
// ใน enqueue: ต้องรัน front-end เดียวกับตอน train ก่อนป้อนเข้า tensor
int AIM_FALL_enqueue(const float *in) {
    // IMU 6 แกน -> normalize เหมือนตอน train -> เขียนลง input tensor
    // (Fall ใช้ IMU ตรงๆ ไม่มี FFT — ง่ายกว่าเสียง/เรดาร์มาก)
}

นี่คือ "the #1 silent failure" ที่เอกสารเตือน — เลือก Fall (IMU) เป็นตัวแรกเพราะ front-end ของมันเบาสุด ไม่มี FFT ให้พลาด พอคล่องแล้วค่อยขยับไปงานเสียง

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก Edge AI Developer (รศ.วิรุฬห์ ศรีบริรักษ์, BUU) · CC BY-NC 4.0