Mapping the Internal STM32WL Sub-GHz Radio Interface Signals to GPIO Pins

Introduction

Unlike the other peripherals on the STM32WL line of MCUs, the user does not interact with the sub-GHz radio through a set of memory mapped registers. Rather, a dedicated internal SPI interface (called SUBGHZSPI) is used to communicate with the RF subsystem, as shown in the highlighted portion of Figure 1. Thus, the firmware for the radio system can be designed in much the same way the firmware for an external transceiver would be. Likewise, this makes migrating an existing application based on a standard MCU and external transceiver far simpler. However, when issues arise during firmware development resulting in a need to debug the system, one might wonder if and how these internal communication signals can be inspected using external tools (e.g., a logic analyzer or oscilloscope). The answer is as simple as configuring a few extra GPIO pins.


Figure 1: The internal SPI interface dedicated to the sub-GHz radio system in the STM32WL55/54xx block diagram. Derived from Figure 1 in DS13293.

Configuring the GPIO Pins

Exactly which internal signals are available to be made external for debugging purposes? Figure 2 shows the most important ones on the right-hand side of the sub-GHz radio block diagram. They are the SUBGHZSPI (NSS, SCK, MISO, and MOSI) signals, the BUSY signal, the interrupt (IRQ0, IRQ1, and IRQ2) signals, and the HSERDY signal. Not shown but also available are the NRESET, SMPSRDY, and LDORDY signals. Please consult the Reference Manual for detailed documentation on the functionality of these signals.


Figure 2: The sub-GHz radio block diagram. Figure 9 in RM0453.

Table 1 below lists all available signals, their corresponding GPIO pins, and the alternate function values required to activate them for the for the internal radio interface of the STM32WL55JC device. This device was chosen for demonstration as it is utilized on the Nucleo-WL55JC evaluation board (the only STM32WL Nucleo board available at the time of writing). See Table 20 in this device’s datasheet for the source of these configurations. Naturally, if using a different device, be sure to refer to it’s datasheet.

Table 1: The internal signals used to interact with the STM32WL sug-GHz radio peripheral along with the GPIO pin and alternate function combination used to make them external.

Signal Pin Alternate Function
DEBUG_SUBGHZSPI_NSSOUT PA4 AF13
DEBUG_SUBGHZSPI_SCKOUT PA5 AF13
DEBUG_SUBGHZSPI_MISOOUT PA6 AF13
DEBUG_SUBGHZSPI_MOSIOUT PA7 AF13
DEBUG_RF_HSE32RDY PA10 AF13
DEBUG_RF_NRESET PA11 AF13
DEBUG_RF_SMPSRDY PB2 AF13
DEBUG_RF_LDORDY PB4 AF13
RF_BUSY PA12 AF6
RF_IRQ0 PB3 AF6
RF_IRQ1 PB5 AF6
RF_IRQ2 PB8 AF6

NOTE: There is another DEBUG_RF signal called DEBUG_RF_DTB1 available on pin PB3 with alternate function value AF13. This signal was not included in Table 1 for two reasons. 1) It conflicts with RF_IRQ0 and 2) it is (to the best of my knowledge) undocumented.

The debugging GPIO pins are configured like any other GPIO. The simplest, most common configuration method is to use the HAL library as shown in Listing 1. Because this code is intended to be used as a reference, all of the pins in Table 1 are configured. This may not be possible in some applications where a subset of these pins are being used for other purposes, resulting in conflicts. Which pins you configure and at what point in the code you configure them will depend entirely on your application-specific debugging needs.

Listing 1: Configuring GPIO pins to mirror the values of the internal sub-GHz radio interface signals using the HAL library.

GPIO_InitTypeDef GPIO_InitStruct = {0};

// Enable GPIO Clocks
__HAL_RCC_GPIOA_CLK_ENABLE();
__HAL_RCC_GPIOB_CLK_ENABLE();

// DEBUG_SUBGHZSPI_{NSSOUT, SCKOUT, MSIOOUT, MOSIOUT} pins
GPIO_InitStruct.Pin = GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7;
GPIO_InitStruct.Mode = GPIO_MODE_AF_PP;
GPIO_InitStruct.Pull = GPIO_NOPULL;
GPIO_InitStruct.Speed = GPIO_SPEED_FREQ_VERY_HIGH;
GPIO_InitStruct.Alternate = GPIO_AF13_DEBUG_SUBGHZSPI;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// DEBUG_RF_{HSE32RDY, NRESET} pins
GPIO_InitStruct.Pin = GPIO_PIN_10 | GPIO_PIN_11;
GPIO_InitStruct.Alternate = GPIO_AF13_DEBUG_RF;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// DEBUG_RF_{SMPSRDY, LDORDY} pins
GPIO_InitStruct.Pin = GPIO_PIN_2 | GPIO_PIN_4;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

