From 82a8ad171e42957e6a5843c1b3d20ca3e55fa5fa Mon Sep 17 00:00:00 2001 From: Arthur Heymans Date: Fri, 20 Mar 2026 15:39:19 +0100 Subject: [PATCH 1/4] feat: add sunxi FEL SPI NOR programmer with firmware-accelerated write/erase Add rflasher-sunxi-fel crate implementing SPI NOR flash programming through the Allwinner FEL (USB boot) protocol. The programmer uploads a pre-compiled SPI driver payload to the SoC's SRAM and drives SPI via a bytecode protocol, matching xfel's approach. Architecture: - SpiMaster for probe, status registers, write protection, generic SPI - OpaqueMaster for firmware-accelerated bulk read/write (via HybridFlashDevice) - native_erase_block on SpiMaster for on-SoC erase busy-wait Write acceleration (fixes write failure at ~53 KiB): - Batched page programming: ~215 pages per payload execution using SPI_CMD_FAST (WREN) + SPI_CMD_TXBUF (PP data) + SPI_CMD_SPINOR_WAIT (on-SoC busy polling), reducing USB round-trips by ~128x - Previous approach used 3+ separate payload executions per page with host-side status register polling, which failed after ~211 pages Read optimization: - SPI_CMD_FAST embeds small TX data (opcode+address) in the command buffer, eliminating a separate FEL write per 64 KiB chunk - ~7% faster reads (49.7s vs 53.6s for 16 MB on D1/F133) Erase acceleration: - native_erase_block uses SPI_CMD_FAST + SPI_CMD_SPINOR_WAIT for single-execution erase with on-SoC busy-wait Currently supports D1/F133 (RISC-V). Other SoC families need their payload binaries extracted from xfel's chip source files. --- Cargo.lock | 11 + Cargo.toml | 9 +- .../rflasher-core/src/flash/hybrid_device.rs | 27 +- crates/rflasher-core/src/flash/spi_device.rs | 27 +- crates/rflasher-core/src/programmer/traits.rs | 29 + crates/rflasher-flash/Cargo.toml | 2 + crates/rflasher-flash/src/registry.rs | 56 ++ crates/rflasher-sunxi-fel/Cargo.toml | 17 + crates/rflasher-sunxi-fel/src/chips.rs | 215 +++++++ crates/rflasher-sunxi-fel/src/device.rs | 532 ++++++++++++++++++ crates/rflasher-sunxi-fel/src/error.rs | 40 ++ crates/rflasher-sunxi-fel/src/lib.rs | 63 +++ crates/rflasher-sunxi-fel/src/protocol.rs | 230 ++++++++ 13 files changed, 1237 insertions(+), 21 deletions(-) create mode 100644 crates/rflasher-sunxi-fel/Cargo.toml create mode 100644 crates/rflasher-sunxi-fel/src/chips.rs create mode 100644 crates/rflasher-sunxi-fel/src/device.rs create mode 100644 crates/rflasher-sunxi-fel/src/error.rs create mode 100644 crates/rflasher-sunxi-fel/src/lib.rs create mode 100644 crates/rflasher-sunxi-fel/src/protocol.rs diff --git a/Cargo.lock b/Cargo.lock index 10e77d2..4c410ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3010,6 +3010,7 @@ dependencies = [ "rflasher-linux-spi", "rflasher-raiden", "rflasher-serprog", + "rflasher-sunxi-fel", ] [[package]] @@ -3123,6 +3124,16 @@ dependencies = [ "wasm-bindgen-futures", ] +[[package]] +name = "rflasher-sunxi-fel" +version = "0.1.0" +dependencies = [ + "log", + "maybe-async", + "nusb", + "rflasher-core", +] + [[package]] name = "rflasher-wasm" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index cec22d8..7b41d57 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "crates/rflasher-linux-gpio", "crates/rflasher-dummy", "crates/rflasher-raiden", + "crates/rflasher-sunxi-fel", "crates/rflasher-repl", "crates/rflasher-wasm", ] @@ -37,6 +38,7 @@ default-members = [ "crates/rflasher-linux-gpio", "crates/rflasher-dummy", "crates/rflasher-raiden", + "crates/rflasher-sunxi-fel", "crates/rflasher-repl", ] @@ -85,7 +87,7 @@ edition.workspace = true license.workspace = true [features] -default = ["dummy", "ch341a", "ch347", "dediprog", "serprog", "ft4222", "ftdi", "linux-spi", "linux-mtd", "internal", "raiden"] +default = ["dummy", "ch341a", "ch347", "dediprog", "serprog", "ft4222", "ftdi", "linux-spi", "linux-mtd", "internal", "raiden", "sunxi-fel"] # Programmer features (passed through to rflasher-flash) dummy = ["rflasher-flash/dummy"] @@ -102,14 +104,15 @@ linux-mtd = ["rflasher-flash/linux-mtd"] linux-gpio = ["rflasher-flash/linux-gpio"] internal = ["rflasher-flash/internal"] raiden = ["rflasher-flash/raiden"] +sunxi-fel = ["rflasher-flash/sunxi-fel"] # REPL feature for scripting with Steel Scheme repl = ["dep:rflasher-repl"] # Enable all programmers -all-programmers = ["dummy", "ch341a", "ch347", "dediprog", "serprog", "ftdi", "ft4222", "linux-spi", "linux-mtd", "linux-gpio", "internal", "raiden"] +all-programmers = ["dummy", "ch341a", "ch347", "dediprog", "serprog", "ftdi", "ft4222", "linux-spi", "linux-mtd", "linux-gpio", "internal", "raiden", "sunxi-fel"] # Enable all programmers with pure-Rust FTDI backend -all-programmers-native = ["dummy", "ch341a", "ch347", "dediprog", "serprog", "ftdi-native", "ft4222", "linux-spi", "linux-mtd", "linux-gpio", "internal", "raiden"] +all-programmers-native = ["dummy", "ch341a", "ch347", "dediprog", "serprog", "ftdi-native", "ft4222", "linux-spi", "linux-mtd", "linux-gpio", "internal", "raiden", "sunxi-fel"] [dependencies] rflasher-core = { workspace = true, features = ["std", "is_sync"] } diff --git a/crates/rflasher-core/src/flash/hybrid_device.rs b/crates/rflasher-core/src/flash/hybrid_device.rs index 4bc45ca..97c6187 100644 --- a/crates/rflasher-core/src/flash/hybrid_device.rs +++ b/crates/rflasher-core/src/flash/hybrid_device.rs @@ -217,15 +217,24 @@ impl FlashDevice for HybridFlashDevice { .block_size_at_offset(offset_in_layout) .unwrap_or(max_block_size); - let result = protocol::erase_block( - self.master(), - opcode, - current_addr, - use_4byte && use_native, - poll_delay_us, - timeout_us, - ) - .await; + // Try native erase first (on-device busy-wait, e.g. SPI_CMD_SPINOR_WAIT). + // Falls back to generic WREN + erase + host-side RDSR polling. + let result = if let Some(r) = + self.master() + .native_erase_block(opcode, current_addr, use_4byte && use_native) + { + r + } else { + protocol::erase_block( + self.master(), + opcode, + current_addr, + use_4byte && use_native, + poll_delay_us, + timeout_us, + ) + .await + }; if result.is_err() { if use_4byte && !use_native { diff --git a/crates/rflasher-core/src/flash/spi_device.rs b/crates/rflasher-core/src/flash/spi_device.rs index 68d1b04..f4b0bbe 100644 --- a/crates/rflasher-core/src/flash/spi_device.rs +++ b/crates/rflasher-core/src/flash/spi_device.rs @@ -329,15 +329,24 @@ impl FlashDevice for SpiFlashDevice { .block_size_at_offset(offset_in_layout) .unwrap_or(max_block_size); - let result = protocol::erase_block( - self.master(), - opcode, - current_addr, - use_4byte && use_native, - poll_delay_us, - timeout_us, - ) - .await; + // Try native erase first (on-device busy-wait, e.g. SPI_CMD_SPINOR_WAIT). + // Falls back to generic WREN + erase + host-side RDSR polling. + let result = if let Some(r) = + self.master() + .native_erase_block(opcode, current_addr, use_4byte && use_native) + { + r + } else { + protocol::erase_block( + self.master(), + opcode, + current_addr, + use_4byte && use_native, + poll_delay_us, + timeout_us, + ) + .await + }; if result.is_err() { if use_4byte && !use_native { diff --git a/crates/rflasher-core/src/programmer/traits.rs b/crates/rflasher-core/src/programmer/traits.rs index ed5bdf7..4078b57 100644 --- a/crates/rflasher-core/src/programmer/traits.rs +++ b/crates/rflasher-core/src/programmer/traits.rs @@ -127,6 +127,26 @@ pub trait SpiMaster { true } + /// Native erase block with on-device busy-waiting. + /// + /// Programmers with firmware-level busy-wait (e.g., `SPI_CMD_SPINOR_WAIT`) + /// should override this to avoid host-side status register polling. + /// + /// Returns `None` if not supported (caller falls back to the generic path). + /// + /// # Arguments + /// * `opcode` - Erase opcode (e.g., 0x20 for 4KB, 0xD8 for 64KB) + /// * `addr` - Block address to erase + /// * `use_4byte` - Whether to use 4-byte addressing + fn native_erase_block( + &mut self, + _opcode: u8, + _addr: u32, + _use_4byte: bool, + ) -> Option> { + None + } + /// Delay for the specified number of microseconds async fn delay_us(&mut self, us: u32); } @@ -187,6 +207,15 @@ impl SpiMaster for alloc::boxed::Box { (**self).probe_opcode(opcode) } + fn native_erase_block( + &mut self, + opcode: u8, + addr: u32, + use_4byte: bool, + ) -> Option> { + (**self).native_erase_block(opcode, addr, use_4byte) + } + fn delay_us(&mut self, us: u32) { (**self).delay_us(us) } diff --git a/crates/rflasher-flash/Cargo.toml b/crates/rflasher-flash/Cargo.toml index 62dba68..ec4d894 100644 --- a/crates/rflasher-flash/Cargo.toml +++ b/crates/rflasher-flash/Cargo.toml @@ -20,6 +20,7 @@ rflasher-linux-mtd = { path = "../rflasher-linux-mtd", optional = true } rflasher-linux-gpio = { path = "../rflasher-linux-gpio", optional = true } rflasher-internal = { path = "../rflasher-internal", optional = true } rflasher-raiden = { path = "../rflasher-raiden", optional = true } +rflasher-sunxi-fel = { path = "../rflasher-sunxi-fel", optional = true } [features] default = ["std", "is_sync"] @@ -43,3 +44,4 @@ linux-mtd = ["dep:rflasher-linux-mtd"] linux-gpio = ["dep:rflasher-linux-gpio"] internal = ["dep:rflasher-internal"] raiden = ["dep:rflasher-raiden"] +sunxi-fel = ["dep:rflasher-sunxi-fel"] diff --git a/crates/rflasher-flash/src/registry.rs b/crates/rflasher-flash/src/registry.rs index 2161070..7f77c8b 100644 --- a/crates/rflasher-flash/src/registry.rs +++ b/crates/rflasher-flash/src/registry.rs @@ -365,6 +365,18 @@ pub fn open_spi_programmer(programmer: &str) -> Result { + log::info!("Opening sunxi FEL programmer for REPL..."); + let master = rflasher_sunxi_fel::SunxiFel::open().map_err(|e| { + format!( + "Failed to open sunxi FEL device: {}\nMake sure the device is in FEL mode and you have USB permissions.", + e + ) + })?; + Ok(Box::new(master)) + } + // Internal and MTD are opaque-only or not SPI-based #[cfg(feature = "internal")] "internal" => { @@ -452,6 +464,9 @@ pub fn open_flash( #[cfg(feature = "raiden")] "raiden_debug_spi" | "raiden" | "raiden_spi" => open_raiden(¶ms, db), + #[cfg(feature = "sunxi-fel")] + "sunxi_fel" | "sunxi-fel" | "fel" => open_sunxi_fel(¶ms, db), + _ => Err(format!("Unknown programmer: {}", params.name).into()), } } @@ -864,6 +879,40 @@ fn open_raiden( probe_and_create_handle(master, db) } +#[cfg(feature = "sunxi-fel")] +fn open_sunxi_fel( + _params: &ProgrammerParams, + db: &ChipDatabase, +) -> Result> { + log::info!("Opening sunxi FEL programmer..."); + + let mut master = rflasher_sunxi_fel::SunxiFel::open().map_err(|e| { + format!( + "Failed to open sunxi FEL device: {}\n\ + Make sure the device is in FEL mode (hold FEL button while plugging in USB)\n\ + and you have USB permissions (VID:1F3A PID:EFE8).", + e + ) + })?; + + log::info!("Connected to: {}", master.soc_name()); + + // Probe the flash chip via SpiMaster + let result = probe_detailed(&mut master, db)?; + log_probe_result(&result); + let chip_info = ChipInfo::from(result); + let ctx = rflasher_core::flash::FlashContext::new(chip_info.chip.clone().unwrap()); + + // Configure 4-byte addressing for OpaqueMaster if needed (flash >16MB) + master.set_use_4byte_addr(ctx.total_size() > 16 * 1024 * 1024); + + // Use HybridFlashDevice: OpaqueMaster for fast bulk read/write (batched SPI + // commands with on-SoC busy-wait), SpiMaster for erase (with native_erase_block), + // status register access, and write protection + let device = HybridFlashDevice::new(master, ctx); + Ok(FlashHandle::with_chip_info(Box::new(device), chip_info)) +} + // Programmer information and listing /// Information about a programmer pub struct ProgrammerInfo { @@ -966,6 +1015,13 @@ pub fn available_programmers() -> Vec { description: "Chrome OS EC USB SPI (serial=,target=)", }); + #[cfg(feature = "sunxi-fel")] + programmers.push(ProgrammerInfo { + name: "sunxi_fel", + aliases: &["sunxi-fel", "fel"], + description: "Allwinner sunxi FEL USB SPI NOR programmer (VID:1F3A PID:EFE8)", + }); + programmers } diff --git a/crates/rflasher-sunxi-fel/Cargo.toml b/crates/rflasher-sunxi-fel/Cargo.toml new file mode 100644 index 0000000..3f4e66d --- /dev/null +++ b/crates/rflasher-sunxi-fel/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "rflasher-sunxi-fel" +version.workspace = true +edition.workspace = true +license.workspace = true +description = "Allwinner sunxi FEL SPI NOR programmer support for rflasher" + +[features] +default = ["std"] +std = ["rflasher-core/std", "is_sync", "nusb"] +is_sync = ["rflasher-core/is_sync", "maybe-async/is_sync"] + +[dependencies] +rflasher-core.workspace = true +nusb = { workspace = true, optional = true } +maybe-async.workspace = true +log.workspace = true diff --git a/crates/rflasher-sunxi-fel/src/chips.rs b/crates/rflasher-sunxi-fel/src/chips.rs new file mode 100644 index 0000000..be14480 --- /dev/null +++ b/crates/rflasher-sunxi-fel/src/chips.rs @@ -0,0 +1,215 @@ +//! Allwinner SoC chip definitions and SPI payload management +//! +//! Each supported SoC has a pre-compiled SPI driver payload (from xfel) +//! that runs natively on the SoC. The payload handles all hardware init +//! (CCU clocks, GPIO pin mux, SPI controller registers) and implements +//! a bytecode interpreter for SPI transactions. +//! +//! The host uploads the payload via FEL, then drives SPI by writing +//! command bytecodes to a command buffer and executing the payload. + +use crate::error::{Error, Result}; +use crate::protocol::FelTransport; + +/// SPI command bytecodes (interpreted by the on-SoC payload) +pub mod spi_cmd { + pub const END: u8 = 0x00; + pub const INIT: u8 = 0x01; + pub const SELECT: u8 = 0x02; + pub const DESELECT: u8 = 0x03; + pub const FAST: u8 = 0x04; + pub const TXBUF: u8 = 0x05; + pub const RXBUF: u8 = 0x06; + pub const SPINOR_WAIT: u8 = 0x07; +} + +/// Memory layout for the SPI payload on the target SoC +#[derive(Debug, Clone, Copy)] +pub struct SpiPayloadInfo { + /// Address where the payload code is loaded + pub payload_addr: u32, + /// Address of the command buffer (bytecodes written here) + pub cmdbuf_addr: u32, + /// Address of the swap buffer (TX/RX data exchanged here) + pub swapbuf: u32, + /// Size of the swap buffer in bytes + pub swaplen: u32, + /// Maximum command buffer length + pub cmdlen: u32, +} + +/// Supported Allwinner SoC chip families +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum ChipFamily { + /// D1/F133 (sun20iw1) - RISC-V C906 + D1, + // Future: H2H3, V3sS3, F1C100s, R528, etc. +} + +impl ChipFamily { + /// Human-readable name for the SoC + pub fn name(&self) -> &'static str { + match self { + ChipFamily::D1 => "D1/F133", + } + } +} + +/// Detect chip family from FEL version ID +pub fn detect_chip(id: u32) -> Option { + match id { + 0x00185900 => Some(ChipFamily::D1), + _ => None, + } +} + +/// D1/F133 SPI payload (1480 bytes, RISC-V machine code from xfel) +/// +/// This payload initializes CCU clocks, GPIO pin mux, and the SPI0 +/// controller, then enters a bytecode interpreter loop that processes +/// SPI_CMD_* commands from the command buffer at 0x00021000. +const D1_SPI_PAYLOAD: &[u8] = &[ + 0x37, 0x03, 0x40, 0x00, 0x73, 0x20, 0x03, 0x7c, 0x37, 0x03, 0x03, 0x00, 0x1b, 0x03, 0x33, 0x01, + 0x73, 0x20, 0x23, 0x7c, 0x6f, 0x00, 0x40, 0x00, 0x13, 0x01, 0x01, 0xfe, 0x23, 0x34, 0x81, 0x00, + 0x23, 0x38, 0x91, 0x00, 0x23, 0x3c, 0x11, 0x00, 0x13, 0x04, 0x05, 0x00, 0x37, 0x15, 0x02, 0x00, + 0xef, 0x00, 0x00, 0x02, 0x83, 0x30, 0x81, 0x01, 0x03, 0x34, 0x81, 0x00, 0x83, 0x34, 0x01, 0x01, + 0x13, 0x01, 0x01, 0x02, 0x67, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x13, 0x01, 0x01, 0xfb, 0x23, 0x34, 0x81, 0x04, 0x23, 0x30, 0x91, 0x04, 0x23, 0x3c, 0x21, 0x03, + 0x23, 0x38, 0x31, 0x03, 0x23, 0x34, 0x41, 0x03, 0x23, 0x30, 0x51, 0x03, 0x23, 0x3c, 0x61, 0x01, + 0x23, 0x38, 0x71, 0x01, 0x23, 0x34, 0x81, 0x01, 0x23, 0x30, 0x91, 0x01, 0x13, 0x08, 0x05, 0x00, + 0x0b, 0x47, 0x18, 0x98, 0xb7, 0x57, 0x02, 0x04, 0xb7, 0xcf, 0x00, 0x00, 0x37, 0xfa, 0xff, 0xff, + 0xb7, 0x19, 0xff, 0xff, 0x37, 0x09, 0xf1, 0xff, 0xb7, 0x04, 0x10, 0xff, 0x37, 0x04, 0x00, 0xfd, + 0xb7, 0x03, 0x00, 0x80, 0x93, 0x06, 0x10, 0x00, 0x13, 0x0f, 0x20, 0x00, 0x93, 0x8f, 0xff, 0x00, + 0x93, 0x85, 0x07, 0x03, 0x13, 0x06, 0x20, 0x00, 0x93, 0x08, 0xf0, 0xff, 0x93, 0x0e, 0xf0, 0xff, + 0x13, 0x0a, 0xfa, 0x0f, 0x93, 0x89, 0xf9, 0xff, 0x13, 0x09, 0xf9, 0xff, 0x93, 0x84, 0xf4, 0xff, + 0x13, 0x04, 0xf4, 0xff, 0x93, 0x83, 0x33, 0x08, 0x63, 0x0c, 0xd7, 0x0c, 0x63, 0x06, 0xe7, 0x1d, + 0x13, 0x03, 0x30, 0x00, 0x63, 0x0c, 0x67, 0x1c, 0x13, 0x03, 0x40, 0x00, 0x63, 0x04, 0x67, 0x1e, + 0x13, 0x03, 0x50, 0x00, 0x63, 0x06, 0x67, 0x28, 0x13, 0x03, 0x60, 0x00, 0x63, 0x0e, 0x67, 0x32, + 0x13, 0x05, 0x70, 0x00, 0x63, 0x00, 0xa7, 0x40, 0x13, 0x05, 0x80, 0x00, 0x63, 0x16, 0xa7, 0x48, + 0x37, 0x07, 0xff, 0xff, 0xb3, 0xf2, 0xe2, 0x00, 0xb3, 0xe2, 0xf2, 0x01, 0x13, 0xf3, 0xff, 0x0f, + 0x0b, 0xbe, 0x8f, 0x3c, 0x37, 0x05, 0x00, 0x80, 0x23, 0xa0, 0xc5, 0x00, 0x23, 0xaa, 0xc7, 0x02, + 0x23, 0xac, 0xc7, 0x02, 0x23, 0x80, 0x67, 0x20, 0x23, 0x80, 0xc7, 0x21, 0x03, 0xa7, 0x87, 0x00, + 0x33, 0x67, 0xa7, 0x00, 0x23, 0xa4, 0xe7, 0x00, 0x03, 0xa7, 0x87, 0x00, 0xe3, 0x4e, 0x07, 0xfe, + 0x03, 0xa7, 0xc7, 0x01, 0x13, 0x77, 0xe7, 0x0f, 0xe3, 0x0c, 0x07, 0xfe, 0x03, 0xc7, 0x07, 0x30, + 0x03, 0xc7, 0x07, 0x30, 0x23, 0xa0, 0xd5, 0x00, 0x23, 0xaa, 0xd7, 0x02, 0x23, 0xac, 0xd7, 0x02, + 0x23, 0x80, 0x17, 0x21, 0x03, 0xa7, 0x87, 0x00, 0x33, 0x67, 0xa7, 0x00, 0x23, 0xa4, 0xe7, 0x00, + 0x03, 0xa7, 0x87, 0x00, 0xe3, 0x4e, 0x07, 0xfe, 0x03, 0xa7, 0xc7, 0x01, 0x13, 0x77, 0xf7, 0x0f, + 0xe3, 0x0c, 0x07, 0xfe, 0x03, 0xc7, 0x07, 0x30, 0x13, 0x77, 0x17, 0x00, 0xe3, 0x16, 0x07, 0xf8, + 0x13, 0x05, 0x08, 0x00, 0x13, 0x08, 0x05, 0x00, 0x0b, 0x47, 0x18, 0x98, 0xe3, 0x18, 0xd7, 0xf2, + 0x37, 0x05, 0x00, 0x02, 0x03, 0x27, 0x05, 0x06, 0x37, 0x23, 0x00, 0x00, 0x33, 0x77, 0x47, 0x01, + 0x13, 0x67, 0x07, 0x20, 0x23, 0x20, 0xe5, 0x06, 0x03, 0x27, 0x05, 0x06, 0x37, 0x0e, 0x20, 0x00, + 0x33, 0x77, 0x37, 0x01, 0x33, 0x67, 0x67, 0x00, 0x23, 0x20, 0xe5, 0x06, 0x03, 0x27, 0x05, 0x06, + 0x37, 0x03, 0x02, 0x00, 0x33, 0x77, 0x27, 0x01, 0x33, 0x67, 0x67, 0x00, 0x23, 0x20, 0xe5, 0x06, + 0x03, 0x23, 0x05, 0x06, 0x37, 0x27, 0x00, 0x02, 0x33, 0x73, 0x93, 0x00, 0x33, 0x63, 0xc3, 0x01, + 0x23, 0x20, 0x65, 0x06, 0x03, 0x25, 0xc7, 0x96, 0x37, 0x03, 0x01, 0x00, 0x33, 0x65, 0x65, 0x00, + 0x23, 0x26, 0xa7, 0x96, 0x03, 0x25, 0x07, 0x94, 0x37, 0x03, 0x00, 0x80, 0x33, 0x65, 0x65, 0x00, + 0x23, 0x20, 0xa7, 0x94, 0x03, 0x25, 0xc7, 0x96, 0x37, 0x03, 0x00, 0x01, 0x13, 0x65, 0x15, 0x00, + 0x23, 0x26, 0xa7, 0x96, 0x03, 0x25, 0x07, 0x94, 0x33, 0x75, 0x85, 0x00, 0x33, 0x65, 0x65, 0x00, + 0x23, 0x20, 0xa7, 0x94, 0x03, 0x25, 0x07, 0x94, 0x13, 0x75, 0xf5, 0xcf, 0x23, 0x20, 0xa7, 0x94, + 0x03, 0x25, 0x07, 0x94, 0x13, 0x75, 0x05, 0xff, 0x13, 0x65, 0x55, 0x00, 0x23, 0x20, 0xa7, 0x94, + 0x37, 0x17, 0x00, 0x00, 0x23, 0xa2, 0xe7, 0x02, 0x03, 0xa7, 0x47, 0x00, 0x33, 0xe7, 0xe3, 0x00, + 0x1b, 0x07, 0x07, 0x00, 0x23, 0xa2, 0xe7, 0x00, 0x03, 0xa7, 0x47, 0x00, 0xe3, 0x4e, 0x07, 0xfe, + 0x03, 0xa7, 0x87, 0x00, 0x37, 0x85, 0x00, 0x80, 0x13, 0x77, 0xc7, 0xff, 0x13, 0x67, 0x47, 0x04, + 0x23, 0xa4, 0xe7, 0x00, 0x03, 0xa7, 0x87, 0x01, 0x33, 0x67, 0xa7, 0x00, 0x23, 0xac, 0xe7, 0x00, + 0x13, 0x05, 0x08, 0x00, 0x6f, 0xf0, 0x1f, 0xf0, 0x03, 0xa7, 0x87, 0x00, 0x13, 0x05, 0x08, 0x00, + 0x13, 0x77, 0xf7, 0xf4, 0x23, 0xa4, 0xe7, 0x00, 0x6f, 0xf0, 0xdf, 0xee, 0x03, 0xa7, 0x87, 0x00, + 0x13, 0x05, 0x08, 0x00, 0x13, 0x77, 0xf7, 0xf4, 0x13, 0x67, 0x07, 0x08, 0x23, 0xa4, 0xe7, 0x00, + 0x6f, 0xf0, 0x5f, 0xed, 0x03, 0x4e, 0x15, 0x00, 0x13, 0x0c, 0x00, 0x04, 0x93, 0x0a, 0x0e, 0x00, + 0x13, 0x05, 0x25, 0x00, 0x93, 0x0b, 0x00, 0x04, 0x37, 0x0b, 0x00, 0x80, 0x63, 0x00, 0x0e, 0x08, + 0x33, 0x33, 0xcc, 0x01, 0x13, 0x43, 0x13, 0x00, 0x13, 0x87, 0x0b, 0x00, 0x0b, 0x17, 0x6e, 0x42, + 0x23, 0xa0, 0xe5, 0x00, 0x23, 0xaa, 0xe7, 0x02, 0x1b, 0x03, 0x07, 0x00, 0x23, 0xac, 0xe7, 0x02, + 0x13, 0x07, 0x00, 0x00, 0x13, 0x00, 0x00, 0x00, 0x8b, 0x4c, 0xe5, 0x80, 0x13, 0x07, 0x17, 0x00, + 0x23, 0x80, 0x97, 0x21, 0x9b, 0x0c, 0x07, 0x00, 0xe3, 0xc8, 0x6c, 0xfe, 0x03, 0xa7, 0x87, 0x00, + 0x33, 0x67, 0x67, 0x01, 0x23, 0xa4, 0xe7, 0x00, 0x03, 0xa7, 0x87, 0x00, 0xe3, 0x4e, 0x07, 0xfe, + 0x03, 0xa7, 0xc7, 0x01, 0x13, 0x77, 0xf7, 0x0f, 0xe3, 0x6c, 0x67, 0xfe, 0x13, 0x07, 0x00, 0x00, + 0x83, 0xcc, 0x07, 0x30, 0x1b, 0x07, 0x17, 0x00, 0xe3, 0x4c, 0x67, 0xfe, 0x0b, 0x37, 0x03, 0x7c, + 0x3b, 0x0e, 0x6e, 0x40, 0x33, 0x05, 0xe5, 0x00, 0xe3, 0x14, 0x0e, 0xf8, 0x1b, 0x87, 0x1a, 0x00, + 0x0b, 0x37, 0x07, 0x7c, 0x33, 0x08, 0xe8, 0x00, 0x13, 0x05, 0x08, 0x00, 0x6f, 0xf0, 0x9f, 0xe2, + 0x03, 0x2e, 0x55, 0x00, 0x03, 0x63, 0x15, 0x00, 0x93, 0x0b, 0x00, 0x04, 0x13, 0x0b, 0x00, 0x04, + 0xb7, 0x0a, 0x00, 0x80, 0x63, 0x02, 0x0e, 0x08, 0x33, 0xb7, 0xcb, 0x01, 0x13, 0x47, 0x17, 0x00, + 0x13, 0x08, 0x0b, 0x00, 0x0b, 0x18, 0xee, 0x42, 0x23, 0xa0, 0x05, 0x01, 0x23, 0xaa, 0x07, 0x03, + 0x23, 0xac, 0x07, 0x03, 0x13, 0x07, 0x00, 0x00, 0x63, 0x06, 0x03, 0x06, 0x13, 0x00, 0x00, 0x00, + 0x0b, 0x4c, 0xe3, 0x80, 0x13, 0x07, 0x17, 0x00, 0x23, 0x80, 0x87, 0x21, 0x1b, 0x0c, 0x07, 0x00, + 0xe3, 0x48, 0x0c, 0xff, 0x03, 0xa7, 0x87, 0x00, 0x33, 0x67, 0x57, 0x01, 0x23, 0xa4, 0xe7, 0x00, + 0x03, 0xa7, 0x87, 0x00, 0xe3, 0x4e, 0x07, 0xfe, 0x03, 0xa7, 0xc7, 0x01, 0x13, 0x77, 0xf7, 0x0f, + 0xe3, 0x6c, 0x07, 0xff, 0x13, 0x07, 0x00, 0x00, 0x03, 0xcc, 0x07, 0x30, 0x1b, 0x07, 0x17, 0x00, + 0xe3, 0x4c, 0x07, 0xff, 0x0b, 0x37, 0x08, 0x7c, 0x33, 0x07, 0xe3, 0x00, 0x3b, 0x0e, 0x0e, 0x41, + 0x0b, 0x13, 0x67, 0x42, 0xe3, 0x12, 0x0e, 0xf8, 0x13, 0x08, 0x95, 0x00, 0x13, 0x05, 0x08, 0x00, + 0x6f, 0xf0, 0x5f, 0xd8, 0x13, 0x8c, 0x07, 0x20, 0x23, 0x00, 0xdc, 0x01, 0x1b, 0x07, 0x17, 0x00, + 0xe3, 0x4c, 0x07, 0xff, 0x6f, 0xf0, 0x1f, 0xfa, 0x03, 0x2e, 0x55, 0x00, 0x03, 0x63, 0x15, 0x00, + 0x93, 0x0b, 0x00, 0x04, 0x13, 0x0b, 0x00, 0x04, 0xb7, 0x0a, 0x00, 0x80, 0xe3, 0x06, 0x0e, 0xfc, + 0x33, 0xb8, 0xcb, 0x01, 0x13, 0x48, 0x18, 0x00, 0x13, 0x07, 0x0b, 0x00, 0x0b, 0x17, 0x0e, 0x43, + 0x23, 0xa0, 0xe5, 0x00, 0x23, 0xaa, 0xe7, 0x02, 0x13, 0x08, 0x07, 0x00, 0x23, 0xac, 0xe7, 0x02, + 0x13, 0x8c, 0x07, 0x20, 0x13, 0x07, 0x00, 0x00, 0x23, 0x00, 0xdc, 0x01, 0x1b, 0x07, 0x17, 0x00, + 0xe3, 0x4c, 0x07, 0xff, 0x03, 0xa7, 0x87, 0x00, 0x33, 0x67, 0x57, 0x01, 0x23, 0xa4, 0xe7, 0x00, + 0x03, 0xa7, 0x87, 0x00, 0xe3, 0x4e, 0x07, 0xfe, 0x03, 0xa7, 0xc7, 0x01, 0x13, 0x77, 0xf7, 0x0f, + 0xe3, 0x6c, 0x07, 0xff, 0x13, 0x07, 0x00, 0x00, 0x83, 0xcc, 0x07, 0x30, 0x13, 0x0c, 0x03, 0x00, + 0x93, 0xfc, 0xfc, 0x0f, 0x1b, 0x07, 0x17, 0x00, 0x63, 0x00, 0x03, 0x02, 0x8b, 0x5c, 0x1c, 0x18, + 0x13, 0x03, 0x0c, 0x00, 0xe3, 0x42, 0x07, 0xff, 0x3b, 0x0e, 0x0e, 0x41, 0xe3, 0x12, 0x0e, 0xf8, + 0x13, 0x08, 0x95, 0x00, 0x6f, 0xf0, 0x9f, 0xf4, 0x1b, 0x0c, 0x17, 0x00, 0xe3, 0x56, 0x07, 0xff, + 0x83, 0xcc, 0x07, 0x30, 0xe3, 0x52, 0x0c, 0xff, 0x03, 0xcc, 0x07, 0x30, 0x1b, 0x07, 0x27, 0x00, + 0x1b, 0x0c, 0x17, 0x00, 0xe3, 0x46, 0x07, 0xff, 0x3b, 0x0e, 0x0e, 0x41, 0xe3, 0x1a, 0x0e, 0xf4, + 0x6f, 0xf0, 0x1f, 0xfd, 0x93, 0xf2, 0x02, 0xf0, 0x93, 0xe2, 0x52, 0x00, 0x13, 0x03, 0x50, 0x00, + 0x13, 0x0e, 0x10, 0x00, 0x37, 0x05, 0x00, 0x80, 0x23, 0xa0, 0xc5, 0x01, 0x23, 0xaa, 0xc7, 0x03, + 0x23, 0xac, 0xc7, 0x03, 0x23, 0x80, 0x67, 0x20, 0x03, 0xa7, 0x87, 0x00, 0x33, 0x67, 0xa7, 0x00, + 0x23, 0xa4, 0xe7, 0x00, 0x13, 0x00, 0x00, 0x00, 0x03, 0xa7, 0x87, 0x00, 0xe3, 0x4e, 0x07, 0xfe, + 0x03, 0xa7, 0xc7, 0x01, 0x13, 0x77, 0xf7, 0x0f, 0xe3, 0x0c, 0x07, 0xfe, 0x03, 0xc7, 0x07, 0x30, + 0x23, 0xa0, 0xd5, 0x00, 0x23, 0xaa, 0xd7, 0x02, 0x23, 0xac, 0xd7, 0x02, 0x23, 0x80, 0x17, 0x21, + 0x03, 0xa7, 0x87, 0x00, 0x33, 0x67, 0xa7, 0x00, 0x23, 0xa4, 0xe7, 0x00, 0x13, 0x00, 0x00, 0x00, + 0x03, 0xa7, 0x87, 0x00, 0xe3, 0x4e, 0x07, 0xfe, 0x03, 0xa7, 0xc7, 0x01, 0x13, 0x77, 0xf7, 0x0f, + 0xe3, 0x0c, 0x07, 0xfe, 0x03, 0xc7, 0x07, 0x30, 0x13, 0x77, 0x17, 0x00, 0xe3, 0x16, 0x07, 0xf8, + 0x13, 0x05, 0x08, 0x00, 0x6f, 0xf0, 0x1f, 0xc1, 0x03, 0x34, 0x81, 0x04, 0x83, 0x34, 0x01, 0x04, + 0x03, 0x39, 0x81, 0x03, 0x83, 0x39, 0x01, 0x03, 0x03, 0x3a, 0x81, 0x02, 0x83, 0x3a, 0x01, 0x02, + 0x03, 0x3b, 0x81, 0x01, 0x83, 0x3b, 0x01, 0x01, 0x03, 0x3c, 0x81, 0x00, 0x83, 0x3c, 0x01, 0x00, + 0x13, 0x01, 0x01, 0x05, 0x67, 0x80, 0x00, 0x00, +]; + +/// Get the SPI payload and memory layout for a chip +pub fn spi_payload(chip: ChipFamily) -> (&'static [u8], SpiPayloadInfo) { + match chip { + ChipFamily::D1 => ( + D1_SPI_PAYLOAD, + SpiPayloadInfo { + payload_addr: 0x00020000, + cmdbuf_addr: 0x00021000, + swapbuf: 0x00022000, + swaplen: 65536, + cmdlen: 4096, + }, + ), + } +} + +/// Upload the SPI payload and send SPI_CMD_INIT +pub fn spi_init(transport: &mut FelTransport, chip: ChipFamily) -> Result { + let (payload, info) = spi_payload(chip); + + // Upload payload to target SRAM + transport.fel_write(info.payload_addr, payload)?; + log::debug!( + "Uploaded {} byte SPI payload to 0x{:08x}", + payload.len(), + info.payload_addr + ); + + // Send SPI_CMD_INIT to initialize hardware (clocks, GPIO, SPI controller) + let init_cmd = [spi_cmd::INIT, spi_cmd::END]; + transport.fel_write(info.cmdbuf_addr, &init_cmd)?; + transport.fel_exec(info.payload_addr)?; + log::debug!("SPI_CMD_INIT complete"); + + Ok(info) +} + +/// Write command buffer and execute the SPI payload +pub fn spi_run(transport: &mut FelTransport, info: &SpiPayloadInfo, cbuf: &[u8]) -> Result<()> { + if cbuf.len() as u32 > info.cmdlen { + return Err(Error::Protocol(format!( + "SPI command buffer too large: {} > {}", + cbuf.len(), + info.cmdlen + ))); + } + transport.fel_write(info.cmdbuf_addr, cbuf)?; + transport.fel_exec(info.payload_addr) +} diff --git a/crates/rflasher-sunxi-fel/src/device.rs b/crates/rflasher-sunxi-fel/src/device.rs new file mode 100644 index 0000000..c840303 --- /dev/null +++ b/crates/rflasher-sunxi-fel/src/device.rs @@ -0,0 +1,532 @@ +//! SunxiFel device - main programmer struct implementing SpiMaster + OpaqueMaster +//! +//! Uses the xfel payload approach: a pre-compiled SPI driver runs natively +//! on the SoC, and the host drives SPI by writing bytecode commands to a +//! shared buffer and executing the payload via FEL. +//! +//! # Architecture +//! +//! SunxiFel implements both `SpiMaster` and `OpaqueMaster`: +//! +//! - **SpiMaster**: Generic SPI command execution for probe, status register +//! reads, write protection, etc. Each `execute()` call builds bytecodes +//! and runs the payload once. +//! +//! - **OpaqueMaster**: Firmware-accelerated bulk read/write. Uses +//! `SPI_CMD_FAST` for small fixed commands (WREN, opcode+address), +//! `SPI_CMD_TXBUF` for page data, and `SPI_CMD_SPINOR_WAIT` for on-SoC +//! busy polling. Writes batch ~215 pages per payload execution, matching +//! xfel's `spinor_helper_write` approach. +//! +//! Use with `HybridFlashDevice` for optimal performance: +//! - read/write → OpaqueMaster (fast bulk path) +//! - erase/WP → SpiMaster (with `native_erase_block` for on-SoC busy-wait) + +use nusb::transfer::{Bulk, In, Out}; +use nusb::MaybeFuture; +use rflasher_core::error::{Error as CoreError, Result as CoreResult}; +use rflasher_core::programmer::{OpaqueMaster, SpiFeatures, SpiMaster}; +use rflasher_core::spi::SpiCommand; + +use crate::chips::{self, spi_cmd, ChipFamily, SpiPayloadInfo}; +use crate::error::{Error, Result}; +use crate::protocol::{FelTransport, FelVersion, FEL_PID, FEL_VID}; + +/// Allwinner FEL SPI programmer +pub struct SunxiFel { + transport: FelTransport, + version: FelVersion, + chip: ChipFamily, + spi_info: SpiPayloadInfo, + _interface: nusb::Interface, + /// Whether to use 4-byte addressing for OpaqueMaster read/write. + /// Set after probing via `set_use_4byte_addr()` when the flash is >16MB. + use_4byte_addr: bool, +} + +impl SunxiFel { + /// Open the first available FEL device + pub fn open() -> Result { + let devices: Vec<_> = nusb::list_devices() + .wait() + .map_err(|e| Error::Usb(format!("failed to enumerate USB devices: {}", e)))? + .filter(|d| d.vendor_id() == FEL_VID && d.product_id() == FEL_PID) + .collect(); + + let device_info = devices.first().ok_or(Error::DeviceNotFound)?; + + log::info!( + "Found FEL device: bus={} addr={}", + device_info.busnum(), + device_info.device_address() + ); + + let device = device_info + .open() + .wait() + .map_err(|e| Error::Usb(format!("failed to open device: {}", e)))?; + + let interface = device + .claim_interface(0) + .wait() + .map_err(|e| Error::Usb(format!("failed to claim interface: {}", e)))?; + + // Find bulk endpoints + let mut ep_in = None; + let mut ep_out = None; + if let Ok(config) = device.active_configuration() { + for alt in config.interface_alt_settings() { + if alt.interface_number() == 0 { + for ep in alt.endpoints() { + match ep.direction() { + nusb::transfer::Direction::In => { + if ep_in.is_none() { + ep_in = Some(ep.address()); + } + } + nusb::transfer::Direction::Out => { + if ep_out.is_none() { + ep_out = Some(ep.address()); + } + } + } + } + break; + } + } + } + + let ep_in_addr = ep_in.ok_or_else(|| Error::Usb("no bulk IN endpoint".into()))?; + let ep_out_addr = ep_out.ok_or_else(|| Error::Usb("no bulk OUT endpoint".into()))?; + + let out_ep = interface + .endpoint::(ep_out_addr) + .map_err(|e| Error::Usb(format!("OUT endpoint: {}", e)))?; + let in_ep = interface + .endpoint::(ep_in_addr) + .map_err(|e| Error::Usb(format!("IN endpoint: {}", e)))?; + + let mut transport = FelTransport::new(out_ep, in_ep); + + // Query FEL version + let version = transport.fel_version()?; + log::info!( + "FEL version: ID=0x{:08x} firmware=0x{:08x} scratchpad=0x{:08x}", + version.id, + version.firmware, + version.scratchpad + ); + + let chip = chips::detect_chip(version.id).ok_or(Error::UnsupportedSoc(version.id))?; + log::info!("Detected SoC: {}", chip.name()); + + // Upload SPI payload and initialize hardware + let spi_info = chips::spi_init(&mut transport, chip)?; + log::info!( + "SPI initialized: swapbuf=0x{:08x} swaplen={} cmdlen={}", + spi_info.swapbuf, + spi_info.swaplen, + spi_info.cmdlen + ); + + Ok(Self { + transport, + version, + chip, + spi_info, + _interface: interface, + use_4byte_addr: false, + }) + } + + /// Set whether to use 4-byte addressing for bulk read/write operations. + /// + /// Call this after probing if the flash chip requires 4-byte addressing + /// (i.e., capacity >16 MiB). This affects `OpaqueMaster::read()` and + /// `OpaqueMaster::write()`. + pub fn set_use_4byte_addr(&mut self, use_4byte: bool) { + self.use_4byte_addr = use_4byte; + } + + /// Get the detected SoC name + pub fn soc_name(&self) -> &'static str { + self.chip.name() + } + + /// Get the FEL version info + pub fn version(&self) -> &FelVersion { + &self.version + } + + /// Perform a SPI transfer using the xfel bytecode protocol. + /// + /// This implements `fel_spi_xfer` from xfel: + /// 1. Build command bytecodes (SELECT, TXBUF/FAST, RXBUF, DESELECT, END) + /// 2. Write TX data to swap buffer (unless using FAST for small TX) + /// 3. Execute the payload (which processes the bytecodes) + /// 4. Read RX data from swap buffer + /// + /// Optimization: when TX data is small (≤64 bytes), uses `SPI_CMD_FAST` + /// to embed the TX bytes directly in the command buffer, avoiding a + /// separate FEL write for the swap buffer. This saves ~9 USB transfers + /// per read chunk (significant for bulk reads). + fn spi_xfer(&mut self, txbuf: &[u8], rxbuf: &mut [u8]) -> Result<()> { + let txlen = txbuf.len() as u32; + let rxlen = rxbuf.len() as u32; + let swapbuf = self.spi_info.swapbuf; + let swaplen = self.spi_info.swaplen; + + if txlen <= swaplen && rxlen <= swaplen { + // Fast path: everything fits in one transfer. + // Use SPI_CMD_FAST for small TX data to avoid a separate fel_write. + let use_fast_tx = txlen > 0 && txlen <= 64; + + let mut cbuf = Vec::with_capacity(32 + txlen as usize); + cbuf.push(spi_cmd::SELECT); + + if use_fast_tx { + // Embed TX data directly in the command buffer + cbuf.push(spi_cmd::FAST); + cbuf.push(txlen as u8); + cbuf.extend_from_slice(txbuf); + } else if txlen > 0 { + cbuf.push(spi_cmd::TXBUF); + cbuf.extend_from_slice(&swapbuf.to_le_bytes()); + cbuf.extend_from_slice(&txlen.to_le_bytes()); + } + if rxlen > 0 { + cbuf.push(spi_cmd::RXBUF); + cbuf.extend_from_slice(&swapbuf.to_le_bytes()); + cbuf.extend_from_slice(&rxlen.to_le_bytes()); + } + + cbuf.push(spi_cmd::DESELECT); + cbuf.push(spi_cmd::END); + + if !use_fast_tx && txlen > 0 { + self.transport.fel_write(swapbuf, txbuf)?; + } + + chips::spi_run(&mut self.transport, &self.spi_info, &cbuf)?; + + if rxlen > 0 { + let data = self.transport.fel_read(swapbuf, rxlen as usize)?; + rxbuf.copy_from_slice(&data); + } + } else { + // Slow path: chunk the transfer + // Select + let cbuf = [spi_cmd::SELECT, spi_cmd::END]; + chips::spi_run(&mut self.transport, &self.spi_info, &cbuf)?; + + // TX in chunks + let mut tx_off = 0u32; + let mut tx_rem = txlen; + while tx_rem > 0 { + let n = tx_rem.min(swaplen); + let mut cbuf = Vec::with_capacity(16); + cbuf.push(spi_cmd::TXBUF); + cbuf.extend_from_slice(&swapbuf.to_le_bytes()); + cbuf.extend_from_slice(&n.to_le_bytes()); + cbuf.push(spi_cmd::END); + + self.transport + .fel_write(swapbuf, &txbuf[tx_off as usize..(tx_off + n) as usize])?; + chips::spi_run(&mut self.transport, &self.spi_info, &cbuf)?; + + tx_off += n; + tx_rem -= n; + } + + // RX in chunks + let mut rx_off = 0u32; + let mut rx_rem = rxlen; + while rx_rem > 0 { + let n = rx_rem.min(swaplen); + let mut cbuf = Vec::with_capacity(16); + cbuf.push(spi_cmd::RXBUF); + cbuf.extend_from_slice(&swapbuf.to_le_bytes()); + cbuf.extend_from_slice(&n.to_le_bytes()); + cbuf.push(spi_cmd::END); + + chips::spi_run(&mut self.transport, &self.spi_info, &cbuf)?; + + let data = self.transport.fel_read(swapbuf, n as usize)?; + rxbuf[rx_off as usize..(rx_off + n) as usize].copy_from_slice(&data); + + rx_off += n; + rx_rem -= n; + } + + // Deselect + let cbuf = [spi_cmd::DESELECT, spi_cmd::END]; + chips::spi_run(&mut self.transport, &self.spi_info, &cbuf)?; + } + + Ok(()) + } + + /// Erase a block using on-SoC bytecodes (FAST for WREN + erase, SPINOR_WAIT). + /// + /// This is a single payload execution per erase block, matching xfel's + /// `spinor_sector_erase_*` functions. The SoC firmware handles busy-wait + /// locally, so there are no USB round-trips for status polling. + fn erase_block_bytecode(&mut self, opcode: u8, addr: u32, use_4byte: bool) -> Result<()> { + let fast_len: u8 = if use_4byte { 5 } else { 4 }; // opcode + addr bytes + + let mut cbuf = Vec::with_capacity(32); + + // WREN + cbuf.push(spi_cmd::SELECT); + cbuf.push(spi_cmd::FAST); + cbuf.push(1); + cbuf.push(0x06); // WREN opcode + cbuf.push(spi_cmd::DESELECT); + + // Erase command (opcode + address) + cbuf.push(spi_cmd::SELECT); + cbuf.push(spi_cmd::FAST); + cbuf.push(fast_len); + cbuf.push(opcode); + if use_4byte { + cbuf.push((addr >> 24) as u8); + } + cbuf.push((addr >> 16) as u8); + cbuf.push((addr >> 8) as u8); + cbuf.push(addr as u8); + cbuf.push(spi_cmd::DESELECT); + + // Wait for erase to complete (on-SoC RDSR polling) + cbuf.push(spi_cmd::SELECT); + cbuf.push(spi_cmd::SPINOR_WAIT); + cbuf.push(spi_cmd::DESELECT); + + cbuf.push(spi_cmd::END); + + chips::spi_run(&mut self.transport, &self.spi_info, &cbuf) + } + + /// Batched page programming matching xfel's `spinor_helper_write`. + /// + /// Packs multiple page programs into a single command+swap buffer pair + /// for maximum throughput. Each page in the batch uses: + /// - `SPI_CMD_FAST` for WREN (1 byte, from command buffer) + /// - `SPI_CMD_TXBUF` for PP data (from swap buffer at page-specific offset) + /// - `SPI_CMD_SPINOR_WAIT` for on-SoC busy polling + /// + /// With cmdlen=4096 and swaplen=65536: ~215 pages per batch (limited by + /// 19 bytes/page in cmd buffer), reducing USB round-trips by ~128× + /// compared to per-page execution. + fn batched_write( + &mut self, + addr: u32, + data: &[u8], + page_size: usize, + use_4byte: bool, + ) -> Result<()> { + let addr_len: usize = if use_4byte { 4 } else { 3 }; + let pp_opcode: u8 = 0x02; // Page Program + let wren_opcode: u8 = 0x06; + + // Per-page overhead: + // cmd buffer: SELECT(1) + FAST(1+1+1) + DESELECT(1) = 5 for WREN + // SELECT(1) + TXBUF(1+4+4) + DESELECT(1) = 11 for PP + // SELECT(1) + SPINOR_WAIT(1) + DESELECT(1) = 3 for wait + // Total: 19 bytes per page + // swap buffer: opcode(1) + addr(3|4) + data(up to page_size) + let per_page_cbuf: usize = 19; + let cmdlen = self.spi_info.cmdlen as usize; + let swaplen = self.spi_info.swaplen as usize; + let swapbuf = self.spi_info.swapbuf; + let max_tx_per_page = 1 + addr_len + page_size; + + let mut remaining = data.len(); + let mut data_offset = 0usize; + let mut current_addr = addr; + + while remaining > 0 { + let mut cbuf = Vec::with_capacity(cmdlen); + let mut txbuf = Vec::with_capacity(swaplen); + + // Pack as many pages as will fit (matching xfel's loop bounds: + // clen < cmdlen - 19 - 1 AND txlen < swaplen - granularity - addr_overhead) + while remaining > 0 + && cbuf.len() + per_page_cbuf + 1 <= cmdlen + && txbuf.len() + max_tx_per_page <= swaplen + { + // Respect page boundaries + let page_offset = (current_addr as usize) % page_size; + let bytes_to_page_end = page_size - page_offset; + let n = remaining.min(bytes_to_page_end); + + // WREN via SPI_CMD_FAST + cbuf.push(spi_cmd::SELECT); + cbuf.push(spi_cmd::FAST); + cbuf.push(1); + cbuf.push(wren_opcode); + cbuf.push(spi_cmd::DESELECT); + + // PP via SPI_CMD_TXBUF (offset into swap buffer for this page) + let swap_offset = swapbuf + txbuf.len() as u32; + let txlen = (1 + addr_len + n) as u32; + cbuf.push(spi_cmd::SELECT); + cbuf.push(spi_cmd::TXBUF); + cbuf.extend_from_slice(&swap_offset.to_le_bytes()); + cbuf.extend_from_slice(&txlen.to_le_bytes()); + cbuf.push(spi_cmd::DESELECT); + + // Wait for page program to complete (on-SoC RDSR polling) + cbuf.push(spi_cmd::SELECT); + cbuf.push(spi_cmd::SPINOR_WAIT); + cbuf.push(spi_cmd::DESELECT); + + // TX data: PP opcode + address + page data + txbuf.push(pp_opcode); + if use_4byte { + txbuf.push((current_addr >> 24) as u8); + } + txbuf.push((current_addr >> 16) as u8); + txbuf.push((current_addr >> 8) as u8); + txbuf.push(current_addr as u8); + txbuf.extend_from_slice(&data[data_offset..data_offset + n]); + + current_addr += n as u32; + data_offset += n; + remaining -= n; + } + + cbuf.push(spi_cmd::END); + + // Upload all page data to swap buffer, then execute all pages at once + self.transport.fel_write(swapbuf, &txbuf)?; + chips::spi_run(&mut self.transport, &self.spi_info, &cbuf)?; + } + + Ok(()) + } + + /// Read flash data using SPI_CMD_FAST for the opcode+address header. + /// + /// For each chunk, embeds the READ command (opcode + address) directly in + /// the command buffer via SPI_CMD_FAST, then receives data via RXBUF. + /// This eliminates the separate FEL write for TX data that `spi_xfer` + /// would do, saving ~9 USB transfers per chunk. + fn fast_read(&mut self, addr: u32, buf: &mut [u8], use_4byte: bool) -> Result<()> { + let swapbuf = self.spi_info.swapbuf; + let swaplen = self.spi_info.swaplen as usize; + let read_opcode: u8 = 0x03; // READ + let addr_len: usize = if use_4byte { 4 } else { 3 }; + + let mut offset = 0usize; + let mut current_addr = addr; + + while offset < buf.len() { + let chunk_len = (buf.len() - offset).min(swaplen); + + // Build command buffer: SELECT + FAST(opcode+addr) + RXBUF + DESELECT + END + let fast_len = (1 + addr_len) as u8; + let mut cbuf = Vec::with_capacity(32); + cbuf.push(spi_cmd::SELECT); + cbuf.push(spi_cmd::FAST); + cbuf.push(fast_len); + cbuf.push(read_opcode); + if use_4byte { + cbuf.push((current_addr >> 24) as u8); + } + cbuf.push((current_addr >> 16) as u8); + cbuf.push((current_addr >> 8) as u8); + cbuf.push(current_addr as u8); + cbuf.push(spi_cmd::RXBUF); + cbuf.extend_from_slice(&swapbuf.to_le_bytes()); + cbuf.extend_from_slice(&(chunk_len as u32).to_le_bytes()); + cbuf.push(spi_cmd::DESELECT); + cbuf.push(spi_cmd::END); + + // Execute (no separate fel_write needed - TX is embedded in cmd buffer) + chips::spi_run(&mut self.transport, &self.spi_info, &cbuf)?; + + // Read result from swap buffer + let data = self.transport.fel_read(swapbuf, chunk_len)?; + buf[offset..offset + chunk_len].copy_from_slice(&data); + + offset += chunk_len; + current_addr += chunk_len as u32; + } + + Ok(()) + } +} + +// ============================================================================= +// SpiMaster implementation (for probe, status registers, WP, generic SPI) +// ============================================================================= + +impl SpiMaster for SunxiFel { + fn features(&self) -> SpiFeatures { + SpiFeatures::FOUR_BYTE_ADDR + } + + fn max_read_len(&self) -> usize { + self.spi_info.swaplen as usize + } + + fn max_write_len(&self) -> usize { + self.spi_info.swaplen as usize + } + + fn execute(&mut self, cmd: &mut SpiCommand<'_>) -> CoreResult<()> { + let header_len = cmd.header_len(); + let mut write_data = vec![0u8; header_len + cmd.write_data.len()]; + cmd.encode_header(&mut write_data); + write_data[header_len..].copy_from_slice(cmd.write_data); + + self.spi_xfer(&write_data, cmd.read_buf) + .map_err(|_| CoreError::ProgrammerError) + } + + fn native_erase_block( + &mut self, + opcode: u8, + addr: u32, + use_4byte: bool, + ) -> Option> { + Some( + self.erase_block_bytecode(opcode, addr, use_4byte) + .map_err(|_| CoreError::ProgrammerError), + ) + } + + fn delay_us(&mut self, us: u32) { + std::thread::sleep(std::time::Duration::from_micros(us as u64)); + } +} + +// ============================================================================= +// OpaqueMaster implementation (for fast bulk read/write via HybridFlashDevice) +// ============================================================================= + +impl OpaqueMaster for SunxiFel { + fn size(&self) -> usize { + // Size is determined by FlashContext after probing; HybridFlashDevice + // uses FlashContext::total_size(), not this method. + 0 + } + + fn read(&mut self, addr: u32, buf: &mut [u8]) -> CoreResult<()> { + self.fast_read(addr, buf, self.use_4byte_addr) + .map_err(|_| CoreError::ReadError { addr }) + } + + fn write(&mut self, addr: u32, data: &[u8]) -> CoreResult<()> { + // Batched page program with on-SoC busy-wait. + // Page size is always 256 for standard SPI NOR. + self.batched_write(addr, data, 256, self.use_4byte_addr) + .map_err(|_| CoreError::WriteError { addr }) + } + + fn erase(&mut self, _addr: u32, _len: u32) -> CoreResult<()> { + // Erase is handled by HybridFlashDevice through SpiMaster + // (with native_erase_block acceleration). Not used directly. + Err(CoreError::ProgrammerError) + } +} diff --git a/crates/rflasher-sunxi-fel/src/error.rs b/crates/rflasher-sunxi-fel/src/error.rs new file mode 100644 index 0000000..ba1a148 --- /dev/null +++ b/crates/rflasher-sunxi-fel/src/error.rs @@ -0,0 +1,40 @@ +//! Error types for the sunxi FEL programmer + +use std::fmt; + +/// Errors that can occur during FEL operations +#[derive(Debug)] +pub enum Error { + /// USB communication error + Usb(String), + /// FEL protocol error + Protocol(String), + /// Unsupported SoC + UnsupportedSoc(u32), + /// SPI initialization failed + SpiInitFailed, + /// SPI transfer failed + SpiTransferFailed, + /// No FEL device found + DeviceNotFound, +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::Usb(msg) => write!(f, "USB error: {}", msg), + Error::Protocol(msg) => write!(f, "FEL protocol error: {}", msg), + Error::UnsupportedSoc(id) => { + write!(f, "Unsupported SoC ID: 0x{:08x}", id) + } + Error::SpiInitFailed => write!(f, "SPI initialization failed"), + Error::SpiTransferFailed => write!(f, "SPI transfer failed"), + Error::DeviceNotFound => write!(f, "No Allwinner FEL device found"), + } + } +} + +impl std::error::Error for Error {} + +/// Result type for FEL operations +pub type Result = std::result::Result; diff --git a/crates/rflasher-sunxi-fel/src/lib.rs b/crates/rflasher-sunxi-fel/src/lib.rs new file mode 100644 index 0000000..fa2c3b0 --- /dev/null +++ b/crates/rflasher-sunxi-fel/src/lib.rs @@ -0,0 +1,63 @@ +//! rflasher-sunxi-fel - Allwinner sunxi FEL SPI NOR programmer support +//! +//! This crate provides support for programming SPI NOR flash through the +//! Allwinner FEL (USB boot) protocol. When an Allwinner SoC boots in FEL +//! mode, its BROM exposes a simple USB protocol for reading/writing memory +//! and executing code. This crate leverages that to drive the SoC's SPI +//! controller and program attached SPI NOR flash chips. +//! +//! # How it works +//! +//! 1. Connect to the device in FEL mode (USB VID:1F3A PID:EFE8) +//! 2. Query the SoC version to identify the chip family +//! 3. Upload a small SPI driver payload to the SoC's SRAM +//! 4. Drive the SPI bus by writing commands to a shared buffer and +//! executing the payload +//! +//! The SPI driver payload interprets a simple bytecode protocol that +//! handles chip select, data transfer, and SPINOR busy-wait operations. +//! +//! # Supported SoCs +//! +//! - Allwinner H2+/H3 (sun8iw7) +//! - Allwinner H5 (sun50iw2) +//! - Allwinner H6 (sun50iw6) +//! - Allwinner H616/H618 (sun50iw9) +//! - Allwinner A64 (sun50iw1) +//! - Allwinner R328 (sun8iw18) +//! - Allwinner D1/F133 (sun20iw1) +//! - Allwinner V3s/S3 (sun8iw12) +//! - Allwinner F1C100s/F1C200s (suniv) +//! - And more... +//! +//! # Example +//! +//! ```no_run +//! use rflasher_sunxi_fel::SunxiFel; +//! use rflasher_core::programmer::SpiMaster; +//! use rflasher_core::spi::{SpiCommand, opcodes}; +//! +//! let mut fel = SunxiFel::open()?; +//! println!("Connected to: {}", fel.soc_name()); +//! let mut id = [0u8; 3]; +//! let mut cmd = SpiCommand::read_reg(opcodes::RDID, &mut id); +//! fel.execute(&mut cmd)?; +//! println!("JEDEC ID: {:02X} {:02X} {:02X}", id[0], id[1], id[2]); +//! # Ok::<(), Box>(()) +//! ``` + +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(feature = "std")] +mod chips; +#[cfg(feature = "std")] +mod device; +#[cfg(feature = "std")] +mod error; +#[cfg(feature = "std")] +mod protocol; + +#[cfg(feature = "std")] +pub use device::SunxiFel; +#[cfg(feature = "std")] +pub use error::{Error, Result}; diff --git a/crates/rflasher-sunxi-fel/src/protocol.rs b/crates/rflasher-sunxi-fel/src/protocol.rs new file mode 100644 index 0000000..a205577 --- /dev/null +++ b/crates/rflasher-sunxi-fel/src/protocol.rs @@ -0,0 +1,230 @@ +//! FEL USB protocol implementation +//! +//! The FEL protocol communicates with the Allwinner BROM over USB bulk +//! transfers. It supports three core operations: +//! - Read memory at an address +//! - Write memory at an address +//! - Execute code at an address +//! +//! All multi-byte values are little-endian. + +use crate::error::{Error, Result}; +use nusb::transfer::{Buffer, Bulk, In, Out}; +use nusb::Endpoint; +use std::time::Duration; + +/// Allwinner FEL USB Vendor ID +pub const FEL_VID: u16 = 0x1f3a; +/// Allwinner FEL USB Product ID +pub const FEL_PID: u16 = 0xefe8; + +/// USB transfer timeout +const USB_TIMEOUT: Duration = Duration::from_secs(10); + +// FEL request types +const FEL_VERSION: u32 = 0x001; +const FEL_WRITE: u32 = 0x101; +const FEL_EXEC: u32 = 0x102; +const FEL_READ: u32 = 0x103; + +// USB request types +const USB_REQUEST_WRITE: u16 = 0x12; +const USB_REQUEST_READ: u16 = 0x11; + +/// FEL version information returned by the BROM +#[derive(Debug, Clone)] +pub struct FelVersion { + /// SoC ID + pub id: u32, + /// Firmware version + pub firmware: u32, + /// Protocol version + pub protocol: u16, + /// Data flag + pub dflag: u8, + /// Data length + pub dlength: u8, + /// Scratchpad address in SRAM + pub scratchpad: u32, +} + +/// Low-level FEL USB transport +pub struct FelTransport { + out_ep: Endpoint, + in_ep: Endpoint, + /// Max packet size for the IN endpoint + max_packet_size: usize, +} + +impl FelTransport { + pub fn new(out_ep: Endpoint, in_ep: Endpoint) -> Self { + let max_packet_size = in_ep.max_packet_size(); + Self { + out_ep, + in_ep, + max_packet_size, + } + } + + /// Round up to next multiple of max packet size (Linux usbfs requirement) + fn round_up_to_mps(&self, len: usize) -> usize { + let mps = self.max_packet_size; + if mps == 0 { + return len; + } + (len + mps - 1) / mps * mps + } + + fn usb_bulk_send(&mut self, data: &[u8]) -> Result<()> { + let max_chunk = 128 * 1024; + let mut offset = 0; + while offset < data.len() { + let chunk_size = (data.len() - offset).min(max_chunk); + let chunk = &data[offset..offset + chunk_size]; + let buf = Buffer::from(chunk.to_vec()); + self.out_ep.submit(buf); + let completion = self + .out_ep + .wait_next_complete(USB_TIMEOUT) + .ok_or_else(|| Error::Usb("bulk send timeout".into()))?; + completion + .status + .map_err(|e| Error::Usb(format!("bulk send: {}", e)))?; + offset += chunk_size; + } + Ok(()) + } + + fn usb_bulk_recv(&mut self, len: usize) -> Result> { + let mut result = Vec::with_capacity(len); + let mut remaining = len; + while remaining > 0 { + let alloc_size = self.round_up_to_mps(remaining); + let buf = Buffer::new(alloc_size); + self.in_ep.submit(buf); + let completion = self + .in_ep + .wait_next_complete(USB_TIMEOUT) + .ok_or_else(|| Error::Usb("bulk recv timeout".into()))?; + completion + .status + .map_err(|e| Error::Usb(format!("bulk recv: {}", e)))?; + let actual = completion.actual_len.min(remaining); + if actual == 0 { + return Err(Error::Usb("bulk recv: zero-length transfer".into())); + } + result.extend_from_slice(&completion.buffer[..actual]); + remaining -= actual; + } + Ok(result) + } + + /// Send USB request header (32 bytes, packed) + fn send_usb_request(&mut self, request_type: u16, length: u32) -> Result<()> { + let mut req = [0u8; 32]; + req[0..4].copy_from_slice(b"AWUC"); + req[8..12].copy_from_slice(&length.to_le_bytes()); + req[12..16].copy_from_slice(&0x0c000000u32.to_le_bytes()); + req[16..18].copy_from_slice(&request_type.to_le_bytes()); + req[18..22].copy_from_slice(&length.to_le_bytes()); + self.usb_bulk_send(&req) + } + + /// Read USB response (13 bytes, "AWUS" magic) + fn read_usb_response(&mut self) -> Result<()> { + let data = self.usb_bulk_recv(13)?; + if data.len() < 4 || &data[0..4] != b"AWUS" { + return Err(Error::Protocol("invalid USB response magic".into())); + } + Ok(()) + } + + fn usb_write(&mut self, data: &[u8]) -> Result<()> { + self.send_usb_request(USB_REQUEST_WRITE, data.len() as u32)?; + self.usb_bulk_send(data)?; + self.read_usb_response() + } + + fn usb_read(&mut self, len: usize) -> Result> { + self.send_usb_request(USB_REQUEST_READ, len as u32)?; + let data = self.usb_bulk_recv(len)?; + self.read_usb_response()?; + Ok(data) + } + + fn send_fel_request(&mut self, request: u32, addr: u32, length: u32) -> Result<()> { + let mut req = [0u8; 16]; + req[0..4].copy_from_slice(&request.to_le_bytes()); + req[4..8].copy_from_slice(&addr.to_le_bytes()); + req[8..12].copy_from_slice(&length.to_le_bytes()); + self.usb_write(&req) + } + + fn read_fel_status(&mut self) -> Result<()> { + self.usb_read(8)?; + Ok(()) + } + + /// Query the FEL version (SoC identification) + pub fn fel_version(&mut self) -> Result { + self.send_fel_request(FEL_VERSION, 0, 0)?; + let data = self.usb_read(32)?; + self.read_fel_status()?; + if data.len() < 32 { + return Err(Error::Protocol("version response too short".into())); + } + Ok(FelVersion { + id: u32::from_le_bytes([data[8], data[9], data[10], data[11]]), + firmware: u32::from_le_bytes([data[12], data[13], data[14], data[15]]), + protocol: u16::from_le_bytes([data[16], data[17]]), + dflag: data[18], + dlength: data[19], + scratchpad: u32::from_le_bytes([data[20], data[21], data[22], data[23]]), + }) + } + + /// Execute code at the given address + pub fn fel_exec(&mut self, addr: u32) -> Result<()> { + self.send_fel_request(FEL_EXEC, addr, 0)?; + self.read_fel_status() + } + + fn fel_read_raw(&mut self, addr: u32, len: usize) -> Result> { + self.send_fel_request(FEL_READ, addr, len as u32)?; + let data = self.usb_read(len)?; + self.read_fel_status()?; + Ok(data) + } + + fn fel_write_raw(&mut self, addr: u32, data: &[u8]) -> Result<()> { + self.send_fel_request(FEL_WRITE, addr, data.len() as u32)?; + self.usb_write(data)?; + self.read_fel_status() + } + + /// Read memory (with chunking for large transfers) + pub fn fel_read(&mut self, addr: u32, len: usize) -> Result> { + let mut result = Vec::with_capacity(len); + let mut offset = 0u32; + let mut remaining = len; + while remaining > 0 { + let n = remaining.min(65536); + let chunk = self.fel_read_raw(addr + offset, n)?; + result.extend_from_slice(&chunk); + offset += n as u32; + remaining -= n; + } + Ok(result) + } + + /// Write memory (with chunking for large transfers) + pub fn fel_write(&mut self, addr: u32, data: &[u8]) -> Result<()> { + let mut offset = 0usize; + while offset < data.len() { + let n = (data.len() - offset).min(65536); + self.fel_write_raw(addr + offset as u32, &data[offset..offset + n])?; + offset += n; + } + Ok(()) + } +} From 2eb5d0c2947a2bba492e76fc4b815c69caab306d Mon Sep 17 00:00:00 2001 From: Arthur Heymans Date: Fri, 20 Mar 2026 15:53:07 +0100 Subject: [PATCH 2/4] refactor: move erase from SpiMaster to OpaqueMaster interface Following flashprog's architecture where erase is a first-class operation on the opaque interface (programmers that don't expose raw bus access must provide their own erase), not on the SPI interface (where generic code constructs erase sequences from raw SPI commands). - Remove native_erase_block from SpiMaster trait - HybridFlashDevice::erase() now tries OpaqueMaster::erase() first; falls back to SPI-based WREN+opcode+RDSR polling if it returns Err - SunxiFel::OpaqueMaster::erase() uses FAST+SPINOR_WAIT bytecodes with automatic block size selection (64K/32K/4K) - Dediprog continues to return Err from OpaqueMaster::erase(), triggering the SPI fallback (no behavior change) - SpiFlashDevice::erase() reverted to the original generic path (no native_erase_block concept needed for pure-SPI programmers) --- .../rflasher-core/src/flash/hybrid_device.rs | 48 +++++++++++-------- crates/rflasher-core/src/flash/spi_device.rs | 27 ++++------- crates/rflasher-core/src/programmer/traits.rs | 29 ----------- crates/rflasher-sunxi-fel/src/device.rs | 43 ++++++++++------- 4 files changed, 63 insertions(+), 84 deletions(-) diff --git a/crates/rflasher-core/src/flash/hybrid_device.rs b/crates/rflasher-core/src/flash/hybrid_device.rs index 97c6187..751564f 100644 --- a/crates/rflasher-core/src/flash/hybrid_device.rs +++ b/crates/rflasher-core/src/flash/hybrid_device.rs @@ -175,15 +175,30 @@ impl FlashDevice for HybridFlashDevice { } // ========================================================================= - // Erase: use SpiMaster (no bulk erase command on Dediprog) + // Erase: try OpaqueMaster first, fall back to SpiMaster + // + // Following flashprog's architecture: erase is a first-class operation + // on the opaque interface. Programmers with firmware-accelerated erase + // (e.g., SPI_CMD_SPINOR_WAIT) implement OpaqueMaster::erase(). Those + // without (e.g., Dediprog) return Err, triggering the SPI fallback. // ========================================================================= async fn erase(&mut self, addr: u32, len: u32) -> Result<()> { - let ctx = self.context(); - if !ctx.is_valid_range(addr, len as usize) { + // Bounds check (borrow ctx briefly, then drop before mutable borrow) + if !self.context().is_valid_range(addr, len as usize) { return Err(Error::AddressOutOfBounds); } + // Try opaque erase first — the programmer handles everything internally + // (block selection, busy-wait, etc.). If it returns Ok, we're done. + // Programmers without firmware erase (e.g., Dediprog) return Err, + // triggering the SPI fallback below. + if OpaqueMaster::erase(&mut self.master, addr, len).await.is_ok() { + return Ok(()); + } + + // Opaque erase not supported — fall back to SPI-based erase + let ctx = self.context(); let erase_block = select_erase_block(ctx.chip.erase_blocks(), addr, len) .ok_or(Error::InvalidAlignment)?; @@ -217,24 +232,15 @@ impl FlashDevice for HybridFlashDevice { .block_size_at_offset(offset_in_layout) .unwrap_or(max_block_size); - // Try native erase first (on-device busy-wait, e.g. SPI_CMD_SPINOR_WAIT). - // Falls back to generic WREN + erase + host-side RDSR polling. - let result = if let Some(r) = - self.master() - .native_erase_block(opcode, current_addr, use_4byte && use_native) - { - r - } else { - protocol::erase_block( - self.master(), - opcode, - current_addr, - use_4byte && use_native, - poll_delay_us, - timeout_us, - ) - .await - }; + let result = protocol::erase_block( + self.master(), + opcode, + current_addr, + use_4byte && use_native, + poll_delay_us, + timeout_us, + ) + .await; if result.is_err() { if use_4byte && !use_native { diff --git a/crates/rflasher-core/src/flash/spi_device.rs b/crates/rflasher-core/src/flash/spi_device.rs index f4b0bbe..68d1b04 100644 --- a/crates/rflasher-core/src/flash/spi_device.rs +++ b/crates/rflasher-core/src/flash/spi_device.rs @@ -329,24 +329,15 @@ impl FlashDevice for SpiFlashDevice { .block_size_at_offset(offset_in_layout) .unwrap_or(max_block_size); - // Try native erase first (on-device busy-wait, e.g. SPI_CMD_SPINOR_WAIT). - // Falls back to generic WREN + erase + host-side RDSR polling. - let result = if let Some(r) = - self.master() - .native_erase_block(opcode, current_addr, use_4byte && use_native) - { - r - } else { - protocol::erase_block( - self.master(), - opcode, - current_addr, - use_4byte && use_native, - poll_delay_us, - timeout_us, - ) - .await - }; + let result = protocol::erase_block( + self.master(), + opcode, + current_addr, + use_4byte && use_native, + poll_delay_us, + timeout_us, + ) + .await; if result.is_err() { if use_4byte && !use_native { diff --git a/crates/rflasher-core/src/programmer/traits.rs b/crates/rflasher-core/src/programmer/traits.rs index 4078b57..ed5bdf7 100644 --- a/crates/rflasher-core/src/programmer/traits.rs +++ b/crates/rflasher-core/src/programmer/traits.rs @@ -127,26 +127,6 @@ pub trait SpiMaster { true } - /// Native erase block with on-device busy-waiting. - /// - /// Programmers with firmware-level busy-wait (e.g., `SPI_CMD_SPINOR_WAIT`) - /// should override this to avoid host-side status register polling. - /// - /// Returns `None` if not supported (caller falls back to the generic path). - /// - /// # Arguments - /// * `opcode` - Erase opcode (e.g., 0x20 for 4KB, 0xD8 for 64KB) - /// * `addr` - Block address to erase - /// * `use_4byte` - Whether to use 4-byte addressing - fn native_erase_block( - &mut self, - _opcode: u8, - _addr: u32, - _use_4byte: bool, - ) -> Option> { - None - } - /// Delay for the specified number of microseconds async fn delay_us(&mut self, us: u32); } @@ -207,15 +187,6 @@ impl SpiMaster for alloc::boxed::Box { (**self).probe_opcode(opcode) } - fn native_erase_block( - &mut self, - opcode: u8, - addr: u32, - use_4byte: bool, - ) -> Option> { - (**self).native_erase_block(opcode, addr, use_4byte) - } - fn delay_us(&mut self, us: u32) { (**self).delay_us(us) } diff --git a/crates/rflasher-sunxi-fel/src/device.rs b/crates/rflasher-sunxi-fel/src/device.rs index c840303..dadf6d5 100644 --- a/crates/rflasher-sunxi-fel/src/device.rs +++ b/crates/rflasher-sunxi-fel/src/device.rs @@ -484,18 +484,6 @@ impl SpiMaster for SunxiFel { .map_err(|_| CoreError::ProgrammerError) } - fn native_erase_block( - &mut self, - opcode: u8, - addr: u32, - use_4byte: bool, - ) -> Option> { - Some( - self.erase_block_bytecode(opcode, addr, use_4byte) - .map_err(|_| CoreError::ProgrammerError), - ) - } - fn delay_us(&mut self, us: u32) { std::thread::sleep(std::time::Duration::from_micros(us as u64)); } @@ -524,9 +512,32 @@ impl OpaqueMaster for SunxiFel { .map_err(|_| CoreError::WriteError { addr }) } - fn erase(&mut self, _addr: u32, _len: u32) -> CoreResult<()> { - // Erase is handled by HybridFlashDevice through SpiMaster - // (with native_erase_block acceleration). Not used directly. - Err(CoreError::ProgrammerError) + fn erase(&mut self, addr: u32, len: u32) -> CoreResult<()> { + // Erase using on-SoC bytecodes (FAST for WREN + erase, SPINOR_WAIT). + // Pick the largest erase block size that aligns with the range. + // Standard SPI NOR erase sizes: 4KB (0x20), 32KB (0x52), 64KB (0xD8). + let use_4byte = self.use_4byte_addr; + let end = addr + len; + let mut current = addr; + + while current < end { + let remaining = end - current; + + // Pick the largest aligned erase block that fits + let (opcode, block_size) = if remaining >= 65536 && current % 65536 == 0 { + (0xD8u8, 65536u32) // 64KB block erase + } else if remaining >= 32768 && current % 32768 == 0 { + (0x52u8, 32768u32) // 32KB block erase + } else { + (0x20u8, 4096u32) // 4KB sector erase + }; + + self.erase_block_bytecode(opcode, current, use_4byte) + .map_err(|_| CoreError::ProgrammerError)?; + + current += block_size; + } + + Ok(()) } } From 0f15398054b5b420778f3a97c7d8fd13bad03e27 Mon Sep 17 00:00:00 2001 From: Arthur Heymans Date: Fri, 20 Mar 2026 15:57:08 +0100 Subject: [PATCH 3/4] refactor: use chip erase blocks from RON/SFDP in OpaqueMaster::erase() Instead of hardcoding erase opcodes (0xD8/0x52/0x20), SunxiFel now stores the chip's actual erase block table after probing and uses select_erase_block() to pick the correct opcode and block size. This also completes the removal of native_erase_block from SpiMaster. HybridFlashDevice::erase() now tries OpaqueMaster::erase() first (which uses the chip's real erase table + SPINOR_WAIT bytecodes), falling back to SPI-based erase if it returns Err (as Dediprog does). - Re-export select_erase_block from rflasher_core::flash - Add set_erase_blocks() on SunxiFel, called after probe in registry - OpaqueMaster::erase() returns Err if no erase blocks configured, gracefully falling back to the SPI path --- crates/rflasher-core/src/flash/mod.rs | 2 +- crates/rflasher-flash/src/registry.rs | 9 +++-- crates/rflasher-sunxi-fel/src/device.rs | 54 ++++++++++++++++--------- 3 files changed, 42 insertions(+), 23 deletions(-) diff --git a/crates/rflasher-core/src/flash/mod.rs b/crates/rflasher-core/src/flash/mod.rs index 151fa31..3725ba1 100644 --- a/crates/rflasher-core/src/flash/mod.rs +++ b/crates/rflasher-core/src/flash/mod.rs @@ -55,7 +55,7 @@ pub use spi_device::SpiFlashDevice; // Re-export low-level SPI operations (work with SpiMaster directly) // For high-level operations that work with any FlashDevice, use the `unified` module -pub use operations::{read, write}; +pub use operations::{read, select_erase_block, write}; // Re-export detailed probe result #[cfg(feature = "std")] diff --git a/crates/rflasher-flash/src/registry.rs b/crates/rflasher-flash/src/registry.rs index 7f77c8b..0fb62e5 100644 --- a/crates/rflasher-flash/src/registry.rs +++ b/crates/rflasher-flash/src/registry.rs @@ -903,12 +903,13 @@ fn open_sunxi_fel( let chip_info = ChipInfo::from(result); let ctx = rflasher_core::flash::FlashContext::new(chip_info.chip.clone().unwrap()); - // Configure 4-byte addressing for OpaqueMaster if needed (flash >16MB) + // Configure OpaqueMaster with chip info discovered during probe master.set_use_4byte_addr(ctx.total_size() > 16 * 1024 * 1024); + master.set_erase_blocks(ctx.chip.erase_blocks().to_vec()); - // Use HybridFlashDevice: OpaqueMaster for fast bulk read/write (batched SPI - // commands with on-SoC busy-wait), SpiMaster for erase (with native_erase_block), - // status register access, and write protection + // Use HybridFlashDevice: OpaqueMaster for fast bulk read/write/erase + // (batched SPI commands with on-SoC busy-wait), SpiMaster for WP and + // status register access let device = HybridFlashDevice::new(master, ctx); Ok(FlashHandle::with_chip_info(Box::new(device), chip_info)) } diff --git a/crates/rflasher-sunxi-fel/src/device.rs b/crates/rflasher-sunxi-fel/src/device.rs index dadf6d5..d0a00d4 100644 --- a/crates/rflasher-sunxi-fel/src/device.rs +++ b/crates/rflasher-sunxi-fel/src/device.rs @@ -19,12 +19,14 @@ //! xfel's `spinor_helper_write` approach. //! //! Use with `HybridFlashDevice` for optimal performance: -//! - read/write → OpaqueMaster (fast bulk path) -//! - erase/WP → SpiMaster (with `native_erase_block` for on-SoC busy-wait) +//! - read/write/erase → OpaqueMaster (firmware-accelerated) +//! - WP/status regs → SpiMaster (generic SPI commands) use nusb::transfer::{Bulk, In, Out}; use nusb::MaybeFuture; +use rflasher_core::chip::EraseBlock; use rflasher_core::error::{Error as CoreError, Result as CoreResult}; +use rflasher_core::flash::select_erase_block; use rflasher_core::programmer::{OpaqueMaster, SpiFeatures, SpiMaster}; use rflasher_core::spi::SpiCommand; @@ -42,6 +44,10 @@ pub struct SunxiFel { /// Whether to use 4-byte addressing for OpaqueMaster read/write. /// Set after probing via `set_use_4byte_addr()` when the flash is >16MB. use_4byte_addr: bool, + /// Chip erase block table, set after probing via `set_erase_blocks()`. + /// Used by `OpaqueMaster::erase()` to select the correct opcode and + /// block size from the chip's actual capabilities (from RON database or SFDP). + erase_blocks: Vec, } impl SunxiFel { @@ -136,9 +142,19 @@ impl SunxiFel { spi_info, _interface: interface, use_4byte_addr: false, + erase_blocks: Vec::new(), }) } + /// Set the chip's erase block table for `OpaqueMaster::erase()`. + /// + /// Call this after probing with the chip's erase blocks from the RON + /// database or SFDP. Without this, `OpaqueMaster::erase()` will return + /// an error and the hybrid adapter falls back to SPI-based erase. + pub fn set_erase_blocks(&mut self, blocks: Vec) { + self.erase_blocks = blocks; + } + /// Set whether to use 4-byte addressing for bulk read/write operations. /// /// Call this after probing if the flash chip requires 4-byte addressing @@ -513,26 +529,28 @@ impl OpaqueMaster for SunxiFel { } fn erase(&mut self, addr: u32, len: u32) -> CoreResult<()> { - // Erase using on-SoC bytecodes (FAST for WREN + erase, SPINOR_WAIT). - // Pick the largest erase block size that aligns with the range. - // Standard SPI NOR erase sizes: 4KB (0x20), 32KB (0x52), 64KB (0xD8). + // Use the chip's actual erase block table (from RON/SFDP) to select + // the right opcode and block size. If no erase blocks are configured, + // return Err so the hybrid adapter falls back to SPI-based erase. + if self.erase_blocks.is_empty() { + return Err(CoreError::ProgrammerError); + } + + let erase_block = + select_erase_block(&self.erase_blocks, addr, len).ok_or(CoreError::ProgrammerError)?; + let use_4byte = self.use_4byte_addr; - let end = addr + len; + let max_block_size = erase_block.max_block_size(); let mut current = addr; + let end = addr + len; while current < end { - let remaining = end - current; - - // Pick the largest aligned erase block that fits - let (opcode, block_size) = if remaining >= 65536 && current % 65536 == 0 { - (0xD8u8, 65536u32) // 64KB block erase - } else if remaining >= 32768 && current % 32768 == 0 { - (0x52u8, 32768u32) // 32KB block erase - } else { - (0x20u8, 4096u32) // 4KB sector erase - }; - - self.erase_block_bytecode(opcode, current, use_4byte) + let offset_in_layout = current - addr; + let block_size = erase_block + .block_size_at_offset(offset_in_layout) + .unwrap_or(max_block_size); + + self.erase_block_bytecode(erase_block.opcode, current, use_4byte) .map_err(|_| CoreError::ProgrammerError)?; current += block_size; From dae7fd0ef5db5778b774480fc25dc00d8deb3db1 Mon Sep 17 00:00:00 2001 From: Arthur Heymans Date: Fri, 20 Mar 2026 15:59:31 +0100 Subject: [PATCH 4/4] fix: resolve rustfmt and clippy CI failures - rustfmt: reformat chained method call in hybrid_device.rs - clippy::int_plus_one: simplify cbuf bounds check to < instead of + 1 <= - clippy::manual_div_ceil: use .div_ceil() in round_up_to_mps --- crates/rflasher-core/src/flash/hybrid_device.rs | 5 ++++- crates/rflasher-sunxi-fel/src/device.rs | 2 +- crates/rflasher-sunxi-fel/src/protocol.rs | 2 +- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/rflasher-core/src/flash/hybrid_device.rs b/crates/rflasher-core/src/flash/hybrid_device.rs index 751564f..1157c8c 100644 --- a/crates/rflasher-core/src/flash/hybrid_device.rs +++ b/crates/rflasher-core/src/flash/hybrid_device.rs @@ -193,7 +193,10 @@ impl FlashDevice for HybridFlashDevice { // (block selection, busy-wait, etc.). If it returns Ok, we're done. // Programmers without firmware erase (e.g., Dediprog) return Err, // triggering the SPI fallback below. - if OpaqueMaster::erase(&mut self.master, addr, len).await.is_ok() { + if OpaqueMaster::erase(&mut self.master, addr, len) + .await + .is_ok() + { return Ok(()); } diff --git a/crates/rflasher-sunxi-fel/src/device.rs b/crates/rflasher-sunxi-fel/src/device.rs index d0a00d4..c251fca 100644 --- a/crates/rflasher-sunxi-fel/src/device.rs +++ b/crates/rflasher-sunxi-fel/src/device.rs @@ -367,7 +367,7 @@ impl SunxiFel { // Pack as many pages as will fit (matching xfel's loop bounds: // clen < cmdlen - 19 - 1 AND txlen < swaplen - granularity - addr_overhead) while remaining > 0 - && cbuf.len() + per_page_cbuf + 1 <= cmdlen + && cbuf.len() + per_page_cbuf < cmdlen && txbuf.len() + max_tx_per_page <= swaplen { // Respect page boundaries diff --git a/crates/rflasher-sunxi-fel/src/protocol.rs b/crates/rflasher-sunxi-fel/src/protocol.rs index a205577..0ce04d4 100644 --- a/crates/rflasher-sunxi-fel/src/protocol.rs +++ b/crates/rflasher-sunxi-fel/src/protocol.rs @@ -72,7 +72,7 @@ impl FelTransport { if mps == 0 { return len; } - (len + mps - 1) / mps * mps + len.div_ceil(mps) * mps } fn usb_bulk_send(&mut self, data: &[u8]) -> Result<()> {