คาบ 5 — อนาล็อกและสัมผัส

Potentiometer + CapSense + กรองสัญญาณให้อ่านรู้เรื่อง

คาถาประจำคาบ: โลกจริงไม่ได้มีแค่ 0 กับ 1 — งานของเราคือแปลงมันให้เป็นตัวเลขที่เชื่อถือได้

ดูของจริงก่อน — เกจวัดของลูกบิด

เกจของลูกบิด (Eva: หน้า Controls) 62.4 % ui.Arc ตามลูกบิด Raw: 40915 Percent: 62.4 หลักสุดท้ายสั่นเอง ลูกบิดจริงบนบอร์ด มือหมุน คำถามของวันนี้ หยุดมือแล้ว ทำไมตัวเลขยังขยับ ไม่ใช่บอร์ดเสีย

Eva Kit เปิดเมนู Controls บนบอร์ด แล้วหมุนลูกบิดช้า ๆ เข็มโค้งบนจอกวาดตามมือเราทันที · TESAIoT Dev Kit ไม่มีเมนู Controls (เฟิร์มแวร์ปิดหน้านี้ไว้) แต่หน้า GPIO & RGB Matrix บนบอร์ดมีแถบ VR1-4 ให้หมุน VR1 บนฐานดูได้ทันที — แถบทำหน้าที่เดียวกับเข็ม · ส่วนแถบสัมผัสให้รัน examples/s05/01_capsense_dimmer.py↗ (ไฟล์แรกของคาบนี้ตามโครงหลักสูตร) แล้วลากนิ้ว

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

นั่นไม่ใช่บอร์ดเสีย และไม่ใช่โปรแกรมผิด มันคือธรรมชาติของการอ่านค่าอนาล็อก คาบนี้เราจะทำสามอย่าง: อ่านค่าให้เป็น, เข้าใจว่าทำไมมันสั่น, แล้วทำให้มันนิ่งลงโดยไม่โกงค่า

เข็มที่กวาดตามมือคือของง่าย ตัวเลขที่สั่นตอนมือหยุดต่างหากคือบทเรียนจริงของวันนี้

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

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

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

  1. อธิบายได้ว่า ADC ทำอะไร และเลขที่ได้มาแปลว่าอะไรเมื่อเทียบกับแรงดันจริง
  2. อ่านลูกบิดด้วย sensors.pot.read() / .percent() / .voltage() แล้วแสดงสามแถวสามสีบนจอ
  3. อ่านปุ่มสัมผัสและแถบเลื่อนด้วย sensors.capsense และเข้าใจว่าแผ่นทองแดงรู้ได้อย่างไรว่านิ้วมาแตะ
  4. ใช้ dsp.EMA และ dsp.Median ลดการสั่น แล้ว เทียบค่าดิบกับค่ากรองแล้วบนจอพร้อมกัน

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

วัดกันที่ "อ่านค่าได้และอธิบายค่าที่ได้เป็น" ไม่ใช่แค่ทำให้เข็มขยับ

ปลายทางของคาบนี้ — จอที่ค่าจากลูกบิดวิ่งอยู่

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

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

  • บนซ้าย ค่าจากลูกบิดเป็น ui.Bar วางทับ ui.Scale ที่มีขีดและตัวเลข 0-100 — ตัวเลขเปอร์เซ็นต์จึงไม่ได้ลอยอยู่เฉย ๆ มันมีพิสัยของตัวเองอยู่ข้าง ๆ พร้อมค่าดิบกับโวลต์กำกับ
  • บนขวา เกณฑ์เตือนเป็น ui.Spinbox ที่ผู้ใช้ตั้งเองด้วยปุ่มเพิ่ม/ลด และ ui.Led สองดวงบอกว่าตอนนี้ต่ำกว่าเกณฑ์หรือเกิน
  • ล่างซ้าย แถบสัมผัสวางบนไม้บรรทัดชุดเดียวกัน กับไฟสองดวงแทนสถานะปุ่มทองแดง และบรรทัดคุณภาพของค่า
  • ล่างขวา ค่าดิบเทียบค่าที่กรองแล้ว (alpha 0.2, คาบ 200 ms) ซึ่งเป็นบทเรียนหลักของครึ่งหลังของคาบ

ค่าเดียวกันแสดงได้หลายแบบ — เลือกให้ตรงกับคำถามที่คนดูจออยากรู้ และค่าที่วัดได้ต้องมาพร้อมเกณฑ์ของมันเสมอ

ทบทวนคาบ 4 — ของที่ต้องหยิบมาใช้ต่อวันนี้

คาบ 4 · จอสั่งฮาร์ดแวร์ นิ้วบนกระจก โค้ดของเรา หลอด LED จริง คาบ 5 · ฮาร์ดแวร์สั่งจอ จอแสดงผล โค้ดของเรา ลูกบิด · นิ้วสัมผัส ทิศทางข้อมูล กลับด้านกัน

คาบที่แล้วเราสร้างแผงควบคุมของทีมเอง จากคาบนั้นมีสี่อย่างที่วันนี้ใช้ซ้ำทั้งหมด

ของจากคาบ 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↗

เข้าใจฮาร์ดแวร์ · ADC หั่นแรงดันต่อเนื่องเป็นขั้นบันได

ลูกบิดบนบอร์ดไม่ได้ส่งตัวเลขออกมา มันส่ง แรงดันไฟ ที่ไล่ต่อเนื่องจาก 0 V ขึ้นไปถึงแรงดันอ้างอิงของวงจร (ตัวเลขจริงของบอร์ดนี้ยังไม่ลงตัว — สองสไลด์ถัดไปว่าด้วยเรื่องนี้โดยเฉพาะ) วงจร SAR ADC ในชิปมีหน้าที่วัดแรงดันนั้นเป็นจังหวะ ๆ แล้วปัดลงขั้นบันไดที่ใกล้ที่สุด

V_ref 0 V เวลา — 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()   # ทศนิยม หน่วยโวลต์ (ดูสไลด์ถัดไปก่อนเชื่อตัวเลข)

ซ้าย: สัญลักษณ์ในผังวงจร ปลายสองข้างคือปลายแทร็กตัวต้านทาน ลูกศรตรงกลางคือ wiper ที่เลื่อนไปตามแทร็ก — ภาพ: “Potentiometer linear” · สาธารณสมบัติ · Wikimedia Commons  |  ขวา: ของจริงที่มือเราหมุน — ภาพ: “23mm knob with numbered dials with potentiometers 01(DXO)” โดย Retired electrician · CC0 1.0 · Wikimedia Commons

ทั้งสามค่ามาจากการวัดครั้งเดียวกัน ต่างกันแค่หน่วยที่เอามาเสิร์ฟ — 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

เลือกหน่วยตาม คนอ่าน ไม่ใช่ตามความสะดวกของโปรแกรม — เกจเป็นเปอร์เซ็นต์ รายงานวิศวกรรมเป็นโวลต์

เข้าใจฮาร์ดแวร์ · ลูกบิดของ Eva Kit ต่ออยู่กับอะไรจริง ๆ

ภาพ: KIT_PSE84_EVAL PSOC™ Edge E84 Evaluation Kit guide, Infineon 002-39007 Rev.*B, รูปที่ 77 (หน้า 88) — ใช้เพื่อการเรียนการสอน · ผังนี้เป็นของ Eva Kit เท่านั้น

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" (n=16n = 16) ไม่ได้แปลว่าฮาร์ดแวร์ละเอียด 65536 ขั้นจริง

p=raw2n−1×100%V=raw2n−1⋅Vrefp = \frac{\text{raw}}{2^{n}-1}\times 100\% \qquad V = \frac{\text{raw}}{2^{n}-1}\cdot V_{\text{ref}}

VLSB=VFS2nεq≤VLSB2V_{\text{LSB}} = \frac{V_{\text{FS}}}{2^{n}} \qquad \varepsilon_q \le \frac{V_{\text{LSB}}}{2}

ไทย: ค่าที่อ่านได้ไม่ต่อเนื่อง มันกระโดดทีละขั้น ขั้นหนึ่งใหญ่แค่ไหนขึ้นกับจำนวนบิต และความคลาดเคลื่อนจากการปัดไม่เกินครึ่งขั้น

คิดให้ดูจริง ๆ สมมติ read() คืน 40915

p=4091565535×100=62.43%p = \frac{40915}{65535}\times 100 = 62.43\%

ส่วน VV ตอบเป็นโวลต์ไม่ได้จนกว่าจะรู้ VrefV_{\text{ref}} ของบอร์ดจริง

ตัวส่วนมีสองแบบ อย่าสลับ: แปลงเป็นค่าอ่านใช้ 2n−12^{n}-1 (ratiometric) · ขนาดหนึ่งขั้นใช้ 2n2^{n}

ภาพ: “Quantization error” โดย Gregory Maxwell — CC BY 3.0 · Wikimedia Commons

สิ่งที่ต้องวัดเอง: เลื่อนลูกบิดช้า ๆ แล้วดูว่า read() เดินทีละเท่าไร ถ้ากระโดดเป็นก้อนละ kk เท่ากันทุกก้อน ความละเอียดจริงคือ 65536/k65536/k ขั้น — ก้อนละ 16 ก็คือ 4096 ขั้น จดลง worksheet อย่าเดาจากสเปกที่ยังไม่ได้อ่าน

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

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

เกร็ด: ข้างใน SAR ADC คือการ "ทายเลข" แบบไบนารีเสิร์ช

ภาพ: “ADC animation 20” โดย Russ Puskarcik — CC BY 3.0 · Wikimedia Commons

SAR ย่อมาจาก Successive Approximation Register — แปลตรงตัวคือ "ทะเบียนของการประมาณทีละขั้น"

วิธีทำงานเหมือนเกมทายเลข 1-100 ที่เราเล่นกันตอนเด็ก: เดาครึ่งทางก่อน ถ้าสูงไปก็ตัดครึ่งบนทิ้ง แล้วเดาครึ่งของที่เหลือต่อไป

  1. ตรึงค่าแรงดันไว้ก่อนด้วยวงจร sample-and-hold (ถ้าไม่ตรึง ค่าจะเปลี่ยนระหว่างที่กำลังทาย)
  2. ทายบิตบนสุด: แรงดันมากกว่าครึ่งของเต็มสเกลไหม → ได้ 1 บิต
  3. ทำซ้ำลงมาทีละบิตจนครบ n บิต

ทำไมต้องรู้: เพราะการแปลงหนึ่งครั้ง กินเวลา n รอบนาฬิกา ไม่ใช่ทันที นี่คือเหตุผลที่ ADC ทุกตัวมีเพดานอัตราการสุ่ม และเป็นที่มาของกฎ Nyquist ที่เราจะเจอเต็ม ๆ ในคาบ 7

DAC ภายในที่ไล่ทายทีละบิตจริง ๆ ขั้นบันไดที่ขยับทีละครั้งคือหัวใจของคำว่าไบนารีเสิร์ช ภาพนิ่งเห็นแค่ผลลัพธ์ — ภาพ: “4-bit Successive Approximation DAC” โดย Uwezi — CC BY-SA 4.0 — Wikimedia Commons

เชื่อมกับวันนี้: ตัวเลขที่ sensors.pot.read() คืนมา คือผลของการทายแบบนี้ที่จบไปแล้วเมื่อเสี้ยววินาทีก่อน ไม่ใช่แรงดัน ณ วินาทีที่เราอ่านตัวแปร

ดูเพิ่ม · ลูกบิดกับตัวแบ่งแรงดันทำงานอย่างไร

วงจรที่อยู่ข้างในลูกบิด — ภาพ: “Voltage divider” · สาธารณสมบัติ · Wikimedia Commons
How Potentiometers Work · CircuitBread
ทำไมต้องต่อครบสามขาถึงจะได้ "แรงดันที่ปรับได้" ต่อสองขาได้แค่ความต้านทานที่ปรับได้เฉย ๆ
Voltage divider · Khan Academy
อนุมานสูตร Vout = Vin × R2 / (R1 + R2) จากกฎของโอห์มทีละขั้น

สูตรที่ทั้งสองคลิปพาไปถึง Vout=Vin⋅R2R1+R2V_{out} = V_{in}\cdot\dfrac{R_2}{R_1+R_2} — ในลูกบิด R1R_1 กับ R2R_2 คือแทร็กสองท่อนที่ wiper แบ่งเอาไว้ หมุนไปครึ่งทางจึงได้แรงดันครึ่งหนึ่ง · ทั้งสองคลิปเป็นของนอกเวลา (ต้องมีอินเทอร์เน็ต) เนื้อหาในคาบเข้าใจได้ครบโดยไม่ต้องเปิดดู

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

CapSense — รู้ได้อย่างไรว่านิ้วมาแตะ ทั้งที่ไม่มีสวิตช์

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

ไม่มีนิ้ว แผ่นทองแดงใต้กระจก ความจุ = C₀ มีนิ้วเข้ามาใกล้ นิ้วดึงเส้นสนามส่วนหนึ่งไป ความจุ = C₀ + ΔC นิ้วคือตัวนำที่ต่อลงกราวด์ผ่านร่างกาย — มันเพิ่มความจุให้แผ่น

นิ้วคนเป็นตัวนำที่ต่อลงกราวด์ผ่านร่างกาย พอนิ้วเข้ามาใกล้ ค่า capacitance ของแผ่นนั้น เพิ่มขึ้น เวลาที่ใช้อัดประจุจึงนานขึ้นเล็กน้อย ชิปวัดความต่างนั้นได้ และสรุปว่า "มีนิ้ว"

แถบเลื่อนใช้หลักการเดียวกัน แต่วางแผ่นทองแดงเรียงกันหลายแผ่น แล้วดูว่านิ้วทำให้แผ่นไหนเปลี่ยนมากที่สุด เอามาถัวเฉลี่ยเป็นตำแหน่ง 0-100

CapSense (ต่อ) — ผิวสัมผัสของจริง ไม่มีสวิตช์ ไม่มีชิ้นส่วนขยับ

