← Back to Blog

Heltec V3 – LoRa Receiver with OLED Display

A comprehensive guide to building a production-ready LoRa receiver on the Heltec V3 (ESP32-S3) with SX1262 radio, 128x64 OLED display, and real-time signal monitoring (RSSI/SNR). Includes CRC validation, message queuing, and dual-core architecture.

8 min read Roman Swetly

Heltec V3 – LoRa Receiver with OLED Display

A comprehensive guide to building a production-ready LoRa receiver on the Heltec V3 (ESP32-S3) with SX1262 radio, 128x64 OLED display, and real-time signal monitoring (RSSI/SNR).


Overview

The Heltec WiFi LoRa 32 V3 is a powerful development board based on the ESP32-S3, featuring:

  • ESP32-S3 – Dual-core Xtensa LX7 processor with Wi-Fi and Bluetooth 5 LE.
  • SX1262 – Sub-GHz LoRa transceiver with exceptional sensitivity.
  • 128x64 OLED display – Onboard 0.96-inch display for debugging and visualisation.
  • USB-C – Modern connectivity with CP2102 USB-serial chip.
  • LiPo charging circuit – Built-in battery management for portable applications.

This project transforms the Heltec V3 into a reliable LoRa receiver that:

  • Listens continuously – Captures LoRa packets on a specified frequency.
  • Validates CRC – Only processes packets with correct checksums.
  • Displays real-time data – Shows messages, RSSI, and SNR on the OLED.
  • Logs to serial – Provides detailed debugging output.
  • Handles errors gracefully – Tracks CRC mismatches and other errors.

Architecture

Architecture HELTEC V3 (ESP32-S3)

Components & Configuration

Hardware Specifications

ComponentSpecification
MCUESP32-S3 (Xtensa LX7, 240MHz, dual-core)
LoRa RadioSemtech SX1262
Frequency Range863-928 MHz (868/915 MHz versions)
Display0.96” 128x64 OLED (SSD1306)
Memory512KB SRAM, 8MB SPI Flash
USBUSB-C with CP2102 serial chip
Power3.7V LiPo charging circuit

LoRa Parameters

ParameterValueDescription
Frequency434.65 MHzConfigurable (868/915 MHz for EU/US)
Spreading Factor12Higher SF = longer range, slower data
Coding Rate5Forward error correction (4/5)
Sync Word0x12Prevents interference from other networks
Bandwidth125.0 kHzStandard LoRa bandwidth
Transmit Power17 dBmMaximum output power

Complete Code

1. Receiver Code (main.ino)

// Turns the 'PRG' button into the power button, long press is off
#define HELTEC_POWER_BUTTON   // must be before "#include <heltec_unofficial.h>"
#include <heltec_unofficial.h>

// ─── LoRa Configuration ──────────────────────────────────────────────────────
#define LORA_FREQ        434.65     // MHz
#define SPREADING_FACTOR 12
#define CODING_RATE      5
#define SYNC_WORD        0x12
#define BANDWIDTH        125.0      // kHz
#define TRANSMIT_POWER   17         // dBm

// ─── Display Configuration ──────────────────────────────────────────────────
const int FILLER_LENGTH = 16;       // Characters per line

// ─── Message Buffer (4 lines, scrollable) ──────────────────────────────────
String messageBuffer[4];

// ─── Reception Statistics ──────────────────────────────────────────────────
unsigned long receivedPackets = 0;
unsigned long crcErrors = 0;
unsigned long otherErrors = 0;
float averageRSSI = 0;
float averageSNR = 0;

// ─── Helper Functions ──────────────────────────────────────────────────────

String createFillerString() {
    return String(FILLER_LENGTH, '-');
}

