Nrf24.Transciever (nrf24 v2.0.0)

View Source

High-level helper around Circuits.SPI and Circuits.GPIO for configuring and communicating with the Nordic nRF24L01+ 2.4GHz transceiver.

The module provides:

  • Convenience functions for setting up addresses, channels, payload sizes, CRC, auto-acknowledgement, retransmit parameters, and power/mode transitions.
  • Helpers for starting RX ("listening") and TX ("sending") operation using a CE (chip enable) GPIO pin.
  • Direct register access utilities for debugging and advanced scenarios.
  • Pretty-printers for common register sets.

SPI communication model (Circuits.SPI):

  • SPI is full-duplex: on every clocked out bit, one bit is clocked in.
  • Circuits.SPI.transfer/2 sends a binary command and returns {:ok, response} where response is a binary of the exact same size as what you sent.
  • To read N bytes from a device register, you typically send a one-byte "read command" followed by N dummy bytes (often 0x00 or 0xFF). The first returned byte is usually the device STATUS; the following N bytes are the data you requested.

Selecting and opening an SPI bus (Circuits.SPI):

  • Call Circuits.SPI.bus_names/0 to list the available SPI devices on your system. On Linux SBCs (like the Raspberry Pi), typical values are: ["spidev0.0", "spidev0.1"]
    • The suffix ".0" and ".1" generally correspond to the controller’s hardware chip-select lines (CS0 and CS1).
  • Open the one you’ve wired your nRF24L01+ to: {:ok, spi} = Circuits.SPI.open("spidev0.0", mode: 0, speed_hz: 8_000_000) Options of interest:
    • mode: SPI mode 0..3 (nRF24L01+ uses mode 0: CPOL=0, CPHA=0).
    • speed_hz: SPI clock frequency in Hz (e.g., 1_000_000 to 10_000_000; the nRF24L01+ tolerates up to 10 MHz per datasheet; choose a stable value for your wiring). The return value is a %Circuits.SPI.SPIDev{} handle used throughout this module.

CE and CSN wiring:

  • CE (chip enable) must be controlled by a GPIO. In RX, CE high enables the receiver; in TX, CE high triggers transmission of the loaded payload.
  • CSN (SPI chip select):
    • In most setups, connect the nRF24L01+'s CSN pin to the hardware SPI chip select of the chosen bus (e.g., Raspberry Pi CE0). Circuits.SPI will then assert CS automatically during transfers.
    • This module’s send/4 additionally allows asserting CSN via a GPIO while writing the TX payload. If your CSN is wired to a general-purpose GPIO, pass that GPIO number as csn_pin in send/4. If CSN is wired to the SPI controller’s chip-select, many platforms don’t expose it as a GPIO; in that case, consider wiring CSN to a dedicated GPIO or remove the manual CS control from send/4 in your local copy. The remainder of this module assumes standard SPI behavior (chip select is asserted during transfers).

Operating modes and defaults:

  • By default, the device is used in Enhanced ShockBurst with auto-ack (ACK) and CRC enabled.
  • All register reads/writes here follow the nRF24L01+ register map.
  • Functions that write registers typically return the low-level Circuits.SPI.transfer/2 return value (e.g., {:ok, <<status, ...>>}) so you can inspect the STATUS if desired. Callers often pattern match on {:ok, _}.

Basic SPI read/write example:

# Read a 3-byte payload using SPI full-duplex:
# 1) Send a single-byte READ-PAYLOAD command
# 2) Follow it with 3 dummy bytes so the device clocks out 3 bytes to us
cmd = <<0x61>> <> <<0x00, 0x00, 0x00>>   # 0x61 == R_RX_PAYLOAD
{:ok, <<_status, payload::binary-size(3)>>} = Circuits.SPI.transfer(spi, cmd)

Typical RX workflow:

{:ok, spi} = Circuits.SPI.open("spidev0.0", mode: 0, speed_hz: 8_000_000)

