/* 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 #include #include #include #include #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; }