คาบ 3 — สั่งฮาร์ดแวร์ด้วยโค้ดของเราเอง

LED ทุกดวงบนบอร์ด ปุ่มหนึ่งปุ่ม และลูปที่ไม่มีวันหยุด

คาถาประจำคาบ: โปรแกรมฝังตัวคือลูปที่อ่านของจริง ตัดสิน แล้วสั่งของจริงกลับไป

เริ่มจากของที่เราเคยแตะแล้ว

วันนี้เราแทนที่ "นิ้ว + หน้า Controls (Eva)" ด้วย "ลูป Python ของเราเอง" หน้า Controls (Eva Kit) แตะด้วยนิ้ว s03_led_button.py gpio.led(i).on() btn.is_pressed() ลูปของทีมเรา บน CM33 ไม่ต้องพึ่งหน้า Controls หลอดจริงบนบอร์ด Eva: D3 D4 D5 · Dev Kit: 5 ดวง เดิม วันนี้ หลอดเดียวกัน ปุ่มเดียวกัน เปลี่ยนแค่ว่าใครเป็นคนสั่ง

เปิดบอร์ด แตะการ์ด Controls แล้วแตะวงกลมสีบนจอ หลอด LED จริงบนบอร์ดติดตามนิ้วเรา (การ์ดนี้มีเฉพาะ Eva Kit — ทีมที่ถือ Dev Kit ให้รัน examples/s01/11_lights_and_a_button.py↗ ซ้ำอีกรอบแล้วมองหลอดแทน ได้เห็นสิ่งเดียวกัน: ของจริงเปลี่ยนสถานะเพราะคำสั่ง)

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

วันนี้เราจะสั่งไฟดวงเดียวกันนี้ด้วยลูปที่ทีมเขียนเอง — คาบ 1 เรารัน examples/s01/11_lights_and_a_button.py↗ ไปแล้ว และเห็นไฟติดจริง แต่นั่นคือโค้ดที่เขียนมาให้ วันนี้เราเปิดโมดูล gpio ออกดูให้ครบทุกคำสั่ง แล้วประกอบลูปของเราขึ้นมาใหม่

หน้า Controls (บน Eva Kit) เป็นโปรแกรมที่คนอื่นเขียนไว้ให้ พอจบคาบนี้เราจะมีโปรแกรมของเราเองที่ทำงานกับหลอดไฟและปุ่มเดียวกัน โดยไม่ต้องพึ่งหน้านั้นอีก — และรันได้ทั้งสองบอร์ด

ของบนบอร์ดไม่ได้เป็นของหน้าจอไหนเป็นเจ้าของ — ใครสั่งก่อนก็ได้ไป

จากคาบที่แล้ว — บันไดสามขั้นของคาบ 2 ยังอยู่ในมือเรา

คาบ 2 ปีนบันไดนี้ไปแล้ว ท้ายคาบวันนี้เราจะปีนซ้ำ แล้ววางปุ่มกับไฟไว้บนยอด 1 · WiFi wifi.connect() 2 · ได้ IP ไม่ใช่ "0.0.0.0" 3 · broker mqtt.connect() ท้ายคาบวันนี้ ปุ่มที่กันเด้งแล้ว ไฟที่จำสถานะเอง ขึ้น broker ทั้งคู่

ทบทวนสั้น ๆ ก่อนลืม เพราะท้ายคาบเราต้องใช้ทุกขั้น

  • wifi.connect() บล็อกได้นานราว 85 วินาทีถ้าวงนั้นไม่มีในห้อง ป้าย "กำลังต่อ" จึงต้องขึ้นจอก่อนบรรทัดนี้
  • wifi.ip() ที่ยังได้ "0.0.0.0" แปลว่ามีลิงก์แต่ยังไม่มีที่อยู่ ส่งอะไรออกไม่ได้
  • mqtt.publish() ตอนสายหลุดไม่ได้คืน False มันโยน OSError · get_message() มีช่องเดียว ใบใหม่ทับใบเก่า

บอร์ดยังต่อ Hotspot มือถือของทีม เหมือนคาบ 2 (WiFi ของคณะต้อง login บอร์ดใช้ไม่ได้) WIFI_SSID WIFI_PASS ในไฟล์ 07 กับ 08 ใช้ชื่อกับรหัสชุดเดิม · คาบนี้ใช้ broker สาธารณะ broker.hivemq.com พอร์ต 1883 และชื่อทีมที่ผู้สอนแจก (team01 ถึง team19) ไฟล์ท้ายคาบตั้งต้นเป็น TEAM = "teamXX" และไม่ยอมรันจนกว่าจะแก้ ใช้ชื่อทีมคนอื่นเมื่อไร บอร์ดของทีมนั้นถูกเตะหลุดทันที

วันนี้ครึ่งแรกเป็นเรื่องบนโต๊ะล้วน ๆ ไฟ ปุ่ม และเวลา พอนับปุ่มได้ตรงแล้ว เราค่อยส่งมันออกไป

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

คำถาม คำตอบของคาบนี้ อยู่ช่วงไหน
Why ทำไมต้องคุมขาสัญญาณเอง ในเมื่อจอก็แสดงผลได้แล้ว เพราะ IoT ไม่ได้จบที่การแสดงค่า มันต้องสั่งของจริงให้ขยับ ทุกเครื่องจักรที่คุณเคยเห็นเปิดปิดเอง มีบรรทัดแบบนี้อยู่ข้างใน ครึ่งแรก · ทฤษฎีกระแสสองเส้นทาง
What มีอะไรให้ใช้บ้าง โมดูล gpio ทั้ง 18 ชื่อ — ตัวไหนตอบ "สิ่งที่วัดได้" ตัวไหนตอบแค่ "สิ่งที่เราสั่งไป" · บวก time.ticks_ms() ticks_diff() สำหรับคุมจังหวะ สไลด์บัญชี 18 ชื่อ
How ประกอบยังไงให้ใช้งานได้จริง ลูป polling ที่อ่านปุ่ม กันสัญญาณเด้ง แล้วสั่งไฟตามจังหวะที่ทีมตั้งเอง หกไฟล์ตัวอย่าง + ใบฝึก · ท้ายคาบอีกสองไฟล์ที่ส่งขึ้น broker (07 งานประยุกต์ · 08 เกมทั้งห้อง)

ปลายทางที่จับต้องได้ — ไฟทุกดวงบนบอร์ดวิ่งไล่กันตามจังหวะที่ทีมตั้ง (Eva 3 ดวง · Dev Kit 5 ดวง — โค้ดชุดเดียวกัน) และทุกครั้งที่กดปุ่มบนบอร์ด ตัวเลขบนจอเพิ่มขึ้นหนึ่ง

คาบ 1 เห็นไฟติดจากโค้ดที่เราเขียนให้ · คาบนี้เป็นคนเขียนลูปที่สั่งมันเอง

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

  1. สั่ง LED บนบอร์ดด้วย gpio.led(n) ได้ทั้ง เปิด ปิด สลับสถานะ และสั่งด้วยตัวเลขที่คำนวณได้
  2. อ่านปุ่มจริงด้วย gpio.button(0).is_pressed() แล้วบอกได้ว่า polling คืออะไร
  3. เขียนลูปที่คุมจังหวะด้วย time.sleep_ms() และ time.ticks_ms()/ticks_diff() เป็น
  4. อธิบายได้ว่าทำไมปุ่มถึง "เด้ง" และแก้ด้วยซอฟต์แวร์ได้
  5. บอกได้ว่าโมดูล gpio มีอะไรอยู่ทั้งโมดูล และตัวไหนตอบ "สิ่งที่วัดได้" ตัวไหนตอบแค่ "สิ่งที่เราสั่งไป"
  6. รู้จักกับดักชื่อของเฟิร์มแวร์ตัวนี้ — ชื่อที่โค้ดคืนกับป้ายบนแผ่นวงจรไม่ใช่ตัวเดียวกัน และรู้ว่าทำไมถึงจงใจให้เป็นแบบนั้น

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

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

ปลายทางของคาบนี้ — สามบรรทัดที่แปลว่าเราคุยกับบอร์ดรู้เรื่องแล้ว

หน้าจอจริงจากการรันโค้ดเฉลยบน BENTO Emulator ที่ 800x480 เท่าจอของทั้งสองบอร์ด — ไม่ใช่ภาพวาด ไม่ใช่ mock-up และไม่ใช่ภาพถ่ายจากบอร์ด · ภาพนี้เก่า ถ่ายตอนเฟิร์มแวร์ยังตอบชื่อปุ่มว่า "SW1" และก่อนโค้ดเฉลยเปลี่ยนสองบรรทัดล่าง (16 ก.ย.) — บนบอร์ดจริงวันนี้บรรทัดล่างจะเป็นตามข้อสามข้างล่าง รอถ่ายใหม่
  • จอมีแค่ Console สามบรรทัด เพราะผลลัพธ์จริงของคาบนี้อยู่ที่ LED บนตัวบอร์ด ไม่ใช่บนจอ
  • บรรทัดแรกเป็นค่าที่ถามบอร์ดเอาเอง: Eva Kit ตอบ LED 3 ดวง · Dev Kit ตอบ 5 ดวง · ปุ่ม 1 ปุ่มทั้งคู่
  • อีกสองบรรทัดคือกับดักชื่อปุ่ม — โค้ดเรียกมันว่า USER Button 1 ซึ่งไม่ใช่ป้ายที่พิมพ์บนแผ่นวงจร และบอร์ดสองรุ่นพิมพ์ต่างกัน (สไลด์ "กับดักชื่อ" เล่าว่าทำไม)

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

สองคาบที่ผ่านมาเราสะสมอะไรไว้บ้าง

คาบ 1 เป็นคาบเดินชมของ เราไล่ดูว่าไลบรารีชุดนี้มีอะไรบ้าง และหนึ่งในนั้นคือ examples/s01/11_lights_and_a_button.py↗ ที่ สั่งหลอดไฟจริงและอ่านปุ่มจริงไปแล้ว — ไฟไล่ทีละดวง หรี่ความสว่างด้วย hold() แล้วรอปุ่ม

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

พูดให้ตรง: วันนี้ไม่ใช่ครั้งแรกที่เราสั่งไฟ ครั้งแรกอยู่ในคาบ 1 แล้ว สิ่งที่ต่างคือคาบนั้นเรา รันของที่เขียนมาให้ เพื่อดูว่าบอร์ดทำอะไรได้บ้าง วันนี้เราเปิดฝาโมดูล gpio ออกดูทั้งใบ แล้วเขียนลูปควบคุมของทีมเราเอง

สามอย่างที่คาบ 1 ให้เห็นผ่าน ๆ แล้ววันนี้จะรู้จริง: เมื่อไร hold() ถึงจำเป็น · ทำไม duty() ถึงเชื่อไม่ได้ · และ ทำไม ปุ่มที่กดครั้งเดียวถึงนับได้หลายครั้ง

เครื่องมือเดิมที่ยังใช้ต่อ: lcd.print() กับ markup, time.sleep_ms(), ลูป for และหน้า Playground

คาบ 1-2 · เดินชมของ รันตัวอย่างที่เขียนมาให้ เห็นไฟติดแล้ว แต่ยังไม่รู้กลไก คาบ 3 · ผู้ควบคุม ลูปของเราสั่งของจริง อ่าน → ตัดสิน → สั่ง คาบ 4-12 · โครงเดิม เปลี่ยนแค่ว่าอ่านอะไร และสั่งอะไร วันนี้คือจุดที่บทบาทเปลี่ยน สองคาบแรกสะสม "รู้ว่ามีอะไร" · วันนี้เพิ่ม "สั่งมันได้"

ทุกคาบต่อจากนี้จะมีลูปหลักเป็นแกน — วันนี้คือลูปแรกของเรา

โมดูล gpio — สี่บรรทัดที่คุมของจริงได้

import gpio
gpio.led(0).on()        # ติดค้าง
gpio.led(0).off()       # ดับ
gpio.led(1).toggle()    # สลับสถานะจากเดิม
gpio.led(2).value(1)    # สั่งด้วยตัวเลข 1 = ติด, 0 = ดับ
gpio.led(2).brightness(50)   # หรี่เป็นเปอร์เซ็นต์ของกำลังไฟ 0-100
pressed = gpio.button(0).is_pressed()   # True ตอนที่นิ้วกดอยู่

gpio.led(n) ไม่ต้อง init ไม่ต้องบอกขา ไม่ต้องตั้งโหมด — เฟิร์มแวร์จัดการให้ตอนเรียกครั้งแรก · .name() ขอชื่อของหลอดตามที่เฟิร์มแวร์รู้จัก — เดี๋ยวจะเจอว่าชื่อนี้มีเรื่อง

n ใช้ได้ตั้งแต่ 0 ถึง gpio.num_leds() - 1 — Eva Kit 3 ดวง (0-2) · Dev Kit 5 ดวง (0-4) นอกช่วงได้ ValueError ทันที · ดวงไหนสีอะไร และดวงไหนมองเห็นบนบอร์ดที่ประกอบแล้ว ให้สั่ง on() ทีละดวงแล้วดูที่บอร์ดของทีม (ตาราง 4.1 ในใบงาน) — บน Dev Kit หลอด LED1/LED2 อยู่บนโมดูล ดวง RGB คือดวง 2-4

สี่คำสั่งนี้ทำอะไรกับสถานะของหลอดหนึ่งดวง .on() บังคับเป็น 1 .off() บังคับเป็น 0 .toggle() กลับด้านจากเดิม .value(1) / .value(0) สั่งด้วยตัวเลขที่คำนวณได้ ผลปลายทางคือหลอดจริง
.brightness(pct) ไม่ได้ลดแรงดันที่ขา แต่ สลับติด-ดับเร็วกว่าที่ตาจับได้ แล้วปล่อยให้ตาเห็นเป็นค่าเฉลี่ย ภาพนิ่งอธิบายเรื่องนี้ไม่ได้ ต้องดูสองภาพซ้ายตอนมันเคลื่อน — ซ้าย สัดส่วนเวลาติดต่อหนึ่งคาบคือสิ่งเดียวที่เปลี่ยน · ขวา เส้นค่าเฉลี่ยไต่ตามความกว้างพัลส์ "ความสว่างที่ตาเห็น" คือค่าเฉลี่ย ไม่ใช่ค่าที่ขาส่งออกจริง (ภาพ: "PWM duty cycle with label" สาธารณสมบัติ · "PWM gemiddelde waarde" โดย Jef daems CC0 1.0 — Wikimedia Commons)

