/* SPDX-License-Identifier: AGPL-3.0-or-later */ /* Copyright (C) 2026 David (Corey) Herrera */ /** * @file ft_tt_launch.h * @brief Foundry's slow-dispatch run control for one Tensix: put binaries in L1, * stage the launch message, take the RISC-Vs out of reset, kick, and find out * whether it finished — with a timeout, because a poll that never returns here * costs a physical power cycle (spec §7.3). * * Module prefix: ft_tt_launch_. Pure C99, libc only. Every device touch goes through * an ft_tt_noc_access vtable, so the whole layer runs against a plain array of host * memory in tests and against a TLB window on the card, with the same code. * * ## What actually runs on the core * * Foundry does not boot the Tensix bare. It loads Foundry's per-processor FIRMWARE * (tt/fw/ft_*_fw.c, built by `build_tensix_kernel.sh` alongside the kernel) and then * hands it work through the mailbox protocol that firmware and this file share * (ft_tt_mailbox.h). The five-line summary: * * 1. host asserts reset on all five RISC-Vs * 2. host writes firmware + kernel images into L1, clears the mailbox region * 3. host DEASSERTS BRISC ONLY — BRISC firmware releases the other four itself * (fw/ft_brisc_fw.c boot(): it sets the subordinates' reset PCs and clears * RISCV_DEBUG_REG_SOFT_RESET_0, tt-isa-documentation TensixTile/SoftReset.md) * 4. host writes the launch message, then the go message * 5. BRISC starts the TRISCs, they run the kernel, BRISC writes DONE back * * The full sequence, with the L1 map and the first-hardware-run order, is in * docs/tt_launch_notes.md. Read it before running any of this against the card. * * @status VERIFIED ON THE CARD. 2026-08-11: all 13 steps of docs/tt_launch_notes.md §6, * including a byte-exact vector add as a full triad repeated 1000 times and eight * discontiguous cores staged and kicked together. 2026-08-12: Tensix→OTHER-Tensix * transactions in both directions, and `core_info` + `_prime_watcher()` bringing up the * firmware's NoC sanitize — a bad endpoint is now a reported, stopped core instead of a * hung NoC (docs/tt_bringup_log.md §15) — on the firmware of that time; Foundry's * own firmware (tt/fw/, 2026-10-08) has no watcher, see the core_info section. The * mailbox protocol is ft_tt_mailbox.h, served by tt/fw/. Everything that touches the NoC is * behind FT_TT_PHASE1B_NOC_ENABLED and is a no-op returning FT_TT_EPERM in a default * build. * @issues * - Kernel ELFs from build_tensix_kernel.sh are NOT execute-in-place: their text * sits at a fixed L1 address. `kernel_text_offset` therefore encodes * `entry - kernel_config_base` in unsigned wraparound arithmetic. See * ft_tt_launch_encode_launch_message(). * - A kernel killed mid-flight can wedge a Tensix past every reset this file can issue. * It cost Tensix 0 on 2026-08-12 and there is still no host-side recovery. * @todo None */ #ifndef FT_TT_LAUNCH_H #define FT_TT_LAUNCH_H #include #include #include #include "ft_tt_coord.h" #include "ft_tt_device.h" #include "ft_tt_elf.h" #include "ft_tt_mailbox.h" #include "ft_tt_memmap.h" #include "ft_tt_noc.h" #include "ft_tt_tlb.h" /* ================================================================ * Core reset control register * ================================================================ */ /** Per-Tensix soft-reset register, addressed over the NoC like any other location: * RISCV_DEBUG_REG_SOFT_RESET_0, tt-isa-documentation TensixTile/SoftReset.md. There is no reset ioctl: * release is a read-modify-write here (docs/tt_driver_notes.md §5). */ #define FT_TT_TENSIX_SOFT_RESET_ADDR 0xFFB121B0u /* Bit per processor, SoftReset.md. Bit 31 has no hardware effect there (bits >= 24 do * nothing); it is kept as a software marker for a staggered grid start. */ #define FT_TT_SOFT_RESET_BRISC (1u << 11) #define FT_TT_SOFT_RESET_TRISC0 (1u << 12) #define FT_TT_SOFT_RESET_TRISC1 (1u << 13) #define FT_TT_SOFT_RESET_TRISC2 (1u << 14) #define FT_TT_SOFT_RESET_NCRISC (1u << 18) #define FT_TT_SOFT_RESET_STAGGERED_START (1u << 31) #define FT_TT_SOFT_RESET_ALL_RISCS \ (FT_TT_SOFT_RESET_BRISC | FT_TT_SOFT_RESET_NCRISC | FT_TT_SOFT_RESET_TRISC0 | \ FT_TT_SOFT_RESET_TRISC1 | FT_TT_SOFT_RESET_TRISC2) /* The SAME register also resets the Tensix BACKEND — the matrix engine the three TRISCs * drive (SoftReset.md bits 0-10 and 15-17). * * ############################################################################ * # DO NOT ASSERT THESE. Doing so CORRUPTS THE CORE'S COMPUTE, PERSISTENTLY. # * ############################################################################ * * They are recorded because knowing they exist is worth having and because the next * person will otherwise reach for them exactly as this file did on 2026-08-12 — * reasoning, wrongly, that a "park" which leaves the matrix engine running is * indefensible. Measured on the card, on two virgin cores, with nothing else on the * card: * * ops PASS (PCC 0.999978292) -> boot asserting these bits and run a vector add * -> the vector add is WRONG (45-111 of 1024 elements) -> ops on that core now * returns PCC -nan FOREVER, for every later program, from any tree. * * Remove the assert and the same core goes ops PASS -> vector add byte-exact 1024/1024 * -> ops PASS again. The RISC-V reset alone (FT_TT_SOFT_RESET_ALL_RISCS) is correct and * is what the boot uses; the backend is brought up by the firmware's own initialisation, * and taking it through reset underneath that leaves it configured wrong. * * There is no FT_TT_SOFT_RESET_WHOLE_TENSIX, deliberately: a convenience macro for * "everything" is exactly the shape of the mistake. */ #define FT_TT_SOFT_RESET_UNPACKER 0x00000003u /* bits 0-1, two unpackers */ #define FT_TT_SOFT_RESET_PACKER 0x0000003Cu /* bits 2-5, four packers */ #define FT_TT_SOFT_RESET_MOVER (1u << 6) #define FT_TT_SOFT_RESET_SEARCH (1u << 7) /* SoftReset.md: a third unpacker reset bit */ #define FT_TT_SOFT_RESET_GLUE (1u << 8) /* SoftReset.md: TDMA-RISC and glue */ #define FT_TT_SOFT_RESET_THCON (1u << 9) #define FT_TT_SOFT_RESET_FPU (1u << 10) #define FT_TT_SOFT_RESET_SRCA_REG (1u << 15) #define FT_TT_SOFT_RESET_SRCB_REG (1u << 16) #define FT_TT_SOFT_RESET_DEST_REG (1u << 17) /* ================================================================ * Tensix instruction FIFO — READ ONLY, and the one thing a read CAN say * ================================================================ * * Two more registers in the SAME 4 KiB debug block as the soft reset above * (`RISCV_DEBUG_REGS_START_ADDR` 0xFFB12000; tt-isa-documentation * TensixTile/BabyRISCV/PushTensixInstruction.md, "Debug registers"), so the * window that already reaches 0xFFB121B0 reaches these with no new plumbing. * * **Why they matter.** Writes to `INSTRN_BUF_BASE` (0xFFE40000) are appended to the Tensix * instruction FIFO, and a full FIFO stalls the pushing RISC-V until there is space * (PushTensixInstruction.md). BRISC firmware's boot pushes ZEROACC, SFPENCC, a NOP, * SFPLOADI, SFPCONFIG and four SEMINITs into **thread 0's** FIFO *before* it releases the * subordinates (fw/ft_brisc_fw.c initialise_tensix() then boot()). So a FIFO that cannot accept * a write stalls BRISC inside a single store, before the subordinates are ever released — * which is exactly the `0x00047000` fingerprint (BRISC deasserted by us, NCRISC/TRISC0..2 * still held, go signal still INIT). * * `FT_TT_TENSIX_INSTRN_BUF_READY` (0x77) is the status value measured on an idle, * ready core of this card (docs/tt_bringup_log.md); RISCV_DEBUG_REG_INSTRN_BUF_STATUS's * fields are described in PushTensixInstruction.md. * * **READ these. Do not write `INSTRN_BUF_CTRL0/1`** — that pair is an override that pushes * instructions into the backend from outside the RISC-Vs, i.e. the same family as the * banned backend reset bits above, and nothing here needs it. */ #define FT_TT_TENSIX_INSTRN_BUF_STATUS_ADDR 0xFFB120A8u #define FT_TT_TENSIX_INSTRN_BUF_READY 0x77u /** Last instruction the backend rejected as invalid (a debug register in the same block; * TensixTile/TileControlDebugStatus.md). */ #define FT_TT_TENSIX_DBG_INVALID_INSTRN_ADDR 0xFFB12094u /** What a snapshot reports when the register did not answer. A snapshot that ABORTED * because a debug register did not answer would be a worse diagnostic than one that says * so, and the mailbox half is the half a wedged core is being read for. */ #define FT_TT_TENSIX_INSTRN_BUF_UNREAD 0xFFFFFFFFu /* ================================================================ * Device access * ================================================================ */ /** * How this layer reaches device memory. One indirection, deliberately: it is what * lets every byte of the protocol be exercised against a simulated L1 on the Mac * before any of it is pointed at the card. * * `addr` is a tile-local address (L1 from 0, or a register such as the soft-reset * one at 0xFFB121B0). Implementations must accept any 4-byte-aligned address and * length; they are not required to accept anything finer. * * Both callbacks return FT_TT_OK or a negative FT_TT_* code. */ typedef struct ft_tt_noc_access { int (*write)(void *user, uint16_t x, uint16_t y, uint64_t addr, const void *src, size_t bytes); int (*read)(void *user, uint16_t x, uint16_t y, uint64_t addr, void *dst, size_t bytes); void *user; } ft_tt_noc_access; /** Caller-owned storage tying an accessor to an ft_tt_noc path over a borrowed * window. Declaring it rather than allocating it keeps the accessor allocation-free * and its lifetime obvious: it lives exactly as long as the caller's stack frame. * * Treat the path as private. It is here rather than hidden because the whole backend * is allocation-free on the host, and because it is what makes this layer's device * access and ft_tt_write_l1()'s the same code. */ typedef struct ft_tt_noc_window_binding { ft_tt_noc_path path; } ft_tt_noc_window_binding; /** * Build a NoC accessor that moves bytes through one claimed TLB window, re-aiming it * as the address moves (docs/tt_driver_notes.md §4 — a re-aim is three MMIO stores, * not an ioctl, though this first version goes through CONFIGURE_TLB for safety). * * The bytes move through ft_tt_noc_transfer_raw() at FT_TT_NOC_WIDTH_WORD32. This * layer cannot use ft_tt_write_l1(): it has to reach a 4-byte register at * 0xFFB121B0, which is in no memory space, and write 4-byte mailbox fields, which * are under the 16 B minimum that helper correctly enforces. * * **PHASE 1B.** In a default build every call through the returned accessor returns * FT_TT_EPERM without reaching the device, because aiming a window is fenced at the * kernel boundary (ft_tt_kmd.h, "THE NOC FENCE"). That fence is the reason this file can be * written and tested without risking the card. * * The window and the binding are borrowed, not owned: both must outlive the * accessor, and the caller still frees the window with ft_tt_tlb_window_free(). * * @param ctx open device context * @param window a 2 MiB window claimed with ft_tt_tlb_window_alloc() * @param binding caller-owned storage, must outlive `out_access` * @param out_access receives the accessor * @return FT_TT_OK or FT_TT_EINVAL * * Thread-safe: no (the window is stateful) Reentrant: no Signal-safe: no */ int ft_tt_launch_noc_over_window(ft_tt_ctx *ctx, ft_tt_tlb_window *window, ft_tt_noc_window_binding *binding, ft_tt_noc_access *out_access); /* ================================================================ * Per-processor L1 geography * ================================================================ */ /** "BRISC", "NCRISC", "TRISC0"... for logs. Never NULL. */ const char *ft_tt_processor_name(unsigned processor); /** Soft-reset bit for an FT_TT_PROC_* index, or 0 if the index is out of range. */ uint32_t ft_tt_processor_reset_bit(unsigned processor); /** L1 address of this processor's firmware text hole (FT_TT_MEM_*_FIRMWARE_BASE), * or 0 if the index is out of range. */ uint32_t ft_tt_processor_firmware_base(unsigned processor); /** Size of that hole in bytes, or 0. Firmware text over this will not fit. */ uint32_t ft_tt_processor_firmware_size(unsigned processor); /** L1 address where this processor's firmware `.data` image is staged, which its * `do_crt1()` copies into private local memory at boot * (FT_TT_MEM_*_INIT_LOCAL_L1_BASE_SCRATCH; fw/ft_*_fw.c initialise_local_memory()). * 0 if the index is out of range. */ uint32_t ft_tt_processor_local_init_scratch(unsigned processor); /* ================================================================ * Image loading * ================================================================ */ /** * Which boot contract an ELF was linked under. This picks where its `.data` image is * staged, and getting it wrong puts the image somewhere nothing reads — a core that * boots into garbage globals with no error anywhere. */ typedef enum ft_tt_image_kind { /** Firmware. `.data` has a local-memory execution address and its ELF load * address is meaningless: the firmware reads its image from * FT_TT_MEM_*_INIT_LOCAL_L1_BASE_SCRATCH (fw/ft_*_fw.c initialise_local_memory()). */ FT_TT_IMAGE_FIRMWARE = 0, /** Kernel, linked either way. Its `.data` load address IS meaningful — the kernel's * own `_start` copies from the linker symbol `__kernel_data_lma` (fw/ft_*_entry.c) * — so the ELF's p_paddr is used verbatim. * * There is deliberately no separate "relocated kernel" kind. The ELF's own text * address says which of the two legal regions it was linked into, and the ELF is * the only party that cannot be wrong about that; a caller-supplied flag can. */ FT_TT_IMAGE_KERNEL = 1 } ft_tt_image_kind; /** Where an image ended up, for logging and for filling in the launch message. */ typedef struct ft_tt_image_placement { uint32_t entry_address; /**< what the firmware jumps to */ uint32_t text_address; uint32_t text_bytes; uint32_t lowest_l1_address; /**< inclusive */ uint32_t highest_l1_address; /**< exclusive */ uint32_t bytes_written; } ft_tt_image_placement; /** * Parse one ELF and write its loadable segments into the target core's L1. * * Refuses before writing anything if: the ELF is malformed; a firmware image's text * is not at this processor's firmware base or overflows its hole; any segment would * land outside L1. Refusing is the point — a bad placement is not a failed test on * this board, it is a wedged core. * * **The kernel rule, in one sentence: entirely inside its own firmware hole, OR * entirely inside its own kernel-config slab, never straddling.** Those are the only * two places a Tensix kernel is linked — the default link puts `.text` at * `__fw_export_text_end` in the tail of this processor's firmware hole, and a * RELOCATE_KERNEL_TEXT=1 link puts it in this processor's 12 KiB slab of the * KERNEL_CONFIG region, which is the only way an op bigger than `vector_add` fits * (ft_tt_op_l1_layout.h). The region is chosen by the ELF's own text address; every * other segment must then land inside the SAME one. So a loaded kernel can reach * neither a firmware image, nor another processor's slab, nor the CB arena. * * @param noc device access * @param x, y NoC coordinates of the target Tensix. Take them from ARC * telemetry or the coordinate module — NEVER a guess (spec §7.3) * @param processor FT_TT_PROC_* index this image belongs to * @param kind firmware or kernel; see ft_tt_image_kind * @param elf_data the ELF blob (typically from ft_tt_kernels.h) * @param elf_size its length * @param out_place may be NULL * @return FT_TT_OK, FT_TT_EINVAL, or whatever the accessor returned * * Thread-safe: no Reentrant: no Signal-safe: no */ int ft_tt_launch_load_image(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, unsigned processor, ft_tt_image_kind kind, const void *elf_data, size_t elf_size, ft_tt_image_placement *out_place); /** * The L1 floor to hand ft_tt_allocator_init() so no buffer it returns can land on a * loaded image: the highest address any of `placements` reached, rounded up to * FT_TT_L1_ALIGNMENT, and never below FT_TT_L1_USABLE_BASE. * * **This is not a formality.** The allocator's own floor is FT_TT_L1_USABLE_BASE * (0x9F80), which is exactly where the per-processor firmware `.data` staging regions * begin (docs/tt_launch_notes.md §2) — so a default allocator will hand out the BRISC * `.data` image of the firmware currently running on the core. The Phase 2 tests dodge * that by hand, with a layout header that starts everything at 0x14000; this is the * number that makes the dodge unnecessary. * * Kernel images are a different case and come out as a no-op: they land in the tail of * their own processor's firmware hole, BELOW the usable base, so they cannot collide * with the arena and the clamp returns FT_TT_L1_USABLE_BASE. * * Entries with `bytes_written == 0` are skipped — that is what an unwritten * ft_tt_image_placement looks like, i.e. a processor this program leaves idle. * * @param placements array indexed however the caller likes; NULL is allowed with * count 0 * @param count number of entries * @return an L1 address suitable for ft_tt_allocator_init(), or 0 for a bad * argument * * Thread-safe: yes Reentrant: yes Signal-safe: yes */ uint32_t ft_tt_image_placement_l1_floor(const ft_tt_image_placement *placements, unsigned count); /* ================================================================ * Reset control * ================================================================ */ /** Read the raw soft-reset register of one Tensix. */ int ft_tt_launch_read_reset(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, uint32_t *out_value); /** * Hold `risc_mask` in reset, leaving the other bits as they are (OR in). */ int ft_tt_launch_assert_reset(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, uint32_t risc_mask); /** * Release `risc_mask` from reset, leaving the other bits as they are (AND out). * * For a Tensix worker pass FT_TT_SOFT_RESET_BRISC and nothing else: BRISC firmware * releases NCRISC and the TRISCs itself once it is running, and releasing them from * the host races that (fw/ft_brisc_fw.c boot()). * * @param staggered set bit 31 so a whole grid starts skewed rather than at once. * Meaningless for a single core; pass false. */ int ft_tt_launch_deassert_reset(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, uint32_t risc_mask, bool staggered); /* ================================================================ * Launch message * ================================================================ */ /** One circular buffer for the host to describe to the firmware. Addresses and sizes * are in BYTES; the device applies its own 16-byte-word shift (fw/ft_trisc_fw.c shifts * by 4 for the compute threads; BRISC and NCRISC keep bytes). */ typedef struct ft_tt_launch_cb_entry { unsigned index; /**< CB id, 0..FT_TT_NUM_CIRCULAR_BUFFERS-1 */ uint32_t address; /**< L1 byte address of the FIFO */ uint32_t size_bytes; /**< total FIFO size */ uint32_t page_size_bytes; /**< one tile, for tile CBs */ uint32_t page_count; /**< number of pages the FIFO holds */ } ft_tt_launch_cb_entry; /** Everything the host has to tell the firmware about one program on one core. * Addresses here are ABSOLUTE L1 addresses; the encoder converts them to the * base-relative offsets the wire format wants and refuses anything that will not * fit its field. */ typedef struct ft_tt_launch_config { /** L1 address the whole message is relative to. Must be 16-byte aligned and at or * above FT_TT_L1_USABLE_BASE. */ uint32_t kernel_config_base; /** Per-processor kernel entry address, 0 for a processor this program leaves * idle. Indexed by FT_TT_PROC_*. */ uint32_t kernel_text_address[FT_TT_PROC_COUNT]; /** Per-processor kernel text size in bytes. */ uint32_t kernel_text_bytes[FT_TT_PROC_COUNT]; /** Per-processor runtime-arg block address, 0 for none. Must be within 64 KiB * above kernel_config_base — the wire field is 16 bits. */ uint32_t runtime_args_address[FT_TT_PROC_COUNT]; /** Bit `p` set enables processor `p`. Usually ft_tt_kernel_program_enables(). */ uint32_t enables; /** L1 address of the local-CB config array, 0 for none. Same 64 KiB constraint. */ uint32_t local_cb_config_address; /** Bit `i` set means CB `i` is configured. */ uint64_t local_cb_mask; /** Which NoC BRISC's dataflow kernel uses, 0 or 1. */ uint8_t brisc_noc_id; } ft_tt_launch_config; /** * Serialise a launch message into `buffer`, which must be at least * sizeof(ft_tt_kernel_config_msg) bytes. * * Nothing is written to the device and no state is touched, so this is the piece * that can be fully verified off-hardware — which is why it is a separate function * from ft_tt_launch_write_launch_message(). * * Two encodings worth knowing about: * * - `kernel_text_offset[p]` is `kernel_text_address[p] - kernel_config_base` in * 32-bit UNSIGNED WRAPAROUND. Our kernels are not execute-in-place: their text is * linked at a fixed low L1 address (just above the firmware) while * kernel_config_base sits in high, usable L1, so the difference is "negative". * The firmware computes `kernel_config_base + kernel_text_offset` in uint32 * (fw/ft_*_fw.c run_launch()), which wraps back to exactly the right address. * Well-defined, and the only encoding that works without execute-in-place kernels. * - A processor with no runtime args gets FT_TT_RTA_NO_ARGS_SENTINEL rather than 0, * which marks "no args" for the kernel's argument reader (ft_tt_mailbox.h). * * @return FT_TT_OK; FT_TT_EINVAL for a misaligned base, an out-of-range 16-bit * offset, an `enables` bit with no text address, or too small a buffer * * Thread-safe: yes Reentrant: yes Signal-safe: yes */ int ft_tt_launch_encode_launch_message(const ft_tt_launch_config *config, void *buffer, size_t buffer_bytes); /** * Serialise CB descriptors into the four-word-per-CB array the firmware's * set_up_local_circular_buffers() reads (fw/ft_*_fw.c): * word 0 address, word 1 size, word 2 page count, word 3 page size. * * The buffer is fully zeroed first, so unconfigured CBs read as zero. * * @param buffer_bytes must be at least * FT_TT_NUM_CIRCULAR_BUFFERS * FT_TT_WORDS_PER_LOCAL_CB_CONFIG * 4 * @param out_mask receives the bitmask to put in ft_tt_launch_config.local_cb_mask * @return FT_TT_OK, or FT_TT_EINVAL for a bad index or a short buffer * * Thread-safe: yes Reentrant: yes Signal-safe: yes */ int ft_tt_launch_encode_cb_config(const ft_tt_launch_cb_entry *entries, unsigned entry_count, void *buffer, size_t buffer_bytes, uint64_t *out_mask); /* ================================================================ * core_info — what the STAGED firmware's NoC sanitize resolved endpoints against * ================================================================ * * HISTORICAL as of 2026-10-08. The firmware this was written for could, in WATCHER builds, * bounds-check every NoC transaction against these tables. Foundry's own firmware (tt/fw/) * and C kernels have no watcher and read none of this, so writing it is harmless and * reading it back reports nothing. Dead: remove it together with a card round (it changes * what the host writes to L1). */ /** * One non-Tensix endpoint the firmware should recognise, in the TRANSLATED coordinate * system — the one that goes on the wire (tt-isa-documentation NoC/Coordinates.md). */ typedef struct ft_tt_launch_non_worker_core { uint16_t x; uint16_t y; ft_tt_addressable_core_type type; } ft_tt_launch_non_worker_core; /** * Everything the host has to tell the firmware about the CHIP, as opposed to the * program. Written once per core, after the mailbox is cleared and before any kernel * runs. * * This existed for one feature of the staged firmware: **NoC sanitize** (WATCHER builds * only). Every data-movement read or write first resolved its endpoint through these * tables and bounds-checked the address; a violation was recorded in * `watcher.sanitize[noc]` and the core spun — **before** the transaction was issued. * That was the whole point: it turned a * bad endpoint from "hangs the NoC, costs a physical power cycle" into "one core stopped, * with the address written down". * * Leave this unwritten and the feature is worse than useless: zeroed tables make * `noc_size_x` 0, `virtual_end_x` underflows to 0xFFFFFFFF, and every coordinate * classifies as TENSIX — so a DRAM address is bounds-checked against L1 and a correct * program is stopped. Hence the seven `WATCHER_DISABLE_*` flags the build scripts pass * by default. */ typedef struct ft_tt_launch_core_info_config { uint8_t noc_size_x; /**< NoC coordinate space, not core count */ uint8_t noc_size_y; uint8_t worker_grid_size_x; /**< enabled Tensix columns */ uint8_t worker_grid_size_y; /**< enabled Tensix rows */ uint8_t absolute_logical_x; /**< THIS core's logical coordinate */ uint8_t absolute_logical_y; uint32_t l1_unreserved_start; uint64_t noc_dram_addr_base; uint64_t noc_dram_addr_end; uint64_t noc_pcie_addr_base; uint64_t noc_pcie_addr_end; /** Borrowed, and may be NULL with count 0. Every slot NOT covered by this list is * stamped with FT_TT_CORE_COORD_INVALID / UNKNOWN, never zeroed. */ const ft_tt_launch_non_worker_core *virtual_non_worker_cores; unsigned virtual_non_worker_count; } ft_tt_launch_core_info_config; /** * Build a config for one Tensix from a live grid: every enabled DRAM bank's NoC ports in * TRANSLATED form, the NoC and worker grid sizes, and the DRAM and PCIe address windows * the firmware bounds-checks against. * * Coordinates come from ft_tt_coord against `grid`, never a literal — a wrong entry here * does not fail a check, it teaches the checker to approve a bad endpoint. So do the grid * sizes and the ports-per-bank count: they are read from the grid's architecture table, * not from a Blackhole macro, because a zeroed `noc_size_x` is not "empty" but WRONG (it * underflows `virtual_end_x` and makes every coordinate classify as TENSIX — dev notes §1) * and a constant one is wrong on any other part. * * **This is also where the kernels' compiled-in board shape meets the live one.** It calls * ft_tt_coord_check_kernel_build_assumptions() first and returns FT_TT_EIO if the card's * core or bank count is not the one `-DNUM_L1_BANKS` / `-DNUM_DRAM_BANKS` were built with. * This function is the single place that builds the device-visible view of the grid, which * is why the check lives here rather than at every call site. * * Ethernet is deliberately NOT registered: 8 banks x 3 ports already fills 24 of the 29 * virtual slots the ABI provides, and Foundry never targets an ETH core. The consequence * is the safe direction — an ETH target would be REFUSED as an unknown endpoint, not * allowed. PCIe is left out for the same reason plus a harder one: ft_tt_coord offers no * TRANSLATED PCIe coordinate, because which of the two endpoints is live comes from a * telemetry tag ft_tt_grid_info does not carry. * * @param grid from ft_tt_arc_telemetry_read_grid(); never a constructed one * @param logical_x, logical_y THIS core's logical coordinate, for `absolute_logical_*` * @param l1_unreserved_start first L1 address the host allocates from * @param storage caller-owned array the config's table points into; must outlive * the config. FT_TT_LAUNCH_MAX_NON_WORKER_CORES is enough * @param storage_count entries in `storage` * @param out_config written only on success * @return FT_TT_OK; FT_TT_EINVAL for a NULL argument; FT_TT_ENOMEM when * `storage` is too small; FT_TT_EIO when the live grid is not the * one the kernels were compiled for; or the coordinate module's code * * Thread-safe: yes Reentrant: yes Signal-safe: no (logs on failure) */ #define FT_TT_LAUNCH_MAX_NON_WORKER_CORES FT_TT_MAX_VIRTUAL_NON_WORKER_CORES int ft_tt_launch_core_info_from_grid(const ft_tt_grid_info *grid, uint32_t logical_x, uint32_t logical_y, uint32_t l1_unreserved_start, ft_tt_launch_non_worker_core *storage, unsigned storage_count, ft_tt_launch_core_info_config *out_config); /** * Serialise a core_info message into `buffer`, which must be at least * sizeof(ft_tt_core_info_msg) bytes. * * Both endpoint tables are filled with the FT_TT_CORE_COORD_INVALID / UNKNOWN sentinel * first, then the caller's entries are written over the virtual one. The physical table * is left entirely sentinel: with coordinate translation on, only the translated table * is meaningful. * * Nothing is written to the device, so this is the piece that is fully verifiable * off-hardware. * * @return FT_TT_OK; FT_TT_EINVAL for a NULL argument, too small a buffer, more entries * than the ABI's 29 virtual slots, or a coordinate that does not fit a byte * * Thread-safe: yes Reentrant: yes Signal-safe: yes */ int ft_tt_launch_encode_core_info(const ft_tt_launch_core_info_config *config, void *buffer, size_t buffer_bytes); /** * Encode and write core_info into the mailbox. * * ORDERING: after ft_tt_launch_clear_mailbox(), which zeroes the whole region and would * otherwise wipe this, and before the first go message. The firmware never writes this * field, so between those two points is the only requirement. */ int ft_tt_launch_write_core_info(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, const ft_tt_launch_core_info_config *config); /** * Make the watcher's report slots usable: `watcher.enable` = WatcherEnabled, and both * `watcher.sanitize[]` return codes = FT_TT_SANITIZE_OK. * * **Without this a NoC-sanitize violation stops the core and tells you nothing.** The * staged firmware only wrote a report into a slot that already read OK — the guard that * keeps the FIRST violation instead of the last — and ft_tt_launch_clear_mailbox() leaves * those slots at 0. Measured on the card 2026-08-12: a deliberate violation hung BRISC at * waypoint `NAWW` with `sanitize[0]` entirely zero. Foundry's firmware has no watcher. * * ORDERING: after ft_tt_launch_clear_mailbox(), like core_info. Harmless on firmware * built without watcher — nothing reads the fields. */ int ft_tt_launch_prime_watcher(const ft_tt_noc_access *noc, uint16_t x, uint16_t y); /** "OK", "noc addr overflow", ... for a sanitize return code (ft_tt_mailbox.h). Never NULL. */ const char *ft_tt_sanitize_return_code_string(uint16_t return_code); /* ================================================================ * Staging and kick * ================================================================ */ /** * Zero the whole mailbox region [FT_TT_MEM_MAILBOX_BASE, FT_TT_MEM_MAILBOX_END). * * Do this while every RISC-V is in reset and BEFORE loading firmware. It is what * makes `go_message_index`, `launch_msg_rd_ptr` and the subordinate-sync bytes start * from a known value instead of whatever the last program left there — and the * firmware indexes arrays with two of those. */ int ft_tt_launch_clear_mailbox(const ft_tt_noc_access *noc, uint16_t x, uint16_t y); /** * Write RUN_MSG_INIT into go_messages[0] so the firmware's boot handshake is * observable. * * Call this after ft_tt_launch_clear_mailbox() and before deasserting BRISC. * Without it the handshake is vacuous and silently so: a cleared mailbox already * reads RUN_MSG_DONE, because DONE is 0 (ft_tt_mailbox.h). Waiting for DONE on a zeroed * mailbox succeeds instantly whether or not any firmware is running. */ int ft_tt_launch_prime_firmware_handshake(const ft_tt_noc_access *noc, uint16_t x, uint16_t y); /** * Write `count` 32-bit runtime args at `l1_address`. Args are plain 4-byte words the * kernel reads through `rta_l1_base` (kernels/ft_tensix_kernel_args.h); there is no ioctl * and no descriptor. `l1_address` must be 4-byte aligned. */ int ft_tt_launch_plant_runtime_args(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, uint32_t l1_address, const uint32_t *args, unsigned count); /** Encode and write the launch message into mailbox launch slot 0. */ int ft_tt_launch_write_launch_message(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, const ft_tt_launch_config *config); /** * Wait for the firmware to finish booting: BRISC writes RUN_MSG_DONE to * go_messages[0] once it and all four subordinates have initialised (fw/ft_brisc_fw.c boot()). * Call this after deasserting BRISC and before the first launch message. */ int ft_tt_launch_wait_firmware_ready(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, uint32_t timeout_ms); /** Write RUN_MSG_GO into go_messages[0]. The launch message must already be there. */ int ft_tt_launch_send_go(const ft_tt_noc_access *noc, uint16_t x, uint16_t y); /* ================================================================ * Multi-core placement * ================================================================ */ /** One core in a placement group: where it is, and what it was told to run. */ typedef struct ft_tt_launch_core_placement { uint16_t x; uint16_t y; ft_tt_launch_config config; } ft_tt_launch_core_placement; /** * A group of cores running the same program with per-core configuration. * * **The capacity is the CALLER'S array, not a constant in this header.** It used to be * `FT_TT_LAUNCH_MAX_CORES 140` — every Tensix on an unharvested p150a — and that is the * shape of mistake Phase 5's derive-from-live rule exists to stop: TT's 140→120 * reduction is a compatibility measure rather than yield binning, so 120-core p150s are * a shipping configuration running this same software (dev notes §5). A group sized from * 140 does not fail there; it reserves storage for cores that are not present, and * anything that then loops to the capacity addresses them. Size it from * `ft_tt_coord_tensix_core_count(grid)` and the question cannot arise. * * Still allocation-free: the storage is the caller's, declared or carved out of one * array shared by several groups (ft_tt_group.h does exactly that). A set that was never * given storage has capacity 0 and refuses every add, rather than writing through NULL. * * Fields are public so the struct can be declared, not allocated. Read them through * ft_tt_launch_core_set_at() and _count(). */ typedef struct ft_tt_launch_core_set { ft_tt_launch_core_placement *cores; /**< borrowed; must outlive the set */ unsigned capacity; unsigned count; } ft_tt_launch_core_set; /** * Bind caller storage to a set and empty it. * * @param storage `capacity` placements, owned by the caller * @param capacity at least 1 * @return FT_TT_OK, or FT_TT_EINVAL for a NULL argument or capacity 0 */ int ft_tt_launch_core_set_init(ft_tt_launch_core_set *set, ft_tt_launch_core_placement *storage, unsigned capacity); /** How many cores the set could hold, or 0 for NULL / unbound. */ unsigned ft_tt_launch_core_set_capacity(const ft_tt_launch_core_set *set); /** Empty the set, keeping its storage. Safe on NULL. */ void ft_tt_launch_core_set_clear(ft_tt_launch_core_set *set); /** How many cores are in the set, or 0 for NULL. */ unsigned ft_tt_launch_core_set_count(const ft_tt_launch_core_set *set); /** The `index`-th placement, or NULL if there is no such entry. */ const ft_tt_launch_core_placement *ft_tt_launch_core_set_at(const ft_tt_launch_core_set *set, unsigned index); /** * Add one core at an already-derived TRANSLATED coordinate. * * A non-NULL config is validated here, by encoding it, so a group cannot be * half-staged before a bad field is discovered — the same "refuse before touching the * device" rule the rest of this file follows. * * `config` may be NULL, which is the normal case: a program's per-processor text * addresses are not known until its images have been LOADED, and loading needs the * coordinate. So build the group first, stage against it, then fill each entry in with * ft_tt_launch_core_set_configure(). An entry left unconfigured has `enables` 0 and is * refused by ft_tt_launch_write_launch_messages() rather than launching nothing. * * A coordinate already in the set is refused: two entries for one Tensix means the * second launch message overwrites the first, and the core runs whichever staged * last while the caller believes both ran. * * @return FT_TT_OK; FT_TT_EINVAL for a NULL set, a duplicate coordinate, or a * config ft_tt_launch_encode_launch_message() rejects; FT_TT_ENOMEM when the * set is full * * Thread-safe: no (mutates the set) Reentrant: no Signal-safe: no */ int ft_tt_launch_core_set_add(ft_tt_launch_core_set *set, uint16_t x, uint16_t y, const ft_tt_launch_config *config); /** * Give the `index`-th entry its config, once its images have been placed. The * coordinate is fixed at add time — that is the half a mistake in cannot be undone — * and the config is the half that is not knowable until then. * * @return FT_TT_OK; FT_TT_EINVAL for a NULL argument, an index past the end, or a * config the encoder rejects */ int ft_tt_launch_core_set_configure(ft_tt_launch_core_set *set, unsigned index, const ft_tt_launch_config *config); /** * Add the `tensix_index`-th enabled Tensix of a live grid, deriving its TRANSLATED * coordinate and CLASSIFYING it before it can be used. * * Prefer this to ft_tt_launch_core_set_add(): it is the only entry point where a * coordinate that is not a Tensix cannot get into a launch. A wrong NoC endpoint does * not fail a write, it hangs the chip, and only a physical power cycle recovers it * (spec §7.3). * * @param grid from ft_tt_arc_telemetry_read_grid(); never a constructed one * @param config may be NULL — see ft_tt_launch_core_set_add() * @return FT_TT_OK; FT_TT_EINVAL as above, or when the derived coordinate does * not classify as a Tensix; FT_TT_ENOMEM; or the coordinate module's code * * Thread-safe: no Reentrant: no Signal-safe: no */ int ft_tt_launch_core_set_add_tensix(ft_tt_launch_core_set *set, const ft_tt_grid_info *grid, uint32_t tensix_index, const ft_tt_launch_config *config); /** * Write every core's launch message. * * STAGE THE WHOLE GROUP BEFORE KICKING ANY OF IT. A loop of * write-message-then-go over N cores is N sequential single-core runs wearing a * loop; the point of a group is that they overlap. * * @param out_failed_index may be NULL; set to the core that failed * @return FT_TT_OK, or the first failure's code. Stops there — * the cores already staged are left staged */ int ft_tt_launch_write_launch_messages(const ft_tt_noc_access *noc, const ft_tt_launch_core_set *set, unsigned *out_failed_index); /** Send RUN_MSG_GO to every core in the set, in order. Same failure semantics as * ft_tt_launch_write_launch_messages(). */ int ft_tt_launch_send_go_all(const ft_tt_noc_access *noc, const ft_tt_launch_core_set *set, unsigned *out_failed_index); /** * Wait for every core to report RUN_MSG_DONE. `timeout_ms` bounds EACH core, not the * group, so the worst case is count * timeout_ms — and, as with the single-core wait, * 0 is refused rather than treated as "forever". * * A core that times out is dumped by ft_tt_launch_wait_done() before this returns. */ int ft_tt_launch_wait_done_all(const ft_tt_noc_access *noc, const ft_tt_launch_core_set *set, uint32_t timeout_ms, unsigned *out_failed_index); /** * Put every RISC-V of every core in the set back in reset. * * This is the cleanup call, so unlike the others it does NOT stop at the first * failure — a core left running is a core that can still issue NoC transactions, and * one unreachable core must not strand the rest. The first error is returned after * every core has been tried. */ int ft_tt_launch_park_all(const ft_tt_noc_access *noc, const ft_tt_launch_core_set *set); /* ================================================================ * Completion, and what to do when it does not come * ================================================================ */ /** Everything worth knowing about a core that did not finish. Read-only. */ typedef struct ft_tt_launch_state { uint32_t reset_register; uint8_t subordinate_sync[FT_TT_SUBORDINATE_MAP_SIZE]; uint32_t launch_msg_rd_ptr; uint32_t go_message_index; ft_tt_go_msg go_messages[FT_TT_GO_MESSAGE_NUM_ENTRIES]; ft_tt_kernel_config_msg kernel_config; /**< the slot launch_msg_rd_ptr selects */ ft_tt_watcher_msg watcher; /** 0xFFB120A8. `FT_TT_TENSIX_INSTRN_BUF_READY` (0x77) is the only value at which the * next firmware boot's `device_setup()` can push an instruction without stalling. */ uint32_t instrn_buf_status; /** 0xFFB12094, for context only — an invalid instruction is a different fault. */ uint32_t invalid_instrn; } ft_tt_launch_state; /** Snapshot a core's run state. Issues only reads, so it is safe on a core that is * stuck — which is exactly when it is wanted. */ int ft_tt_launch_read_state(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, ft_tt_launch_state *out_state); /** * Print a snapshot to stderr through ft_tt_log_error(), decoded: which RISC-Vs are * held in reset, what each subordinate's sync byte says, the go signal, and the * four-character waypoint each processor last recorded. * * Waypoints were written only by the staged firmware's WATCHER builds; Foundry's firmware * writes none, so the waypoint columns read "----" and the sync bytes are the diagnostic. */ void ft_tt_launch_dump_state(const ft_tt_launch_state *state, uint16_t x, uint16_t y); /** * Poll go_messages[0] until the firmware writes RUN_MSG_DONE, or until `timeout_ms` * elapses. * * **This function is the reason Phase 2 cannot hang the machine.** There is no * untimed path: a kernel that never returns costs `timeout_ms` and a dump, not a * walk to the card. Pass a real bound; 0 is rejected. * * On timeout it snapshots the core, dumps it (see ft_tt_launch_dump_state) and * returns FT_TT_EIO. The core is left exactly as it was — recovery is the caller's * decision, because asserting reset under a stuck NoC transaction is itself a risk. * * A go signal that is neither GO nor DONE means something overwrote the mailbox; * that is reported as FT_TT_ERROR with the value logged. * * @param out_state may be NULL; filled on timeout when provided * @return FT_TT_OK; FT_TT_EIO on timeout; FT_TT_ERROR on a corrupt signal * * Thread-safe: no Reentrant: no Signal-safe: no */ int ft_tt_launch_wait_done(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, uint32_t timeout_ms, ft_tt_launch_state *out_state); #endif /* FT_TT_LAUNCH_H */