chunked read #47

Merged
muellerr merged 1 commits from chunked-read into main 2026-02-26 17:19:40 +01:00
10 changed files with 278 additions and 113 deletions
+15 -5
View File
@@ -2,6 +2,7 @@
#![no_main]
use aarch32_cpu::asm::nop;
use arbitrary_int::{traits::Integer as _, u2};
use core::panic::PanicInfo;
use embassy_executor::Spawner;
use embassy_time::{Duration, Ticker};
@@ -90,7 +91,14 @@ async fn main(_spawner: Spawner) -> ! {
let qspi_io_mode = qspi.into_io_mode(false);
let mut spansion_qspi = qspi_spansion::QspiSpansionS25Fl256SIoMode::new(qspi_io_mode, true);
let mut spansion_qspi = qspi_spansion::QspiSpansionS25Fl256SIoMode::new(
qspi_io_mode,
qspi_spansion::Config {
set_quad_bit_if_necessary: true,
latency_config: Some(u2::ZERO),
clear_write_protection: true,
},
);
let rdid = spansion_qspi.read_rdid_extended();
info!(
@@ -103,12 +111,14 @@ async fn main(_spawner: Spawner) -> ! {
);
let cr1 = spansion_qspi.read_configuration_register();
info!("QSPI Configuration Register 1: {:?}", cr1);
let sr = spansion_qspi.read_status_register_1();
info!("QSPI Status Register: {:?}", sr);
let mut write_buf: [u8; u8::MAX as usize + 1] = [0x0; u8::MAX as usize + 1];
let mut write_buf: [u8; 100 * qspi_spansion::PAGE_SIZE] = [0x0; 100 * qspi_spansion::PAGE_SIZE];
for (idx, byte) in write_buf.iter_mut().enumerate() {
*byte = idx as u8;
*byte = (idx % u8::MAX as usize) as u8;
}
let mut read_buf = [0u8; 256];
let mut read_buf = [0u8; 100 * qspi_spansion::PAGE_SIZE];
if ERASE_PROGRAM_READ_TEST {
info!("performing erase, program, read test");
@@ -120,7 +130,7 @@ async fn main(_spawner: Spawner) -> ! {
assert_eq!(*read, 0xFF);
}
read_buf.fill(0);
spansion_qspi.program_page(0x10000, &write_buf).unwrap();
spansion_qspi.write_pages(0x10000, &write_buf).unwrap();
spansion_qspi.read_page_fast_read(0x10000, &mut read_buf, true);
for (read, written) in read_buf.iter().zip(write_buf.iter()) {
assert_eq!(read, written);
+9
View File
@@ -8,6 +8,15 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
# [unreleased]
## Fixed
- QSPI robustness fixes. Read and fast-read operations are now chunked according to the 252 byte
limit specified in the TRM.
## Added
- QSPI constructor can now optionally clear block protection and set latency configuration.
# [v0.1.0]
Initial release
+213 -101
View File
@@ -2,8 +2,8 @@ use core::cell::RefCell;
use arbitrary_int::{prelude::*, u24};
use zynq7000_hal::qspi::{
FIFO_DEPTH, LinearQspiConfig, QspiIoMode, QspiIoTransferGuard, QspiLinearAddressing,
QspiLinearReadGuard,
FIFO_DEPTH, LinearQspiConfig, MAX_BYTES_PER_TRANSFER_IO_MODE, QspiIoMode, QspiIoTransferGuard,
QspiLinearAddressing, QspiLinearReadGuard,
};
pub const QSPI_DEV_COMBINATION_REV_F: zynq7000_hal::qspi::QspiDeviceCombination =
@@ -64,7 +64,8 @@ pub enum SectorArchictecture {
Hybrid = 0x01,
}
pub const PAGE_SIZE: usize = 256;
pub const PAGE_SIZE: usize = 0x100;
pub const SECTOR_SIZE: usize = 0x10000;
#[derive(Debug, Clone, Copy)]
pub struct BaseDeviceId {
@@ -159,8 +160,7 @@ impl ExtendedDeviceId {
}
}
#[bitbybit::bitfield(u8)]
#[derive(Debug)]
#[bitbybit::bitfield(u8, debug)]
pub struct StatusRegister1 {
#[bit(7, rw)]
status_register_write_disable: bool,
@@ -168,25 +168,18 @@ pub struct StatusRegister1 {
programming_error: bool,
#[bit(5, r)]
erase_error: bool,
#[bit(4, r)]
bp_2: bool,
#[bit(3, r)]
bp_1: bool,
#[bit(2, r)]
bp_0: bool,
#[bits(2..=4, rw)]
block_protection: u3,
#[bit(1, r)]
write_enable_latch: bool,
#[bit(0, r)]
write_in_progress: bool,
}
#[bitbybit::bitfield(u8)]
#[derive(Debug)]
#[bitbybit::bitfield(u8, debug)]
pub struct ConfigRegister1 {
#[bit(7, rw)]
latency_code_1: bool,
#[bit(6, rw)]
latency_code_0: bool,
#[bits(6..=7, rw)]
latency_code: u2,
/// This is an OTP bit. It can not be set back to 0 once it has been set to 1!
#[bit(5, rw)]
tbprot: bool,
@@ -227,23 +220,52 @@ pub enum ProgramPageError {
DataLargerThanPage,
}
#[derive(Default, Debug, PartialEq, Eq)]
pub struct Config {
pub set_quad_bit_if_necessary: bool,
pub latency_config: Option<u2>,
pub clear_write_protection: bool,
}
impl Config {
pub fn sr_or_cr_update_possibly_required(&self) -> bool {
self.set_quad_bit_if_necessary
|| self.latency_config.is_some()
|| self.clear_write_protection
}
}
pub struct QspiSpansionS25Fl256SIoMode(RefCell<QspiIoMode>);
impl QspiSpansionS25Fl256SIoMode {
pub fn new(qspi: QspiIoMode, set_quad_bit_if_necessary: bool) -> Self {
pub fn new(qspi: QspiIoMode, config: Config) -> Self {
let mut spansion_qspi = QspiSpansionS25Fl256SIoMode(RefCell::new(qspi));
if set_quad_bit_if_necessary {
spansion_qspi.clear_status();
let mut write_required = false;
if config.sr_or_cr_update_possibly_required() {
let mut cr1 = spansion_qspi.read_configuration_register();
if cr1.quad() {
// Quad bit is already set.
return spansion_qspi;
if config.set_quad_bit_if_necessary && !cr1.quad() {
cr1.set_quad(true);
write_required = true;
}
if let Some(latency_config) = config.latency_config
&& cr1.latency_code() != latency_config
{
cr1.set_latency_code(latency_config);
write_required = true;
}
cr1.set_quad(true);
// Preserve the status register by reading it first.
let sr1 = spansion_qspi.read_status_register_1();
// Safety: Only the QUAD bit was set while all other bits are preserved.
unsafe {
spansion_qspi.write_status_and_config_register(sr1, cr1);
let mut sr1 = spansion_qspi.read_status_register_1();
if config.clear_write_protection && sr1.block_protection() != u3::ZERO {
sr1.set_status_register_write_disable(false);
sr1.set_block_protection(u3::ZERO);
write_required = true;
}
if write_required {
// Safety: Only the QUAD bit was set while all other bits are preserved.
unsafe {
spansion_qspi.write_status_and_config_register(sr1, cr1);
}
}
}
spansion_qspi
@@ -257,6 +279,16 @@ impl QspiSpansionS25Fl256SIoMode {
QspiSpansionS25Fl256SLinearMode(qspi)
}
pub fn set_write_protection(&mut self, write_protection: u3) {
unsafe {
self.modify_status_and_config_register(|mut sr, cr| {
sr.set_status_register_write_disable(false);
sr.set_block_protection(write_protection);
(sr, cr)
});
}
}
pub fn write_enable(&mut self) {
let qspi = self.0.get_mut();
let mut transfer = qspi.transfer_guard();
@@ -301,7 +333,38 @@ impl QspiSpansionS25Fl256SIoMode {
/// # Safety
///
/// Misuse of this API does not lead to undefined behavior. However, it writes the
/// configuration register, which as OTP bits. Changing these bits from 0 to 1 is an
/// configuration register, which is OTP bits. Changing these bits from 0 to 1 is an
/// irreversible operation.
pub unsafe fn modify_status_and_config_register(
&mut self,
f: impl FnOnce(StatusRegister1, ConfigRegister1) -> (StatusRegister1, ConfigRegister1),
) {
self.write_enable();
let mut qspi = self.0.borrow_mut();
let mut transfer = qspi.transfer_guard();
let sr1 = self.read_status_register_1();
let cr1 = self.read_configuration_register();
let (sr1, cr1) = f(sr1, cr1);
transfer.write_word_txd_11(u32::from_ne_bytes([
RegisterId::WriteRegisters as u8,
sr1.raw_value(),
cr1.raw_value(),
0x00,
]));
transfer.start();
while !transfer.read_status().rx_above_threshold() {}
transfer.read_rx_data();
}
/// Write a new value for the status register. It is strongly recommended to read both
/// the status and config register first and preserve all unchanged bits.
///
/// This API must be used if the QUAD bit (CR1\[1\]) is set.
///
/// # Safety
///
/// Misuse of this API does not lead to undefined behavior. However, it writes the
/// configuration register, which is OTP bits. Changing these bits from 0 to 1 is an
/// irreversible operation.
pub unsafe fn write_status_and_config_register(
&mut self,
@@ -388,10 +451,10 @@ impl QspiSpansionS25Fl256SIoMode {
/// This function will block until the operation has completed.
pub fn erase_sector(&mut self, addr: u32) -> Result<(), EraseError> {
if addr + 0x10000 > u24::MAX.as_u32() {
if addr + SECTOR_SIZE as u32 > u24::MAX.as_u32() {
return Err(AddrError::OutOfRange.into());
}
if !addr.is_multiple_of(0x10000) {
if !addr.is_multiple_of(SECTOR_SIZE as u32) {
return Err(AddrError::Alignment.into());
}
self.write_enable();
@@ -429,6 +492,20 @@ impl QspiSpansionS25Fl256SIoMode {
}
}
pub fn write_pages(&mut self, mut addr: u32, data: &[u8]) -> Result<(), ProgramPageError> {
if addr + data.len() as u32 > u24::MAX.as_u32() {
return Err(AddrError::OutOfRange.into());
}
if !addr.is_multiple_of(PAGE_SIZE as u32) {
return Err(AddrError::Alignment.into());
}
for chunk in data.chunks(PAGE_SIZE) {
self.program_page(addr, chunk)?;
addr += PAGE_SIZE as u32;
}
Ok(())
}
/// This function also takes care of enabling writes before programming the page.
/// This function will block until the operation has completed.
///
@@ -437,7 +514,7 @@ impl QspiSpansionS25Fl256SIoMode {
if addr + data.len() as u32 > u24::MAX.as_u32() {
return Err(AddrError::OutOfRange.into());
}
if !addr.is_multiple_of(0x100) {
if !addr.is_multiple_of(PAGE_SIZE as u32) {
return Err(AddrError::Alignment.into());
}
if data.len() > PAGE_SIZE {
@@ -534,89 +611,122 @@ impl QspiSpansionS25Fl256SIoMode {
}
fn generic_read_page(&self, addr: u32, buf: &mut [u8], dummy_byte: bool, fast_read: bool) {
let mut qspi = self.0.borrow_mut();
let mut transfer = qspi.transfer_guard();
let mut offset = 0;
let reg_id = if fast_read {
RegisterId::FastRead
} else {
RegisterId::Read
};
let raw_word: [u8; 4] = [
reg_id as u8,
((addr >> 16) & 0xff) as u8,
((addr >> 8) & 0xff) as u8,
(addr & 0xff) as u8,
];
transfer.write_word_txd_00(u32::from_ne_bytes(raw_word));
let mut read_index = 0;
let mut written_words = 0;
let mut bytes_to_write = buf.len();
let mut qspi = self.0.borrow_mut();
let mut max_chunk_size = MAX_BYTES_PER_TRANSFER_IO_MODE - 4;
if dummy_byte {
bytes_to_write += 1;
}
let fifo_writes = bytes_to_write.div_ceil(4);
// Fill the FIFO until it is full or all 0 bytes have been written.
for _ in 0..core::cmp::min(fifo_writes, FIFO_DEPTH - 1) {
transfer.write_word_txd_00(0);
written_words += 1;
max_chunk_size -= 1;
}
transfer.start();
let mut reply_word_index = 0;
while offset < buf.len() {
// Calculate the size of the current chunk (max 248 bytes)
let chunk_size = core::cmp::min(max_chunk_size, buf.len() - offset);
let current_addr = addr + offset as u32;
while read_index < buf.len() {
let rx_is_above_threshold = transfer.read_status().rx_above_threshold();
// Create a mutable slice for the current chunk
let chunk_slice = &mut buf[offset..offset + chunk_size];
// See p.374 of the TRM: Do a double read to ensure this is correct information.
if rx_is_above_threshold && transfer.read_status().rx_above_threshold() {
let reply = transfer.read_rx_data();
if reply_word_index == 0 {
reply_word_index += 1;
continue;
// This ensures the hardware transaction (Chip Select, etc.) restarts for each chunk.
{
let mut transfer = qspi.transfer_guard();
let raw_word: [u8; 4] = [
reg_id as u8,
((current_addr >> 16) & 0xff) as u8,
((current_addr >> 8) & 0xff) as u8,
(current_addr & 0xff) as u8,
];
transfer.write_word_txd_00(u32::from_ne_bytes(raw_word));
let mut read_index = 0;
let mut written_words = 0;
// Use chunk_size instead of the full buffer length
let mut bytes_to_write = chunk_size;
if dummy_byte {
bytes_to_write += 1;
}
let reply_as_bytes = reply.to_ne_bytes();
let reply_size = core::cmp::min(buf.len() - read_index, 4);
read_index += match (reply_size, reply_word_index == 1 && dummy_byte) {
(1, false) => {
buf[read_index] = reply_as_bytes[0];
1
let fifo_writes = bytes_to_write.div_ceil(4);
// Fill the FIFO until it is full or all 0 bytes have been written.
for _ in 0..core::cmp::min(fifo_writes, FIFO_DEPTH - 1) {
transfer.write_word_txd_00(0);
written_words += 1;
}
transfer.start();
let mut reply_word_index = 0;
// Loop based on the current chunk's size
while read_index < chunk_size {
let rx_is_above_threshold = transfer.read_status().rx_above_threshold();
// See p.374 of the TRM: Do a double read to ensure this is correct information.
if rx_is_above_threshold && transfer.read_status().rx_above_threshold() {
let reply = transfer.read_rx_data();
if reply_word_index == 0 {
reply_word_index += 1;
continue;
}
let reply_as_bytes = reply.to_ne_bytes();
// Calculate remaining bytes in this specific chunk
let reply_size = core::cmp::min(chunk_size - read_index, 4);
read_index += match (reply_size, reply_word_index == 1 && dummy_byte) {
(1, false) => {
chunk_slice[read_index] = reply_as_bytes[0];
1
}
(1, true) => {
chunk_slice[read_index] = reply_as_bytes[1];
1
}
(2, false) => {
chunk_slice[read_index..read_index + 2]
.copy_from_slice(&reply_as_bytes[0..2]);
2
}
(2, true) => {
chunk_slice[read_index..read_index + 2]
.copy_from_slice(&reply_as_bytes[1..3]);
2
}
(3, false) => {
chunk_slice[read_index..read_index + 3]
.copy_from_slice(&reply_as_bytes[0..3]);
3
}
(3, true) => {
chunk_slice[read_index..read_index + 3]
.copy_from_slice(&reply_as_bytes[1..4]);
3
}
(4, false) => {
chunk_slice[read_index..read_index + 4]
.copy_from_slice(&reply_as_bytes[0..4]);
4
}
(4, true) => {
chunk_slice[read_index..read_index + 3]
.copy_from_slice(&reply_as_bytes[1..4]);
3
}
_ => unreachable!(),
};
reply_word_index += 1;
}
(1, true) => {
buf[read_index] = reply_as_bytes[1];
1
if written_words < fifo_writes && !transfer.read_status().tx_full() {
transfer.write_word_txd_00(0);
written_words += 1;
}
(2, false) => {
buf[read_index..read_index + 2].copy_from_slice(&reply_as_bytes[0..2]);
2
}
(2, true) => {
buf[read_index..read_index + 2].copy_from_slice(&reply_as_bytes[1..3]);
2
}
(3, false) => {
buf[read_index..read_index + 3].copy_from_slice(&reply_as_bytes[0..3]);
3
}
(3, true) => {
buf[read_index..read_index + 3].copy_from_slice(&reply_as_bytes[1..4]);
3
}
(4, false) => {
buf[read_index..read_index + 4].copy_from_slice(&reply_as_bytes[0..4]);
4
}
(4, true) => {
buf[read_index..read_index + 3].copy_from_slice(&reply_as_bytes[1..4]);
3
}
_ => unreachable!(),
};
reply_word_index += 1;
}
if written_words < fifo_writes && !transfer.read_status().tx_full() {
transfer.write_word_txd_00(0);
written_words += 1;
}
}
offset += chunk_size;
}
}
@@ -635,10 +745,12 @@ pub struct QspiSpansionS25Fl256SLinearMode(QspiLinearAddressing);
impl QspiSpansionS25Fl256SLinearMode {
pub const BASE_ADDR: usize = QspiLinearAddressing::BASE_ADDRESS;
pub const PAGE_SIZE: usize = PAGE_SIZE;
pub const SECTOR_SIZE: usize = SECTOR_SIZE;
pub fn into_io_mode(self, dual_flash: bool) -> QspiSpansionS25Fl256SIoMode {
let qspi = self.0.into_io_mode(dual_flash);
QspiSpansionS25Fl256SIoMode::new(qspi, false)
QspiSpansionS25Fl256SIoMode::new(qspi, Config::default())
}
pub fn read_guard(&mut self) -> QspiLinearReadGuard<'_> {
+11 -3
View File
@@ -8,9 +8,10 @@
#![no_std]
#![no_main]
use arbitrary_int::u6;
use core::panic::PanicInfo;
use aarch32_cpu::asm::nop;
use arbitrary_int::traits::Integer as _;
use arbitrary_int::{u2, u6};
use core::panic::PanicInfo;
use embedded_io::Write as _;
use log::{error, info};
use zedboard_bsp::qspi_spansion::{self, QspiSpansionS25Fl256SLinearMode};
@@ -162,7 +163,14 @@ fn main() -> ! {
);
let qspi_io_mode = qspi.into_io_mode(false);
let spansion_qspi = qspi_spansion::QspiSpansionS25Fl256SIoMode::new(qspi_io_mode, true);
let spansion_qspi = qspi_spansion::QspiSpansionS25Fl256SIoMode::new(
qspi_io_mode,
qspi_spansion::Config {
set_quad_bit_if_necessary: true,
latency_config: Some(u2::ZERO),
clear_write_protection: true,
},
);
let spansion_lqspi =
spansion_qspi.into_linear_addressed(qspi_spansion::QSPI_DEV_COMBINATION_REV_F.into());
qspi_boot(spansion_lqspi, priv_tim);
@@ -0,0 +1 @@
/boot.bin
@@ -12,5 +12,6 @@ zynq7000-boot-image = { path = "../../host/zynq7000-boot-image" }
zedboard-bsp = { path = "../zedboard-bsp" }
embedded-io = "0.7"
embedded-hal = "1"
arbitrary-int = "2"
log = "0.4"
libm = "0.2"
+9 -1
View File
@@ -3,4 +3,12 @@ Zedboard QSPI flasher
This application flashes a boot binary generated by the AMD `bootgen` utility from DDR
to the Zedboard QSPI. This project contains a `qspi-flasher.tcl` script which can be invoked
with `xsct` to flash a `boot.bin` and the QSPI flasher to DDR adn then run the application.
with `xsct` to flash a `boot.bin` and the QSPI flasher to DDR and then run the application.
The main `justfile` provides a convenience runner:
```sh
just flash-nor-zedboard <path to my boot.bin>
```
Please note that `xsct` must be callable for this to be usable.
+11 -3
View File
@@ -4,6 +4,7 @@
#![no_main]
use aarch32_cpu::asm::nop;
use arbitrary_int::{traits::Integer as _, u2};
use core::panic::PanicInfo;
use embedded_hal::{delay::DelayNs as _, digital::StatefulOutputPin as _};
use embedded_io::Write as _;
@@ -97,7 +98,14 @@ fn main() -> ! {
let qspi_io_mode = qspi.into_io_mode(false);
let mut spansion_qspi = qspi_spansion::QspiSpansionS25Fl256SIoMode::new(qspi_io_mode, true);
let mut spansion_qspi = qspi_spansion::QspiSpansionS25Fl256SIoMode::new(
qspi_io_mode,
qspi_spansion::Config {
set_quad_bit_if_necessary: true,
latency_config: Some(u2::ZERO),
clear_write_protection: true,
},
);
let mut boot_bin_slice = unsafe {
core::slice::from_raw_parts(BOOT_BIN_BASE_ADDR as *const _, BootHeader::FIXED_SIZED_PART)
@@ -121,7 +129,7 @@ fn main() -> ! {
);
let mut current_addr = 0;
let mut read_buf = [0u8; 256];
let mut read_buf = [0u8; qspi_spansion::PAGE_SIZE];
let mut next_checkpoint = 0.05;
while current_addr < boot_bin_size {
if current_addr % 0x10000 == 0 {
@@ -137,7 +145,7 @@ fn main() -> ! {
}
}
}
let write_size = core::cmp::min(256, boot_bin_size - current_addr);
let write_size = core::cmp::min(qspi_spansion::PAGE_SIZE, boot_bin_size - current_addr);
let write_slice = &boot_bin_slice[current_addr..current_addr + write_size];
log::debug!("Programming address {:#x}", current_addr);
match spansion_qspi.program_page(current_addr as u32, write_slice) {
+5
View File
@@ -35,6 +35,11 @@ pub(crate) mod lqspi_configs;
pub const QSPI_MUX_CONFIG: MuxConfig = MuxConfig::new_with_l0();
pub const FIFO_DEPTH: usize = 63;
/// From the TRM:
///
/// > The maximum number of bytes per command sequence in this mode is limited by the depth of the
/// > TxFIFO of 252 bytes.
pub const MAX_BYTES_PER_TRANSFER_IO_MODE: usize = FIFO_DEPTH * 4;
/// In linear-addressed mode, the QSPI is memory-mapped, with the address starting here.
pub const QSPI_START_ADDRESS: usize = 0xFC00_0000;
+3
View File
@@ -57,3 +57,6 @@ run binary:
# Run the GDB debugger in GUI mode.
gdb-multiarch -q -x {{justfile_directory()}}/firmware/gdb.gdb {{binary}} -tui
flash-nor-zedboard boot_binary:
xsct firmware/zedboard-qspi-flasher/qspi-flasher.tcl scripts/ps7_init.tcl -b {{boot_binary}}