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 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:
Instruction | Binary format | # of bytes | Operation |
---|---|---|---|
R_REGISTER | 0x000A AAAA | 1 to 5 | Reads a register, AAAAA = 5 bit memory address |
W_REGISTER | 0x001A AAAA | 1 to 5 | Writes a register, AAAAA = 5 bit memory address |
R_RX_PAYLOAD | 0x0110 0001 | 1 to 32 | Reads payload from RX FIFO. Used in RX-Mode |
W_TX_PAYLOAD | 0x1010 0000 | 1 to 32 | Writes a payload to the TX buffer. Used in TX-Mode |
FLUSH_TX | 0x1110 0001 | 0 | Flush TX FIFO. Used in TX-Mode |
FLUSH_RX | 0x1110 0010 | 0 | Flush RX FIFO. Used in RX-Mode |
REUSE_TX_PL | 0x1110 0011 | 0 | Reuse last send TX payload while CE is high until cleared |
NOP | 0x1111 1111 | 0 | No 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)