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