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}