RAIL Multiprotocol#

 

Overview#

As of version 2.1, the RAIL library supports dynamic multiprotocol. In dynamic multiprotocol, an application can allocate multiple RAIL instances and configure them independently. Some radio operations are then arbitrated by a radio scheduler, which is internal to RAIL, to allow the different instances to coexist. The scheduler attempts to allow as many of these operations to run as possible by moving them around within the bounds allowed by the protocol.

Radio Scheduler#

The radio scheduler is critical for multiprotocol operation. Its job is to arbitrate all radio operations, switch radio configurations, and ensure that radio operations run at the time requested. Not every RAIL function is a radio operation considered by the scheduler. Simple state update functions, such as sl_rail_set_tx_power_dbm(), will directly change the hardware if active or update the state cache if inactive to have that change take affect the next time this protocol is activated. The following RAIL functions are handled by the scheduler:

To arbitrate the radio operations, the scheduler uses a start time along with a priority, slip_time, and transaction_time to schedule the operation. The start time comes from the above APIs implicitly or explicitly requested start time. For example, calling sl_rail_start_tx() uses a start time of right now, while calling sl_rail_start_scheduled_tx() uses the sl_rail_scheduled_tx_config_t::when parameter field as the start time. The other three parameters are passed to the above RAIL functions in their sl_rail_scheduler_info_t p_scheduler_info parameter. The scheduler attempts to run each task at its desired start time, but may also choose to run it up until start time + slip_time. If it cannot run the task within that window, it will trigger a sl_rail_config_t::events_callback with the SL_RAIL_EVENT_SCHEDULER_STATUS event set once that window has passed.

The scheduler is preemptive and will always ensure that a higher-priority task runs over a lower-priority task, but it does not guarantee ordering of these tasks. For example, it could choose to run a short lower-priority task with a small slip_time before a higher-priority task with a long slip_time to reduce dropped operations. Once an operation is started by the radio scheduler, it will either run to completion and trigger a normal RAIL event callback to indicate it is done or it will be aborted by a higher-priority task and ended with a SL_RAIL_EVENT_SCHEDULER_STATUS event. Ultimately, the application is responsible for terminating most operations with a call to sl_rail_yield_radio(). This call cleans up internal state and allows the scheduler to switch to a lower priority operation.

Note

Scheduler Operations#

The APIs mentioned above can be classified as finite operations, infinite operations, and debug operations.

Finite Operations#

Finite operations include the following APIs:

Finite operations make up the majority of the radio scheduler calls. A finite operation has a defined start time and either a defined or estimated end time. Each RAIL handle may only have one of these types of operations queued up at any time. The upper level application code is responsible for waiting until the previous operation has completed before starting a new one like in the single protocol RAIL library. If a new task is issued while a previous task is in progress, or if a higher-priority task interrupts an operation before the user has yielded, it will trigger the SL_RAIL_EVENT_SCHEDULER_STATUS event with an appropriate sl_rail_scheduler_status_t (retrievable by calling sl_rail_get_scheduler_status() within the event handler) and the operation will not be scheduled again.

Once a finite operation has begun, the scheduler will use the same priority until a new finite event is provided or a yield occurs. The scheduler will ensure that unnecessary switches do not occur if this next event is in the near future at the same or a higher priority. For more information see Yielding the Radio.

Infinite Operations#

Infinite operations include the following APIs:

An infinite or background operation is treated by the scheduler as a radio state change. It still has a priority associated with it and will be started whenever it can be, but it will never fail. Another difference is that if this event is interrupted by a higher-priority task, it will be restarted whenever that task finishes where a finite task would be aborted.

