คาบ 2 — ปุ่ม + debounce + FSM

อ่านปุ่มให้แม่น แล้วทำไฟจราจร เขียว → เหลือง → แดง

แล็บแตะฮาร์ดแวร์ตรง ๆ — เปิดฝาดู GPIO

คาถาประจำคาบ: "อ่าน input → กรองให้นิ่ง → เปลี่ยน state → แสดงผล"

2–3 คน / 1 บอร์ด · ช่วง A — พื้นฐาน

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

วันนี้เราจะจับ 3 เรื่องที่เป็น "แกนของเกม" ทุกเกมในคอร์ส:

  1. อ่านปุ่มดิจิทัลgpio.button(0).is_pressed() คืนค่า True/False
  2. edge-detect + debounce ง่าย ๆ — กดค้างหนึ่งครั้งต้องนับเป็น "หนึ่งครั้ง" ไม่ใช่นับรัว ๆ
  3. state machine (FSM) — ไฟจราจร เขียว → เหลือง → แดง → วนกลับ แล้วสะท้อนออก LED

3 เรื่องนี้ต่อกันเป็นวงเดียว — และมันคือคาถาประจำคาบ "อ่าน input → กรองให้นิ่ง → เปลี่ยน state → แสดงผล":

อาจารย์ขอย้ำให้น้อง ๆ จำไว้เลยว่า FSM ตัวนี้คือต้นแบบของ game loop ในคาบถัด ๆ ไป
ตอนนี้ state คือสีไฟ พอถึงเกมจริง state จะกลายเป็น MENU / PLAYING / GAME OVER

ทำไมต้อง debounce — ปุ่มจริงมัน "เด้ง"

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

ถ้าเราอ่านดิบ ๆ ในลูปที่วิ่งเร็ว เรากดทีเดียวแต่โปรแกรมอาจเห็นเป็นกด 5–10 ครั้ง

วิธีแก้แบบง่ายที่สุดมี 2 ส่วนที่ทำงานคู่กัน:

  • edge detection — สนใจเฉพาะ "จังหวะที่เปลี่ยนจากปล่อย → กด" เท่านั้น ไม่สนตอนกดค้าง
  • หน่วงสั้น ๆ — หลังจับขอบได้ ค่อย sleep นิดนึงให้สัญญาณนิ่งก่อนอ่านรอบใหม่

ปล่อย    กด  =  ขอบขาลง (นับ 1 ครั้ง)\text{ปล่อย} \;\rightarrow\; \text{กด} \;=\; \text{ขอบขาลง (นับ 1 ครั้ง)}

bounce ไม่ใช่ทฤษฎี — นี่คือของจริงบนออสซิลโลสโคป

นี่คือสัญญาณปุ่มจริงที่จับด้วยออสซิลโลสโคป (HP 54501A) ตอนกดหนึ่งครั้ง — เห็นไหมว่ามันไม่ได้ลงทีเดียวเรียบ ๆ แต่ กระโดดขึ้นลงหลายที ก่อนจะนิ่ง

ที่มา: "Switch bounce" — Tomoldbury, Public domain, Wikimedia Commons

ช่วงเด้งนี้กินเวลาแค่ราว ๆ ไม่กี่มิลลิวินาที แต่ลูป MicroPython ของเราวิ่งเร็วกว่านั้นมาก — มันเลย "เห็น" การกระโดดทุกครั้งเป็นการกดแยกกัน นี่แหละต้นตอของปัญหานับรัว

อ่านกราฟ bounce ให้ออก — ทำไม 1 กด = นับหลายครั้ง

วาดสัญญาณเดียวกันเป็นไดอะแกรมให้ชัด: ขา idle อยู่ HIGH, พอกดมันเด้งสลับ HIGH/LOW อยู่ช่วงสั้น ๆ แล้วค่อยนิ่งที่ LOW

จุดแดงคือ "ทุกครั้งที่เราอ่านค่า" — ถ้าเช็กแค่ if pressed: เราจะนับช่วงเด้งได้ตั้ง 5–9 ครั้ง ทั้งที่นิ้วกดแค่ทีเดียว

คณิตของลูปอ่านปุ่มคิดง่าย ๆ แบบนี้: ถ้าลูปวิ่งเร็วทุก ๆ TloopT_{loop} และช่วงเด้งนาน tbouncet_{bounce} จำนวนครั้งที่อาจนับผิดคือ

Nfalse    tbounceTloop  =  4 ms0.5 ms  =  8 ครั้งN_{\text{false}} \;\approx\; \frac{t_{bounce}}{T_{loop}} \;=\; \frac{4\text{ ms}}{0.5\text{ ms}} \;=\; 8 \text{ ครั้ง}

ยิ่งลูปเร็ว (TloopT_{loop} เล็ก) ยิ่งนับผิดเยอะ — นี่คือเหตุผลที่เราใส่ time.sleep_ms(10) คุมจังหวะลูป และ ทำ edge detection ควบคู่กัน

debounce คือการ "ปิดหู" ช่วง tbouncet_{bounce} นี้ทั้งหมด — จับขอบแรกครั้งเดียวแล้วหน่วง ~50 ms ข้ามมันไป

เข้าใจฮาร์ดแวร์ · ปุ่มอ่านค่ายังไง

ตอน LED เราสั่งให้ขาเป็น OUTPUT (ชิป "ปล่อยไฟ") — ปุ่มกลับกัน ขาเป็น INPUT (ชิป "อ่าน" ว่าขาเป็น HIGH หรือ LOW)

  • ปุ่มบนบอร์ดเป็นแบบ active-low: กดแล้วต่อขาลง GND → อ่านได้ LOW; ไม่กด → pull-up ดึงขาไว้ที่ HIGH
  • หน้าสัมผัสโลหะตอนกดมัน เด้ง (contact bounce) อ่านได้ HIGH/LOW รัว ๆ ไม่กี่ ms ก่อนนิ่ง

