ทั้งไฟล์อ่านเป็นประโยคเดียว: "เปิดไมค์ → กดปุ่ม label → วนเก็บ BURST แถว (แต่ละแถวมัด IMU+MIC บน t_ms) → ออกแล้วคืนไมค์"
ตัวเลขบรรทัด (
:78,:83,:88,:92) ชี้ไปที่สี่# เติม:ในไฟล์ฝึก จำโครงนี้ไว้ เดี๋ยวไล่ดูทีละส่วน
ส่วนบนของไฟล์ (ให้ไว้แล้ว) เปิดไมโครโฟนหนึ่งครั้ง เตรียม buffer และเขียนหัวตาราง CSV ถ้ายังไม่มีไฟล์:
pdm = PDM_PCM(0, sck="P8_5", data="P8_6", sample_rate=16000)
buf = array.array("h", (0 for _ in range(CHUNK)))
try:
open(PATH, "r").close() # มีไฟล์แล้วไหม
except OSError:
with open(PATH, "w") as f:
f.write("t_ms,label,ax,ay,az,gx,gy,gz,db\n") # ยังไม่มี -> เขียนหัวตาราง
t_ms,label,... คือ schema ของ dataset — เขียนครั้งเดียวตอนไฟล์ยังว่าง
try: open(...,"r")เป็นวิธีเช็ก "ไฟล์มีอยู่ไหม" แบบ MicroPython ถ้าไม่มีจะโยนOSErrorแล้วเราค่อยเขียนหัวตารางใหม่ — เก็บข้อมูลต่อท้ายได้โดยไม่ทับของเก่า
ช่องเติมที่ 1: ก้าวแรกในลูป จับเวลาของแถวนี้เทียบจุดเริ่ม t0:
t0 = time.ticks_ms() # (ให้ไว้แล้ว) จุดศูนย์ของชุดนี้
with open(PATH, "a") as f:
for _ in range(BURST):
# เติม 1: t_ms = time.ticks_diff(time.ticks_ms(), t0)
t_ms = 0
pass
t_ms = 0 / pass ด้วย t_ms = time.ticks_diff(time.ticks_ms(), t0)t_ms = 0 เหมือนกันหมด เส้นเวลาหาย จับคู่ตามเวลาไม่ได้
t0ถูกตั้งใหม่ทุกครั้งที่กดปุ่ม label ดังนั้นแต่ละชุด (burst) เริ่มนับเวลาที่ ~0 ของตัวเอง — สะดวกเวลาเอาไปตัดหน้าต่างในโมดูล 4 (Analysis)
ช่องเติมที่ 2 และ 3: อ่านสองเซนเซอร์ติดกันในรอบเดียว นี่คือจุดที่ทำให้ทั้งคู่ "เวลาเดียวกัน":
# เติม 2: อ่านเซนเซอร์ตัวที่หนึ่ง (IMU 6 แกน)
ax = ay = az = gx = gy = gz = 0.0
pass # -> ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
# เติม 3: อ่านเซนเซอร์ตัวที่สอง (เสียงหนึ่งเฟรม)
pass # -> pdm.readinto(buf)
db = dbfs(buf) # (ให้ไว้แล้ว) ย่อเฟรมเป็นระดับ dBFS
pass ด้วย ax, ay, az, gx, gy, gz = sensors.bmi270.motion() (ลบบรรทัด = 0.0 ทิ้งได้)pass ด้วย pdm.readinto(buf) — เติม buffer ด้วยเสียงชุดล่าสุด แล้ว dbfs() ย่อเป็นค่าเดียวลำดับสำคัญ: อ่านทั้งคู่ ก่อน เขียนแถว ถ้าเผลออ่าน MIC ในรอบนี้แต่เขียน IMU ของรอบก่อน ข้อมูลจะเหลื่อมเวลากันทันที
ช่องเติมที่ 4: รวมทุกอย่างเป็นหนึ่งบรรทัด CSV ที่ทั้ง IMU และ MIC อ้าง t_ms เดียวกัน:
# เติม 4: เขียนหนึ่งแถว
pass
# -> f.write("%d,%s,%.4f,%.4f,%.4f,%.4f,%.4f,%.4f,%.1f\n"
# % (t_ms, label, ax, ay, az, gx, gy, gz, db))
level.value(max(0, int(db + 60))) # (ให้ไว้แล้ว) โชว์ระดับเสียงสดๆ
time.sleep_ms(RATE_MS) # (ให้ไว้แล้ว) คุมจังหวะ ~50 Hz
pass ด้วย f.write(...) ตามรูปแบบในคำใบ้ — ลำดับคอลัมน์ต้องตรง schema หัวตาราง%d สำหรับ t_ms (จำนวนเต็ม ms), %.4f สำหรับค่า IMU, %.1f สำหรับ db
sleep_ms(RATE_MS)คือสิ่งที่ทำให้แถวห่างกัน ~20 ms อย่าเอาออก ไม่งั้นลูปจะรัวจนอัตราสูงเกินและ jitter พุ่ง
ปุ่ม Stop ไม่มีในตัวนี้ (เก็บทีละชุดจบในตัว) แต่ต้อง คืนฮาร์ดแวร์ตอนออก เสมอ:
finally:
pdm.deinit() # คืนไมโครโฟนให้ระบบเสมอ
lcd.console("<span class=ok> จบ: %d แถวใน %s</span>" % (total, PATH))
pdm.deinit() อยู่ใน finally — ไม่ว่าจะออกด้วยปุ่ม back หรือ error ไมโครโฟนจะถูกปิดเสมอนิสัย embedded เดิมจากบทเรียน 1.1–1.3: ออกจากงานยังไง ทิ้งเครื่องไว้ให้เรียบร้อยแบบนั้น — ชุดบทเรียนนี้ "เครื่อง" คือไมโครโฟน PDM
เปิด s05_multicapture.py ในไฟล์มี # เติม: วางไว้ 4 จุด ตรงสี่ก้าวของลูปเก็บข้อมูล:
| # | ก้าว | เติมด้วย | ถ้าลืม |
|---|---|---|---|
| 1 | ประทับเวลา | t_ms = time.ticks_diff(time.ticks_ms(), t0) |
ทุกแถว t_ms = 0 |
| 2 | อ่าน IMU | ax, ay, az, gx, gy, gz = sensors.bmi270.motion() |
คอลัมน์ IMU เป็น 0 |
| 3 | อ่าน MIC | pdm.readinto(buf) |
คอลัมน์ db นิ่ง ไม่ตามเสียง |
| 4 | เขียนแถว | f.write("%d,%s,%.4f,...,%.1f\n" % (...)) |
ไฟล์ว่าง แถวไม่ถูกเขียน |
ขั้นตอน:
# เติม: ทีละจุด แล้วแทน pass ด้วยคำสั่งตามคำใบ้สี่ช่องนี้คือสี่ก้าวของ DAQ หลายเซนเซอร์เป๊ะ — เติมครบเมื่อไร คุณได้ dataset ที่ทุกแถวมัดเสียงกับการเคลื่อนไหวบนเวลาเดียว
ต่างจากชุดบทเรียนก่อน ๆ เสียงจริงต้องมาจาก บอร์ด BENTO จริง (Emulator มี PDM_PCM จำลองที่ส่งเสียงสังเคราะห์ ให้ลองโครงโปรแกรมได้ แต่ไม่ใช่เสียงจริง):
s05_multicapture.py ใน BENTO IDE กด Program to Device/multicapture.csvส่วน IMU กับการเขียน CSV คุณซ้อมแนวคิดได้บน Emulator (จากบทเรียน 2.1–2.2) แต่การอ่านเสียงจริงต้องมาที่บอร์ด — และโปรแกรมเสียงที่รันผ่านบน Emulator ยังไม่ใช่หลักฐานว่าจะรันผ่านบนบอร์ด
ภาพด้านล่างเป็น ภาพวาดจำลองหน้าจอ ให้เห็นล่วงหน้าว่า เมื่อ Program to Device สำเร็จ จอจะมีปุ่ม label สามปุ่ม แถบระดับเสียงสด และตัวนับแถว
t0 ใหม่แล้วเริ่มเก็บ BURST แถวของชุดนั้นdb แบบ realtime ตามบรรทัด level.value(max(0, int(db + 60)))/multicapture.csvภาพนี้วาดมือเพื่อให้นึกหน้าจอออกก่อนถึงบอร์ดจริง ตัวเลข (-38 dBFS, 128 แถว) เป็นตัวอย่าง ของจริงจะวิ่งตามเสียงและท่าที่คุณทำ
Emulator จำลอง IMU / env / dsp / ui ได้ครบ ส่วน ไมโครโฟน PDM จำลองด้วยเสียงสังเคราะห์ ไม่ใช่เสียงจริง และบน TESAIoT Dev Kit (ที่มี audio codec) การเปิด PDM ยังชนกับ clock ของระบบเสียง CM55
ความซื่อตรงเรื่องนี้สำคัญ: เราบอกตรงๆ ว่าอะไรรันที่ไหนได้ ไม่หลอกว่า Emulator ทำได้ทุกอย่าง — คุณจะได้วางแผนบทเรียนถูก
เก็บเสร็จแล้วอย่าเพิ่งเชื่อ ตรวจก่อน ดึงไฟล์ออกด้วย BENTO IDE (file transfer) หรือ mpremote แล้วเปิดดู:
# ตรวจเร็วๆ ใน REPL บนบอร์ด: นับแถว + ดูหัว
n = 0
with open("/multicapture.csv") as f:
head = f.readline() # t_ms,label,ax,...,db
for line in f:
n += 1
print("rows:", n, "| header:", head.strip())
BURST × จำนวนครั้งที่กด, (2) t_ms เพิ่มขึ้นเรื่อยๆ ต่อชุด, (3) db เปลี่ยนตามเสียงจริงdb นิ่งเป็น -96 ตลอด แปลว่าช่อง 3 (readinto) ยังไม่ได้เติม หรือไมค์ไม่ได้เสียงt_ms เป็น 0 หมด แปลว่าช่อง 1 ยังไม่ได้เติมการ "เปิดไฟล์ดูจริง" คือทักษะ DAQ ที่ประเมินค่าไม่ได้ — dataset ที่ดูดีบนจอแต่ไฟล์เพี้ยน จะทำให้โมเดลที่ train ออกมาพังเงียบๆ ตรวจตั้งแต่ต้นทางเสมอ
อยากเข้าใจ PDM / PCM และการเก็บ dataset ให้ลึกขึ้น ลองตามลิงก์เหล่านี้ได้ตามสะดวก (เปิดในเบราว์เซอร์):
วิดีโอ (ภาษาอังกฤษ อธิบายเห็นภาพ)
ภาพ / บทความอ้างอิง (เปิดเสรี)
วิดีโอ/ภาพภายนอกเป็นของเจ้าของต้นฉบับ ใช้เพื่อการศึกษา อ้างอิงลิงก์ต้นทาง เราลิงก์ไปหา ไม่ได้ฝังหรือทำซ้ำในสไลด์
MVP ของบทเรียน 2.3–2.4 (เกณฑ์ผ่านของชุดบทเรียน): คุณเก็บ dataset ที่ log ≥2 เซนเซอร์บนเส้นเวลาเดียว ได้จริง — ไฟล์ /multicapture.csv มีคอลัมน์ t_ms + IMU + db และค่าจริงเปลี่ยนตามท่า/เสียง
t_ms ตรงไหน อ่านสองเซนเซอร์ตรงไหน และทำไมต้องอยู่ในรอบเดียว"เก็บได้" ไม่พอ ต้อง "เก็บตรงเวลา" — คุณต้องชี้ได้ว่าแถวไหนคือช่วงที่คุณเขย่า+ส่งเสียง และ
dbกับ IMU ในแถวนั้นสอดคล้องกัน
ถ้าติด ให้ไต่บันไดนี้ทีละขั้น อย่าเพิ่งกระโดดไปดูเฉลย เพราะของจะเข้าหัวตอนที่คุณพยายามเองก่อน:
# เติม: ทั้ง 4 จุดในไฟล์ฝึก + ตารางสี่ก้าวหน้าที่แล้ว บอกว่าแต่ละช่องเติมอะไรs05_multicapture.py มีโครงครบทั้งไฟล์แล้ว (UI + เปิด PDM + finally) เหลือแค่ 4 บรรทัดในลูปให้เติมs05_multicapture.py เติมครบพร้อมคอมเมนต์อธิบายทุกก้าว (อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์เอง)s05_multicapture_full.py เพิ่มเก็บ WAV เสียงดิบคู่ CSV + วัด dt/jitter จริง + สรุป peak dBFS ต่อ label ลง manifestลองเขียนเองให้สุดก่อนนะ ถ้าติดจริงๆ ค่อยเปิดเฉลยดูทีละช่อง แล้วกลับมาพิมพ์เอง — เดี๋ยวเราค่อย ๆ แกะไปด้วยกัน
การเก็บ dataset หลายเซนเซอร์ซ่อนแนวคิด DAQ หลายชั้นที่จะใช้ไปตลอดคอร์ส:
ฝั่ง DAQ / dataset
t_ms เดียว จึงจับคู่/ตัดหน้าต่างตามเวลาได้sleep_ms แล้วตรวจด้วย t_ms จริงฝั่ง MicroPython / โครงโปรแกรม
PDM_PCM(...) นอกลูป เหมือนสร้าง widget ครั้งเดียวticks_ms / ticks_diff — คู่มาตรฐานวัดเวลาสั้นๆ กัน wrap-aroundfinally: pdm.deinit() คืนไมโครโฟนสู่สถานะที่รู้แน่ทั้งหมดต่อยอดจากบทเรียน 2.1–2.2 ตรงๆ — DAQ ไม่ใช่เรื่องเขียนใหม่ทุกครั้ง แต่คือ pattern ที่ขยายทีละเซนเซอร์
การมัดหลายเซนเซอร์บนเวลาเดียว ไม่ใช่แบบฝึกหัด มันคือวิธีที่ dataset ระดับสินค้าถูกเก็บจริง:
ไฟล์
/multicapture.csvที่คุณเพิ่งเก็บ ใช้หลักการเดียวกับ dataset ของสินค้าจริงเหล่านี้เป๊ะ — เราแค่ทำในสเกลเล็ก เพื่อจะย้อนไป train โมเดลเองในโมดูล 5 (Training)
งานทำเอง (ท้ายบทเรียน):
s05_multicapture.py ให้ครบทั้ง 4 ช่อง รันบนบอร์ดจนได้ /multicapture.csvdb และ IMU เปลี่ยนตามจริงt_ms ในแต่ละชุดเพิ่มขึ้นเรื่อยๆ และห่างกันใกล้ 20 ms ไหม — จดว่าเจอ jitter ตรงไหนใบ้ข้อ 3 — ถ้าช่วงเวลาเพี้ยนมาก ลองลด CHUNK เสียง หรือลดงานในลูป แล้วเทียบ t_ms ใหม่
วันนี้เราได้: อ่านไมโครโฟน PDM เป็น dBFS · เข้าใจเส้นเวลาร่วม · อ่าน IMU+MIC ในลูปเดียว · เก็บ dataset หลายเซนเซอร์ที่ทุกแถวตรงเวลา (t_ms + IMU + db)
ชุดบทเรียนถัดไป (บทเรียน 3.1–3.2) เราเข้าสู่ Pillar 2 (Processing) — เอาสัญญาณดิบที่เก็บมา แปลงเป็นปริมาณเชิงฟิสิกส์ (tilt / altitude / พลังงาน) แล้วโชว์เป็นเกจสดบนจอ เจอกันครับ