Each RAIL handle may have only one infinite task but it may have both an infinite and finite or debug task at the same time. The one interesting aspect of this is that, if a finite task starts for this RAIL handle, the state will be updated to reflect what the infinite task has requested for that finite task's time period even if the infinite task is lower priority than another task in the system. For example, if you have one handle with a sl_rail_start_rx() of priority 5 and another handle with a sl_rail_start_rx() priority of 6 and a sl_rail_start_tx() priority of 3, the scheduler will choose the second handle's finite transmit task. While running this transmit task though, the receiver will be set to on so that, as soon as this transmit completes, the radio will transition from transmit into receive. It will remain in receive until the finite operation yields at which time the first handle's receive will take precedence.

This implementation is intended to get the radio to stay in receive for a very long time on one of the RAIL handles. It is generally only advisable to do this on one radio configuration at a time and for this to be the lowest-priority task. For well defined receive events, such as looking for an ACK scheduled receive, windows or state transitions will generally work better.

Note

  • A known bug exists with turning on an infinite task during a finite task. For now, you must always configure the infinite task first to ensure it takes effect.

  • If using state transitions that can lead from receive to idle, the user must tell the scheduler that this happened with a call to sl_rail_idle(). This restriction will hopefully be removed in future versions.

Debug Operations#

Debug operations include the following APIs:

Debug operations are somewhat special because they don't allow the user to pass a sl_rail_scheduler_info_t structure to them. Instead, they implicitly provide a start time of now and use the highest possible priority. This means that they will have the highest priority and interrupt most other protocol operations to switch into the test mode. Nothing can preempt them once started because the scheduler does not allow tasks of equal priority to preempt. These operations can be stoped by calling sl_rail_stop_tx_stream(), sl_rail_yield_radio(), or sl_rail_idle().

The modal nature of these debug operations is intentional to prevent multiple tone or stream operations from happening concurrently. It is expected that the user will manage these test modes from a higher level to prevent the different protocols from using tone or stream at the same time.

Yielding the Radio#

Yielding is used by finite radio scheduler operations. Once an operation is started, that operation will run until explicitly yielded by the upper layer or interrupted by a higher-priority task. A yield is forced by a call to sl_rail_yield_radio() or sl_rail_idle(). It is important that the application yield the radio as soon as it's done to allow lower-priority tasks to run.

Yielding is not handled by RAIL since terminating an operation can be application-specific. While RAIL may know that an individual transmit or receive operation is completed, there are sometimes other reasons to prevent a switch or to run a follow on operation. For example, if you are using ACKing you may want to wait for the SL_RAIL_EVENT_TXACK_PACKET_SENT after a receive packet instead of yielding right after the receive. This also allows you to chain together many protocol-specific operations without the scheduler constantly context switching. If you do schedule a follow on operation that is far in the future, the scheduler will also be smart enough to treat this as a yield and slot in other lower-priority operations.

This does mean that you must carefully look through the Events in RAIL and subscribe to any that can impact your state machine. Specifically, things like transmit or receive success conditions, transmit and receive error conditions, and scheduler events (SL_RAIL_EVENT_SCHEDULER_STATUS, SL_RAIL_EVENT_CONFIG_UNSCHEDULED, and SL_RAIL_EVENT_CONFIG_SCHEDULED) are important to keep track of. These would likely be needed in any upper layer to properly update the internal state machine as well so they're likely already in the implementation.

Building a Multiprotocol Application#

The RAIL API is the same whether the targeted application is using the multiprotocol version or not. This was done intentionally to make switching between the two versions simple. From a build perspective, choose the desired RAIL library binary file to link into the application. For multiprotocol applications, the file name is librail_multiprotocol_CHIP_COMPILER_release.a while for other applications the file name is librail_CHIP_COMPILER_release.a.

Optionally, non-multiprotocol applications can save memory by passing NULL for all sl_rail_scheduler_info_t pointers in APIs.

Starting in RAIL 2.12, the RAIL multiprotocol library internally provides two statically allocated RAM state buffers supporting two protocols. If your multiprotocol application needs more than two protocols, additional state buffers for them must be provided by calling sl_rail_add_state_buffer_3() or sl_rail_add_state_buffer_4() prior to calling sl_rail_init() for these protocols. If more than four protocols are needed, contact Silicon Labs for advice.

