1. Qué vamos a construir
Tendrás dos placas:
RADIO LoRa
868 MHz aprox.
┌──────────────────┐ ┌──────────────────┐
│ ESP32 #1 │ │ ESP32 #2 │
│ │ │ │
│ SX1276 │ )))))))) │ SX1276 │
│ integrado │ │ integrado │
│ │ │ │
│ OLED │ │ OLED │
└──────────────────┘ └──────────────────┘
EMISOR RECEPTOR
No necesitas:
- ❌ SX1278 externo
- ❌ ESP32-C3
- ❌ Wi-Fi
- ❌ Bluetooth
- ❌ LoRaWAN
La comunicación entre las dos placas será directamente mediante el SX1276.
2. Material
Necesitas:
- 2 × ESP32 LoRa SX1276
- 2 × antenas LoRa correspondientes a la banda de la placa
- 2 × cables USB
- Mac con PlatformIO
La antena es especialmente importante. Heltec incluye una antena LoRa con la WiFi LoRa 32 V2 y especifica versiones de hardware para distintas bandas. Heltec Automation
Conecta la antena antes de transmitir.
3. Antes de empezar: comprobar la frecuencia
Aquí hay que corregir algo de tu artículo.
Actualmente tienes:
LoRa.begin(915E6);
Para una placa europea de la variante 868 MHz utilizaremos:
LoRa.begin(868E6);
Pero no debes asumir que cualquier placa SX1276 es de 868 MHz. Heltec comercializó versiones 433 MHz, 470–510 MHz y 863–923 MHz. Heltec Automation
Si tu placa lleva una indicación como:
868
o
863-923 MHz
entonces la configuración de 868 MHz es la adecuada para esta prueba.
4. Crear el proyecto
Vamos a utilizar PlatformIO, como en tu artículo.
En macOS:
mkdir esp32-sx1276-binario
cd esp32-sx1276-binario
Inicializa el proyecto:
pio project init --board heltec_wifi_lora_32_V2
Esto es importante porque no vamos a seleccionar esp32-c3-devkitm-1.
Estamos trabajando con una placa ESP32 LoRa V2.
5. platformio.ini
Abre:
platformio.ini
y déjalo así:
[env:heltec_wifi_lora_32_V2]
platform = espressif32
board = heltec_wifi_lora_32_V2
framework = arduino
monitor_speed = 115200
lib_deps =
sandeepmistry/LoRa @ ^0.8.0
olikraus/U8g2 @ ^2.35.19
La propia documentación de Heltec indica que WiFi LoRa 32 V2 admite PlatformIO y Arduino. Heltec Automation
6. Estructura
Vamos a tener dos proyectos separados:
esp32-sx1276-emisor/
│
├── platformio.ini
│
└── src/
└── main.cpp
y:
esp32-sx1276-receptor/
│
├── platformio.ini
│
└── src/
└── main.cpp
Puedes copiar el proyecto y cambiar únicamente main.cpp.
7. ESP32 #1 — EMISOR
El primer ESP32 va a generar datos binarios.
No vamos a enviar únicamente:
Hola
Vamos a enviar bytes como:
48 4F 4C 41 00 FF 01 02 03
Es decir:
H O L A [00] [FF] [01] [02] [03]
Crea:
src/main.cpp
con:
#include <Arduino.h>
#include <SPI.h>
#include <LoRa.h>
#include <U8g2lib.h>
// ============================================================
// SX1276 - HELTEC WIFI LORA 32 V2
// ============================================================
#define LORA_SCK 5
#define LORA_MISO 19
#define LORA_MOSI 27
#define LORA_SS 18
#define LORA_RST 14
#define LORA_DIO0 26
// ============================================================
// OLED
// ============================================================
#define OLED_SDA 4
#define OLED_SCL 15
#define OLED_RST 16
U8G2_SSD1306_128X64_NONAME_F_HW_I2C
u8g2(
U8G2_R0,
OLED_RST,
OLED_SCL,
OLED_SDA
);
// ============================================================
// RADIO
// ============================================================
#define LORA_FREQUENCY 868E6
// ============================================================
// CONTADOR
// ============================================================
uint32_t packetCounter = 0;
// ============================================================
// MOSTRAR HEX
// ============================================================
void printHex(const uint8_t *data, size_t length)
{
for (size_t i = 0; i < length; i++)
{
if (data[i] < 0x10)
{
Serial.print("0");
}
Serial.print(data[i], HEX);
if (i + 1 < length)
{
Serial.print(" ");
}
}
Serial.println();
}
// ============================================================
// CRC-16
// ============================================================
uint16_t crc16(
const uint8_t *data,
size_t length
)
{
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < length; i++)
{
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++)
{
if (crc & 1)
{
crc =
(crc >> 1) ^
0xA001;
}
else
{
crc >>= 1;
}
}
}
return crc;
}
// ============================================================
// OLED
// ============================================================
void updateOLED(
uint32_t counter,
const uint8_t *data,
size_t length
)
{
u8g2.clearBuffer();
u8g2.setFont(
u8g2_font_6x10_tf
);
u8g2.drawStr(
0,
10,
"LORA TRANSMISOR"
);
u8g2.setCursor(0, 23);
u8g2.print("PKT: ");
u8g2.print(counter);
u8g2.setCursor(0, 36);
u8g2.print("BYTES: ");
u8g2.print(length);
u8g2.setCursor(0, 49);
u8g2.print("HEX:");
if (length >= 4)
{
u8g2.setCursor(0, 62);
for (size_t i = 0;
i < 4;
i++)
{
if (data[i] < 0x10)
{
u8g2.print("0");
}
u8g2.print(
data[i],
HEX
);
if (i < 3)
{
u8g2.print(" ");
}
}
}
u8g2.sendBuffer();
}
// ============================================================
// ENVIAR
// ============================================================
void sendBinaryPacket()
{
// --------------------------------------------------------
// DATOS BINARIOS
// --------------------------------------------------------
uint8_t data[] =
{
0x48,
0x4F,
0x4C,
0x41,
0x00,
0xFF,
0x01,
0x02,
0x03
};
const uint8_t dataLength =
sizeof(data);
// --------------------------------------------------------
// PAQUETE
// --------------------------------------------------------
uint8_t packet[32];
size_t position = 0;
// --------------------------------------------------------
// CONTADOR - 4 BYTES
// --------------------------------------------------------
packet[position++] =
(packetCounter >> 24) & 0xFF;
packet[position++] =
(packetCounter >> 16) & 0xFF;
packet[position++] =
(packetCounter >> 8) & 0xFF;
packet[position++] =
packetCounter & 0xFF;
// --------------------------------------------------------
// LONGITUD
// --------------------------------------------------------
packet[position++] =
dataLength;
// --------------------------------------------------------
// DATOS
// --------------------------------------------------------
memcpy(
&packet[position],
data,
dataLength
);
position += dataLength;
// --------------------------------------------------------
// CRC
// --------------------------------------------------------
uint16_t crc =
crc16(
data,
dataLength
);
packet[position++] =
crc & 0xFF;
packet[position++] =
(crc >> 8) & 0xFF;
// --------------------------------------------------------
// INFORMACIÓN
// --------------------------------------------------------
Serial.println();
Serial.println(
"=============================="
);
Serial.println(
"TRANSMITIENDO"
);
Serial.println(
"=============================="
);
Serial.print(
"Paquete: "
);
Serial.println(
packetCounter
);
Serial.print(
"Datos: "
);
printHex(
data,
dataLength
);
Serial.print(
"Bytes totales: "
);
Serial.println(
position
);
// --------------------------------------------------------
// RADIO
// --------------------------------------------------------
LoRa.beginPacket();
LoRa.write(
packet,
position
);
int result =
LoRa.endPacket();
if (result == 1)
{
Serial.println(
"Transmision: OK"
);
}
else
{
Serial.println(
"Transmision: ERROR"
);
}
// --------------------------------------------------------
// OLED
// --------------------------------------------------------
updateOLED(
packetCounter,
data,
dataLength
);
packetCounter++;
}
// ============================================================
// SETUP
// ============================================================
void setup()
{
Serial.begin(115200);
delay(2000);
Serial.println();
Serial.println(
"ESP32 + SX1276"
);
Serial.println(
"TRANSMISOR"
);
// --------------------------------------------------------
// OLED
// --------------------------------------------------------
u8g2.begin();
// --------------------------------------------------------
// SPI
// --------------------------------------------------------
SPI.begin(
LORA_SCK,
LORA_MISO,
LORA_MOSI,
LORA_SS
);
// --------------------------------------------------------
// SX1276
// --------------------------------------------------------
LoRa.setPins(
LORA_SS,
LORA_RST,
LORA_DIO0
);
Serial.println(
"Inicializando SX1276..."
);
if (!LoRa.begin(
LORA_FREQUENCY
))
{
Serial.println(
"ERROR SX1276"
);
while (true)
{
delay(1000);
}
}
// --------------------------------------------------------
// PARÁMETROS
// --------------------------------------------------------
LoRa.setSpreadingFactor(7);
LoRa.setSignalBandwidth(
125E3
);
LoRa.setCodingRate4(5);
LoRa.setPreambleLength(8);
LoRa.setSyncWord(0x12);
LoRa.enableCrc();
Serial.println(
"SX1276 inicializado"
);
Serial.println(
"Frecuencia: 868 MHz"
);
Serial.println(
"Emisor preparado"
);
}
// ============================================================
// LOOP
// ============================================================
void loop()
{
sendBinaryPacket();
delay(2000);
}
8. ESP32 #2 — RECEPTOR
El segundo ESP32 tendrá exactamente la misma configuración de hardware.
Su platformio.ini será igual.
En src/main.cpp:
#include <Arduino.h>
#include <SPI.h>
#include <LoRa.h>
#include <U8g2lib.h>
// ============================================================
// SX1276
// ============================================================
#define LORA_SCK 5
#define LORA_MISO 19
#define LORA_MOSI 27
#define LORA_SS 18
#define LORA_RST 14
#define LORA_DIO0 26
// ============================================================
// OLED
// ============================================================
#define OLED_SDA 4
#define OLED_SCL 15
#define OLED_RST 16
U8G2_SSD1306_128X64_NONAME_F_HW_I2C
u8g2(
U8G2_R0,
OLED_RST,
OLED_SCL,
OLED_SDA
);
// ============================================================
// RADIO
// ============================================================
#define LORA_FREQUENCY 868E6
// ============================================================
// HEX
// ============================================================
void printHex(
const uint8_t *data,
size_t length
)
{
for (size_t i = 0; i < length; i++)
{
if (data[i] < 0x10)
{
Serial.print("0");
}
Serial.print(
data[i],
HEX
);
if (i + 1 < length)
{
Serial.print(" ");
}
}
Serial.println();
}
// ============================================================
// CRC
// ============================================================
uint16_t crc16(
const uint8_t *data,
size_t length
)
{
uint16_t crc = 0xFFFF;
for (size_t i = 0; i < length; i++)
{
crc ^= data[i];
for (uint8_t j = 0; j < 8; j++)
{
if (crc & 1)
{
crc =
(crc >> 1) ^
0xA001;
}
else
{
crc >>= 1;
}
}
}
return crc;
}
// ============================================================
// OLED
// ============================================================
void showOLED(
uint32_t counter,
uint8_t dataLength,
int rssi,
float snr,
bool valid
)
{
u8g2.clearBuffer();
u8g2.setFont(
u8g2_font_6x10_tf
);
u8g2.drawStr(
0,
10,
"LORA RECEPTOR"
);
u8g2.setCursor(0, 22);
u8g2.print("PKT: ");
u8g2.print(counter);
u8g2.setCursor(0, 34);
u8g2.print("BYTES: ");
u8g2.print(dataLength);
u8g2.setCursor(0, 46);
u8g2.print("RSSI: ");
u8g2.print(rssi);
u8g2.print(" dBm");
u8g2.setCursor(0, 58);
u8g2.print(
valid ? "CRC: OK" : "CRC: ERROR"
);
u8g2.sendBuffer();
}
// ============================================================
// SETUP
// ============================================================
void setup()
{
Serial.begin(115200);
delay(2000);
Serial.println();
Serial.println(
"ESP32 + SX1276"
);
Serial.println(
"RECEPTOR"
);
// OLED
u8g2.begin();
// SPI
SPI.begin(
LORA_SCK,
LORA_MISO,
LORA_MOSI,
LORA_SS
);
// SX1276
LoRa.setPins(
LORA_SS,
LORA_RST,
LORA_DIO0
);
Serial.println(
"Inicializando SX1276..."
);
if (!LoRa.begin(
LORA_FREQUENCY
))
{
Serial.println(
"ERROR SX1276"
);
while (true)
{
delay(1000);
}
}
// --------------------------------------------------------
// PARÁMETROS RADIO
// --------------------------------------------------------
LoRa.setSpreadingFactor(7);
LoRa.setSignalBandwidth(
125E3
);
LoRa.setCodingRate4(5);
LoRa.setPreambleLength(8);
LoRa.setSyncWord(0x12);
LoRa.enableCrc();
Serial.println(
"SX1276 inicializado"
);
Serial.println(
"Frecuencia: 868 MHz"
);
Serial.println(
"Esperando paquetes..."
);
}
// ============================================================
// LOOP
// ============================================================
void loop()
{
int packetSize =
LoRa.parsePacket();
if (packetSize <= 0)
{
return;
}
// --------------------------------------------------------
// RECIBIR
// --------------------------------------------------------
uint8_t packet[64];
int length = 0;
while (
LoRa.available() &&
length < sizeof(packet)
)
{
packet[length++] =
LoRa.read();
}
Serial.println();
Serial.println(
"=============================="
);
Serial.println(
"PAQUETE RECIBIDO"
);
Serial.println(
"=============================="
);
Serial.print(
"RAW: "
);
printHex(
packet,
length
);
// --------------------------------------------------------
// ESTRUCTURA MÍNIMA
// --------------------------------------------------------
if (length < 7)
{
Serial.println(
"Paquete demasiado pequeño"
);
return;
}
// --------------------------------------------------------
// CONTADOR
// --------------------------------------------------------
uint32_t counter =
((uint32_t)packet[0] << 24) |
((uint32_t)packet[1] << 16) |
((uint32_t)packet[2] << 8) |
packet[3];
// --------------------------------------------------------
// LONGITUD
// --------------------------------------------------------
uint8_t dataLength =
packet[4];
if (
5 +
dataLength +
2 !=
length
)
{
Serial.println(
"Estructura incorrecta"
);
return;
}
// --------------------------------------------------------
// DATOS
// --------------------------------------------------------
uint8_t *data =
&packet[5];
Serial.print(
"Paquete: "
);
Serial.println(
counter
);
Serial.print(
"Longitud: "
);
Serial.println(
dataLength
);
Serial.print(
"Datos HEX: "
);
printHex(
data,
dataLength
);
// --------------------------------------------------------
// TEXTO
// --------------------------------------------------------
Serial.print(
"Texto: "
);
for (
uint8_t i = 0;
i < dataLength;
i++
)
{
if (
data[i] >= 32 &&
data[i] <= 126
)
{
Serial.write(
data[i]
);
}
else
{
Serial.print(
"."
);
}
}
Serial.println();
// --------------------------------------------------------
// CRC
// --------------------------------------------------------
uint16_t receivedCRC =
packet[
5 + dataLength
] |
(
(uint16_t)
packet[
6 + dataLength
]
<< 8
);
uint16_t calculatedCRC =
crc16(
data,
dataLength
);
bool valid =
receivedCRC ==
calculatedCRC;
Serial.print(
"CRC recibido: 0x"
);
Serial.println(
receivedCRC,
HEX
);
Serial.print(
"CRC calculado: 0x"
);
Serial.println(
calculatedCRC,
HEX
);
if (valid)
{
Serial.println(
"INTEGRIDAD: OK"
);
}
else
{
Serial.println(
"INTEGRIDAD: ERROR"
);
}
// --------------------------------------------------------
// RADIO
// --------------------------------------------------------
int rssi =
LoRa.packetRssi();
float snr =
LoRa.packetSnr();
Serial.print(
"RSSI: "
);
Serial.print(
rssi
);
Serial.println(
" dBm"
);
Serial.print(
"SNR: "
);
Serial.print(
snr
);
Serial.println(
" dB"
);
// --------------------------------------------------------
// OLED
// --------------------------------------------------------
showOLED(
counter,
dataLength,
rssi,
snr,
valid
);
}
9. Subir el emisor
Conecta la primera placa al Mac.
Comprueba los puertos:
pio device list
Por ejemplo:
/dev/cu.SLAB_USBtoUART
o:
/dev/cu.usbserial-0001
Entra en el proyecto del emisor:
cd esp32-sx1276-emisor
Compila:
pio run
Sube el programa:
pio run -t upload
Y abre el monitor:
pio device monitor -b 115200
10. Subir el receptor
Desconecta la primera placa y conecta la segunda.
Entra en:
cd esp32-sx1276-receptor
Compila:
pio run
Sube:
pio run -t upload
Monitor:
pio device monitor -b 115200
Puedes mantener los dos monitores abiertos en dos ventanas de Terminal.
11. Resultado
En el emisor aparecerá:
ESP32 + SX1276
TRANSMISOR
SX1276 inicializado
Frecuencia: 868 MHz
Emisor preparado
==============================
TRANSMITIENDO
==============================
Paquete: 0
Datos: 48 4F 4C 41 00 FF 01 02 03
Bytes totales: 16
Transmision: OK
Después:
Paquete: 1
Datos: 48 4F 4C 41 00 FF 01 02 03
Y en el receptor:
ESP32 + SX1276
RECEPTOR
SX1276 inicializado
Frecuencia: 868 MHz
Esperando paquetes...
==============================
PAQUETE RECIBIDO
==============================
RAW:
00 00 00 00 09 48 4F 4C 41 00 FF 01 02 03 XX XX
Paquete: 0
Longitud: 9
Datos HEX:
48 4F 4C 41 00 FF 01 02 03
Texto:
HOLA.....
CRC recibido: 0xXXXX
CRC calculado: 0xXXXX
INTEGRIDAD: OK
RSSI: -52 dBm
SNR: 9.25 dB
La pantalla OLED del receptor mostrará algo parecido a:
LORA RECEPTOR
PKT: 0
BYTES: 9
RSSI: -52 dBm
CRC: OK
12. ¿Qué has conseguido realmente?
Ahora ya no estamos haciendo simplemente:
ESP32 → "Paquete: 42" → ESP32
Sino:
ESP32
│
│ datos binarios
▼
SX1276
│
│ radio sub-GHz
▼
SX1276
│
▼
ESP32
El payload puede contener cualquier secuencia de bytes:
00 FF 7A 12 03 C8 44 00 91
sin necesidad de que sean texto.
Además, cada paquete lleva:
CONTADOR
LONGITUD
PAYLOAD
CRC
y el receptor obtiene:
✓ paquete recibido
✓ tamaño
✓ datos
✓ CRC
✓ RSSI
✓ SNR
La propia documentación de Heltec indica que para comunicación nodo-a-nodo ambos dispositivos deben coincidir en frecuencia, ancho de banda, coding rate, preámbulo, sync word, inversión IQ y spreading factor; por eso he fijado explícitamente esos parámetros en los dos programas.