| คำถาม | คำตอบของคาบนี้ | อยู่ช่วงไหน | |
|---|---|---|---|
| Why | ในเมื่อบอร์ดมี IMU อยู่แล้ว ทำไมยังต้องมานั่งคำนวณมุมเอง | เพราะ ไม่มีชิปตัวไหนวัด "องศา" มาให้ มันวัดความเร่งกับความเร็วเชิงมุม ส่วนมุมเป็นปริมาณที่เราคิดขึ้นมาจากตัวเลขดิบ · และคำนวณผิดลำดับหรือผิดหน่วย โปรแกรมไม่พัง มันแค่ ตอบผิดอย่างเงียบ ๆ ซึ่งเป็นบั๊กที่แพงที่สุด | ครึ่งแรก · จากความเร่งสู่องศา · สไลด์ลำดับค่าที่คืนกลับมา |
| What | มีอะไรให้ใช้บ้าง | ตระกูล IMU ทั้งสิบสี่ชื่อ — sensors.bmi270 5 · sensors.bmm350 5 · ฝั่ง IMU ของ dsp อีก 4 คือ tilt() compass() Madgwick Pedometer |
สไลด์บัญชี 14 ชื่อ |
| How | ประกอบยังไงให้ใช้งานได้จริง | อ่านหกแกนจากการอ่านครั้งเดียวด้วย motion() → dsp.tilt() → กรองด้วย dsp.EMA → หักค่าอ้างอิงที่ปุ่ม "ตั้งศูนย์" เก็บไว้ → ขึ้นแถบสองแกนบนไม้บรรทัด + Seg7 |
ห้าไฟล์ตัวอย่าง + ใบฝึก |
ปลายทางที่จับต้องได้ — เครื่องวัดระดับสองแกน วางราบอ่านได้ราว 0° เอียงแล้วแถบทั้งสองแกนวิ่งถูกทิศ ไฟบอกได้ว่าเกินเกณฑ์ที่ตั้งไว้หรือยัง และกดปุ่มตั้งศูนย์เมื่อไรก็ประกาศจุดอ้างอิงใหม่ได้ทันที
คาบ 5 คือค่าดิบหนึ่งตัว แปลงเป็นหน่วยที่คนอ่านออก · คาบนี้คือค่าดิบสามตัว แปลงเป็นปริมาณที่ไม่มีเซนเซอร์ตัวไหนวัดตรง ๆ
sensors.bmi270.motion() และบอกได้ว่าทำไมต้องอ่านทีเดียวdsp.tilt() โดยจำลำดับค่าที่มันคืนได้ถูกเกินเกณฑ์ผ่าน แต่อยู่ในคาบนี้ เพราะเป็นของกลุ่มเดียวกันและถ้าไม่พูดที่นี่ก็ไม่มีที่ไหนพูด: sensors.bmm350 (เข็มทิศ ห้าคำสั่ง) · dsp.compass() · dsp.Madgwick · dsp.Pedometer — สี่หัวข้อนี้มีสไลด์ของตัวเองและมีไฟล์ตัวอย่างให้ลอง ไม่ได้อยู่ในเกณฑ์ผ่าน M6
กฎเหล็ก 5 ข้อของ ui ยังบังคับใช้เหมือนเดิม — ui.poll() ทุกลูป, time.sleep_ms(200) สำหรับงานหนัก, ไม่เกิน 64 widgets, ใช้ ui.* ครั้งแรกแล้ว sensor auto-task หยุด จึงต้องอ่านเซนเซอร์เองในลูป, ลูปเร็วเกินเฟรมจะหายเงียบ ๆ

หน้าจอของเฉลยปัจจุบันแบ่งเป็นหกการ์ด เรียงจากซ้ายบนลงล่าง
dsp.tilt() คืนค่ามาพอดี แต่ละแกนเป็น ui.Bar วางทับ ui.Scale ที่มีขีดและตัวเลข -90 ถึง 90 พร้อม ui.Seg7 อ่านค่าละเอียดอยู่ข้าง ๆui.Spinbox ที่ตั้งเองได้ด้วยปุ่มเพิ่ม/ลด เพราะงานวางกล้องกับงานวางตู้เย็นยอมได้ไม่เท่ากันui.Led สองดวง บอกว่าตอนนี้อยู่ในเกณฑ์หรือเกินค่าที่อ่านได้ต้องเทียบกับอะไรสักอย่างเสมอ ปุ่มตั้งศูนย์คือการประกาศว่า "ตรงนี้คือศูนย์" และไม้บรรทัดใต้แถบคือการประกาศว่า "เต็มสเกลคือเท่านี้"
คำตอบยอดนิยมคือ "วัดความเร่ง" ซึ่งถูกครึ่งเดียว วางบอร์ดนิ่งสนิทบนโต๊ะ มันไม่ได้เร่งไปไหนเลย แต่ค่าที่อ่านได้คือ
อ่านเป็นภาษาคน: เซนเซอร์ไม่ได้วัดว่าเราเคลื่อนที่เร็วขึ้นแค่ไหน มันวัด แรงที่ตัวเรือนดันมวลเล็ก ๆ ข้างในไว้ไม่ให้ตกอิสระ — โต๊ะดันบอร์ดขึ้น บอร์ดดันมวลขึ้น จึงอ่านได้ +1 g ในทิศขึ้น ปริมาณนี้มีชื่อว่า proper acceleration
ตัวเลขจากบอร์ดจริง: ปล่อยบอร์ดตกอิสระ (อย่าทำ) ทั้งสามแกนจะอ่านได้ ~0 เพราะไม่มีใครดันมวลนั้นแล้ว

แรงโน้มถ่วงอยู่ในค่าที่อ่านได้ ตลอดเวลา — คนอื่นเรียกมันว่าสัญญาณรบกวน วันนี้เราจะเรียกมันว่าสัญญาณ



โครงสร้างข้างใน BMI270 คือ มวลพิสูจน์ (proof mass) เล็กกว่าเม็ดฝุ่น แขวนบนสปริงซิลิคอนคดเคี้ยว พอบอร์ดถูกเร่ง มวลตามไม่ทันเพราะความเฉื่อย มันจึงเลื่อนไปในทางตรงข้าม
แล้ววงจรอ่าน "ระยะที่มวลเลื่อน" ได้ยังไง — มวลมีซี่หวียื่นออกมา สอดสลับกับซี่หวีที่ยึดกับตัวเรือน แต่ละคู่ซี่คือ ตัวเก็บประจุ หนึ่งตัว
วงจรวัด ผลต่างของความจุ ส่งเข้า ADC ในชิป ออกมาเป็นตัวเลขบน I2C

การวัดความเร่งจริง ๆ แล้วคือ การวัดอัตราส่วนความจุ — นี่คือเหตุผลที่มันเล็กและกินไฟน้อยได้ขนาดนี้
เพราะขนาดรวมคงที่ เรารู้ค่าสองแกนก็เดามุมได้ นี่คือเหตุผลที่ สามตัวเลขกลายเป็นสององศา ได้


| ชื่อ | คือการเอียงแบบไหน | ทดสอบด้วยการ | มาจากแกน |
|---|---|---|---|
| roll | ซ้าย-ขวา | ยกขอบด้านที่ทำให้ roll เพิ่ม — ดูว่าขอบไหนของบอร์ดทีม (Eva Kit: ขอบซ้าย · Dev Kit: ยังไม่ได้วัด จดลงใบงาน) | ay กับ az |
| pitch | หน้า-หลัง | ยกขอบด้านที่ทำให้ pitch เพิ่ม — ขอบที่ตั้งฉากกับขอบของ roll (Eva Kit: ขอบบน) | ax เทียบ y-z |
| yaw | หันซ้าย-ขวา | หมุนบอร์ดบนโต๊ะ | หาจาก accel ไม่ได้ |
ทำไม yaw หาไม่ได้จาก accel — การหันบอร์ดบนโต๊ะไม่ทำให้เงาของแรงโน้มถ่วงบนแกนใดเปลี่ยนเลย accel จึงมองไม่เห็นมัน ต้องใช้ magnetometer (คาบ 8) หรือ gyro
dsp.tilt()ให้เราสองแกน ไม่ใช่สาม — และนั่นเป็นข้อจำกัดทางฟิสิกส์ ไม่ใช่ข้อจำกัดของเฟิร์มแวร์
dsp.tilt()ตรวจสุขภาพเซนเซอร์ก่อนเชื่อค่าอะไรทั้งนั้น
ไทย: ไม่ว่าเอียงบอร์ดยังไง ถ้าวางนิ่งค่านี้ต้องได้ราว 9.81 m/s² เสมอ · ตัวเลขจาก Eva Kit (บอร์ดที่ใช้เขียนสไลด์): ax 0.02, ay −0.05, az 9.79 → ผ่าน
สูตรที่แปลงเงาเป็นองศา
ไทย: เวกเตอร์แรงโน้มถ่วงชี้ลงคงที่เสมอ พอบอร์ดเอียง เงาของมันบนแกนก็เปลี่ยน · ตัวเลขจาก Eva Kit ตอนยกขอบซ้าย: ay 4.90, az 8.49 →
ทำไมต้อง atan2 ไม่ใช่ atan — atan2 รับตัวตั้งกับตัวหารแยกกัน จึงแยกควอดรันต์ได้ครบสี่ และไม่ระเบิดเมื่อตัวหารเป็นศูนย์ตอนบอร์ดตั้งฉาก 90°
_BUILD/MEDIA_PLAN.md §7.3) · TESAIoT Dev Kit: ยังไม่มีใครวัดว่าขอบไหนของฐาน QWA309 ทำให้ roll เพิ่ม IMU ตัวเดียวกันแต่ประกอบบนบอร์ดคนละแบบ ห้ามยกคำว่า "ขอบซ้าย" ของ Eva ไปบอกทีม Dev Kit — ให้ทีมยกทีละขอบแล้วจดลงใบงาน 4.1 ว่าขอบไหนขยับ roll