แล้วปุ่มที่เราเรียก gpio.button(0) อยู่ตรงไหนบนบอร์ดจริง? คือ USER BUTTON 1 (SW2) มุมซ้ายล่าง ถัดจากช่องไฟ VIN — หาให้เจอก่อนจะได้กดถูกตัวตอนแล็บ:

ที่มา: รูปบอร์ดจากคู่มือ Infineon KIT_PSE84_EVAL (Figure 2 — Baseboard Top View) · ครอปเฉพาะส่วน USER BTN1 แล้วใส่คำอธิบายเพิ่ม

นี่คือเหตุผลที่เราต้อง debounce ในโค้ด — มันคือการกรอง bounce ทางไฟฟ้าจริง ๆ ไม่ให้ 1 การกดนับเป็นหลายครั้ง

เข้าใจฮาร์ดแวร์ · ปุ่มจริง + วงจร pull-up

ปุ่มกดที่เราเรียก gpio.button(0) ข้างในก็คือ microswitch ตัวเล็ก ๆ แบบนี้ — กดแล้วแผ่นโลหะข้างในแตะกันให้ครบวงจร ปล่อยแล้วสปริงดีดกลับ

ที่มา: "Electronic-Component-Microswitch" — Evan-Amos, Public domain, Wikimedia Commons

แล้วทำไม "ไม่กด" ถึงอ่านได้ HIGH? เพราะมีตัวต้านทาน pull-up คอยดึงขาไว้ที่ 3.3V เมื่อไม่มีอะไรต่อลง GND:

จำสั้น ๆ: active-low = กด → 0, ไม่กด → 1 (ตรงข้ามสัญชาตญาณนิดนึง แต่เป็นวงจรมาตรฐานของปุ่มบนบอร์ดส่วนใหญ่)

โครงคิดของ FSM ไฟจราจร

state มีแค่ 3 ค่า แล้วกดปุ่มทีก็ขยับไปข้างหน้าหนึ่งขั้น พอถึงแดงก็วนกลับเขียว

GREEN (0)
YELLOW (1)
RED (2)

หัวใจของการวนรอบคือสูตรเดียว:

state = (state + 1) % 3   # 0→1→2→0→1→2 ... วนไม่รู้จบ

เลข % 3 (มอดุโล) คือตัวทำให้พอเกิน 2 แล้วเด้งกลับไป 0 เอง ไม่ต้องเขียน if เช็กเอง

FSM เป็นภาพ — สถานะ + เส้นเปลี่ยนสถานะ

วาดเป็นแผนภาพสถานะ (state diagram) จะเห็นภาพชัดว่า "กดแต่ละที" ทำให้กระโดดจากวงไหนไปวงไหน:

มอดุโลคือพระเอกของเส้นโค้งสีแดง — ตอนอยู่ RED (state = 2) กดอีกที (2 + 1) % 3 ได้ 0 พอดี เลยวนกลับ GREEN เองโดยไม่ต้องเขียน if

statenext=(state+1)mod3    0,1,2,0,1,2,\text{state}_{\text{next}} = (\text{state} + 1) \bmod 3 \;\Rightarrow\; 0,1,2,0,1,2,\dots

วงกลม = state, ลูกศร = transition (เงื่อนไขที่ทำให้เปลี่ยน) — นี่คือภาษากลางที่วิศวกรใช้ออกแบบทุกอย่างตั้งแต่ไฟจราจรยันลอจิกของเกม

เข้ารหัส 3 สถานะด้วย LED 2 ดวง

บอร์ดเรามี LED ใช้สะดวก 2 ดวง แต่ state มี 3 แบบ — เราเลยจับคู่ on/off ให้แยกกันชัด

state led_a led_b
GREEN ติด ดับ
YELLOW ติด ติด
RED ดับ ติด

ถ้ามองเป็นภาพ LED จริงสองดวง จะเห็นว่าแต่ละ state คือ "ลายไฟ" ที่ไม่ซ้ำกัน — ค่า (a, b) ในตารางก็คือคู่ on/off นี้เป๊ะ ๆ:

state led_a (เขียว) led_b (แดง) (a, b) GREEN (True, False) YELLOW (True, True) RED (False, True) = ติด (on) = ดับ (off) 2 ดวง เข้ารหัสได้ 3 สถานะแยกกันชัด

เก็บเป็น dictionary ไว้ ทำให้โค้ดอ่านง่ายและเพิ่ม state ใหม่ทีหลังได้สบาย:

# ตารางว่าแต่ละ state ควรเปิด LED ดวงไหนบ้าง (led_a, led_b)
LED_PATTERN = {
    GREEN:  (True,  False),
    YELLOW: (True,  True),
    RED:    (False, True),
}
NAME = {GREEN: "GREEN", YELLOW: "YELLOW", RED: "RED"}

โค้ด — ส่วนตั้งต้น (import + ตัวแปร)

import gpio
import time

# ขอใช้ปุ่มดวงแรก (index 0 = USER BTN1 บนบอร์ด) และไฟ LED 2 ดวงไว้บอกสถานะ
btn = gpio.button(0)
led_a = gpio.led(1)   # LED2 (เขียว) — เลี่ยง LED1 (แดง) ที่ระบบใช้
led_b = gpio.led(2)   # RGB_RED — คู่กับ led_a เข้ารหัส 3 สถานะด้วย 2 ดวง

# นิยาม state ของ FSM ให้อ่านง่าย แทนที่จะใช้ตัวเลขลอย ๆ
GREEN, YELLOW, RED = 0, 1, 2
state = GREEN

