The best default setup for a Raspberry Pi LoRa node is the Raspberry Pi 4 Model B paired with the Dragino LoRa/GPS HAT v1.4 (based on the Semtech SX1276 chip), communicating over the hardware SPI0 bus. This combination gives you a robust, stackable physical layer without the breadboard spaghetti that plagues raw SPI module wiring. Whether you are building a remote soil moisture aggregator or a single-channel LoRaWAN gateway prototype, the SX1276 remains the most documented and reliable sub-GHz transceiver for the Pi ecosystem.

This guide gives you the exact pin mapping, the decision framework to pick your hardware, and a complete Python script with the error handling required to survive real-world SPI bus contention.

The Decision Tree: Which Raspberry Pi LoRa Hardware to Pick?

Before buying parts, you need to match the silicon to your use case. The sub-GHz ISM bands (868 MHz in EU/UK, 915 MHz in US/AU) are crowded with different module form factors. Use this decision matrix to lock in your hardware.

Criteria / Use Case Hardware Option Interface Verdict
Need GPS + clean Pi stack for sensor node Dragino LoRa/GPS HAT v1.4 (SX1276) SPI0 DEFAULT PICK for prototyping and custom nodes.
Need multi-channel LoRaWAN Gateway (TTN) RAKwireless RAK2287 (SX1302) USB / SPI Pick this ONLY if building a production gateway. Overkill for simple node-to-node.
SPI bus is occupied (e.g., using an LCD) Ebyte E22-900T22D (SX1262) UART Pick this if you need long-range point-to-point and must free up the SPI0 pins.
Need ultra-low power sleep modes Seeed Studio LoRa-E5 (STM32WLE5) UART (AT Commands) Pick this for battery-powered remote nodes where the Pi acts only as a bridge.
Bench Note: For this build, we are terminating the decision path on the Dragino LoRa/GPS HAT v1.4 (SX1276, 915MHz version). It maps directly to the Pi's 40-pin header, handles the 3.3V logic level translation internally, and keeps your workbench clean.

Parts List and Exact SPI Pin Mapping

The SX1276 is strictly a 3.3V logic device. Feeding 5V into the MISO/MOSI lines will permanently brick the silicon. The Dragino HAT handles this, but if you are verifying connections, use this exact mapping.

Bill of Materials

  • Compute: Raspberry Pi 4 Model B (4GB) running Raspberry Pi OS (Bookworm or Bullseye, 64-bit).
  • Radio: Dragino LoRa/GPS HAT v1.4 (SX1276, 915MHz for US/AU or 868MHz for EU).
  • Antenna: 915MHz SMA dipole antenna (never transmit without an antenna attached; you will burn out the PA).
  • Software: Python 3.9+, adafruit-blinka, and adafruit-circuitpython-rfm9x.

SPI0 Pin Mapping (BCM Numbering)

Pi Physical Pin BCM GPIO HAT Pin Label Function
19GPIO 10MOSIMaster Out Slave In
21GPIO 9MISOMaster In Slave Out
23GPIO 11SCKSerial Clock
24GPIO 8CSChip Select (CE0)
22GPIO 25RSTRadio Reset
29GPIO 5DIO0Interrupt / RX Ready
13.3V3.3VPower (Max 120mA draw during TX)
6GNDGNDCommon Ground

Step-by-Step Wiring and Software Setup

Follow these steps to prep the Raspberry Pi OS for SPI communication. Do not skip the SPI enablement step, or the Python library will fail silently or throw a bus error.

  1. Enable the SPI Bus: Open the terminal and run sudo raspi-config. Navigate to Interface Options > SPI and select Yes. Alternatively, manually add dtparam=spi=on to the bottom of /boot/firmware/config.txt (or /boot/config.txt on older OS versions) and reboot.
  2. Verify SPI Devices: After rebooting, run ls -l /dev/spi*. You must see /dev/spidev0.0 and /dev/spidev0.1. If you don't, your SPI overlay failed to load.
  3. Create a Virtual Environment: Keep your system Python clean. Run python3 -m venv lora-env followed by source lora-env/bin/activate.
  4. Install Dependencies: Install the Adafruit Blinka compatibility layer and the RFM9x driver. Run:
    pip3 install adafruit-blinka adafruit-circuitpython-rfm9x
  5. Attach the Antenna: Screw the SMA antenna onto the HAT. Never power up the TX sequence without the antenna attached.

Complete Python Code with Error Handling

This script targets the Raspberry Pi 4 Model B and the Dragino SX1276 HAT. It initializes the radio, sets the LoRa modulation parameters, and transmits a payload. I have included explicit try/except blocks to catch the exact hardware initialization failures common on the Pi's SPI bus.

import time
import board
import busio
import digitalio
import adafruit_rfm9x

# --- PIN DEFINITIONS (Dragino HAT on Pi 4) ---
# CS maps to SPI0 CE0 (Physical Pin 24, BCM 8)
CS = digitalio.DigitalInOut(board.CE0)
# Reset maps to BCM GPIO 25 (Physical Pin 22)
RESET = digitalio.DigitalInOut(board.D25)

