While RAIL attempts to be chip-agnostic, certain hardware specifics can't be overlooked. Where possible, missing features will be simulated in software, but performance characteristics may vary. This section covers EFR32-specific content including hardware-specific configurations or calibrations.


High Frequency Clocks

The EFR32 has a configurable clock tree with a number of different prescalars. One thing to keep in mind, however, is that the radio must always run off of an HFXO at 38.4 MHz. This means that you must switch your HFCLK source to the HFXO before calling RAIL_Init() and make sure to leave the CMU->HFPRESC set to 1 to prevent dividing down the radio clock. You are free to scale the peripheral (CMU->HFPERPRESC) or Cortex (CMU->HFCOREPRESC) clocks using their prescalars which are further down the clock tree. Keep in mind that this may introduce extra wait states when interacting with the radio and slow down some operations.

Low Frequency Clocks

RAIL can use a low frequency clock on the EFR32 to synchronize its time base across EM2 sleep. This only happens if you call RAIL_ConfigSleep with RAIL_SleepConfig_t::RAIL_SLEEP_CONFIG_TIMERSYNC_ENABLED. On the EFR32xG1 and EFR32xG12 this uses the RTCC's clock which is the LFE clock with a possible additional prescalar. The slower the clock source here the longer it will take to synchronize since the process requires several ticks of the RTCC clock. On the EFR32xG13 and EFR32xG14 there is a separate timer to help with this synchronization, but either the LFXO or LFRCO are still used. When calling RAIL_ConfigSleep() the code will first try to setup the timer with the LFXO if it's started, then fallback to the LFRCO, and finally assert if no LF clocks are running.

Packet Buffer Restrictions

The RAIL library for the EFR32 optionally allocates an internal buffer to store receive data contiguously. Other than being byte-aligned, the buffer has no further alignment restrictions. The buffer is set to 512 bytes at build time by default, but can be changed by calling RAIL_SetRxFifo, for example in an implementation of RAILCb_SetupRxFifo. The receive FIFO's size must be a power of 2 between 128 and 4096 for hardware compatibility. This choice limits the maximum size of receive packets in packet mode and determines the size of the receive FIFO in FIFO mode. Since each receive packet has 6 bytes of overhead, you can only receive up to one 506 byte packet without overriding the RX FIFO or switching to FIFO mode. In FIFO mode, you must read out packet data as you approach this limit and store it off to construct the full packet later. Note that the 6 byte overhead may increase or decrease in future releases as the functionality is changed though large jumps are not expected in either direction.

The transmit FIFO must be set at runtime with RAIL_SetTxFifo(). The transmit FIFO must also be byte-aligned, with no further restrictions. Its size has the same restrictions as the receive FIFO. The size must be a power of 2 from 64 to 4096 for hardware compatibility. Note that there is no difference between packet and FIFO mode on the transmit side. A packet may either be loaded all at once or in pieces using the RAIL_WriteTxFifo() function. You may also use the RAIL_SetTxFifoThreshold() function and the RAIL_EVENT_TX_FIFO_ALMOST_EMPTY event to load data as there is space available in the FIFO during transmission.

Data Reception Sources

When receiving data, you can configure hardware to provide data from three different hardware sources.

First, you can configure the hardware to provide a packet of information. This configuration uses the built-in demodulator and frame controller. Use RAIL_RxDataSource_t::RX_PACKET_DATA to enable this receive source.

Second, you can configure hardware to provide data directly from the demodulator. In this mode, hardware demodulation is used, but the user is responsible for implementing frame controller functionality. In other words, preamble detection, sync word detection, CRC validation, and so on must be performed in software by the application. All data returned is represented as 8-bit 2's-compliment values. Use RAIL_RxDataSource_t::RX_DEMOD_DATA to enable the receive source.

Third, you can configure hardware to provide data directly from the I and Q ADCs. In this mode, the user is responsible for implementing demodulator and frame controller functionality. The receive signal hardware has a 19-bit dynamic range. The user can select whether to return the upper 16 bits of the 19-bit value (RAIL_RxDataSource_t::RX_IQDATA_FILTMSB) or the lower 16 bits (RAIL_RxDataSource_t::RX_IQDATA_FILTLSB). All data returned is represented as 16-bit 2s-compliment values. The I and Q values are put into the buffer in an alternating fashion where the first 16-bit value is the first I ADC value and the second 16-bit value is the first Q ADC value, and so on.

Interrupt Vectors

