Getting Started with the SPIRIT1 Transceiver

Introduction

The SPIRIT1 Sub-GHz transceiver from STMicroelectronics is a low-power, cost-effective option for many wireless applications; such as wireless sensor networks, home automation, meter reading, etc. It is designed to operate in a wide range of frequency bands below 1 GHz and support the 2-FSK, GFSK, MSK, GMSK, OOK, and ASK modulation schemes. It consumes very little power during both reception (about 9 mA) and transmission (about 21 mA at +11 dBm). In its lowest power mode where the FIFO and register contents are retained, it consumes only 600 nA. The SPIRIT1 boasts several other features to further reduce overall system costs; including an AES 128-bit encryption co-processor, automatic CRC handling, embedded CSMA/CS protocol, etc.

RF modules and evaluation platforms based on the SPIRIT1 transceiver are available to simplify the development and production of products requiring wireless connectivity. The SPSGRF modules include a SPIRIT1 transceiver, chip antenna, filter and balun; all in a small, easy to use form factor. The module is available for either the 868 or 915 MHz frequency bands. All of the SPIRIT1 digital interface pins (GPIO pins, SPI pins, and shutdown pin) are broken out while the analog circuitry (power, crystal, and antenna connections) are routed to the appropriate components.

In the following tutorial, an application will be created utilizing two SPSGRF modules to establish a point-to-point communication network. The first step is to create an STM32Cube project and initialize the MCU peripherals to interact with the module. Then, the X-CUBE-SUBG1 driver will be added to the project, exposing the rich API to the developer. Using this API, the module will be initialized and communication established. Finally, a packet containing a simple message will be transmitted by one node and received by the other.

Required Materials

To follow this tutorial’s instructions verbatim, the hardware and software items listed below should be utilized. However, substitutions such as a Nucleo board with a different targeted STM32, an expansion board based on a different SPIRIT1 module, or the use of a third-party IDE may be made with minor alterations to the below procedure.

1. Create an STM32CubeIDE Project

STM32CubeIDE is used for this tutorial because it is an all-in-one tool providing peripheral configuration, code generation, code compilation, and debugging features for the STM32 line. Note that if another environment is preferred (e.g. Keil, IAR, Atollic), the STM32CubeMX configurator can be installed as a stand-alone tool and used to export the project to a third-party toolchain/IDE. In either case, the same graphical configurator will be used to initialize the required peripherals in a simple and portable fashion. Should the reader be utilizing a hardware configuration different from that outlined above, they can easily create a custom configuration using the following steps as a reference.

1.1 Start a New STM32CubeIDE Project

The first step is to create a new project for the target hardware. Recall that this tutorial utilizes the NUCLEO-L152RE evaluation board.

a. Open STM32CubeIDE and select (or create) your desired workspace. Then, choose File > New > STM32 Project.

b. The Target Selection window should appear. Use either the MCU/MPU Selector or Board Selector tabs to select the desired STM32 target. Figure 1 shows the Board Selector tab option with the MCU/MPU Series filter on the left-hand side utilized to only show boards based on the STM32L1 series. The NUCLEO-L152RE board can then easily be found in the boards list. Select the board and click Next.


Figure 1: Selecting a target STM32 device for the project.

c. Supply a name for the project (for example, “spirit1_helloWorld” as shown in Figure 2). Click Finish. A window should pop up with the prompt “Initialize all peripherals with their default Mode?”. Choose Yes.


Figure 2: Setup options for the STM32CubeIDE project.

1.2 Configure the Target MCU

After adding the new project to the workspace, the STM32CubeMX configuration (.ioc) file should be automatically opened in the Device Configuration Tool perspective. At this point, the target MCU can easily be configured to interface with the X-NUCLEO-IDS01A5 shield.

a. Start by clicking on pin PB3 in the Pinout View and choosing Reset_State from the list (Steps 1-3 in Figure 3).

b. Next, in the component list on the left-hand side of the window, choose the SPI1 peripheral and change the Mode option from Disable to Full-Duplex Master using the drop-down. Then, in the Configuration panel, change the Prescaler parameter to 4. This is demonstrated in Steps 4-6 in Figure 3.


