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}