Skip to content

Commit ed688cf

Browse files
projectgusdpgeorge
authored andcommitted
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 <[email protected]>
1 parent 93bf707 commit ed688cf

File tree

7 files changed

+233
-28
lines changed

7 files changed

+233
-28
lines changed

micropython/lora/README.md

Lines changed: 71 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ Currently these radio modem chipsets are supported:
1616
* SX1277
1717
* SX1278
1818
* SX1279
19+
* STM32WL55 "sub-GHz radio" peripheral
1920

2021
Most radio configuration features are supported, as well as transmitting or
2122
receiving packets.
@@ -37,6 +38,7 @@ modem model that matches your hardware:
3738

3839
- `lora-sx126x` for SX1261 & SX1262 support.
3940
- `lora-sx127x` for SX1276-SX1279 support.
41+
- `lora-stm32wl5` for STM32WL55 support.
4042

4143
It's recommended to install only the packages that you need, to save firmware
4244
size.
@@ -113,6 +115,24 @@ example: lower max frequency, lower maximum SF value) is responsibility of the
113115
calling code. When possible please use the correct class anyhow, as per-part
114116
code may be added in the future.
115117

118+
### Creating STM32WL55
119+
120+
```
121+
from lora import WL55SubGhzModem
122+
123+
def get_modem():
124+
# The LoRa configuration will depend on your board and location, see
125+
# below under "Modem Configuration" for some possible examples.
126+
lora_cfg = { 'freq_khz': SEE_BELOW_FOR_CORRECT_VALUE }
127+
return WL55SubGhzModem(lora_cfg)
128+
129+
modem = get_modem()
130+
```
131+
132+
Note: As this is an internal peripheral of the STM32WL55 microcontroller,
133+
support also depends on MicroPython being built for a board based on this
134+
microcontroller.
135+
116136
### Notes about initialisation
117137