on()/off() เมื่อรู้ว่าอยากได้สถานะไหน · toggle() เมื่อแค่อยากกลับด้าน · brightness(pct) ค่ากลาง 1-99 เลือกทางตามขาของหลอด: ดวงที่มีเส้น PWM ของฮาร์ดแวร์ (Eva 0-2 · Dev Kit ดวง RGB 2-4) ค้างระดับไว้ ไม่บล็อก · ดวงอื่นได้พัลส์ราว 12 ms แล้วจบด้วยหลอดดับ · ค้างได้ทุกดวงด้วย hold(pct, ms) ซึ่งบล็อกจนครบแล้วจบด้วยหลอดดับ — แต่ถ้าเพิ่งสั่ง brightness() ค่ากลางบนดวง PWM ต้องเรียก off() คั่นก่อน ไม่งั้นขายังถูก PWM ถืออยู่ hold() ไม่เห็นผลและหลอดไม่ดับตอนจบ (examples/s03/03 ทำแบบนั้น) · brightness(0)/100 ไม่บล็อก และ 100 ทิ้งหลอดติดค้าง · มองหลอดจริงเป็นคนตัดสิน

ทั้งโมดูล gpio มีอยู่เท่านี้ (1/2) — สิบแปดชื่อ ไม่มีมากกว่านี้

สไลด์นี้ไม่ได้ให้ท่อง แต่ให้ รู้ว่าอะไรมีอยู่ จะได้ไม่ไปเขียนของที่ไม่มี แล้วนั่งงงว่าทำไม error

ระดับโมดูล — ห้าฟังก์ชัน สองชนิด (import gpio แล้วเรียกได้เลย)

ชื่อ คืนอะไร ใช้ตอนไหน
gpio.board_info() dict ห้าช่อง: name leds buttons led_names btn_names เปิดโปรแกรมมา อยากรู้ว่าบอร์ดนี้มีอะไร
gpio.num_leds() int — Eva Kit ได้ 3 · Dev Kit ได้ 5 เขียน range() ให้ย้ายบอร์ดแล้วยังถูก — คอร์สนี้ย้ายจริง
gpio.num_buttons() int — ได้ 1 เสมอ เพราะเฟิร์มแวร์ตรึงไว้ที่ 1 (ทั้งสองบอร์ด) เช็กก่อนวนอ่านปุ่ม
gpio.led(n) อ็อบเจกต์ LED · n นอกช่วง 0..num_leds()-1 โยน ValueError ทุกครั้งที่จะสั่งไฟ
gpio.button(n) อ็อบเจกต์ Button · n ต้องเป็น 0 เท่านั้น ทุกครั้งที่จะอ่านปุ่ม
gpio.LED · gpio.Button ชนิด ไม่ใช่ฟังก์ชัน เรียกสร้างเองไม่ได้ isinstance(x, gpio.LED) ตอนเขียนฟังก์ชันที่รับได้ทั้งไฟและปุ่ม

เมธอดของ LED — แปดตัว · เมธอดของ Button — สามตัว

LED ทำอะไร กับดัก
.on() .off() บังคับเป็น 1 / 0 —
.toggle() กลับด้านจากเดิม ไม่ไปแก้ตัวเลขของ duty() พอ on() แล้ว toggle() หลอดดับแต่ duty() ยังตอบ 100
.value() อ่านระดับขาจริงกลับมา หลัง hold() ได้ 0 เสมอ (และหลัง brightness() ค่ากลางบนดวงที่ไม่มีเส้น PWM) — ตอบระดับขา ไม่ตอบความสว่าง
.value(n) ตัวเดียวกันแต่ใส่อาร์กิวเมนต์ = สั่ง 1/0 ใช้ตอนค่ามาจากการคำนวณ
.name() ชื่อในตารางเฟิร์มแวร์ Eva: ดวงที่ 2 ตอบ "RGB_RED" แต่ติดเป็นน้ำเงิน · Dev Kit: มี RGB_RED RGB_BLUE RGB_GREEN ครบ — หาดวงจากชื่อได้
.brightness(pct) 1-99: ดวงที่มีเส้น PWM ของฮาร์ดแวร์ (Eva 0-2 · Dev Kit 2-4) ค้างระดับไว้ ไม่บล็อก · ดวงอื่นได้พัลส์ 12 ms หนึ่งชุด (200 µs × 60 รอบ) แล้วจบด้วยหลอดดับ 0 กับ 100 เป็นทางลัด สั่งขาตรง ๆ แล้วคืนทันที — 100 จบด้วยหลอดติดค้าง ไม่ได้ดับ · จะ hold() ต่อจาก brightness() ค่ากลางบนดวง PWM ให้ off() คั่นก่อน (examples/s03/03 ทำแบบนั้น)
.hold(pct, ms) ย้ำพัลส์นั้นจนครบ ms · ms ไม่ใส่ได้ ปริยาย 500 · ใช้ได้ทุกดวง จบด้วยหลอดดับ บล็อกจนครบ ระหว่างนั้นปุ่มไม่ถูกอ่าน · 100 ก็ค้างติดเหมือนกัน · ไม่ปลดเส้น PWM ให้ — ถ้าเพิ่ง brightness() ค่ากลางบนดวง PWM แล้วไม่ off() คั่น hold() จะไม่เห็นผลและหลอดไม่ดับตอนจบ
.duty() เปอร์เซ็นต์ที่ เราสั่งไปครั้งล่าสุด ไม่ได้ไปวัดหลอด · on() off() value(n) brightness() hold() เขียนตัวเลขนี้ทั้งหมด แต่ toggle() ไม่เขียน มันจึงเก่าได้

ทั้งโมดูล gpio มีอยู่เท่านี้ (2/2) — ปุ่ม และของที่ไม่มีให้เรียก

เมธอดของ Button — สามตัว

Button ทำอะไร กับดัก
.is_pressed() True ตอนกดอยู่ — แปลความหมายให้แล้ว ใช้ตัวนี้เป็นหลัก
.value() ระดับไฟฟ้าดิบ กด = 0 if btn.value(): ทำงานกลับด้านทั้งหมด
.name() ชื่อในตารางเฟิร์มแวร์ ตอบ "USER Button 1" ทั้งสองบอร์ด — จงใจไม่ใช้ป้ายบนแผ่นวงจร (สไลด์กับดักชื่อเล่าว่าทำไม)

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

ชื่อ สถานะจริง
machine.PWM · machine.ADC · machine.SPI พอร์ตนี้ไม่ได้เขียนไว้เลย ไม่มีไฟล์ ไม่มีชื่อ
machine.Timer มีโค้ดอยู่ในพอร์ต แต่เฟิร์มแวร์ชุดนี้ไม่ได้เปิดสวิตช์คอมไพล์ให้ (เครื่องเกมของคอร์สอื่นเปิด) ผลที่เราเจอจึงเหมือนกันคือเรียกไม่ได้
machine.I2C อันนี้มี แต่ยังไม่ใช่เรื่องของคาบนี้

ไม่มี machine.PWM ให้เรียกจาก Python ก็จริง แต่ฝั่งเฟิร์มแวร์ brightness() ต่อดวง RGB เข้ากับ TCPWM ในตัวชิปให้แล้ว (ตาราง s_led_pwm_routes ใน modgpio.c) ดวงที่ไม่มีเส้นนั้นเท่านั้นที่ต้องกะพริบขาเองด้วยซอฟต์แวร์ — ดังนั้นการหรี่ไฟทำผ่าน gpio ไม่ใช่ผ่าน machine

เห็นคำตอบใน Stack Overflow ที่ขึ้นต้นด้วย machine.PWM(...) เมื่อไร ให้รู้ทันทีว่าคำตอบนั้นไม่ใช่ของบอร์ดเรา

ให้บอร์ดตอบเองว่ามีอะไรอยู่

info = gpio.board_info()
...
n = gpio.num_leds()        # 3 หรือ 5 แล้วแต่บอร์ด
...
for i in range(n):
    gpio.led(i).off()
...
print("board_info() =", info)
# Eva Kit:  {'name': 'PSoC Edge Eval Kit', 'leds': 3, 'buttons': 1,
#            'led_names': ['LED1', 'LED2', 'RGB_RED'], 'btn_names': ['USER Button 1']}
# Dev Kit:  {'name': 'PSoC Edge AI Dev Kit', 'leds': 5, 'buttons': 1,
#            'led_names': ['LED1', 'LED2', 'RGB_RED', 'RGB_BLUE', 'RGB_GREEN'], 'btn_names': ['USER Button 1']}

นิสัยที่ดีของงาน embedded คือ ถามอุปกรณ์ ไม่ใช่เดาจากความจำ — for i in range(3) จะพังเงียบ ๆ เมื่อย้ายไปบอร์ดที่มีห้าดวง (ซึ่งคือ Dev Kit ในห้องนี้) แต่ for i in range(n) ย้ายไปแล้วยังถูก (gpio.num_buttons() ตอบ 1 ทั้งสองบอร์ด) · สังเกตชื่อในรายการให้ดี 'RGB_RED' บน Eva กับ 'USER Button 1' บนทั้งสองบอร์ด — สองชื่อนี้ไม่ตรงกับสิ่งที่ตาเห็นหรือป้ายที่พิมพ์ไว้ คนละเหตุผลกัน

ภาพ: KIT_PSE84_EVAL PSOC™ Edge E84 Evaluation Kit guide, Infineon 002-39007 Rev.*B, รูปที่ 2 (หน้า 9) — ใช้เพื่อการเรียนการสอน · นี่คือ Eva Kit: หลอด D3/D4/D5 และปุ่มผู้ใช้ (ป้ายบนแผ่นวงจร SW2) คือของที่ board_info() รายงานเป็น 3 ดวงกับ 1 ปุ่ม — Dev Kit รายงาน 5 ดวงกับ 1 ปุ่ม และคำตอบที่บอร์ดพูดเองคือคำตอบที่ย้ายบอร์ดแล้วยังถูก · ภาพหน้าจอของ examples/s03/01_board_info.py↗ ถอดออกชั่วคราว ไฟล์ถูกลดจำนวน widget ลงเมื่อ 14 ส.ค. รอถ่ายใหม่

เขียนโค้ดให้ถามบอร์ด แล้วโค้ดจะรอดตอนย้ายบอร์ด

กับดักชื่อ — เรื่องที่ต้องรู้ก่อนเสียเวลาทั้งบ่าย

สิ่งที่โค้ดบอก ความจริงบนบอร์ด ต้องทำอย่างไร
gpio.led(2).name() คืน "RGB_RED" Eva Kit: หลอดที่ติดจริงเป็นสี น้ำเงิน (ชื่อตกทอดจากตารางร่วม) · Dev Kit: ตารางเฟิร์มแวร์ (modgpio.c) บอกว่าดวง 2 คือแดง และมี RGB_BLUE RGB_GREEN เป็นดวง 3-4 — ยังไม่มีใครยืนยันด้วยตาบนบอร์ดที่ประกอบแล้ว ทีมเป็นคนแรก เชื่อสายตา — สั่ง on() ทีละดวงแล้วดู ไม่ต้องเชื่อชื่อ
gpio.button(0).name() คืน "USER Button 1" ไม่ใช่ป้ายบนแผ่นวงจร — Eva Kit พิมพ์ว่า SW2 · Dev Kit: บนฐาน QWA309 สวิตช์ที่พิมพ์ว่า SW หลายตัวเป็นสวิตช์ตัดไฟเลี้ยง (ในผังวงจร SW2/SW3/SW4/SW11 คือสวิตช์ไฟของกล้อง/mikroBUS/ADU/RGB) ห้ามโยกสวิตช์ใดบนฐานที่บทเรียนไม่ได้สั่ง ตั้งใจ ให้เป็นแบบนี้ — เรียกปุ่มด้วยชื่อจาก .name() เสมอ ห้ามบอกเพื่อนว่า "กด SW2"
บนบอร์ดมีปุ่มอื่นอีก (Eva: SW4 · Dev Kit: ปุ่มกดสองปุ่มของฐาน ใช้ผ่านโมดูล buttons เท่านั้น — ชื่อบนแผ่นรุ่นผลิต SW4/SW5) gpio.button(1) โยน ValueError ทั้งสองบอร์ด คาบนี้ใช้ปุ่มเดียวคือ gpio.button(0)
gpio.led(2).value() คืน 0 ทั้งที่หลอดเพิ่งสว่าง อ่านกลับได้จริง แต่ hold() (และ brightness() ค่ากลางบนดวงที่ไม่มี PWM) จบด้วยขาต่ำ จำสถานะไว้ในตัวแปรของเราเอง
duty() ตอบ 100 ทั้งที่หลอดดับอยู่ เพิ่งเรียก toggle() ซึ่ง ไม่ไปแก้ตัวเลขของ duty() duty() คือ "สั่งอะไรไปล่าสุด" ไม่ใช่ "หลอดเป็นอย่างไรตอนนี้"

Eva Kit: led(0) แดง · led(1) เขียว · led(2) น้ำเงิน — จำจากสายตา ไม่ใช่จากชื่อ · Dev Kit: ดวง RGB คือ 2-4 ตามชื่อใน led_names ส่วน LED1/LED2 อยู่บนโมดูล — ยืนยันด้วยสายตาบนบอร์ดของทีมเหมือนกัน

สองชื่อนี้หลอกด้วยเหตุผลคนละแบบ RGB_RED มาจากตารางร่วมของเฟิร์มแวร์ที่บอร์ดหลายรุ่นใช้ด้วยกัน ดวงที่สามของ Eva จึงได้ชื่อของดวงแดงบนบอร์ดอื่น · USER Button 1 ตรงกันข้าม มันถูกเลือกโดยตั้งใจให้ไม่ตรงกับป้ายบนแผ่นวงจร เพราะบน Dev Kit คำว่า "SW2" ชี้ไปที่สวิตช์ตัดไฟบนฐาน — ชื่อที่มีไว้ให้คนหาปุ่มด้วยนิ้ว ต้องไม่พาไปกดของผิดชิ้น (เหตุผลเขียนไว้ใน modgpio.c ตรงตารางปุ่ม) · สิ่งที่วิศวกรทำคือ ทดลองหนึ่งครั้งแล้วจดไว้ ไม่ใช่เถียงกับเอกสาร