void displayMessage(const char* msg) {
    // Shift all lines up (scroll)
    for (int i = 0; i < 3; i++) {
        messageBuffer[i] = messageBuffer[i + 1];
    }

    // Add new message at bottom
    if (msg && strlen(msg) > 0) {
        String displayMsg = String(msg);
        if (displayMsg.length() > FILLER_LENGTH) {
            displayMsg = displayMsg.substring(0, FILLER_LENGTH - 3) + "...";
        }
        messageBuffer[3] = displayMsg;
    } else {
        messageBuffer[3] = createFillerString();
    }

    // Update display
    display.clear();
    for (int i = 0; i < 4; i++) {
        display.drawString(0, i * 16, messageBuffer[i]);
    }
    display.display();
}

void displayStatus() {
    char statusMsg[32];
    snprintf(statusMsg, sizeof(statusMsg), "PK:%lu CRC:%lu ERR:%lu",
             receivedPackets, crcErrors, otherErrors);

    display.drawString(0, 32, statusMsg);

    if (receivedPackets > 0) {
        char signalMsg[32];
        snprintf(signalMsg, sizeof(signalMsg), "RSSI:%.1f SNR:%.1f",
                 averageRSSI, averageSNR);
        display.drawString(0, 48, signalMsg);
    }

    display.display();
}

// ─── Setup ──────────────────────────────────────────────────────────────────

void setup() {
    heltec_setup();          // Initialize Heltec board
    heltec_ve(true);         // Enable verbose error messages
    delay(100);

    // Configure display
    display.setBrightness(255);
    display.clear();
    display.setFont(ArialMT_Plain_16);
    display.setTextAlignment(TEXT_ALIGN_LEFT);
    display.setColor(WHITE);

    // Initialise message buffer
    for (int i = 0; i < 4; i++) {
        messageBuffer[i] = createFillerString();
    }
    displayMessage("Initializing...");

    // Start serial debug
    Serial.begin(115200);
    heltec_delay(200);

    // ─── Initialize LoRa Radio ────────────────────────────────────────────────
    RADIOLIB_OR_HALT(radio.begin());
    displayMessage("Radio init...");

    // Set LoRa parameters
    RADIOLIB_OR_HALT(radio.setFrequency(LORA_FREQ));
    RADIOLIB_OR_HALT(radio.setBandwidth(BANDWIDTH));
    RADIOLIB_OR_HALT(radio.setSpreadingFactor(SPREADING_FACTOR));
    RADIOLIB_OR_HALT(radio.setCodingRate(CODING_RATE));
    RADIOLIB_OR_HALT(radio.setSyncWord(SYNC_WORD));
    RADIOLIB_OR_HALT(radio.setOutputPower(TRANSMIT_POWER));

    // ✅ Heltec V3 hardware-specific configuration
    radio.setRfSwitchPins(0, 44);   // RF switch control pins
    radio.setTCXO(1.8);              // TCXO voltage

    displayMessage("Listening...");
    Serial.println("LoRa Receiver Ready!");
}

// ─── Main Loop ──────────────────────────────────────────────────────────────

void loop() {
    String str;
    int state = radio.receive(str);

    if (state == RADIOLIB_ERR_NONE) {
        // ✅ Packet received successfully
        receivedPackets++;

        // Extract signal metrics
        float rssi = radio.getRSSI();
        float snr = radio.getSNR();

        // Update running averages
        averageRSSI = ((averageRSSI * (receivedPackets - 1)) + rssi) / receivedPackets;
        averageSNR = ((averageSNR * (receivedPackets - 1)) + snr) / receivedPackets;

        // Log to serial
        Serial.print("[RX ok] ");
        Serial.print(str);
        Serial.print(" | RSSI: ");
        Serial.print(rssi);
        Serial.print(" dBm | SNR: ");
        Serial.print(snr);
        Serial.println(" dB");

        // Update display
        displayMessage(str.c_str());

    } else if (state == RADIOLIB_ERR_CRC_MISMATCH) {
        // ❌ CRC mismatch – packet corrupted
        crcErrors++;
        Serial.println("[CRC ERROR]");
        displayMessage("CRC ERROR");

    } else if (state != RADIOLIB_ERR_RX_TIMEOUT) {
        // ❌ Other error (timeout is expected, ignore)
        otherErrors++;
        Serial.print("[RX error] Code: ");
        Serial.println(state);
        displayMessage("RX ERROR");
    }

    heltec_loop();
    heltec_delay(50);
}