ภาพถ่ายผิวสัมผัสแบบ capacitive ของจริง ไม่มีสวิตช์ ไม่มีชิ้นส่วนขยับ มีแต่แผ่นตัวนำ นี่คือสิ่งที่นิ้วไปเปลี่ยนค่าของมัน — ภาพ: “Capacitive touchscreen” โดย Medvedev — CC BY 3.0 — Wikimedia Commons

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

อิเล็กโทรดจริงบน Eva Kit — และวิดีโอจากผู้ผลิตชิป

ภาพ: KIT_PSE84_EVAL PSOC™ Edge E84 Evaluation Kit guide, Infineon 002-39007 Rev.*B, รูปที่ 52 (หน้า 65) — ใช้เพื่อการเรียนการสอน
PSoC 101 Lesson 13: CapSense
วิดีโอสอนจากผู้ผลิตชิปเดิม (ของนอกเวลา ต้องมีเน็ต)

บน Eva Kit ปุ่มสองปุ่ม CSB1 / CSB2 เป็นแบบ CSX (mutual-capacitance) — สังเกตว่าแต่ละปุ่มมีขา Tx กับ Rx แยกกัน ไม่ใช่ขาเดียว ประจุถูกส่งจาก Tx ไป Rx และนิ้วเข้ามา "ขโมย" ส่วนหนึ่งไป · แถบเลื่อนคือ 5 อิเล็กโทรด (RX0-RX4) ที่ชิปถัวเฉลี่ยออกมาเป็นตำแหน่งเดียว 0-100 · ผังอิเล็กโทรดของ Dev Kit ยังไม่มีในเอกสารชุดนี้ — แต่ค่าที่โค้ดได้รับคือชุดเดียวกัน

ในโค้ดเราเห็นแค่ btn0 / btn1 / slider สามค่า เหมือนกันทั้งสองบอร์ด — ข้างใต้มันคืออิเล็กโทรดหลายแผ่นกับชิปอีกตัวหนึ่งที่ทำงานให้ตลอดเวลา

เข้าใจฮาร์ดแวร์ · ชิปคนละตัว คุยกันผ่าน I2C สามไบต์

งานสัมผัสไม่ได้ทำโดยชิปหลัก แต่ทำโดย PSoC 4000T อีกตัวหนึ่งบนบอร์ด ทำหน้าที่นี้อย่างเดียวตลอดเวลา — ทั้ง Eva Kit และ Dev Kit ใช้ทางเดียวกัน คือชิปตัวนี้ส่งค่าให้คอร์จอ (CM55) แล้ว Python ขอต่อจากคอร์จอ

แผ่นทองแดง BTN0 · BTN1 แถบเลื่อนหลายช่อง PSoC 4000T อัดประจุ จับเวลา เทียบกับ baseline สรุปเป็นแตะ/ไม่แตะ ต้องถูก flash เฟิร์มแวร์มาก่อน คอร์จอ CM55 → CM33 Python sensors.capsense.read() ขอ dict สามช่องต่อจากคอร์จอ I2C แอดเดรส 0x08 คอร์จออ่านรวด 3 ไบต์ ไบต์ 0 = ปุ่ม 0 · ไบต์ 1 = ปุ่ม 1 · ไบต์ 2 = ตำแหน่งแถบเลื่อน 0-100

หมายเหตุผู้สอน: ถ้าชิป 4000T ยังไม่ถูก flash ทุกค่าที่อ่านได้จะเป็น 255 หรือโยน OSError — ต้องตรวจให้ครบทุกบอร์ดก่อนเข้าคาบนี้

baseline — เหตุผลที่ต้อง "ปล่อยมือ" ตอนโปรแกรมเริ่ม

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

raw count difference count = ระยะนี้ finger threshold baseline ไล่ตามช้า ๆ (อุณหภูมิ/ความชื้น) ค่าดิบจริง 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 · วงจรข้างเคียง ชั้นตัวอุปกรณ์ หน้าสัมผัสของ wiper ถูตัวเอง ชั้นการปัดขั้น ค่าจริงอยู่กึ่งกลางสองขั้นพอดี สามชั้นนี้บวกกันแล้วโผล่ออกมาเป็นหลักสุดท้ายที่กระพริบบนจอ

สาเหตุมีจริงและซ้อนกันหลายชั้น ไม่ใช่เรื่องลึกลับ

ชั้นไฟฟ้า — สายทุกเส้นรับสัญญาณรบกวนจากวงจรข้างเคียง จอที่กำลังรีเฟรช และ WiFi ที่กำลังส่ง ล้วนเหวี่ยงแรงดันระดับมิลลิโวลต์เข้ามาได้

ชั้นตัวอุปกรณ์เอง — ลูกบิดคือแผ่นตัวต้านทานที่มีหน้าสัมผัสถูตัวมันเอง ความต้านทานตรงจุดสัมผัสไม่ได้นิ่ง 100%

ชั้นการปัดขั้น — ถ้าแรงดันจริงตกอยู่ กึ่งกลาง ระหว่างสองขั้นบันได ADC จะปัดขึ้นบ้างลงบ้างสลับกันไป บิตล่างสุดจึงกระพริบตลอดเวลา

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

dsp.EMA — ค่าเฉลี่ยที่จำอดีตแบบจาง ๆ

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

y[n]=α⋅x[n]+(1−α)⋅y[n−1]y[n] = \alpha \cdot x[n] + (1-\alpha)\cdot y[n-1]

ค่าใหม่มีน้ำหนัก α\alpha ที่เหลือคือค่าเดิมทั้งก้อน อดีตไม่ได้หายไปไหน แต่ จางลงเรื่อย ๆ ทุกรอบ

คิดให้ดูจริง ๆ ค่าเดิม y=62.00y=62.00 ค่าใหม่เข้ามา x=63.00x=63.00 ที่ α=0.2\alpha=0.2 → y=0.2(63.00)+0.8(62.00)=62.20y = 0.2(63.00)+0.8(62.00) = 62.20 ขยับแค่ 0.20 ทั้งที่อินพุตกระโดดไป 1.00 นั่นคือความนิ่งที่เราซื้อมาด้วยความหน่วง

ตัวอย่างแรกไม่ได้ถูกคูณ α\alpha ค่าที่ป้อนเข้าไปครั้งแรกถูกใช้เป็นค่าตั้งต้นตรง ๆ ไม่งั้นทุกฟิลเตอร์จะเริ่มจากศูนย์แล้วต้องไต่ขึ้นมาก่อนหลายวินาที ทั้งที่ค่าจริงอยู่ตรงนั้นตั้งแต่แรก · .value() กับ .reset() มีเหมือนกันทุกตัวในตระกูลนี้

น้ำหนักของค่าย้อนหลังแต่ละตัวใน EMA — ตัวล่าสุดหนักสุด แล้วลดแบบเอกซ์โพเนนเชียล ไม่มีตัวไหนถูกตัดทิ้งสนิท · ภาพ: “Exponential moving average weights N=15” โดย Д.Ильин — CC0 1.0 · Wikimedia Commons

