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