Foundry/tt/tests/test_core_readiness.c

392 lines
17 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* @file test_core_readiness.c
* @brief Off-hardware unit test for the may-I-boot-here classifier
* (ft_tt_core_readiness.{h,c}) and for the verdict it rests on
* (ft_tt_core_status_classify()).
*
* TOUCHES NO DEVICE. Every case is a pure function of a hand-built
* `ft_tt_core_status` and five hand-built liveness samples, so all of it runs on
* a machine with no card in it. That is the point twice over: the code under
* test decides whether firmware gets booted onto a Tensix, and getting that
* wrong in the permissive direction costs a whole-card reset while getting it
* wrong in the strict direction drops the whole model to the ~26 s/position CPU
* forward. Neither is a thing to find out on silicon.
*
* ---------------------------------------------------------------------------
* THE TWO ARMS THAT MATTER, AND THEY PULL IN OPPOSITE DIRECTIONS
* ---------------------------------------------------------------------------
* RED — the leftover: a reader that already exited, a compute thread whose
* heartbeat is still moving. The control block's own heartbeat is
* FROZEN in this case, which is exactly why `ft_tt_core_status`'s
* reading alone is not enough and this module samples the per-processor
* slots as well.
* GREEN — the healthy core, in each of the shapes a clean exit really leaves:
* cold and held, firmware idle with no queue at all, and firmware idle
* with a retired queue whose slots still hold EXIT and whose heartbeats
* have stopped. A guard that refuses ANY of these is worse than the bug.
*
* The third group is the stale-word group, and it is the false-positive trap
* this classifier is specifically built around: a memory-family residency
* (DM0|DM1) leaves its three TRISC slots holding whatever the previous program
* wrote, because they never ran and never wrote EXIT. Those cases MUST come back
* ready, and they are checked.
*
* @status New 2026-08-24.
* @issues None
* @todo None
*/
#include <inttypes.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
#include "ft_tt_core_readiness.h"
#include "ft_tt_core_status.h"
#include "ft_tt_launch.h"
#include "ft_tt_mailbox.h"
#include "ft_tt_memmap.h"
#include "ft_tt_queue.h"
static unsigned checks_run;
static unsigned 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);
}
}
/* ------------------------------------------------------------------------- */
/* Building a reading by hand */
/* ------------------------------------------------------------------------- */
/** Every RISC-V held in reset — the cold/parked register value. */
static uint32_t all_held(void)
{
uint32_t bits = 0u;
unsigned processor;
for (processor = 0; processor < FT_TT_PROC_COUNT; processor++)
bits |= ft_tt_processor_reset_bit(processor);
return bits;
}
/**
* A core that answers, with every subordinate synced DONE, a ready instruction
* FIFO, and every RISC-V out of reset. `go` picks the go signal, which is what
* separates FIRMWARE_IDLE from KERNEL_RUNNING.
*/
static void status_running(ft_tt_core_status *out, uint8_t go)
{
memset(out, 0, sizeof(*out));
out->index = 90u;
out->x = 5u;
out->y = 7u;
out->read_result = FT_TT_OK;
out->state.reset_register = 0u;
out->state.instrn_buf_status = FT_TT_TENSIX_INSTRN_BUF_READY;
out->state.go_messages[0].signal = go;
/* subordinate_sync is already all-zero from the memset, and DONE is 0x00. */
out->verdict = ft_tt_core_status_classify(out);
}
/** The cold core: everything held in reset. */
static void status_held(ft_tt_core_status *out)
{
memset(out, 0, sizeof(*out));
out->index = 90u;
out->read_result = FT_TT_OK;
out->state.reset_register = all_held();
out->state.instrn_buf_status = FT_TT_TENSIX_INSTRN_BUF_READY;
out->state.go_messages[0].signal = FT_TT_RUN_MSG_DONE;
out->verdict = ft_tt_core_status_classify(out);
}
/** Mark a queue control block as present, so the slots are consulted at all. */
static void with_queue(ft_tt_core_status *status, bool control_heartbeat_ticking)
{
status->queue_present = true;
status->queue_control[FT_TT_QUEUE_FIELD_MAGIC] = FT_TT_QUEUE_MAGIC;
status->queue_heartbeat_ticking = control_heartbeat_ticking;
}
/** Every slot read, holding `state`, none of them ticking. */
static void slots_quiet(ft_tt_core_processor_liveness *slots, uint32_t state)
{
unsigned processor;
memset(slots, 0, FT_TT_PROC_COUNT * sizeof(*slots));
for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) {
slots[processor].slot_read = true;
slots[processor].state = state;
slots[processor].heartbeat = 4242u;
}
}
/* ------------------------------------------------------------------------- */
/* The verdict itself — the backbone the readiness rests on */
/* ------------------------------------------------------------------------- */
static void verdict_cases(void)
{
ft_tt_core_status status;
printf("ft_tt_core_status_classify — the verdicts readiness rests on\n");
status_held(&status);
check(status.verdict == FT_TT_CORE_VERDICT_HELD, "all five in reset is HELD");
status_running(&status, FT_TT_RUN_MSG_DONE);
check(status.verdict == FT_TT_CORE_VERDICT_FIRMWARE_IDLE,
"out of reset, synced, go DONE is FIRMWARE_IDLE");
status_running(&status, FT_TT_RUN_MSG_GO);
check(status.verdict == FT_TT_CORE_VERDICT_KERNEL_RUNNING, "go GO is KERNEL_RUNNING");
status_running(&status, FT_TT_RUN_MSG_INIT);
check(status.verdict == FT_TT_CORE_VERDICT_BOOT_INCOMPLETE,
"go still INIT with BRISC running is BOOT_INCOMPLETE");
/* The core-60 signature: BRISC out of reset, every subordinate held, and a
* sync byte that is not DONE. BRISC is inside tensix_sync() waiting on
* processors that can never answer. */
memset(&status, 0, sizeof(status));
status.read_result = FT_TT_OK;
status.state.instrn_buf_status = FT_TT_TENSIX_INSTRN_BUF_READY;
status.state.reset_register = all_held() & ~ft_tt_processor_reset_bit(FT_TT_PROC_DM0);
status.state.subordinate_sync[1] = FT_TT_RUN_MSG_GO;
status.verdict = ft_tt_core_status_classify(&status);
check(status.verdict == FT_TT_CORE_VERDICT_SYNC_BLOCKED,
"BRISC up, subordinates held, sync pending is SYNC_BLOCKED");
memset(&status, 0, sizeof(status));
status.read_result = FT_TT_EIO;
status.verdict = ft_tt_core_status_classify(&status);
check(status.verdict == FT_TT_CORE_VERDICT_NO_ANSWER, "a failed mailbox read is NO_ANSWER");
}
/* ------------------------------------------------------------------------- */
/* GREEN — every shape a healthy core really comes in */
/* ------------------------------------------------------------------------- */
static void healthy_cases(void)
{
ft_tt_core_processor_liveness slots[FT_TT_PROC_COUNT];
ft_tt_core_readiness readiness;
ft_tt_core_status status;
printf("\nHEALTHY — a guard that refuses any of these takes the model to the CPU forward\n");
/* 1. A cold card. No queue was ever placed here, so the slots mean nothing
* and are not even read. */
status_held(&status);
memset(slots, 0, sizeof(slots));
ft_tt_core_readiness_classify(&status, slots, &readiness);
check(readiness.ready, "cold card, all five held, no queue — READY");
/* 2. Firmware up and idle with no queue: what a finished ft_tt_core probe
* leaves behind, and what a bind_core that never started leaves. */
status_running(&status, FT_TT_RUN_MSG_DONE);
memset(slots, 0, sizeof(slots));
ft_tt_core_readiness_classify(&status, slots, &readiness);
check(readiness.ready, "firmware idle, no queue control block — READY");
/* 3. THE RE-ARM. A residency that retired cleanly: firmware idle, the queue
* block still carries our magic, every slot says EXIT and no heartbeat
* moves. This is the ordinary second arm and it must not be refused. */
status_running(&status, FT_TT_RUN_MSG_DONE);
with_queue(&status, false);
slots_quiet(slots, (uint32_t)FT_TT_QUEUE_STATE_EXIT);
ft_tt_core_readiness_classify(&status, slots, &readiness);
check(readiness.ready, "retired residency: idle, all slots EXIT, nothing ticking — READY");
check(readiness.ticking_mask == 0u, " and nothing is reported ticking");
/* 4. THE STALE-WORD TRAP. The memory family enables DM0|DM1 only, so its
* three TRISCs never run the program and never write EXIT — their slots
* hold whatever the last program left. Refusing on a state word rather
* than on a moving heartbeat would refuse this healthy core. */
status_running(&status, FT_TT_RUN_MSG_DONE);
with_queue(&status, false);
slots_quiet(slots, (uint32_t)FT_TT_QUEUE_STATE_EXIT);
slots[FT_TT_PROC_MATH0].state = (uint32_t)FT_TT_QUEUE_STATE_CB_WAIT_FRONT;
slots[FT_TT_PROC_MATH1].state = (uint32_t)FT_TT_QUEUE_STATE_EXEC;
slots[FT_TT_PROC_MATH2].state = 0xDEADBEEFu;
ft_tt_core_readiness_classify(&status, slots, &readiness);
check(readiness.ready,
"memory family's stale TRISC state words, no heartbeat moving — READY");
/* 5. The debug register did not answer. UNREAD is not evidence of a wedge
* and must not be read as one. */
status_running(&status, FT_TT_RUN_MSG_DONE);
status.state.instrn_buf_status = FT_TT_TENSIX_INSTRN_BUF_UNREAD;
status.verdict = ft_tt_core_status_classify(&status);
memset(slots, 0, sizeof(slots));
ft_tt_core_readiness_classify(&status, slots, &readiness);
check(readiness.ready, "instrn_buf UNREAD (register did not answer) — READY, not a wedge");
}
/* ------------------------------------------------------------------------- */
/* RED — the leftover, and everything else that must be refused */
/* ------------------------------------------------------------------------- */
static void refusal_cases(void)
{
ft_tt_core_processor_liveness slots[FT_TT_PROC_COUNT];
ft_tt_core_readiness readiness;
ft_tt_core_status status;
printf("\nREFUSED — booting onto any of these is the wedge\n");
/* THE BUG, EXACTLY AS SILICON PRODUCED IT. A refused descriptor: the reader
* on DM0 aborted and wrote EXIT, so the CONTROL heartbeat is frozen and the
* status read alone calls the core quiet — while TRISC0 spins out its bound
* and its OWN heartbeat moves. Reader exited, compute thread has not. */
status_running(&status, FT_TT_RUN_MSG_GO);
with_queue(&status, false);
slots_quiet(slots, (uint32_t)FT_TT_QUEUE_STATE_EXIT);
slots[FT_TT_PROC_MATH0].state = (uint32_t)FT_TT_QUEUE_STATE_CB_WAIT_FRONT;
slots[FT_TT_PROC_MATH0].ticking = true;
ft_tt_core_readiness_classify(&status, slots, &readiness);
check(!readiness.ready, "THE LEFTOVER: reader exited, TRISC0 heartbeat moving — REFUSED");
check(readiness.state == FT_TT_CORE_NOT_READY_STILL_EXECUTING,
" and it is named STILL_EXECUTING, not a generic failure");
check(readiness.waiting_can_help, " and waiting can help — the spin is bounded");
check(readiness.ticking_mask == (1u << FT_TT_PROC_MATH0),
" and the mask names TRISC0 alone");
check(!status.queue_heartbeat_ticking,
" CONTROL heartbeat is frozen — the status read alone would have passed this core");
/* The same leftover after the go signal has gone DONE. A heartbeat outranks
* every verdict, including the two bootable ones, and this is the case that
* makes that ordering load-bearing rather than decorative. */
status_running(&status, FT_TT_RUN_MSG_DONE);
with_queue(&status, false);
slots_quiet(slots, (uint32_t)FT_TT_QUEUE_STATE_EXIT);
slots[FT_TT_PROC_MATH0].ticking = true;
ft_tt_core_readiness_classify(&status, slots, &readiness);
check(!readiness.ready && readiness.state == FT_TT_CORE_NOT_READY_STILL_EXECUTING,
"FIRMWARE_IDLE with a moving heartbeat — REFUSED, the heartbeat outranks the verdict");
/* THE 0x66 WEDGE. Already wedged, and no amount of waiting fixes it: rung 1
* does not clear it and only a card reset does. */
status_running(&status, FT_TT_RUN_MSG_DONE);
status.state.instrn_buf_status = 0x66u;
status.verdict = ft_tt_core_status_classify(&status);
memset(slots, 0, sizeof(slots));
ft_tt_core_readiness_classify(&status, slots, &readiness);
check(!readiness.ready && readiness.state == FT_TT_CORE_NOT_READY_INSTRUCTION_FIFO,
"instrn_buf 0x66 — REFUSED as the instruction-FIFO wedge");
check(!readiness.waiting_can_help, " and waiting CANNOT help — it needs a card reset");
/* 0x66 while something is still ticking. The FIFO reading wins, because it
* is the one that says the wait would be dead time before an inevitable
* refusal. */
status_running(&status, FT_TT_RUN_MSG_GO);
status.state.instrn_buf_status = 0x66u;
status.verdict = ft_tt_core_status_classify(&status);
with_queue(&status, false);
slots_quiet(slots, (uint32_t)FT_TT_QUEUE_STATE_EXEC);
slots[FT_TT_PROC_MATH0].ticking = true;
ft_tt_core_readiness_classify(&status, slots, &readiness);
check(readiness.state == FT_TT_CORE_NOT_READY_INSTRUCTION_FIFO && !readiness.waiting_can_help,
"0x66 outranks a moving heartbeat — no wait before an inevitable refusal");
/* A launched kernel with nothing making progress. Not something to wait for
* and not something to boot onto. */
status_running(&status, FT_TT_RUN_MSG_GO);
with_queue(&status, false);
slots_quiet(slots, (uint32_t)FT_TT_QUEUE_STATE_EXEC);
ft_tt_core_readiness_classify(&status, slots, &readiness);
check(!readiness.ready && readiness.state == FT_TT_CORE_NOT_READY_KERNEL_STALLED,
"go GO, nothing ticking — REFUSED as a stalled kernel");
check(!readiness.waiting_can_help, " and waiting cannot help");
/* A kernel running with no queue block at all: a non-queue program. Same
* refusal, reached without any slot being read. */
status_running(&status, FT_TT_RUN_MSG_GO);
memset(slots, 0, sizeof(slots));
ft_tt_core_readiness_classify(&status, slots, &readiness);
check(!readiness.ready, "go GO with no queue control block — REFUSED");
status_running(&status, FT_TT_RUN_MSG_INIT);
memset(slots, 0, sizeof(slots));
ft_tt_core_readiness_classify(&status, slots, &readiness);
check(!readiness.ready && readiness.state == FT_TT_CORE_NOT_READY_BOOT_STUCK,
"BOOT_INCOMPLETE — REFUSED as a stuck boot");
check(!readiness.waiting_can_help, " and waiting cannot help");
memset(&status, 0, sizeof(status));
status.read_result = FT_TT_EIO;
status.verdict = ft_tt_core_status_classify(&status);
memset(slots, 0, sizeof(slots));
ft_tt_core_readiness_classify(&status, slots, &readiness);
check(!readiness.ready && readiness.state == FT_TT_CORE_NOT_READY_NO_ANSWER,
"a core that does not answer — REFUSED");
check(!readiness.waiting_can_help, " and waiting cannot help");
/* MIXED: some running, some held, and nothing above matches. Report, do not
* guess, and certainly do not boot. */
memset(&status, 0, sizeof(status));
status.read_result = FT_TT_OK;
status.state.instrn_buf_status = FT_TT_TENSIX_INSTRN_BUF_READY;
status.state.reset_register = ft_tt_processor_reset_bit(FT_TT_PROC_MATH2);
status.state.go_messages[0].signal = FT_TT_RUN_MSG_DONE;
status.verdict = ft_tt_core_status_classify(&status);
memset(slots, 0, sizeof(slots));
ft_tt_core_readiness_classify(&status, slots, &readiness);
check(status.verdict == FT_TT_CORE_VERDICT_MIXED, "one TRISC held, the rest running is MIXED");
check(!readiness.ready && readiness.state == FT_TT_CORE_NOT_READY_MIXED,
"MIXED — REFUSED");
}
/* ------------------------------------------------------------------------- */
/* Every state has a string, and none of them is the fallthrough */
/* ------------------------------------------------------------------------- */
static void string_cases(void)
{
static const ft_tt_core_readiness_state all[] = {
FT_TT_CORE_READY,
FT_TT_CORE_NOT_READY_NO_ANSWER,
FT_TT_CORE_NOT_READY_INSTRUCTION_FIFO,
FT_TT_CORE_NOT_READY_STILL_EXECUTING,
FT_TT_CORE_NOT_READY_KERNEL_STALLED,
FT_TT_CORE_NOT_READY_BOOT_STUCK,
FT_TT_CORE_NOT_READY_MIXED
};
unsigned index;
bool all_named = true;
printf("\nEvery readiness state names itself\n");
for (index = 0; index < sizeof(all) / sizeof(all[0]); index++) {
const char *text = ft_tt_core_readiness_string(all[index]);
if (!text || !*text || strcmp(text, "?") == 0)
all_named = false;
}
check(all_named, "no readiness state falls through to \"?\"");
}
int main(void)
{
printf("ft_tt_core_readiness — off-hardware classifier suite\n");
printf("====================================================\n\n");
verdict_cases();
healthy_cases();
refusal_cases();
string_cases();
printf("\n%u checks, %u failed\n", checks_run, checks_failed);
return checks_failed == 0u ? 0 : 1;
}