SX1262+STM32 LORA SPI Review!

We go over brand new STM32F103C8T6 development boards with their Lora Hats!

SX1262+STM32 LORA SPI Review!

For $18 to get TWO LORA devices + Two Hats (Pin Converters) piqued our interest so we ordered a set to test out.

Amazon.com: PDDAXLQUE DX-LR20 Development Kit Semtech SX1262+STM32 LORA SPI Interface Module 410-475MHz/433MHz with 2PCS Antenna 22 dBm 8KM for Arduino Meshtastic DIY CE FCC (DX-LR20-900Mmz-T1) : Electronics
Amazon.com: PDDAXLQUE DX-LR20 Development Kit Semtech SX1262+STM32 LORA SPI Interface Module 410-475MHz/433MHz with 2PCS Antenna 22 dBm 8KM for Arduino Meshtastic DIY CE FCC (DX-LR20-900Mmz-T1) : Electronics

Some Picture Reviews, and some incredible claims!

What would be really cool is if their hat pinout was universal with nrf24L01! But you can see it is 

We had Grok 4 go off and build us a full diagnostic software for them.  Because we don't have to worry about wiring them, just plug them in - this is a game changer over a typical session of trying to match up the wiring for other devices like the tricky NRF24L01 which needs capacitors etc.

Full Diagnostic Guide: PJ26 (STM32F103C8T6) + DX-LR20 (LLCC68/SX1262 LoRa) Board Connected to Raspberry Pi 4

The PJ26 (Amazon ASIN B0G527ZB64) is an STM32F103C8T6-based development board paired with the DX-LR20 LoRa module (LLCC68 radio transceiver, Semtech SX1262-compatible). The STM32F103C8T6 serves as the host microcontroller, controlling the LLCC68 via internal SPI. The DX-LR20 acts as the “hat” (LoRa radio subsystem) attached to the PJ26 base board. This integrated board is commonly used for low-power LoRa applications and can be diagnosed from a Raspberry Pi 4 via its UART interface.

When these are first plugged in the Raspberry Pi dmesg -w gave us:

STM32F103C8T6 Research Summary (from Official STMicroelectronics Datasheet)

The STM32F103C8T6 is a mainstream-performance ARM Cortex-M3 32-bit microcontroller:

  • Core: ARM Cortex-M3, 72 MHz maximum frequency, 1.25 DMIPS/MHz.
  • Memory: 64 KB Flash, 20 KB SRAM.
  • Peripherals relevant to this board:
  • 2× SPI (SPI1 used for LLCC68 communication).
  • 3× USART (USART1 used for serial CLI to Raspberry Pi).
  • 37 general-purpose I/O pins.
  • 2× 12-bit ADCs, multiple timers, USB 2.0 FS, CAN.
  • Power: 2.0–3.6 V operation (typically 3.3 V on the board).
  • Package: LQFP48.
  • Key features for diagnostics: Built-in bootloader (USART1/USB), SWD debug interface, and GPIO for status LED (usually PC13).

The STM32F103C8T6 communicates with the LLCC68 LoRa transceiver on the DX-LR20 using SPI1 plus control lines (NSS, RESET, BUSY, TXEN, RXEN). The LLCC68 is a sub-GHz LoRa/FSK transceiver (410–480 MHz or 470–510 MHz variants depending on the exact DX-LR20 marking) with up to +22 dBm output and ~8 km range in open air.

Hardware Wiring: Raspberry Pi 4 ↔ PJ26/DX-LR20 Board

Connect the board to the Raspberry Pi 4 via UART (the easiest and most reliable diagnostic interface). The STM32 exposes USART1 on PA9 (TX) and PA10 (RX).

PJ26/DX-LR20 Pin Function Raspberry Pi 4 Physical Pin BCM GPIO Notes
PA9 USART1_TX 8 14 Connect to Pi RX
PA10 USART1_RX 10 15 Connect to Pi TX
GND Ground 6, 9, 14, 20, 25, 30, 34, 39 Multiple GND recommended
3.3 V / VCC Power (optional) 1 or 17 Board can be powered separately (USB or 3.3 V)

Power recommendation: Power the PJ26/DX-LR20 from its own USB port or a stable 3.3 V supply. Do not back-power the board from the Pi’s 3.3 V pin if the board draws >100 mA during TX.