gpio.button(0) คือปุ่ม USER BTN1 บนบอร์ด ส่วน gpio.led(1) / gpio.led(2) คือ LED สองดวง (เลี่ยง LED1 แดงที่ระบบใช้)
ตรงนี้เรายังไม่ทำอะไร แค่ "จองอุปกรณ์" ไว้ใช้ และตั้งชื่อ state ให้คนอ่านเข้าใจ

โค้ด — ฟังก์ชัน show() สะท้อน state ออก LED

def show(s):
    # สะท้อน state ปัจจุบันออก LED ตามตารางด้านบน
    a_on, b_on = LED_PATTERN[s]
    led_a.on() if a_on else led_a.off()
    led_b.on() if b_on else led_b.off()
    print("STATE =", NAME[s])

เราแยกเรื่อง "แสดงผล" ออกมาเป็นฟังก์ชันเดียว — ที่ไหนอยากอัปเดต LED ก็เรียก show(state)

นี่คือนิสัยที่ดีของโปรแกรมเมอร์: logic การเปลี่ยน state กับ การแสดงผล แยกกันคนละที่
พอถึงเกมจริง show() จะกลายเป็นการวาดภาพบนจอ แต่โครงคิดเหมือนกันเป๊ะ

โค้ด — ลูปหลัก (debounce + เปลี่ยน state)

# ตัวแปรสำหรับ debounce: จำสถานะปุ่มรอบก่อน เพื่อจับ "ขอบขาลง" (กดลงครั้งเดียว)
prev_pressed = btn.is_pressed()
show(state)

print("กดปุ่ม USER BTN1 เพื่อเปลี่ยนไฟจราจร (Ctrl+C เพื่อหยุด)")
while True:
    pressed = btn.is_pressed()

    # debounce แบบง่าย: นับเป็น "กด" เฉพาะตอนที่เพิ่งเปลี่ยนจากปล่อย -> กด
    # (เป็น edge detection กันปุ่มเด้งซ้ำ ๆ จากการกดค้างหนึ่งครั้ง)
    if pressed and not prev_pressed:
        state = (state + 1) % 3   # เลื่อน FSM ไปสถานะถัดไปแบบวนรอบ
        show(state)
        time.sleep_ms(50)         # หน่วงสั้น ๆ ให้สัญญาณปุ่มนิ่งก่อนอ่านต่อ

    prev_pressed = pressed
    time.sleep_ms(10)             # คุมจังหวะ loop ~100 รอบ/วินาที ไม่ให้ busy เกินจำเป็น

อ่านลูปนี้ให้ขาด

หัวใจอยู่ที่บรรทัด if pressed and not prev_pressed: — แปลเป็นภาษาคนว่า
"ตอนนี้กดอยู่ และ รอบที่แล้วยังไม่กด" นั่นแหละคือ จังหวะกดลงพอดี

  • ถ้ากดค้างไว้ → รอบถัดไป prev_pressed ก็เป็น True แล้ว เงื่อนไขเลยไม่จริง → ไม่นับซ้ำ
  • บรรทัดสุดท้าย prev_pressed = pressed คือการ "จำไว้ใช้รอบหน้า" ห้ามลืม

จุดที่มักสับสน — "rising edge" ของอะไร? ทางไฟฟ้า ปุ่มเป็น active-low: กดแล้วขาถูกดึงลง LOW (0V) ซึ่งคือ falling edge. แต่ btn.is_pressed() กลับขั้วให้แล้ว มันคืน True เมื่อกด ดังนั้น pressed and not prev_pressed (s02_button_fsm.py:50) ทำงานบนค่าตรรกะ (True = กด) ไม่ใช่บนระดับแรงดันไฟ — "rising edge ของสัญญาณตรรกะ" (False → True) จึงตรงกับ "falling edge ทางไฟฟ้า" (HIGH → LOW) พอดี เราเลยคิดเป็น "ปล่อย → กด" ได้ตรง ๆ โดยไม่ต้องพะวงว่าไฟจริงเป็น 0 หรือ 1

ลองไล่ทั้ง 4 ความเป็นไปได้ของ (prev, ตอนนี้) — มีแค่แถวเดียวที่เงื่อนไขเป็นจริง:

ลองคิดดูว่าถ้าเราลบ prev_pressed ทิ้ง แล้วเช็กแค่ if pressed: จะเกิดอะไร?
คำตอบ: กดทีเดียวไฟจะวิ่งรัวจนกว่าจะปล่อยมือ — นี่คือเหตุผลที่ต้อง debounce

โปรแกรมทั้งตัว · ไล่ตั้งแต่เปิดเครื่องจนวนลูป

โค้ด s02_button_fsm.py ทั้งไฟล์เดินตามผังนี้ — ตั้งค่าครั้งเดียว แล้ววน "อ่าน → เช็กขอบ → เปลี่ยน state → จำ" ไม่รู้จบ:

START ตั้งค่า + show()btn/led/state · :14–42 อ่านปุ่มpressed · :46 pressed andnot prev ?:50 state=(state+1)%3show() → sleep50 · :51–53 prev = pressedsleep10 · :55–56 Yes No วนรอบถัดไป (~100 Hz)

สัญกรณ์ผังงาน (ใช้เหมือนกันทุกคาบ): สี่เหลี่ยมมน = จุดเริ่ม/จบ · สี่เหลี่ยม = ทำงาน (process) · ข้าวหลามตัด = ตัดสินใจ (มีป้าย Yes/No) · สีเขียว = อ่าน input / แสดงผล · เส้นประ = เส้นวนกลับ

เห็นภาพเคลื่อนไหว — edge detection กับตัวนับสองแบบ