Figure 3: Using the device configurator to enable the SPI peripheral

c. In the Pinout View, configure pins PB6 and PA10 as GPIO outputs by clicking on them and choosing GPIO_Output from the list. Configure pin PC7 as an external interrupt by clicking on it and choosing GPIO_EXTI7. The result of doing so is shown in Figure 4.


Figure 4: Enabling the remaining GPIO pins used to interface with the SPIRIT1

d. Choose Pins/Signals Options… from the Pinout menu (Figure 5). In the table that pops up, click on the empty boxes in the left-hand column to ensure every signal is pinned. Also, provide User Labels to the GPIO pins as outlined in Table 1.


Figure 5: Opening the Pins/Signals Options… window

Table 1: GPIO pin user labels for the X-NUCLEO-IDS01A5 shield

PIN User Label
PB6 SPIRIT1_SPI_CSn
PA10 SPIRIT1_SDN
PC7 SPIRIT1_GPIO3

The configuration should appear as shown in Figure 6. Click OK.

1_userLabels
Figure 6: Configuration of every enabled pin on the target device

e. In the component list, select the GPIO peripheral. Then, under the GPIO tab in the configuration panel, select the row in the table corresponding the pin PC7. Change the GPIO mode to External Interrupt Mode with Falling edge trigger detection using the drop-down. See Figure 7 as a guide.


Figure 7: Configuring the external interrupt to trigger on a falling edge

f. In the component list, select the NVIC peripheral. Then, under the NVIC tab in the configuration panel, check the box to enable the EXTI line[9:5] interrupts as shown in Figure 8.


Figure 8:: Enabling the external interrupt line in the Nested Vectored Interrupt Controller (NVIC)

g. In the Project Manager view, choose the Code Generator tab and check the box next to Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral (Figure 9).


Figure 9: Changing the code configuration settings to generate a pair of .c/.h files for each peripheral.

h. Finally, save the .ioc file to generate the code for the project.

2. Add the SPIRIT1 Driver

The basic procedure for adding the SPIRIT1 driver to the project is 1) copy or link the files into the project, 2) create definitions for the functions declared in MCU_Interface.h, and 3) add the new files to the project include paths. The instructions that follow are just one implementation of these three basic steps. They were designed to be simple, understandable, and quick to perform. However, they can absolutely be modified based on one’s end-application requirements or personal preference.

a. In the Project Explorer, right-click on the Drivers directory in the current project and choose Import… as shown in Figure 10.


Figure 10: Opening the import wizard selector to add resources to the Drivers directory

b. Choose the File System import wizard under the General category (see Figure 11). Click Next.


Figure 11: Selecting the File System import wizard

c. Populate the From Directory: field with the location of the extracted X-CUBE-SUBG1 driver. As shown in Figure 12, the path should resemble the following:

<extraction_location>/en.x-cube-subg1_firmware/STM32CubeExpansion_SUBG1_V3.3.0/Drivers 

In the file explorer window, navigate to the SPIRIT1_Library directory and check the corresponding box. Click Finish.


Figure 12: Copy the SPIRIT1 library files to the Drivers directory while maintaining the source directory structure (i.e. create the parent directories)

d. Use the Project Explorer to navigate to and open the file Drivers/BSP/Components/spirit1/SPIRIT1_Library/Inc/MCU_Interface.h. Make the following corrections to lines 115 through 117:

void RadioEnterShutdown(void); //void EnterShutdown(void);
void RadioExitShutdown(void); //void ExitShutdown(void);
SpiritFlagStatus RadioCheckShutdown(void); //SpiritFlagStatus CheckShutdown(void);

e. In the Drivers/BSP/Components/spirit1 directory, create two new files called radio_target.h and radio_target.c. These files will contain the implementations of the functions declared in the MCU_Interface.h file. For a working example, copy the contents of these radio_target.h and radio_target.c files into ones just created.

NOTE: Again, you could choose to structure your implementation differently than I am. The files could go elsewhere in the project, be named differently, be broken up into several files, etc. This is just a simple example to help you get started.

