Working with Data Queues

Data queues are used for transferring packets from the RF core to the main CPU and vice versa. They are implemented as linked lists of queue entries. This document describes the available queue entry types and provides code snippets to create queues and to interact with them.

Queue Entry Types

The RF core supports 3 different kind of queue entries:

Name Data type Description
Single packet entry rfc_dataEntryGeneral_t One packet per entry; data after the header.
Pointer entry rfc_dataEntryPointer_t One entry per packet; data at another memory location.
Partial entry rfc_dataEntryPartial_t Stores packets with unknown or unlimited length; data after the header.

Only partial entries can be used for transmitting data. All other types are exclusively used together with RX commands.

All queue entries start with a common header part, containing a pointer to the next entry, a configuration field and the length of the entry in bytes.

../_images/aafig-b36588106f9ae56487538aea25f15dc8aba90e2d.png

It is very important that each queue entry is aligned to 4-byte boundaries. Otherwise the RF core will fail to access the queue entry.

Table 4. Description of rfc_dataEntry_t header fields.
Field Type Description
pNextEntry uint8_t* Points to the next queue entry.
status uint8_t

Current status of the entry.

  1. DATA_ENTRY_PENDING: The entry is not yet in use by the RF core. This status is written by the system CPU before submitting the entry.
  2. DATA_ENTRY_ACTIVE: The entry is currently opened for writing (RX) or reading (TX) by the RF core.
  3. DATA_ENTRY_BUSY: An ongoing radio operation is writing or reading an unfinished packet. Modifications are forbidden.
  4. DATA_ENTRY_FINISHED: The RF core has finished writing (RX) or reading (TX) data. The system CPU may now access the packet.
  1. DATA_ENTRY_UNFINISHED: For partial RX entries only. The RF has run out of entries and could not complete the packet.
config type uint8_t : 2

Specifies the entry type.

  1. DATA_ENTRY_TYPE_GEN: single packet entry
  2. Reserved
  3. DATA_ENTRY_TYPE_PTR pointer entry
  4. DATA_ENTRY_TYPE_PARTIAL: partial read RX

lenSz

uint8_t : 2

Specifies the size of the packet length field in the data section. This is used for variable packet length.

  1. No separate length info. Packet has a known size.
  2. One byte length field
  3. Two byte length field
  4. Reserved
irqIntv uint8_t : 4
  • Partial entries: The number of bytes between IRQ_RX_N_DATA_WRITTEN interrupts (RF_EventNDataWritten when using the RF driver). 0 means: 16 bytes.
  • ALl other entries: unused
length uint16_t
  • Single packet entries: Number of bytes in the data section.
  • Multi packet entries: Number of additional header bytes + buffer bytes in data section (not the packet size).
  • Pointer entries: Number of bytes in the data buffer pointed to.
  • Partial entries: Number of additional header bytes + buffer bytes in data section (not the packet size).

The current state of buffer entries during packet reception is kept in the status field. Figure Figure 26. shows a typical scenario where the RF core writes to a queue entry and the application reads the packet payload before it makes the entry writable again.

 @startuml
 scale 0.8

 state pending as "DATA_ENTRY_PENDING"
 state busy as "DATA_ENTRY_BUSY" : writePayload();
 state finished as "DATA_ENTRY_FINISHED\n(DATA_ENTRY_UNFINISHED)"

 [*] --> pending : initialized
 pending -> busy : syncword found
 busy --> finished : packet done OR entry full /\nIRQ_RX_ENTRY_DONE
 finished --> pending : application has\nfinished reading

 @enduml

Figure 26. The status field of queue entries during packet reception.

Single Packet Entries rfc_dataEntryGeneral_t

Single packet entries contain one packet per entry and store the data directly behind the header so that the whole queue can be allocated in a linear memory section. They are the simplest entry type and sufficient for many applications. The data section may contain the packet length, configured by config.lenSz, the payload and optionally appended meta data, like the RX timestamp or the CRC.

The length indicator at the begin of the data section may be omitted when the packet size can be determined otherwise. For packets with variable length it is necessary.

../_images/aafig-4b46122ac1b89736ff854a9f502906187efb0c70.png
Table 5. Description of additional rfc_dataEntryGeneral_t fields.
Field Type Description
rfc_dataEntry_t Generic header described in Table 4.
data uint8_t Dummy field representing the first byte in the data section.

