Transmitting a Packet#
This tutorial is the continuation of a series started with Introduction.
Please read it first if you haven't.
The completed example for this article is available on GitHub. You can either import this example, as documented in the readme on Github, or follow this article and modify a RAIL - SoC Empty example. Note that the code snippets here just like the code on Github is missing a lot of error handling for easier readability. Therefore, it shouldn't be used as a base of development.
In this tutorial, we're going to modify the project RAIL - SoC Empty to transmit a packet. We're going to use the default, fixed length 16 Bytes 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:
#define SL_TUTORIAL_TRANSMIT_BUFFER_LENGTH 256
SL_RAIL_DECLARE_FIFO_BUFFER(tx_fifo, SL_TUTORIAL_TRANSMIT_BUFFER_LENGTH);On all Series 2 chips, except EFR32xG21, RAIL requires a 4 bytes aligned FIFO.
In the code above, we used SL_RAIL_DECLARE_FIFO_BUFFER which guarantees good
alignment on any EFR32.
The Tx buffer can hold more than one packet, 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.
Note, that the transmitted packet size can be bigger than the FIFO, but that requires special care, that will be detailed in a later tutorial.
Writing to the Buffer Directly#
In app_process.c:
#define SL_TUTORIAL_TRANSMIT_PAYLOAD_LENGTH 16
#define SL_TUTORIAL_TRANSMIT_BUFFER_LENGTH 256
uint8_t payload[SL_TUTORIAL_TRANSMIT_PAYLOAD_LENGTH] =
{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0F};
SL_RAIL_DECLARE_FIFO_BUFFER(tx_fifo, SL_TUTORIAL_TRANSMIT_BUFFER_LENGTH);
void load_payload_directly(sl_rail_handle_t rail_handle) {
memcpy(tx_fifo,
payload,
SL_TUTORIAL_TRANSMIT_PAYLOAD_LENGTH);
sl_rail_set_tx_fifo(rail_handle,
tx_fifo,
SL_TUTORIAL_TRANSMIT_BUFFER_LENGTH,
SL_TUTORIAL_TRANSMIT_PAYLOAD_LENGTH,
0);
}The memcpy() is just a standard C function to write to buffers, and we use
sl_rail_set_tx_fifo() to pass that buffer to RAIL. We also tell RAIL how long
the buffer is, how much data it has already in it, and the start position of the
TX data from the FIFO base address.


Note : This illustrates the simple case of one sl_rail_set_tx_fifo() call. In
effect, different buffers are used for Rx and Tx.
Writing to the Buffer Indirectly#
In app_init.c:
#define SL_TUTORIAL_TRANSMIT_BUFFER_LENGTH 256
SL_RAIL_DECLARE_FIFO_BUFFER(tx_fifo, SL_TUTORIAL_TRANSMIT_BUFFER_LENGTH);
void app_init(void)
{
sl_rail_handle_t rail_handle = sl_rail_util_get_handle(SL_RAIL_UTIL_HANDLE_INST0);
sl_rail_set_tx_fifo(rail_handle, tx_fifo, SL_TUTORIAL_TRANSMIT_BUFFER_LENGTH, 0, 0);
}In app_process.c:
#define SL_TUTORIAL_TRANSMIT_PAYLOAD_LENGTH 16
uint8_t payload[SL_TUTORIAL_TRANSMIT_PAYLOAD_LENGTH] =
{0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0F};
static void load_payload_indirectly(sl_rail_handle_t rail_handle) {
sl_rail_write_tx_fifo(rail_handle,
payload,
SL_TUTORIAL_TRANSMIT_PAYLOAD_LENGTH,
false);
}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
sl_rail_write_tx_fifo 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. Buffer wrapping is also supported internally by RAIL.
Calling
sl_rail_set_tx_fifo()does not move any memory, just sets a few registers, while callingsl_rail_write_tx_fifo()does need some time to copy the payload to the buffer.Calling
sl_rail_set_tx_fifo()is not allowed during transmission. Whereas using sl_rail_write_tx_fifo() is safe to use, even when radio transmission is occurring.Indirect mode clearly separates init and process code.
For simpler applications, sl_rail_set_tx_fifo() can be a better option. If you
want to send almost the same packet, with just a few bytes modification, it's
much better, since you don't have to write the whole packet again. (Note that if
you want to transmit the exact same message over and over, you should use
SL_RAIL_TX_OPTION_RESEND, detailed in a later
article).
On the other hand, the separation in RAIL examples between app_init and
app_process works nicely with the indirect method, and in most real cases, you
need to move memory anyway. If you must start transmitting before you can
construct the whole packet, you must use sl_rail_write_tx_fifo(), as you can
call it during transmit. It's also useful if you want to send out a lot of
packets: Use sl_rail_write_tx_fifo() 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 sl_rail_write_tx_fifo can reset the FIFO, which means it
will invalidate the data already in there. This is also possible with
sl_rail_reset_fifo.
It's a good idea to run some tests without resetting the FIFO, as it can mask small errors, e.g., writing more to the FIFO than needed. However, in production, it's a good idea to reset the FIFO if you think it should be empty, as in production, masking errors are beneficial.
Transmitting the Packet#
Starting the transmission is simple:
sl_rail_start_tx(rail_handle, 0, SL_RAIL_TX_OPTIONS_DEFAULT, NULL);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 an article on the topic). 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 sl_rail_set_fixed_length(), 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 sl_rail_set_fixed_length(rail_handle,
SL_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 Proprietary Radio Configurator Guide. 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 16 bytes frame, both for the above described variable length and 16 bytes fixed length mode, so this is used in the attached sample code:
uint8_t payload[SL_TUTORIAL_TRANSMIT_PAYLOAD_LENGTH] =
{SL_TUTORIAL_TRANSMIT_PAYLOAD_LENGTH-1, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0F};We use SL_TUTORIAL_TRANSMIT_PAYLOAD_LENGTH-1 in the length field, since the 1 byte length field
itself shouldn't be counted.
The routine sl_rail_set_fixed_length() is available in variable length mode,
and it changes the radio to fixed length operation (again, both for Rx and Tx).
Calling sl_rail_set_fixed_length(rail_handle, SL_RAIL_SETFIXEDLENGTH_INVALID)
will restore it to variable length mode.
Setting up the Example#
Button Handling
The linked 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):
void sl_button_on_change(const sl_button_t* handle){
if ( sl_button_get_state(handle) == SL_SIMPLE_BUTTON_PRESSED ){
send_packet = true;
}
}By default, the Simple Button is configured for a pin which matches the development kit's PB0 button.
Since this callback is in interrupt context, we avoid using RAIL API directly.
It is safe in almost all cases, but calling sl_rail_start_tx() 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:
void app_process_action(void)
{
if (send_packet) {
sl_rail_handle_t rail_handle = sl_rail_util_get_handle(
SL_RAIL_UTIL_HANDLE_INST0);
send_packet = false;
// Increment the last byte of the payload
payload[SL_TUTORIAL_TRANSMIT_PAYLOAD_LENGTH - 1]++;
sl_rail_write_tx_fifo(rail_handle,
payload,
SL_TUTORIAL_TRANSMIT_PAYLOAD_LENGTH,
false);
sl_rail_start_tx(rail_handle,
SL_TUTORIAL_TRANSMIT_DEFAULT_CHANNEL,
SL_RAIL_TX_OPTIONS_DEFAULT,
NULL);
}
}In the above snippet, we're also incrementing the last byte of the payload before every transmit.
Conclusion#
We didn't care about possible errors generated by RAIL: we do that next time, which also provides hints for receiving packets.