Example#

Below is a simple example showing how to initialize two RAIL protocols and have one stay in infinite receive while the other one periodically transmits a packet at a higher priority.

#include <stdint.h>
#include <stdbool.h>
#include <assert.h>
#include "sl_rail.h"
#include "sl_rail_util_pa_conversions.h" // in sl_rail_util_pa plugin
#include "rail_config.h" // Generated by radio calculator

#define TX_FIFO_BYTES (128)  // Any power of 2 from [64, 4096] on the EFR32

static sl_rail_handle_t rail_handle_1 = SL_RAIL_EFR32_HANDLE;
static sl_rail_handle_t rail_handle_2 = SL_RAIL_EFR32_HANDLE;

static sl_rail_tx_power_t tx_power_ddbm = 200; // Default to 20 dBm

static SL_RAIL_DECLARE_FIFO_BUFFER(aligned_tx_fifo_1, TX_FIFO_BYTES);
static uint8_t *tx_fifo_1 = (uint8_t *)aligned_tx_fifo_1; // For byte access
static SL_RAIL_DECLARE_FIFO_BUFFER(aligned_tx_fifo_2, TX_FIFO_BYTES);
static uint8_t *tx_fifo_2 = (uint8_t *)aligned_tx_fifo_2; // For byte access

// Boolean to track when a packet has been sent.
static bool packet_send_complete = true;

static void radio_event_handler(sl_rail_handle_t rail_handle,
                                sl_rail_events_t events)
{
  // Note that two different callbacks above could be used,
  // but this example only uses one which is split based on the handle.
  if (rail_handle == rail_handle_1) {
    // Handle events for protocol 1.
    // NOTE: Since in infinite receive, the radio does not have to be yielded
    // here as below. If this were changed, the radio would have to be yielded here.
  } else if (rail_handle == rail_handle_2) {
    // Handle any packet completion event (success or failure) and set us up
    // to send another packet.
    if (events & (SL_RAIL_EVENT_TX_PACKET_SENT
                  | SL_RAIL_EVENT_TX_ABORTED
                  | SL_RAIL_EVENT_TX_UNDERFLOW
                  | SL_RAIL_EVENT_SCHEDULER_STATUS)) {
      packet_send_complete = true;
      sl_rail_yield_radio(rail_handle);
    }
  }
}