118138
* 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:
157177
| `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. | |
158178
| `ant`_sw | No | Optional antenna switch object instance, see below for description. | |
159179

180+
#### STM32WL55
181+
182+
| Parameter | Required | Description |
183+
|-------------------|----------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
184+
| `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. |
185+
| `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. |
186+
| 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. |
187+
188+
160189
## Modem Configuration
161190

162191
It is necessary to correctly configure the modem before use. At minimum, the
@@ -383,10 +412,11 @@ Type: `str`, not case sensitive
383412

384413
Default: RFO_HF or RFO_LF (low power)
385414

386-
SX127x modems have multiple antenna pins for different power levels and
387-
frequency ranges. The board/module that the LoRa modem chip is on may have
388-
particular antenna connections, or even an RF switch that needs to be set via a
389-
GPIO to connect an antenna pin to a particular output (see `ant_sw`, below).
415+
SX127x modems and STM32WL55 microcontrollers have multiple antenna pins for
416+
different power levels and frequency ranges. The board/module that the LoRa
417+
modem chip is on may have particular antenna connections, or even an RF switch
418+
that needs to be set via a GPIO to connect an antenna pin to a particular output
419+
(see `ant_sw`, below).
390420

391421
The driver must configure the modem to use the correct pin for a particular
392422
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.
396426

397427
Consult modem datasheet for more details.
398428

399-
SX127x values:
429+
##### SX127x tx_ant
400430

401431
| Value | RF Transmit Pin |
402432
|-----------------|----------------------------------|
@@ -407,23 +437,33 @@ Pin "RFO_HF" is automatically used for frequencies above 862MHz, and is not
407437
supported on SX1278. "RFO_LF" is used for frequencies below 862MHz. Consult
408438
datasheet Table 32 "Frequency Bands" for more details.
409439

410-
**Important**: If changing `tx_ant` value, configure `output_power` at the same
440+
##### WL55SubGhzModem tx_ant
441+
442+
| Value | RF Transmit Pin |
443+
|-----------------|-------------------------|
444+
| `"PA_BOOST"` | RFO_HP pin (high power) |
445+
| Any other value | RFO_LP pin (low power) |
446+
447+
448+
**Important**: If setting `tx_ant` value, also set `output_power` at the same
411449
time or again before transmitting.
412450

413451
#### `output_power` - Transmit output power level
414452
Type: `int`
415453

416454
Default: Depends on modem
417455

418-
Nominal TX output power in dBm. The possible range depends on the modem and (for
419-
SX127x only) the `tx_ant` configuration.
456+
Nominal TX output power in dBm. The possible range depends on the modem and for
457+
some modems the `tx_ant` configuration.
420458

421-
| Modem | `tx_ant` value | Range | "Optimal" |
422-
|--------|------------------|-------------------|------------------------|
423-
| SX1261 | N/A | -17 to +15 | +10, +14 or +15 [*][^] |
424-
| SX1262 | N/A | -9 to +22 | +14, +17, +20, +22 [*] |
425-
| SX127x | "PA_BOOST" | +2 to +17, or +20 | Any |
426-
| SX127x | RFO_HF or RFO_LF | -4 to +15 | Any |
459+
| Modem | `tx_ant` value | Range (dBm) | "Optimal" (dBm) | |
460+
|-----------------|----------------------------|-------------------|------------------------|---|
461+
| SX1261 | N/A | -17 to +15 | +10, +14 or +15 [*][^] | |
462+
| SX1262 | N/A | -9 to +22 | +14, +17, +20, +22 [*] | |
463+
| SX127x | "PA_BOOST" | +2 to +17, or +20 | Any | |
464+
| SX127x | RFO_HF or RFO_LF | -4 to +15 | Any | |
465+
| WL55SubGhzModem | "PA_BOOST" | -9 to +22 | +14, +17, +20, +22 [*] | |
466+
| WL55SubGhzModem | Any other value (not None) | -17 to +14 | +10, +14 or +15 [*][^] | |
427467

428468
Values which are out of range for the modem will be clamped at the
429469
minimum/maximum values shown above.
@@ -432,14 +472,14 @@ Actual radiated TX power for RF regulatory purposes depends on the RF hardware,
432472
antenna, and the rest of the modem configuration. It should be measured and
433473
tuned empirically not determined from this configuration information alone.
434474

435-
[*] For SX1261 and SX1262 the datasheet shows "Optimal" Power Amplifier
475+
[*] For some modems the datasheet shows "Optimal" Power Amplifier
436476
configuration values for these output power levels. If setting one of these
437477
levels, the optimal settings from the datasheet are applied automatically by the
438478
driver. Therefore it is recommended to use one of these power levels if
439479
possible.
440480

441-
[^] For SX1261 +15dBm is only possible with frequency above 400MHz, will be +14dBm
442-
otherwise.
481+
[^] In the marked configurations +15dBm is only possible with frequency above
482+
400MHz, will be +14dBm otherwise.
443483

444484
#### `implicit_header` - Implicit/Explicit Header Mode
445485
Type: `bool`
@@ -1137,9 +1177,21 @@ The meaning of `tx_arg` depends on the modem:
11371177
above), and `False` otherwise.
11381178
* For SX1262 it is `True` (indicating High Power mode).
11391179
* For SX1261 it is `False` (indicating Low Power mode).
1180+
* For WL55SubGhzModem it is `True` if the `PA_BOOST` `tx_ant` setting is in use (see above), and `False` otherwise.
11401181

11411182
This parameter can be ignored if it's already known what modem and antenna is being used.
11421183

1184+
### WL55SubGhzModem ant_sw
1185+
1186+
When instantiating the `WL55SubGhzModem` and `AsyncWL55SubGHzModem` classes, the
1187+
default `ant_sw` parameter is not `None`. Instead, the default will instantiate
1188+
an object of type `lora.NucleoWL55RFConfig`. This implements the antenna switch
1189+
connections for the ST NUCLEO-WL55 development board (as connected to GPIO pins
1190+
C4, C5 and C3). See ST document [UM2592][ST-UM2592-p27] (PDF) Figure 18 for details.
1191+
1192+
When using these modem classes (only), to disable any automatic antenna
1193+
switching behaviour it's necessary to explicitly set `ant_sw=None`.
1194+
11431195
## Troubleshooting
11441196

11451197
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
11501202
provide the necessary clock signal when starting a transmit or receive
11511203
operation, or moving into "standby" mode.
11521204

1153-
Usually, this means the constructor parameter `dio3_tcxo_millivolts` (see above)
1205+
Sometimes, this means the constructor parameter `dio3_tcxo_millivolts` (see above)
11541206
must be set as the SX126x chip DIO3 output pin is the power source for the TCXO
11551207
connected to the modem. Often this parameter should be set to `3300` (3.3V) but
11561208
it may be another value, consult the documentation for your LoRa modem module.
11571209

11581210
[isr_rules]: https://docs.micropython.org/en/latest/reference/isr_rules.html
1211+
[ST-UM2592-p27]: https://www.st.com/resource/en/user_manual/dm00622917-stm32wl-nucleo64-board-mb1389-stmicroelectronics.pdf#page=27
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# MicroPython LoRa STM32WL55 embedded sub-ghz radio driver
2+
# MIT license; Copyright (c) 2022 Angus Gratton
3+
#
4+
# This driver is essentially an embedded SX1262 with a custom internal interface block.
5+
# Requires the stm module in MicroPython to be compiled with STM32WL5 subghz radio support.
6+
#
7+
# LoRa is a registered trademark or service mark of Semtech Corporation or its affiliates.
8+
from machine import Pin, SPI
9+
import stm
10+
from . import sx126x
11+
from micropython import const
12+
13+
_CMD_CLR_ERRORS = const(0x07)
14+
15+
_REG_OCP = const(0x8E7)
16+
17+
# Default antenna switch config is as per Nucleo WL-55 board. See UM2592 Fig 18.
18+
# Possible to work with other antenna switch board configurations by passing
19+
# different ant_sw_class arguments to the modem, any class that creates an object with rx/tx
20+
21+
22+
class NucleoWL55RFConfig:
23+
def __init__(self):
24+
self._FE_CTRL = (Pin(x, mode=Pin.OUT) for x in ("C4", "C5", "C3"))
25+
26+
def _set_fe_ctrl(self, values):
27+
for pin, val in zip(self._FE_CTRL, values):
28+
pin(val)
29+
30+
def rx(self):
31+
self._set_fe_ctrl((1, 0, 1))
32+
33+
def tx(self, hp):
34+
self._set_fe_ctrl((0 if hp else 1, 1, 1))
35+
36+
def idle(self):
37+
pass
38+
39+
40+
class DIO1:
41+
# Dummy DIO1 "Pin" wrapper class to pass to the _SX126x class
42+
def irq(self, handler, _):
43+
stm.subghz_irq(handler)
44+
45+
46+
class _WL55SubGhzModem(sx126x._SX126x):
47+
# Don't construct this directly, construct lora.WL55SubGhzModem or lora.AsyncWL55SubGHzModem
48+
def __init__(
49+
self,
50+
lora_cfg=None,
51+
tcxo_millivolts=1700,
52+
ant_sw=NucleoWL55RFConfig,
53+
):
54+
self._hp = False
55+
56+
if ant_sw == NucleoWL55RFConfig:
57+
# To avoid the default argument being an object instance
58+
ant_sw = NucleoWL55RFConfig()
59+
60+
super().__init__(
61+
# RM0453 7.2.13 says max 16MHz, but this seems more stable
62+
SPI("SUBGHZ", baudrate=8_000_000),
63+
stm.subghz_cs,
64+
stm.subghz_is_busy,
65+
DIO1(),
66+
False, # dio2_rf_sw
67+
tcxo_millivolts, # dio3_tcxo_millivolts
68+
1000, # dio3_tcxo_start_time_us
69+
None, # reset
70+
lora_cfg,
71+
ant_sw,
72+
)
73+
74+
def _clear_errors(self):
75+
# A weird difference between STM32WL55 and SX1262, WL55 only takes one
76+
# parameter byte for the Clr_Error() command compared to two on SX1262.
77+
# The bytes are always zero in both cases.
78+
#
79+
# (Not clear if sending two bytes will also work always/sometimes, but
80+
# sending one byte to SX1262 definitely does not work!
81+
self._cmd("BB", _CMD_CLR_ERRORS, 0x00)
82+
83+
def _clear_irq(self, clear_bits=0xFFFF):
84+
super()._clear_irq(clear_bits)
85+
# SUBGHZ Radio IRQ requires manual re-enabling after interrupt
86+
stm.subghz_irq(self._radio_isr)
87+
88+
def _tx_hp(self):
89+
# STM32WL5 supports both High and Low Power antenna pins depending on tx_ant setting
90+
return self._hp
91+
92+
def _get_pa_tx_params(self, output_power, tx_ant):
93+
# Given an output power level in dBm and the tx_ant setting (if any),
94+
# return settings for SetPaConfig and SetTxParams.
95+
#
96+
# ST document RM0453 Set_PaConfig() reference and accompanying Table 35
97+
# show values that are an exact superset of the SX1261 and SX1262
98+
# available values, depending on which antenna pin is to be
99+
# used. Therefore, call either modem's existing _get_pa_tx_params()
100+
# function depending on the current tx_ant setting (default is low
101+
# power).
102+
103+
if tx_ant is not None:
104+
self._hp = tx_ant == "PA_BOOST"
105+
106+
# Update the OCP register to match the maximum power level
107+
self._reg_write(_REG_OCP, 0x38 if self._hp else 0x18)
108+
109+
if self._hp:
110+
return sx126x._SX1262._get_pa_tx_params(self, output_power, tx_ant)
111+
else:
112+
return sx126x._SX1261._get_pa_tx_params(self, output_power, tx_ant)
113+
114+
115+
# Define the actual modem classes that use the SyncModem & AsyncModem "mixin-like" classes
116+
# to create sync and async variants.
117+
118+
try:
119+
from .sync_modem import SyncModem
120+
121+
class WL55SubGhzModem(_WL55SubGhzModem, SyncModem):
122+
pass
123+
124+
except ImportError:
125+
pass
126+
127+
try:
128+
from .async_modem import AsyncModem
129+
130+
class AsyncWL55SubGhzModem(_WL55SubGhzModem, AsyncModem):
131+
pass
132+
133+
except ImportError:
134+
pass
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
metadata(version="0.1")
2+
require("lora-sx126x")
3+
package("lora")

micropython/lora/lora-sx126x/lora/sx126x.py

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,7 @@
9999
# In any case, timeouts here are to catch broken/bad hardware or massive driver
100100
# bugs rather than commonplace issues.
101101
#
102-
_CMD_BUSY_TIMEOUT_BASE_US = const(200)
102+
_CMD_BUSY_TIMEOUT_BASE_US = const(3000)
103103

104104
# Datasheet says 3.5ms needed to run a full Calibrate command (all blocks),
105105
# however testing shows it can be as much as as 18ms.
@@ -141,9 +141,11 @@ def __init__(
141141
self._sleep = True # assume the radio is in sleep mode to start, will wake on _cmd
142142
self._dio1 = dio1
143143

144-
busy.init(Pin.IN)
145-
cs.init(Pin.OUT, value=1)
146-
if dio1:
144+
if hasattr(busy, "init"):
145+
busy.init(Pin.IN)
146+
if hasattr(cs, "init"):
147+
cs.init(Pin.OUT, value=1)
148+
if hasattr(dio1, "init"):
147149
dio1.init(Pin.IN)
148150

149151
self._busy_timeout = _CMD_BUSY_TIMEOUT_BASE_US
@@ -231,7 +233,7 @@ def __init__(
231233
0x0, # DIO2Mask, not used
232234
0x0, # DIO3Mask, not used
233235
)
234-
dio1.irq(self._radio_isr, trigger=Pin.IRQ_RISING)
236+
dio1.irq(self._radio_isr, Pin.IRQ_RISING)
235237

236238
self._clear_irq()
237239

@@ -382,7 +384,9 @@ def configure(self, lora_cfg):
382384
self._cmd(">BBH", _CMD_WRITE_REGISTER, _REG_LSYNCRH, syncword)
383385

384386
if "output_power" in lora_cfg:
385-
pa_config_args, self._output_power = self._get_pa_tx_params(lora_cfg["output_power"])
387+
pa_config_args, self._output_power = self._get_pa_tx_params(
388+
lora_cfg["output_power"], lora_cfg.get("tx_ant", None)
389+
)
386390
self._cmd("BBBBB", _CMD_SET_PA_CONFIG, *pa_config_args)
387391

388392
if "pa_ramp_us" in lora_cfg:
@@ -760,7 +764,7 @@ def _tx_hp(self):
760764
# SX1262 has High Power only (deviceSel==0)
761765
return True
762766

763-
def _get_pa_tx_params(self, output_power):
767+
def _get_pa_tx_params(self, output_power, tx_ant):
764768
# Given an output power level in dB, return a 2-tuple:
765769
# - First item is the 3 arguments for SetPaConfig command
766770
# - Second item is the power level argument value for SetTxParams command.
@@ -831,7 +835,7 @@ def _tx_hp(self):
831835
# SX1261 has Low Power only (deviceSel==1)
832836
return False
833837

834-
def _get_pa_tx_params(self, output_power):
838+
def _get_pa_tx_params(self, output_power, tx_ant):
835839
# Given an output power level in dB, return a 2-tuple:
836840
# - First item is the 3 arguments for SetPaConfig command
837841
# - Second item is the power level argument value for SetTxParams command.
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
metadata(version="0.1.0")
1+
metadata(version="0.1.1")
22
require("lora")
33
package("lora")

0 commit comments

Comments
 (0)