dsp.EMA — alpha คือ "ความรู้สึก" และไฟล์ที่แปลงมันเป็นวินาที

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 ใช้หน่วยความจำแค่ตัวเลขเดียว จึงเป็นฟิลเตอร์ที่ระบบฝังตัวเล็ก ๆ เลือกใช้บ่อยที่สุด

dsp.Median — คนละหน้าที่กับ 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) แล้วมันบอกค่าที่ได้จริงมาให้ ไม่ต้องเดา

ค่าที่ไหลเข้ามาตามเวลา — หน้าต่างกว้าง 5 เลื่อนตามไปเรื่อย ๆ 41 42 95 43 42 44 43 หน้าต่างปัจจุบัน 5 ค่า เรียง: 41 42 42 43 95 ตัวกลาง = 42 spike 95 ถูกโยนทิ้งทั้งก้อน ถ้าใช้ค่าเฉลี่ยธรรมดา 95 จะถูกหารเฉลี่ยเข้าไปในคำตอบเสมอ

ภาพ: MothNik, Wikimedia Commons, CC0 1.0 — หน้าต่างในภาพบนวาดไว้นิ่ง ๆ ได้ตำแหน่งเดียว แต่ของจริงมันเลื่อนไปทีละค่าตามข้อมูลที่ไหลเข้ามา · คำว่า window=5 ในบรรทัดโค้ดบนสุดของสไลด์นี้คือความกว้างของกรอบที่กำลังเลื่อนอยู่นี้ และเหตุผลที่มันหน่วง ก็เพราะคำตอบของแต่ละรอบต้องรอให้กรอบเต็มก่อน

dsp.Median — คิดให้ดูจริง ๆ แล้วเทียบกับ EMA

yt=median⁡(xt−N+1, …, xt)y_t = \operatorname{median}\bigl(x_{t-N+1},\,\dots,\,x_{t}\bigr)

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

คิดให้ดูจริง ๆ จากชุดตัวเลขในสไลด์ก่อน Median คืน 42 เหมือน spike ไม่เคยเกิด · ส่วน EMA(α=0.2\alpha=0.2) ที่ค่าเดิม 42 เจอ 95 ได้ 0.2(95)+0.8(42)=52.60.2(95)+0.8(42) = 52.6 คือกระเด็นตาม · ที่ลูป 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 คำนวณ α=ΔtRC+Δt\alpha = \dfrac{\Delta t}{RC+\Delta t} ให้จากค่า cutoff และ fs ที่เราบอก ส่วน EMA ให้เราใส่ α\alpha เอง เลือกทางไหนก็ได้ แต่ ถ้าบอก 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 ทุกประการ โดยที่

α=ΔtRC+Δt\alpha = \frac{\Delta t}{RC + \Delta t}

แรงดันคร่อมตัวเก็บประจุเมื่อป้อนสัญญาณขั้นบันได — ไต่ถึงราว 63% ที่เวลา τ = RC เหมือนกับที่ EMA ไต่ตามค่าใหม่ · ภาพ: “Series RC capacitor voltage” — สาธารณสมบัติ · Wikimedia Commons

โมดูล dsp บนบอร์ดนี้มี dsp.LPF(cutoff=, fs=) ที่คำนวณ α\alpha จากสูตรนี้ให้เลย — พูดง่าย ๆ คือ LPF กับ EMA เป็นตัวเดียวกัน ต่างกันแค่เราตั้งค่าด้วยความถี่ตัดหรือด้วยน้ำหนักโดยตรง

เชื่อมกับวันนี้: ตอนที่น้อง ๆ ปรับ alpha แล้วรู้สึกว่า "หน่วงขึ้น" นั่นคือการหมุนลูกบิดเสมือนของตัวเก็บประจุที่ไม่มีอยู่จริง วิศวกรรุ่นก่อนต้องบัดกรีเปลี่ยนตัว C เพื่อทำสิ่งเดียวกับที่เราแก้ตัวเลขบรรทัดเดียว

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

เฟิร์มแวร์ทำให้แล้ว 70% งานของเรา 30% ตั้งค่า ADC · แปลงหน่วย · คุย I2C · หัก baseline · ฟิลเตอร์เขียนด้วย C ตัดสินใจว่าจะเชื่อค่าไหน คณิตศาสตร์ทั้งหมดถูกทำไว้ให้แล้ว เลือก widget · alpha · จังหวะลูป

สิ่งที่เฟิร์มแวร์ทำให้แล้ว (70%)
ตั้งค่า SAR ADC และอ่านค่าจากขาลูกบิด (Eva: P15[1] · Dev Kit: VR1 บนฐาน) · สเกลค่า 12 บิตที่วัดได้ให้เป็นช่วง 0-65535 และคิดเปอร์เซ็นต์/โวลต์ให้ · คุย I2C กับชิป 4000T และหักลบ baseline ให้เรียบร้อย · ฟิลเตอร์ทั้งชุดใน dsp เขียนเป็นภาษา C มาแล้ว · การวาดทุกอย่างบนจอ

สิ่งที่เป็นงานของเรา (30%)
เลือกว่าจะแสดงค่าไหนด้วย widget อะไร · ตั้งค่า alpha ให้เหมาะกับงาน · จัดจังหวะลูปให้จอตามทันและเซนเซอร์ไม่ถูกอ่านถี่เกินจำเป็น · ตัดสินใจว่าค่าที่เห็นเชื่อถือได้หรือยัง

โจทย์ของวิศวกรวันนี้ไม่ใช่ "เขียนฟิลเตอร์" แต่คือ เลือกฟิลเตอร์และปกป้องตัวเลือกนั้นได้

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

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() 200 ms — ให้ CM55 ล้างจบ sensors.pot.read() ใน try ครั้งแรกหลังรีเซ็ตอาจต้องรอ (Eva วัดได้ถึง ~16 s) อ่านค่าแรกได้ Eva: init() = OSError · Dev Kit: ผ่าน ไม่ต้องเรียก

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

แกะโค้ดจริง — ท่าที่ 2 ค่าที่วัดได้ ต้องมาพร้อมพิสัยและเกณฑ์

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)
0 40 80 55.4 % ตัวเลขเดียวกัน แต่ตอบได้แล้วว่าสูงไหม 70 เกณฑ์ที่ผู้ใช้ตั้งเอง ต่ำกว่า เกิน ค่า · พิสัย · เกณฑ์ อยู่ในสายตาเดียว

ตัวเลข 55.4 % ลอย ๆ ตอบไม่ได้ว่าสูงหรือต่ำ ui.Scale คือไม้บรรทัดที่พาพิสัยมาอยู่บนจอด้วยกัน และเพราะเป็นไม้บรรทัด มันจึง ไม่รับ .value() ตัวที่ขยับคือ ui.Bar ที่วางทับ (หน้าปัดวงกลมมีเข็มจริง — คาบ 6) · .ticks(ทั้งหมด, ใส่เลขทุกกี่ขีด) คุมความหนาแน่น ที่นี่ 11 ขีดใส่เลขทุกขีดที่สอง ได้ 0 20 40 60 80 100 พอดี

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

