คาบ 6 — IMU กับมุมเอียง

เครื่องวัดระดับดิจิทัล: จากความเร่งดิบ สู่องศาที่คนอ่านรู้เรื่อง

คาถาประจำคาบ: เซนเซอร์ไม่ได้บอกมุมมาให้ เราต่างหากที่เป็นคนคิดมันขึ้นมาจากตัวเลขดิบ

ดูของจริงก่อน — เมนู Sensor Dashboard

Sensor Dashboard — กราฟ accel AZ ค้างสูง = แรงโน้มถ่วง AY ไต่ขึ้นมาแทนตอนเอียง สามอย่างที่ต้องลองก่อนเข้าเนื้อหา วางนิ่ง — มีเส้นหนึ่งค้างสูง ไม่ลงศูนย์ ยกขอบด้านหนึ่ง — เส้นนั้นลด อีกเส้นไต่ขึ้น เขย่าแล้ววาง — gyro พุ่งแล้วนิ่งทันที

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

เข็มทิศทางขวาของหน้านั้นก็มาจากเซนเซอร์อีกตัว คาบ 8 เราจะไปหยิบมันมาใช้

ทำไม · คืออะไร · ทำยังไง — แผนที่ของคาบนี้

คำถาม คำตอบของคาบนี้ อยู่ช่วงไหน
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 คือค่าดิบหนึ่งตัว แปลงเป็นหน่วยที่คนอ่านออก · คาบนี้คือค่าดิบสามตัว แปลงเป็นปริมาณที่ไม่มีเซนเซอร์ตัวไหนวัดตรง ๆ

เป้าหมายของคาบนี้ · และสิ่งที่ยกมาจากคาบ 5

  1. อธิบายได้ว่า accelerometer วัดอะไรกันแน่ และทำไมวางนิ่ง ๆ ค่ายังไม่เป็นศูนย์
  2. อ่านหกแกนพร้อมกันด้วย sensors.bmi270.motion() และบอกได้ว่าทำไมต้องอ่านทีเดียว
  3. แปลงความเร่งเป็นองศาด้วย dsp.tilt() โดยจำลำดับค่าที่มันคืนได้ถูก
  4. รู้ว่า gyro กับ accel เสียคนละแบบ และฟิลเตอร์เข้ามาช่วยตรงไหน
  5. สร้าง เครื่องวัดระดับ สองแกนที่มีปุ่ม "ตั้งศูนย์" ใช้งานได้จริง — วางราบอ่าน ~0° เอียงแล้วแถบทั้งสองแกนวิ่งถูกทิศ และมีเกณฑ์ยอมรับที่ผู้ใช้ตั้งเองได้

เกินเกณฑ์ผ่าน แต่อยู่ในคาบนี้ เพราะเป็นของกลุ่มเดียวกันและถ้าไม่พูดที่นี่ก็ไม่มีที่ไหนพูด: sensors.bmm350 (เข็มทิศ ห้าคำสั่ง) · dsp.compass() · dsp.Madgwick · dsp.Pedometer — สี่หัวข้อนี้มีสไลด์ของตัวเองและมีไฟล์ตัวอย่างให้ลอง ไม่ได้อยู่ในเกณฑ์ผ่าน M6

คาบ 5 — ค่าดิบหนึ่งตัว แปลงเป็นหน่วยที่คนอ่านออก pot.read() 0-65535 เปอร์เซ็นต์ · โวลต์ Bar + Scale + Seg7 คาบ 6 — ค่าดิบสามตัว แปลงเป็นปริมาณที่ไม่มีเซนเซอร์ตัวไหนวัดตรง ๆ ax, ay, az (m/s²) roll · pitch (องศา) Bar + Scale + Seg7 โครง UI เหมือนเดิม เปลี่ยนแค่แหล่งข้อมูล และสูตรที่อยู่ตรงกลาง dsp.EMA กลับมาอีกครั้ง

กฎเหล็ก 5 ข้อของ ui ยังบังคับใช้เหมือนเดิม — ui.poll() ทุกลูป, time.sleep_ms(200) สำหรับงานหนัก, ไม่เกิน 64 widgets, ใช้ ui.* ครั้งแรกแล้ว sensor auto-task หยุด จึงต้องอ่านเซนเซอร์เองในลูป, ลูปเร็วเกินเฟรมจะหายเงียบ ๆ

ปลายทางของคาบนี้ — เครื่องวัดระดับสองแกน

หน้าจอจริงจากการรันโค้ดเฉลยบน BENTO Emulator ที่ 800x480 เท่าจอของ Eva Kit และ Dev Kit — ไม่ใช่ภาพวาด ไม่ใช่ mock-up และไม่ใช่ภาพถ่ายจากบอร์ด · ภาพนี้ยังเป็นหน้าจอของเฉลยรุ่นก่อนหน้า รอถ่ายใหม่ให้ตรงกับโค้ดปัจจุบันที่อธิบายไว้ข้างล่าง

หน้าจอของเฉลยปัจจุบันแบ่งเป็นหกการ์ด เรียงจากซ้ายบนลงล่าง

  • สองการ์ดซ้าย คือสองแกน ROLL ซ้าย-ขวา และ PITCH หน้า-หลัง เรียงตามลำดับที่ dsp.tilt() คืนค่ามาพอดี แต่ละแกนเป็น ui.Bar วางทับ ui.Scale ที่มีขีดและตัวเลข -90 ถึง 90 พร้อม ui.Seg7 อ่านค่าละเอียดอยู่ข้าง ๆ
  • ขวาบน เกณฑ์ยอมรับหน่วยองศา เป็น ui.Spinbox ที่ตั้งเองได้ด้วยปุ่มเพิ่ม/ลด เพราะงานวางกล้องกับงานวางตู้เย็นยอมได้ไม่เท่ากัน
  • ขวากลาง ui.Led สองดวง บอกว่าตอนนี้อยู่ในเกณฑ์หรือเกิน
  • ล่างซ้าย ปุ่ม "ตั้งศูนย์" กับ "ออกจากโปรแกรม" และบรรทัดอ้างอิง R +0.0 P +0.0 ที่เก็บค่าศูนย์ล่าสุดไว้
  • ล่างขวา คุณภาพของค่า พร้อมไฟ "ค่าค้าง" ที่ติดเมื่อรอบนั้นอ่านเซนเซอร์ไม่ได้

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