รู้สูตรไว้ไม่ใช่เพื่อพิมพ์เอง แต่เพื่อ เดาถูกว่าจะพังตรงไหน เมื่อค่าออกมาแปลก
dsp.tilt() คืน (roll, pitch) — roll มาก่อน pitch เสมอ ตำราและไลบรารีจำนวนมากเขียนเรียงว่า "pitch, roll" จนติดปาก
ถ้าเผลอเขียนสลับ โปรแกรมจะ ไม่ error เลย จอขึ้นตัวเลขสวยงามครบทั้งสองแกน แต่เอียงซ้าย-ขวาแล้วแถบ PITCH ดันวิ่ง ส่วนแถบ ROLL นิ่ง — เหมือนภาพขวา
บั๊กที่ไม่ทำให้โปรแกรมพัง คือบั๊กที่แพงที่สุด เพราะไม่มีใครรู้ว่ามันอยู่ตรงนั้น
motion() — หกแกนจากการอ่านครั้งเดียว และหน่วยที่ต้องอ่านให้ถูกax, ay, az, gx, gy, gz = sensors.bmi270.motion()
หกค่านี้มาจาก การอ่านครั้งเดียวกัน — กลไกต่างกันตามบอร์ด: บน Eva Kit motion() หยิบจาก snapshot ชุดเดียวที่คอร์จออ่านค้างไว้ (ไม่มีการจองบัสฝั่ง Python) · บน Dev Kit CM33 จองบัส I2C ของตัวเอง (lock) อ่านหกแกนแล้วปล่อย · ถ้าเรียก acceleration() แล้ว gyroscope() แยกสองครั้ง ระหว่างสองครั้งนั้น snapshot อาจถูกเปลี่ยนชุด (Eva) หรือบัสถูกปล่อยว่าง (Dev Kit) และถ้าตอนนั้นบอร์ดกำลังขยับ ค่า accel กับ gyro ที่ได้จะมาจาก คนละท่าของบอร์ด
| ค่า | หน่วย | จุดอ้างอิงที่ควรจำ |
|---|---|---|
ax, ay, az |
m/s² | 1 g = 9.81 · วางราบ az ≈ +9.81 · ตกอิสระ = 0 |
gx, gy, gz |
deg/s | วางนิ่ง ≈ 0 · หมุนด้วยมือ 30-90 · สะบัดข้อมือ > 500 |
อย่าสับสน — accel บอก ตำแหน่งเชิงมุมเทียบกับแนวดิ่ง ส่วน gyro บอก อัตราการเปลี่ยนมุม ไม่ใช่ตัวมุมเอง
ข้อมูลที่จะเอาไปรวมกัน (fuse) ต้องมาจากเวลาเดียวกัน ไม่งั้นเราไม่ได้รวมข้อมูล เรากำลังรวมความมั่ว


โครงสร้างในชิปถูกขับให้ สั่นในแนวหนึ่ง ตลอดเวลา ถ้าชิปไม่หมุน มันก็สั่นอยู่แนวเดิม
พอชิปเริ่มหมุน แรง Coriolis ผลักมวลที่กำลังสั่นให้เบนออกไปใน แนวตั้งฉาก — วงจรวัดการเบนนั้นแล้วแปลงเป็น deg/s
ภาพซ้ายคือ Coriolis ในกรอบหมุน (มุมมองนิ่ง เทียบกับ มุมมองหมุน) ภาพกลางคือโครงสร้าง tuning fork ที่ใช้จริง
gyro ไม่ได้ "รู้ว่าอยู่ที่กี่องศา" มันรู้แค่ว่า ตอนนี้กำลังหมุนเร็วแค่ไหน ที่เหลือเราต้องบวกเอาเอง
accel — ถูกในระยะยาว แต่กระตุกในระยะสั้น
อ้างอิงแรงโน้มถ่วงซึ่งไม่มีวันเปลี่ยน วางทิ้งสามชั่วโมงค่าก็ยังถูก แต่มันวัด "แรงทั้งหมด" ไม่ได้แยกว่าอันไหนคือแรงโน้มถ่วง อันไหนคือมือที่เขย่า พัดลม เคาะโต๊ะ หรือ noise ทางไฟฟ้า
gyro — นิ่งและไวในระยะสั้น แต่หนีในระยะยาว
วัดอัตราการหมุนโดยตรง แรงสั่นภายนอกรบกวนได้น้อย ตอบสนองทันที แต่การจะได้ มุม ต้องเอาอัตรามาบวกสะสม
ตัวเลขจาก Eva Kit (บอร์ดที่ใช้เขียนสไลด์): วางนิ่งแล้ว gz ไม่เคยเป็น 0.00 เป๊ะ มันเป็น 0.03-0.05 deg/s สมมติ bias = 0.05 deg/s
ไทย: ความผิดพลาดจิ๋วที่ถูกบวกสะสมทุกรอบ ไม่มีอะไรดึงกลับ ปรากฏการณ์นี้ชื่อ drift

จุดอ่อนของสองตัวนี้อยู่คนละย่านความถี่กันพอดี — และนั่นคือช่องว่างที่วิศวกรใช้ประโยชน์
ไทย: เชื่อ gyro ในช่วงสั้น ๆ เพราะมันนิ่งไม่สั่น แล้วให้ accel คอยดึงกลับในระยะยาวเพราะ gyro ไหลไปเรื่อย ๆ
สองครึ่งนี้บวกกันได้พอดีหนึ่งเสมอ จึงเรียกว่า complementary
ความถี่ตัดของการผสม
ไทย: τ คือเส้นแบ่ง — เร็วกว่านี้ฟังจาก gyro ช้ากว่านี้ฟังจาก accel
ตัวเลขของลูปเรา: s s


นี่คือ sensor fusion ฉบับย่อที่สุด: เซนเซอร์สองตัวที่ต่างก็ไม่สมบูรณ์ รวมกันแล้วดีกว่าตัวใดตัวหนึ่ง
บน MicroPython คาบนี้เราใช้ dsp.tilt() ซึ่งคำนวณจาก accel อย่างเดียว แล้วต่อท้ายด้วย dsp.EMA เพื่อไล่การกระตุกออก พูดให้ตรงคือเราทำแค่ ครึ่ง low-pass ไม่ได้ทำครึ่ง gyro
ไทย: ค่าใหม่มีน้ำหนัก α ที่เหลือคือความจำของค่าเก่า · ตัวเลขของลูปเรา: s, s
| สมการ | alpha สูง แปลว่า | |
|---|---|---|
dsp.EMA(alpha=) |
เชื่อ ค่าใหม่ มาก → ไวขึ้น สั่นขึ้น | |
| complementary | เชื่อ ค่าเดิม + gyro มาก → นิ่งขึ้น |
ได้อะไร เส้นนิ่ง ไม่มี drift สะสมเลยแม้เปิดทิ้งทั้งวัน เพราะไม่ได้อินทิเกรตอะไร · เสียอะไร มี lag ราวหนึ่งวินาที
ถ้าอยากได้ fusion เต็มรูป เฟิร์มแวร์มี dsp.Madgwick ให้ต่อยอด — สไลด์ถัดไปว่าด้วยเรื่องนั้นโดยตรง
อย่าเชื่อชื่อพารามิเตอร์ ให้เปิดดูสมการ —
alpha=0.98ในสองสูตรนี้ให้ผลตรงข้ามกันสุดขั้ว
dsp.Madgwick และ dsp.Pedometer — สองคลาสที่เหลือของตระกูล IMUคาบ 5 ตัวกรองหกตัวรับค่าทีละตัวคืนทีละตัว สองคลาสนี้รับ หลายแกนพร้อมกัน และคืน หลายค่า — Madgwick ให้สิ่งที่ tilt() ไม่ให้คือ yaw และความนิ่งตอนบอร์ดขยับ · Pedometer นับก้าวเสร็จในตัว · ค่าตั้งต้น beta=0.1 fs=100.0 / threshold=1.5 min_interval=300 · ทั้งคู่ ไม่มี .value()
ahrs = dsp.Madgwick(beta=0.15, fs=10.0) # beta และ fs เป็น keyword-only
roll, pitch, yaw = ahrs.update(ax, ay, az, rx, ry, rz) # rx = math.radians(gx) ... คืนสามมุม (องศา)
w, x, y, z = ahrs.quaternion() # สถานะจริงที่มันเก็บไว้
ahrs.reset() # กลับไปท่าอ้างอิง (1,0,0,0)
ped = dsp.Pedometer(threshold=1.25, min_interval=300)
steps, active = ped.update(ax / G, ay / G, az / G) # หน่วย g · คืน (จำนวนก้าว, เพิ่งนับรอบนี้ไหม)
ped.reset()
กับดักที่หนึ่ง — หน่วยของ gyro update() ต้องการ gyro หน่วย เรเดียนต่อวินาที แต่ motion() คืน องศาต่อวินาที ลืมแปลง = ป้อนใหญ่เกินจริงราว 57 เท่า มุมหมุนติ้ว และ ไม่มี error ให้เห็น — ต้องเขียน math.radians(gx) เอง
กับดักที่สอง — หน่วยของ threshold ค่าตั้งต้น 1.5 คือหน่วย g ถ้าป้อน m/s² (วางนิ่งก็ 9.81 แล้ว) มันจะข้ามเกณฑ์ค้างตั้งแต่รอบแรกและนับก้าวเรื่อย ๆ ทั้งที่บอร์ดไม่ขยับ · เกณฑ์ปล่อยตรึงไว้ที่ 80% ของ threshold ตั้งเองไม่ได้ · fs ต้องเท่าคาบลูปจริง — ลูป 200 ms คือ fs=5.0 ไม่ใช่ค่าตั้งต้น 100

