คาบ 1 — REPL + GPIO

ไฟ LED ดวงแรก

เปิดคอร์ส Fundamental of Embedded Systems Developer I

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

MicroPython บนบอร์ด PSoC Edge

ดูของจริงก่อน — การบูตบอร์ดครั้งแรก บนบอร์ด BENTO

เดโมรันบนบอร์ด BENTO จริง — โดย อ.วิรุฬห์ ศรีบริรักษ์ (YouTube)

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

วันนี้เราจะทำให้ LED บนบอร์ดติดด้วยมือเราเอง ทีละก้าว:

  1. รู้จัก REPL — ช่องคุยกับบอร์ดแบบโต้ตอบทันที
  2. ถามบอร์ดว่ามี LED กี่ดวง ด้วย gpio.num_leds()
  3. สั่ง LED ดวงแรก ติด / ดับ / สลับ ด้วย .on() .off() .toggle()
  4. ทำ ไฟกะพริบ (blink) ด้วย time.sleep_ms
  5. ต่อยอดเป็น ไฟวิ่ง knight-rider ข้าม LED ทุกดวง

ปลายทางของวันนี้ หน้าตาเป็นแบบนี้ — ไฟวิ่งกลับไปกลับมาเอง:

ปลายทาง: เปิดบอร์ดมาแล้วไฟวิ่งกลับไปกลับมาเอง จากโค้ดที่น้อง ๆ พิมพ์เอง

Embedded คืออะไร (สั้น ๆ ก่อนเริ่ม)

ปกติเราเขียนโปรแกรมให้ขึ้นบนจอคอม แต่ embedded คือเขียนโค้ดไปสั่ง ฮาร์ดแวร์จริง — ขาไฟ, ไฟ LED, เซนเซอร์, มอเตอร์

  • โค้ดของเราจะไปรันบน ไมโครคอนโทรลเลอร์ (บอร์ด PSoC Edge) ไม่ใช่บนเครื่องคอม
  • ภาษาที่เราใช้คือ MicroPython — Python รุ่นเล็กที่รันบนบอร์ดได้เลย
  • เสน่ห์ของมันคือ เห็นผลทันที — สั่งติด ไฟติดตรงหน้าเรา ไม่ต้องจินตนาการ

embedded อยู่รอบตัวเราอยู่แล้ว — เช่นแผงควบคุมในเครื่องซักผ้า ก็คือไมโครคอนโทรลเลอร์ที่ถูกสั่งด้วยโค้ดให้คุมมอเตอร์/วาล์ว/ไฟสถานะ:

ที่มา: "Washing machine power module" — Phiarc, CC BY-SA 4.0, Wikimedia Commons

อาจารย์อยากให้น้อง ๆ จับความรู้สึกนี้ให้ได้ตั้งแต่คาบแรก: โค้ด = การกระทำในโลกจริง

เข้าใจฮาร์ดแวร์ · ทัวร์บอร์ด BENTO

ก่อนสั่งโค้ด มารู้จักของจริงตรงหน้าก่อน — นี่คือ BENTO PSoC Edge บอร์ดที่เราจะใช้ทั้งคอร์ส

ทุกอย่างที่เราจะสั่งด้วยโค้ด มีตัวจริงอยู่บนแผ่นนี้ — LED · ปุ่ม · จอ · จอย/พอร์ต USB · ชิป WiFi · เซนเซอร์

รูปจากคู่มือ Infineon KIT_PSE84_EVAL · ลองหาตำแหน่ง LED กับปุ่ม USER บนบอร์ดจริงของกลุ่มเรา

เข้าใจฮาร์ดแวร์ · ทำไมบอร์ดมีหลาย "สมอง"

ในชิป PSoC Edge E84 มี CPU หลายตัว แต่ละตัวถนัดคนละงาน:

  • Cortex-M33 (200 MHz) — คอร์ควบคุม (ปุ่ม/WiFi/ความปลอดภัย) → โค้ด MicroPython รันที่นี่
  • Cortex-M55 (400 MHz) — คอร์แรง วาดภาพลงจอ + งาน AI/กราฟิก
  • สองคอร์คุยกันผ่าน "กล่องจดหมาย" ภายในชิป (IPC)

เวลาเราเขียน game.run() โค้ด Python รันบน M33 แล้วส่งภาพให้ M55 วาดลงจอ — แบ่งงานกันเกมจึงลื่น

เกร็ด: ทำไม embedded สำคัญ — Pong / Odyssey (1972)

เกมเครื่องแรกของโลก ไม่มี CPU เลย — Pong สร้างจาก logic gate (ชิป TTL 7400 ราว 66 ตัว) ต่อสายเป็นวงจร · state ของเกมอยู่ใน flip-flop ไม่ใช่ RAM

เชื่อมกับวันนี้: วันนี้น้องสั่ง LED ด้วย โค้ด ไม่กี่บรรทัด แทนการต่อสายทั้งวงจร — นั่นคือก้าวกระโดดของทั้งวงการ

ที่มา: Magnavox Odyssey · ภาพ Evan-Amos (public domain) · en.wikipedia.org/wiki/Magnavox_Odyssey

flip-flop จำ "1 บิต" ได้ยังไง — ความจำก่อนยุค RAM

ภาพเมื่อกี้คือ SR latch: เอา NAND 2 ตัวมา ป้อนกลับหากัน (cross-couple) — output ของตัวบนวิ่งไปเป็น input ของตัวล่าง และกลับกัน จนวงจร "ค้างค่า" ได้เอง

  • ป้อนกลับ → มี 2 สถานะนิ่ง (Q=0 หรือ Q=1) เหมือนสวิตช์ไฟที่ค้างอยู่ ไม่เด้งกลับเอง
  • กด Set → ค้างที่ 1 · กด Reset → ค้างที่ 0 · ไม่กดอะไร → จำค่าเดิม ← นี่คือ "ความจำ 1 บิต"
  • ความจำนี้เกิดจาก การต่อสาย ล้วน ๆ ไม่มี RAM ไม่มี CPU

