คาบ 7 — กราฟ Real-time

ui.Chart หลาย Series: เห็นสัญญาณเป็นเส้นเวลา ไม่ใช่ตัวเลขกระพริบ

คาถาประจำคาบ: ตัวเลขบอกว่าตอนนี้เท่าไร กราฟบอกว่ามันกำลังจะไปทางไหน

ดูของจริงก่อน — กราฟที่วิ่งอยู่บนบอร์ดตั้งแต่คาบแรก

Sensor Dashboard — accel 3 แกน Z Y X ลองสามอย่างตามลำดับ วางนิ่ง — เส้น Z ลอยสูงกว่าเพื่อน เขย่าเบา ๆ — รอยกระเพื่อมเลื่อนซ้าย เอียงค้าง — เส้นยกตัวแล้วค้างระดับใหม่

วันนี้เราจะสร้างหน้านี้ขึ้นมาเอง ด้วยโค้ดไม่ถึงเจ็ดสิบบรรทัด

คาบ 6 เราวัด "ตอนนี้เอียงกี่องศา" — คาบนี้เราจะเก็บ "สิบวินาทีที่ผ่านมาเป็นยังไง"

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

คำถาม คำตอบของคาบนี้ อยู่ช่วงไหน
Why ตัวเลขบนจอก็อ่านค่าได้อยู่แล้ว ทำไมต้องมีกราฟ เพราะตัวเลขตอบได้คำถามเดียวคือ "ตอนนี้เท่าไร" ส่วนคำถามที่งานจริงถามคือ "เมื่อกี้มันเป็นยังไง" และ "มันกำลังจะไปทางไหน" · และการสุ่มไม่ทันไม่ส่งเสียงเตือน มันให้คำตอบที่ผิดและดูน่าเชื่อถือ ต้องรู้ล่วงหน้าว่าสัญญาณของเราเร็วแค่ไหน ครึ่งแรก · Nyquist และ aliasing
What มีอะไรให้ใช้บ้าง ui.Chart กับเมธอดของ Widget ทั้ง 14 ตัว — สองตัวเป็นของกราฟล้วน ๆ · บวกตัวสร้าง widget ของ ui ทั้ง 16 ตัว ที่กางครบในตารางอ้างอิงของคาบนี้ สไลด์บัญชี 14 เมธอด + ตารางอ้างอิง widget
How ประกอบยังไงให้ใช้งานได้จริง สร้างกราฟหนึ่งใบสามเส้น ป้อนด้วย int() ทุกรอบ · ปุ่มเริ่ม/หยุดบันทึกที่เป็น flag ในลูป ไม่ใช่การหยุดลูป · ตารางเก็บค่าสุดขีดที่กราฟจำไม่ได้ · และนาฬิกาที่จับคาบลูปจริงมาโชว์ สามไฟล์ตัวอย่าง + ใบฝึก

ปลายทางที่จับต้องได้ — กราฟสามสีของแกน X Y Z วิ่งตามการเขย่ามือ มีปุ่มหยุด-เดินต่อที่ทำงานจริง และบรรทัดที่บอกว่าลูปของเราหมุนอยู่ที่กี่มิลลิวินาที

คาบ 6 เราวัด "ตอนนี้เอียงกี่องศา" · คาบนี้เราเก็บ "สิบวินาทีที่ผ่านมาเป็นยังไง"

เป้าหมายของคาบนี้

  1. อธิบายได้ว่า time series คืออะไร และ อัตราสุ่ม (sampling rate) มีผลกับสิ่งที่เราเห็นอย่างไร
  2. ใช้ ui.Chart กับ .add_series() และ .set_next() ได้ถูกต้อง รวมถึงรู้ว่าทำไมต้อง int()
  3. เลือกช่วงแกน Y เองเป็น และรู้ว่าค่าที่เกินช่วงจะหายไปไหน
  4. ทำปุ่มเริ่ม/หยุดบันทึกที่แยกกันด้วย flag ในลูป ที่ทำงานได้จริง ไม่ใช่ปุ่มที่กดแล้วเงียบ — ให้ไฟกับป้ายเป็นคนบอกสถานะ
  5. วัด คาบลูปจริง ด้วย time.ticks_ms() / time.ticks_diff() แล้วเอาขึ้นจอ
  6. รู้ว่าทำไม ลูปที่มีแต่ set_next() ถึงวาดช้ากว่าลูปที่มี .text() อยู่ด้วยถึง 40 เท่า

ปลายทางของวันนี้: จอบอร์ดมีกราฟสามสีวิ่งตามการเขย่า ตารางค่าสุดขีดสามแกน ปุ่มเริ่ม/หยุดบันทึก และตัวเลขบอกว่าลูปของเราหมุนจริงที่กี่มิลลิวินาที

คาบนี้เราไม่ได้แค่ทำให้กราฟขึ้น เราต้องรู้ด้วยว่ากราฟนั้น เชื่อถือได้แค่ไหน

ปลายทางของคาบนี้ — กราฟที่วิ่งตามมือเรา

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

หน้าจอของเฉลยปัจจุบันแบ่งเป็นสามส่วน และสองส่วนแรกตอบคนละคำถามกัน

  • ซ้าย ui.Chart เส้นสามสีของแกน X Y Z ตอบคำถามว่า "เมื่อกี้มันเป็นยังไง" (ตอนบอร์ดวางนิ่ง เส้นจึงราบ)
  • ขวา ui.Table สี่คอลัมน์ แกน · ต่ำสุด · สูงสุด · ล่าสุด ตอบคำถามที่กราฟตอบไม่ได้ คือ ค่าสุดขีดที่ผ่านไปแล้ว เพราะกราฟเก็บได้เท่าจำนวนช่องของมัน (ปริยาย 50) จุดที่เก่ากว่านั้นถูกเขี่ยทิ้งไปแล้ว
  • ซ้ายล่าง ใต้กราฟ ui.Led บอกว่ากำลังบันทึกอยู่หรือหยุดแล้ว บรรทัดคาบลูปจริง และ ปุ่มเริ่มกับปุ่มหยุดที่แยกกันคนละปุ่ม (สูง 88 px ตามขนาดเป้าสัมผัส)

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

ทบทวนคาบ 6 — แถบกับตัวเลขตอบคำถามหนึ่งแบบ กราฟตอบอีกแบบ

คาบ 6 เราอ่าน sensors.bmi270.motion() → dsp.tilt(ax, ay, az) → (roll, pitch) → ui.Bar บน ui.Scale สองแกน + ui.Seg7

สิ่งที่แถบกับ Seg7 ทำได้ดี: บอก ค่าปัจจุบัน ชัดเจน · สิ่งที่ทำไม่ได้เลย: บอกว่า เมื่อกี้เป็นยังไง

คำถามที่ทีมอยากรู้ Bar / Seg7 Chart
ตอนนี้เอียงกี่องศา ตอบได้ทันที ต้องเพ่งที่ปลายเส้น
เมื่อ 5 วินาทีที่แล้วสั่นไหม ตอบไม่ได้ ตอบได้
การสั่นถี่ขึ้นหรือเบาลง ตอบไม่ได้ เห็นเป็นรูปร่างทันที
มีค่ากระโดดผิดปกติแวบเดียวไหม พลาดแน่นอน เห็นเป็นหนามบนเส้น

ภาพขวาคือสัญญาณ accel จริงตอนคนเดิน 10 ก้าว — ยอดที่ทำเครื่องหมายสีแดงคือก้าว ข้อมูลชุดนี้อ่านจากแถบหรือตัวเลขไม่ได้เลย ต้องเห็นเป็นเส้นเวลาเท่านั้น

ภาพ: Zieliński M. et al., Sensors 25(20):6358 (2025), CC BY 4.0

เลือก widget ตาม คำถามที่ต้องการคำตอบ ไม่ใช่ตามความสวย

Time series คืออะไร — และ sample-and-hold ที่อยู่เบื้องหลัง

Time series คือข้อมูลชุดหนึ่งที่ แต่ละค่ามีเวลากำกับ และเรียงตามเวลา

เซนเซอร์ให้สัญญาณต่อเนื่องซึ่งมีค่าอยู่ทุกเสี้ยววินาที แต่คอมพิวเตอร์เก็บค่าต่อเนื่องไม่ได้ มันทำได้อย่างเดียวคือ แอบมองเป็นระยะ ๆ แล้วจดค่าไว้ — เรียกว่า การสุ่มตัวอย่าง (sampling)

fs=1Tsf_s=\frac{1}{T_s}

ไทย: อัตราสุ่มคือส่วนกลับของระยะห่างระหว่างการมองสองครั้ง
ตัวเลขของลูปเรา: Ts=200T_s = 200 ms ⇒fs=5\Rightarrow f_s = 5 Hz

วงจร sample-and-hold (ภาพขวาบน) คือฮาร์ดแวร์ที่ทำเรื่องนี้จริง ๆ ในชิป: ปิดสวิตช์ชั่วขณะเพื่อชาร์จตัวเก็บประจุ แล้วเปิดสวิตช์ค้างค่านั้นไว้ให้ ADC อ่านทัน — เอาต์พุตจึงเป็นขั้นบันได ไม่ใช่เส้นโค้ง

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

ภาพ: Giacomo Alessandroni, Wikimedia Commons, CC BY-SA 4.0

ภาพ: Rbj / wdwd, Wikimedia Commons, สาธารณสมบัติ — ต่อเนื่อง → ไม่ต่อเนื่อง

กราฟทุกกราฟในโลก embedded คือ "ค่าที่เราเลือกจะมอง" ไม่ใช่ "ทุกอย่างที่เกิดขึ้น"

สุ่มเร็วพอไหม — เงื่อนไข Nyquist

ภาพ: Jacopo Bertolotti, Wikimedia Commons, CC0 1.0 — สุ่มถี่พอ สร้างสัญญาณเดิมกลับมาได้

ภาพ: Pluke, Wikimedia Commons, CC0 1.0 — ซ้าย 2 จุดต่อคาบ (พอดีขีดจำกัด) · ขวา 1 จุดต่อคาบ (ต่ำกว่า Nyquist)
Analog Snippets · 12:08 · EN — "สุ่มเร็วกว่าที่จำเป็น" ซื้ออะไรกลับมา

ปี 1928 แฮร์รี ไนควิสต์ พิสูจน์ไว้ว่า ถ้าจะเก็บสัญญาณที่มีความถี่สูงสุด fmax⁡f_{\max} ให้ครบ ต้องสุ่มเร็วกว่าสองเท่าของมัน

fs>2fmax⁡⟺fmax⁡<fs2f_s > 2 f_{\max}\qquad\Longleftrightarrow\qquad f_{\max} < \frac{f_s}{2}

ไทย: จะเห็นคลื่นความถี่หนึ่งได้ ต้องสุ่มเร็วกว่าสองเท่าของคลื่นนั้น
ตัวเลขของลูปเรา: fs=5f_s = 5 Hz ⇒\Rightarrow เห็นการสั่นได้ไม่เกิน 2.5 Hz

การเขย่ามือของคนอยู่ราว 2-5 Hz เราจึงเห็นมันได้ · อัตราสุ่มของเราไม่ได้ตั้งด้วยฮาร์ดแวร์ แต่เกิดจากความเร็วลูป Python

fs=1Tloop,Tloop=Tsleep+Tworkf_s = \frac{1}{T_{\text{loop}}},\qquad T_{\text{loop}} = T_{\text{sleep}} + T_{\text{work}}

ไทย: งานในลูปหนักขึ้น อัตราสุ่มตกลงเงียบ ๆ — จึงต้องวัดคาบลูปจริง (ท่าที่ 5)

สุ่มไม่ทันไม่ได้แปลว่า "มองไม่เห็น" มันแปลว่า "เห็นผิด" — สไลด์ถัดไปคือเหตุผล

Aliasing — สัญญาณที่เร็วเกินไปไม่ได้หายไป มันปลอมตัว

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

ภาพ: Moxfyre, Wikimedia Commons, CC BY-SA 3.0 — สองความถี่ให้จุดสุ่มชุดเดียวกันเป๊ะ

falias=∣ f−kfs ∣,k=จำนวนเต็มที่ทำให้ falias≤fs2f_{\text{alias}} = \bigl|\,f - k f_s\,\bigr|,\qquad k = \text{จำนวนเต็มที่ทำให้ } f_{\text{alias}} \le \tfrac{f_s}{2}

ไทย: ถ้าสัญญาณเร็วเกินไป มันจะไม่หายไปเฉย ๆ แต่จะ ปลอมตัว มาเป็นคลื่นช้ากว่าความจริง ซึ่งอันตรายกว่าการมองไม่เห็น

ตัวเลขจากบอร์ดนี้ — เดโมที่ทำได้ในคาบ: เขย่าบอร์ดที่ราว 6 Hz ขณะลูปสุ่มที่ fs=5f_s = 5 Hz

