diff --git a/bittide-instances/tests/Wishbone/ScatterGather.hs b/bittide-instances/tests/Wishbone/ScatterGather.hs index a92ae4c42..3d84efdab 100644 --- a/bittide-instances/tests/Wishbone/ScatterGather.hs +++ b/bittide-instances/tests/Wishbone/ScatterGather.hs @@ -73,5 +73,31 @@ case_scatter_gather_c_test = do where msg = "Received the following from the CPU over UART:\n" <> simResultC +-- Aligned ringbuffer test simulation +simAlignedRingbuffer :: IO () +simAlignedRingbuffer = putStr simResultAlignedRingbuffer + +simResultAlignedRingbuffer :: (HasCallStack) => String +simResultAlignedRingbuffer = chr . fromIntegral <$> catMaybes uartStream + where + uartStream = sampleC def{timeoutAfter = 200_000} dutNoMM + + dutNoMM :: (HasCallStack) => Circuit () (Df System (BitVector 8)) + dutNoMM = circuit $ do + mm <- ignoreMM + uartTx <- + withClockResetEnable clockGen (resetGenN d2) enableGen + $ (dutWithBinary "aligned_ringbuffer_test") + -< mm + idC -< uartTx + +case_aligned_ringbuffer_test :: Assertion +case_aligned_ringbuffer_test = do + assertBool + msg + ("*** ALL TESTS PASSED ***" `isInfixOf` simResultAlignedRingbuffer) + where + msg = "Received the following from the CPU over UART:\n" <> simResultAlignedRingbuffer + tests :: TestTree tests = $(testGroupGenerator) diff --git a/firmware-binaries/Cargo.lock b/firmware-binaries/Cargo.lock index d83cc5cff..316ad7669 100644 --- a/firmware-binaries/Cargo.lock +++ b/firmware-binaries/Cargo.lock @@ -11,6 +11,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned_ringbuffer_test" +version = "0.1.0" +dependencies = [ + "bittide-hal", + "bittide-sys", + "memmap-generate", + "riscv-rt", + "ufmt", +] + [[package]] name = "axi_stream_self_test" version = "0.1.0" diff --git a/firmware-binaries/Cargo.toml b/firmware-binaries/Cargo.toml index 66c14bbc6..6524f5324 100644 --- a/firmware-binaries/Cargo.toml +++ b/firmware-binaries/Cargo.toml @@ -16,6 +16,7 @@ members = [ "examples/c_hello", "examples/smoltcp_client", + "sim-tests/aligned_ringbuffer_test", "sim-tests/axi_stream_self_test", "sim-tests/registerwb_test", "sim-tests/capture_ugn_test", diff --git a/firmware-binaries/sim-tests/aligned_ringbuffer_test/Cargo.toml b/firmware-binaries/sim-tests/aligned_ringbuffer_test/Cargo.toml new file mode 100644 index 000000000..1cabc1300 --- /dev/null +++ b/firmware-binaries/sim-tests/aligned_ringbuffer_test/Cargo.toml @@ -0,0 +1,21 @@ +# SPDX-FileCopyrightText: 2025 Google LLC +# +# SPDX-License-Identifier: CC0-1.0 + +[package] +name = "aligned_ringbuffer_test" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +authors = ["Google LLC"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +riscv-rt = "0.11.0" +bittide-sys = { path = "../../../firmware-support/bittide-sys" } +bittide-hal = { path = "../../../firmware-support/bittide-hal" } +ufmt = "0.2.0" + +[build-dependencies] +memmap-generate = { path = "../../../firmware-support/memmap-generate" } diff --git a/firmware-binaries/sim-tests/aligned_ringbuffer_test/build.rs b/firmware-binaries/sim-tests/aligned_ringbuffer_test/build.rs new file mode 100644 index 000000000..70193e4cc --- /dev/null +++ b/firmware-binaries/sim-tests/aligned_ringbuffer_test/build.rs @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2025 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +use memmap_generate::build_utils::standard_memmap_build; + +fn main() { + standard_memmap_build("ScatterGatherPe.json", "DataMemory", "InstructionMemory"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/firmware-binaries/sim-tests/aligned_ringbuffer_test/memory.x b/firmware-binaries/sim-tests/aligned_ringbuffer_test/memory.x new file mode 100644 index 000000000..751b2cfd3 --- /dev/null +++ b/firmware-binaries/sim-tests/aligned_ringbuffer_test/memory.x @@ -0,0 +1,18 @@ +/* +SPDX-FileCopyrightText: 2025 Google LLC + +SPDX-License-Identifier: CC0-1.0 +*/ + +MEMORY +{ + IMEM : ORIGIN = 0x80000000, LENGTH = 64K + DMEM : ORIGIN = 0x20000000, LENGTH = 32K +} + +REGION_ALIAS("REGION_TEXT", IMEM); +REGION_ALIAS("REGION_RODATA", DMEM); +REGION_ALIAS("REGION_DATA", DMEM); +REGION_ALIAS("REGION_BSS", DMEM); +REGION_ALIAS("REGION_HEAP", DMEM); +REGION_ALIAS("REGION_STACK", DMEM); diff --git a/firmware-binaries/sim-tests/aligned_ringbuffer_test/src/main.rs b/firmware-binaries/sim-tests/aligned_ringbuffer_test/src/main.rs new file mode 100644 index 000000000..dd6e2f877 --- /dev/null +++ b/firmware-binaries/sim-tests/aligned_ringbuffer_test/src/main.rs @@ -0,0 +1,306 @@ +// SPDX-FileCopyrightText: 2025 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 +#![no_std] +#![cfg_attr(not(test), no_main)] + +use bittide_hal::{ + manual_additions::aligned_ringbuffer::{ReceiveRingbuffer, TransmitRingbuffer}, + scatter_gather_pe::DeviceInstances, + shared_devices::Uart, + types::ValidEntry_12, +}; +use core::fmt::Write; +#[cfg(not(test))] +use riscv_rt::entry; + +const INSTANCES: DeviceInstances = unsafe { DeviceInstances::new() }; + +/// The ringbuffer size in 64-bit words (must match scatter/gather memory size) +const RINGBUFFER_SIZE: usize = 16; + +/// Initialize scatter and gather calendars with incrementing counter entries. +/// Each calendar entry has a duration of 0 (no repeat), creating a ringbuffer +/// pattern where index N in TX maps to index N in RX. +fn initialize_calendars(uart: &mut Uart) { + writeln!(uart, "Initializing scatter/gather calendars").unwrap(); + + let scatter_calendar = INSTANCES.scatter_calendar; + let gather_calendar = INSTANCES.gather_calendar; + + // Write incrementing entries to both calendars + for n in 0..RINGBUFFER_SIZE { + let entry = ValidEntry_12 { + ve_entry: n as u8, + ve_repeat: 0, + }; + + scatter_calendar.set_shadow_entry(entry); + scatter_calendar.set_write_addr(n as u8); + + gather_calendar.set_shadow_entry(entry); + gather_calendar.set_write_addr(n as u8); + } + + // Set the depth (max index) for both calendars + scatter_calendar.set_shadow_depth_index((RINGBUFFER_SIZE - 1) as u8); + gather_calendar.set_shadow_depth_index((RINGBUFFER_SIZE - 1) as u8); + + // Activate the new calendar configurations + scatter_calendar.set_swap_active(true); + gather_calendar.set_swap_active(true); + + writeln!( + uart, + "Calendars initialized with {} entries", + RINGBUFFER_SIZE + ) + .unwrap(); +} + +#[cfg_attr(not(test), entry)] +fn main() -> ! { + let mut uart = INSTANCES.uart; + let scatter_unit = INSTANCES.scatter_unit; + let gather_unit = INSTANCES.gather_unit; + + writeln!(uart, "=== Aligned Ringbuffer Test ===").unwrap(); + + // Initialize calendars to create ringbuffer behavior + initialize_calendars(&mut uart); + + // Test 1: Basic unaligned transmission + writeln!(uart, "\n--- Test 1: Unaligned transmission ---").unwrap(); + writeln!( + uart, + "Write to TX buffer and verify it appears somewhere in RX" + ) + .unwrap(); + + let tx_ringbuffer = TransmitRingbuffer::new(gather_unit); + let mut rx_ringbuffer = ReceiveRingbuffer::new(scatter_unit, 0); + + tx_ringbuffer.clear(); + + const TEST_SIZE: usize = 4; + let test_pattern: [[u8; 8]; TEST_SIZE] = + core::array::from_fn(|i| (0x1000 + i as u64).to_le_bytes()); + + writeln!(uart, "Writing pattern to TX at offset 0").unwrap(); + tx_ringbuffer.write_slice(&test_pattern, 0); + + // Scan entire RX buffer to find the pattern + writeln!(uart, "Scanning RX buffer for pattern").unwrap(); + let mut found = false; + let mut found_offset = 0; + + for offset in 0..RINGBUFFER_SIZE { + let mut rx_data: [[u8; 8]; TEST_SIZE] = [[0u8; 8]; TEST_SIZE]; + rx_ringbuffer.read_slice(&mut rx_data, offset); + + if rx_data == test_pattern { + found = true; + found_offset = offset; + break; + } + } + + if found { + writeln!(uart, "SUCCESS: Pattern found at RX offset {}", found_offset).unwrap(); + } else { + writeln!(uart, "FAILURE: Pattern not found in RX buffer").unwrap(); + } + + // Test 2: Find alignment offset + writeln!(uart, "\n--- Test 2: Alignment discovery ---").unwrap(); + writeln!(uart, "Running find_alignment_offset procedure").unwrap(); + + tx_ringbuffer.clear(); + + let discovered_offset = + bittide_hal::manual_additions::aligned_ringbuffer::find_alignment_offset( + &tx_ringbuffer, + &rx_ringbuffer, + ); + + writeln!( + uart, + "SUCCESS: Discovered rx_offset = {}", + discovered_offset + ) + .unwrap(); + + // Test 3: Aligned transmission + writeln!(uart, "\n--- Test 3: Aligned transmission ---").unwrap(); + writeln!( + uart, + "Write to TX start, read from RX start with alignment offset" + ) + .unwrap(); + + rx_ringbuffer.set_offset(discovered_offset); + + tx_ringbuffer.clear(); + + const ALIGNED_TEST_SIZE: usize = 8; + let aligned_pattern: [[u8; 8]; ALIGNED_TEST_SIZE] = + core::array::from_fn(|i| (0x2000 + i as u64).to_le_bytes()); + + writeln!( + uart, + "Writing {} words to TX at offset 0", + ALIGNED_TEST_SIZE + ) + .unwrap(); + tx_ringbuffer.write_slice(&aligned_pattern, 0); + + let mut rx_data: [[u8; 8]; ALIGNED_TEST_SIZE] = [[0u8; 8]; ALIGNED_TEST_SIZE]; + writeln!( + uart, + "Reading {} words from RX at offset 0", + ALIGNED_TEST_SIZE + ) + .unwrap(); + rx_ringbuffer.read_slice(&mut rx_data, 0); + + let aligned_matches = aligned_pattern + .iter() + .zip(rx_data.iter()) + .filter(|(a, b)| a == b) + .count(); + + if aligned_matches == ALIGNED_TEST_SIZE { + writeln!(uart, "SUCCESS: All {} words matched!", ALIGNED_TEST_SIZE).unwrap(); + } else { + writeln!( + uart, + "FAILURE: Only {}/{} words matched", + aligned_matches, ALIGNED_TEST_SIZE + ) + .unwrap(); + for i in 0..ALIGNED_TEST_SIZE { + if aligned_pattern[i] != rx_data[i] { + writeln!( + uart, + " Mismatch at index {}: sent {:?}, received {:?}", + i, aligned_pattern[i], rx_data[i] + ) + .unwrap(); + } + } + } + + // Test 4: Wrapping behavior + writeln!(uart, "\n--- Test 4: Buffer wrapping ---").unwrap(); + writeln!( + uart, + "Write slice exceeding buffer end, verify split read/write" + ) + .unwrap(); + + tx_ringbuffer.clear(); + + const WRAP_SIZE: usize = 4; + const WRAP_OFFSET: usize = 15; // Start at 15, will wrap (buffer size is 16) + let wrap_pattern: [[u8; 8]; WRAP_SIZE] = + core::array::from_fn(|i| (0x3000 + i as u64).to_le_bytes()); + + writeln!( + uart, + "Writing {} words at TX offset {} (wraps at boundary)", + WRAP_SIZE, WRAP_OFFSET + ) + .unwrap(); + tx_ringbuffer.write_slice(&wrap_pattern, WRAP_OFFSET); + + let mut wrap_rx_data: [[u8; 8]; WRAP_SIZE] = [[0u8; 8]; WRAP_SIZE]; + writeln!( + uart, + "Reading {} words from RX offset {}", + WRAP_SIZE, WRAP_OFFSET + ) + .unwrap(); + rx_ringbuffer.read_slice(&mut wrap_rx_data, WRAP_OFFSET); + + let wrap_matches = wrap_pattern + .iter() + .zip(wrap_rx_data.iter()) + .filter(|(a, b)| a == b) + .count(); + + if wrap_matches == WRAP_SIZE { + writeln!( + uart, + "SUCCESS: All {} words matched across wrap boundary!", + WRAP_SIZE + ) + .unwrap(); + } else { + writeln!( + uart, + "FAILURE: Only {}/{} words matched", + wrap_matches, WRAP_SIZE + ) + .unwrap(); + for i in 0..WRAP_SIZE { + if wrap_pattern[i] != wrap_rx_data[i] { + writeln!( + uart, + " Mismatch at index {}: sent {:?}, received {:?}", + i, wrap_pattern[i], wrap_rx_data[i] + ) + .unwrap(); + } + } + } + + // Final summary + writeln!(uart, "\n=== Test Summary ===").unwrap(); + writeln!( + uart, + "Unaligned transmission: {}", + if found { "PASS" } else { "FAIL" } + ) + .unwrap(); + writeln!(uart, "Alignment discovery: PASS").unwrap(); + writeln!( + uart, + "Aligned transmission: {}", + if aligned_matches == ALIGNED_TEST_SIZE { + "PASS" + } else { + "FAIL" + } + ) + .unwrap(); + writeln!( + uart, + "Buffer wrapping: {}", + if wrap_matches == WRAP_SIZE { + "PASS" + } else { + "FAIL" + } + ) + .unwrap(); + + let all_passed = found && aligned_matches == ALIGNED_TEST_SIZE && wrap_matches == WRAP_SIZE; + if all_passed { + writeln!(uart, "\n*** ALL TESTS PASSED ***").unwrap(); + } else { + writeln!(uart, "\n*** SOME TESTS FAILED ***").unwrap(); + } + + loop { + continue; + } +} + +#[panic_handler] +fn panic_handler(info: &core::panic::PanicInfo) -> ! { + let mut uart = INSTANCES.uart; + writeln!(uart, "Panicked! #{info}").unwrap(); + loop { + continue; + } +} diff --git a/firmware-support/bittide-hal/src/manual_additions/aligned_ringbuffer.rs b/firmware-support/bittide-hal/src/manual_additions/aligned_ringbuffer.rs new file mode 100644 index 000000000..21b85a026 --- /dev/null +++ b/firmware-support/bittide-hal/src/manual_additions/aligned_ringbuffer.rs @@ -0,0 +1,204 @@ +// SPDX-FileCopyrightText: 2025 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +//! Aligned Ringbuffer Abstraction +//! +//! Provides `ReceiveRingbuffer` and `TransmitRingbuffer` wrappers around +//! `ScatterUnit` (RX) and `GatherUnit` (TX) for point-to-point communication. +//! See [Ringbuffer Alignment Protocol](../../../docs/sections/ringbuffer-alignment.md) +//! for alignment details. +//! +//! Both types automatically handle buffer wrapping when reads or writes extend +//! beyond the buffer boundary. +//! +//! # Reliability +//! +//! The physical link is unreliable due to hardware/CPU pointer races. Use a +//! higher-level protocol (e.g., TCP/IP via smoltcp) for reliable communication. + +use crate::hals::scatter_gather_pe::devices::{GatherUnit, ScatterUnit}; + +/// Alignment protocol marker values +const ALIGNMENT_EMPTY: u64 = 0; +const ALIGNMENT_ANNOUNCE: u64 = 0xBADC0FFEE; +const ALIGNMENT_ACKNOWLEDGE: u64 = 0xDEADABBA; + +/// Find the RX alignment offset by performing a two-phase discovery protocol. +/// +/// **Phase 1 (Discovery):** Writes `ALIGNMENT_ANNOUNCE` to TX buffer index 0, +/// then scans the entire RX buffer until it finds either `ALIGNMENT_ANNOUNCE` +/// or `ALIGNMENT_ACKNOWLEDGE` from the remote node. The index where the marker +/// is found becomes the RX alignment offset. +/// +/// **Phase 2 (Confirmation):** Writes `ALIGNMENT_ACKNOWLEDGE` to TX buffer +/// index 0, then polls the RX buffer at the discovered offset until receiving +/// an acknowledgment from the remote node, confirming bidirectional alignment. +/// +/// Returns the discovered RX alignment offset. +/// +/// # Note +/// +/// This function will loop indefinitely until alignment succeeds. Both nodes +/// must run this procedure simultaneously for it to complete. +pub fn find_alignment_offset(tx: &TransmitRingbuffer, rx: &ReceiveRingbuffer) -> usize { + let buffer_size = ScatterUnit::SCATTER_MEMORY_LEN; + + // Initialize TX buffer: write ANNOUNCE at index 0, clear the rest + let announce_pattern = [ALIGNMENT_ANNOUNCE.to_le_bytes()]; + tx.write_slice(&announce_pattern, 0); + + let empty_pattern: [[u8; 8]; 1] = [ALIGNMENT_EMPTY.to_le_bytes()]; + for i in 1..buffer_size { + tx.write_slice(&empty_pattern, i); + } + + // Phase 1: Scan RX buffer to find ANNOUNCE or ACKNOWLEDGE + // Read directly from scatter memory using read_slice with offset 0 + let rx_offset = 'outer: loop { + for rx_idx in 0..buffer_size { + let mut data_buf = [[0u8; 8]; 1]; + // Read directly from physical index by using scatter's read_slice + rx.scatter.read_slice(&mut data_buf, rx_idx); + let value = u64::from_le_bytes(data_buf[0]); + + if value == ALIGNMENT_ANNOUNCE || value == ALIGNMENT_ACKNOWLEDGE { + break 'outer rx_idx; + } + } + }; + + // Phase 2: Send ACKNOWLEDGE and wait for confirmation + let ack_pattern = [ALIGNMENT_ACKNOWLEDGE.to_le_bytes()]; + tx.write_slice(&ack_pattern, 0); + + loop { + let mut data_buf = [[0u8; 8]; 1]; + // Read directly from physical index + rx.scatter.read_slice(&mut data_buf, rx_offset); + let value = u64::from_le_bytes(data_buf[0]); + + if value == ALIGNMENT_ACKNOWLEDGE { + break; + } + } + + rx_offset +} + +/// Receive ringbuffer wrapping `ScatterUnit` with alignment offset. +/// +/// The offset indicates where the remote transmitter's index 0 appears in the +/// local receive buffer. Reads automatically wrap at buffer boundaries. +pub struct ReceiveRingbuffer { + /// The underlying scatter unit (RX buffer - hardware writes, CPU reads) + scatter: ScatterUnit, + /// The alignment offset: the index in our RX ringbuffer where the remote + /// TX index 0 appears + rx_offset: usize, +} + +impl ReceiveRingbuffer { + /// Create a new receive ringbuffer with the specified alignment offset. + pub fn new(scatter: ScatterUnit, rx_offset: usize) -> Self { + Self { scatter, rx_offset } + } + + /// Read from the ringbuffer at a logical offset, adjusted by alignment offset. + /// + /// Wraps automatically if the read extends beyond the buffer boundary. + /// + /// # Panics + /// + /// Panics if `user_offset` is >= buffer size. + pub fn read_slice(&self, dst: &mut [[u8; 8]], user_offset: usize) { + // Panic if the user offset is out of bounds + if user_offset >= ScatterUnit::SCATTER_MEMORY_LEN { + panic!( + "Offset {} out of bounds for scatter memory length {}", + user_offset, + ScatterUnit::SCATTER_MEMORY_LEN + ); + } + // Adjust the user offset by our rx_offset, wrapping around if necessary + let mut offset: usize = self.rx_offset + user_offset; + if offset >= ScatterUnit::SCATTER_MEMORY_LEN { + offset -= ScatterUnit::SCATTER_MEMORY_LEN + }; + + // Read from scatter memory with wrapping if necessary + if offset + dst.len() <= ScatterUnit::SCATTER_MEMORY_LEN { + // No wrapping needed + self.scatter.read_slice(dst, offset); + } else { + // Wrapping needed - split into two reads + let first_part_len = ScatterUnit::SCATTER_MEMORY_LEN - offset; + let (first, second) = dst.split_at_mut(first_part_len); + self.scatter.read_slice(first, offset); + self.scatter.read_slice(second, 0); + } + } + + /// Returns the alignment offset. + pub fn offset(&self) -> usize { + self.rx_offset + } + + /// Sets the alignment offset. + pub fn set_offset(&mut self, offset: usize) { + self.rx_offset = offset; + } +} + +/// Transmit ringbuffer wrapping `GatherUnit`. +/// +/// Writes automatically wrap at buffer boundaries. +pub struct TransmitRingbuffer { + /// The underlying gather unit (TX buffer - CPU writes, hardware reads) + gather: GatherUnit, +} + +impl TransmitRingbuffer { + /// Create a new transmit ringbuffer. + pub fn new(gather: GatherUnit) -> Self { + Self { gather } + } + + /// Write to the ringbuffer at an offset. + /// + /// Wraps automatically if the write extends beyond the buffer boundary. + /// + /// # Panics + /// + /// Panics if `offset` is >= buffer size. + pub fn write_slice(&self, src: &[[u8; 8]], offset: usize) { + // Panic if the offset is out of bounds + if offset >= GatherUnit::GATHER_MEMORY_LEN { + panic!( + "Offset {} out of bounds for gather memory length {}", + offset, + GatherUnit::GATHER_MEMORY_LEN + ); + } + + // Write to gather memory with wrapping if necessary + if offset + src.len() <= GatherUnit::GATHER_MEMORY_LEN { + // No wrapping needed + self.gather.write_slice(src, offset); + } else { + // Wrapping needed - split into two writes + let first_part_len = GatherUnit::GATHER_MEMORY_LEN - offset; + let (first, second) = src.split_at(first_part_len); + self.gather.write_slice(first, offset); + self.gather.write_slice(second, 0); + } + } + + /// Clears the buffer by writing zeros to all entries. + pub fn clear(&self) { + let zero_pattern = [[0u8; 8]; 1]; + for i in 0..GatherUnit::GATHER_MEMORY_LEN { + self.write_slice(&zero_pattern, i); + } + } +} diff --git a/firmware-support/bittide-hal/src/manual_additions/mod.rs b/firmware-support/bittide-hal/src/manual_additions/mod.rs index e0c2dcdd1..37bad3f0d 100644 --- a/firmware-support/bittide-hal/src/manual_additions/mod.rs +++ b/firmware-support/bittide-hal/src/manual_additions/mod.rs @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 +pub mod aligned_ringbuffer; pub mod calendar; pub mod capture_ugn; pub mod dna;