NRF24L01

RF Ring oscillator with NRF24L01. Note: the cursors are measuring half a cycle.

This RF ring oscillator runs on the NRF24L01 using an Xmega 8E5 microcontroller running at 32 MHz. In the oscilloscope trace above, yellow and blue traces each represent a module. A pin is pulled high when the radio is active and low while the radio is switching modes. By measuring two periods of this waveform, we can determine the round trip time (two transmits and two receives).

Layout and PCB.

C code is available in the linked files (nrf-ftdi-ring.c, nrf-ftdi-ring.make, serial.h), or visible below.


#ifndef F_CPU
#define F_CPU 32000000UL
#endif

#include "serial.h"
#include 
#include 
#include 
#include 

#define SS_BM PIN4_bm
#define CE_BM PIN0_bm
#define IRQ_BM PIN2_bm

//NRF24L01 registers
//https://www.nordicsemi.com/eng/content/download/2726/34069/file/nrf24L01P_Product_Specification_1_0.pdf
//Every new command must be started by a high to low transition on CSN.
const uint8_t CONFIG = 0x00;
const uint8_t EN_AA = 0x01;
const uint8_t EN_RXADDR = 0x02;
const uint8_t SETUP_AW = 0x03;
const uint8_t SETUP_RETR = 0x04;
const uint8_t RF_CH = 0x05;
const uint8_t RF_SETUP = 0x06;
const uint8_t STATUS = 0x07;
const uint8_t OBSERVE_TX = 0x08;
const uint8_t RPD = 0x09;
const uint8_t RX_ADDR_P0 = 0x0A;
const uint8_t RX_ADDR_P1 = 0x0B;
const uint8_t RX_ADDR_P2 = 0x0C;
const uint8_t RX_ADDR_P3 = 0x0D;
const uint8_t RX_ADDR_P4 = 0x0E;
const uint8_t RX_ADDR_P5 = 0x0F;
const uint8_t TX_ADDR = 0x10;
const uint8_t RX_PW_P0 = 0x11;
const uint8_t RX_PW_P1 = 0x12;
const uint8_t RX_PW_P2 = 0x13;
const uint8_t RX_PW_P3 = 0x14;
const uint8_t RX_PW_P4 = 0x15;
const uint8_t RX_PW_P5 = 0x16;
const uint8_t FIFO_STATUS = 0x17;
const uint8_t DYNPD = 0x1C;
const uint8_t FEATURE = 0x1D;

const uint8_t PWR_UP = 1 << 1;
const uint8_t PRIM_RX = 1 << 0;
const uint8_t R_REGISTER = 0;
const uint8_t W_REGISTER = 1<<5;
const uint8_t R_RX_PAYLOAD = (1<<6) | (1<<5) | 1;
const uint8_t W_TX_PAYLOAD = (1<<7) | (1<<5);
const uint8_t FLUSH_TX = (1<<7) | (1<<6) | (1<<5) | 1;
const uint8_t FLUSH_RX = (1<<7) | (1<<6) | (1<<5) | (1<<1);
const uint8_t MAX_RT = 1<<4;
const uint8_t TX_DS = 1<<5; //tx data sent interrupt
const uint8_t RX_DR = 1<<6; //rx data ready interrupt


uint8_t read_register(uint8_t reg){
   PORTC.OUTCLR = SS_BM; //SS low      
   _delay_us(1);  //give time after ss low 
   SPIC.DATA = R_REGISTER | reg; while(!(SPIC.STATUS & SPI_IF_bm)) {};
   SPIC.DATA = 0; while(!(SPIC.STATUS & SPI_IF_bm)) {};
   uint8_t temp = SPIC.DATA;
   PORTC.OUTSET = SS_BM; //SS high
   _delay_us(1);  //give time after ss high
   return temp;
}
void write_register(uint8_t reg, uint8_t val){
   //must be in standby mode before calling this function!
   PORTC.OUTCLR = SS_BM; //SS low 
   _delay_us(1);     
   SPIC.DATA = W_REGISTER | reg; while(!(SPIC.STATUS & SPI_IF_bm)) {};
   SPIC.DATA = val ; while(!(SPIC.STATUS & SPI_IF_bm)) {};
   PORTC.OUTSET = SS_BM; //SS high   
   _delay_us(1);
}

USART_data_t USART_data;

