| คำถาม | คำตอบของชุดบทเรียนนี้ | อยู่ช่วงไหน | |
|---|---|---|---|
| Why | ตัวเลขบนจอก็อ่านค่าได้อยู่แล้ว ทำไมต้องมีกราฟ | เพราะตัวเลขตอบได้คำถามเดียวคือ "ตอนนี้เท่าไร" ส่วนคำถามที่งานจริงถามคือ "เมื่อกี้มันเป็นยังไง" และ "มันกำลังจะไปทางไหน" · และการสุ่มไม่ทันไม่ส่งเสียงเตือน มันให้คำตอบที่ผิดและดูน่าเชื่อถือ ต้องรู้ล่วงหน้าว่าสัญญาณของเราเร็วแค่ไหน | ครึ่งแรก · Nyquist และ aliasing |
| What | มีอะไรให้ใช้บ้าง | ui.Chart กับเมธอดของ Widget 14 ตัวที่ต้องรู้ — สองตัวเป็นของกราฟล้วน ๆ · บวกตัวสร้าง widget 16 ตัว ที่กางในตารางอ้างอิงของชุดบทเรียนนี้ (บัญชีเต็มของโมดูล ui อยู่ในบทเรียน 2.4) |
สไลด์บัญชี 14 เมธอด + ตารางอ้างอิง widget |
| How | ประกอบยังไงให้ใช้งานได้จริง | สร้างกราฟหนึ่งใบสามเส้น ป้อนด้วย int() ทุกรอบ · ปุ่มเริ่ม/หยุดบันทึกที่เป็น flag ในลูป ไม่ใช่การหยุดลูป · ตารางเก็บค่าสุดขีดที่กราฟจำไม่ได้ · และนาฬิกาที่จับคาบลูปจริงมาโชว์ |
สามไฟล์ตัวอย่าง + ไฟล์ฝึก |
ปลายทางที่จับต้องได้ — กราฟสามสีของแกน X Y Z วิ่งตามการเขย่ามือ มีปุ่มหยุด-เดินต่อที่ทำงานจริง และบรรทัดที่บอกว่าลูปของเราหมุนอยู่ที่กี่มิลลิวินาที
บทเรียน 3.1–3.3 เราวัด "ตอนนี้เอียงกี่องศา" · ชุดบทเรียนนี้เราเก็บ "สิบวินาทีที่ผ่านมาเป็นยังไง"
ui.Chart กับ .add_series() และ .set_next() ได้ถูกต้อง รวมถึงรู้ว่าทำไมต้อง int()time.ticks_ms() / time.ticks_diff() แล้วเอาขึ้นจอset_next() ถึงวาดช้ากว่าลูปที่มี .text() อยู่ด้วยถึง 40 เท่าปลายทางของวันนี้: จอบอร์ดมีกราฟสามสีวิ่งตามการเขย่า ตารางค่าสุดขีดสามแกน ปุ่มเริ่ม/หยุดบันทึก และตัวเลขบอกว่าลูปของเราหมุนจริงที่กี่มิลลิวินาที
ชุดบทเรียนนี้เราไม่ได้แค่ทำให้กราฟขึ้น เราต้องรู้ด้วยว่ากราฟนั้น เชื่อถือได้แค่ไหน

