| คำถาม | คำตอบของคาบนี้ | อยู่ช่วงไหน | |
|---|---|---|---|
| Why | ปุ่มกับสวิตช์ก็สั่งงานได้แล้ว ทำไมต้องมาอ่านลูกบิดกับแผ่นสัมผัสอีก | เพราะของที่ IoT ต้องวัดจริง ๆ — อุณหภูมิ ระดับน้ำ แรงดัน ความชื้น ตำแหน่งวาล์ว — ไม่มีตัวไหนตอบมาเป็น 0 กับ 1 มันตอบมาเป็นค่าต่อเนื่องที่สั่นตลอดเวลา และงานของวิศวกรไม่ใช่กำจัดการสั่น แต่คือ เลือกฟิลเตอร์แล้วปกป้องตัวเลือกนั้นได้ | ครึ่งแรก · ADC และ CapSense · สไลด์ "ทำไมค่าดิบถึงสั่น" |
| What | มีอะไรให้ใช้บ้าง | โมดูล sensors สิบห้าชื่อบน Eva Kit — เก้าฟังก์ชัน (บน Eva สี่ตอบ ห้าปฏิเสธด้วย OSError · บน Dev Kit ห้าตัวนั้นทำงานจริง) กับหกเซนเซอร์ย่อย (Dev Kit มีเพิ่ม dps368 sht40 radar) · และโมดูล dsp ทั้งสิบหกชื่อ คือ 8 คลาสกับ 8 ฟังก์ชัน (สองตัวสเปกตรัมเพิ่ม 2026-08-20) ใช้ได้ครบทุกตัวทั้งสองบอร์ด |
สองสไลด์แผนที่โมดูล |
| How | ประกอบยังไงให้ใช้งานได้จริง | อ่านจาก sensors.pot กับ sensors.capsense ป้อนเข้า dsp.EMA และ dsp.Median แล้ววางค่าดิบกับค่ากรองแล้วไว้ข้างกันบนจอเดียว |
สิบไฟล์ตัวอย่าง + ใบฝึก |
ปลายทางที่จับต้องได้ — เข็มโค้งหมุนตามลูกบิด แถบเลื่อนวิ่งตามนิ้ว และสองบรรทัดล่างที่ฟ้องด้วยตาว่าฟิลเตอร์กำลังทำงานอยู่จริง
คาบ 4 คือ "จอสั่งฮาร์ดแวร์" · คาบนี้กลับด้าน ฮาร์ดแวร์เป็นฝ่ายสั่งจอ
sensors.pot.read() / .percent() / .voltage() แล้วแสดงสามแถวสามสีบนจอsensors.capsense และเข้าใจว่าแผ่นทองแดงรู้ได้อย่างไรว่านิ้วมาแตะdsp.EMA และ dsp.Median ลดการสั่น แล้ว เทียบค่าดิบกับค่ากรองแล้วบนจอพร้อมกันปลายทางของวันนี้: หน้าจอเดียวที่มีเข็มโค้งตามลูกบิด แถบเลื่อนตามนิ้ว และสองบรรทัดล่างที่ฟ้องให้เห็นว่าฟิลเตอร์ทำงานอยู่จริง
วัดกันที่ "อ่านค่าได้และอธิบายค่าที่ได้เป็น" ไม่ใช่แค่ทำให้เข็มขยับ

หน้าจอของเฉลยปัจจุบันแบ่งเป็นสี่การ์ด และทุกการ์ดตอบคำถามคนละข้อ
ui.Bar วางทับ ui.Scale ที่มีขีดและตัวเลข 0-100 — ตัวเลขเปอร์เซ็นต์จึงไม่ได้ลอยอยู่เฉย ๆ มันมีพิสัยของตัวเองอยู่ข้าง ๆ พร้อมค่าดิบกับโวลต์กำกับui.Spinbox ที่ผู้ใช้ตั้งเองด้วยปุ่มเพิ่ม/ลด และ ui.Led สองดวงบอกว่าตอนนี้ต่ำกว่าเกณฑ์หรือเกินค่าเดียวกันแสดงได้หลายแบบ — เลือกให้ตรงกับคำถามที่คนดูจออยากรู้ และค่าที่วัดได้ต้องมาพร้อมเกณฑ์ของมันเสมอ
คาบที่แล้วเราสร้างแผงควบคุมของทีมเอง จากคาบนั้นมีสี่อย่างที่วันนี้ใช้ซ้ำทั้งหมด
| ของจากคาบ 4 | วันนี้ใช้ตรงไหน |
|---|---|
ui.Label / ui.Bar / ui.Panel และ x, y, w, h, color |
สร้างการ์ด แถบค่า และบรรทัดตัวเลข |
ui.poll() ทุกลูป |
ยังบังคับเหมือนเดิม ไม่เรียกแล้ว widget หายไปได้ถึง 2 วินาที และปุ่มบนจอกดไม่ติด |
time.sleep_ms() คุมจังหวะ |
คาบนี้ใช้ 200 ms เพราะจอมีของหลายชิ้น |
| เพดาน 64 widgets | โปรแกรมวันนี้ใช้ 33 ชิ้น เหลือที่ให้ต่อยอดอีก 31 |
ของใหม่ที่เพิ่มเข้ามา: โมดูล sensors (ลูกบิด + สัมผัส) · โมดูล dsp (ฟิลเตอร์) · widget สามตัวของหน้าจอ HMI คือ ui.Scale (ไม้บรรทัด) ui.Led (ไฟสถานะ) ui.Spinbox (ช่องป้อนเลข) — ตัวอย่างประกอบที่ examples/s04/09_scale_led_spinbox.py↗
ลูกบิดบนบอร์ดไม่ได้ส่งตัวเลขออกมา มันส่ง แรงดันไฟ ที่ไล่ต่อเนื่องจาก 0 V ขึ้นไปถึงแรงดันอ้างอิงของวงจร (ตัวเลขจริงของบอร์ดนี้ยังไม่ลงตัว — สองสไลด์ถัดไปว่าด้วยเรื่องนี้โดยเฉพาะ) วงจร SAR ADC ในชิปมีหน้าที่วัดแรงดันนั้นเป็นจังหวะ ๆ แล้วปัดลงขั้นบันไดที่ใกล้ที่สุด
ตัวเลขที่ Python อ่านได้ ไม่ใช่แรงดันจริง แต่เป็น ค่าที่ถูกปัดเข้าขั้นบันไดที่ใกล้ที่สุด ณ วินาทีที่วัด
import sensors
# ไม่ต้องเรียก sensors.init() ทั้งสองบอร์ด - บน Eva Kit เรียกแล้วขึ้น OSError
# ทันที (คอร์จอเป็นเจ้าของบัส) ส่วนบน Dev Kit ผ่าน แต่ก็ไม่จำเป็นอยู่ดี
raw = sensors.pot.read() # 0 - 65535 จำนวนเต็ม ทั้งสองบอร์ด (Dev Kit = VR1)
pct = sensors.pot.percent() # 0.0 - 100.0 เปอร์เซ็นต์ของช่วงเต็มสเกล
volts = sensors.pot.voltage() # ทศนิยม หน่วยโวลต์ (ดูสไลด์ถัดไปก่อนเชื่อตัวเลข)

ทั้งสามค่ามาจากการวัดครั้งเดียวกัน ต่างกันแค่หน่วยที่เอามาเสิร์ฟ — read() ดูความละเอียดดิบและความสั่น · percent() เข้ากับ ui.Arc / ui.Bar ที่คิดเป็น 0-100 อยู่แล้ว · voltage() ไว้เทียบกับมัลติมิเตอร์
เปลี่ยนจากที่เคยสอน: สามคำสั่งข้างบน ไม่ได้ตั้งค่า ADC เอง — บน Eva Kit คอร์จอ (CM55) อ่านลูกบิดแล้ว Python หยิบค่าจาก snapshot · บน Dev Kit CM33 อ่าน VR1 บนฐาน QWA309 ตรง (อีกสามตัวอยู่ที่ pots.read(1..3) คืน 0-4095 คนละสเกล) · ทั้งสองทางคืน 0-65535 หน้าตาเดียวกัน — ไม่ต้องมี init() ไม่ต้องหน่วง 500 ms
เลือกหน่วยตาม คนอ่าน ไม่ใช่ตามความสะดวกของโปรแกรม — เกจเป็นเปอร์เซ็นต์ รายงานวิศวกรรมเป็นโวลต์

R34 คือ pot 10 kΩ แบบ linear ต่อคร่อมระหว่างรางไฟกับกราวด์ ขากลาง (wiper) ออกมาเป็น POT_OUT — ตัวแบ่งแรงดันที่ปรับได้ด้วยมือ ตรงตามสัญลักษณ์ในสไลด์ที่แล้ว · กล่องขวา: thermistor TH1 ใช้เส้นทางเดียวกันผ่านตัวต้านทาน 0 Ω ที่ mark ว่า DNI — Eva Kit ใช้ pot กับ thermistor พร้อมกันไม่ได้ ค่าเริ่มต้นคือ pot
บน TESAIoT Dev Kit ลูกบิดที่ sensors.pot อ่านคือ VR1 บนฐาน QWA309 — ผัง QWA309 ระบุแรงดันอ้างอิงของ VR1-4 ไว้ 0-1.8 V (SAR 12 บิต) ขณะที่ voltage() คูณด้วย 3.3 V — ขัดกันแบบเดียวกับ Eva · ส่วนค่าความต้านทาน ทิศหมุน และ taper ของ VR1 ยังไม่ได้วัด ให้ทีมจดจากบอร์ดจริงลงใบงาน อย่าเอาตัวเลขของ R34 ไปใช้
หมายเหตุผู้สอน (ต้องเคลียร์ก่อนสอน): ผัง Eva ต่อ R34 กับ VDD_1V8 และผัง QWA309 ก็ระบุ 0-1.8 V แต่
sensors.pot.voltage()คูณด้วย 3.3 V ที่สมมติไว้ ทั้งสองบอร์ด — ยังไม่มีใครเอามิเตอร์ไปยืนยันบนบอร์ดไหนเลย ห้ามพูดตัวเลขใดตัวเลขหนึ่งเป็นความจริงบนกระดาน ให้หมุนสุดแล้วอ่านpot.read()/pot.voltage()เทียบกับมัลติมิเตอร์ที่ขาลูกบิดของบอร์ดจริงก่อน (Eva: ขา P15[1] · Dev Kit: ขากลางของ VR1) แล้วค่อยเขียนตัวเลขลงสไลด์
read() คืนจำนวนเต็ม 0 - 65535 นั่นคือ "สเกลของ API" () ไม่ได้แปลว่าฮาร์ดแวร์ละเอียด 65536 ขั้นจริง
ไทย: ค่าที่อ่านได้ไม่ต่อเนื่อง มันกระโดดทีละขั้น ขั้นหนึ่งใหญ่แค่ไหนขึ้นกับจำนวนบิต และความคลาดเคลื่อนจากการปัดไม่เกินครึ่งขั้น
คิดให้ดูจริง ๆ สมมติ read() คืน 40915
ส่วน ตอบเป็นโวลต์ไม่ได้จนกว่าจะรู้ ของบอร์ดจริง
ตัวส่วนมีสองแบบ อย่าสลับ: แปลงเป็นค่าอ่านใช้ (ratiometric) · ขนาดหนึ่งขั้นใช้

สิ่งที่ต้องวัดเอง: เลื่อนลูกบิดช้า ๆ แล้วดูว่า read() เดินทีละเท่าไร ถ้ากระโดดเป็นก้อนละ เท่ากันทุกก้อน ความละเอียดจริงคือ ขั้น — ก้อนละ 16 ก็คือ 4096 ขั้น จดลง worksheet อย่าเดาจากสเปกที่ยังไม่ได้อ่าน
สิ่งที่รู้จากซอร์สเฟิร์มแวร์: ADC ของทั้งสองบอร์ดอ่านได้ 12 บิต (0-4095) แล้วเฟิร์มแวร์คูณขยายเป็น 0-65535 ก่อนส่งให้เรา — 65535 จึงเป็นสเกลของ API ไม่ใช่ความละเอียดของ ADC
"เลขใหญ่" กับ "เลขละเอียด" เป็นคนละเรื่องกัน สเกลขยายได้ แต่ความจริงที่วัดมามีอยู่เท่าเดิม