// RF_BUSY pin
GPIO_InitStruct.Pin = GPIO_PIN_12;
GPIO_InitStruct.Alternate = GPIO_AF6_RF_BUSY;
HAL_GPIO_Init(GPIOA, &GPIO_InitStruct);

// RF_{IRQ0, IRQ1, IRQ2} pins
GPIO_InitStruct.Pin = GPIO_PIN_3 | GPIO_PIN_5 | GPIO_PIN_8;
HAL_GPIO_Init(GPIOB, &GPIO_InitStruct);

If the HAL library is not available (or you simply don’t want to use it), the code provided below in Listing 2 is functionally equivalent to that provided in Listing 1. The difference is that it configures the GPIO registers directly rather than relying on hardware abstraction.

Listing 2: Configuring GPIO pins to mirror the values of the internal sub-GHz radio interface signals by modifying registers directly.

// Enable GPIO Clocks
SET_BIT(RCC->AHB2ENR, (RCC_AHB2ENR_GPIOAEN | RCC_AHB2ENR_GPIOBEN)); // Enable IO port A and B clocks
__asm("nop"); // After the enable bit is set, there is a two clock cycles delay
__asm("nop"); // before the clock is active in the peripheral (7.2.21 in RM0453).

// Configure GPIO
CLEAR_BIT(GPIOA->OTYPER, GPIO_OTYPER_OT4); // output push-pull
CLEAR_BIT(GPIOA->OTYPER, GPIO_OTYPER_OT5); // output push-pull
CLEAR_BIT(GPIOA->OTYPER, GPIO_OTYPER_OT6); // output push-pull
CLEAR_BIT(GPIOA->OTYPER, GPIO_OTYPER_OT7); // output push-pull
CLEAR_BIT(GPIOA->OTYPER, GPIO_OTYPER_OT10); // output push-pull
CLEAR_BIT(GPIOA->OTYPER, GPIO_OTYPER_OT11); // output push-pull
CLEAR_BIT(GPIOB->OTYPER, GPIO_OTYPER_OT2); // output push-pull
CLEAR_BIT(GPIOB->OTYPER, GPIO_OTYPER_OT4); // output push-pull
CLEAR_BIT(GPIOA->OTYPER, GPIO_OTYPER_OT12); // output push-pull
CLEAR_BIT(GPIOB->OTYPER, GPIO_OTYPER_OT3); // output push-pull
CLEAR_BIT(GPIOB->OTYPER, GPIO_OTYPER_OT5); // output push-pull
CLEAR_BIT(GPIOB->OTYPER, GPIO_OTYPER_OT8); // output push-pull

MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE4_Msk, (0b10 << GPIO_MODER_MODE4_Pos)); // Alternate function mode
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE5_Msk, (0b10 << GPIO_MODER_MODE5_Pos)); // Alternate function mode
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE6_Msk, (0b10 << GPIO_MODER_MODE6_Pos)); // Alternate function mode
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE7_Msk, (0b10 << GPIO_MODER_MODE7_Pos)); // Alternate function mode
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE10_Msk, (0b10 << GPIO_MODER_MODE10_Pos)); // Alternate function mode
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE11_Msk, (0b10 << GPIO_MODER_MODE11_Pos)); // Alternate function mode
MODIFY_REG(GPIOB->MODER, GPIO_MODER_MODE2_Msk, (0b10 << GPIO_MODER_MODE2_Pos)); // Alternate function mode
MODIFY_REG(GPIOB->MODER, GPIO_MODER_MODE4_Msk, (0b10 << GPIO_MODER_MODE4_Pos)); // Alternate function mode
MODIFY_REG(GPIOA->MODER, GPIO_MODER_MODE12_Msk, (0b10 << GPIO_MODER_MODE12_Pos)); // Alternate function mode
MODIFY_REG(GPIOB->MODER, GPIO_MODER_MODE3_Msk, (0b10 << GPIO_MODER_MODE3_Pos)); // Alternate function mode
MODIFY_REG(GPIOB->MODER, GPIO_MODER_MODE5_Msk, (0b10 << GPIO_MODER_MODE5_Pos)); // Alternate function mode
MODIFY_REG(GPIOB->MODER, GPIO_MODER_MODE8_Msk, (0b10 << GPIO_MODER_MODE8_Pos)); // Alternate function mode

