Skip to content

Adding support for STM32WL integrated transceivers #588

Description

@matthijskooijman

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:

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

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

  3. 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:

  1. Modify STM32Duino to add a virtual RADIO_IRQ pin and modify
    attachInterrupt() to recognize it.
  2. Override the Module callback for attachInterrupt and enable
    the appropriate IRQ there (and define SUBGHZ_Radio_IRQHandler to
    handle the IRQ).
  3. 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).

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions