ปุ่มกดบนบอร์ดฐานและเมนูที่ไม่ใช้จอสัมผัส
เป้าหมาย
หัวข้อที่มีชื่อว่า “เป้าหมาย”- อ่านปุ่ม active-low แบบ pull-up และนับจำนวนครั้งที่กดได้ถูกต้อง
- นำทางเมนู LVGL ด้วยปุ่มกายภาพสองปุ่ม (Move / Select) แบบ kiosk
บอร์ดฐาน QWA309 มีอะไรให้ฝึก
หัวข้อที่มีชื่อว่า “บอร์ดฐาน QWA309 มีอะไรให้ฝึก”บอร์ดฐาน QWA309 ของ TESAIoT Dev Kit มีอุปกรณ์จริงให้ฝึก ได้แก่ ปุ่มกด potentiometer 4 ตัว CAN transceiver และ header สำหรับต่ออุปกรณ์ภายนอก บทเรียนนี้ใช้แบบฝึกของ Developer Hub ที่เขียนไว้สำหรับบอร์ดนี้โดยตรง สองแบบฝึกแรกใช้ปุ่มกดสองตัวบนบอร์ดฐานเป็นข้อมูลเข้าเหมือนกัน แต่ใช้ตรรกะอ่านค่าคนละแบบ: แบบแรกอ่าน “สถานะปัจจุบัน” (level) ตลอดเวลา ส่วนแบบที่สองจับ “จังหวะที่เพิ่งเปลี่ยน” (edge) เท่านั้น
ปุ่มกด active-low พร้อม internal pull-up
หัวข้อที่มีชื่อว่า “ปุ่มกด active-low พร้อม internal pull-up”ทั้งสองแบบฝึกตั้งขาปุ่มด้วย Cy_GPIO_Pin_FastInit(port, pin, CY_GPIO_DM_PULLUP, 1UL, HSIOM_SEL_GPIO) พารามิเตอร์ CY_GPIO_DM_PULLUP สั่งให้ชิปเปิด pull-up ตัวต้านทานในตัว ดึงขาขึ้น HIGH ตลอดเวลาที่ไม่มีอะไรมาแตะ ส่วน HSIOM_SEL_GPIO เลือกให้ขาทำหน้าที่ digital I/O ล้วน ๆ ไม่ผูกกับเพอริเฟอรัลตัวอื่น ปุ่มต่อขาลงกราวด์เมื่อกด จึงดึงระดับสัญญาณลงเป็น LOW และ Cy_GPIO_Read() คืนค่า 0 เมื่อกด — โค้ดจึงเขียนเงื่อนไข “กด” เป็น 0U == Cy_GPIO_Read(...) ทั้งใน Push Button Monitor และ Hardware Button Menu การใช้ internal pull-up แบบนี้ทำให้ไม่ต้องมีตัวต้านทานภายนอกต่อเพิ่ม และสถานะตอนปล่อยปุ่มจะแน่นอนเสมอ (ไม่ลอย)
กรองสัญญาณเด้งด้วยการนับรอบก่อนเชื่อค่าใหม่ (Push Button Monitor)
หัวข้อที่มีชื่อว่า “กรองสัญญาณเด้งด้วยการนับรอบก่อนเชื่อค่าใหม่ (Push Button Monitor)”button_monitor_ui.c โพลปุ่มด้วย lv_timer_create(button_timer_cb, BUTTON_REFRESH_PERIOD_MS, NULL) ที่ BUTTON_REFRESH_PERIOD_MS = 25 (ทุก 25 ms) ในแต่ละรอบ update_button() เทียบค่าที่อ่านได้ใหม่ (sampled_pressed) กับค่าครั้งก่อน ถ้าค่าเปลี่ยนจะรีเซ็ต debounce_count เป็น 0 และเริ่มนับใหม่ ถ้าค่ายังเหมือนเดิมจะนับ debounce_count ขึ้นทีละ 1 จนกว่าจะถึง BUTTON_DEBOUNCE_TICKS = 2 โค้ดจึงยอมรับว่าสถานะเปลี่ยนจริง (stable_pressed) นั่นคือค่าต้องอ่านได้ระดับเดียวกันติดต่อกัน 3 ครั้ง ซึ่งที่รอบโพล 25 ms คือประมาณ 50 ms ก่อนสถานะจะเปลี่ยนจริง ช่วงเวลานี้กรองการเด้งของหน้าสัมผัสปุ่มที่สั้นกว่านั้นทิ้งไป press_count เพิ่มขึ้นเฉพาะตอนสถานะ “เสถียร” เปลี่ยนจากปล่อยเป็นกด (ไม่ใช่ทุกครั้งที่อ่านค่าได้ LOW) ส่วน hold_time_ms จะรีเซ็ตเป็น 0 เมื่อปล่อย และสะสมเพิ่มทีละ BUTTON_REFRESH_PERIOD_MS ทุกรอบที่ stable_pressed เป็นจริง ค่านี้จึงมีความละเอียดเป็นขั้นละ 25 ms ไม่ใช่ค่านาฬิกาต่อเนื่อง
จับ “ขอบขาลง” แทนสถานะค้าง (Hardware Button Menu)
หัวข้อที่มีชื่อว่า “จับ “ขอบขาลง” แทนสถานะค้าง (Hardware Button Menu)”hw_button_menu_ui.c ใช้แนวทางต่างออกไป: btn_pressed_edge() มี debounce แบบเดียวกัน (MENU_DEBOUNCE = 2 นับที่รอบโพล MENU_POLL_MS = 30 ms) แต่คืนค่า true เฉพาะ “รอบที่สถานะเสถียรเพิ่งเปลี่ยนจากปล่อยเป็นกด” เท่านั้น ไม่ใช่คืน true ทุกรอบที่ปุ่มยังถูกกดอยู่ — นี่คือความต่างสำคัญจาก update_button() ของ Push Button Monitor ที่ผู้เรียกอ่าน stable_pressed เป็นระดับสัญญาณทุกรอบ ผลคือถ้ากดปุ่ม MOVE ค้างไว้นาน menu_timer_cb() จะขยับ highlight ไปข้างหน้าเพียงครั้งเดียวต่อการกดหนึ่งครั้ง เมนูมี MENU_ITEMS = 4 รายการ และวนกลับด้วยเลขคณิตมอดุโล (s_sel + 1U) % MENU_ITEMS ปุ่ม SW6 (MOVE) กับ SW5 (SELECT) แต่ละตัวมีโครงสร้าง btn_t และตัวแปร debounce ของตัวเอง (s_move, s_selb) จึงกดสองปุ่มพร้อมกันได้โดยไม่รบกวนกัน กด SELECT เพียงเปลี่ยนข้อความสถานะ ไม่ขยับ highlight
ชื่อปุ่มที่ไม่ตรงกันระหว่างคำอธิบายกับโค้ด: SW9/SW10 เทียบกับ SW5/SW6
หัวข้อที่มีชื่อว่า “ชื่อปุ่มที่ไม่ตรงกันระหว่างคำอธิบายกับโค้ด: SW9/SW10 เทียบกับ SW5/SW6”ไฟล์ metadata.json และคอมเมนต์บนสุดของ main_example.c ของ Push Button Monitor เขียนว่าเป็นปุ่ม “SW9 (P17.5) และ SW10 (P17.7)” แต่โค้ดจริงใน button_monitor_ui.c ตั้งชื่อปุ่มในอาร์เรย์ buttons[] ว่า "SW5" (พิน P17.7) และ "SW6" (พิน P17.5) ซึ่งเป็นข้อความที่ปรากฏบนจอจริงด้วย ส่วน Hardware Button Menu ที่ใช้พินเดียวกันเรียกปุ่มทั้งสองว่า SW6 (P17.5, MOVE) และ SW5 (P17.7, SELECT) ตรงกับชื่อในโค้ดของ Push Button Monitor พอดี สรุปคือชื่อ SW9/SW10 เป็นข้อความสรุปที่คลาดเคลื่อนใน Developer Hub เอง ให้ยึดตามโค้ดที่รันจริงและลายพิมพ์บนบอร์ดเป็นหลัก
ตัวอย่างสมบูรณ์
หัวข้อที่มีชื่อว่า “ตัวอย่างสมบูรณ์”แบบฝึกชุด QWA309 ของ Developer Hub (อ้างอิงที่ commit e5c7722) รันบน TESAIoT Dev Kit เท่านั้น เพราะใช้อุปกรณ์บนบอร์ดฐาน
- QWA309 — Push Button Monitor — อ่านปุ่มกด SW9 (P17.5) และ SW10 (P17.7) แบบ active-low pull-up แสดงสถานะกด/ปล่อย + นับจำนวนครั้งบน LVGL README · โค้ด · Developer Hub
- QWA309 — Hardware Button Menu — นำทางเมนู LVGL ด้วยปุ่มกายภาพ SW6=Move SW5=Select (ไม่ใช้ touch) — headless/kiosk UX pattern README · โค้ด · Developer Hub
โค้ดตัวอย่างด้านล่างคัดลอกจากไฟล์จริงที่ commit เดียวกัน (Apache-2.0, tesaiot/developer-hub)
button_monitor_ui.c — ตั้งขาปุ่มเป็น input pull-up ก่อนเริ่มอ่านค่า:
static void button_inputs_init(void){ Cy_GPIO_Pin_FastInit(P17_5_PORT, P17_5_PIN, CY_GPIO_DM_PULLUP, 1UL, HSIOM_SEL_GPIO); Cy_GPIO_Pin_FastInit(P17_7_PORT, P17_7_PIN, CY_GPIO_DM_PULLUP, 1UL, HSIOM_SEL_GPIO);}button_monitor_ui.c — debounce แบบนับรอบก่อนยอมรับว่าสถานะเปลี่ยนจริง (level):
static void update_button(button_channel_t *button){ bool sampled_pressed = (0U == Cy_GPIO_Read(button->port, button->pin_num));
if (sampled_pressed == button->last_sample_pressed) { if (button->debounce_count < BUTTON_DEBOUNCE_TICKS) { button->debounce_count++; } } else { button->last_sample_pressed = sampled_pressed; button->debounce_count = 0U; }
if ((button->debounce_count >= BUTTON_DEBOUNCE_TICKS) && (sampled_pressed != button->stable_pressed)) { button->stable_pressed = sampled_pressed; /* ... press_count++ on press / hold_time_ms reset on release ... */ } /* ... hold_time_ms accumulation, apply_button_visual(button) ... */}hw_button_menu_ui.c — debounce แบบเดียวกัน แต่คืน true แค่ครั้งเดียวต่อการกด (edge):
static bool btn_pressed_edge(btn_t *b){ bool raw = (0U == Cy_GPIO_Read(b->port, b->pin)); /* active low */ bool edge = false; if (raw == b->last) { if (b->cnt < MENU_DEBOUNCE) { b->cnt++; } if ((b->cnt >= MENU_DEBOUNCE) && (raw != b->stable)) { b->stable = raw; if (raw) { edge = true; } /* press edge */ } } else { b->cnt = 0U; } b->last = raw; return edge;}hw_button_menu_ui.c — ใช้ผลจาก edge เพื่อขยับ highlight แค่ครั้งเดียวต่อการกด:
static void menu_timer_cb(lv_timer_t *timer){ (void)timer; if (btn_pressed_edge(&s_move)) { s_sel = (uint8_t)((s_sel + 1U) % MENU_ITEMS); highlight(); lv_label_set_text_fmt(s_status, "MOVE -> %s", s_items[s_sel]); } if (btn_pressed_edge(&s_selb)) { lv_label_set_text_fmt(s_status, "SELECT: %s", s_items[s_sel]); }}จุดที่มักพลาด
หัวข้อที่มีชื่อว่า “จุดที่มักพลาด”- ลืมตั้ง pull-up แล้วปล่อยขาเป็น high-Z — ถ้าตั้งขาปุ่มเป็น input ธรรมดาไม่มี pull-up ขณะที่ปุ่มต่อลงกราวด์เพียงด้านเดียว ตอนปล่อยปุ่มขาจะลอย (floating) ค่าที่อ่านได้จะแกว่งตามสัญญาณรบกวน ตัวนับอาจเพิ่มเองโดยไม่มีใครกด ต้องตั้ง
CY_GPIO_DM_PULLUPเสมอ - คิดว่ากดปุ่มค้างแล้วเมนูจะเลื่อนซ้ำหลายครั้ง — ใน Hardware Button Menu highlight เลื่อนแค่ครั้งเดียวต่อการกด เพราะ
btn_pressed_edge()คืนtrueเฉพาะขอบขาลง ไม่ใช่ระดับสัญญาณ ถ้าต้องการ auto-repeat เมื่อกดค้างต้องเขียนตรรกะเพิ่มเอง - ผสมสองรูปแบบเข้าด้วยกัน — ถ้านำโค้ดแบบอ่านระดับสัญญาณ (
stable_pressedของ Push Button Monitor) ไปใช้กับเมนูโดยตรง highlight จะขยับทุกรอบโพลตราบใดที่ยังกดปุ่มอยู่ (ทุก 30 ms) ไม่ใช่ครั้งเดียวต่อการกด ต้องเลือกรูปแบบ (level หรือ edge) ให้ตรงกับพฤติกรรมที่ต้องการ
ชื่อปุ่มในคำอธิบายของ Developer Hub (SW9/SW10) ไม่ตรงกับโค้ดของแบบฝึก (SW5/SW6) ให้ยึดตามโค้ดและลายพิมพ์บนบอร์ด
build และ flash
หัวข้อที่มีชื่อว่า “build และ flash”# ในโฟลเดอร์ master template (ดูบทเรียน 1.1)# 1) ลบไฟล์ของ episode เก่าใน proj_cm55/apps/# 2) คัดลอกไฟล์ทั้งหมดของ episode นี้ลงใน proj_cm55/apps/make buildmake program # flash ผ่าน KitProg3- ทาย ก่อนแก้: เลือกค่าหนึ่งค่าที่ README ของตัวอย่างอธิบายไว้ในส่วน How แล้วเขียนว่าจะเห็นอะไรเปลี่ยนบนจอหรือใน log
- แก้และรัน build + flash แล้วเทียบกับที่ทายไว้ ถ้าไม่ตรง ให้หาว่าเข้าใจส่วนไหนผิด
- ทำเพิ่ม ต่อยอดหนึ่งอย่างที่ตัวอย่างยังไม่มี แล้วเก็บภาพหรือวิดีโอไว้ใน portfolio
เช็กความเข้าใจ
หัวข้อที่มีชื่อว่า “เช็กความเข้าใจ”- ทำไมปุ่ม active-low อ่านได้ 0 ตอนกด
- กดครั้งเดียวแต่ตัวนับขึ้นสองครั้ง สาเหตุคืออะไรและแก้อย่างไร
คำตอบอยู่ใน README ของตัวอย่างและในโค้ด ถ้าตอบข้อใดไม่ได้ ให้กลับไปอ่านส่วน Why / What / How อีกครั้ง
แหล่งอ้างอิง
หัวข้อที่มีชื่อว่า “แหล่งอ้างอิง”- แบบฝึกทั้งหมดของ TESAIoT Dev Kit · commit
e5c7722 - โค้ดเป็นของ Developer Hub และอ้างอิงด้วยลิงก์ ไม่ได้คัดลอกเข้าคลังนี้
คำถามทบทวน
ลองตอบเองก่อน แล้วค่อยเปิดดูเฉลย
-
ทำไมโค้ดถือว่าปุ่มถูกกดเมื่อ Cy_GPIO_Read() คืน 0 (เป้าหมายข้อ 1)
- เพราะ PDL กลับค่าให้อัตโนมัติ
- เพราะขาถูกตั้งเป็น CY_GPIO_DM_PULLUP ตอนปล่อย pull-up ภายในดึงขาเป็น HIGH เมื่อกด ปุ่มต่อขาลงกราวด์จึงอ่านได้ LOW
- เพราะปุ่มต่อกับไฟ 3.3 V เมื่อกด
- เพราะ 0 หมายถึงยังไม่มีการอ่านค่า
ดูเฉลย
คำตอบ: B. เพราะขาถูกตั้งเป็น CY_GPIO_DM_PULLUP ตอนปล่อย pull-up ภายในดึงขาเป็น HIGH เมื่อกด ปุ่มต่อขาลงกราวด์จึงอ่านได้ LOW
button_inputs_init() เรียก Cy_GPIO_Pin_FastInit(…, CY_GPIO_DM_PULLUP, 1UL, HSIOM_SEL_GPIO) และ update_button() ใช้ sampled_pressed = (0U == Cy_GPIO_Read(…)) รูปแบบ active-low นี้นิยมเพราะใช้ pull-up ภายในชิปได้โดยไม่ต้องมีตัวต้านทานภายนอก
-
ถ้าตั้งขาปุ่มเป็น input แบบ high-Z ไม่มี pull-up ขณะที่ปุ่มต่อลงกราวด์อย่างเดียว จะเกิดอะไร (เป้าหมายข้อ 1)
- อ่านได้ 1 ตลอดเวลา
- ทำงานเหมือนเดิมทุกอย่าง
- ตอนปล่อยปุ่มขาลอย ค่าที่อ่านได้แกว่งตามสัญญาณรบกวน ตัวนับอาจเพิ่มเองโดยไม่มีใครกด
- ชิปเสียหายทันที
ดูเฉลย
คำตอบ: C. ตอนปล่อยปุ่มขาลอย ค่าที่อ่านได้แกว่งตามสัญญาณรบกวน ตัวนับอาจเพิ่มเองโดยไม่มีใครกด
เมื่อปุ่มเปิด ไม่มีอะไรกำหนดแรงดันให้ขา (floating) debounce กรองการแกว่งสั้น ๆ ได้บ้าง แต่ถ้าขาลอยค้างที่ LOW นานพอ โค้ดก็นับเป็นการกด pull-up ทำให้สถานะตอนปล่อยแน่นอน
-
ตัวอย่าง poll ทุก 25 ms และ BUTTON_DEBOUNCE_TICKS = 2 ทำไมกดครั้งเดียวจึงนับแค่หนึ่ง แม้หน้าสัมผัสจะเด้งหลายครั้งในช่วงแรก (เป้าหมายข้อ 1)
- ระดับใหม่ต้องถูกอ่านได้เหมือนเดิมติดกันจนตัวนับครบ 2 รอบ (เห็นระดับเดียวกัน 3 ครั้งติด ราว 50 ms) จึงเปลี่ยน stable_pressed และ press_count เพิ่มเฉพาะตอน stable_pressed เปลี่ยนจาก false เป็น true
- เพราะ LVGL กรองการกดซ้ำให้
- เพราะ pull-up ภายในดูดซับการเด้ง
- เพราะ Cy_GPIO_Read() คืนค่าเฉลี่ย
ดูเฉลย
คำตอบ: A. ระดับใหม่ต้องถูกอ่านได้เหมือนเดิมติดกันจนตัวนับครบ 2 รอบ (เห็นระดับเดียวกัน 3 ครั้งติด ราว 50 ms) จึงเปลี่ยน stable_pressed และ press_count เพิ่มเฉพาะตอน stable_pressed เปลี่ยนจาก false เป็น true
ใน update_button() เมื่อค่าที่อ่านต่างจากครั้งก่อน debounce_count ถูกรีเซ็ตเป็น 0 ค่าต้องคงเดิมต่อไปอีกสองรอบจึงยอมรับ การเด้งที่สั้นกว่านั้นจึงไม่ทำให้สถานะเปลี่ยน ถ้าเพิ่ม BUTTON_REFRESH_PERIOD_MS เป็น 100 ms การตอบสนองจะช้าลงเป็นราว 200 ms และ hold time จะละเอียดแค่ 100 ms
-
ในเมนูปุ่มกายภาพ ถ้ากดปุ่ม MOVE (P17.5) ค้างไว้ 3 วินาที highlight จะเลื่อนกี่ครั้ง (เป้าหมายข้อ 2)
- ทุก 30 ms (ราว 100 ครั้ง)
- ทุกวินาที (3 ครั้ง)
- ไม่เลื่อนเลยจนกว่าจะปล่อย
- ครั้งเดียว เพราะ btn_pressed_edge() คืน true เฉพาะขอบที่สถานะเสถียรเปลี่ยนจากปล่อยเป็นกด
ดูเฉลย
คำตอบ: D. ครั้งเดียว เพราะ btn_pressed_edge() คืน true เฉพาะขอบที่สถานะเสถียรเปลี่ยนจากปล่อยเป็นกด
menu_timer_cb() poll ทุก MENU_POLL_MS = 30 ms แต่ทำงานเฉพาะเมื่อ btn_pressed_edge() เจอ press edge การจับขอบแทนการอ่านสถานะค้างเป็นหัวใจของ UI แบบปุ่มล้วน ถ้าอยากให้เลื่อนซ้ำเมื่อกดค้างต้องเขียน auto-repeat เพิ่มเอง
-
ข้อใดถูกต้องเกี่ยวกับเมนูปุ่มกายภาพในตัวอย่างนี้ (เลือกทุกข้อที่ถูก) (เป้าหมายข้อ 2)
- อยู่ที่ Settings (รายการสุดท้าย) แล้วกด MOVE จะวนกลับไป Dashboard
- กด SELECT แสดง “SELECT: <ชื่อรายการ>” โดย highlight ไม่เลื่อน
- ต้องแตะจอเพื่อยืนยันก่อน SELECT จะทำงาน
- ปุ่มแต่ละตัวมีสถานะ debounce ของตัวเอง กดสองปุ่มพร้อมกันจึงไม่รบกวนกัน
ดูเฉลย
คำตอบ: A. อยู่ที่ Settings (รายการสุดท้าย) แล้วกด MOVE จะวนกลับไป Dashboard · B. กด SELECT แสดง “SELECT: <ชื่อรายการ>” โดย highlight ไม่เลื่อน · D. ปุ่มแต่ละตัวมีสถานะ debounce ของตัวเอง กดสองปุ่มพร้อมกันจึงไม่รบกวนกัน
s_sel = (s_sel + 1) % MENU_ITEMS ทำให้วนรอบ SELECT แค่เขียนข้อความสถานะ และ s_move กับ s_selb เป็น btn_t คนละตัวที่เก็บ stable, last และ cnt ของตัวเอง เมนูนี้ไม่ใช้ touch เลย จึงเหมาะกับอุปกรณ์แบบ kiosk หรือเครื่องที่ไม่มีจอสัมผัส
อ้างอิงบทเรียนนี้
ถ้านำบทเรียนนี้ไปสอน ทำสไลด์ หรือทำเอกสารต่อ ให้อ้างอิงด้วยข้อความนี้ ถ้าดัดแปลงเนื้อหา ให้เติม (ดัดแปลง)ต่อท้ายชื่อบทเรียน
"ปุ่มกดบนบอร์ดฐานและเมนูที่ไม่ใช้จอสัมผัส" จาก TESA Open Knowledge โดยสมาคมสมองกลฝังตัวไทย (Thai Embedded Systems Association: TESA) https://github.com/tesaiot/tesa-qualification-program สัญญาอนุญาต CC BY-NC 4.0
ข้อความอ้างอิงภาษาอังกฤษ: "Base-board buttons and a menu without touch" from TESA Open Knowledge by the Thai Embedded Systems Association (TESA), https://github.com/tesaiot/tesa-qualification-program, licensed under CC BY-NC 4.0
ลิงก์บทเรียน: https://tesaiot.github.io/tesa-qualification-program/courses/tesaiot-firmware-stack/m04-qwa309-hardware/l01-buttons-and-menu/
บทเรียนนี้ดัดแปลงจากต้นฉบับด้านล่าง เมื่ออ้างอิงให้คงเครดิตต้นฉบับไว้ด้วย
https://github.com/tesaiot/developer-hub/blob/e5c772252e7d20f715463e0d27df9ece4e569c38/prac_qwa309_button_monitor · Code stays in the Developer Hub and is linked at pinned commits, never copied: the episodes, practice codes and main-branch examples are Apache-2.0; the master template and the OPTIGA client carry Infineon/Cypress EULAs.
TESA Open Knowledge · © 2026 สมาคมสมองกลฝังตัวไทย (TESA) · CC BY-NC 4.0
เนื้อหาเผยแพร่ภายใต้ CC BY-NC 4.0 นำไปใช้ต่อในงานที่ไม่ใช่เพื่อการค้าได้ โปรดอ้างอิงสมาคมสมองกลฝังตัวไทย (TESA) ทุกครั้ง · วิธีอ้างอิง TESA