ดูสัญญาณเดียวกันแต่เทียบตัวนับสองตัวแบบสด ๆ: ฝั่งซ้าย (แดง) อ่านดิบ if pressed: นับทุก step ที่เป็น LOW, ฝั่งขวา (เขียว) pressed and not prev_pressed: + หน่วง

  • ช่วงที่แถบเขียวคลุม = ตอน time.sleep_ms(50) ทำงาน เรา "ปิดหู" ข้ามช่วงเด้งทั้งหมด
  • ตัวนับแดงพุ่งไปเรื่อย ๆ (busy) ส่วนตัวนับเขียวค้างที่ 1 = นับ 1 กด เป็น 1 ครั้งเป๊ะตามที่เราต้องการ

นี่คือภาพเดียวกับโค้ดในสไลด์ที่แล้ว แค่เห็นมันเดินทีละ step จะเข้าใจ prev_pressed ทันที

ลงมือทำ — Program to Device

ขั้นตอนวันนี้ ไม่ต้องพิมพ์ exec(open(...)) ใด ๆ ทั้งสิ้น:

  1. เปิดไฟล์ practise_codes/s02_button_fsm.py ใน BENTO IDE
  2. กดปุ่ม Program to Device ที่แถบเครื่องมือ
  3. รอจนบอร์ดรีสตาร์ตและขึ้นข้อความ กดปุ่ม USER BTN1 เพื่อเปลี่ยนไฟจราจร
  4. กด USER BTN1 บนบอร์ดทีละครั้ง — สังเกต LED และข้อความ STATE = ใน console

หน้าจอ BENTO IDE จริงเป็นแบบนี้ — คือปุ่ม Program to Device ที่แถบเครื่องมือ (ข้อ 2) กดแล้วดูที่ ช่อง console จะขึ้น Saved /main.py แล้ว Restarting device... (ข้อ 3):

ที่มา: ภาพหน้าจอ BENTO IDE (course asset) · ในภาพเป็นอีกตัวอย่างหนึ่ง แต่ปุ่มและลำดับ console เหมือนกันทุกแล็บ — สำหรับคาบนี้ console จะขึ้น กดปุ่ม USER BTN1 เพื่อเปลี่ยนไฟจราจร

สิ่งที่ควรเห็น:

  • กด 1 ครั้ง = ขยับ 1 สถานะ ไม่กระโดดข้าม ไม่วิ่งรัว
  • ลำดับวน เขียว → เหลือง → แดง → เขียว ... ไม่มีหลุด

ถ้ามันไม่เป็นอย่างที่คิด (เช็กก่อนเรียกอาจารย์)

  • กดทีเดียวแต่ข้ามหลายสถานะ → ค่า time.sleep_ms(50) อาจถูกลบ หรือ debounce ไม่ครบ ลองเช็กบรรทัด prev_pressed = pressed
  • กดแล้วไม่ขยับเลย → ปุ่มอาจไม่ใช่ index 0 หรือลืมเรียก show(state) หลังเปลี่ยน state
  • LED ไม่ติดตามตาราง → ดูฟังก์ชัน show() ว่าจับคู่ a_on / b_on ถูกดวงไหม
  • console เงียบ → ตรวจว่า Program to Device สำเร็จ และเชื่อม REPL อยู่

น้อง ๆ จำไว้ว่าการ debug คือการ "ตั้งสมมติฐานทีละข้อแล้วพิสูจน์" ไม่ใช่เดามั่ว

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

ไฟจราจรเล็ก ๆ ที่กดปุ่มแล้วเปลี่ยนสี ซ่อนรากฐานที่เราจะใช้ตลอดคอร์ส:

ฝั่ง Embedded / ฮาร์ดแวร์

  • GPIO เป็น digital input — อ่านขาว่าเป็น HIGH/LOW; คู่ตรงข้ามของ output ในคาบที่แล้ว
  • pull-up + active-low — วงจรมาตรฐานของปุ่ม: ไม่กด = 1, กด = 0
  • contact bounce — ปรากฏการณ์ทางไฟฟ้าจริงที่ต้องกรองด้วยซอฟต์แวร์ (debounce)

ฝั่ง Python / อัลกอริทึม

  • state variable + เงื่อนไขif pressed and not prev_pressed คือ edge detection
  • modulo (%) — เลขคณิตที่ทำให้ค่าวนเป็นวง โดยไม่ต้องเขียน if เช็กขอบเอง
  • dictionaryLED_PATTERN แมป state → เอาต์พุต ทำให้เพิ่ม state ใหม่ได้ง่าย

ฝั่ง FSM (รากฐานของ game loop)

  • finite state machine — แยก "สถานะ" กับ "เส้นเปลี่ยนสถานะ" ชัด ๆ; คาบหน้า ๆ state จะกลายเป็น MENU / PLAYING / GAME OVER

ดูคลิป (เสริม) — อธิบาย bounce บนออสซิลโลสโคปจริง แล้วโชว์ทั้ง debounce ทางฮาร์ดแวร์และซอฟต์แวร์ ตรงกับที่เราทำวันนี้:

What is Switch Bounce and How to Debounce – ATM | Digi-Key Electronics — DigiKey

สรุปคาบ 2 + การบ้าน (ทำเอง 30%)

วันนี้เราได้แล้ว:

  • อ่านปุ่มด้วย gpio.button(0).is_pressed()
  • debounce ด้วย edge detection (pressed and not prev_pressed) + หน่วงสั้น ๆ
  • FSM 3 สถานะ วนด้วย (state + 1) % 3 แล้วสะท้อนออก LED ผ่าน show()

การบ้าน — เติมช่องในฉบับฝึก:
เปิด practise_codes/s02_button_fsm.py หาคำว่า "เติม:" 4 จุด แล้วเขียนเองให้ครบ
(เปิด/ปิด led_a, เปิด/ปิด led_b, อ่าน prev_pressed ตอนเริ่ม, และสูตรวน state)

ลองด้วยตัวเองให้สุดก่อนนะ — เดี๋ยวเฉลยพร้อมกันในห้องเรียน
อยากต่อยอด: เพิ่ม state ที่ 4 (ไฟกะพริบ) ดูว่าต้องแก้ตรงไหนบ้าง — เกมเต็มอยู่ใน full_games/