falias=∣6−1×5∣=1 Hzf_{\text{alias}} = |6 - 1\times 5| = 1\ \text{Hz}

กราฟจะโชว์คลื่นช้า ๆ 1 Hz ที่ ไม่มีอยู่จริง ทั้งที่มือเราเขย่าอยู่ 6 ครั้งต่อวินาที

นี่คือเหตุผลเดียวกับที่ล้อรถในหนังบางทีดูเหมือนหมุนถอยหลัง — กล้องคือ ADC ที่สุ่มที่ 24 เฟรมต่อวินาที

เจอตอนไหน ค่าที่เร็วเกินอัตราสุ่มไม่มี error ไม่มีคำเตือน มันแค่ให้คำตอบที่ผิด และดูน่าเชื่อถือด้วย

ภาพ: Qef, Wikimedia Commons, สาธารณสมบัติ — ที่ความถี่วิกฤต: จุดสุ่มตกที่เดิมทุกคาบ กราฟจึงกลายเป็นเส้นตรง

ภาพ: “Nyquist Shannon theorem sine visualisation animation” โดย Enormator — CC0 1.0 · Wikimedia Commons — สามภาพนิ่งเห็นแค่ผลลัพธ์ · ภาพนี้ค่อย ๆ ลดอัตราสุ่มลงให้ดู จังหวะที่คลื่นเริ่ม "ปลอมตัว" คือสิ่งที่ต้องเห็นตอนมันเคลื่อน

Aliasing บนบอร์ดจริง — สูตรจากสไลด์ก่อน เดินให้ดูทีละขั้น

หน้าจอจริงตอนรัน examples/s07/03_aliasing_nyquist.py↗ — สูตรในสไลด์ก่อนเดินให้ดูบนบอร์ดจริงทีละขั้น อัตราสุ่มถูกตรึงไว้ที่ 40 Hz ตลอด เปลี่ยนเฉพาะความถี่ของคลื่นจริง · ภาพนี้คือขั้นแรกซึ่งเป็นตัวคุม f จริง = 5 Hz ยังต่ำกว่า Nyquist ที่ 20 Hz อยู่มาก ความถี่ที่เครื่อง "เห็น" จึงเป็น 5 เท่ากัน และเส้นทับกันสนิท — ผีจะโผล่ก็ต่อเมื่อกดเดินหน้าจนความถี่จริงเกิน 20 Hz · ภาพหน้าจอจริงจากบอร์ด Eva Kit บันทึกโดยผู้สอน · ขวา — หน้าจอที่ examples/lvgl_ports/sec3_sensor_viz/eva/ex16_spectrum_analyzer.py สร้างบน Emulator (โปรไฟล์ Eva): สเปกตรัมจาก dsp.fft_mag ของจริงใน C — sine 1 kHz ยอดเดี่ยวที่ bin 5 อ่าน Dominant 937 Hz เพราะความละเอียดต่อ bin คือ 48000/256 = 187.5 Hz (firmware 2026-08-20 ขึ้นไป)

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

int() ก่อน set_next — และราคาที่ต้องจ่าย

Chart เก็บค่าเป็น จำนวนเต็ม เท่านั้น แต่ motion() คืนทศนิยม เช่น 9.78

chart.set_next(0, int(ax))       # ถูก
chart.set_next(0, ax)            # TypeError: can't convert float to int

ราคาที่ต้องจ่ายคือ int() ตัดทศนิยมทิ้ง ไม่ใช่ปัดเศษ — 0.9 เป็น 0 และ −0.9 ก็เป็น 0 กราฟจึงหยาบเป็นขั้นละ 1 m/s² นี่คือ quantization ตัวเดียวกับคาบ 5 แค่คราวนี้เราเป็นคนสร้างเอง

สูตร map ช่วงจริงลงช่วงแกน — วิธีแก้ที่ใช้ในงานจริง

vplot=round⁡ ⁣(a−amin⁡amax⁡−amin⁡×(ymax⁡−ymin⁡)+ymin⁡)v_{\text{plot}} = \operatorname{round}\!\left(\frac{a - a_{\min}}{a_{\max} - a_{\min}}\times\bigl(y_{\max} - y_{\min}\bigr) + y_{\min}\right)

ไทย: แกน Y ของ Chart เป็นจำนวนเต็ม ต้อง map ช่วงค่าจริงลงช่วงแกนก่อน ไม่งั้นกราฟแบนหรือทะลุขอบ

chart = ui.Chart(..., min=-2000, max=2000)   # หน่วยกลายเป็น 0.01 m/s²
chart.set_next(0, int(ax * 100))             # 9.78 -> 978

ตัวเลขจากบอร์ดนี้: ช่วง ±20 กับ int() ตรง ๆ ได้ความละเอียด 1 m/s² · คูณ 100 แล้วขยายช่วงเป็น ±2000 ได้ 0.01 m/s² — ละเอียดขึ้น 100 เท่าโดยไม่แตะเซนเซอร์เลย

ภาพ: Gregory Maxwell, Wikimedia Commons, CC BY 3.0 — ความคลาดเคลื่อนจากการปัดเข้าขั้น

ภาพ: Wikimedia Commons, CC BY-SA 3.0 — สุ่มตามเวลา + ปัดตามระดับ เป็นคนละเรื่องกัน
Microchip Developer Help · ยาว: ยังไม่ยืนยัน · EN — quantization กับ resolution (คลิปเดียวกับคาบ 5)

ตัวเลขบนแกน Y ไม่จำเป็นต้องเป็นหน่วยจริง ขอแค่ เรากับคนอ่านกราฟรู้ตรงกัน ว่ามันคูณอะไรไว้

Chart คือ ring buffer 50 ช่อง — set_next() ไม่ได้ "วาดกราฟ"

ค่าใหม่ดันเข้าทางขวา ค่าเก่าที่สุดหล่นออกทางซ้าย … ช่องที่ 1 ช่องที่ 50 set_next() ดันเข้าท้ายแถว ค่าเก่าสุดหล่นหาย — ไม่มีใครเก็บให้ ช่อง ↔ จุดบนกราฟ คือของสิ่งเดียวกัน หน้าต่างเวลา 50 × 200 ms = 10 วินาที สูงสุด 4 series ต่อกราฟหนึ่งใบ · แต่ละ series มีบัฟเฟอร์ 50 ช่องของตัวเอง

Twindow=Npoints×Tloop=50×0.2 s=10 sT_{\text{window}} = N_{\text{points}} \times T_{\text{loop}} = 50 \times 0.2\ \text{s} = 10\ \text{s}

ไทย: หน้าจอเรากว้าง 10 วินาที ของเก่ากว่านั้นถูกดันตกขอบไปแล้ว · อยากเห็นย้อนหลังนานขึ้นมีสองปุ่มให้หมุน — ยืดคาบให้ห่างขึ้น หรือ เพิ่มจำนวนจุด ด้วย ch.prop(ui.PROP_CHART_POINTS, n) ตั้งได้ 10-400 (firmware 2026-08-20 ขึ้นไป · รุ่นก่อนหน้า 50 ตายตัว) แต่ทุกจุดที่เพิ่มคือข้อความ IPC ที่ต้องส่งเพิ่มเวลาวาดทั้งเส้น คาบที่ห่างขึ้นจึงยังเป็นปุ่มที่ถูกกว่าเสมอ

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

เข้าใจฮาร์ดแวร์ · ทำไมกราฟช้า ทั้งที่ลูปเราเร็ว

โค้ด Python อยู่บน CM33 กราฟถูกวาดโดย CM55 chart.set_next() ไม่ได้วาดทันที มันแค่ ฝากคำสั่งลงคิว IPC แล้วกลับมาทำงานต่อ

ฝั่ง CM55 มีตัวจับเวลาคอยหยิบคำสั่งจากคิวมาทำ และมันมี สองความเร็ว

โหมด คาบของตัวจับเวลา หยิบได้ต่อครั้ง อัตราการระบายจริง
ปกติ (idle) 200 ms 16 คำสั่ง 80 คำสั่ง/วินาที
เร่ง (fast) 5 ms 16 คำสั่ง 3,200 คำสั่ง/วินาที

อัตราระบาย=Nต่อครั้งTtimer=160.2=80เทียบกับ160.005=3,200\text{อัตราระบาย} = \frac{N_{\text{ต่อครั้ง}}}{T_{\text{timer}}} = \frac{16}{0.2} = 80 \quad\text{เทียบกับ}\quad \frac{16}{0.005} = 3{,}200

และนี่คือกับดัก — คำสั่งที่ ปลุกโหมดเร่ง มีอยู่ชุดหนึ่ง คำสั่งที่ ไม่ปลุก ก็มีอีกชุด

ปลุกโหมดเร่ง ไม่ปลุกโหมดเร่ง
สร้าง widget · .text() · .pos() · .size() · .color() .value() · .set_next() · .show() / .hide()

โหมดเร่งจะกลับเป็นปกติเองหลังเงียบไป 500 ms

ลูปที่มีแต่ set_next() ตัวจับเวลาเดินที่ 200 ms 80 จุด/วินาที เต็มจอ 50 จุด ใช้ 0.63 วินาที ≈ 1.6 เฟรม/วินาที — กระตุก ลูปที่มี .text() อยู่ด้วย ตัวจับเวลาเร่งเป็น 5 ms 3,200 จุด/วินาที
ที่มาของตัวเลขทั้งหมดบนสไลด์นี้: ซอร์สเฟิร์มแวร์ BENTO-TESAIoT-libraries/claw/common/modules/ipc_ui/ipc_ui.c — IPC_UI_TIMER_MS=200, IPC_UI_FAST_TIMER_MS=5, IPC_UI_MAX_PER_TICK=16, IPC_UI_FAST_TIMEOUT_MS=500 และรายการ opcode ที่เรียก ui_arm_fast_mode()

set_next() เป็นคำสั่งที่ ไม่ปลุก โหมดเร่ง — กราฟที่วิ่งอยู่ตัวเดียวบนจอ จึงเป็นกรณีที่ช้าที่สุดพอดี

กฎปฏิบัติ: มี Label สถานะอยู่ในลูปเสมอ

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

rec_msg = "กำลังบันทึก"     # เปลี่ยนเฉพาะตอนกดปุ่ม
while True:
    ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
    chart.set_next(0, int(ax))          # ไม่ปลุกโหมดเร่ง
    chart.set_next(s_ay, int(ay))       # ไม่ปลุก
    chart.set_next(s_az, int(az))       # ไม่ปลุก
    led_rec.value(1)                    # ไม่ปลุก - .value() ทุกตัวไม่ปลุก
    lbl_rec.text(rec_msg)               # ← ปลุก และค้างไว้อีก 500 ms
    ui.poll()
    time.sleep_ms(PERIOD_MS)

ทำไมมันได้ผลกับลูป 200 ms ของเรา — โหมดเร่งค้างอยู่ 500 ms หลังคำสั่งสุดท้าย ลูปเราหมุนทุก 200 ms ซึ่งสั้นกว่า 500 ms ตัวจับเวลาจึงไม่มีโอกาสกลับเป็นโหมดปกติเลยตลอดการรัน

กฎนี้ชนกับกฎ "ตัวเลขเปลี่ยนไม่เกินวินาทีละครั้ง" พอดี และทางออกอยู่ตรงกลาง: ส่ง .text() ทุกรอบ แต่ส่ง ข้อความเดิม เฉลยของคาบนี้จึงเก็บข้อความสถานะไว้ในตัวแปร rec_msg ซึ่งเปลี่ยนเฉพาะตอนกดปุ่ม แล้วส่งซ้ำทุกรอบ · เฟิร์มแวร์เทียบข้อความเก่ากับใหม่ก่อนวาด ถ้าเท่าเดิมมันไม่วาดซ้ำ เราจึงจ่ายแค่ค่าส่งข้ามคอร์ ไม่ได้จ่ายค่าวาด และไม่มีตัวเลขไหนบนจอวิ่งเร็วกว่าที่คนอ่านทัน

คำสั่งที่ปลุกโหมดเร่งได้มีสี่กลุ่ม คือสร้าง widget · .text() · .pos() / .size() / .color() · และคำสั่งของ collection อย่าง .cell() .add_row() .prop() — ส่วน .value() ทั้งหมด ไม่ว่าจะเป็นแถบ ไฟ หรือ set_next() ของกราฟ ไม่ปลุก (ตรวจจาก ipc_ui.c โดยตรง)

อย่าตีความเกินกว่านี้ — โหมดเร่งไม่ได้ทำให้เซนเซอร์อ่านเร็วขึ้น และไม่ได้ทำให้ลูป Python เร็วขึ้น มันแค่ทำให้ คำสั่งที่เราส่งไปแล้ว ถูกวาดออกจอเร็วขึ้น

