ภาพพื้นหลังปกบทเรียน

บทเรียน 2.2 — หลังไฟและปุ่ม: active-low กันเด้ง และลูปที่ไม่หยุด

สั่งฮาร์ดแวร์ด้วยโค้ดของเราเอง · LED ทุกดวงบนบอร์ด ปุ่มหนึ่งปุ่ม และลูปที่ไม่มีวันหยุด

โมดูล 2 — จากจอสู่ฮาร์ดแวร์

ต่อจากบทเรียน 2.1 — โมดูล gpio: LED ปุ่ม และบอร์ดที่บอกได้ว่ามีอะไร

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

เข้าใจฮาร์ดแวร์ · ทำไมไฟ "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 = ติด

ผังวงจร LED ผู้ใช้บนบอร์ด Eva Kit ที่ขับผ่านทรานซิสเตอร์ สั่ง 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 · ภาพหน้าจอของ 04_button_active_low.py ถอดออกชั่วคราว ไฟล์ถูกลดจำนวน widget ลงเมื่อ 14 ส.ค. รอถ่ายใหม่

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

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

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

ดัดแปลงจาก 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}

ภาพเคลื่อนไหว MOSFET ชนิด n-channel enhancement ที่นำกระแสเมื่อแรงดัน gate สูงพอ
ภาพ: 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 น้ำเงินราว 3.0 V) ดวงน้ำเงินได้กระแสกี่ mA และทำไมผู้ออกแบบยอมให้กระแสต่ำขนาดนั้น · ระวัง: VFV_F สูงกว่าที่กระแสเท่ากัน ต้องใช้ R น้อยกว่า ไม่ใช่มากกว่า

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

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) ได้ทำงานด้วย ลูปที่ไม่มีการพักเลยจะกินเครื่องจนของอื่นกระตุก

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

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

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

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

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

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

ภาพจอออสซิลโลสโคปของสัญญาณสวิตช์ที่เด้งเป็นหนามหลายครั้งตอนกด

ภาพ: Tomoldbury / Wikimedia Commons — สาธารณสมบัติ · หนามที่เห็นคือหน้าสัมผัสโลหะที่ยังเด้งอยู่จริง ไม่ใช่สัญญาณรบกวนจากที่อื่น · ภาพหน้าจอของ 05_debounce_count.py ถอดออกชั่วคราว ไฟล์ถูกลดจำนวน widget ลงเมื่อ 14 ส.ค. รอถ่ายใหม่ — ตอนรันจริงจะเห็นเลขสองตัววิ่งคู่กัน ส้มคือนับดิบ เขียวคือนับกันเด้ง
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.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                    # นิ่งครบ 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 จะรู้สึกว่าปุ่มหน่วง

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

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

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

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

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 เป็นตัวจับเวลา

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

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

ตู้ชุมสายโทรศัพท์ยุค 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

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

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

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

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

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

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

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

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

แกะโค้ดจริง — ท่าที่ 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 - บทเรียน 2.1–2.3 บอร์ด: PSoC Edge Eval Kit | LED: 3 | ปุ่ม: 1 (Dev Kit: PSoC Edge AI Dev Kit | LED: 5) ปุ่มผู้ใช้มีตัวเดียว ดัชนี 0 · โค้ดเรียกมันว่า USER Button 1

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

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

แกะโค้ดจริง — ท่าที่ 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.1–1.3

ทำไมต้องมีตัวแปร 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 ถูกเสมอ เพราะเราเป็นคนเขียนเอง สถานะที่โปรแกรมสั่งเอง เชื่อได้กว่าค่าที่อ่านกลับมา

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

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

แกะโค้ดจริง — ท่าที่ 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 เงื่อนไขจะไม่มีวันเป็นจริง แล้วไฟจะไม่วิ่งเลย

เทียบกับ 02_led_blink.py ได้เลย ไฟล์นั้นกะพริบดวงเดียวพร้อมนับรอบขึ้นจอ แต่จับเวลาด้วย time.sleep_ms() ซึ่งบล็อกทั้งลูประหว่างรอ ตัวเลขที่เดินขึ้นบอกแค่ว่าโปรแกรมยังไม่ค้าง ส่วนท่าไฟวิ่งนี้ใช้ ticks_diff ลูปจึงยังว่างไปอ่านปุ่มระหว่างรอ

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

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

แกะโค้ดจริง — ท่าที่ 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 ขณะรัน 06_button_picks_led.py ที่กดปุ่มเพื่อเลือกหลอดไฟ
ภาพหน้าจอจริงจากบอร์ด Eva Kit ขณะรัน 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: ออกดูสักครั้ง แล้วจะเห็นเลขกระโดดทีละสอง — เพราะปล่อยปุ่มก็คือการเปลี่ยนสถานะเหมือนกัน

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

แกะโค้ดจริง — ท่าที่ 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.1–1.3

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

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

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

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

ปุ่มผู้ใช้ นิ้วกด = ขาลง 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 ทุกครั้งโดยเราไม่ต้องสั่ง — ของแถมที่ทำให้เห็นภาพ

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

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

  1. เปิดไฟล์ 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 รันใหม่

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

TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · ดัดแปลงจาก AIoT in Action (AIC มหาวิทยาลัยบูรพา) · CC BY-NC 4.0

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