Skip to main content

pamoja_radios/sx1302/
firmware.rs

1//! Putting firmware into the two microcontrollers the concentrator runs.
2//!
3//! The chip does not receive anything on its own. Two microcontrollers inside it do the work,
4//! and both start empty: the gain control one sets the front end gains, and the arbiter shares
5//! the radios between the receivers. Each holds eight kilobytes, written over the same SPI bus
6//! as everything else, into a memory window the host takes control of first.
7//!
8//! The order matters and the chip does not enforce it. The host holds the microcontroller in
9//! reset, claims its memory, writes, reads the same bytes back and compares them, releases it,
10//! and only then asks whether the parity check passed. Skipping the read-back means a bad bus
11//! looks like a working one until the concentrator quietly hears nothing.
12//!
13//! The firmware images themselves are not in this crate. They belong to Semtech and are
14//! carried by the caller, so a [`Load`] says what to do with the bytes rather than holding
15//! them. The reference implementation distributes them as C source rather than as files of
16//! bytes, so [`read_source`] reads that form directly and a gateway points at the file it
17//! already has.
18
19use super::register::{self, Register};
20
21/// How many bytes of firmware each microcontroller holds.
22///
23/// The memory is fourteen bits wide, so this is twice the number of words.
24pub const FIRMWARE_LEN: usize = 8192;
25
26/// Where the gain control firmware is written.
27pub const AGC_MEMORY: u16 = 0x0000;
28
29/// Where the arbiter firmware is written.
30pub const ARB_MEMORY: u16 = 0x2000;
31
32/// Which microcontroller is being loaded.
33#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub enum Mcu {
35    /// The gain control microcontroller, which drives the front end gains.
36    Agc,
37    /// The arbiter, which shares the radios between the receivers.
38    Arb,
39}
40
41impl Mcu {
42    /// Where this microcontroller keeps its firmware.
43    ///
44    /// # Returns
45    ///
46    /// The address the first byte is written to.
47    #[must_use]
48    pub const fn memory(&self) -> u16 {
49        match self {
50            Mcu::Agc => AGC_MEMORY,
51            Mcu::Arb => ARB_MEMORY,
52        }
53    }
54
55    /// The register that holds this microcontroller in reset.
56    ///
57    /// # Returns
58    ///
59    /// The register.
60    #[must_use]
61    pub const fn clear(&self) -> Register {
62        match self {
63            Mcu::Agc => register::AGC_MCU_CLEAR,
64            Mcu::Arb => register::ARB_MCU_CLEAR,
65        }
66    }
67
68    /// The register that gives the host this microcontroller memory.
69    ///
70    /// # Returns
71    ///
72    /// The register.
73    #[must_use]
74    pub const fn host_prog(&self) -> Register {
75        match self {
76            Mcu::Agc => register::AGC_MCU_HOST_PROG,
77            Mcu::Arb => register::ARB_MCU_HOST_PROG,
78        }
79    }
80
81    /// The register the chip reports a failed parity check in.
82    ///
83    /// # Returns
84    ///
85    /// The register.
86    #[must_use]
87    pub const fn parity_error(&self) -> Register {
88        match self {
89            Mcu::Agc => register::AGC_MCU_PARITY_ERROR,
90            Mcu::Arb => register::ARB_MCU_PARITY_ERROR,
91        }
92    }
93
94    /// What this microcontroller is called.
95    ///
96    /// # Returns
97    ///
98    /// The name, for a message a person reads.
99    #[must_use]
100    pub const fn name(&self) -> &'static str {
101        match self {
102            Mcu::Agc => "gain control",
103            Mcu::Arb => "arbiter",
104        }
105    }
106}
107
108/// A step in loading firmware.
109///
110/// The whole sequence is data, so a driver walks it against a bus and a test walks it against
111/// nothing.
112#[derive(Clone, Copy, Debug, PartialEq, Eq)]
113pub enum Load {
114    /// Write a value to a register.
115    Write(Register, u8),
116    /// Write the firmware to memory, starting here.
117    WriteFirmware(u16),
118    /// Read the same span back, to compare it with what went out.
119    ReadBack(u16),
120    /// Read the parity register, which must come back zero.
121    CheckParity(Register),
122}
123
124/// Why firmware did not load.
125#[derive(Clone, Copy, Debug, PartialEq, Eq)]
126pub enum LoadError {
127    /// The image handed over is not the size the microcontroller holds.
128    WrongSize {
129        /// How many bytes the caller offered.
130        offered: usize,
131    },
132    /// The bytes read back are not the bytes written, so the bus is not carrying them.
133    ReadBackDiffers {
134        /// Where the first difference is.
135        at: usize,
136    },
137    /// The bytes arrived but the chip refused them, so the image itself is wrong.
138    ParityFailed,
139}
140
141impl core::fmt::Display for LoadError {
142    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
143        match self {
144            LoadError::WrongSize { offered } => write!(
145                f,
146                "firmware is {offered} bytes, and a microcontroller holds {FIRMWARE_LEN}"
147            ),
148            LoadError::ReadBackDiffers { at } => write!(
149                f,
150                "the firmware read back differs at byte {at}, so the bus is not carrying it"
151            ),
152            LoadError::ParityFailed => {
153                f.write_str("the concentrator refused the firmware on its own parity check")
154            }
155        }
156    }
157}
158
159/// The steps that load one microcontroller, in order.
160///
161/// # Arguments
162///
163/// * `mcu` - which microcontroller to load.
164///
165/// # Returns
166///
167/// Hold it in reset, claim its memory, put the memory window on the first page, write, read
168/// back, release it, and check the parity the chip reports.
169///
170/// # Examples
171///
172/// ```
173/// use pamoja_radios::sx1302::firmware::{steps, Load, Mcu};
174/// use pamoja_radios::sx1302::register;
175///
176/// let plan = steps(Mcu::Agc);
177///
178/// // The microcontroller is stopped before its memory is touched.
179/// assert_eq!(plan[0], Load::Write(register::AGC_MCU_CLEAR, 1));
180/// assert_eq!(plan[1], Load::Write(register::AGC_MCU_HOST_PROG, 1));
181///
182/// // And the chip has the last word on whether the firmware took.
183/// assert_eq!(plan[6], Load::CheckParity(register::AGC_MCU_PARITY_ERROR));
184/// ```
185#[must_use]
186pub const fn steps(mcu: Mcu) -> [Load; 7] {
187    [
188        Load::Write(mcu.clear(), 1),
189        Load::Write(mcu.host_prog(), 1),
190        Load::Write(register::COMMON_PAGE, 0),
191        Load::WriteFirmware(mcu.memory()),
192        Load::ReadBack(mcu.memory()),
193        Load::Write(mcu.host_prog(), 0),
194        Load::CheckParity(mcu.parity_error()),
195    ]
196}
197
198/// Checks an image before any of it goes out.
199///
200/// # Arguments
201///
202/// * `firmware` - the image the caller holds.
203///
204/// # Returns
205///
206/// `Ok(())` when it is the size a microcontroller holds.
207///
208/// # Errors
209///
210/// Returns [`LoadError::WrongSize`] otherwise, rather than writing a truncated image and
211/// leaving the chip to fail its own parity check later.
212pub const fn check_size(firmware: &[u8]) -> Result<(), LoadError> {
213    if firmware.len() == FIRMWARE_LEN {
214        Ok(())
215    } else {
216        Err(LoadError::WrongSize {
217            offered: firmware.len(),
218        })
219    }
220}
221
222/// Reads an image out of the C source the reference implementation carries it in.
223///
224/// Semtech distributes each image as an array of byte literals rather than as a file of
225/// bytes, so converting it by hand is the first thing that stands between a new board and a
226/// running gateway. This reads the literals in the order they are written, which turns that
227/// step into naming the file.
228///
229/// Anything that is not a byte literal is skipped, so the declaration around the array, the
230/// commas, and the closing brace need no stripping.
231///
232/// # Arguments
233///
234/// * `text` - the file, as text.
235/// * `into` - where the bytes go, which is exactly what a microcontroller holds.
236///
237/// # Returns
238///
239/// `Ok(())` once every byte is in place.
240///
241/// # Errors
242///
243/// Returns [`LoadError::WrongSize`] carrying how many literals the text holds when that is
244/// not [`FIRMWARE_LEN`], rather than loading a partial image. A count below it means the file
245/// is truncated, and one above means it carries more than the single array.
246///
247/// # Examples
248///
249/// ```
250/// use pamoja_radios::sx1302::firmware::{read_source, FIRMWARE_LEN};
251///
252/// // The shape Semtech ships, shortened here to what fits on the page.
253/// let mut source = String::from("static uint8_t arb_firmware[8192] = {\n");
254/// for at in 0..FIRMWARE_LEN {
255///     source.push_str(&format!("0x{:02X}, ", at % 256));
256/// }
257/// source.push_str("\n};\n");
258///
259/// let mut image = [0u8; FIRMWARE_LEN];
260/// read_source(&source, &mut image).expect("the array holds a whole image");
261/// assert_eq!(image[0], 0x00);
262/// assert_eq!(image[255], 0xFF);
263/// ```
264pub fn read_source(text: &str, into: &mut [u8; FIRMWARE_LEN]) -> Result<(), LoadError> {
265    let raw = text.as_bytes();
266    let mut found = 0usize;
267    let mut at = 0usize;
268
269    while at + 2 < raw.len() {
270        let prefixed = raw[at] == b'0' && (raw[at + 1] | 0x20) == b'x';
271        let Some(high) = hex_digit(raw[at + 2]).filter(|_| prefixed) else {
272            at += 1;
273            continue;
274        };
275
276        // A literal is written with two digits throughout the reference files, but one is
277        // still a byte, so a single digit is read rather than silently dropped.
278        let (value, width) = match raw.get(at + 3).copied().and_then(hex_digit) {
279            Some(low) => (high * 16 + low, 4),
280            None => (high, 3),
281        };
282
283        if let Some(slot) = into.get_mut(found) {
284            *slot = value;
285        }
286        found += 1;
287        at += width;
288    }
289
290    if found == FIRMWARE_LEN {
291        Ok(())
292    } else {
293        Err(LoadError::WrongSize { offered: found })
294    }
295}
296
297/// Reads one hexadecimal digit.
298const fn hex_digit(byte: u8) -> Option<u8> {
299    match byte {
300        b'0'..=b'9' => Some(byte - b'0'),
301        b'a'..=b'f' => Some(byte - b'a' + 10),
302        b'A'..=b'F' => Some(byte - b'A' + 10),
303        _ => None,
304    }
305}
306
307/// Compares what was read back with what was written.
308///
309/// # Arguments
310///
311/// * `written` - the image that went out.
312/// * `read_back` - the same span read from the chip.
313///
314/// # Returns
315///
316/// `Ok(())` when every byte matches.
317///
318/// # Errors
319///
320/// Returns [`LoadError::ReadBackDiffers`] naming the first byte that does not, which says
321/// where on the bus the trouble starts rather than only that there is some.
322pub fn compare(written: &[u8], read_back: &[u8]) -> Result<(), LoadError> {
323    for (at, (out, back)) in written.iter().zip(read_back.iter()).enumerate() {
324        if out != back {
325            return Err(LoadError::ReadBackDiffers { at });
326        }
327    }
328    if written.len() == read_back.len() {
329        Ok(())
330    } else {
331        Err(LoadError::ReadBackDiffers {
332            at: written.len().min(read_back.len()),
333        })
334    }
335}
336
337#[cfg(test)]
338mod tests {
339    use super::*;
340
341    #[test]
342    fn an_image_is_read_out_of_the_source_it_is_distributed_as() {
343        let mut source = String::from("static uint8_t agc_firmware_sx1250[8192] = { \n");
344        for at in 0..FIRMWARE_LEN {
345            source.push_str(&format!("0x{:02X}, ", at % 251));
346            if at % 16 == 15 {
347                source.push('\n');
348            }
349        }
350        source.push_str("};\n");
351
352        let mut image = [0u8; FIRMWARE_LEN];
353        read_source(&source, &mut image).expect("the array holds a whole image");
354
355        // The declaration, the commas and the closing brace carry no literals, so none of
356        // them shift the bytes.
357        for (at, byte) in image.iter().enumerate() {
358            assert_eq!(*byte, (at % 251) as u8, "byte {at}");
359        }
360    }
361
362    #[test]
363    fn a_truncated_file_is_refused_with_what_it_held() {
364        let source = "static uint8_t arb_firmware[8192] = { 0x01, 0x02, 0x03 };";
365
366        let mut image = [0u8; FIRMWARE_LEN];
367        assert_eq!(
368            read_source(source, &mut image),
369            Err(LoadError::WrongSize { offered: 3 })
370        );
371    }
372
373    #[test]
374    fn a_file_carrying_more_than_one_array_is_refused() {
375        let mut source = String::new();
376        for _ in 0..=FIRMWARE_LEN {
377            source.push_str("0x00, ");
378        }
379
380        // Counting past the end rather than stopping at it is what lets the error say how
381        // much the file actually holds.
382        let mut image = [0u8; FIRMWARE_LEN];
383        assert_eq!(
384            read_source(&source, &mut image),
385            Err(LoadError::WrongSize {
386                offered: FIRMWARE_LEN + 1
387            })
388        );
389    }
390
391    #[test]
392    fn the_case_of_a_literal_does_not_matter() {
393        let mut source = String::new();
394        for at in 0..FIRMWARE_LEN {
395            source.push_str(if at % 2 == 0 { "0xab, " } else { "0XCD, " });
396        }
397
398        let mut image = [0u8; FIRMWARE_LEN];
399        read_source(&source, &mut image).expect("both cases are literals");
400        assert_eq!(image[0], 0xAB);
401        assert_eq!(image[1], 0xCD);
402    }
403
404    #[test]
405    fn each_microcontroller_has_its_own_memory_and_controls() {
406        assert_eq!(Mcu::Agc.memory(), 0x0000);
407        assert_eq!(Mcu::Arb.memory(), 0x2000);
408
409        // The two never share a register, or loading one would disturb the other.
410        assert_ne!(Mcu::Agc.clear(), Mcu::Arb.clear());
411        assert_ne!(Mcu::Agc.host_prog(), Mcu::Arb.host_prog());
412        assert_ne!(Mcu::Agc.parity_error(), Mcu::Arb.parity_error());
413
414        assert_eq!(Mcu::Agc.name(), "gain control");
415        assert_eq!(Mcu::Arb.name(), "arbiter");
416    }
417
418    #[test]
419    fn the_firmware_spans_the_memory_exactly() {
420        // Eight kilobytes at each address, and the two spans do not overlap.
421        assert_eq!(FIRMWARE_LEN, 8192);
422        assert_eq!(AGC_MEMORY as usize + FIRMWARE_LEN, ARB_MEMORY as usize);
423    }
424
425    #[test]
426    fn the_sequence_stops_the_chip_before_writing_and_checks_after() {
427        for mcu in [Mcu::Agc, Mcu::Arb] {
428            let plan = steps(mcu);
429
430            // Stopped and claimed before the memory is touched.
431            assert_eq!(plan[0], Load::Write(mcu.clear(), 1));
432            assert_eq!(plan[1], Load::Write(mcu.host_prog(), 1));
433            assert_eq!(plan[2], Load::Write(register::COMMON_PAGE, 0));
434
435            // Written, then read back before anything is released.
436            assert_eq!(plan[3], Load::WriteFirmware(mcu.memory()));
437            assert_eq!(plan[4], Load::ReadBack(mcu.memory()));
438
439            // Released, then asked whether it took.
440            assert_eq!(plan[5], Load::Write(mcu.host_prog(), 0));
441            assert_eq!(plan[6], Load::CheckParity(mcu.parity_error()));
442        }
443    }
444
445    #[test]
446    fn an_image_of_the_wrong_size_never_reaches_the_bus() {
447        assert_eq!(check_size(&[0u8; FIRMWARE_LEN]), Ok(()));
448        assert_eq!(
449            check_size(&[0u8; 16]),
450            Err(LoadError::WrongSize { offered: 16 })
451        );
452        assert_eq!(check_size(&[]), Err(LoadError::WrongSize { offered: 0 }));
453    }
454
455    #[test]
456    fn a_read_back_says_where_the_bus_went_wrong() {
457        let written = [0xa5u8; 64];
458        assert_eq!(compare(&written, &written), Ok(()));
459
460        // One byte dropped, and the caller is told which.
461        let mut back = written;
462        back[40] = 0x00;
463        assert_eq!(
464            compare(&written, &back),
465            Err(LoadError::ReadBackDiffers { at: 40 })
466        );
467
468        // A short read is a difference too, at the point it stopped.
469        assert_eq!(
470            compare(&written, &written[..20]),
471            Err(LoadError::ReadBackDiffers { at: 20 })
472        );
473    }
474
475    #[test]
476    fn every_failure_says_what_to_do_about_it() {
477        // A person reading the log should be able to tell a wiring fault from a bad image.
478        let wiring = LoadError::ReadBackDiffers { at: 7 };
479        let image = LoadError::ParityFailed;
480        assert!(format!("{wiring}").contains("bus is not carrying"));
481        assert!(format!("{image}").contains("parity"));
482        assert!(format!("{}", LoadError::WrongSize { offered: 3 }).contains("8192"));
483    }
484}