Pong (1972) เก็บ state ทั้งเกมได้โดยไม่มี RAM เพราะใช้ flip-flop + counter ต่อสายมือแบบนี้ · วันนี้เราเก็บ state ใน RAM แล้วเปลี่ยนพฤติกรรมด้วยการ แก้โค้ด แทนแก้สาย

เตรียมเครื่องมือ — VSCode + flash บอร์ด + Git Cheat Sheet

เตรียมครั้งเดียว 2 อย่าง: ติดตั้ง VSCode (ฝั่งคอม) + flash BENTO firmware ลงบอร์ด (ฝั่งบอร์ด ให้พร้อม coding) แล้วใช้ Terminal ใน VSCode รันคำสั่ง Git — กดที่ภาพ cheat sheet เพื่อขยายดูรายละเอียด:

1) คลิปติดตั้ง VSCode (ทำตามครั้งเดียว)
2) คลิป flash BENTO Game Firmware ลงบอร์ด — เตรียมบอร์ดให้พร้อม coding (ทำครั้งเดียว)
กดที่ภาพเพื่อเปิดเต็มจอ (zoom) · เครดิต: อ.JoM

คำสั่ง Git ที่จะใช้บ่อย (หน้าถัดไปเราลงมือ fork → clone จริง):

git status                 # ดูว่ามีไฟล์อะไรเปลี่ยน
git add team.md            # เลือกไฟล์ที่จะบันทึก
git commit -m "first commit"
git push                   # ส่งขึ้น GitHub

วันนี้ขอแค่ clone + commit แรกให้ผ่าน — ส่วน branch / PR / CI เราจะใช้บ่อยขึ้นใน Block E (DevOps)

Onboarding — เครื่องมือ 2 ตัว (คนละหน้าที่)

ติดตั้ง VSCode + ดู Git Cheat Sheet หน้าที่แล้วเรียบร้อย — ทีนี้แยกหน้าที่เครื่องมือ 2 ตัวให้ชัด อย่าสับสนกัน:

VSCode — แก้โค้ด + ใช้ Git ผ่าน CLI (fork / clone / commit / push)
เครื่องมือฝั่งคอม สำหรับเก็บ/ส่งโค้ดขึ้น GitHub
BENTO IDEWeb App สำหรับ MicroPython
วางโค้ด → กด Program to Device ลงบอร์ด + พิมพ์ REPL
ไม่ใช่ที่เปิด repo — ใช้รันโค้ดลงบอร์ดเท่านั้น

เตรียมที่เก็บโค้ดของทีม — ทำใน VSCode:

  1. เข้า repo คอร์สบน GitHub กด Fork เข้าบัญชีตัวเอง
  2. เปิด VSCode → เปิด Terminal (Terminal → New Terminal) → clone:
git clone https://github.com/<ชื่อบัญชีของเรา>/game-development-on-embedded-systems.git
cd game-development-on-embedded-systems
  1. เปิดโฟลเดอร์นี้ใน VSCode (File → Open Folder) แล้วรันคำสั่ง Git จาก Terminal ใน VSCode
  2. เสียบบอร์ด PSoC Edge เข้าคอมด้วยสาย USB → เปิด BENTO IDE ไว้รันโค้ด MicroPython ลงบอร์ด

หน้าตา BENTO IDE (web app) — เขียน MicroPython → กด Program to Device → ดูผล/พิมพ์คำสั่งใน Terminal (REPL) · กดที่ภาพเพื่อขยาย:

ถ้าใครติดขั้นตอนไหน ยกมือเลย อาจารย์กับเพื่อนช่วยกันได้ ไม่ต้องเกร็ง

REPL — ช่องคุยกับบอร์ดแบบสด ๆ

REPL ย่อมาจาก Read–Eval–Print–Loop: พิมพ์ทีละบรรทัด บอร์ดตอบกลับทันที

ใน BENTO IDE จะมีช่อง REPL ให้พิมพ์โต้ตอบ ลองพิมพ์ดู:

>>> import gpio
>>> gpio.num_leds()
3
>>> gpio.led(0).on()
  • พอกด Enter บรรทัด gpio.led(0).on() ไฟดวงแรกจะติดทันที
  • พิมพ์ gpio.led(0).off() ไฟดับ

หน้าจอ REPL ใน BENTO IDE หลังพิมพ์ครบ จะหน้าตาแบบนี้ — ตรงกับที่น้องเห็นบนเครื่อง:

ลองดูจังหวะพิมพ์จริง — พิมพ์ทีละบรรทัด พอถึง .on() ไฟติดทันที (วงรอบ Read–Eval–Print):

REPL เหมาะกับการ "ลองเล่น" ทีละคำสั่ง ส่วนโค้ดยาว ๆ เราเก็บเป็นไฟล์ .py แล้วสั่งรันทั้งไฟล์

รู้จักโมดูล gpio

โมดูล gpio จัดการขา GPIO ที่ต่อกับ LED ให้เราเป็น output เรียบร้อยแล้ว เราแค่สั่งงาน

คำสั่ง ทำอะไร
gpio.num_leds() คืนจำนวน LED ที่บอร์ดมี
gpio.board_info() บอกข้อมูลบอร์ดที่เราใช้อยู่
gpio.led(n) คืน object ของ LED ดวงที่ n (เริ่มนับจาก 0)
.on() สั่งให้ติด
.off() สั่งให้ดับ
.toggle() สลับสถานะ — ติดอยู่ก็ดับ ดับอยู่ก็ติด

นับ index จาก 0 เสมอ ดวงแรกคือ gpio.led(0) ไม่ใช่ gpio.led(1)

เข้าใจฮาร์ดแวร์ · led.on() ทำอะไรจริง ๆ ในวงจร

