The goal of this project was to develop a software driver that would give students an easy-to-use, robust interface to the nRF24L01 radio.  The radio’s core capabilities—transmit and receive with auto-ack—have been implemented successfully and some features have been added that were unavailable in previous radio drivers.  There is still a lot of room for development, and I’ve listed some ideas for improving the driver in the section on future changes.

Packet Format

The driver requires students to use a specific packet format that must be modified to match the required application.  Each packet includes a 1-byte packet type identifier, a 2-byte timestamp, and a 29-byte payload.  The packet is always 32 bytes long (including headers), that being the radio’s maximum data payload width.  The payload format is defined by the programmer, and consists of a union of structures.  The union includes the payload structures that the application needs to transmit, and a 29-byte array to make sure that the packet structure is 32 bytes in length in case the payload structures are too short.  The packet structure is defined as follows:

typedef struct _rp
    PACKET_TYPE type;
    uint16_t timestamp;
    payloadformat_t payload;
} radiopacket_t;

The PACKET_TYPE enumeration and the payloadformat_t union are implemented by the application developer.  Here is an example:

typedef enum _pt
typedef union _pf
    uint8_t _filler[29];    // makes sure the packet is exactly 32 bytes long - this array should not be accessed directly.
    pf_type0_t type0;
    pf_type1_t type1;
} payloadformat_t;

In my code, the payloadformat_t union has two payload format structures that correspond to the TYPE0 and TYPE1 messages.  These are to be replaced by the application programmer.  The payload structures must be less than or equal to 29 bytes in length, or they will expand the packet structure beyond its 32 byte limit.

typedef struct _t0
     // Replace this structure
     uint8_t sensor_x_data;
     uint16_t sensor_y_data;
     uint16_t four_data_points[4];
} pf_type0_t;
typedef struct _t1
    // Replace this structure
    uint8_t message_code;
    uint8_t message[28];
} pf_type1_t;

All of these packet data structures are contained in packet.h.  The example packet structure is populated like this:

radiopacket_t packet;
packet.type = TYPE0;
packet.payload.type0.sensor_x_data = get_sensor_x();
packet.payload.type0.sensor_y_data = get_sensor_y();

Or, for packets of type 1, like this:

radiopacket_t packet;
packet.type = TYPE1;
packet.payload.type1.message_code = MESSAGE_CODE_X;
snprintf(packet.payload.type1.message, 28, "A string to be transmitted.");

The packet is transmitted using Radio_Transmit (see below).  The same structure is used as an argument to Radio_Receive as the destination memory to which the radio driver copies received packets.  The timestamp field is filled out by the driver, so the application doesn’t need to write to it.

Basic Usage

The basic use case for the radio is two or more stations sending packets back and forth in a low-noise environment with low throughput requirements.  The programmer can leave the radio defaults as they are, but I recommend configuring the receiver pipes and transmitter explicitly to make sure that the driver state matches the internal state of the radio, and that the configuration matches the programmer’s assumptions.  Before the driver can be used, the application must call Radio_Init:

void Radio_Init()

This will initialize the SPI module, power up the radio, and configure the register defaults.  It includes a 2 ms delay to give the radio time to power up.  It also checks the size of the radiopacket_t structure to make sure that it’s the correct size; if the structure is not 32 bytes, then the driver throws an RTOS error.  The Radio_Init function must be called after the radio powers up, either when the system starts up or when the transistor switch described in the hardware section is toggled.

Radio Configuration

The radio radio is configured using the Radio_Configure function:

void Radio_Configure(RADIO_DATA_RATE dr, RADIO_TX_POWER power)

This function configures the radio to transmit and receive at the given data rate, and to transmit at the given output power.

Rx Pipe Configuration

The radio has six pipes that are configured individually using the Radio_Configure_Rx function:

void Radio_Configure_Rx(RADIO_PIPE pipe, uint8_t* address, ON_OFF enable)