SAR ย่อมาจาก Successive Approximation Register — แปลตรงตัวคือ "ทะเบียนของการประมาณทีละขั้น"
วิธีทำงานเหมือนเกมทายเลข 1-100 ที่เราเล่นกันตอนเด็ก: เดาครึ่งทางก่อน ถ้าสูงไปก็ตัดครึ่งบนทิ้ง แล้วเดาครึ่งของที่เหลือต่อไป
ทำไมต้องรู้: เพราะการแปลงหนึ่งครั้ง กินเวลา n รอบนาฬิกา ไม่ใช่ทันที นี่คือเหตุผลที่ ADC ทุกตัวมีเพดานอัตราการสุ่ม และเป็นที่มาของกฎ Nyquist ที่เราจะเจอเต็ม ๆ ในคาบ 7

เชื่อมกับวันนี้: ตัวเลขที่ sensors.pot.read() คืนมา คือผลของการทายแบบนี้ที่จบไปแล้วเมื่อเสี้ยววินาทีก่อน ไม่ใช่แรงดัน ณ วินาทีที่เราอ่านตัวแปร
สูตรที่ทั้งสองคลิปพาไปถึง — ในลูกบิด กับ คือแทร็กสองท่อนที่ wiper แบ่งเอาไว้ หมุนไปครึ่งทางจึงได้แรงดันครึ่งหนึ่ง · ทั้งสองคลิปเป็นของนอกเวลา (ต้องมีอินเทอร์เน็ต) เนื้อหาในคาบเข้าใจได้ครบโดยไม่ต้องเปิดดู
ลูกบิดคือตัวแบ่งแรงดันที่เราหมุนเปลี่ยนอัตราส่วนได้ด้วยมือ ทุกอย่างที่เหลือในคาบนี้ต่อยอดจากประโยคนี้
ใต้แผ่นพลาสติกของปุ่มสัมผัส (บน Eva Kit คือตรงที่เขียนว่า BTN0 / BTN1 · บน Dev Kit ตำแหน่งและป้ายของแผ่นสัมผัสยังไม่ได้บันทึกในเอกสารชุดนี้ ให้หาบนบอร์ดของทีม — ในโค้ดชื่อ btn0 btn1 slider เหมือนกันทั้งสองบอร์ด) ไม่มีปุ่มกดอยู่เลย มีแค่ แผ่นทองแดงบนแผ่นวงจร ที่เก็บประจุได้จำนวนหนึ่ง เรียกว่า capacitance ชิปจะอัดประจุเข้าไปแล้วจับเวลาว่ามันเต็มเร็วแค่ไหน
นิ้วคนเป็นตัวนำที่ต่อลงกราวด์ผ่านร่างกาย พอนิ้วเข้ามาใกล้ ค่า capacitance ของแผ่นนั้น เพิ่มขึ้น เวลาที่ใช้อัดประจุจึงนานขึ้นเล็กน้อย ชิปวัดความต่างนั้นได้ และสรุปว่า "มีนิ้ว"
แถบเลื่อนใช้หลักการเดียวกัน แต่วางแผ่นทองแดงเรียงกันหลายแผ่น แล้วดูว่านิ้วทำให้แผ่นไหนเปลี่ยนมากที่สุด เอามาถัวเฉลี่ยเป็นตำแหน่ง 0-100

ปุ่มสัมผัสไม่ได้วัด "แรงกด" มันวัด ความใกล้ของตัวนำ — นี่คือเหตุผลที่ใส่ถุงมือยางหนา ๆ แล้วมันไม่ติด

บน Eva Kit ปุ่มสองปุ่ม CSB1 / CSB2 เป็นแบบ CSX (mutual-capacitance) — สังเกตว่าแต่ละปุ่มมีขา Tx กับ Rx แยกกัน ไม่ใช่ขาเดียว ประจุถูกส่งจาก Tx ไป Rx และนิ้วเข้ามา "ขโมย" ส่วนหนึ่งไป · แถบเลื่อนคือ 5 อิเล็กโทรด (RX0-RX4) ที่ชิปถัวเฉลี่ยออกมาเป็นตำแหน่งเดียว 0-100 · ผังอิเล็กโทรดของ Dev Kit ยังไม่มีในเอกสารชุดนี้ — แต่ค่าที่โค้ดได้รับคือชุดเดียวกัน
ในโค้ดเราเห็นแค่
btn0 / btn1 / sliderสามค่า เหมือนกันทั้งสองบอร์ด — ข้างใต้มันคืออิเล็กโทรดหลายแผ่นกับชิปอีกตัวหนึ่งที่ทำงานให้ตลอดเวลา
งานสัมผัสไม่ได้ทำโดยชิปหลัก แต่ทำโดย PSoC 4000T อีกตัวหนึ่งบนบอร์ด ทำหน้าที่นี้อย่างเดียวตลอดเวลา — ทั้ง Eva Kit และ Dev Kit ใช้ทางเดียวกัน คือชิปตัวนี้ส่งค่าให้คอร์จอ (CM55) แล้ว Python ขอต่อจากคอร์จอ
หมายเหตุผู้สอน: ถ้าชิป 4000T ยังไม่ถูก flash ทุกค่าที่อ่านได้จะเป็น 255 หรือโยน
OSError— ต้องตรวจให้ครบทุกบอร์ดก่อนเข้าคาบนี้
ค่าความจุของแผ่นทองแดงไม่ได้คงที่ตายตัว มันเปลี่ยนตามความชื้น อุณหภูมิ และแม้แต่ฝุ่นบนหน้าจอ ชิปจึงไม่ตัดสินจากค่าดิบ แต่ตัดสินจาก ส่วนต่างเทียบกับค่าตอนไม่มีนิ้ว ค่านั้นเรียกว่า baseline
บนบอร์ดของเรา baseline ถูกเก็บ ครั้งแรกที่ไดรเวอร์อ่านชิปตัวนี้ และไดรเวอร์นั้นอยู่บนคอร์จอ ไม่ใช่ในสคริปต์ของเรา — แปลว่า baseline ถูกเก็บ ตอนบอร์ดบูต ไม่ใช่ตอนเรากด Program to Device · เผลอวางนิ้วค้างบนปุ่มตอนบูต ชิปจะจำ "สภาพมีนิ้ว" ว่าเป็นสภาพปกติ แล้วรายงานกลับด้านทั้งคาบ
b0, b1 = sensors.capsense.buttons() # (True/False, True/False) เทียบกับ baseline แล้ว
pos = sensors.capsense.slider() # 0 - 100
d = sensors.capsense.read() # {'btn0': bool, 'btn1': bool, 'slider': int}
เจอปุ่มรายงานกลับด้านเมื่อไร อย่าไปแก้โค้ด ให้ยกนิ้วออกให้หมดแล้ว ถอดสาย USB แล้วเสียบกลับ — บน Dev Kit ห้ามโยกสวิตช์บนฐาน สวิตช์พวกนั้นคือสวิตช์ไฟ ไม่ใช่ปุ่มรีเซ็ต · รันสคริปต์ใหม่เฉย ๆ ไม่ได้เก็บ baseline ใหม่
sensors (1/2) — สิบห้าชื่อบน Eva Kit และสองบอร์ดตอบไม่เหมือนกันสิบห้าชื่อแบ่งเป็นสองพวก — เก้าตัวเป็นฟังก์ชันที่เรียกตรงได้ และอีกหกตัวเป็นเซนเซอร์ย่อยกับตัววินิจฉัย ซึ่งมีเมธอดของตัวเองอีกชั้น · หน้านี้คือหกแถวที่ สองบอร์ดตอบเหมือนกัน ห้าตัวที่ตอบต่างกันอยู่หน้าถัดไป
| ชื่อ | ทำอะไร | บน Eva Kit | บน Dev Kit |
|---|---|---|---|
snapshot() |
ขอค่าทั้งชุดครั้งเดียว คืน dict สามกลุ่ม bmi270 capsense pot |
ทางหลัก — ค่าทั้งชุดมาจากคอร์จอ | ทางหลักเหมือนกัน dict หน้าตาเดียวกัน — IMU อ่านสดจาก CM33 · pot กับ capsense ใน snapshot มาจากคอร์จอที่อ่านทุก 200 ms (sensors.pot.read() ต่างหากที่อ่านตรง) |
read_all() |
อ่านทุกเซนเซอร์ที่มี | เรียก snapshot() ต่อให้ตรง ๆ คืนของเหมือนกันเป๊ะ |
คืน pot เป็น float ไม่ใช่ dict — คนละหน้าตากับ snapshot() |
auto_status() |
สถานะงานเบื้องหลัง คืน running rate_ms push_count mask |
เรียกได้ ใช้ดูว่ามีใครถูกเปิดไว้ | เรียกได้ |
auto_rate(ms) |
ตั้งจังหวะงานเบื้องหลัง หนีบไว้ 20-5000 ms เงียบ ๆ | เรียกได้ แต่ไม่มีผล เพราะ auto() เปิดไม่ได้ |
เรียกได้ และมีผลเมื่อ auto() เปิดอยู่ |
bmi270 bmm350 capsense pot |
สี่กลุ่มเซนเซอร์ที่มีทั้งสองบอร์ด — แต่ละตัวมีเมธอดของตัวเอง | ใช้ได้ทุกตัว | ใช้ได้ทุกตัว + มี dps368 sht40 radar เพิ่ม |
bmm350_diag bmm350_debug |
ตัววินิจฉัยเข็มทิศ ใช้ตอนหาสาเหตุว่าทำไมค่าเพี้ยน | เรียกได้ | เรียกได้ |
dps368 sht40 และ radar ไม่มีอยู่ในโมดูลเลยบน Eva Kit — พิมพ์ sensors.sht40 จะได้ AttributeError เพราะธง BSP ตั้งไว้ 0 ตั้งแต่คอมไพล์ (Eva ไม่มีชิปพวกนั้น) · บน Dev Kit สามชื่อนี้มีจริง จึงเป็นสามชื่อที่ print(dir(sensors)) บนสองบอร์ดให้ผลต่างกัน — รายชื่อบนบอร์ดคือความจริง ตารางนี้คือแผนที่
sensors (2/2) — ห้าตัวที่ Eva ปฏิเสธ และทำไมห้าในเก้าฟังก์ชันตอบต่างกันตามบอร์ด — บน Eva Kit ปฏิเสธด้วย OSError บน Dev Kit ทำงานจริง
| ชื่อ | ทำอะไร | บน Eva Kit | บน Dev Kit |
|---|---|---|---|
init() |
ปลุกเซนเซอร์และตั้งค่าบัส | OSError | ผ่าน (ไฟล์ทัวร์พิมพ์ค่าที่คืนให้ดู) — แต่ไม่ต้องเรียก เฟิร์มแวร์ปลุกให้ตั้งแต่บูต |
scan() |
ไล่หาอุปกรณ์บนบัส I2C ทีละแอดเดรส | OSError | ผ่าน คืนรายการที่อยู่ที่พบ |
push() |
ยัดค่าที่อ่านได้ข้ามไปให้คอร์จอ | OSError | ส่งค่าจริงหนึ่งครั้ง |
live_push() |
ทำแบบ push() วนไปเรื่อย ๆ |
OSError | วนไม่รู้จบจนกด Ctrl+C — อย่าเรียกเล่น |
auto() |
เปิดงานเบื้องหลังให้อ่านเซนเซอร์เอง | OSError | เปิด background task ค้างไว้ |
ห้าตัวที่ Eva ปฏิเสธ ปฏิเสธด้วยเหตุผลเดียวกันหมด ทั้งห้าจบลงที่การขับบัส SCB0 ซึ่งบน Eva คอร์จอถือไว้ ตัวที่อันตรายที่สุดคือ auto() เพราะมันไม่ได้ขับบัสเอง มันไปเปิดงานเบื้องหลังที่ขับบัสแทน อาการค้างจึงอยู่ต่อหลังบรรทัดนั้นจบไปแล้ว และไม่มี try/except ไหนช่วยได้ เพราะการค้างไม่ใช่ exception · บน Dev Kit บัส I2C ของ IMU เป็นของ CM33 เอง ห้าตัวนี้จึง "ทำงาน" — ซึ่งไม่ได้แปลว่าควรเรียก: live_push() วนจนกด Ctrl+C และ auto() เปิดงานค้างไว้หลังสคริปต์จบ
ลองเรียกเก้าตัวที่เรียกตรงได้ด้วยตัวเองที่
examples/s05/09_sensors_api_tour.py↗ — มันเรียกจริงแล้ว ให้บอร์ดเป็นคนบอก ว่าใครตอบ ใครปฏิเสธ ไม่ใช่ตารางที่พิมพ์ค้างไว้ (บน Dev Kit ไฟล์นี้ตั้งใจข้ามpush()live_push()auto()ด้วยเหตุผลข้างบน)
snapshot() คืนอะไรมาบ้าง — และคำว่า sequence มีไว้ทำไมd = sensors.snapshot() # dict หน้าตาเดียวกันทั้งสองบอร์ด
d['pot'] # {'raw': 0-65535, 'percent': 0.0-100.0, 'voltage': float, 'sequence': int}
d['capsense'] # {'btn0': bool, 'btn1': bool, 'slider': 0-100, 'sequence': int}
d['bmi270'] # {'ax','ay','az' m/s2, 'gx','gy','gz' deg/s, 'sequence': int}
การเรียกครั้งเดียวได้ครบทั้งสามกลุ่ม จากการอ่านจังหวะเดียวกัน ต่างจากเรียก pot.percent() แล้ว capsense.slider() ซึ่งเป็นการถามแยกสองรอบ ได้ค่าคนละจังหวะ และช้ากว่าเท่าตัว · read_all() บน Eva Kit คืนของเดียวกับ snapshot() เป๊ะ แต่บน Dev Kit มันคืน pot เป็น float — ในคอร์สนี้ใช้ snapshot() ให้ตรงกันทั้งสองบอร์ด
ช่อง sequence คือเลขนับตัวอย่างของเซนเซอร์กลุ่มนั้น ถ้าเลขนี้ไม่ขยับระหว่างสองรอบ แปลว่าเราอ่านค่าเดิมซ้ำ ไม่ใช่ว่าโลกหยุดนิ่ง — บน Eva Kit คอร์จออ่านทุก 200 ms ถ้าลูปของเราเร็วกว่านั้น เราจะเจอค่าเดิมแน่นอน (บน Dev Kit ค่า IMU ใน snapshot อ่านสดจาก CM33 ส่วน pot กับ capsense ยังมาจากคอร์จอทุก 200 ms ตัวเลขจึงเดินคนละจังหวะกัน) นี่คือเครื่องมือที่บอกได้ว่าจังหวะลูปเราเร็วเกินความจริงของข้อมูลหรือยัง
ยังมีอีกสองชื่อสำหรับงานซ่อม sensors.bmm350_diag() คืน dict สิบสองช่องสำหรับไล่ปัญหาเข็มทิศ และ sensors.bmm350_debug() พิมพ์ค่ารีจิสเตอร์ออกคอนโซล ทั้งคู่เป็นเครื่องมือของผู้สอนตอนบอร์ดมีปัญหา ไม่ใช่ของที่ใช้ในงานปกติ — และทั้งคู่ หยุดงานเบื้องหลังแล้ว re-init ชิป จึงกินเวลาและกวนค่าที่กำลังอ่านอยู่
read_all()บน Eva Kit เป็นบทเรียนเงียบ ๆ อีกข้อ: ชื่อที่ฟังดูทรงพลังกว่า ไม่ได้แปลว่าทำมากกว่า — เปิดซอร์สดูจึงรู้ว่ามันคือบรรทัดเดียวที่เรียกsnapshot()ต่อ (บน Dev Kit มันเป็นคนละฟังก์ชันและคืนpotเป็น float)
สาเหตุมีจริงและซ้อนกันหลายชั้น ไม่ใช่เรื่องลึกลับ
ชั้นไฟฟ้า — สายทุกเส้นรับสัญญาณรบกวนจากวงจรข้างเคียง จอที่กำลังรีเฟรช และ WiFi ที่กำลังส่ง ล้วนเหวี่ยงแรงดันระดับมิลลิโวลต์เข้ามาได้
ชั้นตัวอุปกรณ์เอง — ลูกบิดคือแผ่นตัวต้านทานที่มีหน้าสัมผัสถูตัวมันเอง ความต้านทานตรงจุดสัมผัสไม่ได้นิ่ง 100%
ชั้นการปัดขั้น — ถ้าแรงดันจริงตกอยู่ กึ่งกลาง ระหว่างสองขั้นบันได ADC จะปัดขึ้นบ้างลงบ้างสลับกันไป บิตล่างสุดจึงกระพริบตลอดเวลา
การสั่นระดับบิตล่างเป็น ปกติของทุกระบบวัด สิ่งที่วิศวกรทำไม่ใช่กำจัดมัน แต่คือตัดสินใจว่ายอมรับได้แค่ไหน
import dsp
ema = dsp.EMA(alpha=0.2) # keyword-only — dsp.EMA(0.2) ขึ้น TypeError
smooth = ema.update(pct) # ป้อนค่าใหม่ ได้ค่ากรองแล้ว
ema.value() # ขอค่าล่าสุดซ้ำ ไม่ป้อนอะไรเข้าไป
ema.reset() # ล้างความจำ รอบหน้าเริ่มนับหนึ่งใหม่
dsp.EMA() # ไม่ใส่ alpha ได้ ค่าตั้งต้นคือ 0.1 ไม่ใช่ 0.5
ค่าใหม่มีน้ำหนัก ที่เหลือคือค่าเดิมทั้งก้อน อดีตไม่ได้หายไปไหน แต่ จางลงเรื่อย ๆ ทุกรอบ
คิดให้ดูจริง ๆ ค่าเดิม ค่าใหม่เข้ามา ที่ → ขยับแค่ 0.20 ทั้งที่อินพุตกระโดดไป 1.00 นั่นคือความนิ่งที่เราซื้อมาด้วยความหน่วง
ตัวอย่างแรกไม่ได้ถูกคูณ ค่าที่ป้อนเข้าไปครั้งแรกถูกใช้เป็นค่าตั้งต้นตรง ๆ ไม่งั้นทุกฟิลเตอร์จะเริ่มจากศูนย์แล้วต้องไต่ขึ้นมาก่อนหลายวินาที ทั้งที่ค่าจริงอยู่ตรงนั้นตั้งแต่แรก · .value() กับ .reset() มีเหมือนกันทุกตัวในตระกูลนี้
| alpha | ผลที่เห็นบนจอ | เหมาะกับ |
|---|---|---|
| 0.05 | นิ่งมาก แต่ตามมือช้าจนรู้สึกหน่วง | ค่าที่เปลี่ยนช้า เช่น อุณหภูมิ |
| 0.2 | นิ่งพอควรและยังตามทัน (ค่าที่เราใช้วันนี้) | ลูกบิด เกจทั่วไป |
| 0.8 | เกือบเท่าค่าดิบ กรองได้นิดเดียว | สัญญาณที่ต้องการความไวสูง |