โหมดเร่งค้าง 500 ms 0 600 ms .text() .text() .text() ทุก 200 ms < 500 ms → ไม่มีวันหลุดโหมดเร่ง กราฟลื่น ปุ่มตอบไว ตัดบรรทัด .text() ออกเมื่อไร กราฟกลับไปกระตุกทันที

ลูปที่อัปเดตกราฟอย่างเดียวคือลูปที่ช้าที่สุด — ใส่ป้ายสถานะไว้ในลูปเดียวกันเสมอ ได้ทั้งความลื่นและได้ตัวเลขให้คนอ่าน

รู้จัก ui.Chart — สามคำสั่งที่ต้องจำ

chart = ui.Chart(x=24, y=52, w=292, h=144, min=-20, max=20, color=COL_AX)
s_ay = chart.add_series(COL_AY)      # คืนค่าเป็น "หมายเลข series"
s_az = chart.add_series(COL_AZ)
...
chart.set_next(0, int(ax))           # เติมจุดใหม่ให้ series 0
chart.set_next(s_ay, int(ay))        # เติมจุดใหม่ให้ series ที่เพิ่งเพิ่ม

ข้อที่หนึ่ง ตอนสร้าง ui.Chart มันสร้าง series 0 ให้อัตโนมัติ โดยใช้สีจาก color= เราจึงไม่ต้องเรียก add_series สำหรับเส้นแรก (COL_AX COL_AY COL_AZ คือ 0x4A9EFF 0x8E7BFF 0x2FB6A8 จากจานสีเส้นข้อมูล — บรรทัดจริงจาก solution_codes/s07_accel_chart.py↗)

ข้อที่สอง .add_series(color) คืนค่าเป็นตัวเลข ให้เก็บใส่ตัวแปรไว้เสมอ ถ้าไม่เก็บ ก็ไม่มีทางเติมข้อมูลให้เส้นนั้นได้อีกเลย ค่าที่คืนมาคือ 1 แล้ว 2 แล้ว 3 ตามลำดับที่เรียก เพราะ 0 ถูกจองไปแล้วตั้งแต่ตอนสร้าง เรียกครั้งที่สี่จะได้ RuntimeError: ui: add_series failed (max 4)

ข้อที่สาม min= กับ max= คือช่วงแกน Y ตั้งครั้งเดียวตอนสร้าง ไม่มี autoscale

ข้อที่สี่ set_next() เป็นคำสั่งแบบยิงแล้วลืม ส่ง idx ที่ไม่มีอยู่จริง เช่น set_next(3, ...) ทั้งที่มีแค่สามเส้น จะไม่มี error อะไรทั้งสิ้น ข้อมูลนั้นหายไปเฉย ๆ — ต่างจากค่าที่เป็นทศนิยม ซึ่งได้ TypeError ตั้งแต่ฝั่ง Python

max = +20 min = -20 series 2 series 1 series 0 (มาฟรีตอนสร้าง) เผลอ add_series ให้แกน X ด้วย = มีสี่เส้น และ series 0 ว่างตลอดกาล เพดาน 4 series ต่อกราฟหนึ่งใบ

add_series คืน "ชื่อเรียก" ของเส้น — ไม่เก็บไว้ เท่ากับสร้างเส้นที่เราส่งข้อมูลไปหาไม่ได้

เลือกช่วงแกน Y และสีของเส้น — งานที่ไม่มีใครทำแทนเรา

ui.Chart ไม่มี autoscale ค่าที่เกินช่วงจะถูกกดให้ติดขอบ ดูเผิน ๆ เหมือนสัญญาณอิ่มตัว ทั้งที่จริงคือเราตั้งกรอบไว้แคบไป

ความเร่งบนบอร์ดนี้: วางนิ่ง Z ราว 9.8 m/s² · X, Y ราว 0 · เขย่าแรงพุ่ง 15-20 · กระแทกโต๊ะทะลุ 30

ช่วงที่ตั้ง ผลที่ได้
−2 ถึง +2 เห็นการสั่นเล็ก ๆ ชัด แต่แกน Z ติดขอบบนตลอด ใช้ไม่ได้
−20 ถึง +20 เห็นทั้งแรงโน้มถ่วงและการเขย่า สมดุลที่สุดสำหรับคาบนี้
−100 ถึง +100 ไม่มีอะไรตกขอบ แต่เส้นแบนติดกลางจอ

สีคือภาษาที่คนอ่านกราฟใช้ร่วมกัน — สามเส้นนี้หยิบจาก "จานสีเส้นข้อมูล" ของหลักสูตร ไม่ใช่ แดง/เขียว/น้ำเงิน ตามธรรมเนียมเดิม

แกน สีที่เห็นบนจอ ค่าคงที่ในโค้ด
X ฟ้า 0x4A9EFF — เส้นที่ 1
Y ม่วง 0x8E7BFF — เส้นที่ 2
Z เขียวน้ำทะเล 0x2FB6A8 — เส้นที่ 3

ทำไมไม่ใช้ แดง/เขียว/น้ำเงิน — จานสีชุดนี้ไม่มีสีสถานะปนอยู่เลยสักตัว โดยตั้งใจ หน้าจอนี้มีไฟเตือนอยู่ด้วย และ §S7.7.2 ห้ามใช้สีของการแจ้งเตือนกับอย่างอื่นในหน้าจอเดียวกัน เส้น X สีแดงที่แปลว่า "แกน X" เฉย ๆ จะแย่งความหมายของแดงที่แปลว่า "ต้องรีบดู" ไปจนหมด

หน้า Sensor Dashboard ของเฟิร์มแวร์ ที่เห็นในสไลด์แรกยังใช้ แดง/เขียว/น้ำเงิน อยู่ ของเราจึงไม่เหมือนของมัน — ตั้งใจให้ไม่เหมือน นี่คือตัวอย่างจริงของหน้าจอที่เขียนก่อนจะมีกฎข้อนี้

ช่วงแคบไป — Z ติดขอบ อ่านไม่ได้ว่าจริง ๆ เท่าไร ±20 — สมดุล ช่วงแคบ = ละเอียด แต่เสี่ยงตกขอบ ช่วงกว้าง = ปลอดภัย แต่ไม่ชัด

clamp ก่อนส่ง (max(-20, min(20, ax))) ทำให้เรา รู้ตัว ว่ากำลังตัดข้อมูล · ถ้าคนดูต้องถามว่า "เส้นไหนคือแกนอะไร" กราฟยังไม่เสร็จ

ปุ่มหยุดบันทึกคือ flag ไม่ใช่การหยุดโปรแกรม

เวลาเห็นอะไรน่าสนใจวิ่งผ่านกราฟ เราอยากหยุดดู แต่ ห้ามหยุดลูป เด็ดขาด

เพราะถ้าลูปหยุด ui.poll() ก็หยุดตาม กฎเหล็กข้อแรกเล่นงานทันที — จอซ่อน widget ภายในราวสองวินาที ปุ่มเริ่มบันทึกที่จะกดก็หายไปด้วย กลายเป็นหยุดถาวรที่กู้ไม่ได้

running = True
while True:
    for ev in ui.poll():             # ต้องเรียกทุกลูป ไม่ว่าจะหยุดหรือไม่
        h = ev.get('handle')
        if h == start_id:
            running = True           # ปุ่มเริ่ม มีหน้าที่เดียว
            led_rec.value(1)
        elif h == stop_id:
            running = False          # ปุ่มหยุด มีหน้าที่เดียว
            led_rec.value(0)
    if running:
        chart.set_next(0, int(ax))   # เติมข้อมูลเฉพาะตอนเดิน
    time.sleep_ms(200)               # หน่วงเท่าเดิมเสมอ
หยุดข้อมูล — ลูปยังหมุน ui.poll() ยังถูกเรียกทุกรอบ กราฟนิ่ง · ปุ่มเริ่มบันทึกยังกดได้ หยุดลูป — จอตายถาวร ไม่มี ui.poll() → 2 วินาที widget หายทั้งจอ กู้ไม่ได้
ปุ่มเดียวที่สลับไปกลับ เขียนง่ายกว่า แต่ใช้งานแย่กว่า เพราะปุ่มที่เขียนว่า PAUSE บอกได้แค่ว่ากดแล้วจะเกิดอะไร ไม่ได้บอกว่า ตอนนี้อยู่สถานะไหน คนที่เดินมาเห็นจอกลางคันต้องตีความเอาเอง และตีความผิดได้เสมอ โดยเฉพาะตอนที่ป้ายบนปุ่มยังไม่ทันอัปเดต · แผงควบคุมจริงจึงแยกเป็น ปุ่มเริ่มกับปุ่มหยุดคนละใบ แต่ละใบมีหน้าที่เดียว กดซ้ำได้โดยไม่มีผลข้างเคียง แล้วให้ ui.Led เป็นคนบอกสถานะปัจจุบัน ไม่ใช่ให้ปุ่มบอก · กฎเดียวกันนี้ใช้กับปุ่มเปิด-ปิดของอุปกรณ์จริงทุกชนิด

หยุดข้อมูล ไม่ใช่หยุดลูป — ลูปที่ยังหมุนอยู่คือสิ่งเดียวที่ทำให้จอยังมีชีวิต

วัดคาบลูปจริง — เพราะ sleep_ms ไม่ใช่ความจริงทั้งหมด

เราสั่ง time.sleep_ms(200) แต่ลูปหนึ่งรอบไม่ได้ใช้เวลา 200 ms พอดี เพราะยังมีเวลาอ่านเซนเซอร์ ส่งข้าม IPC และอัปเดตป้ายอีกหลายใบ

Tloop=Tsleep+Tworkfs=1TloopT_{\text{loop}} = T_{\text{sleep}} + T_{\text{work}} \qquad f_s = \frac{1}{T_{\text{loop}}}

ไทย: คาบจริง = เวลาที่หน่วง + เวลาที่ทำงาน · ตัวเลขที่ควรอ่านได้ (วัดบน Eva Kit · Dev Kit อ่าน IMU คนละเส้นทาง ตัวเลขอาจต่าง ให้จดของทีมเอง): 205-215 ms ถ้าพุ่งถึง 300 ms แปลว่างานในลูปหนักกว่าที่คิด และ fsf_s ตกจาก 5 Hz เหลือ 3.3 Hz โดยที่โค้ดไม่บอกเราสักคำ

now = time.ticks_ms()                  # นาฬิกามิลลิวินาทีของระบบ
dt  = time.ticks_diff(now, last_ms)    # ผลต่างจากรอบที่แล้ว
last_ms = now

ทำไมต้อง ticks_diff แทน now - last_ms — ticks_ms() เป็นตัวนับที่ วนกลับไปเริ่มใหม่ เมื่อชนเพดาน ถ้าลบเองในจังหวะที่มันวนพอดี จะได้ค่าติดลบมหาศาลแบบไม่มีปี่มีขลุ่ย ส่วน ticks_diff รู้เรื่องการวนนี้และคืนค่าที่ถูกต้องเสมอ

หนึ่งรอบลูปประกอบด้วยอะไร อ่าน IPC ป้าย sleep 200 T_loop จริง ≈ 208 ms ตัวนับวนกลับ ลบเองตรงนี้ = ได้ค่าติดลบมหาศาล ticks_diff รู้เรื่องนี้ให้แล้ว

โปรแกรมที่วัดตัวเองได้ ดีบักง่ายกว่าโปรแกรมที่ต้องเดาเสมอ

ย้าย ย่อ ซ่อน — สี่เมธอดจากคาบ 4 ที่กราฟก็ใช้ได้เหมือนกัน

คาบ 4 เราเจอสี่เมธอดนี้กับกล่องสี่เหลี่ยมใบเดียว วันนี้ขอย้ำอีกครั้ง เพราะ Chart ก็รับคำสั่งชุดเดียวกันนี้ และคนส่วนใหญ่ไม่คิดถึงมันตอนออกแบบหน้าจอที่มีกราฟ

w.pos(x, y)      # ย้ายไปพิกัดใหม่ - สองอาร์กิวเมนต์ ขาดไม่ได้
w.size(cw, ch)   # เปลี่ยนขนาด - สองอาร์กิวเมนต์เหมือนกัน
w.hide()         # ยังอยู่ แต่ไม่วาด
w.show()         # กลับมาวาด

ทั้งสี่ตัวใช้ได้กับ widget ทุกชนิด ไม่เว้นแม้แต่ Chart, Panel หรือ Compass เพราะมันอยู่ในตารางเมธอดชุดเดียวกันหมด