// Initializes the two PHY configurations for the radio.
void radio_initialize(void)
{
  sl_rail_status_t status;
  // Creates each RAIL handle with their own configuration structures.
  // Below configs for each protocol utilize a shared RX FIFO and packet buffer
  // but have distinct TX FIFOs.
  sl_rail_config_t rail_config_1 = {
    .events_callback = &radio_event_handler,
  //.p_opaque_handle_0 = ... // app-specific if desired
  //.p_opaque_handle_1 = ... // app-specific if desired
  //.opaque_value = ... // app-specific if desired
    .rx_packet_queue_entries = SL_RAIL_BUILTIN_RX_PACKET_QUEUE_ENTRIES,
    .rx_fifo_bytes = SL_RAIL_BUILTIN_RX_FIFO_BYTES,
    .tx_fifo_bytes = sizeof(aligned_tx_fifo_1),
    .tx_fifo_init_bytes = 0U,
    .p_rx_packet_queue = sl_rail_builtin_rx_packet_queue_ptr,
    .p_rx_fifo_buffer = sl_rail_builtin_rx_fifo_ptr,
    .p_tx_fifo_buffer = aligned_tx_fifo_1,
  };
  sl_rail_config_t rail_config_1 = {
    .events_callback = &radio_event_handler,
  //.p_opaque_handle_0 = ... // app-specific if desired
  //.p_opaque_handle_1 = ... // app-specific if desired
  //.opaque_value = ... // app-specific if desired
    .rx_packet_queue_entries = SL_RAIL_BUILTIN_RX_PACKET_QUEUE_ENTRIES,
    .rx_fifo_bytes = SL_RAIL_BUILTIN_RX_FIFO_BYTES,
    .tx_fifo_bytes = sizeof(aligned_tx_fifo_2),
    .tx_fifo_init_bytes = 0U,
    .p_rx_packet_queue = sl_rail_builtin_rx_packet_queue_ptr,
    .p_rx_fifo_buffer = sl_rail_builtin_rx_fifo_ptr,
    .p_tx_fifo_buffer = aligned_tx_fifo_2,
  };
  sl_rail_util_pa_init(); // Establish PA conversion table(s)
  status = sl_rail_init(&rail_handle_1, &railconfig_1, NULL);
  assert(status == SL_RAIL_STATUS_NO_ERROR);
  status = sl_rail_init(&rail_handle_2, &railconfig_1, NULL);
  assert(status == SL_RAIL_STATUS_NO_ERROR);
  // Here rail_handle_1 and _2 have been converted to a real protocol instance handles

  // Configures radio according to the generated radio settings.
  status = sl_rail_config_channels(rail_handle_1, channelConfigs[0],
                                   &sl_rail_util_pa_on_channel_config_change);
  assert(status == SL_RAIL_STATUS_NO_ERROR);
  status = sl_rail_config_channels(rail_handle_2, channelConfigs[1],
                                   &sl_rail_util_pa_on_channel_config_change);
  assert(status == SL_RAIL_STATUS_NO_ERROR);
  status = sl_rail_set_tx_power_dbm(rail_handle_2, tx_power_ddbm);
  assert(status == SL_RAIL_STATUS_NO_ERROR);

  // Configures the most useful callbacks plus catch a few errors.
  status = sl_rail_config_events(rail_handle_1,
                                 SL_RAIL_EVENTS_ALL,
                                 (SL_RAIL_EVENT_TX_PACKET_SENT
                                  | SL_RAIL_EVENT_TX_ABORTED
                                  | SL_RAIL_EVENT_TX_UNDERFLOW
                                  | SL_RAIL_EVENT_SCHEDULER_STATUS
                                  | SL_RAIL_EVENT_RX_PACKET_RECEIVED
                                  | SL_RAIL_EVENT_RX_FRAME_ERROR));
  assert(status == SL_RAIL_STATUS_NO_ERROR);
  status = sl_rail_config_events(rail_handle_2,
                                 SL_RAIL_EVENTS_ALL,
                                 (SL_RAIL_EVENT_TX_PACKET_SENT
                                  | SL_RAIL_EVENT_TX_ABORTED
                                  | SL_RAIL_EVENT_TX_UNDERFLOW
                                  | SL_RAIL_EVENT_SCHEDULER_STATUS
                                  | SL_RAIL_EVENT_RX_PACKET_RECEIVED
                                  | SL_RAIL_EVENT_RX_FRAME_ERROR));
  assert(status == SL_RAIL_STATUS_NO_ERROR);
}

