Khóa cửa ESP32 mở bằng thẻ RFID hoặc mật khẩu: RC522, bàn phím 4x4, còi, khóa chốt điện từ 12 V

Làm khóa cửa ESP32 mở bằng thẻ RFID RC522 hoặc mật khẩu bàn phím 4x4: nối dây, nguồn và diode cho khóa chốt 12 V, thêm xóa thẻ, đổi mật khẩu, tạm khóa khi sai 3 lần.

Khóa cửa điện tử là dự án hay chọn sau khi đã làm quen với từng linh kiện: ghép đầu đọc thẻ RFID RC522, bàn phím 4×4, còi báo và một khóa chốt điện từ 12 V vào cùng một ESP32. Cửa mở khi quẹt thẻ đã đăng ký hoặc nhập đúng mật khẩu; thêm, xóa thẻ và đổi mật khẩu làm ngay trên bàn phím; nhập sai 3 lần thì tạm khóa 30 giây. Bài này đi qua cấu tạo, nối dây, nguồn cho khóa chốt, chương trình và các lỗi hay gặp.

1. Lưu ý trước khi lắp lên cửa thật

  • Đường thoát hiểm: cửa ra vào phải mở được từ bên trong bằng tay, không phụ thuộc vào điện hay chương trình. Khóa điện tử nên đi kèm khóa cơ có chìa dự phòng.
  • Mức bảo mật: chương trình so sánh mã UID của thẻ. Thẻ phổ thông 13,56 MHz (MIFARE Classic) đọc được UID bằng bất kỳ đầu đọc nào, và có loại thẻ cho ghi lại UID, nên thẻ có thể bị sao chép. Dự án hợp cho tủ đồ, phòng làm việc, mô hình học tập; cửa cần bảo mật cao nên dùng sản phẩm thương mại có xác thực bằng khóa mã hóa.
  • Đổi ngay mật khẩu mặc định 1234 sau lần chạy đầu.

2. Cấu tạo

Sơ đồ khối khóa cửa: ESP32 ở giữa nối đầu đọc RFID RC522 qua SPI, bàn phím 4x4 qua 8 GPIO, còi qua transistor ở D17, module relay ở D16; relay đóng nguồn 12 V cho khóa chốt điện từ; adapter 12 V và mạch hạ áp cấp 5 V cho ESP32
ESP32 đọc thẻ và bàn phím, báo bằng còi, đóng relay để cấp điện cho khóa chốt.
  • Đầu đọc RC522: phát sóng 13,56 MHz, đọc mã thẻ ở khoảng cách khoảng 1–3 cm, giao tiếp SPI, cấp 3,3 V.
  • Bàn phím màng 4×4: 16 phím, 4 dây hàng và 4 dây cột; ESP32 quét từng hàng để biết phím nào đang bấm.
  • Còi thụ động qua transistor NPN: phát tiếng bíp khi bấm phím, tiếng báo đúng, sai, tạm khóa.
  • Module relay và khóa chốt điện từ 12 V: loại khóa tủ phổ biến giữ chốt bằng lò xo, cấp điện thì cuộn dây hút chốt vào; mất điện thì cửa vẫn khóa. Chương trình chỉ cấp điện 3 giây mỗi lần mở vì cuộn dây nóng lên nếu cấp liên tục.
  • Nguồn: một adapter 12 V cho khóa chốt, qua mạch hạ áp lấy 5 V cho ESP32.

3. Chuẩn bị

4. Nối dây

Nếu đã làm bài RC522 và bài bàn phím 4×4 trong cùng loạt mô-đun, sẽ thấy hai bài dùng trùng chân: D27 vừa là RST của RC522 vừa là hàng R3 của bàn phím, D23 vừa là MOSI vừa là chân còi. Bài này giữ nguyên chân bàn phím và SPI, chuyển RST sang D22, còi sang D17, relay sang D16.

Bảng nối chân: RC522 SDA D5, SCK D18, MOSI D23, MISO D19, RST D22, 3.3V và GND; bàn phím hàng R1 đến R4 vào D13, D14, D27, D26, cột C1 đến C4 vào D25, D33, D32, D4; mạch còi IN vào D17; relay IN vào D16, VCC và GND
Bảng nối chân của cả hệ thống.

