บทเรียน 7.1 — สแตก Edge AI: tri-core, ai_engine, IPC model link และ TFLite-Micro

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

Researcher — อ่านสแตกจริง (เปิดสาย Researcher)

คาถาประจำบทเรียน: "ทุกครั้งที่เราเรียก edge_ai.result() มันวิ่งข้ามคอร์ผ่าน IPC ไปหา NPU แล้วกลับมาเป็น dict — วันนี้เราจะชี้ได้ว่าแต่ละค่ามาจากไฟล์:บรรทัดไหน"

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

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

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

ตลอด 17 ชุดบทเรียนที่ผ่านมาเราเรียก edge_ai มานับครั้งไม่ถ้วน แต่ยังไม่เคยเปิดฝากระโปรงดูว่ามันทำงานยังไง ชุดบทเรียนนี้เราจะ แกะ ไม่ใช่ สร้าง — เอาของที่เราใช้จนคล่องมากางให้เห็นทั้งเส้น

รันของที่คุ้นก่อน result() ที่ใช้ทุกบทเรียน แกะดูข้างใน สแตก 3 ชั้น ชี้ไฟล์:บรรทัด SDK: ai_engine.h พร้อมต่อยอด 7.3–7.4 เพิ่มโมเดล

ยังใช้แนว PRIMM เหมือนทั้งคอร์ส แต่รอบนี้ "Investigate" คือพระเอก — โค้ดที่เราเติมเป็นแค่ เครื่องมือส่อง ไม่ใช่แอปใหม่ ของจริงที่ต้องเข้าใจอยู่ในเฟิร์มแวร์ ซอร์สและเอกสารสถาปัตยกรรมภายในยังไม่เปิดเผย ส่วนที่เปิดคือ header ของ engine และของ IPC model link ใน SDK สาธารณะ

ชุดบทเรียนนี้ไม่มีอะไรใหม่ให้ท่องจำ ทุกอย่างเราแตะมาแล้วทั้งคอร์ส แค่คราวนี้เราจะ "เห็นทั้งเครื่อง" ไม่ใช่แค่ปุ่มที่กด

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

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

จบชุดบทเรียนนี้เราจะไล่สแตก Edge AI ได้ครบ 3 ชั้น แล้วชี้ต้นทางของทุกค่าที่ edge_ai คืนมาได้:

  1. tri-core — PSoC Edge E84 มีสามคอร์ งาน Edge AI อยู่คอร์ไหน ทำไมต้องแบ่ง
  2. ai_engine — เครื่องยนต์อนุมานบน CM55 กับทะเบียน s_models[] และคู่ index s_active/s_current
  3. IPC model link — สะพานข้ามคอร์: control plane (สั่ง) กับ query plane (อ่าน)
  4. TFLite-Micro → NPU — runtime จริงที่รันทั้ง int8 + float32, DEEPCRAFT เป็นแค่ wrapper
  5. ลงมือ: เติม 5 คำสั่งฝั่ง อ่าน ของ edge_ai (links / model / active / result / latency) แล้ว trace ทั้งเส้น

ปลายทางของวันนี้: กด Trace แล้วอ่าน log สามชั้น (transport → control → result) พร้อมชี้ฟังก์ชันหรือฟิลด์ต้นทางของแต่ละชั้นใน ai_engine.h กับ ipc_model_link_defs.h ของ SDK สาธารณะ

ชุดบทเรียนนี้เราวัดกันที่ "อธิบายได้" ไม่ใช่ "รันผ่าน" — โค้ดสั้น แต่ความเข้าใจต้องลึกถึงระดับข้ามคอร์

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

ชุดบทเรียนนี้อยู่ตรงไหนของหลักสูตร

17 ชุดบทเรียนก่อนหน้าเราเดินครบ 5 เสาหลัก: DAQ → Processing → Analysis → Training → Apps ตอนนี้เรามีของครบมือแล้ว บทเรียน 7.1–7.2 เปิดสาย Researcher ด้วยการหันกลับไปมองเครื่องมือที่ใช้มาตลอด

1.1–6.6 ใช้ edge_ai เป็น 7.1–7.2 (วันนี้) เข้าใจว่าใต้ edge_ai คืออะไร 7.3–8.2 เพิ่มโมเดลเอง + capstone
  • Foundation ของชุดบทเรียนนี้ = ทั้งคอร์ส — เราจะเข้าใจสแตกได้ก็ต่อเมื่อเคยใช้มันจริงมาก่อน
  • ทำไมต้องรู้ระดับนี้: ชุดบทเรียนถัดไป (บทเรียน 7.3–7.4) เราจะ เพิ่มโมเดลของตัวเอง ถ้าไม่รู้ว่า s_models[] กับ ROW macro อยู่ตรงไหน ก็เพิ่มไม่ถูกที่