# Initialize the hardware SPI0 bus
spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)

# --- RADIO INITIALIZATION WITH ERROR HANDLING ---
try:
    # 915.0 MHz for US/AU, change to 868.0 for EU/UK
    rfm9x = adafruit_rfm9x.RFM9x(spi, CS, RESET, 915.0)
    
    # Configure LoRa Parameters
    rfm9x.tx_power = 23  # Max power for SX1276 (5 to 23 dBm)
    rfm9x.signal_bandwidth = 125000  # 125 kHz bandwidth
    rfm9x.spreading_factor = 7  # SF7 (Fastest airtime, lowest range)
    rfm9x.coding_rate = 5  # 4/5 coding rate
    rfm9x.preamble_length = 8
    rfm9x.enable_crc = True

    print('RFM9x LoRa Radio Initialized Successfully.')
    print(f'Silicon Version Register: 0x{rfm9x.version:02X}')

except RuntimeError as e:
    print(f'FATAL HARDWARE ERROR: {e}')
    print('Check SPI enablement, CS pin wiring, and 3.3V power.')
    raise

# --- TRANSMISSION LOOP ---
try:
    while True:
        payload = b'Pi4-Node-01: Soil Moisture 42%'
        print(f'Transmitting: {payload.decode("utf-8")}')
        rfm9x.send(payload)
        time.sleep(10) # Respect local duty cycle limits

except KeyboardInterrupt:
    print('\nTransmission halted by user.')
    # Put radio in sleep mode to save power on exit
    rfm9x.sleep()
Understanding the Parameters: Spreading Factor (SF) and Bandwidth (BW) dictate your airtime. SF7 at 125kHz BW yields an airtime of roughly 46ms for a 20-byte payload. If you drop to SF12 for maximum range, that same payload takes over 1,300ms. In the 868MHz EU band, strict 1% duty cycle limits mean an SF12 transmission forces you to wait over two minutes before transmitting again. Use SF7-SF9 for rapid sensor polling.

Debugging: "RuntimeError: Failed to find RFM9x LoRa radio"

If your script crashes on initialization, you will almost certainly see this exact error string:

RuntimeError: Failed to find RFM9x LoRa radio

This is thrown by the Adafruit library when it attempts to read the SX1276 silicon version register (address 0x42) over SPI and receives 0x00 or 0xFF instead of the expected 0x12. Here are the first three things to check, ranked by probability:

  1. SPI is Disabled or Contended (80% of cases): You forgot to enable SPI in raspi-config, or another process (like an LCD screen or an active SPI daemon) has locked /dev/spidev0.0. Run lsmod | grep spi to ensure spidev is loaded, and check lsof /dev/spidev0.0 for locking processes.
  2. Chip Select (CS) Mapping Error (15% of cases): The Dragino HAT uses CE0 (BCM 8). If your code accidentally defines CS = digitalio.DigitalInOut(board.CE1) (BCM 7), the Pi will clock data out, but the SX1276 will ignore it because its CS pin is held high. Verify you are using board.CE0.
  3. 3.3V Rail Sag or Logic Mismatch (5% of cases): The SX1276 draws up to 120mA during peak TX. If your Pi's 3.3V regulator is sagging below 3.0V due to a poor power supply, the radio will brownout during initialization. Measure the 3.3V pin with a multimeter under load. If it reads below 3.1V, upgrade your Pi's USB-C power supply.

For deeper hardware validation, use a logic analyzer on the MISO line. If you see the Pi sending clock pulses (SCK) and CS dropping low, but MISO remains flat at 0V or 3.3V, the SX1276 module is either dead or not seated properly on the header.

Extending and Simplifying the Build

Once you have raw point-to-point LoRa working, you need to decide how this node fits into your broader architecture.

How to Extend (Adding MQTT and Cloud Integration)

To bridge this LoRa node to a dashboard, add the paho-mqtt library. Wrap the receive loop in a threaded MQTT publisher. When the Pi receives a LoRa packet via rfm9x.receive(), parse the byte string and publish it to a local Mosquitto broker topic like sensors/field/node01. From there, Node-RED can ingest the MQTT stream and log it to InfluxDB. This keeps the RF layer isolated from the network layer, preventing Wi-Fi latency spikes from causing SPI buffer overruns.

How to Simplify (The Gateway Route)

If your actual goal is to connect off-the-shelf LoRaWAN sensors (like Milesight or Dragino commercial sensors) to The Things Network (TTN), stop writing custom Python. Custom SX1276 scripts only handle raw LoRa PHY, not the LoRaWAN MAC layer (OTAA joining, ADR, frame counters). Instead, simplify the build by flashing Raspberry Pi OS Lite and installing LoRa Basics Station or the ChirpStack Gateway Bridge. These pre-compiled binaries handle the LoRaWAN protocol overhead and connect directly to TTN via your Pi's Ethernet or Wi-Fi interface.