accelerometer วัดอะไร — คำตอบที่คนส่วนใหญ่ตอบผิด

คำตอบยอดนิยมคือ "วัดความเร่ง" ซึ่งถูกครึ่งเดียว วางบอร์ดนิ่งสนิทบนโต๊ะ มันไม่ได้เร่งไปไหนเลย แต่ค่าที่อ่านได้คือ

ax≈0ay≈0az≈+9.81 m/s2a_x \approx 0 \qquad a_y \approx 0 \qquad a_z \approx +9.81\ \mathrm{m/s^2}

อ่านเป็นภาษาคน: เซนเซอร์ไม่ได้วัดว่าเราเคลื่อนที่เร็วขึ้นแค่ไหน มันวัด แรงที่ตัวเรือนดันมวลเล็ก ๆ ข้างในไว้ไม่ให้ตกอิสระ — โต๊ะดันบอร์ดขึ้น บอร์ดดันมวลขึ้น จึงอ่านได้ +1 g ในทิศขึ้น ปริมาณนี้มีชื่อว่า proper acceleration

ตัวเลขจากบอร์ดจริง: ปล่อยบอร์ดตกอิสระ (อย่าทำ) ทั้งสามแกนจะอ่านได้ ~0 เพราะไม่มีใครดันมวลนั้นแล้ว

ภาพ: Gregors, Wikimedia Commons, CC BY-SA 3.0 — ข้อตกลงแกนขวามือที่ใช้ทั้งคาบ: x ไปทางขวาของบอร์ด, y ออกไปด้านบน, z ตั้งฉากออกจากหน้าบอร์ด · ทิศ x/y เทียบขอบบอร์ดจริง ต้องยืนยันบนโต๊ะก่อนขึ้นสอน ทั้ง Eva Kit และ Dev Kit — ยังไม่มีบันทึกการวัดของบอร์ดไหน (กล่องเตือนในสไลด์สูตร)
Veritasium · 17:33 · EN — ดูเพื่อเข้าใจว่า "ตัวที่วางนิ่งต่างหากที่กำลังถูกเร่ง" · แนะนำเริ่มที่ 2:30 หรือให้เป็นการบ้าน

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

ข้างในชิปมีของที่ขยับได้จริง — มวลพิสูจน์บนสปริง

ภาพ: pbroks13, Wikimedia Commons, สาธารณสมบัติ

ภาพ: Tosaka, Wikimedia Commons, CC BY 3.0

ภาพ: Guillermo Bossio, Wikimedia Commons, CC BY-SA 4.0

ภาพถ่าย SEM ของโครงสร้างจริง — Zhang H. et al., Micromachines 10(6):380 (2019), CC BY 4.0

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

แล้ววงจรอ่าน "ระยะที่มวลเลื่อน" ได้ยังไง — มวลมีซี่หวียื่นออกมา สอดสลับกับซี่หวีที่ยึดกับตัวเรือน แต่ละคู่ซี่คือ ตัวเก็บประจุ หนึ่งตัว

  • มวลอยู่ตรงกลาง → d1=d2d_1 = d_2 → C1=C2C_1 = C_2
  • มวลเลื่อนไปซ้าย → d1<d2d_1 < d_2 → C1>C2C_1 > C_2

วงจรวัด ผลต่างของความจุ ส่งเข้า ADC ในชิป ออกมาเป็นตัวเลขบน I2C

Bosch Sensortec · 1:01 · แทบไม่มีคำพูด — ดูเพื่อเชื่อว่า "ในชิปมีของที่ขยับได้จริง" · Bosch คือผู้ผลิต BMI270 บนบอร์ดเรา

ภาพ: Jejmule, Wikimedia Commons, CC BY-SA 4.0 — ซี่หวีที่ย่อหน้าบนพูดถึง ตอนมันขยับจริง ซี่ชุดหนึ่งยึดกับตัวเรือน อีกชุดยื่นออกมาจากมวลที่เลื่อนได้ ระยะระหว่างซี่ที่เปลี่ยนไปคือความจุที่วงจรอ่าน

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

เข้าใจฮาร์ดแวร์ · แรงโน้มถ่วงทอดเงาลงบนแกนที่เอียง

บอร์ดเอียงไปเรื่อย ๆ · เวกเตอร์ g ชี้ลงเสมอ g = 9.81 ชี้ลงเสมอ เงาที่ตกบนแกนของบอร์ด az ay 0° → az 9.81 · ay 0.00 30° → az 8.50 · ay 4.91 60° → az 4.91 · ay 8.50 90° → az 0.00 · ay 9.81 √(ay² + az²) = 9.81 ทุกมุม รู้เงาสองอัน จึงเดามุมกลับได้
แผนภาพวาดเอง (บันทึกการค้น: ค้น Commons หมวด Accelerometers / Inclined planes ด้วยคำ "accelerometer gravity projection axes animation", "tilt sensing gravity vector components" — พบแต่ภาพนิ่งของระนาบเอียงเชิงกลศาสตร์ ไม่มีภาพที่ผูกกับแกนของเซนเซอร์ จึงวาดเอง · ภาพนิ่งที่ใกล้ที่สุดอยู่สไลด์ถัดไป)

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

เรียกมุมพวกนี้ว่าอะไร — roll กับ pitch มาจากวงการการบิน

ภาพ: NASA Glenn Research Center, สาธารณสมบัติ — **roll** หมุนรอบแกนยาวของลำตัว

ภาพ: NASA Glenn Research Center, สาธารณสมบัติ — **pitch** ก้ม-เงย
ชื่อ คือการเอียงแบบไหน ทดสอบด้วยการ มาจากแกน
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()

ตรวจสุขภาพเซนเซอร์ก่อนเชื่อค่าอะไรทั้งนั้น

∥a⃗∥=ax2+ay2+az2\lVert \vec{a} \rVert = \sqrt{a_x^{2} + a_y^{2} + a_z^{2}}

