RF 433 MHz là gì: điều chế ASK/OOK và tự viết giao thức phát, thu bằng ESP32
Giải thích sóng RF 433 MHz và điều chế ASK/OOK, cách mã hóa bit bằng độ rộng xung và khung tin 24 bit. Hướng dẫn tự viết chương trình phát và thu trên hai mạch ESP32, không dùng thư viện ngoài.
Module phát và thu RF 433 MHz là cách rẻ và đơn giản nhất để hai mạch nói chuyện không dây: mỗi module chỉ có ba chân và không cần cấu hình gì. Đổi lại, chúng không làm gì ngoài việc chuyển mức logic thành sóng và ngược lại; địa chỉ, khung tin, chống nhiễu đều phải tự lo. Bài viết giải thích điều chế ASK/OOK, cách mã hóa bit bằng độ rộng xung, rồi tự viết chương trình phát và thu trên hai mạch ESP32 mà không dùng thư viện ngoài, để thấy rõ từng xung trên đường truyền.
1. RF 433 MHz và điều chế ASK/OOK
- Sóng mang là sóng vô tuyến tần số 433,92 MHz. Băng tần quanh 433 MHz dành cho các thiết bị cự ly ngắn, công suất thấp như điều khiển từ xa, chuông cửa không dây, cảm biến thời tiết.
- ASK (Amplitude Shift Keying) là truyền dữ liệu bằng cách thay đổi biên độ sóng mang. Dạng đơn giản nhất là OOK (On-Off Keying): mức 1 là có sóng, mức 0 là tắt hẳn.
- Module phát chỉ là một bộ dao động được bật, tắt bởi chân DATA. Module thu dò xem có sóng 433 MHz hay không rồi đưa kết quả ra chân DATA của nó.
- Module không hiểu dữ liệu: không có địa chỉ, không kiểm tra lỗi, không phát lại. Đường truyền chỉ đi một chiều, từ mạch phát sang mạch thu.
- Khi không có ai phát, mạch thu tự tăng độ nhạy cho tới khi bắt được nhiễu nền, nên chân DATA của nó nhảy mức liên tục. Chương trình thu phải tách được khung tin thật ra khỏi nhiễu.
2. Mã hóa bit và khung tin
Không thể nối thẳng UART vào module: mạch thu cần tín hiệu đổi mức đều đặn để giữ được ngưỡng so sánh, một chuỗi dài toàn bit 0 hoặc toàn bit 1 sẽ bị méo. Cách làm phổ biến là mã hóa mỗi bit bằng độ rộng xung, sao cho bit nào cũng có một đoạn mức cao và một đoạn mức thấp.
- Đơn vị thời gian T = 350 µs. Mọi khoảng thời gian trong giao thức là bội số của T.
- Bit 0: cao 1T, thấp 3T. Bit 1: cao 3T, thấp 1T. Hai loại bit dài bằng nhau (4T = 1,4 ms), mạch thu chỉ cần so xem đoạn nào dài hơn.
- Xung đồng bộ: cao 1T rồi thấp 31T. Khoảng thấp dài bất thường này báo cho mạch thu biết một khung mới bắt đầu.
- Khung tin gồm xung đồng bộ và 24 bit, bit cao gửi trước: 16 bit mã thiết bị và 8 bit lệnh. Mã thiết bị đóng vai trò địa chỉ: mạch thu chỉ làm theo khung mang đúng mã của mình.
- Lặp lại: mạch phát không biết mạch thu có nhận được hay không, nên mỗi lần nhấn nút nó phát khung 6 lần liên tiếp. Mạch thu chỉ tin một mã khi nhận được hai khung liền nhau giống hệt; nhiễu ngẫu nhiên gần như không thể tạo ra điều đó.
Một khung dài 128T = 44,8 ms, cả lượt phát dài 280 ms. Kiểu mã hóa này cũng được nhiều bộ điều khiển 433 MHz mã cố định sử dụng, nên mạch thu có thể bắt được mã của thiết bị khác ở gần; chương trình sẽ bỏ qua các mã đó.
3. Chuẩn bị
- Hai mạch ESP32 DevKit V1 30 chân (hoặc bản dùng chip CH340).
- Một module phát và một module thu 433 MHz loại ASK (thường bán theo cặp).
- Hai đoạn dây đồng một lõi dài 17,3 cm làm ăng-ten.
- Điện trở 2,2 kΩ và 3,3 kΩ, cần khi module thu chạy nguồn 5 V.
- Hai breadboard, dây cắm và hai cáp USB.
- Arduino IDE 2.x với gói esp32 by Espressif Systems (bài dùng phiên bản 3.3.10). Không cần cài thêm thư viện.
4. Sơ đồ nối
Mạch phát
- Chân DATA của module phát là ngõ vào, nhận thẳng mức 3,3 V từ D4.
- Module phát thường chạy được ở dải điện áp rộng; cấp 5 V từ chân VIN cho sóng mạnh hơn so với cấp 3,3 V.
- Nút nhấn dùng luôn nút BOOT trên mạch (GPIO0), không phải nối thêm.
Mạch thu
| Loại module thu | Nguồn | Nối chân DATA |
|---|---|---|
| Ghi rõ chạy được 3,3 V (thường là loại siêu ngoại sai) | 3V3 | Nối thẳng vào D27 |
| Chỉ chạy 5 V (thường là loại siêu tái sinh, giá rẻ) | VIN (5 V) | Qua mạch chia áp 2,2 kΩ và 3,3 kΩ |
- Chân của ESP32 chỉ chịu được 3,3 V. Mạch chia áp 2,2 kΩ và 3,3 kΩ đưa mức 5 V xuống 3 V.
- Ăng-ten: một phần tư bước sóng của 433,92 MHz là 17,3 cm. Hàn đoạn dây vào lỗ ANT của cả hai module và để dây thẳng. Thiếu ăng-ten, hai mạch chỉ liên lạc được trong vài chục centimet.
- Module thu loại siêu ngoại sai thu xa hơn và ít bị nhiễu hơn loại siêu tái sinh.
5. Chương trình mạch phát
/*
PYWORLD.VN - RF 433 MHz với ESP32: mạch phát, tự tạo khung 24 bit theo kiểu điều chế độ rộng xung
Mỗi lần nhấn nút BOOT (GPIO0), mạch phát lần lượt lệnh bật rồi lệnh tắt; mỗi khung được phát lặp lại 6 lần
Module phát 433 MHz: DATA = D4, VCC = VIN (5 V), GND = GND; hàn thêm dây ăng-ten dài 17,3 cm
*/
#define TX_PIN 4
#define NUT_PIN 0 // Nút BOOT trên mạch, nhấn là mức thấp
const uint16_t MA_THIET_BI = 0xA5C3; // 16 bit nhận dạng: mạch thu chỉ nghe mã này
const uint8_t LENH_BAT = 0x01;
const uint8_t LENH_TAT = 0x02;
const unsigned int T_US = 350; // Đơn vị thời gian của giao thức, micro giây
const int SO_LAN_LAP = 6;
void phatXung(int soTCao, int soTThap) {
digitalWrite(TX_PIN, HIGH); // Mức cao: module phát sóng mang
delayMicroseconds(T_US * soTCao);
digitalWrite(TX_PIN, LOW); // Mức thấp: tắt sóng mang
delayMicroseconds(T_US * soTThap);
}
// Khung: xung đồng bộ (cao 1T, thấp 31T) rồi 24 bit, bit cao trước.
// Bit 0 = cao 1T, thấp 3T. Bit 1 = cao 3T, thấp 1T.
void phatKhung(uint32_t ma24) {
for (int lan = 0; lan < SO_LAN_LAP; lan++) {
phatXung(1, 31);
for (int i = 23; i >= 0; i--) {
if ((ma24 >> i) & 1) {
phatXung(3, 1);
} else {
phatXung(1, 3);
}
}
}
phatXung(1, 31); // Xung kết thúc để mạch thu chốt bit cuối của khung cuối
}
void setup() {
Serial.begin(115200);
pinMode(TX_PIN, OUTPUT);
digitalWrite(TX_PIN, LOW);
pinMode(NUT_PIN, INPUT_PULLUP);
Serial.printf("\nMach phat RF 433 MHz, ma thiet bi 0x%04X. Nhan nut BOOT de phat\n", MA_THIET_BI);
}
void loop() {
static bool dangBat = false;
if (digitalRead(NUT_PIN) == LOW) {
delay(30); // Chống dội nút nhấn
if (digitalRead(NUT_PIN) == LOW) {
dangBat = !dangBat;
uint8_t lenh = dangBat ? LENH_BAT : LENH_TAT;
uint32_t ma24 = ((uint32_t)MA_THIET_BI << 8) | lenh;
phatKhung(ma24);
Serial.printf("Da phat ma 0x%06X (%s), lap %d lan\n", (unsigned)ma24, dangBat ? "BAT" : "TAT", SO_LAN_LAP);
while (digitalRead(NUT_PIN) == LOW) {
delay(10); // Chờ nhả nút
}
}
}
}
phatXung()tạo một đoạn mức cao rồi một đoạn mức thấp, độ dài tính theo số lần T.phatKhung()ghép xung đồng bộ với 24 bit, gửi bit cao trước, lặp lại 6 lần. Xung cuối cùng sau vòng lặp tạo ra sườn lên để mạch thu chốt được bit cuối của khung cuối.- Mã 24 bit ghép từ mã thiết bị và lệnh:
0xA5C3với lệnh0x01thành0xA5C301. - Hãy đổi
MA_THIET_BIthành một số riêng, giống nhau ở cả hai chương trình.
6. Chương trình mạch thu
/*
PYWORLD.VN - RF 433 MHz với ESP32: mạch thu, giải mã khung 24 bit bằng cách đo độ rộng xung trong hàm ngắt
Chỉ chấp nhận mã khi nhận được hai khung liên tiếp giống nhau và đúng mã thiết bị; lệnh 0x01 bật, 0x02 tắt LED GPIO2
Module thu 433 MHz: DATA = D27 (qua mạch chia áp nếu module chạy 5 V), VCC và GND theo loại module
*/
#define RX_PIN 27
#define LED_PIN 2
const uint16_t MA_THIET_BI = 0xA5C3;
const uint8_t LENH_BAT = 0x01;
const uint8_t LENH_TAT = 0x02;
const unsigned int T_US = 350; // Phải trùng với mạch phát
const unsigned int DONG_BO_MIN_US = T_US * 24; // Khoảng thấp của xung đồng bộ là 31T, cho phép sai lệch
const unsigned int DONG_BO_MAX_US = T_US * 40;
const unsigned int BIT_MIN_US = T_US * 4 * 6 / 10; // Một bit dài 4T, cho phép sai lệch 40 %
const unsigned int BIT_MAX_US = T_US * 4 * 14 / 10;
const unsigned long CUA_SO_LAP_MS = 300; // Hai khung giống nhau phải tới trong khoảng này
volatile uint32_t maVuaNhan = 0;
volatile bool coMaMoi = false;
// Chạy mỗi khi chân DATA đổi mức: đo thời gian mức cao và mức thấp của từng xung
void IRAM_ATTR khiDoiMuc() {
static unsigned long lucTruoc = 0;
static uint32_t thoiGianCao = 0;
static int soBit = -1; // -1: đang chờ xung đồng bộ
static uint32_t ma = 0;
unsigned long bayGio = micros();
uint32_t doDai = bayGio - lucTruoc;
lucTruoc = bayGio;
if (digitalRead(RX_PIN) == LOW) { // Vừa hết một đoạn mức cao
thoiGianCao = doDai;
return;
}
// Vừa hết một đoạn mức thấp: đã có đủ một cặp cao, thấp
uint32_t thoiGianThap = doDai;
if (thoiGianThap > DONG_BO_MIN_US && thoiGianThap < DONG_BO_MAX_US && thoiGianCao < 2 * T_US) {
soBit = 0; // Xung đồng bộ: bắt đầu một khung mới
ma = 0;
return;
}
if (soBit < 0) {
return;
}
uint32_t tong = thoiGianCao + thoiGianThap;
if (tong < BIT_MIN_US || tong > BIT_MAX_US) {
soBit = -1; // Không phải bit hợp lệ: bỏ khung, chờ đồng bộ lại
return;
}
ma = (ma << 1) | (thoiGianCao > thoiGianThap ? 1 : 0);
soBit++;
if (soBit == 24) {
maVuaNhan = ma;
coMaMoi = true;
soBit = -1;
}
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
pinMode(RX_PIN, INPUT);
attachInterrupt(digitalPinToInterrupt(RX_PIN), khiDoiMuc, CHANGE);
Serial.printf("\nMach thu RF 433 MHz, chi nghe ma thiet bi 0x%04X\n", MA_THIET_BI);
}
void loop() {
static uint32_t maTruoc = 0;
static unsigned long lucMaTruoc = 0;
static unsigned long lucXuLyCuoi = 0;
if (!coMaMoi) {
return;
}
noInterrupts();
uint32_t ma = maVuaNhan;
coMaMoi = false;
interrupts();
// Nhiễu có thể tạo ra một khung ngẫu nhiên; hai khung liên tiếp giống hệt nhau thì gần như chắc chắn là thật
bool lapLai = (ma == maTruoc) && (millis() - lucMaTruoc < CUA_SO_LAP_MS);
maTruoc = ma;
lucMaTruoc = millis();
if (!lapLai || millis() - lucXuLyCuoi < 500) {
return; // Chưa đủ tin cậy, hoặc vừa xử lý chính lần nhấn này
}
lucXuLyCuoi = millis();
uint16_t maThietBi = ma >> 8;
uint8_t lenh = ma & 0xFF;
Serial.printf("Nhan ma 0x%06X: thiet bi 0x%04X, lenh 0x%02X", (unsigned)ma, maThietBi, lenh);
if (maThietBi != MA_THIET_BI) {
Serial.println(" -> khong phai ma cua minh, bo qua");
} else if (lenh == LENH_BAT) {
digitalWrite(LED_PIN, HIGH);
Serial.println(" -> BAT LED");
} else if (lenh == LENH_TAT) {
digitalWrite(LED_PIN, LOW);
Serial.println(" -> TAT LED");
} else {
Serial.println(" -> lenh khong ro");
}
}
Hàm ngắt khiDoiMuc() chạy mỗi khi chân DATA đổi mức và đo thời gian từ lần đổi mức trước:
- Ở sườn xuống, khoảng thời gian vừa đo là độ dài đoạn mức cao; hàm ghi lại rồi thoát.
- Ở sườn lên, khoảng thời gian vừa đo là độ dài đoạn mức thấp; lúc này đã có đủ một cặp cao, thấp.
- Cặp có đoạn thấp dài 24T đến 40T và đoạn cao ngắn là xung đồng bộ: bắt đầu đếm bit từ 0.
- Các cặp tiếp theo phải có tổng thời gian gần 4T (cho phép lệch 40 %). Đoạn cao dài hơn đoạn thấp là bit 1, ngược lại là bit 0.
- Gặp một cặp sai độ dài thì bỏ cả khung và chờ xung đồng bộ kế tiếp. Nhiễu nền bị loại gần hết ở bước này.
- Đủ 24 bit thì chuyển mã cho
loop()qua hai biếnvolatile.
Trong loop(), mã chỉ được chấp nhận khi trùng với mã của khung ngay trước đó trong vòng 300 ms. Sau khi thực hiện lệnh, chương trình bỏ qua các khung lặp còn lại trong 500 ms để một lần nhấn nút chỉ được xử lý một lần. Cách dùng ngắt trên ESP32 có trong bài ngắt GPIO và chống dội nút nhấn.
7. Kết quả mong đợi
- Nạp chương trình phát cho mạch thứ nhất, chương trình thu cho mạch thứ hai. Mở Tools > Serial Monitor ở 115200 baud cho mạch thu.
- Nhấn nút BOOT trên mạch phát: đèn LED trên mạch thu sáng. Nhấn lần nữa: đèn tắt.
- Đưa hai mạch ra xa dần để biết tầm liên lạc; thử lại sau khi tháo ăng-ten để thấy khác biệt.
- Đổi
MA_THIET_BIở mạch phát rồi nạp lại: mạch thu in mã nhận được và báo không phải mã của mình.
8. Giới hạn và lưu ý khi dùng
- Không có xác nhận: đường truyền một chiều, mạch phát không biết lệnh đã tới nơi chưa. Việc quan trọng cần đường truyền hai chiều có trả lời, như ESP-NOW hoặc LoRa.
- Không bảo mật: mã cố định truyền đi không mã hóa; ai có một mạch thu 433 MHz ở gần cũng đọc được và phát lại được. Không dùng cách này cho khóa cửa, cổng hay báo động. Thiết bị cần bảo mật dùng mã thay đổi sau mỗi lần nhấn (mã nhảy) hoặc giao thức có mã hóa.
- Băng tần dùng chung: nhiều thiết bị cùng phát ở 433 MHz. Khi hai thiết bị phát cùng lúc, cả hai khung đều hỏng; khung lặp lại và mã thiết bị giúp giảm ảnh hưởng nhưng không loại bỏ được.
- Chỉ dùng cho thiết bị của chính mình và giữ nguyên công suất của module bán sẵn, không gắn thêm mạch khuếch đại công suất.
- Tốc độ thấp: khoảng 700 bit mỗi giây, hợp với lệnh bật tắt và số đo nhỏ. Dữ liệu dài hơn nên thêm mã kiểm tra lỗi CRC vào khung.
9. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Mạch thu không in gì dù hai mạch đặt gần nhau | Sai chân DATA, module chưa có nguồn, hai chương trình khác giá trị T | Kiểm tra dây theo bảng nối; để T_US giống nhau ở hai chương trình |
| Vẫn không nhận được khi module thu chạy 5 V qua mạch chia áp | Một số module thu có mức cao ở chân DATA thấp hơn nguồn khá nhiều; sau chia áp không còn đủ để ESP32 nhận là mức cao | Đo mức cao của chân DATA bằng máy hiện sóng; nếu không vượt 3,6 V thì bỏ mạch chia áp, nối thẳng DATA vào D27 |
| Chỉ nhận được ở khoảng cách rất gần | Thiếu ăng-ten, ăng-ten sai chiều dài hoặc cuộn lại; mạch phát cấp nguồn thấp | Hàn dây 17,3 cm cho cả hai module, để thẳng; cấp 5 V cho module phát |
| Lúc nhận lúc không | Nhiễu từ thiết bị 433 MHz khác, nguồn của module thu bị nhiễu | Tăng SO_LAN_LAP; gắn tụ 100 nF và 10 µF sát chân nguồn module thu; đặt module xa mạch nguồn xung |
| Mạch thu in mã lạ | Bắt được thiết bị 433 MHz khác ở gần | Bình thường; chương trình đã bỏ qua mã không khớp MA_THIET_BI |
| Phải nhấn hai lần đèn mới đổi | Một lượt phát bị lỡ; mạch phát gửi lệnh bật, tắt xen kẽ nên lần nhấn sau mang lệnh còn lại | Xem Serial Monitor của mạch phát để biết lệnh vừa gửi; tăng SO_LAN_LAP |
| Thêm Wi-Fi hoặc việc nặng khác vào chương trình thì nhận kém | Hàm ngắt bị trễ, thời gian đo sai lệch nhiều hơn | Tăng T ở cả hai chương trình, ví dụ lên 500 µs |
10. Xem thêm
- Ngắt GPIO trên ESP32 và chống dội nút nhấn.
- Điều khiển relay bằng ESP32: thay đèn LED trên mạch thu bằng một relay.
- Deep sleep trên ESP32: cho mạch phát chạy pin, chỉ thức dậy khi nhấn nút.
Xem thêm