The STM32WL55 series combines an MCU with a LoRa-transceiver in a single module. I'm working together with STMicroelectronics to get some support for these chips in the Arduino ecosystem, and we're currently investigating implementing and contributing support in RadioLib.
I'm currently in the process of investigating feasibility and already looking at what changes would be needed. I'm writing this issue to 1) see if there is interest in accepting such a contribution and 2) getting some feedback on the implementation approach.
I've added my implementation plan/idea below, I welcome comments about this. One thing that I would like some specific feedback on, is whether it would be acceptable to add some virtual methods to the SX126x class, to allow overriding them with STM32WL-specific behavior, or if you have other ideas on how to achieve this behavior (see below on details on how I could imagin this).
The STM32WL55 is a dual-core (M4 & M0+) MCU (of which the Arduino core currently only uses the M4) paired up with a LoRa transceiver which is essentially an SX1261/SX1262 (it has the exact same SPI command interface, but has both the low-power amplifier of the SX1261 and the high power amplifier of the SX1262). This means that most of the SX126x code should be usable without modification, though some changes are needed (mostly related to power selection or special handling of the GPIO and interrrupt connection).
New subclass
To support the radio, a new STM32WL subclass of SX126x can be
created. There is no point in subclassing either SX1261 or SX1262,
since most if not all of that code must be overridden anyway.
Board-specific vs chip-specific
One point to note is that most customizations needed are specific to the
way the STM32WL is wired, but some things are specific to the way the
Nucleo board used is built.
The fact that an STM32WL chip is used can easily be detected using the
various macros set by the STM32Duino core (based on the board selected
for compilation), so that is known at compiletime.
What board is selected can also be detected using macros, so the code
can contain some code mapping board macros to board-specific settings,
but it is probably good to also allow these settings to be specified by
the sketch to support custom boards as well without having to modify the
library for such custom boars (configuration from the sketch is the
approach used for other radio chips as well).
Board-specific configuration
-
For DCDC and TCXO configuration, the STM32WL subclass can provide
appropriate defaults to the corresponding parameters to begin(),
passing these values to the superclass. These defaults can based on
the board macros defined by the stm32duino core/variant). By
keeping the parameters available, this also allows supporting other
boards with the same CPU by passing different values.
-
For TX power configuration, the sketch must be able to configure
whether the high-power, low-power or both outputs are connected. This
can be done by adding a STM32WL::setOutputConfiguration() method or
so to be called by the sketch. Again the default value can be based
on board macros.
With that info, setOutputPower() can be overridden to select the right
output mode (and set the appropriate registers) based on the power value
passed.
-
For the RF switch control, the STM32WL55JC1 Nucleo board has three
control pins instead of two, since it must also switch between high
and low power output pins. These control pins are actual GPIO pins,
controlling external hardware on the board.
- Since this is board-specific, not chip-specific this should be
somehow configurable by the sketch. An extra challenge is that on
the STM32WL nucleo board, the pin values seem somewhat arbitrary
mapped to the various modes (i.e. not one RX, one LP and one HP
pin, but probably one "enable" pin and two pins driving a 3-to-1
mux or so).
- This probably means the sketch must be able to supply (up to)
three pin numbers and define an arbitrary mapping of the supported
modes (OFF, RX, LP, HP) to the values for the three pins. Since
the LP/HP mode switch is fairly specific to the STM32WL chip, this
API for this (setting pins and values mapping) should probably be
added specifically to the STM32WL class, leaving the rfswitch
handing in Module completely unused.
- Alternatively, the
Module rfswitch handling could maybe be
generalized with the mapping approach suggested above, where the
Module code just handles mapping opaque "mode" values to pin
opaque pin numbers, with the default mapping corresponding to the
current RX-enable and TX-enable handling.
SPI Communication
Since the STM32WL radio supports the same commands as the SX126x
transceiver, the higher level communication code should be usable as-is.
Work is underway in Stm32Duino to expose the SPI bus connected to the
radio through the regular SPI libary, so its SPIClass instance can be
passed to the Module constructor as normal.
Virtual GPIO pins
In the STM32WL chip, some signals (BUSY, IRQ, RESET, NSS) that are
normally handled externally are routed internally. This means they are
accessed through special registers instead of through the normal GPIO
module. This means these pins cannot be accessed through e.g.
digitalRead() and digitalWrite().
Some ways to handle this could be:
-
Modify the STM32Duino core to expose virtual pin numbers for these
that access the registers instead of the normal GPIO module. This is
certainly the easiest option, requiring little or no modification of
RadioLib. It does result in some special casing in the core and can
have some performance impact, so this should be carefully
considered.
-
Define virtual pin numbers for these within RadioLib and replacing
the the pinMode, digitalRead, and digitalWrite callbacks in
Module with versions that handle these pin numbers specially (and
call the regular function for other pins). Since this behavior is
chip-specific and not board-specific, this behavior should be
triggered from the STM32WL class. This is possible by simply
letting the STM32WL constructor call various Module::setCB_...()
methods.
This approach is not really elegant and suggests that the used
abstractions do not fit this usecase properly, but it is a very
pragmatic and easy to implement option.
-
Use virtual methods (or some other override mechanism) to let the
STM32WL class replace the code that calls e.g.
Module::digitalRead() for these pins. Since this code is
currentlyy embedded in bigger methods (containg other code that
should not be duplicated), this involves extracting all code that
accesses these pins in new SX126x methods (e.g. getBusy(),
getIrq(), setReset() and setNss()). Making these new methods
virtual allows STM32WL to override them with the appropriate
implementations.
If acceptable for the STM32duino core, option 1 is probably the easiest.
If this must be handled in radiolib (which might be needed for the IRQ
pin anyway, since that might not be readable without further changes,
see below), option 3 is the most elegant, but adds some overhead by
adding virtual methods. If that is a problem, option 2 is probably
a pragmatic compromise.
For reference, these signals can be accessed as follows:
- NSS (aka CS) is (somehwat surprisingly) controlled using the
PWR module, using
LL_PWR_SelectSUBGHZSPI_NSS and LL_PWR_UnselectSUBGHZSPI_NSS.
- The BUSY pin is also in the
PWR module. There is a direct
(RFBUSYS) and "masked" (RFBUSYMS) version, but it is not directly
clear when to use which (at first glance it seems the direct verions
matches the normal SX126x output and the masked version additionally
always returns busy after a command, but the HAL
SUBGHZ_WaitOnBusy() seems to use the masked version). These bits
can be read using LL_PWR_IsActiveFlag_RFBUSYS() and
LL_PWR_IsActiveFlag_RFBUSYMS() respectively.
- The RESET signal for the module is controlled using
RFRST bit in
the RCC module (using LL_RCC_RF_EnableReset() and
LL_RCC_RF_DisableReset()).
- The IRQ signal seems a bit more tricky to read, since it is directly
connected the the MCU's NVIC (to trigger a dedicated Radio IRQ
vector) and there seems no way to read its value directly. It might
be possible to read its value from the NIVC pending bit, but this
needs some additional investigation and depends on how interrupts are
implemented exactly (See <> below).
Interrupt attachment
In the STM32WL chip the three SX126x DIO pins are internally wired
together into a single IRQ line connected to the CPU directly, which
triggers a dedicated Radio IRQ handler. Since this IRQ is not part of
the GPIO IRQs, it cannot be accessed using attachInterrupt().
It seems this can be solved in the same three ways as proposed for the
GPIO handling:
- Modify STM32Duino to add a virtual RADIO_IRQ pin and modify
attachInterrupt() to recognize it.
- Override the
Module callback for attachInterrupt and enable
the appropriate IRQ there (and define SUBGHZ_Radio_IRQHandler to
handle the IRQ).
- Make
Sx126x::setDio1Action() virtual and override it in STM32WL.
Again, option 1 would be easiest, but it does require the core to
capture the radio ISR (by defining it unconditionally), this might have
more compatibility implications than the GPIO case. Of the alternatives
again I would prefer option 3.
Level vs edge interrupts
There does seem to be an additional issue: Documentation suggests that
te MCU NVIC is level sensitive on its interrupt lines and the
transceiver DIO lines are (probably in binary or configuration) directly
connected to the NVIC (not through the EXTI module which is used for
GPIO interrupts to make them edge sensitive).
In practice, this would mean that if the radio asserts one of its DIO
pins to signal an interrupt and the interrupt handler does not clear
the interrupt flags inside the radio (using SPI commands), the interrupt
would indefinitely keep triggering, offering the main loop no chance to
detect and process the interrupt.
If this is indeed the case (to be verified by tests), this is a tricky
problem to solve. The ISR could (before or after calling the sketch
callback) clear the radio interrupt flags, but that prevents the main
loop code from reading and processing the interrupt flags to determine
the result of an operation.
If the flags cannot be cleared, then the ISR must then disable the IRQ
enable flag in the NVIC (before or after calling the sketch callback) to
prevent the ISR from being called again. The ISR must then be enabled
again as soon as the radio interrupt flags are cleared in
SX126x::clearIrqStatus() (also clearing the pending flag to prevent
triggering the ISR immediately based on the old pending flag).
This means the clearIrqStatus() method must be made virtual (or some
other way of overriding must be provided) so the STM32WL subclass can
add this additional behavior. Unlike for the GPIO overrides, it seems
there is no obvious way to handle this by replacing a Module callback
(unless you replace the SPIbeginTransaction, SPItransfer and
SPIEndTransaction callbacks to deduce that a clear IRQ status command
was sent, but that is horrible and fragile).
If interrupts handling is indeed temporary disabled like this, this also
means that the NVIC pending flag can be used as a replacement for
reading the Irq/DIO pin in blocking code (like SX126x::transmit()).
One completely different alternative would be to map DIO1 / IRQ0 onto
a GPIO pin (this is possible using an "alternate function" on PB3) and
then (if this is actually possible - to be confirmed) reading back that
pin value and even attaching interrupts to that external pin (which
would go through the EXTI module, so be edge-triggered as expected).
One big downside of this approach is that it actually configures PB3 as
an output pin and puts the DIO signal on that pin, which prevents using
this pin for other purposes (which is particular problematic when using
Arduino shields that use it - it is connected to pin D4 on the Arduino
headers on the Nucleo board).
The STM32WL55 series combines an MCU with a LoRa-transceiver in a single module. I'm working together with STMicroelectronics to get some support for these chips in the Arduino ecosystem, and we're currently investigating implementing and contributing support in RadioLib.
I'm currently in the process of investigating feasibility and already looking at what changes would be needed. I'm writing this issue to 1) see if there is interest in accepting such a contribution and 2) getting some feedback on the implementation approach.
I've added my implementation plan/idea below, I welcome comments about this. One thing that I would like some specific feedback on, is whether it would be acceptable to add some virtual methods to the
SX126xclass, to allow overriding them with STM32WL-specific behavior, or if you have other ideas on how to achieve this behavior (see below on details on how I could imagin this).The STM32WL55 is a dual-core (M4 & M0+) MCU (of which the Arduino core currently only uses the M4) paired up with a LoRa transceiver which is essentially an SX1261/SX1262 (it has the exact same SPI command interface, but has both the low-power amplifier of the SX1261 and the high power amplifier of the SX1262). This means that most of the SX126x code should be usable without modification, though some changes are needed (mostly related to power selection or special handling of the GPIO and interrrupt connection).
New subclass
To support the radio, a new
STM32WLsubclass ofSX126xcan becreated. There is no point in subclassing either
SX1261orSX1262,since most if not all of that code must be overridden anyway.
Board-specific vs chip-specific
One point to note is that most customizations needed are specific to the
way the STM32WL is wired, but some things are specific to the way the
Nucleo board used is built.
The fact that an STM32WL chip is used can easily be detected using the
various macros set by the STM32Duino core (based on the board selected
for compilation), so that is known at compiletime.
What board is selected can also be detected using macros, so the code
can contain some code mapping board macros to board-specific settings,
but it is probably good to also allow these settings to be specified by
the sketch to support custom boards as well without having to modify the
library for such custom boars (configuration from the sketch is the
approach used for other radio chips as well).
Board-specific configuration
For DCDC and TCXO configuration, the
STM32WLsubclass can provideappropriate defaults to the corresponding parameters to
begin(),passing these values to the superclass. These defaults can based on
the board macros defined by the stm32duino core/variant). By
keeping the parameters available, this also allows supporting other
boards with the same CPU by passing different values.
For TX power configuration, the sketch must be able to configure
whether the high-power, low-power or both outputs are connected. This
can be done by adding a
STM32WL::setOutputConfiguration()method orso to be called by the sketch. Again the default value can be based
on board macros.
With that info,
setOutputPower()can be overridden to select the rightoutput mode (and set the appropriate registers) based on the power value
passed.
For the RF switch control, the STM32WL55JC1 Nucleo board has three
control pins instead of two, since it must also switch between high
and low power output pins. These control pins are actual GPIO pins,
controlling external hardware on the board.
somehow configurable by the sketch. An extra challenge is that on
the STM32WL nucleo board, the pin values seem somewhat arbitrary
mapped to the various modes (i.e. not one RX, one LP and one HP
pin, but probably one "enable" pin and two pins driving a 3-to-1
mux or so).
three pin numbers and define an arbitrary mapping of the supported
modes (OFF, RX, LP, HP) to the values for the three pins. Since
the LP/HP mode switch is fairly specific to the STM32WL chip, this
API for this (setting pins and values mapping) should probably be
added specifically to the
STM32WLclass, leaving the rfswitchhanding in
Modulecompletely unused.Modulerfswitch handling could maybe begeneralized with the mapping approach suggested above, where the
Modulecode just handles mapping opaque "mode" values to pinopaque pin numbers, with the default mapping corresponding to the
current RX-enable and TX-enable handling.
SPI Communication
Since the STM32WL radio supports the same commands as the SX126x
transceiver, the higher level communication code should be usable as-is.
Work is underway in Stm32Duino to expose the SPI bus connected to the
radio through the regular SPI libary, so its
SPIClassinstance can bepassed to the
Moduleconstructor as normal.Virtual GPIO pins
In the STM32WL chip, some signals (BUSY, IRQ, RESET, NSS) that are
normally handled externally are routed internally. This means they are
accessed through special registers instead of through the normal GPIO
module. This means these pins cannot be accessed through e.g.
digitalRead()anddigitalWrite().Some ways to handle this could be:
Modify the STM32Duino core to expose virtual pin numbers for these
that access the registers instead of the normal GPIO module. This is
certainly the easiest option, requiring little or no modification of
RadioLib. It does result in some special casing in the core and can
have some performance impact, so this should be carefully
considered.
Define virtual pin numbers for these within
RadioLiband replacingthe the
pinMode,digitalRead, anddigitalWritecallbacks inModulewith versions that handle these pin numbers specially (andcall the regular function for other pins). Since this behavior is
chip-specific and not board-specific, this behavior should be
triggered from the
STM32WLclass. This is possible by simplyletting the
STM32WLconstructor call variousModule::setCB_...()methods.
This approach is not really elegant and suggests that the used
abstractions do not fit this usecase properly, but it is a very
pragmatic and easy to implement option.
Use virtual methods (or some other override mechanism) to let the
STM32WLclass replace the code that calls e.g.Module::digitalRead()for these pins. Since this code iscurrentlyy embedded in bigger methods (containg other code that
should not be duplicated), this involves extracting all code that
accesses these pins in new
SX126xmethods (e.g.getBusy(),getIrq(),setReset()andsetNss()). Making these new methodsvirtual allows
STM32WLto override them with the appropriateimplementations.
If acceptable for the STM32duino core, option 1 is probably the easiest.
If this must be handled in radiolib (which might be needed for the IRQ
pin anyway, since that might not be readable without further changes,
see below), option 3 is the most elegant, but adds some overhead by
adding virtual methods. If that is a problem, option 2 is probably
a pragmatic compromise.
For reference, these signals can be accessed as follows:
PWRmodule, usingLL_PWR_SelectSUBGHZSPI_NSSandLL_PWR_UnselectSUBGHZSPI_NSS.PWRmodule. There is a direct(
RFBUSYS) and "masked" (RFBUSYMS) version, but it is not directlyclear when to use which (at first glance it seems the direct verions
matches the normal SX126x output and the masked version additionally
always returns busy after a command, but the HAL
SUBGHZ_WaitOnBusy()seems to use the masked version). These bitscan be read using
LL_PWR_IsActiveFlag_RFBUSYS()andLL_PWR_IsActiveFlag_RFBUSYMS()respectively.RFRSTbit inthe
RCCmodule (usingLL_RCC_RF_EnableReset()andLL_RCC_RF_DisableReset()).connected the the MCU's NVIC (to trigger a dedicated Radio IRQ
vector) and there seems no way to read its value directly. It might
be possible to read its value from the NIVC pending bit, but this
needs some additional investigation and depends on how interrupts are
implemented exactly (See <> below).
Interrupt attachment
In the STM32WL chip the three SX126x DIO pins are internally wired
together into a single IRQ line connected to the CPU directly, which
triggers a dedicated Radio IRQ handler. Since this IRQ is not part of
the GPIO IRQs, it cannot be accessed using
attachInterrupt().It seems this can be solved in the same three ways as proposed for the
GPIO handling:
attachInterrupt()to recognize it.Modulecallback forattachInterruptand enablethe appropriate IRQ there (and define
SUBGHZ_Radio_IRQHandlertohandle the IRQ).
Sx126x::setDio1Action()virtual and override it inSTM32WL.Again, option 1 would be easiest, but it does require the core to
capture the radio ISR (by defining it unconditionally), this might have
more compatibility implications than the GPIO case. Of the alternatives
again I would prefer option 3.
Level vs edge interrupts
There does seem to be an additional issue: Documentation suggests that
te MCU NVIC is level sensitive on its interrupt lines and the
transceiver DIO lines are (probably in binary or configuration) directly
connected to the NVIC (not through the EXTI module which is used for
GPIO interrupts to make them edge sensitive).
In practice, this would mean that if the radio asserts one of its DIO
pins to signal an interrupt and the interrupt handler does not clear
the interrupt flags inside the radio (using SPI commands), the interrupt
would indefinitely keep triggering, offering the main loop no chance to
detect and process the interrupt.
If this is indeed the case (to be verified by tests), this is a tricky
problem to solve. The ISR could (before or after calling the sketch
callback) clear the radio interrupt flags, but that prevents the main
loop code from reading and processing the interrupt flags to determine
the result of an operation.
If the flags cannot be cleared, then the ISR must then disable the IRQ
enable flag in the NVIC (before or after calling the sketch callback) to
prevent the ISR from being called again. The ISR must then be enabled
again as soon as the radio interrupt flags are cleared in
SX126x::clearIrqStatus()(also clearing the pending flag to preventtriggering the ISR immediately based on the old pending flag).
This means the
clearIrqStatus()method must be made virtual (or someother way of overriding must be provided) so the
STM32WLsubclass canadd this additional behavior. Unlike for the GPIO overrides, it seems
there is no obvious way to handle this by replacing a
Modulecallback(unless you replace the
SPIbeginTransaction,SPItransferandSPIEndTransactioncallbacks to deduce that a clear IRQ status commandwas sent, but that is horrible and fragile).
If interrupts handling is indeed temporary disabled like this, this also
means that the NVIC pending flag can be used as a replacement for
reading the Irq/DIO pin in blocking code (like
SX126x::transmit()).One completely different alternative would be to map DIO1 / IRQ0 onto
a GPIO pin (this is possible using an "alternate function" on PB3) and
then (if this is actually possible - to be confirmed) reading back that
pin value and even attaching interrupts to that external pin (which
would go through the EXTI module, so be edge-triggered as expected).
One big downside of this approach is that it actually configures PB3 as
an output pin and puts the DIO signal on that pin, which prevents using
this pin for other purposes (which is particular problematic when using
Arduino shields that use it - it is connected to pin D4 on the Arduino
headers on the Nucleo board).