Nrf24

Elixir library for receiving and sending data through the Nordic nRF24L01+ 2.4GHz wireless transceiver module.

The library wraps Circuits.SPI and Circuits.GPIO to handle common nRF24L01+ configuration (addresses, channels, payload sizes, ACK/CRC, retransmits) and runtime operations (listening for RX and sending TX payloads with CE control). It defaults to Enhanced ShockBurst with auto-acknowledgement (ACK) and CRC enabled.

This README includes wiring guidance, setup instructions, and step-by-step examples for Raspberry Pi and Arduino interoperability. It also includes a brief advanced section for using the low-level Nrf24.Transciever API directly.

Important electrical notes:

Pinout

nRF24L01 Pinout

Raspberry Pi wiring

Typical Raspberry Pi (BCM numbering) wiring:

nRF24L01+ Raspberry Pi (GPIO) RPi 3B+ pin no.
GND GND 6
VCC 3.3V 1
CE 17 11
CSN 8 24
SCK 11 23
MOSI 10 19
MISO 9 21

Notes:

Raspberry Pi 3B+ pinout reference:

Raspberry PI 3B+ Pinout

Arduino

Wiring

The library was tested with a second nRF24L01+ module connected to an Arduino Nano:

nRF24L01+ Arduino
GND GND
VCC +3V3
CE D9
CSN D10
SCK D13
MOSI D11
MISO D12

Arduino Nano Pinout

Arduino library

Use the RF24 Arduino library and the GettingStarted example for a quick peer to the Elixir side: https://nrf24.github.io/RF24/

Ensure both sides use:

Prerequisites

Selecting an SPI bus:

Circuits.SPI basics (relevant to this library):

This library opens the SPI bus using Circuits.SPI.open(bus_name) with default options (SPI mode 0). nRF24L01+ requires SPI mode 0 (CPOL=0, CPHA=0).

Installation

Add nrf24 to your deps in mix.exs:

def deps do
  [
    {:nrf24, "~> 2.0.0"}
  ]
end

Fetch deps:

mix deps.get

Quick start

The Nrf24 module is a GenServer that owns the SPI handle and knows your CE/CSN GPIOs. It provides convenience functions for common operations.

Addresses and payload sizes:

Data rate and channel must match on both peers. The examples below use:

Receiving data

{:ok, nrf} =
  Nrf24.start_link(
    bus_name: "spidev0.0",
    ce_pin: 17,   # GPIO17 for CE
    csn_pin: 8,   # GPIO8 for CSN (can be any GPIO you wired to CSN)
    channel: 0x4C,
    crc_length: 2,
    speed: :medium,
    pipes: [
      [pipe_no: 0, address: "1Node", payload_size: 4, auto_ack: true]
    ]
  )

# Put the radio into RX and assert CE
:ok = Nrf24.start_listening(nrf)

# Block up to ~30s waiting for one payload (default timeout in library)
case Nrf24.receive(nrf, 4) do
  {:ok, %{pipe: pipe_no, data: <<a, b, c, d>>}} ->
    IO.puts("Received on pipe #{pipe_no}: #{inspect({a, b, c, d})}")

  {:error, :no_data} ->
    IO.puts("No data received within timeout")

  {:error, reason} ->
    IO.puts("Receive error: #{inspect(reason)}")
end

# Deassert CE and power down
:ok = Nrf24.stop_listening(nrf)

Tips:

Sending data

{:ok, nrf} =
  Nrf24.start_link(
    bus_name: "spidev0.0",
    ce_pin: 17,
    csn_pin: 8,
    channel: 0x4C,
    crc_length: 2,
    speed: :medium
  )

# 4-byte little-endian float as an example payload
data = <<9273.69::float-little-size(32)>>

# Send asynchronously to receiver address (must be 5 bytes)
# When auto-ack is on, TX_ADDR must equal the receiver&#39;s RX_ADDR_P0
Nrf24.send(nrf, "2Node", data)

