1084 lines
47 KiB
C
1084 lines
47 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* test_noc.c — the named NoC read/write helpers (ft_tt_noc.{h,c}) and the
|
|
* device-memory bump allocator (ft_tt_buffer.{h,c}).
|
|
*
|
|
* Two halves, deliberately separated:
|
|
*
|
|
* unit Pure arithmetic: the alignment rule table, the transfer-bounds
|
|
* check, and every allocator behaviour. NO DEVICE IS OPENED, so this
|
|
* runs on the Mac and is where the rules get proven wrong — a wrong
|
|
* NoC coordinate hangs the chip and costs a physical power cycle
|
|
* (spec §7.3), so as little as possible is discovered on silicon.
|
|
*
|
|
* l1 Write and read back one Tensix's L1 through ft_tt_write_l1 /
|
|
* ft_tt_read_l1, on a core DERIVED from live telemetry.
|
|
* dram Round-trip every enabled DRAM bank, and prove the banks are
|
|
* distinct endpoints rather than aliases of one.
|
|
* cross A transfer that genuinely spans several 2 MiB windows. This is the
|
|
* re-windowing path, and ft_tt_noc_path_aim_count() is what tells a
|
|
* real multi-window run from a lucky in-window one.
|
|
* align Alignment refusals against a live path: the helper must reject
|
|
* without issuing anything.
|
|
* bw MB/s through the helpers, against the 287 MB/s WC baseline the raw
|
|
* window layer measured (spec §9.8).
|
|
*
|
|
* SAFETY: every coordinate comes from ft_tt_coord against the live grid, and
|
|
* the Tensix used is index 1 or higher — index 0 is reserved for the launch
|
|
* work happening in parallel.
|
|
*
|
|
* Build (unit half needs no fence; the device halves do):
|
|
* make -f Makefile.noc check unit only
|
|
* make -f Makefile.noc PHASE1B=1 all then ./build-noc/test_noc dram
|
|
*/
|
|
|
|
/* clock_gettime/CLOCK_MONOTONIC are POSIX and -std=c99 turns off glibc's implicit
|
|
* feature-test macros. 200809L, not 199309L: the older level also hides snprintf. */
|
|
#define _POSIX_C_SOURCE 200809L
|
|
|
|
#include "ft_tt_arc_telemetry.h"
|
|
#include "ft_tt_buffer.h"
|
|
#include "ft_tt_arch.h"
|
|
#include "ft_tt_coord.h"
|
|
#include "ft_tt_device.h"
|
|
#include "ft_tt_noc.h"
|
|
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
|
|
/* The Tensix this test is allowed to touch. Index 0 belongs to the launch
|
|
* bring-up running concurrently; sharing a core across two agents is how a
|
|
* half-loaded firmware image gets blamed on the wrong change. */
|
|
#define TEST_TENSIX_INDEX 1u
|
|
|
|
/* The DRAM bank the multi-MiB steps (cross, align, bw) write to. Bank 0 belongs to
|
|
* the kernel-toolchain work running in parallel; the all-bank sweep in step_dram
|
|
* still has to touch it, because "the banks are distinct endpoints" is exactly what
|
|
* it proves, but nothing else here needs to. */
|
|
#define TEST_DRAM_BANK 1u
|
|
|
|
#define KIB 1024u
|
|
#define MIB (1024u * 1024u)
|
|
|
|
static int failures;
|
|
static int checks;
|
|
|
|
static void check(const char *what, bool ok, const char *detail)
|
|
{
|
|
checks++;
|
|
if (!ok)
|
|
failures++;
|
|
printf(" %-4s %-58s %s\n", ok ? "ok" : "FAIL", what, detail ? detail : "");
|
|
}
|
|
|
|
static void check_rc(const char *what, int actual, int expected)
|
|
{
|
|
char detail[96];
|
|
|
|
snprintf(detail, sizeof(detail), "rc=%d (%s), expected %d (%s)", actual,
|
|
ft_tt_error_string(actual), expected, ft_tt_error_string(expected));
|
|
check(what, actual == expected, detail);
|
|
}
|
|
|
|
static double now_seconds(void)
|
|
{
|
|
struct timespec ts;
|
|
|
|
clock_gettime(CLOCK_MONOTONIC, &ts);
|
|
return (double)ts.tv_sec + (double)ts.tv_nsec / 1e9;
|
|
}
|
|
|
|
/** Position-dependent so a shifted, truncated or partially-landed transfer is
|
|
* visible instead of looking like a clean success. */
|
|
static void fill_pattern(unsigned char *buffer, size_t bytes, uint32_t seed)
|
|
{
|
|
size_t index;
|
|
|
|
for (index = 0; index + 4u <= bytes; index += 4u) {
|
|
uint32_t word = seed ^ (uint32_t)(index / 4u) * 0x9E3779B9u;
|
|
|
|
memcpy(buffer + index, &word, sizeof(word));
|
|
}
|
|
}
|
|
|
|
/** memcmp with a first-difference report, because "mismatch" alone does not
|
|
* distinguish a dropped tail from a wrong window. */
|
|
static bool compare_pattern(const unsigned char *expected, const unsigned char *got, size_t bytes,
|
|
char *detail, size_t detail_bytes)
|
|
{
|
|
size_t index;
|
|
|
|
for (index = 0; index < bytes; index++) {
|
|
if (expected[index] != got[index]) {
|
|
snprintf(detail, detail_bytes,
|
|
"first difference at byte %zu of %zu (wrote 0x%02x, read 0x%02x)", index,
|
|
bytes, expected[index], got[index]);
|
|
return false;
|
|
}
|
|
}
|
|
snprintf(detail, detail_bytes, "%zu bytes byte-exact", bytes);
|
|
return true;
|
|
}
|
|
|
|
/* ================================================================
|
|
* unit: alignment and bounds
|
|
* ================================================================ */
|
|
|
|
static void unit_alignment_rules(void)
|
|
{
|
|
char detail[96];
|
|
|
|
printf("\n-- alignment rules (ft_tt_noc_alignment) --\n");
|
|
|
|
snprintf(detail, sizeof(detail), "L1 write %u, L1 read %u",
|
|
ft_tt_noc_alignment(FT_TT_MEM_SPACE_L1, false),
|
|
ft_tt_noc_alignment(FT_TT_MEM_SPACE_L1, true));
|
|
check("L1 is 16 B in both directions",
|
|
ft_tt_noc_alignment(FT_TT_MEM_SPACE_L1, false) == 16u &&
|
|
ft_tt_noc_alignment(FT_TT_MEM_SPACE_L1, true) == 16u,
|
|
detail);
|
|
|
|
snprintf(detail, sizeof(detail), "DRAM write %u, DRAM read %u",
|
|
ft_tt_noc_alignment(FT_TT_MEM_SPACE_DRAM, false),
|
|
ft_tt_noc_alignment(FT_TT_MEM_SPACE_DRAM, true));
|
|
check("DRAM writes take 16 B, DRAM reads take 64 B",
|
|
ft_tt_noc_alignment(FT_TT_MEM_SPACE_DRAM, false) == 16u &&
|
|
ft_tt_noc_alignment(FT_TT_MEM_SPACE_DRAM, true) == 64u,
|
|
detail);
|
|
|
|
check("an unknown space has no alignment", ft_tt_noc_alignment((ft_tt_memory_space)7, false) == 0u,
|
|
NULL);
|
|
|
|
snprintf(detail, sizeof(detail), "L1 %llu, DRAM %llu",
|
|
(unsigned long long)ft_tt_noc_space_size(FT_TT_MEM_SPACE_L1),
|
|
(unsigned long long)ft_tt_noc_space_size(FT_TT_MEM_SPACE_DRAM));
|
|
check("space sizes are 1536 KiB and 0xff000000",
|
|
ft_tt_noc_space_size(FT_TT_MEM_SPACE_L1) == (uint64_t)FT_TT_MEM_L1_SIZE &&
|
|
ft_tt_noc_space_size(FT_TT_MEM_SPACE_DRAM) == (uint64_t)FT_TT_MEM_DRAM_SIZE,
|
|
detail);
|
|
}
|
|
|
|
static void unit_transfer_checks(void)
|
|
{
|
|
uint64_t l1_top = (uint64_t)FT_TT_MEM_L1_SIZE;
|
|
uint64_t dram_top = (uint64_t)FT_TT_MEM_DRAM_SIZE;
|
|
|
|
printf("\n-- transfer legality (ft_tt_noc_check_transfer) --\n");
|
|
|
|
check_rc("L1 write, 16 B aligned both ways",
|
|
ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_L1, false, 0x9F80u, 4096u), FT_TT_OK);
|
|
check_rc("L1 write with a misaligned address is refused",
|
|
ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_L1, false, 0x9F88u, 4096u), FT_TT_EINVAL);
|
|
check_rc("L1 write with a misaligned length is refused",
|
|
ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_L1, false, 0x9F80u, 4100u), FT_TT_EINVAL);
|
|
check_rc("a zero-length transfer is legal",
|
|
ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_L1, false, 0x9F80u, 0u), FT_TT_OK);
|
|
|
|
check_rc("a 16 B-aligned DRAM WRITE is legal",
|
|
ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_DRAM, false, 16u, 16u), FT_TT_OK);
|
|
check_rc("the same address is refused for a DRAM READ (64 B rule)",
|
|
ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_DRAM, true, 16u, 64u), FT_TT_EINVAL);
|
|
check_rc("a 64 B-aligned DRAM read is legal",
|
|
ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_DRAM, true, 64u, 64u), FT_TT_OK);
|
|
check_rc("a DRAM read of 16 B is refused — the LENGTH needs 64 too",
|
|
ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_DRAM, true, 64u, 16u), FT_TT_EINVAL);
|
|
|
|
check_rc("a transfer ending exactly at the top of L1 is legal",
|
|
ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_L1, false, l1_top - 16u, 16u), FT_TT_OK);
|
|
check_rc("one 16 B unit past the top of L1 is refused",
|
|
ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_L1, false, l1_top - 16u, 32u), FT_TT_EINVAL);
|
|
check_rc("a transfer starting at the top of L1 is refused",
|
|
ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_L1, false, l1_top, 16u), FT_TT_EINVAL);
|
|
check_rc("a transfer ending exactly at the top of a DRAM bank is legal",
|
|
ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_DRAM, true, dram_top - 64u, 64u), FT_TT_OK);
|
|
check_rc("past the top of a DRAM bank is refused",
|
|
ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_DRAM, true, dram_top, 64u), FT_TT_EINVAL);
|
|
check_rc("an unknown space is refused",
|
|
ft_tt_noc_check_transfer((ft_tt_memory_space)7, false, 0u, 16u), FT_TT_EINVAL);
|
|
|
|
/* ---------------------------------------------------------------------
|
|
* THE PAIRING. Everything above scores ONE end against ONE space, which is what the
|
|
* checker could do until now — and it is why a transfer whose two ends are each legal
|
|
* on their own could still be illegal. Upstream tests the CONGRUENCE and nothing
|
|
* weaker.
|
|
*
|
|
* The first case below is the whole point: an L1 address at 0x9F80 and a DRAM address
|
|
* at 0x40 BOTH pass their own rule — 0x9F80 is 16-aligned, 0x40 is 64-aligned — and
|
|
* they sit at different offsets inside the 64-byte window the stricter end imposes, so
|
|
* the transfer is not one the NoC will do. `ft_tt_noc_check_transfer()` calls it legal
|
|
* from either side. That is the hole.
|
|
* ------------------------------------------------------------------- */
|
|
printf("\n-- transfer legality, BOTH ends (ft_tt_noc_check_transfer_pair) --\n");
|
|
|
|
check_rc("each end of a 16-vs-64 mismatch passes its OWN rule: the L1 end",
|
|
ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_L1, false, 0x9F90u, 64u), FT_TT_OK);
|
|
check_rc("...and the DRAM end",
|
|
ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_DRAM, true, 0x40u, 64u), FT_TT_OK);
|
|
check_rc("but the PAIR is refused — 0x9F90 is 16 into the 64 B window and 0x40 is 0",
|
|
ft_tt_noc_check_transfer_pair(FT_TT_MEM_SPACE_L1, 0x9F90u, FT_TT_MEM_SPACE_DRAM,
|
|
0x40u, true, 64u),
|
|
FT_TT_EINVAL);
|
|
check_rc("move the L1 end onto the same offset and the pair is legal",
|
|
ft_tt_noc_check_transfer_pair(FT_TT_MEM_SPACE_L1, 0x9FC0u, FT_TT_MEM_SPACE_DRAM,
|
|
0x40u, true, 64u),
|
|
FT_TT_OK);
|
|
/* THE HISTORICAL CASE, replayed. 0x137A0 was the Phase 2 test map's first derived L1
|
|
* base: 16- and 32-byte aligned, and 32 bytes into the 64-byte window. As the LOCAL end
|
|
* of a DRAM read it produced tiles that were byte-exact up to element 992 of 1024 and
|
|
* wrong in their last 64 bytes, with no refusal, no error, and three of four stages
|
|
* still passing (DEV_NOTES.md §3). Nothing in the checker could see it, because each
|
|
* end passed its own rule. */
|
|
check_rc("0x137A0 is a legal L1 address on its own",
|
|
ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_L1, false, 0x137A0u, 2048u), FT_TT_OK);
|
|
check_rc("and DRAM tile 0 is a legal DRAM read on its own",
|
|
ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_DRAM, true, 0u, 2048u), FT_TT_OK);
|
|
check_rc("the PAIR is refused — this is the 992-of-1024 bug, caught before it is issued",
|
|
ft_tt_noc_check_transfer_pair(FT_TT_MEM_SPACE_L1, 0x137A0u, FT_TT_MEM_SPACE_DRAM,
|
|
0u, true, 2048u),
|
|
FT_TT_EINVAL);
|
|
/* An L1-to-L1 pair is judged in a 16 B window, and both ends already have to be
|
|
* 16-aligned to pass their own test — so the congruence is automatic there and adds
|
|
* nothing. It is the DRAM READ direction, and only that direction, where the window is
|
|
* wider than either end's own rule. Stated as a check so a future alignment change has
|
|
* to face it. */
|
|
check_rc("an L1-to-L1 pair is judged in a 16 B window, so congruence is automatic",
|
|
ft_tt_noc_check_transfer_pair(FT_TT_MEM_SPACE_L1, 0x10000u, FT_TT_MEM_SPACE_L1,
|
|
0x20010u, false, 64u),
|
|
FT_TT_OK);
|
|
check_rc("the pair still refuses what a single end would: a misaligned local address",
|
|
ft_tt_noc_check_transfer_pair(FT_TT_MEM_SPACE_L1, 0x9F88u, FT_TT_MEM_SPACE_L1,
|
|
0x9F88u, false, 64u),
|
|
FT_TT_EINVAL);
|
|
check_rc("and a span past the top of DRAM",
|
|
ft_tt_noc_check_transfer_pair(FT_TT_MEM_SPACE_L1, 0x10000u, FT_TT_MEM_SPACE_DRAM,
|
|
dram_top, true, 64u),
|
|
FT_TT_EINVAL);
|
|
/* The DRAM WRITE direction takes the 16-byte rule on the DRAM end, so the window the
|
|
* pair is judged in is 16 and not 64 — a distinction that only exists because the
|
|
* alignment is direction-dependent, and the reason the window is computed rather than
|
|
* fixed. */
|
|
check_rc("a DRAM WRITE judges the pair in a 16 B window, not a 64 B one",
|
|
ft_tt_noc_check_transfer_pair(FT_TT_MEM_SPACE_L1, 0x9F90u, FT_TT_MEM_SPACE_DRAM,
|
|
0x10u, false, 16u),
|
|
FT_TT_OK);
|
|
}
|
|
|
|
/* ================================================================
|
|
* unit: the shared window walk
|
|
* ================================================================ */
|
|
|
|
/**
|
|
* ft_tt_noc_window_slice_at() is the arithmetic BOTH device paths run on — the
|
|
* named L1/DRAM helpers and ft_tt_launch's run control, which since the Phase 2
|
|
* reconciliation goes through ft_tt_noc_transfer_raw() instead of its own copy
|
|
* of this loop. It is also the piece that fails QUIETLY: a wrong window base is
|
|
* still a valid transaction, just to the wrong address on the right core. So it
|
|
* is a pure function, and it gets proven here rather than on silicon.
|
|
*/
|
|
static void unit_window_slices(void)
|
|
{
|
|
const size_t window = 2u * MIB;
|
|
ft_tt_noc_window_slice slice;
|
|
char detail[160];
|
|
|
|
printf("\n-- window slicing (ft_tt_noc_window_slice_at) --\n");
|
|
|
|
check_rc("a transfer wholly inside one window slices whole",
|
|
ft_tt_noc_window_slice_at(0x9F80u, 4096u, window, &slice), FT_TT_OK);
|
|
snprintf(detail, sizeof(detail), "base 0x%llx, offset %zu, %zu bytes",
|
|
(unsigned long long)slice.window_base, slice.offset_in_window, slice.bytes);
|
|
check("it aims at 0 and takes all 4096 bytes",
|
|
slice.window_base == 0u && slice.offset_in_window == 0x9F80u && slice.bytes == 4096u,
|
|
detail);
|
|
|
|
check_rc("a transfer that straddles a boundary is cut at it",
|
|
ft_tt_noc_window_slice_at(window - 64u * KIB, 5u * MIB, window, &slice), FT_TT_OK);
|
|
snprintf(detail, sizeof(detail), "base 0x%llx, offset %zu, %zu bytes",
|
|
(unsigned long long)slice.window_base, slice.offset_in_window, slice.bytes);
|
|
check("the first slice stops exactly at the 2 MiB line",
|
|
slice.window_base == 0u && slice.offset_in_window == window - 64u * KIB &&
|
|
slice.bytes == 64u * KIB,
|
|
detail);
|
|
|
|
check_rc("an address exactly on a boundary starts a fresh window",
|
|
ft_tt_noc_window_slice_at(window, 32u, window, &slice), FT_TT_OK);
|
|
snprintf(detail, sizeof(detail), "base 0x%llx, offset %zu",
|
|
(unsigned long long)slice.window_base, slice.offset_in_window);
|
|
check("base is the boundary and the offset is 0",
|
|
slice.window_base == (uint64_t)window && slice.offset_in_window == 0u &&
|
|
slice.bytes == 32u,
|
|
detail);
|
|
|
|
/* The soft-reset register is why this function cannot assume a memory space:
|
|
* 0xFFB121B0 is in neither L1 nor a DRAM bank (docs/tt_driver_notes.md §5). */
|
|
check_rc("the soft-reset register slices like anything else",
|
|
ft_tt_noc_window_slice_at(0xFFB121B0u, 4u, window, &slice), FT_TT_OK);
|
|
snprintf(detail, sizeof(detail), "base 0x%llx, offset 0x%zx",
|
|
(unsigned long long)slice.window_base, slice.offset_in_window);
|
|
/* 0xFFB00000 is NOT 2 MiB aligned, so the window lands 1 MiB lower and the
|
|
* register sits 0x1121B0 into the aperture — still inside it. */
|
|
check("base 0xFFA00000, offset 0x1121B0, 4 bytes",
|
|
slice.window_base == 0xFFA00000u && slice.offset_in_window == 0x1121B0u &&
|
|
slice.bytes == 4u && slice.offset_in_window < window,
|
|
detail);
|
|
|
|
check_rc("a zero-length transfer slices to nothing",
|
|
ft_tt_noc_window_slice_at(0x9F80u, 0u, window, &slice), FT_TT_OK);
|
|
check("and it asks for no bytes", slice.bytes == 0u, NULL);
|
|
|
|
check_rc("a window size that is not a power of two is refused",
|
|
ft_tt_noc_window_slice_at(0u, 16u, 3u * MIB, &slice), FT_TT_EINVAL);
|
|
check_rc("a zero window size is refused", ft_tt_noc_window_slice_at(0u, 16u, 0u, &slice),
|
|
FT_TT_EINVAL);
|
|
check_rc("a NULL slice is refused", ft_tt_noc_window_slice_at(0u, 16u, window, NULL),
|
|
FT_TT_EINVAL);
|
|
|
|
/* Walk a whole 5 MiB transfer the way ft_tt_noc_transfer_raw() does. This is
|
|
* the off-hardware twin of the `cross` step: same span, same expected four
|
|
* window moves, no card. */
|
|
{
|
|
uint64_t address = (uint64_t)window - 64u * KIB;
|
|
size_t remaining = 5u * MIB;
|
|
size_t moved = 0;
|
|
unsigned windows = 0;
|
|
uint64_t previous_base = UINT64_MAX;
|
|
bool bases_ascend = true;
|
|
|
|
while (remaining > 0u && windows < 64u) {
|
|
if (ft_tt_noc_window_slice_at(address, remaining, window, &slice) != FT_TT_OK)
|
|
break;
|
|
if (previous_base != UINT64_MAX && slice.window_base != previous_base + window)
|
|
bases_ascend = false;
|
|
previous_base = slice.window_base;
|
|
address += slice.bytes;
|
|
moved += slice.bytes;
|
|
remaining -= slice.bytes;
|
|
windows++;
|
|
}
|
|
snprintf(detail, sizeof(detail), "%u windows, %zu of %u bytes moved", windows, moved,
|
|
5u * MIB);
|
|
check("a 5 MiB span 64 KiB below a boundary walks four windows and loses nothing",
|
|
windows == 4u && moved == (size_t)5u * MIB && remaining == 0u && bases_ascend,
|
|
detail);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* The two store widths. WORD32 exists because a 4-byte device register cannot be
|
|
* written 16 bytes at a time; if this table ever said otherwise, the launch path
|
|
* would write the soft-reset register's three neighbours with it.
|
|
*/
|
|
static void unit_transfer_widths(void)
|
|
{
|
|
char detail[128];
|
|
|
|
printf("\n-- store widths (ft_tt_noc_transfer_width_bytes) --\n");
|
|
|
|
snprintf(detail, sizeof(detail), "BLOCK16 %u, WORD32 %u",
|
|
ft_tt_noc_transfer_width_bytes(FT_TT_NOC_WIDTH_BLOCK16),
|
|
ft_tt_noc_transfer_width_bytes(FT_TT_NOC_WIDTH_WORD32));
|
|
check("BLOCK16 is 16 B and WORD32 is 4 B",
|
|
ft_tt_noc_transfer_width_bytes(FT_TT_NOC_WIDTH_BLOCK16) == 16u &&
|
|
ft_tt_noc_transfer_width_bytes(FT_TT_NOC_WIDTH_WORD32) == 4u,
|
|
detail);
|
|
check("an unknown width has no size",
|
|
ft_tt_noc_transfer_width_bytes((ft_tt_noc_transfer_width)9) == 0u, NULL);
|
|
|
|
check_rc("a 4 B register access is legal at WORD32",
|
|
ft_tt_noc_check_raw_transfer(FT_TT_NOC_WIDTH_WORD32, 0xFFB121B0u, 4u), FT_TT_OK);
|
|
check_rc("the same 4 B access is REFUSED at BLOCK16 — a 16 B store over a register",
|
|
ft_tt_noc_check_raw_transfer(FT_TT_NOC_WIDTH_BLOCK16, 0xFFB121B0u, 4u), FT_TT_EINVAL);
|
|
check_rc("a 2-byte-misaligned address is refused at WORD32",
|
|
ft_tt_noc_check_raw_transfer(FT_TT_NOC_WIDTH_WORD32, 0xFFB121B2u, 4u), FT_TT_EINVAL);
|
|
check_rc("a length that is not a whole number of words is refused",
|
|
ft_tt_noc_check_raw_transfer(FT_TT_NOC_WIDTH_WORD32, 0x1000u, 6u), FT_TT_EINVAL);
|
|
check_rc("a 16 B-aligned bulk transfer is legal at BLOCK16",
|
|
ft_tt_noc_check_raw_transfer(FT_TT_NOC_WIDTH_BLOCK16, 0x1000u, 4096u), FT_TT_OK);
|
|
check_rc("an unknown width is refused",
|
|
ft_tt_noc_check_raw_transfer((ft_tt_noc_transfer_width)9, 0u, 16u), FT_TT_EINVAL);
|
|
check_rc("a zero-length raw transfer is legal",
|
|
ft_tt_noc_check_raw_transfer(FT_TT_NOC_WIDTH_WORD32, 0x1000u, 0u), FT_TT_OK);
|
|
}
|
|
|
|
/* ================================================================
|
|
* unit: the allocator
|
|
* ================================================================ */
|
|
|
|
/** The topology this card reports, built by hand so the allocator can be tested
|
|
* with no device present. Matches what ARC telemetry returns here. */
|
|
static ft_tt_grid_info fake_grid(uint32_t dram_bank_count)
|
|
{
|
|
ft_tt_grid_info grid;
|
|
|
|
memset(&grid, 0, sizeof(grid));
|
|
grid.valid = true;
|
|
/* Off-hardware nothing selects an architecture, so the fixture names one. */
|
|
grid.arch = ft_tt_arch_blackhole();
|
|
grid.tensix_enabled_col = 16383u;
|
|
grid.tensix_column_count = 14u;
|
|
grid.tensix_core_count = 140u;
|
|
grid.enabled_gddr = 255u;
|
|
grid.dram_bank_count = dram_bank_count;
|
|
return grid;
|
|
}
|
|
|
|
static void unit_allocator_init(void)
|
|
{
|
|
ft_tt_allocator allocator;
|
|
ft_tt_grid_info grid = fake_grid(8u);
|
|
ft_tt_grid_info unread;
|
|
char detail[128];
|
|
|
|
printf("\n-- allocator setup --\n");
|
|
|
|
memset(&unread, 0, sizeof(unread));
|
|
check_rc("a grid that was never read from telemetry is refused",
|
|
ft_tt_allocator_init(&allocator, NULL, &unread, 0u), FT_TT_EUNAVAIL);
|
|
|
|
{
|
|
ft_tt_grid_info zero_banks = fake_grid(0u);
|
|
ft_tt_grid_info too_many = fake_grid(FT_TT_MAX_DRAM_BANKS + 1u);
|
|
ft_tt_grid_info no_arch = fake_grid(8u);
|
|
ft_tt_grid_info more_than_the_part;
|
|
ft_tt_arch six_bank_part;
|
|
|
|
check_rc("zero DRAM banks is refused",
|
|
ft_tt_allocator_init(&allocator, NULL, &zero_banks, 0u), FT_TT_EINVAL);
|
|
check_rc("more banks than the arena array can hold is refused",
|
|
ft_tt_allocator_init(&allocator, NULL, &too_many, 0u), FT_TT_EINVAL);
|
|
|
|
/* The bank count is meaningless without knowing the part it came from,
|
|
* and guessing one is how a wrong bank becomes a NoC write to nowhere. */
|
|
no_arch.arch = NULL;
|
|
check_rc("a grid with no architecture is refused",
|
|
ft_tt_allocator_init(&allocator, NULL, &no_arch, 0u), FT_TT_EINVAL);
|
|
|
|
/* The bound that is NOT the compiled-in constant: telemetry claiming more
|
|
* banks than the SELECTED part has means the mask was misread. A part with
|
|
* fewer banks than the capacity is the case a 6-bank arch would hit, and
|
|
* the only way to reach it from here is to name one. */
|
|
six_bank_part = *ft_tt_arch_blackhole();
|
|
six_bank_part.dram.bank_count = 6u;
|
|
more_than_the_part = fake_grid(8u);
|
|
more_than_the_part.arch = &six_bank_part;
|
|
check_rc("8 live banks on a 6-bank part is refused",
|
|
ft_tt_allocator_init(&allocator, NULL, &more_than_the_part, 0u), FT_TT_EINVAL);
|
|
|
|
more_than_the_part.dram_bank_count = 6u;
|
|
check_rc("...and 6 on the same part initialises",
|
|
ft_tt_allocator_init(&allocator, NULL, &more_than_the_part, 0u), FT_TT_OK);
|
|
|
|
/* The bank bound at USE time must be the live count, not the array
|
|
* capacity. Asked as an ALLOCATION and against EINVAL specifically:
|
|
* querying free bytes on an out-of-range arena answers 0 either way,
|
|
* because the untouched arenas are zeroed — so the obvious version of
|
|
* this check passes on a bound that is wrong. */
|
|
{
|
|
ft_tt_buffer past_the_end;
|
|
|
|
check_rc("bank 6 on a 6-bank part names no arena, whatever the capacity is",
|
|
ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_DRAM, 6u, 64u, &past_the_end),
|
|
FT_TT_EINVAL);
|
|
check_rc("...and bank 5 does",
|
|
ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_DRAM, 5u, 64u, &past_the_end),
|
|
FT_TT_OK);
|
|
}
|
|
}
|
|
|
|
check_rc("an L1 floor below the usable base is refused",
|
|
ft_tt_allocator_init(&allocator, NULL, &grid, FT_TT_L1_USABLE_BASE - 16u),
|
|
FT_TT_EINVAL);
|
|
check_rc("an L1 floor at the top of L1 is refused",
|
|
ft_tt_allocator_init(&allocator, NULL, &grid, FT_TT_MEM_L1_SIZE), FT_TT_EINVAL);
|
|
|
|
check_rc("a default allocator initialises", ft_tt_allocator_init(&allocator, NULL, &grid, 0u),
|
|
FT_TT_OK);
|
|
|
|
snprintf(detail, sizeof(detail), "%zu free, FT_TT_L1_USABLE_SIZE is %u",
|
|
ft_tt_allocator_free_bytes(&allocator, FT_TT_MEM_SPACE_L1, 0u), FT_TT_L1_USABLE_SIZE);
|
|
check("the L1 arena is exactly the L1 above the reserved map",
|
|
ft_tt_allocator_free_bytes(&allocator, FT_TT_MEM_SPACE_L1, 0u) ==
|
|
(size_t)FT_TT_L1_USABLE_SIZE,
|
|
detail);
|
|
|
|
snprintf(detail, sizeof(detail), "MEM_MAX_KERNEL_SIZE %u overshoots by %u bytes",
|
|
FT_TT_MEM_MAX_KERNEL_SIZE, FT_TT_MEM_MAX_KERNEL_SIZE - FT_TT_L1_USABLE_SIZE);
|
|
check("and it is SMALLER than MEM_MAX_KERNEL_SIZE, which is not a bound",
|
|
(size_t)FT_TT_L1_USABLE_SIZE < (size_t)FT_TT_MEM_MAX_KERNEL_SIZE, detail);
|
|
|
|
snprintf(detail, sizeof(detail), "%zu free per bank",
|
|
ft_tt_allocator_free_bytes(&allocator, FT_TT_MEM_SPACE_DRAM, 0u));
|
|
check("each DRAM arena is one whole bank",
|
|
ft_tt_allocator_free_bytes(&allocator, FT_TT_MEM_SPACE_DRAM, 0u) ==
|
|
(size_t)FT_TT_MEM_DRAM_SIZE,
|
|
detail);
|
|
check("a bank index past the enabled count names no arena",
|
|
ft_tt_allocator_free_bytes(&allocator, FT_TT_MEM_SPACE_DRAM, 8u) == 0u, NULL);
|
|
}
|
|
|
|
static void unit_allocator_alloc(void)
|
|
{
|
|
ft_tt_allocator allocator;
|
|
ft_tt_grid_info grid = fake_grid(8u);
|
|
ft_tt_buffer first;
|
|
ft_tt_buffer second;
|
|
ft_tt_buffer dram;
|
|
char detail[160];
|
|
|
|
printf("\n-- allocation --\n");
|
|
(void)ft_tt_allocator_init(&allocator, NULL, &grid, 0u);
|
|
|
|
check_rc("a zero-byte allocation is refused",
|
|
ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 0u, 0u, &first), FT_TT_EINVAL);
|
|
check_rc("an L1 allocation with a non-zero bank is refused",
|
|
ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 1u, 64u, &first), FT_TT_EINVAL);
|
|
|
|
check_rc("a 1-byte L1 allocation succeeds",
|
|
ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 0u, 1u, &first), FT_TT_OK);
|
|
snprintf(detail, sizeof(detail), "address 0x%llx, %zu asked, %zu reserved",
|
|
(unsigned long long)first.address, first.bytes, first.reserved_bytes);
|
|
check("it starts at the usable L1 floor and reserves a whole 16 B unit",
|
|
first.address == (uint64_t)FT_TT_L1_USABLE_BASE && first.reserved_bytes == 16u, detail);
|
|
|
|
check_rc("a second allocation follows it",
|
|
ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 0u, 4096u, &second), FT_TT_OK);
|
|
snprintf(detail, sizeof(detail), "0x%llx then 0x%llx", (unsigned long long)first.address,
|
|
(unsigned long long)second.address);
|
|
check("the second address is the first plus its reservation",
|
|
second.address == first.address + (uint64_t)first.reserved_bytes, detail);
|
|
|
|
check_rc("a DRAM allocation on bank 3 succeeds",
|
|
ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_DRAM, 3u, 100u, &dram), FT_TT_OK);
|
|
snprintf(detail, sizeof(detail), "address 0x%llx, %zu reserved",
|
|
(unsigned long long)dram.address, dram.reserved_bytes);
|
|
check("DRAM rounds up to the 64 B read alignment", dram.reserved_bytes == 128u, detail);
|
|
check_rc("and the result is legal for a DRAM READ, unaided",
|
|
ft_tt_noc_check_transfer(FT_TT_MEM_SPACE_DRAM, true, dram.address,
|
|
dram.reserved_bytes),
|
|
FT_TT_OK);
|
|
check("allocating on bank 3 did not move bank 4",
|
|
ft_tt_allocator_used_bytes(&allocator, FT_TT_MEM_SPACE_DRAM, 4u) == 0u, NULL);
|
|
check_rc("a bank past the enabled count is refused",
|
|
ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_DRAM, 8u, 64u, &dram), FT_TT_EINVAL);
|
|
|
|
printf("\n-- exhaustion --\n");
|
|
(void)ft_tt_allocator_reset(&allocator);
|
|
check_rc("the whole usable L1 can be taken in one go",
|
|
ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 0u, FT_TT_L1_USABLE_SIZE, &first),
|
|
FT_TT_OK);
|
|
check_rc("one more 16 B unit does not fit",
|
|
ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 0u, 16u, &second), FT_TT_ENOMEM);
|
|
|
|
(void)ft_tt_allocator_reset(&allocator);
|
|
check_rc("MEM_MAX_KERNEL_SIZE bytes of L1 CANNOT be allocated — it overshoots real L1",
|
|
ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 0u, FT_TT_MEM_MAX_KERNEL_SIZE,
|
|
&first),
|
|
FT_TT_ENOMEM);
|
|
|
|
(void)ft_tt_allocator_reset(&allocator);
|
|
check_rc("a whole DRAM bank can be taken in one go",
|
|
ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_DRAM, 0u, FT_TT_MEM_DRAM_SIZE, &dram),
|
|
FT_TT_OK);
|
|
check_rc("and then a single 64 B unit does not",
|
|
ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_DRAM, 0u, 64u, &second), FT_TT_ENOMEM);
|
|
}
|
|
|
|
static void unit_allocator_free(void)
|
|
{
|
|
ft_tt_allocator allocator;
|
|
ft_tt_grid_info grid = fake_grid(8u);
|
|
ft_tt_buffer first;
|
|
ft_tt_buffer second;
|
|
ft_tt_buffer other_bank;
|
|
char detail[128];
|
|
|
|
printf("\n-- LIFO free --\n");
|
|
(void)ft_tt_allocator_init(&allocator, NULL, &grid, 0u);
|
|
(void)ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 0u, 256u, &first);
|
|
(void)ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_L1, 0u, 256u, &second);
|
|
|
|
check_rc("freeing the OLDER buffer first is refused, not silently ignored",
|
|
ft_tt_buffer_free(&allocator, &first), FT_TT_EINVAL);
|
|
snprintf(detail, sizeof(detail), "%u live", ft_tt_allocator_live_count(&allocator, FT_TT_MEM_SPACE_L1, 0u));
|
|
check("and the refusal left the arena untouched",
|
|
ft_tt_allocator_live_count(&allocator, FT_TT_MEM_SPACE_L1, 0u) == 2u &&
|
|
ft_tt_allocator_used_bytes(&allocator, FT_TT_MEM_SPACE_L1, 0u) == 512u,
|
|
detail);
|
|
|
|
check_rc("freeing the newest buffer works", ft_tt_buffer_free(&allocator, &second), FT_TT_OK);
|
|
check_rc("then the older one works too", ft_tt_buffer_free(&allocator, &first), FT_TT_OK);
|
|
check("the arena is back to empty",
|
|
ft_tt_allocator_used_bytes(&allocator, FT_TT_MEM_SPACE_L1, 0u) == 0u &&
|
|
ft_tt_allocator_live_count(&allocator, FT_TT_MEM_SPACE_L1, 0u) == 0u,
|
|
NULL);
|
|
check_rc("a third free of an empty arena is refused",
|
|
ft_tt_buffer_free(&allocator, &first), FT_TT_EINVAL);
|
|
|
|
(void)ft_tt_buffer_alloc(&allocator, FT_TT_MEM_SPACE_DRAM, 1u, 4096u, &other_bank);
|
|
other_bank.logical_bank = 2u;
|
|
check_rc("a buffer freed against the wrong bank is refused",
|
|
ft_tt_buffer_free(&allocator, &other_bank), FT_TT_EINVAL);
|
|
|
|
check_rc("reset drops everything", ft_tt_allocator_reset(&allocator), FT_TT_OK);
|
|
check("every arena is back at its floor",
|
|
ft_tt_allocator_used_bytes(&allocator, FT_TT_MEM_SPACE_DRAM, 1u) == 0u &&
|
|
ft_tt_allocator_live_count(&allocator, FT_TT_MEM_SPACE_DRAM, 1u) == 0u,
|
|
NULL);
|
|
}
|
|
|
|
static void run_unit(void)
|
|
{
|
|
unit_alignment_rules();
|
|
unit_transfer_checks();
|
|
unit_window_slices();
|
|
unit_transfer_widths();
|
|
unit_allocator_init();
|
|
unit_allocator_alloc();
|
|
unit_allocator_free();
|
|
}
|
|
|
|
/* ================================================================
|
|
* Hardware steps
|
|
* ================================================================ */
|
|
|
|
typedef struct device_session {
|
|
ft_tt_ctx *ctx;
|
|
ft_tt_grid_info grid;
|
|
ft_tt_allocator allocator;
|
|
ft_tt_noc_path path;
|
|
} device_session;
|
|
|
|
/** Open the card, re-read the grid, and stand up an allocator and a WC path.
|
|
* Refuses to go further if the grid is not one the coordinate map can express
|
|
* — every device step below derives its endpoint from it. */
|
|
static int session_open(device_session *session)
|
|
{
|
|
int rc;
|
|
|
|
memset(session, 0, sizeof(*session));
|
|
|
|
session->ctx = ft_tt_init_device(0);
|
|
if (!session->ctx) {
|
|
fprintf(stderr, "could not open /dev/tenstorrent/0\n");
|
|
return FT_TT_ENODEV;
|
|
}
|
|
|
|
rc = ft_tt_arc_telemetry_read_grid(session->ctx, &session->grid);
|
|
check_rc("ARC telemetry read", rc, FT_TT_OK);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
|
|
rc = ft_tt_coord_check_grid(&session->grid);
|
|
check_rc("the grid is expressible by the coordinate map", rc, FT_TT_OK);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
|
|
rc = ft_tt_allocator_init(&session->allocator, session->ctx, &session->grid, 0u);
|
|
check_rc("allocator initialised from the live grid", rc, FT_TT_OK);
|
|
if (rc != FT_TT_OK)
|
|
return rc;
|
|
|
|
rc = ft_tt_noc_path_open(session->ctx, FT_TT_KMD_CACHE_WC, &session->path);
|
|
check_rc("WC data path opened", rc, FT_TT_OK);
|
|
return rc;
|
|
}
|
|
|
|
static void session_close(device_session *session)
|
|
{
|
|
if (session->path.window)
|
|
(void)ft_tt_noc_path_close(&session->path);
|
|
if (session->ctx)
|
|
ft_tt_ctx_free(session->ctx);
|
|
memset(session, 0, sizeof(*session));
|
|
}
|
|
|
|
/** L1 round-trip on a Tensix that is NOT index 0. */
|
|
static void step_l1(device_session *session)
|
|
{
|
|
const size_t bytes = 4u * KIB;
|
|
ft_tt_noc_coord core;
|
|
ft_tt_buffer buffer;
|
|
unsigned char *expected = NULL;
|
|
unsigned char *got = NULL;
|
|
char detail[192];
|
|
uint64_t aims_after_write;
|
|
int rc;
|
|
|
|
printf("\n-- L1 round-trip on Tensix %u --\n", TEST_TENSIX_INDEX);
|
|
|
|
rc = ft_tt_coord_tensix_by_index(&session->grid, TEST_TENSIX_INDEX, FT_TT_COORD_TRANSLATED,
|
|
&core);
|
|
snprintf(detail, sizeof(detail), "translated (%u, %u)", core.x, core.y);
|
|
check("Tensix coordinate derived from telemetry", rc == FT_TT_OK, detail);
|
|
if (rc != FT_TT_OK)
|
|
return;
|
|
|
|
rc = ft_tt_buffer_alloc(&session->allocator, FT_TT_MEM_SPACE_L1, 0u, bytes, &buffer);
|
|
check_rc("L1 buffer allocated", rc, FT_TT_OK);
|
|
if (rc != FT_TT_OK)
|
|
return;
|
|
|
|
expected = malloc(bytes);
|
|
got = malloc(bytes);
|
|
if (!expected || !got) {
|
|
check("host buffers", false, "out of memory");
|
|
free(expected);
|
|
free(got);
|
|
return;
|
|
}
|
|
fill_pattern(expected, bytes, 0xA5A5A5A5u);
|
|
memset(got, 0, bytes);
|
|
|
|
rc = ft_tt_write_l1(&session->path, core, buffer.address, expected, bytes);
|
|
check_rc("ft_tt_write_l1", rc, FT_TT_OK);
|
|
aims_after_write = ft_tt_noc_path_aim_count(&session->path);
|
|
|
|
rc = ft_tt_read_l1(&session->path, core, buffer.address, got, bytes);
|
|
check_rc("ft_tt_read_l1", rc, FT_TT_OK);
|
|
|
|
check("L1 round-trip is byte-exact", compare_pattern(expected, got, bytes, detail, sizeof(detail)),
|
|
detail);
|
|
|
|
snprintf(detail, sizeof(detail), "%llu aims for the write, %llu total",
|
|
(unsigned long long)aims_after_write,
|
|
(unsigned long long)ft_tt_noc_path_aim_count(&session->path));
|
|
check("4 KiB inside one window costs ONE aim, and the read costs none",
|
|
aims_after_write == 1u && ft_tt_noc_path_aim_count(&session->path) == 1u, detail);
|
|
|
|
(void)ft_tt_buffer_free(&session->allocator, &buffer);
|
|
free(expected);
|
|
free(got);
|
|
}
|
|
|
|
/** Round-trip every enabled DRAM bank, then prove they are not aliases. */
|
|
static void step_dram(device_session *session)
|
|
{
|
|
const size_t bytes = 64u * KIB;
|
|
unsigned char *expected = malloc(bytes);
|
|
unsigned char *got = malloc(bytes);
|
|
ft_tt_buffer buffers[FT_TT_NUM_DRAM_BANKS];
|
|
ft_tt_noc_coord coords[FT_TT_NUM_DRAM_BANKS];
|
|
uint32_t banks = session->grid.dram_bank_count;
|
|
uint32_t bank;
|
|
char detail[192];
|
|
|
|
memset(buffers, 0, sizeof(buffers));
|
|
memset(coords, 0, sizeof(coords));
|
|
|
|
printf("\n-- DRAM round-trip, all %u banks --\n", banks);
|
|
if (!expected || !got) {
|
|
check("host buffers", false, "out of memory");
|
|
free(expected);
|
|
free(got);
|
|
return;
|
|
}
|
|
|
|
for (bank = 0; bank < banks; bank++) {
|
|
char label[96];
|
|
int rc = ft_tt_coord_dram_bank(&session->grid, bank, FT_TT_COORD_TRANSLATED, &coords[bank]);
|
|
|
|
snprintf(label, sizeof(label), "bank %u coordinate derived", bank);
|
|
snprintf(detail, sizeof(detail), "translated (%u, %u)", coords[bank].x, coords[bank].y);
|
|
check(label, rc == FT_TT_OK, detail);
|
|
if (rc != FT_TT_OK) {
|
|
free(expected);
|
|
free(got);
|
|
return;
|
|
}
|
|
|
|
rc = ft_tt_buffer_alloc(&session->allocator, FT_TT_MEM_SPACE_DRAM, bank, bytes,
|
|
&buffers[bank]);
|
|
if (rc != FT_TT_OK) {
|
|
snprintf(label, sizeof(label), "bank %u buffer allocated", bank);
|
|
check_rc(label, rc, FT_TT_OK);
|
|
free(expected);
|
|
free(got);
|
|
return;
|
|
}
|
|
|
|
/* Seed by bank so bank n's data can never be mistaken for bank m's. */
|
|
fill_pattern(expected, bytes, 0x5A5A0000u + bank);
|
|
memset(got, 0, bytes);
|
|
|
|
rc = ft_tt_write_dram(&session->path, coords[bank], buffers[bank].address, expected, bytes);
|
|
snprintf(label, sizeof(label), "bank %u ft_tt_write_dram", bank);
|
|
check_rc(label, rc, FT_TT_OK);
|
|
|
|
rc = ft_tt_read_dram(&session->path, coords[bank], buffers[bank].address, got, bytes);
|
|
snprintf(label, sizeof(label), "bank %u ft_tt_read_dram", bank);
|
|
check_rc(label, rc, FT_TT_OK);
|
|
|
|
snprintf(label, sizeof(label), "bank %u round-trip is byte-exact", bank);
|
|
check(label, compare_pattern(expected, got, bytes, detail, sizeof(detail)), detail);
|
|
}
|
|
|
|
/* Every bank was written a different pattern and every bank was read back
|
|
* correctly above; re-reading bank 0 now proves the later writes did not
|
|
* land on it, i.e. these are eight endpoints and not one aliased eight
|
|
* times. */
|
|
if (banks > 1u) {
|
|
int rc;
|
|
|
|
fill_pattern(expected, bytes, 0x5A5A0000u);
|
|
memset(got, 0, bytes);
|
|
rc = ft_tt_read_dram(&session->path, coords[0], buffers[0].address, got, bytes);
|
|
check_rc("re-read bank 0 after writing all the others", rc, FT_TT_OK);
|
|
check("bank 0 still holds ITS pattern — the banks are distinct endpoints",
|
|
compare_pattern(expected, got, bytes, detail, sizeof(detail)), detail);
|
|
}
|
|
|
|
for (bank = banks; bank > 0u; bank--)
|
|
(void)ft_tt_buffer_free(&session->allocator, &buffers[bank - 1u]);
|
|
free(expected);
|
|
free(got);
|
|
}
|
|
|
|
/**
|
|
* A transfer that starts 64 KiB below a 2 MiB boundary and runs for 5 MiB, so
|
|
* it covers four windows. This is the re-windowing path; nothing else in the
|
|
* tree exercises it.
|
|
*/
|
|
static void step_cross(device_session *session)
|
|
{
|
|
const size_t bytes = 5u * MIB;
|
|
const uint64_t offset = (uint64_t)2u * MIB - 64u * KIB;
|
|
unsigned char *expected = malloc(bytes);
|
|
unsigned char *got = malloc(bytes);
|
|
ft_tt_buffer region;
|
|
ft_tt_noc_coord bank_port;
|
|
uint64_t start;
|
|
char detail[192];
|
|
uint64_t aims_before;
|
|
uint64_t aims_after_write;
|
|
int rc;
|
|
|
|
printf("\n-- window-crossing transfer (%u MiB starting 64 KiB below a 2 MiB boundary) --\n",
|
|
(unsigned)(bytes / MIB));
|
|
if (!expected || !got) {
|
|
check("host buffers", false, "out of memory");
|
|
free(expected);
|
|
free(got);
|
|
return;
|
|
}
|
|
|
|
rc = ft_tt_coord_dram_bank(&session->grid, TEST_DRAM_BANK, FT_TT_COORD_TRANSLATED,
|
|
&bank_port);
|
|
check_rc("test DRAM bank coordinate derived", rc, FT_TT_OK);
|
|
if (rc != FT_TT_OK) {
|
|
free(expected);
|
|
free(got);
|
|
return;
|
|
}
|
|
|
|
/* Reserve the whole span through the allocator, so the two Phase 1 pieces
|
|
* are exercised together rather than each against a hand-picked address. */
|
|
rc = ft_tt_buffer_alloc(&session->allocator, FT_TT_MEM_SPACE_DRAM, TEST_DRAM_BANK,
|
|
(size_t)offset + bytes,
|
|
®ion);
|
|
check_rc("DRAM region allocated for the crossing", rc, FT_TT_OK);
|
|
if (rc != FT_TT_OK) {
|
|
free(expected);
|
|
free(got);
|
|
return;
|
|
}
|
|
start = region.address + offset;
|
|
|
|
fill_pattern(expected, bytes, 0xC0FFEE00u);
|
|
memset(got, 0, bytes);
|
|
|
|
/* Park the window on the LAST span this transfer will touch, so the walk
|
|
* cannot start out already in position and quietly need one aim fewer. */
|
|
(void)ft_tt_write_dram(&session->path, bank_port, start + 4u * MIB, expected, 64u);
|
|
aims_before = ft_tt_noc_path_aim_count(&session->path);
|
|
|
|
rc = ft_tt_write_dram(&session->path, bank_port, start, expected, bytes);
|
|
check_rc("ft_tt_write_dram across four windows", rc, FT_TT_OK);
|
|
aims_after_write = ft_tt_noc_path_aim_count(&session->path);
|
|
|
|
snprintf(detail, sizeof(detail), "%llu window moves",
|
|
(unsigned long long)(aims_after_write - aims_before));
|
|
check("the write really did move the window four times",
|
|
aims_after_write - aims_before == 4u, detail);
|
|
|
|
rc = ft_tt_read_dram(&session->path, bank_port, start, got, bytes);
|
|
check_rc("ft_tt_read_dram across four windows", rc, FT_TT_OK);
|
|
snprintf(detail, sizeof(detail), "%llu window moves",
|
|
(unsigned long long)(ft_tt_noc_path_aim_count(&session->path) - aims_after_write));
|
|
check("and so did the read", ft_tt_noc_path_aim_count(&session->path) - aims_after_write == 4u,
|
|
detail);
|
|
|
|
check("the whole span survived re-windowing",
|
|
compare_pattern(expected, got, bytes, detail, sizeof(detail)), detail);
|
|
|
|
(void)ft_tt_buffer_free(&session->allocator, ®ion);
|
|
free(expected);
|
|
free(got);
|
|
}
|
|
|
|
/** Alignment refusals against a live path: nothing may reach the device. */
|
|
static void step_align(device_session *session)
|
|
{
|
|
unsigned char scratch[128];
|
|
ft_tt_noc_coord bank_port;
|
|
ft_tt_noc_coord core;
|
|
ft_tt_noc_coord bad_coord;
|
|
uint64_t aims_before;
|
|
char detail[128];
|
|
int rc;
|
|
|
|
printf("\n-- alignment refusals on a live path --\n");
|
|
memset(scratch, 0x11, sizeof(scratch));
|
|
|
|
rc = ft_tt_coord_dram_bank(&session->grid, TEST_DRAM_BANK, FT_TT_COORD_TRANSLATED,
|
|
&bank_port);
|
|
check_rc("test DRAM bank coordinate derived", rc, FT_TT_OK);
|
|
if (rc != FT_TT_OK)
|
|
return;
|
|
rc = ft_tt_coord_tensix_by_index(&session->grid, TEST_TENSIX_INDEX, FT_TT_COORD_TRANSLATED,
|
|
&core);
|
|
check_rc("Tensix coordinate derived", rc, FT_TT_OK);
|
|
if (rc != FT_TT_OK)
|
|
return;
|
|
|
|
aims_before = ft_tt_noc_path_aim_count(&session->path);
|
|
|
|
check_rc("a misaligned DRAM write is refused",
|
|
ft_tt_write_dram(&session->path, bank_port, 8u, scratch, 64u), FT_TT_EINVAL);
|
|
check_rc("a 16 B-aligned DRAM read is refused (64 B rule)",
|
|
ft_tt_read_dram(&session->path, bank_port, 16u, scratch, 64u), FT_TT_EINVAL);
|
|
check_rc("an L1 write running past the top of L1 is refused",
|
|
ft_tt_write_l1(&session->path, core, FT_TT_MEM_L1_SIZE - 16u, scratch, 32u),
|
|
FT_TT_EINVAL);
|
|
check_rc("a NULL host buffer is refused",
|
|
ft_tt_write_dram(&session->path, bank_port, 0u, NULL, 64u), FT_TT_EINVAL);
|
|
|
|
/* A coordinate too large for the 6-bit x_end/y_end fields would be
|
|
* truncated into a DIFFERENT, live endpoint. That is the failure mode that
|
|
* costs a power cycle, so it must be refused rather than masked. */
|
|
bad_coord.x = 64u;
|
|
bad_coord.y = 0u;
|
|
check_rc("a coordinate that does not fit the 6-bit TLB fields is refused",
|
|
ft_tt_write_dram(&session->path, bad_coord, 0u, scratch, 64u), FT_TT_EINVAL);
|
|
|
|
snprintf(detail, sizeof(detail), "%llu aims before, %llu after",
|
|
(unsigned long long)aims_before,
|
|
(unsigned long long)ft_tt_noc_path_aim_count(&session->path));
|
|
check("not one of those refusals aimed the window",
|
|
ft_tt_noc_path_aim_count(&session->path) == aims_before, detail);
|
|
}
|
|
|
|
/** MB/s through the helpers, against the raw-window 287 MB/s WC baseline. */
|
|
static void step_bandwidth(device_session *session)
|
|
{
|
|
const size_t bytes = 8u * MIB;
|
|
unsigned char *host = malloc(bytes);
|
|
ft_tt_buffer region;
|
|
ft_tt_noc_coord bank_port;
|
|
double seconds;
|
|
double megabytes = (double)bytes / (1024.0 * 1024.0);
|
|
double t0;
|
|
int rc;
|
|
|
|
printf("\n-- bandwidth through the helpers (WC, %u MiB, four windows) --\n",
|
|
(unsigned)(bytes / MIB));
|
|
if (!host) {
|
|
check("host buffer", false, "out of memory");
|
|
return;
|
|
}
|
|
memset(host, 0x5A, bytes);
|
|
|
|
rc = ft_tt_coord_dram_bank(&session->grid, TEST_DRAM_BANK, FT_TT_COORD_TRANSLATED,
|
|
&bank_port);
|
|
check_rc("test DRAM bank coordinate derived", rc, FT_TT_OK);
|
|
if (rc == FT_TT_OK)
|
|
rc = ft_tt_buffer_alloc(&session->allocator, FT_TT_MEM_SPACE_DRAM, TEST_DRAM_BANK, bytes,
|
|
®ion);
|
|
check_rc("DRAM region allocated", rc, FT_TT_OK);
|
|
if (rc != FT_TT_OK) {
|
|
free(host);
|
|
return;
|
|
}
|
|
|
|
t0 = now_seconds();
|
|
rc = ft_tt_write_dram(&session->path, bank_port, region.address, host, bytes);
|
|
seconds = now_seconds() - t0;
|
|
check_rc("bulk write", rc, FT_TT_OK);
|
|
printf(" host -> card %8.1f MB/s (raw window layer measured 287 MB/s WC)\n",
|
|
megabytes / seconds);
|
|
|
|
t0 = now_seconds();
|
|
rc = ft_tt_read_dram(&session->path, bank_port, region.address, host, bytes);
|
|
seconds = now_seconds() - t0;
|
|
check_rc("bulk read", rc, FT_TT_OK);
|
|
printf(" card -> host %8.1f MB/s (raw window layer measured 2.3 MB/s)\n",
|
|
megabytes / seconds);
|
|
|
|
(void)ft_tt_buffer_free(&session->allocator, ®ion);
|
|
free(host);
|
|
}
|
|
|
|
/* ================================================================
|
|
* main
|
|
* ================================================================ */
|
|
|
|
static void usage(void)
|
|
{
|
|
fprintf(stderr, "usage: test_noc [unit|l1|dram|cross|align|bw|hw]\n"
|
|
" unit no device is opened (default)\n"
|
|
" hw l1 + dram + cross + align, in that order\n");
|
|
}
|
|
|
|
int main(int argc, char **argv)
|
|
{
|
|
const char *step = (argc > 1) ? argv[1] : "unit";
|
|
device_session session;
|
|
bool want_l1;
|
|
bool want_dram;
|
|
bool want_cross;
|
|
bool want_align;
|
|
bool want_bw;
|
|
|
|
printf("=== test_noc: %s ===\n", step);
|
|
|
|
if (strcmp(step, "unit") == 0) {
|
|
run_unit();
|
|
printf("\n%d checks, %d failed\n", checks, failures);
|
|
return failures == 0 ? 0 : 1;
|
|
}
|
|
|
|
want_l1 = strcmp(step, "l1") == 0 || strcmp(step, "hw") == 0;
|
|
want_dram = strcmp(step, "dram") == 0 || strcmp(step, "hw") == 0;
|
|
want_cross = strcmp(step, "cross") == 0 || strcmp(step, "hw") == 0;
|
|
want_align = strcmp(step, "align") == 0 || strcmp(step, "hw") == 0;
|
|
want_bw = strcmp(step, "bw") == 0;
|
|
|
|
if (!want_l1 && !want_dram && !want_cross && !want_align && !want_bw) {
|
|
usage();
|
|
return 2;
|
|
}
|
|
|
|
if (session_open(&session) != FT_TT_OK) {
|
|
session_close(&session);
|
|
printf("\n%d checks, %d failed\n", checks, failures);
|
|
return 1;
|
|
}
|
|
|
|
if (failures == 0 && want_l1)
|
|
step_l1(&session);
|
|
if (failures == 0 && want_dram)
|
|
step_dram(&session);
|
|
if (failures == 0 && want_cross)
|
|
step_cross(&session);
|
|
if (failures == 0 && want_align)
|
|
step_align(&session);
|
|
if (failures == 0 && want_bw)
|
|
step_bandwidth(&session);
|
|
|
|
session_close(&session);
|
|
printf("\n%d checks, %d failed\n", checks, failures);
|
|
return failures == 0 ? 0 : 1;
|
|
}
|