NRF24L01+ Transceiver | Linux Device Driver
NRF24L01+ Transceiver | Linux Device Driver

NRF24L01+ Transceiver

  • The nRF24L01+ is a single-chip 2.4GHz transceiver operating at the ISM frequency band at 2.400 – 2.4835GHz.
  • The radio front end uses GFSK modulation.
  • 126 RF channels.
  • Supports an air data rate of 250 kbps, 1 Mbps and 2Mbps.
  • Enhanced ShockBurstTM supports
    • 1 to 32 bytes dynamic payload length.
    • Automatic packet handling.
    • Auto packet transaction handling.
    • 6 data pipe MultiCeiverTM for 1:6 star networks.
  • Host Interface – 4-pin hardware SPI.
  • 3 separate 32 bytes TX and RX FIFOs.
  • 2 devices is required in order to transmit or receive data with each other.

Linux device driver

Steps needed to write SPI client drivers.

  • Declare device IDs supported by the driver using spi_device_id or of_device_id.
  • Register the SPI client driver with SPI core or OF core with MODULE_DEVICE_TABLE().
  • Implementing probe and remove functions, and filling a struct spi_driver structure.
  • Implementing init and exit functions and registering with module_init() and module_exit().

Steps performed in the driver init.

  • Dynamically reserve a major and a range of minors with alloc_chrdev_region().
  • Create a class for our device with class_create(), visible as /sys/class/transceiver.
  • Register the SPI device driver using spi_register_driver().

Steps performed in the driver probe.

  • Initializing regmap and implementing custom read/write functions.
  • Reading device address to make sure communication with the device is successful.
  • Allocating memory for the device’s private data structure.
  • Register the device with the system using device_register().
  • Initialize GPIO’s for CE and IRQ. Configure interrupt by registering an interrupt handler.
  • Defining two wait queues for the Tx thread.
  • Initialize a kernel FIFO for handling Tx data.
  • Creating worker threads for handling ISR and Rx data.
  • Creating a sub-device and character file for pipe0 with a unique minor number.
  • Defining open, read, write, and poll file operations for the character file.
  • Initialize a kernel FIFO for handling pipe0 Rx data.
  • Defining two wait queues for the Rx thread.
  • Configuring NRF24l01p device with following
    • 2-byte CRC.
    • Auto Retransmission Delay – 4000us, Auto Retransmit Count – 15.
    • Power 2dBm and data rate 2mbps.
    • Enable auto ack.
    • Enable dynamic payload.
    • Enable Pipe0 for receiving.
    • Flush RX and Tx FIFO.
    • Power up.
  • After configuration NRF24l01p will be in receive mode.
  • Creating and waking the kernel thread to handle Tx data.

PTX operation

  • If there is a packet present in the TX FIFO the nRF24L01+ enters TX mode and transmits the packet.
  • The nRF24L01+ enters RX mode to receive an ACK packet after receiving TX_DS IRQ is asserted.

Steps performed in Tx thread.

static int nrf24l01p_tx_thread(void *data)
{
	struct nrf24l0_data *nrf24l01p = data;
	struct nrf24l0_tx_data txdata;
	struct nrf24l0_pipe *pipe0;
	int ret;

	while(true) {
		//Wait for data from user space.
		wait_event_interruptible(nrf24l01p->tx_wait_queue,
					 kthread_should_stop() ||
					 (!nrf24l01p->rx_active && !kfifo_is_empty(&nrf24l01p->tx_fifo)));

		if (kthread_should_stop())
			return 0;
		
		//Mutex to protect Tx FIFO
		if (mutex_lock_interruptible(&nrf24l01p->tx_fifo_mutex))
			continue;

		//Get data from TX FIFO
		ret = kfifo_out(&nrf24l01p->tx_fifo, &txdata, sizeof(txdata));
		if (ret != sizeof(txdata)) {
			dev_err(&nrf24l01p->dev, "get tx_data from fifo failed\n");
			continue;
		}
		mutex_unlock(&nrf24l01p->tx_fifo_mutex);
		pipe0 = txdata.pipe0;
	
		//Disable device
		nrf24_ce_pin(nrf24l01p, 0);

		//Set TX mode
		ret = nrf24l01p_set_mode(nrf24l01p, NRF24_MODE_TX);
		if (ret) {
			dev_err(&nrf24l01p->dev, "Mode set to Tx failed\n");
			goto gotoRX;
		}

		//Write data into FIFO
		ret = nrf24l01p_write_tx_pload(nrf24l01p, txdata.pload, txdata.size);
		if (ret < 0) {
			dev_err(&nrf24l01p->dev,
				"write TX PLOAD failed (%d)\n",
				ret);
			goto gotoRX;
		}

		//Enable device
		nrf24_ce_pin(nrf24l01p, 1);

		//Wait for ack
		nrf24l01p->tx_done = false;
		wait_event_interruptible(nrf24l01p->tx_done_wait_queue,
					 (nrf24l01p->tx_done ||
					 kthread_should_stop()));

		if (kthread_should_stop())
			return 0;

		pipe0->write_done = true;
		//Wake up write fops to wait for next Tx data
		wake_up_interruptible(&pipe0->write_wait_queue);

gotoRX:
		//Again go back to Rx mode
		if (kfifo_is_empty(&nrf24l01p->tx_fifo) || nrf24l01p->rx_active) {

			//enter Standby-I
			nrf24_ce_pin(nrf24l01p, 0);
			ret = nrf24l01p_set_mode(nrf24l01p, NRF24_MODE_RX);
			if (ret < 0) {
				dev_err(&nrf24l01p->dev,
						"Set rx mode failed (%d)\n",
						ret);
			}
			nrf24_ce_pin(nrf24l01p, 1);
		}
	}
	return 0;
}