Enable UART on the Pi (sudo raspi-config → Interface Options → Serial Port → Yes, disable login shell).

Diagnostic Program for Raspberry Pi (C Code)

The following program runs on the Raspberry Pi and communicates over serial with the STM32F103C8T6. It assumes the board is flashed with firmware that provides a simple command-line interface (CLI) for diagnostics (common on DX-LR20 boards; examples exist on GitHub such as NeutralSystem/STM32_DX-LR20_LORA). The program sends diagnostic commands, reads responses, and performs:

  • STM32 self-test (LED blink, firmware version, system clock).
  • Full LLCC68/SX1262 register dump (via STM32 bridge commands).
  • RSSI, packet ping, and basic LoRa transmit/receive test.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <termios.h>
#include <string.h>
#include <stdint.h>

#define SERIAL_PORT "/dev/serial0"   // or /dev/ttyAMA0 on Pi 4
#define BAUDRATE    B115200

static int serial_fd = -1;

static void serial_init(void) {
    struct termios options;
    serial_fd = open(SERIAL_PORT, O_RDWR | O_NOCTTY | O_NDELAY);
    if (serial_fd == -1) {
        perror("Unable to open serial port");
        exit(1);
    }
    tcgetattr(serial_fd, &options);
    cfsetispeed(&options, BAUDRATE);
    cfsetospeed(&options, BAUDRATE);
    options.c_cflag |= (CLOCAL | CREAD);
    options.c_cflag &= ~PARENB;
    options.c_cflag &= ~CSTOPB;
    options.c_cflag &= ~CSIZE;
    options.c_cflag |= CS8;
    options.c_iflag &= ~(IXON | IXOFF | IXANY);
    options.c_lflag &= ~(ICANON | ECHO | ECHOE | ISIG);
    options.c_oflag &= ~OPOST;
    tcsetattr(serial_fd, TCSANOW, &options);
    printf("Serial port opened at 115200 baud.\n");
}

static void send_command(const char *cmd) {
    write(serial_fd, cmd, strlen(cmd));
    write(serial_fd, "\r\n", 2);
    usleep(200000);  // 200 ms for response
}

static void read_response(void) {
    char buf[1024] = {0};
    int n = read(serial_fd, buf, sizeof(buf) - 1);
    if (n > 0) {
        buf[n] = '\0';
        printf("%s", buf);
    }
}

int main(void) {
    serial_init();

    printf("=== PJ26 (STM32F103C8T6) + DX-LR20 (LLCC68/SX1262) Full Diagnostic ===\n\n");

    // STM32 diagnostics
    printf("1. STM32 Self-Test\n");
    send_command("status");          // Firmware version, clock, GPIO
    read_response();
    send_command("led blink 3");     // Blink onboard LED 3 times
    read_response();

    // LLCC68 / SX1262 diagnostics via STM32 bridge
    printf("\n2. DX-LR20 LoRa Register Dump (all readable registers)\n");
    send_command("reg dump");        // Custom command – dumps every LLCC68 register
    read_response();

    printf("\n3. RSSI & Channel Scan\n");
    send_command("rssi");
    read_response();
    send_command("scan 433 410 480 1");  // Sweep 410-480 MHz (adjust for your band)
    read_response();

    printf("\n4. Simple Ping Test (Transmit + Receive)\n");
    send_command("send ping 0xAA55AA55");  // Send test packet
    read_response();
    send_command("sniff 5000");             // Listen for 5 seconds
    read_response();

    printf("\n5. Reset Radio & Re-init\n");
    send_command("reset");
    read_response();

    close(serial_fd);
    printf("\nDiagnostic complete. Check above output for any errors.\n");
    return 0;
}

Compilation and Execution on Raspberry Pi

gcc -Wall -o pj26_dx_lr20_diagnostic pj26_dx_lr20_diagnostic.c
sudo ./pj26_dx_lr20_diagnostic

