ต้องทำในบทเรียน · เปิดตามลำดับนี้ ทั้งชุดราว 35 นาที
| ลำดับ · เรื่อง · เวลา | ไฟล์ | ลงมือทำอะไร แล้วจะเข้าใจอะไร |
|---|---|---|
| 1 · ถามบอร์ดก่อนว่ามีอะไร · 10 นาที | 01_board_info.py |
รันแล้วจดว่ามีไฟกี่ดวง ปุ่มกี่ปุ่ม ชื่อไหนคู่กับดัชนีอะไร · จะไม่ตกหลุมชื่อปุ่มกับชื่อสีที่ไม่ตรงกับความจริง เพราะเขียนจากค่าที่ถามมา |
| 2 · ปุ่มนี้ 0 คือกด · 10 นาที | 04_button_active_low.py |
กดปุ่มผู้ใช้ค้างไว้แล้วดูเลขกับเส้นกราฟตกลงพร้อมกัน · จะรู้ว่า value() คืนแรงดันดิบ ส่วน is_pressed() คืนความหมาย และ if btn.value(): ทำงานกลับด้านหมด |
| 3 · นับให้ตรงด้วยการรอให้นิ่ง · 15 นาที | 05_debounce_count.py |
กดสิบครั้งแล้วเทียบเลขนับดิบกับเลขนับกันเด้ง · จะอธิบายได้ว่าถ้าถอดโค้ดกันเด้งออกจะเกิดอะไร ซึ่งเป็นข้อที่ตกกันมากที่สุดของ MVP |
ติดตรงไหน เปิดอันนี้
| อาการที่เจอ | ไฟล์ที่ตอบอาการนั้น |
|---|---|
| ไฟยังกะพริบไม่เป็นจังหวะ หรือจบโปรแกรมแล้วไฟค้างติดโดยไม่ตั้งใจ | 02_led_blink.py — กะพริบจริงพร้อมนับรอบขึ้นจอ toggle() ขึ้นกับค่าเดิมเสมอ ส่วน on() กับ off() ไม่สนใจค่าเดิม จึงต้องปิดท้ายด้วย off() |
| สั่งหรี่ไฟแล้วเงยหน้าไปมองหลอด ไม่เห็นความต่างสักระดับ | 03_led_brightness.py — ไฟล์เลือกดวง RGB ให้ แล้วสั่ง brightness(40) ครั้งเดียวให้มองหลอดเองว่าค้างหรือวูบ (ดวงที่มีเส้น PWM ค้าง ดวงอื่นวูบ 12 ms แล้วดับ) จากนั้นเทียบสองระดับด้วย led.hold(pct, ms) ที่ค้างได้ทุกดวง และไฟล์นี้ยังพิสูจน์ให้เห็นว่า duty() ตอบเลขที่เราสั่ง ไม่ได้ไปวัดหลอด |
| ปุ่มเดียวต้องคุมไฟหลายดวง แต่จำไม่ได้ว่าตอนนี้ดวงไหนติด | 06_button_picks_led.py — กดปุ่มเดิมซ้ำ ๆ แล้วดูดัชนีเลื่อนไปทีละดวง สถานะที่โปรแกรมจำไว้เองคือของที่เชื่อได้ที่สุด |
อ่านเสริมนอกเวลา — เรื่องนี้อยู่นอกเกณฑ์ผ่านของบทเรียน 2.1–2.3 แต่คือรูปร่างที่ลูปวันนี้ไปโผล่ในโรงงานจริง: 01_andon_severity_lamp.py เสาไฟ andon หนึ่งระดับความรุนแรงคือไฟหนึ่งดวง และต้องดับทุกดวงก่อนจุดดวงใหม่เสมอ ไม่งั้นคนที่มองจากอีกฝั่งโรงงานจะอ่านระดับผิด
สามไฟล์แรกคือของที่ต้องเปิดจริงในบทเรียน ตารางล่างเปิดเฉพาะตอนเจออาการนั้น
ฝั่งระบบสมองกลฝังตัว
GPIO เอาต์พุตกับอินพุต · แนวคิด active-high / active-low และตัวต้านทาน pull-up · polling loop กับการเลือกคาบเวลาสุ่มตัวอย่าง · contact bounce และการกันเด้ง · ค่าที่อ่านกลับจากขา บอกระดับของขา ไม่ได้บอกความตั้งใจของโปรแกรม
ฝั่ง Python และวิทยาการคอมพิวเตอร์
while ที่มีเงื่อนไขจบ · ตัวดำเนินการมอดุโล % สำหรับการวนเป็นวง · state machine ขนาดเล็กด้วยตัวแปรสามตัว (last_raw, stable, count) · edge detection เทียบกับ level detection
ฝั่งการออกแบบระบบ
แยกค่าที่ปรับได้ไว้บนสุดของไฟล์ · ให้โปรแกรมจำสถานะของตัวเอง ไม่ฝากไว้กับอุปกรณ์ · รายงานเมื่อมีเหตุการณ์ ไม่ใช่รายงานตามเวลา · เก็บกวาดสถานะก่อนจบโปรแกรม
%ที่ใช้วนไฟทุกดวงวันนี้ คือตัวเดียวกับที่ใช้วน buffer ข้อมูลเซนเซอร์ในบทเรียน 3.4–3.6
| topic | ทาง | ส่งเมื่อไร | JSON ตัวอย่าง |
|---|---|---|---|
bento-aiot/team03/event |
บอร์ด → เว็บ | ทันทีที่กด (ผ่านกันเด้งแล้ว) | {"ev":"press","presses":4,"id":"team03","n":17} |
bento-aiot/team03/telemetry |
บอร์ด → เว็บ | ทุก 2 วินาที | {"presses":4,"btn":0,"leds":[1,0,0],"az":9.79,"id":"team03","n":18} |
bento-aiot/team03/cmd |
เว็บ → บอร์ด | ตอนมีคนกดบนหน้าเว็บ | {"cmd":"led","n":0,"on":1} · {"cmd":"beep"} |
team03คือตัวอย่าง ใช้รหัสTEAMของคุณ ·nคือเลขใบที่เดินต่อกันทุกใบ อีกฝั่งเห็นเลขกระโดดก็รู้ว่ามีใบหาย · ตัวเลขในตารางเป็นตัวอย่างรูปร่าง ไม่ใช่ค่าที่วัดมา
07_button_to_broker.py — สามงานในลูปเดียวwhile time.ticks_diff(time.ticks_ms(), t0) < RUN_MS:
now = time.ticks_ms()
raw = btn.is_pressed()
# งานที่ 1: กันเด้งแบบไฟล์ 05
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: # ส่งเฉพาะขอบ "เริ่มกด"
presses = presses + 1
if not send(TOPIC_EVENT, {"ev": "press", "presses": presses}):
lost = True; break
# งานที่ 2: telemetry ตามนาฬิกา ไม่ใช่ตาม sleep
if time.ticks_diff(now, last_state) >= STATE_MS:
last_state = now
state = {"presses": presses, "btn": 1 if stable else 0, "leds": leds}
if not send(TOPIC_STATE, state):
lost = True; break
# งานที่ 3: หยิบคำสั่งทุกรอบ กล่องรับมีช่องเดียว
msg = mqtt.get_message()
if msg is not None:
handle(msg[1])
if not mqtt.is_connected():
lost = True; break
ui.poll()
time.sleep_ms(POLL_MS)
send() สองจุดกับ get_message() หนึ่งจุด · ; ใช้บีบบรรทัดบนสไลด์เท่านั้นleds มาจากตัวแปร handle() สั่ง gpio.led(i).on() แล้วจด leds[i] ทันที ไม่มีบรรทัดไหนถามขาsend() ดักสองทาง: คืน False = ใบนั้นหาย สายยังอยู่ · โยน OSError = สายหลุด ออกจากลูป ดับไฟ บอกบนจอ| สิ่งที่เฟิร์มแวร์ทำ | ที่มาในซอร์ส | ถ้าไม่รู้จะเจออะไร | 07 รับมืออย่างไร |
|---|---|---|---|
publish() ส่งแบบ retain = false เสมอ แม้ docstring จะเขียนว่ารับ retain= |
modmqtt.c:316 |
เปิดหน้าเว็บทีหลังแล้วจอว่าง เพราะ broker ไม่ได้เก็บใบล่าสุดไว้ให้ใคร | ส่ง telemetry ซ้ำทุก 2 วินาที หน้าเว็บที่เปิดช้าที่สุดก็ตามทันภายใน 2 วินาที |
get_message() เป็นกล่องช่องเดียว ใบใหม่ทับใบเก่า |
modmqtt.c:80-98 ตอนรับ · :385-400 ตอนหยิบ |
กดปุ่มบนหน้าเว็บรัว ๆ สามครั้ง บอร์ดเห็นใบเดียว | หยิบทุกรอบลูป รอบละ 5 ms ไม่หลับยาวที่ไหนเลย |
publish() ตอนสายหลุดโยน OSError ไม่ได้คืน False |
modmqtt.c:292-294 |
โปรแกรมพังกลางบทเรียนพร้อม traceback | send() ดัก OSError แล้วคืน False ให้ลูปหยุดอย่างสุภาพ |
ไม่ต่อใหม่ให้เอง และ clean_session เป็นจริงเสมอ (broker ไม่จำว่าเราเคยขอฟังอะไร) |
modmqtt.c:256 · ไม่มีโค้ดต่อใหม่ในโมดูล |
สายหลุดแล้วบอร์ดเงียบ คนดูคิดว่าไม่มีใครกดปุ่ม | ถาม is_connected() ทุกรอบ หลุดเมื่อไรขึ้นแดงบนจอ แล้วให้รันใหม่ |
อีกหนึ่งกฎที่ทั้งห้องต้องรู้: client_id ซ้ำกันไม่ได้ บน broker ตัวเดียวกัน ใครต่อทีหลังจะเตะคนก่อนหลุด (ทดลองแล้วบน broker สาธารณะทั้งสองตัว) ไฟล์ 07 ใช้ "bento-aiot-" + TEAM จึงรันได้บอร์ดเดียวต่อทีม ส่วนหน้าเว็บสุ่มชื่อ web-... เอง เปิดกี่แท็บก็ไม่เตะบอร์ด
ข้อที่เฟิร์มแวร์ "ทำไม่ได้" ไม่ใช่ข้อตำหนิ มันคือข้อมูลออกแบบ โค้ดที่ดีเขียนรอบข้อจำกัดของของจริง ไม่ใช่ของที่อยากให้เป็น
my_first_reader.htmlไม่ต้องติดตั้งอะไรเลย ใช้แค่เบราว์เซอร์ (มือถือก็ได้)
team05 ท้ายลิงก์เป็นรหัสของคุณ ไม่ต้องบันทึกหรือแก้ไฟล์ · หน้านี้บนเว็บ AIC รับเฉพาะรหัสรูป teamNN ถ้าใช้รหัสของตัวเองอย่าง nok4821 ให้ดาวน์โหลด my_first_reader.html ของรีโพนี้ไปเปิดในเบราว์เซอร์แทน// ทางเลือก: ดาวน์โหลด shared/web/my_first_reader.html ของรีโพนี้ไปเปิดเอง แล้วแก้บรรทัดนี้แทนการต่อท้ายลิงก์
let TEAM = "teamXX"; // รหัสเดียวกับ TEAM ในไฟล์ Python เช่น "nok4821" (เรียนเป็นกลุ่มใช้เลขที่ผู้จัดแจก เช่น "team05")
07_button_to_broker.py บนบอร์ด ภายใน 2 วินาทีกล่องค่าจะขึ้น presses btn leds (และ az ถ้าอ่าน IMU ได้)leds ใบถัดไปเปลี่ยนตาม (บน Dev Kit ดวง 0 อยู่บนโมดูล อาจมองไม่เห็นหลอด แต่ leds บนหน้าเว็บยังเปลี่ยนให้เห็น · อยากสั่งดวง 2 ซึ่งเป็นดวง RGB มีทั้งสองบอร์ด ให้ใช้ไฟล์ที่ดาวน์โหลดแล้วแก้ปุ่มเป็น n: 2)หน้าเว็บของทั้งห้องคือ mqtt_dashboard.html ผู้สอนฉายขึ้นจอ ทุกทีมเป็นการ์ดหนึ่งใบ https://advance-innovation-centre-aic.github.io/embedded-systems-for-aiot-developer/examples/web/mqtt_dashboard.html
สมาชิกที่นั่ง Emulator — Emulator ต่อ broker จริงได้แล้ว รันไฟล์ 07 บน Emulator ด้วย TEAM ของทีมก็ขึ้นหน้าเว็บเดียวกัน (Emulator เติม -emu ท้าย client_id จึงไม่เตะบอร์ดจริง) หรือเปิดหน้าเว็บนี้ดูและสั่งบอร์ดของทีมจากที่นั่งตัวเองก็ได้
ต่อไม่ติด: เปลี่ยนทั้งสองฝั่งไปตัวสำรอง
test.mosquitto.org(บอร์ด 1883 · เว็บwss://test.mosquitto.org:8081/mqtt) · เครือข่ายขององค์กรยังไม่เคยทดสอบ อาจปิดพอร์ตใดพอร์ตหนึ่งไว้ · ห้าม subscribe#บน broker สาธารณะ ฟังเฉพาะbento-aiot/<ทีม>/#
โจทย์ เลือกค่าหนึ่งตัวที่ทีมอยากให้คนนอกห้องเห็น แล้วทำให้มันไปถึงหน้าเว็บ
az มีให้แล้ว ลองแกนอื่นหรือเซนเซอร์อื่น) หรือสถานะ GPIO (ปุ่มค้างนานแค่ไหน · ดวงไหนติดอยู่ · กดยาวหรือกดสั้น)state ของไฟล์ 07 ไปกับ telemetry ทุก 2 วินาที ถ้าเป็นเหตุการณ์ที่เกิดทีเดียว ส่งเป็น event แทนmy_first_reader.html ให้ตอบสนองค่านั้น เช่น เปลี่ยนสีเมื่อเกินเกณฑ์ หรือเพิ่มปุ่มสั่งไฟดวงอื่นhandle() แล้วเพิ่มปุ่มบนหน้าเว็บที่ส่งมันทำครบเมื่อ (การบ้านหรือทำเมื่อเสร็จก่อนเวลา ไม่อยู่ในเกณฑ์ผ่าน)
id n และค่าที่ทีมเลือกอย่างน้อยหนึ่งตัวpresses เพิ่มสิบพอดีleds ใน telemetry ใบถัดไปเปลี่ยนตามleds จากตัวแปร ไม่ใช่จากขาจดลงบันทึกการเรียน · ถ้าเจอพฤติกรรมที่ไม่ตรงกับสไลด์ จดไว้แล้วบอกผู้สอน นั่นคือผลการวัด ไม่ใช่ความผิดของทีม
บอร์ด: 08_class_race.py (แก้ WiFi กับ TEAM จาก teamXX เหมือนไฟล์ 07 · เปิดเกมไว้ 45 นาที) · จอหน้าห้อง: class_game.html ผู้สอนเปิดลิงก์นี้ฉายขึ้นจอ https://advance-innovation-centre-aic.github.io/embedded-systems-for-aiot-developer/examples/web/class_game.html
bento-aiot/all/cmdevent ของทีมเปลี่ยนคนนั่งบอร์ดทุกรอบ เล่นหกรอบ ทีมสี่คนได้นั่งบอร์ดครบทุกคนภายในสี่รอบแรก · ระหว่างรอบ ทั้งห้องช่วยกันกดให้ตัวนับรวมบนจอหน้าห้องถึง 300
ทายก่อน — ทีมที่ผลขึ้นจอหน้าห้องก่อน คือทีมที่เร็วที่สุด ☐ ใช่ ☐ ไม่ใช่ เพราะ ________ (เขียนลงบันทึกการเรียน ก่อนรอบแรก)
ระหว่างรอบ ใครส่ง
ledหรือbeepเข้าบอร์ดเพื่อแกล้งทีมอื่นก็ไม่ได้ผล ไฟล์ 08 ปฏิเสธเองอยู่แล้ว ลองหาดูว่าบรรทัดไหน · คนที่นั่ง Emulator กดปุ่มตอนว่างเพื่อเล่นรอบซ้อมได้ แต่ Emulator ไม่ได้ยินคำสั่งจากหน้าห้อง (ไฟล์นี้ยังไม่เคยลองบน Emulator)
จอหน้าห้องมีสองคอลัมน์ เวลาบนบอร์ด กับ มาถึงลำดับ และจอเรียงอันดับตามเวลาบนบอร์ด ไม่ใช่ตามลำดับที่มาถึง
กันเด้ง 40 ms ทำให้บอร์ด "เชื่อ" ช้ากว่านิ้ว 40 ms ไฟล์ 08 จึงจดเวลาตอนขาเริ่มเปลี่ยน แล้วใช้เวลานั้นเมื่อกันเด้งยืนยันแล้ว ลองข้อ 2 ในบล็อก "ตาคุณ" ท้ายไฟล์แล้ววัดเอง
หน้าเว็บไม่นับผลที่มาถึงเร็วเกินกว่าที่เป็นไปได้ (เร็วกว่าเวลารอขั้นต่ำบวกเวลาที่อ้าง) แต่คนที่ปลอมผลแล้วรอนานพอ หน้าเว็บจับไม่ได้ เพราะใครก็ส่งเข้าหัวข้อของทีมไหนก็ได้บน broker สาธารณะ บทเรียน 4.4–4.6 กับ 4.7–4.9 เราจะปิดช่องนี้
วันนี้เราได้:
สั่ง LED จริงด้วยโค้ดของเราเองเป็นครั้งแรก · อ่านปุ่มจริงด้วย polling และเข้าใจว่าทำไมต้องกันเด้ง · แยกออกระหว่าง sleep_ms กับ ticks_diff และรู้ว่าเมื่อไรควรใช้อันไหน · เห็นกับดักชื่อของเฟิร์มแวร์กับตาตัวเอง และรู้ว่าทำไมปุ่มถึงชื่อ USER Button 1 · (Eva Kit) เห็น IPC ทำงานผ่านหน้า Controls โดยไม่ต้องเขียนโค้ดเพิ่ม
การบ้านของทีม: เลือกทำ 1 ข้อจากสี่ข้อในสไลด์ "ต่อยอด — คิดต่อเอง" จดลงบันทึกการเรียน
ชุดบทเรียนถัดไป: ปุ่มจริงหนึ่งปุ่มมันน้อยไป เราจะสร้างปุ่มบนจอสัมผัสเองด้วย ui.Button และ ui.Switch แล้วเอามาคุม LED สามสี (แดง เขียว น้ำเงิน) ของบอร์ด — พร้อมกฎเหล็กห้าข้อของการใช้ ui · และปุ่มบนจอพวกนั้นจะ publish ขึ้น bento-aiot/<ทีม>/event แบบเดียวกับปุ่มจริงวันนี้ ส่วนคำสั่งจากหน้าเว็บจะขยับ widget บนจอได้
ลูปที่เขียนวันนี้จะกลายเป็นโครงของทุกโปรแกรมในบทเรียนที่เหลือ ต่างกันแค่ว่าอ่านอะไรและสั่งอะไร
s03_led_button.py — ส่วนที่หนึ่ง: ค่าคงที่อ่านให้เข้าใจ แล้วพิมพ์เอง การพิมพ์เองคือตอนที่มือกับสมองจำโครงสร้างได้
import gpio
import lcd
import time
import ui
STEP_MS = 150 # จังหวะไฟวิ่ง ปรับตรงนี้เพื่อเปลี่ยนความเร็ว
DEBOUNCE_MS = 40 # เวลาที่ปุ่มต้องนิ่งก่อนเราจะเชื่อ
POLL_MS = 5 # ความถี่ที่ลูปถามปุ่ม
RUN_MS = 30000 # อายุของโปรแกรมรอบนี้
NUM_LEDS = gpio.num_leds()
btn = gpio.button(0)
ค่าคงที่สี่ตัวบนสุดคือ หน้าปัดของโปรแกรม ทีมปรับได้โดยไม่ต้องอ่านตรรกะข้างล่าง วิธีนี้ทำให้สไลด์ MVP ข้อ "ปรับจังหวะได้" กลายเป็นการแก้เลขตัวเดียว
NUM_LEDS = gpio.num_leds() ดีกว่าเขียน 3 ตรง ๆ เพราะย้ายไป Dev Kit ที่มีไฟห้าดวง โค้ดนี้วิ่งครบทุกดวงเองโดยไม่ต้องแก้ — ในห้องนี้มีทั้งสองบอร์ด ไฟล์เดียวกันจึงต้องถูกทั้งคู่
ตั้งต้น — รายงานสิ่งที่รู้ ดับไฟให้หมด ประกาศตัวแปรสถานะ แล้วล้างจอ (ตัดจาก s03_led_button.py ตรง ๆ)
info = gpio.board_info()
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>")
...
for i in range(NUM_LEDS):
gpio.led(i).off()
...
led_index = 0 # ตอนนี้ไฟดวงไหนกำลังติด
count = 0 # จำนวนครั้งที่กดปุ่ม
raw = False # ค่าดิบของปุ่มรอบนี้
last_raw = False # ค่าดิบของปุ่มรอบก่อน
stable = False # ค่าปุ่มที่ผ่านการกันเด้งแล้ว
...
COL_TEXT, COL_DIM = 0xE8EAED, 0x9AA3AF
COL_CARD, COL_OK, COL_RUN = 0x171B22, 0x30A46C, 0x4A9EFF
UI_MS = 100 # จอถูกอัปเดตทุก 100 ms ไม่ใช่ทุกรอบลูป
...
ui.screen()
time.sleep_ms(200)
ui.Label("แผงคุมไฟวิ่ง - ชุด 3", x=24, y=8, color=COL_TEXT, value=28)
lbl_status = ui.Label("ไฟวิ่งกำลังเดิน", x=360, y=12, color=COL_DIM, value=20)
สองบรรทัด lcd.print ของชื่อปุ่มจงใจแยกกัน — รวมเป็นบรรทัดเดียวยาว 163 ไบต์ เกินเพดาน 127 แล้วถูกตัดทิ้งเงียบ ๆ · ชื่อปุ่มเอาจาก btn.name() ไม่ใช่จากป้ายบนแผ่นวงจร ซึ่งบอร์ดสองรุ่นพิมพ์ต่างกัน
led_index ต้องจำเอง เพราะ gpio.led(n).value() ตอบระดับของขา ณ วินาทีที่ถาม ไม่ใช่สิ่งที่เราสั่ง · ห้าสีบนสุดคือจานสีของหลักสูตร · UI_MS = 100 คือจังหวะเขียนจอ ไม่ใช่จังหวะลูป — ลูปถามปุ่มทุก 5 ms แต่จอไม่ต้องรู้ทุกครั้ง
ค่าที่ต้องปรับบ่อยอยู่บนสุด ตรรกะอยู่ล่าง — เกณฑ์ "ปรับจังหวะได้" จึงเหลือแค่แก้
STEP_MSตัวเดียวในส่วนที่หนึ่ง
PITCH = min(128, 408 // NUM_LEDS)
LED_W = 48 if PITCH >= 128 else 36
led_ui = []
for i in range(NUM_LEDS):
x = 40 + i * PITCH
led_ui.append(ui.Led(x=x, y=104, w=LED_W, h=LED_W, color=COL_OK, value=0))
...
btn_run = ui.Button("เดินไฟวิ่ง", x=40, y=268, w=176, h=88, color=0x30A46C, value=20)
btn_stop = ui.Button("หยุดไฟวิ่ง", x=248, y=268, w=176, h=88, color=0x3A4150, value=20)
...
box = ui.MsgBox("ยืนยันหยุด\nไฟทุกดวงจะดับทันที",
x=48, y=96, w=496, h=160, color=COL_CARD)
box.hide()
...
btn_yes = ui.Button("ยืนยัน", x=568, y=96, w=152, h=88, color=0x3A4150, value=20)
btn_no = ui.Button("ยกเลิก", x=568, y=216, w=152, h=88, color=0x3A4150, value=20)
btn_yes.hide()
btn_no.hide()
...
t0 = time.ticks_ms()
last_step = t0
last_change = t0
last_ui = t0
chase_on = True # ไฟวิ่งเดินอยู่ไหม - ปุ่มบนจอเป็นคนเปลี่ยนค่านี้
asking = False # กำลังรอคำตอบจากกล่องยืนยันอยู่ไหม
เวลาสี่ตัวตั้งต้นจาก ticks_ms() ค่าเดียวกัน ถ้าเรียกแยกกัน รอบแรกจะเพี้ยนไปไม่กี่มิลลิวินาที · แผงบนจอสร้าง ครั้งเดียวก่อนเข้าลูป — งบ widget ของคอร์สคือ 32 ต่อหน้า (เพดานเฟิร์มแวร์ 64) และการสร้างซ้ำทุกรอบคือการยิง IPC ทิ้งเปล่า ๆ 200 ครั้งต่อวินาที · PITCH คิดจาก NUM_LEDS — 3 ดวงได้ 128 · 5 ดวงได้ 81 ไฟล์เดียวกันวางพอดีทั้งสองบอร์ด
btn_run กับ btn_stop เป็น คนละปุ่ม โดยตั้งใจ (สูง 88 px ตามระยะนิ้วจริง) ปุ่มสลับปุ่มเดียวบอกไม่ได้ว่าตอนนี้อยู่สถานะไหน · กล่องยืนยันกับปุ่มคำตอบสองปุ่มถูก สร้างแล้วซ่อนไว้ ตั้งแต่ต้น — การสร้างของตอนคนกำลังรอคำตอบ คือการเพิ่มความหน่วงในจังหวะที่แย่ที่สุด · จุดเริ่มเวลาที่ตรงกัน ทำให้ตอนดีบักไม่ต้องสงสัยว่าความคลาดเคลื่อนมาจากไหน
while time.ticks_diff(time.ticks_ms(), t0) < RUN_MS:
now = time.ticks_ms()
if chase_on and time.ticks_diff(now, last_step) >= STEP_MS:
gpio.led(led_index).off()
led_index = (led_index + 1) % NUM_LEDS
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)
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)
if time.ticks_diff(now, last_ui) >= UI_MS:
last_ui = now
led_btn.value(1 if stable else 0)
left_s = (RUN_MS - time.ticks_diff(now, t0)) // 1000
if left_s != last_sec:
last_sec = left_s
seg_count.text(str(count))
lbl_left.text(str(left_s) + " วิ")
bar_left.value(left_s)
for ev in ui.poll():
... # ปุ่มไหนถูกกด แล้วทำอะไรต่อ - อยู่ในไฟล์เฉลยเต็ม
time.sleep_ms(POLL_MS)
now = time.ticks_ms() เรียกครั้งเดียวต่อรอบ แล้วใช้ร่วมกันทั้งสามงาน — ถ้าเรียกใหม่ทุกจุด เวลาของงานไฟกับงานปุ่มจะไม่ใช่เวลาเดียวกัน
งานที่สามคือ จอ และมันเดินที่ 100 ms ไม่ใช่ 5 ms — ui.poll() ทุกรอบลูปคือการยิง IPC ข้ามคอร์ 200 ครั้งต่อวินาที เพื่อรอนิ้วที่มาถึงวินาทีละครั้ง
if left_s != last_sec: คือกฎ ตัวเลขที่คนต้องอ่าน เขียนใหม่ไม่เกินวินาทีละครั้ง ตัวเลขที่กระพริบสิบครั้งต่อวินาทีอ่านไม่ได้ และไม่มีใครได้ประโยชน์จากมัน ส่วนแถบกับไฟขยับเร็วกว่านั้นได้ เพราะตาอ่านรูปทรงไม่ได้อ่านหลัก
time.sleep_ms(POLL_MS) อยู่ท้ายสุดของลูปเสมอ เป็นจุดเดียวที่โปรแกรมยอมพัก
อ่านลูปนี้จากบนลงล่างแล้วพูดออกมาดัง ๆ ว่า "ถึงเวลาขยับไฟไหม แล้วปุ่มขยับไหม แล้วพัก" — ถ้าพูดได้ แปลว่าเข้าใจแล้ว
ท่า 1 ถามบอร์ด มาก่อน เพราะถ้า gpio ใช้ไม่ได้หรือบอร์ดตอบแปลก เรารู้ตั้งแต่บรรทัดแรก ไม่ใช่หลังเขียนไปแล้วแปดสิบบรรทัด
ท่า 2 ดับให้หมด มาก่อนลูป เพื่อให้ทุกการรันเริ่มจากจุดเดียวกัน ผลการทดลองถึงจะเทียบกันได้
ท่า 3 ไฟวิ่ง มาก่อนปุ่ม เพราะไฟวิ่งเป็นงานที่ ไม่ต้องพึ่งอินพุตจากใคร ถ้าไฟวิ่งได้ แปลว่าโครงลูปกับนาฬิกาถูกแล้ว
ท่า 4 ปุ่ม มาทีหลัง ตอนนี้ถ้าพัง เรารู้แน่ว่าพังที่เรื่องปุ่มหรือการกันเด้ง ไม่ใช่ที่โครงลูป
ท่า 5 ปิดท้าย มาสุดท้าย เพราะมันคือคำแถลงว่าทุกอย่างข้างบนผ่านหมดแล้ว
หลักการเดียวกับบทเรียน 1.1–1.3: ไล่จากสิ่งที่ยืนได้ด้วยตัวเอง ไปหาสิ่งที่ต้องพึ่งของอื่น แล้วให้แต่ละขั้นยืนยันขั้นก่อนหน้า
เทคนิคนี้ชื่อว่า bring-up ทีละชั้น วิศวกรฮาร์ดแวร์ใช้ตอนบอร์ดใหม่มาถึงโต๊ะ และมันใช้ได้กับซอฟต์แวร์เหมือนกัน
คำถามคิดต่อ: ถ้าเปลี่ยนจาก "อ่านปุ่ม" เป็น "อ่านความเร่งจาก IMU" ลูปนี้ต้องแก้กี่บรรทัด · การกันเด้งของปุ่ม กับการกรองสัญญาณเซนเซอร์ที่จะเจอบทเรียน 2.7–2.9 เหมือนกันตรงไหน · ถ้าต้องคุมของ 20 ชิ้นพร้อมกัน โครงแบบนี้ยังไหวไหม
ทั้งสี่ช่องใช้โครงโค้ดเดียวกับที่เราเพิ่งเขียน ต่างกันที่ค่าเวลาและสิ่งที่ต่อปลายสาย
ข้อ 1 · ปุ่มเปลี่ยนความเร็ว — ให้การกดปุ่มวนเปลี่ยนจังหวะไฟวิ่งระหว่าง 400, 200 และ 100 ms โดยตัวนับยังทำงานถูกต้อง · ใบ้: เก็บความเร็วเป็น list แล้วใช้ % แบบเดียวกับที่ใช้วนไฟ
ข้อ 2 · รูปแบบไฟของทีม — ออกแบบลำดับไฟเป็น list ที่สร้างจาก gpio.num_leds() เช่น ไป-กลับ 0..N-1..1 (Eva 3 ดวงได้ 0,1,2,1 · Dev Kit 5 ดวงได้ 0,1,2,3,4,3,2,1) หรือกะพริบพร้อมกันสลับกับวิ่ง แล้วบันทึกว่าทีมเลือกแบบไหนเพราะอะไร
ข้อ 3 · แยกกดสั้นกับกดยาว — วัดเวลาที่กดค้างด้วย ticks_diff ระหว่างขอบขาเข้ากับขาออก เกิน 800 ms ถือเป็น "กดยาว" แล้วสั่งงานคนละอย่าง
ข้อ 4 · ทดลองค่ากันเด้ง — ตั้ง DEBOUNCE_MS เป็น 0, 5, 40 และ 200 อย่างละรอบ กดค่าละสิบครั้ง จดตัวเลขจริงเป็นตาราง แล้วสรุปว่าค่าไหนเหมาะกับปุ่มบนบอร์ดนี้
อยากหรี่ไฟด้วย ให้ต่อยอดจาก
03_led_brightness.py—brightness(pct)ค่ากลางค้างระดับได้บนดวง RGB (มีเส้น PWM ทั้งสองบอร์ด) และไม่บล็อก ส่วนดวงที่ไม่มีเส้นนั้นได้พัลส์ราว 12 ms แล้วดับ ท่าที่ค้างได้ทุกดวงคือled.hold(pct, ms)ซึ่งย้ำพัลส์จนครบเวลาที่สั่ง (msไม่ใส่ได้ ปริยาย 500) แต่จำไว้ว่ามันบล็อกจนครบmsและจบด้วยหลอดดับ — ถ้าเพิ่งสั่งbrightness()ค่ากลางบนดวง PWM ต้องเรียกoff()คั่นก่อนhold()ไม่งั้นขายังถูก PWM ถืออยู่hold()ไม่เห็นผลและหลอดไม่ดับตอนจบ · ทีมที่ทำข้อนี้ต้องแบ่งเวลาให้ตัวนับยังเดินถูก · และอย่าเผลอใช้duty()เป็นตัวตรวจ มันคืนเลขที่สั่งไป ไม่ได้วัดหลอด