เกณฑ์เตือนไม่ควรเป็นค่าคงที่ในโค้ด คนหน้างานคือคนรู้ว่างานนี้ยอมได้แค่ไหน ui.Spinbox หนีบค่าในพิสัยให้เอง แต่บนจอสัมผัส spinbox เปล่า ๆ นิ้วเปลี่ยนค่าไม่ได้ ตัวที่เพิ่มลดค่าจริงคือ ui.Button สองปุ่มข้าง ๆ (สูง 88 px ตามขนาดเป้าสัมผัสของหลักสูตร)

ไฟสองดวงแทนสถานะด้วยสีตัวอักษร เพราะไฟมี รูปทรงและความสว่าง ถ่ายจอขาวดำแล้วยังแยกออก และ .value(0) คือ หรี่ ไม่ใช่หาย โดยตั้งใจ · จำกฎคาบ 4: value= ของ ui.Label คือ ขนาดฟอนต์ ส่วน Bar/Scale ใช้ min/max เป็นพิสัยจริง — ตัวอย่างประกอบที่ examples/s04/09_scale_led_spinbox.py↗

ค่าดิบกับโวลต์ยังอยู่ครบ แต่ลดเป็นบรรทัดกำกับ เพราะคนเดินมาดูจอถามคำถามเดียวก่อน: ตอนนี้สูงเกินหรือยัง

แกะโค้ดจริง — ท่าที่ 3 แถบเลื่อนสัมผัสและไฟสองดวง

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)
นิ้วอยู่ตรงไหน แถบไปตรงนั้น แถบทองแดง 5 ช่องบนบอร์ด 0 50 100 ไฟแทนคำว่า ON กับขีด ครอบ max(0, min(100, ...)) เสมอ

ui.Bar ไม่รับการแตะและไม่ส่ง event มันเป็น จอแสดงผลอย่างเดียว ซึ่งตรงกับที่ต้องการพอดี เพราะตัวสั่งงานคือนิ้วบนแถบทองแดง ไม่ใช่นิ้วบนกระจก และไม้บรรทัดชุดเดิมถูกใช้ซ้ำด้วยเหตุผลเดียวกับการ์ดบน: 62 ไม่มีความหมายจนกว่าจะรู้ว่าเต็มสเกลคือเท่าไร

สถานะปุ่มทองแดงเคยเขียนเป็นข้อความ BTN0 ON BTN1 - ซึ่ง ไม่ผ่านการทดสอบขาวดำ — เป็นเกรย์สเกลแล้ว ON สีม่วงกับขีดสีเทาแยกกันไม่ออก ไฟสองดวงแยกออกทันทีเพราะดวงที่ติดสว่างกว่าดวงที่หรี่

ครอบด้วย max(0, min(100, ...)) ไม่ใช่เพราะไม่เชื่อไดรเวอร์ แต่ถ้าชิป 4000T ไม่พร้อม ไบต์ที่ได้อาจเป็น 255 การครอบทำให้จอยังแสดงผลได้แทนที่จะพังทั้งหน้า

ค่าที่มาจากภายนอกโปรแกรมเราเสมอ ๆ ควรถูก ตรวจขอบเขตก่อนใช้ นี่คือนิสัยที่ติดตัวไปทุกภาษา

แกะโค้ดจริง — ท่าที่ 4 เทียบค่าดิบกับค่ากรองแล้ว และบอกคุณภาพของค่า

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)           # นอกประตูหนึ่งวินาที - ส่งซ้ำทุกรอบโดยตั้งใจ
สองเส้นบนจอเดียว RAW — สั่นทุกรอบ EMA — นิ่งกว่า แต่ตามช้ากว่า spike เดี่ยว ๆ ตรงนี้ ไม่มีของฟรี — นิ่งขึ้นแลกกับหน่วงขึ้น

จุดที่พลาดบ่อยที่สุดคือ เผลอสร้างฟิลเตอร์ไว้ในลูป — ทุกรอบได้ฟิลเตอร์ใหม่ที่ความจำว่าง ค่าที่ออกมาเท่าค่าดิบเป๊ะ แล้วสรุปผิดว่า "ฟิลเตอร์ไม่ทำงาน" · ทศนิยมสองตำแหน่งทั้งสองบรรทัดเป็นเรื่องจงใจ ปัดเหลือจำนวนเต็มเมื่อไร ความสั่นถูกซ่อน บทเรียนทั้งคาบหายไปด้วย

แกะโค้ดจริง — ท่าที่ 4 (ต่อ) บรรทัดคุณภาพของค่า และประตูหนึ่งวินาที

บรรทัด คุณภาพของค่า แก้ปัญหาที่ไม่มีใครเห็นจนกว่าจะเกิด: อ่านรอบนี้ขึ้น OSError โปรแกรมใช้ค่าเดิมต่อ จอจึงยังโชว์ตัวเลขสวยงามทั้งที่ไม่มีค่าใหม่แล้ว ค่าที่ค้างจึงต้องเขียนกำกับว่าไม่ใช่ค่าปัจจุบัน · สังเกตลำดับ ตั้งสีก่อน แล้วค่อยเขียนข้อความ — สองคำสั่งข้ามคอร์คนละครั้ง สลับกันจะมีเสี้ยววินาทีที่ข้อความใหม่ใส่สีของสถานะเก่า

lbl_health.text(health) อยู่ นอก ประตูหนึ่งวินาทีโดยตั้งใจ — ฝั่งจออยู่ โหมดเร็ว ต่อไปอีก 500 ms ทุกครั้งที่ได้คำสั่งเขียนข้อความ แต่ .value() ของแถบกับไฟ ไม่ปลุก โหมดนั้น ลูปที่มีแต่แถบกับไฟจึงถอยไปโหมดช้า ภาพกระตุก · ข้อความเท่าเดิมเฟิร์มแวร์ไม่วาดซ้ำ เราจ่ายแค่ค่าส่ง

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

แกะโค้ดจริง — ท่าที่ 5 ลูปหลักและจังหวะที่ปลอดภัย

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)   # จังหวะที่ผ่านการทดสอบ
หนึ่งรอบลูป = 200 ms อ่านเซนเซอร์ → อัปเดต widget → poll sleep 200 ms — คืนเวลาให้ CM55 วาด 5 ครั้งต่อวินาที — เร็วกว่าตาคนแยกออก ถ้าลูปเร็วเกิน เช่น 20 ms เฟรมส่วนเกินถูกทิ้งเงียบ ๆ ไม่มี error จอกระตุกทั้งที่โค้ดดูถูกทุกบรรทัด

แกะโค้ดจริง — ท่าที่ 5 (ต่อ) ทำไม 200 ms และทำไมตัวเลขเดินช้ากว่าแถบ

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 ของปุ่มเพิ่ม/ลดเกณฑ์ ถ้าลืมบรรทัดนี้ อาการที่เห็นจะเป็น "จอกระตุกและปุ่มกดไม่ติด" ซึ่งดูเหมือนของสองเรื่องแต่มีสาเหตุเดียว

เร็วกว่าที่จำเป็นไม่ได้แปลว่าดีกว่า ในระบบฝังตัวมันมักแปลว่า เปลืองพลังงานและได้ภาพที่แย่ลง

