diff --git a/driver/BUILD.gn b/driver/BUILD.gn index 03486df5e..7f149097d 100644 --- a/driver/BUILD.gn +++ b/driver/BUILD.gn @@ -20,6 +20,7 @@ build_rust("blueos_driver") { deps = [ "//external/vendor/bitflags-2.10.0:bitflags", "//external/vendor/cfg-if-1.0.4:cfg_if", + "//external/vendor/embedded-hal-1.0.0:embedded_hal", "//external/vendor/safe-mmio-0.2.5:safe_mmio", "//external/vendor/spin-0.9.8:spin", "//external/vendor/tock-registers-0.9.0:tock_registers", diff --git a/driver/src/lib.rs b/driver/src/lib.rs index d279dbbaa..c599d5e5f 100644 --- a/driver/src/lib.rs +++ b/driver/src/lib.rs @@ -16,6 +16,7 @@ #![feature(const_nonnull_new)] pub mod clock_control; +pub mod gpio; pub mod hwinfo; pub mod gpio; pub mod i2c; diff --git a/kconfig/config/qemu_virt64_aarch64/coverage/defconfig b/kconfig/config/qemu_virt64_aarch64/coverage/defconfig index 0ad2cb053..a6ee3ecf7 100644 --- a/kconfig/config/qemu_virt64_aarch64/coverage/defconfig +++ b/kconfig/config/qemu_virt64_aarch64/coverage/defconfig @@ -61,6 +61,9 @@ CONFIG_DNS_MAX_RESULT_COUNT=1 CONFIG_DNS_MAX_SERVER_COUNT=1 # end of smoltcp TCP/IP Stack Configuration +CONFIG_ENABLE_BLOCK=y +CONFIG_FATFS=y + # # os adapter configuration # diff --git a/kconfig/config/qemu_virt64_aarch64/debug/defconfig b/kconfig/config/qemu_virt64_aarch64/debug/defconfig index 8d21f117c..5efa26337 100644 --- a/kconfig/config/qemu_virt64_aarch64/debug/defconfig +++ b/kconfig/config/qemu_virt64_aarch64/debug/defconfig @@ -62,6 +62,9 @@ CONFIG_DNS_MAX_RESULT_COUNT=1 CONFIG_DNS_MAX_SERVER_COUNT=1 # end of smoltcp TCP/IP Stack Configuration +CONFIG_ENABLE_BLOCK=y +CONFIG_FATFS=y + # # os adapter configuration # diff --git a/kconfig/config/qemu_virt64_aarch64/release/defconfig b/kconfig/config/qemu_virt64_aarch64/release/defconfig index 8d21f117c..5efa26337 100644 --- a/kconfig/config/qemu_virt64_aarch64/release/defconfig +++ b/kconfig/config/qemu_virt64_aarch64/release/defconfig @@ -62,6 +62,9 @@ CONFIG_DNS_MAX_RESULT_COUNT=1 CONFIG_DNS_MAX_SERVER_COUNT=1 # end of smoltcp TCP/IP Stack Configuration +CONFIG_ENABLE_BLOCK=y +CONFIG_FATFS=y + # # os adapter configuration # diff --git a/kconfig/config/seeed_xiao_esp32c3/debug/defconfig b/kconfig/config/seeed_xiao_esp32c3/debug/defconfig index 6e5739bf5..8f2150a56 100644 --- a/kconfig/config/seeed_xiao_esp32c3/debug/defconfig +++ b/kconfig/config/seeed_xiao_esp32c3/debug/defconfig @@ -32,4 +32,6 @@ CONFIG_SERIAL_TX_FIFO_SIZE=256 CONFIG_ENABLE_VFS=y CONFIG_ENABLE_NET=n CONFIG_PROCFS=n +CONFIG_ENABLE_BLOCK=y +CONFIG_FATFS=y CONFIG_UNITTEST_THREAD_NUM=16 diff --git a/kconfig/config/seeed_xiao_esp32c3/release/defconfig b/kconfig/config/seeed_xiao_esp32c3/release/defconfig index c29c801c1..f3bc29c19 100644 --- a/kconfig/config/seeed_xiao_esp32c3/release/defconfig +++ b/kconfig/config/seeed_xiao_esp32c3/release/defconfig @@ -32,4 +32,6 @@ CONFIG_SERIAL_TX_FIFO_SIZE=256 CONFIG_ENABLE_VFS=y CONFIG_ENABLE_NET=n CONFIG_PROCFS=n +CONFIG_ENABLE_BLOCK=y +CONFIG_FATFS=y CONFIG_UNITTEST_THREAD_NUM=16 diff --git a/kernel/src/Kconfig b/kernel/src/Kconfig index a576f08b2..9148375c1 100644 --- a/kernel/src/Kconfig +++ b/kernel/src/Kconfig @@ -45,6 +45,15 @@ config THREAD_STATS default n bool "Enable statistics of thread" +config ENABLE_BLOCK + default n + bool "Enable block device support" + +config FATFS + default n + bool "Enable FAT filesystem" + depends on ENABLE_BLOCK + # os adapter configuration menu "os adapter configuration" choice diff --git a/kernel/src/boards/qemu_virt64_aarch64/init.rs b/kernel/src/boards/qemu_virt64_aarch64/init.rs index c98c81b96..d849f81cf 100644 --- a/kernel/src/boards/qemu_virt64_aarch64/init.rs +++ b/kernel/src/boards/qemu_virt64_aarch64/init.rs @@ -94,6 +94,15 @@ pub const DRAM_BASE: usize = mmu::kernel_phys_to_virt(0x4000_0000); crate::define_pin_states!(None); +#[cfg(fatfs)] +pub const BLOCK_STORAGE_DEVICE_NAME: &str = "virt-storage"; +#[cfg(fatfs)] +pub const BLOCK_STORAGE_MOUNT_POINT: &str = "fat"; + +// virtio block device is registered in virtio::init_virtio(); no extra init. +#[cfg(enable_block)] +pub(crate) fn init_block_devices() {} + pub struct TimerIrq; impl IsrDesc for TimerIrq { fn service_isr(&self) { diff --git a/kernel/src/boards/seeed_xiao_esp32c3/mod.rs b/kernel/src/boards/seeed_xiao_esp32c3/mod.rs index 4062bbc66..2d06b4cb7 100644 --- a/kernel/src/boards/seeed_xiao_esp32c3/mod.rs +++ b/kernel/src/boards/seeed_xiao_esp32c3/mod.rs @@ -145,10 +145,44 @@ crate::define_peripheral! { blueos_driver::uart::esp32_usb_serial::Esp32UsbSerial::new()), (intc, blueos_driver::interrupt_controller::esp32_intc::Esp32Intc, blueos_driver::interrupt_controller::esp32_intc::Esp32Intc::new(0x600c_2000)), + (spi2, blueos_driver::spi::esp32_spi2::Esp32Spi2, + blueos_driver::spi::esp32_spi2::Esp32Spi2::new()), } crate::define_pin_states!(None); +pub const BLOCK_STORAGE_DEVICE_NAME: &str = "flash-storage"; +pub const BLOCK_STORAGE_MOUNT_POINT: &str = "data"; + +#[cfg(enable_block)] +pub(crate) fn init_block_devices() { + use crate::devices::{spi_core::block_spi::BlockSpiBus, storage::spi_flash}; + use blueos_driver::{ + gpio::esp32_gpio::Esp32GpioOutputPin, pinctrl::esp32_pinctrl::Esp32IoMuxPinctrl, + }; + use blueos_hal::pinctrl::AlterFuncPin; + use embedded_hal_bus::spi::ExclusiveDevice; + + // SPI2 pins: SCK=GPIO8, MISO=GPIO9, MOSI=GPIO10, CS=GPIO5. + const PIN_STATES: [Esp32IoMuxPinctrl; 4] = [ + Esp32IoMuxPinctrl::new(8, 1, false, false, false, 2, Some(63), None, false), // SCK + Esp32IoMuxPinctrl::new(9, 1, true, false, false, 2, None, Some(64), false), // MISO + Esp32IoMuxPinctrl::new(10, 1, false, false, false, 2, Some(65), None, false), // MOSI + Esp32IoMuxPinctrl::new(5, 1, false, true, false, 2, None, None, true), // CS + ]; + for pin in PIN_STATES { + pin.init(); + } + + let spi2 = get_device!(spi2); + let bus = BlockSpiBus::new(spi2).expect("Failed to configure SPI2 for flash"); + let cs = Esp32GpioOutputPin::<5>::new(); + let spi_dev = ExclusiveDevice::new(bus, cs, crate::sync::KernelDelay) + .expect("Failed to create SPI flash device"); + spi_flash::init_spi_flash(spi_dev, BLOCK_STORAGE_DEVICE_NAME) + .expect("SPI flash initialization failed"); +} + #[inline(always)] pub(crate) fn send_ipi(_hart: usize) {} diff --git a/kernel/src/boot.rs b/kernel/src/boot.rs index 9cb1f6196..1a9f567ca 100644 --- a/kernel/src/boot.rs +++ b/kernel/src/boot.rs @@ -149,6 +149,8 @@ extern "C" fn init() { net::init(); net::net_manager::init(); } + #[cfg(enable_block)] + boards::init_block_devices(); #[cfg(enable_vfs)] init_vfs(); // it's an bug in fact, but at now we use a workaround let newlib do the c++ runtime initialization diff --git a/kernel/src/devices/block/mod.rs b/kernel/src/devices/block/mod.rs index 514901227..a1403c721 100644 --- a/kernel/src/devices/block/mod.rs +++ b/kernel/src/devices/block/mod.rs @@ -13,26 +13,37 @@ // limitations under the License. use crate::{ - devices::{virtio::VirtioHal, Device, DeviceClass, DeviceId, DeviceManager}, + devices::{Device, DeviceClass, DeviceId, DeviceManager}, sync::SpinLock, }; use alloc::{string::String, sync::Arc, vec}; use core::cmp::min; use embedded_io::{Error as IOError, ErrorKind}; + +#[cfg(enable_block)] +use crate::devices::storage::spi_flash::FlashBlockError; +#[cfg(enable_block)] +use crate::devices::storage::spi_flash_cmd::FlashError; + +#[cfg(virtio)] +use crate::devices::virtio::VirtioHal; +#[cfg(virtio)] use virtio_drivers::{ device::blk::{VirtIOBlk, SECTOR_SIZE}, transport::SomeTransport, Hal, }; +#[cfg(virtio)] pub const VIRTUAL_STORAGE_NAME: &str = "virt-storage"; #[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)] pub enum BlockError { - #[error("Error from the drviver: {0}")] + #[error("Error from the driver: {0}")] Driver(#[from] T), } +#[cfg(virtio)] impl embedded_io::Error for BlockError { fn kind(&self) -> ErrorKind { match self { @@ -53,6 +64,25 @@ impl embedded_io::Error for BlockError { } } +#[cfg(enable_block)] +impl embedded_io::Error for BlockError { + fn kind(&self) -> ErrorKind { + match self { + BlockError::Driver(error) => match error { + FlashBlockError::Flash(flash_err) => match flash_err { + FlashError::Spi(_) => ErrorKind::Other, + FlashError::NotReady => ErrorKind::Other, + FlashError::JedecMismatch { .. } => ErrorKind::NotFound, + FlashError::WriteEnableFailed => ErrorKind::Other, + FlashError::Timeout => ErrorKind::TimedOut, + FlashError::AddrOverflow { .. } => ErrorKind::InvalidInput, + FlashError::InvalidParam(_) => ErrorKind::InvalidInput, + }, + }, + } + } +} + pub trait ErrorType { type Error: embedded_io::Error; } @@ -70,10 +100,12 @@ pub trait BlockDriverOps: Send + Sync + ErrorType { fn flush(&mut self) -> Result<(), Self::Error>; } +#[cfg(virtio)] impl ErrorType for VirtIOBlk> { type Error = BlockError; // : io::Error } +#[cfg(virtio)] impl BlockDriverOps for VirtIOBlk> { fn capacity(&self) -> u64 { self.capacity() @@ -105,10 +137,14 @@ impl BlockDriverOps for VirtIOBlk> { } } +#[cfg(virtio)] pub fn init_virtio_block( driver: VirtIOBlk>, ) -> Result<(), ErrorKind> { - let block = Block::new(VIRTUAL_STORAGE_NAME, Arc::new(SpinLock::new(driver))); + let block = Block::, SECTOR_SIZE>::new( + VIRTUAL_STORAGE_NAME, + Arc::new(SpinLock::new(driver)), + ); DeviceManager::get().register_device(String::from(VIRTUAL_STORAGE_NAME), Arc::new(block)) } @@ -118,7 +154,7 @@ pub struct Block { total_size: u64, // in bytes } -impl Block { +impl Block { pub fn new(name: &str, driver: Arc>>) -> Self { let total_size = { let capacity = driver.lock().capacity(); @@ -132,7 +168,7 @@ impl Block { } } -impl Device for Block { +impl Device for Block { fn name(&self) -> String { self.name.clone() } @@ -246,6 +282,64 @@ impl Device for Block { } } +#[cfg(enable_block)] +#[cfg(test)] +mod flash_tests { + use super::*; + use crate::devices::storage::{spi_flash::FlashBlockError, spi_flash_cmd::FlashError}; + use blueos_test_macro::test; + use embedded_hal::spi::ErrorKind as SpiErrorKind; + use embedded_io::Error as IOError; + + #[test] + fn test_flash_error_spi_maps_to_other() { + let err = BlockError::Driver(FlashBlockError::Flash(FlashError::Spi(SpiErrorKind::Other))); + assert_eq!(IOError::kind(&err), ErrorKind::Other); + } + + #[test] + fn test_flash_error_not_ready_maps_to_other() { + let err = BlockError::Driver(FlashBlockError::Flash(FlashError::NotReady)); + assert_eq!(IOError::kind(&err), ErrorKind::Other); + } + + #[test] + fn test_flash_error_jedec_mismatch_maps_to_not_found() { + let err = BlockError::Driver(FlashBlockError::Flash(FlashError::JedecMismatch { + expected: 0xEF4018, + got: 0x000000, + })); + assert_eq!(IOError::kind(&err), ErrorKind::NotFound); + } + + #[test] + fn test_flash_error_write_enable_failed_maps_to_other() { + let err = BlockError::Driver(FlashBlockError::Flash(FlashError::WriteEnableFailed)); + assert_eq!(IOError::kind(&err), ErrorKind::Other); + } + + #[test] + fn test_flash_error_timeout_maps_to_timed_out() { + let err = BlockError::Driver(FlashBlockError::Flash(FlashError::Timeout)); + assert_eq!(IOError::kind(&err), ErrorKind::TimedOut); + } + + #[test] + fn test_flash_error_addr_overflow_maps_to_invalid_input() { + let err = BlockError::Driver(FlashBlockError::Flash(FlashError::AddrOverflow { + addr: 0x01000000, + })); + assert_eq!(IOError::kind(&err), ErrorKind::InvalidInput); + } + + #[test] + fn test_flash_error_invalid_param_maps_to_invalid_input() { + let err = BlockError::Driver(FlashBlockError::Flash(FlashError::InvalidParam("test"))); + assert_eq!(IOError::kind(&err), ErrorKind::InvalidInput); + } +} + +#[cfg(virtio)] #[cfg(test)] mod tests { use super::*; diff --git a/kernel/src/devices/mod.rs b/kernel/src/devices/mod.rs index 29f1f9563..1d0f8c01d 100644 --- a/kernel/src/devices/mod.rs +++ b/kernel/src/devices/mod.rs @@ -26,7 +26,8 @@ use core::{ use embedded_io::ErrorKind; use libc::*; use spin::{Once, RwLock as SpinRwLock}; -#[cfg(virtio)] +// Block storage subsystem: virtio backend or flash backend. +#[cfg(any(virtio, enable_block))] pub mod block; pub mod bus; pub mod clock; diff --git a/kernel/src/devices/spi_core/block_spi.rs b/kernel/src/devices/spi_core/block_spi.rs new file mode 100644 index 000000000..4ba2bac83 --- /dev/null +++ b/kernel/src/devices/spi_core/block_spi.rs @@ -0,0 +1,293 @@ +// Copyright (c) 2026 vivo Mobile Communication Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! SPI block transport bridge — HAL `Spi` trait → `embedded_hal::spi::SpiBus` +//! +//! BlockSpiBus wraps a HAL `Spi` peripheral into an `embedded_hal::spi::SpiBus`, +//! providing the bus-level interface that device drivers consume via +//! `ExclusiveDevice` to get a full `SpiDevice` with +//! CS lifecycle management. + +use blueos_driver::spi::SpiConfig; +use blueos_hal::PlatPeri; + +/// SPI block transport wrapper (bus-level) +/// +/// Wraps a HAL `Spi` peripheral into an `embedded_hal::spi::SpiBus`. +/// CS management is handled separately by `ExclusiveDevice` combining +/// this bus with a GPIO `OutputPin` (e.g., `Esp32GpioOutputPin`). +pub struct BlockSpiBus { + inner: &'static T, +} + +impl> BlockSpiBus { + /// Create a new BlockSpiBus, configuring the underlying HAL peripheral + /// with SPI NOR Flash default settings (Mode 0, 20MHz, MSB-first). + pub fn new(inner: &'static T) -> Result { + inner.configure(&SpiConfig::spi_flash_default())?; + Ok(BlockSpiBus { inner }) + } +} + +// Error type for SpiBus implementation +impl> embedded_hal::spi::ErrorType for BlockSpiBus { + type Error = SpiBlockError; +} + +/// SPI block transport error type +/// +/// Maps HAL errors to embedded-hal's SPI error framework. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum SpiBlockError { + /// Error originating from the underlying HAL Spi peripheral + HalError, +} + +impl embedded_hal::spi::Error for SpiBlockError { + fn kind(&self) -> embedded_hal::spi::ErrorKind { + match self { + SpiBlockError::HalError => embedded_hal::spi::ErrorKind::Other, + } + } +} + +/// SpiBus implementation — bridges HAL Spi to embedded-hal SpiBus +/// +/// Translates `SpiBus` method calls into the underlying HAL `Spi` trait. +/// The HAL peripheral already waits for completion after each transfer, +/// so `flush()` is a no-op. +impl> embedded_hal::spi::SpiBus for BlockSpiBus { + fn read(&mut self, words: &mut [u8]) -> Result<(), Self::Error> { + self.inner.read(words).map_err(|_| SpiBlockError::HalError) + } + + fn write(&mut self, words: &[u8]) -> Result<(), Self::Error> { + self.inner.write(words).map_err(|_| SpiBlockError::HalError) + } + + fn transfer(&mut self, read: &mut [u8], write: &[u8]) -> Result<(), Self::Error> { + self.inner + .transfer(read, write) + .map_err(|_| SpiBlockError::HalError) + } + + fn transfer_in_place(&mut self, words: &mut [u8]) -> Result<(), Self::Error> { + // Write then read back into same buffer (two-step since HAL transfer can't alias) + self.inner + .write(words) + .map_err(|_| SpiBlockError::HalError)?; + self.inner.read(words).map_err(|_| SpiBlockError::HalError) + } + + fn flush(&mut self) -> Result<(), Self::Error> { + // Esp32Spi2 waits for completion after each transfer, so bus is always idle + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use blueos_hal::err::{HalError, Result as HalResult}; + use blueos_test_macro::test; + use core::sync::atomic::{AtomicUsize, Ordering}; + use embedded_hal::spi::SpiBus; + + /// Mock HAL Spi peripheral for testing BlockSpiBus + /// + /// Implements all required HAL traits (PlatPeri, Configuration, Spi) + /// to create a BlockSpiBus wrapper for testing SpiBus method handling. + static MOCK_SPI: MockHalSpi = MockHalSpi; + + struct MockHalSpi; + + // PlatPeri: required by Spi supertrait (Sync + Send + 'static) + impl blueos_hal::PlatPeri for MockHalSpi { + fn enable(&self) {} + fn disable(&self) {} + } + + // Configuration: required by Spi supertrait + impl blueos_hal::Configuration for MockHalSpi { + type Target = (); + fn configure(&self, _param: &SpiConfig) -> HalResult { + Ok(()) + } + } + + // Spi: required for BlockSpiBus::new and SpiBus impl + impl blueos_hal::spi::Spi for MockHalSpi { + fn transfer(&self, read: &mut [u8], write: &[u8]) -> HalResult<()> { + if SHOULD_FAIL.load(Ordering::Relaxed) != 0 { + return Err(HalError::Fail); + } + // Copy write data into read buffer (simulates SPI loopback) + let len = read.len().min(write.len()); + read[..len].copy_from_slice(&write[..len]); + // Record the operation + WRITE_COUNT.fetch_add(1, Ordering::Relaxed); + Ok(()) + } + + fn read(&self, buf: &mut [u8]) -> HalResult<()> { + if SHOULD_FAIL.load(Ordering::Relaxed) != 0 { + return Err(HalError::Fail); + } + // Fill with configurable test data + let data = READ_DATA.load(Ordering::Relaxed) as u8; + for byte in buf.iter_mut() { + *byte = data; + } + READ_COUNT.fetch_add(1, Ordering::Relaxed); + Ok(()) + } + + fn write(&self, buf: &[u8]) -> HalResult<()> { + if SHOULD_FAIL.load(Ordering::Relaxed) != 0 { + return Err(HalError::Fail); + } + // Record the written data + LAST_WRITE_LEN.store(buf.len(), Ordering::Relaxed); + WRITE_COUNT.fetch_add(1, Ordering::Relaxed); + Ok(()) + } + } + + // Atomic counters for tracking HAL operations in tests + static WRITE_COUNT: AtomicUsize = AtomicUsize::new(0); + static READ_COUNT: AtomicUsize = AtomicUsize::new(0); + static LAST_WRITE_LEN: AtomicUsize = AtomicUsize::new(0); + static READ_DATA: AtomicUsize = AtomicUsize::new(0); + static SHOULD_FAIL: AtomicUsize = AtomicUsize::new(0); + + fn reset_counters() { + WRITE_COUNT.store(0, Ordering::Relaxed); + READ_COUNT.store(0, Ordering::Relaxed); + LAST_WRITE_LEN.store(0, Ordering::Relaxed); + READ_DATA.store(0, Ordering::Relaxed); + SHOULD_FAIL.store(0, Ordering::Relaxed); + } + + #[test] + fn test_block_spi_bus_new() { + let result = BlockSpiBus::new(&MOCK_SPI); + assert!(result.is_ok()); + } + + #[test] + fn test_block_spi_bus_write() { + let mut bus = BlockSpiBus::new(&MOCK_SPI).unwrap(); + reset_counters(); + + bus.write(&[0x9F, 0x00, 0x00, 0x00]).unwrap(); + assert_eq!(WRITE_COUNT.load(Ordering::Relaxed), 1); + assert_eq!(LAST_WRITE_LEN.load(Ordering::Relaxed), 4); + } + + #[test] + fn test_block_spi_bus_read() { + let mut bus = BlockSpiBus::new(&MOCK_SPI).unwrap(); + reset_counters(); + READ_DATA.store(0xAB, Ordering::Relaxed); + + let mut read_buf = [0u8; 3]; + bus.read(&mut read_buf).unwrap(); + + assert_eq!(READ_COUNT.load(Ordering::Relaxed), 1); + assert_eq!(read_buf, [0xAB, 0xAB, 0xAB]); + } + + #[test] + fn test_block_spi_bus_transfer() { + let mut bus = BlockSpiBus::new(&MOCK_SPI).unwrap(); + reset_counters(); + + let mut read_buf = [0u8; 4]; + let write_buf = [0x03, 0x00, 0x10, 0x00]; + bus.transfer(&mut read_buf, &write_buf).unwrap(); + + assert_eq!(WRITE_COUNT.load(Ordering::Relaxed), 1); + // Transfer copies write data into read buffer (loopback behavior) + assert_eq!(read_buf, write_buf); + } + + #[test] + fn test_block_spi_bus_transfer_in_place() { + let mut bus = BlockSpiBus::new(&MOCK_SPI).unwrap(); + reset_counters(); + READ_DATA.store(0xFF, Ordering::Relaxed); + + let mut buf = [0xAA, 0xBB, 0xCC, 0xDD]; + bus.transfer_in_place(&mut buf).unwrap(); + + // TransferInPlace: write then read back (two-step) + assert_eq!(WRITE_COUNT.load(Ordering::Relaxed), 1); + assert_eq!(READ_COUNT.load(Ordering::Relaxed), 1); + // After read, buf is filled with mock read data + assert_eq!(buf, [0xFF, 0xFF, 0xFF, 0xFF]); + } + + #[test] + fn test_block_spi_bus_flush() { + let mut bus = BlockSpiBus::new(&MOCK_SPI).unwrap(); + // flush is a no-op — should always succeed + assert!(bus.flush().is_ok()); + } + + #[test] + fn test_spi_block_error_kind() { + use embedded_hal::spi::Error as SpiError; + + assert_eq!( + SpiError::kind(&SpiBlockError::HalError), + embedded_hal::spi::ErrorKind::Other + ); + } + + #[test] + fn test_hal_write_error_propagation() { + let mut bus = BlockSpiBus::new(&MOCK_SPI).unwrap(); + reset_counters(); + SHOULD_FAIL.store(1, Ordering::Relaxed); + + let result = bus.write(&[0x9F, 0x00, 0x00, 0x00]); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), SpiBlockError::HalError); + } + + #[test] + fn test_hal_read_error_propagation() { + let mut bus = BlockSpiBus::new(&MOCK_SPI).unwrap(); + reset_counters(); + SHOULD_FAIL.store(1, Ordering::Relaxed); + + let mut read_buf = [0u8; 3]; + let result = bus.read(&mut read_buf); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), SpiBlockError::HalError); + } + + #[test] + fn test_hal_transfer_error_propagation() { + let mut bus = BlockSpiBus::new(&MOCK_SPI).unwrap(); + reset_counters(); + SHOULD_FAIL.store(1, Ordering::Relaxed); + + let mut read_buf = [0u8; 4]; + let write_buf = [0x03, 0x00, 0x10, 0x00]; + let result = bus.transfer(&mut read_buf, &write_buf); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), SpiBlockError::HalError); + } +} diff --git a/kernel/src/devices/storage/mod.rs b/kernel/src/devices/storage/mod.rs new file mode 100644 index 000000000..b63976bee --- /dev/null +++ b/kernel/src/devices/storage/mod.rs @@ -0,0 +1,18 @@ +// Copyright (c) 2026 vivo Mobile Communication Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[cfg(enable_block)] +pub mod spi_flash; +#[cfg(enable_block)] +pub mod spi_flash_cmd; diff --git a/kernel/src/devices/storage/spi_flash.rs b/kernel/src/devices/storage/spi_flash.rs new file mode 100644 index 000000000..1bb8385ea --- /dev/null +++ b/kernel/src/devices/storage/spi_flash.rs @@ -0,0 +1,494 @@ +// Copyright (c) 2026 vivo Mobile Communication Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! SPI NOR Flash FTL block driver adapter. + +use alloc::{string::String, sync::Arc, vec, vec::Vec}; +use core::cmp::min; +use embedded_hal::spi::SpiDevice; +use embedded_io::ErrorKind; + +use crate::{ + devices::{ + block::{Block, BlockDriverOps, BlockError, ErrorType}, + storage::spi_flash_cmd::{FlashError, SpiFlashCmd}, + DeviceManager, + }, + sync::SpinLock, +}; + +const FLASH_SECTOR_SIZE: u16 = 512; +const FLASH_ERASE_SIZE: usize = 4096; +const PAGES_PER_ERASE_BLOCK: usize = FLASH_ERASE_SIZE / 256; + +/// Flash block driver error. +#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)] +pub enum FlashBlockError { + #[error("Flash error: {0}")] + Flash(#[from] FlashError), +} + +/// SPI NOR Flash FTL block driver, caching one erase block at a time. +pub struct SpiFlashBlockDriver> { + flash_cmd: SpiFlashCmd, + capacity_bytes: u64, + erase_size: usize, + erase_buf: Vec, + dirty: bool, + current_erase_block: Option, +} + +impl + Send> SpiFlashBlockDriver { + pub fn new(flash_cmd: SpiFlashCmd, capacity_bytes: u64) -> Self { + SpiFlashBlockDriver { + flash_cmd, + capacity_bytes, + erase_size: FLASH_ERASE_SIZE, + erase_buf: vec![0u8; FLASH_ERASE_SIZE], + dirty: false, + current_erase_block: None, + } + } + + fn read_erase_block(&mut self, erase_block_id: usize) -> Result<(), FlashError> { + let addr = erase_block_id * FLASH_ERASE_SIZE; + self.flash_cmd.read(addr as u32, &mut self.erase_buf)?; + self.current_erase_block = Some(erase_block_id); + self.dirty = false; + Ok(()) + } + + fn flush_erase_block(&mut self) -> Result<(), FlashError> { + if !self.dirty || self.current_erase_block.is_none() { + return Ok(()); + } + let erase_block_id = self.current_erase_block.unwrap(); + let addr = (erase_block_id * FLASH_ERASE_SIZE) as u32; + + self.flash_cmd.sector_erase(addr)?; + + for page_idx in 0..PAGES_PER_ERASE_BLOCK { + let page_offset = page_idx * 256; + let page_data = &self.erase_buf[page_offset..page_offset + 256]; + self.flash_cmd + .page_program(addr + page_offset as u32, page_data)?; + } + + self.dirty = false; + Ok(()) + } + + fn ensure_erase_block(&mut self, block_id: usize) -> Result<(), FlashError> { + let erase_block_id = block_id / (FLASH_ERASE_SIZE / FLASH_SECTOR_SIZE as usize); + if self.current_erase_block != Some(erase_block_id) { + self.flush_erase_block()?; + self.read_erase_block(erase_block_id)?; + } + Ok(()) + } + + fn block_offset_in_erase(&self, block_id: usize) -> usize { + (block_id % (FLASH_ERASE_SIZE / FLASH_SECTOR_SIZE as usize)) * FLASH_SECTOR_SIZE as usize + } +} + +impl + Send + Sync> ErrorType for SpiFlashBlockDriver { + type Error = BlockError; +} + +impl + Send + Sync> BlockDriverOps for SpiFlashBlockDriver { + fn capacity(&self) -> u64 { + self.capacity_bytes / FLASH_SECTOR_SIZE as u64 + } + + fn sector_size(&self) -> u16 { + FLASH_SECTOR_SIZE + } + + fn read_blocks(&mut self, block_id: usize, buf: &mut [u8]) -> Result<(), Self::Error> { + let erase_block_id = block_id / (FLASH_ERASE_SIZE / FLASH_SECTOR_SIZE as usize); + + if self.dirty && self.current_erase_block == Some(erase_block_id) { + let offset = self.block_offset_in_erase(block_id); + let copy_len = min(buf.len(), FLASH_ERASE_SIZE - offset); + buf[..copy_len].copy_from_slice(&self.erase_buf[offset..offset + copy_len]); + return Ok(()); + } + + let addr = (block_id * FLASH_SECTOR_SIZE as usize) as u32; + self.flash_cmd + .read(addr, buf) + .map_err(|e| BlockError::Driver(FlashBlockError::Flash(e)))?; + Ok(()) + } + + fn write_blocks(&mut self, block_id: usize, buf: &[u8]) -> Result<(), Self::Error> { + self.ensure_erase_block(block_id) + .map_err(|e| BlockError::Driver(FlashBlockError::Flash(e)))?; + + let offset = self.block_offset_in_erase(block_id); + let copy_len = min(buf.len(), FLASH_ERASE_SIZE - offset); + self.erase_buf[offset..offset + copy_len].copy_from_slice(&buf[..copy_len]); + self.dirty = true; + + Ok(()) + } + + fn flush(&mut self) -> Result<(), Self::Error> { + self.flush_erase_block() + .map_err(|e| BlockError::Driver(FlashBlockError::Flash(e)))?; + Ok(()) + } +} + +/// Initialize the SPI NOR Flash block device and register it under `name`. +pub fn init_spi_flash(spi: SPI, name: &str) -> Result<(), ErrorKind> +where + SPI: SpiDevice + Send + Sync + 'static, +{ + let mut flash_cmd = SpiFlashCmd::new(spi); + + let jedec_id = flash_cmd.jedec_id().map_err(|e| match e { + FlashError::Spi(_) => ErrorKind::Other, + FlashError::Timeout => ErrorKind::TimedOut, + _ => ErrorKind::NotFound, + })?; + let density_byte = (jedec_id & 0xFF) as u8; + + let capacity_bytes: u64 = if density_byte < 31 { + (1u32 << density_byte) as u64 + } else { + 1u64 << density_byte + }; + + let block_driver = SpiFlashBlockDriver::new(flash_cmd, capacity_bytes); + + let block = Block::, { FLASH_SECTOR_SIZE as usize }>::new( + name, + Arc::new(SpinLock::new(block_driver)), + ); + + DeviceManager::get() + .register_device(String::from(name), Arc::new(block)) + .map_err(|_| ErrorKind::AlreadyExists)?; + + Ok(()) +} + +// SPI must be Send + Sync: the driver is shared via SpinLock behind an Arc, so it crosses threads (Send) and is referenced from &self (Sync). +unsafe impl + Send + Sync> Sync for SpiFlashBlockDriver {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::devices::block::{Block, BlockDriverOps, BlockError, ErrorType}; + use alloc::sync::Arc; + use blueos_test_macro::test; + use core::cell::UnsafeCell; + use embedded_hal::spi::{ErrorKind, Operation, SpiDevice}; + + struct MockSpiDevice { + shared: Arc>, + } + + struct MockSpiShared { + writes: alloc::vec::Vec, + delays: usize, + read_queue: alloc::vec::Vec>, + should_fail: bool, + transaction_count: usize, + } + + unsafe impl Send for MockSpiDevice {} + unsafe impl Sync for MockSpiDevice {} + + #[derive(Debug, Clone, Copy)] + struct MockSpiError; + + impl embedded_hal::spi::ErrorType for MockSpiDevice { + type Error = MockSpiError; + } + + impl embedded_hal::spi::Error for MockSpiError { + fn kind(&self) -> ErrorKind { + ErrorKind::Other + } + } + + impl SpiDevice for MockSpiDevice { + fn transaction(&mut self, operations: &mut [Operation<'_, u8>]) -> Result<(), Self::Error> { + let shared = self.shared.get(); + // SAFETY: single-threaded test context, accessed exclusively. + let shared = unsafe { &mut *shared }; + shared.transaction_count += 1; + + if shared.should_fail { + shared.should_fail = false; + return Err(MockSpiError); + } + + for op in operations.iter_mut() { + match op { + Operation::Write(data) => { + shared.writes.extend_from_slice(data); + } + Operation::Read(buf) => { + if !shared.read_queue.is_empty() { + let data = &shared.read_queue[0]; + let len = buf.len().min(data.len()); + buf[..len].copy_from_slice(&data[..len]); + shared.read_queue.remove(0); + } + } + Operation::Transfer(read_buf, write_buf) => { + shared.writes.extend_from_slice(write_buf); + if !shared.read_queue.is_empty() { + let data = &shared.read_queue[0]; + let len = read_buf.len().min(data.len()); + read_buf[..len].copy_from_slice(&data[..len]); + shared.read_queue.remove(0); + } + } + Operation::TransferInPlace(buf) => { + shared.writes.extend_from_slice(buf); + } + Operation::DelayNs(_) => { + shared.delays += 1; + } + } + } + Ok(()) + } + } + + impl MockSpiDevice { + fn new(shared: Arc>) -> Self { + MockSpiDevice { shared } + } + } + + impl MockSpiShared { + fn new() -> Self { + MockSpiShared { + writes: alloc::vec::Vec::new(), + delays: 0, + read_queue: alloc::vec::Vec::new(), + should_fail: false, + transaction_count: 0, + } + } + } + + fn with_shared( + shared: &Arc>, + f: impl FnOnce(&mut MockSpiShared) -> R, + ) -> R { + // SAFETY: test-only, single-threaded context. + f(unsafe { &mut *shared.get() }) + } + + fn create_block_driver( + capacity_bytes: u64, + ) -> ( + SpiFlashBlockDriver, + Arc>, + ) { + let shared = Arc::new(UnsafeCell::new(MockSpiShared::new())); + let mock = MockSpiDevice::new(Arc::clone(&shared)); + let flash_cmd = SpiFlashCmd::new(mock); + let driver = SpiFlashBlockDriver::new(flash_cmd, capacity_bytes); + (driver, shared) + } + + #[test] + fn test_block_driver_capacity() { + let (driver, _shared) = create_block_driver(1024 * 1024); + assert_eq!(driver.capacity(), 1024 * 1024 / 512); + assert_eq!(driver.sector_size(), 512); + } + + #[test] + fn test_read_blocks_from_flash() { + let (mut driver, shared) = create_block_driver(1024 * 1024); + let mut buf = [0u8; 512]; + + with_shared(&shared, |s| { + s.read_queue.push(alloc::vec![0xAA; 512]); + }); + + driver.read_blocks(0, &mut buf).unwrap(); + assert_eq!(buf[0], 0xAA); + + with_shared(&shared, |s| { + assert_eq!(s.writes[0], 0x03); + }); + } + + #[test] + fn test_read_blocks_from_dirty_cache() { + let (mut driver, shared) = create_block_driver(1024 * 1024); + let mut write_buf = [0xBB; 512]; + + with_shared(&shared, |s| { + s.read_queue.push(alloc::vec![0u8; FLASH_ERASE_SIZE]); + }); + driver.write_blocks(0, &write_buf).unwrap(); + + let mut read_buf = [0u8; 512]; + with_shared(&shared, |s| { + s.writes.clear(); + s.transaction_count = 0; + }); + driver.read_blocks(0, &mut read_buf).unwrap(); + assert_eq!(read_buf[0], 0xBB); + + with_shared(&shared, |s| { + assert_eq!(s.transaction_count, 0); + }); + } + + #[test] + fn test_write_marks_dirty() { + let (mut driver, shared) = create_block_driver(1024 * 1024); + let write_data = [0xCC; 512]; + + with_shared(&shared, |s| { + s.read_queue.push(alloc::vec![0u8; FLASH_ERASE_SIZE]); + }); + + driver.write_blocks(0, &write_data).unwrap(); + + assert!(driver.dirty); + assert_eq!(driver.current_erase_block, Some(0)); + } + + #[test] + fn test_flush_erase_block() { + let (mut driver, shared) = create_block_driver(1024 * 1024); + let write_data = [0xDD; 512]; + + with_shared(&shared, |s| { + s.read_queue.push(alloc::vec![0u8; FLASH_ERASE_SIZE]); + s.read_queue.push(alloc::vec![0x02]); + s.read_queue.push(alloc::vec![0x00]); + for _ in 0..PAGES_PER_ERASE_BLOCK { + s.read_queue.push(alloc::vec![0x02]); + s.read_queue.push(alloc::vec![0x00]); + } + }); + + driver.write_blocks(0, &write_data).unwrap(); + driver.flush().unwrap(); + + assert!(!driver.dirty); + } + + #[test] + fn test_ensure_erase_block_switching() { + let (mut driver, shared) = create_block_driver(1024 * 1024); + + with_shared(&shared, |s| { + s.read_queue.push(alloc::vec![0u8; FLASH_ERASE_SIZE]); + s.read_queue.push(alloc::vec![0x02]); + s.read_queue.push(alloc::vec![0x00]); + for _ in 0..PAGES_PER_ERASE_BLOCK { + s.read_queue.push(alloc::vec![0x02]); + s.read_queue.push(alloc::vec![0x00]); + } + s.read_queue.push(alloc::vec![0xFF; FLASH_ERASE_SIZE]); + }); + + driver.write_blocks(0, &[0xAA; 512]).unwrap(); + assert_eq!(driver.current_erase_block, Some(0)); + + driver.write_blocks(8, &[0xBB; 512]).unwrap(); + assert_eq!(driver.current_erase_block, Some(1)); + } + + #[test] + fn test_block_offset_in_erase() { + let (driver, _shared) = create_block_driver(1024 * 1024); + assert_eq!(driver.block_offset_in_erase(0), 0); + assert_eq!(driver.block_offset_in_erase(7), 3584); + assert_eq!(driver.block_offset_in_erase(8), 0); + assert_eq!(driver.block_offset_in_erase(9), 512); + } + + #[test] + fn test_flush_no_dirty_no_op() { + let (mut driver, shared) = create_block_driver(1024 * 1024); + driver.flush().unwrap(); + + with_shared(&shared, |s| { + assert_eq!(s.transaction_count, 0); + }); + } + + #[test] + fn test_spi_error_on_read() { + let (mut driver, shared) = create_block_driver(1024 * 1024); + let mut buf = [0u8; 512]; + + with_shared(&shared, |s| { + s.should_fail = true; + }); + + let result = driver.read_blocks(0, &mut buf); + assert!(result.is_err()); + } + + #[test] + fn test_block_error_kind_mapping() { + use embedded_io::Error as IOError; + + let err = BlockError::Driver(FlashBlockError::Flash( + crate::devices::storage::spi_flash_cmd::FlashError::Spi(ErrorKind::Other), + )); + assert_eq!(IOError::kind(&err), embedded_io::ErrorKind::Other); + + let err = BlockError::Driver(FlashBlockError::Flash( + crate::devices::storage::spi_flash_cmd::FlashError::NotReady, + )); + assert_eq!(IOError::kind(&err), embedded_io::ErrorKind::Other); + + let err = BlockError::Driver(FlashBlockError::Flash( + crate::devices::storage::spi_flash_cmd::FlashError::Timeout, + )); + assert_eq!(IOError::kind(&err), embedded_io::ErrorKind::TimedOut); + + let err = BlockError::Driver(FlashBlockError::Flash( + crate::devices::storage::spi_flash_cmd::FlashError::AddrOverflow { addr: 0x1000000 }, + )); + assert_eq!(IOError::kind(&err), embedded_io::ErrorKind::InvalidInput); + + let err = BlockError::Driver(FlashBlockError::Flash( + crate::devices::storage::spi_flash_cmd::FlashError::JedecMismatch { + expected: 0xEF4018, + got: 0x000000, + }, + )); + assert_eq!(IOError::kind(&err), embedded_io::ErrorKind::NotFound); + + let err = BlockError::Driver(FlashBlockError::Flash( + crate::devices::storage::spi_flash_cmd::FlashError::WriteEnableFailed, + )); + assert_eq!(IOError::kind(&err), embedded_io::ErrorKind::Other); + + let err = BlockError::Driver(FlashBlockError::Flash( + crate::devices::storage::spi_flash_cmd::FlashError::InvalidParam("test"), + )); + assert_eq!(IOError::kind(&err), embedded_io::ErrorKind::InvalidInput); + } +} diff --git a/kernel/src/devices/storage/spi_flash_cmd.rs b/kernel/src/devices/storage/spi_flash_cmd.rs new file mode 100644 index 000000000..d9a49fb7c --- /dev/null +++ b/kernel/src/devices/storage/spi_flash_cmd.rs @@ -0,0 +1,631 @@ +// Copyright (c) 2026 vivo Mobile Communication Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! JEDEC 25-series SPI NOR Flash command layer. + +use embedded_hal::spi::{ErrorKind, Operation, SpiDevice}; + +/// SPI NOR Flash command layer error. +#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)] +pub enum FlashError { + #[error("SPI bus error: {0:?}")] + Spi(ErrorKind), + #[error("Device not ready")] + NotReady, + #[error("JEDEC ID mismatch: expected 0x{expected:06X}, got 0x{got:06X}")] + JedecMismatch { expected: u32, got: u32 }, + #[error("Write enable failed")] + WriteEnableFailed, + #[error("Timeout")] + Timeout, + #[error("Address 0x{addr:08X} exceeds 24-bit range")] + AddrOverflow { addr: u32 }, + #[error("Invalid parameter: {0}")] + InvalidParam(&'static str), +} + +// Inline closure rather than a From impl to avoid coherence issues with the +// generic SPI::Error type parameter. +fn spi_err_to_flash(err: E) -> FlashError { + FlashError::Spi(err.kind()) +} + +/// JEDEC 25-series SPI NOR Flash command layer. +pub struct SpiFlashCmd> { + spi: SPI, +} + +impl> SpiFlashCmd { + pub fn new(spi: SPI) -> Self { + SpiFlashCmd { spi } + } + + /// Read JEDEC manufacturer + device ID (command 0x9F). + pub fn jedec_id(&mut self) -> Result { + let mut id_buf = [0u8; 3]; + self.spi + .transaction(&mut [Operation::Write(&[0x9F]), Operation::Read(&mut id_buf)]) + .map_err(spi_err_to_flash)?; + Ok((id_buf[0] as u32) << 16 | (id_buf[1] as u32) << 8 | (id_buf[2] as u32)) + } + + /// Normal read, 3-byte address (command 0x03). + pub fn read(&mut self, addr: u32, buf: &mut [u8]) -> Result<(), FlashError> { + let addr_bytes = addr_bytes(addr)?; + self.spi + .transaction(&mut [ + Operation::Write(&[0x03, addr_bytes[0], addr_bytes[1], addr_bytes[2]]), + Operation::Read(buf), + ]) + .map_err(spi_err_to_flash)?; + Ok(()) + } + + /// Fast read, 3-byte address + dummy cycles (command 0x0B). + pub fn fast_read(&mut self, addr: u32, buf: &mut [u8]) -> Result<(), FlashError> { + let addr_bytes = addr_bytes(addr)?; + self.spi + .transaction(&mut [ + Operation::Write(&[0x0B, addr_bytes[0], addr_bytes[1], addr_bytes[2]]), + Operation::DelayNs(1_000_000), + Operation::Read(buf), + ]) + .map_err(spi_err_to_flash)?; + Ok(()) + } + + /// Page program: write-enable, then program up to 256 bytes (command 0x02). + pub fn page_program(&mut self, addr: u32, data: &[u8]) -> Result<(), FlashError> { + if addr % 256 != 0 { + return Err(FlashError::InvalidParam( + "page_program address not 256-byte aligned", + )); + } + if data.len() > 256 { + return Err(FlashError::InvalidParam( + "page_program data exceeds 256 bytes", + )); + } + self.write_enable()?; + let addr_bytes = addr_bytes(addr)?; + self.spi + .transaction(&mut [ + Operation::Write(&[0x02, addr_bytes[0], addr_bytes[1], addr_bytes[2]]), + Operation::Write(data), + ]) + .map_err(spi_err_to_flash)?; + self.wait_busy()?; + Ok(()) + } + + /// 4KB sector erase (command 0x20). + pub fn sector_erase(&mut self, addr: u32) -> Result<(), FlashError> { + self.write_enable()?; + let addr_bytes = addr_bytes(addr)?; + self.spi + .transaction(&mut [Operation::Write(&[ + 0x20, + addr_bytes[0], + addr_bytes[1], + addr_bytes[2], + ])]) + .map_err(spi_err_to_flash)?; + self.wait_busy()?; + Ok(()) + } + + /// 32KB block erase (command 0x52). + pub fn block_erase_32k(&mut self, addr: u32) -> Result<(), FlashError> { + self.write_enable()?; + let addr_bytes = addr_bytes(addr)?; + self.spi + .transaction(&mut [Operation::Write(&[ + 0x52, + addr_bytes[0], + addr_bytes[1], + addr_bytes[2], + ])]) + .map_err(spi_err_to_flash)?; + self.wait_busy()?; + Ok(()) + } + + /// 64KB block erase (command 0xD8). + pub fn block_erase_64k(&mut self, addr: u32) -> Result<(), FlashError> { + self.write_enable()?; + let addr_bytes = addr_bytes(addr)?; + self.spi + .transaction(&mut [Operation::Write(&[ + 0xD8, + addr_bytes[0], + addr_bytes[1], + addr_bytes[2], + ])]) + .map_err(spi_err_to_flash)?; + self.wait_busy()?; + Ok(()) + } + + /// Full chip erase (command 0xC7). + pub fn chip_erase(&mut self) -> Result<(), FlashError> { + self.write_enable()?; + self.spi + .transaction(&mut [Operation::Write(&[0xC7])]) + .map_err(spi_err_to_flash)?; + self.wait_busy()?; + Ok(()) + } + + /// Set Write Enable Latch and verify it took effect (command 0x06). + pub fn write_enable(&mut self) -> Result<(), FlashError> { + self.spi + .transaction(&mut [Operation::Write(&[0x06])]) + .map_err(spi_err_to_flash)?; + let status = self.read_status()?; + if status & 0x02 == 0 { + return Err(FlashError::WriteEnableFailed); + } + Ok(()) + } + + /// Read status register byte (command 0x05). + pub fn read_status(&mut self) -> Result { + let mut status_buf = [0u8; 1]; + self.spi + .transaction(&mut [Operation::Write(&[0x05]), Operation::Read(&mut status_buf)]) + .map_err(spi_err_to_flash)?; + Ok(status_buf[0]) + } + + /// Poll the BUSY bit until clear, or return Timeout after 1000 iterations. + pub fn wait_busy(&mut self) -> Result<(), FlashError> { + for _ in 0..1000 { + let status = self.read_status()?; + if status & 0x01 == 0 { + return Ok(()); + } + self.spi + .transaction(&mut [Operation::DelayNs(1_000_000)]) + .map_err(spi_err_to_flash)?; + } + Err(FlashError::Timeout) + } + + /// Release from deep power-down (command 0xAB). + pub fn release_from_deep_power_down(&mut self) -> Result<(), FlashError> { + self.spi + .transaction(&mut [Operation::Write(&[0xAB])]) + .map_err(spi_err_to_flash)?; + Ok(()) + } +} + +/// Split a 24-bit address into 3 MSB-first bytes, rejecting >= 16MB. +fn addr_bytes(addr: u32) -> Result<[u8; 3], FlashError> { + if addr >= 0x0100_0000 { + return Err(FlashError::AddrOverflow { addr }); + } + Ok([ + ((addr >> 16) & 0xFF) as u8, + ((addr >> 8) & 0xFF) as u8, + (addr & 0xFF) as u8, + ]) +} + +#[cfg(test)] +mod tests { + use super::*; + use alloc::sync::Arc; + use blueos_test_macro::test; + use core::cell::UnsafeCell; + + struct MockSpiDevice { + shared: Arc>, + } + + struct MockSpiShared { + writes: alloc::vec::Vec, + delays: usize, + read_queue: alloc::vec::Vec>, + should_fail: bool, + transaction_count: usize, + } + + // MockSpiDevice is accessed exclusively under SpinLock in tests, so sharing + // the UnsafeCell across threads is sound. + unsafe impl Send for MockSpiDevice {} + unsafe impl Sync for MockSpiDevice {} + + impl embedded_hal::spi::ErrorType for MockSpiDevice { + type Error = MockSpiError; + } + + #[derive(Debug, Clone, Copy)] + struct MockSpiError; + + impl embedded_hal::spi::Error for MockSpiError { + fn kind(&self) -> ErrorKind { + ErrorKind::Other + } + } + + impl embedded_hal::spi::SpiDevice for MockSpiDevice { + fn transaction(&mut self, operations: &mut [Operation<'_, u8>]) -> Result<(), Self::Error> { + let shared = self.shared.get(); + // SAFETY: MockSpiDevice is always wrapped in SpinLock which guarantees + // exclusive access. No concurrent access possible. + let shared = unsafe { &mut *shared }; + shared.transaction_count += 1; + + if shared.should_fail { + shared.should_fail = false; + return Err(MockSpiError); + } + + for op in operations.iter_mut() { + match op { + Operation::Write(data) => { + shared.writes.extend_from_slice(data); + } + Operation::Read(buf) => { + if !shared.read_queue.is_empty() { + let read_data = &shared.read_queue[0]; + let len = buf.len().min(read_data.len()); + buf[..len].copy_from_slice(&read_data[..len]); + if read_data.len() <= buf.len() { + shared.read_queue.remove(0); + } + } + } + Operation::Transfer(read_buf, write_buf) => { + shared.writes.extend_from_slice(write_buf); + if !shared.read_queue.is_empty() { + let read_data = &shared.read_queue[0]; + let len = read_buf.len().min(read_data.len()); + read_buf[..len].copy_from_slice(&read_data[..len]); + if read_data.len() <= read_buf.len() { + shared.read_queue.remove(0); + } + } + } + Operation::TransferInPlace(buf) => { + shared.writes.extend_from_slice(buf); + } + Operation::DelayNs(_) => { + shared.delays += 1; + } + } + } + Ok(()) + } + } + + impl MockSpiDevice { + fn new(shared: Arc>) -> Self { + MockSpiDevice { shared } + } + } + + impl MockSpiShared { + fn new() -> Self { + MockSpiShared { + writes: alloc::vec::Vec::new(), + delays: 0, + read_queue: alloc::vec::Vec::new(), + should_fail: false, + transaction_count: 0, + } + } + } + + fn create_flash_cmd() -> (SpiFlashCmd, Arc>) { + let shared = Arc::new(UnsafeCell::new(MockSpiShared::new())); + let mock = MockSpiDevice::new(Arc::clone(&shared)); + let flash_cmd = SpiFlashCmd::new(mock); + (flash_cmd, shared) + } + + fn with_shared( + shared: &Arc>, + f: impl FnOnce(&mut MockSpiShared) -> R, + ) -> R { + // SAFETY: test-only, single-threaded context. + f(unsafe { &mut *shared.get() }) + } + + #[test] + fn test_addr_bytes_valid() { + let result = addr_bytes(0x000000); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), [0x00, 0x00, 0x00]); + + let result = addr_bytes(0x00FFFFFF); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), [0xFF, 0xFF, 0xFF]); + + let result = addr_bytes(0x001234); + assert!(result.is_ok()); + assert_eq!(result.unwrap(), [0x00, 0x12, 0x34]); + } + + #[test] + fn test_addr_bytes_overflow() { + let result = addr_bytes(0x01000000); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + FlashError::AddrOverflow { addr: 0x01000000 } + ); + + let result = addr_bytes(0xFFFFFFFF); + assert!(result.is_err()); + } + + #[test] + fn test_jedec_id() { + let (mut flash_cmd, shared) = create_flash_cmd(); + with_shared(&shared, |s| { + s.read_queue = alloc::vec![alloc::vec![0xEF, 0x40, 0x18]]; + }); + + let jedec_id = flash_cmd.jedec_id().unwrap(); + assert_eq!(jedec_id, 0xEF4018); + + with_shared(&shared, |s| { + assert_eq!(&s.writes, &[0x9F]); + assert_eq!(s.transaction_count, 1); + }); + } + + #[test] + fn test_read_command() { + let (mut flash_cmd, shared) = create_flash_cmd(); + let mut buf = [0u8; 4]; + with_shared(&shared, |s| { + s.read_queue = alloc::vec![alloc::vec![0xAA, 0xBB, 0xCC, 0xDD]]; + }); + + flash_cmd.read(0x001000, &mut buf).unwrap(); + assert_eq!(buf, [0xAA, 0xBB, 0xCC, 0xDD]); + + with_shared(&shared, |s| { + assert_eq!(&s.writes, &[0x03, 0x00, 0x10, 0x00]); + }); + } + + #[test] + fn test_read_addr_overflow() { + let (mut flash_cmd, _shared) = create_flash_cmd(); + let mut buf = [0u8; 4]; + let result = flash_cmd.read(0x01000000, &mut buf); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + FlashError::AddrOverflow { addr: 0x01000000 } + ); + } + + #[test] + fn test_fast_read_command() { + let (mut flash_cmd, shared) = create_flash_cmd(); + let mut buf = [0u8; 4]; + with_shared(&shared, |s| { + s.read_queue = alloc::vec![alloc::vec![0x11, 0x22, 0x33, 0x44]]; + }); + + flash_cmd.fast_read(0x000100, &mut buf).unwrap(); + assert_eq!(buf, [0x11, 0x22, 0x33, 0x44]); + + with_shared(&shared, |s| { + assert_eq!(&s.writes[..4], &[0x0B, 0x00, 0x01, 0x00]); + assert!(s.delays > 0); + }); + } + + #[test] + fn test_page_program_param_validation() { + let (mut flash_cmd, _shared) = create_flash_cmd(); + let data = [0u8; 128]; + + let result = flash_cmd.page_program(0x001001, &data); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + FlashError::InvalidParam("page_program address not 256-byte aligned") + ); + + let big_data = alloc::vec![0u8; 300]; + let result = flash_cmd.page_program(0x000000, &big_data); + assert!(result.is_err()); + assert_eq!( + result.unwrap_err(), + FlashError::InvalidParam("page_program data exceeds 256 bytes") + ); + } + + #[test] + fn test_page_program_success() { + let (mut flash_cmd, shared) = create_flash_cmd(); + with_shared(&shared, |s| { + s.read_queue = alloc::vec![alloc::vec![0x02], alloc::vec![0x00]]; + }); + + let data = [0xAA, 0xBB, 0xCC, 0xDD]; + flash_cmd.page_program(0x000000, &data).unwrap(); + + with_shared(&shared, |s| { + let writes = &s.writes; + assert_eq!(writes[0], 0x06); + assert_eq!(writes[1], 0x05); + assert_eq!(&writes[2..6], &[0x02, 0x00, 0x00, 0x00]); + assert_eq!(&writes[6..10], &[0xAA, 0xBB, 0xCC, 0xDD]); + assert_eq!(writes[10], 0x05); + }); + } + + #[test] + fn test_sector_erase_command() { + let (mut flash_cmd, shared) = create_flash_cmd(); + with_shared(&shared, |s| { + s.read_queue = alloc::vec![alloc::vec![0x02], alloc::vec![0x00]]; + }); + + flash_cmd.sector_erase(0x001000).unwrap(); + + with_shared(&shared, |s| { + let writes = &s.writes; + assert_eq!(writes[0], 0x06); + assert_eq!(writes[1], 0x05); + assert_eq!(writes[2], 0x20); + assert_eq!(&writes[3..6], &[0x00, 0x10, 0x00]); + assert_eq!(writes[6], 0x05); + }); + } + + #[test] + fn test_write_enable_success() { + let (mut flash_cmd, shared) = create_flash_cmd(); + with_shared(&shared, |s| { + s.read_queue = alloc::vec![alloc::vec![0x02]]; + }); + + flash_cmd.write_enable().unwrap(); + + with_shared(&shared, |s| { + assert_eq!(s.writes[0], 0x06); + assert_eq!(s.writes[1], 0x05); + }); + } + + #[test] + fn test_write_enable_failed() { + let (mut flash_cmd, shared) = create_flash_cmd(); + with_shared(&shared, |s| { + s.read_queue = alloc::vec![alloc::vec![0x00]]; + }); + + let result = flash_cmd.write_enable(); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), FlashError::WriteEnableFailed); + } + + #[test] + fn test_read_status() { + let (mut flash_cmd, shared) = create_flash_cmd(); + with_shared(&shared, |s| { + s.read_queue = alloc::vec![alloc::vec![0x03]]; + }); + + let status = flash_cmd.read_status().unwrap(); + assert_eq!(status, 0x03); + + with_shared(&shared, |s| { + assert_eq!(s.writes[0], 0x05); + }); + } + + #[test] + fn test_chip_erase_command() { + let (mut flash_cmd, shared) = create_flash_cmd(); + with_shared(&shared, |s| { + s.read_queue = alloc::vec![alloc::vec![0x02], alloc::vec![0x00]]; + }); + + flash_cmd.chip_erase().unwrap(); + + with_shared(&shared, |s| { + assert_eq!(s.writes[0], 0x06); + assert_eq!(s.writes[1], 0x05); + assert_eq!(s.writes[2], 0xC7); + assert_eq!(s.writes[3], 0x05); + }); + } + + #[test] + fn test_release_from_deep_power_down() { + let (mut flash_cmd, shared) = create_flash_cmd(); + flash_cmd.release_from_deep_power_down().unwrap(); + + with_shared(&shared, |s| { + assert_eq!(&s.writes, &[0xAB]); + }); + } + + #[test] + fn test_block_erase_32k_command() { + let (mut flash_cmd, shared) = create_flash_cmd(); + with_shared(&shared, |s| { + s.read_queue = alloc::vec![alloc::vec![0x02], alloc::vec![0x00]]; + }); + + flash_cmd.block_erase_32k(0x001000).unwrap(); + + with_shared(&shared, |s| { + let writes = &s.writes; + assert_eq!(writes[0], 0x06); + assert_eq!(writes[1], 0x05); + assert_eq!(writes[2], 0x52); + assert_eq!(&writes[3..6], &[0x00, 0x10, 0x00]); + assert_eq!(writes[6], 0x05); + }); + } + + #[test] + fn test_block_erase_64k_command() { + let (mut flash_cmd, shared) = create_flash_cmd(); + with_shared(&shared, |s| { + s.read_queue = alloc::vec![alloc::vec![0x02], alloc::vec![0x00]]; + }); + + flash_cmd.block_erase_64k(0x001000).unwrap(); + + with_shared(&shared, |s| { + let writes = &s.writes; + assert_eq!(writes[0], 0x06); + assert_eq!(writes[1], 0x05); + assert_eq!(writes[2], 0xD8); + assert_eq!(&writes[3..6], &[0x00, 0x10, 0x00]); + assert_eq!(writes[6], 0x05); + }); + } + + #[test] + fn test_wait_busy_timeout() { + let (mut flash_cmd, shared) = create_flash_cmd(); + with_shared(&shared, |s| { + s.read_queue = alloc::vec![alloc::vec![0x01]; 1000]; + }); + + let result = flash_cmd.wait_busy(); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), FlashError::Timeout); + + with_shared(&shared, |s| { + assert_eq!(s.transaction_count, 2000); + assert_eq!(s.delays, 1000); + }); + } + + #[test] + fn test_spi_error_propagation() { + let (mut flash_cmd, shared) = create_flash_cmd(); + with_shared(&shared, |s| { + s.should_fail = true; + }); + + let result = flash_cmd.jedec_id(); + assert!(result.is_err()); + assert_eq!(result.unwrap_err(), FlashError::Spi(ErrorKind::Other)); + } +} diff --git a/kernel/src/vfs/fatfs.rs b/kernel/src/vfs/fatfs.rs index 171b54d9c..00aede44a 100644 --- a/kernel/src/vfs/fatfs.rs +++ b/kernel/src/vfs/fatfs.rs @@ -99,6 +99,9 @@ impl FatFileSystem { ); fatfs::format_volume(&mut storage, format_opts) .expect("[FatFileSystem] Format volume fail."); + storage + .flush() + .expect("[FatFileSystem] Flush after format fail."); fatfs::FileSystem::new(storage, fatfs::FsOptions::new()) .expect("[FatFileSystem] Failed to construct internal fs again") } @@ -550,7 +553,11 @@ impl InodeOps for FatInode { } fn close(&self) -> Result<(), Error> { - Ok(()) + // Persist file contents on close; non-regular inodes have nothing to flush. + if self.type_() != InodeFileType::Regular { + return Ok(()); + } + self.fsync() } fn read_at(&self, offset: usize, buf: &mut [u8], _nonblock: bool) -> Result { diff --git a/kernel/src/vfs/mod.rs b/kernel/src/vfs/mod.rs index 272549ce6..c365c65e0 100644 --- a/kernel/src/vfs/mod.rs +++ b/kernel/src/vfs/mod.rs @@ -28,7 +28,7 @@ use log::{debug, error, warn}; mod dcache; mod devfs; pub mod dirent; -#[cfg(virtio)] +#[cfg(fatfs)] mod fatfs; mod fd_manager; mod file; @@ -77,19 +77,18 @@ pub fn vfs_init() -> Result<(), Error> { let mut fd_manager = get_fd_manager().lock(); fd_manager.init_stdio()?; - #[cfg(virtio)] + #[cfg(fatfs)] { use crate::{ - devices::block::VIRTUAL_STORAGE_NAME, + boards::{BLOCK_STORAGE_DEVICE_NAME, BLOCK_STORAGE_MOUNT_POINT}, vfs::{fatfs::FatFileSystem, fs::FileSystem}, }; use alloc::string::String; debug!("init fatfs"); - match FatFileSystem::new(VIRTUAL_STORAGE_NAME) { + match FatFileSystem::new(BLOCK_STORAGE_DEVICE_NAME) { Ok(fatfs) => { - let fat_name = String::from("fat"); - // create the directory /fat + let fat_name = String::from(BLOCK_STORAGE_MOUNT_POINT); let dev_dir = cwd.new_child( fat_name.as_str(), InodeFileType::Directory, @@ -99,7 +98,7 @@ pub fn vfs_init() -> Result<(), Error> { let fatfs_mount_point = Dcache::new(fatfs.root_inode(), fat_name, cwd.get_weak_ref()); fatfs_mount_point.mount(fatfs)?; - debug!("Mounted fatfs at '/fat'"); + debug!("Mounted fatfs at '/{}'", BLOCK_STORAGE_MOUNT_POINT); } Err(error) => { error!("Fail to init fat file system, {}", error); diff --git a/kernel/src/vfs/mount.rs b/kernel/src/vfs/mount.rs index 9f2bf898b..feed5f140 100644 --- a/kernel/src/vfs/mount.rs +++ b/kernel/src/vfs/mount.rs @@ -12,7 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -#[cfg(virtio)] +#[cfg(fatfs)] use crate::vfs::fatfs::FatFileSystem; #[cfg(procfs)] use crate::vfs::procfs::ProcFileSystem; @@ -94,7 +94,7 @@ pub fn get_mount_manager() -> &'static MountManager { pub fn get_fs(fs_type: &str, device: &str) -> Option> { match fs_type { "tmpfs" => Some(TmpFileSystem::new()), - #[cfg(virtio)] + #[cfg(fatfs)] "fatfs" => match FatFileSystem::new(device) { Ok(fs) => Some(fs), Err(error) => {