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}