Bàn phím 4x4 và còi với ESP32: quét ma trận, chống dội, khóa mật khẩu và âm báo không cần thư viện

Cách quét bàn phím ma trận 4x4, chống dội phím, nối còi thụ động qua transistor, chương trình khóa mật khẩu có đổi mật khẩu, khóa tạm khi nhập sai và âm báo không dùng delay.

Bàn phím màng 4×4 có 16 phím nhưng chỉ cần 8 dây, nhờ cách nối thành ma trận hàng – cột. Ghép với một chiếc còi nhỏ là đủ làm khóa cửa mật khẩu, bảng nhập số cho máy đếm, hay bảng điều khiển cho đồ chơi học tập. Bài này giải thích cách quét ma trận, cách chống dội phím, cách nối còi qua transistor, rồi viết chương trình khóa mật khẩu có đổi mật khẩu, khóa tạm khi nhập sai và âm báo không làm chương trình bị dừng, không cần thư viện ngoài.

1. Bàn phím ma trận quét thế nào

Sơ đồ ma trận 4 hàng R1 đến R4 và 4 cột C1 đến C4, mỗi chỗ giao có một công tắc là một phím; hàng R2 đang ở mức 0, phím 5 đang bấm nên cột C2 bị kéo xuống 0
16 phím nằm ở chỗ giao của 4 hàng và 4 cột; mỗi lúc chỉ một hàng ở mức 0 (hình minh họa).

Mỗi phím là một công tắc nối một hàng với một cột. Chương trình làm như sau:

  1. 4 chân cột đặt INPUT_PULLUP: khi không có gì kéo xuống, cột đọc ra 1.
  2. 4 chân hàng là ngõ ra, bình thường ở mức 1. Lần lượt kéo một hàng xuống 0.
  3. Trong lúc hàng đó ở 0, đọc 4 cột. Cột nào ra 0 thì phím ở chỗ giao của hàng đó với cột đó đang được bấm.
  4. Trả hàng về 1 rồi chuyển sang hàng kế tiếp.

Cả 4 hàng quét xong trong vài chục micro giây, nhanh hơn nhiều so với tay bấm. Khi bấm cùng lúc ba phím ở ba góc của một hình chữ nhật, phím ở góc thứ tư cũng có vẻ như đang bấm (hiện tượng "phím ma"). Chương trình chỉ nhận một phím một lúc nên không bị ảnh hưởng; bàn phím cần nhận nhiều phím cùng lúc phải thêm diode cho từng phím.

2. Chống dội phím

Ba dòng tín hiệu theo thời gian: chân cột C2 dao động vài lần khi bấm và khi nhả; các lần quét cách nhau 10 ms đọc ra 1 hoặc 0; tín hiệu phím đã nhận chỉ đổi sau ba lần quét giống nhau, có chấm đỏ đánh dấu lúc nhận bấm và nhận nhả
Tiếp điểm dội vài mili giây khi bấm và khi nhả; ba lần quét giống nhau mới được nhận (tín hiệu ví dụ).

Tiếp điểm của phím không đóng gọn một lần mà nảy lên xuống vài mili giây. Nếu đọc liên tục, một lần bấm có thể bị đếm thành hai, ba lần. Chương trình quét 10 ms một lần và chỉ chấp nhận kết quả khi 3 lần quét liền nhau giống nhau (30 ms). Ngoài ra nó chỉ báo phím đúng lúc phím vừa được bấm xuống; giữ phím lâu cũng chỉ ra một ký tự.

Cách quét theo thời gian (dùng millis()) thay vì delay() giúp vòng lặp vẫn làm được việc khác, như phát âm báo hay đếm giờ mở khóa.

3. Linh kiện cần chuẩn bị

  • ESP32 DevKit V1 30 chân (hoặc loại 38 chân) và dây USB nạp code.
  • Bàn phím màng 4×4 (dây dẹt 8 sợi).
  • Còi thụ động (loại không tự kêu khi cấp điện một chiều), transistor NPN S8050 hoặc 2N2222, điện trở 1 kΩ, diode 1N4148.
  • Breadboard và dây cắm.
  • Muốn đóng mở khóa điện thật: module relay và khóa chốt điện (khóa từ) đúng điện áp của nó.

4. Nối bàn phím và còi với ESP32

Sơ đồ nối bàn phím 4x4 với ESP32 DevKit V1: R1 D13, R2 D14, R3 D27, R4 D26, C1 D25, C2 D33, C3 D32, C4 D4; mạch còi 5V vào VIN, IN vào D23, GND vào GND
Hàng R1–R4 vào GPIO13, 14, 27, 26; cột C1–C4 vào GPIO25, 33, 32, 4; mạch còi vào GPIO23 (hình minh họa).
Bàn phímR1R2R3R4C1C2C3C4
ESP32D13D14D27D26D25D33D32D4

Thứ tự 8 sợi của bàn phím màng thường là R1 R2 R3 R4 C1 C2 C3 C4 tính từ trái khi nhìn mặt phím, nhưng không phải hãng nào cũng vậy. Dùng đồng hồ vạn năng để kiểm tra: giữ phím 1, sợi R1 và sợi C1 phải thông nhau; giữ phím D, sợi R4 và C4 thông nhau. Bài tránh các chân GPIO12 (ảnh hưởng điện áp bộ nhớ flash lúc khởi động) và GPIO34–39 (chỉ nhận vào, không có điện trở kéo lên).

5. Mạch còi và âm báo

Sơ đồ nguyên lý: D23 qua điện trở 1 kΩ vào base của transistor NPN S8050, còi nối từ 5 V xuống collector, diode 1N4148 song song với còi, emitter nối GND; bên phải là ba dãy âm báo đúng, sai, khóa vẽ theo thời gian và tần số
GPIO chỉ cấp dòng nhỏ vào base; transistor đóng ngắt dòng của còi. Bên phải là ba dãy nốt của chương trình (hình minh họa).

Còi điện từ có điện trở cuộn dây chỉ vài chục ohm, cần dòng lớn hơn mức một chân GPIO nên cấp. Transistor NPN làm công tắc: GPIO23 qua 1 kΩ vào base, còi nối từ 5 V xuống collector. Diode 1N4148 mắc ngược song song với còi để dập điện áp cảm ứng của cuộn dây mỗi lần ngắt. Với còi áp điện (đĩa gốm mỏng), dòng rất nhỏ và không cần diode.

Còi thụ động chỉ kêu khi nhận xung đúng tần số; ESP32 tạo xung bằng khối LEDC: ledcAttach(23, 2000, 10) gắn chân, ledcWriteTone(23, tần số) đổi nốt, ledcWriteTone(23, 0) tắt (chân ở mức 0, transistor ngắt). Xem thêm về LEDC trong bài PWM trên ESP32.

Mỗi âm báo là một dãy nốt (tần số, thời gian). Hàm capNhatCoi() được gọi liên tục trong loop() và tự chuyển sang nốt kế tiếp khi hết giờ, nên trong lúc còi kêu bàn phím vẫn được quét.

6. Chương trình khóa mật khẩu

Cách dùng:

  • Nhập mật khẩu (mặc định 1234) rồi bấm #. Đúng: âm báo đi lên, đèn GPIO2 sáng 3 giây (chỗ này nối relay để mở khóa thật). Sai: âm trầm dài.
  • Bấm * để xóa phần đang nhập.
  • Sai 3 lần liền: bàn phím bị khóa 30 giây, mọi phím chỉ phát âm báo sai.
  • Đổi mật khẩu: bấm A, nhập mật khẩu cũ + #, mật khẩu mới (4–8 số) + #, nhập lại + #. Mật khẩu mới được lưu vào flash bằng thư viện Preferences (xem bài lưu dữ liệu khi mất điện), mất điện không mất.
// PYWORLD.VN - Bàn phím ma trận 4x4 và còi thụ động với ESP32: quét phím chống dội, nhập mật khẩu, đổi mật khẩu, âm báo không chặn chương trình
// Hàng R1-R4 vào GPIO13, 14, 27, 26; cột C1-C4 vào GPIO25, 33, 32, 4. Còi qua transistor NPN ở GPIO23. Đèn GPIO2 sáng 3 giây khi mở khóa
// Nhập mật khẩu rồi bấm #, bấm * để xóa. Bấm A để đổi mật khẩu. Mật khẩu ban đầu 1234, lưu trong flash

#include <Preferences.h>

const int HANG[4] = {13, 14, 27, 26};
const int COT[4] = {25, 33, 32, 4};
const char PHIM[4][4] = {
  {'1', '2', '3', 'A'},
  {'4', '5', '6', 'B'},
  {'7', '8', '9', 'C'},
  {'*', '0', '#', 'D'},
};
const int CHAN_COI = 23, CHAN_KHOA = 2;
const uint32_t THOI_GIAN_MO_MS = 3000;
const int SAI_TOI_DA = 3;
const uint32_t KHOA_TAM_MS = 30000;      // nhập sai 3 lần: khóa bàn phím 30 giây
struct Not { uint16_t tanSo, ms; };     // một nốt của còi; tần số 0 = nghỉ (khai báo trước mọi hàm)

// ---- Quét phím ----
char quetMaTran() {                      // trả về phím đang được bấm, 0 nếu không có
  char ketQua = 0;
  for (int h = 0; h < 4; h++) {
    digitalWrite(HANG[h], LOW);          // chỉ một hàng ở mức thấp tại một thời điểm
    delayMicroseconds(10);               // chờ đường dây ổn định
    for (int c = 0; c < 4; c++) {
      if (digitalRead(COT[c]) == LOW && ketQua == 0) ketQua = PHIM[h][c];
    }
    digitalWrite(HANG[h], HIGH);
  }
  return ketQua;
}

// Quét 10 ms một lần; một phím phải giống nhau 3 lần liền (30 ms) mới được nhận. Trả về phím vừa bấm xuống.
char docPhim() {
  static uint32_t lanQuet = 0;
  static char ungVien = 0, onDinh = 0;
  static int soLan = 0;
  if (millis() - lanQuet < 10) return 0;
  lanQuet = millis();
  char p = quetMaTran();
  if (p == ungVien) {
    if (soLan < 3) soLan++;
  } else {
    ungVien = p;
    soLan = 1;
  }
  if (soLan == 3 && ungVien != onDinh) {
    onDinh = ungVien;
    return onDinh;                       // chỉ báo lúc bấm xuống; giữ phím không lặp lại
  }
  return 0;
}

// ---- Còi: phát một dãy nốt mà không dùng delay() ----
const Not BIP[] = {{2000, 40}};
const Not DUNG[] = {{1319, 90}, {0, 30}, {1760, 90}, {0, 30}, {2637, 160}};
const Not SAI[] = {{330, 400}};
const Not KHOA[] = {{330, 150}, {0, 80}, {330, 150}, {0, 80}, {330, 400}};
const Not *dangPhat = nullptr;
int soNot = 0, viTriNot = 0;
uint32_t batDauNot = 0;

void phat(const Not *day, int n) {
  dangPhat = day;
  soNot = n;
  viTriNot = 0;
  batDauNot = millis();
  ledcWriteTone(CHAN_COI, day[0].tanSo);
}

void capNhatCoi() {
  if (!dangPhat || millis() - batDauNot < dangPhat[viTriNot].ms) return;
  if (++viTriNot >= soNot) {
    ledcWriteTone(CHAN_COI, 0);          // hết dãy: tắt còi (chân ở mức thấp, transistor ngắt)
    dangPhat = nullptr;
    return;
  }
  batDauNot = millis();
  ledcWriteTone(CHAN_COI, dangPhat[viTriNot].tanSo);
}

// ---- Mật khẩu ----
Preferences boNho;
String matKhau, nhap, matKhauMoi;
enum CheDo { NHAP_MK, DOI_NHAP_CU, DOI_NHAP_MOI, DOI_NHAP_LAI } cheDo = NHAP_MK;
int soLanSai = 0;
uint32_t khoaDen = 0, moDen = 0;

void inLoiNhac() {
  const char *nhac[] = {"Nhap mat khau", "Doi mat khau: nhap mat khau cu", "Nhap mat khau moi (4-8 so)", "Nhap lai mat khau moi"};
  Serial.printf("%s, roi bam #\n", nhac[cheDo]);
}

void xuLyXacNhan() {
  if (cheDo == NHAP_MK || cheDo == DOI_NHAP_CU) {
    if (nhap == matKhau) {
      soLanSai = 0;
      phat(DUNG, 5);
      if (cheDo == NHAP_MK) {
        Serial.println("Dung mat khau: mo khoa 3 giay");
        digitalWrite(CHAN_KHOA, HIGH);
        moDen = millis() + THOI_GIAN_MO_MS;
      } else {
        cheDo = DOI_NHAP_MOI;
      }
    } else if (++soLanSai >= SAI_TOI_DA) {
      Serial.println("Sai 3 lan: khoa ban phim 30 giay");
      phat(KHOA, 5);
      khoaDen = millis() + KHOA_TAM_MS;
      soLanSai = 0;
      cheDo = NHAP_MK;
    } else {
      Serial.printf("Sai mat khau (%d/%d)\n", soLanSai, SAI_TOI_DA);
      phat(SAI, 1);
    }
  } else if (cheDo == DOI_NHAP_MOI) {
    if (nhap.length() < 4 || nhap.length() > 8) {
      Serial.println("Mat khau moi phai co 4-8 so");
      phat(SAI, 1);
    } else {
      matKhauMoi = nhap;
      cheDo = DOI_NHAP_LAI;
      phat(BIP, 1);
    }
  } else {                               // DOI_NHAP_LAI
    if (nhap == matKhauMoi) {
      matKhau = matKhauMoi;
      boNho.putString("mk", matKhau);
      Serial.println("Da doi mat khau");
      phat(DUNG, 5);
    } else {
      Serial.println("Hai lan nhap khong giong nhau, giu mat khau cu");
      phat(SAI, 1);
    }
    cheDo = NHAP_MK;
  }
  nhap = "";
  inLoiNhac();
}

