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.
Hai module LoRa giống nhau có thể chỉ liên lạc được vài trăm mét hoặc đi xa hơn nhiều lần, tùy cách đặt bốn tham số: hệ số trải phổ (SF), băng thông (BW), tỉ lệ mã (CR) và công suất phát. Bài viết giải thích từng tham số, cách tính thời gian phát một gói, rồi hướng dẫn làm một bộ thử tầm xa bằng ESP32 và module SX1278: mạch phát đặt cố định, mạch thu cầm tay có màn hình OLED hiện RSSI, SNR và số gói mất.
Bài dùng lại cách nối dây và thư viện của bài LoRa cơ bản với module SX1278.
1. Bốn tham số cấu hình
- Hệ số trải phổ (SF), từ 7 đến 12: mỗi ký hiệu mang SF bit và kéo dài 2SF / BW giây. SF lớn thì ký hiệu dài, máy thu gom được nhiều năng lượng hơn nên nhận được tín hiệu yếu hơn.
- Băng thông (BW): thường dùng 125 kHz, ngoài ra có 250 kHz và 500 kHz. Băng thông hẹp cho độ nhạy tốt hơn nhưng gửi chậm hơn.
- Tỉ lệ mã (CR), từ 4/5 đến 4/8: số bit sửa lỗi thêm vào. 4/5 là ít nhất, 4/8 là nhiều nhất.
- Công suất phát: thư viện cho đặt từ 2 đến 20 dBm. Tăng 3 dB là gấp đôi công suất. Công suất phải nằm trong giới hạn cho phép của quy định vô tuyến nơi sử dụng.
Mạch phát và mạch thu phải đặt giống nhau SF, BW và CR. Công suất phát là việc riêng của mạch phát.
2. SF ảnh hưởng thế nào?
| SF | Tốc độ dữ liệu | Thời gian phát gói 20 byte | Độ nhạy (xấp xỉ) | SNR tối thiểu |
|---|---|---|---|---|
| 7 | 5469 bit/s | 56,6 ms | -123 dBm | -7,5 dB |
| 8 | 3125 bit/s | 102,9 ms | -126 dBm | -10 dB |
| 9 | 1758 bit/s | 185,3 ms | -129 dBm | -12,5 dB |
| 10 | 977 bit/s | 370,7 ms | -132 dBm | -15 dB |
| 11 | 537 bit/s | 741,4 ms | -133 dBm | -17,5 dB |
| 12 | 293 bit/s | 1318,9 ms | -136 dBm | -20 dB |
Từ SF7 lên SF12, máy thu nhạy hơn khoảng 13 dB nhưng một gói 20 byte phát lâu hơn 23 lần. Thời gian phát dài đồng nghĩa tốn pin hơn và chiếm kênh lâu hơn, nên chỉ tăng SF đến mức vừa đủ cho khoảng cách cần dùng.
3. Tính thời gian phát một gói
Thời gian phát (time on air) tính theo công thức trong tài liệu của Semtech:
Tsym = 2^SF / BW
Nsym = 8 + max( ceil( (8 x PL - 4 x SF + 28 + 16) / (4 x (SF - 2 x DE)) ) x (CR + 4), 0 )
Tgoi = (8 + 4,25) x Tsym + Nsym x Tsym
PLlà số byte dữ liệu;CRlà 1 đến 4 ứng với tỉ lệ mã 4/5 đến 4/8.DEbằng 1 khi một ký hiệu dài hơn 16 ms (SF11 và SF12 ở băng thông 125 kHz), còn lại bằng 0. Thư viện tự bật chế độ này.- Công thức ứng với gói có 8 ký hiệu mở đầu, có tiêu đề và có CRC, đúng với cấu hình của chương trình trong bài.
Chương trình phát ở mục 5 có hàm thoiGianPhatMs() tính đúng công thức này và in cạnh thời gian đo được để đối chiếu.
4. Chuẩn bị và nối dây
- Hai mạch ESP32 DevKit V1 30 chân, hai module LoRa SX1278 loại Ra-02 kèm anten 433 MHz.
- Một màn hình OLED 0.96 inch SSD1306 cho mạch thu.
- Nguồn di động cho mạch thu: sạc dự phòng qua cổng USB, hoặc pin 18650 với mô đun pin Lithium-ion 18650.
- Breadboard và dây cắm test board.
- Arduino IDE 2.x, gói esp32 (bài dùng 3.3.10), thư viện LoRa của Sandeep Mistry (0.8.0) và Adafruit SSD1306 (2.5.17) kèm Adafruit GFX Library.
| Linh kiện | Chân | ESP32 DevKit V1 |
|---|---|---|
| Module LoRa | 3.3V | 3V3 |
| GND | GND | |
| MOSI | D23 | |
| MISO | D19 | |
| SCK | D18 | |
| NSS | D5 | |
| RST | D4 | |
| DIO0 | RX2 (GPIO16) | |
| Màn hình OLED (chỉ mạch thu) | VCC | 3V3 |
| GND | GND | |
| SCL | D22 | |
| SDA | D21 |
5. Chương trình cho mạch phát
Mạch phát gửi một gói ngắn mỗi 3 giây. Bốn hằng số ở đầu chương trình là nơi đổi cấu hình cho mỗi lần thử:
/*
PYWORLD.VN - Thử tầm xa LoRa: mạch PHÁT đặt cố định
Phát đều đặn gói có số thứ tự; đổi SF, băng thông, tỉ lệ mã, công suất ở các hằng số bên dưới
Nối dây: NSS = D5, SCK = D18, MISO = D19, MOSI = D23, RST = D4, DIO0 = RX2 (GPIO16), nguồn 3V3
Thư viện: LoRa (Sandeep Mistry)
*/
#include <SPI.h>
#include <LoRa.h>
#define LORA_NSS 5
#define LORA_RST 4
#define LORA_DIO0 16
#define LED_PIN 2
const long TAN_SO_HZ = 433E6;
const int TU_DONG_BO = 0xF3;
// Bốn tham số quyết định tầm xa và tốc độ. Mạch thu phải đặt giống ba tham số đầu
const int HE_SO_TRAI_PHO = 9; // SF: 7 đến 12
const long BANG_THONG_HZ = 125E3; // BW: 125E3, 250E3 hoặc 500E3
const int TI_LE_MA = 5; // CR 4/5 đến 4/8: ghi 5, 6, 7 hoặc 8
const int CONG_SUAT_DBM = 14; // 2 đến 20 dBm; tuân theo quy định về công suất phát
const unsigned long CHU_KY_PHAT_MS = 3000;
unsigned long soThuTu = 0;
unsigned long lanPhatTruoc = 0;
// Thời gian phát một gói (time on air), tính theo công thức của Semtech
// tiLeMa là mẫu số của tỉ lệ mã: 5, 6, 7 hoặc 8
float thoiGianPhatMs(int sf, long bw, int tiLeMa, int soByte) {
float tKyHieu = (float)(1UL << sf) / bw * 1000.0; // Thời gian một ký hiệu, ms
int toiUuTocDoThap = (tKyHieu > 16.0) ? 1 : 0; // Thư viện tự bật khi ký hiệu dài hơn 16 ms
float tu = 8.0 * soByte - 4.0 * sf + 28 + 16; // 16: có CRC; header tường minh
float mau = 4.0 * (sf - 2 * toiUuTocDoThap);
float soKyHieu = 8 + max(ceilf(tu / mau) * tiLeMa, 0.0f);
return (8 + 4.25) * tKyHieu + soKyHieu * tKyHieu; // 8 ký hiệu mở đầu
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
LoRa.setPins(LORA_NSS, LORA_RST, LORA_DIO0);
if (!LoRa.begin(TAN_SO_HZ)) {
Serial.println("Khong tim thay module LoRa, kiem tra day noi va nguon 3V3");
while (true) {
delay(1000);
}
}
LoRa.setSyncWord(TU_DONG_BO);
LoRa.setSpreadingFactor(HE_SO_TRAI_PHO);
LoRa.setSignalBandwidth(BANG_THONG_HZ);
LoRa.setCodingRate4(TI_LE_MA);
LoRa.setTxPower(CONG_SUAT_DBM);
LoRa.enableCrc();
Serial.printf("Mach phat: SF%d, BW %ld Hz, CR 4/%d, %d dBm\n",
HE_SO_TRAI_PHO, BANG_THONG_HZ, TI_LE_MA, CONG_SUAT_DBM);
}
void loop() {
if (millis() - lanPhatTruoc < CHU_KY_PHAT_MS) {
return;
}
lanPhatTruoc = millis();
soThuTu++;
char goi[24];
int soByte = snprintf(goi, sizeof(goi), "T;%lu", soThuTu);
digitalWrite(LED_PIN, HIGH);
unsigned long batDau = millis();
LoRa.beginPacket();
LoRa.write((const uint8_t *)goi, soByte);
LoRa.endPacket();
unsigned long daMat = millis() - batDau;
digitalWrite(LED_PIN, LOW);
Serial.printf("Phat goi %lu (%d byte): do duoc %lu ms, tinh theo cong thuc %.1f ms\n",
soThuTu, soByte, daMat, thoiGianPhatMs(HE_SO_TRAI_PHO, BANG_THONG_HZ, TI_LE_MA, soByte));
}
6. Chương trình cho mạch thu
Mạch thu đếm số gói nhận được, dùng số thứ tự để suy ra số gói mất, rồi hiện kết quả lên màn hình. Nếu không gắn màn hình, chương trình vẫn chạy và in ra Serial Monitor.
/*
PYWORLD.VN - Thử tầm xa LoRa: mạch THU cầm tay có màn hình OLED
Hiện RSSI, SNR, số gói nhận được và số gói mất để mang đi thử ở nhiều vị trí
LoRa: NSS = D5, SCK = D18, MISO = D19, MOSI = D23, RST = D4, DIO0 = RX2 (GPIO16), nguồn 3V3
OLED SSD1306 I2C: VCC = 3V3, GND = GND, SCL = D22, SDA = D21
Thư viện: LoRa (Sandeep Mistry), Adafruit SSD1306, Adafruit GFX
*/
#include <SPI.h>
#include <Wire.h>
#include <LoRa.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>
#define LORA_NSS 5
#define LORA_RST 4
#define LORA_DIO0 16
#define DIA_CHI_OLED 0x3C
const long TAN_SO_HZ = 433E6;
const int TU_DONG_BO = 0xF3;
// Ba tham số này phải giống mạch phát
const int HE_SO_TRAI_PHO = 9;
const long BANG_THONG_HZ = 125E3;
const int TI_LE_MA = 5;
Adafruit_SSD1306 manHinh(128, 64, &Wire, -1);
bool coManHinh = false;
unsigned long soNhan = 0;
unsigned long soMat = 0;
unsigned long soThuTuTruoc = 0;
int rssi = 0;
float snr = 0;
void veManHinh() {
if (!coManHinh) {
return;
}
manHinh.clearDisplay();
manHinh.setTextSize(1);
manHinh.setCursor(0, 4);
manHinh.printf("LoRa SF%d BW%ldk", HE_SO_TRAI_PHO, BANG_THONG_HZ / 1000);
manHinh.setTextSize(2);
manHinh.setCursor(0, 18);
manHinh.printf("%d dBm", rssi);
manHinh.setTextSize(1);
manHinh.setCursor(0, 40);
manHinh.printf("SNR %.1f dB", snr);
manHinh.setCursor(0, 52);
manHinh.printf("Nhan %lu Mat %lu", soNhan, soMat);
manHinh.display();
}
void setup() {
Serial.begin(115200);
coManHinh = manHinh.begin(SSD1306_SWITCHCAPVCC, DIA_CHI_OLED);
if (coManHinh) {
manHinh.setTextColor(SSD1306_WHITE);
manHinh.clearDisplay();
manHinh.setCursor(0, 4);
manHinh.print("Dang cho goi tin...");
manHinh.display();
} else {
Serial.println("Khong tim thay OLED, chi in ra Serial");
}
LoRa.setPins(LORA_NSS, LORA_RST, LORA_DIO0);
if (!LoRa.begin(TAN_SO_HZ)) {
Serial.println("Khong tim thay module LoRa, kiem tra day noi va nguon 3V3");
while (true) {
delay(1000);
}
}
LoRa.setSyncWord(TU_DONG_BO);
LoRa.setSpreadingFactor(HE_SO_TRAI_PHO);
LoRa.setSignalBandwidth(BANG_THONG_HZ);
LoRa.setCodingRate4(TI_LE_MA);
LoRa.enableCrc();
Serial.printf("Mach thu: SF%d, BW %ld Hz, CR 4/%d\n", HE_SO_TRAI_PHO, BANG_THONG_HZ, TI_LE_MA);
}
void loop() {
int kichThuoc = LoRa.parsePacket();
if (kichThuoc == 0) {
return;
}
char goi[24];
int n = 0;
while (LoRa.available() && n < (int)sizeof(goi) - 1) {
goi[n++] = (char)LoRa.read();
}
goi[n] = '\0';
unsigned long soThuTu = 0;
if (sscanf(goi, "T;%lu", &soThuTu) != 1) {
return; // Không phải gói thử tầm xa
}
// Số thứ tự nhảy cóc nghĩa là có gói bị mất ở giữa
if (soNhan > 0 && soThuTu > soThuTuTruoc + 1) {
soMat += soThuTu - soThuTuTruoc - 1;
}
soThuTuTruoc = soThuTu;
soNhan++;
rssi = LoRa.packetRssi();
snr = LoRa.packetSnr();
float tiLeMat = 100.0 * soMat / (soNhan + soMat);
Serial.printf("Goi %lu | RSSI %d dBm | SNR %.1f dB | nhan %lu, mat %lu (%.1f %%)\n",
soThuTu, rssi, snr, soNhan, soMat, tiLeMat);
veManHinh();
}
7. Kết quả mong đợi
Ở mạch phát, thời gian đo được thường lớn hơn số tính theo công thức vài mili giây vì có thêm thời gian trao đổi qua SPI.
8. Cách thử tầm xa
- Đặt mạch phát cố định ở chỗ cao và thoáng, anten dựng thẳng đứng. Ghi lại cấu hình đang dùng.
- Cầm mạch thu đứng cạnh mạch phát vài mét để chắc chắn hai mạch đang nhận được nhau, rồi nhấn nút EN trên mạch thu để đưa bộ đếm về 0.
- Đi ra xa dần. Ở mỗi điểm dừng lại khoảng một phút, ghi khoảng cách, RSSI, SNR và số gói mất.
- Điểm xa nhất còn dùng được là nơi tỉ lệ mất gói còn chấp nhận được với ứng dụng, ví dụ dưới 10 %.
- Đổi một tham số ở cả hai mạch (ví dụ SF9 lên SF11) rồi thử lại đúng các điểm cũ để so sánh.
Đọc kết quả: ở cự ly xa, SNR cho biết nhiều hơn RSSI. Khi SNR xuống gần mức tối thiểu của SF đang dùng (cột cuối trong bảng ở mục 2) thì gói bắt đầu mất. Lúc đó tăng SF sẽ nối lại được liên lạc.
9. Các cách tăng tầm xa
- Anten và vị trí: dùng anten đúng băng 433 MHz, đặt cao, giữ tầm nhìn thẳng giữa hai mạch. Nâng anten lên vài mét thường hiệu quả hơn tăng công suất.
- Tăng SF trước: mỗi bậc SF thêm khoảng 2,5 đến 3 dB mà không tăng công suất phát.
- Giảm băng thông: từ 125 kHz xuống 62,5 kHz thêm khoảng 3 dB độ nhạy, nhưng băng thông càng hẹp thì càng đòi hỏi tần số hai mạch khớp nhau.
- Tăng tỉ lệ mã: dùng 4/8 khi môi trường có nhiễu ngắt quãng.
- Công suất phát: là lựa chọn sau cùng vì tốn điện và bị giới hạn bởi quy định.
- Gói ngắn: gửi dạng nhị phân thay vì chuỗi chữ để giảm số byte và thời gian phát.
10. Xử lý lỗi thường gặp
| Hiện tượng | Nguyên nhân thường gặp | Cách xử lý |
|---|---|---|
| Đổi SF xong thì không nhận được gói nào | Chỉ đổi ở một mạch | Đặt SF, BW, CR giống nhau ở cả hai chương trình rồi nạp lại cả hai |
Màn hình đứng yên ở dòng Dang cho goi tin | Mạch phát chưa chạy, khác tần số, khác từ đồng bộ hoặc đã ra ngoài tầm | Đưa hai mạch lại gần để kiểm tra trước; xem Serial Monitor của mạch phát |
| Số gói mất tăng vọt một lần rồi dừng | Mạch phát vừa khởi động lại nên số thứ tự quay về 1, hoặc mạch thu vừa ra khỏi vùng khuất | Khởi động lại mạch thu để đếm lại từ đầu |
| Ở SF11, SF12 mạch phát in thời gian phát rất dài | Đúng với công thức: mỗi gói mất khoảng 0,7 đến 1,3 giây | Tăng CHU_KY_PHAT_MS để kênh không bị chiếm liên tục |
| RSSI tốt nhưng vẫn mất gói | Có nguồn nhiễu cùng tần số, SNR thấp | Xem SNR; đổi tần số trong dải cho phép hoặc tăng SF |
Không thấy màn hình, Serial in Khong tim thay OLED | Nối nhầm SDA với SCL hoặc địa chỉ khác 0x3C | Kiểm tra dây; quét địa chỉ theo bài giao tiếp I2C trên ESP32 |
11. Xem thêm
- Deep Sleep trên ESP32: kết hợp với thời gian phát ngắn để node LoRa dùng pin được lâu.
- Đọc ADC trên ESP32 và đo điện áp pin 18650.
- Đồng hồ Internet với ESP32 và màn hình OLED 0.96 inch: thêm ví dụ vẽ chữ trên OLED.
- Các mạch ESP32 tại PYWORLD: danh mục ESP32.
Xem thêm