Calling this function configures the pipe specified in the first parameter.  It sets the pipe’s address, and enables or disables the pipe.  Pipes are always configured to use auto-ack, and always have a payload width of 32 bytes.  Pipes 0 and 1 are enabled by default and pipes 2-5 are disabled by default.  See the Rx pipe address register page for a discussion on addresses and for a list of default addresses.

Transmitting Data

An application can transmit a data block using the Radio_Transmit function:

void Radio_Transmit(uint8_t* address, radiopacket_t* payload, RADIO_TX_WAIT wait)

This transmits the given packet data to the given address.  If the wait argument is RADIO_WAIT, then the function will not return until the transaction has completed (i.e. the radio has generated the TX_DS or MAX_RT interrupt).  Otherwise the function will return immediately.  See the section below about task safety for more information on how to use this function safely.

Receiving Data

The application can copy received data from the radio using the Radio_Receive function:

RADIO_RX_STATUS Radio_Receive(radiopacket_t* payload)

This function copies the packet at the head of the radio’s Rx FIFO into the given memory space.  If the FIFO is empty, then the function returns RADIO_RX_FIFO_EMPTY without changing the contents of the payload argument.  If the FIFO still contains more packets after the head is read out, then Radio_Receive returns RADIO_RX_MORE_PACKETS.  Otherwise, it returns RADIO_RX_SUCCESS.

Transmission Failure Rate

The driver provides a function to estimate the packet failure rate:

uint8_t Radio_Packet_Drop_Rate()

This returns a percent value in the range from 0 to 100 in steps of about 6.  The radio keeps a history of the last 16 packets that were transmitted, and when the function above is called it calculates what percentage of those 16 packets were dropped.  When the driver starts it initializes all 16 history slots to success, so if packets are being dropped it will take 16 transmissions to approach the actual failure rate.  A dropped packet means that the transmitter repeated the packet up to the maximum number of times allowed without receiving the packet’s acknowledgement from the receiver (so a transmission reported as failed may still have been successful if the ack packet was lost, although that is unlikely).  Packet sent without auto-ack are assumed to have been successful, but as of this report the driver does not support sending with auto-ack.

For example, if 9 of the last 16 transmitted packets were dropped, then the function would return (9 * 100)/16 = 56.

Task Safety

I didn’t make the driver safe for multitasking when using the RTOS, because that adds needless complexity.  Instead, when using the RTOS, a periodic task should be created specifically for interacting with the radio.  Other tasks should control the radio task to transmit and receive, rather than controlling the radio directly.

The driver keeps track of the time it spends in its transmit and receive functions so that an application can estimate how much time it has left in the periodic task.  The following function returns the ceiling of the time spent in the driver, in milliseconds:

uint8_t Radio_Last_Call_Time()

See below for a table of normal times one can expect to encounter.  For example, if Radio_Receive was called, then Radio_Last_Call_Time will always return 1 ms.  If Radio_Transmit was called with RADIO_WAIT and the receiver was offline, then Radio_Last_Call_Time will return 8 ms.  The AT90’s 16-bit timer rolls over at 65,535 and is incremented at the CPU frequency, so its maximum sample time is about 8 ms.  The default driver will always work with this limitation, but if the retry delay or the maximum number of retries is increased then the timer will roll over more quickly and under some circumstances (such as dropped packets) Radio_Last_Call_Time will return inaccurate results.

An application that does not use the RTOS will be able to use the driver safely, although it will have to change a few lines in the driver to remove references to the RTOS.


Radio Operations
Time to complete Radio_Receive call 0.3 ms
Time to complete Radio_Transmit call (wait is RADIO_DO_NOT_WAIT) 1.5 ms
Time to complete Radio_Transmit call (wait is RADIO_WAIT, no retries) 3.3 ms
Time to complete Radio_Transmit call (wait is RADIO_WAIT and receiver is offline) 7.6 ms

These values were taken with the AT90 running at 8 MHz, SPI running at 4 MHz, the radio set to 2 Mbps, a 500 µs delay between retries, and up to 5 retries.