Phần khóa chốt dùng nguồn 12 V riêng, đóng ngắt qua tiếp điểm của relay:

Mạch nguồn và khóa chốt: adapter 12 V nối vào mạch hạ áp ra 5 V cho chân VIN của ESP32, đồng thời 12 V vào chân COM của relay; chân NO nối cực dương khóa chốt, cực âm về GND; diode 1N4007 nối ngược song song với khóa
12 V vào COM, NO ra cực dương khóa; diode ngược song song với cuộn khóa.
  • Diode 1N4007 nối song song với khóa chốt, vạch (cathode) về phía cực dương. Khi relay nhả, cuộn dây khóa sinh xung điện áp ngược; diode dập xung này để tiếp điểm relay không đánh lửa và ESP32 không bị nhiễu. Diode có sẵn trên module relay chỉ bảo vệ cuộn relay, không bảo vệ khóa.
  • Nối chung GND của adapter, mạch hạ áp, ESP32 và relay.
  • Không lấy điện cho khóa chốt từ chân 5 V của ESP32 hay cổng USB: dòng hút của khóa vượt xa khả năng của các đường đó.
  • Chỉnh mạch hạ áp ra đúng 5 V bằng đồng hồ trước khi nối vào VIN.

5. Chương trình

Chương trình gộp hai chương trình của bài RC522 và bài bàn phím: phần đọc thẻ giao tiếp trực tiếp với thanh ghi của chip MFRC522 (không cần thư viện), phần quét phím có chống dội và còi phát âm báo không dùng delay(). Phần mới là các chế độ, đếm sai chung cho thẻ và mật khẩu, quản lý thẻ bằng bàn phím. Chỉ dùng thư viện có sẵn trong gói esp32 (SPI, Preferences).

// PYWORLD.VN - Khóa cửa ESP32: mở bằng thẻ RFID (RC522) hoặc mật khẩu trên bàn phím 4x4, còi báo, khóa chốt điện từ qua relay
// PYWORLD.VN - Bấm B, nhập mật khẩu, # rồi quẹt thẻ để thêm/xóa thẻ; bấm A để đổi mật khẩu; sai 3 lần thì tạm khóa 30 giây
// PYWORLD.VN - RC522: SS D5, SCK D18, MOSI D23, MISO D19, RST D22; phím: hàng 13, 14, 27, 26, cột 25, 33, 32, 4; còi D17, relay D16, đèn D2

#include <SPI.h>
#include <Preferences.h>

const int CHAN_SS = 5;
const int CHAN_RST = 22;
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 = 17;                 // còi thụ động qua transistor NPN
const int CHAN_RELAY = 16;               // relay đóng nguồn 12 V cho khóa chốt
const int CHAN_DEN = 2;
const bool RELAY_KICH_MUC_THAP = true;   // module relay phổ biến đóng khi IN ở mức thấp
const bool CAN_CA_HAI = false;           // true: phải quẹt thẻ rồi nhập mật khẩu (hai lớp)
const uint32_t THOI_GIAN_MO_MS = 3000;
const uint32_t CHO_MAT_KHAU_MS = 10000;  // chế độ hai lớp: thời gian nhập mật khẩu sau khi quẹt thẻ
const int SAI_TOI_DA = 3;
const uint32_t KHOA_TAM_MS = 30000;
const uint32_t CHO_QUAN_LY_MS = 15000;
const int SO_THE_TOI_DA = 10;
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)

// Thanh ghi và lệnh của MFRC522 (theo datasheet NXP)
enum : uint8_t {
  CommandReg = 0x01, ComIrqReg = 0x04, DivIrqReg = 0x05, ErrorReg = 0x06, FIFODataReg = 0x09, FIFOLevelReg = 0x0A,
  ControlReg = 0x0C, BitFramingReg = 0x0D, CollReg = 0x0E, ModeReg = 0x11, TxModeReg = 0x12, RxModeReg = 0x13,
  TxControlReg = 0x14, TxASKReg = 0x15, ModWidthReg = 0x24, TModeReg = 0x2A, TPrescalerReg = 0x2B,
  TReloadRegH = 0x2C, TReloadRegL = 0x2D, VersionReg = 0x37
};
enum : uint8_t { LENH_NGHI = 0x00, LENH_TRANSCEIVE = 0x0C, LENH_SOFT_RESET = 0x0F };