void setup() {
  Serial.begin(115200);
  for (int i = 0; i < 4; i++) {
    pinMode(HANG[i], OUTPUT);
    digitalWrite(HANG[i], HIGH);
    pinMode(COT[i], INPUT_PULLUP);       // cột nghỉ ở mức cao, bấm phím thì bị hàng đang thấp kéo xuống
  }
  pinMode(CHAN_KHOA, OUTPUT);
  ledcAttach(CHAN_COI, 2000, 10);
  ledcWriteTone(CHAN_COI, 0);
  boNho.begin("khoa", false);
  matKhau = boNho.getString("mk", "1234");
  inLoiNhac();
}

void loop() {
  capNhatCoi();
  if (moDen && (int32_t)(millis() - moDen) >= 0) {
    digitalWrite(CHAN_KHOA, LOW);
    moDen = 0;
    Serial.println("Da khoa lai");
  }
  char p = docPhim();
  if (!p) return;
  if (khoaDen && (int32_t)(millis() - khoaDen) < 0) {
    phat(SAI, 1);                        // đang bị khóa tạm: không nhận phím
    return;
  }
  khoaDen = 0;
  phat(BIP, 1);
  if (p >= '0' && p <= '9') {
    if (nhap.length() < 8) nhap += p;
    Serial.print("Da nhap: ");
    for (unsigned i = 0; i < nhap.length(); i++) Serial.print('*');   // không in mật khẩu ra màn hình
    Serial.println();
  } else if (p == '*') {
    nhap = "";
    Serial.println("Da xoa");
  } else if (p == '#') {
    xuLyXacNhan();
  } else if (p == 'A' && cheDo == NHAP_MK) {
    cheDo = DOI_NHAP_CU;
    nhap = "";
    inLoiNhac();
  }
}
  • quetMaTran() quét 4 hàng, trả về phím đầu tiên tìm thấy; docPhim() làm phần chống dội và chỉ báo lúc bấm xuống.
  • struct Not được khai báo ngay đầu chương trình, trước mọi hàm. Trình dịch Arduino tự chèn khai báo các hàm lên đầu tệp; nếu kiểu Not nằm sau, khai báo của phat() sẽ báo lỗi "does not name a type".
  • Serial Monitor chỉ in dấu *, không in mật khẩu đang nhập.
  • (int32_t)(millis() − moDen) >= 0 so thời gian theo cách vẫn đúng khi millis() quay vòng sau khoảng 49 ngày.

7. Kết quả mong đợi

Serial Monitor: dòng nhập mật khẩu, các dòng Da nhap với dấu sao, Sai mat khau 1/3, rồi Dung mat khau mo khoa 3 giay và Da khoa lai; khung dưới là các bước đổi mật khẩu đến Da doi mat khau
Mật khẩu không in ra màn hình, chỉ in dấu *; khung dưới là đổi mật khẩu (chuỗi phím ví dụ).

Khóa mật khẩu trên bàn phím chỉ là một lớp bảo vệ đơn giản: người đứng sau có thể nhìn thấy mật khẩu, và mạch đặt ngoài cửa có thể bị tháo ra để nối tắt relay. Khi dùng thật, đặt ESP32 và relay ở phía trong cửa, chỉ đưa bàn phím ra ngoài.

8. Xử lý lỗi thường gặp

Hiện tượngNguyên nhân thường gặpCách xử lý
Bấm phím 1 ra 4, bấm 2 ra 5…Hàng nối lệch thứ tựĐo lại thứ tự sợi; sửa mảng HANG
Bấm phím 1 ra A, phím 2 ra 3…Cột nối lệch hoặc đảoSửa mảng COT theo đúng thứ tự đo được
Cả một hàng hoặc một cột không ănDây cắm lỏng ở đầu dây dẹtCắm lại; dùng dây cắm đực cắm sâu vào đầu cái
Một lần bấm ra hai ký tựPhím dội lâu hơn 30 ms (phím cũ, bẩn)Tăng số lần quét giống nhau từ 3 lên 5
Còi không kêu, chỉ nghe tạchDùng còi chủ động (tự kêu khi cấp điện)Thay còi thụ động, hoặc chỉ bật tắt còi chủ động bằng mức 1/0
ESP32 khởi động lại khi còi kêuNối còi thẳng vào GPIO, sụt ápDùng mạch transistor như hình 4

