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..1157c8c 100644 --- a/crates/rflasher-core/src/flash/hybrid_device.rs +++ b/crates/rflasher-core/src/flash/hybrid_device.rs @@ -175,15 +175,33 @@ 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)?; 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/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..0fb62e5 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,41 @@ 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 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/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)) +} + // Programmer information and listing /// Information about a programmer pub struct ProgrammerInfo { @@ -966,6 +1016,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..c251fca --- /dev/null +++ b/crates/rflasher-sunxi-fel/src/device.rs @@ -0,0 +1,561 @@ +//! 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/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; + +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, + /// 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 { + /// 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, + 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 + /// (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 < 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 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<()> { + // 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 max_block_size = erase_block.max_block_size(); + let mut current = addr; + let end = addr + len; + + while current < end { + 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; + } + + Ok(()) + } +} 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..0ce04d4 --- /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.div_ceil(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(()) + } +}