SPISettings caiDatSpi(4000000, MSBFIRST, SPI_MODE0);

void ghi(uint8_t tg, uint8_t giaTri) {
  SPI.beginTransaction(caiDatSpi);
  digitalWrite(CHAN_SS, LOW);
  SPI.transfer((tg << 1) & 0x7E);           // bit 7 = 0: ghi
  SPI.transfer(giaTri);
  digitalWrite(CHAN_SS, HIGH);
  SPI.endTransaction();
}

uint8_t doc(uint8_t tg) {
  SPI.beginTransaction(caiDatSpi);
  digitalWrite(CHAN_SS, LOW);
  SPI.transfer(((tg << 1) & 0x7E) | 0x80);  // bit 7 = 1: đọc
  uint8_t v = SPI.transfer(0);
  digitalWrite(CHAN_SS, HIGH);
  SPI.endTransaction();
  return v;
}

void datBit(uint8_t tg, uint8_t mask) { ghi(tg, doc(tg) | mask); }
void xoaBit(uint8_t tg, uint8_t mask) { ghi(tg, doc(tg) & ~mask); }

// CRC_A theo ISO/IEC 14443-3 (giá trị đầu 0x6363), byte thấp gửi trước.
uint16_t crcA(const uint8_t *du, size_t n) {
  uint16_t crc = 0x6363;
  for (size_t i = 0; i < n; i++) {
    uint8_t c = du[i] ^ (uint8_t)crc;
    c ^= (uint8_t)(c << 4);
    crc = (crc >> 8) ^ ((uint16_t)c << 8) ^ ((uint16_t)c << 3) ^ (c >> 4);
  }
  return crc;
}

void khoiDongRc522() {
  digitalWrite(CHAN_RST, LOW);
  delayMicroseconds(10);
  digitalWrite(CHAN_RST, HIGH);
  delay(50);
  ghi(CommandReg, LENH_SOFT_RESET);
  delay(50);
  ghi(TxModeReg, 0x00);
  ghi(RxModeReg, 0x00);
  ghi(ModWidthReg, 0x26);
  ghi(TModeReg, 0x80);                     // timer tự chạy khi gửi xong
  ghi(TPrescalerReg, 0xA9);                // 13,56 MHz / (2 × 169 + 1) ≈ 40 kHz, mỗi nhịp 25 us
  ghi(TReloadRegH, 0x03);
  ghi(TReloadRegL, 0xE8);                  // 1000 nhịp = 25 ms chờ thẻ trả lời
  ghi(TxASKReg, 0x40);                     // điều chế ASK 100 % theo ISO 14443A
  ghi(ModeReg, 0x3D);                      // giá trị đầu CRC 0x6363
  datBit(TxControlReg, 0x03);              // bật hai chân phát ra anten
}

// Gửi `n` byte (byte cuối chỉ gửi `bitCuoi` bit nếu khác 0) và nhận trả lời. Trả về số byte nhận, -1 nếu lỗi/hết giờ.
int traoDoi(const uint8_t *gui, uint8_t n, uint8_t *nhan, uint8_t toiDa, uint8_t bitCuoi = 0) {
  ghi(CommandReg, LENH_NGHI);
  ghi(ComIrqReg, 0x7F);                    // xóa các cờ ngắt
  ghi(FIFOLevelReg, 0x80);                 // xóa bộ đệm FIFO
  for (uint8_t i = 0; i < n; i++) ghi(FIFODataReg, gui[i]);
  ghi(BitFramingReg, bitCuoi);
  ghi(CommandReg, LENH_TRANSCEIVE);
  datBit(BitFramingReg, 0x80);             // StartSend
  uint32_t batDau = millis();
  while (true) {
    uint8_t irq = doc(ComIrqReg);
    if (irq & 0x30) break;                 // RxIRq hoặc IdleIRq: đã nhận xong
    if ((irq & 0x01) || millis() - batDau > 40) return -1;   // TimerIRq: thẻ không trả lời
  }
  xoaBit(BitFramingReg, 0x80);
  if (doc(ErrorReg) & 0x13) return -1;     // tràn bộ đệm, lỗi chẵn lẻ, lỗi giao thức
  uint8_t soByte = doc(FIFOLevelReg);
  if (soByte > toiDa) return -1;
  for (uint8_t i = 0; i < soByte; i++) nhan[i] = doc(FIFODataReg);
  return soByte;
}

// Chọn thẻ ở một cấp: chống va chạm lấy 4 byte + BCC, rồi SELECT để nhận SAK.
bool chonCap(uint8_t maCap, uint8_t uid4[4], uint8_t &sak) {
  uint8_t gui[9] = {maCap, 0x20};
  uint8_t nhan[5];
  if (traoDoi(gui, 2, nhan, 5) != 5) return false;
  if ((nhan[0] ^ nhan[1] ^ nhan[2] ^ nhan[3]) != nhan[4]) return false;   // BCC sai
  gui[1] = 0x70;
  memcpy(gui + 2, nhan, 5);
  uint16_t crc = crcA(gui, 7);
  gui[7] = crc & 0xFF;
  gui[8] = crc >> 8;
  uint8_t traLoi[3];
  if (traoDoi(gui, 9, traLoi, 3) != 3) return false;
  memcpy(uid4, nhan, 4);
  sak = traLoi[0];
  return true;
}

// Tìm thẻ mới trong vùng đọc. Trả về độ dài UID (4 hoặc 7 byte), 0 nếu không có thẻ.
uint8_t docUid(uint8_t uid[7], uint8_t &sak) {
  xoaBit(CollReg, 0x80);
  uint8_t reqa = 0x26, atqa[2];
  if (traoDoi(&reqa, 1, atqa, 2, 7) != 2) return 0;   // REQA chỉ có 7 bit
  uint8_t phan[4];
  if (!chonCap(0x93, phan, sak)) return 0;
  if (phan[0] != 0x88) {                   // không có cờ nối tầng: UID 4 byte
    memcpy(uid, phan, 4);
    return 4;
  }
  memcpy(uid, phan + 1, 3);                // 0x88 + 3 byte đầu, cấp 2 cho 4 byte còn lại
  if (!chonCap(0x95, phan, sak)) return 0;
  memcpy(uid + 3, phan, 4);
  return 7;
}

void choTheNghi() {                      // HLTA: thẻ ngủ tới khi ra khỏi vùng đọc, không bị đọc lặp
  uint8_t gui[4] = {0x50, 0x00};
  uint16_t crc = crcA(gui, 2);
  gui[2] = crc & 0xFF;
  gui[3] = crc >> 8;
  uint8_t bo[1];
  traoDoi(gui, 4, bo, 1);                  // thẻ không trả lời là đúng
}

// ---- 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_TAM[] = {{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);
}

// ---- Danh sách thẻ, mật khẩu ----
Preferences boNho;
uint8_t danhSach[SO_THE_TOI_DA][7];
uint8_t doDaiThe[SO_THE_TOI_DA];
int soThe = 0;
String matKhau, nhap, matKhauMoi;
enum CheDo { NHAP_MK, DOI_MK_CU, DOI_MK_MOI, DOI_MK_LAI, QL_NHAP_MK, QL_CHO_THE } cheDo = NHAP_MK;
int soLanSai = 0;
uint32_t khoaDen = 0, moDen = 0, hetHanQuanLy = 0, hanThe = 0;

int timThe(const uint8_t *uid, uint8_t n) {
  for (int i = 0; i < soThe; i++)
    if (doDaiThe[i] == n && memcmp(danhSach[i], uid, n) == 0) return i;
  return -1;
}

void luuDanhSach() {
  boNho.putInt("so", soThe);
  boNho.putBytes("uid", danhSach, sizeof(danhSach));
  boNho.putBytes("dai", doDaiThe, sizeof(doDaiThe));
}

void relay(bool dong) {
  digitalWrite(CHAN_RELAY, (dong == RELAY_KICH_MUC_THAP) ? LOW : HIGH);
}

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

bool dangBiKhoa() {
  if (khoaDen && (int32_t)(millis() - khoaDen) < 0) return true;
  khoaDen = 0;
  return false;
}

bool theConHan() {
  return hanThe && (int32_t)(millis() - hanThe) < 0;
}