เวลาเราสั่ง gpio.led(0).on() ในวงจรเกิดอะไรขึ้น:

  • GPIO = ขาหนึ่งเส้นที่ชิปสั่งได้ว่าจะ "ปล่อยไฟ" (HIGH ≈ 3.3V) หรือ "ไม่ปล่อย" (LOW ≈ 0V)
  • .on() = คอร์ M33 เซ็ตบิตเดียวใน register → เปิดทรานซิสเตอร์ → ต่อขาเข้ากับไฟ → LED ติด
  • LED แต่ละดวงต่อ ตัวต้านทาน อนุกรมไว้บนบอร์ดแล้ว (แดง 220Ω, เขียว 200Ω) เพื่อจำกัดกระแสไม่ให้ LED พัง — กฎโอห์ม V = I × R

ไล่เหตุ-ผลทีละขั้นจาก .on() ถึงไฟติด:

ที่ led.on() ปลอดภัยก็เพราะตัวต้านทานนี้ — ถ้าต่อ LED ดิบ ๆ เข้าขาเองโดยไม่มีตัวต้านทาน LED จะไหม้

เข้าใจฮาร์ดแวร์ · ดิจิทัลมีแค่ 1 กับ 0

ขา GPIO เป็น ดิจิทัล — สั่งได้แค่สองค่า ไม่มีค่ากลาง: ปล่อยไฟเต็ม (HIGH) หรือไม่ปล่อยเลย (LOW)

  • .on() ดันขาขึ้น HIGH (3.3V) ค้างไว้ → LED ติดค้าง
  • .off() ดึงขาลง LOW (0V) → LED ดับ
  • .toggle() แค่สลับค่าปัจจุบัน 1↔0 — ถ้าเราสลับเร็ว ๆ ในลูป ก็เห็นเป็น "กะพริบ"

แรงดันที่ขา (square wave) เด้งขึ้น-ลงพร้อมกับไฟกะพริบ — HIGH = ติด, LOW = ดับ:

นี่คือเหตุผลที่ blink ทำได้ง่าย: ไฟกะพริบ = สลับ 1/0 ซ้ำ ๆ พร้อมหน่วงเวลาเล็กน้อยคั่น

เข้าใจฮาร์ดแวร์ · ตัว LED จริง + กฎโอห์ม

ซูมดู LED หนึ่งดวง: โดมพลาสติกใส ข้างในมีขั้วสองขั้ว (ขายาว = ขั้วบวก/anode, ขาสั้น = ลบ/cathode) ต่อออกมาเป็นขาโลหะสองเส้น

ที่มา: "Macro Led" — Timothée Cognard, CC BY-SA 4.0, Wikimedia Commons

ทำไมต้องมีตัวต้านทานคั่น? LED ยอมให้กระแสไหลเพิ่มเร็วมากเมื่อแรงดันเกินค่าจุดติด ถ้าไม่จำกัด กระแสจะพุ่งจน LED ไหม้ เราคุมด้วย กฎโอห์ม:

I=VsupplyVfR=3.3V2.0V220Ω5.9 mAI = \frac{V_{supply} - V_f}{R} = \frac{3.3\text{V} - 2.0\text{V}}{220\,\Omega} \approx 5.9\text{ mA}

VfV_f คือแรงดันตกคร่อม LED (สีแดง ≈ 2.0V) · 5.9 mA อยู่ต่ำกว่าพิกัด LED (≈ 20 mA) สบาย ๆ ไฟจึงสว่างพอดีและไม่พัง

กราฟ · ยิ่งตัวต้านทานน้อย กระแสยิ่งพุ่ง

จากสูตร I=(VsupplyVf)/RI = (V_{supply}-V_f)/R — กระแสแปรผกผันกับ RR ลองดูเป็นกราฟ:

  • จุดเขียว: ตัวต้านทาน 220Ω ที่บอร์ดเราใช้ → กระแส 5.9 mA ปลอดภัย
  • ถ้าลด R ลงมาก (เช่น 50Ω) กระแสทะลุ 20 mA เข้าโซนแดง → LED อายุสั้น/ไหม้
  • ถ้าเพิ่ม R มาก ๆ กระแสน้อยลง → ไฟหรี่ลง

วิศวกรเลือกค่า R เพื่อให้ "สว่างพอ แต่ไม่เกินพิกัด" — นี่คือการออกแบบวงจรจริงที่ซ่อนอยู่หลังคำสั่ง .on() บรรทัดเดียว

วิธีรันโค้ดทั้งไฟล์ใน BENTO IDE

โค้ดยาว ๆ เราไม่พิมพ์ทีละบรรทัดใน REPL แต่เก็บเป็นไฟล์แล้วสั่งรันทั้งไฟล์:

  1. เปิดไฟล์ .py ที่จะรันใน BENTO IDE
  2. กดปุ่ม Program to Device
  3. โค้ดทั้งไฟล์จะถูกส่งไปรันบนบอร์ด แล้วผลจะโผล่ที่ REPL

ปุ่มจริงอยู่มุมซ้ายบนของ BENTO IDE — ปุ่มเขียว Program to Device:

จำง่าย ๆ: ลองเล่นทีละคำสั่ง → REPL / รันทั้งโปรแกรม → Program to Device

โครงของไฟล์ที่เราจะรันวันนี้ (ภาพรวม)

ไฟล์ practise_codes/s01_led_blink.py โชว์ LED 2 รอบ รอบละ 5 ท่า — แต่ละท่ากะพริบคนละจังหวะ:

  • ท่า 1 — กะพริบช้า 1 Hz (.on() / .off() หน่วง 500 ms)
  • ท่า 2 — กะพริบเร็ว 5 Hz ด้วย .toggle()
  • ท่า 3 — จังหวะหัวใจเต้น (ตุบ-ตุบ...พัก)
  • ท่า 4 — รหัส SOS แบบมอร์ส
  • ท่า 5 — ไฟวิ่ง knight-rider ข้าม LED ทุกดวง

เริ่มจากถามบอร์ดว่ามี LED กี่ดวง ปักป้ายบนจอ Playground แล้วค่อยเข้าลูป 2 รอบ:

import gpio
import time
import ui

n = gpio.num_leds()                 # ถามบอร์ดว่ามี LED กี่ดวง (จะได้ไม่สั่งเกินจริง)
print("บอร์ดนี้มี LED ทั้งหมด", n, "ดวง")

ui.clear()                          # เคลียร์จอ Playground แล้วปักป้ายบอกให้มองตรงไหน
ui.Label("มองไฟ LED เล็ก ๆ ใต้จอนี้", x=190, y=70, color=0xFFFFFF, value=28)
status = ui.Label("กำลังเริ่ม...", x=190, y=150, color=0x00FF88, value=24)
hint   = ui.Label("", x=190, y=210, color=0xAAAAAA, value=20)

led = gpio.led(1)                   # เล่นกับ LED2 (เขียว, index 1)
for round_no in range(1, 3):        # โชว์ 2 รอบ (ท่า 1..5 เรียงกันในแต่ละรอบ)
    ...

เราใช้ LED2 (เขียว, index 1) เพราะ LED1 (แดง, index 0) มักถูกใช้เป็นไฟ status ของระบบ — เลี่ยงไว้ปลอดภัยกว่า

ภาพรวมทั้งโปรแกรม · ข้อมูลไหลไปทางไหน

ทั้งไฟล์อ่านได้เป็นประโยคเดียว: "ถามจำนวนไฟ → วน 2 รอบ → ในแต่ละรอบเล่น 5 ท่าเรียงกัน → จบแล้วดับไฟทุกดวง"

เริ่ม num_leds + ui :14-27 วน 2 รอบ for round 1..2 :30 เล่น 5 ท่า ท่า 1 → 5 ครบ 2 รอบ? ดับไฟทุกดวง clear :99-100 ท่า 1 ช้า on/off 500ms :39-42 ท่า 2 เร็ว toggle 100ms :48-52 ท่า 3 หัวใจ heartbeat :59-65 ท่า 4 SOS morse :73-79 ท่า 5 ไฟวิ่ง knight-rider :86-96 ครบ ยังไม่ครบ → เล่นอีกหนึ่งรอบ แถวล่าง = รายละเอียดของกล่อง "เล่น 5 ท่า"

ท่า 1–4 เล่นกับ LED เขียวดวงเดียว (gpio.led(1)) ส่วนท่า 5 วิ่งข้าม ทุกดวง (gpio.led(i)) จบทั้งสองรอบแล้วจึงเคลียร์ไฟดับหมด (:99-100)

ท่า 1 — กะพริบช้า 1 Hz + รู้จัก duty cycle

ท่าพื้นฐานที่สุด: ติดครึ่งวินาที ดับครึ่งวินาที วนลูป 10 ครั้ง (~10 วินาที):

# --- ท่าที่ 1: กะพริบช้า 1 Hz (10 ครั้ง ~ 10 วินาที) ---
for i in range(10):
    led.on()
    time.sleep_ms(500)      # ติดค้าง 500 ms
    led.off()
    time.sleep_ms(500)      # ดับค้าง 500 ms

duty cycle = สัดส่วนของ "เวลาที่ไฟติด" ต่อ "หนึ่งคาบเต็ม" ท่านี้ติด 500 ดับ 500 → duty = 50% ที่ความถี่ 1 Hz:

duty=tonton+toff=500500+500=50%f=11 s=1 Hz\text{duty} = \frac{t_{on}}{t_{on}+t_{off}} = \frac{500}{500+500} = 50\% \qquad f = \frac{1}{1\text{ s}} = 1\text{ Hz}

  • ทฤษฎี duty cycle → โค้ด: time.sleep_ms(500) สองบรรทัดที่ :39-42 คือ tont_{on} กับ tofft_{off} — เท่ากันจึงได้ 50% พอดี
  • ถ้าลดเฉพาะ off เป็น sleep_ms(100) (ติดนานกว่าดับ) duty ขยับเป็น ~83% ไฟจะดู "สว่างค้าง" มากขึ้น

ลองเปลี่ยน 500 เป็น 1000 ดูสิ ไฟติดค้างนานขึ้น (คาบยาวขึ้น = ความถี่ต่ำลง)

ท่า 2 — กะพริบเร็ว 5 Hz ด้วย toggle()

โค้ดเหมือนท่า 1 แต่ เปลี่ยนแค่ตัวเลขเวลา จาก 500 เหลือ 100 — จังหวะเปลี่ยนทันที:

# --- ท่าที่ 2: กะพริบเร็ว 5 เท่า (25 ครั้ง ~ 5 วินาที) ---
for i in range(25):
    led.toggle()            # สลับสถานะ ติด<->ดับ ในบรรทัดเดียว
    time.sleep_ms(100)
led.off()                   # จบท่าแล้วเคลียร์ให้ดับไว้ก่อน
  • toggle() สลับค่าปัจจุบันหนึ่งที (:48-52) — สลับสองครั้งจึงครบหนึ่งคาบ (ติด + ดับ)
  • หน่วง 100 ms ต่อการสลับ → ครบคาบ 200 ms → 5 Hz เร็วกว่าท่า 1 ห้าเท่า
  • led.off() หลังลูป กันไฟค้างติดตอนจบท่า

เทียบท่า 1 กับ 2: ตรรกะเดียวกันเป๊ะ ต่างแค่ค่าเวลา — นี่คือพลังของการแยก "ค่า" ออกจาก "โครงโค้ด"

ท่า 3 — จังหวะหัวใจเต้น (ลูปซ้อนลูป)

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

# --- ท่าที่ 3: heartbeat ตุบ-ตุบ...พัก (8 ครั้ง) ---
for i in range(8):
    for beat in range(2):        # ตุบสองครั้งติดกัน
        led.on()
        time.sleep_ms(120)
        led.off()
        time.sleep_ms(120)
    time.sleep_ms(700)           # แล้วพักยาวหนึ่งช่วง
  • ลูปนอก (:59) นับ 8 เที่ยวชีพจร · ลูปใน (:60-64) ตุบเร็ว ๆ 2 ครั้ง
  • sleep_ms(700) หลังลูปใน (:65) คือช่วง "พัก" ที่ทำให้ได้ยินเป็นจังหวะหัวใจ ไม่ใช่กะพริบสม่ำเสมอ
  • นี่คือ ลูปซ้อนลูป ครั้งแรกของเรา — โครงเดียวกับไฟวิ่งในท่า 5

