/* SPDX-License-Identifier: AGPL-3.0-or-later */ /* Copyright (C) 2026 David (Corey) Herrera */ /** * @file test_udrv_card_noc.c * @brief ON-SILICON bring-up for the userspace driver's NoC TRANSPORT — driver * step 5. Windows we aim ourselves, apertures we map ourselves, bytes moved * with no ioctl anywhere in the data path, and tt-kmd used as the oracle for * every one of them WHILE IT IS STILL HERE TO BE ONE. * * Three questions this file exists to answer, in the order they are asked: * * 1. Does a window WE aimed reach the same endpoint as one tt-kmd aimed? * Byte comparison through two apertures, and a discrimination arm that * must fail, because a wrong aim is a wrong ADDRESS and not an error. * 2. DOES A WRITE-COMBINED STORE REACH THE CARD? Nothing has ever written * through `resource0_wc` — mapping it is not using it — and it is the * bulk upload path, so this is the single most important new fact of the * round. Answered twice, at two different levels, and BOTH read back * through a path that is not the one that wrote them. * 3. `stream_header` and the 3-bit `static_vc` cannot be expressed by * CONFIGURE_TLB, so the driver cannot oracle them and they have never * been swept. Swept here at the register level, where the sweep is free * of risk, and functionally only where an oracle exists. * * WHY THE SWEEP IS SAFE. Configuring a window emits NO NoC transaction; only * touching its 2 MiB aperture does. Every register-level arm below therefore * writes configurations whose apertures are never mapped, which is what makes * it safe to put unreviewed bits in a NoC routing field at all. The arms that * DO dereference an aperture use only endpoints derived from live ARC * telemetry through ft_tt_coord, and only cores this process has claimed. * * WHY A BAD AIM IS DETECTABLE AND NOT MERELY UNLIKELY. `ft_tt_udrv_noc_aim()` * reads its own register triple back and refuses, with nothing dereferenced, * unless all three words are bit-identical to the intent. This file goes one * step further while it can: it re-reads the same triple through TT-KMD's * BAR0 mapping — a different file, a different vma, a different kernel * function — and refuses to touch the aperture unless that agrees too. That * second reader disappears with the module, which is exactly why the * comparison has to be made on this machine. * * @status Runs on the p150a at 0000:12:00.0, with tt-kmd * loaded. Needs root (the BAR sysfs files are 0600 root:root). * @issues * - The functional static_vc arm covers VC 0 only. See stage_static_vc_sweep(). * @todo None */ #define _POSIX_C_SOURCE 200809L #define _FILE_OFFSET_BITS 64 #include #include #include #include #include #include #include #include #include #include #include #include "ft_tt_coord.h" #include "ft_tt_hardware_session.h" #include "ft_tt_kmd_abi.h" #include "ft_tt_memmap.h" #include "ft_tt_noc.h" #include "ft_tt_test_report.h" #include "ft_tt_udrv.h" #include "ft_tt_udrv_noc.h" #include "ft_tt_udrv_pci_sysfs.h" #include "ft_tt_udrv_tlb_encode.h" #define CHARDEV_PATH "/dev/tenstorrent/0" /** The window tt-kmd claims for itself. */ #define KERNEL_WINDOW_ID (FT_TT_UDRV_TLB_2M_WINDOW_COUNT - 1u) /** ARC tile, TRANSLATED. The one coordinate live hardware has confirmed, and * the only tile the register-level arms ever name. */ #define ARC_X 8u #define ARC_Y 0u #define ARC_CSM_BASE 0x10000000ull #define ARC_SCRATCH_BASE 0x80030400ull #define WINDOW_PAGE(addr) ((addr) & ~((uint64_t)FT_TT_UDRV_TLB_2M_WINDOW_SIZE - 1u)) /** Two 64 KiB scratch regions in L1, far above FT_TT_MEM_MAP_END (0x9F80) so * nothing firmware owns is in range, and far below 1536 KiB so the span is * wholly inside L1. */ #define L1_SCRATCH_WC 0x00040000ull #define L1_SCRATCH_UC 0x00060000ull #define PAYLOAD_BYTES (64u * 1024u) /** A DRAM span deliberately chosen to STRADDLE a 2 MiB window boundary, so the * window walk is exercised on silicon and not only in the off-hardware gate. * 64 B aligned, which is the DRAM read alignment. */ #define DRAM_SCRATCH_BASE 0x101FF000ull #define DRAM_SPAN_BYTES (16u * 1024u) /** How many windows this run claims through tt-kmd's arbitrator. */ #define CLAIMED_WINDOW_COUNT 6u /* ------------------------------------------------------------------------ * * Session state * ------------------------------------------------------------------------ */ static ft_tt_udrv_pci_device card; static ft_tt_udrv_noc_device udrv; static bool udrv_open; static int chardev = -1; static const volatile uint32_t *driver_registers; /**< tt-kmd's own BAR0 mapping */ static ft_tt_hardware_session session; static bool session_open; static uint16_t core_a_x, core_a_y; static uint16_t core_b_x, core_b_y; static uint32_t claimed[CLAIMED_WINDOW_COUNT]; static size_t claimed_count; /** Windows this run uses, taken from `claimed` once it is populated. */ static uint32_t window_ours_wc; static uint32_t window_ours_uc; static uint32_t window_theirs; static uint32_t window_register_probe; static uint32_t window_sweep; /* ------------------------------------------------------------------------ * * The fixture pattern — non-periodic, and checked to be * ------------------------------------------------------------------------ */ /** * A pattern that repeats at any power of two would make every byte comparison * below vacuous: an upload that landed one block late, or in the wrong 4 KiB * page, would still compare equal. That exact failure was found here once with * a 256-periodic fixture, so the generator is an LCG over the ABSOLUTE offset * and its non-periodicity is a check in `test_noc_copy.c`, not a comment. */ static unsigned char pattern_byte(size_t offset, uint32_t salt) { uint32_t state = (uint32_t)(offset + 1u) * 2654435761u + salt; state ^= state >> 15; state *= 2246822519u; state ^= state >> 13; return (unsigned char)(state & 0xFFu); } static void fill_pattern(unsigned char *buffer, size_t bytes, uint32_t salt) { size_t index; for (index = 0u; index < bytes; index++) buffer[index] = pattern_byte(index, salt); } static double seconds_since(const struct timespec *start) { struct timespec now; (void)clock_gettime(CLOCK_MONOTONIC, &now); return (double)(now.tv_sec - start->tv_sec) + (double)(now.tv_nsec - start->tv_nsec) / 1e9; } /* ------------------------------------------------------------------------ * * tt-kmd, as arbitrator and as second reader * ------------------------------------------------------------------------ */ static int allocate_window(uint32_t *out_id) { struct ft_tt_kmd_allocate_window_request request; memset(&request, 0, sizeof(request)); request.in.size = (uint64_t)FT_TT_UDRV_TLB_2M_WINDOW_SIZE; if (ioctl(chardev, FT_TT_KMD_IOCTL_ALLOCATE_WINDOW, &request) != 0) return FT_TT_UDRV_EIO; *out_id = request.out.id; return FT_TT_UDRV_OK; } static int free_window(uint32_t id) { struct ft_tt_kmd_free_window_request request; memset(&request, 0, sizeof(request)); request.in.id = id; return (ioctl(chardev, FT_TT_KMD_IOCTL_FREE_WINDOW, &request) == 0) ? FT_TT_UDRV_OK : FT_TT_UDRV_EIO; } static int configure_window_via_ioctl(uint32_t id, const ft_tt_udrv_tlb2m_config *config) { struct ft_tt_kmd_configure_window_request request; memset(&request, 0, sizeof(request)); request.in.id = id; request.in.config.addr = config->addr; request.in.config.x_end = config->x_end; request.in.config.y_end = config->y_end; request.in.config.x_start = config->x_start; request.in.config.y_start = config->y_start; request.in.config.noc = config->noc; request.in.config.mcast = config->multicast; request.in.config.ordering = config->ordering; request.in.config.linked = config->linked; request.in.config.static_vc = config->use_static_vc; return (ioctl(chardev, FT_TT_KMD_IOCTL_CONFIGURE_WINDOW, &request) == 0) ? FT_TT_UDRV_OK : FT_TT_UDRV_EIO; } static bool query_driver_bar0_mapping(uint64_t *out_base) { struct { struct ft_tt_kmd_query_mappings_in in; struct ft_tt_kmd_mapping mappings[8]; } query; size_t index; memset(&query, 0, sizeof(query)); query.in.count = 8u; if (ioctl(chardev, FT_TT_KMD_IOCTL_QUERY_MAPPINGS, &query) != 0) return false; for (index = 0u; index < 8u; index++) { if (query.mappings[index].id != FT_TT_KMD_MAPPING_RESOURCE0_UC) continue; *out_base = query.mappings[index].base; return true; } return false; } /** Window `id`'s register triple, read through TT-KMD's BAR0 mapping. */ static void read_triple_via_driver(uint32_t id, uint32_t out_words[3]) { size_t base = (size_t)id * (FT_TT_UDRV_TLB_REG_SIZE / 4u); out_words[0] = driver_registers[base + 0u]; out_words[1] = driver_registers[base + 1u]; out_words[2] = driver_registers[base + 2u]; } /** * Aim one of OUR windows and confirm the aim through the reader that is not * ours, before anything is allowed to dereference the aperture. * * This is the difference between "a wrong aim is unlikely" and "a wrong aim * cannot be dereferenced". Every other check in this file can fail and cost a * red line; reading or writing through a window aimed at an endpoint nobody * chose is what hangs the NoC and costs a walk to the box. */ static bool aim_ours_verified(uint32_t id, const ft_tt_udrv_tlb2m_config *config) { uint32_t intended[3] = {0u, 0u, 0u}; uint32_t through_driver[3] = {0u, 0u, 0u}; int rc; rc = ft_tt_udrv_noc_window_aim(&udrv, id, config); if (rc != FT_TT_UDRV_OK) { ft_tt_test_check_fmt("our aim landed (self-verified readback)", false, "window %u rc=%d", id, rc); return false; } (void)ft_tt_udrv_tlb2m_encode(config, intended); read_triple_via_driver(id, through_driver); if (memcmp(intended, through_driver, sizeof(intended)) != 0) { ft_tt_test_check_fmt("REFUSING the aperture — TT-KMD's mapping does not see our aim", false, "window %u: wanted 0x%08x 0x%08x 0x%08x, holds 0x%08x 0x%08x " "0x%08x", id, intended[0], intended[1], intended[2], through_driver[0], through_driver[1], through_driver[2]); return false; } return true; } /* ------------------------------------------------------------------------ * * Rung 1 — the card, our register mapping, and the second reader * ------------------------------------------------------------------------ */ static bool stage_open_the_card(bool writing) { size_t count = 0u; uint64_t driver_base = 0u; void *driver_mapping; ft_tt_test_step("Rung 1 — the card, BAR0's register file through sysfs, and TT-KMD as the " "second reader"); ft_tt_test_check_fmt("running as root (the BAR files are 0600 root:root)", geteuid() == 0u, "euid=%u", (unsigned)geteuid()); if (ft_tt_udrv_pci_find_devices(NULL, FT_TT_UDRV_PCI_VENDOR_TENSTORRENT, FT_TT_UDRV_PCI_DEVICE_BLACKHOLE, &card, 1u, &count) != FT_TT_UDRV_OK || count == 0u) { ft_tt_test_check("a Blackhole is present in sysfs", false, "none found"); return false; } ft_tt_test_check_fmt("a Blackhole is present in sysfs", true, "%s", card.address); if (ft_tt_udrv_noc_device_open(&card, writing, &udrv) != FT_TT_UDRV_OK) { ft_tt_test_check_fmt("BAR0's TLB register file maps", false, "errno %d (%s)", errno, strerror(errno)); return false; } udrv_open = true; ft_tt_test_check_fmt("BAR0's TLB register file maps, uncached", true, "resource0 + 0x%08X, %s", FT_TT_UDRV_TLB_REGS_START, writing ? "PROT_READ|PROT_WRITE" : "PROT_READ"); chardev = open(CHARDEV_PATH, O_RDWR | O_CLOEXEC); if (chardev < 0) { ft_tt_test_check_fmt("tt-kmd's chardev opens — it is the ORACLE and the way back", false, "%s errno %d", CHARDEV_PATH, errno); return false; } ft_tt_test_check_fmt("tt-kmd's chardev opens — it is the ORACLE and the way back", true, "fd=%d", chardev); if (!query_driver_bar0_mapping(&driver_base)) { ft_tt_test_check("QUERY_MAPPINGS reports BAR0's UC mapping", false, ""); return false; } driver_mapping = mmap(NULL, FT_TT_UDRV_TLB_REGS_LEN, PROT_READ, MAP_SHARED, chardev, (off_t)(driver_base + FT_TT_UDRV_TLB_REGS_START)); if (driver_mapping == MAP_FAILED) { ft_tt_test_check_fmt("the same registers map through TT-KMD's own BAR0 path", false, "errno %d", errno); return false; } driver_registers = driver_mapping; ft_tt_test_check("the same registers map through TT-KMD's own BAR0 path", true, "map_pci_bar/vm_iomap_memory, not pci_mmap_resource"); /* THE SECOND READER IS ONLY A SECOND READER IF IT AGREES. Compare all 606 * configuration words, both mappings, before anything leans on either. */ { size_t disagreements = 0u; size_t all_ones = 0u; uint32_t id; for (id = 0u; id < FT_TT_UDRV_TLB_2M_WINDOW_COUNT; id++) { uint32_t ours[3] = {0u, 0u, 0u}; uint32_t theirs[3] = {0u, 0u, 0u}; (void)ft_tt_udrv_noc_window_read(&udrv, id, ours); read_triple_via_driver(id, theirs); if (memcmp(ours, theirs, sizeof(ours)) != 0) disagreements++; if (ours[0] == 0xFFFFFFFFu && ours[1] == 0xFFFFFFFFu && ours[2] == 0xFFFFFFFFu) all_ones++; } ft_tt_test_check_fmt("our mapping and TT-KMD's read the same 606 words", disagreements == 0u, "%zu of %u windows disagreed", disagreements, FT_TT_UDRV_TLB_2M_WINDOW_COUNT); ft_tt_test_check_fmt("no window reads all-ones — the aperture decodes, it does not float", all_ones == 0u, "%zu of %u", all_ones, FT_TT_UDRV_TLB_2M_WINDOW_COUNT); } return true; } /* ------------------------------------------------------------------------ * * Rung 2 — coordinates from live telemetry, and the cores claimed * ------------------------------------------------------------------------ */ static bool stage_open_the_session(void) { uint32_t indices[2] = {0u, 1u}; uint32_t core_count; ft_tt_test_step("Rung 2 — coordinates DERIVED from live ARC telemetry, and the two cores " "claimed"); if (ft_tt_hardware_session_open(&session) != FT_TT_OK) { ft_tt_test_check("the tt-kmd session opens — it is the oracle transport", false, ""); return false; } session_open = true; /* The session now honours FT_TT_DRIVER, and this suite's whole value is that * the readback comes through a mapping that is NOT the one under test. An * environment that redirected the oracle onto the userspace driver would make * every comparison here self-instrumenting and every check pass for free. */ if (!ft_tt_hardware_session_kmd_context(&session)) { ft_tt_test_check("the ORACLE session is on tt-kmd — FT_TT_DRIVER must not redirect it", false, "the second reader would be the driver under test"); return false; } core_count = ft_tt_coord_tensix_core_count(&session.grid); ft_tt_test_check_fmt("the grid reports its own core count — nothing is sized from 140", core_count >= 2u, "%u enabled Tensix", core_count); if (ft_tt_hardware_session_claim_cores(&session, indices, 2u, "udrv noc transport bring-up", NULL, NULL) != FT_TT_OK) { ft_tt_test_check("cores 0 and 1 are claimed exclusively", false, "another process holds one; see stderr"); return false; } ft_tt_test_check("cores 0 and 1 are claimed exclusively", true, ""); if (ft_tt_hardware_session_tensix(&session, 0u, &core_a_x, &core_a_y) != FT_TT_OK || ft_tt_hardware_session_tensix(&session, 1u, &core_b_x, &core_b_y) != FT_TT_OK) { ft_tt_test_check("two TRANSLATED Tensix coordinates resolve", false, ""); return false; } ft_tt_test_check_fmt("two TRANSLATED Tensix coordinates resolve, and they are DIFFERENT — the " "coordinate-sensitivity arms need that", !(core_a_x == core_b_x && core_a_y == core_b_y), "A=(%u,%u) B=(%u,%u)", core_a_x, core_a_y, core_b_x, core_b_y); return true; } static bool stage_claim_windows(void) { size_t index; ft_tt_test_step("Rung 2b — claim %u windows (pure arbitration; nothing reaches the card)", CLAIMED_WINDOW_COUNT); for (index = 0u; index < CLAIMED_WINDOW_COUNT; index++) { uint32_t id = 0u; if (allocate_window(&id) != FT_TT_UDRV_OK) break; claimed[claimed_count++] = id; } ft_tt_test_check_fmt("every window was granted", claimed_count == CLAIMED_WINDOW_COUNT, "%zu of %u", claimed_count, CLAIMED_WINDOW_COUNT); if (claimed_count < CLAIMED_WINDOW_COUNT) return false; window_ours_wc = claimed[0]; window_ours_uc = claimed[1]; window_theirs = claimed[2]; window_register_probe = claimed[3]; window_sweep = claimed[4]; ft_tt_test_check_fmt("none of them is tt-kmd's own window", claimed[claimed_count - 1u] != KERNEL_WINDOW_ID, "kernel window %u; ours %u %u %u %u %u %u", KERNEL_WINDOW_ID, claimed[0], claimed[1], claimed[2], claimed[3], claimed[4], claimed[5]); return true; } /* ------------------------------------------------------------------------ * * Rung 3 — a window WE aimed reads what one TT-KMD aimed * ------------------------------------------------------------------------ */ /** * Read the same 4 KiB of the ARC CSM through a window we aimed with our own * register writes and a window tt-kmd aimed through CONFIGURE_TLB. Byte * equality, then a discrimination arm that MUST fail. * * The ARC CSM is chosen because it is an address tt-kmd itself reads on that * tile every time anything touches telemetry, so neither aperture is pointed * anywhere the driver does not already point. */ static void stage_read_agreement(void) { ft_tt_udrv_tlb2m_config aim; ft_tt_udrv_tlb2m_config elsewhere; ft_tt_udrv_noc_path path; unsigned char ours[4096]; unsigned char theirs[4096]; unsigned char other_page[4096]; volatile unsigned char *their_aperture; uint64_t theirs_mmap = 0u; ft_tt_test_step("Rung 3 — a window WE aimed reads the same bytes as one TT-KMD aimed"); memset(&aim, 0, sizeof(aim)); aim.addr = WINDOW_PAGE(ARC_CSM_BASE); aim.x_end = ARC_X; aim.y_end = ARC_Y; aim.ordering = 1u; if (!aim_ours_verified(window_ours_uc, &aim)) return; ft_tt_test_check("our aim landed, and TT-KMD's mapping agrees bit for bit", true, "the aperture may now be dereferenced"); if (ft_tt_udrv_noc_path_open(&udrv, window_ours_uc, false, &path) != FT_TT_UDRV_OK) { ft_tt_test_check_fmt("our aperture maps", false, "errno %d", errno); return; } memcpy(ours, (const void *)path.aperture, sizeof(ours)); /* Theirs: tt-kmd's ioctl aim, and tt-kmd's own mmap cookie. A different * file, a different kernel path, a different vma. The cookie is * QUERY_MAPPINGS' BAR0 base plus id * 2 MiB, which is the same page * ALLOCATE_TLB's mmap_offset_uc names. */ { uint64_t driver_base = 0u; if (!query_driver_bar0_mapping(&driver_base)) { ft_tt_test_check("QUERY_MAPPINGS still answers", false, ""); (void)ft_tt_udrv_noc_path_close(&path); return; } theirs_mmap = driver_base + (uint64_t)window_theirs * FT_TT_UDRV_TLB_2M_WINDOW_SIZE; } (void)configure_window_via_ioctl(window_theirs, &aim); their_aperture = mmap(NULL, (size_t)FT_TT_UDRV_TLB_2M_WINDOW_SIZE, PROT_READ, MAP_SHARED, chardev, (off_t)theirs_mmap); if (their_aperture == MAP_FAILED) { ft_tt_test_check_fmt("tt-kmd's aperture maps", false, "errno %d", errno); (void)ft_tt_udrv_noc_path_close(&path); return; } memcpy(theirs, (const void *)their_aperture, sizeof(theirs)); ft_tt_test_check_fmt("the two apertures return identical bytes", memcmp(ours, theirs, sizeof(ours)) == 0, "%zu bytes", sizeof(ours)); /* THE DISCRIMINATION ARM. Re-aim THEIR window at the ARC scratch page — a * different 2 MiB page of the same tile, and an address tt-kmd itself * reads. The bytes must change; if they do not, the equality above was * measuring nothing. */ memset(&elsewhere, 0, sizeof(elsewhere)); elsewhere.addr = WINDOW_PAGE(ARC_SCRATCH_BASE); elsewhere.x_end = ARC_X; elsewhere.y_end = ARC_Y; elsewhere.ordering = 1u; (void)configure_window_via_ioctl(window_theirs, &elsewhere); memcpy(other_page, (const void *)their_aperture, sizeof(other_page)); ft_tt_test_check("a DIFFERENT page of the same tile returns DIFFERENT bytes — the comparison " "above can say no", memcmp(ours, other_page, sizeof(ours)) != 0, ""); ft_tt_test_check("two offsets within one page differ too — the readback is address-sensitive " "below window granularity", memcmp((const void *)path.aperture, (const void *)((const volatile unsigned char *)path.aperture + 4096u), 4096u) != 0, ""); (void)munmap((void *)their_aperture, (size_t)FT_TT_UDRV_TLB_2M_WINDOW_SIZE); (void)ft_tt_udrv_noc_path_close(&path); } /* ------------------------------------------------------------------------ * * Rung 4a — DOES A WRITE-COMBINED STORE REACH THE CARD? * ------------------------------------------------------------------------ */ /** * The narrowest possible form of the question, with zero NoC transactions * involved: store a TLB configuration triple through a WRITE-COMBINED mapping * of `resource0_wc` and read it back through TT-KMD's uncached BAR0 mapping. * * The target is a configuration register of a window this process owns and * whose aperture is never mapped, so nothing can be emitted whatever the bits * say. If this passes, a WC store reaches the ASIC. If it fails, every WC * upload measurement this project has ever taken went through tt-kmd's mapping * and says nothing about ours. * * The values written are ENCODED CONFIGURATIONS, not arbitrary bit patterns: * three of them, chosen so that between them all three words change, and so * that a stale readback of any one is distinguishable from a correct readback * of another. */ static void stage_wc_store_reaches_the_card(void) { ft_tt_udrv_tlb2m_config configs[3]; void *wc_mapping = NULL; volatile uint32_t *wc_registers; uint32_t before[3] = {0u, 0u, 0u}; size_t base = (size_t)window_register_probe * (FT_TT_UDRV_TLB_REG_SIZE / 4u); size_t index; size_t landed = 0u; bool distinct = true; ft_tt_test_step("Rung 4a — A WRITE-COMBINED STORE, read back through TT-KMD's mapping. " "Nothing has ever written through resource0_wc"); if (ft_tt_udrv_pci_map_bar(&card, 0u, true, true, FT_TT_UDRV_TLB_REGS_START, FT_TT_UDRV_TLB_REGS_LEN, &wc_mapping) != FT_TT_UDRV_OK) { ft_tt_test_check_fmt("resource0_wc maps read-WRITE", false, "errno %d (%s)", errno, strerror(errno)); return; } wc_registers = wc_mapping; ft_tt_test_check("resource0_wc maps read-WRITE", true, "pci_mmap_resource with pgprot_writecombine"); memset(configs, 0, sizeof(configs)); configs[0].addr = WINDOW_PAGE(ARC_CSM_BASE); configs[0].x_end = ARC_X; configs[0].y_end = ARC_Y; configs[1].addr = WINDOW_PAGE(ARC_SCRATCH_BASE); configs[1].x_end = ARC_X; configs[1].y_end = ARC_Y; configs[1].ordering = 1u; configs[2].addr = WINDOW_PAGE(ARC_CSM_BASE); configs[2].x_end = ARC_X; configs[2].y_end = ARC_Y; configs[2].ordering = 3u; configs[2].linked = 1u; configs[2].use_static_vc = 1u; read_triple_via_driver(window_register_probe, before); for (index = 0u; index < 3u; index++) { uint32_t intended[3] = {0u, 0u, 0u}; uint32_t observed[3] = {0u, 0u, 0u}; (void)ft_tt_udrv_tlb2m_encode(&configs[index], intended); /* A readback that equals what was ALREADY there proves nothing. Refuse * to count this arm unless the value is actually changing. */ read_triple_via_driver(window_register_probe, observed); if (memcmp(intended, observed, sizeof(intended)) == 0) distinct = false; wc_registers[base + 0u] = intended[0]; wc_registers[base + 1u] = intended[1]; wc_registers[base + 2u] = intended[2]; ft_tt_noc_copy_store_fence(); read_triple_via_driver(window_register_probe, observed); if (memcmp(intended, observed, sizeof(intended)) == 0) { landed++; } else { ft_tt_test_check_fmt("a WC store lands", false, "config %zu: wrote 0x%08x 0x%08x 0x%08x, TT-KMD reads 0x%08x " "0x%08x 0x%08x", index, intended[0], intended[1], intended[2], observed[0], observed[1], observed[2]); } } ft_tt_test_check_fmt("A WRITE-COMBINED STORE REACHES THE CARD — read back through TT-KMD's " "own BAR0 mapping, which is not the path that wrote it", landed == 3u, "%zu of 3 configurations", landed); ft_tt_test_check("...and every one of them CHANGED the register, so the readback is not " "agreeing with what was already there", distinct, "if this fails the arm above is vacuous"); /* Leave the probe window aimed somewhere harmless. */ { ft_tt_udrv_tlb2m_config benign; memset(&benign, 0, sizeof(benign)); benign.addr = WINDOW_PAGE(ARC_CSM_BASE); benign.x_end = ARC_X; benign.y_end = ARC_Y; benign.ordering = 1u; (void)configure_window_via_ioctl(window_register_probe, &benign); } (void)ft_tt_udrv_pci_unmap_bar(wc_mapping, FT_TT_UDRV_TLB_REGS_LEN); } /* ------------------------------------------------------------------------ * * Rung 4b — the bulk upload path: a WC store through an APERTURE * ------------------------------------------------------------------------ */ /** * The question rung 4a cannot answer: a WC store through a window's APERTURE, * which becomes a NoC write transaction, into a real endpoint — and read back * through TT-KMD's transport, which is a different window, a different mapping * and a different aiming mechanism end to end. * * @param write_combine which cache mode the upload path uses * @param address L1 scratch address for this arm * @param salt makes this arm's pattern different from every other's */ static void upload_and_verify(uint32_t window, bool write_combine, uint64_t address, uint32_t salt, const char *label) { ft_tt_udrv_noc_path path; ft_tt_udrv_tlb2m_config aim; ft_tt_noc_coord core_a; ft_tt_noc_coord core_b; unsigned char *sent = malloc(PAYLOAD_BYTES); unsigned char *received = malloc(PAYLOAD_BYTES); unsigned char *elsewhere = malloc(PAYLOAD_BYTES); struct timespec start; double elapsed; int rc; if (!sent || !received || !elsewhere) { ft_tt_test_check_fmt("allocation", false, "%s", label); goto done; } core_a.x = core_a_x; core_a.y = core_a_y; core_b.x = core_b_x; core_b.y = core_b_y; fill_pattern(sent, PAYLOAD_BYTES, salt); /* THE PRE-ARM. Read the target through the ORACLE before writing anything: * if it already held the pattern, the comparison afterwards would be a * statement about nothing. */ memset(received, 0, PAYLOAD_BYTES); rc = ft_tt_read_l1(&session.runtime.binding.path, core_a, address, received, PAYLOAD_BYTES); ft_tt_test_check_fmt("the oracle can read the target before we write it", rc == FT_TT_OK, "%s, rc=%d", label, rc); ft_tt_test_check_fmt("...and it does NOT already hold our pattern", memcmp(sent, received, PAYLOAD_BYTES) != 0, "%s", label); if (ft_tt_udrv_noc_path_open(&udrv, window, write_combine, &path) != FT_TT_UDRV_OK) { ft_tt_test_check_fmt("our aperture maps", false, "%s errno %d", label, errno); goto done; } /* Confirm the aim through TT-KMD before a single byte is stored. The * transport verifies it too, but only against the mapping that wrote it. */ memset(&aim, 0, sizeof(aim)); aim.addr = WINDOW_PAGE(address); aim.x_end = (uint8_t)core_a_x; aim.y_end = (uint8_t)core_a_y; if (!aim_ours_verified(window, &aim)) { (void)ft_tt_udrv_noc_path_close(&path); goto done; } (void)clock_gettime(CLOCK_MONOTONIC, &start); rc = ft_tt_udrv_noc_transfer(&path, core_a_x, core_a_y, address, sent, PAYLOAD_BYTES, false, FT_TT_NOC_WIDTH_BLOCK16); elapsed = seconds_since(&start); ft_tt_test_check_fmt("the upload returns OK", rc == FT_TT_UDRV_OK, "%s, rc=%d", label, rc); ft_tt_test_check_fmt("...no aim was refused", ft_tt_udrv_noc_path_aim_failures(&path) == 0u, "%s, %llu aims, %llu refused", label, (unsigned long long)ft_tt_udrv_noc_path_aim_count(&path), (unsigned long long)ft_tt_udrv_noc_path_aim_failures(&path)); /* THE READBACK, THROUGH A PATH THAT IS NOT THE ONE THAT WROTE IT: tt-kmd's * window, tt-kmd's mapping, tt-kmd's aim. */ memset(received, 0, PAYLOAD_BYTES); rc = ft_tt_read_l1(&session.runtime.binding.path, core_a, address, received, PAYLOAD_BYTES); ft_tt_test_check_fmt("the oracle reads it back", rc == FT_TT_OK, "%s, rc=%d", label, rc); ft_tt_test_check_fmt("A STORE THROUGH OUR OWN APERTURE REACHED THE CARD — byte-exact through " "TT-KMD's transport, which is not the path that wrote it", memcmp(sent, received, PAYLOAD_BYTES) == 0, "%s, %u bytes, %.1f MB/s " "host->card", label, PAYLOAD_BYTES, elapsed > 0.0 ? ((double)PAYLOAD_BYTES / elapsed) / 1e6 : 0.0); /* ADDRESS SENSITIVITY: the next 64 KiB of the same core must NOT hold it. */ memset(elsewhere, 0, PAYLOAD_BYTES); (void)ft_tt_read_l1(&session.runtime.binding.path, core_a, address + PAYLOAD_BYTES, elsewhere, PAYLOAD_BYTES); ft_tt_test_check_fmt("...and the NEXT 64 KiB of the same core does not — the write went where " "it was aimed, not everywhere", memcmp(sent, elsewhere, PAYLOAD_BYTES) != 0, "%s", label); /* COORDINATE SENSITIVITY: the same address on the other claimed core must * not hold it either. A window aimed at the wrong tile is the failure this * catches, and it is silent by construction. */ memset(elsewhere, 0, PAYLOAD_BYTES); (void)ft_tt_read_l1(&session.runtime.binding.path, core_b, address, elsewhere, PAYLOAD_BYTES); ft_tt_test_check_fmt("...nor does the SAME address on a different core", memcmp(sent, elsewhere, PAYLOAD_BYTES) != 0, "%s, A=(%u,%u) B=(%u,%u)", label, core_a_x, core_a_y, core_b_x, core_b_y); /* AND IT CAN CHANGE. A second upload with a different pattern must be * visible; otherwise the first could have been a coincidence of whatever * was in L1. */ fill_pattern(sent, PAYLOAD_BYTES, salt + 977u); (void)ft_tt_udrv_noc_transfer(&path, core_a_x, core_a_y, address, sent, PAYLOAD_BYTES, false, FT_TT_NOC_WIDTH_BLOCK16); memset(received, 0, PAYLOAD_BYTES); (void)ft_tt_read_l1(&session.runtime.binding.path, core_a, address, received, PAYLOAD_BYTES); ft_tt_test_check_fmt("...a SECOND upload with a different pattern replaces it", memcmp(sent, received, PAYLOAD_BYTES) == 0, "%s", label); /* And the transport reads its own writes back — the weakest of the checks, * recorded because it is the only one the target board will still have. */ memset(received, 0, PAYLOAD_BYTES); rc = ft_tt_udrv_noc_transfer(&path, core_a_x, core_a_y, address, received, PAYLOAD_BYTES, true, FT_TT_NOC_WIDTH_BLOCK16); ft_tt_test_check_fmt("...and OUR OWN transport reads it back too (self-instrumenting, and " "recorded as such)", rc == FT_TT_UDRV_OK && memcmp(sent, received, PAYLOAD_BYTES) == 0, "%s", label); (void)ft_tt_udrv_noc_path_close(&path); done: free(sent); free(received); free(elsewhere); } static void stage_the_upload_path(void) { ft_tt_test_step("Rung 4b — the BULK UPLOAD PATH: a store through an aperture we mapped, " "read back through TT-KMD's transport"); upload_and_verify(window_ours_wc, true, L1_SCRATCH_WC, 0x5A5Au, "WC aperture"); ft_tt_test_step("Rung 4c — the same thing uncached, so the two cache modes are compared " "rather than assumed"); upload_and_verify(window_ours_uc, false, L1_SCRATCH_UC, 0xA5A5u, "UC aperture"); } /* ------------------------------------------------------------------------ * * Rung 4d — the window walk on silicon, and the re-aim fast path * ------------------------------------------------------------------------ */ /** * L1 is 1536 KiB, so every L1 transfer fits one 2 MiB window and the walk is * never exercised by rung 4b. DRAM is 4 GiB per bank, so a span deliberately * placed across a 2 MiB boundary makes the walk move the window mid-transfer — * which is the code path that, when wrong, writes to the right endpoint at the * wrong offset rather than failing. */ static void stage_the_window_walk(void) { ft_tt_udrv_noc_path path; ft_tt_noc_coord bank; unsigned char *sent = malloc(DRAM_SPAN_BYTES); unsigned char *received = malloc(DRAM_SPAN_BYTES); uint64_t before; uint64_t after; int rc; ft_tt_test_step("Rung 4d — a transfer that STRADDLES a 2 MiB boundary, so the window really " "moves mid-copy"); if (!sent || !received) { ft_tt_test_check("allocation", false, ""); goto done; } if (ft_tt_coord_dram_bank(&session.grid, 0u, FT_TT_COORD_TRANSLATED, &bank) != FT_TT_OK) { ft_tt_test_check("a TRANSLATED DRAM bank coordinate resolves", false, ""); goto done; } ft_tt_test_check_fmt("a TRANSLATED DRAM bank coordinate resolves", true, "bank 0 at (%u,%u)", bank.x, bank.y); ft_tt_test_check_fmt("the span really does straddle a boundary", WINDOW_PAGE(DRAM_SCRATCH_BASE) != WINDOW_PAGE(DRAM_SCRATCH_BASE + DRAM_SPAN_BYTES - 1u), "0x%llx .. 0x%llx", (unsigned long long)DRAM_SCRATCH_BASE, (unsigned long long)(DRAM_SCRATCH_BASE + DRAM_SPAN_BYTES)); if (ft_tt_udrv_noc_path_open(&udrv, window_ours_wc, true, &path) != FT_TT_UDRV_OK) { ft_tt_test_check_fmt("our aperture maps", false, "errno %d", errno); goto done; } fill_pattern(sent, DRAM_SPAN_BYTES, 0x1234u); before = ft_tt_udrv_noc_path_aim_count(&path); rc = ft_tt_udrv_noc_transfer(&path, (uint16_t)bank.x, (uint16_t)bank.y, DRAM_SCRATCH_BASE, sent, DRAM_SPAN_BYTES, false, FT_TT_NOC_WIDTH_BLOCK16); after = ft_tt_udrv_noc_path_aim_count(&path); ft_tt_test_check_fmt("the straddling write returns OK", rc == FT_TT_UDRV_OK, "rc=%d", rc); ft_tt_test_check_fmt("...and it aimed the window TWICE, which is the walk actually walking", after - before == 2u, "%llu aims", (unsigned long long)(after - before)); memset(received, 0, DRAM_SPAN_BYTES); rc = ft_tt_read_dram(&session.runtime.binding.path, bank, DRAM_SCRATCH_BASE, received, DRAM_SPAN_BYTES); ft_tt_test_check_fmt("the oracle reads the whole span back byte-exact ACROSS the boundary", rc == FT_TT_OK && memcmp(sent, received, DRAM_SPAN_BYTES) == 0, "rc=%d", rc); /* THE RE-AIM FAST PATH, which is the dangerous optimisation in the walk: a * transfer to an endpoint the window already points at must cost zero aims, * and a transfer to a different endpoint must cost one. */ before = ft_tt_udrv_noc_path_aim_count(&path); (void)ft_tt_udrv_noc_transfer(&path, (uint16_t)bank.x, (uint16_t)bank.y, DRAM_SCRATCH_BASE + (uint64_t)FT_TT_UDRV_TLB_2M_WINDOW_SIZE, sent, 64u, false, FT_TT_NOC_WIDTH_BLOCK16); after = ft_tt_udrv_noc_path_aim_count(&path); ft_tt_test_check_fmt("a transfer inside the page the window already holds costs ZERO aims", after == before, "%llu", (unsigned long long)(after - before)); before = after; (void)ft_tt_udrv_noc_transfer(&path, core_a_x, core_a_y, L1_SCRATCH_WC, sent, 64u, false, FT_TT_NOC_WIDTH_BLOCK16); after = ft_tt_udrv_noc_path_aim_count(&path); ft_tt_test_check_fmt("...and moving to a different TILE costs exactly one", after - before == 1u, "%llu", (unsigned long long)(after - before)); ft_tt_test_check_fmt("no aim was ever refused on this path", ft_tt_udrv_noc_path_aim_failures(&path) == 0u, "%llu", (unsigned long long)ft_tt_udrv_noc_path_aim_failures(&path)); /* Put back what rung 4b left at L1_SCRATCH_WC, so a later arm reading it is * not confused by these 64 bytes. Nothing depends on it; it is hygiene. */ (void)ft_tt_udrv_noc_path_close(&path); done: free(sent); free(received); } /* ------------------------------------------------------------------------ * * Rung 5 — stream_header and static_vc, which the ioctl cannot express * ------------------------------------------------------------------------ */ /** * The two fields CONFIGURE_TLB has no way to set. tt-kmd folds the ioctl's * `static_vc` flag into the register's `use_static_vc` bit and never touches * `stream_header` or the 3-bit `static_vc` id at all — so the driver cannot be * an oracle for them, they have never been swept, and only our own register * writes can reach them. * * WHAT IS SWEPT HERE, and it is a REGISTER-LEVEL sweep on purpose: every one of * the 32 combinations of `stream_header` (2) x `static_vc` (8) x * `use_static_vc` (2), written by us and read back through TT-KMD's BAR0 * mapping. That answers the question that has to be answered first — whether * the hardware IMPLEMENTS those bits at all, or reads them back as zero — and * it emits no NoC transaction, because the swept window's aperture is never * mapped. * * WHAT IS NOT SWEPT, and why. A FUNCTIONAL arm for a non-zero `static_vc` * forces every transaction of that window onto one NoC virtual channel. Nothing * in tt-kmd, and nothing this project has read of tt-umd, ever sets a non-zero * static VC on a host TLB, so there is no oracle and no source that bounds what * a VC reserved for another traffic class does to the fabric. A deadlocked NoC * costs a power cycle and a walk to the box. So exactly one functional arm runs: * `use_static_vc = 1, static_vc = 0`, which is the configuration tt-kmd's own * ioctl produces when a caller passes `static_vc = 1`. Everything above VC 0 * stays register-level, and that is recorded as unproven rather than skipped * quietly. */ static void stage_static_vc_sweep(void) { uint32_t seen_words[32][3]; size_t combination = 0u; size_t disagreements = 0u; size_t stream_header_held = 0u; size_t static_vc_held = 0u; size_t duplicates = 0u; unsigned use_vc; unsigned header; unsigned vc; size_t left; size_t right; ft_tt_test_step("Rung 5 — stream_header and the 3-bit static_vc: 32 combinations " "CONFIGURE_TLB cannot express, read back through TT-KMD's mapping"); for (use_vc = 0u; use_vc <= 1u; use_vc++) { for (header = 0u; header <= 1u; header++) { for (vc = 0u; vc <= 7u; vc++) { ft_tt_udrv_tlb2m_config config; ft_tt_udrv_tlb2m_config decoded; uint32_t intended[3] = {0u, 0u, 0u}; uint32_t observed[3] = {0u, 0u, 0u}; memset(&config, 0, sizeof(config)); config.addr = WINDOW_PAGE(ARC_CSM_BASE); config.x_end = ARC_X; config.y_end = ARC_Y; config.ordering = 1u; config.use_static_vc = (uint8_t)use_vc; config.stream_header = (uint8_t)header; config.static_vc_id = (uint8_t)vc; if (ft_tt_udrv_noc_window_aim(&udrv, window_sweep, &config) != FT_TT_UDRV_OK) { ft_tt_test_check_fmt("our aim self-verifies", false, "use_vc=%u sh=%u vc=%u", use_vc, header, vc); disagreements++; combination++; continue; } (void)ft_tt_udrv_tlb2m_encode(&config, intended); read_triple_via_driver(window_sweep, observed); memcpy(seen_words[combination], observed, sizeof(observed)); if (memcmp(intended, observed, sizeof(intended)) != 0) { ft_tt_test_check_fmt("the register holds what we encoded", false, "use_vc=%u sh=%u vc=%u: ours 0x%08x 0x%08x 0x%08x, card " "0x%08x 0x%08x 0x%08x", use_vc, header, vc, intended[0], intended[1], intended[2], observed[0], observed[1], observed[2]); disagreements++; } else { /* Decode what the CARD holds, not what we sent, so "the bit * is retained" is a statement about the hardware. */ (void)ft_tt_udrv_tlb2m_decode(observed, &decoded); if (decoded.stream_header == (uint8_t)header) stream_header_held++; if (decoded.static_vc_id == (uint8_t)vc) static_vc_held++; } combination++; } } } ft_tt_test_check_fmt("all 32 combinations read back byte-identical through TT-KMD's mapping", disagreements == 0u, "%zu disagreed", disagreements); ft_tt_test_check_fmt("THE HARDWARE RETAINS stream_header — it is not a reserved bit that " "reads back zero", stream_header_held == combination, "%zu of %zu", stream_header_held, combination); ft_tt_test_check_fmt("THE HARDWARE RETAINS the 3-bit static_vc, all eight values", static_vc_held == combination, "%zu of %zu", static_vc_held, combination); /* THE ANTI-VACUITY ARM. If every combination produced the same three words, * the three checks above would pass while proving nothing. */ for (left = 0u; left < combination; left++) { for (right = left + 1u; right < combination; right++) { if (memcmp(seen_words[left], seen_words[right], sizeof(seen_words[left])) == 0) duplicates++; } } ft_tt_test_check_fmt("no two combinations produced the same register words — the sweep is " "actually sweeping", duplicates == 0u, "%zu duplicate pairs of %zu", duplicates, combination * (combination - 1u) / 2u); /* THE ONE FUNCTIONAL ARM: use_static_vc = 1, VC 0. See this function's * documentation for why it stops there. */ { ft_tt_udrv_noc_path path; ft_tt_noc_coord core_a; unsigned char *sent = malloc(4096u); unsigned char *received = malloc(4096u); core_a.x = core_a_x; core_a.y = core_a_y; if (!sent || !received) { ft_tt_test_check("allocation", false, ""); } else if (ft_tt_udrv_noc_path_open(&udrv, window_ours_uc, false, &path) != FT_TT_UDRV_OK) { ft_tt_test_check_fmt("the static-VC path's aperture maps", false, "errno %d", errno); } else { ft_tt_udrv_tlb2m_config aim; int rc; (void)ft_tt_udrv_noc_path_set_aim_template(&path, 0u, 1u, 0u, 1u, 0u, 0u); memset(&aim, 0, sizeof(aim)); aim.addr = WINDOW_PAGE(L1_SCRATCH_UC); aim.x_end = (uint8_t)core_a_x; aim.y_end = (uint8_t)core_a_y; aim.ordering = 1u; aim.use_static_vc = 1u; if (aim_ours_verified(window_ours_uc, &aim)) { fill_pattern(sent, 4096u, 0x777u); rc = ft_tt_udrv_noc_transfer(&path, core_a_x, core_a_y, L1_SCRATCH_UC, sent, 4096u, false, FT_TT_NOC_WIDTH_BLOCK16); ft_tt_test_check_fmt("a write through a window with use_static_vc=1, VC 0 returns " "OK", rc == FT_TT_UDRV_OK, "rc=%d", rc); memset(received, 0, 4096u); rc = ft_tt_read_l1(&session.runtime.binding.path, core_a, L1_SCRATCH_UC, received, 4096u); ft_tt_test_check_fmt("...and the oracle reads exactly those bytes back, so a " "static VC does not silently drop traffic", rc == FT_TT_OK && memcmp(sent, received, 4096u) == 0, "rc=%d", rc); } (void)ft_tt_udrv_noc_path_close(&path); } free(sent); free(received); } ft_tt_test_check("static_vc 1-7 are proven to be HELD by the register and are NOT proven to " "route — no oracle exists for them and a wrong VC is a fabric hazard", true, "recorded as unproven, deliberately"); } /* ------------------------------------------------------------------------ * * Refusals — the transport must say no before the card is reached * ------------------------------------------------------------------------ */ static void stage_refusals(void) { ft_tt_udrv_noc_path path; ft_tt_udrv_tlb2m_config config; unsigned char scratch[64]; ft_tt_test_step("Refusals — every one of these must fail BEFORE a transaction is emitted"); memset(scratch, 0, sizeof(scratch)); if (ft_tt_udrv_noc_path_open(&udrv, window_ours_uc, false, &path) != FT_TT_UDRV_OK) { ft_tt_test_check("a path opens for the refusal arms", false, ""); return; } ft_tt_test_check_fmt("a coordinate above 63 is refused, not truncated onto a live tile", ft_tt_udrv_noc_transfer(&path, 64u, 0u, L1_SCRATCH_UC, scratch, sizeof(scratch), true, FT_TT_NOC_WIDTH_BLOCK16) == FT_TT_UDRV_EINVAL, "x=64"); ft_tt_test_check("a y above 63 likewise", ft_tt_udrv_noc_transfer(&path, 0u, 64u, L1_SCRATCH_UC, scratch, sizeof(scratch), true, FT_TT_NOC_WIDTH_BLOCK16) == FT_TT_UDRV_EINVAL, ""); ft_tt_test_check("a misaligned address is refused", ft_tt_udrv_noc_transfer(&path, core_a_x, core_a_y, L1_SCRATCH_UC + 1u, scratch, sizeof(scratch), true, FT_TT_NOC_WIDTH_BLOCK16) == FT_TT_UDRV_EINVAL, ""); ft_tt_test_check("a length that is not a multiple of the width is refused", ft_tt_udrv_noc_transfer(&path, core_a_x, core_a_y, L1_SCRATCH_UC, scratch, 17u, true, FT_TT_NOC_WIDTH_BLOCK16) == FT_TT_UDRV_EINVAL, ""); ft_tt_test_check("a zero-length transfer is a legal no-op", ft_tt_udrv_noc_transfer(&path, core_a_x, core_a_y, L1_SCRATCH_UC, scratch, 0u, true, FT_TT_NOC_WIDTH_BLOCK16) == FT_TT_UDRV_OK, ""); memset(&config, 0, sizeof(config)); config.addr = WINDOW_PAGE(ARC_CSM_BASE); config.x_end = 64u; /* does not fit the 6-bit field */ ft_tt_test_check("an out-of-range aim is refused rather than silently truncated", ft_tt_udrv_noc_window_aim(&udrv, window_sweep, &config) == FT_TT_UDRV_EINVAL, "x_end=64"); config.x_end = ARC_X; config.addr = WINDOW_PAGE(ARC_CSM_BASE) + 4096u; /* not 2 MiB aligned */ ft_tt_test_check("a misaligned aim address is refused", ft_tt_udrv_noc_window_aim(&udrv, window_sweep, &config) == FT_TT_UDRV_EINVAL, ""); ft_tt_test_check("an id past the window count is refused", ft_tt_udrv_noc_window_aim(&udrv, FT_TT_UDRV_TLB_2M_WINDOW_COUNT, &config) == FT_TT_UDRV_EINVAL, ""); (void)ft_tt_udrv_noc_path_close(&path); } /* ------------------------------------------------------------------------ * * Release * ------------------------------------------------------------------------ */ static void stage_release(void) { ft_tt_udrv_tlb2m_config benign; size_t failures = 0u; size_t index; ft_tt_test_step("Release — re-aim every claimed window somewhere harmless, then hand it back"); memset(&benign, 0, sizeof(benign)); benign.addr = WINDOW_PAGE(ARC_CSM_BASE); benign.x_end = ARC_X; benign.y_end = ARC_Y; benign.ordering = 1u; for (index = 0u; index < claimed_count; index++) { if (configure_window_via_ioctl(claimed[index], &benign) != FT_TT_UDRV_OK) failures++; } ft_tt_test_check_fmt("every window is aimed at the ARC tile, unicast, no multicast bit and no " "static VC left set anywhere", failures == 0u, "%zu of %zu failed", failures, claimed_count); failures = 0u; for (index = 0u; index < claimed_count; index++) { if (free_window(claimed[index]) != FT_TT_UDRV_OK) failures++; } ft_tt_test_check_fmt("every window is released", failures == 0u, "%zu of %zu failed", failures, claimed_count); claimed_count = 0u; if (driver_registers) { (void)munmap((void *)(uintptr_t)driver_registers, FT_TT_UDRV_TLB_REGS_LEN); driver_registers = NULL; } if (udrv_open) { (void)ft_tt_udrv_noc_device_close(&udrv); udrv_open = false; } if (chardev >= 0) { (void)close(chardev); chardev = -1; } if (session_open) { ft_tt_hardware_session_close(&session); session_open = false; } ft_tt_test_check("mappings unmapped, cores released, chardev closed", true, ""); } /* ------------------------------------------------------------------------ */ static void usage(const char *program) { (void)fprintf(stderr, "usage: %s [read | write]\n" " read rungs 1-3: map, compare both readers, and read through a window we\n" " aimed. WRITES NOTHING TO THE CARD beyond the window aim itself.\n" " write the above, plus the write-combined store arms, the window walk and\n" " the stream_header/static_vc sweep.\n" "Default is `read`.\n", program); } int main(int argc, char **argv) { bool writing = false; if (argc > 2) { usage(argv[0]); return 2; } if (argc == 2) { if (strcmp(argv[1], "write") == 0) { writing = true; } else if (strcmp(argv[1], "read") != 0) { usage(argv[0]); return 2; } } if (!stage_open_the_card(writing)) return ft_tt_test_finish(); if (!stage_open_the_session()) { stage_release(); return ft_tt_test_finish(); } if (!stage_claim_windows()) { stage_release(); return ft_tt_test_finish(); } if (!writing) { ft_tt_test_step("Rungs 3-5 SKIPPED — aiming a window is a WRITE, so the read-only run " "stops here. Run with `write`."); stage_release(); return ft_tt_test_finish(); } stage_read_agreement(); stage_wc_store_reaches_the_card(); stage_the_upload_path(); stage_the_window_walk(); stage_static_vc_sweep(); stage_refusals(); stage_release(); return ft_tt_test_finish(); }