MODIFY_REG(GPIOA->PUPDR, GPIO_PUPDR_PUPD4_Msk, 0); // no pull-up/down
MODIFY_REG(GPIOA->PUPDR, GPIO_PUPDR_PUPD5_Msk, 0); // no pull-up/down
MODIFY_REG(GPIOA->PUPDR, GPIO_PUPDR_PUPD6_Msk, 0); // no pull-up/down
MODIFY_REG(GPIOA->PUPDR, GPIO_PUPDR_PUPD7_Msk, 0); // no pull-up/down
MODIFY_REG(GPIOA->PUPDR, GPIO_PUPDR_PUPD10_Msk, 0); // no pull-up/down
MODIFY_REG(GPIOA->PUPDR, GPIO_PUPDR_PUPD11_Msk, 0); // no pull-up/down
MODIFY_REG(GPIOB->PUPDR, GPIO_PUPDR_PUPD2_Msk, 0); // no pull-up/down
MODIFY_REG(GPIOB->PUPDR, GPIO_PUPDR_PUPD4_Msk, 0); // no pull-up/down
MODIFY_REG(GPIOA->PUPDR, GPIO_PUPDR_PUPD12_Msk, 0); // no pull-up/down
MODIFY_REG(GPIOB->PUPDR, GPIO_PUPDR_PUPD3_Msk, 0); // no pull-up/down
MODIFY_REG(GPIOB->PUPDR, GPIO_PUPDR_PUPD5_Msk, 0); // no pull-up/down
MODIFY_REG(GPIOB->PUPDR, GPIO_PUPDR_PUPD8_Msk, 0); // no pull-up/down

MODIFY_REG(GPIOA->OSPEEDR, GPIO_OSPEEDR_OSPEED4_Msk, (0b11 << GPIO_OSPEEDR_OSPEED4_Pos)); // high output speed
MODIFY_REG(GPIOA->OSPEEDR, GPIO_OSPEEDR_OSPEED5_Msk, (0b11 << GPIO_OSPEEDR_OSPEED5_Pos)); // high output speed
MODIFY_REG(GPIOA->OSPEEDR, GPIO_OSPEEDR_OSPEED6_Msk, (0b11 << GPIO_OSPEEDR_OSPEED6_Pos)); // high output speed
MODIFY_REG(GPIOA->OSPEEDR, GPIO_OSPEEDR_OSPEED7_Msk, (0b11 << GPIO_OSPEEDR_OSPEED7_Pos)); // high output speed
MODIFY_REG(GPIOA->OSPEEDR, GPIO_OSPEEDR_OSPEED10_Msk, (0b11 << GPIO_OSPEEDR_OSPEED10_Pos)); // high output speed
MODIFY_REG(GPIOA->OSPEEDR, GPIO_OSPEEDR_OSPEED11_Msk, (0b11 << GPIO_OSPEEDR_OSPEED11_Pos)); // high output speed
MODIFY_REG(GPIOB->OSPEEDR, GPIO_OSPEEDR_OSPEED2_Msk, (0b11 << GPIO_OSPEEDR_OSPEED2_Pos)); // high output speed
MODIFY_REG(GPIOB->OSPEEDR, GPIO_OSPEEDR_OSPEED4_Msk, (0b11 << GPIO_OSPEEDR_OSPEED4_Pos)); // high output speed
MODIFY_REG(GPIOA->OSPEEDR, GPIO_OSPEEDR_OSPEED12_Msk, (0b11 << GPIO_OSPEEDR_OSPEED12_Pos)); // high output speed
MODIFY_REG(GPIOB->OSPEEDR, GPIO_OSPEEDR_OSPEED3_Msk, (0b11 << GPIO_OSPEEDR_OSPEED3_Pos)); // high output speed
MODIFY_REG(GPIOB->OSPEEDR, GPIO_OSPEEDR_OSPEED5_Msk, (0b11 << GPIO_OSPEEDR_OSPEED5_Pos)); // high output speed
MODIFY_REG(GPIOB->OSPEEDR, GPIO_OSPEEDR_OSPEED8_Msk, (0b11 << GPIO_OSPEEDR_OSPEED8_Pos)); // high output speed