f. Finally, the driver files must be added to the project include paths. In the Project Explorer, simply right-click on the <project_name>/Drivers/BSP/Components/spirit1/SPIRIT1_Library/Inc folder and choose Add/remove include path… as shown in Figure 13. In the pop-up window, make sure both the Debug and Release configurations are checked and click OK. Repeat the process for the <project_name>/Drivers/BSP/Components/spirit1 folder (because it contains radio_target.h).


Figure 13: Adding the SPIRIT1 library to the project include paths

Now, to utilize any of the SPIRIT1 API functions in their code, the user can simply include the header file SPIRIT_Config.h in their source file.

3. Initialize the SPIRIT1

With the driver added to the project, the SPIRIT1 API can be used to initialize the transceiver. Because this example utilizes the SPSGRF-915 module, all initialization code is located in two files: spsgrf.h and spsgrf.c. All of the constants encountered in this section are defined in spsgrf.h, several of the values for which (e.g. XTAL_FREQUENCY) are specified in the SPSGRF datasheet. Needless to say, these files should be modified appropriately if different hardware components are used.

Section 1.23 of the SPIRIT1 Library User Manual outlines the steps required to configure a SPIRIT1 device. These steps, along with a few others gleaned from example applications, are explained below.

3.1 Reset the Transceiver

To begin from a known state, the SPIRIT1 should be reset prior to beginning the initialization process. This is easily done by toggling the shutdown pin (SDN). When this pin is driven logic high, the SPIRIT1 is completely shutdown and the contents of the registers are lost. The SpiritEnterShutdown() function (the definition of which was provided in the radio_target.c file in Step 2d) accomplishes this. Conversely, SpiritExitShutdown() function will deassert the pin and place the SPIRIT1 into the READY state. This code is shown below in Listing 1. Note also the use of the SpiritManagementWaExtraCurrent() function which addresses the device limitation described in section 1.2 of the errata.

Listing 1: Reset the SPIRIT1 by entering and exiting the SHUTDOWN state and then wait for it to enter the READY state

// restart the radio
SpiritEnterShutdown();
SpiritExitShutdown();
SpiritManagementWaExtraCurrent(); // To be called at the SHUTDOWN exit. It avoids extra current consumption at SLEEP and STANDBY.

// wait for the radio to enter the ready state
do
{
  for (volatile uint8_t i = 0; i != 0xFF; i++); // delay for state transition
  SpiritRefreshStatus(); // reads the MC_STATUS register
} while (g_xStatus.MC_STATE != MC_STATE_READY);

3.2 Radio Configuration

Once the module is reset, the first step is to configure the fundamental radio parameters such as the base frequency, datarate, modulation, etc. To do this, simply declare an SRadioInit structure and fit it with the desired configuration parameters. Then, pass a pointer to the structure as an argument to the SpiritRadioInit() function as shown below in Listing 2. Note, however, that the SpiritRadioSetXtalFrequency() function must be called first to achieve the expected configuration.

At this point, we can configure the transmission power as well. The SPIRIT1 contains a bank of 8 registers in which different power levels can be stored. However, unless ASK modulation is used, only one power level is required to be set. First, the SpiritRadioSetPALeveldBm() function is used to specify the desired power level at a particular register in the bank. Then, the SpiritRadioSetPALevelMaxIndex() function is called to tell the transceiver from which register to retrieve the power level.

Listing 2: Configure the radio RF parameters and set the desired power level

SRadioInit xRadioInit;

// Initialize radio RF parameters
xRadioInit.nXtalOffsetPpm = XTAL_OFFSET_PPM;
xRadioInit.lFrequencyBase = BASE_FREQUENCY;
xRadioInit.nChannelSpace = CHANNEL_SPACE;
xRadioInit.cChannelNumber = CHANNEL_NUMBER;
xRadioInit.xModulationSelect = MODULATION_SELECT;
xRadioInit.lDatarate = DATARATE;
xRadioInit.lFreqDev = FREQ_DEVIATION;
xRadioInit.lBandwidth = BANDWIDTH;
SpiritRadioSetXtalFrequency(XTAL_FREQUENCY); // Must be called before SpiritRadioInit()
SpiritRadioInit(&xRadioInit);

