Foundry/tt/ft_tt_launch.c

1349 lines
56 KiB
C

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* @file ft_tt_launch.c
* @brief Implementation of Tensix run control: image placement, launch-message
* encoding, reset release, multi-core placement groups, and a completion poll that
* always terminates.
*
* No window walk and no width choice live here. Device access is
* ft_tt_{write,read}_register_words(), through a path attached to the caller's
* window — see ft_tt_launch_noc_over_window().
*
* @status Unit-tested against a simulated L1 (tests/test_launch.c) and run on the card
* through all 13 steps of docs/tt_launch_notes.md §6.
* @issues None
* @todo None
*/
/* clock_gettime and CLOCK_MONOTONIC are POSIX.1-2008; -std=c99 turns off glibc's
* implicit feature-test macros, so ask for them explicitly (same as ft_tt_tlb.c). */
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#include "ft_tt_launch.h"
#include <string.h>
#include <time.h>
/* The kernel-config map. Read, never retyped: this header is the ONE source of the
* relocation map (build_tensix_program.sh parses the same macros out of it), and the
* placement rule below has to agree with the linker exactly. */
#include "ft_tt_op_l1_layout.h"
/* ------------------------------------------------------------------------- */
/* Per-processor geography */
/* ------------------------------------------------------------------------- */
/** Indexed by FT_TT_PROC_*: B, NC, T0, T1, T2 — the launch message's array order
* (ft_tt_mailbox.h, fw/ft_*_fw.c). */
typedef struct processor_geography {
const char *name;
uint32_t reset_bit;
uint32_t firmware_base;
uint32_t firmware_size;
uint32_t local_init_scratch;
} processor_geography;
static const processor_geography k_processors[FT_TT_PROC_COUNT] = {
{"BRISC", FT_TT_SOFT_RESET_BRISC, FT_TT_MEM_BRISC_FIRMWARE_BASE, FT_TT_MEM_BRISC_FIRMWARE_SIZE,
FT_TT_MEM_BRISC_INIT_LOCAL_L1_BASE_SCRATCH},
{"NCRISC", FT_TT_SOFT_RESET_NCRISC, FT_TT_MEM_NCRISC_FIRMWARE_BASE, FT_TT_MEM_NCRISC_FIRMWARE_SIZE,
FT_TT_MEM_NCRISC_INIT_LOCAL_L1_BASE_SCRATCH},
{"TRISC0", FT_TT_SOFT_RESET_TRISC0, FT_TT_MEM_TRISC0_FIRMWARE_BASE, FT_TT_MEM_TRISC0_FIRMWARE_SIZE,
FT_TT_MEM_TRISC0_INIT_LOCAL_L1_BASE_SCRATCH},
{"TRISC1", FT_TT_SOFT_RESET_TRISC1, FT_TT_MEM_TRISC1_FIRMWARE_BASE, FT_TT_MEM_TRISC1_FIRMWARE_SIZE,
FT_TT_MEM_TRISC1_INIT_LOCAL_L1_BASE_SCRATCH},
{"TRISC2", FT_TT_SOFT_RESET_TRISC2, FT_TT_MEM_TRISC2_FIRMWARE_BASE, FT_TT_MEM_TRISC2_FIRMWARE_SIZE,
FT_TT_MEM_TRISC2_INIT_LOCAL_L1_BASE_SCRATCH},
};
static const processor_geography *geography_for(unsigned processor)
{
return processor < FT_TT_PROC_COUNT ? &k_processors[processor] : NULL;
}
const char *ft_tt_processor_name(unsigned processor)
{
const processor_geography *geo = geography_for(processor);
return geo ? geo->name : "?";
}
uint32_t ft_tt_processor_reset_bit(unsigned processor)
{
const processor_geography *geo = geography_for(processor);
return geo ? geo->reset_bit : 0u;
}
uint32_t ft_tt_processor_firmware_base(unsigned processor)
{
const processor_geography *geo = geography_for(processor);
return geo ? geo->firmware_base : 0u;
}
uint32_t ft_tt_processor_firmware_size(unsigned processor)
{
const processor_geography *geo = geography_for(processor);
return geo ? geo->firmware_size : 0u;
}
uint32_t ft_tt_processor_local_init_scratch(unsigned processor)
{
const processor_geography *geo = geography_for(processor);
return geo ? geo->local_init_scratch : 0u;
}
/* ------------------------------------------------------------------------- */
/* Device access helpers */
/* ------------------------------------------------------------------------- */
static int noc_write(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, uint32_t addr,
const void *src, size_t bytes)
{
if (!noc || !noc->write)
return FT_TT_EINVAL;
return noc->write(noc->user, x, y, addr, src, bytes);
}
static int noc_read(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, uint32_t addr, void *dst,
size_t bytes)
{
if (!noc || !noc->read)
return FT_TT_EINVAL;
return noc->read(noc->user, x, y, addr, dst, bytes);
}
static int noc_write_u32(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, uint32_t addr,
uint32_t value)
{
return noc_write(noc, x, y, addr, &value, sizeof(value));
}
static int noc_read_u32(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, uint32_t addr,
uint32_t *out_value)
{
return noc_read(noc, x, y, addr, out_value, sizeof(*out_value));
}
/* ------------------------------------------------------------------------- */
/* NoC access over a TLB window */
/* ------------------------------------------------------------------------- */
/**
* The ft_tt_noc_access vtable over an ft_tt_noc path, and nothing else.
*
* Every byte moves through ft_tt_write_register_words() / ft_tt_read_register_words():
* the window walk, the re-aiming, the copy AND the choice of 32-bit granularity all
* live in ft_tt_noc.c. That width is not an optimisation choice — the soft-reset
* register at 0xFFB121B0 is four bytes wide and so is a go message, and a 16 B store
* over either takes its neighbours with it — which is exactly why it belongs beside the
* width enum rather than in a private helper here.
*/
static int window_write(void *user, uint16_t x, uint16_t y, uint64_t addr, const void *src,
size_t bytes)
{
ft_tt_noc_window_binding *binding = (ft_tt_noc_window_binding *)user;
ft_tt_noc_coord coord;
if (!binding || !src)
return FT_TT_EINVAL;
coord.x = x;
coord.y = y;
return ft_tt_write_register_words(&binding->path, coord, addr, src, bytes);
}
static int window_read(void *user, uint16_t x, uint16_t y, uint64_t addr, void *dst, size_t bytes)
{
ft_tt_noc_window_binding *binding = (ft_tt_noc_window_binding *)user;
ft_tt_noc_coord coord;
if (!binding || !dst)
return FT_TT_EINVAL;
coord.x = x;
coord.y = y;
return ft_tt_read_register_words(&binding->path, coord, addr, dst, bytes);
}
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)
{
int rc;
if (!ctx || !window || !binding || !out_access)
return FT_TT_EINVAL;
/* Attach, not open: the window is the caller's and outlives the accessor. */
rc = ft_tt_noc_path_attach(ctx, window, &binding->path);
if (rc != FT_TT_OK)
return rc;
out_access->write = window_write;
out_access->read = window_read;
out_access->user = binding;
return FT_TT_OK;
}
/* ------------------------------------------------------------------------- */
/* Reset control */
/* ------------------------------------------------------------------------- */
int ft_tt_launch_read_reset(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, uint32_t *out_value)
{
if (!out_value)
return FT_TT_EINVAL;
return noc_read_u32(noc, x, y, FT_TT_TENSIX_SOFT_RESET_ADDR, out_value);
}
int ft_tt_launch_assert_reset(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, uint32_t risc_mask)
{
uint32_t current = 0;
int rc = ft_tt_launch_read_reset(noc, x, y, &current);
if (rc != FT_TT_OK)
return rc;
return noc_write_u32(noc, x, y, FT_TT_TENSIX_SOFT_RESET_ADDR, current | risc_mask);
}
int ft_tt_launch_deassert_reset(const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
uint32_t risc_mask, bool staggered)
{
uint32_t current = 0;
uint32_t updated;
int rc = ft_tt_launch_read_reset(noc, x, y, &current);
if (rc != FT_TT_OK)
return rc;
updated = current & ~risc_mask;
if (staggered)
updated |= FT_TT_SOFT_RESET_STAGGERED_START;
return noc_write_u32(noc, x, y, FT_TT_TENSIX_SOFT_RESET_ADDR, updated);
}
/* ------------------------------------------------------------------------- */
/* Image loading */
/* ------------------------------------------------------------------------- */
/**
* Where the host writes one segment.
*
* Firmware `.data` is the special case: its execution address is in the processor's
* private local memory, its ELF load address is not where the firmware looks for it,
* and the firmware's `do_crt1()` copies it from a fixed per-processor scratch region
* instead (fw/ft_*_fw.c initialise_local_memory()).
*/
static uint32_t destination_of(const ft_tt_elf_segment *segment, ft_tt_image_kind kind,
const processor_geography *geo)
{
bool is_local_memory =
(segment->exec_address & FT_TT_MEM_LOCAL_BASE) == FT_TT_MEM_LOCAL_BASE;
if (kind == FT_TT_IMAGE_FIRMWARE && is_local_memory)
return (segment->exec_address & ~(uint32_t)FT_TT_MEM_LOCAL_BASE) + geo->local_init_scratch;
return segment->load_address;
}
/**
* One L1 span a kernel image is allowed to occupy, named so a refusal can say which
* span it measured against.
*/
typedef struct kernel_text_region {
const char *name;
uint32_t base; /**< inclusive */
uint32_t end; /**< exclusive; equal to `base` means "this processor has no such region" */
} kernel_text_region;
/** Index into the array kernel_text_regions() fills. */
enum {
KERNEL_REGION_FIRMWARE_HOLE = 0,
KERNEL_REGION_CONFIG_SLAB = 1,
KERNEL_REGION_COUNT = 2
};
/**
* The only two places a Tensix kernel is ever linked, in the order a refusal names them.
* The ELF chooses between them by where its text is; nothing here is a caller's opinion.
*
* - FIRMWARE HOLE — the default link. fw/ft_tensix_kernel.ld puts `.text` at `__fw_export_text_end`,
* which is in the TAIL of this processor's own firmware hole, and the linker's only
* bound is MEM_*_KERNEL_SIZE (1497 KiB, i.e. all of L1). What lies above the hole is
* the NEXT processor's firmware. Measured 2026-08-11: TRISC0's `vector_add` clears
* TRISC1's firmware base by 68 bytes, so the hole end is a live bound.
* - KERNEL-CONFIG SLAB — a RELOCATE_KERNEL_TEXT=1 link
* (`--defsym=__fw_export_text_end=<slab>`): every kernel binary at a fixed slab in the
* kernel-config region (ft_tt_op_l1_layout.h). This is not a preference — the firmware tails are
* 1296-1952 B, `matmul_compute` misses TRISC0's by 44 B and `rms_norm_compute` misses
* all three (ft_tt_op_l1_layout.h has the measured table), so without it the op
* library stops at `vector_add`.
*/
static void kernel_text_regions(unsigned processor, const processor_geography *geo,
kernel_text_region regions[KERNEL_REGION_COUNT])
{
uint32_t slab_base = ft_tt_op_text_base(processor);
regions[KERNEL_REGION_FIRMWARE_HOLE].name = "firmware hole";
regions[KERNEL_REGION_FIRMWARE_HOLE].base = geo->firmware_base;
regions[KERNEL_REGION_FIRMWARE_HOLE].end = geo->firmware_base + geo->firmware_size;
/* A slab is legal only while it is inside the kernel-config region: outside it a
* segment would land in the CB arena, or lower down on the mailbox. The map is a
* compile-time constant, so this can only fire if someone edits the header — which
* is exactly when it is wanted. */
regions[KERNEL_REGION_CONFIG_SLAB].name = "kernel-config slab";
regions[KERNEL_REGION_CONFIG_SLAB].base = slab_base;
regions[KERNEL_REGION_CONFIG_SLAB].end = slab_base + FT_TT_OP_TEXT_SLAB_BYTES;
if (slab_base < FT_TT_OP_KERNEL_CONFIG_BASE ||
slab_base + FT_TT_OP_TEXT_SLAB_BYTES >
FT_TT_OP_KERNEL_CONFIG_BASE + FT_TT_OP_KERNEL_CONFIG_BYTES)
regions[KERNEL_REGION_CONFIG_SLAB].end = regions[KERNEL_REGION_CONFIG_SLAB].base;
}
/** The region holding `address`, or NULL. An empty region holds nothing. */
static const kernel_text_region *region_holding(const kernel_text_region *regions,
uint32_t address)
{
unsigned index;
for (index = 0; index < (unsigned)KERNEL_REGION_COUNT; index++) {
if (address >= regions[index].base && address < regions[index].end)
return &regions[index];
}
return NULL;
}
/**
* THE placement rule. Refuse anything that would land outside L1, overrun a firmware
* hole, or escape a kernel-config slab.
*
* Firmware: text exactly at its own base, and no larger than its hole.
*
* Kernel: **entirely inside its own firmware hole, OR entirely inside its own
* kernel-config slab — never straddling.** The region is picked by where the ELF put
* its TEXT, because the ELF is the only party that cannot be wrong about how it was
* linked; every other segment must then agree with that choice, which is what makes a
* straddle a refusal instead of a half-write.
*
* This is the single rule Phase 4 had to keep in two files (`validate_placement()` here
* and `validate_slab()` in ft_tt_op_dispatch.c). Per region it is as strict as before or
* stricter — the hole branch gains a LOWER bound it never had — and it protects the same
* invariant on both branches: a loaded kernel can reach neither a firmware image, nor
* another processor's slab, nor the CB arena.
*/
static int validate_placement(const ft_tt_elf_image *image, ft_tt_image_kind kind,
const processor_geography *geo, unsigned processor)
{
const ft_tt_elf_segment *text = ft_tt_elf_text_segment(image);
kernel_text_region regions[KERNEL_REGION_COUNT];
const kernel_text_region *region = NULL;
unsigned index;
if (kind == FT_TT_IMAGE_FIRMWARE) {
if (text->exec_address != geo->firmware_base) {
ft_tt_log_error("launch: %s firmware text is at 0x%x, expected 0x%x", geo->name,
text->exec_address, geo->firmware_base);
return FT_TT_EINVAL;
}
if (text->memory_bytes > geo->firmware_size) {
ft_tt_log_error("launch: %s firmware text is %u bytes, hole is %u", geo->name,
text->memory_bytes, geo->firmware_size);
return FT_TT_EINVAL;
}
} else {
kernel_text_regions(processor, geo, regions);
region = region_holding(regions, text->exec_address);
if (!region) {
ft_tt_log_error("launch: %s kernel text at 0x%x is in neither legal region — its "
"firmware hole [0x%x,0x%x) nor its kernel-config slab [0x%x,0x%x)",
geo->name, text->exec_address,
regions[KERNEL_REGION_FIRMWARE_HOLE].base,
regions[KERNEL_REGION_FIRMWARE_HOLE].end,
regions[KERNEL_REGION_CONFIG_SLAB].base,
regions[KERNEL_REGION_CONFIG_SLAB].end);
return FT_TT_EINVAL;
}
if (region == &regions[KERNEL_REGION_FIRMWARE_HOLE] &&
text->exec_address == geo->firmware_base) {
ft_tt_log_error("launch: %s kernel text at 0x%x overlaps its firmware at 0x%x",
geo->name, text->exec_address, geo->firmware_base);
return FT_TT_EINVAL;
}
/* A relocated image starts AT its slab. Anything else means the ELF was not built
* with RELOCATE_KERNEL_TEXT, or this map and build_tensix_program.sh's have
* drifted apart — which loads fine and leaves the launch message pointing at
* empty L1. */
if (region == &regions[KERNEL_REGION_CONFIG_SLAB] &&
image->entry_address != region->base) {
ft_tt_log_error("launch: %s kernel entry is 0x%x, its slab starts at 0x%x — the ELF "
"was not built with RELOCATE_KERNEL_TEXT, or the two maps have "
"drifted apart",
geo->name, image->entry_address, region->base);
return FT_TT_EINVAL;
}
}
for (index = 0; index < image->segment_count; index++) {
uint32_t destination = destination_of(&image->segments[index], kind, geo);
uint32_t end = destination + image->segments[index].file_bytes;
if (end < destination || end > FT_TT_MEM_L1_SIZE) {
ft_tt_log_error("launch: %s segment %u would land at [0x%x,0x%x), outside L1",
geo->name, index, destination, end);
return FT_TT_EINVAL;
}
if (image->segments[index].file_bytes == 0u)
continue;
if (region && (destination < region->base || end > region->end)) {
ft_tt_log_error("launch: %s kernel segment %u lands at [0x%x,0x%x), outside the %s "
"[0x%x,0x%x) its text is in — it would overwrite what lives there",
geo->name, index, destination, end, region->name, region->base,
region->end);
return FT_TT_EINVAL;
}
}
return FT_TT_OK;
}
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)
{
const processor_geography *geo = geography_for(processor);
ft_tt_elf_image image;
ft_tt_image_placement placement;
unsigned index;
int rc;
if (!noc || !geo || !elf_data)
return FT_TT_EINVAL;
rc = ft_tt_elf_parse(elf_data, elf_size, &image);
if (rc != FT_TT_OK)
return rc;
rc = validate_placement(&image, kind, geo, processor);
if (rc != FT_TT_OK)
return rc;
memset(&placement, 0, sizeof(placement));
placement.entry_address = image.entry_address;
placement.text_address = image.segments[0].exec_address;
placement.text_bytes = image.segments[0].memory_bytes;
placement.lowest_l1_address = UINT32_MAX;
for (index = 0; index < image.segment_count; index++) {
const ft_tt_elf_segment *segment = &image.segments[index];
uint32_t destination = destination_of(segment, kind, geo);
if (segment->file_bytes == 0u)
continue;
rc = noc_write(noc, x, y, destination, segment->file_data, segment->file_bytes);
if (rc != FT_TT_OK) {
ft_tt_log_error("launch: writing %s segment %u to 0x%x failed: %s", geo->name, index,
destination, ft_tt_error_string(rc));
return rc;
}
if (destination < placement.lowest_l1_address)
placement.lowest_l1_address = destination;
if (destination + segment->file_bytes > placement.highest_l1_address)
placement.highest_l1_address = destination + segment->file_bytes;
placement.bytes_written += segment->file_bytes;
}
if (out_place)
*out_place = placement;
return FT_TT_OK;
}
uint32_t ft_tt_image_placement_l1_floor(const ft_tt_image_placement *placements, unsigned count)
{
uint32_t floor = FT_TT_L1_USABLE_BASE;
unsigned index;
if (!placements && count > 0u)
return 0u;
for (index = 0; index < count; index++) {
/* A placement nothing was written into is an idle processor, not an image
* at address 0. */
if (placements[index].bytes_written == 0u)
continue;
if (placements[index].highest_l1_address > floor)
floor = placements[index].highest_l1_address;
}
/* Round up: the allocator would round anyway, and a floor that splits a 16 B
* unit would put the first buffer's first bytes over the image's last. */
floor = (floor + (FT_TT_L1_ALIGNMENT - 1u)) & ~(uint32_t)(FT_TT_L1_ALIGNMENT - 1u);
return floor;
}
/* ------------------------------------------------------------------------- */
/* Launch-message encoding */
/* ------------------------------------------------------------------------- */
/** Convert an absolute L1 address into a 16-bit offset from the config base, or
* report that it does not fit. 0xFFFF is reserved as the "no args" sentinel. */
static int encode_short_offset(uint32_t address, uint32_t base, const char *what, uint16_t *out)
{
uint32_t offset;
if (address < base) {
ft_tt_log_error("launch: %s at 0x%x is below kernel_config_base 0x%x", what, address, base);
return FT_TT_EINVAL;
}
offset = address - base;
if (offset >= FT_TT_RTA_NO_ARGS_SENTINEL) {
ft_tt_log_error("launch: %s is 0x%x past kernel_config_base, over the 16-bit field", what,
offset);
return FT_TT_EINVAL;
}
*out = (uint16_t)offset;
return FT_TT_OK;
}
int ft_tt_launch_encode_launch_message(const ft_tt_launch_config *config, void *buffer,
size_t buffer_bytes)
{
ft_tt_kernel_config_msg *message = (ft_tt_kernel_config_msg *)buffer;
unsigned processor;
int rc;
if (!config || !buffer)
return FT_TT_EINVAL;
if (buffer_bytes < sizeof(*message)) {
ft_tt_log_error("launch: launch-message buffer is %zu bytes, need %zu", buffer_bytes,
sizeof(*message));
return FT_TT_EINVAL;
}
if ((config->kernel_config_base & (FT_TT_MAX_NOC_WRITE_ALIGNMENT - 1u)) != 0u) {
ft_tt_log_error("launch: kernel_config_base 0x%x is not %u-byte aligned",
config->kernel_config_base, FT_TT_MAX_NOC_WRITE_ALIGNMENT);
return FT_TT_EINVAL;
}
if (config->enables == 0u) {
ft_tt_log_error("launch: `enables` is 0, so the go message would run nothing");
return FT_TT_EINVAL;
}
memset(message, 0, sizeof(*message));
message->kernel_config_base[FT_TT_PROGRAMMABLE_CORE_TYPE_TENSIX] = config->kernel_config_base;
message->mode = FT_TT_DISPATCH_MODE_HOST;
message->enables = config->enables;
message->brisc_noc_id = config->brisc_noc_id;
message->local_cb_mask = config->local_cb_mask;
/* "No remote circular buffers": Foundry's firmware accepts only this value and halts
* on any other (fw/ft_*_fw.c run_launch()). */
message->min_remote_cb_start_index = (uint8_t)FT_TT_NUM_CIRCULAR_BUFFERS;
/* Packed struct: never take the address of a member, assign through a local. */
if (config->local_cb_config_address != 0u) {
uint16_t offset = 0;
rc = encode_short_offset(config->local_cb_config_address, config->kernel_config_base,
"local CB config", &offset);
if (rc != FT_TT_OK)
return rc;
message->local_cb_offset = offset;
}
for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) {
bool enabled = (config->enables & (1u << processor)) != 0u;
message->rta_offset[processor].rta_offset = FT_TT_RTA_NO_ARGS_SENTINEL;
message->rta_offset[processor].crta_offset = FT_TT_RTA_NO_ARGS_SENTINEL;
if (enabled && config->kernel_text_address[processor] == 0u) {
ft_tt_log_error("launch: %s is enabled but has no kernel text address",
ft_tt_processor_name(processor));
return FT_TT_EINVAL;
}
if (config->kernel_text_address[processor] != 0u) {
/* Unsigned wraparound is the encoding, not an accident — see the header. */
message->kernel_text_offset[processor] =
config->kernel_text_address[processor] - config->kernel_config_base;
message->kernel_text_size[processor] = config->kernel_text_bytes[processor];
}
if (config->runtime_args_address[processor] != 0u) {
uint16_t offset = 0;
rc = encode_short_offset(config->runtime_args_address[processor],
config->kernel_config_base, ft_tt_processor_name(processor),
&offset);
if (rc != FT_TT_OK)
return rc;
message->rta_offset[processor].rta_offset = offset;
}
}
return FT_TT_OK;
}
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)
{
const size_t required =
(size_t)FT_TT_NUM_CIRCULAR_BUFFERS * FT_TT_WORDS_PER_LOCAL_CB_CONFIG * sizeof(uint32_t);
uint32_t *words = (uint32_t *)buffer;
uint64_t mask = 0;
unsigned index;
if (!buffer || (!entries && entry_count > 0u) || !out_mask)
return FT_TT_EINVAL;
if (buffer_bytes < required) {
ft_tt_log_error("launch: CB config buffer is %zu bytes, need %zu", buffer_bytes, required);
return FT_TT_EINVAL;
}
memset(buffer, 0, required);
for (index = 0; index < entry_count; index++) {
const ft_tt_launch_cb_entry *entry = &entries[index];
size_t base_word;
if (entry->index >= FT_TT_NUM_CIRCULAR_BUFFERS) {
ft_tt_log_error("launch: CB index %u is out of range (max %u)", entry->index,
FT_TT_NUM_CIRCULAR_BUFFERS - 1u);
return FT_TT_EINVAL;
}
base_word = (size_t)entry->index * FT_TT_WORDS_PER_LOCAL_CB_CONFIG;
words[base_word + 0u] = entry->address;
words[base_word + 1u] = entry->size_bytes;
words[base_word + 2u] = entry->page_count;
words[base_word + 3u] = entry->page_size_bytes;
mask |= (uint64_t)1u << entry->index;
}
*out_mask = mask;
return FT_TT_OK;
}
/* ------------------------------------------------------------------------- */
/* core_info — the endpoint tables NoC sanitize resolves against */
/* ------------------------------------------------------------------------- */
/** Upper bound of the PCIe address window recorded in core_info (historical, see
* ft_tt_launch.h). Foundry never targets PCIe from a kernel. */
#define FT_TT_PCIE_ADDRESS_UPPER_BOUND 0x13FFFFFFFFFFFFFFuLL
const char *ft_tt_sanitize_return_code_string(uint16_t return_code)
{
switch (return_code) {
case FT_TT_SANITIZE_NO_REPORT: return "no report";
case FT_TT_SANITIZE_OK: return "OK";
case FT_TT_SANITIZE_NOC_ADDR_UNDERFLOW: return "address below the space";
case FT_TT_SANITIZE_NOC_ADDR_OVERFLOW: return "address past the end of the space";
case FT_TT_SANITIZE_NOC_ADDR_ZERO_LENGTH: return "zero length";
case FT_TT_SANITIZE_NOC_TARGET_INVALID_XY: return "endpoint is not an addressable core";
case FT_TT_SANITIZE_NOC_MULTICAST_NON_WORKER: return "multicast to a non-worker";
case FT_TT_SANITIZE_NOC_MULTICAST_INVALID_RANGE: return "multicast range invalid";
case FT_TT_SANITIZE_NOC_ALIGNMENT: return "misaligned";
case FT_TT_SANITIZE_NOC_MIXED_VIRTUAL_AND_PHYSICAL: return "mixed virtual and physical";
case FT_TT_SANITIZE_INLINE_WRITE_DRAM_UNSUPPORTED: return "inline write to DRAM";
case FT_TT_SANITIZE_NOC_ADDR_MAILBOX: return "write into the read-only region";
case FT_TT_SANITIZE_NOC_LINKED_TRANSACTION: return "linked-transaction violation";
case FT_TT_SANITIZE_L1_ADDR_OVERFLOW: return "local L1 address past the end";
case FT_TT_SANITIZE_ETH_SRC_L1_ADDR_OVERFLOW: return "eth source past the end";
case FT_TT_SANITIZE_ETH_DEST_L1_ADDR_OVERFLOW: return "eth destination past the end";
case FT_TT_SANITIZE_CB_OUT_OF_BOUNDS: return "outside the circular buffer";
case FT_TT_SANITIZE_NOC_INVALID_TXN_ID: return "invalid transaction id";
default: return "unrecognised";
}
}
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)
{
uint32_t bank_count;
uint32_t ports_per_bank;
uint32_t noc_size_x = 0;
uint32_t noc_size_y = 0;
uint32_t bank;
unsigned used = 0;
int check_rc;
if (!grid || !storage || !out_config)
return FT_TT_EINVAL;
if (logical_x > 0xFFu || logical_y > 0xFFu) {
ft_tt_log_error("core_info: logical (%u, %u) does not fit the byte-wide fields",
logical_x, logical_y);
return FT_TT_EINVAL;
}
/* This is the one place that builds the device-visible view of the grid, so
* it is where the kernels' compiled-in board shape is checked against the
* live one. On a card with a different core or bank count the images'
* addressing describes a grid that is not there, and nothing on the device
* would complain (ft_tt_coord.h). */
check_rc = ft_tt_coord_check_kernel_build_assumptions(grid);
if (check_rc != FT_TT_OK)
return check_rc;
check_rc = ft_tt_coord_noc_size(grid, &noc_size_x, &noc_size_y);
if (check_rc != FT_TT_OK)
return check_rc;
memset(out_config, 0, sizeof(*out_config));
bank_count = ft_tt_coord_dram_bank_count(grid);
ports_per_bank = ft_tt_coord_dram_ports_per_bank(grid);
for (bank = 0; bank < bank_count; bank++) {
uint32_t port;
for (port = 0; port < ports_per_bank; port++) {
ft_tt_noc_coord coord;
int rc;
if (used >= storage_count) {
ft_tt_log_error("core_info: %u slots cannot hold %u DRAM banks x %u ports",
storage_count, bank_count, ports_per_bank);
return FT_TT_ENOMEM;
}
rc = ft_tt_coord_dram_port(grid, bank, port, FT_TT_COORD_TRANSLATED, &coord);
if (rc != FT_TT_OK)
return rc;
storage[used].x = (uint16_t)coord.x;
storage[used].y = (uint16_t)coord.y;
storage[used].type = FT_TT_ADDRESSABLE_CORE_DRAM;
used++;
}
}
/* All four are LIVE, from the grid's architecture and its telemetry masks.
* A zeroed noc_size_x is not "empty", it is wrong — it underflows
* virtual_end_x to 0xFFFFFFFF and makes every coordinate classify as
* TENSIX (dev notes §1) — and a constant one is wrong on any part but this
* one. */
out_config->noc_size_x = (uint8_t)noc_size_x;
out_config->noc_size_y = (uint8_t)noc_size_y;
out_config->worker_grid_size_x = (uint8_t)ft_tt_coord_tensix_column_count(grid);
out_config->worker_grid_size_y = (uint8_t)ft_tt_coord_tensix_row_count(grid);
out_config->absolute_logical_x = (uint8_t)logical_x;
out_config->absolute_logical_y = (uint8_t)logical_y;
out_config->l1_unreserved_start = l1_unreserved_start;
/* A bank's addresses start at 0 and the window covers every bank end to end. */
out_config->noc_dram_addr_base = 0u;
out_config->noc_dram_addr_end =
(uint64_t)bank_count * ft_tt_noc_space_size(FT_TT_MEM_SPACE_DRAM);
out_config->noc_pcie_addr_base = 0u;
out_config->noc_pcie_addr_end = FT_TT_PCIE_ADDRESS_UPPER_BOUND;
out_config->virtual_non_worker_cores = storage;
out_config->virtual_non_worker_count = used;
return FT_TT_OK;
}
/** Stamp every slot of an addressable-core table with the sentinel. Zeroing is the bug
* this function exists to prevent: a zeroed slot is (0, 0) of type TENSIX — a real
* coordinate with a real type — and matches in get_core_type()'s first loop. */
static void fill_addressable_core_table(ft_tt_addressable_core *table, unsigned count)
{
unsigned index;
for (index = 0; index < count; index++) {
table[index].x = FT_TT_CORE_COORD_INVALID;
table[index].y = FT_TT_CORE_COORD_INVALID;
table[index].type = (uint8_t)FT_TT_ADDRESSABLE_CORE_UNKNOWN;
}
}
int ft_tt_launch_encode_core_info(const ft_tt_launch_core_info_config *config, void *buffer,
size_t buffer_bytes)
{
ft_tt_core_info_msg message;
unsigned index;
if (!config || !buffer)
return FT_TT_EINVAL;
if (buffer_bytes < sizeof(message)) {
ft_tt_log_error("core_info: buffer is %zu bytes, need %zu", buffer_bytes, sizeof(message));
return FT_TT_EINVAL;
}
if (config->virtual_non_worker_count > FT_TT_MAX_VIRTUAL_NON_WORKER_CORES) {
ft_tt_log_error("core_info: %u virtual non-worker cores exceeds the ABI's %u slots",
config->virtual_non_worker_count, FT_TT_MAX_VIRTUAL_NON_WORKER_CORES);
return FT_TT_EINVAL;
}
if (config->virtual_non_worker_count != 0u && !config->virtual_non_worker_cores)
return FT_TT_EINVAL;
memset(&message, 0, sizeof(message));
fill_addressable_core_table(message.non_worker_cores, FT_TT_MAX_PHYSICAL_NON_WORKER_CORES);
fill_addressable_core_table(message.virtual_non_worker_cores,
FT_TT_MAX_VIRTUAL_NON_WORKER_CORES);
for (index = 0; index < FT_TT_MAX_HARVESTED_ON_AXIS; index++) {
message.harvested_coords[index] = FT_TT_CORE_COORD_INVALID;
message.virtual_harvested_coords[index] = FT_TT_CORE_COORD_INVALID;
}
for (index = 0; index < config->virtual_non_worker_count; index++) {
const ft_tt_launch_non_worker_core *core = &config->virtual_non_worker_cores[index];
if (core->x >= FT_TT_CORE_COORD_INVALID || core->y >= FT_TT_CORE_COORD_INVALID) {
ft_tt_log_error("core_info: entry %u at (%u, %u) does not fit a byte below the 0xFF "
"sentinel", index, (unsigned)core->x, (unsigned)core->y);
return FT_TT_EINVAL;
}
message.virtual_non_worker_cores[index].x = (uint8_t)core->x;
message.virtual_non_worker_cores[index].y = (uint8_t)core->y;
message.virtual_non_worker_cores[index].type = (uint8_t)core->type;
}
message.noc_pcie_addr_base = config->noc_pcie_addr_base;
message.noc_pcie_addr_end = config->noc_pcie_addr_end;
message.noc_dram_addr_base = config->noc_dram_addr_base;
message.noc_dram_addr_end = config->noc_dram_addr_end;
message.noc_size_x = config->noc_size_x;
message.noc_size_y = config->noc_size_y;
message.worker_grid_size_x = config->worker_grid_size_x;
message.worker_grid_size_y = config->worker_grid_size_y;
message.absolute_logical_x = config->absolute_logical_x;
message.absolute_logical_y = config->absolute_logical_y;
message.l1_unreserved_start = config->l1_unreserved_start;
message.core_magic_number = FT_TT_CORE_MAGIC_WORKER;
memcpy(buffer, &message, sizeof(message));
return FT_TT_OK;
}
int ft_tt_launch_prime_watcher(const ft_tt_noc_access *noc, uint16_t x, uint16_t y)
{
unsigned index;
int rc = noc_write_u32(noc, x, y, FT_TT_MB_ADDR(watcher.enable), FT_TT_WATCHER_ENABLED);
if (rc != FT_TT_OK)
return rc;
for (index = 0; index < FT_TT_MAX_NUM_NOCS_PER_CORE; index++) {
uint16_t ok = FT_TT_SANITIZE_OK;
uint32_t address = FT_TT_MB_WATCHER_SANITIZE_ADDR(index) +
(uint32_t)offsetof(ft_tt_debug_sanitize_addr_msg, return_code);
/* return_code is 16 bits and this layer writes 32, so read-modify-write the word
* it shares with which_risc rather than taking its neighbour with it. */
uint32_t word = 0;
rc = noc_read_u32(noc, x, y, address & ~3u, &word);
if (rc != FT_TT_OK)
return rc;
if ((address & 3u) == 0u)
word = (word & 0xFFFF0000u) | ok;
else
word = (word & 0x0000FFFFu) | ((uint32_t)ok << 16);
rc = noc_write_u32(noc, x, y, address & ~3u, word);
if (rc != FT_TT_OK)
return rc;
}
return FT_TT_OK;
}
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)
{
ft_tt_core_info_msg message;
int rc = ft_tt_launch_encode_core_info(config, &message, sizeof(message));
if (rc != FT_TT_OK)
return rc;
return noc_write(noc, x, y, FT_TT_MB_CORE_INFO_ADDR, &message, sizeof(message));
}
/* ------------------------------------------------------------------------- */
/* Staging and kick */
/* ------------------------------------------------------------------------- */
int ft_tt_launch_clear_mailbox(const ft_tt_noc_access *noc, uint16_t x, uint16_t y)
{
static const uint32_t zeros[128] = {0};
uint32_t address = FT_TT_MEM_MAILBOX_BASE;
uint32_t end = FT_TT_MEM_MAILBOX_END;
/* The region base is 0x60 and its length is not a multiple of the chunk, so walk
* it explicitly rather than assuming either. */
while (address < end) {
size_t chunk = sizeof(zeros);
int rc;
if (chunk > (size_t)(end - address))
chunk = (size_t)(end - address) & ~(size_t)3u;
if (chunk == 0u)
break;
rc = noc_write(noc, x, y, address, zeros, chunk);
if (rc != FT_TT_OK)
return rc;
address += (uint32_t)chunk;
}
return FT_TT_OK;
}
/** Read-modify-write one byte of a go message without disturbing its neighbours. */
static int write_go_signal(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, uint8_t signal)
{
ft_tt_go_msg message;
int rc = noc_read(noc, x, y, FT_TT_MB_GO_SLOT_ADDR(0), &message, sizeof(message));
if (rc != FT_TT_OK)
return rc;
message.signal = signal;
return noc_write(noc, x, y, FT_TT_MB_GO_SLOT_ADDR(0), &message, sizeof(message));
}
int ft_tt_launch_prime_firmware_handshake(const ft_tt_noc_access *noc, uint16_t x, uint16_t y)
{
return write_go_signal(noc, x, y, FT_TT_RUN_MSG_INIT);
}
int ft_tt_launch_send_go(const ft_tt_noc_access *noc, uint16_t x, uint16_t y)
{
return write_go_signal(noc, x, y, FT_TT_RUN_MSG_GO);
}
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)
{
if (!args || count == 0u)
return FT_TT_EINVAL;
if ((l1_address & 3u) != 0u) {
ft_tt_log_error("launch: runtime-arg address 0x%x is not 4-byte aligned", l1_address);
return FT_TT_EINVAL;
}
return noc_write(noc, x, y, l1_address, args, (size_t)count * sizeof(uint32_t));
}
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)
{
ft_tt_kernel_config_msg message;
int rc = ft_tt_launch_encode_launch_message(config, &message, sizeof(message));
if (rc != FT_TT_OK)
return rc;
return noc_write(noc, x, y, FT_TT_MB_LAUNCH_SLOT_ADDR(0), &message, sizeof(message));
}
/* ------------------------------------------------------------------------- */
/* Multi-core placement */
/* ------------------------------------------------------------------------- */
int ft_tt_launch_core_set_init(ft_tt_launch_core_set *set, ft_tt_launch_core_placement *storage,
unsigned capacity)
{
if (!set || !storage || capacity == 0u)
return FT_TT_EINVAL;
set->cores = storage;
set->capacity = capacity;
set->count = 0u;
return FT_TT_OK;
}
unsigned ft_tt_launch_core_set_capacity(const ft_tt_launch_core_set *set)
{
return (set && set->cores) ? set->capacity : 0u;
}
void ft_tt_launch_core_set_clear(ft_tt_launch_core_set *set)
{
if (set)
set->count = 0u;
}
unsigned ft_tt_launch_core_set_count(const ft_tt_launch_core_set *set)
{
return set ? set->count : 0u;
}
const ft_tt_launch_core_placement *ft_tt_launch_core_set_at(const ft_tt_launch_core_set *set,
unsigned index)
{
if (!set || index >= set->count)
return NULL;
return &set->cores[index];
}
/** Encode a config into a throwaway buffer purely to find out whether it is legal. */
static int validate_launch_config(const ft_tt_launch_config *config)
{
ft_tt_kernel_config_msg probe;
return ft_tt_launch_encode_launch_message(config, &probe, sizeof(probe));
}
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)
{
unsigned index;
int rc;
if (!set)
return FT_TT_EINVAL;
/* An unbound set has no storage. Refusing here is what keeps a forgotten
* ft_tt_launch_core_set_init() a returned error instead of a write through NULL. */
if (!set->cores || set->capacity == 0u) {
ft_tt_log_error("launch: this placement group was never given storage — call "
"ft_tt_launch_core_set_init() with an array sized from the LIVE core "
"count");
return FT_TT_EINVAL;
}
if (set->count >= set->capacity) {
ft_tt_log_error("launch: this placement group holds at most %u cores", set->capacity);
return FT_TT_ENOMEM;
}
for (index = 0; index < set->count; index++) {
if (set->cores[index].x == x && set->cores[index].y == y) {
ft_tt_log_error("launch: (%u, %u) is already in this placement group at slot %u; the "
"second launch message would overwrite the first",
(unsigned)x, (unsigned)y, index);
return FT_TT_EINVAL;
}
}
/* Encode now so a group cannot be half-staged before a bad field surfaces. */
if (config) {
rc = validate_launch_config(config);
if (rc != FT_TT_OK)
return rc;
}
set->cores[set->count].x = x;
set->cores[set->count].y = y;
if (config)
set->cores[set->count].config = *config;
else
memset(&set->cores[set->count].config, 0, sizeof(set->cores[set->count].config));
set->count++;
return FT_TT_OK;
}
int ft_tt_launch_core_set_configure(ft_tt_launch_core_set *set, unsigned index,
const ft_tt_launch_config *config)
{
int rc;
if (!set || !config || index >= set->count)
return FT_TT_EINVAL;
rc = validate_launch_config(config);
if (rc != FT_TT_OK)
return rc;
set->cores[index].config = *config;
return FT_TT_OK;
}
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)
{
ft_tt_noc_coord core;
ft_tt_core_type type;
int rc;
if (!set || !grid)
return FT_TT_EINVAL;
rc = ft_tt_coord_tensix_by_index(grid, tensix_index, FT_TT_COORD_TRANSLATED, &core);
if (rc != FT_TT_OK)
return rc;
/* Derived is not the same as verified. Classifying the coordinate before it can
* reach a launch is the whole safety margin here (spec §7.3). */
type = ft_tt_coord_classify(grid, FT_TT_COORD_TRANSLATED, core.x, core.y);
if (type != FT_TT_CORE_TENSIX) {
ft_tt_log_error("launch: Tensix index %u derives to translated (%u, %u), which classifies "
"as %s — refusing to launch on it",
tensix_index, core.x, core.y, ft_tt_core_type_string(type));
return FT_TT_EINVAL;
}
if (core.x > UINT16_MAX || core.y > UINT16_MAX)
return FT_TT_EINVAL;
return ft_tt_launch_core_set_add(set, (uint16_t)core.x, (uint16_t)core.y, config);
}
/** Run `operation` over every core, stopping at the first failure. */
static int for_each_core(const ft_tt_noc_access *noc, const ft_tt_launch_core_set *set,
int (*operation)(const ft_tt_noc_access *, const ft_tt_launch_core_placement *,
uint32_t),
uint32_t argument, unsigned *out_failed_index)
{
unsigned index;
if (!noc || !set)
return FT_TT_EINVAL;
for (index = 0; index < set->count; index++) {
int rc = operation(noc, &set->cores[index], argument);
if (rc != FT_TT_OK) {
if (out_failed_index)
*out_failed_index = index;
return rc;
}
}
return FT_TT_OK;
}
static int write_one_launch_message(const ft_tt_noc_access *noc,
const ft_tt_launch_core_placement *core, uint32_t unused)
{
(void)unused;
return ft_tt_launch_write_launch_message(noc, core->x, core->y, &core->config);
}
static int send_one_go(const ft_tt_noc_access *noc, const ft_tt_launch_core_placement *core,
uint32_t unused)
{
(void)unused;
return ft_tt_launch_send_go(noc, core->x, core->y);
}
static int wait_one_done(const ft_tt_noc_access *noc, const ft_tt_launch_core_placement *core,
uint32_t timeout_ms)
{
return ft_tt_launch_wait_done(noc, core->x, core->y, timeout_ms, NULL);
}
int ft_tt_launch_write_launch_messages(const ft_tt_noc_access *noc,
const ft_tt_launch_core_set *set,
unsigned *out_failed_index)
{
return for_each_core(noc, set, write_one_launch_message, 0u, out_failed_index);
}
int ft_tt_launch_send_go_all(const ft_tt_noc_access *noc, const ft_tt_launch_core_set *set,
unsigned *out_failed_index)
{
return for_each_core(noc, set, send_one_go, 0u, out_failed_index);
}
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)
{
return for_each_core(noc, set, wait_one_done, timeout_ms, out_failed_index);
}
int ft_tt_launch_park_all(const ft_tt_noc_access *noc, const ft_tt_launch_core_set *set)
{
unsigned index;
int first_error = FT_TT_OK;
if (!noc || !set)
return FT_TT_EINVAL;
/* Deliberately not for_each_core(): this is the cleanup path, and one core that
* cannot be reached must not leave the rest of the group running. */
for (index = 0; index < set->count; index++) {
int rc = ft_tt_launch_assert_reset(noc, set->cores[index].x, set->cores[index].y,
FT_TT_SOFT_RESET_ALL_RISCS);
if (rc != FT_TT_OK && first_error == FT_TT_OK)
first_error = rc;
}
return first_error;
}
/* ------------------------------------------------------------------------- */
/* Completion */
/* ------------------------------------------------------------------------- */
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_launch_read_state(const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
ft_tt_launch_state *out_state)
{
uint32_t slot;
int rc;
if (!out_state)
return FT_TT_EINVAL;
memset(out_state, 0, sizeof(*out_state));
rc = ft_tt_launch_read_reset(noc, x, y, &out_state->reset_register);
if (rc != FT_TT_OK)
return rc;
/* Same 4 KiB debug block as the reset register, so this costs nothing to reach — and it
* is the one thing a read can say that a mailbox read cannot (ft_tt_launch.h).
* BEST EFFORT on purpose: this is a diagnostic snapshot, and giving up on the whole
* mailbox because a debug register did not answer would lose the half that matters. */
if (noc_read_u32(noc, x, y, FT_TT_TENSIX_INSTRN_BUF_STATUS_ADDR,
&out_state->instrn_buf_status) != FT_TT_OK)
out_state->instrn_buf_status = FT_TT_TENSIX_INSTRN_BUF_UNREAD;
if (noc_read_u32(noc, x, y, FT_TT_TENSIX_DBG_INVALID_INSTRN_ADDR,
&out_state->invalid_instrn) != FT_TT_OK)
out_state->invalid_instrn = FT_TT_TENSIX_INSTRN_BUF_UNREAD;
rc = noc_read(noc, x, y, FT_TT_MB_SUBORDINATE_SYNC_ADDR, out_state->subordinate_sync,
sizeof(out_state->subordinate_sync));
if (rc != FT_TT_OK)
return rc;
rc = noc_read_u32(noc, x, y, FT_TT_MB_LAUNCH_RD_PTR_ADDR, &out_state->launch_msg_rd_ptr);
if (rc != FT_TT_OK)
return rc;
rc = noc_read_u32(noc, x, y, FT_TT_MB_GO_MESSAGE_INDEX_ADDR, &out_state->go_message_index);
if (rc != FT_TT_OK)
return rc;
rc = noc_read(noc, x, y, FT_TT_MB_GO_MESSAGES_ADDR, out_state->go_messages,
sizeof(out_state->go_messages));
if (rc != FT_TT_OK)
return rc;
/* The read pointer comes from the device and may be garbage on a wedged core, so
* clamp it rather than reading an arbitrary L1 address. */
slot = out_state->launch_msg_rd_ptr % FT_TT_LAUNCH_MSG_BUFFER_NUM_ENTRIES;
rc = noc_read(noc, x, y, FT_TT_MB_LAUNCH_SLOT_ADDR(slot), &out_state->kernel_config,
sizeof(out_state->kernel_config));
if (rc != FT_TT_OK)
return rc;
return noc_read(noc, x, y, FT_TT_MB_WATCHER_ADDR, &out_state->watcher,
sizeof(out_state->watcher));
}
/** Four waypoint characters, or "----" when the firmware was built without watcher
* support and the bytes are all zero. */
static void format_waypoint(const uint8_t *bytes, char out[5])
{
unsigned index;
for (index = 0; index < 4u; index++)
out[index] = (bytes[index] >= 0x20u && bytes[index] < 0x7Fu) ? (char)bytes[index] : '-';
out[4] = '\0';
}
void ft_tt_launch_dump_state(const ft_tt_launch_state *state, uint16_t x, uint16_t y)
{
unsigned processor;
if (!state)
return;
ft_tt_log_error("--- Tensix (%u, %u) run state ---", (unsigned)x, (unsigned)y);
ft_tt_log_error(" soft reset 0x%08x: BRISC=%s NCRISC=%s TRISC0=%s TRISC1=%s TRISC2=%s",
state->reset_register,
(state->reset_register & FT_TT_SOFT_RESET_BRISC) ? "held" : "running",
(state->reset_register & FT_TT_SOFT_RESET_NCRISC) ? "held" : "running",
(state->reset_register & FT_TT_SOFT_RESET_TRISC0) ? "held" : "running",
(state->reset_register & FT_TT_SOFT_RESET_TRISC1) ? "held" : "running",
(state->reset_register & FT_TT_SOFT_RESET_TRISC2) ? "held" : "running");
/* subordinate_sync is [NCRISC, TRISC0, TRISC1, TRISC2] — NCRISC first (ft_tt_mailbox.h,
* fw/ft_brisc_fw.c). 0x00 = done, 0x80 = go, 0x40 = init, 1 = load, 3 = reset streams. */
ft_tt_log_error(" subordinate sync NCRISC=0x%02x TRISC0=0x%02x TRISC1=0x%02x TRISC2=0x%02x",
state->subordinate_sync[0], state->subordinate_sync[1],
state->subordinate_sync[2], state->subordinate_sync[3]);
ft_tt_log_error(" go[0].signal=0x%02x go_message_index=%u launch_msg_rd_ptr=%u",
state->go_messages[0].signal, state->go_message_index,
state->launch_msg_rd_ptr);
ft_tt_log_error(" instrn_buf_status=0x%08x (%s) invalid_instrn=0x%08x",
state->instrn_buf_status,
state->instrn_buf_status == FT_TT_TENSIX_INSTRN_BUF_READY
? "ready — a boot can push into the Tensix FIFO"
: (state->instrn_buf_status == FT_TT_TENSIX_INSTRN_BUF_UNREAD
? "unread"
: "NOT READY — BRISC is stalled pushing into the Tensix FIFO"),
state->invalid_instrn);
ft_tt_log_error(" launch: enables=0x%08x mode=%u config_base=0x%08x cb_offset=0x%04x",
state->kernel_config.enables, state->kernel_config.mode,
state->kernel_config.kernel_config_base[FT_TT_PROGRAMMABLE_CORE_TYPE_TENSIX],
state->kernel_config.local_cb_offset);
for (processor = 0; processor < FT_TT_PROC_COUNT; processor++) {
char waypoint[5];
format_waypoint(state->watcher.debug_waypoint[processor].waypoint, waypoint);
ft_tt_log_error(" %-6s text_offset=0x%08x size=%u waypoint=%s",
ft_tt_processor_name(processor),
state->kernel_config.kernel_text_offset[processor],
state->kernel_config.kernel_text_size[processor], waypoint);
}
ft_tt_log_error(" (waypoints read '----' unless the firmware was built with "
"WATCHER_ENABLED)");
/* The sanitize slots are the reason a hang can be a report. Print them only when one
* is populated: without WATCHER_ENABLED / with WATCHER_DISABLE_NOC_SANITIZE the
* firmware never writes here, and two lines of zeros every timeout is noise. */
for (processor = 0; processor < FT_TT_MAX_NUM_NOCS_PER_CORE; processor++) {
const ft_tt_debug_sanitize_addr_msg *report = &state->watcher.sanitize[processor];
if (report->return_code == FT_TT_SANITIZE_NO_REPORT)
continue;
ft_tt_log_error(" NoC %u SANITIZE: %s (code %u) — %s %s at noc_addr 0x%016llx, "
"local L1 0x%08x, %u bytes, risc %u%s", processor,
ft_tt_sanitize_return_code_string(report->return_code),
report->return_code, report->is_write ? "write" : "read",
report->is_target ? "target" : "local buffer",
(unsigned long long)report->noc_addr, report->l1_addr, report->len,
report->which_risc, report->is_multicast ? ", multicast" : "");
ft_tt_log_error(" the transaction was NOT issued — sanitize ran before the "
"command register write");
}
}
/**
* Poll one 4-byte go message until it reads `target`, bounded by a wall-clock
* timeout. Shared by the firmware-boot handshake and the kernel-completion wait,
* which differ only in the signal they start from.
*/
static int poll_go_signal(const ft_tt_noc_access *noc, uint16_t x, uint16_t y, uint8_t pending,
uint32_t timeout_ms, const char *what, ft_tt_launch_state *out_state)
{
uint64_t deadline;
if (timeout_ms == 0u) {
ft_tt_log_error("launch: %s was asked to wait forever; refusing (a hang here costs a "
"power cycle)", what);
return FT_TT_EINVAL;
}
deadline = monotonic_milliseconds() + timeout_ms;
for (;;) {
ft_tt_go_msg message;
int rc = noc_read(noc, x, y, FT_TT_MB_GO_SLOT_ADDR(0), &message, sizeof(message));
if (rc != FT_TT_OK)
return rc;
if (message.signal == FT_TT_RUN_MSG_DONE)
return FT_TT_OK;
if (message.signal != pending) {
ft_tt_log_error("launch: %s read go signal 0x%02x, expected 0x%02x or done (0x%02x) "
"— the mailbox has been overwritten",
what, message.signal, pending, FT_TT_RUN_MSG_DONE);
return FT_TT_ERROR;
}
if (monotonic_milliseconds() >= deadline) {
ft_tt_launch_state state;
ft_tt_log_error("launch: %s timed out after %u ms on Tensix (%u, %u)", what, timeout_ms,
(unsigned)x, (unsigned)y);
if (ft_tt_launch_read_state(noc, x, y, &state) == FT_TT_OK) {
ft_tt_launch_dump_state(&state, x, y);
if (out_state)
*out_state = state;
} else {
ft_tt_log_error("launch: could not read the core's state either");
}
return FT_TT_EIO;
}
}
}
int ft_tt_launch_wait_firmware_ready(const ft_tt_noc_access *noc, uint16_t x, uint16_t y,
uint32_t timeout_ms)
{
return poll_go_signal(noc, x, y, FT_TT_RUN_MSG_INIT, timeout_ms, "firmware boot", NULL);
}
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)
{
return poll_go_signal(noc, x, y, FT_TT_RUN_MSG_GO, timeout_ms, "kernel completion", out_state);
}