PRX operation

  • In nRF24L01+ enters RX mode and starts searching for packets
  • If the packet is new the payload is made available in the RX FIFO and the RX_DR IRQ is asserted.
  • Then transmitter is acknowledged with an ACK packet.

Steps performed by RX worker

static void nrf24_rx_work_handler(struct work_struct *work)
{
	struct nrf24l0_data *nrf24l01p;
	u8 payload[NRF24L0P_PLOAD_MAX];
	struct nrf24l0_pipe *pipe0;
	int length;

	nrf24l01p = container_of(work, struct nrf24l0_data, rx_work);
	pipe0 = nrf24l01p->pipe0;

	//Check for data in device FIFO
	while(!nrf24l01p_is_rx_fifo_empty(nrf24l01p))
	{
		memset(payload, 0, NRF24L0P_PLOAD_MAX);
		//Read the data from FIFO
		length = nrf24l01_get_rx_payload(nrf24l01p, payload);
		if (length < 0) {
			dev_info(&nrf24l01p->dev, "payload invalid\n");
		}
		nrf24l01p->rx_active = false;
		//Mutex to protect FIFO
		if (mutex_lock_interruptible(&pipe0->rx_fifo_mutex))
			return;
		//Put the data into Kernel FIFO
		kfifo_in(&pipe0->rx_fifo, payload, length);
		mutex_unlock(&pipe0->rx_fifo_mutex);
		//Wake up read file operation
		wake_up_interruptible(&pipe0->read_wait_queue);
	}
}
  • When RX_DR IRQ asserts the ISR thread schedule the Rx worker thread.
  • The RX worker thread will check FIFO status.
  • If the FIFO has data then it will read the FIFO contents and write into kernel FIFO.
  • Then unblocks the read file operations so that received data is presented to the userspace.

File operation write

static ssize_t nrf24l01p_write(struct file *filp,
			   const char __user *buf,
			   size_t size,
			   loff_t *f_pos)
{
	struct nrf24l0_data *nrf24l01p;
	struct nrf24l0_pipe *pipe0;
	struct nrf24l0_tx_data txdata;

	pipe0 = filp->private_data;
	nrf24l01p = to_nrf24_device(pipe0->dev->parent);

	txdata.size = min_t(size_t, size, NRF24L0P_PLOAD_MAX);
	txdata.pipe0 = pipe0;

	memset(txdata.pload, 0, NRF24L0P_PLOAD_MAX);
	//Copy data from userspace to kernel buffer
	if (copy_from_user(txdata.pload, buf, txdata.size)) {
		dev_err(&nrf24l01p->dev,"%s\n","Data copy error\n");
		goto exit_lock;
	}

	//Mutex to protect kernel FIFO
	if (mutex_lock_interruptible(&nrf24l01p->tx_fifo_mutex))
		goto exit_lock;

	//Put the data into kernel FIFO
	if (kfifo_in(&nrf24l01p->tx_fifo, &txdata, sizeof(txdata)) != sizeof(txdata)) {
		dev_err(&nrf24l01p->dev,"%s\n","Fifo write error\n");
		goto exit_kfifo;
	}
	mutex_unlock(&nrf24l01p->tx_fifo_mutex);
	//Wakeup the Tx thread
	wake_up_interruptible(&nrf24l01p->tx_wait_queue);

	pipe0->write_done = false;
	//Wait for ack
	wait_event_interruptible(pipe0->write_wait_queue, pipe0->write_done);

	return size;
exit_kfifo:
	mutex_unlock(&nrf24l01p->tx_fifo_mutex);

exit_lock:
	wake_up_interruptible(&nrf24l01p->tx_wait_queue);
	return size;
}
  • Wait for data from userspace then copy data from userspace to the kernel buffer.
  • Write the copied data into kernel Tx FIFO.
  • Wake up the Tx thread.
  • Wait for ACK before receiving another data from userspace.