เฉลย s02_button_fsm.py — อ่านให้เข้าใจแล้วพิมพ์เอง

เฉลยนี้ไม่ได้มีไว้ให้ copy-paste ส่ง คะแนนของคาบนี้อยู่ที่ใบงานกับการอธิบายด้วยคำพูดของน้องเอง ไม่ใช่ที่โค้ดตรงกับเฉลย วิธีใช้ให้ได้ของจริงคือ อ่านให้เข้าใจ ปิดไฟล์ แล้วพิมพ์ใหม่ด้วยมือตัวเอง — ช่วงที่นิ้วพิมพ์เองนี่แหละที่สมองจำ pattern ได้ ต่อไปเราจะแกะทีละส่วน เพราะทุกบรรทัดมีเหตุผล ไม่ได้เขียนสุ่ม ๆ

ส่วนแรก — จองอุปกรณ์ กับตั้งชื่อ state:

import gpio
import time

# ขอใช้ปุ่มดวงแรก (index 0 = USER BTN1 บนบอร์ด) และไฟ LED 2 ดวงไว้บอกสถานะ
# ใช้ LED2 (เขียว) + RGB_RED — เลี่ยง LED1 (แดง, index 0) ที่ระบบหลักมักใช้เป็นไฟ status
btn = gpio.button(0)
led_a = gpio.led(1)   # LED2 (เขียว) — ใช้คู่กับ led_b เข้ารหัส 3 สถานะด้วย 2 ดวง
led_b = gpio.led(2)   # RGB_RED

# นิยาม state ของ FSM ให้อ่านง่าย แทนที่จะใช้ตัวเลขลอย ๆ
GREEN, YELLOW, RED = 0, 1, 2
state = GREEN
  • สองบรรทัด import (:9-10) บอกว่าวันนี้เราใช้ของสองอย่าง: gpio อ่านปุ่ม/สั่งไฟ กับ time คุมจังหวะลูป — ตรงกับสองในสี่ท่อนของคาถาประจำคาบ "อ่าน input" กับ "กรองให้นิ่ง"
  • btn = gpio.button(0) กับ led_a/led_b (:14-16) เราเก็บ object ลงตัวแปร ครั้งเดียว แล้วใช้ซ้ำทั้งไฟล์ ไม่เรียก gpio.button(0) ใหม่ทุกรอบลูป — ถ้าเรียกซ้ำในลูปที่วิ่ง ~100 Hz คือการจองอุปกรณ์เดิมพันครั้งเปล่า ๆ
  • ทำไมต้อง GREEN, YELLOW, RED = 0, 1, 2 (:19) แทนที่จะเขียนเลข 0/1/2 ลอย ๆ? เพราะพอโค้ดยาวขึ้น if state == 2 อ่านไม่ออกว่าหมายถึงไฟอะไร แต่ if state == RED อ่านออกทันที นี่คือ named constant — ต่อจากสไลด์ "โครงคิดของ FSM ไฟจราจร" ที่เรานิยาม 3 สถานะไว้
  • ตั้งค่าเริ่มต้น state = GREEN (:20) — ทุก FSM ต้องมี "สถานะตั้งต้น" ที่รู้แน่ ไม่ใช่ปล่อยให้ตัวแปรลอย

จำหลักนี้ไว้: อะไรที่ใช้ซ้ำทั้งโปรแกรม (ปุ่ม ไฟ ชื่อสถานะ) ตั้งชื่อเก็บไว้บนหัวไฟล์ก่อน แล้วโค้ดข้างล่างจะอ่านเหมือนภาษาคน

เฉลย · ส่วนที่สอง — แยก "แสดงผล" ออกเป็นตาราง + ฟังก์ชัน

# ตารางว่าแต่ละ state ควรเปิด LED ดวงไหนบ้าง (led_a, led_b)
# เขียว = ดวง A, เหลือง = ทั้งคู่, แดง = ดวง B  (เลือกให้แยกกันได้ชัด)
LED_PATTERN = {
    GREEN:  (True,  False),
    YELLOW: (True,  True),
    RED:    (False, True),
}
NAME = {GREEN: "GREEN", YELLOW: "YELLOW", RED: "RED"}
def show(s):
    # สะท้อน state ปัจจุบันออก LED ตามตารางด้านบน
    a_on, b_on = LED_PATTERN[s]
    led_a.on() if a_on else led_a.off()
    led_b.on() if b_on else led_b.off()
    print("STATE =", NAME[s])
  • LED_PATTERN (:24-28) คือ dictionary ที่แมป state → คู่ on/off ของไฟสองดวง — นี่คือตารางเดียวกับสไลด์ "เข้ารหัส 3 สถานะด้วย LED 2 ดวง" เป๊ะ ๆ แค่ย้ายจากตารางบนสไลด์มาเป็นข้อมูลในโค้ด เขียน 3 สถานะด้วยไฟ 2 ดวงโดยเลือกลายไฟให้ไม่ซ้ำกัน
  • ทำไมเก็บเป็น dict แทนที่จะเขียน if state == GREEN: ... elif ...? เพราะถ้าวันหน้าจะเพิ่มไฟกะพริบเป็นสถานะที่ 4 เราแค่เติมอีกหนึ่งแถวในตาราง ไม่ต้องแก้ show() เลย — ข้อมูลขับพฤติกรรม ไม่ใช่ if ซ้อน if ยาว ๆ
  • show(s) (:32-37) รวบเรื่อง "สะท้อน state ออกฮาร์ดแวร์" ไว้ที่เดียว บรรทัด led_a.on() if a_on else led_a.off() คือ ternary — อ่านว่า "ถ้า a_on ให้ติด ไม่งั้นดับ" ตรงกับท่อนสุดท้ายของคาถา "แสดงผล"
  • print("STATE =", NAME[s]) มี NAME (:29) ไว้แปลงเลข state กลับเป็นคำ — ตอน debug เราอยากเห็น STATE = RED ไม่ใช่ STATE = 2 ที่ต้องมานั่งแปลเอง