Researcher tier ไม่ใช่ "ยากขึ้น" แต่ "ลึกขึ้น" — เราเลิกเป็นผู้ใช้ API แล้วเริ่มเป็นคนที่แก้/ต่อ API ได้

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

คำถามหลักของชุดบทเรียน — result() มาจากไหน

ลองนึกถึงบรรทัดที่เราเขียนมาตั้งแต่บทเรียน 1.1–1.3:

r = edge_ai.result()      # r['label'], r['conf'], r['scores'], r['seq'] ...

ดูเหมือนเรียกฟังก์ชันธรรมดา แต่จริงๆ บรรทัดนี้ทำงานข้าม สองคอร์:

  • โค้ด Python รันบน CM33_NS แต่โมเดลอนุมานบน CM55 คนละคอร์กัน เรียกฟังก์ชันตรงๆ ไม่ได้
  • result() จึงเป็นการ pull — ส่งคำถามข้ามคอร์ผ่าน IPC แล้วรอ CM55 เติมคำตอบกลับมา
  • ค่าที่ได้มาทุก key (label/conf/scores/seq) มีต้นทางเป็นฟิลด์ใน struct ฝั่ง C ชื่อ ai_result_t s_res

วันนี้เราจะเดินย้อนจาก dict ตัวนี้กลับไปให้ถึงต้นทาง: จาก Python → IPC → ai_engine → NPU แล้วกลับมา ทั้งวงในหนึ่งภาพ

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

tri-core — สามสมองของ PSoC Edge E84

ชิปตัวนี้มีสาม Arm core งาน Edge AI กระจายอยู่สองคอร์ แล้วคุยกับ MicroPython บนอีกคอร์:

CM33_S (secure) boot · TrustZone · provisioning — ไม่มีงาน Edge AI CM33_NS (non-secure) FreeRTOS + MicroPython + เซนเซอร์ (BMI270/BMM350/DPS368/SHT40) · โมดูล edge_ai, sensors, dsp, ui CM55 (high-perf) FreeRTOS + LVGL 9.2 + Ethos-U55 NPU · ai_engine · 6 โมเดล · radar task · deepcraft_task (ปลาย IPC)
  • โค้ด MicroPython ของเรา + เซนเซอร์ IMU/audio อยู่บน CM33_NS
  • NPU + โมเดลหนักๆ + LVGL อยู่บน CM55 (คอร์เร็ว) — เรดาร์ก็ต่อ SPI ที่ CM55 เป็นข้อยกเว้น
  • สองคอร์นี้แลกคำสั่งกับผลกันผ่าน IPC pipe เดียว (เดี๋ยวเจาะ)

ทำไมต้องแยก: งานที่กินแรง (NPU, โมเดล, จอ) ไปอยู่คอร์เร็ว ส่วน REPL กับเซนเซอร์อยู่คอร์ควบคุม — แต่ละคอร์ทำสิ่งที่ตัวเองถนัด

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

ai_engine — เครื่องยนต์อนุมานบน CM55

หัวใจของทั้งสแตกคือไฟล์ proj_cm55/modules/ai_models/ai_engine.c มันคุม FreeRTOS task ชื่อ ai_task ที่รัน ทีละหนึ่งโมเดล เลือกจากทะเบียน s_models[]

s_models[] ทะเบียนโมเดล feed_*() เซนเซอร์ → enqueue dequeue() NPU อนุมาน Ethos-U55 publish() → ai_result_t s_res ai_task วนตลอด: อ่านโมเดลที่ขอ → feed → dequeue → publish วนรอบถัดไป
  • ai_engine คือ แหล่งความจริงเดียว ว่า "ตอนนี้รันโมเดลอะไร และมันตอบว่าอะไร"
  • กฎเหล็ก: ห้าม finalize() โมเดลเพื่อสลับ — เคยทำแล้ว TFLite-Micro interpreter หลุดมือ NPU จน IPC ค้างถาวร ทุกโมเดลจึง resident อยู่ตลอด สลับแค่เปลี่ยน index ที่ถูก feed