จังหวะก็เป็นภาษาแบบหนึ่ง: สั้น-สั้น-เว้นยาว สื่อความหมายได้ เหมือนที่ท่า 4 จะใช้ส่งรหัส SOS

ท่า 4 — รหัส SOS แบบมอร์ส (duty แปรค่า)

สั้นสามครั้ง = S, ยาวสามครั้ง = O, สั้นสามครั้ง = S — ไฟดวงเดียวก็ส่งข้อความขอความช่วยเหลือได้:

# --- ท่าที่ 4: SOS  ... --- ...  (2 เที่ยว) ---
for i in range(2):
    for length_ms in (150, 150, 150, 500, 500, 500, 150, 150, 150):
        led.on()
        time.sleep_ms(length_ms)    # เวลา "ติด" แปรตามค่าใน tuple
        led.off()
        time.sleep_ms(200)          # เวลา "ดับ" คงที่ 200
    time.sleep_ms(800)              # เว้นก่อนส่งซ้ำ
  • tuple ที่ :74 คือคาบ "ติด" ที่ แปรค่า: จุดสั้น (dot) = 150 ms, ขีดยาว (dash) = 500 ms
  • นี่คือ variable duty: ต่างจากท่า 1 ที่ tont_{on} คงที่ ท่านี้ tont_{on} เปลี่ยนทุกก้าว (ส่วน tofft_{off} คงที่ 200) — ยาว/สั้นที่ตาเห็นคือ duty ที่มากขึ้น/น้อยลง
  • ทฤษฎี → โค้ด: ตัวแปร length_ms ที่วิ่งวนจาก tuple (:74-76) คือ tont_{on} ของแต่ละสัญลักษณ์มอร์ส

มอร์สคือการเข้ารหัสข้อมูลด้วย "ความยาวของการติด" ล้วน ๆ — รากฐานเดียวกับการมอดูเลตสัญญาณดิจิทัล

ท่า 5 — ไฟวิ่ง knight-rider ข้ามทุกดวง

คราวนี้ใช้ LED ทุกดวง: วิ่งไปสุดแล้ววิ่งย้อนกลับ ทำซ้ำ 6 เที่ยว:

# --- ท่าที่ 5: ไฟวิ่ง knight-rider ข้าม LED ทุกดวง ---
for sweep in range(6):
    for i in range(n):              # ไปข้างหน้า: 0, 1, 2, ...
        gpio.led(i).on()
        time.sleep_ms(120)
        gpio.led(i).off()
    for i in range(n - 2, 0, -1):   # ย้อนกลับ: ..., 2, 1 (ข้ามหัวท้ายกันจังหวะซ้ำ)
        gpio.led(i).on()
        time.sleep_ms(120)
        gpio.led(i).off()

  • ท่านี้เปลี่ยนจาก gpio.led(1) ดวงเดียว มาเป็น gpio.led(i) ทุกดวง ตามจำนวน n
  • for sweep in range(6) (:86) = วิ่งไป-กลับ 6 เที่ยว · ลูปในสองอันคือ "ขาไป" กับ "ขากลับ"

นี่คือ ลูปซ้อนลูป อีกครั้ง — ลูปนอกนับรอบ sweep ลูปในวิ่งไฟทีละดวง

อ่านลูปย้อนกลับให้ขาด

range(n - 2, 0, -1) คือหัวใจของขาวิ่งกลับ ลองแกะที่ละตัว:

  • เริ่มที่ n - 2 — ข้ามดวงสุดท้าย (n - 1) เพราะขาไปเพิ่งจุดมันค้างไว้แล้ว
  • ไปจนถึง (ไม่รวม) 0 — หยุดก่อนถึงดวงแรก กันค้างซ้ำจังหวะตอนเริ่ม sweep ใหม่
  • -1 = นับถอยหลังทีละ 1

ลองดูตัวอย่างบอร์ด 6 ดวงrange(n-2, 0, -1) คือ 4,3,2,1 วิ่งถอยหลัง (ข้ามดวง 5 และดวง 0):

ถ้าบอร์ดมี LED 3 ดวง ขากลับจะวิ่งแค่ดวง index 1 ดวงเดียว ลองวาดบนกระดาษดูแล้วจะเห็นภาพ

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

เปิด practise_codes/s01_led_blink.py ขึ้นมา ในไฟล์มี pass วางไว้ ทั้ง 5 ท่า ตรงจุดที่เราต้องเติมคำสั่งจริง เช่นท่า 1:

# --- ท่าที่ 1: กะพริบช้า 1 Hz ---
led = gpio.led(1)                       # LED2 เขียว (index 1)
for i in range(10):
    # เติม: สั่งให้ LED ติด ด้วย led.on()
    pass
    time.sleep_ms(500)
    # เติม: สั่งให้ LED ดับ ด้วย led.off()
    pass
    time.sleep_ms(500)

จุด pass ที่ต้องแทนด้วยคำสั่งจริง กระจายอยู่ครบทั้ง 5 ท่า:

  • ท่า 1led.on() / led.off() · ท่า 2led.toggle() · ท่า 3time.sleep_ms(700) (ช่วงพัก)
  • ท่า 4time.sleep_ms(length_ms) (หน่วงตามค่ามอร์ส) · ท่า 5gpio.led(i).on() / gpio.led(i).off()

ขั้นตอน:

  1. ไล่หา pass ทีละจุด แล้วเติมคำสั่งตามคำใบ้ในคอมเมนต์ (# เติม:)
  2. เปิดไฟล์ใน BENTO IDE กด Program to Device
  3. ดูไฟบนบอร์ด ถ้ายังไม่ติดตามคาด กลับมาเช็ก indent กับชื่อคำสั่ง

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

ไฟ LED ดวงเดียวที่กะพริบ ซ่อนรากฐานวิศวกรรมหลายชั้นที่เราจะใช้ไปตลอดคอร์ส:

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

  • GPIO เป็น digital output — ขาที่สั่งได้แค่ HIGH/LOW (3.3V/0V); ทุกการสั่งฮาร์ดแวร์เริ่มจากตรงนี้
  • Pin mappinggpio.led(0/1/2) ถูกแมปไว้กับขาจริงของชิป PSoC Edge E84 ให้แล้ว
  • Current-limiting — ตัวต้านทานอนุกรม + กฎโอห์ม คือการออกแบบวงจรที่กันของพัง

ฝั่ง Python

  • function.on() / .off() / .toggle() คือการเรียกฟังก์ชันสั่งงาน
  • loopfor คือหัวใจของ blink และไฟวิ่ง (ทำซ้ำ + หน่วงเวลา)
  • REPL — read–eval–print loop: พิมพ์ → รัน → เห็นผล วนไป คือวิธีลองของที่เร็วที่สุด

ดูคลิป (เสริม): บอร์ดคนละรุ่น แต่แนวคิด MicroPython + GPIO + blink เหมือนกับที่เราทำเป๊ะ

Raspberry Pi Pico using MicroPython - Tutorial #1: Blinking an LED (Digital Outputs) — Robocraze

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

งานทำเอง (ส่งท้ายคาบ):

  1. เติม practise_codes/s01_led_blink.py ให้ครบทั้ง 5 ท่า (2 รอบ) รันได้จริงบนบอร์ด
  2. ปรับ sleep_ms ในท่า 5 ให้ไฟวิ่ง เร็วขึ้น และ ช้าลง อย่างละแบบ
  3. ท้าทาย: เพิ่มจำนวน sweep หรือแต่งจังหวะมอร์สในท่า 4 ให้เป็นตัวอักษรอื่น (เช่น รหัสชื่อทีม)

ใบ้ข้อ 2 — sleep_ms น้อย = ไฟวิ่งเร็ว, มาก = ไฟวิ่งช้า:

ลองเขียนเองให้สุดก่อนนะ จะได้ของจริง — เดี๋ยวเฉลยพร้อมกันในห้องเรียน

วันนี้เราได้: คุยกับบอร์ดผ่าน REPL · สั่ง GPIO ติด/ดับ/สลับ · ทำ blink และไฟวิ่งด้วย sleep_ms และลูป

คาบหน้าเราจะรับ input จากบอร์ดบ้าง แล้วเริ่มเอามาคุมไฟ — เจอกันครับ

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

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

ส่วนแรก — ถามบอร์ดก่อนสั่ง:

import gpio
import time
import ui

# ก่อนอื่นถามบอร์ดดูว่ามี LED ให้เล่นกี่ดวง จะได้ไม่สั่งเกินจำนวนจริง
n = gpio.num_leds()
print("บอร์ดนี้มี LED ทั้งหมด", n, "ดวง")
print("ข้อมูลบอร์ด:", gpio.board_info())
  • ทำไมต้อง gpio.num_leds() ก่อน แทนที่จะเขียนเลข 3 ลงไปตรง ๆ? เพราะเราเก็บจำนวนจริงไว้ในตัวแปร n แล้วท่า 5 (knight-rider) จะวิ่ง range(n) ตามจำนวนจริง ถ้าเรา hard-code เลขไว้ผิด บอร์ดที่มีไฟไม่เท่ากันจะ สั่งเกิน index จนโปรแกรมพัง หรือข้ามไฟบางดวง นี่คือนิสัย "ถามฮาร์ดแวร์ก่อน อย่าเดา" ที่ตรงกับตาราง gpio ที่เราดูไปแล้ว
  • gpio.board_info() เป็นการ introspect — ให้บอร์ดบอกว่าตัวเองเป็นรุ่นอะไร เวลาโค้ดไปรันบนบอร์ดของกลุ่มอื่น เราจะรู้ทันทีว่ากำลังคุยกับเครื่องตัวไหน

import สามบรรทัดบนสุดคือการบอกว่าวันนี้เราจะใช้ของสามอย่าง: gpio คุมไฟ · time คุมจังหวะ · ui เขียนป้ายบนจอ เท่านี้พอสำหรับทั้งไฟล์

เฉลย · ส่วนที่สอง — ป้ายบนจอ กับการเลือกดวงไฟ

ui.clear()
ui.Label("มองไฟ LED เล็ก ๆ ใต้จอนี้", x=190, y=70, color=0xFFFFFF, value=28)
status = ui.Label("กำลังเริ่ม...", x=190, y=150, color=0x00FF88, value=24)
hint = ui.Label("", x=190, y=210, color=0xAAAAAA, value=20)

led = gpio.led(1)

โปรแกรมนี้มี ช่องบอกผลสองช่อง ทำงานคู่กัน: ตัว LED จริงบนบอร์ด กับป้ายบนจอ ที่ต้องมีป้ายด้วยเพราะไฟดวงเดียวเล็กมาก คนดูอาจไม่รู้ว่ากำลังดูท่าอะไรอยู่ — ป้ายบนจอเลยทำหน้าที่ "คำบรรยาย" ให้

  • ป้ายแรก (ui.Label เฉย ๆ) เป็นหัวข้อคงที่ ไม่ต้องแก้ทีหลัง เราเลยไม่เก็บลงตัวแปร
  • status กับ hint เรา เก็บลงตัวแปร เพราะทุกท่าจะเรียก status.text(...) เปลี่ยนข้อความ ถ้าไม่เก็บ object ไว้ก็อ้างถึงมันทีหลังไม่ได้ นี่คือเหตุผลว่าทำไมบางป้ายมีชื่อ บางป้ายไม่มี — ขึ้นกับว่า "ต้องแก้ทีหลังไหม"
  • color=0x00FF88 (เขียว) กับ 0xAAAAAA (เทา) คือรหัสสี RGB แบบเลขฐานสิบหก — status ใช้สีสด hint ใช้สีจาง เพราะ status สำคัญกว่า
  • led = gpio.led(1) เลือก LED2 เขียว (index 1) ไม่ใช่ดวง 0 ด้วยเหตุผลเดิมที่เราคุยกันไปแล้ว: ดวง 0 (แดง) มักถูกใช้เป็นไฟ status ของระบบ เลี่ยงไว้ปลอดภัยกว่า และเราเก็บ object ไว้ในตัวแปร led ครั้งเดียว แล้วใช้ซ้ำทั้งท่า 1–4 ไม่ต้องเรียก gpio.led(1) ใหม่ทุกครั้ง

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

เฉลย · ส่วนที่สาม — วนสองรอบ แล้วเก็บกวาดให้เรียบร้อย

for round_no in range(1, 3):        # โชว์ 2 รอบ กันคนดูหาไฟไม่ทันรอบแรก
    print("=== รอบที่", round_no, "/ 2 ===")
    # ... ท่า 1 ถึง 5 เรียงกันอยู่ในนี้ ...

# จบการแสดง เคลียร์ไฟให้ดับหมดทุกดวง
for i in range(n):
    gpio.led(i).off()

status.text("จบแล้ว เก่งมาก")
  • range(1, 3) ได้ round_no เป็น 1 กับ 2 (ไม่รวม 3) — เราเริ่มนับที่ 1 ตรงนี้เพราะเอาไว้โชว์บนจอให้คนอ่านเข้าใจง่าย ("รอบที่ 1/2") ต่างจากการวน index ที่เริ่มที่ 0 เลือกจุดเริ่มให้เหมาะกับงาน
  • ทำไมต้องสองรอบ? เหตุผลคือ UX ล้วน ๆ รอบแรกคนดูมักยังหาตำแหน่งไฟไม่เจอ พอมีรอบสองจะได้ดูทัน โค้ดไม่ได้ซับซ้อนขึ้นเลย แค่เอาทุกอย่างมาห่อด้วยลูปนอกอีกชั้น
  • ลูปปิดท้าย for i in range(n): gpio.led(i).off() สำคัญกว่าที่คิด มันไล่ดับ ทุกดวง ไม่ใช่แค่ดวงที่เราเล่น เพราะท่า 5 จุดไฟหลายดวง ถ้าจบโปรแกรมทิ้งไว้เฉย ๆ อาจมีไฟค้างติดจนกว่าจะรีบูต การเก็บกวาดให้ฮาร์ดแวร์กลับสู่ สถานะที่รู้แน่ (known state) คือนิสัยของงาน embedded จริง: ออกจากงานยังไง ทิ้งเครื่องไว้ให้เรียบร้อยแบบนั้น
  • status.text("จบแล้ว เก่งมาก") เป็นการปิด loop การสื่อสารกับคนดู — เริ่มด้วย "กำลังเริ่ม..." จบด้วย "จบแล้ว" คนดูรู้ตลอดว่าโปรแกรมอยู่ตรงไหน

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

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

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

ท่า บรรทัด แนวคิดใหม่ที่เพิ่มเข้ามา ของเดิมที่เอากลับมาใช้
1 ช้า :38-42 pattern พื้นฐาน on → sleep → off → sleep + duty cycle for ลูป
2 เร็ว :49-52 เปลี่ยน ค่า โดยไม่แตะ โครง + toggle() ลูปเดิมของท่า 1
3 หัวใจ :59-65 ลูปซ้อนลูป + จังหวะไม่สม่ำเสมอ on/off/sleep ของท่า 1
4 SOS :73-79 ขับ pattern ด้วย ข้อมูล (tuple) แทนเลขตายตัว ลูปซ้อนลูปของท่า 3
5 ไฟวิ่ง :86-96 ขยายจาก 1 ดวง เป็น n ดวง + range ถอยหลัง ลูปซ้อนลูป + sleep
  • ท่า 1→2 สอนบทเรียนที่ลึกที่สุดของคาบนี้: แยก "ค่า" (500, 100) ออกจาก "โครงโค้ด" เปลี่ยนพฤติกรรมได้โดยไม่ต้องเขียนใหม่
  • ท่า 3→4 ยกระดับจาก "เขียนจังหวะด้วยมือ" ไปเป็น "ให้ข้อมูลใน tuple เป็นตัวขับ" — พอ pattern อยู่ในข้อมูล เราแก้ข้อความมอร์สได้โดยไม่แตะลูปเลย
  • ท่า 5 คือการ scale: โครงเดิมทั้งหมด แค่เปลี่ยน gpio.led(1) ดวงเดียว เป็น gpio.led(i) ที่วิ่งครบทุกดวง

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

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

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

ที่มา — ไฟดวงนี้ยืนอยู่บนอะไร ย้อนกลับไปช่วงต้นคาบ เราคุยเรื่อง Pong ปี 1972 ที่ยังไม่มี CPU เลย เกมทั้งเกมเก็บ state ด้วย flip-flop ที่ต่อสายค้างค่าไว้ทีละบิต ทีนี้ลองถามตัวเองดู: led.toggle() ที่สลับไฟ 1↔0 ในท่า 2 มันต่างจาก flip-flop ที่ค้างค่า 1 บิตตรงไหน? คำตอบคือ แนวคิดเดียวกันเป๊ะ ต่างแค่สมัยก่อนต้องต่อสาย วันนี้เราแก้ด้วยการพิมพ์โค้ด นี่คือเหตุผลที่เราเริ่มคอร์สด้วยเรื่องนี้ — ไม่ใช่เพราะไฟสวย แต่เพราะมันคือรากที่ลึกที่สุดของคอมพิวเตอร์: การจำและสลับสถานะ

ที่ไป — ไฟดวงนี้จะโตเป็นอะไร ตอนต้นคาบเราเห็นสไลด์เรื่องสองคอร์ M33/M55 คุยกันผ่าน IPC แล้วอาจารย์บอกว่า game.run() คือ Python รันบน M33 ส่งภาพให้ M55 วาด ลองสังเกตดี ๆ: โครงของท่า 5 คือ วน → ทำบางอย่างกับของทุกชิ้น → หน่วงเวลา → วนอีก — โครงนี้ชื่อจริงของมันคือ game loop และมันไม่เคยหายไปไหนเลยตลอดคอร์ส เกมทุกเกมที่เราจะเขียนก็คือลูปนี้ที่โตขึ้น

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

  • จำ time.sleep_ms(500) ที่คุมจังหวะกะพริบได้ไหม ถ้าวันหน้าเราวาดตัวละครใหม่ทุก ๆ 16 ms แทนที่จะกะพริบไฟ นั่นเรียกว่าอะไร? (ใบ้: 1000/16 ≈ 60 …)
  • ท่า 5 เราวน for i in range(n) กับไฟทุกดวง ถ้าเปลี่ยน "ไฟทุกดวง" เป็น "ศัตรูทุกตัวในเกม" โค้ดจะหน้าตาเปลี่ยนไปมากไหม?
  • ถ้าวันนี้เราคุม LED ดวงเดียวให้ติด/ดับตามใจได้ พรุ่งนี้เราจะคุม จุดสีบนจอ (pixel) แบบเดียวกันได้ไหม? จอก็คือ LED เล็ก ๆ หลายพันดวงต่อกันนั่นเอง
ที่มา flip-flop จำ 1 บิต (Pong 1972) วันนี้ blink = สลับ + จังหวะ + ลูป (LED ดวงเดียว) ที่ไป game loop คุมพิกเซล+input (ทั้งเกม)

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

ใช้จริงที่ไหน — GPIO output · blink · duty cycle

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

ไฟสถานะเครื่องใช้ไฟฟ้า ช้า standby เร็ว error Heartbeat / watchdog · 1 Hz liveness — ไฟหยุดเต้น = เฟิร์มแวร์ค้าง หรี่ไฟด้วย PWM · duty cycle 20% หรี่ 50% กลาง 90% สว่าง รหัสวินิจฉัย (blink codes) สั้น · สั้น · ยาว จำนวน + ความยาว = ข้อความ (เช่น error 3)
  • ไฟสถานะเครื่องใช้ไฟฟ้า — เราเตอร์ WiFi, เครื่องซักผ้า, ที่ชาร์จ ใช้ จังหวะกะพริบสื่อสถานะ: ติดนิ่ง = ปกติ, ช้า = standby/กำลังเชื่อมต่อ, เร็ว = error จังหวะเดียวกับท่า 1–2 แต่คนละความหมาย
  • Heartbeat / watchdog LED — ระบบ embedded จริงมักโยน LED ดวงหนึ่งให้กะพริบ 1 Hz อยู่เบื้องหลัง บอกว่า "ระบบยังไม่แฮงก์" (liveness) ถ้าไฟหยุดกะพริบ ช่างรู้ทันทีว่าเฟิร์มแวร์ค้าง นี่คือท่า 1 ที่ทำงานจริงในโรงงาน
  • หรี่ไฟด้วย PWM — ไฟหน้าปัดรถยนต์, แบ็คไลต์จอมือถือ ใช้หลัก duty cycle เดียวกับท่า 1 แต่สลับ on/off เร็วระดับกิโลเฮิรตซ์ จนตาเห็นเป็นความสว่างต่อเนื่อง duty มาก = สว่าง, duty น้อย = หรี่ blink ช้า ๆ ของเราคือ PWM ความถี่ต่ำ
  • รหัสวินิจฉัยด้วยการกะพริบ (blink codes) — ไฟ check-engine ในรถ, error code ตอนบูตเมนบอร์ด เข้ารหัสความผิดพลาดเป็น "จำนวนครั้งที่กะพริบ" ตรงกับท่า 4 (SOS/มอร์ส): ไฟดวงเดียวส่งข้อความได้ด้วยความยาว-จำนวนของการติด

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

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

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

แบบบล็อก — time.sleep_ms() toggle time.sleep_ms(500) — CPU หยุดนิ่ง toggle กดปุ่มช่วงนี้ → ตกหล่นหมด อ่านไม่ได้เลย เปลี่ยนวิธีคิด แบบไม่บล็อก — จำเวลาที่ผ่านไป ทุกวนรอบ: อ่านปุ่ม + วาดเกม — ไม่หยุดเลย toggle เมื่อครบ 500 ms ปุ่มกดตอนไหนก็รับได้ — เกมยังเดินอยู่
  • ออกแบบภาษาไฟ 5 สถานะ — ถ้าต้องใช้ LED ดวงเดียวสื่อ 5 สถานะ (พร้อม / ทำงาน / กำลังรอ / เตือน / พัง) จะออกแบบจังหวะแต่ละสถานะให้ แยกออกจากกันด้วยตาเปล่า ยังไง ลองเล่นกับสามตัวแปร: ความถี่, duty cycle, และจำนวนครั้งต่อชุด
  • LED ดวงเดียวส่งข้อมูลได้แค่ไหน — ถ้ามองว่า ติด = 1, ดับ = 0 ส่งทีละบิตทุก 100 ms จะได้กี่ bit ต่อวินาที? แล้วถ้าใช้ไฟ 3 ดวงส่งพร้อมกันล่ะ นี่คือคำถามเดียวกับที่วิศวกรสื่อสารต้องตอบ
  • ภาษาไฟของทีมเอง — ให้ไฟวิ่ง knight-rider (ท่า 5) เปลี่ยนทิศหรือความเร็วตามเหตุการณ์ เช่น วิ่งเร็วขึ้นเมื่อใกล้แพ้ จังหวะไฟกลายเป็นส่วนหนึ่งของการเล่นได้ไหม
  • จังหวะที่ไม่บล็อกเกม — ตอนนี้ time.sleep_ms หยุด ทุกอย่าง ระหว่างหน่วง แต่ในเกมจริงหยุดไม่ได้เพราะต้องรับปุ่มไปด้วย ลองคิดว่าจะ "จำเวลาที่ผ่านไป" แทนการ sleep ยังไง ให้ heartbeat ท่า 3 เต้นไปพร้อมกับเกมที่ยังเดินอยู่ — นี่คือ non-blocking timing ที่ game loop จริงต้องใช้

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

fit-css

← Roadmap (TOC)