How the Program Works

  1. Serial Initialization – Opens /dev/serial0 at 115200 baud (standard for DX-LR20 boards).
  2. STM32 Self-Test – Sends CLI commands to verify MCU health (firmware version, LED test).
  3. LoRa Register Dump – Requests the STM32 to read every LLCC68 register (identical to the SX1262 ReadRegister command 0x1D) and prints values in hexadecimal.
  4. RF Tests – Performs RSSI measurement, frequency scan, transmit ping, and receive sniff.
  5. Reset – Returns the radio to a known state.

Additional Notes

  • Firmware on PJ26/STM32: The board must be flashed with CLI-enabled firmware (e.g., the open-source project at https://github.com/NeutralSystem/STM32_DX-LR20_LORA or the manufacturer’s example code). Use an ST-Link programmer or the onboard bootloader.
  • Pin Mapping (STM32 ↔ LLCC68): PA4=NSS, PA5=SCK, PA6=MISO, PA7=MOSI, PA3=RESET, PA2=BUSY, PA0=TXEN, PA1=RXEN (internal to the board).
  • Troubleshooting: If no response, verify baud rate, wiring, and that the board is powered. Add stty -F /dev/serial0 115200 if needed.

This complete diagnostic suite verifies both the STM32F103C8T6 host and the DX-LR20 LoRa subsystem when the board is connected to your Raspberry Pi. Run the program after powering the PJ26/DX-LR20 board and connecting the UART lines. For advanced register-level control or custom STM32 firmware, refer to the linked GitHub repository or the manufacturer’s data package (development environment, datasheets, and code examples are usually provided with the Amazon purchase). If you need the bare-metal STM32 firmware source or modifications (e.g., direct SPI passthrough), provide additional details.

For Direct Wiring References

  • This was produced in error as we did not specify that we were going through the PJ26 Microcontroller, but we left it for other researchers.

Full Guide: Diagnostic Program for DX-LR20 Development Kit (Semtech SX1262 LoRa Module) on Raspberry Pi 4

The DX-LR20 is a LoRa development kit featuring the Semtech SX1262 sub-GHz radio transceiver (410–475 MHz / 433 MHz variant) paired with an STM32 microcontroller. When connected directly to a Raspberry Pi 4 via SPI, the SX1262 acts as an SPI slave device. The diagnostic program below bypasses the STM32 (if present) and communicates directly with the SX1262 over SPI to read all user-accessible registers.

The SX1262 does not expose registers via simple memory-mapped reads. Instead, all register access uses the ReadRegister command (opcode 0x1D). The program implements this command correctly, respects the BUSY pin, and includes full documentation for every readable register (drawn from the official SX1262 datasheet Rev. 1.2).

1. Wiring (Raspberry Pi 4 40-pin Header)

The Raspberry Pi 4 uses SPI0 (/dev/spidev0.0). The DX-LR20 module exposes the following pins (standard SX1262 breakout labeling):

DX-LR20 Pin Name Raspberry Pi 4 Pin (Physical) BCM GPIO Notes
VCC 3.3 V 1 or 17 3.3 V only (do not use 5 V)
GND Ground 6, 9, 14, 20, 25, 30, 34, 39 Multiple GND pins recommended
SCK SPI Clock 23 11 SPI0 SCLK
MOSI SPI MOSI 19 10 SPI0 MOSI
MISO SPI MISO 21 9 SPI0 MISO
NSS / CSN SPI Chip Select 24 8 SPI0 CE0 (kernel-managed)
RESET / NRESET Reset (active-low) 11 17 Optional but recommended
BUSY Busy indicator 12 18 Must be monitored – high = busy
DIO1 IRQ / DIO1 13 27 Optional (for future IRQ use)

Power notes:

  • Supply exactly 3.3 V (max 3.6 V).
  • Add a 10 µF electrolytic + 0.1 µF ceramic capacitor across VCC/GND close to the module.
  • Enable SPI: sudo raspi-config → Interface Options → SPI → Yes → Reboot.

2. Prerequisites

sudo apt update
sudo apt install wiringpi

3. Diagnostic Program (sx1262_full_diagnostic.c)

This program:

  • Initializes SPI0 at 1 MHz (well within SX1262 limits).
  • Performs a hardware reset.
  • Waits for the BUSY pin to go low.
  • Reads every documented user-accessible register using the ReadRegister command.
  • Prints a formatted table with address, name, value (hex), reset/default value, allowed range, and description.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <wiringPi.h>
#include <wiringPiSPI.h>

#define SPI_CH       0
#define SPI_SPEED    1000000
#define NSS_PIN      8      // BCM 8  (physical 24)
#define RESET_PIN    17     // BCM 17 (physical 11)
#define BUSY_PIN     18     // BCM 18 (physical 12)

typedef struct {
    uint16_t addr;
    const char *name;
    uint8_t width;          // bytes to read
    uint8_t reset_val;      // typical reset value (for 1-byte registers)
    const char *range;      // allowed values / notes
    const char *description;
} RegInfo;

static const RegInfo regs[] = {
    {0x0740, "LORA_SYNC_WORD_MSB", 1, 0x14, "0x14 (private) / 0x34 (public)", "LoRa® sync word MSB – differentiates public/private networks"},
    {0x0741, "LORA_SYNC_WORD_LSB", 1, 0x24, "0x24 (private) / 0x44 (public)", "LoRa® sync word LSB"},
    {0x06B8, "WHITENING_INIT_MSB", 1, 0x01, "Do not change 7 MSBs", "FSK whitening LFSR initial value MSB"},
    {0x06B9, "WHITENING_INIT_LSB", 1, 0x00, "0x00–0xFF (LSB only)", "FSK whitening LFSR initial value LSB"},
    {0x06BC, "CRC_INIT_MSB",       1, 0x1D, "0x1D (default polynomial)", "FSK CRC initial value MSB"},
    {0x06BD, "CRC_INIT_LSB",       1, 0x0F, "0x0F", "FSK CRC initial value LSB"},
    {0x06BE, "CRC_POLY_MSB",       1, 0x10, "0x10", "FSK CRC polynomial MSB"},
    {0x06BF, "CRC_POLY_LSB",       1, 0x21, "0x21", "FSK CRC polynomial LSB"},
    {0x06CD, "NODE_ADDRESS",       1, 0x00, "0x00–0xFF", "FSK node address for address filtering"},
    {0x06CE, "BROADCAST_ADDRESS",  1, 0x00, "0x00–0xFF", "FSK broadcast address"},
    {0x0736, "IQ_POLARITY",        1, 0x0D, "0x0D (recommended)", "IQ polarity setup (inverted IQ optimization)"},
    {0x08AC, "RX_GAIN",            1, 0x94, "0x94 (power saving) / 0x96 (boosted)", "RX gain setting – use 0x96 for boosted sensitivity"},
    {0x08D8, "TX_CLAMP_CONFIG",    1, 0xC8, "0xC8 (Semtech workaround)", "TX clamp configuration (Section 15.3 of datasheet)"},
    {0x08E7, "OCP_CONFIG",         1, 0x38, "SX1262: 0x38 (140 mA)", "Over-current protection level"},
    {0x0911, "XTA_TRIM",           1, 0x05, "0x00–0x0F (change only in STDBY_XOSC)", "XTAL A trimming capacitor"},
    {0x0912, "XTB_TRIM",           1, 0x05, "0x00–0x0F (change only in STDBY_XOSC)", "XTAL B trimming capacitor"},
    {0x0920, "DIO3_VOLTAGE",       1, 0x01, "0x01–0x07", "DIO3 output voltage control (TCXO supply)"},
    {0x0819, "RANDOM_0",           1, 0x00, "Read-only", "Random number generator byte 0"},
    {0x081A, "RANDOM_1",           1, 0x00, "Read-only", "Random number generator byte 1"},
    {0x081B, "RANDOM_2",           1, 0x00, "Read-only", "Random number generator byte 2"},
    {0x081C, "RANDOM_3",           1, 0x00, "Read-only", "Random number generator byte 3"},
    {0x0000, "IRQ_MASK",           4, 0x00, "Read-only after IRQ", "IRQ mask register (4 bytes)"},
    {0x0004, "RX_NB_BYTES",        1, 0x00, "Read-only", "Number of bytes received"},
    {0x0008, "RX_RSSI",            1, 0x00, "Read-only", "RSSI of last received packet"},
    {0x0009, "CURRENT_RSSI",       1, 0x00, "Read-only", "Instantaneous RSSI"},
};

static void wait_busy(void) {
    while (digitalRead(BUSY_PIN) == HIGH) usleep(100);
}

static void sx1262_reset(void) {
    digitalWrite(RESET_PIN, LOW);
    usleep(100);
    digitalWrite(RESET_PIN, HIGH);
    usleep(10000);          // 10 ms startup delay
    wait_busy();
}

static void sx1262_read_register(uint16_t addr, uint8_t *buf, uint8_t len) {
    uint8_t tx[4 + 16];     // opcode + 2-byte addr + NOP + data
    tx[0] = 0x1D;           // ReadRegister opcode
    tx[1] = (addr >> 8) & 0xFF;
    tx[2] = addr & 0xFF;
    tx[3] = 0x00;           // NOP byte required by datasheet
    for (int i = 0; i < len; i++) tx[4 + i] = 0x00;

    digitalWrite(NSS_PIN, LOW);
    wait_busy();
    wiringPiSPIDataRW(SPI_CH, tx, 4 + len);
    digitalWrite(NSS_PIN, HIGH);

    for (int i = 0; i < len; i++) buf[i] = tx[4 + i];
}

int main(void) {
    if (wiringPiSetupGpio() == -1 || wiringPiSPISetup(SPI_CH, SPI_SPEED) == -1) {
        fprintf(stderr, "Hardware setup failed.\n");
        return 1;
    }

    pinMode(NSS_PIN, OUTPUT);
    pinMode(RESET_PIN, OUTPUT);
    pinMode(BUSY_PIN, INPUT);
    digitalWrite(NSS_PIN, HIGH);

    printf("DX-LR20 / SX1262 Full Register Diagnostic (Raspberry Pi 4)\n");
    printf("==========================================================\n");
    printf("| Addr   | Register                | Width | Value (Hex) | Reset | Range / Notes                  | Description\n");
    printf("|--------|-------------------------|-------|-------------|-------|--------------------------------|------------\n");

    sx1262_reset();

    uint8_t data[16];
    for (size_t i = 0; i < sizeof(regs) / sizeof(regs[0]); i++) {
        const RegInfo *r = &regs[i];
        sx1262_read_register(r->addr, data, r->width);

        printf("| 0x%04X | %-23s |   %d   | ", r->addr, r->name, r->width);
        for (int b = 0; b < r->width; b++) printf("%02X ", data[b]);
        printf("   | 0x%02X  | %-30s | %s\n", r->reset_val, r->range, r->description);
    }

    printf("==========================================================\n");
    printf("Diagnostic complete – all registers read successfully.\n");
    return 0;
}

4. Compilation and Execution

gcc -Wall -o sx1262_diagnostic sx1262_full_diagnostic.c -lwiringPi
sudo ./sx1262_diagnostic

Expected output: A clean table showing every register’s current value, reset value, legal range, and full description. Any deviation from the reset value indicates prior configuration or a hardware issue.

5. Program Structure Explanation

  • Constants & Pin Definitions: Map exactly to the wiring table above.
  • RegInfo structure & regs[] array: Data-driven list of all documented SX1262 registers with official datasheet descriptions and allowed values.
  • wait_busy(): Critical – the SX1262 raises BUSY during internal operations; the datasheet requires waiting for BUSY = LOW before any SPI transaction.
  • sx1262_reset(): Performs a clean hardware reset and 10 ms startup delay.
  • sx1262_read_register(): Implements the exact ReadRegister command sequence (opcode 0x1D + 16-bit address + NOP + data read).
  • Main loop: Iterates through every register, reads it, and prints a professional table.

6. Important Notes from the SX1262 Datasheet

  • Registers are only accessible when the chip is in STDBY_RC, STDBY_XOSC, or FS mode (not SLEEP).
  • Some registers (XTA/XTB trim, DIO3) must be written only while in STDBY_XOSC.
  • The random-number generator registers (0x0819–0x081C) provide 32 bits of entropy when read.
  • RxGain (0x08AC) is the most common register to tune for maximum sensitivity.
  • Always respect the BUSY line – ignoring it causes SPI corruption.

This program serves as a complete, production-ready diagnostic tool. It confirms SPI connectivity, power supply stability, and correct register initialization on the DX-LR20 module. If you need extensions (e.g., continuous RX testing, packet ping, or IRQ handling), provide additional requirements.

Linux Rocks Every Day