int main(void)
{
  sl_rail_status_t status;
  sl_rail_scheduler_info_t scheduler_info;
  uint32_t send_time;

  // Initializes the radio and creates the two RAIL handles.
  radio_initialize();

  // Only set priority because transaction_time is meaningless for infinite
  // operations and slip_time has a reasonable default for relative operations.
  sl_rail_scheduler_info_t scheduler_info = {
    .priority = 200,
  };
  status = sl_rail_start_rx(rail_handle_1, 0, &scheduler_info);
  assert(status == SL_RAIL_STATUS_NO_ERROR);

  // Starts the first send 2 seconds after getting set up.
  send_time = sl_rail_get_time(rail_handle_2) + 2000000;

  // Issues a transmit periodically on the second RAIL handle.
  while (true) {
    if (packet_send_complete) {
      sl_rail_schedule_tx_config_t scheduled_tx_config = {
        .when = send_time,
        .mode = SL_RAIL_TIME_ABSOLUTE,
      };
      uint8_t packet_data[] = { 0, 1, 2, 3, 4, 5, 6, 7, 7, 8, 10 };

      // This assumes the Tx time is around 10 ms but should be tweaked based on
      // the specific PHY configuration.
      scheduler_info.priority = 100;
      scheduler_info.slip_time = 50000;
      scheduler_info.transactionTime = 10000;

      // Loads the transmit buffer with something to send.
      status = sl_rail_write_tx_fifo(rail_handle_2, packet_data,
                                     sizeof(packet_data), true);
      assert(status == SL_RAIL_STATUS_NO_ERROR);

      // Transmits this packet at the specified time or up to 50 ms late.
      status = sl_rail_start_scheduled_tx(rail_handle_2,
                                          0,
                                          SL_RAIL_TX_OPTIONS_DEFAULT,
                                          &scheduled_tx_config,
                                          &scheduler_info);
      if (status == SL_RAIL_STATUS_NO_ERROR) {
        packet_send_complete = false;
        send_time += 2000000; // Add 2 seconds to the previous time.
      } else {
        // In the current configuration this should never happen.
        assert(false);
      }
    }
  }

  return 0;
}

Understanding the Protocol Switch Time#

Current EFR32 chips that run dynamic multiprotocol code only have a single radio in their hardware. As a result, when DMP switches protocols, it must fully reconfigure the radio accordingly. The time this reconfiguration takes, as well as the time it takes the scheduler to decide which radio task to run, and timings passed into RAIL via sl_rail_set_state_timing() (idle_to_rx, idle_to_tx), make up the Protocol Switch Time. This is essentially the amount of time in advance the radio must be made aware of new tasks in order to execute them on time. If this time is not respected at higher layers, users may get a sl_rail_scheduler_status_t that indicates a failed scheduled event. By default, RAIL uses SL_RAIL_TRANSITION_TIME_US as the time for all protocol switches. The following sections explain how to characterize this time for a specific app and reconfigure it.

Measuring the Protocol Switch Time#

For users using only Silicon Labs provided standards-based stacks, the provided SL_RAIL_TRANSITION_TIME_US will be accurate. However, users implementing proprietary stacks or complex callbacks around scheduler events (SL_RAIL_EVENT_CONFIG_UNSCHEDULED, SL_RAIL_EVENT_CONFIG_SCHEDULED, and SL_RAIL_EVENT_SCHEDULER_STATUS) may need to recharacterize this time if they find that the current switch time is causing scheduling failures in their application. Users can empirically determine this time for their system with the following algorithm: On the EFR, set a small (e.g. 0) transition time using sl_rail_set_transition_time() (this API must be called before sl_rail_init() is called on any protocols). On protocol 1, enter infinite RX with sl_rail_start_rx(). On protocol 2, request an absolutely scheduled TX for some time in future (e.g. sl_rail_get_time(rail_handle_2) + 1000000). For small transition time values, the application should get SL_RAIL_EVENT_SCHEDULER_STATUS on protocol 2, with a scheduler status of SL_RAIL_SCHEDULER_STATUS_SCHEDULED_TX_FAIL. Repeat this test (resetting the device each time between runs to ensure sl_rail_set_transition_time()) is called before sl_rail_init()) with increasing transition times. Eventually, for some large enough value, the application should begin getting SL_RAIL_EVENT_TX_PACKET_SENT events, indicating that the supplied transition time was sufficient, and thus the radio was able to transition in time to complete the scheduled transmit at the correct time. Ideally this test should be run in both directions (i.e. repeat the test, swapping protocols 1 and 2).

Sample code for the above algorithm is provided below:

#include <stdint.h>
#include <stdbool.h>
#include <assert.h>
#include "sl_rail.h"
#include "sl_rail_util_pa_conversions.h" // in sl_rail_util_pa plugin
#include "rail_config.h" // Generated by radio calculator

