Nrf24.Transciever (nrf24 v2.0.0)
View SourceHigh-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.
Set the TX address.
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
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} otherwiseExample:
# Disable auto-ack on pipe 2:
{:ok, _} = Nrf24.Transciever.ack(spi, 2, false)
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 an RX pipe in EN_RXADDR.
Parameters:
* spi: %Circuits.SPI.SPIDev{}
* pipe_no: 0..5Return:
* {:ok, response_binary} on success
* {:error, :invalid_spi_or_pipe_number} otherwise
Enable an RX pipe in EN_RXADDR.
Parameters:
* spi: %Circuits.SPI.SPIDev{}
* pipe_no: 0..5Return:
* {:ok, response_binary} on success
* {:error, :invalid_spi_or_pipe_number} otherwise
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}")
Set PWR_UP=0 (power down the device).
Parameters:
* spi: %Circuits.SPI.SPIDev{}Return:
* {:ok, response_binary} on success
* {:error, :invalid_spi} otherwise
Set PWR_UP=1 (power up the device).
Parameters:
* spi: %Circuits.SPI.SPIDev{}Return:
* {:ok, response_binary} on success
* {:error, :invalid_spi} otherwiseNotes:
* Respect the datasheet’s power-up timing before issuing further commands thatdepend on the device being fully powered.
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 invalidDetails:
* Implements the SPI read pattern by sending the READ_REGISTER command andappending 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)
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 configuredRX_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 argumentDetails:
* 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 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)
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 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 CEBehavior:
* Asserts CSN low (via GPIO), writes W_TX_PAYLOAD (0xA0) + data over SPI, thenasserts 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 meaningfulin 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 expectationsExample:
{:ok, ce} = Nrf24.Transciever.send(spi, <<"hello">>, csn_pin, ce_pin)
:ok = Nrf24.Transciever.stop_sending(ce)
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 registerreturned 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)
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 thedefault configuration; if not, enable with CONFIG.EN_CRC.
Return:
* {:ok, response_binary} on success
* {:error, :invalid_spi_or_crc_length} otherwiseExample:
{:ok, _} = Nrf24.Transciever.set_crc_length(spi, 2)
Set the static payload size for a given RX pipe.
Parameters:
* spi: %Circuits.SPI.SPIDev{}
* pipe_no: 0..5
* payload_size: 1..32 bytesReturn:
* {:ok, response_binary} on success
* {:error, :invalid_spi_or_payload_size} otherwiseExample:
{:ok, _} = Nrf24.Transciever.set_payload_size(spi, 1, 6)
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 failsExamples:
# 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 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} otherwiseExamples:
# 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)
Put the radio into receive mode (PRIM_RX=1).
Parameters:
* spi: %Circuits.SPI.SPIDev{}Return:
* {:ok, response_binary} on success
* {:error, :invalid_spi} otherwiseNotes:
* After setting receive mode, call start_listening/2 to assert CE and power up.
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} otherwiseNote:
* This function writes bits [3:0] of SETUP_RETR and overwrites the entireregister. Combine with set_retransmit_delay/2 carefully (see its docs).
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} otherwiseNotes:
* 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).
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} otherwiseExample:
{:ok, _} = Nrf24.Transciever.set_speed(spi, :max)
Set the TX address.
Parameters:
* spi: %Circuits.SPI.SPIDev{}
* address: 5-byte binaryBehavior:
* 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
Put the radio into transmit mode (PRIM_RX=0).
Parameters:
* spi: %Circuits.SPI.SPIDev{}Return:
* {:ok, response_binary} on success
* {:error, :invalid_spi} otherwiseNotes:
* Typically combined with start_sending/2 which also sets addresses and powers up.
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 CEReturn:
* {:ok, nil} on success
* {:error, reason} otherwiseNotes:
* After calling this, poll receive/2 to fetch data. When finished, call
stop_listening/2 to power down and deassert CE.
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 binaryReturn:
* :ok on success
* {:error, reason} otherwiseFollow up:
* Call send/4 to write the payload and assert CE to transmit, then
stop_sending/1 to deassert CE.
Stop receiving: power down and drive CE low.
Parameters:
* spi: %Circuits.SPI.SPIDev{}
* ce_pin_no: integer GPIO number for CEReturn:
* {:ok, nil} on success
* {:error, reason} otherwise
Bring CE low and release the CE GPIO reference obtained from send/4.
Parameters:
* ce: %Circuits.GPIO{} returned by send/4Return:
* :okNotes:
- Call this after a send to complete the TX cycle and deassert CE.
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/2Notes:
* The response’s first byte is the device STATUS; additional bytes (if any)are what the device clocked out while your value was clocked in.