The RAIL library for EFR32 implements all of the interrupt vectors for radio peripherals, which is required for the RAIL library to function correctly. RAIL does not, however, set the priorities of these interrupt vectors. That must be handled by your application using the CMSIS NVIC_SetPriority() API or direct manipulation of the NVIC. Below is the full list of interrupts used by the radio. You must run them all at the same priority which is what all Silicon Labs RAIL applications do by default. You are free to choose what that priority is based on the requirements of your application (e.g., putting them below FreeRTOS OS atomic for access to OS functions in RAIL callbacks). Keep in mind that putting the radio interrupts at too low a priority can cause missed packets and other radio performance issues. A restriction on EFR32xG12 and newer is that you must not disable radio interrupts for longer than a quarter of the RAIL timebase (2^32/4 microseconds or 18 minutes) to ensure proper timekeeping. See EFR32xG1x_Interrupts or follow the links below for more information.

Chip-specific Initialization

EFR32 Hardware Initialization

EFR32 includes EMLIB and EMDRV to create a basic HAL layer. A lot of this initialization code is completely up to you, but there are a couple of requirements when building a RAIL app. Specifically, the radio will only work if you are running off a high-precision crystal oscillator. Since some APIs will assume this is running, make sure to initialize and switch to the crystal before calling any radio APIs.

For the Wireless Starter Kit (WSTK), you can use the crystal configuration in the HAL configuration header file for your specific kit. Example code for this is shown below. If you have a custom hardware layout, you may want to create your own HFXOInit structure to account for things such as your specific CTUNE value.

#include "bsp.h" // Contains WSTK versions of the HFXO init structure
void efrStartup(void)
// Initialize the HFXO using the settings from the WSTK bspconfig.h
// Note: This configures things like the capacitive tuning CTUNE variable
// which can vary based on your hardware design.
// Switch HFCLK to HFXO and disable HFRCO
CMU_ClockSelectSet(cmuClock_HF, cmuSelect_HFXO);
CMU_OscillatorEnable(cmuOsc_HFRCO, false, false);

Radio-specific Initialization

Currently, RAIL_ConfigTxPower() and RAIL_ConfigPti() initialization functions depend on the board configuration and must be manually called at startup. You MUST initialize the power amplifier (PA) to transmit. You may initialize the packet trace interface (PTI) for use in debugging.

Power Amplifier (PA) Initialization

High Power 2.4GHz PA, Low Power 2.4GHz PA, and Sub GHz PA PAs are available for the EFR32xG1 family of chips. The specific set of PAs you have available and the supported power levels for those PAs are determined by your part number. See the appropriate data sheet for more details.

Each PA supports a raw power level value of type RAIL_TxPowerLevel_t where a numerically smaller number causes less power to be output from the chip than a higher number. Keep in mind that these values may be capped at the upper and lower ends and do not necessarily map linearly to output power in dBm. To map these values to and from dBm values, the RAIL library uses the RAIL_ConvertRawToDbm() and RAIL_ConvertDbmToRaw() APIs. By default, these use whatever piecewise linear power curves were passed into RAIL_InitTxPowerCurves() to do the conversion. On Silicon Labs boards, these curves are included with the board header files. For custom boards, this must be measured using test equipment to take into account the differences in the RF path that may exist. In addition, for even more custom use cases, the conversion functions can be strongly defined in the customer application and provide whatever type of mapping is found to be most effective. Below is an example of using the standard APIs to initialize all PAs on a dual band chip and switch to the 2.4 GHz high-power PA.