Notes:

API overview

Common operations:

# Change RF channel (0..125 typical)
{:ok, _} = Nrf24.set_channel(nrf, 76)

# CRC length: 1 or 2 bytes
{:ok, _} = Nrf24.set_crc_length(nrf, 2)

# Power management
{:ok, _} = Nrf24.power_on(nrf)
{:ok, _} = Nrf24.power_off(nrf)

# RX/TX mode
{:ok, _} = Nrf24.set_receive_mode(nrf)
{:ok, _} = Nrf24.set_transmit_mode(nrf)

# Enable/disable auto-ack per pipe (0..5)
{:ok, _} = Nrf24.ack_on(nrf, 0)
{:ok, _} = Nrf24.ack_off(nrf, 0)

# Enable/disable a pipe
{:ok, _} = Nrf24.enable_pipe(nrf, 0)
{:ok, _} = Nrf24.disable_pipe(nrf, 0)

# Configure a pipe in one go
{:ok, nil} =
  Nrf24.set_pipe(nrf, 1,
    address: "Rcvr1",
    payload_size: 6,
    auto_acknowledgement: true
  )

# Retransmit tuning
{:ok, _} = Nrf24.set_retransmit_delay(nrf, 2)  # 2->750µs (see datasheet mapping)
{:ok, _} = Nrf24.set_retransmit_count(nrf, 15) # up to 15 retries

# Low-level register access
{:ok, _} = Nrf24.write_register(nrf, :rf_ch, 76)
val = Nrf24.read_register(nrf, :rf_ch)
addr_p1 = Nrf24.read_register(nrf, :rx_addr_p1, 5)

# Reset device to a known baseline configuration
{:ok, _} = Nrf24.reset_device(nrf)

Notes on speed:

Advanced: using the low-level Transceiver API

If you want full control or to script SPI operations directly in IEx, use Nrf24.Transciever.

Example (TX) using direct SPI:

alias Circuits.SPI
alias Circuits.GPIO
alias Nrf24.Transciever

# Choose your SPI bus from Circuits.SPI.bus_names()
{:ok, spi} = SPI.open("spidev0.0")

# Configure
Transciever.reset(spi)  # Optional: baseline
Transciever.set_channel(spi, 76)
Transciever.set_crc_length(spi, 2)
# Data rate configuration is library-dependent;
# :medium is a safe default path via Nrf24 GenServer.

# Prepare TX: sets TX mode, programs TX_ADDR and RX_ADDR_P0
# to same 5-byte value, enables ACK on P0, clears IRQ, powers up
Transciever.start_sending(spi, "2Node")

# Send: CSN is toggled via a GPIO you supply, CE toggled via another GPIO
csn_pin = 8
ce_pin = 17
payload = <<"Hi!">>
{:ok, ce} = Transciever.send(spi, payload, csn_pin, ce_pin)

# Finish TX
Transciever.stop_sending(ce)

Example (RX) using direct SPI:

alias Circuits.SPI
alias Nrf24.Transciever

{:ok, spi} = SPI.open("spidev0.0")

Transciever.reset(spi) # Optional
Transciever.set_channel(spi, 76)
Transciever.set_pipe(
  spi,
  0,
  address: "1Node",
  payload_size: 4,
  auto_acknowledgement: true)

# Start listening (CE high while in RX mode)
Transciever.start_listening(spi, 17)

# Poll for a single payload (4 bytes)
case Transciever.receive(spi, 4) do
  {:ok, %{pipe: p, data: <<a, b, c, d>>}} ->
    IO.puts("RX pipe #{p}: #{inspect({a, b, c, d})}")

  {:error, :no_data} ->
    IO.puts("No data available")

  {:error, reason} ->
    IO.puts("RX error: #{inspect(reason)}")
end

# Stop listening (CE low, power down)
Transciever.stop_listening(spi, 17)

Debug/inspection helpers:

Tips and troubleshooting