ชื่อที่โค้ดตอบ ของจริงที่ตาเห็น led(2).name() = "RGB_RED" Eva: หลอด D5 สว่างเป็นสีน้ำเงิน ไม่ตรงกัน (ชื่อตกทอด) button(0).name() = "USER Button 1" Eva พิมพ์ SW2 · Dev Kit: SW2 บนฐานคือสวิตช์ไฟ ไม่ตรงกัน (ตั้งใจ) ทางแก้: ดูสีด้วยตาครั้งเดียว แล้วตั้ง LED_BLUE = ดัชนีที่เห็นว่าน้ำเงิน · ปุ่มเรียกด้วย btn.name()

อย่าให้ชื่อตัวแปรของทีมเราสืบทอดความสับสนต่อ — หาดัชนีดวงน้ำเงินครั้งเดียว (ตาเห็น หรือชื่อ RGB_BLUE ถ้าบอร์ดมี) แล้วตั้งชื่อว่า LED_BLUE ไปเลย · ส่วนปุ่ม ให้ป้ายบนจอพิมพ์ btn.name() ออกมาตรง ๆ คนก้มดูบอร์ดกับคนมองโค้ดจะได้เรียกปุ่มเดียวกันด้วยชื่อเดียวกัน

เข้าใจฮาร์ดแวร์ · ทำไมไฟ "1 คือติด" แต่ปุ่ม "0 คือกด"

LED · active-high (ผ่าน MOSFET) 3.3 V R LED ขาชิป P16[7] สั่ง 1 = เปิดประตู = หลอดติด ปุ่ม · active-low (pull-up) 3.3 V R ดึงขึ้น ไปที่ขาชิป ปุ่ม → กราวด์ ไม่กด = 1 กด = 0 (ลงกราวด์)

ฝั่ง LED มีของที่คนส่วนใหญ่ไม่ทันคิด: ขาชิปไม่ได้จ่ายกระแสให้หลอดเลย มันไปที่ gate ของ n-MOSFET กระแสของหลอดมาจากราง 3.3 V ผ่านตัวต้านทาน ลงหลอด ลง drain แล้วลงกราวด์ — วงจรและเลขขาในหน้านี้มาจากคู่มือ Eva Kit (ขา P16.7/P16.6/P16.5) ส่วน Dev Kit ดวง RGB อยู่ที่ P20.6/P20.5/P20.4 ตามตารางในเฟิร์มแวร์ วงจรขับยังไม่ได้เปิดคู่มือตรวจ แต่ฝั่งโค้ดเหมือนกันทุกประการ: สั่ง 1 = ติด

ภาพ: KIT_PSE84_EVAL PSOC™ Edge E84 Evaluation Kit guide, Infineon 002-39007 Rev.*B, รูปที่ 78 (หน้า 89) — ใช้เพื่อการเรียนการสอน · วงจรของ Eva Kit อ่านได้เลย D3 แดง 220 Ω · D4 เขียว 200 Ω · D5 น้ำเงิน 2.4 kΩ ขับด้วย MOSFET เบอร์เดียวกันทั้งสามดวง · ฝั่งปุ่ม is_pressed() กลับด้านให้แล้ว กดคืน True ส่วน button(0).value() คืนไฟฟ้าดิบ กดได้ 0 · ภาพหน้าจอของ examples/s03/04_button_active_low.py↗ ถอดออกชั่วคราว ไฟล์ถูกลดจำนวน widget ลงเมื่อ 14 ส.ค. รอถ่ายใหม่

ขาชิปสั่ง "ประตู" ไม่ได้สั่ง "น้ำ" — และเพราะประตูเปิด-ปิดได้เร็วมาก brightness() จึงหรี่ไฟได้ด้วยการกระพริบ gate ถี่ ๆ ไม่ใช่ด้วยการลดแรงดัน

กระแสสองเส้นทางที่แยกกัน — และเลขที่คำนวณได้จริง

ดัดแปลงจาก KIT_PSE84_EVAL user guide รูปที่ 78 — วงจรของ D3 หนึ่งดวงบน Eva Kit VDD_3V3 R324 = 220 Ω D3 แดง Q13 · n-MOSFET ขาชิป P16[7] ไปที่ gate — แทบไม่กินกระแสเลย เส้นแดง = กระแสของหลอด มาจากราง 3.3 V เส้นน้ำเงิน = สัญญาณจากชิป ไปสั่งเปิด-ปิดประตู สั่ง 1 ที่ gate → ประตูเปิด → หลอดติด (active-high)

Rseries=Vsupply−VFIF⟹IF=3.3−VF220 Ω≈3.3−2.0220≈5.9 mAR_{\text{series}} = \frac{V_{\text{supply}} - V_F}{I_F} \qquad\Longrightarrow\qquad I_F = \frac{3.3 - V_F}{220\,\Omega} \approx \frac{3.3 - 2.0}{220} \approx 5.9\ \text{mA}

ภาพ: Osbert Joel, Wikimedia Commons, CC BY-SA 4.0 — ช่องนำกระแสก่อตัวเมื่อแรงดันที่ gate สูงพอ นี่คือ "ประตู" ในภาพบน ต้องดูตอนเคลื่อนไหว
ซ้าย How to Select a Resistor for an LED — DigiKey (EN) · ขวา การคำนวณค่าตัวต้านทาน R สำหรับหลอด LED — KruNarut (ไทย) · ความยาวยังไม่ยืนยันทั้งคู่ · ดูว่าเขาอ่าน VF กับ IF จาก datasheet มาแทนในสูตรเดียวกันนี้อย่างไร
ตัวเลข 220 Ω มาจากคู่มือ Eva Kit จริง ส่วน VF ≈ 2.0 V เป็นค่า สมมติทั่วไปของ LED แดง — Dev Kit ยังไม่ได้เปิดคู่มือดูค่าตัวต้านทาน ทีมที่ถือ Dev Kit ใช้สูตรเดียวกันได้ทันทีที่หาค่าเจอ

ทำไมน้ำเงินใช้ 2.4 kΩ ทั้งที่แดงใช้ 220 Ω — VFV_F ของ LED น้ำเงินสูงกว่า ลองแทนเลขในสูตรเดียวกันแล้วเทียบดู

polling — วิธีที่โปรแกรมฝังตัวส่วนใหญ่รับรู้โลก

โปรแกรมของเราไม่มีทางรู้เองว่ามีคนกดปุ่ม สิ่งที่ทำได้คือ ถามซ้ำ ๆ ให้ถี่พอ

while True:
    pressed = btn.is_pressed()    # ถาม
    # ...ตัดสินใจจากค่าที่ได้...
    time.sleep_ms(5)              # พักหายใจ แล้วถามใหม่

รอบหนึ่งของลูปมีสามจังหวะเสมอ: อ่านของเข้า → ตัดสิน → สั่งของออก วนแบบนี้ไปเรื่อย ๆ

ถามถี่แค่ไหนถึงพอ นิ้วคนกดปุ่มเร็วสุดราว 50-80 ms ต่อครั้ง ถ้าเราถามทุก 5-20 ms ก็ไม่มีทางพลาด แต่ถ้าถามทุก 300 ms จะมีการกดที่หลุดหายไปเงียบ ๆ

ถามทุก 5 ms — การกดยาว 60 ms ถูกเห็นสิบกว่ารอบ ไม่มีทางพลาด นิ้วกดค้าง ~60 ms ถามทุก 300 ms — รอบถามอยู่ห่างกว่าที่นิ้วกดค้าง การกดหายเงียบ ๆ นิ้วกดค้าง ~60 ms

time.sleep_ms(5) ในลูปไม่ได้มีไว้ถ่วงเวลาเล่น ๆ — มันคือการคืนซีพียูให้งานอื่น (จอ เซนเซอร์ WiFi) ได้ทำงานด้วย ลูปที่ไม่มีการพักเลยจะกินเครื่องจนของอื่นกระตุก

ลูปที่ดีไม่ใช่ลูปที่เร็วที่สุด แต่คือลูปที่ เร็วพอ และเหลือที่ให้คนอื่นหายใจ

เข้าใจฮาร์ดแวร์ · ปุ่มหนึ่งครั้ง ทำไมนับได้สามครั้ง

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

1 0 เวลา (แต่ละช่วง = 5 ms) ช่วงเด้ง 1-20 ms ยังไม่กด กดค้างอยู่ นิ่งแล้ว นับตรงนี้ครั้งเดียว

ถ้านับทุกครั้งที่เห็นการเปลี่ยน ตัวเลขจะกระโดดทีละ 3-5 ทั้งที่นิ้วกดครั้งเดียว · ภาพล่างคือ ของจริง จากออสซิลโลสโคป

ภาพ: Tomoldbury / Wikimedia Commons — สาธารณสมบัติ · หนามที่เห็นคือหน้าสัมผัสโลหะที่ยังเด้งอยู่จริง ไม่ใช่สัญญาณรบกวนจากที่อื่น · ภาพหน้าจอของ examples/s03/05_debounce_count.py↗ ถอดออกชั่วคราว ไฟล์ถูกลดจำนวน widget ลงเมื่อ 14 ส.ค. รอถ่ายใหม่ — ตอนรันจริงจะเห็นเลขสองตัววิ่งคู่กัน ส้มคือนับดิบ เขียวคือนับกันเด้ง

กันเด้งด้วยซอฟต์แวร์ — รอให้นิ่งก่อนค่อยเชื่อ

วิธีคิด: อย่าเชื่อค่าที่เพิ่งเปลี่ยน รอให้มันคงที่นานพอ แล้วค่อยยอมรับว่าเปลี่ยนจริง

raw = btn.is_pressed()              # ค่าดิบจากปุ่มรอบนี้
if raw != last_raw:                 # ค่าเพิ่งขยับ - จับเวลาใหม่ ยังไม่เชื่อ
    last_raw = raw
    last_change = now
elif raw != stable and time.ticks_diff(now, last_change) >= DEBOUNCE_MS:
    stable = raw                    # นิ่งครบ 40 ms แล้ว ยอมรับเป็นของจริง
    if stable:                      # ขอบขาเข้า = เพิ่งเปลี่ยนจากปล่อยเป็นกด
        count += 1

ตัวแปรสามตัวที่ต้องแยกให้ออก: raw ค่าดิบรอบนี้ · last_raw ค่าดิบรอบก่อน · stable ค่าที่เรายอมเชื่อแล้ว · count += 1 อยู่ใต้ if stable: เพราะเราต้องการนับ ตอนกดลง ไม่ใช่ตอนปล่อย

ค่าดิบเด้ง แต่ค่าที่เรา "เชื่อ" ขยับครั้งเดียว raw เด้ง stable รอครบ DEBOUNCE_MS = 40 ms count += 1 ตรงนี้ ครั้งเดียว

DEBOUNCE_MS 40 ms ใช้ได้ดีกับปุ่มทั่วไป น้อยกว่า 10 ยังเด้งหลุด มากกว่า 200 จะรู้สึกว่าปุ่มหน่วง

อยากเห็นตัวเลขจริง เปิด examples/s03/05_debounce_count.py↗ แล้วลดค่า DEBOUNCE_MS ลงทีละ 10 จนเลขนับดิบกับเลขนับกันเด้งเริ่มแยกจากกัน นั่นคือจุดที่หน้าต่างเวลาสั้นเกินไปจนรับการเด้งตอนปล่อยมาเป็นการกดครั้งใหม่

การกันเด้งคือ การไม่รีบเชื่อข้อมูลที่เพิ่งมาถึง — แนวคิดเดียวกันนี้จะกลับมาอีกตอนกรองสัญญาณเซนเซอร์ในคาบ 5

นาฬิกาสองแบบในลูปเดียว

time.sleep_ms(150)                        # หยุดโปรแกรมทั้งตัว 150 ms
now = time.ticks_ms()                     # ขอเวลาปัจจุบันของบอร์ด
time.ticks_diff(now, last_step) >= 150    # ผ่านมา 150 ms หรือยัง

sleep_ms ง่ายแต่หยาบ ระหว่างที่หลับ ปุ่มกดยังไงก็ไม่มีใครเห็น ถ้าเราหลับทีละ 150 ms เพื่อรอไฟวิ่ง การกดปุ่มสั้น ๆ จะหายไปเลย

ticks_ms() คือมิลลิวินาทีนับจากบอร์ดบูต เอาไว้ตอบคำถามว่า "ถึงเวลาทำอันนั้นหรือยัง" โดยไม่ต้องหยุดโปรแกรม

ทำไมต้อง ticks_diff(a, b) แทนการลบตรง ๆ — ตัวเลขนี้วิ่งถึงเพดานแล้ววนกลับไปเริ่มใหม่ ถ้าลบเองจะได้ค่าติดลบมหาศาลตอนวน ticks_diff จัดการเรื่องนี้ให้แล้ว

sleep_ms(150) — ตาบอด 150 ms การกดสั้นหายทั้งครั้ง หลับ 150 ms หลับ 150 ms หลับ 150 ms การกดตกลงมาตอนหลับ — ไม่มีใครเห็น sleep_ms(5) + ticks_diff — ตื่นถี่ แล้วถามนาฬิกาว่าถึงคิวไฟหรือยัง จุดเล็ก = รอบถามปุ่มทุก 5 ms · จุดใหญ่ = ครบ 150 ms แล้วขยับไฟหนึ่งดวง

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

งานสองอย่างที่จังหวะไม่เท่ากัน อยู่ในลูปเดียวกันได้ ถ้าเลิกใช้ sleep เป็นตัวจับเวลา

เกร็ด: ปัญหาปุ่มเด้งเก่ากว่าคอมพิวเตอร์

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

ซ้าย รูปคลื่นของหน้าสัมผัสที่เด้ง — ภาพ: Super Rad! / Wikimedia Commons — CC0 1.0 · ขวา Pull Up Resistor Tutorial | AddOhms #15 ช่อง AddOhms · ความยาว: ยังไม่ยืนยัน · ดูเพื่อตอบว่า "ถ้าไม่มีตัวต้านทานดึงขึ้น ขาที่ยังไม่ถูกกดจะอ่านค่าอะไร"

