diff --git a/Makefile b/Makefile index 351c064c..395c0dae 100644 --- a/Makefile +++ b/Makefile @@ -81,6 +81,10 @@ HAVE_CSR ?= NO FIRMWARE_NAME ?= retrosoc_fw APP ?= shell LINK_TYPE ?= ld2_sram +SW_ISA_PROFILE ?= COMPAT +SW_OPT ?= APP +SW_LTO ?= NO +WS2812_P3_ACCEPTANCE ?= NO COREMARK_MODE ?= quick NPU_P5_ACCEPTANCE ?= NO NPU_P6_ACCEPTANCE ?= NO @@ -116,6 +120,13 @@ CONFIG_KEY_VARS := SOC MINI_MODE PDK HAVE_PLL HAVE_SRAM_IF HAVE_SRAM_MACRO SR HAVE_HP HP_CONFIG BUILD_RELEASE JTAG_IDCODE EXT_CLK_HZ AUD_CLK_HZ CLINT_TIMEBASE_HZ MGMT_CPU_CLK_HZ \ ISA HAVE_CSR APP LINK_TYPE COREMARK_MODE RTL_TOP FIRMWARE_NAME CONFIG_KEY_VARS += NPU_P5_ACCEPTANCE NPU_P6_ACCEPTANCE NPU_P6_WORKLOAD +# Preserve existing baseline keys; experimental software inputs have distinct variants. +ifneq ($(SW_ISA_PROFILE):$(SW_OPT):$(SW_LTO),COMPAT:APP:NO) +CONFIG_KEY_VARS += SW_ISA_PROFILE SW_OPT SW_LTO +endif +ifeq ($(WS2812_P3_ACCEPTANCE),YES) +CONFIG_KEY_VARS += WS2812_P3_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)) @@ -235,7 +246,7 @@ VALID_BOOL := YES NO VALID_HP_CONFIG := rv64imafdc_zicbom_max VALID_ISA := RV32E RV32I RV32IM VALID_APP := benchmark bringup ci_smoke coremark debug hp_boot shell xpi_flash_loader apu_release -VALID_LINK_TYPE := xip jtag_sram ld2_all_sram ld2_sram ld2_psram ld2_sdram +VALID_LINK_TYPE := xip jtag_sram ld2_all_sram ld2_sram ld2_psram ld2_sdram ld2_tiny_banked VALID_COREMARK_MODE := quick standard VALID_SRAM_SIZE_KIB := 4 16 32 64 128 @@ -272,6 +283,15 @@ $(call validate_value,COREMARK_MODE,$(VALID_COREMARK_MODE)) $(call validate_value,NPU_P5_ACCEPTANCE,$(VALID_BOOL)) $(call validate_value,NPU_P6_ACCEPTANCE,$(VALID_BOOL)) $(call validate_value,NPU_P6_WORKLOAD,kws vww) +$(call validate_value,SW_ISA_PROFILE,COMPAT TINY_PERF) +$(call validate_value,SW_OPT,APP O2 O3 Os) +$(call validate_value,SW_LTO,$(VALID_BOOL)) +$(call validate_value,WS2812_P3_ACCEPTANCE,$(VALID_BOOL)) +ifeq ($(WS2812_P3_ACCEPTANCE),YES) +ifneq ($(SOC):$(APP):$(HAVE_CSR),MINI:ci_smoke:YES) +$(error WS2812_P3_ACCEPTANCE requires SOC=MINI APP=ci_smoke HAVE_CSR=YES) +endif +endif ifeq ($(NPU_P5_ACCEPTANCE),YES) ifeq ($(filter $(APP),ci_smoke hp_boot),) diff --git a/app/apps/ci_smoke/app.mk b/app/apps/ci_smoke/app.mk index 43ffbb00..0db5f6e3 100644 --- a/app/apps/ci_smoke/app.mk +++ b/app/apps/ci_smoke/app.mk @@ -1,4 +1,7 @@ APP_SRCS += $(ROOT_PATH)/app/apps/ci_smoke/main.c +ifeq ($(WS2812_P3_ACCEPTANCE),YES) +APP_SRCS := $(ROOT_PATH)/app/apps/ci_smoke/ws2812_r2.c +endif # GA2D Phase 6 acceptance cases (bounded-wait and DMA contention evidence) # outgrew the 32 KiB SRAM image at the global -O3; build this test app -Os. APP_CFLAGS += -Os @@ -8,4 +11,4 @@ APP_SRCS += $(NPU_P5_KWS_C) APP_SRCS += $(NPU_P5_ACCEPTANCE_C) APP_INC_DIRS += $(NPU_P5_KWS_DIR) APP_INC_DIRS += $(NPU_P5_ACCEPTANCE_DIR) -endif \ No newline at end of file +endif diff --git a/app/apps/ci_smoke/mcu_acceptance.S b/app/apps/ci_smoke/mcu_acceptance.S index 2da87716..e48bfe8c 100644 --- a/app/apps/ci_smoke/mcu_acceptance.S +++ b/app/apps/ci_smoke/mcu_acceptance.S @@ -26,6 +26,11 @@ rs_mcu_atomic_probe: .type rs_mcu_unmapped_probe,@function rs_mcu_unmapped_probe: li a0, 0x38000000 + /* The acceptance trap handler advances MEPC by four bytes. Keep this + * faulting instruction 32-bit even in the separate compressed ISA build. */ + .option push + .option norvc lw a0, 0(a0) + .option pop ret .size rs_mcu_unmapped_probe, .-rs_mcu_unmapped_probe diff --git a/app/apps/ci_smoke/tiny.c b/app/apps/ci_smoke/tiny.c index 213975fb..df318bc1 100644 --- a/app/apps/ci_smoke/tiny.c +++ b/app/apps/ci_smoke/tiny.c @@ -222,7 +222,7 @@ static bool rs_mcu_rtc(void) { .enable = true, }; const rs_rtc_time_t value = {.seconds = UINT64_C(1234), .subsecond = 0U}; - rs_rtc_time_t observed; + rs_rtc_time_t observed = {0}; return (rs_rtc_probe() == RS_OK) && (rs_rtc_configure(&config, RS_TIMEOUT_DEFAULT) == RS_OK) && (rs_rtc_set_time(&value, RS_TIMEOUT_DEFAULT) == RS_OK) && @@ -234,7 +234,7 @@ int main(void) { rs_archinfo_t info; rs_onchip_sram_info_t memory; rs_sysctrl_fault_status_t fault; - rs_watchdog_status_t watchdog; + rs_watchdog_status_t watchdog = {0}; uint64_t before; uint64_t after; const rs_watchdog_config_t watchdog_config = { diff --git a/app/apps/ci_smoke/tiny_r2_p3.c b/app/apps/ci_smoke/tiny_r2_p3.c new file mode 100644 index 00000000..1c1a3312 --- /dev/null +++ b/app/apps/ci_smoke/tiny_r2_p3.c @@ -0,0 +1,348 @@ +/* Copyright (c) 2026 Yuchi Miao; SPDX-License-Identifier: MulanPSL-2.0 */ +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +/* P3 compact experiment: four retained destinations fit B3. This is not the + * original P1 matched binary. Verification remains outside measured windows. */ +#define RS_P3_WORDS 1024U +#define RS_P3_JOBS 4U +#define RS_P3_GUARDS 16U +#define RS_P3_CASES 8U +#define RS_P3_ITERATIONS 4096U +#define RS_P3_SEED UINT32_C(0x12345678) +#define RS_P3_EXPECTED UINT32_C(0xf6e37410) +#define RS_P3_PATTERN UINT32_C(0xa519c300) +#define RS_P3_GUARD UINT32_C(0x5a36c9e7) + +typedef struct { + uint32_t before[RS_P3_GUARDS]; + uint32_t words[RS_P3_WORDS]; + uint32_t after[RS_P3_GUARDS]; +} rs_p3_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_p3_result_t; + +static _Alignas(64) volatile rs_p3_buffer_t rs_p3_source + __attribute__((section(".bss.rs_dma.buffers"))); +static _Alignas(64) volatile rs_p3_buffer_t rs_p3_destinations[RS_P3_JOBS] + __attribute__((section(".bss.rs_dma.buffers"))); +static _Alignas(64) rs_dma_tcd_t rs_p3_tcds[RS_P3_JOBS][4] + __attribute__((section(".bss.rs_dma.tcd"))); +static volatile uint32_t rs_p3_dma_cookie __attribute__((section(".data.rs_dma"))) = + UINT32_C(0x7135a9c2); +static rs_dma_session_t rs_p3_owner; +uint32_t rs_p3_isa_check(void); +extern uint32_t _stack_point[]; +extern uint32_t _ebss[]; +static uintptr_t rs_p3_stack_floor; +static uintptr_t rs_p3_stack_painted_end; +static rs_p3_result_t rs_p3_results[RS_P3_CASES]; +static volatile uint32_t rs_p3_seed = RS_P3_SEED; + +static uint64_t rs_p3_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_p3_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_p3_kernel(uint32_t value) { + for (uint32_t index = 0U; index < RS_P3_ITERATIONS; ++index) { + value ^= value << 13U; + value ^= value >> 17U; + value ^= value << 5U; + } + return value; +} + +static bool rs_p3_cpu(rs_p3_result_t *result) { + const uint32_t seed = rs_p3_seed; + const uint64_t begin = rs_p3_cycles(); + const uint32_t value = rs_p3_kernel(seed); + __COMPILER_BARRIER(); + result->kernel_cycles += rs_p3_cycles() - begin; + result->checksum = value; + ++result->kernel_calls; + return value == RS_P3_EXPECTED; +} + +static void rs_p3_initialize(void) { + for (uint32_t index = 0U; index < RS_P3_GUARDS; ++index) { + rs_p3_source.before[index] = RS_P3_GUARD; + rs_p3_source.after[index] = RS_P3_GUARD; + } + for (uint32_t index = 0U; index < RS_P3_WORDS; ++index) { + rs_p3_source.words[index] = RS_P3_PATTERN ^ index; + } + for (uint32_t job = 0U; job < RS_P3_JOBS; ++job) { + for (uint32_t index = 0U; index < RS_P3_GUARDS; ++index) { + rs_p3_destinations[job].before[index] = RS_P3_GUARD; + rs_p3_destinations[job].after[index] = RS_P3_GUARD; + } + for (uint32_t index = 0U; index < RS_P3_WORDS; ++index) { + rs_p3_destinations[job].words[index] = UINT32_C(0xcccccccc); + } + for (uint32_t part = 0U; part < 4U; ++part) { + rs_dma_tcd_t *const tcd = &rs_p3_tcds[job][part]; + *tcd = (rs_dma_tcd_t){0}; + if (part < 3U) { + tcd->next_ptr = (uint32_t)(uintptr_t)&rs_p3_tcds[job][part + 1U]; + } + tcd->source = (uint32_t)(uintptr_t)&rs_p3_source.words[part * 256U]; + tcd->destination = (uint32_t)(uintptr_t)&rs_p3_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_p3_readback(uint32_t *checksum) { + uint32_t hash = UINT32_C(2166136261); + for (uint32_t index = 0U; index < RS_P3_GUARDS; ++index) { + if ((rs_p3_source.before[index] != RS_P3_GUARD) || + (rs_p3_source.after[index] != RS_P3_GUARD)) { + return false; + } + } + for (uint32_t job = 0U; job < RS_P3_JOBS; ++job) { + for (uint32_t index = 0U; index < RS_P3_GUARDS; ++index) { + if ((rs_p3_destinations[job].before[index] != RS_P3_GUARD) || + (rs_p3_destinations[job].after[index] != RS_P3_GUARD)) { + return false; + } + } + for (uint32_t index = 0U; index < RS_P3_WORDS; ++index) { + const uint32_t expected = RS_P3_PATTERN ^ index; + const uint32_t observed = rs_p3_destinations[job].words[index]; + if ((observed != expected) || (rs_p3_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_p3_dma(uint32_t case_id, rs_p3_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; + const uint32_t chunk = (case_id == 7U) ? 256U : 4096U; + rs_dma_status_t status; + rs_dma_config_t config = { + .kind = RS_DMA_KIND_MM_TO_MM, + .request = RS_DMA_REQUEST_SOFTWARE, + .byte_count = chunk, + .width = RS_DMA_WIDTH_32, + .source_increment = true, + .destination_increment = true, + .priority = 0U, + .burst_beats = burst, + }; + if (rs_dma_session_acquire(&rs_p3_owner, RS_DMA_CHANNEL_BULK) != RS_OK) { + return false; + } + for (uint32_t job = 0U; job < RS_P3_JOBS; ++job) { + for (uint32_t offset = 0U; offset < 4096U; offset += chunk) { + config.source = (uintptr_t)&rs_p3_source.words[offset / 4U]; + config.destination = (uintptr_t)&rs_p3_destinations[job].words[offset / 4U]; + if (rs_dma_session_configure(&rs_p3_owner, &config) != RS_OK) { + return false; + } + /* The verification image owns this channel for the finite hardware + * chain. The existing convenience TCD helper submits one at a time. */ + RS_DMA_CH_REG(RS_DMA_CHANNEL_BULK, RS_DMA_CH_REG_TCD_HEAD) = + descriptors ? (uint32_t)(uintptr_t)&rs_p3_tcds[job][0] : 0U; + RS_DMA_CH_REG(RS_DMA_CHANNEL_BULK, RS_DMA_CH_REG_TCD_COUNT) = descriptors ? 4U : 0U; + if (rs_dma_session_start(&rs_p3_owner) != RS_OK) { + return false; + } + if (concurrent_cpu && (offset == 0U) && !rs_p3_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.error || status.aborted || !status.done || + (status.bytes_done != (descriptors ? 1024U : chunk))) { + return false; + } + result->payload_bytes += chunk; + __asm__ volatile("fence rw, rw" ::: "memory"); + } + } + return rs_dma_session_release(&rs_p3_owner) == RS_OK; +} + +static bool rs_p3_case(uint32_t case_id) { + rs_p3_result_t *const result = &rs_p3_results[case_id]; + uint64_t cycles_begin; + uint64_t instructions_begin; + bool pass = true; + + if (case_id >= 2U) { + rs_p3_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_p3_cycles(); + instructions_begin = rs_p3_instructions(); + if (case_id == 1U) { + pass = rs_p3_cpu(result); + } else if (case_id == 2U) { + for (uint32_t job = 0U; job < RS_P3_JOBS; ++job) { + for (uint32_t index = 0U; index < RS_P3_WORDS; ++index) { + rs_p3_destinations[job].words[index] = rs_p3_source.words[index]; + } + } + result->payload_bytes = UINT32_C(16384); + } else if (case_id >= 3U) { + pass = rs_p3_dma(case_id, result); + } else { + /* Empty-window calibration intentionally includes measurement overhead. */ + } + __COMPILER_BARRIER(); + result->instructions = rs_p3_instructions() - instructions_begin; + result->cycles = rs_p3_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_p3_readback(&result->checksum); + } + return pass && (result->memory.error_responses == 0U); +} + +static void rs_p3_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_P3_CASES] = { + "empty", "cpu", "memory", "dma1", "dma16", "contention", "tcd", "chunk256", + }; + 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_p3_finish(false, 1U); + } + if ((rs_p3_dma_cookie != UINT32_C(0x7135a9c2)) || + (rs_p3_source.words[RS_P3_WORDS - 1U] != 0U) || + (rs_p3_destinations[RS_P3_JOBS - 1U].words[RS_P3_WORDS - 1U] != 0U) || + (rs_p3_isa_check() != 0U)) { + rs_p3_finish(false, 20U); + } + rs_p3_stack_floor = (uintptr_t)_stack_point - 4096U; + __asm__ volatile("mv %0, sp" : "=r"(rs_p3_stack_painted_end)); + if (((uintptr_t)_ebss > rs_p3_stack_floor) || + (rs_p3_stack_painted_end < rs_p3_stack_floor + 128U)) { + rs_p3_finish(false, 21U); + } + rs_p3_stack_painted_end -= 128U; + for (uintptr_t address = rs_p3_stack_floor; address < rs_p3_stack_painted_end; address += 4U) { + *(volatile uint32_t *)address = UINT32_C(0xc35a9e71); + } + for (uint32_t case_id = 0U; case_id < RS_P3_CASES; ++case_id) { + if (!rs_p3_case(case_id)) { + printf("P3_ERROR case=%u\n", case_id); + rs_p3_finish(false, (uint8_t)(case_id + 2U)); + } + } + /* UART output is entirely outside every performance window. */ + printf("P3_HEADER version=3 cpu_hz=%u words=%u jobs=%u iterations=%u seed=0x%08x\n", + RS_CPU_CLOCK_HZ, RS_P3_WORDS, RS_P3_JOBS, RS_P3_ITERATIONS, RS_P3_SEED); + for (uint32_t case_id = 0U; case_id < RS_P3_CASES; ++case_id) { + const rs_p3_result_t *const result = &rs_p3_results[case_id]; + printf("P3_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); + } + { + uintptr_t first = rs_p3_stack_floor; + while ((first < rs_p3_stack_painted_end) && + (*(volatile const uint32_t *)first == UINT32_C(0xc35a9e71))) { + first += 4U; + } + printf("P3_STACK reserved=4096 observed=%u\n", (uint32_t)((uintptr_t)_stack_point - first)); + } + printf("P3_ISA PASS\n"); + printf("P3_COMPLETE version=3 cases=%u\n", RS_P3_CASES); + rs_p3_finish(true, 0U); + return 0; +} diff --git a/app/apps/ci_smoke/tiny_r2_p3_isa.S b/app/apps/ci_smoke/tiny_r2_p3_isa.S new file mode 100644 index 00000000..5e9b22ca --- /dev/null +++ b/app/apps/ci_smoke/tiny_r2_p3_isa.S @@ -0,0 +1,63 @@ +/* Execute independent known-answer instructions and SRAM instruction visibility. */ + .text + .balign 4 + .global rs_p3_isa_check + .type rs_p3_isa_check,@function +rs_p3_isa_check: + mv t6, ra + li t0, 3 + li t1, 5 + mul t2, t0, t1 + li t3, 15 + bne t2, t3, isa_bad + div t2, t2, t0 + bne t2, t1, isa_bad +#ifdef __riscv_zbb + sh1add t2, t0, t1 + li t3, 11 + bne t2, t3, isa_bad + clz t2, t0 + li t3, 30 + bne t2, t3, isa_bad + clmul t2, t0, t1 + li t3, 15 + bne t2, t3, isa_bad + pack t2, t0, t1 + li t3, 0x50003 + bne t2, t3, isa_bad + bset t2, zero, t0 + li t3, 8 + bne t2, t3, isa_bad + li t0, 0x76543210 + li t1, 0x10 + xperm4 t2, t0, t1 + bne t2, t1, isa_bad + li a0, 0 + c.addi a0, 1 + li t3, 1 + bne a0, t3, isa_bad +#endif + la t0, rs_p3_patch_target + li t1, 0x00700513 + sw t1, 0(t0) + .option push + .option arch, +zifencei + fence.i + .option pop + jalr ra, t0, 0 + li t3, 7 + bne a0, t3, isa_bad + li a0, 0 + jr t6 +isa_bad: + li a0, 1 + jr t6 + .size rs_p3_isa_check, .-rs_p3_isa_check + + .balign 4 + .global rs_p3_patch_target + .type rs_p3_patch_target,@function +rs_p3_patch_target: + .word 0x00000513 + .word 0x00008067 + .size rs_p3_patch_target, .-rs_p3_patch_target diff --git a/app/apps/ci_smoke/ws2812_r2.c b/app/apps/ci_smoke/ws2812_r2.c new file mode 100644 index 00000000..e6e2ad30 --- /dev/null +++ b/app/apps/ci_smoke/ws2812_r2.c @@ -0,0 +1,153 @@ +/* Option A: Mini PIO compatibility only. DMA/MMIO is a separate unsupported + * product path; its failure evidence and production-crossbar test are retained. */ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +_Static_assert(RS_PCLK_CLOCK_HZ == 24000000U, "Mini PIO acceptance requires PCLK24"); + +static uint32_t rs_ws_pixels[65]; +static volatile bool rs_ws_event; +static volatile uint32_t rs_ws_irqs; +static volatile bool rs_ws_irq_error; +static const rs_ws2812_config_t rs_ws_timing = { + .source_clock_hz = RS_PCLK_CLOCK_HZ, + .bit_period_ns = RS_WS2812_DEFAULT_BIT_PERIOD_NS, + .t0h_ns = RS_WS2812_DEFAULT_T0H_NS, + .t1h_ns = RS_WS2812_DEFAULT_T1H_NS, + .reset_ns = RS_WS2812_DEFAULT_RESET_NS, + .fifo_watermark = 8U, +}; + +static void rs_ws_interrupt(uintptr_t cause, uintptr_t stack) { + (void)cause; + (void)stack; + ++rs_ws_irqs; + rs_ws_event = true; + /* Leave sticky status for the foreground HAL to consume. */ + if (rs_ws2812_irq_enable(0U) != RS_OK) { + rs_ws_irq_error = true; + } +} + +static void rs_ws_finish(bool pass, uint8_t code) { + rs_uart_status_t uart; + bool drained = false; + __disable_irq(); + printf("WS2812_P3_MINI_PIO %s code=%u irqs=%u dma_mmio=UNSUPPORTED\n", pass ? "PASS" : "FAIL", + code, rs_ws_irqs); + for (uint32_t remaining = RS_TIMEOUT_DEFAULT; remaining != 0U; --remaining) { + if ((rs_uart_get_status(&uart) == RS_OK) && (uart.tx_level == 0U) && + ((uart.flags & 4U) == 0U)) { + drained = true; + break; + } + } + rs_sysctrl_write_test_status(pass && drained, code); + for (;;) { + } +} + +static bool rs_ws_idle(bool expect_underflow) { + rs_ws2812_status_t status; + if (rs_ws2812_get_status(&status) != RS_OK) { + return false; + } + return !status.busy && !status.reset_active && status.fifo_empty && + (expect_underflow ? ((status.error_status & RS_WS2812_ERROR_UNDERFLOW) != 0U) + : (status.error_status == 0U)); +} + +static bool rs_ws_pio_frame(uint32_t words, bool check_interrupt) { + rs_ws_event = false; + if ((rs_ws2812_init(&rs_ws_timing) != RS_OK) || + (rs_ws2812_irq_enable(RS_WS2812_INTR_DONE | RS_WS2812_INTR_ERROR) != RS_OK)) { + return false; + } + if (check_interrupt) { + /* This short frame fits entirely in the FIFO. Wait for a real DONE IRQ + * before consuming its status so polling cannot acknowledge it first. */ + for (uint32_t index = 0U; index < words; ++index) { + if (rs_ws2812_push(rs_ws_pixels[index], RS_TIMEOUT_DEFAULT) != RS_OK) { + return false; + } + } + if (rs_ws2812_start(words) != RS_OK) { + return false; + } + uint32_t remaining = RS_TIMEOUT_DEFAULT; + while (!rs_ws_event && (remaining != 0U)) { + --remaining; + } + if (!rs_ws_event || (rs_ws2812_wait(RS_TIMEOUT_DEFAULT) != RS_OK)) { + return false; + } + } else if (rs_ws2812_write(rs_ws_pixels, words, RS_TIMEOUT_DEFAULT) != RS_OK) { + return false; + } + if (rs_ws_irq_error || !rs_ws_idle(false)) { + return false; + } + printf("WS2812_P3_MINI_PIO_FRAME words=%u\n", words); + return true; +} + +static bool rs_ws_pio_underflow(void) { + if ((rs_ws2812_init(&rs_ws_timing) != RS_OK) || + (rs_ws2812_irq_enable(RS_WS2812_INTR_ERROR | RS_WS2812_INTR_ABORTED) != RS_OK) || + (rs_ws2812_push(rs_ws_pixels[0], RS_TIMEOUT_DEFAULT) != RS_OK) || + (rs_ws2812_start(2U) != RS_OK) || (rs_ws2812_wait(RS_TIMEOUT_DEFAULT) != RS_EIO) || + (rs_ws2812_abort(RS_TIMEOUT_DEFAULT) != RS_OK)) { + return false; + } + return rs_ws_idle(true); +} + +static bool rs_ws_pio_abort(void) { + if (rs_ws2812_init(&rs_ws_timing) != RS_OK) { + return false; + } + for (uint32_t index = 0U; index < 16U; ++index) { + if (rs_ws2812_push(rs_ws_pixels[index], RS_TIMEOUT_DEFAULT) != RS_OK) { + return false; + } + } + return (rs_ws2812_start(65U) == RS_OK) && (rs_ws2812_abort(RS_TIMEOUT_DEFAULT) == RS_OK) && + rs_ws_idle(false); +} + +int main(void) { + const rs_gpio_config_t gpio = { + .mode = RS_GPIO_MODE_ALT1, + .pull = RS_GPIO_PULL_NONE, + .trigger = RS_GPIO_TRIGGER_NONE, + .input_cmos = false, + }; + for (uint32_t index = 0U; index < 65U; ++index) { + rs_ws_pixels[index] = (index * UINT32_C(0x010203)) & UINT32_C(0xffffff); + } + if ((rs_uart_init(RS_CPU_CLOCK_HZ, UART_BPS) != RS_OK) || + (rs_gpio_configure(2U, &gpio) != RS_OK) || + (rs_irq_enable_external(RS_SOC_EXT_IRQ_WS2812, rs_ws_interrupt) != RS_OK)) { + rs_ws_finish(false, 1U); + } + __enable_irq(); + if (!rs_ws_pio_frame(4U, true) || !rs_ws_pio_frame(16U, false) || + !rs_ws_pio_frame(33U, false) || !rs_ws_pio_frame(65U, false)) { + rs_ws_finish(false, 2U); + } + if (!rs_ws_pio_underflow() || !rs_ws_pio_abort()) { + rs_ws_finish(false, 3U); + } + rs_ws_finish((rs_ws_irqs != 0U) && !rs_ws_irq_error, 0U); + return 0; +} diff --git a/configs/README.md b/configs/README.md index 17fa9335..c0f19a12 100644 --- a/configs/README.md +++ b/configs/README.md @@ -66,6 +66,13 @@ For supported profiles and commands, see the root [README](../README.md) and ## Tiny MCU +`benchmark/ics55-tiny-performance.mk` is the separate R2-P3 compiler experiment +entrypoint: the same SAFE24/parked-PLL hardware, validated candidate ISA, +`SW_OPT=O2` and `SW_LTO=NO`. Optimizer/LTO and `ld2_tiny_banked` placement are +explicit variants; the CI profile remains compatible RV32IM. See the +[software scheduling runbook](../docs/ip/tiny-soc-r2-software-scheduling.md) +for the matrix and its pending/current evidence boundary. + The [2026-10-07 Tiny refreeze](../docs/ip/tiny-soc.md#ics55-default-platform-and-final-timing-gate-2026-10-07) sets the default to ICS55 with `HAVE_PLL=YES` and SAFE24 boot. `ci/ics55-tiny.mk` implements that entrypoint with the PLL held off; standalone diff --git a/configs/benchmark/ics55-tiny-performance.mk b/configs/benchmark/ics55-tiny-performance.mk new file mode 100644 index 00000000..8b2295ed --- /dev/null +++ b/configs/benchmark/ics55-tiny-performance.mk @@ -0,0 +1,5 @@ +# R2-P3 software experiment only: external SAFE24, no hardware acceleration changes. +include $(dir $(lastword $(MAKEFILE_LIST)))../ci/ics55-tiny.mk +SW_ISA_PROFILE := TINY_PERF +SW_OPT := O2 +SW_LTO := NO diff --git a/crt/README.md b/crt/README.md index dbd81f2f..8c5809eb 100644 --- a/crt/README.md +++ b/crt/README.md @@ -105,6 +105,14 @@ deterministic channel assignments; see the [DMA V2 contract](../docs/ip/dma.md). The SDK accepts aligned 32-bit transfers, partial final beats for MM-to-MM, and 64-byte aligned TCD chains. +`rs_dma_session_*` adds single-hart channel leases across finite transfers. +Legacy channel mutation is rejected while leased, and BUSY prevents release. +Mini's hardware LP/HP ownership still applies. Shared `rs_ws2812_dma_*` service +uses a persistent, event-driven finite-refill session with channel 3 reserved +until wire completion or safe cleanup. Callers retain source buffers after a +timeout while status is active. See the [P3 runbook](../docs/ip/tiny-soc-r2-software-scheduling.md); +these shared APIs do not add WS2812 to the current Tiny integration. + `` provides bounded AES PIO/DMA, SHA-224/256, raw RSA-2048 modular exponentiation, zeroize, and known-answer self-test APIs. V2 callers first use `rs_crypto_init(timeout)` to load and lock CRYC1. It diff --git a/crt/arch/riscv/startup.S b/crt/arch/riscv/startup.S index dc0789c5..4eb954b9 100644 --- a/crt/arch/riscv/startup.S +++ b/crt/arch/riscv/startup.S @@ -105,6 +105,41 @@ PSRAM_READY_WAIT: #endif app_loader_start: +#ifdef RS_TINY_BANKED + la t1, _copy_table_start + la t2, _copy_table_end +copy_entry: + bgeu t1, t2, zero_entries_start + lw a0, 0(t1) + lw a1, 4(t1) + lw a2, 8(t1) + addi t1, t1, 12 + add a2, a1, a2 + beq a0, a1, copy_entry + bgeu a1, a2, copy_entry +copy_words: + lw t0, 0(a0) + sw t0, 0(a1) + addi a0, a0, 4 + addi a1, a1, 4 + bltu a1, a2, copy_words + j copy_entry +zero_entries_start: + la t1, _zero_table_start + la t2, _zero_table_end +zero_entry: + bgeu t1, t2, main_start + lw a0, 0(t1) + lw a1, 4(t1) + addi t1, t1, 8 + add a1, a0, a1 + bgeu a0, a1, zero_entry +zero_words: + sw zero, 0(a0) + addi a0, a0, 4 + bltu a0, a1, zero_words + j zero_entry +#else la a0, _ram_lma la a1, _ram_vma beq a0, a1, data_loader_start # flash xip @@ -139,7 +174,15 @@ bss_cleaner: addi a0, a0, 4 blt a0, a1, bss_cleaner +#endif main_start: +#ifdef RS_SOC_TINY + /* Hazard3 implements Zifencei even for compatible RV32IM images. */ + .option push + .option arch, +zifencei + fence.i + .option pop +#endif # call main #ifdef CSR_ENABLE call _premain_init # init irq diff --git a/crt/include/retrosoc/arch/riscv/system_base.h b/crt/include/retrosoc/arch/riscv/system_base.h index 8e249fd8..3a2ac160 100644 --- a/crt/include/retrosoc/arch/riscv/system_base.h +++ b/crt/include/retrosoc/arch/riscv/system_base.h @@ -619,7 +619,7 @@ __STATIC_FORCEINLINE void __disable_all_counter(void) { * and data streams. */ __STATIC_FORCEINLINE void __FENCE_I(void) { - __ASM volatile("fence.i"); + __ASM volatile(".option push\n.option arch, +zifencei\nfence.i\n.option pop" ::: "memory"); } /** \brief Read & Write Memory barrier */ diff --git a/crt/include/retrosoc/hal/dma.h b/crt/include/retrosoc/hal/dma.h index 42ba7e82..bf35d7ae 100644 --- a/crt/include/retrosoc/hal/dma.h +++ b/crt/include/retrosoc/hal/dma.h @@ -104,6 +104,19 @@ typedef struct { _Static_assert(sizeof(rs_dma_tcd_t) == 64U, "DMA TCD ABI must be 64 bytes"); +/* Single-hart software lease. Keep this object alive and unchanged until release. + * Mini's LP/HP hardware owner must already permit this hart's DMA accesses. + * A timeout never releases a lease; accepted bus work must first drain. */ +typedef struct { + uint32_t channel; +} rs_dma_session_t; + +rs_status_t rs_dma_session_acquire(rs_dma_session_t *session, uint32_t channel); +rs_status_t rs_dma_session_configure(rs_dma_session_t *session, const rs_dma_config_t *config); +rs_status_t rs_dma_session_start(rs_dma_session_t *session); +rs_status_t rs_dma_session_abort(rs_dma_session_t *session); +rs_status_t rs_dma_session_release(rs_dma_session_t *session); + #define RS_DMA_TCD_VALID UINT32_C(0x00000001) #define RS_DMA_TCD_SRC_INC UINT32_C(0x00000002) #define RS_DMA_TCD_DST_INC UINT32_C(0x00000004) diff --git a/crt/include/retrosoc/hal/dma_regs.h b/crt/include/retrosoc/hal/dma_regs.h index ccf87f06..596c8e1d 100644 --- a/crt/include/retrosoc/hal/dma_regs.h +++ b/crt/include/retrosoc/hal/dma_regs.h @@ -89,8 +89,12 @@ #define RS_DMA_CHANNEL_OFFSET(channel, offset) \ (RS_DMA_CH_BASE + ((uint32_t)(channel)*RS_DMA_CH_STRIDE) + (offset)) +#ifdef RS_DMA_TEST_MMIO +extern volatile uint32_t rs_dma_test_mmio[1024]; +#define RS_DMA_REG(offset) rs_dma_test_mmio[(offset) / 4U] +#else #define RS_DMA_REG(offset) RS_SOC_REG32(RS_SOC_APB4_DMA_BASE, (offset)) -#define RS_DMA_CH_REG(channel, offset) \ - RS_SOC_REG32(RS_SOC_APB4_DMA_BASE, RS_DMA_CHANNEL_OFFSET((channel), (offset))) +#endif +#define RS_DMA_CH_REG(channel, offset) RS_DMA_REG(RS_DMA_CHANNEL_OFFSET((channel), (offset))) #endif diff --git a/crt/include/retrosoc/hal/ws2812.h b/crt/include/retrosoc/hal/ws2812.h index 262a0976..34f9db59 100644 --- a/crt/include/retrosoc/hal/ws2812.h +++ b/crt/include/retrosoc/hal/ws2812.h @@ -50,6 +50,27 @@ typedef struct { bool reset_active; } rs_ws2812_status_t; +typedef struct { + bool active; + bool draining; + bool dma_pending; + uint32_t remaining; + uint32_t batches; + rs_status_t result; +} rs_ws2812_dma_status_t; + +/* Foreground-only, one transmitter session. IRQ handlers publish events; a timer + * must also call service so missing IRQs cannot disable the software deadline. + * now/deadline use caller-defined monotonic uint64_t ticks, without wraparound. + * Pixels remain immutable and alive until status.active becomes false, including + * after timeout. cancel/service continue recovery without busy waits. */ +rs_status_t rs_ws2812_dma_begin(const uint32_t *pixels, size_t pixel_count, uint64_t now, + uint64_t deadline); +rs_status_t rs_ws2812_dma_service(uint64_t now); +rs_status_t rs_ws2812_dma_status(rs_ws2812_dma_status_t *status); +rs_status_t rs_ws2812_dma_cancel(void); +rs_status_t rs_ws2812_refill_words(uint32_t remaining, uint32_t level, uint32_t *words); + uint32_t rs_ws2812_pack_grb(uint8_t red, uint8_t green, uint8_t blue); rs_status_t rs_ws2812_timing_from_ns(const rs_ws2812_config_t *config, rs_ws2812_timing_t *timing); rs_status_t rs_ws2812_init(const rs_ws2812_config_t *config); diff --git a/crt/linker/ld2_tiny_banked.lds b/crt/linker/ld2_tiny_banked.lds new file mode 100644 index 00000000..e3697eaa --- /dev/null +++ b/crt/linker/ld2_tiny_banked.lds @@ -0,0 +1,51 @@ +/* Software placement over the existing flat 128 KiB aperture, not P4 hardware banks. */ +ENTRY(_start) +INCLUDE memory_regions.ld +PHDRS +{ + boot PT_LOAD FLAGS(5); + code PT_LOAD FLAGS(5); + cpu PT_LOAD FLAGS(6); + dma PT_LOAD FLAGS(6); +} +MEMORY +{ + CODE (rx) : ORIGIN = ORIGIN(SRAM), LENGTH = 64K + CPU (rw) : ORIGIN = ORIGIN(SRAM) + 64K, LENGTH = 32K + DMA (rw) : ORIGIN = ORIGIN(SRAM) + 96K, LENGTH = 32K +} +SECTIONS +{ + .init : { KEEP(*(.init)) . = ALIGN(4); } > FLASH :boot + .copy_table : { + _copy_table_start = .; + LONG(LOADADDR(.text)); LONG(ADDR(.text)); LONG(SIZEOF(.text)); + LONG(LOADADDR(.dma_data)); LONG(ADDR(.dma_data)); LONG(SIZEOF(.dma_data)); + LONG(LOADADDR(.data)); LONG(ADDR(.data)); LONG(SIZEOF(.data)); + _copy_table_end = .; + _zero_table_start = .; + LONG(ADDR(.dma_bss)); LONG(SIZEOF(.dma_bss)); + LONG(ADDR(.bss)); LONG(SIZEOF(.bss)); + _zero_table_end = .; + } > FLASH :boot + .text : { + *(.text*) *(.rodata*) *(.srodata*) + . = ALIGN(4); + } > CODE AT> FLASH :code + /* Consume annotated DMA inputs before the general data/BSS wildcards. */ + .dma_data : ALIGN(64) { *(.data.rs_dma*) . = ALIGN(4); } > DMA AT> FLASH :dma + .dma_bss (NOLOAD) : ALIGN(64) { *(.bss.rs_dma*) . = ALIGN(4); } > DMA :dma + .data : ALIGN(4) { *(.data*) *(.sdata*) . = ALIGN(4); } > CPU AT> FLASH :cpu + .bss (NOLOAD) : ALIGN(4) { + _sbss = .; *(.bss*) *(.sbss*) *(COMMON) . = ALIGN(16); _ebss = .; + } > CPU :cpu + _heap_start = _ebss; + _stack_point = ORIGIN(CPU) + LENGTH(CPU); + _stack_bottom = _stack_point - 4096; + ASSERT(LENGTH(SRAM) == 128K, "Tiny bank placement requires exactly 128 KiB") + ASSERT(_ebss <= _stack_bottom, "Tiny CPU data overlaps reserved 4 KiB stack") + ASSERT((ADDR(.dma_bss) + SIZEOF(.dma_bss)) <= (ORIGIN(DMA) + LENGTH(DMA)), + "Tiny DMA storage overflows B3") + ASSERT((ADDR(.text) + SIZEOF(.text)) <= (ORIGIN(CODE) + LENGTH(CODE)), + "Tiny code overflows B0/B1") +} diff --git a/crt/src/hal/dma.c b/crt/src/hal/dma.c index 7ff0b074..bd9c2a81 100644 --- a/crt/src/hal/dma.c +++ b/crt/src/hal/dma.c @@ -3,17 +3,50 @@ #include #include +static const rs_dma_session_t *rs_dma_owners[RS_DMA_CHANNEL_COUNT]; + +static uint32_t rs_dma_lock(void) { + uint32_t saved = 0U; +#if defined(__riscv) && defined(CSR_ENABLE) + __asm__ volatile("csrrci %0, mstatus, 8" : "=r"(saved)::"memory"); +#endif + return saved; +} + +static void rs_dma_unlock(uint32_t saved) { +#if defined(__riscv) && defined(CSR_ENABLE) + if ((saved & UINT32_C(8)) != 0U) { + __asm__ volatile("csrsi mstatus, 8" ::: "memory"); + } +#else + (void)saved; +#endif +} + +static void rs_dma_fence(void) { +#ifdef __riscv + __asm__ volatile("fence rw, rw" ::: "memory"); +#else + __asm__ volatile("" ::: "memory"); +#endif +} + static bool rs_dma_channel_valid(uint32_t channel) { return channel < RS_DMA_CHANNEL_COUNT; } -rs_status_t rs_dma_configure(uint32_t channel, const rs_dma_config_t *config) { +static rs_status_t rs_dma_configure_owned(uint32_t channel, const rs_dma_config_t *config, + const rs_dma_session_t *owner) { uint32_t channel_config; + uint32_t saved; if (rs_dma_config_validate(channel, config) != RS_OK) { return RS_EINVAL; } - if ((RS_DMA_CH_REG(channel, RS_DMA_CH_REG_STATUS) & RS_DMA_STATUS_BUSY) != 0U) { + saved = rs_dma_lock(); + if ((rs_dma_owners[channel] != owner) || + ((RS_DMA_CH_REG(channel, RS_DMA_CH_REG_STATUS) & RS_DMA_STATUS_BUSY) != 0U)) { + rs_dma_unlock(saved); return RS_EIO; } @@ -40,6 +73,95 @@ rs_status_t rs_dma_configure(uint32_t channel, const rs_dma_config_t *config) { RS_DMA_CH_REG(channel, RS_DMA_CH_REG_REQUEST_SEL) = (uint32_t)config->request; RS_DMA_CH_REG(channel, RS_DMA_CH_REG_BURST_CFG) = (uint32_t)config->burst_beats; RS_DMA_CH_REG(channel, RS_DMA_CH_REG_CRC_EXPECTED) = config->crc_expected; + rs_dma_unlock(saved); + return RS_OK; +} + +rs_status_t rs_dma_configure(uint32_t channel, const rs_dma_config_t *config) { + return rs_dma_configure_owned(channel, config, NULL); +} + +static rs_status_t rs_dma_command(uint32_t channel, uint32_t command, + const rs_dma_session_t *owner) { + uint32_t saved; + if (!rs_dma_channel_valid(channel)) { + return RS_EINVAL; + } + saved = rs_dma_lock(); + if (rs_dma_owners[channel] != owner) { + rs_dma_unlock(saved); + return RS_EIO; + } + if ((command == RS_DMA_CH_CTRL_RESET) && + ((RS_DMA_CH_REG(channel, RS_DMA_CH_REG_STATUS) & RS_DMA_STATUS_BUSY) != 0U)) { + rs_dma_unlock(saved); + return RS_EIO; + } + if (command == RS_DMA_CH_CTRL_START) { + RS_DMA_CH_REG(channel, RS_DMA_CH_REG_EVENT_STATUS) = RS_DMA_EVENT_ALL; + rs_dma_fence(); + } + RS_DMA_CH_REG(channel, RS_DMA_CH_REG_CTRL) = command; + rs_dma_unlock(saved); + return RS_OK; +} + +rs_status_t rs_dma_session_acquire(rs_dma_session_t *session, uint32_t channel) { + uint32_t saved; + if ((session == NULL) || !rs_dma_channel_valid(channel)) { + return RS_EINVAL; + } + saved = rs_dma_lock(); + for (uint32_t index = 0U; index < RS_DMA_CHANNEL_COUNT; ++index) { + if (rs_dma_owners[index] == session) { + rs_dma_unlock(saved); + return RS_EIO; + } + } + if ((rs_dma_owners[channel] != NULL) || + ((RS_DMA_CH_REG(channel, RS_DMA_CH_REG_STATUS) & RS_DMA_STATUS_BUSY) != 0U)) { + rs_dma_unlock(saved); + return RS_EIO; + } + session->channel = channel; + rs_dma_owners[channel] = session; + /* A new direct session must not inherit the previous owner's TCD chain. */ + RS_DMA_CH_REG(channel, RS_DMA_CH_REG_CTRL) = RS_DMA_CH_CTRL_RESET; + RS_DMA_CH_REG(channel, RS_DMA_CH_REG_TCD_HEAD) = 0U; + RS_DMA_CH_REG(channel, RS_DMA_CH_REG_TCD_COUNT) = 0U; + rs_dma_unlock(saved); + return RS_OK; +} + +rs_status_t rs_dma_session_configure(rs_dma_session_t *session, const rs_dma_config_t *config) { + return (session == NULL) ? RS_EINVAL + : rs_dma_configure_owned(session->channel, config, session); +} + +rs_status_t rs_dma_session_start(rs_dma_session_t *session) { + return (session == NULL) ? RS_EINVAL + : rs_dma_command(session->channel, RS_DMA_CH_CTRL_START, session); +} + +rs_status_t rs_dma_session_abort(rs_dma_session_t *session) { + return (session == NULL) ? RS_EINVAL + : rs_dma_command(session->channel, RS_DMA_CH_CTRL_ABORT, session); +} + +rs_status_t rs_dma_session_release(rs_dma_session_t *session) { + uint32_t saved; + if ((session == NULL) || !rs_dma_channel_valid(session->channel)) { + return RS_EINVAL; + } + saved = rs_dma_lock(); + if ((rs_dma_owners[session->channel] != session) || + ((RS_DMA_CH_REG(session->channel, RS_DMA_CH_REG_STATUS) & RS_DMA_STATUS_BUSY) != 0U)) { + rs_dma_unlock(saved); + return RS_EIO; + } + rs_dma_fence(); + rs_dma_owners[session->channel] = NULL; + rs_dma_unlock(saved); return RS_OK; } @@ -92,7 +214,7 @@ rs_status_t rs_dma_submit_tcd_chain(uint32_t channel, rs_dma_tcd_t *first, uint3 } RS_DMA_CH_REG(channel, RS_DMA_CH_REG_TCD_HEAD) = (uint32_t)(uintptr_t)current; RS_DMA_CH_REG(channel, RS_DMA_CH_REG_TCD_COUNT) = UINT32_C(1); - __asm__ volatile("fence rw, rw" ::: "memory"); + rs_dma_fence(); result = rs_dma_start(channel); if (result != RS_OK) { return result; @@ -118,36 +240,19 @@ rs_status_t rs_dma_submit_tcd_chain(uint32_t channel, rs_dma_tcd_t *first, uint3 } rs_status_t rs_dma_start(uint32_t channel) { - if (!rs_dma_channel_valid(channel)) { - return RS_EINVAL; - } - RS_DMA_CH_REG(channel, RS_DMA_CH_REG_EVENT_STATUS) = RS_DMA_EVENT_ALL; - RS_DMA_CH_REG(channel, RS_DMA_CH_REG_CTRL) = RS_DMA_CH_CTRL_START; - return RS_OK; + return rs_dma_command(channel, RS_DMA_CH_CTRL_START, NULL); } rs_status_t rs_dma_suspend(uint32_t channel) { - if (!rs_dma_channel_valid(channel)) { - return RS_EINVAL; - } - RS_DMA_CH_REG(channel, RS_DMA_CH_REG_CTRL) = RS_DMA_CH_CTRL_SUSPEND; - return RS_OK; + return rs_dma_command(channel, RS_DMA_CH_CTRL_SUSPEND, NULL); } rs_status_t rs_dma_resume(uint32_t channel) { - if (!rs_dma_channel_valid(channel)) { - return RS_EINVAL; - } - RS_DMA_CH_REG(channel, RS_DMA_CH_REG_CTRL) = RS_DMA_CH_CTRL_RESUME; - return RS_OK; + return rs_dma_command(channel, RS_DMA_CH_CTRL_RESUME, NULL); } rs_status_t rs_dma_abort(uint32_t channel) { - if (!rs_dma_channel_valid(channel)) { - return RS_EINVAL; - } - RS_DMA_CH_REG(channel, RS_DMA_CH_REG_CTRL) = RS_DMA_CH_CTRL_ABORT; - return RS_OK; + return rs_dma_command(channel, RS_DMA_CH_CTRL_ABORT, NULL); } rs_status_t rs_dma_abort_wait(uint32_t channel, rs_timeout_t timeout) { @@ -165,14 +270,7 @@ rs_status_t rs_dma_abort_wait(uint32_t channel, rs_timeout_t timeout) { } rs_status_t rs_dma_reset(uint32_t channel) { - if (!rs_dma_channel_valid(channel)) { - return RS_EINVAL; - } - if ((RS_DMA_CH_REG(channel, RS_DMA_CH_REG_STATUS) & RS_DMA_STATUS_BUSY) != 0U) { - return RS_EIO; - } - RS_DMA_CH_REG(channel, RS_DMA_CH_REG_CTRL) = RS_DMA_CH_CTRL_RESET; - return RS_OK; + return rs_dma_command(channel, RS_DMA_CH_CTRL_RESET, NULL); } rs_status_t rs_dma_get_status(uint32_t channel, rs_dma_status_t *status) { diff --git a/crt/src/hal/ws2812.c b/crt/src/hal/ws2812.c index 8bf345d6..47c5cf94 100644 --- a/crt/src/hal/ws2812.c +++ b/crt/src/hal/ws2812.c @@ -35,16 +35,36 @@ #define RS_WS2812_INTR_ENABLE_OFFSET UINT32_C(0x34) #define RS_WS2812_IP_INFO_OFFSET UINT32_C(0x3C) +/* Persistent storage also keeps legacy blocking-wrapper failures recoverable. */ +static rs_ws2812_dma_status_t rs_ws2812_transfer; +static rs_dma_session_t rs_ws2812_dma_owner; +static const uint32_t *rs_ws2812_pixels; +static uint32_t rs_ws2812_next; +static uint32_t rs_ws2812_batch_words; +static uint32_t rs_ws2812_saved_watermark; +static uint64_t rs_ws2812_deadline; +static bool rs_ws2812_abort_sent; + static volatile uint32_t *rs_ws2812_register(uint32_t offset) { return (volatile uint32_t *)(uintptr_t)(RS_SOC_APB4_WS2812_BASE + (uintptr_t)offset); } static uint32_t rs_ws2812_read(uint32_t offset) { +#ifdef RS_WS2812_TEST_MMIO + extern uint32_t rs_ws2812_test_read(uint32_t offset); + return rs_ws2812_test_read(offset); +#else return *rs_ws2812_register(offset); +#endif } static void rs_ws2812_write_register(uint32_t offset, uint32_t value) { +#ifdef RS_WS2812_TEST_MMIO + extern void rs_ws2812_test_write(uint32_t offset, uint32_t value); + rs_ws2812_test_write(offset, value); +#else *rs_ws2812_register(offset) = value; +#endif } static uint32_t rs_ws2812_fifo_depth(void) { @@ -92,6 +112,10 @@ rs_status_t rs_ws2812_init(const rs_ws2812_config_t *config) { rs_ws2812_timing_t timing; uint32_t fifo_depth; + if (rs_ws2812_transfer.active) { + return RS_EIO; + } + if (rs_ws2812_timing_from_ns(config, &timing) != RS_OK) { return RS_EINVAL; } @@ -140,6 +164,10 @@ rs_status_t rs_ws2812_get_status(rs_ws2812_status_t *status) { rs_status_t rs_ws2812_push(uint32_t pixel, rs_timeout_t timeout) { uint32_t status; + if (rs_ws2812_transfer.active) { + return RS_EIO; + } + if ((pixel & ~RS_WS2812_PIXEL_MASK) != 0U) { return RS_EINVAL; } @@ -159,7 +187,7 @@ rs_status_t rs_ws2812_push(uint32_t pixel, rs_timeout_t timeout) { return RS_ETIMEOUT; } -rs_status_t rs_ws2812_start(uint32_t pixel_count) { +static rs_status_t rs_ws2812_start_frame(uint32_t pixel_count) { if ((pixel_count == 0U) || ((rs_ws2812_read(RS_WS2812_STATUS_OFFSET) & RS_WS2812_STATUS_BUSY) != 0U)) { return RS_EINVAL; @@ -174,7 +202,14 @@ rs_status_t rs_ws2812_start(uint32_t pixel_count) { : RS_EIO; } +rs_status_t rs_ws2812_start(uint32_t pixel_count) { + return rs_ws2812_transfer.active ? RS_EIO : rs_ws2812_start_frame(pixel_count); +} + rs_status_t rs_ws2812_wait(rs_timeout_t timeout) { + if (rs_ws2812_transfer.active) { + return RS_EIO; + } while (timeout-- != 0U) { const uint32_t interrupt_state = rs_ws2812_read(RS_WS2812_INTR_STATE_OFFSET); if ((interrupt_state & RS_WS2812_INTR_ERROR) != 0U) { @@ -193,6 +228,9 @@ rs_status_t rs_ws2812_wait(rs_timeout_t timeout) { } rs_status_t rs_ws2812_abort(rs_timeout_t timeout) { + if (rs_ws2812_transfer.active) { + return RS_EIO; + } if ((rs_ws2812_read(RS_WS2812_STATUS_OFFSET) & RS_WS2812_STATUS_BUSY) == 0U) { return RS_OK; } @@ -226,6 +264,10 @@ rs_status_t rs_ws2812_write(const uint32_t *pixels, size_t pixel_count, rs_timeo size_t preloaded; rs_status_t status; + if (rs_ws2812_transfer.active) { + return RS_EIO; + } + status = rs_ws2812_prepare(pixels, pixel_count, &preloaded); if (status != RS_OK) { return status; @@ -244,51 +286,215 @@ rs_status_t rs_ws2812_write(const uint32_t *pixels, size_t pixel_count, rs_timeo return rs_ws2812_wait(timeout); } -rs_status_t rs_ws2812_write_dma(const uint32_t *pixels, size_t pixel_count, rs_timeout_t timeout) { - size_t preloaded; - size_t dma_words; - rs_status_t status; - rs_dma_config_t dma_config; - - status = rs_ws2812_prepare(pixels, pixel_count, &preloaded); - if (status != RS_OK) { - return status; +rs_status_t rs_ws2812_dma_status(rs_ws2812_dma_status_t *status) { + if (status == NULL) { + return RS_EINVAL; } - dma_words = pixel_count - preloaded; - if (dma_words != 0U) { - if (dma_words > (UINT32_MAX / sizeof(*pixels))) { + *status = rs_ws2812_transfer; + return RS_OK; +} + +static rs_status_t rs_ws2812_dma_fail(rs_status_t result) { + if (!rs_ws2812_transfer.draining) { + rs_ws2812_transfer.result = result; + rs_ws2812_transfer.draining = true; + (void)rs_dma_session_abort(&rs_ws2812_dma_owner); + } + return rs_ws2812_transfer.result; +} + +rs_status_t rs_ws2812_dma_cancel(void) { + return rs_ws2812_transfer.active ? rs_ws2812_dma_fail(RS_EIO) : RS_OK; +} + +rs_status_t rs_ws2812_dma_begin(const uint32_t *pixels, size_t pixel_count, uint64_t now, + uint64_t deadline) { + size_t preloaded = 0U; + rs_status_t result; + if (rs_ws2812_transfer.active) { + return RS_EIO; + } + if ((pixels == NULL) || (pixel_count == 0U) || (pixel_count > (UINT32_MAX / sizeof(*pixels))) || + (deadline <= now) || (((uintptr_t)pixels % sizeof(*pixels)) != 0U) || + ((uintptr_t)pixels > (UINT32_MAX - (pixel_count * sizeof(*pixels))))) { + return RS_EINVAL; + } + /* Validate the complete array before touching either device. */ + for (size_t index = 0U; index < pixel_count; ++index) { + if ((pixels[index] & ~RS_WS2812_PIXEL_MASK) != 0U) { return RS_EINVAL; } - dma_config.kind = RS_DMA_KIND_MM_TO_MM; - dma_config.request = RS_DMA_REQUEST_SOFTWARE; - dma_config.source = (uintptr_t)&pixels[preloaded]; - dma_config.destination = (uintptr_t)rs_ws2812_register(RS_WS2812_TXDATA_OFFSET); - dma_config.byte_count = (uint32_t)(dma_words * sizeof(*pixels)); - dma_config.width = RS_DMA_WIDTH_32; - dma_config.source_increment = true; - dma_config.destination_increment = false; - dma_config.priority = 1U; - dma_config.burst_beats = 1U; - status = rs_dma_configure(RS_DMA_CHANNEL_BULK, &dma_config); - if (status != RS_OK) { - return status; + } + if ((rs_ws2812_fifo_depth() != 16U) || + ((rs_ws2812_read(RS_WS2812_STATUS_OFFSET) & RS_WS2812_STATUS_BUSY) != 0U)) { + return RS_EIO; + } + result = rs_dma_session_acquire(&rs_ws2812_dma_owner, RS_DMA_CHANNEL_BULK); + if (result != RS_OK) { + return result; + } + rs_ws2812_transfer = (rs_ws2812_dma_status_t){.active = true, .result = RS_OK}; + rs_ws2812_abort_sent = false; + rs_ws2812_deadline = deadline; + rs_ws2812_pixels = pixels; + rs_ws2812_saved_watermark = rs_ws2812_read(RS_WS2812_FIFO_WATERMARK_OFFSET); + rs_ws2812_write_register(RS_WS2812_FIFO_WATERMARK_OFFSET, 8U); + result = rs_ws2812_prepare(pixels, pixel_count, &preloaded); + if (result == RS_OK) { + rs_ws2812_next = (uint32_t)preloaded; + rs_ws2812_transfer.remaining = (uint32_t)(pixel_count - preloaded); + result = rs_ws2812_start_frame((uint32_t)pixel_count); + } + return (result == RS_OK) ? RS_OK : rs_ws2812_dma_fail(result); +} + +static rs_status_t rs_ws2812_dma_finish(void) { + rs_status_t result = rs_dma_session_release(&rs_ws2812_dma_owner); + if (result != RS_OK) { + return rs_ws2812_dma_fail(result); + } + rs_ws2812_write_register(RS_WS2812_CTRL_OFFSET, RS_WS2812_CTRL_FIFO_FLUSH); + rs_ws2812_write_register(RS_WS2812_FIFO_WATERMARK_OFFSET, rs_ws2812_saved_watermark); + rs_ws2812_write_register(RS_WS2812_INTR_STATE_OFFSET, RS_WS2812_INTR_ALL); + rs_ws2812_transfer.active = false; + rs_ws2812_transfer.dma_pending = false; + rs_ws2812_pixels = NULL; + return rs_ws2812_transfer.result; +} + +rs_status_t rs_ws2812_dma_service(uint64_t now) { + uint32_t dma_flags; + uint32_t transmitter_flags; + uint32_t interrupts; + uint32_t level; + uint32_t words; + + if (!rs_ws2812_transfer.active) { + return rs_ws2812_transfer.result; + } + /* This bounded refill path needs terminal flags, not the diagnostic CRC, + * stall counters or hardware REMAINING_WORDS. Avoid full diagnostic reads + * and struct unpacking before the first admitted FIFO write. */ + dma_flags = RS_DMA_CH_REG(RS_DMA_CHANNEL_BULK, RS_DMA_CH_REG_STATUS); + if (rs_ws2812_transfer.draining) { + /* Keep the consumer running until all accepted DMA responses return. */ + if ((dma_flags & RS_DMA_STATUS_BUSY) == 0U) { + transmitter_flags = rs_ws2812_read(RS_WS2812_STATUS_OFFSET); + if (!rs_ws2812_abort_sent) { + rs_ws2812_write_register(RS_WS2812_CTRL_OFFSET, RS_WS2812_CTRL_ABORT); + rs_ws2812_abort_sent = true; + } else if ((transmitter_flags & + (RS_WS2812_STATUS_BUSY | RS_WS2812_STATUS_RESET_ACTIVE)) == 0U) { + return rs_ws2812_dma_finish(); + } + } + return rs_ws2812_transfer.result; + } + interrupts = rs_ws2812_read(RS_WS2812_INTR_STATE_OFFSET); + if (((dma_flags & (RS_DMA_STATUS_ERROR | RS_DMA_STATUS_ABORTED)) != 0U) || + (rs_ws2812_read(RS_WS2812_ERROR_STATUS_OFFSET) != 0U) || + ((interrupts & RS_WS2812_INTR_ABORTED) != 0U)) { + return rs_ws2812_dma_fail(RS_EIO); + } + if (now >= rs_ws2812_deadline) { + return rs_ws2812_dma_fail(RS_ETIMEOUT); + } + if (rs_ws2812_transfer.dma_pending) { + if ((dma_flags & RS_DMA_STATUS_BUSY) != 0U) { + return RS_OK; + } + if (((dma_flags & RS_DMA_STATUS_DONE) == 0U) || + (RS_DMA_CH_REG(RS_DMA_CHANNEL_BULK, RS_DMA_CH_REG_BYTES_DONE) != + (rs_ws2812_batch_words * sizeof(uint32_t)))) { + return rs_ws2812_dma_fail(RS_EIO); + } + rs_ws2812_transfer.dma_pending = false; + } + transmitter_flags = rs_ws2812_read(RS_WS2812_STATUS_OFFSET); + if ((interrupts & RS_WS2812_INTR_DONE) != 0U) { + if ((rs_ws2812_transfer.remaining != 0U) || + ((transmitter_flags & (RS_WS2812_STATUS_BUSY | RS_WS2812_STATUS_RESET_ACTIVE)) != 0U)) { + return rs_ws2812_dma_fail(RS_EIO); + } + return rs_ws2812_dma_finish(); + } + if ((transmitter_flags & RS_WS2812_STATUS_BUSY) == 0U) { + return rs_ws2812_dma_fail(RS_EIO); + } + if (rs_ws2812_transfer.remaining == 0U) { + rs_ws2812_write_register(RS_WS2812_INTR_STATE_OFFSET, RS_WS2812_INTR_FIFO_LOW); + return RS_OK; + } + level = rs_ws2812_read(RS_WS2812_FIFO_LEVEL_OFFSET); + if (level > 16U) { + return rs_ws2812_dma_fail(RS_EIO); + } + if (level > 8U) { + /* FIFO_LOW is sticky and set dominates W1C while occupancy is low. + * Clear the completed batch's event after the level condition is gone, + * before the caller rearms its interrupt. */ + rs_ws2812_write_register(RS_WS2812_INTR_STATE_OFFSET, RS_WS2812_INTR_FIFO_LOW); + return RS_OK; + } + if (rs_ws2812_refill_words(rs_ws2812_transfer.remaining, level, &words) != RS_OK) { + return rs_ws2812_dma_fail(RS_EIO); + } + if (words == 0U) { + return RS_OK; + } + const rs_dma_config_t config = { + .kind = RS_DMA_KIND_MM_TO_MM, + .request = RS_DMA_REQUEST_SOFTWARE, + .source = (uintptr_t)&rs_ws2812_pixels[rs_ws2812_next], + .destination = (uintptr_t)rs_ws2812_register(RS_WS2812_TXDATA_OFFSET), + .byte_count = words * sizeof(uint32_t), + .width = RS_DMA_WIDTH_32, + .source_increment = true, + .destination_increment = false, + .crc_enable = false, + .crc_final = false, + .crc_expected = 0U, + .priority = 1U, + .burst_beats = 1U, + }; + if ((rs_dma_session_configure(&rs_ws2812_dma_owner, &config) != RS_OK) || + (rs_dma_session_start(&rs_ws2812_dma_owner) != RS_OK)) { + return rs_ws2812_dma_fail(RS_EIO); + } + rs_ws2812_next += words; + rs_ws2812_transfer.remaining -= words; + rs_ws2812_batch_words = words; + rs_ws2812_transfer.dma_pending = true; + ++rs_ws2812_transfer.batches; + rs_ws2812_write_register(RS_WS2812_INTR_STATE_OFFSET, RS_WS2812_INTR_FIFO_LOW); + return RS_OK; +} + +rs_status_t rs_ws2812_write_dma(const uint32_t *pixels, size_t pixel_count, rs_timeout_t timeout) { + rs_status_t result; + if (rs_ws2812_transfer.active) { + return RS_EIO; + } + result = rs_ws2812_dma_begin(pixels, pixel_count, 0U, timeout); + if (result == RS_OK) { + for (uint64_t tick = 0U; tick <= timeout; ++tick) { + result = rs_ws2812_dma_service(tick); + if (!rs_ws2812_transfer.active || (result != RS_OK)) { + break; + } } } - status = rs_ws2812_start((uint32_t)pixel_count); - if (status != RS_OK) { - return status; - } - if (dma_words != 0U) { - status = rs_dma_start(RS_DMA_CHANNEL_BULK); - if (status == RS_OK) { - status = rs_dma_wait(RS_DMA_CHANNEL_BULK, timeout); - } - if (status != RS_OK) { - (void)rs_ws2812_abort(timeout); - return status; + if (rs_ws2812_transfer.active && rs_ws2812_transfer.draining) { + /* A separate bounded cleanup budget, never an unbounded wait. Persistent + * state keeps ownership if the bus or transmitter cannot be drained. */ + for (rs_timeout_t remaining = timeout; remaining != 0U; --remaining) { + (void)rs_ws2812_dma_service(timeout); + if (!rs_ws2812_transfer.active) { + break; + } } } - return rs_ws2812_wait(timeout); + return result; } void ip_ws2812_test(int argc, char **argv) { diff --git a/crt/src/hal/ws2812_math.c b/crt/src/hal/ws2812_math.c index 332359e4..9a159bf0 100644 --- a/crt/src/hal/ws2812_math.c +++ b/crt/src/hal/ws2812_math.c @@ -3,6 +3,14 @@ #include +rs_status_t rs_ws2812_refill_words(uint32_t remaining, uint32_t level, uint32_t *words) { + if ((words == NULL) || (level > 16U)) { + return RS_EINVAL; + } + *words = (remaining < (16U - level)) ? remaining : (16U - level); + return RS_OK; +} + #define RS_NANOSECONDS_PER_SECOND UINT64_C(1000000000) static rs_status_t rs_ws2812_cycles_nearest(uint32_t frequency_hz, uint32_t duration_ns, diff --git a/docs/ip/tiny-soc-r2-p3-review-fixes.md b/docs/ip/tiny-soc-r2-p3-review-fixes.md new file mode 100644 index 00000000..cc06926b --- /dev/null +++ b/docs/ip/tiny-soc-r2-p3-review-fixes.md @@ -0,0 +1,288 @@ +# TINY-R2-P3 review fixes and validation boundary + +This record covers the two P1 findings approved for follow-up on +`bab01352f8f7c80e39546b04e3ed37a3d927f475` plus the existing P3 worktree. +It does not approve Mini fabric enablement or advance TINY-R2-P4. + +## Retained Tiny Icarus finding: closed + +The previously running banked O2/no-LTO Icarus attempt completed successfully: + +`build/ics55-tiny-performance-2026-10-08-17-20-b6fb322b1bce/meta/tiny-r2-p3/runs/iverilog/attempt-zljt308n/record.json` + +All three exits are zero, with strict Tiny TEST_STATUS/SIM_TEST_PASS and no +forbidden error markers. Durations are 6229.576, 5619.359 and 5943.890 seconds. +Revalidation checks each log, command, image/model/tool artifact and all eight +workloads; the three samples also equal the retained Verilator samples at +`runs/verilator/attempt-wnwm6ynp/record.json`. The source revision is the original +retained image/model snapshot, not a replacement attribution to subsequent fixes. + +The replay audit is retained under the same variant at +`meta/tiny-r2-p3/review-fixes/retained-tiny.json` and `retained-tiny.log`. +The audit also verifies that the actual Tiny image's compilation source files +and linker inputs remain unchanged. WS2812 HAL/application and Mini-only DV +changes do not create a new Tiny integration result. This closes one selected +image's cross-simulator gate, not the full eight-configuration compiler matrix. + +## Mini WS2812 finding: diagnosed, still open + +The original 33-word frame consumed its sixteen preloaded words, then +underflowed. Source-bound diagnostic roots are: + +- `build/ics55-2026-10-08-17-35-d0c568d0142e/`: rebuilt ordinary Mini acceptance + image with diagnostic fields; no source-cache inference is needed. +- `build/ics55-2026-10-08-21-00-d0c568d0142e/`: the same configuration with + observation-only timestamps. FIFO-low occurred at PCLK cycle 450141; + service first read DMA status at 452268, and DMA START at 458446 was later + than underflow at 456621. +- `build/ics55-2026-10-08-21-00-042fb076f2e1/`, + `build/ics55-2026-10-08-21-00-425535420bc4/` and + `build/ics55-2026-10-08-21-00-2d7056520d6b/`: diagnostic firmware variants + replayed on the preceding Mini model to isolate software cost. Their different + firmware/configuration identities are retained; these are diagnostics, not + matched-profile acceptance. The last variant starts DMA before underflow, + yet records zero completed bytes and a DMA error, with no FIFO push observed. + +The transport is unavailable in current Mini PRODUCT hardware: + +1. `rtl/mini/top/soc_data_plane.sv` connects central DMA to I/O source 0, + through PCLK-to-HP CDC and an AXI32-to-64 upsizer, to data-plane master 2. +2. `rtl/mini/top/axi4_data_crossbar.sv::decode_target` only decodes SRAM, + SDRAM, QPI PSRAM, OPI PSRAM and XPI/Flash. WS2812 TXDATA at `0x10008010` + is unmapped and selects error target 5. This matches the current + [Mini data-plane contract](../axi4-interconnect.md), which gives central DMA + memory targets rather than a DMA-to-APB route. +3. Even with a new control route, `rtl/mini/top/axi42apb4.svh` currently accepts + only single-beat INCR. The shared DMA's specified one-beat FIXED FIFO + transfers would require a separate compatibility correction there as well. + +The new directed case in `tests/rtl/axi4_data_crossbar_tb.sv` drives the actual +production crossbar with master 2, ID `0x10`, address `0x10008010`, AWSIZE=2, +AWLEN=0 and AWBURST=FIXED. It verifies the unmapped target, master/address/write +attribution and returned SLVERR, while retaining the existing concurrency/ACL +checks. It reports: + +```text +MINI_DMA_MMIO_BOUNDARY master=2 address=10008010 target=5 reason=1 response=SLVERR +``` + +This diagnostic passes because it verifies the existing unsupported boundary; +it does not turn the failed Mini WS2812 DMA acceptance into a pass. Its gate +and raw trace are under +`build/ics55-2026-10-08-21-00-425535420bc4/meta/tiny-r2-p3/` +as `mini-dma-mmio-boundary.{json,log}` and `mini-mmio-route-trace.{json,log}`. +No production fabric, route, ACL, clock/reset, DMA hardware or dependency is +modified by this follow-up. + +## Software and verification corrections retained + +- Shared service reads only the state needed in the active lifecycle state; + diagnostic stall/CRC/remaining snapshots and DMA configuration initialization + are removed from no-work/pending hot paths. Full configuration initialization + still occurs before admission. +- Clear sticky FIFO-low after the actual low-level condition has ended, before + interrupt rearm; keep set-dominant W1C behavior. Preserve the original + `min(remaining, 16-L)` admission rule; the speculative four-word cap was + withdrawn and is not part of the implementation. +- The fixture prepares the invariant background job before START, keeps all + sixteen 256-byte jobs/burst-16/four-TCD traffic, enables only terminal + channel-3 DMA events, and uses a 100-us foreground deadline checked with + stable SAFE24 CPU cycle reads. The deliberate 600-us starvation interval is + measured after frame startup, not before validation/preload. +- Host coverage additionally checks stale FIFO-low acknowledgement and wire + DONE before delayed final DMA completion: neither may release the lease early. +- The observer's summary runs at finalization because the C++ harness need not + execute another PCLK edge after TEST_STATUS. Descriptor reads overlapping an + active refill window are counted separately. Positive Mini timing qualification + is still missing; these changes are not presented as closing its hardware gap. + +## Approved option A (2026-10-08) + +The original P3 plan incorrectly assumed that the selected Mini product could +execute the shared fixed-MMIO DMA path. Changing only scheduling or accepting +UART text cannot supply the missing route. The maintainer explicitly selected +option A ("按照A执行"), correcting the validation plan without enabling Mini MMIO. + +Approved P3-only correction: retain the failed Mini run and the explicit +unsupported-route diagnostic; validate the shared finite DMA service through +the existing host/standalone production-DMA/WS2812 fixtures, and use supported +Mini PIO/SDK compatibility checks. Keep product-integrated DMA/IRQ service +budget acceptance pending the platform that actually provides that path +(Tiny R2-P6). No failed DMA run becomes a passing PIO result. + +`app/apps/ci_smoke/ws2812_r2.c` now performs explicitly labeled Mini PIO +compatibility checks (4/16/33/65 words, actual DONE IRQ, underflow and abort). +The wrapper observes the complete serial waveform and reset-low intervals and +asserts zero DMA starts. Its finalization summary is required in addition to +the ordinary command exit, TEST_STATUS/SIM_TEST_PASS and forbidden-marker checks. +The before-change source archive is retained at +`build/ics55-tiny-performance-2026-10-08-17-20-b6fb322b1bce/meta/tiny-r2-p3/review-fixes/option-a-2026-10-08-22-41/before-change.json`. + +The software deadline/preconfiguration experiments described above refer to the +retained failed DMA fixture. They are not part of the new PIO measurement or a +claim that the absent Mini DMA route was repaired. Shared service lifecycle, +occupancy and delayed-response checks remain in the host and standalone suites. + +### Option A execution results + +The supported compatibility run uses +`build/ics55-2026-10-08-22-57-d0c568d0142e/`, profile `configs/ci/ics55.mk`, +SOC=MINI, PDK=ICS55, APP=ci_smoke, WS2812_P3_ACCEPTANCE=YES, HAVE_CSR=YES, +LINK_TYPE=ld2_all_sram and the locked Verilator with `--fast-flash`. +Firmware and simulator command exit successfully. The 329.941-second run has +TEST_STATUS/SIM_TEST_PASS, no forbidden markers and the additional required +PIO/observer markers: + +```text +WS2812_P3_MINI_PIO PASS code=0 irqs=5 dma_mmio=UNSUPPORTED +WS2812_P3_MINI_PIO_OBSERVER frames=6 completed=4 underflows=1 aborts=2 dma_starts=0 +``` + +All 4/16/33/65-word frames pass exact serial-waveform, length and reset-low +checks. The two additional starts exercise deliberate underflow and abort; +the underflow cleanup accounts for the second abort event. No DMA transaction +is started by this compatibility image. + +`meta/tiny-r2-p3/option-a-evidence.json` binds configuration, tools, ELF/BIN/HEX, +linker map, disassembly, simulator, source archive and input hashes, compile/run +results and both additional verdict checks. The capture is explicitly post-build: +487 Verilator input sizes/mtimes are rechecked against its compilation metadata +before the source snapshot is retained; it is not mislabeled a pre-build capture. + +Focused validation passes (`96 passed`), including the shared finite DMA/refill +fixtures, DMA register parity and the unsupported Mini MMIO boundary. Its exact +command/log/result are under +`build/ics55-2026-10-08-22-41-d0c568d0142e/meta/tiny-r2-p3/focused.{json,log}`. +C formatting/policy/host tests and RTL formatting/style/readiness pass under the +PIO variant's `meta/tiny-r2-p3/quality.{json,log}`. Ruff and `git diff --check` +also pass. No new MISRA deviation, dependency update, warning baseline or +metrics-policy change is introduced. + +The first option-A firmware compile failed because removing the DMA include +also removed a transitive declaration of UART_BPS. The explicit core/soc.h +include fixes the build; the failure log is retained in the 22-41 variant at +`meta/tiny-r2-p3/initial-build-missing-include.log`. It is not a GNU ISA failure +and no alternative compiler was downloaded or selected. + +The two review findings are addressed at their approved boundaries: retained +Tiny cross-simulator evidence is complete; the invalid positive Mini DMA gate +is replaced by explicitly approved, passing PIO compatibility plus the retained +unsupported-route diagnostic. Product DMA/IRQ budgets and the full P3 acceptance +matrix are still pending; this is not automatic phase acceptance. + +Not selected: separately preflight Mini central-DMA MMIO enablement. That work +must define routing, source identity, privileged-window ACLs, clock crossings, +accepted read/write drain and reset/ownership behavior, plus one-beat FIXED +handling at the APB boundary. It is outside the current P3 software repair and +must not be folded into Tiny's later fabric phases without approval. + +The full P3 compiler matrix, broader current-source regressions and final +phase acceptance remain open. Neither these diagnostics nor prior negative +STA observations qualify a new physical operating point. + +## Compiler-matrix execution corrections (2026-10-09) + +The initial flat-image collector could not read `_stack_point` from its symbol +listing. Flat placement now falls back to the retained linker map's explicit +`PROVIDE (_stack_point = .)` value; the exact SRAM endpoint and 4 KiB stack +headroom checks remain mandatory. Banked placement still requires its ELF symbol. + +LTO links exposed compiler-generated references to the freestanding runtime +after its otherwise-unused definitions had been removed. LTO selections now +retain the existing memory and RV32 arithmetic helper symbols through linker +`--undefined` options. No hosted runtime, ISA extension or dependency is added. +The failed flat/LTO attempts under the `2026-10-09-09-00`, `10-00` and `10-45` +experiment labels remain historical failures; labels are not elapsed-time data. + +The `2026-10-09-11-30` campaign was deliberately interrupted before long Icarus +coverage after its O2/LTO ordinary acceptance build exposed two self-owned +`-Wmaybe-uninitialized` warnings. Managed RTC/watchdog implementations write the +snapshot on every successful return; callers retain their error checks and now +explicitly initialize the two local snapshots. The matrix Make wrapper also +rejects self-owned C warnings from the ordinary firmware, in addition to the +existing separate measurement-image warning check. A negative test requires a +zero-exit Make command with such a warning to be recorded as failed. + +The interrupted master is +`build/ics55-tiny-performance-2026-10-09-11-30-53cc6674d532/meta/tiny-r2-p3/matrix/attempt-zpde30dv/record.json`. +Its last checkpoint can still say `running`; that is not a live-process verdict +or complete campaign acceptance. Completed samples retain their original source +identity and are not relabeled as measurements of the corrected source. + +### Current execution snapshot (2026-10-09, not phase acceptance) + +The replacement compiler campaign is +`build/ics55-tiny-performance-2026-10-09-09-25-53cc6674d532/meta/tiny-r2-p3/matrix/attempt-fc26_1s7/record.json`, +with source digest +`e817e2d264105068aecd22f49fa7d7016765878ace168a1329dcd4f9ad864370`. +All eight configurations complete ordinary Tiny acceptance and three deterministic +Verilator measurement runs. Selected Icarus groups remain in progress at this +snapshot; this is not yet a complete cross-simulator compiler matrix. + +Focused validation reports 111 passed. Full Pytest at the same source reports +1531 passed, 4 failed, 94 fixture errors and 1 skipped in 4250.64 seconds. +The exact command and complete trace are retained in the campaign root at +`meta/tiny-r2-p3/quality/pytest.{json,log}`. Diagnosed causes are: + +- `test_clock_reset_domain_inventory_matches_the_rcu` requires a literal + `AUD_CLK_HZ` declaration in every profile. The performance profile inherits + 24000000 from `ics55-tiny.mk`; its effective clock value is correct, but the + declaration gate fails. +- Publication API metadata still binds the old `rs_dma_start` body. The changed + lease/fence/command behavior needs semantic review, not just a digest refresh. +- Publication software metadata still binds the old assembly `premain` block. + This also prevents dependent system/diagram/retrieval fixtures from collecting. +- The publication C fixture extracts legacy DMA entrypoints without the newly + called `rs_dma_command` helper: 34 scenarios fail fixture compilation with + an implicit-declaration error. The remaining 60 fixture errors belong to + dependent publication collectors. +- One PDF inspection test skips because `pypdf` is unavailable in this environment. + +Focused diagnostics are retained beside the full result as +`clock-inventory-diagnostic.{json,log}` and `publication-diagnostic.{json,log}`. +The independent CLINT test passes; its initial diagnostic did not explain the +full-suite failure. No publication revision, PDF, warning baseline or dependency +is changed to turn these failures into passes. + +The IHP130 Tiny PR regression completes with exit zero under +`build/ihp130-tiny-2026-10-09-09-27-bff91e3529ba/`; its exact command and result are +`meta/tiny-r2-p3/regress-ihp130.{json,log}`. Behavioral Icarus includes JTAG +halt/resume, netlist boot requires Tiny SIM_TEST_PASS, and synthesis/STA commands +complete. Actual ci_smoke-variant metrics record 208545 cells, area +9109293.080998 in the synthesis report's units, max WNS -94.28 ns and max TNS +-1594880 ns. These are SAFE24 smoke observations, not timing closure or physical +qualification. The lint warning comparison in the separate `2dc2e5c747e5` +variant fails and remains a non-blocking regression observation. + +The ICS55 Tiny PR regression also completes with exit zero under +`build/ics55-tiny-2026-10-09-09-26-42d5beccb726/`, with exact command and result +at `meta/tiny-r2-p3/regress-ics55.{json,log}`. Its source-capture/platform audit +passes at `meta/tiny-ics55-p1/report.json`. The actual ci_smoke metrics record +197703 cells, area 1786703.340799 in the synthesis report's units, max WNS +-41.14 ns and max TNS -508669.53 ns. Behavioral Icarus, JTAG halt/resume, +netlist Tiny boot, synthesis and STA commands complete; negative timing margins +remain observations, not a qualified operating point. Relative to their retained +lint baselines, ICS55 reports 459 new signatures and IHP130 403, with zero +increased or resolved signatures in either comparison. These counts are baseline +differences, not attribution of every warning to the P3 implementation. + +The regression runner's final generic metrics observations use default bringup +variants. The numbers above instead come from explicit metric collection in the +actual APP=ci_smoke synthesis/STA variants, with their matching `meta/metrics.json`. +No missing result in a different variant is credited as a pass of these gates. + +Mini's first behavioral-only regression stops before RTL execution because SBT +cannot create its boot socket in the sandbox. Two escalation requests time out +in automatic approval review without executing. Inspecting the installed SBT +1.10.0 implementation identifies its `sbt.server.forcestart=true` fallback, which +continues without that boot socket while the generator still sets +`sbt.server.autostart=false`. The sandboxed retry uses that environment option +and the original locked sources. Both attempts are retained under +`build/ics55-2026-10-09-09-28-d1cacf6d3b62/meta/tiny-r2-p3/` as +`regress-mini.{json,log}` and `regress-mini-no-boot-socket.{json,log}`. +The retry completes with exit zero, including SDRAM/Verilator acceptance under +the `dc24d22ad53d` variant and Icarus assembly boot under `ec9e1f2e9c2c`, both with +the selected target's successful simulation checks. This behavioral-only run +provides no Mini synthesis, netlist, STA or physical evidence. Mini lint baseline +differences remain non-blocking observations, separately retained in the +`56f28cd9230c` variant's `meta/rtl-lint-warnings.json`. diff --git a/docs/ip/tiny-soc-r2-software-scheduling.md b/docs/ip/tiny-soc-r2-software-scheduling.md new file mode 100644 index 00000000..cbfd4f0b --- /dev/null +++ b/docs/ip/tiny-soc-r2-software-scheduling.md @@ -0,0 +1,205 @@ +# Tiny R2-P3 software and DMA scheduling + +This implements only [TINY-R2-P3](tiny-soc.md#tiny-r2-p3---software-and-dma-scheduling). +The approved source is `bab01352f8f7c80e39546b04e3ed37a3d927f475`; actual runs +must also retain the implementation input snapshot and worktree diff. Results +and human acceptance belong in the [ledger](tiny-soc-verification.md). + +The [P3 review-fix record](tiny-soc-r2-p3-review-fixes.md) documents a discovered +platform prerequisite: Mini PRODUCT central DMA has no APB/MMIO route. The +maintainer approved option A on 2026-10-08: retain the failed DMA integration +evidence, use the existing standalone DMA/WS2812 checks, and validate Mini's +supported PIO path. The Mini command below now selects explicitly labeled PIO +acceptance. It does not supply product-integrated DMA/IRQ service-budget evidence. + +## Platform and experiment boundary + +The primary profile is `configs/ci/ics55-tiny.mk`: Tiny, ICS55, HAVE_PLL=YES, +external SAFE24 with the PLL disabled. `configs/ci/ihp130-tiny.mk` remains the +explicit IHP130/no-PLL compatibility path. CPU and all 128 KiB main SRAM remain +on SYS24. Package, pads, IRQ/request/register ABIs and the private Crypto +storage contract are unchanged. WS2812/audio/Crypto are not added to Tiny here. + +`configs/benchmark/ics55-tiny-performance.mk` inherits the primary platform +and selects `SW_ISA_PROFILE=TINY_PERF`, `SW_OPT=O2`, `SW_LTO=NO`. These are +experiment defaults, not promotion of a new CI baseline. The candidate is +`-march=rv32imc_zicsr_zifencei_zba_zbb_zbc_zbkb_zbkx_zbs -mabi=ilp32`. +The locked GNU compiler is used first. A tool replacement requires retained +failure evidence and the authorized dependency-maintenance workflow; changing +compiler identity invalidates an otherwise matched comparison. + +`SW_ISA_PROFILE=COMPAT`, `SW_OPT=APP`, `SW_LTO=NO` preserve legacy selection, +including Tiny's effective `-Os`. Nondefault selections enter the canonical +variant key. No A, Zilsd, Zcb or Zcmp compiler selection, CPU parameter change, +faster operating point or half-rate SRAM is introduced. + +## Placement and startup + +`LINK_TYPE=ld2_all_sram` retains flat placement. `ld2_tiny_banked` partitions +the existing aperture for software experiments: + +| Region | Address range | Placement | +| --- | --- | --- | +| B0/B1 | `0x30000000..0x3000ffff` | Code, interrupt paths and read-only data | +| B2 | `0x30010000..0x30017fff` | CPU data/BSS and a reserved 4 KiB stack | +| B3 | `0x30018000..0x3001ffff` | `.data.rs_dma*` and `.bss.rs_dma*` buffers/descriptors | + +The banked startup walks explicit load/copy and zero tables, including DMA +initialized data and BSS. Copy boundaries are word aligned; TCDs/buffers are +64-byte aligned. Linker and ELF checks reject overflow, wrong placement and +stack overlap. The old flat linker naturally collects the annotated input +sections through its existing wildcards. These are placement regions, not +four hardware bank services; SRAM discovery still reports 32 physical macros. + +Tiny performs FENCE.I after relocation. The explicit assembler extension scope +keeps compatible RV32IM compilation valid on the existing Hazard3. Known-answer +ISA probes exercise M, C and each selected bit-manipulation extension, plus +store/FENCE.I/execute on SRAM code. The ordinary acceptance faulting load is +forced to 32 bits because its trap handler advances MEPC by four bytes; a +compressed load would skip the following return. This does not change trap ABI. + +## Shared ownership and finite service + +`rs_dma_session_*` adds single-hart software channel leases. Keep the session +object alive until release. Conflicting acquisition, legacy channel mutation +while leased, and release while BUSY fail. The existing Mini LP/HP hardware +owner must already allow access; software leases do not provide cross-hart +isolation. Direct MMIO that bypasses the HAL remains the caller's responsibility. +START fences published source data; release fences completed DMA data. IRQ +state acknowledgement is distinct from sticky terminal channel status. + +`rs_ws2812_dma_begin/service/status/cancel` operates one persistent transmitter +session in foreground context. IRQ handlers publish events; a periodic timer +also invokes service so an absent IRQ cannot disable the caller's deadline. +`now` and `deadline` are caller-defined monotonic 64-bit ticks without wrap. +RS_OK from service means progress; completion requires `status.active == false` +and `status.result == RS_OK`. The caller retains immutable pixels while active. + +The session reserves channel 3 across the entire frame, temporarily selects +watermark 8, preloads at most 16 words and starts the exact frame length. On +service it reads FIFO level L and submits at most `min(remaining, 16-L)` words. +Remaining excludes preloaded and already submitted words; hardware +REMAINING_WORDS is not used as DMA credit. CPU pushes and competing frame +starts are rejected while the session owns the transmitter. DMA configuration +is fully initialized, uses the existing software request and one-beat fixed +TXDATA writes, and waits for terminal status with BUSY clear before reuse. + +Channel 3 is released only after transmitter DONE, or after error recovery has +drained DMA including accepted write responses and completed transmitter abort +and reset-low. Cleanup never resets a busy channel. A stalled target returns +an error/timeout while persistent state retains ownership and buffer lifetime; +subsequent service calls may complete recovery. The blocking write_dma wrapper +uses this same engine with finite transfer and cleanup polling budgets. + +Application budgets specify channel owners, maximum bursts/chunks, descriptor +count and competing targets. The fixtures use up-to-16-beat background bursts, +256-byte chunks and finite four-descriptor chains. Priority cannot preempt an +accepted AXI write, an APB FIFO wait, a 16-beat descriptor fetch or an active +XPI command. There is no cyclic/2D engine or autonomous WS2812 request sideband. +Camera/audio/XPI concurrency without a verified bound is not advertised. + +## Measurements and evidence + +Keep the original P1 image and provenance unchanged. It retains sixteen +destinations and cannot fit entirely in B3. P3 uses a separate compact protocol: +1024 words, four retained destinations, 4096 xorshift iterations and seed +`0x12345678`. Its complete word-wise payload hash is `0xc4a58dc5`; all source, +destination and guard words are also checked. CPU hash remains `0xf6e37410`. + +The eight cases are empty-window calibration, CPU, CPU copy, DMA burst 1, +DMA burst 16, CPU/DMA contention, hardware TCD chain and 256-byte chunked DMA. +Each data case transfers 16384 bytes. Contention, TCD and chunked cases each +execute four CPU kernels, preserving CPU work when varying chunk size. The +TCD case fetches sixteen 64-byte descriptors in addition to payload bytes. +Initialization, full readback and UART reporting remain outside measurement +windows. No CPU accesses a DMA-owned destination. + +Record cycles, instructions, CPI, kernel time, DMA bytes, CPU/DMA admission +waits, AHB terminal latency and AXI address-to-response latency, descriptor +bytes and longest observed backpressure. CPU transactions straddling a window +remain explicitly censored. Independent-bank conflicts are unavailable. +Retain map/ELF section sizes, stack-usage files and stack-canary observation; +observed stack depth is not a formal call-graph bound. + +The compiler matrix comprises compatible Os/flat, performance O2/O3/Os with +and without LTO on flat placement, and performance O2/no-LTO/banked placement. +Every variant builds and runs ordinary Tiny acceptance, then runs its retained +P3 image for three Verilator cold starts. Compatible Os/flat, performance +O2+LTO/flat and performance O2/no-LTO/banked each additionally run three native +Icarus cold starts. Compare ISA-only, optimizer/LTO-only and placement-only +pairs explicitly; none is a matched-binary architectural improvement claim. + +Shared WS2812 evidence has three explicitly different layers: production HAL +host-MMIO tests; the production-DMA/WS2812 16-entry finite-refill fixture +(33 words, occupancy credits and delayed B); and Mini PIO compatibility selected +by `WS2812_P3_ACCEPTANCE=YES`. The latter runs 4/16/33/65-word PIO frames, a real +DONE interrupt, deliberate underflow and bounded abort recovery. Its observer +checks exact word/bit order, 30-cycle symbols, 8/17-cycle high times and at least +7200 cycles of reset-low at PCLK24, and rejects any DMA START. Its verdict labels +are `WS2812_P3_MINI_PIO`, never DMA or Tiny integration acceptance. + +The production crossbar diagnostic separately locks the current unsupported +Mini DMA-to-MMIO boundary. Original failed DMA firmware attempts and their +timing/descriptor observations remain historical failed evidence. At PCLK24, +eight words at 1.25 us/bit provide nominal 5760 cycles / 240 us slack; measured +product-integrated IRQ/scheduling/source/target budgets remain pending a platform +that actually supplies the DMA route, including Tiny R2-P6. Standalone BFM timing +is not CPU interrupt latency, and PIO success does not qualify that DMA budget. +No Mini fabric, DMA hardware or target-frequency change is authorized by option A. + +Evidence belongs below `build//meta/tiny-r2-p3/`: immutable attempt +directories, images with ELF/BIN/HEX/map/disassembly/symbols/stack files, normal +acceptance images, source archives and input hashes, locked tool identities, +runtime models, per-run logs/flow records, matrix records and `report.json`. +Image and model identities remain separate. The collector rejects missing, +duplicate, altered or mismatched samples; measurement_complete is not human +phase acceptance. Failed attempts and their actual budgets are retained. + +## Execution + +Use one timestamp for a fixed executable source; choose another after changes. +Run focused checks and software policy before long simulations. Missing tools +or skipped required fixtures cannot qualify a gate. + +```sh +source .cache/retrosoc/development/tiny-r2-p1/activate.sh +export BUILD_TIMESTAMP=$(date +%Y-%m-%d-%H-%M) +python3 -m pytest -q tests/test_tiny_r2_p3.py tests/test_tiny.py \ + tests/test_tiny_r2_baseline.py tests/test_dma.py \ + tests/test_dma_register_parity.py tests/test_ws2812.py +make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 \ + sw-format-check sw-policy-check sw-host-test \ + rtl-format-check rtl-style-check rtl-readiness-check +ruff check . +make CONFIG=configs/benchmark/ics55-tiny-performance.mk SOC=TINY PDK=ICS55 \ + SOC_SIM_TIME=21600 JOBS=3 tiny-r2-p3-matrix +make CONFIG=configs/benchmark/ics55-tiny-performance.mk SOC=TINY PDK=ICS55 tiny-r2-p3-report +make CONFIG=configs/ci/ihp130-tiny.mk SOC=TINY PDK=IHP130 \ + SW_ISA_PROFILE=TINY_PERF SW_OPT=O2 SW_LTO=NO LINK_TYPE=ld2_tiny_banked \ + SIMU=VERILATOR firmware sim +make CONFIG=configs/ci/ics55.mk SOC=MINI PDK=ICS55 APP=ci_smoke \ + WS2812_P3_ACCEPTANCE=YES HAVE_CSR=YES LINK_TYPE=ld2_all_sram \ + SIMU=VERILATOR VERILATOR_SIM_ARGS=--fast-flash SOC_SIM_TIME=3600 firmware sim +python3 scripts/regress.py --root . --suite pr --soc TINY --pdk ICS55 --dry-run +python3 scripts/regress.py --root . --suite nightly --soc TINY --pdk ICS55 --dry-run +python3 scripts/regress.py --root . --suite pr --soc TINY --pdk IHP130 --dry-run +python3 scripts/regress.py --root . --suite pr --soc MINI --pdk ICS55 --behavioral-only --dry-run +python3 -m pytest -q +python3 scripts/regress.py --root . --suite pr --soc TINY --pdk ICS55 +python3 scripts/regress.py --root . --suite pr --soc TINY --pdk IHP130 +python3 scripts/regress.py --root . --suite pr --soc MINI --pdk ICS55 --behavioral-only +git diff --check +``` + +For ICS55 synthesis/STA, follow the platform runbook's fresh capture/report +steps on the actual APP=ci_smoke variant. Those source-bound observations remain +separate from compiler workload variants. Tiny nightly currently expands to PR; +verify that before avoiding a duplicate long execution. Poll long Icarus groups +every 30 minutes. `TINY_P3_RESUME=YES` resumes only identical-source matrix work +after revalidating retained results; retries use new child attempts. + +Tiny acceptance requires command success, TEST_STATUS/SIM_TEST_PASS and no +forbidden error markers. Mini assembly UART-only acceptance does not qualify +Tiny or these full C images. Keep warnings/metrics at their existing policy. +Pre-final negative timing is observational; no behavioral run qualifies a +physical frequency. Do not advance P4, stage/commit or publish automatically. diff --git a/docs/ip/tiny-soc-verification.md b/docs/ip/tiny-soc-verification.md index bb18d585..f95f9e08 100644 --- a/docs/ip/tiny-soc-verification.md +++ b/docs/ip/tiny-soc-verification.md @@ -1,5 +1,67 @@ # Tiny MCU Verification Record +## TINY-R2-P3 implementation in progress (2026-10-08–09) + +The 2026-10-09 execution snapshot in the [P3 review-fix record](tiny-soc-r2-p3-review-fixes.md) +records all eight ordinary Tiny/Verilator compiler experiments completed, with +selected Icarus groups still running. The current-source full Pytest result is +1531 passed, 4 failed, 94 fixture errors and 1 skipped; configuration-declaration +and publication source/fixture drift remain open. IHP130 Tiny PR completes with +non-blocking lint observations and negative STA margin. These results do not +close the remaining matrix, quality or human phase-acceptance gates. + +The [P3 review-fix record](tiny-soc-r2-p3-review-fixes.md) closes the retained +banked O2 Icarus finding: all three runs completed successfully and match the +three Verilator samples after artifact/log/command revalidation. The Mini +WS2812 finding remains open: the current Mini memory-only DMA data plane has +no route to WS2812 TXDATA, independently reproduced by the production crossbar +fixture. Its failed integration run is not relabeled a pass. The user explicitly +approved option A on 2026-10-08: retain this unsupported boundary, validate the +shared DMA algorithm through host/standalone checks, and use the supported Mini +PIO path for compatibility. No production fabric change is part of this follow-up; +product-integrated DMA/IRQ service budgets remain pending an actual route. + +Option A is implemented and its supported-path checks pass. Mini PIO evidence +is bound at +`build/ics55-2026-10-08-22-57-d0c568d0142e/meta/tiny-r2-p3/option-a-evidence.json`: +4/16/33/65-word frames, real DONE IRQ, exact serial waveform/reset-low, +underflow/abort recovery, zero DMA starts and strict TEST_STATUS/SIM_TEST_PASS. +Focused checks report 96 passed; C/RTL quality, Ruff and diff checks pass. +The old Mini DMA failure remains failed/unsupported evidence, not a PIO pass. +The full compiler/placement matrix, broader current-source regressions and human +phase acceptance remain pending. See the review-fix record for exact commands, +artifacts, historical attribution and unrun gates. + +The user explicitly approved the P3 preflight and implementation on source +`bab01352f8f7c80e39546b04e3ed37a3d927f475`, with a clean worktree at entry. +The specification blob is `7e19de49d49efaafacccfa85671e314d53dd15a4` and the +preflight ledger blob is `bb18d58598eb7e60a166c19fc9da60407ce4bd4c`. This authorizes +continuing from the implemented R2-P2 and ICS55-P1 SAFE24 foundation; it does not +retroactively turn their recorded human-review or physical gaps into passes. +Original phase IDs, historical evidence and qualification limits are retained. + +The [P3 runbook](tiny-soc-r2-software-scheduling.md) defines compiler/layout +experiments, shared finite DMA/WS2812 service, ownership and exact commands. +Implementation and validation are in progress; this entry does not claim +phase completion or performance improvement. The existing GNU toolchain is +used first, as requested; no dependency version has been changed. + +Early development testing found a compressed-instruction acceptance bug: +`rs_mcu_unmapped_probe` assembled its faulting load as 16-bit `4108`, while +the test trap handler advances MEPC by four bytes. The performance image +therefore skipped its return, first surfacing as I/O code 10 and, with diagnostic +layout, a subsequent store-access fault. Explicit `.option norvc` around the +faulting load restores the intended fixed-width probe. The repaired performance +banked normal image passes strict Tiny acceptance. This is an acceptance-fixture +fix, not a CPU, register ABI or toolchain replacement. + +Provisional development evidence is under +`build/ics55-tiny-performance-2026-10-08-16-14-b6fb322b1bce/`; +`meta/tiny-r2-p3/` retains the first failure, separate diagnostic source/ELF +and the Mini SBT sandbox failure. These are development attempts, not the final +source-frozen compiler matrix. Mini SBT required the existing environment +restriction on Unix socket creation to be lifted for the same approved command. + ## TINY-ICS55-P1 implementation (2026-10-07–08) Implementation is based on committed source diff --git a/docs/ip/ws2812.md b/docs/ip/ws2812.md index 325613c1..de461f3a 100644 --- a/docs/ip/ws2812.md +++ b/docs/ip/ws2812.md @@ -15,12 +15,23 @@ Product routing and the actual PCLK frequency are supplied to the common GPIO/timing APIs; they are not a second WS2812 driver. Tiny's binding and PCLK24/48/60 timing require their own implementation evidence. +The current Mini PRODUCT central DMA data plane has memory targets only and +does not route fixed-MMIO accesses to this APB window. The shared DMA/WS2812 +block tests do not establish that missing product path. See the +[P3 review-fix diagnosis](tiny-soc-r2-p3-review-fixes.md) for the reproduced +unmapped response and the still-failed Mini DMA acceptance. CPU PIO and the +controller/register contract remain separate from DMA transport availability. +The approved P3 option-A compatibility image verifies 4/16/33/65-word Mini PIO +frames, DONE IRQ and underflow/abort recovery with exact waveform/reset checks +and zero DMA starts. Its PIO pass does not qualify Mini DMA or future Tiny wiring. + Tiny's `TINY-R2-P3` scheduling contract selects FIFO-low watermark 8 and finite DMA batches bounded by observed free FIFO credits, with implementation and contention acceptance carried into `TINY-R2-P6`. See the linked product -contract for ownership, refill timing and recovery requirements. This is -planned shared transport work, not the behavior of the current full-tail DMA -helper. It preserves this IP's full-FIFO APB backpressure, register/FIFO ABI +contract for ownership, refill timing and recovery requirements. The shared +finite-refill implementation and its evidence boundary are described in the +[P3 runbook](tiny-soc-r2-software-scheduling.md). It preserves this IP's +full-FIFO APB backpressure, register/FIFO ABI and software request selection; an autonomous credit sideband remains deferred. | Property | Value | @@ -104,12 +115,18 @@ reset interval. `ABORT` has the same output-safety behavior and additionally sets the aborted interrupt state. `FIFO_FLUSH` is accepted only while idle. `rs_ws2812_write()` implements bounded CPU refill. `rs_ws2812_write_dma()` -preloads the FIFO, programs the generic DMA for incrementing source and fixed -`TXDATA` destination writes, then starts the frame and DMA. The DMA emits +drives the same finite-refill engine as the nonblocking +`rs_ws2812_dma_begin/service/status/cancel` interfaces. It reserves channel 3, +preloads the FIFO, and submits occupancy-bounded incrementing-source/fixed +`TXDATA` batches at watermark 8. The DMA emits native one-beat AXI4 `FIXED` writes for this APB4 endpoint and waits for its backpressure; it never sends an APB4 burst or uses RIB. Both paths validate the complete pixel array before modifying peripheral state and use caller timeouts -for all waits. +for all waits. Nonblocking callers provide monotonic ticks and a deadline; +service performs bounded work per call. Pixels and channel ownership remain +live until the complete DMA response drain and wire DONE/abort cleanup, even +after a timeout. The original watermark is restored on safe release. Tiny +integration and its full application acceptance remain R2-P6 work. ## Verification and current scope diff --git a/rtl/mini/dv/verilator/rtl/retrosoc_top.sv b/rtl/mini/dv/verilator/rtl/retrosoc_top.sv index 67590e5d..40bb28ac 100644 --- a/rtl/mini/dv/verilator/rtl/retrosoc_top.sv +++ b/rtl/mini/dv/verilator/rtl/retrosoc_top.sv @@ -186,4 +186,90 @@ module retrosoc_top ( .dq_io (s_sdram_dq) ); +`ifdef RETROSOC_WS2812_P3_OBSERVE + // Approved option A: actual Mini PIO frames and recovery, never DMA latency. + `define _P3_PERIPH u_retrosoc_asic.u_retrosoc.u_apb4_periph + `define _P3_WS `_P3_PERIPH.u_apb4_ws2812 + int unsigned s_p3_frames = 0; + int unsigned s_p3_done = 0; + int unsigned s_p3_underflows = 0; + int unsigned s_p3_aborts = 0; + int unsigned s_p3_dma_starts = 0; + int unsigned s_p3_word = 0; + int unsigned s_p3_bit = 0; + int unsigned s_p3_bit_cycle = 0; + int unsigned s_p3_reset_cycles = 0; + int unsigned s_p3_expected_words; + logic [23:0] s_p3_pixel; + logic s_p3_busy_previous = 1'b0; + logic s_p3_expected_high; + // Sample settled outputs halfway between active edges. The model has no + // timing delays and the production waveform is never driven by this observer. + always @(negedge u_retrosoc_asic.u_retrosoc.clk_pclk_i) begin + if (`_P3_WS.s_busy && !s_p3_busy_previous) begin + s_p3_frames = s_p3_frames + 1; + s_p3_word = 0; + s_p3_bit = 0; + s_p3_bit_cycle = 0; + s_p3_reset_cycles = 0; + end + if (`_P3_WS.s_underflow) s_p3_underflows = s_p3_underflows + 1; + if (`_P3_WS.s_aborted) s_p3_aborts = s_p3_aborts + 1; + if (`_P3_PERIPH.u_apb4_dma.s_start != '0) s_p3_dma_starts = s_p3_dma_starts + 1; + if (`_P3_WS.s_busy && !`_P3_WS.s_reset_active && s_p3_frames <= 4) begin + s_p3_pixel = 24'(s_p3_word * 32'h010203); + s_p3_expected_high = s_p3_bit_cycle < (s_p3_pixel[23-s_p3_bit] ? 17 : 8); + if (`_P3_WS.s_dat !== s_p3_expected_high) + $fatal( + 1, + "SIM_TEST_FAIL Mini PIO waveform frame=%0d word=%0d bit=%0d cycle=%0d", + s_p3_frames, + s_p3_word, + s_p3_bit, + s_p3_bit_cycle + ); + s_p3_bit_cycle = s_p3_bit_cycle + 1; + if (s_p3_bit_cycle == 30) begin + s_p3_bit_cycle = 0; + s_p3_bit = s_p3_bit + 1; + if (s_p3_bit == 24) begin + s_p3_bit = 0; + s_p3_word = s_p3_word + 1; + end + end + end + if (`_P3_WS.s_reset_active) begin + if (`_P3_WS.s_dat !== 1'b0) $fatal(1, "SIM_TEST_FAIL Mini PIO reset-low"); + s_p3_reset_cycles = s_p3_reset_cycles + 1; + end + if (!`_P3_WS.s_busy && s_p3_busy_previous) begin + if (s_p3_reset_cycles < 7200) $fatal(1, "SIM_TEST_FAIL Mini PIO short reset interval"); + if (s_p3_frames <= 4) begin + case (s_p3_frames) + 1: s_p3_expected_words = 4; + 2: s_p3_expected_words = 16; + 3: s_p3_expected_words = 33; + default: s_p3_expected_words = 65; + endcase + if (s_p3_word != s_p3_expected_words || s_p3_bit != 0 || s_p3_bit_cycle != 0) + $fatal(1, "SIM_TEST_FAIL Mini PIO frame length"); + s_p3_done = s_p3_done + 1; + end + end + s_p3_busy_previous = `_P3_WS.s_busy; + end + // The C++ harness calls final() immediately on TEST_STATUS. + final begin + if (test_done_o && test_pass_o) begin + if (s_p3_frames != 6 || s_p3_done != 4 || s_p3_underflows != 1 || + s_p3_aborts < 1 || s_p3_dma_starts != 0) + $fatal(1, "SIM_TEST_FAIL incomplete Mini PIO observations"); + $display( + "WS2812_P3_MINI_PIO_OBSERVER frames=%0d completed=%0d underflows=%0d aborts=%0d dma_starts=%0d", + s_p3_frames, s_p3_done, s_p3_underflows, s_p3_aborts, s_p3_dma_starts); + end + end + `undef _P3_WS + `undef _P3_PERIPH +`endif endmodule diff --git a/rtl/mini/mk/verilator.mk b/rtl/mini/mk/verilator.mk index 1c654349..a45e3899 100644 --- a/rtl/mini/mk/verilator.mk +++ b/rtl/mini/mk/verilator.mk @@ -31,6 +31,9 @@ VERILATOR_FLAGS += --timescale "1ns/1ns" -Wno-fatal VERILATOR_FLAGS += -o $(BUILD_DIR)/emu VERILATOR_FLAGS += -Mdir $(SOC_COMPILE_HOME) VERILATOR_FLAGS += $(SOC_VSRC_INCLPATH) $(SOC_CXXFILES) $(SOC_VXXFILES) +ifeq ($(WS2812_P3_ACCEPTANCE),YES) +VERILATOR_FLAGS += -DRETROSOC_WS2812_P3_OBSERVE +endif RTL_LINT_FLAGS := --lint-only --no-timing --top-module $(SOC_VSRC_TOP) RTL_LINT_FLAGS += --assert --Wall --timescale "1ns/1ns" -Wno-fatal @@ -141,4 +144,4 @@ clean: python3 $(ROOT_PATH)/scripts/clean.py --root $(ROOT_PATH) --path $(BUILD_DIR) python3 $(ROOT_PATH)/scripts/clean.py --root $(ROOT_PATH) --path $(RTL_LINT_DIR) -.PHONY: lint rtl-lint check-rtl-lint comp sim debug-sim clean \ No newline at end of file +.PHONY: lint rtl-lint check-rtl-lint comp sim debug-sim clean diff --git a/rtl/mk/software.mk b/rtl/mk/software.mk index f3d01b9f..41ea82aa 100644 --- a/rtl/mk/software.mk +++ b/rtl/mk/software.mk @@ -7,13 +7,34 @@ OBJC = $(CROSS)objcopy DUMP = $(CROSS)objdump GCC_FLAGS := -std=gnu11 -Wall -Wextra \ - -Wl,-Bstatic,-T,$(LINK_TYPE).lds,--strip-debug,--gc-sections -O3 \ + -Wl,-Bstatic,-T,$(LINK_TYPE).lds,--strip-debug,--gc-sections $(if $(filter APP,$(SW_OPT)),-O3,-$(SW_OPT)) \ -ffreestanding -nostdlib -ffunction-sections -fdata-sections SW_WARN_FLAGS := -Wformat=2 -Wshadow -Wstrict-prototypes -Wmissing-prototypes \ -Wcast-align -Werror=implicit-function-declaration \ -Werror=return-type -Werror=incompatible-pointer-types \ -Werror=int-conversion -Werror=format +ifeq ($(filter $(SW_OPT),APP O2 O3 Os),) +$(error SW_OPT must be APP, O2, O3 or Os) +endif +ifeq ($(filter $(SW_LTO),NO YES),) +$(error SW_LTO must be NO or YES) +endif +ifeq ($(filter $(SW_ISA_PROFILE),COMPAT TINY_PERF),) +$(error SW_ISA_PROFILE must be COMPAT or TINY_PERF) +endif +ifeq ($(SW_ISA_PROFILE),TINY_PERF) +ifneq ($(SOC):$(ISA):$(HAVE_CSR),TINY:RV32IM:YES) +$(error TINY_PERF requires SOC=TINY ISA=RV32IM HAVE_CSR=YES) +endif +endif +ifeq ($(SW_LTO),YES) +GCC_FLAGS += -flto -fno-builtin +GCC_FLAGS += -Wl,--undefined=memset,--undefined=memcpy,--undefined=memmove +GCC_FLAGS += -Wl,--undefined=__clzsi2,--undefined=__divdi3 +GCC_FLAGS += -Wl,--undefined=__ffssi2,--undefined=__udivdi3,--undefined=__umoddi3 +endif + ifeq ($(ISA),RV32E) ISA_FLAGS := -mabi=ilp32e else @@ -40,6 +61,15 @@ ISA_FLAGS += -march=rv32im endif endif +ifeq ($(SW_ISA_PROFILE),TINY_PERF) +ISA_FLAGS := -march=rv32imc_zicsr_zifencei_zba_zbb_zbc_zbkb_zbkx_zbs -mabi=ilp32 +endif +ifeq ($(LINK_TYPE),ld2_tiny_banked) +ifneq ($(SOC),TINY) +$(error ld2_tiny_banked requires SOC=TINY) +endif +DEF_VAL += -DRS_TINY_BANKED +endif DEF_VAL += -DAPP_$(APP) ifeq ($(APU_ENABLE_P7),YES) DEF_VAL += -DRS_APU_RELEASE_P7 @@ -177,7 +207,14 @@ ifneq ($(strip $(APP_CRT_SRCS)),) CRT_SRCS := $(APP_CRT_SRCS) endif +ifneq ($(SW_OPT),APP) +APP_CFLAGS := $(filter-out -O0 -O1 -O2 -O3 -Os -Og -Oz,$(APP_CFLAGS)) +endif CFLAGS += $(APP_CFLAGS) +CFLAGS += -Wl,-Map,$(SW_BUILD_DIR)/$(FIRMWARE_NAME).map +ifneq ($(SW_ISA_PROFILE):$(SW_OPT):$(SW_LTO),COMPAT:APP:NO) +CFLAGS += -fstack-usage +endif APP_INC_DIRS += $(MPW_OUTPUT_DIR) ifneq ($(filter RV32E RV32I,$(ISA)),) @@ -266,4 +303,4 @@ firmware: $(FIRMWARE_ELF) firmware asm: | manifest -.PHONY: FORCE_VERSION \ No newline at end of file +.PHONY: FORCE_VERSION diff --git a/rtl/tiny/Makefile b/rtl/tiny/Makefile index d39deecf..5c72a341 100644 --- a/rtl/tiny/Makefile +++ b/rtl/tiny/Makefile @@ -175,3 +175,29 @@ 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 + +# Independent P3 experiment protocol; never rewrite or relabel a P1 image. +TINY_P3_TOOL := $(ROOT_PATH)/scripts/tiny_r2_p3.py +TINY_P3_IMAGE ?= +TINY_P3_RESUME ?= NO +tiny-r2-p3-image: firmware + $(PYTHON) $(TINY_P3_TOOL) --root $(ROOT_PATH) --variant-root $(VARIANT_ROOT) image -- \ + $(CC) $(CFLAGS) $(INC_PATH) $(CRT_SRCS) \ + $(ROOT_PATH)/app/apps/ci_smoke/tiny_r2_p3.c $(ROOT_PATH)/app/apps/ci_smoke/tiny_r2_p3_isa.S + +tiny-r2-p3-sim: | manifest + $(PYTHON) $(TINY_P3_TOOL) --root $(ROOT_PATH) --variant-root $(VARIANT_ROOT) simulate \ + --simulator $(SIM_TOOL_NAME) --timeout $(SOC_SIM_TIME) --jobs $(JOBS) \ + $(if $(TINY_P3_IMAGE),--image $(TINY_P3_IMAGE)) -- \ + 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-p3-matrix: | manifest + $(PYTHON) $(TINY_P3_TOOL) --root $(ROOT_PATH) --variant-root $(VARIANT_ROOT) matrix \ + --timestamp $(BUILD_TIMESTAMP) --timeout $(SOC_SIM_TIME) --jobs $(JOBS) \ + $(if $(filter YES,$(TINY_P3_RESUME)),--resume) + +tiny-r2-p3-report: + $(PYTHON) $(TINY_P3_TOOL) --root $(ROOT_PATH) --variant-root $(VARIANT_ROOT) report + +.PHONY: tiny-r2-p3-image tiny-r2-p3-sim tiny-r2-p3-matrix tiny-r2-p3-report diff --git a/rtl/tiny/dv/retrosoc_tiny_tb.sv b/rtl/tiny/dv/retrosoc_tiny_tb.sv index 08172052..8adbfac3 100644 --- a/rtl/tiny/dv/retrosoc_tiny_tb.sv +++ b/rtl/tiny/dv/retrosoc_tiny_tb.sv @@ -77,7 +77,11 @@ module retrosoc_tiny_tb; // 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 + if (ObserveUartAlways || (ObserveUartBaseline && ($test$plusargs( + "tiny_r2_baseline" + ) || $test$plusargs( + "tiny_r2_p3" + )))) begin forever begin @(negedge s_uart0_tx); #1627.604; @@ -98,7 +102,7 @@ module retrosoc_tiny_tb; 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"); + s_baseline_enabled = $test$plusargs("tiny_r2_baseline") || $test$plusargs("tiny_r2_p3"); if (s_baseline_enabled) begin if (!$value$plusargs( "baseline_tcd_base=%h", s_baseline_tcd_base diff --git a/rtl/tiny/mk/config.mk b/rtl/tiny/mk/config.mk index 1833a157..79848404 100644 --- a/rtl/tiny/mk/config.mk +++ b/rtl/tiny/mk/config.mk @@ -46,8 +46,8 @@ endif ifeq ($(filter $(APP),bringup ci_smoke),) $(error SOC=TINY supports APP=bringup or ci_smoke) endif -ifneq ($(LINK_TYPE),ld2_all_sram) -$(error SOC=TINY requires LINK_TYPE=ld2_all_sram) +ifeq ($(filter $(LINK_TYPE),ld2_all_sram ld2_tiny_banked),) +$(error SOC=TINY requires LINK_TYPE=ld2_all_sram or ld2_tiny_banked) endif ifneq ($(HAVE_CSR),YES) $(error SOC=TINY requires HAVE_CSR=YES) @@ -55,6 +55,9 @@ endif ifneq ($(ISA),RV32IM) $(error SOC=TINY firmware uses ISA=RV32IM) endif +ifneq ($(WS2812_P3_ACCEPTANCE),NO) +$(error SOC=TINY does not yet integrate WS2812) +endif ifneq ($(filter YES,$(APU_ENABLE_P7) $(NPU_P5_ACCEPTANCE) $(NPU_P6_ACCEPTANCE)),) $(error SOC=TINY does not include multimedia accelerators) endif diff --git a/scripts/run_c_tests.py b/scripts/run_c_tests.py index dcb18307..50795283 100644 --- a/scripts/run_c_tests.py +++ b/scripts/run_c_tests.py @@ -23,6 +23,7 @@ TEST_SOURCES = ( "crt/src/hal/gpio_math.c", "crt/src/hal/i2c_math.c", "crt/src/hal/ws2812_math.c", + "crt/src/hal/ws2812.c", "crt/src/hal/uart_math.c", "crt/src/hal/timer_math.c", "crt/src/hal/psram_math.c", @@ -32,6 +33,7 @@ TEST_SOURCES = ( "crt/src/hal/spisd_math.c", "crt/src/hal/i2s_math.c", "crt/src/hal/dma_math.c", + "crt/src/hal/dma.c", "crt/src/hal/crypto_lifecycle.c", "crt/src/hal/jpeg_math.c", "crt/src/hal/apu.c", @@ -105,6 +107,13 @@ def main() -> int: "-Wextra", "-Werror", "-fno-builtin", + "-fno-pie", + "-no-pie", + "-ffunction-sections", + "-fdata-sections", + "-Wl,--gc-sections", + "-DRS_DMA_TEST_MMIO", + "-DRS_WS2812_TEST_MMIO", "-DRS_APU_TEST_MMIO", "-DRS_CRYPTO_TEST_MMIO", "-I", diff --git a/scripts/tiny_r2_p3.py b/scripts/tiny_r2_p3.py new file mode 100644 index 00000000..ad9057c0 --- /dev/null +++ b/scripts/tiny_r2_p3.py @@ -0,0 +1,546 @@ +#!/usr/bin/env python3 +"""Source-bound Tiny P3 compiler/placement experiments, separate from P1 replay.""" + +from __future__ import annotations + +import argparse +from concurrent.futures import ThreadPoolExecutor, as_completed +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 baseline # noqa: E402 +from scripts.check_simulation import DEFAULT_FAILURE # noqa: E402 +from scripts.check_c_warnings import self_owned_warnings # noqa: E402 +from scripts.setup_helpers import sha256 # noqa: E402 + +PHASE = "TINY-R2-P3" +CASES = (*baseline.CASES, "chunk256") +PARAMETERS = dict(baseline.PARAMETERS, version=3, jobs=4) +MATRIX = [("compat-os-flat", "COMPAT", "Os", "NO", "ld2_all_sram")] +MATRIX += [(f"perf-{opt.lower()}-{'lto' if lto == 'YES' else 'plain'}-flat", + "TINY_PERF", opt, lto, "ld2_all_sram") + for opt in ("O2", "O3", "Os") for lto in ("NO", "YES")] +MATRIX += [("perf-o2-plain-banked", "TINY_PERF", "O2", "NO", "ld2_tiny_banked")] +ICARUS_CASES = {"compat-os-flat", "perf-o2-lto-flat", "perf-o2-plain-banked"} +dump, artifact, read_json = baseline.dump, baseline.artifact, baseline.read_json +execute, check_artifact = baseline.execute, baseline.check_artifact + + +def execute_make(root: Path, directory: Path, label: str, command: list[str]) -> dict: + """Run a Make flow directly; avoid nesting run_flow around Make's own flows.""" + log = directory / (label + ".log") + result = directory / (label + ".json") + completed = subprocess.run(command, cwd=root, text=True, capture_output=True, check=False) + log.write_text(completed.stdout + completed.stderr, encoding="utf-8") + warnings = self_owned_warnings(root, completed.stdout + completed.stderr) + record = {"status": "passed" if completed.returncode == 0 and not warnings else "failed", + "exit_code": completed.returncode, "command": command, + "self_owned_warnings": warnings} + dump(result, record) + if completed.returncode != 0: + raise ValueError(f"Make flow failed; retained result: {result}") + if warnings: + raise ValueError(f"self-owned compiler warnings; retained result: {result}") + return record + + +def configuration(variant: Path) -> dict: + manifest = read_json(variant / "meta/manifest.json") + cfg = manifest["configuration"] + required = dict(baseline.COMPATIBILITY) + required.pop("LINK_TYPE") + if cfg.get("PDK") == "ICS55": + required.update(PDK="ICS55", HAVE_PLL="YES") + if any(cfg.get(k) != v for k, v in required.items()) or cfg.get("LINK_TYPE") not in ( + "ld2_all_sram", "ld2_tiny_banked" + ): + raise ValueError("P3 requires an explicit Tiny SAFE24 macro configuration") + if cfg.get("SW_ISA_PROFILE", "COMPAT") not in ("COMPAT", "TINY_PERF"): + raise ValueError("unknown P3 ISA selection") + return manifest + + +def inputs(root: Path, variant: Path) -> dict: + result = baseline.source_inputs(root, configuration(variant)["configuration"]["PDK"]) + for path in (root / "configs").rglob("*.mk"): + result[str(path.relative_to(root))] = sha256(path) + result["docs/ip/tiny-soc.md"] = sha256(root / "docs/ip/tiny-soc.md") + extra = subprocess.check_output( + ["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", "rtl/mini", "tests"], + cwd=root).decode().split("\0") + for name in extra: + path = root / name + if name and path.is_file() and path.suffix not in (".md", ".png", ".pdf"): + result[name] = sha256(path) + return result + + +def evidence(variant: Path) -> Path: + return variant / "meta/tiny-r2-p3" + + +def new_record(variant: Path, group: str) -> tuple[Path, dict]: + directory = baseline.new_attempt(evidence(variant) / group, "attempt-") + dump(directory.parent / "latest.json", {"path": str(directory / "record.json")}) + record = {"schema_version": 1, "phase": PHASE, "status": "running"} + dump(directory / "record.json", record) + return directory, record + + +def selected(variant: Path, group: str) -> Path: + return Path(read_json(evidence(variant) / group / "latest.json")["path"]) + + +def symbol(symbols: str, name: str) -> tuple[int, int]: + match = re.search(r"^([0-9a-f]+)(?: ([0-9a-f]+))? [A-Za-z] " + re.escape(name) + + r"(?:\.lto_priv\.\d+)?$", symbols, re.M) + if match is None: + raise ValueError(f"missing ELF symbol: {name}") + return int(match[1], 16), int(match[2] or "0", 16) + + +def check_layout(symbols: str, banked: bool, stack_point_override: int | None = None) -> dict: + names = ("rs_p3_source", "rs_p3_destinations", "rs_p3_tcds", "rs_p3_dma_cookie") + locations = {name: symbol(symbols, name) for name in names} + low, high = (0x30018000, 0x30020000) if banked else (0x30000000, 0x30020000) + for name, (address, size) in locations.items(): + if size == 0 or address < low or address + size > high: + raise ValueError(f"DMA placement outside declared aperture: {name}") + if name != "rs_p3_dma_cookie" and address % 64: + raise ValueError(f"DMA buffer/TCD alignment: {name}") + tcd, length = locations["rs_p3_tcds"] + if length != 1024: + raise ValueError("P3 requires exactly sixteen 64-byte descriptors") + stack = stack_point_override if stack_point_override is not None else symbol(symbols, "_stack_point")[0] + end, _ = symbol(symbols, "_ebss") + if stack != (0x30018000 if banked else 0x30020000) or end > stack - 4096: + raise ValueError("missing 4 KiB stack reserve") + if banked: + for name in ("main", "system_trap_entry", "rs_p3_isa_check", "rs_p3_patch_target"): + address, _ = symbol(symbols, name) + if not 0x30000000 <= address < 0x30010000: + raise ValueError(f"hot code outside B0/B1: {name}") + for start, stop in (("_copy_table_start", "_copy_table_end"), + ("_zero_table_start", "_zero_table_end")): + if symbol(symbols, stop)[0] <= symbol(symbols, start)[0]: + raise ValueError("empty startup initialization table") + return {"placements": locations, "descriptor_range": [tcd, tcd + length], + "stack_reserved": 4096, "stack_headroom": stack - end} + + +def check_exception_probe(disassembly: str) -> None: + probe = re.search(r":\n(.*?)(?:\n\n|\Z)", disassembly, re.S) + if probe is None or not re.search(r"\b00052503\s+lw\s+a0,0\(a0\)", probe[1]): + raise ValueError("exception probe must retain a 32-bit faulting load for MEPC += 4") + + +def image(args: argparse.Namespace) -> int: + root, variant = args.root, args.variant_root + directory, record = new_record(variant, "images") + try: + before = inputs(root, variant) + generated = {str(p.relative_to(root)): sha256(p) + for folder in (variant / "generated/tiny", variant / "sw/include") + for p in folder.rglob("*") if p.is_file()} + record.update(configuration=configuration(variant), tools=baseline.tools_identity(root), + source_digest=baseline.digest_map(before), + source=baseline.snapshot(root, directory / "inputs", before | generated)) + elf = directory / "tiny_p3.elf" + command = args.command[1:] if args.command[:1] == ["--"] else args.command + if not command: + raise ValueError("compiler command required") + if Path(shutil.which(command[0]) or command[0]).resolve() != Path( + record["tools"]["riscv_gnu"]["executable"]["path"] + ).resolve(): + raise ValueError("compiler command does not use the locked GNU executable") + command = [*command, "-Wl,-Map," + str(directory / "tiny_p3.map"), "-o", str(elf)] + execute(root, directory, "compile", command, cwd=variant / "sw", quiet=True) + if self_owned_warnings(root, (directory / "compile.log").read_text()): + raise ValueError("self-owned compiler warnings") + for suffix, form in (("hex", "verilog"), ("bin", "binary")): + execute(root, directory, "objcopy-" + suffix, + ["riscv32-unknown-elf-objcopy", "-O", form, str(elf), + str(directory / ("tiny_p3." + suffix))], quiet=True) + for label, tool, flags in (("symbols", "nm", ["-S", "-n"]), + ("disassembly", "objdump", ["-d"]), + ("sections", "objdump", ["-h"]), + ("attributes", "readelf", ["-A"]), + ("size", "size", ["-A"])): + execute(root, directory, label, ["riscv32-unknown-elf-" + tool, *flags, str(elf)], quiet=True) + execute(root, directory, "normal-disassembly", + ["riscv32-unknown-elf-objdump", "-d", str(variant / "sw/firmware")], quiet=True) + check_exception_probe((directory / "normal-disassembly.log").read_text()) + cfg = record["configuration"]["configuration"] + symbols_text = (directory / "symbols.log").read_text() + banked = cfg["LINK_TYPE"] == "ld2_tiny_banked" + try: + layout = check_layout(symbols_text, banked) + except ValueError as error: + if banked or "_stack_point" not in str(error): + raise + map_text = (directory / "tiny_p3.map").read_text() + stack_match = re.search(r"0x([0-9a-fA-F]+)\s+PROVIDE \(_stack_point = \.\)", map_text) + if stack_match is None: + raise + layout = check_layout(symbols_text, False, int(stack_match.group(1), 16)) + disassembly = (directory / "disassembly.log").read_text() + if cfg.get("SW_ISA_PROFILE") == "TINY_PERF": + for mnemonic in ("sh1add", "clz", "clmul", "pack", "xperm4", "bset"): + if not re.search(r"\s" + mnemonic + r"\s", disassembly): + raise ValueError(f"missing instruction evidence: {mnemonic}") + if before != inputs(root, variant) or any(sha256(root / p) != h for p, h in generated.items()): + raise ValueError("inputs changed during compilation") + record.update(status="passed", compiler_command=command, parameters=PARAMETERS, + layout=layout, artifacts={p.name: artifact(p) for p in directory.iterdir() + if p.is_file() and p.name != "record.json"}) + (directory / "normal").mkdir() + for name in ("firmware", "retrosoc_fw.hex", "retrosoc_fw.map"): + shutil.copyfile(variant / "sw" / name, directory / "normal" / name) + record["normal_firmware"] = {name: artifact(directory / "normal" / name) + for name in ("firmware", "retrosoc_fw.hex", "retrosoc_fw.map")} + except (OSError, ValueError, subprocess.SubprocessError) as error: + record.update(status="failed", error=str(error)) + raise + finally: + dump(directory / "record.json", record) + print(f"P3 image retained: {directory}", flush=True) + return 0 + + +def parse_log(log: str, flow: dict) -> dict: + if flow.get("status") != "passed" or flow.get("exit_code") != 0 or DEFAULT_FAILURE.search(log): + raise ValueError("failed simulator command or forbidden marker") + if "SIM_TEST_PASS Tiny" not in log or "P3_ERROR" in log or log.count("P3_ISA PASS") != 1: + raise ValueError("missing strict Tiny/ISA acceptance") + parsed = {"cases": [], "axi": [], "ahb": []} + headers = re.findall(r"^P3_HEADER (.+)$", log, re.M) + if len(headers) != 1 or baseline.parse_fields("P3_HEADER " + headers[0], set(PARAMETERS)) != PARAMETERS: + raise ValueError("wrong experiment parameters") + for line in log.splitlines(): + for prefix, key, fields, strings in ( + ("P3_CASE ", "cases", baseline.CASE_FIELDS, {"name"}), + ("R2_AXI ", "axi", baseline.AXI_FIELDS, set()), + ("R2_AHB ", "ahb", baseline.AHB_FIELDS, set()), + ): + if line.startswith(prefix): + parsed[key].append(baseline.parse_fields(line, fields, strings=strings)) + parsed["cases"].sort(key=lambda row: row["id"]) + if len(parsed["cases"]) != 8 or log.count("P3_COMPLETE version=3 cases=8") != 1: + raise ValueError("incomplete experiment") + for index, row in enumerate(parsed["cases"]): + if row["id"] != index or row["name"] != CASES[index] or row["sram_errors"]: + raise ValueError("wrong case or SRAM error") + checksum = 0 if index == 0 else 0xf6e37410 if index == 1 else 0xc4a58dc5 + calls = 1 if index == 1 else 4 if index in (5, 6, 7) else 0 + if (row["checksum"] != checksum or row["payload_bytes"] != (16384 if index >= 2 else 0) + or row["kernel_calls"] != calls or not row["cycles"] or not row["instructions"]): + raise ValueError("data/workload accounting mismatch") + for key, kind in (("axi", "owner"), ("ahb", "kind")): + parsed[key].sort(key=lambda row: (row["window"], row[kind])) + if [(r["window"], r[kind]) for r in parsed[key]] != [(w, k) for w in range(1, 9) for k in range(2)]: + raise ValueError("missing or duplicate observer records") + for row in parsed["axi"]: + if row["owner"] == 1: + payload = 16384 if row["window"] >= 4 else 0 + descriptors = 1024 if row["window"] == 7 else 0 + if (row["read_bytes"] != payload or row["write_bytes"] != payload + or row["descriptor_bytes"] != descriptors or row["pending_reads"] + or row["pending_writes"] or row["reads"] != row["read_done"] + or row["writes"] != row["write_done"]): + raise ValueError("DMA payload/descriptor/drain accounting mismatch") + stack = re.findall(r"^P3_STACK reserved=4096 observed=(\d+)$", log, re.M) + if len(stack) != 1 or not 0 < int(stack[0]) < 4096: + raise ValueError("missing stack observation or exhausted stack") + parsed["stack_observed"] = int(stack[0]) + return parsed + + +def validate_image(path: Path) -> dict: + image_record = read_json(path) + if image_record.get("phase") != PHASE or image_record.get("status") != "passed": + raise ValueError("not a completed P3 image") + for item in image_record["artifacts"].values(): + check_artifact(item) + for item in image_record.get("normal_firmware", {}).values(): + check_artifact(item) + for name in ("inputs", "snapshot", "patch"): + check_artifact(image_record["source"][name]) + if read_json(path.parent / "compile.json")["command"] != image_record["compiler_command"]: + raise ValueError("compiler command provenance mismatch") + return image_record + + +def simulate(args: argparse.Namespace) -> int: + root, variant = args.root, args.variant_root + directory, record = new_record(variant, "runs/" + args.simulator) + try: + image_path = args.image or selected(variant, "images") + if image_path.suffix == ".hex": + image_path = image_path.parent / "record.json" + image_record = validate_image(image_path) + cfg = configuration(variant) + if image_record["configuration"]["configuration"] != cfg["configuration"]: + # Simulator selection is a flow, not an architecture/compiler input. + a, b = (dict(c["configuration"]) for c in (image_record["configuration"], cfg)) + for key in ("SIMU", "SYNTH", "STA", "SYNTH_RECIPE"): + a.pop(key, None) + b.pop(key, None) + if a != b: + raise ValueError("image and model configurations differ") + before = inputs(root, variant) + captured = read_json(check_artifact(image_record["source"]["inputs"])) + if any(captured.get(name) != digest for name, digest in before.items()): + raise ValueError("P3 image was not compiled from the current experiment source") + tools = baseline.tools_identity(root) + command = args.command[1:] if args.command[:1] == ["--"] else args.command + if not command: + raise ValueError("fresh model compilation command required") + execute_make(root, directory, "compile-model", command) + if before != inputs(root, variant): + raise ValueError("source changed while compiling model") + runtime = baseline.runtime_inputs(root, variant, args.simulator) + record.update(image=artifact(image_path), configuration=cfg, tools=tools, + source_digest=baseline.digest_map(before), + source=baseline.snapshot(root, directory / "inputs", before | runtime), runs=[]) + record["compilation"] = {suffix: artifact(directory / ("compile-model." + suffix)) + for suffix in ("log", "json")} + model = variant / "sim" / args.simulator / ("emu" if args.simulator == "verilator" else "simv") + record["model"] = artifact(model) + launch = [str(model)] + if args.simulator == "iverilog": + record["runtime_tool"] = baseline.vvp_identity("vvp", tools["iverilog"]) + launch.insert(0, record["runtime_tool"]["executable"]["path"]) + start, end = image_record["layout"]["descriptor_range"] + launch += ["+firmware=" + image_record["artifacts"]["tiny_p3.hex"]["path"], + "+tiny_r2_p3", f"+baseline_tcd_base={start:x}", f"+baseline_tcd_end={end:x}", + "+max_cycles=100000000"] + record.update(command=launch, timeout_seconds=args.timeout) + + def once(repetition: int) -> dict: + label = f"run-{repetition}" + check_artifact(record["model"]) + validate_image(image_path) + flow = execute(root, directory, label, + ["timeout", "--foreground", f"{args.timeout}s", *launch], quiet=True) + log = directory / (label + ".log") + samples = parse_log(log.read_text(), flow) + return {"repetition": repetition, "log": artifact(log), + "flow": artifact(directory / (label + ".json")), "samples": samples} + + with ThreadPoolExecutor(max_workers=min(args.jobs, 3)) as pool: + futures = [pool.submit(once, repeat) for repeat in (1, 2, 3)] + errors = [] + for future in as_completed(futures): + try: + record["runs"].append(future.result()) + except (OSError, ValueError, subprocess.SubprocessError) as error: + errors.append(str(error)) + dump(directory / "record.json", record) + if errors: + raise ValueError("; ".join(errors)) + record["runs"].sort(key=lambda run: run["repetition"]) + if any(run["samples"] != record["runs"][0]["samples"] for run in record["runs"]): + raise ValueError("cold-start samples are not deterministic") + if before != inputs(root, variant) or runtime != baseline.runtime_inputs(root, variant, args.simulator): + raise ValueError("source/model inputs changed during execution") + check_artifact(record["model"]) + validate_image(image_path) + record["status"] = "passed" + except (OSError, ValueError, subprocess.SubprocessError) as error: + record.update(status="failed", error=str(error)) + raise + finally: + dump(directory / "record.json", record) + print(f"P3 {args.simulator}: three deterministic runs at {directory}", flush=True) + return 0 + + +def validate_simulation(descriptor: dict, image_descriptor: dict, simulator: str) -> dict: + run = read_json(check_artifact(descriptor)) + image_record = validate_image(check_artifact(image_descriptor)) + if (run.get("status") != "passed" or run.get("phase") != PHASE or run["image"] != image_descriptor + or run["source_digest"] != image_record["source_digest"]): + raise ValueError("failed or mismatched simulation source/image") + check_artifact(run["model"]) + for item in run["compilation"].values(): + check_artifact(item) + if read_json(check_artifact(run["compilation"]["json"]))["status"] != "passed": + raise ValueError("unsuccessful model compilation") + source_map = read_json(check_artifact(run["source"]["inputs"])) + if baseline.digest_map(source_map) != run["source"]["input_digest"]: + raise ValueError("model input digest mismatch") + for name in ("snapshot", "patch"): + check_artifact(run["source"][name]) + for tool in run["tools"].values(): + check_artifact(tool["executable"]) + check_artifact(tool["archive"]) + expected = [run["model"]["path"]] + if simulator == "iverilog": + runtime = check_artifact(run["runtime_tool"]["executable"]) + if runtime.resolve().parent != Path(run["tools"]["iverilog"]["executable"]["path"]).resolve().parent: + raise ValueError("mixed Icarus runtime installation") + expected.insert(0, str(runtime)) + first, end = image_record["layout"]["descriptor_range"] + expected += ["+firmware=" + image_record["artifacts"]["tiny_p3.hex"]["path"], "+tiny_r2_p3", + f"+baseline_tcd_base={first:x}", f"+baseline_tcd_end={end:x}", "+max_cycles=100000000"] + if run["command"] != expected or run["timeout_seconds"] <= 0: + raise ValueError("wrong model/image/observer launch") + if sorted(r["repetition"] for r in run["runs"]) != [1, 2, 3]: + raise ValueError("missing/duplicate cold start") + samples = None + for repetition in run["runs"]: + flow = read_json(check_artifact(repetition["flow"])) + if flow["command"] != ["timeout", "--foreground", f"{run['timeout_seconds']}s", *expected]: + raise ValueError("launch provenance mismatch") + parsed = parse_log(check_artifact(repetition["log"]).read_text(), flow) + if parsed != repetition["samples"] or (samples is not None and samples != parsed): + raise ValueError("changed or mismatched samples") + samples = parsed + return samples + + +def check_acceptance(entry: dict) -> None: + flow = read_json(check_artifact(entry["acceptance"]["flow"])) + log = check_artifact(entry["acceptance"]["log"]).read_text() + if (flow.get("exit_code") != 0 or flow.get("status") != "passed" or DEFAULT_FAILURE.search(log) + or "SIM_TEST_PASS Tiny" not in log): + raise ValueError("normal Tiny application did not pass") + + +def matrix(args: argparse.Namespace) -> int: + source_digest = baseline.digest_map(inputs(args.root, args.variant_root)) + if args.resume: + path = selected(args.variant_root, "matrix") + directory, record = path.parent, read_json(path) + if record.get("source_digest") != source_digest or record.get("phase") != PHASE: + raise ValueError("resume requires identical source/specification/inputs") + record["status"] = "running" + else: + directory, record = new_record(args.variant_root, "matrix") + record.update(cases=[], source_digest=source_digest) + try: + if configuration(args.variant_root)["configuration"]["PDK"] != "ICS55": + raise ValueError("P3 compiler matrix uses the ICS55 default platform") + for name, isa, opt, lto, layout in MATRIX: + print(f"P3 matrix case={name} isa={isa} opt={opt} lto={lto} layout={layout}", flush=True) + command = ["make", "--no-print-directory", "CONFIG=configs/benchmark/ics55-tiny-performance.mk", + "SOC=TINY", "PDK=ICS55", "BUILD_TIMESTAMP=" + args.timestamp, + "SW_ISA_PROFILE=" + isa, "SW_OPT=" + opt, "SW_LTO=" + lto, + "LINK_TYPE=" + layout, "SOC_SIM_TIME=" + str(args.timeout), + "JOBS=" + str(args.jobs)] + config = subprocess.check_output([*command, "-s", "config"], cwd=args.root, text=True) + variant = Path(re.search(r"^VARIANT_ROOT\s+(.+)$", config, re.M)[1]) + entry = next((c for c in record["cases"] if c["name"] == name), None) + if entry is None: + entry = {"name": name, "variant": str(variant), "simulations": {}, "command": command} + record["cases"].append(entry) + if "image" not in entry: + print(f"P3 matrix {name}: image", flush=True) + execute_make(args.root, directory, name + "-image", [*command, "tiny-r2-p3-image"]) + entry["image"] = artifact(selected(variant, "images")) + validate_image(check_artifact(entry["image"])) + if "acceptance" not in entry: + print(f"P3 matrix {name}: acceptance", flush=True) + execute_make(args.root, directory, name + "-acceptance", + [*command, "SIMU=VERILATOR", "firmware", "sim"]) + entry["acceptance"] = {"flow": artifact(variant / "sim/verilator/result-sim.json"), + "log": artifact(variant / "sim/verilator/sim.log")} + check_acceptance(entry) + if "verilator" not in entry["simulations"]: + print(f"P3 matrix {name}: verilator", flush=True) + execute_make(args.root, directory, name + "-verilator", + [*command, "SIMU=VERILATOR", "tiny-r2-p3-sim"]) + entry["simulations"]["verilator"] = artifact(selected(variant, "runs/verilator")) + validate_simulation(entry["simulations"]["verilator"], entry["image"], "verilator") + dump(directory / "record.json", record) + print("P3 matrix completed: " + name, flush=True) + # Finish fast coverage before the three long native Icarus groups. + for entry in record["cases"]: + if entry["name"] not in ICARUS_CASES: + continue + if "iverilog" not in entry["simulations"]: + print(f"P3 matrix {entry['name']}: iverilog", flush=True) + execute_make(args.root, directory, entry["name"] + "-iverilog", + [*entry["command"], "SIMU=IVERILOG", "tiny-r2-p3-sim"]) + entry["simulations"]["iverilog"] = artifact(selected(Path(entry["variant"]), "runs/iverilog")) + validate_simulation(entry["simulations"]["iverilog"], entry["image"], "iverilog") + dump(directory / "record.json", record) + record["status"] = "passed" + except (OSError, ValueError, subprocess.SubprocessError) as error: + record.update(status="failed", error=str(error)) + raise + finally: + dump(directory / "record.json", record) + return 0 + + +def report(args: argparse.Namespace) -> int: + matrix_path = selected(args.variant_root, "matrix") + campaign = read_json(matrix_path) + if campaign.get("status") != "passed" or [c["name"] for c in campaign["cases"]] != [c[0] for c in MATRIX]: + raise ValueError("incomplete compiler matrix") + if campaign.get("source_digest") != baseline.digest_map(inputs(args.root, args.variant_root)): + raise ValueError("experiment source changed since matrix execution") + summary = [] + for case in campaign["cases"]: + image_record = validate_image(check_artifact(case["image"])) + check_acceptance(case) + samples = None + required = {"verilator", "iverilog"} if case["name"] in ICARUS_CASES else {"verilator"} + if set(case["simulations"]) != required: + raise ValueError("missing selected simulator") + for selected_sim, descriptor in case["simulations"].items(): + parsed = validate_simulation(descriptor, case["image"], selected_sim) + if samples is not None and samples != parsed: + raise ValueError("cross-simulator samples differ") + samples = parsed + summary.append({"name": case["name"], "image": case["image"], "layout": image_record["layout"], + "samples": samples, + "cpi": [r["cycles"] / r["instructions"] for r in samples["cases"]]}) + output = evidence(args.variant_root) / "report.json" + dump(output, {"phase": PHASE, "status": "measurement_complete", "matrix": artifact(matrix_path), + "cases": summary, "qualification": "Software experiments only; phase gates remain in ledger", + "bank_conflicts": "unavailable: P4 hardware is not implemented"}) + print(output) + return 0 + + +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_parser = commands.add_parser("image") + image_parser.add_argument("command", nargs=argparse.REMAINDER) + sim = commands.add_parser("simulate") + sim.add_argument("--simulator", choices=("verilator", "iverilog"), required=True) + sim.add_argument("--image", type=Path) + sim.add_argument("--timeout", type=int, default=21600) + sim.add_argument("--jobs", type=int, default=3) + sim.add_argument("command", nargs=argparse.REMAINDER) + campaign = commands.add_parser("matrix") + campaign.add_argument("--timestamp", required=True) + campaign.add_argument("--timeout", type=int, default=21600) + campaign.add_argument("--jobs", type=int, default=3) + campaign.add_argument("--resume", action="store_true") + commands.add_parser("report") + args = parser.parse_args() + args.root, args.variant_root = args.root.resolve(), args.variant_root.resolve() + if not args.variant_root.is_relative_to(args.root / "build"): + parser.error("P3 evidence must be below repository build/") + if getattr(args, "timeout", 1) <= 0 or getattr(args, "jobs", 1) <= 0: + parser.error("positive timeout/jobs required") + try: + return {"image": image, "simulate": simulate, "matrix": matrix, "report": report}[args.action](args) + except (OSError, ValueError, KeyError, subprocess.SubprocessError) as error: + print(f"P3: {error}", file=sys.stderr) + return 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/c/test_runtime.c b/tests/c/test_runtime.c index f09f7542..a1cf4bea 100644 --- a/tests/c/test_runtime.c +++ b/tests/c/test_runtime.c @@ -65,6 +65,197 @@ uint32_t rs_ga2d_test_mem_pad_mode; volatile uint32_t rs_npu_test_mmio[1024]; volatile uint32_t rs_fabric_monitor_test_mmio[1024]; volatile uint32_t rs_resource_test_mmio[1024]; +volatile uint32_t rs_dma_test_mmio[1024]; +static uint32_t ws_registers[16]; +static uint32_t ws_pushed; +static uint32_t ws_pixels[33]; + +uint32_t rs_ws2812_test_read(uint32_t offset) { + return ws_registers[offset / 4U]; +} + +void rs_ws2812_test_write(uint32_t offset, uint32_t value) { + if ((offset == 0x2cU) || (offset == 0x30U)) { + ws_registers[offset / 4U] &= ~value; + } else if (offset == 0x10U) { + ++ws_pushed; + ++ws_registers[0x20U / 4U]; + } else if (offset == 0x14U) { + if (value == 1U) { + ws_registers[0x18U / 4U] = 9U; + } else if (value == 2U) { + ws_registers[0x18U / 4U] = 8U; + ws_registers[0x30U / 4U] |= RS_WS2812_INTR_ABORTED; + } else if (value == 4U) { + ws_registers[0x20U / 4U] = 0U; + } + } else { + ws_registers[offset / 4U] = value; + } +} + +static void ws_test_reset(void) { + for (uint32_t index = 0U; index < 1024U; ++index) { + rs_dma_test_mmio[index] = 0U; + } + for (uint32_t index = 0U; index < 16U; ++index) { + ws_registers[index] = 0U; + } + ws_registers[0x3cU / 4U] = UINT32_C(0x18100100); + ws_registers[0x18U / 4U] = 8U; + ws_registers[0x24U / 4U] = 3U; + ws_pushed = 0U; +} + +static int test_dma_sessions(void) { + rs_dma_session_t first = {0}, second = {0}; + rs_dma_config_t config = {.kind = RS_DMA_KIND_MM_TO_MM, .source = 0x1000U, + .destination = 0x2000U, .byte_count = 32U, .width = RS_DMA_WIDTH_32, + .source_increment = true, .destination_increment = true, .burst_beats = 1U}; + ws_test_reset(); + if ((rs_dma_session_acquire(&first, 3U) != RS_OK) || + (rs_dma_session_acquire(&first, 2U) != RS_EIO) || + (rs_dma_session_acquire(&second, 3U) != RS_EIO) || + (rs_dma_configure(3U, &config) != RS_EIO) || (rs_dma_start(3U) != RS_EIO) || + (rs_dma_abort(3U) != RS_EIO) || (rs_dma_reset(3U) != RS_EIO) || + (rs_dma_session_configure(&first, &config) != RS_OK) || + (rs_dma_session_start(&first) != RS_OK)) { + return 1; + } + RS_DMA_CH_REG(3U, RS_DMA_CH_REG_STATUS) = RS_DMA_STATUS_BUSY; + if ((rs_dma_session_release(&first) != RS_EIO) || + (rs_dma_session_abort(&first) != RS_OK)) { + return 2; + } + RS_DMA_CH_REG(3U, RS_DMA_CH_REG_STATUS) = RS_DMA_STATUS_ABORTED; + if ((rs_dma_session_release(&first) != RS_OK) || + (rs_dma_session_release(&first) != RS_EIO) || + (rs_dma_session_acquire(&second, 3U) != RS_OK) || + (rs_dma_session_release(&second) != RS_OK)) { + return 3; + } + return 0; +} + +static int test_ws2812_refill(void) { + rs_ws2812_dma_status_t transfer; + rs_dma_session_t competitor = {0}; + uint32_t words; + for (uint32_t level = 0U; level <= 16U; ++level) { + for (uint32_t remaining = 0U; remaining <= 33U; ++remaining) { + if ((rs_ws2812_refill_words(remaining, level, &words) != RS_OK) || + (words > remaining) || ((words + level) > 16U) || + ((words != remaining) && ((words + level) != 16U))) { + return 1; + } + } + } + if ((rs_ws2812_refill_words(1U, 17U, &words) != RS_EINVAL) || + (rs_ws2812_refill_words(1U, 0U, NULL) != RS_EINVAL)) { + return 2; + } + ws_test_reset(); + ws_pixels[32] = UINT32_C(0xff000000); + if ((rs_ws2812_dma_begin(ws_pixels, 33U, 0U, 100U) != RS_EINVAL) || ws_pushed) { + return 3; + } + ws_pixels[32] = 0U; + if ((rs_ws2812_dma_begin(ws_pixels, 33U, 0U, 100U) != RS_OK) || (ws_pushed != 16U) || + (ws_registers[0x24U / 4U] != 8U) || (rs_ws2812_push(0U, 1U) != RS_EIO) || + (rs_ws2812_start(1U) != RS_EIO) || + (rs_dma_session_acquire(&competitor, 3U) != RS_EIO)) { + return 4; + } + ws_registers[0x30U / 4U] = RS_WS2812_INTR_FIFO_LOW; + (void)rs_ws2812_dma_service(1U); + (void)rs_ws2812_dma_status(&transfer); + if ((transfer.batches != 0U) || + ((ws_registers[0x30U / 4U] & RS_WS2812_INTR_FIFO_LOW) != 0U)) { + return 5; + } + /* Hardware REMAINING includes queued words: it must never size the batch. */ + ws_registers[0x28U / 4U] = 33U; + for (uint32_t batch = 0U; batch < 3U; ++batch) { + const uint32_t expected = (batch == 2U) ? 1U : 8U; + ws_registers[0x20U / 4U] = 8U; + if ((rs_ws2812_dma_service(2U + (batch * 3U)) != RS_OK) || + (RS_DMA_CH_REG(3U, RS_DMA_CH_REG_BYTE_COUNT) != expected * 4U) || + (RS_DMA_CH_REG(3U, RS_DMA_CH_REG_CRC_EXPECTED) != 0U)) { + return 6; + } + RS_DMA_CH_REG(3U, RS_DMA_CH_REG_STATUS) = RS_DMA_STATUS_BUSY | RS_DMA_STATUS_DONE; + ws_registers[0x20U / 4U] = 0U; + if (rs_ws2812_dma_service(3U + (batch * 3U)) != RS_OK) { + return 7; + } + (void)rs_ws2812_dma_status(&transfer); + if (!transfer.dma_pending || (transfer.batches != batch + 1U)) { + return 8; + } + RS_DMA_CH_REG(3U, RS_DMA_CH_REG_STATUS) = RS_DMA_STATUS_DONE; + RS_DMA_CH_REG(3U, RS_DMA_CH_REG_BYTES_DONE) = expected * 4U; + ws_registers[0x20U / 4U] = 16U; + if (rs_ws2812_dma_service(4U + (batch * 3U)) != RS_OK) { + return 9; + } + } + if (rs_dma_session_acquire(&competitor, 3U) != RS_EIO) { + return 10; + } + ws_registers[0x18U / 4U] = 8U; + ws_registers[0x30U / 4U] = RS_WS2812_INTR_DONE; + if ((rs_ws2812_dma_service(15U) != RS_OK) || + (rs_dma_session_acquire(&competitor, 3U) != RS_OK) || + (rs_dma_session_release(&competitor) != RS_OK) || + (ws_registers[0x24U / 4U] != 3U)) { + return 11; + } + /* Wire DONE may precede a delayed final B. Retain the lease until both + * completions have been observed, even if the FIFO is already empty. */ + ws_test_reset(); + if (rs_ws2812_dma_begin(ws_pixels, 17U, 0U, 100U) != RS_OK) { + return 15; + } + ws_registers[0x20U / 4U] = 8U; + (void)rs_ws2812_dma_service(1U); + RS_DMA_CH_REG(3U, RS_DMA_CH_REG_STATUS) = RS_DMA_STATUS_BUSY; + ws_registers[0x18U / 4U] = 8U; + ws_registers[0x30U / 4U] = RS_WS2812_INTR_DONE; + if ((rs_ws2812_dma_service(2U) != RS_OK) || + (rs_dma_session_acquire(&competitor, 3U) != RS_EIO)) { + return 16; + } + RS_DMA_CH_REG(3U, RS_DMA_CH_REG_STATUS) = RS_DMA_STATUS_DONE; + RS_DMA_CH_REG(3U, RS_DMA_CH_REG_BYTES_DONE) = 4U; + if ((rs_ws2812_dma_service(3U) != RS_OK) || + (rs_dma_session_acquire(&competitor, 3U) != RS_OK) || + (rs_dma_session_release(&competitor) != RS_OK)) { + return 17; + } + /* Timeout with an outstanding B cannot release either ownership or source. */ + ws_test_reset(); + if (rs_ws2812_dma_begin(ws_pixels, 33U, 0U, 3U) != RS_OK) { + return 12; + } + ws_registers[0x20U / 4U] = 8U; + (void)rs_ws2812_dma_service(1U); + RS_DMA_CH_REG(3U, RS_DMA_CH_REG_STATUS) = RS_DMA_STATUS_BUSY; + if ((rs_ws2812_dma_service(3U) != RS_ETIMEOUT) || + (rs_ws2812_dma_service(100U) != RS_ETIMEOUT) || + (rs_dma_session_acquire(&competitor, 3U) != RS_EIO)) { + return 13; + } + RS_DMA_CH_REG(3U, RS_DMA_CH_REG_STATUS) = RS_DMA_STATUS_ABORTED; + (void)rs_ws2812_dma_service(101U); + (void)rs_ws2812_dma_service(102U); + (void)rs_ws2812_dma_status(&transfer); + if (transfer.active || (transfer.result != RS_ETIMEOUT) || + (rs_dma_session_acquire(&competitor, 3U) != RS_OK) || + (rs_dma_session_release(&competitor) != RS_OK)) { + return 14; + } + return 0; +} #define APU_TEST_REG(offset) rs_apu_test_mmio[(offset) / 4U] #define GA2D_TEST_REG(offset) rs_ga2d_test_mmio[(offset) / 4U] @@ -2796,6 +2987,8 @@ int main(void) { test_crypto_lifecycle_contract(), test_clock_frequency_contract(), test_ws2812_helpers(), + test_dma_sessions(), + test_ws2812_refill(), test_timer_helpers(), test_psram_helpers(), test_sdram_helpers(), diff --git a/tests/rtl/axi4_data_crossbar_tb.sv b/tests/rtl/axi4_data_crossbar_tb.sv index b6b86719..fa1e36cb 100644 --- a/tests/rtl/axi4_data_crossbar_tb.sv +++ b/tests/rtl/axi4_data_crossbar_tb.sv @@ -329,6 +329,48 @@ module axi4_data_crossbar_tb; repeat (3) @(posedge clk_i); rst_n_i = 1'b1; + // Current Mini central DMA is master 2 of the memory-only data plane. + // Diagnose its existing MMIO boundary; this is not WS2812 DMA acceptance. + @(negedge clk_i); + fault_ready_i = 1'b0; + masters[2].awid = 7'h10; + masters[2].awaddr = 32'h1000_8010; + masters[2].awsize = 3'd2; + masters[2].awburst = 2'b00; + masters[2].awvalid = 1'b1; + #1; + if (!targets[5].awvalid || targets[0].awvalid || targets[1].awvalid || + targets[2].awvalid || targets[3].awvalid || targets[4].awvalid) + $fatal(1, "Mini DMA MMIO must retain the documented unmapped error route"); + do @(posedge clk_i); while (!masters[2].awready); + @(negedge clk_i); + masters[2].awvalid = 1'b0; + if (!fault_valid_o || fault_master_o != 4'd2 || fault_target_o != 3'd5 || + fault_reason_o != 4'd1 || fault_addr_o != 32'h1000_8010 || !fault_write_o) + $fatal(1, "Mini DMA MMIO fault attribution mismatch"); + masters[2].wdata = 64'h0011_2233; + masters[2].wstrb = 8'h0f; + masters[2].wvalid = 1'b1; + do @(posedge clk_i); while (!masters[2].wready); + @(negedge clk_i); + masters[2].wvalid = 1'b0; + targets[5].bid = 7'h10; + targets[5].bresp = 2'b10; + targets[5].bvalid = 1'b1; + #1; + if (!masters[2].bvalid || masters[2].bresp != 2'b10) + $fatal(1, "Mini DMA MMIO error response was lost"); + do @(posedge clk_i); while (!targets[5].bready); + @(negedge clk_i); + targets[5].bvalid = 1'b0; + targets[5].bresp = '0; + masters[2].awsize = 3'd3; + masters[2].awburst = 2'b01; + fault_ready_i = 1'b1; + @(posedge clk_i); + @(negedge clk_i); + $display("MINI_DMA_MMIO_BOUNDARY master=2 address=10008010 target=5 reason=1 response=SLVERR"); + @(negedge clk_i); master_block_i[1] = 1'b1; masters[1].arid = 6'b001_111; diff --git a/tests/rtl/ws2812_refill_tb.sv b/tests/rtl/ws2812_refill_tb.sv new file mode 100644 index 00000000..d293deec --- /dev/null +++ b/tests/rtl/ws2812_refill_tb.sv @@ -0,0 +1,416 @@ +`timescale 1ns / 1ps + +`include "ws2812_define.svh" + +module ws2812_refill_tb; + localparam logic [31:0] SourceBase = 32'h4000_0000; + localparam logic [31:0] Ws2812Txdata = 32'h1000_8010; + localparam int BitCycles = 30; + localparam int T0hCycles = 8; + localparam int T1hCycles = 17; + localparam int ResetCycles = 7200; + + typedef enum logic [2:0] { + AxiIdle, + AxiReadResponse, + AxiWriteWait, + AxiWriteSetup, + AxiWriteAccess, + AxiWriteResponse + } axi_state_e; + + logic clk_i = 1'b0; + logic rst_n_i = 1'b0; + logic dma_start = 1'b0; + logic [ 31:0] dma_cfg = '0; + logic [ 31:0] dma_src_addr = '0; + logic [ 31:0] dma_dst_addr = '0; + logic [ 31:0] dma_byte_count = '0; + logic [ 31:0] dma_request = '0; + logic [ 31:0] dma_burst = '0; + logic dma_done; + logic dma_error; + logic [ 3:0] dma_done_vector; + logic [ 3:0] dma_error_vector; + logic [127:0] dma_error_status_vector; + logic [127:0] dma_error_addr_vector; + logic host_active = 1'b1; + logic host_write_enable = 1'b1; + logic [ 3:0] dma_busy_vector; + integer s_b_delay_q = 0; + integer s_cycle = 0; + integer s_service_start; + integer s_service_max = 0; + integer s_submitted = 16; + integer s_batch_words; + logic [ 31:0] s_level; + logic host_psel = 1'b0; + logic host_penable = 1'b0; + logic [ 31:0] host_addr = '0; + logic [ 31:0] host_wdata = '0; + logic [ 3:0] host_wstrb = '0; + axi_state_e axi_state_q = AxiIdle; + logic [ 31:0] axi_read_addr_q = '0; + integer dma_write_count = 0; + integer dma_backpressure_cycles = 0; + logic [ 23:0] pixels [0:32]; + integer observed_pixel = 0; + integer observed_bit = 0; + integer observed_cycle = 0; + integer reset_low_cycles = 0; + + dma_req_if req (); + axi4_if axi4 ( + .aclk (clk_i), + .aresetn(rst_n_i) + ); + apb4_if apb4 ( + .pclk (clk_i), + .presetn(rst_n_i) + ); + ws2812_if ws2812 (); + axi4_stream_if i2s_tx_axis ( + .aclk (clk_i), + .aresetn(rst_n_i) + ); + axi4_stream_if i2s_rx_axis ( + .aclk (clk_i), + .aresetn(rst_n_i) + ); + axi4_stream_if dvp_rx_axis ( + .aclk (clk_i), + .aresetn(rst_n_i) + ); + axi4_stream_if crypto_in_axis ( + .aclk (clk_i), + .aresetn(rst_n_i) + ); + axi4_stream_if crypto_out_axis ( + .aclk (clk_i), + .aresetn(rst_n_i) + ); + + always #20.833 clk_i = ~clk_i; + + initial begin + repeat (1000000) @(posedge clk_i); + $fatal(1, "WS2812 DMA integration test timeout"); + end + + assign apb4.psel = host_active ? host_psel : + ((axi_state_q == AxiWriteSetup) || (axi_state_q == AxiWriteAccess)); + assign apb4.penable = host_active ? host_penable : (axi_state_q == AxiWriteAccess); + assign apb4.pwrite = host_active ? host_write_enable : 1'b1; + assign apb4.paddr = host_active ? host_addr : Ws2812Txdata; + assign apb4.pwdata = host_active ? host_wdata : axi4.wdata; + assign apb4.pstrb = host_active ? host_wstrb : axi4.wstrb; + assign apb4.pprot = '0; + + assign axi4.arready = axi_state_q == AxiIdle; + assign axi4.rid = '0; + assign axi4.rdata = pixels[axi_read_addr_q[7:2]]; + assign axi4.rresp = `AXI4_RESP_OKAY; + assign axi4.rlast = 1'b1; + assign axi4.ruser = '0; + assign axi4.rvalid = axi_state_q == AxiReadResponse; + assign axi4.awready = (axi_state_q == AxiIdle) && !axi4.arvalid; + assign axi4.wready = (axi_state_q == AxiWriteAccess) && apb4.pready; + assign axi4.bid = '0; + assign axi4.bresp = `AXI4_RESP_OKAY; + assign axi4.buser = '0; + assign axi4.bvalid = (axi_state_q == AxiWriteResponse) && (s_b_delay_q >= 12); + assign i2s_tx_axis.tready = 1'b0; + assign i2s_rx_axis.tdata = '0; + assign i2s_rx_axis.tkeep = '0; + assign i2s_rx_axis.tstrb = '0; + assign i2s_rx_axis.tlast = 1'b0; + assign i2s_rx_axis.tid = '0; + assign i2s_rx_axis.tdest = '0; + assign i2s_rx_axis.tuser = '0; + assign i2s_rx_axis.tvalid = 1'b0; + assign dvp_rx_axis.tdata = '0; + assign dvp_rx_axis.tkeep = '0; + assign dvp_rx_axis.tstrb = '0; + assign dvp_rx_axis.tlast = 1'b0; + assign dvp_rx_axis.tid = '0; + assign dvp_rx_axis.tdest = '0; + assign dvp_rx_axis.tuser = '0; + assign dvp_rx_axis.tvalid = 1'b0; + assign crypto_in_axis.tready = 1'b0; + assign crypto_out_axis.tdata = '0; + assign crypto_out_axis.tkeep = '0; + assign crypto_out_axis.tstrb = '0; + assign crypto_out_axis.tlast = 1'b0; + assign crypto_out_axis.tid = '0; + assign crypto_out_axis.tdest = '0; + assign crypto_out_axis.tuser = '0; + assign crypto_out_axis.tvalid = 1'b0; + + always_ff @(posedge clk_i or negedge rst_n_i) begin + if (!rst_n_i) begin + axi_state_q <= AxiIdle; + axi_read_addr_q <= '0; + dma_write_count <= 0; + dma_backpressure_cycles <= 0; + end else begin + s_b_delay_q <= (axi_state_q == AxiWriteResponse) ? s_b_delay_q + 1 : 0; + if (axi4.wvalid && !axi4.wready) begin + dma_backpressure_cycles <= dma_backpressure_cycles + 1; + end + unique case (axi_state_q) + AxiIdle: begin + if (axi4.arvalid && axi4.arready) begin + if ((axi4.arlen != 8'd0) || (axi4.araddr < (SourceBase + 32'd64)) || + (axi4.araddr > (SourceBase + 32'd128))) begin + $fatal(1, "DMA issued an invalid WS2812 source read"); + end + axi_read_addr_q <= axi4.araddr; + axi_state_q <= AxiReadResponse; + end else if (axi4.awvalid && axi4.awready) begin + if ((axi4.awlen != 8'd0) || (axi4.awaddr != Ws2812Txdata) || + (axi4.awburst != `AXI4_BURST_TYPE_FIXED)) begin + $fatal(1, "fixed WS2812 destination was not a single AXI4 FIXED beat"); + end + axi_state_q <= AxiWriteWait; + end + end + AxiReadResponse: begin + if (axi4.rvalid && axi4.rready) begin + axi_state_q <= AxiIdle; + end + end + AxiWriteWait: begin + if (axi4.wvalid) begin + axi_state_q <= AxiWriteSetup; + end + end + AxiWriteSetup: axi_state_q <= AxiWriteAccess; + AxiWriteAccess: begin + if (axi4.wvalid && axi4.wready) begin + if (!axi4.wlast) begin + $fatal(1, "single-beat WS2812 write did not assert WLAST"); + end + dma_write_count <= dma_write_count + 1; + axi_state_q <= AxiWriteResponse; + end + end + AxiWriteResponse: begin + if (axi4.bvalid && axi4.bready) begin + axi_state_q <= AxiIdle; + end + end + default: axi_state_q <= AxiIdle; + endcase + end + end + + dma_core #( + .NumChannels (4), + .MaxBurstBeats(16), + .FifoDepth (16) + ) u_dma_core ( + .clk_i (clk_i), + .rst_n_i (rst_n_i), + .global_reset_i (1'b0), + .global_error_clear_i (1'b0), + .ch_cfg_i ({dma_cfg, 96'd0}), + .src_addr_i ({dma_src_addr, 96'd0}), + .dst_addr_i ({dma_dst_addr, 96'd0}), + .byte_count_i ({dma_byte_count, 96'd0}), + .request_sel_i ({dma_request, 96'd0}), + .burst_cfg_i ({dma_burst, 96'd0}), + .start_i ({dma_start, 3'd0}), + .suspend_i ('0), + .resume_i ('0), + .abort_i ('0), + .channel_reset_i ('0), + .event_clear_i ('0), + .busy_o (dma_busy_vector), + .suspended_o (), + .done_o (dma_done_vector), + .aborted_o (), + .error_o (dma_error_vector), + .stream_last_o (), + .event_status_o (), + .error_status_o (dma_error_status_vector), + .error_addr_o (dma_error_addr_vector), + .current_src_o (), + .current_dst_o (), + .remaining_o (), + .bytes_done_o (), + .stall_cycles_lo_o (), + .stall_cycles_hi_o (), + .first_error_valid_o (), + .first_error_channel_o(), + .first_error_status_o (), + .first_error_addr_hi_o(), + .request_status_o (), + .xpi_xfer_done_o (), + .req (req), + .axi4 (axi4), + .i2s_tx_axis (i2s_tx_axis), + .i2s_rx_axis (i2s_rx_axis), + .dvp_rx_axis (dvp_rx_axis), + .crypto_in_axis (crypto_in_axis), + .crypto_out_axis (crypto_out_axis) + ); + + assign dma_done = dma_done_vector[3]; + assign dma_error = dma_error_vector[3]; + + apb4_ws2812 #( + .TxFifoDepth(16) + ) u_ws2812 ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .apb4 (apb4), + .ws2812 (ws2812) + ); + + task automatic host_write(input logic [31:0] address, input logic [31:0] data); + begin + @(negedge clk_i); + host_write_enable = 1'b1; + host_addr = address; + host_wdata = data; + host_wstrb = 4'hF; + host_psel = 1'b1; + host_penable = 1'b0; + @(negedge clk_i); + host_penable = 1'b1; + while (!apb4.pready) @(negedge clk_i); + if (apb4.pslverr) $fatal(1, "WS2812 host write %h failed", address); + host_psel = 1'b0; + host_penable = 1'b0; + end + endtask + + task automatic host_read(input logic [31:0] address, output logic [31:0] data); + @(negedge clk_i); + host_write_enable = 1'b0; + host_addr = address; + host_wstrb = '0; + host_psel = 1'b1; + host_penable = 1'b0; + @(negedge clk_i); + host_penable = 1'b1; + do @(negedge clk_i); while (!apb4.pready); + if (apb4.pslverr) $fatal(1, "occupancy read rejected"); + data = apb4.prdata; + host_psel = 1'b0; + host_penable = 1'b0; + endtask + + always @(posedge clk_i) begin + s_cycle = s_cycle + 1; + #1; + if ((axi_state_q == AxiWriteResponse) && (s_b_delay_q < 12) && dma_done) + $fatal(1, "DMA completed before delayed B response"); + if (rst_n_i && (u_ws2812.u_ws2812_reg.s_tx_count > 16)) $fatal(1, "FIFO credit overflow"); + if (rst_n_i && u_ws2812.s_busy && !u_ws2812.s_reset_active) begin + integer high_cycles; + logic expected_data; + + high_cycles = pixels[observed_pixel][23-observed_bit] ? T1hCycles : T0hCycles; + expected_data = observed_cycle < high_cycles; + if (ws2812.dat_o !== expected_data) begin + $fatal(1, "DMA waveform mismatch pixel=%0d bit=%0d cycle=%0d", observed_pixel, + observed_bit, observed_cycle); + end + observed_cycle = observed_cycle + 1; + if (observed_cycle == BitCycles) begin + observed_cycle = 0; + observed_bit = observed_bit + 1; + if (observed_bit == 24) begin + observed_bit = 0; + observed_pixel = observed_pixel + 1; + end + end + end + if (rst_n_i && u_ws2812.s_reset_active) begin + if (ws2812.dat_o !== 1'b0) begin + $fatal(1, "DMA frame reset interval drove the output high"); + end + reset_low_cycles = reset_low_cycles + 1; + end + if (dma_error) begin + $fatal(1, "DMA failed during WS2812 transfer status=%h address=%h", + dma_error_status_vector[31:0], dma_error_addr_vector[31:0]); + end + end + + initial begin + for (int pixel = 0; pixel < 33; pixel++) pixels[pixel] = 24'((pixel * 32'h010203) ^ 32'h800001); + req.i2s_tx_proc = 1'b1; + req.i2s_rx_proc = 1'b1; + req.qspi_tx_proc = 1'b1; + req.qspi_rx_proc = 1'b1; + req.uart_tx_proc = 1'b1; + req.uart_rx_proc = 1'b1; + req.i2c0_tx_proc = 1'b1; + req.i2c0_rx_proc = 1'b1; + req.i2c1_tx_proc = 1'b1; + req.i2c1_rx_proc = 1'b1; + req.crypto_in_proc = 1'b1; + req.crypto_out_proc = 1'b1; + + + repeat (3) @(posedge clk_i); + rst_n_i = 1'b1; + host_write(`APB4_WS2812_BIT_CYCLES, BitCycles); + host_write(`APB4_WS2812_T0H_CYCLES, T0hCycles); + host_write(`APB4_WS2812_T1H_CYCLES, T1hCycles); + host_write(`APB4_WS2812_RESET_CYCLES, ResetCycles); + host_write(`APB4_WS2812_FIFO_WATERMARK, 8); + host_write(`APB4_WS2812_INTR_ENABLE, 3); + for (int pixel = 0; pixel < 16; pixel++) host_write(`APB4_WS2812_TXDATA, {8'd0, pixels[pixel]}); + host_write(`APB4_WS2812_FRAME_WORDS, 33); + host_write(`APB4_WS2812_CTRL, 1); + + while (s_submitted < 33) begin + wait (ws2812.irq_o); + s_service_start = s_cycle; + // Declared scheduling latency; consumer-only draining makes an older L safe. + repeat (37) @(posedge clk_i); + host_read(`APB4_WS2812_FIFO_LEVEL, s_level); + if (s_level > 16) $fatal(1, "invalid occupancy"); + if (s_level <= 8) begin + s_batch_words = (33 - s_submitted < 16 - int'(s_level)) ? + 33 - s_submitted : 16 - int'(s_level); + if ((s_batch_words < 1) || (s_batch_words > 16 - int'(s_level))) + $fatal(1, "invalid finite batch"); + host_write(`APB4_WS2812_INTR_STATE, 2); + @(negedge clk_i); + host_active = 1'b0; + dma_cfg = {22'd0, 2'd1, 1'b0, 1'b1, 2'd2, 1'b0, 3'd0}; + dma_src_addr = SourceBase + 32'(s_submitted * 4); + dma_dst_addr = Ws2812Txdata; + dma_byte_count = 32'(s_batch_words * 4); + dma_request = '0; + dma_burst = 1; + dma_start = 1'b1; + @(negedge clk_i); + dma_start = 1'b0; + wait (dma_done && !dma_busy_vector[3]); + if ((s_cycle - s_service_start) > s_service_max) s_service_max = s_cycle - s_service_start; + s_submitted = s_submitted + s_batch_words; + @(negedge clk_i); + host_active = 1'b1; + end else begin + host_write(`APB4_WS2812_INTR_STATE, 2); + end + end + wait (!u_ws2812.s_busy); + @(posedge clk_i); + #1; + if ((dma_write_count != 17) || (observed_pixel != 33) || + (reset_low_cycles < ResetCycles) || !ws2812.irq_o || + (s_service_max >= 5760) || (u_ws2812.u_ws2812_reg.s_err_stat_q != 0)) begin + $fatal(1, "finite refill failed writes=%0d pixels=%0d latency=%0d", dma_write_count, + observed_pixel, s_service_max); + end + $display("WS2812_REFILL words=33 watermark=8 b_delay=12 service_max_cycles=%0d", s_service_max); + $display("WS2812 finite refill integration test passed"); + $finish; + end +endmodule diff --git a/tests/test_tiny_r2_p3.py b/tests/test_tiny_r2_p3.py new file mode 100644 index 00000000..ac5c61d1 --- /dev/null +++ b/tests/test_tiny_r2_p3.py @@ -0,0 +1,136 @@ +"""P3 rejects incompatible builds, incorrect measurements and unsafe placements.""" + +from pathlib import Path +import subprocess + +import pytest + +from scripts import tiny_r2_p3 as p3 + +ROOT = Path(__file__).resolve().parents[1] + + +def test_make_success_does_not_hide_normal_firmware_warnings(tmp_path, monkeypatch): + warning = f"{ROOT}/app/apps/ci_smoke/tiny.c:257:8: warning: snapshot may be used uninitialized\n" + monkeypatch.setattr(p3.subprocess, "run", lambda *args, **kwargs: + subprocess.CompletedProcess(args[0], 0, stdout="", stderr=warning)) + with pytest.raises(ValueError, match="self-owned compiler warnings"): + p3.execute_make(ROOT, tmp_path, "normal-image", ["make", "firmware"]) + result = p3.read_json(tmp_path / "normal-image.json") + assert result["exit_code"] == 0 + assert result["status"] == "failed" + assert len(result["self_owned_warnings"]) == 1 + + +def sample_log() -> str: + lines = ["P3_HEADER version=3 cpu_hz=24000000 words=1024 jobs=4 iterations=4096 seed=0x12345678"] + for i, name in enumerate(p3.CASES): + row = dict.fromkeys(p3.baseline.CASE_FIELDS, 0) + row.update(id=i, name=name, cycles=100, instructions=50, + checksum=0 if i == 0 else 0xf6e37410 if i == 1 else 0xc4a58dc5, + payload_bytes=16384 if i >= 2 else 0, + kernel_calls=1 if i == 1 else 4 if i in (5, 6, 7) else 0) + lines.append("P3_CASE " + " ".join(f"{k}={v}" for k, v in row.items())) + for owner in range(2): + axi = dict.fromkeys(p3.baseline.AXI_FIELDS, 0) + axi.update(window=i+1, owner=owner) + if owner == 1 and i >= 3: + axi.update(read_bytes=16384, write_bytes=16384, reads=1, writes=1, + read_done=1, write_done=1, descriptor_bytes=1024 if i == 6 else 0) + lines.append("R2_AXI " + " ".join(f"{k}={v}" for k, v in axi.items())) + ahb = dict.fromkeys(p3.baseline.AHB_FIELDS, 0) + ahb.update(window=i+1, kind=owner) + lines.append("R2_AHB " + " ".join(f"{k}={v}" for k, v in ahb.items())) + return "\n".join([*lines, "P3_STACK reserved=4096 observed=256", "P3_ISA PASS", + "P3_COMPLETE version=3 cases=8", "SIM_TEST_PASS Tiny code=0"]) + + +def test_complete_log_and_independent_payload_oracle(): + value = 2166136261 + for _ in range(4): + for index in range(1024): + value = ((value ^ (0xa519c300 ^ index)) * 16777619) & 0xffffffff + result = p3.parse_log(sample_log(), {"status": "passed", "exit_code": 0}) + assert result["cases"][2]["checksum"] == value + assert result["axi"][13]["descriptor_bytes"] == 1024 + + +@pytest.mark.parametrize("marker", ["FAILED", "FATAL", "assertion failed", "%Error", + "SIM_TEST_FAIL", "SIM_TEST_TIMEOUT", "P3_ERROR"]) +def test_later_pass_does_not_hide_error(marker): + with pytest.raises(ValueError): + p3.parse_log(marker + "\n" + sample_log(), {"status": "passed", "exit_code": 0}) + + +@pytest.mark.parametrize("old,new", [ + ("SIM_TEST_PASS Tiny code=0", "Hello retroSoC!"), + ("P3_ISA PASS", ""), ("jobs=4", "jobs=16"), + ("descriptor_bytes=1024", "descriptor_bytes=0"), + ("write_bytes=16384", "write_bytes=16380"), + ("pending_writes=0", "pending_writes=1"), + ("observed=256", "observed=4096"), + ("P3_COMPLETE version=3 cases=8", "P3_COMPLETE version=3 cases=7"), +]) +def test_incomplete_or_wrong_measurement_fails(old, new): + with pytest.raises(ValueError): + p3.parse_log(sample_log().replace(old, new), {"status": "passed", "exit_code": 0}) + + +def test_failed_command_and_duplicate_records_fail(): + with pytest.raises(ValueError): + p3.parse_log(sample_log(), {"status": "passed", "exit_code": 124}) + line = next(line for line in sample_log().splitlines() if line.startswith("P3_CASE")) + with pytest.raises(ValueError): + p3.parse_log(sample_log() + "\n" + line, {"status": "passed", "exit_code": 0}) + + +def layout_symbols() -> str: + return "\n".join([ + "30018000 00001080 b rs_p3_source", "30019080 00004200 b rs_p3_destinations", + "3001d280 00000400 b rs_p3_tcds", "3001d680 00000004 d rs_p3_dma_cookie", + "30018000 A _stack_point", "30010200 B _ebss", + "30000000 00000100 T main", "30000200 00000100 T system_trap_entry", + "30000400 00000100 T rs_p3_isa_check", "30000600 00000008 T rs_p3_patch_target", + "00000100 D _copy_table_start", "00000124 D _copy_table_end", + "00000124 D _zero_table_start", "00000134 D _zero_table_end", + ]) + + +def test_valid_banked_placement(): + assert p3.check_layout(layout_symbols(), True)["descriptor_range"] == [0x3001d280, 0x3001d680] + + +@pytest.mark.parametrize("old,new", [("3001d280", "3001d284"), ("30018000 00001080", "30017000 00001080"), + ("30010200", "30017800"), ("30000000 00000100", "30010000 00000100"), + ("00000400 b", "00001000 b"), ("00000124 D _copy_table_end", "00000100 D _copy_table_end")]) +def test_unsafe_placement_fails(old, new): + with pytest.raises(ValueError): + p3.check_layout(layout_symbols().replace(old, new), True) + + +@pytest.mark.parametrize("override", ["SW_ISA_PROFILE=RV32IMA", "SW_OPT=O0", "SW_LTO=maybe", + "LINK_TYPE=ld2_psram", "WS2812_P3_ACCEPTANCE=YES"]) +def test_tiny_rejects_invalid_experiment(override): + result = subprocess.run(["make", "-s", "CONFIG=configs/ci/ics55-tiny.mk", override, "config"], + cwd=ROOT, capture_output=True, text=True) + assert result.returncode != 0 + + +def test_performance_isa_is_not_enabled_for_mini(): + result = subprocess.run(["make", "-n", "CONFIG=configs/ci/ics55.mk", "SIMU=VERILATOR", + "SW_ISA_PROFILE=TINY_PERF", "firmware"], cwd=ROOT, + capture_output=True, text=True) + assert result.returncode != 0 + + +def test_eight_experiments_separate_isa_optimization_and_placement(): + assert len(p3.MATRIX) == 8 + assert {row[2:4] for row in p3.MATRIX if row[1] == "TINY_PERF"} == { + (opt, lto) for opt in ("O2", "O3", "Os") for lto in ("NO", "YES")} + + +def test_trap_probe_must_not_compress_under_performance_flags(): + normal = "30001000 :\n30001000: 00052503 lw a0,0(a0)\n30001004: 8082 ret\n" + p3.check_exception_probe(normal) + with pytest.raises(ValueError, match="32-bit"): + p3.check_exception_probe(normal.replace("00052503", "4108")) diff --git a/tests/test_ws2812.py b/tests/test_ws2812.py index fa95162d..a9c57108 100644 --- a/tests/test_ws2812.py +++ b/tests/test_ws2812.py @@ -7,6 +7,10 @@ import subprocess import sys from pathlib import Path +import pytest + +from scripts.check_simulation import DEFAULT_FAILURE + ROOT = Path(__file__).resolve().parents[1] @@ -62,11 +66,15 @@ def test_ws2812_register_waveform_streaming_and_errors(tmp_path: Path) -> None: assert "WS2812 register, waveform, streaming, and error test passed" in result.stdout -def test_ws2812_accepts_dma_fixed_destination_backpressure(tmp_path: Path) -> None: +@pytest.mark.parametrize("top,marker", [ + ("ws2812_dma_tb", "WS2812 native AXI4 DMA backpressure integration test passed"), + ("ws2812_refill_tb", "WS2812 finite refill integration test passed"), +]) +def test_ws2812_accepts_dma_fixed_destination_backpressure(tmp_path: Path, top: str, marker: str) -> None: iverilog = shutil.which("iverilog") vvp = shutil.which("vvp") - if iverilog is None or vvp is None: - return + if iverilog is None or vvp is None or shutil.which("sv2v") is None: + pytest.fail("required WS2812 DMA/refill simulator or converter unavailable") memory_map = tmp_path / "memory_map" subprocess.run( @@ -111,7 +119,7 @@ def test_ws2812_accepts_dma_fixed_destination_backpressure(tmp_path: Path) -> No str(serial / "ws2812_reg.sv"), str(serial / "ws2812_core.sv"), str(serial / "apb4_ws2812.sv"), - str(ROOT / "tests/rtl/ws2812_dma_tb.sv"), + str(ROOT / "tests/rtl" / (top + ".sv")), "", ] ), @@ -131,8 +139,9 @@ def test_ws2812_accepts_dma_fixed_destination_backpressure(tmp_path: Path) -> No ) simulation = tmp_path / "ws2812_dma_tb" subprocess.run( - [iverilog, "-g2012", "-s", "ws2812_dma_tb", "-o", str(simulation), str(converted)], + [iverilog, "-g2012", "-s", top, "-o", str(simulation), str(converted)], check=True, ) result = subprocess.run([vvp, str(simulation)], text=True, capture_output=True, check=True) - assert "WS2812 native AXI4 DMA backpressure integration test passed" in result.stdout + assert marker in result.stdout + assert not DEFAULT_FAILURE.search(result.stdout + result.stderr)