Skip to main content

pamoja_radios/sx126x/
config.rs

1//! The settings an SX126x is configured with, and the values its registers take.
2//!
3//! Every value here comes from the SX1261/2 datasheet (Rev 2.2): the frequency word of
4//! section 13.4.1, the timeout steps of 13.1.4, the image calibration codes of 9.2.1, the
5//! LoRa modulation and packet parameters of 13.4.5 and 13.4.6, the power amplifier
6//! settings of 13.1.14 and 13.4.4, the sync words of Table 12-1, and the register values
7//! the chapter 15 workarounds write. The LoRa settings are built from a
8//! [`LinkSettings`], so a radio sends exactly the frames `pamoja-lora` computes the
9//! airtime of.
10
11use pamoja_lora::budget::{Decibels, LinkBudget};
12use pamoja_lora::LinkSettings;
13
14/// The crystal frequency the synthesizer divides, in hertz.
15pub const XTAL_HZ: u32 = 32_000_000;
16
17/// Returns the word SetRfFrequency takes for a frequency.
18///
19/// The datasheet defines the frequency as the word times the crystal frequency over
20/// 2^25, so one step of the word is 0.95 Hz. The word is rounded to the nearest step.
21///
22/// # Arguments
23///
24/// * `frequency_hz` - the carrier frequency in hertz.
25///
26/// # Returns
27///
28/// The 32-bit frequency word.
29///
30/// # Examples
31///
32/// ```
33/// use pamoja_radios::sx126x::config::frequency_word;
34///
35/// assert_eq!(frequency_word(868_100_000), 0x3641_999A);
36/// assert_eq!(frequency_word(915_000_000), 0x3930_0000);
37/// ```
38pub const fn frequency_word(frequency_hz: u32) -> u32 {
39    (((frequency_hz as u64) << 25) + (XTAL_HZ as u64 / 2)).div_euclid(XTAL_HZ as u64) as u32
40}
41
42/// Returns the frequency a SetRfFrequency word selects.
43///
44/// # Arguments
45///
46/// * `word` - the 32-bit frequency word.
47///
48/// # Returns
49///
50/// The carrier frequency in hertz, rounded to the nearest hertz.
51pub const fn frequency_from_word(word: u32) -> u32 {
52    ((word as u64 * XTAL_HZ as u64 + (1 << 24)) >> 25) as u32
53}
54
55/// The timeout word that disables a transmit or receive timeout.
56pub const NO_TIMEOUT: u32 = 0;
57
58/// The receive timeout word that keeps the radio listening until told otherwise.
59pub const RX_CONTINUOUS: u32 = 0xFF_FFFF;
60
61/// Returns the 24-bit timeout word for a duration.
62///
63/// SetTx, SetRx, and SetDIO3AsTCXOCtrl count time in steps of 15.625 us. A nonzero
64/// duration never becomes a zero word, which would disable the timeout, and a duration
65/// past the longest the chip counts, about 262 seconds, stops one step short of
66/// [`RX_CONTINUOUS`].
67///
68/// # Arguments
69///
70/// * `micros` - the duration in microseconds.
71///
72/// # Returns
73///
74/// The number of steps, from 0 to 0xFFFFFE.
75///
76/// # Examples
77///
78/// ```
79/// use pamoja_radios::sx126x::config::timeout_steps;
80///
81/// assert_eq!(timeout_steps(1_000_000), 64_000);
82/// assert_eq!(timeout_steps(0), 0);
83/// ```
84pub const fn timeout_steps(micros: u64) -> u32 {
85    if micros == 0 {
86        return NO_TIMEOUT;
87    }
88    let steps = micros.saturating_mul(64) / 1000;
89    if steps == 0 {
90        1
91    } else if steps >= RX_CONTINUOUS as u64 {
92        RX_CONTINUOUS - 1
93    } else {
94        steps as u32
95    }
96}
97
98/// Returns the two CalibrateImage codes that cover a band.
99///
100/// Section 9.2.1 calibrates image rejection between two codes in steps of 4 MHz, taking
101/// the floor of the lower edge and the ceiling of the upper one, so the calibrated range
102/// always covers the band.
103///
104/// # Arguments
105///
106/// * `low_hz` - the lower edge of the band in hertz.
107/// * `high_hz` - the upper edge of the band in hertz.
108///
109/// # Returns
110///
111/// `freq1` and `freq2`, the parameters of CalibrateImage.
112///
113/// # Examples
114///
115/// ```
116/// use pamoja_radios::sx126x::config::image_calibration;
117///
118/// assert_eq!(image_calibration(863_000_000, 870_000_000), [0xD7, 0xDA]);
119/// assert_eq!(image_calibration(902_000_000, 928_000_000), [0xE1, 0xE8]);
120/// ```
121pub const fn image_calibration(low_hz: u32, high_hz: u32) -> [u8; 2] {
122    const STEP_HZ: u32 = 4_000_000;
123    let low = low_hz / STEP_HZ;
124    let high = high_hz.div_ceil(STEP_HZ);
125    [clamp_byte(low), clamp_byte(high)]
126}
127
128const fn clamp_byte(value: u32) -> u8 {
129    if value > 0xFF {
130        0xFF
131    } else {
132        value as u8
133    }
134}
135
136/// The modem a SetPacketType command selects, from Table 13-38.
137#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
138pub enum PacketType {
139    /// GFSK (0x00).
140    Gfsk,
141    /// LoRa (0x01).
142    Lora,
143    /// Long Range FHSS (0x03).
144    LrFhss,
145}
146
147impl PacketType {
148    /// Returns the parameter byte.
149    ///
150    /// # Returns
151    ///
152    /// The PacketType value.
153    pub const fn code(self) -> u8 {
154        match self {
155            PacketType::Gfsk => 0x00,
156            PacketType::Lora => 0x01,
157            PacketType::LrFhss => 0x03,
158        }
159    }
160}
161
162/// The standby mode a SetStandby command selects, from Table 13-4.
163#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
164pub enum StandbyMode {
165    /// Running on the 13 MHz RC oscillator (0).
166    Rc,
167    /// Running on the 32 MHz crystal (1).
168    Xosc,
169}
170
171impl StandbyMode {
172    /// Returns the parameter byte.
173    ///
174    /// # Returns
175    ///
176    /// The StdbyConfig value.
177    pub const fn code(self) -> u8 {
178        match self {
179            StandbyMode::Rc => 0,
180            StandbyMode::Xosc => 1,
181        }
182    }
183}
184
185/// How the chip regulates its supply, from Table 13-16.
186#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
187pub enum RegulatorMode {
188    /// Only the LDO, for every mode (0).
189    Ldo,
190    /// The DC-DC converter and the LDO, for STBY_XOSC, FS, RX, and TX (1).
191    DcDc,
192}
193
194impl RegulatorMode {
195    /// Returns the parameter byte.
196    ///
197    /// # Returns
198    ///
199    /// The regModeParam value.
200    pub const fn code(self) -> u8 {
201        match self {
202            RegulatorMode::Ldo => 0,
203            RegulatorMode::DcDc => 1,
204        }
205    }
206}
207
208/// The mode the chip returns to after a transmission or reception, from Table 13-23.
209#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
210pub enum FallbackMode {
211    /// Frequency synthesis (0x40).
212    Fs,
213    /// Standby on the crystal (0x30).
214    StandbyXosc,
215    /// Standby on the RC oscillator, the default (0x20).
216    StandbyRc,
217}
218
219impl FallbackMode {
220    /// Returns the parameter byte.
221    ///
222    /// # Returns
223    ///
224    /// The fallbackMode value.
225    pub const fn code(self) -> u8 {
226        match self {
227            FallbackMode::Fs => 0x40,
228            FallbackMode::StandbyXosc => 0x30,
229            FallbackMode::StandbyRc => 0x20,
230        }
231    }
232}
233
234/// The voltage DIO3 supplies a TCXO with, from Table 13-35.
235#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
236pub enum TcxoVoltage {
237    /// 1.6 V (0x00).
238    V1_6,
239    /// 1.7 V (0x01).
240    V1_7,
241    /// 1.8 V (0x02).
242    V1_8,
243    /// 2.2 V (0x03).
244    V2_2,
245    /// 2.4 V (0x04).
246    V2_4,
247    /// 2.7 V (0x05).
248    V2_7,
249    /// 3.0 V (0x06).
250    V3_0,
251    /// 3.3 V (0x07).
252    V3_3,
253}
254
255impl TcxoVoltage {
256    /// Names the TCXO voltage DIO3 supplies at a number of millivolts.
257    ///
258    /// # Arguments
259    ///
260    /// * `millivolts` - the supply voltage, such as `1700` for 1.7 V.
261    ///
262    /// # Returns
263    ///
264    /// The voltage, or `None` for one DIO3 cannot supply.
265    ///
266    /// # Examples
267    ///
268    /// ```
269    /// use pamoja_radios::sx126x::config::TcxoVoltage;
270    ///
271    /// assert_eq!(TcxoVoltage::from_millivolts(1700), Some(TcxoVoltage::V1_7));
272    /// assert_eq!(TcxoVoltage::from_millivolts(1900), None);
273    /// ```
274    pub const fn from_millivolts(millivolts: u16) -> Option<TcxoVoltage> {
275        match millivolts {
276            1600 => Some(TcxoVoltage::V1_6),
277            1700 => Some(TcxoVoltage::V1_7),
278            1800 => Some(TcxoVoltage::V1_8),
279            2200 => Some(TcxoVoltage::V2_2),
280            2400 => Some(TcxoVoltage::V2_4),
281            2700 => Some(TcxoVoltage::V2_7),
282            3000 => Some(TcxoVoltage::V3_0),
283            3300 => Some(TcxoVoltage::V3_3),
284            _ => None,
285        }
286    }
287
288    /// Returns the parameter byte.
289    ///
290    /// # Returns
291    ///
292    /// The tcxoVoltage value.
293    pub const fn code(self) -> u8 {
294        match self {
295            TcxoVoltage::V1_6 => 0x00,
296            TcxoVoltage::V1_7 => 0x01,
297            TcxoVoltage::V1_8 => 0x02,
298            TcxoVoltage::V2_2 => 0x03,
299            TcxoVoltage::V2_4 => 0x04,
300            TcxoVoltage::V2_7 => 0x05,
301            TcxoVoltage::V3_0 => 0x06,
302            TcxoVoltage::V3_3 => 0x07,
303        }
304    }
305}
306
307/// How long the power amplifier takes to ramp up, from Table 13-41.
308#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
309pub enum RampTime {
310    /// 10 us (0x00).
311    Us10,
312    /// 20 us (0x01).
313    Us20,
314    /// 40 us (0x02).
315    Us40,
316    /// 80 us (0x03).
317    Us80,
318    /// 200 us (0x04).
319    Us200,
320    /// 800 us (0x05).
321    Us800,
322    /// 1700 us (0x06).
323    Us1700,
324    /// 3400 us (0x07).
325    Us3400,
326}
327
328impl RampTime {
329    /// Returns the parameter byte.
330    ///
331    /// # Returns
332    ///
333    /// The RampTime value.
334    pub const fn code(self) -> u8 {
335        match self {
336            RampTime::Us10 => 0x00,
337            RampTime::Us20 => 0x01,
338            RampTime::Us40 => 0x02,
339            RampTime::Us80 => 0x03,
340            RampTime::Us200 => 0x04,
341            RampTime::Us800 => 0x05,
342            RampTime::Us1700 => 0x06,
343            RampTime::Us3400 => 0x07,
344        }
345    }
346
347    /// Returns the ramp time in microseconds.
348    ///
349    /// # Returns
350    ///
351    /// The duration Table 13-41 gives.
352    pub const fn micros(self) -> u32 {
353        match self {
354            RampTime::Us10 => 10,
355            RampTime::Us20 => 20,
356            RampTime::Us40 => 40,
357            RampTime::Us80 => 80,
358            RampTime::Us200 => 200,
359            RampTime::Us800 => 800,
360            RampTime::Us1700 => 1_700,
361            RampTime::Us3400 => 3_400,
362        }
363    }
364
365    /// Returns the shortest ramp time that lasts at least a duration.
366    ///
367    /// # Arguments
368    ///
369    /// * `micros` - the least ramp time wanted, in microseconds; past 3400 us, the longest
370    ///   ramp the chip offers.
371    ///
372    /// # Returns
373    ///
374    /// The ramp time.
375    ///
376    /// # Examples
377    ///
378    /// ```
379    /// use pamoja_radios::sx126x::config::RampTime;
380    ///
381    /// assert_eq!(RampTime::at_least(40), RampTime::Us40);
382    /// assert_eq!(RampTime::at_least(100), RampTime::Us200);
383    /// assert_eq!(RampTime::at_least(5_000).micros(), 3_400);
384    /// ```
385    pub const fn at_least(micros: u32) -> RampTime {
386        if micros <= 10 {
387            RampTime::Us10
388        } else if micros <= 20 {
389            RampTime::Us20
390        } else if micros <= 40 {
391            RampTime::Us40
392        } else if micros <= 80 {
393            RampTime::Us80
394        } else if micros <= 200 {
395            RampTime::Us200
396        } else if micros <= 800 {
397            RampTime::Us800
398        } else if micros <= 1_700 {
399            RampTime::Us1700
400        } else {
401            RampTime::Us3400
402        }
403    }
404}
405
406/// A LoRa signal bandwidth, from Table 13-48.
407#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
408pub enum LoraBandwidth {
409    /// 7.81 kHz (0x00).
410    Khz7_8,
411    /// 10.42 kHz (0x08).
412    Khz10_4,
413    /// 15.63 kHz (0x01).
414    Khz15_6,
415    /// 20.83 kHz (0x09).
416    Khz20_8,
417    /// 31.25 kHz (0x02).
418    Khz31_25,
419    /// 41.67 kHz (0x0A).
420    Khz41_7,
421    /// 62.5 kHz (0x03).
422    Khz62_5,
423    /// 125 kHz (0x04).
424    Khz125,
425    /// 250 kHz (0x05).
426    Khz250,
427    /// 500 kHz (0x06).
428    Khz500,
429}
430
431impl LoraBandwidth {
432    const ALL: [LoraBandwidth; 10] = [
433        LoraBandwidth::Khz7_8,
434        LoraBandwidth::Khz10_4,
435        LoraBandwidth::Khz15_6,
436        LoraBandwidth::Khz20_8,
437        LoraBandwidth::Khz31_25,
438        LoraBandwidth::Khz41_7,
439        LoraBandwidth::Khz62_5,
440        LoraBandwidth::Khz125,
441        LoraBandwidth::Khz250,
442        LoraBandwidth::Khz500,
443    ];
444
445    /// Returns the parameter byte.
446    ///
447    /// # Returns
448    ///
449    /// The BW value of SetModulationParams.
450    pub const fn code(self) -> u8 {
451        match self {
452            LoraBandwidth::Khz7_8 => 0x00,
453            LoraBandwidth::Khz10_4 => 0x08,
454            LoraBandwidth::Khz15_6 => 0x01,
455            LoraBandwidth::Khz20_8 => 0x09,
456            LoraBandwidth::Khz31_25 => 0x02,
457            LoraBandwidth::Khz41_7 => 0x0A,
458            LoraBandwidth::Khz62_5 => 0x03,
459            LoraBandwidth::Khz125 => 0x04,
460            LoraBandwidth::Khz250 => 0x05,
461            LoraBandwidth::Khz500 => 0x06,
462        }
463    }
464
465    /// Returns the bandwidth in hertz, rounded to the nearest hertz.
466    ///
467    /// # Returns
468    ///
469    /// The bandwidth: 500 kHz halved, or 125 kHz divided by 3 and halved, as many times
470    /// as the setting takes.
471    pub const fn hz(self) -> u32 {
472        match self {
473            LoraBandwidth::Khz7_8 => 7_813,
474            LoraBandwidth::Khz10_4 => 10_417,
475            LoraBandwidth::Khz15_6 => 15_625,
476            LoraBandwidth::Khz20_8 => 20_833,
477            LoraBandwidth::Khz31_25 => 31_250,
478            LoraBandwidth::Khz41_7 => 41_667,
479            LoraBandwidth::Khz62_5 => 62_500,
480            LoraBandwidth::Khz125 => 125_000,
481            LoraBandwidth::Khz250 => 250_000,
482            LoraBandwidth::Khz500 => 500_000,
483        }
484    }
485
486    /// Finds the setting for a bandwidth in hertz.
487    ///
488    /// # Arguments
489    ///
490    /// * `hz` - the bandwidth in hertz, within 1% of one the chip supports, so both
491    ///   `10_417` and the datasheet's rounded `10_420` select 10.42 kHz.
492    ///
493    /// # Returns
494    ///
495    /// The setting, or `None` for a bandwidth the SX126x does not offer.
496    pub fn from_hz(hz: u32) -> Option<LoraBandwidth> {
497        LoraBandwidth::ALL.into_iter().find(|bandwidth| {
498            let nominal = u64::from(bandwidth.hz());
499            u64::from(hz).abs_diff(nominal) * 100 <= nominal
500        })
501    }
502}
503
504/// A LoRa coding rate, from Table 13-49.
505#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
506pub enum CodingRate {
507    /// 4/5 (0x01).
508    Cr4_5,
509    /// 4/6 (0x02).
510    Cr4_6,
511    /// 4/7 (0x03).
512    Cr4_7,
513    /// 4/8 (0x04).
514    Cr4_8,
515    /// 4/5 with the long interleaver (0x05).
516    Cr4_5LongInterleaver,
517    /// 4/6 with the long interleaver (0x06).
518    Cr4_6LongInterleaver,
519    /// 4/8 with the long interleaver (0x07).
520    Cr4_8LongInterleaver,
521}
522
523impl CodingRate {
524    /// Returns the parameter byte.
525    ///
526    /// # Returns
527    ///
528    /// The CR value of SetModulationParams.
529    pub const fn code(self) -> u8 {
530        match self {
531            CodingRate::Cr4_5 => 0x01,
532            CodingRate::Cr4_6 => 0x02,
533            CodingRate::Cr4_7 => 0x03,
534            CodingRate::Cr4_8 => 0x04,
535            CodingRate::Cr4_5LongInterleaver => 0x05,
536            CodingRate::Cr4_6LongInterleaver => 0x06,
537            CodingRate::Cr4_8LongInterleaver => 0x07,
538        }
539    }
540
541    /// Returns the standard-interleaver coding rate for a denominator.
542    ///
543    /// The airtime `pamoja-lora` computes assumes the standard interleaver, so a link's
544    /// coding rate always maps to one of the first four settings.
545    ///
546    /// # Arguments
547    ///
548    /// * `denominator` - the coding-rate denominator, clamped to 5 to 8.
549    ///
550    /// # Returns
551    ///
552    /// The coding rate.
553    pub const fn from_denominator(denominator: u8) -> CodingRate {
554        match denominator {
555            0..=5 => CodingRate::Cr4_5,
556            6 => CodingRate::Cr4_6,
557            7 => CodingRate::Cr4_7,
558            _ => CodingRate::Cr4_8,
559        }
560    }
561}
562
563/// The LoRa parameters of a SetModulationParams command, from Tables 13-47 to 13-50.
564///
565/// # Examples
566///
567/// ```
568/// use pamoja_lora::LinkSettings;
569/// use pamoja_radios::sx126x::config::LoraModulation;
570///
571/// // SF12 at 125 kHz needs low data rate optimization.
572/// let modulation = LoraModulation::from_link(&LinkSettings::new(12, 125_000)).unwrap();
573/// assert_eq!(modulation.to_params(), [0x0C, 0x04, 0x01, 0x01]);
574/// ```
575#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
576pub struct LoraModulation {
577    /// The spreading factor, 5 to 12.
578    pub spreading_factor: u8,
579    /// The signal bandwidth.
580    pub bandwidth: LoraBandwidth,
581    /// The coding rate.
582    pub coding_rate: CodingRate,
583    /// Whether low data rate optimization is on.
584    pub low_data_rate_optimization: bool,
585}
586
587impl LoraModulation {
588    /// Builds the modulation parameters of a link.
589    ///
590    /// # Arguments
591    ///
592    /// * `link` - the link settings, whose spreading factor, bandwidth, coding rate, and
593    ///   low data rate optimization the radio must match.
594    ///
595    /// # Returns
596    ///
597    /// The parameters, or `None` when the link's bandwidth is not one the SX126x
598    /// offers.
599    pub fn from_link(link: &LinkSettings) -> Option<LoraModulation> {
600        Some(LoraModulation {
601            spreading_factor: link.spreading_factor(),
602            bandwidth: LoraBandwidth::from_hz(link.bandwidth_hz())?,
603            coding_rate: CodingRate::from_denominator(link.coding_rate_denominator()),
604            low_data_rate_optimization: link.low_data_rate_optimization(),
605        })
606    }
607
608    /// Returns the four parameter bytes: SF, BW, CR, and LowDataRateOptimize.
609    ///
610    /// # Returns
611    ///
612    /// ModParam1 to ModParam4.
613    pub const fn to_params(&self) -> [u8; 4] {
614        [
615            self.spreading_factor,
616            self.bandwidth.code(),
617            self.coding_rate.code(),
618            self.low_data_rate_optimization as u8,
619        ]
620    }
621}
622
623/// Reports whether an LLCC68 can use a spreading factor at a bandwidth.
624///
625/// The LLCC68 takes the SX1262's commands but not all of its settings. Its datasheet
626/// (DS.LLCC68.W.APP Rev 1.1) lists SF5 to SF11 in Table 13-47 and only the 125, 250, and
627/// 500 kHz bandwidths in Table 13-48, and notes under Table 6-1 that not every spreading
628/// factor is available at every bandwidth. Semtech's LLCC68 driver refuses SF10 and SF11 at
629/// 125 kHz and SF11 at 250 kHz, which leaves the LoRaWAN data rates the LLCC68 product page
630/// lists: up to SF9 at 125 kHz, SF10 at 250 kHz, and SF11 at 500 kHz.
631///
632/// # Arguments
633///
634/// * `spreading_factor` - the spreading factor.
635/// * `bandwidth` - the signal bandwidth.
636///
637/// # Returns
638///
639/// `true` when the LLCC68 supports the pair.
640///
641/// # Examples
642///
643/// ```
644/// use pamoja_radios::sx126x::config::{llcc68_supports, LoraBandwidth};
645///
646/// assert!(llcc68_supports(9, LoraBandwidth::Khz125));
647/// assert!(!llcc68_supports(10, LoraBandwidth::Khz125));
648/// assert!(llcc68_supports(10, LoraBandwidth::Khz250));
649/// assert!(!llcc68_supports(11, LoraBandwidth::Khz250));
650/// assert!(llcc68_supports(11, LoraBandwidth::Khz500));
651/// assert!(!llcc68_supports(12, LoraBandwidth::Khz500));
652/// assert!(!llcc68_supports(7, LoraBandwidth::Khz62_5));
653/// ```
654pub const fn llcc68_supports(spreading_factor: u8, bandwidth: LoraBandwidth) -> bool {
655    if spreading_factor < 5 || spreading_factor > 11 {
656        return false;
657    }
658    match bandwidth {
659        LoraBandwidth::Khz125 => spreading_factor <= 9,
660        LoraBandwidth::Khz250 => spreading_factor <= 10,
661        LoraBandwidth::Khz500 => true,
662        _ => false,
663    }
664}
665
666/// The LoRa parameters of a SetPacketParams command, from Tables 13-66 to 13-70.
667///
668/// # Examples
669///
670/// ```
671/// use pamoja_lora::LinkSettings;
672/// use pamoja_radios::sx126x::config::LoraPacket;
673///
674/// // An eight-symbol preamble, an explicit header, a 20-byte payload, CRC on, standard IQ.
675/// let packet = LoraPacket::from_link(&LinkSettings::new(7, 125_000), 20, false);
676/// assert_eq!(packet.to_params(), [0x00, 0x08, 0x00, 0x14, 0x01, 0x00]);
677/// ```
678#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
679pub struct LoraPacket {
680    /// The preamble length in symbols.
681    pub preamble_symbols: u16,
682    /// Whether the frame carries an explicit header; implicit when `false`.
683    pub explicit_header: bool,
684    /// The payload length to send, or the most a receiver accepts.
685    pub payload_len: u8,
686    /// Whether the frame carries a CRC.
687    pub crc: bool,
688    /// Whether the IQ polarity is inverted, as LoRaWAN downlinks use.
689    pub invert_iq: bool,
690}
691
692impl LoraPacket {
693    /// Builds the packet parameters of a link for a payload.
694    ///
695    /// # Arguments
696    ///
697    /// * `link` - the link settings, whose preamble, header, and CRC the frame uses.
698    /// * `payload_len` - the payload length to send, or the most to accept.
699    /// * `invert_iq` - `true` for inverted IQ polarity.
700    ///
701    /// # Returns
702    ///
703    /// The parameters.
704    pub fn from_link(link: &LinkSettings, payload_len: u8, invert_iq: bool) -> LoraPacket {
705        LoraPacket {
706            preamble_symbols: link.preamble_symbols(),
707            explicit_header: link.explicit_header(),
708            payload_len,
709            crc: link.crc(),
710            invert_iq,
711        }
712    }
713
714    /// Returns the six parameter bytes: the preamble length, most significant byte first,
715    /// then the header type, the payload length, the CRC type, and the IQ setup.
716    ///
717    /// # Returns
718    ///
719    /// PacketParam1 to PacketParam6.
720    pub const fn to_params(&self) -> [u8; 6] {
721        let preamble = self.preamble_symbols.to_be_bytes();
722        [
723            preamble[0],
724            preamble[1],
725            !self.explicit_header as u8,
726            self.payload_len,
727            self.crc as u8,
728            self.invert_iq as u8,
729        ]
730    }
731}
732
733/// Which power amplifier a chip has.
734#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
735pub enum PowerAmplifier {
736    /// The low power amplifier of the SX1261, up to +15 dBm.
737    LowPower,
738    /// The high power amplifier of the SX1262 and the LLCC68, up to +22 dBm.
739    HighPower,
740}
741
742impl PowerAmplifier {
743    /// Returns the output power range SetTxParams accepts, from section 13.4.4.
744    ///
745    /// # Returns
746    ///
747    /// The lowest and highest settings in dBm: -17 to +14 for the low power amplifier
748    /// and -9 to +22 for the high power one.
749    pub const fn setting_range_dbm(self) -> (i8, i8) {
750        match self {
751            PowerAmplifier::LowPower => (-17, 14),
752            PowerAmplifier::HighPower => (-9, 22),
753        }
754    }
755}
756
757/// The parameters of a SetPaConfig command, from Table 13-20.
758#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
759pub struct PaConfig {
760    /// The duty cycle, or conduction angle, of the amplifier.
761    pub duty_cycle: u8,
762    /// The size of the SX1262's amplifier, 0x00 to 0x07; no effect on the SX1261.
763    pub hp_max: u8,
764    /// The device: 0 for the SX1262, 1 for the SX1261.
765    pub device: u8,
766    /// Reserved, always 0x01.
767    pub lut: u8,
768}
769
770impl PaConfig {
771    /// The SX1262 at +22 dBm, from Table 13-21.
772    pub const SX1262_22_DBM: PaConfig = PaConfig::new(0x04, 0x07, 0x00);
773    /// The SX1262 at +20 dBm, from Table 13-21.
774    pub const SX1262_20_DBM: PaConfig = PaConfig::new(0x03, 0x05, 0x00);
775    /// The SX1262 at +17 dBm, from Table 13-21.
776    pub const SX1262_17_DBM: PaConfig = PaConfig::new(0x02, 0x03, 0x00);
777    /// The SX1262 at +14 dBm, from Table 13-21.
778    pub const SX1262_14_DBM: PaConfig = PaConfig::new(0x02, 0x02, 0x00);
779    /// The SX1261 at +15 dBm, from Table 13-21.
780    pub const SX1261_15_DBM: PaConfig = PaConfig::new(0x06, 0x00, 0x01);
781    /// The SX1261 at +14 dBm, from Table 13-21.
782    pub const SX1261_14_DBM: PaConfig = PaConfig::new(0x04, 0x00, 0x01);
783    /// The SX1261 at +10 dBm, from Table 13-21.
784    pub const SX1261_10_DBM: PaConfig = PaConfig::new(0x01, 0x00, 0x01);
785
786    const fn new(duty_cycle: u8, hp_max: u8, device: u8) -> PaConfig {
787        PaConfig {
788            duty_cycle,
789            hp_max,
790            device,
791            lut: 0x01,
792        }
793    }
794
795    /// Returns the four parameter bytes.
796    ///
797    /// # Returns
798    ///
799    /// paDutyCycle, hpMax, deviceSel, and paLut.
800    pub const fn to_params(&self) -> [u8; 4] {
801        [self.duty_cycle, self.hp_max, self.device, self.lut]
802    }
803}
804
805/// The amplifier configuration and power setting that produce an output power.
806///
807/// # Examples
808///
809/// ```
810/// use pamoja_lora::budget::{Decibels, LinkBudget};
811/// use pamoja_radios::sx126x::config::{PaConfig, PowerAmplifier, TxPower};
812///
813/// // A 2.15 dBi whip on half a decibel of pigtail under a 16 dBm EIRP ceiling.
814/// let whip = LinkBudget {
815///     transmit_antenna_gain_dbi: Decibels::from_hundredths(215),
816///     transmit_cable_loss_db: Decibels::from_tenths(5),
817///     ..LinkBudget::default()
818/// };
819/// let power = TxPower::under_ceiling(PowerAmplifier::HighPower, &whip, Decibels::from_db(16));
820/// assert_eq!(power.setting_dbm, 14);
821/// assert_eq!(power.pa, PaConfig::SX1262_22_DBM);
822/// ```
823#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
824pub struct TxPower {
825    /// The SetPaConfig parameters.
826    pub pa: PaConfig,
827    /// The power byte of SetTxParams, in dBm.
828    pub setting_dbm: i8,
829}
830
831impl TxPower {
832    /// Chooses the settings for an output power.
833    ///
834    /// The high power amplifier keeps its +22 dBm configuration and takes the power in
835    /// SetTxParams, clamped to -9 to +22 dBm. The low power amplifier uses its +14 dBm
836    /// configuration with the power clamped to -17 to +14 dBm, and its +15 dBm
837    /// configuration, which Table 13-21 drives with a setting of +14 dBm, for anything
838    /// above.
839    ///
840    /// # Arguments
841    ///
842    /// * `amplifier` - the chip's power amplifier.
843    /// * `output_dbm` - the output power wanted at the antenna port.
844    ///
845    /// # Returns
846    ///
847    /// The configuration and the setting.
848    pub const fn for_output(amplifier: PowerAmplifier, output_dbm: i8) -> TxPower {
849        match amplifier {
850            PowerAmplifier::HighPower => TxPower {
851                pa: PaConfig::SX1262_22_DBM,
852                setting_dbm: clamp_power(output_dbm, -9, 22),
853            },
854            PowerAmplifier::LowPower if output_dbm >= 15 => TxPower {
855                pa: PaConfig::SX1261_15_DBM,
856                setting_dbm: 14,
857            },
858            PowerAmplifier::LowPower => TxPower {
859                pa: PaConfig::SX1261_14_DBM,
860                setting_dbm: clamp_power(output_dbm, -17, 14),
861            },
862        }
863    }
864
865    /// Chooses the settings that keep a link's EIRP at or under a ceiling.
866    ///
867    /// # Arguments
868    ///
869    /// * `amplifier` - the chip's power amplifier.
870    /// * `budget` - the link budget, whose transmitting antenna and cable apply.
871    /// * `eirp_ceiling_dbm` - the EIRP limit, such as a channel plan's ceiling for the
872    ///   frequency in use.
873    ///
874    /// # Returns
875    ///
876    /// The configuration and the setting, rounded down to whole decibels so the EIRP
877    /// stays under the ceiling.
878    pub fn under_ceiling(
879        amplifier: PowerAmplifier,
880        budget: &LinkBudget,
881        eirp_ceiling_dbm: Decibels,
882    ) -> TxPower {
883        let most = budget.max_transmit_power_dbm(eirp_ceiling_dbm).floor_db();
884        let output = most.clamp(i32::from(i8::MIN), i32::from(i8::MAX)) as i8;
885        TxPower::for_output(amplifier, output)
886    }
887}
888
889const fn clamp_power(dbm: i8, low: i8, high: i8) -> i8 {
890    if dbm < low {
891        low
892    } else if dbm > high {
893        high
894    } else {
895        dbm
896    }
897}
898
899/// A LoRa sync word, written to the two sync word registers at 0x0740, from Table 12-1.
900#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
901pub enum SyncWord {
902    /// 0x3444, for a public network such as LoRaWAN.
903    Public,
904    /// 0x1424, for a private network, and the chip's reset value.
905    Private,
906    /// Another two-byte value.
907    Custom(u16),
908}
909
910impl SyncWord {
911    /// Returns the two register bytes, most significant first.
912    ///
913    /// # Returns
914    ///
915    /// The values of the sync word registers at 0x0740 and 0x0741.
916    pub const fn to_bytes(self) -> [u8; 2] {
917        match self {
918            SyncWord::Public => [0x34, 0x44],
919            SyncWord::Private => [0x14, 0x24],
920            SyncWord::Custom(word) => word.to_be_bytes(),
921        }
922    }
923}
924
925/// The register addresses the driver reads and writes, from Table 12-1.
926pub mod register {
927    /// The most significant byte of the LoRa sync word; the least follows at 0x0741.
928    pub const LORA_SYNC_WORD: u16 = 0x0740;
929    /// The IQ polarity setup, bit 2 of which the inverted IQ workaround sets.
930    pub const IQ_POLARITY: u16 = 0x0736;
931    /// The TX modulation register, bit 2 of which the 500 kHz workaround sets.
932    pub const TX_MODULATION: u16 = 0x0889;
933    /// The receive gain.
934    pub const RX_GAIN: u16 = 0x08AC;
935    /// The PA clamping configuration the antenna mismatch workaround raises.
936    pub const TX_CLAMP_CONFIG: u16 = 0x08D8;
937    /// The over current protection level.
938    pub const OCP_CONFIGURATION: u16 = 0x08E7;
939    /// The RTC control register the implicit header timeout workaround stops.
940    pub const RTC_CONTROL: u16 = 0x0902;
941    /// The trimming capacitor on the XTA pin.
942    pub const XTA_TRIM: u16 = 0x0911;
943    /// The trimming capacitor on the XTB pin.
944    pub const XTB_TRIM: u16 = 0x0912;
945    /// The event mask the implicit header timeout workaround clears.
946    pub const EVENT_MASK: u16 = 0x0944;
947}
948
949/// The receive gain register value for power saving, the default, from Table 9-3.
950pub const RX_GAIN_POWER_SAVING: u8 = 0x94;
951
952/// The receive gain register value for boosted sensitivity, from Table 9-3.
953pub const RX_GAIN_BOOSTED: u8 = 0x96;
954
955/// The RTC control value that stops the timer, from section 15.3.
956pub const RTC_STOP: u8 = 0x00;
957
958/// Returns the TX modulation register value for a transmission, from section 15.1.
959///
960/// Bit 2 is cleared for a 500 kHz LoRa bandwidth and set for every other bandwidth.
961///
962/// # Arguments
963///
964/// * `current` - the register value read back from the chip.
965/// * `bandwidth` - the LoRa bandwidth about to be used.
966///
967/// # Returns
968///
969/// The value to write.
970pub const fn tx_modulation(current: u8, bandwidth: LoraBandwidth) -> u8 {
971    match bandwidth {
972        LoraBandwidth::Khz500 => current & !0x04,
973        _ => current | 0x04,
974    }
975}
976
977/// Returns the TX clamp register value that resists antenna mismatch, from section 15.2.
978///
979/// Bits 4 to 1 are set, which the datasheet asks of the SX1262 after a power on reset or
980/// a cold wake.
981///
982/// # Arguments
983///
984/// * `current` - the register value read back from the chip.
985///
986/// # Returns
987///
988/// The value to write.
989pub const fn tx_clamp(current: u8) -> u8 {
990    current | 0x1E
991}
992
993/// Returns the IQ polarity register value for a polarity, from section 15.4.
994///
995/// Bit 2 is cleared for inverted IQ and set for standard IQ.
996///
997/// # Arguments
998///
999/// * `current` - the register value read back from the chip.
1000/// * `invert_iq` - `true` for inverted IQ polarity.
1001///
1002/// # Returns
1003///
1004/// The value to write.
1005pub const fn iq_polarity(current: u8, invert_iq: bool) -> u8 {
1006    if invert_iq {
1007        current & !0x04
1008    } else {
1009        current | 0x04
1010    }
1011}
1012
1013/// Returns the event mask value that clears a pending RTC timeout, from section 15.3.
1014///
1015/// The datasheet names the register; the bit, bit 1, is the one Semtech's reference
1016/// `sx126x_driver` sets in `sx126x_stop_rtc`.
1017///
1018/// # Arguments
1019///
1020/// * `current` - the register value read back from the chip.
1021///
1022/// # Returns
1023///
1024/// The value to write.
1025pub const fn event_clear(current: u8) -> u8 {
1026    current | 0x02
1027}
1028
1029#[cfg(test)]
1030mod tests {
1031    use super::*;
1032
1033    #[test]
1034    fn the_frequency_word_matches_the_reference_driver() {
1035        // Semtech's sx126x_convert_freq_in_hz_to_pll_step, evaluated for these frequencies.
1036        let words = [
1037            (433_175_000, 0x1B12_CCCD),
1038            (868_100_000, 0x3641_999A),
1039            (868_300_000, 0x3644_CCCD),
1040            (868_500_000, 0x3648_0000),
1041            (869_525_000, 0x3658_6666),
1042            (902_300_000, 0x3864_CCCD),
1043            (915_000_000, 0x3930_0000),
1044            (923_300_000, 0x39B4_CCCD),
1045            (150_000_000, 0x0960_0000),
1046            (960_000_000, 0x3C00_0000),
1047        ];
1048        for (hz, word) in words {
1049            assert_eq!(frequency_word(hz), word, "{hz} Hz");
1050            assert!(frequency_from_word(word).abs_diff(hz) <= 1, "{hz} Hz back");
1051        }
1052    }
1053
1054    #[test]
1055    fn the_timeout_counts_in_steps_of_15625_nanoseconds() {
1056        assert_eq!(timeout_steps(15_625), 1_000);
1057        assert_eq!(timeout_steps(3_000_000), 192_000);
1058        assert_eq!(timeout_steps(1), 1);
1059        assert_eq!(timeout_steps(u64::MAX), RX_CONTINUOUS - 1);
1060    }
1061
1062    #[test]
1063    fn the_image_calibration_takes_the_floor_and_the_ceiling() {
1064        assert_eq!(image_calibration(430_000_000, 440_000_000), [0x6B, 0x6E]);
1065        assert_eq!(image_calibration(470_000_000, 510_000_000), [0x75, 0x80]);
1066        assert_eq!(image_calibration(779_000_000, 787_000_000), [0xC2, 0xC5]);
1067        assert_eq!(image_calibration(863_000_000, 870_000_000), [0xD7, 0xDA]);
1068        assert_eq!(image_calibration(902_000_000, 928_000_000), [0xE1, 0xE8]);
1069    }
1070
1071    #[test]
1072    fn every_bandwidth_round_trips_through_its_frequency() {
1073        for bandwidth in LoraBandwidth::ALL {
1074            assert_eq!(LoraBandwidth::from_hz(bandwidth.hz()), Some(bandwidth));
1075        }
1076        assert_eq!(LoraBandwidth::from_hz(10_420), Some(LoraBandwidth::Khz10_4));
1077        assert_eq!(LoraBandwidth::from_hz(7_810), Some(LoraBandwidth::Khz7_8));
1078        assert_eq!(LoraBandwidth::from_hz(200_000), None);
1079    }
1080
1081    #[test]
1082    fn the_codes_follow_the_datasheet_tables() {
1083        let bandwidths = [
1084            (LoraBandwidth::Khz7_8, 0x00),
1085            (LoraBandwidth::Khz10_4, 0x08),
1086            (LoraBandwidth::Khz15_6, 0x01),
1087            (LoraBandwidth::Khz20_8, 0x09),
1088            (LoraBandwidth::Khz31_25, 0x02),
1089            (LoraBandwidth::Khz41_7, 0x0A),
1090            (LoraBandwidth::Khz62_5, 0x03),
1091            (LoraBandwidth::Khz125, 0x04),
1092            (LoraBandwidth::Khz250, 0x05),
1093            (LoraBandwidth::Khz500, 0x06),
1094        ];
1095        for (bandwidth, code) in bandwidths {
1096            assert_eq!(bandwidth.code(), code);
1097        }
1098        assert_eq!(CodingRate::from_denominator(5).code(), 0x01);
1099        assert_eq!(CodingRate::from_denominator(8).code(), 0x04);
1100        assert_eq!(CodingRate::Cr4_8LongInterleaver.code(), 0x07);
1101        assert_eq!(RampTime::Us3400.code(), 0x07);
1102        assert_eq!(TcxoVoltage::V1_8.code(), 0x02);
1103        assert_eq!(FallbackMode::Fs.code(), 0x40);
1104        assert_eq!(PacketType::LrFhss.code(), 0x03);
1105    }
1106
1107    #[test]
1108    fn a_link_becomes_its_modulation_and_packet_parameters() {
1109        let slow = LinkSettings::new(11, 125_000).with_coding_rate(6);
1110        assert_eq!(
1111            LoraModulation::from_link(&slow).map(|m| m.to_params()),
1112            Some([0x0B, 0x04, 0x02, 0x01])
1113        );
1114        let fast = LinkSettings::new(7, 500_000);
1115        assert_eq!(
1116            LoraModulation::from_link(&fast).map(|m| m.to_params()),
1117            Some([0x07, 0x06, 0x01, 0x00])
1118        );
1119        assert_eq!(
1120            LoraModulation::from_link(&LinkSettings::new(7, 200_000)),
1121            None
1122        );
1123        let bare = LinkSettings::new(9, 125_000)
1124            .with_preamble(300)
1125            .implicit_header()
1126            .without_crc();
1127        assert_eq!(
1128            LoraPacket::from_link(&bare, 51, true).to_params(),
1129            [0x01, 0x2C, 0x01, 0x33, 0x00, 0x01]
1130        );
1131    }
1132
1133    #[test]
1134    fn the_amplifier_settings_follow_table_13_21_and_section_13_4_4() {
1135        assert_eq!(
1136            PaConfig::SX1262_22_DBM.to_params(),
1137            [0x04, 0x07, 0x00, 0x01]
1138        );
1139        assert_eq!(
1140            PaConfig::SX1262_20_DBM.to_params(),
1141            [0x03, 0x05, 0x00, 0x01]
1142        );
1143        assert_eq!(
1144            PaConfig::SX1262_17_DBM.to_params(),
1145            [0x02, 0x03, 0x00, 0x01]
1146        );
1147        assert_eq!(
1148            PaConfig::SX1262_14_DBM.to_params(),
1149            [0x02, 0x02, 0x00, 0x01]
1150        );
1151        assert_eq!(
1152            PaConfig::SX1261_15_DBM.to_params(),
1153            [0x06, 0x00, 0x01, 0x01]
1154        );
1155        assert_eq!(
1156            PaConfig::SX1261_14_DBM.to_params(),
1157            [0x04, 0x00, 0x01, 0x01]
1158        );
1159        assert_eq!(
1160            PaConfig::SX1261_10_DBM.to_params(),
1161            [0x01, 0x00, 0x01, 0x01]
1162        );
1163
1164        let high = |dbm| TxPower::for_output(PowerAmplifier::HighPower, dbm);
1165        assert_eq!(high(22).setting_dbm, 22);
1166        assert_eq!(high(30).setting_dbm, 22);
1167        assert_eq!(high(-20).setting_dbm, -9);
1168        let low = |dbm| TxPower::for_output(PowerAmplifier::LowPower, dbm);
1169        assert_eq!(
1170            low(15),
1171            TxPower {
1172                pa: PaConfig::SX1261_15_DBM,
1173                setting_dbm: 14
1174            }
1175        );
1176        assert_eq!(
1177            low(14),
1178            TxPower {
1179                pa: PaConfig::SX1261_14_DBM,
1180                setting_dbm: 14
1181            }
1182        );
1183        assert_eq!(low(-30).setting_dbm, -17);
1184        assert_eq!(PowerAmplifier::LowPower.setting_range_dbm(), (-17, 14));
1185    }
1186
1187    #[test]
1188    fn the_power_under_a_ceiling_rounds_down_for_the_antenna() {
1189        let collinear = LinkBudget {
1190            transmit_antenna_gain_dbi: Decibels::from_db(6),
1191            ..LinkBudget::default()
1192        };
1193        let power =
1194            TxPower::under_ceiling(PowerAmplifier::HighPower, &collinear, Decibels::from_db(16));
1195        assert_eq!(power.setting_dbm, 10);
1196        let yagi = LinkBudget {
1197            transmit_antenna_gain_dbi: Decibels::from_hundredths(915),
1198            ..LinkBudget::default()
1199        };
1200        let power = TxPower::under_ceiling(PowerAmplifier::HighPower, &yagi, Decibels::from_db(30));
1201        assert_eq!(power.setting_dbm, 20);
1202    }
1203
1204    #[test]
1205    fn the_sync_words_and_workaround_values_follow_the_datasheet() {
1206        assert_eq!(SyncWord::Public.to_bytes(), [0x34, 0x44]);
1207        assert_eq!(SyncWord::Private.to_bytes(), [0x14, 0x24]);
1208        assert_eq!(SyncWord::Custom(0x1234).to_bytes(), [0x12, 0x34]);
1209        assert_eq!(tx_modulation(0x01, LoraBandwidth::Khz125), 0x05);
1210        assert_eq!(tx_modulation(0x05, LoraBandwidth::Khz500), 0x01);
1211        assert_eq!(tx_clamp(0xC8), 0xDE);
1212        assert_eq!(iq_polarity(0x0D, true), 0x09);
1213        assert_eq!(iq_polarity(0x09, false), 0x0D);
1214        assert_eq!(event_clear(0x00), 0x02);
1215    }
1216}