Analog Multiplexing: 10 ปุ่มบน 1 ขา Interrupt
บทนำ
Interrupts นี่มันของดี! นอกจากจะทำให้โค้ดบางทีดูง่ายขึ้นแล้ว มันยังใช้จับเวลาพอดีเป๊ะ หรือปลุก Arduino ให้ตื่นจากโหมดหลับได้อีกด้วย
สมมติว่าน้องทำ UI เป็นรีโมทคอนโทรลที่ใช้แบตเตอรี่ น้องอาจจะอยากให้ Arduino (หรือ ATmega ตัวเดี่ยว) เข้าโหมด power-down เพื่อประหยัดไฟ แต่เวลาที่ Arduino หลับแบบนั้น มันจะตื่นได้ก็ต่อเมื่อมี external interrupt เท่านั้น ชิป ATmega328P ที่อยู่ใน Arduino Uno นี่มี external pin interrupts แค่ 2 ขานะ (INT0 กับ INT1 ที่ขา 2 กับ 3) แต่ UI ของเรามักจะมีปุ่มมากกว่า 2 ปุ่มอยู่แล้ว งานนี้มีปัญหา!
วิธีมาตรฐานที่จะแก้ก็คือ ต่อปุ่มทั้งหมดแบบปกติ แต่ก็ต่อพวกมันเข้ากับขา interrupt ด้วยไดโอดด้วย แต่วิธีนี้จะทำให้วงจรซับซ้อนขึ้นมากๆ

นอกจาก external interrupts แบบมาตรฐานแล้ว ATmega328P ยังมี pin change interrupts อีกด้วย มีไลบรารีหลายตัวจัดการเรื่องนี้ และมันก็เป็นทางออกที่ดีสำหรับปัญหานี้
แต่! ตอนแข่งโปรแกรมมิ่งอยู่ดีๆ พี่ก็คิดวิธีทำแบบนี้โดยใช้แค่ external interrupts แบบมาตรฐาน โดยไม่ต้องใช้อุปกรณ์อิเล็กทรอนิกส์เพิ่มเติมเลย
วงจร
เรามีปุ่มกดหลายปุ่ม พี่ใช้สิบปุ่ม เพราะมันวางบนเบรดบอร์ดได้พอดี (และพี่ก็ไม่มีมากกว่านี้แล้ว) น้องสามารถใช้ปุ่มหนึ่งปุ่มต่อหนึ่งพินได้ ซึ่งหมายความว่าสูงสุด 20 ปุ่มบน Uno และสูงสุด 70 ปุ่มบน Mega! (ถ้าน้องต้องการปุ่ม 70 ปุ่มจริงๆ พี่แนะนำให้ใช้ multiplexing ไปเลย ไม่ต้องใช้ Mega ทั้งตัวหรอก)
ปุ่มแต่ละปุ่นจะมีด้านหนึ่งต่อกับพินไหนก็ได้ (ของพี่คือพิน 4-13) ส่วนอีกด้านของปุ่มทุกปุ่มจะต่อรวมกันเข้าที่ขา interrupt เพียงขาเดียว (ของพี่คือขา 2)




โค้ด
โค้ดอยู่ด้านล่างเลยจ้า อยากให้ตัวอย่างนี้ทำงานก็อัปโหลดลงบอร์ดซะ เปิด Serial Monitor ขึ้นมา เวลากดปุ่มไหน หมายเลขปุ่มมันก็จะโผล่มาให้เห็น อย่างที่เห็นนะ เราไม่ได้ใช้ฟังก์ชัน loop() เลยสักนิด

มันทำงานยังไง?
แน่นอนว่าต้องมีอินเตอร์รัพต์ (Interrupt) อยู่แล้ว ในเคสของพี่ มันต่ออยู่กับพิน 2 และตั้งค่าเป็น FALLING
เพื่อไม่ต้องใช้ไดโอด (Diodes) อะไรให้ยุ่งยาก Arduino ของเราจะสลับการต่อวงจรแบบเรียลไทม์ มีโหมดการทำงาน 2 แบบ: โหมด Common กับ โหมด Distinct
โหมด Common (โหมดรวม)
ส่วนใหญ่แล้ววงจรจะทำงานในโหมดนี้ พินอินเตอร์รัพต์จะถูกตั้งเป็น INPUT_PULLUP ส่วนพินอื่นๆ จะเป็น OUTPUT และดันค่าเป็น LOW

