Dynamic Multiprotocol (DMP) in RAIL#

Before reading this article, make sure to read the Introduction to Multi-PHY and Multiprotocol, 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 all the other training articles before reading this (DMP) document, since DMP affects most APIs in one way or another.

Note that this article 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.

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 use case 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 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 works on the principle that each protocol operates as is scheduled "alone" on the radio transceiver. A bit in a similar fashion that a process on the computer running and not being necessarily aware that other processes are scheduled at the same time.

Multiple RAIL protocol 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.

"Static" Multiprotocol#

Static multiprotocol is a common requirement in IoT. For example, you only need Bluetooth for commissioning, and while Bluetooth is enabled, you don't need the main protocol running. For this use case, we also recommend DMP. However development is much simpler for it. You only need to initialize DMP and make sure to call RAIL_Idle() before switching protocols. So you only need to read the Setting up a Project for RAIL-RAIL DMP chapter here. Everything else is just recommended.

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 FreeRTOS), 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 a fixed amount of time. This is defined by TRANSITION_TIME_US, and could differ from part to part. As of writing this article, it is between 430-510us.

This transition time can be tuned and overridden.

Setting up a Project for RAIL-RAIL DMP#

If you need RAIL+Bluetooth DMP, follow AN1269. 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.

DMP
componentDMP
component

Next, you'll need PHY configurations for your protocols. You can either use Protocol Based Multi-PHY (see the Multi-PHY usecases and examples article for details), or you can use embedded protocols, e.g. IEEE 802.15.4 2.4GHz 250kbps by calling RAIL_IEEE802154_Config2p4GHzRadio() or configuring the RAIL Utility, Initialization component accordingly.

RTOS#

RTOS 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 RTOS-enabled projects. Therefore, RTOS will not be discussed in this article; it doesn't impact how you use RAIL APIs in RAIL-only DMP.

RAM Allocation#

RAIL requires RAM allocated for each initialized protocol, to store its state variables. Since GSDK v4.0, the multiprotocol RAIL library automatically allocates RAM for two protocols. For more than two protocols, call RAIL_AddStateBuffer3(RAIL_EFR32_HANDLE) before initializing the 3rd protocol (or earlier). For even more protocols, use RAIL_AddStateBuffer4(RAIL_EFR32_HANDLE) and the generic RAIL_AddStateBuffer().

Note that RAIL_EFR32_HANDLE is a special RAIL handle, which can be used if a RAIL handle is needed before RAIL_Init(), like the above case.

Before GSDK v4.0, RAM was allocated in the application and needed to be passed to RAIL_Init().

RAIL Initialization#

Since GSDK v3.x, we recommend using the component RAIL Utility, Initialization to initialize RAIL. This is an instancable component; you can create multiple instances for RAIL-RAIL DMP.

It's a good idea to select the correct radio config for each instance using this component. E.g., depicted below, we selected the second protocol of the radio configurator, channelConfigs[1]. This is also the location where we could easily select IEEE 802.15.4 PHY.

PHYsetupPHYsetup

If you wish not to use the component, initializing RAIL can be done exactly the same as single protocol. You will find some DMP related arguments, but those are ignored since GSDK v4.0 (they were used for memory allocation before 4.0).

RAIL API Calls and Code Separation#

RAIL APIs are the same for all protocols, and their RAIL_Handle_t argument decides which protocol is called. The RAIL handle can be obtained via sl_rail_util_get_handle(SL_RAIL_UTIL_HANDLE_INST0) where INST0 is the name of the init component instance, with all uppercase.

It is highly recommended to implement each RAIL protocol in separate C files. Otherwise, it can be difficult to follow which call is intended for which protocol.

Assertion#

RAIL assertion, i.e., the RAILCb_AssertFailed() should be common for all protocols. If you add RAIL next to BLE for example, you should disable this assert in your own protocol, as it is handled in the stack. This can be done in the RAIL Utility, Callbacks component.

assertionassertion

Other APIs#

Most of the RAIL APIs work exactly the same as in single protocol mode. However, you should be careful with:

  • protocol independent APIs, i.e. the ones that don't have a rail_handle 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 rail_handle, 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:

(Note that RAIL_StartRx() and RAIL_StartTxStream() are 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 members:

  • 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.

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() or RAIL_Idle(). The latter also turns off the radio, which should have already occurred for finite tasks. 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, which 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.

For more details, see UG305 chapter 5.

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.

FIFOs#

DMP maintains separate Tx FIFO memory locations for the used protocols; however, the Rx FIFO is common for all of them. This means that the Rx FIFO is cleared during protocol switch, to avoid information leaking from one protocol to the other. This also means that the FIFO must be downloaded before yielding the radio. In practical terms, this means that it's recommended to download received packet in the event handler, and the use of the RAIL_HoldRxPacket() API should be avoided.

Error Handling#

In single protocol RAIL, you generally need to implement the following error handling:

  • 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_GetSchedulerStatusAlt().

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.

RAIL_GetSchedulerStatusAlt() returns two errors:

The first one is difficult to understand if you just read the documentation. It's cleaner if you check it in rail_types.h.

The upper 4 bits, masked by RAIL_SCHEDULER_TASK_MASK reports the task the error is originated from. For example, RAIL_SCHEDULER_TASK_SINGLE_TX means the radio scheduler was working on a RAIL_StartTx() task.

The lower 4 bits, masked by RAIL_SCHEDULER_STATUS_MASK is the error itself, which can have one of the RAIL_SCHEDULER_STATUS_* errors (see the API doc for details).

Usually an application will need to implement RAIL_SCHEDULER_STATUS_SCHEDULE_FAIL and RAIL_SCHEDULER_STATUS_EVENT_INTERRUPTED specifically. These could normally happen, resulted by conflicts with other radio tasks. The first one means the task is impossible to schedule due to other tasks, while the latter means that the task started, but was interrupted by a higher priority task. Note that these errors happen rarely so one must carefully implement the error handling in a DMP app based on what could theoretically happen.

The other errors mean more serious problems. RAIL_SCHEDULER_STATUS_TASK_FAIL means the internal, single protocol call of the API returned an error. In this case, the second parameter is also meaningful. RAIL saves the returned error into RAIL_Status_t parameter.

RAIL_SCHEDULER_STATUS_INTERNAL_ERROR shouldn't happen, but is theoretically possible, so should be handled. Finally RAIL_SCHEDULER_STATUS_UNSUPPORTED is currently only reported if the RAIL_GetSchedulerStatus() is called with the single protocol RAIL library.

So, for example RAIL_SCHEDULER_TASK_SCHEDULED_TX | RAIL_SCHEDULER_STATUS_EVENT_INTERRUPTED means that a task, requested by RAIL_StartScheduledTx() was interrupted by a higher priority operation.

Debugging#

When debugging DMP, Rx/Tx PRS channels are still useful. See the article 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 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 rarely needed, and since RAIL_Idle() also yields the radio, it should be 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.

  • Implement careful error handling in the RAIL_EVENT_SCHEDULER_STATUS event

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 article 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.