เรื่องเฉพาะของ Eva Kit: วงจรปุ่มผู้ใช้ (ป้าย SW2/SW4 บนแผ่นวงจร) มีที่ว่างไว้ให้ใส่ตัวต้านทาน 10 kΩ และตัวเก็บประจุ 0.1 µF แต่คู่มือ ระบุว่าไม่ได้ลงอุปกรณ์จริง (DNI) — แปลว่าบอร์ดนี้ไม่มีวงจรกันเด้งแบบฮาร์ดแวร์เลย ต้องพึ่ง pull-up ในตัวชิปกับโค้ดของเราล้วน ๆ · Dev Kit ยังไม่ได้เปิดวงจรตรวจ — ให้ผลการทดลองข้อ 4 ของทีมเป็นคนตอบว่าปุ่มของบอร์ดนั้นเด้งแค่ไหน

เชื่อมกับวันนี้: บรรทัด DEBOUNCE_MS = 40 คือค่าที่เมื่อ 90 ปีก่อนต้องเปลี่ยนตัวเก็บประจุถึงจะปรับได้ วันนี้พิมพ์เลขใหม่แล้วกด Program to Device

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

สิ่งที่เฟิร์มแวร์ทำให้แล้ว (70%)
ตั้งค่าขา GPIO ให้เป็นเอาต์พุต/อินพุตพร้อม pull-up · แปลงระดับไฟฟ้าเป็น True/False ให้ · ส่งสถานะ LED ข้าม IPC ไปให้จอวาดตาม · จัดการนาฬิกาของระบบให้ ticks_ms() ใช้ได้

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

70% · เฟิร์มแวร์ทำให้แล้ว 30% · งานของเรา ตั้งค่าขา · แปลงระดับไฟเป็น True/False · ส่งสถานะข้าม IPC · นาฬิกาของระบบ ลูปหนึ่งรอบทำอะไร เมื่อไรถึงจะเชื่อค่าที่อ่านได้ 30% ของคาบนี้ไม่มีไวยากรณ์ Python ใหม่เลยสักบรรทัด — มีแต่การตัดสินใจ

สังเกตว่า 30% ของเราคราวนี้ไม่ใช่ไวยากรณ์ Python เลย แต่เป็น การตัดสินใจเชิงออกแบบ ทั้งหมด

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

แกะโค้ดจริง — ท่าที่ 1 ถามบอร์ดก่อนลงมือ

# --- ท่าที่ 1: ถามบอร์ดก่อนว่ามีอะไรให้เล่นบ้าง ---
info = gpio.board_info()
btn = gpio.button(0)

lcd.clear()
lcd.console("<h2>AIoT in Action - คาบ 3</h2>")
lcd.print("บอร์ด:", info["name"], "| LED:", info["leds"], "| ปุ่ม:", info["buttons"])
lcd.print("<span class=muted>ปุ่มผู้ใช้มีตัวเดียว ดัชนี 0</span>")
lcd.print("<span class=muted>โค้ดเรียกมันว่า " + btn.name() + "</span>")

เราเก็บ gpio.button(0) ไว้ในตัวแปร btn ครั้งเดียว แล้วใช้ซ้ำทั้งโปรแกรม แทนที่จะเรียก gpio.button(0) ใหม่ทุกรอบลูป — อ่านง่ายกว่า และไม่ต้องเสียเวลาค้นหาซ้ำหลายพันครั้งต่อนาที

บรรทัด btn.name() มีไว้ให้เห็นกับตาว่าเฟิร์มแวร์ตอบว่า USER Button 1 ซึ่งไม่ใช่ป้ายที่พิมพ์บนแผ่นวงจร — และเป็นชื่อเดียวที่ควรใช้เวลาบอกเพื่อนว่าให้กดปุ่มไหน (บน Dev Kit "SW2" คือสวิตช์ตัดไฟบนฐาน) เจอครั้งเดียวแล้วจะไม่ลืม

ผลของท่านี้บนจอ Playground AIoT in Action - คาบ 3 บอร์ด: PSoC Edge Eval Kit | LED: 3 | ปุ่ม: 1 (Dev Kit: PSoC Edge AI Dev Kit | LED: 5) ปุ่มผู้ใช้มีตัวเดียว ดัชนี 0 · โค้ดเรียกมันว่า USER Button 1

เริ่มโปรแกรมด้วยการ รายงานสิ่งที่เรารู้เกี่ยวกับฮาร์ดแวร์ ทำให้ตอนดีบักไม่ต้องเดา

แกะโค้ดจริง — ท่าที่ 2 ตั้งต้นจากสถานะที่รู้แน่

# --- ท่าที่ 2: ดับไฟให้หมดก่อน แล้วเตรียมตัวแปรสถานะ ---
for i in range(NUM_LEDS):
    gpio.led(i).off()
led_index = 0          # ตอนนี้ไฟดวงไหนกำลังติด
count = 0              # จำนวนครั้งที่กดปุ่ม
last_raw = False       # ค่าดิบของปุ่มรอบก่อน
stable = False         # ค่าปุ่มที่ผ่านการกันเด้งแล้ว

โปรแกรมก่อนหน้าอาจทิ้งไฟติดค้างไว้ การดับให้หมดก่อนทำให้รอบแรกของไฟวิ่งเริ่มจากจุดที่เรารู้แน่ — หลักการเดียวกับ lcd.clear() ในคาบ 1

ทำไมต้องมีตัวแปร led_index ทั้งที่ถามหลอดไฟเองก็ได้ — gpio.led(2).value() อ่านกลับได้จริง (เฟิร์มแวร์เรียก Cy_GPIO_Read()) แต่มันตอบระดับของขา ณ วินาทีที่ถาม ไม่ใช่ความสว่างที่เราตั้งใจ · hold() จบด้วยขาต่ำเสมอ (และ brightness() ค่ากลาง 1-99 ก็เช่นกันบนดวงที่ไม่มีเส้น PWM) อ่านตามหลังไปจึงได้ 0 ทั้งที่เพิ่งเห็นหลอดสว่าง

ถาม duty() แทนได้ไหม — ไม่ได้เหมือนกัน แต่คนละเหตุผล duty() ไม่ได้วัดหลอด มันคืน ตัวเลขที่เราสั่งไปครั้งล่าสุด และ toggle() ไม่ได้แก้ตัวเลขนั้น เรียก on() แล้ว toggle() หลอดดับ แต่ duty() ยังตอบ 100 · สองทางนี้ผิดคนละแบบ และทั้งคู่ชี้ไปที่คำตอบเดียวกัน

ถามหลอดว่าตอนนี้ติดอยู่ไหม led(2).value() บอกแค่ระดับขา หลัง hold() ขาต่ำ จึงได้ 0 ทั้งที่เพิ่งเห็นหลอดสว่าง จำไว้ในตัวแปรของเราเอง led_index = 0 count · last_raw · stable ถูกเสมอ เพราะเราเป็นคนเขียนเอง สถานะที่โปรแกรมสั่งเอง เชื่อได้กว่าค่าที่อ่านกลับมา

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

แกะโค้ดจริง — ท่าที่ 3 ไฟวิ่งที่ไม่หยุดโปรแกรม

# --- ท่าที่ 3: ไฟวิ่งตามนาฬิกา ไม่ใช่ตาม sleep ---
if chase_on and time.ticks_diff(now, last_step) >= STEP_MS:
    gpio.led(led_index).off()              # ดับดวงเดิมก่อน
    led_index = (led_index + 1) % NUM_LEDS # เลื่อนไปดวงถัดไป วนกลับที่ 0 เอง
    gpio.led(led_index).on()               # จุดดวงใหม่
    last_step = now                        # จดเวลาไว้สำหรับรอบหน้า
    for k in range(NUM_LEDS):              # ไฟบนจอสะท้อนหลอดจริง
        led_ui[k].value(1 if k == led_index else 0)
ผลของท่านี้: ดับดวงเดิม → เลื่อน index → จุดดวงใหม่ ทุก STEP_MS led_index = 0 1 2 (2+1) % 3 = 0 Dev Kit: (4+1) % 5 = 0

% NUM_LEDS คือหัวใจของคำว่า "วิ่งวน" — พอ led_index ถึง 3 เศษของการหารด้วย 3 คือ 0 มันจึงกลับไปเริ่มดวงแรกเองโดยไม่ต้องเขียน if (บน Dev Kit NUM_LEDS เป็น 5 บรรทัดเดียวกันวนที่ 5 เอง — นี่คือเหตุผลที่ไม่เขียนเลข 3 ลงไปตรง ๆ)

ลำดับ ดับก่อน-เลื่อน-จุดใหม่ สำคัญมาก ถ้าสลับเป็นจุดใหม่ก่อนแล้วค่อยดับ จะมีเสี้ยวเวลาที่ไฟติดพร้อมกันสองดวง ตาอาจไม่ทัน แต่มันคือความไม่ตรงกับที่เราตั้งใจ · chase_on คือธงที่ปุ่ม "หยุดไฟวิ่ง" บนจอเป็นคนพลิก (ท่าที่ 4 ครึ่งหลังในไฟล์เฉลย) — ไฟหยุดได้โดยลูปยังเดินอ่านปุ่มต่อ · สองบรรทัดท้ายให้ไฟบนจอ led_ui สะท้อนหลอดจริง ดวงที่ดับหรี่ ไม่ใช่หาย

last_step = now ต้องอยู่ใน if เท่านั้น ถ้าเลื่อนออกไปนอก if เงื่อนไขจะไม่มีวันเป็นจริง แล้วไฟจะไม่วิ่งเลย

เทียบกับ examples/s03/02_led_blink.py↗ ได้เลย ไฟล์นั้นกะพริบดวงเดียวพร้อมนับรอบขึ้นจอ ตัวเลขที่เดินขึ้นทีละหนึ่งคือหลักฐานว่าลูปยังวิ่งอยู่ ไม่ได้ถูกอะไรบล็อกไว้

ปรับ STEP_MS แล้วรันใหม่ — นี่คือ "ปรับจังหวะได้" ตามเกณฑ์ผ่านของคาบนี้

แกะโค้ดจริง — ท่าที่ 4 อ่านปุ่ม กันเด้ง แล้วนับ

# --- ท่าที่ 4: อ่านปุ่มทุกรอบ แต่เชื่อเฉพาะค่าที่นิ่งแล้ว ---
raw = btn.is_pressed()

if raw != last_raw:
    last_raw = raw
    last_change = now
elif raw != stable and time.ticks_diff(now, last_change) >= DEBOUNCE_MS:
    stable = raw
    if stable:
        count += 1
        lcd.print("กดครั้งที่", count)
ภาพหน้าจอจริงจากบอร์ด Eva Kit ขณะรัน examples/s03/06_button_picks_led.py↗ — บันทึกโดยผู้สอน · จำนวนครั้งที่นับได้ตรงกับจำนวนครั้งที่กดจริง คือหลักฐานว่าโค้ดกันเด้งทำงาน · ไฟล์นั้นยังสอนว่าความจริงเรื่อง "ดวงไหนติดอยู่" ควรอยู่ที่ตัวแปรของเรา เพราะ led().value() ตอบแค่ระดับของขา ณ วินาทีที่ถาม ซึ่งเป็น 0 หลัง hold() (และหลัง brightness() ค่ากลางบนดวงที่ไม่มีเส้น PWM) · ภาพนี้เก่า ยังพิมพ์ "กดปุ่ม SW2 บนบอร์ด (โค้ดเรียก SW1)" — ไฟล์ปัจจุบันพิมพ์ชื่อจาก btn.name() รอถ่ายใหม่

บล็อกนี้ทำงานทุกรอบลูป คือทุก 5 ms ในขณะที่ไฟวิ่งขยับทุก 150 ms — สองจังหวะอยู่ในลูปเดียวกันได้เพราะไม่มีใครใช้ sleep ยาว · lcd.print อยู่ในบล็อกนี้เพราะเราอยากพิมพ์ เฉพาะตอนที่มีเหตุการณ์จริง ถ้าย้ายออกไปพิมพ์ทุกรอบ จอจะถูกยิง 200 บรรทัดต่อวินาทีจนอ่านอะไรไม่ได้

เครื่องสถานะเล็ก ๆ ที่มีสามตัวแปร และทางออกทางเดียวที่นับ raw ขยับ จับเวลาใหม่ ยังไม่เชื่อ raw นิ่งครบ 40 ms ยอมรับเป็น stable stable เป็น True count += 1 · พิมพ์หนึ่งบรรทัด ถ้าเอา if stable: ออก — ตอนปล่อยปุ่มก็ผ่านทางนี้ด้วย เลขจะเพิ่มทีละสอง

ลองเอา if stable: ออกดูสักครั้ง แล้วจะเห็นเลขกระโดดทีละสอง — เพราะปล่อยปุ่มก็คือการเปลี่ยนสถานะเหมือนกัน

แกะโค้ดจริง — ท่าที่ 5 จบให้สะอาด

# --- ท่าที่ 5: ดับไฟ แล้วสรุปผลปิดท้าย ---
for i in range(NUM_LEDS):
    gpio.led(i).off()

lcd.print("<span class=ok>จบรอบทดสอบ กดปุ่มทั้งหมด " + str(count) + " ครั้ง</span>")
print("โปรแกรมจบแล้ว - ไฟทุกดวงถูกดับเรียบร้อย")
จบแบบไม่เก็บของ ไฟค้างอยู่ ทีมถัดไปไม่รู้ว่ามาจากใคร จบแบบเก็บของ ดับหมด + บรรทัดสรุปสีเขียวบนจอ ลูปจบเมื่อครบ RUN_MS ไม่ใช่ while True — บอร์ดถูกคืนให้ทีมทดลองรอบต่อไปได้ทันที

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

str(count) จำเป็นเพราะเรากำลังต่อสตริงด้วย + เพื่อแทรกตัวเลขไว้กลางแท็ก <span> — ถ้าใช้จุลภาคแบบ lcd.print("...", count) จะได้ช่องว่างเกินติดขอบแท็ก แบบเดียวกับที่เจอในคาบ 1