// Set the transmitter power level
SpiritRadioSetPALeveldBm(POWER_INDEX, POWER_DBM);
SpiritRadioSetPALevelMaxIndex(POWER_INDEX);

3.3 Packet Configuration

The SPIRIT1 provides three configurable packet structures to cover a wide range of applications. These are the Basic, STack, and WM-BUS packet formats. For details about these structures, their differences, and their available configuration options; please consult section 9.7 of the SPIRIT1 datasheet. As a quick reference, see Figures 14-16 below.


Figure 14: Basic packet structure


Figure 15: STack packet structure


Figure 16: WM-BUS packet structure

The choice of which format to use is made by declaring and fitting the corresponding API structures and passing them as arguments to the proper initialization functions. The names of these structures and functions for each of the three packet formats are provided in Table 2 below.

Table 2: The initialization structures and initialization functions that must be used for each of the three packet formats.

Packet Type Initialization Structures Initialization Functions
Basic
  • PktBasicInit
  • PktBasicAddressInit
  • SpiritPktBasicInit()
  • SpiritPktBasicAddressInit()
STack
  • PktStackInit
  • PktStackAddressInit
  • PktStackLlpInit
  • SpiritPktStackInit()
  • SpiritPktStackAddressesInit()
  • SpiritPktStackLlpInit()
WM-BUS
  • PktMbusInit
  • SpiritPktMbusInit()

The code example below configures the device to utilize the basic packet format. It assigns an address to the device (i.e. “MY_ADDRESS”) as well as multicast and broadcast addresses. Packet filtering is enabled for each address, meaning if a packet is received and its destination address does not match either the my address, multicast address, or broadcast address values; the packet will be discarded.

Listing 3: Configure the SPIRIT1 to use the Basic packet format by calling the SpiritPktBasicInit() and SpiritPktBasicAddressInit() functions.

PktBasicInit xBasicInit;
PktBasicAddressesInit xBasicAddress;
  
// Configure packet handler to use the Basic packet format
xBasicInit.xPreambleLength = PREAMBLE_LENGTH;
xBasicInit.xSyncLength = SYNC_LENGTH;
xBasicInit.lSyncWords = SYNC_WORD;
xBasicInit.xFixVarLength = LENGTH_TYPE;
xBasicInit.cPktLengthWidth = LENGTH_WIDTH;
xBasicInit.xCrcMode = CRC_MODE;
xBasicInit.xControlLength = CONTROL_LENGTH;
xBasicInit.xAddressField = EN_ADDRESS;
xBasicInit.xFec = EN_FEC;
xBasicInit.xDataWhitening = EN_WHITENING;
SpiritPktBasicInit(&xBasicInit);

// Configure destination address criteria for automatic packet filtering
xBasicAddress.xFilterOnMyAddress = EN_FILT_MY_ADDRESS;
xBasicAddress.cMyAddress = MY_ADDRESS;
xBasicAddress.xFilterOnMulticastAddress = EN_FILT_MULTICAST_ADDRESS;
xBasicAddress.cMulticastAddress = MULTICAST_ADDRESS;
xBasicAddress.xFilterOnBroadcastAddress = EN_FILT_BROADCAST_ADDRESS;
xBasicAddress.cBroadcastAddress = BROADCAST_ADDRESS;
SpiritPktBasicAddressesInit(&xBasicAddress);

3.4 GPIO and IRQ Configuration

The SPIRIT1 includes 4 GPIO pins, each of which can be configured as one of several functions. These include interrupt request, low battery detection, and wake-up timer expiration signals. Each pin can also be set to logic high or low, allowing it to emulate an additional GPIO of the MCU. For a complete list of the available functions, see Tables 37 and 38 in the SPIRIT1 datasheet. In the code example in Listing 4, GPIO3 is configured as an interrupt request signal by fitting the SGpioInit structure and passing it to the SpiritGpioInit() function.

There are many events capable of generating an interrupt request. For a complete list, see Table 36 in the SPIRIT1 datasheet. In Listing 4, the TX data sent, RX data ready, RX data discarded, and the RX operation timeout events are enabled. Now, if any of these events occur, the signal on the SPIRIT1 GPIO3 pin will switch from logic high to logic low. Note that pin PC7 on the MCU was configured to trigger an external interrupt when this occurs (Section 1.2 above). See Listing 10 in the next section for an example of handling these events.

