Downloading Received Messages#

This tutorial is the continuation of a series started with the article Introduction. The previous chapter was about Event Handling and Automatic State Transitions.

Please read them first if you haven't.

The completed example for this article is available on GitHub. You can either import this example, as documented in the readme on Github, or follow this article and modify the example from the previous article.


In the previous tutorial, we had both transmit and receive working, but we couldn't see what we received. This tutorial will solve that problem.

We're also adding calibrations, which is recommended for all projects.

You can find a SimpleTRX example in Simplicity Studio, which has a similar purpose. However, the result of this tutorial will be somewhat different. Mainly, this tutorial is not very pedantic on checking errors returned by all functions, to make it easier to understand. However, we highly recommend checking all possible errors, therefore using the SimpleTRX example in Studio as a reference.

Enabling Calibrations#

All calibrations are enabled by default in RAIL Utility, Initialization as depicted below, but require some application code to perform them.

RAIL utility calibrationRAIL utility calibration

First, we need to add the related event during initialization to enable it (in app_init.c):

RAIL_Handle_t app_init(void)
{
  //...
  RAIL_Status_t status = RAIL_ConfigEvents(rail_handle, RAIL_EVENTS_ALL,
                                           RAIL_EVENTS_TX_COMPLETION
                                           | RAIL_EVENTS_RX_COMPLETION
                                           | RAIL_EVENT_CAL_NEEDED
                                           );
  //...
}

Next, we need to perform calibrations when RAIL requests them (in app_process.c):

void sl_rail_util_on_event(RAIL_Handle_t rail_handle, RAIL_Events_t events)
{
  //...
  if (events & RAIL_EVENT_CAL_NEEDED) {
    RAIL_Status_t status = RAIL_Calibrate(rail_handle,
                                          NULL,
                                          RAIL_CAL_ALL_PENDING);
    if (status != RAIL_STATUS_NO_ERROR) {
      sl_led_toggle(&sl_led_led1);
    }
  }
}

This is all that's needed from an application to perform all calibrations available on EFR32, to have the maximum available performance. Calibrations are detailed further in another tutorial.

Preparing for Printing#

In the previous tutorials, we already enabled LEDs and buttons, but we'll need a more user friendly output: We need a serial console. To enable that, install the Log component under Application/Utility. This should pull up its dependencies, including an IO Stream: EUSART or UART component, which is the serial driver itself.

Finally, we need to enable Virtual COM UART on the Board Control component:

Enabling VCOMEnabling VCOM

This pulls up a GPIO, which will enable the USB-UART bridge on the development kit (on the motherboard, if you use a dual board development kit).

Receive FIFO#

We already configured Tx FIFO in multiple ways. RAIL also implements an Rx FIFO. By default, it's 512 Bytes, allocated by the RAIL library. We will use that, but it is configurable. See RAILCb_SetupRxFifo() and RAIL_SetRxFifo() in the API documentation for details.

By default, you cannot use the whole 512 Bytes, as RAIL adds a few bytes to each packet to store timestamp, RSSI, and similar information in the FIFO (sometimes we call this "appended info").

We're working on what we call packet mode. It's the simpler, and recommended way, but it's not possible to handle packets bigger than the receive FIFO with this method, in which case FIFO mode should be used. We return to that topic in a later tutorial.

The Packet Handle#

The FIFO is accessible with RAIL_RxPacketHandle_t variables or packet handles. Obviously, we don't have the handle of the packet we just received, but we have "sentinel" handles:

  • RAIL_RX_PACKET_HANDLE_NEWEST

  • RAIL_RX_PACKET_HANDLE_OLDEST

  • RAIL_RX_PACKET_HANDLE_OLDEST_COMPLETE

  • RAIL_RX_PACKET_HANDLE_INVALID

Note that RAIL_RX_PACKET_HANDLE_OLDEST and RAIL_RX_PACKET_HANDLE_NEWEST will always return with packet information. For example, OLDEST will return with one of the following:

  1. the oldest packet received, if available

  2. the packet the radio is receiving at the moment, if available and if no fully received packet is available

  3. the placeholder which will be used for the next packet the radio will receive, if no fully or partially received packet is available

If you don't care about upcoming or ongoing packets, you can use RAIL_RX_PACKET_HANDLE_OLDEST_COMPLETE. It will only return with fully received packets.

When to Read the FIFO#

