ทบทวนสั้น ๆ ก่อนลืม เพราะท้ายคาบเราต้องใช้ทุกขั้น
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 เห็นไฟติดจากโค้ดที่เราเขียนให้ · คาบนี้เป็นคนเขียนลูปที่สั่งมันเอง
gpio.led(n) ได้ทั้ง เปิด ปิด สลับสถานะ และสั่งด้วยตัวเลขที่คำนวณได้gpio.button(0).is_pressed() แล้วบอกได้ว่า polling คืออะไรtime.sleep_ms() และ time.ticks_ms()/ticks_diff() เป็นgpio มีอะไรอยู่ทั้งโมดูล และตัวไหนตอบ "สิ่งที่วัดได้" ตัวไหนตอบแค่ "สิ่งที่เราสั่งไป"ปลายทางของวันนี้: ไฟทุกดวงบนบอร์ดวิ่งไล่กันตามจังหวะที่ทีมตั้งเอง และทุกครั้งที่กดปุ่มบนบอร์ด ตัวเลขบนจอเพิ่มขึ้นหนึ่ง
คาบ 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
ทุกคาบต่อจากนี้จะมีลูปหลักเป็นแกน — วันนี้คือลูปแรกของเรา
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


.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ทิ้งหลอดติดค้าง · มองหลอดจริงเป็นคนตัดสิน
สไลด์นี้ไม่ได้ให้ท่อง แต่ให้ รู้ว่าอะไรมีอยู่ จะได้ไม่ไปเขียนของที่ไม่มี แล้วนั่งงงว่าทำไม 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() ไม่เขียน มันจึงเก่าได้ |
เมธอดของ 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' บนทั้งสองบอร์ด — สองชื่อนี้ไม่ตรงกับสิ่งที่ตาเห็นหรือป้ายที่พิมพ์ไว้ คนละเหตุผลกัน

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

is_pressed() กลับด้านให้แล้ว กดคืน True ส่วน button(0).value() คืนไฟฟ้าดิบ กดได้ 0 · ภาพหน้าจอของ examples/s03/04_button_active_low.py↗ ถอดออกชั่วคราว ไฟล์ถูกลดจำนวน widget ลงเมื่อ 14 ส.ค. รอถ่ายใหม่ขาชิปสั่ง "ประตู" ไม่ได้สั่ง "น้ำ" — และเพราะประตูเปิด-ปิดได้เร็วมาก
brightness()จึงหรี่ไฟได้ด้วยการกระพริบ gate ถี่ ๆ ไม่ใช่ด้วยการลดแรงดัน

