Foundry/tt/ft_tt_core_readiness.c

192 lines
7.8 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* @file ft_tt_core_readiness.c
* @brief Implementation of the may-I-boot-here question (see ft_tt_core_readiness.h).
*
* @status New 2026-08-24. Classifier covered off-hardware by tt/tests/test_core_readiness.c.
* @issues None
* @todo None
*/
#include "ft_tt_core_readiness.h"
#include <string.h>
#include "ft_tt_launch.h"
#include "ft_tt_memmap.h"
#include "ft_tt_op_l1_layout.h"
#include "ft_tt_queue.h"
const char *ft_tt_core_readiness_string(ft_tt_core_readiness_state state)
{
switch (state) {
case FT_TT_CORE_READY:
return "ready";
case FT_TT_CORE_NOT_READY_NO_ANSWER:
return "the core does not answer a mailbox read";
case FT_TT_CORE_NOT_READY_INSTRUCTION_FIFO:
return "the Tensix instruction FIFO is not ready — the wedge, and only a card reset "
"clears it";
case FT_TT_CORE_NOT_READY_STILL_EXECUTING:
return "a processor is still executing — a heartbeat moved between two reads";
case FT_TT_CORE_NOT_READY_KERNEL_STALLED:
return "a kernel is launched and no processor is making progress";
case FT_TT_CORE_NOT_READY_BOOT_STUCK:
return "the core is stuck in its boot handshake";
case FT_TT_CORE_NOT_READY_MIXED:
return "the RISC-Vs are in a mixed reset state nothing here can name";
}
return "?";
}
/** Did any processor's heartbeat move? Fills `out->ticking_mask` on the way past. */
static bool anything_ticking(const ft_tt_core_processor_liveness *processor,
ft_tt_core_readiness *out)
{
unsigned index;
out->ticking_mask = 0u;
if (!processor)
return false;
for (index = 0; index < FT_TT_PROC_COUNT; index++)
if (processor[index].slot_read && processor[index].ticking)
out->ticking_mask |= 1u << index;
return out->ticking_mask != 0u;
}
void ft_tt_core_readiness_classify(const ft_tt_core_status *status,
const ft_tt_core_processor_liveness *processor,
ft_tt_core_readiness *out)
{
bool ticking;
if (!status || !out)
return;
memset(out, 0, sizeof(*out));
out->status = *status;
if (processor)
memcpy(out->processor, processor, sizeof(out->processor));
/* THE MAILBOX FIRST. Everything below reads words the mailbox snapshot brought back, so
* a failed read makes all of them meaningless rather than false. */
if (status->read_result != FT_TT_OK) {
out->state = FT_TT_CORE_NOT_READY_NO_ANSWER;
return;
}
/* THE INSTRUCTION FIFO BEFORE THE VERDICT, because it is the one reading that says
* WAITING CANNOT HELP, and it is true whatever the verdict says. BRISC firmware's
* device_setup() pushes into this FIFO before it releases the subordinates, so a boot
* onto a core whose FIFO cannot accept a write stalls BRISC inside a single store —
* which is the wedge itself, not a symptom of it. MEASURED 2026-08-17 over 141 cores:
* healthy is 0x77 in every state seen; the wedged core reads 0x66 and keeps reading it
* across further park/assert cycles. UNREAD (0xFFFFFFFF) means the debug register did
* not answer, which is not evidence of a wedge and is not treated as one. */
if (status->state.instrn_buf_status != FT_TT_TENSIX_INSTRN_BUF_READY &&
status->state.instrn_buf_status != FT_TT_TENSIX_INSTRN_BUF_UNREAD) {
out->state = FT_TT_CORE_NOT_READY_INSTRUCTION_FIFO;
(void)anything_ticking(processor, out);
return;
}
ticking = anything_ticking(processor, out);
/* A TICKING HEARTBEAT OUTRANKS EVERY VERDICT, INCLUDING THE TWO BOOTABLE ONES.
*
* This is the whole reason the module exists. The verdict rests on the go signal and the
* reset register, and the failure being guarded against is a compute thread spinning
* while the reader that owns those words has already left. A moving heartbeat is a
* first-hand statement that a processor is executing; it is checked before anything that
* would let the core through. It also self-clears — the spin is bounded — so this is the
* one not-ready state a caller may wait out. */
if (ticking) {
out->state = FT_TT_CORE_NOT_READY_STILL_EXECUTING;
out->waiting_can_help = true;
return;
}
switch (status->verdict) {
case FT_TT_CORE_VERDICT_HELD:
case FT_TT_CORE_VERDICT_FIRMWARE_IDLE:
/* THE TWO STATES FIRMWARE MAY BE BOOTED ONTO, and they are exactly the two states a
* clean shutdown leaves behind: HELD is cold or parked, FIRMWARE_IDLE is what a
* retired residency and a finished probe both leave (ft_tt_core_probe.h: "firmware
* running and idle, NOT parked"). Nothing else is on this list. */
out->state = FT_TT_CORE_READY;
out->ready = true;
return;
case FT_TT_CORE_VERDICT_KERNEL_RUNNING:
/* GO in the mailbox with nothing ticking. Either a non-queue kernel is running, or a
* queue kernel is somewhere that does not bump a heartbeat. Neither is something to
* wait for, and neither is something to boot onto. */
out->state = FT_TT_CORE_NOT_READY_KERNEL_STALLED;
return;
case FT_TT_CORE_VERDICT_BOOT_INCOMPLETE:
case FT_TT_CORE_VERDICT_SYNC_BLOCKED:
out->state = FT_TT_CORE_NOT_READY_BOOT_STUCK;
return;
case FT_TT_CORE_VERDICT_NO_ANSWER:
out->state = FT_TT_CORE_NOT_READY_NO_ANSWER;
return;
case FT_TT_CORE_VERDICT_MIXED:
break;
}
out->state = FT_TT_CORE_NOT_READY_MIXED;
}
/**
* Sample the five per-processor queue status slots TWICE and report what moved.
*
* The slots are at a fixed offset from `FT_TT_OP_QUEUE_BASE`, which is where every residency
* in this stack puts its control block — so a process that did not create the queue can
* still read them. They are consulted only when the control block carries our magic;
* otherwise the words belong to nobody and mean nothing.
*/
static void read_processor_liveness(const ft_tt_noc_access *noc, const ft_tt_core_status *status,
ft_tt_core_processor_liveness *out)
{
unsigned processor;
memset(out, 0, FT_TT_PROC_COUNT * sizeof(*out));
if (!status->queue_present)
return;
for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) {
const uint32_t address = FT_TT_QUEUE_STATUS_ADDRESS(FT_TT_OP_QUEUE_BASE, processor);
uint32_t first[FT_TT_QUEUE_STATUS_WORDS];
uint32_t again[FT_TT_QUEUE_STATUS_WORDS];
if (noc->read(noc->user, status->x, status->y, address, first, sizeof(first)) != FT_TT_OK)
continue;
if (noc->read(noc->user, status->x, status->y, address, again, sizeof(again)) != FT_TT_OK)
continue;
out[processor].slot_read = true;
out[processor].state = first[FT_TT_QUEUE_STATUS_STATE];
out[processor].heartbeat = first[FT_TT_QUEUE_STATUS_HEARTBEAT];
out[processor].ticking =
again[FT_TT_QUEUE_STATUS_HEARTBEAT] != first[FT_TT_QUEUE_STATUS_HEARTBEAT];
}
}
int ft_tt_core_readiness_read(const ft_tt_noc_access *noc, const ft_tt_grid_info *grid,
uint32_t index, ft_tt_core_readiness *out)
{
ft_tt_core_processor_liveness processor[FT_TT_PROC_COUNT];
ft_tt_core_status status;
int rc;
if (!noc || !noc->read || !grid || !out)
return FT_TT_EINVAL;
rc = ft_tt_core_status_read(noc, grid, index, &status);
if (rc != FT_TT_OK)
return rc;
memset(processor, 0, sizeof(processor));
if (status.read_result == FT_TT_OK)
read_processor_liveness(noc, &status, processor);
ft_tt_core_readiness_classify(&status, processor, out);
return FT_TT_OK;
}