ท่าพื้นฐานที่สุด: ติดครึ่งวินาที ดับครึ่งวินาที วนลูป 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:
time.sleep_ms(500) สองบรรทัดที่ :39-42 คือ กับ — เท่ากันจึงได้ 50% พอดีoff เป็น sleep_ms(100) (ติดนานกว่าดับ) duty ขยับเป็น ~83% ไฟจะดู "สว่างค้าง" มากขึ้น
ลองเปลี่ยน
500เป็น1000ดูสิ ไฟติดค้างนานขึ้น (คาบยาวขึ้น = ความถี่ต่ำลง)
โค้ดเหมือนท่า 1 แต่ เปลี่ยนแค่ตัวเลขเวลา จาก 500 เหลือ 100 — จังหวะเปลี่ยนทันที:
# --- ท่าที่ 2: กะพริบเร็ว 5 เท่า (25 ครั้ง ~ 5 วินาที) ---
for i in range(25):
led.toggle() # สลับสถานะ ติด<->ดับ ในบรรทัดเดียว
time.sleep_ms(100)
led.off() # จบท่าแล้วเคลียร์ให้ดับไว้ก่อน
toggle() สลับค่าปัจจุบันหนึ่งที (:48-52) — สลับสองครั้งจึงครบหนึ่งคาบ (ติด + ดับ)led.off() หลังลูป กันไฟค้างติดตอนจบท่า
เทียบท่า 1 กับ 2: ตรรกะเดียวกันเป๊ะ ต่างแค่ค่าเวลา — นี่คือพลังของการแยก "ค่า" ออกจาก "โครงโค้ด"
จังหวะไม่จำเป็นต้องสม่ำเสมอ: ตุบสองครั้งติดกัน แล้วเว้นยาว เหมือนชีพจร — ทำ 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) คือช่วง "พัก" ที่ทำให้ได้ยินเป็นจังหวะหัวใจ ไม่ใช่กะพริบสม่ำเสมอจังหวะก็เป็นภาษาแบบหนึ่ง: สั้น-สั้น-เว้นยาว สื่อความหมายได้ เหมือนที่ท่า 4 จะใช้ส่งรหัส SOS
สั้นสามครั้ง = 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) # เว้นก่อนส่งซ้ำ
:74 คือคาบ "ติด" ที่ แปรค่า: จุดสั้น (dot) = 150 ms, ขีดยาว (dash) = 500 mslength_ms ที่วิ่งวนจาก tuple (:74-76) คือ ของแต่ละสัญลักษณ์มอร์สมอร์สคือการเข้ารหัสข้อมูลด้วย "ความยาวของการติด" ล้วน ๆ — รากฐานเดียวกับการมอดูเลตสัญญาณดิจิทัล
คราวนี้ใช้ 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) ทุกดวง ตามจำนวน nfor 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 ท่า:
led.on() / led.off() · ท่า 2 — led.toggle() · ท่า 3 — time.sleep_ms(700) (ช่วงพัก)time.sleep_ms(length_ms) (หน่วงตามค่ามอร์ส) · ท่า 5 — gpio.led(i).on() / gpio.led(i).off()
ขั้นตอน:
pass ทีละจุด แล้วเติมคำสั่งตามคำใบ้ในคอมเมนต์ (# เติม:)ไฟ LED ดวงเดียวที่กะพริบ ซ่อนรากฐานวิศวกรรมหลายชั้นที่เราจะใช้ไปตลอดคอร์ส:
ฝั่ง Embedded / ฮาร์ดแวร์
gpio.led(0/1/2) ถูกแมปไว้กับขาจริงของชิป PSoC Edge E84 ให้แล้วฝั่ง Python
.on() / .off() / .toggle() คือการเรียกฟังก์ชันสั่งงานfor คือหัวใจของ blink และไฟวิ่ง (ทำซ้ำ + หน่วงเวลา)
ดูคลิป (เสริม): บอร์ดคนละรุ่น แต่แนวคิด MicroPython + GPIO + blink เหมือนกับที่เราทำเป๊ะ
Raspberry Pi Pico using MicroPython - Tutorial #1: Blinking an LED (Digital Outputs) — Robocraze
งานทำเอง (ส่งท้ายคาบ):
practise_codes/s01_led_blink.py↗ ให้ครบทั้ง 5 ท่า (2 รอบ) รันได้จริงบนบอร์ดsleep_ms ในท่า 5 ให้ไฟวิ่ง เร็วขึ้น และ ช้าลง อย่างละแบบใบ้ข้อ 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 เลือกจุดเริ่มให้เหมาะกับงานfor i in range(n): gpio.led(i).off() สำคัญกว่าที่คิด มันไล่ดับ ทุกดวง ไม่ใช่แค่ดวงที่เราเล่น เพราะท่า 5 จุดไฟหลายดวง ถ้าจบโปรแกรมทิ้งไว้เฉย ๆ อาจมีไฟค้างติดจนกว่าจะรีบูต การเก็บกวาดให้ฮาร์ดแวร์กลับสู่ สถานะที่รู้แน่ (known state) คือนิสัยของงาน embedded จริง: ออกจากงานยังไง ทิ้งเครื่องไว้ให้เรียบร้อยแบบนั้นstatus.text("จบแล้ว เก่งมาก") เป็นการปิด loop การสื่อสารกับคนดู — เริ่มด้วย "กำลังเริ่ม..." จบด้วย "จบแล้ว" คนดูรู้ตลอดว่าโปรแกรมอยู่ตรงไหนอยากให้จับหลักนี้ไว้: โปรแกรมที่ดีไม่ได้จบตอนงานหลักเสร็จ แต่จบตอน เก็บกวาดเรียบร้อยและบอกผลชัดเจน แล้ว
ลำดับของห้าท่าถูกจัดมาให้ เพิ่มแนวคิดใหม่ทีละอย่าง โดยยังใช้โครงเดิมที่เพิ่งเรียนไป นี่คือวิธีเดียวกับที่เราจะค่อย ๆ ต่อเกมทั้งเครื่องขึ้นมา:
| ท่า | บรรทัด | แนวคิดใหม่ที่เพิ่มเข้ามา | ของเดิมที่เอากลับมาใช้ |
|---|---|---|---|
| 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 |
gpio.led(1) ดวงเดียว เป็น gpio.led(i) ที่วิ่งครบทุกดวงถ้าจับจังหวะการไต่ระดับนี้ได้ น้องจะเห็นว่าเกมทั้งเกมก็สร้างแบบเดียวกัน: เริ่มจากของเล็กที่สุดที่รันได้ แล้วเติมทีละแนวคิด อย่ากระโดดไปเขียนทั้งเกมรวดเดียว
อยากให้หยุดคิดสักครู่ก่อนปิดคาบ ไม่ใช่เรื่องโค้ด แต่เรื่อง "มองให้ทะลุ" ว่าไฟกะพริบดวงเดียวที่เราเพิ่งทำ จริง ๆ แล้วมันคือชิ้นส่วนของอะไร
ที่มา — ไฟดวงนี้ยืนอยู่บนอะไร ย้อนกลับไปช่วงต้นคาบ เราคุยเรื่อง Pong ปี 1972 ที่ยังไม่มี CPU เลย เกมทั้งเกมเก็บ state ด้วย flip-flop ที่ต่อสายค้างค่าไว้ทีละบิต ทีนี้ลองถามตัวเองดู: led.toggle() ที่สลับไฟ 10 ในท่า 2 มันต่างจาก flip-flop ที่ค้างค่า 1 บิตตรงไหน? คำตอบคือ แนวคิดเดียวกันเป๊ะ ต่างแค่สมัยก่อนต้องต่อสาย วันนี้เราแก้ด้วยการพิมพ์โค้ด นี่คือเหตุผลที่เราเริ่มคอร์สด้วยเรื่องนี้ — ไม่ใช่เพราะไฟสวย แต่เพราะมันคือรากที่ลึกที่สุดของคอมพิวเตอร์: การจำและสลับสถานะ
ที่ไป — ไฟดวงนี้จะโตเป็นอะไร ตอนต้นคาบเราเห็นสไลด์เรื่องสองคอร์ M33/M55 คุยกันผ่าน IPC แล้วอาจารย์บอกว่า game.run() คือ Python รันบน M33 ส่งภาพให้ M55 วาด ลองสังเกตดี ๆ: โครงของท่า 5 คือ วน → ทำบางอย่างกับของทุกชิ้น → หน่วงเวลา → วนอีก — โครงนี้ชื่อจริงของมันคือ game loop และมันไม่เคยหายไปไหนเลยตลอดคอร์ส เกมทุกเกมที่เราจะเขียนก็คือลูปนี้ที่โตขึ้น
ลองตอบคำถามพวกนี้ในใจ นี่แหละคือการเชื่อมจุดด้วยตัวเอง:
time.sleep_ms(500) ที่คุมจังหวะกะพริบได้ไหม ถ้าวันหน้าเราวาดตัวละครใหม่ทุก ๆ 16 ms แทนที่จะกะพริบไฟ นั่นเรียกว่าอะไร? (ใบ้: 1000/16 ≈ 60 …)for i in range(n) กับไฟทุกดวง ถ้าเปลี่ยน "ไฟทุกดวง" เป็น "ศัตรูทุกตัวในเกม" โค้ดจะหน้าตาเปลี่ยนไปมากไหม?ถ้าตอบสามคำถามข้างบนได้ว่า "อ๋อ มันคือเรื่องเดียวกัน" — นั่นคือการหยั่งรู้ที่อาจารย์อยากให้เกิด ของที่ดูเล็กวันนี้ไม่ได้เล็ก มันคือเมล็ดของทั้งเครื่องเกม เราแค่ยังไม่ได้รดน้ำมัน
เทคนิควันนี้ไม่ใช่แบบฝึกหัดลอย ๆ มันคือสิ่งที่เฟิร์มแวร์จริงใช้กันทุกวัน:
เห็นไหมว่าห้าท่าที่เราเล่นวันนี้ ไม่มีท่าไหนเป็นของสมมติเลย ทุกท่ามีเครื่องจริงในโลกที่ทำงานด้วยหลักการเดียวกันอยู่
ลองเอาโจทย์พวกนี้ไปคิดต่อ ไม่มีคำตอบเดียวตายตัว ทุกข้อโยงกลับไปหาโปรเจกต์จริงได้:
time.sleep_ms หยุด ทุกอย่าง ระหว่างหน่วง แต่ในเกมจริงหยุดไม่ได้เพราะต้องรับปุ่มไปด้วย ลองคิดว่าจะ "จำเวลาที่ผ่านไป" แทนการ sleep ยังไง ให้ heartbeat ท่า 3 เต้นไปพร้อมกับเกมที่ยังเดินอยู่ — นี่คือ non-blocking timing ที่ game loop จริงต้องใช้อยากให้ลองเลือกมาสักข้อ แล้วเขียนลงใบงานว่า "ถ้าเป็นเรา จะออกแบบยังไง" ไม่ต้องมีคำตอบถูก ขอแค่คิดต่อจากของที่พิมพ์เองวันนี้ — ตรงนั้นแหละคือจุดที่น้องเริ่มเป็นวิศวกร ไม่ใช่แค่คนพิมพ์ตามเฉลย
fit-css