ทำไมโปรแกรมต้องมีวันจบ — ลูปของเราวิ่งตามเวลาที่ตั้งไว้ใน RUN_MS ไม่ใช่ while True เพราะเราอยากให้มันคืนบอร์ดให้ทีมได้ทดลองรอบต่อไปโดยไม่ต้องกด RESTART ทุกครั้ง

เขียนโปรแกรมที่ เก็บของก่อนกลับบ้าน — นิสัยนี้จะช่วยชีวิตตอนโปรเจกต์ใหญ่

ข้อมูลไหลไปทางไหน — นิ้ว ไฟ และจอ ในรอบเดียวกัน

ปุ่มผู้ใช้ นิ้วกด = ขาลง 0 ลูปของเรา บน CM33 is_pressed() ทุก 5 ms กันเด้ง 40 ms แล้วนับ ticks_diff ครบ 150 ms แล้วขยับไฟหนึ่งดวง LED จริงบนบอร์ด ติดทันที ไม่ผ่านใคร IPC แจ้งสถานะไฟให้ CM55 จอ 4.3 นิ้ว Playground: ตัวนับ Controls (Eva): ไฟ คำสั่ง LED หนึ่งครั้ง เดินสองเส้นทางพร้อมกัน: ไปที่ขาไฟจริง และไปที่จอ

gpio.led(n).on() ส่งสถานะข้าม IPC ให้ CM55 ทุกครั้งโดยเราไม่ต้องสั่ง — ของแถมที่ทำให้เห็นภาพ

วิธีรันบนบอร์ด — และรอบพิเศษที่ต้องลอง

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

รอบพิเศษ — ดู IPC ด้วยตาตัวเอง (Eva Kit เท่านั้น — Dev Kit ไม่มีการ์ด Controls ข้ามรอบนี้ได้)
รันซ้ำอีกครั้ง แต่คราวนี้ก่อนกด Program to Device ให้แตะการ์ด Controls ค้างไว้แทน Playground แล้วมองวงกลมสีบนหน้า Controls ระหว่างที่โปรแกรมของเราวิ่ง

วงกลมบนจอจะติด-ดับตามไฟวิ่งของเรา ทั้งที่โค้ดเราไม่ได้สั่งจออะไรเลยสักบรรทัด — เพราะทุกครั้งที่เราสั่ง LED เฟิร์มแวร์ส่งสถานะข้าม IPC ไปบอก CM55 ให้เอง

1 · เติม pass ครบ 6 จุด 2 · เปิด Playground ค้างหน้าไว้ 3 · Program มองไฟ + มองจอ 4 · กดปุ่ม ×10 ต้องได้ 10 พอดี 5 · พิเศษ (Eva) เปิด Controls แทน รอบที่ 5 คือรอบที่ทำให้เห็น IPC ด้วยตา ไม่ใช่แค่ได้ยินว่ามี

ดูรอบพิเศษเสร็จแล้วกลับมาที่หน้า Playground เพื่ออ่านตัวนับ แล้วกด RESTART รันใหม่

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

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

LED chaser ครบทุกดวงของบอร์ด ปรับจังหวะได้ + กดปุ่มผู้ใช้ (เรียกด้วยชื่อจาก .name()) นับครั้งแสดงบน LCD

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

  • [ ] ไฟวิ่งไล่กันเป็นวงครบทุกดวงที่ gpio.num_leds() บอก ไม่มีดวงไหนค้างติดหรือข้ามดวง (Dev Kit: ดวง LED1/LED2 อยู่บนโมดูล ถ้ามองไม่เห็นให้ดูดวง RGB สามดวงเป็นหลัก แล้วจดไว้ว่าเห็นกี่ดวง)
  • [ ] แก้ค่า STEP_MS แล้วรันใหม่ จังหวะไฟเปลี่ยนตามจริง (สาธิตอย่างน้อยสองค่า)
  • [ ] กดปุ่มผู้ใช้ (gpio.button(0)) สิบครั้ง ตัวเลขบนจอขึ้นสิบพอดี
  • [ ] อธิบายให้ผู้สอนฟังได้ว่าถ้าเอาโค้ดกันเด้งออก จะเกิดอะไรขึ้นและเพราะอะไร
  • [ ] จบโปรแกรมแล้วไฟทุกดวงดับ พร้อมบรรทัดสรุปสีเขียวบนจอ
  • [ ] ถ่ายรูปหรือคลิปสั้นแนบใน worksheet

ข้อที่ยากที่สุดคือข้อสี่ — ทำได้ไม่ยาก แต่ อธิบายได้ ต้องเข้าใจจริง

กับดักที่เจอบ่อย

อาการ สาเหตุที่แท้จริง วิธีแก้
กดหนึ่งครั้ง เลขขึ้น 3-5 ยังไม่ได้กันเด้ง หรือ DEBOUNCE_MS น้อยเกิน ตั้ง DEBOUNCE_MS = 40 แล้วลองใหม่
กดหนึ่งครั้ง เลขขึ้นทีละ 2 ลืม if stable: จึงนับทั้งตอนกดและตอนปล่อย นับเฉพาะขอบขาเข้า
กดแล้วเลขไม่ขึ้นเลย ใช้ sleep_ms ยาวในลูป ปุ่มหลุดตอนหลับ ใช้ ticks_diff คุมจังหวะ หลับแค่ 5 ms
ไฟไม่วิ่ง ค้างดวงเดียว last_step = now อยู่นอก if ย้ายเข้าไปในบล็อก if
ไฟติดพร้อมกันสองดวง จุดดวงใหม่ก่อนดับดวงเดิม เรียง off → เลื่อน index → on
(Eva) ไฟดวงที่ 3 เป็นสีน้ำเงิน ทั้งที่ชื่อ RGB_RED ชื่อในตารางร่วมของเฟิร์มแวร์ ตกทอดมาจากบอร์ดรุ่นอื่น เชื่อสายตา ตั้งค่าคงที่ของทีมเอง
(Dev Kit) สั่ง led(0)/led(1) แล้วมองไม่เห็นอะไรติด LED1/LED2 อยู่บนโมดูล — มองเห็นบนบอร์ดที่ประกอบแล้วหรือไม่ ยังไม่ได้วัด ดูดวง RGB (ดัชนี 2-4) เป็นหลัก แล้วจดผลที่เห็นลงใบงาน
กดปุ่มอื่นบนบอร์ดแล้วไม่มีอะไรเกิดขึ้น Python เข้าถึงได้ปุ่มเดียวคือ index 0 ใช้ปุ่มที่ gpio.button(0).name() บอก — บน Dev Kit ห้ามโยกสวิตช์ใดบนฐานที่บทเรียนไม่ได้สั่ง หลายตัวคือสวิตช์ตัดไฟเลี้ยง · ปุ่มกดสองปุ่มของฐานใช้ผ่านโมดูล buttons เท่านั้น
จอไม่ขึ้นตัวเลข แต่ไฟวิ่งปกติ ไม่ได้เปิดหน้า Playground ไว้ แตะการ์ด Playground แล้ว RESTART
duty() ตอบไม่ตรงกับหลอดที่เห็น toggle() ไม่ได้ไปแก้ตัวเลขของ duty() อ่าน duty() ว่า "สั่งอะไรไปล่าสุด" เท่านั้น
AttributeError ตอนเรียก machine.PWM PWM ADC SPI ไม่มีในพอร์ตนี้ · Timer มีโค้ดแต่เฟิร์มแวร์ชุดนี้ไม่ได้เปิดให้ หรี่ไฟด้วย gpio.led().brightness() (ดวง RGB ค้างระดับได้) หรือ hold(pct, ms) แทน
เรียก hold(80, 3000) แล้วปุ่มกดไม่ติดสามวินาที hold() บล็อกจนครบ ms ระหว่างนั้นไม่มีใครอ่านปุ่ม แบ่งเป็น hold() สั้น ๆ หลายครั้งในลูป

สังเกตว่ากับดักสามข้อแรกคือ ตรรกะเวลา ไม่ใช่ไวยากรณ์ — นี่คือลักษณะเฉพาะของงานฝังตัว

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

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

for i in range(NUM_LEDS):
    # เติม: gpio.led(i).off()
    pass

    # เติม: led_index = (led_index + 1) % NUM_LEDS
    pass

    # เติม: raw = btn.is_pressed()
    pass

        # เติม: last_change = now
        pass

            # เติม: count += 1
            pass

# เติม: lcd.print("<span class=ok>จบรอบทดสอบ กดปุ่มทั้งหมด " + str(count) + " ครั้ง</span>")
pass
หกจุด แบ่งเป็นสามกลุ่ม — เติมแล้วรันทีละกลุ่ม อย่ารวดเดียว กลุ่มไฟ · จุด 1-2 ดับให้หมด + เลื่อน index รันแล้วต้องเห็นไฟวิ่ง กลุ่มปุ่ม · จุด 3-5 อ่านดิบ + จับเวลา + นับ ระวังการเยื้องสามชั้น กลุ่มปิดท้าย · จุด 6 บรรทัดสรุปสีเขียว ใส่ str(count) ให้ถูก

ระวังการเยื้องบรรทัด — สามจุดกลางอยู่คนละชั้นกัน ถ้าเยื้องผิด Python จะเอาโค้ดไปไว้ผิดเงื่อนไข · ลำดับที่แนะนำ: เติมจุด 1-2 ให้ไฟวิ่งก่อน แล้วค่อยจุด 3-5 เรื่องปุ่ม สุดท้ายจุด 6

เติมทีละกลุ่มแล้วรัน — พอไฟวิ่งได้แล้วปุ่มพัง เรารู้ทันทีว่าปัญหาไม่ได้อยู่ที่ไฟ

ตัวอย่างชุดคาบ 3 — สามไฟล์แรกคือชุดที่กันไม่ให้ MVP พังทั้งวัน

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

ลำดับ · เรื่อง · เวลา ไฟล์ ลงมือทำอะไร แล้วจะเข้าใจอะไร
1 · ถามบอร์ดก่อนว่ามีอะไร · 10 นาที examples/s03/01_board_info.py↗ รันแล้วจดว่ามีไฟกี่ดวง ปุ่มกี่ปุ่ม ชื่อไหนคู่กับดัชนีอะไร · จะไม่ตกหลุมชื่อปุ่มกับชื่อสีที่ไม่ตรงกับความจริง เพราะเขียนจากค่าที่ถามมา
2 · ปุ่มนี้ 0 คือกด · 10 นาที examples/s03/04_button_active_low.py↗ กดปุ่มผู้ใช้ค้างไว้แล้วดูเลขกับเส้นกราฟตกลงพร้อมกัน · จะรู้ว่า value() คืนแรงดันดิบ ส่วน is_pressed() คืนความหมาย และ if btn.value(): ทำงานกลับด้านหมด
3 · นับให้ตรงด้วยการรอให้นิ่ง · 15 นาที examples/s03/05_debounce_count.py↗ กดสิบครั้งแล้วเทียบเลขนับดิบกับเลขนับกันเด้ง · จะอธิบายได้ว่าถ้าถอดโค้ดกันเด้งออกจะเกิดอะไร ซึ่งเป็นข้อที่ตกกันมากที่สุดของ MVP

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

อาการที่เจอ ไฟล์ที่ตอบอาการนั้น
ไฟยังกะพริบไม่เป็นจังหวะ หรือจบโปรแกรมแล้วไฟค้างติดโดยไม่ตั้งใจ examples/s03/02_led_blink.py↗ — กะพริบจริงพร้อมนับรอบขึ้นจอ toggle() ขึ้นกับค่าเดิมเสมอ ส่วน on() กับ off() ไม่สนใจค่าเดิม จึงต้องปิดท้ายด้วย off()
สั่งหรี่ไฟแล้วเงยหน้าไปมองหลอด ไม่เห็นความต่างสักระดับ examples/s03/03_led_brightness.py↗ — ไฟล์เลือกดวง RGB ให้ แล้วสั่ง brightness(40) ครั้งเดียวให้มองหลอดเองว่าค้างหรือวูบ (ดวงที่มีเส้น PWM ค้าง ดวงอื่นวูบ 12 ms แล้วดับ) จากนั้นเทียบสองระดับด้วย led.hold(pct, ms) ที่ค้างได้ทุกดวง และไฟล์นี้ยังพิสูจน์ให้เห็นว่า duty() ตอบเลขที่เราสั่ง ไม่ได้ไปวัดหลอด
ปุ่มเดียวต้องคุมไฟหลายดวง แต่จำไม่ได้ว่าตอนนี้ดวงไหนติด examples/s03/06_button_picks_led.py↗ — กดปุ่มเดิมซ้ำ ๆ แล้วดูดัชนีเลื่อนไปทีละดวง สถานะที่โปรแกรมจำไว้เองคือของที่เชื่อได้ที่สุด

อ่านเสริมนอกเวลา — เรื่องนี้อยู่นอกเกณฑ์ผ่านของคาบ 3 แต่คือรูปร่างที่ลูปวันนี้ไปโผล่ในโรงงานจริง: examples/usecase/01_andon_severity_lamp.py↗ เสาไฟ andon หนึ่งระดับความรุนแรงคือไฟหนึ่งดวง และต้องดับทุกดวงก่อนจุดดวงใหม่เสมอ ไม่งั้นคนที่มองจากอีกฝั่งโรงงานจะอ่านระดับผิด

สามไฟล์แรกคือของที่ต้องเปิดจริงในคาบ ตารางล่างเปิดเฉพาะตอนเจออาการนั้น

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

ฝั่งระบบสมองกลฝังตัว
GPIO เอาต์พุตกับอินพุต · แนวคิด active-high / active-low และตัวต้านทาน pull-up · polling loop กับการเลือกคาบเวลาสุ่มตัวอย่าง · contact bounce และการกันเด้ง · ค่าที่อ่านกลับจากขา บอกระดับของขา ไม่ได้บอกความตั้งใจของโปรแกรม

ฝั่ง Python และวิทยาการคอมพิวเตอร์
while ที่มีเงื่อนไขจบ · ตัวดำเนินการมอดุโล % สำหรับการวนเป็นวง · state machine ขนาดเล็กด้วยตัวแปรสามตัว (last_raw, stable, count) · edge detection เทียบกับ level detection

