feat: add display UI with splash screen, dashboard, and RSSI sparkline
Implement full SSD1306 OLED display support: - Splash screen on boot (1.5s) with project name, version, board - Dashboard showing radio state, config, counters, RSSI/SNR - RSSI history sparkline (64-sample ring buffer, 2px-wide bars) - DisplayOn/DisplayOff host commands routed via dedicated channel - Pure render functions against generic DrawTarget (no hardware coupling) - Display on/off control bypasses radio task entirely Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
17624 files changed
+4564928
-38
No files matched your search
Vendored
+96
@@ -0,0 +1,96 @@
|
||||
use defmt::trace;
|
||||
use embedded_hal_async::spi::{Operation, SpiDevice};
|
||||
|
||||
use crate::mod_params::RadioError::{self, SPI};
|
||||
use crate::mod_traits::InterfaceVariant;
|
||||
|
||||
pub(crate) struct SpiInterface<SPI, IV> {
|
||||
pub(crate) spi: SPI,
|
||||
pub(crate) iv: IV,
|
||||
}
|
||||
|
||||
impl<SPI, IV> SpiInterface<SPI, IV>
|
||||
where
|
||||
SPI: SpiDevice<u8>,
|
||||
IV: InterfaceVariant,
|
||||
{
|
||||
pub fn new(spi: SPI, iv: IV) -> Self {
|
||||
Self { spi, iv }
|
||||
}
|
||||
|
||||
// Write a buffer to the radio.
|
||||
pub async fn write(&mut self, write_buffer: &[u8], is_sleep_command: bool) -> Result<(), RadioError> {
|
||||
self.spi.write(write_buffer).await.map_err(|_| SPI)?;
|
||||
trace!("write: {=[u8]:02x}", write_buffer);
|
||||
|
||||
if !is_sleep_command {
|
||||
self.iv.wait_on_busy().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Write
|
||||
pub async fn write_with_payload(
|
||||
&mut self,
|
||||
write_buffer: &[u8],
|
||||
payload: &[u8],
|
||||
is_sleep_command: bool,
|
||||
) -> Result<(), RadioError> {
|
||||
let mut ops = [Operation::Write(write_buffer), Operation::Write(payload)];
|
||||
self.spi.transaction(&mut ops).await.map_err(|_| SPI)?;
|
||||
trace!("write_buf: {=[u8]:02x} -> {=[u8]:02x}", write_buffer, payload);
|
||||
|
||||
if !is_sleep_command {
|
||||
self.iv.wait_on_busy().await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Request a read, filling the provided buffer.
|
||||
pub async fn read(&mut self, write_buffer: &[u8], read_buffer: &mut [u8]) -> Result<(), RadioError> {
|
||||
{
|
||||
let mut ops = [Operation::Write(write_buffer), Operation::Read(read_buffer)];
|
||||
|
||||
self.spi.transaction(&mut ops).await.map_err(|_| SPI)?;
|
||||
}
|
||||
|
||||
self.iv.wait_on_busy().await?;
|
||||
|
||||
trace!(
|
||||
"read: addr={=[u8]:02x}, len={}, data={=[u8]:02x}",
|
||||
write_buffer,
|
||||
read_buffer.len(),
|
||||
read_buffer
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Request a read with status, filling the provided buffer and returning the status.
|
||||
pub async fn read_with_status(&mut self, write_buffer: &[u8], read_buffer: &mut [u8]) -> Result<u8, RadioError> {
|
||||
let mut status = [0u8];
|
||||
{
|
||||
let mut ops = [
|
||||
Operation::Write(write_buffer),
|
||||
Operation::Read(&mut status),
|
||||
Operation::Read(read_buffer),
|
||||
];
|
||||
|
||||
self.spi.transaction(&mut ops).await.map_err(|_| SPI)?;
|
||||
}
|
||||
|
||||
self.iv.wait_on_busy().await?;
|
||||
|
||||
trace!(
|
||||
"read: addr={=[u8]:02x}, len={}, status={:02x}, buf={=[u8]:02x}",
|
||||
write_buffer,
|
||||
read_buffer.len(),
|
||||
status[0],
|
||||
read_buffer
|
||||
);
|
||||
|
||||
Ok(status[0])
|
||||
}
|
||||
}
|
||||
Vendored
+173
@@ -0,0 +1,173 @@
|
||||
use embedded_hal::digital::OutputPin;
|
||||
use embedded_hal_async::delay::DelayNs;
|
||||
use embedded_hal_async::digital::Wait;
|
||||
|
||||
use crate::mod_params::RadioError;
|
||||
use crate::mod_params::RadioError::*;
|
||||
use crate::mod_traits::InterfaceVariant;
|
||||
|
||||
/// Base for the InterfaceVariant implementation for the Sx127x combination
|
||||
pub struct GenericSx127xInterfaceVariant<CTRL, WAIT> {
|
||||
reset: CTRL,
|
||||
irq: WAIT,
|
||||
rf_switch_rx: Option<CTRL>,
|
||||
rf_switch_tx: Option<CTRL>,
|
||||
}
|
||||
|
||||
impl<CTRL, WAIT> GenericSx127xInterfaceVariant<CTRL, WAIT>
|
||||
where
|
||||
CTRL: OutputPin,
|
||||
WAIT: Wait,
|
||||
{
|
||||
/// Create an InterfaceVariant instance for an stm32l0/sx1276 combination
|
||||
pub fn new(
|
||||
reset: CTRL,
|
||||
irq: WAIT,
|
||||
rf_switch_rx: Option<CTRL>,
|
||||
rf_switch_tx: Option<CTRL>,
|
||||
) -> Result<Self, RadioError> {
|
||||
Ok(Self {
|
||||
reset,
|
||||
irq,
|
||||
rf_switch_rx,
|
||||
rf_switch_tx,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<CTRL, WAIT> InterfaceVariant for GenericSx127xInterfaceVariant<CTRL, WAIT>
|
||||
where
|
||||
CTRL: OutputPin,
|
||||
WAIT: Wait,
|
||||
{
|
||||
async fn reset(&mut self, delay: &mut impl DelayNs) -> Result<(), RadioError> {
|
||||
delay.delay_ms(10).await;
|
||||
self.reset.set_low().map_err(|_| Reset)?;
|
||||
delay.delay_ms(10).await;
|
||||
self.reset.set_high().map_err(|_| Reset)?;
|
||||
delay.delay_ms(10).await;
|
||||
Ok(())
|
||||
}
|
||||
async fn wait_on_busy(&mut self) -> Result<(), RadioError> {
|
||||
Ok(())
|
||||
}
|
||||
async fn await_irq(&mut self) -> Result<(), RadioError> {
|
||||
self.irq.wait_for_high().await.map_err(|_| Irq)
|
||||
}
|
||||
|
||||
async fn enable_rf_switch_rx(&mut self) -> Result<(), RadioError> {
|
||||
match &mut self.rf_switch_tx {
|
||||
Some(pin) => pin.set_low().map_err(|_| RfSwitchTx)?,
|
||||
None => (),
|
||||
};
|
||||
match &mut self.rf_switch_rx {
|
||||
Some(pin) => pin.set_high().map_err(|_| RfSwitchRx),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
async fn enable_rf_switch_tx(&mut self) -> Result<(), RadioError> {
|
||||
match &mut self.rf_switch_rx {
|
||||
Some(pin) => pin.set_low().map_err(|_| RfSwitchRx)?,
|
||||
None => (),
|
||||
};
|
||||
match &mut self.rf_switch_tx {
|
||||
Some(pin) => pin.set_high().map_err(|_| RfSwitchTx),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
async fn disable_rf_switch(&mut self) -> Result<(), RadioError> {
|
||||
match &mut self.rf_switch_rx {
|
||||
Some(pin) => pin.set_low().map_err(|_| RfSwitchRx)?,
|
||||
None => (),
|
||||
};
|
||||
match &mut self.rf_switch_tx {
|
||||
Some(pin) => pin.set_low().map_err(|_| RfSwitchTx),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Base for the InterfaceVariant implementation for a generic Sx126x LoRa board
|
||||
pub struct GenericSx126xInterfaceVariant<CTRL, WAIT> {
|
||||
reset: CTRL,
|
||||
dio1: WAIT,
|
||||
busy: WAIT,
|
||||
rf_switch_rx: Option<CTRL>,
|
||||
rf_switch_tx: Option<CTRL>,
|
||||
}
|
||||
|
||||
impl<CTRL, WAIT> GenericSx126xInterfaceVariant<CTRL, WAIT>
|
||||
where
|
||||
CTRL: OutputPin,
|
||||
WAIT: Wait,
|
||||
{
|
||||
/// Create an InterfaceVariant instance for an nrf52840/sx1262 combination
|
||||
pub fn new(
|
||||
reset: CTRL,
|
||||
dio1: WAIT,
|
||||
busy: WAIT,
|
||||
rf_switch_rx: Option<CTRL>,
|
||||
rf_switch_tx: Option<CTRL>,
|
||||
) -> Result<Self, RadioError> {
|
||||
Ok(Self {
|
||||
reset,
|
||||
dio1,
|
||||
busy,
|
||||
rf_switch_rx,
|
||||
rf_switch_tx,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl<CTRL, WAIT> InterfaceVariant for GenericSx126xInterfaceVariant<CTRL, WAIT>
|
||||
where
|
||||
CTRL: OutputPin,
|
||||
WAIT: Wait,
|
||||
{
|
||||
async fn reset(&mut self, delay: &mut impl DelayNs) -> Result<(), RadioError> {
|
||||
delay.delay_ms(10).await;
|
||||
self.reset.set_low().map_err(|_| Reset)?;
|
||||
delay.delay_ms(20).await;
|
||||
self.reset.set_high().map_err(|_| Reset)?;
|
||||
delay.delay_ms(10).await;
|
||||
Ok(())
|
||||
}
|
||||
async fn wait_on_busy(&mut self) -> Result<(), RadioError> {
|
||||
self.busy.wait_for_low().await.map_err(|_| Busy)
|
||||
}
|
||||
async fn await_irq(&mut self) -> Result<(), RadioError> {
|
||||
self.dio1.wait_for_high().await.map_err(|_| DIO1)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn enable_rf_switch_rx(&mut self) -> Result<(), RadioError> {
|
||||
match &mut self.rf_switch_tx {
|
||||
Some(pin) => pin.set_low().map_err(|_| RfSwitchTx)?,
|
||||
None => (),
|
||||
};
|
||||
match &mut self.rf_switch_rx {
|
||||
Some(pin) => pin.set_high().map_err(|_| RfSwitchRx),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
async fn enable_rf_switch_tx(&mut self) -> Result<(), RadioError> {
|
||||
match &mut self.rf_switch_rx {
|
||||
Some(pin) => pin.set_low().map_err(|_| RfSwitchRx)?,
|
||||
None => (),
|
||||
};
|
||||
match &mut self.rf_switch_tx {
|
||||
Some(pin) => pin.set_high().map_err(|_| RfSwitchTx),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
async fn disable_rf_switch(&mut self) -> Result<(), RadioError> {
|
||||
match &mut self.rf_switch_rx {
|
||||
Some(pin) => pin.set_low().map_err(|_| RfSwitchRx)?,
|
||||
None => (),
|
||||
};
|
||||
match &mut self.rf_switch_tx {
|
||||
Some(pin) => pin.set_low().map_err(|_| RfSwitchTx),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+365
@@ -0,0 +1,365 @@
|
||||
#![no_std]
|
||||
#![allow(async_fn_in_trait)]
|
||||
#![warn(missing_docs)]
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
#[cfg(feature = "lorawan-radio")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "lorawan-radio")))]
|
||||
/// Provides an implementation of the async LoRaWAN device trait.
|
||||
pub mod lorawan_radio;
|
||||
|
||||
/// The read/write interface between an embedded framework/MCU combination and a LoRa chip
|
||||
pub(crate) mod interface;
|
||||
/// InterfaceVariant implementations using `embedded-hal`.
|
||||
pub mod iv;
|
||||
/// Parameters used across the lora-phy crate to support various use cases
|
||||
pub mod mod_params;
|
||||
/// Traits implemented externally or internally to support control of LoRa chips
|
||||
pub mod mod_traits;
|
||||
/// Specific implementation to support Semtech Sx126x chips
|
||||
pub mod sx126x;
|
||||
/// Specific implementation to support Semtech Sx127x chips
|
||||
pub mod sx127x;
|
||||
|
||||
pub use crate::mod_params::RxMode;
|
||||
|
||||
pub use embedded_hal_async::delay::DelayNs;
|
||||
use interface::*;
|
||||
use mod_params::*;
|
||||
use mod_traits::*;
|
||||
|
||||
/// Provides the physical layer API to support LoRa chips
|
||||
pub struct LoRa<RK, DLY>
|
||||
where
|
||||
RK: RadioKind,
|
||||
DLY: DelayNs,
|
||||
{
|
||||
radio_kind: RK,
|
||||
delay: DLY,
|
||||
radio_mode: RadioMode,
|
||||
enable_public_network: bool,
|
||||
cold_start: bool,
|
||||
calibrate_image: bool,
|
||||
}
|
||||
|
||||
impl<RK, DLY> LoRa<RK, DLY>
|
||||
where
|
||||
RK: RadioKind,
|
||||
DLY: DelayNs,
|
||||
{
|
||||
/// Build and return a new instance of the LoRa physical layer API to control an initialized LoRa radio
|
||||
pub async fn new(radio_kind: RK, enable_public_network: bool, delay: DLY) -> Result<Self, RadioError> {
|
||||
let mut lora = Self {
|
||||
radio_kind,
|
||||
delay,
|
||||
radio_mode: RadioMode::Sleep,
|
||||
enable_public_network,
|
||||
cold_start: true,
|
||||
calibrate_image: true,
|
||||
};
|
||||
lora.init().await?;
|
||||
|
||||
Ok(lora)
|
||||
}
|
||||
|
||||
/// Wait for an IRQ event to occur
|
||||
pub async fn wait_for_irq(&mut self) -> Result<(), RadioError> {
|
||||
self.radio_kind.await_irq().await
|
||||
}
|
||||
|
||||
/// Process an IRQ event and return the new state of the radio
|
||||
pub async fn process_irq_event(&mut self) -> Result<Option<IrqState>, RadioError> {
|
||||
self.radio_kind.process_irq_event(self.radio_mode, None, false).await
|
||||
}
|
||||
|
||||
/// Create modulation parameters for a communication channel
|
||||
pub fn create_modulation_params(
|
||||
&mut self,
|
||||
spreading_factor: SpreadingFactor,
|
||||
bandwidth: Bandwidth,
|
||||
coding_rate: CodingRate,
|
||||
frequency_in_hz: u32,
|
||||
) -> Result<ModulationParams, RadioError> {
|
||||
self.radio_kind
|
||||
.create_modulation_params(spreading_factor, bandwidth, coding_rate, frequency_in_hz)
|
||||
}
|
||||
|
||||
/// Create packet parameters for a send operation on a communication channel
|
||||
pub fn create_tx_packet_params(
|
||||
&mut self,
|
||||
preamble_length: u16,
|
||||
implicit_header: bool,
|
||||
crc_on: bool,
|
||||
iq_inverted: bool,
|
||||
modulation_params: &ModulationParams,
|
||||
) -> Result<PacketParams, RadioError> {
|
||||
self.radio_kind.create_packet_params(
|
||||
preamble_length,
|
||||
implicit_header,
|
||||
0,
|
||||
crc_on,
|
||||
iq_inverted,
|
||||
modulation_params,
|
||||
)
|
||||
}
|
||||
|
||||
/// Create packet parameters for a receive operation on a communication channel
|
||||
pub fn create_rx_packet_params(
|
||||
&mut self,
|
||||
preamble_length: u16,
|
||||
implicit_header: bool,
|
||||
max_payload_length: u8,
|
||||
crc_on: bool,
|
||||
iq_inverted: bool,
|
||||
modulation_params: &ModulationParams,
|
||||
) -> Result<PacketParams, RadioError> {
|
||||
self.radio_kind.create_packet_params(
|
||||
preamble_length,
|
||||
implicit_header,
|
||||
max_payload_length,
|
||||
crc_on,
|
||||
iq_inverted,
|
||||
modulation_params,
|
||||
)
|
||||
}
|
||||
|
||||
/// Initialize a Semtech chip as the radio for LoRa physical layer communications
|
||||
pub async fn init(&mut self) -> Result<(), RadioError> {
|
||||
self.cold_start = true;
|
||||
self.radio_kind.reset(&mut self.delay).await?;
|
||||
self.radio_kind.ensure_ready(self.radio_mode).await?;
|
||||
self.radio_kind.set_standby().await?;
|
||||
self.radio_mode = RadioMode::Standby;
|
||||
self.do_cold_start().await
|
||||
}
|
||||
|
||||
async fn do_cold_start(&mut self) -> Result<(), RadioError> {
|
||||
self.radio_kind.init_lora(self.enable_public_network).await?;
|
||||
self.radio_kind.set_tx_power_and_ramp_time(0, None, false).await?;
|
||||
self.radio_kind.set_irq_params(Some(self.radio_mode)).await?;
|
||||
self.cold_start = false;
|
||||
self.calibrate_image = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Place the LoRa physical layer in standby mode
|
||||
pub async fn enter_standby(&mut self) -> Result<(), RadioError> {
|
||||
self.radio_kind.set_standby().await
|
||||
}
|
||||
|
||||
/// Place the LoRa physical layer in low power mode, specifying cold or warm start (if the Semtech chip supports it)
|
||||
pub async fn sleep(&mut self, warm_start_if_possible: bool) -> Result<(), RadioError> {
|
||||
if self.radio_mode != RadioMode::Sleep {
|
||||
self.radio_kind.ensure_ready(self.radio_mode).await?;
|
||||
self.radio_kind
|
||||
.set_sleep(warm_start_if_possible, &mut self.delay)
|
||||
.await?;
|
||||
if !warm_start_if_possible {
|
||||
self.cold_start = true;
|
||||
}
|
||||
self.radio_mode = RadioMode::Sleep;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Prepare the Semtech chip for a send operation
|
||||
pub async fn prepare_for_tx(
|
||||
&mut self,
|
||||
mdltn_params: &ModulationParams,
|
||||
tx_pkt_params: &mut PacketParams,
|
||||
output_power: i32,
|
||||
buffer: &[u8],
|
||||
) -> Result<(), RadioError> {
|
||||
self.prepare_modem(mdltn_params).await?;
|
||||
|
||||
self.radio_kind.set_modulation_params(mdltn_params).await?;
|
||||
self.radio_kind
|
||||
.set_tx_power_and_ramp_time(output_power, Some(mdltn_params), true)
|
||||
.await?;
|
||||
self.radio_kind.ensure_ready(self.radio_mode).await?;
|
||||
if self.radio_mode != RadioMode::Standby {
|
||||
self.radio_kind.set_standby().await?;
|
||||
self.radio_mode = RadioMode::Standby;
|
||||
}
|
||||
|
||||
tx_pkt_params.set_payload_length(buffer.len())?;
|
||||
self.radio_kind.set_packet_params(tx_pkt_params).await?;
|
||||
self.radio_kind.set_channel(mdltn_params.frequency_in_hz).await?;
|
||||
self.radio_kind.set_payload(buffer).await?;
|
||||
self.radio_mode = RadioMode::Transmit;
|
||||
self.radio_kind.set_irq_params(Some(self.radio_mode)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Execute a send operation
|
||||
pub async fn tx(&mut self) -> Result<(), RadioError> {
|
||||
if let RadioMode::Transmit = self.radio_mode {
|
||||
self.radio_kind.do_tx().await?;
|
||||
loop {
|
||||
self.wait_for_irq().await?;
|
||||
match self.radio_kind.process_irq_event(self.radio_mode, None, true).await {
|
||||
Ok(Some(IrqState::Done | IrqState::PreambleReceived)) => {
|
||||
self.radio_mode = RadioMode::Standby;
|
||||
return Ok(());
|
||||
}
|
||||
Ok(None) => continue,
|
||||
Err(err) => {
|
||||
self.radio_kind.ensure_ready(self.radio_mode).await?;
|
||||
self.radio_kind.set_standby().await?;
|
||||
self.radio_mode = RadioMode::Standby;
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(RadioError::InvalidRadioMode)
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare radio to receive a frame in either single or continuous packet mode.
|
||||
/// Notes:
|
||||
/// * sx126x SetRx(0 < timeout < MAX) will listen util LoRa packet header is detected,
|
||||
/// therefore we only use 0 (Single Mode) and MAX (continuous) values.
|
||||
/// TODO: Find a way to express timeout for sx126x, allowing waiting for packet upto 262s
|
||||
/// TODO: Allow DutyCycle as well?
|
||||
pub async fn prepare_for_rx(
|
||||
&mut self,
|
||||
listen_mode: RxMode,
|
||||
mdltn_params: &ModulationParams,
|
||||
rx_pkt_params: &PacketParams,
|
||||
) -> Result<(), RadioError> {
|
||||
defmt::trace!("RX mode: {}", listen_mode);
|
||||
self.prepare_modem(mdltn_params).await?;
|
||||
|
||||
self.radio_kind.set_modulation_params(mdltn_params).await?;
|
||||
self.radio_kind.set_packet_params(rx_pkt_params).await?;
|
||||
self.radio_kind.set_channel(mdltn_params.frequency_in_hz).await?;
|
||||
self.radio_mode = listen_mode.into();
|
||||
self.radio_kind.set_irq_params(Some(self.radio_mode)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start receiving and wait for result
|
||||
pub async fn rx(
|
||||
&mut self,
|
||||
packet_params: &PacketParams,
|
||||
receiving_buffer: &mut [u8],
|
||||
) -> Result<(u8, PacketStatus), RadioError> {
|
||||
if let RadioMode::Receive(listen_mode) = self.radio_mode {
|
||||
self.radio_kind.do_rx(listen_mode).await?;
|
||||
loop {
|
||||
self.wait_for_irq().await?;
|
||||
match self.radio_kind.process_irq_event(self.radio_mode, None, true).await {
|
||||
Ok(Some(actual_state)) => match actual_state {
|
||||
IrqState::PreambleReceived => continue,
|
||||
IrqState::Done => {
|
||||
let received_len = self.radio_kind.get_rx_payload(packet_params, receiving_buffer).await?;
|
||||
let rx_pkt_status = self.radio_kind.get_rx_packet_status().await?;
|
||||
return Ok((received_len, rx_pkt_status));
|
||||
}
|
||||
},
|
||||
Ok(None) => continue,
|
||||
Err(err) => {
|
||||
// if in rx continuous mode, allow the caller to determine whether to keep receiving
|
||||
if self.radio_mode != RadioMode::Receive(RxMode::Continuous) {
|
||||
self.radio_kind.ensure_ready(self.radio_mode).await?;
|
||||
self.radio_kind.set_standby().await?;
|
||||
self.radio_mode = RadioMode::Standby;
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Err(RadioError::InvalidRadioMode)
|
||||
}
|
||||
}
|
||||
|
||||
/// Prepare the Semtech chip for a channel activity detection operation
|
||||
pub async fn prepare_for_cad(&mut self, mdltn_params: &ModulationParams) -> Result<(), RadioError> {
|
||||
self.prepare_modem(mdltn_params).await?;
|
||||
|
||||
self.radio_kind.set_modulation_params(mdltn_params).await?;
|
||||
self.radio_kind.set_channel(mdltn_params.frequency_in_hz).await?;
|
||||
self.radio_mode = RadioMode::ChannelActivityDetection;
|
||||
self.radio_kind.set_irq_params(Some(self.radio_mode)).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Start channel activity detection operation and return the result
|
||||
pub async fn cad(&mut self, mdltn_params: &ModulationParams) -> Result<bool, RadioError> {
|
||||
if self.radio_mode == RadioMode::ChannelActivityDetection {
|
||||
self.radio_kind.do_cad(mdltn_params).await?;
|
||||
self.wait_for_irq().await?;
|
||||
let mut cad_activity_detected = false;
|
||||
match self
|
||||
.radio_kind
|
||||
.process_irq_event(self.radio_mode, Some(&mut cad_activity_detected), true)
|
||||
.await
|
||||
{
|
||||
Ok(Some(IrqState::Done)) => Ok(cad_activity_detected),
|
||||
Err(err) => {
|
||||
self.radio_kind.ensure_ready(self.radio_mode).await?;
|
||||
self.radio_kind.set_standby().await?;
|
||||
self.radio_mode = RadioMode::Standby;
|
||||
Err(err)
|
||||
}
|
||||
Ok(_) => unreachable!(),
|
||||
}
|
||||
} else {
|
||||
Err(RadioError::InvalidRadioMode)
|
||||
}
|
||||
}
|
||||
|
||||
/// Place radio in continuous wave mode, generally for regulatory testing
|
||||
///
|
||||
/// SemTech app note AN1200.26 “Semtech LoRa FCC 15.247 Guidance” covers usage.
|
||||
///
|
||||
/// Presumes that init() is called before this function
|
||||
pub async fn continuous_wave(
|
||||
&mut self,
|
||||
mdltn_params: &ModulationParams,
|
||||
output_power: i32,
|
||||
) -> Result<(), RadioError> {
|
||||
self.prepare_modem(mdltn_params).await?;
|
||||
|
||||
let tx_pkt_params = self
|
||||
.radio_kind
|
||||
.create_packet_params(0, false, 16, false, false, mdltn_params)?;
|
||||
self.radio_kind.set_packet_params(&tx_pkt_params).await?;
|
||||
self.radio_kind.set_modulation_params(mdltn_params).await?;
|
||||
self.radio_kind
|
||||
.set_tx_power_and_ramp_time(output_power, Some(mdltn_params), true)
|
||||
.await?;
|
||||
|
||||
self.radio_kind.ensure_ready(self.radio_mode).await?;
|
||||
if self.radio_mode != RadioMode::Standby {
|
||||
self.radio_kind.set_standby().await?;
|
||||
self.radio_mode = RadioMode::Standby;
|
||||
}
|
||||
self.radio_kind.set_channel(mdltn_params.frequency_in_hz).await?;
|
||||
self.radio_mode = RadioMode::Transmit;
|
||||
self.radio_kind.set_irq_params(Some(self.radio_mode)).await?;
|
||||
self.radio_kind.set_tx_continuous_wave_mode().await
|
||||
}
|
||||
|
||||
async fn prepare_modem(&mut self, mdltn_params: &ModulationParams) -> Result<(), RadioError> {
|
||||
self.radio_kind.ensure_ready(self.radio_mode).await?;
|
||||
if self.radio_mode != RadioMode::Standby {
|
||||
self.radio_kind.set_standby().await?;
|
||||
self.radio_mode = RadioMode::Standby;
|
||||
}
|
||||
|
||||
if self.cold_start {
|
||||
self.do_cold_start().await?;
|
||||
}
|
||||
|
||||
if self.calibrate_image {
|
||||
self.radio_kind.calibrate_image(mdltn_params.frequency_in_hz).await?;
|
||||
self.calibrate_image = false;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Vendored
+177
@@ -0,0 +1,177 @@
|
||||
#![allow(missing_docs)]
|
||||
|
||||
use super::mod_params::{PacketParams, RadioError};
|
||||
use super::mod_traits::RadioKind;
|
||||
use super::{DelayNs, LoRa, RxMode};
|
||||
|
||||
use lora_modulation::BaseBandModulationParams;
|
||||
use lorawan_device::async_device::{
|
||||
radio::{PhyRxTx, RxConfig, RxMode as LorawanRxMode, RxQuality, RxStatus, TxConfig},
|
||||
Timings,
|
||||
};
|
||||
|
||||
const DEFAULT_RX_WINDOW_LEAD_TIME: u32 = 50;
|
||||
|
||||
/// LoRa radio using the physical layer API in the external lora-phy crate.
|
||||
///
|
||||
/// The const generic P is the max power the radio may be instructed to transmit at. The const
|
||||
/// generic G is the antenna gain and board loss in dBi.
|
||||
pub struct LorawanRadio<RK, DLY, const P: u8, const G: i8 = 0>
|
||||
where
|
||||
RK: RadioKind,
|
||||
DLY: DelayNs,
|
||||
{
|
||||
pub(crate) lora: LoRa<RK, DLY>,
|
||||
rx_pkt_params: Option<PacketParams>,
|
||||
rx_window_lead_time: u32,
|
||||
rx_window_buffer: u32,
|
||||
}
|
||||
|
||||
impl<RK, DLY, const P: u8, const G: i8> From<LoRa<RK, DLY>> for LorawanRadio<RK, DLY, P, G>
|
||||
where
|
||||
RK: RadioKind,
|
||||
DLY: DelayNs,
|
||||
{
|
||||
fn from(lora: LoRa<RK, DLY>) -> Self {
|
||||
Self {
|
||||
lora,
|
||||
rx_pkt_params: None,
|
||||
rx_window_lead_time: DEFAULT_RX_WINDOW_LEAD_TIME,
|
||||
rx_window_buffer: DEFAULT_RX_WINDOW_LEAD_TIME,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<RK, DLY, const P: u8, const G: i8> LorawanRadio<RK, DLY, P, G>
|
||||
where
|
||||
RK: RadioKind,
|
||||
DLY: DelayNs,
|
||||
{
|
||||
pub fn set_rx_window_lead_time(&mut self, lt: u32) {
|
||||
self.rx_window_lead_time = lt;
|
||||
}
|
||||
pub fn set_rx_window_buffer(&mut self, buffer: u32) {
|
||||
self.rx_window_buffer = buffer;
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide the timing values for boards supported by the external lora-phy crate
|
||||
impl<RK, DLY, const P: u8, const G: i8> Timings for LorawanRadio<RK, DLY, P, G>
|
||||
where
|
||||
RK: RadioKind,
|
||||
DLY: DelayNs,
|
||||
{
|
||||
fn get_rx_window_buffer(&self) -> u32 {
|
||||
self.rx_window_lead_time
|
||||
}
|
||||
|
||||
fn get_rx_window_lead_time_ms(&self) -> u32 {
|
||||
self.rx_window_lead_time
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, defmt::Format)]
|
||||
pub enum Error {
|
||||
Radio(RadioError),
|
||||
NoRxParams,
|
||||
}
|
||||
|
||||
impl From<RadioError> for Error {
|
||||
fn from(err: RadioError) -> Self {
|
||||
Error::Radio(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// Provide the LoRa physical layer rx/tx interface for boards supported by the external lora-phy
|
||||
/// crate
|
||||
impl<RK, DLY, const P: u8, const G: i8> PhyRxTx for LorawanRadio<RK, DLY, P, G>
|
||||
where
|
||||
RK: RadioKind,
|
||||
DLY: DelayNs,
|
||||
{
|
||||
type PhyError = Error;
|
||||
|
||||
const ANTENNA_GAIN: i8 = G;
|
||||
|
||||
const MAX_RADIO_POWER: u8 = P;
|
||||
|
||||
async fn tx(&mut self, config: TxConfig, buffer: &[u8]) -> Result<u32, Self::PhyError> {
|
||||
let mdltn_params = self.lora.create_modulation_params(
|
||||
config.rf.bb.sf,
|
||||
config.rf.bb.bw,
|
||||
config.rf.bb.cr,
|
||||
config.rf.frequency,
|
||||
)?;
|
||||
let mut tx_pkt_params = self
|
||||
.lora
|
||||
.create_tx_packet_params(8, false, true, false, &mdltn_params)?;
|
||||
|
||||
self.lora
|
||||
.prepare_for_tx(&mdltn_params, &mut tx_pkt_params, config.pw.into(), buffer)
|
||||
.await?;
|
||||
self.lora.tx().await?;
|
||||
Ok(0)
|
||||
}
|
||||
|
||||
async fn setup_rx(&mut self, config: RxConfig) -> Result<(), Self::PhyError> {
|
||||
let mdltn_params = self.lora.create_modulation_params(
|
||||
config.rf.bb.sf,
|
||||
config.rf.bb.bw,
|
||||
config.rf.bb.cr,
|
||||
config.rf.frequency,
|
||||
)?;
|
||||
let rx_pkt_params = self
|
||||
.lora
|
||||
.create_rx_packet_params(8, false, 255, true, true, &mdltn_params)?;
|
||||
self.lora
|
||||
.prepare_for_rx(RxMode::from(config.mode, config.rf.bb), &mdltn_params, &rx_pkt_params)
|
||||
.await?;
|
||||
self.rx_pkt_params = Some(rx_pkt_params);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rx_single(&mut self, buf: &mut [u8]) -> Result<RxStatus, Self::PhyError> {
|
||||
if let Some(rx_params) = &self.rx_pkt_params {
|
||||
match self.lora.rx(rx_params, buf).await {
|
||||
Ok((len, q)) => Ok(RxStatus::Rx(len as usize, RxQuality::new(q.rssi, q.snr as i8))),
|
||||
Err(RadioError::ReceiveTimeout) => Ok(RxStatus::RxTimeout),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
} else {
|
||||
Err(Error::NoRxParams)
|
||||
}
|
||||
}
|
||||
async fn rx_continuous(&mut self, receiving_buffer: &mut [u8]) -> Result<(usize, RxQuality), Self::PhyError> {
|
||||
if let Some(rx_params) = &self.rx_pkt_params {
|
||||
match self.lora.rx(rx_params, receiving_buffer).await {
|
||||
Ok((received_len, rx_pkt_status)) => {
|
||||
Ok((
|
||||
received_len as usize,
|
||||
RxQuality::new(rx_pkt_status.rssi, rx_pkt_status.snr as i8), // downcast snr
|
||||
))
|
||||
}
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
} else {
|
||||
Err(Error::NoRxParams)
|
||||
}
|
||||
}
|
||||
async fn low_power(&mut self) -> Result<(), Self::PhyError> {
|
||||
self.lora.sleep(false).await.map_err(|e| e.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl RxMode {
|
||||
fn from(mode: LorawanRxMode, bb: BaseBandModulationParams) -> Self {
|
||||
match mode {
|
||||
LorawanRxMode::Continuous => RxMode::Continuous,
|
||||
LorawanRxMode::Single { ms } => {
|
||||
// Since both sx126x and sx127x have a preamble-based timeout, we translate
|
||||
// the additional millisecond delay into symbols and add it to the amount of preamble symbols.
|
||||
const PREAMBLE_SYMBOLS: u16 = 13; // 12.25
|
||||
let num_symbols = PREAMBLE_SYMBOLS + bb.delay_in_symbols(ms);
|
||||
RxMode::Single(num_symbols)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Vendored
+114
@@ -0,0 +1,114 @@
|
||||
pub use lora_modulation::{Bandwidth, CodingRate, SpreadingFactor};
|
||||
|
||||
/// Errors types reported during LoRa physical layer processing
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
#[derive(Debug, defmt::Format, PartialEq)]
|
||||
#[allow(missing_docs)]
|
||||
pub enum RadioError {
|
||||
SPI,
|
||||
Reset,
|
||||
RfSwitchRx,
|
||||
RfSwitchTx,
|
||||
Busy,
|
||||
Irq,
|
||||
DIO1,
|
||||
InvalidConfiguration,
|
||||
InvalidRadioMode,
|
||||
OpError(u8),
|
||||
InvalidBaseAddress(usize, usize),
|
||||
PayloadSizeUnexpected(usize),
|
||||
PayloadSizeMismatch(usize, usize),
|
||||
UnavailableSpreadingFactor,
|
||||
UnavailableBandwidth,
|
||||
InvalidBandwidthForFrequency,
|
||||
InvalidSF6ExplicitHeaderRequest,
|
||||
InvalidOutputPowerForFrequency,
|
||||
TransmitTimeout,
|
||||
ReceiveTimeout,
|
||||
DutyCycleUnsupported,
|
||||
RngUnsupported,
|
||||
}
|
||||
|
||||
/// Status for a received packet
|
||||
#[derive(Clone, Copy)]
|
||||
#[allow(missing_docs)]
|
||||
pub struct PacketStatus {
|
||||
pub rssi: i16,
|
||||
pub snr: i16,
|
||||
}
|
||||
|
||||
/// The state of the radio
|
||||
#[derive(Clone, Copy, defmt::Format, PartialEq)]
|
||||
pub enum RadioMode {
|
||||
/// Sleep mode
|
||||
Sleep,
|
||||
/// Standby mode
|
||||
Standby,
|
||||
/// Frequency synthesis mode
|
||||
FrequencySynthesis,
|
||||
/// Transmit (TX) mode
|
||||
Transmit,
|
||||
/// Receive (RX) mode
|
||||
Receive(RxMode),
|
||||
/// Channel activity detection (CAD) mode
|
||||
ChannelActivityDetection,
|
||||
}
|
||||
|
||||
impl From<RxMode> for RadioMode {
|
||||
fn from(rx_mode: RxMode) -> Self {
|
||||
RadioMode::Receive(rx_mode)
|
||||
}
|
||||
}
|
||||
|
||||
/// Listening mode for LoRaWAN packet detection/reception
|
||||
#[derive(Clone, Copy, defmt::Format, PartialEq)]
|
||||
pub enum RxMode {
|
||||
/// Single shot Rx Mode to listen until packet preamble is detected or RxTimeout occurs.
|
||||
/// The device will stay in RX Mode until a packet is received.
|
||||
/// Preamble length as symbols is configured via following registers:
|
||||
/// sx126x: uses `SetLoRaSymbNumTimeout(0 < n < 255)` + `SetStopRxTimerOnPreamble(1)`
|
||||
/// sx127x: uses `RegSymbTimeout (4 < n < 1023)`
|
||||
// TODO: Single mode with time-based timeout is available on sx126x, but not sx127x
|
||||
Single(u16),
|
||||
/// Continuous Rx mode to listen for incoming packets continuously
|
||||
Continuous,
|
||||
/// Receive in Duty Cycle mode (NB! Not supported on sx127x)
|
||||
DutyCycle(DutyCycleParams),
|
||||
}
|
||||
|
||||
/// Modulation parameters for a send and/or receive communication channel
|
||||
pub struct ModulationParams {
|
||||
pub(crate) spreading_factor: SpreadingFactor,
|
||||
pub(crate) bandwidth: Bandwidth,
|
||||
pub(crate) coding_rate: CodingRate,
|
||||
pub(crate) low_data_rate_optimize: u8,
|
||||
pub(crate) frequency_in_hz: u32,
|
||||
}
|
||||
|
||||
/// Packet parameters for a send or receive communication channel
|
||||
pub struct PacketParams {
|
||||
pub(crate) preamble_length: u16, // number of LoRa symbols in the preamble
|
||||
pub(crate) implicit_header: bool, // if the header is explicit, it will be transmitted in the LoRa packet, but is not transmitted if the header is implicit (known fixed length)
|
||||
pub(crate) payload_length: u8,
|
||||
pub(crate) crc_on: bool,
|
||||
pub(crate) iq_inverted: bool,
|
||||
}
|
||||
|
||||
impl PacketParams {
|
||||
pub(crate) fn set_payload_length(&mut self, payload_length: usize) -> Result<(), RadioError> {
|
||||
if payload_length > 255 {
|
||||
return Err(RadioError::PayloadSizeUnexpected(payload_length));
|
||||
}
|
||||
self.payload_length = payload_length as u8;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Receive duty cycle parameters
|
||||
#[derive(Clone, Copy, defmt::Format, PartialEq)]
|
||||
pub struct DutyCycleParams {
|
||||
/// receive interval
|
||||
pub rx_time: u32,
|
||||
/// sleep interval
|
||||
pub sleep_time: u32,
|
||||
}
|
||||
Vendored
+113
@@ -0,0 +1,113 @@
|
||||
use embedded_hal_async::delay::DelayNs;
|
||||
|
||||
use crate::mod_params::*;
|
||||
|
||||
/// Functions implemented for an embedded framework for an MCU/LoRa chip combination
|
||||
/// to allow this crate to control the LoRa chip.
|
||||
pub trait InterfaceVariant {
|
||||
/// Reset the LoRa chip
|
||||
async fn reset(&mut self, delay: &mut impl DelayNs) -> Result<(), RadioError>;
|
||||
/// Wait for the LoRa chip to become available for an operation
|
||||
async fn wait_on_busy(&mut self) -> Result<(), RadioError>;
|
||||
/// Wait for the LoRa chip to indicate an event has occurred
|
||||
async fn await_irq(&mut self) -> Result<(), RadioError>;
|
||||
/// Enable an antenna used for receive operations, disabling other antennas
|
||||
async fn enable_rf_switch_rx(&mut self) -> Result<(), RadioError>;
|
||||
/// Enable an antenna used for send operations, disabling other antennas
|
||||
async fn enable_rf_switch_tx(&mut self) -> Result<(), RadioError>;
|
||||
/// Disable all antennas
|
||||
async fn disable_rf_switch(&mut self) -> Result<(), RadioError>;
|
||||
}
|
||||
|
||||
/// Specifies an IRQ processing state to run the loop to
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum IrqState {
|
||||
/// Runs the loop until after the preamble has been received
|
||||
PreambleReceived,
|
||||
/// Runs the loop until the operation is fully complete
|
||||
Done,
|
||||
}
|
||||
|
||||
/// Functions implemented for a specific kind of LoRa chip, called internally by the outward facing
|
||||
/// LoRa physical layer API
|
||||
pub trait RadioKind {
|
||||
/// Initialize lora radio
|
||||
async fn init_lora(&mut self, is_public_network: bool) -> Result<(), RadioError>;
|
||||
/// Create modulation parameters specific to the LoRa chip kind and type
|
||||
fn create_modulation_params(
|
||||
&self,
|
||||
spreading_factor: SpreadingFactor,
|
||||
bandwidth: Bandwidth,
|
||||
coding_rate: CodingRate,
|
||||
frequency_in_hz: u32,
|
||||
) -> Result<ModulationParams, RadioError>;
|
||||
/// Create packet parameters specific to the LoRa chip kind and type
|
||||
fn create_packet_params(
|
||||
&self,
|
||||
preamble_length: u16,
|
||||
implicit_header: bool,
|
||||
payload_length: u8,
|
||||
crc_on: bool,
|
||||
iq_inverted: bool,
|
||||
modulation_params: &ModulationParams,
|
||||
) -> Result<PacketParams, RadioError>;
|
||||
/// Reset the loRa chip
|
||||
async fn reset(&mut self, delay: &mut impl DelayNs) -> Result<(), RadioError>;
|
||||
/// Ensure the LoRa chip is in the appropriate state to allow operation requests
|
||||
async fn ensure_ready(&mut self, mode: RadioMode) -> Result<(), RadioError>;
|
||||
/// Place the LoRa chip in standby mode
|
||||
async fn set_standby(&mut self) -> Result<(), RadioError>;
|
||||
/// Place the LoRa chip in power-saving mode
|
||||
async fn set_sleep(&mut self, warm_start_if_possible: bool, delay: &mut impl DelayNs) -> Result<(), RadioError>;
|
||||
/// Set the LoRa chip send and receive buffer base addresses
|
||||
async fn set_tx_rx_buffer_base_address(
|
||||
&mut self,
|
||||
tx_base_addr: usize,
|
||||
rx_base_addr: usize,
|
||||
) -> Result<(), RadioError>;
|
||||
/// Perform any necessary LoRa chip power setup prior to a send operation
|
||||
async fn set_tx_power_and_ramp_time(
|
||||
&mut self,
|
||||
output_power: i32,
|
||||
mdltn_params: Option<&ModulationParams>,
|
||||
is_tx_prep: bool,
|
||||
) -> Result<(), RadioError>;
|
||||
/// Set the LoRa chip modulation parameters prior to using a communication channel
|
||||
async fn set_modulation_params(&mut self, mdltn_params: &ModulationParams) -> Result<(), RadioError>;
|
||||
/// Set the LoRa chip packet parameters prior to sending or receiving packets
|
||||
async fn set_packet_params(&mut self, pkt_params: &PacketParams) -> Result<(), RadioError>;
|
||||
/// Set the LoRa chip to support a given communication channel frequency
|
||||
async fn calibrate_image(&mut self, frequency_in_hz: u32) -> Result<(), RadioError>;
|
||||
/// Set the frequency for a communication channel
|
||||
async fn set_channel(&mut self, frequency_in_hz: u32) -> Result<(), RadioError>;
|
||||
/// Set a payload for a subsequent send operation
|
||||
async fn set_payload(&mut self, payload: &[u8]) -> Result<(), RadioError>;
|
||||
/// Perform a transmit operation
|
||||
async fn do_tx(&mut self) -> Result<(), RadioError>;
|
||||
/// Set up to perform a receive operation (single-shot, continuous, or duty cycle)
|
||||
async fn do_rx(&mut self, rx_mode: RxMode) -> Result<(), RadioError>;
|
||||
/// Get an available packet made available as the result of a receive operation
|
||||
async fn get_rx_payload(
|
||||
&mut self,
|
||||
rx_pkt_params: &PacketParams,
|
||||
receiving_buffer: &mut [u8],
|
||||
) -> Result<u8, RadioError>;
|
||||
/// Get the RSSI and SNR for the packet made available as the result of a receive operation
|
||||
async fn get_rx_packet_status(&mut self) -> Result<PacketStatus, RadioError>;
|
||||
/// Perform a channel activity detection operation
|
||||
async fn do_cad(&mut self, mdltn_params: &ModulationParams) -> Result<(), RadioError>;
|
||||
/// Set the LoRa chip to provide notification of specific events based on radio state
|
||||
async fn set_irq_params(&mut self, radio_mode: Option<RadioMode>) -> Result<(), RadioError>;
|
||||
/// Set the LoRa chip into the TxContinuousWave mode
|
||||
async fn set_tx_continuous_wave_mode(&mut self) -> Result<(), RadioError>;
|
||||
|
||||
/// Await for an IRQ event. This is droppable and thus safe to use in a select branch.
|
||||
async fn await_irq(&mut self) -> Result<(), RadioError>;
|
||||
/// Process LoRa radio IRQs
|
||||
async fn process_irq_event(
|
||||
&mut self,
|
||||
radio_mode: RadioMode,
|
||||
cad_activity_detected: Option<&mut bool>,
|
||||
clear_interrupts: bool,
|
||||
) -> Result<Option<IrqState>, RadioError>;
|
||||
}
|
||||
Vendored
+969
@@ -0,0 +1,969 @@
|
||||
mod radio_kind_params;
|
||||
|
||||
use defmt::debug;
|
||||
use embedded_hal_async::delay::DelayNs;
|
||||
use embedded_hal_async::spi::*;
|
||||
pub use radio_kind_params::TcxoCtrlVoltage;
|
||||
use radio_kind_params::*;
|
||||
|
||||
use crate::mod_params::*;
|
||||
use crate::mod_traits::IrqState;
|
||||
use crate::{InterfaceVariant, RadioKind, SpiInterface};
|
||||
mod variant;
|
||||
pub use variant::*;
|
||||
|
||||
// Syncwords for public and private networks
|
||||
const LORA_MAC_PUBLIC_SYNCWORD: u16 = 0x3444; // corresponds to sx127x 0x34
|
||||
const LORA_MAC_PRIVATE_SYNCWORD: u16 = 0x1424; // corresponds to sx127x 0x12
|
||||
|
||||
// Maximum number of registers that can be added to the retention list
|
||||
const MAX_NUMBER_REGS_IN_RETENTION: u8 = 4;
|
||||
|
||||
// Internal frequency of the radio
|
||||
const SX126X_XTAL_FREQ: u32 = 32000000;
|
||||
|
||||
// Scaling factor used to perform fixed-point operations
|
||||
const SX126X_PLL_STEP_SHIFT_AMOUNT: u32 = 14;
|
||||
|
||||
// PLL step - scaled with SX126X_PLL_STEP_SHIFT_AMOUNT
|
||||
const SX126X_PLL_STEP_SCALED: u32 = SX126X_XTAL_FREQ >> (25 - SX126X_PLL_STEP_SHIFT_AMOUNT);
|
||||
|
||||
// Maximum value for parameter symbNum
|
||||
const SX126X_MAX_LORA_SYMB_NUM_TIMEOUT: u8 = 248;
|
||||
|
||||
// Time required for the TCXO to wakeup [ms].
|
||||
const BRD_TCXO_WAKEUP_TIME: u32 = 10;
|
||||
|
||||
// SetRx timeout argument for enabling continuous mode
|
||||
const RX_CONTINUOUS_TIMEOUT: u32 = 0xffffff;
|
||||
|
||||
/// Power amplifier selection
|
||||
#[repr(u8)]
|
||||
pub enum DeviceSel {
|
||||
/// Low power, power amplifier, used by sx1261
|
||||
LowPowerPA = 1,
|
||||
/// High power, power amplifier, used by sx1262
|
||||
HighPowerPA = 0,
|
||||
}
|
||||
|
||||
/// Configuration for SX126x-based boards
|
||||
pub struct Config<C: Sx126xVariant + Sized> {
|
||||
/// LoRa chip variant on this board
|
||||
pub chip: C,
|
||||
/// Board is using TCXO (once enabled DIO3 cannot be used as IRQ)
|
||||
pub tcxo_ctrl: Option<TcxoCtrlVoltage>,
|
||||
/// Whether board is using optional DCDC in addition to LDO
|
||||
pub use_dcdc: bool,
|
||||
/// Whether to boost receive
|
||||
pub rx_boost: bool,
|
||||
}
|
||||
|
||||
/// Base for the RadioKind implementation for the LoRa chip kind and board type
|
||||
pub struct Sx126x<SPI, IV, C: Sx126xVariant + Sized> {
|
||||
intf: SpiInterface<SPI, IV>,
|
||||
config: Config<C>,
|
||||
}
|
||||
|
||||
impl<SPI, IV, C> Sx126x<SPI, IV, C>
|
||||
where
|
||||
SPI: SpiDevice<u8>,
|
||||
IV: InterfaceVariant,
|
||||
C: Sx126xVariant,
|
||||
{
|
||||
/// Create an instance of the RadioKind implementation for the LoRa chip kind and board type
|
||||
pub fn new(spi: SPI, iv: IV, config: Config<C>) -> Self {
|
||||
let intf = SpiInterface::new(spi, iv);
|
||||
Self { intf, config }
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
async fn add_register_to_retention_list(&mut self, register: Register) -> Result<(), RadioError> {
|
||||
let mut buffer = [0x00u8; (1 + (2 * MAX_NUMBER_REGS_IN_RETENTION)) as usize];
|
||||
|
||||
// Read the address and registers already added to the list
|
||||
self.intf
|
||||
.read(
|
||||
&[
|
||||
OpCode::ReadRegister.value(),
|
||||
Register::RetentionList.addr1(),
|
||||
Register::RetentionList.addr2(),
|
||||
0x00u8,
|
||||
],
|
||||
&mut buffer,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let number_of_registers = buffer[0];
|
||||
for i in 0..number_of_registers {
|
||||
if register.addr1() == buffer[(1 + (2 * i)) as usize] && register.addr2() == buffer[(2 + (2 * i)) as usize]
|
||||
{
|
||||
return Ok(()); // register already in list
|
||||
}
|
||||
}
|
||||
|
||||
if number_of_registers < MAX_NUMBER_REGS_IN_RETENTION {
|
||||
buffer[0] += 1; // increment number of registers
|
||||
|
||||
buffer[(1 + (2 * number_of_registers)) as usize] = register.addr1();
|
||||
buffer[(2 + (2 * number_of_registers)) as usize] = register.addr2();
|
||||
|
||||
let register = [
|
||||
OpCode::WriteRegister.value(),
|
||||
Register::RetentionList.addr1(),
|
||||
Register::RetentionList.addr2(),
|
||||
];
|
||||
self.intf.write_with_payload(®ister, &buffer, false).await
|
||||
} else {
|
||||
Err(RadioError::InvalidConfiguration)
|
||||
}
|
||||
}
|
||||
|
||||
async fn update_retention_list(&mut self) -> Result<(), RadioError> {
|
||||
self.add_register_to_retention_list(Register::RxGain).await?;
|
||||
self.add_register_to_retention_list(Register::TxModulation).await
|
||||
}
|
||||
|
||||
// Set the number of symbols the radio will wait to detect a reception
|
||||
async fn set_lora_symbol_num_timeout(&mut self, symbol_num: u16) -> Result<(), RadioError> {
|
||||
let mut exp = 0u8;
|
||||
let mut mant = ((symbol_num.min(SX126X_MAX_LORA_SYMB_NUM_TIMEOUT.into()) + 1) >> 1) as u8;
|
||||
while mant > 31 {
|
||||
mant = (mant + 3) >> 2;
|
||||
exp += 1;
|
||||
}
|
||||
let val: u8 = mant << ((2 * exp) + 1);
|
||||
self.intf
|
||||
.write(&[OpCode::SetLoRaSymbTimeout.value(), val], false)
|
||||
.await?;
|
||||
|
||||
if symbol_num > 0 {
|
||||
let val = exp + (mant << 3);
|
||||
let buf = [
|
||||
OpCode::WriteRegister.value(),
|
||||
Register::SynchTimeout.addr1(),
|
||||
Register::SynchTimeout.addr2(),
|
||||
val,
|
||||
];
|
||||
self.intf.write(&buf, false).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_pa_config(&mut self, pa_duty_cycle: u8, hp_max: u8, device_sel: DeviceSel) -> Result<(), RadioError> {
|
||||
const PA_LUT_RESERVED: u8 = 0x01;
|
||||
let op_code_and_pa_config = [
|
||||
OpCode::SetPAConfig.value(),
|
||||
pa_duty_cycle,
|
||||
hp_max,
|
||||
device_sel as u8,
|
||||
PA_LUT_RESERVED,
|
||||
];
|
||||
self.intf.write(&op_code_and_pa_config, false).await
|
||||
}
|
||||
|
||||
fn timeout_1(timeout: u32) -> u8 {
|
||||
((timeout >> 16) & 0xFF) as u8
|
||||
}
|
||||
fn timeout_2(timeout: u32) -> u8 {
|
||||
((timeout >> 8) & 0xFF) as u8
|
||||
}
|
||||
fn timeout_3(timeout: u32) -> u8 {
|
||||
(timeout & 0xFF) as u8
|
||||
}
|
||||
|
||||
fn convert_freq_in_hz_to_pll_step(freq_in_hz: u32) -> u32 {
|
||||
// Get integer and fractional parts of the frequency computed with a PLL step scaled value
|
||||
let steps_int = freq_in_hz / SX126X_PLL_STEP_SCALED;
|
||||
let steps_frac = freq_in_hz - (steps_int * SX126X_PLL_STEP_SCALED);
|
||||
|
||||
(steps_int << SX126X_PLL_STEP_SHIFT_AMOUNT)
|
||||
+ (((steps_frac << SX126X_PLL_STEP_SHIFT_AMOUNT) + (SX126X_PLL_STEP_SCALED >> 1)) / SX126X_PLL_STEP_SCALED)
|
||||
}
|
||||
}
|
||||
|
||||
impl<SPI, IV, C> RadioKind for Sx126x<SPI, IV, C>
|
||||
where
|
||||
SPI: SpiDevice<u8>,
|
||||
IV: InterfaceVariant,
|
||||
C: Sx126xVariant,
|
||||
{
|
||||
async fn init_lora(&mut self, is_public_network: bool) -> Result<(), RadioError> {
|
||||
// DC-DC regulator setup (default is LDO)
|
||||
if self.config.use_dcdc {
|
||||
let reg_data = [OpCode::SetRegulatorMode.value(), RegulatorMode::UseDCDC.value()];
|
||||
self.intf.write(®_data, false).await?;
|
||||
}
|
||||
// DIO2 acting as RF Switch (default is DIO2 as IRQ)
|
||||
if self.config.chip.use_dio2_as_rfswitch() {
|
||||
let cmd = [
|
||||
OpCode::SetDIO2AsRfSwitchCtrl.value(),
|
||||
self.config.chip.use_dio2_as_rfswitch() as u8,
|
||||
];
|
||||
self.intf.write(&cmd, false).await?;
|
||||
}
|
||||
|
||||
// DIO3 acting as TCXO controller (default is DIO3 as IRQ)
|
||||
if let Some(voltage) = self.config.tcxo_ctrl {
|
||||
// When TCXO is used, XOSC_START_ERR flag is raised at POR or at
|
||||
// wake-up from Sleep mode in cold-start condition. This is an
|
||||
// expected behaviour since chip is not yet aware of being clocked
|
||||
// by TCXO and therefore this should be initially cleared manually.
|
||||
let mut buf = [0u8; 2];
|
||||
let _ = self
|
||||
.intf
|
||||
.read_with_status(&[OpCode::ClearDeviceErrors.value()], &mut buf)
|
||||
.await?;
|
||||
|
||||
// Each unit is 15.625uS (which is 1/64th ms)
|
||||
let timeout = BRD_TCXO_WAKEUP_TIME << 6;
|
||||
let op_code_and_tcxo_control = [
|
||||
OpCode::SetTCXOMode.value(),
|
||||
voltage.value() & 0x07,
|
||||
Self::timeout_1(timeout),
|
||||
Self::timeout_2(timeout),
|
||||
Self::timeout_3(timeout),
|
||||
];
|
||||
self.intf.write(&op_code_and_tcxo_control, false).await?;
|
||||
// Re-run calibration now that chip knows that it's running from TCXO
|
||||
self.intf
|
||||
.write(&[OpCode::Calibrate.value(), 0b0111_1111], false)
|
||||
.await?;
|
||||
self.intf.iv.wait_on_busy().await?;
|
||||
}
|
||||
|
||||
// Enable LoRa packet engine...
|
||||
self.intf
|
||||
.write(&[OpCode::SetPacketType.value(), PacketType::LoRa.value()], false)
|
||||
.await?;
|
||||
let word = match is_public_network {
|
||||
true => u16::to_be_bytes(LORA_MAC_PUBLIC_SYNCWORD),
|
||||
false => u16::to_be_bytes(LORA_MAC_PRIVATE_SYNCWORD),
|
||||
};
|
||||
// ...and network syncword
|
||||
let lora_syncword_set = [
|
||||
OpCode::WriteRegister.value(),
|
||||
Register::LoRaSyncword.addr1(),
|
||||
Register::LoRaSyncword.addr2(),
|
||||
word[0],
|
||||
word[1],
|
||||
];
|
||||
self.intf.write(&lora_syncword_set, false).await?;
|
||||
|
||||
self.set_tx_rx_buffer_base_address(0, 0).await?;
|
||||
// Update register list to support warm starts from sleep mode
|
||||
self.update_retention_list().await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_modulation_params(
|
||||
&self,
|
||||
spreading_factor: SpreadingFactor,
|
||||
bandwidth: Bandwidth,
|
||||
coding_rate: CodingRate,
|
||||
frequency_in_hz: u32,
|
||||
) -> Result<ModulationParams, RadioError> {
|
||||
// Parameter validation
|
||||
spreading_factor_value(spreading_factor)?;
|
||||
bandwidth_value(bandwidth)?;
|
||||
coding_rate_value(coding_rate)?;
|
||||
if ((bandwidth == Bandwidth::_250KHz) || (bandwidth == Bandwidth::_500KHz)) && (frequency_in_hz < 400_000_000) {
|
||||
return Err(RadioError::InvalidBandwidthForFrequency);
|
||||
}
|
||||
|
||||
let mut low_data_rate_optimize = 0x00u8;
|
||||
if (((spreading_factor == SpreadingFactor::_11) || (spreading_factor == SpreadingFactor::_12))
|
||||
&& (bandwidth == Bandwidth::_125KHz))
|
||||
|| ((spreading_factor == SpreadingFactor::_12) && (bandwidth == Bandwidth::_250KHz))
|
||||
{
|
||||
low_data_rate_optimize = 0x01u8;
|
||||
}
|
||||
Ok(ModulationParams {
|
||||
spreading_factor,
|
||||
bandwidth,
|
||||
coding_rate,
|
||||
low_data_rate_optimize,
|
||||
frequency_in_hz,
|
||||
})
|
||||
}
|
||||
|
||||
fn create_packet_params(
|
||||
&self,
|
||||
mut preamble_length: u16,
|
||||
implicit_header: bool,
|
||||
payload_length: u8,
|
||||
crc_on: bool,
|
||||
iq_inverted: bool,
|
||||
modulation_params: &ModulationParams,
|
||||
) -> Result<PacketParams, RadioError> {
|
||||
if ((modulation_params.spreading_factor == SpreadingFactor::_5)
|
||||
|| (modulation_params.spreading_factor == SpreadingFactor::_6))
|
||||
&& (preamble_length < 12)
|
||||
{
|
||||
preamble_length = 12;
|
||||
}
|
||||
|
||||
Ok(PacketParams {
|
||||
preamble_length,
|
||||
implicit_header,
|
||||
payload_length,
|
||||
crc_on,
|
||||
iq_inverted,
|
||||
})
|
||||
}
|
||||
|
||||
async fn reset(&mut self, delay: &mut impl DelayNs) -> Result<(), RadioError> {
|
||||
self.intf.iv.reset(delay).await
|
||||
}
|
||||
|
||||
// Wakeup the radio if it is in Sleep or ReceiveDutyCycle mode; otherwise, ensure it is not busy.
|
||||
async fn ensure_ready(&mut self, mode: RadioMode) -> Result<(), RadioError> {
|
||||
match mode {
|
||||
RadioMode::Sleep | RadioMode::Receive(RxMode::DutyCycle(_)) => {
|
||||
let op_code_and_null = [OpCode::GetStatus.value(), 0x00u8];
|
||||
self.intf.write(&op_code_and_null, false).await?;
|
||||
}
|
||||
_ => self.intf.iv.wait_on_busy().await?,
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Use standby mode RC (not XOSC).
|
||||
async fn set_standby(&mut self) -> Result<(), RadioError> {
|
||||
let op_code_and_standby_mode = [OpCode::SetStandby.value(), StandbyMode::RC.value()];
|
||||
self.intf.write(&op_code_and_standby_mode, false).await?;
|
||||
self.intf.iv.disable_rf_switch().await
|
||||
}
|
||||
|
||||
async fn set_sleep(&mut self, warm_start_if_possible: bool, delay: &mut impl DelayNs) -> Result<(), RadioError> {
|
||||
self.intf.iv.disable_rf_switch().await?;
|
||||
let sleep_params = SleepParams {
|
||||
wakeup_rtc: false,
|
||||
reset: false,
|
||||
warm_start: warm_start_if_possible,
|
||||
};
|
||||
let op_code_and_sleep_params = [OpCode::SetSleep.value(), sleep_params.value()];
|
||||
self.intf.write(&op_code_and_sleep_params, true).await?;
|
||||
delay.delay_ms(2).await;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_tx_rx_buffer_base_address(
|
||||
&mut self,
|
||||
tx_base_addr: usize,
|
||||
rx_base_addr: usize,
|
||||
) -> Result<(), RadioError> {
|
||||
if tx_base_addr > 255 || rx_base_addr > 255 {
|
||||
return Err(RadioError::InvalidBaseAddress(tx_base_addr, rx_base_addr));
|
||||
}
|
||||
let op_code_and_base_addrs = [
|
||||
OpCode::SetBufferBaseAddress.value(),
|
||||
tx_base_addr as u8,
|
||||
rx_base_addr as u8,
|
||||
];
|
||||
self.intf.write(&op_code_and_base_addrs, false).await
|
||||
}
|
||||
|
||||
// Set parameters associated with power for a send operation. Currently, over current protection (OCP) uses the default set automatically after set_pa_config()
|
||||
// output_power desired RF output power (dBm)
|
||||
// mdltn_params needed for a power vs channel frequency validation
|
||||
// tx_boosted_if_possible not pertinent for sx126x
|
||||
// is_tx_prep indicates which ramp up time to use
|
||||
async fn set_tx_power_and_ramp_time(
|
||||
&mut self,
|
||||
output_power: i32,
|
||||
mdltn_params: Option<&ModulationParams>,
|
||||
is_tx_prep: bool,
|
||||
) -> Result<(), RadioError> {
|
||||
let tx_params_power;
|
||||
let ramp_time = match is_tx_prep {
|
||||
true => RampTime::Ramp40Us, // for instance, prior to TX or CAD
|
||||
false => RampTime::Ramp200Us, // for instance, on initialization
|
||||
};
|
||||
|
||||
match self.config.chip.get_device_sel() {
|
||||
DeviceSel::LowPowerPA => {
|
||||
const LOW_POWER_MIN: i32 = -17;
|
||||
const LOW_POWER_MAX: i32 = 15;
|
||||
// Clamp power between [-17, 15] dBm
|
||||
let txp = output_power.clamp(LOW_POWER_MIN, LOW_POWER_MAX);
|
||||
|
||||
if txp == 15 {
|
||||
if let Some(m_p) = mdltn_params {
|
||||
if m_p.frequency_in_hz < 400_000_000 {
|
||||
return Err(RadioError::InvalidOutputPowerForFrequency);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For SX1261:
|
||||
// if f < 400 MHz, paDutyCycle should not be higher than 0x04,
|
||||
// if f > 400 Mhz, paDutyCycle should not be higher than 0x07.
|
||||
// From Table 13-21: PA Operating Modes with Optimal Settings
|
||||
match txp {
|
||||
LOW_POWER_MAX => {
|
||||
self.set_pa_config(0x06, 0x00, DeviceSel::LowPowerPA).await?;
|
||||
tx_params_power = 14;
|
||||
}
|
||||
11..=14 => {
|
||||
self.set_pa_config(0x04, 0x00, DeviceSel::LowPowerPA).await?;
|
||||
tx_params_power = txp as u8;
|
||||
}
|
||||
// 10 and less
|
||||
LOW_POWER_MIN..=10 => {
|
||||
self.set_pa_config(0x01, 0x00, DeviceSel::LowPowerPA).await?;
|
||||
// table indicates 10 dBm => txp = 13, therefore we add 3 to values below 10
|
||||
tx_params_power = txp as u8 + 3;
|
||||
}
|
||||
_ => unreachable!("Invalid output power value for low power PA!"),
|
||||
}
|
||||
}
|
||||
DeviceSel::HighPowerPA => {
|
||||
const HIGH_POWER_MIN: i32 = -9;
|
||||
const HIGH_POWER_MAX: i32 = 22;
|
||||
// Clamp power between [-9, 22] dBm
|
||||
let txp = output_power.clamp(HIGH_POWER_MIN, HIGH_POWER_MAX);
|
||||
// Provide better resistance of the SX1262 Tx to antenna mismatch (see DS_SX1261-2_V1.2 datasheet chapter 15.2)
|
||||
let mut tx_clamp_cfg = [0x00u8];
|
||||
self.intf
|
||||
.read(
|
||||
&[
|
||||
OpCode::ReadRegister.value(),
|
||||
Register::TxClampCfg.addr1(),
|
||||
Register::TxClampCfg.addr2(),
|
||||
0x00u8,
|
||||
],
|
||||
&mut tx_clamp_cfg,
|
||||
)
|
||||
.await?;
|
||||
tx_clamp_cfg[0] |= 0x0F << 1;
|
||||
let register_and_tx_clamp_cfg = [
|
||||
OpCode::WriteRegister.value(),
|
||||
Register::TxClampCfg.addr1(),
|
||||
Register::TxClampCfg.addr2(),
|
||||
tx_clamp_cfg[0],
|
||||
];
|
||||
self.intf.write(®ister_and_tx_clamp_cfg, false).await?;
|
||||
|
||||
// From Table 13-21: PA Operating Modes with Optimal Settings
|
||||
match txp {
|
||||
21..=HIGH_POWER_MAX => {
|
||||
self.set_pa_config(0x04, 0x07, DeviceSel::HighPowerPA).await?;
|
||||
tx_params_power = 22;
|
||||
}
|
||||
18..=20 => {
|
||||
self.set_pa_config(0x03, 0x05, DeviceSel::HighPowerPA).await?;
|
||||
// table indicates 20 dBm => txp = 22, therefore we add 2 to this range
|
||||
tx_params_power = txp as u8 + 2;
|
||||
}
|
||||
15..=17 => {
|
||||
self.set_pa_config(0x02, 0x03, DeviceSel::HighPowerPA).await?;
|
||||
// table indicates 17 dBm => txp = 22, therefore we add 5 to this range
|
||||
tx_params_power = txp as u8 + 5;
|
||||
}
|
||||
HIGH_POWER_MIN..=14 => {
|
||||
self.set_pa_config(0x02, 0x02, DeviceSel::HighPowerPA).await?;
|
||||
// table indicates 14 dBm => txp = 22, therefore we add 8 to this range
|
||||
tx_params_power = txp as u8 + 8;
|
||||
}
|
||||
_ => {
|
||||
unreachable!("Invalid output power value for high power PA!")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let op_code_and_tx_params = [OpCode::SetTxParams.value(), tx_params_power, ramp_time.value()];
|
||||
self.intf.write(&op_code_and_tx_params, false).await
|
||||
}
|
||||
|
||||
async fn set_modulation_params(&mut self, mdltn_params: &ModulationParams) -> Result<(), RadioError> {
|
||||
let spreading_factor_val = spreading_factor_value(mdltn_params.spreading_factor)?;
|
||||
let bandwidth_val = bandwidth_value(mdltn_params.bandwidth)?;
|
||||
let coding_rate_val = coding_rate_value(mdltn_params.coding_rate)?;
|
||||
debug!(
|
||||
"sf = {}, bw = {}, cr = {}",
|
||||
spreading_factor_val, bandwidth_val, coding_rate_val
|
||||
);
|
||||
let op_code_and_mod_params = [
|
||||
OpCode::SetModulationParams.value(),
|
||||
spreading_factor_val,
|
||||
bandwidth_val,
|
||||
coding_rate_val,
|
||||
mdltn_params.low_data_rate_optimize,
|
||||
];
|
||||
self.intf.write(&op_code_and_mod_params, false).await?;
|
||||
|
||||
// Handle modulation quality with the 500 kHz LoRa bandwidth (see DS_SX1261-2_V1.2 datasheet chapter 15.1)
|
||||
let mut tx_mod = [0x00u8];
|
||||
self.intf
|
||||
.read(
|
||||
&[
|
||||
OpCode::ReadRegister.value(),
|
||||
Register::TxModulation.addr1(),
|
||||
Register::TxModulation.addr2(),
|
||||
0x00u8,
|
||||
],
|
||||
&mut tx_mod,
|
||||
)
|
||||
.await?;
|
||||
if mdltn_params.bandwidth == Bandwidth::_500KHz {
|
||||
let register_and_tx_mod_update = [
|
||||
OpCode::WriteRegister.value(),
|
||||
Register::TxModulation.addr1(),
|
||||
Register::TxModulation.addr2(),
|
||||
tx_mod[0] & (!(1 << 2)),
|
||||
];
|
||||
self.intf.write(®ister_and_tx_mod_update, false).await
|
||||
} else {
|
||||
let register_and_tx_mod_update = [
|
||||
OpCode::WriteRegister.value(),
|
||||
Register::TxModulation.addr1(),
|
||||
Register::TxModulation.addr2(),
|
||||
tx_mod[0] | (1 << 2),
|
||||
];
|
||||
self.intf.write(®ister_and_tx_mod_update, false).await
|
||||
}
|
||||
}
|
||||
|
||||
async fn set_packet_params(&mut self, pkt_params: &PacketParams) -> Result<(), RadioError> {
|
||||
let op_code_and_pkt_params = [
|
||||
OpCode::SetPacketParams.value(),
|
||||
((pkt_params.preamble_length >> 8) & 0xFF) as u8,
|
||||
(pkt_params.preamble_length & 0xFF) as u8,
|
||||
pkt_params.implicit_header as u8,
|
||||
pkt_params.payload_length,
|
||||
pkt_params.crc_on as u8,
|
||||
pkt_params.iq_inverted as u8,
|
||||
];
|
||||
self.intf.write(&op_code_and_pkt_params, false).await?;
|
||||
|
||||
// Optimize Inverted IQ Operation, otherwise packet loss with longer packets might occur.
|
||||
let mut iq_polarity = [0x00u8];
|
||||
self.intf
|
||||
.read(
|
||||
&[
|
||||
OpCode::ReadRegister.value(),
|
||||
Register::IQPolarity.addr1(),
|
||||
Register::IQPolarity.addr2(),
|
||||
0x00u8,
|
||||
],
|
||||
&mut iq_polarity,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let reg = if pkt_params.iq_inverted {
|
||||
iq_polarity[0] & (!(1 << 2))
|
||||
} else {
|
||||
iq_polarity[0] | (1 << 2)
|
||||
};
|
||||
|
||||
let op = [
|
||||
OpCode::WriteRegister.value(),
|
||||
Register::IQPolarity.addr1(),
|
||||
Register::IQPolarity.addr2(),
|
||||
reg,
|
||||
];
|
||||
self.intf.write(&op, false).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Calibrate the image rejection based on the given frequency
|
||||
async fn calibrate_image(&mut self, frequency_in_hz: u32) -> Result<(), RadioError> {
|
||||
let mut cal_freq = [0x00u8, 0x00u8];
|
||||
|
||||
if frequency_in_hz > 900000000 {
|
||||
cal_freq[0] = 0xE1;
|
||||
cal_freq[1] = 0xE9;
|
||||
} else if frequency_in_hz > 850000000 {
|
||||
cal_freq[0] = 0xD7;
|
||||
cal_freq[1] = 0xDB;
|
||||
} else if frequency_in_hz > 770000000 {
|
||||
cal_freq[0] = 0xC1;
|
||||
cal_freq[1] = 0xC5;
|
||||
} else if frequency_in_hz > 460000000 {
|
||||
cal_freq[0] = 0x75;
|
||||
cal_freq[1] = 0x81;
|
||||
} else if frequency_in_hz > 425000000 {
|
||||
cal_freq[0] = 0x6B;
|
||||
cal_freq[1] = 0x6F;
|
||||
}
|
||||
|
||||
let op_code_and_cal_freq = [OpCode::CalibrateImage.value(), cal_freq[0], cal_freq[1]];
|
||||
self.intf.write(&op_code_and_cal_freq, false).await
|
||||
}
|
||||
|
||||
async fn set_channel(&mut self, frequency_in_hz: u32) -> Result<(), RadioError> {
|
||||
debug!("channel = {}", frequency_in_hz);
|
||||
let freq_in_pll_steps = Self::convert_freq_in_hz_to_pll_step(frequency_in_hz);
|
||||
let op_code_and_pll_steps = [
|
||||
OpCode::SetRFFrequency.value(),
|
||||
((freq_in_pll_steps >> 24) & 0xFF) as u8,
|
||||
((freq_in_pll_steps >> 16) & 0xFF) as u8,
|
||||
((freq_in_pll_steps >> 8) & 0xFF) as u8,
|
||||
(freq_in_pll_steps & 0xFF) as u8,
|
||||
];
|
||||
self.intf.write(&op_code_and_pll_steps, false).await
|
||||
}
|
||||
|
||||
async fn set_payload(&mut self, payload: &[u8]) -> Result<(), RadioError> {
|
||||
let op_code_and_offset = [OpCode::WriteBuffer.value(), 0x00u8];
|
||||
self.intf.write_with_payload(&op_code_and_offset, payload, false).await
|
||||
}
|
||||
|
||||
async fn do_tx(&mut self) -> Result<(), RadioError> {
|
||||
self.intf.iv.enable_rf_switch_tx().await?;
|
||||
|
||||
// Disable timeout
|
||||
let cmd = [
|
||||
OpCode::SetTx.value(),
|
||||
Self::timeout_1(0),
|
||||
Self::timeout_2(0),
|
||||
Self::timeout_3(0),
|
||||
];
|
||||
self.intf.write(&cmd, false).await
|
||||
}
|
||||
|
||||
async fn do_rx(&mut self, rx_mode: RxMode) -> Result<(), RadioError> {
|
||||
self.intf.iv.enable_rf_switch_rx().await?;
|
||||
|
||||
// Stop the Rx timer on preamble detection
|
||||
let op_code_and_true_flag = [OpCode::SetStopRxTimerOnPreamble.value(), 0x01u8];
|
||||
self.intf.write(&op_code_and_true_flag, false).await?;
|
||||
|
||||
let num_symbols = match rx_mode {
|
||||
RxMode::DutyCycle(_) | RxMode::Continuous => 0,
|
||||
RxMode::Single(n) => n,
|
||||
};
|
||||
self.set_lora_symbol_num_timeout(num_symbols).await?;
|
||||
|
||||
let rx_gain = if self.config.rx_boost { 0x96 } else { 0x94 };
|
||||
let register_and_rx_gain = [
|
||||
OpCode::WriteRegister.value(),
|
||||
Register::RxGain.addr1(),
|
||||
Register::RxGain.addr2(),
|
||||
rx_gain,
|
||||
];
|
||||
self.intf.write(®ister_and_rx_gain, false).await?;
|
||||
|
||||
match rx_mode {
|
||||
RxMode::DutyCycle(args) => {
|
||||
let op = [
|
||||
OpCode::SetRxDutyCycle.value(),
|
||||
Self::timeout_1(args.rx_time),
|
||||
Self::timeout_2(args.rx_time),
|
||||
Self::timeout_3(args.rx_time),
|
||||
Self::timeout_1(args.sleep_time),
|
||||
Self::timeout_2(args.sleep_time),
|
||||
Self::timeout_3(args.sleep_time),
|
||||
];
|
||||
self.intf.write(&op, false).await
|
||||
}
|
||||
RxMode::Single(_) => {
|
||||
let op = [
|
||||
OpCode::SetRx.value(),
|
||||
Self::timeout_1(0),
|
||||
Self::timeout_2(0),
|
||||
Self::timeout_3(0),
|
||||
];
|
||||
self.intf.write(&op, false).await
|
||||
}
|
||||
RxMode::Continuous => {
|
||||
let op = [
|
||||
OpCode::SetRx.value(),
|
||||
Self::timeout_1(RX_CONTINUOUS_TIMEOUT),
|
||||
Self::timeout_2(RX_CONTINUOUS_TIMEOUT),
|
||||
Self::timeout_3(RX_CONTINUOUS_TIMEOUT),
|
||||
];
|
||||
self.intf.write(&op, false).await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_rx_payload(
|
||||
&mut self,
|
||||
rx_pkt_params: &PacketParams,
|
||||
receiving_buffer: &mut [u8],
|
||||
) -> Result<u8, RadioError> {
|
||||
let op_code = [OpCode::GetRxBufferStatus.value()];
|
||||
let mut rx_buffer_status = [0x00u8; 2];
|
||||
let read_status = self.intf.read_with_status(&op_code, &mut rx_buffer_status).await?;
|
||||
if OpStatusErrorMask::is_error(read_status) {
|
||||
return Err(RadioError::OpError(read_status));
|
||||
}
|
||||
|
||||
let mut payload_length_buffer = [0x00u8];
|
||||
if rx_pkt_params.implicit_header {
|
||||
self.intf
|
||||
.read(
|
||||
&[
|
||||
OpCode::ReadRegister.value(),
|
||||
Register::PayloadLength.addr1(),
|
||||
Register::PayloadLength.addr2(),
|
||||
0x00u8,
|
||||
],
|
||||
&mut payload_length_buffer,
|
||||
)
|
||||
.await?;
|
||||
} else {
|
||||
payload_length_buffer[0] = rx_buffer_status[0];
|
||||
}
|
||||
|
||||
let payload_length = payload_length_buffer[0];
|
||||
let offset = rx_buffer_status[1];
|
||||
|
||||
if (payload_length as usize) > receiving_buffer.len() {
|
||||
Err(RadioError::PayloadSizeMismatch(
|
||||
payload_length as usize,
|
||||
receiving_buffer.len(),
|
||||
))
|
||||
} else {
|
||||
self.intf
|
||||
.read(
|
||||
&[OpCode::ReadBuffer.value(), offset, 0x00u8],
|
||||
&mut receiving_buffer[..payload_length as usize],
|
||||
)
|
||||
.await?;
|
||||
Ok(payload_length)
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_rx_packet_status(&mut self) -> Result<PacketStatus, RadioError> {
|
||||
let op_code = [OpCode::GetPacketStatus.value()];
|
||||
let mut pkt_status = [0x00u8; 3];
|
||||
let read_status = self.intf.read_with_status(&op_code, &mut pkt_status).await?;
|
||||
if OpStatusErrorMask::is_error(read_status) {
|
||||
return Err(RadioError::OpError(read_status));
|
||||
}
|
||||
// check this ???
|
||||
let rssi = ((-(pkt_status[0] as i32)) >> 1) as i16;
|
||||
let snr = (((pkt_status[1] as i8) + 2) >> 2) as i16;
|
||||
let _signal_rssi = ((-(pkt_status[2] as i32)) >> 1) as i16; // unused currently
|
||||
|
||||
Ok(PacketStatus { rssi, snr })
|
||||
}
|
||||
|
||||
async fn do_cad(&mut self, mdltn_params: &ModulationParams) -> Result<(), RadioError> {
|
||||
self.intf.iv.enable_rf_switch_rx().await?;
|
||||
|
||||
let mut rx_gain_final = 0x94u8;
|
||||
// if Rx boosted, set max LNA gain, increase current by ~2mA for around ~3dB in sensitivity
|
||||
if self.config.rx_boost {
|
||||
rx_gain_final = 0x96u8;
|
||||
}
|
||||
|
||||
let register_and_rx_gain = [
|
||||
OpCode::WriteRegister.value(),
|
||||
Register::RxGain.addr1(),
|
||||
Register::RxGain.addr2(),
|
||||
rx_gain_final,
|
||||
];
|
||||
self.intf.write(®ister_and_rx_gain, false).await?;
|
||||
|
||||
// See:
|
||||
// https://lora-developers.semtech.com/documentation/tech-papers-and-guides/channel-activity-detection-ensuring-your-lora-packets-are-sent/how-to-ensure-your-lora-packets-are-sent-properly
|
||||
// for default values used here.
|
||||
let spreading_factor_val = spreading_factor_value(mdltn_params.spreading_factor)?;
|
||||
let op_code_and_cad_params = [
|
||||
OpCode::SetCADParams.value(),
|
||||
CADSymbols::_8.value(), // number of symbols for detection
|
||||
spreading_factor_val + 13u8, // limit for detection of SNR peak
|
||||
10u8, // minimum symbol recognition
|
||||
0x00u8, // CAD exit mode without listen-before-send or subsequent receive processing
|
||||
0x00u8, // no timeout
|
||||
0x00u8,
|
||||
0x00u8,
|
||||
];
|
||||
self.intf.write(&op_code_and_cad_params, false).await?;
|
||||
|
||||
let op_code_for_set_cad = [OpCode::SetCAD.value()];
|
||||
self.intf.write(&op_code_for_set_cad, false).await
|
||||
}
|
||||
|
||||
// Set the IRQ mask and DIO masks
|
||||
async fn set_irq_params(&mut self, radio_mode: Option<RadioMode>) -> Result<(), RadioError> {
|
||||
let mut irq_mask: u16 = IrqMask::None.value();
|
||||
let mut dio1_mask: u16 = IrqMask::None.value();
|
||||
let dio2_mask: u16 = IrqMask::None.value();
|
||||
let dio3_mask: u16 = IrqMask::None.value();
|
||||
|
||||
match radio_mode {
|
||||
Some(RadioMode::Standby) => {
|
||||
irq_mask = IrqMask::All.value();
|
||||
dio1_mask = IrqMask::All.value();
|
||||
}
|
||||
Some(RadioMode::Transmit) => {
|
||||
irq_mask = IrqMask::TxDone.value() | IrqMask::RxTxTimeout.value();
|
||||
dio1_mask = IrqMask::TxDone.value() | IrqMask::RxTxTimeout.value();
|
||||
}
|
||||
Some(RadioMode::Receive(_)) => {
|
||||
irq_mask = IrqMask::All.value();
|
||||
dio1_mask = IrqMask::All.value();
|
||||
}
|
||||
Some(RadioMode::ChannelActivityDetection) => {
|
||||
irq_mask = IrqMask::CADDone.value() | IrqMask::CADActivityDetected.value();
|
||||
dio1_mask = IrqMask::CADDone.value() | IrqMask::CADActivityDetected.value();
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
let op_code_and_masks = [
|
||||
OpCode::CfgDIOIrq.value(),
|
||||
((irq_mask >> 8) & 0x00FF) as u8,
|
||||
(irq_mask & 0x00FF) as u8,
|
||||
((dio1_mask >> 8) & 0x00FF) as u8,
|
||||
(dio1_mask & 0x00FF) as u8,
|
||||
((dio2_mask >> 8) & 0x00FF) as u8,
|
||||
(dio2_mask & 0x00FF) as u8,
|
||||
((dio3_mask >> 8) & 0x00FF) as u8,
|
||||
(dio3_mask & 0x00FF) as u8,
|
||||
];
|
||||
self.intf.write(&op_code_and_masks, false).await
|
||||
}
|
||||
|
||||
async fn set_tx_continuous_wave_mode(&mut self) -> Result<(), RadioError> {
|
||||
self.intf.iv.enable_rf_switch_tx().await?;
|
||||
|
||||
let op_code = [OpCode::SetTxContinuousWave.value()];
|
||||
self.intf.write(&op_code, false).await
|
||||
}
|
||||
|
||||
async fn await_irq(&mut self) -> Result<(), RadioError> {
|
||||
self.intf.iv.await_irq().await
|
||||
}
|
||||
|
||||
/// Process the radio IRQ. Log unexpected interrupts. Packets from other
|
||||
/// devices can cause unexpected interrupts.
|
||||
///
|
||||
/// NB! Do not await this future in a select branch as interrupting it
|
||||
/// mid-flow could cause radio lock up.
|
||||
async fn process_irq_event(
|
||||
&mut self,
|
||||
radio_mode: RadioMode,
|
||||
cad_activity_detected: Option<&mut bool>,
|
||||
clear_interrupts: bool,
|
||||
) -> Result<Option<IrqState>, RadioError> {
|
||||
let op_code = [OpCode::GetIrqStatus.value()];
|
||||
let mut irq_status = [0x00u8, 0x00u8];
|
||||
// Assuming intf.read_with_status is an existing async method that reads the IRQ status.
|
||||
let read_status = self.intf.read_with_status(&op_code, &mut irq_status).await?;
|
||||
let irq_flags = ((irq_status[0] as u16) << 8) | (irq_status[1] as u16);
|
||||
|
||||
if clear_interrupts && irq_flags != 0 {
|
||||
let op_code_and_irq_status = [OpCode::ClrIrqStatus.value(), irq_status[0], irq_status[1]];
|
||||
self.intf.write(&op_code_and_irq_status, false).await?;
|
||||
}
|
||||
|
||||
if OpStatusErrorMask::is_error(read_status) {
|
||||
debug!(
|
||||
"process_irq read status error = 0x{:x} in radio mode {}",
|
||||
read_status, radio_mode
|
||||
);
|
||||
}
|
||||
|
||||
debug!(
|
||||
"process_irq satisfied: irq_flags = 0x{:x} in radio mode {}",
|
||||
irq_flags, radio_mode
|
||||
);
|
||||
|
||||
if (irq_flags & IrqMask::HeaderValid.value()) == IrqMask::HeaderValid.value() {
|
||||
debug!("HeaderValid in radio mode {}", radio_mode);
|
||||
}
|
||||
if (irq_flags & IrqMask::PreambleDetected.value()) == IrqMask::PreambleDetected.value() {
|
||||
debug!("PreambleDetected in radio mode {}", radio_mode);
|
||||
}
|
||||
if (irq_flags & IrqMask::SyncwordValid.value()) == IrqMask::SyncwordValid.value() {
|
||||
debug!("SyncwordValid in radio mode {}", radio_mode);
|
||||
}
|
||||
|
||||
match radio_mode {
|
||||
RadioMode::Transmit => {
|
||||
if (irq_flags & IrqMask::TxDone.value()) == IrqMask::TxDone.value() {
|
||||
return Ok(Some(IrqState::Done));
|
||||
}
|
||||
if (irq_flags & IrqMask::RxTxTimeout.value()) == IrqMask::RxTxTimeout.value() {
|
||||
return Err(RadioError::TransmitTimeout);
|
||||
}
|
||||
}
|
||||
RadioMode::Receive(rx_mode) => {
|
||||
if (irq_flags & IrqMask::HeaderError.value()) == IrqMask::HeaderError.value() {
|
||||
debug!("HeaderError in radio mode {}", radio_mode);
|
||||
}
|
||||
if (irq_flags & IrqMask::CRCError.value()) == IrqMask::CRCError.value() {
|
||||
debug!("CRCError in radio mode {}", radio_mode);
|
||||
}
|
||||
if (irq_flags & IrqMask::RxDone.value()) == IrqMask::RxDone.value() {
|
||||
debug!("RxDone in radio mode {}", radio_mode);
|
||||
if rx_mode != RxMode::Continuous {
|
||||
// implicit header mode timeout behavior (see DS_SX1261-2_V1.2 datasheet chapter 15.3)
|
||||
let register_and_clear = [
|
||||
OpCode::WriteRegister.value(),
|
||||
Register::RTCCtrl.addr1(),
|
||||
Register::RTCCtrl.addr2(),
|
||||
0x00u8,
|
||||
];
|
||||
self.intf.write(®ister_and_clear, false).await?;
|
||||
|
||||
let mut evt_clr = [0x00u8];
|
||||
self.intf
|
||||
.read(
|
||||
&[
|
||||
OpCode::ReadRegister.value(),
|
||||
Register::EvtClr.addr1(),
|
||||
Register::EvtClr.addr2(),
|
||||
0x00u8,
|
||||
],
|
||||
&mut evt_clr,
|
||||
)
|
||||
.await?;
|
||||
evt_clr[0] |= 1 << 1;
|
||||
let register_and_evt_clear = [
|
||||
OpCode::WriteRegister.value(),
|
||||
Register::EvtClr.addr1(),
|
||||
Register::EvtClr.addr2(),
|
||||
evt_clr[0],
|
||||
];
|
||||
self.intf.write(®ister_and_evt_clear, false).await?;
|
||||
}
|
||||
return Ok(Some(IrqState::Done));
|
||||
}
|
||||
if IrqMask::PreambleDetected.is_set_in(irq_flags) || IrqMask::HeaderValid.is_set_in(irq_flags) {
|
||||
return Ok(Some(IrqState::PreambleReceived));
|
||||
}
|
||||
if (irq_flags & IrqMask::RxTxTimeout.value()) == IrqMask::RxTxTimeout.value() {
|
||||
return Err(RadioError::ReceiveTimeout);
|
||||
}
|
||||
}
|
||||
RadioMode::ChannelActivityDetection => {
|
||||
if (irq_flags & IrqMask::CADDone.value()) == IrqMask::CADDone.value() {
|
||||
if let Some(detected) = cad_activity_detected {
|
||||
*detected =
|
||||
(irq_flags & IrqMask::CADActivityDetected.value()) == IrqMask::CADActivityDetected.value();
|
||||
}
|
||||
return Ok(Some(IrqState::Done));
|
||||
}
|
||||
}
|
||||
RadioMode::Sleep | RadioMode::Standby => {
|
||||
defmt::warn!("IRQ during sleep/standby?");
|
||||
}
|
||||
RadioMode::FrequencySynthesis => todo!(),
|
||||
}
|
||||
|
||||
// If none of the specific conditions are met, return None to indicate no IRQ state change.
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
// use super::*;
|
||||
|
||||
#[test]
|
||||
// -17 (0xEF) to +14 (0x0E) dBm by step of 1 dB if low power PA is selected
|
||||
// -9 (0xF7) to +22 (0x16) dBm by step of 1 dB if high power PA is selected
|
||||
fn power_level_negative_value_conversion() {
|
||||
let mut i32_val: i32 = -17;
|
||||
assert_eq!(i32_val as u8, 0xefu8);
|
||||
i32_val = -9;
|
||||
assert_eq!(i32_val as u8, 0xf7u8);
|
||||
}
|
||||
}
|
||||
+304
@@ -0,0 +1,304 @@
|
||||
use crate::mod_params::*;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub enum PacketType {
|
||||
GFSK = 0x00,
|
||||
LoRa = 0x01,
|
||||
None = 0x0F,
|
||||
}
|
||||
|
||||
impl PacketType {
|
||||
pub const fn value(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum IrqMask {
|
||||
None = 0x0000,
|
||||
TxDone = 0x0001,
|
||||
RxDone = 0x0002,
|
||||
PreambleDetected = 0x0004,
|
||||
SyncwordValid = 0x0008,
|
||||
HeaderValid = 0x0010,
|
||||
HeaderError = 0x0020,
|
||||
CRCError = 0x0040,
|
||||
CADDone = 0x0080,
|
||||
CADActivityDetected = 0x0100,
|
||||
RxTxTimeout = 0x0200,
|
||||
All = 0xFFFF,
|
||||
}
|
||||
|
||||
impl IrqMask {
|
||||
pub fn value(self) -> u16 {
|
||||
self as u16
|
||||
}
|
||||
|
||||
pub fn is_set_in(self, mask: u16) -> bool {
|
||||
self.value() & mask == self.value()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[allow(dead_code)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub enum Register {
|
||||
PacketParams = 0x0704, // packet configuration
|
||||
PayloadLength = 0x0702, // payload size
|
||||
/// Number of symbols given as SX126X_REG_LR_SYNCH_TIMEOUT[7:3] * 2 ^ (2*SX126X_REG_LR_SYNCH_TIMEOUT[2:0] + 1)
|
||||
/// Info from SDK (not present in user manual).
|
||||
SynchTimeout = 0x0706,
|
||||
Syncword = 0x06C0, // Syncword values
|
||||
LoRaSyncword = 0x0740, // LoRa Syncword value
|
||||
GeneratedRandomNumber = 0x0819, //32-bit generated random number
|
||||
AnaLNA = 0x08E2, // disable the LNA
|
||||
AnaMixer = 0x08E5, // disable the mixer
|
||||
RxGain = 0x08AC, // RX gain (0x94: power saving, 0x96: rx boosted)
|
||||
XTATrim = 0x0911, // device internal trimming capacitor
|
||||
OCP = 0x08E7, // over current protection max value
|
||||
RetentionList = 0x029F, // retention list
|
||||
/// Inverted IQ operation optimization - possible packet loss for longer packets
|
||||
/// DS.SX1261-2.W.APP Rev.2.1 - Chapter 15.4
|
||||
IQPolarity = 0x0736,
|
||||
TxModulation = 0x0889, // modulation quality with 500 kHz LoRa Bandwidth (see DS_SX1261-2_V1.2 datasheet chapter 15.1)
|
||||
TxClampCfg = 0x08D8, // better resistance to antenna mismatch (see DS_SX1261-2_V1.2 datasheet chapter 15.2)
|
||||
RTCCtrl = 0x0902, // RTC control
|
||||
EvtClr = 0x0944, // event clear
|
||||
}
|
||||
|
||||
impl Register {
|
||||
pub fn addr1(self) -> u8 {
|
||||
((self as u16 & 0xFF00) >> 8) as u8
|
||||
}
|
||||
pub fn addr2(self) -> u8 {
|
||||
(self as u16 & 0x00FF) as u8
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
pub enum OpCode {
|
||||
GetStatus = 0xC0,
|
||||
WriteRegister = 0x0D,
|
||||
ReadRegister = 0x1D,
|
||||
WriteBuffer = 0x0E,
|
||||
ReadBuffer = 0x1E,
|
||||
SetSleep = 0x84,
|
||||
SetStandby = 0x80,
|
||||
SetFS = 0xC1,
|
||||
SetTx = 0x83,
|
||||
SetRx = 0x82,
|
||||
SetRxDutyCycle = 0x94,
|
||||
SetCAD = 0xC5,
|
||||
SetTxContinuousWave = 0xD1,
|
||||
SetTxContinuousPremable = 0xD2,
|
||||
SetPacketType = 0x8A,
|
||||
GetPacketType = 0x11,
|
||||
SetRFFrequency = 0x86,
|
||||
SetTxParams = 0x8E,
|
||||
SetPAConfig = 0x95,
|
||||
SetCADParams = 0x88,
|
||||
SetBufferBaseAddress = 0x8F,
|
||||
SetModulationParams = 0x8B,
|
||||
SetPacketParams = 0x8C,
|
||||
GetRxBufferStatus = 0x13,
|
||||
GetPacketStatus = 0x14,
|
||||
GetRSSIInst = 0x15,
|
||||
GetStats = 0x10,
|
||||
ResetStats = 0x00,
|
||||
CfgDIOIrq = 0x08,
|
||||
GetIrqStatus = 0x12,
|
||||
ClrIrqStatus = 0x02,
|
||||
Calibrate = 0x89,
|
||||
CalibrateImage = 0x98,
|
||||
SetRegulatorMode = 0x96,
|
||||
GetDeviceErrors = 0x17,
|
||||
ClearDeviceErrors = 0x07,
|
||||
SetTCXOMode = 0x97,
|
||||
SetTxFallbackMode = 0x93,
|
||||
SetDIO2AsRfSwitchCtrl = 0x9d,
|
||||
SetStopRxTimerOnPreamble = 0x9F,
|
||||
SetLoRaSymbTimeout = 0xA0,
|
||||
}
|
||||
|
||||
impl OpCode {
|
||||
pub fn value(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
|
||||
// See RM0453 Reference manual STM32WL5x advanced Arm®-based 32-bit MCUs with sub-GHz radio solution, section 5.8.5
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
pub enum OpStatusErrorMask {
|
||||
Timeout = (0x03 << 1),
|
||||
ProcessingError = (0x04 << 1),
|
||||
ExecutionError = (0x05 << 1),
|
||||
}
|
||||
|
||||
impl OpStatusErrorMask {
|
||||
pub fn is_error(status: u8) -> bool {
|
||||
let error_flags = status & 0x0e;
|
||||
OpStatusErrorMask::Timeout as u8 == error_flags
|
||||
|| OpStatusErrorMask::ProcessingError as u8 == error_flags
|
||||
|| OpStatusErrorMask::ExecutionError as u8 == error_flags
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct SleepParams {
|
||||
pub wakeup_rtc: bool, // get out of sleep mode if wakeup signal received from RTC
|
||||
pub reset: bool,
|
||||
pub warm_start: bool,
|
||||
}
|
||||
|
||||
impl SleepParams {
|
||||
pub fn value(self) -> u8 {
|
||||
((self.warm_start as u8) << 2) | ((self.reset as u8) << 1) | (self.wakeup_rtc as u8)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
#[allow(dead_code)]
|
||||
#[allow(clippy::upper_case_acronyms)]
|
||||
pub enum StandbyMode {
|
||||
RC = 0x00,
|
||||
XOSC = 0x01,
|
||||
}
|
||||
|
||||
impl StandbyMode {
|
||||
pub fn value(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[allow(dead_code)]
|
||||
pub enum RegulatorMode {
|
||||
UseLDO = 0x00,
|
||||
UseDCDC = 0x01,
|
||||
}
|
||||
|
||||
impl RegulatorMode {
|
||||
pub fn value(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[allow(dead_code)]
|
||||
pub struct CalibrationParams {
|
||||
pub rc64k_enable: bool, // calibrate RC64K clock
|
||||
pub rc13m_enable: bool, // calibrate RC13M clock
|
||||
pub pll_enable: bool, // calibrate PLL
|
||||
pub adc_pulse_enable: bool, // calibrate ADC Pulse
|
||||
pub adc_bulkn_enable: bool, // calibrate ADC bulkN
|
||||
pub adc_bulkp_enable: bool, // calibrate ADC bulkP
|
||||
pub img_enable: bool,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl CalibrationParams {
|
||||
pub fn value(self) -> u8 {
|
||||
((self.img_enable as u8) << 6)
|
||||
| ((self.adc_bulkp_enable as u8) << 5)
|
||||
| ((self.adc_bulkn_enable as u8) << 4)
|
||||
| ((self.adc_pulse_enable as u8) << 3)
|
||||
| ((self.pll_enable as u8) << 2)
|
||||
| ((self.rc13m_enable as u8) << 1)
|
||||
| (self.rc64k_enable as u8)
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum TcxoCtrlVoltage {
|
||||
Ctrl1V6 = 0x00,
|
||||
Ctrl1V7 = 0x01,
|
||||
Ctrl1V8 = 0x02,
|
||||
Ctrl2V2 = 0x03,
|
||||
Ctrl2V4 = 0x04,
|
||||
Ctrl2V7 = 0x05,
|
||||
Ctrl3V0 = 0x06,
|
||||
Ctrl3V3 = 0x07,
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
impl TcxoCtrlVoltage {
|
||||
pub fn value(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[allow(dead_code)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum RampTime {
|
||||
Ramp10Us = 0x00,
|
||||
Ramp20Us = 0x01,
|
||||
Ramp40Us = 0x02,
|
||||
Ramp80Us = 0x03,
|
||||
Ramp200Us = 0x04,
|
||||
Ramp800Us = 0x05,
|
||||
Ramp1700Us = 0x06,
|
||||
Ramp3400Us = 0x07,
|
||||
}
|
||||
|
||||
impl RampTime {
|
||||
pub fn value(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spreading_factor_value(spreading_factor: SpreadingFactor) -> Result<u8, RadioError> {
|
||||
match spreading_factor {
|
||||
SpreadingFactor::_5 => Ok(0x05),
|
||||
SpreadingFactor::_6 => Ok(0x06),
|
||||
SpreadingFactor::_7 => Ok(0x07),
|
||||
SpreadingFactor::_8 => Ok(0x08),
|
||||
SpreadingFactor::_9 => Ok(0x09),
|
||||
SpreadingFactor::_10 => Ok(0x0A),
|
||||
SpreadingFactor::_11 => Ok(0x0B),
|
||||
SpreadingFactor::_12 => Ok(0x0C),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bandwidth_value(bandwidth: Bandwidth) -> Result<u8, RadioError> {
|
||||
match bandwidth {
|
||||
Bandwidth::_7KHz => Ok(0x00),
|
||||
Bandwidth::_10KHz => Ok(0x08),
|
||||
Bandwidth::_15KHz => Ok(0x01),
|
||||
Bandwidth::_20KHz => Ok(0x09),
|
||||
Bandwidth::_31KHz => Ok(0x02),
|
||||
Bandwidth::_41KHz => Ok(0x0a),
|
||||
Bandwidth::_62KHz => Ok(0x03),
|
||||
Bandwidth::_125KHz => Ok(0x04),
|
||||
Bandwidth::_250KHz => Ok(0x05),
|
||||
Bandwidth::_500KHz => Ok(0x06),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn coding_rate_value(coding_rate: CodingRate) -> Result<u8, RadioError> {
|
||||
match coding_rate {
|
||||
CodingRate::_4_5 => Ok(0x01),
|
||||
CodingRate::_4_6 => Ok(0x02),
|
||||
CodingRate::_4_7 => Ok(0x03),
|
||||
CodingRate::_4_8 => Ok(0x04),
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum CADSymbols {
|
||||
_1 = 0x00,
|
||||
_2 = 0x01,
|
||||
_4 = 0x02,
|
||||
_8 = 0x03,
|
||||
_16 = 0x04,
|
||||
}
|
||||
|
||||
impl CADSymbols {
|
||||
pub fn value(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
use super::DeviceSel;
|
||||
|
||||
/// Implement this trait on your custom variant or use provided impls
|
||||
pub trait Sx126xVariant {
|
||||
/// whether to use high or low power PA
|
||||
fn get_device_sel(&self) -> DeviceSel;
|
||||
|
||||
/// use dio2 as rf switch output
|
||||
fn use_dio2_as_rfswitch(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Sx1261 uses only LowPowerPA
|
||||
pub struct Sx1261;
|
||||
impl Sx126xVariant for Sx1261 {
|
||||
fn get_device_sel(&self) -> super::DeviceSel {
|
||||
super::DeviceSel::LowPowerPA
|
||||
}
|
||||
}
|
||||
|
||||
/// Sx1262 uses only HighPowerPA
|
||||
pub struct Sx1262;
|
||||
|
||||
impl Sx126xVariant for Sx1262 {
|
||||
fn get_device_sel(&self) -> super::DeviceSel {
|
||||
super::DeviceSel::HighPowerPA
|
||||
}
|
||||
}
|
||||
|
||||
/// Stm32wl variant.
|
||||
pub struct Stm32wl {
|
||||
/// select which output to use. (Switching is not supported)
|
||||
pub use_high_power_pa: bool,
|
||||
}
|
||||
impl Sx126xVariant for Stm32wl {
|
||||
fn get_device_sel(&self) -> super::DeviceSel {
|
||||
if self.use_high_power_pa {
|
||||
DeviceSel::HighPowerPA
|
||||
} else {
|
||||
DeviceSel::LowPowerPA
|
||||
}
|
||||
}
|
||||
fn use_dio2_as_rfswitch(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
Vendored
+541
@@ -0,0 +1,541 @@
|
||||
mod radio_kind_params;
|
||||
mod sx1272;
|
||||
pub use sx1272::Sx1272;
|
||||
mod sx1276;
|
||||
pub use sx1276::Sx1276;
|
||||
|
||||
use defmt::debug;
|
||||
use embedded_hal_async::delay::DelayNs;
|
||||
use embedded_hal_async::spi::*;
|
||||
use radio_kind_params::*;
|
||||
|
||||
use crate::mod_params::*;
|
||||
use crate::mod_traits::IrqState;
|
||||
use crate::{InterfaceVariant, RadioKind, SpiInterface};
|
||||
|
||||
// Syncwords for public and private networks
|
||||
const LORA_MAC_PUBLIC_SYNCWORD: u8 = 0x34; // corresponds to sx126x 0x3444
|
||||
const LORA_MAC_PRIVATE_SYNCWORD: u8 = 0x12; // corresponds to sx126x 0x1424
|
||||
|
||||
// TCXO flag
|
||||
const TCXO_FOR_OSCILLATOR: u8 = 0x10u8;
|
||||
|
||||
// Frequency synthesizer step for frequency calculation (Hz)
|
||||
const FREQUENCY_SYNTHESIZER_STEP: f64 = 61.03515625; // FXOSC (32 MHz) * 1000000 (Hz/MHz) / 524288 (2^19)
|
||||
|
||||
// Limits for preamble detection window in single reception mode
|
||||
const SX127X_MIN_LORA_SYMB_NUM_TIMEOUT: u16 = 4;
|
||||
const SX127X_MAX_LORA_SYMB_NUM_TIMEOUT: u16 = 1023;
|
||||
|
||||
// Constant values need to compute the RSSI value
|
||||
const SX1272_RSSI_OFFSET: i16 = -139;
|
||||
const SX1276_RSSI_OFFSET_LF: i16 = -164;
|
||||
const SX1276_RSSI_OFFSET_HF: i16 = -157;
|
||||
const SX1276_RF_MID_BAND_THRESH: u32 = 525_000_000;
|
||||
|
||||
/// Configuration for SX127x-based boards
|
||||
pub struct Config<C: Sx127xVariant> {
|
||||
/// LoRa chip used on specific board
|
||||
pub chip: C,
|
||||
/// Whether board is using crystal oscillator or external clock
|
||||
pub tcxo_used: bool,
|
||||
/// Whether to use PA_BOOST for transmit instead of RFO (sx1272) or RFO_LF (sx1276).
|
||||
/// NB! Depends on board layout.
|
||||
pub tx_boost: bool,
|
||||
/// Whether to boost receive
|
||||
pub rx_boost: bool,
|
||||
}
|
||||
|
||||
/// Base for the RadioKind implementation for the LoRa chip kind and board type
|
||||
pub struct Sx127x<SPI, IV, C: Sx127xVariant + Sized> {
|
||||
intf: SpiInterface<SPI, IV>,
|
||||
config: Config<C>,
|
||||
}
|
||||
|
||||
impl<SPI, IV, C> Sx127x<SPI, IV, C>
|
||||
where
|
||||
SPI: SpiDevice<u8>,
|
||||
IV: InterfaceVariant,
|
||||
C: Sx127xVariant,
|
||||
{
|
||||
/// Create an instance of the RadioKind implementation for the LoRa chip kind and board type
|
||||
pub fn new(spi: SPI, iv: IV, config: Config<C>) -> Self {
|
||||
let intf = SpiInterface::new(spi, iv);
|
||||
Self { intf, config }
|
||||
}
|
||||
|
||||
// Utility functions
|
||||
async fn write_register(&mut self, register: Register, value: u8) -> Result<(), RadioError> {
|
||||
let write_buffer = [register.write_addr(), value];
|
||||
self.intf.write(&write_buffer, false).await
|
||||
}
|
||||
|
||||
async fn read_register(&mut self, register: Register) -> Result<u8, RadioError> {
|
||||
let write_buffer = [register.read_addr()];
|
||||
let mut read_buffer = [0x00u8];
|
||||
self.intf.read(&write_buffer, &mut read_buffer).await?;
|
||||
Ok(read_buffer[0])
|
||||
}
|
||||
|
||||
async fn read_buffer(&mut self, register: Register, buf: &mut [u8]) -> Result<(), RadioError> {
|
||||
self.intf.read(&[register.read_addr()], buf).await
|
||||
}
|
||||
|
||||
async fn write_buffer(&mut self, register: Register, buf: &[u8]) -> Result<(), RadioError> {
|
||||
self.intf.write_with_payload(&[register.write_addr()], buf, false).await
|
||||
}
|
||||
|
||||
// Set the number of symbols the radio will wait to detect a reception (up to 1023 symbols)
|
||||
async fn set_lora_symbol_num_timeout(&mut self, symbol_num: u16) -> Result<(), RadioError> {
|
||||
let val = symbol_num.min(SX127X_MAX_LORA_SYMB_NUM_TIMEOUT);
|
||||
|
||||
let symbol_num_msb = ((val >> 8) & 0x03) as u8;
|
||||
let symbol_num_lsb = (val & 0xff) as u8;
|
||||
let mut config_2 = self.read_register(Register::RegModemConfig2).await?;
|
||||
config_2 = (config_2 & 0xfcu8) | symbol_num_msb;
|
||||
self.write_register(Register::RegModemConfig2, config_2).await?;
|
||||
self.write_register(Register::RegSymbTimeoutLsb, symbol_num_lsb).await
|
||||
}
|
||||
|
||||
// Set the over current protection (mA) on the radio
|
||||
async fn set_ocp(&mut self, ocp_trim: OcpTrim) -> Result<(), RadioError> {
|
||||
self.write_register(Register::RegOcp, ocp_trim.value()).await
|
||||
}
|
||||
}
|
||||
|
||||
impl<SPI, IV, C> RadioKind for Sx127x<SPI, IV, C>
|
||||
where
|
||||
SPI: SpiDevice<u8>,
|
||||
IV: InterfaceVariant,
|
||||
C: Sx127xVariant,
|
||||
{
|
||||
async fn init_lora(&mut self, is_public_network: bool) -> Result<(), RadioError> {
|
||||
if self.config.tcxo_used {
|
||||
self.write_register(C::reg_txco(), TCXO_FOR_OSCILLATOR).await?;
|
||||
}
|
||||
|
||||
let syncword = if is_public_network {
|
||||
LORA_MAC_PUBLIC_SYNCWORD
|
||||
} else {
|
||||
LORA_MAC_PRIVATE_SYNCWORD
|
||||
};
|
||||
self.write_register(Register::RegSyncWord, syncword).await?;
|
||||
|
||||
self.set_tx_rx_buffer_base_address(0, 0).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_modulation_params(
|
||||
&self,
|
||||
spreading_factor: SpreadingFactor,
|
||||
bandwidth: Bandwidth,
|
||||
coding_rate: CodingRate,
|
||||
frequency_in_hz: u32,
|
||||
) -> Result<ModulationParams, RadioError> {
|
||||
// Parameter validation
|
||||
spreading_factor_value(spreading_factor)?;
|
||||
coding_rate_value(coding_rate)?;
|
||||
C::bandwidth_value(bandwidth)?;
|
||||
if ((bandwidth == Bandwidth::_250KHz) || (bandwidth == Bandwidth::_500KHz)) && (frequency_in_hz < 400_000_000) {
|
||||
return Err(RadioError::InvalidBandwidthForFrequency);
|
||||
}
|
||||
|
||||
// Section 4.1.1.5 and 4.1.1.6
|
||||
let bw_in_hz = u32::from(bandwidth);
|
||||
let symbol_duration = 1000 / (bw_in_hz / (0x01u32 << spreading_factor_value(spreading_factor)?));
|
||||
let mut low_data_rate_optimize = 0x00u8;
|
||||
if symbol_duration > 16 {
|
||||
low_data_rate_optimize = 0x01u8
|
||||
}
|
||||
|
||||
Ok(ModulationParams {
|
||||
spreading_factor,
|
||||
bandwidth,
|
||||
coding_rate,
|
||||
low_data_rate_optimize,
|
||||
frequency_in_hz,
|
||||
})
|
||||
}
|
||||
|
||||
fn create_packet_params(
|
||||
&self,
|
||||
preamble_length: u16,
|
||||
implicit_header: bool,
|
||||
payload_length: u8,
|
||||
crc_on: bool,
|
||||
iq_inverted: bool,
|
||||
modulation_params: &ModulationParams,
|
||||
) -> Result<PacketParams, RadioError> {
|
||||
// Parameter validation
|
||||
if (modulation_params.spreading_factor == SpreadingFactor::_6) && !implicit_header {
|
||||
return Err(RadioError::InvalidSF6ExplicitHeaderRequest);
|
||||
}
|
||||
|
||||
Ok(PacketParams {
|
||||
preamble_length,
|
||||
implicit_header,
|
||||
payload_length,
|
||||
crc_on,
|
||||
iq_inverted,
|
||||
})
|
||||
}
|
||||
|
||||
async fn reset(&mut self, delay: &mut impl DelayNs) -> Result<(), RadioError> {
|
||||
self.intf.iv.reset(delay).await?;
|
||||
self.set_sleep(false, delay).await?; // ensure sleep mode is entered so that the LoRa mode bit is set
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn ensure_ready(&mut self, _mode: RadioMode) -> Result<(), RadioError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_standby(&mut self) -> Result<(), RadioError> {
|
||||
self.write_register(Register::RegOpMode, LoRaMode::Standby.value())
|
||||
.await?;
|
||||
self.intf.iv.disable_rf_switch().await
|
||||
}
|
||||
|
||||
async fn set_sleep(&mut self, _warm_start_if_possible: bool, _delay: &mut impl DelayNs) -> Result<(), RadioError> {
|
||||
// Warm start is unavailable for sx127x
|
||||
self.intf.iv.disable_rf_switch().await?;
|
||||
let buf = [Register::RegOpMode.write_addr(), LoRaMode::Sleep.value()];
|
||||
// NB! Switching to sleep mode is "sleep" command...
|
||||
self.intf.write(&buf, true).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_tx_rx_buffer_base_address(
|
||||
&mut self,
|
||||
tx_base_addr: usize,
|
||||
rx_base_addr: usize,
|
||||
) -> Result<(), RadioError> {
|
||||
if tx_base_addr > 255 || rx_base_addr > 255 {
|
||||
return Err(RadioError::InvalidBaseAddress(tx_base_addr, rx_base_addr));
|
||||
}
|
||||
self.write_register(Register::RegFifoTxBaseAddr, 0x00u8).await?;
|
||||
self.write_register(Register::RegFifoRxBaseAddr, 0x00u8).await
|
||||
}
|
||||
|
||||
// Set parameters associated with power for a send operation.
|
||||
// p_out desired RF output power (dBm)
|
||||
// mdltn_params needed for a power vs channel frequency validation
|
||||
// tx_boosted_if_possible determine if transmit boost is requested
|
||||
// is_tx_prep indicates which ramp up time to use
|
||||
async fn set_tx_power_and_ramp_time(
|
||||
&mut self,
|
||||
p_out: i32,
|
||||
_mdltn_params: Option<&ModulationParams>,
|
||||
is_tx_prep: bool,
|
||||
) -> Result<(), RadioError> {
|
||||
debug!("tx power = {}", p_out);
|
||||
|
||||
// Configure tx power and boost
|
||||
C::set_tx_power(self, p_out, self.config.tx_boost).await?;
|
||||
|
||||
let ramp_time = match is_tx_prep {
|
||||
true => RampTime::Ramp40Us, // for instance, prior to TX or CAD
|
||||
false => RampTime::Ramp250Us, // for instance, on initialization
|
||||
};
|
||||
|
||||
let val = C::ramp_value(ramp_time);
|
||||
self.write_register(Register::RegPaRamp, val).await
|
||||
}
|
||||
|
||||
async fn set_modulation_params(&mut self, mdltn_params: &ModulationParams) -> Result<(), RadioError> {
|
||||
let sf_val = spreading_factor_value(mdltn_params.spreading_factor)?;
|
||||
let bw_val = C::bandwidth_value(mdltn_params.bandwidth)?;
|
||||
let coding_rate_denominator_val = coding_rate_denominator_value(mdltn_params.coding_rate)?;
|
||||
debug!(
|
||||
"sf = {}, bw = {}, cr_denom = {}",
|
||||
sf_val, bw_val, coding_rate_denominator_val
|
||||
);
|
||||
// Configure LoRa optimization (0x31) and detection threshold registers (0x37)
|
||||
let (opt, thr) = match mdltn_params.spreading_factor {
|
||||
SpreadingFactor::_6 => (0x05, 0x0c),
|
||||
_ => (0x03, 0x0a),
|
||||
};
|
||||
let reg_val = self.read_register(Register::RegDetectionOptimize).await?;
|
||||
// Keep reserved bits [6:3] for RegDetectOptimize
|
||||
let val = (reg_val & 0b0111_1000) | opt;
|
||||
self.write_register(Register::RegDetectionOptimize, val).await?;
|
||||
self.write_register(Register::RegDetectionThreshold, thr).await?;
|
||||
// Spreading Factor, Bandwidth, codingrate, ldro
|
||||
|
||||
C::set_modulation_params(self, mdltn_params).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_packet_params(&mut self, pkt_params: &PacketParams) -> Result<(), RadioError> {
|
||||
self.write_register(
|
||||
Register::RegPreambleMsb,
|
||||
((pkt_params.preamble_length >> 8) & 0x00ff) as u8,
|
||||
)
|
||||
.await?;
|
||||
self.write_register(Register::RegPreambleLsb, (pkt_params.preamble_length & 0x00ff) as u8)
|
||||
.await?;
|
||||
|
||||
C::set_packet_params(self, pkt_params).await?;
|
||||
|
||||
// IQ inversion:
|
||||
// RegInvertiq - [0x33]
|
||||
// [6] - InvertIQRX
|
||||
// [5:1] - Reserved: 0x13
|
||||
// [0] - InvertIQTX
|
||||
// RegInvertiq2 - [0x3b]
|
||||
// Set to 0x19 when RX, otherwise set 0x1d
|
||||
let (iq1, iq2) = match pkt_params.iq_inverted {
|
||||
true => (1 << 6, 0x19),
|
||||
false => (1 << 0, 0x1d),
|
||||
};
|
||||
// Keep reserved value for InvertIq as well
|
||||
self.write_register(Register::RegInvertiq, (0x13 << 1) | iq1).await?;
|
||||
self.write_register(Register::RegInvertiq2, iq2).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// Calibrate the image rejection based on the given frequency
|
||||
async fn calibrate_image(&mut self, _frequency_in_hz: u32) -> Result<(), RadioError> {
|
||||
// An automatic process, but can set bit ImageCalStart in RegImageCal, when the device is in Standby mode.
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_channel(&mut self, frequency_in_hz: u32) -> Result<(), RadioError> {
|
||||
debug!("channel = {}", frequency_in_hz);
|
||||
let frf = (frequency_in_hz as f64 / FREQUENCY_SYNTHESIZER_STEP) as u32;
|
||||
self.write_register(Register::RegFrfMsb, ((frf & 0x00FF0000) >> 16) as u8)
|
||||
.await?;
|
||||
self.write_register(Register::RegFrfMid, ((frf & 0x0000FF00) >> 8) as u8)
|
||||
.await?;
|
||||
self.write_register(Register::RegFrfLsb, (frf & 0x000000FF) as u8).await
|
||||
}
|
||||
|
||||
async fn set_payload(&mut self, payload: &[u8]) -> Result<(), RadioError> {
|
||||
self.write_register(Register::RegFifoAddrPtr, 0x00u8).await?;
|
||||
self.write_register(Register::RegPayloadLength, 0x00u8).await?;
|
||||
self.write_buffer(Register::RegFifo, payload).await?;
|
||||
self.write_register(Register::RegPayloadLength, payload.len() as u8)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn do_tx(&mut self) -> Result<(), RadioError> {
|
||||
self.intf.iv.enable_rf_switch_tx().await?;
|
||||
|
||||
self.write_register(Register::RegOpMode, LoRaMode::Tx.value()).await
|
||||
}
|
||||
|
||||
async fn do_rx(&mut self, rx_mode: RxMode) -> Result<(), RadioError> {
|
||||
let (num_symbols, mode) = match rx_mode {
|
||||
RxMode::DutyCycle(_) => Err(RadioError::DutyCycleUnsupported),
|
||||
RxMode::Single(ns) => Ok((ns.max(SX127X_MIN_LORA_SYMB_NUM_TIMEOUT), LoRaMode::RxSingle)),
|
||||
RxMode::Continuous => Ok((0, LoRaMode::RxContinuous)),
|
||||
}?;
|
||||
|
||||
self.intf.iv.enable_rf_switch_rx().await?;
|
||||
|
||||
self.set_lora_symbol_num_timeout(num_symbols).await?;
|
||||
|
||||
let lna_gain = if self.config.rx_boost {
|
||||
LnaGain::G1.boosted_value()
|
||||
} else {
|
||||
LnaGain::G1.value()
|
||||
};
|
||||
self.write_register(Register::RegLna, lna_gain).await?;
|
||||
|
||||
self.write_register(Register::RegFifoAddrPtr, 0x00u8).await?;
|
||||
self.write_register(Register::RegPayloadLength, 0xffu8).await?;
|
||||
|
||||
self.write_register(Register::RegOpMode, mode.value()).await
|
||||
}
|
||||
|
||||
async fn get_rx_payload(
|
||||
&mut self,
|
||||
_rx_pkt_params: &PacketParams,
|
||||
receiving_buffer: &mut [u8],
|
||||
) -> Result<u8, RadioError> {
|
||||
let payload_length = self.read_register(Register::RegRxNbBytes).await?;
|
||||
if (payload_length as usize) > receiving_buffer.len() {
|
||||
return Err(RadioError::PayloadSizeMismatch(
|
||||
payload_length as usize,
|
||||
receiving_buffer.len(),
|
||||
));
|
||||
}
|
||||
let fifo_addr = self.read_register(Register::RegFifoRxCurrentAddr).await?;
|
||||
self.write_register(Register::RegFifoAddrPtr, fifo_addr).await?;
|
||||
self.read_buffer(Register::RegFifo, &mut receiving_buffer[0..payload_length as usize])
|
||||
.await?;
|
||||
self.write_register(Register::RegFifoAddrPtr, 0x00u8).await?;
|
||||
|
||||
Ok(payload_length)
|
||||
}
|
||||
|
||||
async fn get_rx_packet_status(&mut self) -> Result<PacketStatus, RadioError> {
|
||||
let snr = {
|
||||
let packet_snr = self.read_register(Register::RegPktSnrValue).await?;
|
||||
packet_snr as i8 as i16 / 4
|
||||
};
|
||||
|
||||
let rssi = {
|
||||
let packet_rssi = self.read_register(Register::RegPktRssiValue).await?;
|
||||
|
||||
let rssi_offset = C::rssi_offset(self).await?;
|
||||
|
||||
if snr >= 0 {
|
||||
rssi_offset + (packet_rssi as f32 * 16.0 / 15.0) as i16
|
||||
} else {
|
||||
rssi_offset + (packet_rssi as i16) + snr
|
||||
}
|
||||
};
|
||||
|
||||
Ok(PacketStatus { rssi, snr })
|
||||
}
|
||||
|
||||
async fn do_cad(&mut self, _mdltn_params: &ModulationParams) -> Result<(), RadioError> {
|
||||
self.intf.iv.enable_rf_switch_rx().await?;
|
||||
|
||||
let mut lna_gain_final = LnaGain::G1.value();
|
||||
if self.config.rx_boost {
|
||||
lna_gain_final = LnaGain::G1.boosted_value();
|
||||
}
|
||||
self.write_register(Register::RegLna, lna_gain_final).await?;
|
||||
|
||||
self.write_register(Register::RegOpMode, LoRaMode::Cad.value()).await
|
||||
}
|
||||
|
||||
// Set the IRQ mask to disable unwanted interrupts,
|
||||
// enable interrupts on DIO pins (sx127x has multiple),
|
||||
// and allow interrupts.
|
||||
async fn set_irq_params(&mut self, radio_mode: Option<RadioMode>) -> Result<(), RadioError> {
|
||||
match radio_mode {
|
||||
Some(RadioMode::Transmit) => {
|
||||
self.write_register(
|
||||
Register::RegIrqFlagsMask,
|
||||
IrqMask::All.value() ^ IrqMask::TxDone.value(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut dio_mapping_1 = self.read_register(Register::RegDioMapping1).await?;
|
||||
dio_mapping_1 = (dio_mapping_1 & DioMapping1Dio0::Mask.value()) | DioMapping1Dio0::TxDone.value();
|
||||
self.write_register(Register::RegDioMapping1, dio_mapping_1).await?;
|
||||
|
||||
self.write_register(Register::RegIrqFlags, 0x00u8).await?;
|
||||
}
|
||||
Some(RadioMode::Receive(_)) => {
|
||||
self.write_register(
|
||||
Register::RegIrqFlagsMask,
|
||||
IrqMask::All.value()
|
||||
^ (IrqMask::RxDone.value()
|
||||
| IrqMask::RxTimeout.value()
|
||||
| IrqMask::CRCError.value()
|
||||
| IrqMask::HeaderValid.value()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// HeaderValid and CRCError are mutually exclusive when attempting to
|
||||
// trigger DIO-based interrupt, so our approach is to trigger HeaderValid
|
||||
// as this is required for preamble detection.
|
||||
// TODO: RxTimeout should be configured on DIO1
|
||||
let dio_mapping_1 = self.read_register(Register::RegDioMapping1).await?;
|
||||
let val = (dio_mapping_1 & DioMapping1Dio0::Mask.value() & DioMapping1Dio3::Mask.value())
|
||||
| (DioMapping1Dio0::RxDone.value() | DioMapping1Dio3::ValidHeader.value());
|
||||
self.write_register(Register::RegDioMapping1, val).await?;
|
||||
|
||||
self.write_register(Register::RegIrqFlags, 0x00u8).await?;
|
||||
}
|
||||
Some(RadioMode::ChannelActivityDetection) => {
|
||||
self.write_register(
|
||||
Register::RegIrqFlagsMask,
|
||||
IrqMask::All.value() ^ (IrqMask::CADDone.value() | IrqMask::CADActivityDetected.value()),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut dio_mapping_1 = self.read_register(Register::RegDioMapping1).await?;
|
||||
dio_mapping_1 = (dio_mapping_1 & DioMapping1Dio0::Mask.value()) | DioMapping1Dio0::CadDone.value();
|
||||
self.write_register(Register::RegDioMapping1, dio_mapping_1).await?;
|
||||
|
||||
self.write_register(Register::RegIrqFlags, 0x00u8).await?;
|
||||
}
|
||||
_ => {
|
||||
self.write_register(Register::RegIrqFlagsMask, IrqMask::All.value())
|
||||
.await?;
|
||||
|
||||
let mut dio_mapping_1 = self.read_register(Register::RegDioMapping1).await?;
|
||||
dio_mapping_1 = (dio_mapping_1 & DioMapping1Dio0::Mask.value()) | DioMapping1Dio0::Other.value();
|
||||
self.write_register(Register::RegDioMapping1, dio_mapping_1).await?;
|
||||
self.write_register(Register::RegIrqFlags, 0xffu8).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn await_irq(&mut self) -> Result<(), RadioError> {
|
||||
self.intf.iv.await_irq().await
|
||||
}
|
||||
|
||||
/// Process the radio IRQ. Log unexpected interrupts. Packets from other
|
||||
/// devices can cause unexpected interrupts.
|
||||
///
|
||||
/// NB! Do not await this future in a select branch as interrupting it
|
||||
/// mid-flow could cause radio lock up.
|
||||
async fn process_irq_event(
|
||||
&mut self,
|
||||
radio_mode: RadioMode,
|
||||
cad_activity_detected: Option<&mut bool>,
|
||||
clear_interrupts: bool,
|
||||
) -> Result<Option<IrqState>, RadioError> {
|
||||
let irq_flags = self.read_register(Register::RegIrqFlags).await?;
|
||||
if clear_interrupts {
|
||||
self.write_register(Register::RegIrqFlags, 0xffu8).await?; // clear all interrupts
|
||||
}
|
||||
|
||||
match radio_mode {
|
||||
RadioMode::Transmit => {
|
||||
if (irq_flags & IrqMask::TxDone.value()) == IrqMask::TxDone.value() {
|
||||
debug!("TxDone in radio mode {}", radio_mode);
|
||||
return Ok(Some(IrqState::Done));
|
||||
}
|
||||
}
|
||||
RadioMode::Receive(RxMode::Continuous) | RadioMode::Receive(RxMode::Single(_)) => {
|
||||
if (irq_flags & IrqMask::RxDone.value()) == IrqMask::RxDone.value() {
|
||||
debug!("RxDone in radio mode {}", radio_mode);
|
||||
return Ok(Some(IrqState::Done));
|
||||
}
|
||||
if (irq_flags & IrqMask::RxTimeout.value()) == IrqMask::RxTimeout.value() {
|
||||
debug!("RxTimeout in radio mode {}", radio_mode);
|
||||
return Err(RadioError::ReceiveTimeout);
|
||||
}
|
||||
if IrqMask::HeaderValid.is_set_in(irq_flags) {
|
||||
debug!("HeaderValid in radio mode {}", radio_mode);
|
||||
return Ok(Some(IrqState::PreambleReceived));
|
||||
}
|
||||
}
|
||||
RadioMode::ChannelActivityDetection => {
|
||||
if (irq_flags & IrqMask::CADDone.value()) == IrqMask::CADDone.value() {
|
||||
debug!("CADDone in radio mode {}", radio_mode);
|
||||
// TODO: don't like how we mutate the cad_activity_detected parameter
|
||||
if cad_activity_detected.is_some() {
|
||||
// Check if the CAD (Channel Activity Detection) Activity Detected flag is set in irq_flags and then update the reference
|
||||
*(cad_activity_detected.unwrap()) =
|
||||
(irq_flags & IrqMask::CADActivityDetected.value()) == IrqMask::CADActivityDetected.value();
|
||||
}
|
||||
return Ok(Some(IrqState::Done));
|
||||
}
|
||||
}
|
||||
RadioMode::Sleep | RadioMode::Standby => {
|
||||
defmt::warn!("IRQ during sleep/standby?");
|
||||
}
|
||||
RadioMode::FrequencySynthesis => todo!(),
|
||||
RadioMode::Receive(RxMode::DutyCycle(_)) => todo!(),
|
||||
}
|
||||
|
||||
// If no specific IRQ condition is met, return None
|
||||
Ok(None)
|
||||
}
|
||||
/// Set the LoRa chip into the TxContinuousWave mode
|
||||
async fn set_tx_continuous_wave_mode(&mut self) -> Result<(), RadioError> {
|
||||
C::set_tx_continuous_wave_mode(self).await
|
||||
}
|
||||
}
|
||||
+341
@@ -0,0 +1,341 @@
|
||||
use crate::mod_params::*;
|
||||
use crate::mod_traits::InterfaceVariant;
|
||||
use crate::sx127x::Sx127x;
|
||||
use embedded_hal_async::spi::SpiDevice;
|
||||
|
||||
pub trait Sx127xVariant {
|
||||
fn bandwidth_value(bw: Bandwidth) -> Result<u8, RadioError>;
|
||||
fn reg_txco() -> Register;
|
||||
async fn set_tx_power<SPI: SpiDevice<u8>, IV: InterfaceVariant>(
|
||||
radio: &mut Sx127x<SPI, IV, Self>,
|
||||
p_out: i32,
|
||||
tx_boost: bool,
|
||||
) -> Result<(), RadioError>
|
||||
where
|
||||
Self: Sized;
|
||||
fn ramp_value(ramp_time: RampTime) -> u8;
|
||||
|
||||
async fn set_modulation_params<SPI: SpiDevice<u8>, IV: InterfaceVariant>(
|
||||
radio: &mut Sx127x<SPI, IV, Self>,
|
||||
mdltn_params: &ModulationParams,
|
||||
) -> Result<(), RadioError>
|
||||
where
|
||||
Self: Sized;
|
||||
async fn set_packet_params<SPI: SpiDevice<u8>, IV: InterfaceVariant>(
|
||||
radio: &mut Sx127x<SPI, IV, Self>,
|
||||
pkt_params: &PacketParams,
|
||||
) -> Result<(), RadioError>
|
||||
where
|
||||
Self: Sized;
|
||||
|
||||
async fn rssi_offset<SPI: SpiDevice<u8>, IV: InterfaceVariant>(
|
||||
radio: &mut Sx127x<SPI, IV, Self>,
|
||||
) -> Result<i16, RadioError>
|
||||
where
|
||||
Self: Sized;
|
||||
async fn set_tx_continuous_wave_mode<SPI: SpiDevice<u8>, IV: InterfaceVariant>(
|
||||
radio: &mut Sx127x<SPI, IV, Self>,
|
||||
) -> Result<(), RadioError>
|
||||
where
|
||||
Self: Sized;
|
||||
}
|
||||
|
||||
/// Internal sx127x LoRa modes (signified by most significant bit flag)
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum LoRaMode {
|
||||
Sleep = 0x00,
|
||||
Standby = 0x01,
|
||||
Tx = 0x03,
|
||||
RxContinuous = 0x05,
|
||||
RxSingle = 0x06,
|
||||
Cad = 0x07,
|
||||
}
|
||||
|
||||
impl LoRaMode {
|
||||
/// Mode value, including LoRa flag
|
||||
pub fn value(self) -> u8 {
|
||||
(self as u8) | 0x80u8
|
||||
}
|
||||
}
|
||||
|
||||
// IRQ mapping for sx127x chips:
|
||||
// DIO0 - RxDone, TxDone, CadDone
|
||||
// DIO1 - RxTimeout, FhssChangeChannel, CadDetected
|
||||
// DIO2 - 3x FhssChangeChannel
|
||||
// DIO3 - CadDone, ValidHeader, PayloadCrcError
|
||||
// DIO4 - CadDetected, *PllLock, *PllLock
|
||||
// DIO5 - *ModeReady, *ClkOut, *ClkOut
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub enum DioMapping1Dio0 {
|
||||
RxDone = 0x00,
|
||||
TxDone = 0x40,
|
||||
CadDone = 0x80,
|
||||
Other = 0xc0,
|
||||
Mask = 0x3f,
|
||||
}
|
||||
|
||||
impl DioMapping1Dio0 {
|
||||
pub fn value(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub enum DioMapping1Dio1 {
|
||||
RxTimeOut = 0b00 << 2,
|
||||
FhssChangeChannel = 0b01 << 2,
|
||||
CadDetected = 0b10 << 2,
|
||||
Other = 0b11 << 2,
|
||||
Mask = 0xf3,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
impl DioMapping1Dio1 {
|
||||
pub fn value(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub enum DioMapping1Dio3 {
|
||||
CadDone = 0,
|
||||
ValidHeader = 0b01,
|
||||
PayloadCrcError = 0b10,
|
||||
Other = 0b11,
|
||||
Mask = 0xfc,
|
||||
}
|
||||
|
||||
impl DioMapping1Dio3 {
|
||||
pub fn value(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[allow(dead_code)]
|
||||
pub enum IrqMask {
|
||||
None = 0x00,
|
||||
CADActivityDetected = 0x01,
|
||||
FhssChangedChannel = 0x02,
|
||||
CADDone = 0x04,
|
||||
TxDone = 0x08,
|
||||
HeaderValid = 0x10,
|
||||
CRCError = 0x20,
|
||||
RxDone = 0x40,
|
||||
RxTimeout = 0x80,
|
||||
All = 0xFF,
|
||||
}
|
||||
|
||||
impl IrqMask {
|
||||
pub fn value(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
|
||||
pub fn is_set_in(self, mask: u8) -> bool {
|
||||
self.value() & mask == self.value()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[allow(dead_code)]
|
||||
pub enum Register {
|
||||
RegFifo = 0x00,
|
||||
RegOpMode = 0x01,
|
||||
RegFrfMsb = 0x06,
|
||||
RegFrfMid = 0x07,
|
||||
RegFrfLsb = 0x08,
|
||||
RegPaConfig = 0x09,
|
||||
RegPaRamp = 0x0a,
|
||||
RegOcp = 0x0b,
|
||||
RegLna = 0x0c,
|
||||
RegFifoAddrPtr = 0x0d,
|
||||
RegFifoTxBaseAddr = 0x0e,
|
||||
RegFifoRxBaseAddr = 0x0f,
|
||||
RegFifoRxCurrentAddr = 0x10,
|
||||
RegIrqFlagsMask = 0x11,
|
||||
RegIrqFlags = 0x12,
|
||||
RegRxNbBytes = 0x13,
|
||||
RegPktSnrValue = 0x19,
|
||||
RegModemStat = 0x18,
|
||||
RegPktRssiValue = 0x1a,
|
||||
RegModemConfig1 = 0x1d,
|
||||
RegModemConfig2 = 0x1e,
|
||||
RegSymbTimeoutLsb = 0x1f,
|
||||
RegPreambleMsb = 0x20,
|
||||
RegPreambleLsb = 0x21,
|
||||
RegPayloadLength = 0x22,
|
||||
RegMaxPayloadLength = 0x23,
|
||||
RegModemConfig3 = 0x26,
|
||||
RegFreqErrorMsb = 0x28,
|
||||
RegFreqErrorMid = 0x29,
|
||||
RegFreqErrorLsb = 0x2a,
|
||||
RegRssiWideband = 0x2c,
|
||||
RegDetectionOptimize = 0x31,
|
||||
RegInvertiq = 0x33,
|
||||
RegDetectionThreshold = 0x37,
|
||||
RegSyncWord = 0x39,
|
||||
RegInvertiq2 = 0x3b,
|
||||
RegDioMapping1 = 0x40,
|
||||
RegVersion = 0x42,
|
||||
RegPaDacSX1272 = 0x5a,
|
||||
RegPaDacSX1276 = 0x4d,
|
||||
RegTcxoSX1276 = 0x4b,
|
||||
RegTcxoSX1272 = 0x58,
|
||||
}
|
||||
|
||||
impl Register {
|
||||
pub fn read_addr(self) -> u8 {
|
||||
(self as u8) & 0x7f
|
||||
}
|
||||
pub fn write_addr(self) -> u8 {
|
||||
(self as u8) | 0x80
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[allow(dead_code)]
|
||||
pub enum RampTime {
|
||||
Ramp3_4Ms = 0x00,
|
||||
Ramp2Ms = 0x01,
|
||||
Ramp1Ms = 0x02,
|
||||
Ramp500Us = 0x03,
|
||||
Ramp250Us = 0x04,
|
||||
Ramp125Us = 0x05,
|
||||
Ramp100Us = 0x06,
|
||||
Ramp62Us = 0x07,
|
||||
Ramp50Us = 0x08,
|
||||
Ramp40Us = 0x09,
|
||||
Ramp31Us = 0x0a,
|
||||
Ramp25Us = 0x0b,
|
||||
Ramp20Us = 0x0c,
|
||||
Ramp15Us = 0x0d,
|
||||
Ramp12Us = 0x0e,
|
||||
Ramp10Us = 0x0f,
|
||||
}
|
||||
|
||||
impl RampTime {
|
||||
pub fn value(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[allow(dead_code)]
|
||||
pub enum LnaGain {
|
||||
G1 = 0x20, // maximum gain (default)
|
||||
G2 = 0x40,
|
||||
G3 = 0x60,
|
||||
G4 = 0x80,
|
||||
G5 = 0xa0,
|
||||
G6 = 0xc0, // minumum gain
|
||||
}
|
||||
|
||||
impl LnaGain {
|
||||
pub fn value(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
pub fn boosted_value(self) -> u8 {
|
||||
(self as u8) | 0x03u8
|
||||
}
|
||||
}
|
||||
|
||||
/// PA DAC configuration - sx1276+
|
||||
#[derive(Clone, Copy)]
|
||||
#[allow(dead_code)]
|
||||
pub enum PaDac {
|
||||
_20DbmOn = 0x87,
|
||||
_20DbmOff = 0x84,
|
||||
}
|
||||
|
||||
impl PaDac {
|
||||
pub fn value(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
|
||||
/// PA configuration - sx1276+
|
||||
#[derive(Clone, Copy)]
|
||||
#[allow(dead_code)]
|
||||
pub enum PaConfig {
|
||||
PaBoost = 0x80,
|
||||
MaxPower7NoPaBoost = 0x70,
|
||||
}
|
||||
|
||||
impl PaConfig {
|
||||
pub fn value(self) -> u8 {
|
||||
self as u8
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
#[allow(dead_code)]
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
pub enum OcpTrim {
|
||||
_45Ma = 0x00,
|
||||
_50Ma = 0x01,
|
||||
_55Ma = 0x02,
|
||||
_60Ma = 0x03,
|
||||
_65Ma = 0x04,
|
||||
_70Ma = 0x05,
|
||||
_75Ma = 0x06,
|
||||
_80Ma = 0x07,
|
||||
_85Ma = 0x08,
|
||||
_90Ma = 0x09,
|
||||
_95Ma = 0x0a,
|
||||
_100Ma = 0x0b,
|
||||
_105Ma = 0x0c,
|
||||
_110Ma = 0x0d,
|
||||
_115Ma = 0x0e,
|
||||
_120Ma = 0x0f,
|
||||
_130Ma = 0x10,
|
||||
_140Ma = 0x11,
|
||||
_150Ma = 0x12,
|
||||
_160Ma = 0x13,
|
||||
_170Ma = 0x14,
|
||||
_180Ma = 0x15,
|
||||
_190Ma = 0x16,
|
||||
_200Ma = 0x17,
|
||||
_210Ma = 0x18,
|
||||
_220Ma = 0x19,
|
||||
_230Ma = 0x1a,
|
||||
_240Ma = 0x1b,
|
||||
}
|
||||
|
||||
impl OcpTrim {
|
||||
pub fn value(self) -> u8 {
|
||||
(self as u8) | 0x20u8 // value plus OCP on flag
|
||||
}
|
||||
}
|
||||
|
||||
pub fn spreading_factor_value(spreading_factor: SpreadingFactor) -> Result<u8, RadioError> {
|
||||
match spreading_factor {
|
||||
SpreadingFactor::_5 => Err(RadioError::UnavailableSpreadingFactor),
|
||||
SpreadingFactor::_6 => Ok(0x06),
|
||||
SpreadingFactor::_7 => Ok(0x07),
|
||||
SpreadingFactor::_8 => Ok(0x08),
|
||||
SpreadingFactor::_9 => Ok(0x09),
|
||||
SpreadingFactor::_10 => Ok(0x0A),
|
||||
SpreadingFactor::_11 => Ok(0x0B),
|
||||
SpreadingFactor::_12 => Ok(0x0C),
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn coding_rate_value(coding_rate: CodingRate) -> Result<u8, RadioError> {
|
||||
match coding_rate {
|
||||
CodingRate::_4_5 => Ok(0x01),
|
||||
CodingRate::_4_6 => Ok(0x02),
|
||||
CodingRate::_4_7 => Ok(0x03),
|
||||
CodingRate::_4_8 => Ok(0x04),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn coding_rate_denominator_value(coding_rate: CodingRate) -> Result<u8, RadioError> {
|
||||
match coding_rate {
|
||||
CodingRate::_4_5 => Ok(0x05),
|
||||
CodingRate::_4_6 => Ok(0x06),
|
||||
CodingRate::_4_7 => Ok(0x07),
|
||||
CodingRate::_4_8 => Ok(0x08),
|
||||
}
|
||||
}
|
||||
Vendored
+122
@@ -0,0 +1,122 @@
|
||||
use crate::mod_params::{ModulationParams, PacketParams, RadioError};
|
||||
use crate::mod_traits::InterfaceVariant;
|
||||
use crate::sx127x::radio_kind_params::{coding_rate_value, spreading_factor_value, RampTime, Register, Sx127xVariant};
|
||||
use crate::sx127x::{Sx127x, SX1272_RSSI_OFFSET};
|
||||
use embedded_hal_async::spi::SpiDevice;
|
||||
use lora_modulation::Bandwidth;
|
||||
|
||||
/// Sx1272 implements the Sx127xVariant trait
|
||||
pub struct Sx1272;
|
||||
|
||||
impl Sx127xVariant for Sx1272 {
|
||||
fn bandwidth_value(bw: Bandwidth) -> Result<u8, RadioError> {
|
||||
match bw {
|
||||
Bandwidth::_125KHz => Ok(0x00),
|
||||
Bandwidth::_250KHz => Ok(0x01),
|
||||
Bandwidth::_500KHz => Ok(0x02),
|
||||
_ => Err(RadioError::UnavailableBandwidth),
|
||||
}
|
||||
}
|
||||
|
||||
fn reg_txco() -> Register {
|
||||
Register::RegTcxoSX1272
|
||||
}
|
||||
|
||||
async fn set_tx_power<SPI: SpiDevice<u8>, IV: InterfaceVariant>(
|
||||
radio: &mut Sx127x<SPI, IV, Self>,
|
||||
p_out: i32,
|
||||
tx_boost: bool,
|
||||
) -> Result<(), RadioError> {
|
||||
// SX1272 has two output pins:
|
||||
// 1) RFO: (-1 to +14 dBm)
|
||||
// 2) PA_BOOST: (+2 to +17 dBm and +5 to 20 +dBm)
|
||||
|
||||
// RegPaConfig - 0x32
|
||||
// [7] - PaSelect (0: RFO, 1: PA_BOOST)
|
||||
// [6:4] - Unused: 0
|
||||
// [3:0] - Output power in dB steps
|
||||
|
||||
// RegPaDac - 0x5a (SX1272)
|
||||
// [7:3] - Reserved (0x10 as default)
|
||||
// [2:0] - PaDac: 0x04 default, 0x07 - enable +20 dBm on PA_BOOST
|
||||
|
||||
// TODO: Shall we also touch OCP settings?
|
||||
if tx_boost {
|
||||
// Deal with two ranges, +17dBm enables extra boost
|
||||
if p_out > 17 {
|
||||
// PA_BOOST out: +5 .. +20 dBm
|
||||
let val = (p_out.clamp(5, 20) - 5) as u8 & 0x0f;
|
||||
radio.write_register(Register::RegPaConfig, (1 << 7) | val).await?;
|
||||
radio.write_register(Register::RegPaDacSX1272, 0x87).await?;
|
||||
} else {
|
||||
// PA_BOOST out: +2 .. +17 dBm
|
||||
let val = (p_out.clamp(2, 17) - 2) as u8 & 0x0f;
|
||||
radio.write_register(Register::RegPaConfig, (1 << 7) | val).await?;
|
||||
radio.write_register(Register::RegPaDacSX1272, 0x84).await?;
|
||||
}
|
||||
} else {
|
||||
// RFO out: -1 to +14 dBm
|
||||
let val = (p_out.clamp(-1, 14) + 1) as u8 & 0x0f;
|
||||
radio.write_register(Register::RegPaConfig, val).await?;
|
||||
radio.write_register(Register::RegPaDacSX1272, 0x84).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ramp_value(ramp_time: RampTime) -> u8 {
|
||||
// Sx1272 - default: 0x19
|
||||
// [4]: LowPnTxPllOff - use higher power, lower phase noise PLL
|
||||
// only when the transmitter is used (default: 1)
|
||||
// 0 - Standard PLL used in Rx mode, Lower PN PLL in Tx
|
||||
// 1 - Standard PLL used in both Tx and Rx modes
|
||||
ramp_time.value() | (1 << 4)
|
||||
}
|
||||
|
||||
async fn set_modulation_params<SPI: SpiDevice<u8>, IV: InterfaceVariant>(
|
||||
radio: &mut Sx127x<SPI, IV, Self>,
|
||||
mdltn_params: &ModulationParams,
|
||||
) -> Result<(), RadioError> {
|
||||
let bw_val = Self::bandwidth_value(mdltn_params.bandwidth)?;
|
||||
let sf_val = spreading_factor_value(mdltn_params.spreading_factor)?;
|
||||
|
||||
let cfg1 = radio.read_register(Register::RegModemConfig1).await?;
|
||||
let ldro = mdltn_params.low_data_rate_optimize;
|
||||
let cr_val = coding_rate_value(mdltn_params.coding_rate)?;
|
||||
let val = (cfg1 & 0b110) | (bw_val << 6) | (cr_val << 3) | ldro;
|
||||
radio.write_register(Register::RegModemConfig1, val).await?;
|
||||
let cfg2 = radio.read_register(Register::RegModemConfig2).await?;
|
||||
let val = (cfg2 & 0b1111) | (sf_val << 4);
|
||||
radio.write_register(Register::RegModemConfig2, val).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn set_packet_params<SPI: SpiDevice<u8>, IV: InterfaceVariant>(
|
||||
radio: &mut Sx127x<SPI, IV, Self>,
|
||||
pkt_params: &PacketParams,
|
||||
) -> Result<(), RadioError>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
let modemcfg1 = radio.read_register(Register::RegModemConfig1).await?;
|
||||
|
||||
let hdr = pkt_params.implicit_header as u8;
|
||||
let crc = pkt_params.crc_on as u8;
|
||||
|
||||
let cfg1 = (modemcfg1 & 0b1111_1001) | (hdr << 2) | (crc << 1);
|
||||
radio.write_register(Register::RegModemConfig1, cfg1).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rssi_offset<SPI: SpiDevice<u8>, IV: InterfaceVariant>(
|
||||
_: &mut Sx127x<SPI, IV, Self>,
|
||||
) -> Result<i16, RadioError> {
|
||||
Ok(SX1272_RSSI_OFFSET)
|
||||
}
|
||||
|
||||
async fn set_tx_continuous_wave_mode<SPI: SpiDevice<u8>, IV: InterfaceVariant>(
|
||||
_: &mut Sx127x<SPI, IV, Self>,
|
||||
) -> Result<(), RadioError> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
Vendored
+170
@@ -0,0 +1,170 @@
|
||||
use crate::mod_params::{ModulationParams, PacketParams, RadioError};
|
||||
use crate::mod_traits::InterfaceVariant;
|
||||
use crate::sx127x::radio_kind_params::{
|
||||
coding_rate_denominator_value, spreading_factor_value, OcpTrim, PaConfig, PaDac, RampTime, Register, Sx127xVariant,
|
||||
};
|
||||
use crate::sx127x::{
|
||||
Sx127x, FREQUENCY_SYNTHESIZER_STEP, SX1276_RF_MID_BAND_THRESH, SX1276_RSSI_OFFSET_HF, SX1276_RSSI_OFFSET_LF,
|
||||
};
|
||||
use embedded_hal_async::spi::SpiDevice;
|
||||
use lora_modulation::Bandwidth;
|
||||
|
||||
/// Sx1276 implements the Sx127xVariant trait
|
||||
pub struct Sx1276;
|
||||
|
||||
impl Sx127xVariant for Sx1276 {
|
||||
fn bandwidth_value(bw: Bandwidth) -> Result<u8, RadioError> {
|
||||
match bw {
|
||||
Bandwidth::_7KHz => Ok(0x00),
|
||||
Bandwidth::_10KHz => Ok(0x01),
|
||||
Bandwidth::_15KHz => Ok(0x02),
|
||||
Bandwidth::_20KHz => Ok(0x03),
|
||||
Bandwidth::_31KHz => Ok(0x04),
|
||||
Bandwidth::_41KHz => Ok(0x05),
|
||||
Bandwidth::_62KHz => Ok(0x06),
|
||||
Bandwidth::_125KHz => Ok(0x07),
|
||||
Bandwidth::_250KHz => Ok(0x08),
|
||||
Bandwidth::_500KHz => Ok(0x09),
|
||||
}
|
||||
}
|
||||
|
||||
fn reg_txco() -> Register {
|
||||
Register::RegTcxoSX1276
|
||||
}
|
||||
|
||||
async fn set_tx_power<SPI: SpiDevice<u8>, IV: InterfaceVariant>(
|
||||
radio: &mut Sx127x<SPI, IV, Self>,
|
||||
p_out: i32,
|
||||
tx_boost: bool,
|
||||
) -> Result<(), RadioError> {
|
||||
let pa_reg = Register::RegPaDacSX1276;
|
||||
if tx_boost {
|
||||
// Output via PA_BOOST: [2, 20] dBm
|
||||
let txp = p_out.clamp(2, 20);
|
||||
|
||||
// Pout=17-(15-OutputPower)
|
||||
let output_power: i32 = txp - 2;
|
||||
|
||||
if txp > 17 {
|
||||
radio.write_register(pa_reg, PaDac::_20DbmOn.value()).await?;
|
||||
radio.set_ocp(OcpTrim::_240Ma).await?;
|
||||
} else {
|
||||
radio.write_register(pa_reg, PaDac::_20DbmOff.value()).await?;
|
||||
radio.set_ocp(OcpTrim::_100Ma).await?;
|
||||
}
|
||||
radio
|
||||
.write_register(Register::RegPaConfig, PaConfig::PaBoost.value() | (output_power as u8))
|
||||
.await?;
|
||||
} else {
|
||||
// Clamp output: [-4, 14] dBm
|
||||
let txp = p_out.clamp(-4, 14);
|
||||
|
||||
// Pmax=10.8+0.6*MaxPower, where MaxPower is set below as 7 and therefore Pmax is 15
|
||||
// Pout=Pmax-(15-OutputPower)
|
||||
let output_power: i32 = txp;
|
||||
|
||||
radio.write_register(pa_reg, PaDac::_20DbmOff.value()).await?;
|
||||
radio.set_ocp(OcpTrim::_100Ma).await?;
|
||||
radio
|
||||
.write_register(
|
||||
Register::RegPaConfig,
|
||||
PaConfig::MaxPower7NoPaBoost.value() | (output_power as u8),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ramp_value(ramp_time: RampTime) -> u8 {
|
||||
// Sx1276 - default: 0x09
|
||||
// [4]: reserved (0x00)
|
||||
ramp_time as u8
|
||||
}
|
||||
|
||||
async fn set_modulation_params<SPI: SpiDevice<u8>, IV: InterfaceVariant>(
|
||||
radio: &mut Sx127x<SPI, IV, Self>,
|
||||
mdltn_params: &ModulationParams,
|
||||
) -> Result<(), RadioError> {
|
||||
let bw_val = Self::bandwidth_value(mdltn_params.bandwidth)?;
|
||||
let sf_val = spreading_factor_value(mdltn_params.spreading_factor)?;
|
||||
let coding_rate_denominator_val = coding_rate_denominator_value(mdltn_params.coding_rate)?;
|
||||
|
||||
let mut config_2 = radio.read_register(Register::RegModemConfig2).await?;
|
||||
config_2 = (config_2 & 0x0fu8) | ((sf_val << 4) & 0xf0u8);
|
||||
radio.write_register(Register::RegModemConfig2, config_2).await?;
|
||||
|
||||
let mut config_1 = radio.read_register(Register::RegModemConfig1).await?;
|
||||
config_1 = (config_1 & 0x0fu8) | (bw_val << 4);
|
||||
radio.write_register(Register::RegModemConfig1, config_1).await?;
|
||||
|
||||
let cr = coding_rate_denominator_val - 4;
|
||||
config_1 = radio.read_register(Register::RegModemConfig1).await?;
|
||||
config_1 = (config_1 & 0xf1u8) | (cr << 1);
|
||||
radio.write_register(Register::RegModemConfig1, config_1).await?;
|
||||
|
||||
let mut ldro_agc_auto_flags = 0x00u8; // LDRO and AGC Auto both off
|
||||
if mdltn_params.low_data_rate_optimize != 0 {
|
||||
ldro_agc_auto_flags = 0x08u8; // LDRO on and AGC Auto off
|
||||
}
|
||||
let mut config_3 = radio.read_register(Register::RegModemConfig3).await?;
|
||||
config_3 = (config_3 & 0xf3u8) | ldro_agc_auto_flags;
|
||||
radio.write_register(Register::RegModemConfig3, config_3).await
|
||||
}
|
||||
|
||||
async fn set_packet_params<SPI: SpiDevice<u8>, IV: InterfaceVariant>(
|
||||
radio: &mut Sx127x<SPI, IV, Self>,
|
||||
pkt_params: &PacketParams,
|
||||
) -> Result<(), RadioError> {
|
||||
let mut config_1 = radio.read_register(Register::RegModemConfig1).await?;
|
||||
|
||||
if pkt_params.implicit_header {
|
||||
config_1 |= 0x01u8;
|
||||
} else {
|
||||
config_1 &= 0xfeu8;
|
||||
}
|
||||
radio.write_register(Register::RegModemConfig1, config_1).await?;
|
||||
|
||||
let mut config_2 = radio.read_register(Register::RegModemConfig2).await?;
|
||||
if pkt_params.crc_on {
|
||||
config_2 |= 0x04u8;
|
||||
} else {
|
||||
config_2 &= 0xfbu8;
|
||||
}
|
||||
radio.write_register(Register::RegModemConfig2, config_2).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn rssi_offset<SPI: SpiDevice<u8>, IV: InterfaceVariant>(
|
||||
radio: &mut Sx127x<SPI, IV, Self>,
|
||||
) -> Result<i16, RadioError> {
|
||||
let frequency_in_hz = {
|
||||
let msb = radio.read_register(Register::RegFrfMsb).await? as u32;
|
||||
let mid = radio.read_register(Register::RegFrfMid).await? as u32;
|
||||
let lsb = radio.read_register(Register::RegFrfLsb).await? as u32;
|
||||
let frf = (msb << 16) + (mid << 8) + lsb;
|
||||
(frf as f64 * FREQUENCY_SYNTHESIZER_STEP) as u32
|
||||
};
|
||||
|
||||
if frequency_in_hz > SX1276_RF_MID_BAND_THRESH {
|
||||
Ok(SX1276_RSSI_OFFSET_HF)
|
||||
} else {
|
||||
Ok(SX1276_RSSI_OFFSET_LF)
|
||||
}
|
||||
}
|
||||
|
||||
async fn set_tx_continuous_wave_mode<SPI: SpiDevice<u8>, IV: InterfaceVariant>(
|
||||
radio: &mut Sx127x<SPI, IV, Self>,
|
||||
) -> Result<(), RadioError> {
|
||||
radio.intf.iv.enable_rf_switch_rx().await?;
|
||||
let pa_config = radio.read_register(Register::RegPaConfig).await?;
|
||||
let new_pa_config = pa_config | 0b1000_0000;
|
||||
radio.write_register(Register::RegPaConfig, new_pa_config).await?;
|
||||
radio.write_register(Register::RegOpMode, 0b1100_0011).await?;
|
||||
let modem_config = radio.read_register(Register::RegModemConfig2).await?;
|
||||
let new_modem_config = modem_config | 0b0000_1000;
|
||||
radio
|
||||
.write_register(Register::RegModemConfig2, new_modem_config)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user