Time, Timestamping, and Scheduling#
Timing is an important aspect of most wireless protocols. While it's possible to use a generic timer for most of the required tasks, Silicon Labs' Wireless MCUs provide a protocol timer, which works together with the radio state machine, providing many advantages. This protocol timer is the base of all RAIL timing features.
Using the protocol timer, you can completely eliminate interrupt latencies, and you can get more accurate and more repeatable timing, even if you update the SDK version. Using the protocol timer in dynamic multiprotocol (DMP) is highly recommended as it tells the radio scheduler what you are planning to use the radio for.
RAIL Time#
RAIL timebase (or the resolution of RAIL time) is 1 µs regardless of the
oscillator's frequency, counted from boot. The same timebase is used for
everything (timestamping and absolute timers). It's stored in
sl_rail_time_t,
currently implemented as uint32_t, what means that it wraps every 1.19 hours.
Note that different hardware might run at different speeds, which is converted
to this universal 1 µs timebase. For this reason, you should never compare
RAIL times with equality operators (== and !=) as it might skip values.
RAIL provides the usual getter/setter for this:
sl_rail_time_t sl_rail_get_time(sl_rail_handle_t rail_handle);
sl_rail_status_t sl_rail_set_time(sl_rail_handle_t rail_handle, sl_rail_time_t time);Note that it's generally a bad practice to use sl_rail_set_time(): It can cause serious issues when timers are running. Also, RAIL time is one of the few shared resources between protocols in dynamic multiprotocol mode.
Timestamps#
By default, each Tx and Rx packet is timestamped with the RAIL time on a fixed position of the packet.
While it would be possible to save the value of a timer at a specific RAIL event (like SL_RAIL_EVENT_TX_PACKET_SENT, SL_RAIL_EVENT_RX_SYNC_0_DETECT, SL_RAIL_EVENT_RX_PACKET_RECEIVED, etc.) this isn't recommended.
The reason is that at the RAIL API level, there's no way to guarantee time accuracy between the transmission/reception routine is called and the event is raised. Moreover, this would not be possible if radio interrupts are disabled.
Acquire Tx Timestamp#
To get the timestamp of transmitted packets, use sl_rail_get_tx_packet_details() in the context of the SL_RAIL_EVENT_TX_PACKET_SENT event:
if ( events & SL_RAIL_EVENT_TX_PACKET_SENT ){
sl_rail_tx_packet_details_t packet_details;
packet_details.is_ack = false;
sl_rail_get_tx_packet_details(rail_handle, &packet_details);
// ...
// Timestamp position discussed later
}Note that in case of using sl_rail_get_tx_packet_details(), the is_ack must always be initialized to get proper timestamp;
We return to the time positions soon, but first, let's see how to get the timestamp of a received packet.
Acquire Rx Timestamp#
Similarly to Tx, RAIL defines sl_rail_get_rx_packet_details() API for Rx, and you should use it like this:
sl_rail_rx_packet_info_t packet_info;
sl_rail_rx_packet_details_t packet_details;
sl_rail_rx_packet_handle_t packet_handle = sl_rail_get_rx_packet_info(rail_handle, SL_RAIL_RX_PACKET_HANDLE_NEWEST, &packet_info);
sl_rail_get_rx_packet_details(rail_handle, packet_handle, &packet_details);Note that a packet_handle was used to identify the packet we need to be
timestamped. This means that the timestamp of the received packet is stored as
long as the packet is not released either automatically or by calling
sl_rail_release_rx_packet()
after
sl_rail_hold_rx_packet().
As a result, these routines do not need to be called in the RAIL event handler
(interrupt context).
This means that the Tx timestamps can only be obtained in the RAIL event handler (interrupt context), as opposed to the RX timestamps, which persist until the received packets are released.
Timestamp Positions#
Position Conversion APIs#
RAIL provides several timestamp time positions via the sl_rail_packet_time_position_t enumeration. The following values are part of it:
The first three are self-explanatory. SL_RAIL_PACKET_TIME_DEFAULT is the hardware implementation, which never needs the total_packet_bytes. The sl_rail_get_rx_packet_details() function will update the time_position to one of the first three, which is the default on the hardware.
On EFR32 Series 1, this will be SL_RAIL_PACKET_TIME_AT_SYNC_END for Rx and SL_RAIL_PACKET_TIME_AT_PACKET_END for Tx packet.
On EFR32 Series 2, SL_RAIL_PACKET_TIME_AT_PACKET_END is used for both Tx and Rx.
To convert the timestamp to a given position, one of the following APIs must be called:
//Rx APIs:
sl_rail_status_t sl_rail_get_rx_time_frame_end(sl_rail_handle_t rail_handle, sl_rail_rx_packet_details_t *p_packet_details);
sl_rail_status_t sl_rail_get_rx_time_preamble_start(sl_rail_handle_t rail_handle, sl_rail_rx_packet_details_t *p_packet_details);
sl_rail_status_t sl_rail_get_rx_time_sync_word_end(sl_rail_handle_t rail_handle, sl_rail_rx_packet_details_t *p_packet_details);
//Tx APIs:
sl_rail_status_t sl_rail_get_tx_time_frame_end(sl_rail_handle_t rail_handle, sl_rail_tx_packet_details_t *p_packet_details);
sl_rail_status_t sl_rail_get_tx_time_preamble_start(sl_rail_handle_t rail_handle, sl_rail_tx_packet_details_t *p_packet_details);
sl_rail_status_t sl_rail_get_tx_time_sync_word_end(sl_rail_handle_t rail_handle, sl_rail_tx_packet_details_t *p_packet_details);These conversion APIs all need the p_packet_details previously provided by the
sl_rail_get_tx_packet_details()
(refer to the API documentation of each function for exceptions)
To convert the default timestamp, RAIL needs total_packet_bytes. The functions will update the time_position. to one of the following values:
This can be used to recognize an inaccurate timestamp, if total_packet_bytes is unknown.
Note, that
sl_rail_get_tx_time_preamble_start()
can be used with SL_RAIL_TX_STARTED_BYTES to get an accurate preamble start
timestamp without knowing the
total_packet_bytes.
General Purpose Timer#
By default, you can set up a single general purpose timer on the RAIL timebase. The following APIs are usable for this timer (for the detailed arguments, please check the API documentation):
sl_rail_set_timer(sl_rail_handle_t, sl_rail_time_t, sl_rail_time_mode_t, sl_rail_timer_callback_t)
sl_rail_get_timer(sl_rail_handle_t)
sl_rail_cancel_timer(sl_rail_handle_t)
sl_rail_is_timer_expired(sl_rail_handle_t)
sl_rail_is_timer_running(sl_rail_handle_t)
There are a few parameters that require explanation. sl_rail_timer_callback_t is a function pointer type to the function which will be called when the timer fires. sl_rail_time_mode_t can take two values in this case:
If SL_RAIL_TIME_ABSOLUTE is used, RAIL interprets the timing in radio time
base absolute time. On the other hand, if SL_RAIL_TIME_DELAY is used, the time
specified is a delay starting from the the function call in the RAIL timebase.
However, SL_RAIL_TIME_DELAY only guarantees that at least the specified
delay is elapsed; it does not compensate the API processing time. If you need
accurate timing, you should use SL_RAIL_TIME_ABSOLUTE:
sl_rail_time_t anchor = sl_rail_get_time(rail_handle);
sl_rail_set_timer(rail_handle, anchor + 1000, SL_RAIL_TIME_DELAY, timer_cb);

