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 <angus@redyak.com.au>
pull/572/head
Angus Gratton 2022-11-10 12:55:49 +11:00 zatwierdzone przez Damien George
rodzic 93bf707d6f
commit ed688cf019
7 zmienionych plików z 233 dodań i 28 usunięć

Wyświetl plik

@ -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

Wyświetl plik

@ -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

Wyświetl plik

@ -0,0 +1,3 @@
metadata(version="0.1")
require("lora-sx126x")
package("lora")

Wyświetl plik

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

Wyświetl plik

@ -1,3 +1,3 @@
metadata(version="0.1.0")
metadata(version="0.1.1")
require("lora")
package("lora")

Wyświetl plik

@ -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

Wyświetl plik

@ -1,2 +1,2 @@
metadata(version="0.1.1")
metadata(version="0.2.0")
package("lora")