iron's blog

Wireless communication with an NRF24L01

We all use wireless devices all the time, from phones to remotes. A wireless device that I often use is a remote for a gate. However, my wireless receiver released the magic smoke that it once contained. An image of the magic-smoke container is show below. While I am capable of replacing the magic-smoke container; it would simple release its smoke again.

The magic smoke container

The device it fairly simple, all it does is toggle a relay when it receives a message from a paired remote. Its so simple infact, that I wanted to make it myself. That is why I ordered a handful of parts, including the NRF24L01 wireless module. The NRF24L01 is an easy to use IC that enables ordinary microcontrollers (such as my favorite ATtiny404) to have a wireless interface. The NRF24L01 uses an SPI connection to the microcontroller, and also has an additional IRQ which can trigger an interrupt whenever data is available.

Interfacing with the NRF24L01

Interfacing with the NRF24L01 over SPI is based on a handful of commands:

InstructionBinary format# of bytesOperation
R_REGISTER0x000A AAAA1 to 5Reads a register, AAAAA = 5 bit memory address
W_REGISTER0x001A AAAA1 to 5Writes a register, AAAAA = 5 bit memory address
R_RX_PAYLOAD0x0110 00011 to 32Reads payload from RX FIFO. Used in RX-Mode
W_TX_PAYLOAD0x1010 00001 to 32Writes a payload to the TX buffer. Used in TX-Mode
FLUSH_TX0x1110 00010Flush TX FIFO. Used in TX-Mode
FLUSH_RX0x1110 00100Flush RX FIFO. Used in RX-Mode
REUSE_TX_PL0x1110 00110Reuse last send TX payload while CE is high until cleared
NOP0x1111 11110No Operation, can be used to read the status register

Which means that configuring it is essentially the same as configuring the microcontroller itself. I wrote a simple interface that directly translates the SPI instructions into normal functions

  // Starts a new command by transitioning CSN from High to Low
  void StartCommand();
  // Ends a command by transitioning CSN from Low to High
  void EndCommand();

  // Reads Registers
  // - Argument 'len' must be between 1 and 5
  void R_REGISTER(uint8_t address, uint8_t* data, uint8_t len);
  // Writes Registers
  // - Argument 'len' must be between 1 and 5
  // - Only usable in 'Power Down' or 'standby' modes
  void W_REGISTER(uint8_t address, uint8_t* data, uint8_t len);

  // Reads RX-Payload
  // - Argument 'len' must be between 1 and 32
  // - Used in RX mode
  void R_RX_PAYLOAD(uint8_t* data, uint8_t len);
  // Writes TX-Payload
  // Argument 'len' must be between 1 and 32
  // - Used in TX mode
  void W_TX_PAYLOAD(uint8_t* data, uint8_t len);

  // Flush TX FIFO
  // - Used in TX mode
  void FLUSH_TX();
  // Flush RX FIFO
  // - Used in RX mode
  // - Should not be executed during transmission of acknowledge
  void FLUSH_RX();

  // Reuse last sent payload
  // - Must not be changed during package transmission
  // - Active until W_TX_PAYLOAD or FLUSH_TX
  void REUSE_TX_PL();

  // No Operation
  // - Might be used to read STATUS register
  uint8_t NOP();

If you have an easy to use SPI interface, implementing these functions is very easy:

void NRF24L01::StartCommand() {
  GPIO->SetPin(NRF_PORT, NRF_CSN, LOW);
}

void NRF24L01::EndCommand() {
  GPIO->SetPin(NRF_PORT, NRF_CSN, HIGH);
}

void NRF24L01::R_REGISTER(uint8_t address, uint8_t* data, uint8_t len) {
  StartCommand();
  uint8_t cmd = 0b00000000 | address;
  SPI->Send(&cmd, sizeof(cmd));
  SPI->Receive(data, len);
  EndCommand();
}

void NRF24L01::W_REGISTER(uint8_t address, uint8_t* data, uint8_t len) {
  StartCommand();
  uint8_t cmd = 0b00100000 | address;
  SPI->Send(&cmd, sizeof(cmd));
  SPI->Send(data, len);
  EndCommand();
}

...

And that is nearly all there is to it! Simply write a convenient wrapper with a handul of helper-functions and sending data is as simple as simply filling a buffer, and sending it to the NRF24L01.

uint8_t nrfAddress[5] = {0x43, 0x34, 0x11, 0x12, 0x01};
uint8_t nrfData[32];

nrf.InitTX(40, nrfAddress, 5);
nrf.Transmit(nrfData);

Receiving data is simple aswell;

uint8_t nrfAddress[5] = {0x43, 0x34, 0x11, 0x12, 0x01};
uint8_t nrfData[32];

nrf.InitRX(40, nrfAddress, 5)
nrf.AttachInterruptRX([]() {
  nrf.Receive(nrfData);
});

All that is left now is implementing a protocol, and you’re succesfully transferring data over a wireless interface.

The full version of the code, schematics and other information will be available on my Gitlab instance soon(tm)

Thank you for reading this article.
If you spot any mistakes or if you would like to contact me, visit the contact page for more details.