ฝั่งการออกแบบระบบ
แยกค่าที่ปรับได้ไว้บนสุดของไฟล์ · ให้โปรแกรมจำสถานะของตัวเอง ไม่ฝากไว้กับอุปกรณ์ · รายงานเมื่อมีเหตุการณ์ ไม่ใช่รายงานตามเวลา · เก็บกวาดสถานะก่อนจบโปรแกรม

ฮาร์ดแวร์ active-high / active-low pull-up · contact bounce วิทยาการคอมพิวเตอร์ state machine 3 ตัวแปร edge เทียบกับ level · % การออกแบบ จำสถานะของตัวเอง รายงานตามเหตุการณ์ สามรากที่คาบนี้ปักลงไป และจะโผล่อีกทุกคาบที่เหลือ

% ที่ใช้วนไฟทุกดวงวันนี้ คือตัวเดียวกับที่ใช้วน buffer ข้อมูลเซนเซอร์ในคาบ 7

ปุ่มกับไฟขึ้น broker — ภาพทั้งวง

บอร์ดของทีม (examples/s03/07) ปุ่ม ทุก 5 ms กันเด้ง นิ่ง 40 ms ตัวแปร presses, leds publish event, telemetry LED จริง gpio.led(n) ตรวจ n แล้วจด leds[n] get_message หยิบทุกรอบ broker broker.hivemq.com บอร์ด: 1883 เว็บ: wss 8884 ไม่เก็บใบล่าสุด หน้าเว็บ telemetry event ส่ง cmd แถวบนคือของที่ขึ้น แถวล่างคือของที่ลง ทั้งสองแถววิ่งอยู่ในลูปเดียวกัน
topic ทาง ส่งเมื่อไร JSON ตัวอย่าง
bento-aiot/team03/event บอร์ด → เว็บ ทันทีที่กด (ผ่านกันเด้งแล้ว) {"ev":"press","presses":4,"id":"team03","n":17}
bento-aiot/team03/telemetry บอร์ด → เว็บ ทุก 2 วินาที {"presses":4,"btn":0,"leds":[1,0,0],"az":9.79,"id":"team03","n":18}
bento-aiot/team03/cmd เว็บ → บอร์ด ตอนมีคนกดบนหน้าเว็บ {"cmd":"led","n":0,"on":1} · {"cmd":"beep"}

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

แกะ examples/s03/07_button_to_broker.py↗ — สามงานในลูปเดียว

while time.ticks_diff(time.ticks_ms(), t0) < RUN_MS:
    now = time.ticks_ms()
    raw = btn.is_pressed()
    # งานที่ 1: กันเด้งแบบไฟล์ 05
    if raw != last_raw:
        last_raw = raw; last_change = now
    elif raw != stable and time.ticks_diff(now, last_change) >= DEBOUNCE_MS:
        stable = raw
        if stable:                               # ส่งเฉพาะขอบ "เริ่มกด"
            presses = presses + 1
            if not send(TOPIC_EVENT, {"ev": "press", "presses": presses}):
                lost = True; break
    # งานที่ 2: telemetry ตามนาฬิกา ไม่ใช่ตาม sleep
    if time.ticks_diff(now, last_state) >= STATE_MS:
        last_state = now
        state = {"presses": presses, "btn": 1 if stable else 0, "leds": leds}
        if not send(TOPIC_STATE, state):
            lost = True; break
    # งานที่ 3: หยิบคำสั่งทุกรอบ กล่องรับมีช่องเดียว
    msg = mqtt.get_message()
    if msg is not None:
        handle(msg[1])
    if not mqtt.is_connected():
        lost = True; break
    ui.poll()
    time.sleep_ms(POLL_MS)
  • โครงนี้คือลูปเดิมของวันนี้ ที่เพิ่มมามีแค่ send() สองจุดกับ get_message() หนึ่งจุด · ; ใช้บีบบรรทัดบนสไลด์เท่านั้น
  • leds มาจากตัวแปร handle() สั่ง gpio.led(i).on() แล้วจด leds[i] ทันที ไม่มีบรรทัดไหนถามขา
  • send() ดักสองทาง: คืน False = ใบนั้นหาย สายยังอยู่ · โยน OSError = สายหลุด ออกจากลูป ดับไฟ บอกบนจอ

สี่เรื่องของเฟิร์มแวร์ตัวนี้ — โค้ดเราจึงหน้าตาแบบนี้

สิ่งที่เฟิร์มแวร์ทำ ที่มาในซอร์ส ถ้าไม่รู้จะเจออะไร 07 รับมืออย่างไร
publish() ส่งแบบ retain = false เสมอ แม้ docstring จะเขียนว่ารับ retain= modmqtt.c:316 เปิดหน้าเว็บทีหลังแล้วจอว่าง เพราะ broker ไม่ได้เก็บใบล่าสุดไว้ให้ใคร ส่ง telemetry ซ้ำทุก 2 วินาที หน้าเว็บที่เปิดช้าที่สุดก็ตามทันภายใน 2 วินาที
get_message() เป็นกล่องช่องเดียว ใบใหม่ทับใบเก่า modmqtt.c:80-98 ตอนรับ · :385-400 ตอนหยิบ กดปุ่มบนหน้าเว็บรัว ๆ สามครั้ง บอร์ดเห็นใบเดียว หยิบทุกรอบลูป รอบละ 5 ms ไม่หลับยาวที่ไหนเลย
publish() ตอนสายหลุดโยน OSError ไม่ได้คืน False modmqtt.c:292-294 โปรแกรมพังกลางคาบพร้อม traceback send() ดัก OSError แล้วคืน False ให้ลูปหยุดอย่างสุภาพ
ไม่ต่อใหม่ให้เอง และ clean_session เป็นจริงเสมอ (broker ไม่จำว่าเราเคยขอฟังอะไร) modmqtt.c:256 · ไม่มีโค้ดต่อใหม่ในโมดูล สายหลุดแล้วบอร์ดเงียบ คนดูคิดว่าไม่มีใครกดปุ่ม ถาม is_connected() ทุกรอบ หลุดเมื่อไรขึ้นแดงบนจอ แล้วให้รันใหม่

อีกหนึ่งกฎที่ทั้งห้องต้องรู้: client_id ซ้ำกันไม่ได้ บน broker ตัวเดียวกัน ใครต่อทีหลังจะเตะคนก่อนหลุด (ทดลองแล้วบน broker สาธารณะทั้งสองตัว) ไฟล์ 07 ใช้ "bento-aiot-" + TEAM จึงรันได้บอร์ดเดียวต่อทีม ส่วนหน้าเว็บสุ่มชื่อ web-... เอง เปิดกี่แท็บก็ไม่เตะบอร์ด

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

เปิดหน้าเว็บของทีม — examples/web/my_first_reader.html

ไม่ต้องติดตั้งอะไรเลย ใช้แค่เบราว์เซอร์ (มือถือก็ได้)

  1. เปิดลิงก์นี้ เปลี่ยน team05 ท้ายลิงก์เป็นเลขทีมของคุณ ไม่ต้องบันทึกหรือแก้ไฟล์

https://advance-innovation-centre-aic.github.io/embedded-systems-for-aiot-developer/examples/web/my_first_reader.html?team=team05

// ทางเลือก: ดาวน์โหลดไฟล์ไปเปิดเอง แล้วแก้บรรทัดนี้แทนการต่อท้ายลิงก์
let TEAM = "teamXX";          // ทีมของคุณ ตามที่อาจารย์แจก เช่น "team05"
  1. บรรทัดสถานะบนหน้าเว็บต้องขึ้น "ต่อแล้ว (bento-aiot/team05)" ตามเลขทีมของคุณ
  2. รัน examples/s03/07_button_to_broker.py↗ บนบอร์ด ภายใน 2 วินาทีกล่องค่าจะขึ้น presses btn leds (และ az ถ้าอ่าน IMU ได้)
  3. กดปุ่มบนบอร์ด ช่อง "เหตุการณ์ล่าสุด" เปลี่ยนทันที · กด "เปิดไฟ LED 0" บนหน้าเว็บ leds ใบถัดไปเปลี่ยนตาม (บน Dev Kit ดวง 0 อยู่บนโมดูล อาจมองไม่เห็นหลอด แต่ leds บนหน้าเว็บยังเปลี่ยนให้เห็น · อยากสั่งดวง 2 ซึ่งเป็นดวง RGB มีทั้งสองบอร์ด ให้ใช้ไฟล์ที่ดาวน์โหลดแล้วแก้ปุ่มเป็น n: 2)

หน้าเว็บของทั้งห้องคือ examples/web/mqtt_dashboard.html ผู้สอนฉายขึ้นจอ ทุกทีมเป็นการ์ดหนึ่งใบ https://advance-innovation-centre-aic.github.io/embedded-systems-for-aiot-developer/examples/web/mqtt_dashboard.html

สมาชิกที่นั่ง Emulator — Emulator ต่อ broker จริงได้แล้ว รันไฟล์ 07 บน Emulator ด้วย TEAM ของทีมก็ขึ้นหน้าเว็บเดียวกัน (Emulator เติม -emu ท้าย client_id จึงไม่เตะบอร์ดจริง) หรือเปิดหน้าเว็บนี้ดูและสั่งบอร์ดของทีมจากที่นั่งตัวเองก็ได้

