Foundry/tt/tests/ft_tt_hardware_session.c

506 lines
20 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* @file ft_tt_hardware_session.c
* @brief Implementation of the on-card Phase 2 test scaffolding.
*
* @status Runs on the p150a test card, over EITHER transport since
* 2026-08-18: the open goes through `ft_tt_runtime_open_with_driver()` and
* `FT_TT_DRIVER=udrv` runs the on-card suites with no vendor module.
* @issues None
* @todo None
*/
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#include "ft_tt_hardware_session.h"
#include "ft_tt_udrv.h"
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
/** File names of the five firmware images, indexed by FT_TT_PROC_*. */
static const char *const k_firmware_file_names[FT_TT_PROC_COUNT] = {
"fw_brisc.elf", "fw_ncrisc.elf", "fw_trisc0.elf", "fw_trisc1.elf", "fw_trisc2.elf"
};
int ft_tt_hardware_session_open(ft_tt_hardware_session *out_session)
{
ft_tt_runtime_driver driver = FT_TT_RUNTIME_DRIVER_KMD;
int rc;
if (!out_session)
return FT_TT_EINVAL;
/* Filled IN PLACE. The accessors' `user` pointers are into `runtime`, which is
* inside this struct, so a build-then-copy would need the same re-binding the
* runtime removed — and getting that wrong is a wrong window, not a crash. */
memset(out_session, 0, sizeof(*out_session));
rc = ft_tt_runtime_driver_select(&driver, NULL);
if (rc != FT_TT_OK)
return rc; /* already explained: an unrecognised transport is a refusal */
rc = ft_tt_runtime_open_with_driver(0, driver, &out_session->runtime);
if (rc != FT_TT_OK) {
ft_tt_log_error("session: could not open device 0 over %s: %s",
ft_tt_runtime_driver_string(driver), ft_tt_error_string(rc));
return rc;
}
out_session->noc = out_session->runtime.noc;
out_session->grid = out_session->runtime.grid;
/* On stderr, not stdout: an on-card suite's stdout is compared between the two
* transports, and a line naming the transport would be the one guaranteed
* difference in every such comparison. */
ft_tt_log_error("session: card open over %s, %u Tensix in %u columns",
ft_tt_runtime_driver_string(driver), out_session->grid.tensix_core_count,
out_session->grid.tensix_column_count);
return FT_TT_OK;
}
void ft_tt_hardware_session_close(ft_tt_hardware_session *session)
{
unsigned slot;
if (!session)
return;
/* Cores go back first: a claim outliving the window that drove it would name
* a process that can no longer reach the card. */
for (slot = 0; slot < FT_TT_HARDWARE_SESSION_MAX_CLAIMS; slot++) {
ft_tt_tensix_claim_release(session->cores[slot]);
session->cores[slot] = NULL;
}
ft_tt_runtime_close(&session->runtime);
memset(session, 0, sizeof(*session));
}
ft_tt_ctx *ft_tt_hardware_session_kmd_context(const ft_tt_hardware_session *session)
{
if (!session || session->runtime.driver != FT_TT_RUNTIME_DRIVER_KMD)
return NULL;
return session->runtime.ctx;
}
/**
* The card identity claims coordinate under: the PCI BDF, so two cards in one box
* never share a core pool.
*
* Through `ft_tt_runtime_pci_address()`, which answers on BOTH transports. It used
* to go through `ft_tt_device_get_info()` on the tt-kmd context, which is the same
* mistake `ft_tt_core` had: the claim is a flock under /tmp and has never wanted a
* device handle, and routing its NAME through one is what tied it to a chardev.
*
* Falls back to NULL — which lets ft_tt_tensix_claim read the environment and then
* its own single-card default — when neither transport can answer. A claim under a
* slightly wrong name still excludes everyone using the same name, which is
* strictly better than not claiming at all.
*/
static const char *session_card_identity(const ft_tt_hardware_session *session,
char *buffer, size_t buffer_bytes)
{
if (ft_tt_runtime_pci_address(&session->runtime, buffer, buffer_bytes) != FT_TT_OK ||
buffer[0] == '\0')
return NULL;
return buffer;
}
int ft_tt_hardware_session_claim_cores(ft_tt_hardware_session *session, const uint32_t *indices,
size_t count, const char *reason,
ft_tt_tensix_claim_retire_fn retire, void *retire_context)
{
char card[FT_TT_UDRV_CLAIM_NAMESPACE_MAX];
ft_tt_tensix_claim_request request;
ft_tt_tensix_claim_refusal refusal;
unsigned free_slot = 0;
int rc;
if (!session || !indices || count == 0u)
return FT_TT_EINVAL;
while (free_slot < FT_TT_HARDWARE_SESSION_MAX_CLAIMS && session->cores[free_slot])
free_slot++;
if (free_slot == FT_TT_HARDWARE_SESSION_MAX_CLAIMS) {
ft_tt_log_error("session: no room for another core claim (max %u)",
FT_TT_HARDWARE_SESSION_MAX_CLAIMS);
return FT_TT_EINVAL;
}
memset(&request, 0, sizeof(request));
request.card = session_card_identity(session, card, sizeof(card));
request.indices = indices;
request.count = count;
request.reason = reason;
/* reserved stays NULL on purpose: that is what reads FT_TT_TENSIX_RESERVED,
* which is how an operator keeps a sweep out of another family's cores and
* out of anything a serve predating this module is using. */
request.retire = retire;
request.retire_context = retire_context;
rc = ft_tt_tensix_claim_acquire(&request, &session->cores[free_slot], &refusal);
if (rc == FT_TT_UDRV_OK)
return FT_TT_OK;
ft_tt_tensix_claim_refusal_print(&refusal);
session->cores[free_slot] = NULL;
/* FT_TT_UDRV_EAGAIN and FT_TT_EPERM are the same number by design (see
* ft_tt_udrv.h), but the caller's table is ft_tt_device.h's, so name it. */
return (rc == FT_TT_UDRV_EAGAIN) ? FT_TT_EPERM : rc;
}
int ft_tt_hardware_session_tensix(const ft_tt_hardware_session *session, uint32_t index,
uint16_t *out_x, uint16_t *out_y)
{
ft_tt_noc_coord core;
int rc;
if (!session || !out_x || !out_y)
return FT_TT_EINVAL;
rc = ft_tt_coord_tensix_by_index(&session->grid, index, FT_TT_COORD_TRANSLATED, &core);
if (rc != FT_TT_OK)
return rc;
*out_x = (uint16_t)core.x;
*out_y = (uint16_t)core.y;
return FT_TT_OK;
}
int ft_tt_tensix_index_from_environment(uint32_t *out_index)
{
const char *value = getenv(FT_TT_TENSIX_INDEX_ENVIRONMENT);
char *end = NULL;
unsigned long parsed;
if (!out_index)
return FT_TT_EINVAL;
if (!value || value[0] == '\0') {
*out_index = 0u;
return FT_TT_OK;
}
errno = 0;
parsed = strtoul(value, &end, 10);
/* Only the SYNTAX is checked here. The bound used to be FT_TT_LAUNCH_MAX_CORES, a
* literal 140 that is wrong on a 120-core p150 — and the refusal that matters was
* always the LIVE one in ft_tt_coord_tensix_by_index(), which bounds against
* ft_tt_coord_tensix_core_count(). Two bounds, one derived and one constant, is how
* the constant survives. */
if (errno != 0 || end == value || *end != '\0' || parsed > UINT32_MAX) {
ft_tt_log_error("session: %s=\"%s\" is not a Tensix index",
FT_TT_TENSIX_INDEX_ENVIRONMENT, value);
return FT_TT_EINVAL;
}
*out_index = (uint32_t)parsed;
return FT_TT_OK;
}
int ft_tt_hardware_session_tensix_from_environment(ft_tt_hardware_session *session,
uint16_t *out_x, uint16_t *out_y,
uint32_t *out_index)
{
uint32_t index = 0;
int rc = ft_tt_tensix_index_from_environment(&index);
if (rc != FT_TT_OK)
return rc;
rc = ft_tt_hardware_session_tensix(session, index, out_x, out_y);
if (rc != FT_TT_OK) {
ft_tt_log_error("session: no enabled Tensix at index %u (%s)", index,
ft_tt_error_string(rc));
return rc;
}
/* Claim BEFORE announcing the coordinate: a log line saying "target Tensix 71"
* for a core another process owns is the line that sends someone debugging
* the wrong thing. `retire` is NULL here because this entry point serves
* every single-core harness and does not know what any of them run — a
* harness with a resident queue supplies its own via
* ft_tt_hardware_session_claim_cores(). */
rc = ft_tt_hardware_session_claim_cores(session, &index, 1u,
"single-core harness (FT_TT_TENSIX_INDEX)", NULL,
NULL);
if (rc != FT_TT_OK)
return rc;
printf("target Tensix %u at translated (%u, %u) [%s, claimed]\n", index, (unsigned)*out_x,
(unsigned)*out_y, FT_TT_TENSIX_INDEX_ENVIRONMENT);
if (out_index)
*out_index = index;
return FT_TT_OK;
}
const char *ft_tt_firmware_directory(void)
{
static char fallback[4096];
const char *value = getenv(FT_TT_FIRMWARE_DIRECTORY_ENVIRONMENT);
const char *home = getenv("HOME");
if (value && value[0] != '\0')
return value;
/* Default: $HOME/ttprog, the conventional install location for the boot firmware. */
if (home && home[0] != '\0' &&
snprintf(fallback, sizeof fallback, "%s/ttprog", home) < (int)sizeof fallback)
return fallback;
ft_tt_log_error("session: %s is not set and $HOME is unknown -- point it at the program "
"directory whose fw_*.elf to boot (built by build_tensix_program.sh, e.g. "
"tt/programs/<op>)", FT_TT_FIRMWARE_DIRECTORY_ENVIRONMENT);
return NULL;
}
/** Read a whole file into a fresh buffer. */
static int read_whole_file(const char *path, unsigned char **out_data, size_t *out_size)
{
FILE *file = fopen(path, "rb");
unsigned char *buffer = NULL;
long size;
if (!file) {
ft_tt_log_error("session: cannot open %s", path);
return FT_TT_EIO;
}
if (fseek(file, 0, SEEK_END) != 0 || (size = ftell(file)) < 0 ||
fseek(file, 0, SEEK_SET) != 0) {
fclose(file);
return FT_TT_EIO;
}
buffer = malloc((size_t)size);
if (!buffer) {
fclose(file);
return FT_TT_ENOMEM;
}
if (fread(buffer, 1, (size_t)size, file) != (size_t)size) {
ft_tt_log_error("session: short read on %s", path);
free(buffer);
fclose(file);
return FT_TT_EIO;
}
fclose(file);
*out_data = buffer;
*out_size = (size_t)size;
return FT_TT_OK;
}
int ft_tt_firmware_set_load(const char *directory, ft_tt_firmware_set *out_set)
{
unsigned processor;
if (!directory || !out_set)
return FT_TT_EINVAL;
memset(out_set, 0, sizeof(*out_set));
for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) {
char path[512];
int rc;
int written = snprintf(path, sizeof(path), "%s/%s", directory,
k_firmware_file_names[processor]);
if (written < 0 || (size_t)written >= sizeof(path)) {
ft_tt_firmware_set_release(out_set);
return FT_TT_EINVAL;
}
rc = read_whole_file(path, &out_set->elf_data[processor], &out_set->elf_size[processor]);
if (rc != FT_TT_OK) {
ft_tt_firmware_set_release(out_set);
return rc;
}
}
return FT_TT_OK;
}
void ft_tt_firmware_set_release(ft_tt_firmware_set *set)
{
unsigned processor;
if (!set)
return;
for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) {
free(set->elf_data[processor]);
set->elf_data[processor] = NULL;
set->elf_size[processor] = 0;
}
}
/**
* Read the Tensix instruction FIFO's status and say whether a boot can push into it.
*
* **The one thing a read CAN say about a wedged core.** BRISC firmware's `device_setup()`
* pushes ZEROACC / SFPENCC / load-const / four SEMINITs into THREAD 0's instruction FIFO
* (in its boot path) *before* it releases the
* four subordinates. A write to `INSTRN_BUF_BASE` stalls the RISC-V core until the FIFO
* has space, so a FIFO that cannot accept one stalls BRISC in a
* single store — with the subordinates still held and the go signal still INIT. That is
* the `0x00047000` fingerprint exactly, and it is our own intermediate register state
* rather than anything the hardware invented.
*
* MEASURED 2026-08-17: 140 of 140 healthy cores read 0x77 with `invalid_instrn` 0; the
* core the 1x5 multicast relaunch arm wedged read **0x66 with `invalid_instrn` 1**.
*/
static int read_instrn_buf_status(const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
uint32_t *out_status, uint32_t *out_invalid)
{
int rc = noc->read(noc->user, x, y, FT_TT_TENSIX_INSTRN_BUF_STATUS_ADDR, out_status,
sizeof(*out_status));
if (rc != FT_TT_OK)
return rc;
return noc->read(noc->user, x, y, FT_TT_TENSIX_DBG_INVALID_INSTRN_ADDR, out_invalid,
sizeof(*out_invalid));
}
int ft_tt_core_boot_firmware(const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
const ft_tt_firmware_set *firmware,
ft_tt_image_placement placements[FT_TT_PROC_COUNT])
{
unsigned processor;
uint32_t instrn_buf_status = 0u;
uint32_t invalid_instrn = 0u;
int rc;
if (!noc || !firmware)
return FT_TT_EINVAL;
if (placements)
memset(placements, 0, sizeof(*placements) * FT_TT_PROC_COUNT);
/* The five RISC-Vs and NOTHING ELSE. The same register has bits for the matrix
* backend, and asserting them here corrupts that core's compute permanently — see
* the warning block in ft_tt_launch.h. The firmware brings the backend up itself. */
rc = ft_tt_launch_assert_reset(noc, x, y, FT_TT_SOFT_RESET_ALL_RISCS);
if (rc != FT_TT_OK)
return rc;
/* AFTER the assert and BEFORE the deassert, which is the only place this reading is
* attributable: it says what the PARK left behind, not what the boot did. Reported
* always, so a clean boot records its own baseline. */
if (read_instrn_buf_status(noc, x, y, &instrn_buf_status, &invalid_instrn) != FT_TT_OK)
instrn_buf_status = FT_TT_TENSIX_INSTRN_BUF_UNREAD;
if (instrn_buf_status == FT_TT_TENSIX_INSTRN_BUF_UNREAD)
; /* a diagnostic read that did not answer must not abort a boot */
else if (instrn_buf_status != FT_TT_TENSIX_INSTRN_BUF_READY)
ft_tt_log_error("boot: Tensix (%u, %u) instrn_buf_status=0x%08x (want 0x%02x) "
"invalid_instrn=0x%08x with all five RISC-Vs held — the Tensix "
"instruction FIFO cannot accept a write, so device_setup() will "
"stall BRISC before it releases the subordinates",
(unsigned)x, (unsigned)y, instrn_buf_status,
(unsigned)FT_TT_TENSIX_INSTRN_BUF_READY, invalid_instrn);
else if (getenv("FT_TT_TRACE_INSTRN_BUF"))
ft_tt_log_error("boot: Tensix (%u, %u) instrn_buf_status=0x%08x READY, "
"invalid_instrn=0x%08x (trace)", (unsigned)x, (unsigned)y,
instrn_buf_status, invalid_instrn);
rc = ft_tt_launch_clear_mailbox(noc, x, y);
if (rc != FT_TT_OK)
return rc;
for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) {
rc = ft_tt_launch_load_image(noc, x, y, processor, FT_TT_IMAGE_FIRMWARE,
firmware->elf_data[processor], firmware->elf_size[processor],
placements ? &placements[processor] : NULL);
if (rc != FT_TT_OK)
return rc;
}
/* RUN_MSG_DONE is 0, so a cleared mailbox already reads DONE. Without this the
* boot wait below is vacuous — see ft_tt_launch.h. */
rc = ft_tt_launch_prime_firmware_handshake(noc, x, y);
if (rc != FT_TT_OK)
return rc;
/* BRISC only: its firmware releases the other four itself. */
rc = ft_tt_launch_deassert_reset(noc, x, y, FT_TT_SOFT_RESET_BRISC, false);
if (rc != FT_TT_OK)
return rc;
return ft_tt_launch_wait_firmware_ready(noc, x, y, FT_TT_FIRMWARE_BOOT_TIMEOUT_MS);
}
int ft_tt_core_load_program(const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
const ft_tt_kernel_program *program,
ft_tt_image_placement placements[FT_TT_PROC_COUNT])
{
unsigned index;
if (!noc || !program)
return FT_TT_EINVAL;
if (placements)
memset(placements, 0, sizeof(*placements) * FT_TT_PROC_COUNT);
for (index = 0; index < program->image_count; index++) {
const ft_tt_kernel_image *image = &program->images[index];
ft_tt_image_placement place;
int rc = ft_tt_launch_load_image(noc, x, y, image->processor, FT_TT_IMAGE_KERNEL,
image->elf_data, image->elf_size, &place);
if (rc != FT_TT_OK)
return rc;
if (placements && image->processor < FT_TT_PROC_COUNT)
placements[image->processor] = place;
}
return FT_TT_OK;
}
int ft_tt_core_park(const ft_tt_noc_access *noc, uint16_t x, uint16_t y)
{
uint32_t instrn_buf_status = 0u;
uint32_t invalid_instrn = 0u;
/* Read BEFORE the assert. Together with the reading ft_tt_core_boot_firmware() takes
* after its own assert, this brackets the park: it separates "the previous run left
* the Tensix instruction FIFO stuck" from "asserting reset is what stuck it". Reading
* only; a park that refused here would change every suite's teardown on a baseline
* that has not been measured for a RUNNING core. */
if (read_instrn_buf_status(noc, x, y, &instrn_buf_status, &invalid_instrn) == FT_TT_OK &&
(instrn_buf_status != FT_TT_TENSIX_INSTRN_BUF_READY ||
getenv("FT_TT_TRACE_INSTRN_BUF")))
ft_tt_log_error("park: Tensix (%u, %u) instrn_buf_status=0x%08x (ready is 0x%02x) "
"invalid_instrn=0x%08x BEFORE the assert%s",
(unsigned)x, (unsigned)y, instrn_buf_status,
(unsigned)FT_TT_TENSIX_INSTRN_BUF_READY, invalid_instrn,
instrn_buf_status == FT_TT_TENSIX_INSTRN_BUF_READY
? " (trace)"
: " — the Tensix instruction FIFO is ALREADY stuck, and reset "
"removes the only processors that could drain it");
return ft_tt_launch_assert_reset(noc, x, y, FT_TT_SOFT_RESET_ALL_RISCS);
}
static uint64_t monotonic_milliseconds(void)
{
struct timespec now;
if (clock_gettime(CLOCK_MONOTONIC, &now) != 0)
return 0;
return (uint64_t)now.tv_sec * 1000u + (uint64_t)(now.tv_nsec / 1000000L);
}
int ft_tt_core_wait_sync_registers_ready(const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
uint32_t timeout_ms)
{
uint64_t deadline;
if (!noc || timeout_ms == 0u)
return FT_TT_EINVAL;
deadline = monotonic_milliseconds() + timeout_ms;
for (;;) {
uint8_t sync[FT_TT_SUBORDINATE_MAP_SIZE];
int rc = noc->read(noc->user, x, y, FT_TT_MB_SUBORDINATE_SYNC_ADDR, sync, sizeof(sync));
if (rc != FT_TT_OK)
return rc;
/* subordinate_sync is [NCRISC, TRISC0, TRISC1, TRISC2]. */
if (sync[1] == FT_TT_RUN_SYNC_MSG_DONE)
return FT_TT_OK;
if (monotonic_milliseconds() >= deadline) {
ft_tt_log_error("session: TRISC0 still at 0x%02x after %u ms; sync registers were "
"never re-initialised", sync[1], timeout_ms);
return FT_TT_EIO;
}
}
}