Pointer Entries rfc_dataEntryPointer_t

A pointer entry is similar to a single packet entry, but doesn’t contain the data after the header. Instead, it holds a pointer to another memory location which contains the data. This queue type is useful when you want to:

  • keep the data storage separate from the queue entries share the same buffers
  • between different queues without recreating the queue structure move buffers
  • to another queue (for instance a custom queue implementation) without
  • copying the content
../_images/aafig-c91c29277d64c9d0cf9c182c03d9785755f87639.png
Table 6. Description of additional rfc_dataEntryPointer_t fields.
Field Type Description
rfc_dataEntry_t Generic header described in Table 4.
pData uint8_t* Pointer to the physical data storage location

Partial Entries rfc_dataEntryPartial

The proprietary PHY supports an entry type where the data can be accessed before the entire packet is received over the air. It can be used for the following purposes:

  • When data must be read before the entire packet is received. For instance,

when the packet contains a length field that is incompatible to the supported packet formats.

  • When the length of the packet is not known at the beginning of the packet.
  • When the length of the packet is too long for a single packet entry or when using unlimited packet length.

Partial entries may contain several packets. In this case, each packet in the data section starts with a length field of size lenSz which can be used to calculate the start of the next packet within the same entry. The field nextIndex contains the total number of written bytes in the entry.

Table 7. Description of additional rfc_dataEntryPartial_t fields.
Field Type Description
rfc_dataEntry_t Generic header described in Table 4.
pktStatus numElements uint16_t : 13 Number of packets committed to this entry.
bEntryOpen uint16_t : 1
  1. RF core access is finished.
  2. The Entry contains an element that is still accessed by the RF core.
bFirstCont uint16_t : 1
  1. The first element in the entry starts a new packet.
  2. The first element in the entry continues a packet from the previous entry.
bLastCont uint16_t : 1
  1. The last element in the entry marks the end of a packet.
  2. The last element in the entry will be continued in the next entry.
nextIndex uint16_t The total number of committed bytes in the data section. This field is constantly being updated by the RF core while receiving.
data uint8_t Dummy field representing the first byte in the data section.

The entry is updated as follows:

  • The nextIndex field is updated whenever a new bytes is written to the data section.
  • While a packet is being received, pktStatus.bEntryOpen is set to 1 by the RF core.

When an entry element is finished, either because the packet ended or because the element reached the end of the entry, pktStatus.bEntryOpen is set to 0 by the RF core and pktStatus.numElements is incremented. If the packet continues in the next entry, pktStatus.bLastCont is set to 1 by the radio CPU. In this case, the pktStatus.bFirstCont bit of the next entry is also set to 1 by the RF core. If no next entry is available, the status is set to DATA_ENTRY_UNFINISHED, otherwise it is set to DATA_ENTRY_FINISHED.

Each entry element starts with a length info field (depending on config.lenSz). If an entry contains a single packet, then the length info is equal to the packet length. If the packet doesn’t fit into the current entry, it is continued in the next entry. In this case, the length info contains only the number of bytes within the current entry.

For a partial read RX entry, the RF core generates an Rx_Data_Written interrupt to the system CPU whenever one or more bytes are written to the entry. In addition, it generates an IRQ_RX_N_DATA_WRITTEN interrupt when config.irqIntv bytes have been written since the last interrupt or since the start of the entry element. The RF driver propagates this interrupt as RF_EventNDataWritten.

Creating a Queue

In order to create a data queue, the following steps are necessary:

  1. Allocate sufficient memory for all queue entries.
  2. Initialize each queue entry header.
  3. Create a queue object

The object of the type dataQueue_t is defined in the header file <ti/devices/${DEVICE_FAMILY}/driverlib/rf_mailbox.h>:

// The queue structure
typedef struct {
   uint8_t *pCurrEntry;   // Points to the first entry, NULL for an empty queue
   uint8_t *pLastEntry;   // Pointer to the last entry, NULL for a circular queue
} dataQueue_t;

It holds a pointer to the first and to the last entry and may either have a fixed or unlimited size. In the former case pLastEntry points to the last entry. In a circular queue, pLastEntry is a null pointer.