สามข้อที่ต้องรู้ก่อนใช้

  1. รับจำนวนเต็มเท่านั้น และต้องครบสองตัว — w.pos(10) ได้ TypeError เรื่องจำนวนอาร์กิวเมนต์ ส่วน w.pos(10.5, 20) ได้ TypeError: can't convert float to int เหมือน set_next()
  2. ไม่มีตัวอ่านกลับ — ไม่มี .x() ไม่มี .y() ไม่มีอะไรถามตำแหน่งปัจจุบันได้เลย ย้ายไปไหนแล้ว เราต้องจำเอง ในตัวแปรฝั่ง Python
  3. .hide() ไม่ใช่ .delete() — ของที่ซ่อนยัง กินช่องในตาราง 64 ตัวอยู่เต็ม ๆ ส่วน .delete() คืนช่องให้ แต่ตัวแปร Python ยังชี้ไปที่ handle เดิมที่ตายแล้ว สั่งอะไรต่อก็เงียบ
ซ่อน กับ ลบ ไม่เหมือนกัน .hide() หายจากจอ · ช่องยังถูกจอง .show() เรียกกลับมาได้ทันที งบยังเท่าเดิม .delete() ช่องถูกคืน งบว่างขึ้นหนึ่ง ตัวแปรเดิมชี้ไปที่ของที่ตายแล้ว สั่งต่อ = ไม่มีอะไรเกิดขึ้น และไม่มี error ให้เห็นด้วย
เรื่องความเร็วซ่อนอยู่ตรงนี้ด้วย — .pos() กับ .size() ปลุก โหมดเร่ง ส่วน .show()/.hide() ไม่ปลุก หน้าที่สลับการ์ดด้วย .hide() ล้วน ๆ จึงตอบช้าแบบเดียวกับกราฟที่มีแต่ set_next() วิธีแก้เดียวกัน: มีป้ายสถานะ "ตอนนี้อยู่หน้าไหน" อัปเดตไปด้วย · ของจริง: examples/s04/07_find_move_hide_delete.py↗ นับ ui.list() ให้ดูว่าซ่อนแล้วตัวเลขไม่ลด แต่ลบแล้วลด

ย้ายของที่มีอยู่แล้วถูกกว่าสร้างใหม่เสมอ — ทั้งเรื่องงบ 64 ช่อง และเรื่องเวลาที่ CM55 ต้องใช้วาด

widget ที่เหลือของ ui — ตารางอ้างอิง กับกับดักประจำตัวของแต่ละตัว

คาบ 1-6 เราใช้ไปแล้วห้าหกตัว วันนี้เพิ่ม Chart ส่วนที่เหลือมีอยู่จริงและเรียกได้ทันที ตารางนี้ไม่ต้องท่อง แต่ต้องรู้ว่ามีอะไรอยู่ จะได้ไม่ไปเขียนเองในสิ่งที่เฟิร์มแวร์มีให้แล้ว

widget สร้างด้วยอะไร สั่งงานด้วยอะไร กับดักที่ต้องรู้
Chart min= max= color= .add_series(สี) → 1,2,3 · .set_next(idx, ค่า) series 0 มาฟรี · เพดาน 4 เส้น · เส้นละ 50 จุดปริยาย ตั้ง 10-400 ผ่าน .prop(ui.PROP_CHART_POINTS, n) (fw 2026-08-20 ขึ้นไป) · idx เกินถูกทิ้งเงียบ
DotMatrix cols= rows= (ไม่ใช่ min/max — สองตัวนั้นถูกเขียนทับ) .set_pixels(บัฟเฟอร์) 1 บิตต่อหนึ่งดอก MSB ก่อน ไหลต่อกันข้ามไบต์ ไม่มีการเติมให้ครบไบต์ท้ายแถว · เพดาน 16×16 ดอก และ 126 ไบต์ต่อครั้ง เกินกว่านั้นถูกตัดเงียบ · สีดอกตายตัว color= ไม่มีผล
Image text= คือ ชื่อไอคอน ไม่ใช่ข้อความ .icon(ชื่อ) · .set_image(RGB565) ชื่อผิดตอนสร้าง = RuntimeError แต่ชื่อผิดใน .icon() = เงียบสนิท · ผืนว่างถูกบีบไว้ที่ 48×48
Arc min= max= value= .value(n) ตั้ง · .value() อ่านกลับได้จริง เป็นของที่ คนลากได้ ส่ง value_changed เข้า poll() ด้วย ถ้าไม่อยากให้ลาก ใช้ Bar
Spinner มีแค่ x y w h ไม่มีอะไรให้สั่ง นอกจาก .show() / .hide() min max value text ถูกเมินหมด · ไม่ส่ง w มาจะกลายเป็น 80×80
Dropdown text="แดง\nเขียว\nน้ำเงิน" — ขึ้นบรรทัดใหม่คั่นตัวเลือก .value() คืน หมายเลขที่เลือก · poll() ส่ง value_changed value= คือ ขนาดฟอนต์ ไม่ใช่ตัวที่เลือก · ตัวเลือกทั้งชุดรวมกันต้องไม่เกิน 126 ไบต์
Textarea text= ข้อความตั้งต้น · value= ขนาดฟอนต์ .text("...") เขียนทับได้ อ่านกลับไม่ได้ .text() แบบไม่ใส่อาร์กิวเมนต์คืน None เสมอ และมันไม่ส่ง event เลยสักชนิด — เป็นช่องแสดงผล ไม่ใช่ช่องกรอก
Compass w= คือ เส้นผ่านศูนย์กลาง h ถูกเมิน .value(องศา) 0 คือทิศเหนือ รับจำนวนเต็ม · .value() อ่านกลับได้ 0 เสมอ จำองศาไว้เองฝั่ง Python
Panel color=พื้น min=สีขอบ max=รัศมี value=ความหนา — ไม่ได้เป็น "พ่อ" ของ widget ที่วางทับ ต้องสร้างก่อนเสมอ ไม่งั้นมันบังของอื่นหมด
Seg7 w h color .text("123") .value(n) ได้จำนวนเต็ม ถ้าต้องการทศนิยมต้อง .text()

ชื่อไอคอนที่มีให้ใช้ มีอยู่ 16 ชื่อพอดี ผิดจากนี้ไม่ขึ้น: heart star flag trophy skull arrow_up arrow_down arrow_left arrow_right check cross smiley car boat plane home

.set_image() มีจริงแต่เราจะไม่ใช้ในคาบ — ภาพ 48×48 แบบ RGB565 คือ 4,608 ไบต์ ซึ่งต้องหั่นส่งข้าม IPC ถึง 38 ครั้ง โดยมีการหน่วง 200 ไมโครวินาทีคั่นทุกครั้ง ราคานั้นแพงเกินสำหรับของที่อัปเดตในลูป ใช้ .icon() แทนถ้าต้องการรูปสัญลักษณ์

ของที่สองบอร์ดมีไม่เท่ากัน — ui.Sprite และค่าคงที่ SPR_* ทั้งชุด ไม่ถูกคอมไพล์เข้ามาในเฟิร์มแวร์ของ Eva Kit (ธง ENABLE_GAME_SPRITES ปิด) เขียนถึงมันบน Eva จะได้ AttributeError · บน TESAIoT Dev Kit ธงนี้เปิดอยู่ (Makefile.micropython ของโปรเจกต์นั้น ตั้งแต่ 2026-08-20) ui.Sprite จึงมีจริง — โค้ดที่ต้องรันได้ทั้งสองบอร์ดจึงห้ามพึ่งมัน หรือต้องตรวจ hasattr(ui, "Sprite") ก่อน · ส่วน ui.tone() กับ ui.sfx() มีทั้งสองบอร์ด เพราะมีชิปเสียงเหมือนกัน — examples/s08/05_door_open_switch.py↗ ใช้ ui.tone() อยู่จริง

ตัวไหนมีของให้ลองอยู่ที่ไหน — DotMatrix + .set_pixels() ที่ examples/s07/02_fft64_two_tones.py↗ (สเปกตรัมเป็นตารางไฟ) และ examples/s01/14_the_board_hears_you.py↗ · Image .icon() และ Spinner ที่ examples/s01/15_one_number_many_faces.py↗ · Dropdown กับ Textarea ที่ examples/s04/08_dropdown_textarea.py↗ · เหลือ .set_image() ตัวเดียวที่ไม่มีไฟล์ไหนใช้ ด้วยเหตุผลย่อหน้าบน

รู้ว่ามีอะไรอยู่ในกล่องเครื่องมือ สำคัญกว่าท่องวิธีใช้ทุกตัว — เปิดตารางนี้ตอนคิดไม่ออกว่าจะแสดงค่านี้ด้วยอะไรดี

ทั้งชุดเมธอดของ Widget มีอยู่เท่านี้ — สิบสี่ตัว ไม่มีมากกว่านี้

สไลด์ "ย้าย ย่อ ซ่อน" หยิบมาสี่ตัว สไลด์นี้ปิดบัญชี — เมธอดของอ็อบเจกต์ widget มีสิบสี่ชื่อ ไม่มีชื่อที่สิบห้า

เก้าตัวที่ใช้ได้กับ widget ทุกชนิด รวมทั้ง Chart เพราะทุกชนิดใช้ตารางเมธอดชุดเดียวกัน

เมธอด ทำอะไร คาบนี้ใช้ตรงไหน
.id() เลขประจำตัว 0-63 ที่ตรงกับ ev['handle'] ใช้ — แยกว่าปุ่มเริ่มหรือปุ่มหยุดถูกกด
.text() / .text("...") อ่านคืน None เสมอ / เขียนข้อความทับ ใช้ทุกรอบ — ป้ายค่าและป้ายสถานะ · และเป็นตัวที่ทำให้กราฟลื่นขึ้นตามสไลด์ "ทำไมกราฟช้า"
.value() / .value(n) อ่าน / สั่งค่า มีหกชนิดเท่านั้นที่ตอบได้จริง ไม่ใช้ในคาบนี้ · Chart ไม่ใช่หนึ่งในหกชนิดนั้น
.pos(x, y) .size(w, h) ย้าย / เปลี่ยนขนาดหลังสร้างแล้ว ใช้ตอนจัดหน้า · สองตัวนี้ ปลุกโหมดเร่ง ส่วน show/hide ไม่ปลุก
.color(0xRRGGBB) เปลี่ยนสี ใช้เป็นช่องรายงานสถานะได้ ตาอ่านสีเร็วกว่าตัวอักษร
.show() / .hide() ซ่อน-แสดงโดยไม่ลบ ของที่ซ่อนอยู่ ยังกินโควตา 64 เต็ม ๆ
.delete() ลบจริง คืนโควตาหนึ่งช่อง ตัวแปร Python ยังชี้ไปที่ handle ที่ตายแล้ว สั่งต่อก็เงียบ

สองตัวที่เป็นของ Chart ล้วน ๆ — หัวใจของคาบนี้

เมธอด ทำอะไร · กับดัก
.add_series(สี) คืนหมายเลขเส้น 1, 2, 3 ตามลำดับที่เรียก · เพดานสี่เส้น เรียกเกินได้ RuntimeError
.set_next(idx, ค่า) ยิงแล้วลืม · idx ผิดถูกทิ้งเงียบ · ค่าทศนิยมได้ TypeError ตั้งแต่ฝั่ง Python

สามตัวที่เป็นของฝั่งภาพ — คาบนี้ไม่ได้ใช้ในโครงหลัก

.icon(ชื่อ) เปลี่ยนไอคอนจาก 16 ชื่อที่มี · .set_image(RGB565) วางภาพ 48×48 · .set_pixels(บัฟเฟอร์) ของ DotMatrix — ตัวหลังโผล่จริงในตัวอย่างเสริม examples/s07/02_fft64_two_tones.py↗

และฝั่งตัวสร้างมีสิบหกตัว ตารางอ้างอิงข้างบนกางไว้สิบชนิด อีกหกตัวที่เหลือคือของที่เราใช้มาตั้งแต่คาบ 1-6 อยู่แล้ว: Label Button Switch Slider Checkbox Bar — บัญชีเต็มของทั้งโมดูล ui อยู่ในคาบ 4

เกร็ด: กราฟเวลาบอกได้ไม่หมด — โดเมนความถี่บอกส่วนที่เหลือ

ภาพ: Lucas Vieira, Wikimedia Commons, สาธารณสมบัติ — สัญญาณเดียวกัน มองจากสองโดเมน
Byte-Sized Learning · 5:01 · EN — decimation: สุ่มเร็วมากแล้วค่อยลดอัตราลงทีหลัง

กราฟเวลาที่เราทำวันนี้ตอบได้ว่า "ค่าเปลี่ยนไปยังไงตามเวลา" แต่ตอบไม่ได้ว่า "การสั่นนี้ประกอบด้วยความถี่อะไรบ้าง"

ภาพขวาคือข้อมูลจริงจากงานเฝ้าระวังตลับลูกปืน แถวบนเป็นโดเมนเวลา แถวล่างเป็นสเปกตรัมของข้อมูล ชุดเดียวกันเป๊ะ — กราฟเวลาของตัวปกติกับตัวเสียดูคล้ายกันมาก แต่สเปกตรัมแยกออกทันที