นิสัยสำคัญที่ซ่อนอยู่: logic การเปลี่ยน state อยู่คนละที่กับ การแสดงผล พอถึงเกมจริง show() จะกลายเป็นการวาดฉากลงจอ แต่โครงคิด "state อยู่ในตัวแปร ส่วนการวาดแยกออกมา" เหมือนเดิมทุกอย่าง

เฉลย · ส่วนที่สาม — ลูปหลัก: อ่าน → เช็กขอบ → เปลี่ยน → จำ

# ตัวแปรสำหรับ debounce: จำสถานะปุ่มรอบก่อน เพื่อจับ "ขอบขาลง" (กดลงครั้งเดียว)
prev_pressed = btn.is_pressed()
show(state)

print("กดปุ่ม USER BTN1 เพื่อเปลี่ยนไฟจราจร (Ctrl+C เพื่อหยุด)")
while True:
    pressed = btn.is_pressed()

    # debounce แบบง่าย: นับเป็น "กด" เฉพาะตอนที่เพิ่งเปลี่ยนจากปล่อย -> กด
    # (เป็น edge detection กันปุ่มเด้งซ้ำ ๆ จากการกดค้างหนึ่งครั้ง)
    if pressed and not prev_pressed:
        state = (state + 1) % 3   # เลื่อน FSM ไปสถานะถัดไปแบบวนรอบ
        show(state)
        time.sleep_ms(50)         # หน่วงสั้น ๆ ให้สัญญาณปุ่มนิ่งก่อนอ่านต่อ

    prev_pressed = pressed
    time.sleep_ms(10)             # คุมจังหวะ loop ~100 รอบ/วินาที ไม่ให้ busy เกินจำเป็น
  • ก่อนเข้าลูป เราอ่าน prev_pressed = btn.is_pressed() (:41) และ show(state) (:42) ครั้งหนึ่งก่อน — ตั้ง "ความจำรอบก่อน" กับปักไฟตั้งต้นให้ตรงกับ state = GREEN ถ้าลืมบรรทัดนี้ รอบแรกสุดจะเทียบกับค่าขยะ
  • หัวใจอยู่ที่ if pressed and not prev_pressed: (:50) — "ตอนนี้กด และ รอบที่แล้วยังไม่กด" คือ edge detection ที่เราแกะกันไปแล้วในสไลด์ "อ่านลูปนี้ให้ขาด" กดค้างไว้ก็นับครั้งเดียว เพราะรอบถัดไป prev_pressed เป็น True แล้ว
  • state = (state + 1) % 3 (:51) คือมอดุโลจากสไลด์ "โครงคิดของ FSM ไฟจราจร" — พอถึง RED (2) กดอีกที (2+1)%3 ได้ 0 วนกลับ GREEN เองโดยไม่ต้องเขียน if เช็กขอบ
  • time.sleep_ms(50) หลังเปลี่ยน state (:53) คือการ "ปิดหู" ข้ามช่วง bounce ที่เราคำนวณไว้ในสไลด์ "อ่านกราฟ bounce ให้ออก" (Nfalsetbounce/TloopN_{false} \approx t_{bounce}/T_{loop}) — ส่วน prev_pressed = pressed (:55) กับ sleep_ms(10) (:56) คือ "จำไว้ใช้รอบหน้า" และคุมจังหวะลูปไม่ให้ busy — ตรงกับกล่องสุดท้ายในผังงาน "โปรแกรมทั้งตัว"

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

เฉลย · สี่ส่วนนี้ประกอบกันเป็นคาถาได้ยังไง

โค้ดทั้งไฟล์ไม่ได้เขียนเรียงมั่ว แต่ละส่วนรับผิดชอบท่อนหนึ่งของคาถา "อ่าน input → กรองให้นิ่ง → เปลี่ยน state → แสดงผล" แล้วต่อกันเป็นวงเดียว:

ส่วน บรรทัด รับผิดชอบท่อนไหนของคาถา รากฐานที่เอามาใช้
จองอุปกรณ์ + ชื่อ state :14-20 เตรียมของก่อนเข้าลูป named constant + GPIO input/output
LED_PATTERN + NAME :24-29 แสดงผล (ข้อมูลลายไฟ) dictionary แมป state → เอาต์พุต
show(s) :32-37 แสดงผล (ลงมือสั่งไฟ) แยก display ออกจาก logic
อ่านปุ่ม + edge detect :46-50 อ่าน input → กรองให้นิ่ง is_pressed() + pressed and not prev
(state + 1) % 3 + sleep_ms(50) :51-53 เปลี่ยน state + debounce modulo วนรอบ + หน่วงข้าม bounce
prev = pressed + sleep_ms(10) :55-56 จำไว้ใช้รอบหน้า + คุมจังหวะ edge detection ต้องมีความจำ
  • ไล่จากบนลงล่างคือ "ของนิ่งก่อน แล้วค่อยของที่ขยับ": ค่าคงที่/อุปกรณ์/ตารางอยู่บนหัว ส่วนที่เปลี่ยนทุกรอบ (อ่าน → เช็ก → เปลี่ยน → จำ) อยู่ในลูป
  • สังเกตว่า แสดงผล ถูกแยกเป็นสองชั้น: ตาราง LED_PATTERN เก็บ "จะให้ไฟเป็นยังไง" ส่วน show() เก็บ "ลงมือสั่งยังไง" — แก้ลายไฟไม่ต้องแตะลูป แก้จังหวะลูปไม่ต้องแตะลายไฟ
  • นี่คือโครงเดียวกับ game loop ที่จะโตขึ้นในคาบถัด ๆ ไป: ตั้งค่าครั้งเดียว แล้ววน "รับ input → อัปเดต state → วาดผล" ไม่รู้จบ

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

