ตัวอย่างการสร้างคลาส C++ เพื่อใช้งานสำหรับ Arduino: 4x4 Keypad#
Keywords: Arduino, AVR, Object-Oriented Programming, User-defined C++ Class, 4x4 Keypad, Membrane Keypad
▷ การตรวจสอบสถานะปุ่มกด: Keypad Scan#
บทความนี้กล่าวถึงตัวอย่างการสร้างคลาสในภาษา C++ สำหรับใช้งานร่วมกับ Arduino เพื่อนำมาใช้ในการตรวจสอบดูว่า มีการกดปุ่มคีย์ (Keypress) บนโมดูลที่มีลักษณะเป็นแผ่นปุ่มกดขนาด 4x4 ในรูปแบบที่เรียกว่า Membrane Keypad และมีข้อดีคือ มีความทนทาน กันน้ำกันฝุ่นได้ดี และผู้ใช้จะสามารถกดปุ่มได้เพียงครั้งละหนึ่งปุ่มเท่านั้น ซึ่งมีความแตกต่างจากแป้นคีย์ที่เป็นปุ่มกดแบบ Tactile / Mechanical Keypad โดยทั่วไป
รูป: ตัวอย่างโมดูล Mechanical 4x4 Keypad (Alphanumeric) สำหรับการเปรียบเทียบ (Source: Sparkfun)
รูป: ตัวอย่างโมดูล 4x4 Membrane Keypad ที่ได้นำมาทดลองใช้งาน พร้อม Male Pin Headers แบบขายาว สำหรับการแปลงคอนเนกเตอร์แบบตัวเมียให้เป็นตัวผู้ (Female-to-Male)
ขาสัญญาณ I/O ของโมดูล 4x4 Membrane Keypad มี 8 ขา แบ่งเป็น 4 ขา สำหรับขาสัญญาณในแต่ละแถวแนวนอน
(Rows: R0..R3) และขาสำหรับสัญญาณในแต่ละแถวแนวตั้งหรือคอลัมน์ (Columns: C0..C3)
หากดูรูปตัวอย่างของโมดูล จะเห็นว่า ตำแหน่ง R0C0 ก็คือ ปุ่ม '1'
เรียงไปตามลำดับจนถึง R3C3 ซึ่งก็คือ ปุ่ม 'D'
รูป: ขาสัญญาณ I/O ของโมดูล 4x4 Membrane Keypad
การสแกนแป้นปุ่มกด (Keypad) ก็คือ การตรวจสอบว่า มีการกดปุ่มใด ๆ หรือไม่ โดยจะทำไปทีละแถวในแนวนอน หรือแนวตั้งก็ได้ โดยเลือกแบบใดแบบหนึ่ง
- รูปแบบที่ 1) ให้ R0..R3 ต่อขากับอินพุต-ดิจิทัลของไมโครคอนโทรลเลอร์ ซึ่งจะต้องมีการต่อตัวต้านแบบ Pull-up ทั้งหมด 4 ตัว ที่ขาสัญญาณดังกล่าว และให้ C0..C3 ต่อกับขาเอาต์พุต-ดิจิทัลของไมโครคอนโทรลเลอร์
- รูปแบบที่ 2) ให้ C0..C3 ต่อขากับอินพุต-ดิจิทัลของไมโครคอนโทรลเลอร์ ซึ่งจะต้องมีการต่อตัวต้านแบบ Pull-up ทั้งหมด 4 ตัว ที่ขาสัญญาณดังกล่าว และให้ R0..R3 ต่อกับขาเอาต์พุต-ดิจิทัลของไมโครคอนโทรลเลอร์
ในบทความนี้ จะเลือกใช้รูปแบบที่ 1 ในการต่อวงจร และเขียนโค้ดเพื่อตรวจสอบการกดปุ่ม
- การตรวจสอบจะเริ่มต้นที่คอลัมน์ C0 แล้วเรียงตามลำดับไปถึง C3 จนครบหนึ่งรอบ
- ในแต่ละคอลัมน์ จะมีการกำหนดค่าเอาต์พุตให้ขา C0..C3 ดังนี้ หากต้องการจะตรวจสอบคอลัมน์ใด ก็ให้เอาต์พุตคอลัมน์นั้นเป็น 0 (Low) และให้เอาต์พุตของคอลัมน์อื่นเป็น 1 (High) แล้วจึงอ่านค่าอินพุตที่ขา R0..R3 จำนวน 4 บิต (สามารถอ่านค่าไปทีละขาได้)
- ค่าของเอาต์พุตที่ขา C0..C3 จะมีรูปแบบดังนี้: "0111" -> "1011" -> "1101" -> "1110" เรียงไปตามลำดับ
- ถ้าขาอินพุตใด (R0..R3) มีค่าเป็น 0 แสดงว่า ในขณะนั้นมีการกดปุ่มตรงกับแถวนอนและคอลัมน์ที่กำลังตรวจสอบอยู่ ค่าพิกัดหรือหมายเลขแถวแนวนอนและแนวตั้ง จะถูกนำไปใช้สำหรับระบุว่าเป็นคีย์ในที่มีทั้งหมด 16 กรณี
- หากตรวจสอบแล้วว่า ไม่มีการกดปุ่มใด ๆ ในระหว่างการสแกนตรวจสอบหนึ่งรอบ ก็จะได้ค่าเป็น
'\0'
(NO_KEY
)
▷ การใช้ไลบรารี: Arduino Keypad Library#
ถัดไปเป็นการสาธิตการใช้งานไลบรารีสำหรับ Arduino เพื่อตรวจสอบสถานะการกดปุ่ม ในตัวอย่างนี้ได้เลือกใช้ไลบรารีที่มีชื่อว่า
Keypad
ดังนั้นจะต้องมีการติดตั้งไลบรารีดังกล่าวใน Arduino IDE ก่อน
โดยใช้ Arduino Library Manager (ไปที่เมนู Tools > Manage Libraries )
ในไฟล์ Arduino Sketch สำหรับทดลองใช้งานไลบรารีดังกล่าว จะต้องมีคำสั่ง #include <Keypad.h>
จากนั้นจึงสามารถใช้งานคลาสที่มีชื่อว่า Keypad
ได้
เริ่มต้นจะต้องมีการอ็อบเจกต์จากคลาส Keypad
(อ้างอิงโดยตัวแปรชื่อ keypad
ในโค้ดตัวอย่าง)
และจะต้องมีการกำหนดรายการขาหรือพินของ Arduinoจำนวน 8 ขา แบ่งเป็น 2 อาร์เรย์
สำหรับแถวแนวนอนและแถวแนวตั้ง (คอลัมน์) ซึ่งอ้างอิงโดยตัวแปร ROW_PINS[]
และ COL_PINS[]
ตามลำดับ ขาสำหรับ R0..R3 คือ {D9, D8, D7, D6} และขาสำหรับ
C0..C3 คือ {D5, D4, D3, D2} ตามลำดับ
นอกจากนั้นแล้วยังต้องระบุอาร์เรย์แบบ 2 มิติ (อ้างอิงโดยตัวแปร keys[][]
ในโค้ดตัวอย่าง)
เพื่อใช้ระบุค่าของคีย์ในแต่ละตำแหน่งของปุ่มกด
เนื่องจากว่า ขาสำหรับแถวแนวนอน (Rows: R0..R3) ได้ถูกกำหนดให้เป็นขาอินพุต ดังนั้นจึงต้องต่อตัวต้านทานแบบ Pull-up แต่ในตัวอย่างนี้ได้เลือกการใช้งานแบบ Internal Pull-up จึงไม่จำเป็นต้องต่อตัวต้านทานภายนอก
// source: https://github.com/Chris--A/Keypad
#include <Keypad.h> // use the Arduino Keypad library
#define NUM_ROWS (4)
#define NUM_COLS (4)
const byte ROW_PINS[] = {9,8,7,6};
const byte COL_PINS[] = {5,4,3,2};
const char keys[NUM_ROWS][NUM_COLS] = {
{'1','2','3','A'},
{'4','5','6','B'},
{'7','8','9','C'},
{'*','0','#','D'}
};
// create a Keypad object
Keypad keypad = Keypad( (char *)keys,
ROW_PINS, COL_PINS,
NUM_ROWS, NUM_COLS );
void setup(){
Serial.begin( 115200 );
for (int i=0; i < NUM_ROWS; i++ ) {
// enable internal pullup on input pins for R0..R3
pinMode( ROW_PINS[i], INPUT_PULLUP );
}
}
char sbuf[32]; // string buffer for sprintf()
char key; // keypress
void loop(){
if ( (key=keypad.getKey()) != NO_KEY ) { // get key
sprintf( sbuf, "Key: '%c' @%lu msec", key, millis() );
Serial.println( sbuf );
}
}
▷ การจำลองการทำงานด้วย Wokwi AVR Simulator#
การตรวจสอบหรือทดลองการทำงานของโค้ดตัวอย่าง สามารถทำได้ในเบื้องต้นโดยใช้ซอฟต์แวร์จำลองการทำงาน และยังไม่จำเป็นต้องมีอุปกรณ์จริง ถัดไปให้สร้างโปรเจกต์ใหม่บนหน้าเว็บของ WokWi AVR Simulator และต่อวงจรตามรูปตัวอย่าง
รูป: ตัวอย่างการต่อวงจรเสมือนจริงโดยใช้บอร์ด Arduino Nano v3 และโมดูล 4x4 Membrane Keypad
รูป: ในส่วนที่เรียกว่า Library Manager ของ WokWi Simulator ให้ค้นหาไลบรารีตามชื่อ Keypad
แล้วกดปุ่มเพิ่มไลบรารีเพื่อใช้งานในโปรเจกต์ (จากนั้นจะมีรายการเพิ่มในไฟล์ libraries.txt
)
รูป: แสดงผลการจำลองการทำงานของโค้ดตัวอย่าง เมื่อมีการกดปุ่มต่าง ๆ บน Keypad
▷ การตรวจสอบการกดปุ่มโดยไม่ใช้ไลบรารี#
จากตัวอย่างที่แล้วซึ่งได้แสดงให้เห็นตัวอย่างการใช้งานไลบรารีสำหรับ Arduino เพื่ออ่านค่าจาก Keypad ถัดไปเป็นการสาธิตการเขียนโค้ด โดยไม่ใช้ไลบรารีดังกล่าว ทั้งนี้ก็เพื่อเป็นการฝึกเขียนโค้ด และจะทำให้เข้าใจวิธีการสแกนปุ่มกดสำหรับ Keypad
ในตัวอย่างนี้ มีการสร้างฟังก์ชัน getKey()
เพื่อใช้สำหรับการสแกนปุ่มกดในหนึ่งรอบซึ่งจะมีการตรวจสอบไปทีละคอลัมน์ (C0..C3)
โดยจะทำทุก ๆ 20 มิลลิวินาทีต่อหนึ่งรอบ (กำหนดค่าโดยใช้สัญลักษณ์ SCAN_INTERVAL_MS
)
หากพบว่ามีปุ่มกดในตำแหน่งใดถูกกดอยู่ในขณะนั้น ฟังก์ชันนี้จะให้ค่าของปุ่มกดดังกล่าวเป็นข้อมูลแบบ char
ตามที่กำหนดไว้ในอาร์เรย์
KEY_MAP[][]
แต่ถ้าไม่มีการกดปุ่มหรือไม่มีการเปลี่ยนแปลงการกดปุ่มเกิดขึ้น จะให้ค่าเป็น NO_KEY
#define NUM_ROWS (4)
#define NUM_COLS (4)
#define SCAN_INTERVAL_MS (20)
#define NO_KEY ('\0')
const uint8_t ROW_PINS[] = {9,8,7,6};
const uint8_t COL_PINS[] = {5,4,3,2};
const char KEY_MAP[NUM_ROWS][NUM_COLS] = {
{'1','2','3','A'},
{'4','5','6','B'},
{'7','8','9','C'},
{'*','0','#','D'}
};
char getKey(); // function prototype
void setup(){
Serial.begin( 115200 );
for ( uint8_t i=0; i < NUM_ROWS; i++ ) {
// enable internal pullup on input pins for R0..R3
pinMode( ROW_PINS[i], INPUT_PULLUP );
}
for ( uint8_t i=0; i < NUM_COLS; i++ ) {
uint8_t col = COL_PINS[i];
pinMode( col, OUTPUT );
digitalWrite( col, HIGH );
}
}
// global variables
char sbuf[32]; // string buffer for sprintf()
char key; // keypress
void loop(){
if ( (key=getKey()) != NO_KEY ) { // get key
sprintf( sbuf, "Key: '%c' @%lu msec", key, millis() );
Serial.println( sbuf );
}
}
// global variables
uint32_t ts = 0; // used to save the timestamp for keypad scan
char prev_key = NO_KEY; // used to save the keypress
char getKey() {
uint32_t now = millis();
char new_key = NO_KEY;
if ( now - ts >= SCAN_INTERVAL_MS ) {
ts = now; // saved timestamp
// scan column by column
for ( uint8_t c=0; c < NUM_COLS; c++ ) {
for ( uint8_t i=0; i < NUM_COLS; i++) {
// pull only one column to LOW, the rest to HIGH
digitalWrite( COL_PINS[i], (c==i) ? LOW : HIGH );
}
uint8_t row_input;
// read the row inputs one by one
for ( uint8_t r=0; r < NUM_ROWS; r++) {
row_input = digitalRead( ROW_PINS[r] );
if ( row_input == 0 ) { // active
// get the associated key char
new_key = KEY_MAP[r][c];
}
}
// relase the pulled-down column
digitalWrite( COL_PINS[c], HIGH );
if ( new_key != NO_KEY ) {
if ( prev_key != new_key ) {
prev_key = new_key;
return new_key; // new keypress detected
} else {
return NO_KEY;
}
}
}
prev_key = new_key;
}
return NO_KEY;
}
รูป: แสดงผลการจำลองการทำงานของโค้ดตัวอย่าง
รูป: คอมไพล์โค้ดตัวอย่างด้วย Arduino IDE
รูป: ทดลองกับอุปกรณ์จริง
▷ ตัวอย่างการสร้างคลาสสำหรับ Keypad#
ถัดไปเป็นการสร้างคลาส C++ เพื่อนำมาใช้งานกับโมดูล Keypad ในเบื้องต้น แทนที่การใช้งานไลบรารี
Arduino Keypad
(ดาวน์โหลดไฟล์ keypad_demo.zip
) โดยแบ่งออกเป็น 3 ส่วน คือ
- ไฟล์
Keypad.h
ซึ่งเป็นการกำหนดรูปแบบของคลาส - ไฟล์
Keypad.cpp
เป็นส่วนที่สร้างคลาสตามรูปแบบที่กำหนด และ - ไฟล์
keypad_demo.ino
เป็นตัวอย่างการใช้งานคลาสดังกล่าว
File: Keypad.h
(C++ Header File)
#ifndef _KEYPAD_H
#define _KEYPAD_H
#include <Arduino.h>
#include <inttypes.h>
#define SCAN_INTERVAL_MS (20)
#define NO_KEY ('\0')
class Keypad {
public:
Keypad( const char *keymap,
const uint8_t *rowPins, const uint8_t *colPins,
uint8_t numRows, uint8_t numCols );
char getKey();
private:
char * _keymap;
uint8_t * _row_pins;
uint8_t * _col_pins;
uint8_t _num_rows;
uint8_t _num_cols;
char _prev_key;
uint32_t _ts;
};
#endif // _KEYPAD_H
File: Keypad.cpp
(C++ Class Implementation File)
#include "Keypad.h"
Keypad::Keypad( const char *keymap,
const uint8_t *rowPins,
const uint8_t *colPins,
uint8_t numRows, uint8_t numCols )
{
_keymap = keymap;
_row_pins = rowPins;
_col_pins = colPins;
_num_rows = numRows;
_num_cols = numCols;
_prev_key = NO_KEY;
_ts = 0;
for ( uint8_t i=0; i < _num_rows; i++ ) {
// enable internal pullup on input pins for R0..R3
pinMode( _row_pins[i], INPUT_PULLUP );
}
for ( uint8_t i=0; i < _num_cols; i++ ) {
uint8_t col = _col_pins[i];
pinMode( col, OUTPUT );
digitalWrite( col, HIGH );
}
}
char Keypad::getKey() {
uint32_t now = millis();
char new_key = NO_KEY;
if ( now - _ts >= SCAN_INTERVAL_MS ) {
_ts = now; // saved timestamp
// scan column by column
for ( uint8_t c=0; c < _num_cols; c++ ) {
for ( uint8_t i=0; i < _num_cols; i++) {
// pull only one column to LOW, the rest to HIGH
digitalWrite( _col_pins[i], (c==i) ? LOW : HIGH );
}
uint8_t row_input;
// read the row inputs one by one
for ( uint8_t r=0; r < _num_rows; r++) {
row_input = digitalRead( _row_pins[r] );
if ( row_input == 0 ) { // active
// get the associated key char
new_key = _keymap[r*_num_rows + c];
}
}
// relase the pulled-down column
digitalWrite( _col_pins[c], HIGH );
if ( new_key != NO_KEY ) {
if ( _prev_key != new_key ) {
_prev_key = new_key;
return new_key; // new keypress detected
} else {
return NO_KEY;
}
}
}
_prev_key = new_key;
}
return NO_KEY;
}
File: keypad_demo.ino
#include "Keypad.h"
#define NUM_ROWS (4)
#define NUM_COLS (4)
const byte ROW_PINS[] = {9,8,7,6};
const byte COL_PINS[] = {5,4,3,2};
const char keys[NUM_ROWS][NUM_COLS] = {
{'1','2','3','A'},
{'4','5','6','B'},
{'7','8','9','C'},
{'*','0','#','D'}
};
// create a Keypad object
Keypad keypad = Keypad( (char *)keys,
ROW_PINS, COL_PINS,
NUM_ROWS, NUM_COLS );
void setup(){
Serial.begin( 115200 );
}
char sbuf[32]; // string buffer for sprintf()
char key; // keypress
void loop(){
if ( (key=keypad.getKey()) != NO_KEY ) { // get key
sprintf( sbuf, "Key: '%c' @%lu msec", key, millis() );
Serial.println( sbuf );
}
}
▷ กล่าวสรุป#
บทความนี้ได้นำเสนอตัวอย่างการออกแบบและสร้างคลาสในภาษา C++ ในเบื้องต้น เพื่อนำไปใช้งานกับบอร์ด Arduino โดยได้เลือกตัวอย่างเป็นโมดูล 4x4 Membrane Keypad และเขียนโค้ดโดยใช้ไลบรารีที่มีอยู่แล้วสำหรับ Arduino เปรียบเทียบกับการลองสร้างไลบรารีที่มีฟังก์ชันการทำงานอย่างง่ายในรูปแบบของ C++ Class ตรวจสอบผลการทำงานของโค้ดด้วยใช้ซอฟต์แวร์ WokWi Simulator
This work is licensed under a Creative Commons Attribution-ShareAlike 4.0 International License.
Created: 2022-02-19 | Last Updated: 2022-11-07