ลงมือ:
examples/s06/05_madgwick_and_pedometer.py↗ — ตัวนับสองตัว ตัวที่ป้อน m/s² นับขึ้นเรื่อย ๆ ทั้งที่บอร์ดวางนิ่ง
sensors.bmm350 ห้าคำสั่ง และตัวเลขหนึ่งตัวที่ยังไม่มีข้อสรุปทั้งสองบอร์ดมีแมกนีโตมิเตอร์ BMM350 (ชิปตัวเดียวกัน) · บน Eva มันเป็นเซนเซอร์ ตัวเดียวในคาบนี้ที่ Python คุยกับชิปตรง ๆ ได้ เพราะอยู่บนบัส I3C ขา P3[0]/P3[1] ไม่ใช่ SCB0 ที่คอร์จอถือไว้ — จึงไม่ถูกปฏิเสธเหมือน sensors.init() · บน Dev Kit ทั้ง BMM350 และ BMI270 อ่านตรงจาก CM33 ห้าคำสั่งข้างล่างจึงใช้ได้เหมือนกันทั้งสองบอร์ด
| คำสั่ง | คืนอะไร |
|---|---|
bmm350.magnetic() |
(mx, my, mz) ป้ายหน่วยในซอร์สเขียนว่า µT |
bmm350.heading() |
ทิศ 0-360 องศา · 0 = เหนือ · หักค่า offset แล้วเฉลี่ยแบบวงกลมย้อนหลัง 10 ค่า |
bmm350.chip_id() |
เลขประจำรุ่นของชิป ใช้เช็กว่ามันยังมีชีวิต |
bmm350.cal_reset() |
ล้างค่าสอบเทียบ แล้วต้องหมุนบอร์ดครบรอบใหม่ |
bmm350.cal_status() |
{'valid': bool, 'offset_x': float, 'offset_y': float} |
dsp.compass() ไม่ใช่ตัวเดียวกับ bmm350.heading() — heading() ใช้ atan2(x, y) ส่วน compass() ใช้ atan2(y, x) จึงคืนคนละมุมจากสนามเดียวกัน ไม่ใช่ตัวใดพัง · dsp.compass(mx, my, mz) รับ mz แล้วทิ้ง (ซอร์สเขียนว่า reserved for tilt compensation) แปลว่า ยังไม่ชดเชยการเอียง เอียงบอร์ดเมื่อไรทิศเพี้ยนทันที
cal_status()['valid'] เปลี่ยนเป็น True ได้เองโดยเราไม่ได้สั่ง เพราะงานเบื้องหลังของบอร์ดป้อนตัวอย่างให้ตัวสะสมอยู่ตลอด ต้องครบ 50 ตัวอย่าง และ ช่วงกว้างเกิน 15 หน่วย ทั้งสองแกน จึงจะผ่าน
ตัวเลขที่ยังไม่มีข้อสรุป: บอร์ดจริงอ่านขนาดสนามราว 1532 แต่สนามแม่เหล็กโลกคือ 25-65 µT — ทิศถูก (วัดได้ 215.8°) แต่ตัวเลขขนาดกับป้ายหน่วย µT ถูกพร้อมกันไม่ได้ อย่าสอนตัวเลขขนาดเป็นข้อเท็จจริง
สามสไลด์ที่ผ่านมาเปิดทีละกลุ่ม สไลด์นี้วางทั้งตระกูลไว้ข้างกัน — สองโมดูลย่อยของ sensors กับอีกสี่ชื่อของ dsp ที่เป็นเรื่อง IMU ล้วน ๆ ไม่ได้ให้ท่อง แต่ให้รู้ว่าอะไรมีอยู่
sensors.bmi270 — ห้าชื่อ ทั้งโมดูลย่อยมีเท่านี้ ไม่มีคำสั่งตั้งย่านวัด ไม่มี FIFO ไม่มี interrupt ให้เรียกจาก Python
| ชื่อ | คืนอะไร | คาบนี้ใช้ไหม |
|---|---|---|
motion() |
หกแกนจากการอ่านครั้งเดียว (ax, ay, az, gx, gy, gz) |
หัวใจของโครงหลัก — เหตุผลอยู่ในสไลด์ "หกแกนจากการอ่านครั้งเดียว" |
acceleration() |
(ax, ay, az) หน่วย m/s² |
ใช้ในตัวอย่าง 03_tilt_from_gravity.py |
gyroscope() |
(gx, gy, gz) หน่วย องศา/วินาที |
ใช้ตอนเทียบว่า accel กับ gyro เสียคนละแบบ |
temperature() |
อุณหภูมิของชิป | OSError บน Eva Kit — ค่ามาทางคอร์จอ ไม่มีช่องนี้ใน snapshot · ใช้ได้บน Dev Kit ที่ CM33 อ่านชิปตรง |
chip_id() |
เลขประจำรุ่น | OSError บน Eva Kit ด้วยเหตุผลเดียวกัน · ใช้ได้บน Dev Kit |
sensors.bmm350 — ห้าชื่อ ทั้งห้าเรียกได้จริงทั้งสองบอร์ด เพราะอยู่คนละบัสกับที่คอร์จอ Eva ถือไว้ · กางรายละเอียดไว้แล้วในสไลด์ "เข็มทิศบนบอร์ด"
magnetic() |
heading() |
chip_id() |
cal_reset() |
cal_status() |
|---|---|---|---|---|
(mx, my, mz) |
ทิศ 0-360° | เลขรุ่น | ล้างค่าสอบเทียบ | valid + offset สองแกน |
ทั้งห้าตัว เกินเกณฑ์ผ่าน M6 อยู่ในคาบนี้เพราะเป็นของตระกูลเดียวกัน ไม่ได้อยู่ในสิ่งที่วัดผล
ฝั่ง IMU ของ dsp — สี่ชื่อ สองฟังก์ชัน สองคลาส · อีกสิบชื่อที่เหลือของ dsp (ตัวกรองหกตัวกับฟังก์ชันสิ่งแวดล้อมสี่ตัว) อยู่ในแผนที่โมดูลของคาบ 5
| ชื่อ | ชนิด | รับ / คืน | คาบนี้ใช้ไหม |
|---|---|---|---|
tilt(ax, ay, az) |
ฟังก์ชัน | ความเร่ง 3 แกน → (roll, pitch) องศา |
ใช้ในโครงหลัก · ลำดับที่คืนกลับมาคือกับดักที่ตั้งใจดักทั้งคาบ |
compass(mx, my, mz) |
ฟังก์ชัน | สนามแม่เหล็ก 3 แกน → 0-360° | ไม่อยู่ในโครงหลัก · ทิ้ง mz ทั้งดุ้น ยังไม่ชดเชยการเอียง |
Madgwick(beta=, fs=) |
คลาส | สามเมธอด .update() .quaternion() .reset() |
ไม่อยู่ในโครงหลัก · ให้ yaw ที่ tilt() ให้ไม่ได้ · gyro ต้องเป็นเรเดียน/วินาที |
Pedometer(threshold=, min_interval=) |
คลาส | สองเมธอด .update() .reset() |
ไม่อยู่ในโครงหลัก · .update() คืน (steps, active) · ไม่มี .value() |
นับรวม: 5 + 5 + 4 = 14 ชื่อ และบนสองคลาสนั้นมีอีก 5 เมธอด ซึ่งนับแยก เพราะอยู่บนอ็อบเจกต์ ไม่ได้อยู่บนโมดูล
สองชื่อของ
bmi270ใช้ไม่ได้บน Eva Kit (ใช้ได้บน Dev Kit) และแปดในสิบสี่ชื่ออยู่นอกเกณฑ์ผ่าน — รู้ว่ามีอะไรอยู่ก่อน แล้วค่อยเลือกว่าจะหยิบตัวไหนไปใช้กับงานของทีม
วางบอร์ดบนโต๊ะที่คิดว่าราบแล้วอ่านค่า จะเจอ 1.5° บ้าง 2.3° บ้าง แทบไม่มีทางได้ 0.0 เป๊ะ สาเหตุมีสามชั้นซ้อนกัน
ไทย: จำมุมตอนกดปุ่ม "ตั้งศูนย์" ไว้ แล้วลบออกทุกครั้ง
ตัวเลขจาก Eva Kit (บอร์ดที่ใช้เขียนสไลด์): กดตั้งศูนย์ตอน → ต่อจากนั้น 1.8° กลายเป็น 0.0° และ 4.3° กลายเป็น 2.5°
roll_zero = roll_f # จำท่าปัจจุบันไว้เป็นจุดอ้างอิง
roll_show = roll_f - roll_zero
เครื่องมือวัดที่ดีไม่ได้แปลว่าค่าดิบแม่นเป๊ะ แต่แปลว่า ผู้ใช้กำหนดจุดอ้างอิงเองได้
ราวปี 1661 Melchisédech Thévenot ทำเครื่องวัดระดับแบบหลอดแก้วใส่ของเหลวที่มีฟองอากาศ — ฟองเบากว่าของเหลว มันจึงลอยไปอยู่จุดสูงสุดของหลอดเสมอ ช่างไม้ทั่วโลกใช้มันมาสามร้อยกว่าปีโดยไม่ต้องมีแบตเตอรี่
ปี 1991 Analog Devices ออก ADXL50 ซึ่งเป็น accelerometer แบบ MEMS ตัวแรกที่ผลิตขายจำนวนมาก เป้าหมายแรกคือถุงลมนิรภัย — ต้องแยก "ชนจริง" ออกจาก "ตกหลุมถนน" ให้ได้ในไม่กี่มิลลิวินาที
ภาพขวาบนคือ IMU ทั้งชุดวางเทียบกับเหรียญเพนนี ภาพขวาล่างคือภาพถ่ายไดจริงของ IMU ที่เปิดฝาออก
เชื่อมกับวันนี้: โปรแกรมที่เราจะเขียนอีกครู่คือหลอดแก้วของ Thévenot ที่ถูกเขียนใหม่ด้วย Python สิบกว่าบรรทัด และมันทำสิ่งที่หลอดแก้วทำไม่ได้ — บอกเป็นตัวเลของศา จำค่าอ้างอิงได้ และส่งค่าขึ้นเครือข่ายได้ในคาบที่ 10


