| // Licensed under the Apache-2.0 license |
| // SPDX-License-Identifier: Apache-2.0 |
| |
| //! Pure helper logic for SMC address and timing calculations. |
| |
| use core::convert::TryFrom; |
| |
| use crate::smc::types::ChipSelect; |
| use crate::smc::types::FlashConfig; |
| use crate::smc::types::SmcConfig; |
| use crate::smc::types::SmcError; |
| |
| const SMC_WINDOW_SIZE_BYTES: usize = 256 * 1024 * 1024; |
| const DMA_MAX_TRANSFER_LENGTH: u32 = 0x20_0000; // 32MBytes |
| pub(crate) const SPI_CTRL_FREQ_MASK: u32 = 0x0F00_0F00; |
| |
| /// DMA engine's base address for flash-side memory (fmc084 / spi084). |
| /// DMA flash address = flash_window_base[cs] - SPI_DMA_FLASH_MAP_BASE + cs_offset. |
| pub(crate) const SPI_DMA_FLASH_MAP_BASE: usize = 0x6000_0000; |
| |
| /// DMA engine's base address for SRAM-side memory (fmc088 / spi088). |
| /// DMA DRAM address = physical_sram_addr + SPI_DMA_RAM_MAP_BASE. |
| pub(crate) const SPI_DMA_RAM_MAP_BASE: u32 = 0x8000_0000; |
| |
| /// Written to spi080/fmc080 to request the DMA bus (sets DMAReq, bit 31). |
| /// On SPI1/SPI2 this asserts the hardware arbiter request line. |
| /// On FMC, bits 20–31 are Reserved — the write is a no-op. |
| /// Matches aspeed-rust `SPI_DMA_GET_REQ_MAGIC`. |
| pub(crate) const SPI_DMA_GET_REQ_MAGIC: u32 = 0xaeed_0000; |
| |
| /// Written to spi080/fmc080 to release the DMA bus grant (DMADiscard). |
| /// On SPI1/SPI2 this releases the arbiter grant after DMA completes. |
| /// On FMC, bits 20–31 are Reserved — the write is a no-op. |
| /// Matches aspeed-rust `SPI_DMA_DISCARD_REQ_MAGIC`. |
| pub(crate) const SPI_DMA_DISCARD_REQ_MAGIC: u32 = 0xdeea_0000; |
| |
| /// spi080 bit 31: DMA bus request is pending (SPI1/SPI2 only). |
| /// Reads as 0 on FMC (Reserved). |
| pub(crate) const SPI_DMA_REQUEST: u32 = 1 << 31; |
| |
| /// spi080 bit 30: DMA bus grant is held (SPI1/SPI2 only). |
| /// Reads as 0 on FMC (Reserved). |
| pub(crate) const SPI_DMA_GRANT: u32 = 1 << 30; |
| |
| pub(crate) const SPI_DMA_CALIB_MODE: u32 = 1 << 3; |
| pub(crate) const SPI_DMA_CALC_CKSUM: u32 = 1 << 2; |
| pub(crate) const SPI_DMA_ENABLE: u32 = 1 << 0; |
| |
| /// Validated parameters for a DMA read operation, ready to be written to hardware registers. |
| /// |
| /// Produced by [`validate_dma_read`] after bounds-checking and address translation. |
| /// All fields are in the format expected directly by the DMA engine registers. |
| pub(crate) struct ValidatedDmaRead { |
| /// DMA engine flash-side address (written to fmc084 / spi084). |
| /// |
| /// Computed as `flash_window_base[cs] - SPI_DMA_FLASH_MAP_BASE + cs_offset`. |
| pub flash_start: usize, |
| /// DMA engine DRAM-side address (written to fmc088 / spi088). |
| /// |
| /// Must be 4-byte aligned |
| pub dram_addr: u32, |
| /// Value to write to the DMA length register (transfer length minus one). |
| /// [0:24] 0x1F_FFFF:32Mbytes |
| pub dma_len_reg: u32, |
| } |
| |
| pub(crate) fn flash_capacity_bytes(config: Option<FlashConfig>) -> Result<usize, SmcError> { |
| match config { |
| Some(config) => (config.capacity_mb as usize) |
| .checked_mul(1024 * 1024) |
| .ok_or(SmcError::InvalidCapacity), |
| None => Ok(0), |
| } |
| } |
| |
| pub(crate) fn cs_capacity_bytes(config: &SmcConfig, cs: ChipSelect) -> Result<usize, SmcError> { |
| let slot = match cs { |
| ChipSelect::Cs0 => config.cs0, |
| ChipSelect::Cs1 => config.cs1, |
| }; |
| match slot { |
| Some(_) => flash_capacity_bytes(slot), |
| None => Err(SmcError::InvalidChipSelect), |
| } |
| } |
| |
| pub(crate) fn total_capacity_bytes( |
| cs0: Option<FlashConfig>, |
| cs1: Option<FlashConfig>, |
| ) -> Result<usize, SmcError> { |
| let cs0_size = flash_capacity_bytes(cs0)?; |
| let cs1_size = flash_capacity_bytes(cs1)?; |
| let total = cs0_size |
| .checked_add(cs1_size) |
| .ok_or(SmcError::InvalidCapacity)?; |
| if total > SMC_WINDOW_SIZE_BYTES { |
| return Err(SmcError::InvalidCapacity); |
| } |
| Ok(total) |
| } |
| |
| pub(crate) fn validate_mapped_range( |
| offset: u32, |
| len: usize, |
| capacity_bytes: usize, |
| ) -> Result<usize, SmcError> { |
| let offset = offset as usize; |
| let end = offset.checked_add(len).ok_or(SmcError::InvalidCapacity)?; |
| if end > capacity_bytes { |
| return Err(SmcError::InvalidCapacity); |
| } |
| Ok(offset) |
| } |
| |
| /// Validate a DMA read request and compute DMA register values. |
| /// |
| /// `flash_window_base`: per-CS AHB window base addresses from `Smc<Ready>`. |
| /// `cs0_capacity`: byte size of CS0; used to derive which CS the offset falls in. |
| /// |
| /// The chip select is never accepted as a caller argument. The controller-relative |
| /// `flash_offset` encodes it implicitly: any offset in `[0, cs0_capacity)` names |
| /// CS0; any offset in `[cs0_capacity, capacity_bytes)` names CS1. See |
| /// EVD-20260511-dma-flash-offset-encodes-cs for the full rationale. |
| pub(crate) fn validate_dma_read( |
| flash_offset: u32, |
| flash_win_base: usize, |
| cs_capacity: usize, |
| dram_addr: usize, |
| len: u32, |
| ) -> Result<ValidatedDmaRead, SmcError> { |
| if len == 0 || len > DMA_MAX_TRANSFER_LENGTH { |
| return Err(SmcError::InvalidCapacity); |
| } |
| |
| // Flash offset must be 4-byte aligned (spec §1.3, matching aspeed-rust). |
| if flash_offset & 0x3 != 0 { |
| return Err(SmcError::InvalidCapacity); |
| } |
| |
| // Bounds-check the controller-relative offset. |
| let _ = validate_mapped_range(flash_offset, len as usize, cs_capacity)?; |
| |
| // Compute the DMA engine's flash address (written to fmc084/spi084). |
| // Formula from aspeed-rust fmccontroller.rs::read_dma: |
| // fmc084 = decode_addr[cs].start + op.address.value - SPI_DMA_FLASH_MAP_BASE |
| // = flash_window_base[cs] - SPI_DMA_FLASH_MAP_BASE + cs_offset |
| let base = flash_win_base |
| .checked_sub(SPI_DMA_FLASH_MAP_BASE) |
| .ok_or(SmcError::InvalidCapacity)?; |
| |
| let flash_start = base |
| .checked_add(flash_offset as usize) |
| .ok_or(SmcError::InvalidCapacity)?; |
| |
| let dram_addr = u32::try_from(dram_addr).map_err(|_| SmcError::InvalidCapacity)?; |
| if dram_addr & 0x3 != 0 { |
| return Err(SmcError::InvalidCapacity); |
| } |
| |
| Ok(ValidatedDmaRead { |
| flash_start, |
| dram_addr, |
| dma_len_reg: len - 1, |
| }) |
| } |
| |
| /// Encode an FMC memory segment into hardware register format. |
| /// |
| /// FMC decode fields use 512 KiB alignment. `end` is exclusive. |
| pub(crate) fn encode_fmc_segment(start: usize, end: usize) -> Result<u32, SmcError> { |
| if end == 0 || end <= start { |
| return Err(SmcError::InvalidCapacity); |
| } |
| |
| let start = start as u32; |
| let inclusive_end = (end - 1) as u32; |
| Ok(((((start >> 19) << 19) >> 16) & 0x0ff8) | (((inclusive_end >> 19) << 19) & 0x0ff8_0000)) |
| } |
| |
| /// Encode an SPI1/SPI2 memory segment into hardware register format. |
| /// |
| /// SPI decode fields use 1 MiB alignment. `end` is exclusive. |
| pub(crate) fn encode_spi_segment(start: usize, end: usize) -> Result<u32, SmcError> { |
| if end == 0 || end <= start { |
| return Err(SmcError::InvalidCapacity); |
| } |
| |
| let start = start as u32; |
| let inclusive_end = (end - 1) as u32; |
| Ok(((((start >> 20) << 20) >> 16) & 0xffff) | (((inclusive_end >> 20) << 20) & 0xffff_0000)) |
| } |
| |
| /// Calculate AST-compatible SPI clock divider field for CS control registers. |
| /// |
| /// Ported from aspeed-rust's silicon-tested divisor search logic. |
| pub(crate) fn spi_freq_div(sysclk_mhz: u32, max_freq_mhz: u32) -> Result<u32, SmcError> { |
| if max_freq_mhz == 0 { |
| return Err(SmcError::HardwareError); |
| } |
| |
| let div_arr = [15u32, 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8, 0]; |
| |
| for i in 0..0x0f { |
| for (j, div_val) in div_arr.iter().copied().enumerate() { |
| if i == 0 && j == 0 { |
| continue; |
| } |
| |
| let divisor = (j as u32) + 1 + (i * 16); |
| let freq = sysclk_mhz / divisor; |
| if max_freq_mhz >= freq { |
| return Ok((i << 24) | (div_val << 8)); |
| } |
| } |
| } |
| |
| Ok(0) |
| } |
| /// Finds the midpoint of the longest consecutive sequence of 1's in a buffer. |
| /// |
| /// Returns the midpoint index if the longest run is at least length 4, |
| /// otherwise returns -1. |
| /// |
| /// # Arguments |
| /// * `buf` - slice of bytes (each should be 0 or 1). |
| pub(crate) fn get_mid_point_of_longest_one(buf: &[u8]) -> i32 { |
| let mut start = 0; |
| let mut mid_point = 0; |
| let mut max_cnt = 0; |
| let mut cnt = 0; |
| |
| for (i, &val) in buf.iter().enumerate() { |
| if val == 1 { |
| cnt += 1; |
| } else { |
| cnt = 0; |
| start = i; |
| } |
| |
| if cnt > max_cnt { |
| max_cnt = cnt; |
| mid_point = start + (cnt / 2); |
| } |
| } |
| |
| if max_cnt < 4 { |
| return -1; |
| } else { |
| return i32::try_from(mid_point).unwrap(); |
| } |
| } |
| |
| pub(crate) fn spi_calibration_enable(buf: &[u8]) -> Result<bool, SmcError> { |
| if buf.len() < 4 { |
| return Ok(false); |
| } |
| |
| let mut valid_count = 0; |
| |
| // Process 4 bytes at a time |
| for chunk in buf.chunks_exact(4) { |
| // Convert 4 bytes to u32 in little-endian order |
| let word = u32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); |
| |
| if word != 0 && word != 0xFFFF_FFFF { |
| valid_count += 1; |
| } |
| if valid_count > 100 { |
| return Ok(true); |
| } |
| } |
| |
| Ok(false) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| |
| #[test] |
| fn test_encode_segment() { |
| let seg = encode_fmc_segment(0, 16 * 1024 * 1024).unwrap(); |
| let start_512k = ((seg & 0x0ff8) << 16) >> 19; |
| let end_512k = (((seg & 0x0ff8_0000) | 0x0007_ffff) >> 19); |
| assert_eq!(start_512k, 0); |
| assert_eq!(end_512k, 31); |
| } |
| |
| #[test] |
| fn test_encode_segment_8mb_cs0() { |
| let seg = encode_fmc_segment(0, 8 * 1024 * 1024).unwrap(); |
| let start_512k = ((seg & 0x0ff8) << 16) >> 19; |
| let end_512k = (((seg & 0x0ff8_0000) | 0x0007_ffff) >> 19); |
| assert_eq!(start_512k, 0); |
| assert_eq!(end_512k, 15); |
| } |
| |
| #[test] |
| fn test_encode_segment_64mb_cs1_after_8mb_cs0() { |
| let start = 8 * 1024 * 1024; |
| let end = start + 64 * 1024 * 1024; |
| let seg = encode_fmc_segment(start, end).unwrap(); |
| let start_512k = ((seg & 0x0ff8) << 16) >> 19; |
| let end_512k = (((seg & 0x0ff8_0000) | 0x0007_ffff) >> 19); |
| assert_eq!(start_512k, 16); |
| assert_eq!(end_512k, 143); |
| } |
| |
| fn decode_divisor(encoded: u32) -> u32 { |
| let table = [15u32, 7, 14, 6, 13, 5, 12, 4, 11, 3, 10, 2, 9, 1, 8, 0]; |
| let hi = (encoded >> 24) & 0x0f; |
| let lo = (encoded >> 8) & 0x0f; |
| let j = table |
| .iter() |
| .position(|&v| v == lo) |
| .expect("encoded lo nibble must exist in divisor table") as u32; |
| j + 1 + (hi * 16) |
| } |
| |
| #[test] |
| fn test_spi_freq_div_25mhz_bound() { |
| let encoded = spi_freq_div(200, 25).unwrap(); |
| let divisor = decode_divisor(encoded); |
| assert_eq!(200 / divisor, 25); |
| assert!(encoded & !SPI_CTRL_FREQ_MASK == 0); |
| } |
| |
| #[test] |
| fn test_spi_freq_div_50mhz_bound() { |
| let encoded = spi_freq_div(200, 50).unwrap(); |
| let divisor = decode_divisor(encoded); |
| assert_eq!(200 / divisor, 50); |
| assert!(encoded & !SPI_CTRL_FREQ_MASK == 0); |
| } |
| |
| #[test] |
| fn test_spi_freq_div_rejects_zero_target() { |
| assert!(spi_freq_div(200, 0).is_err()); |
| } |
| |
| #[test] |
| fn test_segment_overflow() { |
| let result = encode_fmc_segment(0, 512 * 1024 * 1024); |
| assert!(result.is_err()); |
| } |
| |
| #[test] |
| fn test_total_capacity_overflow() { |
| let result = total_capacity_bytes( |
| Some(FlashConfig { |
| capacity_mb: 128, |
| page_size: 256, |
| sector_size: 4096, |
| block_size: 65536, |
| spi_clock_mhz: 25, |
| }), |
| Some(FlashConfig { |
| capacity_mb: 129, |
| page_size: 256, |
| sector_size: 4096, |
| block_size: 65536, |
| spi_clock_mhz: 25, |
| }), |
| ); |
| assert!(result.is_err()); |
| } |
| |
| #[test] |
| fn test_validate_mapped_range_accepts_exact_fit() { |
| let offset = validate_mapped_range(4092, 4, 4096).unwrap(); |
| assert_eq!(offset, 4092); |
| } |
| |
| #[test] |
| fn test_validate_mapped_range_rejects_overflow() { |
| let result = validate_mapped_range(u32::MAX, 8, SMC_WINDOW_SIZE_BYTES); |
| assert!(result.is_err()); |
| } |
| |
| #[test] |
| fn test_validate_mapped_range_rejects_out_of_bounds() { |
| let result = validate_mapped_range(1024, 1, 1024); |
| assert!(result.is_err()); |
| } |
| |
| // FMC window base for tests: 0x8000_0000; SPI_DMA_FLASH_MAP_BASE: 0x6000_0000 |
| // Expected fmc084 for CS0 offset 0x1000: 0x8000_0000 - 0x6000_0000 + 0x1000 = 0x2000_1000 |
| const TEST_WINDOW: [usize; 2] = [0x8000_0000, 0x8100_0000]; // CS0=16MB, CS1 starts at +16MB |
| const TEST_CS0_CAP: usize = 16 * 1024 * 1024; |
| //const TEST_CAP: usize = 16 * 1024 * 1024; |
| |
| #[test] |
| fn test_validate_dma_read_accepts_valid_request() { |
| let validated = |
| validate_dma_read(0x1000, TEST_WINDOW, TEST_CS0_CAP, 0x0008_0000, 512).unwrap(); |
| assert_eq!(validated.flash_start, 0x2000_1000); // 0x8000_0000 - 0x6000_0000 + 0x1000 |
| assert_eq!(validated.dram_addr, 0x0008_0000); |
| assert_eq!(validated.dma_len_reg, 511); |
| } |
| |
| #[test] |
| fn test_validate_dma_read_cs1_region() { |
| // Offset falls in CS1 region (past 16 MB CS0) on a 32 MB dual-CS controller. |
| const DUAL_WINDOW: [usize; 2] = [0x8000_0000, 0x8100_0000]; |
| const CS0_CAP: usize = 16 * 1024 * 1024; |
| const TOTAL_CAP: usize = 32 * 1024 * 1024; |
| let cs1_offset = CS0_CAP + 0x1000; |
| let validated = |
| validate_dma_read(cs1_offset as u32, DUAL_WINDOW, CS0_CAP, 0x0008_0000, 512).unwrap(); |
| // fmc084 = 0x8100_0000 - 0x6000_0000 + 0x1000 = 0x2100_1000 |
| assert_eq!(validated.flash_start, 0x2100_1000); |
| assert_eq!(validated.dma_len_reg, 511); |
| } |
| |
| #[test] |
| fn test_validate_dma_read_rejects_zero_length() { |
| let result = validate_dma_read(0, TEST_WINDOW, TEST_CS0_CAP, 0x0008_0000, 0); |
| assert!(result.is_err()); |
| } |
| |
| #[test] |
| fn test_validate_dma_read_rejects_unaligned_flash_offset() { |
| let result = validate_dma_read(0x1001, TEST_WINDOW, TEST_CS0_CAP, 0x0008_0000, 256); |
| assert!(result.is_err()); |
| } |
| |
| #[test] |
| fn test_validate_dma_read_rejects_unaligned_dram() { |
| let result = validate_dma_read(0, TEST_WINDOW, TEST_CS0_CAP, 0x0008_0002, 256); |
| assert!(result.is_err()); |
| } |
| |
| #[test] |
| fn test_validate_dma_read_rejects_masked_dram_bits() { |
| let result = validate_dma_read(0, TEST_WINDOW, TEST_CS0_CAP, 0x1000_0000, 256); |
| assert!(result.is_err()); |
| } |
| |
| #[test] |
| fn test_validate_dma_read_rejects_flash_range_overflow() { |
| let result = validate_dma_read(0x00ff_ff00, TEST_WINDOW, TEST_CS0_CAP, 0x0008_0000, 0x200); |
| assert!(result.is_err()); |
| } |
| } |