Building a LoRa gateway Raspberry Pi node bridges the gap between low-power field sensors and cloud dashboards. While commercial concentrators handle eight or more channels simultaneously for production The Things Network (TTN) deployments, a single-channel DIY gateway using the Dragino LoRa/GPS HAT and a Raspberry Pi 4 is the definitive bench tool. It allows you to debug node payloads, test line-of-sight range, and prototype MQTT integrations before committing to expensive multi-channel hardware.
This guide walks through the physical assembly, SPI configuration, and Python-based packet sniffing for a DIY single-channel gateway. We are targeting the Raspberry Pi 4 Model B (4GB) running Raspberry Pi OS (64-bit, Bookworm) paired with the Dragino LoRa/GPS HAT v1.4 (based on the Semtech SX1276 transceiver).
Hardware Spec Sheet & Pin Mapping
Before writing any code, you need to verify your bill of materials and understand how the HAT routes the SX1276 pins to the Raspberry Pi’s 40-pin GPIO header. The SX1276 relies on hardware SPI for high-speed register access and discrete GPIO pins for interrupt routing.
| Component | Exact Variant / Model | Estimated Cost (2026) | Notes |
|---|---|---|---|
| Single Board Computer | Raspberry Pi 4 Model B (4GB RAM) | $55.00 | 2GB works, but 4GB prevents OOM errors when running local ChirpStack. |
| LoRa Transceiver HAT | Dragino LoRa/GPS HAT v1.4 (868/915MHz) | $45.00 | Ensure you buy the correct frequency band for your region (EU868 vs US915). |
| Storage | SanDisk Extreme 32GB microSD (A1/A2) | $12.00 | High endurance rated; prevents filesystem corruption from frequent logging. |
| Antenna | 3dBi SMA Male Dipole (868/915MHz) | $8.00 | Never power the HAT without an antenna attached; you will fry the SX1276 PA. |
| Hardware | M2.5 Brass Standoff Kit (11mm + 15mm) | $6.00 | Prevents the HAT USB port from shorting against the Pi Ethernet jack. |
| SX1276 Pin Function | Raspberry Pi GPIO (BCM) | Physical Pin # | Direction / Purpose |
|---|---|---|---|
| SPI MOSI | GPIO 10 | 19 | Pi to SX1276 (Data In) |
| SPI MISO | GPIO 9 | 21 | SX1276 to Pi (Data Out) |
| SPI SCK | GPIO 11 | 23 | Pi to SX1276 (Clock) |
| SPI NSS (CS) | GPIO 25 | 22 | Pi to SX1276 (Chip Select, Active Low) |
| Reset | GPIO 17 | 11 | Pi to SX1276 (Hardware Reset, Active Low) |
| DIO0 (IRQ) | GPIO 4 | 7 | SX1276 to Pi (RX_DONE / TX_DONE Interrupt) |
Assembly and SPI Configuration
Physical assembly is straightforward, but the Raspberry Pi’s SPI bus is disabled by default. Follow these steps to prep the OS.
- Mount the Standoffs: Thread the 11mm M2.5 brass standoffs into the four mounting holes on the Raspberry Pi 4. Place the Dragino HAT over the 40-pin header and secure it with the remaining standoffs. Ensure the HAT’s micro-USB port clears the Pi’s Ethernet jack.
- Attach the Antenna: Screw the 868/915MHz SMA antenna onto the gold RF connector on the Dragino HAT. Hand-tighten only; do not use pliers.
- Enable SPI: Boot the Pi and open a terminal. Run
sudo raspi-config. Navigate to Interface Options > SPI and select Yes to enable the SPI peripheral. - Verify SPI Devices: Reboot the Pi. After reboot, run
ls -l /dev/spidev*. You must see/dev/spidev0.0and/dev/spidev0.1. If they are missing, the SPI overlay failed to load. - Install Python Dependencies: Install the required libraries for hardware control.
sudo apt update sudo apt install python3-pip python3-dev pip3 install spidev RPi.GPIO
Gateway Python Receiver Script
While production gateways run the Semtech UDP Packet Forwarder (a C-based daemon), a Python script is vastly superior for bench debugging. The script below targets the Raspberry Pi 4 + Dragino HAT variant. It initializes the SX1276, verifies the silicon ID, configures the radio for LoRa RX Continuous mode, and polls the DIO0 interrupt pin to read incoming payloads.
lora_rx_gateway.py and run it with python3 lora_rx_gateway.py. Keep a serial-connected LoRa node (like an ESP32 + SX1276) on your desk to transmit test payloads.
import spidev
import RPi.GPIO as GPIO
import time
import sys
# --- PIN DEFINITIONS (Dragino HAT v1.4 on RPi 4) ---
CS_PIN = 25 # SPI Chip Select
RST_PIN = 17 # SX1276 Hardware Reset
DIO0_PIN = 4 # Interrupt Pin (RX_DONE)
# --- SX1276 REGISTER ADDRESSES ---
REG_FIFO = 0x00
REG_OP_MODE = 0x01
REG_FRF_MSB = 0x06
REG_FRF_MID = 0x07
REG_FRF_LSB = 0x08
REG_PA_CONFIG = 0x09
REG_LORA_MODE = 0x80
REG_MODEM_CONFIG_1 = 0x1D
REG_MODEM_CONFIG_2 = 0x1E
REG_PAYLOAD_LENGTH = 0x22
REG_FIFO_ADDR_PTR = 0x0D
REG_FIFO_RX_CURRENT_ADDR = 0x10
REG_IRQ_FLAGS = 0x12
REG_RX_NB_BYTES = 0x13
REG_VERSION = 0x42
def setup_gpio():
GPIO.setmode(GPIO.BCM)
GPIO.setup(CS_PIN, GPIO.OUT, initial=GPIO.HIGH)
GPIO.setup(RST_PIN, GPIO.OUT, initial=GPIO.HIGH)
GPIO.setup(DIO0_PIN, GPIO.IN, pull_up_down=GPIO.PUD_DOWN)
def reset_sx1276():
GPIO.output(RST_PIN, GPIO.LOW)
time.sleep(0.01)
GPIO.output(RST_PIN, GPIO.HIGH)
time.sleep(0.01)
def write_reg(spi, addr, val):
GPIO.output(CS_PIN, GPIO.LOW)
spi.xfer2([addr | 0x80, val])
GPIO.output(CS_PIN, GPIO.HIGH)
def read_reg(spi, addr):
GPIO.output(CS_PIN, GPIO.LOW)
res = spi.xfer2([addr & 0x7F, 0x00])[1]
GPIO.output(CS_PIN, GPIO.HIGH)
return res
def main():
spi = spidev.SpiDev()
try:
spi.open(0, 0) # Bus 0, CS 0
spi.max_speed_hz = 5000000 # 5MHz is safe for breadboard/HAT traces
spi.mode = 0
except PermissionError as e:
print(f"FATAL: {e}. Run with sudo or add user to 'spi' group.")
sys.exit(1)
except FileNotFoundError as e:
print(f"FATAL: {e}. SPI is not enabled. Run raspi-config.")
sys.exit(1)
setup_gpio()
reset_sx1276()
# Verify Silicon ID
version = read_reg(spi, REG_VERSION)
if version != 0x12:
print(f"RuntimeError: SX1276 RegVersion returned 0x{version:02X}, expected 0x12.")
print("Check SPI wiring and HAT seating.")
spi.close()
GPIO.cleanup()
sys.exit(1)
print("SX1276 detected. Configuring for LoRa RX...")
# Set Sleep mode, then LoRa mode
write_reg(spi, REG_OP_MODE, 0x00)
write_reg(spi, REG_OP_MODE, REG_LORA_MODE | 0x00)
# Set Frequency: 915.0 MHz (US) or adjust for 868.0 MHz (EU)
# Frf = (Freq * 2^19) / 32. 915MHz = 0xE4C000
write_reg(spi, REG_FRF_MSB, 0xE4)
write_reg(spi, REG_FRF_MID, 0xC0)
write_reg(spi, REG_FRF_LSB, 0x00)
# Config: BW=125kHz (0x70), CR=4/5 (0x02), SF=7 (0x70)
write_reg(spi, REG_MODEM_CONFIG_1, 0x72)
write_reg(spi, REG_MODEM_CONFIG_2, 0x70)
# Set RX Continuous mode (0x05)
write_reg(spi, REG_OP_MODE, REG_LORA_MODE | 0x05)
print("Listening for LoRa packets on 915.0 MHz (SF7, BW125)...")
try:
while True:
if GPIO.input(DIO0_PIN) == GPIO.HIGH:
irq_flags = read_reg(spi, REG_IRQ_FLAGS)
write_reg(spi, REG_IRQ_FLAGS, 0xFF) # Clear IRQ flags
if (irq_flags & 0x40) != 0: # RxDone mask
rx_addr = read_reg(spi, REG_FIFO_RX_CURRENT_ADDR)
rx_len = read_reg(spi, REG_RX_NB_BYTES)
write_reg(spi, REG_FIFO_ADDR_PTR, rx_addr)
GPIO.output(CS_PIN, GPIO.LOW)
payload = spi.xfer2([REG_FIFO & 0x7F] + [0x00]*rx_len)[1:]
GPIO.output(CS_PIN, GPIO.HIGH)
try:
decoded = bytes(payload).decode('utf-8')
print(f"[{time.strftime('%H:%M:%S')}] RX Payload ({rx_len}B): {decoded}")
except UnicodeDecodeError:
print(f"[{time.strftime('%H:%M:%S')}] RX Hex: {payload.hex()}")
time.sleep(0.05)
except KeyboardInterrupt:
print("\nShutting down gateway...")
finally:
spi.close()
GPIO.cleanup()
if __name__ == '__main__':
main()
Debugging: Exact Error Strings and Ranked Causes
When bridging Linux SPI drivers with raw silicon registers, things will fail. If your script crashes on startup, identify the exact error string in your terminal and follow the ranked troubleshooting path below.
Error 1: RuntimeError: SX1276 RegVersion returned 0x00, expected 0x12.
The Pi successfully opened the SPI bus, but the SX1276 is returning zeros. This means the radio is unpowered, held in reset, or the MISO line is floating.
- Check the Reset Pin: The Dragino HAT routes GPIO 17 to the SX1276 reset line. If your script doesn't toggle this pin HIGH after pulling it LOW, the radio stays in a hard reset state. Verify the
reset_sx1276()function is executing. - Inspect the HAT Header: Look closely at the 40-pin header. A common assembly error is misaligning the HAT by one pin offset, shifting MISO (Pin 21) to a ground pin. Power down and reseat the HAT.
- Measure 3.3V Rail: Use a multimeter to check the 3.3V output on the HAT’s breakout pins. If it reads 0V, the Pi’s polyfuse may have tripped, or the HAT’s onboard LDO is dead.
Error 2: PermissionError: [Errno 13] Permission denied: '/dev/spidev0.0'
The OS is blocking your user account from accessing the hardware peripheral.
- Run with Sudo: The quickest fix is running
sudo python3 lora_rx_gateway.py. - Fix Group Permissions (Permanent): Add your user to the SPI and GPIO groups:
sudo usermod -aG spi,gpio $USER, then log out and log back in.
Error 3: OSError: [Errno 2] No such file or directory: '/dev/spidev0.0'
The Linux kernel hasn't loaded the SPI device tree overlay.
- Verify raspi-config: Run
sudo raspi-configand ensure SPI is explicitly enabled. - Check config.txt: Open
/boot/firmware/config.txt(or/boot/config.txton older OS versions) and ensure the linedtparam=spi=onis present and uncommented. Reboot after editing.
Extending and Simplifying the Build
A single-channel Python gateway is perfect for the workbench, but it is not compliant with The Things Network (TTN) production standards, which require listening to all 8 (EU) or 64 (US) uplink channels simultaneously. Depending on your end goal, you should either scale up or strip down.
How to Extend: The Multi-Channel Concentrator
If your goal is to deploy a permanent gateway that feeds TTN or a local ChirpStack instance, retire the Dragino HAT. Instead, purchase a RAK2245 Pi HAT (approx. $110). The RAK2245 uses the Semtech SX1301 baseband chip, which acts as a multi-channel DSP, demodulating 8 channels concurrently. You will mount it to the Pi 4, install the Semtech UDP Packet Forwarder, and configure the global_conf.json file with your TTN Gateway ID. The Python script provided above is not used in this architecture; the C-daemon handles the SPI traffic directly.
How to Simplify: The ESP32 Bridge
If you realize you don't need the Linux environment, MQTT broker hosting, or local database logging that the Pi provides, drop the Raspberry Pi entirely. An ESP32-WROOM-32 paired with an SX1276 breakout board (total cost under $15) can run the sandeepmistry/arduino-LoRa library. The ESP32 can receive the LoRa payload and push it directly to AWS IoT or a local MQTT broker over WiFi, eliminating the OS overhead, SD card corruption risks, and SPI permission headaches inherent to Linux-based SBCs.
Choose the Raspberry Pi gateway when you need local packet capture, Python-based payload decryption, or edge-computing (like running a local Node-RED dashboard). Choose the ESP32 when you just need a dumb pipe to move RF bytes to the cloud.






