388 lines
15 KiB
C
388 lines
15 KiB
C
/* SPDX-License-Identifier: AGPL-3.0-or-later */
|
|
/* Copyright (C) 2026 David (Corey) Herrera */
|
|
/**
|
|
* @file test_device.c
|
|
* @brief Phase 1a acceptance test for the Foundry Blackhole backend: device
|
|
* open/close, enumeration, BAR mappings, derived TLB counts, TLB window
|
|
* lifecycle, and the Phase 1a NoC fence.
|
|
*
|
|
* READ-ONLY AGAINST THE DEVICE. The only ioctls this issues are
|
|
* GET_DEVICE_INFO, GET_DRIVER_INFO, QUERY_MAPPINGS, ALLOCATE_TLB and FREE_TLB;
|
|
* the only device memory it touches is the TLB configuration register file in
|
|
* BAR0, which it reads. It never calls CONFIGURE_TLB, never reads or writes
|
|
* through a TLB window, and never resets, pins or powers anything.
|
|
*
|
|
* @status Runs green on the p150a at 12:00.0. Every case has been
|
|
* observed RED by mutating the code it covers — see docs/tt_bringup_log.md
|
|
* for the mutation table and the failure counts.
|
|
* @issues None
|
|
* @todo None
|
|
*
|
|
* Expected values and where they come from:
|
|
* spec §2.1 — p150a, BAR0 512 MiB, BAR2 1 MiB, no BAR4, PCI 12:00.0
|
|
* kernel driver — the fixed mmap offsets
|
|
* kernel driver — 202 windows, index 201 is the kernel's,
|
|
* 4 GiB count derived from BAR4
|
|
* (see BlackholeA0/PCIExpressTile/HostToDeviceTLBs.md)
|
|
*/
|
|
|
|
#include <inttypes.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <string.h>
|
|
|
|
#include "ft_tt_arc_telemetry.h"
|
|
#include "ft_tt_device.h"
|
|
#include "ft_tt_tlb.h"
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Expected values */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
#define EXPECTED_PCI_VENDOR_ID 0x1e52u
|
|
#define EXPECTED_PCI_DEVICE_ID 0xb140u /* Blackhole */
|
|
#define EXPECTED_PCI_BUS 0x12u /* 12:00.0 */
|
|
|
|
#define EXPECTED_BAR0_LEN (512u * 1024u * 1024u)
|
|
#define EXPECTED_BAR2_LEN (1u * 1024u * 1024u)
|
|
#define EXPECTED_BAR4_LEN 0u /* spec §2.3 — flashed out, and that is correct */
|
|
|
|
#define EXPECTED_BAR0_UC_OFFSET (UINT64_C(0) << 36)
|
|
#define EXPECTED_BAR0_WC_OFFSET (UINT64_C(1) << 36)
|
|
#define EXPECTED_BAR2_UC_OFFSET (UINT64_C(2) << 36)
|
|
#define EXPECTED_BAR2_WC_OFFSET (UINT64_C(3) << 36)
|
|
|
|
#define EXPECTED_TLB_2M_TOTAL 202u
|
|
#define EXPECTED_TLB_2M_AVAILABLE 201u
|
|
#define EXPECTED_TLB_4G_TOTAL 0u
|
|
|
|
#define MAX_WINDOWS_TRACKED 256
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Tiny harness */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
static int checks_run;
|
|
static int checks_failed;
|
|
|
|
static void check(int condition, const char *what)
|
|
{
|
|
checks_run++;
|
|
if (condition) {
|
|
printf(" ok %s\n", what);
|
|
} else {
|
|
checks_failed++;
|
|
printf(" FAIL %s\n", what);
|
|
}
|
|
}
|
|
|
|
static void check_u64(uint64_t got, uint64_t want, const char *what)
|
|
{
|
|
checks_run++;
|
|
if (got == want) {
|
|
printf(" ok %s = %" PRIu64 "\n", what, got);
|
|
} else {
|
|
checks_failed++;
|
|
printf(" FAIL %s = %" PRIu64 " (expected %" PRIu64 ")\n", what, got, want);
|
|
}
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
/* Cases */
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
/* The API version is answerable without a device, which is how a caller checks
|
|
* ABI compatibility before opening anything. */
|
|
static void test_driver_api_version_without_device(void)
|
|
{
|
|
uint64_t version = 0;
|
|
uint64_t major = 0;
|
|
|
|
printf("driver attributes without a device\n");
|
|
check(ft_tt_kmd_driver_attr(NULL, FT_TT_KMD_DRIVER_API_VERSION, &version) == 0,
|
|
"API version is answerable with a NULL device");
|
|
check_u64(version, 2, "ioctl API version");
|
|
check(ft_tt_kmd_driver_attr(NULL, FT_TT_KMD_DRIVER_VERSION_MAJOR, &major) != 0,
|
|
"driver semver is refused without a device");
|
|
}
|
|
|
|
static void test_device_identity(const ft_tt_ctx *ctx)
|
|
{
|
|
ft_tt_device_info info;
|
|
|
|
printf("device identity\n");
|
|
memset(&info, 0, sizeof(info));
|
|
check(ft_tt_device_get_info(ctx, &info) == FT_TT_OK, "ft_tt_device_get_info succeeds");
|
|
check_u64(info.pci_vendor_id, EXPECTED_PCI_VENDOR_ID, "PCI vendor id");
|
|
check_u64(info.pci_device_id, EXPECTED_PCI_DEVICE_ID, "PCI device id");
|
|
check_u64(info.chip_arch, (uint64_t)FT_TT_KMD_ARCH_BLACKHOLE, "chip arch (2 = Blackhole)");
|
|
check_u64(info.pci_bus, EXPECTED_PCI_BUS, "PCI bus");
|
|
check_u64(info.pci_device, 0, "PCI device");
|
|
check_u64(info.pci_function, 0, "PCI function");
|
|
check_u64(info.driver_api_version, 2, "driver ioctl API version");
|
|
check(info.driver_version_major != 0 || info.driver_version_minor != 0,
|
|
"driver semver is populated");
|
|
printf(" driver %u.%u.%u\n", info.driver_version_major, info.driver_version_minor,
|
|
info.driver_version_patch);
|
|
}
|
|
|
|
/* QUERY_MAPPINGS is the source of truth for BAR lengths, and therefore for the
|
|
* TLB counts. The absence of BAR4 must show up as a length of 0, not as a
|
|
* missing-field error. */
|
|
static void test_bar_mappings(const ft_tt_ctx *ctx)
|
|
{
|
|
ft_tt_kmd_resources res;
|
|
|
|
printf("BAR mappings\n");
|
|
memset(&res, 0, sizeof(res));
|
|
check(ft_tt_device_get_resources(ctx, &res) == FT_TT_OK, "QUERY_MAPPINGS cache is readable");
|
|
check_u64(res.bar0_len, EXPECTED_BAR0_LEN, "BAR0 length");
|
|
check_u64(res.bar2_len, EXPECTED_BAR2_LEN, "BAR2 length");
|
|
check_u64(res.bar4_len, EXPECTED_BAR4_LEN, "BAR4 length");
|
|
check_u64(res.bar0_uc_offset, EXPECTED_BAR0_UC_OFFSET, "BAR0 UC mmap offset");
|
|
check_u64(res.bar0_wc_offset, EXPECTED_BAR0_WC_OFFSET, "BAR0 WC mmap offset");
|
|
check_u64(res.bar2_uc_offset, EXPECTED_BAR2_UC_OFFSET, "BAR2 UC mmap offset");
|
|
check_u64(res.bar2_wc_offset, EXPECTED_BAR2_WC_OFFSET, "BAR2 WC mmap offset");
|
|
check(res.bar4_uc_offset == 0 && res.bar4_wc_offset == 0,
|
|
"BAR4 has no mapping entries at all");
|
|
}
|
|
|
|
/* Window counts derived from BAR lengths (ft_tt_kmd.h): upstream would report eight 4 GiB
|
|
* windows on this board. There are none, because there is no BAR4. */
|
|
static void test_derived_tlb_counts(const ft_tt_ctx *ctx)
|
|
{
|
|
ft_tt_tlb_counts counts;
|
|
|
|
printf("TLB counts derived from BAR lengths\n");
|
|
memset(&counts, 0, sizeof(counts));
|
|
check(ft_tt_tlb_counts_get(ctx, &counts) == FT_TT_OK, "counts are readable");
|
|
check_u64(counts.window_2m_total, EXPECTED_TLB_2M_TOTAL, "2 MiB windows on the board");
|
|
check_u64(counts.window_2m_available, EXPECTED_TLB_2M_AVAILABLE,
|
|
"2 MiB windows left after the driver's own");
|
|
check_u64(counts.window_4g_total, EXPECTED_TLB_4G_TOTAL,
|
|
"4 GiB windows (upstream's table would say 8)");
|
|
}
|
|
|
|
/* The TLB register file is BAR0 configuration space, not a NoC aperture:
|
|
* reading it issues no NoC transaction. Reading it is also the only way to
|
|
* prove the mapping landed where we think it did. */
|
|
static void test_tlb_register_file(const ft_tt_ctx *ctx)
|
|
{
|
|
const volatile uint32_t *regs = ft_tt_tlb_registers(ctx);
|
|
uint32_t sink = 0;
|
|
size_t word;
|
|
|
|
printf("TLB register file\n");
|
|
check(regs != NULL, "BAR0 + 0x1FC00000 is mapped");
|
|
check_u64((uint64_t)ft_tt_tlb_registers_size(ctx), FT_TT_TLB_REGS_LEN, "mapped bytes");
|
|
|
|
if (!regs)
|
|
return;
|
|
|
|
/* Every window's registers must be inside the mapping. */
|
|
check((size_t)FT_TT_TLB_TOTAL_WINDOW_COUNT * FT_TT_TLB_REG_STRIDE <=
|
|
ft_tt_tlb_registers_size(ctx),
|
|
"all per-window registers fit in the mapping");
|
|
check((size_t)FT_TT_TLB_STRIDED_REGS_OFFSET +
|
|
(size_t)FT_TT_TLB_STRIDED_WINDOW_COUNT * 4u <=
|
|
ft_tt_tlb_registers_size(ctx),
|
|
"the strided-multicast register bank fits too");
|
|
|
|
for (word = 0; word < 8; word++)
|
|
sink |= regs[word];
|
|
check(1, "register file is readable without faulting");
|
|
printf(" first 8 words OR to 0x%08" PRIx32 "\n", sink);
|
|
}
|
|
|
|
static void test_window_lifecycle(ft_tt_ctx *ctx)
|
|
{
|
|
ft_tt_tlb_window *windows[4] = {NULL, NULL, NULL, NULL};
|
|
size_t i;
|
|
size_t j;
|
|
int all_distinct = 1;
|
|
int all_in_range = 1;
|
|
|
|
printf("TLB window lifecycle\n");
|
|
check_u64(ft_tt_ctx_live_window_count(ctx), 0, "live windows before allocating");
|
|
|
|
for (i = 0; i < 4; i++) {
|
|
int rc = ft_tt_tlb_window_alloc(ctx, (size_t)FT_TT_KMD_WINDOW_SIZE_2M,
|
|
FT_TT_KMD_CACHE_WC, &windows[i]);
|
|
if (rc != FT_TT_OK) {
|
|
printf(" FAIL allocating window %zu: %s\n", i, ft_tt_error_string(rc));
|
|
checks_run++;
|
|
checks_failed++;
|
|
break;
|
|
}
|
|
}
|
|
|
|
check(windows[3] != NULL, "four 2 MiB windows allocate");
|
|
check_u64(ft_tt_ctx_live_window_count(ctx), 4, "live window tally");
|
|
|
|
for (i = 0; i < 4 && windows[i]; i++) {
|
|
uint32_t id = ft_tt_tlb_window_id(windows[i]);
|
|
|
|
if (id >= EXPECTED_TLB_2M_TOTAL || id == FT_TT_TLB_KERNEL_WINDOW_INDEX)
|
|
all_in_range = 0;
|
|
for (j = 0; j < i; j++) {
|
|
if (ft_tt_tlb_window_id(windows[j]) == id)
|
|
all_distinct = 0;
|
|
}
|
|
if (ft_tt_tlb_window_register_offset(windows[i]) != (size_t)id * FT_TT_TLB_REG_STRIDE)
|
|
all_in_range = 0;
|
|
}
|
|
|
|
check(all_in_range,
|
|
"ids are in [0, 202), never the kernel's 201, and map to id * 12 register bytes");
|
|
check(all_distinct, "ids are distinct");
|
|
check(ft_tt_tlb_window_size(windows[0]) == (size_t)FT_TT_KMD_WINDOW_SIZE_2M,
|
|
"window size is 2 MiB");
|
|
check(ft_tt_tlb_window_aperture(windows[0]) != NULL, "the aperture is mapped");
|
|
check(!ft_tt_tlb_window_is_aimed(windows[0]), "a fresh window is aimed nowhere");
|
|
|
|
for (i = 0; i < 4; i++) {
|
|
if (windows[i])
|
|
check(ft_tt_tlb_window_free(ctx, windows[i]) == FT_TT_OK, "window frees");
|
|
}
|
|
check_u64(ft_tt_ctx_live_window_count(ctx), 0, "live windows after freeing");
|
|
}
|
|
|
|
/* Direct proof that the derived 4 GiB count matches the driver: with no BAR4
|
|
* the kernel finds zero windows of that size and rejects the request. A caller trusting upstream's table would call this
|
|
* eight times and fail eight times. */
|
|
static void test_4g_window_is_refused(ft_tt_ctx *ctx)
|
|
{
|
|
ft_tt_tlb_window *window = NULL;
|
|
int rc;
|
|
|
|
printf("4 GiB window request\n");
|
|
rc = ft_tt_tlb_window_alloc(ctx, (size_t)FT_TT_KMD_WINDOW_SIZE_4G, FT_TT_KMD_CACHE_WC,
|
|
&window);
|
|
check(rc == FT_TT_EINVAL, "the driver refuses a 4 GiB window with -EINVAL");
|
|
check(window == NULL, "no handle is produced");
|
|
if (window)
|
|
(void)ft_tt_tlb_window_free(ctx, window);
|
|
}
|
|
|
|
/* Exhaust the pool. This is the only way to observe that 201, not 202, windows
|
|
* are actually obtainable — the kernel claims index 201 at init. Nothing else
|
|
* on the card host holds windows on this card. */
|
|
static void test_window_pool_exhaustion(ft_tt_ctx *ctx)
|
|
{
|
|
static ft_tt_tlb_window *windows[MAX_WINDOWS_TRACKED];
|
|
uint32_t obtained = 0;
|
|
uint32_t kernel_window_seen = 0;
|
|
size_t i;
|
|
|
|
printf("TLB window pool exhaustion\n");
|
|
memset(windows, 0, sizeof(windows));
|
|
|
|
while (obtained < MAX_WINDOWS_TRACKED) {
|
|
ft_tt_tlb_window *window = NULL;
|
|
|
|
if (ft_tt_tlb_window_alloc(ctx, (size_t)FT_TT_KMD_WINDOW_SIZE_2M, FT_TT_KMD_CACHE_WC,
|
|
&window) != FT_TT_OK)
|
|
break;
|
|
if (ft_tt_tlb_window_id(window) == FT_TT_TLB_KERNEL_WINDOW_INDEX)
|
|
kernel_window_seen++;
|
|
windows[obtained++] = window;
|
|
}
|
|
|
|
check_u64(obtained, EXPECTED_TLB_2M_AVAILABLE, "windows obtainable before -ENOMEM");
|
|
check_u64(kernel_window_seen, 0, "the driver never hands out its own window");
|
|
|
|
for (i = 0; i < obtained; i++)
|
|
(void)ft_tt_tlb_window_free(ctx, windows[i]);
|
|
|
|
check_u64(ft_tt_ctx_live_window_count(ctx), 0, "every window is returned");
|
|
}
|
|
|
|
/* Capacity is a telemetry question, and telemetry is Phase 1b. The contract is
|
|
* that this REFUSES rather than reporting the nominal 8 banks as a fact. */
|
|
static void test_mem_info_is_honest(const ft_tt_ctx *ctx)
|
|
{
|
|
ft_tt_grid_info grid;
|
|
size_t free_bytes = 12345;
|
|
size_t total_bytes = 12345;
|
|
|
|
printf("capacity reporting\n");
|
|
memset(&grid, 0, sizeof(grid));
|
|
check(ft_tt_grid_get(ctx, &grid) == FT_TT_OK, "grid is readable");
|
|
check(!grid.valid, "grid is marked invalid until ARC telemetry is read");
|
|
check(ft_tt_mem_info(ctx, &free_bytes, &total_bytes) == FT_TT_EUNAVAIL,
|
|
"ft_tt_mem_info refuses rather than guessing");
|
|
check(free_bytes == 12345 && total_bytes == 12345,
|
|
"it writes nothing when it cannot answer");
|
|
check_u64(ft_tt_ctx_alloc_bytes(ctx), 0, "no device memory is allocated in Phase 1a");
|
|
}
|
|
|
|
/* The Phase 1a fence (ft_tt_kmd.h, "THE NOC FENCE"). Aiming a window is the operation
|
|
* that hung this card; in a default build it must be mechanically unreachable,
|
|
* not merely unused. */
|
|
static void test_noc_fence_is_closed(ft_tt_ctx *ctx)
|
|
{
|
|
ft_tt_tlb_window *window = NULL;
|
|
ft_tt_grid_info grid;
|
|
int rc;
|
|
|
|
printf("Phase 1a NoC fence\n");
|
|
memset(&grid, 0, sizeof(grid));
|
|
|
|
check(ft_tt_tlb_window_alloc(ctx, (size_t)FT_TT_KMD_WINDOW_SIZE_2M, FT_TT_KMD_CACHE_UC,
|
|
&window) == FT_TT_OK,
|
|
"a window allocates");
|
|
if (!window)
|
|
return;
|
|
|
|
rc = ft_tt_tlb_window_aim(ctx, window, FT_TT_ARC_NOC0_X, FT_TT_ARC_NOC0_Y, 0x80000000u);
|
|
check(rc == FT_TT_EPERM, "aiming a window is refused (CONFIGURE_TLB never issued)");
|
|
check(!ft_tt_tlb_window_is_aimed(window), "the window is still aimed nowhere");
|
|
check_u64(ft_tt_tlb_window_base_address(window), 0, "no base address was recorded");
|
|
|
|
check(ft_tt_tlb_window_free(ctx, window) == FT_TT_OK, "the window frees");
|
|
|
|
check(ft_tt_arc_telemetry_read_grid(ctx, &grid) == FT_TT_EPERM,
|
|
"the ARC telemetry reader stops at the fence");
|
|
check(!grid.valid, "no grid is produced");
|
|
}
|
|
|
|
/* ------------------------------------------------------------------------- */
|
|
|
|
int main(void)
|
|
{
|
|
ft_tt_ctx *ctx = NULL;
|
|
|
|
printf("ft_tt Phase 1a device test — READ-ONLY against the card\n\n");
|
|
|
|
test_driver_api_version_without_device();
|
|
|
|
ctx = ft_tt_init_device(0);
|
|
checks_run++;
|
|
if (!ctx) {
|
|
checks_failed++;
|
|
printf(" FAIL ft_tt_init_device(0)\n");
|
|
printf("\n%d/%d checks failed\n", checks_failed, checks_run);
|
|
return 1;
|
|
}
|
|
printf(" ok ft_tt_init_device(0)\n");
|
|
|
|
check(ft_tt_ctx_available(ctx), "context reports available");
|
|
check_u64((uint64_t)ft_tt_ctx_device_index(ctx), 0, "device index");
|
|
check(ft_tt_ctx_fd(ctx) >= 0, "device fd is open");
|
|
|
|
test_device_identity(ctx);
|
|
test_bar_mappings(ctx);
|
|
test_derived_tlb_counts(ctx);
|
|
test_tlb_register_file(ctx);
|
|
test_window_lifecycle(ctx);
|
|
test_4g_window_is_refused(ctx);
|
|
test_window_pool_exhaustion(ctx);
|
|
test_mem_info_is_honest(ctx);
|
|
test_noc_fence_is_closed(ctx);
|
|
|
|
ft_tt_ctx_free(ctx);
|
|
|
|
printf("\n%d/%d checks failed\n", checks_failed, checks_run);
|
|
return checks_failed == 0 ? 0 : 1;
|
|
}
|