# Configure a pipe (address, payload size, enable, ACK on):
addr = "Rcvr1"                 # 5-byte address for pipe 1
:ok = Nrf24.Transciever.reset(spi)
{:ok, _} = Nrf24.Transciever.set_channel(spi, 76)  # example channel
{:ok, nil} =
  Nrf24.Transciever.set_pipe(spi, 1,
    address: addr,
    payload_size: 6,
    auto_acknowledgement: true
  )

# Start listening (CE high):
ce_pin = 25
{:ok, nil} = Nrf24.Transciever.start_listening(spi, ce_pin)

# Poll for data:
case Nrf24.Transciever.receive(spi, 6) do
  {:ok, %{pipe: p, data: <<a, b, c, d, e, f>>}} ->
    IO.puts("Got 6 bytes on pipe #{p}: #{inspect({a, b, c, d, e, f})}")
  {:error, :no_data} ->
    :timer.sleep(5)
  {:error, reason} ->
    IO.inspect(reason, label: "rx error")
end

# Stop listening (CE low, power down):
{:ok, nil} = Nrf24.Transciever.stop_listening(spi, ce_pin)

Typical TX workflow:

{:ok, spi} = Circuits.SPI.open("spidev0.0", mode: 0, speed_hz: 8_000_000)

# TX and pipe0 must share the same 5-byte address when auto-ack is enabled:
tx_addr = <<0x0a, 0x0b, 0x0c, 0x0d, 0x0e>>
:ok = Nrf24.Transciever.reset(spi)
{:ok, _} = Nrf24.Transciever.set_channel(spi, 76)

# Prepare the radio for TX:
:ok = Nrf24.Transciever.start_sending(spi, tx_addr)

# Trigger a send:
csn_pin = 8   # Example only; see "CE and CSN wiring" notes above
ce_pin  = 25
data = <<"hello!">>  # up to 32 bytes
{:ok, ce} = Nrf24.Transciever.send(spi, data, csn_pin, ce_pin)

# When finished, bring CE low and release the pin:
:ok = Nrf24.Transciever.stop_sending(ce)

Safety and wiring reminders:

  • The nRF24L01+ is a 3.3V device. Do not supply it with 5V and ensure SPI pins are 3.3V compatible.
  • Keep SPI wires short and use a decoupling capacitor near the module.

Summary

Functions

Enable or disable auto-acknowledgement per pipe.

Return a string with multi-line dump of key configuration and FIFO status to stdout.

Disable an RX pipe in EN_RXADDR.

Enable an RX pipe in EN_RXADDR.

Read the current RF channel.

Set PWR_UP=0 (power down the device).

Set PWR_UP=1 (power up the device).

Read one or more bytes from a register.

Read one payload from RX FIFO, if available.

Reset registers to a known default configuration.

Clear RX_DR, TX_DS, and MAX_RT in STATUS, i.e., reset the standard IRQ flags.

Send a payload (1..32 bytes). Uses auto-ack by default.

Set the RF channel (frequency) used to receive/transmit.

Configure CRC length.

Set the static payload size for a given RX pipe.

Configure an RX pipe: address, payload size, auto-ack, and enable it.

Set an RX pipe address.

Put the radio into receive mode (PRIM_RX=1).

Set Auto Retransmit Count (ARC).

Set Auto Retransmit Delay (ARD).

Configure RF data rate.

Put the radio into transmit mode (PRIM_RX=0).

Prepare the device for receiving data.

Prepare the device for TX.

Stop receiving: power down and drive CE low.

Bring CE low and release the CE GPIO reference obtained from send/4.

Write a register with an integer or binary value.

Functions

ack(spi, pipe_no, on \\ true)

Enable or disable auto-acknowledgement per pipe.

Parameters:

* spi: %Circuits.SPI.SPIDev{}
* pipe_no: 0..5
* on: boolean (default: true)

Return:

* {:ok, response_binary} on success
* {:error, :invalid_spi_or_pipe_value} otherwise

Example:

# Disable auto-ack on pipe 2:
{:ok, _} = Nrf24.Transciever.ack(spi, 2, false)