ไทย: ไม่ว่าเอียงบอร์ดยังไง ถ้าวางนิ่งค่านี้ต้องได้ราว 9.81 m/s² เสมอ · ตัวเลขจาก Eva Kit (บอร์ดที่ใช้เขียนสไลด์): ax 0.02, ay −0.05, az 9.79 → ∥a⃗∥=9.79\lVert \vec{a} \rVert = 9.79 ผ่าน

สูตรที่แปลงเงาเป็นองศา

ϕroll=atan2⁡(ay,  az)θpitch=atan2⁡(−ax,  ay2+az2)ϕdeg⁡=ϕ⋅180π\phi_{\text{roll}} = \operatorname{atan2}\bigl(a_y,\; a_z\bigr) \qquad \theta_{\text{pitch}} = \operatorname{atan2}\Bigl(-a_x,\; \sqrt{a_y^{2} + a_z^{2}}\Bigr) \qquad \phi_{\deg} = \phi \cdot \frac{180}{\pi}

ไทย: เวกเตอร์แรงโน้มถ่วงชี้ลงคงที่เสมอ พอบอร์ดเอียง เงาของมันบนแกนก็เปลี่ยน · ตัวเลขจาก Eva Kit ตอนยกขอบซ้าย: ay 4.90, az 8.49 → atan2⁡(4.90,8.49)=0.524 rad=30.0∘\operatorname{atan2}(4.90, 8.49) = 0.524\ \mathrm{rad} = 30.0^\circ

ทำไมต้อง atan2 ไม่ใช่ atan — atan2 รับตัวตั้งกับตัวหารแยกกัน จึงแยกควอดรันต์ได้ครบสี่ และไม่ระเบิดเมื่อตัวหารเป็นศูนย์ตอนบอร์ดตั้งฉาก 90°

ต้องยืนยันข้อตกลงแกนกับบอร์ดจริงก่อนขึ้นสอน — แยกบอร์ด — Eva Kit: เอียงบอร์ดไปทางขวาแล้วดูว่าแกนไหนเปลี่ยนเครื่องหมาย ถ้าไม่ตรง ให้สลับเครื่องหมายในสไลด์ ไม่ใช่ไปแก้ที่ผู้เรียน (ที่มา: _BUILD/MEDIA_PLAN.md §7.3) · TESAIoT Dev Kit: ยังไม่มีใครวัดว่าขอบไหนของฐาน QWA309 ทำให้ roll เพิ่ม IMU ตัวเดียวกันแต่ประกอบบนบอร์ดคนละแบบ ห้ามยกคำว่า "ขอบซ้าย" ของ Eva ไปบอกทีม Dev Kit — ให้ทีมยกทีละขอบแล้วจดลงใบงาน 4.1 ว่าขอบไหนขยับ roll

ภาพ: Klaus-Dieter Keller, Wikimedia Commons, CC0 — แยกเวกเตอร์ g เป็นองค์ประกอบตามมุมของระนาบเอียง

ภาพ: VT DLA, Wikimedia Commons, CC BY-SA 3.0 — ตรีโกณของมุมเดียวกันที่ `atan2` ใช้

รู้สูตรไว้ไม่ใช่เพื่อพิมพ์เอง แต่เพื่อ เดาถูกว่าจะพังตรงไหน เมื่อค่าออกมาแปลก

กับดักที่ตั้งใจดักทั้งคาบ — ลำดับค่าที่คืนกลับมา

ถูก · roll, pitch = dsp.tilt(...) เอียงซ้าย-ขวา ROLL วิ่ง PITCH นิ่ง ผิด · pitch, roll = dsp.tilt(...) เอียงซ้าย-ขวา ROLL นิ่ง PITCH วิ่ง

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 บอก อัตราการเปลี่ยนมุม ไม่ใช่ตัวมุมเอง

อ่านแยกสองครั้ง accel gyro ช่องว่าง — ค่าเปลี่ยนชุดได้ บอร์ดขยับไปแล้ว motion() — อ่านครั้งเดียว accel + gyro พร้อมกัน ได้ภาพนิ่งของบอร์ด ณ เวลาเดียวกัน

ข้อมูลที่จะเอาไปรวมกัน (fuse) ต้องมาจากเวลาเดียวกัน ไม่งั้นเราไม่ได้รวมข้อมูล เรากำลังรวมความมั่ว

gyro ทำงานยังไง — แรง Coriolis กับส้อมเสียงจิ๋ว

ภาพ: Jacopo Bertolotti, Wikimedia Commons, CC0

ภาพ: Minh Ngoc Nguyen et al., Wikimedia Commons, CC BY 4.0

โครงสร้างในชิปถูกขับให้ สั่นในแนวหนึ่ง ตลอดเวลา ถ้าชิปไม่หมุน มันก็สั่นอยู่แนวเดิม

พอชิปเริ่มหมุน แรง Coriolis ผลักมวลที่กำลังสั่นให้เบนออกไปใน แนวตั้งฉาก — วงจรวัดการเบนนั้นแล้วแปลงเป็น deg/s

ภาพซ้ายคือ Coriolis ในกรอบหมุน (มุมมองนิ่ง เทียบกับ มุมมองหมุน) ภาพกลางคือโครงสร้าง tuning fork ที่ใช้จริง

nanolearning · 13:44 · EN — ดูเพื่อเห็นโหมดขับกับโหมดรับที่ตั้งฉากกัน · แนะนำเริ่มที่ 4:00

gyro ไม่ได้ "รู้ว่าอยู่ที่กี่องศา" มันรู้แค่ว่า ตอนนี้กำลังหมุนเร็วแค่ไหน ที่เหลือเราต้องบวกเอาเอง

ทำไม accel ถึงสั่น และทำไม gyro ถึงหนี

accel — ถูกในระยะยาว แต่กระตุกในระยะสั้น
อ้างอิงแรงโน้มถ่วงซึ่งไม่มีวันเปลี่ยน วางทิ้งสามชั่วโมงค่าก็ยังถูก แต่มันวัด "แรงทั้งหมด" ไม่ได้แยกว่าอันไหนคือแรงโน้มถ่วง อันไหนคือมือที่เขย่า พัดลม เคาะโต๊ะ หรือ noise ทางไฟฟ้า