นี่คือสาเหตุที่งาน predictive maintenance ในโรงงานทำ FFT ก่อนตัดสินใจ ไม่ได้ดูกราฟเวลาอย่างเดียว สเปกตรัมคือที่ที่ความต่างซ่อนอยู่ ทั้งที่กราฟเวลาบอกไม่ได้

เชื่อมกับวันนี้: ที่ fs=5f_s = 5 Hz เราทำ FFT ที่มีประโยชน์ไม่ได้ เพราะเห็นได้แค่ถึง 2.5 Hz ส่วนความสั่นของเครื่องจักรจริงอยู่หลักกิโลเฮิรตซ์ — อัตราแบบนั้นลูป Python เอื้อมไม่ถึง ต้องให้ เฟิร์มแวร์เป็นคนสุ่มตัวอย่าง แล้วส่งค่าที่ยุบแล้วขึ้นมาให้ เหมือนที่โมดูล mic ทำในคาบ 8

โมดูล dsp มี FFT ในตัวแล้ว: dsp.fft_mag(ชุด, n=256) คืนสเปกตรัม n/2 ช่อง จบใน C ราวหนึ่งมิลลิวินาที (เพิ่ม 2026-08-20 — firmware รุ่นก่อนหน้ายังไม่มี · ulab ยังไม่มีเช่นเดิม) — ข้างในของมันคือ radix-2 ที่สไลด์อ่านเสริมท้ายเด็คแกะให้ดูทั้งตัว

ภาพ: Kolok P. et al., Sensors 25(21):6610 (2025), CC BY 4.0 — โดเมนเวลา

ภาพ: Kolok P. et al., Sensors 25(21):6610 (2025), CC BY 4.0 — โดเมนความถี่ของข้อมูลชุดเดียวกัน

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

70% — เฟิร์มแวร์ทำให้แล้ว 30% — งานของเรา ไดรเวอร์ BMI270 · วาดเส้น/กริด · ring buffer · IPC · นาฬิกา สุ่มถี่แค่ไหน · แกน Y เท่าไร สีอะไร · หยุดดูได้ไหม · ทันไหม

สิ่งที่เฟิร์มแวร์ทำให้แล้ว (70%)
ไดรเวอร์ BMI270 และการอ่านหกแกนจากการอ่านครั้งเดียว (Eva: snapshot ของคอร์จอ · Dev Kit: ล็อกบัสของ CM33) · การวาดกราฟ เส้น กริด และการเลื่อนบัฟเฟอร์บน CM55 · การส่งคำสั่งข้าม IPC พร้อมโหมดเร่ง · การรับสัมผัสจากจอแล้วแปลงเป็น event · นาฬิกาของระบบ

สิ่งที่เป็นงานของเรา (30%)
ตัดสินใจว่า สุ่มถี่แค่ไหน · ช่วงแกน Y เท่าไร · เส้นไหนสีอะไร · ให้ผู้ใช้หยุดดูได้ไหม · และ จะรู้ได้ยังไงว่าลูปเรายังทันจริง

ห้าข้อนี้ไม่มีใน API เล่มไหน มันคือการออกแบบระบบวัด ซึ่งเป็นงานของวิศวกร ไม่ใช่งานของไลบรารี

ไลบรารีวาดเส้นให้เราได้ แต่ตัดสินใจแทนเราไม่ได้สักข้อ

อุ่นเครื่องก่อนจับเซนเซอร์ — เครื่องกำเนิดสัญญาณ

ก่อนต่อเซนเซอร์จริง ลองป้อนสัญญาณที่เรารู้คำตอบล่วงหน้าเข้ากราฟดูก่อน วิธีนี้แยกได้ชัดว่า ถ้ากราฟเพี้ยน มันเพี้ยนที่กราฟหรือเพี้ยนที่เซนเซอร์

import ui, time, math
ui.screen()
ch = ui.Chart(x=40, y=60, w=700, h=250, min=-100, max=100, color=0x4A9EFF)
lbl = ui.Label("i = 0", x=40, y=330)          # ปลุกโหมดเร่งทุกรอบ

for i in range(200):
    val = int(90 * math.sin(i * 0.15))               # ไซน์
    # val = 80 if (i // 8) % 2 == 0 else -80         # สี่เหลี่ยม
    # val = (i % 20) * 9 - 90                        # ฟันเลื่อย
    ch.set_next(0, val)
    lbl.text("i = {}".format(i))
    ui.poll()
    time.sleep_ms(100)

ลองสลับบรรทัดที่คอมเมนต์ไว้ทีละแบบ แล้วดูว่ารูปคลื่นบนจอตรงกับที่คิดไหม จากนั้นเปลี่ยน 0.15 เป็น 0.6 — ไซน์จะเริ่มดูไม่เหมือนไซน์ เพราะเราสุ่มไม่ทันมันแล้ว นั่นคือ aliasing ในสิบบรรทัด

สังเกตบรรทัด lbl.text(...) ที่เพิ่มเข้ามา — นั่นคือกฎจากสไลด์ก่อนหน้า ลองลบมันออกแล้วรันใหม่ กราฟจะเดินช้าลงอย่างเห็นได้ชัดทั้งที่ sleep_ms เท่าเดิม

รูปคลื่นสามแบบ ไซน์ สี่เหลี่ยม ฟันเลื่อย

ภาพ: Russ Puskarcik, Wikimedia Commons, CC BY 3.0 — ADC ไล่หาค่าทีละบิต

ทดสอบด้วยสัญญาณที่เรารู้คำตอบก่อนเสมอ แล้วค่อยเอาของจริงเข้าไป

แกะโค้ดจริง — ท่าที่ 1 รอให้เซนเซอร์พร้อม แล้วเคลียร์จอ

# --- ท่าที่ 1: เตรียมเซนเซอร์และหน้าจอให้พร้อม ---
# ไม่มี sensors.init() ทั้งสองบอร์ด (Eva: ได้ OSError / Dev Kit: ไม่จำเป็น)
try:
    sensors.bmi270.motion()          # อ่านทิ้งหนึ่งครั้ง ให้การรอไปเกิดตรงนี้
except OSError:
    print("อ่านเซนเซอร์รอบแรกยังไม่ได้ - ลองใหม่ในลูป")

ui.clear()
time.sleep_ms(200)
ค่าขยะอยู่บนกราฟนาน 10 วินาที ค่าขยะ ค่าที่เชื่อถือได้ อ่านทิ้งหนึ่งครั้งก่อนเข้าลูป ตัดปัญหานี้ทิ้งเลย

ไม่ต้องเรียก sensors.init() ทั้งสองบอร์ด — บน Eva Kit เรียกแล้วได้ OSError ทันที เพราะคอร์จอ (CM55) เป็นเจ้าของบัส I2C ตัวนั้น ค่าเซนเซอร์มาจากภาพสแกนที่ CM55 เก็บไว้ให้ · บน Dev Kit CM33 อ่าน IMU ตรงจาก I2C เอง และเฟิร์มแวร์ปลุกมันไว้ตั้งแต่บูต · ทั้งสองทางจึงเรียก sensors.bmi270.motion() ได้ตรง ๆ โค้ดชุดเดียวกันรันได้ทั้งคู่

การอ่านครั้งแรกทำหน้าที่แทนการหน่วงเวลา — หลังรีเซ็ต การอ่านครั้งแรกอาจต้องรอเซนเซอร์ตอบ (Eva วัดได้: CM55 เริ่มตอบที่ราว 13 วินาที การอ่านแรกรอได้ถึง 16 วินาที · Dev Kit ยังไม่ได้วัด) ครั้งต่อ ๆ ไปไม่เกิน 1 วินาที เราอ่านทิ้งหนึ่งครั้ง ก่อนสร้างกราฟ เพื่อไม่ให้ค่าชุดแรกที่ยังไม่นิ่งไปค้างในบัฟเฟอร์กราฟนาน 10 วินาที · ui.clear() ล้าง widget ของโปรแกรมก่อนหน้าออกให้หมด เริ่มจากสถานะที่รู้แน่

จำกฎเหล็กข้อที่สี่: การใช้ ui.* ครั้งแรกจะหยุด sensor auto-task — เลือกใช้ ui เมื่อไร ก็ต้องรับหน้าที่อ่านเซนเซอร์เองในลูปเมื่อนั้น

แกะโค้ดจริง — ท่าที่ 2 สร้างกราฟและเส้นทั้งสาม

# --- ท่าที่ 2: กราฟหนึ่งใบ สามเส้น และตารางสรุปข้าง ๆ ---
chart = ui.Chart(x=24, y=52, w=292, h=144, min=-20, max=20, color=COL_AX)
s_ay = chart.add_series(COL_AY)
s_az = chart.add_series(COL_AZ)
tbl = ui.Table(x=332, y=52, w=436, h=280, cols=4, value=16)
tbl.col_width(0, 100)          # กว้างพอสำหรับข้อความที่ยาวที่สุด
tbl.col_width(1, 108)
tbl.col_width(2, 108)
tbl.col_width(3, 108)
tbl.add_row("แกน", "ต่ำสุด", "สูงสุด", "ล่าสุด")
tbl.add_row("X", "-", "-", "-")
tbl.add_row("Y", "-", "-", "-")
tbl.add_row("Z", "-", "-", "-")
จอ Playground 792 × 398 Chart 292 × 144 ไฟ + คาบลูป ปุ่มสองใบ สูง 88 Table 436 × 280 อย่า add_series ให้แกน X series 0 มาฟรีแล้วตอนสร้าง Chart

บรรทัดแรกได้กราฟ พร้อม series 0 สีฟ้า (COL_AX) มาเลย — จุดที่พลาดบ่อยที่สุดคือเผลอเรียก add_series ให้แกน X อีกครั้ง กลายเป็นสี่เส้นแล้วเส้นแรกไม่มีข้อมูลตลอดกาล · s_ay กับ s_az ได้ 1 และ 2 แต่ ไม่ควรพิมพ์ 1 กับ 2 ลงไปตรง ๆ สลับลำดับการสร้างเมื่อไร เลข hardcode จะชี้ผิดเส้นทันที

ui.Table ไม่ใช่ของซ้ำซ้อนกับกราฟ กราฟเก็บได้เท่าจำนวนช่อง (ปริยาย 50) ถาม "เมื่อครู่แรงสุดเท่าไร" จึงไม่มีคำตอบ ตารางเก็บค่าต่ำสุด-สูงสุดในตัวแปรฝั่ง Python · ระวังความกว้างคอลัมน์ แคบกว่าข้อความที่ยาวที่สุดเมื่อไร ข้อความขึ้นบรรทัดใหม่ แล้ว ทุกแถวสูงเป็นสองเท่า แถว Z หายใต้ขอบโดยไม่มี error · value= ของ ui.Table คือ ขนาดฟอนต์ในช่อง ไม่บอกจะได้ 20 · กราฟเตี้ยลงเหลือ 144 px เพื่อเหลือคอลัมน์ซ้ายใต้กราฟให้ไฟ คาบลูป และปุ่มสูง 88 px สองใบ

add_series คืนเลขอะไรมา ให้เก็บไว้ใช้ อย่าเดาเอง อย่าพิมพ์เอง

แกะโค้ดจริง — ท่าที่ 3 ปุ่มกับ flag

# --- ท่าที่ 3: ไฟบอกสถานะ และปุ่มเริ่มกับปุ่มหยุดที่แยกกัน ---
led_rec = ui.Led(x=24, y=212, w=48, h=48, color=COL_RUN, value=1)
lbl_rec = ui.Label("กำลังบันทึก", x=88, y=216, color=COL_TEXT, value=20)
rec_msg = "กำลังบันทึก"       # ข้อความสถานะล่าสุด เปลี่ยนเฉพาะตอนกดปุ่ม
lbl_dt = ui.Label("คาบลูป -- ms", x=88, y=252, color=COL_DIM, value=20)
btn_start = ui.Button("เริ่มบันทึก", x=24, y=296, w=128, h=88,
                      color=0x30A46C, value=20)
btn_stop = ui.Button("หยุดบันทึก", x=184, y=296, w=128, h=88,
                     color=0x3A4150, value=20)
start_id = btn_start.id()
stop_id = btn_stop.id()
ui.poll()
running = True                # ตัวแปรที่ต้องจำสถานะข้ามรอบ ต้องเกิดนอกลูป
ประกาศนอกลูป running = True while True: ค่าคงอยู่ข้ามรอบ ประกาศในลูป while True: running = True ← รีเซ็ตทุกรอบ