details(spi)

Return a string with multi-line dump of key configuration and FIFO status to stdout.

Parameters:

* spi: %Circuits.SPI.SPIDev{}

Output:

* Lines include STATUS, addresses, RX payload widths, CONFIG, EN_AA, EN_RXADDR,

RF_CH, RF_SETUP, and FIFO_STATUS with bit-level interpretation.

Example:

Nrf24.Transciever.print_details(spi)
|> IO.puts()

disable_pipe(spi, pipe_no)

Disable an RX pipe in EN_RXADDR.

Parameters:

* spi: %Circuits.SPI.SPIDev{}
* pipe_no: 0..5

Return:

* {:ok, response_binary} on success
* {:error, :invalid_spi_or_pipe_number} otherwise

enable_pipe(spi, pipe_no)

Enable an RX pipe in EN_RXADDR.

Parameters:

* spi: %Circuits.SPI.SPIDev{}
* pipe_no: 0..5

Return:

* {:ok, response_binary} on success
* {:error, :invalid_spi_or_pipe_number} otherwise

get_channel(spi)

Read the current RF channel.

Parameters:

* spi: %Circuits.SPI.SPIDev{}

Return:

* channel :: 0..255 as integer
* {:error, :invalid_spi} if argument is invalid.

Example:

ch = Nrf24.Transciever.get_channel(spi)
IO.puts("Channel = #{ch}")

power_off(spi)

Set PWR_UP=0 (power down the device).

Parameters:

* spi: %Circuits.SPI.SPIDev{}

Return:

* {:ok, response_binary} on success
* {:error, :invalid_spi} otherwise

power_on(spi)

Set PWR_UP=1 (power up the device).

Parameters:

* spi: %Circuits.SPI.SPIDev{}

Return:

* {:ok, response_binary} on success
* {:error, :invalid_spi} otherwise

Notes:

* Respect the datasheets power-up timing before issuing further commands that

depend on the device being fully powered.

read_register(spi, reg, bytes_no \\ 1)

Read one or more bytes from a register.

Parameters:

* spi: %Circuits.SPI.SPIDev{}
* reg: atom for the target register
* bytes_no: positive integer (default 1)

Return:

* When bytes_no == 1: integer (0..255)
* When bytes_no > 1: binary of length bytes_no
* {:error, :invalid_spi} if arguments are invalid

Details:

* Implements the SPI read pattern by sending the READ_REGISTER command and

appending bytes_no dummy bytes (0xFF) to clock out the register contents.

Examples:

# Single byte:
config = Nrf24.Transciever.read_register(spi, :config)

# Multiple bytes (e.g., 5-byte address):
addr = Nrf24.Transciever.read_register(spi, :rx_addr_p1, 5)

receive(spi, payload_size)

Read one payload from RX FIFO, if available.

Parameters:

* spi: %Circuits.SPI.SPIDev{}
* payload_size: number of bytes to read (1..32). Must match the configured

RX_PW_Px for the pipe that delivered the payload unless dynamic payloads are enabled.

Return:

* {:ok, %{pipe: integer_pipe_no, data: bitstring_payload}} on success
* {:error, :no_data} if RX FIFO is empty
* {:error, :unexpected_data_in_fifo} for unexpected responses
* {:error, :invalid_spi} if invalid argument

Details:

* STATUS.RX_P_NO bits indicate which pipe has available data.
* Uses R_RX_PAYLOAD (0x61) comman and appends dummy bytes to clock out the payload.

Status register bits are (pg. 56 of datasheet PDF):

* Bit 7 - Reserved
* Bit 6 (`RX_DR`) - Data ready RX FIFO interrupt. Asserted when
  new data arrives RX FIFO.
* Bit 5 (`TX_DS`) - Data sent TX FIFO interrupt. Asserted when
  packet transmitted on TX.
* Bit 4 (`MAX_RT`) - Maximum number of TX retransmits interrupt.
* Bit 3-1 (`RX_P_NO`) - Data pipe number for the payload available
  for reading from RX_FIFO.
  000 - 101: Data pipe number
  110: Not used
  111: RX FIFO empty
* Bit 0 (`TX_FULL`) - TX FIFO full flag.
  1 - TX FIFO full.
  0 - Available locations in TX FIFO.

Example:

case Nrf24.Transciever.receive(spi, 6) do
  {:ok, %{pipe: p, data: payload}} -> IO.inspect({p, payload})
  {:error, :no_data} -> :ok
end

reset(spi)

Reset registers to a known default configuration.

Parameters:

* spi: %Circuits.SPI.SPIDev{}

Return:

* {:ok, response_binary} from the last write (intermediate writes also occur)

Details:

* Writes default CONFIG, RX/TX addresses, payload widths, EN_AA, EN_RXADDR,

RF_CH, RF_SETUP, etc., similar to datasheet defaults.

* Useful before custom configuration to ensure a clean baseline.

Example:

:ok = Nrf24.Transciever.reset(spi)

reset_status(spi)

Clear RX_DR, TX_DS, and MAX_RT in STATUS, i.e., reset the standard IRQ flags.

Parameters:

* spi: %Circuits.SPI.SPIDev{}

Return:

* {:ok, response_binary} on success
* {:error, :invalid_spi} otherwise

send(spi, data, csn_pin, ce_pin)

Send a payload (1..32 bytes). Uses auto-ack by default.

Parameters:

* spi: %Circuits.SPI.SPIDev{}
* data: binary (max 32 bytes)
* csn_pin: integer GPIO for CSN (see "CE and CSN wiring" in moduledoc)
* ce_pin: integer GPIO for CE

Behavior:

* Asserts CSN low (via GPIO), writes W_TX_PAYLOAD (0xA0) + data over SPI, then

asserts CE high to trigger the send. Returns the CE GPIO resource so the caller can later call stop_sending/1.

* The SPI write follows full-duplex semantics; only the STATUS byte is meaningful

in the returned value from the internal SPI.transfer call. It’s ignored here.

Return:

* {:ok, ce_gpio_ref} on success
* {:error, :invalid_spi} if the input does not match expectations

Example:

{:ok, ce} = Nrf24.Transciever.send(spi, <<"hello">>, csn_pin, ce_pin)
:ok = Nrf24.Transciever.stop_sending(ce)

set_channel(spi, channel)

Set the RF channel (frequency) used to receive/transmit.

Parameters:

* spi: %Circuits.SPI.SPIDev{} returned by Circuits.SPI.open/2
* channel: integer 1..255 (nRF24L01+ datasheet defines valid channels as 0..125,

but this function accepts 1..255 as a guard; ensure your chosen channel is compatible with your peer and region).

Return:

* {:ok, response_binary} on success, where the first byte is the STATUS register

returned during the write operation; subsequent bytes may be undefined.

* {:error, :invalid_spi_or_channel_value} if arguments are invalid.

Example:

{:ok, spi} = SPI.open("spidev0.0", mode: 0, speed_hz: 8_000_000)
{:ok, _} = Nrf24.Transciever.set_channel(spi, 76)

set_crc_length(spi, length)

Configure CRC length.

Parameters:

* spi: %Circuits.SPI.SPIDev{}
* length: 1 or 2 (bytes)

Behavior:

* Sets the CRCO bit in CONFIG accordingly. CRC is expected to be enabled in the

default configuration; if not, enable with CONFIG.EN_CRC.

Return:

* {:ok, response_binary} on success
* {:error, :invalid_spi_or_crc_length} otherwise

Example:

{:ok, _} = Nrf24.Transciever.set_crc_length(spi, 2)

set_payload_size(spi, pipe_no, payload_size)

Set the static payload size for a given RX pipe.

Parameters:

* spi: %Circuits.SPI.SPIDev{}
* pipe_no: 0..5
* payload_size: 1..32 bytes

Return:

* {:ok, response_binary} on success
* {:error, :invalid_spi_or_payload_size} otherwise