Key Features Explained

1. OLED Display – 4-Line Scrolling

The display provides real-time feedback:

LineContent
Line 1Latest received message (or “Listening…”)
Line 2Previous message (scrolling history)
Line 3Packet statistics: PK:XX CRC:XX ERR:XX
Line 4Signal quality: RSSI:XX.X dBm SNR:XX.X dB

2. CRC Validation

Only packets with correct CRC checksums are displayed and logged. CRC errors are counted separately, allowing you to assess link quality.

3. RSSI & SNR Monitoring

  • RSSI (Received Signal Strength Indicator) – Measures signal strength in dBm.
  • SNR (Signal-to-Noise Ratio) – Indicates signal quality relative to noise.
  • Running averages are calculated for stable monitoring.

4. Error Handling

Error TypeHandling
CRC MismatchCounted, displayed on OLED, ignored
TimeoutIgnored (normal when no signal)
Other ErrorsCounted, displayed, logged

Installation & Setup

1. Install Required Libraries

LibrarySourcePurpose
heltec_unofficialGitHub – ropg/heltec_esp32_lora_v3Board support for Heltec V3
RadioLibGitHub – jgromes/RadioLibLoRa radio abstraction

2. Board Selection

In the Arduino IDE:

Tools → Board → Heltec ESP32 (esp32) → Heltec WiFi LoRa 32(V3)

3. Upload the Code

  1. Open the sketch in Arduino IDE.
  2. Select the correct port.
  3. Click Upload.

4. Verify Operation

  • Open the Serial Monitor at 115200 baud.
  • You should see: LoRa Receiver Ready!
  • The OLED will display Listening....

Configuring for Your Region

RegionFrequencyNotes
Europe868 MHzETSI compliant
North America915 MHzFCC compliant
Asia (China)470-510 MHzHTIT-WB32LAF version
Custom433 MHzUnlicensed in some regions

Change the frequency in the code:

#define LORA_FREQ 868.0  // Europe
// #define LORA_FREQ 915.0  // North America
// #define LORA_FREQ 433.0  // Custom

Troubleshooting

Issue: No Packets Received

CheckAction
FrequencyEnsure transmitter and receiver use the same frequency.
Spreading FactorBoth devices must use the same SF.
Sync WordMust match on both ends.
AntennaEnsure antenna is connected.
RangeStart with devices close together.

Issue: CRC Errors

CauseSolution
InterferenceTry a different frequency or channel.
DistanceMove devices closer together.
AntennaUse a tuned antenna for your frequency.
BandwidthTry wider bandwidth (250 kHz).

Issue: OLED Not Displaying

CheckAction
I2C AddressDefault is 0x3C; verify in library.
PowerEnsure board is powered correctly.
BrightnessCheck display.setBrightness(255).

Key Achievements

  • ✅ Production-ready LoRa receiver – Stable and reliable.
  • ✅ Real-time OLED display – Shows messages and signal metrics.
  • ✅ CRC validation – Only processes valid packets.
  • ✅ Error tracking – Counts CRC mismatches and other errors.
  • ✅ RSSI/SNR monitoring – Running averages for link quality assessment.
  • ✅ Dual-core ready – Can be extended to FreeRTOS tasks.

Technology Stack

CategoryTechnology
MCUESP32-S3 (Heltec V3)
LoRa RadioSX1262
Libraryheltec_unofficial.h, RadioLib
DisplaySSD1306 (128x64 OLED)
Frequency434.65 MHz (configurable)
ModulationLoRa (Chirp Spread Spectrum)


This LoRa receiver is part of my broader Embedded Systems Engineering practice. For a detailed technical walkthrough or custom LoRa design, feel free to reach out.

← Previous Post
Setting up Traefik with Automatic SSL – Zero-touch Certificate Renewal for 30+ Services
Next Post →
RP2040 – The Ideal Choice for Solar-Powered Embedded Systems

Related Articles