จำคำนี้ไว้: "สลับโมเดล = เปลี่ยนว่าใครได้กินข้อมูล ไม่ใช่ปิดเปิดโมเดล" — นี่คือบทเรียนที่แลกมาด้วยการ debug หลายชั่วโมง

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

s_active กับ s_current — ต่างกันหนึ่ง tick

ai_engine เก็บ index สองตัว ที่คนมักสับสน แต่ต่างกันสำคัญมาก:

s_active = โมเดลที่ "ขอ" ตั้งทันทีที่ ai_engine_start() อ่านด้วย ai_engine_requested() s_current = โมเดลที่ "สลับจริง" task ตามมาช้ากว่าได้ถึง 1 tick อ่านด้วย ai_engine_active() → edge_ai.active()
  • edge_ai.active() คืน s_current (โมเดลที่สลับไปแล้วจริง) ไม่ใช่ตัวที่เพิ่งขอ
  • ถ้าอ่าน active() ทันทีหลังสั่ง select อาจได้ค่าเก่า เพราะ task ยังตามไม่ทัน
  • เอกสารเตือน: guard ค่า default ให้ดู requested() (s_active) ไม่ใช่ active() — ไม่งั้นทับ selection ใหม่โดยไม่ตั้งใจ

นี่คือ "gotcha" อันดับหนึ่งของสแตกนี้ ในสคริปต์ฝึกเราจะเรียก active() หลัง select() เพื่อ เห็นกับตา ว่ามันสลับจริงหรือยัง

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

model descriptor + ROW macro

แต่ละแถวใน s_models[] เป็น struct ai_model_desc_t ประกาศด้วย ROW macro ที่มี guard EDGE_AI_MODEL_<name> — โมเดลจะคอมไพล์เข้ามาก็ต่อเมื่อ Makefile ขอ:

#define MOTION_ROW { .name = "Motion Detection", \
    .sensor = AI_SENSOR_IMU, .class_count = 3, \
    .class_labels = { "idle", "circle", "shaking" }, \
    .flash_bytes = 28272u, .period_ms = 200u, \
    .init = AIM_MOTION_init, .enqueue = AIM_MOTION_enqueue, \
    .dequeue = AIM_MOTION_dequeue, .finalize = AIM_MOTION_finalize }
  • ทุกฟิลด์ที่ edge_ai.models() คืนมา (name/sensor/labels) มาจาก ROW นี้ตรงๆ
  • ทะเบียน (และเมนูใน MicroPython) ถูกประกอบ ทั้งหมด จากสิ่งที่ Makefile เลือก — ไม่มี hard-code ฝั่ง Python
  • ทุกโมเดลต้องให้ 4 ฟังก์ชันตรง contract: init / enqueue / dequeue / finalize

นี่คือจุดที่ชุดบทเรียนถัดไป (บทเรียน 7.3–7.4) เราจะแตะ — เพิ่มโมเดล = เพิ่ม ROW หนึ่งแถว + define ใน Makefile + ไฟล์โมเดล วันนี้แค่รู้จักหน้าตามันก่อน

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

feed functions — เซนเซอร์เข้าโมเดลยังไง

ai_task ดูว่าโมเดลปัจจุบันใช้เซนเซอร์อะไร แล้วเรียก feed function ให้ตรงชนิด:

เซนเซอร์ feed fn ทำอะไร
AI_SENSOR_IMU feed_imu() ดึง snapshot BMI270 ผ่าน IPC, remap แกน (-X,-Y,Z), แปลง counts → g/dps, enqueue() ต่อ sequence ใหม่
AI_SENSOR_RADAR feed_radar() ดูด chirp int16 128 sample จาก ring, cast ADC → float (ไม่ scale), enqueue() ต่อ chirp
AI_SENSOR_MIC feed_audio() เริ่ม PDM mic, normalize int16 → [-1,1], วน enqueue+dequeue+publish ต่อ sample
  • feed function คือที่อยู่ของ signal front-end ฝั่ง C — โมเดลเสียงมี FFT/mel front-end รันก่อนกราฟตรงนี้
  • นี่ตอบคำถามคอร์ส "โมเดลเห็นอะไรจริงๆ" — สิ่งที่เข้ากราฟไม่ใช่ raw เซนเซอร์ แต่ผ่าน feed แปลงแล้ว

ถ้าอยากทำ feature เดียวกันซ้ำนอกบอร์ด (เช่นตอน train) ต้องเลียนแบบ feed function นี้ให้เป๊ะ — นี่คือสะพานเชื่อม โมดูล 4 (Analysis) กับโมดูล 5 (Training) ที่เราเดินมา

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