เชื่อมจุดให้เห็นภาพ — ปุ่มนี้มาจากไหน แล้วจะพาเราไปถึงไหน

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

ที่มา — วันนี้ต่อยอดมาจากอะไร คาบที่แล้วเราสั่งไฟ LED ได้อย่างเดียว โปรแกรมพูดฝ่ายเดียว บอร์ดทำตามอย่างเดียว ไม่มีทางรู้เลยว่าคนกำลังทำอะไรอยู่ วันนี้เราเติมครึ่งที่หายไป คือ การรับ input ผ่าน gpio.button(0).is_pressed() ลองย้อนไปดูสไลด์ "เป้าหมายของคาบนี้" ที่บอกว่า 3 เรื่องต่อกันเป็นวงเดียว — พอบอร์ดอ่านปุ่มได้ วงจรการสื่อสารสองทางจึงครบ จากนั้น FSM (สไลด์ "โครงคิดของ FSM ไฟจราจร") ก็เข้ามาเป็น "ความจำ" ให้โปรแกรม ว่าตอนนี้อยู่สถานะไหน — เหมือน flip-flop ในคาบแรก แค่คราวนี้ความจำอยู่ในตัวแปร state ไม่ใช่ในสายไฟ

ที่ไป — ปุ่มนี้จะโตเป็นอะไร ในสไลด์ "เชื่อมโยงรากฐาน" อาจารย์ทิ้งท้ายไว้ว่า FSM 3 สีวันนี้คือต้นแบบของ MENU → PLAYING → GAME OVER ในเกมจริง ลองสังเกต: โครงลูปวันนี้คือ "อ่านปุ่ม → ถ้ากดขอบขาลง → เปลี่ยน state → วาดผล" ถ้าเปลี่ยน "ไฟจราจร 3 สี" เป็น "หน้าจอเกม 3 ฉาก" และเปลี่ยน "กดเลื่อนสี" เป็น "กด Start เริ่มเกม / ตายแล้วไป Game Over" โค้ดแทบไม่ต่างเลย — ปุ่มที่เราเพิ่งอ่านได้วันนี้ คือปุ่มควบคุมเกมของทั้งคอร์ส

ลองตอบคำถามพวกนี้ในใจ นี่แหละคือการเชื่อมจุดด้วยตัวเอง:

  • จำ prev_pressed and not ... จากสไลด์ "อ่านลูปนี้ให้ขาด" ได้ไหม ถ้าวันหน้าปุ่มนี้คือปุ่ม "กระโดด" ในเกม เราจะยังใช้ edge detection ตัวเดิมกันกระโดดรัวไหม?
  • ถ้าวันนี้เราวน state ได้ด้วย (state + 1) % 3 พรุ่งนี้ถ้ามี 4 ฉาก เราจะเปลี่ยนเป็น % 4 แล้วมันก็วนได้เองเลยไหม?
  • สังเกตไหมว่า "กดปุ่มแล้วเปลี่ยนสถานะ" กับ "กดปุ่มเมนูแล้วเปลี่ยนหน้าจอ" มันคือเรื่องเดียวกัน? (ใบ้: ต่างกันแค่ LED_PATTERN กลายเป็น "ฉากที่จะวาด")
ที่มา คาบ 1: สั่ง output อย่างเดียว (บอร์ดพูดฝ่ายเดียว) วันนี้ input + debounce + FSM (กดปุ่ม → เปลี่ยน state) ที่ไป MENU / PLAYING / GAME OVER (ปุ่มควบคุมเกม)

ถ้าตอบสามคำถามข้างบนได้ว่า "อ๋อ มันคือเรื่องเดียวกัน" — นั่นคือการหยั่งรู้ที่อาจารย์อยากให้เกิด: ไฟจราจรเล็ก ๆ วันนี้คือ state machine ของทั้งเกม เราแค่ยังไม่ได้เปลี่ยนไฟเป็นฉาก

ใช้จริงที่ไหน — debounce · edge detection · FSM

เทคนิควันนี้ไม่ใช่แค่แบบฝึกหัด มันคือสิ่งที่เฟิร์มแวร์จริงใช้กันทุกวัน:

ปุ่มลิฟต์ / เครื่องใช้ไฟฟ้า · debounce กดจริง (เด้ง) 1 ครั้ง กดชั้น 3 ทีเดียว ลิฟต์รับเป็น 1 คำสั่ง เครื่องซักผ้า / ATM · คือ FSM แช่ ซัก ปั่น จบ กดปุ่ม = เลื่อนสถานะ เหมือนไฟจราจรเรา จอยเกม / รีโมต · edge detection 0 1 กด → กระโดด 1 กดค้าง = ไม่นับซ้ำ กด A ทีเดียว = กระโดดครั้งเดียว (rising edge) คีย์บอร์ด / คีย์แมทริกซ์ a 'a' ไม่ใช่ 'aaaaa' สแกนร้อยปุ่ม + debounce ทุกปุ่ม
  • ปุ่มลิฟต์ / ปุ่มกดเครื่องใช้ไฟฟ้า — ทุกปุ่มที่คนกดต้องผ่าน debounce ก่อน ไม่งั้นกดชั้น 3 ทีเดียวลิฟต์อาจรับเป็นสามที (สไลด์ "ทำไมต้อง debounce") — หน้าสัมผัสโลหะเด้งจริง ต้องกรองด้วยซอฟต์แวร์เสมอ
  • สถานะเครื่องจริงคือ FSM — เครื่องซักผ้า (แช่ → ซัก → ปั่น → จบ), ตู้เอทีเอ็ม, ไมโครเวฟ ล้วนเป็น finite state machine ที่ "กดปุ่มแล้วเลื่อนสถานะ" แบบไฟจราจรเรา — ต่างแค่จำนวน state กับเงื่อนไข transition (สไลด์ "FSM เป็นภาพ")
  • ปุ่มควบคุมในจอยเกม / รีโมต — ใช้ edge detection แยก "เพิ่งกด" (rising edge) ออกจาก "กดค้าง": กด A ทีเดียวกระโดดครั้งเดียว ส่วนกดค้างปุ่มทิศให้เดินต่อเนื่อง คือ pressed and not prev_pressed (สไลด์ "อ่านลูปนี้ให้ขาด")
  • แป้นพิมพ์ / คีย์แมทริกซ์ — สแกนปุ่มเป็นร้อยด้วยลูปเร็ว ๆ แล้ว debounce ทุกปุ่ม กด 'a' ทีเดียวได้ 'a' ตัวเดียว ไม่ใช่ 'aaaaa' — คือ NfalseN_{false} จากสไลด์ "อ่านกราฟ bounce ให้ออก" ที่ถูกกำจัดทิ้ง

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

อีกวิธีแสดงผล — ไฟจราจร "ติดทีละดวง" (อ่านเสริม)

เวอร์ชันหลักของคาบนี้ใช้ LED 2 ดวงเข้ารหัส 3 สถานะ (เหลือง = ติดทั้งคู่) — เป็นวิธีที่ฉลาดเมื่อมีดวงไฟน้อยกว่าจำนวนสถานะ ลองดูอีกแบบที่ ติดทีละดวง เหมือนไฟจราจรจริง แล้วสังเกตว่าทั้งสองแบบใช้ FSM เดียวกันเป๊ะ:

import gpio, time

btn  = gpio.button(0)
lamp = {0: gpio.led(1),   # GREEN  -> LED2
        1: gpio.led(0),   # YELLOW -> LED1  (คราวนี้ได้ใช้)
        2: gpio.led(2)}   # RED    -> RGB_RED
NAME = {0: "GREEN", 1: "YELLOW", 2: "RED"}

def show(s):
    for i, led in lamp.items():
        led.on() if i == s else led.off()   # ติดดวงที่ตรงสถานะ ดับที่เหลือ
    print("STATE =", NAME[s])

state, prev = 0, btn.is_pressed()
show(state)
while True:
    pressed = btn.is_pressed()
    if pressed and not prev:          # edge detection เดิม
        state = (state + 1) % 3        # FSM เดิม
        show(state)
        time.sleep_ms(50)
    prev = pressed
    time.sleep_ms(10)

จุดที่อยากให้สังเกต: แกน FSM ไม่เปลี่ยนเลยสักบรรทัด (edge detection + (state+1)%3 เหมือนเดิม) เปลี่ยนแค่ show() — นี่คือหลัก "แยกตรรกะ (state) ออกจากการแสดงผล (display)" เปลี่ยนหน้าตาได้โดยไม่แตะสมองของเกม เดี๋ยวบล็อกเกมจริงจะใช้หลักนี้ตลอด · โค้ดเต็มอยู่ที่ solution_codes/s02_traffic_v2.py (อ่านเสริม ไม่คิดคะแนน)

ต่อยอด — คิดต่อเอง

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

แบบวันนี้ — ปุ่มดิจิทัล อ่านได้แค่ 0 / 1 0 1 edge: False → True pressed and not prev เปลี่ยนวิธีคิด: ขอบ → เกินเส้น threshold คาบหน้า — จอยแอนะล็อก อ่านเป็นช่วง -100..100 -100 +100 threshold ค่าเกินเส้น = ทริกเกอร์ edge detection → threshold crossing
  • ไฟจราจรที่เดินเองตามเวลา — ถ้าอยากให้เปลี่ยนสีเองทุก ๆ 3 วินาที (แต่กดปุ่มแล้วข้ามได้ทันที) จะออกแบบยังไง? ใบ้: ผสม "จำเวลาที่ผ่านไป" กับ edge detection ของปุ่ม — ทั้งเวลาและปุ่มต่างก็ทำให้ transition เกิดได้
  • กดยาว กับ กดสั้น ต่างกัน — ถ้าอยากให้ "กดสั้น" เลื่อนสถานะ แต่ "กดค้าง 2 วินาที" รีเซ็ตกลับ GREEN จะต้องจำอะไรเพิ่มจาก prev_pressed? (ใบ้: ต้องจำ เวลาที่เริ่มกด ด้วย) นี่คือหลักเดียวกับปุ่ม power ของมือถือ
  • สองปุ่ม เดินหน้า-ถอยหลัง — ถ้าเพิ่มปุ่มที่สอง gpio.button(1) ให้ปุ่มหนึ่งเลื่อนไปข้างหน้า อีกปุ่มถอยหลัง สูตร (state + 1) % 3 จะกลายเป็นอะไร แล้วถอยหลังจาก GREEN (0) ต้องวนไปที่ RED (2) ยังไงไม่ให้ติดลบ?
  • สะพานสู่คาบหน้า — คาบหน้าเราจะขยับจาก "ปุ่มดิจิทัลกด/ไม่กด" ไปเป็น จอยสติ๊กแบบแอนะล็อก ที่อ่านค่าเป็นช่วง ไม่ใช่แค่ 0/1 ลองคิดล่วงหน้า: ถ้า input ไม่ใช่ True/False แล้ว แต่เป็นตัวเลข -100 ถึง 100 แนวคิด edge detection ของวันนี้จะยังใช้ได้ไหม หรือเราต้องคิดเรื่อง "เกินเส้น threshold" แทน?

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

fit-css

← Roadmap (TOC)