Introduction
The STM32WL series of devices includes a build in sub-GHz radio peripheral capable of LoRa (STM32WLE5/55 devices only), (G)FSK, (G)MSK, and BPSK modulation schemes. Communication with this radio peripheral is done via an internal SPI interface using the commands outlined in section 5.8 of the device Reference Manual. While an abstraction layer to this RF interface is defined in the Sub-GHz Phy middleware (available in the STM32CubeWL MCU Package), adding this middleware to a project using STM32CubeMX requires an advanced configuration with dependencies between several other peripherals and libraries. This results in a larger, more complex project which consumes more device memory and introduces several abstraction-related inefficiencies. For simple applications demanding minimum power consumption, it may be beneficial to isolate the RF interface driver from the Sub-GHz Phy middleware and utilize it directly.
The Sub-GHz Phy middleware is composed of a high-level layer (radio.c
) and a low-level layer (radio_driver.c
). The high-level driver provides many helpful functions which abstract the low-level radio functionality, such as RadioInit()
, RadioSetTxConfig()
, and RadioSend()
. However, convenient as these functions are, they come at the cost of inefficiency in the form of redundant function calls and an over-reliance on utilities such as a sequencer and timer server. The low-level driver simply implements the SUBGHZSPI commands outlined in the Reference Manual and provides definitions for the sub-GHz radio registers. At the expense of some quality attributes such as maintainability and portability, coding with this driver directly allows the programmer to exercise greater control over their application. In this tutorial, it is demonstrated how this low-level layer can be isolated from the Sub-GHz Phy middleware and added to an STM32CubeIDE project directly.
Requirements
To follow along with the tutorial exactly, the following items are required.
- STM32CubeIDE (last tested version: 1.16.0)
- STM32CubeWL MCU Package (last tested version 1.3.0)
- NUCLEO-WL55JC1
Procedure
Create and Configure Project
The first step is to establish a project to which the driver can be added. Here, a new STM32 project will be created from scratch and the SubGHz peripheral will be enabled. An existing project which has been properly configured may also be used.
-
Launch the STM32CubeIDE application and open (or create) the desired workspace. To create a new project, start the STM32 Project Wizard by choosing File > New > STM32 Project.
-
Use the Board Selector tab to select the NUCLEO-WL55JC1 evaluation board. Click Next.
- Give the project a suitable name (e.g., “radioDriverExample”) and uncheck the Enable Multi Cpus Configuration option. Click Finish.
- If you’re using an older version of STM32CubeIDE, a pop-up window will appear asking if the peripherals should be initialized with their default modes. Click No. If you’re using a newer version of STM32CubeIDE, a Board Project Options windows should pop up (shown below). Uncheck all of the boxes and click OK.
- The wizard will generate a device configuration file with the extension “.ioc” and this file will be opened in STM32CubeIDE. In the Pinout & Configuration tab, select the USART2 peripheral under the Connectivity category. Change Mode to “Asynchronous” and verify that pins PA2 and PA3 turn green in the Pinout view.
- Still under the Connectivity category, select the SUBGHZ peripheral. Check the box next to Activated.
- In the SUBGHZ Configuration section, change the Baudrate Prescaler Value to “4” under the Parameter Settings tab.
In the NVIC Settings tab, check the box to enable the SUBGHZ Radio Interrupt.
- Switch to the Clock Configuration tab. Change the MSI RC value to “48000”.
- Finally, switch to the Project Manager tab and open the Code Generator options. Check the box next to Generate peripheral initialization as a pair of ‘.c/.h’ files per peripheral.
- Save the modified .ioc file and click Yes when it asks “Do you want to generate code?” Click Yes again if it asks whether you want to open the C/C++ perspective.
Add the SubGHz Radio Driver
- As a preliminary step, download the STM32CubeWL MCU Package from ST’s website and extract its contents to a location of your choosing.
Add the BSP Driver
Because the Sub-GHz middleware is hardware agnostic, a Board Support Package (BSP) is required to allow the driver to control the RF switch.
- In the Project Explorer, right-click on the Drivers directory and select Import…. In the wizard that appears, within the General category, choose File System as shown below. Click Next.
- Populate the From directory field with the location of the Drivers directory in the STM32CubeWL MCU package (i.e., <extraction location>/STM32Cube_FW_WL_V1.1.0/Drivers). Use the file explorer windows to select all of the .h and .c files in the Drivers/BSP/STM32WLxx_Nucleo directory. Click Finish.
- Find the imported file named
stm32wlxx_nucleo_conf_template.h
in the Project Explorer and rename it tostm32wlxx_nucleo_conf.h
. The project structure should now appear as shown.
- In order for the compiler to locate these new header files, the directory containing them must be added to the list of include directories. Right-click on the “STM32WLxx_Nucleo” directory and select Add/remove include directory. A window will pop-up asking which configurations should be modified. Ensure both the Debug and Release options are checked and click OK.
Add the Required Utilities
As it stands, the low-level radio driver still has a few minor dependencies on the utility functions. These dependencies could easily be eliminated with a few modifications to the driver files, but it’s just as easy to include these files in the project.
- Select File > New > Source Folder. Name the folder “Utils” and click Finish.
Note: Do not name this source folder “Utilities”. As we discovered in the comments below, doing so causes it to be deleted if the project code is re-generated.
-
Right-click on the Utils folder in the Project Explorer and select Import…. In the wizard that appears, within the General category, chose File System. Click Next.
-
Populate the From directory field with the location of the Utilities directory in the STM32CubeWL MCU package (i.e., <extraction location>/STM32Cube_FW_WL_V1.1.0/Utilities). Use the file explorer windows to select the following files:
Utilities/conf/utilities_conf_template.h
Utilities/misc/stm32_mem.c
Utilities/misc/stm32_mem.h
This is shown in the two figures below.
- Find the imported file named
utilities_conf_template.h
in the Project Explorer and rename it toutilities_conf.h
. The project structure should now appear as shown.
- Repeat Step 4 for both the
Utils/conf
andUtils/misc
directories to add them to the list of include paths.
Radio Drivers
With the dependencies taken care of, the radio driver itself can be added!
-
Create a new folder in the Drivers directory named “Radio” (or whatever you prefer). Into this folder, copy the following files from the STM32CubeWL MCU package:
<extraction_location>/STM32Cube_FW_WL_V1.1.0/Middlewares/Third_Party/SubGHz_Phy/stm32_radio_driver/radio_driver.c
<extraction_location>/STM32Cube_FW_WL_V1.1.0/Middlewares/Third_Party/SubGHz_Phy/stm32_radio_driver/radio_driver.h
<extraction_location>/STM32Cube_FW_WL_V1.1.0/Middlewares/Third_Party/SubGHz_Phy/Conf/radio_conf_template.h
<extraction_location>/STM32Cube_FW_WL_V1.1.0/Projects/NUCLEO-WL55JC/Applications/SubGHz_Phy/SubGHz_Phy_PingPong/SubGHz_Phy/Target/radio_board_if.c
<extraction_location>/STM32Cube_FW_WL_V1.1.0/Projects/NUCLEO-WL55JC/Applications/SubGHz_Phy/SubGHz_Phy_PingPong/SubGHz_Phy/Target/radio_board_if.h
-
Rename the file
radio_conf_template.h
toradio_conf.h
. The project structure should now appear as shown.
- Open the
radio_conf.h
file and comment out the include directives formw_log_conf.h
,utilities_def.h
, andsys_debug.h
. Add an include directive forsubghz.h
if it’s not there already.
-
Similarly, open the file
radio_driver.c
and comment out the include directive formw_log_conf.h
. -
Repeat Step 4 for the
Drivers/Radio
directory to add it to the list of include paths. -
Copy the file
<extraction_location>/STM32Cube_FW_WL_V1.1.0/Projects/NUCLEO-WL55JC/Applications/SubGHz_Phy/SubGHz_Phy_PingPong/Core/Inc/platform.h
into theCore/Inc
directory.
-
Open
platform.h
and comment out the include directive forstm32wlxx_ll_gpio.h
. -
Finally, to begin using the radio driver, add the line
#include "radio_driver.h"
to the top of main.c within the appropriate user code section.
Example Application
As an example of using the low-level Sub-GHz Phy driver in a standalone fashion, two example programs (available on the GitHub Repository) have been created. These examples duplicate the high-level functionality of the SubGHz_Phy_PingPong example in the STM32CubeWL MCU Package. That is, they both implement the state machine shown in Figure 1. The only difference between the two examples is that one utilizes the LoRa modem while the other utilizes the FSK modem.
Two NUCLEO-WL55JC1 boards are required to run these examples, one of which will act as the master while the other will act as the slave. Initially, both boards are in the master state sending “PING” messages at random intervals and waiting for responses. Eventually, the two boards synchronize so only one device sends “PING” messages and the other device sends “PONG” messages in response.
To execute the application, follow the steps provided in the previous section to create a project which includes the low-level sub-GHz radio driver. Then, simply replace the contents of the project’s main.c
file with the contents of one of the files in the GitHub Repository, depending on which modulation scheme you wish for the example to utilize. Lastly, build the project and use it to program both Nucleo boards.
Note that these examples are compatible with the SubGHz_Phy_PingPong example. That is, one board can be programmed with the above application and the other with the SubGHz_Phy_PingPong application, and they will both work together as expected. However, to utilize GFSK modulation the SubGHz_Phy_PingPong example must first be modified slightly. Open the subghz_phy_app.h
file and change the first define directives as follows:
#define USE_MODEM_LORA 0 //1
#define USE_MODEM_FSK 1 //0
#define REGION_US915 //REGION_EU868
Then, find the RadioRandom()
function in radio.c
and comment out the line RadioSetModem( MODEM_LORA );
Not only is this line not necessary to obtain a random number, it erases the radio configuration set in the prior initialization step. Thus, in this case, it is considered to be a bug and should not be included. The SubGHz_Phy_PingPong example is now ready to be complied and flashed to one of the NUCLEO-WL55JC1 boards. The other board should be programed according to the above instructions using the contents of the main_gfsk.c
file in the GitHub Repository.
Before initializing and executing the finite state machine shown in Figure 1, the radio is initialized by calling the radioInit()
function defined below in Listing 1. This function uses the same radio configuration as the SubGHz_Phy_PingPong example, with one exception. At the end of section 6.1 in the Reference Manual, it states:
The SMPS needs a clock to be functional. If for any reason this clock stops, the device may be destroyed. To avoid this situation, a clock detection is used to, in case of a clock failure, switch off the SMPS and enable the LDO. The SMPS clock detection is enabled by the sub-GHz radio SUBGHZ_SMPSC0R.CLKDE. By default, the SMPS clock detection is disabled and must be enabled before enabling the SMPS.
Despite this warning, neither the high-level nor low-level layers of the Sub-GHz Phy Middleware bother to enable the SMPS clock detection. Because DCDC_ENABLE
is defined in radio_config.h
, the SUBGRF_SetRegulatorMode()
function will enable the SMPS step-down converter. So, right before this function call, the SMPS clock detection is manually enabled.
Listing 1: radioInit()
definition from main_gfsk.c
void radioInit(void)
{
// Initialize the hardware (SPI bus, TCXO control, RF switch)
SUBGRF_Init(RadioOnDioIrq);
// Use DCDC converter if `DCDC_ENABLE` is defined in radio_conf.h
// "By default, the SMPS clock detection is disabled and must be enabled before enabling the SMPS." (6.1 in RM0453)
SUBGRF_WriteRegister(SUBGHZ_SMPSC0R, (SUBGRF_ReadRegister(SUBGHZ_SMPSC0R) | SMPS_CLK_DET_ENABLE));
SUBGRF_SetRegulatorMode();
// Use the whole 256-byte buffer for both TX and RX
SUBGRF_SetBufferBaseAddress(0x00, 0x00);
SUBGRF_SetRfFrequency(RF_FREQUENCY);
SUBGRF_SetRfTxPower(TX_OUTPUT_POWER);
SUBGRF_SetStopRxTimerOnPreambleDetect(false);
SUBGRF_SetPacketType(PACKET_TYPE_GFSK);
ModulationParams_t modulationParams;
modulationParams.PacketType = PACKET_TYPE_GFSK;
modulationParams.Params.Gfsk.Bandwidth = SUBGRF_GetFskBandwidthRegValue(FSK_BANDWIDTH);
modulationParams.Params.Gfsk.BitRate = FSK_DATARATE;
modulationParams.Params.Gfsk.Fdev = FSK_FDEV;
modulationParams.Params.Gfsk.ModulationShaping = MOD_SHAPING_G_BT_1;
SUBGRF_SetModulationParams(&modulationParams);
packetParams.PacketType = PACKET_TYPE_GFSK;
packetParams.Params.Gfsk.AddrComp = RADIO_ADDRESSCOMP_FILT_OFF;
packetParams.Params.Gfsk.CrcLength = RADIO_CRC_2_BYTES_CCIT;
packetParams.Params.Gfsk.DcFree = RADIO_DC_FREEWHITENING;
packetParams.Params.Gfsk.HeaderType = RADIO_PACKET_VARIABLE_LENGTH;
packetParams.Params.Gfsk.PayloadLength = 0xFF;
packetParams.Params.Gfsk.PreambleLength = (FSK_PREAMBLE_LENGTH << 3); // bytes to bits
packetParams.Params.Gfsk.PreambleMinDetect = RADIO_PREAMBLE_DETECTOR_08_BITS;
packetParams.Params.Gfsk.SyncWordLength = (FSK_SYNCWORD_LENGTH << 3); // bytes to bits
SUBGRF_SetPacketParams(&packetParams);
SUBGRF_SetSyncWord((uint8_t[]){0xC1, 0x94, 0xC1, 0x00, 0x00, 0x00, 0x00, 0x00});
SUBGRF_SetWhiteningSeed(0x01FF);
}
The other functions utilizing the low-level radio driver are the state entry functions. As an example of using these driver functions to receive and send packets, the enterMasterRx()
and enterMasterTx()
functions are provided in Listings 2 and 3 respectively. For a detailed explanation on the use of the SUBGRF_SetDioIrqParams()
function, please see Sub-GHz Radio Interrupts on the STM32WL Series.
Listing 2: enterMasterRx()
definition from main_gfsk.c
void enterMasterRx(pingPongFSM_t *const fsm)
{
HAL_UART_Transmit(&huart2, "Master Rx start\r\n", 17, HAL_MAX_DELAY);
SUBGRF_SetDioIrqParams( IRQ_RX_DONE | IRQ_RX_TX_TIMEOUT | IRQ_CRC_ERROR,
IRQ_RX_DONE | IRQ_RX_TX_TIMEOUT | IRQ_CRC_ERROR,
IRQ_RADIO_NONE,
IRQ_RADIO_NONE );
SUBGRF_SetSwitch(RFO_LP, RFSWITCH_RX);
packetParams.Params.Gfsk.PayloadLength = 0xFF;
SUBGRF_SetPacketParams(&packetParams);
SUBGRF_SetRx(fsm->rxTimeout << 6);
}
Listing 3: enterMasterTx()
definition from main_gfsk.c
void enterMasterTx(pingPongFSM_t *const fsm)
{
HAL_Delay(fsm->rxMargin);
HAL_UART_Transmit(&huart2, "...PING\r\n", 9, HAL_MAX_DELAY);
HAL_UART_Transmit(&huart2, "Master Tx start\r\n", 17, HAL_MAX_DELAY);
SUBGRF_SetDioIrqParams( IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT,
IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT,
IRQ_RADIO_NONE,
IRQ_RADIO_NONE );
SUBGRF_SetSwitch(RFO_LP, RFSWITCH_TX);
// Workaround 5.1 in DS.SX1261-2.W.APP (before each packet transmission)
SUBGRF_WriteRegister(0x0889, (SUBGRF_ReadRegister(0x0889) | 0x04));
packetParams.Params.Gfsk.PayloadLength = 0x4;
SUBGRF_SetPacketParams(&packetParams);
SUBGRF_SendPayload((uint8_t *)"PING", 4, 0);
}
Again, see the complete example code for both the LoRa and the GFSK modulation schemes on the GitHub Repository.