Example:

{:ok, _} = Nrf24.Transciever.set_payload_size(spi, 1, 6)

set_pipe(spi, pipe_no, options \\ [])

Configure an RX pipe: address, payload size, auto-ack, and enable it.

Parameters:

* spi: %Circuits.SPI.SPIDev{}
* pipe_no: 0..5
* options (Keyword):
  * :address
    * Pipe 0 and 1: 5-byte binary or 5-char string
    * Pipes 2..5: 1-byte binary (<<x>>) or integer x (0..255)
  * :payload_size (default: 32)
  * :auto_acknowledgement (default: true)

Return:

* {:ok, nil} on success
* {:error, reason} if any of the steps fails

Examples:

# Enable pipe 1 with address 0x0a0b0c0d0e and payload size 6:
{:ok, spi} = SPI.open("spidev0.0")
address = <<0x0a, 0x0b, 0x0c, 0x0d, 0x0e>>
{:ok, nil} = Nrf24.Transciever.set_pipe(spi, 1, address: address, payload_size: 6)

# Enable pipe 4 with address 0xa7 and payload size 12:
{:ok, nil} = Nrf24.Transciever.set_pipe(spi, 4, address: <<0xa7>>, payload_size: 12)

set_pipe_address(spi, pipe_no, address)

Set an RX pipe address.

Parameters:

* spi: %Circuits.SPI.SPIDev{}
* pipe_no: 0..5
* address:
  * For pipes 0 and 1: a 5-byte binary (<<_::8, _::8, _::8, _::8, _::8>>), e.g., "Recv1"
or <<0xE7, 0xA0, 0x17, 0x13, 0x25>>.
  * For pipes 2..5: a single byte integer (0..255). These pipes share the high
4 bytes with pipe 1 and only override the least significant byte.

Return:

* {:ok, response_binary} on success
* {:error, :invalid_pipe_address} otherwise

Examples:

# Pipe 0, mnemonic string:
{:ok, _} = Nrf24.Transciever.set_pipe_address(spi, 0, "Recv1")

# Pipe 1, explicit 5-byte address:
{:ok, _} = Nrf24.Transciever.set_pipe_address(spi, 1, <<0xe7, 0xa0, 0x17, 0x13, 0x25>>)

# Pipe 4, single-byte suffix:
{:ok, _} = Nrf24.Transciever.set_pipe_address(spi, 4, 147)

set_receive_mode(spi)

Put the radio into receive mode (PRIM_RX=1).

Parameters:

* spi: %Circuits.SPI.SPIDev{}

Return:

* {:ok, response_binary} on success
* {:error, :invalid_spi} otherwise

Notes:

* After setting receive mode, call start_listening/2 to assert CE and power up.

set_retransmit_count(spi, count)

Set Auto Retransmit Count (ARC).

Parameters:

* spi: %Circuits.SPI.SPIDev{}
* count: integer 0..15 (number of retransmissions before giving up)

Return:

* {:ok, response_binary} on success
* {:error, :invalid_spi_or_retransmit_count_value} otherwise

Note:

* This function writes bits [3:0] of SETUP_RETR and overwrites the entire

register. Combine with set_retransmit_delay/2 carefully (see its docs).

set_retransmit_delay(spi, delay)

Set Auto Retransmit Delay (ARD).

Parameters:

* spi: %Circuits.SPI.SPIDev{}
* delay: integer 0..15 corresponding to:

0 -> 250 µs, 1 -> 500 µs, 2 -> 750 µs, ... 15 -> 4000 µs (Time from end of a TX to start of next TX.)

Return:

* {:ok, response_binary} on success
* {:error, :invalid_spi_or_retransmit_delay_value} otherwise

Notes:

* nRF24L01+ timing constraints:
  * 2 Mbps: if ACK payload > 15 bytes, ARD must be >= 500 µs.
  * 1 Mbps: if ACK payload > 5 bytes, ARD must be >= 500 µs.
  * 250 kbps: ARD must be >= 500 µs.

