จบชุดบทเรียนนี้เราจะเดินครบ แล้วปิดท้ายด้วยเกจฟิสิกส์ที่คำนวณเอง:
raw → derived → viz (อ่านดิบ → คำนวณ → แสดงผล)dsp.tilt) · พลังงาน (energy) · ความสูง (dsp.altitude) · ระดับเสียง (dBFS)s06_physics_viz.py แล้วดูเกจขยับตามการเคลื่อนไหวจริงปลายทางของวันนี้: เลือกปริมาณใน dropdown แล้วขยับบอร์ด — ค่าที่ เราคำนวณเอง โชว์เป็นตัวเลข + แถบ + กราฟ
วันนี้เน้น "อ่านดิบเป็น + แปลงเป็น + แสดงเป็น" ส่วนการกรองสัญญาณรบกวนและ FFT เก็บไว้ โมดูล 4 (Analysis)
จำวงจรชีวิตข้อมูล 5 ขั้นจากบทเรียน 1.1–1.3 ได้ไหม — ชุดบทเรียนนี้เราเดินถึง ขั้นที่ 2: Processing
Processing คือสะพานระหว่าง "ตัวเลขที่เซนเซอร์เห็น" กับ "ปริมาณที่คนเข้าใจ" — ถ้าข้ามขั้นนี้ โมเดลกับหน้าจอจะได้แต่ตัวเลขดิบที่ไม่มีความหมาย
ลองดูของจริง sensors.bmi270.motion() คืนอะไรมา:
ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
# เช่น: (0.20, -6.97, 6.87, 1.1, -0.4, 0.3)
ax, ay, az ไปเข้าสูตรตรีโกณ จะได้ มุมเอียง ที่คนเข้าใจทันทีคำถามของชุดบทเรียนนี้: "ตัวเลขจากเซนเซอร์นี้ แปลงเป็นปริมาณที่มีความหมายได้ยังไง?" — คำตอบคือคณิตกับฟิสิกส์ไม่กี่บรรทัด
ทุกปริมาณที่เราทำวันนี้เดินตามสามจังหวะเดียวกันเป๊ะ จำโครงนี้ให้ขึ้นใจ เพราะมันคือ MVP ของชุดบทเรียน (MVP ของบทเรียน 3.1–3.2)
MVP ของบทเรียน 3.1–3.2: สัญญาณดิบ → ปริมาณที่คำนวณได้ → แสดงเห็นบนจอ ครบวง สำหรับอย่างน้อยหนึ่งปริมาณ
ที่เราให้ dropdown สลับ 4 ปริมาณ ก็เพราะทั้งสี่ใช้โครงเดียวกัน ต่างกันแค่ "สูตรตรงกลาง" — เห็นโครงร่วมแล้วจะเติมได้ทั้งสี่โดยไม่งง
แต่ละปริมาณมาจากเซนเซอร์ต่างกัน แต่เข้าโครง raw → derived → viz เหมือนกันหมด
| ปริมาณ | เซนเซอร์ (raw) | สูตรแปลง (derived) | หน่วย | รันที่ไหน |
|---|---|---|---|---|
| Tilt มุมเอียง | IMU bmi270.motion() |
dsp.tilt(ax,ay,az) |
องศา | Emulator + บอร์ด |
| Energy พลังงาน | IMU bmi270.motion() |
abs(accel) - 1g |
g | Emulator + บอร์ด |
| Altitude ความสูง | baro dps368 |
dsp.altitude(p, p0) |
เมตร | Emulator + บอร์ด |
| Sound ระดับเสียง | MIC (PDM) | 20·log10(rms/32768) |
dBFS | บอร์ดเท่านั้น |
สังเกตว่า 2 ใน 4 มาจาก IMU ตัวเดียว — เซนเซอร์เดียวแปลงได้หลายปริมาณ ขึ้นอยู่กับ "สูตรที่เราเลือกใช้"
ชุดบทเรียนนี้ไม่ได้เริ่มจากศูนย์ เรายืนบนสองชุดบทเรียนก่อนหน้า:
import → สร้างครั้งเดียว → ลูป → ui.poll ที่เราจะเจอซ้ำในไฟล์วันนี้ทุกบรรทัดsensors.bmi270.motion(), sensors.dps368...) วันนี้เราต่อยอดด้วยการ แปลง ค่าที่อ่านมาdsp และ "สูตรฟิสิกส์" ที่ทำให้ค่าดิบมีความหมายถ้าลืมโครงร่วมของบทเรียน 1.4–1.5 กลับไปเปิด
s02ทวนสัก 5 นาทีก่อน เพราะไฟล์วันนี้เดินตามโครงนั้นเป๊ะ ต่างกันแค่ในลูปเราคำนวณฟิสิกส์แทนการอ่านโมเดล
dsp (Digital Signal Processing) คือกล่องเครื่องมือคณิตของ BENTO คำนวณฝั่ง C จึงเร็ว วันนี้เราแตะสองฟังก์ชันสำเร็จรูป + เขียนสูตรเองสองอัน
| กลุ่ม | ตัวอย่าง | ชุดบทเรียนนี้ใช้ |
|---|---|---|
| ฟิวชัน IMU (มุม) | dsp.tilt(ax,ay,az) → (roll,pitch) |
ใช้ (Tilt) |
| ความดัน → ความสูง | dsp.altitude(p[, p0]) → เมตร |
ใช้ (Altitude) |
| ตัวกรองสัญญาณ | dsp.EMA / Median / Kalman1D |
บทเรียน 4.1–4.2 |
| สภาพอากาศ | dsp.dew_point / heat_index |
บทเรียน 3.3–3.4 |
dsp — เราแค่เรียกใช้ (แต่จะเข้าใจว่ามันคำนวณอะไร)math.sqrt / math.log10 (นี่คือ "คณิตที่คุณเขียน")
dspคำนวณฝั่ง C ก็จริง แต่เราจะไม่เรียกแบบกล่องดำ — ทุกฟังก์ชันที่ใช้ เราจะเปิดดูว่ามันแปลงตัวเลขด้วยฟิสิกส์อะไร จะได้ remix เองเป็นในชุดบทเรียนถัด ๆ ไป
Accelerometer ไม่ได้วัด "มุม" ตรงๆ มันวัด เวกเตอร์แรงโน้มถ่วง ที่ชี้ลงพื้นเสมอ พอบอร์ดเอียง เวกเตอร์นี้จะกระจายลงสามแกนต่างกัน — เราถอดมุมกลับออกมาด้วยตรีโกณ
เพราะ tilt ใช้ อัตราส่วน ของแกน (atan2) หน่วยของ accelerometer จึงหักล้างกัน — นี่คือเหตุผลที่ตัวอย่าง 02 เอา
motion()ยัดเข้าdsp.tiltได้เลยโดยไม่ต้องแปลงหน่วยก่อน
สูตรเต็มของมุมเอียง เขียนเป็นคณิตได้สองบรรทัด (ผลลัพธ์เป็นเรเดียน คูณ ให้เป็นองศา):
อ่านทีละตัว:
bmi270.motion() (หน่วย m/s²)ทำไมสำคัญกับชุดบทเรียนนี้: เพราะสูตรเป็น อัตราส่วน ของแกน (เช่น ) หน่วยจึงหักล้างกันหมด นี่คือเหตุผลที่ป้อน motion() เข้า dsp.tilt ได้ตรงๆ โดยไม่ต้องแปลงหน่วยก่อน และ ยังกันปัญหาหารด้วยศูนย์ตอน (บอร์ดตั้งฉากพอดี) ที่ ธรรมดาทำไม่ได้
▸ ลองเล่นสด (GeoGebra): เปิด Interactive Math Lab — ลากเวกเตอร์แรงโน้มถ่วง ดู atan2 คำนวณมุมเอียงสด (applet 5)
จาก raw ของ IMU สามค่า เรียก dsp.tilt ครั้งเดียวได้มุมสองแกน แล้วเลือกแกนที่เอียงมากสุดมาโชว์:
ax, ay, az, gx, gy, gz = sensors.bmi270.motion() # raw
roll, pitch = dsp.tilt(ax, ay, az) # derived — สูตรที่คุณเติม
dom = pitch if abs(pitch) >= abs(roll) else roll # แกนที่เอียงมากสุด
seg.text("%d" % int(dom)) # viz: ตัวเลข
norm = clamp100(abs(dom) / 90.0 * 100) # 0..90 องศา -> 0..100
bar.value(norm) # viz: แถบ
dsp.tilt คืน (roll, pitch) — ระวังลำดับ! roll มาก่อน pitchclamp100 บีบค่าเป็น 0..100 เพราะ Bar/Chart รับช่วงนี้ (เราตั้ง min=0, max=100)
dsp.tilt(ax,ay,az) -> (roll, pitch)ตรวจจากเอกสาร API แล้ว ลำดับคือ roll ก่อน ถ้าสลับ เกจสองแกนจะสลับกัน — ลองสังเกตตอนรัน
ถ้าอยากรู้ว่าบอร์ด "ขยับแรงแค่ไหน" (ไม่สนทิศ) เราวัด ขนาดของเวกเตอร์ความเร่ง ตอนวางนิ่ง ขนาดนี้ ~ 1g (แรงโน้มถ่วงล้วน) พอเขย่า ขนาดจะเบนออกจาก 1g
นี่ไม่มีฟังก์ชันสำเร็จใน
dsp— เราเขียนสูตรเองด้วยmath.sqrtขนาดเวกเตอร์คือรากที่สองของผลรวมกำลังสอง (พีทาโกรัสสามมิติ) หาร 9.81 ให้เป็นหน่วย g แล้วลบ 1g ออกเพื่อให้ "นิ่ง = 0"
ax, ay, az, gx, gy, gz = sensors.bmi270.motion() # raw
mag = math.sqrt(ax*ax + ay*ay + az*az) / 9.81 # ขนาดเวกเตอร์ (m/s² -> g)
energy = abs(mag - 1.0) # derived — ลบฐาน 1g
seg.text("%.2f" % energy) # viz
norm = clamp100(energy / 2.0 * 100) # 0..2g -> 0..100
ax*ax เร็วกว่า ax**2 เล็กน้อยบน MicroPython และอ่านง่ายพอกันmotion() คืนความเร่งหน่วย m/s² (วางนิ่ง ≈ 9.81) เราหาร 9.81 ให้เป็นหน่วย g (วางนิ่ง = 1g) แล้วลบ 1.0 — ถ้าไม่ลบ ตอนนิ่งค่าจะค้างที่ 1 ไม่ใช่ 0abs(...) กัน energy ติดลบตอน mag < 1g (เช่นตกอิสระชั่วขณะ)ทำไมต้อง "ลบ 1g"? เพราะเราสนใจ การเปลี่ยนแปลง ไม่ใช่แรงโน้มถ่วงคงที่ นี่คือไอเดียเดียวกับ high-pass filter ที่จะเจอเต็มๆ ในบทเรียน 4.1–4.2
ยิ่งสูง อากาศยิ่งเบาบาง ความดันจึงลดลงตามความสูง ความสัมพันธ์นี้เป็นสูตรมาตรฐาน (barometric formula) dsp.altitude ใส่สูตรนี้มาให้แล้ว
ทำไมต้องมี
p0? ถ้าไม่มีจุดอ้างอิง เราจะได้แต่ "ความสูงเหนือน้ำทะเล" ซึ่งขึ้นกับสภาพอากาศวันนั้น การจับp0ตอนเริ่มทำให้ได้ ความสูงสัมพัทธ์ ที่แม่นและเห็นผลชัดเวลายกบอร์ด
# ก่อนลูป: จับความดันอ้างอิงครั้งเดียว
p0, _ = sensors.dps368.pressure_temperature()
# ในลูป:
p, t = sensors.dps368.pressure_temperature() # raw
alt = dsp.altitude(p, p0) # derived — สูตรที่คุณเติม
seg.text("%.1f" % alt) # viz (เมตร)
norm = clamp100((alt + 5.0) / 10.0 * 100) # โชว์ช่วง -5..+5 ม.
dps368.pressure_temperature() คืน (hPa, °C) — เราใช้แค่ความดัน ส่วนอุณหภูมิทิ้ง (_)
dsp.altitude(p[, p0])—p0เป็น argument ที่ไม่บังคับ ถ้าไม่ใส่จะใช้ความดันมาตรฐานระดับน้ำทะเล (1013.25) เราเลือกใส่p0เอง เพื่อให้จุด 0 อยู่ที่ "ตอนเริ่มโปรแกรม"
ไมค์คืนคลื่นเสียงเป็นตัวอย่างหลายพันจุดต่อวินาที เราย่อมันเป็น "ระดับความดัง" ตัวเลขเดียวสองก้าว: RMS (พลังงานเฉลี่ย) แล้วแปลงเป็น dBFS (สเกล log)
32768คือค่าเต็มสเกลของตัวอย่างเสียง 16-bit (2¹⁵) เราเทียบ RMS กับค่าเต็มสเกล จึงได้ "dBFS" = decibel เทียบ Full Scale ค่าจะเป็น 0 เมื่อดังสุด และติดลบเมื่อเบาลง
ย่อคลื่นเสียง จุดให้เหลือตัวเลขเดียว ด้วยสองก้าว:
อ่านทีละตัว:
len(mbuf))ทำไมสำคัญกับชุดบทเรียนนี้: สัมประสิทธิ์เป็น ไม่ใช่ เพราะ RMS เป็น แอมพลิจูด ไม่ใช่กำลัง ( มาจาก ในนิยามกำลัง) ค่าจึงออกมาเป็น dBFS เมื่อดังเต็มสเกลและติดลบเมื่อเบาลง ตรงกับที่หูคนรับรู้เสียงแบบ log พอดี
if has_mic: # PDM มีเฉพาะบนบอร์ด
pdm.readinto(mbuf) # raw: อ่านคลื่นเข้า buffer
acc = 0
for s in mbuf:
acc += s * s # ผลรวมกำลังสอง
rms = math.sqrt(acc / len(mbuf)) # ค่ากลางกำลังสอง
db = 20 * math.log10(rms / 32768.0) # derived — สูตรที่คุณเติม
seg.text("%d" % int(db)) # viz
has_mic ก่อน (บน Emulator จะได้เสียงสังเคราะห์ ไม่ใช่เสียงจริง)rms > 0 ก่อน log10 เพราะ log10(0) ไม่มีค่า (เงียบสนิท = ให้ค่าพื้น -96 dBFS)pdm.deinit() ใน finally — คืนฮาร์ดแวร์ไมค์เสมอปริมาณนี้ต้องใช้ เสียงจริงบนบอร์ด — บนบอร์ดให้ลองปรบมือ/พูดใส่ไมค์ดู ส่วน Emulator เล่นสามปริมาณแรก (Tilt/Energy/Altitude) ได้เต็มที่
ทำไม dBFS ใช้ log10 ไม่ใช่ค่าดิบตรงๆ? เพราะ หูคนรับรู้เสียงแบบ log ไม่ใช่แบบเชิงเส้น
log สเกลไม่ได้มีแค่ในเสียง — ความสว่าง แผ่นดินไหว (Richter) ค่า pH ล้วนใช้ log เพราะธรรมชาติหลายอย่างครอบคลุมช่วงกว้างมาก การ "บีบด้วย log" ทำให้ตัวเลขอ่านออก
ปริมาณต่างชนิดเหมาะกับการแสดงผลต่างแบบ นี่คือทักษะของขั้น viz — เลือกให้ผู้ใช้ "อ่านออกในพริบตา"
| widget | เหมาะกับ | ใช้กับปริมาณ |
|---|---|---|
ui.Seg7 |
ตัวเลขเด่นตัวเดียว (headline) | ทุกปริมาณ (ค่าจริง) |
ui.Bar |
ระดับเทียบกับช่วง (0..100) | ทุกปริมาณ (normalized) |
ui.Chart |
แนวโน้มย้อนหลัง | ทุกปริมาณ (ประวัติ) |
ui.Arc |
มุม/เข็มหมุน | เหมาะ Tilt เป็นพิเศษ |
Bar และ Chart รับช่วง min..max เราตั้ง 0..100 แล้ว normalize ทุกปริมาณเข้าช่วงนี้ด้วย clamp100Arc คู่กับ tilt เพราะเข็มหมุนสื่อ "มุม" ได้ตรงกว่าแถบ — ลองปรับใช้ในงานทำเองได้viz ไม่ใช่แค่ "โชว์ตัวเลข" แต่คือการเลือกภาพที่ตรงกับปริมาณ ระดับเสียงเป็นแถบ มุมเป็นเข็ม แนวโน้มเป็นกราฟ — เลือกผิด ผู้ใช้ก็อ่านยากขึ้นทันที