ข้อมูลไหลไปทางไหน — จากมือถึงจอ

ลูกบิด แรงดัน 0 ถึง Vref นิ้วบนแผ่นทองแดง ความจุเปลี่ยน SAR ADC 12 บิต → 0-65535 PSoC 4000T I2C → คอร์จอ CM55 CM33 · Python ขอค่าจากคอร์จอ dsp.EMA กรอง IPC คำสั่งวาด CM55 · LVGL Bar · Scale · Led ค่าดิบสั่นตั้งแต่ต้นทาง — เราจึงเลือกกรองที่ CM33 ก่อนส่งภาพข้ามไปให้ CM55 วาด

สังเกตว่าสองเส้นทางต้นทางต่างกันคนละแบบ (ADC กับ I2C) แต่มาบรรจบเป็นโค้ด Python บรรทัดเดียวกันที่ CM33

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

จอบอร์ด เปิด Playground ค้างไว้ ยกนิ้วออกให้หมด ตอนบอร์ดบูต — baseline เติม pass ทีละจุด แล้ว Program to Device หมุน · ลาก · แตะ ครบทั้งสามอย่าง ลำดับการรัน — ขั้นที่สองคือขั้นที่ทีมส่วนใหญ่ลืม วางนิ้วค้างตอนบอร์ดบูต = ปุ่มรายงานกลับด้านไปทั้งคาบ
  1. บนจอบอร์ด แตะการ์ด BENTO Playground บนหน้า Home ค้างหน้านี้ไว้
  2. ตอนเสียบสาย USB (หรือถอดแล้วเสียบกลับ) ยกนิ้วออกจากปุ่มสัมผัสให้หมด เพราะ baseline ถูกเก็บตอนคอร์จอเริ่มอ่านชิป ไม่ใช่ตอนสคริปต์เริ่ม · "รีเซ็ต" ในคอร์สนี้แปลว่าถอด USB แล้วเสียบกลับเท่านั้น — บน Dev Kit ห้ามโยกสวิตช์บนฐาน นั่นคือสวิตช์ไฟ
  3. เปิด practise_codes/s05_pot_capsense.py↗ ใน BENTO IDE
  4. เติมช่องว่าง pass ให้ครบทั้ง 6 จุดตามคำใบ้ # เติม: — เติมทีละจุด รันดูทีละครั้ง
  5. กด Program to Device แล้วหมุนลูกบิด ลากนิ้วบนแถบเลื่อน และแตะปุ่มสัมผัสทั้งสองปุ่ม

หมายเหตุผู้สอน: ขึ้น 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 ให้เรียกผู้สอนก่อน อย่าเสียเวลาทั้งคาบแก้โค้ดที่ไม่ได้ผิด

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

หมุน pot คุม ui.Bar บน ui.Scale + ตั้งเกณฑ์ด้วย ui.Spinbox + ui.Led บอกสถานะ + นิ้วเลื่อน CapSense คุม ui.Bar + แสดง raw/filtered สองเส้นเทียบกัน

หน้าตาของคำว่า "ผ่าน" 0 50 100 62.4 % ค่าดิบ 40915 โวลต์ ?.??? V 70 เกณฑ์ แถบสัมผัส ดิบ 62.43 % กรอง 62.21 % คุณภาพของค่า อ่านค่าปกติ ทั้ง 33 ชิ้นทำงานพร้อมกัน

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

  • [ ] หมุนลูกบิดแล้วแถบเดินตามได้ตลอดช่วง 0 ถึง 100 และตัวเลขบนไม้บรรทัดใต้แถบอ่านออกทุกขีด
  • [ ] ค่าดิบ / เปอร์เซ็นต์ / โวลต์ ขึ้นครบและตรงกันเชิงตรรกะ (สุดขวา ≈ 65535 และ 100% ส่วนโวลต์ให้จดค่าที่อ่านได้จริงไว้เทียบกับมัลติมิเตอร์)
  • [ ] กดปุ่มเพิ่ม/ลด แล้วเลขในช่องเกณฑ์เปลี่ยนตาม และหยุดที่ขอบพิสัย 10 กับ 95 เอง
  • [ ] หมุนลูกบิดข้ามเกณฑ์แล้วไฟสลับกันติด ทีละดวงเท่านั้น ไม่ติดพร้อมกันสองดวง
  • [ ] ลากนิ้วบนแถบสัมผัสแล้วแถบล่างเดินตามตำแหน่งนิ้ว และแตะปุ่มทองแดงแล้วไฟสองดวงติดถูกดวง (ไม่กลับด้าน)
  • [ ] สองบรรทัดขวาล่างแสดงค่าดิบกับค่ากรองพร้อมกัน และเห็นชัดว่าบรรทัดค่ากรองนิ่งกว่า
  • [ ] ตัวเลขบนจอเปลี่ยนวินาทีละครั้ง ไม่ใช่ห้าครั้งต่อวินาที (จ้องดูสิบวินาทีแล้วนับ)
  • [ ] ทีมตอบได้ว่า ถ้าเปลี่ยน alpha เป็น 0.05 กับ 0.8 ผลต่างกันอย่างไร
  • [ ] ถ่ายรูปหน้าจอตอนหมุนลูกบิดค้างไว้ แนบใน worksheet

ข้อที่ยากที่สุดไม่ใช่ข้อที่โค้ดยาวที่สุด แต่คือข้อสุดท้ายที่ต้อง อธิบายพฤติกรรมได้

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

อาการ สาเหตุที่แท้จริง วิธีแก้
จอขึ้นครบแต่ทุกค่าค้างที่ 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

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

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

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

อาการ สาเหตุที่แท้จริง วิธีแก้
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
เติมแล้วจอ "ตื่น" ทีละส่วน จุดที่ 2 เสร็จ ตัวเลขเปอร์เซ็นต์เริ่มขยับ จุดที่ 3 เสร็จ แถบเดินไปตามไม้บรรทัด จุดที่ 4 เสร็จ ไฟสองดวงเริ่มสลับกันติด จุดที่ 6 เสร็จ ปุ่มเกณฑ์กดติด จอไม่กระตุก

ลงมือทำ (ต่อ) — เติมทีละจุด รันทีละครั้ง แล้วดูจอ "ตื่น" ทีละส่วน

ไฟล์ฝึกตั้งค่าเริ่มต้นไว้ให้แล้ว (pct = 0.0, slider = 0, events = []) โปรแกรมจึง รันได้ตั้งแต่ยังไม่เติมอะไรเลย แต่ตัวเลขจะนิ่งสนิท ใช้ตรงนี้เป็นเครื่องมือ: เติมทีละจุด รันทีละครั้ง แล้วดูว่าจอ "ตื่น" ขึ้นทีละส่วน

หน้าจอทั้ง 33 ชิ้นเขียนไว้ให้ครบแล้ว ไม่ต้องแตะ งานของเราคือทำให้ค่าไหลเข้าไปในนั้น จุดที่ 6 คือจุดที่คนข้ามบ่อยที่สุด เพราะไฟล์รันได้อยู่แล้วโดยไม่มีมัน อาการที่ตามมาคือปุ่มเพิ่ม/ลดเกณฑ์กดไม่ติด ซึ่งไม่มี error ให้เห็นเลยสักบรรทัด

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