#define TX_FIFO_BYTES (128)  // Any power of 2 from [64, 4096] on the EFR32

static sl_rail_handle_t rail_handle_1 = SL_RAIL_EFR32_HANDLE;
static sl_rail_handle_t rail_handle_2 = SL_RAIL_EFR32_HANDLE;

static SL_RAIL_DECLARE_FIFO_BUFFER(aligned_tx_fifo_1, TX_FIFO_BYTES);
static uint8_t *tx_fifo_1 = (uint8_t *)aligned_tx_fifo_1; // For byte access
static SL_RAIL_DECLARE_FIFO_BUFFER(aligned_tx_fifo_2, TX_FIFO_BYTES);
static uint8_t *tx_fifo_2 = (uint8_t *)aligned_tx_fifo_2; // For byte access

// Boolean to track when a packet has been sent.
static bool packet_send_complete = true;

uint32_t transition_time = 0;

void success(void) {
  // Indicate test success to user
}

void fail(void) {
  // Indicate test failure to user
}

static void radio_event_handler(sl_rail_handle_t rail_handle,
                                sl_rail_events_t events)
{
  // Note that two different callbacks above could be used,
  // but this example only uses one which is split based on the handle.
  if (rail_handle == rail_handle_1) {
    // We don't care about events on protocol 1 for this test
    return;
  } else if (rail_handle == rail_handle_2) {
    // Handle any packet completion event (success or failure) and set us up
    // to send another packet.
    if (events & SL_RAIL_EVENT_TX_PACKET_SENT) {
      success();
    }
    if (events & RAIL_EVENT_SCHEDULER_STATUS) {
      sl_rail_scheduler_status_t scheduler_status;
      sl_rail_status_t rail_status;
      if ((sl_rail_get_scheduler_status(rail_handle, &scheduler_status, &rail_status)
           != SL_RAIL_STATUS_NO_ERROR)
          || (scheduler_status & RAIL_SCHEDULER_STATUS_SCHEDULED_TX_FAIL)) {
        fail();
      }
    }
  }
}

// Initializes the two PHY configurations for the radio.
void radio_initialize(void)
{
  sl_rail_status_t status;
  // transition_time should be swept via testing instrumentation between
  // runs
  sl_rail_set_transition_time(transition_time)

  // Creates each RAIL handle with their own configuration structures.
  // Below configs for each protocol utilize a shared RX FIFO and packet buffer
  // but have distinct TX FIFOs.
  sl_rail_config_t rail_config_1 = {
    .events_callback = &radio_event_handler,
  //.p_opaque_handle_0 = ... // app-specific if desired
  //.p_opaque_handle_1 = ... // app-specific if desired
  //.opaque_value = ... // app-specific if desired
    .rx_packet_queue_entries = SL_RAIL_BUILTIN_RX_PACKET_QUEUE_ENTRIES,
    .rx_fifo_bytes = SL_RAIL_BUILTIN_RX_FIFO_BYTES,
    .tx_fifo_bytes = sizeof(aligned_tx_fifo_1),
    .tx_fifo_init_bytes = 0U,
    .p_rx_packet_queue = sl_rail_builtin_rx_packet_queue_ptr,
    .p_rx_fifo_buffer = sl_rail_builtin_rx_fifo_ptr,
    .p_tx_fifo_buffer = aligned_tx_fifo_1,
  };
  sl_rail_config_t rail_config_1 = {
    .events_callback = &radio_event_handler,
  //.p_opaque_handle_0 = ... // app-specific if desired
  //.p_opaque_handle_1 = ... // app-specific if desired
  //.opaque_value = ... // app-specific if desired
    .rx_packet_queue_entries = SL_RAIL_BUILTIN_RX_PACKET_QUEUE_ENTRIES,
    .rx_fifo_bytes = SL_RAIL_BUILTIN_RX_FIFO_BYTES,
    .tx_fifo_bytes = sizeof(aligned_tx_fifo_2),
    .tx_fifo_init_bytes = 0U,
    .p_rx_packet_queue = sl_rail_builtin_rx_packet_queue_ptr,
    .p_rx_fifo_buffer = sl_rail_builtin_rx_fifo_ptr,
    .p_tx_fifo_buffer = aligned_tx_fifo_2,
  };
  sl_rail_util_pa_init(); // Establish PA conversion table(s)
  status = sl_rail_init(&rail_handle_1, &railconfig_1, NULL);
  assert(status == SL_RAIL_STATUS_NO_ERROR);
  status = sl_rail_init(&rail_handle_2, &railconfig_1, NULL);
  assert(status == SL_RAIL_STATUS_NO_ERROR);
  // Here rail_handle_1 and _2 have been converted to a real protocol instance handles

  // Configures radio according to the generated radio settings.
  status = sl_rail_config_channels(rail_handle_1, channelConfigs[0],
                                   &sl_rail_util_pa_on_channel_config_change);
  assert(status == SL_RAIL_STATUS_NO_ERROR);
  status = sl_rail_config_channels(rail_handle_2, channelConfigs[1],
                                   &sl_rail_util_pa_on_channel_config_change);
  assert(status == SL_RAIL_STATUS_NO_ERROR);
  status = sl_rail_set_tx_power_dbm(rail_handle_2, tx_power_ddbm);
  assert(status == SL_RAIL_STATUS_NO_ERROR);

  // Only need a few events on the second handle for this test
  status = sl_rail_config_events(rail_handle_2,
                                 SL_RAIL_EVENTS_ALL,
                                 (SL_RAIL_EVENT_TX_PACKET_SENT
                                  | SL_RAIL_EVENT_SCHEDULER_STATUS));
  assert(status == SL_RAIL_STATUS_NO_ERROR);
}

int main(void)
{
  sl_rail_status_t status;
  sl_rail_scheduler_info_t scheduler_info;
  uint32_t send_time;

  // Initializes the radio and creates the two RAIL handles.
  radio_initialize();

  // Get into receive on handle 1
  sl_rail_scheduler_info_t scheduler_info = {
    .priority = 200,
    .transactionTime = 1000,
    .slipTime = 0,
  };
  status = sl_rail_start_rx(rail_handle_1, 0, &scheduler_info);
  assert(status == SL_RAIL_STATUS_NO_ERROR);

  // Starts the first send 1 second after getting set up.
  send_time = sl_rail_get_time(rail_handle_2) + 1000000;

  // Request a TX for some time in the future
  sl_rail_schedule_tx_config_t scheduled_tx_config = {
    .when = send_time,
    .mode = SL_RAIL_TIME_ABSOLUTE,
  };
  schedulerInfo.priority = 100;
  status = sl_rail_start_scheduled_tx(rail_handle_2,
                                      0,
                                      SL_RAIL_TX_OPTIONS_DEFAULT,
                                      &scheduled_tx_config,
                                      &scheduler_info);
  return 0;
}

Minimizing the Protocol Switch Time#

Currently, the best way to minimize the switch time is by proper design of sl_rail_config_t::events_callback. Specifically, the three scheduler events SL_RAIL_EVENT_CONFIG_UNSCHEDULED, SL_RAIL_EVENT_CONFIG_SCHEDULED, and SL_RAIL_EVENT_SCHEDULER_STATUS, will be passed to the callback independently of all other events. Additionally, they are only raised during protocol switch process, and so their handling is part of the critical path for the protocol switch. Ideally, if they are not needed, sl_rail_config_events() should disable these events. Otherwise they should be handled first, after which the event handler should return immediately.

Additionally, short transition times should be specified in sl_rail_set_state_timing(). Note that 0 for these values can be unreliable in a DMP context, as that tells the radio to go "as fast as possible" as opposed to a reliable, known value.