60d137029f
Currently, the LoRa SX126x driver dynamically creates at least one, sometimes two, memoryview objects with each call to `_cmd`. This commit simply provides the class with a long-lived memoryview object for `_cmd` to easily slice as necessary. Unlike the SX127x chips, Semtech unfortunately designed the SX126x modems to be more command-centric (as opposed to directly setting registers). Given the amount `_cmd` is called during normal device operation, even a minor improvement here should have a decent impact. Basic TX and RX tests pass on hardware. Signed-off-by: Max Holliday <maholli@stanford.edu> |
||
---|---|---|
.. | ||
examples | ||
lora | ||
lora-async | ||
lora-stm32wl5 | ||
lora-sx126x | ||
lora-sx127x | ||
lora-sync | ||
tests | ||
README.md |
README.md
LoRa driver
This MicroPython library provides synchronous and asynchronous wireless drivers for Semtech's LoRa (Long Range Radio) modem devices.
(LoRa is a registered trademark or service mark of Semtech Corporation or its affiliates.)
Support
Currently these radio modem chipsets are supported:
- SX1261
- SX1262
- SX1276
- SX1277
- SX1278
- SX1279
- STM32WL55 "sub-GHz radio" peripheral
Most radio configuration features are supported, as well as transmitting or receiving packets.
This library can be used on any MicroPython port which supports the machine.SPI
interface.
Installation
First, install at least one of the following "base" LoRa packages:
lora-sync
to use the synchronous LoRa modem API.lora-async
to use the asynchronous LoRa modem API with asyncio. Support forasyncio
must be included in your MicroPython build to uselora-async
.
Second, install at least one of the following modem chipset drivers for the 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.
Installing any of these packages will automatically also install a common
base package, lora
.
For more information about how to install packages, or "freeze" them into a firmware image, consult the MicroPython documentation on "Package management".
Initializing Driver
Creating SX1262 or SX1261
This is the synchronous modem class, and requires lora-sync
to be installed:
from machine import SPI, Pin
import lora import SX1262 # or SX1261, depending on which you have
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 }
# To instantiate SPI correctly, see
# https://docs.micropython.org/en/latest/library/machine.SPI.html
spi = SPI(0, baudrate=2000_000)
cs = Pin(9)
# or SX1261(), depending on which you have
return SX1262(spi, cs,
busy=Pin(2), # Required
dio1=Pin(20), # Optional, recommended
reset=Pin(15), # Optional, recommended
lora_cfg=lora_cfg)
modem = get_modem()
Creating SX127x
This is the synchronous modem class, and requires lora-sync
to be installed:
from machine import SPI, Pin
# or SX1277, SX1278, SX1279, depending on which you have
from lora import SX1276
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 }
# To instantiate SPI correctly, see
# https://docs.micropython.org/en/latest/library/machine.SPI.html
spi = SPI(0, baudrate=2000_000)
cs = Pin(9)
# or SX1277, SX1278, SX1279, depending on which you have
return SX1276(spi, cs,
dio0=Pin(10), # Optional, recommended
dio1=Pin(11), # Optional, recommended
reset=Pin(13), # Optional, recommended
lora_cfg=lora_cfg)
modem = get_modem()
Note: Because SX1276, SX1277, SX1278 and SX1279 are very similar, currently the driver uses the same code for any. Dealing with per-part limitations (for 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 LoRa registers. - Connecting radio "dio" pins as shown above is optional but recommended so the driver can use pin interrupts for radio events. If not, the driver needs to poll the chip instead. Interrupts allow reduced power consumption and may also improve receive sensitivity (by removing SPI bus noise during receive operations.)
All constructor parameters
Here is a full list of parameters that can be passed to both constructors:
S1261/SX1262
(Note: It's important to instantiate the correct object as these two modems have small differences in their command protocols.)
Parameter | Required | Description |
---|---|---|
spi |
Yes | Instance of a machine.SPI object or compatible, for the modem's SPI interface (modem MISO, MOSI, SCK pins). |
cs |
Yes | Instance of a machine.Pin input, as connected to the modem's NSS pin. |
busy |
Yes | Instance of a machine.Pin input, as connected to the modem's BUSY pin. |
dio1 |
No | Instance of a machine.Pin input, as connected to the modem's DIO1 pin. If not provided then interrupts cannot be used to detect radio events. |
dio2_rf_sw |
No, defaults to True |
By default, configures the modem's DIO2 pin as an RF switch. The modem will drive this pin high when transmitting and low otherwise. Set this parameter to False if DIO2 is connected elsewhere on your LoRa board/module and you don't want it toggling on transmit. |
dio3_tcxo_millivolts |
No | If set to an integer value, DIO3 will be used as a variable voltage source for the modem's main TCXO clock source. DIO3 will automatically disable the TCXO to save power when the 32MHz clock source is not needed. The value is units of millivolts and should be one of the voltages listed in the SX1261 datasheet section 13.3.6 "SetDIO3AsTCXOCtrl". Any value between 1600 and 3300 can be specified and the driver will round down to a lower supported voltage step if necessary. The manufacturer of the LoRa board or module you are using should be able to tell you what value to pass here, if any. |
dio3_tcxo_start_time_us |
No | This value is ignored unless dio3_tcxo_millivolts is set, and is the startup delay in microseconds for the TCXO connected to DIO3. Each time the modem needs to enable the TCXO, it will wait this long. The default value is 1000 (1ms). Values can be set in multiples of 15.625 us and range from 0us to 262 seconds (settings this high will make the modem unusable). |
reset | No | If set to a machine.Pin output attached to the modem's NRESET pin , then it will be used to hard reset the modem before initializing it. If unset, the programmer is responsible for ensuring the modem is in an idle state when the constructor is called. |
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. |
SX1276/SX1277/SX1278/SX1279
Parameter | Required | Description | |
---|---|---|---|
spi |
Yes | Instance of a machine.SPI object or compatible, for the modem's SPI interface (modem MISO, MOSI, SCK pins). |
|
cs |
Yes | Instance of a machine.Pin input, as connected to the modem's NSS pin. |
|
dio0 |
No | Instance of a machine.Pin input, as connected to the modem's DIO0 pin. If set, allows the driver to use interrupts to detect "RX done" and "TX done" events. |
|
dio1 |
No | Instance of a machine.Pin input, as connected to the modem's DIO1/DCLK pin. If set, allows the driver to use interrupts to detect "RX timeout" events. Setting this pin requires dio0 to also be set. |
|
reset |
No | If set to a machine.Pin output attached the modem's NRESET pin , it will be used to hard reset the modem before initializing it. If unset, the programmer is responsible for ensuring the modem should be is in an idle state when the object is instantiated. |
|
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 correct RF frequency must be set. There are many additional LoRa modem radio configuration settings that may be important. For two LoRa modem modems to communicate, their radio configurations must be compatible.
Different regions in the world also have RF regulations which you must abide by. Check RF communications regulations for the location you are in, to determine which configurations are legal for you to use.
Modem configuration can be set in two ways:
- Pass
lora_cfg
keyword parameter to the modem class constructor (see examples above). - Call
modem.configure(lora_cfg)
at any time
Where lora_cfg
is a dict
containing configuration keys and values. If a key
is missing, the value set in the modem is unchanged.
Basic Configuration
The minimal configuration is the modem frequency:
lora_cfg = { 'freq_khz': 916000 }
modem.configure(lora_cfg)
The example above sets the main frequency to 916.0MHz (916,000kHz), and leaves the rest of the modem settings at their defaults. If you have two of the same module using this driver, setting the same frequency on both like this may be enough for them to communicate. However, the other default settings are a compromise that may not give the best range, bandwidth, reliability, etc. for your application. The defaults may not even be legal to transmit with in your region!
Other parameters, particularly Bandwidth (lora_cfg["bw"]
), Spreading Factor
(lora_cfg["sf"]
), and transmit power (lora_cfg["output_power"]
) may be
limited in your region. You should find this out as well and ensure the
configuration you are using is allowed. This is your responsibility!
Defaults
If you don't configure anything, the default settings are equivalent to:
lora_cfg = { 'freq_khz': None, # Must set this
'sf': 7,
'coding_rate': 5, # 4/5 Coding
'bw': '125',
}
With the default output_power
level depending on the radio in use.
Choosing Other Parameters
Valid choices are determined by two things:
-
Regulatory rules in your region. This information is provided by regional authorities, but it may also be useful to consult the LoRaWAN Regional Parameters document (official) and the (Things Network (unofficial) for LoRaWAN frequency plans.
Even if you're not connecting to a LoRaWAN network, if you choose frequency and bandwidth settings from the LoRaWAN recommendations for your region then they should be legal to use.
-
Design of the radio module/modem/board you are using. RF antenna components are usually tailored for particular frequency ranges. One some boards only particular antenna ports or other features may be connected.
Longer Range Configuration
Here is an example aiming for higher range and lower data rate with the main frequency set again to 916Mhz (for the "AU915" Australian region):
lora_cfg = { 'freq_khz': 916000,
'sf': 12,
'bw': '62.5', # kHz
'coding_rate': 8,
'output_power': 20, # dBm
'rx_boost': True,
}
Quick explanation of these settings, for more detailed explanations see the next section below:
- Setting
sf
to maximum (higher "Spreading Factor") means each LoRa "chirp" takes longer, for more range but lower data rate. - Setting
bw
bandwidth setting lower makes the signal less susceptible to noise, but again slower. 62.5kHz is the lowest setting recommended by Semtech unless the modem uses a TCXO for high frequency stability, rather than a cheaper crystal. - Setting
coding_rate
higher to 4/8 means that more Forward Error Correction information is sent, slowing the data rate but increasing the chance of a packet being received correctly. - Setting
output_power
to 20dBm will select the maximum (or close to it) for the radio, which may be less than 20dBm. - Enabling
rx_boost
will increase the receive sensitivity of the radio, if it supports this.
Additional Australia-specific regulatory explanation
The LoRaWAN AU915 specifications suggest 125kHz bandwidth. To tell that it's OK
to set bw
lower, consult the Australian Low Interference Potential Devices
class license. This class
license allows Digital Modulation Transmitters in the 915-928MHz band to
transmit up to 1W Maximum EIRP provided "The radiated peak power spectral
density in any 3 kHz must not exceed 25 mW per 3 kHz".
output_power
set to 20dBm is 100mW, over 62.5kHz bandwidth gives
1.6mW/kHz. This leaves significant headroom for antenna gain that might increase
radiated power in some directions.)
Configuration Keys
These keys can be set in the lora_cfg
dict argument to configure()
,
and correspond to the parameters documented in this section.
Consult the datasheet for the LoRa modem you are using for an in-depth description of each of these parameters.
Values which are unset when configure()
is called will keep their existing
values.
freq_khz
- RF Frequency
Type: int
(recommended) or float
(if supported by port)
LoRa RF frequency in kHz. See above for notes about regulatory limits on this value.
The antenna and RF matching components on a particular LoRa device may only support a particular frequency range. Consult the manufacturer's documentation.
sf
- Spreading Factor
Type: int
Spreading Factor, numeric value only. Higher spreading factors allow reception of weaker signals but have slower data rate.
The supported range of SF values varies depending on the modem chipset:
Spreading Factor | Supported SX126x | Supported SX127x |
---|---|---|
5 | Yes | No |
6 | Yes [*] | Yes [*] |
7 | Yes | Yes |
8 | Yes | Yes |
9 | Yes | Yes |
10 | Yes | Yes, except SX1277[^] |
11 | Yes | Yes, except SX1277[^] |
12 | Yes | Yes, except SX2177[^] |
[*] SF6 is not compatible between SX126x and SX127x chipsets.
[^] SX1276, SX1278 and SX1279 all support SF6-SF12. SX1277 only supports SF6-SF9. This limitation is not checked by the driver.
bw
- Bandwidth
Type: int
or str
Default: 125
Bandwidth value in kHz. Must be exactly one of these LoRa bandwidth values:
- 7.8
- 10.4
- 15.6
- 20.8
- 31.25
- 41.7
- 62.5
- 125
- 250
- 500
Higher bandwidth transmits data faster and reduces peak spectral density when transmitting, but is more susceptible to interference.
IF setting bandwidth below 62.5kHz then Semtech recommends using a hardware TCXO as the modem clock source, not a cheaper crystal. Consult the modem datasheet and your hardware maker's reference for more information and to determine which clock source your LoRa modem hardware is using.
For non-integer bandwidth values, it's recommended to always set this parameter
as a str
(i.e. "15.6"
) not a numeric float
.
coding_rate
- FEC Coding Rate
Type: int
Default: 5
Forward Error Correction (FEC) coding rate is expressed as a ratio, 4/N
. The
value passed in the configuration is N
:
Value | Error coding rate |
---|---|
5 | 4/5 |
6 | 4/6 |
7 | 4/7 |
8 | 4/8 |
Setting a higher value makes transmission slower but increases the chance of receiving successfully in a noisy environment
In explicit header mode (the default), coding_rate
only needs to be set by the
transmitter and the receiver will automatically choose the correct rate when
receiving based on the received header. In implicit header mode (see
implicit_header
), this value must be set the same on both transmitter and
receiver.
tx_ant
- TX Antenna
Supported: SX127x only.
Type: str
, not case sensitive
Default: RFO_HF or RFO_LF (low power)
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 chooses the correct pin based on the selected frequency.
A common symptom of incorrect tx_ant
setting is an extremely weak RF signal.
Consult modem datasheet for more details.
SX127x tx_ant
Value | RF Transmit Pin |
---|---|
"PA_BOOST" |
PA_BOOST pin (high power) |
Any other value | RFO_HF or RFO_LF pin (low power) |
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.
WL55SubGhzModem tx_ant
Value | RF Transmit Pin |
---|---|
"PA_BOOST" |
RFO_HP pin (high power) |
Any other value | RFO_LP pin (low power) |
NOTE: Currently the PA_BOOST
HP antenna output is lower than it should be
on this board, due to an unknown driver bug.
If setting tx_ant
value, also set output_power
at the same time or again
before transmitting.
output_power
- Transmit output power level
Type: int
Default: Depends on modem
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 (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.
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 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.
[^] In the marked configurations +15dBm is only possible with frequency above 400MHz, will be +14dBm otherwise.
implicit_header
- Implicit/Explicit Header Mode
Type: bool
Default: False
LoRa supports both implicit and explicit header modes. Explicit header mode
(implicit_header
set to False) is the default.
implicit_header
must be set the same on both sender and receiver.
- In explicit header mode (default), each transmitted LoRa packet has a header
which contains information about the payload length,
coding_rate
value in use, and whether the payload has a CRC attached (crc_en
). The receiving modem decodes and verifies the header and uses the values to receive the correct length payload and verify the CRC if enabled. - In implicit header mode (
implicit_header
set to True), this header is not sent and this information must be already be known and configured by both sender and receiver. Specifically:crc_en
setting should be set the same on both sender and receiver.coding_rate
setting must match between the sender and receiver.- Receiver must provide the
rx_length
argument when calling eitherrecv()
orstart_recv()
. This length must match the length in bytes of the payload sent by the sender.
crc_en
- Enable CRCs
Type: bool
Default: True
LoRa packets can have a 16-bit CRC attached to determine if a packet is received correctly without corruption.
- In explicit header mode (default), the sender will attach a CRC if
crc_en
is True.crc_en
parameter is ignored by the receiver, which determines if there is a CRC based on the received header and will check it if so. - In implicit header mode, the sender will only include a CRC if
crc_en
is True and the receiver will only check the CRC ifcrc_en
is True.
By default, if CRC checking is enabled on the receiver then the LoRa modem driver
silently drops packets with invalid CRCs. Setting modem.rx_crc_error = True
will change this so that packets with failed CRCs are returned to the caller,
with the crc_error
field set to True (see RxPacket
, below).
auto_image_cal
- Automatic Image Calibration
Supported: SX127x only.
Type: bool
Default: False
If set True, enable automatic image re-calibration in the modem if the temperature changes significantly. This may avoid RF performance issues caused by frequency drift, etc. Setting this value may lead to dropped packets received when an automatic calibration event is in progress.
Consult SX127x datasheet for more information.
syncword
- Sync Word
Type: int
Default: 0x12
LoRa Sync Words are used to differentiate LoRa packets as being for Public or Private networks. Sync Word must match between sender and receiver.
For SX127x this value is an 8-bit integer. Supported values 0x12 for Private Networks (default, most users) and 0x34 for Public Networks (LoRaWAN only).
For SX126x this value is a 16-bit integer. Supported values 0x1424 for Private
Networks (default, most users) and 0x3444 for Public Networks. However the driver will automatically translate values configured using the 8-bit SX127x format for software compatibility, so setting an 8-bit value is supported on all modems.
You probably shouldn't change this value from the default, unless connecting to a LoRaWAN network.
pa_ramp_us
- PA Ramp Time
Type: int
Default: 40
us
Power Amplifier ramp up/down time, as expressed in microseconds.
The exact values supported on each radio are different. Configuring an unsupported value will cause the driver to choose the next highest value that is supported for that radio.
Value (us) | Supported SX126x | Supported SX127x |
---|---|---|
10 | Yes | Yes |
12 | No | Yes |
15 | No | Yes |
20 | Yes | Yes |
25 | No | Yes |
31 | No | Yes |
40 | Yes | Yes |
50 | No | Yes |
62 | No | Yes |
80 | Yes | No |
100 | No | Yes |
125 | No | Yes |
200 | Yes | No |
250 | No | Yes |
500 | No | Yes |
800 | Yes | No |
1000 | No | Yes |
1700 | Yes | No |
2000 | No | Yes |
3400 | Yes | Yes |
preamble_len
- Preamble Length
Type: int
Default: 12
Length of the preamble sequence, in units of symbols.
invert_iq_tx
/invert_iq_rx
- Invert I/Q
Type: bool
Default: Both False
If invert_iq_tx
or invert_iq_rx
is set then IQ polarity is inverted in the
radio for either TX or RX, respectively. The receiver's invert_iq_rx
setting
must match the sender's invert_iq_tx
setting.
This is necessary for LoRaWAN where end-devices transmit with inverted IQ relative to gateways.
Note: The current SX127x datasheet incorrectly documents the modem register
setting corresponding to invert_iq_tx
. This driver configures TX polarity
correctly for compatibility with other LoRa modems, most other SX127x drivers,
and LoRaWAN. However, there are some SX127x drivers that follow the datasheet
description, and they will set invert_iq_tx
opposite to this.
rx_boost
- Boost receive sensitivity
Type: bool
Default: False
Enable additional receive sensitivity if available.
- On SX126x, this makes use of the "Rx Boosted gain" option.
- On SX127x, this option is available for HF bands only and sets the LNA boost register field.
lna_gain
- Receiver LNA gain
Supported: SX127x only.
Type: int
or None
Default: 1
Adjust the LNA gain level for receiving. Valid values are None
to enable
Automatic Gain Control, or integer gain levels 1 to 6 where 1 is maximum gain
(default).
Sending & Receiving
Simple API
The driver has a "simple" API to easily send and receive LoRa packets. The API is fully synchronous, meaning the caller is blocked until the LoRa operation (send or receive) is done. The Simple API doesn't support starting a send while a receive in progress (or vice versa). It is suitable for simple applications only.
For an example that uses the simple API, see examples/reliable_delivery/sender.py
.
send
To send (transmit) a LoRa packet using the configured modulation settings:
def send(self, packet, tx_at_ms=None)
Example:
modem.send(b'Hello world')
send()
transmits a LoRa packet with the provided payload bytes, and returns once transmission is complete.- The return value is the timestamp when transmission completed, as a
time.ticks_ms()
result. It will be more accurate if the modem was initialized to use interrupts.
For precise timing of sent packets, there is an optional tx_at_ms
argument
which is a timestamp (as a time.ticks_ms()
value). If set, the packet will be
sent as close as possible to this timestamp and the function will block until
that time arrives:
modem.send(b'Hello world', time.ticks_add(time.ticks_ms(), 250))
(This allows more precise timing of sent packets, without needing to account for the length of the packet to be copied to the modem.)
receive
def recv(self, timeout_ms=None, rx_length=0xFF, rx_packet=None)
Examples:
with_timeout = modem.recv(2000)
print(repr(with_timeout))
wait_forever = modem.recv()
print(repr(wait_forever))
recv()
receives a LoRa packet from the modem.- Returns None on timeout, or an
RxPacket
instance with the packet on success. - Optional arguments:
timeout_ms
. Optional, sets a receive timeout in milliseconds. If None (default value), then the function will block indefinitely until a packet is received.rx_length
. Necessary to set ifimplicit_header
is set toTrue
(see above). This is the length of the packet to receive. Ignored in the default LoRa explicit header mode, where the received radio header includes the length.rx_packet
. Optional, this can be anRxPacket
object previously received from the modem. If the newly received packet has the same length, this object is reused and returned to save an allocation. If the newly received packet has a different length, a newRxPacket
object is allocated and returned instead.
RxPacket
RxPacket
is a class that wraps a bytearray
holding the LoRa packet payload,
meaning it can be passed anywhere that accepts a buffer object (like bytes
,
bytearray
).
However it also has the following metadata object variables:
-
ticks_ms
- is a timestamp oftime.ticks_ms()
called at the time the packet was received. Timestamp will be more accurate if the modem was initialized to use interrupts. -
snr
- is the Signal to Noise ratio of the received packet, in units ofdB * 4
. Higher values indicate better signal. -
rssi
- is the Received Signal Strength indicator value in units of dBm. Higher (less negative) values indicate more signal strength. -
crc_error
- In the default configuration, this value will always be False as packets with invalid CRCs are dropped. If themodem.rx_crc_error
flag is set to True, then a packet with an invalid CRC will be returned with this flag set to True.Note that CRC is only ever checked on receive in particular configurations, see the
crc_en
configuration item above for an explanation. If CRC is not checked on receive, andcrc_error
will always be False.
Example:
rx = modem.recv(1000)
if rx:
print(f'Received {len(rx)} byte packet at '
f'{rx.ticks_ms}ms, with SNR {rx.snr} '
f'RSSI {rx.rssi} valid_crc {rx.valid_crc}')
Asynchronous API
Not being able to do anything else while waiting for the modem is very limiting. Async Python is an excellent match for this kind of application!
To use async Python, first install lora-async
and then instantiate the async
version of the LoRA modem class. The async versions have the prefix Async
at
the beginning of the class name. For example:
import asyncio
from lora import AsyncSX1276
def get_async_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 }
# To instantiate SPI correctly, see
# https://docs.micropython.org/en/latest/library/machine.SPI.html
spi = SPI(0, baudrate=2000_000)
cs = Pin(9)
# or AsyncSX1261, AsyncSX1262, AsyncSX1277, AsyncSX1278, SX1279, etc.
return AsyncSX1276(spi, cs,
dio0=Pin(10), # Optional, recommended
dio1=Pin(11), # Optional, recommended
reset=Pin(13), # Optional, recommended
lora_cfg=lora_cfg)
modem = get_async_modem()
async def recv_coro():
rx = await modem.recv(2000)
if rx:
print(f'Received: {rx}')
else:
print('Timeout!')
async def send_coro():
counter = 0
while True:
await modem.send(f'Hello world #{counter}'.encode())
print('Sent!')
await asyncio.sleep(5)
counter += 1
async def init():
await asyncio.gather(
asyncio.create_task(send_coro()),
asyncio.create_task(recv_coro())
)
asyncio.run(init())
For a more complete example, see examples/reliable_delivery/sender_async.py
.
- The
modem.recv()
andmodem.send()
coroutines take the same arguments as the synchronous class' functionsrecv()
andsend()
, as documented above. - However, because these are async coroutines it's possible for other async tasks to execute while they are blocked waiting for modem operations.
- It is possible to await the
send()
coroutine while arecv()
is in progress. The receive will automatically resume once the modem finishes sending. Send always has priority over receive. - However, at most one task should be awaiting each of receive and send. For
example, it's not possible for two tasks to
await modem.send()
at the same time.
Async Continuous Receive
An additional API provides a Python async iterator that will continuously receive packets from the modem:
async def keep_receiving():
async for packet in am.recv_continuous():
print(f'Received: {packet}')
For a more complete example, see examples/reliable_delivery/receiver_async.py
.
Receiving will continue and the iterator will yield packets unless another task
calls modem.stop()
or modem.standby()
(see below for a description of these
functions).
Same as the async recv()
API, it's possible for another task to send while
this iterator is in use.
Low-Level API
This API allows other code to execute while waiting for LoRa operations, without using asyncio coroutines.
This is a traditional asynchronous-style API that requires manual management of modem timing, interrupts, packet timeouts, etc. It's very easy to write spaghetti code with this API. If asyncio is available on your board, the async Python API is probably an easier choice to get the same functionality with less complicated code.
However, if you absolutely need maximum control over the modem and the rest of your board then this may be the API for you!
Receiving
will_irq = modem.start_recv(timeout_ms=1000, continuous=False)
rx = True
while rx is True:
if will_irq:
# Add code to sleep and wait for an IRQ,
# if necessary call modem.irq_triggered() to verify
# that the modem IRQ was actually triggered.
pass
rx = modem.poll_recv()
# Do anything else you need the application to do
if rx: # isinstance(rx, lora.RxPacket)
print(f'Received: {rx}')
else: # rx is False
print('Timed out')
For an example that uses the low-level receive API for continuous receive, see
examples/reliable_delivery/receiver.py
.
The steps to receive packet(s) with the low-level API are:
-
Call
modem.start_recv(timeout_ms=None, continuous=False, rx_length=0xFF)
.timeout_ms
is an optional timeout in milliseconds, same as the Simple API recv().- Set
continuous=True
for the modem to continuously receive and not go into standby after the first packet is received. If settingcontinuous
toTrue
,timeout_ms
must beNone
. rx_length
is an optional argument, only used when LoRa implicit headers are configured. See the Simple API description above for details.
The return value of this function is truthy if interrupts will be used for the receive, falsey otherwise.
-
If interrupts are being used, wait for an interrupt to occur. Steps may include configuring the modem interrupt pins as wake sources and putting the host into a light sleep mode. See the general description of "Interrupts", below.
Alternatively, if
timeout_ms
was set then caller can wait for at least the timeout period before checking if the modem received anything or timed out.It is also possible to simply call
poll_recv()
in a loop, but doing this too frequently may significantly degrade the RF receive performance depending on the hardware. -
Call
modem.poll_recv()
. This function checks the receive state and returns a value indicating the current state:-
True
if the modem is still receiving and the caller should call this function again in the future. This can be caused by any of:- Modem is still waiting in 'single' mode (
continuous=False
) to receive a packet or time out. - Modem is in continuous receive mode so will always be receiving.
- The modem is actually sending right now, but the driver will resume receiving after the send completes.
- The modem received a packet with an invalid CRC (and
modem.rx_crc_error = False
). The driver has just now discarded it and resumed the modem receive operation.
- Modem is still waiting in 'single' mode (
-
False
if the modem is not currently receiving. This can be caused by any of:- No receive has been started.
- A single receive has timed out.
- The receive was aborted. See the
standby()
andsleep()
functions below.
-
An instance of the
RxPacket
class. This means the modem has received this packet since the last call topoll_recv()
. Whether or not the modem is still receiving after this depends on whether the receive was started incontinuous
mode or not.)
-
-
If
poll_recv()
returnedTrue
, go back to step 2 and wait for the next opportunity to callpoll_recv()
. (Note that it's necessary to test usingis True
to distinguish betweenTrue
and a new packet.)
It is possible to also send packets while receiving and looping between
steps 2 and 4. The driver will automatically suspend receiving and resume it
again once sending is done. It's OK to call either the Simple API
send()
function or the low-level send API (see below) in order to do
this.
The purpose of the low-level API is to allow code to perform other unrelated
functions during steps 2 and 3. It's still recommended to call
modem.poll_recv()
as soon as possible after a modem interrupt has
occurred, especially in continuous receive mode when multiple packets may be
received rapidly.
To cancel a receive in progress, call modem.standby()
or modem.sleep()
, see
below for descriptions of these functions.
Important: None of the MicroPython lora driver is thread-safe. It's OK for different MicroPython threads to manage send and receive, but the caller is responsible for adding locking so that different threads are not calling any modem APIs concurrently. Async MicroPython may provide a cleaner and simpler choice for this kind of firmware architecture.
Sending
The low-level API for sending is similar to the low-level API for receiving:
-
Call
modem.prepare_send(payload)
with the packet payload. This will put the modem into standby (pausing receive if necessary), configure the modem registers, and copy the payload into the modem FIFO buffer. -
Call
modem.start_send(packet)
to actually start sending.Sending is split into these two steps to allow accurate send timing.
prepare_send()
may take a variable amount of time to copy data to the modem, configure registers, etc. Thenstart_send()
only performs the minimum fixed duration operation to start sending, so transmit should start very soon after this function is called.The return value of
start_send()
function is truthy if an interrupt is enabled to signal the send completing, falsey otherwise.Not calling both
prepare_send()
orstart_send()
in order, or calling any other modem functions betweenprepare_send()
andstart_send()
, is not supported and will result in incorrect behaviour. -
Wait for the send to complete. This is possible in any of three different ways:
- If interrupts are enabled, wait for an interrupt to occur. Steps may include configuring the modem interrupt pins as wake sources and putting the host into a light sleep mode. See the general description of "Interrupts", below.
- Calculate the packet "time on air" by calling
modem.get_time_on_air_us(len(packet))
and wait at least this long. - Call
modem.poll_send()
in a loop (see next step) until it confirms the send has completed.
-
Call
modem.poll_send()
to check transmission state, and to automatically resume a receive operation if one was suspended byprepare_send()
. The result of this function is one of:-
True
if a send is in progress and the caller should call again. -
False
if no send is in progress. -
An
int
value. This is returned the first timepoll_send()
is called after a send ended. The value is thetime.ticks_ms()
timestamp of the time that the send completed. If interrupts are enabled, this is the time the "send done" ISR executed. Otherwise, it will be the time thatpoll_send()
was just called.
Note that
modem.poll_send()
returns anint
only one time per successful transmission. Any subsequent calls will returnFalse
as there is no longer a send in progress.To abort a send in progress, call
modem.standby()
ormodem.sleep()
, see the descriptions of these functions below. Subsequent calls topoll_send()
will returnFalse
. -
-
If
poll_send()
returnedTrue
, repeat steps 3 through 5.
Important: Unless a transmission is aborted, poll_send()
MUST be
called at least once after start_send()
and should be repeatedly called
until it returns a value other than True
. poll_send()
can also be called
after a send is aborted, but this is optional. If poll_send()
is not
called correctly then the driver's internal state will not correctly update and
no subsequent receive will be able to start.
It's also possible to mix the simple send()
API with the low-level receive
API, if this is more convenient for your application.
Interrupts
If interrupt pins are in use then it's important for a programmer using the low-level API to handle interrupts correctly.
It's only possible to rely on interrupts if the correct hardware interrupt lines
are configured. Consult the modem reference datasheet, or check if the value of
start_recv()
or start_send()
is truthy, in order to know if hardware
interrupts can be used. Otherwise, the modem must be polled to know when an
operation has completed.
There are two kinds of interrupts:
-
A hardware interrupt (set in the driver by
Pin.irq()
) will be triggered on the rising edge of a modem interrupt line (DIO0, DIO1, etc). The driver will attempt to configure these forRX Done
,RX Timeout
andTX Done
events if possible and applicable for the modem operation, and will handle them.It's possible for the programmer to configure these pins as hardware wake sources and put the board into a low-power sleep mode, to be woken when the modem finishes its operation.
-
A "soft" interrupt is triggered by the driver if an operation is aborted (see
standby()
andsleep()
, below), or if a receive operation "soft times out". A receive "soft times out" if a receive is paused by a send operation and after the send operation completes then the timeout period for the receive has already elapsed. In these cases, the driver's radio ISR routine is called but no hardware interrupt occurs.
To detect if a modem interrupt has occurred, the programmer can use any of the following different approaches:
- Port-specific functions to determine a hardware wakeup cause. Note that this can only detect hardware interrupts.
- Call the
modem.irq_triggered()
function. This is a lightweight function that returns True if the modem ISR has been executed since the last time a send or receive started. It is cleared whenpoll_recv()
orpoll_send()
is called after an interrupt, or when a new operation is started. The idea is to use this as a lightweight "should I callpoll_recv()
orpoll_send()
now?" check function if there's no easy way to determine which interrupt has woken the board up. - Implement a custom interrupt callback function and call
modem.set_irq_callback()
to install it. The function will be called if a hardware interrupt occurs, possibly in hard interrupt context. Refer to the documentation about writing interrupt handlers for more information. It may also be called if the driver triggers a soft interrupt. Thelora-async
modem classes install their own callback here, so it's not possible to mix this approach with the provided asynchronous API. - Call
modem.poll_recv()
ormodem.poll_send()
. This takes more time and uses more power as it reads the modem IRQ status directly from the modem via SPI, but it also give the most definite result.
As a "belts and braces" protection against unknown driver bugs or modem bugs, it's best practice to not rely on an interrupt occurring and to also include some logic that periodically times out and polls the modem state "just in case".
Other Functions
CRC Error Counter
Modem objects have a variable modem.crc_errors
which starts at 0
and
is incremented by one each time a received CRC error or packet header error is
detected by the modem. The programmer can read this value to know the current CRC
error count, and also write it (for example, to clear it periodically by setting
to 0
).
For an alternative method to know about CRC errors when they occur, set
modem.rx_crc_error = True
(see crc_en
, above, for more details.)
Modem Standby
Calling modem.standby()
puts the modem immediately into standby mode. In the
case of SX1261 and SX1262, the 32MHz oscillator is started.
Any current send or receive operations are immediately aborted. The implications of this depends on the API in use:
- The simple API does not support calling
standby()
while a receive or send is in progress. - The async API handles this situation automatically. Any blocked
send()
orrecv()
async coroutine will return None. Therecv_continuous()
iterator will stop iterating. - The low-level API relies on the programmer to handle this case. When the modem
goes to standby, a "soft interrupt" occurs that will trigger the radio ISR and
any related callback, but this is not a hardware interrupt so may not wake the
CPU if the programmer has put it back to sleep. Any subsequent calls to
poll_recv()
orpoll_send()
will both return(False, None)
as no operation is in progress. The programmer needs to ensure that any code that is blocking waiting for an interrupt has the chance to wake up and callpoll_recv()
and/orpoll_send()
to detect that the operation(s) have been aborted.
Modem Sleep
Calling modem.sleep()
puts the modem into a low power sleep mode with
configuration retention. The modem will automatically wake the next time an
operation is started, or can be woken manually by calling
modem.standby()
. Waking the modem may take some time, consult the modem
datasheet for details.
As with standby()
, any current send or receive operations are immediately
aborted. The implications of this are the same as listed for standby, above.
Check if modem is idle
The modem.is_idle()
function will return True unless the modem is currently
sending or receiving.
Packet length calculations
Calling modem.get_time_on_air_us(plen)
will return the "on air time" in
microseconds for a packet of length plen
, according to the current modem
configuration. This can be used to synchronise modem operations, choose
timeouts, or predict when a send will complete.
Unlike the other modem API functions, this function doesn't interact with hardware at all so it can be safely called concurrently with other modem APIs.
Antenna switch object
The modem constructors have an optional ant_sw
parameter which allows passing
in an antenna switch object to be called by the driver. This allows
automatically configuring some GPIOs or other hardware settings each time the
modem changes between TX and RX modes, and goes idle.
The argument should be an object which implements three functions: tx(tx_arg)
,
rx()
, and idle()
. For example:
class MyAntennaSwitch:
def tx(self, tx_arg):
ant_sw_gpio(1) # Set GPIO high
def rx(self):
ant_sw_gpio(0) # Set GPIO low
def idle(self):
pass
tx()
is called a short time before the modem starts sending.rx()
is called a short time before the modem starts receiving.idle()
is called at some point after each send or receive completes, and may be called multiple times.
The meaning of tx_arg
depends on the modem:
- For SX127x it is
True
if thePA_BOOST
tx_ant
setting is in use (see above), andFalse
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 thePA_BOOST
tx_ant
setting is in use (see above), andFalse
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 (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:
RuntimeError: BUSY timeout
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.
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.