spi_flash/
sfdp.rs

1use crate::{Error, Result};
2use alloc::vec::Vec;
3use core::time::Duration;
4
5#[derive(Clone, Debug)]
6pub(crate) struct SFDPHeader {
7    pub nph: usize,
8    pub major: u8,
9    pub minor: u8,
10    pub params: Vec<SFDPParameterHeader>,
11}
12
13impl SFDPHeader {
14    pub fn from_bytes(data: &[u8]) -> Result<Self> {
15        log::debug!("Parsing SFDP header from data: {:X?}", data);
16        if &data[0..4] != b"SFDP" {
17            log::error!("Did not read expected SFDP signature");
18            Err(Error::InvalidSFDPHeader)
19        } else if data[7] != 0xFF {
20            log::error!("Unsupported SFDP access protocol {:02X}", data[7]);
21            Err(Error::InvalidSFDPHeader)
22        } else {
23            let minor = data[4];
24            let major = data[5];
25            let nph = data[6] as usize + 1;
26            log::debug!(
27                "Read SFDP header, NPH={} MAJOR={} MINOR={}",
28                nph,
29                major,
30                minor
31            );
32            if data.len() < (nph + 1) * 8 {
33                log::error!(
34                    "Did not read enough SFDP bytes: got {}, needed {}",
35                    data.len(),
36                    (nph + 1) * 8
37                );
38                Err(Error::InvalidSFDPHeader)
39            } else {
40                let params = data[8..]
41                    .chunks(8)
42                    .map(SFDPParameterHeader::from_bytes)
43                    .collect();
44                Ok(SFDPHeader {
45                    nph,
46                    major,
47                    minor,
48                    params,
49                })
50            }
51        }
52    }
53}
54
55#[derive(Copy, Clone, Debug)]
56pub(crate) struct SFDPParameterHeader {
57    pub plen: usize,
58    pub major: u8,
59    pub minor: u8,
60    pub parameter_id: u16,
61    pub ptp: u32,
62}
63
64impl SFDPParameterHeader {
65    fn from_bytes(data: &[u8]) -> Self {
66        log::debug!("Reading SFDP parameter header from: {:X?}", data);
67        let parameter_id = u16::from_be_bytes([data[7], data[0]]);
68        let minor = data[1];
69        let major = data[2];
70        let plen = data[3] as usize;
71        let ptp = u32::from_be_bytes([0, data[6], data[5], data[4]]);
72        log::debug!(
73            "Read JEDEC parameter header, plen={} major={} minor={} \
74                     ID=0x{:04X} PTP=0x{:06X}",
75            plen,
76            major,
77            minor,
78            parameter_id,
79            ptp
80        );
81        SFDPParameterHeader {
82            plen,
83            major,
84            minor,
85            parameter_id,
86            ptp,
87        }
88    }
89}
90
91/// SFDP JEDEC Basic Flash Parameter Table
92///
93/// This table contains standard SFDP information which may be
94/// read from a flash memory. Only fields relevant to single I/O
95/// operation are parsed.
96///
97/// Fields are taken from JESD216D-01, supporting parameter versions up to 1.7.
98#[derive(Copy, Clone, Debug)]
99pub struct FlashParams {
100    /// Parameter header major version field.
101    pub version_major: u8,
102    /// Parameter header minor version field.
103    pub version_minor: u8,
104
105    /// Number of address bytes to use in read/write commands.
106    pub address_bytes: SFDPAddressBytes,
107    /// Flash memory density in bits.
108    pub density: u64,
109
110    /// If true, 4kB erase is supported.
111    /// Newer memories indicate all erase sizes with `erase_*` fields.
112    pub legacy_4kb_erase_supported: bool,
113    /// Instruction for 4kB erase, or 0xFF if unsupported.
114    /// Newer memories also include this instruction in `erase_*` fields.
115    pub legacy_4kb_erase_inst: u8,
116    /// Write enable instruction for volatile status register, either 0x50 or 0x06.
117    /// Newer memories use `status_1_vol` instead.
118    pub legacy_volatile_write_en_inst: u8,
119    /// If true, Block Protect bits in status register are only volatile,
120    /// otherwise they may be only non-volatile or may be programmed either
121    /// as volatile with instruction 0x50 or non-volatile with instruction 0x06.
122    /// Newer memories use `status_1_vol` instead.
123    pub legacy_block_protect_volatile: bool,
124    /// If true, writes can be performed with byte granularity.
125    /// Newer memories use `page_size`.
126    pub legacy_byte_write_granularity: bool,
127
128    /// Erase instructions.
129    ///
130    /// Up to four erase instructions may be available,
131    /// each specifying the opcode for the instruction
132    /// and the number of bytes erased.
133    pub erase_insts: [Option<SFDPEraseInst>; 4],
134
135    /// Chip erase and programming times, if available.
136    pub timing: Option<SFDPTiming>,
137
138    /// Page size, in bytes.
139    pub page_size: Option<u32>,
140
141    // Omitted: Suspend/Resume support and instructions.
142
143    // Omitted: Deep powerdown support and instructions.
144    /// If true, polling busy status via the flag status register is supported.
145    /// Instruction 0x70 reads the flag register, where bit 7 is 0 if busy and 1 if ready.
146    pub busy_poll_flag: Option<bool>,
147    /// If true, polling busy status via the status register is supported.
148    /// Instruction 0x05 reads the status register, where bit 0 is 0 if ready and 1 if busy.
149    pub busy_poll_status: Option<bool>,
150
151    // Omitted: instructions for entering/exiting 4-byte address mode.
152    /// If true, the device may be reset using instruction 0xF0.
153    pub reset_inst_f0: Option<bool>,
154    /// If true, the device may be reset using instruction 0x66 followed by 0x99.
155    pub reset_inst_66_99: Option<bool>,
156
157    /// Status register 1 volatility and write-enable instruction.
158    pub status_1_vol: Option<SFDPStatus1Volatility>,
159}
160
161/// SFDP Address Bytes field.
162#[derive(Copy, Clone, Debug)]
163pub enum SFDPAddressBytes {
164    /// Three-byte only addressing.
165    Three,
166    /// Three- or four-byte addressing; default is three-byte
167    /// but may be configured for four-byte.
168    ThreeOrFour,
169    /// Four-byte only addressing.
170    Four,
171    /// Reserved as of JESD216D-01, JEDEC Basic Flash Parameters version 1.7.
172    Reserved,
173}
174
175impl SFDPAddressBytes {
176    fn from_bits(bits: u32) -> Self {
177        match bits {
178            0b00 => SFDPAddressBytes::Three,
179            0b01 => SFDPAddressBytes::ThreeOrFour,
180            0b10 => SFDPAddressBytes::Four,
181            _ => SFDPAddressBytes::Reserved,
182        }
183    }
184}
185
186/// SFDP Erase Instruction.
187#[derive(Copy, Clone, Debug)]
188pub struct SFDPEraseInst {
189    /// Opcode for erase instruction.
190    pub opcode: u8,
191    /// Size in bytes of erase instruction.
192    pub size: u32,
193    /// Typical erase time, if known.
194    pub time_typ: Option<Duration>,
195    /// Maximum erase time, if known.
196    pub time_max: Option<Duration>,
197}
198
199impl core::fmt::Display for SFDPEraseInst {
200    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
201        write!(f, "Opcode 0x{:02X}: {} bytes", self.opcode, self.size)?;
202        if let Some(typ) = self.time_typ {
203            write!(f, ", typ {:?}", typ)?;
204        }
205        if let Some(max) = self.time_max {
206            write!(f, ", max {:?}", max)?;
207        }
208        Ok(())
209    }
210}
211
212#[derive(Copy, Clone, Debug)]
213pub enum SFDPStatus1Volatility {
214    /// Status register 1 is non-volatile, powers up to its last state, write-enable with 0x06.
215    NonVolatile06,
216    /// Status register 1 is volatile, powers up to all '1', write-enable with 0x06.
217    Volatile06,
218    /// Status register 1 is volatile, powers up to all '1', write-enable with 0x50.
219    Volatile50,
220    /// Status register 1 powers up to its last non-volatile state, use 0x06
221    /// to write to non-volatile register, or use 0x50 to active and write
222    /// volatile register.
223    NonVolatile06Volatile50,
224    /// Status register 1 contains a mix of volatile and non-volatile bits.
225    /// Use instruction 0x06 to write.
226    Mixed06,
227    /// Reserved volatility mode.
228    Reserved,
229}
230
231impl SFDPStatus1Volatility {
232    pub fn from_bits(bits: u32) -> Self {
233        if bits & 0b000_0001 != 0 {
234            SFDPStatus1Volatility::NonVolatile06
235        } else if bits & 0b000_0010 != 0 {
236            SFDPStatus1Volatility::Volatile06
237        } else if bits & 0b000_0100 != 0 {
238            SFDPStatus1Volatility::Volatile50
239        } else if bits & 0b000_1000 != 0 {
240            SFDPStatus1Volatility::NonVolatile06Volatile50
241        } else if bits & 0b001_0000 != 0 {
242            SFDPStatus1Volatility::Mixed06
243        } else {
244            SFDPStatus1Volatility::Reserved
245        }
246    }
247}
248
249/// Struct of timing information from JESD216A-compliant tables.
250///
251/// Note that erase instruction timing is stored inside the respective erase instructions.
252#[derive(Copy, Clone, Debug)]
253pub struct SFDPTiming {
254    /// Typical time to erase the entire chip, if known.
255    pub chip_erase_time_typ: Duration,
256    /// Maximum time to erase the entire chip, if known.
257    pub chip_erase_time_max: Duration,
258    /// Typical time to program the first byte in a sequence, if known.
259    pub first_byte_prog_time_typ: Duration,
260    /// Maximum time to program the first byte in a sequence, if known.
261    pub first_byte_prog_time_max: Duration,
262    /// Typical time to program each successive byte in a sequence, if known.
263    pub succ_byte_prog_time_typ: Duration,
264    /// Maximum time to program each successive byte in a sequence, if known.
265    pub succ_byte_prog_time_max: Duration,
266    /// Typical time to program a full page, if known.
267    pub page_prog_time_typ: Duration,
268    /// Maximum time to program a full page, if known.
269    pub page_prog_time_max: Duration,
270}
271
272/// Bitfield extraction helper macro.
273///
274/// `bits!(word, length, offset)` extracts `length` number of bits at offset `offset`.
275macro_rules! bits {
276    ($d:expr, $n:expr, $o:expr) => {
277        ($d & (((1 << $n) - 1) << $o)) >> $o
278    };
279}
280
281impl FlashParams {
282    pub fn from_bytes(major: u8, minor: u8, data: &[u8]) -> Result<Self> {
283        log::debug!(
284            "Reading SFDP JEDEC Basic Flash Parameters from: {:X?}",
285            data
286        );
287
288        // Check we have enough data.
289        if data.len() % 4 != 0 {
290            log::error!("SFPD data is not a multiple of 4 bytes.");
291            return Err(Error::InvalidSFDPParams);
292        } else if data.len() < 9 * 4 {
293            log::error!("SFPD data is not long enough for version >= 1.0.");
294            return Err(Error::InvalidSFDPParams);
295        } else if major != 1 {
296            log::error!("Only SFPD major version 1 is supported.");
297            return Err(Error::InvalidSFDPParams);
298        } else if minor > 5 && data.len() < 16 * 4 {
299            log::error!("SFPD data is not long enough for version >= 1.5.");
300            return Err(Error::InvalidSFDPParams);
301        }
302
303        // Convert the bytes into "DWORD"s (u32) for easier reference to the specification.
304        let mut dwords = Vec::new();
305        for bytes in data.chunks(4) {
306            dwords.push(u32::from_be_bytes([bytes[3], bytes[2], bytes[1], bytes[0]]));
307        }
308
309        // Parse the first 9 DWORDs, which must always be available if SFDP is supported.
310        let mut params = Self::read_jesd216(major, minor, &dwords);
311
312        // 1.5: JESD216A adds DWORDs 10-16.
313        if minor >= 5 {
314            params.read_jesd216a(&dwords);
315        }
316
317        // 1.6: JESD216B adds quad-SPI information to DWORD15 bits 8, 14, and 18.
318        // 1.7: JESD216C adds DWORDs 17-20 which describe octal SPI.
319        // 1.8: JESD216D doesn't change the basic flash parameters table.
320
321        Ok(params)
322    }
323
324    /// Get the flash capacity in bytes.
325    pub fn capacity_bytes(&self) -> usize {
326        (self.density / 8) as usize
327    }
328
329    /// Get the smallest erase granularity and its opcode.
330    pub fn sector_erase(&self) -> Option<(usize, u8)> {
331        let mut size = u32::MAX;
332        let mut opcode = 0u8;
333        for inst in self.erase_insts.iter().flatten() {
334            if inst.size < size {
335                size = inst.size;
336                opcode = inst.opcode;
337            }
338        }
339
340        if size != u32::MAX {
341            Some((size as usize, opcode))
342        } else {
343            None
344        }
345    }
346
347    /// Read the legacy information from JESD216 (DWORDs 1-9) and create a new FlashParams object.
348    fn read_jesd216(major: u8, minor: u8, dwords: &[u32]) -> FlashParams {
349        // 1st DWORD
350        let address_bytes = SFDPAddressBytes::from_bits(bits!(dwords[0], 2, 17));
351        let legacy_4kb_erase_inst = bits!(dwords[0], 8, 8) as u8;
352        let legacy_volatile_write_en_inst = match bits!(dwords[0], 1, 4) {
353            0 => 0x50,
354            1 => 0x06,
355            _ => unreachable!(),
356        };
357        let legacy_block_protect_volatile = bits!(dwords[0], 1, 3) == 1;
358        let legacy_byte_write_granularity = bits!(dwords[0], 1, 2) == 1;
359        let legacy_4kb_erase_supported = bits!(dwords[0], 2, 0) == 0b01;
360
361        // 2nd DWORD
362        let density = if dwords[1] >> 31 == 0 {
363            (dwords[1] as u64) + 1
364        } else {
365            1u64 << (dwords[1] & 0x7FFF_FFFF)
366        };
367
368        // DWORDS 3,4, 5, 6, and 7 relate to multiple I/O and are skipped.
369
370        // 8th and 9th DWORD
371        let mut erase_insts = [None; 4];
372        let erase_size_1 = bits!(dwords[7], 8, 0);
373        let erase_size_2 = bits!(dwords[7], 8, 16);
374        let erase_size_3 = bits!(dwords[8], 8, 0);
375        let erase_size_4 = bits!(dwords[8], 8, 16);
376        if erase_size_1 != 0 {
377            let opcode = bits!(dwords[7], 8, 8) as u8;
378            if opcode != 0 {
379                erase_insts[0] = Some(SFDPEraseInst {
380                    opcode,
381                    size: 1 << erase_size_1,
382                    time_typ: None,
383                    time_max: None,
384                });
385            }
386        }
387        if erase_size_2 != 0 {
388            let opcode = bits!(dwords[7], 8, 24) as u8;
389            if opcode != 0 {
390                erase_insts[1] = Some(SFDPEraseInst {
391                    opcode,
392                    size: 1 << erase_size_2,
393                    time_typ: None,
394                    time_max: None,
395                });
396            }
397        }
398        if erase_size_3 != 0 {
399            let opcode = bits!(dwords[8], 8, 8) as u8;
400            if opcode != 0 {
401                erase_insts[2] = Some(SFDPEraseInst {
402                    opcode,
403                    size: 1 << erase_size_3,
404                    time_typ: None,
405                    time_max: None,
406                });
407            }
408        }
409        if erase_size_4 != 0 {
410            let opcode = bits!(dwords[8], 8, 24) as u8;
411            if opcode != 0 {
412                erase_insts[3] = Some(SFDPEraseInst {
413                    opcode,
414                    size: 1 << erase_size_4,
415                    time_typ: None,
416                    time_max: None,
417                });
418            }
419        }
420
421        // Return a FlashParams with the legacy information set and further information
422        // cleared, which can be filled in if additional DWORDs are available.
423        FlashParams {
424            version_major: major,
425            version_minor: minor,
426            address_bytes,
427            density,
428            legacy_4kb_erase_supported,
429            legacy_4kb_erase_inst,
430            legacy_volatile_write_en_inst,
431            legacy_block_protect_volatile,
432            legacy_byte_write_granularity,
433            erase_insts,
434            timing: None,
435            page_size: None,
436            busy_poll_flag: None,
437            busy_poll_status: None,
438            reset_inst_f0: None,
439            reset_inst_66_99: None,
440            status_1_vol: None,
441        }
442    }
443
444    /// Parse JESD216A DWORDs 10 to 16.
445    fn read_jesd216a(&mut self, dwords: &[u32]) {
446        // 10th DWORD: erase instruction timings.
447        let erase_scale = bits!(dwords[9], 4, 0);
448        if let Some(inst) = self.erase_insts[0].as_mut() {
449            let typ = bits!(dwords[9], 7, 4);
450            let (typ, max) = Self::sector_erase_durations(typ, erase_scale);
451            inst.time_typ = Some(typ);
452            inst.time_max = Some(max);
453        }
454        if let Some(inst) = self.erase_insts[1].as_mut() {
455            let typ = bits!(dwords[9], 7, 11);
456            let (typ, max) = Self::sector_erase_durations(typ, erase_scale);
457            inst.time_typ = Some(typ);
458            inst.time_max = Some(max);
459        }
460        if let Some(inst) = self.erase_insts[2].as_mut() {
461            let typ = bits!(dwords[9], 7, 18);
462            let (typ, max) = Self::sector_erase_durations(typ, erase_scale);
463            inst.time_typ = Some(typ);
464            inst.time_max = Some(max);
465        }
466        if let Some(inst) = self.erase_insts[3].as_mut() {
467            let typ = bits!(dwords[9], 7, 25);
468            let (typ, max) = Self::sector_erase_durations(typ, erase_scale);
469            inst.time_typ = Some(typ);
470            inst.time_max = Some(max);
471        }
472
473        // 11th DWORD: chip erase and programming timings, page size.
474        let typ = bits!(dwords[10], 7, 24);
475        let (chip_erase_time_typ, chip_erase_time_max) =
476            Self::chip_erase_duration(typ, erase_scale);
477        let program_scale = bits!(dwords[10], 4, 0);
478        let typ = bits!(dwords[10], 5, 19);
479        let (succ_byte_prog_time_typ, succ_byte_prog_time_max) =
480            Self::byte_program_duration(typ, program_scale);
481        let typ = bits!(dwords[10], 5, 14);
482        let (first_byte_prog_time_typ, first_byte_prog_time_max) =
483            Self::byte_program_duration(typ, program_scale);
484        let typ = bits!(dwords[10], 6, 8);
485        let (page_prog_time_typ, page_prog_time_max) =
486            Self::page_program_duration(typ, program_scale);
487        self.timing = Some(SFDPTiming {
488            chip_erase_time_typ,
489            chip_erase_time_max,
490            first_byte_prog_time_typ,
491            first_byte_prog_time_max,
492            succ_byte_prog_time_typ,
493            succ_byte_prog_time_max,
494            page_prog_time_typ,
495            page_prog_time_max,
496        });
497        self.page_size = Some(1 << bits!(dwords[10], 4, 4));
498
499        // 12th and 13th DWORDs skipped: suspend/resume.
500
501        // 14th DWORD
502        let status_reg_poll = bits!(dwords[13], 6, 2);
503        self.busy_poll_flag = Some((status_reg_poll & 0b00_0010) != 0);
504        self.busy_poll_status = Some((status_reg_poll & 0b00_0001) != 0);
505
506        // 15th DWORD skipped: multiple I/O.
507
508        // 16th DWORD
509        let reset = bits!(dwords[15], 6, 8);
510        self.reset_inst_f0 = Some((reset & 0b00_1000) != 0);
511        self.reset_inst_66_99 = Some((reset & 0b01_0000) != 0);
512        let vol = bits!(dwords[15], 7, 0);
513        self.status_1_vol = Some(SFDPStatus1Volatility::from_bits(vol));
514    }
515
516    /// Convert SFPD sector erase time to typical and maximum Duration.
517    ///
518    /// Uses scale factors of 1ms, 16ms, 128ms, and 1s.
519    fn sector_erase_durations(typ: u32, max_scale: u32) -> (Duration, Duration) {
520        let scale = match bits!(typ, 2, 5) {
521            0b00 => Duration::from_millis(1),
522            0b01 => Duration::from_millis(16),
523            0b10 => Duration::from_millis(128),
524            0b11 => Duration::from_secs(1),
525            _ => unreachable!(),
526        };
527        let count = bits!(typ, 5, 0);
528        let typ = (count + 1) * scale;
529        let max = 2 * (max_scale + 1) * typ;
530        (typ, max)
531    }
532
533    /// Convert SFPD chip erase time to typical and maximum Durations.
534    ///
535    /// Uses scale factors of 16ms, 256ms, 4s, and 64s.
536    fn chip_erase_duration(typ: u32, max_scale: u32) -> (Duration, Duration) {
537        let scale = match bits!(typ, 2, 5) {
538            0b00 => Duration::from_millis(16),
539            0b01 => Duration::from_millis(256),
540            0b10 => Duration::from_secs(4),
541            0b11 => Duration::from_secs(64),
542            _ => unreachable!(),
543        };
544        let count = bits!(typ, 5, 0);
545        let typ = (count + 1) * scale;
546        let max = 2 * (max_scale + 1) * typ;
547        (typ, max)
548    }
549
550    /// Convert SFPD byte program times to typical and maximum Durations.
551    ///
552    /// Uses scale factors of 1µs and 8µs.
553    fn byte_program_duration(typ: u32, max_scale: u32) -> (Duration, Duration) {
554        let scale = match bits!(typ, 1, 4) {
555            0b0 => Duration::from_micros(1),
556            0b1 => Duration::from_micros(8),
557            _ => unreachable!(),
558        };
559        let count = bits!(typ, 4, 0);
560        let typ = (count + 1) * scale;
561        let max = 2 * (max_scale + 1) * typ;
562        (typ, max)
563    }
564
565    /// Convert SFPD page program times to typical and maximum Durations.
566    ///
567    /// Uses scale factors of 8µs and 64µs.
568    fn page_program_duration(typ: u32, max_scale: u32) -> (Duration, Duration) {
569        let scale = match bits!(typ, 1, 5) {
570            0b0 => Duration::from_micros(8),
571            0b1 => Duration::from_micros(64),
572            _ => unreachable!(),
573        };
574        let count = bits!(typ, 5, 0);
575        let typ = (count + 1) * scale;
576        let max = 2 * (max_scale + 1) * typ;
577        (typ, max)
578    }
579}
580
581impl core::fmt::Display for FlashParams {
582    fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result {
583        writeln!(
584            f,
585            "SFDP JEDEC Basic Flash Parameter Table v{}.{}",
586            self.version_major, self.version_minor
587        )?;
588        writeln!(
589            f,
590            "  Density: {} bits ({} KiB)",
591            self.density,
592            self.capacity_bytes() / 1024
593        )?;
594        writeln!(f, "  Address bytes: {:?}", self.address_bytes)?;
595        writeln!(f, "  Legacy information:")?;
596        writeln!(
597            f,
598            "    4kB erase supported: {}",
599            self.legacy_4kb_erase_supported
600        )?;
601        writeln!(
602            f,
603            "    4kB erase opcode: 0x{:02X}",
604            self.legacy_4kb_erase_inst
605        )?;
606        writeln!(
607            f,
608            "    Block Protect always volatile: {}",
609            self.legacy_block_protect_volatile
610        )?;
611        writeln!(
612            f,
613            "    Volatile write enable opcode: 0x{:02X}",
614            self.legacy_volatile_write_en_inst
615        )?;
616        writeln!(
617            f,
618            "    Writes have byte granularity: {}",
619            self.legacy_byte_write_granularity
620        )?;
621        writeln!(f, "  Erase instructions:")?;
622        for i in 0..4 {
623            if let Some(inst) = self.erase_insts[i] {
624                writeln!(f, "    {}: {}", i + 1, inst)?;
625            } else {
626                writeln!(f, "    {}: Not present", i + 1)?;
627            }
628        }
629        if let Some(timing) = self.timing {
630            writeln!(f, "  Timing:")?;
631            writeln!(
632                f,
633                "    Chip erase: typ {:?}, max {:?}",
634                timing.chip_erase_time_typ, timing.chip_erase_time_max
635            )?;
636            writeln!(
637                f,
638                "    First byte program: typ {:?}, max {:?}",
639                timing.first_byte_prog_time_typ, timing.first_byte_prog_time_max
640            )?;
641            writeln!(
642                f,
643                "    Subsequent byte program: typ {:?}, max {:?}",
644                timing.succ_byte_prog_time_typ, timing.succ_byte_prog_time_max
645            )?;
646            writeln!(
647                f,
648                "    Page program: typ {:?}, max {:?}",
649                timing.page_prog_time_typ, timing.page_prog_time_max
650            )?;
651        }
652        if let Some(page_size) = self.page_size {
653            writeln!(f, "  Page size: {} bytes", page_size)?;
654        }
655        if let Some(busy_poll_flag) = self.busy_poll_flag {
656            writeln!(f, "  Poll busy from FSR: {}", busy_poll_flag)?;
657        }
658        if let Some(busy_poll_status) = self.busy_poll_status {
659            writeln!(f, "  Poll busy from SR1: {}", busy_poll_status)?;
660        }
661        if let Some(reset_inst_f0) = self.reset_inst_f0 {
662            writeln!(f, "  Reset using opcode 0xF0: {}", reset_inst_f0)?;
663        }
664        if let Some(reset_inst_66_99) = self.reset_inst_66_99 {
665            writeln!(f, "  Reset using opcodes 0x66, 0x99: {}", reset_inst_66_99)?;
666        }
667        if let Some(status_1_vol) = self.status_1_vol {
668            writeln!(f, "  Status register 1 volatility: {:?}", status_1_vol)?;
669        }
670        Ok(())
671    }
672}