หน้าจอของเฉลยปัจจุบันแบ่งเป็นสามส่วน และสองส่วนแรกตอบคนละคำถามกัน
ui.Chart เส้นสามสีของแกน X Y Z ตอบคำถามว่า "เมื่อกี้มันเป็นยังไง" (ตอนบอร์ดวางนิ่ง เส้นจึงราบ)ui.Table สี่คอลัมน์ แกน · ต่ำสุด · สูงสุด · ล่าสุด ตอบคำถามที่กราฟตอบไม่ได้ คือ ค่าสุดขีดที่ผ่านไปแล้ว เพราะกราฟเก็บได้เท่าจำนวนช่องของมัน (ปริยาย 50) จุดที่เก่ากว่านั้นถูกเขี่ยทิ้งไปแล้วui.Led บอกว่ากำลังบันทึกอยู่หรือหยุดแล้ว บรรทัดคาบลูปจริง และ ปุ่มเริ่มกับปุ่มหยุดที่แยกกันคนละปุ่ม (สูง 88 px ตามขนาดเป้าสัมผัส)กราฟตอบ "เมื่อกี้มันเป็นยังไง" ส่วนตารางตอบ "ที่ผ่านมาแรงสุดเท่าไร" — คนละคำถาม จึงต้องมีทั้งคู่
บทเรียน 3.1–3.3 เราอ่าน sensors.bmi270.motion() → dsp.tilt(ax, ay, az) → (roll, pitch) → ui.Bar บน ui.Scale สองแกน + ui.Seg7
สิ่งที่แถบกับ Seg7 ทำได้ดี: บอก ค่าปัจจุบัน ชัดเจน · สิ่งที่ทำไม่ได้เลย: บอกว่า เมื่อกี้เป็นยังไง
| คำถามที่ทีมอยากรู้ | Bar / Seg7 | Chart |
|---|---|---|
| ตอนนี้เอียงกี่องศา | ตอบได้ทันที | ต้องเพ่งที่ปลายเส้น |
| เมื่อ 5 วินาทีที่แล้วสั่นไหม | ตอบไม่ได้ | ตอบได้ |
| การสั่นถี่ขึ้นหรือเบาลง | ตอบไม่ได้ | เห็นเป็นรูปร่างทันที |
| มีค่ากระโดดผิดปกติแวบเดียวไหม | พลาดแน่นอน | เห็นเป็นหนามบนเส้น |
ภาพขวาคือสัญญาณ accel จริงตอนคนเดิน 10 ก้าว — ยอดที่ทำเครื่องหมายสีแดงคือก้าว ข้อมูลชุดนี้อ่านจากแถบหรือตัวเลขไม่ได้เลย ต้องเห็นเป็นเส้นเวลาเท่านั้น

เลือก widget ตาม คำถามที่ต้องการคำตอบ ไม่ใช่ตามความสวย
Time series คือข้อมูลชุดหนึ่งที่ แต่ละค่ามีเวลากำกับ และเรียงตามเวลา
เซนเซอร์ให้สัญญาณต่อเนื่องซึ่งมีค่าอยู่ทุกเสี้ยววินาที แต่คอมพิวเตอร์เก็บค่าต่อเนื่องไม่ได้ มันทำได้อย่างเดียวคือ แอบมองเป็นระยะ ๆ แล้วจดค่าไว้ — เรียกว่า การสุ่มตัวอย่าง (sampling)
ไทย: อัตราสุ่มคือส่วนกลับของระยะห่างระหว่างการมองสองครั้ง
ตัวเลขของลูปเรา: ms Hz
วงจร sample-and-hold (ภาพขวาบน) คือฮาร์ดแวร์ที่ทำเรื่องนี้จริง ๆ ในชิป: ปิดสวิตช์ชั่วขณะเพื่อชาร์จตัวเก็บประจุ แล้วเปิดสวิตช์ค้างค่านั้นไว้ให้ ADC อ่านทัน — เอาต์พุตจึงเป็นขั้นบันได ไม่ใช่เส้นโค้ง
กราฟบนจอไม่ใช่สัญญาณจริง มันคือ จุดที่เราจดไว้ แล้วลากเส้นตรงเชื่อมกัน ระหว่างจุดสองจุดเกิดอะไรขึ้นบ้าง เราไม่มีทางรู้จากกราฟนี้เลย
กราฟทุกกราฟในโลก embedded คือ "ค่าที่เราเลือกจะมอง" ไม่ใช่ "ทุกอย่างที่เกิดขึ้น"