ต้องทำในคาบ · เปิดตามลำดับนี้ ทั้งชุดราว 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 จากตัวเลข ไม่ใช่จากการเดา

ตัวอย่างชุดคาบ 5 (ต่อ) — เปิดตามอาการ และอ่านเสริมนอกเวลา

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

อาการที่เจอ ไฟล์ที่ตอบอาการนั้น
แตะแถบสัมผัสแล้วไม่แน่ใจว่าบอร์ดรับไปหรือยัง โดยเฉพาะตอนมือเปียก 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↗ ครึ่งทางของลูกบิดไม่ใช่ครึ่งหนึ่งของความดังที่หูได้ยิน หูตอบสนองเป็นลอการิทึม การแมปตรง ๆ จึงรู้สึกว่าดังพรวดตั้งแต่ยังหมุนไม่ถึงไหน

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

เชื่อมโยงรากฐาน — วันนี้เราแตะอะไรไปบ้าง

อิเล็กทรอนิกส์และการวัด อนาล็อก → ดิจิทัล · ความละเอียด แรงดันอ้างอิง · baseline ประมวลผลสัญญาณ noise มาจากไหน · EMA · Median แลกความนิ่งกับความหน่วง Python และการออกแบบ ออบเจกต์ที่จำสถานะข้ามรอบ ตรวจขอบเขตค่าจากภายนอก สามเสาที่วันนี้แตะพร้อมกัน เสาแรกคือเสาที่ทำให้เราตอบได้ว่า "ตัวเลขนี้เชื่อได้แค่ไหน" ไม่ใช่แค่ "ตัวเลขนี้คืออะไร"

ฝั่งอิเล็กทรอนิกส์และการวัด
การแปลงอนาล็อกเป็นดิจิทัล · ความละเอียด (บิต) กับช่วงการวัด · แรงดันอ้างอิงและผลของมันต่อค่าที่อ่านได้ · การตรวจจับแบบ capacitive · แนวคิด baseline และการวัดแบบส่วนต่าง

ฝั่งการประมวลผลสัญญาณ
noise มาจากไหนและมีกี่ชั้น · ค่าเฉลี่ยถ่วงน้ำหนักแบบมีความจำ (EMA) · ตัวกลางแบบหน้าต่างเลื่อน (Median) · การแลกกันระหว่างความนิ่งกับความหน่วง

ฝั่ง Python และการออกแบบระบบ
ออบเจกต์ที่เก็บสถานะข้ามรอบลูป · keyword-only arguments · การตรวจขอบเขตค่าจากภายนอก · การออกแบบหน้าจอให้หนึ่งค่าเล่าได้หลายหน่วย

เรื่อง trade-off ระหว่าง "นิ่ง" กับ "ไว" จะกลับมาหาน้อง ๆ อีกในคาบ 6 และ 7 แต่คราวนั้นข้อมูลจะมาจาก IMU

งานทำเอง 30% + สรุปคาบ

อ่านลูกบิด raw · % · โวลต์ อ่านสัมผัส ปุ่ม + แถบเลื่อน รู้ที่มาของ noise สามชั้นซ้อนกัน กรองแล้วอธิบายได้ EMA เทียบ RAW คาบหน้า IMU + dsp.tilt สี่อย่างที่ติดมือไปคาบหน้า ฟิลเตอร์ตัวเดิมจะได้ใช้ทันทีกับข้อมูลจาก 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 รอบแรกหลังรีเซ็ตเป็นเรื่องปกติทั้งสองบอร์ด ไม่ใช่ความผิดพลาด

เลขฐานสิบหกที่โผล่กลางโค้ดโดยไม่มีชื่อ คือหนี้ที่คนอ่านคนถัดไปต้องมาจ่าย

เฉลย — ส่วนที่สอง: วางหน้าจอสี่การ์ด รวม 33 ชิ้น

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)
ผังหน้าจอ สี่การ์ด แผงคุมลูกบิดกับแถบสัมผัส ค่า + พิสัย 55.4 % เกณฑ์ + ไฟ 70 แถบสัมผัส + ไฟปุ่ม คุณภาพของค่า ดิบ เทียบ กรอง ดิบ 55.43 กรอง 54.99 33 จาก 64 - เหลือที่ต่อยอด 31

เฉลย — ส่วนที่สอง (ต่อ): นับ widget และจองความกว้างไว้ตั้งแต่แรก

นับ 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)
สองจังหวะในลูปเดียว 1 - อ่านทั้งชุดใน try เดียว 2 - แถบ + ไฟ ทุกรอบ 5 ครั้งต่อวินาที 3 - poll แล้วรับปุ่มเกณฑ์ 4 - ตัวเลข วินาทีละครั้ง if sec != last_sec ของที่ตาอ่านเป็นตำแหน่ง เดินเร็วได้

เฉลย — ส่วนที่สาม (ต่อ): try เดียว และเกณฑ์จากค่าที่กรองแล้ว

การอ่านทั้งห้าบรรทัดอยู่ใน try เดียวกันโดยตั้งใจ: ถ้ารอบนี้คอร์จอไม่ตอบ เราไม่อยากได้ครึ่งชุดเก่าครึ่งชุดใหม่ปนกันบนจอ ค่าทั้งหน้าจะเป็นชุดเดียวกันเสมอ และธง fresh จะไปบอกบรรทัดคุณภาพของค่าว่าตัวเลขที่เห็นตอนนี้เป็นของรอบก่อน

over = ema_pct >= th ใช้ค่าที่ กรองแล้ว ไม่ใช่ค่าดิบ ถ้าใช้ค่าดิบ ไฟจะกระพริบสลับดวงตอนค่าอยู่คาบเกี่ยวกับเกณฑ์พอดี ซึ่งเป็นอาการที่แผงควบคุมจริงเรียกว่า alarm chattering และเป็นเหตุผลหนึ่งที่คนหน้างานปิดเสียงเตือนทิ้ง

ลองแก้ alpha เป็น 0.05 แล้ว 0.8 ดู แล้วจ้องสองบรรทัดขวาล่างสิบวินาที — บทเรียนทั้งคาบอยู่ในความต่างนั้น

เฉลย · ทำไมเรียงห้าท่าแบบนี้ ไม่ใช่สุ่มเรียง

1 ตั้งจอ + อุ่นเครื่อง 2 ลูกบิด + เกณฑ์ 3 CapSense 4 ฟิลเตอร์ 5 จังหวะลูป ของที่ไม่ต้องพึ่งใคร เพิ่มตัวแปรใหม่ทีละหนึ่ง ทำให้อยู่ได้นาน พังตอนไหนก็รู้ทันทีว่าพังที่ของที่เพิ่งเพิ่ม ไม่ใช่ที่ของเดิม

ท่า 1 ตั้งจอ แล้วรอให้คอร์จอตอบ มาก่อน เพราะถ้าจอไม่ขึ้นหรือ sensors.pot.read() รอบอุ่นเครื่องยังขึ้น OSError ท่าที่เหลือไม่มีทางถูกต้องได้เลย