gyro — นิ่งและไวในระยะสั้น แต่หนีในระยะยาว
วัดอัตราการหมุนโดยตรง แรงสั่นภายนอกรบกวนได้น้อย ตอบสนองทันที แต่การจะได้ มุม ต้องเอาอัตรามาบวกสะสม

θgyro=∑kωk Δt\theta_{\text{gyro}} = \sum_{k} \omega_k\,\Delta t

ตัวเลขจาก Eva Kit (บอร์ดที่ใช้เขียนสไลด์): วางนิ่งแล้ว gz ไม่เคยเป็น 0.00 เป๊ะ มันเป็น 0.03-0.05 deg/s สมมติ bias = 0.05 deg/s

ผิดหลัง 60 s=0.05×60=3.0∘ผิดหลัง 600 s=30∘\text{ผิดหลัง 60 s} = 0.05 \times 60 = 3.0^\circ \qquad \text{ผิดหลัง 600 s} = 30^\circ

ไทย: ความผิดพลาดจิ๋วที่ถูกบวกสะสมทุกรอบ ไม่มีอะไรดึงกลับ ปรากฏการณ์นี้ชื่อ drift

ภาพ: Rudyk A.V. et al., Sensors 20(17):4841 (2020), CC BY 4.0 — ข้อมูล gyro จริง อินทิเกรตแล้วมุมไหลออกใน 600 วินาที

จุดอ่อนของสองตัวนี้อยู่คนละย่านความถี่กันพอดี — และนั่นคือช่องว่างที่วิศวกรใช้ประโยชน์

ฟิลเตอร์ complementary — เอาจุดแข็งของทั้งคู่มาต่อกัน

θt=a(θt−1+ωt Δt)+(1−a) θaccel\theta_t = a\bigl(\theta_{t-1} + \omega_t\,\Delta t\bigr) + (1-a)\,\theta_{\text{accel}}

ไทย: เชื่อ gyro ในช่วงสั้น ๆ เพราะมันนิ่งไม่สั่น แล้วให้ accel คอยดึงกลับในระยะยาวเพราะ gyro ไหลไปเรื่อย ๆ

  • ส่วน a(θ+ωΔt)a(\theta + \omega \Delta t) ทำหน้าที่ high-pass ต่อ gyro — ปล่อยการเปลี่ยนแปลงเร็วผ่าน ตัด drift ช้าทิ้ง
  • ส่วน (1−a)θaccel(1-a)\theta_{\text{accel}} ทำหน้าที่ low-pass ต่อ accel — เก็บทิศระยะยาว ตัดการกระตุกทิ้ง

สองครึ่งนี้บวกกันได้พอดีหนึ่งเสมอ จึงเรียกว่า complementary

ความถี่ตัดของการผสม

τ=a Δt1−a\tau = \frac{a\,\Delta t}{1-a}

ไทย: τ คือเส้นแบ่ง — เร็วกว่านี้ฟังจาก gyro ช้ากว่านี้ฟังจาก accel
ตัวเลขของลูปเรา: a=0.98, Δt=0.2a = 0.98,\ \Delta t = 0.2 s ⇒τ=0.98×0.20.02=9.8\Rightarrow \tau = \dfrac{0.98 \times 0.2}{0.02} = 9.8 s

ภาพ: Liu et al., Micromachines 12(11):1373 (2021), CC BY 4.0

ภาพ: Narkhede P. et al., Sensors 21(6):1937 (2021), CC BY 4.0 — ตอบว่าทำไม LPF + HPF รวมกันได้ 1 พอดี

นี่คือ sensor fusion ฉบับย่อที่สุด: เซนเซอร์สองตัวที่ต่างก็ไม่สมบูรณ์ รวมกันแล้วดีกว่าตัวใดตัวหนึ่ง

วันนี้เราใช้ครึ่งเดียว — และ alpha สองตัวนี้อ่านคนละทางกัน

บน MicroPython คาบนี้เราใช้ dsp.tilt() ซึ่งคำนวณจาก accel อย่างเดียว แล้วต่อท้ายด้วย dsp.EMA เพื่อไล่การกระตุกออก พูดให้ตรงคือเราทำแค่ ครึ่ง low-pass ไม่ได้ทำครึ่ง gyro

yt=αxt+(1−α) yt−1τ≈Tsαy_t = \alpha x_t + (1-\alpha)\,y_{t-1} \qquad \tau \approx \frac{T_s}{\alpha}

ไทย: ค่าใหม่มีน้ำหนัก α ที่เหลือคือความจำของค่าเก่า · ตัวเลขของลูปเรา: Ts=0.2T_s = 0.2 s, α=0.2⇒τ≈1.0\alpha = 0.2 \Rightarrow \tau \approx 1.0 s

สมการ alpha สูง แปลว่า
dsp.EMA(alpha=) y=αx+(1−α)yprevy = \alpha x + (1-\alpha)y_{prev} เชื่อ ค่าใหม่ มาก → ไวขึ้น สั่นขึ้น
complementary θ=a(θ+ωΔt)+(1−a)θacc\theta = a(\theta + \omega\Delta t) + (1-a)\theta_{acc} เชื่อ ค่าเดิม + gyro มาก → นิ่งขึ้น

ได้อะไร เส้นนิ่ง ไม่มี drift สะสมเลยแม้เปิดทิ้งทั้งวัน เพราะไม่ได้อินทิเกรตอะไร · เสียอะไร มี lag ราวหนึ่งวินาที
ถ้าอยากได้ fusion เต็มรูป เฟิร์มแวร์มี dsp.Madgwick ให้ต่อยอด — สไลด์ถัดไปว่าด้วยเรื่องนั้นโดยตรง

ภาพ: Д.Ильин, Wikimedia Commons, CC0 1.0 — น้ำหนักของ EMA ลดลงแบบเอกซ์โพเนนเชียลย้อนหลัง

อย่าเชื่อชื่อพารามิเตอร์ ให้เปิดดูสมการ — 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

ภาพ: Yian chen / Wikimedia Commons — CC BY-SA 3.0 — ความเร่งของคนเดินจริง ยอดที่โผล่ซ้ำคือก้าว · threshold คือเส้นแนวนอนที่ตั้งให้มันตัด

ลงมือ: 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 ถูกพร้อมกันไม่ได้ อย่าสอนตัวเลขขนาดเป็นข้อเท็จจริง

ทั้งตระกูล IMU มีอยู่เท่านี้ — สิบสี่ชื่อ ไม่มีมากกว่านี้

สามสไลด์ที่ผ่านมาเปิดทีละกลุ่ม สไลด์นี้วางทั้งตระกูลไว้ข้างกัน — สองโมดูลย่อยของ 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 เป๊ะ สาเหตุมีสามชั้นซ้อนกัน

  1. โต๊ะไม่ราบจริง พื้นห้องเรียนแทบทุกที่เอียงเล็กน้อย
  2. ชิปไม่ได้ติดตั้งตรงเป๊ะบน PCB คลาดเคลื่อนเศษหนึ่งส่วนสิบองศาเป็นเรื่องปกติของงานประกอบ
  3. ตัวชิปมี zero-g offset ติดมาจากโรงงาน เป็นสเปกที่ผู้ผลิตประกาศไว้ ไม่ใช่ของเสีย

ϕแสดงผล=ϕ−ϕ0θแสดงผล=θ−θ0\phi_{\text{แสดงผล}} = \phi - \phi_{0} \qquad \theta_{\text{แสดงผล}} = \theta - \theta_{0}

ไทย: จำมุมตอนกดปุ่ม "ตั้งศูนย์" ไว้ แล้วลบออกทุกครั้ง
ตัวเลขจาก Eva Kit (บอร์ดที่ใช้เขียนสไลด์): กดตั้งศูนย์ตอน ϕ=1.8∘\phi = 1.8^\circ → ต่อจากนั้น 1.8° กลายเป็น 0.0° และ 4.3° กลายเป็น 2.5°

roll_zero = roll_f          # จำท่าปัจจุบันไว้เป็นจุดอ้างอิง
roll_show = roll_f - roll_zero
กดตั้งศูนย์แล้วเกิดอะไร 0° ค่าดิบค้างที่ 1.8° กดปุ่ม จากนี้อ่าน 0.0° เครื่องชั่งในครัวใช้ปุ่มเดียวกันนี้

เครื่องมือวัดที่ดีไม่ได้แปลว่าค่าดิบแม่นเป๊ะ แต่แปลว่า ผู้ใช้กำหนดจุดอ้างอิงเองได้

เกร็ด: จากฟองอากาศในหลอดแก้ว สู่ก้อนซิลิคอนจิ๋ว

ราวปี 1661 Melchisédech Thévenot ทำเครื่องวัดระดับแบบหลอดแก้วใส่ของเหลวที่มีฟองอากาศ — ฟองเบากว่าของเหลว มันจึงลอยไปอยู่จุดสูงสุดของหลอดเสมอ ช่างไม้ทั่วโลกใช้มันมาสามร้อยกว่าปีโดยไม่ต้องมีแบตเตอรี่

ปี 1991 Analog Devices ออก ADXL50 ซึ่งเป็น accelerometer แบบ MEMS ตัวแรกที่ผลิตขายจำนวนมาก เป้าหมายแรกคือถุงลมนิรภัย — ต้องแยก "ชนจริง" ออกจาก "ตกหลุมถนน" ให้ได้ในไม่กี่มิลลิวินาที

ภาพขวาบนคือ IMU ทั้งชุดวางเทียบกับเหรียญเพนนี ภาพขวาล่างคือภาพถ่ายไดจริงของ IMU ที่เปิดฝาออก

CircuitBread · 6:10 · EN — ดูเพื่อเห็นโซ่เต็มเส้น: proof mass → ความจุต่าง → เลขดิจิทัล

เชื่อมกับวันนี้: โปรแกรมที่เราจะเขียนอีกครู่คือหลอดแก้วของ Thévenot ที่ถูกเขียนใหม่ด้วย Python สิบกว่าบรรทัด และมันทำสิ่งที่หลอดแก้วทำไม่ได้ — บอกเป็นตัวเลของศา จำค่าอ้างอิงได้ และส่งค่าขึ้นเครือข่ายได้ในคาบที่ 10

ภาพ: University of Michigan / DARPA, สาธารณสมบัติ

ภาพ: ZeptoBars, Wikimedia Commons, CC BY 3.0

เรื่องที่เราให้ 70% น้อง ๆ เขียน 30%

70% — เฟิร์มแวร์ทำให้แล้ว 30% — งานของเรา I2C · หน่วย m/s² · atan2 · dsp.EMA · วาด Bar/Scale/Seg7 เลือก alpha · นิยาม "ศูนย์" จัดหน้าจอให้อ่านได้ใน 2 วินาที

สิ่งที่เฟิร์มแวร์ทำให้แล้ว (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 ไม่เป็น จะได้เครื่องมือที่ใช้งานจริงไม่ได้

แกะโค้ดจริง — ท่าที่ 1 เช็กว่าเซนเซอร์ตอบแล้ว

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("อ่านเซนเซอร์รอบแรกยังไม่ได้ - ลองใหม่ในลูป")
ไทม์ไลน์ตอนเริ่มโปรแกรม ui.screen() ล้างจอ motion() ใน try รอเซนเซอร์ อ่านได้ เรียกทันทีหลังรีเซ็ต = ต้องรอ (Eva วัดได้ถึง 16 วินาที)

บน 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 ใช้ได้) — โค้ดของคอร์สจึงไม่พึ่งสองคำสั่งนี้

แกะโค้ดจริง — ท่าที่ 2 สองแกน สองแถบ และไม้บรรทัดที่บอกพิสัยของมันเอง

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 รับสตริง จึงใส่ + − และทศนิยมได้ · ไฟสองดวงแทนตัวหนังสือสี ถ่ายจอเป็นขาวดำแล้วยังแยกออกว่าดวงไหนติด

เครื่องวัดระดับสองแกน ROLL เอียงซ้าย-ขวา -90 0 90 +12.4 PITCH เอียงหน้า-หลัง -03.1 เกณฑ์ (องศา) 05 + สถานะ ตั้งศูนย์ ออกจากโปรแกรม min=-90 max=90 เท่านั้นที่ทำให้แถบขยับ และไม้บรรทัดคือตัวที่ทำให้เลขมีความหมาย

