Using the Low-Level Sub-GHz Radio Driver for the STM32WL Series


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.


To follow along with the tutorial exactly, the following items are required.


Create and Configure Project

The first step is to establish a project that the driver can be added to. 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.

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

  2. Use the Board Selector tab to select the NUCLEO-WL55JC1 evaluation board. Click Next.

  1. Give the project a suitable name (e.g., “radioDriverExample”) and uncheck the Enable Multi Cpus Configuration option. Click Finish.
  1. A pop-up window will appear asking if the peripherals should be initialized with their default modes. Click Yes.

  2. 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 SUBGHZ peripheral under the Connectivity category. Check the box next to Activated.

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

  1. Switch to the Clock Configuration tab. Change the MSI RC value to “48000”.
  1. 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.
  1. 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

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

  1. 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.
  1. 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.
  1. Find the imported file named stm32wlxx_nucleo_conf_template.h in the Project Explorer and rename it to stm32wlxx_nucleo_conf.h. The project structure should now appear as shown.


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

  1. Select File > New > Source Folder. Name the folder “Utilities” and click Finish.


  1. Right-click on the Utilities folder in the Project Explorer and select Import…. In the wizard that appears, within the General category, chose File System. Click Next.

  2. 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
    • Utiliites/misc/stm32_mem.c
    • Utilities/misc/stm32_mem.h

This is shown in the two figures below.

  1. Find the imported file named utilities_conf_template.h in the Project Explorer and rename it to utilities_conf.h. The project structure should now appear as shown.


  1. Repeat Step 4 for both the Utilities/conf and Utilites/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!

  1. 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
  2. Rename the file radio_conf_template.h to radio_conf.h. The project structure should now appear as shown.

  1. Open the radio_conf.h file and comment out the include directives for mw_log_conf.h, utilities_def.h, and sys_debug.h. Add an include directive for subghz.h.


  1. Similarly, open the file radio_driver.c and comment out the include directive for mw_log_conf.h.

  2. Repeat Step 4 for the Drivers/Radio directory to add it to the list of include paths.

  3. 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 the Core/Inc directory.

  1. Open platform.h and comment out the include directive for stm32wlxx_ll_gpio.h.

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

Figure 1: Low-level radio driver Ping Pong example project finite state machine

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)

  // 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)

  // Use the whole 256-byte buffer for both TX and RX
  SUBGRF_SetBufferBaseAddress(0x00, 0x00);



  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;

  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_SetSyncWord((uint8_t[]){0xC1, 0x94, 0xC1, 0x00, 0x00, 0x00, 0x00, 0x00});

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);
                          IRQ_RX_DONE | IRQ_RX_TX_TIMEOUT | IRQ_CRC_ERROR,
                          IRQ_RADIO_NONE );
  packetParams.Params.Gfsk.PayloadLength = 0xFF;
  SUBGRF_SetRx(fsm->rxTimeout << 6);

Listing 3: enterMasterTx() definition from main_gfsk.c

void enterMasterTx(pingPongFSM_t *const fsm)

  HAL_UART_Transmit(&huart2, "...PING\r\n", 9, HAL_MAX_DELAY);
  HAL_UART_Transmit(&huart2, "Master Tx start\r\n", 17, HAL_MAX_DELAY);
                          IRQ_TX_DONE | IRQ_RX_TX_TIMEOUT,
                          IRQ_RADIO_NONE );
  // 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_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.

1 Like

Dear @Matt_Mielke ,
Thank you for your sharing. It has been a very useful article for me. In my project, I wanted to communicate SX1262 and SX1276 devices with LoRa. I managed to do this by making a few changes to the settings you shared. But for now I can use the SX1262 as a transmitter. I have not yet succeeded in communicating the SX1262 as a receiver.

@Matt_Mielke This is fantastic! Very well laid out

Any chance you could do the write about the setup for LoRa modulation?

Thanks @user146! I’ll try to replicate the application using LoRa modulation later this week.

@user146, I’ve added a file to the GitHub repo which utilizes LoRa modulation rather than GFSK modulation. I’ve also updated the post to reflect this. Thanks again for your suggestion!


@Matt_Mielke You rock! I’ll be looking our for more posts from you, I really like your thorough but concise approach. Makes getting into micro controllers fun and easy(easier :upside_down_face:) to teach my kid too

1 Like

Dear @Matt_Mielke . Thank you very much for sharing about Lora. This will help me a lot.

1 Like