examples/s05/06_ema_time_constant.py↗ — ตารางข้างบนบอกว่า alpha ให้ "ความรู้สึก" แบบไหน ไฟล์นี้เปลี่ยนความรู้สึกนั้นให้เป็นวินาที · ภาพจับที่ขั้นแรกซึ่ง alpha = 1.00 คือไม่กรองเลย เส้นแดงจึงทับเส้นฟ้าสนิท และตัวเลขที่ตามมาคือ tau = 0 ms กับ "ถึง 63.2% ที่ตัวอย่างที่ 1" — ไล่กดเดินหน้าแล้ว alpha จะลดลง ทั้งสองค่านั้นจะโตขึ้นให้เห็นเป็นตัวเลข ไม่ใช่ความรู้สึก · ภาพหน้าจอจริงจากบอร์ด Eva Kit บันทึกโดยผู้สอนEMA ใช้หน่วยความจำแค่ตัวเลขเดียว จึงเป็นฟิลเตอร์ที่ระบบฝังตัวเล็ก ๆ เลือกใช้บ่อยที่สุด
med = dsp.Median(window=5) # window เป็น keyword-only เช่นเดียวกัน
clean = med.update(raw) # เก็บ 5 ค่าล่าสุด เรียงลำดับ แล้วคืนค่าตรงกลาง
dsp.Median(window=4) # ขอ 4 ได้ 5 — มันบังคับเป็นเลขคี่ให้เงียบ ๆ
dsp.Median(window=99) # ขอ 99 ได้ 15 — เพดานคือ 15 พื้นคือ 3
หนีบค่าเงียบ ๆ ไม่มี error เพราะบัฟเฟอร์ข้างในเป็นอาเรย์ขนาดคงที่ในภาษา C · ต้องเป็นเลขคี่เพราะ "ตัวกลาง" ของจำนวนคู่ไม่มีตัวเดียว · พิมพ์ print(med) แล้วมันบอกค่าที่ได้จริงมาให้ ไม่ต้องเดา

