Introduce Rust FSBL
ci / Check build (pull_request) Has been cancelled
ci / Check formatting (pull_request) Has been cancelled
ci / Check Documentation Build (pull_request) Has been cancelled
ci / Clippy (pull_request) Has been cancelled
ci / Check build (push) Has been cancelled
ci / Check formatting (push) Has been cancelled
ci / Check Documentation Build (push) Has been cancelled
ci / Clippy (push) Has been cancelled

This PR introduces some major features while also changing the project structure to be more flexible
for multiple platforms (e.g. host tooling). It also includes a lot of
bugfixes, renamings for consistency purposes and dependency updates.

Added features:

1. Pure Rust FSBL for the Zedboard. This first variant is simplistic. It
   is currently only capable of QSPI boot. It searches for a bitstream
   and ELF file inside the boot binary, flashes them and jumps to them.
2. QSPI flasher for the Zedboard.
3. DDR, QSPI, DEVC, private CPU timer and PLL configuration modules
3. Tooling to auto-generate board specific DDR and DDRIOB config
   parameters from the vendor provided ps7init.tcl file

Changed project structure:

1. All target specific project are inside a dedicated workspace inside
   the `zynq` folder now.
2. All tool intended to be run on a host are inside a `tools` workspace
3. All other common projects are at the project root

Major bugfixes:

1. SPI module: CPOL was not configured properly
2. Logger flush implementation was empty, implemented properly now.
This commit is contained in:
2025-08-01 14:32:08 +02:00
committed by Robin Mueller
parent 0cf5bf6885
commit 5d0f2837d1
166 changed files with 9496 additions and 979 deletions
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "zedboard-bsp"
version = "0.1.0"
edition = "2024"
[dependencies]
zynq7000 = { path = "../zynq7000" }
zynq7000-hal = { path = "../zynq7000-hal" }
bitbybit = "1.4"
arbitrary-int = "2"
num_enum = { version = "0.7", default-features = false }
thiserror = { version = "2", default-features = false }
@@ -0,0 +1,100 @@
#![doc = r"This file was auto-generated by the [zynq7000-ps7init-extract](https://egit.irs.uni-stuttgart.de/rust/zynq7000-rs/src/branch/main/tools/zynq7000-ps7init-extract) program."]
#![doc = r""]
#![doc = r"This configuration file contains static DDR configuration parameters extracted from the"]
#![doc = r"AMD ps7init.tcl file"]
use zynq7000::ddrc::regs;
use zynq7000_hal::ddr::DdrcConfigSet;
pub const DDRC_CONFIG_ZEDBOARD: DdrcConfigSet = DdrcConfigSet {
ctrl: regs::DdrcControl::new_with_raw_value(0x00000080),
two_rank: regs::TwoRankConfig::new_with_raw_value(0x00001082),
hpr: regs::LprHprQueueControl::new_with_raw_value(0x03c0780f),
lpr: regs::LprHprQueueControl::new_with_raw_value(0x02001001),
wr: regs::WriteQueueControl::new_with_raw_value(0x00014001),
dram_param_0: regs::DramParamReg0::new_with_raw_value(0x0004159b),
dram_param_1: regs::DramParamReg1::new_with_raw_value(0x44e458d3),
dram_param_2: regs::DramParamReg2::new_with_raw_value(0x7282bce5),
dram_param_3: regs::DramParamReg3::new_with_raw_value(0x270872d0),
dram_param_4: regs::DramParamReg4::new_with_raw_value(0x00000000),
dram_init_param: regs::DramInitParam::new_with_raw_value(0x00002007),
dram_emr: regs::DramEmr::new_with_raw_value(0x00000008),
dram_emr_mr: regs::DramEmrMr::new_with_raw_value(0x00040b30),
dram_burst8_rdwr: regs::DramBurst8ReadWrite::new_with_raw_value(0x000116d4),
disable_dq: regs::DisableDq::new_with_raw_value(0x00000000),
dram_addr_map_bank: regs::DramAddrMapBank::new_with_raw_value(0x00000777),
dram_addr_map_col: regs::DramAddrMapColumn::new_with_raw_value(0xfff00000),
dram_addr_map_row: regs::DramAddrMapRow::new_with_raw_value(0x0ff66666),
dram_odt: regs::DramOdt::new_with_raw_value(0x0003c008),
phy_cmd_timeout_rddata_cpt: regs::PhyCmdTimeoutRdDataCpt::new_with_raw_value(0x77010800),
dll_calib: regs::DllCalib::new_with_raw_value(0x00000000),
odt_delay_hold: regs::OdtDelayHold::new_with_raw_value(0x00005003),
ctrl_reg1: regs::CtrlReg1::new_with_raw_value(0x0000003e),
ctrl_reg2: regs::CtrlReg2::new_with_raw_value(0x00020000),
ctrl_reg3: regs::CtrlReg3::new_with_raw_value(0x00284141),
ctrl_reg4: regs::CtrlReg4::new_with_raw_value(0x00001610),
ctrl_reg5: regs::CtrlReg5::new_with_raw_value(0x00466111),
ctrl_reg6: regs::CtrlReg6::new_with_raw_value(0x00032222),
che_t_zq: regs::CheTZq::new_with_raw_value(0x10200802),
che_t_zq_short_interval_reg: regs::CheTZqShortInterval::new_with_raw_value(0x10200802),
deep_powerdown: regs::DeepPowerdown::new_with_raw_value(0x000001fe),
reg_2c: regs::Reg2c::new_with_raw_value(0x1cffffff),
reg_2d: regs::Reg2d::new_with_raw_value(0x00000200),
dfi_timing: regs::DfiTiming::new_with_raw_value(0x00200066),
che_ecc_ctrl: regs::CheEccControl::new_with_raw_value(0x00000000),
ecc_scrub: regs::EccScrub::new_with_raw_value(0x00000008),
phy_receiver_enable: regs::PhyReceiverEnable::new_with_raw_value(0x00000000),
phy_config: [
regs::PhyConfig::new_with_raw_value(0x40000001),
regs::PhyConfig::new_with_raw_value(0x40000001),
regs::PhyConfig::new_with_raw_value(0x40000001),
regs::PhyConfig::new_with_raw_value(0x40000001),
],
phy_init_ratio: [
regs::PhyInitRatio::new_with_raw_value(0x00033c03),
regs::PhyInitRatio::new_with_raw_value(0x00034003),
regs::PhyInitRatio::new_with_raw_value(0x0002f400),
regs::PhyInitRatio::new_with_raw_value(0x00030400),
],
phy_rd_dqs_config: [
regs::PhyDqsConfig::new_with_raw_value(0x00000035),
regs::PhyDqsConfig::new_with_raw_value(0x00000035),
regs::PhyDqsConfig::new_with_raw_value(0x00000035),
regs::PhyDqsConfig::new_with_raw_value(0x00000035),
],
phy_wr_dqs_config: [
regs::PhyDqsConfig::new_with_raw_value(0x00000083),
regs::PhyDqsConfig::new_with_raw_value(0x00000083),
regs::PhyDqsConfig::new_with_raw_value(0x0000007f),
regs::PhyDqsConfig::new_with_raw_value(0x00000078),
],
phy_we_cfg: [
regs::PhyWriteEnableConfig::new_with_raw_value(0x00000124),
regs::PhyWriteEnableConfig::new_with_raw_value(0x00000125),
regs::PhyWriteEnableConfig::new_with_raw_value(0x00000112),
regs::PhyWriteEnableConfig::new_with_raw_value(0x00000116),
],
phy_wr_data_slv: [
regs::PhyWriteDataSlaveConfig::new_with_raw_value(0x000000c3),
regs::PhyWriteDataSlaveConfig::new_with_raw_value(0x000000c3),
regs::PhyWriteDataSlaveConfig::new_with_raw_value(0x000000bf),
regs::PhyWriteDataSlaveConfig::new_with_raw_value(0x000000b8),
],
reg64: regs::Reg64::new_with_raw_value(0x00040080),
reg65: regs::Reg65::new_with_raw_value(0x0001fc82),
page_mask: 0x00000000,
axi_priority_wr_port: [
regs::AxiPriorityWritePort::new_with_raw_value(0x000003ff),
regs::AxiPriorityWritePort::new_with_raw_value(0x000003ff),
regs::AxiPriorityWritePort::new_with_raw_value(0x000003ff),
regs::AxiPriorityWritePort::new_with_raw_value(0x000003ff),
],
axi_priority_rd_port: [
regs::AxiPriorityReadPort::new_with_raw_value(0x000003ff),
regs::AxiPriorityReadPort::new_with_raw_value(0x000003ff),
regs::AxiPriorityReadPort::new_with_raw_value(0x000003ff),
regs::AxiPriorityReadPort::new_with_raw_value(0x000003ff),
],
lpddr_ctrl_0: regs::LpddrControl0::new_with_raw_value(0x00000000),
lpddr_ctrl_1: regs::LpddrControl1::new_with_raw_value(0x00000000),
lpddr_ctrl_2: regs::LpddrControl2::new_with_raw_value(0x00005125),
lpddr_ctrl_3: regs::LpddrControl3::new_with_raw_value(0x000012a8),
};
@@ -0,0 +1,15 @@
#![doc = r"This file was auto-generated by the [zynq7000-ps7init-extract](https://egit.irs.uni-stuttgart.de/rust/zynq7000-rs/src/branch/main/tools/zynq7000-ps7init-extract) program."]
#![doc = r""]
#![doc = r"This configuration file contains static DDRIOB configuration parameters extracted from the"]
#![doc = r"AMD ps7init.tcl file"]
use zynq7000::ddrc::regs;
use zynq7000_hal::ddr::DdriobConfigSet;
pub const DDRIOB_CONFIG_SET_ZEDBOARD: DdriobConfigSet = DdriobConfigSet {
addr0: regs::DdriobConfig::new_with_raw_value(0x00000600),
addr1: regs::DdriobConfig::new_with_raw_value(0x00000600),
data0: regs::DdriobConfig::new_with_raw_value(0x00000672),
data1: regs::DdriobConfig::new_with_raw_value(0x00000672),
diff0: regs::DdriobConfig::new_with_raw_value(0x00000674),
diff1: regs::DdriobConfig::new_with_raw_value(0x00000674),
clock: regs::DdriobConfig::new_with_raw_value(0x00000600),
};
+6
View File
@@ -0,0 +1,6 @@
#![no_std]
pub mod ddrc_config_autogen;
pub mod ddriob_config_autogen;
pub mod phy_marvell;
pub mod qspi_spansion;
+246
View File
@@ -0,0 +1,246 @@
use arbitrary_int::{u2, u4, u5};
#[derive(Clone, Debug)]
pub struct PhyIdentifier {
pub oui: u32,
pub model: u8,
pub rev: u8,
}
// Organizational Unique Identifier for Marvell 88E1518 PHY
pub const MARVELL_88E1518_OUI: u32 = 0x005043;
pub const MARVELL_88E1518_MODELL_NUMBER: u8 = 0b011101;
#[bitbybit::bitenum(u5, exhaustive = false)]
pub enum MarvellRegistersPage0 {
CopperControl = 0,
CopperStatus = 1,
IdReg1 = 2,
IdReg2 = 3,
CopperSpecificStatus = 17,
PageSel = 22,
}
#[bitbybit::bitfield(u16)]
pub struct CopperControlRegister {
#[bit(15, rw)]
copper_reset: bool,
#[bit(14, rw)]
loopback: bool,
#[bit(12, rw)]
auto_negotiation_enable: bool,
#[bit(11, rw)]
power_down: bool,
#[bit(10, rw)]
isolate: bool,
#[bit(9, rw)]
restart_auto_negotiation: bool,
/// 1: Full-duplex, 0: Half-duplex
#[bit(8, rw)]
copper_duplex_mode: bool,
#[bits([13, 6], rw)]
speed_selection: u2,
}
#[bitbybit::bitenum(u1, exhaustive = true)]
#[derive(Debug, PartialEq, Eq)]
pub enum LatchingLinkStatus {
Up = 1,
DownSinceLastRead = 0,
}
#[bitbybit::bitfield(u16)]
pub struct CopperStatusRegister {
/// Always 0, the 100BASE-T4 protocol is not available on Marvell 88E15XX.
#[bit(15, r)]
p_100_base_t4: bool,
/// Always 1 for Marvell 88E15XX
#[bit(14, r)]
p_100_base_x_full_duplex: bool,
/// Always 1 for Marvell 88E15XX
#[bit(13, r)]
p_100_base_x_half_duplex: bool,
/// Always 1 for Marvell 88E15XX
#[bit(12, r)]
p_10_base_t_full_duplex: bool,
/// Always 1 for Marvell 88E15XX
#[bit(11, r)]
p_10_base_t_half_duplex: bool,
/// Always 0 for Marvell 88E15XX
#[bit(10, r)]
p_100_base_t2_full_duplex: bool,
/// Always 0 for Marvell 88E15XX
#[bit(9, r)]
p_100_base_t2_half_duplex: bool,
/// Always 1 for Marvell 88E15XX
#[bit(8, r)]
extended_status: bool,
/// Always 1 for Marvell 88E15XX
#[bit(6, r)]
mf_preamble_suppression: bool,
#[bit(5, r)]
auto_negotiation_complete: bool,
// Latching high register bit.
#[bit(4, r)]
copper_remote_fault: bool,
/// Always 1 for Marvell 88E15XX
#[bit(3, r)]
auto_negotation_ability: bool,
// Latching low register bit. For the current link status, this register should be read back
// to back, or the link real time register (17_0.10) should be read
#[bit(2, r)]
copper_link_status: LatchingLinkStatus,
// Latching high register bit.
#[bit(1, r)]
jabber_detect: bool,
/// Always 1 for Marvell 88E15XX
#[bit(0, r)]
extended_capabilities: bool,
}
#[bitbybit::bitenum(u2, exhaustive = true)]
#[derive(Debug, PartialEq, Eq)]
pub enum PhySpeedBits {
Reserved = 0b11,
Mbps1000 = 0b10,
Mbps100 = 0b01,
Mbps10 = 0b00,
}
impl PhySpeedBits {
#[inline]
pub fn as_zynq7000_eth_speed(&self) -> Option<zynq7000_hal::eth::Speed> {
match self {
PhySpeedBits::Reserved => None,
PhySpeedBits::Mbps1000 => Some(zynq7000_hal::eth::Speed::Mbps1000),
PhySpeedBits::Mbps100 => Some(zynq7000_hal::eth::Speed::Mbps100),
PhySpeedBits::Mbps10 => Some(zynq7000_hal::eth::Speed::Mbps10),
}
}
}
#[bitbybit::bitenum(u1, exhaustive = true)]
#[derive(Debug, PartialEq, Eq)]
pub enum PhyDuplexBit {
Full = 1,
Half = 0,
}
impl PhyDuplexBit {
#[inline]
pub fn as_zynq7000_eth_duplex(&self) -> zynq7000_hal::eth::Duplex {
match self {
PhyDuplexBit::Full => zynq7000_hal::eth::Duplex::Full,
PhyDuplexBit::Half => zynq7000_hal::eth::Duplex::Half,
}
}
}
#[bitbybit::bitfield(u16)]
pub struct CopperSpecificStatusRegister {
#[bits(14..=15, r)]
speed: PhySpeedBits,
#[bit(13, r)]
duplex: PhyDuplexBit,
/// Latching high register bit.
#[bit(12, r)]
page_received: bool,
/// This is 1 when auto-negotiation is not enabled.
#[bit(11, r)]
speed_and_duplex_resolved: bool,
/// This is the real-time link status.
#[bit(10, r)]
copper_link: bool,
#[bit(9, r)]
transmit_pause_enabled: bool,
#[bit(8, r)]
received_pause_enabled: bool,
#[bit(6, r)]
mdi_crossover_status: bool,
#[bit(4, r)]
copper_energy_detect_status: bool,
#[bit(3, r)]
global_link_status: bool,
#[bit(1, r)]
polarity: bool,
#[bit(0, r)]
jabber: bool,
}
pub struct Marvell88E1518Phy {
mdio: zynq7000_hal::eth::mdio::Mdio,
addr: u5,
}
impl Marvell88E1518Phy {
pub fn new_autoprobe_addr(mdio: &mut zynq7000_hal::eth::mdio::Mdio) -> Option<(Self, u4)> {
for addr in 0..32 {
let phy_id_1 =
mdio.read_blocking(u5::new(addr), MarvellRegistersPage0::IdReg1.raw_value());
let phy_id_2 =
mdio.read_blocking(u5::new(addr), MarvellRegistersPage0::IdReg2.raw_value());
// PHY ID 1 contains bits 3 to 18 of the OUI in the goofy IEEE ordering scheme,
// which corresponds to bit \[21:6\] of the OUI.
// PHY ID 2 contains bits 19 to 24 which correspond to bits \[5:0\] of the OUI.
let oui = ((phy_id_1 as u32) << 6) | ((phy_id_2 >> 10) & 0b111111) as u32;
let model_number = ((phy_id_2 >> 4) & 0b111111) as u8;
let revision_number = u4::new((phy_id_2 & 0b1111) as u8);
if oui == MARVELL_88E1518_OUI && model_number == MARVELL_88E1518_MODELL_NUMBER {
return Some((
Self {
mdio: unsafe { mdio.clone() },
addr: u5::new(addr),
},
revision_number,
));
}
}
None
}
pub fn new(mdio: zynq7000_hal::eth::mdio::Mdio, addr: u5) -> Self {
Self { mdio, addr }
}
pub fn reset(&mut self) {
let mut ctrl = CopperControlRegister::new_with_raw_value(
self.mdio
.read_blocking(self.addr, MarvellRegistersPage0::CopperControl.raw_value()),
);
ctrl.set_copper_reset(true);
self.mdio.write_blocking(
self.addr,
MarvellRegistersPage0::CopperControl.raw_value(),
ctrl.raw_value(),
);
}
pub fn restart_auto_negotiation(&mut self) {
let mut ctrl = CopperControlRegister::new_with_raw_value(
self.mdio
.read_blocking(self.addr, MarvellRegistersPage0::CopperControl.raw_value()),
);
ctrl.set_auto_negotiation_enable(true);
ctrl.set_restart_auto_negotiation(true);
self.mdio.write_blocking(
self.addr,
MarvellRegistersPage0::CopperControl.raw_value(),
ctrl.raw_value(),
);
}
pub fn read_copper_status(&mut self) -> CopperStatusRegister {
let raw_value = self
.mdio
.read_blocking(self.addr, MarvellRegistersPage0::CopperStatus.raw_value());
CopperStatusRegister::new_with_raw_value(raw_value)
}
pub fn read_copper_specific_status_register_1(&mut self) -> CopperSpecificStatusRegister {
let raw_value = self.mdio.read_blocking(
self.addr,
MarvellRegistersPage0::CopperSpecificStatus.raw_value(),
);
CopperSpecificStatusRegister::new_with_raw_value(raw_value)
}
}
+630
View File
@@ -0,0 +1,630 @@
use core::cell::RefCell;
use arbitrary_int::{prelude::*, u24};
use zynq7000_hal::qspi::{
FIFO_DEPTH, LinearQspiConfig, QspiIoMode, QspiIoTransferGuard, QspiLinearAddressing,
QspiLinearReadGuard,
};
pub const QSPI_DEV_COMBINATION_REV_F: zynq7000_hal::qspi::QspiDeviceCombination =
zynq7000_hal::qspi::QspiDeviceCombination {
vendor: zynq7000_hal::qspi::QspiVendor::WinbondAndSpansion,
operating_mode: zynq7000_hal::qspi::OperatingMode::FastReadQuadOutput,
two_devices: false,
};
#[derive(Debug, Clone, Copy)]
pub enum RegisterId {
/// WRR
WriteRegisters = 0x01,
/// PP
PageProgram = 0x02,
/// READ
Read = 0x03,
/// WRDI
WriteDisable = 0x04,
/// RDSR1
ReadStatus1 = 0x05,
/// RDSR2
ReadStatus2 = 0x07,
/// WREN
WriteEnable = 0x06,
/// FAST_READ
FastRead = 0x0B,
/// SE
SectorErase = 0xD8,
/// CSLR
ClearStatus = 0x30,
/// RDCR
ReadConfig = 0x35,
/// RDID
ReadId = 0x9F,
}
#[derive(Debug, Clone, Copy, num_enum::TryFromPrimitive)]
#[repr(u8)]
pub enum MemoryInterfaceType {
_128Mb = 0x20,
_256Mb = 0x02,
}
#[derive(Debug, Clone, Copy, num_enum::TryFromPrimitive)]
#[repr(u8)]
pub enum Density {
_128Mb = 0x18,
_256Mb = 0x19,
}
#[derive(Debug, Clone, Copy, num_enum::TryFromPrimitive)]
#[repr(u8)]
pub enum SectorArchictecture {
/// Uniform 256 kB sectors.
Uniform = 0x00,
/// 32 4kB sectors and 64 kB sectors.
Hybrid = 0x01,
}
pub const PAGE_SIZE: usize = 256;
#[derive(Debug, Clone, Copy)]
pub struct BaseDeviceId {
manufacturer_id: u8,
device_id: u16,
}
impl BaseDeviceId {
#[inline]
pub const fn new(manufacturer_id: u8, device_id: u16) -> Self {
BaseDeviceId {
manufacturer_id,
device_id,
}
}
#[inline]
pub const fn from_raw(raw: &[u8; 3]) -> Self {
BaseDeviceId::new(raw[0], ((raw[1] as u16) << 8) | raw[2] as u16)
}
#[inline]
pub const fn manufacturer_id(&self) -> u8 {
self.manufacturer_id
}
#[inline]
pub const fn device_id_raw(&self) -> u16 {
self.device_id
}
#[inline]
pub fn memory_interface_type(&self) -> Result<MemoryInterfaceType, u8> {
MemoryInterfaceType::try_from(((self.device_id >> 8) & 0xff) as u8).map_err(|e| e.number)
}
#[inline]
pub fn density(&self) -> Result<Density, u8> {
Density::try_from((self.device_id & 0xff) as u8).map_err(|e| e.number)
}
}
#[derive(Debug)]
pub struct ExtendedDeviceId {
base: BaseDeviceId,
id_cfi_len: u8,
sector_arch: u8,
family_id: u8,
model_number: [u8; 2],
}
impl ExtendedDeviceId {
pub const fn from_raw(raw: &[u8; 8]) -> Self {
ExtendedDeviceId {
base: BaseDeviceId::from_raw(&[raw[0], raw[1], raw[2]]),
id_cfi_len: raw[3],
sector_arch: raw[4],
family_id: raw[5],
model_number: [raw[6], raw[7]],
}
}
pub const fn id_cfi_len(&self) -> u8 {
self.id_cfi_len
}
pub const fn sector_arch_raw(&self) -> u8 {
self.sector_arch
}
pub fn sector_arch(&self) -> Result<SectorArchictecture, u8> {
SectorArchictecture::try_from(self.sector_arch_raw()).map_err(|e| e.number)
}
pub const fn family_id(&self) -> u8 {
self.family_id
}
pub const fn model_number_raw(&self) -> &[u8; 2] {
&self.model_number
}
pub const fn model_number(&self) -> [char; 2] {
[self.model_number[0] as char, self.model_number[1] as char]
}
}
impl ExtendedDeviceId {
#[inline]
pub fn base_id(&self) -> BaseDeviceId {
self.base
}
}
#[bitbybit::bitfield(u8)]
#[derive(Debug)]
pub struct StatusRegister1 {
#[bit(7, rw)]
status_register_write_disable: bool,
#[bit(6, r)]
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,
#[bit(1, r)]
write_enable_latch: bool,
#[bit(0, r)]
write_in_progress: bool,
}
#[bitbybit::bitfield(u8)]
#[derive(Debug)]
pub struct ConfigRegister1 {
#[bit(7, rw)]
latency_code_1: bool,
#[bit(6, rw)]
latency_code_0: bool,
/// 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,
#[bit(3, rw)]
bpnv: bool,
/// This is an OTP bit. It can not be set back to 0 once it has been set to 1!
#[bit(2, rw)]
tbparm: bool,
#[bit(1, rw)]
quad: bool,
#[bit(0, rw)]
freeze: bool,
}
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
pub enum AddrError {
#[error("address out of range")]
OutOfRange,
#[error("address not aligned")]
Alignment,
}
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
pub enum EraseError {
#[error("erase error bit set in status register")]
EraseErrorBitSet,
#[error("address error: {0}")]
Addr(#[from] AddrError),
}
#[derive(Debug, PartialEq, Eq, thiserror::Error)]
pub enum ProgramPageError {
#[error("programming error bit set in status register")]
ProgrammingErrorBitSet,
#[error("address error: {0}")]
Addr(#[from] AddrError),
#[error("data is larger than page size {PAGE_SIZE}")]
DataLargerThanPage,
}
pub struct QspiSpansionS25Fl256SIoMode(RefCell<QspiIoMode>);
impl QspiSpansionS25Fl256SIoMode {
pub fn new(qspi: QspiIoMode, set_quad_bit_if_necessary: bool) -> Self {
let mut spansion_qspi = QspiSpansionS25Fl256SIoMode(RefCell::new(qspi));
if set_quad_bit_if_necessary {
let mut cr1 = spansion_qspi.read_configuration_register();
if cr1.quad() {
// Quad bit is already set.
return spansion_qspi;
}
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);
}
}
spansion_qspi
}
pub fn into_linear_addressed(
self,
config: LinearQspiConfig,
) -> QspiSpansionS25Fl256SLinearMode {
let qspi = self.0.into_inner().into_linear_addressed(config);
QspiSpansionS25Fl256SLinearMode(qspi)
}
pub fn write_enable(&mut self) {
let qspi = self.0.get_mut();
let mut transfer = qspi.transfer_guard();
transfer.write_word_txd_01(RegisterId::WriteEnable as u32);
transfer.start();
while !transfer.read_status().rx_above_threshold() {}
transfer.read_rx_data();
}
pub fn write_disable(&mut self) {
let qspi = self.0.get_mut();
let mut transfer = qspi.transfer_guard();
transfer.write_word_txd_01(RegisterId::WriteDisable as u32);
transfer.start();
while !transfer.read_status().rx_above_threshold() {}
transfer.read_rx_data();
}
/// Write a new value for the status register.
///
/// This API may not be used if the QUAD bit (CR1\[1\]) is set.
pub fn write_status(&mut self, sr: StatusRegister1) {
self.write_enable();
let mut qspi = self.0.borrow_mut();
let mut transfer = qspi.transfer_guard();
transfer.write_word_txd_10(u32::from_ne_bytes([
RegisterId::WriteRegisters as u8,
sr.raw_value(),
0x00,
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 as OTP bits. Changing these bits from 0 to 1 is an
/// irreversible operation.
pub unsafe fn write_status_and_config_register(
&mut self,
sr: StatusRegister1,
cr: ConfigRegister1,
) {
self.write_enable();
let mut qspi = self.0.borrow_mut();
let mut transfer = qspi.transfer_guard();
transfer.write_word_txd_11(u32::from_ne_bytes([
RegisterId::WriteRegisters as u8,
sr.raw_value(),
cr.raw_value(),
0x00,
]));
transfer.start();
while !transfer.read_status().rx_above_threshold() {}
transfer.read_rx_data();
}
pub fn read_status_register_1(&self) -> StatusRegister1 {
let mut qspi = self.0.borrow_mut();
let mut transfer = qspi.transfer_guard();
transfer.write_word_txd_10(RegisterId::ReadStatus1 as u32);
transfer.start();
while !transfer.read_status().rx_above_threshold() {}
let reply = transfer.read_rx_data();
drop(transfer);
// little-endian architecture, so the second byte received is the MSB.
StatusRegister1::new_with_raw_value(((reply >> 24) & 0xff) as u8)
}
pub fn read_configuration_register(&self) -> ConfigRegister1 {
let mut qspi = self.0.borrow_mut();
let mut transfer = qspi.transfer_guard();
transfer.write_word_txd_10(RegisterId::ReadConfig as u32);
transfer.start();
while !transfer.read_status().rx_above_threshold() {}
let reply = transfer.read_rx_data();
drop(transfer);
// little-endian architecture, so the second byte received is the MSB.
ConfigRegister1::new_with_raw_value(((reply >> 24) & 0xff) as u8)
}
pub fn clear_status(&mut self) {
let qspi = self.0.get_mut();
let mut transfer = qspi.transfer_guard();
transfer.write_word_txd_01(RegisterId::ClearStatus as u32);
transfer.start();
while !transfer.read_status().rx_above_threshold() {}
transfer.read_rx_data();
}
pub fn read_rdid_base(&self) -> BaseDeviceId {
let mut qspi = self.0.borrow_mut();
let mut transfer = qspi.transfer_guard();
transfer.write_word_txd_00(RegisterId::ReadId as u32);
transfer.start();
while !transfer.read_status().rx_above_threshold() {}
let reply = transfer.read_rx_data();
drop(transfer);
BaseDeviceId::from_raw(reply.to_ne_bytes()[1..].try_into().unwrap())
}
pub fn read_rdid_extended(&self) -> ExtendedDeviceId {
let mut reply: [u8; 12] = [0; 12];
let mut qspi = self.0.borrow_mut();
let mut transfer = qspi.transfer_guard();
transfer.write_word_txd_00(RegisterId::ReadId as u32);
transfer.write_word_txd_00(0x00);
transfer.write_word_txd_00(0x00);
transfer.start();
let mut read_index = 0;
while read_index < 3 {
if transfer.read_status().rx_above_threshold() {
reply[read_index * 4..(read_index + 1) * 4]
.copy_from_slice(&transfer.read_rx_data().to_ne_bytes());
read_index += 1;
}
}
ExtendedDeviceId::from_raw(reply[1..9].try_into().unwrap())
}
/// 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() {
return Err(AddrError::OutOfRange.into());
}
if !addr.is_multiple_of(0x10000) {
return Err(AddrError::Alignment.into());
}
self.write_enable();
let qspi = self.0.get_mut();
let mut transfer = qspi.transfer_guard();
let raw_word: [u8; 4] = [
RegisterId::SectorErase 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));
transfer.start();
// Finish transfer
while !transfer.read_status().rx_above_threshold() {}
transfer.read_rx_data();
// Drive CS high to initiate the sector erase operation.
drop(transfer);
// Now poll for completion.
loop {
let rdsr1 = self.read_status_register_1();
if rdsr1.erase_error() {
// The datasheet mentions that the status should be cleared and writes
// should be disabled explicitely.
self.clear_status();
self.write_disable();
return Err(EraseError::EraseErrorBitSet);
}
if !rdsr1.write_in_progress() {
return Ok(());
}
}
}
/// This function also takes care of enabling writes before programming the page.
/// This function will block until the operation has completed.
///
/// The data length max not exceed the page size [PAGE_SIZE].
pub fn program_page(&mut self, 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(0x100) {
return Err(AddrError::Alignment.into());
}
if data.len() > PAGE_SIZE {
return Err(ProgramPageError::DataLargerThanPage);
}
self.write_enable();
let qspi = self.0.get_mut();
let mut transfer = qspi.transfer_guard();
let raw_word: [u8; 4] = [
RegisterId::PageProgram 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: u32 = 0;
let mut current_byte_index = 0;
let fifo_writes = data.len().div_ceil(4);
// Fill the FIFO until it is full.
for _ in 0..core::cmp::min(fifo_writes, FIFO_DEPTH - 1) {
transfer.write_word_txd_00(u32::from_ne_bytes(
data[current_byte_index..current_byte_index + 4]
.try_into()
.unwrap(),
));
current_byte_index += 4;
}
transfer.start();
let mut wait_for_tx_slot = |transfer: &mut QspiIoTransferGuard| loop {
let status = transfer.read_status();
if status.rx_above_threshold() {
transfer.read_rx_data();
read_index = read_index.wrapping_add(4);
}
if !status.tx_full() {
break;
}
};
while current_byte_index < data.len() {
// Immediately fill the FIFO again with the remaining 8 bytes.
wait_for_tx_slot(&mut transfer);
let word = match core::cmp::min(4, data.len() - current_byte_index) {
1 => {
let mut bytes = [0; 4];
bytes[0] = data[current_byte_index];
u32::from_ne_bytes(bytes)
}
2 => {
let mut bytes = [0; 4];
bytes[0..2].copy_from_slice(&data[current_byte_index..current_byte_index + 2]);
u32::from_ne_bytes(bytes)
}
3 => {
let mut bytes = [0; 4];
bytes[0..3].copy_from_slice(&data[current_byte_index..current_byte_index + 3]);
u32::from_ne_bytes(bytes)
}
4 => u32::from_ne_bytes(
data[current_byte_index..current_byte_index + 4]
.try_into()
.unwrap(),
),
_ => unreachable!(),
};
transfer.write_word_txd_00(word);
current_byte_index += 4;
}
while read_index < data.len() as u32 {
if transfer.read_status().rx_above_threshold() {
transfer.read_rx_data();
read_index = read_index.wrapping_add(4);
}
}
drop(transfer);
// Now poll for completion.
loop {
let rdsr1 = self.read_status_register_1();
if rdsr1.programming_error() {
// The datasheet mentions that the status should be cleared and writes
// should be disabled explicitely.
self.clear_status();
self.write_disable();
return Err(ProgramPageError::ProgrammingErrorBitSet);
}
if !rdsr1.write_in_progress() {
return Ok(());
}
}
}
pub fn read_page_fast_read(&self, addr: u32, buf: &mut [u8], dummy_byte: bool) {
let mut qspi = self.0.borrow_mut();
let mut transfer = qspi.transfer_guard();
let raw_word: [u8; 4] = [
RegisterId::FastRead 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();
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;
}
transfer.start();
let mut reply_word_index = 0;
while read_index < buf.len() {
if 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();
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
}
(1, true) => {
buf[read_index] = reply_as_bytes[1];
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;
}
}
}
}
/// If the Spansion QSPI is used in linear addressed mode, no IO operations are allowed.
pub struct QspiSpansionS25Fl256SLinearMode(QspiLinearAddressing);
impl QspiSpansionS25Fl256SLinearMode {
pub const BASE_ADDR: usize = QspiLinearAddressing::BASE_ADDRESS;
pub fn into_io_mode(self, dual_flash: bool) -> QspiSpansionS25Fl256SIoMode {
let qspi = self.0.into_io_mode(dual_flash);
QspiSpansionS25Fl256SIoMode::new(qspi, false)
}
pub fn read_guard(&mut self) -> QspiLinearReadGuard<'_> {
self.0.read_guard()
}
}