สิ่งที่เฟิร์มแวร์ทำให้แล้ว (70%)
ตั้งค่าและปลุก BMI270 ผ่าน I2C, จัดการบัสให้ (Eva: คอร์จออ่านแล้วส่ง snapshot มาให้ · Dev Kit: CM33 อ่านตรงใต้ล็อกบัส), แปลงข้อมูลดิบเป็น m/s² และ deg/s, สูตร atan2 ใน dsp.tilt(), คลาสฟิลเตอร์ dsp.EMA, วาด Bar, Scale, Led และ Seg7 บนจอผ่าน IPC
สิ่งที่เป็นงานของเรา (30%)
เลือกว่าจะอ่านด้วย motion() หรืออ่านแยก · เลือก alpha ของฟิลเตอร์ · ออกแบบว่า "ศูนย์" ของเครื่องนี้หมายถึงอะไรและให้ผู้ใช้ตั้งเมื่อไร · ตัดสินใจว่าจะแสดงกี่ตำแหน่งทศนิยม · จัดวางหน้าจอให้คนอ่านเข้าใจใน 2 วินาที
คนที่เขียน
atan2เองได้แต่เลือก alpha ไม่เป็น จะได้เครื่องมือที่ใช้งานจริงไม่ได้
import ui
ui.screen()
import time
import sensors
import dsp
ui.clear()
time.sleep_ms(200)
...
try: # ไม่มี sensors.init() ทั้งสองบอร์ด
sensors.bmi270.motion() # อุ่นเครื่อง ครั้งแรกหลังรีเซ็ตอาจต้องรอ
except OSError:
print("อ่านเซนเซอร์รอบแรกยังไม่ได้ - ลองใหม่ในลูป")
บน Eva Kit ไม่มี sensors.init() ให้เรียกแล้ว บัส I2C ของ BMI270 เป็นของคอร์จอ (CM55) ขับจากสคริปต์แล้วบอร์ดค้างถาวร เฟิร์มแวร์จึง ปฏิเสธด้วย OSError · บน Dev Kit ผ่านแต่ไม่ต้องเรียกเช่นกัน — เฟิร์มแวร์ปลุก BMI270 ไว้ตั้งแต่บูต CM33 อ่านมันตรงจาก I2C · อุ่นเครื่องใน try/except เพราะรอบแรกหลังรีเซ็ตอาจต้องรอ (Eva วัดได้ถึง 16 วินาที) ยังไม่ตอบก็พิมพ์บอกแล้วลองใหม่ในลูป
motion() .acceleration() .gyroscope() คืนค่าหน้าตาเดียวกันทั้งสองบอร์ด ต่างกันแค่ต้นทาง (Eva: snapshot ของคอร์จอ · Dev Kit: อ่านสดจากชิป) · กฎเหล็กข้อ 4 — แตะ ui.* ครั้งแรก sensor auto-task จะหยุด จากนั้นเราต้องอ่านค่าเองทุกรอบ · ui.screen() วางผิดที่เมื่อไร widget หายทั้งหน้าทันที
bmi270.temperature()/.chip_id()ขึ้นOSErrorบน Eva Kit (Dev Kit ใช้ได้) — โค้ดของคอร์สจึงไม่พึ่งสองคำสั่งนี้
ui.Label("ROLL เอียงซ้าย-ขวา (องศา)", x=40, y=60, color=COL_DIM, value=16)
roll_bar = ui.Bar(x=40, y=96, w=160, h=16, color=0x4A9EFF, min=-90, max=90, value=0)
roll_scale = ui.Scale(x=40, y=112, w=160, h=44, color=COL_TEXT, min=-90, max=90)
roll_scale.ticks(9, 4) # -90 0 90
roll_seg = ui.Seg7("+00.0", x=216, y=96, w=96, h=56, color=COL_TEXT)
# pitch ชุดเดียวกัน เลื่อนไปขวา 288 พิกเซล
...
led_in = ui.Led(x=632, y=72, w=48, h=48, color=COL_OK, value=1)
led_out = ui.Led(x=704, y=72, w=48, h=48, color=COL_BAD, value=0)
min=-90, max=90 คือหัวใจของแถบตัวนี้ ค่าเริ่มต้นของ ui.Bar คือ 0-100 ถ้าไม่กำหนดช่วงเอง พอส่ง −30 องศาเข้าไปมันจะถูกปัดเป็น 0 แล้วแถบนิ่งสนิททั้งที่โค้ดคำนวณถูก
ui.Scale แนวนอนคือไม้บรรทัด ไม่ใช่หน้าปัด ไม่รับ .value() ตัวที่ขยับคือ ui.Bar ที่วางทับ ประโยชน์คือพา พิสัย มาอยู่บนจอ (แบบวงกลมมีเข็มจริง — อีกสองสไลด์) · .ticks(9, 4) คือ 9 ขีด ใส่ตัวเลขทุกขีดที่สี่ ได้ -90 0 90 — สองแกนวางเคียงกัน ไม้บรรทัดกว้างแค่ 160 พิกเซล ใส่ถี่กว่านี้ตัวเลขจะเบียดกัน
ui.Seg7 รับสตริง จึงใส่ + − และทศนิยมได้ · ไฟสองดวงแทนตัวหนังสือสี ถ่ายจอเป็นขาวดำแล้วยังแยกออกว่าดวงไหนติด
ตั้งช่วง min/max ให้ตรงกับปริมาณจริงเสมอ ไม่งั้นเราจะไปไล่หาบั๊กผิดที่ทั้งวัน — และค่าที่วัดได้ห้ามอยู่ลำพัง ต้องมีพิสัยหรือเกณฑ์อยู่ข้าง ๆ
sp_tol = ui.Spinbox(x=40, y=240, w=96, h=88, color=COL_TEXT,
min=TOL_MIN, max=TOL_MAX, value=tol)
sp_tol.digits(2, 0)
btn_dn = ui.Button("ลด", x=168, y=240, w=88, h=88, color=0x3A4150, value=20)
btn_up = ui.Button("เพิ่ม", x=288, y=240, w=88, h=88, color=0x3A4150, value=20)
...
zero_btn = ui.Button("ตั้งศูนย์", x=472, y=240, w=144, h=88, color=0x30A46C, value=20)
zero_id = zero_btn.id()
exit_btn = ui.Button("จบการวัด", x=648, y=240, w=104, h=88, color=0x3A4150, value=20)
exit_id = exit_btn.id()
ui.Spinbox ทำให้เกณฑ์เป็นของผู้ใช้ ไม่ใช่ของโปรแกรมเมอร์ งานวางกล้องยอมได้ 1 องศา งานวางตู้เย็นยอมได้ 10 คนที่รู้คือคนหน้างาน TOL_MIN, TOL_MAX = 1, 30 หนีบให้ตั้งเกินพิสัยไม่ได้ และ .digits(2, 0) ทำให้เห็น 05 แทน 0005
บนจอสัมผัส spinbox เปล่า ๆ นิ้วเปลี่ยนค่าไม่ได้ การแตะแค่เลือกตำแหน่งหลัก จึงต้องมี ui.Button เพิ่ม/ลดข้าง ๆ เสมอ — ทั้งห้าเป้าสัมผัสสูง 88 px ตามขนาดเป้าสัมผัสของหลักสูตร เรียงแถวเดียวเว้น 32 px และปุ่มขวาสุดจบเหนือมุมที่ปุ่ม Console จองไว้ (x>690 และ y>340) พอดี
zero_btn.id() / exit_btn.id() เก็บไว้ตั้งแต่สร้าง เพราะในลูปเราจะเทียบ ev['handle'] กับเลขนี้ · ปุ่มสั่งงานสองปุ่มแยกหน้าที่กันคนละปุ่ม ไม่มีปุ่มไหนสลับสองความหมายในตัวเดียว · widget ทั้งหน้า 25 ตัว — อยู่ในงบ 32 ที่ตั้งเอง (เพดานเฟิร์มแวร์ 64)
เกณฑ์ที่ฝังในโค้ดคือเกณฑ์ของคนเขียนโปรแกรมเมื่อสามเดือนก่อน — เกณฑ์ที่ตั้งได้บนจอคือเกณฑ์ของคนหน้างานวันนี้
SC_X, SC_Y, SC_W = CX - 100, 95, 200
sc = ui.Scale(x=SC_X, y=SC_Y, w=SC_W, h=SC_W, color=0x1565C0, min=0, max=100)
sc.prop(ui.PROP_SCALE_MODE, ui.SCALE_ROUND_OUT)
sc.ticks(21, 5)
...
NEEDLE = 85
sc.prop(ui.PROP_SCALE_NEEDLE_COLOR, 0xFF9800) # ครั้งเดียว
...
def draw_needle(v):
# ทุกครั้งที่ค่าเปลี่ยน: เขียน prop เดียว เข็มหมุนเอง
sc.prop(ui.PROP_SCALE_NEEDLE, (NEEDLE << 16) | (int(v) & 0xFFFF))
ค่าที่ส่งแพ็กสองเรื่องในเลขเดียว — สูง 16 บิตคือความยาวเข็มเป็นพิกเซล (NEEDLE = 85) ต่ำ 16 บิตคือค่าบนสเกล · CX = 396 คือกึ่งกลางจอ
เข็มของ lv_scale หมุนอยู่กับที่ จอวาดซ้ำเฉพาะบริเวณเข็ม — วิธีเก่าที่ลบ ui.Line แล้วสร้างใหม่ทุกค่า ทำให้ทั้งหน้าปัดวาบทุกจังหวะ (เจอจริงบนบอร์ด 2026-08-20)
ต้องใช้ firmware 2026-08-20 ขึ้นไป — รุ่นก่อนหน้าใช้ ui.Bar ทับแบบหน้าที่แล้วไปก่อน
ของจริงทั้งไฟล์: examples/lvgl_ports/sec3_sensor_viz/eva/ex10_scale_pitch.py — pitch จาก dsp.tilt ขับเข็มสด