uint8_t token = 0; //token to pass
uint8_t tempval = 0;
const int pll_delay_us = 130;
const int ce_delay_us = 10;

void check_registers(uint8_t id){
   //for debug only
   //enter standby so we can write to configuration register.
   PORTC.OUTCLR = CE_BM; 
   _delay_us(ce_delay_us);         

   PORTC.OUTCLR = SS_BM; //SS low  
   _delay_us(10);            
   SPIC.DATA = R_REGISTER | CONFIG; while(!(SPIC.STATUS & SPI_IF_bm)) {};
   uint8_t status = SPIC.DATA;
   SPIC.DATA = 0; while(!(SPIC.STATUS & SPI_IF_bm)) {};  //read
   uint8_t config = SPIC.DATA;
   PORTC.OUTSET = SS_BM; //SS high
   _delay_us(10); 

   usart_send_byte(&USART_data,id);
   usart_send_byte(&USART_data,status);
   usart_send_byte(&USART_data,config);
   usart_send_byte(&USART_data,token);
   usart_send_byte(&USART_data,10);
}

void rx_from_standby(){
   //call this from standby to enter rx mode
   //tempval = read_register(CONFIG);
   //write_register(CONFIG, tempval | PRIM_RX);
   write_register(CONFIG, 0x13); //replaces the spi read
   //set CE for at least 10 us 
   PORTC.OUTSET = CE_BM;
   //_delay_us(ce_delay_us); //is this necessary?
   //wait for pll to settle
   _delay_us(pll_delay_us);
}

void setup(){
   // set up clock
   OSC.CTRL = OSC_RC32MEN_bm; // enable 32MHz clock
   while (!(OSC.STATUS & OSC_RC32MRDY_bm)); // wait for clock to be ready
   CCP = CCP_IOREG_gc; // enable protected register change
   CLK.CTRL = CLK_SCLKSEL_RC32M_gc; // switch to 32MHz clock

   //set up usart
   PORTD.DIRSET = PIN3_bm; //TXD0
   PORTD.DIRCLR = PIN2_bm; //RXD0
   USART_InterruptDriver_Initialize(&USART_data, &USARTD0, USART_DREINTLVL_LO_gc);
   USART_Format_Set(USART_data.usart, USART_CHSIZE_8BIT_gc,
                     USART_PMODE_DISABLED_gc, 0);
   USART_RxdInterruptLevel_Set(USART_data.usart, USART_RXCINTLVL_LO_gc);
   //take f_sysclk/(BSEL+1) ~= f_baud*16 with zero scale.  See manual or spreadsheet for scale defs
   USART_Baudrate_Set(&USARTD0, 123 , -4); //230400 baud with .08% error
   USART_Rx_Enable(USART_data.usart);
   USART_Tx_Enable(USART_data.usart);
   //enable interrupts
   PMIC.CTRL |= PMIC_LOLVLEX_bm;

   
   PORTC.DIRSET = SS_BM; //set up SS pin on PC4
   PORTC.PIN4CTRL = PORT_OPC_WIREDANDPULL_gc; //wired AND and pull-up on SS
   PORTC.PIN2CTRL = PORT_OPC_PULLUP_gc; //pull-up on IRQ
   PORTC.DIRSET = CE_BM; //set up CE pin on PC0
   //set up spic.ctrl
   SPIC.CTRL   = SPI_PRESCALER_DIV4_gc |        /* SPI prescaler. */
                         (1 ? SPI_CLK2X_bm : 0) |    /* SPI Clock double. */
                         SPI_ENABLE_bm |                 /* Enable SPI module. */
                         SPI_MASTER_bm |                 /* SPI master. */
                         SPI_MODE_0_gc; //bits driven at falling edge, sampled at rising edge  
   SPIC.INTCTRL = SPI_INTLVL_OFF_gc;
   PORTC.DIRSET = PIN5_bm | PIN7_bm; //mosi and sck as outputs
   PORTC.OUTSET = SS_BM; //SS high   
   _delay_ms(5); //warm up

   //nrf config
   //turn off auto-retransmit
   write_register(SETUP_RETR,0);
   //turn off disable auto-acknowledge
   write_register(EN_AA,0);
   //set the PWR_UP bit in the CONFIG register to 1 to enter standby mode
   write_register(CONFIG,PWR_UP);
   _delay_ms(3);
   //set up data pipe 0 
   write_register(EN_RXADDR,1);
   //set data pipe 0 payload length to 1
   write_register(RX_PW_P0,1);
   //set data rate to 2 Mbps with high power
   //I think there may be a typo in the data sheet here.
   write_register(RF_SETUP,(0<<5) | (1<<3) | (1<<2) | (1<<1));
   //flush tx
   PORTC.OUTCLR = SS_BM; //SS low      
   SPIC.DATA = FLUSH_TX; while(!(SPIC.STATUS & SPI_IF_bm)) {};
   PORTC.OUTSET = SS_BM; //SS high
   _delay_ms(1);  //give time to start up 
   //flush rx
   PORTC.OUTCLR = SS_BM; //SS low      
   SPIC.DATA = FLUSH_RX; while(!(SPIC.STATUS & SPI_IF_bm)) {};
   PORTC.OUTSET = SS_BM; //SS high
   _delay_ms(1);  //give time to start up 
   // and mask MAX_RT interrupts on IRQ
   tempval = read_register(CONFIG);
   write_register(CONFIG,tempval | MAX_RT);
   //clear all interrupts
   tempval = read_register(STATUS);
   write_register(STATUS,tempval | MAX_RT | RX_DR | TX_DS);
   _delay_ms(1);

   sei();
   //enter standby so we can write to configuration register.
   PORTC.OUTCLR = CE_BM; 
   _delay_us(ce_delay_us);
   rx_from_standby(); //enter rx mode
}



