From b7a4f2c412e614bfc3c12cba29a7d6ba77e192d0 Mon Sep 17 00:00:00 2001 From: jinwjinl <2532191107@qq.com> Date: Wed, 1 Jul 2026 18:23:03 +0800 Subject: [PATCH 1/3] enable drive spi and adapt esp32c3 --- driver/BUILD.gn | 1 + driver/src/gpio/esp32_gpio.rs | 339 +++++++++++++ driver/src/gpio/mod.rs | 16 + driver/src/lib.rs | 2 + driver/src/spi/esp32_spi2.rs | 619 +++++++++++++++++++++++ driver/src/spi/mod.rs | 68 +++ hal/src/lib.rs | 1 + hal/src/spi.rs | 33 ++ kernel/BUILD.gn | 2 + kernel/src/devices/mod.rs | 1 + kernel/src/devices/spi_core/block_spi.rs | 293 +++++++++++ kernel/src/devices/spi_core/mod.rs | 15 + kernel/src/drivers/sensor/bme280.rs | 18 +- kernel/src/sync/delay.rs | 80 +++ kernel/src/sync/mod.rs | 2 + 15 files changed, 1473 insertions(+), 17 deletions(-) create mode 100644 driver/src/gpio/esp32_gpio.rs create mode 100644 driver/src/gpio/mod.rs create mode 100644 driver/src/spi/esp32_spi2.rs create mode 100644 driver/src/spi/mod.rs create mode 100644 hal/src/spi.rs create mode 100644 kernel/src/devices/spi_core/block_spi.rs create mode 100644 kernel/src/devices/spi_core/mod.rs create mode 100644 kernel/src/sync/delay.rs diff --git a/driver/BUILD.gn b/driver/BUILD.gn index e3c7a9f52..46e7a7214 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/gpio/esp32_gpio.rs b/driver/src/gpio/esp32_gpio.rs new file mode 100644 index 000000000..ab8260cb7 --- /dev/null +++ b/driver/src/gpio/esp32_gpio.rs @@ -0,0 +1,339 @@ +// 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. + +// ESP32-C3 GPIO + IO_MUX + GPIO Matrix driver +// +// Reference: ESP32-C3 Technical Reference Manual, Chapter 4 (IO_MUX/GPIO) +// Reference: ESP-IDF soc/esp32c3/include/soc/gpio_reg.h, io_mux_reg.h, gpio_sig_map.h +// +// ESP32-C3 SPI2 (FSPI) signals are routed through the GPIO Matrix, +// not through IO_MUX FUNC selection. This driver configures both: +// - IO_MUX: per-pin pull-up/pull-down, input enable, MCU_SEL (function select) +// - GPIO Matrix: maps peripheral output signals to GPIO pins +// - GPIO output: direct pin output control for CS (software-controlled) + +use crate::static_ref::StaticRef; +use blueos_hal::pinctrl::AlterFuncPin; +use embedded_hal::digital::OutputPin; +use tock_registers::{ + interfaces::Writeable, register_bitfields, register_structs, registers::ReadWrite, +}; + +// ─── Base addresses ────────────────────────────────────────────────────── + +// GPIO controller (output/enable/matrix) +const GPIO_BASE: StaticRef = + unsafe { StaticRef::new(0x60004000 as *const GpioRegisters) }; + +// ─── GPIO Matrix signal indices (from gpio_sig_map.h) ──────────────────── +// These map SPI2/FSPI peripheral signals to GPIO pins via the GPIO Matrix. + +/// FSPICLK — SPI2 clock output signal +const FSPICLK_OUT_IDX: u32 = 63; +/// FSPIQ — SPI2 MISO (data in) signal +const FSPIQ_IN_IDX: u32 = 64; +/// FSPID — SPI2 MOSI (data out) signal +const FSPID_OUT_IDX: u32 = 65; +/// FSPICS0 — SPI2 chip select 0 output signal +const FSPICS0_OUT_IDX: u32 = 68; + +// ─── IO_MUX per-pin register offsets ───────────────────────────────────── +// IO_MUX registers are NOT at contiguous 4*GPIO offsets. +// Each GPIO has a named register at a specific offset (from io_mux_reg.h). + +/// IO_MUX register offset for each GPIO (0-21), relative to IO_MUX base (0x60009000). +/// Index = GPIO number. Value = offset in bytes. +const IO_MUX_OFFSETS: [u32; 22] = [ + 0x04, // GPIO0 — PERIPHS_IO_MUX_XTAL_32K_P_U + 0x08, // GPIO1 — PERIPHS_IO_MUX_XTAL_32K_N_U + 0x0C, // GPIO2 — PERIPHS_IO_MUX_GPIO2_U + 0x10, // GPIO3 — PERIPHS_IO_MUX_GPIO3_U + 0x14, // GPIO4 — PERIPHS_IO_MUX_MTMS_U + 0x18, // GPIO5 — PERIPHS_IO_MUX_MTDI_U + 0x1C, // GPIO6 — PERIPHS_IO_MUX_MTCK_U + 0x20, // GPIO7 — PERIPHS_IO_MUX_MTDO_U + 0x24, // GPIO8 — PERIPHS_IO_MUX_GPIO8_U + 0x28, // GPIO9 — PERIPHS_IO_MUX_GPIO9_U + 0x2C, // GPIO10 — PERIPHS_IO_MUX_GPIO10_U + 0x30, // GPIO11 — PERIPHS_IO_MUX_VDD_SPI_U + 0x34, // GPIO12 — PERIPHS_IO_MUX_SPIHD_U + 0x38, // GPIO13 — PERIPHS_IO_MUX_SPIWP_U + 0x3C, // GPIO14 — PERIPHS_IO_MUX_SPICS0_U + 0x40, // GPIO15 — PERIPHS_IO_MUX_SPICLK_U + 0x44, // GPIO16 — PERIPHS_IO_MUX_SPID_U + 0x48, // GPIO17 — PERIPHS_IO_MUX_SPIQ_U + 0x4C, // GPIO18 — PERIPHS_IO_MUX_GPIO18_U + 0x50, // GPIO19 — PERIPHS_IO_MUX_GPIO19_U + 0x54, // GPIO20 — PERIPHS_IO_MUX_U0RXD_U + 0x58, // GPIO21 — PERIPHS_IO_MUX_U0TXD_U +]; + +const IO_MUX_BASE: usize = 0x60009000; + +// ─── IO_MUX register bit fields (from io_mux_reg.h) ───────────────────── +// MCU_SEL at bits 12-14 (3 bits, values 0-7) +// FUN_IE at bit 9 (input enable) +// FUN_DRV at bits 10-11 (drive strength, 0-3) +// FUN_PU at bit 8 (pull-up) +// FUN_PD at bit 7 (pull-down) + +register_bitfields! [ + u32, + + pub IoMuxFields [ + MCU_SEL OFFSET(12) NUMBITS(3) [ + Func0 = 0, // Default (JTAG/special) + Func1 = 1, // GPIO function (PIN_FUNC_GPIO) + Func2 = 2, // FSPI (SPI2) function for some pins + ], + FUN_DRV OFFSET(10) NUMBITS(2) [ + Drive0 = 0, // Weakest + Drive1 = 1, + Drive2 = 2, + Drive3 = 3, // Strongest + ], + FUN_IE OFFSET(9) NUMBITS(1) [], // Input enable + FUN_PU OFFSET(8) NUMBITS(1) [], // Pull-up + FUN_PD OFFSET(7) NUMBITS(1) [], // Pull-down + ], +]; + +register_bitfields! [ + u32, + + pub GpioOut [ + DATA OFFSET(0) NUMBITS(26) [], + ], + pub GpioEnable [ + DATA OFFSET(0) NUMBITS(26) [], + ], +]; + +// ─── GPIO controller registers ────────────────────────────────────────── + +register_structs! { + GpioRegisters { + (0x000 => bt_select: ReadWrite), + (0x004 => out: ReadWrite), + (0x008 => out_w1ts: ReadWrite), + (0x00C => out_w1tc: ReadWrite), + (0x010 => _reserved0), + (0x020 => enable: ReadWrite), + (0x024 => enable_w1ts: ReadWrite), + (0x028 => enable_w1tc: ReadWrite), + (0x02C => @END), + } +} + +// ─── GPIO Matrix output function selection (from gpio_reg.h) ───────────── +// GPIO_FUNCx_OUT_SEL_CFG_REG at offset 0x554 + 4*x +// Each register has: +// OUT_SEL: bits 0-7 — which peripheral signal index to route to GPIO x +// INV_SEL: bit 8 — invert the output signal +// OEN_SEL: bit 9 — 0=peripheral controls output enable, 1=GPIO register controls +// OEN_INV_SEL: bit 10 — invert output enable signal +// +// For SPI2 output pins (CLK, MOSI, CS), we route the FSPI signal. +// For CS pin (software-controlled), we set OEN_SEL=1 (GPIO controls enable). + +register_bitfields! [ + u32, + + pub FuncOutSelCfg [ + OUT_SEL OFFSET(0) NUMBITS(8) [], // Peripheral signal index + INV_SEL OFFSET(8) NUMBITS(1) [], // Invert output + OEN_SEL OFFSET(9) NUMBITS(1) [ + Peripheral = 0, // Peripheral controls output enable + GpioReg = 1, // GPIO_ENABLE_REG controls output enable + ], + OEN_INV_SEL OFFSET(10) NUMBITS(1) [], // Invert output enable + ], +]; + +// ─── GPIO Matrix input function selection (from gpio_reg.h) ────────────── +// GPIO_FUNCx_IN_SEL_CFG_REG at offset 0x154 + 4*x +// Each register has: +// IN_SEL: bits 0-4 — which GPIO pin to route to this peripheral input signal +// IN_INV_SEL: bit 5 — invert input + +register_bitfields! [ + u32, + + pub FuncInSelCfg [ + IN_SEL OFFSET(0) NUMBITS(5) [], // GPIO pin number for input + IN_INV_SEL OFFSET(5) NUMBITS(1) [], // Invert input + SEL OFFSET(6) NUMBITS(1) [], // 1 = route via GPIO Matrix (do not bypass), 0 = bypass + ], +]; + +// ─── Helper: write IO_MUX register for a GPIO pin ──────────────────────── + +fn write_io_mux(pin: u8, mcu_sel: u32, ie: bool, pu: bool, pd: bool, drv: u32) { + let offset = IO_MUX_OFFSETS[pin as usize]; + let addr = IO_MUX_BASE + offset as usize; + let reg = unsafe { &*(addr as *const ReadWrite) }; + reg.write( + IoMuxFields::MCU_SEL.val(mcu_sel) + + IoMuxFields::FUN_IE.val(if ie { 1 } else { 0 }) + + IoMuxFields::FUN_PU.val(if pu { 1 } else { 0 }) + + IoMuxFields::FUN_PD.val(if pd { 1 } else { 0 }) + + IoMuxFields::FUN_DRV.val(drv), + ); +} + +// ─── Helper: route peripheral output signal to a GPIO pin ──────────────── +// Writes GPIO_FUNCx_OUT_SEL_CFG_REG to connect signal_idx to GPIO pin. + +fn route_signal_out(pin: u8, signal_idx: u32, oen_sel: u32) { + let offset = 0x554 + 4 * pin as usize; + let addr = 0x60004000 + offset; + let reg = unsafe { &*(addr as *const ReadWrite) }; + reg.write( + FuncOutSelCfg::OUT_SEL.val(signal_idx) + + FuncOutSelCfg::INV_SEL.val(0) + + FuncOutSelCfg::OEN_SEL.val(oen_sel) + + FuncOutSelCfg::OEN_INV_SEL.val(0), + ); +} + +// ─── Helper: route GPIO pin to peripheral input signal ─────────────────── +// Writes GPIO_FUNCx_IN_SEL_CFG_REG to connect GPIO pin to signal_idx. + +fn route_signal_in(signal_idx: u32, pin: u32) { + let offset = 0x154 + 4 * signal_idx as usize; + let addr = 0x60004000 + offset; + let reg = unsafe { &*(addr as *const ReadWrite) }; + reg.write( + FuncInSelCfg::SEL.val(1) + FuncInSelCfg::IN_INV_SEL.val(0) + FuncInSelCfg::IN_SEL.val(pin), + ); +} + +// ─── Esp32IoMuxPinctrl — PinMux configuration for define_pin_states! ──── + +/// ESP32-C3 PinMux configuration entry +/// +/// Combines IO_MUX, GPIO Matrix, and GPIO output enable configuration. +/// Used with `define_pin_states!` macro to configure SPI2 pins. +/// +/// SPI2 (FSPI) signals on ESP32-C3 are routed through the GPIO Matrix, +/// not via IO_MUX FUNC selection. MCU_SEL=1 (GPIO function) is required +/// so the GPIO Matrix can take over signal routing. +pub struct Esp32IoMuxPinctrl { + pin: u8, + mcu_sel: u32, + ie: bool, + pu: bool, + pd: bool, + drv: u32, + out_signal: Option, + in_signal: Option, + gpio_output: bool, +} + +impl Esp32IoMuxPinctrl { + pub const fn new( + pin: u8, + mcu_sel: u32, + ie: bool, + pu: bool, + pd: bool, + drv: u32, + out_signal: Option, + in_signal: Option, + gpio_output: bool, + ) -> Self { + Esp32IoMuxPinctrl { + pin, + mcu_sel, + ie, + pu, + pd, + drv, + out_signal, + in_signal, + gpio_output, + } + } +} + +impl AlterFuncPin for Esp32IoMuxPinctrl { + fn init(&self) { + // 1. Configure IO_MUX: function select, pull-up/down, input enable, drive + write_io_mux(self.pin, self.mcu_sel, self.ie, self.pu, self.pd, self.drv); + + // 2. Route peripheral output signal to this GPIO pin via GPIO Matrix + if let Some(signal_idx) = self.out_signal { + if self.gpio_output { + // Software-controlled pin (e.g., CS): OEN_SEL=1 (GPIO_ENABLE controls) + route_signal_out(self.pin, signal_idx, 1u32); + } else { + // Peripheral-controlled pin (CLK, MOSI): OEN_SEL=0 (peripheral controls) + route_signal_out(self.pin, signal_idx, 0u32); + } + } + + // 3. Route this GPIO pin to peripheral input signal via GPIO Matrix + if let Some(signal_idx) = self.in_signal { + route_signal_in(signal_idx, self.pin as u32); + } + + // 4. Enable GPIO output for software-controlled pins (CS). + // Order matters: pre-set the output latch HIGH *before* enabling output, + // so the pin is driven high the instant output turns on — no low glitch + // on CS during the enable moment. + if self.gpio_output { + let gpio_regs = &*GPIO_BASE; + gpio_regs.out_w1ts.write(GpioOut::DATA.val(1 << self.pin)); + gpio_regs + .enable_w1ts + .write(GpioEnable::DATA.val(1 << self.pin)); + } + } +} + +// ─── Esp32GpioOutputPin — OutputPin for ExclusiveDevice CS control ────── + +/// GPIO output pin for ESP32-C3 +/// +/// Implements `embedded_hal::digital::OutputPin` for use as CS pin +/// with `embedded_hal_bus::spi::ExclusiveDevice`. +/// +/// Uses atomic W1TS/W1TC registers for glitch-free set_low/set_high. +pub struct Esp32GpioOutputPin; + +impl Esp32GpioOutputPin { + pub const fn new() -> Self { + Esp32GpioOutputPin + } +} + +impl embedded_hal::digital::ErrorType for Esp32GpioOutputPin { + type Error = core::convert::Infallible; +} + +impl OutputPin for Esp32GpioOutputPin { + fn set_low(&mut self) -> Result<(), Self::Error> { + // Assert CS (active-low) — drive pin LOW + let gpio_regs = &*GPIO_BASE; + gpio_regs.out_w1tc.write(GpioOut::DATA.val(1 << PIN)); + Ok(()) + } + + fn set_high(&mut self) -> Result<(), Self::Error> { + // Deassert CS — drive pin HIGH + let gpio_regs = &*GPIO_BASE; + gpio_regs.out_w1ts.write(GpioOut::DATA.val(1 << PIN)); + Ok(()) + } +} diff --git a/driver/src/gpio/mod.rs b/driver/src/gpio/mod.rs new file mode 100644 index 000000000..223d420b3 --- /dev/null +++ b/driver/src/gpio/mod.rs @@ -0,0 +1,16 @@ +// 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(target_chip = "esp32c3")] +pub mod esp32_gpio; diff --git a/driver/src/lib.rs b/driver/src/lib.rs index ce49feb44..23f240d73 100644 --- a/driver/src/lib.rs +++ b/driver/src/lib.rs @@ -16,10 +16,12 @@ #![feature(const_nonnull_new)] pub mod clock_control; +pub mod gpio; pub mod i2c; pub mod interrupt_controller; pub mod pinctrl; pub mod reset; +pub mod spi; pub mod static_ref; pub mod systimer; pub mod uart; diff --git a/driver/src/spi/esp32_spi2.rs b/driver/src/spi/esp32_spi2.rs new file mode 100644 index 000000000..7434e17e2 --- /dev/null +++ b/driver/src/spi/esp32_spi2.rs @@ -0,0 +1,619 @@ +// 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. + +// ESP32-C3 GPSPI2 (SPI2) register-level driver +// +// Reference: ESP32-C3 Technical Reference Manual, Chapter 7 (SPI) + +use crate::{ + spi::{SpiBitOrder, SpiConfig, SpiPhase, SpiPolarity}, + static_ref::StaticRef, +}; +use blueos_hal::{Configuration, PlatPeri}; +use tock_registers::{ + interfaces::{ReadWriteable, Readable, Writeable}, + register_bitfields, register_structs, + registers::ReadWrite, +}; + +const SPI2_BASE: StaticRef = + unsafe { StaticRef::new(0x6002_4000 as *const Spi2Registers) }; + +const SYSTEM_BASE: StaticRef = + unsafe { StaticRef::new(0x600C_0000 as *const SystemRegisters) }; + +const APB_CLK_HZ: u32 = 80_000_000; +const SPI2_DATA_BUF_SIZE: usize = 64; + +// Pad byte for full-duplex reads where read length exceeds write length. +const EMPTY_WRITE_PAD: u8 = 0x00; + +register_bitfields! [ + u32, + + pub CMD [ + USR OFFSET(24) NUMBITS(1) [], + UPDATE OFFSET(23) NUMBITS(1) [], + ], + + pub CTRL [ + RD_BIT_ORDER OFFSET(25) NUMBITS(1) [ + MsbFirst = 0, + LsbFirst = 1, + ], + WR_BIT_ORDER OFFSET(26) NUMBITS(1) [ + MsbFirst = 0, + LsbFirst = 1, + ], + ], + + pub CLOCK [ + CLKCNT_L OFFSET(0) NUMBITS(6) [], + CLKCNT_H OFFSET(6) NUMBITS(6) [], + CLKCNT_N OFFSET(12) NUMBITS(6) [], + CLKDIV_PRE OFFSET(18) NUMBITS(4) [], + CLK_EQU_SYSCLK OFFSET(31) NUMBITS(1) [], + ], + + pub USER [ + DOUTDIN OFFSET(0) NUMBITS(1) [ + HalfDuplex = 0, + FullDuplex = 1, + ], + CK_OUT_EDGE OFFSET(9) NUMBITS(1) [ + LeadingEdge = 0, + TrailingEdge = 1, + ], + USR_MOSI OFFSET(27) NUMBITS(1) [], + USR_MISO OFFSET(28) NUMBITS(1) [], + USR_DUMMY OFFSET(29) NUMBITS(1) [], + USR_ADDR OFFSET(30) NUMBITS(1) [], + USR_COMMAND OFFSET(31) NUMBITS(1) [], + ], + + pub USER1 [ + USR_DUMMY_CYCLELEN OFFSET(0) NUMBITS(8) [], + USR_ADDR_BITLEN OFFSET(27) NUMBITS(5) [], + ], + + pub USER2 [ + USR_COMMAND_VALUE OFFSET(0) NUMBITS(16) [], + USR_COMMAND_BITLEN OFFSET(28) NUMBITS(4) [], + ], + + pub MS_DLEN [ + MS_DATA_BITLEN OFFSET(0) NUMBITS(18) [], + ], + + pub MISC [ + CS0_DIS OFFSET(0) NUMBITS(1) [ + Enabled = 0, + Disabled = 1, + ], + CS1_DIS OFFSET(1) NUMBITS(1) [ + Enabled = 0, + Disabled = 1, + ], + CS2_DIS OFFSET(2) NUMBITS(1) [ + Enabled = 0, + Disabled = 1, + ], + CS3_DIS OFFSET(3) NUMBITS(1) [ + Enabled = 0, + Disabled = 1, + ], + CS4_DIS OFFSET(4) NUMBITS(1) [ + Enabled = 0, + Disabled = 1, + ], + CS5_DIS OFFSET(5) NUMBITS(1) [ + Enabled = 0, + Disabled = 1, + ], + CK_IDLE_EDGE OFFSET(29) NUMBITS(1) [ + Low = 0, + High = 1, + ], + CS_KEEP_ACTIVE OFFSET(30) NUMBITS(1) [], + ], + + pub SLAVE [ + CLK_MODE OFFSET(0) NUMBITS(2) [], + CLK_MODE_13 OFFSET(2) NUMBITS(1) [], + SLAVE_MODE OFFSET(26) NUMBITS(1) [ + Master = 0, + Slave = 1, + ], + SOFT_RESET OFFSET(27) NUMBITS(1) [], + ], + + pub CLK_GATE [ + CLK_EN OFFSET(0) NUMBITS(1) [], + MST_CLK_ACTIVE OFFSET(1) NUMBITS(1) [], + MST_CLK_SEL OFFSET(2) NUMBITS(1) [ + XtalClock = 0, + PllClock = 1, + ], + ], + + pub DMA_CONF [ + DMA_RX_ENA OFFSET(27) NUMBITS(1) [], + DMA_TX_ENA OFFSET(28) NUMBITS(1) [], + RX_AFIFO_RST OFFSET(29) NUMBITS(1) [], + BUF_AFIFO_RST OFFSET(30) NUMBITS(1) [], + ], + + pub DMA_INT_RAW [ + TRANS_DONE OFFSET(12) NUMBITS(1) [], + ], + + pub DMA_INT_CLR [ + TRANS_DONE OFFSET(12) NUMBITS(1) [], + ], + + pub PERIP_CLK_EN0 [ + SPI2_CLK_EN OFFSET(6) NUMBITS(1) [ + Enabled = 1, + Disabled = 0, + ], + ], + + pub PERIP_RST_EN0 [ + SPI2_RST OFFSET(6) NUMBITS(1) [ + NoReset = 0, + Reset = 1, + ], + ], +]; + +register_structs! { + Spi2Registers { + (0x00 => cmd: ReadWrite), + (0x04 => addr: ReadWrite), + (0x08 => ctrl: ReadWrite), + (0x0C => clock: ReadWrite), + (0x10 => user: ReadWrite), + (0x14 => user1: ReadWrite), + (0x18 => user2: ReadWrite), + (0x1C => ms_dlen: ReadWrite), + (0x20 => misc: ReadWrite), + (0x24 => din_mode: ReadWrite), + (0x28 => din_num: ReadWrite), + (0x2C => dout_mode: ReadWrite), + (0x30 => dma_conf: ReadWrite), + (0x34 => dma_int_ena: ReadWrite), + (0x38 => dma_int_clr: ReadWrite), + (0x3C => dma_int_raw: ReadWrite), + (0x40 => dma_int_st: ReadWrite), + (0x44 => _reserved0), + (0x98 => w0: ReadWrite), + (0x9C => w1: ReadWrite), + (0xA0 => w2: ReadWrite), + (0xA4 => w3: ReadWrite), + (0xA8 => w4: ReadWrite), + (0xAC => w5: ReadWrite), + (0xB0 => w6: ReadWrite), + (0xB4 => w7: ReadWrite), + (0xB8 => w8: ReadWrite), + (0xBC => w9: ReadWrite), + (0xC0 => w10: ReadWrite), + (0xC4 => w11: ReadWrite), + (0xC8 => w12: ReadWrite), + (0xCC => w13: ReadWrite), + (0xD0 => w14: ReadWrite), + (0xD4 => w15: ReadWrite), + (0xD8 => _reserved1), + (0xE0 => slave: ReadWrite), + (0xE4 => slave1: ReadWrite), + (0xE8 => clk_gate: ReadWrite), + (0xEC => _reserved2), + (0xF0 => @END), + } +} + +// Minimal system registers for SPI2 clock gating and reset +register_structs! { + SystemRegisters { + (0x00 => _reserved_sys0), + (0x10 => perip_clk_en0: ReadWrite), + (0x14 => _reserved_sys1), + (0x18 => perip_rst_en0: ReadWrite), + (0x1C => @END), + } +} + +pub struct Esp32Spi2 {} + +unsafe impl Send for Esp32Spi2 {} +unsafe impl Sync for Esp32Spi2 {} + +impl Esp32Spi2 { + pub const fn new() -> Self { + Self {} + } + + fn write_buf(&self, data: &[u8]) { + debug_assert!(data.len() <= SPI2_DATA_BUF_SIZE); + let regs = &*SPI2_BASE; + let words = data.chunks(4); + for (i, chunk) in words.enumerate() { + let mut word = 0u32; + for (j, byte) in chunk.iter().enumerate() { + word |= (*byte as u32) << (j * 8); + } + match i { + 0 => regs.w0.set(word), + 1 => regs.w1.set(word), + 2 => regs.w2.set(word), + 3 => regs.w3.set(word), + 4 => regs.w4.set(word), + 5 => regs.w5.set(word), + 6 => regs.w6.set(word), + 7 => regs.w7.set(word), + 8 => regs.w8.set(word), + 9 => regs.w9.set(word), + 10 => regs.w10.set(word), + 11 => regs.w11.set(word), + 12 => regs.w12.set(word), + 13 => regs.w13.set(word), + 14 => regs.w14.set(word), + 15 => regs.w15.set(word), + _ => break, + } + } + } + + fn read_buf(&self, data: &mut [u8]) { + let regs = &*SPI2_BASE; + let words = [ + regs.w0.get(), + regs.w1.get(), + regs.w2.get(), + regs.w3.get(), + regs.w4.get(), + regs.w5.get(), + regs.w6.get(), + regs.w7.get(), + regs.w8.get(), + regs.w9.get(), + regs.w10.get(), + regs.w11.get(), + regs.w12.get(), + regs.w13.get(), + regs.w14.get(), + regs.w15.get(), + ]; + for (i, byte) in data.iter_mut().enumerate() { + let word_idx = i / 4; + let byte_idx = i % 4; + if word_idx < words.len() { + *byte = ((words[word_idx] >> (byte_idx * 8)) & 0xFF) as u8; + } + } + } + + fn apply_config(&self) { + let regs = &*SPI2_BASE; + regs.cmd.write(CMD::UPDATE.val(1)); + while regs.cmd.is_set(CMD::UPDATE) {} + } + + // AFIFO reset must be a SET-then-CLEAR pulse; leaving the bit set keeps the + // FIFO held in reset and corrupts subsequent transfers on real hardware. + fn reset_tx_fifo(&self) { + let regs = &*SPI2_BASE; + regs.dma_conf.modify(DMA_CONF::BUF_AFIFO_RST::SET); + regs.dma_conf.modify(DMA_CONF::BUF_AFIFO_RST::CLEAR); + } + + fn reset_rx_fifo(&self) { + let regs = &*SPI2_BASE; + regs.dma_conf.modify(DMA_CONF::RX_AFIFO_RST::SET); + regs.dma_conf.modify(DMA_CONF::RX_AFIFO_RST::CLEAR); + } + + fn reset_tx_rx_fifo(&self) { + let regs = &*SPI2_BASE; + regs.dma_conf + .modify(DMA_CONF::BUF_AFIFO_RST::SET + DMA_CONF::RX_AFIFO_RST::SET); + regs.dma_conf + .modify(DMA_CONF::BUF_AFIFO_RST::CLEAR + DMA_CONF::RX_AFIFO_RST::CLEAR); + } + + fn start_transfer(&self) { + let regs = &*SPI2_BASE; + // Sync config registers into the shadow registers before each transfer. + // Matches esp-hal start_operation: update() first. + regs.cmd.modify(CMD::UPDATE.val(1)); + while regs.cmd.is_set(CMD::UPDATE) {} + // Clear any pending TRANS_DONE from a previous transfer before starting. + regs.dma_int_clr.write(DMA_INT_CLR::TRANS_DONE::SET); + // Kick off the user-defined transaction. USR self-clears on completion. + // Use modify (not write) so other CMD bits stay untouched. + regs.cmd.modify(CMD::USR.val(1)); + while regs.cmd.is_set(CMD::USR) {} + } + + fn wait_done(&self) { + // let regs = &*SPI2_BASE; + // while !regs.dma_int_raw.is_set(DMA_INT_RAW::TRANS_DONE) {} + // regs.dma_int_clr.write(DMA_INT_CLR::TRANS_DONE::SET); + } + + fn configure_clock(&self, baudrate: u32) -> blueos_hal::err::Result<()> { + let regs = &*SPI2_BASE; + if baudrate >= APB_CLK_HZ { + regs.clock.write(CLOCK::CLK_EQU_SYSCLK.val(1)); + return Ok(()); + } + + // f_spi = f_apb / ((pre+1) * (n+1)), minimum divisor = 2 + let divisor = (APB_CLK_HZ / baudrate).max(2); + + // Find pre and n such that (pre+1)*(n+1) = divisor + // Prefer larger n for better duty cycle + let mut best_pre = 0u32; + let mut best_n_plus_one = 0u32; + for pre in 0..16u32 { + let n_plus_one = divisor / (pre + 1); + if n_plus_one >= 2 && n_plus_one <= 64 { + let actual = (pre + 1) * n_plus_one; + if actual == divisor { + best_pre = pre; + best_n_plus_one = n_plus_one; + break; + } + if best_n_plus_one == 0 { + best_pre = pre; + best_n_plus_one = n_plus_one; + } + } + } + + // No valid combination found — minimum achievable is APB/1024 (~78kHz) + if best_n_plus_one == 0 { + return Err(blueos_hal::err::HalError::NotSupport); + } + + let n = best_n_plus_one - 1; + let h = ((best_n_plus_one / 2).max(1) - 1) as u32; + regs.clock.write( + CLOCK::CLKCNT_L.val(n as u32) + + CLOCK::CLKCNT_H.val(h) + + CLOCK::CLKCNT_N.val(n as u32) + + CLOCK::CLKDIV_PRE.val(best_pre as u32) + + CLOCK::CLK_EQU_SYSCLK.val(0), + ); + Ok(()) + } + + fn do_half_duplex_write(&self, data: &[u8]) -> blueos_hal::err::Result<()> { + if data.is_empty() { + return Ok(()); + } + let regs = &*SPI2_BASE; + + regs.user.modify( + USER::DOUTDIN.val(0) + + USER::USR_MOSI::SET + + USER::USR_MISO::CLEAR + + USER::USR_COMMAND::CLEAR + + USER::USR_ADDR::CLEAR + + USER::USR_DUMMY::CLEAR, + ); + + for chunk in data.chunks(SPI2_DATA_BUF_SIZE) { + self.reset_tx_fifo(); + regs.ms_dlen + .write(MS_DLEN::MS_DATA_BITLEN.val((chunk.len() as u32 * 8 - 1))); + self.write_buf(chunk); + self.start_transfer(); + self.wait_done(); + } + Ok(()) + } + + fn do_half_duplex_read(&self, data: &mut [u8]) -> blueos_hal::err::Result<()> { + if data.is_empty() { + return Ok(()); + } + let regs = &*SPI2_BASE; + + // Full-duplex dummy read: write 0x00 while reading, so SCLK is driven by + // the MOSI phase. Matches esp-hal Driver::read (USR_MOSI+USR_MISO+DOUTDIN). + // Half-duplex MISO-only was returning misaligned data on real hardware. + regs.user.modify( + USER::DOUTDIN.val(1) + + USER::USR_MOSI::SET + + USER::USR_MISO::SET + + USER::USR_COMMAND::CLEAR + + USER::USR_ADDR::CLEAR + + USER::USR_DUMMY::CLEAR, + ); + + for chunk in data.chunks_mut(SPI2_DATA_BUF_SIZE) { + self.reset_tx_rx_fifo(); + regs.ms_dlen + .write(MS_DLEN::MS_DATA_BITLEN.val((chunk.len() as u32 * 8 - 1))); + let dummy = [EMPTY_WRITE_PAD; SPI2_DATA_BUF_SIZE]; + self.write_buf(&dummy[..chunk.len()]); + self.start_transfer(); + self.wait_done(); + self.read_buf(chunk); + } + Ok(()) + } + + fn do_full_duplex_transfer( + &self, + read: &mut [u8], + write: &[u8], + ) -> blueos_hal::err::Result<()> { + if read.is_empty() && write.is_empty() { + return Ok(()); + } + let regs = &*SPI2_BASE; + + regs.user.modify( + USER::DOUTDIN.val(1) + + USER::USR_MOSI::SET + + USER::USR_MISO::SET + + USER::USR_COMMAND::CLEAR + + USER::USR_ADDR::CLEAR + + USER::USR_DUMMY::CLEAR, + ); + + // Independent read/write cursors (matches esp-hal Driver::transfer): + // each side advances min(FIFO_SIZE, remaining) on its own. When read + // exceeds write in a chunk, the write side is padded with EMPTY_WRITE_PAD + // so enough SCLK cycles are generated for the read. + let mut write_from = 0usize; + let mut read_from = 0usize; + loop { + let write_inc = core::cmp::min(SPI2_DATA_BUF_SIZE, write.len() - write_from); + let read_inc = core::cmp::min(SPI2_DATA_BUF_SIZE, read.len() - read_from); + if write_inc == 0 && read_inc == 0 { + break; + } + + let this_len = write_inc.max(read_inc); + self.reset_tx_rx_fifo(); + regs.ms_dlen + .write(MS_DLEN::MS_DATA_BITLEN.val((this_len as u32 * 8 - 1))); + + if write_inc < read_inc { + // Read more than we write: pad write side up to read_inc bytes. + let mut buf = [EMPTY_WRITE_PAD; SPI2_DATA_BUF_SIZE]; + buf[..write_inc].copy_from_slice(&write[write_from..][..write_inc]); + self.write_buf(&buf[..read_inc]); + } else { + self.write_buf(&write[write_from..][..write_inc]); + } + + self.start_transfer(); + self.wait_done(); + + if read_inc > 0 { + let mut tmp = [0u8; SPI2_DATA_BUF_SIZE]; + self.read_buf(&mut tmp[..read_inc]); + read[read_from..][..read_inc].copy_from_slice(&tmp[..read_inc]); + } + + write_from += write_inc; + read_from += read_inc; + } + Ok(()) + } +} + +impl PlatPeri for Esp32Spi2 { + fn enable(&self) { + let sys = &*SYSTEM_BASE; + sys.perip_clk_en0.modify(PERIP_CLK_EN0::SPI2_CLK_EN::SET); + // Reset pulse (assert then release), matching ESP-IDF spi_ll_reset_register, + // so SPI2 starts from a clean state — otherwise CMD::UPDATE may never clear. + sys.perip_rst_en0.modify(PERIP_RST_EN0::SPI2_RST::SET); + sys.perip_rst_en0.modify(PERIP_RST_EN0::SPI2_RST::CLEAR); + + let regs = &*SPI2_BASE; + regs.clk_gate.write( + CLK_GATE::CLK_EN.val(1) + + CLK_GATE::MST_CLK_ACTIVE.val(1) + + CLK_GATE::MST_CLK_SEL.val(1), + ); + } + + fn disable(&self) { + let regs = &*SPI2_BASE; + regs.clk_gate.modify(CLK_GATE::MST_CLK_ACTIVE::CLEAR); + let sys = &*SYSTEM_BASE; + sys.perip_clk_en0.modify(PERIP_CLK_EN0::SPI2_CLK_EN::CLEAR); + } +} + +impl Configuration for Esp32Spi2 { + type Target = (); + + fn configure(&self, config: &SpiConfig) -> blueos_hal::err::Result { + let regs = &*SPI2_BASE; + + // Ensure peripheral is enabled + self.enable(); + + // Master mode, soft reset first + regs.slave + .write(SLAVE::SLAVE_MODE.val(0) + SLAVE::SOFT_RESET.val(1)); + regs.slave.modify(SLAVE::SOFT_RESET::CLEAR); + + // No DMA, clear FIFOs + regs.dma_conf + .write(DMA_CONF::DMA_RX_ENA::CLEAR + DMA_CONF::DMA_TX_ENA::CLEAR); + self.reset_tx_rx_fifo(); + + // SPI mode from phase + polarity + let ck_idle_edge = match config.polarity { + SpiPolarity::Low => MISC::CK_IDLE_EDGE::Low, + SpiPolarity::High => MISC::CK_IDLE_EDGE::High, + }; + let ck_out_edge = match (config.polarity, config.phase) { + (SpiPolarity::Low, SpiPhase::Phase0) => USER::CK_OUT_EDGE::LeadingEdge, // Mode 0 + (SpiPolarity::Low, SpiPhase::Phase1) => USER::CK_OUT_EDGE::TrailingEdge, // Mode 1 + (SpiPolarity::High, SpiPhase::Phase0) => USER::CK_OUT_EDGE::TrailingEdge, // Mode 2 + (SpiPolarity::High, SpiPhase::Phase1) => USER::CK_OUT_EDGE::LeadingEdge, // Mode 3 + }; + regs.misc.modify(ck_idle_edge); + regs.user.modify(ck_out_edge); + + // Bit order + let rd_bit_order = match config.bit_order { + SpiBitOrder::MsbFirst => CTRL::RD_BIT_ORDER::MsbFirst, + SpiBitOrder::LsbFirst => CTRL::RD_BIT_ORDER::LsbFirst, + }; + let wr_bit_order = match config.bit_order { + SpiBitOrder::MsbFirst => CTRL::WR_BIT_ORDER::MsbFirst, + SpiBitOrder::LsbFirst => CTRL::WR_BIT_ORDER::LsbFirst, + }; + regs.ctrl.modify(rd_bit_order + wr_bit_order); + + // Clock divider + self.configure_clock(config.baudrate)?; + + // All HW CS lines disabled; CS managed by software GPIO via ExclusiveDevice + regs.misc.modify( + MISC::CS0_DIS.val(1) + + MISC::CS1_DIS.val(1) + + MISC::CS2_DIS.val(1) + + MISC::CS3_DIS.val(1) + + MISC::CS4_DIS.val(1) + + MISC::CS5_DIS.val(1) + + MISC::CS_KEEP_ACTIVE::CLEAR, + ); + + Ok(()) + } +} + +impl blueos_hal::spi::Spi for Esp32Spi2 { + fn transfer(&self, read: &mut [u8], write: &[u8]) -> blueos_hal::err::Result<()> { + self.do_full_duplex_transfer(read, write) + } + + fn read(&self, buf: &mut [u8]) -> blueos_hal::err::Result<()> { + self.do_half_duplex_read(buf) + } + + fn write(&self, buf: &[u8]) -> blueos_hal::err::Result<()> { + self.do_half_duplex_write(buf) + } +} diff --git a/driver/src/spi/mod.rs b/driver/src/spi/mod.rs new file mode 100644 index 000000000..3a03c864e --- /dev/null +++ b/driver/src/spi/mod.rs @@ -0,0 +1,68 @@ +// 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(target_chip = "esp32c3")] +pub mod esp32_spi2; + +/// SPI clock phase configuration +/// +/// Phase0 (CPHA=0): Data captured on the first clock transition. +/// Phase1 (CPHA=1): Data captured on the second clock transition. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SpiPhase { + Phase0, + Phase1, +} + +/// SPI clock polarity configuration +/// +/// Low (CPOL=0): Clock signal low when idle. +/// High (CPOL=1): Clock signal high when idle. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SpiPolarity { + Low, + High, +} + +/// SPI bit order configuration +/// +/// MsbFirst: Most significant bit transmitted first (standard for SPI NOR Flash). +/// LsbFirst: Least significant bit transmitted first. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SpiBitOrder { + MsbFirst, + LsbFirst, +} + +/// SPI peripheral configuration — used as `P` for HAL `Spi` trait +pub struct SpiConfig { + pub baudrate: u32, + pub phase: SpiPhase, + pub polarity: SpiPolarity, + pub bit_order: SpiBitOrder, + pub cs_pin: Option, // Unused — CS managed by ExclusiveDevice via GPIO OutputPin +} + +impl SpiConfig { + /// Mode 0 (CPOL=0, CPHA=0), MSB-first, 20MHz — typical SPI NOR Flash config + pub fn spi_flash_default() -> Self { + SpiConfig { + baudrate: 20_000_000, + phase: SpiPhase::Phase0, + polarity: SpiPolarity::Low, + bit_order: SpiBitOrder::MsbFirst, + cs_pin: None, + } + } +} diff --git a/hal/src/lib.rs b/hal/src/lib.rs index 179f4640c..d37430dfc 100644 --- a/hal/src/lib.rs +++ b/hal/src/lib.rs @@ -26,6 +26,7 @@ pub mod i2c; pub mod isr; pub mod pinctrl; pub mod reset; +pub mod spi; pub mod uart; /// Hardware abstraction layer peripheral configuration trait diff --git a/hal/src/spi.rs b/hal/src/spi.rs new file mode 100644 index 000000000..407e5c1d6 --- /dev/null +++ b/hal/src/spi.rs @@ -0,0 +1,33 @@ +// 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 peripheral trait — full-duplex transfer + half-duplex read/write +pub trait Spi: super::PlatPeri + super::Configuration { + /// Full-duplex transfer: simultaneously write `write` and read into `read`. + /// + /// Runs for `max(read.len(), write.len())` bytes. Overlapping/aliasing + /// buffers is permitted (SPI hardware reads MISO and writes MOSI on + /// separate lines simultaneously). + fn transfer(&self, read: &mut [u8], write: &[u8]) -> super::err::Result<()>; + + /// Half-duplex read: read data into `buf` from the SPI slave. + /// + /// The word value sent on MOSI during reading is implementation-defined, + /// typically `0x00` or `0xFF`. + fn read(&self, buf: &mut [u8]) -> super::err::Result<()>; + + /// Half-duplex write: write data from `buf` to the SPI slave, + /// discarding all incoming bytes on MISO. + fn write(&self, buf: &[u8]) -> super::err::Result<()>; +} diff --git a/kernel/BUILD.gn b/kernel/BUILD.gn index 499ea36da..b98bb1a55 100644 --- a/kernel/BUILD.gn +++ b/kernel/BUILD.gn @@ -50,6 +50,8 @@ _shared_deps = [ "//external/vendor/bitflags-2.10.0:bitflags", "//external/vendor/cfg-if-1.0.4:cfg_if", "//external/vendor/const-default-1.0.0:const_default", + "//external/vendor/embedded-hal-1.0.0:embedded_hal", + "//external/vendor/embedded-hal-bus-0.3.0:embedded_hal_bus", "//external/vendor/embedded-io-0.6.1:embedded_io", "//external/vendor/heapless-0.8.0:heapless", "//external/vendor/log-0.4.28:log", diff --git a/kernel/src/devices/mod.rs b/kernel/src/devices/mod.rs index 86f6cad12..29f1f9563 100644 --- a/kernel/src/devices/mod.rs +++ b/kernel/src/devices/mod.rs @@ -36,6 +36,7 @@ pub mod i2c_core; #[cfg(enable_net)] pub(crate) mod net; mod null; +pub mod spi_core; pub mod tty; #[cfg(virtio)] pub mod virtio; 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/spi_core/mod.rs b/kernel/src/devices/spi_core/mod.rs new file mode 100644 index 000000000..56d071305 --- /dev/null +++ b/kernel/src/devices/spi_core/mod.rs @@ -0,0 +1,15 @@ +// 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. + +pub mod block_spi; diff --git a/kernel/src/drivers/sensor/bme280.rs b/kernel/src/drivers/sensor/bme280.rs index 7d4fe32b1..9b7e2b551 100644 --- a/kernel/src/drivers/sensor/bme280.rs +++ b/kernel/src/drivers/sensor/bme280.rs @@ -12,33 +12,17 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crate::time::Tick; use blueos_driver::i2c::I2cConfig; use blueos_hal::PlatPeri; use blueos_infra::tinyarc::TinyArc; use bme280::i2c::BME280; -use embedded_hal::delay::DelayNs; use crate::{ devices::{bus::Bus, i2c_core::block_i2c::BlockI2c, DeviceData}, drivers::{DriverModule, InitDriver}, - scheduler, - sync::SpinLock, + sync::{KernelDelay, SpinLock}, }; -struct KernelDelay; - -impl DelayNs for KernelDelay { - fn delay_ns(&mut self, ns: u32) { - let ticks = blueos_kconfig::CONFIG_TICKS_PER_SECOND as u32 * ns / 1_000_000_000; - if ticks == 0 { - scheduler::yield_me(); - } else { - scheduler::suspend_me_for::<()>(Tick(ticks as usize), None); - } - } -} - #[derive(Default)] pub struct Bme280Config { pub device_addr: u8, diff --git a/kernel/src/sync/delay.rs b/kernel/src/sync/delay.rs new file mode 100644 index 000000000..6d1799d7d --- /dev/null +++ b/kernel/src/sync/delay.rs @@ -0,0 +1,80 @@ +// 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. + +use crate::{scheduler, time::Tick}; +use embedded_hal::delay::DelayNs; + +/// Kernel delay adapter — implements `embedded_hal::delay::DelayNs` +/// +/// Converts nanosecond delays into kernel scheduler operations: +/// sub-tick delays yield the thread; multi-tick delays suspend with timer wakeup. +pub struct KernelDelay; + +impl DelayNs for KernelDelay { + fn delay_ns(&mut self, ns: u32) { + if !scheduler::is_schedule_ready() { + #[cfg(target_arch = "riscv32")] + { + // rdcycle is unreliable on ESP32-C3 (mcycle may not advance), so a + // cycle-counter-based spin hangs forever. Spin a fixed count instead. + // ~ns CPU cycles at 160 MHz, conservative (spin_loop == 1 cycle). + let spins = (ns as u64).saturating_mul(160) / 1_000; + for _ in 0..spins { + core::hint::spin_loop(); + } + } + #[cfg(not(target_arch = "riscv32"))] + { + let _ = ns; + } + return; + } + let ticks = blueos_kconfig::CONFIG_TICKS_PER_SECOND as u32 * ns / 1_000_000_000; + if ticks == 0 { + // yield_me() is a no-op in single-task shell; spin a real delay so + // flash wait_busy gets a real per-iteration budget. + #[cfg(target_arch = "riscv32")] + { + let spins = (ns as u64).saturating_mul(160) / 1_000; + for _ in 0..spins { + core::hint::spin_loop(); + } + } + #[cfg(not(target_arch = "riscv32"))] + { + scheduler::yield_me(); + } + } else { + scheduler::suspend_me_for::<()>(Tick(ticks as usize), None); + } + } +} + +#[cfg(target_arch = "riscv32")] +#[allow(dead_code)] +#[inline] +fn read_cycle() -> u64 { + let hi: u32; + let lo: u32; + unsafe { + core::arch::asm!( + "rdcycle {lo}", + "rdcycleh {hi}", + hi = out(reg) hi, + lo = out(reg) lo, + options(nostack, nomem), + ); + } + ((hi as u64) << 32) + lo as u64 +} diff --git a/kernel/src/sync/mod.rs b/kernel/src/sync/mod.rs index 98e8ef10b..35c9cc320 100644 --- a/kernel/src/sync/mod.rs +++ b/kernel/src/sync/mod.rs @@ -14,6 +14,8 @@ pub mod atomic_wait; pub use atomic_wait::{atomic_wait, atomic_wake}; +pub mod delay; +pub use delay::KernelDelay; pub mod mqueue; pub mod mutex; pub mod posix_mqueue; From a11e82ce8ae42ef3f8515c835160a77b5bf8c032 Mon Sep 17 00:00:00 2001 From: jinwjinl <2532191107@qq.com> Date: Thu, 2 Jul 2026 15:28:49 +0800 Subject: [PATCH 2/3] Transfer part of the implementation logic to pinctrl --- driver/src/gpio/esp32_gpio.rs | 286 ++-------------------------- driver/src/pinctrl/esp32_pinctrl.rs | 199 +++++++++++++++++++ driver/src/pinctrl/mod.rs | 3 + driver/src/spi/esp32_spi2.rs | 38 ++-- driver/src/spi/mod.rs | 15 +- hal/src/spi.rs | 14 +- kernel/src/sync/delay.rs | 7 +- 7 files changed, 233 insertions(+), 329 deletions(-) create mode 100644 driver/src/pinctrl/esp32_pinctrl.rs diff --git a/driver/src/gpio/esp32_gpio.rs b/driver/src/gpio/esp32_gpio.rs index ab8260cb7..c4cd65f40 100644 --- a/driver/src/gpio/esp32_gpio.rs +++ b/driver/src/gpio/esp32_gpio.rs @@ -12,103 +12,18 @@ // See the License for the specific language governing permissions and // limitations under the License. -// ESP32-C3 GPIO + IO_MUX + GPIO Matrix driver -// -// Reference: ESP32-C3 Technical Reference Manual, Chapter 4 (IO_MUX/GPIO) -// Reference: ESP-IDF soc/esp32c3/include/soc/gpio_reg.h, io_mux_reg.h, gpio_sig_map.h -// -// ESP32-C3 SPI2 (FSPI) signals are routed through the GPIO Matrix, -// not through IO_MUX FUNC selection. This driver configures both: -// - IO_MUX: per-pin pull-up/pull-down, input enable, MCU_SEL (function select) -// - GPIO Matrix: maps peripheral output signals to GPIO pins -// - GPIO output: direct pin output control for CS (software-controlled) +//! ESP32-C3 GPIO output pin driver. Pin mode is configured by +//! `pinctrl::esp32_pinctrl`; this driver only flips the output data latch. use crate::static_ref::StaticRef; -use blueos_hal::pinctrl::AlterFuncPin; use embedded_hal::digital::OutputPin; use tock_registers::{ interfaces::Writeable, register_bitfields, register_structs, registers::ReadWrite, }; -// ─── Base addresses ────────────────────────────────────────────────────── - -// GPIO controller (output/enable/matrix) -const GPIO_BASE: StaticRef = +pub(crate) const GPIO_BASE: StaticRef = unsafe { StaticRef::new(0x60004000 as *const GpioRegisters) }; -// ─── GPIO Matrix signal indices (from gpio_sig_map.h) ──────────────────── -// These map SPI2/FSPI peripheral signals to GPIO pins via the GPIO Matrix. - -/// FSPICLK — SPI2 clock output signal -const FSPICLK_OUT_IDX: u32 = 63; -/// FSPIQ — SPI2 MISO (data in) signal -const FSPIQ_IN_IDX: u32 = 64; -/// FSPID — SPI2 MOSI (data out) signal -const FSPID_OUT_IDX: u32 = 65; -/// FSPICS0 — SPI2 chip select 0 output signal -const FSPICS0_OUT_IDX: u32 = 68; - -// ─── IO_MUX per-pin register offsets ───────────────────────────────────── -// IO_MUX registers are NOT at contiguous 4*GPIO offsets. -// Each GPIO has a named register at a specific offset (from io_mux_reg.h). - -/// IO_MUX register offset for each GPIO (0-21), relative to IO_MUX base (0x60009000). -/// Index = GPIO number. Value = offset in bytes. -const IO_MUX_OFFSETS: [u32; 22] = [ - 0x04, // GPIO0 — PERIPHS_IO_MUX_XTAL_32K_P_U - 0x08, // GPIO1 — PERIPHS_IO_MUX_XTAL_32K_N_U - 0x0C, // GPIO2 — PERIPHS_IO_MUX_GPIO2_U - 0x10, // GPIO3 — PERIPHS_IO_MUX_GPIO3_U - 0x14, // GPIO4 — PERIPHS_IO_MUX_MTMS_U - 0x18, // GPIO5 — PERIPHS_IO_MUX_MTDI_U - 0x1C, // GPIO6 — PERIPHS_IO_MUX_MTCK_U - 0x20, // GPIO7 — PERIPHS_IO_MUX_MTDO_U - 0x24, // GPIO8 — PERIPHS_IO_MUX_GPIO8_U - 0x28, // GPIO9 — PERIPHS_IO_MUX_GPIO9_U - 0x2C, // GPIO10 — PERIPHS_IO_MUX_GPIO10_U - 0x30, // GPIO11 — PERIPHS_IO_MUX_VDD_SPI_U - 0x34, // GPIO12 — PERIPHS_IO_MUX_SPIHD_U - 0x38, // GPIO13 — PERIPHS_IO_MUX_SPIWP_U - 0x3C, // GPIO14 — PERIPHS_IO_MUX_SPICS0_U - 0x40, // GPIO15 — PERIPHS_IO_MUX_SPICLK_U - 0x44, // GPIO16 — PERIPHS_IO_MUX_SPID_U - 0x48, // GPIO17 — PERIPHS_IO_MUX_SPIQ_U - 0x4C, // GPIO18 — PERIPHS_IO_MUX_GPIO18_U - 0x50, // GPIO19 — PERIPHS_IO_MUX_GPIO19_U - 0x54, // GPIO20 — PERIPHS_IO_MUX_U0RXD_U - 0x58, // GPIO21 — PERIPHS_IO_MUX_U0TXD_U -]; - -const IO_MUX_BASE: usize = 0x60009000; - -// ─── IO_MUX register bit fields (from io_mux_reg.h) ───────────────────── -// MCU_SEL at bits 12-14 (3 bits, values 0-7) -// FUN_IE at bit 9 (input enable) -// FUN_DRV at bits 10-11 (drive strength, 0-3) -// FUN_PU at bit 8 (pull-up) -// FUN_PD at bit 7 (pull-down) - -register_bitfields! [ - u32, - - pub IoMuxFields [ - MCU_SEL OFFSET(12) NUMBITS(3) [ - Func0 = 0, // Default (JTAG/special) - Func1 = 1, // GPIO function (PIN_FUNC_GPIO) - Func2 = 2, // FSPI (SPI2) function for some pins - ], - FUN_DRV OFFSET(10) NUMBITS(2) [ - Drive0 = 0, // Weakest - Drive1 = 1, - Drive2 = 2, - Drive3 = 3, // Strongest - ], - FUN_IE OFFSET(9) NUMBITS(1) [], // Input enable - FUN_PU OFFSET(8) NUMBITS(1) [], // Pull-up - FUN_PD OFFSET(7) NUMBITS(1) [], // Pull-down - ], -]; - register_bitfields! [ u32, @@ -120,196 +35,21 @@ register_bitfields! [ ], ]; -// ─── GPIO controller registers ────────────────────────────────────────── - register_structs! { - GpioRegisters { - (0x000 => bt_select: ReadWrite), - (0x004 => out: ReadWrite), - (0x008 => out_w1ts: ReadWrite), - (0x00C => out_w1tc: ReadWrite), + pub GpioRegisters { + (0x000 => pub bt_select: ReadWrite), + (0x004 => pub out: ReadWrite), + (0x008 => pub out_w1ts: ReadWrite), + (0x00C => pub out_w1tc: ReadWrite), (0x010 => _reserved0), - (0x020 => enable: ReadWrite), - (0x024 => enable_w1ts: ReadWrite), - (0x028 => enable_w1tc: ReadWrite), + (0x020 => pub enable: ReadWrite), + (0x024 => pub enable_w1ts: ReadWrite), + (0x028 => pub enable_w1tc: ReadWrite), (0x02C => @END), } } -// ─── GPIO Matrix output function selection (from gpio_reg.h) ───────────── -// GPIO_FUNCx_OUT_SEL_CFG_REG at offset 0x554 + 4*x -// Each register has: -// OUT_SEL: bits 0-7 — which peripheral signal index to route to GPIO x -// INV_SEL: bit 8 — invert the output signal -// OEN_SEL: bit 9 — 0=peripheral controls output enable, 1=GPIO register controls -// OEN_INV_SEL: bit 10 — invert output enable signal -// -// For SPI2 output pins (CLK, MOSI, CS), we route the FSPI signal. -// For CS pin (software-controlled), we set OEN_SEL=1 (GPIO controls enable). - -register_bitfields! [ - u32, - - pub FuncOutSelCfg [ - OUT_SEL OFFSET(0) NUMBITS(8) [], // Peripheral signal index - INV_SEL OFFSET(8) NUMBITS(1) [], // Invert output - OEN_SEL OFFSET(9) NUMBITS(1) [ - Peripheral = 0, // Peripheral controls output enable - GpioReg = 1, // GPIO_ENABLE_REG controls output enable - ], - OEN_INV_SEL OFFSET(10) NUMBITS(1) [], // Invert output enable - ], -]; - -// ─── GPIO Matrix input function selection (from gpio_reg.h) ────────────── -// GPIO_FUNCx_IN_SEL_CFG_REG at offset 0x154 + 4*x -// Each register has: -// IN_SEL: bits 0-4 — which GPIO pin to route to this peripheral input signal -// IN_INV_SEL: bit 5 — invert input - -register_bitfields! [ - u32, - - pub FuncInSelCfg [ - IN_SEL OFFSET(0) NUMBITS(5) [], // GPIO pin number for input - IN_INV_SEL OFFSET(5) NUMBITS(1) [], // Invert input - SEL OFFSET(6) NUMBITS(1) [], // 1 = route via GPIO Matrix (do not bypass), 0 = bypass - ], -]; - -// ─── Helper: write IO_MUX register for a GPIO pin ──────────────────────── - -fn write_io_mux(pin: u8, mcu_sel: u32, ie: bool, pu: bool, pd: bool, drv: u32) { - let offset = IO_MUX_OFFSETS[pin as usize]; - let addr = IO_MUX_BASE + offset as usize; - let reg = unsafe { &*(addr as *const ReadWrite) }; - reg.write( - IoMuxFields::MCU_SEL.val(mcu_sel) - + IoMuxFields::FUN_IE.val(if ie { 1 } else { 0 }) - + IoMuxFields::FUN_PU.val(if pu { 1 } else { 0 }) - + IoMuxFields::FUN_PD.val(if pd { 1 } else { 0 }) - + IoMuxFields::FUN_DRV.val(drv), - ); -} - -// ─── Helper: route peripheral output signal to a GPIO pin ──────────────── -// Writes GPIO_FUNCx_OUT_SEL_CFG_REG to connect signal_idx to GPIO pin. - -fn route_signal_out(pin: u8, signal_idx: u32, oen_sel: u32) { - let offset = 0x554 + 4 * pin as usize; - let addr = 0x60004000 + offset; - let reg = unsafe { &*(addr as *const ReadWrite) }; - reg.write( - FuncOutSelCfg::OUT_SEL.val(signal_idx) - + FuncOutSelCfg::INV_SEL.val(0) - + FuncOutSelCfg::OEN_SEL.val(oen_sel) - + FuncOutSelCfg::OEN_INV_SEL.val(0), - ); -} - -// ─── Helper: route GPIO pin to peripheral input signal ─────────────────── -// Writes GPIO_FUNCx_IN_SEL_CFG_REG to connect GPIO pin to signal_idx. - -fn route_signal_in(signal_idx: u32, pin: u32) { - let offset = 0x154 + 4 * signal_idx as usize; - let addr = 0x60004000 + offset; - let reg = unsafe { &*(addr as *const ReadWrite) }; - reg.write( - FuncInSelCfg::SEL.val(1) + FuncInSelCfg::IN_INV_SEL.val(0) + FuncInSelCfg::IN_SEL.val(pin), - ); -} - -// ─── Esp32IoMuxPinctrl — PinMux configuration for define_pin_states! ──── - -/// ESP32-C3 PinMux configuration entry -/// -/// Combines IO_MUX, GPIO Matrix, and GPIO output enable configuration. -/// Used with `define_pin_states!` macro to configure SPI2 pins. -/// -/// SPI2 (FSPI) signals on ESP32-C3 are routed through the GPIO Matrix, -/// not via IO_MUX FUNC selection. MCU_SEL=1 (GPIO function) is required -/// so the GPIO Matrix can take over signal routing. -pub struct Esp32IoMuxPinctrl { - pin: u8, - mcu_sel: u32, - ie: bool, - pu: bool, - pd: bool, - drv: u32, - out_signal: Option, - in_signal: Option, - gpio_output: bool, -} - -impl Esp32IoMuxPinctrl { - pub const fn new( - pin: u8, - mcu_sel: u32, - ie: bool, - pu: bool, - pd: bool, - drv: u32, - out_signal: Option, - in_signal: Option, - gpio_output: bool, - ) -> Self { - Esp32IoMuxPinctrl { - pin, - mcu_sel, - ie, - pu, - pd, - drv, - out_signal, - in_signal, - gpio_output, - } - } -} - -impl AlterFuncPin for Esp32IoMuxPinctrl { - fn init(&self) { - // 1. Configure IO_MUX: function select, pull-up/down, input enable, drive - write_io_mux(self.pin, self.mcu_sel, self.ie, self.pu, self.pd, self.drv); - - // 2. Route peripheral output signal to this GPIO pin via GPIO Matrix - if let Some(signal_idx) = self.out_signal { - if self.gpio_output { - // Software-controlled pin (e.g., CS): OEN_SEL=1 (GPIO_ENABLE controls) - route_signal_out(self.pin, signal_idx, 1u32); - } else { - // Peripheral-controlled pin (CLK, MOSI): OEN_SEL=0 (peripheral controls) - route_signal_out(self.pin, signal_idx, 0u32); - } - } - - // 3. Route this GPIO pin to peripheral input signal via GPIO Matrix - if let Some(signal_idx) = self.in_signal { - route_signal_in(signal_idx, self.pin as u32); - } - - // 4. Enable GPIO output for software-controlled pins (CS). - // Order matters: pre-set the output latch HIGH *before* enabling output, - // so the pin is driven high the instant output turns on — no low glitch - // on CS during the enable moment. - if self.gpio_output { - let gpio_regs = &*GPIO_BASE; - gpio_regs.out_w1ts.write(GpioOut::DATA.val(1 << self.pin)); - gpio_regs - .enable_w1ts - .write(GpioEnable::DATA.val(1 << self.pin)); - } - } -} - -// ─── Esp32GpioOutputPin — OutputPin for ExclusiveDevice CS control ────── - -/// GPIO output pin for ESP32-C3 -/// -/// Implements `embedded_hal::digital::OutputPin` for use as CS pin -/// with `embedded_hal_bus::spi::ExclusiveDevice`. -/// -/// Uses atomic W1TS/W1TC registers for glitch-free set_low/set_high. +/// GPIO output pin for ESP32-C3, e.g. SPI CS via `embedded_hal_bus::spi::ExclusiveDevice`. pub struct Esp32GpioOutputPin; impl Esp32GpioOutputPin { @@ -324,14 +64,12 @@ impl embedded_hal::digital::ErrorType for Esp32GpioOutputPin impl OutputPin for Esp32GpioOutputPin { fn set_low(&mut self) -> Result<(), Self::Error> { - // Assert CS (active-low) — drive pin LOW let gpio_regs = &*GPIO_BASE; gpio_regs.out_w1tc.write(GpioOut::DATA.val(1 << PIN)); Ok(()) } fn set_high(&mut self) -> Result<(), Self::Error> { - // Deassert CS — drive pin HIGH let gpio_regs = &*GPIO_BASE; gpio_regs.out_w1ts.write(GpioOut::DATA.val(1 << PIN)); Ok(()) diff --git a/driver/src/pinctrl/esp32_pinctrl.rs b/driver/src/pinctrl/esp32_pinctrl.rs new file mode 100644 index 000000000..c739cda95 --- /dev/null +++ b/driver/src/pinctrl/esp32_pinctrl.rs @@ -0,0 +1,199 @@ +// 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. + +//! ESP32-C3 IO_MUX + GPIO Matrix pin controller. + +use crate::{ + gpio::esp32_gpio::{GpioEnable, GpioOut, GpioRegisters, GPIO_BASE}, + static_ref::StaticRef, +}; +use blueos_hal::pinctrl::AlterFuncPin; +use tock_registers::{interfaces::Writeable, register_bitfields, registers::ReadWrite}; + +// SPI2/FSPI signal indices routed through the GPIO Matrix (gpio_sig_map.h). +const FSPICLK_OUT_IDX: u32 = 63; +const FSPIQ_IN_IDX: u32 = 64; +const FSPID_OUT_IDX: u32 = 65; +const FSPICS0_OUT_IDX: u32 = 68; + +// IO_MUX per-pin register offsets relative to IO_MUX base (0x60009000). +const IO_MUX_OFFSETS: [u32; 22] = [ + 0x04, // GPIO0 + 0x08, // GPIO1 + 0x0C, // GPIO2 + 0x10, // GPIO3 + 0x14, // GPIO4 + 0x18, // GPIO5 + 0x1C, // GPIO6 + 0x20, // GPIO7 + 0x24, // GPIO8 + 0x28, // GPIO9 + 0x2C, // GPIO10 + 0x30, // GPIO11 + 0x34, // GPIO12 + 0x38, // GPIO13 + 0x3C, // GPIO14 + 0x40, // GPIO15 + 0x44, // GPIO16 + 0x48, // GPIO17 + 0x4C, // GPIO18 + 0x50, // GPIO19 + 0x54, // GPIO20 + 0x58, // GPIO21 +]; + +const IO_MUX_BASE: usize = 0x60009000; + +register_bitfields! [ + u32, + + pub IoMuxFields [ + MCU_SEL OFFSET(12) NUMBITS(3) [ + Func0 = 0, // Default (JTAG/special) + Func1 = 1, // GPIO function + Func2 = 2, // FSPI (SPI2) function for some pins + ], + FUN_DRV OFFSET(10) NUMBITS(2) [ + Drive0 = 0, + Drive1 = 1, + Drive2 = 2, + Drive3 = 3, + ], + FUN_IE OFFSET(9) NUMBITS(1) [], // Input enable + FUN_PU OFFSET(8) NUMBITS(1) [], // Pull-up + FUN_PD OFFSET(7) NUMBITS(1) [], // Pull-down + ], +]; + +register_bitfields! [ + u32, + + // GPIO_FUNCx_OUT_SEL_CFG_REG: route a peripheral output signal to GPIO x. + pub FuncOutSelCfg [ + OUT_SEL OFFSET(0) NUMBITS(8) [], + INV_SEL OFFSET(8) NUMBITS(1) [], + OEN_SEL OFFSET(9) NUMBITS(1) [ + Peripheral = 0, // Peripheral controls output enable + GpioReg = 1, // GPIO_ENABLE_REG controls output enable + ], + OEN_INV_SEL OFFSET(10) NUMBITS(1) [], + ], +]; + +register_bitfields! [ + u32, + + // GPIO_FUNCx_IN_SEL_CFG_REG: route GPIO pin to a peripheral input signal. + pub FuncInSelCfg [ + IN_SEL OFFSET(0) NUMBITS(5) [], + IN_INV_SEL OFFSET(5) NUMBITS(1) [], + SEL OFFSET(6) NUMBITS(1) [], // 1 = route via GPIO Matrix, 0 = bypass + ], +]; + +fn write_io_mux(pin: u8, mcu_sel: u32, ie: bool, pu: bool, pd: bool, drv: u32) { + let addr = IO_MUX_BASE + IO_MUX_OFFSETS[pin as usize] as usize; + let reg = unsafe { &*(addr as *const ReadWrite) }; + reg.write( + IoMuxFields::MCU_SEL.val(mcu_sel) + + IoMuxFields::FUN_IE.val(if ie { 1 } else { 0 }) + + IoMuxFields::FUN_PU.val(if pu { 1 } else { 0 }) + + IoMuxFields::FUN_PD.val(if pd { 1 } else { 0 }) + + IoMuxFields::FUN_DRV.val(drv), + ); +} + +fn route_signal_out(pin: u8, signal_idx: u32, oen_sel: u32) { + let addr = 0x60004000 + 0x554 + 4 * pin as usize; + let reg = unsafe { &*(addr as *const ReadWrite) }; + reg.write( + FuncOutSelCfg::OUT_SEL.val(signal_idx) + + FuncOutSelCfg::INV_SEL.val(0) + + FuncOutSelCfg::OEN_SEL.val(oen_sel) + + FuncOutSelCfg::OEN_INV_SEL.val(0), + ); +} + +fn route_signal_in(signal_idx: u32, pin: u32) { + let addr = 0x60004000 + 0x154 + 4 * signal_idx as usize; + let reg = unsafe { &*(addr as *const ReadWrite) }; + reg.write( + FuncInSelCfg::SEL.val(1) + FuncInSelCfg::IN_INV_SEL.val(0) + FuncInSelCfg::IN_SEL.val(pin), + ); +} + +/// PinMux configuration entry used with `define_pin_states!`. +pub struct Esp32IoMuxPinctrl { + pin: u8, + mcu_sel: u32, + ie: bool, + pu: bool, + pd: bool, + drv: u32, + out_signal: Option, + in_signal: Option, + gpio_output: bool, +} + +impl Esp32IoMuxPinctrl { + pub const fn new( + pin: u8, + mcu_sel: u32, + ie: bool, + pu: bool, + pd: bool, + drv: u32, + out_signal: Option, + in_signal: Option, + gpio_output: bool, + ) -> Self { + Esp32IoMuxPinctrl { + pin, + mcu_sel, + ie, + pu, + pd, + drv, + out_signal, + in_signal, + gpio_output, + } + } +} + +impl AlterFuncPin for Esp32IoMuxPinctrl { + fn init(&self) { + write_io_mux(self.pin, self.mcu_sel, self.ie, self.pu, self.pd, self.drv); + + if let Some(signal_idx) = self.out_signal { + // Software-controlled pins (CS) use OEN_SEL=1; peripheral pins use OEN_SEL=0. + let oen_sel = if self.gpio_output { 1u32 } else { 0u32 }; + route_signal_out(self.pin, signal_idx, oen_sel); + } + + if let Some(signal_idx) = self.in_signal { + route_signal_in(signal_idx, self.pin as u32); + } + + // For software-controlled pins, pre-set output HIGH before enabling to + // avoid a low glitch on CS at the enable moment. + if self.gpio_output { + let gpio_regs = &*GPIO_BASE; + gpio_regs.out_w1ts.write(GpioOut::DATA.val(1 << self.pin)); + gpio_regs + .enable_w1ts + .write(GpioEnable::DATA.val(1 << self.pin)); + } + } +} diff --git a/driver/src/pinctrl/mod.rs b/driver/src/pinctrl/mod.rs index d9c41c44a..b123b30f7 100644 --- a/driver/src/pinctrl/mod.rs +++ b/driver/src/pinctrl/mod.rs @@ -15,3 +15,6 @@ pub mod gd32_af; pub mod gd32_afio; pub mod rpi_pico; + +#[cfg(target_chip = "esp32c3")] +pub mod esp32_pinctrl; diff --git a/driver/src/spi/esp32_spi2.rs b/driver/src/spi/esp32_spi2.rs index 7434e17e2..29bea7bca 100644 --- a/driver/src/spi/esp32_spi2.rs +++ b/driver/src/spi/esp32_spi2.rs @@ -12,9 +12,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -// ESP32-C3 GPSPI2 (SPI2) register-level driver -// -// Reference: ESP32-C3 Technical Reference Manual, Chapter 7 (SPI) +//! ESP32-C3 GPSPI2 (SPI2) register-level driver. use crate::{ spi::{SpiBitOrder, SpiConfig, SpiPhase, SpiPolarity}, @@ -222,7 +220,7 @@ register_structs! { } } -// Minimal system registers for SPI2 clock gating and reset +// System registers for SPI2 clock gating and reset. register_structs! { SystemRegisters { (0x00 => _reserved_sys0), @@ -309,8 +307,7 @@ impl Esp32Spi2 { while regs.cmd.is_set(CMD::UPDATE) {} } - // AFIFO reset must be a SET-then-CLEAR pulse; leaving the bit set keeps the - // FIFO held in reset and corrupts subsequent transfers on real hardware. + // AFIFO reset is a SET-then-CLEAR pulse. fn reset_tx_fifo(&self) { let regs = &*SPI2_BASE; regs.dma_conf.modify(DMA_CONF::BUF_AFIFO_RST::SET); @@ -333,22 +330,16 @@ impl Esp32Spi2 { fn start_transfer(&self) { let regs = &*SPI2_BASE; - // Sync config registers into the shadow registers before each transfer. - // Matches esp-hal start_operation: update() first. + // Sync shadow registers, clear stale TRANS_DONE, then start (USR self-clears). regs.cmd.modify(CMD::UPDATE.val(1)); while regs.cmd.is_set(CMD::UPDATE) {} - // Clear any pending TRANS_DONE from a previous transfer before starting. regs.dma_int_clr.write(DMA_INT_CLR::TRANS_DONE::SET); - // Kick off the user-defined transaction. USR self-clears on completion. - // Use modify (not write) so other CMD bits stay untouched. regs.cmd.modify(CMD::USR.val(1)); while regs.cmd.is_set(CMD::USR) {} } fn wait_done(&self) { - // let regs = &*SPI2_BASE; - // while !regs.dma_int_raw.is_set(DMA_INT_RAW::TRANS_DONE) {} - // regs.dma_int_clr.write(DMA_INT_CLR::TRANS_DONE::SET); + // No DMA in use; with DMA, poll dma_int_raw TRANS_DONE then clear it via dma_int_clr. } fn configure_clock(&self, baudrate: u32) -> blueos_hal::err::Result<()> { @@ -358,11 +349,9 @@ impl Esp32Spi2 { return Ok(()); } - // f_spi = f_apb / ((pre+1) * (n+1)), minimum divisor = 2 + // f_spi = f_apb / ((pre+1) * (n+1)), minimum divisor = 2; prefer larger n for duty cycle. let divisor = (APB_CLK_HZ / baudrate).max(2); - // Find pre and n such that (pre+1)*(n+1) = divisor - // Prefer larger n for better duty cycle let mut best_pre = 0u32; let mut best_n_plus_one = 0u32; for pre in 0..16u32 { @@ -381,7 +370,7 @@ impl Esp32Spi2 { } } - // No valid combination found — minimum achievable is APB/1024 (~78kHz) + // No valid combination: minimum is APB/1024 (~78kHz). if best_n_plus_one == 0 { return Err(blueos_hal::err::HalError::NotSupport); } @@ -430,9 +419,8 @@ impl Esp32Spi2 { } let regs = &*SPI2_BASE; - // Full-duplex dummy read: write 0x00 while reading, so SCLK is driven by - // the MOSI phase. Matches esp-hal Driver::read (USR_MOSI+USR_MISO+DOUTDIN). - // Half-duplex MISO-only was returning misaligned data on real hardware. + // Full-duplex dummy read (write 0x00 while reading): half-duplex MISO-only + // returned misaligned data on real hardware. regs.user.modify( USER::DOUTDIN.val(1) + USER::USR_MOSI::SET @@ -474,10 +462,7 @@ impl Esp32Spi2 { + USER::USR_DUMMY::CLEAR, ); - // Independent read/write cursors (matches esp-hal Driver::transfer): - // each side advances min(FIFO_SIZE, remaining) on its own. When read - // exceeds write in a chunk, the write side is padded with EMPTY_WRITE_PAD - // so enough SCLK cycles are generated for the read. + // Independent read/write cursors; pad the shorter side with EMPTY_WRITE_PAD. let mut write_from = 0usize; let mut read_from = 0usize; loop { @@ -521,8 +506,7 @@ impl PlatPeri for Esp32Spi2 { fn enable(&self) { let sys = &*SYSTEM_BASE; sys.perip_clk_en0.modify(PERIP_CLK_EN0::SPI2_CLK_EN::SET); - // Reset pulse (assert then release), matching ESP-IDF spi_ll_reset_register, - // so SPI2 starts from a clean state — otherwise CMD::UPDATE may never clear. + // Reset pulse; without it CMD::UPDATE may never clear. sys.perip_rst_en0.modify(PERIP_RST_EN0::SPI2_RST::SET); sys.perip_rst_en0.modify(PERIP_RST_EN0::SPI2_RST::CLEAR); diff --git a/driver/src/spi/mod.rs b/driver/src/spi/mod.rs index 3a03c864e..8e5d1f477 100644 --- a/driver/src/spi/mod.rs +++ b/driver/src/spi/mod.rs @@ -15,30 +15,21 @@ #[cfg(target_chip = "esp32c3")] pub mod esp32_spi2; -/// SPI clock phase configuration -/// -/// Phase0 (CPHA=0): Data captured on the first clock transition. -/// Phase1 (CPHA=1): Data captured on the second clock transition. +/// SPI clock phase (CPHA). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SpiPhase { Phase0, Phase1, } -/// SPI clock polarity configuration -/// -/// Low (CPOL=0): Clock signal low when idle. -/// High (CPOL=1): Clock signal high when idle. +/// SPI clock polarity (CPOL). #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SpiPolarity { Low, High, } -/// SPI bit order configuration -/// -/// MsbFirst: Most significant bit transmitted first (standard for SPI NOR Flash). -/// LsbFirst: Least significant bit transmitted first. +/// SPI bit order. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SpiBitOrder { MsbFirst, diff --git a/hal/src/spi.rs b/hal/src/spi.rs index 407e5c1d6..e502053e3 100644 --- a/hal/src/spi.rs +++ b/hal/src/spi.rs @@ -14,20 +14,12 @@ /// SPI peripheral trait — full-duplex transfer + half-duplex read/write pub trait Spi: super::PlatPeri + super::Configuration { - /// Full-duplex transfer: simultaneously write `write` and read into `read`. - /// - /// Runs for `max(read.len(), write.len())` bytes. Overlapping/aliasing - /// buffers is permitted (SPI hardware reads MISO and writes MOSI on - /// separate lines simultaneously). + /// Full-duplex transfer over `max(read.len(), write.len())` bytes. fn transfer(&self, read: &mut [u8], write: &[u8]) -> super::err::Result<()>; - /// Half-duplex read: read data into `buf` from the SPI slave. - /// - /// The word value sent on MOSI during reading is implementation-defined, - /// typically `0x00` or `0xFF`. + /// Half-duplex read; MOSI value during read is implementation-defined. fn read(&self, buf: &mut [u8]) -> super::err::Result<()>; - /// Half-duplex write: write data from `buf` to the SPI slave, - /// discarding all incoming bytes on MISO. + /// Half-duplex write, discarding MISO. fn write(&self, buf: &[u8]) -> super::err::Result<()>; } diff --git a/kernel/src/sync/delay.rs b/kernel/src/sync/delay.rs index 6d1799d7d..6c000534e 100644 --- a/kernel/src/sync/delay.rs +++ b/kernel/src/sync/delay.rs @@ -26,9 +26,7 @@ impl DelayNs for KernelDelay { if !scheduler::is_schedule_ready() { #[cfg(target_arch = "riscv32")] { - // rdcycle is unreliable on ESP32-C3 (mcycle may not advance), so a - // cycle-counter-based spin hangs forever. Spin a fixed count instead. - // ~ns CPU cycles at 160 MHz, conservative (spin_loop == 1 cycle). + // rdcycle may not advance on ESP32-C3; spin a fixed count (~ns cycles @ 160MHz). let spins = (ns as u64).saturating_mul(160) / 1_000; for _ in 0..spins { core::hint::spin_loop(); @@ -42,8 +40,7 @@ impl DelayNs for KernelDelay { } let ticks = blueos_kconfig::CONFIG_TICKS_PER_SECOND as u32 * ns / 1_000_000_000; if ticks == 0 { - // yield_me() is a no-op in single-task shell; spin a real delay so - // flash wait_busy gets a real per-iteration budget. + // yield_me() is a no-op in single-task shell; spin so wait_busy gets a real budget. #[cfg(target_arch = "riscv32")] { let spins = (ns as u64).saturating_mul(160) / 1_000; From 79fbdf732818635e71d5f8bbe7cb27b81e6a88f8 Mon Sep 17 00:00:00 2001 From: jinwjinl <2532191107@qq.com> Date: Thu, 2 Jul 2026 18:17:25 +0800 Subject: [PATCH 3/3] Enable to drive Nor Flash. --- .../qemu_virt64_aarch64/coverage/defconfig | 3 + .../qemu_virt64_aarch64/debug/defconfig | 3 + .../qemu_virt64_aarch64/release/defconfig | 3 + .../config/seeed_xiao_esp32c3/debug/defconfig | 2 + .../seeed_xiao_esp32c3/release/defconfig | 2 + kernel/src/Kconfig | 9 + kernel/src/boards/qemu_virt64_aarch64/init.rs | 9 + kernel/src/boards/seeed_xiao_esp32c3/mod.rs | 34 + kernel/src/boot.rs | 2 + kernel/src/devices/block/mod.rs | 104 ++- kernel/src/devices/mod.rs | 6 +- kernel/src/devices/storage/mod.rs | 18 + kernel/src/devices/storage/spi_flash.rs | 494 ++++++++++++++ kernel/src/devices/storage/spi_flash_cmd.rs | 631 ++++++++++++++++++ kernel/src/vfs/fatfs.rs | 9 +- kernel/src/vfs/mod.rs | 13 +- kernel/src/vfs/mount.rs | 4 +- 17 files changed, 1330 insertions(+), 16 deletions(-) create mode 100644 kernel/src/devices/storage/mod.rs create mode 100644 kernel/src/devices/storage/spi_flash.rs create mode 100644 kernel/src/devices/storage/spi_flash_cmd.rs 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 e901e11ce..df5c84686 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(); init_apps(); 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..3730ab01a 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; @@ -36,7 +37,10 @@ pub mod i2c_core; #[cfg(enable_net)] pub(crate) mod net; mod null; +#[cfg(enable_block)] pub mod spi_core; +#[cfg(enable_block)] +pub mod storage; pub mod tty; #[cfg(virtio)] pub mod virtio; 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) => {