9. Xem thêm


Tin tức liên quan

Hướng dẫn dùng LoRa SX1278 (Ra-02) với ESP32: nối dây SPI, gửi và nhận gói tin đầu tiên
Hướng dẫn dùng LoRa SX1278 (Ra-02) với ESP32: nối dây SPI, gửi và nhận gói tin đầu tiên

Hướng dẫn nối module LoRa SX1278 Ra-02 433 MHz với ESP32 qua SPI, cài thư viện LoRa trên Arduino IDE, viết chương trình gửi và nhận gói tin, đọc RSSI, SNR và xử lý lỗi thường gặp.

Cấu hình LoRa trên ESP32: SF, băng thông, tỉ lệ mã, công suất và cách thử tầm xa
Cấu hình LoRa trên ESP32: SF, băng thông, tỉ lệ mã, công suất và cách thử tầm xa

Giải thích bốn tham số cấu hình LoRa (SF, băng thông, tỉ lệ mã, công suất), cách tính thời gian phát một gói và hướng dẫn làm bộ thử tầm xa bằng ESP32, SX1278 và màn hình OLED.

Micro I2S INMP441 với ESP32: đo mức âm thanh dBFS, dB SPL và vỗ tay hai lần bật tắt đèn
Micro I2S INMP441 với ESP32: đo mức âm thanh dBFS, dB SPL và vỗ tay hai lần bật tắt đèn

Khung I2S của micro INMP441, nối với ESP32, đổi số đo 24 bit ra dBFS và ước tính dB SPL, nhận tiếng vỗ tay bằng mức nền và độ vọt, bật tắt đèn khi vỗ tay hai lần.

LED ma trận MAX7219 và LED 7 đoạn TM1637 với ESP32: chữ chạy 32 × 8 và đồng hồ, không cần thư viện
LED ma trận MAX7219 và LED 7 đoạn TM1637 với ESP32: chữ chạy 32 × 8 và đồng hồ, không cần thư viện

So sánh LED ma trận MAX7219 và LED 7 đoạn TM1637, nối với ESP32 an toàn mức điện áp, giải thích khung 16 bit và ba lệnh TM1637, hai chương trình chữ chạy và đồng hồ không cần thư viện.

Làm gateway LoRa sang MQTT bằng ESP32: node cảm biến chạy pin gửi dữ liệu về broker
Làm gateway LoRa sang MQTT bằng ESP32: node cảm biến chạy pin gửi dữ liệu về broker

Hướng dẫn làm hệ thống LoRa hoàn chỉnh với ESP32 và SX1278: node chạy pin đo điện áp, gửi gói rồi ngủ sâu; gateway nhận gói LoRa và publish dữ liệu JSON lên broker MQTT qua Wi-Fi.

Hướng dẫn ESP-NOW trên ESP32: truyền dữ liệu giữa hai mạch không cần router Wi-Fi
Hướng dẫn ESP-NOW trên ESP32: truyền dữ liệu giữa hai mạch không cần router Wi-Fi

Hướng dẫn dùng ESP-NOW để hai mạch ESP32 truyền dữ liệu trực tiếp không cần router: xem địa chỉ MAC, chương trình gửi, chương trình nhận, đọc RSSI và xử lý lỗi thường gặp.

Đo dòng điện với ESP32: INA219 hay ACS712, đo dòng, điện áp, công suất và mAh không cần thư viện
Đo dòng điện với ESP32: INA219 hay ACS712, đo dòng, điện áp, công suất và mAh không cần thư viện

So sánh INA219 (điện trở shunt, I2C) và ACS712 (cảm biến Hall), nối với ESP32 có cầu phân áp, chương trình đo dòng, điện áp, công suất, điện lượng mAh không cần thư viện, kèm bảng lỗi.

ESP32 với Firebase Realtime Database qua REST: đăng nhập ẩn danh, làm mới token, điều khiển relay
ESP32 với Firebase Realtime Database qua REST: đăng nhập ẩn danh, làm mới token, điều khiển relay

ESP32 dùng Firebase Realtime Database không cần thư viện Firebase: đăng nhập ẩn danh, tự làm mới token, đọc lệnh relay, ghi trạng thái và lịch sử, Rules theo uid.


Bình luận
Đã thêm vào giỏ hàng