Custom manufacturing tokens are similar to custom stack token, but the purpose is to write once (in manufacturing case) and read multiple times (as opposed to custom stack tokens where the purpose is to modify the value several times at runtime).
While for stack tokens there is a Token Manager component where one can specify the name and the location of the header file that contains the custom stack tokens, this is not the case for custom manufacturing tokens.
For custom stack tokens the default location is the project's config directory and the default name is sl_custom_token_header.h. Don't place the custom manufacturing token defines in this file.
Instead, create a file with the content required for custom manufacturing token(s), name it freely (ex.: sl_custom_mfg_token_header.h - preferably don't use sl_custom_token_header.h to avoid conflicts) place it in the project's config directory (not necessarily, but seems a good choice, beside sl_custom_token_header.h).
Then add -D APPLICATION_MFG_TOKEN_HEADER="sl_custom_mfg_token_header.h" to the compile options. The easiest way to apply this is opening Project Properties -> C/C++ Build -> Settings -> Preprocessor -> Defined symbols -> + (Add..) and add APPLICATION_MFG_TOKEN_HEADER="sl_custom_mfg_token_header.h":
Sample manufacturing token example from SimplicityStudio_v5/developer/sdks/gecko_sdk_suite/v3.0/platform/service/token_manager/inc/sl_token_manufacturing_series_1.h:
In this tutorial, we're going to combine what we had from the previous tutorial. We're going to switch between transmit and receive, and we're going to use a few peripherals on the WSTK as a user interface.
Our goal is to transmit a predefined packet when either of the buttons are pressed, and print the packet to the serial console when received. We're also going to toggle LEDs on finished Rx/Tx, but we ignore errors.
You can find a SimpleTRX example in Simplicity Studio, which has similar purpose. However, the result of this tutorial will be somewhat different.
Preparations
We're going to start from the usual, the Simple RAIL with HAL example. However, we'll need some modifications in the Hardware Configurator because we'll need the UART bridge and buttons, which are not enabled by default. To do that, enable the following defaultMode peripheral modules:
Virtual COM Port
Button
Next, we'll have to include a few things to have these peripherals, and while some of the peripherals are automatically initialized, not all, so we're going to do that in a function:
#include "bsp.h"
#include "retargetserial.h"
#include "gpiointerrupt.h"
#include <stdio.h>
typedef struct ButtonArray{
GPIO_Port_TypeDef port;
unsigned int pin;
} ButtonArray_t;
static const ButtonArray_t buttonArray[BSP_BUTTON_COUNT] = BSP_BUTTON_INIT;
void gpioCallback(uint8_t pin);
void peripheralInit(){
RETARGET_SerialCrLf(1); //converts \n to \r\n
//set up button GPIOs to input with pullups
for ( int i= 0; i < BSP_BUTTON_COUNT; i++){
GPIO_PinModeSet(buttonArray[i].port, buttonArray[i].pin, gpioModeInputPull, 1);
}
//set up interrupt based callback function on falling edge
GPIOINT_Init();
GPIOINT_CallbackRegister(buttonArray[0].pin, gpioCallback);
GPIOINT_CallbackRegister(buttonArray[1].pin, gpioCallback);
GPIO_IntConfig(buttonArray[0].port, buttonArray[0].pin, false, true, true);
GPIO_IntConfig(buttonArray[1].port, buttonArray[1].pin, false, true, true);
}
As you see, gpioCallback() will be called on button press.
Radio init
By default, after receiving or transmitting a packet, RAIL will switch to idle mode (i.e., turn off the radio). Since we want it to keep receiving future packets, we can use RAIL's auto state transition feature with the RAIL_SetRxTransitions and RAIL_SetTxTransitions APIs.
We're basically saying, after the packets are received/transmitted, we want to return to Rx. We're also enabling some events as we've seen before. The only new one is RAIL_EVENT_CAL_NEEDED - it will have it's own tutorial; for now, it is important to know that it should always be implemented to have the maximum available performance.
There's not much new here. We start by initializing everything then going to Rx mode. When a button is pressed startTx will be true, which will start transmitting a packet (changing from Rx to Tx mode). Note that the variable startTx is volatile, because it's changing in interrupt context. If you're unfamiliar with this practice, look it up; it's very important for low-level embedded applications.
In the event handler, we blink LEDs on Rx/Tx completion. In the CAL_NEEDED handler we're basically saying: If any calibration is needed, run it. Again, this is important to get the maximum available performance, but we won't go into details for now.
At this point, you should see LEDs blinking on both devices when you press a button on either of them. So at this point, we added a single concept: we return to Rx state both after receiving a packet, and after transmitting one. This enabled a combined Rx/Tx application. However, there's one important thing we didn't do so far: we did not download the received packet.
Downloading the Packet
Receive FIFO
We already configured Tx FIFO multiple ways. RAIL also implements an Rx FIFO. By default, it's 512B, allocated by the RAIL library. We will use that, but it is configurable. See RAILCb_SetupRxFifo() and RAIL_SetRxFifo() in the API documentation for details.
By default, you cannot use the whole 512B, as RAIL adds a few bytes to each packet to store timestamp, RSSI and similar information in the FIFO (sometimes we call this "appended info").
We're working on what we call packet mode. It's the simpler, and recommended way, but it's not possible to handle packets bigger than 512B with this method,; in which case FIFO mode should be used. We return to that topic in a later tutorial.
The Packet Handler
The FIFO is accessible with RAIL_RxPacketHandle_t variables or packet handles. Obviously, we don't have the handle of the packet we just received, but we have "sentinel" handles:
RAIL_RX_PACKET_HANDLE_OLDEST
RAIL_RX_PACKET_HANDLE_NEWEST
RAIL_RX_PACKET_HANDLE_INVALID
The names are self-explanatory.
When to Read the FIFO
Let's say we want to download the packet when it's fully received (although it's possible to access it during reception). We must let RAIL know this in the event RAIL_EVENT_RX_PACKET_RECEIVED. If we don't, RAIL automatically frees the memory allocated to the packet we just received.
We have two options:
Download the packet from the event handler
Hold the packet in the FIFO and download later
While the first one is simpler, we also want to print the message on UART, which takes a long time, and that's not a good idea in interrupt context, so we'll use the second one.
To do that, let's create a volatile packetHandle, with invalid value:
We should only write the handle if it's invalid, otherwise we might lose the handle to a locked packet (and essentially lose part of the receive buffer). We'll also need a buffer to copy the packet before printing it.
The Event Handler
Let's implement the event handler so it will only store packets if packetHandle is invalid:
First, RAIL_GetRxPacketInfo will return with packetInfo, which has the length of the received packet, and pointers to download it. Note that unlike transmit, this is independent of the length coding method (fixed or variable) we used in the Configurator.
Since the receive buffer is a ring buffer, the download will be always the same 3 lines of code, which is done in the RAIL_CopyRxPacket inline function.
The API RAIL_GetRxPacketDetails will return some useful information of the packet, such as RSSI.
Finally, we release the packet, and set the handle to invalid, so the event handle can write it.
Download in the Event Handle
Downloading the packet in the event handler is essentially the same as what we did in the main loop above:
Note that packetDetails should be a global variable as well in this case.
Conclusion
With this, you can use RAIL for basic transmit and receive, which concludes the getting started series. However, RAIL can do much more - you can continue with the basic tutorials from the table of contents site.
This article describes how DSSS sequences are generated on EFR32 devices. It also demonstrates (for multiple configurations and modulation types) how these chip codes can be read out when transmitting a DSSS-coded frame. The DSSS symbols captured with this technique can then be fed directly to a signal generator to emulate an EFR32 transmitter.
In the following sections, we'll address questions like:
How can you record the DSSS symbols transmitted by an EFR32?
How do you encode the chip sequences on the entire transmitted frame?
How can you generate the DSSS chip sequences for a given configuration?
Our primary goals are to help you understand DSSS configuration within Simplicity Studio v4, and how, in practice, the resulting DSSS signal can be captured without any laboratory equipment.
What Does DSSS Mean
Application note AN971: EFR32 Radio Configurator Guide for RAIL in Simplicity Studio v4 provides a great summary on DSSS. Thus, we'll present here only the critical elements as a quick reference. Experienced readers may skip this section, while those curious for additional detail are invited to review AN971.
Direct Sequence Spread Spectrum takes a bit or group of bits and replaces them with a longer chip sequence, in order to impact the spectral properties of the transmitted signal. It implies much higher bit rate (which is called chip rate hereafter), and thus increases bandwidth - hence the name: spread spectrum. At the receive side, the received chip sequence is replaced by the corresponding bit or group of bits.
One of the most essential attributes of DSSS coding is the spreading factor, which is defined as the quotient of the chip rate and the bit rate.
As suggested by the above characteristics, DSSS does not affect the transmission time for the same transmitted packet.
The chip sequences are generated by a given algorithm (which will be covered in a following section) and the Base chip sequence.
Advantages of DSSS - When to Use it?
The chip sequences should be chosen to have weak correlation (that is, they should look different), so that even if the chip sequences are not demodulated perfectly on the receiver, the corresponding bit values can still be detected. This is how we get the processing gain, which, in practice, will improve the sensitivity.
Note: In theory the processing gain is maximized by increasing spreading factor, but in practice the sensitivity performance degrades for spreading factors higher than 8.
Another advantage of DSSS is immunity against narrowband interferers. This can be explained by the decreased information/frequency factor: even if there are unwanted signals on the occupied bandwidth, the vast majority of the spread spectrum isn't affected by narrowband signal interference.
Also, DSSS can enable the use of relatively inaccurate XOs (crystal oscillators).
Based on the above traits, we can conclude that DSSS is a good fit for PHYs that have relatively low data rates.
How to Configure
The Bitrate field on the radio configurator GUI expects the actual bit rate (as opposed to the chip rate). Note that even when applying DSSS to 4FSK or OQPSK modulation, this field should specify the bitrate (not the symbol rate).
Although the chip rate is automatically calculated by the radio configurator, don't forget to multiply the nominal frequency deviation (specified by the Deviation field) by the DSSS Spreading Factor.
The bit endianness configuration for each part of the packet (preamble, sync word, payload, CRC) is applied on the unencoded bit/bit groups. Therefore, in DSSS encoding cases we say LSCS (Least significant chip sequence) or MSCS (Most significant chip sequence). The bit endianness setting isn't applied on the chip sequences, but on the bits/bit groups of each bytes of the packet before encoding. The chip sequences are always sent out in LSB first order.
Note: don't worry if the above is unclear, we'll further explore its meaning through examples in the second half of this article.
The DSSS Chipping Code Base field expects its value in hexadecimal format. A good selection for Base is one where the generated chip sequences are weakly correlated.
Note: the radio configurator does not warn when an incorrect Base format is selected: it won't check whether all the generated chip sequences are different.
Only predefined combinations of the spreading factor and the chipping code length are accepted by the configurator. For valid combinations, visit AN971.
The Preamble Base Pattern field is irrelevant when DSSS is enabled, as the preamble bits are always substituted with the Base chip sequence.
Sync word search is performed on the already decoded bit stream. Hence, the sync detector block won't look for the DSSS signal format, but the originally-encoded bits themselves.
Note: it may be necessary to tune multiple advanced configuration fields in order to optimize the PHY's performance. You can file a support ticket, and we'll reach out to you with guidance for your particular case.
DSSS Chip Sequence Generation Examples
AN971: EFR32 Radio Configurator Guide for RAIL in Simplicity Studio v4 summarizes how DSSS can be configured. However, it is worthwhile to extend that documentation with a review of practical cases - for example to consider configurations with longer chip sequences. We present multiple such cases below - and also include a script with which you can generate all the chip sequences for a given configuration.
How to Exploit PRS Signals to Obtain the DSSS Chip Sequences
In this section, we show how to use the DOUT and DCLK signals to understand how the payload is encoded into DSSS chip sequences.
RAIL Tutorial: Debugging serves as an excellent tutorial for introducing how such PRS signals can be put out on any GPIO pin.
In the examples that follow, the payload is 0x0F0123456789ABCDEF00005555AAAAFF for all PHYs, just to ensure that all bit-groups show up in the payload. The packet also contains preamble bits, sync word and CRC sequence as detailed in each example.
The DOUT signal represents the deviation's sign (in the case of a phase/frequency modulated signal), and therefore the bits of the packet in the 2(G)FSK case. For OQPSK modulation, it's a little bit problematic to retrieve the chip sequences from the DOUT signal. An algorithm will be presented (below) that addresses this problem.
Note: in 4(G)FSK modulation DOUT doesn't distinguish between the inner and the outer deviations, making the DOUT signal unsuitable to expose the chip sequences.
Note: in MSK modulation, the DOUT signal will look like exactly the same as what is seen when using 2(G)FSK modulation.
2FSK without DSSS Encoding
First we'll take a look at a built-in config - choose the 868M 2GFSK 38.4Kbps 20K PHY from the Base Profile. This config doesn't use DSSS coding, but it is the simplest example to show how to interpret DCLK and DOUT signals:
Since the Frame Bit Endian field on the configurator GUI is set to LSB_FIRST, the payload has been sent out in this order. However, the sync word, the preamble and the CRC are represented as we configured it (these were set to their defaults) on the GUI.
Our logic analyzer software (Saleae) can visualize the parsed datastream by setting up a SPI decoder on the captured signals. If you take a closer look at the payload (after 0xF68D sync word), you'll see that the payload has been sent out in LSB first order as expected.
Note: the DOUT signal is sampled on the rising edge of DCLK.
2FSK with DSSS Encoding
Now, enable DSSS encoding on this PHY. Set the DSSS Chipping Code Length to 8, DSSS Spreading Factor to 4, and the DSSS Chipping Code Base to 0x4D. Don't forget to multiply the deviation by the spreading factor!
Note: as long as we're observing the transmitter only, there's no need care about the performance of the PHY.
According to AN971, this configuration generates these four chip sequences:
Bits (bin)
Chip sequence (hex)
Chip sequence (bin)
Operation (takes place on the LSB first ordered representation)
Chip sequence in LSB first order (bin)
Chip sequence in LSB first order (hex)
00
4D
0100 1101
Base
1011 0010
B2
01
D4
1101 0100
Base right shifted by 4
0010 1011
2B
10
B2
1011 0010
Base inverted
0100 1101
4D
11
2B
0010 1011
Base inverted and right shifted by 4
1101 0100
D4
Note: from this point on we'll represent the chip sequences only in LSB first bit order.
Generate the new configuration and build the application. Once the binary is ready, download it to the target device and transmit a packet while the logic analyzer is waiting for a trigger on the GPIO to which DOUT signal is assigned. Once the DOUT and DCLK signals are captured, export the SPI decoder's result and analyze it.
The first striking thing you'll notice is that, though the transmission time is the same as before, there are many more edges on DCLK. The A* Timing Measurement Pair shows a 4x higher frequency for this configuration compared to the default config. This (4) equals the DSSS spreading factor, so you may then suspect that we can directly capture the chipping sequences on DOUT.
That's exactly the case! Amazingly, using only the transmitter we can capture the chip sequences for a given packet.
Taking a closer look at the parsed DOUT capture, we first see 20 copies of 0xB2. This is the Base chip sequence in LSB first order - just as we expect. According to AN971 the Bit/Chip sequence is 2 for this DSSS configuration, so 20 repeated chip sequences is consistent with the preamble configuration length (40 bit).
Preamble (20 * B2)
Chip sequence
B2
Bits
00
Then we expect a 16 bit sync word (0xF68D), which is encoded to 32 chips. Take the next 8 bytes and decode them to bit groups.
Sync Word
Chip sequence [hex]
D4
D4
4D
2B
2B
B2
D4
4D
Bits [bin]
11
11
10
01
01
00
11
10
How can we explain this behavior? We should first understand how the sync word is represented in the radio's register.
What we set in the configurator GUI is what we would like to see when we transmit the packet w/o DSSS encoding, so it should be represented in the transmission order (from left to right). However, the sync word is always sent out in LSB first order, so the configurator swaps the bits and fits them to the right of the 32-bit wide register. Therefore, if you set up a longer sequence than what you configured as sync word's length, the extra rightmost bits (on the GUI) won't be taken into account.
In the next step the DSSS encoder will fetch up Bit/Chip sequence (2 in this case) long bit groups in each byte and will encode them, keeping the bit groups' endianness.
And as the final step, it will transmit all the chip sequences (per each byte) with the configured byte end bit endianness. This is what we mentioned in the previous chapter, that the bit order settings will applied on the sequences (or on the unencoded bit groups) in each byte, not on the chips in the chip sequences or on only the bits in the payload bytes.
See the steps on the image depicted below:
Take some time to check the same logic on the payload chip sequence below, to completely understand the encoding and transmission order.
Payload
D4
D4
B2
B2
2B
B2
B2
B2
D4
B2
4D
B2
2B
2B
B2
2B
D4
2B
4D
2B
2B
4D
B2
4D
D4
Bits [bin]
11
11
00
00
01
00
00
00
11
00
10
00
01
01
00
01
11
01
10
01
01
10
00
10
If you aren't lost yet, here is one more twist in the story: the CRC is calculated as if it would had been calculated without DSSS encoding, but it is transmitted in the exact same way as we've seen above in the payload's case. Use this algorithm to calculate what will be sent out as CRC:
Apply the endianness config on the "raw" payload you set.
Calculate the CRC as if there would be no DSSS encoding applied (so the bit endianness in this step is bit endianness indeed not the sequence endianness).
Encode the CRC by fetching up its bit groups one by one.
Take the CRC bytes according to its byte endianness settings.
Fetch up the chip sequences in each byte in the configured bit endianness order, which is the sequence or bit group endianness.
CRC (0xBC29)
Chip sequence [hex]
2B
D4
D4
B2
B2
2B
2B
4D
Bits [bin]
01
11
11
00
00
01
01
10
For your reference, here is a capture where the payload endianness was set to MSB first:
and its CRC sequence (0x3D94 without DSSS):
While developing this example, we couldn't comprehensively demonstrate that the generator sequence is generated by the algorithm above: we couldn't confirm that the chip sequences should be rotated to the right (and not left), and we're still not sure how the rotation and the inversion operation takes place when increasing the bit groups' value one by one.
Let's now look at a more complicated example, where 16 chip sequences are used.
2FSK with 8 Chip Sequences
Set up the following configuration:
Spreading factor: 4,
Chipping code length: 16,
and the Base: 0xF649.
In this case there are 16 distinct chip sequences generated, according to the table below:
Note: the Base is already in LSB first order (see the first row).
Bits (bin)
Operation
Chip sequence in LSB first order (bin)
Chip sequence in LSB first order (hex)
0000
Base
1001001001101111
926F
0001
Base right shifted by 2
1110010010011011
E49B
0010
Base right shifted by 4
1111100100100110
F926
0011
Base right shifted by 6
1011111001001001
BE49
0100
Base right shifted by 8
0110111110010010
6F92
0101
Base right shifted by 10
1001101111100100
9BE4
0110
Base right shifted by 12
0010011011111001
26F9
0111
Base right shifted by 14
0100100110111110
49BE
1000
Base inverted
0110110110010000
6D90
1001
Base inverted and right shifted by 2
0001101101100100
1B64
1010
Base inverted and right shifted by 4
0000011011011001
06D9
1011
Base inverted and right shifted by 6
0100000110110110
41B6
1100
Base inverted and right shifted by 8
1001000001101101
906D
1101
Base inverted and right shifted by 10
0110010000011011
641B
1110
Base inverted and right shifted by 12
1101100100000110
D906
1111
Base inverted and right shifted by 14
1011011001000001
B641
Let's again compare actual captured chip sequences to decoded source bits. The Bit/Chip Sequence is now 4 and the preamble is configured to 32 bit length. The sync word is configured to 0x904E and we are using the same payload.
Preamble (8 * 0x926F)
Chip sequence [hex]
926F
Bits [bin]
0000
Sync Word (0x904E)
Chip sequence [hex]
1B64
926F
F926
49BE
Bits [bin]
1001
0000
0010
0111
Payload
Chip sequence [hex]
B641
926F
E49B
926F
BE49
F926
9BE4
6F92
49BE
26F9
Bits [bin]
1111
0000
0001
0000
0011
0010
0101
0100
0111
0110
CRC
Chip sequence [hex]
6D90
E49B
49BE
41B6
Bits [bin]
1000
0001
0111
1011
I encourage you to try and verify the encoding for this (or any other valid config) on your own, to develop a better understanding and get some practice in.
Now, we'll find out what we can do when OQPSK modulation is used.
Investigate DOUT Signal in Case of OQPSK Modulation
It's a bit tricky to get the DSSS chip code base by the transmitter's DOUT signal when using OQPSK modulation; in this case the DOUT signal represents the frequency deviation's sign as well, which won't represent the transmitted bits (or the chip sequences) per se.
Still, we can use the DOUT signal stream if we have an OQPSK signal format. For this task, first gaining some familiarity with how OQPSK modulation works is highly recommended..
The Algorithm
Although the DOUT level does not instantaneously indicate what kind of bit is actively being transmitted, all bits/chips can be calculated using both the present level of DOUT (the actual deviation), and its previous value. Let's denote the bits/chips as C_n and the DOUT signal's level as D_n, where n starts from 0. The n is increased with each rising edge of DCLK.
With these notations C_n equals to C_n-1 ^ D_n, if n % 2 == 0 (so for all even indexed bits/chips) where ^ denotes the XOR operation, and !(C_n-1 ^ D_n), in case of odd indexes, where ! stands for negation. For n = 0, C_n-1 is 0, and the first bit/chip equals the first deviation's sign.
OQPSK DSSS Encoding
Similar to the 2FSK case, first we take a look at the payload when the DSSS encoder is disabled.
This is the block diagram for the OQPSK decoder (fed by the first nibble of the preamble):
One can decode the packet using the DOUT signal and the algorithm.
Preamble (0101 over 40 bit)
DOUT [hex]
2A
AA
AA
AA
...
Decoded bits [hex]
55
55
55
55
...
Sync Word
DOUT [hex]
(AA)
EE
6B
Decoded bits [hex]
(55)
2D
D4
Payload
DOUT [hex]
(6B)
DD
95
F3
A6
C0
8C
6A
3F
59
D5
55
AA
AA
2A
AA
55
Decoded bits [hex]
(D4)
F0
80
C4
A2
E6
91
D5
B3
F7
00
00
AA
AA
55
55
FF
CRC
DOUT [hex]
(55)
E8
37
Decoded bits [hex]
(FF)
29
BC
With DSSS Encoding
What if we now apply DSSS on this configuration?
Spreading factor: 4
Chipping code length: 16
Code base: 0x7AC9
The chip sequences are generated slightly differently than the previous cases where the modulation was not OQPSK. For OQPSK modulation cases, instead of inversion the complex conjugation operation takes place if the bit groups top left bit is 1. See the chip sequences in the table below.
Note: owing to the nature of the OQPSK modulation, inversing all bits in a particular chip sequence would result in the same transmitted signal format except the first chip. Therefore the signal formats would have strong correlation (not desired) if the inversion operation were applied.
Bits to encode
Operation
Chip sequence in LSB first order (bin)
Chip sequence in LSB first order (hex)
0000
Base
0111101011001001
7AC9
0001
Base right shifted by 2
1110101100100101
EB25
0010
Base right shifted by 4
1010110010010111
AC97
0011
Base right shifted by 6
1011001001011110
B25E
0100
Base right shifted by 8
1100100101111010
C97A
0101
Base right shifted by 10
0010010111101011
25EB
0110
Base right shifted by 12
1001011110101100
97AC
0111
Base right shifted by 14
0101111010110010
5EB2
1000
Base complex conjugated
1101000001100011
D063
1001
Base complex conjugated and right shifted by 2
0100000110001111
418F
1010
Base complex conjugated and right shifted by 4
0000011000111101
063D
1011
Base complex conjugated and right shifted by 6
0001100011110100
18F4
1100
Base complex conjugated and right shifted by 8
0110001111010000
63D0
1101
Base complex conjugated and right shifted by 10
1000111101000001
8F41
1110
Base complex conjugated and right shifted by 12
0011110100000110
3D06
1111
Base complex conjugated and right shifted by 14
1111010000011000
F418
Again, set up the DSSS configuration, hit the Generate button, build the example, download, and transmit the same packet while the logic analyzer is listening on the DOUT pin.
We won't detail the OQPSK decoding (you can perform this task, as we demonstrated in the previous example), but will share the results here:
Preamble: (20 * 0x7AC9)
Chip sequence [hex]
7AC9
Bits [bin]
0000
Sync Word
Chip sequence [hex]
C97A
18F4
18F4
AC97
Bits [bin]
0100
1011
1011
0010
Payload
Chip sequence [hex]
F418
7AC9
EB25
7AC9
B25E
AC97
25EB
C97A
5EB2
97AC
418F
D063
18F4
063D
Bits [bin]
1111
0000
0001
0000
0011
0010
0101
0100
0111
0110
1001
1000
1011
1010
CRC
Chip sequence [hex]
C97A
418F
8F41
B25E
Bits [bin]
0100
1001
1101
0011
Introducing the Python Scripts
In this section, we introduce two helpful python scripts that we've developed:
to generate chip sequences for a given DSSS configuration, and
to decode captured DOUT datastreams to bits/chips (when OQPSK modulation is used).
Note: the scripts are written in Python 3, so you shouldn't run them with Python 2.
The DSSS Chip Sequence Generator
The DSSS_sequence_generator.py script expects the DSSS configuration parameters:
the spreading factor,
the code length,
the Base sequence,
and whether it is for OQPSK modulation or not.
It first validates the spreading factor/code length combination according to AN971 (Table 3.5.). There is only one function implemented in this script, which prints out the chip sequences in ascending order. This function is called if the script is ran from a terminal like python3 DSSS_sequence_generator.py.
The chipping codes are printed out in reversed bit order, just as those that are captured on DOUT.
The OQPSK Decoder
The OQPSK_decoder.py expects the DOUT signal in hexadecimal format (actually as a python integer variable).
It then calculates the number of nibbles in the whole sequence. This sequence will be calculated by the algorithm defined above. At the end, it prints out the decoded packets in the given format in CL long chunks.
Note: ensure that the DOUT signal does not start with 0, otherwise the length calculation will be wrong.
Takeaways from this Article
Guidance to configure the DSSS correctly in Simplicity Studio v4,
An understanding of the bit endianness when DSSS encoding is applied,
Debugging skills to capture DOUT and DCLK PRS signals and parse out the DSSS chip sequences of a particular transmitted packet (for MSK, 2FSK and OQPSK modulation),
Examples for greater than 4 chip sequences, both for 2FSK and OQPSK modulation,
Python scripts for generating chip sequences for a given configuration and retrieving bits/chips when using OQPSK.
EZRadioPRO devices are designed to be compliant with 802.15.4g standard (subGHz FSK) configured via the Wireless Development Suite (WDS) as per the datasheets (Si4467/68, Si4460/1/3/4). However the provided example does not configure the devices perfectly, there are two bugs which should be addressed by the user for now.
If you just download the example with the same PHY on two development boards the receiver won't send the ACK packet back as it is expected.
Note: the example runs on Si4467 and Si4468 with the regular (legacy) boot mode as well.
The bit endianness of the payload
The 802.15.4g standard defines the bit order to LSB first everywhere except the length field (within the PHR configured as Field 1) which must be in MSB first order (see our article about the 802.15.4 standard on RAIL).
The example defines the Data bit order in MSB first by default for all payload fields as it is depicted in the following picture.
You should set it to LSB first since the application is responsible for swapping the bits on Field 1 whenever it is needed (where it loads the TX FIFO, or reads out the RX FIFO).
Therefore the PHR parsing will fail expecting more or less payload bytes than the transmitter actually sends (not to mention that it will mix up the whitening and CRC configurations too).
The 4byte CRC seed
Though, as long as the bit endianness configured correctly (and the transmitted/expected Field contents matches) the two radio boards can communicate with each other and the receiver will always send an ACK packet back regardless of the transmitter's configuration (whether it uses data whitening or not and whether the 2 or the 4 bytes long CRC will be transmitted), the devices will not be compliant with the standard: the 4bytes CRC will be miscalculated due to it is using '0' seed, instead of the '1's (see the referred article above).
Unfortunately this bug cannot be solved by using the WDS only, you should modify the generatedradio_config.hfile by hand. According to the API docs the PKT_CRC_CONFIG.CRC_SEED bitfield is responsible for this configuration. Modify the property's value from 0x57 to 0xD7.
Testing the example against an EFR32 development kit
Prepare the EFR32
Build a RAILTest application, load it onto any EFR32 development kit and issue the following commands in this order (see UG409: RAILTest User's Guide for reference on the commands):
Load the example config attached to this article (find at the bottom of this page) on any development kit with a revC or revA radio. Note that the Base frequency field had been overwritten to 863.25MHz and the 100kbps 2FSK config had been selected.
Use EZRadioPRO as transmitter
If you miss to configure the EZRadioPRO to LSB first order, sending a packet will not be received by the EFR32 at all.
Correct the field bit endianness then send a packet by the EZRadioPRO.
The EFR32 will print out the following line on the command line:
Note: pay attention to how the payload's length (len:) and its first byte changes with the configuration's changes!
You should configure the same payload on the EFR32 by the following commands to be compliant with the 802.15.4g bidirectional packet example's default payload:
With this payload the 2byte CRC will be applied with no whitening, and the LEDs will light up on the EZRadioPRO development board even if only the bit endianness bug was addressed.
Also, the packet will be received correctly if the whitening is enabled on the EFR32:
setTxPayload 0 0x18 0xA0 0x02 0x00 0x6A
If you don't correct the 4bytes CRC calculator's seed this packet won't be received, nor with enabled whitening:
setTxPayload 0 0x00 0xE0 0x02 0x00 0x6A
or
setTxPayload 0 0x10 0xE0 0x02 0x00 0x6A
Summary
In this article we introduced two bugs found in the 802.15.4g bidirectional packet example and the workaround on how to deal with them. Also a minimal test guide had been introduced in order to show how can we validate our CRC, whitening and payload configuration for the 802.15.4g standard.
On the previous tutorial, we had both transmit and receive working, but we couldn't see what we received. This tutorial will solve that problem.
We're also adding calibrations, which is recommended for all projects.
You can find a SimpleTRX example in Simplicity Studio, which has a similar purpose. However, the result of this tutorial will be somewhat different. Mainly, this tutorial is not very pedantic on checking errors returned by all functions, to make it easier to understand. However, we highly recommend checking all possible errors, therefore using the SimpleTRX example in Studio as a reference.
Enabling Calibrations
First, let's load PA factory calibrations: Open the configuration for the component RAIL Utility, Power Amplifier, and turn on the Enable PA Calibration switch.
All other calibrations are enabled by default in RAIL Utility, Initialization, but require some application code to perform them. First, we need to add the related event during initialization to enable it (in app_init.c):
This is all that's needed from an application to perform all calibrations available on EFR32, to have the maximum available performance. Calibrations are detailed further in another tutorial.
Preparing for printing
In the previous tutorials, we already enabled LEDs and buttons, but we'll need a more user friendly output: We need a serial console. To enable that, install the following components:
IO Stream: USART on vcom (the text to serial driver)
Tiny printf (the printf implementation we recommend) - note that you should use #include "printf.h" to use it, not stdio.h!
IO Stream: Retarget STDIO (to connect the two)
Finally, we need to enable Virtual COM UART on the Board Control component:
This pulls up a GPIO, which will enable the USB-UART bridge on the WSTK.
Receive FIFO
We already configured Tx FIFO multiple ways. RAIL also implements an Rx FIFO. By default, it's 512B, allocated by the RAIL library. We will use that, but it is configurable. See RAILCb_SetupRxFifo() and RAIL_SetRxFifo() in the API documentation for details.
By default, you cannot use the whole 512B, as RAIL adds a few bytes to each packet to store timestamp, RSSI, and similar information in the FIFO (sometimes we call this "appended info").
We're working on what we call packet mode. It's the simpler, and recommended way, but it's not possible to handle packets bigger than 512B with this method, in which case FIFO mode should be used. We return to that topic in a later tutorial.
The Packet Handle
The FIFO is accessible with RAIL_RxPacketHandle_t variables or packet handles. Obviously, we don't have the handle of the packet we just received, but we have "sentinel" handles:
RAIL_RX_PACKET_HANDLE_NEWEST
RAIL_RX_PACKET_HANDLE_OLDEST
RAIL_RX_PACKET_HANDLE_OLDEST_COMPLETE
RAIL_RX_PACKET_HANDLE_INVALID
Note that RAIL_RX_PACKET_HANDLE_OLDEST and RAIL_RX_PACKET_HANDLE_NEWEST will always return with packet information. For example, OLDEST will return with one of the following:
the oldest packet received, if available
the packet the radio is receiving at the moment, if available and if no fully received packet is available
the placeholder which will be used for the next packet the radio will receive, if not fully or partially received packet is available
If you don't care about upcoming or ongoing packets, you can use RAIL_RX_PACKET_HANDLE_OLDEST_COMPLETE: It will only return with fully received packets.
When to Read the FIFO
Let's say we want to download the packet when it's fully received (although it's possible to access it during reception). We must let RAIL know this in the event RAIL_EVENT_RX_PACKET_RECEIVED. If we don't, RAIL automatically frees the memory allocated to the packet we just received.
We have two options:
Download the packet from the event handler
Hold the packet in the FIFO and download later
While the first one is simpler, it is desirable in most cases to keep large memory copy operations outside of interrupt handlers, therefore the attached example demonstrates the second method.
The Event Handler
In the event handler, we want to instruct RAIL to hold all successfully received frames:
void sl_rail_app_on_event(RAIL_Handle_t rail_handle, RAIL_Events_t events)
{
//...
if ( events & RAIL_EVENTS_RX_COMPLETION ) {
if ( events & RAIL_EVENT_RX_PACKET_RECEIVED ) {
RAIL_HoldRxPacket(rail_handle);
sl_led_toggle(&sl_led_led0);
} else {
sl_led_toggle(&sl_led_led1); //all other events in RAIL_EVENTS_RX_COMPLETION are errors
}
}
}
The API RAIL_HoldRxPacket will also return a packet handler, but we ignore that since we're going to use sentinel handles. Keep in mind that you must release all packets that were held, otherwise, you will lose part of your RX fifo, essentially leaking memory.
Download and Print
First, let's create a buffer we can use to download the packet:
static uint8_t rx_buffer[256];
In app_process_action(), if there's something in the handle, let's download and print it:
First, RAIL_GetRxPacketInfo() will return with a valid packet_handle if we have a completely received frame. In that case, it also returns a usable packet_info which has the length of the received packet, and pointers to download it. Note that the length will include the full frame, and is always valid, whichever length configuration is used (fixed or variable).
Since the receive buffer is a ring buffer, the packet in the buffer might not be in a single sequential buffer, copying from that using memcpy() is a bit complicated: RAIL_CopyRxPacket() inline function implements that.
The API RAIL_GetRxPacketDetails() will return some useful information of the packet, such as RSSI or timestamps.
Finally, we release the packet, using RAIL_ReleaseRxPacket().
Note that this method is safe, even if we receive a new packet while downloading one: That packet will be downloaded and printed when app_process_action() is called the next time.
Download in the Event Handler
Downloading the packet in the event handler is essentially the same as what we did in the main loop above, except we don't need to worry about releasing:
Note that packet_details should be a global variable as well in this case.
Conclusion
With this, you can use RAIL for basic transmit and receive, which concludes the getting started series. However, RAIL can do much more - you can continue with the basic tutorials from the table of contents site.
In the previous tutorials, we set up RAIL to do various tasks through API commands. However, RAIL can inform the application on various events almost real time (basically, wiring interrupts through RAIL). So let's improve what we had in the previous tutorial.
The Event Handler Function
You might have already seen this function in app_process.c:
This is the event handler function, which is called by the RAIL library, and routed through the RAIL Utility, Initialization component.
The events variable is a bit field, holding all possible events. Be careful, this bit field does not fit into a 32-bit variable, which is the size of the int on our MCUs.
The event handler should be prepared for multiple events in a single call. There's no "clearFlag" mechanism, each event will be signaled once, and the application is responsible for handling it.
Keep in mind that almost all events are generated from interrupts, so the event handler is usually running from interrupt context (i.e., interrupts are disabled). Therefore, the event handler should run fast; if you have to do something that takes more time, set a flag, and do it from the main loop.
You should be also careful with RAIL interrupt safety. Calling state changing APIs (RAIL_Start*, RAIL_Stop* or RAIL_Idle) from the interrupt handler is allowed, but we recommend to read the interrupt safety article before doing so.
Configuring Events
The events enabled by default are configured on the RAIL Utility, Initialization component (under 'Platform/Radio', press 'Configure'):
However, using the RAIL API directly is actually simpler than using the event configuration capabilities of this component. The API for this is RAIL_ConfigEvents(). You can keep the event setup enabled, it doesn't really matter.
RAIL_ConfigEvents() has a mask and an events parameter, making it simple to configure multiple events at the same time; e.g., to enable or disable a single event, leaving the others untouched:
However, you only need to do this in rare cases. Enabling events is not like unmasking interrupts: Enabling them will not trigger the event handler on past events.
Tx Error Handling
In the last tutorial, we sent out a frame, but we didn't check the results because it would have needed the event handler. Let's turn on led0 on success and led1 on any error (we'll be turning led1 on when transmitting and clearing led1 if no Tx error occurs).
First, for the LEDs, enable the Simple LED component with led0 and led1.
In app_process.c (to get access to the led instances, now declared in autogen/sl_simple_led_instances.h):
#include "sl_simple_led_instances.h"
For the actual radio related changes, we'll need to enable the success and error events. We can do that during init since we don't really need to disable anything at runtime for such simple applications:
Next, let's check for errors returned by the StartTx call:
RAIL_Status_t status = RAIL_StartTx(rail_handle, 0, RAIL_TX_OPTIONS_DEFAULT, NULL);
if ( status != RAIL_STATUS_NO_ERROR ) {
sl_led_toggle(&sl_led_led1);
}
Finally, let's check the events for success and errors:
void sl_rail_app_on_event(RAIL_Handle_t rail_handle, RAIL_Events_t events)
{
(void)rail_handle;
if ( events & RAIL_EVENTS_TX_COMPLETION ) {
if ( events & RAIL_EVENT_TX_PACKET_SENT ) {
sl_led_toggle(&sl_led_led0);
} else {
sl_led_toggle(&sl_led_led1); //all other events in RAIL_EVENTS_TX_COMPLETION are errors
}
}
}
With this, you should see led0 toggling on every transmitted packet.
Receiving Packets
Let's add receive capabilities to this application! Again, use led0 to report success and led1 for errors.
First, we need to modify the init code.
In app_init.c (to get access to the led instances):
#include "sl_simple_led_instances.h"
We need RX success/error events enabled, and we need to start the radio in RX mode:
RAIL_Handle_t app_init(void)
{
//...
RAIL_ConfigEvents(rail_handle, RAIL_EVENTS_ALL,
RAIL_EVENTS_TX_COMPLETION | RAIL_EVENTS_RX_COMPLETION);
RAIL_Status_t status = RAIL_StartRx(rail_handle, 0, NULL);
if ( status != RAIL_STATUS_NO_ERROR ){
sl_led_toggle(&sl_led_led1);
}
}
The API RAIL_StartRx() is similar to Tx: the only important option is the second one, which is the channel.
Finally, let's check the events for success and errors (in app_process.c):
void sl_rail_app_on_event(RAIL_Handle_t rail_handle, RAIL_Events_t events)
{
//...
if ( events & RAIL_EVENTS_RX_COMPLETION ) {
if ( events & RAIL_EVENT_RX_PACKET_RECEIVED ) {
sl_led_toggle(&sl_led_led0);
} else {
sl_led_toggle(&sl_led_led1); //all other events in RAIL_EVENTS_RX_COMPLETION are errors
}
}
}
Auto state transitions
By default, after receiving or transmitting a packet, RAIL will switch to idle mode (i.e., turn off the radio). Since we want it to keep receiving future packets, we can use RAIL's auto state transition feature. This can be configured on the RAIL Utility, Initialization component, let's set all dropdowns to RX:
We're basically saying that after the packets are received/transmitted, we want to return to Rx. These configurations can be also changed in run-time, using the RAIL_SetRxTransitions() and RAIL_SetTxTransitions() APIs.
If you compile and flash the program after this modification, you should see that now both WSTK toggles led0 on every button press.
Testing and Conclusion
If you install this example to two boards (make sure you attach the antennas if you use sub-GHz kits on sub-GHz config), you will see that on the press of btn0 on either device, led0 on both devices toggle. However, we can't see what we're receiving, since we never actually download the messages.
In this tutorial, we're going to modify the project Flex (RAIL) - Empty Example to transmit a packet. We're going to use the default, fixed length 16Byte 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:
Note that you can't use any arbitrary size; EFR32 creates a FIFO ring buffer on this memory area, and it can only handle buffer sizes of power of 2 between 64 and 4096 (i.e., 64, 128, 256, 512, 1024, 2048, or 4096).
The tx buffer can hold more than one packets, 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.
The memcpy() is just a standard C instruction to write to buffers, and we use RAIL_SetTxFifo() to pass that buffer to RAIL. We also tell RAIL how long the buffer is, and how much data it has already in it.
Writing to the Buffer Indirectly
(See the attached app_init.c and app_process.c files for reference code)
In app_init.c:
#define BUFFER_LENGTH 256
static uint8_t tx_buffer[BUFFER_LENGTH];
RAIL_Handle_t app_init(void)
{
// Get RAIL handle, used later by the application
RAIL_Handle_t rail_handle = sl_rail_util_get_handle();
RAIL_SetTxFifo(rail_handle, tx_buffer, 0, BUFFER_LENGTH);
return rail_handle;
}
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 WriteTxFifo 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.
Calling RAIL_SetTxFifo() does not move any memory, just sets a few register, while calling RAIL_WriteTxFifo() does need some time to copy the payload to the buffer.
Calling RAIL_SetTxFifo() is not allowed during transmission.
Indirect mode very clearly separates init and process code.
For simpler applications, RAIL_SetTxFifo() can be simpler. If you want to send the same packet over and over (or only change a few bytes in it), it's much better, since you don't have to write the whole packet again.
On the other hand, the separation in RAIL examples between app_init and app_process works very nicely with the indirect method, and in most real cases, you need to move memory anyway. If you want to send longer packets than your buffer, you must use RAIL_WriteTxFifo(), as you can call it during transmit. It's also useful if you want to send out a lot of packets: Use RAIL_WriteTxFifo() 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 RAIL_WriteTxFifo can reset the FIFO, which means it will invalidate the data already in there. This is also possible with RAIL_ResetFifo. We generally recommend not to use it, a well-written code shouldn't need it (as it should only store in the FIFO what should be sent out), and it might mask bugs. Resetting the FIFO also takes extra time.
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 RAIL API reference). 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 RAIL_SetFixedLength(), 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 RAIL_SetFixedLength(railHandle, 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 AN1253. 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 16B frame, both for the above described variable length and 16 Byte fixed length mode, so this is used in the attached sample code:
We use PAYLOAD_LENGTH-1 in the length field, since the 1B length field itself shouldn't be counted.
The API RAIL_SetFixedLength() is available in variable length mode, and it changes the radio to fixed length operation (again, both for rx and tx). Calling RAIL_SetFixedLength(railHandle, RAIL_SETFIXEDLENGTH_INVALID) will restore it to variable length mode.
Setting up the example
Button handling
The attached 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):
By default, the Simple Button is configured for PF6, which matches the WSTK PB0 button
Since this callback is in interrupt context, we avoid using RAIL API directly. It is safe in almost all cases, but calling RAIL_StartTx() 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:
RAIL is short for Radio Abstraction Interface Layer, and is the most direct interface available for EFR32 radios. You can think of it as a radio driver. While this sounds like a restriction, this actually makes software development for proprietary wireless much simpler:
EFR32 has a very flexible radio, which also makes it quite complex. RAIL (and the radio configurator) provides an easy-to-use interface of EFR32's features.
The various generations of EFR32s are slightly different, but the RAIL API is the same (except maybe the chip specific updates), which makes hardware updates almost seamless.
We plan to add RAIL support to all of our future wireless MCUs.
RAIL application development is currently possible in the Flex SDK in Simplicity Studio. To progress further with this tutorial, please remember to open the RAIL API documentation.
When to Use RAIL?
If you have an existing protocol, and you must be compatible with it, you'd probably have to use RAIL (as all of our stacks use a standardized frame format).
If you want to use sub-GHz communication, you have a few options, including RAIL. For simple point to point communication, RAIL might be the simplest solution. However, as soon as you need security or addressing, it might be better choose a protocol stack, such as Connect.
RAIL also has the benefit that it adds the least amount of delays to all radio events: In fact, some event notification will happen in interrupt context.
What is covered by RAIL?
RAIL only supports what the radio hardware supports. For example, auto ACK and address filter is available, as the hardware provides support for it. On the other hand, while security is important for wireless communication, it's not really a radio task, hence RAIL does not support it. The crypto engine is an independent peripheral that can be used to encrypt payloads before giving them to RAIL and the radio hardware.
Supported Energy Modes
EM1p or higher is required for the radio to be operational (i.e., Transmitting, receiving, or waiting for packets, RAIL timer running from HF clock). On devices without EM1p support, EM1 is required for the radio. See AN1244: EFR32 Migration Guide for Proprietary Applications for more details on EM1p.
EM2 or higher is required for RAIL scheduling or timers (Running from LF clock, which needs configuration, a topic in Tutorial 5).
EM3 or higher is required for RAIL to work without re-initialization.
Writing Code from Scratch
We do not recommend writing code from scratch because it is much simpler to have something existing that sets up the include paths and linker settings correctly. The best way to start is the example Flex (RAIL) - Empty Example.
RAIL Components
The empty application includes the following RAIL related components (under 'Platform/Radio'):
RAIL Library, Single Protocol, which is the library itself
RAIL Utility, Power Amplifier, which configures and initializes the power amplifier
RAIL Utility, Initialization, which initializes RAIL and loads an initial configuration
There are a few components that you might want to install
RAIL Utility Packet Trace Information, which enables PTI, the hardware that feeds data to Network Analyzer
Radio Utility, Front End Module, which can be used to drive a FEM (or external PA/LNA), if you HW uses one
If you work on a starter kit, you don't need to change the configuration of most of these components (for custom hardware, there will be a new article coming soon), except the Initialization component. However, the initialization component also includes a good starting point, that we don't need to modify for simple applications.
The Radio Configurator
The Radio Configurator is accessible by configuring the component Advanced Configurators/Radio Configurator. The usage of the radio configurator is out of scope for this tutorial series. For more details, see AN1253: EFR32 Radio Configurator Guide.
The project structure
Projects always include the following parts:
autogen folder: Only the autogen folder includes generated code. It includes the PHY configuration (rail_config.c), init code, the linker script, and other generated code used by components, like the command descriptors for the CLI interface
config folder: Component configuration headers are placed into this folder. These can be edited with the Component Editor that can be opened on the Project Configurator via the Configure button, but directly editing the header file is also possible.
gecko_sdk folder (with version number): Source and binary files added by components
files in the root folder: Only the application specific files should be in the root folder, including source files, the project configurator (.slcp file) and the Pin tool (.pintool file)
Note that all files related to the project should be visible in the project explorer, including header and library files.
All projects include main.c, which is not recommended to modify. Instead, add the initialization code to app_init.c, and implement the main loop in app_process.c. This way, the System components can initialize components, and call the "process" function of the components that requires this. Additionally, enabling an RTOS will convert app_process's main loop into an RTOS task.
Conclusion
This project is ready to send out frames. We're going to do that in the next part. You can also find other tutorials from the Table of Content site.
The crystal or TCXO reference spur is located +/- XO frequency (typ. 38.4 MHz for EFR32 Series 1-based designs) from the RF carrier frequency. This KBA is focusing on the possible HW modifications that can be suggested to enhance the suppression of these XO reference spurs around the carrier frequency. Also, the KBA is based on the measurement results of EFR32 Series 1-based radio boards at the 868/915 MHz frequency bands.
An evident approach could be to apply a high-Q band-pass filter, e.g. SAW filter, in the RF path to filter out the XO reference spurs. This will ensure very low level of spurs in a reliable way, but on the other it will also increase the insertion loss at the fundamental frequency while it is not a desirable solution for cost sensitive applications.
The XO coupling is basically occurred through two main domains within the part: VCO and PA. These blocks are supplied through RFVDD and PAVDD and thus forming two coupling paths. The power supplies can be modulated either by VDD and/or GND. Leakage current paths outside the chip depend on board design through impedance at HFXO frequency between supply domains.
So, a filtering scheme focused for the XO frequency on both RFVDD and PAVDD nets can help suppress these spurs effectively. The table below shows some measurement data with different RFVDD and PAVDD filtering configurations, captured on BRD4164A Rev A02 radio board at 915 MHz / +20 dBm.
Fund power in dBm
XO spur at -38.4MHz
XO spur at +38.4MHz
subG PAVDD
RFVDD
dBm
dBc
dBm
dBc
19,48
-57,9
-77,4
-58,5
-78,0
ferrite + 1nF
ferrite + 1nF
19,51
-57,6
-77,1
-58,3
-77,8
ferrite + 1nF + 220pF
ferrite + 1nF
19,53
-57,7
-77,2
-58,3
-77,8
1nF + 220pF
ferrite + 1nF
19,57
-63,8
-83,4
-57,3
-76,9
ferrite + 56pF
1nF
19,6
-61,8
-81,4
-56,6
-76,2
56pF
1nF
19,57
-61,6
-81,2
-56,6
-76,2
56pF
56pF
19,49
-61,7
-81,2
-56,7
-76,2
1nF
56pF
19,46
-62,3
-81,8
-56,9
-76,4
ferrite + 1nF
56pF
19,45
-52,4
-71,9
-63,2
-82,7
ferrite + 1nF *
ferrite + 56pF *
19,48
-60,8
-80,3
-57,2
-76,7
ferrite + 1nF
ferrite + 56pF + 47nF
19,48
-60,9
-80,4
-57,2
-76,7
1nF
56pF + 47nF
19,5
-60,8
-80,3
-57
-76,5
47nF
56pF + 47nF
* Note: close to the original BOM of reference radio boards.
Some good performer configurations in terms of spur suppression are highlighted in yellow above.
The RFVDD is filtered at the RFVDD pin, while the subG PAVDD is filtered at the PAVDD pin and at the BIAS pin of the external ceramic balun.
Some conclusions:
The spur suppression of the radio board with the original filtering configuration can be improved.
Low-side XO spur is high when RFVDD has series ferrite but without nF-ranged capacitor. If RFVDD has no ferrite then a capacitor (either pF- or nF-ranged) is enough to have low-level low-side XO spur. Low-side spur is high on radio boards because RFVDD has a ferrite but with pF-ranged capacitors only.
The high-side XO spur is unfortunately pretty good only when the RFVDD has ferrite without nF-ranged capacitor.
Overall performance is better if the focus is on the low-side XO spur suppression, since that is more critical with the radio board's original BOM.
In absolute level, however, the spurs are well under -51dBm in any configuration shown above.
Further layout considerations:
If the RFVDD and PAVDD pins are supplied from the same power net, then consider adding a series filtering placeholder mounted between them.
Maximize the isolation between the crystal/TCXO, XO traces and RFVDD trace on the board layout. Ensure having a GND strip with stitching vias between the XO and RFVDD traces.
Proprietary Knowledge Base
Latest patch for EZRadio/PRO and do I need to use it?
(Relevant part numbers: Si4x55-C2A, Si4x6x-C2A, Si446x-A2A)
It is described in detail in section "9.6. Patching the Radio" of
AN633: Pprogramming Guide for EZRadioPRO Si4x6x Devices
The EZRadio and EZRadio Pro Family Rev C2A/A2A Device Errata contains this information.
It is important to verify the exact device part number and revision.
This can be identified by the top marking of the package or by the replay of the PART_INFO API command.
See details in the article Using PART_INFO command to identify EZRadio/PRO part number
If you are affected by an errata item that has a patch fix, you should apply that patch.
The latest patches are attached to this article.
(Recently a new patch (ID 0x4668) was created, that replaces patch 0xCA90,
and an other (ID 0xD046) that replaces patch 0x80FE.
Both are fixing an additional new errata item (#13), that will be added to the next release of the errata.
The new errata item is about a wake up timer (WUT) related crash that may happen when going to SLEEP state.)
Yes, if WDS was updated since the latest patch release.
WDS stores the patch files at C:\Program Files (x86)\SiliconLabs\WDS3\Patch\ .
It is always advised to make sure the proper patch is applied by WDS, and to correct if necessary.
(Currently latest WDS v.3.2.11.0 / 16.12.2016 does not contain the latest patches.
In the WDS patch folder the content of Si446x_C2_GENERAL.csg should be replaced by fw_6_0_2_4668.csg
The content of Si446x_A2_PSM.csg should be replaced by fw_6_0_7_D046.csg
The original WDS filenames should be kept.)
After patching and executing the POWER_UP API command, you can get back the patch ID as the PATCH replay field of the FUNC_INFO API command.
Creating custom manufacturing tokens in Connect based apps
Custom manufacturing tokens are similar to custom stack token, but the purpose is to write once (in manufacturing case) and read multiple times (as opposed to custom stack tokens where the purpose is to modify the value several times at runtime).
While for stack tokens there is a Token Manager component where one can specify the name and the location of the header file that contains the custom stack tokens, this is not the case for custom manufacturing tokens.
For custom stack tokens the default location is the project's config directory and the default name is sl_custom_token_header.h. Don't place the custom manufacturing token defines in this file.
Instead, create a file with the content required for custom manufacturing token(s), name it freely (ex.: sl_custom_mfg_token_header.h - preferably don't use sl_custom_token_header.h to avoid conflicts) place it in the project's config directory (not necessarily, but seems a good choice, beside sl_custom_token_header.h).
Then add -D APPLICATION_MFG_TOKEN_HEADER="sl_custom_mfg_token_header.h" to the compile options. The easiest way to apply this is opening Project Properties -> C/C++ Build -> Settings -> Preprocessor -> Defined symbols -> + (Add..) and add APPLICATION_MFG_TOKEN_HEADER="sl_custom_mfg_token_header.h":
Sample manufacturing token example from SimplicityStudio_v5/developer/sdks/gecko_sdk_suite/v3.0/platform/service/token_manager/inc/sl_token_manufacturing_series_1.h:
The header file should contain only the token definitions, avoid using header guards.
RAIL Tutorial 4: Combining Transmit and Receive
This tutorial builds on the following tutorials:
Please read them first if you haven't.
You can find other tutorials on the table of contents site.
In this tutorial, we're going to combine what we had from the previous tutorial. We're going to switch between transmit and receive, and we're going to use a few peripherals on the WSTK as a user interface.
Our goal is to transmit a predefined packet when either of the buttons are pressed, and print the packet to the serial console when received. We're also going to toggle LEDs on finished Rx/Tx, but we ignore errors.
You can find a SimpleTRX example in Simplicity Studio, which has similar purpose. However, the result of this tutorial will be somewhat different.
Preparations
We're going to start from the usual, the Simple RAIL with HAL example. However, we'll need some modifications in the Hardware Configurator because we'll need the UART bridge and buttons, which are not enabled by default. To do that, enable the following defaultMode peripheral modules:
Next, we'll have to include a few things to have these peripherals, and while some of the peripherals are automatically initialized, not all, so we're going to do that in a function:
As you see,
gpioCallback()
will be called on button press.Radio init
By default, after receiving or transmitting a packet, RAIL will switch to idle mode (i.e., turn off the radio). Since we want it to keep receiving future packets, we can use RAIL's auto state transition feature with the
RAIL_SetRxTransitions
andRAIL_SetTxTransitions
APIs.We're basically saying, after the packets are received/transmitted, we want to return to Rx. We're also enabling some events as we've seen before. The only new one is
RAIL_EVENT_CAL_NEEDED
- it will have it's own tutorial; for now, it is important to know that it should always be implemented to have the maximum available performance.Basic Rx and Tx Combined
There's not much new here. We start by initializing everything then going to Rx mode. When a button is pressed
startTx
will betrue
, which will start transmitting a packet (changing from Rx to Tx mode). Note that the variablestartTx
is volatile, because it's changing in interrupt context. If you're unfamiliar with this practice, look it up; it's very important for low-level embedded applications.In the event handler, we blink LEDs on Rx/Tx completion. In the CAL_NEEDED handler we're basically saying: If any calibration is needed, run it. Again, this is important to get the maximum available performance, but we won't go into details for now.
At this point, you should see LEDs blinking on both devices when you press a button on either of them. So at this point, we added a single concept: we return to Rx state both after receiving a packet, and after transmitting one. This enabled a combined Rx/Tx application. However, there's one important thing we didn't do so far: we did not download the received packet.
Downloading the Packet
Receive FIFO
We already configured Tx FIFO multiple ways. RAIL also implements an Rx FIFO. By default, it's 512B, allocated by the RAIL library. We will use that, but it is configurable. See
RAILCb_SetupRxFifo()
andRAIL_SetRxFifo()
in the API documentation for details.By default, you cannot use the whole 512B, as RAIL adds a few bytes to each packet to store timestamp, RSSI and similar information in the FIFO (sometimes we call this "appended info").
We're working on what we call packet mode. It's the simpler, and recommended way, but it's not possible to handle packets bigger than 512B with this method,; in which case FIFO mode should be used. We return to that topic in a later tutorial.
The Packet Handler
The FIFO is accessible with
RAIL_RxPacketHandle_t
variables or packet handles. Obviously, we don't have the handle of the packet we just received, but we have "sentinel" handles:The names are self-explanatory.
When to Read the FIFO
Let's say we want to download the packet when it's fully received (although it's possible to access it during reception). We must let RAIL know this in the event
RAIL_EVENT_RX_PACKET_RECEIVED
. If we don't, RAIL automatically frees the memory allocated to the packet we just received.We have two options:
While the first one is simpler, we also want to print the message on UART, which takes a long time, and that's not a good idea in interrupt context, so we'll use the second one.
To do that, let's create a volatile packetHandle, with invalid value:
We should only write the handle if it's invalid, otherwise we might lose the handle to a locked packet (and essentially lose part of the receive buffer). We'll also need a buffer to copy the packet before printing it.
The Event Handler
Let's implement the event handler so it will only store packets if packetHandle is invalid:
The API
RAIL_HoldRxPacket
will lock the packet we just received (or receiving) and return its handle.Download and Print
In the while loop, if there's something in the handle, let's download and print it:
Let's go through this, line by line:
First,
RAIL_GetRxPacketInfo
will return with packetInfo, which has the length of the received packet, and pointers to download it. Note that unlike transmit, this is independent of the length coding method (fixed or variable) we used in the Configurator.Since the receive buffer is a ring buffer, the download will be always the same 3 lines of code, which is done in the
RAIL_CopyRxPacket
inline function.The API
RAIL_GetRxPacketDetails
will return some useful information of the packet, such as RSSI.Finally, we release the packet, and set the handle to invalid, so the event handle can write it.
Download in the Event Handle
Downloading the packet in the event handler is essentially the same as what we did in the main loop above:
Note that
packetDetails
should be a global variable as well in this case.Conclusion
With this, you can use RAIL for basic transmit and receive, which concludes the getting started series. However, RAIL can do much more - you can continue with the basic tutorials from the table of contents site.
API Introduced in this Tutorial
Functions
Types and enums
Understanding DSSS Encoding and Decoding on EFR32 Devices
Introduction
This article describes how DSSS sequences are generated on EFR32 devices. It also demonstrates (for multiple configurations and modulation types) how these chip codes can be read out when transmitting a DSSS-coded frame. The DSSS symbols captured with this technique can then be fed directly to a signal generator to emulate an EFR32 transmitter.
In the following sections, we'll address questions like:
Our primary goals are to help you understand DSSS configuration within Simplicity Studio v4, and how, in practice, the resulting DSSS signal can be captured without any laboratory equipment.
What Does DSSS Mean
Application note AN971: EFR32 Radio Configurator Guide for RAIL in Simplicity Studio v4 provides a great summary on DSSS. Thus, we'll present here only the critical elements as a quick reference. Experienced readers may skip this section, while those curious for additional detail are invited to review AN971.
Direct Sequence Spread Spectrum takes a bit or group of bits and replaces them with a longer chip sequence, in order to impact the spectral properties of the transmitted signal. It implies much higher bit rate (which is called chip rate hereafter), and thus increases bandwidth - hence the name: spread spectrum. At the receive side, the received chip sequence is replaced by the corresponding bit or group of bits.
One of the most essential attributes of DSSS coding is the spreading factor, which is defined as the quotient of the chip rate and the bit rate.
As suggested by the above characteristics, DSSS does not affect the transmission time for the same transmitted packet.
The chip sequences are generated by a given algorithm (which will be covered in a following section) and the Base chip sequence.
Advantages of DSSS - When to Use it?
The chip sequences should be chosen to have weak correlation (that is, they should look different), so that even if the chip sequences are not demodulated perfectly on the receiver, the corresponding bit values can still be detected. This is how we get the processing gain, which, in practice, will improve the sensitivity.
Another advantage of DSSS is immunity against narrowband interferers. This can be explained by the decreased information/frequency factor: even if there are unwanted signals on the occupied bandwidth, the vast majority of the spread spectrum isn't affected by narrowband signal interference.
Also, DSSS can enable the use of relatively inaccurate XOs (crystal oscillators).
Based on the above traits, we can conclude that DSSS is a good fit for PHYs that have relatively low data rates.
How to Configure
The Bitrate field on the radio configurator GUI expects the actual bit rate (as opposed to the chip rate). Note that even when applying DSSS to 4FSK or OQPSK modulation, this field should specify the bitrate (not the symbol rate).
Although the chip rate is automatically calculated by the radio configurator, don't forget to multiply the nominal frequency deviation (specified by the Deviation field) by the DSSS Spreading Factor.
The bit endianness configuration for each part of the packet (preamble, sync word, payload, CRC) is applied on the unencoded bit/bit groups. Therefore, in DSSS encoding cases we say LSCS (Least significant chip sequence) or MSCS (Most significant chip sequence). The bit endianness setting isn't applied on the chip sequences, but on the bits/bit groups of each bytes of the packet before encoding. The chip sequences are always sent out in LSB first order.
The DSSS Chipping Code Base field expects its value in hexadecimal format. A good selection for Base is one where the generated chip sequences are weakly correlated.
Only predefined combinations of the spreading factor and the chipping code length are accepted by the configurator. For valid combinations, visit AN971.
The Preamble Base Pattern field is irrelevant when DSSS is enabled, as the preamble bits are always substituted with the Base chip sequence.
Sync word search is performed on the already decoded bit stream. Hence, the sync detector block won't look for the DSSS signal format, but the originally-encoded bits themselves.
DSSS Chip Sequence Generation Examples
AN971: EFR32 Radio Configurator Guide for RAIL in Simplicity Studio v4 summarizes how DSSS can be configured. However, it is worthwhile to extend that documentation with a review of practical cases - for example to consider configurations with longer chip sequences. We present multiple such cases below - and also include a script with which you can generate all the chip sequences for a given configuration.
How to Exploit PRS Signals to Obtain the DSSS Chip Sequences
In this section, we show how to use the DOUT and DCLK signals to understand how the payload is encoded into DSSS chip sequences.
RAIL Tutorial: Debugging serves as an excellent tutorial for introducing how such PRS signals can be put out on any GPIO pin.
In the examples that follow, the payload is
0x0F0123456789ABCDEF00005555AAAAFF
for all PHYs, just to ensure that all bit-groups show up in the payload. The packet also contains preamble bits, sync word and CRC sequence as detailed in each example.The DOUT signal represents the deviation's sign (in the case of a phase/frequency modulated signal), and therefore the bits of the packet in the 2(G)FSK case. For OQPSK modulation, it's a little bit problematic to retrieve the chip sequences from the DOUT signal. An algorithm will be presented (below) that addresses this problem.
2FSK without DSSS Encoding
First we'll take a look at a built-in config - choose the
868M 2GFSK 38.4Kbps 20K
PHY from theBase Profile
. This config doesn't use DSSS coding, but it is the simplest example to show how to interpret DCLK and DOUT signals:Since the
Frame Bit Endian
field on the configurator GUI is set toLSB_FIRST
, the payload has been sent out in this order. However, the sync word, the preamble and the CRC are represented as we configured it (these were set to their defaults) on the GUI.Our logic analyzer software (Saleae) can visualize the parsed datastream by setting up a SPI decoder on the captured signals. If you take a closer look at the payload (after 0xF68D sync word), you'll see that the payload has been sent out in LSB first order as expected.
2FSK with DSSS Encoding
Now, enable DSSS encoding on this PHY. Set the DSSS Chipping Code Length to 8, DSSS Spreading Factor to 4, and the DSSS Chipping Code Base to 0x4D. Don't forget to multiply the deviation by the spreading factor!
According to AN971, this configuration generates these four chip sequences:
Generate the new configuration and build the application. Once the binary is ready, download it to the target device and transmit a packet while the logic analyzer is waiting for a trigger on the GPIO to which DOUT signal is assigned. Once the DOUT and DCLK signals are captured, export the SPI decoder's result and analyze it.
The first striking thing you'll notice is that, though the transmission time is the same as before, there are many more edges on DCLK. The A* Timing Measurement Pair shows a 4x higher frequency for this configuration compared to the default config. This (4) equals the DSSS spreading factor, so you may then suspect that we can directly capture the chipping sequences on DOUT.
That's exactly the case! Amazingly, using only the transmitter we can capture the chip sequences for a given packet.
Taking a closer look at the parsed DOUT capture, we first see 20 copies of 0xB2. This is the Base chip sequence in LSB first order - just as we expect. According to AN971 the Bit/Chip sequence is 2 for this DSSS configuration, so 20 repeated chip sequences is consistent with the preamble configuration length (40 bit).
Preamble (20 * B2)
Then we expect a 16 bit sync word (0xF68D), which is encoded to 32 chips. Take the next 8 bytes and decode them to bit groups.
Sync Word
How can we explain this behavior? We should first understand how the sync word is represented in the radio's register.
What we set in the configurator GUI is what we would like to see when we transmit the packet w/o DSSS encoding, so it should be represented in the transmission order (from left to right). However, the sync word is always sent out in LSB first order, so the configurator swaps the bits and fits them to the right of the 32-bit wide register. Therefore, if you set up a longer sequence than what you configured as sync word's length, the extra rightmost bits (on the GUI) won't be taken into account.
In the next step the DSSS encoder will fetch up Bit/Chip sequence (2 in this case) long bit groups in each byte and will encode them, keeping the bit groups' endianness.
And as the final step, it will transmit all the chip sequences (per each byte) with the configured byte end bit endianness. This is what we mentioned in the previous chapter, that the bit order settings will applied on the sequences (or on the unencoded bit groups) in each byte, not on the chips in the chip sequences or on only the bits in the payload bytes.
See the steps on the image depicted below:
Take some time to check the same logic on the payload chip sequence below, to completely understand the encoding and transmission order.
Payload
If you aren't lost yet, here is one more twist in the story: the CRC is calculated as if it would had been calculated without DSSS encoding, but it is transmitted in the exact same way as we've seen above in the payload's case. Use this algorithm to calculate what will be sent out as CRC:
CRC (0xBC29)
For your reference, here is a capture where the payload endianness was set to MSB first:
and its CRC sequence (0x3D94 without DSSS):
While developing this example, we couldn't comprehensively demonstrate that the generator sequence is generated by the algorithm above: we couldn't confirm that the chip sequences should be rotated to the right (and not left), and we're still not sure how the rotation and the inversion operation takes place when increasing the bit groups' value one by one.
Let's now look at a more complicated example, where 16 chip sequences are used.
2FSK with 8 Chip Sequences
Set up the following configuration:
In this case there are 16 distinct chip sequences generated, according to the table below:
Let's again compare actual captured chip sequences to decoded source bits. The Bit/Chip Sequence is now 4 and the preamble is configured to 32 bit length. The sync word is configured to 0x904E and we are using the same payload.
Preamble (8 * 0x926F)
Sync Word (0x904E)
Payload
CRC
I encourage you to try and verify the encoding for this (or any other valid config) on your own, to develop a better understanding and get some practice in.
Now, we'll find out what we can do when OQPSK modulation is used.
Investigate DOUT Signal in Case of OQPSK Modulation
It's a bit tricky to get the DSSS chip code base by the transmitter's DOUT signal when using OQPSK modulation; in this case the DOUT signal represents the frequency deviation's sign as well, which won't represent the transmitted bits (or the chip sequences) per se.
Still, we can use the DOUT signal stream if we have an OQPSK signal format. For this task, first gaining some familiarity with how OQPSK modulation works is highly recommended..
The Algorithm
Although the DOUT level does not instantaneously indicate what kind of bit is actively being transmitted, all bits/chips can be calculated using both the present level of DOUT (the actual deviation), and its previous value. Let's denote the bits/chips as
C_n
and the DOUT signal's level asD_n
, wheren
starts from 0. Then
is increased with each rising edge of DCLK.With these notations
C_n
equals toC_n-1 ^ D_n
, ifn % 2 == 0
(so for all even indexed bits/chips) where^
denotes the XOR operation, and!(C_n-1 ^ D_n)
, in case of odd indexes, where!
stands for negation. Forn = 0
,C_n-1
is0
, and the first bit/chip equals the first deviation's sign.OQPSK DSSS Encoding
Similar to the 2FSK case, first we take a look at the payload when the DSSS encoder is disabled.
This is the block diagram for the OQPSK decoder (fed by the first nibble of the preamble):
One can decode the packet using the DOUT signal and the algorithm.
Preamble (
0101
over 40 bit)Sync Word
Payload
CRC
With DSSS Encoding
What if we now apply DSSS on this configuration?
The chip sequences are generated slightly differently than the previous cases where the modulation was not OQPSK. For OQPSK modulation cases, instead of inversion the complex conjugation operation takes place if the bit groups top left bit is
1
. See the chip sequences in the table below.Again, set up the DSSS configuration, hit the
Generate
button, build the example, download, and transmit the same packet while the logic analyzer is listening on the DOUT pin.We won't detail the OQPSK decoding (you can perform this task, as we demonstrated in the previous example), but will share the results here:
Preamble: (20 * 0x7AC9)
Sync Word
Payload
CRC
Introducing the Python Scripts
In this section, we introduce two helpful python scripts that we've developed:
The DSSS Chip Sequence Generator
The DSSS_sequence_generator.py script expects the DSSS configuration parameters:
It first validates the spreading factor/code length combination according to AN971 (Table 3.5.). There is only one function implemented in this script, which prints out the chip sequences in ascending order. This function is called if the script is ran from a terminal like
python3 DSSS_sequence_generator.py
.The chipping codes are printed out in reversed bit order, just as those that are captured on DOUT.
The OQPSK Decoder
The OQPSK_decoder.py expects the DOUT signal in hexadecimal format (actually as a python integer variable).
It then calculates the number of nibbles in the whole sequence. This sequence will be calculated by the algorithm defined above. At the end, it prints out the decoded packets in the given format in
CL
long chunks.Takeaways from this Article
EZRadioPRO - bugfixes for the IEEE 802.15.4g bidirectional example application
EZRadioPRO devices are designed to be compliant with 802.15.4g standard (subGHz FSK) configured via the Wireless Development Suite (WDS) as per the datasheets (Si4467/68, Si4460/1/3/4). However the provided example does not configure the devices perfectly, there are two bugs which should be addressed by the user for now.
We highly recommend to read section 10.10 in AN633: Programming Guide for EZRadioPRO® Si4x6x Devices, before going further.
If you just download the example with the same PHY on two development boards the receiver won't send the ACK packet back as it is expected.
The bit endianness of the payload
The 802.15.4g standard defines the bit order to LSB first everywhere except the length field (within the PHR configured as Field 1) which must be in MSB first order (see our article about the 802.15.4 standard on RAIL).
The example defines the Data bit order in MSB first by default for all payload fields as it is depicted in the following picture.
You should set it to LSB first since the application is responsible for swapping the bits on Field 1 whenever it is needed (where it loads the TX FIFO, or reads out the RX FIFO).
Therefore the PHR parsing will fail expecting more or less payload bytes than the transmitter actually sends (not to mention that it will mix up the whitening and CRC configurations too).
The 4byte CRC seed
Though, as long as the bit endianness configured correctly (and the transmitted/expected Field contents matches) the two radio boards can communicate with each other and the receiver will always send an ACK packet back regardless of the transmitter's configuration (whether it uses data whitening or not and whether the 2 or the 4 bytes long CRC will be transmitted), the devices will not be compliant with the standard: the 4bytes CRC will be miscalculated due to it is using '0' seed, instead of the '1's (see the referred article above).
Unfortunately this bug cannot be solved by using the WDS only, you should modify the generated
radio_config.h
file by hand. According to the API docs thePKT_CRC_CONFIG.CRC_SEED
bitfield is responsible for this configuration. Modify the property's value from 0x57 to 0xD7.Testing the example against an EFR32 development kit
Prepare the EFR32
Build a RAILTest application, load it onto any EFR32 development kit and issue the following commands in this order (see UG409: RAILTest User's Guide for reference on the commands):
Prepare the EZRadioPRO
Load the example config attached to this article (find at the bottom of this page) on any development kit with a revC or revA radio. Note that the
Base frequency
field had been overwritten to 863.25MHz and the 100kbps 2FSK config had been selected.Use EZRadioPRO as transmitter
If you miss to configure the EZRadioPRO to LSB first order, sending a packet will not be received by the EFR32 at all.
Correct the field bit endianness then send a packet by the EZRadioPRO.
The EFR32 will print out the following line on the command line:
{{(rxPacket)}{len:7}{timeUs:xxxxx}{timePos:4}{crc:Pass}{rssi:x}{lqi:xxx}{phy:0}{isAck:False}{syncWordId:0}{antenna:0}{channelHopIdx:254}{ed154:255}{lqi154:255}{payload: 0x08 0xa0 0x02 0x00 0x6a 0xe4 0x79}}
If you enable whitening on the EZRadioPRO only one bit will differ in the received payload:
{{(rxPacket)}{len:7}{timeUs:xxxxx}{timePos:4}{crc:Pass}{rssi:x}{lqi:xxx}{phy:0}{isAck:False}{syncWordId:0}{antenna:0}{channelHopIdx:254}{ed154:255}{lqi154:255}{payload: 0x18 0xa0 0x02 0x00 0x6a 0xe4 0x79}}
Now disable data whitening and check
Transmit Primary CRC (IEEE 32-bit)
checkbox. If you transmit a packet this line will appear on the console:{{(rxPacket)}{len:9}{timeUs:xxxxx}{timePos:4}{crc:Fail}{rssi:x}{lqi:xxx}{phy:0}{isAck:False}{syncWordId:0}{antenna:0}{channelHopIdx:254}{ed154:255}{lqi154:255}{payload: 0x00 0xe0 0x02 0x00 0x6a 0x59 0xb4 0xe4 0xca}}
You might have guessed what will be the result if you check the
Use data whitening
checkbox:{{(rxPacket)}{len:9}{timeUs:xxxxx}{timePos:4}{crc:Fail}{rssi:x}{lqi:xxx}{phy:0}{isAck:False}{syncWordId:0}{antenna:0}{channelHopIdx:254}{ed154:255}{lqi154:255}{payload: 0x10 0xe0 0x02 0x00 0x6a 0x59 0xb4 0xe4 0xca}}
If you overwrite the
PKT_CRC_CONFIG.CRC_SEED
field in theradio_config.h
the 4byte CRC will be calculated correctly and received by the EFR32 as well:{{(rxPacket)}{len:9}{timeUs:xxxxx}{timePos:4}{crc:Pass}{rssi:x}{lqi:xxx}{phy:0}{isAck:False}{syncWordId:0}{antenna:0}{channelHopIdx:254}{ed154:255}{lqi154:255}{payload: 0x10 0xe0 0x02 0x00 0x6a 0xba 0x94 0x5f 0x14}}
Use the EZRadioPRO as receiver
You should configure the same payload on the EFR32 by the following commands to be compliant with the
802.15.4g bidirectional packet
example's default payload:With this payload the 2byte CRC will be applied with no whitening, and the LEDs will light up on the EZRadioPRO development board even if only the bit endianness bug was addressed.
Also, the packet will be received correctly if the whitening is enabled on the EFR32:
If you don't correct the 4bytes CRC calculator's seed this packet won't be received, nor with enabled whitening:
or
Summary
In this article we introduced two bugs found in the
802.15.4g bidirectional packet
example and the workaround on how to deal with them. Also a minimal test guide had been introduced in order to show how can we validate our CRC, whitening and payload configuration for the 802.15.4g standard.RAIL Tutorial 4 (studio5): Combining Transmit and Receive
This tutorial builds on the following tutorials:
Please read them first if you haven't.
You can find other tutorials on the table of contents site.
On the previous tutorial, we had both transmit and receive working, but we couldn't see what we received. This tutorial will solve that problem.
We're also adding calibrations, which is recommended for all projects.
You can find a SimpleTRX example in Simplicity Studio, which has a similar purpose. However, the result of this tutorial will be somewhat different. Mainly, this tutorial is not very pedantic on checking errors returned by all functions, to make it easier to understand. However, we highly recommend checking all possible errors, therefore using the SimpleTRX example in Studio as a reference.
Enabling Calibrations
First, let's load PA factory calibrations: Open the configuration for the component RAIL Utility, Power Amplifier, and turn on the Enable PA Calibration switch.
All other calibrations are enabled by default in RAIL Utility, Initialization, but require some application code to perform them. First, we need to add the related event during initialization to enable it (in app_init.c):
Next, we need to perform calibrations when RAIL requests them (in app_process.c):
This is all that's needed from an application to perform all calibrations available on EFR32, to have the maximum available performance. Calibrations are detailed further in another tutorial.
Preparing for printing
In the previous tutorials, we already enabled LEDs and buttons, but we'll need a more user friendly output: We need a serial console. To enable that, install the following components:
#include "printf.h"
to use it, notstdio.h
!Finally, we need to enable Virtual COM UART on the Board Control component:
This pulls up a GPIO, which will enable the USB-UART bridge on the WSTK.
Receive FIFO
We already configured Tx FIFO multiple ways. RAIL also implements an Rx FIFO. By default, it's 512B, allocated by the RAIL library. We will use that, but it is configurable. See
RAILCb_SetupRxFifo()
andRAIL_SetRxFifo()
in the API documentation for details.By default, you cannot use the whole 512B, as RAIL adds a few bytes to each packet to store timestamp, RSSI, and similar information in the FIFO (sometimes we call this "appended info").
We're working on what we call packet mode. It's the simpler, and recommended way, but it's not possible to handle packets bigger than 512B with this method, in which case FIFO mode should be used. We return to that topic in a later tutorial.
The Packet Handle
The FIFO is accessible with
RAIL_RxPacketHandle_t
variables or packet handles. Obviously, we don't have the handle of the packet we just received, but we have "sentinel" handles:RAIL_RX_PACKET_HANDLE_NEWEST
RAIL_RX_PACKET_HANDLE_OLDEST
RAIL_RX_PACKET_HANDLE_OLDEST_COMPLETE
RAIL_RX_PACKET_HANDLE_INVALID
Note that
RAIL_RX_PACKET_HANDLE_OLDEST
andRAIL_RX_PACKET_HANDLE_NEWEST
will always return with packet information. For example, OLDEST will return with one of the following:If you don't care about upcoming or ongoing packets, you can use
RAIL_RX_PACKET_HANDLE_OLDEST_COMPLETE
: It will only return with fully received packets.When to Read the FIFO
Let's say we want to download the packet when it's fully received (although it's possible to access it during reception). We must let RAIL know this in the event
RAIL_EVENT_RX_PACKET_RECEIVED
. If we don't, RAIL automatically frees the memory allocated to the packet we just received.We have two options:
While the first one is simpler, it is desirable in most cases to keep large memory copy operations outside of interrupt handlers, therefore the attached example demonstrates the second method.
The Event Handler
In the event handler, we want to instruct RAIL to hold all successfully received frames:
The API
RAIL_HoldRxPacket
will also return a packet handler, but we ignore that since we're going to use sentinel handles. Keep in mind that you must release all packets that were held, otherwise, you will lose part of your RX fifo, essentially leaking memory.Download and Print
First, let's create a buffer we can use to download the packet:
In
app_process_action()
, if there's something in the handle, let's download and print it:Let's go through this, line by line:
First,
RAIL_GetRxPacketInfo()
will return with a validpacket_handle
if we have a completely received frame. In that case, it also returns a usablepacket_info
which has the length of the received packet, and pointers to download it. Note that the length will include the full frame, and is always valid, whichever length configuration is used (fixed or variable).Since the receive buffer is a ring buffer, the packet in the buffer might not be in a single sequential buffer, copying from that using
memcpy()
is a bit complicated:RAIL_CopyRxPacket()
inline function implements that.The API
RAIL_GetRxPacketDetails()
will return some useful information of the packet, such as RSSI or timestamps.Finally, we release the packet, using
RAIL_ReleaseRxPacket()
.Note that this method is safe, even if we receive a new packet while downloading one: That packet will be downloaded and printed when
app_process_action()
is called the next time.Download in the Event Handler
Downloading the packet in the event handler is essentially the same as what we did in the main loop above, except we don't need to worry about releasing:
Note that
packet_details
should be a global variable as well in this case.Conclusion
With this, you can use RAIL for basic transmit and receive, which concludes the getting started series. However, RAIL can do much more - you can continue with the basic tutorials from the table of contents site.
API Introduced in this Tutorial
Functions
Types and enums
RAIL Tutorial 3 (studio5): Event Handling
This tutorial builds on the following tutorials:
Please read them first if you haven't.
You can find other tutorials on the Table of Contents site.
In the previous tutorials, we set up RAIL to do various tasks through API commands. However, RAIL can inform the application on various events almost real time (basically, wiring interrupts through RAIL). So let's improve what we had in the previous tutorial.
The Event Handler Function
You might have already seen this function in
app_process.c
:This is the event handler function, which is called by the RAIL library, and routed through the RAIL Utility, Initialization component.
The
events
variable is a bit field, holding all possible events. Be careful, this bit field does not fit into a 32-bit variable, which is the size of theint
on our MCUs.The event handler should be prepared for multiple events in a single call. There's no "clearFlag" mechanism, each event will be signaled once, and the application is responsible for handling it.
Keep in mind that almost all events are generated from interrupts, so the event handler is usually running from interrupt context (i.e., interrupts are disabled). Therefore, the event handler should run fast; if you have to do something that takes more time, set a flag, and do it from the main loop.
You should be also careful with RAIL interrupt safety. Calling state changing APIs (
RAIL_Start*
,RAIL_Stop*
orRAIL_Idle
) from the interrupt handler is allowed, but we recommend to read the interrupt safety article before doing so.Configuring Events
The events enabled by default are configured on the RAIL Utility, Initialization component (under 'Platform/Radio', press 'Configure'):
However, using the RAIL API directly is actually simpler than using the event configuration capabilities of this component. The API for this is
RAIL_ConfigEvents()
. You can keep the event setup enabled, it doesn't really matter.RAIL_ConfigEvents()
has amask
and anevents
parameter, making it simple to configure multiple events at the same time; e.g., to enable or disable a single event, leaving the others untouched:RAIL also defines a few event groups (defined in rail_types.h, documented in events), the most useful are:
RAIL_EVENTS_ALL
includes all the eventsRAIL_EVENTS_NONE
includes no events (equivalent to 0)RAIL_EVENTS_TX_COMPLETION
includes all possible events after a successfulRAIL_StartTx
RAIL_EVENTS_RX_COMPLETION
includes all possible events that can result in state transition from Rx modeThis can be used, for example, to disable or enable all events:
If you enable/disable events in group, it's a good practice to create similar group of events in your application.
RAIL has no API to flush pending events, but disabling and enabling them will have that result:
However, you only need to do this in rare cases. Enabling events is not like unmasking interrupts: Enabling them will not trigger the event handler on past events.
Tx Error Handling
In the last tutorial, we sent out a frame, but we didn't check the results because it would have needed the event handler. Let's turn on led0 on success and led1 on any error (we'll be turning led1 on when transmitting and clearing led1 if no Tx error occurs).
First, for the LEDs, enable the Simple LED component with led0 and led1.
In app_process.c (to get access to the led instances, now declared in autogen/sl_simple_led_instances.h):
For the actual radio related changes, we'll need to enable the success and error events. We can do that during init since we don't really need to disable anything at runtime for such simple applications:
Next, let's check for errors returned by the StartTx call:
Finally, let's check the events for success and errors:
With this, you should see led0 toggling on every transmitted packet.
Receiving Packets
Let's add receive capabilities to this application! Again, use led0 to report success and led1 for errors.
First, we need to modify the init code.
In app_init.c (to get access to the led instances):
We need RX success/error events enabled, and we need to start the radio in RX mode:
The API
RAIL_StartRx()
is similar to Tx: the only important option is the second one, which is the channel.Finally, let's check the events for success and errors (in app_process.c):
Auto state transitions
By default, after receiving or transmitting a packet, RAIL will switch to idle mode (i.e., turn off the radio). Since we want it to keep receiving future packets, we can use RAIL's auto state transition feature. This can be configured on the RAIL Utility, Initialization component, let's set all dropdowns to RX:
We're basically saying that after the packets are received/transmitted, we want to return to Rx. These configurations can be also changed in run-time, using the
RAIL_SetRxTransitions()
andRAIL_SetTxTransitions()
APIs.If you compile and flash the program after this modification, you should see that now both WSTK toggles led0 on every button press.
Testing and Conclusion
If you install this example to two boards (make sure you attach the antennas if you use sub-GHz kits on sub-GHz config), you will see that on the press of btn0 on either device, led0 on both devices toggle. However, we can't see what we're receiving, since we never actually download the messages.
We're going to fix this Next time
API Introduced in this Tutorial
Functions
Types and enums
RAIL Tutorial 2 (studio5): Transmitting a Packet
This tutorial builds on the following tutorial/s:
Please read them first if you haven't.
You can find other tutorials on the Table of Contents site.
In this tutorial, we're going to modify the project Flex (RAIL) - Empty Example to transmit a packet. We're going to use the default, fixed length 16Byte 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.
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:
Note that you can't use any arbitrary size; EFR32 creates a FIFO ring buffer on this memory area, and it can only handle buffer sizes of power of 2 between 64 and 4096 (i.e., 64, 128, 256, 512, 1024, 2048, or 4096).
The tx buffer can hold more than one packets, 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.
Writing to the Buffer Directly
In app_process.c:
The
memcpy()
is just a standard C instruction to write to buffers, and we useRAIL_SetTxFifo()
to pass that buffer to RAIL. We also tell RAIL how long the buffer is, and how much data it has already in it.Writing to the Buffer Indirectly
In
app_init.c
:In
app_process.c
: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 WriteTxFifo 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:
RAIL_SetTxFifo()
does not move any memory, just sets a few register, while callingRAIL_WriteTxFifo()
does need some time to copy the payload to the buffer.RAIL_SetTxFifo()
is not allowed during transmission.For simpler applications,
RAIL_SetTxFifo()
can be simpler. If you want to send the same packet over and over (or only change a few bytes in it), it's much better, since you don't have to write the whole packet again.On the other hand, the separation in RAIL examples between app_init and app_process works very nicely with the indirect method, and in most real cases, you need to move memory anyway. If you want to send longer packets than your buffer, you must use
RAIL_WriteTxFifo()
, as you can call it during transmit. It's also useful if you want to send out a lot of packets: UseRAIL_WriteTxFifo()
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
RAIL_WriteTxFifo
can reset the FIFO, which means it will invalidate the data already in there. This is also possible withRAIL_ResetFifo
. We generally recommend not to use it, a well-written code shouldn't need it (as it should only store in the FIFO what should be sent out), and it might mask bugs. Resetting the FIFO also takes extra time.Transmitting the Packet
Starting the transmission is very simple:
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 RAIL API reference). 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
RAIL_SetFixedLength()
, 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 useRAIL_SetFixedLength(railHandle, 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:
For more information on this setup, and on more advanced variable length configs, see AN1253. 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 16B frame, both for the above described variable length and 16 Byte fixed length mode, so this is used in the attached sample code:
We use
PAYLOAD_LENGTH-1
in the length field, since the 1B length field itself shouldn't be counted.The API
RAIL_SetFixedLength()
is available in variable length mode, and it changes the radio to fixed length operation (again, both for rx and tx). CallingRAIL_SetFixedLength(railHandle, RAIL_SETFIXEDLENGTH_INVALID)
will restore it to variable length mode.Setting up the example
Button handling
The attached 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):
Also add the button callback code (the callback is already declared in sl_button.h):
Since this callback is in interrupt context, we avoid using RAIL API directly. It is safe in almost all cases, but calling
RAIL_StartTx()
from interrupt context should be done carefully. Instead, we set the volatilesend_packet
variable to true, and call RAIL APIs fromapp_process_action()
only:To simply set this up:
Conclusion
We didn't care about possible errors generated by RAIL: we do that next time, which also provides hints for receiving packets.
API Introduced in this Tutorial
Functions
Types and enums
RAIL Tutorial 1 (studio5): Introduction, RAIL components and the Empty Example
What is RAIL?
RAIL is short for Radio Abstraction Interface Layer, and is the most direct interface available for EFR32 radios. You can think of it as a radio driver. While this sounds like a restriction, this actually makes software development for proprietary wireless much simpler:
See this KBA for more details.
RAIL application development is currently possible in the Flex SDK in Simplicity Studio. To progress further with this tutorial, please remember to open the RAIL API documentation.
When to Use RAIL?
If you have an existing protocol, and you must be compatible with it, you'd probably have to use RAIL (as all of our stacks use a standardized frame format).
If you want to use sub-GHz communication, you have a few options, including RAIL. For simple point to point communication, RAIL might be the simplest solution. However, as soon as you need security or addressing, it might be better choose a protocol stack, such as Connect.
RAIL also has the benefit that it adds the least amount of delays to all radio events: In fact, some event notification will happen in interrupt context.
What is covered by RAIL?
RAIL only supports what the radio hardware supports. For example, auto ACK and address filter is available, as the hardware provides support for it. On the other hand, while security is important for wireless communication, it's not really a radio task, hence RAIL does not support it. The crypto engine is an independent peripheral that can be used to encrypt payloads before giving them to RAIL and the radio hardware.
Supported Energy Modes
EM1p or higher is required for the radio to be operational (i.e., Transmitting, receiving, or waiting for packets, RAIL timer running from HF clock). On devices without EM1p support, EM1 is required for the radio. See AN1244: EFR32 Migration Guide for Proprietary Applications for more details on EM1p.
EM2 or higher is required for RAIL scheduling or timers (Running from LF clock, which needs configuration, a topic in Tutorial 5).
EM3 or higher is required for RAIL to work without re-initialization.
Writing Code from Scratch
We do not recommend writing code from scratch because it is much simpler to have something existing that sets up the include paths and linker settings correctly. The best way to start is the example Flex (RAIL) - Empty Example.
RAIL Components
The empty application includes the following RAIL related components (under 'Platform/Radio'):
There are a few components that you might want to install
If you work on a starter kit, you don't need to change the configuration of most of these components (for custom hardware, there will be a new article coming soon), except the Initialization component. However, the initialization component also includes a good starting point, that we don't need to modify for simple applications.
The Radio Configurator
The Radio Configurator is accessible by configuring the component Advanced Configurators/Radio Configurator. The usage of the radio configurator is out of scope for this tutorial series. For more details, see AN1253: EFR32 Radio Configurator Guide.
The project structure
Projects always include the following parts:
rail_config.c
), init code, the linker script, and other generated code used by components, like the command descriptors for the CLI interface.slcp
file) and the Pin tool (.pintool
file)Note that all files related to the project should be visible in the project explorer, including header and library files.
All projects include
main.c
, which is not recommended to modify. Instead, add the initialization code toapp_init.c
, and implement the main loop inapp_process.c
. This way, the System components can initialize components, and call the "process" function of the components that requires this. Additionally, enabling an RTOS will convert app_process's main loop into an RTOS task.Conclusion
This project is ready to send out frames. We're going to do that in the next part. You can also find other tutorials from the Table of Content site.
Enhanced filtering of EFR32 Series 1-based designs for XO reference spur suppression
The crystal or TCXO reference spur is located +/- XO frequency (typ. 38.4 MHz for EFR32 Series 1-based designs) from the RF carrier frequency. This KBA is focusing on the possible HW modifications that can be suggested to enhance the suppression of these XO reference spurs around the carrier frequency. Also, the KBA is based on the measurement results of EFR32 Series 1-based radio boards at the 868/915 MHz frequency bands.
An evident approach could be to apply a high-Q band-pass filter, e.g. SAW filter, in the RF path to filter out the XO reference spurs. This will ensure very low level of spurs in a reliable way, but on the other it will also increase the insertion loss at the fundamental frequency while it is not a desirable solution for cost sensitive applications.
The XO coupling is basically occurred through two main domains within the part: VCO and PA. These blocks are supplied through RFVDD and PAVDD and thus forming two coupling paths. The power supplies can be modulated either by VDD and/or GND. Leakage current paths outside the chip depend on board design through impedance at HFXO frequency between supply domains.
So, a filtering scheme focused for the XO frequency on both RFVDD and PAVDD nets can help suppress these spurs effectively. The table below shows some measurement data with different RFVDD and PAVDD filtering configurations, captured on BRD4164A Rev A02 radio board at 915 MHz / +20 dBm.
* Note: close to the original BOM of reference radio boards.
Some good performer configurations in terms of spur suppression are highlighted in yellow above.
The RFVDD is filtered at the RFVDD pin, while the subG PAVDD is filtered at the PAVDD pin and at the BIAS pin of the external ceramic balun.
Some conclusions:
Further layout considerations: