จบชุดบทเรียนนี้เราจะเดินครบ แล้วปิดท้ายด้วยโมเดลของเราเองรันบนบอร์ด:
init / enqueue / dequeue / finalize.a)edge_ai.count() / models()ปลายทางของวันนี้: โมเดลตัวที่ 7 (Fall Detection) โผล่ใน edge_ai.models() เลือกรันแล้วอ่าน verdict ได้จริง
วันนี้เราแตะ C จริง เป็นครั้งแรกของคอร์ส แต่ไม่ต้องเขียนโมเดลเอง — เราแค่ "ต่อสาย" โมเดลที่มีอยู่เข้าทะเบียน แล้วให้ MicroPython มองเห็น
ชุดบทเรียนก่อนหน้า (บทเรียน 7.1–7.2) เราไล่ stack จากปลายถึงต้น วันนี้เราสนใจแค่ท่อนบน: จาก edge_ai.models() ลงไปถึง s_models[]
จุดสำคัญ: MicroPython ไม่ได้ hard-code ชื่อโมเดลไว้เลย มันถามลงไปที่
s_models[]ทุกครั้ง เพราะงั้นเราแก้ที่s_models[]ที่เดียว ทั้งสาย (IPC + Python + จอ) ปรับตามเอง
เอกสารภายในของเฟิร์มแวร์สรุปการเพิ่มโมเดลไว้สั้นมาก มีแค่สามที่ที่ต้องแตะ (ต้องมีซอร์สเฟิร์มแวร์ตัวเต็มซึ่งยังไม่เปิดเผย ใน SDK สาธารณะ ai_engine มาเป็นไลบรารี prebuilt จึงเพิ่มแถวใหม่ด้วย ai_engine_register() ตอนรัน ใส่โมเดลแทนช่องเดิม หรือโหลดโมเดล IMU แบบ staged ตาม ai_model_staged.h แทน Edit 2):
สังเกตว่า ไม่มี "Edit 4: แก้ MicroPython" — นั่นคือความงามของ shape-driven registry ที่เราจะอธิบายหน้าถัดไป
โค้ด MicroPython (modedgeai.c) กับ IPC model-link ไม่รู้จักชื่อโมเดลใดๆ เลย มันแค่ส่งต่อ "รูปร่าง" ที่ s_models[] บอก:
count() → ตอบ sizeof(s_models)/sizeof(...) — เพิ่ม ROW หนึ่งตัว เลขนี้ขึ้นเองmodel(n) → คัดลอก s_models[n].name / sensor / class_labels ส่งกลับ — ไม่มีชื่อไหน hard-codeselect(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 ล้วน การเพิ่มของใหม่ก็แค่เพิ่มแถวข้อมูล ไม่ต้องไล่แก้โค้ดหลายที่
การแก้แรกง่ายสุด บอก build system ว่า image นี้จะบรรจุโมเดลอะไรบ้าง:
# proj_cm55/Makefile
ifeq ($(EDGE_AI_MODEL),combo)
AI_MODELS := motion audio radar cough alarm siren fall
endif # ^ เติม fall
fall:
-DEDGE_AI_MODEL_fall (ตัวที่ #if defined(...) ในซอร์สเช็ก)CY_ML_MODEL_MEM.a) สร้าง LDLIBS guard ให้combo = image ที่บรรจุหลายโมเดลแล้วสลับตอนรันได้ (ที่เราใช้ทั้งคอร์ส)ข้อควรระวังจากบันทึกจริงของโปรเจกต์: flag ใน
.mkต้องเป็นคำเปล่าๆ เว้นวรรคหลังคำเกินมาจะทำifeqพัง — พิมพ์fallให้สะอาด อย่ามี trailing space
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 = จังหวะป้อนข้อมูลก้อนนี้เป็นของจริงจาก
ai_engine.c—FALL_ROWเขียนไว้ให้แล้วในซอร์ส เพียงแต่EDGE_AI_MODEL_fallยังไม่ถูกนิยาม (จนกว่าเราจะทำ Edit 1)
เขียน ROW ไว้อย่างเดียวยังไม่พอ ต้อง "เสียบ" มันเข้าทะเบียนด้วย มิฉะนั้นมันลอยอยู่เฉยๆ:
static const ai_model_desc_t s_models[] = {
MOTION_ROW
AUDIO_ROW
RADAR_ROW
COUGH_ROW
ALARM_ROW
SIREN_ROW
FALL_ROW // <- เติมบรรทัดนี้ (ลำดับที่นี่ = ลำดับในเมนู)
};
_ROW จบด้วย }, ในตัวมาโครเองแล้วfall → FALL_ROW ขยายเป็นว่าง → ทะเบียนสั้นลงเองอย่างปลอดภัยจำ pattern สองจังหวะนี้: ประกาศ ROW (define) แล้ว เสียบเข้า array (ต่อท้าย) เหมือน UI page ในคอร์สเกมที่ต้อง register แล้วต้องใส่การ์ด — ลืมข้อใดข้อหนึ่งแล้ว "มีแต่ไม่โผล่" หรือ "โผล่แต่พัง"
การแก้ที่สามคือเอา "ตัวโมเดล" มาวางในโฟลเดอร์ proj_cm55/modules/ai_models/ มีสองแบบ:
.a ทั้งสาม (แต่ละตัว objcopy prefix ให้ไม่ชนกัน)ปัญหาใหญ่ของ ready-model คือ สัญลักษณ์ชนกัน (
.aหลายตัว exportIMAI_initเหมือนกัน) วิธีแก้คือ objcopy เปลี่ยนชื่อเป็นIMAI_COUGH_*/IMAI_ALARM_*ให้ nm เห็นแยกกันสนิท
ไม่ว่าโมเดลมาจากทางไหน มันต้องให้ครบสี่ฟังก์ชันนี้ (ลายเซ็นตรงเป๊ะ) 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 (ถามว่ามีคำตอบยัง)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 ช่องนี้เท่านั้น
โมเดลไม่ได้อยู่ที่เดียว มันแยกร่างลงสองหน่วยความจำ — weights อยู่ใน flash, arena อยู่ใน RAM เขียนเป็นสูตรง่ายๆ ได้แบบนี้:
อ่านทีละตัว (ภาษาคน):
.flash_bytes = 40000u ที่เราประกาศใน FALL_ROW) · = โค้ด kernel ที่รันโมเดลarena ต้องใหญ่แค่ไหน? ใหญ่พอสำหรับ "จังหวะที่ tensor มีชีวิตพร้อมกันเยอะสุด" ในกราฟ:
ทำไมเรื่องนี้สำคัญ วันนี้: ถ้าเราประเมิน
.flash_bytesต่ำไป weights ก้อนใหญ่จะทะลุ flash wall (0x60900000) — นี่คือเหตุผลที่กฎ checklist บอกให้ย้าย weights ก้อนโตไป section.ml_weights· และถ้า arena ไม่พอinit()จะคืนค่า<0ทำให้select()โยนOSError(จำได้ไหมว่าเราห่อtry/exceptไว้ทำไม)
หน้าที่แล้วเราเห็นสี่ฟังก์ชันเป็นโค้ด ทีนี้มองมันเป็นคณิตสั้นๆ — โมเดลหนึ่งตัวคือ ทูเพิลของฟังก์ชันสี่ตัว:
มันเป็นแบบ streaming — ป้อนทีละ sample สะสมจนเต็มหน้าต่างยาว ก่อน ถึงจะมี verdict โผล่:
พอ dequeue คืน 0 ตัว คือคะแนนของแต่ละคลาส เราเลือกคลาสที่คะแนนสูงสุดเป็นคำตอบ:
อ่านทีละตัว: = ความยาวหน้าต่าง (ผูกกับ period_ms × sample rate) · = ความมั่นใจของคลาส (รวมกันได้ 1) · = คลาสที่ชนะ คือ label ที่ edge_ai.result() คืนกลับมา
ทำไมเรื่องนี้สำคัญ วันนี้: สี่ฟังก์ชันนี้คือ "หน้าตา" ที่ ROW ผูกด้วย function pointer — โมเดลของเราจะต่อสายเข้าทะเบียนได้ก็ต่อเมื่อมันครบทั้งสี่และลายเซ็นตรงเป๊ะ · และ นี่แหละคือเลขที่ฉบับเต็มเอาไปเทียบกับ
CONF_FLOORก่อนจะเชื่อว่า "ล้มจริง" ไม่ใช่สัญญาณรบกวน
enqueue ต้องการ "ข้อมูลในหน่วยที่โมเดลฝึกมา" ตัวที่แปลงข้อมูลดิบจากเซนเซอร์ให้อยู่ในหน่วยนั้นเรียกว่า feed ในเฟิร์มแวร์มี feed อยู่แล้ว 3 ตัว ตามเซนเซอร์:
.sensor = AI_SENSOR_IMU บอกให้ ai_task ใช้ feed_imu ที่มีอยู่แล้ว — เราไม่ต้องเขียนบรรทัด feed เลยเอกสารระบุชัด:
FALLยืม feed ของ IMU,GESTUREยืม radar,KEYWORDยืม mic — โมเดลที่ใช้เซนเซอร์เดิม ไม่ต้องมี 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; // <- เพิ่มตรงนี้
}
การเลือก "เซนเซอร์เดิม" ไม่ใช่การขี้เกียจ — เป็นการตัดสินใจเชิงวิศวกรรมที่ฉลาด: เริ่มจากเส้นทางที่พิสูจน์แล้วว่าเดินได้ ค่อยขยายทีหลัง (ตรงกับกฎ "reuse proven path, don't guess")
ทำไมเราเลือก 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)
จำสเปกตรัมเป้าหมายจาก บทเรียน 1.1–1.3 ได้ไหม? Web/Cortex-A ใช้ .tflite เดิมได้เลย แต่ MCU ตัวเดียวที่ต้องคอมไพล์เพิ่ม ก่อนรันบน NPU:
.tflite int8 → คำสั่งเฉพาะของ Ethos-U55 NPUquantize_vela.sh) — ผลลัพธ์ _vela.tflite คือสิ่งที่ฝังในไฟล์โมเดล (Edit 3)AddEthosU()) โมเดลถึงเรียก NPU ได้int8 คือ "ตัวหารร่วม" ที่ MCU บังคับ — Vela รับเฉพาะ int8 นี่คือเหตุผลที่ โมดูล 5 (Training) เน้น quantization ไม่ใช่แค่ความแม่น แต่เพื่อให้ผ่าน Vela ลง NPU ได้
ถ้าเป็น .tflite ที่คุณเทรนเองใน โมดูล 5 (Training) จะทำให้บอร์ดรันได้ยังไง? มีสองทาง:
Path A เหมาะกับคอร์สทั่วไป Path B คือสิ่งที่สาย extension/research สอน — คุมทุกอย่างเองแลกกับงานที่มากขึ้น ชุดบทเรียนนี้เราเข้าใจทั้งสอง แล้วเลือก Path ที่เหมาะกับโมเดลของเรา
จุดที่ทำให้โมเดลที่ฝึกดีๆ "ใบ้สนิท" บนบอร์ด มักไม่ใช่กราฟผิด แต่เป็น front-end ไม่ตรงกับตอนเทรน:
.tflite ไม่มี ขั้น FFT/mel อยู่ในกราฟ — feed/enqueue ของคุณต้องทำเอง ให้ตรงเป๊ะกับตอนฝึก// ใน 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 ให้พลาด พอคล่องแล้วค่อยขยับไปงานเสียง