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