Let's say we want to download the packet when it's fully received (although it's possible to access it during reception). We must let RAIL know this in the event RAIL_EVENT_RX_PACKET_RECEIVED. If we don't, RAIL automatically frees the memory allocated to the packet we just received.

We have two options:

  • Download the packet from the event handler

  • Hold the packet in the FIFO and download later

While the first one is simpler, it is desirable in most cases to keep large memory copy operations outside of interrupt handlers, therefore the attached example demonstrates the second method.

Note however, that if you plan to use dynamic multiprotocol, DMP, you must read the FIFO before yielding the radio. On every DMP protocol change, RAIL resets the receive FIFO. In this case, it's usually better to download the packet in the event handler.

The Event Handler#

In the event handler, we want to instruct RAIL to hold all successfully received frames:

void sl_rail_util_on_event(RAIL_Handle_t rail_handle, RAIL_Events_t events)
{
  //...
  if (events & RAIL_EVENTS_RX_COMPLETION) {
    if (events & RAIL_EVENT_RX_PACKET_RECEIVED) {
      if (RAIL_HoldRxPacket(rail_handle) == RAIL_RX_PACKET_HANDLE_INVALID) {
        sl_led_toggle(&sl_led_led1);
      }
      sl_led_toggle(&sl_led_led0);
    } else {
      sl_led_toggle(&sl_led_led1); // all other events in
                                   // RAIL_EVENTS_RX_COMPLETION are errors
    }
  }
}

The API RAIL_HoldRxPacket will also return a packet handler, but we only report an error if it did not. We don't need the handle itself, since we're going to use sentinel handles. Keep in mind that you must release all packets that were held; otherwise, you will lose part of your Rx fifo, essentially leaking memory. Note also that this API shouldn't be used when developing for dynamic multiprotocol (DMP).

Packet holdPacket hold

Download and Print#

First, let's create a buffer we can use to download the packet:

static uint8_t rx_buffer[256];

In app_process_action(), if there's something in the packet handle, let's download and print it:

void app_process_action(RAIL_Handle_t rail_handle)
{
  static RAIL_RxPacketHandle_t packet_handle;
  static RAIL_RxPacketInfo_t packet_info;
  static RAIL_RxPacketDetails_t packet_details;
  //...
  packet_handle = RAIL_GetRxPacketInfo(rail_handle,
                                       RAIL_RX_PACKET_HANDLE_OLDEST_COMPLETE,
                                       &packet_info);
  if (packet_handle != RAIL_RX_PACKET_HANDLE_INVALID) {
    RAIL_CopyRxPacket(rx_buffer, &packet_info);
    RAIL_Status_t status =
      RAIL_GetRxPacketDetails(rail_handle, packet_handle, &packet_details);
    if (status != RAIL_STATUS_NO_ERROR) {
      sl_led_toggle(&sl_led_led1);
    }
    status = RAIL_ReleaseRxPacket(rail_handle, packet_handle);
    if (status != RAIL_STATUS_NO_ERROR) {
      sl_led_toggle(&sl_led_led1);
    }

    app_log("RX");
    for (int i = 0; i < packet_info.packetBytes; i++) {
      app_log(" 0x%02X", rx_buffer[i]);
    }
    app_log("; RSSI=%d dBm\n", packet_details.rssi);
  }
}

Let's go through this, line by line:

First, RAIL_GetRxPacketInfo() will return with a valid packet_handle if we have a completely received frame. In that case, it also returns a usable packet_info which has the length of the received packet, and pointers to download it. Note that the length will include the full frame, and is always valid, whichever length configuration is used (fixed or variable).

Since the receive buffer is a ring buffer, the packet in the buffer might not be in a single sequential buffer, copying from that using memcpy() is a bit complicated: RAIL_CopyRxPacket() macro implements that.

The API RAIL_GetRxPacketDetails() will return some useful information of the packet, such as RSSI or timestamps.

Finally, we release the packet, using RAIL_ReleaseRxPacket().

Note that this method is safe, even if we receive a new packet while downloading one. That packet will be downloaded and printed when app_process_action() is called the next time.

Rx cycleRx cycle

It is important to note here that there is a subtle difference between RAIL_PACKET_HANDLE_OLDEST and RAIL_PACKET_HANDLE_OLDEST_COMPLETE.

RAIL_PACKET_HANDLE_OLDEST as well as RAIL_PACKET_HANDLE_NEWEST become valid at beginning of frame reception. As a result, it cannot be used for packets reception easily in an active loop.

The following tab clarifies the differences between the handles:

NEWEST

OLDEST

OLDEST_COMPLETE

FIFO Empty

INVALID

INVALID

INVALID

FIFO receiving packet A

Points to packet A

Points to packet A

INVALID

FIFO has received packet A

Points to packet A

Points to packet A

Points to packet A

FIFO receiving packet B

Points to packet B

Points to packet A

Points to packet A

FIFO receiving packet B and A is copied/released

Points to packet B

Points to packet B

INVALID

While this isn't exhaustive, it helps to clarify the ambiguity between all three handles.

Download in the Event Handler#

Downloading the packet in the event handler is essentially the same as what we did in the main loop above, except we don't need to worry about releasing:

if (events & RAIL_EVENT_RX_PACKET_RECEIVED) {
  RAIL_RxPacketInfo_t packet_info;
  RAIL_RxPacketHandle_t packet_handle = RAIL_GetRxPacketInfo(rail_handle, RAIL_RX_PACKET_HANDLE_NEWEST, &packet_info);
  RAIL_CopyRxPacket(rx_buffer, &packet_info);
  RAIL_Status_t status = RAIL_GetRxPacketDetails(rail_handle, packet_handle, &packet_details);
}

Note that packet_details should be a global variable (i.e. declared outside of the functions to be available to both functions) in this case.

Conclusion#

With this, you can use RAIL for basic transmit and receive, which concludes the getting started series. However, RAIL can do much more. You can continue with the basic tutorials.

API Introduced in this Article#

Functions#

Types and Enums#