diff --git a/app/apps/ci_smoke/tiny_baseline.c b/app/apps/ci_smoke/tiny_baseline.c new file mode 100644 index 00000000..4def78d0 --- /dev/null +++ b/app/apps/ci_smoke/tiny_baseline.c @@ -0,0 +1,313 @@ +/* Copyright (c) 2026 Yuchi Miao; SPDX-License-Identifier: MulanPSL-2.0 */ +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +/* Verification image only. All placement uses the existing flat SRAM linker. + * Separate destinations preserve every job's result until validation, outside + * the measured window. No CPU access is made to a DMA-owned destination. */ +#define RS_BASELINE_WORDS 1024U +#define RS_BASELINE_JOBS 16U +#define RS_BASELINE_GUARDS 16U +#define RS_BASELINE_CASES 7U +#define RS_BASELINE_ITERATIONS 4096U +#define RS_BASELINE_SEED UINT32_C(0x12345678) +#define RS_BASELINE_EXPECTED UINT32_C(0xf6e37410) +#define RS_BASELINE_PATTERN UINT32_C(0xa519c300) +#define RS_BASELINE_GUARD UINT32_C(0x5a36c9e7) + +typedef struct { + uint32_t before[RS_BASELINE_GUARDS]; + uint32_t words[RS_BASELINE_WORDS]; + uint32_t after[RS_BASELINE_GUARDS]; +} rs_baseline_buffer_t; + +typedef struct { + uint64_t cycles; + uint64_t instructions; + uint64_t kernel_cycles; + uint64_t cpu_wait; + uint64_t dma_wait; + rs_onchip_sram_perf_t memory; + uint32_t checksum; + uint32_t kernel_calls; + uint32_t payload_bytes; +} rs_baseline_result_t; + +static _Alignas(64) volatile rs_baseline_buffer_t rs_baseline_source; +static _Alignas(64) volatile rs_baseline_buffer_t rs_baseline_destinations[RS_BASELINE_JOBS]; +static _Alignas(64) rs_dma_tcd_t rs_baseline_tcds[RS_BASELINE_JOBS][4]; +static rs_baseline_result_t rs_baseline_results[RS_BASELINE_CASES]; +static volatile uint32_t rs_baseline_seed = RS_BASELINE_SEED; + +static uint64_t rs_baseline_cycles(void) { + uint32_t high; + uint32_t low; + uint32_t again; + do { + high = __RV_CSR_READ(CSR_CYCLEH); + low = __RV_CSR_READ(CSR_CYCLE); + again = __RV_CSR_READ(CSR_CYCLEH); + } while (high != again); + return ((uint64_t)high << 32U) | low; +} + +static uint64_t rs_baseline_instructions(void) { + uint32_t high; + uint32_t low; + uint32_t again; + do { + high = __RV_CSR_READ(CSR_INSTRETH); + low = __RV_CSR_READ(CSR_INSTRET); + again = __RV_CSR_READ(CSR_INSTRETH); + } while (high != again); + return ((uint64_t)high << 32U) | low; +} + +static uint32_t rs_baseline_kernel(uint32_t value) { + for (uint32_t index = 0U; index < RS_BASELINE_ITERATIONS; ++index) { + value ^= value << 13U; + value ^= value >> 17U; + value ^= value << 5U; + } + return value; +} + +static bool rs_baseline_cpu(rs_baseline_result_t *result) { + const uint32_t seed = rs_baseline_seed; + const uint64_t begin = rs_baseline_cycles(); + const uint32_t value = rs_baseline_kernel(seed); + __COMPILER_BARRIER(); + result->kernel_cycles += rs_baseline_cycles() - begin; + result->checksum = value; + ++result->kernel_calls; + return value == RS_BASELINE_EXPECTED; +} + +static void rs_baseline_initialize(void) { + for (uint32_t index = 0U; index < RS_BASELINE_GUARDS; ++index) { + rs_baseline_source.before[index] = RS_BASELINE_GUARD; + rs_baseline_source.after[index] = RS_BASELINE_GUARD; + } + for (uint32_t index = 0U; index < RS_BASELINE_WORDS; ++index) { + rs_baseline_source.words[index] = RS_BASELINE_PATTERN ^ index; + } + for (uint32_t job = 0U; job < RS_BASELINE_JOBS; ++job) { + for (uint32_t index = 0U; index < RS_BASELINE_GUARDS; ++index) { + rs_baseline_destinations[job].before[index] = RS_BASELINE_GUARD; + rs_baseline_destinations[job].after[index] = RS_BASELINE_GUARD; + } + for (uint32_t index = 0U; index < RS_BASELINE_WORDS; ++index) { + rs_baseline_destinations[job].words[index] = UINT32_C(0xcccccccc); + } + for (uint32_t part = 0U; part < 4U; ++part) { + rs_dma_tcd_t *const tcd = &rs_baseline_tcds[job][part]; + *tcd = (rs_dma_tcd_t){0}; + if (part < 3U) { + tcd->next_ptr = (uint32_t)(uintptr_t)&rs_baseline_tcds[job][part + 1U]; + } + tcd->source = (uint32_t)(uintptr_t)&rs_baseline_source.words[part * 256U]; + tcd->destination = + (uint32_t)(uintptr_t)&rs_baseline_destinations[job].words[part * 256U]; + tcd->byte_count = UINT32_C(1024); + tcd->control = RS_DMA_TCD_VALID | RS_DMA_TCD_SRC_INC | RS_DMA_TCD_DST_INC | + (UINT32_C(16) << RS_DMA_TCD_BURST_SHIFT); + } + } + __asm__ volatile("fence rw, rw" ::: "memory"); +} + +static bool rs_baseline_readback(uint32_t *checksum) { + uint32_t hash = UINT32_C(2166136261); + for (uint32_t index = 0U; index < RS_BASELINE_GUARDS; ++index) { + if ((rs_baseline_source.before[index] != RS_BASELINE_GUARD) || + (rs_baseline_source.after[index] != RS_BASELINE_GUARD)) { + return false; + } + } + for (uint32_t job = 0U; job < RS_BASELINE_JOBS; ++job) { + for (uint32_t index = 0U; index < RS_BASELINE_GUARDS; ++index) { + if ((rs_baseline_destinations[job].before[index] != RS_BASELINE_GUARD) || + (rs_baseline_destinations[job].after[index] != RS_BASELINE_GUARD)) { + return false; + } + } + for (uint32_t index = 0U; index < RS_BASELINE_WORDS; ++index) { + const uint32_t expected = RS_BASELINE_PATTERN ^ index; + const uint32_t observed = rs_baseline_destinations[job].words[index]; + if ((observed != expected) || (rs_baseline_source.words[index] != expected)) { + return false; + } + /* Explicit unsigned modulo-2^32 diagnostic hash; full comparison + * above is authoritative, rather than a hash-only memory check. */ + hash = (hash ^ observed) * UINT32_C(16777619); + } + } + *checksum = hash; + return true; +} + +static bool rs_baseline_dma(uint32_t case_id, rs_baseline_result_t *result) { + const bool descriptors = case_id == 6U; + const bool concurrent_cpu = case_id >= 5U; + const uint8_t burst = (case_id == 3U) ? 1U : 16U; + rs_dma_status_t status; + rs_dma_config_t config = { + .kind = RS_DMA_KIND_MM_TO_MM, + .request = RS_DMA_REQUEST_SOFTWARE, + .source = (uintptr_t)&rs_baseline_source.words[0], + .byte_count = UINT32_C(4096), + .width = RS_DMA_WIDTH_32, + .source_increment = true, + .destination_increment = true, + .burst_beats = burst, + }; + + for (uint32_t job = 0U; job < RS_BASELINE_JOBS; ++job) { + config.destination = (uintptr_t)&rs_baseline_destinations[job].words[0]; + if (rs_dma_configure(RS_DMA_CHANNEL_BULK, &config) != RS_OK) { + return false; + } + /* The existing convenience HAL waits for each TCD in software. This + * verification image uses the public handwritten register ABI to start + * one finite four-TCD hardware job, so CPU work can overlap its fetches. + * No scheduling/HAL contract is changed. */ + RS_DMA_CH_REG(RS_DMA_CHANNEL_BULK, RS_DMA_CH_REG_TCD_HEAD) = + descriptors ? (uint32_t)(uintptr_t)&rs_baseline_tcds[job][0] : UINT32_C(0); + RS_DMA_CH_REG(RS_DMA_CHANNEL_BULK, RS_DMA_CH_REG_TCD_COUNT) = descriptors ? 4U : 0U; + __asm__ volatile("fence rw, rw" ::: "memory"); + if (rs_dma_start(RS_DMA_CHANNEL_BULK) != RS_OK) { + return false; + } + if (concurrent_cpu && !rs_baseline_cpu(result)) { + return false; + } + if ((rs_dma_wait(RS_DMA_CHANNEL_BULK, RS_TIMEOUT_DEFAULT) != RS_OK) || + (rs_dma_get_status(RS_DMA_CHANNEL_BULK, &status) != RS_OK) || status.busy || + !status.done || status.error || status.aborted || (status.remaining != 0U) || + (status.bytes_done != (descriptors ? UINT32_C(1024) : UINT32_C(4096)))) { + return false; + } + /* BYTES_DONE is per descriptor in hardware, not a chain total. */ + result->payload_bytes += UINT32_C(4096); + __asm__ volatile("fence rw, rw" ::: "memory"); + } + return true; +} + +static bool rs_baseline_case(uint32_t case_id) { + rs_baseline_result_t *const result = &rs_baseline_results[case_id]; + uint64_t cycles_begin; + uint64_t instructions_begin; + bool pass = true; + + if (case_id >= 2U) { + rs_baseline_initialize(); + } + if ((rs_dma_reset(RS_DMA_CHANNEL_BULK) != RS_OK) || + (rs_sysctrl_set_perf_control(false, true, false) != RS_OK) || + (rs_sysctrl_set_perf_control(true, false, false) != RS_OK)) { + return false; + } + __COMPILER_BARRIER(); + cycles_begin = rs_baseline_cycles(); + instructions_begin = rs_baseline_instructions(); + if (case_id == 1U) { + pass = rs_baseline_cpu(result); + } else if (case_id == 2U) { + for (uint32_t job = 0U; job < RS_BASELINE_JOBS; ++job) { + for (uint32_t index = 0U; index < RS_BASELINE_WORDS; ++index) { + rs_baseline_destinations[job].words[index] = rs_baseline_source.words[index]; + } + } + result->payload_bytes = UINT32_C(65536); + } else if (case_id >= 3U) { + pass = rs_baseline_dma(case_id, result); + } else { + /* Empty-window calibration intentionally includes measurement overhead. */ + } + __COMPILER_BARRIER(); + result->instructions = rs_baseline_instructions() - instructions_begin; + result->cycles = rs_baseline_cycles() - cycles_begin; + if ((rs_sysctrl_set_perf_control(false, false, true) != RS_OK) || + (rs_sysctrl_read_perf_counter(RS_SYSCTRL_PERF_MGMT_WAIT, &result->cpu_wait) != RS_OK) || + (rs_sysctrl_read_perf_counter(RS_SYSCTRL_PERF_DMA_WAIT, &result->dma_wait) != RS_OK) || + (rs_onchip_sram_read_performance(&result->memory) != RS_OK)) { + return false; + } + if (pass && (case_id >= 2U)) { + pass = rs_baseline_readback(&result->checksum); + } + return pass && (result->memory.error_responses == 0U); +} + +static void rs_baseline_finish(bool pass, uint8_t code) { + rs_uart_status_t status; + uint32_t timeout = RS_TIMEOUT_DEFAULT; + bool drained = false; + + while (timeout != 0U) { + --timeout; + if (rs_uart_get_status(&status) != RS_OK) { + break; + } + if ((status.tx_level == 0U) && ((status.flags & UINT32_C(4)) == 0U)) { + drained = true; + break; + } + } + rs_sysctrl_write_test_status(pass && drained, drained ? code : UINT8_C(250)); + for (;;) { + } +} + +int main(void) { + static const char *const names[RS_BASELINE_CASES] = { + "empty", "cpu", "memory", "dma1", "dma16", "contention", "tcd", + }; + rs_onchip_sram_info_t memory; + + /* Source identity is retained by the host evidence manifest. Replay checks + * the stable memory/ISA contract, not equality to a future RTL git hash. + * The separate normal acceptance image keeps its full ArchInfo checks. */ + if ((rs_uart_init(RS_CPU_CLOCK_HZ, UART_BPS) != RS_OK) || + (rs_onchip_sram_probe(&memory) != RS_OK) || !memory.present || + (memory.memory_bytes != UINT32_C(131072)) || (memory.bank_count != 32U) || + (memory.data_bytes != 4U) || ((__RV_CSR_READ(CSR_MISA) & UINT32_C(1)) != 0U)) { + rs_baseline_finish(false, 1U); + } + for (uint32_t case_id = 0U; case_id < RS_BASELINE_CASES; ++case_id) { + if (!rs_baseline_case(case_id)) { + printf("R2_ERROR case=%u\n", case_id); + rs_baseline_finish(false, (uint8_t)(case_id + 2U)); + } + } + /* UART output is entirely outside every performance window. */ + printf("R2_HEADER version=1 cpu_hz=%u words=%u jobs=%u iterations=%u seed=0x%08x\n", + RS_CPU_CLOCK_HZ, RS_BASELINE_WORDS, RS_BASELINE_JOBS, RS_BASELINE_ITERATIONS, + RS_BASELINE_SEED); + for (uint32_t case_id = 0U; case_id < RS_BASELINE_CASES; ++case_id) { + const rs_baseline_result_t *const result = &rs_baseline_results[case_id]; + printf("R2_CASE id=%u name=%s cycles=%llu instructions=%llu kernel_cycles=%llu " + "kernel_calls=%u payload_bytes=%u checksum=0x%08x cpu_wait=%llu dma_wait=%llu " + "sram_reads=%u sram_writes=%u sram_rbeats=%u sram_wbeats=%u sram_stalls=%u " + "sram_errors=%u\n", + case_id, names[case_id], (unsigned long long)result->cycles, + (unsigned long long)result->instructions, (unsigned long long)result->kernel_cycles, + result->kernel_calls, result->payload_bytes, result->checksum, + (unsigned long long)result->cpu_wait, (unsigned long long)result->dma_wait, + result->memory.read_requests, result->memory.write_requests, + result->memory.read_beats, result->memory.write_beats, result->memory.stall_cycles, + result->memory.error_responses); + } + printf("R2_COMPLETE version=1 cases=%u\n", RS_BASELINE_CASES); + rs_baseline_finish(true, 0U); + return 0; +} diff --git a/docs/ip/tiny-soc-r2-baseline.md b/docs/ip/tiny-soc-r2-baseline.md new file mode 100644 index 00000000..d8bcdb36 --- /dev/null +++ b/docs/ip/tiny-soc-r2-baseline.md @@ -0,0 +1,299 @@ +# Tiny R2-P1 reproducible baseline + +This runbook implements the measurement boundary of +[TINY-R2-P1](tiny-soc.md#tiny-r2-p1---reproducible-baseline-constraints-and-measurements). +It does not implement the later R2 architecture. Actual results and open gates +belong in the [verification ledger](tiny-soc-verification.md). The approved +starting revision is `bfdc3ffaf6cb0a134314272ac1b0f8b504d0b09c`; uncommitted +implementation runs identify that parent **and** their retained input contents, +not a fictional clean implementation commit. + +## Configuration and ownership + +Use `configs/ci/ihp130-tiny.mk`, `SOC=TINY`, `PDK=IHP130`, no PLL, 24 MHz, +128 KiB macro SRAM, CSR-enabled RV32IM firmware and `ld2_all_sram`. The current +hart uses `hazard3_cpu_1port`, has C/debug enabled and A disabled, and shares a +globally serialized AXI32 fabric with four-channel central DMA. Ordinary +`bringup`/`ci_smoke` firmware and their ArchInfo identity checks are unchanged. + +The separate `tiny-r2-baseline-image` target substitutes only the application +entrypoint, using the existing Tiny-selected CRT, effective `-Os`, +`-march=rv32im_zicsr -mabi=ilp32` and flat linker script. A linker map is added +as evidence, not a placement experiment. No new APP, register, SDK API, DMA +request, interrupt or capability is introduced. No managed source is edited. + +QFN64/IO assignments, 128 KiB main storage and the six private Crypto banks +specified for future integration remain protected. The current Tiny source +closure has 32 main-SRAM macros and no Crypto instance; tied-off stream ports +are not Crypto integration. No CPU, banking, reset, interconnect, RCU, PSRAM, +DVP, SPI, PIO-lite or PPALite implementation is included here. Compiler/LTO +and bank-placement experiments belong to R2-P3 and later work. + +## Workload and measurement contract + +The image executes these seven cases in order, without enabling interrupts or +starting other DMA clients. It owns channel 3 and uses bounded waits. Each +simulator replays the **same retained HEX**, from cold reset, three times. + +| ID | Case | Fixed work | +| ---: | --- | --- | +| 0 | `empty` | Empty-window instrumentation calibration; no automatic subtraction. | +| 1 | `cpu` | 4096 xorshift32 iterations, seed `0x12345678`, shifts 13/17/5; expected `0xf6e37410`. | +| 2 | `memory` | Copy 1024 words to each of 16 destinations; source word `i` is `0xa519c300 XOR i`. | +| 3 | `dma1` | Sixteen 4096-byte direct memory copies, one-beat bursts. | +| 4 | `dma16` | The same copies with up-to-16-beat bursts. | +| 5 | `contention` | The same burst-16 jobs, each with one fixed CPU kernel after START and before bounded polling. | +| 6 | `tcd` | Sixteen finite jobs, each with four linked 1024-byte hardware descriptors and the same concurrent CPU kernel. | + +The source and each destination have 64-byte alignment and 16 guard words on +each side. Sixteen distinct destinations retain every job's data until full +readback after the measured window. This uses existing SRAM; the linker map +and symbol check require at least 4 KiB of remaining stack headroom. No buffer +is assigned to a particular physical macro or future 32 KiB arbitration group. + +Every destination and source word is compared, and every guard is checked. +The diagnostic word-wise FNV-style unsigned modulo-2^32 hash is `0xbc5f5dc5` +over the complete 65536-byte result; it does not replace full comparison. +Descriptors are 64-byte aligned. The existing convenience TCD HAL waits for +individual descriptors in software; this image uses the existing handwritten +register ABI for one finite four-descriptor hardware submission. Hardware +`BYTES_DONE` is per descriptor: the final value is 1024, while the observed +chain payload is 4096. No scheduler or HAL semantics are changed. + +Initialization, descriptor construction, full readback, hashing and all UART +reporting occur outside the seven performance windows. DMA configuration, +START, bounded polling and status checks are inside the workload envelope; +CPU kernel cycles are also reported separately. UART must drain before the +terminal status is written. + +The image clears the existing performance counters, enables PERF_CTRL, reads +cycle/instruction counters using stable RV32 high/low/high reads, executes the +case, then disables/snapshots counters. Report raw cycle and instruction +deltas, CPI, kernel calls/cycles, payload bytes, CPU/DMA admission waits and +SRAM requests/beats/stalls/errors. The CSR interval and PERF_CTRL interval +have different endpoints; neither silently subtracts the other. + +The testbench's `+tiny_r2_baseline` observers report: + +- AXI address handshakes, accepted-address-to-B/RLAST maximum latency, + accepted payload bytes and longest consecutive `VALID && !READY` interval + for each of AW/W/B/AR/R, separately for CPU and DMA. +- Descriptor read bytes, using the address range taken from the retained ELF + symbol rather than a new hardware register. For case 6, descriptor reads + total 4096 bytes in addition to 65536 payload read and write bytes. +- AHB address acceptance and terminal data completion, separated by HPROT's + instruction/data bit. The existing single port is not an independent I/D + local-SRAM path. The future three-cycle local budget is not asserted here. + +CPU transactions can straddle PERF_CTRL boundaries. Pending counts identify +right-censored AXI transactions; their unknown terminal latency is not zero. +DMA must have no pending transaction at each completed window. Request/response +accounting is checked. Observed maximum stalls are workload observations, +not universal service bounds. Independent-bank conflicts are unavailable on +this serialized architecture. Monitors drive no design signal and are excluded +from netlist elaboration. Icarus's optional UART decoder only observes the pad; +Verilator retains the existing accepted-TX-byte diagnostic output. + +## Reproducibility and verdicts + +Tool installation uses the unchanged dependency lock and an isolated cache. +Activate it before invoking the new targets: + +```sh +python3 scripts/development_environment.py \ + --cache .cache/retrosoc/development/tiny-r2-p1 \ + --tool verilator --tool verible --tool sv2v --tool iverilog \ + --tool yosys --tool opensta --tool riscv_gnu bootstrap +source .cache/retrosoc/development/tiny-r2-p1/activate.sh +``` + +The runner verifies archive SHA-256, installation markers and selected binary +paths, records tool versions/binary hashes, and checks the seven managed Tiny +dependencies against their clean locked revisions. A checksum mismatch is a +failure, not permission to replace the lock. Existing user installations stay +untouched. The Python requirement files are included in the input digest. + +For Icarus, the selected `VVP` must resolve to `vvp` beside the verified +`iverilog` executable. A name, absolute path or symlink to that same runtime +is accepted; another installation or missing executable is rejected before +model compilation or simulation. The attempt records the runtime's absolute +path, version, binary hash and locked archive identity, launches that absolute +path, and rechecks the binary before each cold run and at final validation. +Aggregation also checks the retained runtime and each actual launch command. +Historical attempts without this identity remain historical evidence; they +cannot satisfy the repaired collector's Icarus aggregation requirement. + +New image manifests retain `compiler_command` independently of disassembly, +symbol and size commands. It must match the associated `compile.json` command. +The original `image-ozwo38_b` manifest incorrectly stored the final size command +in this convenience field; its hashed `compile.json` retains the actual GCC +command. That historical manifest is not rewritten. The original image remains +usable for matched-binary replay with its original artifacts and source identity. + +Use one timestamp for a source snapshot; use a new timestamp after executable +inputs change. From the repository root: + +```sh +export BUILD_TIMESTAMP=$(date +%Y-%m-%d-%H-%M) +make CONFIG=configs/ci/ihp130-tiny.mk SOC=TINY PDK=IHP130 tiny-r2-baseline-image +make CONFIG=configs/ci/ihp130-tiny.mk SOC=TINY PDK=IHP130 \ + SIMU=VERILATOR tiny-r2-baseline-sim +stdbuf -oL -eL make CONFIG=configs/ci/ihp130-tiny.mk SOC=TINY PDK=IHP130 \ + SIMU=IVERILOG SOC_SIM_TIME=14400 tiny-r2-baseline-sim +make CONFIG=configs/ci/ihp130-tiny.mk SOC=TINY PDK=IHP130 tiny-r2-baseline-report +``` + +The image target builds normal firmware first to prepare the unchanged runtime +and linker inputs, then always compiles a new retained measurement image. The +simulation target builds a fresh model once before its three runs. Ordinary +`make --dry-run` prints these commands without executing the evidence driver. +The measurement-only cycle limit is `TINY_BASELINE_MAX_CYCLES=100000000`; +the existing `SOC_SIM_TIME` bounds each run in wall seconds (default 1800). +The pin-level Icarus command above explicitly allows 14400 seconds. The first +10800-second campaign reached the last UART result line after all seven cases +and readbacks, then timed out before terminal acceptance. Those attempts remain +failed. The larger host budget preserves the workload and binary; `stdbuf` +only makes host log output line buffered. Its GNU coreutils 8.32 executable +and preload-library hashes are retained in the timeout diagnosis. Neither the +host budget nor output buffering is a hardware operating point. Up to `min(JOBS, 3)` independent +cold-reset simulator processes run concurrently, sharing only immutable model +and firmware files. Logs and verdicts remain separate. These limits are retained +in the attempt and do not change normal Tiny acceptance or workload cycles. + +Generated evidence stays under the approved variant: + +```text +build/ihp130-tiny--/ + meta/tiny-r2-p1/ + binaries/image-*/ ELF/BIN/HEX, linker map, symbols, disassembly, size + inputs/ input hashes, source archive, worktree patch + workloads//attempt-*/ + inputs/ actual RTL/generated/model inputs and source snapshot + run-*.log full output, flow JSON and strict verdict JSON + attempt.json firmware identity separate from RTL identity + validation/ exact command logs and structured gate results + baseline-report.json both simulators, same image, all six successful runs +``` + +Attempts are never reused. A latest-attempt pointer is changed before work +starts, so a new failure cannot expose an old successful attempt as current. +Input contents are checked before/after compilation and simulation; archives +include new untracked implementation files. Reports revalidate logs, hashes, +terminal verdicts, actual runtime input contents and compiled-model identity. +All cold-start repetitions and both simulators must agree on the measured +records. A missing/failed run leaves the report incomplete and returns failure. + +For later matched-binary comparisons, supply +`TINY_BASELINE_HEX=/absolute/path/to/binaries/image-.../tiny_baseline.hex` to +the simulation target. Preserve its adjacent original `image.json` and +artifacts. The image validates stable SRAM/ISA requirements, not equality to +the candidate RTL's source ID. Its original source/configuration stays recorded +separately from the new RTL source. Normal Tiny acceptance remains responsible +for candidate ArchInfo/build-identity verification. Do not rebuild and relabel +a binary as the old baseline. + +Successful execution requires command exit zero, valid passing SYSCTRL +TEST_STATUS, `SIM_TEST_PASS Tiny`, complete validated workload/observer records, +and absence of the repository's forbidden markers: `FAILED`, `FATAL`, +`assertion failed`, `%Error`, `SIM_TEST_FAIL`, `SIM_TEST_TIMEOUT`. UART alone +cannot pass. The usual `check_simulation.py` verdict is retained separately. +No baseline report is a timing, power or physical qualification result. + +## Clock, reset, macro and exception audit + +The source-bound baseline consumes Tiny's clock inventory, not Mini's. +`u_clock_buffer/clk_o` feeds the CPU, SRAM, AXI/APB, RTC and watchdog at 24 MHz. +CLINT uses a divide-by-24 tick enable. There is no dynamic SYS/MEM/PCLK +transition, main-SRAM divider, or main-SRAM CDC in this executable baseline. +CPU WFI clock gating is not enabled by the current wrapper's XH3POWER setting. + +The 32 main macros bind through +`u_soc.u_sram.gen_memory.gen_bank[0..31].u_ram.u_mem` to +`RM_IHPSG13_1P_1024x32_c2_bm_bist`, with `A_CLK` on the system clock. +The locked PDK supplies Verilog/LEF/GDS and typical 1.20 V/25 C, slow +1.08 V/125 C and fast 1.32 V/-55 C SRAM Liberty views. The physical adapter +pairs the last view with the standard-cell fast -40 C corner; record this +actual pairing, not an assumed common temperature. + +The SRAM views contain setup/hold, clock-to-output and pulse-width information; +no explicit `minimum_period`/`min_period` entry was found in the selected views. +This is a characterization gap for a qualified macro maximum frequency, not +proof of a failed or supported rate. Icarus RTL uses `-gno-specify`; behavioral +results do not evaluate those timing arcs. + +| Reset path | Current source behavior and endpoint | +| --- | --- | +| External reset -> `u_por_rst_sync` | Common default STAGE=3, async assertion/synchronous release; POR domain includes watchdog functional timer. | +| POR and watchdog request -> `u_system_rst_sync` | Default STAGE=3; resets fabric, SRAM control, peripherals, tick logic and debug wrapper. SRAM payload has no reset clear. | +| System/hart request -> `u_mgmt_debug_reset.u_core_rst_sync` | Default STAGE=3; waits for CPU AHB/AXI adapter idle, resets the hart/adapter and acknowledges release. | +| TRST_N -> `u_jtag_rst_sync` | Default STAGE=3 in buffered TCK domain. | +| DMI hard reset -> synchronizer -> `u_dmi_rst_sync` | System-domain default STAGE=3; Hazard3's existing async APB bridge owns the DMI crossing. | + +The JSON inventory lists only system/JTAG domains and debug DMI/hard-reset +crossings. It omits the POR, hart and DMI release endpoint detail above and +the broader external-input synchronizer inventory. Passing its checker is +structural metadata evidence, not exhaustive CDC/RDC or reset-release proof. +P1 records these omissions; it does not implement the later five-local-edge, +CPU-last/domain-barrier reset contract or reduce FIFO reset load. + +Core STA's generator creates a 41.666666667 ns clock at +`u_clock_buffer/clk_o` and a 100 ns JTAG port clock, declares asynchronous +groups, applies 0.2/0.1 ns setup/hold uncertainty and 0.1 ns transition, then +false-paths external reset ports and all inputs/outputs. There is no extra +generated system clock in this core-STA inventory. Its broad port exceptions +exclude board/pad and external reset timing; do not extend them to hide an +internal reset path. + +The separate physical SDC creates external/JTAG clocks at pad p2c pins and +a divide-by-one generated system clock at `u_clock_buffer.u_sg13g2_buf_1/X`. +It uses 20%-period IO delay assumptions, 0.006 output load, early/late derates +0.95/1.05 and fanout 10. These assumptions are not a qualified board contract. +The historical adapter has 52 signal and 84 placed PADs; it does not qualify +the frozen QFN64 package. P1 does not run place/route or change that adapter. + +After approved synthesis/STA, retain the actual consumed SDC, netlist/config, +library hashes, macro count, cell/area reports, reset-path endpoints, fanout +and setup/hold WNS/TNS. Keep tool completion separate from timing closure. +Historical -455.18 ns reset-path WNS must retain its original attribution. + +The frozen future CPU/main-SRAM SYS targets remain 96/192/240 MHz, including +actual macro periods of approximately 5.208/4.167 ns at 192/240 MHz. Joint +macro/system qualification needs minimum period/pulse widths, setup/hold, +clock-to-output, bank/return paths, CTS/skew, extracted PVT and reset evidence. +XPI keeps its divided MEM ceiling and Crypto storage remains PCLK. P1 enables +none of these rates; a half-rate main-SRAM fallback is forbidden. + +## Validation and acceptance boundary + +Run focused baseline/parser tests and existing Tiny protocol, shared debug +reset, SRAM-capacity and register-parity tests before software/style gates, +firmware and both RTL simulators. A skipped/no-op RTL fixture is unrun coverage. + +```sh +python3 -m pytest -q tests/test_tiny_r2_baseline.py tests/test_tiny.py \ + tests/test_mgmt_debug.py tests/test_dma_register_parity.py \ + tests/test_onchip_sram_register_parity.py \ + 'tests/test_onchip_ram.py::test_onchip_ram_axi4_capacity_protocol_and_performance[128]' +make CONFIG=configs/ci/ihp130-tiny.mk sw-format-check sw-policy-check sw-host-test +ruff check . +python3 scripts/regress.py --root . --suite pr --soc TINY --pdk IHP130 --dry-run +python3 scripts/regress.py --root . --suite nightly --soc TINY --pdk IHP130 --dry-run +python3 -m pytest -q +python3 scripts/regress.py --root . --suite pr --soc TINY --pdk IHP130 +``` + +The Tiny PR matrix includes SVA Verilator, full SDK Icarus, Yosys, +`netsim-boot` and OpenSTA; Tiny-filtered nightly currently adds no distinct +case. Preserve the 10800-second Yosys allowance. A failed synthesis prevents +dependent netlist/STA acceptance. Compact netlist boot is not full C netlist +coverage. Hosted `--behavioral-only` CI supplies none of these physical stages. + +P1 accepts a reproducible measurement baseline and explicit attempted-gate +results, not an automatically passing product. Negative timing, external +Tiny JTAG, exhaustive reset/CDC/RDC, package, PLL/Pad, extracted PVT and silicon +qualification remain downstream gates. The current testbench ties TCK low; +shared debug-reset tests must not be relabeled external Tiny debug acceptance. +Existing warning baselines and observational metrics policy are unchanged. +No Required-rule MISRA deviation is added; formatting/policy/host tests remain +partial checks, not certification. Review is required before accepting P1 or +authorizing a new phase. diff --git a/docs/ip/tiny-soc-verification.md b/docs/ip/tiny-soc-verification.md index d975cbd1..634a0b48 100644 --- a/docs/ip/tiny-soc-verification.md +++ b/docs/ip/tiny-soc-verification.md @@ -7,8 +7,9 @@ or register ABI version. The selected executable baseline remains `configs/ci/ihp130-tiny.mk`, IHP130, 24 MHz/no PLL; faster configurations and their physical acceptance do not exist merely because this plan names them. -The R2 freeze changes documentation only. All R2 firmware, RTL/formal, -performance, synthesis/netlist, timing and physical results are **pending**. +The R2 freeze itself changed documentation only. Separately authorized +R2-P1 implementation and validation are recorded below; the other R2 +implementation and qualification phases remain **pending**. Historical `TINY-P0` through `TINY-P12` identifiers and evidence are preserved under their original scope; the mapping in the main contract transfers outstanding obligations without renaming old results or declaring them passed. @@ -170,7 +171,7 @@ not a prerequisite to the architectural improvements in R2-P3 through R2-P5. | Active phase | Required evidence and completion boundary | Status | | --- | --- | --- | | TINY-R2-P0 - Performance Contract and Roadmap Freeze | Reviewable requirements, exact old/new phase mapping, invariant package/IO counts, same-frequency CPU/main-SRAM ownership, explicit performance budgets, links and commands. Record actual documentation checks separately. | Documentation freeze only; no hardware result | -| TINY-R2-P1 - Reproducible Baseline, Constraints and Measurements | Reviewed source/profile/lock/tool identity; current 24 MHz functional and workload baseline; clocks, reset endpoints, constraints and timing-exception audit; counters and comparison methodology. Historical WNS is a risk reference, not a refreshed measurement. | Pending | +| TINY-R2-P1 - Reproducible Baseline, Constraints and Measurements | Reviewed source/profile/lock/tool identity; current 24 MHz functional and workload baseline; clocks, reset endpoints, constraints and timing-exception audit; counters and comparison methodology. Historical WNS is a risk reference, not a refreshed measurement. | Implementation and baseline evidence ready for review; timing and warning failures recorded; phase acceptance pending human review | | TINY-R2-P2 - Reset Distribution and CPU/SRAM Clock Feasibility | Reset distribution and payload-reset semantics; locked IHP130 main-SRAM macro timing, candidate common CPU/SRAM periods and representative paths; early clock/reset feasibility with explicit gaps. No final routed/PVT pass is implied. | Pending | | TINY-R2-P3 - Software and DMA Scheduling | Compiler and placement A/B results, finite DMA ownership, burst/chunk scheduling, WS2812 refill budget and workload correctness on the available platform. | Pending | | TINY-R2-P4 - Dual-Port Hazard3 and Four-Bank Local SRAM | Actual separate I/D paths, four independent 32 KiB bank frontends, SYS-clocked physical main-SRAM macros, local latency/fairness, FENCE.I, debug/reset and early inventory/SDC/macro-binding updates. | Pending | @@ -330,6 +331,212 @@ netlist, STA and physical qualification were not run in this documentation phase. Actual main-SRAM macro timing and joint high-frequency qualification remain pending; no SRAM or CPU maximum frequency is established here. +## R2-P1 implementation evidence (2026-10-05) + +Target SoCs: `TINY`; profile `configs/ci/ihp130-tiny.mk`; PDK `IHP130`, +24 MHz/no PLL. The approved source parent is +`bfdc3ffaf6cb0a134314272ac1b0f8b504d0b09c`. These are uncommitted implementation +runs, identified by retained source/input hashes and archives in addition to +the Git parent. They must not be presented as clean-commit release evidence. +The [baseline runbook](tiny-soc-r2-baseline.md) specifies workloads, observation +endpoints, retained-image replay, commands and the clock/reset/constraint audit. + +The implementation adds a separate finite measurement image, optional +testbench observers and fail-closed evidence collection. Normal Tiny +acceptance firmware, hardware, register/SDK ABI, profile, dependency versions, +warning baselines and metrics policy are unchanged. The existing flat SRAM +layout is retained; no compiler or banking experiment belongs to these results. + +Environment restoration uses +`.cache/retrosoc/development/tiny-r2-p1/activate.sh`. The original local +Verilator/Icarus/OpenSTA archives did not match the lock. The new isolated +installation verifies all seven selected tool archives against the unchanged +lock; the original installations are preserved. A sandbox DNS failure and a +Yosys download timeout are retained as failed attempts; the successful restore +reused the checksum-matching local Yosys/GCC archives. Locked Python packages +and both behavioral/full-flow doctor checks passed. + +Initial development evidence is under +`build/ihp130-tiny-2026-10-05-11-19-fa1c4ce1e303/meta/tiny-r2-p1/`. +The PR campaign uses `BUILD_TIMESTAMP=2026-10-05-12-15`; the final workload +campaign uses `BUILD_TIMESTAMP=2026-10-05-12-30`. Each configuration retains +its own variant. Workload attempts preserve the +binary's original identity separately from the simulator source. Early failed +instrumentation attempts are not acceptance: a comment interpreted as a tool +pragma and an incorrectly formatted AXI observation record were corrected in +the testbench without modifying production RTL. An intermediate Icarus +measurement attempt was explicitly interrupted to strengthen artifact and +repeat-consistency revalidation; its outer result records exit 130 and cannot +qualify the baseline. Independent normal acceptance and PR runs were preserved. + +Current completed checks: + +| Gate | Retained result and boundary | +| --- | --- | +| Lock and configuration | Dependency lock, Tiny address/pin/topology/clock-reset checks and selected tool doctors passed. | +| Focused tests | Initial 58-case Tiny/protocol/reset/SRAM/parity/collector set passed. The final collector's 41 unit tests also passed, including modified-image, mixed-repeat and mismatched-verdict rejection. Counts overlap and are not additive coverage. | +| Software and source policy | Software format/policy/host tests, Make/RTL format, owned RTL style, readiness and Ruff passed. SVA ASIC lint completed; warnings retain the unchanged observation policy. | +| Normal firmware, Verilator | Command and terminal checks passed at 3,744,261 cycles. | +| Normal firmware, Icarus | Command and terminal checks passed at the same 3,744,261 cycles; wall duration 1522.098 seconds. This is full SDK/IRQ/DMA/watchdog acceptance, not netlist or physical evidence. | +| Final workload, Verilator | Three independent cold-reset runs passed with identical records at 18,458,948 terminal cycles. | +| Final workload, Icarus | All three retry runs passed strict command/terminal/log acceptance at the same 18,458,948 cycles; wall durations 10878.451, 10851.610 and 10832.133 seconds. The earlier 10800-second attempts remain failed. | +| Cross-simulator baseline | Final report passed: all six cold-reset runs use the same retained HEX and agree on every parsed workload/observer record; source, model, firmware, log and verdict integrity checks passed. This is functional/measurement evidence, not physical qualification. | +| Regression selection | Explicit TINY/IHP130 PR and nightly dry-runs passed and select the same current Tiny matrix. Nightly adds no distinct Tiny execution. | +| Full Pytest | 1476 passed, 1 skipped in 3574.13 seconds. The skipped PDF retrieval test requires `pypdf`. The suite began before final collector hardening; the 41-case collector suite was run after that hardening. | +| PR runner | Completed with exit zero, including SVA Verilator, full-firmware Icarus, synthesis, netlist boot and STA. Warning observations failed and timing did not close; runner completion is not a physical acceptance verdict. | +| Yosys, balanced | Completed in 311.874 seconds, 207697 cells, 9107224.664998 square micrometers of library area, 32 main-SRAM macros. These are current baseline values, not a like-for-like improvement over historical results. | +| Icarus netlist boot | Strict command/terminal/checker success at 29362 cycles, 151.389 seconds. Full C firmware netlist execution remains unrun. | +| OpenSTA | Command completed in 13.521 seconds. Setup WNS -455.18 ns and TNS -13049600 ns; hold WNS/TNS zero. **Timing qualification failed.** | +| Clock/reset netlist audit | 32 direct flat main-SRAM A_CLK loads of `u_clock_buffer/clk_o`; 29605 structural reset-reachable endpoints using all arcs. This is not CDC/RDC or reset-tree timing closure. | + +The retained workload image is +`build/ihp130-tiny-2026-10-05-12-30-fa1c4ce1e303/meta/tiny-r2-p1/binaries/image-ozwo38_b/`. +BIN SHA-256 is +`b98b981f397af5f3a1d9cc15157726e77a8d7f21c68c1691cdc9b1beecbf4293`; +HEX SHA-256 is +`ba24c2dfd727a905f39b644df254e47262c39ab4960ff2614be44402020758b2`. +Its `.text` is 9836 bytes, `.bss` is 76736 bytes, BIN is 10080 bytes and +remaining SRAM above `_ebss` is 44480 bytes. The normal flat linker still +reports its existing RWX LOAD-segment warning; it is not hidden by a linker +or warning-policy change. + +The final Verilator attempt `workloads/verilator/attempt-vvytd6pz` and Icarus +retry `workloads/iverilog/attempt-yu79tkc8` produced the following identical +raw measurements in all six accepted runs. No partial or timed-out run +contributes to this table. Cycles and instructions cover the specified workload envelope, +including submission/polling overhead but excluding initialization, readback +and UART. Empty-window overhead is reported without subtraction. + +| Case | Cycles | Retired instructions | Payload bytes | CPU kernel cycles | CPU / DMA admission-wait cycles | +| --- | ---: | ---: | ---: | ---: | ---: | +| empty | 137 | 12 | 0 | 0 | 0 / 0 | +| cpu | 98663 | 32830 | 0 | 98343 | 0 / 0 | +| memory | 754159 | 213076 | 65536 | 0 | 0 / 0 | +| dma1 | 258772 | 32594 | 65536 | 0 | 114688 / 105056 | +| dma16 | 71444 | 7601 | 65536 | 0 | 37888 / 6416 | +| contention | 1638244 | 530481 | 65536 | 1603696 | 37888 / 5520 | +| tcd | 1639540 | 530498 | 65536 | 1604848 | 39040 / 6208 | + +The contention cases each execute 16 CPU kernels; the CPU-only case executes +one. Each TCD case additionally reads 4096 descriptor bytes. Full per-owner +latency, backpressure, outstanding-transaction and SRAM records are retained +in the attempt JSON/logs. Payload-per-cycle is payload bytes divided by the +whole workload envelope; contention-case values include the concurrent CPU +work and must not be labeled isolated DMA bandwidth or physical throughput. + +The synthesis/netlist/STA variant is +`build/ihp130-tiny-2026-10-05-12-15-19f1e9c571ee/` (`APP=ci_smoke`). +Its netlist SHA-256 is +`69195dc4050725d5cdbb9fedc213f675cedc6470492c84b2704f8ba535b4a29f`. +The fresh STA command consumes that netlist and its generated core SDC at the +slow corner; the netlist hash differs from the inspected October 2 run. +The equal historical/current WNS values therefore remain separately attributed. +The current worst path is `u_soc.s_rst_n_reg/Q` through the reset-dependent +I2C0 FIFO data path to `u_soc.u_i2c0/u_i2c_reg.u_rx_fifo.r_storage[4]_0__reg/D`. +No reset optimization or new timing exception was used to hide that result. + +The final workload variant's `meta/tiny-r2-p1/constraints/` contains +`source-audit.json`, the generated `reset-clock-audit.tcl` and its command +result, clock/reset endpoint reports, and `synthesis-sta-evidence.json` binding +the consumed libraries, netlist, SDC, reports and compact boot binaries. The +query reuses the completed netlist and unchanged SDC; it does not run PnR or +alter constraints. Synthesis uses typical 1.20 V/25 C libraries, while STA +uses the selected slow 1.08 V/125 C views and IO 3.0 V corner. + +There is no committed `quality/warnings/ihp130-tiny/` baseline. Consequently +the ASIC lint observation reports 399 new signatures; the actual CI variant +reports one Icarus and 14 Yosys signatures. Icarus reports unsupported +edge-sensitive `ifnone` model paths. Yosys observations include asynchronous +load-value warnings, combinational-network messages and the temporary SCL-cache +rename/conversion fallback to Liberty. The standalone warning commands fail; +the regression preserves its existing non-blocking observation behavior. +No warning signature or metrics policy was edited. Explicit CI-variant metrics +were collected because the runner's final default-APP observation targets do +not select its CI synthesis variant. + +The first final-source Icarus campaign +(`workloads/iverilog/attempt-cwvkmh4t`) reached the 10800-second host timeout +in all three runs (exit 124). Each log contains all 28 observer records and +reaches the last UART case result. Since the firmware prints these records +only after every case and complete memory readback succeeds, this establishes +continued functional progress, but the missing final terminal marker still +prevents acceptance. No RTL error or cycle-limit timeout was reported. +`validation/iverilog-timeout-diagnosis.json` retains the failed results, logs +and host-stdio tool hashes. The previously observed 22 complete observer rows +matched Verilator in all three runs; partial output was never promoted to PASS. + +The retry uses the identical retained image and production RTL with +`SOC_SIM_TIME=14400` and host line-buffered output. It does not change the +100000000-cycle bound or verdict rules. All three runs completed successfully +between 18:46:45 and 18:47:31 Asia/Shanghai. The final cross-simulator +aggregation also passed. Its result is +`build/ihp130-tiny-2026-10-05-12-30-fa1c4ce1e303/meta/tiny-r2-p1/baseline-report.json`; +the exact aggregation command and exit-zero result are retained under the same +root in `validation/baseline-report.log` and `validation/baseline-report.json`. +Implementation and reproducible baseline evidence are ready for human review; +this record does not automatically accept P1 or authorize another phase. +P1 does not qualify 24 MHz timing, the QFN64 package, external Tiny JTAG, +96/192/240 MHz operation, or any later R2/PIO/SPI/PPALite phase. No additional +MISRA deviation is introduced; the automated software gates remain partial +checks rather than certification. Historical records below retain their +original source, dates, stage IDs and qualification boundaries. + +### R2-P1 evidence-tool repair (2026-10-05 through 2026-10-06) + +The read-only review of the seven-file implementation found two issues: +the selected `VVP` executable was not bound to the recorded locked Icarus +installation (P1), and `image.json.compiler_command` was overwritten by the +last post-processing command (P2). The original image's hashed `compile.json` +still contains the actual GCC invocation. Neither finding changes the +previously recorded simulation results or establishes a hardware failure. + +The separately approved repair changes only `scripts/tiny_r2_baseline.py`, +its focused tests, this ledger and the baseline runbook. The runner validates +the selected runtime before compilation, launches its verified absolute path, +retains its version/binary/archive identity, and rechecks it with actual run +commands at aggregation. The image producer preserves the compiler command +separately from its post-processing commands. Historical artifacts are not +rewritten to add provenance that they did not originally record. + +Repair evidence uses +`build/ihp130-tiny-2026-10-05-20-02-fa1c4ce1e303/meta/tiny-r2-p1/`. +The focused suite passed 55 cases, including rejection before compilation for +external/missing runtimes and invalid installation markers, retained-runtime +and launch-command checks, and compiler-command preservation. Full Pytest +passed 1492 cases with one skipped PDF retrieval test (`pypdf` unavailable), +in 3539.14 seconds. Ruff and explicit TINY/IHP130 PR/nightly dry-runs passed. +These are local results; no new hosted CI or physical run is claimed. + +The new metadata-validation image is `binaries/image-watrhx9n/`. Its retained +`compiler_command` equals the actual GCC command in `compile.json`; the +independent check is retained as `validation/compiler-metadata.json` and its +log. This image does not replace the original matched binary. +Verilator attempt `workloads/verilator/attempt-86h7qnqw` passed all three +cold-reset repetitions using the explicit original +`image-ozwo38_b/tiny_baseline.hex`. Icarus attempt +`workloads/iverilog/attempt-axv8qwr6` also passed all three repetitions at +18,458,948 cycles; wall durations were 10858.884, 10844.051 and 10923.896 +seconds. All completed between 00:10:02 and 00:11:22 Asia/Shanghai on October 6. +The recorded runtime is Icarus vvp 13.0 from the isolated locked installation, +binary SHA-256 +`7b5521fbaf86c519603e824e189c767790424ea6914f3dadeda438136d5eaba7`. +Each retained flow command launches that absolute executable. Runtime binary, +archive and launch-command checks passed after simulation and at aggregation. + +The repair root's `baseline-report.json` passed all six runs, with original HEX +SHA-256 `ba24c2dfd727a905f39b644df254e47262c39ab4960ff2614be44402020758b2`. +`validation/previous-baseline-comparison.json` additionally checks that both +simulators retain the original image manifest and identical measurements to +the preceding campaign. Only the evidence collector changed among the +archived executable inputs; the old image, old attempts and old report remain +unmodified. Both review fixes now have focused and end-to-end evidence ready +for human re-review, not automatic phase acceptance. + +Production RTL, testbench, firmware, Make targets, selected profile, dependency +versions, warning baselines and metrics policy are unchanged by this repair. +The earlier failed timing qualification and all unrun physical/debug/PVT gates +remain open. No phase acceptance or subsequent-phase authorization is implied. + ## Archived P6/P10 planning and documentation evidence The remainder of this section preserves the prior planning and P10 document diff --git a/rtl/tiny/Makefile b/rtl/tiny/Makefile index faaaa48b..5eb17870 100644 --- a/rtl/tiny/Makefile +++ b/rtl/tiny/Makefile @@ -148,4 +148,28 @@ netsim-boot: netcomp tiny-boot-image | manifest $(PYTHON) $(ROOT_PATH)/scripts/check_simulation.py --log $(NETLIST_SIM_ROOT)/sim.log \ --result $(NETLIST_SIM_ROOT)/result-sim-check.json --require 'SIM_TEST_PASS' -.PHONY: tiny-boot-image \ No newline at end of file +.PHONY: tiny-boot-image + +# R2-P1 verification image: normal APP selection and the flat linker stay intact. +# Image creation always compiles and snapshots inputs before retaining a binary. +# Simulation rebuilds its model once, then replays the same image three times. +TINY_BASELINE_TOOL := $(ROOT_PATH)/scripts/tiny_r2_baseline.py +TINY_BASELINE_HEX ?= +TINY_BASELINE_MAX_CYCLES ?= 100000000 + +tiny-r2-baseline-image: firmware + $(PYTHON) $(TINY_BASELINE_TOOL) --root $(ROOT_PATH) --variant-root $(VARIANT_ROOT) image \ + --objcopy $(OBJC) --objdump $(DUMP) --nm $(CROSS)nm --size $(CROSS)size -- \ + $(CC) $(CFLAGS) $(INC_PATH) $(CRT_SRCS) $(ROOT_PATH)/app/apps/ci_smoke/tiny_baseline.c + +tiny-r2-baseline-sim: | manifest + $(PYTHON) $(TINY_BASELINE_TOOL) --root $(ROOT_PATH) --variant-root $(VARIANT_ROOT) simulate \ + --simulator $(SIM_TOOL_NAME) --vvp $(VVP) --timeout $(SOC_SIM_TIME) --jobs $(JOBS) \ + --max-cycles $(TINY_BASELINE_MAX_CYCLES) $(if $(TINY_BASELINE_HEX),--hex $(TINY_BASELINE_HEX)) -- \ + make --no-print-directory -j1 -B CONFIG=$(CONFIG_PATH) BUILD_TIMESTAMP=$(BUILD_TIMESTAMP) \ + $(foreach var,$(CONFIG_KEY_VARS),$(var)=$($(var))) SIMU=$(SIMU) SYNTH=NONE STA=NONE comp + +tiny-r2-baseline-report: + $(PYTHON) $(TINY_BASELINE_TOOL) --root $(ROOT_PATH) --variant-root $(VARIANT_ROOT) report + +.PHONY: tiny-r2-baseline-image tiny-r2-baseline-sim tiny-r2-baseline-report \ No newline at end of file diff --git a/rtl/tiny/dv/retrosoc_tiny_tb.sv b/rtl/tiny/dv/retrosoc_tiny_tb.sv index d29f15c6..2242b6bb 100644 --- a/rtl/tiny/dv/retrosoc_tiny_tb.sv +++ b/rtl/tiny/dv/retrosoc_tiny_tb.sv @@ -61,18 +61,218 @@ module retrosoc_tiny_tb; end `ifdef RETROSOC_SOC__TINY_NETLIST - // UART0 diagnostic output; SYSCTRL remains the authoritative test verdict. - initial - forever begin - @(negedge s_uart0_tx); - #1627.604; - for (int bit_index = 0; bit_index < 8; bit_index++) begin - s_uart_byte[bit_index] = s_uart0_tx; - #1085.069; + localparam bit ObserveUartAlways = 1'b1; +`else + localparam bit ObserveUartAlways = 1'b0; +`endif +`ifdef SIMU_IVERILOG + localparam bit ObserveUartBaseline = 1'b1; +`else + localparam bit ObserveUartBaseline = 1'b0; +`endif + // The Verilator RTL already prints accepted TX bytes. The Icarus observer is + // opt-in for measurements; SYSCTRL remains the authoritative verdict. + initial begin + if (ObserveUartAlways || (ObserveUartBaseline && $test$plusargs("tiny_r2_baseline"))) begin + forever begin + @(negedge s_uart0_tx); + #1627.604; + for (int bit_index = 0; bit_index < 8; bit_index++) begin + s_uart_byte[bit_index] = s_uart0_tx; + #1085.069; + end + $write("%c", s_uart_byte); end - $write("%c", s_uart_byte); end + end + +`ifndef RETROSOC_SOC__TINY_NETLIST + // Verification-only R2-P1 instrumentation. PERF_CTRL retains its existing + // software meaning; its enable selects the observation window. No design + // signal is driven, and the ordinary acceptance path leaves this disabled. + logic s_baseline_enabled = 1'b0; + logic [31:0] s_baseline_tcd_base, s_baseline_tcd_end; + initial begin + s_baseline_enabled = $test$plusargs("tiny_r2_baseline"); + if (s_baseline_enabled) begin + if (!$value$plusargs( + "baseline_tcd_base=%h", s_baseline_tcd_base + ) || !$value$plusargs( + "baseline_tcd_end=%h", s_baseline_tcd_end + ) || (s_baseline_tcd_end <= s_baseline_tcd_base)) begin + $fatal(1, "SIM_TEST_FAIL missing baseline descriptor range"); + end + end + end + + // Address acceptance and terminal response are distinct events. A CPU + // transaction can straddle PERF_CTRL itself; pending counts explicitly mark + // the right-censored window instead of inventing its completion latency. + for (genvar owner = 0; owner < 2; owner++) begin : gen_baseline_axi + longint unsigned s_cycle = 0; + longint unsigned s_window_cycles = 0; + longint unsigned s_read_start, s_write_start; + longint unsigned s_reads, s_writes, s_read_done, s_write_done; + longint unsigned s_read_bytes, s_write_bytes, s_descriptor_bytes; + longint unsigned s_read_latency, s_write_latency; + longint unsigned s_stall_run[5], s_stall_max[5]; + logic s_previous_enable = 1'b0; + logic s_read_measured = 1'b0, s_write_measured = 1'b0; + logic s_descriptor_read = 1'b0; + logic [2:0] s_read_size; + integer s_window = 0; + wire s_enable = u_dut.u_soc.s_perf_enable && s_baseline_enabled; + `define _BASELINE_AXI u_dut.u_soc.u_masters_axi4_if[owner] + wire [4:0] s_stalls = { + `_BASELINE_AXI.rvalid && !`_BASELINE_AXI.rready, + `_BASELINE_AXI.arvalid && !`_BASELINE_AXI.arready, + `_BASELINE_AXI.bvalid && !`_BASELINE_AXI.bready, + `_BASELINE_AXI.wvalid && !`_BASELINE_AXI.wready, + `_BASELINE_AXI.awvalid && !`_BASELINE_AXI.awready + }; + + always @(posedge s_clk) begin + s_cycle = s_cycle + 1; + if (!u_dut.u_soc.s_rst_n) begin + s_previous_enable = 1'b0; + s_read_measured = 1'b0; + s_write_measured = 1'b0; + s_window = 0; + end else if (s_baseline_enabled) begin + if (s_enable && !s_previous_enable) begin + s_window = s_window + 1; + s_window_cycles = 0; + s_reads = 0; + s_writes = 0; + s_read_done = 0; + s_write_done = 0; + s_read_bytes = 0; + s_write_bytes = 0; + s_descriptor_bytes = 0; + s_read_latency = 0; + s_write_latency = 0; + s_read_measured = 1'b0; + s_write_measured = 1'b0; + for (int channel = 0; channel < 5; channel++) begin + s_stall_run[channel] = 0; + s_stall_max[channel] = 0; + end + end + if (s_enable) begin + s_window_cycles = s_window_cycles + 1; + for (int channel = 0; channel < 5; channel++) begin + s_stall_run[channel] = s_stalls[channel] ? s_stall_run[channel] + 1 : 0; + if (s_stall_run[channel] > s_stall_max[channel]) + s_stall_max[channel] = s_stall_run[channel]; + end + if (`_BASELINE_AXI.arvalid && `_BASELINE_AXI.arready) begin + s_reads = s_reads + 1; + s_read_start = s_cycle; + s_read_size = `_BASELINE_AXI.arsize; + s_read_measured = 1'b1; + s_descriptor_read = (owner == 1) && + (`_BASELINE_AXI.araddr >= s_baseline_tcd_base) && + (`_BASELINE_AXI.araddr < s_baseline_tcd_end); + end + if (`_BASELINE_AXI.awvalid && `_BASELINE_AXI.awready) begin + s_writes = s_writes + 1; + s_write_start = s_cycle; + s_write_measured = 1'b1; + end + if (`_BASELINE_AXI.rvalid && `_BASELINE_AXI.rready && s_read_measured) begin + if (s_descriptor_read) s_descriptor_bytes = s_descriptor_bytes + (64'd1 << s_read_size); + else s_read_bytes = s_read_bytes + (64'd1 << s_read_size); + if (`_BASELINE_AXI.rlast) begin + s_read_done = s_read_done + 1; + if ((s_cycle - s_read_start) > s_read_latency) + s_read_latency = s_cycle - s_read_start; + s_read_measured = 1'b0; + end + end + if (`_BASELINE_AXI.wvalid && `_BASELINE_AXI.wready && s_write_measured) begin + for (int lane = 0; lane < 4; lane++) begin + if (`_BASELINE_AXI.wstrb[lane]) s_write_bytes = s_write_bytes + 1; + end + end + if (`_BASELINE_AXI.bvalid && `_BASELINE_AXI.bready && s_write_measured) begin + s_write_done = s_write_done + 1; + if ((s_cycle - s_write_start) > s_write_latency) + s_write_latency = s_cycle - s_write_start; + s_write_measured = 1'b0; + end + end + if (!s_enable && s_previous_enable) begin + // No timing control occurs between writes, so one observer emits + // one complete record without interleaving another observer's line. + $write("R2_AXI window=%0d owner=%0d cycles=%0d reads=%0d writes=%0d ", s_window, owner, + s_window_cycles, s_reads, s_writes); + $write("read_done=%0d write_done=%0d read_bytes=%0d write_bytes=%0d ", s_read_done, + s_write_done, s_read_bytes, s_write_bytes); + $write("descriptor_bytes=%0d read_latency_max=%0d write_latency_max=%0d ", + s_descriptor_bytes, s_read_latency, s_write_latency); + $write("aw_stall_max=%0d w_stall_max=%0d b_stall_max=%0d ", s_stall_max[0], + s_stall_max[1], s_stall_max[2]); + $display("ar_stall_max=%0d r_stall_max=%0d pending_reads=%0d pending_writes=%0d", + s_stall_max[3], s_stall_max[4], s_read_measured, s_write_measured); + end + s_previous_enable = s_enable; + end + end + `undef _BASELINE_AXI + end + + // The baseline has one AHB port. HPROT[0] distinguishes instruction/data; + // these observations do not assert future independent local-I/D latency. + longint unsigned s_ahb_cycle = 0; + longint unsigned s_ahb_start = 0; + longint unsigned s_ahb_accept[2], s_ahb_done[2], s_ahb_latency[2]; + logic s_ahb_pending = 1'b0, s_ahb_measured = 1'b0; + logic s_ahb_data = 1'b0, s_ahb_previous_enable = 1'b0; + integer s_ahb_window = 0; + wire s_ahb_enable = u_dut.u_soc.s_perf_enable && s_baseline_enabled; + `define _BASELINE_AHB u_dut.u_soc.u_cpu.u_ahbl_if + always @(posedge s_clk) begin + s_ahb_cycle = s_ahb_cycle + 1; + if (!u_dut.u_soc.s_rst_n) begin + s_ahb_pending = 1'b0; + s_ahb_measured = 1'b0; + s_ahb_previous_enable = 1'b0; + s_ahb_window = 0; + end else if (s_baseline_enabled) begin + if (s_ahb_enable && !s_ahb_previous_enable) begin + s_ahb_window = s_ahb_window + 1; + s_ahb_measured = 1'b0; + for (int kind = 0; kind < 2; kind++) begin + s_ahb_accept[kind] = 0; + s_ahb_done[kind] = 0; + s_ahb_latency[kind] = 0; + end + end + if (`_BASELINE_AHB.hready) begin + if (s_ahb_pending && s_ahb_measured && s_ahb_enable) begin + s_ahb_done[s_ahb_data] = s_ahb_done[s_ahb_data] + 1; + if ((s_ahb_cycle - s_ahb_start) > s_ahb_latency[s_ahb_data]) + s_ahb_latency[s_ahb_data] = s_ahb_cycle - s_ahb_start; + end + s_ahb_pending = `_BASELINE_AHB.htrans[1]; + s_ahb_measured = s_ahb_pending && s_ahb_enable; + if (s_ahb_measured) begin + s_ahb_data = `_BASELINE_AHB.hprot[0]; + s_ahb_start = s_ahb_cycle; + s_ahb_accept[s_ahb_data] = s_ahb_accept[s_ahb_data] + 1; + end + end + if (!s_ahb_enable && s_ahb_previous_enable) begin + for (int kind = 0; kind < 2; kind++) begin + $display("R2_AHB window=%0d kind=%0d accepted=%0d completed=%0d latency_max=%0d", + s_ahb_window, kind, s_ahb_accept[kind], s_ahb_done[kind], s_ahb_latency[kind]); + end + end + s_ahb_previous_enable = s_ahb_enable; + end + end + `undef _BASELINE_AHB `endif retrosoc_tiny_asic u_dut ( diff --git a/scripts/tiny_r2_baseline.py b/scripts/tiny_r2_baseline.py new file mode 100644 index 00000000..f8e4959e --- /dev/null +++ b/scripts/tiny_r2_baseline.py @@ -0,0 +1,564 @@ +#!/usr/bin/env python3 +"""Retain and replay the Tiny R2-P1 workload without changing product policy.""" + +from __future__ import annotations + +import argparse +from concurrent.futures import ThreadPoolExecutor, as_completed +from datetime import datetime, timezone +import hashlib +import json +import os +from pathlib import Path +import re +import shutil +import subprocess +import sys +import tarfile +import tempfile + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +from scripts.check_simulation import DEFAULT_FAILURE # noqa: E402 +from scripts.manifest import git_info # noqa: E402 +from scripts.rtl.filelist import parse_filelists # noqa: E402 +from scripts.setup_helpers import atomic_write, sha256 # noqa: E402 +from scripts.setup_tiny import TINY_SOURCES # noqa: E402 + +PHASE = "TINY-R2-P1" +CASES = ("empty", "cpu", "memory", "dma1", "dma16", "contention", "tcd") +PARAMETERS = {"version": 1, "cpu_hz": 24000000, "words": 1024, "jobs": 16, + "iterations": 4096, "seed": 0x12345678} +COMPATIBILITY = {"SOC": "TINY", "PDK": "IHP130", "HAVE_PLL": "NO", + "SRAM_SIZE_KIB": "128", "EXT_CLK_HZ": "24000000", "ISA": "RV32IM", + "HAVE_CSR": "YES", "LINK_TYPE": "ld2_all_sram", "PDK_BEHAV": "NO"} +TOOL_PROGRAMS = {"verilator": "verilator", "verible": "verible-verilog-format", + "sv2v": "sv2v", "iverilog": "iverilog", "yosys": "yosys", + "opensta": "sta", "riscv_gnu": "riscv32-unknown-elf-gcc"} +SOURCE_PATHS = ("Makefile", "configs/ci/ihp130-tiny.mk", "dependencies/dependencies.lock.json", + "requirements", "rtl/tiny", "rtl/ip", "rtl/tech", "rtl/mk/software.mk", + "crt", "app/apps/ci_smoke", "app/apps/bringup", "scripts", + "physical/smoke", "physical/librelane/tiny") +CASE_FIELDS = {"id", "name", "cycles", "instructions", "kernel_cycles", "kernel_calls", + "payload_bytes", "checksum", "cpu_wait", "dma_wait", "sram_reads", + "sram_writes", "sram_rbeats", "sram_wbeats", "sram_stalls", "sram_errors"} +AXI_FIELDS = {"window", "owner", "cycles", "reads", "writes", "read_done", "write_done", + "read_bytes", "write_bytes", "descriptor_bytes", "read_latency_max", + "write_latency_max", "aw_stall_max", "w_stall_max", "b_stall_max", + "ar_stall_max", "r_stall_max", "pending_reads", "pending_writes"} +AHB_FIELDS = {"window", "kind", "accepted", "completed", "latency_max"} + + +def dump(path: Path, value: object) -> None: + atomic_write(path, json.dumps(value, sort_keys=True, indent=2) + "\n") + + +def read_json(path: Path) -> dict: + result = json.loads(path.read_text()) + if not isinstance(result, dict): + raise ValueError(f"expected JSON object: {path}") + return result + + +def artifact(path: Path) -> dict: + return {"path": str(path.resolve()), "sha256": sha256(path), "bytes": path.stat().st_size} + + +def check_artifact(value: dict) -> Path: + path = Path(value["path"]) + if not path.is_file() or artifact(path) != value: + raise ValueError(f"missing or changed artifact: {path}") + return path + + +def digest_map(value: dict) -> str: + return hashlib.sha256(json.dumps(value, sort_keys=True).encode()).hexdigest() + + +def source_inputs(root: Path) -> dict[str, str]: + names = subprocess.check_output( + ["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", + *SOURCE_PATHS], cwd=root, + ).decode().split("\0") + paths = {root / name for name in names if name and not name.endswith( + (".md", ".png", ".jpg", ".svg", ".pdf"))} + lock = read_json(root / "dependencies/dependencies.lock.json") + for name in TINY_SOURCES: + spec = lock["sources"][name] + checkout = root / spec["destination"] + revision = subprocess.check_output(["git", "rev-parse", "HEAD"], cwd=checkout, + text=True).strip() + dirty = subprocess.check_output(["git", "status", "--porcelain"], cwd=checkout, + text=True).strip() + if revision != spec["revision"] or dirty: + raise ValueError(f"managed input is not clean at its lock: {name}") + # Source inputs plus the three SRAM views and exact core-STA libraries. + if name == "pdk_ihp130": + base = checkout / "ihp-sg13g2/libs.ref" + paths.update((base / "sg13g2_sram/lib").glob( + "RM_IHPSG13_1P_1024x32_c2_bm_bist_*.lib")) + paths.update((base / "sg13g2_stdcell/lib").glob("*slow_1p08V_125C.lib")) + paths.update((base / "sg13g2_io/lib").glob("*slow_1p08V_3p0V_125C.lib")) + else: + for suffix in ("*.v", "*.sv", "*.vh", "*.svh", "*.c", "*.h"): + paths.update(checkout.rglob(suffix)) + return {str(path.relative_to(root)): sha256(path) for path in sorted(paths) if path.is_file()} + + +def runtime_inputs(root: Path, variant: Path, simulator: str) -> dict[str, str]: + paths = set((variant / "generated/tiny").rglob("*")) + filelists = sorted((variant / "sim" / simulator / "filelists").glob("*.fl")) + if not filelists: + raise ValueError("missing generated simulator filelists") + resolved = parse_filelists(filelists) + paths.update(filelists) + paths.update(resolved.files) + paths.update(resolved.library_files) + for directory in resolved.incdirs: + for suffix in ("*.svh", "*.vh"): + paths.update(directory.rglob(suffix)) + converted = variant / "sim" / simulator / "converted_soc.v" + if converted.exists(): + paths.add(converted) + return {str(path.relative_to(root)): sha256(path) for path in sorted(paths) if path.is_file()} + + +def configuration(variant: Path) -> dict: + manifest = read_json(variant / "meta/manifest.json") + config = manifest["configuration"] + if manifest["profile"] != "ihp130-tiny" or any( + config.get(key) != expected for key, expected in COMPATIBILITY.items() + ): + raise ValueError("baseline requires the committed Tiny/IHP130 24 MHz macro profile") + return manifest + + +def tools_identity(root: Path) -> dict: + cache_value = os.environ.get("RETROSOC_DEVELOPMENT_CACHE") + if not cache_value: + raise ValueError("activate the verified locked development environment first") + cache = Path(cache_value).resolve() + lock = read_json(root / "dependencies/dependencies.lock.json")["toolchains"]["ubuntu-22.04"] + result = {} + for name, program in TOOL_PROGRAMS.items(): + spec = lock[name] + installed = cache / "toolchains" / f"{name}-{spec['version']}" + archive = cache / "downloads" / spec["archive"] + executable = shutil.which(program) + if (not executable or Path(executable).resolve() != (installed / spec["path"] / program).resolve() + or (installed / ".complete").read_text().strip() != spec["sha256"] + or sha256(archive) != spec["sha256"]): + raise ValueError(f"tool path/archive is not the locked installation: {name}") + option = "-version" if name == "opensta" else "-V" if name == "iverilog" else "--version" + version = subprocess.run([executable, option], text=True, capture_output=True, check=True) + result[name] = {"locked_version": spec["version"], "archive": artifact(archive), + "executable": artifact(Path(executable)), + "version": (version.stdout or version.stderr).splitlines()[0]} + return result + + +def vvp_identity(selected: str, compiler: dict) -> dict: + """Bind the selected runtime to the already verified Icarus installation.""" + expected = Path(compiler["executable"]["path"]).with_name("vvp").resolve() + executable = shutil.which(selected) + if not executable or Path(executable).resolve() != expected: + raise ValueError("selected vvp is not the locked Icarus runtime") + version = subprocess.run([str(expected), "-V"], text=True, capture_output=True, check=True) + version_lines = (version.stdout or version.stderr).splitlines() + if not version_lines: + raise ValueError("locked vvp did not report its version") + return {"locked_version": compiler["locked_version"], "archive": compiler["archive"], + "executable": artifact(expected), + "version": version_lines[0]} + + +def validate_runtime_tool(root: Path, attempt: dict) -> None: + if attempt["simulator"] != "iverilog": + return + runtime = attempt.get("runtime_tool") + if not isinstance(runtime, dict): + raise ValueError("missing locked vvp runtime identity") + spec = read_json(root / "dependencies/dependencies.lock.json")["toolchains"]["ubuntu-22.04"]["iverilog"] + compiler = attempt["tools"]["iverilog"] + executable = check_artifact(runtime["executable"]) + expected = Path(compiler["executable"]["path"]).with_name("vvp").resolve() + check_artifact(runtime["archive"]) + if (executable != expected or runtime["archive"] != compiler["archive"] + or runtime["archive"]["sha256"] != spec["sha256"] + or runtime["locked_version"] != spec["version"] or not runtime.get("version") + or attempt["command"][0] != str(executable)): + raise ValueError("vvp runtime does not match its locked identity and launch command") + for run in attempt["runs"]: + command = read_json(check_artifact(run["flow"]))["command"] + if command != ["timeout", "--foreground", f"{attempt['timeout_seconds']}s", *attempt["command"]]: + raise ValueError("simulation did not launch the recorded locked vvp runtime") + + +def snapshot(root: Path, destination: Path, inputs: dict[str, str]) -> dict: + destination.mkdir(parents=True, exist_ok=True) + with tarfile.open(destination / "inputs.tar.gz", "w:gz", dereference=True) as archive: + for name in inputs: + archive.add(root / name, arcname=name, recursive=False) + diff = subprocess.check_output(["git", "diff", "HEAD", "--binary"], cwd=root, text=True) + atomic_write(destination / "worktree.patch", diff) + dump(destination / "input-hashes.json", inputs) + return {"repository": git_info(root), "input_digest": digest_map(inputs), + "inputs": artifact(destination / "input-hashes.json"), + "snapshot": artifact(destination / "inputs.tar.gz"), + "patch": artifact(destination / "worktree.patch")} + + +def execute(root: Path, directory: Path, label: str, command: list[str], *, + cwd: Path | None = None, quiet: bool = False) -> dict: + result = directory / f"{label}.json" + invocation = [sys.executable, str(root / "scripts/run_flow.py"), "--tool", label, + "--log", str(directory / f"{label}.log"), "--result", str(result)] + if cwd is not None: + invocation += ["--cwd", str(cwd)] + completed = subprocess.run([*invocation, "--", *command], check=False, + stdout=subprocess.PIPE if quiet else None) + record = read_json(result) + if completed.returncode != 0 or record.get("status") != "passed": + raise ValueError(f"command failed; retained result: {result}") + return record + + +def new_attempt(parent: Path, prefix: str) -> Path: + parent.mkdir(parents=True, exist_ok=True) + return Path(tempfile.mkdtemp(prefix=prefix, dir=parent)) + + +def payload_checksum() -> int: + value = 2166136261 + for _ in range(16): + for index in range(1024): + value = ((value ^ (0xA519C300 ^ index)) * 16777619) & 0xFFFFFFFF + return value + + +def cpu_checksum() -> int: + value = PARAMETERS["seed"] + for _ in range(PARAMETERS["iterations"]): + value ^= (value << 13) & 0xFFFFFFFF + value ^= value >> 17 + value ^= (value << 5) & 0xFFFFFFFF + return value + + +def parse_fields(line: str, expected: set[str], *, strings: set[str] = frozenset()) -> dict: + fields = {} + for token in line.split()[1:]: + key, separator, raw = token.partition("=") + if not separator or key in fields or key not in expected: + raise ValueError(f"malformed or duplicate record field: {line}") + value = raw if key in strings else int(raw, 0) + if key not in strings and not 0 <= value < (1 << 64): + raise ValueError(f"counter outside unsigned 64-bit range: {key}") + fields[key] = value + if set(fields) != expected: + raise ValueError(f"incomplete record: {line}") + return fields + + +def parse_log(content: str, flow_result: dict) -> dict: + if flow_result.get("exit_code") != 0 or flow_result.get("status") != "passed": + raise ValueError("simulation command did not succeed") + if DEFAULT_FAILURE.search(content) or "R2_ERROR" in content or "SIM_TEST_PASS Tiny" not in content: + raise ValueError("missing Tiny terminal pass or forbidden failure marker") + records: dict[str, list[dict]] = {name: [] for name in ("header", "case", "axi", "ahb", "complete")} + definitions = {"R2_HEADER": ("header", set(PARAMETERS)), "R2_CASE": ("case", CASE_FIELDS), + "R2_AXI": ("axi", AXI_FIELDS), "R2_AHB": ("ahb", AHB_FIELDS), + "R2_COMPLETE": ("complete", {"version", "cases"})} + for line in content.splitlines(): + prefix = line.split(maxsplit=1)[0] if line.strip() else "" + if prefix in definitions: + name, fields = definitions[prefix] + records[name].append(parse_fields(line, fields, strings={"name"} if name == "case" else set())) + if records["header"] != [PARAMETERS] or records["complete"] != [{"version": 1, "cases": 7}]: + raise ValueError("missing, duplicate or incompatible workload identity") + if [(row["id"], row["name"]) for row in records["case"]] != list(enumerate(CASES)): + raise ValueError("workload cases are missing, duplicated or reordered") + for name, field in (("axi", "owner"), ("ahb", "kind")): + keys = [(row["window"], row[field]) for row in records[name]] + if sorted(keys) != [(window, index) for window in range(1, 8) for index in range(2)]: + raise ValueError(f"missing or duplicate {name} observations") + # Independent always blocks need not print in the same scheduler order. + records[name].sort(key=lambda row: (row["window"], row[field])) + axi = {(row["window"], row["owner"]): row for row in records["axi"]} + for index, row in enumerate(records["case"]): + expected_checksum = 0 if index == 0 else cpu_checksum() if index == 1 else payload_checksum() + expected_calls = 1 if index == 1 else 16 if index >= 5 else 0 + if (row["checksum"] != expected_checksum or row["kernel_calls"] != expected_calls + or row["payload_bytes"] != (65536 if index >= 2 else 0) + or row["cycles"] <= 0 or row["instructions"] <= 0 or row["sram_errors"] != 0 + or row["kernel_cycles"] > row["cycles"] + or (expected_calls > 0) != (row["kernel_cycles"] > 0)): + raise ValueError(f"invalid workload result: {row['name']}") + dma = axi[(index + 1, 1)] + expected_bytes = 65536 if index >= 3 else 0 + if (dma["read_bytes"] != expected_bytes or dma["write_bytes"] != expected_bytes + or dma["descriptor_bytes"] != (4096 if index == 6 else 0) + or dma["pending_reads"] or dma["pending_writes"]): + raise ValueError(f"DMA payload/descriptor/drain accounting mismatch: {row['name']}") + for owner in range(2): + observed = axi[(index + 1, owner)] + if (observed["reads"] != observed["read_done"] + observed["pending_reads"] + or observed["writes"] != observed["write_done"] + observed["pending_writes"]): + raise ValueError("AXI accepted/completed accounting mismatch") + row["cpi"] = row["cycles"] / row["instructions"] + row["payload_bytes_per_cycle"] = row["payload_bytes"] / row["cycles"] + for row in records["ahb"]: + if row["completed"] > row["accepted"] or row["accepted"] - row["completed"] > 1: + raise ValueError("AHB accepted/completed accounting mismatch") + return records + + +def build_image(args: argparse.Namespace) -> int: + root, variant = args.root.resolve(), args.variant_root.resolve() + evidence = variant / "meta/tiny-r2-p1" + directory = new_attempt(evidence / "binaries", "image-") + output = directory / "image.json" + # Move the pointer before starting: a failed attempt must not expose an old success. + dump(evidence / "binaries/latest.json", {"path": str(output)}) + record = {"schema_version": 1, "phase": PHASE, "status": "running"} + dump(output, record) + try: + manifest = configuration(variant) + tools = tools_identity(root) + before = source_inputs(root) + # Archive generated C headers and the preprocessed flat linker input too. + generated = {str(path.relative_to(root)): sha256(path) + for path in (variant / "generated/tiny").rglob("*") if path.is_file()} + for path in (variant / "sw").rglob("*"): + if path.is_file() and path.suffix in (".h", ".lds", ".ld"): + generated[str(path.relative_to(root))] = sha256(path) + record.update(manifest=manifest, tools=tools, source=snapshot(root, directory / "inputs", before | generated), + parameters=PARAMETERS, created_at=datetime.now(timezone.utc).isoformat()) + elf = directory / "tiny_baseline.elf" + compiler_command = list(args.command[1:] if args.command[:1] == ["--"] else args.command) + if not compiler_command: + raise ValueError("image compiler command is required") + compiler_command += ["-Wl,-Map," + str(directory / "tiny_baseline.map"), "-o", str(elf)] + execute(root, directory, "compile", compiler_command, cwd=variant / "sw") + for name, form in (("hex", "verilog"), ("bin", "binary")): + execute(root, directory, f"objcopy-{name}", [args.objcopy, "-O", form, str(elf), + str(directory / f"tiny_baseline.{name}")]) + for label, command in (("disassembly", [args.objdump, "-d", str(elf)]), + ("symbols", [args.nm, "-n", "-S", str(elf)]), + ("size", [args.size, "-A", str(elf)])): + execute(root, directory, label, command, quiet=True) + symbols = (directory / "symbols.log").read_text() + match = re.search(r"^([0-9a-f]+) ([0-9a-f]+) [bBdD] rs_baseline_tcds$", symbols, re.MULTILINE) + if match is None or int(match[2], 16) != 4096: + raise ValueError("missing complete 64-descriptor SRAM symbol") + base = int(match[1], 16) + ebss = re.search(r"^([0-9a-f]+)(?: [0-9a-f]+)? [A-Za-z] _ebss$", symbols, re.MULTILINE) + if ebss is None or not 0x30000000 <= int(ebss[1], 16) <= 0x3001F000: + raise ValueError("image does not preserve at least 4 KiB of stack headroom") + if before != source_inputs(root): + raise ValueError("source changed while compiling the retained image") + if any(not (root / name).is_file() or sha256(root / name) != digest + for name, digest in generated.items()): + raise ValueError("generated software inputs changed during compilation") + record.update(status="passed", compiler_command=compiler_command, + descriptor_range=[base, base + 4096], stack_headroom=0x30020000-int(ebss[1], 16), + artifacts={p.name: artifact(p) for p in directory.iterdir() + if p.is_file() and p != output}) + except (OSError, ValueError, subprocess.SubprocessError) as error: + record.update(status="failed", error=str(error)) + raise + finally: + dump(output, record) + print(f"retained Tiny baseline: {directory / 'tiny_baseline.hex'}") + return 0 + + +def retained_image(evidence: Path, selected: Path | None) -> tuple[Path, dict]: + manifest_path = selected.resolve().parent / "image.json" if selected else Path( + read_json(evidence / "binaries/latest.json")["path"]) + value = read_json(manifest_path) + if value.get("status") != "passed" or value.get("phase") != PHASE or value.get("parameters") != PARAMETERS: + raise ValueError("retained image lacks successful compatible build provenance") + image = check_artifact(value["artifacts"]["tiny_baseline.hex"]) + if selected and image != selected.resolve(): + raise ValueError("selected image does not match its original manifest") + for identity in value["artifacts"].values(): + check_artifact(identity) + for identity in (value["source"][key] for key in ("inputs", "snapshot", "patch")): + check_artifact(identity) + return manifest_path, value + + +def validate_runs(runs: list[dict]) -> None: + """Recheck acceptance from retained logs, never just a producer's label.""" + if [run.get("repetition") for run in runs] != [1, 2, 3]: + raise ValueError("expected three distinct ordered cold-start repetitions") + for run in runs: + log = check_artifact(run["log"]) + flow = read_json(check_artifact(run["flow"])) + verdict = read_json(check_artifact(run["verdict"])) + if (not flow.get("command") or Path(flow.get("log", "")).resolve() != log.resolve() + or verdict.get("status") != "passed" + or Path(verdict.get("log", "")).resolve() != log.resolve() + or parse_log(log.read_text(), flow) != run["samples"]): + raise ValueError("changed or invalid simulation verdict") + if any(run["samples"] != runs[0]["samples"] for run in runs[1:]): + raise ValueError("cold-start repetitions were not deterministic") + + +def simulate(args: argparse.Namespace) -> int: + root, variant = args.root.resolve(), args.variant_root.resolve() + evidence = variant / "meta/tiny-r2-p1" + directory = new_attempt(evidence / "workloads" / args.simulator, "attempt-") + output = directory / "attempt.json" + dump(directory.parent / "latest.json", {"path": str(output)}) + record = {"schema_version": 1, "phase": PHASE, "status": "running", "runs": [], + "simulator": args.simulator} + dump(output, record) + try: + image_path, image = retained_image(evidence, args.hex) + record.update(image_manifest=artifact(image_path), image_sha256=image["artifacts"]["tiny_baseline.hex"]["sha256"], + firmware_source=image["source"], manifest=configuration(variant), tools=tools_identity(root)) + if args.simulator == "iverilog": + record["runtime_tool"] = vvp_identity(args.vvp, record["tools"]["iverilog"]) + before = source_inputs(root) + command = args.command[1:] if args.command[:1] == ["--"] else args.command + if not command: + raise ValueError("a fresh simulator compilation command is required") + execute(root, directory, "compile-model", command, quiet=True) + if before != source_inputs(root): + raise ValueError("source changed while compiling the simulator") + runtime = runtime_inputs(root, variant, args.simulator) + record["rtl_source"] = snapshot(root, directory / "inputs", before | runtime) + model = variant / "sim" / args.simulator / ("emu" if args.simulator == "verilator" else "simv") + record["model"] = artifact(model) + launch = [str(model)] if args.simulator == "verilator" else [record["runtime_tool"]["executable"]["path"], str(model)] + base, end = image["descriptor_range"] + launch += ["+firmware=" + image["artifacts"]["tiny_baseline.hex"]["path"], + "+tiny_r2_baseline", f"+baseline_tcd_base={base:x}", f"+baseline_tcd_end={end:x}", + f"+max_cycles={args.max_cycles}"] + record.update(command=launch, timeout_seconds=args.timeout, max_cycles=args.max_cycles, + simulation_jobs=min(args.jobs, 3)) + + def run_once(repetition: int) -> dict: + label = f"run-{repetition}" + if args.simulator == "iverilog": + check_artifact(record["runtime_tool"]["executable"]) + flow = execute(root, directory, label, + ["timeout", "--foreground", f"{args.timeout}s", *launch], quiet=True) + log = directory / f"{label}.log" + execute(root, directory, f"{label}-check", [sys.executable, str(root / "scripts/check_simulation.py"), + "--log", str(log), "--result", str(directory / f"{label}-verdict.json"), + "--require", "SIM_TEST_PASS"], quiet=True) + samples = parse_log(log.read_text(), flow) + return {"repetition": repetition, "samples": samples, + "log": artifact(log), "flow": artifact(directory / f"{label}.json"), + "verdict": artifact(directory / f"{label}-verdict.json")} + + # Each child has its own reset state and logs; only immutable model and + # firmware inputs are shared. Host scheduling cannot advance HDL clocks. + errors = [] + with ThreadPoolExecutor(max_workers=min(args.jobs, 3)) as executor: + futures = [executor.submit(run_once, repetition) for repetition in range(1, 4)] + for future in as_completed(futures): + try: + run = future.result() + record["runs"].append(run) + record["runs"].sort(key=lambda value: value["repetition"]) + print(f"{args.simulator} repetition {run['repetition']}: passed", flush=True) + except (OSError, ValueError, subprocess.SubprocessError) as error: + errors.append(str(error)) + record["run_errors"] = errors + dump(output, record) + if errors: + raise ValueError("; ".join(errors)) + if before != source_inputs(root) or runtime != runtime_inputs(root, variant, args.simulator): + raise ValueError("source or generated inputs changed during simulation") + validate_runs(record["runs"]) + validate_runtime_tool(root, record) + # Retention is part of acceptance: a binary changed after launch is + # still a failed evidence bundle even if the loaded model completed. + check_artifact(record["image_manifest"]) + retained_image(evidence, Path(image["artifacts"]["tiny_baseline.hex"]["path"])) + record["status"] = "passed" + except (OSError, ValueError, subprocess.SubprocessError) as error: + record.update(status="failed", error=str(error)) + raise + finally: + dump(output, record) + print(f"Tiny baseline {args.simulator}: 3 reproducible runs ({output})") + return 0 + + +def report(args: argparse.Namespace) -> int: + root, variant = args.root.resolve(), args.variant_root.resolve() + evidence = variant / "meta/tiny-r2-p1" + output = evidence / "baseline-report.json" + result = {"schema_version": 1, "phase": PHASE, "status": "incomplete", "simulators": {}, + "qualification": "functional and simulated workload evidence only; not physical qualification"} + try: + for simulator in ("verilator", "iverilog"): + path = Path(read_json(evidence / "workloads" / simulator / "latest.json")["path"]) + attempt = read_json(path) + if attempt.get("status") != "passed" or len(attempt.get("runs", [])) != 3: + raise ValueError(f"incomplete {simulator} baseline") + validate_runtime_tool(root, attempt) + image_path = check_artifact(attempt["image_manifest"]) + image = read_json(image_path) + selected, _ = retained_image(evidence, Path(image["artifacts"]["tiny_baseline.hex"]["path"])) + if selected != image_path: + raise ValueError("image provenance points to a different retained manifest") + check_artifact(attempt["model"]) + source = attempt["rtl_source"] + inputs = read_json(check_artifact(source["inputs"])) + if inputs != source_inputs(root) | runtime_inputs(root, variant, simulator): + raise ValueError(f"stale {simulator} source inputs") + check_artifact(source["snapshot"]) + validate_runs(attempt["runs"]) + result["simulators"][simulator] = {"attempt": artifact(path), **attempt} + first, second = (result["simulators"][name] for name in ("verilator", "iverilog")) + if first["image_sha256"] != second["image_sha256"]: + raise ValueError("simulators did not replay an identical binary") + if first["runs"][0]["samples"] != second["runs"][0]["samples"]: + raise ValueError("simulators disagree on workload or cycle measurements") + result["status"] = "passed" + except (OSError, ValueError, KeyError) as error: + result["error"] = str(error) + dump(output, result) + print(f"Tiny baseline report: {result['status']} ({output})") + return int(result["status"] != "passed") + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--root", type=Path, default=ROOT) + parser.add_argument("--variant-root", type=Path, required=True) + commands = parser.add_subparsers(dest="action", required=True) + image = commands.add_parser("image") + for name in ("objcopy", "objdump", "nm", "size"): + image.add_argument("--" + name, default="riscv32-unknown-elf-" + name) + image.add_argument("command", nargs=argparse.REMAINDER) + simulation = commands.add_parser("simulate") + simulation.add_argument("--simulator", choices=("verilator", "iverilog"), required=True) + simulation.add_argument("--hex", type=Path) + simulation.add_argument("--timeout", type=int, default=1800) + simulation.add_argument("--max-cycles", type=int, default=100000000) + simulation.add_argument("--jobs", type=int, default=1) + simulation.add_argument("--vvp", default="vvp") + simulation.add_argument("command", nargs=argparse.REMAINDER) + commands.add_parser("report") + args = parser.parse_args() + if args.action == "simulate" and min(args.jobs, args.timeout, args.max_cycles) <= 0: + parser.error("simulation jobs, timeout and cycle limit must be positive") + variant = args.variant_root.resolve() + if not variant.is_relative_to(args.root.resolve() / "build"): + parser.error("baseline evidence must be below this repository's build variants") + try: + return {"image": build_image, "simulate": simulate, "report": report}[args.action](args) + except (OSError, ValueError, KeyError, subprocess.SubprocessError) as error: + print(f"Tiny R2 baseline: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/test_tiny_r2_baseline.py b/tests/test_tiny_r2_baseline.py new file mode 100644 index 00000000..32dbf25d --- /dev/null +++ b/tests/test_tiny_r2_baseline.py @@ -0,0 +1,354 @@ +"""Independent workload expectations and fail-closed Tiny baseline evidence.""" + +from __future__ import annotations + +import argparse +import copy +import json +import os +from pathlib import Path +import subprocess + +import pytest + +from scripts import tiny_r2_baseline as baseline + + +def log_fixture() -> str: + """Small protocol records, not a mocked hardware qualification result.""" + lines = ["R2_HEADER version=1 cpu_hz=24000000 words=1024 jobs=16 iterations=4096 seed=0x12345678"] + for index, name in enumerate(("empty", "cpu", "memory", "dma1", "dma16", "contention", "tcd")): + calls = 1 if index == 1 else 16 if index >= 5 else 0 + checksum = 0 if index == 0 else 0xF6E37410 if index == 1 else 0xBC5F5DC5 + lines.append( + f"R2_CASE id={index} name={name} cycles=1000000 instructions=100000 " + f"kernel_cycles={calls * 1000} kernel_calls={calls} " + f"payload_bytes={65536 if index >= 2 else 0} checksum={checksum} " + "cpu_wait=11 dma_wait=7 sram_reads=30 sram_writes=20 sram_rbeats=32 " + "sram_wbeats=24 sram_stalls=8 sram_errors=0" + ) + for owner in range(2): + payload = 65536 if owner == 1 and index >= 3 else 0 + descriptors = 4096 if owner == 1 and index == 6 else 0 + lines.append( + f"R2_AXI window={index + 1} owner={owner} cycles=1000100 reads=2 writes=2 " + f"read_done=2 write_done=2 read_bytes={payload} write_bytes={payload} " + f"descriptor_bytes={descriptors} read_latency_max=9 write_latency_max=10 " + "aw_stall_max=1 w_stall_max=2 b_stall_max=3 ar_stall_max=4 r_stall_max=5 " + "pending_reads=0 pending_writes=0" + ) + lines.append(f"R2_AHB window={index + 1} kind={owner} accepted=5 completed=5 latency_max=8") + lines += ["R2_COMPLETE version=1 cases=7", "SIM_TEST_PASS Tiny cycles=1234567"] + return "\n".join(lines) + + +PASS = {"status": "passed", "exit_code": 0} + + +def test_cpu_oracle_matches_a_bit_vector_reference() -> None: + # A shift-register bit-vector formulation independent of the firmware's + # integer shift expressions; checks every bit at the final iteration. + bits = [(0x12345678 >> position) & 1 for position in range(32)] + for _ in range(4096): + bits = [bit ^ (bits[i - 13] if i >= 13 else 0) for i, bit in enumerate(bits)] + bits = [bit ^ (bits[i + 17] if i < 15 else 0) for i, bit in enumerate(bits)] + bits = [bit ^ (bits[i - 5] if i >= 5 else 0) for i, bit in enumerate(bits)] + expected = sum(bit << index for index, bit in enumerate(bits)) + assert expected == 0xF6E37410 == baseline.cpu_checksum() + + +def test_complete_log_includes_both_monitors_and_dma_descriptor_bytes() -> None: + parsed = baseline.parse_log(log_fixture(), PASS) + assert len(parsed["case"]) == 7 + assert parsed["case"][5]["cpi"] == 10 + assert parsed["case"][6]["payload_bytes_per_cycle"] == 0.065536 + + +@pytest.mark.parametrize("marker", ["FAILED", "FATAL", "assertion failed", "%Error", + "SIM_TEST_FAIL", "SIM_TEST_TIMEOUT", "R2_ERROR"]) +def test_a_later_pass_cannot_hide_an_error(marker: str) -> None: + with pytest.raises(ValueError, match="failure marker"): + baseline.parse_log(marker + "\n" + log_fixture(), PASS) + + +@pytest.mark.parametrize("result", [{"status": "passed", "exit_code": 1}, + {"status": "running", "exit_code": 0}, + {"status": "interrupted", "exit_code": 130}]) +def test_terminal_marker_cannot_override_command_failure(result: dict) -> None: + with pytest.raises(ValueError, match="command"): + baseline.parse_log(log_fixture(), result) + + +def test_uart_records_do_not_replace_terminal_status() -> None: + with pytest.raises(ValueError, match="terminal"): + baseline.parse_log(log_fixture().replace("SIM_TEST_PASS Tiny", "Hello retroSoC!"), PASS) + + +@pytest.mark.parametrize("prefix", ["R2_HEADER", "R2_CASE", "R2_AXI", "R2_AHB", "R2_COMPLETE"]) +@pytest.mark.parametrize("duplicate", [False, True]) +def test_incomplete_or_duplicate_measurements_fail(prefix: str, duplicate: bool) -> None: + lines = log_fixture().splitlines() + index = next(i for i, line in enumerate(lines) if line.startswith(prefix)) + if duplicate: + lines.append(lines[index]) + else: + lines.pop(index) + with pytest.raises(ValueError): + baseline.parse_log("\n".join(lines), PASS) + + +@pytest.mark.parametrize("old,new", [ + ("cpu_hz=24000000", "cpu_hz=96000000"), + ("checksum=4142101520", "checksum=1"), + ("descriptor_bytes=4096", "descriptor_bytes=0"), + ("read_bytes=65536", "read_bytes=69632"), + ("write_bytes=65536", "write_bytes=65532"), + ("sram_errors=0", "sram_errors=1"), + ("completed=5", "completed=6"), + ("read_done=2", "read_done=1"), + ("instructions=100000", "instructions=0"), + ("cycles=1000000", "cycles=-1"), +]) +def test_wrong_data_clock_and_accounting_fail(old: str, new: str) -> None: + content = log_fixture() + assert old in content + with pytest.raises(ValueError): + baseline.parse_log(content.replace(old, new, 1), PASS) + + +def test_cpu_boundary_transaction_is_reported_as_pending() -> None: + content = log_fixture().replace("write_done=2", "write_done=1", 1) + content = content.replace("pending_writes=0", "pending_writes=1", 1) + baseline.parse_log(content, PASS) + + +def test_observer_print_order_is_not_a_cycle_difference() -> None: + lines = log_fixture().splitlines() + observations = [line for line in lines if line.startswith(("R2_AXI", "R2_AHB"))] + rest = [line for line in lines if not line.startswith(("R2_AXI", "R2_AHB"))] + assert baseline.parse_log("\n".join(rest + observations[::-1]), PASS) == baseline.parse_log( + log_fixture(), PASS) + + +def test_changed_artifact_cannot_keep_its_identity(tmp_path: Path) -> None: + path = tmp_path / "sim.log" + path.write_text(log_fixture()) + identity = baseline.artifact(path) + assert baseline.check_artifact(identity) == path + path.write_text(path.read_text() + "\nSIM_TEST_FAIL\n") + with pytest.raises(ValueError, match="changed artifact"): + baseline.check_artifact(identity) + + +def test_retained_image_keeps_original_source_identity(tmp_path: Path) -> None: + evidence = tmp_path / "evidence" + directory = evidence / "binaries/original" + directory.mkdir(parents=True) + image = directory / "tiny_baseline.hex" + image.write_text("@0\n00\n") + source = directory / "source.json" + source.write_text("{}\n") + value = {"status": "passed", "phase": baseline.PHASE, "parameters": baseline.PARAMETERS, + "artifacts": {"tiny_baseline.hex": baseline.artifact(image)}, + "source": {"repository": {"commit": "original-revision"}, + **{key: baseline.artifact(source) for key in ("inputs", "snapshot", "patch")}}} + (directory / "image.json").write_text(json.dumps(value)) + baseline.dump(evidence / "binaries/latest.json", {"path": str(directory / "image.json")}) + _, retained = baseline.retained_image(evidence, None) + assert retained["source"]["repository"]["commit"] == "original-revision" + bad = copy.deepcopy(value) + bad["status"] = "failed" + (directory / "image.json").write_text(json.dumps(bad)) + with pytest.raises(ValueError, match="provenance"): + baseline.retained_image(evidence, image) + (directory / "image.json").write_text(json.dumps(value)) + image.write_text("@0\nff\n") + with pytest.raises(ValueError, match="changed artifact"): + baseline.retained_image(evidence, image) + + +def retained_runs(directory: Path, *, differing: bool = False) -> list[dict]: + runs = [] + for repetition in range(1, 4): + log = directory / f"run-{repetition}.log" + content = log_fixture() + if differing and repetition == 2: + content = content.replace("cycles=1000000", "cycles=1000001", 1) + log.write_text(content) + flow = directory / f"run-{repetition}.json" + flow.write_text(json.dumps({**PASS, "command": ["fixture"], "log": str(log)})) + verdict = directory / f"verdict-{repetition}.json" + verdict.write_text(json.dumps({"status": "passed", "log": str(log)})) + runs.append({"repetition": repetition, "samples": baseline.parse_log(content, PASS), + "log": baseline.artifact(log), "flow": baseline.artifact(flow), + "verdict": baseline.artifact(verdict)}) + return runs + + +def test_aggregate_rechecks_repetitions_instead_of_trusting_pass_labels(tmp_path: Path) -> None: + baseline.validate_runs(retained_runs(tmp_path)) + with pytest.raises(ValueError, match="not deterministic"): + baseline.validate_runs(retained_runs(tmp_path, differing=True)) + + +def test_aggregate_rejects_reused_repetition_and_mismatched_verdict_log(tmp_path: Path) -> None: + runs = retained_runs(tmp_path) + with pytest.raises(ValueError, match="three distinct"): + baseline.validate_runs([runs[0], runs[0], runs[2]]) + verdict = Path(runs[0]["verdict"]["path"]) + verdict.write_text(json.dumps({"status": "passed", "log": "another-simulation.log"})) + runs[0]["verdict"] = baseline.artifact(verdict) + with pytest.raises(ValueError, match="simulation verdict"): + baseline.validate_runs(runs) + + +def test_report_without_runs_is_incomplete(tmp_path: Path) -> None: + import argparse + + variant = tmp_path / "build/ihp130-tiny-test" + assert baseline.report(argparse.Namespace(root=tmp_path, variant_root=variant)) == 1 + assert baseline.read_json(variant / "meta/tiny-r2-p1/baseline-report.json")["status"] == "incomplete" + + +def test_make_dry_run_does_not_execute_baseline_driver() -> None: + stamp = "2000-01-01-00-01" + build = baseline.ROOT / "build" + before = set(build.glob(f"ihp130-tiny-{stamp}-*")) + result = subprocess.run(["make", "--dry-run", "CONFIG=configs/ci/ihp130-tiny.mk", + f"BUILD_TIMESTAMP={stamp}", "tiny-r2-baseline-sim"], + cwd=baseline.ROOT, text=True, capture_output=True, check=True) + assert "--simulator verilator" in result.stdout + assert "SOC=TINY" in result.stdout and "PDK=IHP130" in result.stdout + assert "--always-make" not in result.stdout # The explicit -B is passed to the child only. + assert "-j1 -B CONFIG=" in result.stdout + assert set(build.glob(f"ihp130-tiny-{stamp}-*")) == before + + +@pytest.fixture +def locked_icarus(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> tuple[Path, Path]: + cache = tmp_path / "cache" + installed = cache / "toolchains/iverilog-test" + binaries = installed / "bin" + binaries.mkdir(parents=True) + archive = cache / "downloads/icarus.tar.gz" + archive.parent.mkdir() + archive.write_bytes(b"locked test archive") + spec = {"version": "test", "archive": archive.name, "sha256": baseline.sha256(archive), "path": "bin"} + baseline.dump(tmp_path / "dependencies/dependencies.lock.json", + {"toolchains": {"ubuntu-22.04": {"iverilog": spec}}}) + (installed / ".complete").write_text(spec["sha256"]) + for name in ("iverilog", "vvp"): + program = binaries / name + program.write_text("#!/bin/sh\nprintf 'Icarus fixture version\\n'\n") + program.chmod(0o755) + monkeypatch.setenv("RETROSOC_DEVELOPMENT_CACHE", str(cache)) + monkeypatch.setenv("PATH", str(binaries) + os.pathsep + os.defpath) + monkeypatch.setattr(baseline, "TOOL_PROGRAMS", {"iverilog": "iverilog"}) + return tmp_path, binaries / "vvp" + + +@pytest.mark.parametrize("selection", ["name", "absolute", "alias"]) +def test_selected_vvp_resolves_to_locked_executable(locked_icarus, selection: str) -> None: + root, program = locked_icarus + selected = "vvp" if selection == "name" else str(program) + if selection == "alias": + alias = root / "runtime-alias" + alias.symlink_to(program) + selected = str(alias) + compiler = baseline.tools_identity(root)["iverilog"] + identity = baseline.vvp_identity(selected, compiler) + assert identity["executable"] == baseline.artifact(program) + assert identity["archive"] == compiler["archive"] + assert identity["version"] == "Icarus fixture version" + + +@pytest.mark.parametrize("failure", ["external", "missing", "bad-marker", "missing-marker"]) +def test_invalid_runtime_fails_before_compilation_or_simulation(locked_icarus, monkeypatch, failure: str) -> None: + root, program = locked_icarus + selected = str(program) + if failure == "external": + external = root / "external-vvp" + external.write_bytes(program.read_bytes()) + external.chmod(0o755) + selected = str(external) + elif failure == "missing": + selected = str(root / "missing-vvp") + elif failure == "bad-marker": + (program.parent.parent / ".complete").write_text("wrong archive") + else: + (program.parent.parent / ".complete").unlink() + image_path = root / "image.json" + image_path.write_text("{}") + image = {"source": {}, "artifacts": {"tiny_baseline.hex": {"sha256": "fixture"}}} + monkeypatch.setattr(baseline, "retained_image", lambda *_: (image_path, image)) + monkeypatch.setattr(baseline, "configuration", lambda *_: {}) + monkeypatch.setattr(baseline, "execute", lambda *_args, **_kwargs: pytest.fail("tool execution preceded rejection")) + args = argparse.Namespace(root=root, variant_root=root / "build/variant", simulator="iverilog", + hex=None, vvp=selected, command=["unexpected-build"]) + with pytest.raises((ValueError, OSError)): + baseline.simulate(args) + pointer = baseline.read_json(args.variant_root / "meta/tiny-r2-p1/workloads/iverilog/latest.json") + attempt = baseline.read_json(Path(pointer["path"])) + assert attempt["status"] == "failed" and not attempt["runs"] + + +@pytest.mark.parametrize("mutation", ["none", "missing", "executable", "archive", "launch", "flow"]) +def test_runtime_retention_and_actual_launch_are_rechecked(locked_icarus, mutation: str) -> None: + root, program = locked_icarus + tools = baseline.tools_identity(root) + runtime = baseline.vvp_identity(str(program), tools["iverilog"]) + launch = [str(program), "model", "+firmware=retained.hex"] + flow_path = root / "run.json" + baseline.dump(flow_path, {"command": ["timeout", "--foreground", "10s", *launch]}) + attempt = {"simulator": "iverilog", "tools": tools, "runtime_tool": runtime, + "command": launch, "timeout_seconds": 10, "runs": [{"flow": baseline.artifact(flow_path)}]} + if mutation == "missing": + del attempt["runtime_tool"] + elif mutation == "executable": + program.write_bytes(b"changed executable") + elif mutation == "archive": + Path(runtime["archive"]["path"]).write_bytes(b"changed archive") + elif mutation == "launch": + attempt["command"][0] = "/unlocked/vvp" + elif mutation == "flow": + baseline.dump(flow_path, {"command": ["timeout", "--foreground", "10s", "/unlocked/vvp", "model"]}) + attempt["runs"][0]["flow"] = baseline.artifact(flow_path) + if mutation == "none": + baseline.validate_runtime_tool(root, attempt) + else: + with pytest.raises(ValueError): + baseline.validate_runtime_tool(root, attempt) + + +def test_image_retains_compiler_command_after_postprocessing(tmp_path: Path, monkeypatch) -> None: + variant = tmp_path / "build/variant" + monkeypatch.setattr(baseline, "configuration", lambda *_: {}) + monkeypatch.setattr(baseline, "tools_identity", lambda *_: {}) + monkeypatch.setattr(baseline, "source_inputs", lambda *_: {}) + monkeypatch.setattr(baseline, "snapshot", lambda *_: {}) + commands = {} + + def execute(_root, directory, label, command, **_kwargs): + commands[label] = list(command) + baseline.dump(directory / f"{label}.json", {"command": command, **PASS}) + if label == "compile": + Path(command[-1]).write_bytes(b"ELF fixture") + elif label.startswith("objcopy-"): + Path(command[-1]).write_bytes(b"image fixture") + elif label == "symbols": + (directory / "symbols.log").write_text("30001000 00001000 b rs_baseline_tcds\n30004000 B _ebss\n") + return PASS + + monkeypatch.setattr(baseline, "execute", execute) + original = ["--", "gcc-fixture", "-Os", "input.c"] + args = argparse.Namespace(root=tmp_path, variant_root=variant, command=original, + objcopy="objcopy-fixture", objdump="objdump-fixture", + nm="nm-fixture", size="size-fixture") + assert baseline.build_image(args) == 0 + manifest = Path(baseline.read_json(variant / "meta/tiny-r2-p1/binaries/latest.json")["path"]) + image = baseline.read_json(manifest) + compile_result = baseline.read_json(manifest.parent / "compile.json") + assert image["compiler_command"] == compile_result["command"] == commands["compile"] + assert commands["size"][0] == "size-fixture" + assert image["compiler_command"][0] == "gcc-fixture" + assert original == ["--", "gcc-fixture", "-Os", "input.c"]