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. 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