ปุ่มสูง 88 px ตามขนาดเป้าสัมผัสของหลักสูตร — เดิมอยู่ในการ์ดสูง 64 px แล้วยื่นพ้นจอ กดไม่ได้ ตอนนี้วางเป็นสามชั้นในคอลัมน์ซ้ายใต้กราฟแทน · .id() คือหมายเลขประจำตัวของปุ่ม เก็บไว้เทียบกับ ev.get('handle') ตอนรับ event เพราะบนจอมีสองปุ่ม เราต้องรู้ว่าใครถูกกด · ปุ่มบนจอส่ง event ชนิด 'clicked' เท่านั้น (Switch ส่ง 'toggled' Slider ส่ง 'value_changed' — คาบ 4)

ปุ่มเริ่มกับปุ่มหยุดแยกกันคนละใบ ไม่ใช่ปุ่มเดียวสลับ ปุ่มที่เขียนว่า PAUSE บอกได้แค่ว่ากดแล้วจะเกิดอะไร ไม่ได้บอกว่าตอนนี้อยู่สถานะไหน คนที่เดินมาเห็นจอกลางคันจึงต้องเดา · ตัวที่บอกสถานะคือ ui.Led กับป้ายข้าง ๆ และ .value(0) ทำให้ไฟ หรี่ ไม่ใช่หาย โดยตั้งใจ · running = True ประกาศไว้ นอกลูป — ประกาศในลูปเมื่อไร มันถูกตั้งเป็น True ใหม่ทุกรอบ กดหยุดยังไงก็ไม่หยุด เป็นบั๊กที่หาไม่เจอง่าย ๆ เพราะโค้ดดูถูกทุกบรรทัด

แกะโค้ดจริง — ท่าที่ 4 อ่านเซนเซอร์แล้วป้อนกราฟ

# --- ท่าที่ 4: อ่านหกแกนจาก snapshot ชุดเดียว แล้วป้อนสามเส้น ---
if running:
    try:
        ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
    except OSError:
        pass                  # อ่านพลาดหนึ่งรอบ ใช้ค่าเดิมไปก่อน
    chart.set_next(0, int(ax))
    chart.set_next(s_ay, int(ay))
    chart.set_next(s_az, int(az))
    note(0, ax)               # จำต่ำสุด-สูงสุดไว้ให้ตาราง
    note(1, ay)
    note(2, az)

motion() คืนหกค่าในการอ่านครั้งเดียว เราใช้แค่สามค่าแรก แต่ยังต้องรับให้ครบหกตัวแปร ไม่งั้น Python จะฟ้อง ValueError · ห่อ try ไว้เพราะอ่านพลาดหนึ่งรอบไม่ควรทำให้กราฟดับทั้งหน้า

ค่าเดียว สองปลายทาง ax = 9.78 กราฟ int() → 9 ป้าย +9.78 ปลายทางเครื่อง กับ ปลายทางคน คนละรูปแบบ ข้อมูลเดียวกัน
Bosch Sensortec · 1:01 — ทบทวนว่าตัวเลขสามตัวที่วิ่งเข้ากราฟนี้เกิดขึ้นได้อย่างไรในชิป (คลิปเดียวกับคาบ 6)

ทำไมไม่เรียก acceleration() ที่คืนสามค่าพอดี — เพราะ motion() ให้หกแกน จากการอ่านครั้งเดียวกัน (Eva: snapshot ชุดเดียวจากคอร์จอ · Dev Kit: CM33 ล็อกบัส I2C ของตัวเองครั้งเดียว) ค่าจึงสอดคล้องกันและถามแค่รอบเดียว — เหตุผลเดียวกับคาบ 6 · เราส่ง int(ax) เข้ากราฟ แต่ส่ง ax ตัวเต็มให้ note() — กราฟรับได้แค่จำนวนเต็ม แต่คนอ่านอยากเห็นทศนิยม ค่าสุดขีดจึงเก็บด้วยความละเอียดเต็มไว้ฝั่ง Python แล้วค่อยจัดรูปตอนเขียนลงตาราง · note() ตั้งต้นด้วย None ไม่ใช่ 9999 เพราะ None แปลว่า "ยังไม่เคยวัด" ตรง ๆ — ค่าเดียวกันแปลงคนละแบบตามว่าปลายทางเป็นเครื่องหรือคน

แกะโค้ดจริง — ท่าที่ 5 นาฬิกาจับตัวเอง

# --- ท่าที่ 5: วัดคาบลูปจริงแล้วรายงาน ---
now = time.ticks_ms()
dt = time.ticks_diff(now, last_ms)
last_ms = now

sec = time.ticks_ms() // 1000        # ตัวเลขเปลี่ยนวินาทีละครั้ง
if sec != last_sec:
    last_sec = sec
    for i, v in enumerate((ax, ay, az)):
        tbl.cell(i + 1, 1, cell(lo[i]))    # แถว 0 คือหัวตาราง
        tbl.cell(i + 1, 2, cell(hi[i]))
        tbl.cell(i + 1, 3, "{:+.2f}".format(v))
    lbl_dt.text("คาบลูป {} ms".format(dt))

time.sleep_ms(PERIOD_MS)
อ่าน dt แล้วแปลว่าอะไร 205-215 ms ปกติ · fs ≈ 4.8 Hz 240-280 ms งานในลูปเริ่มหนัก · fs ตก > 300 ms fs 3.3 Hz — Nyquist เหลือ 1.6 Hz ตัวเลขนี้คือหลักฐาน ไม่ใช่ของประดับ

สามบรรทัดแรกคือแพตเทิร์นมาตรฐานของการวัดคาบ: จับเวลาตอนนี้ ลบด้วยเวลารอบที่แล้ว แล้วจำเวลาตอนนี้ไว้ใช้รอบหน้า

ค่า dt รอบแรกจะแปลก ๆ เพราะยังไม่มีรอบก่อนหน้าให้เทียบ เราจึงตั้งค่าตั้งต้นให้มันไว้ก่อนเข้าลูป

แกะโค้ดจริง — ท่าที่ 5 (ต่อ) กราฟกับตัวเลขเดินคนละจังหวะ และตัวเลขที่ควรอ่านได้

เส้นกราฟกับตัวเลขในตารางเดินคนละจังหวะโดยตั้งใจ เส้นกราฟถูกป้อนทุกรอบ คือ 5 ครั้งต่อวินาที เพราะตาคนอ่าน "รูปร่าง" ได้โดยไม่ต้องหยุดอ่าน ส่วนตัวเลขทศนิยมในตารางเขียนใหม่ ไม่เกินวินาทีละครั้ง — เลขที่วิ่งห้าครั้งต่อวินาทีคือเลขที่อ่านไม่ทัน · .cell(แถว, คอลัมน์, ข้อความ) เขียนทับช่องเดิม ไม่ได้เพิ่มแถวใหม่ ต่างจาก .add_row() ที่ใช้ตอนสร้าง

ตัวเลขที่ควรอ่านได้ (วัดบน Eva Kit · บน Dev Kit ยังไม่ได้วัด จดของทีมเอง) ราว 205-215 ms ถือว่าปกติดี ถ้าพุ่งถึง 300 ms ให้สงสัยว่าอัปเดตป้ายมากเกินจำเป็น ลองลดจำนวนป้ายที่อัปเดตทุกรอบลงแล้ววัดใหม่ — แต่ อย่าลดจนเหลือศูนย์ ไม่งั้นโหมดเร่งจะหลุด แล้วกราฟจะกระตุกแทน

ตัวเลขนี้คือ หลักฐาน ไม่ใช่ของประดับ — เอาไว้ตอบคำถามว่าลูปเรายังทันไหม

ข้อมูลไหลไปทางไหน — จากการเขย่ามือถึงเส้นบนจอ

ทุกจุดบนกราฟ เดินทางผ่านห้าด่านนี้ BMI270 ความเร่งจริง ต่อเนื่อง CM33 · Python motion() ทุก 200 ms int() + set_next() คิว IPC 64 ช่อง ล้นแล้วทิ้งเงียบ CM55 · LVGL ระบาย 80 หรือ 3,200/s บัฟเฟอร์ 50 จุด จอ 4.3" เส้นสามสี เราคุมได้ตรงนี้: ความถี่ · แกน Y · int() · มี .text() ไหม ตรงนี้ เฟิร์มแวร์จัดการให้แล้ว กราฟกระตุกหรือจุดหาย ให้สงสัยด่านที่สามกับสี่ก่อนเสมอ

เห็นเส้นทางนี้แล้ว จะรู้ว่าเวลากราฟผิดปกติ ควรไปดูที่ จังหวะการส่ง ก่อนไปโทษเซนเซอร์

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

1 · Playground ค้างหน้านี้ไว้ 2 · เติม pass บนลงล่าง ทีละจุด 3 · วางนิ่งก่อน Z ต้องอยู่แถว 9-10 4 · เขย่าเบา ๆ ดูรอยเลื่อนซ้าย 5 · กด หยุด เขย่าตอนหยุดบันทึกแล้วกราฟยังวิ่ง = flag ยังไม่ได้ถูกใช้จริงในลูป
  1. บนจอบอร์ด แตะการ์ด BENTO Playground แล้วค้างหน้านี้ไว้
  2. บนคอม เปิด BENTO IDE เชื่อมต่อบอร์ด แล้วเปิดไฟล์ practise_codes/s07_accel_chart.py↗
  3. เติมช่องว่าง pass ไล่จากบนลงล่างทีละจุด แล้วส่งขึ้นบอร์ดดูผลทุกจุด อย่าเติมรวดเดียวหกจุด
  4. กด Program to Device แล้วรอที่บรรทัดอ่านค่าครั้งแรก — ถ้าเพิ่งรีเซ็ตบอร์ด (ถอด USB แล้วเสียบกลับ) บรรทัดนั้นรอได้พักใหญ่ (บน Eva วัดได้ถึง 16 วินาที) ห้ามคิดว่าค้าง
  5. วางบอร์ดนิ่ง ๆ ก่อน ดูว่าเส้นเขียวน้ำทะเล (Z) ลอยอยู่แถว 9-10 ส่วนอีกสองเส้นอยู่แถวศูนย์
  6. เขย่าเบา ๆ แล้วดูเส้นกระเพื่อม จากนั้นกด หยุดบันทึก แล้วเขย่าซ้ำ — กราฟต้องนิ่งสนิท แต่ไฟต้องหรี่ลง ไม่ใช่หายไป
  7. กด เริ่มบันทึก แล้วดูว่าเส้นกลับมาวิ่งต่อ และตารางเริ่มนับค่าสุดขีดใหม่จากศูนย์

โปรแกรมนี้เป็นลูปไม่รู้จบ หยุดด้วย Ctrl+C ที่คอนโซล หรือกด RESTART บนหน้า Playground

เขย่าตอนหยุดบันทึกแล้วกราฟยังวิ่ง แปลว่า flag ยังไม่ได้ถูกใช้จริงในลูป

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

กราฟ accel 3 แกนวิ่งสด + ตารางสรุปค่าสุดขีด + ปุ่มเริ่มกับปุ่มหยุดที่แยกกัน

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

  • [ ] จอมีกราฟหนึ่งใบ มีเส้น สามสีแยกกันชัดเจน (X ฟ้า, Y ม่วง, Z เขียวน้ำทะเล)
  • [ ] วางบอร์ดนิ่ง เส้น Z อยู่ราว 9-10 ส่วน X และ Y อยู่ราวศูนย์
  • [ ] เขย่าบอร์ดแล้วทั้งสามเส้นตอบสนองทันที และรอยนั้นเลื่อนไปทางซ้าย
  • [ ] ตารางขวามือขึ้นครบสี่แถว ไม่มีช่องไหนตกบรรทัด และแถว Z ไม่หายไปใต้ขอบ
  • [ ] เขย่าแล้วค่าสูงสุดในตารางขยับขึ้นและ ไม่ลดกลับเอง ส่วนค่าล่าสุดวิ่งตามตัวเลขปัจจุบัน
  • [ ] ตัวเลขในตารางเปลี่ยน วินาทีละครั้ง ส่วนเส้นกราฟยังเดินห้าครั้งต่อวินาที (จ้องดูสิบวินาทีแล้วนับ)
  • [ ] กดหยุดบันทึกแล้วเขย่าแรง ๆ กราฟต้อง ไม่ขยับเลย ไฟหรี่ลง (ไม่ใช่หายไป) และป้ายเปลี่ยนเป็น "หยุดแล้ว"
  • [ ] กดเริ่มบันทึกแล้วกราฟกลับมาวิ่งต่อ ค่าสุดขีดในตารางถูกล้างกลับเป็นขีด สลับไปกลับได้อย่างน้อยสามรอบ
  • [ ] มีป้ายบนจอบอกคาบลูปจริงเป็นมิลลิวินาที และค่าอยู่ในช่วงที่อธิบายได้
  • [ ] รันต่อเนื่อง 3 นาทีโดยไม่ค้างและไม่มี error