brightness(40) ก่อน

เอกสารของผู้ผลิตและตำรา — KIT_PSE84_EVAL PSOC™ Edge E84 Evaluation Kit guide (Eva Kit), Infineon 002-39007 Rev.*B: §3.2.2.15 User LEDs (หน้า 88–89, รูปที่ 78 ให้ค่า 220/200/2.4 kΩ) · §3.2.2.16 Reset and user buttons (หน้า 89–90, รูปที่ 80 แสดงว่า R411/R408 กับ C230/C229 เป็น DNI คือไม่ได้ลงอุปกรณ์) · Practical Electronics for Inventors, 4th ed. §2.12, §5.3.4, §13.5.1
ปุ่มเด้งและ pull-up — A Guide to Debouncing, Jack Ganssle (วัดสวิตช์จริง 16 ตัว bounce แย่สุด 157 ms, แนะช่วง 20–50 ms) https://www.ganssle.com/debouncing.htm · Pico Technology https://www.picotech.com/library/articles/blog/what-is-switch-bounce-how-to-implement-debounce · Pull-up Resistors, SparkFun (CC BY-SA 4.0) · ภาษาไทย IoT Engineering Education, KMUTNB (CC BY-SA 4.0) https://iot-kmutnb.github.io/blogs/arduino/arduino_avr_io_follower/
วิดีโอที่ตรวจแล้ว (ความยาวยังไม่ยืนยันทั้งหมวด) — AddOhms #15 pull-up wxjerCHCEMg · DigiKey เลือกตัวต้านทานให้ LED UWx2BEx7xyI · KruNarut (ไทย) U9JYM1VdF5U · DigiKey switch bounce IvU8m_30iK0