
ทำไมน้ำเงินใช้ 2.4 kΩ ทั้งที่แดงใช้ 220 Ω — ลองแทนเลขในสูตรเดียวกัน (สมมติ ของ LED น้ำเงินราว 3.0 V) ดวงน้ำเงินได้กระแสกี่ mA และทำไมผู้ออกแบบยอมให้กระแสต่ำขนาดนั้น · ระวัง: สูงกว่าที่กระแสเท่ากัน ต้องใช้ R น้อยกว่า ไม่ใช่มากกว่า
โปรแกรมของเราไม่มีทางรู้เองว่ามีคนกดปุ่ม สิ่งที่ทำได้คือ ถามซ้ำ ๆ ให้ถี่พอ
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 ทั้งที่นิ้วกดครั้งเดียว · ภาพล่างคือ ของจริง จากออสซิลโลสโคป

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 จะรู้สึกว่าปุ่มหน่วง
อยากเห็นตัวเลขจริง เปิด 05_debounce_count.py แล้วลดค่า DEBOUNCE_MS ลงทีละ 10 จนเลขนับกันเด้งเริ่มเกินจำนวนครั้งที่กดจริง (ไล่เข้าหาเลขนับดิบ) นั่นคือจุดที่หน้าต่างเวลาสั้นเกินไปจนรับการเด้งตอนปล่อยมาเป็นการกดครั้งใหม่
การกันเด้งคือ การไม่รีบเชื่อข้อมูลที่เพิ่งมาถึง — แนวคิดเดียวกันนี้จะกลับมาอีกตอนกรองสัญญาณเซนเซอร์ในบทเรียน 2.7–2.9
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.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 · สองทางนี้ผิดคนละแบบ และทั้งคู่ชี้ไปที่คำตอบเดียวกัน
บทเรียนที่ใหญ่กว่ากับดักตัวมันเอง: โปรแกรมควรจำสถานะที่ตัวเองสั่งไว้เสมอ — หลอดชุดนี้อ่านกลับได้ แต่เอาต์พุตอีกหลายชนิดในโลกจริงสั่งได้อย่างเดียว (วาล์ว รีเลย์ มอเตอร์) โปรแกรมที่จำสถานะของตัวเองไว้ ย้ายไปคุมของพวกนั้นได้ทันที
# --- ท่าที่ 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 เงื่อนไขจะไม่มีวันเป็นจริง แล้วไฟจะไม่วิ่งเลย
เทียบกับ 02_led_blink.py ได้เลย ไฟล์นั้นกะพริบดวงเดียวพร้อมนับรอบขึ้นจอ แต่จับเวลาด้วย time.sleep_ms() ซึ่งบล็อกทั้งลูประหว่างรอ ตัวเลขที่เดินขึ้นบอกแค่ว่าโปรแกรมยังไม่ค้าง ส่วนท่าไฟวิ่งนี้ใช้ ticks_diff ลูปจึงยังว่างไปอ่านปุ่มระหว่างรอ
ปรับ
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)

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.1–1.3
ทำไมโปรแกรมต้องมีวันจบ — ลูปของเราวิ่งตามเวลาที่ตั้งไว้ใน RUN_MS ไม่ใช่ while True เพราะเราอยากให้มันคืนบอร์ดให้ทีมได้ทดลองรอบต่อไปโดยไม่ต้องกด RESTART ทุกครั้ง
เขียนโปรแกรมที่ เก็บของก่อนกลับบ้าน — นิสัยนี้จะช่วยชีวิตตอนโปรเจกต์ใหญ่
gpio.led(n).on()ส่งสถานะข้าม IPC ให้ CM55 ทุกครั้งโดยเราไม่ต้องสั่ง — ของแถมที่ทำให้เห็นภาพ
s03_led_button.py เติมช่องว่าง pass ให้ครบตามคำใบ้ # เติม:รอบพิเศษ — ดู IPC ด้วยตาตัวเอง (Eva Kit เท่านั้น — Dev Kit ไม่มีการ์ด Controls ข้ามรอบนี้ได้)
รันซ้ำอีกครั้ง แต่คราวนี้ก่อนกด Program to Device ให้แตะการ์ด Controls ค้างไว้แทน Playground แล้วมองวงกลมสีบนหน้า Controls ระหว่างที่โปรแกรมของเราวิ่ง
วงกลมบนจอจะติด-ดับตามไฟวิ่งของเรา ทั้งที่โค้ดเราไม่ได้สั่งจออะไรเลยสักบรรทัด — เพราะทุกครั้งที่เราสั่ง LED เฟิร์มแวร์ส่งสถานะข้าม IPC ไปบอก CM55 ให้เอง
ดูรอบพิเศษเสร็จแล้วกลับมาที่หน้า Playground เพื่ออ่านตัวนับ แล้วกด RESTART รันใหม่
โปรแกรมเดียวกัน แต่เปิดคนละหน้า เห็นคนละด้านของระบบ — ลองทั้งสองรอบก่อนสรุปว่าเข้าใจแล้ว
นี่ไม่ใช่บั๊กของโค้ด และไม่ใช่ปุ่มเสีย — เป็นธรรมชาติของหน้าสัมผัสโลหะทุกตัวในโลก