Listing 4: Configure GPIO3 to send an interrupt signal when a TX/RX operation is completed, RX data was filtered and discarded, and/or the RX operation timed out.

SGpioInit xGpioInit;

// Configure GPIO3 as interrupt request pin (active low)
xGpioInit.xSpiritGpioPin = SPIRIT_GPIO_3;
xGpioInit.xSpiritGpioMode = SPIRIT_GPIO_MODE_DIGITAL_OUTPUT_LP;
xGpioInit.xSpiritGpioIO = SPIRIT_GPIO_DIG_OUT_IRQ;
SpiritGpioInit(&xGpioInit);

// Generate an interrupt request for the following IRQs
SpiritIrqDeInit(NULL);
SpiritIrq(TX_DATA_SENT, S_ENABLE);
SpiritIrq(RX_DATA_READY, S_ENABLE);
SpiritIrq(RX_DATA_DISC, S_ENABLE);
SpiritIrq(RX_TIMEOUT, S_ENABLE);
SpiritIrqClearStatus();

3.5 Receiver Quality Indicators Configuration

During and after reception of a signal, the SPIRIT1 relies on three quality indicators: Received Signal Strength Indicator (RSSI), Preamble Quality Indicator (PQI), and Synchronization Quality Indicator (SQI). The PQI and SQI values both correspond to the ‘correctness’ of the received preamble and synchronization bytes, respectively (see Figures 14-16). The PQI check and/or SQI check can be enabled to abort packet demodulation if the indicator(s) fall below a set threshold. Because section 9.10.4 of the SPIRIT1 datasheet states “It is recommended to always enable the SQI check”, the example code in Listing 5 does exactly that. By setting the SQI threshold to 0, a perfect match is required between the expected synchronization byte(s) and the received synchronization byte(s).

The RSSI measurement enables the Carrier Sense (CS) functionality to detect if a signal is being received. The CS signal is asserted when the RSSI value is above a predefined threshold (-112 dBm by default) and is used to initiate signal demodulation, can be used to stop the RX timer, and is used for the CSMA algorithm. The SpiritQiSetRssiThresholddBM() function can be used to raise or lower this threshold, as shown in Listing 5.

Listing 5: Set the RSSI/SQI thresholds and enable the SQI check to abort packet demodulation if the received synchronization field is corrupted.

// Enable the synchronization quality indicator check (perfect match required)
// NOTE: 9.10.4: "It is recommended to always enable the SQI check."
SpiritQiSetSqiThreshold(SQI_TH_0);
SpiritQiSqiCheck(S_ENABLE);

// Set the RSSI Threshold for Carrier Sense (9.10.2)
// NOTE: CS_MODE = 0 at reset
SpiritQiSetRssiThresholddBm(RSSI_THRESHOLD);

3.6 Timer Configuration

To minimize the energy consumed by the SPIRIT1 transceiver, a timer can be configured to automatically abort reception after a timeout expires. This timeout duration is configurable via the SpiritTimerSetRxTimoutMs() function . Alternatively, the SET_INFINITE_RX_TIMEOUT() macro can be used to disable the timeout, in which case reception is stopped either when a packet is received or the SABORT command strobe is issued. In Listing 6, the timeout is either set to 2 seconds or disabled entirely, depending on whether RECEIVE_TIMEOUT has been defined by the user.

To prevent the timeout from occurring during the reception of a valid packet, the RX timer can be stopped on the condition that one or more of the receiver quality indicators (see above) exceeds their corresponding thresholds. In Listing 6, the RX timer is stopped if the SQI exceeds the threshold defined above in Listing 5.

Listing 6: Conditionally set an RX timout of 2 seconds or infinity as well as configure the timer stop condition

// Configure the RX timeout
#ifdef RECEIVE_TIMEOUT
SpiritTimerSetRxTimeoutMs(2000.0);
#else
SET_INFINITE_RX_TIMEOUT();
#endif /* RECIEVE_TIMEOUT */
SpiritTimerSetRxTimeoutStopCondition(SQI_ABOVE_THRESHOLD);