You can notice that the timer starts from the anchor and not from the
sl_rail_set_timer()
function call. If SL_RAIL_TIME_DELAY were used, this is how it would look
like, with the small processing time added to the delay:


Note though, that this processing time is consistent in the same environment. So
if for some reason, you cannot get an anchor point, you can use a
SL_RAIL_TIME_DELAY, measure it, and compensate from the application. However,
if you update RAIL, or change the part the code is running on, the delay might
change.
Restarting timers (i.e., to change the timeout) is not allowed. If the timer might be running when you set it, you should call sl_rail_cancel_timer() first.
Multi-timer#
RAIL provides timer virtualization over RAIL timer. In single protocol mode, it's
not enabled to save code space. To enable it, call
sl_rail_config_multi_timer()
with true argument.
Multi-timer is always enabled in dynamic multiprotocol (DMP) mode.
It provides basically the same APIs and the same precision as the regular RAIL timer:
sl_rail_set_multi_timer(sl_rail_multi_timer_t*, sl_rail_time_t, sl_rail_time_mode_t, sl_rail_multi_timer_callback_t, void*)
sl_rail_get_multi_timer(sl_rail_multi_timer_t*)
sl_rail_cancel_timer(sl_rail_multi_timer_t*)
sl_rail_is_timer_expired(sl_rail_multi_timer_t*)
sl_rail_is_timer_running(sl_rail_multi_timer_t*)
There are only two differences:
Each API needs a sl_rail_multi_timer_t
timer_handle. Every timer you plan to use should have its unique handler statically allocated.The callback function has a customizable,
void*argument.
The regular RAIL timer can still be used when multi-timer is enabled. It's still implemented as a multi-timer, but its handle is allocated by RAIL. For more information on how RAIL can be used to implement multi-protocol, refer to the Dynamic Multiprotocol User's Guide.
Note: multi-protocol does not necessarily mean multi PHYs. For more information on the differences between the two, refer to the article about multi PHYs and multiprotocol
Scheduling Tx#
Scheduling transmissions on the RAIL time is quite simple:
sl_rail_scheduled_tx_config_t schedule_config = {
.when = 1000,
.mode = SL_RAIL_TIME_DELAY,
.tx_during_rx = SL_RAIL_SCHEDULED_TX_DURING_RX_POSTPONE_TX,
};
sl_rail_start_scheduled_tx(rail_handle, 0, SL_RAIL_TX_OPTIONS_DEFAULT, &schedule_config, NULL);The parameters of
sl_rail_start_scheduled_tx()
are the same as the parameters of
sl_rail_start_tx(),
except the
sl_rail_scheduled_tx_config_t
pointer config argument, which works just like running the RAIL timer. The
only new argument field here is
tx_during_rx;
it can have the following values:
If you have the radio in receive mode, and when your scheduled packet starts
transmitting, you might be in the middle of receiving a packet. In this case,
SL_RAIL_SCHEDULED_TX_DURING_RX_POSTPONE_TX will start the transmission when
the reception completes, so transmission will be delayed from the intended
scheduling. SL_RAIL_SCHEDULED_TX_DURING_RX_ABORT_TX on the other hand cancels
the transmission.
Scheduling Rx#
Scheduling Rx is similar to scheduling Tx, but it provides more options:
sl_rail_scheduled_rx_config_t schedule_config = {
.start = 1000,
.start_mode = SL_RAIL_TIME_DELAY,
.end = 10000,
.end_mode = SL_RAIL_TIME_DELAY
.rx_transition_end_schedule = false;
.hard_window_end = false;
};
sl_rail_start_scheduled_rx(rail_handle, 0, SL_RAIL_RX_OPTIONS_DEFAULT, &schedule_config, NULL);The following illustrates the scheduled reception as defined above:


The arguments start and start_mode work just like the Tx scheduling setup (or a regular timer), but for Rx, we can also configure when to end and return to idle mode. In the case above, the delay starts when the receiver starts, so the config above will start receiving 1 ms after the API call, and stop it 11 ms after the API call, closing a 10 ms Rx window.
The end timer is just an option: You can use SL_RAIL_TIME_DISABLED, in which case the radio will stay in Rx mode indefinitely. However, keep in mind that auto state transitions affect scheduled Rx mode as well; if a packet is received, it can go to idle or Tx automatically, or it can go back to Rx, in which case it will wait until the original end time, and go to idle at that point.
When the end timer fires, RAIL will trigger the SL_RAIL_EVENT_RX_SCHEDULED_RX_END event.
The last two parameters are rarely used; see the API documentation for it.
Note also that this is one of the few APIs that behave differently in Dynamic multiprotocol (DMP) mode. For details, see the article on DMP.
Multiple Radio Operations#
It is not possible to schedule multiple radio operations, whether Rx or Tx. Only one operation is possible to scheduled at a time; the next one can be scheduled when the current one is finished.
Although Dynamic multiprotocol does essentially allow it, it is, in reality rarely needed. For more info, see the introductory article on DMP.
State Timing#
RAIL lets you configure the delays needed for each radio state transitions described below. This is useful, in effect, in cases expecting ACKs to be received after a determined time following a transmission. The radio transceiver doesn't need to be turned on immediately, hence, saving power.
RAIL defines a few configurable transition times in sl_rail_state_timing_t:
idle_to_rx: delay from scheduled time of Rx to actual Rx
idle_to_tx: delay from scheduled time of Tx to actual Tx
tx_to_rx: delay from Tx to Rx in auto state transition
rx_to_tx: delay from Rx to Tx in auto state transition
All of these are configurable with sl_rail_set_state_timing(). By default, all of them are set to a delay the chip can guarantee. This is somewhere between 100 µs and 150 µs, depending on the part number.
You can set it to 0, which is best effort. It is the fastest possible though not guaranteed switch time. It is however consistent within the same major part number (e.g., all EFR32xG23), rail version and radio config, so it can be used for tight timings.
Normal (not scheduled) Rx/Tx will also use the above configuration, but they have some additional delay because the MCU has to process the command, and might need to change the channel, which could take longer.
Calling sl_rail_start_tx() while the radio is in receive mode is not an Rx to Tx transition: It will first go to idle, then Tx.
There are two other delays that are not configurable:
anything to idle
Rx to Rx (i.e., return to Rx on the same channel, after receiving a packet)
Anything to idle usually takes about 20 µs, while Rx to Rx is about 10 µs, these are always best effort delays.
Search Timeout#
You can change two search timeouts as well with sl_rail_set_state_timing():
rx_search_timeoutdefines the length of time the radio will search for a packet when coming from idle.
tx_to_rx_search_timeoutdefines length of time the radio will search for a packet when coming from Tx.
Both of these means that Rx will turn off automatically if the radio does not receive a sync word until the timeout. The timeout will trigger a SL_RAIL_EVENT_RX_TIMEOUT event. This is especially useful in the second case, e.g., you can set up to wait 5 ms for acknowledgment. If you combine it with sl_rail_set_state_timing(), you can configure a 5 ms Rx window 1 ms after transmitted packets.
Energy Modes and Sleep#
By default, RAIL timers only work in EM0 and EM1. When configured correctly, RAIL can use an RTC, which enables the timers to work almost seamlessly in EM2.
The hardware provides very accurate synchronization with RTC. Since RTC runs much slower than the radio timer (1-32 kHz vs 1 MHz), this is not as simple as saving the value of both timers at the same time: It saves the timer's value when RTC changes its value, eliminating errors caused by the synchronization.
For details, see the article about timer synchronization.