ข้อที่เจ็ดคือหัวใจ — ปุ่มที่กดแล้วไฟเปลี่ยนแต่ข้อมูลยังวิ่ง คือปุ่มที่ยังไม่ทำงาน

กับดักที่เจอบ่อย (1/2)

อาการ สาเหตุที่แท้จริง วิธีแก้
TypeError ตอนเรียก set_next ส่งทศนิยมเข้าไปตรง ๆ ครอบด้วย int() ทุกครั้ง
NameError: name 's_az' ยังไม่ได้เติมช่อง add_series แต่โค้ดข้างล่างอ้างถึงแล้ว เติมไล่จากบนลงล่าง
มีสี่เส้นแทนที่จะเป็นสาม เผลอ add_series ให้แกน X ด้วย ทั้งที่ series 0 มีมาแล้ว ใช้ set_next(0, ...) กับแกน X
กราฟเดินอืด ๆ ทั้งที่ลูปตั้ง 200 ms ลูปมีแต่ set_next() ซึ่งไม่ปลุกโหมดเร่ง — CM55 ระบายแค่ 80 คำสั่ง/วินาที เพิ่ม lbl.text(...) ในลูปเดียวกัน
กราฟกระตุก จุดหายเป็นช่วง ลูปเร็วเกิน คำสั่งล้นคิว IPC 64 ช่องแล้วถูกทิ้ง เพิ่ม sleep_ms กลับไปที่ 200
เส้นแบนติดขอบบนตลอด ช่วงแกน Y แคบไป แกน Z ชนเพดาน ขยายเป็น min=-20, max=20
กดหยุดบันทึกแล้วกราฟยังวิ่ง if running: ไม่ได้ครอบส่วนที่ป้อนข้อมูล ย้าย set_next เข้าไปใต้ if running:
กดหยุดบันทึกแล้ว widget หายทั้งจอ ไปหยุดลูป ทำให้ ui.poll() ไม่ถูกเรียก การหยุดคือ flag ห้ามหยุดลูป

กับดักที่เจอบ่อย (2/2)

อาการ สาเหตุที่แท้จริง วิธีแก้
ตารางขึ้นแค่สองแถวครึ่ง แถว Z หายไป คอลัมน์แคบกว่าข้อความ ข้อความตกบรรทัด ทุกแถวเลยสูงสองเท่า ขยาย .col_width() จนไม่มีช่องไหนตกบรรทัด
ตัวหนังสือไทยในตารางขึ้นเป็นกล่องสี่เหลี่ยม ช่องของตารางวาดจากคนละส่วนกับ Label เฟิร์มแวร์ตั้งฟอนต์ไทยที่ส่วน ITEMS ให้แล้ว ถ้ายังเป็นกล่อง ให้แจ้งผู้สอน
ไฟดับแล้วยังเห็นเป็นวงจาง ๆ ตั้งใจ .value(0) คือหรี่ ไม่ใช่หาย ไฟที่หายไปทำให้แยกไม่ออกว่าดับหรือจอเสีย
เส้นกราฟกระตุกทั้งที่ลูปยังเดิน ทั้งลูปมีแต่ .value() กับ set_next() ซึ่งไม่ปลุกโหมดเร่ง ส่ง .text() อย่างน้อยหนึ่งครั้งต่อรอบ ส่งข้อความเดิมก็ได้
กดปุ่มแล้วไม่มีอะไรเกิดขึ้น เทียบ handle กับ id ผิดตัว หรือลืมเก็บ btn.id() ตรวจว่าเก็บ id ไว้ก่อนเข้าลูป
ค่าเซนเซอร์ค้างตัวเลขเดิม หลังใช้ ui.* auto-task หยุด แต่เราไม่ได้อ่านเองในลูป อ่าน motion() ทุกรอบ
เขย่าเร็วแล้วกราฟโชว์คลื่นช้าแปลก ๆ aliasing — เขย่าเกิน 2.5 Hz ที่ fsf_s 5 Hz รับไหว ไม่ใช่บั๊ก บันทึกไว้

สิบสองในสิบห้าข้อนี้ไม่มี error message — มีแต่ "กราฟดูแปลก ๆ" หรือ "ตารางดูแปลก ๆ" ต้องอ่านอาการเป็น

ลงมือทำ — เติมช่องว่างในไฟล์ฝึก

เปิด practise_codes/s07_accel_chart.py↗ มีช่องว่างให้เติม 6 จุด

# เติม: s_az = chart.add_series(COL_AZ)
pass

if running:
    ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
    # เติม: chart.set_next(0, int(ax))
    pass

# เติม: dt = time.ticks_diff(now, last_ms)
pass

for ev in ui.poll():
    h = ev.get('handle')
    if h == start_id:
        # เติม: running = True
        pass
    elif h == stop_id:
        # เติม: running = False
        pass

# เติม: time.sleep_ms(PERIOD_MS)
pass
เติมเป็นสามขั้น แล้วรันทุกขั้น ขั้น 1 · จุดที่ 1-2 ควรเห็นสามเส้นวิ่ง ขั้น 2 · จุดที่ 3 ควรเห็นตัวเลขคาบลูป ขั้น 3 · จุดที่ 4-6 ทดสอบปุ่มสลับไปกลับ

ลำดับการเติมสำคัญ — โค้ดข้างล่างอ้างถึงตัวแปรที่เกิดจากช่องว่างข้างบน

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

ต้องทำในคาบ · เปิดตามลำดับนี้ ทั้งชุดราว 20 นาที

ลำดับ · เรื่อง · เวลา ไฟล์ ลงมือทำอะไร แล้วจะเข้าใจอะไร
1 · สุ่มช้าไปแล้วได้ความถี่ผี · 8 นาที examples/s07/03_aliasing_nyquist.py↗ ตั้งค่า PERIOD_MS ของทีมโดยมีเหตุผลรองรับ แทนที่จะเดาแล้วมาแก้ทีหลัง · จะเข้าใจว่าสุ่มช้าเกินไป แล้วได้ความถี่ที่ไม่เคยมีอยู่จริงบนกราฟ
2 · เฝ้าการสั่นด้วยเกณฑ์ที่วัดเอง · 12 นาที examples/s07/01_imu_vibration_monitor.py↗ ลอกโครงลูปอ่านเซนเซอร์ เส้นเกณฑ์ และปุ่มหยุดกราฟ ไปใส่ไฟล์ของทีมได้ทันที · จะได้รูปร่างของงานเฝ้าการสั่น คือกราฟคู่กับเส้นเกณฑ์ที่วัดมาเอง ไม่ใช่ตัวเลขที่หยิบมาจากที่อื่น

ติดตรงไหน เปิดอันนี้

อาการที่เจอ ไฟล์ที่ตอบอาการนั้น
กดปุ่มเริ่ม/หยุดแล้วจอไม่เปลี่ยนอะไรเลย examples/s04/02_event_types.py↗ — Button ส่ง clicked ส่วน Switch ส่ง toggled คนละชนิดกัน ดักผิดชนิดคือเงียบ

ตัวอย่างชุดคาบ 7 (ต่อ) — อ่านเสริมนอกเวลา: FFT ด้วยมือ แล้วของจริงใน C

อ่านเสริมนอกเวลา — เรื่องนี้อยู่นอกเกณฑ์ผ่านของคาบ 7 เพราะ MVP วันนี้วัดที่กราฟสามเส้น ตาราง และปุ่มเริ่ม/หยุดบันทึก ไม่ได้วัดสเปกตรัม: examples/s07/02_fft64_two_tones.py↗ เขียน FFT 64 จุดด้วยมือทั้งตัว แล้วชี้ว่าข้อมูลชุดเดิมมองคนละมุมได้คำตอบคนละแบบ · พอเข้าใจข้างในแล้ว ของจริงใช้ dsp.fft_mag() ที่คำนวณใน C (firmware 2026-08-20 ขึ้นไป) — examples/lvgl_ports/sec3_sensor_viz/eva/ex16_spectrum_analyzer.py คือเครื่องวิเคราะห์สเปกตรัมทั้งเครื่องที่ประกอบจากมัน เก็บไว้เปิดตอนอยากต่อยอดไปงาน predictive maintenance จริง

ซ้าย — หน้าจอจริงตอนรัน examples/s07/02_fft64_two_tones.py↗ — บนคือคลื่นที่ป้อนเข้าไป ล่างซ้ายคือสเปกตรัมที่ออกมา วาดบนตารางไฟ 8x8 ด้วย DotMatrix.set_pixels() · ภาพจับที่ขั้นแรก ป้อนโทนเดียว 3 รอบเข้าไป ยอดจึงขึ้นที่ bin 3 เดี่ยว ๆ และเพราะความละเอียดต่อ bin เท่ากับ fs/N = 1.000 Hz เลข bin กับเลขความถี่จึงบังเอิญตรงกันพอดีที่ 3 · บรรทัดล่างคือราคาที่ประหยัดได้จริง FFT เร็วกว่า DFT ตรง ๆ 21 เท่าที่ N = 64 · ภาพหน้าจอจริงจากบอร์ด Eva Kit บันทึกโดยผู้สอน

คาบนี้เหลือตัวอย่างสามไฟล์ หลังตัดเรื่องเลข int16 · บิตมาสก์ · การแพ็กฟิลด์ลงไบต์ ออกจากหลักสูตร ทั้งสามเรื่องไม่มีคาบไหนสอนแล้ว และไม่มีไฟล์ไหนต้องแก้ค่าคงที่ก่อนกด Run

เฉลย s07_accel_chart.py↗ — ตั้งต้น กราฟ ตาราง ไฟ และปุ่มสองปุ่ม

import ui
ui.screen()
import time
import sensors

COL_TEXT, COL_DIM = 0xE8EAED, 0x9AA3AF
COL_AX = 0x4A9EFF      # แกน X - เส้นที่ 1 ของจานสีเส้นข้อมูล สีฟ้า
COL_AY = 0x8E7BFF      # แกน Y - เส้นที่ 2 ของจานสีเส้นข้อมูล สีม่วง
COL_AZ = 0x2FB6A8      # แกน Z - เส้นที่ 3 ของจานสีเส้นข้อมูล สีเขียวน้ำทะเล
COL_RUN = 0x4A9EFF     # สีเน้นของจานบทบาท - ใช้กับไฟบอกว่ากำลังบันทึกอยู่
PERIOD_MS = 200        # คาบการสุ่มที่เราเลือก
...
chart = ui.Chart(x=24, y=52, w=292, h=144, min=-20, max=20, color=COL_AX)
s_ay = chart.add_series(COL_AY)
s_az = chart.add_series(COL_AZ)
tbl = ui.Table(x=332, y=52, w=436, h=280, cols=4, value=16)
...                    # col_width + add_row สี่แถว - ท่าที่ 2
led_rec = ui.Led(x=24, y=212, w=48, h=48, color=COL_RUN, value=1)
lbl_rec = ui.Label("กำลังบันทึก", x=88, y=216, color=COL_TEXT, value=20)
lbl_dt = ui.Label("คาบลูป -- ms", x=88, y=252, color=COL_DIM, value=20)
...                    # ปุ่มเริ่ม/หยุด + start_id stop_id - ท่าที่ 3
lo = [None, None, None]
hi = [None, None, None]
นับ widget Chart 1 + Table 1 + ไฟ 1 + ปุ่ม 2 หัวเรื่อง + ป้ายสองป้าย อีก 3 รวม 8 ชิ้น ป้ายใช้สีเดียวกับเส้นในกราฟ ตารางหนึ่งใบแทนป้ายสิบสองช่อง
แถวของตารางถูกสร้างด้วย - ไว้ก่อน ไม่ใช่เว้นว่าง เพราะช่องว่างเปล่าอ่านได้สองแบบ คือ "ยังไม่มีค่า" กับ "จอเสีย" ส่วนขีดอ่านได้แบบเดียว · lo กับ hi เกิดนอกลูปเพราะมันคือความจำของโปรแกรม · เส้นในกราฟใช้สามสีแยกแกน ส่วนตารางใช้ ตัวอักษร X Y Z แยกแทน เพื่อให้อ่านออกแม้พิมพ์เป็นขาวดำ · start_id กับ stop_id เก็บครั้งเดียวตอนสร้าง เพราะบนจอมีสองปุ่ม

เฉลย — ลูปหลัก และทำไมเรียงห้าท่าแบบนี้

running = True
last_ms = time.ticks_ms()
last_sec = -1

