use super::{frame_with, indexed};
const SDRAM_TYPE: [u8; 2] = [0x1a, 0x00];
const OP_DATA: u8 = 0x01;
const OP_PROGRAM: u8 = 0x03;
const OP_ERASE: u8 = 0x05;
pub const CHUNK: usize = 1024;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Partition {
Primary,
Golden,
}
impl Partition {
const fn selector(self) -> u8 {
match self {
Self::Primary => 0x04,
Self::Golden => 0x0d,
}
}
}
fn sdram_frame(sel: u16, op: u8, flag: u8, offset: u32, len: u32, data: &[u8]) -> Vec<u8> {
let body = data.len().max(CHUNK);
frame_with(SDRAM_TYPE, 12 + body, |p| {
indexed(p, sel, op);
p[4] = flag;
p[5..8].copy_from_slice(&offset.to_be_bytes()[1..]);
p[8..12].copy_from_slice(&len.to_be_bytes());
p[12..12 + data.len()].copy_from_slice(data);
})
}
#[must_use]
pub fn sdram_chunk(sel: u16, offset: u32, data: &[u8]) -> Vec<u8> {
sdram_frame(sel, OP_DATA, 0x00, offset, data.len() as u32, data)
}
#[must_use]
pub fn sdram_erase(sel: u16, partition: Partition, len: u32) -> Vec<u8> {
sdram_frame(sel, OP_ERASE, partition.selector(), 0, len, &[])
}
#[must_use]
pub fn sdram_program(sel: u16, partition: Partition, len: u32) -> Vec<u8> {
sdram_frame(sel, OP_PROGRAM, partition.selector(), 0, len, &[])
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Descriptor {
pub start: u32,
pub image_len: u32,
pub file_len: u32,
pub flash_op_type: u8,
capabilities: u8,
}
impl Descriptor {
#[must_use]
pub const fn supports_sdram(self) -> bool {
self.capabilities & 0b0001 != 0
}
#[must_use]
pub const fn has_golden(self) -> bool {
self.capabilities & 0b0010 != 0
}
#[must_use]
pub const fn supports_select_part(self) -> bool {
self.capabilities & 0b0100 != 0
}
#[must_use]
pub const fn supports_golden_upgrade(self) -> bool {
self.capabilities & 0b1000 != 0
}
#[must_use]
pub const fn chunks(self) -> usize {
(self.image_len as usize).div_ceil(CHUNK)
}
#[must_use]
pub const fn first_poll_ms(self) -> u64 {
let blocks = (self.image_len as u64).div_ceil(0x10000);
let by_size = 150 * blocks;
if by_size > 1000 {
by_size
} else {
1000
}
}
#[must_use]
pub const fn estimated_ms(self) -> u64 {
let blocks = (self.image_len as u64).div_ceil(0x10000);
let pages = (self.image_len as u64).div_ceil(0x100);
500 * blocks + 3 * pages
}
}
#[must_use]
pub fn parse_descriptor(eth_frame: &[u8]) -> Option<Descriptor> {
let p = eth_frame.get(12..)?;
if *p.first()? != 0x08 || p.get(1)? & 0b10 == 0 {
return None;
}
let byte = |i: usize| p.get(i).copied();
let image_len = u32::from(byte(0x18)?) << 16 | u32::from(byte(0x19)?) << 8;
let low = u32::from(byte(0x1b)?.wrapping_add(byte(0x1a)?.wrapping_mul(4)));
Some(Descriptor {
start: u32::from(byte(0x16)?) << 16 | u32::from(byte(0x17)?) << 8,
image_len,
file_len: image_len | (low & 0xff),
flash_op_type: byte(0x12)?,
capabilities: byte(0x13)?,
})
}
#[must_use]
pub fn programming_finished(eth_frame: &[u8]) -> bool {
eth_frame
.get(12 + 0xc0)
.is_some_and(|status| status & 0b10 != 0)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::BROADCAST;
fn real_reply() -> Vec<u8> {
let mut f = vec![0u8; 12];
f.extend_from_slice(&[0x08, 0x02]);
f.extend_from_slice(&[
0x00, 0x1a, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0xff, 0x00, 0x36, 0x05, 0x00, 0x00, 0x00, 0x00, 0x0b, 0x00, 0x20, 0x00,
]);
f.resize(1070, 0);
f
}
#[test]
fn the_real_reply_decodes_as_the_card_reported() {
let d = parse_descriptor(&real_reply()).expect("should decode");
assert_eq!(d.start, 0x0000_0000, "image starts at zero");
assert_eq!(d.image_len, 0x000b_0000, "720896 bytes programmed");
assert_eq!(d.file_len, 0x000b_0080, "721024 bytes of source file");
assert_eq!(d.flash_op_type, 0x36);
assert!(d.supports_sdram(), "this card stages via SDRAM");
assert!(!d.has_golden(), "and reports no golden bank");
assert!(d.supports_select_part());
assert!(!d.supports_golden_upgrade());
}
#[test]
fn the_image_needs_704_chunks() {
let d = parse_descriptor(&real_reply()).unwrap();
assert_eq!(d.chunks(), 704);
}
#[test]
fn timings_match_the_vendor_formulas() {
let d = parse_descriptor(&real_reply()).unwrap();
assert_eq!(d.first_poll_ms(), 1650);
assert_eq!(d.estimated_ms(), 500 * 11 + 3 * 2816);
}
#[test]
fn a_reply_of_the_wrong_type_is_rejected() {
let mut f = real_reply();
f[12] = 0x09;
assert!(parse_descriptor(&f).is_none());
}
#[test]
fn a_chunk_frame_matches_the_documented_layout() {
let data: Vec<u8> = (0..CHUNK).map(|i| i as u8).collect();
let f = sdram_chunk(BROADCAST, 0x000c_0400, &data);
assert_eq!(f.len(), 1050, "12 MAC + 2 type + 12 header + 1024 data");
assert_eq!(&f[12..14], &SDRAM_TYPE);
assert_eq!(&f[15..17], &BROADCAST.to_be_bytes());
assert_eq!(f[17], OP_DATA);
assert_eq!(f[18], 0x00);
assert_eq!(&f[19..22], &[0x0c, 0x04, 0x00], "24-bit big-endian offset");
assert_eq!(&f[22..26], &1024u32.to_be_bytes());
assert_eq!(&f[26..], &data[..]);
}
#[test]
fn erase_and_program_carry_the_partition_and_length() {
let len = 0x000b_0000;
let e = sdram_erase(BROADCAST, Partition::Primary, len);
assert_eq!(e[17], OP_ERASE);
assert_eq!(e[18], 0x04, "primary partition selector");
assert_eq!(&e[19..22], &[0, 0, 0], "no address on erase");
assert_eq!(&e[22..26], &len.to_be_bytes());
let p = sdram_program(BROADCAST, Partition::Primary, len);
assert_eq!(p[17], OP_PROGRAM);
assert_eq!(&p[18..], &e[18..]);
}
#[test]
fn the_golden_partition_uses_its_own_selector() {
let g = sdram_erase(BROADCAST, Partition::Golden, 0x1000);
assert_eq!(g[18], 0x0d);
}
#[test]
fn completion_is_read_from_the_status_bit() {
let mut f = vec![0u8; 12 + 0xc1];
assert!(!programming_finished(&f));
f[12 + 0xc0] = 0b10;
assert!(programming_finished(&f));
}
}