mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
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:
7 files changed
+1974
-13
No files matched your search
@@ -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;
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
@@ -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
|
||||
@@ -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 (
|
||||
|
||||
@@ -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())
|
||||
@@ -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"]
|
||||
Reference in new issue
Block a user