ตั้งช่วง min/max ให้ตรงกับปริมาณจริงเสมอ ไม่งั้นเราจะไปไล่หาบั๊กผิดที่ทั้งวัน — และค่าที่วัดได้ห้ามอยู่ลำพัง ต้องมีพิสัยหรือเกณฑ์อยู่ข้าง ๆ

แกะโค้ดจริง — ท่าที่ 2 (ต่อ) เกณฑ์ที่ผู้ใช้ตั้งเอง และปุ่มสั่งงาน

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)

เกณฑ์ที่ฝังในโค้ดคือเกณฑ์ของคนเขียนโปรแกรมเมื่อสามเดือนก่อน — เกณฑ์ที่ตั้งได้บนจอคือเกณฑ์ของคนหน้างานวันนี้

หน้าปัดแบบมีเข็มจริง — Scale วงกลมเพิ่งได้เข็มของตัวเอง

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 ขับเข็มสด

หน้าจอที่ตัวอย่างสร้างบน Emulator (โปรไฟล์ Eva) — เข็มส้มชี้ 50 ขณะบอร์ดวางราบ (pitch 0.1°) · รันบนบอร์ดจริงแล้วภาพเดียวกัน ตรวจ 2026-08-20

แกะโค้ดจริง — ท่าที่ 3 อ่านหกแกนแล้วแปลงเป็นมุม

# --- ท่าที่ 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 แล้วข้ามรอบนั้นไป

หกค่าที่ได้มา ax ay az | gx gy gz สามตัวแรก dsp.tilt() (roll, pitch) gyro แสดงเฉย ๆ roll มาก่อน pitch

การอ่านเซนเซอร์ล้มเหลวเป็นเรื่อง ปกติ ที่ต้องออกแบบรองรับ ไม่ใช่เรื่องผิดปกติที่รอให้เกิดแล้วค่อยแก้

แกะโค้ดจริง — ท่าที่ 4 กรองและหักค่าอ้างอิง

# --- ท่าที่ 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 (ค่าที่กรองแล้ว) ไม่ใช่ค่าดิบ — ถ้าเก็บค่าดิบที่กำลังสั่นอยู่พอดี จุดอ้างอิงจะเพี้ยนตามความบังเอิญของเสี้ยววินาทีนั้น

ค่าดิบ เทียบ ค่าที่ผ่าน EMA ดิบ EMA α=0.2 นิ่งขึ้น แลกกับตามช้าลงราว 1 วินาที ตั้งศูนย์จากเส้นน้ำเงินเสมอ

ตั้งศูนย์จาก ค่าที่กรองแล้ว เสมอ — จุดอ้างอิงที่สั่น แย่กว่าไม่มีจุดอ้างอิง

แกะโค้ดจริง — ท่าที่ 5 ปุ่มตั้งศูนย์ผ่าน 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)
ลูปที่เรียก poll ทุกรอบ heartbeat ถึง CM55 ทุก 200 ms widget ครบทั้งหน้า ลูปที่ลืม poll ~2 วินาที จอซ่อน widget ทั้งหน้า

ui.poll() คืน list ของ dict แต่ละใบมีคีย์ handle, type, value (สูงสุด 8 event ต่อครั้ง)

ต้องเรียกทุกรอบลูป แม้รู้อยู่แล้วว่าไม่มีใครกด เพราะฝั่ง CM55 ใช้การ poll เป็นสัญญาณว่าโปรแกรมยังมีชีวิต เงียบไปราวสองวินาที widget จะถูกซ่อนทั้งหน้า

ปุ่มเพิ่มกับปุ่มลดเป็นคนละปุ่มกันโดยตั้งใจ ปุ่มเดียวที่สลับสองทิศบอกไม่ได้ว่าตอนนี้กดแล้วจะขึ้นหรือจะลง คนกดจึงต้องเดา และเดาผิดได้เสมอ กฎเดียวกันนี้ใช้กับปุ่มเปิด-ปิดอุปกรณ์จริงทุกชนิด · sp_tol.value(tol) คือการเขียนค่ากลับลงช่อง ไม่ใช่การอ่าน — ตัวแปร tol ฝั่ง Python คือความจริง ช่องบนจอคือการแสดงผลของมัน

ui.poll() ไม่ใช่แค่ "อ่านปุ่ม" มันคือ ชีพจรที่เราส่งให้อีกคอร์รู้ว่าเรายังอยู่

แกะโค้ดจริง — ท่าที่ 6 อัปเดตหน้าปัดและคุมจังหวะ