window=5 ในบรรทัดโค้ดบนสุดของสไลด์นี้คือความกว้างของกรอบที่กำลังเลื่อนอยู่นี้ และเหตุผลที่มันหน่วง ก็เพราะคำตอบของแต่ละรอบต้องรอให้กรอบเต็มก่อนไทย: เรียงค่าล่าสุด N ตัวแล้วหยิบตัวกลาง — ค่าที่กระโดดโดดเดี่ยวถูกดันไปอยู่ริมสุด ไม่มีใครหยิบมันขึ้นมา
คิดให้ดูจริง ๆ จากชุดตัวเลขในสไลด์ก่อน Median คืน 42 เหมือน spike ไม่เคยเกิด · ส่วน EMA() ที่ค่าเดิม 42 เจอ 95 ได้ คือกระเด็นตาม · ที่ลูป 200 ms หน้าต่าง 5 ค่า = 1.0 วินาที หน่วงราว 0.4 วินาที
dsp.EMA |
dsp.Median |
|
|---|---|---|
| เก่งเรื่อง | สั่นเล็ก ๆ ต่อเนื่อง | spike กระโดดเดี่ยว |
| วิธีคิด | ถ่วงน้ำหนักเก่า/ใหม่ | เรียงแล้วเลือกตัวกลาง |
| spike เดี่ยว | ถูกเฉลี่ยเข้าไปบางส่วน | หายไปทั้งก้อน |
| ความหน่วง | จางลงต่อเนื่อง | ราวครึ่งหนึ่งของ window |
| หน่วยความจำ | 1 ค่า | เท่ากับ window |
ใช้ร่วมกันได้: Median เก็บกวาด spike ก่อน แล้วส่งต่อให้ EMA · เลือกฟิลเตอร์จาก หน้าตาของ noise ไม่ใช่จากชื่อ
dsp ทั้งโมดูล (1/3) — 8 คลาส 8 ฟังก์ชัน ใช้ได้ครบทุกตัวทั้งสองบอร์ดคาบ 1 เราเรียก dsp.EMA dsp.Median และ dsp.tilt ไปแล้วอย่างละครั้ง ที่ examples/s01/13_raw_and_filtered.py↗ — สามชื่อจากสิบหก นี่คืออีกสิบสามชื่อที่เหลือ
คลาส — สร้างครั้งเดียวนอกลูป เพราะทุกตัวมีความจำ ทุกตัวมี .update() และ .reset()
| คลาส | อาร์กิวเมนต์ (keyword-only) | ค่าตั้งต้น | มีเมธอดอะไรอีก |
|---|---|---|---|
EMA |
alpha= |
0.1 | .value() |
SMA |
window= (หนีบ 2-64) |
10 | .value() |
LPF |
cutoff= fs= |
5.0 Hz, 100 Hz | .value() |
HPF |
cutoff= fs= |
0.5 Hz, 100 Hz | .value() |
Median |
window= (หนีบ 3-15 บังคับคี่) |
5 | .value() |
Kalman1D |
q= r= |
0.01, 0.1 | .value() |
Madgwick |
beta= fs= |
0.1, 100 Hz | .quaternion() — ไม่มี .value() |
Pedometer |
threshold= min_interval= |
1.5, 300 ms | ไม่มี .value() |
dsp ทั้งโมดูล (2/3) — 8 ฟังก์ชัน ไม่มีความจำ เรียกตรงได้เลย| ฟังก์ชัน | รับ | คืน | คาบไหน |
|---|---|---|---|
tilt(ax,ay,az) |
ความเร่ง 3 แกน | (roll, pitch) องศา |
คาบ 6 |
compass(mx,my,mz) |
สนามแม่เหล็ก 3 แกน | ทิศ 0-360 องศา | คาบ 6 |
altitude(hpa, sea=1013.25) |
ความดัน hPa (อาร์กิวเมนต์เรียงตำแหน่ง ไม่ใช่ keyword) | ความสูงเป็นเมตร | อ้างอิง |
dew_point(t, rh) |
องศา C, %RH | จุดน้ำค้าง องศา C | อ้างอิง |
heat_index(t, rh) |
องศา C, %RH | อุณหภูมิที่รู้สึก องศา C | อ้างอิง |
comfort_zone(t, rh) |
องศา C, %RH | สตริง: cold hot dry humid comfortable acceptable |
อ้างอิง |
fft_mag(ชุด, n=256, window=True, demean=True) |
list หรือ buffer int16 ทั้งชุด | ขนาดสเปกตรัม n/2 ค่า สเกล 2/N | คาบ 7 |
s16(บัฟเฟอร์, step=1) |
buffer int16 LE (เช่น mic.raw()) |
list ของจำนวนเต็ม | คาบ 7 |
สองแถวล่างเพิ่มเข้าโมดูลเมื่อ 2026-08-20 — firmware รุ่นก่อนหน้ายังไม่มี ใช้ในห้องต้องแฟลชรุ่นนั้นขึ้นไป
dsp ทั้งโมดูล (3/3) — สี่ฟังก์ชันอ้างอิง และกฎ "ป้อนทีละค่า"สี่ฟังก์ชันอ้างอิง (altitude dew_point heat_index comfort_zone) เป็นคณิตศาสตร์ล้วน เรียกได้ปกติทุกเมื่อ แต่ Eva Kit ไม่มี DPS368 (ความดัน) และไม่มี SHT40 (อุณหภูมิ/ความชื้น) จึง ไม่มีแหล่งข้อมูลบนบอร์ดให้ป้อน ป้อนตัวเลขที่พิมพ์เองหรือรับมาจากเครือข่ายได้ · Dev Kit มีชิปทั้งสองตัว (sensors.dps368 / sensors.sht40) จึงป้อนค่าจริงเข้าสี่ฟังก์ชันนี้ได้ — แต่ทั้งสี่ตัวยังอยู่ในตารางในฐานะแถวอ้างอิง ไม่ใช่งานลงมือของคาบนี้ ไม่ว่าบอร์ดไหน
ตัวกรองทุกตัวรับทีละค่า ป้อนทีละตัวอย่าง — ใครมาจาก numpy ต้องเปลี่ยนวิธีคิดตรงนี้ก่อน · ข้อยกเว้นเดียวของโมดูลคือคู่
fft_magกับs16ในตารางก่อนหน้า ที่รับทั้งชุด เพราะสเปกตรัมคำนวณจากตัวอย่างทั้งก้อนพร้อมกัน — คาบ 7 ได้ใช้จริง
| ตัวกรอง | วิธีคิด | เก่งเรื่อง | ราคาที่จ่าย |
|---|---|---|---|
SMA(window=8) |
เฉลี่ยตรง ๆ ของ N ค่าล่าสุด | เข้าใจง่าย อธิบายให้ใครก็ได้ฟัง | จำ N ค่า และหน่วงราวครึ่ง window |
LPF(cutoff=, fs=) |
EMA ที่ตั้งด้วย ความถี่ แทนน้ำหนัก | บอกเป็น Hz ได้ว่าตัดอะไรทิ้ง | ต้องรู้คาบลูปจริง ไม่งั้น fs โกหก |
HPF(cutoff=, fs=) |
เก็บเฉพาะส่วนที่ เปลี่ยนเร็ว | จับการสั่น การเคาะ การกระแทก | ทิ้งระดับของสัญญาณไปหมด |
Kalman1D(q=, r=) |
ชั่งน้ำหนักระหว่าง "เชื่อการวัด" กับ "เชื่อค่าเดิม" ทุกรอบ | ปรับตัวเองได้ นิ่งกว่า EMA ที่ความไวเท่ากัน | ต้องจูนสองตัวเลข ไม่ใช่ตัวเดียว |
LPF กับ EMA เป็นสมการเดียวกัน ต่างแค่ทางเข้า — LPF คำนวณ ให้จากค่า cutoff และ fs ที่เราบอก ส่วน EMA ให้เราใส่ เอง เลือกทางไหนก็ได้ แต่ ถ้าบอก fs=100 ทั้งที่ลูปเดินจริงที่ 5 Hz ตัวเลข cutoff ที่ตั้งไว้จะไม่เป็นความจริงเลย
HPF คืน 0.0 เสมอในรอบแรก ไม่ใช่บั๊ก — มันตอบว่า "เปลี่ยนไปเท่าไร" และรอบแรกยังไม่มีค่าก่อนหน้าให้ลบ · Kalman1D ที่ r สูงแปลว่าไม่ค่อยเชื่อเซนเซอร์ จึงนิ่งมากแต่ตามช้า ส่วน q สูงแปลว่าคิดว่าโลกเปลี่ยนเร็ว จึงไวขึ้น
ลงมือ:
examples/s05/08_six_filters_one_signal.py↗ ป้อนสัญญาณเส้นเดียวกันเข้าทั้งหกตัวพร้อมกัน แล้วโชว์.value()ของทุกตัวเรียงกัน — สัญญาณสร้างเองในไฟล์ จึงรันซ้ำได้ผลเดิมทุกครั้ง เถียงกันด้วยตัวเลขได้
ก่อนจะมีไมโครคอนโทรลเลอร์ราคาถูก การทำสัญญาณให้เรียบทำด้วยของจริงสองชิ้น คือตัวต้านทานต่อกับตัวเก็บประจุ เรียกว่าวงจร RC low-pass filter ความถี่สูง ๆ ถูกกลืนหายไปกับตัวเก็บประจุ เหลือแต่ส่วนที่เปลี่ยนช้า ๆ ออกมา
ถ้าเขียนสมการของวงจร RC ออกมาในรูปดิจิทัล จะได้หน้าตาเหมือน EMA ทุกประการ โดยที่
โมดูล dsp บนบอร์ดนี้มี dsp.LPF(cutoff=, fs=) ที่คำนวณ จากสูตรนี้ให้เลย — พูดง่าย ๆ คือ LPF กับ EMA เป็นตัวเดียวกัน ต่างกันแค่เราตั้งค่าด้วยความถี่ตัดหรือด้วยน้ำหนักโดยตรง
เชื่อมกับวันนี้: ตอนที่น้อง ๆ ปรับ alpha แล้วรู้สึกว่า "หน่วงขึ้น" นั่นคือการหมุนลูกบิดเสมือนของตัวเก็บประจุที่ไม่มีอยู่จริง วิศวกรรุ่นก่อนต้องบัดกรีเปลี่ยนตัว C เพื่อทำสิ่งเดียวกับที่เราแก้ตัวเลขบรรทัดเดียว
สิ่งที่เฟิร์มแวร์ทำให้แล้ว (70%)
ตั้งค่า SAR ADC และอ่านค่าจากขาลูกบิด (Eva: P15[1] · Dev Kit: VR1 บนฐาน) · สเกลค่า 12 บิตที่วัดได้ให้เป็นช่วง 0-65535 และคิดเปอร์เซ็นต์/โวลต์ให้ · คุย I2C กับชิป 4000T และหักลบ baseline ให้เรียบร้อย · ฟิลเตอร์ทั้งชุดใน dsp เขียนเป็นภาษา C มาแล้ว · การวาดทุกอย่างบนจอ
สิ่งที่เป็นงานของเรา (30%)
เลือกว่าจะแสดงค่าไหนด้วย widget อะไร · ตั้งค่า alpha ให้เหมาะกับงาน · จัดจังหวะลูปให้จอตามทันและเซนเซอร์ไม่ถูกอ่านถี่เกินจำเป็น · ตัดสินใจว่าค่าที่เห็นเชื่อถือได้หรือยัง
โจทย์ของวิศวกรวันนี้ไม่ใช่ "เขียนฟิลเตอร์" แต่คือ เลือกฟิลเตอร์และปกป้องตัวเลือกนั้นได้
import ui
ui.screen() # ล้าง widget เดิมทั้งหมด เริ่มจากจอว่างที่รู้แน่
import time
import sensors
import dsp
ui.clear()
time.sleep_ms(200)
...
# ไม่มี sensors.init() ทั้งสองบอร์ด (Eva: ขึ้น OSError / Dev Kit: ไม่จำเป็น)
try:
sensors.pot.read() # อุ่นเครื่อง รอบแรกหลังรีเซ็ตอาจต้องรอคอร์จอตอบ
except OSError:
print("อ่านเซนเซอร์รอบแรกยังไม่ได้ - ลองใหม่ในลูป")
ui.screen() มาก่อนเสมอ เพราะการใช้ ui.* ครั้งแรกจะสั่งหยุด sensor auto-task ของเฟิร์มแวร์ หลังจากนี้ เราต้องอ่านค่าเองทุกรอบในลูป · อุ่นเครื่องใน try/except เพราะรอบแรกหลังรีเซ็ตอาจต้องรอ (Eva วัดได้ถึง 16 วินาที) ยังไม่ตอบก็พิมพ์บอกแล้วลองใหม่ในลูป
บน Eva Kit บัส I2C ของเซนเซอร์เป็นของคอร์จอ (CM55) sensors.init()/scan() จึงถูก ปฏิเสธด้วย OSError (ขับบัสจากสองคอร์เคยทำให้บอร์ดค้างถาวร) · บน Dev Kit สองคำสั่งนั้นผ่าน เพราะบัส I2C ของ IMU เป็นของ CM33 เอง แต่เฟิร์มแวร์ปลุกให้ตั้งแต่บูต โค้ดของคอร์สจึง ไม่เรียกทั้งสองบอร์ด
เส้นทางของค่า — Eva: ทุกตัวมาจาก snapshot ของ CM55 · Dev Kit: pot.read() อ่าน SAR ตรง (ลูกบิดไม่อยู่บนบัส) IMU อ่านสดจาก CM33 ส่วนแถบสัมผัส 4000T และช่อง pot ใน snapshot() มาจากคอร์จอทุก 200 ms
pot_bar = ui.Bar(x=28, y=80, w=428, h=16, color=0x4A9EFF, min=0, max=100, value=0)
pot_scale = ui.Scale(x=28, y=92, w=428, h=44, color=COL_TEXT, min=0, max=100)
pot_scale.ticks(11, 2) # 11 ขีด ใส่เลขทุกขีดที่สอง
...
lbl_pct = ui.Label("0.0 %", x=28, y=180, color=COL_TEXT, value=28)
...
sp_th = ui.Spinbox(x=508, y=76, w=256, h=88, color=COL_TEXT,
min=TH_MIN, max=TH_MAX, value=th)
sp_th.digits(2, 0) # ไม่บอกจะเห็น 0070
btn_up = ui.Button("เพิ่ม", x=644, y=172, w=120, h=88, color=0x3A4150, value=16)
btn_dn = ui.Button("ลด", x=508, y=172, w=120, h=88, color=0x3A4150, value=16)
...
led_ok = ui.Led(x=492, y=304, w=48, h=48, color=COL_OK, value=1)
...
led_bad = ui.Led(x=492, y=348, w=48, h=48, color=COL_BAD, value=0)
ตัวเลข 55.4 % ลอย ๆ ตอบไม่ได้ว่าสูงหรือต่ำ ui.Scale คือไม้บรรทัดที่พาพิสัยมาอยู่บนจอด้วยกัน และเพราะเป็นไม้บรรทัด มันจึง ไม่รับ .value() ตัวที่ขยับคือ ui.Bar ที่วางทับ (หน้าปัดวงกลมมีเข็มจริง — คาบ 6) · .ticks(ทั้งหมด, ใส่เลขทุกกี่ขีด) คุมความหนาแน่น ที่นี่ 11 ขีดใส่เลขทุกขีดที่สอง ได้ 0 20 40 60 80 100 พอดี
เกณฑ์เตือนไม่ควรเป็นค่าคงที่ในโค้ด คนหน้างานคือคนรู้ว่างานนี้ยอมได้แค่ไหน ui.Spinbox หนีบค่าในพิสัยให้เอง แต่บนจอสัมผัส spinbox เปล่า ๆ นิ้วเปลี่ยนค่าไม่ได้ ตัวที่เพิ่มลดค่าจริงคือ ui.Button สองปุ่มข้าง ๆ (สูง 88 px ตามขนาดเป้าสัมผัสของหลักสูตร)
ไฟสองดวงแทนสถานะด้วยสีตัวอักษร เพราะไฟมี รูปทรงและความสว่าง ถ่ายจอขาวดำแล้วยังแยกออก และ .value(0) คือ หรี่ ไม่ใช่หาย โดยตั้งใจ · จำกฎคาบ 4: value= ของ ui.Label คือ ขนาดฟอนต์ ส่วน Bar/Scale ใช้ min/max เป็นพิสัยจริง — ตัวอย่างประกอบที่ examples/s04/09_scale_led_spinbox.py↗
ค่าดิบกับโวลต์ยังอยู่ครบ แต่ลดเป็นบรรทัดกำกับ เพราะคนเดินมาดูจอถามคำถามเดียวก่อน: ตอนนี้สูงเกินหรือยัง
touch_bar = ui.Bar(x=28, y=284, w=428, h=16, color=0x4A9EFF, min=0, max=100, value=0)
touch_scale = ui.Scale(x=28, y=300, w=428, h=40, color=COL_TEXT, min=0, max=100)
touch_scale.ticks(11, 2)
...
led_b0 = ui.Led(x=28, y=344, w=48, h=48, color=COL_OK, value=0)
...
led_b1 = ui.Led(x=160, y=344, w=48, h=48, color=COL_OK, value=0)
...
slider = sensors.capsense.slider() # 0 - 100 อยู่แล้ว ไม่ต้องแปลงหน่วย
b0, b1 = sensors.capsense.buttons() # คืน tuple สองช่อง แกะพร้อมกันได้เลย
...
touch_bar.value(max(0, min(100, slider)))
...
led_b0.value(1 if b0 else 0)
led_b1.value(1 if b1 else 0)
ui.Bar ไม่รับการแตะและไม่ส่ง event มันเป็น จอแสดงผลอย่างเดียว ซึ่งตรงกับที่ต้องการพอดี เพราะตัวสั่งงานคือนิ้วบนแถบทองแดง ไม่ใช่นิ้วบนกระจก และไม้บรรทัดชุดเดิมถูกใช้ซ้ำด้วยเหตุผลเดียวกับการ์ดบน: 62 ไม่มีความหมายจนกว่าจะรู้ว่าเต็มสเกลคือเท่าไร
สถานะปุ่มทองแดงเคยเขียนเป็นข้อความ BTN0 ON BTN1 - ซึ่ง ไม่ผ่านการทดสอบขาวดำ — เป็นเกรย์สเกลแล้ว ON สีม่วงกับขีดสีเทาแยกกันไม่ออก ไฟสองดวงแยกออกทันทีเพราะดวงที่ติดสว่างกว่าดวงที่หรี่
ครอบด้วย max(0, min(100, ...)) ไม่ใช่เพราะไม่เชื่อไดรเวอร์ แต่ถ้าชิป 4000T ไม่พร้อม ไบต์ที่ได้อาจเป็น 255 การครอบทำให้จอยังแสดงผลได้แทนที่จะพังทั้งหน้า
ค่าที่มาจากภายนอกโปรแกรมเราเสมอ ๆ ควรถูก ตรวจขอบเขตก่อนใช้ นี่คือนิสัยที่ติดตัวไปทุกภาษา
lbl_health = ui.Label("อ่านค่าปกติ", x=292, y=364, color=COL_TEXT, value=16)
...
ema = dsp.EMA(alpha=0.2) # สร้างครั้งเดียว นอกลูป
...
ema_pct = ema.update(pct) # ป้อนค่าใหม่ ได้ค่าที่กรองแล้วกลับมาทันที
...
raw_line.text("ดิบ {:.2f} %".format(pct))
ema_line.text("กรอง {:.2f} %".format(ema_pct))
...
if not fresh: # รอบนี้อ่านเซนเซอร์ไม่ได้
lbl_health.color(COL_WARN)
health = "ค่าค้าง - เลขคือค่าล่าสุด"
...
lbl_health.text(health) # นอกประตูหนึ่งวินาที - ส่งซ้ำทุกรอบโดยตั้งใจ
จุดที่พลาดบ่อยที่สุดคือ เผลอสร้างฟิลเตอร์ไว้ในลูป — ทุกรอบได้ฟิลเตอร์ใหม่ที่ความจำว่าง ค่าที่ออกมาเท่าค่าดิบเป๊ะ แล้วสรุปผิดว่า "ฟิลเตอร์ไม่ทำงาน" · ทศนิยมสองตำแหน่งทั้งสองบรรทัดเป็นเรื่องจงใจ ปัดเหลือจำนวนเต็มเมื่อไร ความสั่นถูกซ่อน บทเรียนทั้งคาบหายไปด้วย
บรรทัด คุณภาพของค่า แก้ปัญหาที่ไม่มีใครเห็นจนกว่าจะเกิด: อ่านรอบนี้ขึ้น OSError โปรแกรมใช้ค่าเดิมต่อ จอจึงยังโชว์ตัวเลขสวยงามทั้งที่ไม่มีค่าใหม่แล้ว ค่าที่ค้างจึงต้องเขียนกำกับว่าไม่ใช่ค่าปัจจุบัน · สังเกตลำดับ ตั้งสีก่อน แล้วค่อยเขียนข้อความ — สองคำสั่งข้ามคอร์คนละครั้ง สลับกันจะมีเสี้ยววินาทีที่ข้อความใหม่ใส่สีของสถานะเก่า
lbl_health.text(health) อยู่ นอก ประตูหนึ่งวินาทีโดยตั้งใจ — ฝั่งจออยู่ โหมดเร็ว ต่อไปอีก 500 ms ทุกครั้งที่ได้คำสั่งเขียนข้อความ แต่ .value() ของแถบกับไฟ ไม่ปลุก โหมดนั้น ลูปที่มีแต่แถบกับไฟจึงถอยไปโหมดช้า ภาพกระตุก · ข้อความเท่าเดิมเฟิร์มแวร์ไม่วาดซ้ำ เราจ่ายแค่ค่าส่ง
อยากเห็นฟิลเตอร์ทำงาน ต้อง แสดงคู่กันบนจอเดียว ถ้าดูทีละอันจะไม่มีวันรู้ว่ามันช่วยจริงหรือแค่รู้สึกไปเอง
while True:
fresh = True
try:
raw = sensors.pot.read()
volts = sensors.pot.voltage()
pct = sensors.pot.percent()
slider = sensors.capsense.slider()
b0, b1 = sensors.capsense.buttons()
except OSError:
fresh = False # ใช้ค่าเดิมต่อ แต่ต้องบอกคนดู
b0, b1 = 0, 0
pot_bar.value(int(max(0, min(100, pct))))
# ... ไฟกับแถบอัปเดตทุกรอบ ...
for ev in ui.poll(): # ต้องมีทุกรอบ ปุ่มถึงจะกดติด
...
sec = time.ticks_ms() // 1000
if sec != last_sec: # ตัวเลขเปลี่ยนวินาทีละครั้ง
last_sec = sec
lbl_pct.text("{:.1f} %".format(pct))
time.sleep_ms(200) # จังหวะที่ผ่านการทดสอบ
ui.poll() มีหน้าที่มากกว่ารับ event จากการแตะจอ มันคือจังหวะที่ฝั่ง Python เปิดโอกาสให้ระบบ UI จัดการคิวของตัวเอง ถ้าไม่เรียก widget บางตัวจะถูกซ่อนไว้นานถึงสองวินาที คาบนี้เราไม่ได้ใช้ event เลย แต่ยังต้องเรียกทุกรอบอยู่ดี
ทำไม 200 ms ไม่ใช่ 20 ms — เพราะลูปที่เร็วเกินไปจะยิงคำสั่งวาดข้ามคอร์ถี่กว่าที่ CM55 วาดทัน เฟรมส่วนเกินจะถูกทิ้งเงียบ ๆ ไม่มี error ให้เห็น ผลคือจอกระตุกโดยที่โค้ดดู "ถูกต้อง" ทุกบรรทัด
อีกมุมหนึ่ง 200 ms = อ่านเซนเซอร์ 5 ครั้งต่อวินาที ซึ่งเร็วกว่าที่ตาคนแยกออกอยู่แล้วสำหรับการหมุนลูกบิดด้วยมือ
แต่แถบกับตัวเลขไม่ได้เดินจังหวะเดียวกัน และนี่คือรายละเอียดที่แยกหน้าจอควบคุมออกจากหน้าจอสาธิต แถบและไฟอัปเดตทุกรอบ คือ 5 ครั้งต่อวินาที เพราะตาคนอ่าน "ตำแหน่ง" ได้โดยไม่ต้องหยุดอ่าน ส่วน ตัวเลขที่ต้องอ่านเป็นตัวเลข เขียนใหม่ไม่เกินวินาทีละครั้ง ตัวเลขทศนิยมที่วิ่งห้าครั้งต่อวินาทีคือตัวเลขที่อ่านไม่ทัน แล้วคนจะเลิกอ่านมันไปเลย ประตูที่ใช้กั้นคือบรรทัด if sec != last_sec: ซึ่งเทียบแค่ว่า "วินาทีเปลี่ยนแล้วหรือยัง"
for ev in ui.poll(): ทำสองอย่างพร้อมกันในบรรทัดเดียว คือเรียก poll ตามกฎ และรับ event ของปุ่มเพิ่ม/ลดเกณฑ์ ถ้าลืมบรรทัดนี้ อาการที่เห็นจะเป็น "จอกระตุกและปุ่มกดไม่ติด" ซึ่งดูเหมือนของสองเรื่องแต่มีสาเหตุเดียว
เร็วกว่าที่จำเป็นไม่ได้แปลว่าดีกว่า ในระบบฝังตัวมันมักแปลว่า เปลืองพลังงานและได้ภาพที่แย่ลง
สังเกตว่าสองเส้นทางต้นทางต่างกันคนละแบบ (ADC กับ I2C) แต่มาบรรจบเป็นโค้ด Python บรรทัดเดียวกันที่ CM33
practise_codes/s05_pot_capsense.py↗ ใน BENTO IDEpass ให้ครบทั้ง 6 จุดตามคำใบ้ # เติม: — เติมทีละจุด รันดูทีละครั้งหมายเหตุผู้สอน: ขึ้น OSError: CapSense: CM55 did not answer... ในไม่กี่บรรทัดแรก ให้รอสักครู่แล้วรันซ้ำก่อน (หลังรีเซ็ต คอร์จอต้องใช้เวลาก่อนตอบสายเซนเซอร์ — Eva วัดได้ราว 13 วินาที) ยังขึ้นซ้ำค่อยตรวจว่าชิป PSoC 4000T ถูก flash แล้วหรือยัง · ขึ้น OSError ที่ sensors.init() = ลอกโค้ดรุ่นเก่ามา ให้ลบบรรทัดนั้นทิ้ง (บน Dev Kit ผ่านเงียบ ๆ ก็ลบเหมือนกัน) · ลูกบิดและ CapSense มีทั้ง Eva Kit และ Dev Kit — AI Kit เปล่า ๆ ที่ไม่มีฐาน QWA309 ไม่มีทั้งสองอย่าง · เจอ error ตอนอ่าน CapSense ให้เรียกผู้สอนก่อน อย่าเสียเวลาทั้งคาบแก้โค้ดที่ไม่ได้ผิด
หมุน pot คุม ui.Bar บน ui.Scale + ตั้งเกณฑ์ด้วย ui.Spinbox + ui.Led บอกสถานะ + นิ้วเลื่อน CapSense คุม ui.Bar + แสดง raw/filtered สองเส้นเทียบกัน
แปลเป็นสิ่งที่ตรวจได้จริง
alpha เป็น 0.05 กับ 0.8 ผลต่างกันอย่างไรข้อที่ยากที่สุดไม่ใช่ข้อที่โค้ดยาวที่สุด แต่คือข้อสุดท้ายที่ต้อง อธิบายพฤติกรรมได้
| อาการ | สาเหตุที่แท้จริง | วิธีแก้ |
|---|---|---|
| จอขึ้นครบแต่ทุกค่าค้างที่ 0 | ยังไม่ได้เติมจุดอ่านค่าในลูป | เติม sensors.pot.percent() / capsense.slider() |
TypeError ตอนสร้างฟิลเตอร์ |
เขียน dsp.EMA(0.2) |
keyword-only: dsp.EMA(alpha=0.2) |
| บรรทัด EMA เท่ากับ RAW ทุกรอบ | สร้าง dsp.EMA() ไว้ในลูป ความจำถูกล้าง |
ย้ายไปสร้างครั้งเดียวนอกลูป |
| ปุ่มสัมผัสรายงานกลับด้าน | วางนิ้วค้างตอน บอร์ดบูต | ยกนิ้วออกให้หมดแล้ว ถอด USB เสียบกลับ (Dev Kit: ห้ามโยกสวิตช์บนฐาน นั่นคือสวิตช์ไฟ) |
OSError ที่บรรทัด sensors.init() |
บน Eva Kit เฟิร์มแวร์ปฏิเสธคำสั่งนี้ (บน Dev Kit ผ่านเงียบ ๆ แต่ไม่จำเป็น) | ลบบรรทัดนั้นทิ้ง ไม่ต้องมี init เลย ทั้งสองบอร์ด |
OSError: ... CM55 did not answer ที่บรรทัดแรก ๆ |
เพิ่งรีเซ็ต คอร์จอยังไม่ตอบสายเซนเซอร์ | รอสักครู่แล้วรันซ้ำ ถ้ายังซ้ำค่อยแจ้งผู้สอน |
| widget หายไปเป็นวินาที จอกระตุก | ลืม ui.poll() ในลูป |
ใส่ ui.poll() ทุกรอบก่อน sleep_ms |
ui.Bar ไม่ขยับทั้งที่ค่าเปลี่ยน |
ส่งค่าทศนิยมเข้าไป | ครอบด้วย int() และคุมช่วง 0-100 |
| อาการ | สาเหตุที่แท้จริง | วิธีแก้ |
|---|---|---|
ui.Scale ไม่ขยับเลยสักครั้ง |
มันคือไม้บรรทัด ไม่รับ .value() |
ตัวที่ต้องขยับคือ ui.Bar ที่วางทับ (แบบวงกลมมีเข็มจริงผ่าน .prop(ui.PROP_SCALE_NEEDLE, ...) — fw 2026-08-20 ขึ้นไป ดูคาบ 6) |
| ตัวเลขบนไม้บรรทัดเบียดกันจนอ่านไม่ออก | ขีดเยอะเกินไปสำหรับความกว้างที่มี | .ticks(ทั้งหมด, ใส่เลขทุกกี่ขีด) เช่น (11, 2) |
ui.Spinbox แตะแล้วค่าไม่เปลี่ยน |
จอสัมผัสไม่มีลูกบิดหมุน การแตะแค่เลือกตำแหน่งหลัก | ต้องมี ui.Button เพิ่ม/ลดข้าง ๆ เสมอ |
ช่องเกณฑ์ขึ้น 0070 ทั้งที่ตั้ง 70 |
ค่าตั้งต้นของ spinbox คือสี่หลัก | sp.digits(2, 0) = สองหลัก ไม่มีจุดทศนิยม |
| ไฟดับแล้วยังเห็นเป็นวงจาง ๆ | ตั้งใจ .value(0) คือหรี่ ไม่ใช่หาย |
ไฟที่หายไปทำให้แยกไม่ออกว่าดับหรือจอเสีย |
| เปลี่ยนสีแถบตอนเกินเกณฑ์แล้วไม่ได้ผล | .color() ของ ui.Bar ไปลงที่ "ราง" ไม่ใช่แถบค่า |
บอกสถานะด้วยไฟกับตัวหนังสือแทน |
| ค่าเซนเซอร์ค้างเป็นค่าเดียวตลอด | ใช้ ui.* แล้ว sensor auto-task หยุด |
อ่านเซนเซอร์เองในลูปทุกรอบ |
OSError ที่บรรทัด sensors.scan() |
บน Eva Kit เฟิร์มแวร์ปฏิเสธเพื่อกันบัสชนกัน (บน Dev Kit ผ่าน) | ห้ามใช้คำสั่งนี้ในคอร์สนี้ทั้งสองบอร์ด ใช้ sensors.snapshot() แทน |
| อาการ | สาเหตุที่แท้จริง | วิธีแก้ |
|---|---|---|
AttributeError ตอนอ่าน pot |
รันบน AI Kit เปล่า ๆ ที่ไม่มีฐาน | ลูกบิดและ CapSense มีบน Eva Kit และ Dev Kit |
AttributeError: sht40 / dps368 |
สองชิปนี้ไม่มีบน Eva Kit ธง BSP ตั้งไว้ 0 (Dev Kit มี) | บน Eva ไม่มีทางแก้ด้วยโค้ด ใช้ตัวเลขที่พิมพ์เองป้อน dsp.dew_point() แทน |
Median(window=4) แล้วได้ 5 |
หนีบ 3-15 และบังคับเป็นเลขคี่ เงียบ ๆ | ไม่ใช่บั๊ก · print(med) บอกค่าที่ได้จริง |
SMA(window=200) แล้วนิ่งน้อยกว่าที่คิด |
หนีบเพดานไว้ 64 เงียบ ๆ เหมือนกัน | ขอเกินได้ แต่ไม่ได้ตามขอ ตรวจด้วย print() |
HPF บรรทัดแรกได้ 0.0 ทุกครั้ง |
ยังไม่มีค่าก่อนหน้าให้ลบ | ปกติ ทิ้งค่าแรกไปหนึ่งรอบ |
LPF(cutoff=2, fs=100) แต่กรองไม่เหมือนที่คำนวณ |
ลูปจริงเดิน 5 Hz ไม่ใช่ 100 Hz | fs ต้องเท่ากับคาบลูปจริง ไม่ใช่ค่าตั้งต้น |
sensors.auto_rate(5) แล้วไม่มีอะไรเปลี่ยน |
หนีบไว้ 20 ms และบน Eva Kit งานเบื้องหลังก็ไม่ได้เดินอยู่แล้ว | "เรียกได้" ไม่ได้แปลว่า "มีผล" |
เจ็ดในยี่สิบสามข้อนี้ไม่ได้เกิดจากโค้ดผิด แต่เกิดจาก ลำดับการทำงานกับฮาร์ดแวร์ อีกสี่ข้อเกิดจาก ค่าที่ถูกหนีบเงียบ ๆ และอีกเจ็ดข้อล่างสุดเกิดจาก การเข้าใจ widget ผิดตัว — ทั้งสามกลุ่มไม่มี error ให้เห็นสักบรรทัด
เปิด practise_codes/s05_pot_capsense.py↗ มีช่องว่างให้เติม 6 จุด
# เติม: ema = dsp.EMA(alpha=0.2)
pass
while True:
try:
# เติม: pct = sensors.pot.percent()
pass
# เติม: slider = sensors.capsense.slider()
pass
except OSError:
fresh = False
# เติม: pot_bar.value(int(max(0, min(100, pct))))
pass
# เติม: ema_pct = ema.update(pct)
pass
# เติม: events = ui.poll()
events = []
pass
ไฟล์ฝึกตั้งค่าเริ่มต้นไว้ให้แล้ว (pct = 0.0, slider = 0, events = []) โปรแกรมจึง รันได้ตั้งแต่ยังไม่เติมอะไรเลย แต่ตัวเลขจะนิ่งสนิท ใช้ตรงนี้เป็นเครื่องมือ: เติมทีละจุด รันทีละครั้ง แล้วดูว่าจอ "ตื่น" ขึ้นทีละส่วน
หน้าจอทั้ง 33 ชิ้นเขียนไว้ให้ครบแล้ว ไม่ต้องแตะ งานของเราคือทำให้ค่าไหลเข้าไปในนั้น จุดที่ 6 คือจุดที่คนข้ามบ่อยที่สุด เพราะไฟล์รันได้อยู่แล้วโดยไม่มีมัน อาการที่ตามมาคือปุ่มเพิ่ม/ลดเกณฑ์กดไม่ติด ซึ่งไม่มี error ให้เห็นเลยสักบรรทัด
ต้องทำในคาบ · เปิดตามลำดับนี้ ทั้งชุดราว 40 นาที
| ลำดับ · เรื่อง · เวลา | ไฟล์ | ลงมือทำอะไร แล้วจะเข้าใจอะไร |
|---|---|---|
| 1 · เลขจาก ADC ไม่ใช่โวลต์ · 10 นาที | examples/s05/05_adc_counts_to_volts.py↗ |
ลดจำนวนบิตทีละท่าแล้วดูบันไดหยาบขึ้น · จะแปลงเลขดิบเป็นโวลต์ได้เอง เพราะรู้แล้วว่าต้องรู้แรงดันเต็มสเกลกับจำนวนบิตเสมอ |
| 2 · แถบสัมผัสคุมความสว่าง · 15 นาที | examples/s05/01_capsense_dimmer.py↗ |
ลากนิ้วแล้วปล่อย แล้วดูว่าเส้นแดงค้างอยู่ตอนไม่มีนิ้ว · จะรู้ว่า slider() เท่ากับ 0 ไม่ได้แปลว่าปล่อยนิ้ว ต้องดูว่าค่าหยุดเปลี่ยนแทน |
| 3 · alpha แปลเป็นวินาทีได้ · 15 นาที | examples/s05/06_ema_time_constant.py↗ |
เดินทีละท่าเปลี่ยน alpha แล้วอ่านค่า tau ที่ไฟล์คำนวณให้ · จะเลือก alpha ของ MVP จากตัวเลข ไม่ใช่จากการเดา |
ติดตรงไหน เปิดอันนี้
| อาการที่เจอ | ไฟล์ที่ตอบอาการนั้น |
|---|---|
| แตะแถบสัมผัสแล้วไม่แน่ใจว่าบอร์ดรับไปหรือยัง โดยเฉพาะตอนมือเปียก | examples/s05/02_capsense_menu_wet_hand.py↗ — ปุ่มสัมผัสไม่มีแรงต้านให้นิ้วรู้สึก จึงต้องตอบกลับด้วยจอ ไฟ หรือเสียงทุกครั้ง |
| กราฟที่กรองแล้วยังกระโดดตามค่าหลุดค่าเดียวอยู่ดี | examples/s05/07_median_beats_mean.py↗ — ค่าเฉลี่ยเอาค่าที่หลุดมาบวกด้วย ส่วน median เรียงแล้วหยิบตัวกลาง ค่าหลุดจึงไม่มีสิทธิ์ถูกหยิบ · หน้าต่าง N ทน spike ที่ติดกันได้ไม่เกิน (N-1)//2 ตัว |
| ปล่อยมือจากลูกบิดแล้ว ค่าที่รายงานยังกระดิกไม่หยุด | examples/s05/03_pot_setpoint_deadband.py↗ — dead-band ตัดการกระดิกทิ้งโดยผู้ใช้ไม่รู้สึกว่าเสียอะไร นิสัยนี้ใช้ยาวไปถึงคาบ 10 ตอนต้องลดจำนวนข้อความที่ส่งออก |
อ่านเสริมนอกเวลา — เรื่องนี้อยู่นอกเกณฑ์ผ่านของคาบ 5 แต่ตอบคำถามที่ทีมเสียงมักติด: examples/s05/04_pot_taper_volume.py↗ ครึ่งทางของลูกบิดไม่ใช่ครึ่งหนึ่งของความดังที่หูได้ยิน หูตอบสนองเป็นลอการิทึม การแมปตรง ๆ จึงรู้สึกว่าดังพรวดตั้งแต่ยังหมุนไม่ถึงไหน
โปรแกรมที่รันได้แต่ยังไม่ถูกต้อง ดีบักง่ายกว่าโปรแกรมที่รันไม่ขึ้นเสมอ
ฝั่งอิเล็กทรอนิกส์และการวัด
การแปลงอนาล็อกเป็นดิจิทัล · ความละเอียด (บิต) กับช่วงการวัด · แรงดันอ้างอิงและผลของมันต่อค่าที่อ่านได้ · การตรวจจับแบบ capacitive · แนวคิด baseline และการวัดแบบส่วนต่าง
ฝั่งการประมวลผลสัญญาณ
noise มาจากไหนและมีกี่ชั้น · ค่าเฉลี่ยถ่วงน้ำหนักแบบมีความจำ (EMA) · ตัวกลางแบบหน้าต่างเลื่อน (Median) · การแลกกันระหว่างความนิ่งกับความหน่วง
ฝั่ง Python และการออกแบบระบบ
ออบเจกต์ที่เก็บสถานะข้ามรอบลูป · keyword-only arguments · การตรวจขอบเขตค่าจากภายนอก · การออกแบบหน้าจอให้หนึ่งค่าเล่าได้หลายหน่วย
เรื่อง trade-off ระหว่าง "นิ่ง" กับ "ไว" จะกลับมาหาน้อง ๆ อีกในคาบ 6 และ 7 แต่คราวนั้นข้อมูลจะมาจาก IMU
วันนี้เราได้:
อ่านค่าอนาล็อกจากลูกบิดครบสามหน่วยและอธิบายที่มาของแต่ละหน่วยได้ · เข้าใจว่า ADC ปัดค่าอย่างไรและทำไมบิตล่างถึงกระพริบ · อ่านปุ่มสัมผัสและแถบเลื่อนพร้อมรู้เรื่อง baseline · ใช้ dsp.EMA และ dsp.Median แล้วเห็นผลเทียบกันบนจอเดียว
การบ้านของทีม: เลือกทำ 1 ข้อจากสี่ข้อในสไลด์ถัดไป บันทึกลง worksheet ข้อ 8
คาบหน้า: เราจะเปลี่ยนจากลูกบิดที่มีคนหมุน ไปเป็นเซนเซอร์ที่วัดโลกจริง — sensors.bmi270.motion() กับ dsp.tilt() แล้วสร้างเครื่องวัดระดับดิจิทัลที่บอกองศาการเอียงของบอร์ดได้จริง ฟิลเตอร์ที่เรียนวันนี้จะได้ใช้ทันทีในคาบหน้า
เก็บค่า
alphaที่ทีมชอบไว้ให้ดี คาบหน้าจะได้ไม่ต้องลองใหม่ตั้งแต่ศูนย์
s05_pot_capsense.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
TH_MIN, TH_MAX, TH_STEP = 10, 95, 5
th = 70 # เกณฑ์ตั้งต้น หน่วยเปอร์เซ็นต์
# ไม่มี sensors.init() ในไฟล์นี้ และไม่ควรมี (ทั้งสองบอร์ด)
try:
sensors.pot.read() # อุ่นเครื่อง รอบแรกหลังรีเซ็ตอาจต้องรอคอร์จอตอบ
except OSError:
print("อ่านเซนเซอร์รอบแรกยังไม่ได้ - ลองใหม่ในลูป")
เฉลยรุ่นนี้เปลี่ยนจานสีทั้งชุด จากเดิมที่ให้ สีบอกหน่วย (เขียว = ค่าดิบ, ฟ้า = เปอร์เซ็นต์, เหลือง = โวลต์) มาเป็นจานสีของแผงควบคุมจริง ที่ สีบอกสถานะ เท่านั้น
เหตุผลไม่ใช่ความชอบ: สีเหลืองในหน้าจออุตสาหกรรมแปลว่า "เฝ้าระวัง" ตายตัว การเอาไปใช้กับบรรทัดโวลต์ที่ปกติดีทุกอย่าง คือการสอนให้ตาคนชินกับสีเตือน แล้ววันที่มันเตือนจริง จะไม่มีใครเห็น หน่วยของแต่ละบรรทัดบอกด้วย คำ ได้อยู่แล้ว ไม่ต้องยืมสีมาบอก
ค่าสีถูกตั้งเป็นค่าคงที่ชื่ออ่านรู้เรื่องไว้บนสุด ไม่ได้ใส่เลข 0x30A46C ลงไปกลางโค้ดตรง ๆ เพราะวันหนึ่งที่อยากเปลี่ยนธีมทั้งจอ เราจะแก้ที่เดียวจบ — ค่าทั้งชุดคือจานสีของหลักสูตร (พื้น 0x0E1116 · การ์ด 0x171B22 · ปุ่มรอง 0x3A4150 · ตัวหนังสือ 0xE8EAED · จาง 0x9AA3AF · ปกติ/เตือน/เสีย 0x30A46C 0xF5A623 0xE5484D) · การอุ่นเครื่องอยู่ใน try/except เพราะ OSError รอบแรกหลังรีเซ็ตเป็นเรื่องปกติทั้งสองบอร์ด ไม่ใช่ความผิดพลาด
เลขฐานสิบหกที่โผล่กลางโค้ดโดยไม่มีชื่อ คือหนี้ที่คนอ่านคนถัดไปต้องมาจ่าย
ui.Label("แผงคุมลูกบิดกับแถบสัมผัส", x=16, y=8, color=COL_TEXT, value=24)
# การ์ดบนซ้าย - ค่าที่วัดได้ พร้อมพิสัยของมันเอง
ui.Panel(x=12, y=40, w=468, h=200, color=COL_CARD, min=COL_DIM, max=12, value=1)
ui.Label("ลูกบิดเทียบพิสัย 0-100", x=28, y=48, color=COL_DIM, value=20)
pot_bar = ui.Bar(x=28, y=80, w=428, h=16, color=0x4A9EFF, min=0, max=100, value=0)
pot_scale = ui.Scale(x=28, y=92, w=428, h=44, color=COL_TEXT, min=0, max=100)
pot_scale.ticks(11, 2)
...
lbl_pct = ui.Label("0.0 %", x=28, y=180, color=COL_TEXT, value=28)
lbl_raw = ui.Label("ค่าดิบ 0", x=252, y=148, color=COL_DIM, value=16)
lbl_volt = ui.Label("โวลต์ 0.000 V", x=252, y=172, color=COL_DIM, value=16)
# การ์ดบนขวา - เกณฑ์ที่ผู้ใช้ตั้งเอง กับไฟสองดวง
ui.Panel(x=492, y=40, w=288, h=224, color=COL_CARD, min=COL_DIM, max=12, value=1)
...
sp_th = ui.Spinbox(x=508, y=76, w=256, h=88, color=COL_TEXT,
min=TH_MIN, max=TH_MAX, value=th)
sp_th.digits(2, 0)
btn_up = ui.Button("เพิ่ม", x=644, y=172, w=120, h=88, color=0x3A4150, value=16)
btn_dn = ui.Button("ลด", x=508, y=172, w=120, h=88, color=0x3A4150, value=16)
...
led_ok = ui.Led(x=492, y=304, w=48, h=48, color=COL_OK, value=1)
...
led_bad = ui.Led(x=492, y=348, w=48, h=48, color=COL_BAD, value=0)
นับ widget ให้ครบทุกครั้งก่อนรัน: 33 ชิ้น จากเพดาน 64 เหลือที่ว่างอีก 31 ชิ้นสำหรับงานต่อยอด · ui.Panel สี่ใบไม่ได้มีไว้ให้สวย มันคือเส้นที่บอกตาว่า "ของกลุ่มนี้เกี่ยวกันนะ" หน้าจอที่ของทุกชิ้นลอยอยู่บนพื้นเดียวกันหมด คนดูต้องจัดกลุ่มเองด้วยสายตาทุกครั้งที่มอง
ทุกตัวถูกสร้างด้วยข้อความตั้งต้นที่มีรูปแบบเหมือนตอนมีค่าจริง เช่น "โวลต์ 0.000 V" ไม่ใช่ "..." เพราะการจองความกว้างไว้ตั้งแต่แรกทำให้ข้อความไม่กระตุกเวลาความยาวเปลี่ยน
ข้อควรรู้เรื่องความยาวข้อความ: ข้อความที่เดินทางไปกับตอนสร้าง widget มีท่อกว้าง 95 ไบต์ ภาษาไทยตัวละ 3 ไบต์ แปลว่าราว 31 ตัวอักษร ยาวกว่านั้น ถูกตัดเงียบ ๆ ไม่มี error ให้เห็น ส่วน .text() ที่เรียกทีหลังมีท่อกว้างกว่า คือ 126 ไบต์ ป้ายยาว ๆ จึงควรตั้งสั้นตอนสร้าง แล้วค่อยเขียนเต็มด้วย .text()
ออกแบบหน้าจอโดยคิดถึง "ตอนมีค่าจริง" เสมอ ไม่ใช่ตอนที่ยังว่างเปล่า
ema = dsp.EMA(alpha=0.2) # สร้างนอกลูป
last_sec = -1
while True:
fresh = True
try:
raw = sensors.pot.read()
volts = sensors.pot.voltage()
pct = sensors.pot.percent()
slider = sensors.capsense.slider()
b0, b1 = sensors.capsense.buttons()
except OSError:
fresh = False
b0, b1 = 0, 0
pot_bar.value(int(max(0, min(100, pct))))
touch_bar.value(max(0, min(100, slider)))
ema_pct = ema.update(pct)
over = ema_pct >= th # หนึ่งดวงติดเท่านั้น
led_ok.value(0 if over else 1)
led_bad.value(1 if over else 0)
led_b0.value(1 if b0 else 0)
led_b1.value(1 if b1 else 0)
for ev in ui.poll():
if ev.get("type") != "clicked":
continue
if ev.get("handle") == btn_up.id():
th = min(TH_MAX, th + TH_STEP)
sp_th.value(th)
elif ev.get("handle") == btn_dn.id():
th = max(TH_MIN, th - TH_STEP)
sp_th.value(th)
sec = time.ticks_ms() // 1000 # ตัวเลขเปลี่ยนวินาทีละครั้ง
if sec != last_sec:
last_sec = sec
lbl_pct.text("{:.1f} %".format(pct))
lbl_raw.text("ค่าดิบ {}".format(raw))
lbl_volt.text("โวลต์ {:.3f} V".format(volts))
raw_line.text("ดิบ {:.2f} %".format(pct))
ema_line.text("กรอง {:.2f} %".format(ema_pct))
time.sleep_ms(200)
try เดียว และเกณฑ์จากค่าที่กรองแล้วการอ่านทั้งห้าบรรทัดอยู่ใน try เดียวกันโดยตั้งใจ: ถ้ารอบนี้คอร์จอไม่ตอบ เราไม่อยากได้ครึ่งชุดเก่าครึ่งชุดใหม่ปนกันบนจอ ค่าทั้งหน้าจะเป็นชุดเดียวกันเสมอ และธง fresh จะไปบอกบรรทัดคุณภาพของค่าว่าตัวเลขที่เห็นตอนนี้เป็นของรอบก่อน
over = ema_pct >= th ใช้ค่าที่ กรองแล้ว ไม่ใช่ค่าดิบ ถ้าใช้ค่าดิบ ไฟจะกระพริบสลับดวงตอนค่าอยู่คาบเกี่ยวกับเกณฑ์พอดี ซึ่งเป็นอาการที่แผงควบคุมจริงเรียกว่า alarm chattering และเป็นเหตุผลหนึ่งที่คนหน้างานปิดเสียงเตือนทิ้ง
ลองแก้
alphaเป็น 0.05 แล้ว 0.8 ดู แล้วจ้องสองบรรทัดขวาล่างสิบวินาที — บทเรียนทั้งคาบอยู่ในความต่างนั้น
ท่า 1 ตั้งจอ แล้วรอให้คอร์จอตอบ มาก่อน เพราะถ้าจอไม่ขึ้นหรือ sensors.pot.read() รอบอุ่นเครื่องยังขึ้น OSError ท่าที่เหลือไม่มีทางถูกต้องได้เลย
ท่า 2 ลูกบิดกับเกณฑ์ มาก่อน CapSense เพราะลูกบิดต่ออยู่กับ ADC ในชิปหลักโดยตรง ไม่ต้องพึ่งชิปตัวที่สองและไม่ต้องพึ่งบัส I2C — ถ้าท่านี้ผ่าน แปลว่าเส้นทาง sensor → Python → จอ ใช้ได้แล้วทั้งเส้น
ท่านี้พ่วง พิสัย (ui.Scale ใต้แถบ) กับ เกณฑ์ (ui.Spinbox และไฟสองดวง) ไว้ด้วยกัน เพราะมันคือคำถามเดียวกัน: ค่านี้เทียบกับอะไร — แยกไปคนละท่า ผู้เรียนจะจำได้แค่ว่า "มี widget อีกตัว" ไม่ใช่ว่า "ค่าที่วัดได้ห้ามอยู่ลำพัง"
ท่า 3 CapSense มาทีหลัง เพราะมันเพิ่มตัวแปรใหม่เข้ามาสองอย่าง (ชิป 4000T และบัส I2C) ถ้าพังตอนนี้ เรารู้ทันทีว่าพังที่ของใหม่ ไม่ใช่ที่ของเก่า
ท่า 4 ฟิลเตอร์กับบรรทัดคุณภาพของค่า ต้องมาหลังจากเห็นค่าดิบสั่นด้วยตาตัวเองแล้ว ไม่งั้นจะไม่มีวันรู้ว่ามันแก้ปัญหาอะไรให้ · สองอย่างนี้อยู่ท่าเดียวกันเพราะตอบคำถามเดียวกัน: "ตัวเลขที่เห็นเชื่อได้แค่ไหน"
ท่า 5 จังหวะลูป ปิดท้าย เพราะมันคือสิ่งที่ทำให้ทุกท่าข้างบนอยู่ร่วมกันได้อย่างเสถียรตลอดสิบนาที ไม่ใช่แค่สิบวินาทีแรก
คำถามคิดต่อ: ถ้าต้องส่งค่าลูกบิดขึ้นคลาวด์ทุก 5 วินาที จะส่งค่าดิบหรือค่าที่กรองแล้ว · การส่งค่าที่กรองแล้วทำให้เราสูญเสียข้อมูลอะไรไปบ้าง · ถ้าปลายทางเป็นระบบแจ้งเตือน ความหน่วงจากฟิลเตอร์มีราคาเท่าไร
ทั้งสี่มุมนี้ใช้ความรู้ชุดเดียวกับที่เพิ่งเขียนไป ต่างกันแค่ว่าค่าที่ผิดพลาดไปหนึ่งเปอร์เซ็นต์ ราคาของมันแพงแค่ไหน
ข้อ 1 · สนามทดลองค่า alpha — ทำสามบรรทัดเทียบกัน: RAW, EMA(0.05), EMA(0.5) บนจอเดียว หมุนเร็ว ๆ แล้วปล่อยนิ่ง จดว่าแต่ละเส้นตามทันต่างกันกี่วินาที
ข้อ 2 · Median ปะทะ spike — ใส่ dsp.Median(window=5) เพิ่มอีกบรรทัด แล้วจงใจสร้าง spike ด้วยการเคาะบอร์ด บันทึกว่า EMA กับ Median รับมือต่างกันอย่างไร
ข้อ 3 · ลูกบิดสั่งไฟจริง — ใช้ gpio.led() จากคาบ 3 คู่กับค่าเปอร์เซ็นต์: ยิ่งหมุนมากยิ่งติดหลายดวง — แบ่ง 0-100 % เป็นช่วงเท่ากันตาม gpio.num_leds() (Eva 3 ดวง = ดวงละ 33 % · Dev Kit 5 ดวง = ดวงละ 20 %) ไม่พิมพ์ 3 ตายตัว · กันกะพริบตรงรอยต่อด้วยช่วงเผื่อ ไม่ใช่จุดเดียว · ดวงที่ต้องเป็นสีให้หาตามชื่อใน gpio.board_info()["led_names"] แบบ led_named() ใน examples/s07/01
ข้อ 4 · ปุ่มสัมผัสสลับหน้าที่จอ — BTN0 สลับหน่วยของเกจ · BTN1 เปิด-ปิดฟิลเตอร์ · ตรวจขอบขาขึ้นแบบคาบ 3 กันแตะครั้งเดียวสลับหลายรอบ
เขียนคำตอบลง worksheet ข้อ 8 แล้วเอามาเล่าให้เพื่อนฟังต้นคาบหน้า
ui.Roller
examples/s05/10_roller_picks_the_filter.py↗ สร้าง สัญญาณบนกราฟไฟล์นั้นสร้างขึ้นเอง ไม่ได้มาจากลูกบิดหรือแผ่นสัมผัสคาบนี้มีตัวกรองหกตัวให้เลือก และวิธีเลือกก็เป็นบทเรียนของมันเอง — 08_six_filters_one_signal.py ใช้ปุ่ม "ตัวถัดไป" ซึ่งแปลว่าจะไปดู Kalman ต้องกดห้าครั้ง และตลอดเวลานั้นไม่มีใครเห็นว่ามีอะไรให้เลือกบ้าง
| ตัวเลือกยาว ๆ ทำด้วยอะไรได้บ้าง | ข้อเสียบนจอสัมผัส 4.3 นิ้ว |
|---|---|
| ปุ่ม "ตัวถัดไป" ที่กดวน | ซ่อนรายการทั้งชุด และไปตัวที่ห้าต้องกดห้าครั้ง |
ui.Dropdown |
ต้องแตะเปิดก่อนถึงจะเห็นตัวเลือก และรายการไปทับของอื่น |
ui.Roller |
กางตัวเลือกค้างไว้ เลื่อนด้วยนิ้วรวดเดียว ตัวที่เลือกอยู่ไฮไลต์ตลอดเวลา |
ui.Roller (ต่อ) — ใช้ที่ไหน และสองเรื่องที่วัดมาแล้วในงานจริงมันคือ widget ของ "ทางเลือกที่มีจำกัดและรู้ล่วงหน้า" — โหมดเครื่อง ช่วงพิสัยของหัววัด กะการทำงาน ปลายทางที่จะส่งข้อมูล ทั้งหมดนี้เป็นชุดที่ปิดแล้ว ไม่ใช่ของที่ผู้ใช้พิมพ์เอง
สองเรื่องที่วัดมาแล้วและต้องรู้ก่อนใช้
value= ตอนสร้างทำสองหน้าที่พร้อมกัน คือ บรรทัดที่เลือก และ ขนาดฟอนต์ไทย (ui_widget_mgr.c ส่ง cfg->init_val เข้า ui_apply_content_font() ตัวเดียวกัน) ตั้ง value=24 เพราะอยากได้ตัวหนังสือขนาด 24 จะได้บรรทัดที่ 24 แถมมาด้วย — ไฟล์ตัวอย่างจึงไม่ตั้ง value= เลย แล้วสั่งเลือกด้วย .value(n) ทีหลัง.prop(ui.PROP_VISIBLE_ROWS, n) เขียนทับความสูงที่ตั้งไว้ด้วย h= และคำนวณจากฟอนต์ธีม ไม่ใช่ฟอนต์ไทยที่วงล้อใช้จริง วัดบนตัวจำลอง: ขอ 5 บรรทัด ได้กล่องเตี้ยลงจนเห็นจริง 3 บรรทัด — เลือกอย่างใดอย่างหนึ่ง อย่าสั่งทั้งสองทางจุดที่ต้องมองตอนตรวจภาษาไทย — ดูที่แถบสีเน้น ไม่ใช่ที่บรรทัดอื่น เพราะ LVGL วาดบรรทัดที่เลือกจากส่วน
LV_PART_SELECTEDคนละส่วนกับบรรทัดที่เหลือ ตั้งฟอนต์ไทยที่MAINอย่างเดียวจะได้ตัวอักษรทุกบรรทัดยกเว้นบรรทัดที่เลือก