#include "rail.h"
#include "rail_chip_specific.h"
#include "plugin/pa-conversion/pa_conversion_efr32.h"
RAIL_DECLARE_TX_POWER_VBAT_CURVES(piecewiseSegments, curvesSg, curves24Hp, curves24Lp);
// Must be called with a valid RAIL_Handle_t returned by RAIL_Init()
void initPa(RAIL_Handle_t myRailHandle)
// Initialize the RAIL Tx power curves for all PAs on this chip
RAIL_TxPowerCurvesConfig_t txPowerCurvesConfig = {
if (RAIL_InitTxPowerCurves(&txPowerCurvesConfig) != RAIL_STATUS_NO_ERROR) {
// Could not initialize transmit power curves so something is configured
// wrong. Please fix and rebuild.
// Switch to the 2.4GHz HP PA powered off the 1.8V DCDC connection
RAIL_TxPowerConfig_t railTxPowerConfig = {
RAIL_TX_POWER_MODE_2P4_HP, // 2.4GHz HP Power Amplifier mode
1800, // 1.8V vPA voltage for DCDC connection
10 // Desired ramp time in us
if (RAIL_ConfigTxPower(myRailHandle, &railTxPowerConfig)
// Error: The requested PA could not be selected. Fix your configuration
// and try again.
// Set the output power to the maximum supported by this chip
RAIL_SetTxPower(myRailHandle, 255);

Packet Trace Interface (PTI) Initialization

Packet trace on the EFR32 provides a mechanism for viewing transmitted and received radio packets for network sniffing or debugging. It can also be captured by a WSTK and sent to Simplicity Studio for viewing data in its Network Analyzer tool.

To use this functionality, configure the pins to use for this output and, optionally, how to format data. Note that the WSTK requires the following output format:

Mode: 8 bit UART mode
Baudrate: 1.6Mbps
Framing Signal: Enabled

Introducing changes is currently unsupported by the WSTK. Other output formats are currently unsupported.

To choose the pins, look in the applicable data sheet and select a valid route location for the FRC_DOUT and FRC_DFRAME signals. These map to PTI.DATA and PTI.FRAME on the WSTK respectively. Once you've found pins that work for your hardware, configure the RouteLocation, port, and pin fields in the PTI initialization structure. When using the WSTK for example, initialize as follows:

#include "rail.h"
#include "rail_chip_specific.h"
void initPti(void)
RAIL_PtiConfig_t ptiConfig = {
RAIL_PTI_MODE_UART, // Only supported output mode for the WSTK
1600000, // Choose 1.6 Mbps for the WSTK
6, // WSTK uses location 6 for DOUT
gpioPortB, // FRC_DOUT#6 is PB12
12, // FRC_DOUT#6 is PB12
6, // WSTK uses location 6 for DFRAME
gpioPortB, // FRC_DOUT#6 is PB13
13, // FRC_DOUT#6 is PB13
// Initialize the Packet Trace Interface (PTI) for the EFR32
// Enable Packet Trace (PTI)

Other Radio GPIO Functions

Various useful signals related to the radio can be output on a GPIO using the Peripheral Reflex System (PRS). PRS is an advanced system where you can route signals to channels and then output those channels to a number of configurable locations. For more information, see the PRS chapter in the reference manual.

A list of some of the most interesting PRS signals related to the radio is shown below along with how to enable them in the PRS. The definition for these signals for a given chip can be found in the release at platform/Device/SiliconLabs/<chipFamily>/Include/<chipFamily>_prs_signals.h where <chipFamily> would be the beginning of the part number (EFR32MG12P for example).

Signal Summary
PRS_RAC_ACTIVE Radio enabled
PRS_RAC_TX Transmit mode enabled
PRS_RAC_RX Receive mode enabled
PRS_RAC_LNAEN LNA enabled for Rx
PRS_RAC_PAEN PA enabled for Tx
PRS_MODEM_PREDET Preamble detected
PRS_MODEM_TIMDET Timing detected

The example below shows how to configure a PRS channel to output RAC_RX on a GPIO. In this example, the WSTK is used with the BRD4153A radio board. The code below will put RAC_RX on PRS Channel 0 and output PRS Channel 0 on pin PC10 which is wired to WSTK_P12 and EXP_HEADER15 on the WSTK.

#include "em_cmu.h"
#include "em_prs.h"
#include "em_gpio.h"
#include "em_device.h"
void enableDebugGpios(void)
// Turn on the PRS and GPIO clocks to access their registers.
CMU_ClockEnable(cmuClock_PRS, true);
CMU_ClockEnable(cmuClock_GPIO, true);
// Configure PC10 as an output.
GPIO_PinModeSet(gpioPortC, 10, gpioModePushPull, 0);
// Configure PRS Channel 0 to output RAC_RX.
// Configure PRS Channel 0 to use output location 12 (PC10 - see data sheet).
// Enable PRS Channel 0.

Required Dependencies

Most of the RAIL library is self-contained, however, there are some dependencies on external functions, such as functions from the C standard library, EMLIB, and CMSIS. Below is a complete list of these dependencies. Note that changing the implementation of any of these functions while maintaining their functionality could still impact RAIL operation.

Group Functions
CMSIS SystemHFXOClockGet()
EMLIB (em_cmu) CMU_ClockEnable
EMLIB (em_gpio) GPIO_PinModeSet()
EMLIB (em_system) SYSTEM_ChipRevisionGet()
EMLIB (em_core) CORE_EnterCritical()
EMLIB (em_emu) EMU_DCDCLnRcoBandSet()
stdlib memcpy()

Peripherals Consumed by RAIL

Certain functionality in RAIL requires some of the chip's peripherals. The specifics of these requirements are enumerated below.

RAIL Timer Synchronization

If you call RAIL_ConfigSleep with RAIL_SleepConfig_t::RAIL_SLEEP_CONFIG_TIMERSYNC_ENABLED to keep the clock synchronized across sleep RAIL must use the PRS and RTCC.

  • On all platforms PRS channel 7 is used to perform the synchronization. This channel is only initialized one time when calling the RAIL_ConfigSleep function so you must not change it after that or the synchronization may fail.
  • On the EFR32xG1 and EFR32xG12 platforms the timer synchronization also uses RTCC channel 0. This channel should not be used by the application after a call to RAIL_Sleep and before the corresponding RAIL_Wake call. The application is also responsible for enabling this top level interrupt if they want it to serve as a wake source and implementing an interrupt handler that clears the RTCC_IFC_CC0 flag in the RTCC->IF register any time it is pended to prevent the chip from getting stuck in the handler. RAIL does not need to be tied into the interrupt handler in any other way.

Entropy Generation

EFR32 supports true entropy collection using the radio. It is able to collect 1 bit per radio clock cycle while in receive mode. This means that the receiver must be enabled when collecting entropy. If you attempt to transmit or otherwise delay entry into receive, data collection will take longer. You can still receive packets while collecting entropy. This means that requesting random numbers longer than the length of your preamble and sync word may trigger a packet reception. This may be fixed in a future revision but for now it ensures a proper packet reception during a random data collection.

Due to the nature of random data collection, either the number of bytes or zero is returned after the full amount requested. Zero is returned if the radio is uninitialized and cannot be enabled for collection.

RAIL Timebase

EFR32 uses a dedicated radio timer to create the RAIL timebase and perform scheduled transmit and receive operations. This timer ticks at roughly every 2 us on the EFR32xG1 platform and 0.5 us on the EFR32xG12 and newer. The exact tick value depends on the high-frequency crystal in use so there may be a small rounding error. All internal APIs account for this error, so over long periods of time it is generally unnoticeable. Note that RAIL uses the hardware tick rate to create a 1 us time base that is exposed to the user. This is all that should matter for most users and the information here is included to explain the underlying implementation.

Radio Calibration

EFR32 supports image rejection (IR) calibration and VCO temperature calibration. You may choose to enable one or both depending on your use case. Using both is recommended for optimal performance of the radio.

  • Image Rejection Calibration (IRCAL)
    • This calibration should be run each time your PHY configuration changes. It is based on the modulation scheme, frequency band, and other radio settings. RAIL will request this by calling RAIL_Config_t::eventsCallback with the RAIL_EVENT_CAL_NEEDED bit set and the RAIL_CAL_ONETIME_IRCAL bit set in RAIL_GetPendingCal().
    • Using a proper value for this will improve sensitivity by several dBm, so it's highly recommended.
    • It can take on the order of 700 ms to complete this calibration. You may want to save off a known good value for this calibration and load it each time you switch PHYs to save time.
    • This calibration should be initialized before calling RAIL_ConfigChannels() to ensure that it is properly configured by the time the first channel is set up. The initialization involves enabling the algorithm and passing in parameters to configure the algorithm for the specific PHY generated by the EFR32 Radio Configurator.
    • You cannot use the radio while this calibration is being performed or you may generate an incorrect calibration. Application code should ensure that the radio remains in the idle state during this calibration.
    • This calibration is only meaningful for Zigbee, BLE, and sub GHz radio configurations. For all others you should still enable the calibration but the algorithm will apply a safe, default, calibration value.
  • VCO Temperature Calibration (VCO_TEMPCAL)
    • When staying in receive for a very long time and the temperature changes causing the radio to drift off-frequency, RAIL will request this calibration via RAIL_Config_t::eventsCallback with the RAIL_EVENT_CAL_NEEDED bit set and the RAIL_CAL_TEMP_VCO bit set in RAIL_CalPendingGet().
    • This calibration is automatically run every time receive is entered. If the application, by its nature, frequently re-enters receive mode, this calibration may not need to be enabled.
    • On EFR, the application will get this event when the absolute temperature crosses 0C degrees as well as when the temperature delta from the last calibration increases or decreases by 70C.
    • It is always recommended to enable and handle this calibration since it doesn't add much overhead and is much safer.

State Transition Timing

EFR32 allows automatic transitions from receive to transmit to precisely time transmitted packets after packet reception. During this process, internal calculations are performed which require the received packet's duration, from sync word to end of CRC, to be less than 32 ms. If received packets in a given protocol have an on-air time greater than 32 ms, the automatic transition from receive to transmit may not be used with that protocol because of incorrect timing of that transition.

Radio State Verification

RAIL includes a radio state verification feature capable of verifying radio register contents. There are multiple ways of configuring and running the verification process. All radio state verification should occur when the radio is idle. If not idle, the number of radio registers different from their reference values varies at any given time.

Default vs. Custom Radio Config

When verification occurs, a reference value is compared with a radio register's contents. The reference value is provided through one of two ways.

The radio configuration (originally passed into RAIL_ConfigChannels()) is used as a reference with which to compare radio register values. When the default radio configuration is used for verification, only those addresses flagged as being verifiable by the radio configurator will be checked. Bit 27 of an encoded address indicates whether that address is verifiable or not. For example, encoded address 0x08020004 is flagged as being verifiable and will be verified when RAIL_Verify() is called, but the encoded address 0x00020008 will not be verified.

A custom radio configuration can be provided to the verification API (passed into RAIL_ConfigVerification()). When a custom radio configuration is provided, all addresses in the provided radio configuration will be checked, regardless of whether or not their encoded addresses are flagged as being verifiable.

Approval Callback

When verification occurs, no register value differences are allowed unless an application-level approval callback is defined.

Without an approval callback, any difference will be flagged as data corruption, with RAIL_Verify() returning a status of RAIL_STATUS_INVALID_STATE.

With an approval callback, the application can scrutinize all differences and deem them acceptable or not. By providing an approval callback, individual register fields, instead of the entire register contents, can be compared against reference values at the application level.


Verification can be restarted if necessary.

When running verification from the beginning to completion (by specifying a sufficiently long test duration or by specifying a duration of RAIL_VERIFY_DURATION_MAX), the verification process is indifferent to the restart parameter.

When running verification for a duration that does not permit verification to run to completion, the RAIL_Verify() function will return with a status of RAIL_STATUS_SUSPENDED. This indicates test success for those values already verified, but sufficient time was not permitted to verify all values. In this scenario, the initial run of RAIL_Verify() is indifferent to the value of the restart input, but subsequent runs of RAIL_Verify() should set the restart input to false so that verification will resume where it left off on the previous call. To record where a previous verification left off, the RAIL_VerifyConfig_t structure must be declared by the application and provided to RAIL. These structure contents should only be altered by RAIL.

Configuration and Running Options

With the ability to specify default radio configuration vs. custom radio configuration, approval callback vs. no approval callback, and run to completion vs. run for a specific time, there are multiple ways in which to run RAIL's radio state verification feature. The example below shows running verification in each of these modes.

#include "rail.h"
#include "rail_types.h"
#include "rail_config.h"
RAIL_VerifyConfig_t configVerify;
const uint32_t customRadioConfig[] = {
0x00010048UL, 0x00000000UL,
0x000400A0UL, 0x00004CFFUL,
/* 00A4 */ 0x00000000UL,
/* 00A8 */ 0x00004DFFUL,
/* 00AC */ 0x00000000UL,
0x00012000UL, 0x00000744UL,
bool RAILCb_VerificationApproval(uint32_t address,
uint32_t expectedValue,
uint32_t actualValue)
address, expectedValue, actualValue);
return true OR false; // true = change approved; false = change unapproved
main ()
RAIL_Status_t result;
// Initialize the radio.
railHandle = RAIL_Init(..., ...);
// Associate a radio config with this RAIL handle.
RAIL_ConfigChannels(railHandle, channelConfigs[0], ...);
// Idle the radio before verifying registers.
RAIL_Idle(railHandle, RAIL_IDLE, true);
uint8_t verifyConfigOption = 1; // 1,2,3,4
uint8_t verifyRunOption = 1; // 1,2
// Configure the radio state verification in one of 4 ways:
if (1 == verifyConfigOption) {
// Config Option 1: Use default radio config with no approval callback.
NULL, // use channelConfigs[0] for verification
} else if (2 == verifyConfigOption) {
// Config Option 2: Use default radio config with approval callback.
NULL, // use channelConfigs[0] for verification
} else if (3 == verifyConfigOption) {
// Config Option 3: Use custom radio config with no approval callback.
} else {
// Config Option 4: Use custom radio config with approval callback.
// Run the radio state verification in one of 2 ways:
if (1 == verifyRunOption) {
// Run Option 1: Run verification once to completion.
result = RAIL_Verify(railHandle,
true); // run from the beginning of verification
} else {
// Run Option 2: Run verification for a specific time before returning.
result = RAIL_Verify(railHandle,
100, // run for 100 microseconds before returning
true); // run from the beginning of verification
while (RAIL_STATUS_SUSPENDED == result) {
"verification suspended - run again to continue");
result = RAIL_Verify(railHandle,
100, // run for 100us before returning
false); // run from where verification left off
switch (result) {
responsePrint("verifyRadio", "success, done");
responsePrint("verifyRadio", "invalid input parameter");
responsePrint("verifyRadio", "data corruption");