while True:
    if running:
        try:
            ax, ay, az, gx, gy, gz = sensors.bmi270.motion()
        except OSError:
            pass
        chart.set_next(0, int(ax))
        chart.set_next(s_ay, int(ay))
        chart.set_next(s_az, int(az))
        note(0, ax)          # กราฟลืมจุดที่เก่ากว่า 50 จุด ตารางไม่ลืม

    now = time.ticks_ms()
    dt = time.ticks_diff(now, last_ms)
    last_ms = now

    for ev in ui.poll():     # poll นอก if running เสมอ ไม่งั้นปุ่มตายตอนหยุด
        h = ev.get('handle')
        if h == start_id:
            running = True
            lo = [None, None, None]
            hi = [None, None, None]
            led_rec.value(1)
        elif h == stop_id:
            running = False
            led_rec.value(0)          # หรี่ ไม่ใช่หาย

    sec = time.ticks_ms() // 1000
    if sec != last_sec:               # ตัวเลขขยับวินาทีละครั้ง กราฟขยับ 5 ครั้ง
        last_sec = sec
        for i, v in enumerate((ax, ay, az)):
            tbl.cell(i + 1, 1, cell(lo[i]))
        lbl_dt.text("คาบลูป {} ms".format(dt))

    lbl_rec.text(rec_msg)     # ชีพจร - ส่งข้อความเดิมซ้ำทุกรอบโดยตั้งใจ
    time.sleep_ms(PERIOD_MS)
ห้าท่า เรียงเพื่อตัดกองบั๊ก 1 เตรียมเซนเซอร์และจอ ถ้าฐานพัง ที่เหลือไม่มีความหมาย 2 สร้างกราฟ อยากเห็นกรอบเปล่าขึ้นจอก่อน 3 ปุ่มและ flag ปุ่มคือทางออกฉุกเฉิน มาก่อนข้อมูล 4 ป้อนข้อมูลจริง เพี้ยนตอนนี้ = เพี้ยนที่การอ่าน 5 วัดคาบลูป เครื่องมือตรวจของสี่ท่าข้างบน

ui.poll() อยู่ นอก if running: — ปุ่มหยุดที่ฆ่าทางกลับของตัวเองคือบั๊กที่เจอบ่อยที่สุดในหน้าที่มีปุ่มหยุด และมันดูเหมือนจอค้าง ทั้งที่โปรแกรมยังวิ่งครบทุกบรรทัด

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

ค่าเดี่ยว ไปเป็นเส้นเวลา ไปเป็นแดชบอร์ด ไปเป็น telemetry คาบ 5-6 อ่านค่าเดี่ยว แล้ววาดเป็นแถบกับตัวเลข วันนี้ · คาบ 7 เก็บเป็นเส้นเวลา คุมจังหวะการสุ่ม คาบ 8 รวมเป็นแดชบอร์ด ภายใต้งบ widget คาบ 9-12 ส่งเส้นเวลานี้ ขึ้นแพลตฟอร์ม

วันนี้เราได้: เข้าใจว่า time series คืออะไรและอัตราสุ่มมีผลอย่างไร · รู้จัก Nyquist และ aliasing พร้อมตัวเลขของลูปเราเอง · ใช้ ui.Chart กับหลาย series ได้ · รู้ว่าทำไมต้อง int() และมันแลกอะไรไป · เลือกช่วงแกน Y ด้วยเหตุผลของตัวเอง · ทำปุ่มเริ่ม/หยุดบันทึกด้วย flag ที่ไม่ฆ่าลูป · วัดคาบลูปจริงและเอาขึ้นจอ · และรู้ว่าทำไมลูปที่มีแต่กราฟถึงช้ากว่าลูปที่มีป้ายด้วย 40 เท่า

รากฐานที่แตะไป: การสุ่มตัวอย่างและคาบการสุ่ม · ทฤษฎีบทไนควิสต์และ aliasing · การสูญเสียความละเอียดจากการแปลงเป็นจำนวนเต็ม · ring buffer และคิวที่มีขนาดจำกัด · การวัดเวลาด้วยตัวนับที่วนกลับ · สถานะที่ต้องอยู่นอกลูป · การใช้สีเป็นภาษาสื่อความหมาย · การทำให้โปรแกรมรายงานสมรรถนะของตัวเอง

คำถามคิดต่อ: ถ้าต้องส่งข้อมูลนี้ขึ้นคลาวด์ในคาบ 10 จะส่งทุกจุดที่ 5 Hz หรือส่งเฉพาะตอนที่มีอะไรน่าสนใจ · ค่าอะไรควรสรุปก่อนส่ง · ถ้าเน็ตหลุดสามนาที ข้อมูลช่วงนั้นควรหายไปเลยหรือควรเก็บไว้

เก็บไฟล์ของวันนี้ไว้ให้ดี คาบ 8 เราจะเปิดมันขึ้นมาต่อยอด ไม่ได้เริ่มใหม่

ใช้จริงที่ไหน — สี่มุมของกราฟ real-time ในสนามจริง

โรงงาน · Vibration Monitoring กราฟความสั่นของมอเตอร์แบบสด ช่างเห็นทันที ว่าลูกปืนเริ่มเปลี่ยนรูปแบบการสั่นหรือยัง อัตราสุ่มจริง: หลักกิโลเฮิรตซ์ ไม่ใช่ 5 Hz การแพทย์ · Patient Monitor คลื่นหัวใจและออกซิเจนวิ่งบนจอข้างเตียง ปุ่ม freeze ให้หมอหยุดดูจังหวะที่ผิดปกติ ปุ่ม freeze คือ flag ตัวเดียวกับที่เราเพิ่งเขียน ขนส่ง · Fleet Telematics กราฟความเร่งของรถบรรทุก จับการเบรกกะทันหัน และการเข้าโค้งแรง เพื่อประเมินพฤติกรรมคนขับ เซนเซอร์ตัวเดียวกับบนบอร์ดเรา ต่างที่โจทย์ โครงสร้าง · Structural Health เซนเซอร์บนสะพานและอาคารสูง เฝ้าการสั่น ก่อนและหลังแผ่นดินไหว เทียบรูปคลื่นย้อนหลัง ที่นี่ "หน้าต่างเวลา" ยาวเป็นวัน ไม่ใช่ 10 วินาที

ทั้งสี่มุมใช้แนวคิดเดียวกับวันนี้ทั้งหมด ต่างกันที่ อัตราสุ่ม ความยาวหน้าต่างเวลา และสิ่งที่ทำต่อกับข้อมูล

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

ข้อ 1 · เครื่องกำเนิดสัญญาณของทีม
ต่อยอดจากโค้ดอุ่นเครื่อง ทำหน้าที่ป้อนคลื่นสี่แบบ (สี่เหลี่ยม ไซน์ สามเหลี่ยม ฟันเลื่อย) ลงกราฟโดยไม่ใช้เซนเซอร์เลย แล้วใส่ปุ่มสลับรูปคลื่น บันทึกว่าคลื่นแบบไหนดูผิดเพี้ยนเร็วที่สุดเมื่อเพิ่มความถี่ และเทียบกับที่ Nyquist ทำนายไว้

ข้อ 2 · กราฟความละเอียดสูง
เปลี่ยนไปใช้ int(ax * 100) พร้อมขยายช่วงเป็น min=-2000, max=2000 แล้ววางเทียบกับกราฟเดิม บันทึกว่าเห็นรายละเอียดอะไรเพิ่มขึ้น และเสียอะไรไปหรือไม่

ข้อ 3 · ตารางทดลอง cadence
ทดลอง PERIOD_MS ที่ 500, 200, 100, 50 และ 20 บันทึกคาบลูปจริงที่วัดได้ในแต่ละค่า แล้วหาว่าจุดไหนที่เริ่มเห็นอาการเฟรมหาย · โจทย์เพิ่ม: ทำการทดลองซ้ำสองรอบ รอบหนึ่งมีบรรทัด lbl.text() รอบหนึ่งไม่มี แล้วเทียบตัวเลข

ข้อ 4 · เส้นดิบเทียบเส้นกรอง
เพิ่ม series ที่สี่เป็นค่าที่ผ่าน dsp.EMA(alpha=0.2) ของแกนใดแกนหนึ่ง วางทับเส้นดิบของแกนเดียวกัน แล้วอธิบายว่าฟิลเตอร์แลกอะไรกับอะไร (ลองปรับ alpha เป็น 0.05 กับ 0.5 ประกอบ)

ภาพ: Christophe Dang Ngoc Chan, Wikimedia Commons, CC BY-SA 4.0 — ค่าเฉลี่ยเคลื่อนที่บนสัญญาณรบกวน · ใช้ประกอบข้อ 4

ภาพ: Yves-Laurent Allaert, Wikimedia Commons, CC BY-SA 3.0 — สัญญาณสะอาด เทียบ สัญญาณที่มีสัญญาณรบกวน

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

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

01 เฝ้าการสั่นของเครื่องจักร · 02 FFT radix-2 เขียนเองทั้งตัว 64 จุด · 03 สุ่มช้าเกินไป แล้วได้ความถี่ที่ไม่เคยมีอยู่จริง
ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของ Eva Kit และ Dev Kit — แสดงหน้าจอที่ตัวอย่างสร้าง ค่าจากเซนเซอร์ WiFi และไมค์เป็นค่าแทนบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด

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

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

  • Chapter 20: Analog to Digital Conversion — ADI University Wiki — https://wiki.analog.com/university/courses/electronics/text/chapter-20 (มีเงื่อนไข Nyquist อยู่ในบทเดียวกัน)
  • Practical Electronics for Inventors, 4th ed. — §6.1.1 Precision/Accuracy/Resolution (หน้า 817) · §12.9.5-6 ADC
  • An Intuitive Look at Moving Average and CIC Filters — Tom Verbeure — https://tomverbeure.github.io/2020/09/30/Moving-Average-and-CIC-Filters.html
  • BMI270 datasheet BST-BMI270-DS000-08 rev 1.6 — Bosch Sensortec
  • ตัวเลขโหมดเร่งของ IPC อ่านจากซอร์สเฟิร์มแวร์เอง: .../ipc_ui/ipc_ui.c — IPC_UI_TIMER_MS, IPC_UI_FAST_TIMER_MS, IPC_UI_MAX_PER_TICK, IPC_UI_FAST_TIMEOUT_MS, ui_arm_fast_mode() (สไลด์ผู้สอน)

วิดีโอที่ฝังไว้ในคาบนี้ (ตรวจแล้วว่าเปิดได้ ตาม _BUILD/MEDIA_PLAN.md)

nac9qyJT-sY An intuitive introduction to oversampling and noise shaping — Analog Snippets · 12:08 · plq_Nmud5CM ADC Quantization and Resolution — Microchip Developer Help · ยาวยังไม่ยืนยัน · yYHHuhDwhec Sigma-Delta ADC: Oversampling, Noise Shaping & Decimation — Byte-Sized Learning · 5:01 · RLQGZl0lpjQ Working principle of an accelerometer — Bosch Sensortec · 1:01 — คำอธิบายว่าดูเพื่ออะไร อยู่ใต้คลิปในสไลด์ที่ฝังไว้แล้ว

เครดิตภาพ — Wikimedia Commons และ PMC open-access (CC0 / CC BY / CC BY-SA / PD) · รายละเอียดที่ CREDITS.md

fit-css

VIDEO-SLOT: คลิป 45-60 วินาที ถ่ายจอบอร์ดที่เมนู Sensor Dashboard — เริ่มจากบอร์ดวางนิ่งให้เห็นเส้นสามสีเกือบเรียบ แล้วเขย่าเบา ๆ ให้เห็นเส้นกระเพื่อม แล้วเอียงบอร์ดค้างไว้ให้เห็นเส้นหนึ่งยกตัวค้างแล้วเลื่อนไปทางซ้าย

.set_next() ไม่เคยฟ้อง และ .value() ของ Chart ก็ไม่เคยตอบ — กราฟจึงเป็น widget ที่จอเป็นพยานคนเดียว ถ้าไม่มีป้ายตัวเลขคู่กันไว้ ก็ไม่มีทางรู้ว่าข้อมูลเข้าไปจริงไหม

VIDEO-SLOT: คลิป 30-45 วินาที ถ่ายผลลัพธ์ MVP — เริ่มที่บอร์ดวางนิ่งให้เห็นสามเส้นแยกสี เส้น Z ลอยสูง แล้วเขย่าให้เห็นทั้งสามเส้นตอบสนองและรอยเลื่อนไปทางซ้าย พร้อมตัวเลขสูงสุดในตารางที่ขยับตาม จากนั้นกด "หยุดบันทึก" ให้เห็นไฟหรี่ลงและป้ายเปลี่ยนเป็น "หยุดแล้ว" แล้วเขย่าแรง ๆ ให้เห็นว่ากราฟนิ่งสนิทแต่ตารางยังค้างค่าสุดขีดของรอบที่แล้วไว้ ปิดท้ายด้วยการกด "เริ่มบันทึก" แล้วซูมให้เห็นค่าสุดขีดถูกล้างกลับเป็นขีด

☰ สารบัญ