The following example explains how to create a circular queue of 4 single- packet entries that can store up to 32 bytes.

  1. As a first step, we need to allocate sufficient memory for all queue entries:

    #include <ti/devices/DeviceFamily.h>
    #include DeviceFamily_constructPath(driverlib/rfc_data_entry.h)
    #include DeviceFamily_constructPath(driverlib/rfc_mailbox.h)
    
    #define BUFFER_ENTRIES      4
    
    // Must be word-aligned
    #define DATA_SECTION_SIZE   32
    
    // -1: Do not count the dummy data byte in the entry
    #define ENTRY_HEADER_SIZE   (sizeof(rfc_dataEntryGeneral_t) - 1)
    
    #define BUFFER_SIZE_BYTES   (BUFFER_ENTRIES * (ENTRY_HEADER_SIZE + DATA_SECTION_SIZE))
    
    // Align the buffer to word boundaries. Example for GCC.
    uint8_t buffer[BUFFER_SIZE_BYTES] __attribute__ ((aligned (4)));
    
All queue entries need to be word-aligned. When the expected payload size is not a multiple of 4, choose the next greater word-aligned value as DATA_SECTION_SIZE.
  1. Prepare the entry headers and close the ring:

    rfc_dataEntryGeneral_t item = (rfc_dataEntryGeneral_t*)&buffer[0];
    for (uint8_t i = 0; i < BUFFER_ENTRIES; i++)
    {
        item->config.type = DATA_ENTRY_TYPE_GEN;
        item->length = DATA_SECTION_SIZE; // Note: When creating partial items, add 4
                                          // bytes for the additional header fields.
    
        item->status = DATA_ENTRY_PENDING;
        item->pNextEntry = ((uint8_t*)item) + ENTRY_HEADER_SIZE + DATA_SECTION_SIZE;
    
        if (i == (elements - 1))
        {
            // Close the circle for the last item
            item->pNextEntry = buffer;
        }
    
        item = (rfc_dataEntryGeneral_t*)item->pNextEntry;
    }
    
  2. Create a circular queue object:

    dataQueue_t queue = {
        .pCurrEntry = &buffer[0];
        .pLastEntry = NULL;
    };
    

Reading Data from a Queue

Whenever the RF core completes a packet, it raises the interrupt IRQ_RX_ENTRY_DONE which maps to the event RF_EventRxEntryDone in the RF driver. When handling the interrupt and executing the callback, the packet data is read from entry and the status field must be set back to DATA_ENTRY_PENDING before the RF core can re-used it.

Keep the current entry in a variable:

// Initialize to the first entry
rfc_dataEntryGeneral_t* rxEntry = (rfc_dataEntryGeneral_t*)queue.pCurrEntry;

Read a packet from a single packet entry in an RX queue. This can either be done within a RF driver callback or in the RX task:

// The current entry to be read.
rfc_dataEntryGeneral_t* rxEntry;

// Packet starts with 1 byte length information (lenSz = 1)
uint8_t packetLength      = *(uint8_t*)(&rxEntry->data);

// Payload follows.
uint8_t* packetDataPointer = (uint8_t*)(&rxEntry->data + sizeof(packetLength));

// Correct: Use memcpy to read the payload from the buffer.
uint32_t myValue;
memcpy(&myValue, packetDataPointer, sizeof(myValue));

// Dangerous: De-reference potentially misaligned pointers in the packet payload:
// myValue = *((uint32_t*)packetDataPointer);

Set rxEntry to the next queue item for the next iteration:

// Mark the entry as being read
((volatile rfc_dataEntryGeneral_t*)rxEntry)->status = DATA_ENTRY_PENDING;

// Get the next entry
rfc_dataEntryGeneral_t* rxEntry =  ((rfc_dataEntryGeneral_t*)rxEntry->pNextEntry);

Warning

When interacting with queue entries while the RF core is accessing the entry as well, you must make sure that the value being read by the system CPU is up-to-date. For instance when writing loops, the compiler might cache values in a register, especially when turning on optimization:

// Wrong code: Might cause a deadlock
rfc_dataEntryGeneral_t* entry;
while (entry->status != DATA_ENTRY_FINISHED) {}

Force the compiler to reload fields with concurrent access from memory on every access:

// Correct: status is being reloaded from memory on every access
rfc_dataEntryGeneral_t* entry;
while (((volatile rfc_dataEntryGeneral_t*)entry)->status != DATA_ENTRY_FINISHED) {}