feat: add comprehensive tests for Tiny baseline evidence and log parsing

- Introduced a new test file  to validate the functionality of the Tiny baseline evidence.
- Implemented tests for CPU checksum verification, log parsing, error handling, and artifact identity checks.
- Added parameterized tests to cover various edge cases, including handling of incomplete or duplicate measurements and incorrect data.
- Ensured that the tests validate the integrity of simulation results and the retention of compiler commands after post-processing.
This commit is contained in:
maksyuki committed 2026-10-06 09:15:00 +08:00
1 parent bfdc3ffaf6
commit f06c039e79
7 files changed
+1974 -13

No files matched your search

+313
View File
@@ -0,0 +1,313 @@
/* Copyright (c) 2026 Yuchi Miao; SPDX-License-Identifier: MulanPSL-2.0 */
#include <stdbool.h>
#include <stdint.h>
#include <retrosoc/arch/riscv/system_base.h>
#include <retrosoc/core/soc.h>
#include <retrosoc/hal/dma.h>
#include <retrosoc/hal/onchip_sram.h>
#include <retrosoc/hal/sysctrl.h>
#include <retrosoc/hal/uart.h>
#include <retrosoc/lib/printf.h>
/* 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;
}
+299
View File
@@ -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-<timestamp>-<config-hash>/
meta/tiny-r2-p1/
binaries/image-*/ ELF/BIN/HEX, linker map, symbols, disassembly, size
inputs/ input hashes, source archive, worktree patch
workloads/<simulator>/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.
+210 -3
View File
@@ -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
+25 -1
View File
@@ -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
.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
+209 -9
View File
@@ -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 (
+564
View File
@@ -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())
+354
View File
@@ -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"]