void moKhoa(const char *cach) {
  relay(true);
  digitalWrite(CHAN_DEN, HIGH);
  moDen = millis() + THOI_GIAN_MO_MS;
  soLanSai = 0;
  hanThe = 0;
  phat(DUNG, 5);
  Serial.printf("MO KHOA bang %s\n", cach);
}

void ghiSai(const char *lyDo) {
  if (++soLanSai >= SAI_TOI_DA) {
    khoaDen = millis() + KHOA_TAM_MS;
    soLanSai = 0;
    hanThe = 0;
    phat(KHOA_TAM, 5);
    Serial.printf("%s: sai %d lan, tam khoa 30 giay\n", lyDo, SAI_TOI_DA);
  } else {
    phat(SAI, 1);
    Serial.printf("%s (%d/%d)\n", lyDo, soLanSai, SAI_TOI_DA);
  }
}

void xuLyXacNhan() {
  if (cheDo == NHAP_MK) {
    if (nhap != matKhau) {
      ghiSai("Sai mat khau");
    } else if (!CAN_CA_HAI) {
      moKhoa("mat khau");
    } else if (theConHan()) {
      moKhoa("the + mat khau");
    } else {
      Serial.println("Che do hai lop: quet the truoc roi nhap mat khau");
      phat(SAI, 1);
    }
  } else if (cheDo == DOI_MK_CU || cheDo == QL_NHAP_MK) {
    if (nhap == matKhau) {
      soLanSai = 0;
      phat(DUNG, 5);
      if (cheDo == DOI_MK_CU) {
        cheDo = DOI_MK_MOI;
      } else {
        cheDo = QL_CHO_THE;
        hetHanQuanLy = millis() + CHO_QUAN_LY_MS;
        Serial.println("Quet the moi de THEM, the da co de XOA (trong 15 giay)");
      }
    } else {
      ghiSai("Sai mat khau");
      cheDo = NHAP_MK;
    }
  } else if (cheDo == DOI_MK_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_MK_LAI;
      phat(BIP, 1);
    }
  } else if (cheDo == DOI_MK_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 xuLyPhim(char p) {
  if (dangBiKhoa()) {
    phat(SAI, 1);
    return;
  }
  phat(BIP, 1);
  if (p >= '0' && p <= '9') {
    if (nhap.length() < 8) nhap += p;
  } else if (p == '*') {
    nhap = "";
    if (cheDo == NHAP_MK) {
      Serial.println("Da xoa");
    } else {
      cheDo = NHAP_MK;
      Serial.println("Huy");
      inLoiNhac();
    }
  } else if (p == '#') {
    xuLyXacNhan();
  } else if (p == 'A' && cheDo == NHAP_MK) {
    cheDo = DOI_MK_CU;
    nhap = "";
    inLoiNhac();
  } else if (p == 'B' && cheDo == NHAP_MK) {
    cheDo = QL_NHAP_MK;
    nhap = "";
    inLoiNhac();
  }
}

void themHoacXoaThe(const uint8_t *uid, uint8_t n, int viTri, const char *chuoi) {
  if (viTri >= 0) {
    for (int i = viTri; i < soThe - 1; i++) {
      memcpy(danhSach[i], danhSach[i + 1], 7);
      doDaiThe[i] = doDaiThe[i + 1];
    }
    soThe--;
    Serial.printf("Da XOA the %s, con %d the\n", chuoi, soThe);
  } else if (soThe < SO_THE_TOI_DA) {
    memcpy(danhSach[soThe], uid, n);
    doDaiThe[soThe++] = n;
    Serial.printf("Da THEM the %s, co %d the\n", chuoi, soThe);
  } else {
    Serial.println("Danh sach day, xoa bot the truoc");
  }
  luuDanhSach();
  phat(DUNG, 5);
  cheDo = NHAP_MK;
  inLoiNhac();
}

void xuLyThe() {
  uint8_t uid[7], sak;
  uint8_t n = docUid(uid, sak);
  if (!n) return;
  choTheNghi();
  char chuoi[22];
  for (uint8_t i = 0; i < n; i++) sprintf(chuoi + i * 3, "%02X%s", uid[i], i + 1 < n ? ":" : "");
  int viTri = timThe(uid, n);

  if (cheDo == QL_CHO_THE) {
    themHoacXoaThe(uid, n, viTri, chuoi);
    return;
  }
  if (dangBiKhoa()) {
    phat(SAI, 1);
    Serial.println("Dang tam khoa, chua nhan the");
    return;
  }
  if (viTri < 0) {
    Serial.printf("The %s khong co trong danh sach\n", chuoi);
    ghiSai("The khong hop le");
  } else if (CAN_CA_HAI) {
    hanThe = millis() + CHO_MAT_KHAU_MS;
    phat(BIP, 1);
    Serial.printf("The %s hop le, nhap mat khau trong 10 giay\n", chuoi);
  } else {
    String cach = String("the ") + chuoi;
    moKhoa(cach.c_str());
  }
}

void setup() {
  Serial.begin(115200);
  pinMode(CHAN_SS, OUTPUT);
  digitalWrite(CHAN_SS, HIGH);
  pinMode(CHAN_RST, OUTPUT);
  pinMode(CHAN_RELAY, OUTPUT);
  relay(false);
  pinMode(CHAN_DEN, OUTPUT);
  for (int i = 0; i < 4; i++) {
    pinMode(HANG[i], OUTPUT);
    digitalWrite(HANG[i], HIGH);
    pinMode(COT[i], INPUT_PULLUP);
  }
  ledcAttach(CHAN_COI, 2000, 10);
  ledcWriteTone(CHAN_COI, 0);
  SPI.begin(18, 19, 23, CHAN_SS);
  khoiDongRc522();
  uint8_t ban = doc(VersionReg);
  if (ban == 0x00 || ban == 0xFF) Serial.println("Khong doc duoc RC522: kiem tra day SPI, nguon 3.3V");
  boNho.begin("khoacua", false);
  soThe = boNho.getInt("so", 0);
  boNho.getBytes("uid", danhSach, sizeof(danhSach));
  boNho.getBytes("dai", doDaiThe, sizeof(doDaiThe));
  matKhau = boNho.getString("mk", "1234");
  Serial.printf("Khoa cua: %d the da luu\n", soThe);
  inLoiNhac();
}

void loop() {
  capNhatCoi();
  if (moDen && (int32_t)(millis() - moDen) >= 0) {
    relay(false);
    digitalWrite(CHAN_DEN, LOW);
    moDen = 0;
    Serial.println("Da khoa lai");
  }
  if (cheDo == QL_CHO_THE && (int32_t)(millis() - hetHanQuanLy) >= 0) {
    cheDo = NHAP_MK;
    Serial.println("Het thoi gian quan ly the");
    inLoiNhac();
  }
  char p = docPhim();
  if (p) xuLyPhim(p);
  static uint32_t lanDocThe = 0;
  if (millis() - lanDocThe >= 100) {       // dò thẻ 10 lần mỗi giây, xen kẽ với quét phím
    lanDocThe = millis();
    xuLyThe();
  }
}

Giải thích

  • Hằng số đầu chương trình: chân nối, RELAY_KICH_MUC_THAP (đổi thành false nếu module relay kích mức cao), THOI_GIAN_MO_MS (thời gian cấp điện cho khóa), SAI_TOI_DA, KHOA_TAM_MS.
  • loop() không có chỗ nào chờ: mỗi vòng cập nhật còi, đóng khóa lại khi hết 3 giây, quét bàn phím; cứ 100 ms dò thẻ một lần. Nhờ vậy bấm phím và quẹt thẻ đều phản hồi ngay.
  • ghiSai() dùng chung cho mật khẩu sai và thẻ lạ: sai lần thứ 3 thì đặt khoaDen 30 giây; trong thời gian đó bàn phím và đầu đọc chỉ kêu báo, không xử lý.
  • Quản lý thẻ: bài RC522 dùng nút BOOT trên bo để thêm thẻ, nhưng khi lắp vào hộp thì không bấm được nút đó, và ai đứng ngoài cửa cũng không được tự thêm thẻ. Ở đây phải bấm B, nhập mật khẩu, bấm #, rồi quẹt thẻ trong 15 giây: thẻ chưa có thì được thêm, thẻ đã có thì bị xóa.
  • Lưu trữ: danh sách thẻ (tối đa 10, UID 4 hoặc 7 byte) và mật khẩu nằm trong vùng khoacua của Preferences, không mất khi tắt điện.
  • Hai lớp: đặt CAN_CA_HAI = true thì thẻ đúng chưa mở cửa ngay; phải nhập đúng mật khẩu trong 10 giây sau khi quẹt. Mất thẻ thì người nhặt được cũng không mở được.
Sơ đồ các chế độ: từ trạng thái chờ thẻ hoặc mật khẩu, thẻ đúng hoặc mật khẩu đúng thì mở khóa 3 giây, sai 3 lần thì tạm khóa 30 giây, bấm A sang đổi mật khẩu, bấm B và mật khẩu sang quản lý thẻ; mỗi nhánh xong thì quay về chờ
Các chế độ của chương trình.

6. Cách dùng và kết quả

  1. Nạp chương trình, mở Serial Monitor tốc độ 115200. Lần đầu chưa có thẻ nào.
  2. Thêm thẻ: bấm B, nhập 1234, bấm #, quẹt thẻ. Lặp lại cho từng thẻ.
  3. Đổi mật khẩu: bấm A, nhập mật khẩu cũ và #, nhập mật khẩu mới (4–8 số) và #, nhập lại lần nữa và #.
  4. Bấm * để xóa số đang nhập; đang ở bước đổi mật khẩu hoặc quản lý thẻ thì * là hủy.
Serial Monitor: khởi động 0 thẻ, quẹt thẻ lạ báo không có trong danh sách, bấm B nhập mật khẩu, quẹt thẻ được thêm, quẹt lại thì mở khóa, 3 giây sau khóa lại; khung dưới nhập sai mật khẩu hai lần và quẹt thẻ lạ thì tạm khóa 30 giây, thẻ đúng cũng bị chặn
Thêm thẻ rồi mở khóa; sai 3 lần thì tạm khóa (mã thẻ ví dụ).

Khi lắp hoàn chỉnh không cần máy tính: còi cho biết kết quả (tiếng lên cao là đúng, tiếng trầm dài là sai, ba tiếng trầm là tạm khóa), đèn trên bo sáng trong lúc khóa mở.

7. Lỗi hay gặp

Hiện tượngNguyên nhân thường gặpCách xử lý
Serial báo "Khong doc duoc RC522"Sai dây SPI, RC522 chưa có nguồn 3,3 VKiểm tra lại bảng chân, đặc biệt SDA vào D5 và RST vào D22
Quẹt thẻ không có phản ứngThẻ quá xa, sau thẻ có kim loại, thẻ 125 kHzĐưa thẻ sát mặt anten; RC522 chỉ đọc thẻ 13,56 MHz
Relay kêu nhưng khóa không rút chốtNối nhầm NC, adapter yếu, chốt bị kẹt do lệch cửaĐo 12 V ở hai cực khóa lúc relay đóng; chỉnh vị trí chốt
ESP32 khởi động lại khi khóa mởThiếu diode ở khóa, nguồn chung yếu, dây GND dàiThêm diode, dùng adapter đủ dòng; xem bài relay làm ESP32 bị reset
Bấm một phím ra ký tự khácSai thứ tự dây bàn phímĐo thông mạch: bấm phím 1 thì sợi R1 và C1 thông nhau
Khóa mở ngược (mở khi lẽ ra đóng)Module relay kích mức caoĐặt RELAY_KICH_MUC_THAP = false
Quên mật khẩuMật khẩu lưu trong flashTrong Arduino IDE bật Erase All Flash Before Sketch Upload rồi nạp lại: mật khẩu về 1234, danh sách thẻ bị xóa

8. Hướng mở rộng

  • Thêm màn hình OLED 0.96 inch (I2C, dùng D21 và D22 thì chuyển RST của RC522 sang chân khác) để hiện dấu * khi nhập và thông báo.
  • Ghi nhật ký mở cửa có giờ vào thẻ nhớ micro SD; lấy giờ từ Internet hoặc từ module thời gian thực.
  • Gửi thông báo lên điện thoại mỗi khi cửa mở hoặc có người nhập sai nhiều lần.
  • Thêm công tắc từ ở khung cửa để biết cửa đang đóng hay mở, và cảnh báo khi cửa để mở quá lâu.

9. Xem thêm


Tin tức liên quan

Máy cho cá ăn tự động bằng ESP32: servo SG90, DS3231 giữ giờ khi mất điện, đặt lịch trên web
Máy cho cá ăn tự động bằng ESP32: servo SG90, DS3231 giữ giờ khi mất điện, đặt lịch trên web

Làm máy cho cá ăn tự động bằng ESP32: servo SG90 quay đĩa có lỗ, DS3231 giữ giờ khi mất điện, đồng bộ giờ Internet, trang web đặt 4 giờ cho ăn, không cho ăn lặp sau mất điện.

Từ breadboard đến thiết bị dùng thật: đưa ESP32 vào hộp, khoan lỗ jack Type-C, GX16, jack DC
Từ breadboard đến thiết bị dùng thật: đưa ESP32 vào hộp, khoan lỗ jack Type-C, GX16, jack DC

Đưa dự án ESP32 từ breadboard vào hộp: cấp nguồn USB 5 V hoặc adapter 12 V qua LM2596, bố trí linh kiện, khoan lỗ jack Type-C, jack DC, GX16 và chương trình hộp relay có nút nhấn.

Đóng relay làm ESP32, Arduino bị reset hoặc treo: nguyên nhân và cách khắc phục
Đóng relay làm ESP32, Arduino bị reset hoặc treo: nguyên nhân và cách khắc phục

Vì sao đóng relay làm ESP32, Arduino tự khởi động lại hoặc treo: sụt áp nguồn, xung cuộn dây, hồ quang tiếp điểm, chân khởi động; cách khắc phục và chương trình đo sụt áp.

Tự làm tay điều khiển flycam bằng ESP32 và ESP-NOW, bộ thu xuất PPM, SBUS cho mạch điều khiển bay
Tự làm tay điều khiển flycam bằng ESP32 và ESP-NOW, bộ thu xuất PPM, SBUS cho mạch điều khiển bay

Tự làm tay điều khiển flycam bằng ESP32: hai joystick, hai công tắc, gửi 8 kênh qua ESP-NOW; bộ thu ESP32 xuất PPM bằng RMT và SBUS bằng UART, có failsafe, dùng với Betaflight.

ESP32 điều khiển động cơ bước 28BYJ-48 với ULN2003: nửa bước, quay đúng góc, kim giây
ESP32 điều khiển động cơ bước 28BYJ-48 với ULN2003: nửa bước, quay đúng góc, kim giây

Hướng dẫn điều khiển động cơ bước 28BYJ-48 bằng ESP32 qua mạch ULN2003: chế độ nửa bước, 4076 bước một vòng, quay đúng góc theo lệnh, kim giây đồng hồ và thí nghiệm STEM.

Hướng dẫn sử dụng màn hình LCD 1602 I2C với Arduino Uno
Hướng dẫn sử dụng màn hình LCD 1602 I2C với Arduino Uno

Hướng dẫn nối màn hình LCD 1602 I2C với Arduino Uno, cài thư viện LiquidCrystal_I2C, chạy code hiển thị và quét địa chỉ I2C 0x27/0x3F.

Hướng dẫn dùng ngắt GPIO trên ESP32: đọc nút nhấn, chống dội và đếm xung
Hướng dẫn dùng ngắt GPIO trên ESP32: đọc nút nhấn, chống dội và đếm xung

Hướng dẫn dùng ngắt GPIO trên ESP32 với Arduino IDE: attachInterrupt, các kiểu kích ngắt, chống dội nút nhấn, đếm xung đo tần số và quy tắc viết hàm ngắt an toàn.

Hướng dẫn dùng mạch tạo trễ NE555 có relay: đấu dây và chỉnh thời gian trễ
Hướng dẫn dùng mạch tạo trễ NE555 có relay: đấu dây và chỉnh thời gian trễ

Hướng dẫn dùng mạch tạo trễ NE555 có relay bản 5 V và 12 V: nguyên lý tạo trễ, ý nghĩa các cọc VCC, GND, COM, CK, CB, cách chỉnh thời gian trễ, sơ đồ đấu dây và xử lý lỗi thường gặp.


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