Skip to main content

pamoja_radios/sx127x/
config.rs

1//! The settings an SX127x is configured with in LoRa mode, and the values its registers take.
2//!
3//! Every value comes from the SX1276/77/78/79 datasheet (Rev 7): the carrier word of RegFrf,
4//! the bandwidth, coding rate, spreading factor, header, CRC, and low data rate settings of
5//! RegModemConfig1 to 3, the amplifier modes of Tables 33 and 34, the current limit of
6//! Table 37, the SF6 detection settings, and the sync word. Two sets of values follow
7//! Semtech's LoRaMac-node reference driver instead, and say so where they are defined: the
8//! IQ polarity bit of the transmit path, which the datasheet describes the wrong way round,
9//! and the register writes of the errata for 500 kHz sensitivity and spurious reception.
10//! The LoRa settings are built from a [`LinkSettings`], so a radio sends exactly the frames
11//! `pamoja-lora` computes the airtime of.
12
13use pamoja_lora::budget::{Decibels, LinkBudget};
14use pamoja_lora::LinkSettings;
15
16use super::status::MID_BAND_HZ;
17
18/// The crystal frequency the synthesizer divides, in hertz.
19pub const XTAL_HZ: u32 = 32_000_000;
20
21/// The top of the lowest band, 137 to 175 MHz, where the 250 and 500 kHz bandwidths are not
22/// supported.
23pub const LOW_BAND_TOP_HZ: u32 = 175_000_000;
24
25/// Returns the 24-bit RegFrf word for a frequency.
26///
27/// The datasheet defines the carrier as the word times the crystal frequency over 2^19, a
28/// step of 61.035 Hz. The word is rounded to the nearest step.
29///
30/// # Arguments
31///
32/// * `frequency_hz` - the carrier frequency in hertz.
33///
34/// # Returns
35///
36/// The frequency word.
37///
38/// # Examples
39///
40/// ```
41/// use pamoja_radios::sx127x::config::frequency_word;
42///
43/// // 434 MHz is the RegFrf reset value the datasheet gives.
44/// assert_eq!(frequency_word(434_000_000), 0x6C_8000);
45/// assert_eq!(frequency_word(868_100_000), 0xD9_0666);
46/// assert_eq!(frequency_word(915_000_000), 0xE4_C000);
47/// ```
48pub const fn frequency_word(frequency_hz: u32) -> u32 {
49    ((((frequency_hz as u64) << 19) + (XTAL_HZ as u64 / 2)) / XTAL_HZ as u64) as u32
50}
51
52/// Returns the frequency a RegFrf word selects.
53///
54/// # Arguments
55///
56/// * `word` - the 24-bit frequency word.
57///
58/// # Returns
59///
60/// The carrier frequency in hertz, rounded to the nearest hertz.
61pub const fn frequency_from_word(word: u32) -> u32 {
62    ((word as u64 * XTAL_HZ as u64 + (1 << 18)) >> 19) as u32
63}
64
65/// Returns the three bytes written to RegFrfMsb, RegFrfMid, and RegFrfLsb for a frequency.
66///
67/// # Arguments
68///
69/// * `frequency_hz` - the carrier frequency in hertz.
70///
71/// # Returns
72///
73/// The frequency word, most significant byte first.
74pub const fn frequency_bytes(frequency_hz: u32) -> [u8; 3] {
75    let word = frequency_word(frequency_hz);
76    [(word >> 16) as u8, (word >> 8) as u8, word as u8]
77}
78
79/// A LoRa signal bandwidth, the Bw bits of RegModemConfig1.
80#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
81pub enum LoraBandwidth {
82    /// 7.8 kHz (0000).
83    Khz7_8,
84    /// 10.4 kHz (0001).
85    Khz10_4,
86    /// 15.6 kHz (0010).
87    Khz15_6,
88    /// 20.8 kHz (0011).
89    Khz20_8,
90    /// 31.25 kHz (0100).
91    Khz31_25,
92    /// 41.7 kHz (0101).
93    Khz41_7,
94    /// 62.5 kHz (0110).
95    Khz62_5,
96    /// 125 kHz (0111).
97    Khz125,
98    /// 250 kHz (1000).
99    Khz250,
100    /// 500 kHz (1001).
101    Khz500,
102}
103
104impl LoraBandwidth {
105    const ALL: [LoraBandwidth; 10] = [
106        LoraBandwidth::Khz7_8,
107        LoraBandwidth::Khz10_4,
108        LoraBandwidth::Khz15_6,
109        LoraBandwidth::Khz20_8,
110        LoraBandwidth::Khz31_25,
111        LoraBandwidth::Khz41_7,
112        LoraBandwidth::Khz62_5,
113        LoraBandwidth::Khz125,
114        LoraBandwidth::Khz250,
115        LoraBandwidth::Khz500,
116    ];
117
118    /// Returns the Bw bits.
119    ///
120    /// # Returns
121    ///
122    /// The four bit code, 0 to 9.
123    pub const fn code(self) -> u8 {
124        match self {
125            LoraBandwidth::Khz7_8 => 0,
126            LoraBandwidth::Khz10_4 => 1,
127            LoraBandwidth::Khz15_6 => 2,
128            LoraBandwidth::Khz20_8 => 3,
129            LoraBandwidth::Khz31_25 => 4,
130            LoraBandwidth::Khz41_7 => 5,
131            LoraBandwidth::Khz62_5 => 6,
132            LoraBandwidth::Khz125 => 7,
133            LoraBandwidth::Khz250 => 8,
134            LoraBandwidth::Khz500 => 9,
135        }
136    }
137
138    /// Returns the bandwidth in hertz, rounded to the nearest hertz.
139    ///
140    /// # Returns
141    ///
142    /// The bandwidth: 500 kHz halved, or 125 kHz divided by 3 and halved, as many times as
143    /// the setting takes.
144    pub const fn hz(self) -> u32 {
145        match self {
146            LoraBandwidth::Khz7_8 => 7_813,
147            LoraBandwidth::Khz10_4 => 10_417,
148            LoraBandwidth::Khz15_6 => 15_625,
149            LoraBandwidth::Khz20_8 => 20_833,
150            LoraBandwidth::Khz31_25 => 31_250,
151            LoraBandwidth::Khz41_7 => 41_667,
152            LoraBandwidth::Khz62_5 => 62_500,
153            LoraBandwidth::Khz125 => 125_000,
154            LoraBandwidth::Khz250 => 250_000,
155            LoraBandwidth::Khz500 => 500_000,
156        }
157    }
158
159    /// Finds the setting for a bandwidth in hertz.
160    ///
161    /// # Arguments
162    ///
163    /// * `hz` - the bandwidth in hertz, within 1% of one the chip supports.
164    ///
165    /// # Returns
166    ///
167    /// The setting, or `None` for a bandwidth the SX127x does not offer.
168    pub fn from_hz(hz: u32) -> Option<LoraBandwidth> {
169        LoraBandwidth::ALL.into_iter().find(|bandwidth| {
170            let nominal = u64::from(bandwidth.hz());
171            u64::from(hz).abs_diff(nominal) * 100 <= nominal
172        })
173    }
174
175    /// Finds the setting the Bw bits of RegModemConfig1 name.
176    ///
177    /// # Arguments
178    ///
179    /// * `code` - the four bit code.
180    ///
181    /// # Returns
182    ///
183    /// The setting, or `None` for a code above 9, which the datasheet reserves.
184    ///
185    /// # Examples
186    ///
187    /// ```
188    /// use pamoja_radios::sx127x::config::LoraBandwidth;
189    ///
190    /// assert_eq!(LoraBandwidth::from_code(7), Some(LoraBandwidth::Khz125));
191    /// assert_eq!(LoraBandwidth::from_code(10), None);
192    /// ```
193    pub fn from_code(code: u8) -> Option<LoraBandwidth> {
194        LoraBandwidth::ALL
195            .into_iter()
196            .find(|bandwidth| bandwidth.code() == code)
197    }
198
199    /// Reports whether the bandwidth is available at a carrier frequency.
200    ///
201    /// # Arguments
202    ///
203    /// * `frequency_hz` - the carrier frequency in hertz.
204    ///
205    /// # Returns
206    ///
207    /// `false` for 250 and 500 kHz at or below [`LOW_BAND_TOP_HZ`], which the datasheet
208    /// excludes in the lowest band, and `true` otherwise.
209    pub const fn in_band(self, frequency_hz: u32) -> bool {
210        !(frequency_hz <= LOW_BAND_TOP_HZ
211            && matches!(self, LoraBandwidth::Khz250 | LoraBandwidth::Khz500))
212    }
213}
214
215/// Why a link's settings cannot be put on an SX127x.
216#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
217pub enum ModulationError {
218    /// The bandwidth, in hertz, is not one the SX127x offers, or not one it offers at the
219    /// carrier frequency.
220    Bandwidth(u32),
221    /// The spreading factor is below SF6, the lowest the SX127x offers.
222    SpreadingFactor(u8),
223    /// SF6 with an explicit header, which the datasheet does not allow.
224    ExplicitHeaderAtSf6,
225}
226
227/// The LoRa modem settings of a link, as RegModemConfig1 to 3 and the SF6 detection
228/// registers carry them.
229///
230/// # Examples
231///
232/// ```
233/// use pamoja_lora::LinkSettings;
234/// use pamoja_radios::sx127x::config::LoraModulation;
235///
236/// // SF7 at 125 kHz, coding rate 4/5, an explicit header, and a CRC.
237/// let modulation = LoraModulation::from_link(&LinkSettings::new(7, 125_000)).unwrap();
238/// assert_eq!(modulation.modem_config_1(), 0x72);
239/// assert_eq!(modulation.modem_config_2(0), 0x74);
240/// assert_eq!(modulation.modem_config_3(), 0x04);
241/// ```
242#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
243pub struct LoraModulation {
244    /// The spreading factor, 6 to 12.
245    pub spreading_factor: u8,
246    /// The signal bandwidth.
247    pub bandwidth: LoraBandwidth,
248    /// The coding rate denominator, 5 to 8 for 4/5 to 4/8.
249    pub coding_rate_denominator: u8,
250    /// Whether packets carry an explicit header.
251    pub explicit_header: bool,
252    /// Whether packets carry a payload CRC.
253    pub crc: bool,
254    /// Whether low data rate optimization is on.
255    pub low_data_rate_optimization: bool,
256}
257
258impl LoraModulation {
259    /// Builds the modem settings of a link.
260    ///
261    /// # Arguments
262    ///
263    /// * `link` - the link settings, whose spreading factor, bandwidth, coding rate, header,
264    ///   CRC, and low data rate optimization the radio must match.
265    ///
266    /// # Returns
267    ///
268    /// The settings.
269    ///
270    /// # Errors
271    ///
272    /// Returns [`ModulationError::Bandwidth`] for a bandwidth the SX127x does not offer,
273    /// [`ModulationError::SpreadingFactor`] below SF6, and
274    /// [`ModulationError::ExplicitHeaderAtSf6`] for SF6 with an explicit header.
275    pub fn from_link(link: &LinkSettings) -> Result<LoraModulation, ModulationError> {
276        let bandwidth = LoraBandwidth::from_hz(link.bandwidth_hz())
277            .ok_or(ModulationError::Bandwidth(link.bandwidth_hz()))?;
278        let spreading_factor = link.spreading_factor();
279        if spreading_factor < 6 {
280            return Err(ModulationError::SpreadingFactor(spreading_factor));
281        }
282        if spreading_factor == 6 && link.explicit_header() {
283            return Err(ModulationError::ExplicitHeaderAtSf6);
284        }
285        Ok(LoraModulation {
286            spreading_factor,
287            bandwidth,
288            coding_rate_denominator: link.coding_rate_denominator(),
289            explicit_header: link.explicit_header(),
290            crc: link.crc(),
291            low_data_rate_optimization: link.low_data_rate_optimization(),
292        })
293    }
294
295    /// Returns RegModemConfig1: the bandwidth in bits 7 to 4, the coding rate in bits 3 to 1,
296    /// and ImplicitHeaderModeOn in bit 0.
297    ///
298    /// # Returns
299    ///
300    /// The register value.
301    pub const fn modem_config_1(&self) -> u8 {
302        let coding_rate = if self.coding_rate_denominator < 5 {
303            1
304        } else if self.coding_rate_denominator > 8 {
305            4
306        } else {
307            self.coding_rate_denominator - 4
308        };
309        (self.bandwidth.code() << 4) | (coding_rate << 1) | (!self.explicit_header as u8)
310    }
311
312    /// Returns RegModemConfig2: the spreading factor in bits 7 to 4, RxPayloadCrcOn in bit 2,
313    /// and the top two bits of the symbol timeout in bits 1 and 0.
314    ///
315    /// # Arguments
316    ///
317    /// * `symbol_timeout` - the single reception timeout in symbols, from
318    ///   [`symbol_timeout`].
319    ///
320    /// # Returns
321    ///
322    /// The register value; TxContinuousMode is off.
323    pub const fn modem_config_2(&self, symbol_timeout: u16) -> u8 {
324        (self.spreading_factor << 4)
325            | ((self.crc as u8) << 2)
326            | ((symbol_timeout >> 8) as u8 & 0x03)
327    }
328
329    /// Returns RegModemConfig3: LowDataRateOptimize in bit 3, and AgcAutoOn in bit 2 so the
330    /// automatic gain control sets the LNA gain.
331    ///
332    /// # Returns
333    ///
334    /// The register value.
335    pub const fn modem_config_3(&self) -> u8 {
336        ((self.low_data_rate_optimization as u8) << 3) | MODEM_CONFIG_3_AGC_AUTO_ON
337    }
338
339    /// Returns RegDetectOptimize with the DetectionOptimize bits set for the spreading
340    /// factor: 0x05 for SF6 and 0x03 for SF7 to SF12.
341    ///
342    /// # Arguments
343    ///
344    /// * `current` - the register's current value, whose other bits are kept.
345    ///
346    /// # Returns
347    ///
348    /// The register value.
349    pub const fn detect_optimize(&self, current: u8) -> u8 {
350        let optimize = if self.spreading_factor == 6 {
351            0x05
352        } else {
353            0x03
354        };
355        (current & 0xF8) | optimize
356    }
357
358    /// Returns RegDetectionThreshold for the spreading factor: 0x0C for SF6 and 0x0A for
359    /// SF7 to SF12.
360    ///
361    /// # Returns
362    ///
363    /// The register value.
364    pub const fn detection_threshold(&self) -> u8 {
365        if self.spreading_factor == 6 {
366            0x0C
367        } else {
368            0x0A
369        }
370    }
371}
372
373/// RegModemConfig3 bit 2: the LNA gain comes from the automatic gain control.
374pub const MODEM_CONFIG_3_AGC_AUTO_ON: u8 = 0x04;
375
376/// The shortest single reception timeout the datasheet allows, in symbols.
377pub const SYMBOL_TIMEOUT_MIN: u16 = 4;
378
379/// The longest single reception timeout RegModemConfig2 and RegSymbTimeoutLsb hold, in
380/// symbols.
381pub const SYMBOL_TIMEOUT_MAX: u16 = 1023;
382
383/// Returns the single reception timeout for a duration, in the link's symbols.
384///
385/// # Arguments
386///
387/// * `link` - the link settings, whose symbol time counts the timeout.
388/// * `timeout_us` - how long to listen for a preamble, in microseconds.
389///
390/// # Returns
391///
392/// The timeout rounded up to whole symbols, from [`SYMBOL_TIMEOUT_MIN`] to
393/// [`SYMBOL_TIMEOUT_MAX`].
394///
395/// # Examples
396///
397/// ```
398/// use pamoja_lora::LinkSettings;
399/// use pamoja_radios::sx127x::config::symbol_timeout;
400///
401/// // SF7 at 125 kHz sends a symbol every 1.024 ms.
402/// let link = LinkSettings::new(7, 125_000);
403/// assert_eq!(symbol_timeout(&link, 100_000), 98);
404/// assert_eq!(symbol_timeout(&link, 0), 4);
405/// assert_eq!(symbol_timeout(&link, 10_000_000), 1023);
406/// ```
407pub fn symbol_timeout(link: &LinkSettings, timeout_us: u64) -> u16 {
408    let symbols = timeout_us.div_ceil(link.symbol_time_us().max(1));
409    symbols.clamp(u64::from(SYMBOL_TIMEOUT_MIN), u64::from(SYMBOL_TIMEOUT_MAX)) as u16
410}
411
412/// Returns RegPreambleMsb and RegPreambleLsb for a link.
413///
414/// # Arguments
415///
416/// * `link` - the link settings.
417///
418/// # Returns
419///
420/// The preamble length in symbols, most significant byte first; the modem adds 4.25
421/// symbols of its own.
422pub fn preamble_bytes(link: &LinkSettings) -> [u8; 2] {
423    link.preamble_symbols().to_be_bytes()
424}
425
426/// A LoRa sync word, written to RegSyncWord.
427#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
428pub enum SyncWord {
429    /// 0x34, which the datasheet reserves for LoRaWAN networks.
430    Public,
431    /// 0x12, for a private network, and the chip's reset value.
432    Private,
433    /// Another value.
434    Custom(u8),
435}
436
437impl SyncWord {
438    /// Returns the register value.
439    ///
440    /// # Returns
441    ///
442    /// The RegSyncWord byte.
443    pub const fn to_byte(self) -> u8 {
444        match self {
445            SyncWord::Public => 0x34,
446            SyncWord::Private => 0x12,
447            SyncWord::Custom(word) => word,
448        }
449    }
450}
451
452/// Which amplifier output a module wires to its antenna, from Table 33.
453#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
454pub enum PaOutput {
455    /// The high efficiency amplifier on RFO_LF or RFO_HF, -4 to +15 dBm.
456    Rfo,
457    /// The regulated amplifier on PA_BOOST, +2 to +17 dBm, or +20 dBm with the high power
458    /// setting of Table 34. The RFM95W wires its antenna here.
459    PaBoost,
460}
461
462impl PaOutput {
463    /// Returns the output powers the amplifier delivers.
464    ///
465    /// # Returns
466    ///
467    /// The lowest and highest in dBm.
468    pub const fn range_dbm(self) -> (i8, i8) {
469        match self {
470            PaOutput::Rfo => (-4, 15),
471            PaOutput::PaBoost => (2, 20),
472        }
473    }
474}
475
476/// RegPaDac at its reset value.
477pub const PA_DAC_DEFAULT: u8 = 0x84;
478
479/// RegPaDac with the +20 dBm setting of Table 34 on PA_BOOST.
480pub const PA_DAC_HIGH_POWER: u8 = 0x87;
481
482/// Returns RegOcp for a current limit, from Table 37.
483///
484/// # Arguments
485///
486/// * `milliamps` - the most current the amplifier may draw.
487///
488/// # Returns
489///
490/// The register value with OcpOn set and the trim that gives the limit: 45 + 5 * trim mA up
491/// to 120 mA, -30 + 10 * trim mA up to 240 mA, and 240 mA above.
492///
493/// # Examples
494///
495/// ```
496/// use pamoja_radios::sx127x::config::ocp_register;
497///
498/// // 100 mA is the RegOcp reset value.
499/// assert_eq!(ocp_register(100), 0x2B);
500/// assert_eq!(ocp_register(140), 0x31);
501/// ```
502pub const fn ocp_register(milliamps: u16) -> u8 {
503    let trim = if milliamps <= 45 {
504        0
505    } else if milliamps <= 120 {
506        (milliamps - 45) / 5
507    } else if milliamps <= 240 {
508        (milliamps + 30) / 10
509    } else {
510        28
511    };
512    0x20 | trim as u8
513}
514
515/// The amplifier settings that produce an output power: RegPaConfig, RegPaDac, and RegOcp.
516///
517/// # Examples
518///
519/// ```
520/// use pamoja_radios::sx127x::config::{PaOutput, TxPower, PA_DAC_HIGH_POWER};
521///
522/// // +20 dBm on the PA_BOOST pin of an RFM95W.
523/// let power = TxPower::for_output(PaOutput::PaBoost, 20);
524/// assert_eq!(power.pa_config, 0xFF);
525/// assert_eq!(power.pa_dac, PA_DAC_HIGH_POWER);
526/// assert_eq!(power.output_dbm, 20);
527/// ```
528#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
529pub struct TxPower {
530    /// RegPaConfig: PaSelect in bit 7, MaxPower in bits 6 to 4, OutputPower in bits 3 to 0.
531    pub pa_config: u8,
532    /// RegPaDac: [`PA_DAC_HIGH_POWER`] above +17 dBm on PA_BOOST, else [`PA_DAC_DEFAULT`].
533    pub pa_dac: u8,
534    /// RegOcp: a 140 mA limit with the high power setting, whose amplifier draws 120 mA at
535    /// +20 dBm, and the 100 mA reset limit otherwise.
536    pub ocp: u8,
537    /// The output power these settings produce, in dBm.
538    pub output_dbm: i8,
539}
540
541impl TxPower {
542    /// Chooses the settings for an output power.
543    ///
544    /// The steps follow Semtech's LoRaMac-node. On RFO, MaxPower 7 gives a +15 dBm maximum
545    /// and OutputPower the power itself, and at 0 dBm and below MaxPower 0 and OutputPower
546    /// the power plus 4, which lands 0.2 dB under. On PA_BOOST, OutputPower is the power
547    /// less 2 up to +17 dBm, and with the high power setting the power less 5 above it.
548    ///
549    /// # Arguments
550    ///
551    /// * `output` - the amplifier output the module uses.
552    /// * `output_dbm` - the output power wanted, clamped to what the output delivers.
553    ///
554    /// # Returns
555    ///
556    /// The settings.
557    pub const fn for_output(output: PaOutput, output_dbm: i8) -> TxPower {
558        let (low, high) = output.range_dbm();
559        let dbm = if output_dbm < low {
560            low
561        } else if output_dbm > high {
562            high
563        } else {
564            output_dbm
565        };
566        match output {
567            PaOutput::Rfo if dbm > 0 => TxPower {
568                pa_config: 0x70 | dbm as u8,
569                pa_dac: PA_DAC_DEFAULT,
570                ocp: ocp_register(100),
571                output_dbm: dbm,
572            },
573            PaOutput::Rfo => TxPower {
574                pa_config: (dbm + 4) as u8,
575                pa_dac: PA_DAC_DEFAULT,
576                ocp: ocp_register(100),
577                output_dbm: dbm,
578            },
579            PaOutput::PaBoost if dbm > 17 => TxPower {
580                pa_config: 0xF0 | (dbm - 5) as u8,
581                pa_dac: PA_DAC_HIGH_POWER,
582                ocp: ocp_register(140),
583                output_dbm: dbm,
584            },
585            PaOutput::PaBoost => TxPower {
586                pa_config: 0xF0 | (dbm - 2) as u8,
587                pa_dac: PA_DAC_DEFAULT,
588                ocp: ocp_register(100),
589                output_dbm: dbm,
590            },
591        }
592    }
593
594    /// Chooses the settings that keep a link's EIRP at or under a ceiling.
595    ///
596    /// # Arguments
597    ///
598    /// * `output` - the amplifier output the module uses.
599    /// * `budget` - the link budget, whose transmitting antenna and cable apply.
600    /// * `eirp_ceiling_dbm` - the EIRP limit, such as a channel plan's ceiling for the
601    ///   frequency in use.
602    ///
603    /// # Returns
604    ///
605    /// The settings, rounded down to whole decibels so the EIRP stays under the ceiling.
606    pub fn under_ceiling(
607        output: PaOutput,
608        budget: &LinkBudget,
609        eirp_ceiling_dbm: Decibels,
610    ) -> TxPower {
611        let most = budget.max_transmit_power_dbm(eirp_ceiling_dbm).floor_db();
612        let dbm = most.clamp(i32::from(i8::MIN), i32::from(i8::MAX)) as i8;
613        TxPower::for_output(output, dbm)
614    }
615
616    /// Returns the amplifier output these settings select.
617    ///
618    /// # Returns
619    ///
620    /// [`PaOutput::PaBoost`] when PaSelect is set, else [`PaOutput::Rfo`].
621    pub const fn output(&self) -> PaOutput {
622        if self.pa_config & 0x80 != 0 {
623            PaOutput::PaBoost
624        } else {
625            PaOutput::Rfo
626        }
627    }
628}
629
630/// The reserved bits of RegInvertIQ at their reset value.
631const INVERT_IQ_RESERVED: u8 = 0x26;
632
633/// Returns RegInvertIQ for the IQ polarity of each path.
634///
635/// Bit 6 inverts the receive path as the datasheet describes. Bit 0 of the transmit path
636/// works the other way from its description: Semtech's LoRaMac-node sets it for normal IQ
637/// and clears it to invert, which RadioLib and arduino-LoRa also do after finding the
638/// datasheet's reading inverts the wrong frames.
639///
640/// # Arguments
641///
642/// * `receive` - whether to invert the receive path, as a LoRaWAN device does for downlinks.
643/// * `transmit` - whether to invert the transmit path, as a gateway does.
644///
645/// # Returns
646///
647/// The register value.
648///
649/// # Examples
650///
651/// ```
652/// use pamoja_radios::sx127x::config::invert_iq;
653///
654/// assert_eq!(invert_iq(false, false), 0x27);
655/// assert_eq!(invert_iq(true, true), 0x66);
656/// ```
657pub const fn invert_iq(receive: bool, transmit: bool) -> u8 {
658    INVERT_IQ_RESERVED | if receive { 0x40 } else { 0x00 } | if transmit { 0x00 } else { 0x01 }
659}
660
661/// Returns RegInvertIQ2, which the chip needs set to 0x19 while a path is inverted.
662///
663/// # Arguments
664///
665/// * `inverted` - whether the path in use is inverted.
666///
667/// # Returns
668///
669/// 0x19 when inverted, else its reset value 0x1D.
670pub const fn invert_iq_2(inverted: bool) -> u8 {
671    if inverted {
672        0x19
673    } else {
674        0x1D
675    }
676}
677
678/// RegDioMapping1 with DIO0 signaling RxDone, from Table 18.
679pub const DIO0_RX_DONE: u8 = 0x00;
680/// RegDioMapping1 with DIO0 signaling TxDone.
681pub const DIO0_TX_DONE: u8 = 0x40;
682/// RegDioMapping1 with DIO0 signaling CadDone.
683pub const DIO0_CAD_DONE: u8 = 0x80;
684
685/// RegImageCal bit 6: starts an image and RSSI calibration.
686pub const IMAGE_CAL_START: u8 = 0x40;
687/// RegImageCal bit 5: set while a calibration runs.
688pub const IMAGE_CAL_RUNNING: u8 = 0x20;
689
690/// Returns RegImageCal to start a calibration.
691///
692/// # Arguments
693///
694/// * `current` - the register's current value.
695///
696/// # Returns
697///
698/// The value with ImageCalStart set and AutoImageCalOn clear, since the datasheet recommends
699/// triggering calibration deliberately rather than on a temperature change.
700pub const fn image_cal_start(current: u8) -> u8 {
701    (current & 0x3F) | IMAGE_CAL_START
702}
703
704/// RegLna with the maximum gain and the 150% LNA current of the high frequency port, which
705/// LoRaMac-node sets at startup; with AgcAutoOn the gain is the AGC's.
706pub const LNA_BOOSTED: u8 = 0x23;
707
708/// RegTcxo with TcxoInputOn set, for a module clocked by a TCXO on XTA.
709pub const TCXO_INPUT_ON: u8 = 0x19;
710
711/// RegHighBwOptimize1 and, where it changes, RegHighBwOptimize2 for a bandwidth.
712///
713/// These are the writes of erratum 2.1, sensitivity optimization with a 500 kHz bandwidth, as
714/// Semtech's LoRaMac-node makes them: 0x02 and 0x64 above [`MID_BAND_HZ`], 0x02 and 0x7F
715/// below, and 0x03 alone for any other bandwidth.
716///
717/// # Arguments
718///
719/// * `bandwidth` - the signal bandwidth.
720/// * `frequency_hz` - the carrier frequency in hertz.
721///
722/// # Returns
723///
724/// The RegHighBwOptimize1 value, and the RegHighBwOptimize2 value when one is written.
725pub const fn high_bw_optimize(bandwidth: LoraBandwidth, frequency_hz: u32) -> (u8, Option<u8>) {
726    match bandwidth {
727        LoraBandwidth::Khz500 if frequency_hz > MID_BAND_HZ => (0x02, Some(0x64)),
728        LoraBandwidth::Khz500 => (0x02, Some(0x7F)),
729        _ => (0x03, None),
730    }
731}
732
733/// The receive settings of erratum 2.3, receiver spurious reception of a LoRa signal.
734#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
735pub struct SpuriousReception {
736    /// Whether AutomaticIFOn, bit 7 of RegDetectOptimize, stays on.
737    pub automatic_if: bool,
738    /// The value for RegIfFreq2 at 0x2F, with RegIfFreq1 at 0x30 cleared, when the IF is set
739    /// by hand.
740    pub if_freq_2: Option<u8>,
741    /// How far above the carrier to receive, in hertz.
742    pub offset_hz: u32,
743}
744
745/// Returns the receive settings of erratum 2.3 for a bandwidth, as Semtech's LoRaMac-node
746/// applies them.
747///
748/// At 500 kHz the automatic IF stays on. Below it the IF is set by hand, and at 41.7 kHz and
749/// narrower the receiver also tunes one bandwidth above the carrier.
750///
751/// # Arguments
752///
753/// * `bandwidth` - the signal bandwidth.
754///
755/// # Returns
756///
757/// The settings.
758///
759/// # Examples
760///
761/// ```
762/// use pamoja_radios::sx127x::config::{spurious_reception, LoraBandwidth};
763///
764/// let narrow = spurious_reception(LoraBandwidth::Khz20_8);
765/// assert_eq!((narrow.if_freq_2, narrow.offset_hz), (Some(0x44), 20_830));
766/// assert!(spurious_reception(LoraBandwidth::Khz500).automatic_if);
767/// ```
768pub const fn spurious_reception(bandwidth: LoraBandwidth) -> SpuriousReception {
769    let (if_freq_2, offset_hz) = match bandwidth {
770        LoraBandwidth::Khz500 => {
771            return SpuriousReception {
772                automatic_if: true,
773                if_freq_2: None,
774                offset_hz: 0,
775            }
776        }
777        LoraBandwidth::Khz7_8 => (0x48, 7_810),
778        LoraBandwidth::Khz10_4 => (0x44, 10_420),
779        LoraBandwidth::Khz15_6 => (0x44, 15_620),
780        LoraBandwidth::Khz20_8 => (0x44, 20_830),
781        LoraBandwidth::Khz31_25 => (0x44, 31_250),
782        LoraBandwidth::Khz41_7 => (0x44, 41_670),
783        LoraBandwidth::Khz62_5 | LoraBandwidth::Khz125 | LoraBandwidth::Khz250 => (0x40, 0),
784    };
785    SpuriousReception {
786        automatic_if: false,
787        if_freq_2: Some(if_freq_2),
788        offset_hz,
789    }
790}
791
792/// Returns RegDetectOptimize with AutomaticIFOn as erratum 2.3 wants it.
793///
794/// # Arguments
795///
796/// * `current` - the register's current value, whose other bits are kept.
797/// * `automatic_if` - whether the automatic IF stays on.
798///
799/// # Returns
800///
801/// The register value.
802pub const fn automatic_if(current: u8, automatic_if: bool) -> u8 {
803    if automatic_if {
804        current | 0x80
805    } else {
806        current & 0x7F
807    }
808}
809
810#[cfg(test)]
811mod tests {
812    use super::*;
813
814    #[test]
815    fn frequency_words_round_trip_to_within_half_a_step() {
816        for frequency in [
817            137_000_000,
818            433_175_000,
819            868_100_000,
820            902_300_000,
821            1_020_000_000,
822        ] {
823            let back = frequency_from_word(frequency_word(frequency));
824            assert!(
825                back.abs_diff(frequency) <= 31,
826                "{frequency} came back as {back}"
827            );
828        }
829        assert_eq!(frequency_bytes(868_100_000), [0xD9, 0x06, 0x66]);
830    }
831
832    #[test]
833    fn bandwidths_take_the_codes_of_modem_config_1_and_match_within_one_percent() {
834        assert_eq!(LoraBandwidth::from_hz(125_000), Some(LoraBandwidth::Khz125));
835        assert_eq!(LoraBandwidth::from_hz(10_420), Some(LoraBandwidth::Khz10_4));
836        assert_eq!(LoraBandwidth::from_hz(203_125), None);
837        assert_eq!(LoraBandwidth::Khz7_8.code(), 0);
838        assert_eq!(LoraBandwidth::Khz500.code(), 9);
839        assert!(!LoraBandwidth::Khz500.in_band(169_000_000));
840        assert!(LoraBandwidth::Khz125.in_band(169_000_000));
841        assert!(LoraBandwidth::Khz500.in_band(433_000_000));
842    }
843
844    #[test]
845    fn a_slow_link_turns_on_low_data_rate_optimization() {
846        let slow = LoraModulation::from_link(&LinkSettings::new(12, 125_000)).unwrap();
847        assert_eq!(slow.modem_config_1(), 0x72);
848        assert_eq!(slow.modem_config_2(0), 0xC4);
849        assert_eq!(slow.modem_config_3(), 0x0C);
850    }
851
852    #[test]
853    fn the_modem_settings_carry_coding_rate_header_crc_and_timeout() {
854        let link = LinkSettings::new(9, 250_000)
855            .with_coding_rate(8)
856            .implicit_header()
857            .without_crc();
858        let modulation = LoraModulation::from_link(&link).unwrap();
859        assert_eq!(modulation.modem_config_1(), 0x89);
860        assert_eq!(modulation.modem_config_2(0x3FF), 0x93);
861        assert_eq!(modulation.detect_optimize(0xC5), 0xC3);
862        assert_eq!(modulation.detection_threshold(), 0x0A);
863    }
864
865    #[test]
866    fn sf6_needs_an_implicit_header_and_its_own_detection_settings() {
867        assert_eq!(
868            LoraModulation::from_link(&LinkSettings::new(6, 125_000)),
869            Err(ModulationError::ExplicitHeaderAtSf6)
870        );
871        let sf6 =
872            LoraModulation::from_link(&LinkSettings::new(6, 125_000).implicit_header()).unwrap();
873        assert_eq!(sf6.detect_optimize(0xC3), 0xC5);
874        assert_eq!(sf6.detection_threshold(), 0x0C);
875        assert_eq!(
876            LoraModulation::from_link(&LinkSettings::new(5, 125_000)),
877            Err(ModulationError::SpreadingFactor(5))
878        );
879    }
880
881    #[test]
882    fn the_current_limit_follows_table_37() {
883        assert_eq!(ocp_register(45), 0x20);
884        assert_eq!(ocp_register(120), 0x2F);
885        assert_eq!(ocp_register(130), 0x30);
886        assert_eq!(ocp_register(240), 0x3B);
887        assert_eq!(ocp_register(300), 0x3C);
888    }
889
890    #[test]
891    fn each_output_power_lands_on_the_formula_of_its_amplifier() {
892        let rfo = TxPower::for_output(PaOutput::Rfo, 14);
893        assert_eq!((rfo.pa_config, rfo.pa_dac, rfo.ocp), (0x7E, 0x84, 0x2B));
894        assert_eq!(TxPower::for_output(PaOutput::Rfo, 0).pa_config, 0x04);
895        assert_eq!(TxPower::for_output(PaOutput::Rfo, -9).pa_config, 0x00);
896        assert_eq!(TxPower::for_output(PaOutput::Rfo, 30).output_dbm, 15);
897
898        let boost = TxPower::for_output(PaOutput::PaBoost, 17);
899        assert_eq!(
900            (boost.pa_config, boost.pa_dac, boost.ocp),
901            (0xFF, 0x84, 0x2B)
902        );
903        assert_eq!(TxPower::for_output(PaOutput::PaBoost, 2).pa_config, 0xF0);
904        let high = TxPower::for_output(PaOutput::PaBoost, 18);
905        assert_eq!((high.pa_config, high.pa_dac, high.ocp), (0xFD, 0x87, 0x31));
906        assert_eq!(TxPower::for_output(PaOutput::PaBoost, 0).output_dbm, 2);
907        assert_eq!(boost.output(), PaOutput::PaBoost);
908        assert_eq!(rfo.output(), PaOutput::Rfo);
909    }
910
911    #[test]
912    fn the_ceiling_leaves_whole_decibels_under_the_eirp_limit() {
913        let whip = LinkBudget {
914            transmit_antenna_gain_dbi: Decibels::from_hundredths(215),
915            transmit_cable_loss_db: Decibels::from_tenths(5),
916            ..LinkBudget::default()
917        };
918        let power = TxPower::under_ceiling(PaOutput::PaBoost, &whip, Decibels::from_db(16));
919        assert_eq!(power.output_dbm, 14);
920        assert_eq!(power.pa_config, 0xFC);
921    }
922
923    #[test]
924    fn iq_polarity_follows_the_reference_drivers_not_the_description() {
925        assert_eq!(invert_iq(true, false), 0x67);
926        assert_eq!(invert_iq(false, true), 0x26);
927        assert_eq!(invert_iq_2(true), 0x19);
928        assert_eq!(invert_iq_2(false), 0x1D);
929    }
930
931    #[test]
932    fn the_errata_writes_match_the_reference_driver() {
933        assert_eq!(
934            high_bw_optimize(LoraBandwidth::Khz500, 915_000_000),
935            (0x02, Some(0x64))
936        );
937        assert_eq!(
938            high_bw_optimize(LoraBandwidth::Khz500, 433_000_000),
939            (0x02, Some(0x7F))
940        );
941        assert_eq!(
942            high_bw_optimize(LoraBandwidth::Khz125, 868_100_000),
943            (0x03, None)
944        );
945        assert_eq!(
946            spurious_reception(LoraBandwidth::Khz7_8),
947            SpuriousReception {
948                automatic_if: false,
949                if_freq_2: Some(0x48),
950                offset_hz: 7_810
951            }
952        );
953        assert_eq!(spurious_reception(LoraBandwidth::Khz125).offset_hz, 0);
954        assert_eq!(automatic_if(0xC3, false), 0x43);
955        assert_eq!(automatic_if(0x43, true), 0xC3);
956        assert_eq!(image_cal_start(0x82), 0x42);
957    }
958}