4. A Simple Application

To demonstrate the transmission and reception of a packet with the SPIRIT1, this section presents an extremely simple, unidirectional communication example. In it, one node periodically transmits a short message while another node receives it and prints it to a serial console. For a more advanced bidirectional communication example, see the Point-to-Point Example included in the X-CUBE-SUBG1 firmware package (in the en.x-cube-subg1_firmware/STM32CubeExpansion_SUBG1_V3.3.0/Projects/STM32L152RE-NUCLEO/Examples/SPIRIT1/P2P_demo directory).

4.1 Helper Functions

To encourage portability of the main application, the SPIRIT1 API function calls have been abstracted into several helper functions.

SPSGRF_StartTx()

This function, shown in Listing 7, starts a transmission operation using the data buffer provided as an argument. It first flushes the TX FIFO to avoid sending any unexpected data and then caps the buffer length so it doesn’t exceed the length of the TX FIFO. It then loads the FIFO with the contents of the TX buffer and sets the payload length. Finally, it sends the TX command to start the transmission.

Listing 7: SPSGRF_StartTx() definition

void SPSGRF_StartTx(uint8_t *txBuff, uint8_t txLen)
{
  // flush the TX FIFO
  SpiritCmdStrobeFlushTxFifo();

  // Avoid TX FIFO overflow
  txLen = (txLen > MAX_BUFFER_LEN ? txLen : MAX_BUFFER_LEN);
  
  // start TX operation
  SpiritSpiWriteLinearFifo(txLen, txBuff);
  SpiritPktBasicSetPayloadLength(txLen);
  SpiritCmdStrobeTx();
}

Notice that this function does not wait for the transmission operation to complete before returning. That responsibility is left to the calling module. While there are many ways to determine when the operation is complete, the most common is to enable the TX data sent interrupt event (as shown above in Listing 4).

SPSGRF_StartRx()

To start a reception, all that must be done is send the RX command as shown below. Like the above function, this function does not block until the operation is complete. The calling module can determine if data has been received using several methods, but the most straightforward is to configure an RX data ready interrupt request (as shown above in Listing 4).

Listing 8: SPSGRF_StartRx() definition

void SPSGRF_StartRx(void)
{
  SpiritCmdStrobeRx();
}

SPSGRF_GetRxData()

Once data has been received by the SPIRIT1, it must be read from the RX FIFO. This function does so by first getting the number of bytes currently stored in the FIFO and then reading that many bytes from it. The data is stored in the RX buffer supplied to the function as an argument.

Listing 9: SPSGRF_GetRxData() definition

uint8_t SPSGRF_GetRxData(uint8_t *rxBuff)
{
  uint8_t len;

  len = SpiritLinearFifoReadNumElementsRxFifo();
  SpiritSpiReadLinearFifo(len, rxBuff);

  return len;
}

4.2 The Interrupt Handler

In Listing 4, GPIO3 on the SPIRIT1 was configured to generate an interrupt signal (i.e. toggle from logic high to logic low) if any of the four specified events occurs. In Section 1.2, the MCU pin connected to GPIO3 was configured as an external interrupt line with a falling edge trigger. Therefore, when GPIO3 on the SPIRIT1 becomes low, the HAL_GPIO_EXTI_Callback() function will be executed on the MCU. For this application, the callback is defined as shown below in Listing 10. After ensuring GPIO3 is indeed the source of the interrupt, the status registers on the SPIRIT1 device are read to determine which event(s) triggered the interrupt request. If it was the either of the TX data sent or RX data sent events, their respective flags are set. If it was the RX data discarded event or RX timeout event, the RX command is reissued to place the SPIRIT1 back in the receiving state.

Listing 10: HAL_GPIO_EXTI_Callback() definition

