Điều khiển hồng ngoại hoạt động thế nào: sóng mang 38 kHz, giao thức NEC và ESP32
Giải thích điều khiển hồng ngoại: sóng mang 38 kHz, mắt thu và giao thức NEC với khung 32 bit, mã lặp. Hướng dẫn tự giải mã phím điều khiển và tự phát lệnh bằng ESP32, không dùng thư viện ngoài.
Điều khiển tivi, quạt, máy lạnh, đầu thu đều dùng chung một kỹ thuật: một LED hồng ngoại nhấp nháy theo quy luật, và một mắt thu ở thiết bị đọc lại quy luật đó. Đây là kiểu giao tiếp không dây một chiều đơn giản và rẻ nhất, vẫn rất hữu ích cho các dự án điều khiển trong phòng. Bài viết giải thích vì sao phải có sóng mang 38 kHz, giao thức NEC mã hóa địa chỉ và lệnh thế nào, rồi tự viết chương trình giải mã và chương trình phát trên ESP32 mà không dùng thư viện ngoài.
1. Sóng mang 38 kHz và mắt thu hồng ngoại
- Vì sao cần sóng mang? Ánh nắng, bóng đèn và cả cơ thể người đều phát hồng ngoại. Nếu chỉ bật, tắt LED đơn thuần, mắt thu không phân biệt được tín hiệu với ánh sáng nền. Cho LED nhấp nháy ở đúng 38 kHz, mắt thu chỉ cần lọc lấy thành phần 38 kHz.
- Xung và nghỉ: một xung (mark) là khoảng thời gian LED nhấp nháy 38 kHz; một khoảng nghỉ (space) là lúc LED tắt hẳn.
- Mắt thu hồng ngoại loại ba chân có sẵn đi-ốt thu, mạch khuếch đại tự chỉnh độ nhạy, bộ lọc 38 kHz và mạch tách sóng. Ngõ ra chỉ còn hai mức và đảo ngược: mức thấp khi có xung, mức cao khi nghỉ.
- Dữ liệu không nằm ở mức cao hay thấp mà nằm ở độ dài của từng xung và từng khoảng nghỉ. Mỗi hãng quy định các độ dài đó theo một giao thức riêng; NEC là giao thức phổ biến nhất.
2. Giao thức NEC
Đơn vị thời gian của NEC là T = 562,5 µs.
| Thành phần | Xung | Nghỉ | Ý nghĩa |
|---|---|---|---|
| Mở đầu | 9 ms (16T) | 4,5 ms (8T) | Báo hiệu bắt đầu khung, giúp mắt thu ổn định độ nhạy |
| Bit 0 | 562,5 µs (1T) | 562,5 µs (1T) | Tổng 1,125 ms |
| Bit 1 | 562,5 µs (1T) | 1687,5 µs (3T) | Tổng 2,25 ms |
| Kết thúc | 562,5 µs (1T) | không có | Đánh dấu hết khoảng nghỉ của bit cuối |
| Mã lặp | 9 ms, rồi 1T sau khoảng nghỉ | 2,25 ms (4T) | Phím vẫn đang được giữ |
- Khung 32 bit gồm bốn byte: địa chỉ, địa chỉ đảo, lệnh, lệnh đảo. Mỗi byte gửi bit thấp trước.
- Địa chỉ cho biết lệnh dành cho thiết bị nào, nhờ vậy điều khiển tivi không làm quạt đổi tốc độ. Lệnh là mã của phím.
- Byte đảo là byte liền trước với mọi bit lật lại. Bên thu kiểm tra: lệnh XOR lệnh đảo phải bằng 0xFF, sai thì bỏ khung. Đây là cách kiểm tra lỗi của NEC.
- Nhờ byte đảo, khung nào cũng có đúng 16 bit 0 và 16 bit 1 nên luôn dài như nhau, khoảng 68 ms.
- Mã lặp: khi giữ phím, điều khiển chỉ gửi khung đầy đủ một lần; sau đó cứ 110 ms gửi một mã lặp ngắn. Thiết bị dùng mã lặp để tăng âm lượng liên tục, còn phím nguồn thì bỏ qua mã lặp để không bật tắt liên hồi.
- NEC mở rộng: nhiều điều khiển dùng cả hai byte đầu làm địa chỉ 16 bit, không còn byte địa chỉ đảo; phần lệnh và lệnh đảo giữ nguyên.
3. Chuẩn bị
- Một hoặc hai mạch ESP32 DevKit V1 30 chân (hoặc bản dùng chip CH340).
- Một mắt thu hồng ngoại 38 kHz loại ba chân (dạng linh kiện rời hoặc module).
- Một LED hồng ngoại 940 nm và một điện trở 100 Ω.
- Một điều khiển hồng ngoại bất kỳ trong nhà để thử chương trình thu.
- Breadboard, dây cắm và 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
- Phần lớn mắt thu 38 kHz chạy được ở 3,3 V, nên cấp nguồn từ chân 3V3 và nối thẳng ngõ ra vào D18.
- Thứ tự chân của mắt thu không giống nhau giữa các loại. Xem ký hiệu trên module hoặc tài liệu của linh kiện; nối ngược nguồn làm hỏng mắt thu.
- Điện trở 100 Ω giới hạn dòng qua LED hồng ngoại ở khoảng 15 đến 20 mA, mức một chân ESP32 cấp trực tiếp được. Cách nối này đủ để thử ở khoảng cách gần.
- Muốn phát xa hơn, dùng một transistor NPN để đóng cắt LED từ nguồn 5 V, chân D4 chỉ điều khiển cực nền của transistor qua một điện trở.
- Mắt người không thấy LED hồng ngoại sáng. Camera điện thoại thường thấy được: LED hiện thành một đốm tím nhạt khi đang phát.
5. Chương trình 1: giải mã điều khiển
/*
PYWORLD.VN - Hồng ngoại NEC với ESP32: mạch thu, giải mã khung 32 bit bằng cách đo thời gian trong hàm ngắt
In địa chỉ và lệnh của phím vừa nhấn, nhận biết mã lặp khi giữ phím; lệnh 0x45 đổi trạng thái LED GPIO2
Mắt thu hồng ngoại 38 kHz: OUT = D18, VCC = 3V3, GND = GND (ngõ ra ở mức thấp khi có tín hiệu)
*/
#define IR_RX_PIN 18
#define LED_PIN 2
const uint8_t LENH_DOI_DEN = 0x45; // Sửa theo lệnh của phím muốn dùng
// Giới hạn thời gian, micro giây (giá trị chuẩn ghi trong ngoặc)
const uint32_t XUNG_DAU_MIN = 7000, XUNG_DAU_MAX = 11000; // Xung mở đầu (9000)
const uint32_t NGHI_DAU_MIN = 3500, NGHI_DAU_MAX = 5500; // Khoảng nghỉ sau xung mở đầu (4500)
const uint32_t NGHI_LAP_MIN = 1700, NGHI_LAP_MAX = 2800; // Khoảng nghỉ của mã lặp (2250)
const uint32_t XUNG_BIT_MIN = 300, XUNG_BIT_MAX = 900; // Xung của mỗi bit (562)
const uint32_t NGHI_BIT0_MIN = 300, NGHI_BIT0_MAX = 900; // Khoảng nghỉ của bit 0 (562)
const uint32_t NGHI_BIT1_MIN = 1200, NGHI_BIT1_MAX = 2100; // Khoảng nghỉ của bit 1 (1687)
enum TrangThai : uint8_t { CHO_XUNG_DAU, CHO_NGHI_DAU, NHAN_BIT };
volatile uint32_t khungVuaNhan = 0;
volatile bool coKhungMoi = false;
volatile bool coMaLap = false;
// Chạy mỗi khi ngõ ra của mắt thu đổi mức
void IRAM_ATTR khiDoiMuc() {
static unsigned long lucTruoc = 0;
static TrangThai trangThai = CHO_XUNG_DAU;
static uint8_t soBit = 0;
static uint32_t duLieu = 0;
unsigned long bayGio = micros();
uint32_t doDai = bayGio - lucTruoc;
lucTruoc = bayGio;
if (digitalRead(IR_RX_PIN) == HIGH) {
// Sườn lên: vừa hết một đoạn mức thấp, tức một xung hồng ngoại
if (doDai > XUNG_DAU_MIN && doDai < XUNG_DAU_MAX) {
trangThai = CHO_NGHI_DAU;
} else if (trangThai == NHAN_BIT && (doDai < XUNG_BIT_MIN || doDai > XUNG_BIT_MAX)) {
trangThai = CHO_XUNG_DAU; // Xung sai độ dài: bỏ khung
}
return;
}
// Sườn xuống: vừa hết một khoảng nghỉ
if (trangThai == CHO_NGHI_DAU) {
if (doDai > NGHI_DAU_MIN && doDai < NGHI_DAU_MAX) {
trangThai = NHAN_BIT; // Khung đầy đủ: 32 bit theo sau
soBit = 0;
duLieu = 0;
} else {
if (doDai > NGHI_LAP_MIN && doDai < NGHI_LAP_MAX) {
coMaLap = true; // Mã lặp: phím vẫn đang được giữ
}
trangThai = CHO_XUNG_DAU;
}
return;
}
if (trangThai != NHAN_BIT) {
return;
}
if (doDai > NGHI_BIT1_MIN && doDai < NGHI_BIT1_MAX) {
duLieu |= (1UL << soBit); // Bit thấp tới trước
} else if (doDai < NGHI_BIT0_MIN || doDai > NGHI_BIT0_MAX) {
trangThai = CHO_XUNG_DAU; // Khoảng nghỉ sai độ dài: bỏ khung
return;
}
soBit++;
if (soBit == 32) {
khungVuaNhan = duLieu;
coKhungMoi = true;
trangThai = CHO_XUNG_DAU;
}
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
pinMode(IR_RX_PIN, INPUT_PULLUP);
attachInterrupt(digitalPinToInterrupt(IR_RX_PIN), khiDoiMuc, CHANGE);
Serial.printf("\nMach thu hong ngoai NEC tren D%d. Huong dieu khien vao mat thu roi nhan phim\n", IR_RX_PIN);
}
void loop() {
static bool denDangSang = false;
if (coKhungMoi) {
noInterrupts();
uint32_t khung = khungVuaNhan;
coKhungMoi = false;
coMaLap = false;
interrupts();
uint8_t diaChi = khung & 0xFF;
uint8_t diaChiDao = (khung >> 8) & 0xFF;
uint8_t lenh = (khung >> 16) & 0xFF;
uint8_t lenhDao = (khung >> 24) & 0xFF;
if ((uint8_t)(lenh ^ lenhDao) != 0xFF) {
Serial.printf("Khung 0x%08X: lenh va byte dao khong khop, bo qua\n", (unsigned)khung);
return;
}
if ((uint8_t)(diaChi ^ diaChiDao) == 0xFF) {
Serial.printf("NEC: dia chi 0x%02X, lenh 0x%02X", diaChi, lenh);
} else {
Serial.printf("NEC mo rong: dia chi 0x%04X, lenh 0x%02X", (unsigned)(khung & 0xFFFF), lenh);
}
if (lenh == LENH_DOI_DEN) {
denDangSang = !denDangSang;
digitalWrite(LED_PIN, denDangSang ? HIGH : LOW);
Serial.print(denDangSang ? " -> BAT LED" : " -> TAT LED");
}
Serial.println();
} else if (coMaLap) {
coMaLap = false;
Serial.println("Ma lap: phim dang duoc giu");
}
}
Hàm ngắt khiDoiMuc() chạy ở mỗi lần ngõ ra của mắt thu đổi mức và đo thời gian từ lần đổi mức trước. Vì ngõ ra đảo mức, sườn lên nghĩa là một xung vừa kết thúc, sườn xuống nghĩa là một khoảng nghỉ vừa kết thúc.
- Gặp một xung dài khoảng 9 ms: đó là xung mở đầu, chuyển sang chờ khoảng nghỉ theo sau.
- Khoảng nghỉ đó dài khoảng 4,5 ms thì bắt đầu nhận 32 bit; dài khoảng 2,25 ms thì đây là mã lặp.
- Với mỗi bit, xung phải dài khoảng 1T; khoảng nghỉ khoảng 1T là bit 0, khoảng 3T là bit 1. Bit tới trước được ghi vào vị trí thấp trước.
- Bất kỳ độ dài nào nằm ngoài khoảng cho phép đều làm khung bị bỏ; hàm quay về chờ xung mở đầu.
- Đủ 32 bit thì chuyển cho
loop(), ở đó chương trình kiểm tra byte đảo rồi in địa chỉ và lệnh.
Các giới hạn thời gian để rộng vì mắt thu thường kéo dài xung và rút ngắn khoảng nghỉ một chút. Nhấn lần lượt các phím của điều khiển và ghi lại lệnh của từng phím; sửa LENH_DOI_DEN thành lệnh của phím muốn dùng để bật tắt đèn LED trên mạch.
6. Chương trình 2: phát lệnh NEC
/*
PYWORLD.VN - Hồng ngoại NEC với ESP32: mạch phát, tạo sóng mang 38 kHz bằng LEDC và tự dựng khung 32 bit
Nhấn nút BOOT (GPIO0) để gửi một khung; giữ nút thì cứ 110 ms gửi thêm một mã lặp, đúng như điều khiển thật
LED hồng ngoại: cực dương nối D4 qua điện trở 100 ôm, cực âm nối GND
*/
#define IR_TX_PIN 4
#define NUT_PIN 0 // Nút BOOT trên mạch, nhấn là mức thấp
const uint32_t TAN_SO_SONG_MANG = 38000; // Hz
const uint8_t DO_PHAN_GIAI_BIT = 8; // Độ rộng xung tính trên thang 0 đến 255
const uint32_t DO_RONG_KHI_PHAT = 85; // Khoảng 33 %: LED sáng một phần ba mỗi chu kỳ sóng mang
const uint8_t DIA_CHI = 0x00;
const uint8_t LENH = 0x45;
// Thời gian của giao thức NEC, micro giây
const unsigned int XUNG_DAU_US = 9000;
const unsigned int NGHI_DAU_US = 4500;
const unsigned int NGHI_LAP_US = 2250;
const unsigned int XUNG_BIT_US = 563;
const unsigned int NGHI_BIT0_US = 562;
const unsigned int NGHI_BIT1_US = 1687;
const unsigned long CHU_KY_LAP_MS = 110;
// Xung: bật sóng mang 38 kHz trong khoảng thời gian cho trước
void phatXung(unsigned int thoiGianUs) {
ledcWrite(IR_TX_PIN, DO_RONG_KHI_PHAT);
delayMicroseconds(thoiGianUs);
}
// Nghỉ: tắt hẳn LED
void nghi(unsigned int thoiGianUs) {
ledcWrite(IR_TX_PIN, 0);
delayMicroseconds(thoiGianUs);
}
void guiKhungNec(uint8_t diaChi, uint8_t lenh) {
// Thứ tự trên đường truyền: địa chỉ, địa chỉ đảo, lệnh, lệnh đảo; mỗi byte gửi bit thấp trước
uint32_t duLieu = (uint32_t)diaChi | ((uint32_t)(uint8_t)~diaChi << 8)
| ((uint32_t)lenh << 16) | ((uint32_t)(uint8_t)~lenh << 24);
phatXung(XUNG_DAU_US);
nghi(NGHI_DAU_US);
for (int i = 0; i < 32; i++) {
phatXung(XUNG_BIT_US);
nghi(((duLieu >> i) & 1) ? NGHI_BIT1_US : NGHI_BIT0_US);
}
phatXung(XUNG_BIT_US); // Xung kết thúc: đánh dấu hết khoảng nghỉ của bit cuối
nghi(0);
}
void guiMaLap() {
phatXung(XUNG_DAU_US);
nghi(NGHI_LAP_US);
phatXung(XUNG_BIT_US);
nghi(0);
}
void setup() {
Serial.begin(115200);
pinMode(NUT_PIN, INPUT_PULLUP);
ledcAttach(IR_TX_PIN, TAN_SO_SONG_MANG, DO_PHAN_GIAI_BIT);
ledcWrite(IR_TX_PIN, 0);
Serial.printf("\nMach phat hong ngoai NEC: song mang %u Hz tren D%d. Nhan nut BOOT de gui\n",
(unsigned)TAN_SO_SONG_MANG, IR_TX_PIN);
}
void loop() {
if (digitalRead(NUT_PIN) != LOW) {
return;
}
delay(30); // Chống dội nút nhấn
if (digitalRead(NUT_PIN) != LOW) {
return;
}
unsigned long lucBatDau = millis();
guiKhungNec(DIA_CHI, LENH);
Serial.printf("Da gui khung NEC: dia chi 0x%02X, lenh 0x%02X\n", DIA_CHI, LENH);
int soMaLap = 0;
while (digitalRead(NUT_PIN) == LOW) { // Còn giữ nút: gửi mã lặp theo chu kỳ 110 ms
if (millis() - lucBatDau >= CHU_KY_LAP_MS) {
lucBatDau = millis();
guiMaLap();
soMaLap++;
}
delay(1);
}
if (soMaLap > 0) {
Serial.printf("Da gui them %d ma lap\n", soMaLap);
}
}
- Sóng mang do bộ LEDC của ESP32 tạo ra:
ledcAttach()đặt tần số 38 kHz với độ phân giải 8 bit. Ghi giá trị 85 (khoảng 33 %) là bật sóng mang, ghi 0 là tắt. - LED chỉ sáng một phần ba mỗi chu kỳ: đủ cho mắt thu nhận, lại giảm dòng trung bình qua LED.
guiKhungNec()ghép bốn byte thành một số 32 bit rồi gửi từ bit 0 đến bit 31, đúng thứ tự bit thấp trước của NEC.- Khi còn giữ nút, vòng
whilegửi mã lặp mỗi 110 ms tính từ lúc bắt đầu lần gửi trước.
Để điều khiển một thiết bị có sẵn, dùng chương trình 1 đọc địa chỉ và lệnh của phím trên điều khiển gốc, rồi điền hai giá trị đó vào DIA_CHI và LENH. Điều khiển dùng NEC mở rộng có địa chỉ 16 bit; khi đó cần sửa guiKhungNec() để gửi hai byte địa chỉ thay cho địa chỉ và địa chỉ đảo. Nền tảng về LEDC có trong bài PWM trên ESP32.
7. Kết quả mong đợi
- Nạp chương trình 1, mở Tools > Serial Monitor ở 115200 baud. Hướng một điều khiển trong nhà vào mắt thu và nhấn phím: mỗi phím cho một dòng địa chỉ và lệnh.
- Giữ phím: sau dòng đầu tiên là các dòng báo mã lặp.
- Nạp chương trình 2 cho mạch thứ hai, hướng LED hồng ngoại vào mắt thu của mạch thứ nhất rồi nhấn nút BOOT: mạch thu in địa chỉ 0x00, lệnh 0x45 và đèn LED trên mạch đổi trạng thái.
- Giữ nút BOOT: mạch thu in thêm các dòng mã lặp, đèn không đổi trạng thái thêm.
8. Giới hạn và hướng mở rộng
- Không phải điều khiển nào cũng dùng NEC. Một số hãng dùng sóng mang 36 kHz hoặc 40 kHz và cách mã hóa bit khác; điều khiển máy lạnh thường gửi khung rất dài chứa toàn bộ trạng thái. Chương trình 1 không in gì với các điều khiển đó.
- Truyền một chiều, cần nhìn thấy nhau: hồng ngoại không xuyên tường, mạch phát không biết lệnh đã tới nơi chưa.
- Ánh nắng trực tiếp và một số loại đèn làm giảm tầm thu. Đặt mắt thu khuất nắng.
- Thời gian chính xác: chương trình phát dùng
delayMicroseconds()nên chiếm trọn vi điều khiển trong khoảng 68 ms mỗi khung. Dự án lớn có thể chuyển sang bộ RMT của ESP32, phần cứng chuyên tạo và đo chuỗi xung.
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ý |
|---|---|---|
| Nhấn phím điều khiển mà không in gì | Nối sai thứ tự chân mắt thu, hoặc điều khiển không dùng NEC | Kiểm tra lại ba chân; thử một điều khiển khác |
| Báo lệnh và byte đảo không khớp | Nhiễu ánh sáng hoặc điều khiển ở quá xa làm sai một vài bit | Đưa điều khiển lại gần, che nắng cho mắt thu |
| Chỉ thấy mã lặp, không thấy khung đầy đủ | Khung đầu tiên bị lỗi, các mã lặp theo sau vẫn nhận được | Nhả phím rồi nhấn lại; hướng điều khiển thẳng vào mắt thu |
| Mạch thu không nhận được mạch phát | LED hồng ngoại cắm ngược, sai chân, hoặc đặt quá xa | Nhìn LED qua camera điện thoại khi nhấn nút; đổi chiều LED; đưa hai mạch lại gần |
| Thiết bị thật không phản ứng với lệnh đã chép | Điều khiển gốc dùng NEC mở rộng, hoặc sai địa chỉ | Xem chương trình 1 in "NEC mo rong" hay không; gửi đúng hai byte địa chỉ |
| Tầm phát quá ngắn | Dòng qua LED nhỏ, góc phát hẹp | Dùng transistor để tăng dòng xung; hướng LED thẳng vào mắt thu |
10. Xem thêm
- PWM trên ESP32 với LEDC: nền tảng để tạo sóng mang 38 kHz.
- Ngắt GPIO trên ESP32: cách đo thời gian giữa hai sườn tín hiệu.
- Điều khiển relay bằng ESP32: dùng một phím điều khiển để đóng cắt thiết bị.
Xem thêm