# --- ท่าที่ 3: อ่านหกแกนใน lock เดียว แล้วแปลงเป็นองศา ---
ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
roll, pitch = dsp.tilt(ax, ay, az)
สองบรรทัดนี้คือแก่นของทั้งคาบ บรรทัดบนได้ภาพนิ่งของบอร์ด บรรทัดล่างเปลี่ยนมันเป็นองศา
dsp.tilt() รับแค่ สามค่าแรก เท่านั้น มันไม่ต้องการ gyro เพราะคำนวณจากแรงโน้มถ่วงล้วน ๆ ส่วน gx, gy, gz ที่รับมาด้วยเราจะเอาไปแสดงเป็นข้อมูลอ้างอิงท้ายจอ
การอ่าน I2C พลาดได้เป็นครั้งคราวโดยเฉพาะตอนบัสหนาแน่น ในโค้ดจริงเราจึงห่อบรรทัดนี้ด้วย try / except OSError แล้วข้ามรอบนั้นไป
การอ่านเซนเซอร์ล้มเหลวเป็นเรื่อง ปกติ ที่ต้องออกแบบรองรับ ไม่ใช่เรื่องผิดปกติที่รอให้เกิดแล้วค่อยแก้
# --- ท่าที่ 4: กรองความสั่น แล้วหักค่าอ้างอิงที่ตั้งศูนย์ไว้ ---
roll_f = ema_roll.update(roll)
pitch_f = ema_pitch.update(pitch)
roll_show = roll_f - roll_zero
pitch_show = pitch_f - pitch_zero
.update() ทำสองอย่างพร้อมกัน: จำสถานะใหม่ไว้ในตัวมันเอง และคืนค่าที่กรองแล้วออกมาทันที
ลำดับสำคัญมาก ต้องกรองก่อน แล้วค่อยหักค่าศูนย์ เพราะตอนกดปุ่มตั้งศูนย์เราต้องเก็บ roll_f (ค่าที่กรองแล้ว) ไม่ใช่ค่าดิบ — ถ้าเก็บค่าดิบที่กำลังสั่นอยู่พอดี จุดอ้างอิงจะเพี้ยนตามความบังเอิญของเสี้ยววินาทีนั้น
ตั้งศูนย์จาก ค่าที่กรองแล้ว เสมอ — จุดอ้างอิงที่สั่น แย่กว่าไม่มีจุดอ้างอิง
ui.poll()# --- ท่าที่ 5: รับเหตุการณ์จากจอ ---
for ev in ui.poll():
h = ev['handle']
if h == zero_id:
roll_zero = roll_f
pitch_zero = pitch_f
ref.text("อ้างอิง R {:+.1f} P {:+.1f}".format(roll_zero, pitch_zero))
elif h == exit_id:
running = False
elif h == btn_up.id():
tol = min(TOL_MAX, tol + TOL_STEP)
sp_tol.value(tol)
elif h == btn_dn.id():
tol = max(TOL_MIN, tol - TOL_STEP)
sp_tol.value(tol)
ui.poll() คืน list ของ dict แต่ละใบมีคีย์ handle, type, value (สูงสุด 8 event ต่อครั้ง)
ต้องเรียกทุกรอบลูป แม้รู้อยู่แล้วว่าไม่มีใครกด เพราะฝั่ง CM55 ใช้การ poll เป็นสัญญาณว่าโปรแกรมยังมีชีวิต เงียบไปราวสองวินาที widget จะถูกซ่อนทั้งหน้า
ปุ่มเพิ่มกับปุ่มลดเป็นคนละปุ่มกันโดยตั้งใจ ปุ่มเดียวที่สลับสองทิศบอกไม่ได้ว่าตอนนี้กดแล้วจะขึ้นหรือจะลง คนกดจึงต้องเดา และเดาผิดได้เสมอ กฎเดียวกันนี้ใช้กับปุ่มเปิด-ปิดอุปกรณ์จริงทุกชนิด · sp_tol.value(tol) คือการเขียนค่ากลับลงช่อง ไม่ใช่การอ่าน — ตัวแปร tol ฝั่ง Python คือความจริง ช่องบนจอคือการแสดงผลของมัน
ui.poll()ไม่ใช่แค่ "อ่านปุ่ม" มันคือ ชีพจรที่เราส่งให้อีกคอร์รู้ว่าเรายังอยู่
# --- ท่าที่ 6: ส่งค่าขึ้นหน้าปัด แล้วเว้นจังหวะ ---
roll_bar.value(int(clamp90(roll_show))) # แถบขยับทุกรอบ
pitch_bar.value(int(clamp90(pitch_show)))
in_tol = abs(roll_show) <= tol and abs(pitch_show) <= tol
led_in.value(1 if in_tol else 0) # ติดทีละดวงเท่านั้น
led_out.value(0 if in_tol else 1)
sec = time.ticks_ms() // 1000 # ตัวเลขวินาทีละครั้ง
if sec != last_sec:
last_sec = sec
roll_seg.text("{:+.1f}".format(roll_show))
pitch_seg.text("{:+.1f}".format(pitch_show))
if not ok_read: # รอบนี้อ่านไม่ได้ - บอกเป็นคำ
lbl_health.color(COL_WARN)
health = "ค่าค้าง นี่คือค่าล่าสุด"
lbl_health.text(health) # ชีพจร - ส่งข้อความเดิมซ้ำทุกรอบ
time.sleep_ms(200)
lbl_health) เขียนคำว่า "ค่าค้าง" ด้วยสีเตือนเมื่อ motion() ขึ้น OSError แล้วเราใช้ค่าเดิมต่อ — ตั้งสีก่อน แล้วค่อยเขียนข้อความ · lbl_health.text(health) ส่งซ้ำทุกรอบ นอก ประตูหนึ่งวินาทีโดยตั้งใจ เพราะคำสั่งเขียนข้อความคือตัวปลุกจอให้อยู่โหมดเร็ว ส่วน .value() ของแถบกับไฟไม่ปลุก (กลไกเดียวกับคาบ 5) · sleep_ms(200): 5 ครั้ง/วินาที IPC ไม่ล้น และ EMA α=0.2 ให้ τ ≈ 1 วินาทีพอดีint() ก่อนส่งให้ ui.Bar ทุกครั้ง แถบรับจำนวนเต็ม ส่วน ui.Seg7 รับสตริงจึงโชว์ทศนิยมได้ · clamp90() บีบค่าไว้ใน −90..+90 เพราะคว่ำบอร์ดจนเกือบกลับหัว roll วิ่งถึง 180° เกินช่วงแถบแล้วค้างปลายสุดโดยไม่บอกอะไร · แถบกับไฟขยับทุกรอบ (5 ครั้ง/วินาที) เพราะตาอ่าน "ตำแหน่ง" ได้ ส่วนตัวเลขบน Seg7 เขียนใหม่ ไม่เกินวินาทีละครั้ง — เลขที่วิ่งห้าครั้งต่อวินาทีคือเลขที่อ่านไม่ทัน ประตูคือ if sec != last_sec:
ทุกตัวเลขในสองบรรทัดสุดท้ายนี้ผูกกันหมด — เปลี่ยน cadence เมื่อไร ต้องกลับไปคิดเรื่อง alpha ใหม่ด้วย
ทุกกล่องในภาพนี้แทนได้ด้วยหนึ่งบรรทัด Python ยกเว้นกล่องแรกกล่องเดียว
practise_codes/s06_digital_level.py↗ ใน BENTO IDE แล้วเติมช่องว่าง pass ให้ครบ 6 จุดเอียงช้าคือการทดสอบที่ถูกต้อง เอียงเร็วคือการทดสอบ ข้อจำกัด ของฟิลเตอร์ ทำทั้งสองอย่างแต่ให้รู้ว่ากำลังทำอะไร
เครื่องวัดระดับ — วางราบอ่าน ~0°, เอียงแล้วแถบทั้งสองแกนวิ่งถูกทิศ และไฟบอกได้ว่าเกินเกณฑ์หรือยัง
แปลเป็นสิ่งที่ตรวจได้จริง:
ข้อที่แปดคือข้อที่พิสูจน์ว่าเรา ไม่ได้ มีปัญหา drift เพราะเราไม่ได้อินทิเกรตอะไรเลย
| อาการ | สาเหตุที่แท้จริง | วิธีแก้ |
|---|---|---|
| เอียงซ้าย-ขวา แต่แถบ PITCH วิ่งแทน | เขียน pitch, roll = dsp.tilt(...) สลับลำดับ |
สลับเป็น roll, pitch = ... |
| แถบนิ่งสนิททั้งที่ตัวเลขเปลี่ยน | ui.Bar ไม่ได้ตั้ง min=-90, max=90 (ค้างที่ 0-100) |
เติม min/max ตอนสร้างแถบ |
ui.Scale ไม่ขยับเลยสักครั้ง |
แนวนอนคือไม้บรรทัด ไม่รับ .value() |
ตัวที่ต้องขยับคือ ui.Bar ที่วางทับ · แบบวงกลมใช้ .prop(ui.PROP_SCALE_NEEDLE, ...) (fw 2026-08-20 ขึ้นไป) |
| ตัวเลขบนไม้บรรทัดเบียดกันจนอ่านไม่ออก | ขีดเยอะเกินไปสำหรับความกว้างที่มี | .ticks(9, 4) บนความกว้าง 160 พิกเซล — ได้ -90 0 90 |
ui.Spinbox แตะแล้วค่าไม่เปลี่ยน |
จอสัมผัสไม่มีลูกบิดหมุน การแตะแค่เลือกตำแหน่งหลัก | ต้องมี ui.Button เพิ่ม/ลดข้าง ๆ เสมอ |
ช่องเกณฑ์ขึ้น 0005 ทั้งที่ตั้ง 5 |
ค่าตั้งต้นของ spinbox คือสี่หลัก | sp_tol.digits(2, 0) |
| ไฟดับแล้วยังเห็นเป็นวงจาง ๆ | ตั้งใจ .value(0) คือหรี่ ไม่ใช่หาย |
ไฟที่หายไปทำให้แยกไม่ออกว่าดับหรือจอเสีย |
| แถบกระตุกเป็นช่วง ๆ อ่านยาก | ยังไม่ได้ต่อ EMA หรือ alpha สูงเกินไป | ใช้ dsp.EMA(alpha=0.2) แล้วป้อน .update() |
| แถบตามมือช้ามาก | alpha ต่ำเกินไป (เช่น 0.02) | ขยับขึ้นเป็น 0.2-0.3 แล้วลองใหม่ |
| วางราบแล้วไม่ได้ 0.0 สักที | โต๊ะเอียง + offset ของชิป | วางนิ่งแล้วกดปุ่ม ตั้งศูนย์ |
| widget หายทั้งจอหลังรันไปพักหนึ่ง | ลืม ui.poll() ในลูป |
เรียก ui.poll() ทุกรอบเสมอ |
| อาการ | สาเหตุที่แท้จริง | วิธีแก้ |
|---|---|---|
| ค่าเพี้ยนหนักตอนถือเดินไปมา | ความเร่งจากการเคลื่อนที่ปนกับแรงโน้มถ่วง | วางนิ่งแล้วเอียงช้า ๆ หรือรอ fusion เต็มรูป |
OSError เด้งกลางคัน |
เซนเซอร์ไม่ตอบทันในรอบนั้น (Eva: คอร์จอยังไม่ตอบ · Dev Kit: บัส I2C ยังไม่ว่าง) | ห่อด้วย try / except OSError แล้วข้ามรอบนั้น |
OSError ที่บรรทัด sensors.init() |
บน Eva Kit เฟิร์มแวร์ปฏิเสธคำสั่งนี้ (บน Dev Kit ผ่านเงียบ ๆ แต่ไม่จำเป็น) | ลบบรรทัดนั้นทิ้ง ไม่ต้องมี init เลย ทั้งสองบอร์ด |
OSError ที่ bmi270.temperature() / chip_id() |
บน Eva Kit สองค่านี้ไม่อยู่ใน snapshot (Dev Kit ใช้ได้) | อย่าพึ่งสองคำสั่งนี้ · IMU ยังมีชีวิตไหม ดู snapshot()['bmi270']['sequence'] เดินขึ้นหรือเปล่า |
Madgwick มุมหมุนติ้วจนอ่านไม่ได้ |
ป้อน gyro เป็น deg/s ทั้งที่มันต้องการ rad/s | ครอบด้วย math.radians() ทั้งสามแกน |
Madgwick มุมเดินเร็ว/ช้ากว่าความจริง |
fs= ไม่ตรงกับคาบลูปจริง |
ลูป 200 ms คือ fs=5.0 ไม่ใช่ค่าตั้งต้น 100 |
AttributeError: value ตอนเรียก ahrs.value() |
Madgwick กับ Pedometer ไม่มี .value() |
ใช้ .quaternion() หรือเก็บค่าที่ .update() คืนมาเอง |
Pedometer นับก้าวขึ้นทั้งที่บอร์ดวางนิ่ง |
ค่าตั้งต้น threshold=1.5 เป็นหน่วย g แต่ป้อน m/s² เข้าไป |
หารด้วย 9.81 ก่อนป้อน หรือตั้ง threshold=12.0 ให้ตรงหน่วย |
dsp.compass() กับ bmm350.heading() ไม่ตรงกัน |
คนละสูตร — atan2(y,x) กับ atan2(x,y) |
ไม่ใช่บั๊ก เลือกใช้ตัวใดตัวหนึ่งให้ตลอดทั้งโปรแกรม |
| เอียงบอร์ดแล้วทิศเปลี่ยนทั้งที่ไม่ได้หมุน | dsp.compass() ทิ้ง mz จึงยังไม่ชดเชยการเอียง |
ถือบอร์ดให้ราบตอนอ่านทิศ |
เปิด practise_codes/s06_digital_level.py↗ มีช่องว่างให้เติม 6 จุด
# เติม: sensors.bmi270.motion() (ห้ามใส่ sensors.init())
pass
# เติม: ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
pass
# เติม: roll, pitch = dsp.tilt(ax, ay, az)
pass
# เติม: roll_f = ema_roll.update(roll)
pass
# เติม: roll_zero = roll_f แล้วบรรทัดถัดไป pitch_zero = pitch_f
pass
# เติม: roll_bar.value(int(clamp90(roll_show)))
pass
หน้าจอทั้ง 25 ชิ้นเขียนไว้ให้ครบแล้ว ไม่ต้องแตะ ทั้งไม้บรรทัด ช่องเกณฑ์ ปุ่มเพิ่ม/ลด ไฟสองดวง และบรรทัดคุณภาพของค่า งานของเราคือทำให้ค่าไหลเข้าไปในนั้น
เติมทีละจุดแล้วรัน จะรู้ทันทีว่าพังที่จุดไหน — เติมครบหกจุดแล้วรันทีเดียวคือการเดา
ต้องทำในคาบ · เปิดตามลำดับนี้ ทั้งชุดราว 35 นาที
| ลำดับ · เรื่อง · เวลา | ไฟล์ | ลงมือทำอะไร แล้วจะเข้าใจอะไร |
|---|---|---|
1 · ข้างในของ dsp.tilt() · 10 นาที |
examples/s06/03_tilt_from_gravity.py↗ |
เอียงบอร์ดแล้วดูสามคอลัมน์: ค่าดิบ · คำตอบของ dsp.tilt() · คำตอบที่ไฟล์นี้คำนวณเองด้วย atan2 · สองคอลัมน์ขวาตรงกันเสมอ และแถวล่างแสดงให้เห็นว่าถ้าแกะสลับลำดับจะอ่านได้เป็นอะไร |
| 2 · กรองก่อนค่อยตัดสิน · 10 นาที | examples/s06/01_imu_step_counter.py↗ |
เดินถือบอร์ดสิบก้าวแล้วเทียบเลขที่นับได้กับที่เดินจริง · จะเห็นว่าถ้าไม่กรองก่อนและไม่มีเวลาห้ามนับซ้ำ สั่นครั้งเดียวจะถูกนับหลายก้าว |
| 3 · เหตุการณ์เดียวไม่พอ · 15 นาที | examples/s06/02_imu_fall_detection.py↗ |
ลองวางบอร์ดลงแรง ๆ ให้มันเข้าใจผิดว่าล้ม แล้วดูว่าขั้นไหนไม่ผ่าน · การล้มคือลำดับของเหตุการณ์ ไม่ใช่ความแรงอย่างเดียว |
เปิดตามความสนใจ · นอกเกณฑ์ผ่านของคาบนี้ แต่ปิดช่องว่างของโมดูล
| เรื่อง | ไฟล์ | ทำไมถึงมี |
|---|---|---|
| เข็มทิศ และข้อบกพร่องที่ยังค้าง | examples/s06/04_compass_and_magnetometer.py↗ |
ครบทั้งห้าคำสั่งของ bmm350 และแสดง dsp.compass() เทียบกับ heading() ให้เห็นว่าคนละสูตร |
| fusion เต็มรูป และตัวนับก้าวสำเร็จรูป | examples/s06/05_madgwick_and_pedometer.py↗ |
dsp.Madgwick กับ dsp.Pedometer สองคลาสสุดท้ายของโมดูล พร้อมกับดักเรื่องหน่วยทั้งสองข้อ |
ติดตรงไหน เปิดอันนี้
| อาการที่เจอ | ไฟล์ที่ตอบอาการนั้น |
|---|---|
กรอกช่อง dsp.EMA(alpha= ในตาราง 4.2 ไม่ถูก ไม่รู้จะใส่เท่าไร |
examples/s05/06_ema_time_constant.py↗ — แปลง alpha เป็นเวลา tau เป็นวินาที เลือกจากตัวเลขได้เลย ไม่ต้องเดา |
| วางบอร์ดนิ่งแล้วแถบยังกระตุก ทั้งที่ใส่ตัวกรองไปแล้ว | examples/s05/07_median_beats_mean.py↗ — ค่าหลุดค่าเดียวลากค่าเฉลี่ยไปด้วย แต่ทำอะไร median ไม่ได้ เพราะ median เรียงแล้วหยิบตัวกลาง |
s06_digital_level.py↗ — ตั้งต้นและหน้าจอimport ui
ui.screen()
import time
import sensors
import dsp
ui.clear()
time.sleep_ms(200)
COL_TEXT, COL_DIM = 0xE8EAED, 0x9AA3AF
COL_CARD = 0x171B22
COL_OK, COL_WARN, COL_BAD = 0x30A46C, 0xF5A623, 0xE5484D
TOL_MIN, TOL_MAX, TOL_STEP = 1, 30, 1
tol = 5 # เกณฑ์ยอมรับ หน่วยองศา
... # ท่า 1-2 ดูสไลด์ "แกะโค้ดจริง"
ema_roll = dsp.EMA(alpha=0.2)
ema_pitch = dsp.EMA(alpha=0.2)
roll_zero = 0.0
pitch_zero = 0.0
...
last_sec = -1 # วินาทีที่เพิ่งเขียนตัวเลขลงจอ
def clamp90(v):
return 90.0 if v > 90.0 else (-90.0 if v < -90.0 else v)
0x4A9EFF ปุ่มรอง 0x3A4150 ปุ่มหลัก 0x30A46C ตามจานสีของหลักสูตร · ตัวกรองสองตัวเพราะสองสัญญาณ ตัวกรองมี ความจำrunning = True
while running:
ok_read = True
try:
ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
roll, pitch = dsp.tilt(ax, ay, az)
except OSError:
ok_read = False
if ok_read:
roll_f = ema_roll.update(roll)
pitch_f = ema_pitch.update(pitch)
roll_show = roll_f - roll_zero
pitch_show = pitch_f - pitch_zero
for ev in ui.poll():
h = ev['handle']
if h == zero_id:
roll_zero = roll_f
pitch_zero = pitch_f
elif h == exit_id:
running = False
# ปุ่มเพิ่ม/ลด tol - ท่าที่ 5
...
roll_bar.value(int(clamp90(roll_show))) # แถบ ไฟ ตัวเลข - ท่าที่ 6
...
lbl_health.text(health)
time.sleep_ms(200)
if ok_read — รอบที่อ่านไม่ได้ก็ยังต้องวาดจอด้วยค่าเดิม แล้วเขียนบรรทัดคุณภาพ lbl_health ว่า "ค่าค้าง" ด้วยสีเตือน ถ้าปล่อยให้การวาดอยู่ใน if จอจะแช่ภาพเดิมโดยไม่มีอะไรบอกคนดู · in_tol ใช้ค่าที่ กรองแล้วและหักศูนย์แล้ว ไม่ใช่ค่าดิบ ไม่งั้นไฟจะกระพริบสลับดวงตอนค่าคาบเกี่ยวเกณฑ์ (alarm chattering) · for ev in ui.poll() อยู่ นอก if ok_read โดยตั้งใจ — เซนเซอร์ตายไม่ได้แปลว่าทั้งเครื่องต้องตายตามวันนี้เราได้: เข้าใจว่า accelerometer วัดแรงที่ดันมวลไว้ (proper acceleration) · อ่านหกแกนจากการอ่านครั้งเดียวและรู้ว่าทำไม (Eva: snapshot ชุดเดียว · Dev Kit: lock บัสครั้งเดียว) · แปลงเวกเตอร์แรงโน้มถ่วงเป็นสององศาด้วย dsp.tilt() โดยจำลำดับ (roll, pitch) ได้ · รู้ว่า gyro drift กับ accel jitter คนละปัญหากัน · สร้างเครื่องมือวัดที่มีปุ่มตั้งศูนย์แบบเดียวกับเครื่องมือจริง
รากฐานที่แตะไป: เซนเซอร์ MEMS และความหมายทางฟิสิกส์ของค่าที่คืน · atomic read บนบัสร่วม · ตรีโกณมิติกับ atan2 · low-pass แบบ recursive และแนวคิด time constant · การอินทิเกรตกับการสะสมความผิดพลาด · calibration และ tare · การเลือกช่วงสเกลให้ตรงกับปริมาณจริง
คาบหน้า: เอา accel สามแกนเดียวกันนี้ไปวาดเป็น กราฟ real-time พร้อมตารางค่าสุดขีดและปุ่มเริ่ม/หยุดบันทึก
atan2กับ EMA จะตามน้อง ๆ ไปทุกงานที่มีเซนเซอร์ ไม่ว่าจะเปลี่ยนภาษา เปลี่ยนบอร์ด หรือเปลี่ยนบริษัท
สังเกตช่อง "ต้องการ" ของทั้งสี่มุม — โจทย์ต่างกัน จึงเลือกฟิลเตอร์และ cadence ต่างกัน ไม่มีสูตรเดียวใช้ได้หมด
ข้อ 1 · โหมดฟองอากาศ
ทำระดับน้ำแบบฟองอากาศด้วย ui.Panel วงเล็กหนึ่งใบ แล้วขยับด้วย .pos(x, y) ตามค่า roll และ pitch ให้เหมือนฟองในหลอดแก้ว วางราบแล้วฟองต้องอยู่กลางวงกลมอ้างอิง
ข้อ 2 · เตือนเมื่อไม่ได้ระดับ
ถ้ามุมแกนใดเกิน ±3° ให้เปลี่ยนสี Seg7 เป็นแดงด้วย .color() และจุดไฟจริงหนึ่งดวงพร้อมกัน — เลือกดวงตามชื่อใน gpio.board_info()["led_names"] (ดู led_named() ใน examples/s07/01) ไม่ใช่ gpio.led(0) ตายตัว เพราะเลข 0 เป็นคนละดวง (ชื่อต่างกัน) บนสองบอร์ด · คิดต่อว่าจะกัน "กะพริบถี่" ตอนค่าแกว่งอยู่ที่ขอบพอดีได้อย่างไร
ข้อ 3 · แข่ง alpha
สร้าง dsp.EMA สองตัวด้วย alpha ต่างกัน (0.05 กับ 0.5) ป้อน roll ตัวเดียวกันเข้าทั้งคู่ จับเวลาด้วย time.ticks_ms() ว่าแต่ละตัวใช้กี่มิลลิวินาทีกว่าจะไล่ตามการเอียงทัน แล้วสรุปเป็นตัวเลขว่าแลกอะไรกับอะไร
ข้อ 4 · พิสูจน์ว่า gyro drift จริงไหม
เขียนมุมจาก gyro ล้วน ๆ ด้วย gyro_roll = gyro_roll + gx * 0.2 ทุกรอบ แสดงคู่กับ roll จาก dsp.tilt() วางนิ่งสามนาที บันทึกว่าเส้นไหนหนีกี่องศา

เขียนคำตอบลง worksheet ข้อ 8 แล้วเอามาเล่าให้เพื่อนฟังต้นคาบหน้า


แหล่งปฐมภูมิ
วิดีโอที่ฝังไว้ในคาบนี้ (ตรวจแล้วว่าเปิดได้ ตาม _BUILD/MEDIA_PLAN.md §7)
RLQGZl0lpjQ Working principle of an accelerometer — Bosch Sensortec · 1:01 · KuekQ-m9xpw How does an Accelerometer work? — CircuitBread · 6:10 · XRr1kaXKBsU What Everyone Gets Wrong About Gravity — Veritasium · 17:33 (เริ่ม 2:30) · PK05u9c3yWI How do MEMS gyroscopes work? — nanolearning · 13:44 (เริ่ม 4:00) — คำอธิบายว่าดูเพื่ออะไร อยู่ใต้คลิปในสไลด์ที่ฝังไว้แล้ว
เครดิตภาพ — ทุกภาพมาจาก Wikimedia Commons หรือบทความ open-access ใน PMC (CC0 / CC BY / CC BY-SA / สาธารณสมบัติ) รายละเอียดที่ CREDITS.md · ห้ามใช้ บทความไทยที่ติดอันดับสูงสุดเรื่อง gyro/accel เพราะอธิบายผิดหลักการ ดู _BUILD/MEDIA_PLAN.md §7.2
ตัวอย่างโค้ด — examples/s06/ ห้าไฟล์ · คลังคาบนี้ถูกคัดจากหกไฟล์เหลือสองเมื่อ 14 ส.ค. (ไฟล์ที่ถอดออกเป็นเรื่องบิตกับเรื่องตัวกรอง ซึ่งเป็นของคาบอื่น) แล้ว เพิ่มกลับสามไฟล์เมื่อ 15 ส.ค. เพื่อปิดช่องว่างของ API ที่คาบนี้เป็นเจ้าของ: 03_tilt_from_gravity.py (dsp.tilt ซึ่งเป็นแกนกลางของคาบและเดิมไม่มีตัวอย่างเลย) · 04_compass_and_magnetometer.py (sensors.bmm350 ทั้งห้าคำสั่ง + dsp.compass) · 05_madgwick_and_pedometer.py (dsp.Madgwick + dsp.Pedometer) · ตัวกรองที่คาบนี้ยืมมาใช้อยู่ที่ examples/s05/06_ema_time_constant.py↗ และ examples/s05/07_median_beats_mean.py↗ ส่วนตระกูลเต็มอยู่ที่ examples/s05/08_six_filters_one_signal.py↗ · ปลายทางเรื่องการสั่นอยู่ที่ examples/s07/01_imu_vibration_monitor.py↗
เปลี่ยนจากเฟิร์มแวร์รุ่นก่อน — บน Eva Kit sensors.init() scan() push() live_push() auto() ถูกปฏิเสธด้วย OSError ทั้งห้าตัว · sensors.bmi270.motion() / .acceleration() / .gyroscope() อ่านผ่าน snapshot ของคอร์จอ ส่วน .temperature() / .chip_id() ใช้ไม่ได้ · sensors.bmm350.* ทั้งห้าคำสั่งอ่านตรงจากชิปได้ เพราะอยู่คนละบัส (I3C) · บน TESAIoT Dev Kit CM33 อ่าน BMI270 ตรงจาก I2C: ห้าตัวข้างต้นไม่ถูกปฏิเสธ (แต่คอร์สนี้ไม่เรียก) และ .temperature() / .chip_id() ใช้ได้ · snapshot() มีทั้งสองบอร์ด dict หน้าตาเดียวกัน · ทิศแกน x/y เทียบขอบฐาน QWA309 ยังไม่ได้วัด · ความเร่งเป็นช่วง +/-8g (4096 LSB/g) และ gyro เป็น +/-2000 dps (16.4 LSB/dps) · ตรวจจากซอร์ส BENTO-TESAIoT-libraries/claw/common/mpy/modsensors.c, modsensors_bmm350.c, sensor_bmm350.c และ moddsp_imu.c โดยตรง
fit-css
VIDEO-SLOT: คลิป 45-60 วินาที ถ่ายจอบอร์ดที่หน้า Sensor Dashboard — เอียงบอร์ดซ้าย-ขวาช้า ๆ ให้เห็นเส้นกราฟ accel สามเส้นแยกกันวิ่ง แล้วเขย่าเร็ว ๆ ให้เห็นว่าเส้น gyro พุ่งแรงกว่า แล้วปิดท้ายด้วยการวางบอร์ดนิ่งบนโต๊ะให้เห็นว่าเส้นยังสั่นเบา ๆ ไม่นิ่งสนิท
จากคาบนี้ไป เราไม่ได้แค่ "แสดงค่าที่เซนเซอร์ให้มา" แต่เริ่มคำนวณปริมาณใหม่ขึ้นมาเอง
โน้ตผู้สอน (รายละเอียดของข้อบกพร่องที่ยังค้าง): ลงมือดูเองที่ examples/s06/04_compass_and_magnetometer.py · ทิศถูกแปลว่าอัตราส่วนระหว่างแกนและระบบแกนถูก · ซอร์สหารค่าดิบด้วยค่าคงที่ 14.55 (แกน X,Y) และ 9.0 (แกน Z) โดยคอมเมนต์เขียนเองว่า approximate, without OTP calibration และ "สำหรับ atan2 แค่นี้พอ" — ยังไม่มีใครตัดสินว่าตัวคงที่ผิดหรือป้ายหน่วยผิด
โน้ตผู้สอน: ฝั่งจอมีตัวจับเวลาสองจังหวะ โหมดเร็ว 5 ms กับโหมดปกติ 200 ms และอยู่โหมดเร็วต่ออีก 500 ms ทุกครั้งที่ได้รับคำสั่งเขียนข้อความ/ย้าย/เปลี่ยนสี ลูปที่อัปเดตเฉพาะแถบกับไฟจึงเงียบเกินครึ่งวินาทีแล้วจอถอยไปโหมดช้า ภาพกระตุกทั้งที่โค้ดไม่ได้เปลี่ยน · ข้อความเท่าเดิมเฟิร์มแวร์ไม่วาดซ้ำ เราจ่ายแค่ค่าส่งข้ามคอร์
VIDEO-SLOT: คลิป 30-45 วินาที ถ่ายผลลัพธ์ MVP — วางบอร์ดราบบนโต๊ะ กดปุ่มตั้งศูนย์ให้เห็นตัวเลขทั้งสองแกนกระโดดไปที่ 0.0 แล้วยกขอบด้าน roll ขึ้นช้า ๆ (Eva: ขอบซ้าย · Dev Kit: ขอบที่วัดได้ใน Phase 8) ให้เห็นเฉพาะแถบ ROLL วิ่ง จากนั้นยกขอบด้าน pitch ให้เห็นเฉพาะ PITCH วิ่ง แล้วเอียงต่อจนไฟ "เกิน" ติดแทนไฟ "อยู่ในเกณฑ์" ปิดท้ายด้วยการกดปุ่มเพิ่มเกณฑ์ให้เห็นไฟกลับมาเขียวโดยที่บอร์ดไม่ได้ขยับเลย
โน้ตผู้สอน: สิบสองในยี่สิบสามข้อนี้ไม่ทำให้โปรแกรม crash — มันแค่ให้คำตอบผิดอย่างเงียบ ๆ ซึ่งอันตรายกว่า · ห้าข้อล่างสุดเป็นเรื่อง **หน่วย** ล้วน ๆ และหกข้อกลางเป็นเรื่อง **เข้าใจ widget ผิดตัว** ทั้งสามกลุ่มไม่มี error ให้เห็น
โน้ตผู้สอน: เปลี่ยนจากรุ่นก่อน (15 ส.ค.): เดิมสไลด์นี้เขียนว่า "ยังไม่มีไฟล์ตัวอย่างไหนในคลังที่คำนวณ roll/pitch" ซึ่งเป็นจริงจนถึงวันนั้น · ตอนนี้ 03_tilt_from_gravity.py ทำหน้าที่นั้นแล้ว และเปิดให้เห็น atan2 สองบรรทัดที่อยู่ข้างในด้วย — งาน Build ใน practise_codes/s06_digital_level.py ยังเป็นการเขียนเองเหมือนเดิม ไฟล์ตัวอย่างอธิบาย *ทำไม* ไม่ได้ยกคำตอบมาให้
โน้ตผู้สอน: อ่านเสริมนอกเวลา — เรื่องนี้เป็นของคาบ 7 ไม่ใช่เกณฑ์ผ่านของวันนี้ แต่เป็นปลายทางที่ IMU ตัวเดียวกันนี้ไปได้ไกลที่สุดในงานอุตสาหกรรม: examples/s07/01_imu_vibration_monitor.py วางบอร์ดบนของที่สั่นแล้วดูค่า RMS เทียบเส้นฐานที่โปรแกรมเก็บเอง · การสั่นวัดด้วยค่าเฉลี่ยไม่ได้ เพราะบวกลบหักล้างกันจนเหลือศูนย์