MODIFY_REG(GPIOA->AFR[0], GPIO_AFRL_AFSEL4_Msk, (0xD << GPIO_AFRL_AFSEL4_Pos)); // AF13 (Debug) selected
MODIFY_REG(GPIOA->AFR[0], GPIO_AFRL_AFSEL5_Msk, (0xD << GPIO_AFRL_AFSEL5_Pos)); // AF13 (Debug) selected
MODIFY_REG(GPIOA->AFR[0], GPIO_AFRL_AFSEL6_Msk, (0xD << GPIO_AFRL_AFSEL6_Pos)); // AF13 (Debug) selected
MODIFY_REG(GPIOA->AFR[0], GPIO_AFRL_AFSEL7_Msk, (0xD << GPIO_AFRL_AFSEL7_Pos)); // AF13 (Debug) selected
MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFSEL10_Msk, (0xD << GPIO_AFRH_AFSEL10_Pos)); // AF13 (Debug) selected
MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFSEL11_Msk, (0xD << GPIO_AFRH_AFSEL11_Pos)); // AF13 (Debug) selected
MODIFY_REG(GPIOB->AFR[0], GPIO_AFRL_AFSEL2_Msk, (0xD << GPIO_AFRL_AFSEL2_Pos)); // AF13 (Debug) selected
MODIFY_REG(GPIOB->AFR[0], GPIO_AFRL_AFSEL4_Msk, (0xD << GPIO_AFRL_AFSEL4_Pos)); // AF13 (Debug) selected
MODIFY_REG(GPIOA->AFR[1], GPIO_AFRH_AFSEL12_Msk, (0x6 << GPIO_AFRH_AFSEL12_Pos)); // AF6 (RF) selected
MODIFY_REG(GPIOB->AFR[0], GPIO_AFRL_AFSEL3_Msk, (0x6 << GPIO_AFRL_AFSEL3_Pos)); // AF6 (RF) selected
MODIFY_REG(GPIOB->AFR[0], GPIO_AFRL_AFSEL5_Msk, (0x6 << GPIO_AFRL_AFSEL5_Pos)); // AF6 (RF) selected
MODIFY_REG(GPIOB->AFR[1], GPIO_AFRH_AFSEL8_Msk, (0x6 << GPIO_AFRH_AFSEL8_Pos)); // AF6 (RF) selected

Inspecting the Sub-GHz Radio Interface Signals using the Nucleo-WL55JC

As an example of using the provided code to make the internal radio interface signals externally observable, the example project entitled “SubGhz_Phy_PingPong” (available in the STM32CubeWL MCU Package) will be modified, compiled, and loaded onto a Nucleo-WL55JC evaluation board. Then, a logic analyzer will be used to capture and visualize the signal activity.

The first step is to check for GPIO conflicts. By opening the project’s STM32CubeMX device configuration file (the .ioc file) and studying the pinout view, we see that all of the GPIO pins outlined in Table 1 are unused. Therefore, it is safe to configure all of the pins to match the their corresponding internal signals without impacting the application functionality.

Next, the GPIO configuration code must be added to the project source code. In this case, the simplest way to do this is to copy the contents of either Listing 1 or Listing 2 and paste them into the main.c file, just before the radio initialization routine. That is, paste the code immediately after the SystemClock_Config() function between the /* USER CODE BEGIN SysInit */ and /* USER CODE END SysInit */ comments. Then, build the application and program the Nucleo board.


Figure 3: Locations of the sub-GHz radio interface signals on the Nucleo-WL55JC board when the corresponding GPIO pins are suitably configured.

Figure 3 shows which header pins on the Nucleo board correspond to the signal pins in Table 1. The digital I/O lines of an Analog Discovery 2 device were connected to these pins and the WaveForms application was properly configured to capture the signal values over a suitable time frame. The results of one such capture are shown in Figure 4. The activity on the SPI bus in this case appears to be a result of an interrupt request on the IRQ0 line. Notice also the change in the SMPSRDY signal as the radio enters standby mode and the change in the BUSY signal as the radio processes a command. Zooming in would allow the values of the SPI protocol interpreter to become visible, allowing one to determine exactly which commands were sent to the radio system and what the responses were.


Figure 4: Screenshot of logic analyzer window in WaveForms, showing a timing diagram of the internal radio interface signals.