From ed688cf01950cb0e7ceeb6482495909e6103d453 Mon Sep 17 00:00:00 2001 From: Angus Gratton Date: Thu, 10 Nov 2022 12:55:49 +1100 Subject: [PATCH] lora: Add STM32WL55 subghz LoRa modem class. Support depends on hardware support in MicroPython. Also includes some tweaks in the SX126x base class, to deal with slightly different platform configuration on STM32WL55, longer timeouts, tx_ant options, etc. This work was funded through GitHub Sponsors. Signed-off-by: Angus Gratton --- micropython/lora/README.md | 89 +++++++++--- .../lora/lora-stm32wl5/lora/stm32wl5.py | 134 ++++++++++++++++++ micropython/lora/lora-stm32wl5/manifest.py | 3 + micropython/lora/lora-sx126x/lora/sx126x.py | 20 +-- micropython/lora/lora-sx126x/manifest.py | 2 +- micropython/lora/lora/lora/__init__.py | 11 ++ micropython/lora/lora/manifest.py | 2 +- 7 files changed, 233 insertions(+), 28 deletions(-) create mode 100644 micropython/lora/lora-stm32wl5/lora/stm32wl5.py create mode 100644 micropython/lora/lora-stm32wl5/manifest.py diff --git a/micropython/lora/README.md b/micropython/lora/README.md index fdb8363..c32ae91 100644 --- a/micropython/lora/README.md +++ b/micropython/lora/README.md @@ -16,6 +16,7 @@ Currently these radio modem chipsets are supported: * SX1277 * SX1278 * SX1279 +* STM32WL55 "sub-GHz radio" peripheral Most radio configuration features are supported, as well as transmitting or receiving packets. @@ -37,6 +38,7 @@ modem model that matches your hardware: - `lora-sx126x` for SX1261 & SX1262 support. - `lora-sx127x` for SX1276-SX1279 support. +- `lora-stm32wl5` for STM32WL55 support. It's recommended to install only the packages that you need, to save firmware size. @@ -113,6 +115,24 @@ example: lower max frequency, lower maximum SF value) is responsibility of the calling code. When possible please use the correct class anyhow, as per-part code may be added in the future. +### Creating STM32WL55 + +``` +from lora import WL55SubGhzModem + +def get_modem(): + # The LoRa configuration will depend on your board and location, see + # below under "Modem Configuration" for some possible examples. + lora_cfg = { 'freq_khz': SEE_BELOW_FOR_CORRECT_VALUE } + return WL55SubGhzModem(lora_cfg) + +modem = get_modem() +``` + +Note: As this is an internal peripheral of the STM32WL55 microcontroller, +support also depends on MicroPython being built for a board based on this +microcontroller. + ### Notes about initialisation * See below for details about the `lora_cfg` structure that configures the modem's @@ -157,6 +177,15 @@ Here is a full list of parameters that can be passed to both constructors: | `lora_cfg` | No | If set to an initial LoRa configuration then the modem is set up with this configuration. If not set here, can be set by calling `configure()` later on. | | | `ant`_sw | No | Optional antenna switch object instance, see below for description. | | +#### STM32WL55 + +| Parameter | Required | Description | +|-------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `lora_cfg` | No | If set to an initial LoRa configuration then the modem is set up with this configuration. If not set here, can be set by calling `configure()` later on. | +| `tcxo_millivolts` | No | Defaults to 1700. The voltage supplied on pin PB0_VDDTCXO. See `dio3_tcxo_millivolts` above for details, this parameter has the same behaviour. | +| ant_sw | No | Defaults to an instance of `lora.NucleoWL55RFConfig` class for the NUCLEO-WL55 development board. Set to `None` to disable any automatic antenna switching. See below for description. | + + ## Modem Configuration It is necessary to correctly configure the modem before use. At minimum, the @@ -383,10 +412,11 @@ Type: `str`, not case sensitive Default: RFO_HF or RFO_LF (low power) -SX127x modems have multiple antenna pins for different power levels and -frequency ranges. The board/module that the LoRa modem chip is on may have -particular antenna connections, or even an RF switch that needs to be set via a -GPIO to connect an antenna pin to a particular output (see `ant_sw`, below). +SX127x modems and STM32WL55 microcontrollers have multiple antenna pins for +different power levels and frequency ranges. The board/module that the LoRa +modem chip is on may have particular antenna connections, or even an RF switch +that needs to be set via a GPIO to connect an antenna pin to a particular output +(see `ant_sw`, below). The driver must configure the modem to use the correct pin for a particular hardware antenna connection before transmitting. When receiving, the modem @@ -396,7 +426,7 @@ A common symptom of incorrect `tx_ant` setting is an extremely weak RF signal. Consult modem datasheet for more details. -SX127x values: +##### SX127x tx_ant | Value | RF Transmit Pin | |-----------------|----------------------------------| @@ -407,7 +437,15 @@ Pin "RFO_HF" is automatically used for frequencies above 862MHz, and is not supported on SX1278. "RFO_LF" is used for frequencies below 862MHz. Consult datasheet Table 32 "Frequency Bands" for more details. -**Important**: If changing `tx_ant` value, configure `output_power` at the same +##### WL55SubGhzModem tx_ant + +| Value | RF Transmit Pin | +|-----------------|-------------------------| +| `"PA_BOOST"` | RFO_HP pin (high power) | +| Any other value | RFO_LP pin (low power) | + + +**Important**: If setting `tx_ant` value, also set `output_power` at the same time or again before transmitting. #### `output_power` - Transmit output power level @@ -415,15 +453,17 @@ Type: `int` Default: Depends on modem -Nominal TX output power in dBm. The possible range depends on the modem and (for -SX127x only) the `tx_ant` configuration. +Nominal TX output power in dBm. The possible range depends on the modem and for +some modems the `tx_ant` configuration. -| Modem | `tx_ant` value | Range | "Optimal" | -|--------|------------------|-------------------|------------------------| -| SX1261 | N/A | -17 to +15 | +10, +14 or +15 [*][^] | -| SX1262 | N/A | -9 to +22 | +14, +17, +20, +22 [*] | -| SX127x | "PA_BOOST" | +2 to +17, or +20 | Any | -| SX127x | RFO_HF or RFO_LF | -4 to +15 | Any | +| Modem | `tx_ant` value | Range (dBm) | "Optimal" (dBm) | | +|-----------------|----------------------------|-------------------|------------------------|---| +| SX1261 | N/A | -17 to +15 | +10, +14 or +15 [*][^] | | +| SX1262 | N/A | -9 to +22 | +14, +17, +20, +22 [*] | | +| SX127x | "PA_BOOST" | +2 to +17, or +20 | Any | | +| SX127x | RFO_HF or RFO_LF | -4 to +15 | Any | | +| WL55SubGhzModem | "PA_BOOST" | -9 to +22 | +14, +17, +20, +22 [*] | | +| WL55SubGhzModem | Any other value (not None) | -17 to +14 | +10, +14 or +15 [*][^] | | Values which are out of range for the modem will be clamped at the minimum/maximum values shown above. @@ -432,14 +472,14 @@ Actual radiated TX power for RF regulatory purposes depends on the RF hardware, antenna, and the rest of the modem configuration. It should be measured and tuned empirically not determined from this configuration information alone. -[*] For SX1261 and SX1262 the datasheet shows "Optimal" Power Amplifier +[*] For some modems the datasheet shows "Optimal" Power Amplifier configuration values for these output power levels. If setting one of these levels, the optimal settings from the datasheet are applied automatically by the driver. Therefore it is recommended to use one of these power levels if possible. -[^] For SX1261 +15dBm is only possible with frequency above 400MHz, will be +14dBm -otherwise. +[^] In the marked configurations +15dBm is only possible with frequency above +400MHz, will be +14dBm otherwise. #### `implicit_header` - Implicit/Explicit Header Mode Type: `bool` @@ -1137,9 +1177,21 @@ The meaning of `tx_arg` depends on the modem: above), and `False` otherwise. * For SX1262 it is `True` (indicating High Power mode). * For SX1261 it is `False` (indicating Low Power mode). +* For WL55SubGhzModem it is `True` if the `PA_BOOST` `tx_ant` setting is in use (see above), and `False` otherwise. This parameter can be ignored if it's already known what modem and antenna is being used. +### WL55SubGhzModem ant_sw + +When instantiating the `WL55SubGhzModem` and `AsyncWL55SubGHzModem` classes, the +default `ant_sw` parameter is not `None`. Instead, the default will instantiate +an object of type `lora.NucleoWL55RFConfig`. This implements the antenna switch +connections for the ST NUCLEO-WL55 development board (as connected to GPIO pins +C4, C5 and C3). See ST document [UM2592][ST-UM2592-p27] (PDF) Figure 18 for details. + +When using these modem classes (only), to disable any automatic antenna +switching behaviour it's necessary to explicitly set `ant_sw=None`. + ## Troubleshooting Some common errors and their causes: @@ -1150,9 +1202,10 @@ The SX1261/2 drivers will raise this exception if the modem's TCXO fails to provide the necessary clock signal when starting a transmit or receive operation, or moving into "standby" mode. -Usually, this means the constructor parameter `dio3_tcxo_millivolts` (see above) +Sometimes, this means the constructor parameter `dio3_tcxo_millivolts` (see above) must be set as the SX126x chip DIO3 output pin is the power source for the TCXO connected to the modem. Often this parameter should be set to `3300` (3.3V) but it may be another value, consult the documentation for your LoRa modem module. [isr_rules]: https://docs.micropython.org/en/latest/reference/isr_rules.html +[ST-UM2592-p27]: https://www.st.com/resource/en/user_manual/dm00622917-stm32wl-nucleo64-board-mb1389-stmicroelectronics.pdf#page=27 diff --git a/micropython/lora/lora-stm32wl5/lora/stm32wl5.py b/micropython/lora/lora-stm32wl5/lora/stm32wl5.py new file mode 100644 index 0000000..ba71288 --- /dev/null +++ b/micropython/lora/lora-stm32wl5/lora/stm32wl5.py @@ -0,0 +1,134 @@ +# MicroPython LoRa STM32WL55 embedded sub-ghz radio driver +# MIT license; Copyright (c) 2022 Angus Gratton +# +# This driver is essentially an embedded SX1262 with a custom internal interface block. +# Requires the stm module in MicroPython to be compiled with STM32WL5 subghz radio support. +# +# LoRa is a registered trademark or service mark of Semtech Corporation or its affiliates. +from machine import Pin, SPI +import stm +from . import sx126x +from micropython import const + +_CMD_CLR_ERRORS = const(0x07) + +_REG_OCP = const(0x8E7) + +# Default antenna switch config is as per Nucleo WL-55 board. See UM2592 Fig 18. +# Possible to work with other antenna switch board configurations by passing +# different ant_sw_class arguments to the modem, any class that creates an object with rx/tx + + +class NucleoWL55RFConfig: + def __init__(self): + self._FE_CTRL = (Pin(x, mode=Pin.OUT) for x in ("C4", "C5", "C3")) + + def _set_fe_ctrl(self, values): + for pin, val in zip(self._FE_CTRL, values): + pin(val) + + def rx(self): + self._set_fe_ctrl((1, 0, 1)) + + def tx(self, hp): + self._set_fe_ctrl((0 if hp else 1, 1, 1)) + + def idle(self): + pass + + +class DIO1: + # Dummy DIO1 "Pin" wrapper class to pass to the _SX126x class + def irq(self, handler, _): + stm.subghz_irq(handler) + + +class _WL55SubGhzModem(sx126x._SX126x): + # Don't construct this directly, construct lora.WL55SubGhzModem or lora.AsyncWL55SubGHzModem + def __init__( + self, + lora_cfg=None, + tcxo_millivolts=1700, + ant_sw=NucleoWL55RFConfig, + ): + self._hp = False + + if ant_sw == NucleoWL55RFConfig: + # To avoid the default argument being an object instance + ant_sw = NucleoWL55RFConfig() + + super().__init__( + # RM0453 7.2.13 says max 16MHz, but this seems more stable + SPI("SUBGHZ", baudrate=8_000_000), + stm.subghz_cs, + stm.subghz_is_busy, + DIO1(), + False, # dio2_rf_sw + tcxo_millivolts, # dio3_tcxo_millivolts + 1000, # dio3_tcxo_start_time_us + None, # reset + lora_cfg, + ant_sw, + ) + + def _clear_errors(self): + # A weird difference between STM32WL55 and SX1262, WL55 only takes one + # parameter byte for the Clr_Error() command compared to two on SX1262. + # The bytes are always zero in both cases. + # + # (Not clear if sending two bytes will also work always/sometimes, but + # sending one byte to SX1262 definitely does not work! + self._cmd("BB", _CMD_CLR_ERRORS, 0x00) + + def _clear_irq(self, clear_bits=0xFFFF): + super()._clear_irq(clear_bits) + # SUBGHZ Radio IRQ requires manual re-enabling after interrupt + stm.subghz_irq(self._radio_isr) + + def _tx_hp(self): + # STM32WL5 supports both High and Low Power antenna pins depending on tx_ant setting + return self._hp + + def _get_pa_tx_params(self, output_power, tx_ant): + # Given an output power level in dBm and the tx_ant setting (if any), + # return settings for SetPaConfig and SetTxParams. + # + # ST document RM0453 Set_PaConfig() reference and accompanying Table 35 + # show values that are an exact superset of the SX1261 and SX1262 + # available values, depending on which antenna pin is to be + # used. Therefore, call either modem's existing _get_pa_tx_params() + # function depending on the current tx_ant setting (default is low + # power). + + if tx_ant is not None: + self._hp = tx_ant == "PA_BOOST" + + # Update the OCP register to match the maximum power level + self._reg_write(_REG_OCP, 0x38 if self._hp else 0x18) + + if self._hp: + return sx126x._SX1262._get_pa_tx_params(self, output_power, tx_ant) + else: + return sx126x._SX1261._get_pa_tx_params(self, output_power, tx_ant) + + +# Define the actual modem classes that use the SyncModem & AsyncModem "mixin-like" classes +# to create sync and async variants. + +try: + from .sync_modem import SyncModem + + class WL55SubGhzModem(_WL55SubGhzModem, SyncModem): + pass + +except ImportError: + pass + +try: + from .async_modem import AsyncModem + + class AsyncWL55SubGhzModem(_WL55SubGhzModem, AsyncModem): + pass + +except ImportError: + pass diff --git a/micropython/lora/lora-stm32wl5/manifest.py b/micropython/lora/lora-stm32wl5/manifest.py new file mode 100644 index 0000000..8c6fe5c --- /dev/null +++ b/micropython/lora/lora-stm32wl5/manifest.py @@ -0,0 +1,3 @@ +metadata(version="0.1") +require("lora-sx126x") +package("lora") diff --git a/micropython/lora/lora-sx126x/lora/sx126x.py b/micropython/lora/lora-sx126x/lora/sx126x.py index 7fbcce2..0e62740 100644 --- a/micropython/lora/lora-sx126x/lora/sx126x.py +++ b/micropython/lora/lora-sx126x/lora/sx126x.py @@ -99,7 +99,7 @@ _IRQ_DRIVER_RX_MASK = const(_IRQ_RX_DONE | _IRQ_TIMEOUT | _IRQ_CRC_ERR | _IRQ_HE # In any case, timeouts here are to catch broken/bad hardware or massive driver # bugs rather than commonplace issues. # -_CMD_BUSY_TIMEOUT_BASE_US = const(200) +_CMD_BUSY_TIMEOUT_BASE_US = const(3000) # Datasheet says 3.5ms needed to run a full Calibrate command (all blocks), # however testing shows it can be as much as as 18ms. @@ -141,9 +141,11 @@ class _SX126x(BaseModem): self._sleep = True # assume the radio is in sleep mode to start, will wake on _cmd self._dio1 = dio1 - busy.init(Pin.IN) - cs.init(Pin.OUT, value=1) - if dio1: + if hasattr(busy, "init"): + busy.init(Pin.IN) + if hasattr(cs, "init"): + cs.init(Pin.OUT, value=1) + if hasattr(dio1, "init"): dio1.init(Pin.IN) self._busy_timeout = _CMD_BUSY_TIMEOUT_BASE_US @@ -231,7 +233,7 @@ class _SX126x(BaseModem): 0x0, # DIO2Mask, not used 0x0, # DIO3Mask, not used ) - dio1.irq(self._radio_isr, trigger=Pin.IRQ_RISING) + dio1.irq(self._radio_isr, Pin.IRQ_RISING) self._clear_irq() @@ -382,7 +384,9 @@ class _SX126x(BaseModem): self._cmd(">BBH", _CMD_WRITE_REGISTER, _REG_LSYNCRH, syncword) if "output_power" in lora_cfg: - pa_config_args, self._output_power = self._get_pa_tx_params(lora_cfg["output_power"]) + pa_config_args, self._output_power = self._get_pa_tx_params( + lora_cfg["output_power"], lora_cfg.get("tx_ant", None) + ) self._cmd("BBBBB", _CMD_SET_PA_CONFIG, *pa_config_args) if "pa_ramp_us" in lora_cfg: @@ -760,7 +764,7 @@ class _SX1262(_SX126x): # SX1262 has High Power only (deviceSel==0) return True - def _get_pa_tx_params(self, output_power): + def _get_pa_tx_params(self, output_power, tx_ant): # Given an output power level in dB, return a 2-tuple: # - First item is the 3 arguments for SetPaConfig command # - Second item is the power level argument value for SetTxParams command. @@ -831,7 +835,7 @@ class _SX1261(_SX126x): # SX1261 has Low Power only (deviceSel==1) return False - def _get_pa_tx_params(self, output_power): + def _get_pa_tx_params(self, output_power, tx_ant): # Given an output power level in dB, return a 2-tuple: # - First item is the 3 arguments for SetPaConfig command # - Second item is the power level argument value for SetTxParams command. diff --git a/micropython/lora/lora-sx126x/manifest.py b/micropython/lora/lora-sx126x/manifest.py index 57b9d21..1936a50 100644 --- a/micropython/lora/lora-sx126x/manifest.py +++ b/micropython/lora/lora-sx126x/manifest.py @@ -1,3 +1,3 @@ -metadata(version="0.1.0") +metadata(version="0.1.1") require("lora") package("lora") diff --git a/micropython/lora/lora/lora/__init__.py b/micropython/lora/lora/lora/__init__.py index a12ec45..7f8930b 100644 --- a/micropython/lora/lora/lora/__init__.py +++ b/micropython/lora/lora/lora/__init__.py @@ -23,7 +23,18 @@ except ImportError as e: if "no module named 'lora." not in str(e): raise +try: + from .stm32wl5 import * # noqa: F401 + + ok = True +except ImportError as e: + if "no module named 'lora." not in str(e): + raise + + if not ok: raise ImportError( "Incomplete lora installation. Need at least one of lora-sync, lora-async and one of lora-sx126x, lora-sx127x" ) + +del ok diff --git a/micropython/lora/lora/manifest.py b/micropython/lora/lora/manifest.py index e4e325a..586c47c 100644 --- a/micropython/lora/lora/manifest.py +++ b/micropython/lora/lora/manifest.py @@ -1,2 +1,2 @@ -metadata(version="0.1.1") +metadata(version="0.2.0") package("lora")