From 3f5bc615058c0c77b2b004a98c5f8091c59ee1ca Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Tue, 20 Jan 2026 17:15:40 +0100 Subject: [PATCH 01/70] Use `addressableBytesWb` in `wbStorage` From a525d642384369e5f49723a6d1d97b46e318323e Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Tue, 20 Jan 2026 17:15:40 +0100 Subject: [PATCH 02/70] Use `addressableBytesWb` in `wbStorage` From 48bbfb5d1fb313ff281d46fc9df7a02725437aa9 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Tue, 20 Jan 2026 17:15:40 +0100 Subject: [PATCH 03/70] Use `addressableBytesWb` in `wbStorage` From 84c7f3a4e8be9a286a9faa20c87288fbeb20a35a Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Tue, 20 Jan 2026 17:15:40 +0100 Subject: [PATCH 04/70] Use `addressableBytesWb` in `wbStorage` From 5c199f048b16c98ecd4cd84abf93d2c010de0d9f Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Tue, 20 Jan 2026 17:15:40 +0100 Subject: [PATCH 05/70] Use `addressableBytesWb` in `wbStorage` From 59ed7244909c856213e2f47ebc333e03c67a853a Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Tue, 20 Jan 2026 17:15:40 +0100 Subject: [PATCH 06/70] Use `addressableBytesWb` in `wbStorage` From 925457403da4780989925ab2d519b74a0444f4ca Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Tue, 20 Jan 2026 17:15:40 +0100 Subject: [PATCH 07/70] Use `addressableBytesWb` in `wbStorage` From 6db718728a13f901368a601330215efac33387be Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Tue, 20 Jan 2026 17:15:40 +0100 Subject: [PATCH 08/70] Use `addressableBytesWb` in `wbStorage` From 0018d1fa4f983d456b5a3121b7fcde51a832536b Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Tue, 20 Jan 2026 17:15:40 +0100 Subject: [PATCH 09/70] Use `addressableBytesWb` in `wbStorage` From 7e43c5c7b8f47062803c2ec0396f991bf461cdb5 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Tue, 20 Jan 2026 17:15:40 +0100 Subject: [PATCH 10/70] Use `addressableBytesWb` in `wbStorage` From 4f1796ebba981085f557074e1153c65080c35edf Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Tue, 20 Jan 2026 17:15:40 +0100 Subject: [PATCH 11/70] Use `addressableBytesWb` in `wbStorage` From e11711c671bc806192d9b44572b072b896fe1866 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Tue, 20 Jan 2026 17:15:40 +0100 Subject: [PATCH 12/70] Use `addressableBytesWb` in `wbStorage` From 0c48eb0765aa58bfaefded807e70176edf1120dc Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Tue, 20 Jan 2026 17:15:40 +0100 Subject: [PATCH 13/70] Use `addressableBytesWb` in `wbStorage` From f801a648838fc93ba4767758c12a770e7a01a80b Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Tue, 20 Jan 2026 17:15:40 +0100 Subject: [PATCH 14/70] Use `addressableBytesWb` in `wbStorage` From 40a008126db7c6bfa8c7dd601daa1d95d3660aba Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Thu, 18 Dec 2025 16:20:13 +0100 Subject: [PATCH 15/70] Add types and test for implementing ringbuffer abstractions in rust --- .../tests/Wishbone/ScatterGather.hs | 26 ++ firmware-binaries/Cargo.lock | 11 + firmware-binaries/Cargo.toml | 1 + .../aligned_ringbuffer_test/Cargo.toml | 21 ++ .../aligned_ringbuffer_test/build.rs | 10 + .../aligned_ringbuffer_test/memory.x | 18 ++ .../aligned_ringbuffer_test/src/main.rs | 306 ++++++++++++++++++ .../manual_additions/aligned_ringbuffer.rs | 204 ++++++++++++ .../bittide-hal/src/manual_additions/mod.rs | 2 +- 9 files changed, 598 insertions(+), 1 deletion(-) create mode 100644 firmware-binaries/sim-tests/aligned_ringbuffer_test/Cargo.toml create mode 100644 firmware-binaries/sim-tests/aligned_ringbuffer_test/build.rs create mode 100644 firmware-binaries/sim-tests/aligned_ringbuffer_test/memory.x create mode 100644 firmware-binaries/sim-tests/aligned_ringbuffer_test/src/main.rs create mode 100644 firmware-support/bittide-hal/src/manual_additions/aligned_ringbuffer.rs 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 eee8720c2..8698baab0 100644 --- a/firmware-binaries/Cargo.lock +++ b/firmware-binaries/Cargo.lock @@ -24,6 +24,17 @@ dependencies = [ "memchr", ] +[[package]] +name = "aligned_ringbuffer_test" +version = "0.1.0" +dependencies = [ + "bittide-hal", + "bittide-sys", + "memmap-generate", + "riscv-rt 0.11.0", + "ufmt", +] + [[package]] name = "axi_stream_self_test" version = "0.1.0" diff --git a/firmware-binaries/Cargo.toml b/firmware-binaries/Cargo.toml index de9b1ef8f..fe6730e3c 100644 --- a/firmware-binaries/Cargo.toml +++ b/firmware-binaries/Cargo.toml @@ -17,6 +17,7 @@ members = [ "examples/smoltcp_client", "sim-tests/addressable_bytes_wb_test", + "sim-tests/aligned_ringbuffer_test", "sim-tests/axi_stream_self_test", "sim-tests/registerwb_test", "sim-tests/ringbuffer_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 5da1ace56..b2df1ad9d 100644 --- a/firmware-support/bittide-hal/src/manual_additions/mod.rs +++ b/firmware-support/bittide-hal/src/manual_additions/mod.rs @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 pub mod addressable_buffer; -pub mod aligned; +pub mod aligned_ringbuffer; pub mod calendar; pub mod capture_ugn; pub mod dna; From fc6e77e9b392a80fe4e2e7be6c76701d18404d44 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Thu, 8 Jan 2026 09:35:25 +0100 Subject: [PATCH 16/70] Increase `ScatterGather` test instance memory size in preparation for smoltcp test --- .../src/Bittide/Instances/Tests/ScatterGather.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittide-instances/src/Bittide/Instances/Tests/ScatterGather.hs b/bittide-instances/src/Bittide/Instances/Tests/ScatterGather.hs index bf30bceb6..60c6da619 100644 --- a/bittide-instances/src/Bittide/Instances/Tests/ScatterGather.hs +++ b/bittide-instances/src/Bittide/Instances/Tests/ScatterGather.hs @@ -101,5 +101,5 @@ dutWithBinary binaryName = withBittideByteOrder $ circuit $ \mm -> do } {-# OPAQUE dutWithBinary #-} -type IMemWords = DivRU (64 * 1024) 4 -type DMemWords = DivRU (32 * 1024) 4 +type IMemWords = DivRU (300 * 1024) 4 +type DMemWords = DivRU (256 * 1024) 4 From 6f2d36af729fba18de310febdaadcbb5be37f64c Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Thu, 8 Jan 2026 09:43:01 +0100 Subject: [PATCH 17/70] Add `smoltcp` `Device` implementation for pairs of `TransmitRingbuffer` and `ReceiveRingbuffer` --- firmware-support/bittide-sys/src/smoltcp.rs | 1 + .../bittide-sys/src/smoltcp/ringbuffer.rs | 261 ++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs diff --git a/firmware-support/bittide-sys/src/smoltcp.rs b/firmware-support/bittide-sys/src/smoltcp.rs index 6eaa828e6..4693cf4d0 100644 --- a/firmware-support/bittide-sys/src/smoltcp.rs +++ b/firmware-support/bittide-sys/src/smoltcp.rs @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 pub mod axi; +pub mod ringbuffer; use smoltcp::wire::EthernetAddress; diff --git a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs new file mode 100644 index 000000000..e84fd1501 --- /dev/null +++ b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs @@ -0,0 +1,261 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +//! smoltcp Device implementation for aligned ringbuffers. +//! +//! This module provides a `Device` implementation that uses scatter/gather units +//! as ringbuffers for point-to-point IP communication. The ringbuffers must +//! be aligned using the alignment protocol before use. +//! +//! The device operates at the IP layer (not Ethernet) since the connections are +//! point-to-point and don't require MAC addressing or routing. +//! +//! The ringbuffer abstraction handles all offset and wrapping management internally, +//! so we simply read/write packets from the start each time. + +use bittide_hal::hals::scatter_gather_pe::devices::{GatherUnit, ScatterUnit}; +use bittide_hal::manual_additions::aligned_ringbuffer::{ReceiveRingbuffer, TransmitRingbuffer}; +use log::{debug, trace}; +use smoltcp::phy::{self, Device, DeviceCapabilities, Medium}; +use smoltcp::time::Instant; + +/// Number of times we can see the same packet ID before considering it stale +const STALE_PACKET_THRESHOLD: u32 = 3; + +/// Size of packet ID header in bytes (32-bit identifier) +const PACKET_ID_SIZE: usize = 4; + +/// Device implementation for ringbuffer communication. +/// +/// Provides a simple interface for point-to-point IP communication using +/// scatter/gather units. The ringbuffers handle all internal state management. +/// +/// The MTU is automatically calculated from the minimum of the scatter and gather +/// buffer sizes (in bytes). +pub struct RingbufferDevice { + rx_buffer: ReceiveRingbuffer, + tx_buffer: TransmitRingbuffer, + mtu: usize, + /// Next packet ID to send + tx_packet_id: u32, + /// Last packet ID we received + rx_packet_id: u32, + /// How many times we've seen the current RX packet ID + rx_packet_id_count: u32, +} + +impl RingbufferDevice { + /// Create a new ringbuffer device. + /// + /// The ringbuffers must already be aligned using the alignment protocol. + /// The MTU is calculated as the minimum of the RX and TX buffer sizes in bytes. + pub fn new(rx_buffer: ReceiveRingbuffer, tx_buffer: TransmitRingbuffer) -> Self { + // Calculate MTU from buffer sizes (each word is 8 bytes) + // Reserve space for packet ID header + let rx_bytes = ScatterUnit::SCATTER_MEMORY_LEN * 8; + let tx_bytes = GatherUnit::GATHER_MEMORY_LEN * 8; + let mtu = rx_bytes.min(tx_bytes) - PACKET_ID_SIZE; + + Self { + rx_buffer, + tx_buffer, + mtu, + tx_packet_id: 1, // Has to be different from first rx packet id + rx_packet_id: 0, + rx_packet_id_count: 0, + } + } + + /// Get the maximum transmission unit (in bytes) for this device. + pub fn mtu(&self) -> usize { + self.mtu + } +} + +impl Device for RingbufferDevice { + type RxToken<'a> = RxToken<'a>; + type TxToken<'a> = TxToken<'a>; + + fn capabilities(&self) -> DeviceCapabilities { + let mut cap = DeviceCapabilities::default(); + cap.max_transmission_unit = self.mtu; + cap.medium = Medium::Ethernet; + cap + } + + fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { + // Read the packet ID (first 4 bytes) + let mut peek_buf: [[u8; 8]; 1] = [[0u8; 8]; 1]; + self.rx_buffer.read_slice(&mut peek_buf, 0); + + // Extract packet ID from first 4 bytes (little-endian) + let packet_id = u32::from_le_bytes(peek_buf[0][..4].try_into().unwrap()); + + // Check if this is the same packet we saw before + if packet_id == self.rx_packet_id { + self.rx_packet_id_count += 1; + trace!( + "Packet ID {} seen {} times", + packet_id, + self.rx_packet_id_count + ); + + // If we've seen it too many times, consider it stale and ignore + if self.rx_packet_id_count >= STALE_PACKET_THRESHOLD { + trace!("Packet {} is stale, ignoring", packet_id); + return None; + } + } else { + // New packet ID + trace!( + "New packet ID: {} (previous: {})", + packet_id, + self.rx_packet_id + ); + self.rx_packet_id = packet_id; + self.rx_packet_id_count = 1; + } + + let rx = RxToken { + rx_buffer: &mut self.rx_buffer, + mtu: self.mtu, + }; + let tx = TxToken { + tx_buffer: &mut self.tx_buffer, + mtu: self.mtu, + packet_id: &mut self.tx_packet_id, + }; + Some((rx, tx)) + } + + fn transmit(&mut self, _timestamp: Instant) -> Option> { + Some(TxToken { + tx_buffer: &mut self.tx_buffer, + mtu: self.mtu, + packet_id: &mut self.tx_packet_id, + }) + } +} + +/// Receive token for ringbuffer device +pub struct RxToken<'a> { + rx_buffer: &'a mut ReceiveRingbuffer, + mtu: usize, +} + +impl phy::RxToken for RxToken<'_> { + fn consume(self, f: F) -> R + where + F: FnOnce(&[u8]) -> R, + { + // Read packet from ringbuffer starting at offset 0 + // First 4 bytes are packet ID, rest is actual packet data + let total_bytes = self.mtu + PACKET_ID_SIZE; + let num_words = total_bytes.div_ceil(8); + + // Allocate buffer based on actual MTU + packet ID + let max_words = ScatterUnit::SCATTER_MEMORY_LEN; + let mut buffer = [[0u8; 8]; ScatterUnit::SCATTER_MEMORY_LEN]; + let words = &mut buffer[..num_words.min(max_words)]; + + self.rx_buffer.read_slice(words, 0); + + // Convert to byte slice for processing + let mut bytes = [0u8; ScatterUnit::SCATTER_MEMORY_LEN * 8]; + let mut idx = 0; + for word in words.iter() { + for &byte in word.iter() { + bytes[idx] = byte; + idx += 1; + if idx >= total_bytes { + break; + } + } + if idx >= total_bytes { + break; + } + } + + // Extract packet ID for logging + let packet_id = u32::from_le_bytes(bytes[..4].try_into().unwrap()); + + // Process the packet (skip the 4-byte packet ID header) + let result = f(&bytes[PACKET_ID_SIZE..PACKET_ID_SIZE + self.mtu]); + trace!( + "Consumed packet {} ({} bytes from RX buffer)", + packet_id, + self.mtu + ); + result + } +} + +/// Transmit token for ringbuffer device +pub struct TxToken<'a> { + tx_buffer: &'a mut TransmitRingbuffer, + mtu: usize, + packet_id: &'a mut u32, +} + +impl phy::TxToken for TxToken<'_> { + fn consume(self, len: usize, f: F) -> R + where + F: FnOnce(&mut [u8]) -> R, + { + // Ensure the packet doesn't exceed MTU + if len > self.mtu { + debug!("Packet length {} exceeds MTU {}", len, self.mtu); + } + assert!( + len <= self.mtu, + "Packet length {} exceeds MTU {}", + len, + self.mtu + ); + + // Prepare buffer: packet ID (4 bytes) + packet data + let mut bytes = [0u8; GatherUnit::GATHER_MEMORY_LEN * 8]; + + // Write packet ID in first 4 bytes (little-endian) + let packet_id_bytes = self.packet_id.to_le_bytes(); + bytes[0] = packet_id_bytes[0]; + bytes[1] = packet_id_bytes[1]; + bytes[2] = packet_id_bytes[2]; + bytes[3] = packet_id_bytes[3]; + + // Let smoltcp fill the packet data after the ID + let packet = &mut bytes[PACKET_ID_SIZE..PACKET_ID_SIZE + len]; + let result = f(packet); + + // Convert to word array for ringbuffer (packet ID + data) + let total_len = PACKET_ID_SIZE + len; + let num_words = total_len.div_ceil(8); + let max_words = GatherUnit::GATHER_MEMORY_LEN; + let mut buffer = [[0u8; 8]; GatherUnit::GATHER_MEMORY_LEN]; + + for (i, chunk) in bytes[..total_len].chunks(8).enumerate() { + if i >= max_words { + break; + } + for (j, &byte) in chunk.iter().enumerate() { + buffer[i][j] = byte; + } + } + + // Write to ringbuffer starting at offset 0 + let words = &buffer[..num_words.min(max_words)]; + self.tx_buffer.write_slice(words, 0); + + trace!( + "Transmitted packet {} ({} bytes to TX buffer)", + *self.packet_id, + len + ); + + // Increment packet ID for next transmission + *self.packet_id = self.packet_id.wrapping_add(1); + + result + } +} From d1fcea00c93f1efe63e13ed4ed28b36e2727276a Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Thu, 8 Jan 2026 10:09:04 +0100 Subject: [PATCH 18/70] Add ringbuffer smoltcp loopback test --- .../tests/Wishbone/ScatterGather.hs | 26 ++ firmware-binaries/Cargo.lock | 14 ++ firmware-binaries/Cargo.toml | 1 + .../ringbuffer_smoltcp_test/Cargo.toml | 29 +++ .../ringbuffer_smoltcp_test/build.rs | 10 + .../ringbuffer_smoltcp_test/src/main.rs | 227 ++++++++++++++++++ 6 files changed, 307 insertions(+) create mode 100644 firmware-binaries/sim-tests/ringbuffer_smoltcp_test/Cargo.toml create mode 100644 firmware-binaries/sim-tests/ringbuffer_smoltcp_test/build.rs create mode 100644 firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs diff --git a/bittide-instances/tests/Wishbone/ScatterGather.hs b/bittide-instances/tests/Wishbone/ScatterGather.hs index 3d84efdab..9cfbdd023 100644 --- a/bittide-instances/tests/Wishbone/ScatterGather.hs +++ b/bittide-instances/tests/Wishbone/ScatterGather.hs @@ -99,5 +99,31 @@ case_aligned_ringbuffer_test = do where msg = "Received the following from the CPU over UART:\n" <> simResultAlignedRingbuffer +-- Ringbuffer smoltcp test simulation +simRingbufferSmoltcp :: IO () +simRingbufferSmoltcp = putStr simResultRingbufferSmoltcp + +simResultRingbufferSmoltcp :: (HasCallStack) => String +simResultRingbufferSmoltcp = chr . fromIntegral <$> catMaybes uartStream + where + uartStream = sampleC def{timeoutAfter = 1_000_000} dutNoMM + + dutNoMM :: (HasCallStack) => Circuit () (Df System (BitVector 8)) + dutNoMM = circuit $ do + mm <- ignoreMM + uartTx <- + withClockResetEnable clockGen (resetGenN d2) enableGen + $ (dutWithBinary "ringbuffer_smoltcp_test") + -< mm + idC -< uartTx + +case_ringbuffer_smoltcp_test :: Assertion +case_ringbuffer_smoltcp_test = do + assertBool + msg + ("SUCCESS: Data matches!" `isInfixOf` simResultRingbufferSmoltcp) + where + msg = "Received the following from the CPU over UART:\n" <> simResultRingbufferSmoltcp + tests :: TestTree tests = $(testGroupGenerator) diff --git a/firmware-binaries/Cargo.lock b/firmware-binaries/Cargo.lock index 8698baab0..61b160749 100644 --- a/firmware-binaries/Cargo.lock +++ b/firmware-binaries/Cargo.lock @@ -499,6 +499,20 @@ dependencies = [ "ufmt", ] +[[package]] +name = "ringbuffer_smoltcp_test" +version = "0.1.0" +dependencies = [ + "bittide-hal", + "bittide-sys", + "log", + "memmap-generate", + "riscv 0.10.1", + "riscv-rt 0.11.0", + "smoltcp 0.12.0", + "ufmt", +] + [[package]] name = "ringbuffer_test" version = "0.1.0" diff --git a/firmware-binaries/Cargo.toml b/firmware-binaries/Cargo.toml index fe6730e3c..38554b178 100644 --- a/firmware-binaries/Cargo.toml +++ b/firmware-binaries/Cargo.toml @@ -21,6 +21,7 @@ members = [ "sim-tests/axi_stream_self_test", "sim-tests/registerwb_test", "sim-tests/ringbuffer_test", + "sim-tests/ringbuffer_smoltcp_test", "sim-tests/capture_ugn_test", "sim-tests/clock-control-wb", "sim-tests/dna_port_e2_test", diff --git a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/Cargo.toml b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/Cargo.toml new file mode 100644 index 000000000..4b480216c --- /dev/null +++ b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/Cargo.toml @@ -0,0 +1,29 @@ +# SPDX-FileCopyrightText: 2025 Google LLC +# +# SPDX-License-Identifier: CC0-1.0 + +[package] +name = "ringbuffer_smoltcp_test" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +authors = ["Google LLC"] + +[dependencies] +riscv-rt = "0.11.0" +riscv = "0.10.1" +bittide-sys = { path = "../../../firmware-support/bittide-sys" } +bittide-hal = { path = "../../../firmware-support/bittide-hal" } +ufmt = "0.2.0" + +[dependencies.smoltcp] +version = "0.12.0" +default-features = false +features = ["medium-ethernet", "proto-ipv4", "socket-tcp"] + +[dependencies.log] +version = "0.4.21" +features = ["max_level_trace", "release_max_level_info"] + +[build-dependencies] +memmap-generate = { path = "../../../firmware-support/memmap-generate" } diff --git a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/build.rs b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/build.rs new file mode 100644 index 000000000..70193e4cc --- /dev/null +++ b/firmware-binaries/sim-tests/ringbuffer_smoltcp_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/ringbuffer_smoltcp_test/src/main.rs b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs new file mode 100644 index 000000000..85a58396f --- /dev/null +++ b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs @@ -0,0 +1,227 @@ +// SPDX-FileCopyrightText: 2025 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 +#![no_std] +#![cfg_attr(not(test), no_main)] +#![feature(sync_unsafe_cell)] + +use bittide_hal::manual_additions::aligned_ringbuffer::{ + find_alignment_offset, ReceiveRingbuffer, TransmitRingbuffer, +}; +use bittide_hal::scatter_gather_pe::DeviceInstances; +use bittide_sys::smoltcp::ringbuffer::RingbufferDevice; +use core::fmt::Write; +use log::LevelFilter; +use smoltcp::iface::{Config, Interface, SocketSet, SocketStorage}; +use smoltcp::socket::tcp; +use smoltcp::time::Instant as SmolInstant; +use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr}; + +#[cfg(not(test))] +use riscv_rt::entry; + +const INSTANCES: DeviceInstances = unsafe { DeviceInstances::new() }; + +const SERVER_PORT: u16 = 8080; +const CLIENT_PORT: u16 = 49152; + +#[cfg_attr(not(test), entry)] +fn main() -> ! { + let mut uart = INSTANCES.uart; + + writeln!(uart, "\n=== Ringbuffer smoltcp Loopback Test ===").ok(); + + // Set up logging + unsafe { + use bittide_sys::uart::log::LOGGER; + let logger = &mut (*LOGGER.get()); + logger.set_logger(uart.clone()); + logger.display_source = LevelFilter::Debug; + log::set_logger_racy(logger).ok(); + log::set_max_level_racy(LevelFilter::Trace); + } + + // Step 1: Find alignment offset + writeln!(uart, "Step 1: Finding ringbuffer alignment...").ok(); + let rx_offset = { + let scatter = INSTANCES.scatter_unit; + let gather = INSTANCES.gather_unit; + let hal_tx_temp = TransmitRingbuffer::new(gather); + let hal_rx_temp = ReceiveRingbuffer::new(scatter, 0); + find_alignment_offset(&hal_tx_temp, &hal_rx_temp) + }; + writeln!(uart, " Alignment offset: {}", rx_offset).ok(); + + // Step 2: Create smoltcp device + writeln!(uart, "Step 2: Creating RingbufferDevice...").ok(); + let scatter = INSTANCES.scatter_unit; + let gather = INSTANCES.gather_unit; + let rx_buffer = ReceiveRingbuffer::new(scatter, rx_offset); + let tx_buffer = TransmitRingbuffer::new(gather); + let mut device = RingbufferDevice::new(rx_buffer, tx_buffer); + let mtu = device.mtu(); + writeln!(uart, " MTU: {} bytes", mtu).ok(); + + // Step 3: Configure interface with static IP + writeln!(uart, "Step 3: Configuring network interface...").ok(); + let mac_addr = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]); + let ip_addr = IpCidr::new(IpAddress::v4(127, 0, 0, 1), 8); // Loopback address + let config = Config::new(mac_addr.into()); + let now = SmolInstant::from_millis(0); + let mut iface = Interface::new(config, &mut device, now); + iface.update_ip_addrs(|addrs| { + addrs.push(ip_addr).unwrap(); + }); + writeln!(uart, " IP: {}", ip_addr).ok(); + + // Step 4: Create TCP sockets + writeln!(uart, "Step 4: Creating TCP sockets...").ok(); + + // Server socket - reduced buffer sizes to fit in memory + static mut SERVER_RX_BUF: [u8; 256] = [0; 256]; + static mut SERVER_TX_BUF: [u8; 256] = [0; 256]; + let server_rx_buffer = tcp::SocketBuffer::new(unsafe { &mut SERVER_RX_BUF[..] }); + let server_tx_buffer = tcp::SocketBuffer::new(unsafe { &mut SERVER_TX_BUF[..] }); + let server_socket = tcp::Socket::new(server_rx_buffer, server_tx_buffer); + + // Client socket - reduced buffer sizes to fit in memory + static mut CLIENT_RX_BUF: [u8; 256] = [0; 256]; + static mut CLIENT_TX_BUF: [u8; 256] = [0; 256]; + let client_rx_buffer = tcp::SocketBuffer::new(unsafe { &mut CLIENT_RX_BUF[..] }); + let client_tx_buffer = tcp::SocketBuffer::new(unsafe { &mut CLIENT_TX_BUF[..] }); + let client_socket = tcp::Socket::new(client_rx_buffer, client_tx_buffer); + + let mut sockets_storage: [SocketStorage; 2] = Default::default(); + let mut sockets = SocketSet::new(&mut sockets_storage[..]); + let server_handle = sockets.add(server_socket); + let client_handle = sockets.add(client_socket); + + // Step 5: Set up server to listen + writeln!( + uart, + "Step 5: Starting TCP server on port {}...", + SERVER_PORT + ) + .ok(); + let server = sockets.get_mut::(server_handle); + server.listen(SERVER_PORT).unwrap(); + writeln!(uart, " Server listening").ok(); + + // Step 6: Connect client to server + writeln!(uart, "Step 6: Connecting client to server...").ok(); + let client = sockets.get_mut::(client_handle); + let cx = iface.context(); + client + .connect(cx, (IpAddress::v4(127, 0, 0, 1), SERVER_PORT), CLIENT_PORT) + .unwrap(); + writeln!(uart, " Connection initiated").ok(); + + // Step 7: Poll until connection established + writeln!(uart, "Step 7: Waiting for connection...").ok(); + let mut connection_established = false; + for tick in 0..100 { + let timestamp = SmolInstant::from_millis(tick * 10); + iface.poll(timestamp, &mut device, &mut sockets); + + let client = sockets.get::(client_handle); + if client.is_active() && !connection_established { + writeln!(uart, " Connection established!").ok(); + connection_established = true; + break; + } + } + + if !connection_established { + writeln!(uart, " FAILURE: Connection timeout!").ok(); + loop { + unsafe { riscv::asm::wfi() }; + } + } + + // Step 8: Send data from client + writeln!(uart, "Step 8: Sending test data...").ok(); + let test_data = b"Hello from smoltcp!"; + for tick in 100..150 { + let timestamp = SmolInstant::from_millis(tick * 10); + iface.poll(timestamp, &mut device, &mut sockets); + + let client = sockets.get_mut::(client_handle); + if client.can_send() { + match client.send_slice(test_data) { + Ok(sent) => { + writeln!(uart, " Sent {} bytes", sent).ok(); + break; + } + Err(_) => continue, + } + } + } + + // Step 9: Receive data on server + writeln!(uart, "Step 9: Receiving data on server...").ok(); + let mut received_data = [0u8; 64]; + let mut received_len = 0; + + for tick in 150..200 { + let timestamp = SmolInstant::from_millis(tick * 10); + iface.poll(timestamp, &mut device, &mut sockets); + + let server = sockets.get_mut::(server_handle); + if server.can_recv() { + match server.recv_slice(&mut received_data) { + Ok(len) => { + received_len = len; + writeln!(uart, " Received {} bytes", len).ok(); + break; + } + Err(_) => continue, + } + } + } + + // Step 10: Verify data + writeln!(uart, "Step 10: Verifying data...").ok(); + let received_slice = &received_data[..received_len]; + + if received_len == test_data.len() && received_slice == test_data { + writeln!(uart, " SUCCESS: Data matches!").ok(); + writeln!( + uart, + " Sent: {:?}", + core::str::from_utf8(test_data).unwrap() + ) + .ok(); + writeln!( + uart, + " Received: {:?}", + core::str::from_utf8(received_slice).unwrap() + ) + .ok(); + } else { + writeln!(uart, " FAILURE: Data mismatch!").ok(); + writeln!( + uart, + " Expected {} bytes: {:?}", + test_data.len(), + test_data + ) + .ok(); + writeln!(uart, " Got {} bytes: {:?}", received_len, received_slice).ok(); + } + + writeln!(uart, "\n=== Test Complete ===").ok(); + + loop { + unsafe { riscv::asm::wfi() }; + } +} + +#[cfg(not(test))] +#[panic_handler] +fn panic(info: &core::panic::PanicInfo) -> ! { + let mut uart = INSTANCES.uart; + writeln!(uart, "PANIC: {}", info).ok(); + loop { + unsafe { riscv::asm::wfi() }; + } +} From ae17b977ab41ef93755e192af2e03a5041e42ddc Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Fri, 9 Jan 2026 11:55:24 +0100 Subject: [PATCH 19/70] Add timing peripheral to `ScatterGather` test architecture Used in smoltcp test --- .../src/Bittide/Instances/Tests/ScatterGather.hs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/bittide-instances/src/Bittide/Instances/Tests/ScatterGather.hs b/bittide-instances/src/Bittide/Instances/Tests/ScatterGather.hs index 60c6da619..90c2f4d9b 100644 --- a/bittide-instances/src/Bittide/Instances/Tests/ScatterGather.hs +++ b/bittide-instances/src/Bittide/Instances/Tests/ScatterGather.hs @@ -58,7 +58,7 @@ dutMM = -- | Parameterized DUT that loads a specific firmware binary. dutWithBinary :: - (HasCallStack, HiddenClockResetEnable dom) => + (HasCallStack, HiddenClockResetEnable dom, 1 <= DomainPeriod dom) => String -> Circuit (ToConstBwd Mm) (Df dom (BitVector 8)) dutWithBinary binaryName = withBittideByteOrder $ circuit $ \mm -> do @@ -68,11 +68,13 @@ dutWithBinary binaryName = withBittideByteOrder $ circuit $ \mm -> do , wbGu , wbSuCal , wbGuCal + , timeBus ] <- processingElement NoDumpVcd (peConfig binaryName) -< (mm, jtagIdle) (uartTx, _uartStatus) <- uartInterfaceWb d16 d2 uartBytes -< (uartBus, uartRx) Fwd link <- gatherUnitWbC gatherConfig -< (wbGu, wbGuCal) scatterUnitWbC scatterConfig link -< (wbSu, wbSuCal) + _cnt <- timeWb -< timeBus idC -< uartTx where peConfig binary = unsafePerformIO $ do From 9b9a006d913cd5009b1c37b158c896d0f13eee3e Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Fri, 9 Jan 2026 11:56:08 +0100 Subject: [PATCH 20/70] Lower smoltcp simtest frequency. This test may wait for a certain amount of time --- bittide-instances/tests/Wishbone/ScatterGather.hs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/bittide-instances/tests/Wishbone/ScatterGather.hs b/bittide-instances/tests/Wishbone/ScatterGather.hs index 9cfbdd023..80b14daf9 100644 --- a/bittide-instances/tests/Wishbone/ScatterGather.hs +++ b/bittide-instances/tests/Wishbone/ScatterGather.hs @@ -1,6 +1,8 @@ -- SPDX-FileCopyrightText: 2024 Google LLC -- -- SPDX-License-Identifier: Apache-2.0 +-- Don't warn about orphan instances, caused by `createDomain`. +{-# OPTIONS_GHC -Wno-orphans #-} -- Don't warn about partial functions: this is a test, so we'll see it fail. {-# OPTIONS_GHC -Wno-x-partial #-} @@ -22,6 +24,9 @@ import Bittide.Instances.Tests.ScatterGather (dutWithBinary) import qualified Prelude as P +createDomain vSystem{vName = "Slow", vPeriod = hzToPeriod 1000000} + +-- Simple sim :: IO () sim = putStr simResult @@ -108,7 +113,7 @@ simResultRingbufferSmoltcp = chr . fromIntegral <$> catMaybes uartStream where uartStream = sampleC def{timeoutAfter = 1_000_000} dutNoMM - dutNoMM :: (HasCallStack) => Circuit () (Df System (BitVector 8)) + dutNoMM :: (HasCallStack) => Circuit () (Df Slow (BitVector 8)) dutNoMM = circuit $ do mm <- ignoreMM uartTx <- From c8e911c558d3c4b4fe7e4959a70cc9ca951c4903 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Fri, 9 Jan 2026 12:00:40 +0100 Subject: [PATCH 21/70] Use timer peripheral in `ringbuffer_smoltcp_test` --- .../ringbuffer_smoltcp_test/src/main.rs | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs index 85a58396f..143d262ed 100644 --- a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs +++ b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs @@ -8,13 +8,13 @@ use bittide_hal::manual_additions::aligned_ringbuffer::{ find_alignment_offset, ReceiveRingbuffer, TransmitRingbuffer, }; +use bittide_hal::manual_additions::timer::Instant; use bittide_hal::scatter_gather_pe::DeviceInstances; use bittide_sys::smoltcp::ringbuffer::RingbufferDevice; use core::fmt::Write; use log::LevelFilter; use smoltcp::iface::{Config, Interface, SocketSet, SocketStorage}; use smoltcp::socket::tcp; -use smoltcp::time::Instant as SmolInstant; use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr}; #[cfg(not(test))] @@ -25,9 +25,14 @@ const INSTANCES: DeviceInstances = unsafe { DeviceInstances::new() }; const SERVER_PORT: u16 = 8080; const CLIENT_PORT: u16 = 49152; +fn to_smoltcp_instant(instant: Instant) -> smoltcp::time::Instant { + smoltcp::time::Instant::from_micros(instant.micros() as i64) +} + #[cfg_attr(not(test), entry)] fn main() -> ! { let mut uart = INSTANCES.uart; + let timer = INSTANCES.timer; writeln!(uart, "\n=== Ringbuffer smoltcp Loopback Test ===").ok(); @@ -36,6 +41,7 @@ fn main() -> ! { use bittide_sys::uart::log::LOGGER; let logger = &mut (*LOGGER.get()); logger.set_logger(uart.clone()); + logger.set_timer(INSTANCES.timer); logger.display_source = LevelFilter::Debug; log::set_logger_racy(logger).ok(); log::set_max_level_racy(LevelFilter::Trace); @@ -67,7 +73,7 @@ fn main() -> ! { let mac_addr = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]); let ip_addr = IpCidr::new(IpAddress::v4(127, 0, 0, 1), 8); // Loopback address let config = Config::new(mac_addr.into()); - let now = SmolInstant::from_millis(0); + let now = to_smoltcp_instant(timer.now()); let mut iface = Interface::new(config, &mut device, now); iface.update_ip_addrs(|addrs| { addrs.push(ip_addr).unwrap(); @@ -119,8 +125,8 @@ fn main() -> ! { // Step 7: Poll until connection established writeln!(uart, "Step 7: Waiting for connection...").ok(); let mut connection_established = false; - for tick in 0..100 { - let timestamp = SmolInstant::from_millis(tick * 10); + for _ in 0..100 { + let timestamp = to_smoltcp_instant(timer.now()); iface.poll(timestamp, &mut device, &mut sockets); let client = sockets.get::(client_handle); @@ -141,8 +147,8 @@ fn main() -> ! { // Step 8: Send data from client writeln!(uart, "Step 8: Sending test data...").ok(); let test_data = b"Hello from smoltcp!"; - for tick in 100..150 { - let timestamp = SmolInstant::from_millis(tick * 10); + for _ in 0..50 { + let timestamp = to_smoltcp_instant(timer.now()); iface.poll(timestamp, &mut device, &mut sockets); let client = sockets.get_mut::(client_handle); @@ -162,8 +168,8 @@ fn main() -> ! { let mut received_data = [0u8; 64]; let mut received_len = 0; - for tick in 150..200 { - let timestamp = SmolInstant::from_millis(tick * 10); + for _ in 0..50 { + let timestamp = to_smoltcp_instant(timer.now()); iface.poll(timestamp, &mut device, &mut sockets); let server = sockets.get_mut::(server_handle); From 2a1a88aea6aa71cb4c5f2c90781d72e17b2633e8 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Fri, 9 Jan 2026 12:07:29 +0100 Subject: [PATCH 22/70] Change `Device` implementation for ringbuffers to operate on `Ip` instead of `Ethernet` --- .../Bittide/Instances/Tests/ScatterGather.hs | 2 +- firmware-binaries/Cargo.lock | 16 + .../examples/smoltcp_client/Cargo.toml | 2 +- .../ringbuffer_smoltcp_test/Cargo.toml | 2 +- .../ringbuffer_smoltcp_test/src/main.rs | 6 +- firmware-support/Cargo.lock | 16 + firmware-support/bittide-hal/Cargo.toml | 2 +- firmware-support/bittide-sys/Cargo.toml | 3 +- .../bittide-sys/src/smoltcp/ringbuffer.rs | 286 ++++++++++-------- 9 files changed, 202 insertions(+), 133 deletions(-) diff --git a/bittide-instances/src/Bittide/Instances/Tests/ScatterGather.hs b/bittide-instances/src/Bittide/Instances/Tests/ScatterGather.hs index 90c2f4d9b..4444d17da 100644 --- a/bittide-instances/src/Bittide/Instances/Tests/ScatterGather.hs +++ b/bittide-instances/src/Bittide/Instances/Tests/ScatterGather.hs @@ -74,7 +74,7 @@ dutWithBinary binaryName = withBittideByteOrder $ circuit $ \mm -> do (uartTx, _uartStatus) <- uartInterfaceWb d16 d2 uartBytes -< (uartBus, uartRx) Fwd link <- gatherUnitWbC gatherConfig -< (wbGu, wbGuCal) scatterUnitWbC scatterConfig link -< (wbSu, wbSuCal) - _cnt <- timeWb -< timeBus + _cnt <- timeWb Nothing -< timeBus idC -< uartTx where peConfig binary = unsafePerformIO $ do diff --git a/firmware-binaries/Cargo.lock b/firmware-binaries/Cargo.lock index 61b160749..918a9cd2d 100644 --- a/firmware-binaries/Cargo.lock +++ b/firmware-binaries/Cargo.lock @@ -95,6 +95,7 @@ name = "bittide-sys" version = "0.1.0" dependencies = [ "bittide-hal", + "crc", "fdt", "heapless", "itertools", @@ -228,6 +229,21 @@ dependencies = [ "typewit", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "critical-section" version = "1.2.0" diff --git a/firmware-binaries/examples/smoltcp_client/Cargo.toml b/firmware-binaries/examples/smoltcp_client/Cargo.toml index af8ecfd04..bc160a03d 100644 --- a/firmware-binaries/examples/smoltcp_client/Cargo.toml +++ b/firmware-binaries/examples/smoltcp_client/Cargo.toml @@ -36,7 +36,7 @@ features = ["max_level_trace", "release_max_level_info"] [dependencies.smoltcp] version = "0.12.0" default-features = false -features = ["medium-ethernet", "proto-ipv4", "socket-tcp", "socket-dhcpv4"] +features = ["medium-ip", "medium-ethernet", "proto-ipv4", "socket-tcp", "socket-dhcpv4"] [build-dependencies] memmap-generate = { path = "../../../firmware-support/memmap-generate" } diff --git a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/Cargo.toml b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/Cargo.toml index 4b480216c..a8dc71393 100644 --- a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/Cargo.toml +++ b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/Cargo.toml @@ -19,7 +19,7 @@ ufmt = "0.2.0" [dependencies.smoltcp] version = "0.12.0" default-features = false -features = ["medium-ethernet", "proto-ipv4", "socket-tcp"] +features = ["medium-ip", "medium-ethernet", "proto-ipv4", "socket-tcp"] [dependencies.log] version = "0.4.21" diff --git a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs index 143d262ed..083a04d23 100644 --- a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs +++ b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs @@ -15,7 +15,7 @@ use core::fmt::Write; use log::LevelFilter; use smoltcp::iface::{Config, Interface, SocketSet, SocketStorage}; use smoltcp::socket::tcp; -use smoltcp::wire::{EthernetAddress, IpAddress, IpCidr}; +use smoltcp::wire::{HardwareAddress, IpAddress, IpCidr}; #[cfg(not(test))] use riscv_rt::entry; @@ -70,9 +70,9 @@ fn main() -> ! { // Step 3: Configure interface with static IP writeln!(uart, "Step 3: Configuring network interface...").ok(); - let mac_addr = EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]); + let hw_addr = HardwareAddress::Ip; let ip_addr = IpCidr::new(IpAddress::v4(127, 0, 0, 1), 8); // Loopback address - let config = Config::new(mac_addr.into()); + let config = Config::new(hw_addr); let now = to_smoltcp_instant(timer.now()); let mut iface = Interface::new(config, &mut device, now); iface.update_ip_addrs(|addrs| { diff --git a/firmware-support/Cargo.lock b/firmware-support/Cargo.lock index 24deb6ec4..fe8ef5a0d 100644 --- a/firmware-support/Cargo.lock +++ b/firmware-support/Cargo.lock @@ -89,6 +89,7 @@ name = "bittide-sys" version = "0.1.0" dependencies = [ "bittide-hal", + "crc", "fdt", "heapless", "itertools", @@ -135,6 +136,21 @@ dependencies = [ "typewit", ] +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + [[package]] name = "crc32fast" version = "1.5.0" diff --git a/firmware-support/bittide-hal/Cargo.toml b/firmware-support/bittide-hal/Cargo.toml index 3381e81c5..3e93274ad 100644 --- a/firmware-support/bittide-hal/Cargo.toml +++ b/firmware-support/bittide-hal/Cargo.toml @@ -19,7 +19,7 @@ ufmt = "0.2.0" git = "https://github.com/smoltcp-rs/smoltcp.git" rev = "dc08e0b42e668c331bb2b6f8d80016301d0efe03" default-features = false -features = ["log", "medium-ethernet", "proto-ipv4", "socket-tcp"] +features = ["log", "medium-ip", "medium-ethernet", "proto-ipv4", "socket-tcp"] [dependencies.subst_macros] git = "https://github.com/QBayLogic/subst_macros.git" diff --git a/firmware-support/bittide-sys/Cargo.toml b/firmware-support/bittide-sys/Cargo.toml index e58d240d0..0e6b02b3c 100644 --- a/firmware-support/bittide-sys/Cargo.toml +++ b/firmware-support/bittide-sys/Cargo.toml @@ -21,6 +21,7 @@ log = "0.4.21" ufmt = "0.2.0" bittide-hal = { path = "../bittide-hal" } itertools = { version = "0.14.0", default-features = false } +crc = { version = "3.0", default-features = false } [dependencies.heapless] version = "0.8" @@ -34,7 +35,7 @@ default-features = false [dependencies.smoltcp] version = "0.12.0" default-features = false -features = ["log", "medium-ethernet", "proto-ipv4", "socket-tcp"] +features = ["log", "medium-ip", "medium-ethernet", "proto-ipv4", "socket-tcp"] [dev-dependencies] proptest = "1.0" diff --git a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs index e84fd1501..5e8304e4c 100644 --- a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs +++ b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs @@ -5,26 +5,61 @@ //! smoltcp Device implementation for aligned ringbuffers. //! //! This module provides a `Device` implementation that uses scatter/gather units -//! as ringbuffers for point-to-point IP communication. The ringbuffers must +//! as ringbuffers for point-to-point communication. The ringbuffers must //! be aligned using the alignment protocol before use. //! -//! The device operates at the IP layer (not Ethernet) since the connections are -//! point-to-point and don't require MAC addressing or routing. +//! The device uses IP medium for point-to-point links. //! -//! The ringbuffer abstraction handles all offset and wrapping management internally, -//! so we simply read/write packets from the start each time. +//! # Packet Format +//! +//! Each packet consists of: +//! - Header (8 bytes): CRC32 (4 bytes LE) + sequence number (2 bytes LE) + length (2 bytes LE) +//! - Payload (variable, up to MTU) +//! +//! The CRC32 is calculated over sequence + length + payload (i.e., everything except the CRC itself). +//! +//! # Volatile Buffer Handling +//! +//! The receive ringbuffer is volatile - its contents can change at any time due to +//! incoming data. To safely handle this, we: +//! 1. Read the packet header to get CRC, sequence number, and length +//! 2. Copy the entire packet (header + payload) to a local buffer +//! 3. Verify the CRC32 over sequence + length + payload +//! 4. If CRC validates, extract payload and consume packet +//! 5. Track sequence numbers to detect repeated packets use bittide_hal::hals::scatter_gather_pe::devices::{GatherUnit, ScatterUnit}; use bittide_hal::manual_additions::aligned_ringbuffer::{ReceiveRingbuffer, TransmitRingbuffer}; -use log::{debug, trace}; +use crc::{Crc, CRC_32_ISCSI}; +use log::trace; use smoltcp::phy::{self, Device, DeviceCapabilities, Medium}; use smoltcp::time::Instant; -/// Number of times we can see the same packet ID before considering it stale -const STALE_PACKET_THRESHOLD: u32 = 3; +/// Size of packet header: CRC32 (4 bytes) + sequence (2 bytes) + length (2 bytes) +const PACKET_HEADER_SIZE: usize = 8; -/// Size of packet ID header in bytes (32-bit identifier) -const PACKET_ID_SIZE: usize = 4; +/// Minimum IP packet size (20 byte IPv4 header minimum) +const MIN_IP_PACKET_SIZE: usize = 20; + +/// CRC-32 (Castagnoli) instance for packet integrity checking. +/// Initialized at compile-time for zero-cost runtime usage. +const CRC: Crc = Crc::::new(&CRC_32_ISCSI); + +/// Verify packet integrity by checking the CRC32 in the header. +/// +/// Returns true if the CRC stored in the first 4 bytes matches the calculated +/// CRC over bytes[4..] (sequence + length + payload). Uses unaligned-safe reads. +/// +/// # Arguments +/// * `buffer` - Complete packet buffer including header and payload +fn is_valid(buffer: &[u8]) -> bool { + if buffer.len() < PACKET_HEADER_SIZE { + return false; + } + let stored_crc = u32::from_le_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]); + let calculated_crc = CRC.checksum(&buffer[4..]); + stored_crc == calculated_crc +} /// Device implementation for ringbuffer communication. /// @@ -32,17 +67,15 @@ const PACKET_ID_SIZE: usize = 4; /// scatter/gather units. The ringbuffers handle all internal state management. /// /// The MTU is automatically calculated from the minimum of the scatter and gather -/// buffer sizes (in bytes). +/// buffer sizes (in bytes), minus space for packet header (which includes CRC32). pub struct RingbufferDevice { rx_buffer: ReceiveRingbuffer, tx_buffer: TransmitRingbuffer, mtu: usize, - /// Next packet ID to send - tx_packet_id: u32, - /// Last packet ID we received - rx_packet_id: u32, - /// How many times we've seen the current RX packet ID - rx_packet_id_count: u32, + /// Last valid sequence number we saw (to detect repeated packets) + last_rx_seq: u16, + /// Transmit sequence number (incremented for each packet sent) + tx_seq_num: u16, } impl RingbufferDevice { @@ -52,18 +85,17 @@ impl RingbufferDevice { /// The MTU is calculated as the minimum of the RX and TX buffer sizes in bytes. pub fn new(rx_buffer: ReceiveRingbuffer, tx_buffer: TransmitRingbuffer) -> Self { // Calculate MTU from buffer sizes (each word is 8 bytes) - // Reserve space for packet ID header + // Reserve space for packet header (CRC is part of header) let rx_bytes = ScatterUnit::SCATTER_MEMORY_LEN * 8; let tx_bytes = GatherUnit::GATHER_MEMORY_LEN * 8; - let mtu = rx_bytes.min(tx_bytes) - PACKET_ID_SIZE; + let mtu = rx_bytes.min(tx_bytes) - PACKET_HEADER_SIZE; Self { rx_buffer, tx_buffer, mtu, - tx_packet_id: 1, // Has to be different from first rx packet id - rx_packet_id: 0, - rx_packet_id_count: 0, + last_rx_seq: u16::MAX, + tx_seq_num: 0, } } @@ -74,57 +106,99 @@ impl RingbufferDevice { } impl Device for RingbufferDevice { - type RxToken<'a> = RxToken<'a>; + type RxToken<'a> = RxToken; type TxToken<'a> = TxToken<'a>; fn capabilities(&self) -> DeviceCapabilities { let mut cap = DeviceCapabilities::default(); cap.max_transmission_unit = self.mtu; - cap.medium = Medium::Ethernet; + cap.medium = Medium::Ip; cap } fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { - // Read the packet ID (first 4 bytes) - let mut peek_buf: [[u8; 8]; 1] = [[0u8; 8]; 1]; - self.rx_buffer.read_slice(&mut peek_buf, 0); - - // Extract packet ID from first 4 bytes (little-endian) - let packet_id = u32::from_le_bytes(peek_buf[0][..4].try_into().unwrap()); + // Read packet header: CRC32 (4 bytes) + sequence (2 bytes) + length (2 bytes) + let mut header_buf: [[u8; 8]; 1] = [[0u8; 8]; 1]; + self.rx_buffer.read_slice(&mut header_buf, 0); + + // Extract CRC, sequence number, and length from header + let _stored_crc = u32::from_le_bytes([ + header_buf[0][0], + header_buf[0][1], + header_buf[0][2], + header_buf[0][3], + ]); + let seq_num = u16::from_le_bytes([header_buf[0][4], header_buf[0][5]]); + let packet_len = u16::from_le_bytes([header_buf[0][6], header_buf[0][7]]) as usize; + + // Check if this is the same packet we saw before (based on sequence number) + if seq_num == self.last_rx_seq { + trace!("Detected repeated packet with seq {}", seq_num); + return None; + } - // Check if this is the same packet we saw before - if packet_id == self.rx_packet_id { - self.rx_packet_id_count += 1; + // Validate packet length + if packet_len < MIN_IP_PACKET_SIZE || packet_len > self.mtu { trace!( - "Packet ID {} seen {} times", - packet_id, - self.rx_packet_id_count + "Invalid packet length: {} (must be {}-{})", + packet_len, + MIN_IP_PACKET_SIZE, + self.mtu ); + return None; + } + + // Calculate total packet size: header + payload + let total_len = PACKET_HEADER_SIZE + packet_len; + let num_words = total_len.div_ceil(8); + let mut buffer = [[0u8; 8]; ScatterUnit::SCATTER_MEMORY_LEN]; + let words = &mut buffer[..num_words]; - // If we've seen it too many times, consider it stale and ignore - if self.rx_packet_id_count >= STALE_PACKET_THRESHOLD { - trace!("Packet {} is stale, ignoring", packet_id); - return None; + // Copy entire packet from ringbuffer (header + payload) + self.rx_buffer.read_slice(words, 0); + + // Convert to contiguous byte buffer + let mut packet_buffer = [0u8; ScatterUnit::SCATTER_MEMORY_LEN * 8]; + let mut idx = 0; + for word in words.iter() { + for &byte in word.iter() { + packet_buffer[idx] = byte; + idx += 1; + if idx >= total_len { + break; + } + } + if idx >= total_len { + break; } - } else { - // New packet ID - trace!( - "New packet ID: {} (previous: {})", - packet_id, - self.rx_packet_id - ); - self.rx_packet_id = packet_id; - self.rx_packet_id_count = 1; } + // Validate CRC + if !is_valid(&packet_buffer[..total_len]) { + trace!("CRC validation failed for packet seq {}", seq_num); + return None; + } + + trace!( + "Valid packet: seq {}, payload {} bytes", + seq_num, + packet_len + ); + self.last_rx_seq = seq_num; + + // Extract payload (skip header, exclude CRC) + let mut payload = [0u8; ScatterUnit::SCATTER_MEMORY_LEN * 8]; + payload[..packet_len] + .copy_from_slice(&packet_buffer[PACKET_HEADER_SIZE..PACKET_HEADER_SIZE + packet_len]); + let rx = RxToken { - rx_buffer: &mut self.rx_buffer, - mtu: self.mtu, + buffer: payload, + length: packet_len, }; let tx = TxToken { tx_buffer: &mut self.tx_buffer, mtu: self.mtu, - packet_id: &mut self.tx_packet_id, + seq_num: &mut self.tx_seq_num, }; Some((rx, tx)) } @@ -133,61 +207,27 @@ impl Device for RingbufferDevice { Some(TxToken { tx_buffer: &mut self.tx_buffer, mtu: self.mtu, - packet_id: &mut self.tx_packet_id, + seq_num: &mut self.tx_seq_num, }) } } -/// Receive token for ringbuffer device -pub struct RxToken<'a> { - rx_buffer: &'a mut ReceiveRingbuffer, - mtu: usize, +/// Receive token for ringbuffer device. +/// +/// Contains a local copy of the packet payload that has been validated +/// against CRC32 corruption. +pub struct RxToken { + buffer: [u8; ScatterUnit::SCATTER_MEMORY_LEN * 8], + length: usize, } -impl phy::RxToken for RxToken<'_> { +impl phy::RxToken for RxToken { fn consume(self, f: F) -> R where F: FnOnce(&[u8]) -> R, { - // Read packet from ringbuffer starting at offset 0 - // First 4 bytes are packet ID, rest is actual packet data - let total_bytes = self.mtu + PACKET_ID_SIZE; - let num_words = total_bytes.div_ceil(8); - - // Allocate buffer based on actual MTU + packet ID - let max_words = ScatterUnit::SCATTER_MEMORY_LEN; - let mut buffer = [[0u8; 8]; ScatterUnit::SCATTER_MEMORY_LEN]; - let words = &mut buffer[..num_words.min(max_words)]; - - self.rx_buffer.read_slice(words, 0); - - // Convert to byte slice for processing - let mut bytes = [0u8; ScatterUnit::SCATTER_MEMORY_LEN * 8]; - let mut idx = 0; - for word in words.iter() { - for &byte in word.iter() { - bytes[idx] = byte; - idx += 1; - if idx >= total_bytes { - break; - } - } - if idx >= total_bytes { - break; - } - } - - // Extract packet ID for logging - let packet_id = u32::from_le_bytes(bytes[..4].try_into().unwrap()); - - // Process the packet (skip the 4-byte packet ID header) - let result = f(&bytes[PACKET_ID_SIZE..PACKET_ID_SIZE + self.mtu]); - trace!( - "Consumed packet {} ({} bytes from RX buffer)", - packet_id, - self.mtu - ); - result + trace!("Consuming validated packet ({} bytes)", self.length); + f(&self.buffer[..self.length]) } } @@ -195,7 +235,7 @@ impl phy::RxToken for RxToken<'_> { pub struct TxToken<'a> { tx_buffer: &'a mut TransmitRingbuffer, mtu: usize, - packet_id: &'a mut u32, + seq_num: &'a mut u16, } impl phy::TxToken for TxToken<'_> { @@ -203,10 +243,6 @@ impl phy::TxToken for TxToken<'_> { where F: FnOnce(&mut [u8]) -> R, { - // Ensure the packet doesn't exceed MTU - if len > self.mtu { - debug!("Packet length {} exceeds MTU {}", len, self.mtu); - } assert!( len <= self.mtu, "Packet length {} exceeds MTU {}", @@ -214,47 +250,47 @@ impl phy::TxToken for TxToken<'_> { self.mtu ); - // Prepare buffer: packet ID (4 bytes) + packet data - let mut bytes = [0u8; GatherUnit::GATHER_MEMORY_LEN * 8]; + // Prepare buffer: header + payload + let mut buffer = [0u8; GatherUnit::GATHER_MEMORY_LEN * 8]; + + // Header format: CRC32 (4 bytes) + sequence (2 bytes) + length (2 bytes) + // Checksum added later + buffer[4..6].copy_from_slice(&(self.seq_num.to_le_bytes())); + buffer[6..8].copy_from_slice(&(len as u16).to_le_bytes()); - // Write packet ID in first 4 bytes (little-endian) - let packet_id_bytes = self.packet_id.to_le_bytes(); - bytes[0] = packet_id_bytes[0]; - bytes[1] = packet_id_bytes[1]; - bytes[2] = packet_id_bytes[2]; - bytes[3] = packet_id_bytes[3]; + // Let smoltcp fill the packet data + let result = f(&mut buffer[PACKET_HEADER_SIZE..PACKET_HEADER_SIZE + len]); + // Calculate total length and seal packet with CRC in header + let total_len = PACKET_HEADER_SIZE + len; - // Let smoltcp fill the packet data after the ID - let packet = &mut bytes[PACKET_ID_SIZE..PACKET_ID_SIZE + len]; - let result = f(packet); + let crc = CRC.checksum(&buffer[4..total_len]); + buffer[0..4].copy_from_slice(&(crc.to_le_bytes())); - // Convert to word array for ringbuffer (packet ID + data) - let total_len = PACKET_ID_SIZE + len; + // Convert to word array for ringbuffer let num_words = total_len.div_ceil(8); let max_words = GatherUnit::GATHER_MEMORY_LEN; - let mut buffer = [[0u8; 8]; GatherUnit::GATHER_MEMORY_LEN]; + let mut word_buffer = [[0u8; 8]; GatherUnit::GATHER_MEMORY_LEN]; - for (i, chunk) in bytes[..total_len].chunks(8).enumerate() { + for (i, chunk) in buffer[..total_len].chunks(8).enumerate() { if i >= max_words { break; } for (j, &byte) in chunk.iter().enumerate() { - buffer[i][j] = byte; + word_buffer[i][j] = byte; } } // Write to ringbuffer starting at offset 0 - let words = &buffer[..num_words.min(max_words)]; + let words = &word_buffer[..num_words.min(max_words)]; self.tx_buffer.write_slice(words, 0); trace!( - "Transmitted packet {} ({} bytes to TX buffer)", - *self.packet_id, - len + "Transmitted packet: checksum {}, seq {}, total length {}", + crc, + self.seq_num, + total_len, ); - - // Increment packet ID for next transmission - *self.packet_id = self.packet_id.wrapping_add(1); + *self.seq_num = self.seq_num.wrapping_add(1); result } From d1cf2cdd6f3c6a835e5ae1a6ca0419aec129e038 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Fri, 9 Jan 2026 12:30:51 +0100 Subject: [PATCH 23/70] Optimize ringbuffer interactions in smoltcp device We want to directly copy the packet instead of first changing its shape manually --- .../bittide-sys/src/smoltcp/ringbuffer.rs | 79 ++++++++----------- 1 file changed, 31 insertions(+), 48 deletions(-) diff --git a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs index 5e8304e4c..1579f3ccf 100644 --- a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs +++ b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs @@ -121,15 +121,11 @@ impl Device for RingbufferDevice { let mut header_buf: [[u8; 8]; 1] = [[0u8; 8]; 1]; self.rx_buffer.read_slice(&mut header_buf, 0); - // Extract CRC, sequence number, and length from header - let _stored_crc = u32::from_le_bytes([ - header_buf[0][0], - header_buf[0][1], - header_buf[0][2], - header_buf[0][3], - ]); - let seq_num = u16::from_le_bytes([header_buf[0][4], header_buf[0][5]]); - let packet_len = u16::from_le_bytes([header_buf[0][6], header_buf[0][7]]) as usize; + // Extract CRC, sequence number, and length from header using direct pointer reads + let header_ptr = header_buf[0].as_ptr(); + let _stored_crc = unsafe { (header_ptr as *const u32).read_unaligned() }; + let seq_num = unsafe { (header_ptr.add(4) as *const u16).read_unaligned() }; + let packet_len = unsafe { (header_ptr.add(6) as *const u16).read_unaligned() } as usize; // Check if this is the same packet we saw before (based on sequence number) if seq_num == self.last_rx_seq { @@ -151,29 +147,19 @@ impl Device for RingbufferDevice { // Calculate total packet size: header + payload let total_len = PACKET_HEADER_SIZE + packet_len; let num_words = total_len.div_ceil(8); - let mut buffer = [[0u8; 8]; ScatterUnit::SCATTER_MEMORY_LEN]; - let words = &mut buffer[..num_words]; - // Copy entire packet from ringbuffer (header + payload) - self.rx_buffer.read_slice(words, 0); - - // Convert to contiguous byte buffer + // Allocate aligned buffer for reading from ringbuffer + // Use a flat byte buffer and cast it to [[u8; 8]] for the API let mut packet_buffer = [0u8; ScatterUnit::SCATTER_MEMORY_LEN * 8]; - let mut idx = 0; - for word in words.iter() { - for &byte in word.iter() { - packet_buffer[idx] = byte; - idx += 1; - if idx >= total_len { - break; - } - } - if idx >= total_len { - break; - } - } - // Validate CRC + let word_slice = unsafe { + core::slice::from_raw_parts_mut(packet_buffer.as_mut_ptr() as *mut [u8; 8], num_words) + }; + + // Copy entire packet from ringbuffer (header + payload) + self.rx_buffer.read_slice(word_slice, 0); + + // Validate CRC (packet_buffer is already in the right format) if !is_valid(&packet_buffer[..total_len]) { trace!("CRC validation failed for packet seq {}", seq_num); return None; @@ -253,36 +239,33 @@ impl phy::TxToken for TxToken<'_> { // Prepare buffer: header + payload let mut buffer = [0u8; GatherUnit::GATHER_MEMORY_LEN * 8]; + // Write header fields using direct pointer writes // Header format: CRC32 (4 bytes) + sequence (2 bytes) + length (2 bytes) - // Checksum added later - buffer[4..6].copy_from_slice(&(self.seq_num.to_le_bytes())); - buffer[6..8].copy_from_slice(&(len as u16).to_le_bytes()); + let header_ptr = buffer.as_mut_ptr(); + unsafe { + // Write sequence and length (CRC written later after payload) + (header_ptr.add(4) as *mut u16).write_unaligned(self.seq_num.to_le()); + (header_ptr.add(6) as *mut u16).write_unaligned((len as u16).to_le()); + } // Let smoltcp fill the packet data let result = f(&mut buffer[PACKET_HEADER_SIZE..PACKET_HEADER_SIZE + len]); + // Calculate total length and seal packet with CRC in header let total_len = PACKET_HEADER_SIZE + len; - let crc = CRC.checksum(&buffer[4..total_len]); - buffer[0..4].copy_from_slice(&(crc.to_le_bytes())); + unsafe { + (header_ptr as *mut u32).write_unaligned(crc.to_le()); + } - // Convert to word array for ringbuffer + // Convert to word array for ringbuffer using pointer cast let num_words = total_len.div_ceil(8); - let max_words = GatherUnit::GATHER_MEMORY_LEN; - let mut word_buffer = [[0u8; 8]; GatherUnit::GATHER_MEMORY_LEN]; - - for (i, chunk) in buffer[..total_len].chunks(8).enumerate() { - if i >= max_words { - break; - } - for (j, &byte) in chunk.iter().enumerate() { - word_buffer[i][j] = byte; - } - } + // The buffer is correctly sized and alignment is maintained. + let word_slice = + unsafe { core::slice::from_raw_parts(buffer.as_ptr() as *const [u8; 8], num_words) }; // Write to ringbuffer starting at offset 0 - let words = &word_buffer[..num_words.min(max_words)]; - self.tx_buffer.write_slice(words, 0); + self.tx_buffer.write_slice(word_slice, 0); trace!( "Transmitted packet: checksum {}, seq {}, total length {}", From e8d909c21ae6be3d1f015ebd7a2676bed655e6e3 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Fri, 9 Jan 2026 12:48:31 +0100 Subject: [PATCH 24/70] Simplify ringbuffer_smoltcp test --- .../ringbuffer_smoltcp_test/src/main.rs | 128 ++++++++---------- 1 file changed, 57 insertions(+), 71 deletions(-) diff --git a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs index 083a04d23..dccdf3f9b 100644 --- a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs +++ b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs @@ -122,103 +122,91 @@ fn main() -> ! { .unwrap(); writeln!(uart, " Connection initiated").ok(); - // Step 7: Poll until connection established - writeln!(uart, "Step 7: Waiting for connection...").ok(); + // Main event loop + writeln!(uart, "Step 7: Running main event loop...").ok(); + let test_data = b"Hello from smoltcp!"; + let mut received_data = [0u8; 64]; + let mut received_len = 0; + let mut data_sent = false; let mut connection_established = false; - for _ in 0..100 { + + for _ in 0..200 { let timestamp = to_smoltcp_instant(timer.now()); iface.poll(timestamp, &mut device, &mut sockets); + // Check if connection is established let client = sockets.get::(client_handle); if client.is_active() && !connection_established { writeln!(uart, " Connection established!").ok(); connection_established = true; - break; - } - } - - if !connection_established { - writeln!(uart, " FAILURE: Connection timeout!").ok(); - loop { - unsafe { riscv::asm::wfi() }; } - } - - // Step 8: Send data from client - writeln!(uart, "Step 8: Sending test data...").ok(); - let test_data = b"Hello from smoltcp!"; - for _ in 0..50 { - let timestamp = to_smoltcp_instant(timer.now()); - iface.poll(timestamp, &mut device, &mut sockets); - let client = sockets.get_mut::(client_handle); - if client.can_send() { - match client.send_slice(test_data) { - Ok(sent) => { + // Send data from client + if client.is_active() && !data_sent { + let client = sockets.get_mut::(client_handle); + if client.can_send() { + if let Ok(sent) = client.send_slice(test_data) { writeln!(uart, " Sent {} bytes", sent).ok(); - break; + data_sent = true; } - Err(_) => continue, } } - } - - // Step 9: Receive data on server - writeln!(uart, "Step 9: Receiving data on server...").ok(); - let mut received_data = [0u8; 64]; - let mut received_len = 0; - - for _ in 0..50 { - let timestamp = to_smoltcp_instant(timer.now()); - iface.poll(timestamp, &mut device, &mut sockets); - let server = sockets.get_mut::(server_handle); - if server.can_recv() { - match server.recv_slice(&mut received_data) { - Ok(len) => { + // Receive data on server + if data_sent && received_len == 0 { + let server = sockets.get_mut::(server_handle); + if server.can_recv() { + if let Ok(len) = server.recv_slice(&mut received_data) { received_len = len; writeln!(uart, " Received {} bytes", len).ok(); break; } - Err(_) => continue, } } } - // Step 10: Verify data - writeln!(uart, "Step 10: Verifying data...").ok(); - let received_slice = &received_data[..received_len]; - - if received_len == test_data.len() && received_slice == test_data { - writeln!(uart, " SUCCESS: Data matches!").ok(); - writeln!( - uart, - " Sent: {:?}", - core::str::from_utf8(test_data).unwrap() - ) - .ok(); - writeln!( - uart, - " Received: {:?}", - core::str::from_utf8(received_slice).unwrap() - ) - .ok(); + // Verify results + writeln!(uart, "Step 8: Verifying results...").ok(); + + if !connection_established { + writeln!(uart, " FAILURE: Connection timeout!").ok(); + } else if !data_sent { + writeln!(uart, " FAILURE: Failed to send data!").ok(); + } else if received_len == 0 { + writeln!(uart, " FAILURE: Failed to receive data!").ok(); } else { - writeln!(uart, " FAILURE: Data mismatch!").ok(); - writeln!( - uart, - " Expected {} bytes: {:?}", - test_data.len(), - test_data - ) - .ok(); - writeln!(uart, " Got {} bytes: {:?}", received_len, received_slice).ok(); + let received_slice = &received_data[..received_len]; + if received_len == test_data.len() && received_slice == test_data { + writeln!(uart, " SUCCESS: Data matches!").ok(); + writeln!( + uart, + " Sent: {:?}", + core::str::from_utf8(test_data).unwrap() + ) + .ok(); + writeln!( + uart, + " Received: {:?}", + core::str::from_utf8(received_slice).unwrap() + ) + .ok(); + } else { + writeln!(uart, " FAILURE: Data mismatch!").ok(); + writeln!( + uart, + " Expected {} bytes: {:?}", + test_data.len(), + test_data + ) + .ok(); + writeln!(uart, " Got {} bytes: {:?}", received_len, received_slice).ok(); + } } writeln!(uart, "\n=== Test Complete ===").ok(); loop { - unsafe { riscv::asm::wfi() }; + continue; } } @@ -227,7 +215,5 @@ fn main() -> ! { fn panic(info: &core::panic::PanicInfo) -> ! { let mut uart = INSTANCES.uart; writeln!(uart, "PANIC: {}", info).ok(); - loop { - unsafe { riscv::asm::wfi() }; - } + loop {} } From 7d1d2a812e15b26c2c2740d6127bce8d1efe206f Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Thu, 5 Mar 2026 12:51:33 +0100 Subject: [PATCH 25/70] Actually test out of range in `testIncreaseBuswidth` --- bittide/tests/Tests/Clash/Protocols/Wishbone/Extra.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittide/tests/Tests/Clash/Protocols/Wishbone/Extra.hs b/bittide/tests/Tests/Clash/Protocols/Wishbone/Extra.hs index 84db21f95..b9d7e536c 100644 --- a/bittide/tests/Tests/Clash/Protocols/Wishbone/Extra.hs +++ b/bittide/tests/Tests/Clash/Protocols/Wishbone/Extra.hs @@ -166,7 +166,7 @@ testIncreaseBuswidth power = property $ do -- Depth is half the number of addresses to also tests error on out-of-range accesses. depth = SNat @(2 ^ (AddressWidth - 2)) - lastAddress = snatToNum $ predSNat depth + lastAddress = snatToNum $ mulSNat d2 depth lastAddressSmall = lastAddress * (natToNum @(2 ^ power)) genAddr = Gen.integral (Range.linear 0 lastAddressSmall) genInputs = Gen.list (Range.linear 1 10) (genWishboneTransfer genAddr) From 0f62f04def9037a785652e660b654b8ae58750a3 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Thu, 5 Mar 2026 12:52:21 +0100 Subject: [PATCH 26/70] Refactor ringbuffer tests --- .../src/Bittide/Instances/Tests/Ringbuffer.hs | 65 +++- .../tests/Wishbone/Ringbuffer.hs | 43 ++- .../tests/Wishbone/ScatterGather.hs | 52 --- .../aligned_ringbuffer_test/build.rs | 4 +- .../aligned_ringbuffer_test/src/main.rs | 302 +++++------------- .../c_scatter_gather_test/src/main.c | 1 + .../sim-tests/c_timer_wb/src/main.c | 1 + .../ringbuffer_smoltcp_test/build.rs | 2 +- .../ringbuffer_smoltcp_test/src/main.rs | 28 +- .../sim-tests/ringbuffer_test/src/main.rs | 66 ++-- .../sim-tests/scatter_gather_test/src/main.rs | 1 + .../include/bittide_ring_transmit.h | 2 +- .../manual_additions/aligned_ringbuffer.rs | 204 ------------ .../bittide-hal/src/manual_additions/mod.rs | 3 +- .../manual_additions/ringbuffer_test/mod.rs | 4 + .../ringbuffer_test/ringbuffers.rs | 259 +++++++++++++++ .../bittide-sys/src/smoltcp/ringbuffer.rs | 29 +- 17 files changed, 505 insertions(+), 561 deletions(-) delete mode 100644 firmware-support/bittide-hal/src/manual_additions/aligned_ringbuffer.rs create mode 100644 firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/mod.rs create mode 100644 firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/ringbuffers.rs diff --git a/bittide-instances/src/Bittide/Instances/Tests/Ringbuffer.hs b/bittide-instances/src/Bittide/Instances/Tests/Ringbuffer.hs index 83cb1d6c6..16264a715 100644 --- a/bittide-instances/src/Bittide/Instances/Tests/Ringbuffer.hs +++ b/bittide-instances/src/Bittide/Instances/Tests/Ringbuffer.hs @@ -5,9 +5,10 @@ module Bittide.Instances.Tests.Ringbuffer where -import Clash.Explicit.Prelude +import Clash.Explicit.Prelude hiding (delayN) import Clash.Prelude ( HiddenClockResetEnable, + delayN, exposeEnable, hasClock, withClockResetEnable, @@ -43,14 +44,15 @@ dutMM :: (HasCallStack) => Protocols.MemoryMap.MemoryMap dutMM = (\(SimOnly mm, _) -> mm) $ withClockResetEnable @System clockGen (resetGenN d2) enableGen - $ toSignals (dutWithBinary "") ((), pure $ deepErrorX "memoryMap") + $ toSignals (dutWithBinary d0 "") ((), pure $ deepErrorX "memoryMap") --- | Parameterized DUT that loads a specific firmware binary. +-- | Parameterized DUT that loads a specific firmware binary with configurable latency. dutWithBinary :: - (HasCallStack, HiddenClockResetEnable dom, 1 <= DomainPeriod dom) => + (HasCallStack, HiddenClockResetEnable dom, 1 <= DomainPeriod dom, KnownNat latency) => + SNat latency -> String -> Circuit (ToConstBwd Mm) (Df dom (BitVector 8)) -dutWithBinary binaryName = withBittideByteOrder $ circuit $ \mm -> do +dutWithBinary latency binaryName = withBittideByteOrder $ circuit $ \mm -> do (uartRx, jtagIdle) <- idleSource [uartBus, wbTx, wbRx, timeBus] <- processingElement NoDumpVcd (peConfig binaryName) -< (mm, jtagIdle) @@ -58,8 +60,10 @@ dutWithBinary binaryName = withBittideByteOrder $ circuit $ \mm -> do txOut <- transmitRingbufferWb (exposeEnable $ blockRamByteAddressable (Vec (repeat 0))) memDepth -< wbTx + -- Add configurable latency between TX and RX ringbuffers + txOutDelayed <- applyC (toSignal . delayN latency 0 . fromSignal) id -< txOut receiveRingbufferWb (\ena -> blockRam hasClock ena (replicate memDepth 0)) memDepth - -< (wbRx, txOut) + -< (wbRx, txOutDelayed) _cnt <- timeWb Nothing -< timeBus idC -< uartTx where @@ -94,18 +98,53 @@ type DMemWords = DivRU (256 * 1024) 4 -- Ringbuffer test simulation simRingbuffer :: IO () -simRingbuffer = putStr simResultRingbuffer +simRingbuffer = putStr $ simResultRingbuffer d0 -simResultRingbuffer :: (HasCallStack) => String -simResultRingbuffer = chr . fromIntegral <$> catMaybes uartStream +simResultRingbuffer :: forall latency. (HasCallStack, KnownNat latency) => SNat latency -> String +simResultRingbuffer lat = chr . fromIntegral <$> catMaybes uartStream where - uartStream = sampleC def{timeoutAfter = 250_000} dutNoMM + uartStream = sampleC def{timeoutAfter = 500_000} (dutNoMM lat) - dutNoMM :: (HasCallStack) => Circuit () (Df System (BitVector 8)) - dutNoMM = circuit $ do + dutNoMM :: (HasCallStack, KnownNat n) => SNat n -> Circuit () (Df System (BitVector 8)) + dutNoMM latency = circuit $ do mm <- ignoreMM uartTx <- withClockResetEnable clockGen (resetGenN d2) enableGen - $ (dutWithBinary "ringbuffer_test") + $ (dutWithBinary latency "ringbuffer_test") + -< mm + idC -< uartTx + +simSmolTcp :: IO () +simSmolTcp = putStr $ simResultSmolTcp d0 + +simResultSmolTcp :: forall latency. (HasCallStack, KnownNat latency) => SNat latency -> String +simResultSmolTcp lat = chr . fromIntegral <$> catMaybes uartStream + where + uartStream = sampleC def{timeoutAfter = 500_000} (dutNoMM lat) + + dutNoMM :: (HasCallStack, KnownNat n) => SNat n -> Circuit () (Df System (BitVector 8)) + dutNoMM latency = circuit $ do + mm <- ignoreMM + uartTx <- + withClockResetEnable clockGen (resetGenN d2) enableGen + $ (dutWithBinary latency "ringbuffer_smoltcp_test") + -< mm + idC -< uartTx + +simAlignedRingbuffer :: IO () +simAlignedRingbuffer = putStr $ simResultAlignedRingbuffer d0 + +simResultAlignedRingbuffer :: + forall latency. (HasCallStack, KnownNat latency) => SNat latency -> String +simResultAlignedRingbuffer lat = chr . fromIntegral <$> catMaybes uartStream + where + uartStream = sampleC def{timeoutAfter = 250_000} (dutNoMM lat) + + dutNoMM :: (HasCallStack, KnownNat n) => SNat n -> Circuit () (Df System (BitVector 8)) + dutNoMM latency = circuit $ do + mm <- ignoreMM + uartTx <- + withClockResetEnable clockGen (resetGenN d2) enableGen + $ (dutWithBinary latency "aligned_ringbuffer_test") -< mm idC -< uartTx diff --git a/bittide-instances/tests/Wishbone/Ringbuffer.hs b/bittide-instances/tests/Wishbone/Ringbuffer.hs index 796553fb1..599c887fd 100644 --- a/bittide-instances/tests/Wishbone/Ringbuffer.hs +++ b/bittide-instances/tests/Wishbone/Ringbuffer.hs @@ -10,20 +10,45 @@ module Wishbone.Ringbuffer where import Clash.Explicit.Prelude +import Control.Monad.IO.Class (liftIO) import Data.List (isInfixOf) +import Data.Proxy (Proxy (..)) +import qualified Hedgehog as H +import qualified Hedgehog.Gen as Gen +import qualified Hedgehog.Range as Range import Test.Tasty -import Test.Tasty.HUnit +import Test.Tasty.Hedgehog (testProperty) import Test.Tasty.TH -import Bittide.Instances.Tests.Ringbuffer (simResultRingbuffer) +import Bittide.Instances.Tests.Ringbuffer (simResultAlignedRingbuffer, simResultRingbuffer) -case_ringbuffer_test :: Assertion -case_ringbuffer_test = do - assertBool - msg - ("TEST PASSED" `isInfixOf` simResultRingbuffer) - where - msg = "Received the following from the CPU over UART:\n" <> simResultRingbuffer +prop_ringbuffer_test :: H.Property +prop_ringbuffer_test = H.withTests 1 $ H.property $ do + latency <- H.forAll $ Gen.integral (Range.constant 0 100) + liftIO $ putStrLn $ "Testing ringbuffer_test with latency " <> show latency <> " cycles" + let result = case someNatVal (fromInteger latency) of + Just (SomeNat (_ :: Proxy n)) -> simResultRingbuffer (SNat @n) + Nothing -> error $ "Invalid latency value: " <> show latency + H.annotate + $ "Running ringbuffer_test with latency " + <> show latency + <> " cycles\nReceived the following from the CPU over UART:\n" + <> result + H.assert ("TEST PASSED" `isInfixOf` result) + +prop_aligned_ringbuffer_test :: H.Property +prop_aligned_ringbuffer_test = H.withTests 1 $ H.property $ do + latency <- H.forAll $ Gen.integral (Range.constant 0 100) + liftIO $ putStrLn $ "Testing ringbuffer_test with latency " <> show latency <> " cycles" + let result = case someNatVal (fromInteger latency) of + Just (SomeNat (_ :: Proxy n)) -> simResultAlignedRingbuffer (SNat @n) + Nothing -> error $ "Invalid latency value: " <> show latency + H.annotate + $ "Running aligned_ringbuffer_test with latency " + <> show latency + <> " cycles\nReceived the following from the CPU over UART:\n" + <> result + H.assert ("ALL TESTS PASSED" `isInfixOf` result) tests :: TestTree tests = $(testGroupGenerator) diff --git a/bittide-instances/tests/Wishbone/ScatterGather.hs b/bittide-instances/tests/Wishbone/ScatterGather.hs index 80b14daf9..2ed15c771 100644 --- a/bittide-instances/tests/Wishbone/ScatterGather.hs +++ b/bittide-instances/tests/Wishbone/ScatterGather.hs @@ -78,57 +78,5 @@ 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 - --- Ringbuffer smoltcp test simulation -simRingbufferSmoltcp :: IO () -simRingbufferSmoltcp = putStr simResultRingbufferSmoltcp - -simResultRingbufferSmoltcp :: (HasCallStack) => String -simResultRingbufferSmoltcp = chr . fromIntegral <$> catMaybes uartStream - where - uartStream = sampleC def{timeoutAfter = 1_000_000} dutNoMM - - dutNoMM :: (HasCallStack) => Circuit () (Df Slow (BitVector 8)) - dutNoMM = circuit $ do - mm <- ignoreMM - uartTx <- - withClockResetEnable clockGen (resetGenN d2) enableGen - $ (dutWithBinary "ringbuffer_smoltcp_test") - -< mm - idC -< uartTx - -case_ringbuffer_smoltcp_test :: Assertion -case_ringbuffer_smoltcp_test = do - assertBool - msg - ("SUCCESS: Data matches!" `isInfixOf` simResultRingbufferSmoltcp) - where - msg = "Received the following from the CPU over UART:\n" <> simResultRingbufferSmoltcp - tests :: TestTree tests = $(testGroupGenerator) diff --git a/firmware-binaries/sim-tests/aligned_ringbuffer_test/build.rs b/firmware-binaries/sim-tests/aligned_ringbuffer_test/build.rs index 70193e4cc..e72dc82e9 100644 --- a/firmware-binaries/sim-tests/aligned_ringbuffer_test/build.rs +++ b/firmware-binaries/sim-tests/aligned_ringbuffer_test/build.rs @@ -1,10 +1,10 @@ -// SPDX-FileCopyrightText: 2025 Google LLC +// SPDX-FileCopyrightText: 2026 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"); + standard_memmap_build("RingbufferTest.json", "DataMemory", "InstructionMemory"); println!("cargo:rerun-if-changed=build.rs"); } diff --git a/firmware-binaries/sim-tests/aligned_ringbuffer_test/src/main.rs b/firmware-binaries/sim-tests/aligned_ringbuffer_test/src/main.rs index dd6e2f877..73794930c 100644 --- a/firmware-binaries/sim-tests/aligned_ringbuffer_test/src/main.rs +++ b/firmware-binaries/sim-tests/aligned_ringbuffer_test/src/main.rs @@ -1,14 +1,12 @@ -// SPDX-FileCopyrightText: 2025 Google LLC +// SPDX-FileCopyrightText: 2026 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, + manual_additions::{ringbuffer_test::ringbuffers::AlignedReceiveBuffer, timer::Duration}, + ringbuffer_test::{devices::TransmitRingbuffer, DeviceInstances}, }; use core::fmt::Write; #[cfg(not(test))] @@ -16,280 +14,142 @@ 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; + let timer = INSTANCES.timer; 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); + let tx_ringbuffer = INSTANCES.transmit_ringbuffer; + let rx_ringbuffer = INSTANCES.receive_ringbuffer; - 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); + // Step 1: Perform alignment procedure + writeln!(uart, "\n--- Step 1: Alignment Discovery ---").unwrap(); + writeln!(uart, "Running alignment procedure...").unwrap(); - 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(); - } + // Create a copy of rx_ringbuffer for alignment, then get back the aligned buffer + let rx_copy = + unsafe { bittide_hal::ringbuffer_test::devices::ReceiveRingbuffer::new(rx_ringbuffer.0) }; + let mut rx_aligned = AlignedReceiveBuffer::new(rx_copy); + rx_aligned.align(&tx_ringbuffer); + let alignment_offset = rx_aligned + .get_alignment_offset() + .expect("Failed to discover alignment offset"); - // 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" + "SUCCESS: Discovered alignment offset = {} words ({} bytes)", + alignment_offset, + alignment_offset * 8 ) .unwrap(); - rx_ringbuffer.set_offset(discovered_offset); + // Step 2: Test aligned transmission + writeln!(uart, "\n--- Step 2: Aligned Transmission Test ---").unwrap(); + writeln!(uart, "Writing pattern and verifying with alignment").unwrap(); + // Clear TX buffer 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()); + // Create pattern: each word = frame number + let tx_pattern: [[u8; 8]; TransmitRingbuffer::DATA_LEN] = + core::array::from_fn(|i| (0x1000 + i as u64).to_le_bytes()); writeln!( uart, "Writing {} words to TX at offset 0", - ALIGNED_TEST_SIZE + TransmitRingbuffer::DATA_LEN ) .unwrap(); - tx_ringbuffer.write_slice(&aligned_pattern, 0); + tx_ringbuffer.write_slice(&tx_pattern, 0); - let mut rx_data: [[u8; 8]; ALIGNED_TEST_SIZE] = [[0u8; 8]; ALIGNED_TEST_SIZE]; + // Wait for data to propagate + timer.wait(Duration::from_cycles( + TransmitRingbuffer::DATA_LEN as u32, + timer.frequency(), + )); + + // Read using aligned buffer + let mut rx_data: [[u8; 8]; TransmitRingbuffer::DATA_LEN] = + [[0u8; 8]; TransmitRingbuffer::DATA_LEN]; writeln!( uart, - "Reading {} words from RX at offset 0", - ALIGNED_TEST_SIZE + "Reading {} words from RX at offset 0 (with alignment)", + TransmitRingbuffer::DATA_LEN ) .unwrap(); - rx_ringbuffer.read_slice(&mut rx_data, 0); + rx_aligned.read_slice(&mut rx_data, 0); - let aligned_matches = aligned_pattern - .iter() - .zip(rx_data.iter()) - .filter(|(a, b)| a == b) - .count(); + // Verify all words match + let mut all_match = true; + let mut first_mismatch = None; - 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(); - } + for (i, (expected, actual)) in tx_pattern.iter().zip(rx_data.iter()).enumerate() { + if expected != actual && first_mismatch.is_none() { + first_mismatch = Some((i, expected, actual)); + all_match = false; } } - // 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 { + if all_match { writeln!( uart, - "SUCCESS: All {} words matched across wrap boundary!", - WRAP_SIZE + "*** TEST PASSED: All {} words matched with alignment! ***", + TransmitRingbuffer::DATA_LEN ) .unwrap(); } else { + writeln!(uart, "\n*** TEST FAILED: Data corruption detected ***").unwrap(); + + if let Some((idx, expected, actual)) = first_mismatch { + writeln!( + uart, + "First mismatch at word {}: expected {:02x?}, got {:02x?}", + idx, expected, actual + ) + .unwrap(); + } + + writeln!(uart, "\nTX pattern written:").unwrap(); + for (i, word) in tx_pattern.iter().enumerate() { + write!(uart, " TX[{:2}]: ", i).unwrap(); + for byte in word { + write!(uart, "{:02x} ", byte).unwrap(); + } + writeln!(uart).unwrap(); + } + writeln!( uart, - "FAILURE: Only {}/{} words matched", - wrap_matches, WRAP_SIZE + "\nRX pattern received (with alignment offset {}):", + alignment_offset ) .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(); + for (i, (expected, actual)) in tx_pattern.iter().zip(rx_data.iter()).enumerate() { + write!(uart, " RX[{:2}]: ", i).unwrap(); + for byte in actual { + write!(uart, "{:02x} ", byte).unwrap(); } + writeln!(uart, "{}", if expected == actual { "✓" } else { "✗" }).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" - } + if all_match { "PASS" } else { "FAIL" } ) .unwrap(); - let all_passed = found && aligned_matches == ALIGNED_TEST_SIZE && wrap_matches == WRAP_SIZE; - if all_passed { + if all_match { writeln!(uart, "\n*** ALL TESTS PASSED ***").unwrap(); } else { writeln!(uart, "\n*** SOME TESTS FAILED ***").unwrap(); } + writeln!(uart, "Test done").unwrap(); loop { continue; diff --git a/firmware-binaries/sim-tests/c_scatter_gather_test/src/main.c b/firmware-binaries/sim-tests/c_scatter_gather_test/src/main.c index 28a7e8087..bcc1ab8f5 100644 --- a/firmware-binaries/sim-tests/c_scatter_gather_test/src/main.c +++ b/firmware-binaries/sim-tests/c_scatter_gather_test/src/main.c @@ -152,6 +152,7 @@ void c_main(void) { } else { uart_puts(uart, "Scatter/Gather HAL tests FAILED\n"); } + uart_puts(uart, "Test done\n"); while (1) { } diff --git a/firmware-binaries/sim-tests/c_timer_wb/src/main.c b/firmware-binaries/sim-tests/c_timer_wb/src/main.c index 5c66b9d44..480482de1 100644 --- a/firmware-binaries/sim-tests/c_timer_wb/src/main.c +++ b/firmware-binaries/sim-tests/c_timer_wb/src/main.c @@ -174,6 +174,7 @@ int c_main(void) { uart_puts(uart, "\r\n=== All tests PASSED! ===\r\n\r\n"); uart_puts(uart, "C Timer HAL test completed successfully!\r\n"); + uart_puts(uart, "Test done\r\n"); // Infinite loop to keep program running while (1) { diff --git a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/build.rs b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/build.rs index 70193e4cc..46314f9a2 100644 --- a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/build.rs +++ b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/build.rs @@ -5,6 +5,6 @@ use memmap_generate::build_utils::standard_memmap_build; fn main() { - standard_memmap_build("ScatterGatherPe.json", "DataMemory", "InstructionMemory"); + standard_memmap_build("RingbufferTest.json", "DataMemory", "InstructionMemory"); println!("cargo:rerun-if-changed=build.rs"); } diff --git a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs index dccdf3f9b..3b72e4a11 100644 --- a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs +++ b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs @@ -5,11 +5,9 @@ #![cfg_attr(not(test), no_main)] #![feature(sync_unsafe_cell)] -use bittide_hal::manual_additions::aligned_ringbuffer::{ - find_alignment_offset, ReceiveRingbuffer, TransmitRingbuffer, -}; +use bittide_hal::manual_additions::ringbuffer_test::ringbuffers::AlignedReceiveBuffer; use bittide_hal::manual_additions::timer::Instant; -use bittide_hal::scatter_gather_pe::DeviceInstances; +use bittide_hal::ringbuffer_test::DeviceInstances; use bittide_sys::smoltcp::ringbuffer::RingbufferDevice; use core::fmt::Write; use log::LevelFilter; @@ -47,24 +45,20 @@ fn main() -> ! { log::set_max_level_racy(LevelFilter::Trace); } - // Step 1: Find alignment offset + // Set up ringbuffers writeln!(uart, "Step 1: Finding ringbuffer alignment...").ok(); - let rx_offset = { - let scatter = INSTANCES.scatter_unit; - let gather = INSTANCES.gather_unit; - let hal_tx_temp = TransmitRingbuffer::new(gather); - let hal_rx_temp = ReceiveRingbuffer::new(scatter, 0); - find_alignment_offset(&hal_tx_temp, &hal_rx_temp) - }; + let tx_buffer = INSTANCES.transmit_ringbuffer; + let rx_buffer = INSTANCES.receive_ringbuffer; + let mut rx_aligned = AlignedReceiveBuffer::new(rx_buffer); + rx_aligned.align(&tx_buffer); + let rx_offset = rx_aligned + .get_alignment_offset() + .expect("Failed to find RX buffer alignment"); writeln!(uart, " Alignment offset: {}", rx_offset).ok(); // Step 2: Create smoltcp device writeln!(uart, "Step 2: Creating RingbufferDevice...").ok(); - let scatter = INSTANCES.scatter_unit; - let gather = INSTANCES.gather_unit; - let rx_buffer = ReceiveRingbuffer::new(scatter, rx_offset); - let tx_buffer = TransmitRingbuffer::new(gather); - let mut device = RingbufferDevice::new(rx_buffer, tx_buffer); + let mut device = RingbufferDevice::new(rx_aligned, tx_buffer); let mtu = device.mtu(); writeln!(uart, " MTU: {} bytes", mtu).ok(); diff --git a/firmware-binaries/sim-tests/ringbuffer_test/src/main.rs b/firmware-binaries/sim-tests/ringbuffer_test/src/main.rs index d69ade07e..51d36b627 100644 --- a/firmware-binaries/sim-tests/ringbuffer_test/src/main.rs +++ b/firmware-binaries/sim-tests/ringbuffer_test/src/main.rs @@ -4,13 +4,11 @@ #![no_std] #![cfg_attr(not(test), no_main)] -use bittide_hal::{ - manual_additions::timer::Duration, - ringbuffer_test::{devices::TransmitRingbuffer, DeviceInstances}, -}; +use bittide_hal::{manual_additions::timer::Duration, ringbuffer_test::DeviceInstances}; use core::fmt::Write; #[cfg(not(test))] use riscv_rt::entry; +use ufmt::{uwrite, uwriteln}; const INSTANCES: DeviceInstances = unsafe { DeviceInstances::new() }; @@ -24,7 +22,7 @@ fn main() -> ! { let mut uart = INSTANCES.uart; let timer = INSTANCES.timer; - writeln!(uart, "=== Ringbuffer Loopback Test (Byte-Level) ===").unwrap(); + uwriteln!(uart, "=== Ringbuffer Loopback Test (Byte-Level) ===").unwrap(); let tx_ringbuffer = INSTANCES.transmit_ringbuffer; let rx_ringbuffer = INSTANCES.receive_ringbuffer; @@ -47,10 +45,13 @@ fn main() -> ! { } // Wait for data to propagate through the loopback - timer.wait(Duration::from_cycles( - TransmitRingbuffer::DATA_LEN as u32, - timer.frequency(), - )); + // Need to wait long enough for: + // - TX to cycle through entire buffer (DATA_LEN cycles) + // - Propagation delay through configurable latency (0-100 cycles) + // - Pipeline delays in TX/RX logic + // - RX to write all frames + // Wait for 2000 cycles to definitively rule out timing issues + timer.wait(Duration::from_cycles(2000, timer.frequency())); // Read RX buffer byte by byte let mut rx_bytes = [0u8; TOTAL_BYTES]; @@ -94,30 +95,33 @@ fn main() -> ! { } if all_match { - writeln!(uart, "*** TEST PASSED: All data matches! ***").unwrap(); + uwriteln!(uart, "*** TEST PASSED: All data matches! ***").unwrap(); } else { - writeln!(uart, "\n*** TEST FAILED: Data corruption detected ***").unwrap(); + uwriteln!(uart, "\n*** TEST FAILED: Data corruption detected ***").unwrap(); if let Some((frame, byte_idx, expected, actual)) = first_mismatch { - writeln!( + uwriteln!( uart, "First mismatch at RX frame {}, byte {}: expected 0x{:02x}, got 0x{:02x}", - frame, byte_idx, expected, actual + frame, + byte_idx, + expected, + actual ) .unwrap(); } - writeln!(uart, "\nTX pattern written (byte-by-byte):").unwrap(); + uwriteln!(uart, "\nTX pattern written (byte-by-byte):").unwrap(); for frame in 0..RINGBUFFER_SIZE { let start = frame * 8; - write!(uart, " TX[{:2}]: ", frame).unwrap(); + uwrite!(uart, " TX[{}]: ", frame).unwrap(); for byte_idx in 0..8 { - write!(uart, "{:02x} ", tx_pattern[start + byte_idx]).unwrap(); + uwrite!(uart, "{:02x} ", tx_pattern[start + byte_idx]).unwrap(); } - writeln!(uart).unwrap(); + uwriteln!(uart, "").unwrap(); } - writeln!( + uwriteln!( uart, "\nRX pattern received (starting at frame {}):", offset @@ -128,45 +132,47 @@ fn main() -> ! { let tx_start = frame * 8; let rx_start = rx_frame * 8; - write!(uart, " RX[{:2}]: ", rx_frame).unwrap(); + uwrite!(uart, " RX[{:02x}]: ", rx_frame).unwrap(); let mut frame_matches = true; for byte_idx in 0..8 { let expected = tx_pattern[tx_start + byte_idx]; let actual = rx_bytes[rx_start + byte_idx]; - write!(uart, "{:02x} ", actual).unwrap(); + uwrite!(uart, "{:02x} ", actual).unwrap(); if actual != expected { frame_matches = false; } } - writeln!(uart, "{}", if frame_matches { "✓" } else { "✗" }).unwrap(); + uwriteln!(uart, "{}", if frame_matches { "✓" } else { "✗" }).unwrap(); } + uwriteln!(uart, "Test done").unwrap(); } } else { - writeln!( + uwriteln!( uart, "\n*** TEST FAILED: First frame not found in RX buffer ***" ) .unwrap(); - writeln!(uart, "\nTX pattern written (byte-by-byte):").unwrap(); + uwriteln!(uart, "\nTX pattern written (byte-by-byte):").unwrap(); for frame in 0..RINGBUFFER_SIZE { let start = frame * 8; - write!(uart, " TX[{:2}]: ", frame).unwrap(); + uwrite!(uart, " TX[{}]: ", frame).unwrap(); for byte_idx in 0..8 { - write!(uart, "{:02x} ", tx_pattern[start + byte_idx]).unwrap(); + uwrite!(uart, "{:02x} ", tx_pattern[start + byte_idx]).unwrap(); } - writeln!(uart).unwrap(); + uwriteln!(uart, "").unwrap(); } - writeln!(uart, "\nRX buffer contents (byte-by-byte):").unwrap(); + uwriteln!(uart, "\nRX buffer contents (byte-by-byte):").unwrap(); for frame in 0..RINGBUFFER_SIZE { let start = frame * 8; - write!(uart, " RX[{:2}]: ", frame).unwrap(); + uwrite!(uart, " RX[{:02x}]: ", frame).unwrap(); for byte_idx in 0..8 { - write!(uart, "{:02x} ", rx_bytes[start + byte_idx]).unwrap(); + uwrite!(uart, "{:02x} ", rx_bytes[start + byte_idx]).unwrap(); } - writeln!(uart).unwrap(); + uwriteln!(uart, "").unwrap(); } + uwriteln!(uart, "Test done").unwrap(); } loop { diff --git a/firmware-binaries/sim-tests/scatter_gather_test/src/main.rs b/firmware-binaries/sim-tests/scatter_gather_test/src/main.rs index badf44426..3f455b0b6 100644 --- a/firmware-binaries/sim-tests/scatter_gather_test/src/main.rs +++ b/firmware-binaries/sim-tests/scatter_gather_test/src/main.rs @@ -63,6 +63,7 @@ fn main() -> ! { writeln!(uart, "Read from scatter memory:").unwrap(); writeln!(uart, "{:?}", destination).unwrap(); } + writeln!(uart, "Test done").unwrap(); loop { continue; } diff --git a/firmware-support/bittide-hal-c/include/bittide_ring_transmit.h b/firmware-support/bittide-hal-c/include/bittide_ring_transmit.h index 359015a52..84d27f77e 100644 --- a/firmware-support/bittide-hal-c/include/bittide_ring_transmit.h +++ b/firmware-support/bittide-hal-c/include/bittide_ring_transmit.h @@ -41,7 +41,7 @@ */ static inline void transmit_ringbuffer_write_slice_unchecked( TransmitRingbuffer unit, uint64_t *src, uint32_t offset, uint32_t len) { - for (uint32_t i = 0; i < len; i++) { + for (uint32_t i = 0; i < len; i++) { transmit_ringbuffer_set_data_unchecked(unit, offset + i, (uint8_t const *)&src[i]); } diff --git a/firmware-support/bittide-hal/src/manual_additions/aligned_ringbuffer.rs b/firmware-support/bittide-hal/src/manual_additions/aligned_ringbuffer.rs deleted file mode 100644 index 21b85a026..000000000 --- a/firmware-support/bittide-hal/src/manual_additions/aligned_ringbuffer.rs +++ /dev/null @@ -1,204 +0,0 @@ -// 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 b2df1ad9d..2caae2262 100644 --- a/firmware-support/bittide-hal/src/manual_additions/mod.rs +++ b/firmware-support/bittide-hal/src/manual_additions/mod.rs @@ -3,11 +3,12 @@ // SPDX-License-Identifier: Apache-2.0 pub mod addressable_buffer; -pub mod aligned_ringbuffer; +pub mod aligned; pub mod calendar; pub mod capture_ugn; pub mod dna; pub mod elastic_buffer; +pub mod ringbuffer_test; pub mod scatter_gather_pe; pub mod si539x_spi; pub mod soft_ugn_demo_mu; diff --git a/firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/mod.rs b/firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/mod.rs new file mode 100644 index 000000000..2590006d7 --- /dev/null +++ b/firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/mod.rs @@ -0,0 +1,4 @@ +// SPDX-FileCopyrightText: 2025 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 +pub mod ringbuffers; diff --git a/firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/ringbuffers.rs b/firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/ringbuffers.rs new file mode 100644 index 000000000..b006b801d --- /dev/null +++ b/firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/ringbuffers.rs @@ -0,0 +1,259 @@ +// SPDX-FileCopyrightText: 2025 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 +use crate::hals::ringbuffer_test::devices::{ReceiveRingbuffer, TransmitRingbuffer}; + +/// Alignment protocol marker values +const ALIGNMENT_EMPTY: u64 = 0; +const ALIGNMENT_ANNOUNCE: u64 = 0xBADC0FFEE; +const ALIGNMENT_ACKNOWLEDGE: u64 = 0xDEADABBA; + +impl TransmitRingbuffer { + /// Write a slice to the transmit buffer. + /// + /// # Panics + /// + /// The source memory size must be smaller or equal to the memory size of + /// the `TransmitRingbuffer` memory. + pub fn write_slice(&self, src: &[[u8; 8]], offset: usize) { + assert!(src.len() + offset <= Self::DATA_LEN); + unsafe { + self.write_slice_unchecked(src, offset); + } + } + + /// Write a slice to the transmit buffer without checking bounds. The caller must + /// ensure that `src.len() + offset` does not exceed the buffer size. + /// + /// # Safety + /// + /// This function is unsafe because it can cause out-of-bounds memory access if the caller + /// does not ensure that `src.len() + offset` is within the bounds of the transmit buffer. + pub unsafe fn write_slice_unchecked(&self, src: &[[u8; 8]], offset: usize) { + let mut off = offset; + for &val in src { + unsafe { + self.set_data_unchecked(off, val); + } + off += 1; + } + } + + /// Write a slice to the transmit buffer with automatic wrapping. If we write more bytes + /// than the buffer size, we wrap back to the beginning of the buffer. + /// + /// # Panics + /// + /// This function will panic if `src.len()` is greater than the buffer size. + pub fn write_slice_with_wrap(&self, src: &[[u8; 8]], offset: usize) { + assert!(src.len() <= Self::DATA_LEN); + unsafe { + self.write_slice_with_wrap_unchecked(src, offset); + } + } + + /// Write a slice to the transmit buffer with automatic wrapping. If we write more bytes + /// than the buffer size, we wrap back to the beginning of the buffer. + /// + /// # Safety + /// This function is unsafe because it can cause out-of-bounds memory access if the caller + /// does not ensure that `src.len()` is smaller or equal to the buffer size. + pub unsafe fn write_slice_with_wrap_unchecked(&self, src: &[[u8; 8]], offset: usize) { + if src.len() + offset <= Self::DATA_LEN { + // No wrapping needed + self.write_slice(src, offset); + } else { + // Wrapping needed - split into two writes + let first_part_len = Self::DATA_LEN - offset; + let (first, second) = src.split_at(first_part_len); + self.write_slice(first, offset); + self.write_slice(second, 0); + } + } + + /// Clear the entire transmit buffer by writing zeros to all entries. + pub fn clear(&self) { + for i in 0..Self::DATA_LEN { + unsafe { + self.set_data_unchecked(i, [0u8; 8]); + } + } + } +} + +impl ReceiveRingbuffer { + /// Read a slice from the receive buffer. + /// + /// # Panics + /// + /// The destination memory size must be smaller or equal to the memory size + /// of the `ReceiveRingbuffer`. + pub fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { + assert!(dst.len() + offset <= Self::DATA_LEN); + unsafe { + self.read_slice_unchecked(dst, offset); + } + } + + /// Reads a slice from the receive buffer without checking bounds. The caller must + /// ensure that `dst.len() + offset` does not exceed the buffer size. + /// + /// # Safety + /// + /// This function is unsafe because it can cause out-of-bounds memory access if the caller + /// does not ensure that `dst.len() + offset` is within the bounds of the receive buffer. + pub unsafe fn read_slice_unchecked(&self, dst: &mut [[u8; 8]], offset: usize) { + let mut off = offset; + for val in dst { + unsafe { + *val = self.data_unchecked(off); + } + off += 1; + } + } + + /// Reads a slice from the receive buffer with automatic wrapping. If we read more bytes + /// than the buffer size, we wrap back to the beginning of the buffer. + /// + /// # Panics + /// + /// This function will panic if `dst.len()` is greater than the buffer size. + pub fn read_slice_with_wrap(&self, dst: &mut [[u8; 8]], offset: usize) { + assert!(dst.len() <= Self::DATA_LEN); + unsafe { + self.read_slice_with_wrap_unchecked(dst, offset); + } + } + /// Reads a slice from the receive buffer with automatic wrapping. If we read more bytes + /// than the buffer size, we wrap back to the beginning of the buffer. + /// + /// # Safety + /// + /// This function is unsafe because it can cause out-of-bounds memory access if the caller + /// does not ensure that `dst.len()` is smaller or equal to the buffer size. + pub unsafe fn read_slice_with_wrap_unchecked(&self, dst: &mut [[u8; 8]], offset: usize) { + if dst.len() + offset <= Self::DATA_LEN { + // No wrapping needed + self.read_slice(dst, offset); + } else { + // Wrapping needed - split into two reads + let first_part_len = Self::DATA_LEN - offset; + let (first, second) = dst.split_at_mut(first_part_len); + self.read_slice(first, offset); + self.read_slice(second, 0); + } + } +} + +pub struct AlignedReceiveBuffer { + pub rx: ReceiveRingbuffer, + rx_alignment_offset: Option, + tx_reference: usize, // Allows us to verify the tx_buffer we are aligned to. +} +impl AlignedReceiveBuffer { + /// Perform the alignment discovery protocol and return a new `AlignedReceiveBuffer` + /// with the discovered RX alignment offset. + /// + /// # Panics + /// + /// This function will panic if the transmit and receive buffers do not have the same size, + /// since that is a requirement for the alignment protocol to work. + pub fn new(rx: ReceiveRingbuffer) -> Self { + Self { + rx, + rx_alignment_offset: None, + tx_reference: 0, + } + } + + /// Returns true if the alignment offset has been discovered and the buffer is aligned. + pub fn is_aligned(&self) -> bool { + self.rx_alignment_offset.is_some() + } + + /// Returns the discovered alignment offset, or None if the offset has not been discovered yet. + pub fn get_alignment_offset(&self) -> Option { + self.rx_alignment_offset + } + + /// Performs the alignment discovery protocol. After this function completes, the `rx_alignment_offset` + /// field will be set with the discovered offset, and the RX buffer will be aligned to the neighbor's TX buffer. + pub fn align(&mut self, tx: &TransmitRingbuffer) { + // 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..TransmitRingbuffer::DATA_LEN { + 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..ReceiveRingbuffer::DATA_LEN { + let mut data_buf = [[0u8; 8]; 1]; + // Read directly from physical index by using scatter's read_slice + self.rx.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 + self.rx.read_slice(&mut data_buf, rx_offset); + let value = u64::from_le_bytes(data_buf[0]); + + if value == ALIGNMENT_ACKNOWLEDGE { + break; + } + } + self.rx_alignment_offset = Some(rx_offset); + self.tx_reference = tx.0 as *const _ as usize; + } + + /// Unsets the discovered alignment offset. + pub fn clear_alignment(&mut self) { + self.rx_alignment_offset = None; + self.tx_reference = 0; + } + + /// Returns true if the RX buffer is aligned to the provided TX buffer. + pub fn verify_aligned_to(&self, tx: &TransmitRingbuffer) -> bool { + self.is_aligned() && self.tx_reference == (tx.0 as *const _ as usize) + } + + /// Returns the reference address of the TX buffer that this RX buffer is aligned to. + pub fn get_alignment_reference(&self) -> usize { + self.tx_reference + } + /// Read a slice from the receive buffer using the discovered alignment offset. + /// After aligning the buffer pair, the user can use this function to read from the receive + /// buffer without needing to worry about the physical alignment offset. + /// + /// # Panics + /// + /// This function will panic if `dst.len()` is greater than the buffer size. + /// This function will also panic if the caller tries to read beyond the end of the buffer. + /// This function will also panic if `align()` has not been called yet to discover the alignment offset. + pub fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { + assert!(dst.len() + offset <= ReceiveRingbuffer::DATA_LEN); + let rx_offset = self + .rx_alignment_offset + .expect("Alignment offset not discovered yet. Call align() first."); + let mut aligned_offset = offset + rx_offset; + if aligned_offset >= ReceiveRingbuffer::DATA_LEN { + aligned_offset -= ReceiveRingbuffer::DATA_LEN; + } + self.rx.read_slice_with_wrap(dst, aligned_offset) + } +} diff --git a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs index 1579f3ccf..91aae23e8 100644 --- a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs +++ b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs @@ -28,8 +28,8 @@ //! 4. If CRC validates, extract payload and consume packet //! 5. Track sequence numbers to detect repeated packets -use bittide_hal::hals::scatter_gather_pe::devices::{GatherUnit, ScatterUnit}; -use bittide_hal::manual_additions::aligned_ringbuffer::{ReceiveRingbuffer, TransmitRingbuffer}; +use bittide_hal::hals::ringbuffer_test::devices::{ReceiveRingbuffer, TransmitRingbuffer}; +use bittide_hal::manual_additions::ringbuffer_test::ringbuffers::AlignedReceiveBuffer; use crc::{Crc, CRC_32_ISCSI}; use log::trace; use smoltcp::phy::{self, Device, DeviceCapabilities, Medium}; @@ -69,7 +69,7 @@ fn is_valid(buffer: &[u8]) -> bool { /// The MTU is automatically calculated from the minimum of the scatter and gather /// buffer sizes (in bytes), minus space for packet header (which includes CRC32). pub struct RingbufferDevice { - rx_buffer: ReceiveRingbuffer, + rx_buffer: AlignedReceiveBuffer, tx_buffer: TransmitRingbuffer, mtu: usize, /// Last valid sequence number we saw (to detect repeated packets) @@ -83,13 +83,22 @@ impl RingbufferDevice { /// /// The ringbuffers must already be aligned using the alignment protocol. /// The MTU is calculated as the minimum of the RX and TX buffer sizes in bytes. - pub fn new(rx_buffer: ReceiveRingbuffer, tx_buffer: TransmitRingbuffer) -> Self { + pub fn new(rx_buffer: AlignedReceiveBuffer, tx_buffer: TransmitRingbuffer) -> Self { // Calculate MTU from buffer sizes (each word is 8 bytes) // Reserve space for packet header (CRC is part of header) - let rx_bytes = ScatterUnit::SCATTER_MEMORY_LEN * 8; - let tx_bytes = GatherUnit::GATHER_MEMORY_LEN * 8; + let rx_bytes = ReceiveRingbuffer::DATA_LEN * 8; + let tx_bytes = TransmitRingbuffer::DATA_LEN * 8; let mtu = rx_bytes.min(tx_bytes) - PACKET_HEADER_SIZE; + if rx_buffer.is_aligned() { + assert!( + rx_buffer.verify_aligned_to(&tx_buffer), + "RX buffer is aligned but not to the provided TX buffer, expected reference {:p}, got 0x{:X}", + &tx_buffer.0, + rx_buffer.get_alignment_reference(), + ); + } + Self { rx_buffer, tx_buffer, @@ -150,7 +159,7 @@ impl Device for RingbufferDevice { // Allocate aligned buffer for reading from ringbuffer // Use a flat byte buffer and cast it to [[u8; 8]] for the API - let mut packet_buffer = [0u8; ScatterUnit::SCATTER_MEMORY_LEN * 8]; + let mut packet_buffer = [0u8; ReceiveRingbuffer::DATA_LEN * 8]; let word_slice = unsafe { core::slice::from_raw_parts_mut(packet_buffer.as_mut_ptr() as *mut [u8; 8], num_words) @@ -173,7 +182,7 @@ impl Device for RingbufferDevice { self.last_rx_seq = seq_num; // Extract payload (skip header, exclude CRC) - let mut payload = [0u8; ScatterUnit::SCATTER_MEMORY_LEN * 8]; + let mut payload = [0u8; ReceiveRingbuffer::DATA_LEN * 8]; payload[..packet_len] .copy_from_slice(&packet_buffer[PACKET_HEADER_SIZE..PACKET_HEADER_SIZE + packet_len]); @@ -203,7 +212,7 @@ impl Device for RingbufferDevice { /// Contains a local copy of the packet payload that has been validated /// against CRC32 corruption. pub struct RxToken { - buffer: [u8; ScatterUnit::SCATTER_MEMORY_LEN * 8], + buffer: [u8; ReceiveRingbuffer::DATA_LEN * 8], length: usize, } @@ -237,7 +246,7 @@ impl phy::TxToken for TxToken<'_> { ); // Prepare buffer: header + payload - let mut buffer = [0u8; GatherUnit::GATHER_MEMORY_LEN * 8]; + let mut buffer = [0u8; TransmitRingbuffer::DATA_LEN * 8]; // Write header fields using direct pointer writes // Header format: CRC32 (4 bytes) + sequence (2 bytes) + length (2 bytes) From 245016f8499e279debec4000c7800e80da57a17f Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Mon, 9 Mar 2026 13:49:09 +0100 Subject: [PATCH 27/70] Change `release_max_level_info` to `release_max_level_trace` --- firmware-binaries/examples/smoltcp_client/Cargo.toml | 2 +- firmware-binaries/sim-tests/ringbuffer_smoltcp_test/Cargo.toml | 2 +- firmware-binaries/sim-tests/switch_demo_pe_test/Cargo.toml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/firmware-binaries/examples/smoltcp_client/Cargo.toml b/firmware-binaries/examples/smoltcp_client/Cargo.toml index bc160a03d..89165d899 100644 --- a/firmware-binaries/examples/smoltcp_client/Cargo.toml +++ b/firmware-binaries/examples/smoltcp_client/Cargo.toml @@ -31,7 +31,7 @@ default-features = false [dependencies.log] version = "0.4.21" -features = ["max_level_trace", "release_max_level_info"] +features = ["max_level_trace", "release_max_level_trace"] [dependencies.smoltcp] version = "0.12.0" diff --git a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/Cargo.toml b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/Cargo.toml index a8dc71393..6daf1838c 100644 --- a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/Cargo.toml +++ b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/Cargo.toml @@ -23,7 +23,7 @@ features = ["medium-ip", "medium-ethernet", "proto-ipv4", "socket-tcp"] [dependencies.log] version = "0.4.21" -features = ["max_level_trace", "release_max_level_info"] +features = ["max_level_trace", "release_max_level_trace"] [build-dependencies] memmap-generate = { path = "../../../firmware-support/memmap-generate" } diff --git a/firmware-binaries/sim-tests/switch_demo_pe_test/Cargo.toml b/firmware-binaries/sim-tests/switch_demo_pe_test/Cargo.toml index a2dae01d9..8b8e8a4d9 100644 --- a/firmware-binaries/sim-tests/switch_demo_pe_test/Cargo.toml +++ b/firmware-binaries/sim-tests/switch_demo_pe_test/Cargo.toml @@ -19,7 +19,7 @@ ufmt = "0.2.0" [dependencies.log] version = "0.4.21" -features = ["max_level_trace", "release_max_level_info"] +features = ["max_level_trace", "release_max_level_trace"] [build-dependencies] memmap-generate = { path = "../../../firmware-support/memmap-generate" } From 73cfe4b1c6325e9db9ba96eb00414735e77d3937 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Mon, 9 Mar 2026 13:55:00 +0100 Subject: [PATCH 28/70] Rewrite `ringbuffer-smoltcp` to use new networking infra --- .../src/Bittide/Instances/Tests/Ringbuffer.hs | 26 +- firmware-binaries/Cargo.lock | 22 + .../ringbuffer_smoltcp_test/src/main.rs | 340 +++++++++---- firmware-support/Cargo.lock | 26 +- firmware-support/bittide-sys/Cargo.toml | 1 + firmware-support/bittide-sys/src/lib.rs | 1 + firmware-support/bittide-sys/src/net_state.rs | 476 ++++++++++++++++++ firmware-support/bittide-sys/src/smoltcp.rs | 1 + .../bittide-sys/src/smoltcp/ringbuffer.rs | 10 +- .../src/smoltcp/soft_ugn_ringbuffer.rs | 300 +++++++++++ 10 files changed, 1083 insertions(+), 120 deletions(-) create mode 100644 firmware-support/bittide-sys/src/net_state.rs create mode 100644 firmware-support/bittide-sys/src/smoltcp/soft_ugn_ringbuffer.rs diff --git a/bittide-instances/src/Bittide/Instances/Tests/Ringbuffer.hs b/bittide-instances/src/Bittide/Instances/Tests/Ringbuffer.hs index 16264a715..e46bbdbb6 100644 --- a/bittide-instances/src/Bittide/Instances/Tests/Ringbuffer.hs +++ b/bittide-instances/src/Bittide/Instances/Tests/Ringbuffer.hs @@ -34,6 +34,8 @@ import Bittide.Ringbuffer import Bittide.SharedTypes (withBittideByteOrder) import Bittide.Wishbone +import qualified Data.List as L + createDomain vSystem{vName = "Slow", vPeriod = hzToPeriod 1000000} -- | Memory depth for the ringbuffers (16 entries of 8 bytes each) @@ -43,7 +45,7 @@ memDepth = SNat dutMM :: (HasCallStack) => Protocols.MemoryMap.MemoryMap dutMM = (\(SimOnly mm, _) -> mm) - $ withClockResetEnable @System clockGen (resetGenN d2) enableGen + $ withClockResetEnable @Slow clockGen (resetGenN d2) enableGen $ toSignals (dutWithBinary d0 "") ((), pure $ deepErrorX "memoryMap") -- | Parameterized DUT that loads a specific firmware binary with configurable latency. @@ -96,16 +98,22 @@ dutWithBinary latency binaryName = withBittideByteOrder $ circuit $ \mm -> do type IMemWords = DivRU (300 * 1024) 4 type DMemWords = DivRU (256 * 1024) 4 +takeUntilList :: (Eq a) => [a] -> [a] -> [a] +takeUntilList _ [] = [] +takeUntilList prefix xs@(y : ys) + | prefix `L.isPrefixOf` xs = [] + | otherwise = y : takeUntilList prefix ys + -- Ringbuffer test simulation simRingbuffer :: IO () simRingbuffer = putStr $ simResultRingbuffer d0 simResultRingbuffer :: forall latency. (HasCallStack, KnownNat latency) => SNat latency -> String -simResultRingbuffer lat = chr . fromIntegral <$> catMaybes uartStream +simResultRingbuffer lat = takeUntilList "=== Test Complete ===" $ chr . fromIntegral <$> catMaybes uartStream where - uartStream = sampleC def{timeoutAfter = 500_000} (dutNoMM lat) + uartStream = sampleC def{timeoutAfter = 1_000_000} (dutNoMM lat) - dutNoMM :: (HasCallStack, KnownNat n) => SNat n -> Circuit () (Df System (BitVector 8)) + dutNoMM :: (HasCallStack, KnownNat n) => SNat n -> Circuit () (Df Slow (BitVector 8)) dutNoMM latency = circuit $ do mm <- ignoreMM uartTx <- @@ -118,11 +126,11 @@ simSmolTcp :: IO () simSmolTcp = putStr $ simResultSmolTcp d0 simResultSmolTcp :: forall latency. (HasCallStack, KnownNat latency) => SNat latency -> String -simResultSmolTcp lat = chr . fromIntegral <$> catMaybes uartStream +simResultSmolTcp lat = takeUntilList "=== Test Complete ===" $ chr . fromIntegral <$> catMaybes uartStream where - uartStream = sampleC def{timeoutAfter = 500_000} (dutNoMM lat) + uartStream = sampleC def{timeoutAfter = 10_000_000} (dutNoMM lat) - dutNoMM :: (HasCallStack, KnownNat n) => SNat n -> Circuit () (Df System (BitVector 8)) + dutNoMM :: (HasCallStack, KnownNat n) => SNat n -> Circuit () (Df Slow (BitVector 8)) dutNoMM latency = circuit $ do mm <- ignoreMM uartTx <- @@ -136,11 +144,11 @@ simAlignedRingbuffer = putStr $ simResultAlignedRingbuffer d0 simResultAlignedRingbuffer :: forall latency. (HasCallStack, KnownNat latency) => SNat latency -> String -simResultAlignedRingbuffer lat = chr . fromIntegral <$> catMaybes uartStream +simResultAlignedRingbuffer lat = takeUntilList "=== Test Complete ===" $ chr . fromIntegral <$> catMaybes uartStream where uartStream = sampleC def{timeoutAfter = 250_000} (dutNoMM lat) - dutNoMM :: (HasCallStack, KnownNat n) => SNat n -> Circuit () (Df System (BitVector 8)) + dutNoMM :: (HasCallStack, KnownNat n) => SNat n -> Circuit () (Df Slow (BitVector 8)) dutNoMM latency = circuit $ do mm <- ignoreMM uartTx <- diff --git a/firmware-binaries/Cargo.lock b/firmware-binaries/Cargo.lock index 918a9cd2d..1f5e2113d 100644 --- a/firmware-binaries/Cargo.lock +++ b/firmware-binaries/Cargo.lock @@ -103,6 +103,7 @@ dependencies = [ "rand", "smoltcp 0.12.0", "ufmt", + "zerocopy", ] [[package]] @@ -952,3 +953,24 @@ dependencies = [ "riscv-rt 0.11.0", "ufmt", ] + +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", +] diff --git a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs index 3b72e4a11..853397315 100644 --- a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs +++ b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs @@ -8,12 +8,14 @@ use bittide_hal::manual_additions::ringbuffer_test::ringbuffers::AlignedReceiveBuffer; use bittide_hal::manual_additions::timer::Instant; use bittide_hal::ringbuffer_test::DeviceInstances; +use bittide_sys::net_state::{Manager, NetMedium, Subordinate, UgnEdge, UgnReport}; use bittide_sys::smoltcp::ringbuffer::RingbufferDevice; use core::fmt::Write; -use log::LevelFilter; -use smoltcp::iface::{Config, Interface, SocketSet, SocketStorage}; +use log::{info, trace, LevelFilter}; +use smoltcp::iface::{Config, Interface, SocketHandle, SocketSet, SocketStorage}; use smoltcp::socket::tcp; use smoltcp::wire::{HardwareAddress, IpAddress, IpCidr}; +use ufmt::uwriteln; #[cfg(not(test))] use riscv_rt::entry; @@ -27,26 +29,180 @@ fn to_smoltcp_instant(instant: Instant) -> smoltcp::time::Instant { smoltcp::time::Instant::from_micros(instant.micros() as i64) } +struct SmoltcpManagerMedium<'a, 'b> { + iface: &'a mut Interface, + sockets: &'a mut SocketSet<'b>, + client_handle: SocketHandle, +} + +impl<'a, 'b> SmoltcpManagerMedium<'a, 'b> { + fn new( + iface: &'a mut Interface, + sockets: &'a mut SocketSet<'b>, + client_handle: SocketHandle, + ) -> Self { + Self { + iface, + sockets, + client_handle, + } + } +} + +impl NetMedium for SmoltcpManagerMedium<'_, '_> { + fn phy_ready(&self, _link: usize) -> bool { + true + } + + fn setup_interface(&mut self, _link: usize, local_ip: [u8; 4]) { + let ip = IpCidr::new( + IpAddress::v4(local_ip[0], local_ip[1], local_ip[2], local_ip[3]), + 24, + ); + self.iface.update_ip_addrs(|addrs| { + if !addrs.contains(&ip) { + let _ = addrs.push(ip); + } + }); + } + + fn connect(&mut self, _link: usize, peer_ip: [u8; 4]) -> bool { + let client = self.sockets.get_mut::(self.client_handle); + if !client.is_open() && !client.is_active() { + let cx = self.iface.context(); + let _ = client.connect( + cx, + ( + IpAddress::v4(peer_ip[0], peer_ip[1], peer_ip[2], peer_ip[3]), + SERVER_PORT, + ), + CLIENT_PORT, + ); + } + client.is_active() + } + + fn listen(&mut self, _link: usize) -> bool { + false + } + + fn send(&mut self, _link: usize, data: &[u8]) -> bool { + let client = self.sockets.get_mut::(self.client_handle); + if client.can_send() { + let _ = client.send_slice(data); + return true; + } + false + } + + fn recv(&mut self, _link: usize, buf: &mut [u8]) -> Option { + let client = self.sockets.get_mut::(self.client_handle); + if client.can_recv() { + if let Ok(len) = client.recv_slice(buf) { + return Some(len); + } + } + None + } + + fn timed_out(&self, _link: usize) -> bool { + false + } +} + +struct SmoltcpSubordinateMedium<'a, 'b> { + iface: &'a mut Interface, + sockets: &'a mut SocketSet<'b>, + server_handle: SocketHandle, +} + +impl<'a, 'b> SmoltcpSubordinateMedium<'a, 'b> { + fn new( + iface: &'a mut Interface, + sockets: &'a mut SocketSet<'b>, + server_handle: SocketHandle, + ) -> Self { + Self { + iface, + sockets, + server_handle, + } + } +} + +impl NetMedium for SmoltcpSubordinateMedium<'_, '_> { + fn phy_ready(&self, _link: usize) -> bool { + true + } + + fn setup_interface(&mut self, _link: usize, local_ip: [u8; 4]) { + let ip = IpCidr::new( + IpAddress::v4(local_ip[0], local_ip[1], local_ip[2], local_ip[3]), + 24, + ); + self.iface.update_ip_addrs(|addrs| { + if !addrs.contains(&ip) { + let _ = addrs.push(ip); + } + }); + } + + fn connect(&mut self, _link: usize, _peer_ip: [u8; 4]) -> bool { + false + } + + fn listen(&mut self, _link: usize) -> bool { + let server = self.sockets.get_mut::(self.server_handle); + if !server.is_open() { + let _ = server.listen(SERVER_PORT); + } + server.is_open() + } + + fn send(&mut self, _link: usize, data: &[u8]) -> bool { + let server = self.sockets.get_mut::(self.server_handle); + if server.can_send() { + let _ = server.send_slice(data); + return true; + } + false + } + + fn recv(&mut self, _link: usize, buf: &mut [u8]) -> Option { + let server = self.sockets.get_mut::(self.server_handle); + if server.can_recv() { + if let Ok(len) = server.recv_slice(buf) { + return Some(len); + } + } + None + } + + fn timed_out(&self, _link: usize) -> bool { + false + } +} + #[cfg_attr(not(test), entry)] fn main() -> ! { let mut uart = INSTANCES.uart; let timer = INSTANCES.timer; - writeln!(uart, "\n=== Ringbuffer smoltcp Loopback Test ===").ok(); - // Set up logging unsafe { use bittide_sys::uart::log::LOGGER; let logger = &mut (*LOGGER.get()); logger.set_logger(uart.clone()); logger.set_timer(INSTANCES.timer); - logger.display_source = LevelFilter::Debug; + logger.display_source = LevelFilter::Warn; log::set_logger_racy(logger).ok(); log::set_max_level_racy(LevelFilter::Trace); } + info!("=== Ringbuffer smoltcp Loopback Test ==="); + // Set up ringbuffers - writeln!(uart, "Step 1: Finding ringbuffer alignment...").ok(); + info!("Step 1: Finding ringbuffer alignment..."); let tx_buffer = INSTANCES.transmit_ringbuffer; let rx_buffer = INSTANCES.receive_ringbuffer; let mut rx_aligned = AlignedReceiveBuffer::new(rx_buffer); @@ -54,28 +210,26 @@ fn main() -> ! { let rx_offset = rx_aligned .get_alignment_offset() .expect("Failed to find RX buffer alignment"); - writeln!(uart, " Alignment offset: {}", rx_offset).ok(); + trace!(" Alignment offset: {}", rx_offset); // Step 2: Create smoltcp device - writeln!(uart, "Step 2: Creating RingbufferDevice...").ok(); + info!("Step 2: Creating RingbufferDevice..."); let mut device = RingbufferDevice::new(rx_aligned, tx_buffer); let mtu = device.mtu(); - writeln!(uart, " MTU: {} bytes", mtu).ok(); + trace!(" MTU: {} bytes", mtu); - // Step 3: Configure interface with static IP - writeln!(uart, "Step 3: Configuring network interface...").ok(); + // Step 3: Configure network interface + info!("Step 3: Configuring network interface..."); let hw_addr = HardwareAddress::Ip; - let ip_addr = IpCidr::new(IpAddress::v4(127, 0, 0, 1), 8); // Loopback address let config = Config::new(hw_addr); let now = to_smoltcp_instant(timer.now()); let mut iface = Interface::new(config, &mut device, now); iface.update_ip_addrs(|addrs| { - addrs.push(ip_addr).unwrap(); + addrs.clear(); }); - writeln!(uart, " IP: {}", ip_addr).ok(); // Step 4: Create TCP sockets - writeln!(uart, "Step 4: Creating TCP sockets...").ok(); + info!("Step 4: Creating TCP sockets..."); // Server socket - reduced buffer sizes to fit in memory static mut SERVER_RX_BUF: [u8; 256] = [0; 256]; @@ -96,114 +250,94 @@ fn main() -> ! { let server_handle = sockets.add(server_socket); let client_handle = sockets.add(client_socket); - // Step 5: Set up server to listen - writeln!( - uart, - "Step 5: Starting TCP server on port {}...", - SERVER_PORT - ) - .ok(); - let server = sockets.get_mut::(server_handle); - server.listen(SERVER_PORT).unwrap(); - writeln!(uart, " Server listening").ok(); - - // Step 6: Connect client to server - writeln!(uart, "Step 6: Connecting client to server...").ok(); - let client = sockets.get_mut::(client_handle); - let cx = iface.context(); - client - .connect(cx, (IpAddress::v4(127, 0, 0, 1), SERVER_PORT), CLIENT_PORT) - .unwrap(); - writeln!(uart, " Connection initiated").ok(); + // Step 5: Initialize link state machines + info!("Step 5: Initializing link state machines..."); // Main event loop - writeln!(uart, "Step 7: Running main event loop...").ok(); - let test_data = b"Hello from smoltcp!"; - let mut received_data = [0u8; 64]; - let mut received_len = 0; - let mut data_sent = false; - let mut connection_established = false; - - for _ in 0..200 { + info!("Step 7: Running main event loop..."); + let mut done_logged = false; + + let mut manager = Manager::new(); + let mut subordinate = Subordinate::new(); + subordinate.set_report(build_placeholder_report()); + + for _ in 0..1000 { let timestamp = to_smoltcp_instant(timer.now()); iface.poll(timestamp, &mut device, &mut sockets); - // Check if connection is established - let client = sockets.get::(client_handle); - if client.is_active() && !connection_established { - writeln!(uart, " Connection established!").ok(); - connection_established = true; - } + let mut manager_medium = SmoltcpManagerMedium::new(&mut iface, &mut sockets, client_handle); + manager.step(&mut manager_medium, 0); - // Send data from client - if client.is_active() && !data_sent { - let client = sockets.get_mut::(client_handle); - if client.can_send() { - if let Ok(sent) = client.send_slice(test_data) { - writeln!(uart, " Sent {} bytes", sent).ok(); - data_sent = true; - } - } - } + let mut subordinate_medium = + SmoltcpSubordinateMedium::new(&mut iface, &mut sockets, server_handle); + subordinate.step(&mut subordinate_medium, 0); - // Receive data on server - if data_sent && received_len == 0 { - let server = sockets.get_mut::(server_handle); - if server.can_recv() { - if let Ok(len) = server.recv_slice(&mut received_data) { - received_len = len; - writeln!(uart, " Received {} bytes", len).ok(); - break; - } - } + if manager.is_done() && subordinate.is_done() && !done_logged { + info!(" Manager collected UGN report"); + done_logged = true; + break; } } // Verify results - writeln!(uart, "Step 8: Verifying results...").ok(); - - if !connection_established { - writeln!(uart, " FAILURE: Connection timeout!").ok(); - } else if !data_sent { - writeln!(uart, " FAILURE: Failed to send data!").ok(); - } else if received_len == 0 { - writeln!(uart, " FAILURE: Failed to receive data!").ok(); - } else { - let received_slice = &received_data[..received_len]; - if received_len == test_data.len() && received_slice == test_data { - writeln!(uart, " SUCCESS: Data matches!").ok(); - writeln!( - uart, - " Sent: {:?}", - core::str::from_utf8(test_data).unwrap() - ) - .ok(); - writeln!( - uart, - " Received: {:?}", - core::str::from_utf8(received_slice).unwrap() - ) - .ok(); - } else { - writeln!(uart, " FAILURE: Data mismatch!").ok(); - writeln!( - uart, - " Expected {} bytes: {:?}", - test_data.len(), - test_data - ) - .ok(); - writeln!(uart, " Got {} bytes: {:?}", received_len, received_slice).ok(); + info!("Step 8: Verifying results..."); + + if !done_logged { + info!(" FAILURE: UGN report timeout!"); + } else if let Some(report) = manager.report() { + info!(" SUCCESS: Manager received {} UGN edges", report.count); + for (idx, edge) in report.edges.iter().enumerate() { + if idx >= report.count as usize { + break; + } + info!( + " Edge {}: {}:{} -> {}:{}, ugn={}, valid={}", + idx, + edge.src_node, + edge.src_port, + edge.dst_node, + edge.dst_port, + edge.ugn, + edge.is_valid + ); } + } else { + info!(" FAILURE: Missing UGN report data!"); } - writeln!(uart, "\n=== Test Complete ===").ok(); + uwriteln!(uart, "=== Test Complete ===").unwrap(); loop { continue; } } +fn build_placeholder_report() -> UgnReport { + let mut report = UgnReport { + count: 2, + ..Default::default() + }; + report.edges[0] = UgnEdge { + src_node: 1, + src_port: 0, + dst_node: 0, + dst_port: 0, + ugn: 123, + is_valid: 1, + _padding: [0; 7], + }; + report.edges[1] = UgnEdge { + src_node: 1, + src_port: 1, + dst_node: 0, + dst_port: 1, + ugn: 456, + is_valid: 1, + _padding: [0; 7], + }; + report +} + #[cfg(not(test))] #[panic_handler] fn panic(info: &core::panic::PanicInfo) -> ! { diff --git a/firmware-support/Cargo.lock b/firmware-support/Cargo.lock index fe8ef5a0d..4c9ad547a 100644 --- a/firmware-support/Cargo.lock +++ b/firmware-support/Cargo.lock @@ -103,6 +103,7 @@ dependencies = [ "tempfile", "test-strategy", "ufmt", + "zerocopy 0.7.35", ] [[package]] @@ -428,7 +429,7 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" dependencies = [ - "zerocopy", + "zerocopy 0.8.30", ] [[package]] @@ -880,13 +881,34 @@ version = "0.46.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59" +[[package]] +name = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive 0.7.35", +] + [[package]] name = "zerocopy" version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c" dependencies = [ - "zerocopy-derive", + "zerocopy-derive 0.8.30", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.111", ] [[package]] diff --git a/firmware-support/bittide-sys/Cargo.toml b/firmware-support/bittide-sys/Cargo.toml index 0e6b02b3c..6870636b0 100644 --- a/firmware-support/bittide-sys/Cargo.toml +++ b/firmware-support/bittide-sys/Cargo.toml @@ -22,6 +22,7 @@ ufmt = "0.2.0" bittide-hal = { path = "../bittide-hal" } itertools = { version = "0.14.0", default-features = false } crc = { version = "3.0", default-features = false } +zerocopy = { version = "0.7", default-features = false, features = ["byteorder", "derive"] } [dependencies.heapless] version = "0.8" diff --git a/firmware-support/bittide-sys/src/lib.rs b/firmware-support/bittide-sys/src/lib.rs index b6084b77d..f88e440ee 100644 --- a/firmware-support/bittide-sys/src/lib.rs +++ b/firmware-support/bittide-sys/src/lib.rs @@ -9,6 +9,7 @@ pub mod axi; pub mod callisto; pub mod link_startup; pub mod mac; +pub mod net_state; pub mod program_stream; pub mod sample_store; pub mod smoltcp; diff --git a/firmware-support/bittide-sys/src/net_state.rs b/firmware-support/bittide-sys/src/net_state.rs new file mode 100644 index 000000000..c7b154444 --- /dev/null +++ b/firmware-support/bittide-sys/src/net_state.rs @@ -0,0 +1,476 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +use zerocopy::byteorder::{I64, LE, U32}; +use zerocopy::{AsBytes, FromBytes, FromZeroes, Ref, Unaligned}; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum NodeRole { + Manager, + Subordinate, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ManagerState { + WaitForPhy, + SetupInterface, + WaitForSession, + SendWhoAreYou, + WaitHello, + SendUgnDump, + WaitUgnReport, + Done, + Failed, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SubordinateState { + WaitForPhy, + SetupInterface, + WaitForSession, + WaitWhoAreYou, + SendHello, + WaitUgnDump, + SendUgnReport, + Done, + Failed, +} + +pub const MAX_UGN_EDGES: usize = 2; +pub const UGN_EDGE_BYTES: usize = 32; + +const MSG_WHO_ARE_YOU: u8 = 1; +const MSG_HELLO: u8 = 2; +const MSG_UGN_DUMP: u8 = 3; +const MSG_UGN_REPORT: u8 = 4; +const ROLE_MANAGER: u8 = 0; +const ROLE_SUBORDINATE: u8 = 1; +const WHO_ARE_YOU_BYTES: usize = 8; +const HELLO_BYTES: usize = 8; +const UGN_DUMP_BYTES: usize = 1; +const UGN_REPORT_BYTES: usize = 2 + MAX_UGN_EDGES * UGN_EDGE_BYTES; + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct UgnEdge { + pub src_node: u32, + pub src_port: u32, + pub dst_node: u32, + pub dst_port: u32, + pub ugn: i64, + pub is_valid: u8, + pub _padding: [u8; 7], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct UgnReport { + pub count: u8, + pub _padding: [u8; 7], + pub edges: [UgnEdge; MAX_UGN_EDGES], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, FromZeroes, FromBytes, AsBytes, Unaligned)] +struct UgnEdgeWire { + src_node: U32, + src_port: U32, + dst_node: U32, + dst_port: U32, + ugn: I64, + is_valid: u8, + _padding: [u8; 7], +} + +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, FromZeroes, FromBytes, AsBytes, Unaligned)] +struct UgnReportWire { + msg_type: u8, + count: u8, + edges: [UgnEdgeWire; MAX_UGN_EDGES], +} + +impl From for UgnEdgeWire { + fn from(edge: UgnEdge) -> Self { + Self { + src_node: U32::new(edge.src_node), + src_port: U32::new(edge.src_port), + dst_node: U32::new(edge.dst_node), + dst_port: U32::new(edge.dst_port), + ugn: I64::new(edge.ugn), + is_valid: edge.is_valid, + _padding: [0; 7], + } + } +} + +impl From for UgnEdge { + fn from(edge: UgnEdgeWire) -> Self { + Self { + src_node: edge.src_node.get(), + src_port: edge.src_port.get(), + dst_node: edge.dst_node.get(), + dst_port: edge.dst_port.get(), + ugn: edge.ugn.get(), + is_valid: edge.is_valid, + _padding: [0; 7], + } + } +} + +const _: [(); UGN_EDGE_BYTES] = [(); core::mem::size_of::()]; +const _: [(); UGN_REPORT_BYTES] = [(); core::mem::size_of::()]; + +pub trait NetMedium { + fn phy_ready(&self, link: usize) -> bool; + fn setup_interface(&mut self, link: usize, local_ip: [u8; 4]); + fn connect(&mut self, link: usize, peer_ip: [u8; 4]) -> bool; + fn listen(&mut self, link: usize) -> bool; + fn send(&mut self, link: usize, data: &[u8]) -> bool; + fn recv(&mut self, link: usize, buf: &mut [u8]) -> Option; + fn timed_out(&self, link: usize) -> bool; +} + +pub struct Manager { + state: ManagerState, + retries: u8, + nonce_seed: u32, + nonce: u32, + report: Option, +} + +impl Manager { + pub fn new() -> Self { + Self { + state: ManagerState::WaitForPhy, + retries: 0, + nonce_seed: 1, + nonce: 0, + report: None, + } + } + + pub fn step(&mut self, medium: &mut M, link: usize) { + self.state = match self.state { + ManagerState::WaitForPhy => { + if medium.phy_ready(link) { + ManagerState::SetupInterface + } else { + self.state + } + } + ManagerState::SetupInterface => { + medium.setup_interface(link, ip_for_link(NodeRole::Manager, link)); + ManagerState::WaitForSession + } + ManagerState::WaitForSession => { + let peer_ip = ip_for_link(NodeRole::Subordinate, link); + if medium.connect(link, peer_ip) { + ManagerState::SendWhoAreYou + } else if medium.timed_out(link) { + self.bump_retry_or_fail(ManagerState::WaitForSession) + } else { + self.state + } + } + ManagerState::SendWhoAreYou => { + self.nonce = self.next_nonce(); + let msg = encode_who_are_you(self.nonce); + if medium.send(link, &msg) { + ManagerState::WaitHello + } else { + self.state + } + } + ManagerState::WaitHello => { + let mut buf = [0u8; HELLO_BYTES]; + if let Some(len) = medium.recv(link, &mut buf) { + if parse_hello(&buf[..len], self.nonce) { + ManagerState::SendUgnDump + } else { + self.state + } + } else if medium.timed_out(link) { + self.bump_retry_or_fail(ManagerState::SendWhoAreYou) + } else { + self.state + } + } + ManagerState::SendUgnDump => { + let msg = [MSG_UGN_DUMP]; + if medium.send(link, &msg) { + ManagerState::WaitUgnReport + } else { + self.state + } + } + ManagerState::WaitUgnReport => { + let mut buf = [0u8; UGN_REPORT_BYTES]; + if let Some(len) = medium.recv(link, &mut buf) { + if let Some(report) = parse_ugn_report(&buf[..len]) { + self.report = Some(report); + ManagerState::Done + } else { + self.state + } + } else if medium.timed_out(link) { + self.bump_retry_or_fail(ManagerState::SendUgnDump) + } else { + self.state + } + } + ManagerState::Done => self.state, + ManagerState::Failed => self.state, + }; + } + + pub fn is_done(&self) -> bool { + self.state == ManagerState::Done + } + + pub fn is_connected(&self) -> bool { + self.is_done() + } + + pub fn state(&self) -> ManagerState { + self.state + } + + pub fn report(&self) -> Option { + self.report + } + + fn bump_retry_or_fail(&mut self, retry_state: ManagerState) -> ManagerState { + self.retries = self.retries.saturating_add(1); + if self.retries >= 3 { + ManagerState::Failed + } else { + retry_state + } + } + + fn next_nonce(&mut self) -> u32 { + let next = self.nonce_seed; + self.nonce_seed = self.nonce_seed.wrapping_add(1); + next + } +} + +impl Default for Manager { + fn default() -> Self { + Self::new() + } +} + +pub struct Subordinate { + state: SubordinateState, + retries: u8, + nonce: u32, + report: UgnReport, +} + +impl Subordinate { + pub fn new() -> Self { + Self { + state: SubordinateState::WaitForPhy, + retries: 0, + nonce: 0, + report: UgnReport::default(), + } + } + + pub fn with_report(report: UgnReport) -> Self { + Self { + report, + ..Self::new() + } + } + + pub fn set_report(&mut self, report: UgnReport) { + self.report = report; + } + + pub fn step(&mut self, medium: &mut M, link: usize) { + self.state = match self.state { + SubordinateState::WaitForPhy => { + if medium.phy_ready(link) { + SubordinateState::SetupInterface + } else { + self.state + } + } + SubordinateState::SetupInterface => { + medium.setup_interface(link, ip_for_link(NodeRole::Subordinate, link)); + SubordinateState::WaitForSession + } + SubordinateState::WaitForSession => { + if medium.listen(link) { + SubordinateState::WaitWhoAreYou + } else if medium.timed_out(link) { + self.bump_retry_or_fail(SubordinateState::WaitForSession) + } else { + self.state + } + } + SubordinateState::WaitWhoAreYou => { + let mut buf = [0u8; WHO_ARE_YOU_BYTES]; + if let Some(len) = medium.recv(link, &mut buf) { + if let Some(nonce) = parse_who_are_you(&buf[..len]) { + self.nonce = nonce; + SubordinateState::SendHello + } else { + self.state + } + } else if medium.timed_out(link) { + self.bump_retry_or_fail(SubordinateState::WaitForSession) + } else { + self.state + } + } + SubordinateState::SendHello => { + let msg = encode_hello(self.nonce); + if medium.send(link, &msg) { + SubordinateState::WaitUgnDump + } else { + self.state + } + } + SubordinateState::WaitUgnDump => { + let mut buf = [0u8; UGN_DUMP_BYTES]; + if let Some(len) = medium.recv(link, &mut buf) { + if parse_ugn_dump(&buf[..len]) { + SubordinateState::SendUgnReport + } else { + self.state + } + } else if medium.timed_out(link) { + self.bump_retry_or_fail(SubordinateState::WaitUgnDump) + } else { + self.state + } + } + SubordinateState::SendUgnReport => { + let msg = encode_ugn_report(&self.report); + if medium.send(link, &msg) { + SubordinateState::Done + } else { + self.state + } + } + SubordinateState::Done => self.state, + SubordinateState::Failed => self.state, + }; + } + + pub fn is_done(&self) -> bool { + self.state == SubordinateState::Done + } + + pub fn is_connected(&self) -> bool { + self.is_done() + } + + pub fn state(&self) -> SubordinateState { + self.state + } + + fn bump_retry_or_fail(&mut self, retry_state: SubordinateState) -> SubordinateState { + self.retries = self.retries.saturating_add(1); + if self.retries >= 3 { + SubordinateState::Failed + } else { + retry_state + } + } +} + +impl Default for Subordinate { + fn default() -> Self { + Self::new() + } +} + +fn encode_who_are_you(nonce: u32) -> [u8; WHO_ARE_YOU_BYTES] { + let mut msg = [0u8; WHO_ARE_YOU_BYTES]; + msg[0] = MSG_WHO_ARE_YOU; + msg[1] = ROLE_MANAGER; + msg[2] = 0; + msg[3] = 0; + msg[4..8].copy_from_slice(&nonce.to_le_bytes()); + msg +} + +fn encode_hello(nonce: u32) -> [u8; HELLO_BYTES] { + let mut msg = [0u8; HELLO_BYTES]; + msg[0] = MSG_HELLO; + msg[1] = ROLE_SUBORDINATE; + msg[2] = 0; + msg[3] = 0; + msg[4..8].copy_from_slice(&nonce.to_le_bytes()); + msg +} + +fn parse_who_are_you(msg: &[u8]) -> Option { + if msg.len() < WHO_ARE_YOU_BYTES { + return None; + } + if msg[0] != MSG_WHO_ARE_YOU || msg[1] != ROLE_MANAGER { + return None; + } + Some(u32::from_le_bytes([msg[4], msg[5], msg[6], msg[7]])) +} + +fn parse_hello(msg: &[u8], nonce: u32) -> bool { + if msg.len() < HELLO_BYTES { + return false; + } + msg[0] == MSG_HELLO + && msg[1] == ROLE_SUBORDINATE + && u32::from_le_bytes([msg[4], msg[5], msg[6], msg[7]]) == nonce +} + +fn parse_ugn_dump(msg: &[u8]) -> bool { + msg.len() >= UGN_DUMP_BYTES && msg[0] == MSG_UGN_DUMP +} + +fn encode_ugn_report(report: &UgnReport) -> [u8; UGN_REPORT_BYTES] { + let mut wire = UgnReportWire { + msg_type: MSG_UGN_REPORT, + count: report.count, + edges: [UgnEdgeWire::default(); MAX_UGN_EDGES], + }; + for (idx, edge) in report.edges.iter().enumerate() { + wire.edges[idx] = (*edge).into(); + } + let mut buf = [0u8; UGN_REPORT_BYTES]; + buf.copy_from_slice(wire.as_bytes()); + buf +} + +fn parse_ugn_report(msg: &[u8]) -> Option { + let (wire, _) = Ref::<_, UgnReportWire>::new_from_prefix(msg)?; + if wire.msg_type != MSG_UGN_REPORT { + return None; + } + let count = wire.count as usize; + if count > MAX_UGN_EDGES { + return None; + } + let mut report = UgnReport { + count: wire.count, + ..Default::default() + }; + for idx in 0..count { + report.edges[idx] = wire.edges[idx].into(); + } + Some(report) +} + +pub fn ip_for_link(role: NodeRole, port: usize) -> [u8; 4] { + let host = match role { + NodeRole::Manager => 1, + NodeRole::Subordinate => 2, + }; + [10, 0, port as u8, host] +} diff --git a/firmware-support/bittide-sys/src/smoltcp.rs b/firmware-support/bittide-sys/src/smoltcp.rs index 4693cf4d0..88ef6092a 100644 --- a/firmware-support/bittide-sys/src/smoltcp.rs +++ b/firmware-support/bittide-sys/src/smoltcp.rs @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 pub mod axi; pub mod ringbuffer; +pub mod soft_ugn_ringbuffer; use smoltcp::wire::EthernetAddress; diff --git a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs index 91aae23e8..83dc2e9e8 100644 --- a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs +++ b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs @@ -31,7 +31,7 @@ use bittide_hal::hals::ringbuffer_test::devices::{ReceiveRingbuffer, TransmitRingbuffer}; use bittide_hal::manual_additions::ringbuffer_test::ringbuffers::AlignedReceiveBuffer; use crc::{Crc, CRC_32_ISCSI}; -use log::trace; +use log::{trace, warn}; use smoltcp::phy::{self, Device, DeviceCapabilities, Medium}; use smoltcp::time::Instant; @@ -138,17 +138,15 @@ impl Device for RingbufferDevice { // Check if this is the same packet we saw before (based on sequence number) if seq_num == self.last_rx_seq { - trace!("Detected repeated packet with seq {}", seq_num); + // trace!("Detected repeated packet with seq {}", seq_num); return None; } // Validate packet length if packet_len < MIN_IP_PACKET_SIZE || packet_len > self.mtu { - trace!( + warn!( "Invalid packet length: {} (must be {}-{})", - packet_len, - MIN_IP_PACKET_SIZE, - self.mtu + packet_len, MIN_IP_PACKET_SIZE, self.mtu ); return None; } diff --git a/firmware-support/bittide-sys/src/smoltcp/soft_ugn_ringbuffer.rs b/firmware-support/bittide-sys/src/smoltcp/soft_ugn_ringbuffer.rs new file mode 100644 index 000000000..4a14ffb62 --- /dev/null +++ b/firmware-support/bittide-sys/src/smoltcp/soft_ugn_ringbuffer.rs @@ -0,0 +1,300 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +//! smoltcp Device implementation for aligned soft-UGN ringbuffers. + +use bittide_hal::hals::soft_ugn_demo_mu::devices::{ReceiveRingbuffer, TransmitRingbuffer}; +use crc::{Crc, CRC_32_ISCSI}; +use log::trace; +use smoltcp::phy::{self, Device, DeviceCapabilities, Medium}; +use smoltcp::time::Instant; + +const ALIGNMENT_EMPTY: u64 = 0; +const ALIGNMENT_ANNOUNCE: u64 = 0xBADC0FFEE; +const ALIGNMENT_ACKNOWLEDGE: u64 = 0xDEADABBA; +const PACKET_HEADER_SIZE: usize = 8; +const MIN_IP_PACKET_SIZE: usize = 20; +const CRC: Crc = Crc::::new(&CRC_32_ISCSI); + +pub struct AlignedReceiveBuffer { + rx: ReceiveRingbuffer, + rx_alignment_offset: Option, + tx_reference: usize, +} + +impl AlignedReceiveBuffer { + pub fn new(rx: ReceiveRingbuffer) -> Self { + Self { + rx, + rx_alignment_offset: None, + tx_reference: 0, + } + } + + pub fn align(&mut self, tx: &TransmitRingbuffer) { + 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..TransmitRingbuffer::DATA_LEN { + tx.write_slice(&empty_pattern, i); + } + + let rx_offset = 'outer: loop { + for rx_idx in 0..ReceiveRingbuffer::DATA_LEN { + let mut data_buf = [[0u8; 8]; 1]; + self.rx.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; + } + } + }; + + let ack_pattern = [ALIGNMENT_ACKNOWLEDGE.to_le_bytes()]; + tx.write_slice(&ack_pattern, 0); + + loop { + let mut data_buf = [[0u8; 8]; 1]; + self.rx.read_slice(&mut data_buf, rx_offset); + let value = u64::from_le_bytes(data_buf[0]); + if value == ALIGNMENT_ACKNOWLEDGE { + break; + } + } + + self.rx_alignment_offset = Some(rx_offset); + self.tx_reference = tx.0 as *const _ as usize; + } + + pub fn is_aligned(&self) -> bool { + self.rx_alignment_offset.is_some() + } + + pub fn verify_aligned_to(&self, tx: &TransmitRingbuffer) -> bool { + self.is_aligned() && self.tx_reference == (tx.0 as *const _ as usize) + } + + pub fn get_alignment_reference(&self) -> usize { + self.tx_reference + } + + pub fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { + assert!(dst.len() + offset <= ReceiveRingbuffer::DATA_LEN); + let rx_offset = self + .rx_alignment_offset + .expect("Alignment offset not discovered yet. Call align() first."); + let mut aligned_offset = offset + rx_offset; + if aligned_offset >= ReceiveRingbuffer::DATA_LEN { + aligned_offset -= ReceiveRingbuffer::DATA_LEN; + } + read_slice_with_wrap(&self.rx, dst, aligned_offset) + } +} + +fn read_slice_with_wrap(rx: &ReceiveRingbuffer, dst: &mut [[u8; 8]], offset: usize) { + assert!(dst.len() <= ReceiveRingbuffer::DATA_LEN); + if dst.len() + offset <= ReceiveRingbuffer::DATA_LEN { + rx.read_slice(dst, offset); + } else { + let first_part_len = ReceiveRingbuffer::DATA_LEN - offset; + let (first, second) = dst.split_at_mut(first_part_len); + rx.read_slice(first, offset); + rx.read_slice(second, 0); + } +} + +fn is_valid(buffer: &[u8]) -> bool { + if buffer.len() < PACKET_HEADER_SIZE { + return false; + } + let stored_crc = u32::from_le_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]); + let calculated_crc = CRC.checksum(&buffer[4..]); + stored_crc == calculated_crc +} + +pub struct RingbufferDevice { + rx_buffer: AlignedReceiveBuffer, + tx_buffer: TransmitRingbuffer, + mtu: usize, + last_rx_seq: u16, + tx_seq_num: u16, +} + +impl RingbufferDevice { + pub fn new(rx_buffer: AlignedReceiveBuffer, tx_buffer: TransmitRingbuffer) -> Self { + let rx_bytes = ReceiveRingbuffer::DATA_LEN * 8; + let tx_bytes = TransmitRingbuffer::DATA_LEN * 8; + let mtu = rx_bytes.min(tx_bytes) - PACKET_HEADER_SIZE; + + if rx_buffer.is_aligned() { + assert!( + rx_buffer.verify_aligned_to(&tx_buffer), + "RX buffer is aligned but not to the provided TX buffer, expected reference {:p}, got 0x{:X}", + &tx_buffer.0, + rx_buffer.get_alignment_reference(), + ); + } + + Self { + rx_buffer, + tx_buffer, + mtu, + last_rx_seq: u16::MAX, + tx_seq_num: 0, + } + } + + pub fn mtu(&self) -> usize { + self.mtu + } +} + +impl Device for RingbufferDevice { + type RxToken<'a> = RxToken; + type TxToken<'a> = TxToken<'a>; + + fn capabilities(&self) -> DeviceCapabilities { + let mut cap = DeviceCapabilities::default(); + cap.max_transmission_unit = self.mtu; + cap.medium = Medium::Ip; + cap + } + + fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { + let mut header_buf: [[u8; 8]; 1] = [[0u8; 8]; 1]; + self.rx_buffer.read_slice(&mut header_buf, 0); + + let header_ptr = header_buf[0].as_ptr(); + let seq_num = unsafe { (header_ptr.add(4) as *const u16).read_unaligned() }; + let packet_len = unsafe { (header_ptr.add(6) as *const u16).read_unaligned() } as usize; + + if seq_num == self.last_rx_seq { + trace!("Detected repeated packet with seq {}", seq_num); + return None; + } + + if packet_len < MIN_IP_PACKET_SIZE || packet_len > self.mtu { + trace!( + "Invalid packet length: {} (must be {}-{})", + packet_len, + MIN_IP_PACKET_SIZE, + self.mtu + ); + return None; + } + + let total_len = PACKET_HEADER_SIZE + packet_len; + let num_words = total_len.div_ceil(8); + let mut packet_buffer = [0u8; ReceiveRingbuffer::DATA_LEN * 8]; + + let word_slice = unsafe { + core::slice::from_raw_parts_mut(packet_buffer.as_mut_ptr() as *mut [u8; 8], num_words) + }; + + self.rx_buffer.read_slice(word_slice, 0); + + if !is_valid(&packet_buffer[..total_len]) { + trace!("CRC validation failed for packet seq {}", seq_num); + return None; + } + + trace!( + "Valid packet: seq {}, payload {} bytes", + seq_num, + packet_len + ); + self.last_rx_seq = seq_num; + + let mut payload = [0u8; ReceiveRingbuffer::DATA_LEN * 8]; + payload[..packet_len] + .copy_from_slice(&packet_buffer[PACKET_HEADER_SIZE..PACKET_HEADER_SIZE + packet_len]); + + let rx = RxToken { + buffer: payload, + length: packet_len, + }; + let tx = TxToken { + tx_buffer: &mut self.tx_buffer, + mtu: self.mtu, + seq_num: &mut self.tx_seq_num, + }; + Some((rx, tx)) + } + + fn transmit(&mut self, _timestamp: Instant) -> Option> { + Some(TxToken { + tx_buffer: &mut self.tx_buffer, + mtu: self.mtu, + seq_num: &mut self.tx_seq_num, + }) + } +} + +pub struct RxToken { + buffer: [u8; ReceiveRingbuffer::DATA_LEN * 8], + length: usize, +} + +impl phy::RxToken for RxToken { + fn consume(self, f: F) -> R + where + F: FnOnce(&[u8]) -> R, + { + trace!("Consuming validated packet ({} bytes)", self.length); + f(&self.buffer[..self.length]) + } +} + +pub struct TxToken<'a> { + tx_buffer: &'a mut TransmitRingbuffer, + mtu: usize, + seq_num: &'a mut u16, +} + +impl phy::TxToken for TxToken<'_> { + fn consume(self, len: usize, f: F) -> R + where + F: FnOnce(&mut [u8]) -> R, + { + assert!( + len <= self.mtu, + "Packet length {} exceeds MTU {}", + len, + self.mtu + ); + + let mut buffer = [0u8; TransmitRingbuffer::DATA_LEN * 8]; + let header_ptr = buffer.as_mut_ptr(); + unsafe { + (header_ptr.add(4) as *mut u16).write_unaligned(self.seq_num.to_le()); + (header_ptr.add(6) as *mut u16).write_unaligned((len as u16).to_le()); + } + + let result = f(&mut buffer[PACKET_HEADER_SIZE..PACKET_HEADER_SIZE + len]); + + let total_len = PACKET_HEADER_SIZE + len; + let crc = CRC.checksum(&buffer[4..total_len]); + unsafe { + (header_ptr as *mut u32).write_unaligned(crc.to_le()); + } + + let num_words = total_len.div_ceil(8); + let word_slice = + unsafe { core::slice::from_raw_parts(buffer.as_ptr() as *const [u8; 8], num_words) }; + + self.tx_buffer.write_slice(word_slice, 0); + + trace!( + "Transmitted packet: checksum {}, seq {}, total length {}", + crc, + self.seq_num, + total_len, + ); + *self.seq_num = self.seq_num.wrapping_add(1); + + result + } +} From e024fcf16cefa8114ba3014f866e32b9290662ff Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Mon, 9 Mar 2026 14:05:42 +0100 Subject: [PATCH 29/70] Add `waitForLineAny` --- bittide-extra/src/Project/Chan.hs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/bittide-extra/src/Project/Chan.hs b/bittide-extra/src/Project/Chan.hs index 42a74b8b5..5f9b377ac 100644 --- a/bittide-extra/src/Project/Chan.hs +++ b/bittide-extra/src/Project/Chan.hs @@ -5,6 +5,7 @@ module Project.Chan where import Prelude hiding (filter) +import Control.Concurrent.Async (async, waitAnyCancel) import Control.Concurrent.Chan import Data.ByteString (ByteString) import Debug.Trace @@ -46,3 +47,15 @@ Do not use on Handles that might return non-ASCII characters. -} readUntilLine :: Chan ByteString -> String -> IO [String] readUntilLine h = readUntilLineWith h readChan + +{- | Wait for a line to appear on any channel and return its index. +Only use in combination with sensible timeouts. +-} +waitForLineAny :: (HasCallStack) => [Chan ByteString] -> String -> IO Int +waitForLineAny chans expected = do + asyncs <- + mapM + (\(idx, chan) -> async $ waitForLine chan expected >> pure idx) + (zip [0 :: Int ..] chans) + (_, idx) <- waitAnyCancel asyncs + pure idx From b6e9bafb05c75cb9414489aaee2fe7ba3aa76006 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Mon, 9 Mar 2026 14:06:01 +0100 Subject: [PATCH 30/70] Add inbound UGN collection test --- .../Instances/Hitl/SoftUgnDemo/Driver.hs | 101 +++- .../Instances/Hitl/SoftUgnDemo/TopEntity.hs | 4 +- firmware-binaries/Cargo.lock | 13 + firmware-binaries/Cargo.toml | 1 + .../demos/soft-ugn-demo-mu-2/Cargo.toml | 28 + .../demos/soft-ugn-demo-mu-2/build.rs | 10 + .../demos/soft-ugn-demo-mu-2/src/main.rs | 313 ++++++++++ .../ringbuffer_smoltcp_test/src/main.rs | 232 ++------ firmware-support/bittide-sys/src/net_state.rs | 536 ++++++++++++------ 9 files changed, 866 insertions(+), 372 deletions(-) create mode 100644 firmware-binaries/demos/soft-ugn-demo-mu-2/Cargo.toml create mode 100644 firmware-binaries/demos/soft-ugn-demo-mu-2/build.rs create mode 100644 firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs index d3668fb8f..20ac51ede 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs @@ -131,17 +131,6 @@ driver testName targets = do hardwareRoundtrips = calculateRoundtripLatencies $ L.concat hardwareUgns _ <- liftIO $ do putStrLn "\n=== Hardware UGN Roundtrip Latencies ===" - mapM print hardwareRoundtrips - liftIO - $ T.tryWithTimeoutOn - T.PrintActionTime - "Waiting for calendar initialization" - (30_000_000) - goDumpCcSamples - $ forConcurrently_ picocoms - $ \pico -> - waitForLine pico "[MU] All calendars initialized" - softwareUgnsPerNode <- liftIO $ T.tryWithTimeoutOn @@ -222,3 +211,93 @@ driver testName targets = do liftIO goDumpCcSamples pure ExitSuccess + +driver2 :: + (HasCallStack) => + String -> + [(HwTarget, DeviceInfo)] -> + VivadoM ExitCode +driver2 testName targets = do + liftIO + . putStrLn + $ "Running driver function for targets " + <> show ((\(_, info) -> info.deviceId) <$> targets) + + projectDir <- liftIO $ findParentContaining "cabal.project" + let hitlDir = projectDir "_build/hitl" testName + + forM_ targets (assertProbe "probe_test_start") + + let + -- BOOT / MU / CC IDs + expectedJtagIds = [0x0514C001, 0x1514C001, 0x2514C001] + toInitArgs (_, deviceInfo) targetIndex = + Ocd.InitOpenOcdArgs{deviceInfo, expectedJtagIds, hitlDir, targetIndex} + initArgs = L.zipWith toInitArgs targets [0 ..] + optionalBootInitArgs = L.repeat def{Ocd.logPrefix = "boot-", Ocd.initTcl = "vexriscv_boot_init.tcl"} + openOcdBootStarts = liftIO <$> L.zipWith Ocd.initOpenOcd initArgs optionalBootInitArgs + + let picocomStarts = liftIO <$> L.zipWith (initPicocom hitlDir) targets [0 ..] + brackets picocomStarts (liftIO . snd) $ \(L.map fst -> picocoms) -> do + -- Start OpenOCD that will program the boot CPU + brackets openOcdBootStarts (liftIO . (.cleanup)) $ \initOcdsData -> do + let bootTapInfos = parseBootTapInfo <$> initOcdsData + + Gdb.withGdbs (L.length targets) $ \bootGdbs -> do + liftIO + $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo1-boot") bootGdbs bootTapInfos targets + liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) bootGdbs + liftIO $ mapConcurrently_ Gdb.continue bootGdbs + liftIO + $ T.tryWithTimeout T.PrintActionTime "Waiting for done" 60_000_000 + $ forConcurrently_ picocoms + $ \pico -> + waitForLine pico "[BT] Going into infinite loop.." + + let + optionalInitArgs = L.repeat def + openOcdStarts = liftIO <$> L.zipWith Ocd.initOpenOcd initArgs optionalInitArgs + + -- Start OpenOCD instances for all CPUs + brackets openOcdStarts (liftIO . (.cleanup)) $ \initOcdsData -> do + let + allTapInfos = parseTapInfo expectedJtagIds <$> initOcdsData + + _bootTapInfos, muTapInfos, ccTapInfos :: [Ocd.TapInfo] + (_bootTapInfos, muTapInfos, ccTapInfos) + | all (== L.length expectedJtagIds) (L.length <$> allTapInfos) + , [boots, mus, ccs] <- L.transpose allTapInfos = + (boots, mus, ccs) + | otherwise = + error + $ "Unexpected number of OpenOCD taps initialized. Expected: " + <> show (L.length expectedJtagIds) + <> ", but got: " + <> show (L.length <$> allTapInfos) + + Gdb.withGdbs (L.length targets) $ \ccGdbs -> do + liftIO $ zipWithConcurrently3_ (initGdb hitlDir "clock-control") ccGdbs ccTapInfos targets + liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) ccGdbs + + Gdb.withGdbs (L.length targets) $ \muGdbs -> do + liftIO $ zipWithConcurrently3_ (initGdb hitlDir "soft-ugn-demo-mu-2") muGdbs muTapInfos targets + liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) muGdbs + + brackets picocomStarts (liftIO . snd) $ \(L.map fst -> picocoms) -> do + let goDumpCcSamples = dumpCcSamples hitlDir (defCcConf (natToNum @FpgaCount)) ccGdbs + liftIO $ mapConcurrently_ Gdb.continue ccGdbs + liftIO $ mapConcurrently_ Gdb.continue muGdbs + + liftIO + $ T.tryWithTimeoutOn + T.PrintActionTime + "Waiting for CPU test status" + (180_000_000) + goDumpCcSamples + $ forConcurrently_ picocoms + $ \pico -> + waitForLine pico "[MU] Demo complete." + + liftIO goDumpCcSamples + + pure ExitSuccess diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/TopEntity.hs b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/TopEntity.hs index 264ac9b30..bf5a4a806 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/TopEntity.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/TopEntity.hs @@ -109,11 +109,11 @@ tests = , externalHdl = [] , testCases = [ HitlTestCase - { name = "Bittide_Demo_DUT" + { name = "Bittide_Demo_DUT_mu2" , parameters = paramForHwTargets allHwTargets () , postProcData = () } ] - , mDriverProc = Just Driver.driver + , mDriverProc = Just Driver.driver2 , mPostProc = Nothing } diff --git a/firmware-binaries/Cargo.lock b/firmware-binaries/Cargo.lock index 1f5e2113d..72501e3bc 100644 --- a/firmware-binaries/Cargo.lock +++ b/firmware-binaries/Cargo.lock @@ -750,6 +750,19 @@ dependencies = [ "ufmt", ] +[[package]] +name = "soft-ugn-demo-mu-2" +version = "0.1.0" +dependencies = [ + "bittide-hal", + "bittide-sys", + "log", + "memmap-generate", + "riscv-rt 0.11.0", + "smoltcp 0.12.0", + "ufmt", +] + [[package]] name = "soft-ugn-mu" version = "0.1.0" diff --git a/firmware-binaries/Cargo.toml b/firmware-binaries/Cargo.toml index 38554b178..45a69b90a 100644 --- a/firmware-binaries/Cargo.toml +++ b/firmware-binaries/Cargo.toml @@ -42,6 +42,7 @@ members = [ "demos/clock-control", "demos/soft-ugn-mu", + "demos/soft-ugn-demo-mu-2", "demos/switch-demo1-boot", "demos/switch-demo1-mu", "demos/switch-demo2-mu", diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/Cargo.toml b/firmware-binaries/demos/soft-ugn-demo-mu-2/Cargo.toml new file mode 100644 index 000000000..3b04d9cc7 --- /dev/null +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/Cargo.toml @@ -0,0 +1,28 @@ +# SPDX-FileCopyrightText: 2026 Google LLC +# +# SPDX-License-Identifier: CC0-1.0 + +[package] +name = "soft-ugn-demo-mu-2" +version = "0.1.0" +edition = "2021" +license = "Apache-2.0" +authors = ["Google LLC"] + +[dependencies] +riscv-rt = "0.11.0" +bittide-sys = { path = "../../../firmware-support/bittide-sys" } +bittide-hal = { path = "../../../firmware-support/bittide-hal" } +ufmt = "0.2.0" + +[dependencies.smoltcp] +version = "0.12.0" +default-features = false +features = ["medium-ip", "medium-ethernet", "proto-ipv4", "socket-tcp"] + +[dependencies.log] +version = "0.4.21" +features = ["max_level_trace", "release_max_level_trace"] + +[build-dependencies] +memmap-generate = { path = "../../../firmware-support/memmap-generate" } diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/build.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/build.rs new file mode 100644 index 000000000..568ae2efb --- /dev/null +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/build.rs @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +use memmap_generate::build_utils::standard_memmap_build; + +fn main() { + standard_memmap_build("SoftUgnDemoMu.json", "DataMemory", "InstructionMemory"); + println!("cargo:rerun-if-changed=build.rs"); +} diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs new file mode 100644 index 000000000..4f91a095e --- /dev/null +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs @@ -0,0 +1,313 @@ +#![no_std] +#![cfg_attr(not(test), no_main)] +#![feature(sync_unsafe_cell)] + +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +use bittide_hal::hals::soft_ugn_demo_mu::devices::{ReceiveRingbuffer, TransmitRingbuffer}; +use bittide_hal::hals::soft_ugn_demo_mu::DeviceInstances; +use bittide_hal::manual_additions::timer::Instant; +use bittide_sys::link_startup::LinkStartup; +use bittide_sys::net_state::{Manager, SmoltcpLink, Subordinate, UgnEdge, UgnReport}; +use bittide_sys::smoltcp::soft_ugn_ringbuffer::{AlignedReceiveBuffer, RingbufferDevice}; +use bittide_sys::stability_detector::Stability; +use core::panic::PanicInfo; +use log::{info, trace, warn, LevelFilter}; +use smoltcp::iface::{Config, Interface, SocketHandle, SocketSet, SocketStorage}; +use smoltcp::socket::tcp; +use smoltcp::wire::HardwareAddress; +use ufmt::uwriteln; + +const INSTANCES: DeviceInstances = unsafe { DeviceInstances::new() }; +const LINK_COUNT: usize = 7; +const TCP_BUF_SIZE: usize = 256; +const MANAGER_DNA: [u8; 12] = [0; 12]; + +static mut TCP_RX_BUFS: [[u8; TCP_BUF_SIZE]; LINK_COUNT] = [[0; TCP_BUF_SIZE]; LINK_COUNT]; +static mut TCP_TX_BUFS: [[u8; TCP_BUF_SIZE]; LINK_COUNT] = [[0; TCP_BUF_SIZE]; LINK_COUNT]; + +#[cfg(not(test))] +use riscv_rt::entry; + +fn to_smoltcp_instant(instant: Instant) -> smoltcp::time::Instant { + smoltcp::time::Instant::from_micros(instant.micros() as i64) +} + +fn socket_set<'a>(storage: &'a mut [SocketStorage<'static>]) -> SocketSet<'a> { + // SAFETY: Socket buffers are backed by static memory, and SocketSet does not + // outlive the borrow of storage in this scope. + let storage: &'a mut [SocketStorage<'a>] = unsafe { core::mem::transmute(storage) }; + SocketSet::new(storage) +} + +fn make_device(rx: ReceiveRingbuffer, tx: TransmitRingbuffer) -> RingbufferDevice { + let mut rx_aligned = AlignedReceiveBuffer::new(rx); + rx_aligned.align(&tx); + RingbufferDevice::new(rx_aligned, tx) +} + +#[cfg_attr(not(test), entry)] +fn main() -> ! { + let mut uart = INSTANCES.uart; + unsafe { + use bittide_sys::uart::log::LOGGER; + let logger = &mut (*LOGGER.get()); + logger.set_logger(uart.clone()); + logger.set_timer(INSTANCES.timer); + logger.display_source = LevelFilter::Warn; + log::set_logger_racy(logger).ok(); + log::set_max_level_racy(LevelFilter::Trace); + } + + info!("=== Soft UGN Demo MU2 ==="); + let transceivers = &INSTANCES.transceivers; + let cc = INSTANCES.clock_control; + let elastic_buffers = [ + &INSTANCES.elastic_buffer_0, + &INSTANCES.elastic_buffer_1, + &INSTANCES.elastic_buffer_2, + &INSTANCES.elastic_buffer_3, + &INSTANCES.elastic_buffer_4, + &INSTANCES.elastic_buffer_5, + &INSTANCES.elastic_buffer_6, + ]; + let capture_ugns = [ + INSTANCES.capture_ugn_0, + INSTANCES.capture_ugn_1, + INSTANCES.capture_ugn_2, + INSTANCES.capture_ugn_3, + INSTANCES.capture_ugn_4, + INSTANCES.capture_ugn_5, + INSTANCES.capture_ugn_6, + ]; + + // Pseudocode setup: + // 1) Initialize MU peripherals and scatter/gather calendars for ringbuffers. + // 2) Align ringbuffers on all ports (two-phase protocol). + // 3) Run LinkStartup per port to bring up physical links and capture UGNs. + // 4) Wait for clock stability; stop auto-centering and record EB deltas. + // 5) For each port, create RingbufferDevice + smoltcp Interface with static IP. + // 6) Run manager state machines to connect to neighbors and request UGNs. + // 7) Collect UGN edges over TCP and aggregate locally. + + info!("Bringing up links..."); + let mut link_startups = [LinkStartup::new(); LINK_COUNT]; + while !link_startups.iter().all(|ls| ls.is_done()) { + for (i, link_startup) in link_startups.iter_mut().enumerate() { + link_startup.next( + transceivers, + i, + elastic_buffers[i], + capture_ugns[i].has_captured(), + ); + } + } + + info!("Waiting for stability..."); + loop { + let stability = Stability { + stable: cc.links_stable()[0], + settled: 0, + }; + if stability.all_stable() { + break; + } + } + + info!("Stopping auto-centering..."); + elastic_buffers + .iter() + .for_each(|eb| eb.set_auto_center_enable(false)); + elastic_buffers + .iter() + .for_each(|eb| eb.wait_auto_center_idle()); + let eb_deltas = elastic_buffers + .iter() + .map(|eb| eb.auto_center_total_adjustments()); + + for (capture_ugn, eb_delta) in capture_ugns.iter().zip(eb_deltas) { + capture_ugn.set_elastic_buffer_delta(eb_delta); + } + + info!("Captured hardware UGNs"); + for (i, capture_ugn) in capture_ugns.iter().enumerate() { + info!( + "Capture UGN {}: local = {}, remote = {}", + i, + capture_ugn.local_counter(), + capture_ugn.remote_counter() + ); + } + + let mut devices: [RingbufferDevice; LINK_COUNT] = [ + make_device( + INSTANCES.receive_ringbuffer_0, + INSTANCES.transmit_ringbuffer_0, + ), + make_device( + INSTANCES.receive_ringbuffer_1, + INSTANCES.transmit_ringbuffer_1, + ), + make_device( + INSTANCES.receive_ringbuffer_2, + INSTANCES.transmit_ringbuffer_2, + ), + make_device( + INSTANCES.receive_ringbuffer_3, + INSTANCES.transmit_ringbuffer_3, + ), + make_device( + INSTANCES.receive_ringbuffer_4, + INSTANCES.transmit_ringbuffer_4, + ), + make_device( + INSTANCES.receive_ringbuffer_5, + INSTANCES.transmit_ringbuffer_5, + ), + make_device( + INSTANCES.receive_ringbuffer_6, + INSTANCES.transmit_ringbuffer_6, + ), + ]; + let mut ifaces: [Interface; LINK_COUNT] = core::array::from_fn(|idx| { + let now = to_smoltcp_instant(INSTANCES.timer.now()); + let mut iface = Interface::new(Config::new(HardwareAddress::Ip), &mut devices[idx], now); + iface.update_ip_addrs(|addrs| { + addrs.clear(); + }); + iface + }); + let mut sockets_storage: [[SocketStorage<'static>; 1]; LINK_COUNT] = + core::array::from_fn(|_| Default::default()); + let socket_handles: [SocketHandle; LINK_COUNT] = core::array::from_fn(|idx| { + let rx_buf = unsafe { &mut TCP_RX_BUFS[idx][..] }; + let tx_buf = unsafe { &mut TCP_TX_BUFS[idx][..] }; + let socket = tcp::Socket::new( + tcp::SocketBuffer::new(rx_buf), + tcp::SocketBuffer::new(tx_buf), + ); + let mut sockets = socket_set(&mut sockets_storage[idx][..]); + sockets.add(socket) + }); + + let dna = INSTANCES.dna.dna(); + info!("My dna: {:?}", dna); + let is_manager = dna == MANAGER_DNA; + info!( + "Role: {}", + if is_manager { "manager" } else { "subordinate" } + ); + + if is_manager { + info!("Starting manager state machines..."); + let mut managers: [Manager; LINK_COUNT] = core::array::from_fn(|_| Manager::new()); + let mut done = [false; LINK_COUNT]; + + loop { + let now = to_smoltcp_instant(INSTANCES.timer.now()); + for link in 0..LINK_COUNT { + let mut sockets = socket_set(&mut sockets_storage[link][..]); + let _ = ifaces[link].poll(now, &mut devices[link], &mut sockets); + let mut smoltcp_link = SmoltcpLink::new( + &mut ifaces[link], + &mut sockets, + socket_handles[link], + link, + true, + false, + ); + managers[link].step(&mut smoltcp_link); + if managers[link].is_done() { + done[link] = true; + } + } + + if done.iter().all(|v| *v) { + break; + } + } + + info!("UGN reports from subordinates:"); + for (idx, manager) in managers.iter().enumerate() { + if let Some(report) = manager.report() { + info!("Link {}: {} edges", idx, report.count); + for (edge_idx, edge) in report.edges.iter().enumerate() { + if edge_idx >= report.count as usize { + break; + } + if let Some(edge) = edge { + info!( + " Edge {}: {}:{} -> {}:{}, ugn={}", + edge_idx, + edge.src_node, + edge.src_port, + edge.dst_node, + edge.dst_port, + edge.ugn + ); + } else { + warn!(" Edge {}: missing", edge_idx); + } + } + } else { + warn!("Link {}: no report", idx); + } + } + } else { + info!("Starting subordinate state machines..."); + let mut subordinates: [Subordinate; LINK_COUNT] = + core::array::from_fn(|_| Subordinate::new()); + for link in 0..LINK_COUNT { + subordinates[link].set_report(build_report_for_link(link, &capture_ugns[link], &dna)); + } + + loop { + let now = to_smoltcp_instant(INSTANCES.timer.now()); + for link in 0..LINK_COUNT { + let mut sockets = socket_set(&mut sockets_storage[link][..]); + let _ = ifaces[link].poll(now, &mut devices[link], &mut sockets); + let mut smoltcp_link = SmoltcpLink::new( + &mut ifaces[link], + &mut sockets, + socket_handles[link], + link, + true, + false, + ); + subordinates[link].step(&mut smoltcp_link); + } + } + } + + uwriteln!(uart, "Demo complete.").unwrap(); + loop { + continue; + } +} + +#[panic_handler] +fn panic_handler(_: &PanicInfo) -> ! { + loop { + continue; + } +} + +fn build_report_for_link( + link: usize, + capture_ugn: &bittide_hal::shared_devices::CaptureUgn, + dna: &[u8; 12], +) -> UgnReport { + let mut report = UgnReport::new(); + report.count = 1; + report.edges[0] = Some(UgnEdge { + src_node: dna[0] as u32, + src_port: link as u32, + dst_node: 0, + dst_port: link as u32, + ugn: capture_ugn.local_counter() as i64, + }); + trace!("Prepared report for link {}", link); + report +} diff --git a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs index 853397315..4edf36059 100644 --- a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs +++ b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs @@ -8,13 +8,13 @@ use bittide_hal::manual_additions::ringbuffer_test::ringbuffers::AlignedReceiveBuffer; use bittide_hal::manual_additions::timer::Instant; use bittide_hal::ringbuffer_test::DeviceInstances; -use bittide_sys::net_state::{Manager, NetMedium, Subordinate, UgnEdge, UgnReport}; +use bittide_sys::net_state::{Manager, SmoltcpLink, Subordinate, UgnEdge, UgnReport}; use bittide_sys::smoltcp::ringbuffer::RingbufferDevice; use core::fmt::Write; use log::{info, trace, LevelFilter}; -use smoltcp::iface::{Config, Interface, SocketHandle, SocketSet, SocketStorage}; +use smoltcp::iface::{Config, Interface, SocketSet, SocketStorage}; use smoltcp::socket::tcp; -use smoltcp::wire::{HardwareAddress, IpAddress, IpCidr}; +use smoltcp::wire::HardwareAddress; use ufmt::uwriteln; #[cfg(not(test))] @@ -22,167 +22,10 @@ use riscv_rt::entry; const INSTANCES: DeviceInstances = unsafe { DeviceInstances::new() }; -const SERVER_PORT: u16 = 8080; -const CLIENT_PORT: u16 = 49152; - fn to_smoltcp_instant(instant: Instant) -> smoltcp::time::Instant { smoltcp::time::Instant::from_micros(instant.micros() as i64) } -struct SmoltcpManagerMedium<'a, 'b> { - iface: &'a mut Interface, - sockets: &'a mut SocketSet<'b>, - client_handle: SocketHandle, -} - -impl<'a, 'b> SmoltcpManagerMedium<'a, 'b> { - fn new( - iface: &'a mut Interface, - sockets: &'a mut SocketSet<'b>, - client_handle: SocketHandle, - ) -> Self { - Self { - iface, - sockets, - client_handle, - } - } -} - -impl NetMedium for SmoltcpManagerMedium<'_, '_> { - fn phy_ready(&self, _link: usize) -> bool { - true - } - - fn setup_interface(&mut self, _link: usize, local_ip: [u8; 4]) { - let ip = IpCidr::new( - IpAddress::v4(local_ip[0], local_ip[1], local_ip[2], local_ip[3]), - 24, - ); - self.iface.update_ip_addrs(|addrs| { - if !addrs.contains(&ip) { - let _ = addrs.push(ip); - } - }); - } - - fn connect(&mut self, _link: usize, peer_ip: [u8; 4]) -> bool { - let client = self.sockets.get_mut::(self.client_handle); - if !client.is_open() && !client.is_active() { - let cx = self.iface.context(); - let _ = client.connect( - cx, - ( - IpAddress::v4(peer_ip[0], peer_ip[1], peer_ip[2], peer_ip[3]), - SERVER_PORT, - ), - CLIENT_PORT, - ); - } - client.is_active() - } - - fn listen(&mut self, _link: usize) -> bool { - false - } - - fn send(&mut self, _link: usize, data: &[u8]) -> bool { - let client = self.sockets.get_mut::(self.client_handle); - if client.can_send() { - let _ = client.send_slice(data); - return true; - } - false - } - - fn recv(&mut self, _link: usize, buf: &mut [u8]) -> Option { - let client = self.sockets.get_mut::(self.client_handle); - if client.can_recv() { - if let Ok(len) = client.recv_slice(buf) { - return Some(len); - } - } - None - } - - fn timed_out(&self, _link: usize) -> bool { - false - } -} - -struct SmoltcpSubordinateMedium<'a, 'b> { - iface: &'a mut Interface, - sockets: &'a mut SocketSet<'b>, - server_handle: SocketHandle, -} - -impl<'a, 'b> SmoltcpSubordinateMedium<'a, 'b> { - fn new( - iface: &'a mut Interface, - sockets: &'a mut SocketSet<'b>, - server_handle: SocketHandle, - ) -> Self { - Self { - iface, - sockets, - server_handle, - } - } -} - -impl NetMedium for SmoltcpSubordinateMedium<'_, '_> { - fn phy_ready(&self, _link: usize) -> bool { - true - } - - fn setup_interface(&mut self, _link: usize, local_ip: [u8; 4]) { - let ip = IpCidr::new( - IpAddress::v4(local_ip[0], local_ip[1], local_ip[2], local_ip[3]), - 24, - ); - self.iface.update_ip_addrs(|addrs| { - if !addrs.contains(&ip) { - let _ = addrs.push(ip); - } - }); - } - - fn connect(&mut self, _link: usize, _peer_ip: [u8; 4]) -> bool { - false - } - - fn listen(&mut self, _link: usize) -> bool { - let server = self.sockets.get_mut::(self.server_handle); - if !server.is_open() { - let _ = server.listen(SERVER_PORT); - } - server.is_open() - } - - fn send(&mut self, _link: usize, data: &[u8]) -> bool { - let server = self.sockets.get_mut::(self.server_handle); - if server.can_send() { - let _ = server.send_slice(data); - return true; - } - false - } - - fn recv(&mut self, _link: usize, buf: &mut [u8]) -> Option { - let server = self.sockets.get_mut::(self.server_handle); - if server.can_recv() { - if let Ok(len) = server.recv_slice(buf) { - return Some(len); - } - } - None - } - - fn timed_out(&self, _link: usize) -> bool { - false - } -} - #[cfg_attr(not(test), entry)] fn main() -> ! { let mut uart = INSTANCES.uart; @@ -196,7 +39,7 @@ fn main() -> ! { logger.set_timer(INSTANCES.timer); logger.display_source = LevelFilter::Warn; log::set_logger_racy(logger).ok(); - log::set_max_level_racy(LevelFilter::Trace); + log::set_max_level_racy(LevelFilter::Info); } info!("=== Ringbuffer smoltcp Loopback Test ==="); @@ -265,12 +108,16 @@ fn main() -> ! { let timestamp = to_smoltcp_instant(timer.now()); iface.poll(timestamp, &mut device, &mut sockets); - let mut manager_medium = SmoltcpManagerMedium::new(&mut iface, &mut sockets, client_handle); - manager.step(&mut manager_medium, 0); - - let mut subordinate_medium = - SmoltcpSubordinateMedium::new(&mut iface, &mut sockets, server_handle); - subordinate.step(&mut subordinate_medium, 0); + { + let mut link = + SmoltcpLink::new(&mut iface, &mut sockets, client_handle, 0, true, false); + manager.step(&mut link); + } + { + let mut link = + SmoltcpLink::new(&mut iface, &mut sockets, server_handle, 0, true, false); + subordinate.step(&mut link); + } if manager.is_done() && subordinate.is_done() && !done_logged { info!(" Manager collected UGN report"); @@ -290,16 +137,14 @@ fn main() -> ! { if idx >= report.count as usize { break; } - info!( - " Edge {}: {}:{} -> {}:{}, ugn={}, valid={}", - idx, - edge.src_node, - edge.src_port, - edge.dst_node, - edge.dst_port, - edge.ugn, - edge.is_valid - ); + if let Some(edge) = edge { + info!( + " Edge {}: {}:{} -> {}:{}, ugn={}", + idx, edge.src_node, edge.src_port, edge.dst_node, edge.dst_port, edge.ugn + ); + } else { + info!(" Edge {}: missing", idx); + } } } else { info!(" FAILURE: Missing UGN report data!"); @@ -313,28 +158,17 @@ fn main() -> ! { } fn build_placeholder_report() -> UgnReport { - let mut report = UgnReport { - count: 2, - ..Default::default() - }; - report.edges[0] = UgnEdge { - src_node: 1, - src_port: 0, - dst_node: 0, - dst_port: 0, - ugn: 123, - is_valid: 1, - _padding: [0; 7], - }; - report.edges[1] = UgnEdge { - src_node: 1, - src_port: 1, - dst_node: 0, - dst_port: 1, - ugn: 456, - is_valid: 1, - _padding: [0; 7], - }; + let mut report = UgnReport::new(); + report.count = 8; + for idx in 0..8 { + report.edges[idx] = Some(UgnEdge { + src_node: idx as u32, + src_port: idx as u32, + dst_node: (idx as u32).saturating_add(1), + dst_port: idx as u32, + ugn: 100 + idx as i64, + }); + } report } diff --git a/firmware-support/bittide-sys/src/net_state.rs b/firmware-support/bittide-sys/src/net_state.rs index c7b154444..1acd6f01a 100644 --- a/firmware-support/bittide-sys/src/net_state.rs +++ b/firmware-support/bittide-sys/src/net_state.rs @@ -2,6 +2,10 @@ // // SPDX-License-Identifier: Apache-2.0 +use log::{debug, info, trace, warn}; +use smoltcp::iface::{Interface, SocketHandle, SocketSet}; +use smoltcp::socket::tcp; +use smoltcp::wire::{IpAddress, IpCidr}; use zerocopy::byteorder::{I64, LE, U32}; use zerocopy::{AsBytes, FromBytes, FromZeroes, Ref, Unaligned}; @@ -16,10 +20,8 @@ pub enum ManagerState { WaitForPhy, SetupInterface, WaitForSession, - SendWhoAreYou, - WaitHello, - SendUgnDump, - WaitUgnReport, + Identifying, + ReceivingUgns, Done, Failed, } @@ -29,29 +31,23 @@ pub enum SubordinateState { WaitForPhy, SetupInterface, WaitForSession, - WaitWhoAreYou, - SendHello, - WaitUgnDump, - SendUgnReport, + SendingUgns, Done, Failed, } -pub const MAX_UGN_EDGES: usize = 2; -pub const UGN_EDGE_BYTES: usize = 32; +pub const MAX_UGN_EDGES: usize = 8; +pub const UGN_EDGE_BYTES: usize = 16; const MSG_WHO_ARE_YOU: u8 = 1; const MSG_HELLO: u8 = 2; const MSG_UGN_DUMP: u8 = 3; -const MSG_UGN_REPORT: u8 = 4; const ROLE_MANAGER: u8 = 0; const ROLE_SUBORDINATE: u8 = 1; const WHO_ARE_YOU_BYTES: usize = 8; const HELLO_BYTES: usize = 8; const UGN_DUMP_BYTES: usize = 1; -const UGN_REPORT_BYTES: usize = 2 + MAX_UGN_EDGES * UGN_EDGE_BYTES; -#[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct UgnEdge { pub src_node: u32, @@ -59,48 +55,37 @@ pub struct UgnEdge { pub dst_node: u32, pub dst_port: u32, pub ugn: i64, - pub is_valid: u8, - pub _padding: [u8; 7], } -#[repr(C)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct UgnReport { pub count: u8, - pub _padding: [u8; 7], - pub edges: [UgnEdge; MAX_UGN_EDGES], + pub edges: [Option; MAX_UGN_EDGES], +} + +impl UgnReport { + pub fn new() -> Self { + Self { + count: 0, + edges: [None; MAX_UGN_EDGES], + } + } } #[repr(C)] #[derive(Clone, Copy, Debug, Default, FromZeroes, FromBytes, AsBytes, Unaligned)] struct UgnEdgeWire { src_node: U32, - src_port: U32, dst_node: U32, - dst_port: U32, ugn: I64, - is_valid: u8, - _padding: [u8; 7], -} - -#[repr(C)] -#[derive(Clone, Copy, Debug, Default, FromZeroes, FromBytes, AsBytes, Unaligned)] -struct UgnReportWire { - msg_type: u8, - count: u8, - edges: [UgnEdgeWire; MAX_UGN_EDGES], } impl From for UgnEdgeWire { fn from(edge: UgnEdge) -> Self { Self { src_node: U32::new(edge.src_node), - src_port: U32::new(edge.src_port), dst_node: U32::new(edge.dst_node), - dst_port: U32::new(edge.dst_port), ugn: I64::new(edge.ugn), - is_valid: edge.is_valid, - _padding: [0; 7], } } } @@ -109,27 +94,124 @@ impl From for UgnEdge { fn from(edge: UgnEdgeWire) -> Self { Self { src_node: edge.src_node.get(), - src_port: edge.src_port.get(), dst_node: edge.dst_node.get(), - dst_port: edge.dst_port.get(), + src_port: 0, + dst_port: 0, ugn: edge.ugn.get(), - is_valid: edge.is_valid, - _padding: [0; 7], } } } const _: [(); UGN_EDGE_BYTES] = [(); core::mem::size_of::()]; -const _: [(); UGN_REPORT_BYTES] = [(); core::mem::size_of::()]; - -pub trait NetMedium { - fn phy_ready(&self, link: usize) -> bool; - fn setup_interface(&mut self, link: usize, local_ip: [u8; 4]); - fn connect(&mut self, link: usize, peer_ip: [u8; 4]) -> bool; - fn listen(&mut self, link: usize) -> bool; - fn send(&mut self, link: usize, data: &[u8]) -> bool; - fn recv(&mut self, link: usize, buf: &mut [u8]) -> Option; - fn timed_out(&self, link: usize) -> bool; + +const TCP_SERVER_PORT: u16 = 8080; +const TCP_CLIENT_PORT: u16 = 49152; + +pub struct SmoltcpLink<'a, 'b> { + iface: &'a mut Interface, + sockets: &'a mut SocketSet<'b>, + socket_handle: SocketHandle, + link: usize, + phy_ready: bool, + timed_out: bool, +} + +impl<'a, 'b> SmoltcpLink<'a, 'b> { + pub fn new( + iface: &'a mut Interface, + sockets: &'a mut SocketSet<'b>, + socket_handle: SocketHandle, + link: usize, + phy_ready: bool, + timed_out: bool, + ) -> Self { + Self { + iface, + sockets, + socket_handle, + link, + phy_ready, + timed_out, + } + } + + pub fn link(&self) -> usize { + self.link + } + + pub fn phy_ready(&self) -> bool { + self.phy_ready + } + + pub fn timed_out(&self) -> bool { + self.timed_out + } + + pub fn setup_interface(&mut self, role: NodeRole) { + let local_ip = ip_for_link(role, self.link); + let ip = IpCidr::new( + IpAddress::v4(local_ip[0], local_ip[1], local_ip[2], local_ip[3]), + 24, + ); + self.iface.update_ip_addrs(|addrs| { + if !addrs.contains(&ip) { + let _ = addrs.push(ip); + } + }); + } + + pub fn connect(&mut self, peer_ip: [u8; 4]) -> bool { + let socket = self.sockets.get_mut::(self.socket_handle); + if !socket.is_open() && !socket.is_active() { + let cx = self.iface.context(); + let _ = socket.connect( + cx, + ( + IpAddress::v4(peer_ip[0], peer_ip[1], peer_ip[2], peer_ip[3]), + TCP_SERVER_PORT, + ), + TCP_CLIENT_PORT, + ); + } + socket.is_active() + } + + pub fn listen(&mut self) -> bool { + let socket = self.sockets.get_mut::(self.socket_handle); + if !socket.is_open() { + let _ = socket.listen(TCP_SERVER_PORT); + } + socket.is_open() + } + + pub fn send(&mut self, data: &[u8]) -> bool { + let socket = self.sockets.get_mut::(self.socket_handle); + if socket.can_send() { + let _ = socket.send_slice(data); + return true; + } + false + } + + pub fn recv(&mut self, buf: &mut [u8]) -> Option { + let socket = self.sockets.get_mut::(self.socket_handle); + if socket.can_recv() { + if let Ok(len) = socket.recv_slice(buf) { + return Some(len); + } + } + None + } + + pub fn is_open_manager(&mut self) -> bool { + let socket = self.sockets.get::(self.socket_handle); + socket.may_recv() + } + + pub fn close(&mut self) { + let socket = self.sockets.get_mut::(self.socket_handle); + socket.close(); + } } pub struct Manager { @@ -137,7 +219,10 @@ pub struct Manager { retries: u8, nonce_seed: u32, nonce: u32, + received_edges: u8, report: Option, + identifying_sent: bool, + ugn_dump_sent: bool, } impl Manager { @@ -147,82 +232,140 @@ impl Manager { retries: 0, nonce_seed: 1, nonce: 0, + received_edges: 0, report: None, + identifying_sent: false, + ugn_dump_sent: false, } } - pub fn step(&mut self, medium: &mut M, link: usize) { - self.state = match self.state { + pub fn step(&mut self, link: &mut SmoltcpLink<'_, '_>) { + let prev_state = self.state; + let next_state = match self.state { ManagerState::WaitForPhy => { - if medium.phy_ready(link) { + if link.phy_ready() { + debug!("manager phy ready on link {}", link.link()); ManagerState::SetupInterface } else { self.state } } ManagerState::SetupInterface => { - medium.setup_interface(link, ip_for_link(NodeRole::Manager, link)); + debug!("manager setup interface link {}", link.link()); + link.setup_interface(NodeRole::Manager); ManagerState::WaitForSession } ManagerState::WaitForSession => { - let peer_ip = ip_for_link(NodeRole::Subordinate, link); - if medium.connect(link, peer_ip) { - ManagerState::SendWhoAreYou - } else if medium.timed_out(link) { + let peer_ip = ip_for_link(NodeRole::Subordinate, link.link()); + if link.connect(peer_ip) { + debug!("manager connected link {} peer {:?}", link.link(), peer_ip); + self.identifying_sent = false; + self.ugn_dump_sent = false; + ManagerState::Identifying + } else if link.timed_out() { self.bump_retry_or_fail(ManagerState::WaitForSession) } else { self.state } } - ManagerState::SendWhoAreYou => { - self.nonce = self.next_nonce(); - let msg = encode_who_are_you(self.nonce); - if medium.send(link, &msg) { - ManagerState::WaitHello - } else { - self.state + ManagerState::Identifying => { + if !self.identifying_sent { + self.nonce = self.next_nonce(); + let msg = encode_who_are_you(self.nonce); + if link.send(&msg) { + debug!("manager sent who-are-you nonce {}", self.nonce); + self.identifying_sent = true; + } } - } - ManagerState::WaitHello => { - let mut buf = [0u8; HELLO_BYTES]; - if let Some(len) = medium.recv(link, &mut buf) { - if parse_hello(&buf[..len], self.nonce) { - ManagerState::SendUgnDump + + if !self.identifying_sent { + self.state + } else { + let mut buf = [0u8; HELLO_BYTES]; + if let Some(len) = link.recv(&mut buf) { + if parse_hello(&buf[..len], self.nonce) { + debug!("manager received hello nonce {}", self.nonce); + self.ugn_dump_sent = false; + ManagerState::ReceivingUgns + } else { + warn!("manager received invalid hello len {}", len); + self.state + } + } else if link.timed_out() { + self.identifying_sent = false; + self.bump_retry_or_fail(ManagerState::Identifying) } else { self.state } - } else if medium.timed_out(link) { - self.bump_retry_or_fail(ManagerState::SendWhoAreYou) - } else { - self.state } } - ManagerState::SendUgnDump => { - let msg = [MSG_UGN_DUMP]; - if medium.send(link, &msg) { - ManagerState::WaitUgnReport - } else { - self.state + ManagerState::ReceivingUgns => { + if !self.ugn_dump_sent { + let msg = [MSG_UGN_DUMP]; + if link.send(&msg) { + debug!("manager sent ugn dump"); + self.ugn_dump_sent = true; + } } - } - ManagerState::WaitUgnReport => { - let mut buf = [0u8; UGN_REPORT_BYTES]; - if let Some(len) = medium.recv(link, &mut buf) { - if let Some(report) = parse_ugn_report(&buf[..len]) { - self.report = Some(report); - ManagerState::Done + + if !self.ugn_dump_sent { + self.state + } else if !link.is_open_manager() { + if self.report.is_none() { + self.report = Some(UgnReport::new()); + } + info!( + "manager connection closed after {} edges", + self.received_edges + ); + ManagerState::Done + } else { + let mut buf = [0u8; UGN_EDGE_BYTES]; + if let Some(len) = link.recv(&mut buf) { + if let Some(edge) = parse_ugn_edge(&buf[..len]) { + let mut inserted = false; + { + let report = self.report.get_or_insert_with(UgnReport::new); + if insert_edge(report, edge) { + inserted = true; + } + } + if inserted { + self.received_edges = self.received_edges.saturating_add(1); + if let Some(report) = self.report.as_mut() { + report.count = self.received_edges; + } + debug!( + "manager stored edge src {} dst {} ugn {}", + edge.src_node, edge.dst_node, edge.ugn + ); + } else { + trace!( + "manager dropped duplicate/full edge src {} dst {}", + edge.src_node, + edge.dst_node + ); + } + self.state + } else { + debug!("manager received invalid edge len {}", len); + self.state + } + } else if link.timed_out() { + self.ugn_dump_sent = false; + self.bump_retry_or_fail(ManagerState::ReceivingUgns) } else { self.state } - } else if medium.timed_out(link) { - self.bump_retry_or_fail(ManagerState::SendUgnDump) - } else { - self.state } } ManagerState::Done => self.state, ManagerState::Failed => self.state, }; + if next_state != prev_state { + info!("manager state {:?} -> {:?}", prev_state, next_state); + } + self.state = next_state; } pub fn is_done(&self) -> bool { @@ -244,8 +387,13 @@ impl Manager { fn bump_retry_or_fail(&mut self, retry_state: ManagerState) -> ManagerState { self.retries = self.retries.saturating_add(1); if self.retries >= 3 { + warn!("manager retry limit hit, entering Failed"); ManagerState::Failed } else { + info!( + "manager timeout, retry {} -> {:?}", + self.retries, retry_state + ); retry_state } } @@ -268,6 +416,11 @@ pub struct Subordinate { retries: u8, nonce: u32, report: UgnReport, + sent_edges: [bool; MAX_UGN_EDGES], + sent_count: u8, + who_are_you_received: bool, + hello_sent: bool, + ugn_dump_received: bool, } impl Subordinate { @@ -276,7 +429,12 @@ impl Subordinate { state: SubordinateState::WaitForPhy, retries: 0, nonce: 0, - report: UgnReport::default(), + report: UgnReport::new(), + sent_edges: [false; MAX_UGN_EDGES], + sent_count: 0, + who_are_you_received: false, + hello_sent: false, + ugn_dump_received: false, } } @@ -289,78 +447,117 @@ impl Subordinate { pub fn set_report(&mut self, report: UgnReport) { self.report = report; + self.sent_edges = [false; MAX_UGN_EDGES]; + self.sent_count = 0; + self.reset_handshake(); } - pub fn step(&mut self, medium: &mut M, link: usize) { - self.state = match self.state { + pub fn step(&mut self, link: &mut SmoltcpLink<'_, '_>) { + let prev_state = self.state; + let next_state = match self.state { SubordinateState::WaitForPhy => { - if medium.phy_ready(link) { + if link.phy_ready() { + debug!("subordinate phy ready on link {}", link.link()); SubordinateState::SetupInterface } else { self.state } } SubordinateState::SetupInterface => { - medium.setup_interface(link, ip_for_link(NodeRole::Subordinate, link)); + debug!("subordinate setup interface link {}", link.link()); + link.setup_interface(NodeRole::Subordinate); SubordinateState::WaitForSession } SubordinateState::WaitForSession => { - if medium.listen(link) { - SubordinateState::WaitWhoAreYou - } else if medium.timed_out(link) { + if link.listen() { + debug!("subordinate listening on link {}", link.link()); + self.reset_handshake(); + SubordinateState::SendingUgns + } else if link.timed_out() { self.bump_retry_or_fail(SubordinateState::WaitForSession) } else { self.state } } - SubordinateState::WaitWhoAreYou => { - let mut buf = [0u8; WHO_ARE_YOU_BYTES]; - if let Some(len) = medium.recv(link, &mut buf) { - if let Some(nonce) = parse_who_are_you(&buf[..len]) { - self.nonce = nonce; - SubordinateState::SendHello - } else { - self.state + SubordinateState::SendingUgns => { + if link.timed_out() && !self.ugn_dump_received { + self.reset_handshake(); + self.bump_retry_or_fail(SubordinateState::SendingUgns) + } else if !self.who_are_you_received { + let mut buf = [0u8; WHO_ARE_YOU_BYTES]; + if let Some(len) = link.recv(&mut buf) { + if let Some(nonce) = parse_who_are_you(&buf[..len]) { + self.nonce = nonce; + self.who_are_you_received = true; + debug!("subordinate received who-are-you nonce {}", nonce); + } else { + warn!("subordinate received invalid who-are-you len {}", len); + } } - } else if medium.timed_out(link) { - self.bump_retry_or_fail(SubordinateState::WaitForSession) - } else { self.state - } - } - SubordinateState::SendHello => { - let msg = encode_hello(self.nonce); - if medium.send(link, &msg) { - SubordinateState::WaitUgnDump - } else { + } else if !self.hello_sent { + let msg = encode_hello(self.nonce); + if link.send(&msg) { + debug!("subordinate sent hello nonce {}", self.nonce); + self.hello_sent = true; + } self.state - } - } - SubordinateState::WaitUgnDump => { - let mut buf = [0u8; UGN_DUMP_BYTES]; - if let Some(len) = medium.recv(link, &mut buf) { - if parse_ugn_dump(&buf[..len]) { - SubordinateState::SendUgnReport - } else { - self.state + } else if !self.ugn_dump_received { + let mut buf = [0u8; UGN_DUMP_BYTES]; + if let Some(len) = link.recv(&mut buf) { + if parse_ugn_dump(&buf[..len]) { + debug!("subordinate received ugn dump"); + self.ugn_dump_received = true; + } else { + warn!("subordinate received invalid ugn dump len {}", len); + } } - } else if medium.timed_out(link) { - self.bump_retry_or_fail(SubordinateState::WaitUgnDump) - } else { self.state - } - } - SubordinateState::SendUgnReport => { - let msg = encode_ugn_report(&self.report); - if medium.send(link, &msg) { - SubordinateState::Done } else { - self.state + let total = self.report.count as usize; + if total == 0 { + info!("subordinate no edges to send, closing link {}", link.link()); + link.close(); + SubordinateState::Done + } else if (self.sent_count as usize) >= total { + info!("subordinate sent all edges, closing link {}", link.link()); + link.close(); + SubordinateState::Done + } else if let Some((idx, edge)) = self.next_unsent_edge(total) { + let msg = encode_ugn_edge(edge); + if link.send(&msg) { + debug!( + "subordinate sent edge idx {} src {} dst {} ugn {}", + idx, edge.src_node, edge.dst_node, edge.ugn + ); + self.sent_edges[idx] = true; + self.sent_count = self.sent_count.saturating_add(1); + if (self.sent_count as usize) >= total { + info!("subordinate finished edges, closing link {}", link.link()); + link.close(); + SubordinateState::Done + } else { + self.state + } + } else { + self.state + } + } else { + debug!( + "subordinate has no unsent edges (count {}, sent {})", + total, self.sent_count + ); + self.state + } } } SubordinateState::Done => self.state, SubordinateState::Failed => self.state, }; + if next_state != prev_state { + info!("subordinate state {:?} -> {:?}", prev_state, next_state); + } + self.state = next_state; } pub fn is_done(&self) -> bool { @@ -375,14 +572,37 @@ impl Subordinate { self.state } + fn reset_handshake(&mut self) { + self.who_are_you_received = false; + self.hello_sent = false; + self.ugn_dump_received = false; + } + fn bump_retry_or_fail(&mut self, retry_state: SubordinateState) -> SubordinateState { self.retries = self.retries.saturating_add(1); if self.retries >= 3 { + warn!("subordinate retry limit hit, entering Failed"); SubordinateState::Failed } else { + info!( + "subordinate timeout, retry {} -> {:?}", + self.retries, retry_state + ); retry_state } } + + fn next_unsent_edge(&self, total: usize) -> Option<(usize, UgnEdge)> { + let limit = core::cmp::min(total, self.report.edges.len()); + for idx in 0..limit { + if !self.sent_edges[idx] { + if let Some(edge) = self.report.edges[idx] { + return Some((idx, edge)); + } + } + } + None + } } impl Default for Subordinate { @@ -434,37 +654,33 @@ fn parse_ugn_dump(msg: &[u8]) -> bool { msg.len() >= UGN_DUMP_BYTES && msg[0] == MSG_UGN_DUMP } -fn encode_ugn_report(report: &UgnReport) -> [u8; UGN_REPORT_BYTES] { - let mut wire = UgnReportWire { - msg_type: MSG_UGN_REPORT, - count: report.count, - edges: [UgnEdgeWire::default(); MAX_UGN_EDGES], - }; - for (idx, edge) in report.edges.iter().enumerate() { - wire.edges[idx] = (*edge).into(); - } - let mut buf = [0u8; UGN_REPORT_BYTES]; +fn encode_ugn_edge(edge: UgnEdge) -> [u8; UGN_EDGE_BYTES] { + let wire: UgnEdgeWire = edge.into(); + let mut buf = [0u8; UGN_EDGE_BYTES]; buf.copy_from_slice(wire.as_bytes()); buf } -fn parse_ugn_report(msg: &[u8]) -> Option { - let (wire, _) = Ref::<_, UgnReportWire>::new_from_prefix(msg)?; - if wire.msg_type != MSG_UGN_REPORT { - return None; - } - let count = wire.count as usize; - if count > MAX_UGN_EDGES { - return None; +fn parse_ugn_edge(msg: &[u8]) -> Option { + let (wire, _) = Ref::<_, UgnEdgeWire>::new_from_prefix(msg)?; + Some(UgnEdge::from(*wire)) +} + +fn insert_edge(report: &mut UgnReport, edge: UgnEdge) -> bool { + if report + .edges + .iter() + .flatten() + .any(|existing| existing.src_node == edge.src_node && existing.dst_node == edge.dst_node) + { + return false; } - let mut report = UgnReport { - count: wire.count, - ..Default::default() - }; - for idx in 0..count { - report.edges[idx] = wire.edges[idx].into(); + if let Some(slot) = report.edges.iter_mut().find(|slot| slot.is_none()) { + *slot = Some(edge); + true + } else { + false } - Some(report) } pub fn ip_for_link(role: NodeRole, port: usize) -> [u8; 4] { From 1a00c51fde43b15863ced677ec858ea61f7bdcea Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Mon, 9 Mar 2026 14:16:25 +0100 Subject: [PATCH 31/70] Add debug.json --- .github/synthesis/debug.json | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .github/synthesis/debug.json diff --git a/.github/synthesis/debug.json b/.github/synthesis/debug.json new file mode 100644 index 000000000..63b2d418e --- /dev/null +++ b/.github/synthesis/debug.json @@ -0,0 +1,3 @@ +[ + {"top": "softUgnDemoTest", "stage": "test", "cc_report": true} +] From ea89f0ab95b8a7b16f64db31fa8c7b741eb2f20d Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Mon, 9 Mar 2026 17:12:00 +0100 Subject: [PATCH 32/70] Set MANAGER_DNA --- firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs index 4f91a095e..ea948222a 100644 --- a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs @@ -23,7 +23,7 @@ use ufmt::uwriteln; const INSTANCES: DeviceInstances = unsafe { DeviceInstances::new() }; const LINK_COUNT: usize = 7; const TCP_BUF_SIZE: usize = 256; -const MANAGER_DNA: [u8; 12] = [0; 12]; +const MANAGER_DNA: [u8; 12] = [133, 129, 48, 4, 64, 192, 105, 1, 1, 0, 2, 64]; static mut TCP_RX_BUFS: [[u8; TCP_BUF_SIZE]; LINK_COUNT] = [[0; TCP_BUF_SIZE]; LINK_COUNT]; static mut TCP_TX_BUFS: [[u8; TCP_BUF_SIZE]; LINK_COUNT] = [[0; TCP_BUF_SIZE]; LINK_COUNT]; From 9d37fa24901647f5a1f77ce1c056244c7d96617c Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Mon, 9 Mar 2026 17:22:19 +0100 Subject: [PATCH 33/70] Add panic handler --- firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs index ea948222a..1b5d13dfa 100644 --- a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs @@ -13,7 +13,7 @@ use bittide_sys::link_startup::LinkStartup; use bittide_sys::net_state::{Manager, SmoltcpLink, Subordinate, UgnEdge, UgnReport}; use bittide_sys::smoltcp::soft_ugn_ringbuffer::{AlignedReceiveBuffer, RingbufferDevice}; use bittide_sys::stability_detector::Stability; -use core::panic::PanicInfo; +use core::fmt::Write; use log::{info, trace, warn, LevelFilter}; use smoltcp::iface::{Config, Interface, SocketHandle, SocketSet, SocketStorage}; use smoltcp::socket::tcp; @@ -288,7 +288,9 @@ fn main() -> ! { } #[panic_handler] -fn panic_handler(_: &PanicInfo) -> ! { +fn panic_handler(info: &core::panic::PanicInfo) -> ! { + let mut uart = INSTANCES.uart; + writeln!(uart, "Panicked! #{info}").unwrap(); loop { continue; } From d9954e13983d372278fcc445d9f1e12cd4cb4068 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Mon, 9 Mar 2026 17:34:42 +0100 Subject: [PATCH 34/70] Add logging --- .../demos/soft-ugn-demo-mu-2/Cargo.toml | 2 +- .../demos/soft-ugn-demo-mu-2/src/main.rs | 73 +++++++++++- firmware-support/bittide-sys/src/net_state.rs | 105 ++++++++++++++++-- .../src/smoltcp/soft_ugn_ringbuffer.rs | 26 ++++- 4 files changed, 190 insertions(+), 16 deletions(-) diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/Cargo.toml b/firmware-binaries/demos/soft-ugn-demo-mu-2/Cargo.toml index 3b04d9cc7..9d81ac517 100644 --- a/firmware-binaries/demos/soft-ugn-demo-mu-2/Cargo.toml +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/Cargo.toml @@ -18,7 +18,7 @@ ufmt = "0.2.0" [dependencies.smoltcp] version = "0.12.0" default-features = false -features = ["medium-ip", "medium-ethernet", "proto-ipv4", "socket-tcp"] +features = ["log", "medium-ip", "medium-ethernet", "proto-ipv4", "socket-tcp"] [dependencies.log] version = "0.4.21" diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs index 1b5d13dfa..d7822ce2a 100644 --- a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs @@ -10,7 +10,9 @@ use bittide_hal::hals::soft_ugn_demo_mu::devices::{ReceiveRingbuffer, TransmitRi use bittide_hal::hals::soft_ugn_demo_mu::DeviceInstances; use bittide_hal::manual_additions::timer::Instant; use bittide_sys::link_startup::LinkStartup; -use bittide_sys::net_state::{Manager, SmoltcpLink, Subordinate, UgnEdge, UgnReport}; +use bittide_sys::net_state::{ + Manager, ManagerState, SmoltcpLink, Subordinate, SubordinateState, UgnEdge, UgnReport, +}; use bittide_sys::smoltcp::soft_ugn_ringbuffer::{AlignedReceiveBuffer, RingbufferDevice}; use bittide_sys::stability_detector::Stability; use core::fmt::Write; @@ -24,6 +26,7 @@ const INSTANCES: DeviceInstances = unsafe { DeviceInstances::new() }; const LINK_COUNT: usize = 7; const TCP_BUF_SIZE: usize = 256; const MANAGER_DNA: [u8; 12] = [133, 129, 48, 4, 64, 192, 105, 1, 1, 0, 2, 64]; +const LOG_TICK_EVERY: u32 = 500; static mut TCP_RX_BUFS: [[u8; TCP_BUF_SIZE]; LINK_COUNT] = [[0; TCP_BUF_SIZE]; LINK_COUNT]; static mut TCP_TX_BUFS: [[u8; TCP_BUF_SIZE]; LINK_COUNT] = [[0; TCP_BUF_SIZE]; LINK_COUNT]; @@ -204,12 +207,31 @@ fn main() -> ! { info!("Starting manager state machines..."); let mut managers: [Manager; LINK_COUNT] = core::array::from_fn(|_| Manager::new()); let mut done = [false; LINK_COUNT]; + let mut last_states: [ManagerState; LINK_COUNT] = + core::array::from_fn(|_| ManagerState::WaitForPhy); + let mut tick: u32 = 0; + trace!("Starting main event loop..."); loop { + tick = tick.wrapping_add(1); + if tick % LOG_TICK_EVERY == 0 { + info!("manager loop tick {}", tick); + } let now = to_smoltcp_instant(INSTANCES.timer.now()); for link in 0..LINK_COUNT { + trace!("Polling link {}...", link); let mut sockets = socket_set(&mut sockets_storage[link][..]); - let _ = ifaces[link].poll(now, &mut devices[link], &mut sockets); + let poll_result = ifaces[link].poll(now, &mut devices[link], &mut sockets); + trace!("manager link {} poll result {:?}", link, poll_result); + let socket = sockets.get::(socket_handles[link]); + trace!( + "manager link {} socket open {} active {} can_send {} can_recv {}", + link, + socket.is_open(), + socket.is_active(), + socket.can_send(), + socket.can_recv() + ); let mut smoltcp_link = SmoltcpLink::new( &mut ifaces[link], &mut sockets, @@ -218,13 +240,28 @@ fn main() -> ! { true, false, ); + trace!( + "manager link {} step from state {:?}", + link, + managers[link].state() + ); managers[link].step(&mut smoltcp_link); + let state = managers[link].state(); + if state != last_states[link] { + info!( + "manager link {} state {:?} -> {:?}", + link, last_states[link], state + ); + last_states[link] = state; + } if managers[link].is_done() { + trace!("manager link {} is done", link); done[link] = true; } } if done.iter().all(|v| *v) { + info!("All manager links done"); break; } } @@ -259,15 +296,32 @@ fn main() -> ! { info!("Starting subordinate state machines..."); let mut subordinates: [Subordinate; LINK_COUNT] = core::array::from_fn(|_| Subordinate::new()); + let mut last_states: [SubordinateState; LINK_COUNT] = + core::array::from_fn(|_| SubordinateState::WaitForPhy); + let mut tick: u32 = 0; for link in 0..LINK_COUNT { subordinates[link].set_report(build_report_for_link(link, &capture_ugns[link], &dna)); } loop { + tick = tick.wrapping_add(1); + if tick % LOG_TICK_EVERY == 0 { + info!("subordinate loop tick {}", tick); + } let now = to_smoltcp_instant(INSTANCES.timer.now()); for link in 0..LINK_COUNT { let mut sockets = socket_set(&mut sockets_storage[link][..]); - let _ = ifaces[link].poll(now, &mut devices[link], &mut sockets); + let poll_result = ifaces[link].poll(now, &mut devices[link], &mut sockets); + trace!("subordinate link {} poll result {:?}", link, poll_result); + let socket = sockets.get::(socket_handles[link]); + trace!( + "subordinate link {} socket open {} active {} can_send {} can_recv {}", + link, + socket.is_open(), + socket.is_active(), + socket.can_send(), + socket.can_recv() + ); let mut smoltcp_link = SmoltcpLink::new( &mut ifaces[link], &mut sockets, @@ -276,7 +330,20 @@ fn main() -> ! { true, false, ); + trace!( + "subordinate link {} step from state {:?}", + link, + subordinates[link].state() + ); subordinates[link].step(&mut smoltcp_link); + let state = subordinates[link].state(); + if state != last_states[link] { + info!( + "subordinate link {} state {:?} -> {:?}", + link, last_states[link], state + ); + last_states[link] = state; + } } } } diff --git a/firmware-support/bittide-sys/src/net_state.rs b/firmware-support/bittide-sys/src/net_state.rs index 1acd6f01a..9a26736b6 100644 --- a/firmware-support/bittide-sys/src/net_state.rs +++ b/firmware-support/bittide-sys/src/net_state.rs @@ -158,37 +158,92 @@ impl<'a, 'b> SmoltcpLink<'a, 'b> { let _ = addrs.push(ip); } }); + trace!( + "link {} role {:?} ip addrs {:?}", + self.link, + role, + self.iface.ip_addrs() + ); } pub fn connect(&mut self, peer_ip: [u8; 4]) -> bool { let socket = self.sockets.get_mut::(self.socket_handle); if !socket.is_open() && !socket.is_active() { let cx = self.iface.context(); - let _ = socket.connect( + match socket.connect( cx, ( IpAddress::v4(peer_ip[0], peer_ip[1], peer_ip[2], peer_ip[3]), TCP_SERVER_PORT, ), TCP_CLIENT_PORT, - ); + ) { + Ok(()) => { + debug!( + "link {} connect requested to {:?}:{} from {}", + self.link, peer_ip, TCP_SERVER_PORT, TCP_CLIENT_PORT + ); + } + Err(err) => { + debug!("link {} connect error: {:?}", self.link, err); + } + } } + trace!( + "link {} connect state open {} active {} can_send {} can_recv {} may_recv {}", + self.link, + socket.is_open(), + socket.is_active(), + socket.can_send(), + socket.can_recv(), + socket.may_recv() + ); socket.is_active() } pub fn listen(&mut self) -> bool { let socket = self.sockets.get_mut::(self.socket_handle); if !socket.is_open() { - let _ = socket.listen(TCP_SERVER_PORT); + match socket.listen(TCP_SERVER_PORT) { + Ok(()) => { + debug!("link {} listen on port {}", self.link, TCP_SERVER_PORT); + } + Err(err) => { + debug!("link {} listen error: {:?}", self.link, err); + } + } } + trace!( + "link {} listen state open {} active {} can_send {} can_recv {} may_recv {}", + self.link, + socket.is_open(), + socket.is_active(), + socket.can_send(), + socket.can_recv(), + socket.may_recv() + ); socket.is_open() } pub fn send(&mut self, data: &[u8]) -> bool { let socket = self.sockets.get_mut::(self.socket_handle); if socket.can_send() { - let _ = socket.send_slice(data); - return true; + match socket.send_slice(data) { + Ok(len) => { + trace!("link {} sent {} bytes", self.link, len); + return true; + } + Err(err) => { + debug!("link {} send error: {:?}", self.link, err); + } + } + } else { + trace!( + "link {} send blocked open {} active {}", + self.link, + socket.is_open(), + socket.is_active() + ); } false } @@ -196,9 +251,22 @@ impl<'a, 'b> SmoltcpLink<'a, 'b> { pub fn recv(&mut self, buf: &mut [u8]) -> Option { let socket = self.sockets.get_mut::(self.socket_handle); if socket.can_recv() { - if let Ok(len) = socket.recv_slice(buf) { - return Some(len); + match socket.recv_slice(buf) { + Ok(len) => { + trace!("link {} recv {} bytes", self.link, len); + return Some(len); + } + Err(err) => { + debug!("link {} recv error: {:?}", self.link, err); + } } + } else { + trace!( + "link {} recv blocked open {} active {}", + self.link, + socket.is_open(), + socket.is_active() + ); } None } @@ -243,6 +311,7 @@ impl Manager { let prev_state = self.state; let next_state = match self.state { ManagerState::WaitForPhy => { + trace!("Checking phy ready on link {}", link.link()); if link.phy_ready() { debug!("manager phy ready on link {}", link.link()); ManagerState::SetupInterface @@ -257,6 +326,11 @@ impl Manager { } ManagerState::WaitForSession => { let peer_ip = ip_for_link(NodeRole::Subordinate, link.link()); + trace!( + "manager attempting connect to peer {:?} on link {}", + peer_ip, + link.link() + ); if link.connect(peer_ip) { debug!("manager connected link {} peer {:?}", link.link(), peer_ip); self.identifying_sent = false; @@ -269,6 +343,7 @@ impl Manager { } } ManagerState::Identifying => { + trace!("manager identifying link {}", link.link()); if !self.identifying_sent { self.nonce = self.next_nonce(); let msg = encode_who_are_you(self.nonce); @@ -300,6 +375,7 @@ impl Manager { } } ManagerState::ReceivingUgns => { + trace!("manager receiving ugns on link {}", link.link()); if !self.ugn_dump_sent { let msg = [MSG_UGN_DUMP]; if link.send(&msg) { @@ -456,6 +532,11 @@ impl Subordinate { let prev_state = self.state; let next_state = match self.state { SubordinateState::WaitForPhy => { + trace!( + "subordinate state {:?} checking phy ready on link {}", + self.state, + link.link() + ); if link.phy_ready() { debug!("subordinate phy ready on link {}", link.link()); SubordinateState::SetupInterface @@ -469,6 +550,11 @@ impl Subordinate { SubordinateState::WaitForSession } SubordinateState::WaitForSession => { + trace!( + "subordinate state {:?} attempting listen on link {}", + self.state, + link.link() + ); if link.listen() { debug!("subordinate listening on link {}", link.link()); self.reset_handshake(); @@ -480,6 +566,11 @@ impl Subordinate { } } SubordinateState::SendingUgns => { + trace!( + "subordinate state {:?} checking for manager connection on link {}", + self.state, + link.link() + ); if link.timed_out() && !self.ugn_dump_received { self.reset_handshake(); self.bump_retry_or_fail(SubordinateState::SendingUgns) diff --git a/firmware-support/bittide-sys/src/smoltcp/soft_ugn_ringbuffer.rs b/firmware-support/bittide-sys/src/smoltcp/soft_ugn_ringbuffer.rs index 4a14ffb62..6f2131cc0 100644 --- a/firmware-support/bittide-sys/src/smoltcp/soft_ugn_ringbuffer.rs +++ b/firmware-support/bittide-sys/src/smoltcp/soft_ugn_ringbuffer.rs @@ -33,6 +33,7 @@ impl AlignedReceiveBuffer { } pub fn align(&mut self, tx: &TransmitRingbuffer) { + trace!("ringbuffer align start"); let announce_pattern = [ALIGNMENT_ANNOUNCE.to_le_bytes()]; tx.write_slice(&announce_pattern, 0); @@ -48,6 +49,7 @@ impl AlignedReceiveBuffer { let value = u64::from_le_bytes(data_buf[0]); if value == ALIGNMENT_ANNOUNCE || value == ALIGNMENT_ACKNOWLEDGE { + trace!("ringbuffer align saw marker at rx_idx {}", rx_idx); break 'outer rx_idx; } } @@ -67,6 +69,11 @@ impl AlignedReceiveBuffer { self.rx_alignment_offset = Some(rx_offset); self.tx_reference = tx.0 as *const _ as usize; + trace!( + "ringbuffer align done offset {} tx_ref 0x{:X}", + rx_offset, + self.tx_reference + ); } pub fn is_aligned(&self) -> bool { @@ -171,17 +178,25 @@ impl Device for RingbufferDevice { let seq_num = unsafe { (header_ptr.add(4) as *const u16).read_unaligned() }; let packet_len = unsafe { (header_ptr.add(6) as *const u16).read_unaligned() } as usize; + trace!( + "ringbuffer rx header seq {} len {} last {}", + seq_num, + packet_len, + self.last_rx_seq + ); + if seq_num == self.last_rx_seq { - trace!("Detected repeated packet with seq {}", seq_num); + trace!("ringbuffer rx repeated seq {}", seq_num); return None; } if packet_len < MIN_IP_PACKET_SIZE || packet_len > self.mtu { trace!( - "Invalid packet length: {} (must be {}-{})", + "ringbuffer rx invalid len {} (must be {}-{}) seq {}", packet_len, MIN_IP_PACKET_SIZE, - self.mtu + self.mtu, + seq_num ); return None; } @@ -197,12 +212,12 @@ impl Device for RingbufferDevice { self.rx_buffer.read_slice(word_slice, 0); if !is_valid(&packet_buffer[..total_len]) { - trace!("CRC validation failed for packet seq {}", seq_num); + trace!("ringbuffer rx crc fail seq {}", seq_num); return None; } trace!( - "Valid packet: seq {}, payload {} bytes", + "ringbuffer rx valid seq {}, payload {} bytes", seq_num, packet_len ); @@ -225,6 +240,7 @@ impl Device for RingbufferDevice { } fn transmit(&mut self, _timestamp: Instant) -> Option> { + trace!("ringbuffer tx token seq {}", self.tx_seq_num); Some(TxToken { tx_buffer: &mut self.tx_buffer, mtu: self.mtu, From e3ace711ba05e3463344e62723e298a371062911 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Mon, 9 Mar 2026 18:00:29 +0100 Subject: [PATCH 35/70] Sequential ugn collection --- .../demos/soft-ugn-demo-mu-2/src/main.rs | 97 ++++++++++--------- 1 file changed, 51 insertions(+), 46 deletions(-) diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs index d7822ce2a..c142b1db3 100644 --- a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs @@ -11,7 +11,7 @@ use bittide_hal::hals::soft_ugn_demo_mu::DeviceInstances; use bittide_hal::manual_additions::timer::Instant; use bittide_sys::link_startup::LinkStartup; use bittide_sys::net_state::{ - Manager, ManagerState, SmoltcpLink, Subordinate, SubordinateState, UgnEdge, UgnReport, + Manager, SmoltcpLink, Subordinate, SubordinateState, UgnEdge, UgnReport, }; use bittide_sys::smoltcp::soft_ugn_ringbuffer::{AlignedReceiveBuffer, RingbufferDevice}; use bittide_sys::stability_detector::Stability; @@ -182,19 +182,6 @@ fn main() -> ! { }); iface }); - let mut sockets_storage: [[SocketStorage<'static>; 1]; LINK_COUNT] = - core::array::from_fn(|_| Default::default()); - let socket_handles: [SocketHandle; LINK_COUNT] = core::array::from_fn(|idx| { - let rx_buf = unsafe { &mut TCP_RX_BUFS[idx][..] }; - let tx_buf = unsafe { &mut TCP_TX_BUFS[idx][..] }; - let socket = tcp::Socket::new( - tcp::SocketBuffer::new(rx_buf), - tcp::SocketBuffer::new(tx_buf), - ); - let mut sockets = socket_set(&mut sockets_storage[idx][..]); - sockets.add(socket) - }); - let dna = INSTANCES.dna.dna(); info!("My dna: {:?}", dna); let is_manager = dna == MANAGER_DNA; @@ -205,25 +192,35 @@ fn main() -> ! { if is_manager { info!("Starting manager state machines..."); - let mut managers: [Manager; LINK_COUNT] = core::array::from_fn(|_| Manager::new()); - let mut done = [false; LINK_COUNT]; - let mut last_states: [ManagerState; LINK_COUNT] = - core::array::from_fn(|_| ManagerState::WaitForPhy); - let mut tick: u32 = 0; + let mut reports: [Option; LINK_COUNT] = [None; LINK_COUNT]; + for link in 0..LINK_COUNT { + info!("Starting manager for link {}", link); + let rx_buf = unsafe { &mut TCP_RX_BUFS[link][..] }; + let tx_buf = unsafe { &mut TCP_TX_BUFS[link][..] }; + let socket = tcp::Socket::new( + tcp::SocketBuffer::new(rx_buf), + tcp::SocketBuffer::new(tx_buf), + ); + let mut sockets_storage: [SocketStorage<'static>; 1] = Default::default(); + let socket_handle = { + let mut sockets = socket_set(&mut sockets_storage[..]); + sockets.add(socket) + }; + let mut manager = Manager::new(); + let mut last_state = manager.state(); + let mut tick: u32 = 0; - trace!("Starting main event loop..."); - loop { - tick = tick.wrapping_add(1); - if tick % LOG_TICK_EVERY == 0 { - info!("manager loop tick {}", tick); - } - let now = to_smoltcp_instant(INSTANCES.timer.now()); - for link in 0..LINK_COUNT { - trace!("Polling link {}...", link); - let mut sockets = socket_set(&mut sockets_storage[link][..]); + trace!("Starting manager loop for link {}", link); + loop { + tick = tick.wrapping_add(1); + if tick % LOG_TICK_EVERY == 0 { + info!("manager link {} tick {}", link, tick); + } + let now = to_smoltcp_instant(INSTANCES.timer.now()); + let mut sockets = socket_set(&mut sockets_storage[..]); let poll_result = ifaces[link].poll(now, &mut devices[link], &mut sockets); trace!("manager link {} poll result {:?}", link, poll_result); - let socket = sockets.get::(socket_handles[link]); + let socket = sockets.get::(socket_handle); trace!( "manager link {} socket open {} active {} can_send {} can_recv {}", link, @@ -235,7 +232,7 @@ fn main() -> ! { let mut smoltcp_link = SmoltcpLink::new( &mut ifaces[link], &mut sockets, - socket_handles[link], + socket_handle, link, true, false, @@ -243,32 +240,28 @@ fn main() -> ! { trace!( "manager link {} step from state {:?}", link, - managers[link].state() + manager.state() ); - managers[link].step(&mut smoltcp_link); - let state = managers[link].state(); - if state != last_states[link] { + manager.step(&mut smoltcp_link); + let state = manager.state(); + if state != last_state { info!( "manager link {} state {:?} -> {:?}", - link, last_states[link], state + link, last_state, state ); - last_states[link] = state; + last_state = state; } - if managers[link].is_done() { + if manager.is_done() { trace!("manager link {} is done", link); - done[link] = true; + break; } } - - if done.iter().all(|v| *v) { - info!("All manager links done"); - break; - } + reports[link] = manager.report(); } info!("UGN reports from subordinates:"); - for (idx, manager) in managers.iter().enumerate() { - if let Some(report) = manager.report() { + for (idx, report) in reports.iter().enumerate() { + if let Some(report) = report { info!("Link {}: {} edges", idx, report.count); for (edge_idx, edge) in report.edges.iter().enumerate() { if edge_idx >= report.count as usize { @@ -298,6 +291,18 @@ fn main() -> ! { core::array::from_fn(|_| Subordinate::new()); let mut last_states: [SubordinateState; LINK_COUNT] = core::array::from_fn(|_| SubordinateState::WaitForPhy); + let mut sockets_storage: [[SocketStorage<'static>; 1]; LINK_COUNT] = + core::array::from_fn(|_| Default::default()); + let socket_handles: [SocketHandle; LINK_COUNT] = core::array::from_fn(|idx| { + let rx_buf = unsafe { &mut TCP_RX_BUFS[idx][..] }; + let tx_buf = unsafe { &mut TCP_TX_BUFS[idx][..] }; + let socket = tcp::Socket::new( + tcp::SocketBuffer::new(rx_buf), + tcp::SocketBuffer::new(tx_buf), + ); + let mut sockets = socket_set(&mut sockets_storage[idx][..]); + sockets.add(socket) + }); let mut tick: u32 = 0; for link in 0..LINK_COUNT { subordinates[link].set_report(build_report_for_link(link, &capture_ugns[link], &dna)); From e2ce418ba72cca74a4828139fccaf05c13aa36fd Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Mon, 9 Mar 2026 18:07:25 +0100 Subject: [PATCH 36/70] More logging --- .../demos/soft-ugn-demo-mu-2/src/main.rs | 102 ++++++++++-------- 1 file changed, 56 insertions(+), 46 deletions(-) diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs index c142b1db3..feb76cbf7 100644 --- a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs @@ -218,31 +218,36 @@ fn main() -> ! { } let now = to_smoltcp_instant(INSTANCES.timer.now()); let mut sockets = socket_set(&mut sockets_storage[..]); + { + let socket = sockets.get::(socket_handle); + trace!( + "manager link {} socket open {} active {} can_send {} can_recv {} state {:?}", + link, + socket.is_open(), + socket.is_active(), + socket.can_send(), + socket.can_recv(), + socket.state() + ); + } + { + let mut smoltcp_link = SmoltcpLink::new( + &mut ifaces[link], + &mut sockets, + socket_handle, + link, + true, + false, + ); + trace!( + "manager link {} step from state {:?}", + link, + manager.state() + ); + manager.step(&mut smoltcp_link); + } let poll_result = ifaces[link].poll(now, &mut devices[link], &mut sockets); trace!("manager link {} poll result {:?}", link, poll_result); - let socket = sockets.get::(socket_handle); - trace!( - "manager link {} socket open {} active {} can_send {} can_recv {}", - link, - socket.is_open(), - socket.is_active(), - socket.can_send(), - socket.can_recv() - ); - let mut smoltcp_link = SmoltcpLink::new( - &mut ifaces[link], - &mut sockets, - socket_handle, - link, - true, - false, - ); - trace!( - "manager link {} step from state {:?}", - link, - manager.state() - ); - manager.step(&mut smoltcp_link); let state = manager.state(); if state != last_state { info!( @@ -316,31 +321,36 @@ fn main() -> ! { let now = to_smoltcp_instant(INSTANCES.timer.now()); for link in 0..LINK_COUNT { let mut sockets = socket_set(&mut sockets_storage[link][..]); + { + let socket = sockets.get::(socket_handles[link]); + trace!( + "subordinate link {} socket open {} active {} can_send {} can_recv {} state {:?}", + link, + socket.is_open(), + socket.is_active(), + socket.can_send(), + socket.can_recv(), + socket.state() + ); + } + { + let mut smoltcp_link = SmoltcpLink::new( + &mut ifaces[link], + &mut sockets, + socket_handles[link], + link, + true, + false, + ); + trace!( + "subordinate link {} step from state {:?}", + link, + subordinates[link].state() + ); + subordinates[link].step(&mut smoltcp_link); + } let poll_result = ifaces[link].poll(now, &mut devices[link], &mut sockets); trace!("subordinate link {} poll result {:?}", link, poll_result); - let socket = sockets.get::(socket_handles[link]); - trace!( - "subordinate link {} socket open {} active {} can_send {} can_recv {}", - link, - socket.is_open(), - socket.is_active(), - socket.can_send(), - socket.can_recv() - ); - let mut smoltcp_link = SmoltcpLink::new( - &mut ifaces[link], - &mut sockets, - socket_handles[link], - link, - true, - false, - ); - trace!( - "subordinate link {} step from state {:?}", - link, - subordinates[link].state() - ); - subordinates[link].step(&mut smoltcp_link); let state = subordinates[link].state(); if state != last_states[link] { info!( From 3d4668774d6ec602e323a927ad788bb87b4ddfe4 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Mon, 9 Mar 2026 18:23:01 +0100 Subject: [PATCH 37/70] small change --- .../demos/soft-ugn-demo-mu-2/src/main.rs | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs index feb76cbf7..3ecd46975 100644 --- a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs @@ -218,6 +218,8 @@ fn main() -> ! { } let now = to_smoltcp_instant(INSTANCES.timer.now()); let mut sockets = socket_set(&mut sockets_storage[..]); + let poll_result = ifaces[link].poll(now, &mut devices[link], &mut sockets); + trace!("manager link {} poll result {:?}", link, poll_result); { let socket = sockets.get::(socket_handle); trace!( @@ -230,6 +232,11 @@ fn main() -> ! { socket.state() ); } + trace!( + "manager link {} ip addrs {:?}", + link, + ifaces[link].ip_addrs() + ); { let mut smoltcp_link = SmoltcpLink::new( &mut ifaces[link], @@ -246,8 +253,6 @@ fn main() -> ! { ); manager.step(&mut smoltcp_link); } - let poll_result = ifaces[link].poll(now, &mut devices[link], &mut sockets); - trace!("manager link {} poll result {:?}", link, poll_result); let state = manager.state(); if state != last_state { info!( @@ -321,6 +326,8 @@ fn main() -> ! { let now = to_smoltcp_instant(INSTANCES.timer.now()); for link in 0..LINK_COUNT { let mut sockets = socket_set(&mut sockets_storage[link][..]); + let poll_result = ifaces[link].poll(now, &mut devices[link], &mut sockets); + trace!("subordinate link {} poll result {:?}", link, poll_result); { let socket = sockets.get::(socket_handles[link]); trace!( @@ -333,6 +340,11 @@ fn main() -> ! { socket.state() ); } + trace!( + "subordinate link {} ip addrs {:?}", + link, + ifaces[link].ip_addrs() + ); { let mut smoltcp_link = SmoltcpLink::new( &mut ifaces[link], @@ -349,8 +361,6 @@ fn main() -> ! { ); subordinates[link].step(&mut smoltcp_link); } - let poll_result = ifaces[link].poll(now, &mut devices[link], &mut sockets); - trace!("subordinate link {} poll result {:?}", link, poll_result); let state = subordinates[link].state(); if state != last_states[link] { info!( From b4921398f10391e4a67416ec6410914ea157339f Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Tue, 10 Mar 2026 17:17:30 +0100 Subject: [PATCH 38/70] Bump tests --- .../Instances/Hitl/SoftUgnDemo/Core.hs | 2 +- .../src/Bittide/Instances/Tests/Ringbuffer.hs | 28 +- .../demos/soft-ugn-demo-mu-2/src/main.rs | 212 ++--- .../aligned_ringbuffer_test/src/main.rs | 4 +- .../ringbuffer_smoltcp_test/src/main.rs | 111 ++- .../sim-tests/ringbuffer_test/src/main.rs | 4 +- firmware-support/bittide-sys/src/net_state.rs | 789 +++++++----------- .../bittide-sys/src/smoltcp/ringbuffer.rs | 12 +- 8 files changed, 472 insertions(+), 690 deletions(-) diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Core.hs b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Core.hs index 18ef9f888..d2cfcf38f 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Core.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Core.hs @@ -75,7 +75,7 @@ muConfig :: muConfig = PeConfig { cpu = Riscv32imc.vexRiscv1 - , depthI = SNat @(Div (128 * 1024) 4) + , depthI = SNat @(Div (180 * 1024) 4) , depthD = SNat @(Div (64 * 1024) 4) , initI = Nothing , initD = Nothing diff --git a/bittide-instances/src/Bittide/Instances/Tests/Ringbuffer.hs b/bittide-instances/src/Bittide/Instances/Tests/Ringbuffer.hs index e46bbdbb6..369c72245 100644 --- a/bittide-instances/src/Bittide/Instances/Tests/Ringbuffer.hs +++ b/bittide-instances/src/Bittide/Instances/Tests/Ringbuffer.hs @@ -20,6 +20,7 @@ import Data.Maybe (catMaybes) import GHC.Stack (HasCallStack) import Project.FilePath import Protocols +import Protocols.Extra (fmapC) import Protocols.Idle import Protocols.MemoryMap import System.FilePath (()) @@ -35,6 +36,7 @@ import Bittide.SharedTypes (withBittideByteOrder) import Bittide.Wishbone import qualified Data.List as L +import qualified Protocols.Vec as Vec createDomain vSystem{vName = "Slow", vPeriod = hzToPeriod 1000000} @@ -48,6 +50,9 @@ dutMM = $ withClockResetEnable @Slow clockGen (resetGenN d2) enableGen $ toSignals (dutWithBinary d0 "") ((), pure $ deepErrorX "memoryMap") +replicateC :: forall n a b. SNat n -> Circuit a b -> Circuit (Vec n a) (Vec n b) +replicateC SNat c = repeatC c + -- | Parameterized DUT that loads a specific firmware binary with configurable latency. dutWithBinary :: (HasCallStack, HiddenClockResetEnable dom, 1 <= DomainPeriod dom, KnownNat latency) => @@ -56,16 +61,23 @@ dutWithBinary :: Circuit (ToConstBwd Mm) (Df dom (BitVector 8)) dutWithBinary latency binaryName = withBittideByteOrder $ circuit $ \mm -> do (uartRx, jtagIdle) <- idleSource - [uartBus, wbTx, wbRx, timeBus] <- - processingElement NoDumpVcd (peConfig binaryName) -< (mm, jtagIdle) + ([uartBus, timeBus], wbTxs, wbRxs) <- + Vec.split3 + <| processingElement NoDumpVcd (peConfig binaryName) + -< (mm, jtagIdle) (uartTx, _uartStatus) <- uartInterfaceWb d16 d2 uartBytes -< (uartBus, uartRx) - txOut <- - transmitRingbufferWb (exposeEnable $ blockRamByteAddressable (Vec (repeat 0))) memDepth - -< wbTx + txOuts <- + replicateC + d2 + ( transmitRingbufferWb (exposeEnable $ blockRamByteAddressable (Vec (repeat 0))) memDepth + ) + -< wbTxs -- Add configurable latency between TX and RX ringbuffers - txOutDelayed <- applyC (toSignal . delayN latency 0 . fromSignal) id -< txOut - receiveRingbufferWb (\ena -> blockRam hasClock ena (replicate memDepth 0)) memDepth - -< (wbRx, txOutDelayed) + txOutDelayeds <- fmapC (applyC (toSignal . delayN latency 0 . fromSignal) id) -< txOuts + idleSink + <| fmapC (receiveRingbufferWb (\ena -> blockRam hasClock ena (replicate memDepth 0)) memDepth) + <| Vec.zip + -< (wbRxs, txOutDelayeds) _cnt <- timeWb Nothing -< timeBus idC -< uartTx where diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs index 3ecd46975..ad622383c 100644 --- a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs @@ -10,16 +10,14 @@ use bittide_hal::hals::soft_ugn_demo_mu::devices::{ReceiveRingbuffer, TransmitRi use bittide_hal::hals::soft_ugn_demo_mu::DeviceInstances; use bittide_hal::manual_additions::timer::Instant; use bittide_sys::link_startup::LinkStartup; -use bittide_sys::net_state::{ - Manager, SmoltcpLink, Subordinate, SubordinateState, UgnEdge, UgnReport, -}; +use bittide_sys::net_state::{Manager, Subordinate, UgnEdge, UgnReport}; use bittide_sys::smoltcp::soft_ugn_ringbuffer::{AlignedReceiveBuffer, RingbufferDevice}; use bittide_sys::stability_detector::Stability; use core::fmt::Write; use log::{info, trace, warn, LevelFilter}; use smoltcp::iface::{Config, Interface, SocketHandle, SocketSet, SocketStorage}; use smoltcp::socket::tcp; -use smoltcp::wire::HardwareAddress; +use smoltcp::wire::{HardwareAddress, IpAddress, IpCidr}; use ufmt::uwriteln; const INSTANCES: DeviceInstances = unsafe { DeviceInstances::new() }; @@ -27,9 +25,11 @@ const LINK_COUNT: usize = 7; const TCP_BUF_SIZE: usize = 256; const MANAGER_DNA: [u8; 12] = [133, 129, 48, 4, 64, 192, 105, 1, 1, 0, 2, 64]; const LOG_TICK_EVERY: u32 = 500; +const CLIENT_IP: [u8; 4] = [100, 100, 100, 100]; +const SERVER_IP: [u8; 4] = [100, 100, 100, 101]; -static mut TCP_RX_BUFS: [[u8; TCP_BUF_SIZE]; LINK_COUNT] = [[0; TCP_BUF_SIZE]; LINK_COUNT]; -static mut TCP_TX_BUFS: [[u8; TCP_BUF_SIZE]; LINK_COUNT] = [[0; TCP_BUF_SIZE]; LINK_COUNT]; +static mut TCP_RX_BUFS: [u8; TCP_BUF_SIZE] = [0; TCP_BUF_SIZE]; +static mut TCP_TX_BUFS: [u8; TCP_BUF_SIZE] = [0; TCP_BUF_SIZE]; #[cfg(not(test))] use riscv_rt::entry; @@ -38,6 +38,15 @@ fn to_smoltcp_instant(instant: Instant) -> smoltcp::time::Instant { smoltcp::time::Instant::from_micros(instant.micros() as i64) } +fn set_iface_ip(iface: &mut Interface, ip: [u8; 4]) { + iface.update_ip_addrs(|addrs| { + addrs.clear(); + addrs + .push(IpCidr::new(IpAddress::v4(ip[0], ip[1], ip[2], ip[3]), 24)) + .unwrap(); + }); +} + fn socket_set<'a>(storage: &'a mut [SocketStorage<'static>]) -> SocketSet<'a> { // SAFETY: Socket buffers are backed by static memory, and SocketSet does not // outlive the borrow of storage in this scope. @@ -85,7 +94,6 @@ fn main() -> ! { INSTANCES.capture_ugn_5, INSTANCES.capture_ugn_6, ]; - // Pseudocode setup: // 1) Initialize MU peripherals and scatter/gather calendars for ringbuffers. // 2) Align ringbuffers on all ports (two-phase protocol). @@ -144,44 +152,43 @@ fn main() -> ! { ); } - let mut devices: [RingbufferDevice; LINK_COUNT] = [ - make_device( - INSTANCES.receive_ringbuffer_0, - INSTANCES.transmit_ringbuffer_0, - ), - make_device( - INSTANCES.receive_ringbuffer_1, - INSTANCES.transmit_ringbuffer_1, - ), - make_device( - INSTANCES.receive_ringbuffer_2, - INSTANCES.transmit_ringbuffer_2, - ), - make_device( - INSTANCES.receive_ringbuffer_3, - INSTANCES.transmit_ringbuffer_3, - ), - make_device( - INSTANCES.receive_ringbuffer_4, - INSTANCES.transmit_ringbuffer_4, - ), - make_device( - INSTANCES.receive_ringbuffer_5, - INSTANCES.transmit_ringbuffer_5, - ), - make_device( - INSTANCES.receive_ringbuffer_6, - INSTANCES.transmit_ringbuffer_6, - ), + let receive_ringbuffers = [ + INSTANCES.receive_ringbuffer_0, + INSTANCES.receive_ringbuffer_1, + INSTANCES.receive_ringbuffer_2, + INSTANCES.receive_ringbuffer_3, + INSTANCES.receive_ringbuffer_4, + INSTANCES.receive_ringbuffer_5, + INSTANCES.receive_ringbuffer_6, ]; - let mut ifaces: [Interface; LINK_COUNT] = core::array::from_fn(|idx| { - let now = to_smoltcp_instant(INSTANCES.timer.now()); - let mut iface = Interface::new(Config::new(HardwareAddress::Ip), &mut devices[idx], now); - iface.update_ip_addrs(|addrs| { - addrs.clear(); - }); - iface + let transmit_ringbuffers = [ + INSTANCES.transmit_ringbuffer_0, + INSTANCES.transmit_ringbuffer_1, + INSTANCES.transmit_ringbuffer_2, + INSTANCES.transmit_ringbuffer_3, + INSTANCES.transmit_ringbuffer_4, + INSTANCES.transmit_ringbuffer_5, + INSTANCES.transmit_ringbuffer_6, + ]; + let mut receive_iter = receive_ringbuffers.into_iter(); + let mut transmit_iter = transmit_ringbuffers.into_iter(); + let mut devices: [RingbufferDevice; LINK_COUNT] = core::array::from_fn(|_| { + let rx = receive_iter.next().expect("missing receive ringbuffer"); + let tx = transmit_iter.next().expect("missing transmit ringbuffer"); + make_device(rx, tx) }); + + let rx_buf = unsafe { &mut TCP_RX_BUFS[..] }; + let tx_buf = unsafe { &mut TCP_TX_BUFS[..] }; + let socket = tcp::Socket::new( + tcp::SocketBuffer::new(rx_buf), + tcp::SocketBuffer::new(tx_buf), + ); + let mut sockets_storage: [SocketStorage<'static>; 1] = Default::default(); + let socket_handle = { + let mut sockets = socket_set(&mut sockets_storage[..]); + sockets.add(socket) + }; let dna = INSTANCES.dna.dna(); info!("My dna: {:?}", dna); let is_manager = dna == MANAGER_DNA; @@ -195,78 +202,24 @@ fn main() -> ! { let mut reports: [Option; LINK_COUNT] = [None; LINK_COUNT]; for link in 0..LINK_COUNT { info!("Starting manager for link {}", link); - let rx_buf = unsafe { &mut TCP_RX_BUFS[link][..] }; - let tx_buf = unsafe { &mut TCP_TX_BUFS[link][..] }; - let socket = tcp::Socket::new( - tcp::SocketBuffer::new(rx_buf), - tcp::SocketBuffer::new(tx_buf), - ); - let mut sockets_storage: [SocketStorage<'static>; 1] = Default::default(); - let socket_handle = { - let mut sockets = socket_set(&mut sockets_storage[..]); - sockets.add(socket) - }; - let mut manager = Manager::new(); - let mut last_state = manager.state(); - let mut tick: u32 = 0; + let now = to_smoltcp_instant(INSTANCES.timer.now()); + let mut iface = + Interface::new(Config::new(HardwareAddress::Ip), &mut devices[link], now); + set_iface_ip(&mut iface, CLIENT_IP); + let mut manager = Manager::new(iface, socket_handle, link, SERVER_IP); trace!("Starting manager loop for link {}", link); loop { - tick = tick.wrapping_add(1); - if tick % LOG_TICK_EVERY == 0 { - info!("manager link {} tick {}", link, tick); - } let now = to_smoltcp_instant(INSTANCES.timer.now()); let mut sockets = socket_set(&mut sockets_storage[..]); - let poll_result = ifaces[link].poll(now, &mut devices[link], &mut sockets); - trace!("manager link {} poll result {:?}", link, poll_result); - { - let socket = sockets.get::(socket_handle); - trace!( - "manager link {} socket open {} active {} can_send {} can_recv {} state {:?}", - link, - socket.is_open(), - socket.is_active(), - socket.can_send(), - socket.can_recv(), - socket.state() - ); - } - trace!( - "manager link {} ip addrs {:?}", - link, - ifaces[link].ip_addrs() - ); - { - let mut smoltcp_link = SmoltcpLink::new( - &mut ifaces[link], - &mut sockets, - socket_handle, - link, - true, - false, - ); - trace!( - "manager link {} step from state {:?}", - link, - manager.state() - ); - manager.step(&mut smoltcp_link); - } - let state = manager.state(); - if state != last_state { - info!( - "manager link {} state {:?} -> {:?}", - link, last_state, state - ); - last_state = state; - } + manager.poll(now, &mut devices[link], &mut sockets); + trace!("manager link {} state {:?}", link, manager.state()); if manager.is_done() { trace!("manager link {} is done", link); break; } } - reports[link] = manager.report(); + reports[link] = Some(manager.report()); } info!("UGN reports from subordinates:"); @@ -297,15 +250,11 @@ fn main() -> ! { } } else { info!("Starting subordinate state machines..."); - let mut subordinates: [Subordinate; LINK_COUNT] = - core::array::from_fn(|_| Subordinate::new()); - let mut last_states: [SubordinateState; LINK_COUNT] = - core::array::from_fn(|_| SubordinateState::WaitForPhy); let mut sockets_storage: [[SocketStorage<'static>; 1]; LINK_COUNT] = core::array::from_fn(|_| Default::default()); let socket_handles: [SocketHandle; LINK_COUNT] = core::array::from_fn(|idx| { - let rx_buf = unsafe { &mut TCP_RX_BUFS[idx][..] }; - let tx_buf = unsafe { &mut TCP_TX_BUFS[idx][..] }; + let rx_buf = unsafe { &mut TCP_RX_BUFS[..] }; + let tx_buf = unsafe { &mut TCP_TX_BUFS[..] }; let socket = tcp::Socket::new( tcp::SocketBuffer::new(rx_buf), tcp::SocketBuffer::new(tx_buf), @@ -313,6 +262,13 @@ fn main() -> ! { let mut sockets = socket_set(&mut sockets_storage[idx][..]); sockets.add(socket) }); + let mut subordinates: [Subordinate; LINK_COUNT] = core::array::from_fn(|idx| { + let now = to_smoltcp_instant(INSTANCES.timer.now()); + let mut iface = + Interface::new(Config::new(HardwareAddress::Ip), &mut devices[idx], now); + set_iface_ip(&mut iface, SERVER_IP); + Subordinate::new(iface, socket_handles[idx], idx, dna) + }); let mut tick: u32 = 0; for link in 0..LINK_COUNT { subordinates[link].set_report(build_report_for_link(link, &capture_ugns[link], &dna)); @@ -326,8 +282,7 @@ fn main() -> ! { let now = to_smoltcp_instant(INSTANCES.timer.now()); for link in 0..LINK_COUNT { let mut sockets = socket_set(&mut sockets_storage[link][..]); - let poll_result = ifaces[link].poll(now, &mut devices[link], &mut sockets); - trace!("subordinate link {} poll result {:?}", link, poll_result); + subordinates[link].poll(now, &mut devices[link], &mut sockets); { let socket = sockets.get::(socket_handles[link]); trace!( @@ -343,32 +298,13 @@ fn main() -> ! { trace!( "subordinate link {} ip addrs {:?}", link, - ifaces[link].ip_addrs() + subordinates[link].iface().ip_addrs() + ); + trace!( + "subordinate link {} state {:?}", + link, + subordinates[link].state() ); - { - let mut smoltcp_link = SmoltcpLink::new( - &mut ifaces[link], - &mut sockets, - socket_handles[link], - link, - true, - false, - ); - trace!( - "subordinate link {} step from state {:?}", - link, - subordinates[link].state() - ); - subordinates[link].step(&mut smoltcp_link); - } - let state = subordinates[link].state(); - if state != last_states[link] { - info!( - "subordinate link {} state {:?} -> {:?}", - link, last_states[link], state - ); - last_states[link] = state; - } } } } diff --git a/firmware-binaries/sim-tests/aligned_ringbuffer_test/src/main.rs b/firmware-binaries/sim-tests/aligned_ringbuffer_test/src/main.rs index 73794930c..6b7cdf24a 100644 --- a/firmware-binaries/sim-tests/aligned_ringbuffer_test/src/main.rs +++ b/firmware-binaries/sim-tests/aligned_ringbuffer_test/src/main.rs @@ -21,8 +21,8 @@ fn main() -> ! { writeln!(uart, "=== Aligned Ringbuffer Test ===").unwrap(); - let tx_ringbuffer = INSTANCES.transmit_ringbuffer; - let rx_ringbuffer = INSTANCES.receive_ringbuffer; + let tx_ringbuffer = INSTANCES.transmit_ringbuffer_0; + let rx_ringbuffer = INSTANCES.receive_ringbuffer_0; // Step 1: Perform alignment procedure writeln!(uart, "\n--- Step 1: Alignment Discovery ---").unwrap(); diff --git a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs index 4edf36059..010c6be20 100644 --- a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs +++ b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs @@ -8,7 +8,7 @@ use bittide_hal::manual_additions::ringbuffer_test::ringbuffers::AlignedReceiveBuffer; use bittide_hal::manual_additions::timer::Instant; use bittide_hal::ringbuffer_test::DeviceInstances; -use bittide_sys::net_state::{Manager, SmoltcpLink, Subordinate, UgnEdge, UgnReport}; +use bittide_sys::net_state::{Manager, Subordinate, UgnEdge, UgnReport}; use bittide_sys::smoltcp::ringbuffer::RingbufferDevice; use core::fmt::Write; use log::{info, trace, LevelFilter}; @@ -46,39 +46,79 @@ fn main() -> ! { // Set up ringbuffers info!("Step 1: Finding ringbuffer alignment..."); - let tx_buffer = INSTANCES.transmit_ringbuffer; - let rx_buffer = INSTANCES.receive_ringbuffer; - let mut rx_aligned = AlignedReceiveBuffer::new(rx_buffer); - rx_aligned.align(&tx_buffer); - let rx_offset = rx_aligned + let tx_buffer0 = INSTANCES.transmit_ringbuffer_0; + let rx_buffer0 = INSTANCES.receive_ringbuffer_0; + + let tx_buffer1 = INSTANCES.transmit_ringbuffer_1; + let rx_buffer1 = INSTANCES.receive_ringbuffer_1; + let mut rx_aligned0 = AlignedReceiveBuffer::new(rx_buffer0); + let mut rx_aligned1 = AlignedReceiveBuffer::new(rx_buffer1); + + rx_aligned0.align(&tx_buffer0); + rx_aligned1.align(&tx_buffer1); + + let rx_offset0 = rx_aligned0 .get_alignment_offset() .expect("Failed to find RX buffer alignment"); - trace!(" Alignment offset: {}", rx_offset); + let rx_offset1 = rx_aligned1 + .get_alignment_offset() + .expect("Failed to find RX buffer alignment"); + trace!(" Alignment offset 0: {}", rx_offset0); + trace!(" Alignment offset 1: {}", rx_offset1); // Step 2: Create smoltcp device info!("Step 2: Creating RingbufferDevice..."); - let mut device = RingbufferDevice::new(rx_aligned, tx_buffer); - let mtu = device.mtu(); + let mut device0 = RingbufferDevice::new(rx_aligned0, tx_buffer1); + let mut device1 = RingbufferDevice::new(rx_aligned1, tx_buffer0); + let mtu = device0.mtu(); trace!(" MTU: {} bytes", mtu); // Step 3: Configure network interface - info!("Step 3: Configuring network interface..."); + info!("Step 3: Configuring network interfaces..."); let hw_addr = HardwareAddress::Ip; - let config = Config::new(hw_addr); + let config0 = Config::new(hw_addr); + let config1 = Config::new(hw_addr); let now = to_smoltcp_instant(timer.now()); - let mut iface = Interface::new(config, &mut device, now); - iface.update_ip_addrs(|addrs| { - addrs.clear(); + let mut iface0 = Interface::new(config0, &mut device0, now); + let mut iface1 = Interface::new(config1, &mut device1, now); + let server_ip = [100, 100, 100, 100]; + let client_ip = [100, 100, 100, 101]; + iface0.update_ip_addrs(|addrs| { + addrs + .push(smoltcp::wire::IpCidr::new( + smoltcp::wire::IpAddress::v4( + client_ip[0], + client_ip[1], + client_ip[2], + client_ip[3], + ), + 24, + )) + .unwrap(); + }); + iface1.update_ip_addrs(|addrs| { + addrs + .push(smoltcp::wire::IpCidr::new( + smoltcp::wire::IpAddress::v4( + server_ip[0], + server_ip[1], + server_ip[2], + server_ip[3], + ), + 24, + )) + .unwrap(); }); // Step 4: Create TCP sockets info!("Step 4: Creating TCP sockets..."); // Server socket - reduced buffer sizes to fit in memory - static mut SERVER_RX_BUF: [u8; 256] = [0; 256]; - static mut SERVER_TX_BUF: [u8; 256] = [0; 256]; - let server_rx_buffer = tcp::SocketBuffer::new(unsafe { &mut SERVER_RX_BUF[..] }); - let server_tx_buffer = tcp::SocketBuffer::new(unsafe { &mut SERVER_TX_BUF[..] }); + static mut SERVER_RX_BUF0: [u8; 256] = [0; 256]; + static mut SERVER_TX_BUF0: [u8; 256] = [0; 256]; + + let server_rx_buffer = tcp::SocketBuffer::new(unsafe { &mut SERVER_RX_BUF0[..] }); + let server_tx_buffer = tcp::SocketBuffer::new(unsafe { &mut SERVER_TX_BUF0[..] }); let server_socket = tcp::Socket::new(server_rx_buffer, server_tx_buffer); // Client socket - reduced buffer sizes to fit in memory @@ -88,10 +128,13 @@ fn main() -> ! { let client_tx_buffer = tcp::SocketBuffer::new(unsafe { &mut CLIENT_TX_BUF[..] }); let client_socket = tcp::Socket::new(client_rx_buffer, client_tx_buffer); - let mut sockets_storage: [SocketStorage; 2] = Default::default(); - let mut sockets = SocketSet::new(&mut sockets_storage[..]); - let server_handle = sockets.add(server_socket); - let client_handle = sockets.add(client_socket); + let mut server_sockets_storage: [SocketStorage; 1] = Default::default(); + let mut client_sockets_storage: [SocketStorage; 1] = Default::default(); + let mut server_sockets = SocketSet::new(&mut server_sockets_storage[..]); + let mut client_sockets = SocketSet::new(&mut client_sockets_storage[..]); + + let server_handle = server_sockets.add(server_socket); + let client_handle = client_sockets.add(client_socket); // Step 5: Initialize link state machines info!("Step 5: Initializing link state machines..."); @@ -100,24 +143,15 @@ fn main() -> ! { info!("Step 7: Running main event loop..."); let mut done_logged = false; - let mut manager = Manager::new(); - let mut subordinate = Subordinate::new(); + let mut manager = Manager::new(iface0, client_handle, 0, server_ip); + let dna: [u8; 12] = core::array::from_fn(|i| i as u8); + let mut subordinate = Subordinate::new(iface1, server_handle, 0, dna); subordinate.set_report(build_placeholder_report()); for _ in 0..1000 { let timestamp = to_smoltcp_instant(timer.now()); - iface.poll(timestamp, &mut device, &mut sockets); - - { - let mut link = - SmoltcpLink::new(&mut iface, &mut sockets, client_handle, 0, true, false); - manager.step(&mut link); - } - { - let mut link = - SmoltcpLink::new(&mut iface, &mut sockets, server_handle, 0, true, false); - subordinate.step(&mut link); - } + manager.poll(timestamp, &mut device0, &mut client_sockets); + subordinate.poll(timestamp, &mut device1, &mut server_sockets); if manager.is_done() && subordinate.is_done() && !done_logged { info!(" Manager collected UGN report"); @@ -131,7 +165,8 @@ fn main() -> ! { if !done_logged { info!(" FAILURE: UGN report timeout!"); - } else if let Some(report) = manager.report() { + } else { + let report = manager.report(); info!(" SUCCESS: Manager received {} UGN edges", report.count); for (idx, edge) in report.edges.iter().enumerate() { if idx >= report.count as usize { @@ -146,8 +181,6 @@ fn main() -> ! { info!(" Edge {}: missing", idx); } } - } else { - info!(" FAILURE: Missing UGN report data!"); } uwriteln!(uart, "=== Test Complete ===").unwrap(); diff --git a/firmware-binaries/sim-tests/ringbuffer_test/src/main.rs b/firmware-binaries/sim-tests/ringbuffer_test/src/main.rs index 51d36b627..43316708c 100644 --- a/firmware-binaries/sim-tests/ringbuffer_test/src/main.rs +++ b/firmware-binaries/sim-tests/ringbuffer_test/src/main.rs @@ -24,8 +24,8 @@ fn main() -> ! { uwriteln!(uart, "=== Ringbuffer Loopback Test (Byte-Level) ===").unwrap(); - let tx_ringbuffer = INSTANCES.transmit_ringbuffer; - let rx_ringbuffer = INSTANCES.receive_ringbuffer; + let tx_ringbuffer = INSTANCES.transmit_ringbuffer_0; + let rx_ringbuffer = INSTANCES.receive_ringbuffer_0; // Create pattern: 4 MSBs = frame number, 4 LSBs = byte index in frame // Frame 0: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07] diff --git a/firmware-support/bittide-sys/src/net_state.rs b/firmware-support/bittide-sys/src/net_state.rs index 9a26736b6..2e353b596 100644 --- a/firmware-support/bittide-sys/src/net_state.rs +++ b/firmware-support/bittide-sys/src/net_state.rs @@ -5,7 +5,7 @@ use log::{debug, info, trace, warn}; use smoltcp::iface::{Interface, SocketHandle, SocketSet}; use smoltcp::socket::tcp; -use smoltcp::wire::{IpAddress, IpCidr}; +use smoltcp::wire::IpAddress; use zerocopy::byteorder::{I64, LE, U32}; use zerocopy::{AsBytes, FromBytes, FromZeroes, Ref, Unaligned}; @@ -17,8 +17,6 @@ pub enum NodeRole { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum ManagerState { - WaitForPhy, - SetupInterface, WaitForSession, Identifying, ReceivingUgns, @@ -28,25 +26,17 @@ pub enum ManagerState { #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum SubordinateState { - WaitForPhy, - SetupInterface, WaitForSession, + Identifying, SendingUgns, Done, Failed, } pub const MAX_UGN_EDGES: usize = 8; -pub const UGN_EDGE_BYTES: usize = 16; +pub const UGN_EDGE_BYTES: usize = core::mem::size_of::(); -const MSG_WHO_ARE_YOU: u8 = 1; -const MSG_HELLO: u8 = 2; -const MSG_UGN_DUMP: u8 = 3; -const ROLE_MANAGER: u8 = 0; -const ROLE_SUBORDINATE: u8 = 1; -const WHO_ARE_YOU_BYTES: usize = 8; -const HELLO_BYTES: usize = 8; -const UGN_DUMP_BYTES: usize = 1; +const DNA_BYTES: usize = 12; #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct UgnEdge { @@ -77,6 +67,8 @@ impl UgnReport { struct UgnEdgeWire { src_node: U32, dst_node: U32, + src_port: U32, + dst_port: U32, ugn: I64, } @@ -85,6 +77,8 @@ impl From for UgnEdgeWire { Self { src_node: U32::new(edge.src_node), dst_node: U32::new(edge.dst_node), + src_port: U32::new(edge.src_port), + dst_port: U32::new(edge.dst_port), ugn: I64::new(edge.ugn), } } @@ -95,85 +89,183 @@ impl From for UgnEdge { Self { src_node: edge.src_node.get(), dst_node: edge.dst_node.get(), - src_port: 0, - dst_port: 0, + src_port: edge.src_port.get(), + dst_port: edge.dst_port.get(), ugn: edge.ugn.get(), } } } -const _: [(); UGN_EDGE_BYTES] = [(); core::mem::size_of::()]; +// const _: [(); UGN_EDGE_BYTES] = [(); core::mem::size_of::()]; const TCP_SERVER_PORT: u16 = 8080; const TCP_CLIENT_PORT: u16 = 49152; -pub struct SmoltcpLink<'a, 'b> { - iface: &'a mut Interface, - sockets: &'a mut SocketSet<'b>, +pub struct Manager { + iface: Interface, socket_handle: SocketHandle, link: usize, - phy_ready: bool, - timed_out: bool, + partner_ip: [u8; 4], + state: ManagerState, + retries: u8, + report: UgnReport, + partner_dna: Option<[u8; DNA_BYTES]>, } -impl<'a, 'b> SmoltcpLink<'a, 'b> { +impl Manager { pub fn new( - iface: &'a mut Interface, - sockets: &'a mut SocketSet<'b>, + iface: Interface, socket_handle: SocketHandle, link: usize, - phy_ready: bool, - timed_out: bool, + partner_ip: [u8; 4], ) -> Self { Self { iface, - sockets, socket_handle, link, - phy_ready, - timed_out, + partner_ip, + state: ManagerState::WaitForSession, + retries: 0, + report: UgnReport::new(), + partner_dna: None, } } - pub fn link(&self) -> usize { - self.link + pub fn iface(&self) -> &Interface { + &self.iface } - pub fn phy_ready(&self) -> bool { - self.phy_ready + pub fn iface_mut(&mut self) -> &mut Interface { + &mut self.iface } - pub fn timed_out(&self) -> bool { - self.timed_out + pub fn step(&mut self, sockets: &mut SocketSet) { + let prev_state = self.state; + let next_state = match self.state { + ManagerState::WaitForSession => { + self.connect(sockets); + if self.can_receive(sockets) { + debug!( + "manager connected to peer {:?} on link {}", + self.partner_ip, self.link + ); + ManagerState::Identifying + } else { + self.state + } + } + ManagerState::Identifying => { + // Expect the subordinate to identify itself + + let mut buf = [0u8; DNA_BYTES]; + match self.recv(sockets, &mut buf) { + Some(len) if len == DNA_BYTES => { + self.partner_dna = Some(buf); + info!( + "manager received partner DNA {:02X?} on link {}", + self.partner_dna, self.link + ); + ManagerState::ReceivingUgns + } + Some(len) => { + warn!( + "manager received invalid identify len {} on link {}", + len, self.link + ); + self.reset(sockets); + self.bump_retry_or_fail(ManagerState::Identifying) + } + None => self.state, + } + } + ManagerState::ReceivingUgns => { + trace!("manager receiving ugns on link {}", self.link); + if self.can_receive(sockets) { + let mut buf = [0u8; UGN_EDGE_BYTES]; + match self.recv(sockets, &mut buf) { + Some(len) if len == UGN_EDGE_BYTES => match parse_ugn_edge(&buf[..len]) { + Some(edge) => { + info!( + "manager received edge src {} dst {} ugn {} on link {}", + edge.src_node, edge.dst_node, edge.ugn, self.link + ); + self.insert_edge(edge); + self.state + } + None => { + warn!("manager received invalid ugn edge on link {}", self.link); + self.reset(sockets); + self.bump_retry_or_fail(ManagerState::ReceivingUgns) + } + }, + Some(len) => { + warn!( + "manager received invalid ugn edge len {} on link {}", + len, self.link + ); + self.reset(sockets); + self.bump_retry_or_fail(ManagerState::ReceivingUgns) + } + None => self.state, + } + } else { + ManagerState::Done + } + } + ManagerState::Done => self.state, + ManagerState::Failed => self.state, + }; + if next_state != prev_state { + info!("manager state {:?} -> {:?}", prev_state, next_state); + } + self.state = next_state; } - pub fn setup_interface(&mut self, role: NodeRole) { - let local_ip = ip_for_link(role, self.link); - let ip = IpCidr::new( - IpAddress::v4(local_ip[0], local_ip[1], local_ip[2], local_ip[3]), - 24, - ); - self.iface.update_ip_addrs(|addrs| { - if !addrs.contains(&ip) { - let _ = addrs.push(ip); - } - }); - trace!( - "link {} role {:?} ip addrs {:?}", - self.link, - role, - self.iface.ip_addrs() - ); + pub fn poll( + &mut self, + timestamp: smoltcp::time::Instant, + device: &mut impl smoltcp::phy::Device, + sockets: &mut SocketSet, + ) { + self.iface.poll(timestamp, device, sockets); + self.step(sockets); + } + + pub fn is_done(&self) -> bool { + self.state == ManagerState::Done + } + + pub fn is_connected(&self) -> bool { + self.is_done() + } + + pub fn state(&self) -> ManagerState { + self.state + } + + pub fn report(&self) -> UgnReport { + self.report } - pub fn connect(&mut self, peer_ip: [u8; 4]) -> bool { - let socket = self.sockets.get_mut::(self.socket_handle); + fn reset(&mut self, sockets: &mut SocketSet) { + let socket = sockets.get_mut::(self.socket_handle); + debug!("link {} resetting interface", self.link); + socket.close(); + } + + fn connect(&mut self, sockets: &mut SocketSet) -> bool { + let socket = sockets.get_mut::(self.socket_handle); if !socket.is_open() && !socket.is_active() { let cx = self.iface.context(); match socket.connect( cx, ( - IpAddress::v4(peer_ip[0], peer_ip[1], peer_ip[2], peer_ip[3]), + IpAddress::v4( + self.partner_ip[0], + self.partner_ip[1], + self.partner_ip[2], + self.partner_ip[3], + ), TCP_SERVER_PORT, ), TCP_CLIENT_PORT, @@ -181,7 +273,7 @@ impl<'a, 'b> SmoltcpLink<'a, 'b> { Ok(()) => { debug!( "link {} connect requested to {:?}:{} from {}", - self.link, peer_ip, TCP_SERVER_PORT, TCP_CLIENT_PORT + self.link, self.partner_ip, TCP_SERVER_PORT, TCP_CLIENT_PORT ); } Err(err) => { @@ -201,55 +293,8 @@ impl<'a, 'b> SmoltcpLink<'a, 'b> { socket.is_active() } - pub fn listen(&mut self) -> bool { - let socket = self.sockets.get_mut::(self.socket_handle); - if !socket.is_open() { - match socket.listen(TCP_SERVER_PORT) { - Ok(()) => { - debug!("link {} listen on port {}", self.link, TCP_SERVER_PORT); - } - Err(err) => { - debug!("link {} listen error: {:?}", self.link, err); - } - } - } - trace!( - "link {} listen state open {} active {} can_send {} can_recv {} may_recv {}", - self.link, - socket.is_open(), - socket.is_active(), - socket.can_send(), - socket.can_recv(), - socket.may_recv() - ); - socket.is_open() - } - - pub fn send(&mut self, data: &[u8]) -> bool { - let socket = self.sockets.get_mut::(self.socket_handle); - if socket.can_send() { - match socket.send_slice(data) { - Ok(len) => { - trace!("link {} sent {} bytes", self.link, len); - return true; - } - Err(err) => { - debug!("link {} send error: {:?}", self.link, err); - } - } - } else { - trace!( - "link {} send blocked open {} active {}", - self.link, - socket.is_open(), - socket.is_active() - ); - } - false - } - - pub fn recv(&mut self, buf: &mut [u8]) -> Option { - let socket = self.sockets.get_mut::(self.socket_handle); + fn recv(&mut self, sockets: &mut SocketSet, buf: &mut [u8]) -> Option { + let socket = sockets.get_mut::(self.socket_handle); if socket.can_recv() { match socket.recv_slice(buf) { Ok(len) => { @@ -271,296 +316,115 @@ impl<'a, 'b> SmoltcpLink<'a, 'b> { None } - pub fn is_open_manager(&mut self) -> bool { - let socket = self.sockets.get::(self.socket_handle); + fn can_receive(&self, sockets: &SocketSet) -> bool { + let socket = sockets.get::(self.socket_handle); socket.may_recv() } - pub fn close(&mut self) { - let socket = self.sockets.get_mut::(self.socket_handle); - socket.close(); - } -} - -pub struct Manager { - state: ManagerState, - retries: u8, - nonce_seed: u32, - nonce: u32, - received_edges: u8, - report: Option, - identifying_sent: bool, - ugn_dump_sent: bool, -} - -impl Manager { - pub fn new() -> Self { - Self { - state: ManagerState::WaitForPhy, - retries: 0, - nonce_seed: 1, - nonce: 0, - received_edges: 0, - report: None, - identifying_sent: false, - ugn_dump_sent: false, - } - } - - pub fn step(&mut self, link: &mut SmoltcpLink<'_, '_>) { - let prev_state = self.state; - let next_state = match self.state { - ManagerState::WaitForPhy => { - trace!("Checking phy ready on link {}", link.link()); - if link.phy_ready() { - debug!("manager phy ready on link {}", link.link()); - ManagerState::SetupInterface - } else { - self.state - } - } - ManagerState::SetupInterface => { - debug!("manager setup interface link {}", link.link()); - link.setup_interface(NodeRole::Manager); - ManagerState::WaitForSession - } - ManagerState::WaitForSession => { - let peer_ip = ip_for_link(NodeRole::Subordinate, link.link()); - trace!( - "manager attempting connect to peer {:?} on link {}", - peer_ip, - link.link() - ); - if link.connect(peer_ip) { - debug!("manager connected link {} peer {:?}", link.link(), peer_ip); - self.identifying_sent = false; - self.ugn_dump_sent = false; - ManagerState::Identifying - } else if link.timed_out() { - self.bump_retry_or_fail(ManagerState::WaitForSession) - } else { - self.state - } - } - ManagerState::Identifying => { - trace!("manager identifying link {}", link.link()); - if !self.identifying_sent { - self.nonce = self.next_nonce(); - let msg = encode_who_are_you(self.nonce); - if link.send(&msg) { - debug!("manager sent who-are-you nonce {}", self.nonce); - self.identifying_sent = true; - } - } - - if !self.identifying_sent { - self.state - } else { - let mut buf = [0u8; HELLO_BYTES]; - if let Some(len) = link.recv(&mut buf) { - if parse_hello(&buf[..len], self.nonce) { - debug!("manager received hello nonce {}", self.nonce); - self.ugn_dump_sent = false; - ManagerState::ReceivingUgns - } else { - warn!("manager received invalid hello len {}", len); - self.state - } - } else if link.timed_out() { - self.identifying_sent = false; - self.bump_retry_or_fail(ManagerState::Identifying) - } else { - self.state - } - } - } - ManagerState::ReceivingUgns => { - trace!("manager receiving ugns on link {}", link.link()); - if !self.ugn_dump_sent { - let msg = [MSG_UGN_DUMP]; - if link.send(&msg) { - debug!("manager sent ugn dump"); - self.ugn_dump_sent = true; - } - } - - if !self.ugn_dump_sent { - self.state - } else if !link.is_open_manager() { - if self.report.is_none() { - self.report = Some(UgnReport::new()); - } - info!( - "manager connection closed after {} edges", - self.received_edges - ); - ManagerState::Done - } else { - let mut buf = [0u8; UGN_EDGE_BYTES]; - if let Some(len) = link.recv(&mut buf) { - if let Some(edge) = parse_ugn_edge(&buf[..len]) { - let mut inserted = false; - { - let report = self.report.get_or_insert_with(UgnReport::new); - if insert_edge(report, edge) { - inserted = true; - } - } - if inserted { - self.received_edges = self.received_edges.saturating_add(1); - if let Some(report) = self.report.as_mut() { - report.count = self.received_edges; - } - debug!( - "manager stored edge src {} dst {} ugn {}", - edge.src_node, edge.dst_node, edge.ugn - ); - } else { - trace!( - "manager dropped duplicate/full edge src {} dst {}", - edge.src_node, - edge.dst_node - ); - } - self.state - } else { - debug!("manager received invalid edge len {}", len); - self.state - } - } else if link.timed_out() { - self.ugn_dump_sent = false; - self.bump_retry_or_fail(ManagerState::ReceivingUgns) - } else { - self.state - } - } - } - ManagerState::Done => self.state, - ManagerState::Failed => self.state, - }; - if next_state != prev_state { - info!("manager state {:?} -> {:?}", prev_state, next_state); - } - self.state = next_state; - } - - pub fn is_done(&self) -> bool { - self.state == ManagerState::Done - } - - pub fn is_connected(&self) -> bool { - self.is_done() - } - - pub fn state(&self) -> ManagerState { - self.state - } - - pub fn report(&self) -> Option { - self.report - } - fn bump_retry_or_fail(&mut self, retry_state: ManagerState) -> ManagerState { self.retries = self.retries.saturating_add(1); if self.retries >= 3 { warn!("manager retry limit hit, entering Failed"); ManagerState::Failed } else { - info!( - "manager timeout, retry {} -> {:?}", - self.retries, retry_state - ); retry_state } } - fn next_nonce(&mut self) -> u32 { - let next = self.nonce_seed; - self.nonce_seed = self.nonce_seed.wrapping_add(1); - next - } -} - -impl Default for Manager { - fn default() -> Self { - Self::new() + fn insert_edge(&mut self, edge: UgnEdge) -> bool { + if self.report.count >= MAX_UGN_EDGES as u8 { + warn!("manager received edge but report is full, ignoring"); + return false; + } + self.report.edges[self.report.count as usize] = Some(edge); + self.report.count = self.report.count.saturating_add(1); + true } } pub struct Subordinate { + iface: Interface, + socket_handle: SocketHandle, + link: usize, state: SubordinateState, - retries: u8, - nonce: u32, report: UgnReport, sent_edges: [bool; MAX_UGN_EDGES], sent_count: u8, - who_are_you_received: bool, - hello_sent: bool, - ugn_dump_received: bool, + dna: [u8; DNA_BYTES], } impl Subordinate { - pub fn new() -> Self { + pub fn new( + iface: Interface, + socket_handle: SocketHandle, + link: usize, + dna: [u8; DNA_BYTES], + ) -> Self { Self { - state: SubordinateState::WaitForPhy, - retries: 0, - nonce: 0, + iface, + socket_handle, + link, + state: SubordinateState::WaitForSession, report: UgnReport::new(), sent_edges: [false; MAX_UGN_EDGES], sent_count: 0, - who_are_you_received: false, - hello_sent: false, - ugn_dump_received: false, + dna, } } - pub fn with_report(report: UgnReport) -> Self { - Self { - report, - ..Self::new() - } + pub fn iface(&self) -> &Interface { + &self.iface + } + + pub fn iface_mut(&mut self) -> &mut Interface { + &mut self.iface } pub fn set_report(&mut self, report: UgnReport) { self.report = report; self.sent_edges = [false; MAX_UGN_EDGES]; self.sent_count = 0; - self.reset_handshake(); } - pub fn step(&mut self, link: &mut SmoltcpLink<'_, '_>) { + pub fn insert_edge(&mut self, edge: UgnEdge) -> bool { + if self.report.count >= MAX_UGN_EDGES as u8 { + warn!("subordinate received edge but report is full, ignoring"); + return false; + } + self.report.edges[self.report.count as usize] = Some(edge); + self.report.count = self.report.count.saturating_add(1); + true + } + + pub fn step(&mut self, sockets: &mut SocketSet) { let prev_state = self.state; let next_state = match self.state { - SubordinateState::WaitForPhy => { + SubordinateState::WaitForSession => { trace!( - "subordinate state {:?} checking phy ready on link {}", + "subordinate state {:?} attempting listen on link {}", self.state, - link.link() + self.link ); - if link.phy_ready() { - debug!("subordinate phy ready on link {}", link.link()); - SubordinateState::SetupInterface + if self.listen(sockets) { + debug!("subordinate listening on link {}", self.link); + + SubordinateState::Identifying } else { self.state } } - SubordinateState::SetupInterface => { - debug!("subordinate setup interface link {}", link.link()); - link.setup_interface(NodeRole::Subordinate); - SubordinateState::WaitForSession - } - SubordinateState::WaitForSession => { - trace!( - "subordinate state {:?} attempting listen on link {}", - self.state, - link.link() - ); - if link.listen() { - debug!("subordinate listening on link {}", link.link()); - self.reset_handshake(); - SubordinateState::SendingUgns - } else if link.timed_out() { - self.bump_retry_or_fail(SubordinateState::WaitForSession) + SubordinateState::Identifying => { + if self.can_send(sockets) { + let dna = self.dna; + if self.send(sockets, &dna) { + debug!( + "subordinate sent dna {:02X?} on link {}", + self.dna, self.link + ); + SubordinateState::SendingUgns + } else { + self.state + } } else { self.state } @@ -569,77 +433,29 @@ impl Subordinate { trace!( "subordinate state {:?} checking for manager connection on link {}", self.state, - link.link() + self.link ); - if link.timed_out() && !self.ugn_dump_received { - self.reset_handshake(); - self.bump_retry_or_fail(SubordinateState::SendingUgns) - } else if !self.who_are_you_received { - let mut buf = [0u8; WHO_ARE_YOU_BYTES]; - if let Some(len) = link.recv(&mut buf) { - if let Some(nonce) = parse_who_are_you(&buf[..len]) { - self.nonce = nonce; - self.who_are_you_received = true; - debug!("subordinate received who-are-you nonce {}", nonce); - } else { - warn!("subordinate received invalid who-are-you len {}", len); - } - } - self.state - } else if !self.hello_sent { - let msg = encode_hello(self.nonce); - if link.send(&msg) { - debug!("subordinate sent hello nonce {}", self.nonce); - self.hello_sent = true; - } - self.state - } else if !self.ugn_dump_received { - let mut buf = [0u8; UGN_DUMP_BYTES]; - if let Some(len) = link.recv(&mut buf) { - if parse_ugn_dump(&buf[..len]) { - debug!("subordinate received ugn dump"); - self.ugn_dump_received = true; - } else { - warn!("subordinate received invalid ugn dump len {}", len); - } - } - self.state - } else { - let total = self.report.count as usize; - if total == 0 { - info!("subordinate no edges to send, closing link {}", link.link()); - link.close(); - SubordinateState::Done - } else if (self.sent_count as usize) >= total { - info!("subordinate sent all edges, closing link {}", link.link()); - link.close(); - SubordinateState::Done - } else if let Some((idx, edge)) = self.next_unsent_edge(total) { - let msg = encode_ugn_edge(edge); - if link.send(&msg) { + if self.can_send(sockets) { + if let Some((idx, edge)) = self.next_unsent_edge() { + let edge_bytes = encode_ugn_edge(edge); + if self.send(sockets, &edge_bytes) { debug!( - "subordinate sent edge idx {} src {} dst {} ugn {}", - idx, edge.src_node, edge.dst_node, edge.ugn + "subordinate sent edge idx {} src {} dst {} ugn {} on link {}", + idx, edge.src_node, edge.dst_node, edge.ugn, self.link ); self.sent_edges[idx] = true; self.sent_count = self.sent_count.saturating_add(1); - if (self.sent_count as usize) >= total { - info!("subordinate finished edges, closing link {}", link.link()); - link.close(); - SubordinateState::Done - } else { - self.state - } - } else { - self.state } + } + if self.sending_done() { + info!("Closing link {}", self.link); + self.close(sockets); + SubordinateState::Done } else { - debug!( - "subordinate has no unsent edges (count {}, sent {})", - total, self.sent_count - ); self.state } + } else { + self.state } } SubordinateState::Done => self.state, @@ -651,6 +467,16 @@ impl Subordinate { self.state = next_state; } + pub fn poll( + &mut self, + timestamp: smoltcp::time::Instant, + device: &mut impl smoltcp::phy::Device, + sockets: &mut SocketSet, + ) { + self.iface.poll(timestamp, device, sockets); + self.step(sockets); + } + pub fn is_done(&self) -> bool { self.state == SubordinateState::Done } @@ -663,28 +489,60 @@ impl Subordinate { self.state } - fn reset_handshake(&mut self) { - self.who_are_you_received = false; - self.hello_sent = false; - self.ugn_dump_received = false; + fn listen(&mut self, sockets: &mut SocketSet) -> bool { + let socket = sockets.get_mut::(self.socket_handle); + if !socket.is_open() { + match socket.listen(TCP_SERVER_PORT) { + Ok(()) => { + debug!("link {} listen on port {}", self.link, TCP_SERVER_PORT); + } + Err(err) => { + debug!("link {} listen error: {:?}", self.link, err); + } + } + } + trace!( + "link {} listen state open {} active {} can_send {} can_recv {} may_recv {}", + self.link, + socket.is_open(), + socket.is_active(), + socket.can_send(), + socket.can_recv(), + socket.may_recv() + ); + socket.is_open() } - fn bump_retry_or_fail(&mut self, retry_state: SubordinateState) -> SubordinateState { - self.retries = self.retries.saturating_add(1); - if self.retries >= 3 { - warn!("subordinate retry limit hit, entering Failed"); - SubordinateState::Failed + fn send(&mut self, sockets: &mut SocketSet, data: &[u8]) -> bool { + let socket = sockets.get_mut::(self.socket_handle); + if socket.can_send() { + match socket.send_slice(data) { + Ok(len) => { + trace!("link {} sent {} bytes", self.link, len); + return true; + } + Err(err) => { + debug!("link {} send error: {:?}", self.link, err); + } + } } else { - info!( - "subordinate timeout, retry {} -> {:?}", - self.retries, retry_state + trace!( + "link {} send blocked open {} active {}", + self.link, + socket.is_open(), + socket.is_active() ); - retry_state } + false + } + + fn close(&mut self, sockets: &mut SocketSet) { + let socket = sockets.get_mut::(self.socket_handle); + socket.close(); } - fn next_unsent_edge(&self, total: usize) -> Option<(usize, UgnEdge)> { - let limit = core::cmp::min(total, self.report.edges.len()); + fn next_unsent_edge(&self) -> Option<(usize, UgnEdge)> { + let limit = core::cmp::min(self.report.count as usize, self.report.edges.len()); for idx in 0..limit { if !self.sent_edges[idx] { if let Some(edge) = self.report.edges[idx] { @@ -694,55 +552,15 @@ impl Subordinate { } None } -} - -impl Default for Subordinate { - fn default() -> Self { - Self::new() - } -} - -fn encode_who_are_you(nonce: u32) -> [u8; WHO_ARE_YOU_BYTES] { - let mut msg = [0u8; WHO_ARE_YOU_BYTES]; - msg[0] = MSG_WHO_ARE_YOU; - msg[1] = ROLE_MANAGER; - msg[2] = 0; - msg[3] = 0; - msg[4..8].copy_from_slice(&nonce.to_le_bytes()); - msg -} - -fn encode_hello(nonce: u32) -> [u8; HELLO_BYTES] { - let mut msg = [0u8; HELLO_BYTES]; - msg[0] = MSG_HELLO; - msg[1] = ROLE_SUBORDINATE; - msg[2] = 0; - msg[3] = 0; - msg[4..8].copy_from_slice(&nonce.to_le_bytes()); - msg -} -fn parse_who_are_you(msg: &[u8]) -> Option { - if msg.len() < WHO_ARE_YOU_BYTES { - return None; + fn sending_done(&self) -> bool { + self.sent_count as usize >= self.report.count as usize } - if msg[0] != MSG_WHO_ARE_YOU || msg[1] != ROLE_MANAGER { - return None; - } - Some(u32::from_le_bytes([msg[4], msg[5], msg[6], msg[7]])) -} -fn parse_hello(msg: &[u8], nonce: u32) -> bool { - if msg.len() < HELLO_BYTES { - return false; + fn can_send(&self, sockets: &SocketSet) -> bool { + let socket = sockets.get::(self.socket_handle); + socket.can_send() } - msg[0] == MSG_HELLO - && msg[1] == ROLE_SUBORDINATE - && u32::from_le_bytes([msg[4], msg[5], msg[6], msg[7]]) == nonce -} - -fn parse_ugn_dump(msg: &[u8]) -> bool { - msg.len() >= UGN_DUMP_BYTES && msg[0] == MSG_UGN_DUMP } fn encode_ugn_edge(edge: UgnEdge) -> [u8; UGN_EDGE_BYTES] { @@ -757,27 +575,10 @@ fn parse_ugn_edge(msg: &[u8]) -> Option { Some(UgnEdge::from(*wire)) } -fn insert_edge(report: &mut UgnReport, edge: UgnEdge) -> bool { - if report - .edges - .iter() - .flatten() - .any(|existing| existing.src_node == edge.src_node && existing.dst_node == edge.dst_node) - { - return false; - } - if let Some(slot) = report.edges.iter_mut().find(|slot| slot.is_none()) { - *slot = Some(edge); - true - } else { - false - } -} - -pub fn ip_for_link(role: NodeRole, port: usize) -> [u8; 4] { - let host = match role { - NodeRole::Manager => 1, - NodeRole::Subordinate => 2, - }; - [10, 0, port as u8, host] -} +// pub fn ip_for_link(role: NodeRole, port: usize) -> [u8; 4] { +// let host = match role { +// NodeRole::Manager => 1, +// NodeRole::Subordinate => 2, +// }; +// [10, 0, port as u8, host] +// } diff --git a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs index 83dc2e9e8..9fece78e6 100644 --- a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs +++ b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs @@ -91,12 +91,12 @@ impl RingbufferDevice { let mtu = rx_bytes.min(tx_bytes) - PACKET_HEADER_SIZE; if rx_buffer.is_aligned() { - assert!( - rx_buffer.verify_aligned_to(&tx_buffer), - "RX buffer is aligned but not to the provided TX buffer, expected reference {:p}, got 0x{:X}", - &tx_buffer.0, - rx_buffer.get_alignment_reference(), - ); + // assert!( + // rx_buffer.verify_aligned_to(&tx_buffer), + // "RX buffer is aligned but not to the provided TX buffer, expected reference {:p}, got 0x{:X}", + // &tx_buffer.0, + // rx_buffer.get_alignment_reference(), + // ); } Self { From c2f92f7038a3bb7d95c5d3508b7d0d02a0664960 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Wed, 11 Mar 2026 07:43:43 +0100 Subject: [PATCH 39/70] Clear txs before alignment --- .../demos/soft-ugn-demo-mu-2/src/main.rs | 3 + .../manual_additions/soft_ugn_demo_mu/mod.rs | 2 +- .../soft_ugn_demo_mu/ringbuffers.rs | 259 ++++++++++++++++++ .../soft_ugn_demo_mu/scatter_gather.rs | 42 --- 4 files changed, 263 insertions(+), 43 deletions(-) create mode 100644 firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/ringbuffers.rs delete mode 100644 firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/scatter_gather.rs diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs index ad622383c..b82f62ff1 100644 --- a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs @@ -170,6 +170,9 @@ fn main() -> ! { INSTANCES.transmit_ringbuffer_5, INSTANCES.transmit_ringbuffer_6, ]; + for tx in transmit_ringbuffers.iter() { + tx.clear(); + } let mut receive_iter = receive_ringbuffers.into_iter(); let mut transmit_iter = transmit_ringbuffers.into_iter(); let mut devices: [RingbufferDevice; LINK_COUNT] = core::array::from_fn(|_| { diff --git a/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/mod.rs b/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/mod.rs index a09b14ea3..2590006d7 100644 --- a/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/mod.rs +++ b/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/mod.rs @@ -1,4 +1,4 @@ // SPDX-FileCopyrightText: 2025 Google LLC // // SPDX-License-Identifier: Apache-2.0 -pub mod scatter_gather; +pub mod ringbuffers; diff --git a/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/ringbuffers.rs b/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/ringbuffers.rs new file mode 100644 index 000000000..3a042245e --- /dev/null +++ b/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/ringbuffers.rs @@ -0,0 +1,259 @@ +// SPDX-FileCopyrightText: 2025 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 +use crate::hals::soft_ugn_demo_mu::devices::{ReceiveRingbuffer, TransmitRingbuffer}; + +/// Alignment protocol marker values +const ALIGNMENT_EMPTY: u64 = 0; +const ALIGNMENT_ANNOUNCE: u64 = 0xBADC0FFEE; +const ALIGNMENT_ACKNOWLEDGE: u64 = 0xDEADABBA; + +impl TransmitRingbuffer { + /// Write a slice to the transmit buffer. + /// + /// # Panics + /// + /// The source memory size must be smaller or equal to the memory size of + /// the `TransmitRingbuffer` memory. + pub fn write_slice(&self, src: &[[u8; 8]], offset: usize) { + assert!(src.len() + offset <= Self::DATA_LEN); + unsafe { + self.write_slice_unchecked(src, offset); + } + } + + /// Write a slice to the transmit buffer without checking bounds. The caller must + /// ensure that `src.len() + offset` does not exceed the buffer size. + /// + /// # Safety + /// + /// This function is unsafe because it can cause out-of-bounds memory access if the caller + /// does not ensure that `src.len() + offset` is within the bounds of the transmit buffer. + pub unsafe fn write_slice_unchecked(&self, src: &[[u8; 8]], offset: usize) { + let mut off = offset; + for &val in src { + unsafe { + self.set_data_unchecked(off, val); + } + off += 1; + } + } + + /// Write a slice to the transmit buffer with automatic wrapping. If we write more bytes + /// than the buffer size, we wrap back to the beginning of the buffer. + /// + /// # Panics + /// + /// This function will panic if `src.len()` is greater than the buffer size. + pub fn write_slice_with_wrap(&self, src: &[[u8; 8]], offset: usize) { + assert!(src.len() <= Self::DATA_LEN); + unsafe { + self.write_slice_with_wrap_unchecked(src, offset); + } + } + + /// Write a slice to the transmit buffer with automatic wrapping. If we write more bytes + /// than the buffer size, we wrap back to the beginning of the buffer. + /// + /// # Safety + /// This function is unsafe because it can cause out-of-bounds memory access if the caller + /// does not ensure that `src.len()` is smaller or equal to the buffer size. + pub unsafe fn write_slice_with_wrap_unchecked(&self, src: &[[u8; 8]], offset: usize) { + if src.len() + offset <= Self::DATA_LEN { + // No wrapping needed + self.write_slice(src, offset); + } else { + // Wrapping needed - split into two writes + let first_part_len = Self::DATA_LEN - offset; + let (first, second) = src.split_at(first_part_len); + self.write_slice(first, offset); + self.write_slice(second, 0); + } + } + + /// Clear the entire transmit buffer by writing zeros to all entries. + pub fn clear(&self) { + for i in 0..Self::DATA_LEN { + unsafe { + self.set_data_unchecked(i, [0u8; 8]); + } + } + } +} + +impl ReceiveRingbuffer { + /// Read a slice from the receive buffer. + /// + /// # Panics + /// + /// The destination memory size must be smaller or equal to the memory size + /// of the `ReceiveRingbuffer`. + pub fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { + assert!(dst.len() + offset <= Self::DATA_LEN); + unsafe { + self.read_slice_unchecked(dst, offset); + } + } + + /// Reads a slice from the receive buffer without checking bounds. The caller must + /// ensure that `dst.len() + offset` does not exceed the buffer size. + /// + /// # Safety + /// + /// This function is unsafe because it can cause out-of-bounds memory access if the caller + /// does not ensure that `dst.len() + offset` is within the bounds of the receive buffer. + pub unsafe fn read_slice_unchecked(&self, dst: &mut [[u8; 8]], offset: usize) { + let mut off = offset; + for val in dst { + unsafe { + *val = self.data_unchecked(off); + } + off += 1; + } + } + + /// Reads a slice from the receive buffer with automatic wrapping. If we read more bytes + /// than the buffer size, we wrap back to the beginning of the buffer. + /// + /// # Panics + /// + /// This function will panic if `dst.len()` is greater than the buffer size. + pub fn read_slice_with_wrap(&self, dst: &mut [[u8; 8]], offset: usize) { + assert!(dst.len() <= Self::DATA_LEN); + unsafe { + self.read_slice_with_wrap_unchecked(dst, offset); + } + } + /// Reads a slice from the receive buffer with automatic wrapping. If we read more bytes + /// than the buffer size, we wrap back to the beginning of the buffer. + /// + /// # Safety + /// + /// This function is unsafe because it can cause out-of-bounds memory access if the caller + /// does not ensure that `dst.len()` is smaller or equal to the buffer size. + pub unsafe fn read_slice_with_wrap_unchecked(&self, dst: &mut [[u8; 8]], offset: usize) { + if dst.len() + offset <= Self::DATA_LEN { + // No wrapping needed + self.read_slice(dst, offset); + } else { + // Wrapping needed - split into two reads + let first_part_len = Self::DATA_LEN - offset; + let (first, second) = dst.split_at_mut(first_part_len); + self.read_slice(first, offset); + self.read_slice(second, 0); + } + } +} + +pub struct AlignedReceiveBuffer { + pub rx: ReceiveRingbuffer, + rx_alignment_offset: Option, + tx_reference: usize, // Allows us to verify the tx_buffer we are aligned to. +} +impl AlignedReceiveBuffer { + /// Perform the alignment discovery protocol and return a new `AlignedReceiveBuffer` + /// with the discovered RX alignment offset. + /// + /// # Panics + /// + /// This function will panic if the transmit and receive buffers do not have the same size, + /// since that is a requirement for the alignment protocol to work. + pub fn new(rx: ReceiveRingbuffer) -> Self { + Self { + rx, + rx_alignment_offset: None, + tx_reference: 0, + } + } + + /// Returns true if the alignment offset has been discovered and the buffer is aligned. + pub fn is_aligned(&self) -> bool { + self.rx_alignment_offset.is_some() + } + + /// Returns the discovered alignment offset, or None if the offset has not been discovered yet. + pub fn get_alignment_offset(&self) -> Option { + self.rx_alignment_offset + } + + /// Performs the alignment discovery protocol. After this function completes, the `rx_alignment_offset` + /// field will be set with the discovered offset, and the RX buffer will be aligned to the neighbor's TX buffer. + pub fn align(&mut self, tx: &TransmitRingbuffer) { + // 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..TransmitRingbuffer::DATA_LEN { + 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..ReceiveRingbuffer::DATA_LEN { + let mut data_buf = [[0u8; 8]; 1]; + // Read directly from physical index by using scatter's read_slice + self.rx.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 + self.rx.read_slice(&mut data_buf, rx_offset); + let value = u64::from_le_bytes(data_buf[0]); + + if value == ALIGNMENT_ACKNOWLEDGE { + break; + } + } + self.rx_alignment_offset = Some(rx_offset); + self.tx_reference = tx.0 as *const _ as usize; + } + + /// Unsets the discovered alignment offset. + pub fn clear_alignment(&mut self) { + self.rx_alignment_offset = None; + self.tx_reference = 0; + } + + /// Returns true if the RX buffer is aligned to the provided TX buffer. + pub fn verify_aligned_to(&self, tx: &TransmitRingbuffer) -> bool { + self.is_aligned() && self.tx_reference == (tx.0 as *const _ as usize) + } + + /// Returns the reference address of the TX buffer that this RX buffer is aligned to. + pub fn get_alignment_reference(&self) -> usize { + self.tx_reference + } + /// Read a slice from the receive buffer using the discovered alignment offset. + /// After aligning the buffer pair, the user can use this function to read from the receive + /// buffer without needing to worry about the physical alignment offset. + /// + /// # Panics + /// + /// This function will panic if `dst.len()` is greater than the buffer size. + /// This function will also panic if the caller tries to read beyond the end of the buffer. + /// This function will also panic if `align()` has not been called yet to discover the alignment offset. + pub fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { + assert!(dst.len() + offset <= ReceiveRingbuffer::DATA_LEN); + let rx_offset = self + .rx_alignment_offset + .expect("Alignment offset not discovered yet. Call align() first."); + let mut aligned_offset = offset + rx_offset; + if aligned_offset >= ReceiveRingbuffer::DATA_LEN { + aligned_offset -= ReceiveRingbuffer::DATA_LEN; + } + self.rx.read_slice_with_wrap(dst, aligned_offset) + } +} diff --git a/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/scatter_gather.rs b/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/scatter_gather.rs deleted file mode 100644 index 46b60d165..000000000 --- a/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/scatter_gather.rs +++ /dev/null @@ -1,42 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Google LLC -// -// SPDX-License-Identifier: Apache-2.0 -use crate::soft_ugn_demo_mu::devices::{ReceiveRingbuffer, TransmitRingbuffer}; - -impl TransmitRingbuffer { - /// Write a slice to the transmit ringbuffer memory. - /// - /// # Panics - /// - /// The source memory size must be smaller or equal to the memory size of - /// the `TransmitRingbuffer` memory. - pub fn write_slice(&self, src: &[[u8; 8]], offset: usize) { - assert!(src.len() + offset <= Self::DATA_LEN); - let mut off = offset; - for &val in src { - unsafe { - self.set_data_unchecked(off, val); - } - off += 1; - } - } -} - -impl ReceiveRingbuffer { - /// Read a slice from the receive ringbuffer memory. - /// - /// # Panics - /// - /// The destination memory size must be smaller or equal to the memory size - /// of the `ReceiveRingbuffer`. - pub fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { - assert!(dst.len() + offset <= Self::DATA_LEN); - let mut off = offset; - for val in dst { - unsafe { - *val = self.data_unchecked(off); - } - off += 1; - } - } -} From 4660bffed111d7de0c9f5a22eb61f12532a3749c Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Wed, 11 Mar 2026 07:49:04 +0100 Subject: [PATCH 40/70] Add polling traces --- firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs index b82f62ff1..fbc0a75a2 100644 --- a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs @@ -215,6 +215,7 @@ fn main() -> ! { loop { let now = to_smoltcp_instant(INSTANCES.timer.now()); let mut sockets = socket_set(&mut sockets_storage[..]); + trace!("Polling manager link {}", link); manager.poll(now, &mut devices[link], &mut sockets); trace!("manager link {} state {:?}", link, manager.state()); if manager.is_done() { @@ -285,6 +286,7 @@ fn main() -> ! { let now = to_smoltcp_instant(INSTANCES.timer.now()); for link in 0..LINK_COUNT { let mut sockets = socket_set(&mut sockets_storage[link][..]); + trace!("Polling subordinate link {}", link); subordinates[link].poll(now, &mut devices[link], &mut sockets); { let socket = sockets.get::(socket_handles[link]); From 43c059222250ecb7942eb50bc01eac39e5438883 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Wed, 11 Mar 2026 07:57:07 +0100 Subject: [PATCH 41/70] More ringbuffer logging --- .../soft_ugn_demo_mu/ringbuffers.rs | 81 ++++++++++++++++++- 1 file changed, 80 insertions(+), 1 deletion(-) diff --git a/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/ringbuffers.rs b/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/ringbuffers.rs index 3a042245e..84d7c1964 100644 --- a/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/ringbuffers.rs +++ b/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/ringbuffers.rs @@ -2,6 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 use crate::hals::soft_ugn_demo_mu::devices::{ReceiveRingbuffer, TransmitRingbuffer}; +use log::{debug, trace, warn}; /// Alignment protocol marker values const ALIGNMENT_EMPTY: u64 = 0; @@ -16,6 +17,11 @@ impl TransmitRingbuffer { /// The source memory size must be smaller or equal to the memory size of /// the `TransmitRingbuffer` memory. pub fn write_slice(&self, src: &[[u8; 8]], offset: usize) { + trace!( + "TransmitRingbuffer::write_slice: len={}, offset={}", + src.len(), + offset + ); assert!(src.len() + offset <= Self::DATA_LEN); unsafe { self.write_slice_unchecked(src, offset); @@ -30,6 +36,7 @@ impl TransmitRingbuffer { /// This function is unsafe because it can cause out-of-bounds memory access if the caller /// does not ensure that `src.len() + offset` is within the bounds of the transmit buffer. pub unsafe fn write_slice_unchecked(&self, src: &[[u8; 8]], offset: usize) { + trace!("TransmitRingbuffer::write_slice_unchecked: about to write {} bytes starting at offset {}", src.len(), offset); let mut off = offset; for &val in src { unsafe { @@ -61,11 +68,17 @@ impl TransmitRingbuffer { pub unsafe fn write_slice_with_wrap_unchecked(&self, src: &[[u8; 8]], offset: usize) { if src.len() + offset <= Self::DATA_LEN { // No wrapping needed + trace!( + "TransmitRingbuffer::write_slice_with_wrap: no wrap needed, len={}, offset={}", + src.len(), + offset + ); self.write_slice(src, offset); } else { // Wrapping needed - split into two writes let first_part_len = Self::DATA_LEN - offset; let (first, second) = src.split_at(first_part_len); + debug!("TransmitRingbuffer::write_slice_with_wrap: wrapping at offset={}, first_len={}, second_len={}", offset, first.len(), second.len()); self.write_slice(first, offset); self.write_slice(second, 0); } @@ -73,11 +86,16 @@ impl TransmitRingbuffer { /// Clear the entire transmit buffer by writing zeros to all entries. pub fn clear(&self) { + debug!( + "TransmitRingbuffer::clear: about to clear {} entries", + Self::DATA_LEN + ); for i in 0..Self::DATA_LEN { unsafe { self.set_data_unchecked(i, [0u8; 8]); } } + debug!("TransmitRingbuffer::clear: completed"); } } @@ -89,6 +107,11 @@ impl ReceiveRingbuffer { /// The destination memory size must be smaller or equal to the memory size /// of the `ReceiveRingbuffer`. pub fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { + trace!( + "ReceiveRingbuffer::read_slice: len={}, offset={}", + dst.len(), + offset + ); assert!(dst.len() + offset <= Self::DATA_LEN); unsafe { self.read_slice_unchecked(dst, offset); @@ -103,6 +126,11 @@ impl ReceiveRingbuffer { /// This function is unsafe because it can cause out-of-bounds memory access if the caller /// does not ensure that `dst.len() + offset` is within the bounds of the receive buffer. pub unsafe fn read_slice_unchecked(&self, dst: &mut [[u8; 8]], offset: usize) { + trace!( + "ReceiveRingbuffer::read_slice_unchecked: about to read {} bytes starting at offset {}", + dst.len(), + offset + ); let mut off = offset; for val in dst { unsafe { @@ -134,11 +162,17 @@ impl ReceiveRingbuffer { pub unsafe fn read_slice_with_wrap_unchecked(&self, dst: &mut [[u8; 8]], offset: usize) { if dst.len() + offset <= Self::DATA_LEN { // No wrapping needed + trace!( + "ReceiveRingbuffer::read_slice_with_wrap: no wrap needed, len={}, offset={}", + dst.len(), + offset + ); self.read_slice(dst, offset); } else { // Wrapping needed - split into two reads let first_part_len = Self::DATA_LEN - offset; let (first, second) = dst.split_at_mut(first_part_len); + debug!("ReceiveRingbuffer::read_slice_with_wrap: wrapping at offset={}, first_len={}, second_len={}", offset, first.len(), second.len()); self.read_slice(first, offset); self.read_slice(second, 0); } @@ -159,6 +193,7 @@ impl AlignedReceiveBuffer { /// This function will panic if the transmit and receive buffers do not have the same size, /// since that is a requirement for the alignment protocol to work. pub fn new(rx: ReceiveRingbuffer) -> Self { + debug!("AlignedReceiveBuffer::new: creating new buffer"); Self { rx, rx_alignment_offset: None, @@ -179,57 +214,91 @@ impl AlignedReceiveBuffer { /// Performs the alignment discovery protocol. After this function completes, the `rx_alignment_offset` /// field will be set with the discovered offset, and the RX buffer will be aligned to the neighbor's TX buffer. pub fn align(&mut self, tx: &TransmitRingbuffer) { + debug!("AlignedReceiveBuffer::align: starting alignment protocol"); // Initialize TX buffer: write ANNOUNCE at index 0, clear the rest + debug!("AlignedReceiveBuffer::align: about to write ANNOUNCE pattern to TX"); let announce_pattern = [ALIGNMENT_ANNOUNCE.to_le_bytes()]; tx.write_slice(&announce_pattern, 0); + debug!("AlignedReceiveBuffer::align: sent ANNOUNCE pattern"); + debug!("AlignedReceiveBuffer::align: about to clear rest of TX buffer"); let empty_pattern: [[u8; 8]; 1] = [ALIGNMENT_EMPTY.to_le_bytes()]; for i in 1..TransmitRingbuffer::DATA_LEN { tx.write_slice(&empty_pattern, i); } + debug!("AlignedReceiveBuffer::align: cleared rest of TX buffer"); + + debug!("AlignedReceiveBuffer::align: cleared rest of TX buffer"); // Phase 1: Scan RX buffer to find ANNOUNCE or ACKNOWLEDGE // Read directly from scatter memory using read_slice with offset 0 + debug!("AlignedReceiveBuffer::align: phase 1 - scanning RX buffer for alignment marker"); let rx_offset = 'outer: loop { for rx_idx in 0..ReceiveRingbuffer::DATA_LEN { let mut data_buf = [[0u8; 8]; 1]; // Read directly from physical index by using scatter's read_slice + trace!( + "AlignedReceiveBuffer::align: about to read RX at index {}", + rx_idx + ); self.rx.read_slice(&mut data_buf, rx_idx); let value = u64::from_le_bytes(data_buf[0]); if value == ALIGNMENT_ANNOUNCE || value == ALIGNMENT_ACKNOWLEDGE { + debug!("AlignedReceiveBuffer::align: found alignment marker at rx_idx={}, value=0x{:x}", rx_idx, value); break 'outer rx_idx; } } }; + debug!( + "AlignedReceiveBuffer::align: discovered rx_offset={}", + rx_offset + ); // Phase 2: Send ACKNOWLEDGE and wait for confirmation + debug!("AlignedReceiveBuffer::align: phase 2 - about to send ACKNOWLEDGE"); let ack_pattern = [ALIGNMENT_ACKNOWLEDGE.to_le_bytes()]; tx.write_slice(&ack_pattern, 0); + debug!("AlignedReceiveBuffer::align: sent ACKNOWLEDGE, waiting for confirmation"); loop { let mut data_buf = [[0u8; 8]; 1]; // Read directly from physical index + trace!( + "AlignedReceiveBuffer::align: polling RX at offset {} for ACKNOWLEDGE confirmation", + rx_offset + ); self.rx.read_slice(&mut data_buf, rx_offset); let value = u64::from_le_bytes(data_buf[0]); if value == ALIGNMENT_ACKNOWLEDGE { + debug!("AlignedReceiveBuffer::align: received ACKNOWLEDGE confirmation"); break; } } self.rx_alignment_offset = Some(rx_offset); self.tx_reference = tx.0 as *const _ as usize; + debug!( + "AlignedReceiveBuffer::align: alignment complete, offset={}, tx_ref=0x{:x}", + rx_offset, self.tx_reference + ); } /// Unsets the discovered alignment offset. pub fn clear_alignment(&mut self) { + debug!("AlignedReceiveBuffer::clear_alignment: clearing alignment"); self.rx_alignment_offset = None; self.tx_reference = 0; } /// Returns true if the RX buffer is aligned to the provided TX buffer. pub fn verify_aligned_to(&self, tx: &TransmitRingbuffer) -> bool { - self.is_aligned() && self.tx_reference == (tx.0 as *const _ as usize) + let tx_addr = tx.0 as *const _ as usize; + let result = self.is_aligned() && self.tx_reference == tx_addr; + if !result { + warn!("AlignedReceiveBuffer::verify_aligned_to: verification failed, aligned={}, tx_ref=0x{:x}, tx_addr=0x{:x}", self.is_aligned(), self.tx_reference, tx_addr); + } + result } /// Returns the reference address of the TX buffer that this RX buffer is aligned to. @@ -246,6 +315,11 @@ impl AlignedReceiveBuffer { /// This function will also panic if the caller tries to read beyond the end of the buffer. /// This function will also panic if `align()` has not been called yet to discover the alignment offset. pub fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { + trace!( + "AlignedReceiveBuffer::read_slice: len={}, offset={}", + dst.len(), + offset + ); assert!(dst.len() + offset <= ReceiveRingbuffer::DATA_LEN); let rx_offset = self .rx_alignment_offset @@ -254,6 +328,11 @@ impl AlignedReceiveBuffer { if aligned_offset >= ReceiveRingbuffer::DATA_LEN { aligned_offset -= ReceiveRingbuffer::DATA_LEN; } + trace!( + "AlignedReceiveBuffer::read_slice: using aligned_offset={}", + aligned_offset + ); + trace!("AlignedReceiveBuffer::read_slice: about to call read_slice_with_wrap"); self.rx.read_slice_with_wrap(dst, aligned_offset) } } From 905d27ddf63651519df492ddfecde174fce10b67 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Wed, 11 Mar 2026 08:07:30 +0100 Subject: [PATCH 42/70] Increase duration --- .../src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs index 20ac51ede..1558e4884 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs @@ -292,7 +292,7 @@ driver2 testName targets = do $ T.tryWithTimeoutOn T.PrintActionTime "Waiting for CPU test status" - (180_000_000) + (360_000_000) goDumpCcSamples $ forConcurrently_ picocoms $ \pico -> From d5628f7141d3e8bdda5abbbfd49cbfab111ef148 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Wed, 11 Mar 2026 09:37:37 +0100 Subject: [PATCH 43/70] Use Aligned and fix some things --- .../ringbuffer_test/ringbuffers.rs | 40 +++++----- .../soft_ugn_demo_mu/ringbuffers.rs | 37 ++++----- .../bittide-sys/src/smoltcp/ringbuffer.rs | 78 ++++++++++--------- 3 files changed, 78 insertions(+), 77 deletions(-) diff --git a/firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/ringbuffers.rs b/firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/ringbuffers.rs index b006b801d..dfdf2b08c 100644 --- a/firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/ringbuffers.rs +++ b/firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/ringbuffers.rs @@ -1,10 +1,12 @@ +use log::trace; + // SPDX-FileCopyrightText: 2025 Google LLC // // SPDX-License-Identifier: Apache-2.0 use crate::hals::ringbuffer_test::devices::{ReceiveRingbuffer, TransmitRingbuffer}; /// Alignment protocol marker values -const ALIGNMENT_EMPTY: u64 = 0; +// const ALIGNMENT_EMPTY: u64 = 0; const ALIGNMENT_ANNOUNCE: u64 = 0xBADC0FFEE; const ALIGNMENT_ACKNOWLEDGE: u64 = 0xDEADABBA; @@ -16,6 +18,7 @@ impl TransmitRingbuffer { /// The source memory size must be smaller or equal to the memory size of /// the `TransmitRingbuffer` memory. pub fn write_slice(&self, src: &[[u8; 8]], offset: usize) { + // trace!("write_slice called with offset {} and src {:02x?}", offset, src); assert!(src.len() + offset <= Self::DATA_LEN); unsafe { self.write_slice_unchecked(src, offset); @@ -30,13 +33,13 @@ impl TransmitRingbuffer { /// This function is unsafe because it can cause out-of-bounds memory access if the caller /// does not ensure that `src.len() + offset` is within the bounds of the transmit buffer. pub unsafe fn write_slice_unchecked(&self, src: &[[u8; 8]], offset: usize) { - let mut off = offset; - for &val in src { - unsafe { - self.set_data_unchecked(off, val); - } - off += 1; + let src_ptr = src.as_ptr(); + let dst_ptr = self.0.add(offset) as *mut [u8; 8]; + // trace!("Writing slice to transmit buffer at offset {}: {:02x?}", offset, src); + unsafe { + core::ptr::copy_nonoverlapping(src_ptr, dst_ptr, src.len()); } + // trace!("Done"); } /// Write a slice to the transmit buffer with automatic wrapping. If we write more bytes @@ -89,10 +92,16 @@ impl ReceiveRingbuffer { /// The destination memory size must be smaller or equal to the memory size /// of the `ReceiveRingbuffer`. pub fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { + trace!( + "read_slice called with offset {} and dst len {}", + offset, + dst.len() + ); assert!(dst.len() + offset <= Self::DATA_LEN); unsafe { self.read_slice_unchecked(dst, offset); } + trace!("Done"); } /// Reads a slice from the receive buffer without checking bounds. The caller must @@ -103,12 +112,10 @@ impl ReceiveRingbuffer { /// This function is unsafe because it can cause out-of-bounds memory access if the caller /// does not ensure that `dst.len() + offset` is within the bounds of the receive buffer. pub unsafe fn read_slice_unchecked(&self, dst: &mut [[u8; 8]], offset: usize) { - let mut off = offset; - for val in dst { - unsafe { - *val = self.data_unchecked(off); - } - off += 1; + let dst_ptr = dst.as_mut_ptr(); + let src_ptr = self.0 as *const [u8; 8]; + unsafe { + core::ptr::copy_nonoverlapping(src_ptr.add(offset), dst_ptr, dst.len()); } } @@ -180,14 +187,11 @@ impl AlignedReceiveBuffer { /// field will be set with the discovered offset, and the RX buffer will be aligned to the neighbor's TX buffer. pub fn align(&mut self, tx: &TransmitRingbuffer) { // Initialize TX buffer: write ANNOUNCE at index 0, clear the rest + tx.clear(); + 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..TransmitRingbuffer::DATA_LEN { - 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 { diff --git a/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/ringbuffers.rs b/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/ringbuffers.rs index 84d7c1964..293bfabc5 100644 --- a/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/ringbuffers.rs +++ b/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/ringbuffers.rs @@ -37,12 +37,9 @@ impl TransmitRingbuffer { /// does not ensure that `src.len() + offset` is within the bounds of the transmit buffer. pub unsafe fn write_slice_unchecked(&self, src: &[[u8; 8]], offset: usize) { trace!("TransmitRingbuffer::write_slice_unchecked: about to write {} bytes starting at offset {}", src.len(), offset); - let mut off = offset; - for &val in src { - unsafe { - self.set_data_unchecked(off, val); - } - off += 1; + let dst_ptr = self.0 as *mut [u8; 8]; + unsafe { + core::ptr::copy_nonoverlapping(src.as_ptr(), dst_ptr.add(offset), src.len()); } } @@ -131,12 +128,10 @@ impl ReceiveRingbuffer { dst.len(), offset ); - let mut off = offset; - for val in dst { - unsafe { - *val = self.data_unchecked(off); - } - off += 1; + let dst_ptr = dst.as_mut_ptr(); + let src_ptr = self.0 as *const [u8; 8]; + unsafe { + core::ptr::copy_nonoverlapping(src_ptr.add(offset), dst_ptr, dst.len()); } } @@ -215,20 +210,20 @@ impl AlignedReceiveBuffer { /// field will be set with the discovered offset, and the RX buffer will be aligned to the neighbor's TX buffer. pub fn align(&mut self, tx: &TransmitRingbuffer) { debug!("AlignedReceiveBuffer::align: starting alignment protocol"); - // Initialize TX buffer: write ANNOUNCE at index 0, clear the rest - debug!("AlignedReceiveBuffer::align: about to write ANNOUNCE pattern to TX"); - let announce_pattern = [ALIGNMENT_ANNOUNCE.to_le_bytes()]; - tx.write_slice(&announce_pattern, 0); - debug!("AlignedReceiveBuffer::align: sent ANNOUNCE pattern"); - debug!("AlignedReceiveBuffer::align: about to clear rest of TX buffer"); + // Clear TX buffer completely first to remove any stale patterns + debug!("AlignedReceiveBuffer::align: about to clear entire TX buffer"); let empty_pattern: [[u8; 8]; 1] = [ALIGNMENT_EMPTY.to_le_bytes()]; - for i in 1..TransmitRingbuffer::DATA_LEN { + for i in 0..TransmitRingbuffer::DATA_LEN { tx.write_slice(&empty_pattern, i); } - debug!("AlignedReceiveBuffer::align: cleared rest of TX buffer"); + debug!("AlignedReceiveBuffer::align: cleared entire TX buffer"); - debug!("AlignedReceiveBuffer::align: cleared rest of TX buffer"); + // Write ANNOUNCE at index 0 + debug!("AlignedReceiveBuffer::align: about to write ANNOUNCE pattern to TX"); + let announce_pattern = [ALIGNMENT_ANNOUNCE.to_le_bytes()]; + tx.write_slice(&announce_pattern, 0); + debug!("AlignedReceiveBuffer::align: sent ANNOUNCE pattern"); // Phase 1: Scan RX buffer to find ANNOUNCE or ACKNOWLEDGE // Read directly from scatter memory using read_slice with offset 0 diff --git a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs index 9fece78e6..d1c90791c 100644 --- a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs +++ b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs @@ -29,6 +29,7 @@ //! 5. Track sequence numbers to detect repeated packets use bittide_hal::hals::ringbuffer_test::devices::{ReceiveRingbuffer, TransmitRingbuffer}; +use bittide_hal::manual_additions::addressable_buffer::Aligned; use bittide_hal::manual_additions::ringbuffer_test::ringbuffers::AlignedReceiveBuffer; use crc::{Crc, CRC_32_ISCSI}; use log::{trace, warn}; @@ -90,14 +91,7 @@ impl RingbufferDevice { let tx_bytes = TransmitRingbuffer::DATA_LEN * 8; let mtu = rx_bytes.min(tx_bytes) - PACKET_HEADER_SIZE; - if rx_buffer.is_aligned() { - // assert!( - // rx_buffer.verify_aligned_to(&tx_buffer), - // "RX buffer is aligned but not to the provided TX buffer, expected reference {:p}, got 0x{:X}", - // &tx_buffer.0, - // rx_buffer.get_alignment_reference(), - // ); - } + assert!(rx_buffer.is_aligned(), "RX buffer is not aligned "); Self { rx_buffer, @@ -126,12 +120,15 @@ impl Device for RingbufferDevice { } fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { - // Read packet header: CRC32 (4 bytes) + sequence (2 bytes) + length (2 bytes) - let mut header_buf: [[u8; 8]; 1] = [[0u8; 8]; 1]; - self.rx_buffer.read_slice(&mut header_buf, 0); + // Allocate aligned buffer for reading from ringbuffer + let mut packet_buffer = Aligned::new([[0u8; 8]; ReceiveRingbuffer::DATA_LEN]); + + // Read first word containing the header: CRC32 (4 bytes) + sequence (2 bytes) + length (2 bytes) + self.rx_buffer + .read_slice(&mut packet_buffer.get_mut()[0..1], 0); // Extract CRC, sequence number, and length from header using direct pointer reads - let header_ptr = header_buf[0].as_ptr(); + let header_ptr = packet_buffer.get()[0].as_ptr(); let _stored_crc = unsafe { (header_ptr as *const u32).read_unaligned() }; let seq_num = unsafe { (header_ptr.add(4) as *const u16).read_unaligned() }; let packet_len = unsafe { (header_ptr.add(6) as *const u16).read_unaligned() } as usize; @@ -155,19 +152,19 @@ impl Device for RingbufferDevice { let total_len = PACKET_HEADER_SIZE + packet_len; let num_words = total_len.div_ceil(8); - // Allocate aligned buffer for reading from ringbuffer - // Use a flat byte buffer and cast it to [[u8; 8]] for the API - let mut packet_buffer = [0u8; ReceiveRingbuffer::DATA_LEN * 8]; + // Read remaining words if any (we already read the first word) + if num_words > 1 { + self.rx_buffer + .read_slice(&mut packet_buffer.get_mut()[1..num_words], 1); + } - let word_slice = unsafe { - core::slice::from_raw_parts_mut(packet_buffer.as_mut_ptr() as *mut [u8; 8], num_words) + // Flatten to bytes for CRC validation + let packet_bytes = unsafe { + core::slice::from_raw_parts(packet_buffer.get().as_ptr() as *const u8, total_len) }; - // Copy entire packet from ringbuffer (header + payload) - self.rx_buffer.read_slice(word_slice, 0); - - // Validate CRC (packet_buffer is already in the right format) - if !is_valid(&packet_buffer[..total_len]) { + // Validate CRC + if !is_valid(packet_bytes) { trace!("CRC validation failed for packet seq {}", seq_num); return None; } @@ -179,10 +176,15 @@ impl Device for RingbufferDevice { ); self.last_rx_seq = seq_num; - // Extract payload (skip header, exclude CRC) - let mut payload = [0u8; ReceiveRingbuffer::DATA_LEN * 8]; - payload[..packet_len] - .copy_from_slice(&packet_buffer[PACKET_HEADER_SIZE..PACKET_HEADER_SIZE + packet_len]); + // Extract payload (skip header) + let mut payload = Aligned::new([0u8; ReceiveRingbuffer::DATA_LEN * 8]); + let payload_bytes = unsafe { + core::slice::from_raw_parts( + (packet_buffer.get().as_ptr() as *const u8).add(PACKET_HEADER_SIZE), + packet_len, + ) + }; + payload.get_mut()[..packet_len].copy_from_slice(payload_bytes); let rx = RxToken { buffer: payload, @@ -210,7 +212,7 @@ impl Device for RingbufferDevice { /// Contains a local copy of the packet payload that has been validated /// against CRC32 corruption. pub struct RxToken { - buffer: [u8; ReceiveRingbuffer::DATA_LEN * 8], + buffer: Aligned<[u8; ReceiveRingbuffer::DATA_LEN * 8]>, length: usize, } @@ -220,7 +222,7 @@ impl phy::RxToken for RxToken { F: FnOnce(&[u8]) -> R, { trace!("Consuming validated packet ({} bytes)", self.length); - f(&self.buffer[..self.length]) + f(&self.buffer.get()[..self.length]) } } @@ -243,12 +245,12 @@ impl phy::TxToken for TxToken<'_> { self.mtu ); - // Prepare buffer: header + payload - let mut buffer = [0u8; TransmitRingbuffer::DATA_LEN * 8]; + // Prepare aligned buffer: header + payload + let mut buffer = Aligned::new([[0u8; 8]; TransmitRingbuffer::DATA_LEN]); // Write header fields using direct pointer writes // Header format: CRC32 (4 bytes) + sequence (2 bytes) + length (2 bytes) - let header_ptr = buffer.as_mut_ptr(); + let header_ptr = buffer.get_mut().as_mut_ptr() as *mut u8; unsafe { // Write sequence and length (CRC written later after payload) (header_ptr.add(4) as *mut u16).write_unaligned(self.seq_num.to_le()); @@ -256,23 +258,23 @@ impl phy::TxToken for TxToken<'_> { } // Let smoltcp fill the packet data - let result = f(&mut buffer[PACKET_HEADER_SIZE..PACKET_HEADER_SIZE + len]); + let payload_slice = + unsafe { core::slice::from_raw_parts_mut(header_ptr.add(PACKET_HEADER_SIZE), len) }; + let result = f(payload_slice); // Calculate total length and seal packet with CRC in header let total_len = PACKET_HEADER_SIZE + len; - let crc = CRC.checksum(&buffer[4..total_len]); + let crc_data = unsafe { core::slice::from_raw_parts(header_ptr.add(4), total_len - 4) }; + let crc = CRC.checksum(crc_data); unsafe { (header_ptr as *mut u32).write_unaligned(crc.to_le()); } - // Convert to word array for ringbuffer using pointer cast + // Calculate number of words needed let num_words = total_len.div_ceil(8); - // The buffer is correctly sized and alignment is maintained. - let word_slice = - unsafe { core::slice::from_raw_parts(buffer.as_ptr() as *const [u8; 8], num_words) }; // Write to ringbuffer starting at offset 0 - self.tx_buffer.write_slice(word_slice, 0); + self.tx_buffer.write_slice(&buffer.get()[..num_words], 0); trace!( "Transmitted packet: checksum {}, seq {}, total length {}", From ce03ccca7bf62b73732463ef85b87286deff028a Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Wed, 11 Mar 2026 12:41:38 +0100 Subject: [PATCH 44/70] Less tracing in ringbuffers --- .../soft_ugn_demo_mu/ringbuffers.rs | 50 +------------------ 1 file changed, 1 insertion(+), 49 deletions(-) diff --git a/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/ringbuffers.rs b/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/ringbuffers.rs index 293bfabc5..01dd1dcfb 100644 --- a/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/ringbuffers.rs +++ b/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/ringbuffers.rs @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 use crate::hals::soft_ugn_demo_mu::devices::{ReceiveRingbuffer, TransmitRingbuffer}; -use log::{debug, trace, warn}; +use log::{debug, warn}; /// Alignment protocol marker values const ALIGNMENT_EMPTY: u64 = 0; @@ -17,11 +17,6 @@ impl TransmitRingbuffer { /// The source memory size must be smaller or equal to the memory size of /// the `TransmitRingbuffer` memory. pub fn write_slice(&self, src: &[[u8; 8]], offset: usize) { - trace!( - "TransmitRingbuffer::write_slice: len={}, offset={}", - src.len(), - offset - ); assert!(src.len() + offset <= Self::DATA_LEN); unsafe { self.write_slice_unchecked(src, offset); @@ -36,7 +31,6 @@ impl TransmitRingbuffer { /// This function is unsafe because it can cause out-of-bounds memory access if the caller /// does not ensure that `src.len() + offset` is within the bounds of the transmit buffer. pub unsafe fn write_slice_unchecked(&self, src: &[[u8; 8]], offset: usize) { - trace!("TransmitRingbuffer::write_slice_unchecked: about to write {} bytes starting at offset {}", src.len(), offset); let dst_ptr = self.0 as *mut [u8; 8]; unsafe { core::ptr::copy_nonoverlapping(src.as_ptr(), dst_ptr.add(offset), src.len()); @@ -64,12 +58,6 @@ impl TransmitRingbuffer { /// does not ensure that `src.len()` is smaller or equal to the buffer size. pub unsafe fn write_slice_with_wrap_unchecked(&self, src: &[[u8; 8]], offset: usize) { if src.len() + offset <= Self::DATA_LEN { - // No wrapping needed - trace!( - "TransmitRingbuffer::write_slice_with_wrap: no wrap needed, len={}, offset={}", - src.len(), - offset - ); self.write_slice(src, offset); } else { // Wrapping needed - split into two writes @@ -104,11 +92,6 @@ impl ReceiveRingbuffer { /// The destination memory size must be smaller or equal to the memory size /// of the `ReceiveRingbuffer`. pub fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { - trace!( - "ReceiveRingbuffer::read_slice: len={}, offset={}", - dst.len(), - offset - ); assert!(dst.len() + offset <= Self::DATA_LEN); unsafe { self.read_slice_unchecked(dst, offset); @@ -123,11 +106,6 @@ impl ReceiveRingbuffer { /// This function is unsafe because it can cause out-of-bounds memory access if the caller /// does not ensure that `dst.len() + offset` is within the bounds of the receive buffer. pub unsafe fn read_slice_unchecked(&self, dst: &mut [[u8; 8]], offset: usize) { - trace!( - "ReceiveRingbuffer::read_slice_unchecked: about to read {} bytes starting at offset {}", - dst.len(), - offset - ); let dst_ptr = dst.as_mut_ptr(); let src_ptr = self.0 as *const [u8; 8]; unsafe { @@ -156,12 +134,6 @@ impl ReceiveRingbuffer { /// does not ensure that `dst.len()` is smaller or equal to the buffer size. pub unsafe fn read_slice_with_wrap_unchecked(&self, dst: &mut [[u8; 8]], offset: usize) { if dst.len() + offset <= Self::DATA_LEN { - // No wrapping needed - trace!( - "ReceiveRingbuffer::read_slice_with_wrap: no wrap needed, len={}, offset={}", - dst.len(), - offset - ); self.read_slice(dst, offset); } else { // Wrapping needed - split into two reads @@ -231,11 +203,6 @@ impl AlignedReceiveBuffer { let rx_offset = 'outer: loop { for rx_idx in 0..ReceiveRingbuffer::DATA_LEN { let mut data_buf = [[0u8; 8]; 1]; - // Read directly from physical index by using scatter's read_slice - trace!( - "AlignedReceiveBuffer::align: about to read RX at index {}", - rx_idx - ); self.rx.read_slice(&mut data_buf, rx_idx); let value = u64::from_le_bytes(data_buf[0]); @@ -258,11 +225,6 @@ impl AlignedReceiveBuffer { loop { let mut data_buf = [[0u8; 8]; 1]; - // Read directly from physical index - trace!( - "AlignedReceiveBuffer::align: polling RX at offset {} for ACKNOWLEDGE confirmation", - rx_offset - ); self.rx.read_slice(&mut data_buf, rx_offset); let value = u64::from_le_bytes(data_buf[0]); @@ -310,11 +272,6 @@ impl AlignedReceiveBuffer { /// This function will also panic if the caller tries to read beyond the end of the buffer. /// This function will also panic if `align()` has not been called yet to discover the alignment offset. pub fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { - trace!( - "AlignedReceiveBuffer::read_slice: len={}, offset={}", - dst.len(), - offset - ); assert!(dst.len() + offset <= ReceiveRingbuffer::DATA_LEN); let rx_offset = self .rx_alignment_offset @@ -323,11 +280,6 @@ impl AlignedReceiveBuffer { if aligned_offset >= ReceiveRingbuffer::DATA_LEN { aligned_offset -= ReceiveRingbuffer::DATA_LEN; } - trace!( - "AlignedReceiveBuffer::read_slice: using aligned_offset={}", - aligned_offset - ); - trace!("AlignedReceiveBuffer::read_slice: about to call read_slice_with_wrap"); self.rx.read_slice_with_wrap(dst, aligned_offset) } } From f65fca68e2ed602b59c92ca3fc42d4a2f8d0f121 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Wed, 11 Mar 2026 14:45:19 +0100 Subject: [PATCH 45/70] Use macros --- .../bittide-hal/src/manual_additions/mod.rs | 1 + .../src/manual_additions/ringbuffer.rs | 249 ++++++++++++++++++ .../ringbuffer_test/ringbuffers.rs | 118 +-------- .../bittide-sys/src/smoltcp/ringbuffer.rs | 102 +++++-- 4 files changed, 333 insertions(+), 137 deletions(-) create mode 100644 firmware-support/bittide-hal/src/manual_additions/ringbuffer.rs diff --git a/firmware-support/bittide-hal/src/manual_additions/mod.rs b/firmware-support/bittide-hal/src/manual_additions/mod.rs index 2caae2262..57b3b6cdf 100644 --- a/firmware-support/bittide-hal/src/manual_additions/mod.rs +++ b/firmware-support/bittide-hal/src/manual_additions/mod.rs @@ -8,6 +8,7 @@ pub mod calendar; pub mod capture_ugn; pub mod dna; pub mod elastic_buffer; +pub mod ringbuffer; pub mod ringbuffer_test; pub mod scatter_gather_pe; pub mod si539x_spi; diff --git a/firmware-support/bittide-hal/src/manual_additions/ringbuffer.rs b/firmware-support/bittide-hal/src/manual_additions/ringbuffer.rs new file mode 100644 index 000000000..add870f89 --- /dev/null +++ b/firmware-support/bittide-hal/src/manual_additions/ringbuffer.rs @@ -0,0 +1,249 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 + +const ALIGNMENT_ANNOUNCE: u64 = 0xBADC0FFEE; +const ALIGNMENT_ACKNOWLEDGE: u64 = 0xDEADABBA; + +pub trait TransmitRingbufferInterface { + const DATA_LEN: usize; + + /// Get a pointer to the base address of the TransmitRingbuffer + fn base_ptr(&self) -> *mut [u8; 8]; + + /// Write a slice to the transmit buffer at the given offset. The slice must not exceed the buffer length when combined with the offset. + fn write_slice(&self, src: &[[u8; 8]], offset: usize) { + assert!(src.len() + offset <= Self::DATA_LEN); + unsafe { + self.write_slice_unchecked(src, offset); + } + } + + /// Write a slice to the transmit buffer at the given offset without checking bounds. The caller must ensure that `src.len() + offset` does not exceed the buffer length. + /// + /// # Safety + /// This function is unsafe because it can cause out-of-bounds memory access if the caller + /// does not ensure that `src.len() + offset` is within the bounds of the transmit buffer. + unsafe fn write_slice_unchecked(&self, src: &[[u8; 8]], offset: usize) { + let dst_ptr = self.base_ptr().add(offset); + core::ptr::copy_nonoverlapping(src.as_ptr(), dst_ptr, src.len()); + } + + /// Write a slice to the transmit buffer wrapping if `src.len() + offset`exceeds the length + /// of the buffer. + /// + /// # Panic + /// Panics if the length of the source exceeds the length of the buffer. + fn write_slice_with_wrap(&self, src: &[[u8; 8]], offset: usize) { + assert!(src.len() <= Self::DATA_LEN); + unsafe { + self.write_slice_with_wrap_unchecked(src, offset); + } + } + + /// Write a slice to the transmit buffer without bounds checking. It wraps if `src.len() + offset`exceeds the length + /// of the buffer. + /// + /// # Safety + /// Caller must ensure that the size of the source does not exceed the buffer length. + unsafe fn write_slice_with_wrap_unchecked(&self, src: &[[u8; 8]], offset: usize) { + if src.len() + offset <= Self::DATA_LEN { + self.write_slice(src, offset); + } else { + let first_part_len = Self::DATA_LEN - offset; + let (first, second) = src.split_at(first_part_len); + self.write_slice(first, offset); + self.write_slice(second, 0); + } + } + + fn clear(&self) { + let zero = [[0u8; 8]; 1]; + for i in 0..Self::DATA_LEN { + self.write_slice(&zero, i); + } + } +} + +pub trait ReceiveRingbufferInterface { + const DATA_LEN: usize; + + fn base_ptr(&self) -> *const [u8; 8]; + + fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { + assert!(dst.len() + offset <= Self::DATA_LEN); + unsafe { + self.read_slice_unchecked(dst, offset); + } + } + + /// Read a slice from the buffer into the destination without bounds checking. + /// + /// # Safety + /// Will fail if the requested size exceeds the size of the buffer. + unsafe fn read_slice_unchecked(&self, dst: &mut [[u8; 8]], offset: usize) { + let dst_ptr = dst.as_mut_ptr(); + let src_ptr = self.base_ptr().add(offset); + core::ptr::copy_nonoverlapping(src_ptr, dst_ptr, dst.len()); + } + + /// Read a slice from the buffer, wrapping around if the `dst.len() + offset` exceeds the + /// length of the buffer. + /// + /// # Panics + /// Will panic if the destination requests more bytes than the size of the buffer. + fn read_slice_with_wrap(&self, dst: &mut [[u8; 8]], offset: usize) { + assert!(dst.len() <= Self::DATA_LEN); + unsafe { + self.read_slice_with_wrap_unchecked(dst, offset); + } + } + + /// Read a slice from the buffer with wrapping, but no bounds checking + /// + /// # Safety + /// Will fail if the destination requests more bytes than the size of the buffer. + unsafe fn read_slice_with_wrap_unchecked(&self, dst: &mut [[u8; 8]], offset: usize) { + if dst.len() + offset <= Self::DATA_LEN { + self.read_slice(dst, offset); + } else { + let first_part_len = Self::DATA_LEN - offset; + let (first, second) = dst.split_at_mut(first_part_len); + self.read_slice(first, offset); + self.read_slice(second, 0); + } + } +} + +macro_rules! impl_ringbuffer_interfaces { + (rx: $rx:ty, tx: $tx:ty) => { + const _: () = { + if <$rx>::DATA_LEN != <$tx>::DATA_LEN { + const_panic::concat_panic!( + "Ringbuffer sizes do not match for ", + stringify!($rx), + " and ", + stringify!($tx) + ); + } + }; + + impl ReceiveRingbufferInterface for $rx { + const DATA_LEN: usize = <$rx>::DATA_LEN; + + fn base_ptr(&self) -> *const [u8; 8] { + self.0.cast::<[u8; 8]>() + } + } + + impl TransmitRingbufferInterface for $tx { + const DATA_LEN: usize = <$tx>::DATA_LEN; + + fn base_ptr(&self) -> *mut [u8; 8] { + self.0.cast::<[u8; 8]>() + } + } + }; +} + +impl_ringbuffer_interfaces! { + rx: crate::hals::ringbuffer_test::devices::ReceiveRingbuffer, + tx: crate::hals::ringbuffer_test::devices::TransmitRingbuffer +} + +impl_ringbuffer_interfaces! { + rx: crate::hals::soft_ugn_demo_mu::devices::ReceiveRingbuffer, + tx: crate::hals::soft_ugn_demo_mu::devices::TransmitRingbuffer +} + +pub struct AlignedReceiveBuffer +where + Rx: ReceiveRingbufferInterface, + Tx: TransmitRingbufferInterface, +{ + pub rx: Rx, + rx_alignment_offset: Option, + tx_reference: usize, + _tx: core::marker::PhantomData, +} + +impl AlignedReceiveBuffer +where + Rx: ReceiveRingbufferInterface, + Tx: TransmitRingbufferInterface, +{ + pub fn new(rx: Rx) -> Self { + Self { + rx, + rx_alignment_offset: None, + tx_reference: 0, + _tx: core::marker::PhantomData, + } + } + + pub fn is_aligned(&self) -> bool { + self.rx_alignment_offset.is_some() + } + + pub fn get_alignment_offset(&self) -> Option { + self.rx_alignment_offset + } + + pub fn align(&mut self, tx: &Tx) { + tx.clear(); + let announce_pattern = [ALIGNMENT_ANNOUNCE.to_le_bytes()]; + tx.write_slice(&announce_pattern, 0); + + let rx_offset = 'outer: loop { + for rx_idx in 0..Rx::DATA_LEN { + let mut data_buf = [[0u8; 8]; 1]; + self.rx.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; + } + } + }; + + let ack_pattern = [ALIGNMENT_ACKNOWLEDGE.to_le_bytes()]; + tx.write_slice(&ack_pattern, 0); + + loop { + let mut data_buf = [[0u8; 8]; 1]; + self.rx.read_slice(&mut data_buf, rx_offset); + let value = u64::from_le_bytes(data_buf[0]); + + if value == ALIGNMENT_ACKNOWLEDGE { + break; + } + } + self.rx_alignment_offset = Some(rx_offset); + self.tx_reference = tx.base_ptr() as *const _ as usize; + } + + pub fn clear_alignment(&mut self) { + self.rx_alignment_offset = None; + self.tx_reference = 0; + } + + pub fn verify_aligned_to(&self, tx: &Tx) -> bool { + self.is_aligned() && self.tx_reference == (tx.base_ptr() as *const _ as usize) + } + + pub fn get_alignment_reference(&self) -> usize { + self.tx_reference + } + + pub fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { + assert!(dst.len() + offset <= Rx::DATA_LEN); + let rx_offset = self + .rx_alignment_offset + .expect("Alignment offset not discovered yet. Call align() first."); + let mut aligned_offset = offset + rx_offset; + if aligned_offset >= Rx::DATA_LEN { + aligned_offset -= Rx::DATA_LEN; + } + self.rx.read_slice_with_wrap(dst, aligned_offset) + } +} diff --git a/firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/ringbuffers.rs b/firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/ringbuffers.rs index dfdf2b08c..9d714a90b 100644 --- a/firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/ringbuffers.rs +++ b/firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/ringbuffers.rs @@ -5,10 +5,10 @@ use log::trace; // SPDX-License-Identifier: Apache-2.0 use crate::hals::ringbuffer_test::devices::{ReceiveRingbuffer, TransmitRingbuffer}; -/// Alignment protocol marker values -// const ALIGNMENT_EMPTY: u64 = 0; -const ALIGNMENT_ANNOUNCE: u64 = 0xBADC0FFEE; -const ALIGNMENT_ACKNOWLEDGE: u64 = 0xDEADABBA; +pub type AlignedReceiveBuffer = crate::manual_additions::ringbuffer::AlignedReceiveBuffer< + ReceiveRingbuffer, + TransmitRingbuffer, +>; impl TransmitRingbuffer { /// Write a slice to the transmit buffer. @@ -151,113 +151,3 @@ impl ReceiveRingbuffer { } } } - -pub struct AlignedReceiveBuffer { - pub rx: ReceiveRingbuffer, - rx_alignment_offset: Option, - tx_reference: usize, // Allows us to verify the tx_buffer we are aligned to. -} -impl AlignedReceiveBuffer { - /// Perform the alignment discovery protocol and return a new `AlignedReceiveBuffer` - /// with the discovered RX alignment offset. - /// - /// # Panics - /// - /// This function will panic if the transmit and receive buffers do not have the same size, - /// since that is a requirement for the alignment protocol to work. - pub fn new(rx: ReceiveRingbuffer) -> Self { - Self { - rx, - rx_alignment_offset: None, - tx_reference: 0, - } - } - - /// Returns true if the alignment offset has been discovered and the buffer is aligned. - pub fn is_aligned(&self) -> bool { - self.rx_alignment_offset.is_some() - } - - /// Returns the discovered alignment offset, or None if the offset has not been discovered yet. - pub fn get_alignment_offset(&self) -> Option { - self.rx_alignment_offset - } - - /// Performs the alignment discovery protocol. After this function completes, the `rx_alignment_offset` - /// field will be set with the discovered offset, and the RX buffer will be aligned to the neighbor's TX buffer. - pub fn align(&mut self, tx: &TransmitRingbuffer) { - // Initialize TX buffer: write ANNOUNCE at index 0, clear the rest - tx.clear(); - - let announce_pattern = [ALIGNMENT_ANNOUNCE.to_le_bytes()]; - tx.write_slice(&announce_pattern, 0); - - // 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..ReceiveRingbuffer::DATA_LEN { - let mut data_buf = [[0u8; 8]; 1]; - // Read directly from physical index by using scatter's read_slice - self.rx.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 - self.rx.read_slice(&mut data_buf, rx_offset); - let value = u64::from_le_bytes(data_buf[0]); - - if value == ALIGNMENT_ACKNOWLEDGE { - break; - } - } - self.rx_alignment_offset = Some(rx_offset); - self.tx_reference = tx.0 as *const _ as usize; - } - - /// Unsets the discovered alignment offset. - pub fn clear_alignment(&mut self) { - self.rx_alignment_offset = None; - self.tx_reference = 0; - } - - /// Returns true if the RX buffer is aligned to the provided TX buffer. - pub fn verify_aligned_to(&self, tx: &TransmitRingbuffer) -> bool { - self.is_aligned() && self.tx_reference == (tx.0 as *const _ as usize) - } - - /// Returns the reference address of the TX buffer that this RX buffer is aligned to. - pub fn get_alignment_reference(&self) -> usize { - self.tx_reference - } - /// Read a slice from the receive buffer using the discovered alignment offset. - /// After aligning the buffer pair, the user can use this function to read from the receive - /// buffer without needing to worry about the physical alignment offset. - /// - /// # Panics - /// - /// This function will panic if `dst.len()` is greater than the buffer size. - /// This function will also panic if the caller tries to read beyond the end of the buffer. - /// This function will also panic if `align()` has not been called yet to discover the alignment offset. - pub fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { - assert!(dst.len() + offset <= ReceiveRingbuffer::DATA_LEN); - let rx_offset = self - .rx_alignment_offset - .expect("Alignment offset not discovered yet. Call align() first."); - let mut aligned_offset = offset + rx_offset; - if aligned_offset >= ReceiveRingbuffer::DATA_LEN { - aligned_offset -= ReceiveRingbuffer::DATA_LEN; - } - self.rx.read_slice_with_wrap(dst, aligned_offset) - } -} diff --git a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs index d1c90791c..a8c9fd276 100644 --- a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs +++ b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs @@ -28,9 +28,11 @@ //! 4. If CRC validates, extract payload and consume packet //! 5. Track sequence numbers to detect repeated packets -use bittide_hal::hals::ringbuffer_test::devices::{ReceiveRingbuffer, TransmitRingbuffer}; use bittide_hal::manual_additions::addressable_buffer::Aligned; -use bittide_hal::manual_additions::ringbuffer_test::ringbuffers::AlignedReceiveBuffer; +use bittide_hal::manual_additions::ringbuffer::{ + AlignedReceiveBuffer, ReceiveRingbufferInterface, TransmitRingbufferInterface, +}; +use bittide_hal::ringbuffer_test::devices::{ReceiveRingbuffer, TransmitRingbuffer}; use crc::{Crc, CRC_32_ISCSI}; use log::{trace, warn}; use smoltcp::phy::{self, Device, DeviceCapabilities, Medium}; @@ -69,9 +71,19 @@ fn is_valid(buffer: &[u8]) -> bool { /// /// The MTU is automatically calculated from the minimum of the scatter and gather /// buffer sizes (in bytes), minus space for packet header (which includes CRC32). -pub struct RingbufferDevice { - rx_buffer: AlignedReceiveBuffer, - tx_buffer: TransmitRingbuffer, +pub struct RingbufferDeviceImpl< + Rx, + Tx, + const RX_WORDS: usize, + const TX_WORDS: usize, + const RX_BYTES: usize, + const TX_BYTES: usize, +> where + Rx: ReceiveRingbufferInterface, + Tx: TransmitRingbufferInterface, +{ + rx_buffer: AlignedReceiveBuffer, + tx_buffer: Tx, mtu: usize, /// Last valid sequence number we saw (to detect repeated packets) last_rx_seq: u16, @@ -79,18 +91,26 @@ pub struct RingbufferDevice { tx_seq_num: u16, } -impl RingbufferDevice { +impl< + Rx, + Tx, + const RX_WORDS: usize, + const TX_WORDS: usize, + const RX_BYTES: usize, + const TX_BYTES: usize, + > RingbufferDeviceImpl +where + Rx: ReceiveRingbufferInterface + 'static, + Tx: TransmitRingbufferInterface + 'static, +{ /// Create a new ringbuffer device. /// /// The ringbuffers must already be aligned using the alignment protocol. /// The MTU is calculated as the minimum of the RX and TX buffer sizes in bytes. - pub fn new(rx_buffer: AlignedReceiveBuffer, tx_buffer: TransmitRingbuffer) -> Self { + pub fn new(rx_buffer: AlignedReceiveBuffer, tx_buffer: Tx) -> Self { // Calculate MTU from buffer sizes (each word is 8 bytes) // Reserve space for packet header (CRC is part of header) - let rx_bytes = ReceiveRingbuffer::DATA_LEN * 8; - let tx_bytes = TransmitRingbuffer::DATA_LEN * 8; - let mtu = rx_bytes.min(tx_bytes) - PACKET_HEADER_SIZE; - + let mtu = RX_BYTES.min(TX_BYTES) - PACKET_HEADER_SIZE; assert!(rx_buffer.is_aligned(), "RX buffer is not aligned "); Self { @@ -108,9 +128,20 @@ impl RingbufferDevice { } } -impl Device for RingbufferDevice { - type RxToken<'a> = RxToken; - type TxToken<'a> = TxToken<'a>; +impl< + Rx, + Tx, + const RX_WORDS: usize, + const TX_WORDS: usize, + const RX_BYTES: usize, + const TX_BYTES: usize, + > Device for RingbufferDeviceImpl +where + Rx: ReceiveRingbufferInterface + 'static, + Tx: TransmitRingbufferInterface + 'static, +{ + type RxToken<'a> = RxToken; + type TxToken<'a> = TxToken<'a, Tx, TX_WORDS>; fn capabilities(&self) -> DeviceCapabilities { let mut cap = DeviceCapabilities::default(); @@ -121,7 +152,7 @@ impl Device for RingbufferDevice { fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { // Allocate aligned buffer for reading from ringbuffer - let mut packet_buffer = Aligned::new([[0u8; 8]; ReceiveRingbuffer::DATA_LEN]); + let mut packet_buffer = Aligned::new([[0u8; 8]; RX_WORDS]); // Read first word containing the header: CRC32 (4 bytes) + sequence (2 bytes) + length (2 bytes) self.rx_buffer @@ -177,7 +208,7 @@ impl Device for RingbufferDevice { self.last_rx_seq = seq_num; // Extract payload (skip header) - let mut payload = Aligned::new([0u8; ReceiveRingbuffer::DATA_LEN * 8]); + let mut payload = Aligned::new([0u8; RX_BYTES]); let payload_bytes = unsafe { core::slice::from_raw_parts( (packet_buffer.get().as_ptr() as *const u8).add(PACKET_HEADER_SIZE), @@ -211,12 +242,12 @@ impl Device for RingbufferDevice { /// /// Contains a local copy of the packet payload that has been validated /// against CRC32 corruption. -pub struct RxToken { - buffer: Aligned<[u8; ReceiveRingbuffer::DATA_LEN * 8]>, +pub struct RxToken { + buffer: Aligned<[u8; RX_BYTES]>, length: usize, } -impl phy::RxToken for RxToken { +impl phy::RxToken for RxToken { fn consume(self, f: F) -> R where F: FnOnce(&[u8]) -> R, @@ -227,13 +258,19 @@ impl phy::RxToken for RxToken { } /// Transmit token for ringbuffer device -pub struct TxToken<'a> { - tx_buffer: &'a mut TransmitRingbuffer, +pub struct TxToken<'a, Tx, const TX_WORDS: usize> +where + Tx: TransmitRingbufferInterface, +{ + tx_buffer: &'a mut Tx, mtu: usize, seq_num: &'a mut u16, } -impl phy::TxToken for TxToken<'_> { +impl phy::TxToken for TxToken<'_, Tx, TX_WORDS> +where + Tx: TransmitRingbufferInterface, +{ fn consume(self, len: usize, f: F) -> R where F: FnOnce(&mut [u8]) -> R, @@ -246,7 +283,7 @@ impl phy::TxToken for TxToken<'_> { ); // Prepare aligned buffer: header + payload - let mut buffer = Aligned::new([[0u8; 8]; TransmitRingbuffer::DATA_LEN]); + let mut buffer = Aligned::new([[0u8; 8]; TX_WORDS]); // Write header fields using direct pointer writes // Header format: CRC32 (4 bytes) + sequence (2 bytes) + length (2 bytes) @@ -287,3 +324,22 @@ impl phy::TxToken for TxToken<'_> { result } } + +macro_rules! define_ringbuffer_device { + (name: $name:ident, rx: $rx:ty, tx: $tx:ty) => { + pub type $name = RingbufferDeviceImpl< + $rx, + $tx, + { <$rx as ReceiveRingbufferInterface>::DATA_LEN }, + { <$tx as TransmitRingbufferInterface>::DATA_LEN }, + { <$rx as ReceiveRingbufferInterface>::DATA_LEN * 8 }, + { <$tx as TransmitRingbufferInterface>::DATA_LEN * 8 }, + >; + }; +} + +define_ringbuffer_device! { + name: RingbufferDevice, + rx: ReceiveRingbuffer, + tx: TransmitRingbuffer +} From 1d31983dcd1646a74c201c61938a5bbd5733911b Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Wed, 11 Mar 2026 14:55:43 +0100 Subject: [PATCH 46/70] Print device MTUs --- firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs index fbc0a75a2..30c77c87a 100644 --- a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs @@ -175,10 +175,12 @@ fn main() -> ! { } let mut receive_iter = receive_ringbuffers.into_iter(); let mut transmit_iter = transmit_ringbuffers.into_iter(); - let mut devices: [RingbufferDevice; LINK_COUNT] = core::array::from_fn(|_| { + let mut devices: [RingbufferDevice; LINK_COUNT] = core::array::from_fn(|i| { let rx = receive_iter.next().expect("missing receive ringbuffer"); let tx = transmit_iter.next().expect("missing transmit ringbuffer"); - make_device(rx, tx) + let device = make_device(rx, tx); + trace!("Made device for link {}, with MTU {}", i, device.mtu()); + device }); let rx_buf = unsafe { &mut TCP_RX_BUFS[..] }; From e4eb1c95c177ebc2ca6d11dcbc4c7616eb3e840c Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Wed, 11 Mar 2026 14:57:13 +0100 Subject: [PATCH 47/70] First step then poll --- firmware-support/bittide-sys/src/net_state.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware-support/bittide-sys/src/net_state.rs b/firmware-support/bittide-sys/src/net_state.rs index 2e353b596..83c8ca0ce 100644 --- a/firmware-support/bittide-sys/src/net_state.rs +++ b/firmware-support/bittide-sys/src/net_state.rs @@ -227,8 +227,8 @@ impl Manager { device: &mut impl smoltcp::phy::Device, sockets: &mut SocketSet, ) { - self.iface.poll(timestamp, device, sockets); self.step(sockets); + self.iface.poll(timestamp, device, sockets); } pub fn is_done(&self) -> bool { From 3e834c1c690c29ceeea1f58bf69e7ad05bc07bfd Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Wed, 11 Mar 2026 17:17:07 +0100 Subject: [PATCH 48/70] Simplify --- .../demos/soft-ugn-demo-mu-2/src/main.rs | 25 +- .../examples/smoltcp_client/src/main.rs | 2 +- .../aligned_ringbuffer_test/src/main.rs | 5 +- .../ringbuffer_smoltcp_test/src/main.rs | 9 +- .../bittide-hal/src/manual_additions/mod.rs | 2 - .../src/manual_additions/ringbuffer.rs | 18 +- .../manual_additions/ringbuffer_test/mod.rs | 4 - .../ringbuffer_test/ringbuffers.rs | 153 --------- .../manual_additions/soft_ugn_demo_mu/mod.rs | 4 - .../soft_ugn_demo_mu/ringbuffers.rs | 285 ---------------- firmware-support/bittide-sys/src/lib.rs | 2 + .../src/{smoltcp.rs => smoltcp/mac.rs} | 3 - .../bittide-sys/src/smoltcp/mod.rs | 6 + .../bittide-sys/src/smoltcp/ringbuffer.rs | 68 +--- .../src/smoltcp/soft_ugn_ringbuffer.rs | 316 ------------------ 15 files changed, 70 insertions(+), 832 deletions(-) delete mode 100644 firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/mod.rs delete mode 100644 firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/ringbuffers.rs delete mode 100644 firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/mod.rs delete mode 100644 firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/ringbuffers.rs rename firmware-support/bittide-sys/src/{smoltcp.rs => smoltcp/mac.rs} (94%) create mode 100644 firmware-support/bittide-sys/src/smoltcp/mod.rs delete mode 100644 firmware-support/bittide-sys/src/smoltcp/soft_ugn_ringbuffer.rs diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs index 30c77c87a..bb1e845ba 100644 --- a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs @@ -8,10 +8,13 @@ use bittide_hal::hals::soft_ugn_demo_mu::devices::{ReceiveRingbuffer, TransmitRingbuffer}; use bittide_hal::hals::soft_ugn_demo_mu::DeviceInstances; +use bittide_hal::manual_additions::ringbuffer::{ + AlignedReceiveBuffer, TransmitRingbufferInterface, +}; use bittide_hal::manual_additions::timer::Instant; use bittide_sys::link_startup::LinkStartup; use bittide_sys::net_state::{Manager, Subordinate, UgnEdge, UgnReport}; -use bittide_sys::smoltcp::soft_ugn_ringbuffer::{AlignedReceiveBuffer, RingbufferDevice}; +use bittide_sys::smoltcp::ringbuffer::RingbufferDevice; use bittide_sys::stability_detector::Stability; use core::fmt::Write; use log::{info, trace, warn, LevelFilter}; @@ -54,7 +57,10 @@ fn socket_set<'a>(storage: &'a mut [SocketStorage<'static>]) -> SocketSet<'a> { SocketSet::new(storage) } -fn make_device(rx: ReceiveRingbuffer, tx: TransmitRingbuffer) -> RingbufferDevice { +fn make_device( + rx: ReceiveRingbuffer, + tx: TransmitRingbuffer, +) -> RingbufferDevice { let mut rx_aligned = AlignedReceiveBuffer::new(rx); rx_aligned.align(&tx); RingbufferDevice::new(rx_aligned, tx) @@ -175,13 +181,14 @@ fn main() -> ! { } let mut receive_iter = receive_ringbuffers.into_iter(); let mut transmit_iter = transmit_ringbuffers.into_iter(); - let mut devices: [RingbufferDevice; LINK_COUNT] = core::array::from_fn(|i| { - let rx = receive_iter.next().expect("missing receive ringbuffer"); - let tx = transmit_iter.next().expect("missing transmit ringbuffer"); - let device = make_device(rx, tx); - trace!("Made device for link {}, with MTU {}", i, device.mtu()); - device - }); + let mut devices: [RingbufferDevice; LINK_COUNT] = + core::array::from_fn(|i| { + let rx = receive_iter.next().expect("missing receive ringbuffer"); + let tx = transmit_iter.next().expect("missing transmit ringbuffer"); + let device = make_device(rx, tx); + trace!("Made device for link {}, with MTU {}", i, device.mtu()); + device + }); let rx_buf = unsafe { &mut TCP_RX_BUFS[..] }; let tx_buf = unsafe { &mut TCP_TX_BUFS[..] }; diff --git a/firmware-binaries/examples/smoltcp_client/src/main.rs b/firmware-binaries/examples/smoltcp_client/src/main.rs index e7eaabcf9..539c6df69 100644 --- a/firmware-binaries/examples/smoltcp_client/src/main.rs +++ b/firmware-binaries/examples/smoltcp_client/src/main.rs @@ -13,7 +13,7 @@ use bittide_hal::manual_additions::timer::{Duration, Instant}; use bittide_sys::axi::{AxiRx, AxiTx}; use bittide_sys::mac::MacStatus; use bittide_sys::smoltcp::axi::AxiEthernet; -use bittide_sys::smoltcp::{set_local, set_unicast}; +use bittide_sys::smoltcp::mac::{set_local, set_unicast}; use bittide_sys::uart::log::LOGGER; use log::{debug, info, LevelFilter}; diff --git a/firmware-binaries/sim-tests/aligned_ringbuffer_test/src/main.rs b/firmware-binaries/sim-tests/aligned_ringbuffer_test/src/main.rs index 6b7cdf24a..a59ed1756 100644 --- a/firmware-binaries/sim-tests/aligned_ringbuffer_test/src/main.rs +++ b/firmware-binaries/sim-tests/aligned_ringbuffer_test/src/main.rs @@ -5,7 +5,10 @@ #![cfg_attr(not(test), no_main)] use bittide_hal::{ - manual_additions::{ringbuffer_test::ringbuffers::AlignedReceiveBuffer, timer::Duration}, + manual_additions::{ + ringbuffer::{AlignedReceiveBuffer, TransmitRingbufferInterface}, + timer::Duration, + }, ringbuffer_test::{devices::TransmitRingbuffer, DeviceInstances}, }; use core::fmt::Write; diff --git a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs index 010c6be20..0a9997a04 100644 --- a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs +++ b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/src/main.rs @@ -5,8 +5,9 @@ #![cfg_attr(not(test), no_main)] #![feature(sync_unsafe_cell)] -use bittide_hal::manual_additions::ringbuffer_test::ringbuffers::AlignedReceiveBuffer; +use bittide_hal::manual_additions::ringbuffer::AlignedReceiveBuffer; use bittide_hal::manual_additions::timer::Instant; +use bittide_hal::ringbuffer_test::devices::{ReceiveRingbuffer, TransmitRingbuffer}; use bittide_hal::ringbuffer_test::DeviceInstances; use bittide_sys::net_state::{Manager, Subordinate, UgnEdge, UgnReport}; use bittide_sys::smoltcp::ringbuffer::RingbufferDevice; @@ -68,8 +69,10 @@ fn main() -> ! { // Step 2: Create smoltcp device info!("Step 2: Creating RingbufferDevice..."); - let mut device0 = RingbufferDevice::new(rx_aligned0, tx_buffer1); - let mut device1 = RingbufferDevice::new(rx_aligned1, tx_buffer0); + let mut device0: RingbufferDevice = + RingbufferDevice::new(rx_aligned0, tx_buffer1); + let mut device1: RingbufferDevice = + RingbufferDevice::new(rx_aligned1, tx_buffer0); let mtu = device0.mtu(); trace!(" MTU: {} bytes", mtu); diff --git a/firmware-support/bittide-hal/src/manual_additions/mod.rs b/firmware-support/bittide-hal/src/manual_additions/mod.rs index 57b3b6cdf..7e999178a 100644 --- a/firmware-support/bittide-hal/src/manual_additions/mod.rs +++ b/firmware-support/bittide-hal/src/manual_additions/mod.rs @@ -9,10 +9,8 @@ pub mod capture_ugn; pub mod dna; pub mod elastic_buffer; pub mod ringbuffer; -pub mod ringbuffer_test; pub mod scatter_gather_pe; pub mod si539x_spi; -pub mod soft_ugn_demo_mu; pub mod timer; pub mod uart; diff --git a/firmware-support/bittide-hal/src/manual_additions/ringbuffer.rs b/firmware-support/bittide-hal/src/manual_additions/ringbuffer.rs index add870f89..7103e37ff 100644 --- a/firmware-support/bittide-hal/src/manual_additions/ringbuffer.rs +++ b/firmware-support/bittide-hal/src/manual_additions/ringbuffer.rs @@ -2,6 +2,8 @@ // // SPDX-License-Identifier: Apache-2.0 +use log::warn; + const ALIGNMENT_ANNOUNCE: u64 = 0xBADC0FFEE; const ALIGNMENT_ACKNOWLEDGE: u64 = 0xDEADABBA; @@ -26,7 +28,15 @@ pub trait TransmitRingbufferInterface { /// does not ensure that `src.len() + offset` is within the bounds of the transmit buffer. unsafe fn write_slice_unchecked(&self, src: &[[u8; 8]], offset: usize) { let dst_ptr = self.base_ptr().add(offset); - core::ptr::copy_nonoverlapping(src.as_ptr(), dst_ptr, src.len()); + let src_ptr = src.as_ptr(); + if (src_ptr as usize) % 4 != 0 || (dst_ptr as usize) % 4 != 0 { + warn!( + "ringbuffer tx write_slice_unchecked unaligned: src {:p} dst {:p}", + src_ptr, dst_ptr + ); + } + + core::ptr::copy_nonoverlapping(src_ptr, dst_ptr, src.len()); } /// Write a slice to the transmit buffer wrapping if `src.len() + offset`exceeds the length @@ -84,6 +94,12 @@ pub trait ReceiveRingbufferInterface { unsafe fn read_slice_unchecked(&self, dst: &mut [[u8; 8]], offset: usize) { let dst_ptr = dst.as_mut_ptr(); let src_ptr = self.base_ptr().add(offset); + if (src_ptr as usize) % 4 != 0 || (dst_ptr as usize) % 4 != 0 { + warn!( + "ringbuffer rx read_slice_unchecked unaligned: src {:p} dst {:p}", + src_ptr, dst_ptr + ); + } core::ptr::copy_nonoverlapping(src_ptr, dst_ptr, dst.len()); } diff --git a/firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/mod.rs b/firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/mod.rs deleted file mode 100644 index 2590006d7..000000000 --- a/firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Google LLC -// -// SPDX-License-Identifier: Apache-2.0 -pub mod ringbuffers; diff --git a/firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/ringbuffers.rs b/firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/ringbuffers.rs deleted file mode 100644 index 9d714a90b..000000000 --- a/firmware-support/bittide-hal/src/manual_additions/ringbuffer_test/ringbuffers.rs +++ /dev/null @@ -1,153 +0,0 @@ -use log::trace; - -// SPDX-FileCopyrightText: 2025 Google LLC -// -// SPDX-License-Identifier: Apache-2.0 -use crate::hals::ringbuffer_test::devices::{ReceiveRingbuffer, TransmitRingbuffer}; - -pub type AlignedReceiveBuffer = crate::manual_additions::ringbuffer::AlignedReceiveBuffer< - ReceiveRingbuffer, - TransmitRingbuffer, ->; - -impl TransmitRingbuffer { - /// Write a slice to the transmit buffer. - /// - /// # Panics - /// - /// The source memory size must be smaller or equal to the memory size of - /// the `TransmitRingbuffer` memory. - pub fn write_slice(&self, src: &[[u8; 8]], offset: usize) { - // trace!("write_slice called with offset {} and src {:02x?}", offset, src); - assert!(src.len() + offset <= Self::DATA_LEN); - unsafe { - self.write_slice_unchecked(src, offset); - } - } - - /// Write a slice to the transmit buffer without checking bounds. The caller must - /// ensure that `src.len() + offset` does not exceed the buffer size. - /// - /// # Safety - /// - /// This function is unsafe because it can cause out-of-bounds memory access if the caller - /// does not ensure that `src.len() + offset` is within the bounds of the transmit buffer. - pub unsafe fn write_slice_unchecked(&self, src: &[[u8; 8]], offset: usize) { - let src_ptr = src.as_ptr(); - let dst_ptr = self.0.add(offset) as *mut [u8; 8]; - // trace!("Writing slice to transmit buffer at offset {}: {:02x?}", offset, src); - unsafe { - core::ptr::copy_nonoverlapping(src_ptr, dst_ptr, src.len()); - } - // trace!("Done"); - } - - /// Write a slice to the transmit buffer with automatic wrapping. If we write more bytes - /// than the buffer size, we wrap back to the beginning of the buffer. - /// - /// # Panics - /// - /// This function will panic if `src.len()` is greater than the buffer size. - pub fn write_slice_with_wrap(&self, src: &[[u8; 8]], offset: usize) { - assert!(src.len() <= Self::DATA_LEN); - unsafe { - self.write_slice_with_wrap_unchecked(src, offset); - } - } - - /// Write a slice to the transmit buffer with automatic wrapping. If we write more bytes - /// than the buffer size, we wrap back to the beginning of the buffer. - /// - /// # Safety - /// This function is unsafe because it can cause out-of-bounds memory access if the caller - /// does not ensure that `src.len()` is smaller or equal to the buffer size. - pub unsafe fn write_slice_with_wrap_unchecked(&self, src: &[[u8; 8]], offset: usize) { - if src.len() + offset <= Self::DATA_LEN { - // No wrapping needed - self.write_slice(src, offset); - } else { - // Wrapping needed - split into two writes - let first_part_len = Self::DATA_LEN - offset; - let (first, second) = src.split_at(first_part_len); - self.write_slice(first, offset); - self.write_slice(second, 0); - } - } - - /// Clear the entire transmit buffer by writing zeros to all entries. - pub fn clear(&self) { - for i in 0..Self::DATA_LEN { - unsafe { - self.set_data_unchecked(i, [0u8; 8]); - } - } - } -} - -impl ReceiveRingbuffer { - /// Read a slice from the receive buffer. - /// - /// # Panics - /// - /// The destination memory size must be smaller or equal to the memory size - /// of the `ReceiveRingbuffer`. - pub fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { - trace!( - "read_slice called with offset {} and dst len {}", - offset, - dst.len() - ); - assert!(dst.len() + offset <= Self::DATA_LEN); - unsafe { - self.read_slice_unchecked(dst, offset); - } - trace!("Done"); - } - - /// Reads a slice from the receive buffer without checking bounds. The caller must - /// ensure that `dst.len() + offset` does not exceed the buffer size. - /// - /// # Safety - /// - /// This function is unsafe because it can cause out-of-bounds memory access if the caller - /// does not ensure that `dst.len() + offset` is within the bounds of the receive buffer. - pub unsafe fn read_slice_unchecked(&self, dst: &mut [[u8; 8]], offset: usize) { - let dst_ptr = dst.as_mut_ptr(); - let src_ptr = self.0 as *const [u8; 8]; - unsafe { - core::ptr::copy_nonoverlapping(src_ptr.add(offset), dst_ptr, dst.len()); - } - } - - /// Reads a slice from the receive buffer with automatic wrapping. If we read more bytes - /// than the buffer size, we wrap back to the beginning of the buffer. - /// - /// # Panics - /// - /// This function will panic if `dst.len()` is greater than the buffer size. - pub fn read_slice_with_wrap(&self, dst: &mut [[u8; 8]], offset: usize) { - assert!(dst.len() <= Self::DATA_LEN); - unsafe { - self.read_slice_with_wrap_unchecked(dst, offset); - } - } - /// Reads a slice from the receive buffer with automatic wrapping. If we read more bytes - /// than the buffer size, we wrap back to the beginning of the buffer. - /// - /// # Safety - /// - /// This function is unsafe because it can cause out-of-bounds memory access if the caller - /// does not ensure that `dst.len()` is smaller or equal to the buffer size. - pub unsafe fn read_slice_with_wrap_unchecked(&self, dst: &mut [[u8; 8]], offset: usize) { - if dst.len() + offset <= Self::DATA_LEN { - // No wrapping needed - self.read_slice(dst, offset); - } else { - // Wrapping needed - split into two reads - let first_part_len = Self::DATA_LEN - offset; - let (first, second) = dst.split_at_mut(first_part_len); - self.read_slice(first, offset); - self.read_slice(second, 0); - } - } -} diff --git a/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/mod.rs b/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/mod.rs deleted file mode 100644 index 2590006d7..000000000 --- a/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Google LLC -// -// SPDX-License-Identifier: Apache-2.0 -pub mod ringbuffers; diff --git a/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/ringbuffers.rs b/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/ringbuffers.rs deleted file mode 100644 index 01dd1dcfb..000000000 --- a/firmware-support/bittide-hal/src/manual_additions/soft_ugn_demo_mu/ringbuffers.rs +++ /dev/null @@ -1,285 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Google LLC -// -// SPDX-License-Identifier: Apache-2.0 -use crate::hals::soft_ugn_demo_mu::devices::{ReceiveRingbuffer, TransmitRingbuffer}; -use log::{debug, warn}; - -/// Alignment protocol marker values -const ALIGNMENT_EMPTY: u64 = 0; -const ALIGNMENT_ANNOUNCE: u64 = 0xBADC0FFEE; -const ALIGNMENT_ACKNOWLEDGE: u64 = 0xDEADABBA; - -impl TransmitRingbuffer { - /// Write a slice to the transmit buffer. - /// - /// # Panics - /// - /// The source memory size must be smaller or equal to the memory size of - /// the `TransmitRingbuffer` memory. - pub fn write_slice(&self, src: &[[u8; 8]], offset: usize) { - assert!(src.len() + offset <= Self::DATA_LEN); - unsafe { - self.write_slice_unchecked(src, offset); - } - } - - /// Write a slice to the transmit buffer without checking bounds. The caller must - /// ensure that `src.len() + offset` does not exceed the buffer size. - /// - /// # Safety - /// - /// This function is unsafe because it can cause out-of-bounds memory access if the caller - /// does not ensure that `src.len() + offset` is within the bounds of the transmit buffer. - pub unsafe fn write_slice_unchecked(&self, src: &[[u8; 8]], offset: usize) { - let dst_ptr = self.0 as *mut [u8; 8]; - unsafe { - core::ptr::copy_nonoverlapping(src.as_ptr(), dst_ptr.add(offset), src.len()); - } - } - - /// Write a slice to the transmit buffer with automatic wrapping. If we write more bytes - /// than the buffer size, we wrap back to the beginning of the buffer. - /// - /// # Panics - /// - /// This function will panic if `src.len()` is greater than the buffer size. - pub fn write_slice_with_wrap(&self, src: &[[u8; 8]], offset: usize) { - assert!(src.len() <= Self::DATA_LEN); - unsafe { - self.write_slice_with_wrap_unchecked(src, offset); - } - } - - /// Write a slice to the transmit buffer with automatic wrapping. If we write more bytes - /// than the buffer size, we wrap back to the beginning of the buffer. - /// - /// # Safety - /// This function is unsafe because it can cause out-of-bounds memory access if the caller - /// does not ensure that `src.len()` is smaller or equal to the buffer size. - pub unsafe fn write_slice_with_wrap_unchecked(&self, src: &[[u8; 8]], offset: usize) { - if src.len() + offset <= Self::DATA_LEN { - self.write_slice(src, offset); - } else { - // Wrapping needed - split into two writes - let first_part_len = Self::DATA_LEN - offset; - let (first, second) = src.split_at(first_part_len); - debug!("TransmitRingbuffer::write_slice_with_wrap: wrapping at offset={}, first_len={}, second_len={}", offset, first.len(), second.len()); - self.write_slice(first, offset); - self.write_slice(second, 0); - } - } - - /// Clear the entire transmit buffer by writing zeros to all entries. - pub fn clear(&self) { - debug!( - "TransmitRingbuffer::clear: about to clear {} entries", - Self::DATA_LEN - ); - for i in 0..Self::DATA_LEN { - unsafe { - self.set_data_unchecked(i, [0u8; 8]); - } - } - debug!("TransmitRingbuffer::clear: completed"); - } -} - -impl ReceiveRingbuffer { - /// Read a slice from the receive buffer. - /// - /// # Panics - /// - /// The destination memory size must be smaller or equal to the memory size - /// of the `ReceiveRingbuffer`. - pub fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { - assert!(dst.len() + offset <= Self::DATA_LEN); - unsafe { - self.read_slice_unchecked(dst, offset); - } - } - - /// Reads a slice from the receive buffer without checking bounds. The caller must - /// ensure that `dst.len() + offset` does not exceed the buffer size. - /// - /// # Safety - /// - /// This function is unsafe because it can cause out-of-bounds memory access if the caller - /// does not ensure that `dst.len() + offset` is within the bounds of the receive buffer. - pub unsafe fn read_slice_unchecked(&self, dst: &mut [[u8; 8]], offset: usize) { - let dst_ptr = dst.as_mut_ptr(); - let src_ptr = self.0 as *const [u8; 8]; - unsafe { - core::ptr::copy_nonoverlapping(src_ptr.add(offset), dst_ptr, dst.len()); - } - } - - /// Reads a slice from the receive buffer with automatic wrapping. If we read more bytes - /// than the buffer size, we wrap back to the beginning of the buffer. - /// - /// # Panics - /// - /// This function will panic if `dst.len()` is greater than the buffer size. - pub fn read_slice_with_wrap(&self, dst: &mut [[u8; 8]], offset: usize) { - assert!(dst.len() <= Self::DATA_LEN); - unsafe { - self.read_slice_with_wrap_unchecked(dst, offset); - } - } - /// Reads a slice from the receive buffer with automatic wrapping. If we read more bytes - /// than the buffer size, we wrap back to the beginning of the buffer. - /// - /// # Safety - /// - /// This function is unsafe because it can cause out-of-bounds memory access if the caller - /// does not ensure that `dst.len()` is smaller or equal to the buffer size. - pub unsafe fn read_slice_with_wrap_unchecked(&self, dst: &mut [[u8; 8]], offset: usize) { - if dst.len() + offset <= Self::DATA_LEN { - self.read_slice(dst, offset); - } else { - // Wrapping needed - split into two reads - let first_part_len = Self::DATA_LEN - offset; - let (first, second) = dst.split_at_mut(first_part_len); - debug!("ReceiveRingbuffer::read_slice_with_wrap: wrapping at offset={}, first_len={}, second_len={}", offset, first.len(), second.len()); - self.read_slice(first, offset); - self.read_slice(second, 0); - } - } -} - -pub struct AlignedReceiveBuffer { - pub rx: ReceiveRingbuffer, - rx_alignment_offset: Option, - tx_reference: usize, // Allows us to verify the tx_buffer we are aligned to. -} -impl AlignedReceiveBuffer { - /// Perform the alignment discovery protocol and return a new `AlignedReceiveBuffer` - /// with the discovered RX alignment offset. - /// - /// # Panics - /// - /// This function will panic if the transmit and receive buffers do not have the same size, - /// since that is a requirement for the alignment protocol to work. - pub fn new(rx: ReceiveRingbuffer) -> Self { - debug!("AlignedReceiveBuffer::new: creating new buffer"); - Self { - rx, - rx_alignment_offset: None, - tx_reference: 0, - } - } - - /// Returns true if the alignment offset has been discovered and the buffer is aligned. - pub fn is_aligned(&self) -> bool { - self.rx_alignment_offset.is_some() - } - - /// Returns the discovered alignment offset, or None if the offset has not been discovered yet. - pub fn get_alignment_offset(&self) -> Option { - self.rx_alignment_offset - } - - /// Performs the alignment discovery protocol. After this function completes, the `rx_alignment_offset` - /// field will be set with the discovered offset, and the RX buffer will be aligned to the neighbor's TX buffer. - pub fn align(&mut self, tx: &TransmitRingbuffer) { - debug!("AlignedReceiveBuffer::align: starting alignment protocol"); - - // Clear TX buffer completely first to remove any stale patterns - debug!("AlignedReceiveBuffer::align: about to clear entire TX buffer"); - let empty_pattern: [[u8; 8]; 1] = [ALIGNMENT_EMPTY.to_le_bytes()]; - for i in 0..TransmitRingbuffer::DATA_LEN { - tx.write_slice(&empty_pattern, i); - } - debug!("AlignedReceiveBuffer::align: cleared entire TX buffer"); - - // Write ANNOUNCE at index 0 - debug!("AlignedReceiveBuffer::align: about to write ANNOUNCE pattern to TX"); - let announce_pattern = [ALIGNMENT_ANNOUNCE.to_le_bytes()]; - tx.write_slice(&announce_pattern, 0); - debug!("AlignedReceiveBuffer::align: sent ANNOUNCE pattern"); - - // Phase 1: Scan RX buffer to find ANNOUNCE or ACKNOWLEDGE - // Read directly from scatter memory using read_slice with offset 0 - debug!("AlignedReceiveBuffer::align: phase 1 - scanning RX buffer for alignment marker"); - let rx_offset = 'outer: loop { - for rx_idx in 0..ReceiveRingbuffer::DATA_LEN { - let mut data_buf = [[0u8; 8]; 1]; - self.rx.read_slice(&mut data_buf, rx_idx); - let value = u64::from_le_bytes(data_buf[0]); - - if value == ALIGNMENT_ANNOUNCE || value == ALIGNMENT_ACKNOWLEDGE { - debug!("AlignedReceiveBuffer::align: found alignment marker at rx_idx={}, value=0x{:x}", rx_idx, value); - break 'outer rx_idx; - } - } - }; - debug!( - "AlignedReceiveBuffer::align: discovered rx_offset={}", - rx_offset - ); - - // Phase 2: Send ACKNOWLEDGE and wait for confirmation - debug!("AlignedReceiveBuffer::align: phase 2 - about to send ACKNOWLEDGE"); - let ack_pattern = [ALIGNMENT_ACKNOWLEDGE.to_le_bytes()]; - tx.write_slice(&ack_pattern, 0); - debug!("AlignedReceiveBuffer::align: sent ACKNOWLEDGE, waiting for confirmation"); - - loop { - let mut data_buf = [[0u8; 8]; 1]; - self.rx.read_slice(&mut data_buf, rx_offset); - let value = u64::from_le_bytes(data_buf[0]); - - if value == ALIGNMENT_ACKNOWLEDGE { - debug!("AlignedReceiveBuffer::align: received ACKNOWLEDGE confirmation"); - break; - } - } - self.rx_alignment_offset = Some(rx_offset); - self.tx_reference = tx.0 as *const _ as usize; - debug!( - "AlignedReceiveBuffer::align: alignment complete, offset={}, tx_ref=0x{:x}", - rx_offset, self.tx_reference - ); - } - - /// Unsets the discovered alignment offset. - pub fn clear_alignment(&mut self) { - debug!("AlignedReceiveBuffer::clear_alignment: clearing alignment"); - self.rx_alignment_offset = None; - self.tx_reference = 0; - } - - /// Returns true if the RX buffer is aligned to the provided TX buffer. - pub fn verify_aligned_to(&self, tx: &TransmitRingbuffer) -> bool { - let tx_addr = tx.0 as *const _ as usize; - let result = self.is_aligned() && self.tx_reference == tx_addr; - if !result { - warn!("AlignedReceiveBuffer::verify_aligned_to: verification failed, aligned={}, tx_ref=0x{:x}, tx_addr=0x{:x}", self.is_aligned(), self.tx_reference, tx_addr); - } - result - } - - /// Returns the reference address of the TX buffer that this RX buffer is aligned to. - pub fn get_alignment_reference(&self) -> usize { - self.tx_reference - } - /// Read a slice from the receive buffer using the discovered alignment offset. - /// After aligning the buffer pair, the user can use this function to read from the receive - /// buffer without needing to worry about the physical alignment offset. - /// - /// # Panics - /// - /// This function will panic if `dst.len()` is greater than the buffer size. - /// This function will also panic if the caller tries to read beyond the end of the buffer. - /// This function will also panic if `align()` has not been called yet to discover the alignment offset. - pub fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { - assert!(dst.len() + offset <= ReceiveRingbuffer::DATA_LEN); - let rx_offset = self - .rx_alignment_offset - .expect("Alignment offset not discovered yet. Call align() first."); - let mut aligned_offset = offset + rx_offset; - if aligned_offset >= ReceiveRingbuffer::DATA_LEN { - aligned_offset -= ReceiveRingbuffer::DATA_LEN; - } - self.rx.read_slice_with_wrap(dst, aligned_offset) - } -} diff --git a/firmware-support/bittide-sys/src/lib.rs b/firmware-support/bittide-sys/src/lib.rs index f88e440ee..ac267ca77 100644 --- a/firmware-support/bittide-sys/src/lib.rs +++ b/firmware-support/bittide-sys/src/lib.rs @@ -4,6 +4,8 @@ #![no_std] #![feature(sync_unsafe_cell)] +#![allow(incomplete_features)] +#![feature(generic_const_exprs)] pub mod axi; pub mod callisto; diff --git a/firmware-support/bittide-sys/src/smoltcp.rs b/firmware-support/bittide-sys/src/smoltcp/mac.rs similarity index 94% rename from firmware-support/bittide-sys/src/smoltcp.rs rename to firmware-support/bittide-sys/src/smoltcp/mac.rs index 88ef6092a..082cf5d23 100644 --- a/firmware-support/bittide-sys/src/smoltcp.rs +++ b/firmware-support/bittide-sys/src/smoltcp/mac.rs @@ -1,9 +1,6 @@ // SPDX-FileCopyrightText: 2024 Google LLC // // SPDX-License-Identifier: Apache-2.0 -pub mod axi; -pub mod ringbuffer; -pub mod soft_ugn_ringbuffer; use smoltcp::wire::EthernetAddress; diff --git a/firmware-support/bittide-sys/src/smoltcp/mod.rs b/firmware-support/bittide-sys/src/smoltcp/mod.rs new file mode 100644 index 000000000..29b1f0fbe --- /dev/null +++ b/firmware-support/bittide-sys/src/smoltcp/mod.rs @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2026 Google LLC +// +// SPDX-License-Identifier: Apache-2.0 +pub mod axi; +pub mod mac; +pub mod ringbuffer; diff --git a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs index a8c9fd276..5cc144500 100644 --- a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs +++ b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs @@ -27,12 +27,11 @@ //! 3. Verify the CRC32 over sequence + length + payload //! 4. If CRC validates, extract payload and consume packet //! 5. Track sequence numbers to detect repeated packets - use bittide_hal::manual_additions::addressable_buffer::Aligned; use bittide_hal::manual_additions::ringbuffer::{ AlignedReceiveBuffer, ReceiveRingbufferInterface, TransmitRingbufferInterface, }; -use bittide_hal::ringbuffer_test::devices::{ReceiveRingbuffer, TransmitRingbuffer}; + use crc::{Crc, CRC_32_ISCSI}; use log::{trace, warn}; use smoltcp::phy::{self, Device, DeviceCapabilities, Medium}; @@ -71,14 +70,8 @@ fn is_valid(buffer: &[u8]) -> bool { /// /// The MTU is automatically calculated from the minimum of the scatter and gather /// buffer sizes (in bytes), minus space for packet header (which includes CRC32). -pub struct RingbufferDeviceImpl< - Rx, - Tx, - const RX_WORDS: usize, - const TX_WORDS: usize, - const RX_BYTES: usize, - const TX_BYTES: usize, -> where +pub struct RingbufferDevice +where Rx: ReceiveRingbufferInterface, Tx: TransmitRingbufferInterface, { @@ -91,14 +84,7 @@ pub struct RingbufferDeviceImpl< tx_seq_num: u16, } -impl< - Rx, - Tx, - const RX_WORDS: usize, - const TX_WORDS: usize, - const RX_BYTES: usize, - const TX_BYTES: usize, - > RingbufferDeviceImpl +impl RingbufferDevice where Rx: ReceiveRingbufferInterface + 'static, Tx: TransmitRingbufferInterface + 'static, @@ -110,7 +96,7 @@ where pub fn new(rx_buffer: AlignedReceiveBuffer, tx_buffer: Tx) -> Self { // Calculate MTU from buffer sizes (each word is 8 bytes) // Reserve space for packet header (CRC is part of header) - let mtu = RX_BYTES.min(TX_BYTES) - PACKET_HEADER_SIZE; + let mtu = (Rx::DATA_LEN * 8).min(Tx::DATA_LEN * 8) - PACKET_HEADER_SIZE; assert!(rx_buffer.is_aligned(), "RX buffer is not aligned "); Self { @@ -128,20 +114,21 @@ where } } -impl< - Rx, - Tx, - const RX_WORDS: usize, - const TX_WORDS: usize, - const RX_BYTES: usize, - const TX_BYTES: usize, - > Device for RingbufferDeviceImpl +impl Device for RingbufferDevice where Rx: ReceiveRingbufferInterface + 'static, Tx: TransmitRingbufferInterface + 'static, + [(); Rx::DATA_LEN * 8]:, + [(); Tx::DATA_LEN * 8]:, { - type RxToken<'a> = RxToken; - type TxToken<'a> = TxToken<'a, Tx, TX_WORDS>; + type RxToken<'a> + = RxToken<{ Rx::DATA_LEN * 8 }> + where + [(); Rx::DATA_LEN * 8]:; + type TxToken<'a> + = TxToken<'a, Tx, { Tx::DATA_LEN * 8 }> + where + [(); Tx::DATA_LEN * 8]:; fn capabilities(&self) -> DeviceCapabilities { let mut cap = DeviceCapabilities::default(); @@ -152,7 +139,7 @@ where fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { // Allocate aligned buffer for reading from ringbuffer - let mut packet_buffer = Aligned::new([[0u8; 8]; RX_WORDS]); + let mut packet_buffer = Aligned::new([[0u8; 8]; Rx::DATA_LEN]); // Read first word containing the header: CRC32 (4 bytes) + sequence (2 bytes) + length (2 bytes) self.rx_buffer @@ -208,7 +195,7 @@ where self.last_rx_seq = seq_num; // Extract payload (skip header) - let mut payload = Aligned::new([0u8; RX_BYTES]); + let mut payload = Aligned::new([0u8; Rx::DATA_LEN * 8]); let payload_bytes = unsafe { core::slice::from_raw_parts( (packet_buffer.get().as_ptr() as *const u8).add(PACKET_HEADER_SIZE), @@ -324,22 +311,3 @@ where result } } - -macro_rules! define_ringbuffer_device { - (name: $name:ident, rx: $rx:ty, tx: $tx:ty) => { - pub type $name = RingbufferDeviceImpl< - $rx, - $tx, - { <$rx as ReceiveRingbufferInterface>::DATA_LEN }, - { <$tx as TransmitRingbufferInterface>::DATA_LEN }, - { <$rx as ReceiveRingbufferInterface>::DATA_LEN * 8 }, - { <$tx as TransmitRingbufferInterface>::DATA_LEN * 8 }, - >; - }; -} - -define_ringbuffer_device! { - name: RingbufferDevice, - rx: ReceiveRingbuffer, - tx: TransmitRingbuffer -} diff --git a/firmware-support/bittide-sys/src/smoltcp/soft_ugn_ringbuffer.rs b/firmware-support/bittide-sys/src/smoltcp/soft_ugn_ringbuffer.rs deleted file mode 100644 index 6f2131cc0..000000000 --- a/firmware-support/bittide-sys/src/smoltcp/soft_ugn_ringbuffer.rs +++ /dev/null @@ -1,316 +0,0 @@ -// SPDX-FileCopyrightText: 2026 Google LLC -// -// SPDX-License-Identifier: Apache-2.0 - -//! smoltcp Device implementation for aligned soft-UGN ringbuffers. - -use bittide_hal::hals::soft_ugn_demo_mu::devices::{ReceiveRingbuffer, TransmitRingbuffer}; -use crc::{Crc, CRC_32_ISCSI}; -use log::trace; -use smoltcp::phy::{self, Device, DeviceCapabilities, Medium}; -use smoltcp::time::Instant; - -const ALIGNMENT_EMPTY: u64 = 0; -const ALIGNMENT_ANNOUNCE: u64 = 0xBADC0FFEE; -const ALIGNMENT_ACKNOWLEDGE: u64 = 0xDEADABBA; -const PACKET_HEADER_SIZE: usize = 8; -const MIN_IP_PACKET_SIZE: usize = 20; -const CRC: Crc = Crc::::new(&CRC_32_ISCSI); - -pub struct AlignedReceiveBuffer { - rx: ReceiveRingbuffer, - rx_alignment_offset: Option, - tx_reference: usize, -} - -impl AlignedReceiveBuffer { - pub fn new(rx: ReceiveRingbuffer) -> Self { - Self { - rx, - rx_alignment_offset: None, - tx_reference: 0, - } - } - - pub fn align(&mut self, tx: &TransmitRingbuffer) { - trace!("ringbuffer align start"); - 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..TransmitRingbuffer::DATA_LEN { - tx.write_slice(&empty_pattern, i); - } - - let rx_offset = 'outer: loop { - for rx_idx in 0..ReceiveRingbuffer::DATA_LEN { - let mut data_buf = [[0u8; 8]; 1]; - self.rx.read_slice(&mut data_buf, rx_idx); - let value = u64::from_le_bytes(data_buf[0]); - - if value == ALIGNMENT_ANNOUNCE || value == ALIGNMENT_ACKNOWLEDGE { - trace!("ringbuffer align saw marker at rx_idx {}", rx_idx); - break 'outer rx_idx; - } - } - }; - - let ack_pattern = [ALIGNMENT_ACKNOWLEDGE.to_le_bytes()]; - tx.write_slice(&ack_pattern, 0); - - loop { - let mut data_buf = [[0u8; 8]; 1]; - self.rx.read_slice(&mut data_buf, rx_offset); - let value = u64::from_le_bytes(data_buf[0]); - if value == ALIGNMENT_ACKNOWLEDGE { - break; - } - } - - self.rx_alignment_offset = Some(rx_offset); - self.tx_reference = tx.0 as *const _ as usize; - trace!( - "ringbuffer align done offset {} tx_ref 0x{:X}", - rx_offset, - self.tx_reference - ); - } - - pub fn is_aligned(&self) -> bool { - self.rx_alignment_offset.is_some() - } - - pub fn verify_aligned_to(&self, tx: &TransmitRingbuffer) -> bool { - self.is_aligned() && self.tx_reference == (tx.0 as *const _ as usize) - } - - pub fn get_alignment_reference(&self) -> usize { - self.tx_reference - } - - pub fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { - assert!(dst.len() + offset <= ReceiveRingbuffer::DATA_LEN); - let rx_offset = self - .rx_alignment_offset - .expect("Alignment offset not discovered yet. Call align() first."); - let mut aligned_offset = offset + rx_offset; - if aligned_offset >= ReceiveRingbuffer::DATA_LEN { - aligned_offset -= ReceiveRingbuffer::DATA_LEN; - } - read_slice_with_wrap(&self.rx, dst, aligned_offset) - } -} - -fn read_slice_with_wrap(rx: &ReceiveRingbuffer, dst: &mut [[u8; 8]], offset: usize) { - assert!(dst.len() <= ReceiveRingbuffer::DATA_LEN); - if dst.len() + offset <= ReceiveRingbuffer::DATA_LEN { - rx.read_slice(dst, offset); - } else { - let first_part_len = ReceiveRingbuffer::DATA_LEN - offset; - let (first, second) = dst.split_at_mut(first_part_len); - rx.read_slice(first, offset); - rx.read_slice(second, 0); - } -} - -fn is_valid(buffer: &[u8]) -> bool { - if buffer.len() < PACKET_HEADER_SIZE { - return false; - } - let stored_crc = u32::from_le_bytes([buffer[0], buffer[1], buffer[2], buffer[3]]); - let calculated_crc = CRC.checksum(&buffer[4..]); - stored_crc == calculated_crc -} - -pub struct RingbufferDevice { - rx_buffer: AlignedReceiveBuffer, - tx_buffer: TransmitRingbuffer, - mtu: usize, - last_rx_seq: u16, - tx_seq_num: u16, -} - -impl RingbufferDevice { - pub fn new(rx_buffer: AlignedReceiveBuffer, tx_buffer: TransmitRingbuffer) -> Self { - let rx_bytes = ReceiveRingbuffer::DATA_LEN * 8; - let tx_bytes = TransmitRingbuffer::DATA_LEN * 8; - let mtu = rx_bytes.min(tx_bytes) - PACKET_HEADER_SIZE; - - if rx_buffer.is_aligned() { - assert!( - rx_buffer.verify_aligned_to(&tx_buffer), - "RX buffer is aligned but not to the provided TX buffer, expected reference {:p}, got 0x{:X}", - &tx_buffer.0, - rx_buffer.get_alignment_reference(), - ); - } - - Self { - rx_buffer, - tx_buffer, - mtu, - last_rx_seq: u16::MAX, - tx_seq_num: 0, - } - } - - pub fn mtu(&self) -> usize { - self.mtu - } -} - -impl Device for RingbufferDevice { - type RxToken<'a> = RxToken; - type TxToken<'a> = TxToken<'a>; - - fn capabilities(&self) -> DeviceCapabilities { - let mut cap = DeviceCapabilities::default(); - cap.max_transmission_unit = self.mtu; - cap.medium = Medium::Ip; - cap - } - - fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { - let mut header_buf: [[u8; 8]; 1] = [[0u8; 8]; 1]; - self.rx_buffer.read_slice(&mut header_buf, 0); - - let header_ptr = header_buf[0].as_ptr(); - let seq_num = unsafe { (header_ptr.add(4) as *const u16).read_unaligned() }; - let packet_len = unsafe { (header_ptr.add(6) as *const u16).read_unaligned() } as usize; - - trace!( - "ringbuffer rx header seq {} len {} last {}", - seq_num, - packet_len, - self.last_rx_seq - ); - - if seq_num == self.last_rx_seq { - trace!("ringbuffer rx repeated seq {}", seq_num); - return None; - } - - if packet_len < MIN_IP_PACKET_SIZE || packet_len > self.mtu { - trace!( - "ringbuffer rx invalid len {} (must be {}-{}) seq {}", - packet_len, - MIN_IP_PACKET_SIZE, - self.mtu, - seq_num - ); - return None; - } - - let total_len = PACKET_HEADER_SIZE + packet_len; - let num_words = total_len.div_ceil(8); - let mut packet_buffer = [0u8; ReceiveRingbuffer::DATA_LEN * 8]; - - let word_slice = unsafe { - core::slice::from_raw_parts_mut(packet_buffer.as_mut_ptr() as *mut [u8; 8], num_words) - }; - - self.rx_buffer.read_slice(word_slice, 0); - - if !is_valid(&packet_buffer[..total_len]) { - trace!("ringbuffer rx crc fail seq {}", seq_num); - return None; - } - - trace!( - "ringbuffer rx valid seq {}, payload {} bytes", - seq_num, - packet_len - ); - self.last_rx_seq = seq_num; - - let mut payload = [0u8; ReceiveRingbuffer::DATA_LEN * 8]; - payload[..packet_len] - .copy_from_slice(&packet_buffer[PACKET_HEADER_SIZE..PACKET_HEADER_SIZE + packet_len]); - - let rx = RxToken { - buffer: payload, - length: packet_len, - }; - let tx = TxToken { - tx_buffer: &mut self.tx_buffer, - mtu: self.mtu, - seq_num: &mut self.tx_seq_num, - }; - Some((rx, tx)) - } - - fn transmit(&mut self, _timestamp: Instant) -> Option> { - trace!("ringbuffer tx token seq {}", self.tx_seq_num); - Some(TxToken { - tx_buffer: &mut self.tx_buffer, - mtu: self.mtu, - seq_num: &mut self.tx_seq_num, - }) - } -} - -pub struct RxToken { - buffer: [u8; ReceiveRingbuffer::DATA_LEN * 8], - length: usize, -} - -impl phy::RxToken for RxToken { - fn consume(self, f: F) -> R - where - F: FnOnce(&[u8]) -> R, - { - trace!("Consuming validated packet ({} bytes)", self.length); - f(&self.buffer[..self.length]) - } -} - -pub struct TxToken<'a> { - tx_buffer: &'a mut TransmitRingbuffer, - mtu: usize, - seq_num: &'a mut u16, -} - -impl phy::TxToken for TxToken<'_> { - fn consume(self, len: usize, f: F) -> R - where - F: FnOnce(&mut [u8]) -> R, - { - assert!( - len <= self.mtu, - "Packet length {} exceeds MTU {}", - len, - self.mtu - ); - - let mut buffer = [0u8; TransmitRingbuffer::DATA_LEN * 8]; - let header_ptr = buffer.as_mut_ptr(); - unsafe { - (header_ptr.add(4) as *mut u16).write_unaligned(self.seq_num.to_le()); - (header_ptr.add(6) as *mut u16).write_unaligned((len as u16).to_le()); - } - - let result = f(&mut buffer[PACKET_HEADER_SIZE..PACKET_HEADER_SIZE + len]); - - let total_len = PACKET_HEADER_SIZE + len; - let crc = CRC.checksum(&buffer[4..total_len]); - unsafe { - (header_ptr as *mut u32).write_unaligned(crc.to_le()); - } - - let num_words = total_len.div_ceil(8); - let word_slice = - unsafe { core::slice::from_raw_parts(buffer.as_ptr() as *const [u8; 8], num_words) }; - - self.tx_buffer.write_slice(word_slice, 0); - - trace!( - "Transmitted packet: checksum {}, seq {}, total length {}", - crc, - self.seq_num, - total_len, - ); - *self.seq_num = self.seq_num.wrapping_add(1); - - result - } -} From 85991a410b751104ef77eb9bb69d447defb83cf5 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Wed, 11 Mar 2026 17:24:36 +0100 Subject: [PATCH 49/70] Add exception handler --- firmware-binaries/Cargo.lock | 1 + .../demos/soft-ugn-demo-mu-2/Cargo.toml | 1 + .../demos/soft-ugn-demo-mu-2/src/main.rs | 15 +++++++++++++++ 3 files changed, 17 insertions(+) diff --git a/firmware-binaries/Cargo.lock b/firmware-binaries/Cargo.lock index 72501e3bc..645978f1d 100644 --- a/firmware-binaries/Cargo.lock +++ b/firmware-binaries/Cargo.lock @@ -758,6 +758,7 @@ dependencies = [ "bittide-sys", "log", "memmap-generate", + "riscv 0.10.1", "riscv-rt 0.11.0", "smoltcp 0.12.0", "ufmt", diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/Cargo.toml b/firmware-binaries/demos/soft-ugn-demo-mu-2/Cargo.toml index 9d81ac517..d77352a34 100644 --- a/firmware-binaries/demos/soft-ugn-demo-mu-2/Cargo.toml +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/Cargo.toml @@ -10,6 +10,7 @@ license = "Apache-2.0" authors = ["Google LLC"] [dependencies] +riscv = "^0.10" riscv-rt = "0.11.0" bittide-sys = { path = "../../../firmware-support/bittide-sys" } bittide-hal = { path = "../../../firmware-support/bittide-hal" } diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs index bb1e845ba..2b2181189 100644 --- a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs @@ -18,6 +18,7 @@ use bittide_sys::smoltcp::ringbuffer::RingbufferDevice; use bittide_sys::stability_detector::Stability; use core::fmt::Write; use log::{info, trace, warn, LevelFilter}; +use riscv::register::{mcause, mepc, mtval}; use smoltcp::iface::{Config, Interface, SocketHandle, SocketSet, SocketStorage}; use smoltcp::socket::tcp; use smoltcp::wire::{HardwareAddress, IpAddress, IpCidr}; @@ -338,6 +339,20 @@ fn panic_handler(info: &core::panic::PanicInfo) -> ! { } } +#[export_name = "ExceptionHandler"] +fn exception_handler(_trap_frame: &riscv_rt::TrapFrame) -> ! { + let mut uart = INSTANCES.uart; + riscv::interrupt::free(|| { + uwriteln!(uart, "... caught an exception. Looping forever now.\n").unwrap(); + info!("mcause: {:?}\n", mcause::read()); + info!("mepc: {:?}\n", mepc::read()); + info!("mtval: {:?}\n", mtval::read()); + }); + loop { + continue; + } +} + fn build_report_for_link( link: usize, capture_ugn: &bittide_hal::shared_devices::CaptureUgn, From 2cf0375ae197d77e96252fe983e8d494e0e0998b Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Wed, 11 Mar 2026 17:34:45 +0100 Subject: [PATCH 50/70] bound MTU to 1500 --- firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs index 5cc144500..93e9610d6 100644 --- a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs +++ b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs @@ -96,7 +96,7 @@ where pub fn new(rx_buffer: AlignedReceiveBuffer, tx_buffer: Tx) -> Self { // Calculate MTU from buffer sizes (each word is 8 bytes) // Reserve space for packet header (CRC is part of header) - let mtu = (Rx::DATA_LEN * 8).min(Tx::DATA_LEN * 8) - PACKET_HEADER_SIZE; + let mtu = (Rx::DATA_LEN * 8).min(1500).min(Tx::DATA_LEN * 8) - PACKET_HEADER_SIZE; assert!(rx_buffer.is_aligned(), "RX buffer is not aligned "); Self { From 2b986f0d6505b7857eb8fe0f4407bc71c1dac03c Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Wed, 11 Mar 2026 17:53:16 +0100 Subject: [PATCH 51/70] Print alignment info --- firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs index 2b2181189..8d1870b5f 100644 --- a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs @@ -64,6 +64,10 @@ fn make_device( ) -> RingbufferDevice { let mut rx_aligned = AlignedReceiveBuffer::new(rx); rx_aligned.align(&tx); + info!( + "Aligned RX buffer with offset {}", + rx_aligned.get_alignment_offset().unwrap() + ); RingbufferDevice::new(rx_aligned, tx) } From 356b5fbfab802f05f807403b4e438c7eda0aedc3 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Wed, 11 Mar 2026 17:53:58 +0100 Subject: [PATCH 52/70] Trace later --- firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs index 8d1870b5f..0a9234cce 100644 --- a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs @@ -81,7 +81,7 @@ fn main() -> ! { logger.set_timer(INSTANCES.timer); logger.display_source = LevelFilter::Warn; log::set_logger_racy(logger).ok(); - log::set_max_level_racy(LevelFilter::Trace); + log::set_max_level_racy(LevelFilter::Info); } info!("=== Soft UGN Demo MU2 ==="); @@ -213,6 +213,9 @@ fn main() -> ! { "Role: {}", if is_manager { "manager" } else { "subordinate" } ); + unsafe { + log::set_max_level_racy(LevelFilter::Trace); + } if is_manager { info!("Starting manager state machines..."); From e4e67a156a52a881a34935fd00591955970d76a4 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Wed, 11 Mar 2026 17:56:01 +0100 Subject: [PATCH 53/70] Add smoltcp::ringbuffer logging --- .../bittide-sys/src/smoltcp/ringbuffer.rs | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs index 93e9610d6..f1ab32eed 100644 --- a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs +++ b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs @@ -33,7 +33,7 @@ use bittide_hal::manual_additions::ringbuffer::{ }; use crc::{Crc, CRC_32_ISCSI}; -use log::{trace, warn}; +use log::{debug, trace, warn}; use smoltcp::phy::{self, Device, DeviceCapabilities, Medium}; use smoltcp::time::Instant; @@ -98,6 +98,12 @@ where // Reserve space for packet header (CRC is part of header) let mtu = (Rx::DATA_LEN * 8).min(1500).min(Tx::DATA_LEN * 8) - PACKET_HEADER_SIZE; assert!(rx_buffer.is_aligned(), "RX buffer is not aligned "); + debug!( + "ringbuffer device init rx_words {} tx_words {} mtu {}", + Rx::DATA_LEN, + Tx::DATA_LEN, + mtu + ); Self { rx_buffer, @@ -153,7 +159,7 @@ where // Check if this is the same packet we saw before (based on sequence number) if seq_num == self.last_rx_seq { - // trace!("Detected repeated packet with seq {}", seq_num); + trace!("Detected repeated packet with seq {}", seq_num); return None; } @@ -169,6 +175,13 @@ where // Calculate total packet size: header + payload let total_len = PACKET_HEADER_SIZE + packet_len; let num_words = total_len.div_ceil(8); + trace!( + "rx packet seq {} len {} total {} words {}", + seq_num, + packet_len, + total_len, + num_words + ); // Read remaining words if any (we already read the first word) if num_words > 1 { @@ -208,6 +221,7 @@ where buffer: payload, length: packet_len, }; + trace!("rx token ready len {}", packet_len); let tx = TxToken { tx_buffer: &mut self.tx_buffer, mtu: self.mtu, @@ -268,6 +282,7 @@ where len, self.mtu ); + trace!("tx consume len {} mtu {}", len, self.mtu); // Prepare aligned buffer: header + payload let mut buffer = Aligned::new([[0u8; 8]; TX_WORDS]); @@ -296,6 +311,12 @@ where // Calculate number of words needed let num_words = total_len.div_ceil(8); + trace!( + "tx packet len {} total {} words {}", + len, + total_len, + num_words + ); // Write to ringbuffer starting at offset 0 self.tx_buffer.write_slice(&buffer.get()[..num_words], 0); From 3c4b59947b66a3754f29a1c174a1a925b41b7e2a Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Wed, 11 Mar 2026 17:57:10 +0100 Subject: [PATCH 54/70] Instrument ringbuffer implementations with more logging --- .../src/manual_additions/ringbuffer.rs | 49 ++++++++++++++++++- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/firmware-support/bittide-hal/src/manual_additions/ringbuffer.rs b/firmware-support/bittide-hal/src/manual_additions/ringbuffer.rs index 7103e37ff..d0ef7f238 100644 --- a/firmware-support/bittide-hal/src/manual_additions/ringbuffer.rs +++ b/firmware-support/bittide-hal/src/manual_additions/ringbuffer.rs @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -use log::warn; +use log::{debug, trace, warn}; const ALIGNMENT_ANNOUNCE: u64 = 0xBADC0FFEE; const ALIGNMENT_ACKNOWLEDGE: u64 = 0xDEADABBA; @@ -16,6 +16,11 @@ pub trait TransmitRingbufferInterface { /// Write a slice to the transmit buffer at the given offset. The slice must not exceed the buffer length when combined with the offset. fn write_slice(&self, src: &[[u8; 8]], offset: usize) { assert!(src.len() + offset <= Self::DATA_LEN); + trace!( + "ringbuffer tx write_slice len {} offset {}", + src.len(), + offset + ); unsafe { self.write_slice_unchecked(src, offset); } @@ -62,12 +67,19 @@ pub trait TransmitRingbufferInterface { } else { let first_part_len = Self::DATA_LEN - offset; let (first, second) = src.split_at(first_part_len); + debug!( + "ringbuffer tx write_slice_with_wrap offset {} first {} second {}", + offset, + first.len(), + second.len() + ); self.write_slice(first, offset); self.write_slice(second, 0); } } fn clear(&self) { + debug!("ringbuffer tx clear len {}", Self::DATA_LEN); let zero = [[0u8; 8]; 1]; for i in 0..Self::DATA_LEN { self.write_slice(&zero, i); @@ -82,6 +94,11 @@ pub trait ReceiveRingbufferInterface { fn read_slice(&self, dst: &mut [[u8; 8]], offset: usize) { assert!(dst.len() + offset <= Self::DATA_LEN); + trace!( + "ringbuffer rx read_slice len {} offset {}", + dst.len(), + offset + ); unsafe { self.read_slice_unchecked(dst, offset); } @@ -125,6 +142,12 @@ pub trait ReceiveRingbufferInterface { } else { let first_part_len = Self::DATA_LEN - offset; let (first, second) = dst.split_at_mut(first_part_len); + debug!( + "ringbuffer rx read_slice_with_wrap offset {} first {} second {}", + offset, + first.len(), + second.len() + ); self.read_slice(first, offset); self.read_slice(second, 0); } @@ -189,6 +212,7 @@ where Tx: TransmitRingbufferInterface, { pub fn new(rx: Rx) -> Self { + debug!("ringbuffer aligned receive buffer new"); Self { rx, rx_alignment_offset: None, @@ -206,6 +230,7 @@ where } pub fn align(&mut self, tx: &Tx) { + debug!("ringbuffer align start"); tx.clear(); let announce_pattern = [ALIGNMENT_ANNOUNCE.to_le_bytes()]; tx.write_slice(&announce_pattern, 0); @@ -217,6 +242,7 @@ where let value = u64::from_le_bytes(data_buf[0]); if value == ALIGNMENT_ANNOUNCE || value == ALIGNMENT_ACKNOWLEDGE { + debug!("ringbuffer align marker at rx_idx {}", rx_idx); break 'outer rx_idx; } } @@ -231,20 +257,33 @@ where let value = u64::from_le_bytes(data_buf[0]); if value == ALIGNMENT_ACKNOWLEDGE { + debug!("ringbuffer align ack at rx_idx {}", rx_offset); break; } } self.rx_alignment_offset = Some(rx_offset); self.tx_reference = tx.base_ptr() as *const _ as usize; + debug!("ringbuffer align complete offset {}", rx_offset); } pub fn clear_alignment(&mut self) { + debug!("ringbuffer clear alignment"); self.rx_alignment_offset = None; self.tx_reference = 0; } pub fn verify_aligned_to(&self, tx: &Tx) -> bool { - self.is_aligned() && self.tx_reference == (tx.base_ptr() as *const _ as usize) + let tx_addr = tx.base_ptr() as *const _ as usize; + let aligned = self.is_aligned() && self.tx_reference == tx_addr; + if !aligned { + warn!( + "ringbuffer verify aligned failed: aligned {} tx_ref 0x{:x} tx_addr 0x{:x}", + self.is_aligned(), + self.tx_reference, + tx_addr + ); + } + aligned } pub fn get_alignment_reference(&self) -> usize { @@ -260,6 +299,12 @@ where if aligned_offset >= Rx::DATA_LEN { aligned_offset -= Rx::DATA_LEN; } + trace!( + "ringbuffer aligned read len {} offset {} aligned_offset {}", + dst.len(), + offset, + aligned_offset + ); self.rx.read_slice_with_wrap(dst, aligned_offset) } } From 945bd3c04bcebce73d99d7e8f44ca6bd9c5e052f Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Wed, 11 Mar 2026 18:11:37 +0100 Subject: [PATCH 55/70] more device logging --- .../bittide-sys/src/smoltcp/ringbuffer.rs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs index f1ab32eed..a1055fbc0 100644 --- a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs +++ b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs @@ -145,13 +145,16 @@ where fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { // Allocate aligned buffer for reading from ringbuffer + trace!("Creating packet_buffer"); let mut packet_buffer = Aligned::new([[0u8; 8]; Rx::DATA_LEN]); + trace!("Reading packet header from RX buffer"); // Read first word containing the header: CRC32 (4 bytes) + sequence (2 bytes) + length (2 bytes) self.rx_buffer .read_slice(&mut packet_buffer.get_mut()[0..1], 0); // Extract CRC, sequence number, and length from header using direct pointer reads + trace!("Extracting header fields"); let header_ptr = packet_buffer.get()[0].as_ptr(); let _stored_crc = unsafe { (header_ptr as *const u32).read_unaligned() }; let seq_num = unsafe { (header_ptr.add(4) as *const u16).read_unaligned() }; @@ -185,11 +188,13 @@ where // Read remaining words if any (we already read the first word) if num_words > 1 { + trace!("Reading packet payload from RX buffer"); self.rx_buffer .read_slice(&mut packet_buffer.get_mut()[1..num_words], 1); } // Flatten to bytes for CRC validation + trace!("Flattening packet buffer to byte slice for CRC validation"); let packet_bytes = unsafe { core::slice::from_raw_parts(packet_buffer.get().as_ptr() as *const u8, total_len) }; @@ -208,6 +213,7 @@ where self.last_rx_seq = seq_num; // Extract payload (skip header) + trace!("Creating slice of payload bytes"); let mut payload = Aligned::new([0u8; Rx::DATA_LEN * 8]); let payload_bytes = unsafe { core::slice::from_raw_parts( @@ -215,6 +221,8 @@ where packet_len, ) }; + + trace!("Copying payload to local buffer"); payload.get_mut()[..packet_len].copy_from_slice(payload_bytes); let rx = RxToken { @@ -289,6 +297,7 @@ where // Write header fields using direct pointer writes // Header format: CRC32 (4 bytes) + sequence (2 bytes) + length (2 bytes) + trace!("Writing header fields to buffer"); let header_ptr = buffer.get_mut().as_mut_ptr() as *mut u8; unsafe { // Write sequence and length (CRC written later after payload) @@ -297,14 +306,19 @@ where } // Let smoltcp fill the packet data + trace!("Creating payload slice for smoltcp"); let payload_slice = unsafe { core::slice::from_raw_parts_mut(header_ptr.add(PACKET_HEADER_SIZE), len) }; + trace!("Filling payload using provided closure"); let result = f(payload_slice); // Calculate total length and seal packet with CRC in header + trace!("Calculating CRC for packet"); let total_len = PACKET_HEADER_SIZE + len; let crc_data = unsafe { core::slice::from_raw_parts(header_ptr.add(4), total_len - 4) }; let crc = CRC.checksum(crc_data); + + trace!("Writing CRC to header"); unsafe { (header_ptr as *mut u32).write_unaligned(crc.to_le()); } From 0c5d1e2c4ca3c60680b88fee71ceedca5235c7cc Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Thu, 12 Mar 2026 08:04:41 +0100 Subject: [PATCH 56/70] More CSR options --- bittide-cpus/data/Riscv32imc0.scala | 2 +- bittide-cpus/data/Riscv32imc1.scala | 2 +- bittide-cpus/data/Riscv32imc2.scala | 2 +- bittide-cpus/data/Riscv32imc3.scala | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bittide-cpus/data/Riscv32imc0.scala b/bittide-cpus/data/Riscv32imc0.scala index 846ed48b4..87337c303 100644 --- a/bittide-cpus/data/Riscv32imc0.scala +++ b/bittide-cpus/data/Riscv32imc0.scala @@ -44,7 +44,7 @@ object Riscv32imc0 extends App { ), new CsrPlugin( - CsrPluginConfig.smallest.copy( + CsrPluginConfig.all.copy( ebreakGen = true, mtvecAccess = CsrAccess.READ_WRITE, withPrivilegedDebug = true diff --git a/bittide-cpus/data/Riscv32imc1.scala b/bittide-cpus/data/Riscv32imc1.scala index 20548229e..bd949067e 100644 --- a/bittide-cpus/data/Riscv32imc1.scala +++ b/bittide-cpus/data/Riscv32imc1.scala @@ -44,7 +44,7 @@ object Riscv32imc1 extends App { ), new CsrPlugin( - CsrPluginConfig.smallest.copy( + CsrPluginConfig.all.copy( ebreakGen = true, mtvecAccess = CsrAccess.READ_WRITE, withPrivilegedDebug = true diff --git a/bittide-cpus/data/Riscv32imc2.scala b/bittide-cpus/data/Riscv32imc2.scala index 3e57f1238..38f1d8597 100644 --- a/bittide-cpus/data/Riscv32imc2.scala +++ b/bittide-cpus/data/Riscv32imc2.scala @@ -44,7 +44,7 @@ object Riscv32imc2 extends App { ), new CsrPlugin( - CsrPluginConfig.smallest.copy( + CsrPluginConfig.all.copy( ebreakGen = true, mtvecAccess = CsrAccess.READ_WRITE, withPrivilegedDebug = true diff --git a/bittide-cpus/data/Riscv32imc3.scala b/bittide-cpus/data/Riscv32imc3.scala index 90628fdce..420d626f5 100644 --- a/bittide-cpus/data/Riscv32imc3.scala +++ b/bittide-cpus/data/Riscv32imc3.scala @@ -44,7 +44,7 @@ object Riscv32imc3 extends App { ), new CsrPlugin( - CsrPluginConfig.smallest.copy( + CsrPluginConfig.all.copy( ebreakGen = true, mtvecAccess = CsrAccess.READ_WRITE, withPrivilegedDebug = true From 34c3807ddd1a70a42ad5824e6221f9be5132cd9c Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Thu, 12 Mar 2026 09:00:02 +0100 Subject: [PATCH 57/70] Increase management unit memory sizes --- .../src/Bittide/Instances/Hitl/SoftUgnDemo/Core.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Core.hs b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Core.hs index d2cfcf38f..c4b4d3144 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Core.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Core.hs @@ -75,8 +75,8 @@ muConfig :: muConfig = PeConfig { cpu = Riscv32imc.vexRiscv1 - , depthI = SNat @(Div (180 * 1024) 4) - , depthD = SNat @(Div (64 * 1024) 4) + , depthI = SNat @(Div (320 * 1024) 4) + , depthD = SNat @(Div (80 * 1024) 4) , initI = Nothing , initD = Nothing , iBusTimeout = d0 From d412635b785b208b44dc60eff555dc0852575514 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Thu, 12 Mar 2026 09:05:09 +0100 Subject: [PATCH 58/70] Add `CargoBuildType` argument to `initGdb --- .../Instances/Hitl/Driver/Si539xConfiguration.hs | 2 +- .../Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs | 13 +++++++------ .../src/Bittide/Instances/Hitl/SwitchDemo/Driver.hs | 11 ++++++----- .../Bittide/Instances/Hitl/SwitchDemoGppe/Driver.hs | 8 ++++---- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/bittide-instances/src/Bittide/Instances/Hitl/Driver/Si539xConfiguration.hs b/bittide-instances/src/Bittide/Instances/Hitl/Driver/Si539xConfiguration.hs index f30bfddfe..9f9ba2b57 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/Driver/Si539xConfiguration.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/Driver/Si539xConfiguration.hs @@ -68,7 +68,7 @@ driverFunc _name targets = do <> show (L.length <$> allTapInfos) Gdb.withGdbs (L.length targets) $ \gdbs -> do - liftIO $ zipWithConcurrently3_ (initGdb hitlDir "clock-board") gdbs peTapInfos targets + liftIO $ zipWithConcurrently3_ (initGdb hitlDir "clock-board" Release) gdbs peTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) gdbs let picocomStarts = liftIO <$> L.zipWith (initPicocom hitlDir) targets [0 ..] diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs index 1558e4884..f69472f0e 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs @@ -71,7 +71,7 @@ driver testName targets = do Gdb.withGdbs (L.length targets) $ \bootGdbs -> do liftIO - $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo1-boot") bootGdbs bootTapInfos targets + $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo1-boot" Release) bootGdbs bootTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) bootGdbs liftIO $ mapConcurrently_ Gdb.continue bootGdbs liftIO @@ -102,11 +102,11 @@ driver testName targets = do <> show (L.length <$> allTapInfos) Gdb.withGdbs (L.length targets) $ \ccGdbs -> do - liftIO $ zipWithConcurrently3_ (initGdb hitlDir "clock-control") ccGdbs ccTapInfos targets + liftIO $ zipWithConcurrently3_ (initGdb hitlDir "clock-control" Release) ccGdbs ccTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) ccGdbs Gdb.withGdbs (L.length targets) $ \muGdbs -> do - liftIO $ zipWithConcurrently3_ (initGdb hitlDir "soft-ugn-mu") muGdbs muTapInfos targets + liftIO $ zipWithConcurrently3_ (initGdb hitlDir "soft-ugn-mu" Release) muGdbs muTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) muGdbs brackets picocomStarts (liftIO . snd) $ \(L.map fst -> picocoms) -> do @@ -245,7 +245,7 @@ driver2 testName targets = do Gdb.withGdbs (L.length targets) $ \bootGdbs -> do liftIO - $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo1-boot") bootGdbs bootTapInfos targets + $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo1-boot" Release) bootGdbs bootTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) bootGdbs liftIO $ mapConcurrently_ Gdb.continue bootGdbs liftIO @@ -276,11 +276,12 @@ driver2 testName targets = do <> show (L.length <$> allTapInfos) Gdb.withGdbs (L.length targets) $ \ccGdbs -> do - liftIO $ zipWithConcurrently3_ (initGdb hitlDir "clock-control") ccGdbs ccTapInfos targets + liftIO $ zipWithConcurrently3_ (initGdb hitlDir "clock-control" Release) ccGdbs ccTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) ccGdbs Gdb.withGdbs (L.length targets) $ \muGdbs -> do - liftIO $ zipWithConcurrently3_ (initGdb hitlDir "soft-ugn-demo-mu-2") muGdbs muTapInfos targets + liftIO + $ zipWithConcurrently3_ (initGdb hitlDir "soft-ugn-demo-mu-2" Release) muGdbs muTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) muGdbs brackets picocomStarts (liftIO . snd) $ \(L.map fst -> picocoms) -> do diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemo/Driver.hs b/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemo/Driver.hs index 3f4c19e22..3b522d6c5 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemo/Driver.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemo/Driver.hs @@ -115,15 +115,16 @@ dumpCcSamples hitlDir ccConf ccGdbs = do initGdb :: FilePath -> String -> + CargoBuildType -> Gdb -> Ocd.TapInfo -> (HwTarget, DeviceInfo) -> IO () -initGdb hitlDir binName gdb tapInfo (hwT, _d) = do +initGdb hitlDir binName buildType gdb tapInfo (hwT, _d) = do Gdb.setLogging gdb $ hitlDir "gdb-" <> binName <> "-" <> show (getTargetIndex hwT) <> ".log" - Gdb.setFile gdb $ firmwareBinariesDir "riscv32imc" Release binName + Gdb.setFile gdb $ firmwareBinariesDir "riscv32imc" buildType binName Gdb.setTarget gdb tapInfo.gdbPort Gdb.setTimeout gdb Nothing Gdb.runCommand gdb "echo connected to target device" @@ -376,7 +377,7 @@ driver testName targets = do Gdb.withGdbs (L.length targets) $ \bootGdbs -> do liftIO - $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo1-boot") bootGdbs bootTapInfos targets + $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo1-boot" Release) bootGdbs bootTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) bootGdbs liftIO $ mapConcurrently_ Gdb.continue bootGdbs liftIO @@ -407,12 +408,12 @@ driver testName targets = do <> show (L.length <$> allTapInfos) Gdb.withGdbs (L.length targets) $ \ccGdbs -> do - liftIO $ zipWithConcurrently3_ (initGdb hitlDir "clock-control") ccGdbs ccTapInfos targets + liftIO $ zipWithConcurrently3_ (initGdb hitlDir "clock-control" Release) ccGdbs ccTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) ccGdbs Gdb.withGdbs (L.length targets) $ \muGdbs -> do liftIO - $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo1-mu") muGdbs muTapInfos targets + $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo1-mu" Release) muGdbs muTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) muGdbs let goDumpCcSamples = dumpCcSamples hitlDir (defCcConf (natToNum @FpgaCount)) ccGdbs diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemoGppe/Driver.hs b/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemoGppe/Driver.hs index 360f069b0..595eda52e 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemoGppe/Driver.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemoGppe/Driver.hs @@ -70,7 +70,7 @@ driver testName targets = do Gdb.withGdbs (L.length targets) $ \bootGdbs -> do liftIO - $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo1-boot") bootGdbs bootTapInfos targets + $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo1-boot" Release) bootGdbs bootTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) bootGdbs liftIO $ mapConcurrently_ Gdb.continue bootGdbs liftIO @@ -101,17 +101,17 @@ driver testName targets = do <> show (L.length <$> allTapInfos) Gdb.withGdbs (L.length targets) $ \ccGdbs -> do - liftIO $ zipWithConcurrently3_ (initGdb hitlDir "clock-control") ccGdbs ccTapInfos targets + liftIO $ zipWithConcurrently3_ (initGdb hitlDir "clock-control" Release) ccGdbs ccTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) ccGdbs Gdb.withGdbs (L.length targets) $ \muGdbs -> do liftIO - $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo2-mu") muGdbs muTapInfos targets + $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo2-mu" Release) muGdbs muTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) muGdbs Gdb.withGdbs (L.length targets) $ \gppeGdbs -> do liftIO - $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo2-gppe") gppeGdbs gppeTapInfos targets + $ zipWithConcurrently3_ (initGdb hitlDir "switch-demo2-gppe" Release) gppeGdbs gppeTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) gppeGdbs brackets picocomStarts (liftIO . snd) $ \(L.map fst -> picocoms) -> do From 37c7a7cf758c254bf5e1d32dea892f6f16356ed4 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Thu, 12 Mar 2026 09:16:11 +0100 Subject: [PATCH 59/70] Add breakpoints to management unit --- .../src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs index f69472f0e..9f7433603 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs @@ -286,6 +286,15 @@ driver2 testName targets = do brackets picocomStarts (liftIO . snd) $ \(L.map fst -> picocoms) -> do let goDumpCcSamples = dumpCcSamples hitlDir (defCcConf (natToNum @FpgaCount)) ccGdbs + + _ <- liftIO $ do + mapConcurrently + ( \gdb -> do + Gdb.setBreakpoints gdb ["_start_trap_rust"] + Gdb.setBreakpointHook gdb + ) + muGdbs + liftIO $ mapConcurrently_ Gdb.continue ccGdbs liftIO $ mapConcurrently_ Gdb.continue muGdbs From d5bb42a151ed964dfd69ab395e1c424bf15956c1 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Thu, 12 Mar 2026 13:29:39 +0100 Subject: [PATCH 60/70] Export `start` and `stop` from `Gdb --- gdb-hs/src/Gdb.hs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/gdb-hs/src/Gdb.hs b/gdb-hs/src/Gdb.hs index ad886759d..3002cda6a 100644 --- a/gdb-hs/src/Gdb.hs +++ b/gdb-hs/src/Gdb.hs @@ -4,6 +4,8 @@ module Gdb ( -- * GDB process management Gdb, + start, + stop, withGdb, withGdbs, From 91bf1b056c6866d7d8cff485db8bae85ce601511 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Thu, 12 Mar 2026 13:29:57 +0100 Subject: [PATCH 61/70] Get CPU state when stopping GDB --- .../Instances/Hitl/SoftUgnDemo/Driver.hs | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs index 9f7433603..3ff6b02a8 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs @@ -275,11 +275,25 @@ driver2 testName targets = do <> ", but got: " <> show (L.length <$> allTapInfos) - Gdb.withGdbs (L.length targets) $ \ccGdbs -> do + gdbStarts = liftIO <$> fmap (const Gdb.start) targets + gdbCleanupAction gdb = do + putStrLn "Retrieving final CPU state" + Gdb.interruptCommand gdb + Gdb.runCommand gdb + . unlines + $ [ "printf \"Final CPU state\\n\"" + , "i r" + , "bt" + ] + Gdb.stop gdb + + brackets gdbStarts (liftIO . gdbCleanupAction) $ \ccGdbs -> do + -- Gdb.withGdbs (L.length targets) $ \ccGdbs -> do liftIO $ zipWithConcurrently3_ (initGdb hitlDir "clock-control" Release) ccGdbs ccTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) ccGdbs - Gdb.withGdbs (L.length targets) $ \muGdbs -> do + brackets gdbStarts (liftIO . gdbCleanupAction) $ \muGdbs -> do + -- Gdb.withGdbs (L.length targets) $ \muGdbs -> do liftIO $ zipWithConcurrently3_ (initGdb hitlDir "soft-ugn-demo-mu-2" Release) muGdbs muTapInfos targets liftIO $ mapConcurrently_ ((assertEither =<<) . Gdb.loadBinary) muGdbs From a912dd6ebb55c8c8033a11912ab538ce8ac20a52 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Mon, 16 Mar 2026 07:52:19 +0100 Subject: [PATCH 62/70] fixup! Use Aligned and fix some things --- .../bittide-sys/src/smoltcp/ringbuffer.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs index a1055fbc0..44d19d07e 100644 --- a/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs +++ b/firmware-support/bittide-sys/src/smoltcp/ringbuffer.rs @@ -27,7 +27,7 @@ //! 3. Verify the CRC32 over sequence + length + payload //! 4. If CRC validates, extract payload and consume packet //! 5. Track sequence numbers to detect repeated packets -use bittide_hal::manual_additions::addressable_buffer::Aligned; +use bittide_hal::manual_additions::aligned::Aligned4; use bittide_hal::manual_additions::ringbuffer::{ AlignedReceiveBuffer, ReceiveRingbufferInterface, TransmitRingbufferInterface, }; @@ -43,8 +43,7 @@ const PACKET_HEADER_SIZE: usize = 8; /// Minimum IP packet size (20 byte IPv4 header minimum) const MIN_IP_PACKET_SIZE: usize = 20; -/// CRC-32 (Castagnoli) instance for packet integrity checking. -/// Initialized at compile-time for zero-cost runtime usage. +/// CRC-32 (Castagnoli) instance for packet integrity checking.lgienalized at compile-time for zero-cost runtime usage. const CRC: Crc = Crc::::new(&CRC_32_ISCSI); /// Verify packet integrity by checking the CRC32 in the header. @@ -146,7 +145,7 @@ where fn receive(&mut self, _timestamp: Instant) -> Option<(Self::RxToken<'_>, Self::TxToken<'_>)> { // Allocate aligned buffer for reading from ringbuffer trace!("Creating packet_buffer"); - let mut packet_buffer = Aligned::new([[0u8; 8]; Rx::DATA_LEN]); + let mut packet_buffer = Aligned4::new([[0u8; 8]; Rx::DATA_LEN]); trace!("Reading packet header from RX buffer"); // Read first word containing the header: CRC32 (4 bytes) + sequence (2 bytes) + length (2 bytes) @@ -214,7 +213,7 @@ where // Extract payload (skip header) trace!("Creating slice of payload bytes"); - let mut payload = Aligned::new([0u8; Rx::DATA_LEN * 8]); + let mut payload = Aligned4::new([0u8; Rx::DATA_LEN * 8]); let payload_bytes = unsafe { core::slice::from_raw_parts( (packet_buffer.get().as_ptr() as *const u8).add(PACKET_HEADER_SIZE), @@ -252,7 +251,7 @@ where /// Contains a local copy of the packet payload that has been validated /// against CRC32 corruption. pub struct RxToken { - buffer: Aligned<[u8; RX_BYTES]>, + buffer: Aligned4<[u8; RX_BYTES]>, length: usize, } @@ -293,7 +292,7 @@ where trace!("tx consume len {} mtu {}", len, self.mtu); // Prepare aligned buffer: header + payload - let mut buffer = Aligned::new([[0u8; 8]; TX_WORDS]); + let mut buffer = Aligned4::new([[0u8; 8]; TX_WORDS]); // Write header fields using direct pointer writes // Header format: CRC32 (4 bytes) + sequence (2 bytes) + length (2 bytes) From c7a2ad24533a516a263be2fc8297a4176d8e38e8 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Mon, 23 Mar 2026 09:05:19 +0100 Subject: [PATCH 63/70] One socketset per link --- .../demos/soft-ugn-demo-mu-2/src/main.rs | 40 +++++++------------ 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs index 0a9234cce..6a878f96c 100644 --- a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs @@ -32,8 +32,8 @@ const LOG_TICK_EVERY: u32 = 500; const CLIENT_IP: [u8; 4] = [100, 100, 100, 100]; const SERVER_IP: [u8; 4] = [100, 100, 100, 101]; -static mut TCP_RX_BUFS: [u8; TCP_BUF_SIZE] = [0; TCP_BUF_SIZE]; -static mut TCP_TX_BUFS: [u8; TCP_BUF_SIZE] = [0; TCP_BUF_SIZE]; +static mut TCP_RX_BUFS: [[u8; TCP_BUF_SIZE]; LINK_COUNT] = [[0; TCP_BUF_SIZE]; LINK_COUNT]; +static mut TCP_TX_BUFS: [[u8; TCP_BUF_SIZE]; LINK_COUNT] = [[0; TCP_BUF_SIZE]; LINK_COUNT]; #[cfg(not(test))] use riscv_rt::entry; @@ -195,17 +195,17 @@ fn main() -> ! { device }); - let rx_buf = unsafe { &mut TCP_RX_BUFS[..] }; - let tx_buf = unsafe { &mut TCP_TX_BUFS[..] }; - let socket = tcp::Socket::new( - tcp::SocketBuffer::new(rx_buf), - tcp::SocketBuffer::new(tx_buf), - ); - let mut sockets_storage: [SocketStorage<'static>; 1] = Default::default(); - let socket_handle = { - let mut sockets = socket_set(&mut sockets_storage[..]); + let mut sockets_storage: [[SocketStorage<'static>; 1]; LINK_COUNT] = Default::default(); + let socket_handles: [SocketHandle; LINK_COUNT] = core::array::from_fn(|idx: usize| { + let rx_buf = unsafe { &mut TCP_RX_BUFS[idx][..] }; + let tx_buf = unsafe { &mut TCP_TX_BUFS[idx][..] }; + let socket = tcp::Socket::new( + tcp::SocketBuffer::new(rx_buf), + tcp::SocketBuffer::new(tx_buf), + ); + let mut sockets = socket_set(&mut sockets_storage[idx][..]); sockets.add(socket) - }; + }); let dna = INSTANCES.dna.dna(); info!("My dna: {:?}", dna); let is_manager = dna == MANAGER_DNA; @@ -221,6 +221,7 @@ fn main() -> ! { info!("Starting manager state machines..."); let mut reports: [Option; LINK_COUNT] = [None; LINK_COUNT]; for link in 0..LINK_COUNT { + let socket_handle = socket_handles[link]; info!("Starting manager for link {}", link); let now = to_smoltcp_instant(INSTANCES.timer.now()); let mut iface = @@ -231,7 +232,7 @@ fn main() -> ! { trace!("Starting manager loop for link {}", link); loop { let now = to_smoltcp_instant(INSTANCES.timer.now()); - let mut sockets = socket_set(&mut sockets_storage[..]); + let mut sockets = socket_set(&mut sockets_storage[link][..]); trace!("Polling manager link {}", link); manager.poll(now, &mut devices[link], &mut sockets); trace!("manager link {} state {:?}", link, manager.state()); @@ -271,18 +272,7 @@ fn main() -> ! { } } else { info!("Starting subordinate state machines..."); - let mut sockets_storage: [[SocketStorage<'static>; 1]; LINK_COUNT] = - core::array::from_fn(|_| Default::default()); - let socket_handles: [SocketHandle; LINK_COUNT] = core::array::from_fn(|idx| { - let rx_buf = unsafe { &mut TCP_RX_BUFS[..] }; - let tx_buf = unsafe { &mut TCP_TX_BUFS[..] }; - let socket = tcp::Socket::new( - tcp::SocketBuffer::new(rx_buf), - tcp::SocketBuffer::new(tx_buf), - ); - let mut sockets = socket_set(&mut sockets_storage[idx][..]); - sockets.add(socket) - }); + let mut subordinates: [Subordinate; LINK_COUNT] = core::array::from_fn(|idx| { let now = to_smoltcp_instant(INSTANCES.timer.now()); let mut iface = From ecc1f37ba8f72276c53c5d0235befe3aa756a59d Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Mon, 23 Mar 2026 10:42:22 +0100 Subject: [PATCH 64/70] Dont add breakpoints --- .../Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs index 3ff6b02a8..5594ee982 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Driver.hs @@ -301,13 +301,13 @@ driver2 testName targets = do brackets picocomStarts (liftIO . snd) $ \(L.map fst -> picocoms) -> do let goDumpCcSamples = dumpCcSamples hitlDir (defCcConf (natToNum @FpgaCount)) ccGdbs - _ <- liftIO $ do - mapConcurrently - ( \gdb -> do - Gdb.setBreakpoints gdb ["_start_trap_rust"] - Gdb.setBreakpointHook gdb - ) - muGdbs + -- _ <- liftIO $ do + -- mapConcurrently + -- ( \gdb -> do + -- Gdb.setBreakpoints gdb ["_start_trap_rust"] + -- Gdb.setBreakpointHook gdb + -- ) + -- muGdbs liftIO $ mapConcurrently_ Gdb.continue ccGdbs liftIO $ mapConcurrently_ Gdb.continue muGdbs From b65066bd15db07d6b2a7452d15ac00ac95ea5be1 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Mon, 23 Mar 2026 10:50:48 +0100 Subject: [PATCH 65/70] Bitje dr bie --- .../src/Bittide/Instances/Hitl/SoftUgnDemo/BringUp.hs | 2 +- .../src/Bittide/Instances/Hitl/SwitchDemo/BringUp.hs | 2 +- .../src/Bittide/Instances/Hitl/SwitchDemoGppe/BringUp.hs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/BringUp.hs b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/BringUp.hs index 267f68fdc..2dd1f2b3a 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/BringUp.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/BringUp.hs @@ -118,7 +118,7 @@ bringUp refClk refRst = withBittideByteOrder $ circuit $ \(memoryMaps, jtag, gth -- Start UART multiplexing uartTxBytes <- withRefClockResetEnable - $ asciiDebugMux d1024 uartLabels + $ asciiDebugMux (SNat @2048) uartLabels <| Vec.append -< ([bootUartBytes], uartBytes) (_uartInBytes, uartTx) <- withRefClockResetEnable $ uartDf baud -< (uartTxBytes, Fwd 0) diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemo/BringUp.hs b/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemo/BringUp.hs index 4466eb0b9..fb3edf8cd 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemo/BringUp.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemo/BringUp.hs @@ -120,7 +120,7 @@ bringUp refClk refRst = withBittideByteOrder $ circuit $ \(memoryMaps, jtag, gth -- Start UART multiplexing uartTxBytes <- withRefClockResetEnable - $ asciiDebugMux d1024 uartLabels + $ asciiDebugMux (SNat @2048) uartLabels <| Vec.append -< ([bootUartBytes], uartBytes) (_uartInBytes, uartTx) <- withRefClockResetEnable $ uartDf baud -< (uartTxBytes, Fwd 0) diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemoGppe/BringUp.hs b/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemoGppe/BringUp.hs index 971e2de7d..a2ab3d32d 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemoGppe/BringUp.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SwitchDemoGppe/BringUp.hs @@ -119,7 +119,7 @@ bringUp refClk refRst = withBittideByteOrder $ circuit $ \(memoryMaps, jtag, gth -- Start UART multiplexing uartTxBytes <- withRefClockResetEnable - $ asciiDebugMux d1024 uartLabels + $ asciiDebugMux (SNat @2048) uartLabels <| Vec.append -< ([bootUartBytes], uartBytes) (_uartInBytes, uartTx) <- withRefClockResetEnable $ uartDf baud -< (uartTxBytes, Fwd 0) From 98ce73eb55f471ee6687e3ac59c0f1268483e17d Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Mon, 23 Mar 2026 11:56:19 +0100 Subject: [PATCH 66/70] Replace `log` based logging with `uwriteln` --- .../demos/soft-ugn-demo-mu-2/src/main.rs | 112 +++++++----------- 1 file changed, 41 insertions(+), 71 deletions(-) diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs index 6a878f96c..3419413ec 100644 --- a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs @@ -17,7 +17,6 @@ use bittide_sys::net_state::{Manager, Subordinate, UgnEdge, UgnReport}; use bittide_sys::smoltcp::ringbuffer::RingbufferDevice; use bittide_sys::stability_detector::Stability; use core::fmt::Write; -use log::{info, trace, warn, LevelFilter}; use riscv::register::{mcause, mepc, mtval}; use smoltcp::iface::{Config, Interface, SocketHandle, SocketSet, SocketStorage}; use smoltcp::socket::tcp; @@ -28,7 +27,6 @@ const INSTANCES: DeviceInstances = unsafe { DeviceInstances::new() }; const LINK_COUNT: usize = 7; const TCP_BUF_SIZE: usize = 256; const MANAGER_DNA: [u8; 12] = [133, 129, 48, 4, 64, 192, 105, 1, 1, 0, 2, 64]; -const LOG_TICK_EVERY: u32 = 500; const CLIENT_IP: [u8; 4] = [100, 100, 100, 100]; const SERVER_IP: [u8; 4] = [100, 100, 100, 101]; @@ -61,30 +59,24 @@ fn socket_set<'a>(storage: &'a mut [SocketStorage<'static>]) -> SocketSet<'a> { fn make_device( rx: ReceiveRingbuffer, tx: TransmitRingbuffer, + uart: &mut bittide_hal::shared_devices::Uart, ) -> RingbufferDevice { let mut rx_aligned = AlignedReceiveBuffer::new(rx); rx_aligned.align(&tx); - info!( + uwriteln!( + uart, "Aligned RX buffer with offset {}", rx_aligned.get_alignment_offset().unwrap() - ); + ) + .ok(); RingbufferDevice::new(rx_aligned, tx) } #[cfg_attr(not(test), entry)] fn main() -> ! { let mut uart = INSTANCES.uart; - unsafe { - use bittide_sys::uart::log::LOGGER; - let logger = &mut (*LOGGER.get()); - logger.set_logger(uart.clone()); - logger.set_timer(INSTANCES.timer); - logger.display_source = LevelFilter::Warn; - log::set_logger_racy(logger).ok(); - log::set_max_level_racy(LevelFilter::Info); - } - info!("=== Soft UGN Demo MU2 ==="); + uwriteln!(uart, "=== Soft UGN Demo MU2 ===").ok(); let transceivers = &INSTANCES.transceivers; let cc = INSTANCES.clock_control; let elastic_buffers = [ @@ -114,7 +106,7 @@ fn main() -> ! { // 6) Run manager state machines to connect to neighbors and request UGNs. // 7) Collect UGN edges over TCP and aggregate locally. - info!("Bringing up links..."); + uwriteln!(uart, "Bringing up links...").ok(); let mut link_startups = [LinkStartup::new(); LINK_COUNT]; while !link_startups.iter().all(|ls| ls.is_done()) { for (i, link_startup) in link_startups.iter_mut().enumerate() { @@ -127,7 +119,7 @@ fn main() -> ! { } } - info!("Waiting for stability..."); + uwriteln!(uart, "Waiting for stability...").ok(); loop { let stability = Stability { stable: cc.links_stable()[0], @@ -138,7 +130,7 @@ fn main() -> ! { } } - info!("Stopping auto-centering..."); + uwriteln!(uart, "Stopping auto-centering...").ok(); elastic_buffers .iter() .for_each(|eb| eb.set_auto_center_enable(false)); @@ -153,14 +145,16 @@ fn main() -> ! { capture_ugn.set_elastic_buffer_delta(eb_delta); } - info!("Captured hardware UGNs"); + uwriteln!(uart, "Captured hardware UGNs").ok(); for (i, capture_ugn) in capture_ugns.iter().enumerate() { - info!( + uwriteln!( + uart, "Capture UGN {}: local = {}, remote = {}", i, capture_ugn.local_counter(), capture_ugn.remote_counter() - ); + ) + .ok(); } let receive_ringbuffers = [ @@ -190,8 +184,14 @@ fn main() -> ! { core::array::from_fn(|i| { let rx = receive_iter.next().expect("missing receive ringbuffer"); let tx = transmit_iter.next().expect("missing transmit ringbuffer"); - let device = make_device(rx, tx); - trace!("Made device for link {}, with MTU {}", i, device.mtu()); + let device = make_device(rx, tx, &mut uart); + uwriteln!( + uart, + "Made device for link {}, with MTU {}", + i, + device.mtu() + ) + .ok(); device }); @@ -207,53 +207,49 @@ fn main() -> ! { sockets.add(socket) }); let dna = INSTANCES.dna.dna(); - info!("My dna: {:?}", dna); + uwriteln!(uart, "My dna: {:?}", dna).ok(); let is_manager = dna == MANAGER_DNA; - info!( + uwriteln!( + uart, "Role: {}", if is_manager { "manager" } else { "subordinate" } - ); - unsafe { - log::set_max_level_racy(LevelFilter::Trace); - } + ) + .ok(); if is_manager { - info!("Starting manager state machines..."); + uwriteln!(uart, "Starting manager state machines...").ok(); let mut reports: [Option; LINK_COUNT] = [None; LINK_COUNT]; for link in 0..LINK_COUNT { let socket_handle = socket_handles[link]; - info!("Starting manager for link {}", link); + uwriteln!(uart, "Starting manager for link {}", link).ok(); let now = to_smoltcp_instant(INSTANCES.timer.now()); let mut iface = Interface::new(Config::new(HardwareAddress::Ip), &mut devices[link], now); set_iface_ip(&mut iface, CLIENT_IP); let mut manager = Manager::new(iface, socket_handle, link, SERVER_IP); - trace!("Starting manager loop for link {}", link); loop { let now = to_smoltcp_instant(INSTANCES.timer.now()); let mut sockets = socket_set(&mut sockets_storage[link][..]); - trace!("Polling manager link {}", link); manager.poll(now, &mut devices[link], &mut sockets); - trace!("manager link {} state {:?}", link, manager.state()); if manager.is_done() { - trace!("manager link {} is done", link); break; } } reports[link] = Some(manager.report()); } - info!("UGN reports from subordinates:"); + uwriteln!(uart, "UGN reports from subordinates:").ok(); for (idx, report) in reports.iter().enumerate() { if let Some(report) = report { - info!("Link {}: {} edges", idx, report.count); + uwriteln!(uart, "Link {}: {} edges", idx, report.count).ok(); for (edge_idx, edge) in report.edges.iter().enumerate() { if edge_idx >= report.count as usize { break; } if let Some(edge) = edge { - info!( + uwriteln!( + uart, " Edge {}: {}:{} -> {}:{}, ugn={}", edge_idx, edge.src_node, @@ -261,17 +257,18 @@ fn main() -> ! { edge.dst_node, edge.dst_port, edge.ugn - ); + ) + .ok(); } else { - warn!(" Edge {}: missing", edge_idx); + uwriteln!(uart, " Edge {}: missing", edge_idx).ok(); } } } else { - warn!("Link {}: no report", idx); + uwriteln!(uart, "Link {}: no report", idx).ok(); } } } else { - info!("Starting subordinate state machines..."); + uwriteln!(uart, "Starting subordinate state machines...").ok(); let mut subordinates: [Subordinate; LINK_COUNT] = core::array::from_fn(|idx| { let now = to_smoltcp_instant(INSTANCES.timer.now()); @@ -287,36 +284,10 @@ fn main() -> ! { loop { tick = tick.wrapping_add(1); - if tick % LOG_TICK_EVERY == 0 { - info!("subordinate loop tick {}", tick); - } let now = to_smoltcp_instant(INSTANCES.timer.now()); for link in 0..LINK_COUNT { let mut sockets = socket_set(&mut sockets_storage[link][..]); - trace!("Polling subordinate link {}", link); subordinates[link].poll(now, &mut devices[link], &mut sockets); - { - let socket = sockets.get::(socket_handles[link]); - trace!( - "subordinate link {} socket open {} active {} can_send {} can_recv {} state {:?}", - link, - socket.is_open(), - socket.is_active(), - socket.can_send(), - socket.can_recv(), - socket.state() - ); - } - trace!( - "subordinate link {} ip addrs {:?}", - link, - subordinates[link].iface().ip_addrs() - ); - trace!( - "subordinate link {} state {:?}", - link, - subordinates[link].state() - ); } } } @@ -341,9 +312,9 @@ fn exception_handler(_trap_frame: &riscv_rt::TrapFrame) -> ! { let mut uart = INSTANCES.uart; riscv::interrupt::free(|| { uwriteln!(uart, "... caught an exception. Looping forever now.\n").unwrap(); - info!("mcause: {:?}\n", mcause::read()); - info!("mepc: {:?}\n", mepc::read()); - info!("mtval: {:?}\n", mtval::read()); + writeln!(uart, "mcause: {:?}\n", mcause::read()).ok(); + writeln!(uart, "mepc: {:?}\n", mepc::read()).ok(); + writeln!(uart, "mtval: {:?}\n", mtval::read()).ok(); }); loop { continue; @@ -364,6 +335,5 @@ fn build_report_for_link( dst_port: link as u32, ugn: capture_ugn.local_counter() as i64, }); - trace!("Prepared report for link {}", link); report } From 418f2acefa06c56fe7a8ff4d73367ef17675e5c5 Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Mon, 23 Mar 2026 11:57:06 +0100 Subject: [PATCH 67/70] Turn of `log` package --- firmware-binaries/demos/soft-ugn-demo-mu-2/Cargo.toml | 2 +- firmware-binaries/examples/smoltcp_client/Cargo.toml | 2 +- firmware-binaries/sim-tests/ringbuffer_smoltcp_test/Cargo.toml | 2 +- firmware-binaries/sim-tests/switch_demo_pe_test/Cargo.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/Cargo.toml b/firmware-binaries/demos/soft-ugn-demo-mu-2/Cargo.toml index d77352a34..5d6141d83 100644 --- a/firmware-binaries/demos/soft-ugn-demo-mu-2/Cargo.toml +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/Cargo.toml @@ -23,7 +23,7 @@ features = ["log", "medium-ip", "medium-ethernet", "proto-ipv4", "socket-tcp"] [dependencies.log] version = "0.4.21" -features = ["max_level_trace", "release_max_level_trace"] +features = ["max_level_trace", "release_max_level_off"] [build-dependencies] memmap-generate = { path = "../../../firmware-support/memmap-generate" } diff --git a/firmware-binaries/examples/smoltcp_client/Cargo.toml b/firmware-binaries/examples/smoltcp_client/Cargo.toml index 89165d899..acd70f57c 100644 --- a/firmware-binaries/examples/smoltcp_client/Cargo.toml +++ b/firmware-binaries/examples/smoltcp_client/Cargo.toml @@ -31,7 +31,7 @@ default-features = false [dependencies.log] version = "0.4.21" -features = ["max_level_trace", "release_max_level_trace"] +features = ["max_level_trace", "release_max_level_off"] [dependencies.smoltcp] version = "0.12.0" diff --git a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/Cargo.toml b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/Cargo.toml index 6daf1838c..5baf9bfe6 100644 --- a/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/Cargo.toml +++ b/firmware-binaries/sim-tests/ringbuffer_smoltcp_test/Cargo.toml @@ -23,7 +23,7 @@ features = ["medium-ip", "medium-ethernet", "proto-ipv4", "socket-tcp"] [dependencies.log] version = "0.4.21" -features = ["max_level_trace", "release_max_level_trace"] +features = ["max_level_trace", "release_max_level_off"] [build-dependencies] memmap-generate = { path = "../../../firmware-support/memmap-generate" } diff --git a/firmware-binaries/sim-tests/switch_demo_pe_test/Cargo.toml b/firmware-binaries/sim-tests/switch_demo_pe_test/Cargo.toml index 8b8e8a4d9..7af51b109 100644 --- a/firmware-binaries/sim-tests/switch_demo_pe_test/Cargo.toml +++ b/firmware-binaries/sim-tests/switch_demo_pe_test/Cargo.toml @@ -19,7 +19,7 @@ ufmt = "0.2.0" [dependencies.log] version = "0.4.21" -features = ["max_level_trace", "release_max_level_trace"] +features = ["max_level_trace", "release_max_level_off"] [build-dependencies] memmap-generate = { path = "../../../firmware-support/memmap-generate" } From 46fa21f8f74702257ae03a45f9113cb253bd7fbf Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Mon, 23 Mar 2026 11:29:11 +0100 Subject: [PATCH 68/70] Increase instruction memory size --- .../src/Bittide/Instances/Hitl/SoftUgnDemo/Core.hs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Core.hs b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Core.hs index c4b4d3144..6e15d2246 100644 --- a/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Core.hs +++ b/bittide-instances/src/Bittide/Instances/Hitl/SoftUgnDemo/Core.hs @@ -75,7 +75,7 @@ muConfig :: muConfig = PeConfig { cpu = Riscv32imc.vexRiscv1 - , depthI = SNat @(Div (320 * 1024) 4) + , depthI = SNat @(Div (328 * 1024) 4) , depthD = SNat @(Div (80 * 1024) 4) , initI = Nothing , initD = Nothing From 9f197ea0c4a5ac99fc3519dfb51ac0b1b44980ba Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Mon, 23 Mar 2026 12:26:38 +0100 Subject: [PATCH 69/70] More logging --- .../demos/soft-ugn-demo-mu-2/src/main.rs | 72 +++++++++++++++++-- 1 file changed, 67 insertions(+), 5 deletions(-) diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs index 3419413ec..590c1e201 100644 --- a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs @@ -77,6 +77,7 @@ fn main() -> ! { let mut uart = INSTANCES.uart; uwriteln!(uart, "=== Soft UGN Demo MU2 ===").ok(); + uwriteln!(uart, "Initializing peripherals...").ok(); let transceivers = &INSTANCES.transceivers; let cc = INSTANCES.clock_control; let elastic_buffers = [ @@ -108,6 +109,7 @@ fn main() -> ! { uwriteln!(uart, "Bringing up links...").ok(); let mut link_startups = [LinkStartup::new(); LINK_COUNT]; + let mut links_done = [false; LINK_COUNT]; while !link_startups.iter().all(|ls| ls.is_done()) { for (i, link_startup) in link_startups.iter_mut().enumerate() { link_startup.next( @@ -116,33 +118,47 @@ fn main() -> ! { elastic_buffers[i], capture_ugns[i].has_captured(), ); + if !links_done[i] && link_startup.is_done() { + links_done[i] = true; + uwriteln!(uart, "Link {} startup complete", i).ok(); + } } } + uwriteln!(uart, "All links up").ok(); uwriteln!(uart, "Waiting for stability...").ok(); + let mut previously_stable = false; loop { let stability = Stability { stable: cc.links_stable()[0], settled: 0, }; - if stability.all_stable() { + let currently_stable = stability.all_stable(); + if currently_stable && !previously_stable { + uwriteln!(uart, "Clock stability achieved").ok(); + } + if currently_stable { break; } + previously_stable = currently_stable; } uwriteln!(uart, "Stopping auto-centering...").ok(); elastic_buffers .iter() .for_each(|eb| eb.set_auto_center_enable(false)); + uwriteln!(uart, "Waiting for elastic buffers to become idle...").ok(); elastic_buffers .iter() .for_each(|eb| eb.wait_auto_center_idle()); + uwriteln!(uart, "All elastic buffers idle").ok(); let eb_deltas = elastic_buffers .iter() .map(|eb| eb.auto_center_total_adjustments()); - for (capture_ugn, eb_delta) in capture_ugns.iter().zip(eb_deltas) { + for (i, (capture_ugn, eb_delta)) in capture_ugns.iter().zip(eb_deltas).enumerate() { capture_ugn.set_elastic_buffer_delta(eb_delta); + uwriteln!(uart, "Set EB delta for link {}: {}", i, eb_delta).ok(); } uwriteln!(uart, "Captured hardware UGNs").ok(); @@ -175,9 +191,11 @@ fn main() -> ! { INSTANCES.transmit_ringbuffer_5, INSTANCES.transmit_ringbuffer_6, ]; + uwriteln!(uart, "Clearing transmit ringbuffers...").ok(); for tx in transmit_ringbuffers.iter() { tx.clear(); } + uwriteln!(uart, "Creating network devices...").ok(); let mut receive_iter = receive_ringbuffers.into_iter(); let mut transmit_iter = transmit_ringbuffers.into_iter(); let mut devices: [RingbufferDevice; LINK_COUNT] = @@ -195,6 +213,7 @@ fn main() -> ! { device }); + uwriteln!(uart, "Creating TCP sockets...").ok(); let mut sockets_storage: [[SocketStorage<'static>; 1]; LINK_COUNT] = Default::default(); let socket_handles: [SocketHandle; LINK_COUNT] = core::array::from_fn(|idx: usize| { let rx_buf = unsafe { &mut TCP_RX_BUFS[idx][..] }; @@ -204,7 +223,9 @@ fn main() -> ! { tcp::SocketBuffer::new(tx_buf), ); let mut sockets = socket_set(&mut sockets_storage[idx][..]); - sockets.add(socket) + let handle = sockets.add(socket); + uwriteln!(uart, "Created TCP socket for link {}", idx).ok(); + handle }); let dna = INSTANCES.dna.dna(); uwriteln!(uart, "My dna: {:?}", dna).ok(); @@ -215,9 +236,28 @@ fn main() -> ! { if is_manager { "manager" } else { "subordinate" } ) .ok(); + uwriteln!( + uart, + "Client IP: {}.{}.{}.{}", + CLIENT_IP[0], + CLIENT_IP[1], + CLIENT_IP[2], + CLIENT_IP[3] + ) + .ok(); + uwriteln!( + uart, + "Server IP: {}.{}.{}.{}", + SERVER_IP[0], + SERVER_IP[1], + SERVER_IP[2], + SERVER_IP[3] + ) + .ok(); if is_manager { uwriteln!(uart, "Starting manager state machines...").ok(); + uwriteln!(uart, "Manager will connect to {} links", LINK_COUNT).ok(); let mut reports: [Option; LINK_COUNT] = [None; LINK_COUNT]; for link in 0..LINK_COUNT { let socket_handle = socket_handles[link]; @@ -226,20 +266,30 @@ fn main() -> ! { let mut iface = Interface::new(Config::new(HardwareAddress::Ip), &mut devices[link], now); set_iface_ip(&mut iface, CLIENT_IP); + uwriteln!(uart, "Manager link {} IP set to {:?}", link, CLIENT_IP).ok(); let mut manager = Manager::new(iface, socket_handle, link, SERVER_IP); + let mut prev_state = None; loop { let now = to_smoltcp_instant(INSTANCES.timer.now()); let mut sockets = socket_set(&mut sockets_storage[link][..]); manager.poll(now, &mut devices[link], &mut sockets); + let current_state = manager.state(); + if Some(current_state) != prev_state { + writeln!(uart, "Manager link {} state: {:?}", link, current_state).ok(); + prev_state = Some(current_state); + } if manager.is_done() { + uwriteln!(uart, "Manager link {} completed", link).ok(); break; } } reports[link] = Some(manager.report()); + uwriteln!(uart, "Manager link {} report collected", link).ok(); } uwriteln!(uart, "UGN reports from subordinates:").ok(); + uwriteln!(uart, "Processing {} reports...", LINK_COUNT).ok(); for (idx, report) in reports.iter().enumerate() { if let Some(report) = report { uwriteln!(uart, "Link {}: {} edges", idx, report.count).ok(); @@ -267,6 +317,7 @@ fn main() -> ! { uwriteln!(uart, "Link {}: no report", idx).ok(); } } + uwriteln!(uart, "All manager operations complete").ok(); } else { uwriteln!(uart, "Starting subordinate state machines...").ok(); @@ -275,12 +326,17 @@ fn main() -> ! { let mut iface = Interface::new(Config::new(HardwareAddress::Ip), &mut devices[idx], now); set_iface_ip(&mut iface, SERVER_IP); + uwriteln!(uart, "Subordinate link {} IP set to {:?}", idx, SERVER_IP).ok(); Subordinate::new(iface, socket_handles[idx], idx, dna) }); let mut tick: u32 = 0; for link in 0..LINK_COUNT { - subordinates[link].set_report(build_report_for_link(link, &capture_ugns[link], &dna)); + let report = build_report_for_link(link, &capture_ugns[link], &dna); + subordinates[link].set_report(report); + uwriteln!(uart, "Subordinate link {} report set", link).ok(); } + uwriteln!(uart, "Entering subordinate main loop...").ok(); + let mut prev_states = [None; LINK_COUNT]; loop { tick = tick.wrapping_add(1); @@ -288,6 +344,11 @@ fn main() -> ! { for link in 0..LINK_COUNT { let mut sockets = socket_set(&mut sockets_storage[link][..]); subordinates[link].poll(now, &mut devices[link], &mut sockets); + let current_state = subordinates[link].state(); + if Some(current_state) != prev_states[link] { + writeln!(uart, "Subordinate link {} state: {:?}", link, current_state).ok(); + prev_states[link] = Some(current_state); + } } } } @@ -328,12 +389,13 @@ fn build_report_for_link( ) -> UgnReport { let mut report = UgnReport::new(); report.count = 1; + let ugn_value = capture_ugn.local_counter() as i64; report.edges[0] = Some(UgnEdge { src_node: dna[0] as u32, src_port: link as u32, dst_node: 0, dst_port: link as u32, - ugn: capture_ugn.local_counter() as i64, + ugn: ugn_value, }); report } From ef146f7854d4fc55d956b7e86fe64ef73b89a4be Mon Sep 17 00:00:00 2001 From: Lucas Bollen Date: Mon, 23 Mar 2026 13:24:21 +0100 Subject: [PATCH 70/70] remove `sync_unsafe_cell` --- firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs index 590c1e201..185e9e649 100644 --- a/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs +++ b/firmware-binaries/demos/soft-ugn-demo-mu-2/src/main.rs @@ -1,6 +1,5 @@ #![no_std] #![cfg_attr(not(test), no_main)] -#![feature(sync_unsafe_cell)] // SPDX-FileCopyrightText: 2026 Google LLC //