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