diff --git a/Makefile b/Makefile index c8248c45..08e45b04 100644 --- a/Makefile +++ b/Makefile @@ -86,6 +86,7 @@ SW_OPT ?= APP SW_LTO ?= NO WS2812_P3_ACCEPTANCE ?= NO TINY_P7_ACCEPTANCE ?= NO +TINY_P8_ACCEPTANCE ?= NO COREMARK_MODE ?= quick NPU_P5_ACCEPTANCE ?= NO NPU_P6_ACCEPTANCE ?= NO @@ -131,6 +132,9 @@ endif ifeq ($(TINY_P7_ACCEPTANCE),YES) CONFIG_KEY_VARS += TINY_P7_ACCEPTANCE endif +ifeq ($(TINY_P8_ACCEPTANCE),YES) +CONFIG_KEY_VARS += TINY_P8_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)) @@ -292,6 +296,12 @@ $(call validate_value,SW_OPT,APP O2 O3 Os) $(call validate_value,SW_LTO,$(VALID_BOOL)) $(call validate_value,WS2812_P3_ACCEPTANCE,$(VALID_BOOL)) $(call validate_value,TINY_P7_ACCEPTANCE,$(VALID_BOOL)) +$(call validate_value,TINY_P8_ACCEPTANCE,$(VALID_BOOL)) +ifeq ($(TINY_P8_ACCEPTANCE),YES) +ifneq ($(SOC)/$(APP)/$(HAVE_CSR)/$(TINY_P7_ACCEPTANCE),TINY/ci_smoke/YES/NO) +$(error TINY_P8_ACCEPTANCE requires SOC=TINY APP=ci_smoke HAVE_CSR=YES TINY_P7_ACCEPTANCE=NO) +endif +endif ifeq ($(TINY_P7_ACCEPTANCE),YES) ifneq ($(SOC)/$(APP)/$(HAVE_CSR),TINY/ci_smoke/YES) $(error TINY_P7_ACCEPTANCE requires SOC=TINY APP=ci_smoke HAVE_CSR=YES) @@ -1239,4 +1249,4 @@ regress-nightly: python3 $(ROOT_PATH)/scripts/regress.py --root $(ROOT_PATH) --suite nightly $(if $(filter TINY,$(SOC)),--soc TINY --pdk $(PDK)) sim-asm: asm - $(MAKE) SIM_FIRMWARE_NAME=$(ASM_FIRMWARE_NAME) sim \ No newline at end of file + $(MAKE) SIM_FIRMWARE_NAME=$(ASM_FIRMWARE_NAME) sim diff --git a/app/apps/ci_smoke/tiny.mk b/app/apps/ci_smoke/tiny.mk index 4478e0e2..f6995fac 100644 --- a/app/apps/ci_smoke/tiny.mk +++ b/app/apps/ci_smoke/tiny.mk @@ -4,4 +4,7 @@ APP_SRCS += $(ROOT_PATH)/app/apps/ci_smoke/mcu_acceptance.S APP_SRCS += $(ROOT_PATH)/app/apps/ci_smoke/tiny_r2_p6.c ifeq ($(TINY_P7_ACCEPTANCE),YES) APP_SRCS := $(ROOT_PATH)/app/apps/ci_smoke/tiny_r2_p7.c -endif \ No newline at end of file +endif +ifeq ($(TINY_P8_ACCEPTANCE),YES) +APP_SRCS := $(ROOT_PATH)/app/apps/ci_smoke/tiny_r2_p8.c +endif diff --git a/app/apps/ci_smoke/tiny_r2_p8.c b/app/apps/ci_smoke/tiny_r2_p8.c new file mode 100644 index 00000000..78613cae --- /dev/null +++ b/app/apps/ci_smoke/tiny_r2_p8.c @@ -0,0 +1,180 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#define RS_P8_WORDS UINT32_C(256) +#define RS_P8_ADDRESS UINT32_C(0x54000c00) +static _Alignas(64) uint32_t rs_p8_source[RS_P8_WORDS]; +static _Alignas(64) uint32_t rs_p8_readback[RS_P8_WORDS]; +static volatile uint32_t rs_p8_cpu_progress; + +typedef struct { + uint32_t sys_hz, mem_hz; + uint32_t isolated_cycles, contended_cycles, progress; +} rs_p8_measurement_t; +static rs_p8_measurement_t rs_p8_results[3]; + +static bool rs_p8_dma(rs_dma_session_t *session, uintptr_t source, uintptr_t destination, + bool contend, uint32_t *cycles) { + const rs_dma_config_t config = {.kind = RS_DMA_KIND_MM_TO_MM, + .request = RS_DMA_REQUEST_SOFTWARE, + .source = source, + .destination = destination, + .byte_count = sizeof(rs_p8_source), + .width = RS_DMA_WIDTH_32, + .source_increment = true, + .destination_increment = true, + .burst_beats = RS_XPI_PSRAM_BURST_BEATS}; + rs_dma_status_t state = {0}; + rs_timeout_t timeout = RS_TIMEOUT_DEFAULT; + uint64_t start; + if (rs_dma_session_configure(session, &config) != RS_OK || + rs_sysctrl_set_perf_control(true, true, false) != RS_OK) { + return false; + } + __FENCE(rw, rw); + start = __get_rv_cycle(); + if (rs_dma_session_start(session) != RS_OK) { + return false; + } + do { + if (contend) { + /* Defined competing SRAM work, with no race on the DMA buffers. */ + rs_p8_cpu_progress++; + } + if (timeout-- == 0U || rs_dma_get_status(session->channel, &state) != RS_OK) { + return false; + } + } while (state.busy); + *cycles = (uint32_t)(__get_rv_cycle() - start); + __FENCE(rw, rw); + if (rs_sysctrl_set_perf_control(false, false, false) != RS_OK) { + return false; + } + return state.done && !state.error && !state.aborted && + state.bytes_done == sizeof(rs_p8_source) && state.remaining == 0U; +} + +static bool rs_p8_profile(rs_tiny_rcu_profile_t profile, rs_p8_measurement_t *measurement) { + rs_tiny_rcu_snapshot_t clocks; + rs_xpi_psram_t device; + rs_dma_session_t session; + volatile uint32_t *const memory = (volatile uint32_t *)(uintptr_t)RS_P8_ADDRESS; + volatile uint32_t *const first = (volatile uint32_t *)(uintptr_t)RS_XPI_PSRAM_BASE; + volatile uint32_t *const last = (volatile uint32_t *)(uintptr_t)UINT32_C(0x547ffffc); + bool pass = true; + uint32_t unused_cycles; + if (rs_tiny_rcu_request_profile(profile, RS_TIMEOUT_DEFAULT) != RS_OK || + rs_tiny_rcu_get_snapshot(&clocks) != RS_OK || + rs_xpi_psram_initialize(clocks.mem_hz, RS_TIMEOUT_DEFAULT, &device) != RS_OK || + rs_xpi_psram_validate_dma(&device, RS_P8_ADDRESS, sizeof(rs_p8_source), 4U) != RS_OK) { + return false; + } + measurement->sys_hz = clocks.sys_hz; + measurement->mem_hz = clocks.mem_hz; + *first = 0U; + *(volatile uint8_t *)(uintptr_t)(RS_XPI_PSRAM_BASE + 1U) = UINT8_C(0xa5); + *(volatile uint16_t *)(uintptr_t)(RS_XPI_PSRAM_BASE + 2U) = UINT16_C(0xbeef); + *last = UINT32_C(0x2468ace0); + if (*first != UINT32_C(0xbeefa500) || *last != UINT32_C(0x2468ace0)) { + return false; + } + if (rs_dma_session_acquire(&session, RS_DMA_CHANNEL_BULK) != RS_OK) { + return false; + } + for (uint32_t round = 0U; round < 2U && pass; round++) { + for (uint32_t i = 0U; i < RS_P8_WORDS; i++) { + rs_p8_source[i] = UINT32_C(0x13579bdf) ^ (i * UINT32_C(0x01010101)) ^ round; + rs_p8_readback[i] = 0U; + } + *(volatile uint32_t *)(uintptr_t)(RS_P8_ADDRESS - 4U) = UINT32_C(0x55aa55aa); + memory[RS_P8_WORDS] = UINT32_C(0xaa55aa55); + rs_p8_cpu_progress = 0U; + pass = + rs_p8_dma(&session, (uintptr_t)rs_p8_source, RS_P8_ADDRESS, round != 0U, + round == 0U ? &measurement->isolated_cycles : &measurement->contended_cycles); + measurement->progress = rs_p8_cpu_progress; + for (uint32_t i = 0U; i < RS_P8_WORDS && pass; i++) { + pass = memory[i] == rs_p8_source[i]; + } + if (pass) { + pass = rs_p8_dma(&session, RS_P8_ADDRESS, (uintptr_t)rs_p8_readback, false, + &unused_cycles); + } + for (uint32_t i = 0U; i < RS_P8_WORDS && pass; i++) { + pass = rs_p8_readback[i] == rs_p8_source[i]; + } + pass = pass && + (*(volatile uint32_t *)(uintptr_t)(RS_P8_ADDRESS - 4U) == UINT32_C(0x55aa55aa)) && + (memory[RS_P8_WORDS] == UINT32_C(0xaa55aa55)); + } + if (!pass && rs_dma_abort_wait(session.channel, RS_TIMEOUT_DEFAULT) != RS_OK) { + return false; /* Static buffers remain owned; terminal failure follows. */ + } + return rs_dma_session_release(&session) == RS_OK && pass && measurement->progress != 0U; +} + +int main(void) { + const rs_gpio_config_t pin = {.mode = RS_GPIO_MODE_ALT0, + .pull = RS_GPIO_PULL_NONE, + .trigger = RS_GPIO_TRIGGER_NONE, + .output_high = true}; + rs_tiny_rcu_snapshot_t clocks; + rs_xpi_psram_t device; + rs_status_t status; + const uint32_t nor_word = *(volatile const uint32_t *)(uintptr_t)UINT32_C(0x50000000); + uint32_t count = 0U; + uint8_t code = 0U; + __disable_irq(); + if (rs_gpio_configure(29U, &pin) != RS_OK || rs_tiny_rcu_get_snapshot(&clocks) != RS_OK) { + code = 1U; + } else { + status = rs_xpi_psram_initialize(clocks.mem_hz, RS_TIMEOUT_DEFAULT, &device); + if (status == RS_ENOTSUP && !device.initialized && device.capacity_bytes == 0U) { + (void)rs_uart_init(24000000U, 921600U); + printf("P8_ABSENT bounded optional-device rejection\n"); + code = + (*(volatile const uint32_t *)(uintptr_t)UINT32_C(0x50000000) == nor_word) ? 0U : 2U; + } else if (status != RS_OK) { + code = 3U; + } else { + if (!rs_p8_profile(RS_TINY_RCU_PROFILE_SAFE24, &rs_p8_results[count++])) { + code = 4U; + } + if (code == 0U && (clocks.capability & 12U) == 12U) { + if (!rs_p8_profile(RS_TINY_RCU_PROFILE_PLL192, &rs_p8_results[count++]) || + !rs_p8_profile(RS_TINY_RCU_PROFILE_PLL240, &rs_p8_results[count++])) { + code = 5U; + } + } else if (code == 0U && clocks.xin_hz != 24000000U) { + if (!rs_p8_profile(RS_TINY_RCU_PROFILE_XIN, &rs_p8_results[count++])) { + code = 6U; + } + } + if (code == 0U && rs_tiny_rcu_request_profile(RS_TINY_RCU_PROFILE_SAFE24, + RS_TIMEOUT_DEFAULT) != RS_OK) { + code = 7U; + } + if (code == 0U && + *(volatile const uint32_t *)(uintptr_t)UINT32_C(0x50000000) != nor_word) { + code = 8U; + } + } + } + (void)rs_uart_init(24000000U, 921600U); + for (uint32_t i = 0U; i < count; i++) { + printf("P8_TRANSPORT sys_hz=%u mem_hz=%u bytes=1024 burst=4 isolated_cycles=%u " + "contended_cycles=%u progress=%u\n", + rs_p8_results[i].sys_hz, rs_p8_results[i].mem_hz, rs_p8_results[i].isolated_cycles, + rs_p8_results[i].contended_cycles, rs_p8_results[i].progress); + } + printf("P8_RESULT code=%u profiles=%u\n", code, count); + rs_sysctrl_write_test_status(code == 0U, code); + for (;;) { + } +} diff --git a/crt/include/retrosoc/hal/xpi_psram.h b/crt/include/retrosoc/hal/xpi_psram.h new file mode 100644 index 00000000..c267ce19 --- /dev/null +++ b/crt/include/retrosoc/hal/xpi_psram.h @@ -0,0 +1,29 @@ +#ifndef RETROSOC_HAL_XPI_PSRAM_H +#define RETROSOC_HAL_XPI_PSRAM_H + +#include + +#define RS_XPI_PSRAM_BASE UINT32_C(0x54000000) +#define RS_XPI_PSRAM_BYTES UINT32_C(0x00800000) +#define RS_XPI_PSRAM_BURST_BEATS UINT8_C(4) + +typedef struct { + bool initialized; + uint32_t memory_clock_hz; + uint32_t capacity_bytes; + uint8_t id[6]; +} rs_xpi_psram_t; + +/* ESP-PSRAM64H reference profile: slot 1, LUT 8..11, QPI SDR, SCK=MEM/2. + * Caller owns XPI exclusively, routes NSS1, and executes code/IRQs from SRAM. + * Clock changes/reset invalidate this software session; explicitly initialize + * again before using memory. This is not physical part/rate qualification. */ +rs_status_t rs_xpi_psram_timing(uint32_t memory_clock_hz, rs_xpi_slot_config_t *config); +rs_status_t rs_xpi_psram_initialize(uint32_t memory_clock_hz, rs_timeout_t timeout, + rs_xpi_psram_t *device); +/* Direct DMA uses SOFTWARE request and mapped addresses, not XPI TX/RX FIFO. + * Align bases to 16 bytes for four-beat bursts; the DMA splits at 4 KiB. */ +rs_status_t rs_xpi_psram_validate_dma(const rs_xpi_psram_t *device, uint32_t address, + uint32_t bytes, uint8_t burst_beats); + +#endif diff --git a/crt/src/hal/xpi_psram.c b/crt/src/hal/xpi_psram.c new file mode 100644 index 00000000..d64f3c0c --- /dev/null +++ b/crt/src/hal/xpi_psram.c @@ -0,0 +1,147 @@ +#include +#include + +#define RS_XPI_PSRAM_SLOT UINT8_C(1) +#define RS_XPI_PSRAM_COMMAND_SEQUENCE UINT8_C(10) +#define RS_XPI_PSRAM_ID_SEQUENCE UINT8_C(11) + +static rs_status_t rs_xpi_psram_delay(uint32_t microseconds, rs_timeout_t timeout) { + uint64_t first, now; + const uint64_t ticks = + ((uint64_t)microseconds * RS_CLINT_TIMEBASE_HZ + UINT64_C(999999)) / UINT64_C(1000000); + if (rs_clint_get_time(&first) != RS_OK) { + return RS_EIO; + } + while (timeout-- != 0U) { + if (rs_clint_get_time(&now) != RS_OK) { + return RS_EIO; + } + if (now - first >= ticks) { + return RS_OK; + } + } + return RS_ETIMEOUT; +} + +static rs_status_t rs_xpi_psram_command(uint8_t opcode, rs_xpi_pads_t pads, rs_timeout_t timeout) { + const uint16_t lut[] = {rs_xpi_instruction(RS_XPI_INSTR_COMMAND, pads, opcode), 0U}; + const rs_xpi_transfer_t transfer = {.slot = RS_XPI_PSRAM_SLOT, + .sequence = RS_XPI_PSRAM_COMMAND_SEQUENCE}; + rs_status_t status = rs_xpi_enable(false, timeout); + if (status == RS_OK) { + status = rs_xpi_lut_write(RS_XPI_PSRAM_COMMAND_SEQUENCE, lut, 2U, timeout); + } + if (status == RS_OK) { + status = rs_xpi_enable(true, timeout); + } + if (status == RS_OK) { + status = rs_xpi_transfer(&transfer, timeout); + } + return status; +} + +rs_status_t rs_xpi_psram_initialize(uint32_t memory_clock_hz, rs_timeout_t timeout, + rs_xpi_psram_t *device) { + const uint16_t read_lut[] = {UINT16_C(0x18eb), UINT16_C(0x2818), UINT16_C(0x4006), + UINT16_C(0x6800), 0U}; + const uint16_t write_lut[] = {UINT16_C(0x1838), UINT16_C(0x2818), UINT16_C(0x5800), 0U}; + const uint16_t id_lut[] = {UINT16_C(0x109f), UINT16_C(0x2018), UINT16_C(0x6000), 0U}; + rs_xpi_slot_config_t config; + rs_xpi_transfer_t transfer = { + .slot = RS_XPI_PSRAM_SLOT, .sequence = RS_XPI_PSRAM_ID_SEQUENCE, .byte_count = 3U}; + rs_status_t status; + if (device == NULL) { + return RS_EINVAL; + } + *device = (rs_xpi_psram_t){0}; + status = rs_xpi_psram_timing(memory_clock_hz, &config); + if (status != RS_OK) { + return status; + } + status = rs_xpi_probe(); + if (status != RS_OK) { + return status; + } + /* Reject locks before issuing any potentially faulting configuration write. */ + if ((RS_XPI_REG(RS_XPI_REG_CONFIG_LOCK) & + (RS_XPI_CONFIG_LOCK_GLOBAL | RS_XPI_CONFIG_LOCK_SLOT1 | RS_XPI_CONFIG_LOCK_LUT)) != 0U) { + return RS_EIO; + } + status = rs_xpi_enable(false, timeout); + if (status != RS_OK) { + return status; + } + status = rs_xpi_slot_configure(RS_XPI_PSRAM_SLOT, &config, timeout); + if (status == RS_OK) { + status = rs_xpi_lut_write(8U, read_lut, 5U, timeout); + } + if (status == RS_OK) { + status = rs_xpi_lut_write(9U, write_lut, 4U, timeout); + } + if (status == RS_OK) { + status = rs_xpi_lut_write(RS_XPI_PSRAM_ID_SEQUENCE, id_lut, 4U, timeout); + } + if (status == RS_OK) { + status = rs_xpi_enable(true, timeout); + } + if (status == RS_OK) { + status = rs_xpi_psram_delay(150U, timeout); + } + /* Exit QPI is harmless when the device is already in SPI reset mode. */ + if (status == RS_OK) { + status = rs_xpi_psram_command(UINT8_C(0xf5), RS_XPI_PADS_4, timeout); + } + if (status == RS_OK) { + status = rs_xpi_psram_command(UINT8_C(0x66), RS_XPI_PADS_1, timeout); + } + if (status == RS_OK) { + status = rs_xpi_psram_command(UINT8_C(0x99), RS_XPI_PADS_1, timeout); + } + if (status == RS_OK) { + status = rs_xpi_psram_delay(1U, timeout); + } + if (status == RS_OK) { + transfer.rx_data = device->id; + status = rs_xpi_transfer(&transfer, timeout); + } + if ((status == RS_OK) && + ((device->id[0] != UINT8_C(0x0d)) || (device->id[1] != UINT8_C(0x5d)) || + (device->id[2] != UINT8_C(0x26)))) { + status = RS_ENOTSUP; + } + if (status == RS_OK) { + status = rs_xpi_psram_command(UINT8_C(0x35), RS_XPI_PADS_1, timeout); + } + if (status == RS_OK) { + status = rs_xpi_enable(false, timeout); + } + if (status == RS_OK) { + config.memory_read_enable = true; + config.memory_write_enable = true; + status = rs_xpi_slot_configure(RS_XPI_PSRAM_SLOT, &config, timeout); + } + if (status != RS_OK) { + /* Keep the first error snapshot. Never publish capacity after a failed + * probe, and leave mapped NSS1 disabled while restoring NOR access. */ + rs_status_t cleanup = rs_xpi_abort(timeout); + if (cleanup == RS_OK) { + cleanup = rs_xpi_enable(false, timeout); + } + config.memory_read_enable = false; + config.memory_write_enable = false; + if (cleanup == RS_OK) { + cleanup = rs_xpi_slot_configure(RS_XPI_PSRAM_SLOT, &config, timeout); + } + if (cleanup == RS_OK) { + cleanup = rs_xpi_enable(true, timeout); + } + return (cleanup == RS_OK) ? status : cleanup; + } + status = rs_xpi_enable(true, timeout); + if (status == RS_OK) { + device->capacity_bytes = RS_XPI_PSRAM_BYTES; + device->memory_clock_hz = memory_clock_hz; + device->initialized = true; + } + return status; +} diff --git a/crt/src/hal/xpi_psram_math.c b/crt/src/hal/xpi_psram_math.c new file mode 100644 index 00000000..66a2da7c --- /dev/null +++ b/crt/src/hal/xpi_psram_math.c @@ -0,0 +1,67 @@ +#include + +rs_status_t rs_xpi_psram_timing(uint32_t memory_clock_hz, rs_xpi_slot_config_t *config) { + if ((config == NULL) || + ((memory_clock_hz != UINT32_C(24000000)) && (memory_clock_hz != UINT32_C(48000000)) && + (memory_clock_hz != UINT32_C(96000000)) && (memory_clock_hz != UINT32_C(120000000)))) { + return RS_EINVAL; + } + config->enable = true; + config->memory_read_enable = false; + config->memory_write_enable = false; + config->mode3 = false; + config->device_size = RS_XPI_PSRAM_BYTES; + config->read_sequence = UINT8_C(8); + config->write_sequence = UINT8_C(9); + config->clock_divider = UINT8_C(0); + config->cs_setup_cycles = UINT8_C(1); + config->cs_hold_cycles = + (uint8_t)(((uint64_t)memory_clock_hz * UINT64_C(20) + UINT64_C(999999999)) / + UINT64_C(1000000000)); + config->cs_high_cycles = + (uint8_t)(((uint64_t)memory_clock_hz * UINT64_C(50) + UINT64_C(999999999)) / + UINT64_C(1000000000)); + /* Includes core setup/hold/high and consumer stalls. Leave two MEM edges + * of margin below the reference device's 8 us active-CS limit. */ + config->timeout_cycles = (memory_clock_hz / UINT32_C(125000)) - UINT32_C(2); + config->burst_boundary = UINT32_C(1024); + return RS_OK; +} + +rs_status_t rs_xpi_psram_validate_dma(const rs_xpi_psram_t *device, uint32_t address, + uint32_t bytes, uint8_t burst_beats) { + uint32_t offset; + uint32_t cursor; + const uint32_t burst_bytes = (uint32_t)burst_beats * UINT32_C(4); + if (device == NULL) { + return RS_EINVAL; + } + if (!device->initialized || (device->capacity_bytes != RS_XPI_PSRAM_BYTES)) { + return RS_ENOTSUP; + } + if ((address < RS_XPI_PSRAM_BASE) || (bytes == 0U) || ((bytes & 3U) != 0U) || + ((burst_beats != 1U) && (burst_beats != 2U) && (burst_beats != 4U)) || + ((address & ((uint32_t)burst_beats * 4U - 1U)) != 0U)) { + return RS_EINVAL; + } + offset = address - RS_XPI_PSRAM_BASE; + if ((offset >= device->capacity_bytes) || (bytes > device->capacity_bytes - offset)) { + return RS_EINVAL; + } + /* The XPI slot rejects a serial-boundary crossing command, and the DMA + * itself cannot cross a 4 KiB AXI page. Validate every emitted burst before + * admitting the session; do not rely on the target to fail mid-frame. */ + for (cursor = 0U; cursor < bytes; cursor += burst_bytes) { + uint32_t length = bytes - cursor; + const uint32_t serial_offset = (offset + cursor) & UINT32_C(0x3ff); + const uint32_t page_offset = (offset + cursor) & UINT32_C(0xfff); + if (length > burst_bytes) { + length = burst_bytes; + } + if ((length > UINT32_C(0x400) - serial_offset) || + (length > UINT32_C(0x1000) - page_offset)) { + return RS_EINVAL; + } + } + return RS_OK; +} diff --git a/rtl/ip/storage/xpi_mm.sv b/rtl/ip/storage/xpi_mm.sv index ffa13707..9194e542 100644 --- a/rtl/ip/storage/xpi_mm.sv +++ b/rtl/ip/storage/xpi_mm.sv @@ -83,6 +83,7 @@ module xpi_mm ( logic [1:0] s_resp_d, s_resp_q; logic [31:0] s_rdata_d, s_rdata_q; logic s_rvalid_d, s_rvalid_q; + logic s_word_complete_d, s_word_complete_q; logic [2:0] s_byte_idx_d, s_byte_idx_q; logic [6:0] s_cursor_d, s_cursor_q; logic [6:0] s_run_end_d, s_run_end_q; @@ -198,7 +199,7 @@ module xpi_mm ( axi4.awburst, axi4.awlen ) && !axi4.awlock && (axi4.awsize <= `AXI4_BURST_SIZE_4BYTES) && ((axi4.awaddr & ((32'd1 << axi4.awsize) - 1'b1)) == 32'd0) && - (axi4.awaddr[31:12] == s_write_last_addr[31:12]); + (axi4.awaddr[31:12] == s_write_last_addr[31:12]) && s_write_boundary_ok; assign s_legal_read = address_supported( axi4.araddr @@ -216,7 +217,7 @@ module xpi_mm ( axi4.arburst, axi4.arlen ) && !axi4.arlock && (axi4.arsize <= `AXI4_BURST_SIZE_4BYTES) && ((axi4.araddr & ((32'd1 << axi4.arsize) - 1'b1)) == 32'd0) && - (axi4.araddr[31:12] == s_read_last_addr[31:12]); + (axi4.araddr[31:12] == s_read_last_addr[31:12]) && s_read_boundary_ok; assign axi4.awready = (s_state_q == Idle) && accept_enable_i && !axi4.arvalid; assign axi4.arready = (s_state_q == Idle) && accept_enable_i; @@ -247,7 +248,7 @@ module xpi_mm ( (16'd1 << s_size_q)); assign core_tx_valid_o = s_state_q == WriteStream; - assign core_rx_ready_o = (s_state_q == ReadData) && !s_rvalid_q; + assign core_rx_ready_o = (s_state_q == ReadData) && !s_word_complete_q; assign core_tx_data_o = s_write_buffer_q[s_tx_beat][(s_tx_lane*8)+:8]; assign s_err_code_q = xpi_error_e'(s_err_code_bits_q); @@ -306,8 +307,13 @@ module xpi_mm ( end always_comb begin - s_write_lane_mask = (4'd1 << s_size_q) - 1'b1; - s_read_lane = {1'b0, s_addr_q[1:0]} + s_byte_idx_q; + unique case (s_size_q) + 3'd0: s_write_lane_mask = 4'b0001; + 3'd1: s_write_lane_mask = 4'b0011; + 3'd2: s_write_lane_mask = 4'b1111; + default: s_write_lane_mask = 4'b0000; + endcase + s_read_lane = {1'b0, s_addr_q[1:0]} + s_byte_idx_q; end for (genvar write_index = 0; write_index < 16; write_index++) begin : write_buffer_block @@ -339,6 +345,7 @@ module xpi_mm ( s_resp_d = s_resp_q; s_rdata_d = s_rdata_q; s_rvalid_d = s_rvalid_q; + s_word_complete_d = s_word_complete_q; s_byte_idx_d = s_byte_idx_q; s_cursor_d = s_cursor_q; s_run_end_d = s_run_end_q; @@ -362,6 +369,7 @@ module xpi_mm ( s_resp_d = `AXI4_RESP_OKAY; s_rdata_d = '0; s_rvalid_d = 1'b0; + s_word_complete_d = 1'b0; s_byte_idx_d = '0; s_cursor_d = '0; s_frame_done_d = 1'b0; @@ -423,37 +431,45 @@ module xpi_mm ( s_rdata_d[(s_read_lane*8)+:8] = core_rx_data_i; perf_read_byte_event_o = 1'b1; if (s_byte_idx_q == ((3'd1 << s_size_q) - 1'b1)) begin - s_byte_idx_d = '0; - s_rvalid_d = 1'b1; + s_byte_idx_d = '0; + s_word_complete_d = 1'b1; + // Hold the physical command's last beat until STOP/CS completion + // so a late sequence error cannot follow a successful AXI end. + s_rvalid_d = s_coalesce_q && (s_beat_q != s_len_q); end else begin s_byte_idx_d = s_byte_idx_q + 1'b1; end end if (core_done_i) begin s_frame_done_d = 1'b1; - if (core_error_i) begin - s_resp_d = `AXI4_RESP_SLAVE_ERROR; + if (core_error_i || !s_word_complete_q || (s_coalesce_q && (s_beat_q != s_len_q))) begin s_transaction_err_d = 1'b1; - s_err_code_d = core_error_code_i; + s_err_code_d = core_error_i ? core_error_code_i : XpiErrorSequence; s_err_addr_d = s_addr_q; s_err_slot_d = s_slot_q; s_err_pc_d = core_error_pc_i; axi_error_event_o = 1'b1; if (!s_rvalid_q) begin - s_rdata_d = '0; - s_rvalid_d = 1'b1; + s_resp_d = `AXI4_RESP_SLAVE_ERROR; + s_rdata_d = '0; + s_rvalid_d = 1'b1; + s_word_complete_d = 1'b1; end + end else begin + s_rvalid_d = 1'b1; end end if (s_rvalid_q && axi4.rready) begin - s_rvalid_d = 1'b0; - s_rdata_d = '0; + s_rvalid_d = 1'b0; + s_word_complete_d = 1'b0; + s_rdata_d = '0; if (s_beat_q == s_len_q) begin s_state_d = (s_frame_done_q || core_done_i) ? Idle : ReadFinish; end else begin s_addr_d = s_next_addr; s_beat_d = s_beat_q + 1'b1; - if (s_transaction_err_q || core_error_i) begin + if (s_transaction_err_d) begin + s_resp_d = `AXI4_RESP_SLAVE_ERROR; s_state_d = ReadErrorResponse; end else if (s_coalesce_q) begin s_state_d = ReadData; @@ -516,10 +532,12 @@ module xpi_mm ( axi_error_event_o = 1'b1; s_state_d = axi4.wlast ? WriteResponse : WriteDrain; end else if (s_beat_q == s_len_q) begin + s_addr_d = s_start_addr_q; s_beat_d = '0; s_cursor_d = '0; s_state_d = WriteScan; end else begin + s_addr_d = s_next_addr; s_beat_d = s_beat_q + 1'b1; end end @@ -549,7 +567,17 @@ module xpi_mm ( end WriteStream: begin - if (core_tx_valid_o && core_tx_ready_i) begin + // The core can time out or abort before consuming all buffered bytes. + // AXI W is already drained; complete B rather than wait for a dead core. + if (core_done_i) begin + s_resp_d = `AXI4_RESP_SLAVE_ERROR; + s_err_code_d = core_error_i ? core_error_code_i : XpiErrorSequence; + s_err_addr_d = s_addr_q; + s_err_slot_d = s_slot_q; + s_err_pc_d = core_error_pc_i; + axi_error_event_o = 1'b1; + s_state_d = WriteResponse; + end else if (core_tx_valid_o && core_tx_ready_i) begin perf_write_byte_event_o = 1'b1; s_cursor_d = s_cursor_q + 1'b1; if ((s_cursor_q + 1'b1) >= s_run_end_q) begin @@ -609,6 +637,14 @@ module xpi_mm ( .dat_i (s_state_d), .dat_o (s_state_bits_q) ); + dffr #( + .DATA_WIDTH(1) + ) u_word_complete_dffr ( + .clk_i (clk_i), + .rst_n_i(rst_n_i), + .dat_i (s_word_complete_d), + .dat_o (s_word_complete_q) + ); dffr #( .DATA_WIDTH(32) ) u_addr_dffr ( diff --git a/rtl/mini/filelist/tb.fl b/rtl/mini/filelist/tb.fl index fe254d66..c4064997 100644 --- a/rtl/mini/filelist/tb.fl +++ b/rtl/mini/filelist/tb.fl @@ -3,7 +3,7 @@ /model/uart_model.sv /device_model/mic.sv /device_model/w25q128jvxim.sv -/model/ESP_PSRAM64H.sv +/device_model/ESP_PSRAM64H.sv /model/DVP_CAMERA.sv /third_party/model/sdram/sdr.v /tb/retrosoc_tb.sv diff --git a/rtl/mini/mk/verilator.mk b/rtl/mini/mk/verilator.mk index 3889b9b2..0feea6f4 100644 --- a/rtl/mini/mk/verilator.mk +++ b/rtl/mini/mk/verilator.mk @@ -11,7 +11,7 @@ SOC_VSRC_HOME += $(RTL_PATH)/dv/verilator/rtl SOC_COMPILE_HOME := $(BUILD_DIR)/emu_compile SOC_VXXFILES := $(RTL_FLIST) -SOC_VXXFILES += $(RTL_PATH)/dv/model/ESP_PSRAM64H.sv +SOC_VXXFILES += $(ROOT_PATH)/rtl/model/ESP_PSRAM64H.sv SOC_VXXFILES += $(RTL_PATH)/dv/verilator/rtl/flash_read_binder.sv SOC_VXXFILES += $(RTL_PATH)/dv/verilator/rtl/flash_read_byte_binder.sv SOC_VXXFILES += $(RTL_PATH)/dv/verilator/rtl/QSPIFlash.sv @@ -51,7 +51,7 @@ VERILATOR_DEPFILE := $(BUILD_DIR)/verilate.d VERILATOR_EMU := $(BUILD_DIR)/emu RTL_LINT_STAMP := $(RTL_LINT_DIR)/rtl-lint.stamp RTL_LINT_DEPFILE := $(RTL_LINT_DIR)/rtl-lint.d -VERILATOR_EXTRA_SOURCES := $(RTL_PATH)/dv/model/ESP_PSRAM64H.sv \ +VERILATOR_EXTRA_SOURCES := $(ROOT_PATH)/rtl/model/ESP_PSRAM64H.sv \ $(RTL_PATH)/dv/verilator/rtl/flash_read_binder.sv \ $(RTL_PATH)/dv/verilator/rtl/flash_read_byte_binder.sv \ $(RTL_PATH)/dv/verilator/rtl/QSPIFlash.sv \ @@ -144,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 741dc220..b2e76588 100644 --- a/rtl/mk/software.mk +++ b/rtl/mk/software.mk @@ -168,6 +168,8 @@ CRT_SRCS := $(ROOT_PATH)/crt/arch/riscv/startup.S \ $(ROOT_PATH)/crt/src/hal/spisd_math.c \ $(ROOT_PATH)/crt/src/hal/spisd.c \ $(ROOT_PATH)/crt/src/hal/xpi.c \ + $(ROOT_PATH)/crt/src/hal/xpi_psram_math.c \ + $(ROOT_PATH)/crt/src/hal/xpi_psram.c \ $(ROOT_PATH)/crt/src/hal/fabric_monitor.c \ $(ROOT_PATH)/crt/src/hal/resource.c \ $(ROOT_PATH)/crt/src/hal/extension.c \ @@ -180,7 +182,7 @@ CFLAGS += -DRS_SOC_TINY TINY_CRT_NAMES := startup.S clzsi2.c divdi3.c ffssi2.c udivdi3.c umoddi3.c \ stdlib.c string.c console.c printf.c archinfo.c clint.c uart_math.c uart.c \ gpio_math.c gpio.c timer_math.c timer.c pwm.c rtc.c wdg.c watchdog.c i2c_math.c i2c.c \ - dma_math.c dma.c onchip_sram.c xpi.c rng.c crc.c ws2812_math.c ws2812.c \ + dma_math.c dma.c onchip_sram.c xpi.c xpi_psram.c xpi_psram_math.c rng.c crc.c ws2812_math.c ws2812.c \ crypto.c crypto_lifecycle.c i2s_math.c i2s.c sdio_math.c sdio.c rcu.c CRT_SRCS := $(foreach source,$(CRT_SRCS),$(if $(filter $(notdir $(source)),$(TINY_CRT_NAMES)),$(source))) CRT_SRCS += $(ROOT_PATH)/crt/src/hal/sysctrl.c @@ -305,4 +307,4 @@ firmware: $(FIRMWARE_ELF) firmware asm: | manifest -.PHONY: FORCE_VERSION \ No newline at end of file +.PHONY: FORCE_VERSION diff --git a/rtl/mini/dv/model/ESP_PSRAM64H.sv b/rtl/model/ESP_PSRAM64H.sv similarity index 99% rename from rtl/mini/dv/model/ESP_PSRAM64H.sv rename to rtl/model/ESP_PSRAM64H.sv index 7ad94fb7..72f0162a 100644 --- a/rtl/mini/dv/model/ESP_PSRAM64H.sv +++ b/rtl/model/ESP_PSRAM64H.sv @@ -8,7 +8,7 @@ // MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. // See the Mulan PSL v2 for more details. // -// Behavioral model for the external ESP-PSRAM64H 64-Mbit PSRAM. +// Shared behavioral model for the external ESP-PSRAM64H 64-Mbit PSRAM. // Only SDR SPI and QPI transactions are modeled. The model intentionally // does not initialize the memory array unless INITIALIZE_MEMORY is enabled. diff --git a/rtl/tiny/dv/retrosoc_tiny_tb.sv b/rtl/tiny/dv/retrosoc_tiny_tb.sv index 1cef5b6f..72a9c2f3 100644 --- a/rtl/tiny/dv/retrosoc_tiny_tb.sv +++ b/rtl/tiny/dv/retrosoc_tiny_tb.sv @@ -23,13 +23,28 @@ module retrosoc_tiny_tb; wire s_jtag_trst_n = s_rst_n; wire s_jtag_tdo; `include "tiny_tb_gpio.svh" - wire s_xpi_sck; - wire [3:0] s_xpi_nss; - tri [3:0] s_xpi_data; - integer s_cycles; + wire s_xpi_sck; + wire [3:0] s_xpi_nss; + tri1 [3:0] s_xpi_data; + // Optional board population; the unchanged default has no PSRAM dependency. + wire s_psram_csn = ($test$plusargs( + "tiny_p8_psram" + ) && !$test$plusargs( + "p8_absent" + )) ? s_gpio[29] : 1'b1; + ESP_PSRAM64H #( + .TIMING_VIOLATION_FATAL(1), + .MIN_CS_HIGH_NS (50) + ) u_psram ( + .sclk(s_xpi_sck), + .csn (s_psram_csn), + .sio (s_xpi_data) + ); + integer s_cycles; `ifndef RETROSOC_SOC__TINY_NETLIST `include "tiny_p6_devices.svh" `include "tiny_p7_observers.svh" + `include "tiny_p8_observers.svh" `endif integer s_max_cycles = 20000000; integer s_done_cycles = 0; diff --git a/rtl/tiny/dv/tiny_p8_observers.svh b/rtl/tiny/dv/tiny_p8_observers.svh new file mode 100644 index 00000000..649629f0 --- /dev/null +++ b/rtl/tiny/dv/tiny_p8_observers.svh @@ -0,0 +1,63 @@ +// Verification-only MEM-clock observations. No DUT state is driven. +`define _P8_AXI u_dut.u_soc.u_xpi_mem_axi4_if +logic s_p8_previous_enable = 0; +logic s_p8_finished = 0; +longint unsigned s_p8_cycles = 0, s_p8_write_bytes = 0, s_p8_read_bytes = 0; +longint unsigned s_p8_stall_run[5], s_p8_stall_max[5]; +longint unsigned s_p8_all_writes = 0; +integer s_p8_window = 0; +wire [4:0] s_p8_stalls = { + `_P8_AXI.awvalid && !`_P8_AXI.awready, + `_P8_AXI.wvalid && !`_P8_AXI.wready, + `_P8_AXI.arvalid && !`_P8_AXI.arready, + `_P8_AXI.bvalid && !`_P8_AXI.bready, + `_P8_AXI.rvalid && !`_P8_AXI.rready +}; +always @(posedge u_dut.u_soc.s_backend_mem_clk) begin + if ($test$plusargs("tiny_p8_psram") && u_dut.u_soc.s_xpi_rst_n) begin + if (`_P8_AXI.wvalid && `_P8_AXI.wready) s_p8_all_writes++; + if (u_dut.u_soc.s_perf_enable) begin + if (!s_p8_previous_enable) begin + s_p8_cycles = 0; + s_p8_write_bytes = 0; + s_p8_read_bytes = 0; + for (int i = 0; i < 5; i++) begin + s_p8_stall_run[i] = 0; + s_p8_stall_max[i] = 0; + end + end + s_p8_cycles++; + if (`_P8_AXI.wvalid && `_P8_AXI.wready) s_p8_write_bytes += $countones(`_P8_AXI.wstrb); + if (`_P8_AXI.rvalid && `_P8_AXI.rready) s_p8_read_bytes += 4; + for (int i = 0; i < 5; i++) begin + s_p8_stall_run[i] = s_p8_stalls[i] ? s_p8_stall_run[i] + 1 : 0; + if (s_p8_stall_run[i] > s_p8_stall_max[i]) s_p8_stall_max[i] = s_p8_stall_run[i]; + end + end else if (s_p8_previous_enable) begin + $display( + "P8_XPI_WINDOW index=%0d mem_cycles=%0d write_bytes=%0d read_bytes=%0d cs_max=%0d aw_max=%0d w_max=%0d ar_max=%0d b_max=%0d r_max=%0d", + s_p8_window, s_p8_cycles, s_p8_write_bytes, s_p8_read_bytes, s_p8_cs_max, + s_p8_stall_max[0], s_p8_stall_max[1], s_p8_stall_max[2], s_p8_stall_max[3], + s_p8_stall_max[4]); + s_p8_window++; + end + s_p8_previous_enable = u_dut.u_soc.s_perf_enable; + if (u_dut.s_test_done && !s_p8_finished) begin + s_p8_finished = 1; + if (!$test$plusargs("p8_absent") && (s_p8_window < 4 || s_p8_all_writes == 0)) + $fatal(1, "SIM_TEST_FAIL P8 missing mapped transport evidence"); + if ($test$plusargs("p8_absent") && s_p8_all_writes != 0) + $fatal(1, "SIM_TEST_FAIL P8 absent device exposed mapped writes"); + end + end +end +`undef _P8_AXI + +time s_p8_cs_start, s_p8_cs_max = 0; +always @(negedge s_psram_csn) s_p8_cs_start = $time; +always @(posedge s_psram_csn) begin + if ($test$plusargs("tiny_p8_psram") && s_p8_cs_start != 0) begin + if ($time - s_p8_cs_start > 8000) $fatal(1, "SIM_TEST_FAIL P8 active CS exceeds 8 us"); + if ($time - s_p8_cs_start > s_p8_cs_max) s_p8_cs_max = $time - s_p8_cs_start; + end +end diff --git a/rtl/tiny/filelist/tb.fl b/rtl/tiny/filelist/tb.fl index da7d4e7d..37be89bf 100644 --- a/rtl/tiny/filelist/tb.fl +++ b/rtl/tiny/filelist/tb.fl @@ -1,3 +1,4 @@ /dv/tiny_qspi_nor.sv +/device_model/ESP_PSRAM64H.sv /tests/rtl/sdio_native_model.sv /dv/retrosoc_tiny_tb.sv diff --git a/scripts/generate_tiny.py b/scripts/generate_tiny.py index 22d04c40..18af8a3b 100644 --- a/scripts/generate_tiny.py +++ b/scripts/generate_tiny.py @@ -225,7 +225,8 @@ def generate_filelists(output: Path, generated: Path, defines: list[str], prefixes = {"top": TINY / "top", "dv": TINY / "dv", "ip": ROOT / "rtl/ip", "tech": ROOT / "rtl/tech", "clusterip": ROOT / "rtl/managed/clusterip", "hazard3": ROOT / "rtl/managed/hazard3/hdl", "physical": ROOT / "physical", - "cache": ROOT / ".cache", "tests": ROOT / "tests"} + "cache": ROOT / ".cache", "tests": ROOT / "tests", + "device_model": ROOT / "rtl/model"} suffix = "_yosys" if synthesis == "YOSYS" else "_verilator" if simulator == "VERILATOR" else "" pdk_name = "pdk_ihp130.fl" if pdk == "IHP130" else f"pdk_ics55{suffix}.fl" templates = [*sorted((TINY / "filelist").glob("*.fl")), diff --git a/scripts/run_c_tests.py b/scripts/run_c_tests.py index fe896652..942c7148 100644 --- a/scripts/run_c_tests.py +++ b/scripts/run_c_tests.py @@ -27,6 +27,7 @@ TEST_SOURCES = ( "crt/src/hal/uart_math.c", "crt/src/hal/timer_math.c", "crt/src/hal/psram_math.c", + "crt/src/hal/xpi_psram_math.c", "crt/src/hal/sdram_math.c", "crt/src/hal/sdio_math.c", "crt/src/hal/usb2_math.c", diff --git a/scripts/tiny_r2_p8.py b/scripts/tiny_r2_p8.py new file mode 100644 index 00000000..91697271 --- /dev/null +++ b/scripts/tiny_r2_p8.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 +"""Retain P8 inputs and validate CPU/DMA pin-level workload evidence.""" +from __future__ import annotations + +import argparse +from datetime import datetime, timezone +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(ROOT)) +from scripts import tiny_r2_baseline as evidence # noqa: E402 +from scripts.check_simulation import DEFAULT_FAILURE # noqa: E402 + +PHASE = "TINY-R2-P8" + + +def measurements(content: str, config: dict, absent: bool = False) -> dict: + if DEFAULT_FAILURE.search(content) or "SIM_TEST_PASS Tiny cycles=" not in content: + raise ValueError("missing strict Tiny terminal success or forbidden error marker") + rows = [dict((k, int(v)) for k, v in re.findall(r"(\w+)=(\d+)", line)) + for line in content.splitlines() if line.startswith("P8_TRANSPORT ")] + windows = [dict((k, int(v)) for k, v in re.findall(r"(\w+)=(\d+)", line)) + for line in content.splitlines() if line.startswith("P8_XPI_WINDOW ")] + terminal = re.findall(r"P8_RESULT code=(\d+) profiles=(\d+)", content) + if len(terminal) != 1 or terminal[0] != ("0", str(len(rows))): + raise ValueError("missing or inconsistent firmware workload result") + if absent: + if rows or windows or "P8_ABSENT bounded optional-device rejection" not in content: + raise ValueError("absent-device run must reject initialization and perform no mapped DMA") + return {"mode": "absent", "profiles": [], "windows": []} + expected = [(24000000, 24000000)] + if config["PDK"] == "ICS55": + expected += [(192000000, 96000000), (240000000, 120000000)] + elif int(config["EXT_CLK_HZ"]) != 24000000: + expected += [(int(config["EXT_CLK_HZ"]), int(config["EXT_CLK_HZ"]))] + if [(r.get("sys_hz"), r.get("mem_hz")) for r in rows] != expected: + raise ValueError("missing, duplicated or unexpected clock profiles") + if len(windows) != 4 * len(rows): + raise ValueError("missing isolated/contended write and readback observation windows") + for index, row in enumerate(rows): + if row.get("bytes") != 1024 or row.get("burst") != 4 or row.get("progress", 0) <= 0: + raise ValueError("incorrect workload size/burst or no concurrent CPU progress") + for name in ("isolated", "contended"): + cycles = row.get(f"{name}_cycles", 0) + if cycles <= 0: + raise ValueError("invalid measured cycle count") + row[f"{name}_bytes_per_second"] = row["bytes"] * row["sys_hz"] / cycles + for offset, window in enumerate(windows[index * 4:index * 4 + 4]): + counts = (window.get("write_bytes"), window.get("read_bytes")) + if (counts != ((1024, 0) if offset % 2 == 0 else (0, 1024)) or + window.get("index") != index * 4 + offset or window.get("mem_cycles", 0) <= 0): + raise ValueError("wrong mapped-path byte accounting") + for channel in ("aw", "w", "ar", "b", "r"): + stall = window.get(f"{channel}_max", -1) + if not 0 <= stall <= window["mem_cycles"]: + raise ValueError("missing or impossible backpressure measurement") + window[f"{channel}_max_ns"] = stall * 1e9 / row["mem_hz"] + return {"mode": "present", "profiles": rows, "windows": windows, + "limits": "Digital model only; measured stalls apply only to this finite workload. " + "No physical frequency, unrestricted contention or camera rate qualification."} + + +def source_inputs() -> dict: + inputs = evidence.source_inputs(ROOT, "ICS55") | evidence.source_inputs(ROOT, "IHP130") + for folder in ("rtl/model", "tests", "configs/ci"): + for path in (ROOT / folder).rglob("*"): + if path.is_file() and path.suffix in (".sv", ".svh", ".py", ".c", ".h", ".mk"): + inputs[str(path.relative_to(ROOT))] = evidence.sha256(path) + return inputs + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("action", choices=("capture", "report")) + parser.add_argument("--output", type=Path, required=True) + parser.add_argument("--source", type=Path) + parser.add_argument("--variant-root", type=Path) + parser.add_argument("--simulator", choices=("verilator", "iverilog")) + parser.add_argument("--attempt", type=Path) + parser.add_argument("--absent", action="store_true") + args = parser.parse_args() + if args.variant_root: + args.variant_root = args.variant_root.resolve() + try: + if args.output.exists(): + raise ValueError("evidence already exists; select a new attempt") + inputs = source_inputs() + if args.action == "capture": + if not all((args.variant_root, args.simulator)): + raise ValueError("capture requires a built --variant-root and --simulator") + runtime = evidence.runtime_inputs(ROOT, args.variant_root, args.simulator) + for path in (args.variant_root / "sw/firmware", + args.variant_root / "sw/retrosoc_fw.hex", + args.variant_root / "sim" / args.simulator / + ("emu" if args.simulator == "verilator" else "simv")): + runtime[str(path.resolve().relative_to(ROOT))] = evidence.sha256(path) + evidence.dump(args.output, { + "phase": PHASE, "repository": evidence.git_info(ROOT), + "captured_at": datetime.now(timezone.utc).isoformat(), + "variant": str(args.variant_root.resolve()), "simulator": args.simulator, + "runtime_inputs": runtime, + "specification": evidence.artifact(ROOT / "docs/ip/tiny-soc.md"), + "source_inputs": inputs, "source_digest": evidence.digest_map(inputs), + "tools": evidence.tools_identity(ROOT), + "snapshot": evidence.snapshot(ROOT, args.output.parent / "source", inputs), + }) + else: + if not all((args.source, args.variant_root, args.simulator, args.attempt)): + raise ValueError("report requires --source --variant-root --simulator --attempt") + source = evidence.read_json(args.source) + if source.get("phase") != PHASE or source.get("source_inputs") != inputs: + raise ValueError("source changed since the retained snapshot") + if (source.get("variant") != str(args.variant_root.resolve()) or + source.get("simulator") != args.simulator): + raise ValueError("runtime snapshot belongs to another variant or simulator") + for name, digest in source["runtime_inputs"].items(): + if evidence.sha256(ROOT / name) != digest: + raise ValueError(f"runtime input changed after capture: {name}") + manifest = evidence.read_json(args.variant_root / "meta/manifest.json") + config = manifest["configuration"] + if (config.get("SOC") != "TINY" or config.get("TINY_P8_ACCEPTANCE") != "YES" or + config.get("PDK") not in ("ICS55", "IHP130")): + raise ValueError("not an explicit Tiny P8 configuration") + result = evidence.read_json(args.attempt / "result-sim.json") + log = args.attempt / "sim.log" + if (result.get("status") != "passed" or result.get("exit_code") != 0 or + Path(result.get("log", "")).resolve() != log.resolve()): + raise ValueError("simulation command did not complete successfully with this log") + if datetime.fromisoformat(result["started_at"]) < datetime.fromisoformat(source["captured_at"]): + raise ValueError("simulation predates the source/runtime snapshot") + command = result.get("command", []) + binary = args.variant_root / "sim" / args.simulator / ( + "emu" if args.simulator == "verilator" else "simv") + firmware_arg = "+firmware=" + str((args.variant_root / "sw/retrosoc_fw.hex").resolve()) + if (str(binary.resolve()) not in command or firmware_arg not in command or + "+tiny_p8_psram" not in command or (("+p8_absent" in command) != args.absent)): + raise ValueError("simulation command does not match captured binary/firmware/mode") + report = measurements(log.read_text(), config, args.absent) + artifacts = [log, args.attempt / "result-sim.json", args.source, + args.variant_root / "sw/firmware", + args.variant_root / "sw/retrosoc_fw.hex", + args.variant_root / "sim" / args.simulator / + ("emu" if args.simulator == "verilator" else "simv")] + evidence.dump(args.output, {"phase": PHASE, "status": "passed", + "configuration": config, "simulator": args.simulator, "measurements": report, + "source_digest": source["source_digest"], + "artifacts": [evidence.artifact(p) for p in artifacts]}) + print(f"{PHASE} {args.action}: PASS ({args.output})") + return 0 + except (OSError, ValueError, KeyError) as error: + print(f"{PHASE}: {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 664778e6..c2c4d37b 100644 --- a/tests/c/test_runtime.c +++ b/tests/c/test_runtime.c @@ -25,6 +25,7 @@ #include #include #include +#include #include #include #include @@ -638,6 +639,36 @@ static int test_sdram_helpers(void) { return 0; } +static int test_xpi_psram_helpers(void) { + rs_xpi_slot_config_t config; + rs_xpi_psram_t device = {.initialized = true, .capacity_bytes = RS_XPI_PSRAM_BYTES}; + const uint32_t rates[] = {24000000U, 48000000U, 96000000U, 120000000U}; + for (uint32_t i = 0U; i < 4U; i++) { + if (rs_xpi_psram_timing(rates[i], &config) != RS_OK || config.memory_write_enable || + config.memory_read_enable || config.device_size != RS_XPI_PSRAM_BYTES || + config.timeout_cycles >= rates[i] / 125000U || config.burst_boundary != 1024U || + (uint64_t)config.cs_high_cycles * 1000000000U < (uint64_t)rates[i] * 50U) { + return 1; + } + } + if (rs_xpi_psram_timing(UINT32_MAX, &config) != RS_EINVAL || + rs_xpi_psram_timing(24000000U, NULL) != RS_EINVAL || + rs_xpi_psram_validate_dma(&device, RS_XPI_PSRAM_BASE, RS_XPI_PSRAM_BYTES, 4U) != RS_OK || + rs_xpi_psram_validate_dma(&device, 0x54000c00U, 1024U, 4U) != RS_OK || + rs_xpi_psram_validate_dma(&device, 0x547ffffcU, 4U, 1U) != RS_OK || + rs_xpi_psram_validate_dma(&device, 0x547ffffcU, 8U, 1U) != RS_EINVAL || + rs_xpi_psram_validate_dma(&device, 0x540003f4U, 64U, 4U) != RS_EINVAL || + rs_xpi_psram_validate_dma(&device, 0x54000ff4U, 16U, 4U) != RS_EINVAL || + rs_xpi_psram_validate_dma(&device, 0x540003fcU, 64U, 4U) != RS_EINVAL || + rs_xpi_psram_validate_dma(&device, RS_XPI_PSRAM_BASE, UINT32_MAX, 4U) != RS_EINVAL || + rs_xpi_psram_validate_dma(&device, RS_XPI_PSRAM_BASE, 64U, 16U) != RS_EINVAL || + rs_xpi_psram_validate_dma(&device, RS_XPI_PSRAM_BASE, 0U, 4U) != RS_EINVAL) { + return 2; + } + device.initialized = false; + return rs_xpi_psram_validate_dma(&device, RS_XPI_PSRAM_BASE, 4U, 1U) == RS_ENOTSUP ? 0 : 3; +} + static int test_psram_helpers(void) { rs_psram_timing_t timing; @@ -3193,13 +3224,11 @@ static int test_tiny_rcu_boundaries(void) { rs_tiny_rcu_get_snapshot(NULL) != RS_EINVAL || rs_tiny_rcu_get_snapshot(&snapshot) != RS_OK || snapshot.xin_hz != hz || rs_tiny_rcu_set_clkout(5U, 1U) != RS_EINVAL || - rs_tiny_rcu_set_clkout(1U, 0U) != RS_EINVAL || - rs_tiny_rcu_clear_fault(32U) != RS_EINVAL || + rs_tiny_rcu_set_clkout(1U, 0U) != RS_EINVAL || rs_tiny_rcu_clear_fault(32U) != RS_EINVAL || rs_tiny_rcu_clear_reset_cause(64U) != RS_EINVAL) { return 6; } - if (rs_tiny_rcu_set_clkout(1U, 1U) != RS_OK || - rs_tiny_rcu_set_clkout(1U, 2U) != RS_EBUSY || + if (rs_tiny_rcu_set_clkout(1U, 1U) != RS_OK || rs_tiny_rcu_set_clkout(1U, 2U) != RS_EBUSY || rs_tiny_rcu_set_clkout(0U, 0U) != RS_OK) { return 7; } @@ -3222,6 +3251,7 @@ int main(void) { test_ws2812_completion_race(), test_timer_helpers(), test_psram_helpers(), + test_xpi_psram_helpers(), test_sdram_helpers(), test_uart_helpers(), test_i2s_helpers(), diff --git a/tests/rtl/tiny_p8_xpi_tb.sv b/tests/rtl/tiny_p8_xpi_tb.sv new file mode 100644 index 00000000..d7a55e1f --- /dev/null +++ b/tests/rtl/tiny_p8_xpi_tb.sv @@ -0,0 +1,303 @@ +`timescale 1ns / 1ps + +// Production APB/AXI wrapper and real serial device. No memory backdoor. +module tiny_p8_xpi_tb; + import xpi_pkg::*; + logic s_clk = 0, s_rst_n = 0; + apb4_if apb ( + s_clk, + s_rst_n + ); + axi4_if #( + .ADDR_WIDTH(32), + .DATA_WIDTH(32), + .ID_WIDTH (1), + .USER_WIDTH(1) + ) axi (); + xpi_if pins (); + tri1 [3:0] s_data; + logic s_idle, s_tx_stall, s_rx_stall, s_irq; + logic [31:0] s_value; + integer s_commands = 0; + + always #20.833 s_clk = ~s_clk; + for (genvar i = 0; i < 4; i++) begin : gen_pads + assign s_data[i] = pins.io_oe_o[i] ? pins.io_do_o[i] : 1'bz; + end + assign pins.io_di_i = s_data; + always @(negedge pins.nss_o[1]) s_commands++; + + apb4_xpi u_dut ( + .clk_i (s_clk), + .rst_n_i (s_rst_n), + .dma_xfer_done_i (1'b0), + .lifecycle_idle_o(s_idle), + .dma_tx_stall_o (s_tx_stall), + .dma_rx_stall_o (s_rx_stall), + .irq_o (s_irq), + .apb4 (apb), + .mem_axi4 (axi), + .xpi (pins) + ); + ESP_PSRAM64H #( + .POWER_UP_CHECK (1), + .TIMING_VIOLATION_FATAL(1) + ) u_ram ( + .sclk(pins.sck_o), + .csn (pins.nss_o[1]), + .sio (s_data) + ); + + task automatic access (input bit write, input logic [11:0] addr, input logic [31:0] data, + output logic [31:0] result); + @(negedge s_clk); + apb.psel = 1; + apb.penable = 0; + apb.pwrite = write; + apb.paddr = {20'd0, addr}; + apb.pwdata = data; + apb.pstrb = 4'hf; + @(negedge s_clk); + apb.penable = 1; + do @(posedge s_clk); while (!apb.pready); + if (apb.pslverr) $fatal(1, "SIM_TEST_FAIL APB addr=%h", addr); + result = apb.prdata; + @(negedge s_clk); + apb.psel = 0; + apb.penable = 0; + endtask + task automatic wr(input logic [11:0] addr, input logic [31:0] data); + logic [31:0] unused; + access (1, addr, data, unused); + endtask + task automatic rd(input logic [11:0] addr, output logic [31:0] data); + access (0, addr, 0, data); + endtask + task automatic idle; + logic [31:0] status; + do rd(12'h010, status); while ((status & 32'h0e) != 0); + endtask + task automatic program_lut(input int seq, input logic [15:0] a, b, c, d); + wr(12'(12'h200 + seq * 16), {b, a}); + wr(12'(12'h204 + seq * 16), {d, c}); + wr(12'(12'h208 + seq * 16), 0); + wr(12'(12'h20c + seq * 16), 0); + endtask + task automatic command(input logic [7:0] opcode, input logic [1:0] pads); + idle(); + wr(12'h00c, 0); + program_lut(3, xpi_instr(XpiInstrCommand, pads, opcode), 0, 0, 0); + wr(12'h00c, 1); + wr(12'h024, 255); + wr(12'h048, 0); + wr(12'h04c, 0); + wr(12'h050, 32'h31); + wr(12'h014, 1); + idle(); + rd(12'h024, s_value); + if ((s_value & 32'h61) != 1) $fatal(1, "SIM_TEST_FAIL command %h status=%h", opcode, s_value); + endtask + + task automatic write_burst(input logic [31:0] addr, input int beats, input logic [2:0] size, + input logic [1:0] expected, input logic [31:0] pattern, + input logic [3:0] strobes); + @(negedge s_clk); + axi.awaddr = addr; + axi.awlen = 8'(beats - 1); + axi.awsize = size; + axi.awvalid = 1; + do @(posedge s_clk); while (!axi.awready); + @(negedge s_clk); + axi.awvalid = 0; + for (int b = 0; b < beats; b++) begin + axi.wdata = pattern + 32'(b); + axi.wstrb = size < 2 ? 4'(((1 << (1 << size)) - 1) << ((addr + (b << size)) & 3)) : strobes; + axi.wlast = (b == beats - 1); + axi.wvalid = 1; + do @(posedge s_clk); while (!axi.wready); + @(negedge s_clk); + axi.wvalid = 0; + end + do @(posedge s_clk); while (!axi.bvalid); + if (axi.bresp !== expected) + $fatal(1, "SIM_TEST_FAIL write addr=%h resp=%h expected=%h", addr, axi.bresp, expected); + @(negedge s_clk); + endtask + task automatic read_burst(input logic [31:0] addr, input int beats, input logic [1:0] expected, + input logic [31:0] pattern); + @(negedge s_clk); + axi.araddr = addr; + axi.arlen = 8'(beats - 1); + axi.arvalid = 1; + do @(posedge s_clk); while (!axi.arready); + @(negedge s_clk); + axi.arvalid = 0; + for (int b = 0; b < beats; b++) begin + do @(posedge s_clk); while (!axi.rvalid); + if (axi.rresp !== expected || axi.rlast !== (b == beats - 1)) + $fatal(1, "SIM_TEST_FAIL read response addr=%h beat=%0d resp=%h", addr, b, axi.rresp); + if (expected == 0 && axi.rdata !== pattern + 32'(b)) + $fatal( + 1, + "SIM_TEST_FAIL readback addr=%h beat=%0d got=%h expected=%h", + addr, + b, + axi.rdata, + pattern + 32'(b) + ); + @(negedge s_clk); + end + idle(); + endtask + + task automatic held_read; + @(negedge s_clk); + axi.araddr = 32'h54000000; + axi.arlen = 1; + axi.arvalid = 1; + axi.rready = 0; + do @(posedge s_clk); while (!axi.arready); + @(negedge s_clk); + axi.arvalid = 0; + do @(posedge s_clk); while (!axi.rvalid); + for (int i = 0; i < 250; i++) begin + @(negedge s_clk); + if (!axi.rvalid || axi.rresp !== 0 || axi.rdata !== 32'h12345678 || axi.rlast) + $fatal(1, "SIM_TEST_FAIL held read changed across core timeout"); + end + axi.rready = 1; + @(posedge s_clk); + @(negedge s_clk); + do @(posedge s_clk); while (!axi.rvalid); + if (axi.rresp !== 2 || !axi.rlast) $fatal(1, "SIM_TEST_FAIL timeout tail"); + @(negedge s_clk); + idle(); + endtask + + initial begin + #2000000; + $fatal(1, "SIM_TEST_TIMEOUT Tiny P8 XPI"); + end + initial begin + apb.psel = 0; + apb.penable = 0; + apb.pwrite = 0; + apb.paddr = 0; + apb.pwdata = 0; + apb.pstrb = 0; + apb.pprot = 0; + axi.awid = 0; + axi.awaddr = 0; + axi.awlen = 0; + axi.awsize = 2; + axi.awburst = 1; + axi.awlock = 0; + axi.awcache = 0; + axi.awprot = 0; + axi.awqos = 0; + axi.awregion = 0; + axi.awuser = 0; + axi.awvalid = 0; + axi.wdata = 0; + axi.wstrb = 0; + axi.wlast = 0; + axi.wuser = 0; + axi.wvalid = 0; + axi.bready = 1; + axi.arid = 0; + axi.araddr = 0; + axi.arlen = 0; + axi.arsize = 2; + axi.arburst = 1; + axi.arlock = 0; + axi.arcache = 0; + axi.arprot = 0; + axi.arqos = 0; + axi.arregion = 0; + axi.aruser = 0; + axi.arvalid = 0; + axi.rready = 1; + repeat (5) @(negedge s_clk); + s_rst_n = 1; + #150000; + wr(12'h00c, 0); + wr(12'h120, 1); + wr(12'h124, 32'h800000); + wr(12'h128, 32'h21); + wr(12'h12c, 32'h02010100); + wr(12'h130, 192); + wr(12'h134, 1024); + program_lut(1, 16'h18eb, 16'h2818, 16'h4006, 16'h6800); + program_lut(2, 16'h1838, 16'h2818, 16'h5800, 0); + // Read ID: SPI 9f, 24 address bits, six bytes, no dummy clocks. + program_lut(4, 16'h109f, 16'h2018, 16'h6000, 0); + wr(12'h00c, 1); + command(8'h66, 0); + command(8'h99, 0); + #100; + wr(12'h024, 255); + wr(12'h048, 0); + wr(12'h04c, 6); + wr(12'h050, 32'h41); + wr(12'h014, 1); + idle(); + rd(12'h044, s_value); + if (s_value[23:0] !== 24'h265d0d) $fatal(1, "SIM_TEST_FAIL PSRAM identity %h", s_value); + rd(12'h044, s_value); + command(8'h35, 0); + idle(); + wr(12'h00c, 0); + wr(12'h120, 7); + wr(12'h00c, 1); + + if ($test$plusargs("boundary")) begin + read_burst(32'h540003fc, 2, 2, 0); + read_burst(32'h54000ffc, 2, 2, 0); + read_burst(32'h54800000, 1, 2, 0); + write_burst(32'h540003fc, 2, 2, 2, 0, 4'hf); + write_burst(32'h54000ffc, 2, 2, 2, 0, 4'hf); + write_burst(32'h54800000, 1, 2, 2, 0, 4'hf); + write_burst(32'h00000000, 1, 2, 2, 0, 4'hf); + end else if ($test$plusargs("lanes")) begin + write_burst(32'h54000200, 1, 2, 0, 32'h00000000, 4'hf); + write_burst(32'h54000200, 4, 0, 0, 32'h44332211, 4'hf); + read_burst(32'h54000200, 1, 0, 32'h44332211); + write_burst(32'h54000200, 2, 1, 0, 32'h88776655, 4'hf); + read_burst(32'h54000200, 1, 0, 32'h88776655); + write_burst(32'h54000200, 1, 2, 0, 32'h01020304, 4'h5); + read_burst(32'h54000200, 1, 0, 32'h88026604); + write_burst(32'h54000200, 1, 2, 0, 32'hffffffff, 4'h0); + read_burst(32'h54000200, 1, 0, 32'h88026604); + end else if ($test$plusargs("timeout")) begin + // A mapped TX must retire with error even if the core times out + // before accepting its first payload byte. + idle(); + wr(12'h00c, 0); + wr(12'h130, 1); + wr(12'h00c, 1); + write_burst(32'h54000000, 1, 2, 2, 32'h12345678, 4'h1); + end else if ($test$plusargs("short_read")) begin + idle(); + wr(12'h00c, 0); + program_lut(1, 0, 0, 0, 0); + wr(12'h00c, 1); + read_burst(32'h54000000, 1, 2, 0); + end else if (!$test$plusargs("identity")) begin + write_burst(32'h54000000, 1, 2, 0, 32'h12345678, 4'hf); + if ($test$plusargs("held_read")) held_read(); + else if ($test$plusargs("late_error")) begin + idle(); + wr(12'h00c, 0); + wr(12'h218, 32'h0000f000); + wr(12'h00c, 1); + read_burst(32'h54000000, 1, 2, 0); + end else begin + read_burst(32'h54000000, 1, 0, 32'h12345678); + write_burst(32'h547ffffc, 1, 2, 0, 32'h89abcdef, 4'hf); + read_burst(32'h547ffffc, 1, 0, 32'h89abcdef); + end + end + $display("SIM_TEST_PASS Tiny P8 XPI pins"); + $finish; + end +endmodule diff --git a/tests/test_psram.py b/tests/test_psram.py index 4f2d8aad..637eb1a7 100644 --- a/tests/test_psram.py +++ b/tests/test_psram.py @@ -59,7 +59,7 @@ def test_psram_controller_data_integrity_and_fault_isolation(tmp_path: Path) -> str(ROOT / "rtl/ip/memory/psram_phy.sv"), str(ROOT / "rtl/ip/memory/psram_reg.sv"), str(ROOT / "rtl/ip/memory/apb4_psram.sv"), - str(ROOT / "rtl/mini/dv/model/ESP_PSRAM64H.sv"), + str(ROOT / "rtl/model/ESP_PSRAM64H.sv"), str(ROOT / "tests/rtl/psram_tb.sv"), "-Mdir", str(tmp_path / "obj"), diff --git a/tests/test_tiny.py b/tests/test_tiny.py index 330520f4..c39b742d 100644 --- a/tests/test_tiny.py +++ b/tests/test_tiny.py @@ -49,6 +49,9 @@ def test_tiny_sources_and_abi_are_independent(tmp_path: Path) -> None: for source in resolved.files: assert "/rtl/mini/" not in str(source) assert not any(token in source.name.lower() for token in ("rib", "vexii", "radio")) + devices = parse_filelists([filelists / "tb.fl"]) + assert ROOT / "rtl/model/ESP_PSRAM64H.sv" in devices.files + assert all("/rtl/mini/" not in str(source) for source in devices.files) rtl = (output / "memory_map/rtl/mmap_define.svh").read_text() sdk = (output / "memory_map/include/retrosoc/generated/memory_map.h").read_text() assert "RIB" not in rtl diff --git a/tests/test_tiny_r2_p8.py b/tests/test_tiny_r2_p8.py new file mode 100644 index 00000000..bb4e3001 --- /dev/null +++ b/tests/test_tiny_r2_p8.py @@ -0,0 +1,34 @@ +"""Pin-level XPI PSRAM transport; no fast NOR or separate RAM controller.""" +import subprocess + +import pytest + +from test_tiny_r2_p7 import COMMON, ROOT, run_rtl +from scripts.check_simulation import DEFAULT_FAILURE + + +@pytest.fixture(scope="module", params=["iverilog", "verilator"]) +def xpi_simulator(request, tmp_path_factory): + simulator = request.param + tmp_path = tmp_path_factory.mktemp(f"p8-{simulator}") + run_rtl(tmp_path, simulator, "tiny_p8_xpi_tb", [ + COMMON / "utils/fifo.sv", COMMON / "interface/axi4_addr_gen.sv", + *[ROOT / "rtl/ip/storage" / name for name in ( + "xpi_if.sv", "xpi_reg.sv", "xpi_mm.sv", "xpi_indirect.sv", + "xpi_clkgen.sv", "xpi_core.sv", "apb4_xpi.sv")], + ROOT / "rtl/model/ESP_PSRAM64H.sv", + ], defines=[f"-I{ROOT / 'rtl/ip/storage'}", "-DRETROSOC_SOC__TINY"], + packages=[ROOT / "rtl/ip/storage/xpi_pkg.sv"], plusargs=["+identity"]) + launch = [str(tmp_path / "sim")] if simulator == "verilator" else ["vvp", str(tmp_path / "sim")] + return launch, tmp_path + + +@pytest.mark.parametrize("scenario", ["mapped", "boundary", "lanes", "timeout", "held_read", + "short_read", "late_error"]) +def test_xpi_psram_pins(xpi_simulator, scenario: str) -> None: + launch, directory = xpi_simulator + result = subprocess.run([*launch, f"+{scenario}"], capture_output=True, text=True, timeout=30) + log = result.stdout + result.stderr + (directory / f"{scenario}.log").write_text(log) + assert result.returncode == 0 and not DEFAULT_FAILURE.search(log), log + assert "SIM_TEST_PASS Tiny P8 XPI pins" in log, log diff --git a/tests/test_tiny_r2_p8_evidence.py b/tests/test_tiny_r2_p8_evidence.py new file mode 100644 index 00000000..907f63f2 --- /dev/null +++ b/tests/test_tiny_r2_p8_evidence.py @@ -0,0 +1,63 @@ +"""Reject incomplete P8 transport/optional-device evidence.""" +import pytest +from pathlib import Path + +from scripts.tiny_r2_p8 import measurements + +CONFIG = {"PDK": "IHP130", "EXT_CLK_HZ": "24000000"} + + +@pytest.fixture +def valid_log(): + return "\n".join([ + "P8_TRANSPORT sys_hz=24000000 mem_hz=24000000 bytes=1024 burst=4 " + "isolated_cycles=24000 contended_cycles=48000 progress=10", + *[f"P8_XPI_WINDOW index={i} mem_cycles=24000 " + f"write_bytes={1024 if i % 2 == 0 else 0} read_bytes={0 if i % 2 == 0 else 1024} " + "aw_max=10 w_max=20 ar_max=30 b_max=40 r_max=50" for i in range(4)], + "P8_RESULT code=0 profiles=1", "SIM_TEST_PASS Tiny cycles=100000", + ]) + + +def test_p8_measurement_units(valid_log): + result = measurements(valid_log, CONFIG) + assert result["profiles"][0]["isolated_bytes_per_second"] == 1024000 + assert result["profiles"][0]["contended_bytes_per_second"] == 512000 + assert result["windows"][0]["b_max_ns"] == pytest.approx(40e9 / 24e6) + + +@pytest.mark.parametrize("old,new", [ + ("SIM_TEST_PASS Tiny cycles=100000", "Hello retroSoC!"), + ("profiles=1", "profiles=0"), ("code=0", "code=3"), + ("write_bytes=1024", "write_bytes=1020"), ("progress=10", "progress=0"), + ("aw_max=10", "aw_max=25000"), ("isolated_cycles=24000", "isolated_cycles=0"), +]) +def test_p8_rejects_incomplete_measurements(valid_log, old, new): + with pytest.raises(ValueError): + measurements(valid_log.replace(old, new), CONFIG) + + +def test_p8_does_not_promote_missing_profiles_or_errors(valid_log): + with pytest.raises(ValueError): + measurements(valid_log, {"PDK": "ICS55", "EXT_CLK_HZ": "24000000"}) + with pytest.raises(ValueError): + measurements(valid_log + "\nSIM_TEST_FAIL", CONFIG) + with pytest.raises(ValueError): + measurements(valid_log, CONFIG, absent=True) + + +def test_p8_optional_absence_has_separate_verdict(): + log = ("P8_ABSENT bounded optional-device rejection\nP8_RESULT code=0 profiles=0\n" + "SIM_TEST_PASS Tiny cycles=100000\n") + assert measurements(log, CONFIG, absent=True)["mode"] == "absent" + with pytest.raises(ValueError): + measurements(log, CONFIG) + + +def test_p8_observer_labels_match_axi_channels(): + source = (Path(__file__).resolve().parents[1] / + "rtl/tiny/dv/tiny_p8_observers.svh").read_text() + order = source.split("wire [4:0] s_p8_stalls = {", 1)[1].split("};", 1)[0] + channels = [line.split("`_P8_AXI.", 1)[1].split("valid", 1)[0] + for line in order.splitlines() if "`_P8_AXI." in line] + assert channels == ["aw", "w", "ar", "b", "r"]