ปี 1928 แฮร์รี ไนควิสต์ พิสูจน์ไว้ว่า ถ้าจะเก็บสัญญาณที่มีความถี่สูงสุด ให้ครบ ต้องสุ่มเร็วกว่าสองเท่าของมัน
ไทย: จะเห็นคลื่นความถี่หนึ่งได้ ต้องสุ่มเร็วกว่าสองเท่าของคลื่นนั้น
ตัวเลขของลูปเรา: Hz เห็นการสั่นได้ไม่เกิน 2.5 Hz
การเขย่ามือของคนอยู่ราว 2-5 Hz เราจึงเห็นเฉพาะส่วนที่ช้ากว่า 2.5 Hz ส่วนที่เร็วกว่านั้นจะปลอมตัวเป็นคลื่นช้า (สไลด์ถัดไป) · อัตราสุ่มของเราไม่ได้ตั้งด้วยฮาร์ดแวร์ แต่เกิดจากความเร็วลูป Python
ไทย: งานในลูปหนักขึ้น อัตราสุ่มตกลงเงียบ ๆ — จึงต้องวัดคาบลูปจริง (ท่าที่ 5)
สุ่มไม่ทันไม่ได้แปลว่า "มองไม่เห็น" มันแปลว่า "เห็นผิด" — สไลด์ถัดไปคือเหตุผล
ไทย: ถ้าสัญญาณเร็วเกินไป มันจะไม่หายไปเฉย ๆ แต่จะ ปลอมตัว มาเป็นคลื่นช้ากว่าความจริง ซึ่งอันตรายกว่าการมองไม่เห็น
ตัวเลขจากบอร์ดนี้ — เดโมที่ทำได้ในบทเรียน: เขย่าบอร์ดที่ราว 6 Hz ขณะลูปสุ่มที่ Hz
กราฟจะโชว์คลื่นช้า ๆ 1 Hz ที่ ไม่มีอยู่จริง ทั้งที่มือเราเขย่าอยู่ 6 ครั้งต่อวินาที
นี่คือเหตุผลเดียวกับที่ล้อรถในหนังบางทีดูเหมือนหมุนถอยหลัง — กล้องคือ ADC ที่สุ่มที่ 24 เฟรมต่อวินาที
เจอตอนไหน ค่าที่เร็วเกินอัตราสุ่มไม่มี error ไม่มีคำเตือน มันแค่ให้คำตอบที่ผิด และดูน่าเชื่อถือด้วย


03_aliasing_nyquist.py — สูตรในสไลด์ก่อนเดินให้ดูบนบอร์ดจริงทีละขั้น อัตราสุ่มถูกตรึงไว้ที่ 40 Hz ตลอด เปลี่ยนเฉพาะความถี่ของคลื่นจริง · ภาพนี้คือขั้นแรกซึ่งเป็นตัวคุม f จริง = 5 Hz ยังต่ำกว่า Nyquist ที่ 20 Hz อยู่มาก ความถี่ที่เครื่อง "เห็น" จึงเป็น 5 เท่ากัน และเส้นทับกันสนิท — ผีจะโผล่ก็ต่อเมื่อกดเดินหน้าจนความถี่จริงเกิน 20 Hz · ภาพหน้าจอจริงจากบอร์ด Eva Kit บันทึกโดยผู้สอน · ส่วน 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 ตัวเดียวกับบทเรียน 2.7–2.9 แค่คราวนี้เราเป็นคนสร้างเอง
สูตร map ช่วงจริงลงช่วงแกน — วิธีแก้ที่ใช้ในงานจริง
ไทย: แกน 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 เท่าโดยไม่แตะเซนเซอร์เลย

ตัวเลขบนแกน Y ไม่จำเป็นต้องเป็นหน่วยจริง ขอแค่ เรากับคนอ่านกราฟรู้ตรงกัน ว่ามันคูณอะไรไว้
set_next() ไม่ได้ "วาดกราฟ"ไทย: หน้าจอเรากว้าง 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 คำสั่ง/วินาที |
และนี่คือกับดัก — คำสั่งที่ ปลุกโหมดเร่ง มีอยู่ชุดหนึ่ง คำสั่งที่ ไม่ปลุก ก็มีอีกชุด
| ปลุกโหมดเร่ง | ไม่ปลุกโหมดเร่ง |
|---|---|
สร้าง widget · .text() · .pos() · .size() · .color() |
.value() · .set_next() · .show() / .hide() |
โหมดเร่งจะกลับเป็นปกติเองหลังเงียบไป 500 ms
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()เป็นคำสั่งที่ ไม่ปลุก โหมดเร่ง — กราฟที่วิ่งอยู่ตัวเดียวบนจอ จึงเป็นกรณีที่ช้าที่สุดพอดี
รู้กลไกแล้ว ทางแก้เหลือบรรทัดเดียว — ให้ทุกลูปมีคำสั่งที่ปลุกโหมดเร่งอย่างน้อยหนึ่งคำสั่ง และตัวที่เป็นธรรมชาติที่สุดคือป้ายสถานะที่เราอยากเห็นอยู่แล้ว
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 เร็วขึ้น มันแค่ทำให้ คำสั่งที่เราส่งไปแล้ว ถูกวาดออกจอเร็วขึ้น
ลูปที่อัปเดตกราฟอย่างเดียวคือลูปที่ช้าที่สุด — ใส่ป้ายสถานะไว้ในลูปเดียวกันเสมอ ได้ทั้งความลื่นและได้ตัวเลขให้คนอ่าน