# --- ท่าที่ 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)
clamp90 กันแถบค้างเงียบ ๆ ช่วงของแถบ: -90 ถึง +90 -90 0 +90 คว่ำบอร์ด → roll ถึง 180° แถบค้างปลายสุด ไม่มี error
บรรทัด คุณภาพของค่า (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 ใหม่ด้วย

ข้อมูลไหลไปทางไหน — จากแรงโน้มถ่วงถึงแถบบนจอ

ห้าขั้นตอน ในหนึ่งรอบลูป ทุก 200 มิลลิวินาที แรงโน้มถ่วง มวลจิ๋วใน MEMS ถูกดันเอียงไป motion() 6 แกน · อ่านครั้งเดียว m/s² และ deg/s dsp.tilt() atan2 สองสูตร คืน (roll, pitch) EMA + tare กรองความสั่น ลบจุดอ้างอิง Bar + Seg7 + Led IPC ไป CM55 คนอ่านออก แถบไม่ขยับ ให้ไล่หาว่าขาดที่ขั้นไหน — พิมพ์ค่าออกคอนโซลทีละจุดจนเจอจุดที่ค่าหยุดเปลี่ยน

ทุกกล่องในภาพนี้แทนได้ด้วยหนึ่งบรรทัด Python ยกเว้นกล่องแรกกล่องเดียว

วิธีรันบนบอร์ด

1 · Playground ค้างหน้านี้ไว้ 2 · เติม pass 6 จุด ทีละจุด 3 · Program แล้วมองจอบอร์ด 4 · วางราบ แล้วกดตั้งศูนย์ 5 · เอียง ช้า ๆ อย่าถือบอร์ดลอยกลางอากาศแล้วโบก — ความเร่งจากการเหวี่ยงจะปนเข้ากับแรงโน้มถ่วง
  1. บนจอบอร์ด แตะการ์ด BENTO Playground ค้างหน้านี้ไว้ตลอดคาบ
  2. เปิดไฟล์ practise_codes/s06_digital_level.py↗ ใน BENTO IDE แล้วเติมช่องว่าง pass ให้ครบ 6 จุด
  3. กด Program to Device แล้วหันไปมองจอบอร์ด
  4. วางบอร์ดราบบนโต๊ะ ให้นิ่งสนิท แล้วแตะปุ่ม ตั้งศูนย์ หนึ่งครั้ง
  5. เอียงบอร์ดซ้าย-ขวาช้า ๆ ดูวง ROLL จากนั้นเอียงหน้า-หลัง ดูวง PITCH
  6. กดปุ่ม จบโปรแกรม เพื่อออกอย่างเรียบร้อยก่อนส่งโค้ดรอบใหม่

เอียงช้าคือการทดสอบที่ถูกต้อง เอียงเร็วคือการทดสอบ ข้อจำกัด ของฟิลเตอร์ ทำทั้งสองอย่างแต่ให้รู้ว่ากำลังทำอะไร

MVP checkpoint — ผ่านคาบนี้เมื่อ

เครื่องวัดระดับ — วางราบอ่าน ~0°, เอียงแล้วแถบทั้งสองแกนวิ่งถูกทิศ และไฟบอกได้ว่าเกินเกณฑ์หรือยัง

แปลเป็นสิ่งที่ตรวจได้จริง:

  • [ ] วางบอร์ดราบบนโต๊ะ กดตั้งศูนย์ แล้วทั้งสองแกนอ่านได้ในช่วง −1.0° ถึง +1.0°
  • [ ] ยกขอบด้านที่ทำให้ roll เพิ่มขึ้น (Eva: ขอบซ้าย · Dev Kit: ทีมหาเองแล้วจดว่าขอบไหน) แถบ ROLL ขยับ และเครื่องหมายไม่กลับด้าน
  • [ ] ยกขอบด้านที่ตั้งฉากกันขึ้น (Eva: ขอบบน) แถบ PITCH ขยับ ส่วน ROLL แทบไม่ขยับ
  • [ ] ตัวเลข Seg7 เปลี่ยนตามแถบ อ่านออกจากระยะห่างหนึ่งช่วงแขน และเปลี่ยน วินาทีละครั้ง ไม่ใช่ห้าครั้งต่อวินาที
  • [ ] เอียงเกินเกณฑ์แล้วไฟสลับกันติด ทีละดวงเท่านั้น ไม่ติดพร้อมกันสองดวง
  • [ ] กดปุ่มเพิ่ม/ลดเกณฑ์ แล้วเลขในช่องเปลี่ยนตาม หยุดเองที่ขอบพิสัย 1 กับ 30 และไฟเปลี่ยนตามเกณฑ์ใหม่โดยบอร์ดไม่ต้องขยับ
  • [ ] กดปุ่มตั้งศูนย์ในท่าเอียง แล้วค่ากลับไปเป็น ~0° ในท่านั้น
  • [ ] วางบอร์ดนิ่ง 30 วินาที ตัวเลขไม่ไหลหนีไปทางเดียวเรื่อย ๆ
  • [ ] ถ่ายรูปหน้าจอตอนวางราบ และตอนเอียงจนไฟแดงติด แนบใน worksheet

ข้อที่แปดคือข้อที่พิสูจน์ว่าเรา ไม่ได้ มีปัญหา drift เพราะเราไม่ได้อินทิเกรตอะไรเลย

กับดักที่เจอบ่อย (1/2) — หน้าจอและตัวกรอง

อาการ สาเหตุที่แท้จริง วิธีแก้
เอียงซ้าย-ขวา แต่แถบ 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() ทุกรอบเสมอ

กับดักที่เจอบ่อย (2/2) — เซนเซอร์ หน่วย และเข็มทิศ

อาการ สาเหตุที่แท้จริง วิธีแก้
ค่าเพี้ยนหนักตอนถือเดินไปมา ความเร่งจากการเคลื่อนที่ปนกับแรงโน้มถ่วง วางนิ่งแล้วเอียงช้า ๆ หรือรอ 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 ชิ้นเขียนไว้ให้ครบแล้ว ไม่ต้องแตะ ทั้งไม้บรรทัด ช่องเกณฑ์ ปุ่มเพิ่ม/ลด ไฟสองดวง และบรรทัดคุณภาพของค่า งานของเราคือทำให้ค่าไหลเข้าไปในนั้น

เติมเป็นสามขั้น แล้วรันทุกขั้น ขั้น 1 · จุดที่ 1-3 ตัวเลขวิ่งไหม (กระตุกได้) ขั้น 2 · จุดที่ 4 เทียบก่อน-หลังกรอง ขั้น 3 · จุดที่ 5-6 ทดสอบปุ่มตั้งศูนย์

เติมทีละจุดแล้วรัน จะรู้ทันทีว่าพังที่จุดไหน — เติมครบหกจุดแล้วรันทีเดียวคือการเดา

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

ต้องทำในคาบ · เปิดตามลำดับนี้ ทั้งชุดราว 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)
สอง EMA ไม่ใช่หนึ่ง ถูก roll → ema_roll pitch → ema_pitch ผิด roll และ pitch → ema เดียว ค่าปนกัน แถบวิ่งมั่วทั้งคู่
เฉลยรุ่นนี้เลิกใช้ สีบอกแกน (แดง = roll, เขียว = pitch) แล้วใช้ คำบอกแกน แทน — สีแดงกับเขียวในหน้าจอควบคุมจองไว้แล้วสำหรับ เสีย/ปกติ เอาไปบอกแกน วันที่ต้องเตือนจริงจะไม่มีใครแยกออก · แถบใช้สีเน้น 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)
ลำดับหกท่า ถูกบังคับด้วยตรรกะ 1 เช็กคอร์จอตอบ — ฐานของทุกอย่าง 2 สร้าง UI ครั้งเดียว นอกลูป 3 อ่านและแปลง — ตัวเลขมีความหมาย 4 กรอง — หลังพิสูจน์ว่าค่าดิบถูก 5 ตั้งศูนย์ — ต้องเก็บค่าที่กรองแล้ว 6 แสดงผล — ปลายทาง ไม่ใช่ต้นทาง
การแสดงผลทั้งหมดอยู่นอก if ok_read — รอบที่อ่านไม่ได้ก็ยังต้องวาดจอด้วยค่าเดิม แล้วเขียนบรรทัดคุณภาพ lbl_health ว่า "ค่าค้าง" ด้วยสีเตือน ถ้าปล่อยให้การวาดอยู่ใน if จอจะแช่ภาพเดิมโดยไม่มีอะไรบอกคนดู · in_tol ใช้ค่าที่ กรองแล้วและหักศูนย์แล้ว ไม่ใช่ค่าดิบ ไม่งั้นไฟจะกระพริบสลับดวงตอนค่าคาบเกี่ยวเกณฑ์ (alarm chattering) · for ev in ui.poll() อยู่ นอก if ok_read โดยตั้งใจ — เซนเซอร์ตายไม่ได้แปลว่าทั้งเครื่องต้องตายตาม