ท่า 2 ลูกบิดกับเกณฑ์ มาก่อน CapSense เพราะลูกบิดต่ออยู่กับ ADC ในชิปหลักโดยตรง ไม่ต้องพึ่งชิปตัวที่สองและไม่ต้องพึ่งบัส I2C — ถ้าท่านี้ผ่าน แปลว่าเส้นทาง sensor → Python → จอ ใช้ได้แล้วทั้งเส้น

ท่านี้พ่วง พิสัย (ui.Scale ใต้แถบ) กับ เกณฑ์ (ui.Spinbox และไฟสองดวง) ไว้ด้วยกัน เพราะมันคือคำถามเดียวกัน: ค่านี้เทียบกับอะไร — แยกไปคนละท่า ผู้เรียนจะจำได้แค่ว่า "มี widget อีกตัว" ไม่ใช่ว่า "ค่าที่วัดได้ห้ามอยู่ลำพัง"

ท่า 3 CapSense มาทีหลัง เพราะมันเพิ่มตัวแปรใหม่เข้ามาสองอย่าง (ชิป 4000T และบัส I2C) ถ้าพังตอนนี้ เรารู้ทันทีว่าพังที่ของใหม่ ไม่ใช่ที่ของเก่า

ท่า 4 ฟิลเตอร์กับบรรทัดคุณภาพของค่า ต้องมาหลังจากเห็นค่าดิบสั่นด้วยตาตัวเองแล้ว ไม่งั้นจะไม่มีวันรู้ว่ามันแก้ปัญหาอะไรให้ · สองอย่างนี้อยู่ท่าเดียวกันเพราะตอบคำถามเดียวกัน: "ตัวเลขที่เห็นเชื่อได้แค่ไหน"

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

เชื่อมจุดให้เห็นภาพ — วันนี้อยู่ตรงไหนของเส้นทาง

คาบ 3-4 อินพุตแบบมี/ไม่มี ปุ่มกด สวิตช์บนจอ วันนี้ · คาบ 5 อินพุตแบบมีระดับ และการทำให้มันเชื่อถือได้ คาบ 6-8 เซนเซอร์วัดโลกจริง ฟิลเตอร์ตัวเดิมได้ใช้ต่อ คาบ 9-12 ส่งค่าที่กรองแล้ว ขึ้นเครือข่าย จาก 0/1 → มีระดับ → มาจากโลกจริง → ส่งออกไปให้คนอื่นใช้ต่อ

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

ใช้จริงที่ไหน — สี่มุมของงานอนาล็อกในสนามจริง

โรงงาน · วาล์วและตัวปรับความเร็ว ลูกบิดหน้าตู้คุมความเร็วมอเตอร์ผ่าน ADC เดียวกันนี้ ถ้าไม่กรอง มอเตอร์จะกระตุกตามบิตล่างที่กระพริบ ฟิลเตอร์ที่ใช้: EMA alpha ต่ำ เพราะยอมให้หน่วงได้ เครื่องมือแพทย์ · หัววัดชีพจร สัญญาณระดับมิลลิโวลต์ปนกับ noise จากไฟ 50 Hz ต้องนิ่งพอให้อ่าน แต่ห้ามหน่วงจนแจ้งเตือนช้า ฟิลเตอร์ที่ใช้: Median กัน spike + EMA ปรับให้ลื่น เครื่องใช้ไฟฟ้า · แผงสัมผัสไร้ปุ่ม เตาไฟฟ้า ลิฟต์ ตู้เย็น ใช้ CapSense แบบเดียวกับบอร์ดนี้ ไม่มีรูให้ฝุ่นและน้ำเข้า ทำความสะอาดง่าย อายุยาว โจทย์จริง: baseline ต้องขยับตามความชื้นและอุณหภูมิ เกษตร · วัดความชื้นดิน หัววัดในดินให้แรงดันอนาล็อกที่แกว่งตลอดเวลา กรองแล้วค่อยตัดสินใจเปิดปั๊ม ไม่งั้นปั๊มจะเปิด-ปิดรัว โจทย์จริง: ค่าที่กรองแล้วยังต้องมี hysteresis อีกชั้น

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

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

1 · สนาม alpha RAW · 0.05 · 0.5 จับเวลาที่ตามทัน 2 · Median ปะทะ spike เคาะบอร์ดสร้าง spike บันทึกว่าใครกันได้ 3 · ลูกบิดสั่งไฟ ช่วงเท่ากันตาม num_leds() กันกะพริบด้วยช่วงเผื่อ 4 · ปุ่มสลับหน้าที่ BTN0 สลับหน่วย BTN1 เปิด-ปิดฟิลเตอร์ ทั้งสี่ข้อต่อยอดจากไฟล์เฉลยเดิม ไม่ต้องเขียนใหม่ทั้งไฟล์

ข้อ 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

ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของ Eva Kit และ Dev Kit — แสดงหน้าจอที่ 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 อย่างเดียวจะได้ตัวอักษรทุกบรรทัดยกเว้นบรรทัดที่เลือก

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

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

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

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

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

07 ค่าหลุดหนึ่งค่า ทำลายค่าเฉลี่ย แต่ทำอะไร median ไม่ได้ · 08 ตัวกรองทั้งหกตัวของ dsp บนสัญญาณเส้นเดียวกัน · 09 เรียกทุกชื่อในโมดูล sensors แล้วดูว่าใครตอบ ใครปฏิเสธ
ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของ Eva Kit และ Dev Kit — แสดงหน้าจอที่ตัวอย่างสร้าง ค่าจากเซนเซอร์ WiFi และไมค์เป็นค่าแทนบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด

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

เอกสารและตำรา

ภาพ

  • วงจร Pot/Thermistor + ผัง CAPSENSE™ — คู่มือคิต รูปที่ 77 และรูปที่ 52 ใช้เพื่อการเรียนการสอน
  • สาธารณสมบัติ: “Potentiometer linear” · “Series RC capacitor voltage”
  • CC0 1.0: “23mm knob…01(DXO)” (Retired electrician) · “EMA weights N=15” (Д.Ильин)
  • CC BY 3.0: “Quantization error” (G. Maxwell) · “ADC animation 20” (R. Puskarcik) · “Capacitive touchscreen” (Medvedev)
  • CC BY-SA 4.0: “4-bit Successive Approximation DAC” (Uwezi)
  • ที่เหลือวาดใหม่เป็น inline SVG ทั้งหมด

ทุกตัวเลขบนสไลด์ชุดนี้สืบกลับไปที่เอกสารต้นทางหรือซอร์สโค้ดได้ ยกเว้นข้อที่เขียนกำกับไว้ว่ายังต้องวัด

หมายเหตุผู้สอน — ตัวอย่างโค้ด และข้อที่ยังไม่ลงตัว

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

หลักเดิมจากคาบก่อน ๆ ยังใช้ได้: เพิ่มตัวแปรใหม่ทีละหนึ่ง แล้วทุกความผิดพลาดจะชี้กลับไปที่สิ่งที่เราเพิ่งเพิ่มเสมอ

☰ สารบัญ