ทำไมน้ำเงินใช้ 2.4 kΩ ทั้งที่แดงใช้ 220 Ω — ของ LED น้ำเงินสูงกว่า ลองแทนเลขในสูตรเดียวกันแล้วเทียบดู
โปรแกรมของเราไม่มีทางรู้เองว่ามีคนกดปุ่ม สิ่งที่ทำได้คือ ถามซ้ำ ๆ ให้ถี่พอ
while True:
pressed = btn.is_pressed() # ถาม
# ...ตัดสินใจจากค่าที่ได้...
time.sleep_ms(5) # พักหายใจ แล้วถามใหม่
รอบหนึ่งของลูปมีสามจังหวะเสมอ: อ่านของเข้า → ตัดสิน → สั่งของออก วนแบบนี้ไปเรื่อย ๆ
ถามถี่แค่ไหนถึงพอ นิ้วคนกดปุ่มเร็วสุดราว 50-80 ms ต่อครั้ง ถ้าเราถามทุก 5-20 ms ก็ไม่มีทางพลาด แต่ถ้าถามทุก 300 ms จะมีการกดที่หลุดหายไปเงียบ ๆ
time.sleep_ms(5) ในลูปไม่ได้มีไว้ถ่วงเวลาเล่น ๆ — มันคือการคืนซีพียูให้งานอื่น (จอ เซนเซอร์ WiFi) ได้ทำงานด้วย ลูปที่ไม่มีการพักเลยจะกินเครื่องจนของอื่นกระตุก
ลูปที่ดีไม่ใช่ลูปที่เร็วที่สุด แต่คือลูปที่ เร็วพอ และเหลือที่ให้คนอื่นหายใจ
ข้างในปุ่มคือแผ่นโลหะสองชิ้นที่ถูกดันให้ชนกัน โลหะมีความยืดหยุ่น พอชนแล้วมัน กระเด้งแยกออกแล้วชนใหม่ อีกหลายรอบก่อนจะนิ่ง กินเวลาราว 1-20 ms · ตาเราไม่เห็น แต่ลูปที่ถามทุก 5 ms เห็นครบทุกจังหวะ
ถ้านับทุกครั้งที่เห็นการเปลี่ยน ตัวเลขจะกระโดดทีละ 3-5 ทั้งที่นิ้วกดครั้งเดียว · ภาพล่างคือ ของจริง จากออสซิลโลสโคป

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: เพราะเราต้องการนับ ตอนกดลง ไม่ใช่ตอนปล่อย
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 จัดการเรื่องนี้ให้แล้ว
ลูปของเราจึงเป็นแบบนี้: หลับสั้น ๆ 5 ms ทุกรอบ (ปุ่มไม่หลุด) แล้วใช้ ticks_diff ตัดสินว่าถึงคิวขยับไฟหรือยัง
งานสองอย่างที่จังหวะไม่เท่ากัน อยู่ในลูปเดียวกันได้ ถ้าเลิกใช้
sleepเป็นตัวจับเวลา
ตู้ชุมสายโทรศัพท์ยุค 1930 ใช้รีเลย์เป็นพัน ๆ ตัว วิศวกรสมัยนั้นเจอปัญหาเดียวกับเรา — หน้าสัมผัสปิดหนึ่งครั้ง แต่วงจรนับได้หลายครั้ง แล้วสายต่อผิดเลขหมาย · ทางแก้ยุคนั้นคือฮาร์ดแวร์ ใส่ตัวเก็บประจุกับตัวต้านทานให้สัญญาณนิ่งก่อนเข้าวงจรนับ ซึ่งก็คือการรอให้คงที่ เหมือนที่เราทำด้วยโค้ดสี่บรรทัดวันนี้
เรื่องเฉพาะของ Eva Kit: วงจรปุ่มผู้ใช้ (ป้าย SW2/SW4 บนแผ่นวงจร) มีที่ว่างไว้ให้ใส่ตัวต้านทาน 10 kΩ และตัวเก็บประจุ 0.1 µF แต่คู่มือ ระบุว่าไม่ได้ลงอุปกรณ์จริง (DNI) — แปลว่าบอร์ดนี้ไม่มีวงจรกันเด้งแบบฮาร์ดแวร์เลย ต้องพึ่ง pull-up ในตัวชิปกับโค้ดของเราล้วน ๆ · Dev Kit ยังไม่ได้เปิดวงจรตรวจ — ให้ผลการทดลองข้อ 4 ของทีมเป็นคนตอบว่าปุ่มของบอร์ดนั้นเด้งแค่ไหน
เชื่อมกับวันนี้: บรรทัด DEBOUNCE_MS = 40 คือค่าที่เมื่อ 90 ปีก่อนต้องเปลี่ยนตัวเก็บประจุถึงจะปรับได้ วันนี้พิมพ์เลขใหม่แล้วกด Program to Device
สิ่งที่เฟิร์มแวร์ทำให้แล้ว (70%)
ตั้งค่าขา GPIO ให้เป็นเอาต์พุต/อินพุตพร้อม pull-up · แปลงระดับไฟฟ้าเป็น True/False ให้ · ส่งสถานะ LED ข้าม IPC ไปให้จอวาดตาม · จัดการนาฬิกาของระบบให้ ticks_ms() ใช้ได้
สิ่งที่เป็นงานของเรา (30%)
ออกแบบว่า ลูปหนึ่งรอบทำอะไรบ้าง · ตัดสินว่า เมื่อไรถึงจะเชื่อค่าที่อ่านได้ · เลือกจังหวะเวลาที่คนใช้รู้สึกดี
สังเกตว่า 30% ของเราคราวนี้ไม่ใช่ไวยากรณ์ Python เลย แต่เป็น การตัดสินใจเชิงออกแบบ ทั้งหมด
คนที่เขียนไดรเวอร์ GPIO เป็น มีเยอะ · คนที่ออกแบบลูปให้ผู้ใช้รู้สึกว่า "ปุ่มมันตอบสนองดี" มีน้อยกว่ามาก
# --- ท่าที่ 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" คือสวิตช์ตัดไฟบนฐาน) เจอครั้งเดียวแล้วจะไม่ลืม
เริ่มโปรแกรมด้วยการ รายงานสิ่งที่เรารู้เกี่ยวกับฮาร์ดแวร์ ทำให้ตอนดีบักไม่ต้องเดา
# --- ท่าที่ 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 · สองทางนี้ผิดคนละแบบ และทั้งคู่ชี้ไปที่คำตอบเดียวกัน
บทเรียนที่ใหญ่กว่ากับดักตัวมันเอง: โปรแกรมควรจำสถานะที่ตัวเองสั่งไว้เสมอ — หลอดชุดนี้อ่านกลับได้ แต่เอาต์พุตอีกหลายชนิดในโลกจริงสั่งได้อย่างเดียว (วาล์ว รีเลย์ มอเตอร์) โปรแกรมที่จำสถานะของตัวเองไว้ ย้ายไปคุมของพวกนั้นได้ทันที
# --- ท่าที่ 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)
% 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: อ่านปุ่มทุกรอบ แต่เชื่อเฉพาะค่าที่นิ่งแล้ว ---
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)

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 บรรทัดต่อวินาทีจนอ่านอะไรไม่ได้
ลองเอา
if stable:ออกดูสักครั้ง แล้วจะเห็นเลขกระโดดทีละสอง — เพราะปล่อยปุ่มก็คือการเปลี่ยนสถานะเหมือนกัน
# --- ท่าที่ 5: ดับไฟ แล้วสรุปผลปิดท้าย ---
for i in range(NUM_LEDS):
gpio.led(i).off()
lcd.print("<span class=ok>จบรอบทดสอบ กดปุ่มทั้งหมด " + str(count) + " ครั้ง</span>")
print("โปรแกรมจบแล้ว - ไฟทุกดวงถูกดับเรียบร้อย")
โปรแกรมฝังตัวที่จบแล้วทิ้งไฟติดค้างไว้ ถือว่าจบไม่เรียบร้อย คนถัดไปที่มาใช้บอร์ดจะไม่รู้ว่าไฟที่ติดอยู่มาจากโปรแกรมไหน
str(count) จำเป็นเพราะเรากำลังต่อสตริงด้วย + เพื่อแทรกตัวเลขไว้กลางแท็ก <span> — ถ้าใช้จุลภาคแบบ lcd.print("...", count) จะได้ช่องว่างเกินติดขอบแท็ก แบบเดียวกับที่เจอในคาบ 1
ทำไมโปรแกรมต้องมีวันจบ — ลูปของเราวิ่งตามเวลาที่ตั้งไว้ใน RUN_MS ไม่ใช่ while True เพราะเราอยากให้มันคืนบอร์ดให้ทีมได้ทดลองรอบต่อไปโดยไม่ต้องกด RESTART ทุกครั้ง
เขียนโปรแกรมที่ เก็บของก่อนกลับบ้าน — นิสัยนี้จะช่วยชีวิตตอนโปรเจกต์ใหญ่
gpio.led(n).on()ส่งสถานะข้าม IPC ให้ CM55 ทุกครั้งโดยเราไม่ต้องสั่ง — ของแถมที่ทำให้เห็นภาพ
practise_codes/s03_led_button.py↗ เติมช่องว่าง pass ให้ครบตามคำใบ้ # เติม:รอบพิเศษ — ดู IPC ด้วยตาตัวเอง (Eva Kit เท่านั้น — Dev Kit ไม่มีการ์ด Controls ข้ามรอบนี้ได้)
รันซ้ำอีกครั้ง แต่คราวนี้ก่อนกด Program to Device ให้แตะการ์ด Controls ค้างไว้แทน Playground แล้วมองวงกลมสีบนหน้า Controls ระหว่างที่โปรแกรมของเราวิ่ง
วงกลมบนจอจะติด-ดับตามไฟวิ่งของเรา ทั้งที่โค้ดเราไม่ได้สั่งจออะไรเลยสักบรรทัด — เพราะทุกครั้งที่เราสั่ง LED เฟิร์มแวร์ส่งสถานะข้าม IPC ไปบอก CM55 ให้เอง
ดูรอบพิเศษเสร็จแล้วกลับมาที่หน้า Playground เพื่ออ่านตัวนับ แล้วกด RESTART รันใหม่
โปรแกรมเดียวกัน แต่เปิดคนละหน้า เห็นคนละด้านของระบบ — ลองทั้งสองรอบก่อนสรุปว่าเข้าใจแล้ว
LED chaser ครบทุกดวงของบอร์ด ปรับจังหวะได้ + กดปุ่มผู้ใช้ (เรียกด้วยชื่อจาก .name()) นับครั้งแสดงบน LCD
แปลเป็นสิ่งที่ตรวจได้จริง:
gpio.num_leds() บอก ไม่มีดวงไหนค้างติดหรือข้ามดวง (Dev Kit: ดวง LED1/LED2 อยู่บนโมดูล ถ้ามองไม่เห็นให้ดูดวง RGB สามดวงเป็นหลัก แล้วจดไว้ว่าเห็นกี่ดวง)STEP_MS แล้วรันใหม่ จังหวะไฟเปลี่ยนตามจริง (สาธิตอย่างน้อยสองค่า)gpio.button(0)) สิบครั้ง ตัวเลขบนจอขึ้นสิบพอดีข้อที่ยากที่สุดคือข้อสี่ — ทำได้ไม่ยาก แต่ อธิบายได้ ต้องเข้าใจจริง
| อาการ | สาเหตุที่แท้จริง | วิธีแก้ |
|---|---|---|
| กดหนึ่งครั้ง เลขขึ้น 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
ระวังการเยื้องบรรทัด — สามจุดกลางอยู่คนละชั้นกัน ถ้าเยื้องผิด Python จะเอาโค้ดไปไว้ผิดเงื่อนไข · ลำดับที่แนะนำ: เติมจุด 1-2 ให้ไฟวิ่งก่อน แล้วค่อยจุด 3-5 เรื่องปุ่ม สุดท้ายจุด 6
เติมทีละกลุ่มแล้วรัน — พอไฟวิ่งได้แล้วปุ่มพัง เรารู้ทันทีว่าปัญหาไม่ได้อยู่ที่ไฟ
ต้องทำในคาบ · เปิดตามลำดับนี้ ทั้งชุดราว 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
ฝั่งการออกแบบระบบ
แยกค่าที่ปรับได้ไว้บนสุดของไฟล์ · ให้โปรแกรมจำสถานะของตัวเอง ไม่ฝากไว้กับอุปกรณ์ · รายงานเมื่อมีเหตุการณ์ ไม่ใช่รายงานตามเวลา · เก็บกวาดสถานะก่อนจบโปรแกรม
%ที่ใช้วนไฟทุกดวงวันนี้ คือตัวเดียวกับที่ใช้วน buffer ข้อมูลเซนเซอร์ในคาบ 7
| 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ไม่ต้องติดตั้งอะไรเลย ใช้แค่เบราว์เซอร์ (มือถือก็ได้)
team05 ท้ายลิงก์เป็นเลขทีมของคุณ ไม่ต้องบันทึกหรือแก้ไฟล์// ทางเลือก: ดาวน์โหลดไฟล์ไปเปิดเอง แล้วแก้บรรทัดนี้แทนการต่อท้ายลิงก์
let TEAM = "teamXX"; // ทีมของคุณ ตามที่อาจารย์แจก เช่น "team05"
examples/s03/07_button_to_broker.py↗ บนบอร์ด ภายใน 2 วินาทีกล่องค่าจะขึ้น presses btn leds (และ az ถ้าอ่าน IMU ได้)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/<ทีม>/#
โจทย์ เลือกค่าหนึ่งตัวที่ทีมอยากให้คนนอกห้องเห็น แล้วทำให้มันไปถึงหน้าเว็บ
az มีให้แล้ว ลองแกนอื่นหรือเซนเซอร์อื่น) หรือสถานะ GPIO (ปุ่มค้างนานแค่ไหน · ดวงไหนติดอยู่ · กดยาวหรือกดสั้น)state ของไฟล์ 07 ไปกับ telemetry ทุก 2 วินาที ถ้าเป็นเหตุการณ์ที่เกิดทีเดียว ส่งเป็น event แทนmy_first_reader.html ให้ตอบสนองค่านั้น เช่น เปลี่ยนสีเมื่อเกินเกณฑ์ หรือเพิ่มปุ่มสั่งไฟดวงอื่นhandle() แล้วเพิ่มปุ่มบนหน้าเว็บที่ส่งมันทำครบเมื่อ (การบ้านหรือทำเมื่อเสร็จก่อนเวลา ไม่อยู่ในเกณฑ์ผ่านและไม่อยู่ในคะแนน)
id n และค่าที่ทีมเลือกอย่างน้อยหนึ่งตัวpresses เพิ่มสิบพอดีleds ใน telemetry ใบถัดไปเปลี่ยนตาม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
bento-aiot/all/cmdevent ของทีมเปลี่ยนคนนั่งบอร์ดทุกรอบ เล่นหกรอบ ทีมสี่คนได้นั่งบอร์ดครบทุกคนภายในสี่รอบแรก · ระหว่างรอบ ทั้งห้องช่วยกันกดให้ตัวนับรวมบนจอหน้าห้องถึง 300
ทายก่อน — ทีมที่ผลขึ้นจอหน้าห้องก่อน คือทีมที่เร็วที่สุด ☐ ใช่ ☐ ไม่ใช่ เพราะ ________ (เขียนลงใบงานข้อ 4.4 ก่อนรอบแรก)
ระหว่างรอบ ใครส่ง
ledหรือbeepเข้าบอร์ดเพื่อแกล้งทีมอื่นก็ไม่ได้ผล ไฟล์ 08 ปฏิเสธเองอยู่แล้ว ลองหาดูว่าบรรทัดไหน · คนที่นั่ง Emulator กดปุ่มตอนว่างเพื่อเล่นรอบซ้อมได้ แต่ Emulator ไม่ได้ยินคำสั่งจากหน้าห้อง (ไฟล์นี้ยังไม่เคยลองบน Emulator)
จอหน้าห้องมีสองคอลัมน์ เวลาบนบอร์ด กับ มาถึงลำดับ และจอเรียงอันดับตามเวลาบนบอร์ด ไม่ใช่ตามลำดับที่มาถึง
กันเด้ง 40 ms ทำให้บอร์ด "เชื่อ" ช้ากว่านิ้ว 40 ms ไฟล์ 08 จึงจดเวลาตอนขาเริ่มเปลี่ยน แล้วใช้เวลานั้นเมื่อกันเด้งยืนยันแล้ว ลองข้อ 2 ในบล็อก "ตาคุณ" ท้ายไฟล์แล้ววัดเอง
หน้าเว็บไม่นับผลที่มาถึงเร็วเกินกว่าที่เป็นไปได้ (เร็วกว่าเวลารอขั้นต่ำบวกเวลาที่อ้าง) แต่คนที่ปลอมผลแล้วรอนานพอ หน้าเว็บจับไม่ได้ เพราะใครก็ส่งเข้าหัวข้อของทีมไหนก็ได้บน broker สาธารณะ คาบ 10 กับ 11 เราจะปิดช่องนี้
วันนี้เราได้:
สั่ง 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 บนจอได้
ลูปที่เขียนวันนี้จะกลายเป็นโครงของทุกโปรแกรมในคาบที่เหลือ ต่างกันแค่ว่าอ่านอะไรและสั่งอะไร
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)
ค่าคงที่สี่ตัวบนสุดคือ หน้าปัดของโปรแกรม ทีมปรับได้โดยไม่ต้องอ่านตรรกะข้างล่าง วิธีนี้ทำให้สไลด์ MVP ข้อ "ปรับจังหวะได้" กลายเป็นการแก้เลขตัวเดียว
NUM_LEDS = gpio.num_leds() ดีกว่าเขียน 3 ตรง ๆ เพราะย้ายไป Dev Kit ที่มีไฟห้าดวง โค้ดนี้วิ่งครบทุกดวงเองโดยไม่ต้องแก้ — ในห้องนี้มีทั้งสองบอร์ด ไฟล์เดียวกันจึงต้องถูกทั้งคู่
ตั้งต้น — รายงานสิ่งที่รู้ ดับไฟให้หมด ประกาศตัวแปรสถานะ แล้วล้างจอ (ตัดจาก 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ตัวเดียวในส่วนที่หนึ่ง
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() ค่าเดียวกัน ถ้าเรียกแยกกัน รอบแรกจะเพี้ยนไปไม่กี่มิลลิวินาที · แผงบนจอสร้าง ครั้งเดียวก่อนเข้าลูป — งบ widget ของคอร์สคือ 32 ต่อหน้า (เพดานเฟิร์มแวร์ 64) และการสร้างซ้ำทุกรอบคือการยิง IPC ทิ้งเปล่า ๆ 200 ครั้งต่อวินาที · PITCH คิดจาก NUM_LEDS — 3 ดวงได้ 128 · 5 ดวงได้ 81 ไฟล์เดียวกันวางพอดีทั้งสองบอร์ด
btn_run กับ btn_stop เป็น คนละปุ่ม โดยตั้งใจ (สูง 88 px ตามระยะนิ้วจริง) ปุ่มสลับปุ่มเดียวบอกไม่ได้ว่าตอนนี้อยู่สถานะไหน · กล่องยืนยันกับปุ่มคำตอบสองปุ่มถูก สร้างแล้วซ่อนไว้ ตั้งแต่ต้น — การสร้างของตอนคนกำลังรอคำตอบ คือการเพิ่มความหน่วงในจังหวะที่แย่ที่สุด · จุดเริ่มเวลาที่ตรงกัน ทำให้ตอนดีบักไม่ต้องสงสัยว่าความคลาดเคลื่อนมาจากไหน
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() เรียกครั้งเดียวต่อรอบ แล้วใช้ร่วมกันทั้งสามงาน — ถ้าเรียกใหม่ทุกจุด เวลาของงานไฟกับงานปุ่มจะไม่ใช่เวลาเดียวกัน
งานที่สามคือ จอ และมันเดินที่ 100 ms ไม่ใช่ 5 ms — ui.poll() ทุกรอบลูปคือการยิง IPC ข้ามคอร์ 200 ครั้งต่อวินาที เพื่อรอนิ้วที่มาถึงวินาทีละครั้ง
if left_s != last_sec: คือกฎ ตัวเลขที่คนต้องอ่าน เขียนใหม่ไม่เกินวินาทีละครั้ง ตัวเลขที่กระพริบสิบครั้งต่อวินาทีอ่านไม่ได้ และไม่มีใครได้ประโยชน์จากมัน ส่วนแถบกับไฟขยับเร็วกว่านั้นได้ เพราะตาอ่านรูปทรงไม่ได้อ่านหลัก
time.sleep_ms(POLL_MS) อยู่ท้ายสุดของลูปเสมอ เป็นจุดเดียวที่โปรแกรมยอมพัก
อ่านลูปนี้จากบนลงล่างแล้วพูดออกมาดัง ๆ ว่า "ถึงเวลาขยับไฟไหม แล้วปุ่มขยับไหม แล้วพัก" — ถ้าพูดได้ แปลว่าเข้าใจแล้ว
ท่า 1 ถามบอร์ด มาก่อน เพราะถ้า gpio ใช้ไม่ได้หรือบอร์ดตอบแปลก เรารู้ตั้งแต่บรรทัดแรก ไม่ใช่หลังเขียนไปแล้วแปดสิบบรรทัด
ท่า 2 ดับให้หมด มาก่อนลูป เพื่อให้ทุกการรันเริ่มจากจุดเดียวกัน ผลการทดลองถึงจะเทียบกันได้
ท่า 3 ไฟวิ่ง มาก่อนปุ่ม เพราะไฟวิ่งเป็นงานที่ ไม่ต้องพึ่งอินพุตจากใคร ถ้าไฟวิ่งได้ แปลว่าโครงลูปกับนาฬิกาถูกแล้ว
ท่า 4 ปุ่ม มาทีหลัง ตอนนี้ถ้าพัง เรารู้แน่ว่าพังที่เรื่องปุ่มหรือการกันเด้ง ไม่ใช่ที่โครงลูป
ท่า 5 ปิดท้าย มาสุดท้าย เพราะมันคือคำแถลงว่าทุกอย่างข้างบนผ่านหมดแล้ว
หลักการเดียวกับคาบ 1: ไล่จากสิ่งที่ยืนได้ด้วยตัวเอง ไปหาสิ่งที่ต้องพึ่งของอื่น แล้วให้แต่ละขั้นยืนยันขั้นก่อนหน้า
เทคนิคนี้ชื่อว่า bring-up ทีละชั้น วิศวกรฮาร์ดแวร์ใช้ตอนบอร์ดใหม่มาถึงโต๊ะ และมันใช้ได้กับซอฟต์แวร์เหมือนกัน
คำถามคิดต่อ: ถ้าเปลี่ยนจาก "อ่านปุ่ม" เป็น "อ่านความเร่งจาก IMU" ลูปนี้ต้องแก้กี่บรรทัด · การกันเด้งของปุ่ม กับการกรองสัญญาณเซนเซอร์ที่จะเจอคาบ 5 เหมือนกันตรงไหน · ถ้าต้องคุมของ 20 ชิ้นพร้อมกัน โครงแบบนี้ยังไหวไหม
ทั้งสี่ช่องใช้โครงโค้ดเดียวกับที่เราเพิ่งเขียน ต่างกันที่ค่าเวลาและสิ่งที่ต่อปลายสาย
ข้อ 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()เป็นตัวตรวจ มันคืนเลขที่สั่งไป ไม่ได้วัดหลอด

brightness(40) ก่อน รอถ่ายใหม่

btn.name() (= "USER Button 1") รอถ่ายใหม่เอกสารของผู้ผลิตและตำรา — 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
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" ขึ้นทีละบรรทัดพอดี ปิดท้ายด้วยไฟดับหมดพร้อมบรรทัดสรุปสีเขียว ใช้เป็นตัวอย่าง "หน้าตาของคำว่าผ่าน" ให้ทุกทีมเทียบ