mirror of
https://github.com/retroSoC/retroSoC.git
synced 2026-11-04 22:32:36 +00:00
feat(tiny): add R2-P10 performance qualification campaign
Add an isolated SAFE24 QVGA capture, CPU statistics and SDIO save/readback image with passive transaction observers. Keep ordinary images, production RTL, public interfaces and dependency versions unchanged. Retain matched binaries and source-bound stage evidence, distinguish software comparisons from architectural measurements, and reject invalid terminal results or incompatible resume inputs. Count SDIO descriptors and status writebacks separately from pixel payload. Validation on the final worktree includes 2031 passed / 1 skipped Pytest tests, C format/policy/host checks, and directed observer tests on Icarus and Verilator. Selected ICS55 and IHP130 SAFE24 campaigns and compact backend checks pass; negative STA and warning observations are retained. Complete P10 acceptance remains pending final regression and human review. Specification: docs/ip/tiny-soc.md, TINY-R2-P10.
This commit is contained in:
12 files changed
+1422
No files matched your search
@@ -90,6 +90,7 @@ TINY_P8_ACCEPTANCE ?= NO
|
||||
TINY_P9_ACCEPTANCE ?= NO
|
||||
TINY_P9_QUICK ?= NO
|
||||
TINY_P9_WORKLOAD ?= QVGA_RGB565
|
||||
TINY_P10_ACCEPTANCE ?= NO
|
||||
COREMARK_MODE ?= quick
|
||||
NPU_P5_ACCEPTANCE ?= NO
|
||||
NPU_P6_ACCEPTANCE ?= NO
|
||||
@@ -147,6 +148,9 @@ endif
|
||||
ifeq ($(TINY_P9_QUICK),YES)
|
||||
CONFIG_KEY_VARS += TINY_P9_QUICK
|
||||
endif
|
||||
ifeq ($(TINY_P10_ACCEPTANCE),YES)
|
||||
CONFIG_KEY_VARS += TINY_P10_ACCEPTANCE
|
||||
endif
|
||||
VARIANT_ID := $(strip $(shell $(VCS_SHELL_PYTHON) $(ROOT_PATH)/scripts/config_key.py \
|
||||
--lock $(LOCK_FILE) --profile $(PROFILE_NAME) --timestamp $(BUILD_TIMESTAMP) \
|
||||
$(foreach var,$(CONFIG_KEY_VARS),--value $(var)=$($(var))) | tail -n 1))
|
||||
@@ -311,6 +315,12 @@ $(call validate_value,TINY_P7_ACCEPTANCE,$(VALID_BOOL))
|
||||
$(call validate_value,TINY_P8_ACCEPTANCE,$(VALID_BOOL))
|
||||
$(call validate_value,TINY_P9_ACCEPTANCE,$(VALID_BOOL))
|
||||
$(call validate_value,TINY_P9_QUICK,$(VALID_BOOL))
|
||||
$(call validate_value,TINY_P10_ACCEPTANCE,$(VALID_BOOL))
|
||||
ifeq ($(TINY_P10_ACCEPTANCE),YES)
|
||||
ifneq ($(SOC)/$(APP)/$(HAVE_CSR)/$(TINY_P7_ACCEPTANCE)/$(TINY_P8_ACCEPTANCE)/$(TINY_P9_ACCEPTANCE)/$(TINY_P9_QUICK),TINY/ci_smoke/YES/NO/NO/NO/NO)
|
||||
$(error TINY_P10_ACCEPTANCE requires Tiny ci_smoke with CSR and no P7/P8/P9 acceptance image)
|
||||
endif
|
||||
endif
|
||||
$(call validate_value,TINY_P9_WORKLOAD,QVGA_RGB565 VGA_YUV422 RECOVERY)
|
||||
ifneq ($(TINY_P9_WORKLOAD),QVGA_RGB565)
|
||||
ifneq ($(TINY_P9_ACCEPTANCE)/$(TINY_P9_QUICK),YES/NO)
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
#ifndef RETROSOC_P10_FRAME_H
|
||||
#define RETROSOC_P10_FRAME_H
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <retrosoc/core/status.h>
|
||||
|
||||
/* Application statistics only; these are not DVP or hardware counters. */
|
||||
typedef struct {
|
||||
uint32_t red[32];
|
||||
uint32_t pixels;
|
||||
uint32_t checksum;
|
||||
} rs_p10_statistics_t;
|
||||
|
||||
static inline rs_status_t rs_p10_accumulate(rs_p10_statistics_t *statistics, uint32_t word) {
|
||||
if (statistics == NULL || statistics->pixels > UINT32_MAX - 2U) {
|
||||
return RS_EINVAL;
|
||||
}
|
||||
const uint32_t first = (word >> 11U) & UINT32_C(31);
|
||||
const uint32_t second = word >> 27U;
|
||||
if (statistics->red[first] == UINT32_MAX || statistics->red[second] == UINT32_MAX ||
|
||||
(first == second && statistics->red[first] == UINT32_MAX - 1U)) {
|
||||
return RS_EINVAL;
|
||||
}
|
||||
statistics->red[first]++;
|
||||
statistics->red[second]++;
|
||||
statistics->pixels += 2U;
|
||||
statistics->checksum = (statistics->checksum ^ word) * UINT32_C(16777619);
|
||||
return RS_OK;
|
||||
}
|
||||
|
||||
static inline rs_status_t rs_p10_chunk(uint32_t bytes, uint32_t index, uint32_t capacity_blocks,
|
||||
uint32_t *word_offset, uint32_t *block) {
|
||||
if (word_offset == NULL || block == NULL || bytes == 0U || bytes % 1024U != 0U ||
|
||||
index >= bytes / 1024U || capacity_blocks < 16U || bytes / 512U > capacity_blocks - 16U) {
|
||||
return RS_EINVAL;
|
||||
}
|
||||
*word_offset = index * 256U;
|
||||
*block = 16U + index * 2U;
|
||||
return RS_OK;
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -19,4 +19,7 @@ endif
|
||||
ifeq ($(TINY_P9_QUICK),YES)
|
||||
APP_CFLAGS += -DRS_TINY_P9_QUICK
|
||||
endif
|
||||
endif
|
||||
ifeq ($(TINY_P10_ACCEPTANCE),YES)
|
||||
APP_SRCS := $(ROOT_PATH)/app/apps/ci_smoke/tiny_r2_p10.c
|
||||
endif
|
||||
@@ -0,0 +1,263 @@
|
||||
#include <retrosoc/arch/riscv/system_base.h>
|
||||
#include <retrosoc/hal/dma.h>
|
||||
#include <retrosoc/hal/dvp.h>
|
||||
#include <retrosoc/hal/rcu.h>
|
||||
#include <retrosoc/hal/sdio.h>
|
||||
#include <retrosoc/hal/sysctrl.h>
|
||||
#include <retrosoc/hal/uart.h>
|
||||
#include <retrosoc/hal/xpi_psram.h>
|
||||
#include <retrosoc/lib/printf.h>
|
||||
|
||||
#include "p9_frame.h"
|
||||
#include "p9_startup.h"
|
||||
#include "p10_frame.h"
|
||||
|
||||
#define RS_P10_FRAME_ADDRESS UINT32_C(0x54010000)
|
||||
#define RS_P10_BYTES UINT32_C(153600)
|
||||
#define RS_P10_WORDS (RS_P10_BYTES / 4U)
|
||||
#define RS_P10_GUARD UINT32_C(0x5A36C9E7)
|
||||
|
||||
typedef struct {
|
||||
uint32_t before[16];
|
||||
uint32_t words[256];
|
||||
uint32_t after[16];
|
||||
} rs_p10_buffer_t;
|
||||
|
||||
typedef struct {
|
||||
uint64_t cycles;
|
||||
uint64_t instructions;
|
||||
uint32_t bytes;
|
||||
uint32_t calls;
|
||||
} rs_p10_window_t;
|
||||
|
||||
static _Alignas(64) rs_p10_buffer_t rs_p10_transmit;
|
||||
static _Alignas(64) rs_p10_buffer_t rs_p10_receive;
|
||||
static rs_p10_statistics_t rs_p10_statistics = {.checksum = UINT32_C(2166136261)};
|
||||
static rs_p10_window_t rs_p10_windows[5];
|
||||
static uint64_t rs_p10_cycle_start;
|
||||
static uint64_t rs_p10_instruction_start;
|
||||
|
||||
static uint64_t rs_p10_cycles(void) {
|
||||
uint32_t high, low, 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_p10_instructions(void) {
|
||||
uint32_t high, low, 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 bool rs_p10_begin(void) {
|
||||
if (rs_sysctrl_set_perf_control(true, true, false) != RS_OK) {
|
||||
return false;
|
||||
}
|
||||
rs_p10_cycle_start = rs_p10_cycles();
|
||||
rs_p10_instruction_start = rs_p10_instructions();
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool rs_p10_end(uint32_t phase, uint32_t bytes) {
|
||||
const uint64_t instructions = rs_p10_instructions() - rs_p10_instruction_start;
|
||||
const uint64_t cycles = rs_p10_cycles() - rs_p10_cycle_start;
|
||||
if (phase >= 5U || rs_sysctrl_set_perf_control(false, false, true) != RS_OK) {
|
||||
return false;
|
||||
}
|
||||
rs_p10_windows[phase].cycles += cycles;
|
||||
rs_p10_windows[phase].instructions += instructions;
|
||||
rs_p10_windows[phase].bytes += bytes;
|
||||
rs_p10_windows[phase].calls++;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool rs_p10_guards(void) {
|
||||
const volatile uint32_t *const frame = (const volatile uint32_t *)RS_P10_FRAME_ADDRESS;
|
||||
if (*(const volatile uint32_t *)(RS_P10_FRAME_ADDRESS - 4U) != RS_P10_GUARD ||
|
||||
frame[RS_P10_WORDS] != RS_P10_GUARD) {
|
||||
return false;
|
||||
}
|
||||
for (uint32_t index = 0U; index < 16U; ++index) {
|
||||
if (rs_p10_transmit.before[index] != RS_P10_GUARD ||
|
||||
rs_p10_transmit.after[index] != RS_P10_GUARD ||
|
||||
rs_p10_receive.before[index] != RS_P10_GUARD ||
|
||||
rs_p10_receive.after[index] != RS_P10_GUARD) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool rs_p10_pins(uint32_t first, uint32_t last, rs_gpio_mode_t mode) {
|
||||
const rs_gpio_config_t config = {
|
||||
.mode = mode,
|
||||
.pull = RS_GPIO_PULL_NONE,
|
||||
.trigger = RS_GPIO_TRIGGER_NONE,
|
||||
};
|
||||
for (uint32_t pin = first; pin <= last; ++pin) {
|
||||
if (rs_gpio_configure(pin, &config) != RS_OK) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static uint8_t rs_p10_capture(void) {
|
||||
const rs_dvp_config_t config = {
|
||||
.format = RS_DVP_FORMAT_RGB565,
|
||||
.frame_width = 320U,
|
||||
.frame_height = 240U,
|
||||
.crop_width = 320U,
|
||||
.crop_height = 240U,
|
||||
.snapshot = true,
|
||||
.stream_enable = true,
|
||||
};
|
||||
rs_dvp_status_t dvp;
|
||||
rs_dma_status_t dma;
|
||||
rs_xpi_error_t xpi;
|
||||
if (rs_dvp_configure(&config) != RS_OK || !rs_p10_begin()) {
|
||||
return 3U;
|
||||
}
|
||||
const rs_status_t result =
|
||||
rs_dvp_capture_dma(RS_P10_FRAME_ADDRESS, RS_P10_WORDS, RS_TIMEOUT_DEFAULT);
|
||||
if (!rs_p10_end(0U, RS_P10_BYTES) || result != RS_OK || rs_dvp_get_status(&dvp) != RS_OK ||
|
||||
rs_dma_get_status(RS_DMA_CHANNEL_DVP, &dma) != RS_OK || rs_xpi_error_get(&xpi) != RS_OK ||
|
||||
xpi.valid || dvp.error_status != 0U || dvp.active || dvp.pixel_count != 76800U ||
|
||||
dvp.word_count != RS_P10_WORDS || !dma.done || dma.busy || dma.error ||
|
||||
dma.bytes_done != RS_P10_BYTES || !rs_p10_guards()) {
|
||||
return 3U;
|
||||
}
|
||||
/* No further camera frame or DMA work is admitted during processing/save. */
|
||||
return rs_dvp_abort() == RS_OK ? 0U : 3U;
|
||||
}
|
||||
|
||||
static uint8_t rs_p10_process(void) {
|
||||
const volatile uint32_t *const frame = (const volatile uint32_t *)RS_P10_FRAME_ADDRESS;
|
||||
if (!rs_p10_begin()) {
|
||||
return 4U;
|
||||
}
|
||||
for (uint32_t index = 0U; index < RS_P10_WORDS; ++index) {
|
||||
const uint32_t word = frame[index];
|
||||
if (word != rs_p9_frame_word(index / 160U, index % 160U, false) ||
|
||||
rs_p10_accumulate(&rs_p10_statistics, word) != RS_OK) {
|
||||
(void)rs_p10_end(1U, index * 4U);
|
||||
return 4U;
|
||||
}
|
||||
}
|
||||
return rs_p10_end(1U, RS_P10_BYTES) && rs_p10_guards() ? 0U : 4U;
|
||||
}
|
||||
|
||||
static uint8_t rs_p10_save(const rs_sd_memory_info_t *card) {
|
||||
const volatile uint32_t *const frame = (const volatile uint32_t *)RS_P10_FRAME_ADDRESS;
|
||||
for (uint32_t chunk = 0U; chunk < 150U; ++chunk) {
|
||||
uint32_t offset, block;
|
||||
if (rs_p10_chunk(RS_P10_BYTES, chunk, card->capacity_blocks, &offset, &block) != RS_OK ||
|
||||
!rs_p10_begin()) {
|
||||
return 5U;
|
||||
}
|
||||
for (uint32_t word = 0U; word < 256U; ++word) {
|
||||
rs_p10_transmit.words[word] = frame[offset + word];
|
||||
rs_p10_receive.words[word] = UINT32_C(0xDEADBEEF);
|
||||
}
|
||||
if (!rs_p10_end(2U, 1024U) || !rs_p10_begin()) {
|
||||
return 5U;
|
||||
}
|
||||
rs_status_t result = rs_sd_memory_write_blocks(RS_SDIO_INSTANCE_0, card, block, 2U,
|
||||
rs_p10_transmit.words, RS_TIMEOUT_DEFAULT);
|
||||
if (!rs_p10_end(3U, 1024U) || result != RS_OK || !rs_p10_begin()) {
|
||||
return 5U;
|
||||
}
|
||||
result = rs_sd_memory_read_blocks(RS_SDIO_INSTANCE_0, card, block, 2U, rs_p10_receive.words,
|
||||
RS_TIMEOUT_DEFAULT);
|
||||
bool matches = result == RS_OK;
|
||||
for (uint32_t word = 0U; word < 256U; ++word) {
|
||||
const uint32_t index = offset + word;
|
||||
if (rs_p10_receive.words[word] != rs_p9_frame_word(index / 160U, index % 160U, false)) {
|
||||
matches = false;
|
||||
}
|
||||
}
|
||||
if (!rs_p10_end(4U, 1024U) || !matches || !rs_p10_guards()) {
|
||||
return 6U;
|
||||
}
|
||||
if (chunk % 32U == 0U) {
|
||||
printf("P10_PROGRESS chunks=%u\n", chunk + 1U);
|
||||
}
|
||||
}
|
||||
return 0U;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
rs_tiny_rcu_snapshot_t clock;
|
||||
rs_xpi_psram_t psram;
|
||||
rs_sd_memory_info_t card;
|
||||
uint8_t code = 0U;
|
||||
__disable_irq();
|
||||
if (rs_tiny_rcu_get_snapshot(&clock) != RS_OK || !rs_p9_console_ready(&clock, rs_uart_init)) {
|
||||
rs_sysctrl_write_test_status(false, 1U);
|
||||
for (;;) {
|
||||
}
|
||||
}
|
||||
if (clock.sys_hz != 24000000U || clock.mem_hz != 24000000U || clock.pclk_hz != 24000000U ||
|
||||
!rs_p9_psram_pin_ready(rs_gpio_configure) || !rs_p10_pins(12U, 23U, RS_GPIO_MODE_ALT1) ||
|
||||
!rs_p10_pins(0U, 5U, RS_GPIO_MODE_ALT0) || rs_tiny_rcu_set_clkout(1U, 1U) != RS_OK ||
|
||||
rs_xpi_psram_initialize(clock.mem_hz, RS_TIMEOUT_DEFAULT, &psram) != RS_OK ||
|
||||
rs_xpi_psram_validate_dma(&psram, RS_P10_FRAME_ADDRESS, RS_P10_BYTES, 4U) != RS_OK ||
|
||||
rs_sd_memory_initialize(RS_SDIO_INSTANCE_0, clock.pclk_hz, &card, RS_TIMEOUT_DEFAULT) !=
|
||||
RS_OK ||
|
||||
card.capacity_blocks < 316U || card.actual_clock_hz != 12000000U) {
|
||||
code = 2U;
|
||||
} else {
|
||||
*(volatile uint32_t *)(RS_P10_FRAME_ADDRESS - 4U) = RS_P10_GUARD;
|
||||
((volatile uint32_t *)RS_P10_FRAME_ADDRESS)[RS_P10_WORDS] = RS_P10_GUARD;
|
||||
for (uint32_t index = 0U; index < 16U; ++index) {
|
||||
rs_p10_transmit.before[index] = RS_P10_GUARD;
|
||||
rs_p10_transmit.after[index] = RS_P10_GUARD;
|
||||
rs_p10_receive.before[index] = RS_P10_GUARD;
|
||||
rs_p10_receive.after[index] = RS_P10_GUARD;
|
||||
}
|
||||
printf("P10_STAGE capture\n");
|
||||
code = rs_p10_capture();
|
||||
if (code == 0U) {
|
||||
printf("P10_STAGE process\n");
|
||||
code = rs_p10_process();
|
||||
}
|
||||
if (code == 0U) {
|
||||
printf("P10_STAGE save\n");
|
||||
code = rs_p10_save(&card);
|
||||
}
|
||||
if (code == 0U) {
|
||||
rs_xpi_error_t error;
|
||||
rs_sdio_status_t storage;
|
||||
if (rs_xpi_error_get(&error) != RS_OK || error.valid ||
|
||||
rs_sdio_status_get(RS_SDIO_INSTANCE_0, &storage) != RS_OK ||
|
||||
storage.error_status != 0U || storage.busy || !rs_p10_guards()) {
|
||||
code = 7U;
|
||||
}
|
||||
}
|
||||
}
|
||||
(void)rs_sysctrl_set_perf_control(false, false, true);
|
||||
for (uint32_t phase = 0U; phase < 5U; ++phase) {
|
||||
printf("P10_PHASE id=%u calls=%u bytes=%u cycles=%llu instructions=%llu\n", phase,
|
||||
rs_p10_windows[phase].calls, rs_p10_windows[phase].bytes,
|
||||
(unsigned long long)rs_p10_windows[phase].cycles,
|
||||
(unsigned long long)rs_p10_windows[phase].instructions);
|
||||
}
|
||||
for (uint32_t bin = 0U; bin < 32U; ++bin) {
|
||||
printf("P10_HIST bin=%u pixels=%u\n", bin, rs_p10_statistics.red[bin]);
|
||||
}
|
||||
printf("P10_RESULT code=%u pixels=%u bytes=%u sectors=%u checksum=%u\n", code,
|
||||
rs_p10_statistics.pixels, code == 0U ? RS_P10_BYTES : 0U, code == 0U ? 300U : 0U,
|
||||
rs_p10_statistics.checksum);
|
||||
(void)rs_tiny_rcu_set_clkout(0U, 0U);
|
||||
rs_sysctrl_write_test_status(code == 0U, code);
|
||||
for (;;) {
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,7 @@ module retrosoc_tiny_tb;
|
||||
`include "tiny_p8_observers.svh"
|
||||
`include "tiny_p9_observers.svh"
|
||||
`include "tiny_p9_recovery.svh"
|
||||
`include "tiny_p10_observers.svh"
|
||||
`endif
|
||||
integer s_max_cycles = 20000000;
|
||||
integer s_done_cycles = 0;
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
// Verification-only transaction accounting. No DUT signal is driven.
|
||||
module tiny_p10_observer #(
|
||||
parameter integer Owners = 3
|
||||
) (
|
||||
input logic clk_i,
|
||||
input logic rst_n_i,
|
||||
input logic enable_i,
|
||||
input logic [Owners-1:0] read_accept_i,
|
||||
input logic [Owners-1:0] read_done_i,
|
||||
input logic [Owners-1:0] write_accept_i,
|
||||
input logic [Owners-1:0] write_done_i,
|
||||
input logic [Owners-1:0] error_i,
|
||||
input logic [Owners-1:0][7:0] read_bytes_i,
|
||||
input logic [Owners-1:0][7:0] write_bytes_i,
|
||||
input logic [Owners-1:0][4:0] stalled_i,
|
||||
input logic [ 5:0] active_targets_i,
|
||||
input logic [ 3:0] bank_issues_i,
|
||||
input logic dma_beat_overlap_i
|
||||
);
|
||||
longint unsigned s_cycle = 0;
|
||||
integer s_window = 0;
|
||||
logic s_previous = 0;
|
||||
logic [Owners-1:0] s_read_pending = 0, s_write_pending = 0;
|
||||
longint unsigned s_read_start[Owners], s_write_start[Owners];
|
||||
longint unsigned s_reads[Owners], s_writes[Owners], s_read_done[Owners], s_write_done[Owners];
|
||||
longint unsigned s_read_bytes[Owners], s_write_bytes[Owners], s_errors[Owners];
|
||||
longint unsigned s_read_max[Owners], s_write_max[Owners];
|
||||
longint unsigned s_stall[Owners][5], s_stall_max[Owners][5];
|
||||
logic [Owners-1:0] s_read_initial = 0, s_write_initial = 0;
|
||||
integer s_active_max = 0;
|
||||
longint unsigned s_bank_overlap = 0, s_dma_overlap = 0;
|
||||
integer s_bank_count;
|
||||
|
||||
always @(posedge clk_i) begin
|
||||
if (!rst_n_i) begin
|
||||
s_previous = 0;
|
||||
s_read_pending = 0;
|
||||
s_write_pending = 0;
|
||||
s_window = 0;
|
||||
s_cycle = 0;
|
||||
end else begin
|
||||
s_cycle++;
|
||||
if (enable_i && !s_previous) begin
|
||||
s_window++;
|
||||
s_read_initial = s_read_pending;
|
||||
s_write_initial = s_write_pending;
|
||||
s_active_max = 0;
|
||||
s_bank_overlap = 0;
|
||||
s_dma_overlap = 0;
|
||||
for (integer owner = 0; owner < Owners; owner++) begin
|
||||
s_reads[owner] = 0;
|
||||
s_writes[owner] = 0;
|
||||
s_read_done[owner] = 0;
|
||||
s_write_done[owner] = 0;
|
||||
s_read_bytes[owner] = 0;
|
||||
s_write_bytes[owner] = 0;
|
||||
s_errors[owner] = 0;
|
||||
s_read_max[owner] = 0;
|
||||
s_write_max[owner] = 0;
|
||||
for (integer channel = 0; channel < 5; channel++) begin
|
||||
s_stall[owner][channel] = 0;
|
||||
s_stall_max[owner][channel] = 0;
|
||||
end
|
||||
end
|
||||
end
|
||||
if (!enable_i && s_previous) begin
|
||||
for (integer owner = 0; owner < Owners; owner++) begin
|
||||
$display(
|
||||
"P10_BUS window=%0d owner=%0d reads=%0d writes=%0d read_done=%0d write_done=%0d read_initial=%0d write_initial=%0d read_pending=%0d write_pending=%0d read_bytes=%0d write_bytes=%0d errors=%0d read_latency_max=%0d write_latency_max=%0d ar_max=%0d r_max=%0d aw_max=%0d w_max=%0d b_max=%0d",
|
||||
s_window, owner, s_reads[owner], s_writes[owner], s_read_done[owner],
|
||||
s_write_done[owner], s_read_initial[owner], s_write_initial[owner],
|
||||
s_read_pending[owner], s_write_pending[owner], s_read_bytes[owner],
|
||||
s_write_bytes[owner], s_errors[owner], s_read_max[owner], s_write_max[owner],
|
||||
s_stall_max[owner][0], s_stall_max[owner][1], s_stall_max[owner][2],
|
||||
s_stall_max[owner][3], s_stall_max[owner][4]);
|
||||
end
|
||||
$display(
|
||||
"P10_CONCURRENCY window=%0d active_targets_max=%0d bank_overlap=%0d dma_beat_overlap=%0d",
|
||||
s_window, s_active_max, s_bank_overlap, s_dma_overlap);
|
||||
end
|
||||
for (integer owner = 0; owner < Owners; owner++) begin
|
||||
if (read_accept_i[owner]) begin
|
||||
if (s_read_pending[owner]) $fatal(1, "SIM_TEST_FAIL P10 duplicate AR");
|
||||
s_read_pending[owner] = 1;
|
||||
s_read_start[owner] = s_cycle;
|
||||
if (enable_i) s_reads[owner]++;
|
||||
end
|
||||
if (write_accept_i[owner]) begin
|
||||
if (s_write_pending[owner]) $fatal(1, "SIM_TEST_FAIL P10 duplicate AW");
|
||||
s_write_pending[owner] = 1;
|
||||
s_write_start[owner] = s_cycle;
|
||||
if (enable_i) s_writes[owner]++;
|
||||
end
|
||||
if (read_done_i[owner]) begin
|
||||
if (!s_read_pending[owner]) $fatal(1, "SIM_TEST_FAIL P10 unsolicited RLAST");
|
||||
s_read_pending[owner] = 0;
|
||||
if (enable_i) begin
|
||||
s_read_done[owner]++;
|
||||
if (s_cycle - s_read_start[owner] > s_read_max[owner])
|
||||
s_read_max[owner] = s_cycle - s_read_start[owner];
|
||||
end
|
||||
end
|
||||
if (write_done_i[owner]) begin
|
||||
if (!s_write_pending[owner]) $fatal(1, "SIM_TEST_FAIL P10 unsolicited B");
|
||||
s_write_pending[owner] = 0;
|
||||
if (enable_i) begin
|
||||
s_write_done[owner]++;
|
||||
if (s_cycle - s_write_start[owner] > s_write_max[owner])
|
||||
s_write_max[owner] = s_cycle - s_write_start[owner];
|
||||
end
|
||||
end
|
||||
if (enable_i) begin
|
||||
s_read_bytes[owner] += 64'(read_bytes_i[owner]);
|
||||
s_write_bytes[owner] += 64'(write_bytes_i[owner]);
|
||||
if (error_i[owner]) s_errors[owner]++;
|
||||
for (integer channel = 0; channel < 5; channel++) begin
|
||||
s_stall[owner][channel] = ((stalled_i[owner] >> channel) & 5'b1) != 0 ?
|
||||
s_stall[owner][channel] + 1 : 0;
|
||||
if (s_stall[owner][channel] > s_stall_max[owner][channel])
|
||||
s_stall_max[owner][channel] = s_stall[owner][channel];
|
||||
end
|
||||
end
|
||||
end
|
||||
if (enable_i) begin
|
||||
if (integer'(active_targets_i) > s_active_max) s_active_max = integer'(active_targets_i);
|
||||
s_bank_count = 0;
|
||||
for (integer bank = 0; bank < 4; bank++) if (bank_issues_i[bank]) s_bank_count++;
|
||||
if (s_bank_count > 1) s_bank_overlap++;
|
||||
if (dma_beat_overlap_i) s_dma_overlap++;
|
||||
end
|
||||
s_previous = enable_i;
|
||||
end
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,59 @@
|
||||
// Opt-in P10 observations, excluded from mapped-netlist elaboration.
|
||||
wire s_p10_camera = $test$plusargs("tiny_p10_camera");
|
||||
wire s_p10_audio = $test$plusargs("tiny_p10_audio");
|
||||
wire s_p10_enabled = s_p10_camera ? u_dut.u_soc.s_perf_enable : (s_p10_audio && s_p6_audio_enabled);
|
||||
wire [2:0] s_p10_ar, s_p10_r, s_p10_aw, s_p10_b, s_p10_error;
|
||||
wire [2:0][7:0] s_p10_read_bytes, s_p10_write_bytes;
|
||||
wire [2:0][4:0] s_p10_stalled;
|
||||
wire [3:0] s_p10_bank_issues;
|
||||
logic [5:0] s_p10_targets;
|
||||
always_comb begin
|
||||
s_p10_targets = 0;
|
||||
for (integer target_index = 0; target_index < `RETROSOC_TINY__TARGET_COUNT; target_index++)
|
||||
if (!u_dut.u_soc.u_fabric.target_idle_o[target_index]) s_p10_targets++;
|
||||
end
|
||||
for (genvar owner = 0; owner < 3; owner++) begin : gen_p10_bus
|
||||
`define _P10_AXI u_dut.u_soc.u_masters_axi4_if[owner]
|
||||
logic [2:0] s_read_size = 0;
|
||||
always @(posedge u_dut.u_soc.s_exec_clk)
|
||||
if (`_P10_AXI.arvalid && `_P10_AXI.arready)
|
||||
s_read_size <= `_P10_AXI.arsize;
|
||||
assign s_p10_ar[owner] = `_P10_AXI.arvalid && `_P10_AXI.arready;
|
||||
assign s_p10_r[owner] = `_P10_AXI.rvalid && `_P10_AXI.rready && `_P10_AXI.rlast;
|
||||
assign s_p10_aw[owner] = `_P10_AXI.awvalid && `_P10_AXI.awready;
|
||||
assign s_p10_b[owner] = `_P10_AXI.bvalid && `_P10_AXI.bready;
|
||||
assign s_p10_error[owner] = (`_P10_AXI.rvalid && `_P10_AXI.rready && `_P10_AXI.rresp != 0) ||
|
||||
(s_p10_b[owner] && `_P10_AXI.bresp != 0);
|
||||
assign s_p10_read_bytes[owner] = (`_P10_AXI.rvalid && `_P10_AXI.rready && `_P10_AXI.rresp == 0)
|
||||
? (8'd1 << s_read_size) : 8'd0;
|
||||
assign s_p10_write_bytes[owner] = (`_P10_AXI.wvalid && `_P10_AXI.wready) ?
|
||||
8'(`_P10_AXI.wstrb[0]) + 8'(`_P10_AXI.wstrb[1]) +
|
||||
8'(`_P10_AXI.wstrb[2]) + 8'(`_P10_AXI.wstrb[3]) : 8'd0;
|
||||
assign s_p10_stalled[owner] = {
|
||||
`_P10_AXI.bvalid && !`_P10_AXI.bready,
|
||||
`_P10_AXI.wvalid && !`_P10_AXI.wready,
|
||||
`_P10_AXI.awvalid && !`_P10_AXI.awready,
|
||||
`_P10_AXI.rvalid && !`_P10_AXI.rready,
|
||||
`_P10_AXI.arvalid && !`_P10_AXI.arready
|
||||
};
|
||||
`undef _P10_AXI
|
||||
end
|
||||
for (genvar bank = 0; bank < 4; bank++) begin : gen_p10_bank
|
||||
assign s_p10_bank_issues[bank] = u_dut.u_soc.u_sram.gen_group[bank].u_group.s_issue;
|
||||
end
|
||||
tiny_p10_observer u_p10_observer (
|
||||
.clk_i (u_dut.u_soc.s_exec_clk),
|
||||
.rst_n_i (u_dut.u_soc.s_fabric_rst_n && (s_p10_camera || s_p10_audio)),
|
||||
.enable_i (s_p10_enabled),
|
||||
.read_accept_i (s_p10_ar),
|
||||
.read_done_i (s_p10_r),
|
||||
.write_accept_i (s_p10_aw),
|
||||
.write_done_i (s_p10_b),
|
||||
.error_i (s_p10_error),
|
||||
.read_bytes_i (s_p10_read_bytes),
|
||||
.write_bytes_i (s_p10_write_bytes),
|
||||
.stalled_i (s_p10_stalled),
|
||||
.active_targets_i (s_p10_targets),
|
||||
.bank_issues_i (s_p10_bank_issues),
|
||||
.dma_beat_overlap_i((s_p10_read_bytes[1] != 0) && (s_p10_write_bytes[1] != 0))
|
||||
);
|
||||
@@ -3,3 +3,4 @@
|
||||
/dv/tiny_dvp_camera.sv
|
||||
/tests/rtl/sdio_native_model.sv
|
||||
/dv/retrosoc_tiny_tb.sv
|
||||
/dv/tiny_p10_observer.sv
|
||||
@@ -0,0 +1,637 @@
|
||||
"""Single-execution, source-bound Tiny P10 measurements; never phase signoff."""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
sys.path.insert(0, str(ROOT))
|
||||
from scripts import tiny_r2_baseline as b # noqa: E402
|
||||
from scripts import tiny_r2_p3 as p3 # noqa: E402
|
||||
from scripts import tiny_r2_p4 as p4 # noqa: E402
|
||||
from scripts import tiny_r2_p5 as p5 # noqa: E402
|
||||
from scripts import tiny_r2_p6 as p6 # noqa: E402
|
||||
from scripts.check_simulation import DEFAULT_FAILURE # noqa: E402
|
||||
from scripts.check_c_warnings import self_owned_warnings # noqa: E402
|
||||
|
||||
PHASE = "TINY-R2-P10"
|
||||
COLLECTOR_UPDATE_PATHS = {"scripts/tiny_r2_p10.py", "tests/test_tiny_r2_p10.py"}
|
||||
STAGES = ("matched", "software", "audio", "camera", "backend", "report")
|
||||
P1 = "build/ihp130-tiny-2026-10-05-20-02-fa1c4ce1e303/meta/tiny-r2-p1/baseline-report.json"
|
||||
P3 = "build/ics55-tiny-performance-2026-10-09-17-10-53cc6674d532/meta/tiny-r2-p3/report.json"
|
||||
P5 = "build/ics55-tiny-2026-10-10-18-58-b4aa59f3e5d9/meta/tiny-r2-p5/report.json"
|
||||
MATCHED = ("perf-o2-plain-flat", "perf-o2-plain-banked")
|
||||
BUS_FIELDS = set("window owner reads writes read_done write_done read_initial write_initial "
|
||||
"read_pending write_pending read_bytes write_bytes errors read_latency_max "
|
||||
"write_latency_max ar_max r_max aw_max w_max b_max".split())
|
||||
CONCURRENCY_FIELDS = set("window active_targets_max bank_overlap dma_beat_overlap".split())
|
||||
DEFERRED = ["full long Icarus firmware: maintainer deferred to final Tiny qualification",
|
||||
"complete PR/nightly: retained matrix includes deferred Icarus and P7 clock transitions",
|
||||
"fresh full-system VGA: historical evidence retained",
|
||||
"physical/PVT/board/sensor and higher-frequency qualification"]
|
||||
|
||||
|
||||
def rows(content: str, prefix: str, fields: set[str]) -> list[dict]:
|
||||
return [b.parse_fields(line, fields) for line in content.splitlines()
|
||||
if line.startswith(prefix + " ")]
|
||||
|
||||
|
||||
def require_terminal(content: str, flow: dict) -> None:
|
||||
if (flow.get("exit_code") != 0 or flow.get("status") != "passed" or
|
||||
DEFAULT_FAILURE.search(content) or len(re.findall(
|
||||
r"^SIM_TEST_PASS Tiny cycles=[1-9][0-9]*$", content, re.M)) != 1):
|
||||
raise ValueError("Tiny command/terminal/forbidden-marker verdict failed")
|
||||
|
||||
|
||||
def golden() -> tuple[list[int], int]:
|
||||
"""Independent byte/pixel oracle, not firmware's packed-word implementation."""
|
||||
histogram = [0] * 32
|
||||
checksum = 2166136261
|
||||
for row in range(240):
|
||||
pixels = [(((2 * x + 3 * row) & 255) << 8) | ((2 * x + 3 * row + 1) & 255)
|
||||
for x in range(320)]
|
||||
for pixel in pixels:
|
||||
histogram[pixel >> 11] += 1
|
||||
for x in range(0, 320, 2):
|
||||
checksum = ((checksum ^ (pixels[x] | (pixels[x + 1] << 16))) * 16777619) & 0xffffffff
|
||||
return histogram, checksum
|
||||
|
||||
|
||||
def check_observations(content: str, windows: int, targets: int) -> dict:
|
||||
bus = rows(content, "P10_BUS", BUS_FIELDS)
|
||||
concurrency = rows(content, "P10_CONCURRENCY", CONCURRENCY_FIELDS)
|
||||
if [(r["window"], r["owner"]) for r in bus] != [
|
||||
(window, owner) for window in range(1, windows + 1) for owner in range(3)]:
|
||||
raise ValueError("missing, reordered or duplicate bus observation")
|
||||
if [r["window"] for r in concurrency] != list(range(1, windows + 1)):
|
||||
raise ValueError("incomplete concurrency windows")
|
||||
for row in bus:
|
||||
for direction in ("read", "write"):
|
||||
initial, pending = row[direction + "_initial"], row[direction + "_pending"]
|
||||
if (initial > 1 or pending > 1 or row[direction + "s"] + initial !=
|
||||
row[direction + "_done"] + pending):
|
||||
raise ValueError("transaction conservation mismatch")
|
||||
if row["errors"]:
|
||||
raise ValueError("bus error inside successful workload window")
|
||||
if any(r["active_targets_max"] > targets for r in concurrency):
|
||||
raise ValueError("concurrency exceeds canonical topology")
|
||||
return {"bus": bus, "concurrency": concurrency,
|
||||
"units": "SYS cycles; bytes from actual handshakes; censored CPU transactions retained"}
|
||||
|
||||
|
||||
def camera_measurements(content: str, flow: dict, targets: int) -> dict:
|
||||
require_terminal(content, flow)
|
||||
histogram, checksum = golden()
|
||||
result = rows(content, "P10_RESULT", {"code", "pixels", "bytes", "sectors", "checksum"})
|
||||
if result != [dict(code=0, pixels=76800, bytes=153600, sectors=300, checksum=checksum)]:
|
||||
raise ValueError("camera pipeline did not complete exact work")
|
||||
if rows(content, "P10_HIST", {"bin", "pixels"}) != [
|
||||
{"bin": index, "pixels": value} for index, value in enumerate(histogram)]:
|
||||
raise ValueError("CPU histogram differs from independent oracle")
|
||||
phases = rows(content, "P10_PHASE", {"id", "calls", "bytes", "cycles", "instructions"})
|
||||
if len(phases) != 5:
|
||||
raise ValueError("missing pipeline phase measurements")
|
||||
for index, row in enumerate(phases):
|
||||
if (row["id"] != index or row["calls"] != (1 if index < 2 else 150) or
|
||||
row["bytes"] != 153600 or not 0 < row["instructions"] <= row["cycles"]):
|
||||
raise ValueError("invalid pipeline phase accounting")
|
||||
observed = check_observations(content, 452, targets)
|
||||
for row in observed["bus"]:
|
||||
if row["owner"] != 0 and (row["read_pending"] or row["write_pending"]):
|
||||
raise ValueError("DMA work not drained at pipeline boundary")
|
||||
# Capture uses DMA2, saving uses the independent SDIO master and SRAM bounce buffers.
|
||||
if observed["bus"][1]["write_bytes"] != 153600:
|
||||
raise ValueError("camera DMA byte count mismatch")
|
||||
sdio = [row for row in observed["bus"] if row["owner"] == 2]
|
||||
for sample in sdio:
|
||||
# Each HAL call fetches one 16-byte descriptor and writes back its
|
||||
# four-byte status. Preserve those bytes in the raw bus observations.
|
||||
expected = (0, 0)
|
||||
if sample["window"] >= 4 and (sample["window"] - 4) % 3 == 0:
|
||||
expected = (1040, 4)
|
||||
elif sample["window"] >= 5 and (sample["window"] - 5) % 3 == 0:
|
||||
expected = (16, 1028)
|
||||
if (sample["read_bytes"], sample["write_bytes"]) != expected:
|
||||
raise ValueError("SDIO payload/descriptor/writeback accounting mismatch")
|
||||
fifo = rows(content, "P9_OBSERVATION", {"version", "accepted_words", "delivered_words",
|
||||
"fifo_high_water", "backpressure", "pixel_half_ns"})
|
||||
if len(fifo) != 1:
|
||||
raise ValueError("missing pixel FIFO observations")
|
||||
row = fifo[0]
|
||||
available = 512 - 4 * row["fifo_high_water"] - 16
|
||||
if (row["version"] != 1 or row["accepted_words"] != 38400 or row["delivered_words"] != 38400 or
|
||||
row["fifo_high_water"] == 0 or available <= 0 or row["pixel_half_ns"] != 500 or
|
||||
row["backpressure"] >= available * 24):
|
||||
raise ValueError("capture FIFO/rate budget invalid")
|
||||
return {"phases": phases, "histogram": histogram, "checksum": checksum,
|
||||
"observations": observed, "fifo": row,
|
||||
"sdio_bytes": {"payload_read": 153600, "payload_write": 153600,
|
||||
"descriptor_read": 4800, "descriptor_writeback": 1200,
|
||||
"observed_read": sum(r["read_bytes"] for r in sdio),
|
||||
"observed_write": sum(r["write_bytes"] for r in sdio)},
|
||||
"service_budget": {"payload_bytes": 512, "margin_bytes": 16,
|
||||
"empty_no_service_us": 496, "pixel_hz": 1000000,
|
||||
"high_water_no_service_us": available,
|
||||
"observed_backpressure_us": row["backpressure"] / 24}}
|
||||
|
||||
|
||||
def audio_measurements(content: str, flow: dict, targets: int) -> dict:
|
||||
require_terminal(content, flow)
|
||||
result = p6.parse_log(content, flow)
|
||||
result["window_observations"] = check_observations(content, 2, targets)
|
||||
if not any(r["bank_overlap"] for r in result["window_observations"]["concurrency"]):
|
||||
raise ValueError("no useful independent-bank overlap observed")
|
||||
return result
|
||||
|
||||
|
||||
def sources(root: Path, pdk: str) -> dict:
|
||||
result = b.source_inputs(root, pdk)
|
||||
names = subprocess.check_output(["git", "ls-files", "--cached", "--others", "--exclude-standard",
|
||||
"-z", "--", "tests", "configs"], cwd=root, text=True).split("\0")
|
||||
for name in [*names, "docs/ip/tiny-soc.md"]:
|
||||
path = root / name
|
||||
if name and path.is_file():
|
||||
result[name] = b.sha256(path)
|
||||
return result
|
||||
|
||||
|
||||
def make_command(args, stage: str) -> list[str]:
|
||||
command = ["make", "--no-print-directory", f"CONFIG=configs/ci/{args.pdk.lower()}-tiny.mk",
|
||||
"SOC=TINY", f"PDK={args.pdk}", "APP=ci_smoke", "SYNTH=NONE", "STA=NONE",
|
||||
f"BUILD_TIMESTAMP={args.timestamp}", f"JOBS={args.jobs}"]
|
||||
if stage in ("matched", "software"):
|
||||
command += ["SIMU=VERILATOR", "FIRMWARE_NAME=retrosoc_p10_replay"]
|
||||
elif stage in ("audio", "camera"):
|
||||
command += ["SIMU=VERILATOR", "HAVE_SVA=YES"]
|
||||
if stage == "camera":
|
||||
command.append("TINY_P10_ACCEPTANCE=YES")
|
||||
else:
|
||||
command += ["SIMU=IVERILOG"]
|
||||
return command
|
||||
|
||||
|
||||
def variant_for(root: Path, command: list[str]) -> Path:
|
||||
output = subprocess.check_output([*command, "config"], cwd=root, text=True)
|
||||
values = dict(line.split(None, 1) for line in output.splitlines() if line.strip())
|
||||
variant = Path(values["VARIANT_ROOT"]).resolve()
|
||||
if not variant.is_relative_to(root / "build"):
|
||||
raise ValueError("P10 evidence must be below the repository build root")
|
||||
return variant
|
||||
|
||||
|
||||
def location(args, stage: str) -> tuple[Path, Path]:
|
||||
variant = variant_for(args.root, make_command(args, stage))
|
||||
return variant, variant / "meta/tiny-r2-p10" / stage
|
||||
|
||||
|
||||
def capture_artifacts(paths: list[Path]) -> dict:
|
||||
return {str(p): b.artifact(p) for p in paths}
|
||||
|
||||
|
||||
def source_delta(record: dict, source: dict, allow_collector_update: bool = False) -> dict:
|
||||
original = record.get("source_inputs")
|
||||
if not original or not source or record.get("source_after") != original:
|
||||
raise ValueError("missing source identity or inputs changed during execution")
|
||||
changed = {name: {"before": original.get(name), "after": source.get(name)}
|
||||
for name in set(original) | set(source) if original.get(name) != source.get(name)}
|
||||
if changed and (not allow_collector_update or not set(changed) <= COLLECTOR_UPDATE_PATHS):
|
||||
raise ValueError("execution inputs changed beyond the explicit collector-only update")
|
||||
return changed
|
||||
|
||||
|
||||
def checked_record(path: Path, source: dict, *, allow_collector_update: bool = False) -> dict:
|
||||
record = b.read_json(path)
|
||||
source_delta(record, source, allow_collector_update)
|
||||
if (record.get("phase") != PHASE or record.get("status") != "passed" or
|
||||
record.get("stage") not in STAGES[:-1] or record.get("stage") != path.parent.name or
|
||||
record.get("pdk") not in ("ICS55", "IHP130") or record.get("maximum_attempts") != 1 or
|
||||
not record.get("artifacts") or not record.get("tools") or
|
||||
not record.get("started_at") or not record.get("finished_at")):
|
||||
raise ValueError(f"cannot resume incomplete or changed P10 record: {path}")
|
||||
captured = record["source"]
|
||||
if (b.read_json(b.check_artifact(captured["inputs"])) != record["source_inputs"] or
|
||||
captured["input_digest"] != b.digest_map(record["source_inputs"])):
|
||||
raise ValueError("source snapshot differs from stage identity")
|
||||
configuration = record["configuration"]["configuration"]
|
||||
if (configuration["SOC"] != "TINY" or configuration["PDK"] != record["pdk"] or
|
||||
configuration["EXT_CLK_HZ"] != "24000000" or configuration["SRAM_SIZE_KIB"] != "128"):
|
||||
raise ValueError("stage is not the selected Tiny SAFE24 profile")
|
||||
for artifact in record["artifacts"].values():
|
||||
b.check_artifact(artifact)
|
||||
for artifact in record["source"].values():
|
||||
if isinstance(artifact, dict) and "sha256" in artifact:
|
||||
b.check_artifact(artifact)
|
||||
for tool in record.get("tools", {}).values():
|
||||
b.check_artifact(tool["executable"])
|
||||
b.check_artifact(tool["archive"])
|
||||
for tool in record.get("host_tools", {}).values():
|
||||
b.check_artifact(tool)
|
||||
if record.get("stage") in ("audio", "camera"):
|
||||
stage = record["stage"]
|
||||
flow = b.read_json(path.parent / f"{stage}.json")
|
||||
content = (path.parent / f"{stage}.log").read_text()
|
||||
parser = camera_measurements if stage == "camera" else audio_measurements
|
||||
if parser(content, flow, record["target_count"]) != record["measurements"]:
|
||||
raise ValueError("stored measurements differ from retained execution")
|
||||
for case in record.get("cases", []):
|
||||
label = case["name"]
|
||||
flow = b.read_json(path.parent / f"{label}.json")
|
||||
content = (path.parent / f"{label}.log").read_text()
|
||||
if label == "compat-p1":
|
||||
measured = b.parse_log(content, flow)
|
||||
else:
|
||||
measured = p4.parse_log(content, flow)
|
||||
measured["fabric"] = rows(content, "R2_P5_FABRIC", p5.FABRIC_FIELDS)
|
||||
if measured != case["samples"]:
|
||||
raise ValueError("stored replay measurements differ from execution")
|
||||
b.check_artifact(case["image"])
|
||||
if record["stage"] in ("matched", "software"):
|
||||
expected = set(MATCHED) if record["pdk"] == "ICS55" else {"compat-p1"}
|
||||
if record["stage"] == "software":
|
||||
expected = {case[0] for case in p3.MATRIX} - set(MATCHED)
|
||||
names = [case["name"] for case in record["cases"]]
|
||||
if set(names) != expected or len(names) != len(expected):
|
||||
raise ValueError("missing or duplicate replay images")
|
||||
if record["stage"] == "backend":
|
||||
for label in ("firmware-lint", "synthesis", "netlist-boot", "netlist-dvp", "sta"):
|
||||
flow = b.read_json(path.parent / f"{label}.json")
|
||||
if flow.get("exit_code") != 0 or flow.get("status") != "passed":
|
||||
raise ValueError("backend gate failed")
|
||||
variant = path.parents[3]
|
||||
runtime = b.read_json(path.parent / "vvp-pre-simulation.json")
|
||||
for key in ("executable", "archive"):
|
||||
b.check_artifact(runtime["runtime"][key])
|
||||
for sub in ("sim/iverilog/netl", "sim/iverilog/netl/dvp"):
|
||||
directory = variant / sub
|
||||
flow = b.read_json(directory / "result-sim.json")
|
||||
require_terminal((directory / "sim.log").read_text(), flow)
|
||||
if (runtime["runtime"]["executable"]["path"] not in flow["command"] or
|
||||
runtime["captured_at"] > flow["started_at"]):
|
||||
raise ValueError("unbound or late-captured Icarus runtime")
|
||||
audit = b.read_json(variant / "syn/yosys/tiny-r2-p7-netlist.json")
|
||||
for artifact in audit["inputs"]:
|
||||
b.check_artifact(artifact)
|
||||
if (audit["status"] != "passed" or audit["pdk"] != record["pdk"] or
|
||||
audit["binding"]["main_macros"] != 32 or audit["binding"]["private_crypto_macros"] != 6 or
|
||||
audit["binding"]["pll_macros"] != int(record["pdk"] == "ICS55")):
|
||||
raise ValueError("macro binding audit failed")
|
||||
return record
|
||||
|
||||
|
||||
def run_make(args, directory: Path, label: str, command: list[str], *, observation=False) -> dict:
|
||||
if (directory / f"{label}.json").exists():
|
||||
raise ValueError(f"attempt already exists: {label}")
|
||||
print(f"{args.pdk}/{args.stage}: {label}", flush=True)
|
||||
record = {"command": command, "status": "running",
|
||||
"started_at": datetime.now(timezone.utc).isoformat()}
|
||||
with (directory / f"{label}.log").open("w") as log:
|
||||
process = subprocess.Popen(command, cwd=args.root, stdout=log, stderr=subprocess.STDOUT)
|
||||
record["pid"] = process.pid
|
||||
b.dump(directory / f"{label}.json", record)
|
||||
code = process.wait()
|
||||
warnings = self_owned_warnings(args.root, (directory / f"{label}.log").read_text())
|
||||
record.update(exit_code=code, self_owned_warnings=warnings,
|
||||
status="passed" if code == 0 and not warnings else "failed",
|
||||
finished_at=datetime.now(timezone.utc).isoformat())
|
||||
b.dump(directory / f"{label}.json", record)
|
||||
if not observation and record["status"] != "passed":
|
||||
raise ValueError(f"Make gate failed: {directory / (label + '.json')}")
|
||||
return record
|
||||
|
||||
|
||||
def simulate(args, directory: Path, label: str, model: Path, image: Path, flags: list[str],
|
||||
timeout: int, parse) -> tuple[dict, list[Path]]:
|
||||
paths = [model, image]
|
||||
identities = capture_artifacts(paths)
|
||||
command = ["timeout", "--foreground", "--kill-after=30s", f"{timeout}s", str(model),
|
||||
f"+firmware={image}", "+max_cycles=100000000", *flags]
|
||||
b.dump(directory / f"{label}-launch.json", {"command": command, "artifacts": identities,
|
||||
"maximum_attempts": 1})
|
||||
flow = b.execute(args.root, directory, label, command, cwd=args.root, quiet=True)
|
||||
content = (directory / f"{label}.log").read_text()
|
||||
require_terminal(content, flow)
|
||||
measurements = parse(content, flow)
|
||||
for artifact in identities.values():
|
||||
b.check_artifact(artifact)
|
||||
return measurements, [*paths, directory / f"{label}.log", directory / f"{label}.json",
|
||||
directory / f"{label}-launch.json"]
|
||||
|
||||
|
||||
def replay(args, variant: Path, directory: Path, record: dict) -> list[Path]:
|
||||
command = make_command(args, args.stage)
|
||||
model = variant / "sim/verilator/emu"
|
||||
artifacts = []
|
||||
if args.stage == "matched":
|
||||
if model.exists():
|
||||
raise ValueError("fresh replay model required")
|
||||
run_make(args, directory, "compile-model", [*command, "comp", "manifest"])
|
||||
else:
|
||||
_, previous = location(args, "matched")
|
||||
matched = checked_record(previous / "record.json", record["source_inputs"])
|
||||
artifacts += [previous / "record.json"]
|
||||
record["matched_stage"] = b.artifact(previous / "record.json")
|
||||
if matched["pdk"] != "ICS55":
|
||||
raise ValueError("compiler/placement comparison uses ICS55 only")
|
||||
configuration = b.read_json(variant / "meta/manifest.json")
|
||||
record["configuration"] = configuration
|
||||
runtime_before = b.runtime_inputs(args.root, variant, "verilator")
|
||||
record["runtime_inputs"] = runtime_before
|
||||
record["cases"] = []
|
||||
if args.pdk == "IHP130":
|
||||
reference = args.root / P1
|
||||
original = b.read_json(reference)["simulators"]["verilator"]
|
||||
b.validate_runs(original["runs"])
|
||||
image_path = b.check_artifact(original["image_manifest"])
|
||||
image = b.read_json(image_path)
|
||||
firmware = b.check_artifact(image["artifacts"]["tiny_baseline.hex"])
|
||||
b.retained_image(image_path.parent.parent.parent, firmware)
|
||||
first, end = image["descriptor_range"]
|
||||
flags = ["+tiny_r2_baseline", f"+baseline_tcd_base={first:x}", f"+baseline_tcd_end={end:x}"]
|
||||
samples, paths = simulate(args, directory, "compat-p1", model, firmware, flags, 10800, b.parse_log)
|
||||
record["cases"].append({"name": "compat-p1", "image": b.artifact(image_path),
|
||||
"samples": samples, "baseline_samples": original["runs"][0]["samples"]})
|
||||
artifacts += [reference, image_path, *paths]
|
||||
else:
|
||||
reference = args.root / (P5 if args.stage == "matched" else P3)
|
||||
report = b.read_json(reference)
|
||||
if args.stage == "matched":
|
||||
old = p5.validate_replay(b.check_artifact(report["executions"]["verilator"]), "verilator")
|
||||
cases = old["cases"]
|
||||
else:
|
||||
cases = p4.reference_cases(reference, [c["name"] for c in report["cases"]
|
||||
if c["name"] not in MATCHED])
|
||||
for case in cases:
|
||||
image_path = b.check_artifact(case["image"])
|
||||
image = p3.validate_image(image_path)
|
||||
p5.compatible_hardware(image["configuration"], configuration)
|
||||
first, end = image["layout"]["descriptor_range"]
|
||||
flags = ["+tiny_r2_p3", "+tiny_r2_p5", f"+baseline_tcd_base={first:x}",
|
||||
f"+baseline_tcd_end={end:x}"]
|
||||
samples, paths = simulate(args, directory, case["name"], model,
|
||||
b.check_artifact(image["artifacts"]["tiny_p3.hex"]), flags, 10800, p4.parse_log)
|
||||
fabric = rows((directory / f"{case['name']}.log").read_text(), "R2_P5_FABRIC", p5.FABRIC_FIELDS)
|
||||
if [r["window"] for r in fabric] != list(range(1, 9)):
|
||||
raise ValueError("missing replay fabric measurements")
|
||||
samples["fabric"] = fabric
|
||||
record["cases"].append({"name": case["name"], "image": b.artifact(image_path),
|
||||
"samples": samples, "baseline_samples": case["samples"]})
|
||||
artifacts += [image_path, *paths]
|
||||
b.dump(directory / "record.json", record)
|
||||
artifacts.append(reference)
|
||||
if runtime_before != b.runtime_inputs(args.root, variant, "verilator"):
|
||||
raise ValueError("replay model inputs changed")
|
||||
artifacts += [args.root / name for name in runtime_before]
|
||||
return artifacts
|
||||
|
||||
|
||||
def application(args, variant: Path, directory: Path, record: dict) -> list[Path]:
|
||||
command = make_command(args, args.stage)
|
||||
outputs = [variant / "sw/firmware", variant / "sw/retrosoc_fw.hex", variant / "sim/verilator/emu"]
|
||||
if any(p.exists() for p in outputs):
|
||||
raise ValueError("fresh application outputs required")
|
||||
run_make(args, directory, "build", [*command, "firmware", "comp", "manifest"])
|
||||
run_make(args, directory, "size", ["riscv32-unknown-elf-size", str(outputs[0])])
|
||||
run_make(args, directory, "symbols", ["riscv32-unknown-elf-nm", "-n", str(outputs[0])])
|
||||
size = (directory / "size.log").read_text().splitlines()[-1].split()
|
||||
symbols = (directory / "symbols.log").read_text()
|
||||
ebss = re.search(r"^([0-9a-f]+) [A-Za-z] _ebss$", symbols, re.M)
|
||||
if ebss is None or not 0x30000000 < int(ebss[1], 16) <= 0x30020000 - 4096:
|
||||
raise ValueError("application lacks 4 KiB flat-SRAM stack headroom")
|
||||
record["firmware_size"] = dict(zip(("text", "data", "bss"), map(int, size[:3]), strict=True))
|
||||
record["stack_headroom"] = 0x30020000 - int(ebss[1], 16)
|
||||
record["configuration"] = b.read_json(variant / "meta/manifest.json")
|
||||
runtime = b.runtime_inputs(args.root, variant, "verilator")
|
||||
targets = b.read_json(args.root / "rtl/tiny/integration/soc_topology.json")["fabric"]["target_count"]
|
||||
record["target_count"] = targets
|
||||
flags = ["+tiny_p10_audio"]
|
||||
parse = audio_measurements
|
||||
if args.stage == "camera":
|
||||
flags = ["+tiny_psram", "+tiny_p9_camera", "+tiny_p9_half_ns=500", "+tiny_p10_camera"]
|
||||
parse = camera_measurements
|
||||
samples, paths = simulate(args, directory, args.stage, outputs[2], outputs[1], flags,
|
||||
14400 if args.stage == "camera" else 10800,
|
||||
lambda log, flow: parse(log, flow, targets))
|
||||
record["measurements"] = samples
|
||||
record["runtime_inputs"] = runtime
|
||||
if runtime != b.runtime_inputs(args.root, variant, "verilator"):
|
||||
raise ValueError("application runtime inputs changed")
|
||||
return [*outputs, *paths, variant / "sw/retrosoc_fw.map", variant / "sw/retrosoc_fw.bin",
|
||||
variant / "sw/retrosoc_fw.txt", *[args.root / name for name in runtime]]
|
||||
|
||||
|
||||
def backend(args, variant: Path, directory: Path, record: dict) -> list[Path]:
|
||||
command = make_command(args, "backend")
|
||||
netlist = variant / "syn/yosys/out/retrosoc_tiny_asic_yosys.v"
|
||||
if netlist.exists():
|
||||
raise ValueError("fresh backend variant required")
|
||||
run_make(args, directory, "firmware-lint", [*command, "SIMU=VERILATOR", "firmware", "rtl-lint"])
|
||||
run_make(args, directory, "synthesis", [*command, "SYNTH=YOSYS", "YOSYS_TIMEOUT=10800", "synth"])
|
||||
runtime_identity = b.vvp_identity("vvp", record["tools"]["iverilog"])
|
||||
b.dump(directory / "vvp-pre-simulation.json", {
|
||||
"captured_at": datetime.now(timezone.utc).isoformat(), "runtime": runtime_identity})
|
||||
command += ["VVP=" + runtime_identity["executable"]["path"]]
|
||||
run_make(args, directory, "netlist-boot", [*command, "netsim-boot"])
|
||||
model = b.artifact(variant / "sim/iverilog/netl/simv")
|
||||
runtime = b.runtime_inputs(args.root, variant, "iverilog")
|
||||
run_make(args, directory, "netlist-dvp", [*command, "-o", "netcomp", "netsim-dvp"])
|
||||
b.check_artifact(model)
|
||||
if runtime != b.runtime_inputs(args.root, variant, "iverilog"):
|
||||
raise ValueError("netlist simulator inputs changed between workloads")
|
||||
record["runtime_inputs"] = runtime
|
||||
run_make(args, directory, "sta", [*command, "STA=OPENSTA", "sta"])
|
||||
record["observations"] = {}
|
||||
for name in ("check-warnings", "check-metrics"):
|
||||
record["observations"][name] = run_make(args, directory, name, [*command, name], observation=True)
|
||||
paths = [netlist, netlist.with_suffix(".config"), Path(model["path"])]
|
||||
for sub in ("sim/iverilog/netl", "sim/iverilog/netl/dvp"):
|
||||
base = variant / sub
|
||||
flow = b.read_json(base / "result-sim.json")
|
||||
require_terminal((base / "sim.log").read_text(), flow)
|
||||
paths += [base / name for name in ("sim.log", "result-sim.json", "result-sim-check.json")]
|
||||
paths += [variant / "syn/yosys/tiny-r2-p7-netlist.json", variant / "meta/metrics.json",
|
||||
variant / "meta/warnings.json", variant / "sta/opensta/retrosoc_core.sdc"]
|
||||
paths += list((variant / "sta/opensta").glob("*.rpt"))
|
||||
paths += list((variant / "syn/yosys").glob("*.rpt"))
|
||||
paths += [variant / "sw" / name for name in ("tiny_boot.elf", "tiny_boot.hex",
|
||||
"tiny_p9_netlist.elf", "tiny_p9_netlist.hex")]
|
||||
paths += [args.root / name for name in runtime]
|
||||
record["configuration"] = b.read_json(variant / "meta/manifest.json")
|
||||
record["metrics"] = b.read_json(variant / "meta/metrics.json")
|
||||
return paths
|
||||
|
||||
|
||||
def report_stage(args, directory: Path, source: dict) -> int:
|
||||
stages = ("matched", "software", "audio", "camera", "backend") if args.pdk == "ICS55" else (
|
||||
"matched", "audio", "camera", "backend")
|
||||
records, gaps = {}, []
|
||||
for stage in stages:
|
||||
selected = argparse.Namespace(**vars(args))
|
||||
if args.retained_timestamp and stage != "camera":
|
||||
selected.timestamp = args.retained_timestamp
|
||||
_, parent = location(selected, stage)
|
||||
try:
|
||||
record = checked_record(parent / "record.json", source,
|
||||
allow_collector_update=args.allow_collector_update)
|
||||
records[stage] = {"record": b.artifact(parent / "record.json"),
|
||||
"execution_source_digest": record["source"]["input_digest"],
|
||||
"validation_input_delta": source_delta(record, source, args.allow_collector_update),
|
||||
"cases": record.get("cases"), "measurements": record.get("measurements"),
|
||||
"metrics": record.get("metrics"), "observations": record.get("observations"),
|
||||
"firmware_size": record.get("firmware_size"),
|
||||
"stack_headroom": record.get("stack_headroom")}
|
||||
except (OSError, ValueError) as error:
|
||||
gaps.append({"stage": stage, "reason": str(error)})
|
||||
directory.mkdir(parents=True, exist_ok=True)
|
||||
comparisons = []
|
||||
if "matched" in records:
|
||||
for case in records["matched"]["cases"]:
|
||||
comparisons.append({"name": case["name"], "image": case["image"],
|
||||
"firmware": retained_footprint(case["image"]),
|
||||
"workloads": compare_samples(case["baseline_samples"], case["samples"]),
|
||||
"attribution": "whole platform revision; not an isolated CPU/fabric change"})
|
||||
software = []
|
||||
if "matched" in records and "software" in records:
|
||||
cases = {case["name"]: case for stage in ("matched", "software") for case in records[stage]["cases"]}
|
||||
pairs = [("isa", "compat-os-flat", "perf-os-plain-flat"),
|
||||
("optimizer", "perf-o2-plain-flat", "perf-o3-plain-flat"),
|
||||
("optimizer", "perf-o2-plain-flat", "perf-os-plain-flat"),
|
||||
("placement", "perf-o2-plain-flat", "perf-o2-plain-banked")]
|
||||
pairs += [("lto", f"perf-{opt}-plain-flat", f"perf-{opt}-lto-flat") for opt in ("o2", "o3", "os")]
|
||||
for kind, baseline, candidate in pairs:
|
||||
software.append({"kind": kind, "baseline": baseline, "candidate": candidate,
|
||||
"baseline_firmware": retained_footprint(cases[baseline]["image"]),
|
||||
"candidate_firmware": retained_footprint(cases[candidate]["image"]),
|
||||
"workloads": compare_samples(cases[baseline]["samples"], cases[candidate]["samples"])})
|
||||
quality = {}
|
||||
for timestamp in dict.fromkeys((args.retained_timestamp or args.timestamp, args.timestamp)):
|
||||
selected = argparse.Namespace(**vars(args))
|
||||
selected.timestamp = timestamp
|
||||
_, backend_directory = location(selected, "backend")
|
||||
for path in (backend_directory.parent / "validation").glob("*.json"):
|
||||
value = b.read_json(path)
|
||||
quality[timestamp + "/" + path.stem] = {
|
||||
"record": b.artifact(path), "status": value.get("status"), "exit_code": value.get("exit_code")}
|
||||
b.dump(directory / "report.json", {"phase": PHASE, "phase_accepted": False, "pdk": args.pdk,
|
||||
"status": "selected_gates_complete" if not gaps else "incomplete", "stages": records,
|
||||
"gaps": gaps, "deferred": DEFERRED, "matched_comparisons": comparisons,
|
||||
"software_comparisons": software, "quality_attempts": quality,
|
||||
"collector": b.artifact(Path(__file__)), "validation_source_digest": b.digest_map(source),
|
||||
"retained_timestamp": args.retained_timestamp,
|
||||
"qualification": "SAFE24 observations only; full regression and human acceptance remain open"})
|
||||
return int(bool(gaps))
|
||||
|
||||
|
||||
def retained_footprint(identity: dict) -> dict:
|
||||
image = b.read_json(b.check_artifact(identity))
|
||||
log = b.check_artifact(image["artifacts"]["size.log"]).read_text()
|
||||
sections = {name: int(size) for name, size in re.findall(r"^(\.[\w.]+)\s+(\d+)\s+\d+\s*$", log, re.M)}
|
||||
if ".text" not in sections or ".bss" not in sections:
|
||||
raise ValueError("retained image lacks section-size evidence")
|
||||
return {"sections": sections, "layout": image.get("layout"),
|
||||
"stack_headroom": image.get("stack_headroom", image.get("layout", {}).get("stack_headroom")),
|
||||
"size_log": image["artifacts"]["size.log"]}
|
||||
|
||||
|
||||
def compare_samples(baseline: dict, candidate: dict) -> list[dict]:
|
||||
old = baseline.get("cases", baseline.get("case", []))
|
||||
new = candidate.get("cases", candidate.get("case", []))
|
||||
if not old or len(old) != len(new):
|
||||
raise ValueError("comparison workload mismatch")
|
||||
result = []
|
||||
for a, z in zip(old, new, strict=True):
|
||||
if any(a[key] != z[key] for key in ("name", "payload_bytes", "checksum", "kernel_calls")):
|
||||
raise ValueError("comparison is not identical work")
|
||||
result.append({"name": a["name"], "payload_bytes": a["payload_bytes"],
|
||||
"baseline_cycles": a["cycles"], "candidate_cycles": z["cycles"],
|
||||
"baseline_instructions": a["instructions"], "candidate_instructions": z["instructions"],
|
||||
"baseline_cpi": a["cycles"] / a["instructions"],
|
||||
"candidate_cpi": z["cycles"] / z["instructions"],
|
||||
"cycle_ratio": a["cycles"] / z["cycles"]})
|
||||
return result
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("--root", type=Path, default=ROOT)
|
||||
parser.add_argument("--pdk", choices=("ICS55", "IHP130"), required=True)
|
||||
parser.add_argument("--timestamp", required=True)
|
||||
parser.add_argument("--jobs", type=int, default=4)
|
||||
parser.add_argument("--stage", choices=STAGES, required=True)
|
||||
parser.add_argument("--dry-run", action="store_true")
|
||||
parser.add_argument("--resume", action="store_true")
|
||||
parser.add_argument("--retained-timestamp", help="report-only: preserve completed stages from this campaign")
|
||||
parser.add_argument("--allow-collector-update", action="store_true",
|
||||
help="report-only: admit explicitly listed collector/test changes, never DUT changes")
|
||||
args = parser.parse_args()
|
||||
args.root = args.root.resolve()
|
||||
if (not re.fullmatch(r"\d{4}-\d{2}-\d{2}-\d{2}-\d{2}", args.timestamp) or args.jobs < 1 or
|
||||
(args.pdk == "IHP130" and args.stage == "software")):
|
||||
parser.error("invalid timestamp/jobs or unsupported IHP130 software matrix")
|
||||
if (args.retained_timestamp or args.allow_collector_update) and args.stage != "report":
|
||||
parser.error("retained campaign and collector update are report-only")
|
||||
if args.retained_timestamp and not re.fullmatch(r"\d{4}-\d{2}-\d{2}-\d{2}-\d{2}", args.retained_timestamp):
|
||||
parser.error("invalid retained timestamp")
|
||||
variant, directory = location(args, args.stage)
|
||||
if args.dry_run:
|
||||
selections = list(MATCHED) if args.stage == "matched" and args.pdk == "ICS55" else (
|
||||
[case[0] for case in p3.MATRIX if case[0] not in MATCHED] if args.stage == "software" else
|
||||
["compat-p1"] if args.stage == "matched" else [args.stage])
|
||||
print(json.dumps({"stage": args.stage, "pdk": args.pdk, "variant": str(variant),
|
||||
"make": make_command(args, args.stage), "evidence": str(directory),
|
||||
"case_names": selections, "maximum_attempts": 1,
|
||||
"max_cycles": 100000000,
|
||||
"wall_seconds": 14400 if args.stage == "camera" else 10800,
|
||||
"references": {"IHP130": P1, "ICS55": P5, "software": P3},
|
||||
"deferred": DEFERRED}, indent=2))
|
||||
return 0
|
||||
source = sources(args.root, args.pdk)
|
||||
if args.stage == "report":
|
||||
return report_stage(args, directory, source)
|
||||
if directory.exists():
|
||||
if args.resume:
|
||||
checked_record(directory / "record.json", source)
|
||||
print(f"Reused verified completed stage: {directory}")
|
||||
return 0
|
||||
raise ValueError("evidence exists; use --resume for an unchanged successful stage or a fresh timestamp")
|
||||
directory.mkdir(parents=True, exist_ok=False)
|
||||
record = {"phase": PHASE, "phase_accepted": False, "stage": args.stage, "pdk": args.pdk,
|
||||
"source_inputs": source, "status": "running", "pid": os.getpid(),
|
||||
"started_at": datetime.now(timezone.utc).isoformat(), "maximum_attempts": 1}
|
||||
b.dump(directory / "record.json", record)
|
||||
print(f"P10 {args.pdk}/{args.stage}: {directory}", flush=True)
|
||||
try:
|
||||
record["tools"] = b.tools_identity(args.root)
|
||||
record["host_tools"] = {name: b.artifact(Path(shutil.which(name)).resolve()) for name in
|
||||
("make", "cc", "c++", "timeout")}
|
||||
record["source"] = b.snapshot(args.root, directory / "source", source)
|
||||
b.dump(directory / "record.json", record)
|
||||
if args.stage in ("matched", "software"):
|
||||
paths = replay(args, variant, directory, record)
|
||||
elif args.stage in ("audio", "camera"):
|
||||
paths = application(args, variant, directory, record)
|
||||
else:
|
||||
paths = backend(args, variant, directory, record)
|
||||
after = sources(args.root, args.pdk)
|
||||
if source != after:
|
||||
raise ValueError("source inputs changed during P10 execution")
|
||||
record["source_after"] = after
|
||||
paths += [p for p in directory.iterdir() if p.is_file() and p.name != "record.json"]
|
||||
record["artifacts"] = capture_artifacts(paths)
|
||||
record["status"] = "passed"
|
||||
except (OSError, ValueError, subprocess.SubprocessError) as error:
|
||||
record.update(status="failed_or_incomplete", reason=str(error))
|
||||
record["finished_at"] = datetime.now(timezone.utc).isoformat()
|
||||
b.dump(directory / "record.json", record)
|
||||
print(f"P10 {args.pdk}/{args.stage}: {record['status']}", flush=True)
|
||||
return 0 if record["status"] == "passed" else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -44,6 +44,7 @@
|
||||
|
||||
#include "../../crt/src/hal/ga2d_math.h"
|
||||
#include "../../app/apps/ci_smoke/p9_frame.h"
|
||||
#include "../../app/apps/ci_smoke/p10_frame.h"
|
||||
#include "../../crt/src/hal/opipsram_math.c"
|
||||
|
||||
#define TEST_STORAGE_ADDRESS ((uintptr_t)0x1000U)
|
||||
@@ -3472,6 +3473,31 @@ static int test_p9_frame_layout(void) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int test_p10_frame_statistics(void) {
|
||||
rs_p10_statistics_t statistics = {.checksum = UINT32_C(2166136261)};
|
||||
uint32_t offset = UINT32_MAX;
|
||||
uint32_t block = UINT32_MAX;
|
||||
if (rs_p10_accumulate(&statistics, UINT32_C(0xF8000000)) != RS_OK || statistics.pixels != 2U ||
|
||||
statistics.red[0] != 1U || statistics.red[31] != 1U ||
|
||||
rs_p10_accumulate(&statistics, 0U) != RS_OK || statistics.red[0] != 3U) {
|
||||
return 1;
|
||||
}
|
||||
statistics.red[0] = UINT32_MAX - 1U;
|
||||
if (rs_p10_accumulate(&statistics, 0U) != RS_EINVAL || statistics.red[0] != UINT32_MAX - 1U ||
|
||||
rs_p10_accumulate(NULL, 0U) != RS_EINVAL) {
|
||||
return 2;
|
||||
}
|
||||
if (rs_p10_chunk(153600U, 149U, 316U, &offset, &block) != RS_OK || offset != 38144U ||
|
||||
block != 314U || rs_p10_chunk(153600U, 150U, 1024U, &offset, &block) != RS_EINVAL ||
|
||||
rs_p10_chunk(153600U, 0U, 315U, &offset, &block) != RS_EINVAL ||
|
||||
rs_p10_chunk(UINT32_MAX, 0U, UINT32_MAX, &offset, &block) != RS_EINVAL ||
|
||||
rs_p10_chunk(512U, 0U, 1024U, &offset, &block) != RS_EINVAL ||
|
||||
rs_p10_chunk(0U, 0U, 1024U, &offset, &block) != RS_EINVAL) {
|
||||
return 3;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int main(void) {
|
||||
const int results[] = {
|
||||
test_string_helpers(),
|
||||
@@ -3501,6 +3527,7 @@ int main(void) {
|
||||
test_tiny_dma_config_validation(),
|
||||
test_dvp_hal(),
|
||||
test_p9_frame_layout(),
|
||||
test_p10_frame_statistics(),
|
||||
test_opipsram_helpers(),
|
||||
test_user_ip_validation(),
|
||||
test_extension_validation(),
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
module tiny_p10_observer_tb;
|
||||
logic clk = 0;
|
||||
always #5 clk = !clk;
|
||||
logic reset_n = 0, enable = 0;
|
||||
logic [2:0] ar = 0, r = 0, aw = 0, b = 0, errors = 0;
|
||||
logic [2:0][7:0] read_bytes = 0, write_bytes = 0;
|
||||
logic [2:0][4:0] stalled = 0;
|
||||
tiny_p10_observer u_observer (
|
||||
.clk_i (clk),
|
||||
.rst_n_i (reset_n),
|
||||
.enable_i (enable),
|
||||
.read_accept_i (ar),
|
||||
.read_done_i (r),
|
||||
.write_accept_i (aw),
|
||||
.write_done_i (b),
|
||||
.error_i (errors),
|
||||
.read_bytes_i (read_bytes),
|
||||
.write_bytes_i (write_bytes),
|
||||
.stalled_i (stalled),
|
||||
.active_targets_i (6'd3),
|
||||
.bank_issues_i (4'b0101),
|
||||
.dma_beat_overlap_i(read_bytes[1] != 0 && write_bytes[1] != 0)
|
||||
);
|
||||
initial begin
|
||||
repeat (2) @(negedge clk);
|
||||
reset_n = 1;
|
||||
ar[0] = 1;
|
||||
@(negedge clk);
|
||||
ar = 0;
|
||||
enable = 1;
|
||||
aw[1] = 1;
|
||||
write_bytes[1] = 4;
|
||||
stalled[0][0] = 1;
|
||||
@(negedge clk);
|
||||
aw = 0;
|
||||
write_bytes = 0;
|
||||
r[0] = 1;
|
||||
read_bytes[0] = 4;
|
||||
@(negedge clk);
|
||||
r = 0;
|
||||
read_bytes = 0;
|
||||
stalled = 0;
|
||||
b[1] = 1;
|
||||
@(negedge clk);
|
||||
b = 0;
|
||||
enable = 0;
|
||||
@(negedge clk);
|
||||
if ($test$plusargs("orphan")) begin
|
||||
r[2] = 1;
|
||||
@(negedge clk);
|
||||
end
|
||||
enable = 1;
|
||||
ar[2] = 1;
|
||||
@(negedge clk);
|
||||
ar = 0;
|
||||
enable = 0;
|
||||
@(negedge clk);
|
||||
r[2] = 1;
|
||||
@(negedge clk);
|
||||
$display("SIM_TEST_PASS Tiny cycles=9");
|
||||
$finish;
|
||||
end
|
||||
endmodule
|
||||
@@ -0,0 +1,179 @@
|
||||
"""P10 single-run acceptance, observer conservation and configuration isolation."""
|
||||
from __future__ import annotations
|
||||
|
||||
from copy import deepcopy
|
||||
from pathlib import Path
|
||||
import os
|
||||
import subprocess
|
||||
|
||||
import pytest
|
||||
|
||||
from scripts import tiny_r2_p10 as p10
|
||||
from scripts.check_simulation import DEFAULT_FAILURE
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def configuration(*extra):
|
||||
return subprocess.run(["make", "--no-print-directory", "CONFIG=configs/ci/ics55-tiny.mk",
|
||||
"APP=ci_smoke", "BUILD_TIMESTAMP=2026-10-17-00-00", *extra, "config"],
|
||||
cwd=ROOT, text=True, capture_output=True, check=False)
|
||||
|
||||
|
||||
def test_acceptance_configuration_is_separate():
|
||||
baseline = configuration()
|
||||
selected = configuration("TINY_P10_ACCEPTANCE=YES")
|
||||
assert baseline.returncode == selected.returncode == 0
|
||||
assert baseline.stdout != selected.stdout
|
||||
base_values = dict(line.split(None, 1) for line in baseline.stdout.splitlines() if line.strip())
|
||||
new_values = dict(line.split(None, 1) for line in selected.stdout.splitlines() if line.strip())
|
||||
assert base_values["VARIANT_ROOT"] != new_values["VARIANT_ROOT"]
|
||||
for extra in ("TINY_P7_ACCEPTANCE=YES", "TINY_P8_ACCEPTANCE=YES", "TINY_P9_ACCEPTANCE=YES",
|
||||
"APP=bringup", "SOC=MINI", "TINY_P9_QUICK=YES", "TINY_P10_ACCEPTANCE=maybe"):
|
||||
assert configuration("TINY_P10_ACCEPTANCE=YES", extra).returncode != 0
|
||||
|
||||
|
||||
@pytest.mark.parametrize("simulator", ["iverilog", "verilator"])
|
||||
def test_observer_preserves_boundary_transactions(tmp_path, simulator):
|
||||
sources = [ROOT / "rtl/tiny/dv/tiny_p10_observer.sv", ROOT / "tests/rtl/tiny_p10_observer_tb.sv"]
|
||||
model = tmp_path / "sim"
|
||||
if simulator == "iverilog":
|
||||
compile_command = ["iverilog", "-g2012", "-s", "tiny_p10_observer_tb", "-o", str(model)]
|
||||
launch = ["vvp", str(model)]
|
||||
else:
|
||||
compile_command = ["verilator", "--binary", "--timing", "--assert", "-Wno-fatal", "-j", "4",
|
||||
"--top-module", "tiny_p10_observer_tb", "--Mdir", str(tmp_path / "obj"),
|
||||
"-o", str(model)]
|
||||
launch = [str(model)]
|
||||
compiled = subprocess.run([*compile_command, *map(str, sources)], capture_output=True, text=True,
|
||||
timeout=120, check=False, env={**os.environ, "CCACHE_DISABLE": "1"})
|
||||
assert compiled.returncode == 0, compiled.stdout + compiled.stderr
|
||||
good = subprocess.run(launch, capture_output=True, text=True, timeout=30, check=False)
|
||||
content = good.stdout + good.stderr
|
||||
p10.require_terminal(content, {"exit_code": good.returncode, "status": "passed"})
|
||||
result = p10.check_observations(content, 2, 28)
|
||||
first, writer, _, _, _, last = result["bus"]
|
||||
assert (first["reads"], first["read_initial"], first["read_done"], first["read_pending"]) == (0, 1, 1, 0)
|
||||
assert first["read_bytes"] == 4 and first["ar_max"] == 2 and first["read_latency_max"] == 2
|
||||
assert writer["writes"] == writer["write_done"] == 1 and writer["write_bytes"] == 4
|
||||
assert last["reads"] == last["read_pending"] == 1 and last["read_done"] == 0
|
||||
assert result["concurrency"][0]["bank_overlap"] == 3
|
||||
bad = subprocess.run([*launch, "+orphan"], capture_output=True, text=True, timeout=30, check=False)
|
||||
assert DEFAULT_FAILURE.search(bad.stdout + bad.stderr)
|
||||
|
||||
|
||||
def camera_log():
|
||||
histogram, checksum = p10.golden()
|
||||
content = [f"P10_RESULT code=0 pixels=76800 bytes=153600 sectors=300 checksum={checksum}"]
|
||||
content += [f"P10_HIST bin={index} pixels={value}" for index, value in enumerate(histogram)]
|
||||
content += [f"P10_PHASE id={index} calls={1 if index < 2 else 150} bytes=153600 cycles=100 instructions=50"
|
||||
for index in range(5)]
|
||||
for window in range(1, 453):
|
||||
for owner in range(3):
|
||||
row = dict.fromkeys(p10.BUS_FIELDS, 0)
|
||||
row.update(window=window, owner=owner)
|
||||
if window == 1 and owner == 1:
|
||||
row["write_bytes"] = 153600
|
||||
row["writes"] = row["write_done"] = 9600
|
||||
if window >= 4 and (window - 4) % 3 == 0 and owner == 2:
|
||||
row["read_bytes"], row["write_bytes"] = 1040, 4
|
||||
row["reads"] = row["read_done"] = 17
|
||||
row["writes"] = row["write_done"] = 1
|
||||
if window > 2 and (window - 2) % 3 == 0 and owner == 2:
|
||||
row["read_bytes"], row["write_bytes"] = 16, 1028
|
||||
row["reads"] = row["read_done"] = 1
|
||||
row["writes"] = row["write_done"] = 17
|
||||
content.append("P10_BUS " + " ".join(f"{k}={v}" for k, v in sorted(row.items())))
|
||||
content.append(f"P10_CONCURRENCY window={window} active_targets_max=2 bank_overlap=1 dma_beat_overlap=0")
|
||||
content += ["P9_OBSERVATION version=1 accepted_words=38400 delivered_words=38400 "
|
||||
"fifo_high_water=4 backpressure=308 pixel_half_ns=500", "SIM_TEST_PASS Tiny cycles=100"]
|
||||
return "\n".join(content)
|
||||
|
||||
|
||||
def test_camera_oracle_and_complete_pipeline():
|
||||
histogram, checksum = p10.golden()
|
||||
assert sum(histogram) == 76800 and checksum == 2323003269
|
||||
assert histogram[:10] == [2368] * 10 and histogram[-1] == 2371
|
||||
result = p10.camera_measurements(camera_log(), {"exit_code": 0, "status": "passed"}, 28)
|
||||
assert result["phases"][4]["calls"] == 150
|
||||
assert result["sdio_bytes"]["observed_write"] == 154800
|
||||
assert result["sdio_bytes"]["observed_read"] == 158400
|
||||
|
||||
|
||||
@pytest.mark.parametrize("old,new", [
|
||||
("code=0", "code=1"), ("sectors=300", "sectors=299"),
|
||||
("bin=31 pixels=2371", "bin=31 pixels=2370"), ("calls=150", "calls=149"),
|
||||
("read_pending=0", "read_pending=1"), ("errors=0", "errors=1"),
|
||||
("active_targets_max=2", "active_targets_max=29"),
|
||||
("fifo_high_water=4", "fifo_high_water=128"), ("backpressure=308", "backpressure=11904"),
|
||||
("pixel_half_ns=500", "pixel_half_ns=250"), ("SIM_TEST_PASS", "SIM_TEST_FAIL"),
|
||||
("write_bytes=1028", "write_bytes=1024"), ("read_bytes=1040", "read_bytes=1024"),
|
||||
("write_bytes=1028", "write_bytes=1027"), ("read_bytes=1040", "read_bytes=1044"),
|
||||
])
|
||||
def test_pipeline_rejects_incomplete_or_corrupt_evidence(old, new):
|
||||
with pytest.raises(ValueError):
|
||||
p10.camera_measurements(camera_log().replace(old, new, 1), {"exit_code": 0, "status": "passed"}, 28)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("flow", [{"exit_code": 1, "status": "passed"},
|
||||
{"exit_code": 0, "status": "failed"}])
|
||||
def test_terminal_text_never_overrides_command_failure(flow):
|
||||
with pytest.raises(ValueError):
|
||||
p10.require_terminal("SIM_TEST_PASS Tiny cycles=100\n", flow)
|
||||
|
||||
|
||||
def test_resume_rejects_missing_or_changed_source(tmp_path):
|
||||
record = {"phase": p10.PHASE, "status": "passed", "source_inputs": {"a": "one"},
|
||||
"source_after": {"a": "one"}, "artifacts": {}, "source": {}}
|
||||
path = tmp_path / "record.json"
|
||||
p10.b.dump(path, record)
|
||||
with pytest.raises(ValueError):
|
||||
p10.checked_record(path, {"a": "two"})
|
||||
failed = deepcopy(record)
|
||||
failed["status"] = "running"
|
||||
p10.b.dump(path, failed)
|
||||
with pytest.raises(ValueError):
|
||||
p10.checked_record(path, {"a": "one"})
|
||||
|
||||
|
||||
def test_matched_comparison_rejects_different_work():
|
||||
row = dict(name="memory", payload_bytes=16384, checksum=7, kernel_calls=0,
|
||||
cycles=200, instructions=100)
|
||||
other = {**row, "cycles": 150}
|
||||
compared = p10.compare_samples({"cases": [row]}, {"cases": [other]})
|
||||
assert compared[0]["cycle_ratio"] == 200 / 150
|
||||
other["payload_bytes"] = 65536
|
||||
with pytest.raises(ValueError):
|
||||
p10.compare_samples({"cases": [row]}, {"cases": [other]})
|
||||
|
||||
|
||||
def test_collector_update_preserves_original_source_identity():
|
||||
source = {"scripts/tiny_r2_p10.py": "old", "rtl/tiny/top/retrosoc_tiny.sv": "rtl"}
|
||||
record = {"source_inputs": deepcopy(source), "source_after": deepcopy(source)}
|
||||
current = {**source, "scripts/tiny_r2_p10.py": "new"}
|
||||
with pytest.raises(ValueError):
|
||||
p10.source_delta(record, current)
|
||||
assert p10.source_delta(record, current, True) == {
|
||||
"scripts/tiny_r2_p10.py": {"before": "old", "after": "new"}}
|
||||
assert record["source_inputs"] == record["source_after"] == source
|
||||
for path in ("rtl/tiny/top/retrosoc_tiny.sv", "app/apps/ci_smoke/tiny_r2_p10.c",
|
||||
"Makefile", "dependencies/dependencies.lock.json", "tests/unreviewed.py"):
|
||||
with pytest.raises(ValueError):
|
||||
p10.source_delta(record, {**current, path: "changed"}, True)
|
||||
record["source_after"] = current
|
||||
with pytest.raises(ValueError):
|
||||
p10.source_delta(record, current, True)
|
||||
|
||||
|
||||
def test_retained_section_sizes_are_not_elf_file_size(tmp_path):
|
||||
log = tmp_path / "size.log"
|
||||
log.write_text("firmware:\nsection size addr\n.init 204 0\n.text 9936 805306368\n"
|
||||
".bss 23116 805316352\n.comment 15 0\nTotal 33271\n")
|
||||
image = tmp_path / "image.json"
|
||||
p10.b.dump(image, {"artifacts": {"size.log": p10.b.artifact(log)},
|
||||
"layout": {"stack_headroom": 4096}})
|
||||
result = p10.retained_footprint(p10.b.artifact(image))
|
||||
assert result["sections"][".text"] == 9936 and result["stack_headroom"] == 4096
|
||||
log.write_text("modified")
|
||||
with pytest.raises(ValueError):
|
||||
p10.retained_footprint(p10.b.artifact(image))
|
||||
Reference in new issue
Block a user