On 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
First, let's load PA factory calibrations: Open the configuration for the component RAIL Utility, Power Amplifier, and turn on the Enable PA Calibration switch.
All other calibrations are enabled by default in RAIL Utility, Initialization, but require some application code to perform them. First, we need to add the related event during initialization to enable it (in app_init.c):
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 following components:
IO Stream: USART on vcom (the text to serial driver)
Tiny printf (the printf implementation we recommend) - note that you should use #include "printf.h" to use it, not stdio.h!
IO Stream: Retarget STDIO (to connect the two)
Finally, we need to enable Virtual COM UART on the Board Control component:
This pulls up a GPIO, which will enable the USB-UART bridge on the WSTK.
Receive FIFO
We already configured Tx FIFO multiple ways. RAIL also implements an Rx FIFO. By default, it's 512B, 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 512B, 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 512B 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:
the oldest packet received, if available
the packet the radio is receiving at the moment, if available and if no fully received packet is available
the placeholder which will be used for the next packet the radio will receive, if not 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.
The Event Handler
In the event handler, we want to instruct RAIL to hold all successfully received frames:
void sl_rail_app_on_event(RAIL_Handle_t rail_handle, RAIL_Events_t events)
{
//...
if ( events & RAIL_EVENTS_RX_COMPLETION ) {
if ( events & RAIL_EVENT_RX_PACKET_RECEIVED ) {
RAIL_HoldRxPacket(rail_handle);
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 ignore that 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.
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 handle, let's download and print it:
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() inline function 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.
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:
Note that packet_details should be a global variable as well 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 from the table of contents site.
In the previous tutorials, we set up RAIL to do various tasks through API commands. However, RAIL can inform the application on various events almost real time (basically, wiring interrupts through RAIL). So let's improve what we had in the previous tutorial.
The Event Handler Function
You might have already seen this function in app_process.c:
This is the event handler function, which is called by the RAIL library, and routed through the RAIL Utility, Initialization component.
The events variable is a bit field, holding all possible events. Be careful, this bit field does not fit into a 32-bit variable, which is the size of the int on our MCUs.
The event handler should be prepared for multiple events in a single call. There's no "clearFlag" mechanism, each event will be signaled once, and the application is responsible for handling it.
Keep in mind that almost all events are generated from interrupts, so the event handler is usually running from interrupt context (i.e., interrupts are disabled). Therefore, the event handler should run fast; if you have to do something that takes more time, set a flag, and do it from the main loop.
You should be also careful with RAIL interrupt safety. Calling state changing APIs (RAIL_Start*, RAIL_Stop* or RAIL_Idle) from the interrupt handler is allowed, but we recommend to read the interrupt safety article before doing so.
Configuring Events
The events enabled by default are configured on the RAIL Utility, Initialization component (under 'Platform/Radio', press 'Configure'):
However, using the RAIL API directly is actually simpler than using the event configuration capabilities of this component. The API for this is RAIL_ConfigEvents(). You can keep the event setup enabled, it doesn't really matter.
RAIL_ConfigEvents() has a mask and an events parameter, making it simple to configure multiple events at the same time; e.g., to enable or disable a single event, leaving the others untouched:
However, you only need to do this in rare cases. Enabling events is not like unmasking interrupts: Enabling them will not trigger the event handler on past events.
Tx Error Handling
In the last tutorial, we sent out a frame, but we didn't check the results because it would have needed the event handler. Let's turn on led0 on success and led1 on any error (we'll be turning led1 on when transmitting and clearing led1 if no Tx error occurs).
First, for the LEDs, enable the Simple LED component with led0 and led1.
In app_process.c (to get access to the led instances, now declared in autogen/sl_simple_led_instances.h):
#include "sl_simple_led_instances.h"
For the actual radio related changes, we'll need to enable the success and error events. We can do that during init since we don't really need to disable anything at runtime for such simple applications:
Next, let's check for errors returned by the StartTx call:
RAIL_Status_t status = RAIL_StartTx(rail_handle, 0, RAIL_TX_OPTIONS_DEFAULT, NULL);
if ( status != RAIL_STATUS_NO_ERROR ) {
sl_led_toggle(&sl_led_led1);
}
Finally, let's check the events for success and errors:
void sl_rail_app_on_event(RAIL_Handle_t rail_handle, RAIL_Events_t events)
{
(void)rail_handle;
if ( events & RAIL_EVENTS_TX_COMPLETION ) {
if ( events & RAIL_EVENT_TX_PACKET_SENT ) {
sl_led_toggle(&sl_led_led0);
} else {
sl_led_toggle(&sl_led_led1); //all other events in RAIL_EVENTS_TX_COMPLETION are errors
}
}
}
With this, you should see led0 toggling on every transmitted packet.
Receiving Packets
Let's add receive capabilities to this application! Again, use led0 to report success and led1 for errors.
First, we need to modify the init code.
In app_init.c (to get access to the led instances):
#include "sl_simple_led_instances.h"
We need RX success/error events enabled, and we need to start the radio in RX mode:
RAIL_Handle_t app_init(void)
{
//...
RAIL_ConfigEvents(rail_handle, RAIL_EVENTS_ALL,
RAIL_EVENTS_TX_COMPLETION | RAIL_EVENTS_RX_COMPLETION);
RAIL_Status_t status = RAIL_StartRx(rail_handle, 0, NULL);
if ( status != RAIL_STATUS_NO_ERROR ){
sl_led_toggle(&sl_led_led1);
}
}
The API RAIL_StartRx() is similar to Tx: the only important option is the second one, which is the channel.
Finally, let's check the events for success and errors (in app_process.c):
void sl_rail_app_on_event(RAIL_Handle_t rail_handle, RAIL_Events_t events)
{
//...
if ( events & RAIL_EVENTS_RX_COMPLETION ) {
if ( events & RAIL_EVENT_RX_PACKET_RECEIVED ) {
sl_led_toggle(&sl_led_led0);
} else {
sl_led_toggle(&sl_led_led1); //all other events in RAIL_EVENTS_RX_COMPLETION are errors
}
}
}
Auto state transitions
By default, after receiving or transmitting a packet, RAIL will switch to idle mode (i.e., turn off the radio). Since we want it to keep receiving future packets, we can use RAIL's auto state transition feature. This can be configured on the RAIL Utility, Initialization component, let's set all dropdowns to RX:
We're basically saying that after the packets are received/transmitted, we want to return to Rx. These configurations can be also changed in run-time, using the RAIL_SetRxTransitions() and RAIL_SetTxTransitions() APIs.
If you compile and flash the program after this modification, you should see that now both WSTK toggles led0 on every button press.
Testing and Conclusion
If you install this example to two boards (make sure you attach the antennas if you use sub-GHz kits on sub-GHz config), you will see that on the press of btn0 on either device, led0 on both devices toggle. However, we can't see what we're receiving, since we never actually download the messages.
In this tutorial, we're going to modify the project Flex (RAIL) - Empty Example to transmit a packet. We're going to use the default, fixed length 16Byte payload configuration first.
This tutorial references some more advanced articles - we don't recommend reading them yet if you just started learning RAIL. They are only linked to highlight the connection.
Code snippets in this example are for illustration only. Refer to the attached files for reference code.
Buffer Handling
Setting Up the Packet Buffer
RAIL requires a buffer (usually called FIFO) to be configured for Tx. First, we have to allocate some memory for that buffer:
Note that you can't use any arbitrary size; EFR32 creates a FIFO ring buffer on this memory area, and it can only handle buffer sizes of power of 2 between 64 and 4096 (i.e., 64, 128, 256, 512, 1024, 2048, or 4096).
The tx buffer can hold more than one packets, which can be useful if you need to send out a lot of messages quickly.
To load the payload of the packet to the buffer, we have two options: write it via RAIL APIs or write to the memory directly.
The memcpy() is just a standard C instruction to write to buffers, and we use RAIL_SetTxFifo() to pass that buffer to RAIL. We also tell RAIL how long the buffer is, and how much data it has already in it.
Writing to the Buffer Indirectly
(See the attached app_init.c and app_process.c files for reference code)
In app_init.c:
#define BUFFER_LENGTH 256
static uint8_t tx_buffer[BUFFER_LENGTH];
RAIL_Handle_t app_init(void)
{
// Get RAIL handle, used later by the application
RAIL_Handle_t rail_handle = sl_rail_util_get_handle();
RAIL_SetTxFifo(rail_handle, tx_buffer, 0, BUFFER_LENGTH);
return rail_handle;
}
In this case, we pass the buffer to RAIL when it's still empty, and we write to that buffer using a RAIL API. Note that using memcpy() instead of WriteTxFifo would not work: while it would write the memory itself, it wouldn't change the read and write pointers of the FIFO, handled by RAIL, so RAIL would still think the buffer is empty.
Direct or Indirect
There are four main factors that could decide which method to use to write the FIFO:
With indirect mode, the buffer can be used for partial packets. It is simpler to use for multiple packets.
Calling RAIL_SetTxFifo() does not move any memory, just sets a few register, while calling RAIL_WriteTxFifo() does need some time to copy the payload to the buffer.
Calling RAIL_SetTxFifo() is not allowed during transmission.
Indirect mode very clearly separates init and process code.
For simpler applications, RAIL_SetTxFifo() can be simpler. If you want to send the same packet over and over (or only change a few bytes in it), it's much better, since you don't have to write the whole packet again.
On the other hand, the separation in RAIL examples between app_init and app_process works very nicely with the indirect method, and in most real cases, you need to move memory anyway. If you want to send longer packets than your buffer, you must use RAIL_WriteTxFifo(), as you can call it during transmit. It's also useful if you want to send out a lot of packets: Use RAIL_WriteTxFifo() to load each message one after the other, then send them out quickly without any buffer operation.
The attached code uses indirect mode mainly for the clear separation.
FIFO Reset
The last parameter of RAIL_WriteTxFifo can reset the FIFO, which means it will invalidate the data already in there. This is also possible with RAIL_ResetFifo. We generally recommend not to use it, a well-written code shouldn't need it (as it should only store in the FIFO what should be sent out), and it might mask bugs. Resetting the FIFO also takes extra time.
This instructs RAIL to start transmitting the data stored in the FIFO on channel 0 (second parameter). The third parameter can change various options, like using the second configured sync word (see RAIL API reference). The last parameter is only required for DMP (dynamic multiprotocol) mode.
Changing the Packet Length
You can change the configured fixed length on the Radio Configurator, but that's obviously not possible at runtime, which is often needed. Note that the amount of data loaded into the FIFO does not matter as long as it's equal to or greater than the length of the frame.
Changing Packet Length with a Fixed Length Configuration
With the API RAIL_SetFixedLength(), it's possible to change the pre-configured length (stored in rail_config.c) at runtime. Note that this changes the length of the packets on the Rx side as well. If you want to return to the pre-configured value, you can use RAIL_SetFixedLength(railHandle, RAIL_SETFIXEDLENGTH_INVALID).
Using Variable Length Configurations
A lot of protocols store the length of the packet in the header of the frame. For example, in IEEE 802.15.4, the first byte stores the length (and the maximum is 127). To set this up, use the following settings in the Radio Configurator:
Set Frame Length Algorithm to VARIABLE_LENGTH
This automatically enables the Header
Set the length of the header to 1
Set variable length bit size to 8
Set the maximum length to 127
For more information on this setup, and on more advanced variable length configs, see AN1253. Note that the above configuration is not fully IEEE 802.15.4 compatible to make it simpler.
Length decoding works the same way for both rx and tx. This means that during Tx, we have to make sure that the committed length in the header matches the number of bytes you load into the FIFO. It also means that if we set the length field to more than 127, we will get a transmit error.
This would be a valid 16B frame, both for the above described variable length and 16 Byte fixed length mode, so this is used in the attached sample code:
We use PAYLOAD_LENGTH-1 in the length field, since the 1B length field itself shouldn't be counted.
The API RAIL_SetFixedLength() is available in variable length mode, and it changes the radio to fixed length operation (again, both for rx and tx). Calling RAIL_SetFixedLength(railHandle, RAIL_SETFIXEDLENGTH_INVALID) will restore it to variable length mode.
Setting up the example
Button handling
The attached example sets the tx fifo on init, loads, and sends the packet on any button press. This is implemented using the Simple Button component on btn0, and implementing its callback:
In app_process.c (to get access to the button instance, now declared in autogen/sl_simple_button_instances.h):
#include "sl_simple_button_instances.h"
Also add the button callback code (the callback is already declared in sl_button.h):
By default, the Simple Button is configured for PF6, which matches the WSTK PB0 button
Since this callback is in interrupt context, we avoid using RAIL API directly. It is safe in almost all cases, but calling RAIL_StartTx() from interrupt context should be done carefully. Instead, we set the volatile send_packet variable to true, and call RAIL APIs from app_process_action() only:
RAIL is short for Radio Abstraction Interface Layer, and is the most direct interface available for EFR32 radios. You can think of it as a radio driver. While this sounds like a restriction, this actually makes software development for proprietary wireless much simpler:
EFR32 has a very flexible radio, which also makes it quite complex. RAIL (and the radio configurator) provides an easy-to-use interface of EFR32's features.
The various generations of EFR32s are slightly different, but the RAIL API is the same (except maybe the chip specific updates), which makes hardware updates almost seamless.
We plan to add RAIL support to all of our future wireless MCUs.
RAIL application development is currently possible in the Flex SDK in Simplicity Studio. To progress further with this tutorial, please remember to open the RAIL API documentation.
When to Use RAIL?
If you have an existing protocol, and you must be compatible with it, you'd probably have to use RAIL (as all of our stacks use a standardized frame format).
If you want to use sub-GHz communication, you have a few options, including RAIL. For simple point to point communication, RAIL might be the simplest solution. However, as soon as you need security or addressing, it might be better choose a protocol stack, such as Connect.
RAIL also has the benefit that it adds the least amount of delays to all radio events: In fact, some event notification will happen in interrupt context.
What is covered by RAIL?
RAIL only supports what the radio hardware supports. For example, auto ACK and address filter is available, as the hardware provides support for it. On the other hand, while security is important for wireless communication, it's not really a radio task, hence RAIL does not support it. The crypto engine is an independent peripheral that can be used to encrypt payloads before giving them to RAIL and the radio hardware.
Supported Energy Modes
EM1p or higher is required for the radio to be operational (i.e., Transmitting, receiving, or waiting for packets, RAIL timer running from HF clock). On devices without EM1p support, EM1 is required for the radio. See AN1244: EFR32 Migration Guide for Proprietary Applications for more details on EM1p.
EM2 or higher is required for RAIL scheduling or timers (Running from LF clock, which needs configuration, a topic in Tutorial 5).
EM3 or higher is required for RAIL to work without re-initialization.
Writing Code from Scratch
We do not recommend writing code from scratch because it is much simpler to have something existing that sets up the include paths and linker settings correctly. The best way to start is the example Flex (RAIL) - Empty Example.
RAIL Components
The empty application includes the following RAIL related components (under 'Platform/Radio'):
RAIL Library, Single Protocol, which is the library itself
RAIL Utility, Power Amplifier, which configures and initializes the power amplifier
RAIL Utility, Initialization, which initializes RAIL and loads an initial configuration
There are a few components that you might want to install
RAIL Utility Packet Trace Information, which enables PTI, the hardware that feeds data to Network Analyzer
Radio Utility, Front End Module, which can be used to drive a FEM (or external PA/LNA), if you HW uses one
If you work on a starter kit, you don't need to change the configuration of most of these components (for custom hardware, there will be a new article coming soon), except the Initialization component. However, the initialization component also includes a good starting point, that we don't need to modify for simple applications.
The Radio Configurator
The Radio Configurator is accessible by configuring the component Advanced Configurators/Radio Configurator. The usage of the radio configurator is out of scope for this tutorial series. For more details, see AN1253: EFR32 Radio Configurator Guide.
The project structure
Projects always include the following parts:
autogen folder: Only the autogen folder includes generated code. It includes the PHY configuration (rail_config.c), init code, the linker script, and other generated code used by components, like the command descriptors for the CLI interface
config folder: Component configuration headers are placed into this folder. These can be edited with the Component Editor that can be opened on the Project Configurator via the Configure button, but directly editing the header file is also possible.
gecko_sdk folder (with version number): Source and binary files added by components
files in the root folder: Only the application specific files should be in the root folder, including source files, the project configurator (.slcp file) and the Pin tool (.pintool file)
Note that all files related to the project should be visible in the project explorer, including header and library files.
All projects include main.c, which is not recommended to modify. Instead, add the initialization code to app_init.c, and implement the main loop in app_process.c. This way, the System components can initialize components, and call the "process" function of the components that requires this. Additionally, enabling an RTOS will convert app_process's main loop into an RTOS task.
Conclusion
This project is ready to send out frames. We're going to do that in the next part. You can also find other tutorials from the Table of Content site.
RFSense is a low power feature of the EFR32 Wireless MCU family. It can "wake up" an MCU from its EM2 or even EM4 power modes. Practically, it is an ultra low power interrupt source, running on ULFRCO clock.
The RFSense is a wide band circuit, it can detect energy in the 100MHz - 5 GHz frequency range, filtered only by the matching network of the RF front end. This is an advantage, as no need for separate PCB components. But it’s also a drawback: it is sensitive to any kind of interferer signal as well.
EFR32xG22 has an updated RFSense module, which improves the performance compared to EFR32 Series 1 in multiple ways:
RFSense works below 0C degree
RFSense works if voltage is scaled down
Introduces Selective mode
Legacy mode
In legacy mode, EFR32xG22 RFSense is fully compatible with the one in Series 1. This means that if the RFSense module detected energy for a configured time, it generates an interrupt.
Selective mode
Selective mode mitigates the unfiltered nature of RFSense. Instead of simply detecting energy for a given time period, it detects "a pattern of energy", which is essentially an OOK packet. The packet is Manchester coded, uses fixed 1kbps bitrate, 1B preamble and 1-4B sync word (no payload added). This packet can be transmitted by any OOK capable device, including all EFR32 wireless MCUs (Series 1 and Series 2).
Selective mode offers 2 configuration options to select from: "optimized for sensitivity" or "optimized for noisy environment".
The Wakeup Packet
The wakeup packet is a fixed-configuration OOK packet with the following settings:
Starts with a 1 B preamble (always 0x55).
Followed by a 1-4 B sync word.
No payload required.
Both the preamble and sync word are transmitted LSB first.
1 kbps bitrate (before coding).
Recommended carrier is 2.45 GHz.
Selective Mode Transmit Example Without Using API
Assume you select 0xb16e as your sync word, and you want to transmit it with only a signal generator (or a simple radio with MSB-first byte handling and no Manchester coder).
First, flip the endianness of both preamble and sync word: 0x55 becomes 0xaa and 0xb16e becomes 0x768d.
The full packet is then 0xaa768d, which after Manchester coding becomes 0x99996a6995a6.
Configuring this encoded packet and transmitting on 2.45 GHz with high enough TX power should wake up a device configured for selective RF Sense with the 0xb16e sync word
The function RAIL_Idle() takes a mode argument, with which you select one of the four available methods of idling the radio to use. Though the API documentation briefly describes each mode, it might be difficult to understand their differences without seeing examples and usecases. This document aims to help you more clearly understand each mode.
General recommendations
In most cases, RAIL_IDLE is the recommended mode to use with this API. RAIL_IDLE_ABORT is helpful when you want to also abort an ongoing ("active") tx or rx operation.
On the other hand, RAIL_IDLE_FORCE_SHUTDOWN is not recommended for use, and RAIL_IDLE_FORCE_SHUTDOWN_CLEAR_FLAGS should be handled with care.
RAIL_IDLE
The mode RAIL_IDLE turns off the radio after the current operation is finished. It also cancels any transmit or receive scheduled in the future. Current operations that won't be aborted include:
active transmit, which starts when the first bit (usually preamble) is transmitted and ends when the last bit (usually CRC) is transmitted.
active receive, which starts at sync word detect, and ends when the last bit is received (which depends on the decoded length of the packet).
RAIL_IDLE_ABORT
The mode RAIL_IDLE_ABORT works the same as RAIL_IDLE, but it will also abort active operations. However, RAIL_IDLE_ABORT will always wait for a stable state before turning off the radio, e.g. if the radio was preparing to enter rx mode and is waiting for the PLL to lock, RAIL will wait until the rx state is reached before idling the radio.
RAIL_IDLE_FORCE_SHUTDOWN
Unlike RAIL_IDLE_ABORT (which waits for a stable radio state), RAIL_IDLE_FORCE_SHUTDOWN immediately forces the radio off. As this is an abrupt transition, it may corrupt data in the receive or transmit buffers. This buffer corruption can only be resolved by completely clearing the FIFO - which loses data, and can also consume additional time.
In our experience, it's almost always slower than RAIL_IDLE_ABORT, so the costs typically outweigh the benefit of using RAIL_IDLE_FORCE_SHUTDOWN (except when recommended by our support team for diagnostic purposes).
RAIL_IDLE_FORCE_SHUTDOWN_CLEAR_FLAGS
The mode RAIL_IDLE_FORCE_SHUTDOWN_CLEAR_FLAGS works the same as RAIL_IDLE_FORCE_SHUTDOWN, but it also clears any pending events. For more details on scenarios where this can be useful, see the article on RAIL interrupt safety.
In embedded software development, some of the most complicated debug challenges are caused by calling non-reentrant functions in interrupt context. Hence, it's in the developer's best interest to carefully design their application to avoid these scenarios.
To do so, however, requires sufficiently detailed knowledge of the interrupts and API functions - which are not accessible in a closed-source product like RAIL. This document aims to provide the information required to develop interrupt- and thread-safe applications in RAIL.
Thread Safety
In an application without a task scheduler, only an interrupt request can interrupt the main program. If you use a preemptive scheduler, like the scheduler available in most embedded OSes (including Micrium OS), higher priority tasks can interrupt lower priority tasks as well. Regardless, when looking at the RAIL APIs, the same concerns are present in either case:
Is it safe to interrupt this API?
Is it safe to call this API from a thread/interrupt which interrupted something?
The Event Handler
RAIL uses an event handler, which is set up by RAIL_Init(). In our examples, it's usually called RAILCb_Generic(). This function is called by the RAIL library, and it's almost always called from an interrupt handler. This means the event handler should be used with care:
It should be kept in mind that interrupts are disabled when the event handler is running, so the function must not take long to return
More importantly, the function might be interrupting the main loop (or some other task)
Note that the first point above might not be completely true if interrupt priorities are used, in which case only interrupts at the same and lower priorities are disabled. However, the event handler will never be interrupted by another event handler as all RAIL interrupts must be used at the same priority.
General Rules for the RAIL API
First, let's collect the general rules of the API, and we'll detail exceptions in later points:
Calling any RAIL API from the main thread (or a single OS thread) is safe.
Calling any API from multiple threads is unsafe, except for DMP.
Calling most APIs from an interrupt handler is safe (see exceptions below).
Dynamic Multiprotocol (DMP)
In general, if you have a multi-threaded application, you should use RAIL from a single thread. The exception to this guidance is DMP, where in most cases each protocol runs in its own thread. In this scenario, using RAIL from each thread is safe, as each protocol has its own railHandle. So, a more generalized wording of rule 2 is:
Calling any API from multiple threads is only safe if each thread has a dedicated railHandle, and each thread only accesses RAIL with its own handle.
The few APIs that don't use railHandle - like RAIL_GetTime(), RAIL_Sleep(), or RAIL_Wake() - can be called from any thread.
Interrupt Safety in general
In general, calling an API which changes the radio state (i.e. between rx, idle and tx) can be risky. The simplest way to write interrupt safe application is to not call state changing APIs from any interrupt handler, including the RAIL event handler. This can be achieved by setting a flag or changing a state variable in the event handler instead of calling an API directly:
typedef enum {
S_IDLE,
S_START_RX,
S_START_TX,
} state_t;
volatile state_t state;
volatile RAIL_Time_t lastEvent;
int main(){
//init code
state = S_START_TX;
while(1){
switch(state){
case S_START_TX:
RAIL_StartTx(railHandle, 0, RAIL_TX_OPTIONS_DEFAULT, NULL);
state = S_IDLE;
break;
case S_START_RX:
RAIL_StartRx(railHandle, 0, NULL);
state = S_IDLE;
break;
default:
break;
}
}
return 0;
}
void RAILCb_Generic(RAIL_Handle_t railHandle, RAIL_Events_t events)
{
lastEvent = RAIL_GetTime();
if ( events & RAIL_EVENTS_TX_COMPLETION ){
state = S_START_RX;
RAIL_SetTxPower(railHandle, 200);
}
}
Note that some RAIL API was called from the event handler, but none of those were state changing APIs.
Interrupt safety with state changing APIs
In some (usually time critical) cases however, it's not possible to avoid calling state changing APIs from the event handler (or other interrupt handler). State changing APIs are not always risky: Some APIs might be safe, as long as they don't interrupt another specific API.
Hence, in the following list, we identify the risky API after first specifying which initially-running (i.e., "interrupted") API makes it risky (and how). We've included in this list some interrupt combinations that might be "safe", but the end result is not predictable - i.e. the radio might be in rx or in idle, depending on which API is called first.
Interrupting RAIL_Start<something>() with another RAIL_Start<something>() is risky, especially if they would start on different channels.
Interrupting RAIL_Idle(handle, <something>, true) with any RAIL_Start<something>() is risky.
Interrupting RAIL_Idle(handle, <something>, false) with any RAIL_Start<something>() is safe, but the end result is not predictable (i.e. the radio will either be in Idle, or start the requested operation).
Interrupting RAIL_Start<something>() with RAIL_Idle() is safe but the end result is not predictable, and might cause strange events (see the next section for details).
Interrupting RAIL_StopTxStream() with any RAIL_Start<something>() is very risky (the radio might remain in test configuration and start transmitting/receiving).
Interrupting RAIL_StopTx() is safe. Interrupting RAIL_StopTx() with RAIL_Start<something>() is safe but the end result is not predictable (i.e. the radio will either be in Idle, or start the requested operation).
Interrupting anything with RAIL_StopTx() is safe (see next section for important clarification). Interrupting RAIL_StartTx() with RAIL_StopTx(handle, RAIL_STOP_MODE_ACTIVE) is safe, but not predictable.
Interrupting anything with RAIL_StopTxStream() is safe. Interrupting RAIL_StartTxStream() with RAIL_StopTxStream() is safe but not predictable.
RAIL_Idle in the Event Handler
Calling RAIL_Idle() or RAIL_StopTx(handle, RAIL_STOP_MODE_ACTIVE) from the event handler might cause strange results. For example, let's say you're receiving on a channel and want to detect preambles using the event RAIL_EVENT_RX_PREAMBLE_DETECT and RAIL_EVENT_RX_PREAMBLE_LOST. The following scenario may unfold:
Preamble lost interrupt is received, so (at least) other radio interrupts are temporarily disabled.
You enter the event handler with RAIL_EVENT_RX_PREAMBLE_LOST.
At this point, the radio detects a preamble. The interrupt is logged, but the handler cannot run since the interrupts are masked.
Still in the event handler, you decide to turn off the radio with RAIL_Idle(railHandle, RAIL_IDLE_ABORT, true).
The radio turning off will generate a preamble lost interrupt.
The radio is now off, and you return from the event handler.
Interrupts are enabled again, so the pending preamble detect interrupt handler starts running.
You enter the event handler with RAIL_EVENT_RX_PREAMBLE_DETECT and RAIL_EVENT_RX_PREAMBLE_LOST both set at the same time
So you end up with a preamble detect event, even though the radio is off. This is usually harmless, since you always have the _LOST or _ABORTED event as well - but this demonstrates why your design must carefully consider in what order to handle events.
The easiest way to avoid this conflicted outcome is to disable the events that might cause problems when turning off the radio.
Another way to avoid this issue is to use RAIL_Idle(handle, RAIL_IDLE_FORCE_SHUTDOWN_CLEAR_FLAGS, true), which will clear the pending interrupts. However, using RAIL_IDLE_FORCE_SHUTDOWN_CLEAR_FLAGS has other drawbacks. It does force the radio state machine to idle state, and it might corrupt the transmit or receive FIFOs - in which case it must clear them, losing all data that might already be in there. It could also take more time to finish running than RAIL_IDLE_ABORT.
Critical Blocks
One usual way to avoid internal safety issues is to create critical (a.k.a. atomic) blocks, in which interrupts are disabled, in the main thread to make sure some code segment is never interrupted. However, this can create other problems, so it should be used carefully. There's no general rule to avoid this kind of "collateral damage", but here's an example that should be avoided:
RSSI averaging is running, and just before it finishes, we interrupt it with RAIL_StartTx() which is called from a critical block. The following race condition could happen:
We enter the critical block, interrupts are disabled.
RSSI averaging done interrupt is received, but the interrupt handler won't start since interrupts are masked.
StartTx turns off the radio, prepares it for transmit, then starts transmitting.
We leave the critical block, interrupts are enabled again.
RSSI averaging done interrupt handler runs at this point which will turn off the radio, aborting the current transmit.
One way to avoid the problem above is to clear interrupts in the critical block. This can be done by using RAIL_Idle(handle, RAIL_IDLE_FORCE_SHUTDOWN_CLEAR_FLAGS, true) at the beginning of the critical block, but the drawbacks of doing so (mentioned above) should be kept in mind. In general, it's better to avoid risky interrupts without using critical blocks in the main thread.
Using FORCE_SHUTDOWN
In the two sections above we mentioned two usecases where RAIL_IDLE_FORCE_SHUTDOWN_CLEAR_FLAGS can be useful. In general however, RAIL_IDLE or RAIL_IDLE_ABORT is a sufficient and preferred way to stop transmitting/receiving - therefore the FORCE_SHUTDOWN modes should be only used when they are really needed (as in the specific scenarios described here). For more details, see the article on the idle modes.
(*) Make sure to read this one, as DMP is often misunderstood and used in applications that could be better served by multi-PHY. We recommend that you (at least) browse through the other tutorials before reading this (DMP) tutorial, since DMP affects most APIs in one way or another.
Note that this tutorial will not provide you with all of the knowledge required for DMP development. Rather, it will summarize what's already documented, and highlight differences between RAIL single protocol and multiprotocol operations.
Some chapters below are not important if you need RAIL-Bluetooth DMP, but even in that case most chapters are still relevant because they describe RAIL development in a DMP environment.
Note that this tutorial is slightly outdated in Studio v5, since the we now have components, not plugins, and the way we initialize RAIL in the examples are performed by the RAIL Utility, Initialization component, and not by the application. However, these are minor details and the wast majority of this article is accurate.
What is Dynamic Multiprotocol
Dynamic multiprotocol time-slices the radio and rapidly changes configurations to enable different wireless protocols to simultaneously operate reliably in the same application.
A typical usecase involves a RAIL-based proprietary protocol and the Silicon Labs Bluetooth stack running on the same chip at the same time. Since Bluetooth communication is periodic and therefore very predictable, other protocols can use the radio between Bluetooth communication bursts. However, it's also helpful to use DMP when all involved protocols are proprietary RAIL-based, to benefit from the code clarity provided by the separate RAIL instances.
From a firmware perspective, RAIL-DMP is similar to an object-oriented programming (OOP) class. It provides the radio driver, and multiple instances can be "created". Each instance can use the driver in the same way, and do so almost independently.
However, from a hardware perspective the chip has only a single instance of the radio. As such, RAIL-DMP is similar to multitasking: the same resource must be shared among multiple "tasks" demanding its time - this sharing is managed by the radio scheduler.
The Radio Scheduler
The radio scheduler's job is to give (or revoke) radio hardware access to (or from) the protocols running on it. To accomplish this it implements cooperative, preemptive multitasking.
It's cooperative: Protocols should tell the scheduler when and for how long they need the radio, and protocols should yield the radio when not using it anymore
It's preemptive: Protocols should define priority to each radio task, and the scheduler will abort tasks if a higher priority radio task must run
Note that the radio scheduler schedules only radio tasks. Although DMP is often used with an RTOS (like Micrium OS), RTOS tasks and Radio tasks - despite the common name - are very different terms. To make this less confusing, our documentation sometimes uses radio operation instead of radio task.
For the radio scheduler to calculate the time required for a given radio operation, it must know how much time it takes to change radio access from one protocol to another. Therefore, in DMP, a protocol change always takes 450us on EFR32xG1x, which accommodates the worst-case protocol change time. This makes DMP protocol change much slower than Multi-PHY PHY change.
Protocol change time in RAIL 2.5.x (Flex 2.4.x) and earlier was 750us (and it might change further in the future).
Setting up a project for RAIL-RAIL DMP
If you need RAIL+Bluetooth DMP, follow AN1134. This chapter describes how to set up DMP in a RAIL-only project.
To use DMP, you should first switch to the multiprotocol RAIL library, under plugins. Since DMP has a bigger memory footprint (both RAM and code), it's provided in a library which is not enabled in single protocol examples.
Next, you'll need PHY configurations for your protocols. You can either use Protocol Based Multi-PHY (see the Multi-PHY usecases and examples tutorial for details), or you can use embedded protocols, e.g. IEEE 802.15.4 2.4GHz 250kbps by calling RAIL_IEEE802154_Config2p4GHzRadio().
Micrium OS
Micrium OS is not technically required for RAIL DMP, although it's highly recommended to clearly separate protocols (and for RAIL+Bluetooth DMP, we actually only support Micrium-enabled projects.) Hence, Micrium OS will not be discussed in this article - it doesn't impact how you use RAIL APIs in RAIL-only DMP.
RAIL initialization
Let's see how one RAIL protocol should be initialized in DMP:
railSchedState is a new static variable required for RAIL. This keeps the state of the protocol when it's not active (e.g. configured Rx options)
railCfg.protocol is set to NULL. This is also a DMP specific field, but it's only used by the Silicon Labs Bluetooth stack.
RAILCb_Generic is now static: Since all protocols will have their own callback functions, we should either make the function names unique, or put them in different C files as static. Although technically it's possible to use a common callback function for all protocols, it's not recommended as the readability of the code is much better with separate event handlers.
halInit() is no longer called from initRadio1() - since we have multiple protocols to support, it makes more sense to set up RAIL requirements elsewhere in a protocol-independent init function
This code snippet initializes a single protocol. However, all remaining protocols can be initialized in the same way, noting that railHandle, railSchedState and RAILCb_Generic should be different for all protocols (probably by placing in separate C files).
From this point you can use RAIL almost the same way as in single protocol mode. Since almost all APIs have a railHandle argument, RAIL will know which protocol you want to access. However, you should be careful with:
protocol independent APIs, i.e. the ones that don't have a railHandle argument
APIs that affect the state of the radio scheduler, i.e. schedule or stop a radio task
Protocol independent APIs
There are a few APIs that always work the same way, regardless of which protocol they were called from:
RAIL_SetTime()/RAIL_GetTime() - the timebase (which is also used for timestamping) is common for all protocols. It is not recommended to call RAIL_SetTime() in a DMP application (and calling it in a single protocol application can still be dangerous)
RAIL_ConfigMultiTimer() - MultiTimer mode is a requirement in DMP, so it's enabled by default and cannot be disabled
RAIL_Sleep()/RAIL_Wake() - Sleep management should be handled protocol-independently (e.g. in the idle task of the RTOS)
There are some other APIs that don't require railHandle, but they usually need a packet handler or timer handler, and so are still tied to a protocol.
Scheduling a finite radio task
The following APIs will trigger the radio scheduler to schedule a finite length new radio task:
RAIL_ScheduleRx()
RAIL_StartTx()
RAIL_StartScheduledTx()
RAIL_StartCcaCsmaTx()
RAIL_StartCcaLbtTx()
RAIL_StartAverageRssi()
(Note that RAIL_StartRx() and RAIL_StartTxStream() is not listed, as these as these are not finite length tasks.)
All the above APIs have the optional argument RAIL_SchedulerInfo_t. In single protocol mode, this argument is ignored, and can be NULL. In multiprotocol mode, this is the base of the scheduling, with its 3 member:
priority - 0 to 255, 0 being the highest. Equal priority does not preempt
slipTime - maximum time the task can be delayed due to other protocols, in us
transactionTime - the time the task takes, in us
When the radio hardware is available, none of these scheduler info arguments are used. However, if two tasks are scheduled at the same time, the radio scheduler will try to stagger the operations using slipTime in order to satisfy both tasks. If that fails, the lower priority task will not get access to the radio.
It is also possible that an application schedules a high priority radio task when a conflicting lower priority task has already started. In that case, the lower priority task will be aborted (possibly in the middle of frame transmission).
If RAIL_SchedulerInfo_t is NULL, the scheduler will assume all parameters to be 0, i.e. highest priority, with no slipTime - it is highly recommended to not use this feature, and it's only mentioned here to simplify debugging.
Each protocol can schedule at most a single finite task.
After each radio task the application must yield the radio. That can be achieved by calling RAIL_YieldRadio() or RAIL_Idle() - the latter also turns off the radio, which should have already occurred for finite tasks, so it's recommended to use RAIL_YieldRadio(). Manual yielding is required because the scheduler doesn't know about any automatic follow-up tasks (like ACKs) - you might want to yield immediately after receiving a packet, or you may instead want to wait for the auto ACK to go out.
Since yielding the radio will also call the radio scheduler to start/schedule the next radio task, you must first capture everything you need that might be overwritten by another protocol after yield, even in interrupt context. For example, timestamps of a transmitted packet must be accessed before yielding.
A Tx operation scheduled in the future can be cancelled using RAIL_Idle() or RAIL_StopTx(), where the latter doesn't yield the radio. An Rx operation scheduled in the future can only be cancelled using RAIL_Idle().
Background Rx
RAIL_StartRx() receives special handling in the radio scheduler, as it schedules an infinite task - namely, background Rx. The main difference when compared to finite tasks is that an infinite task sets the "default" state of the radio. It can be aborted by higher priority finite tasks, but it will be resumed automatically after the higher priority task is finished. For background Rx, this practically means that the radio will automatically return to Rx after Tx or packet reception, and you don't have to use auto state transitions like you do in single protocol mode. (Currently, the only infinite task supported by RAIL is background Rx).
Keep in mind that RAIL does not store the channel of background Rx: if you interrupt background Rx with a finite task on a different channel, the channel of the background Rx will change as well. E.g. if you're receiving in channel 0, then transmit on channel 1, the radio will "return" to receiving on channel 1. To change the background Rx channel back to 0, simply call RAIL_StartRx() again after you yield the radio.
RAIL_StartRx() has the same RAIL_SchedulerInfo_t configuration as the finite tasks, but depends only on the priority member - which, in general, should be the lowest priority used by the protocol.
Background Rx can be aborted using RAIL_Idle(), which also yields the radio. In DMP mode, it's recommended to only use RAIL_Idle() to stop background Rx.
It is also possible to change the priority of the background RX using RAIL_SetTaskPriority() - this is useful e.g. to increase the priority during packet reception, or after a particular packet.
TxStream
RAIL_StartTxStream() is called a debug task: A debug task must have the highest priority, because it can't be aborted. Otherwise, it works similarly to infinite tasks.
This API is different from all others as it doesn't use RAIL_SchedulerInfo_t. Since a stream can't really be aborted, the radio scheduler will handle this with the highest priority and no slip time.
Stream can be stopped with RAIL_StopTxStream(), RAIL_Idle() or RAIL_YieldRadio(), although it is recommended to use RAIL_StopTxStream() for clarity.
Auto state transitions
You might wonder, "what's the point of auto state transitions in DMP, if background Rx changes the default state to Rx?" The added value of auto state transitions is that the resulting state will inherit the original state's priority.
For example, you probably want the ACK reception on the same priority as the transmission itself, so you can set RAIL_SetTxTransition() to set up receive after the transmission for the ACK, and configure a timeout for it using RAIL_SetStateTiming().
If you have automatic state transition set up, you should only yield the radio after the whole operation (e.g. ACK reception or timeout) is finished. It's highly recommended to have automatic timeouts to prevent a high priority infinite task, but if you haven't set up timeouts, you can cancel the receive and yield the radio using RAIL_Idle().
To summarize:
In RAIL single protocol, the default state is always idle, and auto state transitions should be used to always return the radio to Rx. In DMP, background Rx should be used for this.
In single protocol mode, Rx state started by auto state transition is the same as Rx state started by StartRx. In DMP, auto state transitions are intended for ACK, as they inherit the (usually high) priority of the preceding task.
RAIL includes a timer virtualization service, called MultiTimer. Since its memory footprint is not negligible, this feature is disabled by default in single protocol mode. However, in multiprotocol mode, it is always enabled, since multiple timers are required for the multiple protocols. Therefore, using MultiTimers in your protocol implementations has no drawback in DMP.
Error handling
In single protocol RAIL, you generally need to places to handle errors:
RAIL_StartXYZ()/RAIL_ScheduleXYZ() might return an error. If no error were returned, the operation was either finished, or an event will be triggered
The event handler, where either a success or error event can be triggered, e.g. RAIL_EVENT_TX_PACKET_SENT or RAIL_EVENT_TX_CHANNEL_BUSY
In DMP, there's a third error case to handle above the previous two: You should always handle RAIL_EVENT_SCHEDULER_STATUS, where you might receive an error by calling RAIL_GetSchedulerStatus(). It's usually obvious which API call failed, e.g. RAIL_SCHEDULER_STATUS_SINGLE_TX_FAIL means a RAIL_StartTx() failed, but it might not be possible to know the original call resulted in error.
This was implemented because when you call e.g. RAIL_StartTx, the radio scheduler just creates task. When that task is running, it will actually call the single protocol RAIL_StartTx, and if that returns an error, it will trigger an RAIL_EVENT_SCHEDULER_STATUS event.
The event RAIL_EVENT_SCHEDULER_STATUS is also used when a higher priority task interrupts an ongoing radio operation, in which case, RAIL_SCHEDULER_STATUS_EVENT_INTERRUPTED will be returned by RAIL_GetSchedulerStatus().
Debugging
When debugging DMP, Rx/Tx PRS channels are still very useful, see the tutorial about debugging for details.
For DMP based code, writing out to a GPIO when a protocol is scheduled/unscheduled is really useful. This can be easily done by setting a GPIO on RAIL_EVENT_CONFIG_SCHEDULED and clearing it on RAIL_EVENT_CONFIG_UNSCHEDULED.
Recommendations and practices
Although these practices are very important for DMP applications, you should consider applying them in single protocol applications as well, to simplify a potential future port to DMP:
Only use RAIL_Idle() when it's necessary - In RAIL 1.x almost all APIs required that they be called from idle mode. This requirement was removed in RAIL 2.x, so it's very rarely needed, and since RAIL_Idle() also yields the radio, it should be very rarely used in DMP.
Use the scheduling Rx/Tx features as much as possible - i.e. don't use timers and start Rx/Tx at timer interrupt, use the corresponding RAIL schedule API instead. This helps the radio scheduler to resolve conflicts with the configured slipTimes, or at least promptly let the application know about an unavoidable conflict.
Set the slipTime/transactionTime correctly. Again, this helps the radio scheduler to resolve conflicts.
How not to use DMP
Using multiple DMP instances for multiple radio tasks (e.g. one protocol for advertising, one for connection, or one for Tx, one for Rx) is bad design: while it works, each protocol instance has a significant memory footprint, and switching time between protocols is much slower than switching Rx/Tx inside a protocol. At the moment, it is recommended to use DMP only for serving separate protocol stacks.
Using DMP to handle multiple PHYs for the same protocol stack is also not recommended: Multi-PHY is a much better solution for that, see the introduction to Multi-PHY and Multiprotocol tutorial for more details.
Related documentation
UG305: Dynamic Multiprotocol User's Guide - You can skip the chapters that won't affect your design, e.g. if you don't plan to use zigbee, you can skip the zigbee chapters. This article somewhat overlaps with UG305 as we provide a short summary of the basics here.
If you find any conflicts between this article and the above documents, give those documents priority as they will more quickly receive updates to track new RAIL DMP features and guidance.
If you find any contradiction between the tutorials and the API documentation, just always remember that the API documentation is more accurate.
This tutorial series focuses on the embedded code running on Wireless Geckos. Configuring the radio correctly is equally important, see AN971 for details on that for Studio v4 and AN1253 for Studio v5.
The tutorial series is currently under review and update for the Studio v5 updates. Although RAIL itself haven't changed much, some critical tools did. For details, see AN1254. The tutorials are mostly accurate for both v4 and v5. On those articles where an update will be needed in the near future, a note is added to the first couple of paragraphs.
Getting Started
You should start with these tutorials. All others are based on these, as these are the minimum to develop useful applications on RAIL.
RAIL Utility, Initialization: how it works, how to init RAIL without it - coming soon (studio5 specific)
Power manager with RAIL - coming soon (studio 5 specific)
Advanced Tutorials
These tutorials describe features that are rarely needed - they are available and can be used when necessary, but you probably won't use them all in your application. Generally, you should read them only if you think you need them.
These tutorials are about the Multi-PHY and multiprotocol capabilites of RAIL. If you plan to use that feature, we recommend to read the introduction then what you need, depending on your application.
In this article, we go through a couple of example usecases and their setups using the Multi-PHY radio configurator. If you haven't already, please read Introduction to Multi-PHY and Multiprotocol first to understand the various Multi-PHY and Multiprotocol capabilities of the Wireless Geckos. Some basic understanding of radio configs and the radio configurator is also required.
This tutorial includes videos (without audio) to guide you through the setup on the Configurator GUI.
Note that the videos on this tutorial were recorded on Studio v4, and Studio v5 follows a slightly different configurator workflow. Until the videos are updated, you can find details of the new workflow in AN1253.
Channel-based Multi-PHY
The theory
The main idea behind Channel-based Multi-PHY is to create channel groups, each of which can have different PHYs. Changing between these channel groups is handled by RAIL: from the RAIL API, you can load the required PHY just by selecting a channel.
Channel groups can be created with the icon. By default, these new channel groups have no PHY configuration.
A PHY configuration can be added using the icon. A PHY configuration exposes elements of the radio configurator for each of the channel groups in the selected protocol. The PHY applied to any specific channel group can then be customized by changing the values for each element present in the PHY configuration.
Once you add something to the channel-based configs (as described above), the protocol-based setup will be disabled.
Different bitrate
Let's say you need to work on the same 915MHz frequency, but on both 100kb/s and 500kb/s (with deviation 50kHz and 250kHz, respectively). Typically, this is used as an extension of a protocol which originally only supported the lower bitrate: The default bitrate is 100k, but it's possible to switch to 500k after a handshake.
In the above setup, channel 0 uses 100kb/s bitrate, while channel 1 uses 500 kb/s. Note that the channels are on the exact same frequency, but their configuration is slightly different - you might call these virtual channels. From RAIL, you can change between these virtual channels just as regular channels: If you call RAIL_StartRx(railHandle, 0, NULL) it will search for 100k packets, while RAIL_StartRx(railHandle, 1, NULL) will search for 500k packets.
Different packet
Let's say your protocol defines a variable length data packet (1st byte is length) and a fixed, 2B long ACK packet.
In the above setup channel 0 is for data packets, while channel 1 is for ACK packets (both Tx and Rx) - on the same frequency, so these are virtual channels just like in the previous example. A transmitting device would send a data packet on channel 0, then start listening on channel 1 for ACK, while a receiving device would receive on channel 0, and transmit an ACK on channel 1 after successful reception.
Note that the only difference between the PHYs are the length decoding algorithm itself. This is because the configurator can keep a complete fixed length and a variable length configuration, and changing between those is possible by just changing the the frame length algorithm.
Asymmetric protocol (Wireless M-Bus T mode)
Wireless M-Bus T mode defines a very different protocol based on the direction:
Meter -> Other (M2O): 868.95MHz, 3of6 encoding, 100kb/s
Other -> Meter (O2M): 868.3MHz, Manchester encoding, 32.768kb/s
The above example implements the "Other device" (typically collector): It receives M2O on channel 0 and transmits O2M on channel 1.
Transmitting modeT M2O messages requires other tricks, see AN1119 for details
Wide bandwidth for CSMA/CA
Some regulatory standards also standardize the bandwidth for CSMA/CA, but your protocol would have better sensitivity with a narrower bandwidth. The trick here is to understand that this is actually a subset of the Asymmetric protocol usecase: Since CSMA/CA will listen on the same channel that you will use for transmit and the bandwidth setting in the configurator only affects receive performance, you can define a virtual channel with your setup with the required wider bandwidth:
In the above example, channel 0 uses the bandwidth recommended by the configurator, while channel 1 has the fixed, wide bandwidth. You would receive on channel 0 with high sensitivity, and transmit on channel 1. The CSMA/CA preceding the transmit would also happen on channel 1 using the fixed, wide bandwidth.
You can check the bandwidth configured for channel 0 in the efr32_radio_configurator_log.txt file: It's 4.8kHz.
Uneven channel spacing
Channel groups can be used in "single-PHY" mode as well: one example for that is setting up uneven channel spacing. Let's say you have to receive on 868.0MHz, 868.33MHz and 868.4MHz - so the channel spacing is 330kHz and 70kHz at the same time - You obviously can't configure this with a single channel spacing field, but you can with channel groups:
With the above setup, you will have channels 0, 1 and 2 on the required frequencies.
Power limited channels
This is still a "single-PHY" usecase: Let's say you have to receive on 40 channels, starting at 2.402GHz with channel spacing of 1MHz. However, to meet regulatory standards, you must lower the transmit power on the first and last channel to 16dBm.
The above setup meets the requirements. Note that channel 0 and 39 are both defined twice. First as limited power, then the same as the other 38 channels. The order of channel groups are important: RAIL always loads the first channel group which includes the requested channel.
Channel number offset was used in the above video. The frequency of a given channel is calculated as follows:
Channel number offset is the same as First channel number if it's disabled. This is useful for uneven channel spacing, since you want the channel group to start on the selected base frequency, and not offset by firstChannelNumber*channelSpacing. However, setting it to 0 is useful for overrides: This way, the power limited channel 39 in the above config will be exactly the same as in the original channel group. See understanding RAIL config files tutorial for more details.
You also have to keep in mind that the actual output power depends on your board and antenna design. The recommended way to solve this problem is to create a PA conversion curve documented in AN1127, and enable the PA conversions plugin with the generated header:
Simplicity Studio ships with conversion curve headers for Silicon Labs boards - if you create a project for a Silicon Labs board, simply enable the PA Conversions plugin to include the correct header for your board.
Note that RAIL only lowers and limits the output power on channel change, it won't increase it back if you switch to an unlimited channel from a limited one.
In general, you should disable all advanced settings if you change much in a configuration - the same guidance applies for Multi-PHY. What makes this advice even more important for Multi-PHY is that it's easy to miss a problem if something misconfigured under the protocol setup makes your channel group misbehave.
Inheritance from protocol
When you add an element to the channel-based configuration, it will initially inherit a value from the root protocol - and that is the only time this inheritance happens. If you later change the root protocol (ex: switch to a pre-configured PHY), doing so won't update the channel-based config properties for any channel groups, even if you hadn't changed the value inherited from the original protocol. This behavior is depicted in the following video:
Channel group change performance
If you have significant differences between channel groups, channel changing that involves a channel group change will take somewhat longer - in most cases, this is not too significant, but you should measure and calculate with it if you need short channel switch times (PRS channels can be used for measuring this time, see the tutorial about debugging).
Also, if you have 3 channel groups, and only a single field is different between A and B, but C is very different compared to the other two, channel change between A and B will take the same time as channel change between C and A or C and B.
If you have a lot of channel groups in the protocol, this could also cause a small but measurable delay during channel change: it takes some time for RAIL to look up the channel group which includes the requested channel.
In some cases, it's possible to work around these problems by combining channel-based and protocol-based Multi-PHYs:
Use Channel-based multi-PHY between channels where quick channel change is required
Use Protocol-based multi-PHY where channel change time is not a priority
Protocol-based Multi-PHY
The theory
The main idea behind Protocol-based Multi-PHY is to create multiple - completely independent - configs, and use RAIL_ConfigChannels() to select the required one.
Protocols can be created with the icon.
The order of protocols will be used in the generated channelConfigs[] array, i.e. the first protocol in the list will have index 0 and so on.
Multiple regions
Let's say your device operates on the 868MHz 500kb/s pre-configured PHY in Europe, and on the 915MHz 100kb/s pre-configured PHY in North America.
With the above setup, calling RAIL_ConfigChannels(railHandle, channelConfigs[0], NULL) will load the European config, while RAIL_ConfigChannels(railHandle, channelConfigs[1], NULL) will load the North American config.
RAILTest support
Protocol-based Multi-PHY is also useful if you want to test multiple configuration setups using RAILTest: You can change between protocols using the setConfigIndex RAILTest command.
Configuration for a DMP setup
The above setup can be also used in a Dynamic Multiprotocol setup: You'll need two rail handles, let's call them railHandle868 and railHandle915. During the initialization of the 868MHz protocol, you'll call RAIL_ConfigChannels(railHandle868, channelConfigs[0], NULL), and during the initialization of the 915MHz protocol, you'll call RAIL_ConfigChannels(railHandle915, channelConfigs[1], NULL). From this point, RAIL will automatically load the required config using the RAIL scheduler.
PA configuration
RAIL will automatically change everything needed for Multi-PHY during channel or protocol change, except one thing: The PA (power amplifier) configuration. On Wireless Geckos, changing the PA is needed if you change between a sub-GHz and a 2.4GHz config (or maybe between a low power and high power 2.4GHz config).
To solve this, you should pass a function pointer as the last parameter of RAIL_ConfigChannels. In the examples, we call it RAILCb_RadioConfigChanged and it will be called when RAIL switches between channel groups, with the RAIL_ChannelConfigEntry_t as an argument. You can use that argument to figure out if you need to change the PA or not, and call RAIL_ConfigTxPower if needed.
This is implemented in both RAILTest and in SimpleTRX-MultiPHY.
IR Calibration in Multi-PHY configs
If you're not sure what IR calibration is, please refer to the Calibration tutorial.
Configuration and cache variables in rail_config.c
The first one, generated_irCalConfig, tells RAIL how to run the IR calibration on this PHY. This array is passed as part of the PHY info array. The second one, generated_entryAttr is used to cache the calibration result. When you run RAIL_CalibrateIr(), RAIL_ApplyIrCalibration() or RAIL_Calibrate, it will store the IR calibration result here. If you load a channel with the special entryAttr value of RAIL_CAL_INVALID_VALUE (API doc), it will trigger the event RAIL_EVENT_CAL_NEEDED. This is passed as part of the channel entry configuration.
IR Calibration in Protocol-based Multi-PHY
IR calibration should be performed for all protocols. This will be requested from the application through the RAIL_EVENT_CAL_NEEDED event. After the calibration is performed on each protocol, RAIL can use the cached results from these prior operations. When doing so, the calibration values will be applied automatically and near-instantaneously.
IR Calibration in Channel-based Multi-PHY
The radio configurator will automatically detect if channel groups are similar enough to use a common calibration (e.g. only the packet is different). In these cases, both irCalConfig and entryAttr will be the same. Calibration should be performed on each channel group that has its own irCalConfig and entryAttr. RAIL will detect this and request calibration through RAIL_EVENT_CAL_NEEDED if you select a channel group that has a different IR calibration setup and RAIL_CAL_INVALID_VALUE in entryAttr. Once calibration is performed on each channel group that needs it, RAIL can then cache these values for future use. As in Protocol-based Multi-PHY, with cached results the calibration values will be applied automatically and near-instantaneously.
Calibration on Multi-PHY at startup
In some cases, using the RAIL_EVENT_CAL_NEEDED event to calibrate is not viable, and you might want to explicitly request calibration on everything at startup. To do that, you should
loop through all protocols
loop through all channel groups that have their own irCalConfig and entryAttr
For the latter, you should check the generated rail_config.c, there's no easy way to figure out which channel to calibrate from code. To loop through the channel groups, you can use the API RAIL_PrepareChannel() to trigger the PHY loading. The calibration code will likely look something like this:
In the above example we have 5 channel groups that require calibration (NUMBER_OF_GROUPS). The first channel of each channel group is stored in calibrationChannels.
If you have more protocols, you can simply load them using RAIL_ConfigChannels. Note that the first channel of the protocol is automatically configured, so RAIL_PrepareChannel() is only needed for Channel-Based Multi-PHY.
Multi-PHY in Connect
The Connect stack has some limitations on Multi-PHY features:
It doesn't support Protocol-Based Multi-PHY: Connect always loads the first protocol
It doesn't support PA change, so Multi-PHY channels should use the same frequency band (i.e. either 2.4GHz or sub-GHz)
Proprietary Knowledge Base
RAIL Tutorial 4 (studio5): Combining Transmit and Receive
This tutorial builds on the following tutorials:
Please read them first if you haven't.
You can find other tutorials on the table of contents site.
On 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
First, let's load PA factory calibrations: Open the configuration for the component RAIL Utility, Power Amplifier, and turn on the Enable PA Calibration switch.
All other calibrations are enabled by default in RAIL Utility, Initialization, but require some application code to perform them. First, we need to add the related event during initialization to enable it (in app_init.c):
Next, we need to perform calibrations when RAIL requests them (in app_process.c):
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 following components:
#include "printf.h"
to use it, notstdio.h
!Finally, we need to enable Virtual COM UART on the Board Control component:
This pulls up a GPIO, which will enable the USB-UART bridge on the WSTK.
Receive FIFO
We already configured Tx FIFO multiple ways. RAIL also implements an Rx FIFO. By default, it's 512B, allocated by the RAIL library. We will use that, but it is configurable. See
RAILCb_SetupRxFifo()
andRAIL_SetRxFifo()
in the API documentation for details.By default, you cannot use the whole 512B, 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 512B 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
andRAIL_RX_PACKET_HANDLE_NEWEST
will always return with packet information. For example, OLDEST will return with one of the following: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:
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.
The Event Handler
In the event handler, we want to instruct RAIL to hold all successfully received frames:
The API
RAIL_HoldRxPacket
will also return a packet handler, but we ignore that 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.Download and Print
First, let's create a buffer we can use to download the packet:
In
app_process_action()
, if there's something in the handle, let's download and print it:Let's go through this, line by line:
First,
RAIL_GetRxPacketInfo()
will return with a validpacket_handle
if we have a completely received frame. In that case, it also returns a usablepacket_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()
inline function 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.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:
Note that
packet_details
should be a global variable as well 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 from the table of contents site.
API Introduced in this Tutorial
Functions
Types and enums
RAIL Tutorial 3 (studio5): Event Handling
This tutorial builds on the following tutorials:
Please read them first if you haven't.
You can find other tutorials on the Table of Contents site.
In the previous tutorials, we set up RAIL to do various tasks through API commands. However, RAIL can inform the application on various events almost real time (basically, wiring interrupts through RAIL). So let's improve what we had in the previous tutorial.
The Event Handler Function
You might have already seen this function in
app_process.c
:This is the event handler function, which is called by the RAIL library, and routed through the RAIL Utility, Initialization component.
The
events
variable is a bit field, holding all possible events. Be careful, this bit field does not fit into a 32-bit variable, which is the size of theint
on our MCUs.The event handler should be prepared for multiple events in a single call. There's no "clearFlag" mechanism, each event will be signaled once, and the application is responsible for handling it.
Keep in mind that almost all events are generated from interrupts, so the event handler is usually running from interrupt context (i.e., interrupts are disabled). Therefore, the event handler should run fast; if you have to do something that takes more time, set a flag, and do it from the main loop.
You should be also careful with RAIL interrupt safety. Calling state changing APIs (
RAIL_Start*
,RAIL_Stop*
orRAIL_Idle
) from the interrupt handler is allowed, but we recommend to read the interrupt safety article before doing so.Configuring Events
The events enabled by default are configured on the RAIL Utility, Initialization component (under 'Platform/Radio', press 'Configure'):
However, using the RAIL API directly is actually simpler than using the event configuration capabilities of this component. The API for this is
RAIL_ConfigEvents()
. You can keep the event setup enabled, it doesn't really matter.RAIL_ConfigEvents()
has amask
and anevents
parameter, making it simple to configure multiple events at the same time; e.g., to enable or disable a single event, leaving the others untouched:RAIL also defines a few event groups (defined in rail_types.h, documented in events), the most useful are:
RAIL_EVENTS_ALL
includes all the eventsRAIL_EVENTS_NONE
includes no events (equivalent to 0)RAIL_EVENTS_TX_COMPLETION
includes all possible events after a successfulRAIL_StartTx
RAIL_EVENTS_RX_COMPLETION
includes all possible events that can result in state transition from Rx modeThis can be used, for example, to disable or enable all events:
If you enable/disable events in group, it's a good practice to create similar group of events in your application.
RAIL has no API to flush pending events, but disabling and enabling them will have that result:
However, you only need to do this in rare cases. Enabling events is not like unmasking interrupts: Enabling them will not trigger the event handler on past events.
Tx Error Handling
In the last tutorial, we sent out a frame, but we didn't check the results because it would have needed the event handler. Let's turn on led0 on success and led1 on any error (we'll be turning led1 on when transmitting and clearing led1 if no Tx error occurs).
First, for the LEDs, enable the Simple LED component with led0 and led1.
In app_process.c (to get access to the led instances, now declared in autogen/sl_simple_led_instances.h):
For the actual radio related changes, we'll need to enable the success and error events. We can do that during init since we don't really need to disable anything at runtime for such simple applications:
Next, let's check for errors returned by the StartTx call:
Finally, let's check the events for success and errors:
With this, you should see led0 toggling on every transmitted packet.
Receiving Packets
Let's add receive capabilities to this application! Again, use led0 to report success and led1 for errors.
First, we need to modify the init code.
In app_init.c (to get access to the led instances):
We need RX success/error events enabled, and we need to start the radio in RX mode:
The API
RAIL_StartRx()
is similar to Tx: the only important option is the second one, which is the channel.Finally, let's check the events for success and errors (in app_process.c):
Auto state transitions
By default, after receiving or transmitting a packet, RAIL will switch to idle mode (i.e., turn off the radio). Since we want it to keep receiving future packets, we can use RAIL's auto state transition feature. This can be configured on the RAIL Utility, Initialization component, let's set all dropdowns to RX:
We're basically saying that after the packets are received/transmitted, we want to return to Rx. These configurations can be also changed in run-time, using the
RAIL_SetRxTransitions()
andRAIL_SetTxTransitions()
APIs.If you compile and flash the program after this modification, you should see that now both WSTK toggles led0 on every button press.
Testing and Conclusion
If you install this example to two boards (make sure you attach the antennas if you use sub-GHz kits on sub-GHz config), you will see that on the press of btn0 on either device, led0 on both devices toggle. However, we can't see what we're receiving, since we never actually download the messages.
We're going to fix this Next time
API Introduced in this Tutorial
Functions
Types and enums
RAIL Tutorial 2 (studio5): Transmitting a Packet
This tutorial builds on the following tutorial/s:
Please read them first if you haven't.
You can find other tutorials on the Table of Contents site.
In this tutorial, we're going to modify the project Flex (RAIL) - Empty Example to transmit a packet. We're going to use the default, fixed length 16Byte payload configuration first.
This tutorial references some more advanced articles - we don't recommend reading them yet if you just started learning RAIL. They are only linked to highlight the connection.
Buffer Handling
Setting Up the Packet Buffer
RAIL requires a buffer (usually called FIFO) to be configured for Tx. First, we have to allocate some memory for that buffer:
Note that you can't use any arbitrary size; EFR32 creates a FIFO ring buffer on this memory area, and it can only handle buffer sizes of power of 2 between 64 and 4096 (i.e., 64, 128, 256, 512, 1024, 2048, or 4096).
The tx buffer can hold more than one packets, which can be useful if you need to send out a lot of messages quickly.
To load the payload of the packet to the buffer, we have two options: write it via RAIL APIs or write to the memory directly.
Writing to the Buffer Directly
In app_process.c:
The
memcpy()
is just a standard C instruction to write to buffers, and we useRAIL_SetTxFifo()
to pass that buffer to RAIL. We also tell RAIL how long the buffer is, and how much data it has already in it.Writing to the Buffer Indirectly
In
app_init.c
:In
app_process.c
:In this case, we pass the buffer to RAIL when it's still empty, and we write to that buffer using a RAIL API. Note that using
memcpy()
instead of WriteTxFifo would not work: while it would write the memory itself, it wouldn't change the read and write pointers of the FIFO, handled by RAIL, so RAIL would still think the buffer is empty.Direct or Indirect
There are four main factors that could decide which method to use to write the FIFO:
RAIL_SetTxFifo()
does not move any memory, just sets a few register, while callingRAIL_WriteTxFifo()
does need some time to copy the payload to the buffer.RAIL_SetTxFifo()
is not allowed during transmission.For simpler applications,
RAIL_SetTxFifo()
can be simpler. If you want to send the same packet over and over (or only change a few bytes in it), it's much better, since you don't have to write the whole packet again.On the other hand, the separation in RAIL examples between app_init and app_process works very nicely with the indirect method, and in most real cases, you need to move memory anyway. If you want to send longer packets than your buffer, you must use
RAIL_WriteTxFifo()
, as you can call it during transmit. It's also useful if you want to send out a lot of packets: UseRAIL_WriteTxFifo()
to load each message one after the other, then send them out quickly without any buffer operation.The attached code uses indirect mode mainly for the clear separation.
FIFO Reset
The last parameter of
RAIL_WriteTxFifo
can reset the FIFO, which means it will invalidate the data already in there. This is also possible withRAIL_ResetFifo
. We generally recommend not to use it, a well-written code shouldn't need it (as it should only store in the FIFO what should be sent out), and it might mask bugs. Resetting the FIFO also takes extra time.Transmitting the Packet
Starting the transmission is very simple:
This instructs RAIL to start transmitting the data stored in the FIFO on channel 0 (second parameter). The third parameter can change various options, like using the second configured sync word (see RAIL API reference). The last parameter is only required for DMP (dynamic multiprotocol) mode.
Changing the Packet Length
You can change the configured fixed length on the Radio Configurator, but that's obviously not possible at runtime, which is often needed. Note that the amount of data loaded into the FIFO does not matter as long as it's equal to or greater than the length of the frame.
Changing Packet Length with a Fixed Length Configuration
With the API
RAIL_SetFixedLength()
, it's possible to change the pre-configured length (stored in rail_config.c) at runtime. Note that this changes the length of the packets on the Rx side as well. If you want to return to the pre-configured value, you can useRAIL_SetFixedLength(railHandle, RAIL_SETFIXEDLENGTH_INVALID)
.Using Variable Length Configurations
A lot of protocols store the length of the packet in the header of the frame. For example, in IEEE 802.15.4, the first byte stores the length (and the maximum is 127). To set this up, use the following settings in the Radio Configurator:
For more information on this setup, and on more advanced variable length configs, see AN1253. Note that the above configuration is not fully IEEE 802.15.4 compatible to make it simpler.
Length decoding works the same way for both rx and tx. This means that during Tx, we have to make sure that the committed length in the header matches the number of bytes you load into the FIFO. It also means that if we set the length field to more than 127, we will get a transmit error.
This would be a valid 16B frame, both for the above described variable length and 16 Byte fixed length mode, so this is used in the attached sample code:
We use
PAYLOAD_LENGTH-1
in the length field, since the 1B length field itself shouldn't be counted.The API
RAIL_SetFixedLength()
is available in variable length mode, and it changes the radio to fixed length operation (again, both for rx and tx). CallingRAIL_SetFixedLength(railHandle, RAIL_SETFIXEDLENGTH_INVALID)
will restore it to variable length mode.Setting up the example
Button handling
The attached example sets the tx fifo on init, loads, and sends the packet on any button press. This is implemented using the Simple Button component on btn0, and implementing its callback:
In app_process.c (to get access to the button instance, now declared in autogen/sl_simple_button_instances.h):
Also add the button callback code (the callback is already declared in sl_button.h):
Since this callback is in interrupt context, we avoid using RAIL API directly. It is safe in almost all cases, but calling
RAIL_StartTx()
from interrupt context should be done carefully. Instead, we set the volatilesend_packet
variable to true, and call RAIL APIs fromapp_process_action()
only:To simply set this up:
Conclusion
We didn't care about possible errors generated by RAIL: we do that next time, which also provides hints for receiving packets.
API Introduced in this Tutorial
Functions
Types and enums
RAIL Tutorial 1 (studio5): Introduction, RAIL components and the Empty Example
What is RAIL?
RAIL is short for Radio Abstraction Interface Layer, and is the most direct interface available for EFR32 radios. You can think of it as a radio driver. While this sounds like a restriction, this actually makes software development for proprietary wireless much simpler:
See this KBA for more details.
RAIL application development is currently possible in the Flex SDK in Simplicity Studio. To progress further with this tutorial, please remember to open the RAIL API documentation.
When to Use RAIL?
If you have an existing protocol, and you must be compatible with it, you'd probably have to use RAIL (as all of our stacks use a standardized frame format).
If you want to use sub-GHz communication, you have a few options, including RAIL. For simple point to point communication, RAIL might be the simplest solution. However, as soon as you need security or addressing, it might be better choose a protocol stack, such as Connect.
RAIL also has the benefit that it adds the least amount of delays to all radio events: In fact, some event notification will happen in interrupt context.
What is covered by RAIL?
RAIL only supports what the radio hardware supports. For example, auto ACK and address filter is available, as the hardware provides support for it. On the other hand, while security is important for wireless communication, it's not really a radio task, hence RAIL does not support it. The crypto engine is an independent peripheral that can be used to encrypt payloads before giving them to RAIL and the radio hardware.
Supported Energy Modes
EM1p or higher is required for the radio to be operational (i.e., Transmitting, receiving, or waiting for packets, RAIL timer running from HF clock). On devices without EM1p support, EM1 is required for the radio. See AN1244: EFR32 Migration Guide for Proprietary Applications for more details on EM1p.
EM2 or higher is required for RAIL scheduling or timers (Running from LF clock, which needs configuration, a topic in Tutorial 5).
EM3 or higher is required for RAIL to work without re-initialization.
Writing Code from Scratch
We do not recommend writing code from scratch because it is much simpler to have something existing that sets up the include paths and linker settings correctly. The best way to start is the example Flex (RAIL) - Empty Example.
RAIL Components
The empty application includes the following RAIL related components (under 'Platform/Radio'):
There are a few components that you might want to install
If you work on a starter kit, you don't need to change the configuration of most of these components (for custom hardware, there will be a new article coming soon), except the Initialization component. However, the initialization component also includes a good starting point, that we don't need to modify for simple applications.
The Radio Configurator
The Radio Configurator is accessible by configuring the component Advanced Configurators/Radio Configurator. The usage of the radio configurator is out of scope for this tutorial series. For more details, see AN1253: EFR32 Radio Configurator Guide.
The project structure
Projects always include the following parts:
rail_config.c
), init code, the linker script, and other generated code used by components, like the command descriptors for the CLI interface.slcp
file) and the Pin tool (.pintool
file)Note that all files related to the project should be visible in the project explorer, including header and library files.
All projects include
main.c
, which is not recommended to modify. Instead, add the initialization code toapp_init.c
, and implement the main loop inapp_process.c
. This way, the System components can initialize components, and call the "process" function of the components that requires this. Additionally, enabling an RTOS will convert app_process's main loop into an RTOS task.Conclusion
This project is ready to send out frames. We're going to do that in the next part. You can also find other tutorials from the Table of Content site.
RFSense on EFR32xG22
RFSense is a low power feature of the EFR32 Wireless MCU family. It can "wake up" an MCU from its EM2 or even EM4 power modes. Practically, it is an ultra low power interrupt source, running on ULFRCO clock.
The RFSense is a wide band circuit, it can detect energy in the 100MHz - 5 GHz frequency range, filtered only by the matching network of the RF front end. This is an advantage, as no need for separate PCB components. But it’s also a drawback: it is sensitive to any kind of interferer signal as well.
EFR32xG22 has an updated RFSense module, which improves the performance compared to EFR32 Series 1 in multiple ways:
Legacy mode
In legacy mode, EFR32xG22 RFSense is fully compatible with the one in Series 1. This means that if the RFSense module detected energy for a configured time, it generates an interrupt.
Selective mode
Selective mode mitigates the unfiltered nature of RFSense. Instead of simply detecting energy for a given time period, it detects "a pattern of energy", which is essentially an OOK packet. The packet is Manchester coded, uses fixed 1kbps bitrate, 1B preamble and 1-4B sync word (no payload added). This packet can be transmitted by any OOK capable device, including all EFR32 wireless MCUs (Series 1 and Series 2).
Selective mode offers 2 configuration options to select from: "optimized for sensitivity" or "optimized for noisy environment".
The Wakeup Packet
The wakeup packet is a fixed-configuration OOK packet with the following settings:
Selective Mode Transmit Example Without Using API
Assume you select 0xb16e as your sync word, and you want to transmit it with only a signal generator (or a simple radio with MSB-first byte handling and no Manchester coder).
First, flip the endianness of both preamble and sync word: 0x55 becomes 0xaa and 0xb16e becomes 0x768d.
The full packet is then 0xaa768d, which after Manchester coding becomes 0x99996a6995a6.
Configuring this encoded packet and transmitting on 2.45 GHz with high enough TX power should wake up a device configured for selective RF Sense with the 0xb16e sync word
For more details on selective mode, see AN1244: EFR32 migration guide for Proprietary applications
RAIL Idle modes
The function
RAIL_Idle()
takes a mode argument, with which you select one of the four available methods of idling the radio to use. Though the API documentation briefly describes each mode, it might be difficult to understand their differences without seeing examples and usecases. This document aims to help you more clearly understand each mode.General recommendations
In most cases,
RAIL_IDLE
is the recommended mode to use with this API.RAIL_IDLE_ABORT
is helpful when you want to also abort an ongoing ("active") tx or rx operation.On the other hand,
RAIL_IDLE_FORCE_SHUTDOWN
is not recommended for use, andRAIL_IDLE_FORCE_SHUTDOWN_CLEAR_FLAGS
should be handled with care.RAIL_IDLE
The mode
RAIL_IDLE
turns off the radio after the current operation is finished. It also cancels any transmit or receive scheduled in the future. Current operations that won't be aborted include:RAIL_IDLE_ABORT
The mode
RAIL_IDLE_ABORT
works the same asRAIL_IDLE
, but it will also abort active operations. However,RAIL_IDLE_ABORT
will always wait for a stable state before turning off the radio, e.g. if the radio was preparing to enter rx mode and is waiting for the PLL to lock, RAIL will wait until the rx state is reached before idling the radio.RAIL_IDLE_FORCE_SHUTDOWN
Unlike
RAIL_IDLE_ABORT
(which waits for a stable radio state),RAIL_IDLE_FORCE_SHUTDOWN
immediately forces the radio off. As this is an abrupt transition, it may corrupt data in the receive or transmit buffers. This buffer corruption can only be resolved by completely clearing the FIFO - which loses data, and can also consume additional time.In our experience, it's almost always slower than
RAIL_IDLE_ABORT
, so the costs typically outweigh the benefit of usingRAIL_IDLE_FORCE_SHUTDOWN
(except when recommended by our support team for diagnostic purposes).RAIL_IDLE_FORCE_SHUTDOWN_CLEAR_FLAGS
The mode
RAIL_IDLE_FORCE_SHUTDOWN_CLEAR_FLAGS
works the same asRAIL_IDLE_FORCE_SHUTDOWN
, but it also clears any pending events. For more details on scenarios where this can be useful, see the article on RAIL interrupt safety.Interrupt and thread safety in RAIL
In embedded software development, some of the most complicated debug challenges are caused by calling non-reentrant functions in interrupt context. Hence, it's in the developer's best interest to carefully design their application to avoid these scenarios.
To do so, however, requires sufficiently detailed knowledge of the interrupts and API functions - which are not accessible in a closed-source product like RAIL. This document aims to provide the information required to develop interrupt- and thread-safe applications in RAIL.
Thread Safety
In an application without a task scheduler, only an interrupt request can interrupt the main program. If you use a preemptive scheduler, like the scheduler available in most embedded OSes (including Micrium OS), higher priority tasks can interrupt lower priority tasks as well. Regardless, when looking at the RAIL APIs, the same concerns are present in either case:
The Event Handler
RAIL uses an event handler, which is set up by
RAIL_Init()
. In our examples, it's usually calledRAILCb_Generic()
. This function is called by the RAIL library, and it's almost always called from an interrupt handler. This means the event handler should be used with care:Note that the first point above might not be completely true if interrupt priorities are used, in which case only interrupts at the same and lower priorities are disabled. However, the event handler will never be interrupted by another event handler as all RAIL interrupts must be used at the same priority.
General Rules for the RAIL API
First, let's collect the general rules of the API, and we'll detail exceptions in later points:
Dynamic Multiprotocol (DMP)
In general, if you have a multi-threaded application, you should use RAIL from a single thread. The exception to this guidance is DMP, where in most cases each protocol runs in its own thread. In this scenario, using RAIL from each thread is safe, as each protocol has its own
railHandle
. So, a more generalized wording of rule 2 is:Calling any API from multiple threads is only safe if each thread has a dedicated
railHandle
, and each thread only accesses RAIL with its own handle.The few APIs that don't use
railHandle
- likeRAIL_GetTime()
,RAIL_Sleep()
, orRAIL_Wake()
- can be called from any thread.Interrupt Safety in general
In general, calling an API which changes the radio state (i.e. between rx, idle and tx) can be risky. The simplest way to write interrupt safe application is to not call state changing APIs from any interrupt handler, including the RAIL event handler. This can be achieved by setting a flag or changing a state variable in the event handler instead of calling an API directly:
Note that some RAIL API was called from the event handler, but none of those were state changing APIs.
Interrupt safety with state changing APIs
In some (usually time critical) cases however, it's not possible to avoid calling state changing APIs from the event handler (or other interrupt handler). State changing APIs are not always risky: Some APIs might be safe, as long as they don't interrupt another specific API.
Hence, in the following list, we identify the risky API after first specifying which initially-running (i.e., "interrupted") API makes it risky (and how). We've included in this list some interrupt combinations that might be "safe", but the end result is not predictable - i.e. the radio might be in rx or in idle, depending on which API is called first.
RAIL_Start<something>()
with anotherRAIL_Start<something>()
is risky, especially if they would start on different channels.RAIL_Idle(handle, <something>, true)
with anyRAIL_Start<something>()
is risky.RAIL_Idle(handle, <something>, false)
with anyRAIL_Start<something>()
is safe, but the end result is not predictable (i.e. the radio will either be in Idle, or start the requested operation).RAIL_Start<something>()
withRAIL_Idle()
is safe but the end result is not predictable, and might cause strange events (see the next section for details).RAIL_StopTxStream()
with anyRAIL_Start<something>()
is very risky (the radio might remain in test configuration and start transmitting/receiving).RAIL_StopTx()
is safe. InterruptingRAIL_StopTx()
withRAIL_Start<something>()
is safe but the end result is not predictable (i.e. the radio will either be in Idle, or start the requested operation).RAIL_StopTx()
is safe (see next section for important clarification). InterruptingRAIL_StartTx()
withRAIL_StopTx(handle, RAIL_STOP_MODE_ACTIVE)
is safe, but not predictable.RAIL_StopTxStream()
is safe. InterruptingRAIL_StartTxStream()
withRAIL_StopTxStream()
is safe but not predictable.RAIL_Idle in the Event Handler
Calling
RAIL_Idle()
orRAIL_StopTx(handle, RAIL_STOP_MODE_ACTIVE)
from the event handler might cause strange results. For example, let's say you're receiving on a channel and want to detect preambles using the eventRAIL_EVENT_RX_PREAMBLE_DETECT
andRAIL_EVENT_RX_PREAMBLE_LOST
. The following scenario may unfold:RAIL_EVENT_RX_PREAMBLE_LOST
.RAIL_Idle(railHandle, RAIL_IDLE_ABORT, true)
.RAIL_EVENT_RX_PREAMBLE_DETECT
andRAIL_EVENT_RX_PREAMBLE_LOST
both set at the same timeSo you end up with a preamble detect event, even though the radio is off. This is usually harmless, since you always have the
_LOST
or_ABORTED
event as well - but this demonstrates why your design must carefully consider in what order to handle events.The easiest way to avoid this conflicted outcome is to disable the events that might cause problems when turning off the radio.
Another way to avoid this issue is to use
RAIL_Idle(handle, RAIL_IDLE_FORCE_SHUTDOWN_CLEAR_FLAGS, true)
, which will clear the pending interrupts. However, usingRAIL_IDLE_FORCE_SHUTDOWN_CLEAR_FLAGS
has other drawbacks. It does force the radio state machine to idle state, and it might corrupt the transmit or receive FIFOs - in which case it must clear them, losing all data that might already be in there. It could also take more time to finish running thanRAIL_IDLE_ABORT
.Critical Blocks
One usual way to avoid internal safety issues is to create critical (a.k.a. atomic) blocks, in which interrupts are disabled, in the main thread to make sure some code segment is never interrupted. However, this can create other problems, so it should be used carefully. There's no general rule to avoid this kind of "collateral damage", but here's an example that should be avoided:
RSSI averaging is running, and just before it finishes, we interrupt it with
RAIL_StartTx()
which is called from a critical block. The following race condition could happen:One way to avoid the problem above is to clear interrupts in the critical block. This can be done by using
RAIL_Idle(handle, RAIL_IDLE_FORCE_SHUTDOWN_CLEAR_FLAGS, true)
at the beginning of the critical block, but the drawbacks of doing so (mentioned above) should be kept in mind. In general, it's better to avoid risky interrupts without using critical blocks in the main thread.Using FORCE_SHUTDOWN
In the two sections above we mentioned two usecases where
RAIL_IDLE_FORCE_SHUTDOWN_CLEAR_FLAGS
can be useful. In general however,RAIL_IDLE
orRAIL_IDLE_ABORT
is a sufficient and preferred way to stop transmitting/receiving - therefore theFORCE_SHUTDOWN
modes should be only used when they are really needed (as in the specific scenarios described here). For more details, see the article on the idle modes.RAIL tutorial: Dynamic Multiprotocol (DMP)
This tutorial builds upon the material covered in previous offerings:
(*) Make sure to read this one, as DMP is often misunderstood and used in applications that could be better served by multi-PHY. We recommend that you (at least) browse through the other tutorials before reading this (DMP) tutorial, since DMP affects most APIs in one way or another.
You can find additional RAIL tutorials on the table of contents page.
Note that this tutorial will not provide you with all of the knowledge required for DMP development. Rather, it will summarize what's already documented, and highlight differences between RAIL single protocol and multiprotocol operations.
Some chapters below are not important if you need RAIL-Bluetooth DMP, but even in that case most chapters are still relevant because they describe RAIL development in a DMP environment.
Note that this tutorial is slightly outdated in Studio v5, since the we now have components, not plugins, and the way we initialize RAIL in the examples are performed by the RAIL Utility, Initialization component, and not by the application. However, these are minor details and the wast majority of this article is accurate.
What is Dynamic Multiprotocol
Dynamic multiprotocol time-slices the radio and rapidly changes configurations to enable different wireless protocols to simultaneously operate reliably in the same application.
A typical usecase involves a RAIL-based proprietary protocol and the Silicon Labs Bluetooth stack running on the same chip at the same time. Since Bluetooth communication is periodic and therefore very predictable, other protocols can use the radio between Bluetooth communication bursts. However, it's also helpful to use DMP when all involved protocols are proprietary RAIL-based, to benefit from the code clarity provided by the separate RAIL instances.
From a firmware perspective, RAIL-DMP is similar to an object-oriented programming (OOP) class. It provides the radio driver, and multiple instances can be "created". Each instance can use the driver in the same way, and do so almost independently.
However, from a hardware perspective the chip has only a single instance of the radio. As such, RAIL-DMP is similar to multitasking: the same resource must be shared among multiple "tasks" demanding its time - this sharing is managed by the radio scheduler.
The Radio Scheduler
The radio scheduler's job is to give (or revoke) radio hardware access to (or from) the protocols running on it. To accomplish this it implements cooperative, preemptive multitasking.
Note that the radio scheduler schedules only radio tasks. Although DMP is often used with an RTOS (like Micrium OS), RTOS tasks and Radio tasks - despite the common name - are very different terms. To make this less confusing, our documentation sometimes uses radio operation instead of radio task.
For the radio scheduler to calculate the time required for a given radio operation, it must know how much time it takes to change radio access from one protocol to another. Therefore, in DMP, a protocol change always takes 450us on EFR32xG1x, which accommodates the worst-case protocol change time. This makes DMP protocol change much slower than Multi-PHY PHY change.
Protocol change time in RAIL 2.5.x (Flex 2.4.x) and earlier was 750us (and it might change further in the future).
Setting up a project for RAIL-RAIL DMP
If you need RAIL+Bluetooth DMP, follow AN1134. This chapter describes how to set up DMP in a RAIL-only project.
To use DMP, you should first switch to the multiprotocol RAIL library, under plugins. Since DMP has a bigger memory footprint (both RAM and code), it's provided in a library which is not enabled in single protocol examples.
Next, you'll need PHY configurations for your protocols. You can either use Protocol Based Multi-PHY (see the Multi-PHY usecases and examples tutorial for details), or you can use embedded protocols, e.g. IEEE 802.15.4 2.4GHz 250kbps by calling
RAIL_IEEE802154_Config2p4GHzRadio()
.Micrium OS
Micrium OS is not technically required for RAIL DMP, although it's highly recommended to clearly separate protocols (and for RAIL+Bluetooth DMP, we actually only support Micrium-enabled projects.) Hence, Micrium OS will not be discussed in this article - it doesn't impact how you use RAIL APIs in RAIL-only DMP.
RAIL initialization
Let's see how one RAIL protocol should be initialized in DMP:
Let's compare it to the very first RAIL tutorial:
railSchedState
is a new static variable required for RAIL. This keeps the state of the protocol when it's not active (e.g. configured Rx options)railCfg.protocol
is set toNULL
. This is also a DMP specific field, but it's only used by the Silicon Labs Bluetooth stack.RAILCb_Generic
is nowstatic
: Since all protocols will have their own callback functions, we should either make the function names unique, or put them in different C files as static. Although technically it's possible to use a common callback function for all protocols, it's not recommended as the readability of the code is much better with separate event handlers.halInit()
is no longer called frominitRadio1()
- since we have multiple protocols to support, it makes more sense to set up RAIL requirements elsewhere in a protocol-independent init functionThis code snippet initializes a single protocol. However, all remaining protocols can be initialized in the same way, noting that
railHandle
,railSchedState
andRAILCb_Generic
should be different for all protocols (probably by placing in separate C files).From this point you can use RAIL almost the same way as in single protocol mode. Since almost all APIs have a
railHandle
argument, RAIL will know which protocol you want to access. However, you should be careful with:railHandle
argumentProtocol independent APIs
There are a few APIs that always work the same way, regardless of which protocol they were called from:
RAIL_SetTime()
/RAIL_GetTime()
- the timebase (which is also used for timestamping) is common for all protocols. It is not recommended to callRAIL_SetTime()
in a DMP application (and calling it in a single protocol application can still be dangerous)RAIL_ConfigMultiTimer()
- MultiTimer mode is a requirement in DMP, so it's enabled by default and cannot be disabledRAIL_Sleep()
/RAIL_Wake()
- Sleep management should be handled protocol-independently (e.g. in the idle task of the RTOS)There are some other APIs that don't require
railHandle
, but they usually need a packet handler or timer handler, and so are still tied to a protocol.Scheduling a finite radio task
The following APIs will trigger the radio scheduler to schedule a finite length new radio task:
RAIL_ScheduleRx()
RAIL_StartTx()
RAIL_StartScheduledTx()
RAIL_StartCcaCsmaTx()
RAIL_StartCcaLbtTx()
RAIL_StartAverageRssi()
(Note that
RAIL_StartRx()
andRAIL_StartTxStream()
is not listed, as these as these are not finite length tasks.)All the above APIs have the optional argument
RAIL_SchedulerInfo_t
. In single protocol mode, this argument is ignored, and can be NULL. In multiprotocol mode, this is the base of the scheduling, with its 3 member:priority
- 0 to 255, 0 being the highest. Equal priority does not preemptslipTime
- maximum time the task can be delayed due to other protocols, in ustransactionTime
- the time the task takes, in usWhen the radio hardware is available, none of these scheduler info arguments are used. However, if two tasks are scheduled at the same time, the radio scheduler will try to stagger the operations using
slipTime
in order to satisfy both tasks. If that fails, the lowerpriority
task will not get access to the radio.It is also possible that an application schedules a high priority radio task when a conflicting lower priority task has already started. In that case, the lower priority task will be aborted (possibly in the middle of frame transmission).
If
RAIL_SchedulerInfo_t
isNULL
, the scheduler will assume all parameters to be 0, i.e. highest priority, with no slipTime - it is highly recommended to not use this feature, and it's only mentioned here to simplify debugging.Each protocol can schedule at most a single finite task.
For more details on the radio scheduler, see UG305 chapter 3 and the Multiprotocol description in the RAIL API doc.
Yielding
After each radio task the application must yield the radio. That can be achieved by calling
RAIL_YieldRadio()
orRAIL_Idle()
- the latter also turns off the radio, which should have already occurred for finite tasks, so it's recommended to useRAIL_YieldRadio()
. Manual yielding is required because the scheduler doesn't know about any automatic follow-up tasks (like ACKs) - you might want to yield immediately after receiving a packet, or you may instead want to wait for the auto ACK to go out.Since yielding the radio will also call the radio scheduler to start/schedule the next radio task, you must first capture everything you need that might be overwritten by another protocol after yield, even in interrupt context. For example, timestamps of a transmitted packet must be accessed before yielding.
A Tx operation scheduled in the future can be cancelled using
RAIL_Idle()
orRAIL_StopTx()
, where the latter doesn't yield the radio. An Rx operation scheduled in the future can only be cancelled usingRAIL_Idle()
.Background Rx
RAIL_StartRx()
receives special handling in the radio scheduler, as it schedules an infinite task - namely, background Rx. The main difference when compared to finite tasks is that an infinite task sets the "default" state of the radio. It can be aborted by higher priority finite tasks, but it will be resumed automatically after the higher priority task is finished. For background Rx, this practically means that the radio will automatically return to Rx after Tx or packet reception, and you don't have to use auto state transitions like you do in single protocol mode. (Currently, the only infinite task supported by RAIL is background Rx).Keep in mind that RAIL does not store the channel of background Rx: if you interrupt background Rx with a finite task on a different channel, the channel of the background Rx will change as well. E.g. if you're receiving in channel 0, then transmit on channel 1, the radio will "return" to receiving on channel 1. To change the background Rx channel back to 0, simply call
RAIL_StartRx()
again after you yield the radio.RAIL_StartRx()
has the sameRAIL_SchedulerInfo_t
configuration as the finite tasks, but depends only on thepriority
member - which, in general, should be the lowest priority used by the protocol.Background Rx can be aborted using
RAIL_Idle()
, which also yields the radio. In DMP mode, it's recommended to only useRAIL_Idle()
to stop background Rx.It is also possible to change the priority of the background RX using
RAIL_SetTaskPriority()
- this is useful e.g. to increase the priority during packet reception, or after a particular packet.TxStream
RAIL_StartTxStream()
is called a debug task: A debug task must have the highest priority, because it can't be aborted. Otherwise, it works similarly to infinite tasks.This API is different from all others as it doesn't use
RAIL_SchedulerInfo_t
. Since a stream can't really be aborted, the radio scheduler will handle this with the highest priority and no slip time.Stream can be stopped with
RAIL_StopTxStream()
,RAIL_Idle()
orRAIL_YieldRadio()
, although it is recommended to useRAIL_StopTxStream()
for clarity.Auto state transitions
You might wonder, "what's the point of auto state transitions in DMP, if background Rx changes the default state to Rx?" The added value of auto state transitions is that the resulting state will inherit the original state's priority.
For example, you probably want the ACK reception on the same priority as the transmission itself, so you can set
RAIL_SetTxTransition()
to set up receive after the transmission for the ACK, and configure a timeout for it usingRAIL_SetStateTiming()
.If you have automatic state transition set up, you should only yield the radio after the whole operation (e.g. ACK reception or timeout) is finished. It's highly recommended to have automatic timeouts to prevent a high priority infinite task, but if you haven't set up timeouts, you can cancel the receive and yield the radio using
RAIL_Idle()
.To summarize:
For more details, see UG305 chapter 6
MultiTimers
RAIL includes a timer virtualization service, called MultiTimer. Since its memory footprint is not negligible, this feature is disabled by default in single protocol mode. However, in multiprotocol mode, it is always enabled, since multiple timers are required for the multiple protocols. Therefore, using MultiTimers in your protocol implementations has no drawback in DMP.
Error handling
In single protocol RAIL, you generally need to places to handle errors:
RAIL_StartXYZ()
/RAIL_ScheduleXYZ()
might return an error. If no error were returned, the operation was either finished, or an event will be triggeredRAIL_EVENT_TX_PACKET_SENT
orRAIL_EVENT_TX_CHANNEL_BUSY
In DMP, there's a third error case to handle above the previous two: You should always handle
RAIL_EVENT_SCHEDULER_STATUS
, where you might receive an error by callingRAIL_GetSchedulerStatus()
. It's usually obvious which API call failed, e.g.RAIL_SCHEDULER_STATUS_SINGLE_TX_FAIL
means aRAIL_StartTx()
failed, but it might not be possible to know the original call resulted in error.This was implemented because when you call e.g.
RAIL_StartTx
, the radio scheduler just creates task. When that task is running, it will actually call the single protocolRAIL_StartTx
, and if that returns an error, it will trigger anRAIL_EVENT_SCHEDULER_STATUS
event.The event
RAIL_EVENT_SCHEDULER_STATUS
is also used when a higher priority task interrupts an ongoing radio operation, in which case,RAIL_SCHEDULER_STATUS_EVENT_INTERRUPTED
will be returned byRAIL_GetSchedulerStatus()
.Debugging
When debugging DMP, Rx/Tx PRS channels are still very useful, see the tutorial about debugging for details.
For DMP based code, writing out to a GPIO when a protocol is scheduled/unscheduled is really useful. This can be easily done by setting a GPIO on
RAIL_EVENT_CONFIG_SCHEDULED
and clearing it onRAIL_EVENT_CONFIG_UNSCHEDULED
.Recommendations and practices
Although these practices are very important for DMP applications, you should consider applying them in single protocol applications as well, to simplify a potential future port to DMP:
RAIL_Idle()
when it's necessary - In RAIL 1.x almost all APIs required that they be called from idle mode. This requirement was removed in RAIL 2.x, so it's very rarely needed, and sinceRAIL_Idle()
also yields the radio, it should be very rarely used in DMP.How not to use DMP
Using multiple DMP instances for multiple radio tasks (e.g. one protocol for advertising, one for connection, or one for Tx, one for Rx) is bad design: while it works, each protocol instance has a significant memory footprint, and switching time between protocols is much slower than switching Rx/Tx inside a protocol. At the moment, it is recommended to use DMP only for serving separate protocol stacks.
Using DMP to handle multiple PHYs for the same protocol stack is also not recommended: Multi-PHY is a much better solution for that, see the introduction to Multi-PHY and Multiprotocol tutorial for more details.
Related documentation
If you find any conflicts between this article and the above documents, give those documents priority as they will more quickly receive updates to track new RAIL DMP features and guidance.
RAIL Tutorial Series
Introduction
The goal of this tutorial series is to help you get started with RAIL, the C API for embedded software for Silicon Labs' EFR32 Wireless Geckos.
To read it, you should have some experience with embedded C - you should know how to handle interrupts, what volatile means, etc.
While we try to avoid using it, you might need some experience with radios as well. It definitely helps if you know what preamble or sync word is.
The tutorials should be read together with the RAIL API documentation.
If you find any contradiction between the tutorials and the API documentation, just always remember that the API documentation is more accurate.
This tutorial series focuses on the embedded code running on Wireless Geckos. Configuring the radio correctly is equally important, see AN971 for details on that for Studio v4 and AN1253 for Studio v5.
The tutorial series is currently under review and update for the Studio v5 updates. Although RAIL itself haven't changed much, some critical tools did. For details, see AN1254. The tutorials are mostly accurate for both v4 and v5. On those articles where an update will be needed in the near future, a note is added to the first couple of paragraphs.
Getting Started
You should start with these tutorials. All others are based on these, as these are the minimum to develop useful applications on RAIL.
Studio v5 series
Studio v4 series
Basic Tutorials
These tutorials are common and basic features, you should read them before you start working on a project which uses RAIL.
Advanced Tutorials
These tutorials describe features that are rarely needed - they are available and can be used when necessary, but you probably won't use them all in your application. Generally, you should read them only if you think you need them.
Multi-PHY and Multi protocol Tutorials
These tutorials are about the Multi-PHY and multiprotocol capabilites of RAIL. If you plan to use that feature, we recommend to read the introduction then what you need, depending on your application.
RAIL Tutorial: Multi-PHY usecases and examples
In this article, we go through a couple of example usecases and their setups using the Multi-PHY radio configurator. If you haven't already, please read Introduction to Multi-PHY and Multiprotocol first to understand the various Multi-PHY and Multiprotocol capabilities of the Wireless Geckos. Some basic understanding of radio configs and the radio configurator is also required.
You can find related tutorials on the table of contents site.
This tutorial includes videos (without audio) to guide you through the setup on the Configurator GUI.
Note that the videos on this tutorial were recorded on Studio v4, and Studio v5 follows a slightly different configurator workflow. Until the videos are updated, you can find details of the new workflow in AN1253.
Channel-based Multi-PHY
The theory
The main idea behind Channel-based Multi-PHY is to create channel groups, each of which can have different PHYs. Changing between these channel groups is handled by RAIL: from the RAIL API, you can load the required PHY just by selecting a channel.
Channel groups can be created with the
icon. By default, these new channel groups have no PHY configuration.
A PHY configuration can be added using the
icon. A PHY configuration exposes elements of the radio configurator for each of the channel groups in the selected protocol. The PHY applied to any specific channel group can then be customized by changing the values for each element present in the PHY configuration.
Once you add something to the channel-based configs (as described above), the protocol-based setup will be disabled.
Different bitrate
Let's say you need to work on the same 915MHz frequency, but on both 100kb/s and 500kb/s (with deviation 50kHz and 250kHz, respectively). Typically, this is used as an extension of a protocol which originally only supported the lower bitrate: The default bitrate is 100k, but it's possible to switch to 500k after a handshake.
In the above setup, channel 0 uses 100kb/s bitrate, while channel 1 uses 500 kb/s. Note that the channels are on the exact same frequency, but their configuration is slightly different - you might call these virtual channels. From RAIL, you can change between these virtual channels just as regular channels: If you call
RAIL_StartRx(railHandle, 0, NULL)
it will search for 100k packets, whileRAIL_StartRx(railHandle, 1, NULL)
will search for 500k packets.Different packet
Let's say your protocol defines a variable length data packet (1st byte is length) and a fixed, 2B long ACK packet.
In the above setup channel 0 is for data packets, while channel 1 is for ACK packets (both Tx and Rx) - on the same frequency, so these are virtual channels just like in the previous example. A transmitting device would send a data packet on channel 0, then start listening on channel 1 for ACK, while a receiving device would receive on channel 0, and transmit an ACK on channel 1 after successful reception.
Note that the only difference between the PHYs are the length decoding algorithm itself. This is because the configurator can keep a complete fixed length and a variable length configuration, and changing between those is possible by just changing the the frame length algorithm.
Asymmetric protocol (Wireless M-Bus T mode)
Wireless M-Bus T mode defines a very different protocol based on the direction:
The above example implements the "Other device" (typically collector): It receives M2O on channel 0 and transmits O2M on channel 1.
Transmitting modeT M2O messages requires other tricks, see AN1119 for details
Wide bandwidth for CSMA/CA
Some regulatory standards also standardize the bandwidth for CSMA/CA, but your protocol would have better sensitivity with a narrower bandwidth. The trick here is to understand that this is actually a subset of the Asymmetric protocol usecase: Since CSMA/CA will listen on the same channel that you will use for transmit and the bandwidth setting in the configurator only affects receive performance, you can define a virtual channel with your setup with the required wider bandwidth:
In the above example, channel 0 uses the bandwidth recommended by the configurator, while channel 1 has the fixed, wide bandwidth. You would receive on channel 0 with high sensitivity, and transmit on channel 1. The CSMA/CA preceding the transmit would also happen on channel 1 using the fixed, wide bandwidth.
You can check the bandwidth configured for channel 0 in the
efr32_radio_configurator_log.txt
file: It's 4.8kHz.Uneven channel spacing
Channel groups can be used in "single-PHY" mode as well: one example for that is setting up uneven channel spacing. Let's say you have to receive on 868.0MHz, 868.33MHz and 868.4MHz - so the channel spacing is 330kHz and 70kHz at the same time - You obviously can't configure this with a single channel spacing field, but you can with channel groups:
With the above setup, you will have channels 0, 1 and 2 on the required frequencies.
Power limited channels
This is still a "single-PHY" usecase: Let's say you have to receive on 40 channels, starting at 2.402GHz with channel spacing of 1MHz. However, to meet regulatory standards, you must lower the transmit power on the first and last channel to 16dBm.
The above setup meets the requirements. Note that channel 0 and 39 are both defined twice. First as limited power, then the same as the other 38 channels. The order of channel groups are important: RAIL always loads the first channel group which includes the requested channel.
Channel number offset was used in the above video. The frequency of a given channel is calculated as follows:
Channel number offset is the same as First channel number if it's disabled. This is useful for uneven channel spacing, since you want the channel group to start on the selected base frequency, and not offset by
firstChannelNumber*channelSpacing
. However, setting it to 0 is useful for overrides: This way, the power limited channel 39 in the above config will be exactly the same as in the original channel group. See understanding RAIL config files tutorial for more details.You also have to keep in mind that the actual output power depends on your board and antenna design. The recommended way to solve this problem is to create a PA conversion curve documented in AN1127, and enable the PA conversions plugin with the generated header:
Simplicity Studio ships with conversion curve headers for Silicon Labs boards - if you create a project for a Silicon Labs board, simply enable the PA Conversions plugin to include the correct header for your board.
Note that RAIL only lowers and limits the output power on channel change, it won't increase it back if you switch to an unlimited channel from a limited one.
How Channel-based Multi-PHY actually works
See the Multi-PHY capabilities chapter in the Understanding RAIL config files tutorial.
Tips and Tricks for Channel-based Multi-PHY
Advanced fields
In general, you should disable all advanced settings if you change much in a configuration - the same guidance applies for Multi-PHY. What makes this advice even more important for Multi-PHY is that it's easy to miss a problem if something misconfigured under the protocol setup makes your channel group misbehave.
Inheritance from protocol
When you add an element to the channel-based configuration, it will initially inherit a value from the root protocol - and that is the only time this inheritance happens. If you later change the root protocol (ex: switch to a pre-configured PHY), doing so won't update the channel-based config properties for any channel groups, even if you hadn't changed the value inherited from the original protocol. This behavior is depicted in the following video:
Channel group change performance
If you have significant differences between channel groups, channel changing that involves a channel group change will take somewhat longer - in most cases, this is not too significant, but you should measure and calculate with it if you need short channel switch times (PRS channels can be used for measuring this time, see the tutorial about debugging).
Also, if you have 3 channel groups, and only a single field is different between A and B, but C is very different compared to the other two, channel change between A and B will take the same time as channel change between C and A or C and B.
If you have a lot of channel groups in the protocol, this could also cause a small but measurable delay during channel change: it takes some time for RAIL to look up the channel group which includes the requested channel.
All of these delays are a result of the implementation in RAIL, see the Understanding RAIL config files tutorial for more details.
In some cases, it's possible to work around these problems by combining channel-based and protocol-based Multi-PHYs:
Protocol-based Multi-PHY
The theory
The main idea behind Protocol-based Multi-PHY is to create multiple - completely independent - configs, and use
RAIL_ConfigChannels()
to select the required one.Protocols can be created with the
icon.
The order of protocols will be used in the generated
channelConfigs[]
array, i.e. the first protocol in the list will have index 0 and so on.Multiple regions
Let's say your device operates on the 868MHz 500kb/s pre-configured PHY in Europe, and on the 915MHz 100kb/s pre-configured PHY in North America.
With the above setup, calling
RAIL_ConfigChannels(railHandle, channelConfigs[0], NULL)
will load the European config, whileRAIL_ConfigChannels(railHandle, channelConfigs[1], NULL)
will load the North American config.RAILTest support
Protocol-based Multi-PHY is also useful if you want to test multiple configuration setups using RAILTest: You can change between protocols using the
setConfigIndex
RAILTest command.Configuration for a DMP setup
The above setup can be also used in a Dynamic Multiprotocol setup: You'll need two rail handles, let's call them railHandle868 and railHandle915. During the initialization of the 868MHz protocol, you'll call
RAIL_ConfigChannels(railHandle868, channelConfigs[0], NULL)
, and during the initialization of the 915MHz protocol, you'll callRAIL_ConfigChannels(railHandle915, channelConfigs[1], NULL)
. From this point, RAIL will automatically load the required config using the RAIL scheduler.PA configuration
RAIL will automatically change everything needed for Multi-PHY during channel or protocol change, except one thing: The PA (power amplifier) configuration. On Wireless Geckos, changing the PA is needed if you change between a sub-GHz and a 2.4GHz config (or maybe between a low power and high power 2.4GHz config).
To solve this, you should pass a function pointer as the last parameter of
RAIL_ConfigChannels
. In the examples, we call itRAILCb_RadioConfigChanged
and it will be called when RAIL switches between channel groups, with theRAIL_ChannelConfigEntry_t
as an argument. You can use that argument to figure out if you need to change the PA or not, and callRAIL_ConfigTxPower
if needed.This is implemented in both RAILTest and in SimpleTRX-MultiPHY.
IR Calibration in Multi-PHY configs
If you're not sure what IR calibration is, please refer to the Calibration tutorial.
Configuration and cache variables in rail_config.c
This section is copied from the Understanding RAIL Config Files tutorial.
The radio configurator generates two elements - an array and a struct - that are involved in IR calibration, they look like this:
The first one,
generated_irCalConfig
, tells RAIL how to run the IR calibration on this PHY. This array is passed as part of the PHY info array. The second one,generated_entryAttr
is used to cache the calibration result. When you runRAIL_CalibrateIr()
,RAIL_ApplyIrCalibration()
orRAIL_Calibrate
, it will store the IR calibration result here. If you load a channel with the special entryAttr value ofRAIL_CAL_INVALID_VALUE
(API doc), it will trigger the eventRAIL_EVENT_CAL_NEEDED
. This is passed as part of the channel entry configuration.IR Calibration in Protocol-based Multi-PHY
IR calibration should be performed for all protocols. This will be requested from the application through the
RAIL_EVENT_CAL_NEEDED
event. After the calibration is performed on each protocol, RAIL can use the cached results from these prior operations. When doing so, the calibration values will be applied automatically and near-instantaneously.IR Calibration in Channel-based Multi-PHY
The radio configurator will automatically detect if channel groups are similar enough to use a common calibration (e.g. only the packet is different). In these cases, both
irCalConfig
andentryAttr
will be the same. Calibration should be performed on each channel group that has its ownirCalConfig
andentryAttr
. RAIL will detect this and request calibration throughRAIL_EVENT_CAL_NEEDED
if you select a channel group that has a different IR calibration setup andRAIL_CAL_INVALID_VALUE
inentryAttr
. Once calibration is performed on each channel group that needs it, RAIL can then cache these values for future use. As in Protocol-based Multi-PHY, with cached results the calibration values will be applied automatically and near-instantaneously.Calibration on Multi-PHY at startup
In some cases, using the
RAIL_EVENT_CAL_NEEDED
event to calibrate is not viable, and you might want to explicitly request calibration on everything at startup. To do that, you shouldirCalConfig
andentryAttr
For the latter, you should check the generated rail_config.c, there's no easy way to figure out which channel to calibrate from code. To loop through the channel groups, you can use the API
RAIL_PrepareChannel()
to trigger the PHY loading. The calibration code will likely look something like this:In the above example we have 5 channel groups that require calibration (
NUMBER_OF_GROUPS
). The first channel of each channel group is stored incalibrationChannels
.If you have more protocols, you can simply load them using
RAIL_ConfigChannels
. Note that the first channel of the protocol is automatically configured, soRAIL_PrepareChannel()
is only needed for Channel-Based Multi-PHY.Multi-PHY in Connect
The Connect stack has some limitations on Multi-PHY features:
Related documentation