สรุปคาบ · รากฐานที่แตะ · และวันนี้อยู่ตรงไหนของเส้นทาง

คาบนี้คือจุดที่เราเลิกเป็นผู้อ่านค่า แล้วเริ่มเป็นผู้คำนวณ คาบ 3-5 อ่านค่าดิบจากขา แล้วแปลงหน่วย วันนี้ · คาบ 6 สร้างปริมาณใหม่ ที่เซนเซอร์ไม่ได้ให้มา คาบ 7-8 เก็บประวัติเป็นกราฟ รวมเป็นแดชบอร์ด คาบ 9-12 ส่งมุมขึ้นคลาวด์ เฝ้าระวังจากไกล

วันนี้เราได้: เข้าใจว่า 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 จะตามน้อง ๆ ไปทุกงานที่มีเซนเซอร์ ไม่ว่าจะเปลี่ยนภาษา เปลี่ยนบอร์ด หรือเปลี่ยนบริษัท

ใช้จริงที่ไหน — สี่มุมของการวัดมุมเอียงในสนามจริง

โครงสร้าง · สะพานและปั้นจั่น เซนเซอร์เอียงติดบนตอม่อสะพานและเสาเครน การทรุดตัว 0.1° ที่ค่อย ๆ เพิ่ม คือสัญญาณอันตราย ต้องการ: ค่าไม่ drift เพราะเฝ้าดูกันเป็นปี โรงงาน · ตั้งระดับเครื่องจักร เครื่องกลึงและ CNC ต้องได้ระดับก่อนเดินเครื่อง ช่างใช้ปุ่ม tare เทียบกับฐานอ้างอิงหน้างาน ต้องการ: ปุ่มตั้งศูนย์แบบที่เราทำวันนี้ สุขภาพ · ตรวจจับการล้ม มุมลำตัวเปลี่ยนเร็วผิดปกติ + ความเร่งพุ่งแล้วนิ่ง "นั่งลงเร็ว" กับ "ล้ม" ต่างกันที่มุมหลังพุ่ง ต้องการ: ยืนยันหลายรอบก่อนเตือน แบบคาบ 12 ยานพาหนะ · ท่าทางของตัวรถ รถบรรทุกเตือนเสี่ยงพลิกคว่ำจาก roll ขณะเข้าโค้ง โดรนและหุ่นยนต์ป้อนมุมนี้กลับเข้าตัวควบคุม ต้องการ: fusion เต็มรูป เพราะเร่งและเลี้ยวตลอด

สังเกตช่อง "ต้องการ" ของทั้งสี่มุม — โจทย์ต่างกัน จึงเลือกฟิลเตอร์และ cadence ต่างกัน ไม่มีสูตรเดียวใช้ได้หมด

ต่อยอด — คิดต่อเอง (เลือกทำ 1 ข้อ)

ข้อ 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() วางนิ่งสามนาที บันทึกว่าเส้นไหนหนีกี่องศา

ภาพ: Rudyk A.V. et al., Sensors 20(17):4841 (2020), CC BY 4.0 — Allan deviation แยกชนิดของ noise ที่ทำให้ gyro หนี · ใช้ประกอบข้อ 4

ภาพ: GregorDS, Wikimedia Commons, CC BY-SA 4.0 — 6 องศาอิสระ: เลื่อนสามแกน หมุนสามแกน

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

หน้าจอของทุกไฟล์ในคาบนี้ (1/2)

01 นับก้าวจากความเร่ง · 02 ตรวจการล้มด้วยลำดับสองเหตุการณ์ · 03 dsp.tilt() ทำอะไรกับสามตัวเลข และทำไมลำดับถึงสำคัญ
ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของ Eva Kit และ Dev Kit — แสดงหน้าจอที่ตัวอย่างสร้าง ค่าจากเซนเซอร์ WiFi และไมค์เป็นค่าแทนบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด

หน้าจอของทุกไฟล์ในคาบนี้ (2/2)

04 เข็มทิศบนบอร์ด และตัวเลขหนึ่งตัวที่ยังไม่มีใครตอบได้ · 05 สองคลาส IMU ที่เหลือใน dsp และหน่วยที่ดักไว้ทั้งคู่
ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของ Eva Kit และ Dev Kit — แสดงหน้าจอที่ตัวอย่างสร้าง ค่าจากเซนเซอร์ WiFi และไมค์เป็นค่าแทนบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด

อ้างอิงและเครดิต

แหล่งปฐมภูมิ

วิดีโอที่ฝังไว้ในคาบนี้ (ตรวจแล้วว่าเปิดได้ ตาม _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 เทียบเส้นฐานที่โปรแกรมเก็บเอง · การสั่นวัดด้วยค่าเฉลี่ยไม่ได้ เพราะบวกลบหักล้างกันจนเหลือศูนย์

☰ สารบัญ