void HAL_GPIO_EXTI_Callback(uint16_t GPIO_Pin)
{
  SpiritIrqs xIrqStatus;

  if (GPIO_Pin != SPIRIT1_GPIO3_Pin)
  {
    return;
  }

  SpiritIrqGetStatus(&xIrqStatus);
  if (xIrqStatus.IRQ_TX_DATA_SENT)
  {
    xTxDoneFlag = S_SET;
  }
  if (xIrqStatus.IRQ_RX_DATA_READY)
  {
    xRxDoneFlag = S_SET;
  }
  if (xIrqStatus.IRQ_RX_DATA_DISC || xIrqStatus.IRQ_RX_TIMEOUT)
  {
    SpiritCmdStrobeRx();
  }
}

4.3 The Application

This simple application is completely contained withing the main.c file. The same project is used for both the transmitting node and the receiving node thanks to the APPLICATION_TRANSMITTER macro. To program the transmitting node, simply uncomment this macro definition before building the project to generate its binary file. For the receiver; comment out the definition, re-build, and program the receiver node.

The setup code before the infinite loop is the same for both nodes. Two flags indicating completed transmit and receive operations are globally defined. The data buffer is locally defined and the MCU clock and peripherals are initialized. Finally, the SPIRIT1 device in the SPSGRF module is initialized as described in Section 3. After initialization, the destination address is set using the SpiritPktBasicSetDestinationAddress() function. Note that the transmitter and receiver both have the same address in this simple example, and the destination address must match.

In the main loop, different processes are defined for the two nodes. The transmitter sets the TX done flag to FALSE before telling the SPIRIT1 to send the contents of the data buffer to the receiver. It then waits for the transmission to complete by polling the TX done flag (which will be set to TRUE in the callback function). Once completed, an informative message is printed to the serial console and a two second delay is initiated.

Similarly, the receiver starts by setting the RX done flag to FALSE before telling the SPIRIT1 to enter the receive state. It then continuously polls the RX done flag because the callback function will set it to TRUE once data is received from the transmitter. Once data is received, it is read from the SPIRIT1 device and printed to the serial console.

A simplified version of the main.c file (without extraneous comments and function definitions) is provided below in Listing 11. The complete file can be found here.

Listing 11: Application-specific contents of main.c

#include "main.h"
#include "spi.h"
#include "usart.h"
#include "gpio.h"
#include <stdio.h>
#include "spsgrf.h"

#define APPLICATION_TRANSMITTER // comment out if programming the receiver

volatile SpiritFlagStatus xTxDoneFlag;
volatile SpiritFlagStatus xRxDoneFlag;

int main(void)
{
  char payload[20] = "Hello World!\r\n";
  uint8_t rxLen;
  
  HAL_Init();
  SystemClock_Config();

  /* Initialize all configured peripherals */
  MX_GPIO_Init();
  MX_USART2_UART_Init();
  MX_SPI1_Init();

  SPSGRF_Init();
  SpiritPktBasicSetDestinationAddress(0x44);

  while (1)
  {
#ifdef APPLICATION_TRANSMITTER
    // Send the payload
    xTxDoneFlag = S_RESET;
    SPSGRF_StartTx(payload, strlen(payload));
    while(!xTxDoneFlag);

    HAL_UART_Transmit(&huart2, "Payload Sent\r\n", 14, HAL_MAX_DELAY);

    HAL_Delay(2000); // Block for 2000 ms
#else
    xRxDoneFlag = S_RESET;
    SPSGRF_StartRx();
    while (!xRxDoneFlag);

    rxLen = SPSGRF_GetRxData(payload);
    HAL_UART_Transmit(&huart2, "Received: ", 10, HAL_MAX_DELAY);
    HAL_UART_Transmit(&huart2, payload, rxLen, HAL_MAX_DELAY);
#endif // APPLICATION_TRANSMITTER
  }
}

Summary

The purpose of this tutorial is to demonstrate how to make the SPIRIT1 API available for use in an STM32-based application as well as walk-through its fundamental use cases. For those without a project to build on, instructions are provided showing how to start a new STM32CubeIDE project and configure/initialize the peripherals required to interface with the SPIRIT1. From there, the SPIRIT1 library was included by copying the driver files into the project, providing an implementation of the pre-defined interface, and appending the new resources to the include paths. With the API accessible, the SPIRIT1 was properly initialized for the application and unidirectional communication was established using helper functions and an external interrupt handler. The result is a straightforward, bare bones example application that can easily be expanded upon for a variety of use cases.