void configureCommon() {
pinMode(commonPin, INPUT_PULLUP);
for (int i = 0; i < sizeof(buttonPins) / sizeof(int); i++) {
pinMode(buttonPins[i], OUTPUT);
digitalWrite(buttonPins[i], LOW);
}
}
ในโหมด Common นี่แหละ พอกดปุ่มไหนก็ตาม มันจะดึงพินอินเตอร์รัพต์ของเราลงสู่ LOW ทันที แล้วก็เรียกอินเตอร์รัพต์ขึ้นมา พออินเตอร์รัพต์ทำงาน ISR (Interrupt Service Routine) ของเราก็จะรีบสลับพินทั้งหมดไปทำงานในโหมด Distinct
โหมด Distinct (โหมดแยก)
พออินเตอร์รัพต์ทำงาน เราก็จะสลับไปโหมด Distinct เร็วสุดๆ
โหมด Distinct นี่ตรงกันข้ามกับโหมด Common เลย พินอินเตอร์รัพต์จะกลายเป็น OUTPUT และ LOW ส่วนพินอื่นๆ จะกลายเป็น INPUT_PULLUP

void configureDistinct() {
pinMode(commonPin, OUTPUT);
digitalWrite(commonPin, LOW);
for (int i = 0; i < sizeof(buttonPins) / sizeof(int); i++) {
pinMode(buttonPins[i], INPUT_PULLUP);
}
}
ในโหมด Distinct นี้ พินที่ถูกดึงลงสู่ LOW จะเป็นพินที่ตรงกับปุ่มที่เรากดจริงๆ เท่านั้น เราก็แค่ไล่เช็คพินทั้งหมดทีละตัว ง่ายๆ ก็เจอแล้วว่าปุ่มไหนเป็นคนก่อเรื่อง
เสร็จแล้ว Arduino ก็สามารถสลับกลับไปโหมด Common เพื่อรออินเตอร์รัพต์ครั้งต่อไปได้ หรือถ้าโปรแกรมของน้องต้องการ ก็อาจจะอยู่โหมด Distinct เพื่อประมวลผลอินพุตจากผู้ใช้แบบปกติ แล้วค่อยสลับกลับมาโหมด Common ก่อนที่ Arduino จะเข้าสู่โหมดสลีป (Sleep) ก็ได้
ลงลึกกันอีกนิด: Analog Multiplexing & Hardware Interrupt Trigger
Arduino Uno นี่พินดิจิตอลมันน้อยนิดจะแย่ ถ้าน้องต่อคีย์แพด 10 ปุ่มแบบมาตรฐาน น้องใช้พินไป 10 พินเต็มๆ ไม่เหลือให้ต่อ LCD, อุปกรณ์ SPI หรือรีเลย์เลยสักพิน! เทคนิค 10 ปุ่มด้วยอินเตอร์รัพต์เดียว นี้ใช้การมัลติเพล็กซ์แบบอนาล็อกด้วย "บันไดตัวต้านทาน (Resistor Ladder)" ผสมกับฮาร์ดแวร์เกทแบบจัดหนัก มันบีบปุ่มทั้ง 10 ปุ่มให้วิ่งผ่านพินอนาล็อกอินพุต (A0) เพียงพินเดียว พร้อมกับต่อทริกเกอร์หลักตรงไปยังฮาร์ดแวร์อินเตอร์รัพต์พิน 2 โดยตรง ลดการกินพินจากสิบพินเหลือแค่ สองพิน พอดี!
เมทริกซ์บันไดตัวต้านทาน (analogRead())
แทนที่แต่ละปุ่มจะต่อกับ 5V โดยตรง มันจะต่อแบบอนุกรมผ่านตัวต้านทาน (Resistor) ที่ค่าต่างกัน (เช่น 1K, 2K, 3K, 4K...)
- เมื่อกดปุ่มที่ 1 สัญญาณจะไม่ผ่านตัวต้านทานเลย ส่ง 5V
(Analog 1023)เข้าพิน A0 ตรงๆ - เมื่อกดปุ่มที่ 5 กระแสต้องฝ่าตัวต้านทาน 4,000 โอห์ม ส่งแรงดันประมาณ 2.5V
(Analog 512)เข้าพิน A0 - Arduino ก็จะเอาแรงดันที่เข้ามาไปเทียบกับค่าที่คำนวณไว้ในอาร์เรย์ แล้วก็เดาออกเลยว่าปุ่มไหนถูกกด โดยใช้แค่พินเดียว! เก๋ไก๋สไลเดอร์มาก
ฮาร์ดแวร์อินเตอร์รัพท์ทริกเกอร์ขั้นสุดยอด
ถ้า Arduino วิ่งแค่ analogRead(A0) ใน loop() เนี่ย มันก็เสียรอบสัญญาณนาฬิกาไปฟรีๆ กับการคอยเช็คปุ่มที่ยังไม่ได้กด
- เราเลยสร้าง ลอจิกแมทริกซ์แบบ Diode OR Gate ขึ้นมา!
- ทุกปุ่ม จะต่อเอาต์พุตผ่านไดโอด (ป้องกันกระแสย้อน) ตรงไปยัง Digital Pin 2 เลย
- พอน้องกดปุ่ม ใดก็ได้ กระแสจะไหลไปยัง Analog Pin เพื่อวัดค่า แต่ในขณะเดียวกันก็จะไป ทริกเกอร์ฮาร์ดแวร์อินเตอร์รัพท์บน Pin 2 ด้วย!
void setup() {
// รอหลับสบายๆ จนกว่าฮาร์ดแวร์จะเจอแรงดันกระฉูด!
attachInterrupt(digitalPinToInterrupt(2), buttonWakeISR, RISING);
}
void buttonWakeISR() {
int rawAnalog = analogRead(A0); // อ่านค่าแรงดันทันที!
if (rawAnalog > 950) targetButton = 1;
else if (rawAnalog > 800) targetButton = 2;
// ... ตามด้วยตรรกะเปรียบเทียบแรงดันเป็นขั้นบันไดอีกเพียบ!
actionRequired = true; // ส่งสัญญาณให้ loop() ไปทำงานหนักต่อ!
}
อุปกรณ์วงจรแบบอัดแน่น
- Arduino Uno / Nano / Pro Mini.
- ปุ่มกดแบบ Tactile 10 ปุ่ม.
- ตัวต้านทาน (Resistor) ค่าความคลาดเคลื่อน 1% แม่นยำสุดๆ (เช่น 1K Ohm) ตัวต้านทาน 5% ทั่วไปอาจทำให้ค่าแรงดันทับซ้อนกันจนแยกไม่ออก ว่าเป็นปุ่ม 6 หรือปุ่ม 7!
- ไดโอดสวิตชิ่ง (1N4148) 10 ตัว (สำคัญมาก! ใช้สร้างบัสทริกเกอร์หลักโดยไม่ให้ค่าจากตัวต้านทานแต่ละขั้นลัดวงจรเข้าหากัน).
ตัวอย่างที่ซับซ้อนขึ้นอีกนิด
มาลองทำอะไรที่มันขั้นกว่าเดิมกันดีกว่า เราจะต่อเซอร์โวและแมปแต่ละปุ่มให้เป็นมุมที่ต่างกัน (1=0°, 2=20°... 10=120°) และใช้แบตเตอรี่จ่ายไฟให้ Arduino ด้วย



ในตัวอย่างนี้ เราตั้งให้ Arduino เข้าสู่โหมดพัก (power-down) หลังจากไม่มีการใช้งาน 5 วินาที เพื่อประหยัดพลังงาน ส่วนเรื่องโหมดสลีป มีสอนกันเยอะตามเน็ตอยู่แล้ว ฮ่าๆ นอกจากนั้น เรายังจ่ายไฟให้เซอร์โวผ่านทรานซิสเตอร์เพื่อปิดมันเมื่อไม่ใช้งานด้วย
โค้ดสำหรับตัวอย่างนี้ก็แนบมาด้วยด้านล่างเลย