publish() — เขียนผลแบบ lock-free

เมื่อ dequeue() ได้ verdict publish() เขียนลง global ai_result_t s_res โดยไม่ล็อก:

typedef struct {
    uint8_t  model_index, class_count, top_class, running;
    float    scores[AI_MAX_CLASSES];
    uint32_t inference_us, inference_us_max, inferences, seq;
} ai_result_t;
  • ออกแบบเป็น single-writer, tolerated-torn-read — ปิด interrupt รอบ publish() เสี่ยงหน่วง completion IRQ ของ Ethos-U55
  • ผู้อ่าน (หน้าจอ 30 Hz + MicroPython ผ่าน IPC) ยอมรับ "หนึ่งเฟรมเก่า" แลกกับความเร็ว
  • seq เพิ่มทุกครั้งที่ publish — เราใช้มันเช็ก "มีผลใหม่ไหม" (นี่คือ r['seq'] ที่เราเช็กในลูปทุกบทเรียน)

ทุก key ใน dict ที่ result() คืน map ตรงกับฟิลด์ใน struct นี้ — scores→scores, seq→seq, latency_ms→inference_us/1000, top→top_class

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

6 โมเดลที่ shipped มา — int8 อยู่กับ float32

build แบบ combo รวม 6 โมเดลในภาพเดียว สลับได้ตอนรัน:

โมเดล เซนเซอร์ ชนิด ที่มา
Motion Detection IMU int8 source-gen model_motion.c
Baby Cry Detection MIC int8 source-gen model_audio.c
Push Detection RADAR float32 source-gen model_radar.c
Cough Detection MIC int8 ready-model cough_lib_eval.a
Alarm Detection MIC int8 ready-model alarm_lib_eval.a
Siren Detection MIC float32 ready-model siren_lib_eval.a
  • source-generated — C ที่ DEEPCRAFT ImagiNet compiler สร้าง เปิด API AIM_<NAME>_*
  • ready-model .a — ไลบรารี eval ของ Infineon ทุกตัว export IMAI_* ไม่ prefix → ชนกัน ต้อง objcopy rename เป็น IMAI_<MODEL>_* ให้อยู่ร่วมกันได้

สังเกตว่า int8 (Motion/Cough) กับ float32 (Push/Siren) อยู่ในภาพเดียวกันได้ — คำถามคือมันรันร่วมกันได้ยังไง? หน้าถัดไปตอบ

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

TFLite-Micro — runtime จริง (DEEPCRAFT เป็นแค่ wrapper)

จุดที่คนเข้าใจผิดบ่อย: DEEPCRAFT ไม่ใช่ runtime มันเป็นแค่เปลือกที่ห่อกราฟ .tflite ไว้:

DEEPCRAFT wrapper AIM_*/IMAI_* 4 ฟังก์ชัน TFLite-Micro (runtime จริง) libtensorflow-microlite.a kernel: int8 + float32 int8 → NPU · float → CPU บอร์ดไม่เคย "เห็น" DEEPCRAFT — มันเรียกแค่ 4 ฟังก์ชัน init/enqueue/dequeue/finalize
  • prebuilt libtensorflow-microlite.a พก kernel ทั้ง int8 และ float32 มาในตัว
  • โมเดลไหนก็ตามที่ให้ 4-ฟังก์ชัน contract เดียวกัน (รวมทั้งที่เราเทรนเองแล้วแปลงเป็น .tflite) เสียบเข้าได้
  • นี่คือเหตุผลว่าทำไมบทเรียน Training (บทเรียน 5.3–5.5) เราถึง train เองแล้วเอาลงบอร์ดได้ — contract เดียวกัน

"DEEPCRAFT = wrapper, TFLite-Micro = runtime" — จำประโยคนี้ไว้ เพราะมันปลดล็อกความคิดว่า "เราเทรนโมเดลเองแล้วเสียบแทนได้"

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

int8 + float32 ในภาพเดียว รันร่วมกันยังไง

คำถามค้างจากเมื่อกี้: ทำไม radar/siren (float32) รันในภาพ int8 combo ได้?

  • COMPONENT_ML_INT8x8 / ML_FLOAT32 แค่ตั้ง typedef ตัวชี้ MTB_ML_DATA_T — ไม่ได้ตัดสินว่ารันด้วย kernel ไหน
  • เส้นทางรันจริงเป็น runtime-typed: copy input เป็น byte ดิบ แล้ว switch ตามขนาด output type
  • เพราะ TFLite-Micro พก kernel ครบสองชุด → โมเดล float32 รันบน CPU float kernel ได้ ส่วนโมเดล int8 ใช้ U55 NPU — ไม่ต้อง build แยก