ต่อไม่ติด: เปลี่ยนทั้งสองฝั่งไปตัวสำรอง test.mosquitto.org (บอร์ด 1883 · เว็บ wss://test.mosquitto.org:8081/mqtt) · เครือข่ายของมหาวิทยาลัยยังไม่เคยทดสอบ อาจปิดพอร์ตใดพอร์ตหนึ่งไว้ · ห้าม subscribe # บน broker สาธารณะ ฟังเฉพาะ bento-aiot/<ทีม>/#

งานประยุกต์คาบ 2 + 3 (ไม่บังคับ) — ส่งของของทีมขึ้น broker แล้วให้หน้าเว็บแสดง

โจทย์ เลือกค่าหนึ่งตัวที่ทีมอยากให้คนนอกห้องเห็น แล้วทำให้มันไปถึงหน้าเว็บ

  • เลือกของ ค่าจากเซนเซอร์ (az มีให้แล้ว ลองแกนอื่นหรือเซนเซอร์อื่น) หรือสถานะ GPIO (ปุ่มค้างนานแค่ไหน · ดวงไหนติดอยู่ · กดยาวหรือกดสั้น)
  • ส่ง เพิ่ม key สั้น ตัวเล็ก ลงใน state ของไฟล์ 07 ไปกับ telemetry ทุก 2 วินาที ถ้าเป็นเหตุการณ์ที่เกิดทีเดียว ส่งเป็น event แทน
  • อ่าน หน้าเว็บวาดกล่องให้ทุก key อยู่แล้ว งานของทีมคือแก้ my_first_reader.html ให้ตอบสนองค่านั้น เช่น เปลี่ยนสีเมื่อเกินเกณฑ์ หรือเพิ่มปุ่มสั่งไฟดวงอื่น
  • กลับทาง (ไม่บังคับ) เพิ่มคำสั่งใหม่ใน handle() แล้วเพิ่มปุ่มบนหน้าเว็บที่ส่งมัน

ทำครบเมื่อ (การบ้านหรือทำเมื่อเสร็จก่อนเวลา ไม่อยู่ในเกณฑ์ผ่านและไม่อยู่ในคะแนน)

  • [ ] telemetry ของทีมขึ้นทุก 2 วินาที มี id n และค่าที่ทีมเลือกอย่างน้อยหนึ่งตัว
  • [ ] กดปุ่มบนบอร์ดสิบครั้ง หน้าเว็บเห็น presses เพิ่มสิบพอดี
  • [ ] สั่งไฟดวง 2 จากหน้าเว็บแล้วไฟบนบอร์ดติด และ leds ใน telemetry ใบถัดไปเปลี่ยนตาม
  • [ ] หน้าเว็บของทีมตอบสนองค่าที่เลือกอย่างน้อยหนึ่งอย่าง (แก้เอง ไม่ใช่แค่แสดงดิบ)
  • [ ] อธิบายได้ว่าทำไมต้องกันเด้งก่อน publish และทำไมส่ง leds จากตัวแปร ไม่ใช่จากขา

บันทึกลงใบงานข้อ 4.3 · ถ้าเจอพฤติกรรมที่ไม่ตรงกับสไลด์ จดไว้แล้วบอกผู้สอน นั่นคือผลการวัด ไม่ใช่ความผิดของทีม

เกมกดเร็วทั้งห้อง — จับเวลาที่ไหนถึงยุติธรรม

บอร์ด: examples/s03/08_class_race.py↗ (แก้ WiFi กับ TEAM จาก teamXX เหมือนไฟล์ 07 · เปิดเกมไว้ 45 นาที) · จอหน้าห้อง: examples/web/class_game.html ผู้สอนเปิดลิงก์นี้ฉายขึ้นจอ https://advance-innovation-centre-aic.github.io/embedded-systems-for-aiot-developer/examples/web/class_game.html

  1. ผู้สอนกด เริ่มรอบ คำสั่งใบเดียวไปถึงทุกบอร์ดทาง bento-aiot/all/cmd
  2. บอร์ดแต่ละทีมสุ่มรอเอง 1.5 ถึง 4 วินาที จอขึ้น "เตรียม... อย่าเพิ่งกด" พร้อมเสียงต่ำ
  3. ไฟทุกดวงติด จอขึ้น "กดเลย" กดปุ่มให้เร็วที่สุด บอร์ดจับเวลาจากไฟติดถึงนิ้วกด แล้วส่งผลขึ้น event ของทีม
  4. กดก่อนไฟติด หรือเร็วกว่า 100 ms = ออกตัวก่อน (เกณฑ์ออกตัวของกรีฑาโลก เป็นกติกา ไม่ใช่กฎธรรมชาติ) · ไฟติดแล้วไม่กดใน 3 วินาที = พลาดรอบ

เปลี่ยนคนนั่งบอร์ดทุกรอบ เล่นหกรอบ ทีมสี่คนได้นั่งบอร์ดครบทุกคนภายในสี่รอบแรก · ระหว่างรอบ ทั้งห้องช่วยกันกดให้ตัวนับรวมบนจอหน้าห้องถึง 300

ทายก่อน — ทีมที่ผลขึ้นจอหน้าห้องก่อน คือทีมที่เร็วที่สุด ☐ ใช่ ☐ ไม่ใช่ เพราะ ________ (เขียนลงใบงานข้อ 4.4 ก่อนรอบแรก)

ระหว่างรอบ ใครส่ง led หรือ beep เข้าบอร์ดเพื่อแกล้งทีมอื่นก็ไม่ได้ผล ไฟล์ 08 ปฏิเสธเองอยู่แล้ว ลองหาดูว่าบรรทัดไหน · คนที่นั่ง Emulator กดปุ่มตอนว่างเพื่อเล่นรอบซ้อมได้ แต่ Emulator ไม่ได้ยินคำสั่งจากหน้าห้อง (ไฟล์นี้ยังไม่เคยลองบน Emulator)

ทำไมผลที่มาถึงก่อน ไม่ใช่ผลที่เร็วที่สุด

ผู้สอนกดเริ่มรอบพร้อมกัน แต่สองทีมรอไม่เท่ากัน (ตัวเลขสมมติ) ทีม A สุ่มรอ 3.6 วินาที 180 ms ถึงที่ 2 ทีม B สุ่มรอ 1.9 วินาที 290 ms ถึงที่ 1 ไฟติดถึงนิ้วกด (บอร์ดวัดเฉพาะช่วงนี้) ข้อความเดินทางไปถึงจอหน้าห้อง

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

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

กันเด้ง 40 ms ทำให้บอร์ด "เชื่อ" ช้ากว่านิ้ว 40 ms ไฟล์ 08 จึงจดเวลาตอนขาเริ่มเปลี่ยน แล้วใช้เวลานั้นเมื่อกันเด้งยืนยันแล้ว ลองข้อ 2 ในบล็อก "ตาคุณ" ท้ายไฟล์แล้ววัดเอง

หน้าเว็บไม่นับผลที่มาถึงเร็วเกินกว่าที่เป็นไปได้ (เร็วกว่าเวลารอขั้นต่ำบวกเวลาที่อ้าง) แต่คนที่ปลอมผลแล้วรอนานพอ หน้าเว็บจับไม่ได้ เพราะใครก็ส่งเข้าหัวข้อของทีมไหนก็ได้บน broker สาธารณะ คาบ 10 กับ 11 เราจะปิดช่องนี้

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

วันนี้เราได้:
สั่ง LED จริงด้วยโค้ดของเราเองเป็นครั้งแรก · อ่านปุ่มจริงด้วย polling และเข้าใจว่าทำไมต้องกันเด้ง · แยกออกระหว่าง sleep_ms กับ ticks_diff และรู้ว่าเมื่อไรควรใช้อันไหน · เห็นกับดักชื่อของเฟิร์มแวร์กับตาตัวเอง และรู้ว่าทำไมปุ่มถึงชื่อ USER Button 1 · (Eva Kit) เห็น IPC ทำงานผ่านหน้า Controls โดยไม่ต้องเขียนโค้ดเพิ่ม

การบ้านของทีม: เลือกทำ 1 ข้อจากสี่ข้อในสไลด์ "ต่อยอด — คิดต่อเอง" บันทึกลง worksheet ข้อ 8

คาบหน้า: ปุ่มจริงหนึ่งปุ่มมันน้อยไป เราจะสร้างปุ่มบนจอสัมผัสเองด้วย ui.Button และ ui.Switch แล้วเอามาคุม LED สามสี (แดง เขียว น้ำเงิน) ของบอร์ด — พร้อมกฎเหล็กห้าข้อของการใช้ ui · และปุ่มบนจอพวกนั้นจะ publish ขึ้น bento-aiot/<ทีม>/event แบบเดียวกับปุ่มจริงวันนี้ ส่วนคำสั่งจากหน้าเว็บจะขยับ widget บนจอได้

วันนี้ · อินพุตหนึ่งตัว ปุ่มจริง 1 ปุ่ม → LED ทุกดวง คาบ 4 · อินพุตบนจอ ui.Button ×3 + Switch → LED สามสี โครงลูปไม่เปลี่ยน เปลี่ยนแค่ว่าอินพุตมาจากไหน

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

เฉลย s03_led_button.py↗ — ส่วนที่หนึ่ง: ค่าคงที่

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

import gpio
import lcd
import time
import ui

STEP_MS = 150          # จังหวะไฟวิ่ง ปรับตรงนี้เพื่อเปลี่ยนความเร็ว
DEBOUNCE_MS = 40       # เวลาที่ปุ่มต้องนิ่งก่อนเราจะเชื่อ
POLL_MS = 5            # ความถี่ที่ลูปถามปุ่ม
RUN_MS = 30000         # อายุของโปรแกรมรอบนี้

NUM_LEDS = gpio.num_leds()
btn = gpio.button(0)
ค่าคงที่สี่ตัว = หน้าปัดของโปรแกรม ปรับได้โดยไม่ต้องอ่านตรรกะข้างล่างเลย STEP_MS = 150 ความเร็วไฟวิ่ง DEBOUNCE_MS = 40 ความอดทนก่อนเชื่อ POLL_MS = 5 ความถี่ที่ถามปุ่ม RUN_MS = 30000 อายุของโปรแกรม

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

NUM_LEDS = gpio.num_leds() ดีกว่าเขียน 3 ตรง ๆ เพราะย้ายไป Dev Kit ที่มีไฟห้าดวง โค้ดนี้วิ่งครบทุกดวงเองโดยไม่ต้องแก้ — ในห้องนี้มีทั้งสองบอร์ด ไฟล์เดียวกันจึงต้องถูกทั้งคู่

เฉลย — ส่วนที่สอง (1/2): รายงาน ดับไฟ แล้วเตรียมตัวแปร

ตั้งต้น — รายงานสิ่งที่รู้ ดับไฟให้หมด ประกาศตัวแปรสถานะ แล้วล้างจอ (ตัดจาก solution_codes/s03_led_button.py↗ ตรง ๆ)

info = gpio.board_info()
lcd.clear()
lcd.console("<h2>AIoT in Action - คาบ 3</h2>")
lcd.print("บอร์ด:", info["name"], "| LED:", info["leds"], "| ปุ่ม:", info["buttons"])
...
lcd.print("<span class=muted>ปุ่มผู้ใช้มีตัวเดียว ดัชนี 0</span>")
lcd.print("<span class=muted>โค้ดเรียกมันว่า " + btn.name() + "</span>")
...
for i in range(NUM_LEDS):
    gpio.led(i).off()
...
led_index = 0          # ตอนนี้ไฟดวงไหนกำลังติด
count = 0              # จำนวนครั้งที่กดปุ่ม
raw = False            # ค่าดิบของปุ่มรอบนี้
last_raw = False       # ค่าดิบของปุ่มรอบก่อน
stable = False         # ค่าปุ่มที่ผ่านการกันเด้งแล้ว
...
COL_TEXT, COL_DIM = 0xE8EAED, 0x9AA3AF
COL_CARD, COL_OK, COL_RUN = 0x171B22, 0x30A46C, 0x4A9EFF
UI_MS = 100            # จอถูกอัปเดตทุก 100 ms ไม่ใช่ทุกรอบลูป
...
ui.screen()
time.sleep_ms(200)
ui.Label("แผงคุมไฟวิ่ง - คาบ 3", x=24, y=8, color=COL_TEXT, value=28)
lbl_status = ui.Label("ไฟวิ่งกำลังเดิน", x=360, y=12, color=COL_DIM, value=20)

สองบรรทัด lcd.print ของชื่อปุ่มจงใจแยกกัน — รวมเป็นบรรทัดเดียวยาว 163 ไบต์ เกินเพดาน 127 แล้วถูกตัดทิ้งเงียบ ๆ · ชื่อปุ่มเอาจาก btn.name() ไม่ใช่จากป้ายบนแผ่นวงจร ซึ่งบอร์ดสองรุ่นพิมพ์ต่างกัน

led_index ต้องจำเอง เพราะ gpio.led(n).value() ตอบระดับของขา ณ วินาทีที่ถาม ไม่ใช่สิ่งที่เราสั่ง · ห้าสีบนสุดคือจานสีของหลักสูตร (SPEC §S7.13) · UI_MS = 100 คือจังหวะเขียนจอ ไม่ใช่จังหวะลูป — ลูปถามปุ่มทุก 5 ms แต่จอไม่ต้องรู้ทุกครั้ง

ค่าที่ต้องปรับบ่อยอยู่บนสุด ตรรกะอยู่ล่าง — เกณฑ์ "ปรับจังหวะได้" จึงเหลือแค่แก้ STEP_MS ตัวเดียวในส่วนที่หนึ่ง

เฉลย — ส่วนที่สอง (2/2): แผงบนจอ กล่องยืนยันที่ซ่อนไว้ และนาฬิกาสี่ตัว

PITCH = min(128, 408 // NUM_LEDS)
LED_W = 48 if PITCH >= 128 else 36
led_ui = []
for i in range(NUM_LEDS):
    x = 40 + i * PITCH
    led_ui.append(ui.Led(x=x, y=104, w=LED_W, h=LED_W, color=COL_OK, value=0))
...
btn_run = ui.Button("เดินไฟวิ่ง", x=40, y=268, w=176, h=88, color=0x30A46C, value=20)
btn_stop = ui.Button("หยุดไฟวิ่ง", x=248, y=268, w=176, h=88, color=0x3A4150, value=20)
...
box = ui.MsgBox("ยืนยันหยุด\nไฟทุกดวงจะดับทันที",
                x=48, y=96, w=496, h=160, color=COL_CARD)
box.hide()
...
btn_yes = ui.Button("ยืนยัน", x=568, y=96, w=152, h=88, color=0x3A4150, value=20)
btn_no = ui.Button("ยกเลิก", x=568, y=216, w=152, h=88, color=0x3A4150, value=20)
btn_yes.hide()
btn_no.hide()
...
t0 = time.ticks_ms()
last_step = t0
last_change = t0
last_ui = t0
chase_on = True        # ไฟวิ่งเดินอยู่ไหม - ปุ่มบนจอเป็นคนเปลี่ยนค่านี้
asking = False         # กำลังรอคำตอบจากกล่องยืนยันอยู่ไหม
นาฬิกาสี่ตัว ตั้งต้นจาก ticks_ms() ครั้งเดียว t0 = time.ticks_ms() t0 · อายุโปรแกรม last_step · ไฟวิ่ง last_change · ปุ่ม last_ui · จอ

เวลาสี่ตัวตั้งต้นจาก ticks_ms() ค่าเดียวกัน ถ้าเรียกแยกกัน รอบแรกจะเพี้ยนไปไม่กี่มิลลิวินาที · แผงบนจอสร้าง ครั้งเดียวก่อนเข้าลูป — งบ widget ของคอร์สคือ 32 ต่อหน้า (เพดานเฟิร์มแวร์ 64) และการสร้างซ้ำทุกรอบคือการยิง IPC ทิ้งเปล่า ๆ 200 ครั้งต่อวินาที · PITCH คิดจาก NUM_LEDS — 3 ดวงได้ 128 · 5 ดวงได้ 81 ไฟล์เดียวกันวางพอดีทั้งสองบอร์ด

btn_run กับ btn_stop เป็น คนละปุ่ม โดยตั้งใจ (สูง 88 px ตามระยะนิ้วจริง) ปุ่มสลับปุ่มเดียวบอกไม่ได้ว่าตอนนี้อยู่สถานะไหน · กล่องยืนยันกับปุ่มคำตอบสองปุ่มถูก สร้างแล้วซ่อนไว้ ตั้งแต่ต้น — การสร้างของตอนคนกำลังรอคำตอบ คือการเพิ่มความหน่วงในจังหวะที่แย่ที่สุด · จุดเริ่มเวลาที่ตรงกัน ทำให้ตอนดีบักไม่ต้องสงสัยว่าความคลาดเคลื่อนมาจากไหน

เฉลย — ส่วนที่สาม (1/2): ลูปหลักทั้งก้อน

while time.ticks_diff(time.ticks_ms(), t0) < RUN_MS:
    now = time.ticks_ms()

    if chase_on and time.ticks_diff(now, last_step) >= STEP_MS:
        gpio.led(led_index).off()
        led_index = (led_index + 1) % NUM_LEDS
        gpio.led(led_index).on()
        last_step = now
        for k in range(NUM_LEDS):
            led_ui[k].value(1 if k == led_index else 0)

    raw = btn.is_pressed()
    if raw != last_raw:
        last_raw = raw
        last_change = now
    elif raw != stable and time.ticks_diff(now, last_change) >= DEBOUNCE_MS:
        stable = raw
        if stable:
            count += 1
            lcd.print("กดครั้งที่", count)

    if time.ticks_diff(now, last_ui) >= UI_MS:
        last_ui = now
        led_btn.value(1 if stable else 0)
        left_s = (RUN_MS - time.ticks_diff(now, t0)) // 1000
        if left_s != last_sec:
            last_sec = left_s
            seg_count.text(str(count))
            lbl_left.text(str(left_s) + " วิ")
            bar_left.value(left_s)
        for ev in ui.poll():
            ...        # ปุ่มไหนถูกกด แล้วทำอะไรต่อ - อยู่ในไฟล์เฉลยเต็ม

    time.sleep_ms(POLL_MS)

now = time.ticks_ms() เรียกครั้งเดียวต่อรอบ แล้วใช้ร่วมกันทั้งสามงาน — ถ้าเรียกใหม่ทุกจุด เวลาของงานไฟกับงานปุ่มจะไม่ใช่เวลาเดียวกัน

เฉลย — ส่วนที่สาม (2/2): สี่จังหวะในหนึ่งรอบ

งานที่สามคือ จอ และมันเดินที่ 100 ms ไม่ใช่ 5 ms — ui.poll() ทุกรอบลูปคือการยิง IPC ข้ามคอร์ 200 ครั้งต่อวินาที เพื่อรอนิ้วที่มาถึงวินาทีละครั้ง

if left_s != last_sec: คือกฎ ตัวเลขที่คนต้องอ่าน เขียนใหม่ไม่เกินวินาทีละครั้ง ตัวเลขที่กระพริบสิบครั้งต่อวินาทีอ่านไม่ได้ และไม่มีใครได้ประโยชน์จากมัน ส่วนแถบกับไฟขยับเร็วกว่านั้นได้ เพราะตาอ่านรูปทรงไม่ได้อ่านหลัก

time.sleep_ms(POLL_MS) อยู่ท้ายสุดของลูปเสมอ เป็นจุดเดียวที่โปรแกรมยอมพัก

หนึ่งรอบลูป = สี่จังหวะ วนซ้ำจนครบ RUN_MS now = ticks_ms() อ่านนาฬิกาครั้งเดียว ถึงคิวไฟไหม ครบ 150 ms → ขยับ ปุ่มขยับไหม นิ่งครบ 40 ms → นับ พัก 5 ms คืนซีพียูให้งานอื่น พูดออกมาดัง ๆ ว่า "ถึงเวลาขยับไฟไหม แล้วปุ่มขยับไหม แล้วพัก" — ถ้าพูดได้ แปลว่าเข้าใจแล้ว

อ่านลูปนี้จากบนลงล่างแล้วพูดออกมาดัง ๆ ว่า "ถึงเวลาขยับไฟไหม แล้วปุ่มขยับไหม แล้วพัก" — ถ้าพูดได้ แปลว่าเข้าใจแล้ว

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

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

ท่า 2 ดับให้หมด มาก่อนลูป เพื่อให้ทุกการรันเริ่มจากจุดเดียวกัน ผลการทดลองถึงจะเทียบกันได้

ท่า 3 ไฟวิ่ง มาก่อนปุ่ม เพราะไฟวิ่งเป็นงานที่ ไม่ต้องพึ่งอินพุตจากใคร ถ้าไฟวิ่งได้ แปลว่าโครงลูปกับนาฬิกาถูกแล้ว

ท่า 4 ปุ่ม มาทีหลัง ตอนนี้ถ้าพัง เรารู้แน่ว่าพังที่เรื่องปุ่มหรือการกันเด้ง ไม่ใช่ที่โครงลูป

ท่า 5 ปิดท้าย มาสุดท้าย เพราะมันคือคำแถลงว่าทุกอย่างข้างบนผ่านหมดแล้ว

ไล่จากสิ่งที่ยืนได้ด้วยตัวเอง ไปหาสิ่งที่ต้องพึ่งของอื่น 1 2 3 4 5 ถามบอร์ด ดับให้หมด ไฟวิ่ง ปุ่ม + กันเด้ง ปิดท้าย ไม่พึ่งใคร พึ่งท่า 1 พึ่งนาฬิกา พึ่งโครงลูป พึ่งทุกอย่าง เทคนิคนี้ชื่อ bring-up ทีละชั้น — วิศวกรฮาร์ดแวร์ใช้ตอนบอร์ดใหม่มาถึงโต๊ะ

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

เทคนิคนี้ชื่อว่า bring-up ทีละชั้น วิศวกรฮาร์ดแวร์ใช้ตอนบอร์ดใหม่มาถึงโต๊ะ และมันใช้ได้กับซอฟต์แวร์เหมือนกัน

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

คาบ 1-2 · ผ่านมาแล้ว เล่นของที่มีอยู่ พิมพ์ข้อความขึ้นจอ วันนี้ · คาบ 3 ลูปแรกที่สั่งของจริง ไฟ ปุ่ม เวลา คาบ 4-8 อินพุตเยอะขึ้น จอสัมผัส เซนเซอร์ กราฟ คาบ 9-12 ส่งขึ้นเครือข่าย แล้วสร้างของจริง โครงลูป อ่าน-ตัดสิน-สั่ง ของวันนี้ ไม่เปลี่ยนอีกเลยจนจบคอร์ส

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

ใช้จริงที่ไหน — ลูปแบบนี้ทำงานอยู่ตรงไหนบ้าง

โรงงาน · ปุ่มหยุดฉุกเฉิน อ่านปุ่มถี่ ๆ กันเด้งเข้ม แล้วตัดไฟทันที พลาดหนึ่งครั้งคือคนบาดเจ็บ ไม่ใช่แค่ตัวเลขเพี้ยน เหมือนของเรา: polling + debounce + edge detection ยานยนต์ · ไฟเลี้ยวและไฟสถานะ ไฟกะพริบตามจังหวะที่กำหนดเป๊ะ ขณะที่ยังต้องอ่านสวิตช์อื่นได้ตลอด เหมือนของเรา: ticks_diff แทน sleep ยาว เครื่องมือแพทย์ · แผงสถานะ ไฟไล่วนบอกว่าเครื่องยังทำงาน ไม่ได้ค้าง ไฟหยุดวิ่งเมื่อไร คนไข้กับพยาบาลรู้ทันที เหมือนของเรา: chaser คือสัญญาณว่าลูปยังมีชีวิต เครื่องใช้ในบ้าน · ปุ่มกดสั้น-กดยาว ปุ่มเดียวทำสองหน้าที่ วัดจากเวลาที่กดค้าง ลดจำนวนปุ่ม ลดต้นทุน ลดรูบนฝาเครื่อง เหมือนของเรา: ticks_diff ระหว่างขอบเข้า-ขอบออก

ทั้งสี่ช่องใช้โครงโค้ดเดียวกับที่เราเพิ่งเขียน ต่างกันที่ค่าเวลาและสิ่งที่ต่อปลายสาย

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

ทั้งสี่ข้อต่อยอดจากลูปเดิม — ไม่มีข้อไหนต้องเขียนโครงใหม่ เลือกทำหนึ่งข้อ แล้วบันทึกลง worksheet ข้อ 8 1 · ปุ่มเปลี่ยนความเร็ว 400 → 200 → 100 ms 2 · รูปแบบไฟของทีม ไป-กลับ หรือกะพริบพร้อม 3 · กดสั้น กดยาว เส้นแบ่งที่ 800 ms 4 · ทดลองค่ากันเด้ง 0 · 5 · 40 · 200 ms

ข้อ 1 · ปุ่มเปลี่ยนความเร็ว — ให้การกดปุ่มวนเปลี่ยนจังหวะไฟวิ่งระหว่าง 400, 200 และ 100 ms โดยตัวนับยังทำงานถูกต้อง · ใบ้: เก็บความเร็วเป็น list แล้วใช้ % แบบเดียวกับที่ใช้วนไฟ

ข้อ 2 · รูปแบบไฟของทีม — ออกแบบลำดับไฟเป็น list ที่สร้างจาก gpio.num_leds() เช่น ไป-กลับ 0..N-1..1 (Eva 3 ดวงได้ 0,1,2,1 · Dev Kit 5 ดวงได้ 0,1,2,3,4,3,2,1) หรือกะพริบพร้อมกันสลับกับวิ่ง แล้วบันทึกว่าทีมเลือกแบบไหนเพราะอะไร

ข้อ 3 · แยกกดสั้นกับกดยาว — วัดเวลาที่กดค้างด้วย ticks_diff ระหว่างขอบขาเข้ากับขาออก เกิน 800 ms ถือเป็น "กดยาว" แล้วสั่งงานคนละอย่าง

ข้อ 4 · ทดลองค่ากันเด้ง — ตั้ง DEBOUNCE_MS เป็น 0, 5, 40 และ 200 อย่างละรอบ กดค่าละสิบครั้ง จดตัวเลขจริงเป็นตาราง แล้วสรุปว่าค่าไหนเหมาะกับปุ่มบนบอร์ดนี้

อยากหรี่ไฟด้วย ให้ต่อยอดจาก examples/s03/03_led_brightness.py↗ — brightness(pct) ค่ากลางค้างระดับได้บนดวง RGB (มีเส้น PWM ทั้งสองบอร์ด) และไม่บล็อก ส่วนดวงที่ไม่มีเส้นนั้นได้พัลส์ราว 12 ms แล้วดับ ท่าที่ค้างได้ทุกดวงคือ led.hold(pct, ms) ซึ่งย้ำพัลส์จนครบเวลาที่สั่ง (ms ไม่ใส่ได้ ปริยาย 500) แต่จำไว้ว่ามันบล็อกจนครบ ms และจบด้วยหลอดดับ — ถ้าเพิ่งสั่ง brightness() ค่ากลางบนดวง PWM ต้องเรียก off() คั่นก่อน hold() ไม่งั้นขายังถูก PWM ถืออยู่ hold() ไม่เห็นผลและหลอดไม่ดับตอนจบ · ทีมที่ทำข้อนี้ต้องแบ่งเวลาให้ตัวนับยังเดินถูก · และอย่าเผลอใช้ duty() เป็นตัวตรวจ มันคืนเลขที่สั่งไป ไม่ได้วัดหลอด

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

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

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

04 ปุ่มนี้ 0 คือกด ไม่ใช่ 1 · 05 นับการกดให้ตรง ด้วยการรอให้ปุ่มนิ่งก่อน · 06 ปุ่มเดียวคุมไฟทุกดวง ด้วยการจำสถานะเอง · ภาพชุดนี้เก่า ถ่ายตอนไฟล์ 04/06 ยังเรียกปุ่มว่า "BTN0" — ปัจจุบันไฟล์พิมพ์ชื่อจาก btn.name() (= "USER Button 1") รอถ่ายใหม่
ภาพจากตัวจำลอง bento_sim ซึ่งเรนเดอร์ด้วยโค้ด CM55 ชุดเดียวกับที่รันบนบอร์ด ที่ 800x480 เท่าจอของทั้งสองบอร์ด — แสดงหน้าจอที่ตัวอย่างสร้าง ค่าจากเซนเซอร์ WiFi และไมค์เป็นค่าแทนบนเครื่องโฮสต์ ไม่ใช่ผลการวัดของบอร์ด
07 ปุ่มกับไฟขึ้น broker และ 08 เกมกดเร็วทั้งห้อง ยังไม่มีภาพหน้าจอ ไฟล์ทั้งสองเขียนขึ้นคืนก่อนสอน

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

เอกสารของผู้ผลิตและตำรา — KIT_PSE84_EVAL PSOC™ Edge E84 Evaluation Kit guide (Eva Kit), Infineon 002-39007 Rev.*B: §3.2.2.15 User LEDs (หน้า 88–89, รูปที่ 78 ให้ค่า 220/200/2.4 kΩ) · §3.2.2.16 Reset and user buttons (หน้า 89–90, รูปที่ 80 แสดงว่า R411/R408 กับ C230/C229 เป็น DNI คือไม่ได้ลงอุปกรณ์) · Practical Electronics for Inventors, 4th ed. §2.12, §5.3.4, §13.5.1

ปุ่มเด้งและ pull-up — A Guide to Debouncing, Jack Ganssle (วัดสวิตช์จริง 16 ตัว bounce แย่สุด 157 ms, แนะช่วง 20–50 ms) https://www.ganssle.com/debouncing.htm · Pico Technology https://www.picotech.com/library/articles/blog/what-is-switch-bounce-how-to-implement-debounce · Pull-up Resistors, SparkFun (CC BY-SA 4.0) · ภาษาไทย IoT Engineering Education, KMUTNB (CC BY-SA 4.0) https://iot-kmutnb.github.io/blogs/arduino/arduino_avr_io_follower/

วิดีโอที่ตรวจแล้วใน _BUILD/MEDIA_PLAN.md §3 (ความยาวยังไม่ยืนยันทั้งหมวด) — AddOhms #15 pull-up wxjerCHCEMg · DigiKey เลือกตัวต้านทานให้ LED UWx2BEx7xyI · KruNarut (ไทย) U9JYM1VdF5U · DigiKey switch bounce IvU8m_30iK0

เครดิตภาพ วงจร LED และภาพบอร์ด: KIT_PSE84_EVAL user guide รูปที่ 78 (หน้า 89) และรูปที่ 2 (หน้า 9) — ใช้เพื่อการเรียนการสอน · Wikimedia Commons: Tomoldbury (สาธารณสมบัติ) · Super Rad! (CC0 1.0) · ไดอะแกรมที่เหลือวาดขึ้นใหม่สำหรับหลักสูตรนี้ โดยอ้างค่าจากคู่มือบอร์ด
ข้อเท็จจริงเรื่อง `gpio` (ชื่อ RGB_RED บน Eva และ "USER Button 1" กับเหตุผลของมัน, ตาราง PWM ของดวง RGB, `led(2).value()` อ่านกลับได้จริงผ่าน `Cy_GPIO_Read()` แต่หลัง `hold()` ขาถูกทิ้งไว้ต่ำ, `gpio.button(1)` โยน ValueError) ตรวจจากซอร์ส `modgpio.c` โดยตรง

fit-css

VIDEO-SLOT: คลิป 30-45 วินาที ถ่ายจอบอร์ด Eva Kit — แตะการ์ด Controls บนหน้า Home แล้วแตะวงกลมสีทีละวง ให้เห็นหลอด LED จริงบนบอร์ดติด-ดับตามนิ้ว จบด้วยแพนกล้องไปที่หลอด LED บนบอร์ด (Dev Kit ไม่มีการ์ด Controls — ถ้าถ่าย Dev Kit ให้ใช้ examples/s01/11 แทน)

ภาพหน้าจอของ examples/s03/03_led_brightness.py ถอดออกชั่วคราว ไฟล์ถูกเขียนใหม่เมื่อ 14 ส.ค. รอถ่ายใหม่

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

ภาพหน้าจอของ examples/s03/02_led_blink.py ถอดออกชั่วคราว เพราะไฟล์ถูกเขียนใหม่เมื่อ 14 ส.ค. รอถ่ายใหม่

VIDEO-SLOT: คลิป 20-30 วินาที ถ่ายตอนผ่าน MVP — เริ่มที่หลอด LED ทุกดวงบนบอร์ดวิ่งไล่กันเป็นวง (Eva 3 · Dev Kit 5) แล้วแพนไปที่จอ Playground ตอนนิ้วกดปุ่มผู้ใช้สิบครั้งให้เห็นตัวเลข "กดครั้งที่ 1..10" ขึ้นทีละบรรทัดพอดี ปิดท้ายด้วยไฟดับหมดพร้อมบรรทัดสรุปสีเขียว ใช้เป็นตัวอย่าง "หน้าตาของคำว่าผ่าน" ให้ทุกทีมเทียบ

☰ สารบัญ