* This function writes bits [7:4] of SETUP_RETR (by shifting delay << 4).
  If you also set the retransmit count via set_retransmit_count/2, call
  both in an order that results in the desired final [7:4] and [3:0]
  nibbles (each call overwrites the other nibble in this implementation).

set_speed(spi, speed)

Configure RF data rate.

Parameters:

* spi: %Circuits.SPI.SPIDev{}
* speed: one of:
  * :min    -> 250 kbps  (RF_DR_LOW=1, RF_DR_HIGH=0)
  * :medium -> 1 Mbps    (RF_DR_LOW=0, RF_DR_HIGH=0)
  * :max    -> 2 Mbps    (RF_DR_LOW=0, RF_DR_HIGH=1)

Return:

* {:ok, response_binary} on success
* {:error, :invalid_spi_orspeed_value} otherwise

Example:

{:ok, _} = Nrf24.Transciever.set_speed(spi, :max)

set_transmit_address(spi, address)

Set the TX address.

Parameters:

* spi: %Circuits.SPI.SPIDev{}
* address: 5-byte binary

Behavior:

* In auto-ack mode, TX_ADDR must equal RX_ADDR_P0 on the receiver side.

Return:

* {:ok, response_binary} on success
* {:error, :invalid_spi_or_transmit_address} otherwise

set_transmit_mode(spi)

Put the radio into transmit mode (PRIM_RX=0).

Parameters:

* spi: %Circuits.SPI.SPIDev{}

Return:

* {:ok, response_binary} on success
* {:error, :invalid_spi} otherwise

Notes:

* Typically combined with start_sending/2 which also sets addresses and powers up.

start_listening(spi, ce_pin_no)

Prepare the device for receiving data.

Steps:

* Set PRIM_RX=1.
* Power up.
* Clear IRQ flags in STATUS.
* Drive CE high via the provided GPIO pin.

Parameters:

* spi: %Circuits.SPI.SPIDev{}
* ce_pin_no: integer GPIO number connected to CE

Return:

* {:ok, nil} on success
* {:error, reason} otherwise

Notes:

* After calling this, poll receive/2 to fetch data. When finished, call
  stop_listening/2 to power down and deassert CE.

start_sending(spi, address)

Prepare the device for TX.

Steps:

* Set PRIM_RX=0 (TX mode).
* Program RX_ADDR_P0 and TX_ADDR with the same 5-byte address (required for
  auto-ack with pipe 0).
* Ensure auto-ack is enabled on pipe 0.
* Clear IRQ flags in STATUS.
* Power up.

Parameters:

* spi: %Circuits.SPI.SPIDev{}
* address: 5-byte binary

Return:

* :ok on success
* {:error, reason} otherwise

Follow up:

* Call send/4 to write the payload and assert CE to transmit, then
  stop_sending/1 to deassert CE.

stop_listening(spi, ce_pin_no)

Stop receiving: power down and drive CE low.

Parameters:

* spi: %Circuits.SPI.SPIDev{}
* ce_pin_no: integer GPIO number for CE

Return:

* {:ok, nil} on success
* {:error, reason} otherwise

stop_sending(ce)

Bring CE low and release the CE GPIO reference obtained from send/4.

Parameters:

* ce: %Circuits.GPIO{} returned by send/4

Return:

* :ok

Notes:

  • Call this after a send to complete the TX cycle and deassert CE.

write_register(spi, reg, value)

Write a register with an integer or binary value.

Parameters:

* spi: %Circuits.SPI.SPIDev{}
* reg: one of the known atoms in this module (e.g., :config, :rf_ch, :rx_addr_p0, etc.)
* value:
  * integer (0..255) for single-byte registers
  * binary for multi-byte registers (e.g., 5-byte addresses)

Return:

* {:ok, response_binary} from Circuits.SPI.transfer/2

Notes:

* The responses first byte is the device STATUS; additional bytes (if any)

are what the device clocked out while your value was clocked in.