โมเดล int8 (Motion/Cough/Alarm) → Ethos-U55 NPU (เร็ว ประหยัดไฟ) โมเดล float32 (Push/Siren) → CPU float kernel (ในภาพเดียวกัน)

ตอน Trace ในสคริปต์ ลองเทียบ latency() ของโมเดล int8 กับ float32 — จะเห็นเลยว่า NPU กับ CPU ต่างกันจริง

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

.tflite → NPU + กำแพง .ml_weights

โมเดล .tflite int8 ไปรันบน NPU ได้ต้องผ่านขั้นคอมไพล์ Vela ก่อน แปลงให้ Ethos-U55 อ่านออก แล้ว weights ต้องหาที่อยู่ในแฟลช:

  • ปกติ weights ลงที่ .cy_socmem_data แต่ในภาพ combo weights 6 โมเดล + FFT table จะล้นกำแพง .fw_identity ที่ 0x60900000
  • ทางแก้: CY_ML_MODEL_MEM=.ml_weights ย้าย weights ไปโหลดที่หาง flash ~2.5 MB เหนือกำแพง แล้ว copy ลง SOCMEM ตอน boot (ให้ NPU DMA เอื้อมถึง)
  • เรื่องนี้ mechanical — แตะเฉพาะตอนเพิ่มโมเดลใหญ่ (บทเรียน 7.3–7.4)

จำ address 0x60900000 ไว้ — มันคือ "กำแพง" ที่เราเคยเจอในบันทึกโปรเจกต์จริง ตอน combo image เกือบล้น นี่ไม่ใช่ทฤษฎี เป็นของจริงบนบอร์ดนี้

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

IPC model link — สะพานข้ามคอร์

MicroPython บน CM33_NS เรียก ai_engine บน CM55 ตรงๆ ไม่ได้ ทั้งคู่แลกข้อความขนาดคงที่ผ่าน IPC pipe เดียว มี สอง plane:

CM33_NS edge_ai (MicroPython) CM55 deepcraft_task → ai_engine control plane: SELECT / START / STOP (fire-and-forget) query plane: Q_COUNT / Q_MODEL / Q_RESULT / Q_ACTIVE (pull) callback บน CM55 รันใน ISR: ตอบ query ในที่ · ห้าม send จาก callback (เคยทำ pipe ค้าง)
  • control plane (OP_CTRL) — สั่ง: SELECT(n)=0x90+n, START=0x82, STOP=0x83 แบบยิงแล้วไม่รอ
  • query plane (OP_QUERY) — อ่านแบบ pull: CM33 วางตัวชี้ struct แล้ว block รอ CM55 เติม

ข้อจำกัดที่ต้องรู้: control plane เป็น best-effort ใช้ pipe ร่วม ถ้า flood select() รัวๆ pipe ค้างได้ (กู้ด้วย core reset เท่านั้น) — ใน REPL ให้เว้นการสลับโมเดลเป็นวินาที

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

confirmed by observation — ทำไม select ต้องรอ

การออกแบบที่สำคัญที่สุดของ IPC นี้: อ่านเป็น pull ที่ตอบรับ · สั่งเป็นการยืนยันด้วยการสังเกต

edge_ai.select(n)   # 1) ส่ง SELECT(0x90+n) ผ่าน control plane
                    # 2) poll Q_ACTIVE จนอ่านกลับได้ = n (สูงสุด 25 × 20 ms)
                    #    ถ้าไม่สลับในเวลา -> OSError "select not confirmed"
  • ไม่มี push ที่ไม่ถูกตอบรับ ซึ่งอาจแอบทำ pipe ค้างเงียบๆ
  • select() ไม่เชื่อว่า "สั่งแล้วสำเร็จ" แต่รอ เห็น active() เปลี่ยนเป็น n จริง
  • นี่คือเหตุผลที่เราห่อ select() ด้วย try/except OSError เสมอ

ในสคริปต์ฝึก เราจะเรียก active() ต่อจาก select() ทันที เพื่อ เห็นกับตา ว่ากลไก confirm-by-observation นี้ทำงาน — index ที่ได้ต้องตรงกับที่ขอ

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

edge_ai — API ฝั่ง "อ่าน" ที่ชุดบทเรียนนี้ใช้