เอกสารและตำรา
ภาพ
ทุกตัวเลขบนสไลด์ชุดนี้สืบกลับไปที่เอกสารต้นทางหรือซอร์สโค้ดได้ ยกเว้นข้อที่เขียนกำกับไว้ว่ายังต้องวัด
ตัวอย่างโค้ด — examples/s05/ สิบไฟล์ (ใบล่าสุดคือ 10_roller_picks_the_filter.py) · 07_median_beats_mean.py ย้ายมาจาก examples/s06/ เมื่อ 14 ส.ค. เพราะเรื่องที่มันสอนคือการกรองสัญญาณ ซึ่งเป็นเนื้อหาของคาบนี้ · เพิ่มใหม่ 15 ส.ค. 08_six_filters_one_signal.py ครอบ SMA LPF HPF Kalman1D ที่เดิมไม่มีตัวอย่างเลยสักไฟล์ และ 09_sensors_api_tour.py เรียกเก้าฟังก์ชันของโมดูล sensors จริง ๆ เพื่อพิสูจน์ว่าใครตอบใครปฏิเสธ · ตัวอย่าง SMA ที่เคยอยู่ใน examples/dsp/ ถูกถอดออกจากคลังเมื่อ 14 ส.ค. โฟลเดอร์นั้นจึงว่างอยู่
ข้อที่ยังไม่ลงตัว — ต้องวัดก่อนสอน
แรงดันอ้างอิงของ pot: ผังวงจร Eva แสดง VDD_1V8 แต่ sensors.pot.voltage() ของเฟิร์มแวร์คูณด้วย 3.3 V ที่สมมติไว้ ทั้งสองบอร์ด — ห้ามใช้ตัวเลขใดตัวเลขหนึ่งเป็นข้อเท็จจริงจนกว่าจะวัดที่ขาลูกบิดของบอร์ดจริง (Eva: P15[1] · Dev Kit: VR1) · ทิศหมุนและ taper ของ VR1 บน Dev Kit ก็ยังไม่ได้วัด
เปลี่ยนจากเฟิร์มแวร์รุ่นก่อน — บน Eva Kit ถูกปฏิเสธด้วย OSError ทั้งหมด ห้าตัว ไม่ใช่สองตัว: init() scan() push() live_push() auto() · read_all() ไม่ถูกปฏิเสธ แต่บน Eva มันคืนผลของ snapshot() ตัวเดียวกัน · auto_rate() และ auto_status() เรียกได้ตามปกติ · sensors.pot.* / sensors.capsense.* / sensors.bmi270.* อ่านผ่าน snapshot ของคอร์จอ ส่วน sensors.bmm350.* อ่านตรงจากชิปได้ เพราะมันอยู่คนละบัส (I3C ขา P3[0]/P3[1]) ที่คอร์จอไม่ได้ถือไว้ · บน TESAIoT Dev Kit ห้าตัวนั้นทำงานจริง (live_push() วนจนกด Ctrl+C) · read_all() คืน pot เป็น float · snapshot() มีแล้วและคืน dict หน้าตาเดียวกับ Eva — IMU ในนั้นอ่านสดจาก CM33 ส่วนช่อง pot กับ capsense มาจากคอร์จอที่อ่านทุก 200 ms · sensors.pot.read() ต่างหากที่อ่าน SAR ตรง · มี dps368 sht40 radar เพิ่ม · ตรวจจากซอร์ส BENTO-TESAIoT-libraries/claw/common/mpy/modsensors.c และ moddsp*.c โดยตรง
ข้อที่เขียนกำกับว่ายังต้องวัด ให้วัดก่อนขึ้นสอน แล้วค่อยเขียนตัวเลขลงสไลด์
fit-css
VIDEO-SLOT: คลิป 45-60 วินาที ถ่ายจอบอร์ดจริง — Eva Kit ที่หน้า Controls (Dev Kit ไม่มีเมนูนี้ ให้รัน examples/s05/01_capsense_dimmer.py แทน) หมุนลูกบิดช้า ๆ ให้เห็นเกจกวาดตาม แล้วซูมให้เห็นตัวเลขเปอร์เซ็นต์กระพริบเปลี่ยนเลขหลักสุดท้ายทั้งที่มือหยุดหมุนแล้ว จากนั้นแตะปุ่มสัมผัสสองปุ่มให้ไฟบอกสถานะเปลี่ยน และลากนิ้วบนแถบเลื่อน
คาบ 4 คือ "จอสั่งฮาร์ดแวร์" คาบ 5 คือ "ฮาร์ดแวร์สั่งจอ" — ทิศทางข้อมูลกลับด้านกัน
โน้ตผู้สอน: การอ่านครั้งแรกหลังรีเซ็ตอาจรอคอร์จอนาน (Eva วัดได้ถึง 16 วินาที · Dev Kit ยังไม่ได้วัด) ไฟล์จึงห่ออุ่นเครื่องใน try/except — ยังไม่ตอบก็พิมพ์บอกแล้วไปลองใหม่ในลูป ไม่หยุดทั้งสคริปต์
VIDEO-SLOT: คลิป 25-40 วินาที ถ่ายจอตอนผ่าน MVP — หมุนลูกบิดช้า ๆ ให้เห็นแถบเดินไปตามไม้บรรทัด 0-100 แล้วหมุนข้ามเกณฑ์ให้ไฟสองดวงสลับกันติด จากนั้นหยุดมือค้างไว้ 10 วินาที ถ่ายให้เห็นชัดว่าบรรทัดค่าดิบยังกระพริบแต่บรรทัดค่ากรองนิ่ง แล้วปิดท้ายด้วยการกดปุ่มเพิ่ม/ลดเกณฑ์ให้เห็นไฟเปลี่ยนตาม — คลิปนี้คือ "หน้าตาของคำว่าผ่าน" ที่ทุกทีมใช้เทียบ
หลักเดิมจากคาบก่อน ๆ ยังใช้ได้: เพิ่มตัวแปรใหม่ทีละหนึ่ง แล้วทุกความผิดพลาดจะชี้กลับไปที่สิ่งที่เราเพิ่งเพิ่มเสมอ