void send_token(){
   //call this from standby1
   //put token in tx fifo
   PORTC.OUTCLR = SS_BM; //SS low
   _delay_us(1);           
   SPIC.DATA = W_TX_PAYLOAD; while(!(SPIC.STATUS & SPI_IF_bm)) {};
   tempval = SPIC.DATA; //get status while we have it
   SPIC.DATA = 0; while(!(SPIC.STATUS & SPI_IF_bm)) {};
   PORTC.OUTSET = SS_BM; //SS high
   _delay_us(1); 

   //tempval = read_register(CONFIG);
   write_register(CONFIG, tempval & (~PRIM_RX));
   //pulse CE 
   PORTC.OUTSET = CE_BM; _delay_us(ce_delay_us); PORTC.OUTCLR = CE_BM;
   //wait for pll to settle
   _delay_us(pll_delay_us);
   //wait until transmit complete
   while( PORTC.IN & IRQ_BM ){}
   //clear IRQ -- need to be in standby.
   tempval = read_register(STATUS);
   write_register(STATUS, tempval | TX_DS);
   //enter standby so we can write to configuration register.
   rx_from_standby(); //return to RX mode
}

void read_token(){
   //call this from rx
   //get token from rx fifo
   PORTC.OUTCLR = SS_BM; //SS low
   _delay_us(1);           
   SPIC.DATA = R_RX_PAYLOAD; while(!(SPIC.STATUS & SPI_IF_bm)) {};
   tempval = SPIC.DATA; //get status while we have it
   SPIC.DATA = 0; while(!(SPIC.STATUS & SPI_IF_bm)) {}; 
   token = SPIC.DATA;
   PORTC.OUTSET = SS_BM; //SS high
   _delay_us(1); 

   //transition from RX to standby1 (CE=0)
   PORTC.OUTCLR = CE_BM;   
   _delay_us(ce_delay_us);

   //clear IRQ
   write_register(STATUS, tempval | RX_DR);
   //check_registers(66);
}

int main(void) {
   setup();
   while(1){
      if ( !(PORTC.IN & IRQ_BM)){
         read_token(); //in standby
         token += 1; //increment token
         //check_registers(65);
         send_token();
      }
      //we use a signal on the usart to start the oscillation.
      if (USART_RXBufferData_Available(&USART_data)) {
         USART_RXBuffer_GetByte(&USART_data); //clear usart buffer so we only fire once.
         //transition from RX to standby1 (CE=0)
         PORTC.OUTCLR = CE_BM;   
         _delay_us(ce_delay_us);
         send_token(); 
         //check_registers(64);
      }
      //_delay_ms(10);
   }
}

ISR(USARTD0_RXC_vect){USART_RXComplete(&USART_data);}
ISR(USARTD0_DRE_vect){USART_DataRegEmpty(&USART_data);}

Most of the time is spent switching between TX and RX (one switch takes 130 us).

NRF24 Radio Modes.

Back