ชุดบทเรียนก่อน ๆ เราเน้นฝั่ง "สั่ง" (select/stop) ชุดบทเรียนนี้เราส่องด้วยฝั่ง "อ่าน" 5 ตัว แต่ละตัว map ไป query sub-plane:

คำสั่ง คืนค่า map ไป
edge_ai.links() ('ipc',) — link backend IPC model link (deepcraft_task.c)
edge_ai.model(n) dict descriptor หนึ่งตัว Q_MODEL → ai_engine_model(i)
edge_ai.active() index ที่รันอยู่ (-1 ถ้า idle) Q_ACTIVE → s_current
edge_ai.result() dict verdict ล่าสุด หรือ None Q_RESULT → ai_result_t s_res
edge_ai.latency() เวลาอนุมานล่าสุด (ms) s_res.inference_us / 1000
  • ทั้งห้าตัวเป็น pull ทั้งหมด — ถามเมื่อไรก็ได้ ไม่บล็อกรอผลใหม่ (แค่บล็อกรอ CM55 ตอบ query)
  • on_result(cb) เป็นทางเลือกแบบ event-driven — เฟิร์มแวร์เรียก callback ตอนคลาสเปลี่ยน (เราใช้ในฉบับเต็ม)

โฟกัสห้าตัว: links → model → active → result → latency ห้าตัวนี้คือทั้งเรื่องราวของสคริปต์ส่องสแตกชุดบทเรียนนี้

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

ทั้งเส้นในภาพเดียว — sensor ถึง dict

รวมทุกอย่างเข้าด้วยกัน นี่คือเส้นทางของ หนึ่งการอนุมาน ตั้งแต่เซนเซอร์จนเป็น dict ใน Python:

BMI270 (CM33_NS) I2C read + sequence number feed_imu() (CM55) remap แกน · counts→g/dps enqueue → dequeue Ethos-U55 NPU (int8) publish() → s_res lock-free · seq++ page_edge_ai (CM55) วาด verdict + bars 30 Hz Q_RESULT pull (CM33_NS) edge_ai.result() → dict แอป MicroPython ของเรา

ทั้งหมดคือ "sample เซนเซอร์บนคอร์หนึ่ง กลายเป็น Python dict บนอีกคอร์ ผ่านผล lock-free กับ IPC pull" — ที่เหลือในสแตกเป็นแค่รายละเอียดรอบการทำให้เส้นนี้เร็ว สลับได้ ปลอดภัย

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

แผนภาพ dataflow — 6 ป้ายที่ต้องอ่านออก

สแตกนี้ไม่มีสมการให้ท่อง มันเป็น เส้นทางข้อมูลหกป้าย ที่ไหลจากซ้ายไปขวาหนึ่งรอบต่อหนึ่งการอนุมาน อ่านป้ายให้ออก แล้วคุณ trace ได้ทั้งเส้น:

หนึ่งการอนุมาน = ไหลซ้าย → ขวา ครบหนึ่งรอบ sensor IMU/MIC/RADAR IPC ข้ามคอร์ feed_*() แปลง signal enqueue → dequeue คิวเข้า/ออกกราฟ NPU Ethos-U55 publish() s_res · seq++

อ่านแต่ละป้ายเป็นภาษาคน แล้วโยงว่าทำไมมันสำคัญกับชุดบทเรียนนี้:

ป้าย อ่านว่าอะไร ทำไมสำคัญกับชุดบทเรียนนี้
sensor เซนเซอร์ดิบบน CM33_NS (IMU/MIC/RADAR) ต้นทางของทุกค่า — sample แรกก่อนข้ามคอร์
IPC สะพานข้ามคอร์ CM33_NS ↔ CM55 ชั้น transport ที่ links() ชี้ให้เห็น
feed_*() แปลง raw → feature ที่กราฟกิน ตอบคำถาม "โมเดลเห็นอะไรจริง ๆ"
enqueue → dequeue ป้อนเข้าคิว แล้วดึงออกให้ NPU จุดที่ "สลับโมเดล = เปลี่ยนว่าใครได้ feed"
NPU Ethos-U55 อนุมาน int8 (float ไป CPU) ที่มาของ latency() int8 vs float32
publish() เขียนผลลง s_res แบบ lock-free + seq++ ปลายทางของ result() ทุก key

ไม่มีสูตรให้จำ มีแค่หกป้าย — trace เก่งคือ "เห็นค่าปุ๊บ บอกได้ทันทีว่ามันอยู่ป้ายไหนของเส้นนี้"

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