File operation read

static ssize_t nrf24l01p_read(struct file *filp,
			  char __user *buf,
			  size_t size,
			  loff_t *f_pos)
{
	struct nrf24l0_data *nrf24l01p;
	struct nrf24l0_pipe *pipe0;
	unsigned int copied, n;
	int ret;

	pipe0 = filp->private_data;
	nrf24l01p = to_nrf24_device(pipe0->dev->parent);

	//Check for data in Rx kernel FIFO
	if (kfifo_is_empty(&pipe0->rx_fifo)) {
		if (filp->f_flags & O_NONBLOCK)
			return -EAGAIN;
		//Wait for data in Rx FIFO
		wait_event_interruptible(pipe0->read_wait_queue,
					 !kfifo_is_empty(&pipe0->rx_fifo));
	}

	//Mutex to protect FIFO
	ret = mutex_lock_interruptible(&pipe0->rx_fifo_mutex);
	if (ret)
		return ret;

	//Copy data from Rx FIFO to userspace buffer
	n = kfifo_to_user(&pipe0->rx_fifo, buf, size, &copied);

	mutex_unlock(&pipe0->rx_fifo_mutex);

	return n ? n : copied;
}
  • Check for data in the kernel Rx FIFO.
  • Wait until data is present in Rx kernel FIFO.
  • If data is available copy data from kernel Rx FIFO to the userspace buffer.

DTS changes for IMX6SX-based Udoo neo board

&ecspi5 {                                                                       
        pinctrl-names = "default";                                              
        pinctrl-0 = <&pinctrl_spi4>;                                            
        status = "okay";                                                        
                                                                                
        nrf24: nrf24_radio@0 {                                                  
               compatible = "nordic,nrf24l01p";                                 
               reg = <0>;                                                       
               spi-max-frequency = <5000000>;                                   
               pinctrl-names = "default";                                       
               pinctrl-0 = <&nrf24_gpios>;                                      
               interrupt-parent = <&gpio5>;                                     
               interrupts = <20 IRQ_TYPE_EDGE_FALLING>; /* falling edge */      
               irq-gpio = <&gpio5 20 0>;                                        
               ce-gpio = <&gpio1 8 0>;                                          
               status = "okay";                                                 
        };                                                                      
};

&iomuxc {                                                                       
             nrf24_gpios: nrf_gpios {                                                
                fsl,pins = <MX6SX_PAD_GPIO1_IO08__GPIO1_IO_8 0x3079>,           
                           <MX6SX_PAD_RGMII2_TD2__GPIO5_IO_20 0xF879>;          
        };                                                                      
                                                                                
        pinctrl_spi4: spi4 {                                                    
                fsl,pins =                                                      
                           <MX6SX_PAD_NAND_DATA01__ECSPI5_MOSI  0x100b1>,       
                           <MX6SX_PAD_NAND_DATA00__ECSPI5_MISO  0x100b1>,       
                           <MX6SX_PAD_NAND_DATA02__ECSPI5_SCLK  0x100b1>,       
                           <MX6SX_PAD_NAND_DATA03__ECSPI5_SS0   0x100b1>;       
        };                                                                      
}; 

Sample output

Device-1
root@udoo:/home/udoo/nrf24# ./chat 
Usage:
1.By default application will be in receive
2.Enter data to transmit
Starting poll...
Embeddeddiaries
Transmitting data: Embeddeddiaries
Received data = Linux-kernel

Device-2
root@udoo:/home/udoo/nrf24# ./chat 
Usage:
1.By default application will be in receive
2.Enter data to transmit
Starting poll...
Received data = Embeddeddiaries
Linux-kernel
Transmitting data: Linux-kernel

The source code is available at GitHub: https://github.com/embeddeddiaries/nrf24l01p

Datasheet: http://www.sparkfun.com/datasheets/Wireless/Nordic/nRF24L01P_Product_Specification_1_0.pdf