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
1234sau lần chạy đầu.
2. Cấu tạo

- Đầ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ị
- ESP32 DevKit V1 30 chân và dây USB nạp code
- Module RFID RC522 kèm thẻ hoặc thẻ móc khóa 13,56 MHz
- Bàn phím màng 4×4, còi thụ động 5 V, transistor NPN (S8050 hoặc 2N2222), điện trở 1 kΩ
- Module relay 1 kênh 5 V
- Khóa chốt điện từ 12 V, diode 1N4007, adapter 12 V từ 2 A (xem dòng ghi trên nhãn khóa)
- Mạch hạ áp LM2596 chỉnh sẵn ra 5 V
- Breadboard và dây cắm để thử trên bàn
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.

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:

- 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ànhfalsenế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ì đặtkhoaDen30 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
khoacuacủa Preferences, không mất khi tắt điện. - Hai lớp: đặt
CAN_CA_HAI = truethì 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.

6. Cách dùng và kết quả
- Nạp chương trình, mở Serial Monitor tốc độ 115200. Lần đầu chưa có thẻ nào.
- Thêm thẻ: bấm B, nhập
1234, bấm #, quẹt thẻ. Lặp lại cho từng thẻ. - Đổ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à #.
- 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.

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ượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Serial báo "Khong doc duoc RC522" | Sai dây SPI, RC522 chưa có nguồn 3,3 V | Kiể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 ứng | Thẻ 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ốt | Nố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ài | Thê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ác | Sai 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ẩu | Mật khẩu lưu trong flash | Trong 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
- Module relay 1, 2, 4 kênh: kích mức thấp hay cao, opto cách ly, jumper JD-VCC
- LM2596 là gì: sơ đồ chân, cách chỉnh 5 V
- Công tắc, nút nhấn, cầu chì và relay: cách chọn, cách nối với ESP32
- Bài RC522 (đọc mã thẻ không cần thư viện) và bài bàn phím 4×4 với còi trong cùng loạt mô-đun: giải thích chi tiết từng phần của chương trình này.
Xem thêm