diff --git a/.github/workflows/_regression.yml b/.github/workflows/_regression.yml
index 2c2a24d9..5c76cffe 100644
--- a/.github/workflows/_regression.yml
+++ b/.github/workflows/_regression.yml
@@ -132,7 +132,7 @@ jobs:
- name: Upload reports and structured results
if: always()
- uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: ${{ inputs.suite }}-regression-${{ github.run_id }}-${{ github.run_attempt }}
path: |
diff --git a/.github/workflows/development-environment.yml b/.github/workflows/development-environment.yml
index c95bc215..2fbbe8c6 100644
--- a/.github/workflows/development-environment.yml
+++ b/.github/workflows/development-environment.yml
@@ -110,7 +110,7 @@ jobs:
- name: Upload reports and structured results
if: always()
- uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: environment-${{ matrix.environment }}-ihp130-${{ github.run_id }}-${{ github.run_attempt }}
path: |
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 78306082..fe4ffdc5 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -46,7 +46,7 @@ jobs:
run: make CONFIG=configs/ci/ihp130.mk package
- name: Upload workflow artifact
- uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4.3.3
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: retrosoc-${{ github.ref_name }}
path: dist/**
diff --git a/Makefile b/Makefile
index 6504b975..7510e603 100644
--- a/Makefile
+++ b/Makefile
@@ -56,7 +56,7 @@ JTAG_IDCODE ?= DEADBEEF
EXT_CLK_HZ ?= 72000000
AUD_CLK_HZ ?= 18432000
CLINT_TIMEBASE_HZ ?= 1000000
-MGMT_CPU_CLK_HZ := $(if $(filter PRODUCT,$(MINI_MODE)),24000000,$(EXT_CLK_HZ))
+MGMT_CPU_CLK_HZ := $(if $(filter TINY,$(SOC)),24000000,$(if $(filter PRODUCT,$(MINI_MODE)),24000000,$(EXT_CLK_HZ)))
# Reset-state peripheral clock: the PCLK divider in the clock/reset subsystem
# resets to passthrough on the LP root clock, so PCLK starts at the management
# CPU frequency until software programs a different divider.
@@ -84,6 +84,7 @@ SW_ISA_PROFILE ?= COMPAT
SW_OPT ?= APP
SW_LTO ?= NO
WS2812_P3_ACCEPTANCE ?= NO
+TINY_P7_ACCEPTANCE ?= NO
COREMARK_MODE ?= quick
NPU_P5_ACCEPTANCE ?= NO
# Select the HP (OpenC906) JTAG target before the LP releases it; debug aid
@@ -132,6 +133,9 @@ endif
ifeq ($(WS2812_P3_ACCEPTANCE),YES)
CONFIG_KEY_VARS += WS2812_P3_ACCEPTANCE
endif
+ifeq ($(TINY_P7_ACCEPTANCE),YES)
+CONFIG_KEY_VARS += TINY_P7_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))
@@ -291,6 +295,12 @@ $(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))
+$(call validate_value,TINY_P7_ACCEPTANCE,$(VALID_BOOL))
+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)
+endif
+endif
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)
@@ -387,7 +397,7 @@ endif
ifeq ($(STA),OPENSTA)
ifeq ($(HAVE_PLL),YES)
-ifneq ($(SOC)/$(PDK)/$(TINY_SAFE24_PLL_OFF),TINY/ICS55/YES)
+ifneq ($(SOC)/$(PDK)/$(TINY_R2_P7),TINY/ICS55/YES)
$(error STA=OPENSTA requires a qualified PDK PLL timing profile; HAVE_PLL=YES is unsupported)
endif
endif
@@ -424,6 +434,8 @@ endif
ifeq ($(HAVE_SVA), NO)
DEF_LIST += +define+SV_ASSRT_DISABLE
+else ifeq ($(SOC),TINY)
+ DEF_LIST += +define+HAVE_SVA
endif
ifeq ($(HAVE_HP), YES)
diff --git a/README.md b/README.md
index 41a07c2c..f53eeeda 100644
--- a/README.md
+++ b/README.md
@@ -138,8 +138,10 @@ points, not the complete profile or application inventory.
Tiny's [2026-10-07 target refreeze](docs/ip/tiny-soc.md#ics55-default-platform-and-final-timing-gate-2026-10-07)
selects ICS55 with `HAVE_PLL=YES` and SAFE24 startup as the default.
-The profile instantiates a parked PLL; dynamic SYS switching remains R2-P7 and
-physical qualification is pending. IHP130 remains explicit compatibility.
+The PLL starts parked off. The [R2-P7 implementation](docs/ip/tiny-soc-r2-clock-reset.md)
+adds software-controlled SYS transitions; acceptance evidence is tracked in
+the Tiny ledger and physical qualification remains pending. IHP130 remains
+explicit compatibility.
Locked PLL views can be acquired
with `python3 physical/pdk/setup.py --pdk ICS55 --component pll` independently
of platform implementation or timing qualification.
@@ -266,4 +268,4 @@ Thanks to everyone contributing to retroSoC.
-
\ No newline at end of file
+
diff --git a/app/README.md b/app/README.md
index 8ebe3f1b..57781b97 100644
--- a/app/README.md
+++ b/app/README.md
@@ -176,7 +176,11 @@ configurations, and build artifact locations.
For `SOC=TINY`, `bringup` and `ci_smoke` use the MCU acceptance composition. It
boots through XPI into SRAM, checks CPU C/A behavior and bus exceptions, DMA
-(including UART requests), interrupts, RTC, GPIO, UART1 and I2C error completion,
-then verifies watchdog reset by completing after the next NOR boot. It uses
+(including UART requests), interrupts, RTC, GPIO and I2C0 error completion.
+The P6 composition additionally checks RNG qualification rejection, CRC DMA,
+Crypto initialization/vectors/erasure, audio plus finite WS refill and background
+DMA, and SDIO transfers/recovery with verification-board models. Missing external
+devices fail boundedly; they do not hold CPU boot reset. The image then verifies
+watchdog reset by completing after the next NOR boot. It uses
SYSCTRL terminal status. The small `asm/tiny_boot.S` image separately qualifies
the synthesized SRAM/pad path; it is not a replacement for the full RTL test.
diff --git a/app/apps/bringup/tiny.mk b/app/apps/bringup/tiny.mk
index fd133f66..240ff06f 100644
--- a/app/apps/bringup/tiny.mk
+++ b/app/apps/bringup/tiny.mk
@@ -1,3 +1,4 @@
APP_SRCS += $(ROOT_PATH)/app/apps/ci_smoke/tiny.c
APP_CFLAGS += -Os
-APP_SRCS += $(ROOT_PATH)/app/apps/ci_smoke/mcu_acceptance.S
\ No newline at end of file
+APP_SRCS += $(ROOT_PATH)/app/apps/ci_smoke/mcu_acceptance.S
+APP_SRCS += $(ROOT_PATH)/app/apps/ci_smoke/tiny_r2_p6.c
\ No newline at end of file
diff --git a/app/apps/ci_smoke/mcu_acceptance.S b/app/apps/ci_smoke/mcu_acceptance.S
index e48bfe8c..cd4ee824 100644
--- a/app/apps/ci_smoke/mcu_acceptance.S
+++ b/app/apps/ci_smoke/mcu_acceptance.S
@@ -34,3 +34,15 @@ rs_mcu_unmapped_probe:
.option pop
ret
.size rs_mcu_unmapped_probe, .-rs_mcu_unmapped_probe
+
+ .balign 4
+ .global rs_mcu_execute_probe
+ .type rs_mcu_execute_probe,@function
+rs_mcu_execute_probe:
+ mv t0, a0
+ .option push
+ .option arch, +zifencei
+ fence.i
+ .option pop
+ jr t0
+ .size rs_mcu_execute_probe, .-rs_mcu_execute_probe
diff --git a/app/apps/ci_smoke/mcu_acceptance.h b/app/apps/ci_smoke/mcu_acceptance.h
index 3f05d234..2c791b1d 100644
--- a/app/apps/ci_smoke/mcu_acceptance.h
+++ b/app/apps/ci_smoke/mcu_acceptance.h
@@ -6,5 +6,6 @@
uint32_t rs_mcu_compressed_probe(void);
void rs_mcu_atomic_probe(uint32_t *address);
void rs_mcu_unmapped_probe(void);
+uint32_t rs_mcu_execute_probe(uint32_t address);
#endif
diff --git a/app/apps/ci_smoke/mcu_shared_acceptance.h b/app/apps/ci_smoke/mcu_shared_acceptance.h
new file mode 100644
index 00000000..873f1abc
--- /dev/null
+++ b/app/apps/ci_smoke/mcu_shared_acceptance.h
@@ -0,0 +1,9 @@
+#ifndef RETROSOC_APP_MCU_SHARED_ACCEPTANCE_H
+#define RETROSOC_APP_MCU_SHARED_ACCEPTANCE_H
+
+#include
+
+/* Zero means every P6 acceptance case completed; other values identify failure. */
+uint8_t rs_tiny_p6_acceptance(void);
+
+#endif
diff --git a/app/apps/ci_smoke/tiny.c b/app/apps/ci_smoke/tiny.c
index df318bc1..902a6b81 100644
--- a/app/apps/ci_smoke/tiny.c
+++ b/app/apps/ci_smoke/tiny.c
@@ -12,6 +12,7 @@
#include
#include
#include
+#include
#include
#include
#include
@@ -20,12 +21,16 @@
#include
#include "mcu_acceptance.h"
+#include "mcu_shared_acceptance.h"
static _Alignas(64) uint32_t rs_mcu_source[80];
static _Alignas(64) uint32_t rs_mcu_destination[80];
static volatile uint32_t rs_mcu_irq_count;
static volatile uint32_t rs_mcu_exception_count;
static volatile bool rs_mcu_handler_failed;
+static volatile bool rs_mcu_probes_active;
+static uint8_t rs_mcu_io_code;
+static volatile uint32_t rs_mcu_watchdog_cookie __attribute__((section(".noinit.rs_mcu_wdg")));
static void rs_mcu_finish(bool pass, uint8_t code) {
printf("Tiny acceptance %s code=%u\n", pass ? "passed" : "failed", code);
@@ -39,8 +44,8 @@ static void rs_mcu_finish(bool pass, uint8_t code) {
static void rs_mcu_exception(uintptr_t mcause, uintptr_t stack_pointer) {
volatile uint32_t *const frame = (volatile uint32_t *)stack_pointer;
- if ((mcause != UINT32_C(2)) && (mcause != UINT32_C(5))) {
- rs_mcu_handler_failed = true;
+ if (!rs_mcu_probes_active || ((mcause != UINT32_C(2)) && (mcause != UINT32_C(5)))) {
+ rs_mcu_finish(false, 11U);
}
++rs_mcu_exception_count;
/* system_irq.S saves MEPC in slot 11 of the RV32 trap frame. Both test
@@ -75,7 +80,7 @@ static bool rs_mcu_dma(void) {
rs_mcu_source[index] = UINT32_C(0xa519c300) ^ index;
rs_mcu_destination[index] = UINT32_C(0xcccccccc);
}
- if ((rs_dma_configure(4U, &config) != RS_EINVAL) ||
+ if ((rs_dma_configure(8U, &config) != RS_EINVAL) ||
(rs_dma_configure(RS_DMA_CHANNEL_BULK, &config) != RS_OK) ||
(rs_dma_start(RS_DMA_CHANNEL_BULK) != RS_OK) ||
(rs_dma_wait(RS_DMA_CHANNEL_BULK, RS_TIMEOUT_DEFAULT) != RS_OK)) {
@@ -91,8 +96,26 @@ static bool rs_mcu_dma(void) {
return false;
}
config.kind = RS_DMA_KIND_MM_TO_STREAM;
- config.request = RS_DMA_REQUEST_I2S_TX;
- return rs_dma_config_validate(0U, &config) != RS_OK;
+ config.request = RS_DMA_REQUEST_DVP_RX;
+ if (rs_dma_config_validate(0U, &config) == RS_OK) {
+ return false;
+ }
+ /* DMA owns the destination until completion. FENCE.I is executed by the
+ * assembly helper before entering either the DMA-written or patched code. */
+ rs_mcu_source[0] = UINT32_C(0x02a00513); /* addi a0,zero,42 */
+ rs_mcu_source[1] = UINT32_C(0x00008067); /* jalr zero,ra,0 */
+ config.kind = RS_DMA_KIND_MM_TO_MM;
+ config.request = RS_DMA_REQUEST_SOFTWARE;
+ config.byte_count = UINT32_C(8);
+ config.burst_beats = 1U;
+ if ((rs_dma_configure(RS_DMA_CHANNEL_BULK, &config) != RS_OK) ||
+ (rs_dma_start(RS_DMA_CHANNEL_BULK) != RS_OK) ||
+ (rs_dma_wait(RS_DMA_CHANNEL_BULK, RS_TIMEOUT_DEFAULT) != RS_OK) ||
+ (rs_mcu_execute_probe((uint32_t)(uintptr_t)rs_mcu_destination) != UINT32_C(42))) {
+ return false;
+ }
+ rs_mcu_destination[0] = UINT32_C(0x02b00513); /* addi a0,zero,43 */
+ return rs_mcu_execute_probe((uint32_t)(uintptr_t)rs_mcu_destination) == UINT32_C(43);
}
static bool rs_mcu_interrupts(void) {
@@ -146,9 +169,7 @@ static bool rs_mcu_io(void) {
const uint32_t transmit[4] = {UINT32_C(0x54), UINT32_C(0x49), UINT32_C(0x4e), UINT32_C(0x59)};
uint32_t receive[4] = {0U};
rs_gpio_capabilities_t capabilities;
- rs_uart_timing_t timing;
bool high;
- uint32_t timeout = UINT32_C(10000);
uint8_t payload = UINT8_C(0x55);
const rs_i2c_transfer_t transfer = {
.address = UINT16_C(0x50),
@@ -157,54 +178,51 @@ static bool rs_mcu_io(void) {
};
rs_i2c_status_t status;
+ rs_mcu_io_code = 40U;
if ((rs_gpio_get_capabilities(&capabilities) != RS_OK) ||
((capabilities.pad_features & (RS_GPIO_PAD_CAP_PULL_UP | RS_GPIO_PAD_CAP_PULL_DOWN)) !=
((RS_SOC_GPIO_HAS_PULLS != 0U) ? (RS_GPIO_PAD_CAP_PULL_UP | RS_GPIO_PAD_CAP_PULL_DOWN)
: 0U)) ||
- (rs_gpio_configure(15U, &pull_up) !=
+ (rs_gpio_configure(27U, &pull_up) !=
((RS_SOC_GPIO_HAS_PULLS != 0U) ? RS_OK : RS_ENOTSUP)) ||
- (rs_gpio_configure(15U, &pull_down) !=
+ (rs_gpio_configure(27U, &pull_down) !=
((RS_SOC_GPIO_HAS_PULLS != 0U) ? RS_OK : RS_ENOTSUP)) ||
- (rs_gpio_configure(15U, &output) != RS_OK) || (rs_gpio_write(15U, true) != RS_OK) ||
- (rs_gpio_read(15U, &high) != RS_OK) || !high || (rs_gpio_write(15U, false) != RS_OK) ||
- (rs_gpio_read(15U, &high) != RS_OK) || high) {
+ (rs_gpio_configure(27U, &output) != RS_OK) || (rs_gpio_write(27U, true) != RS_OK) ||
+ (rs_gpio_read(27U, &high) != RS_OK) || !high || (rs_gpio_write(27U, false) != RS_OK) ||
+ (rs_gpio_read(27U, &high) != RS_OK) || high) {
return false;
}
+ rs_mcu_io_code = 41U;
if ((rs_uart_configure(&loopback, RS_TIMEOUT_DEFAULT) != RS_OK) ||
(rs_uart_write_dma(transmit, 4U, RS_TIMEOUT_DEFAULT) != RS_OK) ||
(rs_uart_read_dma(receive, 4U, RS_TIMEOUT_DEFAULT) != RS_OK)) {
return false;
}
+ rs_mcu_io_code = 42U;
for (uint32_t index = 0U; index < 4U; ++index) {
if (transmit[index] != receive[index]) {
return false;
}
}
+ rs_mcu_io_code = 43U;
if ((rs_uart_init(RS_CPU_CLOCK_HZ, UART_BPS) != RS_OK) ||
- (rs_uart_timing_calculate(RS_CPU_CLOCK_HZ, UART_BPS, &timing) != RS_OK)) {
- return false;
- }
- /* UART1 uses the same register ABI; the current shared high-level UART
- * API selects UART0. Exercise UART1 PIO and its internal loopback here. */
- RS_SOC_REG32(RS_SOC_APB4_UART1_BASE, UINT32_C(0x00)) = timing.baud_integer;
- RS_SOC_REG32(RS_SOC_APB4_UART1_BASE, UINT32_C(0x04)) = timing.baud_fraction;
- RS_SOC_REG32(RS_SOC_APB4_UART1_BASE, UINT32_C(0x08)) = UINT32_C(3);
- RS_SOC_REG32(RS_SOC_APB4_UART1_BASE, UINT32_C(0x0c)) = UINT32_C(7);
- RS_SOC_REG32(RS_SOC_APB4_UART1_BASE, UINT32_C(0x10)) = UINT32_C(0x5a);
- while (((RS_SOC_REG32(RS_SOC_APB4_UART1_BASE, UINT32_C(0x18)) & UINT32_C(0x40)) != 0U) &&
- (timeout != 0U)) {
- --timeout;
- }
- if ((timeout == 0U) || ((RS_SOC_REG32(RS_SOC_APB4_UART1_BASE, UINT32_C(0x14)) &
- UINT32_C(0xff)) != UINT32_C(0x5a))) {
+ (rs_i2c_init(RS_I2C_BUS_1, RS_CPU_CLOCK_HZ, UINT32_C(100000)) != RS_ENOTSUP)) {
return false;
}
/* There is no slave on the pulled-up acceptance board: a real address
* transaction must terminate as NACK rather than stick waiting on the bus. */
- for (uint32_t bus = 0U; bus < 2U; ++bus) {
+ for (uint32_t bus = 0U; bus < 1U; ++bus) {
+ rs_mcu_io_code = 44U;
const rs_i2c_bus_t instance = (bus == 0U) ? RS_I2C_BUS_0 : RS_I2C_BUS_1;
- if ((rs_i2c_init(instance, RS_CPU_CLOCK_HZ, UINT32_C(100000)) != RS_OK) ||
- (rs_i2c_transfer(instance, &transfer, RS_TIMEOUT_DEFAULT) != RS_EIO) ||
+ rs_status_t init_result = rs_i2c_init(instance, RS_CPU_CLOCK_HZ, UINT32_C(100000));
+ rs_status_t transfer_result = init_result == RS_OK
+ ? rs_i2c_transfer(instance, &transfer, RS_TIMEOUT_DEFAULT)
+ : init_result;
+ printf("R2_P6_IO i2c_init=%d i2c_transfer=%d\n", (int)init_result, (int)transfer_result);
+ if (rs_i2c_get_status(instance, &status) == RS_OK) {
+ printf("R2_P6_IO i2c_errors=%lx\n", (unsigned long)status.errors);
+ }
+ if ((init_result != RS_OK) || (transfer_result != RS_EIO) ||
(rs_i2c_get_status(instance, &status) != RS_OK) ||
((status.errors & RS_I2C_ERROR_ADDR_NACK) == 0U)) {
return false;
@@ -235,8 +253,10 @@ int main(void) {
rs_onchip_sram_info_t memory;
rs_sysctrl_fault_status_t fault;
rs_watchdog_status_t watchdog = {0};
+ rs_tiny_rcu_snapshot_t reset_snapshot;
uint64_t before;
uint64_t after;
+ uint8_t p6_result;
const rs_watchdog_config_t watchdog_config = {
.prescale_divider = 1U,
.timeout_ticks = UINT32_C(20000),
@@ -251,12 +271,21 @@ int main(void) {
rs_mcu_finish(false, 1U);
}
printf("Hello retroSoC! Tiny MCU\n");
- if (rs_watchdog_get_status(&watchdog, RS_TIMEOUT_DEFAULT) != RS_OK) {
+ if (rs_watchdog_get_status(&watchdog, RS_TIMEOUT_DEFAULT) != RS_OK ||
+ rs_tiny_rcu_get_snapshot(&reset_snapshot) != RS_OK) {
rs_mcu_finish(false, 2U);
}
- if (watchdog.reset_count != 0U) {
- rs_mcu_finish(true, 0U);
+ if ((reset_snapshot.reset_cause & UINT32_C(4)) != 0U) {
+ /* P7 resets WDG functional state. The AON RCU owns the retained cause;
+ * the SRAM cookie proves all pre-reset acceptance work completed. */
+ const bool reset_valid =
+ rs_mcu_watchdog_cookie == UINT32_C(0x57375037) && watchdog.reset_count == 0U &&
+ reset_snapshot.current_profile == 0U && reset_snapshot.sys_hz == UINT32_C(24000000) &&
+ reset_snapshot.gate_status == 0U;
+ rs_mcu_watchdog_cookie = 0U;
+ rs_mcu_finish(reset_valid, reset_valid ? 0U : 2U);
}
+ rs_mcu_watchdog_cookie = 0U;
if ((rs_archinfo_read(&info) != RS_OK) || (rs_archinfo_validate_build(&info) != RS_OK) ||
(info.soc_id != RS_SOC_ID) || (info.topology != UINT32_C(0x20200001)) ||
(info.technology != RS_SOC_TECHNOLOGY_ID) ||
@@ -267,11 +296,14 @@ int main(void) {
}
if ((rs_mcu_compressed_probe() != 42U) || ((__RV_CSR_READ(CSR_MISA) & UINT32_C(1)) != 0U) ||
(rs_irq_register_exception(2U, rs_mcu_exception) != RS_OK) ||
- (rs_irq_register_exception(5U, rs_mcu_exception) != RS_OK)) {
+ (rs_irq_register_exception(5U, rs_mcu_exception) != RS_OK) ||
+ (rs_irq_register_exception(7U, rs_mcu_exception) != RS_OK)) {
rs_mcu_finish(false, 4U);
}
+ rs_mcu_probes_active = true;
rs_mcu_atomic_probe(rs_mcu_source);
rs_mcu_unmapped_probe();
+ rs_mcu_probes_active = false;
if ((rs_mcu_exception_count != 2U) || rs_mcu_handler_failed ||
(rs_sysctrl_get_fault_status(&fault) != RS_OK) || !fault.pending ||
(fault.address != UINT32_C(0x38000000)) || (rs_sysctrl_clear_fault() != RS_OK)) {
@@ -289,9 +321,14 @@ int main(void) {
rs_mcu_finish(false, 8U);
}
if (!rs_mcu_io()) {
- rs_mcu_finish(false, 10U);
+ rs_mcu_finish(false, rs_mcu_io_code);
+ }
+ p6_result = rs_tiny_p6_acceptance();
+ if (p6_result != 0U) {
+ rs_mcu_finish(false, p6_result);
}
printf("Tiny CPU/DMA/IRQ/RTC/I/O passed; exercising watchdog reset\n");
+ rs_mcu_watchdog_cookie = UINT32_C(0x57375037);
if ((rs_watchdog_configure(&watchdog_config) != RS_OK) ||
(rs_watchdog_start(RS_TIMEOUT_DEFAULT) != RS_OK)) {
rs_mcu_finish(false, 9U);
diff --git a/app/apps/ci_smoke/tiny.mk b/app/apps/ci_smoke/tiny.mk
index fd133f66..4478e0e2 100644
--- a/app/apps/ci_smoke/tiny.mk
+++ b/app/apps/ci_smoke/tiny.mk
@@ -1,3 +1,7 @@
APP_SRCS += $(ROOT_PATH)/app/apps/ci_smoke/tiny.c
APP_CFLAGS += -Os
-APP_SRCS += $(ROOT_PATH)/app/apps/ci_smoke/mcu_acceptance.S
\ No newline at end of file
+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
diff --git a/app/apps/ci_smoke/tiny_r2_p6.c b/app/apps/ci_smoke/tiny_r2_p6.c
new file mode 100644
index 00000000..ad74f328
--- /dev/null
+++ b/app/apps/ci_smoke/tiny_r2_p6.c
@@ -0,0 +1,801 @@
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "mcu_shared_acceptance.h"
+
+static volatile uint32_t rs_p6_irq_count;
+static uint32_t rs_p6_irq_selected;
+static volatile bool rs_p6_irq_error;
+static _Alignas(64) uint32_t rs_p6_source[256];
+static _Alignas(64) uint32_t rs_p6_destination[258];
+static _Alignas(64) uint32_t rs_p6_audio_tx[768];
+static _Alignas(64) uint32_t rs_p6_audio_rx[562];
+static _Alignas(64) rs_dma_tcd_t rs_p6_descriptors[4];
+static rs_dma_session_t rs_p6_audio_tx_owner;
+static rs_dma_session_t rs_p6_audio_rx_owner;
+static rs_dma_session_t rs_p6_background_owner;
+static uint32_t rs_p6_pixels[65];
+
+static void rs_p6_irq_handler(uintptr_t cause, uintptr_t stack) {
+ (void)cause;
+ (void)stack;
+ ++rs_p6_irq_count;
+ if (rs_irq_disable_external(rs_p6_irq_selected) != RS_OK) {
+ rs_p6_irq_error = true;
+ }
+}
+
+static bool rs_p6_irq_arm(uint32_t irq) {
+ rs_p6_irq_selected = irq;
+ rs_p6_irq_count = 0U;
+ rs_p6_irq_error = false;
+ if ((rs_irq_register_external(irq, rs_p6_irq_handler) != RS_OK) ||
+ (rs_irq_enable_external(irq, rs_p6_irq_handler) != RS_OK)) {
+ return false;
+ }
+ __RV_CSR_SET(CSR_MSTATUS, UINT32_C(8));
+ return true;
+}
+
+static bool rs_p6_irq_received(void) {
+ uint32_t timeout = UINT32_C(100000);
+ while ((rs_p6_irq_count == 0U) && (timeout != 0U)) {
+ --timeout;
+ }
+ return (rs_p6_irq_count == 1U) && !rs_p6_irq_error;
+}
+
+static bool rs_p6_route(uint32_t pin, bool open_drain) {
+ const rs_gpio_config_t config = {
+ .mode = RS_GPIO_MODE_ALT0,
+ .pull = RS_GPIO_PULL_NONE,
+ .trigger = RS_GPIO_TRIGGER_NONE,
+ .open_drain = open_drain,
+ .filter_enable = false,
+ };
+ return rs_gpio_configure(pin, &config) == RS_OK;
+}
+
+static bool rs_p6_uart(void) {
+ const uint8_t transmit[4] = {0xA5U, 0x5AU, 0x96U, 0x69U};
+ rs_uart_rx_data_t receive[4];
+ const rs_uart_config_t unsupported = {
+ .source_clock_hz = RS_PCLK_CLOCK_HZ,
+ .baud_rate = UART_BPS,
+ .data_bits = 8U,
+ .stop_bits = 1U,
+ .parity = RS_UART_PARITY_NONE,
+ .rx_watermark = 1U,
+ .rx_timeout_bits = 16U,
+ .tx_enable = true,
+ .rx_enable = true,
+ .auto_cts_enable = true,
+ };
+ if ((rs_uart_configure(&unsupported, RS_TIMEOUT_DEFAULT) != RS_ENOTSUP) ||
+ (rs_uart_write(transmit, sizeof(transmit), RS_TIMEOUT_DEFAULT) != RS_OK) ||
+ (rs_uart_read(receive, 4U, RS_TIMEOUT_DEFAULT) != RS_OK)) {
+ return false;
+ }
+ for (uint32_t index = 0U; index < 4U; ++index) {
+ if (receive[index].data != transmit[index] || receive[index].errors != 0U) {
+ return false;
+ }
+ }
+ printf("\nR2_P6_UART pads=1 echo_bytes=4 flow_rejected=1\n");
+ return true;
+}
+
+static bool rs_p6_rng(void) {
+ const rs_rng_config_t config = {.fifo_watermark = 1U};
+ rs_rng_snapshot_t snapshot;
+ uint32_t word = 0U;
+ if ((rs_rng_init(&config) != RS_OK) ||
+ (rs_rng_read_entropy(&word, RS_TIMEOUT_DEFAULT) != RS_ENOTSUP) ||
+ (rs_rng_read_diagnostic(&word, RS_TIMEOUT_DEFAULT) != RS_OK) ||
+ (rs_rng_get_status(&snapshot) != RS_OK) || snapshot.source_qualified ||
+ !rs_p6_irq_arm(RS_SOC_EXT_IRQ_RNG) ||
+ (rs_rng_interrupt_enable(RS_RNG_INTERRUPT_SOURCE_FAULT) != RS_OK) ||
+ (rs_rng_interrupt_test(RS_RNG_INTERRUPT_SOURCE_FAULT) != RS_OK) || !rs_p6_irq_received() ||
+ (rs_rng_interrupt_enable(0U) != RS_OK) ||
+ (rs_rng_interrupt_clear(RS_RNG_INTERRUPT_ALL) != RS_OK)) {
+ return false;
+ }
+ printf("R2_P6_RNG unqualified=1 diagnostic=1 irq=1\n");
+ return true;
+}
+
+static bool rs_p6_dma_capabilities(void) {
+ const uint32_t capability = RS_DMA_REG(RS_DMA_REG_CAPABILITY);
+ rs_dma_config_t config = {
+ .kind = RS_DMA_KIND_MM_TO_MM,
+ .request = RS_DMA_REQUEST_SOFTWARE,
+ .source = (uintptr_t)rs_p6_source,
+ .destination = (uintptr_t)rs_p6_destination,
+ .byte_count = 4U,
+ .width = RS_DMA_WIDTH_32,
+ .source_increment = true,
+ .destination_increment = true,
+ .burst_beats = 1U,
+ };
+ rs_dma_status_t status;
+ const uint32_t unsupported[] = {9U, 10U, 11U, 14U, 15U};
+ if (((capability >> 8U) & UINT32_C(0xFF)) != 8U) {
+ return false;
+ }
+ for (size_t index = 0U; index < sizeof(unsupported) / sizeof(unsupported[0]); ++index) {
+ rs_p6_destination[0] = UINT32_C(0x12345678);
+ if (rs_dma_configure(RS_DMA_CHANNEL_DVP, &config) != RS_OK) {
+ return false;
+ }
+ /* Exercise hardware rejection independently of the SDK's earlier gate. */
+ RS_DMA_CH_REG(RS_DMA_CHANNEL_DVP, RS_DMA_CH_REG_REQUEST_SEL) = unsupported[index];
+ if ((rs_dma_start(RS_DMA_CHANNEL_DVP) != RS_OK) ||
+ (rs_dma_wait(RS_DMA_CHANNEL_DVP, RS_TIMEOUT_DEFAULT) != RS_EIO) ||
+ (rs_dma_get_status(RS_DMA_CHANNEL_DVP, &status) != RS_OK) || !status.error ||
+ status.busy || (status.bytes_done != 0U) ||
+ (rs_p6_destination[0] != UINT32_C(0x12345678))) {
+ return false;
+ }
+ }
+ printf("R2_P6_DMA channels=8 rejected_requests=5\n");
+ return true;
+}
+
+static uint32_t rs_p6_crc_reference(const uint8_t *data, size_t bytes) {
+ uint32_t value = UINT32_MAX;
+ for (size_t index = 0U; index < bytes; ++index) {
+ value ^= (uint32_t)data[index];
+ for (uint32_t bit = 0U; bit < 8U; ++bit) {
+ value = (value >> 1U) ^ (((value & 1U) != 0U) ? UINT32_C(0xEDB88320) : 0U);
+ }
+ }
+ return ~value;
+}
+
+static bool rs_p6_crc(void) {
+ rs_crc_config_t crc;
+ static rs_dma_session_t lease;
+ uint32_t result;
+ rs_dma_config_t transfer = {
+ .kind = RS_DMA_KIND_MM_TO_MM,
+ .request = RS_DMA_REQUEST_SOFTWARE,
+ .source = (uintptr_t)rs_p6_source,
+ .destination = RS_SOC_APB4_CRC_BASE + CRC_DATA_OFFSET,
+ .byte_count = UINT32_C(64),
+ .width = RS_DMA_WIDTH_32,
+ .source_increment = true,
+ .burst_beats = 1U,
+ };
+ for (uint32_t index = 0U; index < 256U; ++index) {
+ rs_p6_source[index] = UINT32_C(0x12345678) ^ (index * UINT32_C(0x01020408));
+ }
+ if ((rs_crc_get_profile(RS_CRC_PROFILE_CRC32_ISO_HDLC, &crc) != RS_OK) ||
+ (rs_dma_session_acquire(&lease, RS_DMA_CHANNEL_BULK) != RS_OK)) {
+ return false;
+ }
+ for (size_t tail = 0U; tail < 4U; ++tail) {
+ if ((rs_crc_init(&crc) != RS_OK) || (rs_crc_start() != RS_OK) ||
+ (rs_dma_session_configure(&lease, &transfer) != RS_OK) ||
+ (rs_dma_session_start(&lease) != RS_OK) ||
+ (rs_dma_wait(RS_DMA_CHANNEL_BULK, RS_TIMEOUT_DEFAULT) != RS_OK) ||
+ (rs_crc_update((const uint8_t *)rs_p6_source + 64U, tail) != RS_OK) ||
+ (rs_crc_finish(&result) != RS_OK) ||
+ (result != rs_p6_crc_reference((const uint8_t *)rs_p6_source, 64U + tail))) {
+ return false;
+ }
+ }
+ if ((rs_dma_session_release(&lease) != RS_OK) ||
+ (rs_crc_compute(&crc, rs_p6_source, 0U, &result) != RS_OK) || (result != 0U)) {
+ return false;
+ }
+ printf("R2_P6_CRC dma=1 tails=4 empty=1\n");
+ return true;
+}
+
+static bool rs_p6_crypto(void) {
+ static uint32_t modulus[RS_CRYPTO_RSA_WORDS];
+ static uint32_t base[RS_CRYPTO_RSA_WORDS];
+ static uint32_t exponent[RS_CRYPTO_RSA_WORDS];
+ static uint32_t rsa_result[RS_CRYPTO_RSA_WORDS];
+ static const uint8_t key[16] = {
+ 0x00U, 0x01U, 0x02U, 0x03U, 0x04U, 0x05U, 0x06U, 0x07U,
+ 0x08U, 0x09U, 0x0AU, 0x0BU, 0x0CU, 0x0DU, 0x0EU, 0x0FU,
+ };
+ static const uint32_t input[4] = {
+ UINT32_C(0x33221100),
+ UINT32_C(0x77665544),
+ UINT32_C(0xBBAA9988),
+ UINT32_C(0xFFEEDDCC),
+ };
+ static const uint32_t expected[4] = {
+ UINT32_C(0xD8E0C469),
+ UINT32_C(0x30047B6A),
+ UINT32_C(0x80B7CDD8),
+ UINT32_C(0x5AC5B470),
+ };
+ if ((rs_crypto_zeroize_wait(1U) != RS_ETIMEOUT) ||
+ (rs_crypto_zeroize_wait(RS_TIMEOUT_DEFAULT) != RS_OK) ||
+ (rs_crypto_init(2050U) != RS_ETIMEOUT) ||
+ (rs_crypto_selftest(RS_TIMEOUT_DEFAULT) != RS_OK) ||
+ (rs_crypto_aes_set_key(key, sizeof(key), RS_TIMEOUT_DEFAULT) != RS_OK)) {
+ return false;
+ }
+ for (uint32_t index = 0U; index < 16U; ++index) {
+ rs_p6_source[index] = input[index % 4U];
+ }
+ rs_p6_destination[0] = UINT32_C(0x5A5AA5A5);
+ rs_p6_destination[17] = UINT32_C(0xA5A55A5A);
+ if (rs_crypto_aes_crypt_dma(RS_CRYPTO_AES_ECB, false, NULL, rs_p6_source, &rs_p6_destination[1],
+ 64U, RS_TIMEOUT_DEFAULT) != RS_OK) {
+ return false;
+ }
+ for (uint32_t index = 0U; index < 16U; ++index) {
+ if (rs_p6_destination[index + 1U] != expected[index % 4U]) {
+ return false;
+ }
+ }
+ /* A small public-operation integration vector exercises the full 2048-bit
+ * storage/control path. Full private vectors remain in the shared suite. */
+ for (uint32_t index = 0U; index < RS_CRYPTO_RSA_WORDS; ++index) {
+ modulus[index] = UINT32_MAX;
+ }
+ base[0] = 2U;
+ exponent[0] = 3U;
+ if ((rs_crypto_rsa_prepare(modulus, RS_TIMEOUT_DEFAULT) != RS_OK) ||
+ (rs_crypto_rsa_modexp(base, exponent, 2U, false, rsa_result, RS_TIMEOUT_DEFAULT) !=
+ RS_OK)) {
+ return false;
+ }
+ for (uint32_t index = 0U; index < RS_CRYPTO_RSA_WORDS; ++index) {
+ if (rsa_result[index] != ((index == 0U) ? 8U : 0U)) {
+ return false;
+ }
+ }
+ if ((rs_p6_destination[0] != UINT32_C(0x5A5AA5A5)) ||
+ (rs_p6_destination[17] != UINT32_C(0xA5A55A5A)) || !rs_p6_irq_arm(RS_SOC_EXT_IRQ_CRYPTO)) {
+ return false;
+ }
+ RS_CRYPTO_REG(RS_CRYPTO_REG_IRQ_STATE) = RS_CRYPTO_IRQ_ALL;
+ RS_CRYPTO_REG(RS_CRYPTO_REG_IRQ_ENABLE) = RS_CRYPTO_IRQ_ZEROIZED;
+ if ((rs_crypto_zeroize_wait(0U) != RS_ETIMEOUT) ||
+ (rs_crypto_zeroize_wait(RS_TIMEOUT_DEFAULT) != RS_OK) || !rs_p6_irq_received()) {
+ return false;
+ }
+ RS_CRYPTO_REG(RS_CRYPTO_REG_IRQ_ENABLE) = 0U;
+ RS_CRYPTO_REG(RS_CRYPTO_REG_IRQ_STATE) = RS_CRYPTO_IRQ_ALL;
+ printf("R2_P6_CRYPTO init=1 aes_sha=1 dma45=1 rsa_public=1 zeroize=1\n");
+ return true;
+}
+
+static bool rs_p6_stop_session(rs_dma_session_t *owner) {
+ rs_dma_status_t status;
+ uint32_t timeout = UINT32_C(100000);
+ if ((rs_dma_get_status(owner->channel, &status) != RS_OK) ||
+ (status.busy && (rs_dma_session_abort(owner) != RS_OK))) {
+ return false;
+ }
+ do {
+ if ((rs_dma_get_status(owner->channel, &status) != RS_OK) || (timeout == 0U)) {
+ return false;
+ }
+ --timeout;
+ } while (status.busy);
+ return rs_dma_session_release(owner) == RS_OK;
+}
+
+static uint32_t rs_p6_audio_sample(bool wide, uint32_t index) {
+ const uint32_t word = rs_p6_audio_rx[1U + (wide ? index : index / 2U)];
+ return wide ? word : ((word >> ((index % 2U) * 16U)) & UINT32_C(0xFFFF));
+}
+
+static bool rs_p6_audio_words(bool wide, uint32_t received_words) {
+ const uint32_t samples = wide ? received_words : received_words * 2U;
+ const uint32_t first = wide ? UINT32_C(0x120020) : UINT32_C(0x2020);
+ uint32_t offset = 0U;
+ /* Ignore only the bounded PHY startup preamble, never a missing payload.
+ * Exactly 256 stereo frames must then match consecutive independent samples. */
+ while ((offset < 40U) && (rs_p6_audio_sample(wide, offset) != first)) {
+ ++offset;
+ }
+ if ((offset >= 40U) || ((offset + 512U) > samples)) {
+ return false;
+ }
+ for (uint32_t sample = 0U; sample < 512U; ++sample) {
+ if (rs_p6_audio_sample(wide, offset + sample) != (first + sample)) {
+ printf("R2_P6_AUDIO mismatch wide=%u sample=%lu got=%lx expected=%lx\n", wide ? 1U : 0U,
+ (unsigned long)sample, (unsigned long)rs_p6_audio_sample(wide, offset + sample),
+ (unsigned long)(first + sample));
+ return false;
+ }
+ }
+ return (rs_p6_audio_rx[0] == UINT32_C(0x5A5AA5A5)) &&
+ (rs_p6_audio_rx[received_words + 1U] == UINT32_C(0xA5A55A5A));
+}
+
+static bool rs_p6_audio_ws(bool wide) {
+ printf("R2_P6_STAGE audio_bits=%u cycles=%lu\n", wide ? 24U : 16U,
+ (unsigned long)__RV_CSR_READ(CSR_MCYCLE));
+ const uint32_t rx_words = wide ? 548U : 280U;
+ const rs_i2s_config_t audio = {
+ .stream_tx = true,
+ .stream_rx = true,
+ .preset = wide ? RS_I2S_PRESET_24B_48K : RS_I2S_PRESET_16B_48K,
+ .upbound = 96U,
+ .lowbound = 32U,
+ };
+ const rs_ws2812_config_t ws = {
+ .source_clock_hz = RS_PCLK_CLOCK_HZ,
+ .bit_period_ns = 1250U,
+ .t0h_ns = 350U,
+ .t1h_ns = 700U,
+ .reset_ns = 300000U,
+ .fifo_watermark = 8U,
+ };
+ rs_dma_config_t tx = {
+ .kind = RS_DMA_KIND_MM_TO_STREAM,
+ .request = RS_DMA_REQUEST_I2S_TX,
+ .source = (uintptr_t)rs_p6_audio_tx,
+ .byte_count = sizeof(rs_p6_audio_tx),
+ .width = RS_DMA_WIDTH_32,
+ .source_increment = true,
+ .priority = 3U,
+ .burst_beats = 16U,
+ };
+ rs_dma_config_t rx = {
+ .kind = RS_DMA_KIND_STREAM_TO_MM,
+ .request = RS_DMA_REQUEST_I2S_RX,
+ .destination = (uintptr_t)&rs_p6_audio_rx[1],
+ .byte_count = rx_words * 4U,
+ .width = RS_DMA_WIDTH_32,
+ .destination_increment = true,
+ .priority = 3U,
+ .burst_beats = 16U,
+ };
+ rs_dma_config_t background = {
+ .kind = RS_DMA_KIND_MM_TO_MM,
+ .request = RS_DMA_REQUEST_SOFTWARE,
+ .source = (uintptr_t)rs_p6_source,
+ .destination = (uintptr_t)&rs_p6_destination[1],
+ .byte_count = 256U,
+ .width = RS_DMA_WIDTH_32,
+ .source_increment = true,
+ .destination_increment = true,
+ .burst_beats = 16U,
+ };
+ rs_i2s_status_t audio_status;
+ rs_dma_status_t rx_status, bg_status;
+ rs_ws2812_dma_status_t ws_status;
+ uint64_t now, deadline, start;
+ uint32_t cycles, instructions;
+ uint32_t frame = 0U, background_jobs = 0U;
+ for (uint32_t pin = 12U; pin <= 17U; ++pin) {
+ if (!rs_p6_route(pin, false)) {
+ return false;
+ }
+ }
+ if (!rs_p6_route(26U, false) || (rs_i2s_disable() != RS_OK) ||
+ (rs_i2s_flush(true, true, RS_TIMEOUT_DEFAULT) != RS_OK) ||
+ (rs_i2s_configure(&audio) != RS_OK) || (rs_ws2812_init(&ws) != RS_OK)) {
+ return false;
+ }
+ printf("R2_P6_STAGE audio_host_ready=1 cycles=%lu\n", (unsigned long)__RV_CSR_READ(CSR_MCYCLE));
+ for (uint32_t index = 0U; index < 768U; ++index) {
+ rs_p6_audio_tx[index] =
+ wide ? UINT32_C(0x120000) + index
+ : ((UINT32_C(0x2001) + 2U * index) << 16U) | (UINT32_C(0x2000) + 2U * index);
+ }
+ for (uint32_t index = 0U; index < 65U; ++index) {
+ rs_p6_pixels[index] = index * UINT32_C(0x010203);
+ }
+ for (uint32_t index = 0U; index < 256U; ++index) {
+ rs_p6_source[index] = UINT32_C(0x5A123456) ^ index;
+ rs_p6_destination[index + 1U] = 0U;
+ }
+ rs_p6_destination[0] = UINT32_C(0x5A5AA5A5);
+ rs_p6_destination[257] = UINT32_C(0xA5A55A5A);
+ rs_p6_audio_rx[0] = UINT32_C(0x5A5AA5A5);
+ rs_p6_audio_rx[rx_words + 1U] = UINT32_C(0xA5A55A5A);
+ for (uint32_t index = 0U; index < 4U; ++index) {
+ rs_p6_descriptors[index] = (rs_dma_tcd_t){
+ .source = (uint32_t)(uintptr_t)&rs_p6_source[index * 64U],
+ .destination = (uint32_t)(uintptr_t)&rs_p6_destination[1U + index * 64U],
+ .byte_count = 256U,
+ .next_ptr = index == 3U ? 0U : (uint32_t)(uintptr_t)&rs_p6_descriptors[index + 1U],
+ .control = RS_DMA_TCD_VALID | RS_DMA_TCD_SRC_INC | RS_DMA_TCD_DST_INC |
+ (UINT32_C(16) << RS_DMA_TCD_BURST_SHIFT),
+ };
+ }
+ if (!rs_p6_irq_arm(RS_SOC_EXT_IRQ_I2S) ||
+ (rs_i2s_interrupt_enable(RS_I2S_INTERRUPT_TX_LOW) != RS_OK) ||
+ (rs_i2s_interrupt_test(RS_I2S_INTERRUPT_TX_LOW) != RS_OK) || !rs_p6_irq_received() ||
+ (rs_i2s_interrupt_enable(0U) != RS_OK) ||
+ (rs_i2s_interrupt_clear(RS_I2S_INTERRUPT_ALL) != RS_OK) ||
+ (rs_dma_session_acquire(&rs_p6_audio_tx_owner, RS_DMA_CHANNEL_I2S_TX) != RS_OK) ||
+ (rs_dma_session_acquire(&rs_p6_audio_rx_owner, RS_DMA_CHANNEL_I2S_RX) != RS_OK) ||
+ (rs_dma_session_acquire(&rs_p6_background_owner, RS_DMA_CHANNEL_DVP) != RS_OK) ||
+ (rs_dma_session_configure(&rs_p6_audio_tx_owner, &tx) != RS_OK) ||
+ (rs_dma_session_configure(&rs_p6_audio_rx_owner, &rx) != RS_OK) ||
+ (rs_dma_session_start(&rs_p6_audio_tx_owner) != RS_OK) ||
+ (rs_dma_session_start(&rs_p6_audio_rx_owner) != RS_OK) ||
+ (rs_clint_get_time(&start) != RS_OK)) {
+ return false;
+ }
+ deadline = start + UINT64_C(50000);
+ printf("R2_P6_STAGE audio_dma_started=1 cycles=%lu\n",
+ (unsigned long)__RV_CSR_READ(CSR_MCYCLE));
+ do {
+ if ((rs_i2s_get_status(&audio_status) != RS_OK) || (rs_clint_get_time(&now) != RS_OK) ||
+ (now >= deadline)) {
+ return false;
+ }
+ } while (audio_status.tx_level < 96U);
+ printf("R2_P6_STAGE audio_prefilled=1 cycles=%lu\n", (unsigned long)__RV_CSR_READ(CSR_MCYCLE));
+ cycles = (uint32_t)__RV_CSR_READ(CSR_MCYCLE);
+ instructions = (uint32_t)__RV_CSR_READ(CSR_MINSTRET);
+ if (!rs_p6_irq_arm(RS_SOC_EXT_IRQ_WS2812) ||
+ (rs_ws2812_irq_enable(RS_WS2812_INTR_FIFO_LOW | RS_WS2812_INTR_DONE) != RS_OK) ||
+ (rs_i2s_enable(true, true) != RS_OK) ||
+ (rs_ws2812_dma_begin(rs_p6_pixels, 33U, now, deadline) != RS_OK)) {
+ return false;
+ }
+ do {
+ if ((rs_clint_get_time(&now) != RS_OK) || (now >= deadline) ||
+ (rs_ws2812_dma_service(now) != RS_OK) || (rs_ws2812_dma_status(&ws_status) != RS_OK) ||
+ (rs_dma_get_status(RS_DMA_CHANNEL_I2S_RX, &rx_status) != RS_OK) ||
+ (rs_dma_get_status(RS_DMA_CHANNEL_DVP, &bg_status) != RS_OK) || rx_status.error ||
+ bg_status.error) {
+ return false;
+ }
+ if (!bg_status.busy && (background_jobs < 64U)) {
+ if (rs_dma_session_configure(&rs_p6_background_owner, &background) != RS_OK) {
+ return false;
+ }
+ /* This application exclusively leases channel 2; the ordinary TCD
+ * helper is blocking, so publish the same finite hardware chain. */
+ RS_DMA_CH_REG(RS_DMA_CHANNEL_DVP, RS_DMA_CH_REG_TCD_HEAD) =
+ wide ? (uint32_t)(uintptr_t)rs_p6_descriptors : 0U;
+ RS_DMA_CH_REG(RS_DMA_CHANNEL_DVP, RS_DMA_CH_REG_TCD_COUNT) = wide ? 4U : 0U;
+ if (rs_dma_session_start(&rs_p6_background_owner) != RS_OK) {
+ return false;
+ }
+ ++background_jobs;
+ }
+ if (!ws_status.active && (frame == 0U)) {
+ if ((ws_status.result != RS_OK) ||
+ (rs_ws2812_dma_begin(rs_p6_pixels, 65U, now, deadline) != RS_OK)) {
+ return false;
+ }
+ frame = 1U;
+ ws_status.active = true;
+ }
+ } while (rx_status.busy || ws_status.active);
+ cycles = (uint32_t)__RV_CSR_READ(CSR_MCYCLE) - cycles;
+ instructions = (uint32_t)__RV_CSR_READ(CSR_MINSTRET) - instructions;
+ if ((rx_status.bytes_done != rx_words * 4U) || !rx_status.done || (ws_status.result != RS_OK) ||
+ (rs_p6_irq_count != 1U) || (rs_i2s_get_status(&audio_status) != RS_OK) ||
+ ((audio_status.interrupt_state &
+ (RS_I2S_INTERRUPT_TX_UNDERRUN | RS_I2S_INTERRUPT_RX_OVERRUN)) != 0U) ||
+ !rs_p6_stop_session(&rs_p6_audio_tx_owner) || !rs_p6_stop_session(&rs_p6_audio_rx_owner) ||
+ (rs_i2s_disable() != RS_OK) ||
+ (rs_dma_wait(RS_DMA_CHANNEL_DVP, RS_TIMEOUT_DEFAULT) != RS_OK) ||
+ (rs_dma_session_release(&rs_p6_background_owner) != RS_OK) ||
+ (rs_i2s_flush(true, true, RS_TIMEOUT_DEFAULT) != RS_OK) ||
+ (rs_ws2812_irq_enable(0U) != RS_OK) || !rs_p6_audio_words(wide, rx_words)) {
+ return false;
+ }
+ for (uint32_t index = 0U; index < (wide ? 256U : 64U); ++index) {
+ if (rs_p6_destination[index + 1U] != rs_p6_source[index]) {
+ return false;
+ }
+ }
+ if ((rs_p6_destination[0] != UINT32_C(0x5A5AA5A5)) ||
+ (rs_p6_destination[257] != UINT32_C(0xA5A55A5A))) {
+ return false;
+ }
+ if (!wide) {
+ for (uint32_t index = 65U; index <= 256U; ++index) {
+ if (rs_p6_destination[index] != 0U) {
+ return false;
+ }
+ }
+ }
+ printf("R2_P6_AUDIO bits=%u frames=256 ws_frames=2 background_jobs=%lu tcd=%u elapsed_us=%lu "
+ "cycles=%lu instructions=%lu\n",
+ wide ? 24U : 16U, (unsigned long)background_jobs, wide ? 1U : 0U,
+ (unsigned long)(now - start), (unsigned long)cycles, (unsigned long)instructions);
+ return background_jobs != 0U;
+}
+
+static bool rs_p6_audio_ws_recovery(void) {
+ const rs_ws2812_config_t config = {
+ .source_clock_hz = RS_PCLK_CLOCK_HZ,
+ .bit_period_ns = 1250U,
+ .t0h_ns = 350U,
+ .t1h_ns = 700U,
+ .reset_ns = 300000U,
+ .fifo_watermark = 8U,
+ };
+ uint64_t now, began;
+ rs_i2s_status_t audio;
+ rs_ws2812_status_t ws;
+ for (uint32_t phase = 0U; phase < 2U; ++phase) {
+ if ((rs_gpio_write(27U, true) != RS_OK) || (rs_clint_get_time(&began) != RS_OK)) {
+ return false;
+ }
+ do {
+ if (rs_clint_get_time(&now) != RS_OK) {
+ return false;
+ }
+ } while ((now - began) < 10U);
+ if ((rs_i2s_flush(true, true, 16U) != RS_ETIMEOUT) ||
+ (rs_i2s_get_status(&audio) != RS_OK)) {
+ return false;
+ }
+ /* A timed-out command remains owned by the controller. Do not issue
+ * another COMMAND while BUSY: that write is correctly rejected. */
+ do {
+ if ((rs_i2s_get_status(&audio) != RS_OK) || (rs_clint_get_time(&now) != RS_OK) ||
+ ((now - began) > 1000U)) {
+ return false;
+ }
+ } while ((audio.status & (RS_I2S_STATUS_TX_FLUSH_BUSY | RS_I2S_STATUS_RX_FLUSH_BUSY)) !=
+ 0U);
+ if (rs_gpio_write(27U, false) != RS_OK) {
+ return false;
+ }
+ }
+ if ((rs_ws2812_init(&config) != RS_OK) || (rs_ws2812_push(0U, RS_TIMEOUT_DEFAULT) != RS_OK) ||
+ (rs_ws2812_start(2U) != RS_OK) || (rs_ws2812_wait(RS_TIMEOUT_DEFAULT) != RS_EIO) ||
+ (rs_ws2812_get_status(&ws) != RS_OK) ||
+ ((ws.error_status & RS_WS2812_ERROR_UNDERFLOW) == 0U) ||
+ (rs_clint_get_time(&began) != RS_OK)) {
+ return false;
+ }
+ do {
+ if ((rs_ws2812_get_status(&ws) != RS_OK) || (rs_clint_get_time(&now) != RS_OK) ||
+ ((now - began) > 1000U)) {
+ return false;
+ }
+ } while (ws.busy);
+ if ((rs_ws2812_init(&config) != RS_OK) || (rs_ws2812_push(0U, RS_TIMEOUT_DEFAULT) != RS_OK) ||
+ (rs_ws2812_start(65U) != RS_OK) || (rs_ws2812_abort(RS_TIMEOUT_DEFAULT) != RS_OK)) {
+ return false;
+ }
+ printf("R2_P6_RECOVERY audio_stops=2 ws_underflow=1 ws_abort=1\n");
+ return true;
+}
+
+static bool rs_p6_sdio_step(rs_status_t result, uint32_t step) {
+ rs_sdio_status_t status;
+ if (result == RS_OK) {
+ return true;
+ }
+ if (rs_sdio_status_get(RS_SDIO_INSTANCE_0, &status) == RS_OK) {
+ printf("R2_P6_SDIO_ERROR step=%lu result=%ld command=%lx data=%lx dma=%lx errors=%lx\n",
+ (unsigned long)step, (long)result, (unsigned long)status.command_status,
+ (unsigned long)status.data_status, (unsigned long)status.dma_status,
+ (unsigned long)status.error_status);
+ }
+ return false;
+}
+
+static bool rs_p6_sdio(void) {
+ static _Alignas(16) rs_sdio_descriptor_t descriptor;
+ rs_sd_memory_info_t card;
+ rs_sdio_clock_t clock;
+ rs_sdio_response_t response;
+ rs_dma_config_t background = {
+ .kind = RS_DMA_KIND_MM_TO_MM,
+ .request = RS_DMA_REQUEST_SOFTWARE,
+ .source = (uintptr_t)rs_p6_audio_tx,
+ .destination = (uintptr_t)&rs_p6_audio_rx[1],
+ .byte_count = 1024U,
+ .width = RS_DMA_WIDTH_32,
+ .source_increment = true,
+ .destination_increment = true,
+ .burst_beats = 16U,
+ };
+ rs_sdio_command_t command = {
+ .index = 17U,
+ .argument = 1U,
+ .response = RS_SDIO_RESPONSE_R1,
+ .crc_check = true,
+ .index_check = true,
+ };
+ const rs_sdio_data_config_t pio = {
+ .block_size = 512U,
+ .block_count = 1U,
+ .direction = RS_SDIO_DATA_FROM_CARD,
+ .dma = false,
+ .block_mode = true,
+ };
+ const rs_sdio_data_config_t dma_write = {
+ .block_size = 512U,
+ .block_count = 2U,
+ .direction = RS_SDIO_DATA_TO_CARD,
+ .dma = true,
+ .block_mode = true,
+ };
+ uint32_t id, version, capability;
+ rs_status_t initialized;
+ printf("R2_P6_STAGE sdio_begin=1\n");
+ for (uint32_t pin = 0U; pin < 6U; ++pin) {
+ if (!rs_p6_route(pin, false)) {
+ return false;
+ }
+ }
+ if ((rs_sdio_probe(RS_SDIO_INSTANCE_1, &id, &version, &capability) != RS_ENOTSUP) ||
+ (rs_sdio_controller_selftest(RS_SDIO_INSTANCE_0) != RS_OK)) {
+ return false;
+ }
+ initialized =
+ rs_sd_memory_initialize(RS_SDIO_INSTANCE_0, RS_CPU_CLOCK_HZ, &card, RS_TIMEOUT_DEFAULT);
+ printf("R2_P6_STAGE sdio_init=%ld clock=%lu width=%u rca=%u\n", (long)initialized,
+ (unsigned long)card.actual_clock_hz, (unsigned)card.bus_width, (unsigned)card.rca);
+ if ((initialized != RS_OK) || (card.actual_clock_hz != UINT32_C(12000000)) ||
+ !card.high_capacity || (card.rca != 1U) || (card.bus_width != RS_SDIO_BUS_WIDTH_4)) {
+ return false;
+ }
+ for (uint32_t index = 0U; index < 256U; ++index) {
+ rs_p6_source[index] = UINT32_C(0x13579BDF) ^ index;
+ rs_p6_destination[index + 1U] = 0U;
+ rs_p6_audio_tx[index] = UINT32_C(0x55AA0000) + index;
+ rs_p6_audio_rx[index + 1U] = ~rs_p6_audio_tx[index];
+ }
+ rs_p6_destination[0] = UINT32_C(0x5A5AA5A5);
+ rs_p6_destination[257] = UINT32_C(0xA5A55A5A);
+ command.index = 25U;
+ if (!rs_p6_sdio_step(rs_dma_session_acquire(&rs_p6_background_owner, RS_DMA_CHANNEL_DVP), 1U) ||
+ !rs_p6_sdio_step(rs_dma_session_configure(&rs_p6_background_owner, &background), 2U) ||
+ !rs_p6_sdio_step(
+ rs_sdio_command_execute(RS_SDIO_INSTANCE_0, &command, &response, RS_TIMEOUT_DEFAULT),
+ 3U) ||
+ !rs_p6_sdio_step(rs_sdio_descriptor_prepare(&descriptor, (uintptr_t)rs_p6_source, 1024U,
+ (uintptr_t)0U, true, false),
+ 4U) ||
+ !rs_p6_sdio_step(rs_sdio_dma_setup(RS_SDIO_INSTANCE_0, &descriptor, 1U, 1024U), 5U) ||
+ !rs_p6_sdio_step(rs_sdio_data_configure(RS_SDIO_INSTANCE_0, &dma_write), 6U) ||
+ !rs_p6_sdio_step(rs_sdio_data_start(RS_SDIO_INSTANCE_0), 7U) ||
+ !rs_p6_sdio_step(rs_dma_session_start(&rs_p6_background_owner), 8U) ||
+ !rs_p6_sdio_step(rs_sdio_data_wait(RS_SDIO_INSTANCE_0, RS_TIMEOUT_DEFAULT), 9U) ||
+ !rs_p6_sdio_step(rs_sdio_dma_wait(RS_SDIO_INSTANCE_0, RS_TIMEOUT_DEFAULT), 10U)) {
+ return false;
+ }
+ printf("R2_P6_STAGE sdio_write=1\n");
+ command.index = 12U;
+ command.argument = 0U;
+ command.response = RS_SDIO_RESPONSE_R1B;
+ if (!rs_p6_sdio_step(
+ rs_sdio_command_execute(RS_SDIO_INSTANCE_0, &command, &response, RS_TIMEOUT_DEFAULT),
+ 11U) ||
+ !rs_p6_sdio_step(rs_sd_memory_read_blocks(RS_SDIO_INSTANCE_0, &card, 1U, 2U,
+ &rs_p6_destination[1], RS_TIMEOUT_DEFAULT),
+ 12U) ||
+ !rs_p6_sdio_step(rs_dma_wait(RS_DMA_CHANNEL_DVP, RS_TIMEOUT_DEFAULT), 13U) ||
+ !rs_p6_sdio_step(rs_dma_session_release(&rs_p6_background_owner), 14U)) {
+ return false;
+ }
+ printf("R2_P6_STAGE sdio_read=1\n");
+ for (uint32_t index = 0U; index < 256U; ++index) {
+ if ((rs_p6_destination[index + 1U] != rs_p6_source[index]) ||
+ (rs_p6_audio_rx[index + 1U] != rs_p6_audio_tx[index])) {
+ return false;
+ }
+ }
+ if ((rs_p6_destination[0] != UINT32_C(0x5A5AA5A5)) ||
+ (rs_p6_destination[257] != UINT32_C(0xA5A55A5A)) ||
+ (rs_sdio_clock_set(RS_SDIO_INSTANCE_0, RS_CPU_CLOCK_HZ, 400000U, &clock) != RS_OK) ||
+ (clock.actual_hz != 400000U)) {
+ return false;
+ }
+ /* Select one-bit card and host consistently before the PIO read. */
+ command.index = 55U;
+ command.argument = (uint32_t)card.rca << 16U;
+ command.response = RS_SDIO_RESPONSE_R1;
+ if (rs_sdio_command_execute(RS_SDIO_INSTANCE_0, &command, &response, RS_TIMEOUT_DEFAULT) !=
+ RS_OK) {
+ return false;
+ }
+ command.index = 6U;
+ command.argument = 0U;
+ if ((rs_sdio_command_execute(RS_SDIO_INSTANCE_0, &command, &response, RS_TIMEOUT_DEFAULT) !=
+ RS_OK) ||
+ (rs_sdio_bus_width_set(RS_SDIO_INSTANCE_0, RS_SDIO_BUS_WIDTH_1) != RS_OK)) {
+ return false;
+ }
+ command.index = 17U;
+ command.argument = 1U;
+ if (!rs_p6_sdio_step(
+ rs_sdio_command_execute(RS_SDIO_INSTANCE_0, &command, &response, RS_TIMEOUT_DEFAULT),
+ 15U) ||
+ !rs_p6_sdio_step(rs_sdio_data_configure(RS_SDIO_INSTANCE_0, &pio), 16U) ||
+ !rs_p6_sdio_step(rs_sdio_data_start(RS_SDIO_INSTANCE_0), 17U) ||
+ !rs_p6_sdio_step(
+ rs_sdio_pio_read(RS_SDIO_INSTANCE_0, &rs_p6_destination[1], 512U, RS_TIMEOUT_DEFAULT),
+ 18U) ||
+ !rs_p6_sdio_step(rs_sdio_data_wait(RS_SDIO_INSTANCE_0, RS_TIMEOUT_DEFAULT), 19U)) {
+ return false;
+ }
+ printf("R2_P6_STAGE sdio_pio=1\n");
+ for (uint32_t index = 0U; index < 128U; ++index) {
+ if (rs_p6_destination[index + 1U] != rs_p6_source[index]) {
+ return false;
+ }
+ }
+ if ((rs_sdio_irq_clear(RS_SDIO_INSTANCE_0, RS_SDIO_IRQ_ALL) != RS_OK) ||
+ !rs_p6_irq_arm(RS_SOC_EXT_IRQ_SDIO) ||
+ (rs_sdio_irq_enable(RS_SDIO_INSTANCE_0, RS_SDIO_IRQ_ALL) != RS_OK) ||
+ (rs_sdio_irq_test(RS_SDIO_INSTANCE_0, 1U) != RS_OK) || !rs_p6_irq_received() ||
+ (rs_sdio_irq_enable(RS_SDIO_INSTANCE_0, 0U) != RS_OK) ||
+ (rs_sdio_irq_clear(RS_SDIO_INSTANCE_0, RS_SDIO_IRQ_ALL) != RS_OK) ||
+ (rs_sdio_timeouts_set(RS_SDIO_INSTANCE_0, 24000U, 24000U, 24000U) != RS_OK)) {
+ return false;
+ }
+ /* Verification card injects faults on these documented CMD13 arguments. */
+ command.index = 13U;
+ command.argument = UINT32_C(0xBADC0DE0);
+ if (rs_sdio_command_execute(RS_SDIO_INSTANCE_0, &command, &response, RS_TIMEOUT_DEFAULT) !=
+ RS_EFORMAT) {
+ return false;
+ }
+ command.argument = UINT32_C(0xBADC0DE1);
+ if (rs_sdio_command_execute(RS_SDIO_INSTANCE_0, &command, &response, RS_TIMEOUT_DEFAULT) !=
+ RS_ETIMEOUT) {
+ return false;
+ }
+ command.argument = (uint32_t)card.rca << 16U;
+ if ((rs_sdio_error_clear(RS_SDIO_INSTANCE_0, RS_SDIO_ERROR_ALL) != RS_OK) ||
+ (rs_sdio_command_execute(RS_SDIO_INSTANCE_0, &command, &response, RS_TIMEOUT_DEFAULT) !=
+ RS_OK)) {
+ return false;
+ }
+ printf("R2_P6_SDIO dma_bytes=2048 pio_bytes=512 width1=1 width4=1 irq=1 errors=2\n");
+ return true;
+}
+
+uint8_t rs_tiny_p6_acceptance(void) {
+ if (!rs_p6_uart()) {
+ return 27U;
+ }
+ if (!rs_p6_dma_capabilities()) {
+ return 19U;
+ }
+ if (!rs_p6_rng()) {
+ return 20U;
+ }
+ if (!rs_p6_crc()) {
+ return 21U;
+ }
+ if (!rs_p6_crypto()) {
+ return 22U;
+ }
+ if (!rs_p6_audio_ws(false)) {
+ return 23U;
+ }
+ if (!rs_p6_audio_ws(true)) {
+ return 24U;
+ }
+ if (!rs_p6_audio_ws_recovery()) {
+ return 26U;
+ }
+ if (!rs_p6_sdio()) {
+ return 25U;
+ }
+ return 0U;
+}
diff --git a/app/apps/ci_smoke/tiny_r2_p7.c b/app/apps/ci_smoke/tiny_r2_p7.c
new file mode 100644
index 00000000..edc785c2
--- /dev/null
+++ b/app/apps/ci_smoke/tiny_r2_p7.c
@@ -0,0 +1,171 @@
+#include
+#include
+
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+/* Short P7 firmware: execute in real main SRAM and use real SYSCTRL/APB
+ * crossings. The full peripheral firmware remains a separate regression. */
+static volatile uint32_t rs_p7_words[128];
+static _Alignas(64) uint32_t rs_p7_source[1024];
+static _Alignas(64) uint32_t rs_p7_destination[1024];
+static volatile uint32_t rs_p7_retained __attribute__((section(".noinit.rs_p7")));
+
+static bool rs_p7_peripheral_rate(uint32_t pclk_hz) {
+ const rs_timer_config_t config = {.mode = RS_TIMER_MODE_FREE_RUNNING,
+ .direction = RS_TIMER_DIRECTION_UP};
+ const volatile uint32_t *pwm_rate =
+ (const volatile uint32_t *)(RS_SOC_APB4_PWM_BASE + PWM_CLOCK_HZ_OFFSET);
+ uint32_t first, last;
+ uint64_t start, end;
+ uint64_t expected;
+ rs_timeout_t budget = RS_TIMEOUT_DEFAULT;
+ if (*pwm_rate != pclk_hz || rs_timer_configure(RS_TIMER_0, &config) != RS_OK ||
+ rs_timer_start(RS_TIMER_0) != RS_OK || rs_clint_get_time(&start) != RS_OK ||
+ rs_timer_get_value(RS_TIMER_0, &first) != RS_OK) {
+ return false;
+ }
+ do {
+ if (rs_clint_get_time(&end) != RS_OK || budget-- == 0U) {
+ return false;
+ }
+ } while (end - start < 200U);
+ if (rs_timer_get_value(RS_TIMER_0, &last) != RS_OK || rs_timer_stop(RS_TIMER_0) != RS_OK) {
+ return false;
+ }
+ expected = (end - start) * (pclk_hz / 1000000U);
+ /* Independent CLINT reference; allow the bounded APB/software read skew.
+ * A wrong /2 or /4 domain cannot fit this +/-25% interval. */
+ return (uint64_t)(last - first) > expected * 3U / 4U &&
+ (uint64_t)(last - first) < expected * 5U / 4U;
+}
+
+static bool rs_p7_sleep(void) {
+ rs_dma_session_t session;
+ rs_dma_config_t config = {.kind = RS_DMA_KIND_MM_TO_MM,
+ .request = RS_DMA_REQUEST_SOFTWARE,
+ .source = (uintptr_t)rs_p7_source,
+ .destination = (uintptr_t)rs_p7_destination,
+ .byte_count = sizeof(rs_p7_source),
+ .width = RS_DMA_WIDTH_32,
+ .source_increment = true,
+ .destination_increment = true,
+ .burst_beats = 16U};
+ uint64_t now;
+ uint32_t index;
+ __disable_irq();
+ for (index = 0U; index < 1024U; index++) {
+ rs_p7_source[index] = index ^ UINT32_C(0x13572468);
+ rs_p7_destination[index] = 0U;
+ }
+ if (rs_tiny_rcu_apply_gate(1U, RS_TIMEOUT_DEFAULT) != RS_OK ||
+ rs_dma_session_acquire(&session, RS_DMA_CHANNEL_BULK) != RS_OK ||
+ rs_dma_session_configure(&session, &config) != RS_OK || rs_clint_get_time(&now) != RS_OK ||
+ rs_clint_set_compare(0U, now + 3000U) != RS_OK || rs_dma_session_start(&session) != RS_OK) {
+ return false;
+ }
+ __RV_CSR_SET(CSR_MIE, MIE_MTIE);
+ __WFI();
+ __RV_CSR_CLEAR(CSR_MIE, MIE_MTIE);
+ if (rs_clint_set_compare(0U, UINT64_MAX) != RS_OK ||
+ rs_dma_wait(RS_DMA_CHANNEL_BULK, RS_TIMEOUT_DEFAULT) != RS_OK ||
+ rs_dma_session_release(&session) != RS_OK ||
+ rs_tiny_rcu_apply_gate(0U, RS_TIMEOUT_DEFAULT) != RS_OK) {
+ return false;
+ }
+ for (index = 0U; index < 1024U; index++) {
+ if (rs_p7_destination[index] != rs_p7_source[index]) {
+ return false;
+ }
+ }
+ return true;
+}
+
+static bool rs_p7_profile(rs_tiny_rcu_profile_t profile, uint32_t sys_hz, uint32_t mem_hz,
+ uint32_t pclk_hz) {
+ rs_tiny_rcu_snapshot_t snapshot;
+ uint32_t index;
+ uint64_t before, after;
+
+ if (rs_clint_get_time(&before) != RS_OK ||
+ rs_tiny_rcu_request_profile(profile, RS_TIMEOUT_DEFAULT) != RS_OK ||
+ rs_clint_get_time(&after) != RS_OK || after <= before) {
+ return false;
+ }
+ if (rs_tiny_rcu_get_snapshot(&snapshot) != RS_OK || snapshot.current_profile != profile ||
+ snapshot.sys_hz != sys_hz || snapshot.mem_hz != mem_hz || snapshot.pclk_hz != pclk_hz) {
+ return false;
+ }
+ for (index = 0U; index < 128U; index++) {
+ if (rs_p7_words[index] != (UINT32_C(0xABCDEF00) ^ index)) {
+ return false;
+ }
+ }
+ return rs_p7_peripheral_rate(pclk_hz);
+}
+
+int main(void) {
+ rs_tiny_rcu_snapshot_t snapshot;
+ uint32_t index;
+ uint8_t code = 0U;
+
+ if (rs_tiny_rcu_get_snapshot(&snapshot) == RS_OK && (snapshot.reset_cause & 2U) != 0U) {
+ code = (rs_p7_retained == UINT32_C(0x7037CAFE) && snapshot.current_profile == 0U &&
+ snapshot.sys_hz == 24000000U && snapshot.gate_status == 0U)
+ ? 0U
+ : 11U;
+ rs_sysctrl_write_test_status(code == 0U, code);
+ for (;;) {
+ }
+ }
+
+ for (index = 0U; index < 128U; index++) {
+ rs_p7_words[index] = UINT32_C(0xABCDEF00) ^ index;
+ }
+ if (rs_tiny_rcu_get_snapshot(&snapshot) != RS_OK || snapshot.sys_hz != 24000000U ||
+ snapshot.target_capability != UINT32_C(0x3FFF)) {
+ code = 1U;
+ } else if ((snapshot.capability & UINT32_C(0xC)) == UINT32_C(0xC)) {
+ if (!rs_p7_profile(RS_TINY_RCU_PROFILE_PLL192, 192000000U, 96000000U, 48000000U)) {
+ code = 2U;
+ } else if (!rs_p7_profile(RS_TINY_RCU_PROFILE_PLL240, 240000000U, 120000000U, 60000000U)) {
+ code = 3U;
+ }
+ } else if (!rs_p7_profile(RS_TINY_RCU_PROFILE_XIN, snapshot.xin_hz, snapshot.xin_hz,
+ snapshot.xin_hz > 60000000U ? snapshot.xin_hz / 2U
+ : snapshot.xin_hz)) {
+ code = 4U;
+ } else {
+ /* No-PLL compatibility has no PLL profile bit. */
+ }
+ if (code == 0U && !rs_p7_profile(RS_TINY_RCU_PROFILE_SAFE24, 24000000U, 24000000U, 24000000U)) {
+ code = 5U;
+ }
+ if (code == 0U && rs_tiny_rcu_apply_gate(UINT32_C(1) << 4U, RS_TIMEOUT_DEFAULT) != RS_OK) {
+ code = 6U;
+ }
+ if (code == 0U && rs_tiny_rcu_apply_gate(0U, RS_TIMEOUT_DEFAULT) != RS_OK) {
+ code = 7U;
+ }
+ if (code == 0U && rs_tiny_rcu_apply_reset(UINT32_C(1) << 4U, RS_TIMEOUT_DEFAULT) != RS_OK) {
+ code = 8U;
+ }
+ if (code == 0U && !rs_p7_sleep()) {
+ code = 9U;
+ }
+ if (code == 0U) {
+ rs_p7_retained = UINT32_C(0x7037CAFE);
+ (void)rs_tiny_rcu_system_reset(RS_TIMEOUT_DEFAULT);
+ code = 10U;
+ }
+ rs_sysctrl_write_test_status(code == 0U, code);
+ for (;;) {
+ }
+}
diff --git a/configs/README.md b/configs/README.md
index 403283bc..f6f2c4d0 100644
--- a/configs/README.md
+++ b/configs/README.md
@@ -78,15 +78,22 @@ 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
-PLL backend tests do not enable RCU/SYS switching. `make SOC=TINY` selects this
+`ci/ics55-tiny.mk` boots with the PLL held off; R2-P7 adds explicit software
+requests for functional PLL192/240 transitions, subject to separate acceptance.
+`ci/ihp130-tiny-xin48.mk` and `ci/ihp130-tiny-xin96.mk` normalize their declared
+external input to REF24 at boot and permit no-PLL XIN transitions. See the
+[P7 runbook](../docs/ip/tiny-soc-r2-clock-reset.md) for the evidence boundary.
+`TINY_P7_ACCEPTANCE=YES APP=ci_smoke` selects the distinct RCU acceptance image
+and configuration digest; ordinary Tiny acceptance firmware remains the default.
+`make SOC=TINY` selects this
committed profile, and `make SOC=TINY PDK=IHP130` selects explicit compatibility.
Do not substitute Mini's `ci/ics55.mk` for Tiny. Acceptance evidence belongs in
the Tiny ledger, and physical qualification remains pending.
`ci/ihp130-tiny.mk` selects `SOC=TINY`, one RV32IMC Hazard3 (A disabled),
-128 KiB SRAM, AXI32/APB4, four DMA channels and the wired peripheral subset.
+128 KiB SRAM and AXI32/APB4. The P6 candidate adds eight-channel DMA and the
+frozen shared-peripheral integration; source presence is not phase acceptance.
The profile uses 24 MHz, `HAVE_HP=NO`, `MINI_MODE=NONE`, CSR-enabled RV32IM
firmware and `ld2_all_sram`. Both `bringup` and `ci_smoke` select Tiny acceptance
-firmware. Icarus and Verilator use the same pin-level Flash model. Other PDKs,
-wireless, external RAM and HP applications are rejected by the Tiny config gate.
+firmware. Icarus and Verilator use the same pin-level Flash model. PDKs other
+than IHP130/ICS55, external-RAM boot layouts and HP applications remain rejected.
diff --git a/configs/ci/ihp130-tiny-xin48.mk b/configs/ci/ihp130-tiny-xin48.mk
new file mode 100644
index 00000000..ce972c70
--- /dev/null
+++ b/configs/ci/ihp130-tiny-xin48.mk
@@ -0,0 +1,19 @@
+# Functional external-input profile; physical qualification is pending.
+SOC := TINY
+MINI_MODE := NONE
+PDK := IHP130
+SIMU := VERILATOR
+HAVE_PLL := NO
+HAVE_SRAM_IF := YES
+HAVE_SRAM_MACRO := YES
+SRAM_SIZE_KIB := 128
+HAVE_HP := NO
+HAVE_SVA := NO
+EXT_CLK_HZ := 48000000
+AUD_CLK_HZ := 24000000
+CLINT_TIMEBASE_HZ := 1000000
+ISA := RV32IM
+HAVE_CSR := YES
+APP := bringup
+LINK_TYPE := ld2_all_sram
+RTL_TOP := retrosoc_tiny_tb
\ No newline at end of file
diff --git a/configs/ci/ihp130-tiny-xin96.mk b/configs/ci/ihp130-tiny-xin96.mk
new file mode 100644
index 00000000..1e84419b
--- /dev/null
+++ b/configs/ci/ihp130-tiny-xin96.mk
@@ -0,0 +1,19 @@
+# Functional external-input profile; physical qualification is pending.
+SOC := TINY
+MINI_MODE := NONE
+PDK := IHP130
+SIMU := VERILATOR
+HAVE_PLL := NO
+HAVE_SRAM_IF := YES
+HAVE_SRAM_MACRO := YES
+SRAM_SIZE_KIB := 128
+HAVE_HP := NO
+HAVE_SVA := NO
+EXT_CLK_HZ := 96000000
+AUD_CLK_HZ := 24000000
+CLINT_TIMEBASE_HZ := 1000000
+ISA := RV32IM
+HAVE_CSR := YES
+APP := bringup
+LINK_TYPE := ld2_all_sram
+RTL_TOP := retrosoc_tiny_tb
\ No newline at end of file
diff --git a/crt/README.md b/crt/README.md
index 8c5809eb..82fa9125 100644
--- a/crt/README.md
+++ b/crt/README.md
@@ -239,11 +239,16 @@ For the complete firmware, simulation, synthesis, and timing flow, see the
Tiny uses the same `rs_` SDK and selects its generated memory, IRQ and capability
headers through the committed profile. The generated memory-map header includes
-Tiny capability metadata, so public DMA headers report four channels without
+Tiny capability metadata, so public DMA headers report eight channels without
requiring an application to reproduce private compiler flags. Unavailable IP
drivers are omitted; Tiny SYSCTRL rejects unsupported lifecycle/PLL operations
before MMIO. Startup skips external-RAM initialization and loads into SRAM.
+P6 selects the existing RNG/CRC/WS2812/Crypto/I2S/SDIO drivers. Crypto uses 4/5,
+audio uses 6/7 and channel 3 remains exclusively leased for a WS frame.
+I2C1, SDIO1, DVP and future DMA endpoints remain unsupported on Tiny. Shared
+Mini defaults are unchanged; Tiny does not acquire Mini's HP channel reservation.
+
`` provides `rs_watchdog_configure`, `rs_watchdog_start`,
`rs_watchdog_service`, `rs_watchdog_get_status` and `rs_watchdog_clear_reset_cause`,
with SDK status values and bounded command waits. These wrappers preserve the
diff --git a/crt/include/retrosoc/core/status.h b/crt/include/retrosoc/core/status.h
index 5cee6728..52f43034 100644
--- a/crt/include/retrosoc/core/status.h
+++ b/crt/include/retrosoc/core/status.h
@@ -12,6 +12,7 @@ typedef enum {
RS_EIO = -5,
RS_ENOTSUP = -6,
RS_EFORMAT = -7,
+ RS_EBUSY = -8,
} rs_status_t;
typedef uint32_t rs_timeout_t;
diff --git a/crt/include/retrosoc/hal/dma.h b/crt/include/retrosoc/hal/dma.h
index bf35d7ae..9e3cc015 100644
--- a/crt/include/retrosoc/hal/dma.h
+++ b/crt/include/retrosoc/hal/dma.h
@@ -50,6 +50,15 @@ typedef enum {
RS_DMA_CHANNEL_BULK = 3,
RS_DMA_CHANNEL_CRYPTO_IN = 4,
RS_DMA_CHANNEL_CRYPTO_OUT = 5,
+#ifdef RS_SOC_TINY
+ RS_DMA_CHANNEL_DVP = 2,
+ RS_DMA_CHANNEL_I2S_TX = 6,
+ RS_DMA_CHANNEL_I2S_RX = 7,
+#else
+ RS_DMA_CHANNEL_DVP = RS_DMA_CHANNEL_BULK,
+ RS_DMA_CHANNEL_I2S_TX = RS_DMA_CHANNEL_BULK,
+ RS_DMA_CHANNEL_I2S_RX = RS_DMA_CHANNEL_BULK,
+#endif
} rs_dma_channel_t;
typedef struct {
diff --git a/crt/include/retrosoc/hal/dma_regs.h b/crt/include/retrosoc/hal/dma_regs.h
index 596c8e1d..fe70e4f1 100644
--- a/crt/include/retrosoc/hal/dma_regs.h
+++ b/crt/include/retrosoc/hal/dma_regs.h
@@ -77,15 +77,13 @@
#define RS_DMA_ERROR_READ UINT32_C(0x00000040)
#define RS_DMA_ERROR_STREAM_LAST UINT32_C(0x00000100)
-#ifdef RS_SOC_TINY
-#define RS_DMA_CHANNEL_COUNT 4U
-#else
-#define RS_DMA_CHANNEL_COUNT 8U
-#endif
+#define RS_DMA_CHANNEL_COUNT 8U
+#ifndef RS_SOC_TINY
#define RS_DMA_CHANNEL_HP 6U
#define RS_DMA_CHANNEL_RESERVED 7U
-#define RS_DMA_MAX_BURST_BEATS 16U
-#define RS_DMA_DATA_WIDTH_BITS 32U
+#endif
+#define RS_DMA_MAX_BURST_BEATS 16U
+#define RS_DMA_DATA_WIDTH_BITS 32U
#define RS_DMA_CHANNEL_OFFSET(channel, offset) \
(RS_DMA_CH_BASE + ((uint32_t)(channel)*RS_DMA_CH_STRIDE) + (offset))
diff --git a/crt/include/retrosoc/hal/rcu.h b/crt/include/retrosoc/hal/rcu.h
new file mode 100644
index 00000000..0965c017
--- /dev/null
+++ b/crt/include/retrosoc/hal/rcu.h
@@ -0,0 +1,42 @@
+#ifndef RETROSOC_HAL_RCU_H
+#define RETROSOC_HAL_RCU_H
+
+#include
+#include
+
+#include
+
+typedef enum {
+ RS_TINY_RCU_PROFILE_SAFE24 = 0,
+ RS_TINY_RCU_PROFILE_XIN = 1,
+ RS_TINY_RCU_PROFILE_PLL192 = 2,
+ RS_TINY_RCU_PROFILE_PLL240 = 3,
+} rs_tiny_rcu_profile_t;
+
+typedef struct {
+ uint32_t capability;
+ uint32_t target_capability;
+ uint32_t status;
+ uint32_t fault;
+ uint32_t reset_cause;
+ uint32_t gate_status;
+ uint32_t xin_hz;
+ uint32_t ref_hz;
+ uint32_t sys_hz;
+ uint32_t mem_hz;
+ uint32_t pclk_hz;
+ uint32_t clint_hz;
+ rs_tiny_rcu_profile_t current_profile;
+} rs_tiny_rcu_snapshot_t;
+
+rs_status_t rs_tiny_rcu_get_snapshot(rs_tiny_rcu_snapshot_t *snapshot);
+rs_status_t rs_tiny_rcu_profile_hz(rs_tiny_rcu_profile_t profile, uint32_t *hz);
+rs_status_t rs_tiny_rcu_request_profile(rs_tiny_rcu_profile_t profile, rs_timeout_t timeout);
+rs_status_t rs_tiny_rcu_apply_gate(uint32_t target_mask, rs_timeout_t timeout);
+rs_status_t rs_tiny_rcu_apply_reset(uint32_t target_mask, rs_timeout_t timeout);
+rs_status_t rs_tiny_rcu_clear_fault(uint32_t mask);
+rs_status_t rs_tiny_rcu_clear_reset_cause(uint32_t mask);
+rs_status_t rs_tiny_rcu_system_reset(rs_timeout_t timeout);
+rs_status_t rs_tiny_rcu_set_clkout(uint32_t source, uint16_t half_period);
+
+#endif
diff --git a/crt/include/retrosoc/hal/rcu_regs.h b/crt/include/retrosoc/hal/rcu_regs.h
new file mode 100644
index 00000000..f2dfc126
--- /dev/null
+++ b/crt/include/retrosoc/hal/rcu_regs.h
@@ -0,0 +1,46 @@
+#ifndef RETROSOC_HAL_RCU_REGS_H
+#define RETROSOC_HAL_RCU_REGS_H
+
+#include
+
+#define RS_TINY_RCU_ID_OFFSET UINT32_C(0x100)
+#define RS_TINY_RCU_VERSION_OFFSET UINT32_C(0x104)
+#define RS_TINY_RCU_CAPABILITY_OFFSET UINT32_C(0x108)
+#define RS_TINY_RCU_CLOCK_REQUEST_OFFSET UINT32_C(0x10C)
+#define RS_TINY_RCU_CLOCK_CURRENT_OFFSET UINT32_C(0x110)
+#define RS_TINY_RCU_COMMAND_OFFSET UINT32_C(0x114)
+#define RS_TINY_RCU_STATUS_OFFSET UINT32_C(0x118)
+#define RS_TINY_RCU_TIMEOUT_OFFSET UINT32_C(0x11C)
+#define RS_TINY_RCU_GATE_REQUEST_OFFSET UINT32_C(0x120)
+#define RS_TINY_RCU_GATE_STATUS_OFFSET UINT32_C(0x124)
+#define RS_TINY_RCU_RESET_REQUEST_OFFSET UINT32_C(0x128)
+#define RS_TINY_RCU_RESET_DONE_OFFSET UINT32_C(0x12C)
+#define RS_TINY_RCU_RESET_CAUSE_OFFSET UINT32_C(0x130)
+#define RS_TINY_RCU_FAULT_OFFSET UINT32_C(0x134)
+#define RS_TINY_RCU_IRQ_STATE_OFFSET UINT32_C(0x138)
+#define RS_TINY_RCU_IRQ_ENABLE_OFFSET UINT32_C(0x13C)
+#define RS_TINY_RCU_XIN_HZ_OFFSET UINT32_C(0x140)
+#define RS_TINY_RCU_REF_HZ_OFFSET UINT32_C(0x144)
+#define RS_TINY_RCU_SYS_HZ_OFFSET UINT32_C(0x148)
+#define RS_TINY_RCU_MEM_HZ_OFFSET UINT32_C(0x14C)
+#define RS_TINY_RCU_PCLK_HZ_OFFSET UINT32_C(0x150)
+#define RS_TINY_RCU_CLINT_HZ_OFFSET UINT32_C(0x154)
+#define RS_TINY_RCU_TARGET_CAP_OFFSET UINT32_C(0x158)
+#define RS_TINY_RCU_CLKOUT_OFFSET UINT32_C(0x15C)
+
+#define RS_TINY_RCU_COMMAND_CLOCK_APPLY UINT32_C(1U << 0)
+#define RS_TINY_RCU_COMMAND_GATE_APPLY UINT32_C(1U << 1)
+#define RS_TINY_RCU_COMMAND_RESET_APPLY UINT32_C(1U << 2)
+#define RS_TINY_RCU_COMMAND_SYSTEM_RESET UINT32_C(1U << 3)
+#define RS_TINY_RCU_STATUS_BUSY UINT32_C(1U << 0)
+#define RS_TINY_RCU_STATUS_SAFE_SOURCE UINT32_C(1U << 1)
+#define RS_TINY_RCU_STATUS_PLL_LOCKED UINT32_C(1U << 2)
+#define RS_TINY_RCU_STATUS_PLL_SELECTED UINT32_C(1U << 3)
+#define RS_TINY_RCU_STATUS_FAULT UINT32_C(1U << 4)
+#define RS_TINY_RCU_CAP_PLL_PRESENT UINT32_C(1U << 8)
+#define RS_TINY_RCU_CAP_TARGET_MASK UINT32_C(0x0003FFFF)
+#define RS_TINY_RCU_FAULT_MASK UINT32_C(0x0000001F)
+#define RS_TINY_RCU_RESET_CAUSE_MASK UINT32_C(0x0000003F)
+#define RS_TINY_RCU_CSR_MSLEEP 0xBF0
+
+#endif
diff --git a/crt/src/hal/dma.c b/crt/src/hal/dma.c
index bd9c2a81..9b3832da 100644
--- a/crt/src/hal/dma.c
+++ b/crt/src/hal/dma.c
@@ -5,16 +5,25 @@
static const rs_dma_session_t *rs_dma_owners[RS_DMA_CHANNEL_COUNT];
+#ifdef RS_DMA_TEST_MMIO
+uint32_t rs_dma_test_irq_save(void);
+void rs_dma_test_irq_restore(uint32_t saved);
+#endif
+
static uint32_t rs_dma_lock(void) {
uint32_t saved = 0U;
-#if defined(__riscv) && defined(CSR_ENABLE)
+#ifdef RS_DMA_TEST_MMIO
+ saved = rs_dma_test_irq_save();
+#elif 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)
+#ifdef RS_DMA_TEST_MMIO
+ rs_dma_test_irq_restore(saved);
+#elif defined(__riscv) && defined(CSR_ENABLE)
if ((saved & UINT32_C(8)) != 0U) {
__asm__ volatile("csrsi mstatus, 8" ::: "memory");
}
@@ -185,6 +194,7 @@ rs_status_t rs_dma_submit_tcd_chain(uint32_t channel, rs_dma_tcd_t *first, uint3
rs_status_t result;
uint32_t control;
uint32_t burst;
+ uint32_t saved;
result = rs_dma_tcd_validate(channel, current);
if (result != RS_OK) {
@@ -208,14 +218,20 @@ rs_status_t rs_dma_submit_tcd_chain(uint32_t channel, rs_dma_tcd_t *first, uint3
config.crc_expected = current->crc_expected;
config.priority = (uint8_t)((control >> RS_DMA_TCD_PRIORITY_SHIFT) & UINT32_C(0x3));
config.burst_beats = (uint8_t)burst;
+ /* Publish configuration, TCD and START before an ISR can lease the
+ * idle channel. Nested helpers preserve this outer interrupt mask.
+ * Restore interrupts before the bounded completion wait. */
+ saved = rs_dma_lock();
result = rs_dma_configure(channel, &config);
if (result != RS_OK) {
+ rs_dma_unlock(saved);
return result;
}
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);
rs_dma_fence();
result = rs_dma_start(channel);
+ rs_dma_unlock(saved);
if (result != RS_OK) {
return result;
}
@@ -411,9 +427,9 @@ void ip_dma_test(void) {
.lowbound = 32U,
}) != RS_OK) ||
(rs_i2s_enable(true, false) != RS_OK) ||
- (rs_dma_configure(RS_DMA_CHANNEL_BULK, &tx_config) != RS_OK) ||
+ (rs_dma_configure(RS_DMA_CHANNEL_I2S_TX, &tx_config) != RS_OK) ||
(rs_dma_start(RS_DMA_CHANNEL_BULK) != RS_OK) ||
- (rs_dma_wait(RS_DMA_CHANNEL_BULK, RS_TIMEOUT_DEFAULT) != RS_OK)) {
+ (rs_dma_wait(RS_DMA_CHANNEL_I2S_TX, RS_TIMEOUT_DEFAULT) != RS_OK)) {
printf("dma transmit failed\n");
return;
}
@@ -432,9 +448,9 @@ void ip_dma_test(void) {
.lowbound = 32U,
}) != RS_OK) ||
(rs_i2s_enable(false, true) != RS_OK) ||
- (rs_dma_configure(RS_DMA_CHANNEL_BULK, &rx_config) != RS_OK) ||
- (rs_dma_start(RS_DMA_CHANNEL_BULK) != RS_OK) ||
- (rs_dma_wait(RS_DMA_CHANNEL_BULK, RS_TIMEOUT_DEFAULT) != RS_OK)) {
+ (rs_dma_configure(RS_DMA_CHANNEL_I2S_RX, &rx_config) != RS_OK) ||
+ (rs_dma_start(RS_DMA_CHANNEL_I2S_RX) != RS_OK) ||
+ (rs_dma_wait(RS_DMA_CHANNEL_I2S_RX, RS_TIMEOUT_DEFAULT) != RS_OK)) {
printf("dma receive failed\n");
(void)rs_i2s_disable();
return;
diff --git a/crt/src/hal/dma_math.c b/crt/src/hal/dma_math.c
index 99c35ca6..4ebd003a 100644
--- a/crt/src/hal/dma_math.c
+++ b/crt/src/hal/dma_math.c
@@ -19,7 +19,8 @@ static bool rs_dma_address_valid(uintptr_t address, uint32_t byte_count, bool in
static bool rs_dma_request_valid(const rs_dma_config_t *config) {
#ifdef RS_SOC_TINY
- if (config->kind != RS_DMA_KIND_MM_TO_MM) {
+ if (((uint32_t)config->request >= 16U) ||
+ ((UINT32_C(0x31FF) & (UINT32_C(1) << (uint32_t)config->request)) == 0U)) {
return false;
}
#endif
@@ -97,7 +98,7 @@ rs_status_t rs_dma_tcd_validate(uint32_t channel, const rs_dma_tcd_t *tcd) {
request = (tcd->control >> RS_DMA_TCD_REQUEST_SHIFT) & UINT32_C(0xF);
burst = (tcd->control >> RS_DMA_TCD_BURST_SHIFT) & UINT32_C(0x1F);
#ifdef RS_SOC_TINY
- if (kind != (uint32_t)RS_DMA_KIND_MM_TO_MM) {
+ if ((UINT32_C(0x31FF) & (UINT32_C(1) << request)) == 0U) {
return RS_ENOTSUP;
}
#endif
diff --git a/crt/src/hal/i2c.c b/crt/src/hal/i2c.c
index 48c59260..dea6ccbc 100644
--- a/crt/src/hal/i2c.c
+++ b/crt/src/hal/i2c.c
@@ -49,8 +49,12 @@
#define RS_I2C_TIMEOUT_MAX_CYCLES UINT32_C(0xFFFFFF)
static uintptr_t rs_i2c_base(rs_i2c_bus_t bus) {
+#ifdef RS_SOC_TINY
+ return (bus == RS_I2C_BUS_0) ? (uintptr_t)RS_SOC_APB4_I2C0_BASE : (uintptr_t)0U;
+#else
return (bus == RS_I2C_BUS_0) ? (uintptr_t)RS_SOC_APB4_I2C0_BASE
: (uintptr_t)RS_SOC_APB4_I2C1_BASE;
+#endif
}
static bool rs_i2c_bus_valid(rs_i2c_bus_t bus) {
@@ -92,8 +96,13 @@ static rs_status_t rs_i2c_gpio_configure(rs_i2c_bus_t bus) {
.filter_enable = false,
.interrupt_enable = false,
};
+#ifdef RS_SOC_TINY
+ const uint32_t scl_pin = UINT32_C(10);
+ const uint32_t sda_pin = UINT32_C(11);
+#else
const uint32_t scl_pin = (bus == RS_I2C_BUS_0) ? UINT32_C(7) : UINT32_C(3);
const uint32_t sda_pin = (bus == RS_I2C_BUS_0) ? UINT32_C(8) : UINT32_C(4);
+#endif
rs_status_t status = rs_gpio_configure(scl_pin, &pin_config);
if (status == RS_OK) {
@@ -228,6 +237,11 @@ static rs_status_t rs_i2c_wait_done(rs_i2c_bus_t bus, rs_timeout_t timeout) {
rs_status_t rs_i2c_configure(rs_i2c_bus_t bus, const rs_i2c_config_t *config,
rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (bus == RS_I2C_BUS_1) {
+ return RS_ENOTSUP;
+ }
+#endif
rs_i2c_timing_t timing;
uint32_t stretch_timeout;
uint32_t bus_idle_timeout;
@@ -287,6 +301,11 @@ rs_status_t rs_i2c_configure(rs_i2c_bus_t bus, const rs_i2c_config_t *config,
}
rs_status_t rs_i2c_init(rs_i2c_bus_t bus, uint32_t source_clock_hz, uint32_t bus_hz) {
+#ifdef RS_SOC_TINY
+ if (bus == RS_I2C_BUS_1) {
+ return RS_ENOTSUP;
+ }
+#endif
const rs_i2c_config_t config = {
.source_clock_hz = source_clock_hz,
.bus_hz = bus_hz,
@@ -303,6 +322,11 @@ rs_status_t rs_i2c_init(rs_i2c_bus_t bus, uint32_t source_clock_hz, uint32_t bus
rs_status_t rs_i2c_transfer(rs_i2c_bus_t bus, const rs_i2c_transfer_t *transfer,
rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (bus == RS_I2C_BUS_1) {
+ return RS_ENOTSUP;
+ }
+#endif
if (transfer == NULL) {
return RS_EINVAL;
}
@@ -338,6 +362,11 @@ static rs_status_t rs_i2c_register_prefix(const rs_i2c_register_access_t *access
rs_status_t rs_i2c_register_write(rs_i2c_bus_t bus, const rs_i2c_register_access_t *access,
const uint8_t *data, size_t length, rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (bus == RS_I2C_BUS_1) {
+ return RS_ENOTSUP;
+ }
+#endif
uint8_t prefix[2];
size_t prefix_length;
rs_status_t status;
@@ -355,6 +384,11 @@ rs_status_t rs_i2c_register_write(rs_i2c_bus_t bus, const rs_i2c_register_access
rs_status_t rs_i2c_register_read(rs_i2c_bus_t bus, const rs_i2c_register_access_t *access,
uint8_t *data, size_t length, rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (bus == RS_I2C_BUS_1) {
+ return RS_ENOTSUP;
+ }
+#endif
uint8_t prefix[2];
size_t prefix_length;
rs_status_t status;
@@ -371,6 +405,11 @@ rs_status_t rs_i2c_register_read(rs_i2c_bus_t bus, const rs_i2c_register_access_
}
rs_status_t rs_i2c_abort(rs_i2c_bus_t bus, rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (bus == RS_I2C_BUS_1) {
+ return RS_ENOTSUP;
+ }
+#endif
if (!rs_i2c_bus_valid(bus)) {
return RS_EINVAL;
}
@@ -382,6 +421,11 @@ rs_status_t rs_i2c_abort(rs_i2c_bus_t bus, rs_timeout_t timeout) {
}
rs_status_t rs_i2c_recover(rs_i2c_bus_t bus, rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (bus == RS_I2C_BUS_1) {
+ return RS_ENOTSUP;
+ }
+#endif
rs_status_t status;
if (!rs_i2c_bus_valid(bus)) {
@@ -408,6 +452,11 @@ rs_status_t rs_i2c_recover(rs_i2c_bus_t bus, rs_timeout_t timeout) {
}
rs_status_t rs_i2c_get_status(rs_i2c_bus_t bus, rs_i2c_status_t *status) {
+#ifdef RS_SOC_TINY
+ if (bus == RS_I2C_BUS_1) {
+ return RS_ENOTSUP;
+ }
+#endif
uint32_t levels;
if (!rs_i2c_bus_valid(bus) || (status == NULL)) {
@@ -423,6 +472,11 @@ rs_status_t rs_i2c_get_status(rs_i2c_bus_t bus, rs_i2c_status_t *status) {
}
rs_status_t rs_i2c_irq_enable(rs_i2c_bus_t bus, uint32_t mask) {
+#ifdef RS_SOC_TINY
+ if (bus == RS_I2C_BUS_1) {
+ return RS_ENOTSUP;
+ }
+#endif
if (!rs_i2c_bus_valid(bus) || ((mask & ~RS_I2C_INTR_ALL) != 0U)) {
return RS_EINVAL;
}
@@ -431,6 +485,11 @@ rs_status_t rs_i2c_irq_enable(rs_i2c_bus_t bus, uint32_t mask) {
}
rs_status_t rs_i2c_irq_ack(rs_i2c_bus_t bus, uint32_t mask) {
+#ifdef RS_SOC_TINY
+ if (bus == RS_I2C_BUS_1) {
+ return RS_ENOTSUP;
+ }
+#endif
if (!rs_i2c_bus_valid(bus) || (mask == 0U) || ((mask & ~RS_I2C_INTR_ALL) != 0U)) {
return RS_EINVAL;
}
@@ -439,6 +498,11 @@ rs_status_t rs_i2c_irq_ack(rs_i2c_bus_t bus, uint32_t mask) {
}
rs_status_t rs_i2c_irq_test(rs_i2c_bus_t bus, uint32_t mask) {
+#ifdef RS_SOC_TINY
+ if (bus == RS_I2C_BUS_1) {
+ return RS_ENOTSUP;
+ }
+#endif
if (!rs_i2c_bus_valid(bus) || (mask == 0U) || ((mask & ~RS_I2C_INTR_ALL) != 0U)) {
return RS_EINVAL;
}
@@ -449,6 +513,11 @@ rs_status_t rs_i2c_irq_test(rs_i2c_bus_t bus, uint32_t mask) {
rs_status_t rs_i2c_write_dma(rs_i2c_bus_t bus, uint16_t address, bool ten_bit_address,
const uint8_t *data, size_t length, rs_i2c_dma_workspace_t *workspace,
rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (bus == RS_I2C_BUS_1) {
+ return RS_ENOTSUP;
+ }
+#endif
rs_status_t status;
size_t index;
const uint32_t channel = (bus == RS_I2C_BUS_0) ? RS_DMA_CHANNEL_I2C0 : RS_DMA_CHANNEL_I2C1;
@@ -494,6 +563,11 @@ rs_status_t rs_i2c_write_dma(rs_i2c_bus_t bus, uint16_t address, bool ten_bit_ad
rs_status_t rs_i2c_read_dma(rs_i2c_bus_t bus, uint16_t address, bool ten_bit_address, uint8_t *data,
size_t length, rs_i2c_dma_workspace_t *workspace,
rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (bus == RS_I2C_BUS_1) {
+ return RS_ENOTSUP;
+ }
+#endif
rs_status_t status;
size_t index;
const uint32_t channel = (bus == RS_I2C_BUS_0) ? RS_DMA_CHANNEL_I2C0 : RS_DMA_CHANNEL_I2C1;
diff --git a/crt/src/hal/rcu.c b/crt/src/hal/rcu.c
new file mode 100644
index 00000000..4dddf99f
--- /dev/null
+++ b/crt/src/hal/rcu.c
@@ -0,0 +1,261 @@
+#include
+
+#include
+#include
+#include
+#if defined(__riscv) && defined(RS_SOC_TINY)
+#include
+#endif
+
+static volatile uint32_t *rs_tiny_rcu_register(uint32_t offset) {
+#if defined(RS_TINY_RCU_TEST_MMIO)
+ extern volatile uint32_t rs_tiny_rcu_test_mmio[1024];
+ return &rs_tiny_rcu_test_mmio[offset / 4U];
+#else
+ return (volatile uint32_t *)(RS_SOC_APB4_SYSCTRL_BASE + (uintptr_t)offset);
+#endif
+}
+
+static rs_status_t rs_tiny_rcu_fault_status(void) {
+ const uint32_t fault = *rs_tiny_rcu_register(RS_TINY_RCU_FAULT_OFFSET);
+ if ((fault & UINT32_C(0x09)) != 0U) {
+ return RS_EIO;
+ }
+ return (fault != 0U) ? RS_ETIMEOUT : RS_OK;
+}
+
+static rs_status_t rs_tiny_rcu_prepare(rs_timeout_t timeout) {
+ if (timeout == 0U) {
+ return RS_EINVAL;
+ }
+ if ((*rs_tiny_rcu_register(RS_TINY_RCU_STATUS_OFFSET) & RS_TINY_RCU_STATUS_BUSY) != 0U) {
+ return RS_EBUSY;
+ }
+ /* The caller owns clearing sticky faults. Do not start an operation whose
+ * result could be confused with a previous failure. */
+ return rs_tiny_rcu_fault_status();
+}
+
+static rs_status_t rs_tiny_rcu_wait(rs_timeout_t timeout, uint32_t *status) {
+ uint32_t value = 0U;
+
+ while (timeout-- != 0U) {
+ value = *rs_tiny_rcu_register(RS_TINY_RCU_STATUS_OFFSET);
+ if ((value & RS_TINY_RCU_STATUS_BUSY) == 0U) {
+ if (status != NULL) {
+ *status = value;
+ }
+ return RS_OK;
+ }
+ }
+ return RS_ETIMEOUT;
+}
+
+rs_status_t rs_tiny_rcu_get_snapshot(rs_tiny_rcu_snapshot_t *snapshot) {
+ uint32_t initial_profile;
+ if (snapshot == NULL) {
+ return RS_EINVAL;
+ }
+ if ((*rs_tiny_rcu_register(RS_TINY_RCU_STATUS_OFFSET) & RS_TINY_RCU_STATUS_BUSY) != 0U) {
+ return RS_EBUSY;
+ }
+ initial_profile = *rs_tiny_rcu_register(RS_TINY_RCU_CLOCK_CURRENT_OFFSET);
+ snapshot->capability = *rs_tiny_rcu_register(RS_TINY_RCU_CAPABILITY_OFFSET);
+ snapshot->target_capability = *rs_tiny_rcu_register(RS_TINY_RCU_TARGET_CAP_OFFSET);
+ snapshot->status = *rs_tiny_rcu_register(RS_TINY_RCU_STATUS_OFFSET);
+ snapshot->fault = *rs_tiny_rcu_register(RS_TINY_RCU_FAULT_OFFSET);
+ snapshot->reset_cause = *rs_tiny_rcu_register(RS_TINY_RCU_RESET_CAUSE_OFFSET);
+ snapshot->gate_status = *rs_tiny_rcu_register(RS_TINY_RCU_GATE_STATUS_OFFSET);
+ snapshot->xin_hz = *rs_tiny_rcu_register(RS_TINY_RCU_XIN_HZ_OFFSET);
+ snapshot->ref_hz = *rs_tiny_rcu_register(RS_TINY_RCU_REF_HZ_OFFSET);
+ snapshot->sys_hz = *rs_tiny_rcu_register(RS_TINY_RCU_SYS_HZ_OFFSET);
+ snapshot->mem_hz = *rs_tiny_rcu_register(RS_TINY_RCU_MEM_HZ_OFFSET);
+ snapshot->pclk_hz = *rs_tiny_rcu_register(RS_TINY_RCU_PCLK_HZ_OFFSET);
+ snapshot->clint_hz = *rs_tiny_rcu_register(RS_TINY_RCU_CLINT_HZ_OFFSET);
+ snapshot->current_profile =
+ (rs_tiny_rcu_profile_t)(*rs_tiny_rcu_register(RS_TINY_RCU_CLOCK_CURRENT_OFFSET) &
+ UINT32_C(3));
+ if (((*rs_tiny_rcu_register(RS_TINY_RCU_STATUS_OFFSET) & RS_TINY_RCU_STATUS_BUSY) != 0U) ||
+ ((uint32_t)snapshot->current_profile != initial_profile)) {
+ return RS_EBUSY;
+ }
+ return RS_OK;
+}
+
+rs_status_t rs_tiny_rcu_profile_hz(rs_tiny_rcu_profile_t profile, uint32_t *hz) {
+ if (hz == NULL) {
+ return RS_EINVAL;
+ }
+ switch (profile) {
+ case RS_TINY_RCU_PROFILE_SAFE24:
+ *hz = UINT32_C(24000000);
+ break;
+ case RS_TINY_RCU_PROFILE_XIN:
+ *hz = *rs_tiny_rcu_register(RS_TINY_RCU_XIN_HZ_OFFSET);
+ break;
+ case RS_TINY_RCU_PROFILE_PLL192:
+ *hz = UINT32_C(192000000);
+ break;
+ case RS_TINY_RCU_PROFILE_PLL240:
+ *hz = UINT32_C(240000000);
+ break;
+ default:
+ return RS_EINVAL;
+ }
+ return RS_OK;
+}
+
+rs_status_t rs_tiny_rcu_request_profile(rs_tiny_rcu_profile_t profile, rs_timeout_t timeout) {
+ uint32_t capability;
+ uint32_t status;
+ uint32_t hz;
+ rs_status_t result;
+
+ if (timeout == 0U || rs_tiny_rcu_profile_hz(profile, &hz) != RS_OK) {
+ return RS_EINVAL;
+ }
+ (void)hz;
+ capability = *rs_tiny_rcu_register(RS_TINY_RCU_CAPABILITY_OFFSET);
+ if ((capability & (UINT32_C(1) << (uint32_t)profile)) == 0U) {
+ return RS_ENOTSUP;
+ }
+ result = rs_tiny_rcu_prepare(timeout);
+ if (result != RS_OK) {
+ return result;
+ }
+ *rs_tiny_rcu_register(RS_TINY_RCU_CLOCK_REQUEST_OFFSET) = (uint32_t)profile;
+ *rs_tiny_rcu_register(RS_TINY_RCU_COMMAND_OFFSET) = RS_TINY_RCU_COMMAND_CLOCK_APPLY;
+ if (rs_tiny_rcu_wait(timeout, &status) != RS_OK) {
+ return RS_ETIMEOUT;
+ }
+ if ((status & RS_TINY_RCU_STATUS_FAULT) != 0U) {
+ return rs_tiny_rcu_fault_status();
+ }
+ if (*rs_tiny_rcu_register(RS_TINY_RCU_CLOCK_CURRENT_OFFSET) != (uint32_t)profile) {
+ return RS_EIO;
+ }
+ return RS_OK;
+}
+
+rs_status_t rs_tiny_rcu_apply_gate(uint32_t target_mask, rs_timeout_t timeout) {
+ uint32_t status;
+ rs_status_t result;
+
+ if (timeout == 0U || (target_mask & ~RS_TINY_RCU_CAP_TARGET_MASK) != 0U) {
+ return RS_EINVAL;
+ }
+ if ((target_mask & ~(*rs_tiny_rcu_register(RS_TINY_RCU_TARGET_CAP_OFFSET))) != 0U) {
+ return RS_ENOTSUP;
+ }
+ result = rs_tiny_rcu_prepare(timeout);
+ if (result != RS_OK) {
+ return result;
+ }
+ *rs_tiny_rcu_register(RS_TINY_RCU_GATE_REQUEST_OFFSET) = target_mask;
+ *rs_tiny_rcu_register(RS_TINY_RCU_COMMAND_OFFSET) = RS_TINY_RCU_COMMAND_GATE_APPLY;
+ if (rs_tiny_rcu_wait(timeout, &status) != RS_OK) {
+ return RS_ETIMEOUT;
+ }
+ result = rs_tiny_rcu_fault_status();
+ if (result != RS_OK) {
+ return result;
+ }
+#if defined(__riscv) && defined(RS_SOC_TINY)
+ /* Hazard3's existing sleep permission complements the RCU leaf gate.
+ * Keep power-down clear: the fabric, SRAM and DMA remain available. */
+ if ((target_mask & 1U) != 0U) {
+ __RV_CSR_CLEAR(RS_TINY_RCU_CSR_MSLEEP, 2U);
+ __RV_CSR_SET(RS_TINY_RCU_CSR_MSLEEP, 1U);
+ } else {
+ __RV_CSR_CLEAR(RS_TINY_RCU_CSR_MSLEEP, 1U);
+ }
+#endif
+ return RS_OK;
+}
+
+rs_status_t rs_tiny_rcu_apply_reset(uint32_t target_mask, rs_timeout_t timeout) {
+ uint32_t status;
+ rs_status_t result;
+
+ if (timeout == 0U || target_mask == 0U || (target_mask & ~RS_TINY_RCU_CAP_TARGET_MASK) != 0U ||
+ (target_mask & 1U) != 0U) {
+ return RS_EINVAL;
+ }
+ if ((target_mask & ~(*rs_tiny_rcu_register(RS_TINY_RCU_TARGET_CAP_OFFSET))) != 0U) {
+ return RS_ENOTSUP;
+ }
+ result = rs_tiny_rcu_prepare(timeout);
+ if (result != RS_OK) {
+ return result;
+ }
+ if ((*rs_tiny_rcu_register(RS_TINY_RCU_GATE_STATUS_OFFSET) & target_mask) != 0U) {
+ return RS_EBUSY;
+ }
+ *rs_tiny_rcu_register(RS_TINY_RCU_RESET_REQUEST_OFFSET) = target_mask;
+ *rs_tiny_rcu_register(RS_TINY_RCU_COMMAND_OFFSET) = RS_TINY_RCU_COMMAND_RESET_APPLY;
+ if (rs_tiny_rcu_wait(timeout, &status) != RS_OK) {
+ return RS_ETIMEOUT;
+ }
+ return rs_tiny_rcu_fault_status();
+}
+
+rs_status_t rs_tiny_rcu_clear_fault(uint32_t mask) {
+ if ((mask & ~RS_TINY_RCU_FAULT_MASK) != 0U) {
+ return RS_EINVAL;
+ }
+ *rs_tiny_rcu_register(RS_TINY_RCU_FAULT_OFFSET) = mask;
+ return RS_OK;
+}
+
+rs_status_t rs_tiny_rcu_clear_reset_cause(uint32_t mask) {
+ if ((mask & ~RS_TINY_RCU_RESET_CAUSE_MASK) != 0U) {
+ return RS_EINVAL;
+ }
+ *rs_tiny_rcu_register(RS_TINY_RCU_RESET_CAUSE_OFFSET) = mask;
+ return RS_OK;
+}
+
+rs_status_t rs_tiny_rcu_system_reset(rs_timeout_t timeout) {
+ uint32_t status;
+ if (timeout == 0U) {
+ return RS_EINVAL;
+ }
+ status = *rs_tiny_rcu_register(RS_TINY_RCU_STATUS_OFFSET);
+ if ((status & RS_TINY_RCU_STATUS_BUSY) != 0U) {
+ return RS_EBUSY;
+ }
+ *rs_tiny_rcu_register(RS_TINY_RCU_COMMAND_OFFSET) = RS_TINY_RCU_COMMAND_SYSTEM_RESET;
+ /* Success restarts execution at the reset vector. Returning means failure. */
+ while (timeout-- != 0U) {
+ status = *rs_tiny_rcu_register(RS_TINY_RCU_STATUS_OFFSET);
+ if ((status & RS_TINY_RCU_STATUS_FAULT) != 0U) {
+ return RS_EIO;
+ }
+ }
+ return RS_ETIMEOUT;
+}
+
+rs_status_t rs_tiny_rcu_set_clkout(uint32_t source, uint16_t half_period) {
+ rs_tiny_rcu_snapshot_t snapshot;
+ rs_status_t result;
+ uint32_t hz;
+ if (source > 4U || (source != 0U && half_period == 0U)) {
+ return RS_EINVAL;
+ }
+ result = rs_tiny_rcu_get_snapshot(&snapshot);
+ if (result != RS_OK) {
+ return result;
+ }
+ hz = source == 2U ? snapshot.sys_hz
+ : source == 3U ? snapshot.mem_hz
+ : source == 4U ? snapshot.pclk_hz
+ : snapshot.ref_hz;
+ if (source != 0U && (uint64_t)hz > (UINT64_C(48000000) * (uint64_t)half_period)) {
+ return RS_EINVAL;
+ }
+ if (source != 0U && (*rs_tiny_rcu_register(RS_TINY_RCU_CLKOUT_OFFSET) & 7U) != 0U) {
+ return RS_EBUSY;
+ }
+ *rs_tiny_rcu_register(RS_TINY_RCU_CLKOUT_OFFSET) = source | ((uint32_t)half_period << 8U);
+ return (*rs_tiny_rcu_register(RS_TINY_RCU_FAULT_OFFSET) != 0U) ? RS_EIO : RS_OK;
+}
diff --git a/crt/src/hal/sdio.c b/crt/src/hal/sdio.c
index bf303255..82b7098e 100644
--- a/crt/src/hal/sdio.c
+++ b/crt/src/hal/sdio.c
@@ -53,6 +53,12 @@
RS_SD_R1_CARD_ECC_FAILED | RS_SD_R1_CC_ERROR | RS_SD_R1_ERROR | RS_SD_R1_CSD_OVERWRITE | \
RS_SD_R1_WP_ERASE_SKIP)
+/* Preserve protocol ceilings while supporting slower product input clocks. */
+static uint32_t rs_sdio_transfer_clock(uint32_t source_hz, uint32_t ceiling_hz) {
+ const uint32_t maximum_hz = source_hz / UINT32_C(2);
+ return (maximum_hz < ceiling_hz) ? maximum_hz : ceiling_hz;
+}
+
static bool rs_sdio_instance_valid(rs_sdio_instance_t instance) {
return (instance == RS_SDIO_INSTANCE_0) || (instance == RS_SDIO_INSTANCE_1);
}
@@ -61,8 +67,10 @@ static uintptr_t rs_sdio_base(rs_sdio_instance_t instance) {
switch (instance) {
case RS_SDIO_INSTANCE_0:
return (uintptr_t)RS_SOC_APB4_SDIO0_BASE;
+#ifndef RS_SOC_TINY
case RS_SDIO_INSTANCE_1:
return (uintptr_t)RS_SOC_APB4_SDIO1_BASE;
+#endif
default:
return (uintptr_t)0U;
}
@@ -257,6 +265,11 @@ static rs_status_t rs_sdio_send_app_command(rs_sdio_instance_t instance, uint16_
rs_status_t rs_sdio_probe(rs_sdio_instance_t instance, uint32_t *ip_id, uint32_t *version,
uint32_t *capability) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
if (!rs_sdio_instance_valid(instance) || (ip_id == NULL) || (version == NULL) ||
(capability == NULL) || (rs_sdio_read(instance, RS_SDIO_REG_IP_ID, ip_id) != RS_OK) ||
(rs_sdio_read(instance, RS_SDIO_REG_IP_VERSION, version) != RS_OK) ||
@@ -270,6 +283,11 @@ rs_status_t rs_sdio_probe(rs_sdio_instance_t instance, uint32_t *ip_id, uint32_t
}
rs_status_t rs_sdio_reset(rs_sdio_instance_t instance, rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
volatile uint32_t *status_reg;
if (!rs_sdio_instance_valid(instance)) {
@@ -294,6 +312,11 @@ rs_status_t rs_sdio_reset(rs_sdio_instance_t instance, rs_timeout_t timeout) {
rs_status_t rs_sdio_clock_set(rs_sdio_instance_t instance, uint32_t source_clock_hz,
uint32_t target_clock_hz, rs_sdio_clock_t *clock) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
rs_sdio_clock_t calculated;
uint32_t value;
uint32_t enabled;
@@ -318,6 +341,11 @@ rs_status_t rs_sdio_clock_set(rs_sdio_instance_t instance, uint32_t source_clock
}
rs_status_t rs_sdio_bus_width_set(rs_sdio_instance_t instance, rs_sdio_bus_width_t width) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
uint32_t value;
if (!rs_sdio_instance_valid(instance) || rs_sdio_busy(instance)) {
@@ -338,6 +366,11 @@ rs_status_t rs_sdio_bus_width_set(rs_sdio_instance_t instance, rs_sdio_bus_width
rs_status_t rs_sdio_timeouts_set(rs_sdio_instance_t instance, uint32_t command_timeout,
uint32_t data_timeout, uint32_t busy_timeout) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
if (!rs_sdio_instance_valid(instance) || rs_sdio_busy(instance) || (command_timeout == 0U) ||
(data_timeout == 0U) || (busy_timeout == 0U)) {
return RS_EINVAL;
@@ -351,6 +384,11 @@ rs_status_t rs_sdio_timeouts_set(rs_sdio_instance_t instance, uint32_t command_t
}
rs_status_t rs_sdio_enable(rs_sdio_instance_t instance, bool enable) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
uint32_t value;
if (!rs_sdio_instance_valid(instance) ||
@@ -366,6 +404,11 @@ rs_status_t rs_sdio_enable(rs_sdio_instance_t instance, bool enable) {
}
rs_status_t rs_sdio_configure(rs_sdio_instance_t instance, const rs_sdio_config_t *config) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
rs_sdio_clock_t clock;
rs_status_t status;
@@ -393,6 +436,11 @@ rs_status_t rs_sdio_configure(rs_sdio_instance_t instance, const rs_sdio_config_
}
rs_status_t rs_sdio_status_get(rs_sdio_instance_t instance, rs_sdio_status_t *status) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
uint32_t present;
if (!rs_sdio_instance_valid(instance) || (status == NULL)) {
@@ -421,6 +469,11 @@ rs_status_t rs_sdio_status_get(rs_sdio_instance_t instance, rs_sdio_status_t *st
}
rs_status_t rs_sdio_command_start(rs_sdio_instance_t instance, const rs_sdio_command_t *command) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
uint32_t cfg;
if (!rs_sdio_instance_valid(instance) || (command == NULL) || (command->index > 63U) ||
@@ -449,6 +502,11 @@ rs_status_t rs_sdio_command_start(rs_sdio_instance_t instance, const rs_sdio_com
}
rs_status_t rs_sdio_response_get(rs_sdio_instance_t instance, rs_sdio_response_t *response) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
if (!rs_sdio_instance_valid(instance) || (response == NULL) ||
(rs_sdio_read(instance, RS_SDIO_REG_RESP0, &response->words[0]) != RS_OK) ||
(rs_sdio_read(instance, RS_SDIO_REG_RESP1, &response->words[1]) != RS_OK) ||
@@ -462,6 +520,11 @@ rs_status_t rs_sdio_response_get(rs_sdio_instance_t instance, rs_sdio_response_t
rs_status_t rs_sdio_command_wait(rs_sdio_instance_t instance, rs_sdio_response_t *response,
rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
rs_status_t status;
if (!rs_sdio_instance_valid(instance) || (response == NULL)) {
@@ -485,6 +548,11 @@ rs_status_t rs_sdio_command_wait(rs_sdio_instance_t instance, rs_sdio_response_t
rs_status_t rs_sdio_command_execute(rs_sdio_instance_t instance, const rs_sdio_command_t *command,
rs_sdio_response_t *response, rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
rs_status_t status;
status = rs_sdio_command_start(instance, command);
@@ -500,6 +568,11 @@ rs_status_t rs_sdio_command_execute(rs_sdio_instance_t instance, const rs_sdio_c
rs_status_t rs_sdio_data_configure(rs_sdio_instance_t instance,
const rs_sdio_data_config_t *config) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
uint32_t value;
if (!rs_sdio_instance_valid(instance) || (config == NULL) || (config->block_size == 0U) ||
@@ -532,6 +605,11 @@ rs_status_t rs_sdio_data_configure(rs_sdio_instance_t instance,
}
rs_status_t rs_sdio_data_start(rs_sdio_instance_t instance) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
if (!rs_sdio_instance_valid(instance) || rs_sdio_busy(instance) ||
(rs_sdio_irq_clear(instance, RS_SDIO_IRQ_DATA_DONE | RS_SDIO_IRQ_DATA_ERROR) != RS_OK) ||
(rs_sdio_error_clear(instance, RS_SDIO_ERROR_DATA_CRC | RS_SDIO_ERROR_DATA_TIMEOUT |
@@ -582,6 +660,11 @@ static rs_status_t rs_sdio_pio_store(rs_sdio_instance_t instance, const uint8_t
rs_status_t rs_sdio_pio_write(rs_sdio_instance_t instance, const void *buffer, size_t byte_count,
rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
const uint8_t *bytes = (const uint8_t *)buffer;
rs_status_t status;
size_t offset = 0U;
@@ -612,6 +695,11 @@ rs_status_t rs_sdio_pio_write(rs_sdio_instance_t instance, const void *buffer, s
rs_status_t rs_sdio_pio_read(rs_sdio_instance_t instance, void *buffer, size_t byte_count,
rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
uint8_t *bytes = (uint8_t *)buffer;
size_t offset = 0U;
@@ -645,6 +733,11 @@ rs_status_t rs_sdio_pio_read(rs_sdio_instance_t instance, void *buffer, size_t b
}
rs_status_t rs_sdio_data_wait(rs_sdio_instance_t instance, rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
rs_status_t status;
if (!rs_sdio_instance_valid(instance)) {
@@ -664,6 +757,11 @@ rs_status_t rs_sdio_data_wait(rs_sdio_instance_t instance, rs_timeout_t timeout)
rs_status_t rs_sdio_dma_setup(rs_sdio_instance_t instance, rs_sdio_descriptor_t *descriptors,
uint16_t count, uint32_t total_bytes) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
uintptr_t address;
if (!rs_sdio_instance_valid(instance) || (total_bytes == 0U) ||
@@ -686,6 +784,11 @@ rs_status_t rs_sdio_dma_setup(rs_sdio_instance_t instance, rs_sdio_descriptor_t
}
rs_status_t rs_sdio_dma_start(rs_sdio_instance_t instance) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
/*
* DATA_START is the coordinated DMA/data-engine launch. Keep this
* compatibility entry point as an alias so software cannot start a DMA
@@ -695,6 +798,11 @@ rs_status_t rs_sdio_dma_start(rs_sdio_instance_t instance) {
}
rs_status_t rs_sdio_dma_abort(rs_sdio_instance_t instance) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
if (!rs_sdio_instance_valid(instance)) {
return RS_EINVAL;
}
@@ -702,6 +810,11 @@ rs_status_t rs_sdio_dma_abort(rs_sdio_instance_t instance) {
}
rs_status_t rs_sdio_dma_status_get(rs_sdio_instance_t instance, rs_sdio_dma_status_t *status) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
uint32_t dma_error;
if (!rs_sdio_instance_valid(instance) || (status == NULL) ||
@@ -720,6 +833,11 @@ rs_status_t rs_sdio_dma_status_get(rs_sdio_instance_t instance, rs_sdio_dma_stat
}
rs_status_t rs_sdio_dma_wait(rs_sdio_instance_t instance, rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
rs_status_t status;
if (!rs_sdio_instance_valid(instance)) {
@@ -738,6 +856,11 @@ rs_status_t rs_sdio_dma_wait(rs_sdio_instance_t instance, rs_timeout_t timeout)
}
rs_status_t rs_sdio_irq_enable(rs_sdio_instance_t instance, uint32_t mask) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
if (!rs_sdio_instance_valid(instance) || ((mask & ~RS_SDIO_IRQ_ALL) != 0U)) {
return RS_EINVAL;
}
@@ -745,6 +868,11 @@ rs_status_t rs_sdio_irq_enable(rs_sdio_instance_t instance, uint32_t mask) {
}
rs_status_t rs_sdio_irq_pending(rs_sdio_instance_t instance, uint32_t *mask) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
if (!rs_sdio_instance_valid(instance) || (mask == NULL)) {
return RS_EINVAL;
}
@@ -752,6 +880,11 @@ rs_status_t rs_sdio_irq_pending(rs_sdio_instance_t instance, uint32_t *mask) {
}
rs_status_t rs_sdio_irq_clear(rs_sdio_instance_t instance, uint32_t mask) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
if (!rs_sdio_instance_valid(instance) || ((mask & ~RS_SDIO_IRQ_ALL) != 0U)) {
return RS_EINVAL;
}
@@ -759,6 +892,11 @@ rs_status_t rs_sdio_irq_clear(rs_sdio_instance_t instance, uint32_t mask) {
}
rs_status_t rs_sdio_irq_test(rs_sdio_instance_t instance, uint32_t mask) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
if (!rs_sdio_instance_valid(instance) || ((mask & ~RS_SDIO_IRQ_ALL) != 0U)) {
return RS_EINVAL;
}
@@ -766,6 +904,11 @@ rs_status_t rs_sdio_irq_test(rs_sdio_instance_t instance, uint32_t mask) {
}
rs_status_t rs_sdio_error_clear(rs_sdio_instance_t instance, uint32_t mask) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
if (!rs_sdio_instance_valid(instance) || ((mask & ~RS_SDIO_ERROR_ALL) != 0U)) {
return RS_EINVAL;
}
@@ -773,6 +916,11 @@ rs_status_t rs_sdio_error_clear(rs_sdio_instance_t instance, uint32_t mask) {
}
rs_status_t rs_sdio_controller_selftest(rs_sdio_instance_t instance) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
rs_sdio_status_t status;
uint32_t ip_id;
uint32_t version;
@@ -916,7 +1064,9 @@ static rs_status_t rs_sd_memory_try_high_speed(rs_sdio_instance_t instance,
return RS_EIO;
}
if (rs_sdio_clock_set(instance, source_clock_hz,
- (source_clock_hz >= 100000000U) ? 50000000U : 36000000U,
+ rs_sdio_transfer_clock(source_clock_hz, (source_clock_hz >= 100000000U)
+ ? 50000000U
+ : 36000000U),
&clock) == RS_OK) {
info->actual_clock_hz = clock.actual_hz;
info->high_speed = true;
@@ -928,6 +1078,11 @@ static rs_status_t rs_sd_memory_try_high_speed(rs_sdio_instance_t instance,
rs_status_t rs_sd_memory_initialize(rs_sdio_instance_t instance, uint32_t source_clock_hz,
rs_sd_memory_info_t *info, rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
rs_sdio_response_t response;
rs_sdio_clock_t clock;
uint32_t payload;
@@ -1008,7 +1163,9 @@ rs_status_t rs_sd_memory_initialize(rs_sdio_instance_t instance, uint32_t source
if (status != RS_OK) {
return status;
}
- if ((rs_sdio_response_field(&response, 31U, 16U, &payload) != RS_OK) ||
+ /* RESP retains the complete 48-bit frame: the R6 payload is [39:8],
+ * so its upper sixteen RCA bits are [39:24], above the status/CRC. */
+ if ((rs_sdio_response_field(&response, 39U, 24U, &payload) != RS_OK) ||
(payload > UINT16_MAX)) {
return RS_EFORMAT;
}
@@ -1046,7 +1203,8 @@ rs_status_t rs_sd_memory_initialize(rs_sdio_instance_t instance, uint32_t source
info->bus_width_fallback = true;
info->bus_width = RS_SDIO_BUS_WIDTH_1;
}
- status = rs_sdio_clock_set(instance, source_clock_hz, 36000000U, &clock);
+ status = rs_sdio_clock_set(instance, source_clock_hz,
+ rs_sdio_transfer_clock(source_clock_hz, 36000000U), &clock);
if (status != RS_OK) {
return status;
}
@@ -1150,12 +1308,22 @@ static rs_status_t rs_sd_memory_transfer(rs_sdio_instance_t instance,
rs_status_t rs_sd_memory_read_blocks(rs_sdio_instance_t instance, const rs_sd_memory_info_t *info,
uint32_t block, uint32_t count, void *buffer,
rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
return rs_sd_memory_transfer(instance, info, block, count, buffer, false, timeout);
}
rs_status_t rs_sd_memory_write_blocks(rs_sdio_instance_t instance, const rs_sd_memory_info_t *info,
uint32_t block, uint32_t count, const void *buffer,
rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
return rs_sd_memory_transfer(instance, info, block, count, (void *)buffer, true, timeout);
}
@@ -1175,6 +1343,11 @@ static rs_status_t rs_sdio_function_command(rs_sdio_instance_t instance, uint8_t
rs_status_t rs_sdio_function_cmd52_read(rs_sdio_instance_t instance, uint8_t function,
uint32_t address, uint8_t *data, rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
rs_sdio_cmd52_t command = {
.function = function,
.address = address,
@@ -1203,6 +1376,11 @@ rs_status_t rs_sdio_function_cmd52_read(rs_sdio_instance_t instance, uint8_t fun
rs_status_t rs_sdio_function_cmd52_write(rs_sdio_instance_t instance, uint8_t function,
uint32_t address, uint8_t data, rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
const rs_sdio_cmd52_t command = {
.function = function,
.address = address,
@@ -1223,6 +1401,11 @@ rs_status_t rs_sdio_function_cmd52_write(rs_sdio_instance_t instance, uint8_t fu
rs_status_t rs_sdio_function_initialize(rs_sdio_instance_t instance, uint32_t source_clock_hz,
rs_sdio_function_info_t *info, rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
rs_sdio_response_t response;
rs_sdio_clock_t clock;
uint32_t payload;
@@ -1345,8 +1528,10 @@ rs_status_t rs_sdio_function_initialize(rs_sdio_instance_t instance, uint32_t so
} else {
info->high_speed_fallback = true;
}
- status = rs_sdio_clock_set(instance, source_clock_hz,
- high_speed_enabled ? 24000000U : 25000000U, &clock);
+ status = rs_sdio_clock_set(
+ instance, source_clock_hz,
+ rs_sdio_transfer_clock(source_clock_hz, high_speed_enabled ? 24000000U : 25000000U),
+ &clock);
if (status != RS_OK) {
return status;
}
@@ -1356,6 +1541,11 @@ rs_status_t rs_sdio_function_initialize(rs_sdio_instance_t instance, uint32_t so
rs_status_t rs_sdio_function_enable(rs_sdio_instance_t instance, rs_sdio_function_info_t *info,
uint8_t function, rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
uint8_t enable;
uint8_t ready;
uint32_t attempts;
@@ -1394,6 +1584,11 @@ rs_status_t rs_sdio_function_cmd53_transfer(rs_sdio_instance_t instance, uint8_t
uint32_t address, void *buffer, size_t byte_count,
bool write, bool block_mode, bool fixed_address,
uint16_t block_size, rs_timeout_t timeout) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
rs_sdio_cmd53_t command;
rs_sdio_data_config_t data_config;
rs_sdio_response_t response;
@@ -1482,6 +1677,11 @@ rs_status_t rs_sdio_function_cmd53_transfer(rs_sdio_instance_t instance, uint8_t
}
rs_status_t rs_sdio_function_irq_enable(rs_sdio_instance_t instance, bool enable) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
uint32_t mask;
if (!rs_sdio_instance_valid(instance) ||
@@ -1501,6 +1701,11 @@ rs_status_t rs_sdio_function_irq_enable(rs_sdio_instance_t instance, bool enable
}
rs_status_t rs_sdio_function_irq_pending(rs_sdio_instance_t instance, bool *pending) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
uint32_t mask;
if ((pending == NULL) || (rs_sdio_irq_pending(instance, &mask) != RS_OK)) {
@@ -1511,5 +1716,10 @@ rs_status_t rs_sdio_function_irq_pending(rs_sdio_instance_t instance, bool *pend
}
rs_status_t rs_sdio_function_irq_ack(rs_sdio_instance_t instance) {
+#ifdef RS_SOC_TINY
+ if (instance == RS_SDIO_INSTANCE_1) {
+ return RS_ENOTSUP;
+ }
+#endif
return rs_sdio_irq_clear(instance, RS_SDIO_IRQ_DAT1);
}
diff --git a/crt/src/hal/sdio_math.c b/crt/src/hal/sdio_math.c
index 997c63d8..37bf7263 100644
--- a/crt/src/hal/sdio_math.c
+++ b/crt/src/hal/sdio_math.c
@@ -136,7 +136,9 @@ rs_status_t rs_sd_memory_parse_csd(const rs_sdio_response_t *response, rs_sd_mem
return RS_EINVAL;
}
block_length = UINT32_C(1) << read_bl_len;
- capacity_bytes = ((uint64_t)c_size + UINT64_C(1)) * (UINT64_C(1) << (c_size_mult + 2U)) *
+ /* C_SIZE_MULT is a three-bit CSD field: the shift is at most nine.
+ * Keep the factor RV32-native; the enclosing product remains 64-bit. */
+ capacity_bytes = ((uint64_t)c_size + UINT64_C(1)) * (UINT32_C(1) << (c_size_mult + 2U)) *
(uint64_t)block_length;
capacity_blocks = capacity_bytes / (uint64_t)RS_SD_MEMORY_BLOCK_SIZE;
info->card_type = RS_SD_MEMORY_SDSC;
diff --git a/crt/src/hal/uart.c b/crt/src/hal/uart.c
index 88f92081..16035267 100644
--- a/crt/src/hal/uart.c
+++ b/crt/src/hal/uart.c
@@ -84,6 +84,12 @@ static rs_status_t rs_uart_flow_gpio_configure(const rs_uart_config_t *config) {
};
rs_status_t status = RS_OK;
+#ifdef RS_SOC_TINY
+ status = rs_gpio_configure(8U, &pin_config);
+ if (status == RS_OK) {
+ status = rs_gpio_configure(9U, &pin_config);
+ }
+#endif
if (config->auto_cts_enable) {
status = rs_gpio_configure(RS_UART_CTS_GPIO, &pin_config);
}
@@ -106,6 +112,11 @@ rs_status_t rs_uart_configure(const rs_uart_config_t *config, rs_timeout_t timeo
return RS_EINVAL;
}
+#ifdef RS_SOC_TINY
+ if (config->auto_cts_enable || config->auto_rts_enable) {
+ return RS_ENOTSUP;
+ }
+#endif
RS_UART_CTRL = 0U;
while (((RS_UART_STATUS & (RS_UART_STATUS_TX_BUSY | RS_UART_STATUS_RX_BUSY)) != 0U) &&
(timeout != 0U)) {
diff --git a/crt/src/hal/ws2812.c b/crt/src/hal/ws2812.c
index 47c5cf94..6fd811a5 100644
--- a/crt/src/hal/ws2812.c
+++ b/crt/src/hal/ws2812.c
@@ -419,7 +419,11 @@ rs_status_t rs_ws2812_dma_service(uint64_t now) {
return rs_ws2812_dma_finish();
}
if ((transmitter_flags & RS_WS2812_STATUS_BUSY) == 0U) {
- return rs_ws2812_dma_fail(RS_EIO);
+ /* Completion may occur between the interrupt and status reads, and
+ * sticky DONE can lag the core's idle transition. Observe terminal
+ * events on the next bounded service call; never refill an idle core.
+ * Errors, aborts and the deadline remain checked on every call. */
+ return RS_OK;
}
if (rs_ws2812_transfer.remaining == 0U) {
rs_ws2812_write_register(RS_WS2812_INTR_STATE_OFFSET, RS_WS2812_INTR_FIFO_LOW);
diff --git a/dependencies/dependencies.lock.json b/dependencies/dependencies.lock.json
index 29b7957f..37b9ebc3 100644
--- a/dependencies/dependencies.lock.json
+++ b/dependencies/dependencies.lock.json
@@ -158,7 +158,7 @@
"cluster_common": {"url": "https://github.com/retroSoC/common.git", "revision": "964a54d70cb78e394ba9a25c05f2aa2b02941fb7", "destination": "rtl/managed/clusterip/common", "license": "NOASSERTION"},
"cluster_crc": {"url": "https://github.com/retroSoC/crc.git", "revision": "8c1a5664c4d23d005c1afad432372a13ca967cc8", "destination": "rtl/managed/clusterip/crc", "license": "MulanPSL-2.0"},
"cluster_ps2": {"url": "https://github.com/retroSoC/ps2.git", "revision": "2097a0d41b07573ff0453d65ba9a1670b3df7b9c", "destination": "rtl/managed/clusterip/ps2", "license": "MulanPSL-2.0"},
- "cluster_pwm": {"url": "https://github.com/retroSoC/pwm.git", "revision": "79ae32f725e848a1e7f6134847b3fb032ed64006", "destination": "rtl/managed/clusterip/pwm", "license": "MulanPSL-2.0"},
+ "cluster_pwm": {"url": "https://github.com/retroSoC/pwm.git", "revision": "38efaf5e20f084a00148920087c60cadab61839b", "destination": "rtl/managed/clusterip/pwm", "license": "MulanPSL-2.0"},
"cluster_rng": {"url": "https://github.com/retroSoC/rng.git", "revision": "36f3e0774d8f3c4b597d3db8a96ca3992532b61e", "destination": "rtl/managed/clusterip/rng", "license": "MulanPSL-2.0"},
"cluster_rtc": {"url": "https://github.com/retroSoC/rtc.git", "revision": "32548c5cbcdf39e09d0880fb611f9dd47940ea88", "destination": "rtl/managed/clusterip/rtc", "license": "NOASSERTION"},
"cluster_wdg": {"url": "https://github.com/retroSoC/wdg.git", "revision": "f5d39fa246054d678d34195824d943367dfcce44", "destination": "rtl/managed/clusterip/wdg", "license": "MulanPSL-2.0"}
diff --git a/docs/engineering.md b/docs/engineering.md
index 9c7bb55c..41a3ed59 100644
--- a/docs/engineering.md
+++ b/docs/engineering.md
@@ -3,8 +3,10 @@
## Supported Profiles
Tiny's [2026-10-07 specification default](ip/tiny-soc.md#ics55-default-platform-and-final-timing-gate-2026-10-07)
-is ICS55/PLL with SAFE24 boot. The executable default keeps the PLL parked off;
-PLL-driven SYS and physical qualification remain later work. The table
+is ICS55/PLL with SAFE24 boot. The PLL starts parked off; the
+[R2-P7 implementation](ip/tiny-soc-r2-clock-reset.md) adds explicit software
+transitions with separate functional evidence. Physical qualification remains
+pending. The table
describes executable profiles, not qualified operating rates.
For Tiny and its SPI/PIO-lite/PPALite extensions only, pre-final post-synthesis
timing closure is observational. Retain STA attempts/failures and all required
diff --git a/docs/ip/i2s.md b/docs/ip/i2s.md
index cf3dbbcc..25d3758d 100644
--- a/docs/ip/i2s.md
+++ b/docs/ip/i2s.md
@@ -105,6 +105,15 @@ change.
in `[23:0]`. `TKEEP` and `TSTRB` are `4'hF`. I2S RX does not assert `TLAST`.
DMA asserts `TLAST` on the final programmed TX word.
+The P6 integration regression uses alternating sample least-significant bits
+in both 16- and 24-bit pin-level loopback. It exposed a receiver defect that
+combined the preceding sample's last bit with the next sample. The corrected
+receiver appends the bit sampled at the LRCK transition to the preceding
+sample's shifted bits and registers completion after that value is available.
+This preserves the host register/stream ABI and does not add slave mode.
+The register frontend uses PWRITE for transfer direction. In particular, a
+write with zero byte strobes cannot be reinterpreted as a side-effecting RX read.
+
## HAL Sequence
Mini applications should use `rs_i2s_configure()`, `rs_i2s_enable()`, and either
diff --git a/docs/ip/sdio.md b/docs/ip/sdio.md
index 0365806b..208b0d36 100644
--- a/docs/ip/sdio.md
+++ b/docs/ip/sdio.md
@@ -180,3 +180,47 @@ The standalone IP does not allocate memory, perform cache maintenance,
implement an IOMMU/firewall, or provide a hosted/runtime C API. The SoC
integration only supplies the two native AXI/APB/IRQ/pad paths; the SDK driver
is maintained by a separate workstream.
+
+## Tiny SAFE24 integration
+
+Tiny R2-P6 connects instance 0 to external AXI owner 2, its independent APB
+branch and IRQ10, and GPIO0..5. Instance 1 remains absent and its public SDK
+operations return `RS_ENOTSUP` before MMIO. These bindings do not replace the
+Mini mappings above. The 24 MHz input permits at most 12 MHz SDCLK; enumeration
+still starts at 400 kHz and the SDK caps later protocol requests at input/2.
+
+R6 is retained as the complete 48-bit frame in RESP. RCA therefore occupies
+bits 39:24, above the payload status and CRC/end bits. The shared initialization
+helper uses this field rather than bits 31:16; no register or public API changes.
+
+The command sender enables its first output bit at its scheduled launch. A
+command admitted on an already running SDCLK rising edge must not expose an
+extra start bit. Directed pad sampling covers repeated command admission at
+both half-cycle phases in Icarus and Verilator. The card model collects the
+complete 48-bit frame after its start bit, including the pulled-high end bit
+after host output release; host output-enable is not the frame length oracle.
+
+The transmit wait indication also covers the last bit of a word when its
+successor is unavailable. The core pauses SDCLK before the next word's launch,
+while retaining the current driven bit and draining the high half-cycle. This
+prevents an idle bit from becoming the next word's MSB under memory backpressure.
+A 1024-byte four-lane DMA case inserts 24 system cycles between read beats and
+checks every card byte, CRC status and descriptor retirement through real
+three-state lines. Inactive lanes stay pulled high in the card's token/tail checks.
+
+Card-to-memory DMA retains descriptor ownership across every AXI burst, rather
+than publishing DONE after the first 64 bytes. Receive backpressure stops the
+next SDCLK launch as soon as a complete word is held; a registered stop loses
+the next card bit. The roundtrip fixture delays descriptor fetch as well as
+payload supply, checks all 1024 received bytes and successful final writeback.
+Its card emits all sixteen CRC bits in order; earlier manual-model tests did
+not reject the skipped CRC bit. These fixes preserve the native one-read/one-write
+capacity, descriptor layout, register ABI and interrupt/error lifecycle.
+
+The existing verification card keeps its manually scheduled standalone mode.
+Tiny selects its optional SDHC enumeration responses with 1024 512-byte blocks
+and default-speed-only capability. Its board fixture checks the observed wire
+command, direction, width and length, then calls the existing data-stimulus API
+at DATA_START. All payload and response bits cross the real pads. This checks
+the repository controller/SDK contract; it does not qualify arbitrary card
+response timing, SD protocol compliance, a physical card or an electrical rate.
diff --git a/docs/ip/tiny-soc-r2-clock-reset.md b/docs/ip/tiny-soc-r2-clock-reset.md
new file mode 100644
index 00000000..6fb820a5
--- /dev/null
+++ b/docs/ip/tiny-soc-r2-clock-reset.md
@@ -0,0 +1,385 @@
+# Tiny R2-P7 clock/reset implementation and evidence
+
+This phase starts at `43a3c8d453b6d41df3536719ebc6d1f4521bc7cd`, against frozen
+specification blob `7e19de49d49efaafacccfa85671e314d53dd15a4`. Implementation
+is authorized; phase acceptance still requires review of the completed gates.
+No P8 implementation, commit, push, or physical frequency qualification follows.
+
+## Product and ownership
+
+`configs/ci/ics55-tiny.mk` remains the default: TINY, ICS55, PLL present,
+SAFE24 boot. The single RCU-owned PLL starts off. Software may request the
+functional PLL192/PLL240 profiles after draining clients. These are digital
+simulation capabilities, not characterized silicon operating points.
+`configs/ci/ihp130-tiny.mk` remains the explicit no-PLL 24 MHz compatibility
+profile. `ihp130-tiny-xin48.mk` and `ihp130-tiny-xin96.mk` add declared external
+input variants, both normalizing the boot/control reference to 24 MHz.
+
+CPU, all 32 main SRAM macros, bank frontends, fabric and SDIO share active SYS.
+Only the CPU leaf stops for WFI. XPI data/control use MEM. DMA, shared serial
+and security IP and the six private Crypto macros use PCLK. Crypto capacity
+and erasure policy are unchanged. AON owns RCU, CLINT and RTC/WDG functional
+timekeeping; AUDIO is optional at boot. The historical ASIC wrapper and frozen
+QFN64/pad contract are retained under the existing P6 qualification boundary.
+
+Tiny owns the clock backend, admission holders, APB/AXI crossings, peripheral
+lifecycle controller, CPU gate, CLKOUT and SYSCTRL ABI. Common clock mux,
+divider, reset barrier, synchronizers and CDC primitives are reused without
+editing managed sources. New optional shared-IP lifecycle outputs preserve
+Mini defaults; the debug wrapper's separate system reset is opt-in. Managed
+PWM/CRC lifecycle mirrors observe successful writes at their existing ABI.
+The already approved PWM lock remains
+`38efaf5e20f084a00148920087c60cadab61839b`.
+
+RCU occupies `SYSCTRL+0x100..0x15c`, IRQ31. Handwritten SVH/C definitions
+have a 24-register parity test. `rcu.h` supplies bounded Tiny-specific profile,
+snapshot, gate, local/system reset, cause/fault and CLKOUT operations. Invalid,
+unsupported, busy, timeout and hardware-error results are distinct. No new
+MISRA Required-rule deviation is introduced. Future targets 14–17 remain
+absent; no SPI, PIO-lite, DVP, PPALite or PSRAM rollout is included.
+
+## Lifecycle and failure boundaries
+
+Clock requests block new admissions after command acceptance, drain accepted
+CPU/local/AXI/APB traffic, park on REF24, configure conservative divisors,
+qualify the source, then commit rate snapshots before unblocking. Stalled
+addresses already accepted into admission holders retain VALID/payload.
+Unaccepted requests may remain pending and resume after switching. An
+insufficient transition budget fails before changing the committed profile.
+PLL lock timeout publishes the complete SAFE24 fallback. PLL stuck-low/high
+requires a system/CDC reset before forced mux recovery; XIN loss needs source
+restoration and external reset.
+
+Local resets hold for five actual leaf edges and release through five-edge
+synchronizers and bus barriers. I2S additionally requires actual AUDIO-side
+reset readiness; missing AUDIO cannot manufacture RESET_DONE. Gate changes
+wait for idle; CPU-only WFI permission does not pause unrelated DMA. Current
+non-CPU lifecycle commands conservatively drain all central-DMA work before
+changing the selected targets. This can reject a request while an unrelated
+DMA stream remains active; it does not stop that stream by gating its clock.
+Warm reset retains cause/fault/debug state, aborts interrupted debug commands,
+restores SAFE24, resets functional RTC/WDG state and preserves main SRAM data.
+
+The 2026-10-14 P7 review correction keeps the XPI fabric target and AXI bridge
+source endpoint on SYS reset while the XPI/MEM endpoint uses its MEM leaf reset.
+The later response-conservation correction uses source-owned response holders:
+already presented B/R responses remain stable and retire once, while missing
+read beats receive DECERR and aborted writes drain W before B. Admission allows
+one outstanding request in each direction. Payload source reset uses the
+five-edge link barrier; the response holders reset only with the source.
+An unfinished warm clear holds isolation until destination reset so that late
+responses cannot enter another transaction. Production DMA/XPI clear inputs
+remain tied low. A failed drain does not trigger an invented autonomous AXI
+timeout or peripheral reset. The focused tests now cover these protocol
+boundaries in both simulators; earlier 36-case results did not establish them.
+
+Source-only reset has a separate production precondition. The DMA source
+may reset locally only after its engine and accepted admission/fabric work
+drain. The controller blocks new admissions, waits at least 16 REF24 cycles
+for the synchronized observations, and leaves the source running on drain
+timeout. XPI has no independent SYS-source reset: the common system reset
+also asserts its MEM endpoint reset. The standalone AXI bridge does not
+cancel a live remote slave's work on an arbitrary source-only reset and must
+not be reused with that assumption.
+
+The focused `tiny_p7_source_reset_tb` extracts the production controller,
+admission, bridge, synchronizers and reset bindings into its fixture and uses
+the real DMA AXI master. It checks stalled addresses, partial writes, late
+responses, held read responses, timeout, recovery and common reset at all five
+declared domain ratios. A scoreboard independent of local reset detects the
+deliberately broken-drain negative case. Unrelated clients and the completion
+mailbox are idle; the target is a behavioral slave. This supplies executable
+integration-contract coverage, not a general arbitrary-reset proof or physical
+qualification. The new evidence is under `06-00.../meta/tiny-r2-p7-source-reset/`
+as detailed in the verification ledger; production RTL and backend inputs are
+unchanged by this follow-up.
+
+The follow-up validation commands are:
+
+```sh
+source .cache/retrosoc/development/tiny-r2-p1/activate.sh
+p7_reset_evidence=build/ics55-tiny-2026-10-14-06-00-ff4b5cef6eb5/meta/tiny-r2-p7-source-reset
+PYTHONDONTWRITEBYTECODE=1 python3 scripts/run_flow.py --tool pytest \
+ --log "$p7_reset_evidence/directed.log" \
+ --result "$p7_reset_evidence/result-directed.json" -- \
+ python3 -m pytest -q tests/test_tiny_r2_p7.py \
+ --basetemp="$p7_reset_evidence/directed-tests"
+make --silent CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 \
+ rtl-format-check rtl-style-check-all check-clock-reset-domains
+ruff check .
+python3 scripts/regress.py --root . --suite pr --soc TINY --pdk ICS55 --dry-run
+python3 scripts/regress.py --root . --suite pr --soc TINY --pdk IHP130 --dry-run
+git diff --check
+```
+
+These paths identify the retained execution, not an instruction to overwrite
+it: use a fresh evidence directory for any retry. `source.json` binds the new
+tests and `verification-update.json` compares them to the original `06-00`
+snapshot. Only the two verification files differ; all 1728 other technical
+inputs and tool identities match. No firmware, full-SoC, synthesis, netlist or
+STA flow is repeated for this verification-only correction. The earlier
+full-suite Pytest and full-Icarus limitations below remain explicit.
+
+The first post-fix physical smoke campaign below is historical. Each PDK has
+its own profile/configuration digest. These results precede the final response
+holders; current source evidence is recorded in the verification ledger.
+
+| PDK | Variant | Synthesis | Netlist boot | STA |
+| --- | --- | --- | --- | --- |
+| ICS55 | `build/ics55-tiny-2026-10-14-01-20-ff4b5cef6eb5` | PASS, 2122.685 s | PASS, 1656.024 s, `SIM_TEST_PASS Tiny netlist boot` | PASS observation; WNS max/min `-144.40/-0.31 ns`, TNS max/min `-4859636.50/-45.04` |
+| IHP130 | `build/ihp130-tiny-2026-10-14-01-20-8232fa11e7f3` | PASS, 1925.337 s | PASS, 586.861 s, `SIM_TEST_PASS Tiny netlist boot` | PASS observation; WNS max/min `-257.81/-0.47 ns`, TNS max/min `-9807803.00/-100.68` |
+
+The current frozen-source `06-00` campaign supersedes the historical table for
+post-fix qualification:
+
+| PDK | Variant | Synthesis | Netlist boot | STA |
+| --- | --- | --- | --- | --- |
+| ICS55 | `build/ics55-tiny-2026-10-14-06-00-ff4b5cef6eb5` | PASS, 2108.388 s | PASS, 1655.980 s, `SIM_TEST_PASS Tiny netlist boot` | PASS observation; WNS max/min `-152.03/-0.31 ns`, TNS max/min `-5622052.50/-44.14` |
+| IHP130 | `build/ihp130-tiny-2026-10-14-06-00-8232fa11e7f3` | PASS, 1934.515 s | PASS, 578.869 s, `SIM_TEST_PASS Tiny netlist boot` | PASS observation; WNS max/min `-253.43/-0.49 ns`, TNS max/min `-10035324.00/-153.37` |
+
+The STA markers explicitly identify SAFE24/observational analysis; neither
+variant qualifies an active PLL, routed implementation, PVT corner or product
+operating point. The `04-00` source candidate was later rejected by the
+response-conservation tests. The `06-00` source-bound post-fix campaign is the
+current qualification run and all requested synthesis, netlist and STA gates
+pass. Its measured results are recorded in the ledger. Full firmware Icarus
+remains deferred to the final Tiny phase.
+
+Source/divisor/enable for CLKOUT are committed together, cross to the selected
+source, and finish the current output high pulse before disabling. GPIO28 ALT0
+and GPIO23 ALT1 consume the same generator. Illegal/live changes return
+PSLVERR and leave the previous configuration unchanged.
+
+## Validation sequence and commands
+
+Activate the locked environment before these commands:
+
+```sh
+source .cache/retrosoc/development/tiny-r2-p1/activate.sh
+python3 scripts/regress.py --root . --suite pr --soc TINY --pdk ICS55 --dry-run
+python3 scripts/regress.py --root . --suite pr --soc TINY --pdk IHP130 --dry-run
+python3 -m pytest -q tests/test_tiny_r2_p7.py tests/test_tiny.py tests/test_tiny_r2_p4.py tests/test_tiny_r2_p5.py
+make sw-format-check sw-policy-check sw-host-test
+make rtl-format-check rtl-style-check-all rtl-readiness-check check-clock-reset-domains
+ruff check .
+python3 scripts/dependency_lock.py --lock dependencies/dependencies.lock.json
+python3 -m pytest -q
+git diff --check
+```
+
+The direct long-flow commands use the selected regression's targets with
+larger explicit wall-time budgets. The identical Tiny nightly matrix does not
+need a second execution. Every row below runs once per PDK, with
+`CONFIG=configs/ci/-tiny.mk SOC=TINY PDK= APP=ci_smoke HAVE_SVA=YES`:
+
+| Purpose | BUILD_TIMESTAMP | Remaining Make arguments |
+| --- | --- | --- |
+| P7 Verilator | `2026-10-13-17-05` | `TINY_P7_ACCEPTANCE=YES SIMU=VERILATOR SOC_SIM_TIME=3600 TINY_SIM_TIMEOUT=6000000 TINY_SIM_ARGS=+p7_monitor sim` |
+| P7 Icarus | `2026-10-13-17-06` | `TINY_P7_ACCEPTANCE=YES SIMU=IVERILOG TINY_SIM_TIMEOUT=6000000 TINY_SIM_ARGS=+p7_monitor 'TINY_RUN=timeout --foreground 14400s $(VVP) $(SIM_BUILD_ROOT)/simv' sim` |
+| Physical macro synthesis | `2026-10-13-17-15` | `HAVE_SVA=NO SYNTH=YOSYS YOSYS_TIMEOUT=10800 synth` |
+| Full firmware/JTAG, P7 watchdog-cause adaptation, Verilator | `2026-10-13-18-25` | `SIMU=VERILATOR SOC_SIM_TIME=7200 TINY_SIM_TIMEOUT=30000000 TINY_SIM_ARGS=+tiny_jtag_smoke sim` |
+| Full firmware/JTAG, P7 watchdog-cause adaptation, Icarus | `2026-10-13-18-26` | `SIMU=IVERILOG TINY_SIM_TIMEOUT=30000000 TINY_SIM_ARGS=+tiny_jtag_smoke 'TINY_RUN=timeout --foreground 36000s $(VVP) $(SIM_BUILD_ROOT)/simv' sim` |
+| Netlist boot, after synthesis | `2026-10-13-17-15` | `HAVE_SVA=NO SIMU=IVERILOG netsim-boot` |
+| Observational core STA, after synthesis | `2026-10-13-17-15` | `HAVE_SVA=NO 'STA_BUILD_ROOT=$(VARIANT_ROOT)/sta/opensta-p7-v3' STA=OPENSTA sta` |
+
+Warning/metric collection uses each corresponding variant. New EDA signatures
+remain observations; no warning baseline or metric policy is edited. Compiler
+warning policy and Tiny functional verdicts remain required.
+
+After correcting the external-input POR reference, the actual-SoC P7 image is
+also run with `CONFIG=configs/ci/ihp130-tiny-xin48.mk` and
+`CONFIG=configs/ci/ihp130-tiny-xin96.mk`, `SOC=TINY PDK=IHP130 APP=ci_smoke
+TINY_P7_ACCEPTANCE=YES HAVE_SVA=YES TINY_SIM_ARGS=+p7_monitor
+TINY_SIM_TIMEOUT=24000000`. Verilator uses timestamp `2026-10-13-18-40`,
+`SIMU=VERILATOR SOC_SIM_TIME=7200 sim`; final Icarus uses `2026-10-13-20-05`,
+`SIMU=IVERILOG 'TINY_RUN=timeout --foreground 21600s $(VVP) $(SIM_BUILD_ROOT)/simv' sim`.
+The larger cycle budget counts raw XIN edges, not SYS/REF24 cycles. These are
+additional source configurations, not repetitions of a single benchmark.
+
+The initial XIN48/96 Icarus attempts (`18-41`) produced a premature terminal
+write but failed the independent P7 lifecycle coverage checks, so they remain
+FAIL. A short actual-SoC comparison reproduced unknown RCU cause/fault state
+with the old reset declaration initialized low at time zero. REF24 has no edges
+while its input divider is reset; an explicit external RESET_N assertion at
+1 ns, before the first XIN edge, initializes the asynchronous state correctly.
+Both old-stimulus negative probes fail and both asserted-reset probes pass,
+under `20-05.../meta/cold-reset/`. The testbench now checks exact known cold
+state and rejects an unknown terminal pass/code. This is a verification-stimulus
+repair, with no forced DUT state or synthesizable RTL change. The Verilator
+source cases and existing 24 MHz runs are retained separately; only the two
+failed Icarus source cases are replayed, with the same firmware hashes.
+
+## Workloads and provenance
+
+The separate `TINY_P7_ACCEPTANCE=YES` configuration key selects the short P7
+firmware without replacing the original acceptance application. It exercises
+all supported profile selections for the chosen backend, 128-word SRAM
+retention, atomic rate readback, PWM rate readback, CLINT monotonicity and an
+independent PCLK timer measurement over at least 200 CLINT ticks. The timer
+check allows bounded APB read skew but rejects /2 or /4 domain mistakes.
+It gates/ungates/resets timer0, copies 1024 words through DMA while CPU WFI is
+armed, wakes on CLINT, and requests software reset with a retained `.noinit`
+sentinel. The observer must see actual CPU gated cycles, DMA responses during
+that interval, supported profile commits and exactly one software reset.
+
+Tiny PASS requires command success, TEST_STATUS/SIM_TEST_PASS, and absence of
+FAILED/FATAL/assertion failed/%Error/SIM_TEST_FAIL/SIM_TEST_TIMEOUT. P7 also
+requires `P7_LIFECYCLE_PASS`. The old full firmware additionally retains its
+P6 peripheral/wire/AXI records and JTAG checks. UART output is diagnostic only.
+
+The completed P7 firmware results below match between Verilator and Icarus.
+All eight images use the same retained binary. Cycles count raw XIN edges and
+must not be compared as CPU cycles across different XIN configurations.
+
+| Source/profile | Terminal cycles in each simulator | Actual gated SYS cycles | DMA responses while gated |
+| --- | ---: | ---: | ---: |
+| ICS55 XIN24, PLL192/240/SAFE24 | 3,525,681 | 71,614 | 64 |
+| IHP130 XIN24, external/SAFE24 | 3,521,913 | 71,623 | 64 |
+| IHP130 XIN48, external/SAFE24 | 7,038,466 | 71,635 | 64 |
+| IHP130 XIN96, external/SAFE24 | 14,071,429 | 71,615 | 64 |
+
+Each run also records exactly one software reset and verifies SRAM retention.
+
+The source record is
+`build/ics55-tiny-2026-10-13-17-05-f3e42d54c18b/meta/tiny-r2-p7/source.json`.
+It retains repository state, the exact specification, locked tool identities,
+source hashes, patch and a source archive, including untracked implementation
+files. `scripts/tiny_r2_p7.py verify-source` checks the retained source set.
+The Make formatter subsequently changed only the terminal newline in eight
+Make/configuration files. `make-format-equivalence.json` checks every changed
+file byte-for-byte except that final newline; RTL, firmware, tests and tool
+scripts are identical. `formatted/source.json` retains the final formatting
+snapshot separately, without overwriting the source consumed by running jobs.
+Subsequent validation corrections update the commercial six-domain SAFE24
+adapter/test and connect the top-level POR counter to normalized REF24. The
+external-source fixture now requires five actual REF24 edges at 24/48/96 MHz
+and compares old/new reset-tree outputs at 24 MHz in both simulators (seven
+checks pass). For every retained long-flow configuration, XIN is 24 MHz and
+the backend directly wires XIN to REF24, so that one port reconnection is an
+exact alias substitution. The original source remains the provenance of those
+runs; the documented equivalence and focused higher-input-rate tests cover
+the correction without repeating default-profile simulations.
+Build variants retain effective configuration, generated filelists, firmware
+ELF/bin/hex/map, simulator binaries, command logs and structured terminal
+results. Match binaries by SHA-256 across simulators, and retain flags and
+plusargs with results; do not mix these images with compiler/layout experiments.
+
+The four P7 binaries (both PDKs and both simulators) have SHA-256
+`0a536fbbae9832de7ba5a82f7688a147b96a57435a66249b5e8de18bad1c01c5`.
+The initial full ICS55 firmware matched between simulators at
+`e4cb5965b9b9ddb1135a0a672327113ed8b378ace56e1a4ee771374309c18a15`;
+the initial full IHP130 firmware matched at
+`048bd15221fcbd25986abaa2d973e74d101f3a8ac8f328a6169442ba8acabacb`.
+The corrected watchdog-acceptance full images match between simulators at
+ICS55 `69b8582099c8735e81014b9c3f5685d9212a58c18b820a8c9a611a6f64a2f6c4`
+and IHP130 `efc0b403f32669332e942cb63ee9bb2b035a6e462e68e0a5a1524dea01c6c298`.
+
+P7 has its own structural netlist audit. It requires 32 main and six private
+Crypto physical SRAM macros, main-SRAM/CPU SYS equality, the CPU-only gate,
+Crypto PCLK binding and exactly one real ICS55 PLL driving the SYS mux.
+The audit binds configuration digest, netlist/JSON and audit-script hashes.
+The strict historical P1 parked-PLL/direct-SYS audit is unchanged.
+
+Core STA analyzes SAFE24 boot, with explicit mux/PLL case analysis. Retain
+clocks, setup/hold, coverage, recovery/removal and constraint-violation reports.
+It excludes pad/board/routed/PVT and active-PLL qualification. Negative slack
+is observational before the final combined product campaign, not evidence of
+qualification and not permission to omit synthesis or netlist functionality.
+The commercial adapter mirrors the same six-domain SAFE24 inventory and
+requires explicit mux/PLL case selections; its host tests pass. Internal /2,
+/4 and CLKOUT-source clocks supplement the six root domains. Settled mux gate
+state is constrained because a select request alone does not propagate through
+the mux's sequential handshake in static analysis. The initial and second STA
+attempts remain under `sta/opensta` and `sta/opensta-p7-v2`; the final observation
+is under `sta/opensta-p7-v3`. No licensed
+commercial execution, active-PLL SDC, routed extraction, board IO or package
+qualification is supplied by those host tests.
+
+Historical `2026-10-13-17-15` SAFE24 observations, in ns, are ICS55 setup/hold WNS −147.48/−0.31,
+TNS −4758121.50/−45.30; IHP130 WNS −275.45/−0.50,
+TNS −9958118.00/−102.84. The worst setup cone begins at the fabric reset leaf,
+whose direct fanout is over 10,000; no reset false path was added to hide it.
+The v3 coverage reports retain excluded pad IO budgets, 42 unclocked ICS55 PLL
+monitor/mux pins while that source is off, and 5 ICS55 / 6 IHP130 control pins
+with multiple clock tags. These are explicit limits of this settled SAFE24
+observation, not complete-mode timing coverage or signoff. These values and
+coverage counts belong to that historical netlist; the current `06-00` WNS/TNS
+are in the table above, with their own retained coverage reports.
+
+The single full Pytest attempt records 1696 passed, 6 failed, 60 setup errors
+and one optional `pypdf` skip. Two failures were the commercial domain policy;
+60 setup errors and one failure shared the missing `RS_EBUSY=-8` SDK return
+entry in the publication's source-checked table. Both are corrected; the
+commercial/STA tests pass and the publication subset records 110 passed/one
+skip. The remaining three Icarus failures used a test module collected before
+the external-reset fixture acquired its `tiny_reset_tree` dependency; the
+updated test command explicitly supplies it and all seven external-source/reset
+checks pass. Keep that original full-suite result and these focused reruns;
+do not report an unexecuted clean full-suite pass. Only the shared return-code
+table changes in the publication; no source identity, PDF, edition or release
+qualification is advanced.
+
+## Interrupted and historical attempts
+
+Earlier October 13 build directories are development snapshots. In particular,
+the `15-45-f3e42d54c18b` Verilator attempt passed at 3,281,032 cycles and observed
+71,612 gated cycles, 64 DMA responses while gated and one warm reset. Later
+corrections mean that result is not final-source acceptance. Earlier synthesis
+attempts failed obsolete P1 checks; a temporary weakened P1 audit is invalid
+evidence and was restored. No current physical result may be inferred from it.
+
+The first final-campaign synthesis attempt (`17-07`, SVA enabled) failed during
+Slang elaboration on unsupported temporal assertion syntax. It did not run
+mapping. The `17-15` retry uses the existing regression's `HAVE_SVA=NO` synthesis
+configuration; behavioral/SVA verification remains separate and unchanged.
+
+The `17-08` complete Verilator runs passed all first-boot peripherals, then
+incorrectly repeated the firmware after watchdog reset and failed recovery
+code 26 on the second pass. The old firmware inferred warm boot from the
+watchdog's functional `reset_count`; P7 intentionally resets that state. The
+corrected firmware checks retained RCU cause bit 2, verifies that watchdog
+functional state was cleared and SAFE24 restored, and requires an SRAM `.noinit`
+cookie set only after all first-boot tests. This preserves the complete test
+sequence and watchdog acceptance rather than restoring obsolete retention.
+The corresponding `17-09` Icarus runs were cancelled as obsolete; controller
+exit 130 is retained in `controller-cancellation.json`, separately from any
+unfinished child result. Corrected full runs use `18-25` / `18-26` and source
+evidence under `build/ics55-tiny-2026-10-13-18-25-f3e42d54c18b/meta/tiny-r2-p7/`.
+
+The corrected full Icarus attempts under `18-26` reached the watchdog-reset
+stage after reporting all first-boot peripherals, but both exhausted the
+36,000-second wall-clock budget with exit 124 and no terminal PASS. Those
+results remain FAIL. A focused test uses the real WDG APB wrapper, command
+mailboxes, Tiny APB bridge, clock backend, reset tree and RCU, and verifies two
+successive hardware watchdog resets, retained RCU cause and cleared functional
+state in both simulators. It does not replace the full firmware gate.
+The exact retained `simv` and firmware were replayed under
+`sim/iverilog/attempt-2026-10-14-04-50/` with `timeout --foreground 72000s vvp -i`,
+the same `+max_cycles=30000000 +tiny_jtag_smoke` and firmware path. The
+maintainer then intentionally interrupted both replays with exit 130 and moved
+this long-running full-Icarus gate to the final Tiny phase. The logs and
+interrupted `result-sim.json` records are retained as unrun/partial evidence;
+they cannot satisfy the P7 terminal verdict. `-i` enabled unbuffered standard
+IO; it did not change the design or acceptance workload. No synthesis,
+simulator compilation or firmware build was repeated for the replay.
+
+PIDs are diagnostics in a particular execution namespace. Absence from another
+tool invocation's process list does not prove termination. Keep long commands
+attached to their execution sessions and use terminal result JSON plus strict
+log checks. `run_flow.py` records child/parent PIDs and closes results on caught
+exceptions; SIGKILL or host loss can still leave a running record. Such a record
+never counts as PASS. NFS clock-skew warnings are retained in the build logs.
+Cancellation now writes an interrupted checkpoint before process-group cleanup
+and ignores a second termination signal during that cleanup. The final result
+records the child's exit code and whether cleanup remains pending; the repeated
+signal/TERM-ignoring-child regression passes. An external executor can still
+kill a process before it handles a signal, so controller cancellation evidence
+must not be mistaken for a fabricated child exit result.
+
+The ledger records the final pass/fail/unrun matrix. Human review of that matrix,
+the diff and remaining qualification boundaries is the P7 acceptance gate.
+The generated `audit.py` under the corrected full-run evidence directory reads
+all source configurations' terminal results, strict Tiny markers, P7 lifecycle
+observations, unchanged P6 workload/wire records, JTAG, paired firmware hashes,
+macro audit, netlist boot and STA artifacts. It launches no hardware flow and
+writes `acceptance.json`; a pending/missing/failed input cannot produce PASS.
diff --git a/docs/ip/tiny-soc-r2-concurrent-fabric.md b/docs/ip/tiny-soc-r2-concurrent-fabric.md
new file mode 100644
index 00000000..68ca60e3
--- /dev/null
+++ b/docs/ip/tiny-soc-r2-concurrent-fabric.md
@@ -0,0 +1,402 @@
+# TINY-R2-P5 concurrent fabric
+
+This implements the approved P5 preflight on
+`3131762b35e0ebfcd196395f955e7c1005bd18e7`, using frozen specification blob
+`7e19de49d49efaafacccfa85671e314d53dd15a4`. The default is Tiny/ICS55 SAFE24
+with its present PLL parked off; IHP130/no PLL remains compatibility.
+The [ledger](tiny-soc-verification.md) separates phase evidence from approval.
+Historical campaigns and their limitations remain below. The P1 correction
+has its own source and validation scope; no P6, frequency or physical
+qualification is implied.
+
+## P1 protocol correction (2026-10-11)
+
+This correction starts from committed `4e6ab1a6fb445521703f0bfa412d3a1e8a38cd69`
+and the unchanged frozen specification. The preceding `09-30` campaign's
+`final-evidence.json` records two reset protocol failures and stale publication
+bindings even though ordinary regressions and matched-image replay passed.
+Those results do not qualify this corrected RTL.
+
+Target reset cancels the downstream session while preserving the live
+upstream session. A reset sampled in Address/Response latches the abort path;
+releasing the target cannot replay its cancelled address or data. Already
+captured R/B responses remain in the Common spills with unchanged VALID,
+payload, ID and order. Only fabric/system reset clears those buffers. The
+downstream stability assertions follow the target's own reset; upstream
+stability assertions remain enabled throughout target-only reset.
+
+A nine-bit count tracks captured read beats, including buffered and already
+retired beats. Missing DECERR beats append through the same spill until the
+original ARLEN+1 count is met; RLAST belongs only to the final beat. AbortWrite
+accepts/discards remaining upstream W beats before creating one DECERR B.
+Captured-B state prevents duplicate responses and preserves an already held
+successful completion. Fault metadata is captured from the first erroneous
+response beat, preserving earlier SLVERR over a subsequent reset DECERR;
+fault counting remains once per terminal transaction. Fully captured successful
+responses do not become reset faults.
+
+The reset fixture drives stimulus away from sampling edges and independently
+checks R/B stability, exact beat counts, W-before-B ordering, one completion,
+credit release and first-error metadata. Its 28 cases include 16-beat SRAM/XPI
+traffic, a production APB bridge, three production DMA-master drain cases,
+target reset held through completion and early target release. A separate
+formal mode varies target reset arbitrarily and proves response/ownership,
+beat-count and W/B ordering properties by induction. The normal formal mode
+and its fixed-response expectations remain intact. Publication bindings retain
+complete terminal/fault predicates, with negative tests for premature
+completion and response flushing; the reviewed publication SHA is unchanged.
+
+Evidence belongs under
+`build/ics55-tiny-2026-10-11-23-35-b4aa59f3e5d9/meta/tiny-r2-p5-p1/`.
+Final focused checks precede one full Pytest and one PR execution per Tiny
+PDK. No matched-image replay is required to claim this bounded correctness
+repair; historical workload measurements retain their source attribution.
+Validation completed on 2026-10-12; the ledger retains human review pending.
+Runtime peripheral-reset controls/quiescence remain P7, and the existing
+five-edge reset leaves remain unchanged. No shared DMA, public ABI, profile,
+dependency, warning baseline or metrics policy changes are included.
+
+The final technical input digest is
+`f670789a2379a8d2e6c59f5ecd714ca0237bb6f1cfe749a2f29b0794d356b5f0`.
+`source.json` preserves the 2011 inputs and locked tools; `evidence.json`
+rechecks their identity and binds all gate/fixture/flow artifacts. Focused
+validation passes 101 tests; full Pytest passes 1685 with one optional `pypdf`
+skip and no failures. Counts overlap. ICS55/IHP130 PR commands each ran once;
+their Tiny nightly dry-run matrices match, so the same gates were not repeated.
+All six ordinary simulations satisfy command success, the Tiny success marker
+and forbidden-marker checks. The scoped reset fixture and formal checks are
+additional protocol evidence, not physical or XPI-device qualification.
+
+| PDK / balanced recipe | Cells | Library area | Max WNS / TNS (ns) | Min WNS / TNS (ns) |
+| --- | ---: | ---: | ---: | ---: |
+| ICS55 | 245037 | 1910664.100799 | -70.04 / -1151596.25 | 0 / 0 |
+| IHP130 | 259394 | 9911158.202394 | -127.01 / -3120300.50 | 0 / 0 |
+
+Current physical-flow observations are under
+`build/ics55-tiny-2026-10-11-23-35-42d5beccb726/` and
+`build/ihp130-tiny-2026-10-11-23-35-bff91e3529ba/`, in `meta/metrics.json`
+and `sta/opensta/{timing_metrics.rpt,p4-clock-bindings.rpt,opensta.log}`.
+Both mapping checks retain four groups, 32 macros and CPU/main SRAM on SYS.
+Timing remains observational and unclosed; library area is not routed die area
+and zero observed min violations do not constitute PVT/hold signoff. Lint
+signature/count sets are identical to `09-30` (470/414 new against the unchanged
+ICS55/IHP130 baselines, zero increased). Successful ABC Liberty-cache fallback
+and NFS clock-skew observations remain logged. No baselines were regenerated.
+
+Commands used the locked activation, `BUILD_TIMESTAMP=2026-10-11-23-35` and
+`JOBS=3`. The `run_flow.py` JSON/log pairs in the correction evidence root retain
+every exact command: `focused`, `quality`, `pytest-full`, `regress-ics55`,
+`regress-ihp130`, `metrics-ics55`, `metrics-ihp130`, four `dry-run-*` records
+and `audit-result`. The principal acceptance commands were:
+
+```sh
+source .cache/retrosoc/development/tiny-r2-p1/activate.sh
+export BUILD_TIMESTAMP=2026-10-11-23-35 JOBS=3
+make rtl-format-check rtl-style-check-all rtl-readiness-check-all
+ruff check .
+python3 -m pytest -q --tb=short --basetemp=build/ics55-tiny-2026-10-11-23-35-b4aa59f3e5d9/meta/tiny-r2-p5-p1/tests-full
+python3 scripts/regress.py --root . --suite pr --soc TINY --pdk ICS55
+python3 scripts/regress.py --root . --suite pr --soc TINY --pdk IHP130
+make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 APP=ci_smoke SYNTH=YOSYS STA=OPENSTA metrics
+make CONFIG=configs/ci/ihp130-tiny.mk SOC=TINY PDK=IHP130 APP=ci_smoke SYNTH=YOSYS STA=OPENSTA metrics
+git diff --check
+```
+
+New executions must select a fresh timestamp/evidence directory. New matched
+workload replay, compiler experiments, full-function C netlist qualification,
+PDF build/visual promotion, CDC/RDC, extracted PVT/MMMC and place/route were
+not run. No performance, new operating-frequency or complete-product claim is
+made by this correction. Error completion does not roll back downstream writes
+or peripheral side effects that committed before reset.
+
+## Target-local reset repair (2026-10-11)
+
+This is the superseded first attempt. Its focused tests passed but omitted
+required protocol checks; the later `09-30` campaign and independent monitors
+rejected it. The following description records that attempted behavior, not
+the corrected implementation above.
+
+The P5 review found that a target reset was previously used only as an
+admission enable while an accepted controller session remained live. The
+attempt carried the explicit per-target reset leaf into each controller.
+Admission and downstream AXI VALID/W/RREADY are blocked while that leaf is
+asserted. An accepted Address or Response session changes to an abort response
+and returns one DECERR RLAST or B response with the saved owner and ID; the
+response remains held until the upstream terminal handshake. Response spill
+registers flush during the target reset, so an old downstream response cannot
+be replayed. The normal terminal handshake releases the fabric owner credit
+and records the reset termination as one fault event.
+
+The focused reset-isolation fixture covers an accepted SRAM read held before
+response and an accepted XPI/APB write held before W, under both Icarus and
+Verilator. It checks DECERR termination, no stale response replay, and owner
+credit release. These local reruns are separate from the earlier matched-image
+campaign; the campaign evidence and PPA/STA attribution remain unchanged until
+a new approved evidence capture is made. The retained focused test output is
+under `build/ics55-tiny-2026-10-10-18-58-b4aa59f3e5d9/meta/tiny-r2-p5/reset-repair-tests/`.
+
+## Ownership and bounds
+
+The three ingress slots are CPU slow path 0, central DMA 1 and reserved SDIO 2.
+Only slots 0/1 have product producers. CPU has one combined read/write credit;
+DMA and the reserved interface each have one read and one write credit.
+The real DMA already reserves complete write data before starting a burst;
+its shared scheduler and all request/channel assignments are unchanged.
+
+Targets 0–3 directly attach the existing SRAM group frontends through Common
+AXI connectors. Target 4 is XPI memory. Targets 5–20 follow the existing APB
+peripheral order, with GPIO's two windows sharing its one branch. Targets 21/22
+are the unchanged DECERR/SLVERR responders. The generator emits the decoder
+and sixteen single-peripheral bridge instances from canonical topology.
+There is no product-wide APB or external transaction lock.
+
+Each target admits one combined transaction and saves owner, direction and
+address attributes on the upstream address handshake. Admission reserves both
+target and ingress direction, even while the downstream address is stalled.
+Owner round robin advances on admission; the chosen owner's direction
+preference alternates when both directions compete. W waits for saved AW
+ownership and downstream AW acceptance, and stops after the terminal data beat.
+Bounded Common response spill registers isolate held R/B values. Ownership and
+ingress credit last through the upstream B/RLAST handshake, including buffered
+terminal responses. A continuously eligible owner can be bypassed by at most
+two other admissions; progress requires clocks and peer completion.
+
+Local CPU I/D, group arbitration, P4 response reservations and main SRAM macro
+paths are unchanged. The local uncontended bound remains three SYS cycles,
+and the P4 external SRAM streaming fixture still requires one beat per cycle.
+All new logic uses SYS; APB bridges follow their existing peripheral reset
+leaves. Target reset must be released for admission. System reset invalidates
+all buffered sessions without clearing SRAM payload. Hart reset drains CPU
+work only. Runtime peripheral resets still require P7's quiescence controls.
+
+## Faults and public compatibility
+
+Each target retains its first failed captured read beat's address/response
+until terminal upstream completion; a successful final beat cannot erase it.
+Write errors retain the accepted AW address. Six external direction events and
+two local CPU events feed the collector. CPU frontend errors carry SLVERR;
+external errors propagated through AHB are not counted again locally.
+
+The collector increments the existing count by every simultaneous event,
+using four-bit multiplicity and a 33-bit saturating sum. It selects metadata
+by ascending owner, CPU instruction before data, then read before write.
+Equal keys use stable input order. SYSCTRL preserves a valid first snapshot;
+W1C clears validity only and a simultaneous hardware event wins over clear.
+FAULT_MASTER adds reserved owner 2 without changing offset or owner 0/1 meanings.
+FAULT_DETAIL remains raw AXI response 2/3. The existing HAL byte-sized owner
+field needs no change. No SDK API, register generator, interrupt, DMA channel,
+capability or pad assignment is added.
+
+The old CPU/DMA wait registers retain their predicate: a requesting owner with
+no address handshake in that cycle counts once. R2_P5_FABRIC records separate
+active-target maxima, DMA simultaneous read/write ownership and simultaneous
+read/write beat handshakes. These are testbench observations. The original
+P3/P4 record formats and evidence retain their own identities.
+
+## Verification and reproducibility
+
+Focused protocol tests use all three owner slots, saved-address perturbations,
+W-before-AW, held addresses/data/responses, independent control branches,
+same-target contention and 96 deterministic randomized reads. Mixed-response
+bursts align two terminal faults while their final responses are OKAY.
+SYSCTRL tests cover multi-event count, owner 2, hardware-over-W1C and saturation.
+The saturation test seeds the Common counter register, then exercises the
+real adder and readback; it does not simulate billions of events.
+
+The system fixture instantiates production retrosoc_tiny, CPU, DMA, SRAM,
+bridges and SYSCTRL. Only the XPI boundary is replaced with a controllable
+verification endpoint and a fixture-only reset ROM. Real DMA copies B2 to B3
+with concurrent read/write beats and CPU readback checks. With an XPI mapped
+response held, SRAM code and SYSCTRL access continue. A held XPI APB write
+then verifies independent controls and hart-reset isolation. Finally CPU aborts
+a held DMA read, checks that BUSY survives accepted work, releases the response
+and verifies ABORTED. Ordinary regressions retain real pin-level XPI/NOR models.
+This fixture is neither a product Boot ROM nor an XPI-device qualification.
+
+The first fixture attempted hart reset while holding the XPI memory response;
+an accepted CPU instruction also needed that target, so reset correctly waited
+for drain. The corrected isolation case retains DMA at the independent APB
+branch and permits CPU progress. Other development attempts preserve sv2v
+diagnostics separately from converted RTL and observe declared interface
+instances rather than module-modport aliases for Icarus compatibility.
+
+Yosys induction checks production target ownership, response retention,
+terminal-fault attribution and service-count fairness. A combinational proof
+checks the collector's count and lexicographic metadata selection for arbitrary
+event vectors. These are scoped control proofs, not full-chip formal signoff.
+
+The P5 replay entrypoint uses the unchanged P3 flat/banked images referenced by
+the repaired P4 report:
+`build/ics55-tiny-2026-10-10-15-15-b4aa59f3e5d9/meta/tiny-r2-p4/report.json`.
+The old reports retain their original 4db27524-based snapshot attribution;
+preflight matched all 1972 technical inputs to the committed P5 starting point.
+P5 report identity, source/configuration/tool hashes and observations are new.
+Parameters stay 1024 words, four jobs, 4096 CPU iterations and seed 0x12345678.
+Each Verilator image runs once and banked Icarus once; all banked observations
+must agree. There is no frozen overall speedup requirement or compiler/layout
+experiment. Different-bank concurrency is proved separately because the
+retained banked placement concentrates DMA storage in B3.
+
+Fresh development evidence begins at
+`build/ics55-tiny-2026-10-10-18-25-b4aa59f3e5d9/meta/tiny-r2-p5/`.
+The completed campaign and its monitor-only follow-up are recorded below.
+Documentation remains here rather than in generated directories.
+The Tiny draft/source extractor follows P5 routing and the fault field width;
+its reviewed publication SHA and PDF status are not advanced.
+
+## Completed campaign and monitor correction
+
+The final evidence root is
+`build/ics55-tiny-2026-10-10-18-58-b4aa59f3e5d9/meta/tiny-r2-p5/`.
+`source.json` binds the initial 2010-input campaign to combined ICS55/IHP130
+digest `e85f4245883bf35f152d607480adf48edcf49debadb46d8b0bde92050a792b11`.
+Both Tiny PR regressions, full Pytest and the first three image executions
+passed on that snapshot. The initial aggregation in `report-result.{json,log}`
+correctly failed: Icarus reported active-target maxima one greater than
+Verilator in all eight banked windows, while every other sample was identical.
+
+The locked Icarus 13 installation miscounts the temporary expression
+`$countones(~a_23_bit_vector)`. `countones_probe.sv` and its retained log
+demonstrate an expected count 1 returning 3/6, while a named vector and explicit
+bit counting return 1. The correction counts the declared 23 bits explicitly,
+rejects unknown target states, and adds an opt-in count 0..23 self-check. No
+postprocessing subtraction, relaxed comparison, tool upgrade or DUT fix is used.
+
+`monitor-source.json` captures the final combined digest
+`0f3ed21f6fd6e932ff2532aab8c9964e71cf4b972e4c27e2c8ca6ab1f22c1015`.
+Its only technical difference from the initial capture is
+`rtl/tiny/dv/retrosoc_tiny_tb.sv`, inside P5 opt-in observation/self-check code.
+The DUT, firmware, verification fixtures, policies and tool inputs are identical.
+The ordinary regression commands do not enable this branch, and netlist boot
+excludes it. Their results, full-suite results and PPA retain their original
+snapshot attribution; they were not rerun or relabeled as new executions.
+The audit checks this exact single-file delta and the original command flags.
+
+Both Verilator images and banked Icarus were rebuilt/replayed after the fix,
+and the monitor self-check passed on both new models. The final ICS55-only
+replay input digest is
+`df14713ed7177d988d0e3fa0a038984589584788eda994dd7480f01a8dfcab7c`.
+`report-final.{json,log}` and `report.json` confirm exact equality of all banked
+observations across simulators, including the corrected active-target statistic.
+`monitor-diagnosis.json` preserves the original discrepancy, logs and input
+identity. Final model copies are retained beside their attempt records and
+bound by `model-backup-*.json`; the initial Icarus model is separately retained.
+
+| Gate | Retained result | Scope/outcome |
+| --- | --- | --- |
+| Focused development suite | Earlier `18-25` root, `focused-final.{json,log}` | 106 passed before final campaign |
+| Initial quality | `quality.{json,log}` | Ruff, diff, RTL format/style/readiness pass; 11.524 s |
+| Full Pytest | `pytest-full.{json,log}` | 1679 passed, optional `pypdf` skip; 3867.891 s; original snapshot |
+| Tiny ICS55 PR | `regress-tiny-ics55.{json,log}` | Pass; 4273.573 s; original snapshot |
+| Tiny IHP130 PR | `regress-tiny-ihp130.{json,log}` | Pass; 3028.340 s; original snapshot |
+| Initial image executions | `replay-verilator` / `replay-iverilog` results | Commands pass; 609.465/5952.278 s; cross-simulator aggregation fails as above |
+| Monitor follow-up quality | `monitor-quality.{json,log}` | Pass; 11.714 s |
+| Count self-checks | `monitor-check-{verilator,iverilog}.{json,log}` | Pass; instrumentation-only, not firmware acceptance |
+| Final image executions | `replay-verilator-monitor` / `replay-iverilog-monitor` results | Pass; 618.974/5978.016 s; one run per selected image |
+| Final report and audit | `report-final.{json,log}`, `report.json`, `evidence.json`, `audit.py` | Measurement complete; source-bound functional validation complete, review pending |
+
+All six ordinary simulations satisfy command success, configured SIM_TEST_PASS
+and absence of forbidden error markers. In the final full suite, six P4 SRAM
+fixtures preserve three-cycle local latency and one-cycle streaming spacing;
+two P4 CPU fixtures, two P5 fabric fixtures, two real-CPU/DMA system fixtures
+and three scoped formal logs pass. The system fixture reports 13 simultaneous
+DMA read/write beat cycles and 27 independent control completions on both tools,
+with hart-reset isolation and abort drain. SYSCTRL tests additionally check
+multiple faults, reserved owner 2, raw detail, clear precedence and saturation.
+These are functional results, not physical or XPI-device qualification.
+
+## Measurements and qualification limits
+
+The final runs preserve the immutable P3 image identities and compare them
+with repaired P4 measurements, without changing compiler or placement:
+
+| Workload | Flat P4 → P5 cycles | Banked P4 → P5 cycles |
+| --- | ---: | ---: |
+| Empty | 69 → 69 | 66 → 66 |
+| CPU | 73911 → 73911 | 73914 → 73914 |
+| CPU copy | 102568 → 102568 | 86214 → 86214 |
+| DMA burst1 | 91306 → 65527 | 89365 → 65502 |
+| DMA burst16 | 17897 → 16974 | 17317 → 15802 |
+| CPU/DMA contention | 299881 → 299990 | 299633 → 299738 |
+| TCD contention | 299878 → 300027 | 299645 → 299750 |
+| Chunk256 | 361285 → 361514 | 359570 → 361355 |
+
+The flat burst16 window records a maximum of three active targets, 124 cycles
+with simultaneous DMA read/write ownership and 48 simultaneous read/write beat
+cycles. Banked DMA stays in B3 and has zero DMA read/write overlap; up to two
+targets are active because independent CPU control can overlap that bank.
+The separate system fixture establishes different-bank DMA concurrency.
+CPU/copy cycles are unchanged. Small contention/TCD increases and the 0.50%
+banked chunk256 increase remain visible; no universal speedup is claimed.
+
+Synthesis/STA observations for the unchanged DUT use
+`build/ics55-tiny-2026-10-10-18-58-42d5beccb726/` and
+`build/ihp130-tiny-2026-10-10-18-58-bff91e3529ba/`.
+Both pass synthesis, netlist boot and the existing 32-macro/direct-SYS binding
+checks. Reports are `meta/metrics.json`, `sta/opensta/timing_metrics.rpt`,
+`sta/opensta/p4-clock-bindings.rpt` and `sta/opensta/retrosoc_sta.log`.
+
+| PDK | Cells | Library area | Max WNS / TNS (ns) | Min WNS / TNS (ns) |
+| --- | ---: | ---: | ---: | ---: |
+| ICS55 | 241309 | 1906156.940799 | -64.75 / -932689.50 | 0 / 0 |
+| IHP130 | 256995 | 9888378.712794 | -111.93 / -2594456.50 | 0 / 0 |
+
+Library area grows 5.56%/6.95% versus repaired P4. Max timing margins worsen;
+timing remains unclosed and observational before the final product campaign.
+Library area is not routed die area, and zero observed min violations is not
+PVT/hold signoff. No new timing exception or rate is introduced.
+
+The unchanged warning baselines report 469/413 new signatures for ICS55/IHP130,
+zero increased and zero resolved. Relative to repaired P4, two new signatures
+are intentional empty `owner_idle_o`/`target_idle_o` connections; their future
+lifecycle use and verification visibility are documented at the instances.
+The former unused interface `aresetn` signature disappears because target
+admission now consumes it. There is no new width/latch signature. The known
+NFS clock-skew warning and successful ABC Liberty-cache fallback remain logged.
+No warning baseline or metrics policy was changed.
+
+Mini behavioral compatibility retains its earlier P4 evidence and source
+identity. The audit rechecks the unchanged shared/Mini/SDK/configuration inputs;
+there is no new Mini execution or physical claim. No C/H implementation or
+MISRA deviation changed. The P3 compiler matrix and three cold repetitions,
+full C netlist campaign, PDF build/visual checks, CDC/RDC/PVT/MMMC, place/route,
+high-rate qualification and P6+ work were not performed. Only the concretely
+affected image measurements were repeated for the monitor defect.
+
+## Executed commands
+
+Commands used the locked activation and `BUILD_TIMESTAMP=2026-10-10-18-58`,
+`JOBS=3`. `scripts/run_flow.py` retains the exact outer commands and JSON/log
+results named above. New evidence is under `meta/tiny-r2-p5`; source snapshots,
+tool identities, input digests, models, images and raw logs remain separate.
+
+```sh
+source .cache/retrosoc/development/tiny-r2-p1/activate.sh
+export BUILD_TIMESTAMP=2026-10-10-18-58 JOBS=3
+make rtl-format-check rtl-style-check-all rtl-readiness-check-all
+ruff check .
+git diff --check
+python3 -m pytest -q --basetemp=build/ics55-tiny-2026-10-10-18-58-b4aa59f3e5d9/meta/tiny-r2-p5/tests-full
+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 nightly --soc TINY --pdk IHP130 --dry-run
+python3 scripts/regress.py --root . --suite pr --soc TINY --pdk ICS55
+python3 scripts/regress.py --root . --suite pr --soc TINY --pdk IHP130
+make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 SIMU=VERILATOR SOC_SIM_TIME=21600 \
+ TINY_P5_REFERENCE=build/ics55-tiny-2026-10-10-15-15-b4aa59f3e5d9/meta/tiny-r2-p4/report.json \
+ TINY_P5_CASES=perf-o2-plain-flat,perf-o2-plain-banked tiny-r2-p5-replay
+make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 SIMU=IVERILOG SOC_SIM_TIME=21600 \
+ TINY_P5_REFERENCE=build/ics55-tiny-2026-10-10-15-15-b4aa59f3e5d9/meta/tiny-r2-p4/report.json \
+ TINY_P5_CASES=perf-o2-plain-banked tiny-r2-p5-replay
+make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 tiny-r2-p5-report
+make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 APP=ci_smoke SYNTH=YOSYS STA=OPENSTA metrics
+make CONFIG=configs/ci/ihp130-tiny.mk SOC=TINY PDK=IHP130 APP=ci_smoke SYNTH=YOSYS STA=OPENSTA metrics
+PYTHONDONTWRITEBYTECODE=1 python3 build/ics55-tiny-2026-10-10-18-58-b4aa59f3e5d9/meta/tiny-r2-p5/audit.py
+```
+
+Tiny PR/nightly definitions matched and were not executed twice. The first
+report attempt failed; only the monitor-corrected executions qualify the final
+cross-simulator report. The original cold executions are not a three-repeat
+qualification campaign. No commit, staging, push, PR or automatic next-phase
+transition belongs to this implementation.
diff --git a/docs/ip/tiny-soc-r2-local-memory.md b/docs/ip/tiny-soc-r2-local-memory.md
new file mode 100644
index 00000000..76fd0200
--- /dev/null
+++ b/docs/ip/tiny-soc-r2-local-memory.md
@@ -0,0 +1,387 @@
+# TINY-R2-P4 local CPU/SRAM implementation
+
+## Approved throughput repair (2026-10-10; validated, review pending)
+
+The maintainer approved the eight-file P4 throughput-repair preflight after
+the read-only review identified the external frontend's per-beat bubbles.
+This follow-up remains P4; it does not enable P5 target concurrency.
+The initial implementation and its measurements below are retained historical
+evidence for the pre-repair snapshot, not qualification of the changed RTL.
+Fresh evidence is under
+`build/ics55-tiny-2026-10-10-15-15-b4aa59f3e5d9/meta/tiny-r2-p4/`.
+
+The group now reserves two response slots for its external client, counting
+both queued completions and a macro read still in flight. A response pop may
+free capacity on the same edge as the next issue. The macro's saved owner/bank
+still selects the captured read value; FIFO data cannot be overwritten by
+later local accesses. A same-client pending read prevents a simultaneous write
+commit into the single-push response FIFO. Local I/D keep one operation each
+and their original completion pipeline.
+
+The external frontend separates issued and retired read positions. W uses a
+two-entry Common FIFO containing captured address, data and byte strobes.
+Only captured legal beats enter arbitration. Invalid strobes or LAST retain
+the existing error policy, including earlier committed beats and later legal
+beats after an invalid strobe. B waits for all queued legal writes and their
+commit responses. External transaction ownership still lasts through B/RLAST.
+These are small register queues, not additional user SRAM or new macros.
+
+Acceptance adds consecutive 16-beat read/write handshakes at one SYS cycle per
+beat after startup, without local competition or bus backpressure. Logs retain
+first-beat latency, total transfer time and final-W-to-B delay. Directed checks
+fill/drain queues, stop READY, interleave local service, retain old read values
+across local writes, exercise buffered malformed writes and drop stale queued
+responses on system reset. Induction checks response conservation/capacity in
+addition to held-response stability and the existing service-count fairness.
+The unchanged CPU debug/reset, discovery/counters and both real-PDK macro
+fixtures remain required.
+
+Use the same immutable P3 flat/banked images and parameters. Each selected
+Verilator image runs once; banked Icarus runs once. Compare both with their P3
+samples and the pre-repair P4 report. The repair requires burst-16 cycles below
+36713/36489 respectively, truthful reporting of all other workload deltas and
+identical banked results across simulators. It does not promise P3 cycle parity
+or change compiler/layout settings. Existing shared Mini debug inputs are
+unchanged by this repair; matching prior compatibility evidence is retained.
+
+### Repair results and provenance
+
+The campaign is complete for review. `source.json`, `report.json`, `audit.py`
+and `evidence.json` in the fresh evidence root bind 1972 technical inputs,
+six fresh Tiny regression simulations, three matched-image executions,
+six real/behavioral SRAM fixtures, two CPU fixtures and control induction.
+The combined ICS55/IHP130 input digest is
+`58daf68fbb9dee199b3085a73ef07d22eb3de6d00ba875c462e85b76b6091a0d`;
+the ICS55 replay input digest is
+`a96a129ffc02d0d38fd2eee7ed8fb62893828db3602bbd4459e0a71ffce0c35f`.
+Only the approved six technical files differ from the preceding P4 snapshot;
+the other two changes are this runbook and the ledger. No commit or staging
+operation is part of this campaign.
+
+All six SRAM runs record `read_gap_max=1 write_gap_max=1 beats=16`, with first
+R at three cycles after AR and final R at cycle18; W begins one cycle after
+AW, final B is at cycle20, four cycles after final W. These are standalone
+uncontended protocol measurements, not end-to-end DMA or physical timing.
+All six retain `local_latency_max=3`. The new queue checks cover full-response
+backpressure, local writes to already captured read addresses, W contention,
+buffered malformed writes, and both queued-R and accepted-but-uncommitted-W
+system resets. CPU debug/patch/hart-reset and induction pass in the final suite.
+
+| Gate | Result under the fresh evidence root | Outcome |
+| --- | --- | --- |
+| Final Ruff, diff, RTL format/style/readiness | `quality-pass.{json,log}` | Pass |
+| Full Pytest | `pytest-full.{json,log}` | 1659 passed, one optional `pypdf` skip; 3903.061 s |
+| Tiny ICS55 PR | `regress-tiny-ics55.{json,log}` | Pass; 3396.035 s |
+| Tiny IHP130 PR | `regress-tiny-ihp130.{json,log}` | Pass; 2340.150 s |
+| Verilator, both immutable images once | `replay-verilator.{json,log}` | Pass; 390.698 s |
+| Icarus, immutable banked image once | `replay-iverilog.{json,log}` | Pass; 5154.223 s |
+| Cross-simulator report | `report-result.{json,log}`, `report.json` | Complete; all banked observations identical |
+| Source/artifact audit | `evidence.json`, `audit.py` | Pass; review/phase acceptance pending |
+
+The initial `quality-final.{json,log}` is a retained failed development attempt:
+the style gate required `s_req_accept`/`s_resp_accept` abbreviations. The names
+were corrected before the final source snapshot and all long gates. The first
+29 focused checks and additional queued-W-reset check also passed; final-suite
+logs qualify the final technical snapshot. No failed attempt is relabeled.
+
+| Workload | Flat pre-repair P4 → repaired P4 | Banked pre-repair P4 → repaired P4 |
+| --- | ---: | ---: |
+| Empty | 69 → 69 | 66 → 66 |
+| CPU | 73911 → 73911 | 73914 → 73914 |
+| CPU copy | 102568 → 102568 | 86214 → 86214 |
+| DMA burst1 | 91306 → 91306 | 89365 → 89365 |
+| DMA burst16 | 36713 → 17897 | 36489 → 17317 |
+| CPU/DMA contention | 303493 → 299881 | 299633 → 299633 |
+| TCD contention | 303702 → 299878 | 299645 → 299645 |
+| Chunk256 | 377705 → 361285 | 376970 → 359570 |
+
+Burst16 cycles decrease 51.25%/52.54%, satisfying the approved repair criterion.
+They remain 8.34%/4.81% above the original P3 16520/16523 cycles. Burst1 still
+retains the earlier P4 overhead versus P3; this repair does not claim universal
+baseline parity. Flat local maximum is four cycles in contended windows, with
+17697 eligible bank-conflict cycles; banked maximum is three with zero conflicts.
+Uncontended local latency is still checked independently. Firmware checksums,
+parameters and each image's compiler/layout identity remain unchanged.
+
+Fresh physical variants are
+`build/ics55-tiny-2026-10-10-15-15-42d5beccb726/` and
+`build/ihp130-tiny-2026-10-10-15-15-bff91e3529ba/`.
+Both pass synthesis, netlist boot and common-SYS/32-macro binding checks.
+Their `meta/metrics.json`, `sta/opensta/timing_metrics.rpt`,
+`sta/opensta/p4-clock-bindings.rpt` and `sta/opensta/retrosoc_sta.log` retain
+the matching observations:
+
+| PDK | Cells | Library area | Setup WNS / TNS (ns) | Hold WNS / TNS (ns) |
+| --- | ---: | ---: | ---: | ---: |
+| ICS55 | 207029 | 1805809.700799 | -36.88 / -489807.34 | 0 / 0 |
+| IHP130 | 218773 | 9245382.228597 | -97.37 / -1640251.62 | 0 / 0 |
+
+Setup remains unclosed; library area is not die area and zero observed hold
+slack violations are not physical signoff. Lint comparisons to the unchanged
+baselines report 468/412 new signatures for ICS55/IHP130, zero increased and
+zero resolved. Relative to pre-repair P4 there are exactly two additional
+signatures: intentionally unused FIFO `cnt_o`, and `s_full`, which is consumed
+by the verification assertions but unused in synthesis-oriented lint. No new
+width/latch warning is attributed to the repair. Warning and metrics policy
+remain observational and no baseline was edited.
+
+Commands below used the same timestamp, with `scripts/run_flow.py` retaining
+outer command/result/log records. Standalone test artifacts use distinct
+`tests-focused`, `tests-reset` and `tests-full` directories under this variant.
+
+```sh
+source .cache/retrosoc/development/tiny-r2-p1/activate.sh
+export BUILD_TIMESTAMP=2026-10-10-15-15 JOBS=3
+python3 -m pytest -q tests/test_tiny_r2_p4.py --basetemp=build/ics55-tiny-2026-10-10-15-15-b4aa59f3e5d9/meta/tiny-r2-p4/tests-focused
+python3 -m pytest -q 'tests/test_tiny_r2_p4.py::test_four_group_local_memory[BEHAV-iverilog]' --basetemp=build/ics55-tiny-2026-10-10-15-15-b4aa59f3e5d9/meta/tiny-r2-p4/tests-reset
+ruff check .
+git diff --check
+make rtl-format-check rtl-style-check-all rtl-readiness-check-all
+python3 -m pytest -q --basetemp=build/ics55-tiny-2026-10-10-15-15-b4aa59f3e5d9/meta/tiny-r2-p4/tests-full
+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 nightly --soc TINY --pdk IHP130 --dry-run
+python3 scripts/regress.py --root . --suite pr --soc TINY --pdk ICS55
+python3 scripts/regress.py --root . --suite pr --soc TINY --pdk IHP130
+make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 SIMU=VERILATOR SOC_SIM_TIME=21600 \
+ TINY_P4_REFERENCE=build/ics55-tiny-performance-2026-10-09-17-10-53cc6674d532/meta/tiny-r2-p3/report.json \
+ TINY_P4_CASES=perf-o2-plain-flat,perf-o2-plain-banked tiny-r2-p4-replay
+make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 SIMU=IVERILOG SOC_SIM_TIME=21600 \
+ TINY_P4_REFERENCE=build/ics55-tiny-performance-2026-10-09-17-10-53cc6674d532/meta/tiny-r2-p3/report.json \
+ TINY_P4_CASES=perf-o2-plain-banked tiny-r2-p4-replay
+make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 tiny-r2-p4-report
+make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 APP=ci_smoke SYNTH=YOSYS STA=OPENSTA metrics
+make CONFIG=configs/ci/ihp130-tiny.mk SOC=TINY PDK=IHP130 APP=ci_smoke SYNTH=YOSYS STA=OPENSTA metrics
+PYTHONDONTWRITEBYTECODE=1 python3 build/ics55-tiny-2026-10-10-15-15-b4aa59f3e5d9/meta/tiny-r2-p4/audit.py
+```
+
+Tiny PR/nightly dry-runs match; no duplicate nightly execution was needed.
+The six fresh ordinary simulations all require `SIM_TEST_PASS`, including
+Tiny netlist boot, together with command success and no forbidden error marker.
+Mini's two prior behavioral results retain their original source attribution;
+the audit verifies that changed technical inputs are confined to Tiny and its
+tests. Mini synthesis/STA, the P3 compiler matrix, additional cold repetitions,
+C format/policy/host reruns, PDF generation, PVT/MMMC, CDC/RDC and place/route
+were not run for this repair. No C/H code or MISRA deviation changed. The
+full-suite optional PDF retrieval check remains skipped for missing `pypdf`.
+No new frequency, physical qualification or P5 authorization is claimed.
+
+## Original implementation snapshot
+
+This implements the approved [P4 contract](tiny-soc.md#tiny-r2-p4---dual-port-hazard3-and-four-bank-local-sram)
+from `4db27524a6282ca40d76b935415b89f043d6ea83`, specification blob
+`7e19de49d49efaafacccfa85671e314d53dd15a4`. The maintainer approved the preflight
+and implementation. No P5 execution, dependency update, publication release or
+physical-frequency qualification is implied. Historical phase evidence remains
+in the [ledger](tiny-soc-verification.md).
+
+## Architecture and lifecycle
+
+Tiny uses locked `hazard3_cpu_2port`, preserving ISA, IRQ, debug, multiply and
+branch-prediction settings; A remains disabled. Separate I/D AHB frontends decode
+SRAM before the tagged slow-path merge. One accepted D operation spans all
+targets. Write data is captured in its AHB data phase; local stores complete
+after macro commit. Uncontended local completion is at most three SYS cycles.
+Slow errors retain two-cycle AHB completion. Instruction origin reaches the
+existing fabric fault path: only NOR boot/NSS0 may be fetched externally, and
+peripheral fetches cannot trigger register reads.
+
+Four 32 KiB groups cover `0x30000000..0x3001ffff`, each with eight existing
+4 KiB macros and I/D/external round robin advancing only on macro issue.
+Per-client registers capture read responses before macro outputs change.
+External W waits and R/B backpressure do not lock local macro service.
+Continuously eligible requests wait for at most two other issues, conditional
+on progress rather than a wall-clock bound for stopped peers.
+
+Each external frontend retains one transaction through B/RLAST and alternates
+read/write preference. A direction-tagged demux preserves the existing external
+fabric attachment. Whole-product different-target DMA read/write concurrency
+remains P5; standalone frontend concurrency is not that product qualification.
+
+Hart reset requests debug halt, prevents new debug instruction injection, drains
+accepted local/slow operations and responses, then resets the hart/frontends.
+A terminal-cycle AHB overlap must still drain. SRAM/external frontends and
+unrelated DMA stay active. System reset invalidates pending responses without
+clearing SRAM payload. Five-edge release and Mini's default single-port reset
+behavior are preserved; shared debug only adds a pending-state output.
+
+## ABI, physical bindings and observations
+
+Shared SRAM register definitions are unchanged: 128 KiB, `BANK_COUNT=32`,
+`BANK_BYTES=4096`. Public counters count external AXI events, saturating sums
+of simultaneous events; stall cycles use OR across groups. Local CPU traffic
+is not silently added. `R2_AHB` observes the separate I/D ports. `R2_P4_LOCAL`
+records local completions, latency and address-admission waits; `R2_P4_BANK`
+records macro issues, eligible-client waits and conflict cycles (two or more
+eligible clients in a group). These are verification-only observations.
+
+Macro `g,b` is `u_soc.u_sram.gen_group[g].u_group.gen_bank[b].u_ram.u_mem`,
+physical index `8*g+b`. IHP130 placement coordinates stay fixed while instance
+and supply hooks track both indices. CPU and all 32 macros remain directly on
+SYS24; ICS55 PLL stays off and IHP130 remains no-PLL compatibility. Mapping
+checks reject stale single-port/flat-bank hierarchies. STA remains observational,
+with no 96/192/240 MHz or physical signoff claim.
+
+The Tiny publication extractor and affected draft prose/diagram follow the new
+bindings. No reviewed publication SHA, delivered PDF or release status advances.
+
+## Acceptance and matched images
+
+`tests/test_tiny_r2_p4.py` uses real IHP130/ICS55 macro models and a behavioral
+model on both simulators: all macro/group boundaries, byte masks, back-to-back
+AHB, independent groups, held responses, W-before-AW, bursts/malformed writes,
+discovery/counter semantics and reset payload retention. Deterministic readback
+uses seed `0x12345678`, 4096 write/read pairs per group. The actual CPU fixture
+checks mixed 16/32-bit cross-group fetch, FENCE.I, instruction faults, JTAG
+patch/resume, and reset with a held slow response plus an unrelated held B.
+Yosys induction proves group control fairness and retained responses with only
+macro payload abstracted. Normal Tiny firmware additionally executes code
+copied through the real DMA, then CPU-patched code, after completion/FENCE.I.
+Its helper is private acceptance assembly, not a new SDK API.
+
+`tiny-r2-p4-replay` validates retained P3 images/logs, builds a fresh P4 model,
+and executes each selected image once. Image source/compiler/layout and new
+RTL/model identities are separate; hardware settings must match. Original P3
+same-source/three-cold-start rules are unchanged. `tiny-r2-p4-report` revalidates
+artifacts and requires identical banked-image results across the two simulators.
+
+The reference is
+`build/ics55-tiny-performance-2026-10-09-17-10-53cc6674d532/meta/tiny-r2-p3/report.json`.
+Select `perf-o2-plain-flat` and `perf-o2-plain-banked` for Verilator and the banked
+image for Icarus. Parameters remain 1024 words, four jobs, 4096 CPU iterations,
+seed `0x12345678`, burst-1/16 DMA, TCD and chunk-256. Compare each unchanged
+binary with its own historical samples, not with a different compiler/layout.
+No fixed speedup is predeclared; latency/fairness requirements remain gates.
+
+Development evidence begins under
+`build/ics55-tiny-2026-10-10-10-05-b4aa59f3e5d9/meta/tiny-r2-p4/`.
+Early fixture failures and corrected runs are retained separately. The final
+campaign below completes planned functional validation and measurements; human
+review/phase acceptance remain pending, including the DMA throughput tradeoff.
+
+## Executed campaign and provenance
+
+The evidence root above contains `source.json`, `evidence.json`, `audit.py` and
+`report.json`. The campaign snapshot contains 1972 unchanged technical inputs,
+base commit `4db27524a6282ca40d76b935415b89f043d6ea83` plus the retained patch and
+new-file archive, and locked tool/executable/archive identities. Its combined
+ICS55/IHP130 digest is
+`9fa311a5500711c99b006eacd95cdff04a74659d52b5b46640c42c49773a7f66`.
+The ICS55-only replay source set has digest
+`2f0d285186f84d41422d8381d09b0a01a2afc066dbd011a11ec7a8a85433ad0d`;
+these are different declared input sets, not conflicting identities.
+
+The final audit rechecks all captured inputs, eight ordinary regression
+simulations, the three matched-image executions, six standalone memory runs,
+two actual-CPU runs and the group-control induction log. Ordinary firmware and
+model artifacts are hashed post-run; the campaign source hashes were captured
+before execution and rechecked afterward. Image/model identities and original
+P3 phase attribution are retained separately.
+
+| Gate | Retained result under the evidence root | Outcome |
+| --- | --- | --- |
+| Format/style/readiness, C policy/host, Ruff/diff | `quality-final.{json,log}` | Pass; no new MISRA deviation; partial checks, not MISRA certification |
+| Full Pytest | `pytest-full.{json,log}` | 1659 passed, one optional `pypdf` environment skip; 3856.630 s |
+| Tiny ICS55 PR | `regress-tiny-ics55.{json,log}` | Pass; 3295.064 s |
+| Tiny IHP130 PR | `regress-tiny-ihp130.{json,log}` | Pass; 2297.373 s |
+| Mini ICS55 behavioral-only PR | `regress-mini-ics55.{json,log}` | Pass; 3093.379 s; no Mini physical evidence |
+| P4 Verilator replay | `replay-verilator.{json,log}` | Both selected images once; 357.537 s including build/audit |
+| P4 Icarus replay | `replay-iverilog.json`, `replay-iverilog.json.log` | Banked image once; 5214.736 s including build/audit |
+| Matched-image aggregation | `report-result.json`, `report.log`, `report.json` | `measurement_complete`; all banked samples match across simulators |
+
+The six standalone memory logs report `local_latency_max=3`, with real
+simultaneous group issues. Those protocol fixtures use a 10 ns verification
+clock; product replays and timing checks use SAFE24. The banked product replay
+also observes three-cycle maximum local latency and zero eligible bank-conflict
+cycles. No physical frequency qualification follows from functional models.
+
+Commands were run through `scripts/run_flow.py` for retained logs/results:
+
+```sh
+source .cache/retrosoc/development/tiny-r2-p1/activate.sh
+export BUILD_TIMESTAMP=2026-10-10-10-05
+export JOBS=3
+make rtl-format-check rtl-style-check-all rtl-readiness-check-all
+make sw-format-check sw-policy-check sw-host-test
+ruff check .
+git diff --check
+python3 -m pytest -q --basetemp=build/ics55-tiny-2026-10-10-10-05-b4aa59f3e5d9/meta/tiny-r2-p4/tests-full
+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 scripts/regress.py --root . --suite pr --soc TINY --pdk ICS55
+python3 scripts/regress.py --root . --suite pr --soc TINY --pdk IHP130
+JAVA_TOOL_OPTIONS=-Dsbt.server.forcestart=true python3 scripts/regress.py \
+ --root . --suite pr --soc MINI --pdk ICS55 --behavioral-only
+make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 SIMU=VERILATOR SOC_SIM_TIME=21600 \
+ TINY_P4_REFERENCE=build/ics55-tiny-performance-2026-10-09-17-10-53cc6674d532/meta/tiny-r2-p3/report.json \
+ TINY_P4_CASES=perf-o2-plain-flat,perf-o2-plain-banked tiny-r2-p4-replay
+make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 SIMU=IVERILOG SOC_SIM_TIME=21600 \
+ TINY_P4_REFERENCE=build/ics55-tiny-performance-2026-10-09-17-10-53cc6674d532/meta/tiny-r2-p3/report.json \
+ TINY_P4_CASES=perf-o2-plain-banked tiny-r2-p4-replay
+make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 tiny-r2-p4-report
+make CONFIG=configs/ci/ics55-tiny.mk SOC=TINY PDK=ICS55 APP=ci_smoke SYNTH=YOSYS STA=OPENSTA metrics
+make CONFIG=configs/ci/ihp130-tiny.mk SOC=TINY PDK=IHP130 APP=ci_smoke SYNTH=YOSYS STA=OPENSTA metrics
+```
+
+Tiny PR and nightly expand to the same commands; nightly was not duplicated.
+Each simulation requires command success, its configured terminal marker and
+no forbidden-error marker. All selected ordinary targets require SIM_TEST_PASS,
+including Tiny netlist boot. UART text alone is insufficient. Development
+attempts are not counted as extra qualifying cold starts.
+
+## Measurements and remaining qualification
+
+Raw cycles below compare each immutable binary with its own P3 baseline.
+The report also retains retired instructions, CPI inputs, empty calibration,
+local waits, macro issues/conflicts and external traffic accounting.
+
+| Workload | Flat P3 → P4 cycles | Banked P3 → P4 cycles |
+| --- | ---: | ---: |
+| CPU | 73951 → 73911 | 73954 → 73914 |
+| CPU copy | 127157 → 102568 | 114875 → 86214 |
+| DMA burst 1 | 64577 → 91306 | 64583 → 89365 |
+| DMA burst 16 | 16520 → 36713 | 16523 → 36489 |
+| CPU/DMA contention | 310368 → 303493 | 310383 → 299633 |
+| TCD contention | 310665 → 303702 | 310683 → 299645 |
+| Chunk 256 | 373812 → 377705 | 374016 → 376970 |
+
+CPU-copy cycles decrease by about 19%/25%, while isolated burst-16 DMA takes
+about 2.2 times the baseline cycles. The current external frontend processes
+issue, completion and bus response separately for each beat; preserving local
+response isolation costs streaming throughput. This regression is retained for
+review, not hidden by the CPU-copy result or a changed compiler/layout. P5's
+external target concurrency alone does not remove these per-beat waits. No
+fixed speedup was an acceptance gate, but the tradeoff needs explicit assessment
+before phase acceptance or a throughput claim.
+
+Actual synthesis/STA variants are
+`build/ics55-tiny-2026-10-10-10-05-42d5beccb726/` and
+`build/ihp130-tiny-2026-10-10-10-05-bff91e3529ba/`.
+Both emit `TINY_R2_P4_BINDING_PASS` and pass netlist boot. Their raw reports are
+`sta/opensta/timing_metrics.rpt`, `p4-clock-bindings.rpt`, `retrosoc_sta.log` and
+`meta/metrics.json`.
+
+| PDK | Cells | Reported library area | Max WNS / TNS (ns) | Min WNS / TNS (ns) |
+| --- | ---: | ---: | ---: | ---: |
+| ICS55 | 206311 | 1803352.980799 | -40.55 / -518700.31 | -0.02 / -11.85 |
+| IHP130 | 216216 | 9218095.882797 | -97.46 / -1626503.88 | -0.17 / -142.67 |
+
+Timing is not closed. Library area is not placed/routed die area. Lint baseline
+comparisons remain non-blocking observations: Tiny ICS55 466 new signatures,
+Tiny IHP130 410 new, Mini ICS55 1537 new / 118 resolved, all with zero increased.
+Compared with the preceding P3 lint runs, newly observed signatures are intentional
+empty named output connections: Tiny's unused master IDs, exclusive/power/fence
+outputs, plus Mini's ignored `reset_pending_o`. Tiny is cacheless with committed
+stores, no A/SBA and no Xh3Power, so these outputs do not add interfaces or clock
+gating. Baselines and metric policy were not edited. The logs retain the known
+generated-file clock skew and successful ABC Liberty fallback observations.
+
+The P3 eight-configuration matrix and three-cold-start campaign were not rerun.
+No place/route, extracted/PVT/CDC/RDC signoff, high-rate qualification, new PDK
+rollout or PDF build/visual check was performed. Publication snapshot enforcement
+remains enabled; the reviewed PDF source revision was not advanced. Mini
+DMA-to-WS2812 stays unsupported under option A, and product DMA/IRQ budgets
+remain for a supported integration. P5 and later features are not implemented.
diff --git a/docs/ip/tiny-soc-r2-p3-review-fixes.md b/docs/ip/tiny-soc-r2-p3-review-fixes.md
index cc06926b..2c0568af 100644
--- a/docs/ip/tiny-soc-r2-p3-review-fixes.md
+++ b/docs/ip/tiny-soc-r2-p3-review-fixes.md
@@ -3,6 +3,9 @@
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.
+Earlier sections retain their original development status and attribution;
+the source-bound campaign below records completed validation on `3e46cc8`.
+The final section separately records the subsequent two HAL race fixes.
## Retained Tiny Icarus finding: closed
@@ -182,6 +185,10 @@ STA observations qualify a new physical operating point.
## Compiler-matrix execution corrections (2026-10-09)
+The intermediate snapshots below are historical. The final source-bound results
+later in this record supersede their in-progress status without relabeling any
+failed attempt or earlier artifact.
+
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
@@ -286,3 +293,280 @@ 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`.
+
+## Final source-bound P3 matrix (2026-10-10)
+
+After the external style commit and the publication/configuration corrections
+settled, the final campaign was run against commit
+`3e46cc80e2d2243233d284b7cd9583714b0773bb`, source digest
+`a7fc5112cd6f20df956243c4d081a6a31ede6192cd270e3c13340557b0e58776`.
+Its master record is
+`build/ics55-tiny-performance-2026-10-09-17-10-53cc6674d532/meta/tiny-r2-p3/matrix/attempt-3jy5g5vo/record.json`;
+the final report is in the same root at `meta/tiny-r2-p3/report.json`.
+
+All eight cases pass image collection, normal Tiny acceptance and three cold
+Verilator runs. The selected native Icarus cases also pass three cold runs:
+
+- `compat-os-flat`: 7464.762, 7891.159 and 7428.430 seconds;
+- `perf-o2-lto-flat`: 5841.742, 5890.497 and 6149.375 seconds;
+- `perf-o2-plain-banked`: 5951.447, 6345.716 and 5964.039 seconds.
+
+The collector validates Tiny `TEST_STATUS`/`SIM_TEST_PASS`, forbidden markers,
+matched retained image/model/tool artifacts, source digests, three repetitions,
+and cross-simulator sample identity. `report.json` remains
+`measurement_complete`; the ledger's human phase gate is still required.
+
+The earlier 09-25, 15-10 and 16-45 campaigns remain historical: source changes
+during execution included the `352b200` style commit, the configuration/publication
+repairs, and the performance profile's trailing-newline change. Their raw passing
+samples and provenance failures are retained and are not relabeled as final
+evidence. The final campaign uses the complete committed corrections in `3e46cc8`.
+
+### Current validation and commands
+
+The tested primary platform is TINY/ICS55, external SAFE24 with HAVE_PLL=YES
+but the PLL parked off. CPU and the existing 128 KiB main SRAM remain at SYS24.
+The IHP130 compatibility profile keeps no PLL. Dependencies, warning baselines,
+metrics policy, hardware integration and the frozen specification blob
+`7e19de49d49efaafacccfa85671e314d53dd15a4` are unchanged by this validation.
+
+After activating `.cache/retrosoc/development/tiny-r2-p1/activate.sh`, the matrix
+and final report commands were:
+
+```sh
+make CONFIG=configs/benchmark/ics55-tiny-performance.mk SOC=TINY PDK=ICS55 \
+ BUILD_TIMESTAMP=2026-10-09-17-10 SOC_SIM_TIME=21600 JOBS=3 \
+ TINY_P3_RESUME=NO tiny-r2-p3-matrix
+make CONFIG=configs/benchmark/ics55-tiny-performance.mk SOC=TINY PDK=ICS55 \
+ BUILD_TIMESTAMP=2026-10-09-17-10 tiny-r2-p3-report
+```
+
+The current full Pytest run reports **1629 passed, 1 skipped** in 4200.10 seconds.
+The one skipped PDF inspection test requires `pypdf`, absent from this environment;
+it is not a hardware or simulation pass. Its command, complete log and result are
+in the matrix root under `meta/tiny-r2-p3/quality/pytest.{log,json}`. The previous
+4 failures and 94 fixture errors remain in their old logs; they are resolved by
+the configuration declaration and publication/source-fixture corrections in
+`3e46cc8`. The focused correction check also passed 149 tests with the same one
+PDF skip. No PDF or publication source revision was advanced by these checks.
+
+`ruff check .`, dependency-lock validation and `git diff --check` pass. The
+current C format/policy/host and RTL format/style/readiness checks pass; their
+exact Make command and outputs are retained under
+`meta/tiny-r2-p3/quality/policy.{log,json}`. These are the repository's mechanical
+quality checks, not complete MISRA certification; no new deviation is recorded.
+
+Regression dry-runs selected TINY explicitly for both PDKs and MINI with
+`--behavioral-only`. Tiny nightly resolves to the same commands as PR, so it was
+not executed a second time. The final executions were:
+
+```sh
+BUILD_TIMESTAMP=2026-10-09-19-55 python3 scripts/regress.py --root . --suite pr --soc TINY --pdk ICS55
+BUILD_TIMESTAMP=2026-10-09-19-56 python3 scripts/regress.py --root . --suite pr --soc TINY --pdk IHP130
+JAVA_TOOL_OPTIONS=-Dsbt.server.forcestart=true BUILD_TIMESTAMP=2026-10-09-19-57 \
+ python3 scripts/regress.py --root . --suite pr --soc MINI --pdk ICS55 --behavioral-only
+```
+
+All three exit zero. Current evidence roots and results are:
+
+| Scope | Evidence root below `build/` | Result |
+| --- | --- | --- |
+| Tiny ICS55 PR | `ics55-tiny-2026-10-09-19-55-42d5beccb726/` | `meta/tiny-r2-p3/regress-ics55.{log,json}`; behavioral/JTAG, netlist boot, synthesis and STA commands pass |
+| Tiny IHP130 PR | `ihp130-tiny-2026-10-09-19-56-bff91e3529ba/` | `meta/tiny-r2-p3/regress-ihp130.{log,json}`; same target-specific gates pass |
+| Mini ICS55 behavioral-only | `ics55-2026-10-09-19-57-d1cacf6d3b62/` | `meta/tiny-r2-p3/regress-mini.{log,json}`; SDRAM Verilator and Icarus assembly boot pass |
+
+Each Tiny PR's HAVE_SVA=YES Verilator acceptance has its separate configuration
+variant, identified in the retained command/log. Mini's SDRAM run uses
+`dc24d22ad53d` and assembly boot uses `ec9e1f2e9c2c` at the same 19-57 timestamp.
+Mini behavioral-only supplies no synthesis, netlist, STA or physical evidence.
+
+The additional IHP130 performance/banked normal-firmware check also passes:
+
+```sh
+make CONFIG=configs/ci/ihp130-tiny.mk SOC=TINY PDK=IHP130 \
+ BUILD_TIMESTAMP=2026-10-10-00-13 SW_ISA_PROFILE=TINY_PERF SW_OPT=O2 SW_LTO=NO \
+ LINK_TYPE=ld2_tiny_banked SIMU=VERILATOR firmware sim
+```
+
+Its root is `build/ihp130-tiny-2026-10-10-00-13-8648aa4845d9/`, with the exact
+command/result at `meta/tiny-r2-p3/performance-banked.{log,json}` and strict Tiny
+simulation check at `sim/verilator/result-sim-check.json`. This is normal
+firmware compatibility; it is not an additional IHP130 compiler measurement matrix.
+
+The ICS55 platform auditor captured inputs before this execution and successfully
+produced `meta/tiny-ics55-p1/report.json` in the actual APP=ci_smoke variant. This
+reuses the platform audit under P3; its original phase label is unchanged. Metrics
+were explicitly collected in each actual synthesis/STA variant rather than the
+regression runner's final default-bringup observation directory:
+
+| TINY PDK | Cells | Reported library area | Max WNS (ns) | Max TNS (ns) |
+| --- | ---: | ---: | ---: | ---: |
+| ICS55 | 197677 | 1786686.260799 | -41.14 | -508669.53 |
+| IHP130 | 208547 | 9109320.296998 | -94.28 | -1594880.00 |
+
+Both min WNS/TNS values are zero in these smoke reports. Full reports are at
+`sta/opensta/timing_metrics.rpt` and `meta/metrics.json` within the corresponding
+APP=ci_smoke roots. Library area is not placed/routed die area; negative margins
+remain unclosed. No 96/192/240 MHz operation, routed/PVT signoff or physical
+qualification is claimed, and no half-rate main-SRAM fallback is introduced.
+
+Lint comparisons remain non-blocking observations: ICS55 Tiny has 459 new
+signatures relative to its baseline, IHP130 Tiny 403, and Mini ICS55 1536 new /
+118 resolved; all three have zero increased signatures. Counts are not attribution
+of every warning to P3. The reports are in each timestamp's lint variant
+(`303f95040df3`, `2dc2e5c747e5`, and `56f28cd9230c` respectively), under
+`meta/rtl-lint-warnings.json`. Baselines were not edited.
+
+### Software measurements and remaining boundaries
+
+Each row below uses the retained P3 workload (1024 words, four jobs, 4096 CPU
+iterations, seed `0x12345678`). Cycle counts are raw measured windows; the report
+also retains the separate empty-window calibration, retired instructions, bus
+observations and CPI. These are different compiler/layout binaries, not a
+matched-binary architectural speedup claim.
+
+| Case | BIN bytes | CPU cycles | Copy cycles | Burst-16 DMA cycles | Chunk-256 cycles | Observed stack bytes |
+| --- | ---: | ---: | ---: | ---: | ---: | ---: |
+| compat-os-flat | 11132 | 98682 | 164096 | 18881 | 511389 | 692 |
+| perf-o2-plain-flat | 10148 | 73951 | 127157 | 16520 | 373812 | 648 |
+| perf-o2-lto-flat | 8592 | 73974 | 127180 | 13980 | 339167 | 552 |
+| perf-o3-plain-flat | 10404 | 73951 | 127157 | 16058 | 372690 | 628 |
+| perf-o3-lto-flat | 9872 | 73974 | 127180 | 13980 | 339167 | 564 |
+| perf-os-plain-flat | 8000 | 74064 | 139487 | 18421 | 394248 | 692 |
+| perf-os-lto-flat | 6360 | 74064 | 139484 | 14169 | 343769 | 584 |
+| perf-o2-plain-banked | 10184 | 73954 | 114875 | 16523 | 374016 | 648 |
+
+Compare compatible Os with performance Os to hold optimization/layout fixed;
+compare performance O2 with/without LTO to isolate that selection; compare the
+two O2/no-LTO rows for placement. BIN bytes exclude BSS, and observed stack use
+does not prove a static upper bound. Each image reserves at least 4096 stack
+bytes. Bank conflicts remain unavailable because P4 hardware is not implemented.
+The original P1 binaries and their historical phase/source identities are retained.
+
+Current Mini PIO compatibility is additionally retained at
+`build/ics55-2026-10-09-20-00-d0c568d0142e/meta/tiny-r2-p3/pio-retry/evidence.json`.
+The initial default 180-second attempt emits SIM_TEST_TIMEOUT despite emulator
+exit zero, and fails the simulation checker. A retry of the identical model and
+BIN with `--fast-flash -t 1800` passes in 323.930 seconds, including 4/16/33/65-word
+frames, IRQ count 5, exact waveform/reset-low checks, underflow/abort recovery,
+and observer totals `frames=6 completed=4 underflows=1 aborts=2 dma_starts=0`.
+The evidence records the before/after model and BIN hashes, command, manifest,
+compile/run logs and a post-build recheck of 487 compiled input sizes/mtimes.
+It is explicitly a post-build audit; neither the failed timeout nor the original
+unsupported Mini DMA attempt is relabeled as a passing run.
+
+Product-integrated WS2812 DMA/IRQ service budgets remain deferred under approved
+option A to an actual supported route, including Tiny R2-P6. Full-product,
+physical, silicon and publication-PDF qualification remain outside these results.
+Human review and acceptance of TINY-R2-P3 are required before authorizing any
+R2-P4 implementation; this execution does not automatically advance a phase.
+
+## Subsequent P1 HAL race fixes (2026-10-10)
+
+This approved follow-up starts from `3e46cc80e2d2243233d284b7cd9583714b0773bb`
+and preserves the preceding uncommitted documentation/evidence updates. The
+specification remains blob `7e19de49d49efaafacccfa85671e314d53dd15a4`.
+Only `crt/src/hal/dma.c`, `crt/src/hal/ws2812.c` and
+`tests/c/test_runtime.c` change executable/test source. No public interface,
+register, RTL, profile, dependency, warning baseline or metric policy changes.
+
+- WS2812: completing between INTR_STATE and STATUS reads no longer produces a
+ false I/O error. Idle without an observed sticky DONE retains ownership and
+ returns to the caller; the next bounded service checks error, abort, deadline
+ and completion again. No refill is admitted to an idle core. DMA response
+ draining and source-buffer lifetime rules are unchanged.
+- DMA: one outer interrupt save/restore spans legacy TCD configuration,
+ descriptor publication and START, including ownership rejection. Nested HAL
+ helpers preserve the saved mask; the blocking completion wait is outside this
+ critical section. Test-only IRQ hooks model a pending ISR at mask restoration
+ and do not appear in target builds.
+- Deterministic production-HAL host tests cover an ISR trying to lease the
+ channel during TCD publication, rejection without modifying an existing lease,
+ an initially masked caller, completion between WS2812 register reads, delayed
+ sticky DONE, genuine error/abort and expiry while idle. Existing final-B drain,
+ timeout ownership and 16-word FIFO admission tests remain enabled.
+
+New evidence is rooted at
+`build/ics55-tiny-2026-10-10-09-00-b4aa59f3e5d9/meta/tiny-r2-p3-races/`.
+The timestamp is a fixed build label; each structured result records actual UTC
+start/end times. `source.json` records the base revision, dirty state, locked
+tool identities, input hashes, source archive and worktree patch. Its combined
+ICS55/IHP130 source/test/spec digest is
+`dfa171fc1adbdfc5c7c9d31ef4ac7ba1a66710caac0fb13b869ddac89186b2f7`.
+This digest's input set differs from the earlier compiler-matrix input set.
+
+Before fixes, `host-red.{log,json}` records host test exit 10 (TCD race).
+After the TCD fix alone, `host-ws-red.{log,json}` records exit 11 (WS2812 race).
+Both fail the enclosing Make command. `host-green.{log,json}` passes after both
+fixes; `policy.{log,json}` additionally covers the final error/abort/deadline and
+interrupt-mask cases, C formatting and policy checks. `focused.{log,json}` has
+156 passing tests, including P3, Tiny baseline, DMA/register parity, WS2812 RTL
+and publication API behavior. No new MISRA deviation is introduced. These are
+partial project checks, not complete MISRA certification.
+
+Commands run with the existing locked environment:
+
+```sh
+source .cache/retrosoc/development/tiny-r2-p1/activate.sh
+make sw-format-check sw-policy-check sw-host-test
+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 \
+ tests/test_publication_api_bundle.py
+ruff check .
+git diff --check
+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
+BUILD_TIMESTAMP=2026-10-10-09-00 JOBS=3 JAVA_TOOL_OPTIONS=-Dsbt.server.forcestart=true \
+ python3 scripts/regress.py --root . --suite pr --soc TINY --pdk ICS55
+BUILD_TIMESTAMP=2026-10-10-09-00 JOBS=3 JAVA_TOOL_OPTIONS=-Dsbt.server.forcestart=true \
+ python3 scripts/regress.py --root . --suite pr --soc TINY --pdk IHP130
+BUILD_TIMESTAMP=2026-10-10-09-00 JOBS=3 JAVA_TOOL_OPTIONS=-Dsbt.server.forcestart=true \
+ python3 scripts/regress.py --root . --suite pr --soc MINI --pdk ICS55 --behavioral-only
+JAVA_TOOL_OPTIONS=-Dsbt.server.forcestart=true 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 BUILD_TIMESTAMP=2026-10-10-09-00 JOBS=3 firmware sim
+```
+
+Regression dry-runs passed; Tiny PR and nightly select identical commands, so
+the long run is not duplicated. All three regression executions passed once;
+their exact commands and verdicts are retained as
+`regress-{tiny-ics55,tiny-ihp130,mini-ics55}.{log,json}` in the new evidence root.
+Durations are 3210.491 seconds (Tiny ICS55), 2205.505 seconds (Tiny IHP130),
+and 3170.591 seconds (Mini ICS55 behavioral-only). Mini PIO passes once in
+423.766 seconds including build, with 4/16/33/65-word frames, five IRQs and
+observer totals `frames=6 completed=4 underflows=1 aborts=2 dma_starts=0`.
+Its variant is `build/ics55-2026-10-10-09-00-d0c568d0142e/`.
+
+`audit.py` and `evidence.json` retain the final artifact audit: all 1287 captured
+source/test/spec inputs are unchanged, 487 Mini PIO compiled inputs retain their
+recorded sizes/mtimes, and the firmware/model/log/result hashes are recorded.
+The Mini model audit is explicitly post-build. Eight ordinary regression
+simulations are independently checked against their executable success marker,
+command exit and forbidden-error rules. All selected targets, including the
+current Mini assembly fixture, require `SIM_TEST_PASS`; UART output is not used
+to infer success. This evidence is in addition to the separate PIO audit.
+
+Current Tiny synthesis/netlist/STA results are in
+`build/ics55-tiny-2026-10-10-09-00-42d5beccb726/` and
+`build/ihp130-tiny-2026-10-10-09-00-bff91e3529ba/`. Both netlist checks pass;
+STA command success does not mean timing closure. Observed max WNS/TNS remain
+-41.14 ns / -508669.53 ns (ICS55) and -94.28 ns / -1594880.00 ns (IHP130),
+with min WNS/TNS zero. Raw reports are `sta/opensta/timing_metrics.rpt`.
+Lint baseline comparisons still fail as non-blocking regression observations:
+459 new signatures for ICS55 Tiny, 403 for IHP130 Tiny, and 1536 new / 118
+resolved for Mini, with zero increased signatures. No baseline was changed.
+Build logs also retain a roughly four-second generated-file clock-skew warning;
+these were fresh variant builds, and the input audit passed.
+
+The old compiler/layout matrix, full Pytest and earlier timing observations stay
+bound to `3e46cc8`; they are not reattributed to this follow-up. The compiler
+matrix and full Pytest are not rerun for these isolated C changes (no build or
+Python implementation changes). Product-integrated WS2812 DMA/IRQ budgets remain
+deferred under option A; no Mini DMA-to-MMIO route is added. Human P3 acceptance
+and separate approval are still required before R2-P4. Per the maintainer's
+instruction, long checks default to a single execution; additional repetitions
+need a concrete failure/inconsistency or an explicit frozen acceptance rule.
diff --git a/docs/ip/tiny-soc-r2-shared-integration.md b/docs/ip/tiny-soc-r2-shared-integration.md
new file mode 100644
index 00000000..9bff33b9
--- /dev/null
+++ b/docs/ip/tiny-soc-r2-shared-integration.md
@@ -0,0 +1,253 @@
+# Tiny R2-P6 shared integration
+
+Implementation in progress from `b3b30a72cd6ef5340c28e5044c96cd13258ec5a6`,
+against specification blob `7e19de49d49efaafacccfa85671e314d53dd15a4`.
+The maintainer approved the single-phase P6 preflight. No phase acceptance,
+new operating-frequency claim, commit or publication promotion follows.
+
+Default `configs/ci/ics55-tiny.mk` remains SAFE24 with its PLL parked off;
+`configs/ci/ihp130-tiny.mk` remains no-PLL compatibility. CPU and all 32 main
+SRAM macros share SYS. Crypto adds its six private 4 KiB macros on the current
+SAFE24 PCLK alias, without reducing the 128 KiB main aperture.
+
+## Product boundary
+
+The eight-channel DMA enables requests 0-8 and 12/13 (`0x31ff`). I2C1 requests
+9/10, DVP11 and future requests stay unsupported. Default owners are UART0=0,
+I2C0=1, camera reservation/explicit memory borrowing=2, serialized bulk=3,
+Crypto=4/5 and audio=6/7. The application lease on channel 3 persists across
+all WS2812 refill batches; CRC and XPI must acquire it only after release.
+SPI16/17 remain outside the existing V2.0 selector ABI and are rejected by
+the SDK. This does not claim hardware rejection of arbitrary raw extended
+selector writes: the common DMA specification explicitly records their legacy
+truncation and requires V2.2 identity/capability guards before SPI integration.
+
+The 20 independent APB branches occupy fabric targets 5-24. SRAM remains
+0-3, XPI memory 4, and decode/protocol errors are 25/26. The original three
+external owners remain; owner 2 is now the actual SDIO private DMA producer.
+P5 response retention, write drain, fault routing and ownership are preserved.
+The 21 system reset leaves exclude the independently released AUDIO reset;
+neither an absent audio reference nor Crypto initialization holds CPU reset.
+
+Gen1 default GPIO routes are SDIO0-5, UART8/9, I2C10/11, audio12-17,
+PWM18-25 (plus its frozen ALT1 outputs), WS26 and XPI chip selects29-31.
+Unfrozen UART/I2C alternate inputs, camera, SPI and CLKOUT remain inactive.
+The historical ASIC wrapper still has legacy dedicated UART/XPI-CS ports and
+its old power-pad inventory. It is not a QFN64 binding. This explicitly
+approved compatibility boundary leaves the complete package/power wrapper
+to final physical implementation, without changing the frozen package table.
+
+## Shared correctness and software
+
+Tiny's shared DMA validator now accepts integrated streams and eight channels.
+Known absent I2C1/SDIO1 and UART flow-control operations fail before MMIO.
+SD/SDIO enumeration caps a protocol clock request at half the actual input
+frequency; the existing low-level clock calculator still rejects invalid
+requests. The bounded SDSC CSD multiplier uses a 32-bit shift factor inside
+the unchanged 64-bit capacity calculation, avoiding an absent RV32 runtime
+helper without replacing the locked GNU toolchain.
+The SD Memory RCA extraction uses raw R6 frame bits 39:24; the preceding
+31:16 selection incorrectly shifted the assigned card address by eight bits.
+
+The independent P6 I2S payload fixture reproduced an existing receive-bit
+assembly defect in both simulators. Completion now follows capture of the
+current sample's LSB at the LRCK boundary. Both widths preserve the existing
+register ABI, FIFO/CDC topology and master-only integration. Shared Mini/APU
+checks remain required; this is not an I2S slave extension.
+
+Adding partial external GPIO drivers exposed a locked-Verilator simulation
+limitation: a packed `tri1` bus loses pulls on its otherwise undriven bits.
+The original I2C test consequently saw released SCL/SDA low and timed out.
+A minimal reproduction fails in Verilator and passes in Icarus; explicit
+independent scalar board nets retain both weak pulls and driven readback in
+both tools. Tiny's generated testbench bindings use those scalar nets and an
+observation-only packed view. ASIC ports, pad cells, power counts and I2C RTL
+are unchanged by this verification repair.
+
+The Tiny `HAVE_SVA=YES` flow now also emits `+define+HAVE_SVA`, which the
+owned P4/P5 assertions use. Previously it only omitted the Common
+`SV_ASSRT_DISABLE` define; focused fixtures explicitly enabled the owned
+assertions but ordinary firmware flows did not. Mini flag behavior is unchanged.
+
+The first full integration reached Crypto but timed out before audio completion.
+A smaller actual-SoC diagnostic isolated an I2S status load fault: Tiny's
+AXI-to-APB bridge retained the preceding write strobes on reads. Read PSTRB is
+now zero as required by APB4, while writes retain their byte lanes. A directed
+write/read sequence checks full/partial writes with a strict receiver.
+The Tiny firmware limits exception skipping to its explicit CPU probes;
+unexpected later load/store faults fail immediately instead of being masked.
+
+Audio shutdown keeps the consumer running while aborting/draining its leased
+TX DMA session. A held stream beat cannot retire into an already stopped/full
+audio FIFO. Only after DMA retirement does the acceptance image disable I2S
+and flush. After a missing-clock flush timeout it polls the outstanding command
+through clock recovery instead of issuing another protected COMMAND write.
+
+SDIO's reused card model originally required manual enumeration/data stimulus;
+its default ACMD41 never reports READY. The command trace confirmed the
+initialization loop. An opt-in SDHC response profile and the existing explicit
+data API now support the Tiny board fixture while preserving standalone defaults.
+The fixture checks wire-command/data-policy agreement and drives only external
+pins. Card timing/protocol/electrical qualification remains excluded. The native
+Icarus parser also needs an equivalent explicit enum branch in this model.
+Tiny Icarus builds now retain separate testbench/header dependencies, including
+the shared card model and P6 board include. Its serial observer emits measurement
+records on ordinary runs; TEST_STATUS and strict error checks remain authoritative.
+
+The 12 MHz command case exposed a running-clock start-bit duplication in the
+shared SDIO sender. Output admission now waits for its first scheduled launch;
+the independent pad sampler covers both admission phases. The card sampler
+retains the complete frame across host OE release, including the pulled-high
+end bit. This preserves the command/register ABI and does not extend card modes.
+
+The full four-lane SDIO block exposed a separate transmit pause defect: when
+memory delayed the next word, a late clock stop corrupted its first bit
+(`0x57` became `0xD7`) and caused CRC rejection. The shared data engine now
+requests the pause before that launch; the clock wrapper honors it while
+draining the current high half-cycle. A full 1024-byte fixture with read-beat
+gaps checks payload, CRC and descriptor retirement. Its scalar three-state
+variant also caught card-model assumptions about unused lanes being zero;
+token and partial-tail checks now account for their pulled-high state.
+
+The receive roundtrip isolated premature descriptor completion after one
+64-byte write burst. SDIO private DMA retains ownership until every payload
+burst retires or an error is reported. A separately reproduced receive stop
+delay shifted the next word (`0x13579BDE` to `0x26AE37BD`); immediate stop
+qualification prevents the extra launch. The card's read-CRC sequencer also
+skipped one bit and is corrected independently. Delayed descriptor/payload
+tests check full readback, CRC, descriptor retirement and both simulators.
+These repairs add no masters, channels, descriptor fields or operating modes.
+
+## Validation and historical publication
+
+Workloads keep the ordinary Tiny boot, SRAM/DMA tail, trap, interrupt, RTC,
+I2C NACK, JTAG and watchdog-reset checks. P6 adds real GPIO8/9 UART echo;
+eight-channel/unsupported-request checks; RNG qualification rejection and IRQ;
+CRC DMA, empty input and byte tails; Crypto initialization, AES/SHA/public RSA,
+channels 4/5 and zeroization. Audio uses the fixed 18.432 MHz external reference
+and 48 kHz master presets, checks 256 stereo frames at both 16 and 24 bits,
+and drives paced 33/65-word WS frames in each case. Channel 2 concurrently runs
+bounded 256-byte jobs, then four-descriptor chains; channels 6/7 own audio.
+The board independently decodes I2S/WS pins, checks FIFO/service bounds and
+stops AUDIO low/high for recovery tests. SDIO uses 400 kHz enumeration/one-bit
+PIO and 12 MHz four-bit DMA, transfers 1024 bytes in each DMA direction and
+512 PIO bytes, overlaps a separate 1024-byte central-DMA job and checks IRQ,
+CRC-error, timeout and recovery behavior.
+
+MCYCLE/MINSTRET measure the audio/WS service loop after prefill; elapsed CLINT
+time also includes its bounded preparation. The independent AXI observers count
+handshakes over the complete firmware run, including boot/control accesses;
+they are not payload-only bandwidth measurements. Write counts mean accepted
+strobes, not rollback guarantees. Descriptor fetch bytes, per-owner AR/AW/W/R/B
+backpressure, WS service gaps and actual SDIO/DMA overlap with CPU progress
+remain explicit. `scripts/tiny_r2_p6.py` retains ELF/hex/bin/map, linker/generated
+software inputs, simulator binaries/filelists, manifests, logs and their hashes.
+Both simulators on both PDKs must have successful commands, Tiny's terminal
+`SIM_TEST_PASS`, all workload/independent-observer records and no forbidden
+error marker. Component diagnostics never satisfy that collector.
+
+The current Tiny PR and nightly dry-run command matrices are identical for
+each PDK. After focused/quality checks, execute full Pytest once and one full
+PR per PDK in independent build directories; these broader gates may run in
+parallel but must all finish before acceptance. Collect warnings/metrics with explicit `APP=ci_smoke`,
+`SYNTH=YOSYS`, `STA=OPENSTA` and the same build timestamp. Do not rerun an
+identical nightly matrix. Shared I2S and SDIO standalone/formal checks plus the
+affected Mini firmware path remain separate compatibility evidence. Core STA
+must execute and bind all 38 exact macros; its slack remains observational.
+The legacy IHP130 LibreLane floorplan generator still places the 32 main macros
+only. It has not placed/connected the six private Crypto macros or implemented
+the final QFN64 wrapper. Full-chip APR/PDN/package work remains outside this
+campaign; neither its old floorplan nor smoke STA establishes P6 physical closure.
+
+Development artifacts begin at the manually selected `2026-10-12-12-00`
+build label, with subsequent diagnostic labels recorded in each command.
+Those labels are not wall-clock measurements; JSON start/end times are.
+Some diagnostic firmware directories were reused while finding defects and
+must not be rebound to later binaries. The `11-42` snapshot's Make-format gate
+failed before long validation. The fresh `2026-10-12-11-50` campaign retains
+its input snapshot under `build/ics55-tiny-2026-10-12-11-50-2eb98c0e1fae/meta/tiny-r2-p6/`.
+Failed attempts retain their own verdicts and are not acceptance evidence.
+
+Both original PR attempts passed complete Verilator/JTAG acceptance and then
+stopped at native Icarus elaboration: `s_token_lanes` referenced `s_data_bytes`
+before its declaration. The declaration relocation changes no expression or
+other model byte. `verification-update.json` retains the second snapshot;
+the collector checks that exact equivalence and rejects any DUT, firmware,
+constraint, tool or other model-behavior change. Only the card model and its
+collector/tests differ. Original Verilator and Mini results retain their
+original source identity. Native Icarus and the remaining physical smoke stages
+resume from the failed step; the original failed PR verdicts are not rewritten.
+Full Pytest retains its original collection, with the equivalent model edit
+during execution; the updated 48-case P6 suite separately covers the native
+parser and fail-closed provenance checks. Counts overlap and must not be added.
+
+The one full Pytest attempt reports 1634 passed, one optional `pypdf` skip,
+five failures and 60 fixture errors. These reduce to two source-consumer issues:
+Mini's publication parser counted inactive Tiny enum branches as Mini channel
+allocations, and the commercial Tiny overlay retained its pre-P6 two-domain
+allowlist. The parser now selects and checks Mini's actual bulk aliases; no book
+identity, chapter, PDF or historical evidence changes. The commercial policy
+and Tcl inventory now include the canonical AUDIO root; no licensed flow or
+qualified I/O run is implied. `consumer-update.json` binds this separate scope
+and rejects changes to RTL, firmware, models or smoke constraints.
+
+Every one of the 65 original failed/error nodes passes in the explicit repair
+selection. The complete affected publication/commercial suite reports 512 passed
+and one optional PDF-library skip; 35 selected provenance tests also pass.
+These counts overlap the full attempt and each other. There is no claimed clean
+full-suite rerun: unaffected long tests were retained with their original scope.
+
+### Completed Verilator observations (partial campaign)
+
+`verilator-observations.json` and `report-functional-before-physical.json` bind
+the following values to successful command/log artifacts. Each row verifies 256
+stereo frames and two WS frames, with 64 completed background jobs. These values
+are functional observations, not phase acceptance or timing closure.
+
+| PDK | Audio bits | Service-loop cycles | Retired instructions | Elapsed including preparation (us) |
+| --- | ---: | ---: | ---: | ---: |
+| ICS55 | 16 | 141917 | 41351 | 8044 |
+| ICS55 | 24 | 138353 | 40283 | 7892 |
+| IHP130 | 16 | 141917 | 41351 | 8065 |
+| IHP130 | 24 | 138350 | 40283 | 7903 |
+
+The independent WS service-gap maxima are 2375/2374 SYS cycles
+(98.96/98.92 us), below the 4800-cycle budget; FIFO peak occupancy is 15/16.
+Both runs observe 4124 cycles of actual SDIO/private-DMA versus central-DMA
+overlap, with 1185 CPU instruction-fetch handshakes in that interval. SDIO's
+external owner reads 1056 bytes and accepts 1032 write bytes, including its
+descriptor fetch/writeback. Central-DMA descriptor fetches total 16384 bytes.
+No cross-architecture speedup or physical rate is inferred from these samples.
+
+Both complete Icarus runs subsequently pass: ICS55 at 16036387 cycles and
+IHP130 at 16035743 cycles, with the
+same workload, WS, contention and bus observations as its Verilator run.
+Its UART observer leaves a partial human-readable line before the testbench's
+terminal `SIM_TEST_PASS Tiny cycles=...` record. `collection-update.json`
+records a collector/test-only correction to accept that exact terminal token
+without requiring a fresh line; JTAG-only/Mini markers, malformed cycle values
+and forbidden errors remain rejected. Forty focused collection/provenance
+checks pass. Raw logs, RTL, firmware, models and constraints are unchanged.
+
+Both netlist boot checks pass at 30030 cycles. Yosys synthesis and OpenSTA run
+for both PDKs and report the exact 32 main plus 6 private Crypto macro bindings.
+Observed top-cell/area values are ICS55 412515/2461020.464702 and IHP130
+433861/13320370.152627. Setup/hold remain negative: ICS55 WNSmax/WNSmin
+−84.93/−0.31 ns, IHP130 −165.41/−0.51 ns. These are smoke-flow observations;
+the frozen phase does not use synthesis slack as an intermediate blocking gate.
+Warning checks retain new Yosys/Verilator/RTL-lint and IHP130 Icarus signatures
+as non-promoted observations under the existing policy; no baseline was edited.
+
+The old Tiny publication remains frozen at its advertised technical revision
+`9c706ba0ff1da1028c299cfff573010a86dfd7ce`. Tests execute the original
+publication adapter/test snapshot `2805ea91833a6bede32f3a82df3d17d20eb5ec77`
+and, separately, the later P5 contract snapshot `b3b30a72cd6ef5340c28e5044c96cd13258ec5a6`.
+Neither snapshot becomes P6 publication evidence. Current P6 source must be
+rejected by the historical collector. The reset mutation checks still run
+against the current target RTL. No frozen chapter, reviewed SHA or old PDF is
+rewritten; unavailable PDF/media inputs remain a separate publication gap.
+
+P7 RCU/variable clocks, PWM dependency upgrades, alternate-input ABI, slave
+audio, PSRAM, DVP and later extensions remain outside this phase. Timing is
+observational before complete-product qualification; exact macro/clock/source
+binding and successful functional verdicts remain mandatory.
diff --git a/docs/ip/tiny-soc-verification.md b/docs/ip/tiny-soc-verification.md
index f95f9e08..4412e430 100644
--- a/docs/ip/tiny-soc-verification.md
+++ b/docs/ip/tiny-soc-verification.md
@@ -1,14 +1,486 @@
# Tiny MCU Verification Record
-## TINY-R2-P3 implementation in progress (2026-10-08–09)
+## TINY-R2-P7 source-reset integration contract (2026-10-14)
+
+The follow-up review identified a missing source-side reset proof, not a
+reproduced product failure. The AXI bridge does not support arbitrarily
+resetting an active source while leaving its slave session alive. Flushing
+the FIFO cannot cancel a request already accepted by that slave. Production
+Tiny prevents that sequence: DMA local reset requires engine, admission and
+fabric drain before resetting the source; XPI's SYS source reset is a common
+system reset that also resets its MEM endpoint. Neither production bridge
+uses warm clear. These preconditions are part of the integration contract,
+not a claim of unrestricted unilateral-reset support by the standalone bridge.
+
+`tests/rtl/tiny_p7_source_reset_tb.sv` uses the production DMA master,
+admission, CDC, lifecycle controller and reset tree. The test extracts the
+actual top-level instances, drain/block synchronizers and generated DMA/XPI
+reset bindings, so a hand-copied reset policy cannot silently replace them.
+An independent outstanding scoreboard survives local reset and rejects
+premature reset. The AXI target is a controllable behavioral slave; unrelated
+CPU/APB/SDIO work and the DMA completion mailbox are idle. This is a focused
+integration test, not a full-SoC firmware or formal CDC/RDC signoff result.
+
+The scenarios cover stalled AR/AW, partial W, downstream W backpressure,
+delayed R/B, source-held R, drain timeout without reset, post-reset reads and
+writes, and common system reset during an outstanding read. The same binary
+is reused at SAFE24, XIN48, XIN96, PLL192 and PLL240 domain ratios. They test
+digital ordering only; the clock generators do not qualify a PLL or PDK rate.
+A negative run falsifies both idle observations and must trip
+`P7_SOURCE_RESET_UNDRAINED`; Icarus may return zero for sv2v's lowered fatal,
+so the forbidden marker and missing PASS still make that simulation invalid.
+
+Evidence for this correction is retained under
+`build/ics55-tiny-2026-10-14-06-00-ff4b5cef6eb5/meta/tiny-r2-p7-source-reset/`.
+The first focused attempt's test-driver failure is retained in `focused-1.log`;
+it is not a DUT failure or acceptance result. The final `directed.log` and
+`result-directed.json` record **54 passed in 191.71 seconds**. This includes
+ten positive source-reset runs (five ratios in each simulator) and two
+expected-negative broken-drain runs inside the two new Pytest cases.
+RTL format/style, clock/reset inventory, Ruff, both Tiny PDK PR dry-runs and
+`git diff --check` pass. The verification-only source comparison is retained
+in `verification-update.json`: only the two test files differ; 1728 other
+technical inputs and tool identities match. The design,
+firmware, configuration, dependencies and physical inputs are unchanged from
+the `06-00` source snapshot; the new tests do not relabel earlier simulations
+as new executions. Full Icarus firmware remains deferred to final Tiny
+qualification. Human P7 acceptance is still required.
+
+## TINY-R2-P7 response conservation correction (2026-10-14)
+
+The earlier review recommendation to give abort unconditional priority over
+source response retirement was incorrect. A cycle with an already exposed
+normal B/R handshake followed by an abort produced two completions for one
+request. Four read/write tests reproduced this in Verilator and Icarus. The
+failed candidate, diagnostics and source archive are retained below
+`build/ics55-tiny-2026-10-14-04-00-ff4b5cef6eb5/meta/tiny-r2-p7-response/`.
+The `04-00` synthesis/netlist/STA results belong to that rejected candidate;
+their successful boot does not close the protocol finding.
+
+The corrected bridge holds presented responses in source-owned registers.
+Remote reset cannot revoke VALID or rewrite their payload under backpressure,
+and a completed source handshake is never followed by a second completion.
+Only missing read beats receive DECERR, with RLAST at the original burst end;
+a reset write consumes the remaining W beats before emitting B. Outstanding
+tracking is one transaction per direction, matching the existing Tiny paths.
+The payload reset uses the five-edge source link barrier. An unfinished warm
+clear quarantines the bridge until a real destination reset; a FIFO clear
+cannot by itself cancel remote slave state or authorize re-use of late replies.
+Production DMA/XPI clear inputs remain tied low; no new software reset API,
+autonomous AXI timeout, DMA owner, or later-phase capability is introduced.
+
+The new tests distinguish completed/held responses from missing responses,
+cover simultaneous clear and response handshakes, remote reset with READY high,
+backpressure, partial bursts and late-response quarantine. That campaign's
+full directed selection passed 52 cases. Earlier 36-case and rejected-
+candidate passes did not cover these failures and are not proof of the repair.
+The failed same-cycle candidate and diagnostics remain in the response
+evidence directory.
+The final input snapshot is
+`build/ics55-tiny-2026-10-14-06-00-ff4b5cef6eb5/meta/tiny-r2-p7-response/final/source-final.json`.
+The `06-00` backend campaign is bound to the final source snapshot and all
+requested post-fix gates pass. Its variants are
+`build/ics55-tiny-2026-10-14-06-00-ff4b5cef6eb5/` and
+`build/ihp130-tiny-2026-10-14-06-00-8232fa11e7f3/`. ICS55
+synthesis/netlist/STA durations are `2108.388/1655.980/43.594 s`, with WNS
+max/min `-152.03/-0.31 ns` and TNS max/min `-5622052.50/-44.14`; IHP130
+durations are `1934.515/578.869/47.109 s`, with WNS max/min
+`-253.43/-0.49 ns` and TNS max/min `-10035324.00/-153.37`. Netlist boot
+emitted `SIM_TEST_PASS Tiny netlist boot` for both PDKs. These timing values
+remain observational and do not qualify closure or an operating point.
+Full Icarus firmware remains deferred by the maintainer to final Tiny
+qualification. It is not a current P7 gate; protocol correctness and human
+phase acceptance remain required.
+
+## TINY-R2-P7 final verification campaign (2026-10-13)
+
+The [P7 runbook](tiny-soc-r2-clock-reset.md) records the current implementation,
+exact profile-specific commands, workload definitions and source snapshot.
+The integrated design now drives actual SYS/MEM/PCLK consumers, CPU-only WFI
+gating, local reset barriers, CLKOUT and the single ICS55 PLL backend. It keeps
+the 32 main SRAM macros at CPU SYS and six private Crypto macros at PCLK.
+Future target capabilities remain absent. No later phase is authorized here.
+
+Development validation includes 68 passing Tiny P7/SYSCTRL/P5 directed tests
+and a later complete 52-case P7 directed pass,
+plus both simulators' real Hazard3 IRQ/debug wake, clock-pause and retained-debug
+warm-reset/aborted-command cases. RCU validation now rejects invalid or BUSY
+staging writes with PSLVERR, and the CLKOUT waveform test exposed and corrected
+enable/divisor ordering. HAL tests cover invalid, absent, busy, gated-reset and
+sticky-timeout/error boundaries. These short checks complement the final long
+flows; they do not replace full firmware or netlist acceptance.
+
+Source evidence is retained at
+`build/ics55-tiny-2026-10-13-17-05-f3e42d54c18b/meta/tiny-r2-p7/source.json`.
+The final command matrix and its retained corrections are listed in the runbook.
+Default ICS55/IHP130 and external XIN48/96 P7 firmware pass in both simulators.
+Each profile's cycle count and WFI/DMA/reset observations match across simulators.
+Both watchdog-corrected full Verilator firmware runs pass the unchanged P6
+peripheral/wire/AXI collector and JTAG. The two corresponding full Icarus
+replays were intentionally interrupted with exit 130 after the maintainer
+requested that this long-running gate move to the final Tiny phase; their
+partial logs and interrupted result records are retained, but they are not
+acceptance evidence.
+Both PDKs pass synthesis, structural macro binding and Tiny netlist boot.
+Current core STA remains observational and negative, with the coverage limits
+explicit in the runbook. These are historical campaign results; current phase
+acceptance requires the response correction above and human review. The full
+Icarus firmware gate belongs to final Tiny qualification. No high-rate physical operating point is
+qualified by these functional simulations or SAFE24 core STA.
+
+## Historical TINY-R2-P7 CDC/reset correction follow-up (2026-10-14)
+
+This checkpoint predates the response-conservation correction and is
+superseded by the `06-00` campaign above. It is not current-source acceptance.
+The P7 review identified two source-bound gaps. The XPI fabric target and bridge source side release with
+the SYS reset endpoint, while the XPI/MEM side retains its MEM leaf reset. The
+Tiny AXI CDC bridge synchronizes destination reset loss, invalidates its
+protected request/response state and blocks new source admissions. Its original
+single-DECERR completion claim was insufficient for held responses and bursts;
+the later response-conservation correction supersedes that claim. The existing DMA/XPI
+instances retain their explicit bridge boundaries and the XPI bridge now uses
+the split SYS/MEM reset mapping.
+
+The new `tests/rtl/tiny_axi4_async_bridge_tb.sv` passes under both Verilator
+and Icarus for normal read response, reset-aborted read/write, DECERR,
+no-replay and bridge recovery. The complete
+`PYTHONDONTWRITEBYTECODE=1 python3 -m pytest -q tests/test_tiny_r2_p7.py`
+selection eventually passed 52 tests after the later corrections; that count
+does not belong to the `01-20` implementation. `make --silent rtl-format-check rtl-style-check-all`
+and `git diff --check` pass. An ICS55 default-profile Verilator `comp` also
+passes; its existing PDK and simulator warnings remain observations. The
+checkpoint's synthesis, netlist boot and observational STA gates passed for
+both PDKs. Its historical evidence is retained under
+`build/ics55-tiny-2026-10-14-01-20-ff4b5cef6eb5/` and
+`build/ihp130-tiny-2026-10-14-01-20-8232fa11e7f3/`. ICS55 reports WNS
+max/min `-144.40/-0.31 ns` and TNS max/min `-4859636.50/-45.04`; IHP130
+reports WNS max/min `-257.81/-0.47 ns` and TNS max/min `-9807803.00/-100.68`.
+These remain observational negative timing results, not closure or operating
+point qualification. The long-running full Icarus firmware gate remains
+intentionally deferred to the final Tiny phase; no physical flow was run.
+
+## Historical initial P7 checkpoint, superseded (2026-10-13)
+
+The following incomplete checkpoint belongs to earlier development snapshots.
+Its unconnected/missing-path statements do not describe the final campaign.
+
+This checkpoint starts from committed source `43a3c8d453b6d41df3536719ebc6d1f4521bc7cd`,
+specification blob `7e19de49d49efaafacccfa85671e314d53dd15a4`, Tiny/ICS55
+profile `configs/ci/ics55-tiny.mk`, and the locked PWM revision
+`38efaf5e20f084a00148920087c60cadab61839b`. The dependency lock validator
+passes with digest `263402a34f2e315ca46ef4bf6366aab64935bd8de7abab3e469f513e988b0208`.
+
+The implementation adds the Tiny-owned RCU register bank and handwritten
+`rcu.h`/`rcu_regs.h`/`rcu.c` contract, reports SAFE24/domain-rate snapshots,
+connects committed PCLK/SYS rate reporting to the upgraded PWM and SDIO
+`CLOCK_ACTUAL` paths, and adds focused APB coverage for identity, capability,
+profile rejection/commit, status and write-only command behavior. Verilator
+and Icarus SYSCTRL focused tests pass; ICS55 Verilator and Icarus top-level
+compilation, Tiny firmware linking, C format/policy/host tests and the SDIO
+suite pass. Build logs are retained under the generated
+`build/ics55-tiny-2026-10-13-09-00-f3e42d54c18b/` and
+`build/ics55-tiny-2026-10-13-10-30-f3e42d54c18b/` roots.
+
+A Tiny-owned `tiny_clock_reset_subsystem` backend is now source-bound and
+filelisted. Its SAFE24/no-PLL focused test passes in both simulators and covers
+five-stage SYS/MEM/PCLK reset release, external-profile handling and rejected
+PLL requests. A second focused test exercises the behavioral PLL lock-timeout
+path in both simulators; it does not claim successful macro lock or frequency
+qualification. The backend is intentionally not advertised or wired into the
+CPU/fabric yet; its SYSCTRL profile/apply handshake is now connected, while
+the backend instance is now present in the Tiny top and its SYS output clocks
+the CPU, fabric, SRAM, AXI interfaces and system reset tree. XPI now crosses
+from SYS to the backend MEM clock through the Tiny-owned warm-flush AXI bridge.
+The RCU command checks CPU/fabric/target idle observations and the XPI bridge
+before accepting a transition. PCLK outputs are still not connected to all
+peripheral consumers.
+
+The focused command is
+`PYTHONDONTWRITEBYTECODE=1 python3 -m pytest -q tests/test_tiny_r2_p7.py`;
+its four cases passed. Supporting gates were
+`make --silent rtl-format-check rtl-style-check-all`,
+`make --silent sw-format-check sw-policy-check sw-host-test`,
+`PYTHONDONTWRITEBYTECODE=1 python3 -m pytest -q tests/test_script_tools.py -k 'run_flow'`,
+`ruff check scripts/run_flow.py tests/test_tiny_r2_p7.py`, and
+`python3 scripts/dependency_lock.py --lock dependencies/dependencies.lock.json`.
+The current ICS55 top-level compile used
+`source .cache/retrosoc/development/tiny-r2-p1/activate.sh && CCACHE_DISABLE=1 make --silent CONFIG=configs/ci/ics55-tiny.mk APP=ci_smoke SIMU=VERILATOR HAVE_SVA=YES BUILD_TIMESTAMP=2026-10-13-16-30 comp`
+and exited zero. These are source/compile/focused checks; none is a strict
+full firmware terminal verdict or physical qualification.
+
+The long-run diagnostic also confirmed that the emulator itself exits normally
+with a Tiny timeout at a bounded cycle count and uses about 26 MiB RSS. The
+earlier `status=running` records lacked a verified terminal result. The initial
+diagnosis attributed this to sandbox cleanup without sufficient evidence;
+that conclusion is withdrawn. Separate tool invocations can have different
+PID namespaces, so process absence in one does not establish that another
+command died. `run_flow.py` now records `parent_pid` and `child_pid`
+in the running result and writes a failed terminal result for unexpected
+Python exceptions; long runs must remain attached to one polling session.
+
+This is not phase acceptance. The current RTL now uses the backend SYS clock
+for the system/fabric path, but does not yet provide the required executable
+MEM/PCLK clock consumers, full CDC bridges, WFI leaf gating, PLL lock/loss
+recovery, or target-local five-cycle reset sequences. A full Tiny Verilator firmware
+run was started but did not produce a terminal `TEST_STATUS`/`SIM_TEST_PASS`
+verdict and was stopped; no physical, STA or netlist qualification is claimed.
+The remaining P7 clock/reset implementation and strict terminal regressions
+require a follow-up correction before human phase approval.
+
+## TINY-R2-P6 implementation complete, human review pending (2026-10-12)
+
+The maintainer approved the P6 preflight on
+`b3b30a72cd6ef5340c28e5044c96cd13258ec5a6`, against unchanged specification blob
+`7e19de49d49efaafacccfa85671e314d53dd15a4`. The
+[P6 runbook](tiny-soc-r2-shared-integration.md) records eight-channel/shared-IP
+integration, the explicitly retained historical ASIC wrapper and publication,
+and the separate source-bound validation scope. Development diagnostics are
+not final acceptance; no P7 or physical-frequency qualification is implied.
+
+Initial integration exposed a shared I2S sample-LSB assembly defect and a
+Verilator packed-tri1 board-model limitation. The runbook records their narrow
+corrections. Directed SDIO integration additionally exposed command admission,
+transmit/receive pause and multi-burst descriptor-retirement defects, plus
+manual card-model gaps; their independent reproductions are recorded there.
+Both Tiny PDKs have complete Verilator and Icarus functional verdicts and
+netlist boots. Yosys synthesis and OpenSTA run for both PDKs with the exact
+32-main plus 6-private-Crypto macro bindings. The final audit is
+`build/ics55-tiny-2026-10-12-11-50-2eb98c0e1fae/meta/tiny-r2-p6/audit-result-v3.json`;
+the source-bound evidence is `evidence.json` in the same directory. The
+functional report retains four simulator records, all workload/AXI/device
+observations and the Mini compatibility result.
+
+Observed setup/hold slack remains negative: ICS55 WNSmax/WNSmin −84.93/−0.31 ns
+and IHP130 −165.41/−0.51 ns. Warning checks report new signatures from the
+existing policy's observation flows; no baseline or metrics policy was edited.
+The full Pytest attempt remains recorded as 1634 passed, one optional `pypdf`
+skip, five failures and 60 fixture errors; all 65 failed/error nodes pass in
+the explicit repair selection, and the affected consumer suite passes 512 with
+one optional skip. These overlapping counts are not a clean full-suite rerun.
+Final human review, complete-product timing closure, package/QFN64 binding and
+physical signoff remain pending. Earlier P5 results and failed P6 attempts
+retain their original attribution.
+
+## TINY-R2-P5 P1 protocol correction; validated, review pending (2026-10-12)
+
+The maintainer requested correction of the reset write-drain/read-retention
+failures and publication-binding mismatch. The implementation starts from
+`4e6ab1a6fb445521703f0bfa412d3a1e8a38cd69`; the specification remains blob
+`7e19de49d49efaafacccfa85671e314d53dd15a4`. The [P5 runbook](tiny-soc-r2-concurrent-fabric.md)
+describes preserved upstream spill responses, complete read/error bursts,
+write drain before B, latched downstream cancellation and first-error capture.
+Profiles remain Tiny/ICS55 SAFE24 with parked PLL and Tiny/IHP130 compatibility.
+
+Current evidence is isolated under
+`build/ics55-tiny-2026-10-11-23-35-b4aa59f3e5d9/meta/tiny-r2-p5-p1/`.
+The older `09-30` campaign failed overall: 1669 Pytest passes, one skip,
+two failures and ten errors (publication), plus independent R stability and
+W-before-B violations. Its passing ordinary regressions, PPA and replay are
+historical observations, not acceptance of the repair.
+
+Final focused validation passes 101 tests. Full Pytest passes 1685 tests,
+with one optional `pypdf` skip and no failures; these counts overlap. Both
+Tiny PDK PR regressions pass, including six strict simulation verdicts,
+synthesis, netlist boot and the 32-macro/common-SYS binding check. Each
+simulator passes all 28 reset scenarios, including three production DMA
+drain cases; normal-target, arbitrary-target-reset and fault-collector
+induction/SAT checks pass. Complete terminal/fault publication bindings and
+three unsafe-completion mutation tests pass without changing the reviewed
+publication SHA. RTL format/style/readiness, Ruff and diff checks pass.
+
+`source.json` and `evidence.json` bind 2011 technical inputs to digest
+`f670789a2379a8d2e6c59f5ecd714ca0237bb6f1cfe749a2f29b0794d356b5f0`,
+locked tools, exact command results and retained artifacts. No warning
+baseline, metric policy, shared DMA/SDK implementation or public ABI changed.
+The lint signatures/counts match the preceding `09-30` campaign; the existing
+baseline comparison remains a non-blocking observation. STA remains unclosed:
+max WNS is -70.04 ns ICS55 and -127.01 ns IHP130. The runbook records full
+area/timing observations and limitations. Matched-image replay, full-function
+C netlist qualification, PDF promotion and physical qualification were not
+repeated for this bounded correctness repair. P5 human review/acceptance and
+all P6+ work remain separate.
+
+## TINY-R2-P5 target-reset isolation repair; focused validation (2026-10-11)
+
+Superseded attempt: later protocol checks rejected its response flushing and
+early B/RLAST behavior. The four focused passes below remain historically
+accurate but were insufficient to establish reset isolation.
+
+Following the P5 review, the fabric now receives the explicit 23-entry target
+reset vector. A reset blocks new admission and downstream handshakes, flushes
+response spill state, and converts any accepted target session into a held
+DECERR terminal response. The existing owner credit is released only by the
+upstream RLAST/B handshake, preventing stale response replay or a permanently
+busy direction. No target map, public register, DMA capacity, clock profile or
+later phase was changed.
+
+The focused command
+`PYTHONDONTWRITEBYTECODE=1 python3 -m pytest -q tests/test_tiny_r2_p5.py -k 'target_reset_isolation or concurrent_fabric'` passed 4 tests: both
+Icarus and Verilator reset-isolation tests and both existing concurrent-fabric
+tests. `make rtl-format-check rtl-style-check-all rtl-readiness-check-all`
+also passed. This is a focused implementation check; the prior P5 campaign
+reports remain source-bound to their recorded snapshots and have not been
+rewritten or relabeled. The retained focused logs are under
+`build/ics55-tiny-2026-10-10-18-58-b4aa59f3e5d9/meta/tiny-r2-p5/reset-repair-tests/`.
+
+## TINY-R2-P5 implementation; validated, review pending (2026-10-10)
+
+The maintainer approved the single-phase P5 plan on clean commit
+`3131762b35e0ebfcd196395f955e7c1005bd18e7`, specification blob
+`7e19de49d49efaafacccfa85671e314d53dd15a4`. The P4 input hashes match this
+starting source, but earlier reports retain their original snapshot attribution.
+The [P5 runbook](tiny-soc-r2-concurrent-fabric.md) records target-local ownership,
+sixteen APB branches, reserved inactive SDIO owner 2, concurrent-fault accounting
+and the unchanged public ABI/clock/memory boundaries.
+
+The completed campaign is under
+`build/ics55-tiny-2026-10-10-18-58-b4aa59f3e5d9/meta/tiny-r2-p5/`;
+the earlier `18-25` root retains development attempts. Full Pytest reports
+1679 passed and one optional `pypdf` skip. Both Tiny PDK PR regressions pass,
+including synthesis, netlist boot and clock/macro binding. The real CPU/DMA
+fixture records 13 overlapping DMA read/write beat cycles, 27 independent control
+completions, hart-reset isolation and abort drain on both simulators. Scoped
+target-control/fault proofs and retained P4 latency/streaming checks pass.
+
+Initial image executions passed but aggregation correctly failed on an Icarus 13
+temporary-expression population-count defect. The only subsequent source change
+is P5 opt-in testbench instrumentation. `source.json` retains the original
+snapshot for the unchanged DUT/full-suite/ordinary-regression/PPA results;
+`monitor-source.json` records the corrected monitor. Only affected image
+measurements and monitor checks were repeated. `monitor-diagnosis.json` retains
+the reproduction and original discrepancy; no comparison rule was relaxed.
+
+Final `report.json` has identical banked observations across simulators. Against
+repaired P4, burst1 cycles change 91306→65527 flat and 89365→65502 banked; burst16
+changes 17897→16974 and 17317→15802. CPU/copy cycles are unchanged; small
+contention/TCD/chunk256 increases remain explicit. The final combined input
+digest is `0f3ed21f6fd6e932ff2532aab8c9964e71cf4b972e4c27e2c8ca6ab1f22c1015`.
+`evidence.json` rechecks 2010 inputs and binds both snapshot scopes, six ordinary
+simulations, final matched-image runs, 15 focused/formal artifacts and reused
+unchanged Mini compatibility evidence. Library area grows 5.56%/6.95%; max WNS
+is -64.75 ns ICS55 and -111.93 ns IHP130. Timing remains unclosed/observational.
+The runbook records exact commands, source attribution, warning observations,
+measurements and unrun qualifications. Human phase acceptance is pending.
+Shared DMA/Mini/SDK behavior, dependencies and warning/metrics policy remain
+unchanged. No P6 execution or physical-frequency qualification is authorized.
+
+## TINY-R2-P4 throughput repair; validated, review pending (2026-10-10)
+
+Following the read-only P4 review, the maintainer approved the bounded-buffer
+repair plan for the P2 external-SRAM throughput regression. The changed Tiny
+group/frontend keep local I/D single-operation semantics, reserve external
+response capacity and overlap burst issuance/retirement; W is captured before
+macro arbitration and B follows complete commit-response drain. Common FIFO
+and register dependencies, ABI, clock/macro bindings and the pre-P5 external
+attachment remain unchanged. No shared Mini debug code is changed by this fix.
+
+Fresh evidence belongs under
+`build/ics55-tiny-2026-10-10-15-15-b4aa59f3e5d9/meta/tiny-r2-p4/`.
+The preceding P4 report below remains the pre-repair comparison, and does not
+qualify these new changes. The final audit rechecks 1972 technical inputs,
+six fresh regression simulations and three immutable-image executions. Full
+Pytest reports 1659 passed and one optional `pypdf` skip. Both Tiny PDK PR
+regressions pass, including synthesis, netlist boot and clock/macro bindings.
+Streaming/backpressure/error/reset checks and response conservation/fairness
+induction pass; all six SRAM fixtures retain three-cycle local completion and
+one-cycle steady read/write beat spacing. Initial style failures and focused
+development attempts remain separate from final qualifying results.
+
+Matched burst16 DMA cycles fall from 36713 to 17897 (flat) and 36489 to 17317
+(banked), reductions of 51.25% and 52.54%. All banked observations match across
+Verilator and Icarus. CPU/copy/burst1 cycles remain unchanged from pre-repair
+P4; burst1 overhead and 8.34%/4.81% burst16 overhead versus original P3 remain
+explicit. The combined input digest is
+`58daf68fbb9dee199b3085a73ef07d22eb3de6d00ba875c462e85b76b6091a0d`.
+`evidence.json` binds the source, artifacts, comparison and reused unchanged
+Mini behavioral evidence. All long gates ran once; Tiny nightly matched PR
+and was not duplicated. Setup WNS remains negative (-36.88 ns ICS55,
+-97.37 ns IHP130), with warning/metrics observations and baselines preserved.
+The [runbook](tiny-soc-r2-local-memory.md) records exact commands, measurements
+and unrun qualifications. Human review remains required; no P5 execution or
+physical-frequency qualification is implied.
+
+## TINY-R2-P4 implementation and validation; review pending (2026-10-10)
+
+The maintainer approved P4 preflight and implementation on clean source
+`4db27524a6282ca40d76b935415b89f043d6ea83`, specification blob
+`7e19de49d49efaafacccfa85671e314d53dd15a4`. The [P4 runbook](tiny-soc-r2-local-memory.md)
+records architecture, lifecycle, physical binding and matched-image boundaries.
+P3 results retain their original source attribution; no historical phase or
+physical qualification is automatically promoted.
+
+Development evidence is under
+`build/ics55-tiny-2026-10-10-10-05-b4aa59f3e5d9/meta/tiny-r2-p4/`.
+All planned functional gates completed: 1659 Pytest passes and one optional
+`pypdf` skip; Tiny ICS55/IHP130 PR and Mini ICS55 behavioral compatibility pass.
+Both Tiny netlists pass boot and the four-group/32-macro/common-SYS binding
+checks. Local uncontended latency is three cycles; group-control induction
+passes. Setup and hold timing remain negative and observational.
+
+`report.json` retains single-execution matched-binary results; the banked image
+matches exactly across Verilator and Icarus. CPU-copy cycles improve about
+19%/25% for flat/banked images, while isolated burst-16 DMA cycles increase
+about 2.2 times. This external-frontend throughput regression requires review;
+no blanket speedup or automatic phase acceptance is claimed. `evidence.json`
+rechecks 1972 source inputs and binds the gate, image, model, log and report
+artifacts. Exact commands, measurements and unrun qualifications are in the runbook.
+
+External fabric stays serialized until P5, which is not authorized. Default
+Tiny stays ICS55 SAFE24 with PLL off; IHP130 is explicit compatibility.
+
+## TINY-R2-P3 implementation in progress (2026-10-08–10)
+
+The retained source-bound compiler/layout matrix is complete for commit
+`3e46cc80e2d2243233d284b7cd9583714b0773bb`, source digest
+`a7fc5112cd6f20df956243c4d081a6a31ede6192cd270e3c13340557b0e58776`.
+All eight configurations pass ordinary Tiny acceptance and three deterministic
+Verilator cold starts. `compat-os-flat`, `perf-o2-lto-flat` and
+`perf-o2-plain-banked` also pass three native Icarus cold starts, with exact
+cross-simulator sample identity. The structured measurement report is
+`build/ics55-tiny-performance-2026-10-09-17-10-53cc6674d532/meta/tiny-r2-p3/report.json`.
+It records software measurements only; it does not close phase acceptance,
+physical timing, hardware bank conflict or Mini integrated-DMA gates.
+
+Quality evidence for that committed source is complete: full Pytest reports 1629 passed and
+one environment skip for missing `pypdf`; focused P3 tests and the embedded/RTL
+quality checks pass. Its Tiny PR regressions pass for ICS55 and IHP130, and
+Mini ICS55 behavioral-only compatibility passes. Negative STA margins and
+non-blocking lint observations remain recorded as observations.
+
+Exact commands, measurements, current evidence roots and remaining qualification
+limits are in the final section of the [P3 review-fix record](tiny-soc-r2-p3-review-fixes.md).
+Human review and acceptance remain required before any R2-P4 work.
+
+### Subsequent P1 race-fix follow-up
+
+The approved follow-up fixes WS2812 completion between register reads and
+legacy TCD publication across an ISR lease acquisition. Current source is
+`3e46cc8` plus the two HAL fixes and deterministic host tests; the matrix above
+remains evidence for the original commit. New evidence is under
+`build/ics55-tiny-2026-10-10-09-00-b4aa59f3e5d9/meta/tiny-r2-p3-races/`.
+The two new tests fail before their respective fixes and pass afterward;
+C format/policy/host checks and 156 focused tests pass. Mini PIO compatibility
+also passes once with strict SIM_TEST_PASS and no forbidden markers. Tiny
+ICS55/IHP130 PR and Mini ICS55 behavioral regressions each pass once. Final
+`evidence.json` checks all 1287 source inputs, 487 Mini PIO compiled inputs and
+all eight ordinary regression simulations. Lint baseline differences and
+negative STA remain observations; no timing closure is claimed. See the final
+review-fix section for commands, hashes, exact paths and unrun gates.
+
+Per the maintainer's follow-up instruction, long checks default to one execution;
+repeat only for a concrete failure/inconsistency or an explicit acceptance
+requirement. This follow-up does not repeat the three-cold-start compiler matrix.
+It does not waive any frozen qualification requirement or reattribute old runs.
+
+### Historical development snapshot
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
+selected Icarus groups still running at that snapshot. The then-current full Pytest result was
1531 passed, 4 failed, 94 fixture errors and 1 skipped; configuration-declaration
-and publication source/fixture drift remain open. IHP130 Tiny PR completes with
+and publication source/fixture drift were still open. IHP130 Tiny PR completed with
non-blocking lint observations and negative STA margin. These results do not
-close the remaining matrix, quality or human phase-acceptance gates.
+replace the final results above or imply human phase acceptance.
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
@@ -28,8 +500,8 @@ is bound at
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,
+The full compiler/placement matrix and broader regressions were pending in this
+initial record; human phase acceptance remains 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
@@ -42,9 +514,9 @@ 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.
+The initial entry recorded implementation and validation in progress; it did not
+claim phase completion or performance improvement. The existing GNU toolchain
+was used 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
@@ -448,8 +920,8 @@ not a prerequisite to the architectural improvements in R2-P3 through R2-P5.
| TINY-R2-P0 - Performance Contract and Roadmap Freeze | Reviewable requirements, exact old/new phase mapping, invariant package/IO counts, same-frequency CPU/main-SRAM ownership, explicit performance budgets, links and commands. Record actual documentation checks separately. | Documentation freeze only; no hardware result |
| TINY-R2-P1 - Reproducible Baseline, Constraints and Measurements | Reviewed source/profile/lock/tool identity; current 24 MHz functional and workload baseline; clocks, reset endpoints, constraints and timing-exception audit; counters and comparison methodology. Historical WNS is a risk reference, not a refreshed measurement. | Functional/measurement baseline accepted by explicit P2 approval on 2026-10-06; timing, warning and physical gaps retained |
| TINY-R2-P2 - Reset Distribution and CPU/SRAM Clock Feasibility | Reset distribution and payload-reset semantics; locked IHP130 main-SRAM macro timing, candidate common CPU/SRAM periods and representative paths; early clock/reset feasibility with explicit gaps. No final routed/PVT pass is implied. | Implementation and evidence ready for review; all analyzed rates retain timing failures; phase acceptance pending human review |
-| TINY-R2-P3 - Software and DMA Scheduling | Compiler and placement A/B results, finite DMA ownership, burst/chunk scheduling, WS2812 refill budget and workload correctness on the available platform. | Pending |
-| TINY-R2-P4 - Dual-Port Hazard3 and Four-Bank Local SRAM | Actual separate I/D paths, four independent 32 KiB bank frontends, SYS-clocked physical main-SRAM macros, local latency/fairness, FENCE.I, debug/reset and early inventory/SDC/macro-binding updates. | Pending |
+| TINY-R2-P3 - Software and DMA Scheduling | Compiler and placement A/B results, finite DMA ownership, burst/chunk scheduling and available-path correctness; approved option A retains product DMA/IRQ budget qualification for the platform that supplies the route. | Matrix retained on `3e46cc8`; subsequent two P1 HAL fixes and focused/compatibility checks complete in the worktree; human review/acceptance pending |
+| TINY-R2-P4 - Dual-Port Hazard3 and Four-Bank Local SRAM | Actual separate I/D paths, four independent 32 KiB bank frontends, SYS-clocked physical main-SRAM macros, local latency/fairness, FENCE.I, debug/reset and early inventory/SDC/macro-binding updates. | Implementation and planned functional validation complete on `4db2752` plus retained worktree snapshot; matched-image DMA throughput regression and human phase acceptance pending review; timing remains observational |
| TINY-R2-P5 - Per-Target Concurrent Fabric | Cross-target concurrency, one combined transaction per target, central-DMA read/write overlap, AW/W binding, errors, simultaneous faults/counters and accepted-transfer drain. | Pending |
| TINY-R2-P6 - Shared IP and Eight-Channel DMA Integration | Legacy P7 obligations; shared PWM reporting, I2S slave and alternate-input-route prerequisites; common ABI/HAL parity; Crypto banks/lifecycle; truthful capabilities and affected Mini compatibility. | Pending |
| TINY-R2-P7 - Tiny RCU and Dual-Mode Clock/Reset Integration | Legacy P8 obligations with main SRAM in SYS and XPI in MEM; atomic rate changes, all actual CDC/reset barriers, clock failures, WFI leaf gating and reserved DVP controls. | Pending |
diff --git a/physical/commercial/README.md b/physical/commercial/README.md
index ba2bf84d..09c765f8 100644
--- a/physical/commercial/README.md
+++ b/physical/commercial/README.md
@@ -158,11 +158,17 @@ false-paths the canonical reset ports. Product overlays
Qualified I/O covers JTAG, DVP (GPIO10-20), ULPI, SDRAM, SDIO, XPI, and
asynchronous pads (UART0/1, remaining GPIO); a reviewed
`TIMING_IO_MODE_HOOK` is mandatory in qualified mode.
-- Tiny (`tiny.tcl`): SAFE24 boot — `clk_system` (external 24 MHz) and
- `clk_jtag` only. The PLL_TOP macro is parked (EN=0), is not a clock source,
+- Tiny (`tiny.tcl`): P7 SAFE24 boot — `clk_aon` (external 24 MHz), derived
+ `clk_system`, `clk_memory`, `clk_peripheral`, `clk_jtag`, and the independent
+ `clk_audio` (GPIO17, 18.432 MHz). Explicit case analysis selects REF24 in all
+ four RCU muxes and parks PLL_TOP (EN=0) for this scenario. The live design
+ supports software transitions, but active PLL timing is not modeled here.
+ In this scenario the PLL is not a clock source,
and has no characterized timing arcs yet (see `docs/ip/tiny-soc.md`
TINY-055); its DB/LEF/GDS/CDL views are still required for link, placement,
- and LVS. Qualified I/O covers JTAG, XPI, and asynchronous pads.
+ and LVS. The legacy I/O template covers JTAG, XPI, and asynchronous pads;
+ this domain inventory update does not qualify P6 I2S/SDIO board timing or
+ the final QFN64 wrapper.
## Legacy-flow parity
diff --git a/physical/commercial/config/products/tiny.mk b/physical/commercial/config/products/tiny.mk
index a65474de..598e6a60 100644
--- a/physical/commercial/config/products/tiny.mk
+++ b/physical/commercial/config/products/tiny.mk
@@ -16,9 +16,9 @@ TOP := retrosoc_tiny_asic
# Canonical clock domains, mirrored by tcl/common/products/tiny.tcl and
# validated against rtl/tiny/integration/clock_reset_domains.json when the
-# commercial timing contract is generated. Tiny boots SAFE24: the PLL_TOP
-# macro is present in the netlist but parked off and is not a clock source.
-COMMERCIAL_CLOCK_DOMAINS := system jtag
+# commercial timing contract is generated. This adapter analyzes the SAFE24
+# boot scenario, explicitly constraining RCU mux selections and PLL enable.
+COMMERCIAL_CLOCK_DOMAINS := aon system memory peripheral jtag audio
# Qualified-I/O interface groups and the budget variables each group requires.
COMMERCIAL_IO_INTERFACES := JTAG XPI ASYNC
@@ -38,7 +38,7 @@ COMMERCIAL_IO_BUDGET_VARIABLES := \
# Tiny has no bidirectional pad-mode selection requirement.
PRODUCT_REQUIRES_IO_MODE_HOOK := NO
-# The Tiny PLL_TOP macro is parked (EN=0) and has no characterized timing
+# In this SAFE24 scenario PLL_TOP is parked (EN=0) and has no characterized timing
# arcs; doctor skips the qualified PLL-mode checks for this product. See
# docs/ip/tiny-soc.md (TINY-055) for the qualification boundary.
PRODUCT_PLL_MODE := parked
diff --git a/physical/commercial/tcl/common/products/tiny.tcl b/physical/commercial/tcl/common/products/tiny.tcl
index 247caba5..7fbd90dd 100644
--- a/physical/commercial/tcl/common/products/tiny.tcl
+++ b/physical/commercial/tcl/common/products/tiny.tcl
@@ -11,15 +11,14 @@
# retroSoC Tiny (retrosoc_tiny_asic) product overlay.
#
# Clock intent: the canonical contract (rtl/tiny/integration/
-# clock_reset_domains.json) owns two domains, system (SAFE24 external clock)
-# and jtag. The ICS55 PLL_TOP macro is present but parked off (EN=0) and is
-# not a clock source; it carries no characterized timing arcs yet, so no PLL
-# overlay clock is created here. See docs/ip/tiny-soc.md (TINY-055).
+# clock_reset_domains.json) owns REF24, derived SYS/MEM/PCLK, JTAG and GPIO17
+# AUDIO. This adapter covers SAFE24 boot only. RCU can enable the physical PLL
+# in functional operation, but its active timing remains unqualified.
namespace eval product {}
proc product::expected_clock_domains {} {
- return {system jtag}
+ return {aon system memory peripheral jtag audio}
}
proc product::custom_clock_domains {} {
@@ -27,7 +26,7 @@ proc product::custom_clock_domains {} {
}
proc product::clock_names {} {
- return {clk_system clk_jtag}
+ return {clk_aon clk_system clk_memory clk_peripheral clk_jtag clk_audio clk_sys_div2 clk_sys_div4 clk_clkout_source}
}
proc product::io_interface_groups {} {
@@ -35,6 +34,31 @@ proc product::io_interface_groups {} {
}
proc product::apply_clock_overlays {} {
+ foreach divisor {2 4} {
+ create_generated_clock -name clk_sys_div$divisor \
+ -source [flow::domain_object system] -divide_by $divisor \
+ [flow::required_net_driver_pin "Tiny SYS /$divisor" \
+ "u_soc/u_clock_backend/s_div${divisor}_clk"]
+ flow::register_clock clk_sys_div$divisor core
+ }
+ foreach mux {u_sys_mux u_mem_mux u_pclk_div2_mux u_pclk_mux} {
+ set_case_analysis 0 [flow::required_pins "Tiny SAFE24 mux $mux" \
+ "u_soc/u_clock_backend/$mux/select_i"]
+ foreach {side enabled} {0 1 1 0} {
+ set_case_analysis $enabled [flow::required_net_driver_pin "Tiny settled clock gate" \
+ "u_soc/u_clock_backend/$mux/r_enable$side"]
+ }
+ }
+ set_case_analysis 0 [flow::required_pins "Tiny SAFE24 PLL enable" \
+ "u_soc/u_clock_backend/u_pll/gen_ecos/u_backend/u_PLL_TOP/EN"]
+ foreach bit {1 2} {
+ set_case_analysis 0 [flow::required_net_driver_pin "Tiny boot CLKOUT source" \
+ "u_soc/u_clock_output/s_cfg_q\[$bit\]"]
+ }
+ create_generated_clock -name clk_clkout_source -source [flow::domain_object aon] \
+ -divide_by 1 [flow::required_net_driver_pin "Tiny boot CLKOUT source clock" \
+ "u_soc/u_clock_output/s_src_clk"]
+ flow::register_clock clk_clkout_source core
}
proc product::apply_io_constraints {} {
diff --git a/physical/librelane/tiny/scripts/generate_chip_config.py b/physical/librelane/tiny/scripts/generate_chip_config.py
index 782be724..85a9e08b 100644
--- a/physical/librelane/tiny/scripts/generate_chip_config.py
+++ b/physical/librelane/tiny/scripts/generate_chip_config.py
@@ -27,7 +27,7 @@ SIDE_ORDER = ("south", "east", "north", "west")
EXPECTED_SIGNAL_PADS = 52
CLOCK_PORTS = ("extclk_i_pad", "jtag_tck_i_pad")
SRAM_MASTER = "RM_IHPSG13_1P_1024x32_c2_bm_bist"
-SRAM_INSTANCE = "u_soc.u_sram.gen_memory.gen_bank[{index}].u_ram.u_mem"
+SRAM_INSTANCE = "u_soc.u_sram.gen_group[{group}].u_group.gen_bank[{bank}].u_ram.u_mem"
SRAM_COLUMNS = 8
SRAM_ORIGIN = (400, 600)
# Detailed routing requires an M2 track inside each bank's 0.26 um bottom-edge
@@ -95,7 +95,7 @@ def sram_instances(capacity_kib: int) -> dict[str, tuple[list[int], str]]:
SRAM_ORIGIN[0] + column * SRAM_PITCH[0],
SRAM_ORIGIN[1] + row * SRAM_PITCH[1],
]
- instances[SRAM_INSTANCE.format(index=index)] = (location, "N")
+ instances[SRAM_INSTANCE.format(group=index // 8, bank=index % 8)] = (location, "N")
return instances
@@ -122,18 +122,9 @@ def macro_config(instances: dict[str, tuple[list[int], str]], header: str) -> di
def macro_hooks(instances: dict[str, tuple[list[int], str]]) -> list[str]:
hooks: list[str] = []
for instance in instances:
- bracket_start = instance.find("[")
- if bracket_start >= 0:
- bracket_end = instance.index("]", bracket_start)
- instance_pattern = (
- re.escape(instance[:bracket_start])
- + ".*"
- + instance[bracket_start + 1 : bracket_end]
- + ".*"
- + re.escape(instance[bracket_end + 1 :])
- )
- else:
- instance_pattern = re.escape(instance)
+ # OpenDB and synthesis may escape array brackets differently. Handle
+ # every index (group and bank), retaining the exact non-index hierarchy.
+ instance_pattern = ".*".join(re.escape(part) for part in re.split(r"[\[\]]", instance))
hooks.extend(
[
f"{instance_pattern} VDD VSS VDDARRAY! VSS!",
diff --git a/physical/smoke/sta/opensta/generate_sdc.py b/physical/smoke/sta/opensta/generate_sdc.py
index b7b1eb42..329e17ea 100644
--- a/physical/smoke/sta/opensta/generate_sdc.py
+++ b/physical/smoke/sta/opensta/generate_sdc.py
@@ -119,9 +119,12 @@ def render(domains: list[dict[str, Any]], reset_ports: list[str]) -> str:
if source_domain is None:
lines.append(f"create_clock -name clk_{name} -period {period} ${pin_variable}")
else:
+ divide_by = sta.get("divide_by", 1)
+ if not isinstance(divide_by, int) or isinstance(divide_by, bool) or divide_by < 1:
+ raise ValueError("generated clock division must be a positive integer")
lines.append(
f"create_generated_clock -name clk_{name} -source ${pin_variables[source_domain]} "
- f"-divide_by 1 ${pin_variable}"
+ f"-divide_by {divide_by} ${pin_variable}"
)
lines.append("")
@@ -175,10 +178,52 @@ def main() -> int:
parser.add_argument("--domains", type=Path, required=True)
parser.add_argument("--pin-map", type=Path, required=True)
parser.add_argument("--output", type=Path, required=True)
+ parser.add_argument("--xin-hz", type=int, default=24000000)
args = parser.parse_args()
try:
domains, reset_ports = parse_document(args.domains, args.pin_map)
- atomic_write(args.output, render(domains, reset_ports))
+ document = json.loads(args.domains.read_text())
+ p7 = document.get("clock_contract") == "TINY-R2-P7"
+ if p7 and args.xin_hz not in (24000000, 48000000, 96000000):
+ raise ValueError("unsupported Tiny external clock")
+ if p7 and args.xin_hz != 24000000:
+ aon = next(domain for domain in domains if domain["name"] == "aon")
+ external = {**aon, "name": "external", "sta": {**aon["sta"],
+ "period_ns": 1e9 / args.xin_hz}}
+ aon["sta"] = {"source_domain": "external", "period_ns": 1e9 / 24000000,
+ "divide_by": args.xin_hz // 24000000, "async_group": "core",
+ "pin": "u_soc.u_clock_backend.gen_ref_divided.u_ref_div/clk_o"}
+ domains.insert(0, external)
+ if p7:
+ # These sources run even while the muxes select their undivided
+ # inputs. Their handover synchronizers still require real clocks.
+ for divisor in (2, 4):
+ domains.append({"name": f"sys_div{divisor}", "sta": {
+ "source_domain": "system", "period_ns": 1e9 * divisor / 24000000,
+ "divide_by": divisor, "async_group": "core", "object_type": "net_driver",
+ "net": f"u_soc.u_clock_backend.s_div{divisor}_clk"}})
+ domains.append({"name": "clkout_source", "sta": {
+ "source_domain": "aon", "period_ns": 1e9 / 24000000,
+ "divide_by": 1, "async_group": "core", "object_type": "net_driver",
+ "net": "u_soc.u_clock_output.s_src_clk"}})
+ result = render(domains, reset_ports)
+ if p7:
+ result += "\n# P7 SAFE24 boot scenario; this does not qualify active PLL timing.\n"
+ for mux in ("u_sys_mux", "u_mem_mux", "u_pclk_div2_mux", "u_pclk_mux"):
+ result += (f"set_case_analysis 0 [require_pins {{P7 SAFE24 selection}} "
+ f"{{u_soc.u_clock_backend.{mux}/select_i}}]\n")
+ # select_i passes through sequential handshake state. A static
+ # mode must also select its settled gate state; setting only
+ # the request cannot prune the other live clock in STA.
+ for side, enabled in ((0, 1), (1, 0)):
+ result += (f"set_case_analysis {enabled} [require_net_driver_pin "
+ f"{{P7 SAFE24 settled gate}} "
+ f"{{u_soc.u_clock_backend.{mux}/r_enable{side}}}]\n")
+ result += "# CLKOUT retains its reset REF24 source in this boot scenario.\n"
+ for bit in (1, 2):
+ result += (f"set_case_analysis 0 [require_net_driver_pin {{P7 CLKOUT source}} "
+ f"{{u_soc.u_clock_output.s_cfg_q_{bit}_}}]\n")
+ atomic_write(args.output, result)
except (OSError, ValueError, json.JSONDecodeError) as error:
parser.error(str(error))
return 0
diff --git a/physical/smoke/sta/opensta/opensta.mk b/physical/smoke/sta/opensta/opensta.mk
index e6d89433..9182bddc 100644
--- a/physical/smoke/sta/opensta/opensta.mk
+++ b/physical/smoke/sta/opensta/opensta.mk
@@ -16,14 +16,14 @@ include $(ROOT_PATH)/physical/smoke/sta/opensta/pdk_timing.mk
$(OPENSTA_SDC): $(OPENSTA_SDC_GEN) $(OPENSTA_DOMAIN_MAP) $(OPENSTA_PIN_MAP) | manifest
@mkdir -p $(dir $@)
- python3 $(OPENSTA_SDC_GEN) --domains $(OPENSTA_DOMAIN_MAP) --pin-map $(OPENSTA_PIN_MAP) --output $@
+ python3 $(OPENSTA_SDC_GEN) --domains $(OPENSTA_DOMAIN_MAP) --pin-map $(OPENSTA_PIN_MAP) --output $@ --xin-hz $(EXT_CLK_HZ)
sta: $(OPENSTA_SDC) | manifest
@mkdir -p $(STA_BUILD_ROOT)
-ifeq ($(TINY_SAFE24_PLL_OFF),YES)
- python3 $(ROOT_PATH)/scripts/tiny_ics55_platform.py --variant-root $(VARIANT_ROOT) verify-netlist \
+ifeq ($(SOC),TINY)
+ python3 $(ROOT_PATH)/scripts/tiny_r2_p7.py --variant-root $(VARIANT_ROOT) verify-netlist \
--netlist $(OPENSTA_NETLIST) --config $(OPENSTA_CONFIG) \
- --output $(SYN_BUILD_ROOT)/tiny-ics55-netlist.json
+ --output $(SYN_BUILD_ROOT)/tiny-r2-p7-netlist.json
endif
@for input in $(OPENSTA_NETLIST) $(OPENSTA_LIBERTY) $(OPENSTA_LINK_LIBS) $(OPENSTA_SRAM_LIBS) $(OPENSTA_SDC) $(OPENSTA_CONFIG); do \
test -f "$$input" || { echo "OpenSTA input missing: $$input" >&2; exit 1; }; \
@@ -45,7 +45,7 @@ endif
--env OPENSTA_TOP=$(OPENSTA_TOP) --env OPENSTA_NETLIST=$(OPENSTA_NETLIST) --env OPENSTA_LIBERTY=$(OPENSTA_LIBERTY) \
--env 'OPENSTA_LINK_LIBS=$(OPENSTA_LINK_LIBS)' --env 'OPENSTA_SRAM_LIBS=$(OPENSTA_SRAM_LIBS)' \
--env OPENSTA_SDC=$(OPENSTA_SDC) --env OPENSTA_REPORT=$(OPENSTA_REPORT) \
- --env OPENSTA_METRICS=$(OPENSTA_METRICS) --env OPENSTA_TINY_ICS55=$(TINY_SAFE24_PLL_OFF) -- \
+ --env OPENSTA_METRICS=$(OPENSTA_METRICS) --env OPENSTA_TINY_ICS55=$(TINY_SAFE24_PLL_OFF) --env OPENSTA_TINY_P7=$(TINY_R2_P7) -- \
$(OPENSTA) -no_init -exit -threads $(OPENSTA_THREADS) $(ROOT_PATH)/physical/smoke/sta/opensta/opensta.tcl
.PHONY: sta
\ No newline at end of file
diff --git a/physical/smoke/sta/opensta/opensta.tcl b/physical/smoke/sta/opensta/opensta.tcl
index 966a9e50..baa5d755 100644
--- a/physical/smoke/sta/opensta/opensta.tcl
+++ b/physical/smoke/sta/opensta/opensta.tcl
@@ -20,6 +20,13 @@ if {[info exists ::env(OPENSTA_TOP)] && $::env(OPENSTA_TOP) ne ""} {
}
link_design $top_design
read_sdc $::env(OPENSTA_SDC)
+if {$top_design eq "retrosoc_tiny_asic"} {
+ if {[info exists ::env(OPENSTA_TINY_P7)] && $::env(OPENSTA_TINY_P7) eq "YES"} {
+ source [file join [file dirname [info script]] tiny_r2_p7_checks.tcl]
+ } else {
+ source [file join [file dirname [info script]] tiny_r2_p4_checks.tcl]
+ }
+}
if {[info exists ::env(OPENSTA_TINY_ICS55)] && $::env(OPENSTA_TINY_ICS55) eq "YES"} {
source [file join [file dirname [info script]] tiny_ics55_checks.tcl]
}
diff --git a/physical/smoke/sta/opensta/tiny_ics55_checks.tcl b/physical/smoke/sta/opensta/tiny_ics55_checks.tcl
index 58642eb7..9c1c671d 100644
--- a/physical/smoke/sta/opensta/tiny_ics55_checks.tcl
+++ b/physical/smoke/sta/opensta/tiny_ics55_checks.tcl
@@ -4,7 +4,7 @@
set audit_dir [file dirname $::env(OPENSTA_REPORT)]
set pll [get_cells -hierarchical -filter {ref_name == PLL_TOP}]
set ram [get_cells -hierarchical -filter {ref_name == ics55_ecos_sram_1024x32_m8}]
-if {[llength $pll] != 1 || [llength $ram] != 32} { error "Tiny ICS55 macro binding mismatch" }
+if {[llength $pll] != 1 || [llength $ram] != 38} { error "Tiny ICS55 macro binding mismatch" }
if {[llength [get_clocks clk_system]] != 1} { error "Tiny SYS clock missing" }
report_clock_properties > $audit_dir/clocks.rpt
check_setup -verbose > $audit_dir/coverage.rpt
diff --git a/physical/smoke/sta/opensta/tiny_r2_p4_checks.tcl b/physical/smoke/sta/opensta/tiny_r2_p4_checks.tcl
new file mode 100644
index 00000000..97d0e23a
--- /dev/null
+++ b/physical/smoke/sta/opensta/tiny_r2_p4_checks.tcl
@@ -0,0 +1,77 @@
+# P4 structural timing prerequisites, common to both supported Tiny PDKs.
+# Negative timing remains observational; missing/incorrect clock objects do not.
+set p4_dir [file dirname $::env(OPENSTA_REPORT)]
+set p4_ram [get_cells -hierarchical -filter {ref_name == RM_IHPSG13_1P_1024x32_c2_bm_bist}]
+set p4_clock_pin A_CLK
+if {[llength $p4_ram] == 0} {
+ set p4_ram [get_cells -hierarchical -filter {ref_name == ics55_ecos_sram_1024x32_m8}]
+ set p4_clock_pin CLK
+}
+if {[llength $p4_ram] != 38} { error "P6 requires 32 main SRAM and six private Crypto macros" }
+set p6_all_ram $p4_ram
+set p4_ram {}
+set p6_crypto {}
+set p6_crypto_expected {}
+for {set bank 0} {$bank < 6} {incr bank} {
+ lappend p6_crypto_expected [format {u_soc.u_crypto.u_storage.gen_bank[%d].gen_macro.u_sram.u_mem} $bank]
+}
+set p6_crypto_seen {}
+foreach cell $p6_all_ram {
+ set name [string map {/ .} [get_full_name $cell]]
+ set is_crypto 0
+ foreach expected $p6_crypto_expected {
+ if {[string equal [string range $name end-[expr {[string length $expected]-1}] end] $expected]} {
+ lappend p6_crypto_seen $expected
+ set is_crypto 1
+ }
+ }
+ if {$is_crypto} { lappend p6_crypto $cell } else { lappend p4_ram $cell }
+}
+if {[llength $p4_ram] != 32 || [lsort $p6_crypto_seen] ne [lsort $p6_crypto_expected]} {
+ error "P6 private/main SRAM hierarchy mismatch"
+}
+set p4_expected {}
+for {set group 0} {$group < 4} {incr group} {
+ for {set bank 0} {$bank < 8} {incr bank} {
+ lappend p4_expected [format {u_soc.u_sram.gen_group[%d].u_group.gen_bank[%d].u_ram.u_mem} $group $bank]
+ }
+}
+set p4_seen {}
+foreach cell $p4_ram {
+ set name [string map {/ .} [get_full_name $cell]]
+ foreach expected $p4_expected {
+ if {[string equal [string range $name end-[expr {[string length $expected]-1}] end] $expected]} {
+ lappend p4_seen $expected
+ }
+ }
+}
+if {[lsort $p4_seen] ne [lsort $p4_expected]} { error "P4 macro group hierarchy mismatch" }
+set p4_source [get_pins -quiet {u_clock_buffer/clk_o}]
+if {[llength $p4_source] != 1} { error "P4 SYS source missing" }
+set p4_direct {}
+foreach pin [get_fanout -from $p4_source -flat -pin_levels 1 -trace_arcs all] {
+ dict set p4_direct [get_full_name $pin] 1
+}
+set p4_clock_report [open $p4_dir/p4-clock-bindings.rpt w]
+foreach cell [concat $p4_ram $p6_crypto] {
+ set clocks {}
+ foreach pin [get_pins -of_objects $cell] {
+ if {[string match "*/$p4_clock_pin" [get_full_name $pin]]} { lappend clocks $pin }
+ }
+ if {[llength $clocks] != 1} { error "P4 macro clock pin missing" }
+ set name [get_full_name [lindex $clocks 0]]
+ if {![dict exists $p4_direct $name]} { error "P4 SRAM clock is not direct SYS: $name" }
+ puts $p4_clock_report $name
+}
+set p4_cpu [get_pins -hierarchical -quiet {*u_hazard3_cpu_2port*/CLK *u_hazard3_cpu_2port*/CK *u_hazard3_cpu_2port*/CKN}]
+set p4_front [get_pins -hierarchical -quiet {*u_cpu_mem*/CLK *u_cpu_mem*/CK *u_cpu_mem*/CKN}]
+if {[llength $p4_cpu] == 0 || [llength $p4_front] == 0} { error "P4 CPU/frontend clock endpoints missing" }
+foreach pin [concat $p4_cpu $p4_front] {
+ set name [get_full_name $pin]
+ if {![dict exists $p4_direct $name]} { error "P4 CPU/frontend does not share SYS: $name" }
+}
+puts $p4_clock_report "CPU clock endpoints: [llength $p4_cpu]"
+puts $p4_clock_report "Frontend clock endpoints: [llength $p4_front]"
+close $p4_clock_report
+puts "TINY_R2_P4_BINDING_PASS: four groups, 32 macros, common CPU/SRAM SYS; not timing qualification"
+puts "TINY_R2_P6_BINDING_PASS: six private Crypto macros on SAFE24 PCLK; no capacity alias"
diff --git a/physical/smoke/sta/opensta/tiny_r2_p7_checks.tcl b/physical/smoke/sta/opensta/tiny_r2_p7_checks.tcl
new file mode 100644
index 00000000..ada232fc
--- /dev/null
+++ b/physical/smoke/sta/opensta/tiny_r2_p7_checks.tcl
@@ -0,0 +1,22 @@
+# P7 uses a dedicated, provenance-checked Python macro/clock audit. Historical
+# P1/P4 checks retain their direct-SYS/parked-PLL assumptions unchanged.
+set p7_dir [file dirname $::env(OPENSTA_REPORT)]
+foreach clock {clk_aon clk_system clk_memory clk_peripheral clk_audio clk_jtag clk_sys_div2 clk_sys_div4 clk_clkout_source} {
+ if {[llength [get_clocks -quiet $clock]] != 1} { error "P7 clock missing: $clock" }
+}
+set p7_pll [get_cells -hierarchical -quiet -filter {ref_name == PLL_TOP}]
+if {[llength $p7_pll] > 1} { error "P7 contains multiple PLL macros" }
+if {[llength $p7_pll] == 1} {
+ # The analysis is explicitly the cold SAFE24 boot operating state.
+ set p7_enable [get_pins -quiet "[get_full_name [lindex $p7_pll 0]]/EN"]
+ if {[llength $p7_enable] != 1} { error "P7 PLL enable pin missing" }
+ set_case_analysis 0 $p7_enable
+}
+report_clock_properties > $p7_dir/clocks.rpt
+check_setup -verbose > $p7_dir/coverage.rpt
+report_check_types -recovery -removal -min_pulse_width -min_period -max_slew -max_capacitance -max_fanout -violators -verbose > $p7_dir/constraint-violations.rpt
+foreach domain {system memory peripheral aon} {
+ report_checks -path_group clk_$domain -path_delay max -group_path_count 20 -fields {capacitance slew fanout} > $p7_dir/$domain-setup.rpt
+ report_checks -path_group clk_$domain -path_delay min -group_path_count 20 -fields {capacitance slew fanout} > $p7_dir/$domain-hold.rpt
+}
+puts "TINY_R2_P7_STA_OBSERVATION: SAFE24 boot only; not active PLL, routed or PVT qualification"
diff --git a/physical/smoke/syn/yosys/script/synth.tcl b/physical/smoke/syn/yosys/script/synth.tcl
index 568ca6d9..8f6c9b8a 100644
--- a/physical/smoke/syn/yosys/script/synth.tcl
+++ b/physical/smoke/syn/yosys/script/synth.tcl
@@ -165,7 +165,7 @@ yosys tee -q -o "${report_dir}/${proj_name}_area_logic.rpt" stat -top $top_desig
# final netlist
yosys write_verilog -noattr -noexpr -nohex -nodec $netlist
-if {$soc == "TINY" && $pdk == "ICS55"} {
+if {$soc == "TINY"} {
yosys write_json ${netlist}.json
}
diff --git a/physical/smoke/syn/yosys/yosys.mk b/physical/smoke/syn/yosys/yosys.mk
index c1f49bc3..304fc307 100644
--- a/physical/smoke/syn/yosys/yosys.mk
+++ b/physical/smoke/syn/yosys/yosys.mk
@@ -59,7 +59,7 @@ gen_synth_filelist: $(SV_FLIST)
## Synthesize netlist using Yosys
synth: $(NETLIST) | manifest
-$(NETLIST): $(SV_FLIST) $(YOSYS_SCRIPTS)
+$(NETLIST): $(SV_FLIST) $(YOSYS_SCRIPTS) | manifest
@mkdir -p $(YOSYS_OUT)
@mkdir -p $(YOSYS_TMP)
@mkdir -p $(YOSYS_RPT)
@@ -74,9 +74,9 @@ $(NETLIST): $(SV_FLIST) $(YOSYS_SCRIPTS)
--env REPORTS=$(YOSYS_RPT) --env NETLIST=$(NETLIST) -- \
timeout --foreground --kill-after=5s $(YOSYS_TIMEOUT)s \
yosys -t --perffile $(YOSYS_BUILD)/yosys-perf.json -c $(YOSYS_DIR)/script/synth.tcl
-ifeq ($(SOC)-$(PDK),TINY-ICS55)
- python3 $(ROOT_PATH)/scripts/tiny_ics55_platform.py --variant-root $(VARIANT_ROOT) audit-netlist \
- --netlist $(NETLIST) --config $(NETLIST_CONFIG) --output $(YOSYS_BUILD)/tiny-ics55-netlist.json
+ifeq ($(SOC),TINY)
+ python3 $(ROOT_PATH)/scripts/tiny_r2_p7.py --variant-root $(VARIANT_ROOT) audit-netlist \
+ --netlist $(NETLIST) --config $(NETLIST_CONFIG) --output $(YOSYS_BUILD)/tiny-r2-p7-netlist.json
endif
$(NETLIST_DEBUG): $(NETLIST)
diff --git a/publications/datasheets/system-reference.json b/publications/datasheets/system-reference.json
index f940dc18..68fa3b48 100644
--- a/publications/datasheets/system-reference.json
+++ b/publications/datasheets/system-reference.json
@@ -4459,7 +4459,8 @@
"RS_ETIMEOUT": -4,
"RS_EIO": -5,
"RS_ENOTSUP": -6,
- "RS_EFORMAT": -7
+ "RS_EFORMAT": -7,
+ "RS_EBUSY": -8
},
"meanings": {
"RS_OK": "Operation completed under that API contract.",
@@ -4469,7 +4470,8 @@
"RS_ETIMEOUT": "Bounded operation timed out.",
"RS_EIO": "I/O operation failed.",
"RS_ENOTSUP": "Operation unsupported.",
- "RS_EFORMAT": "Input format rejected."
+ "RS_EFORMAT": "Input format rejected.",
+ "RS_EBUSY": "Resource or lifecycle operation is busy; retry after the owning operation completes."
},
"observation": "Signed API return value. Exact triggering conditions depend on the called function; the enum definition does not make every code reachable from every API.",
"handling": "Check the return before using output values. No hardware W1C applies to a C return value; preserve the relevant peripheral status if diagnosis requires it.",
diff --git a/publications/datasheets/tiny/chapter-review.md b/publications/datasheets/tiny/chapter-review.md
index 65609ab7..df80a38b 100644
--- a/publications/datasheets/tiny/chapter-review.md
+++ b/publications/datasheets/tiny/chapter-review.md
@@ -11,9 +11,9 @@ All **39 frozen entries / 14 IP chapters** are reviewed below. Register, address
| 3 | Configuration and Scope / 2 | Covered. IHP130, 128 KiB, 24 MHz target, four DMA channels; hardware RV32IMC versus default RV32IM firmware. | [ihp130-tiny.mk](../../../configs/ci/ihp130-tiny.mk), [retrosoc_tiny.sv](../../../rtl/tiny/top/retrosoc_tiny.sv) | Use the committed profile and identity; keep deferred interfaces and physical claims excluded. |
| 4 | Reading and Evidence Conventions / 2 | Covered. Product-specific organization, navigation or revision control; no additional hardware capability. | [datasheet.typ](../../../publications/datasheets/tiny/datasheet.typ), [structure-contract.json](../../../publications/datasheets/tiny/structure-contract.json) | Retain the declared scope, complete source links and independent Tiny identity when updating this section. |
| 5 | System Architecture / 17 | Covered. Product-specific organization, navigation or revision control; no additional hardware capability. | [datasheet.typ](../../../publications/datasheets/tiny/datasheet.typ), [structure-contract.json](../../../publications/datasheets/tiny/structure-contract.json) | Retain the declared scope, complete source links and independent Tiny identity when updating this section. |
-| 6 | Processor and Integration / 17 | Covered. One RV32IMC MCU hart, A disabled, direct AHB-Lite/AXI32 adapter and debug reset boundary. | [retrosoc_tiny.sv](../../../rtl/tiny/top/retrosoc_tiny.sv), [mgmt_core_wrapper.sv](../../../rtl/ip/core/mgmt_core_wrapper.sv) | Retain actual wrapper parameters; do not infer firmware ISA flags from the hardware C extension. |
+| 6 | Processor and Integration / 17 | Working-source P4 update: dual-port RV32IMC hart, A disabled, independent local I/D and tagged slow adapter. Page number belongs to the earlier delivered PDF. | [retrosoc_tiny.sv](../../../rtl/tiny/top/retrosoc_tiny.sv), [tiny_cpu_wrapper.sv](../../../rtl/tiny/top/tiny_cpu_wrapper.sv) | Retain actual wrapper parameters; revalidate publication pages separately before delivery. |
| 7 | Clock, Reset and Operating State / 18 | Covered. System and asynchronous JTAG domains; RTC/WDG share the system clock. | [clock_reset_domains.json](../../../rtl/tiny/integration/clock_reset_domains.json), [retrosoc_tiny.sv](../../../rtl/tiny/top/retrosoc_tiny.sv) | Obtain CDC/RDC and clock/power evidence separately; do not claim independent sleep timing. |
-| 8 | Bus Transactions, Arbitration and Errors / 19 | Covered. One globally active transaction, round-robin acceptance, saved ownership, bounded legal bursts and error/drain behavior. | [tiny_axi4_fabric.sv](../../../rtl/tiny/top/tiny_axi4_fabric.sv), [axi4_error_slave.sv](../../../rtl/ip/interconnect/axi4_error_slave.sv) | Keep local IP capability separate from end-to-end fabric admission; no unlimited liveness guarantee. |
+| 8 | Bus Transactions, Arbitration and Errors / 19 | P5 source draft updated: target-local ownership, bounded independent DMA R/W, separate APB branches and concurrent fault count. | [tiny_axi4_fabric.sv](../../../rtl/tiny/top/tiny_axi4_fabric.sv), [tiny_axi4_target.sv](../../../rtl/tiny/top/tiny_axi4_target.sv) | Page19 belongs to the prior PDF; no new PDF or unconditional liveness claim. SDIO owner2 is reserved only. |
| 9 | Memory Map and Access Conventions / 21 | Covered. Twenty canonical address windows, including alias versus physical-capacity distinctions. | [memory_map.json](../../../rtl/tiny/address_map/memory_map.json) | Reconcile every source-map change; do not use unmapped holes as scratch memory. |
| 10 | Interrupt and DMA Request Routing / 22 | Covered. Fourteen allocated core causes, thirty external positions, enabled DMA request map and channel conventions. | [soc_topology.json](../../../rtl/tiny/integration/soc_topology.json), [dma_math.c](../../../crt/src/hal/dma_math.c) | Keep core input bits, external ordinals, CSR positions and DMA selectors distinct. |
| 11 | Peripherals / 23 | Covered. Fourteen IP chapters and 337 source-extracted register definitions, with instance-specific routing. | [catalog.json](../../../publications/datasheets/tiny/catalog.json), [tiny_reference.py](../../../publications/tiny_reference.py) | Update source inventory, frozen register keys and rendered-coverage checks together after reviewed ABI changes. |
diff --git a/publications/datasheets/tiny/content-review.md b/publications/datasheets/tiny/content-review.md
index 14add1c8..e2ecf055 100644
--- a/publications/datasheets/tiny/content-review.md
+++ b/publications/datasheets/tiny/content-review.md
@@ -1,15 +1,21 @@
# Tiny Gen1 v0.1 DRAFT content review
-The publication describes the independent IHP130 Tiny profile: one RV32IMC
-Hazard3 with A disabled, RV32IM default firmware, 128 KiB SRAM, 24 MHz target,
+The publication describes the independent IHP130 Tiny profile: one dual-port
+RV32IMC Hazard3 with A disabled, RV32IM default firmware, 128 KiB SRAM in four
+independently serviced 32 KiB groups, 24 MHz target,
four DMA channels and AXI32/APB4. Rill (清溪) is the selected prototype brand.
The font/palette/grid are inherited as design rules; product facts are not
inherited from Mini's descriptions or qualification records.
+The P4 working-source update changes CPU/local-SRAM bindings and the affected
+draft architecture prose/figure. It does not advance `tiny.json.source_revision`,
+rebuild or relabel a delivered PDF, or add hardware qualification. A later
+publication release still requires source review, snapshot refresh and PDF checks.
+
| Area | Review and publication decision | Follow-up boundary |
| --- | --- | --- |
| CPU and startup | Publish the actual C/M extensions, disabled A, CSR-enabled firmware and Tiny guard around PSRAM setup. | Keep compiler target separate from implemented ISA; a reset vector is not a completed boot. |
-| Fabric | Document one globally active CPU/DMA read or write, saved ownership, naturally aligned bounded bursts and terminal response/drain. | Do not claim arbitrary AXI4 features, independent read/write concurrency or unrestricted liveness. |
+| Fabric | P5 draft: independent target ownership, one combined CPU slow operation, DMA one read/one write, 16 APB branches and lossless concurrent-fault accounting. Owner2 is reserved, inactive SDIO. | No SDIO integration, arbitrary AXI4 features or unconditional liveness claim. Snapshot/PDF revision is not advanced. |
| Address/IRQ/pads | Derive all 20 regions, 14 allocated core causes and 52 logical signal pads; retain 32 GPIO alternate entries. | Logical pads are not package pin numbers or a board schematic. |
| IP/register scope | Fourteen chapters, thirteen unique register families; Tiny SYSCTRL is decoded independently and ARCHINFO overrides the SoC identity. | Reject missing/extra decode and defaults belonging to other products. |
| DMA | Four channels, request mask 0x07F9, no stream modes, 64-byte descriptors and source-derived field layout. | **Discovery discrepancy:** the four-bit maximum-burst field reads zero after truncating 16. Publish that value and the actual 16-beat limit separately; propose a future ABI/RTL review, do not change RTL here. |
diff --git a/publications/datasheets/tiny/datasheet.typ b/publications/datasheets/tiny/datasheet.typ
index cd9d68c6..1580990b 100644
--- a/publications/datasheets/tiny/datasheet.typ
+++ b/publications/datasheets/tiny/datasheet.typ
@@ -29,9 +29,9 @@
- JTAG debug with coordinated hart reset.
*Memory and interconnect*
- - *128 KiB SRAM*, 32 banks of 4 KiB.
+ - *128 KiB SRAM*, four independently serviced 32 KiB groups backed by 32 banks of 4 KiB.
- Native AXI32 memory paths and one APB4 island.
- - CPU and DMA share one active transaction globally.
+ - Independent local instruction/data ports and target-local external ownership.
- XPI NOR boot and SRAM-resident application layout.
*DMA and control*
@@ -103,16 +103,18 @@ The document uses the approved Mini visual language; all product facts and sourc
= System Architecture
== Processor and Integration
-Hazard3 is hart zero. The shared wrapper selects the C and M extensions and disables A for Tiny. Its AHB-Lite bus terminates
-at a direct AXI32 adapter. JTAG supports the implemented halt/resume, register and system-bus debug path; debug hart reset
-waits for the adapter to become idle instead of abandoning an accepted transaction.
+Hazard3 is hart zero. The locked dual-port wrapper selects the C and M extensions and disables A for Tiny. Instruction fetch
+and load/store use independent AHB ports through separate local SRAM frontends; non-SRAM accesses use the tagged slow path.
+JTAG supports halt/resume, register access and halted-hart abstract-command memory access, without an independent system-bus master.
+Debug hart reset first halts the hart and drains accepted local I/D and slow-path responses. SRAM and unrelated DMA work stay active.
The system has no application hart, MMU/Linux platform, hardware cache-coherent multicore organization or product extension
slots. CPU-visible SRAM, XPI and APB routes are the implemented interconnect. The single-hart architecture does not remove
the need for software/DMA buffer ownership and fences.
#architecture()
#source-note("rtl/tiny/top/retrosoc_tiny.sv",title:"Tiny product integration and parameters")
-#source-note("rtl/ip/core/mgmt_core_wrapper.sv",title:"CPU ISA, AHB adapter and debug wrapper")
+#source-note("rtl/tiny/top/tiny_cpu_wrapper.sv",title:"Dual-port CPU ISA, AHB ports and debug wrapper")
+#source-note("rtl/tiny/top/tiny_sram.sv",title:"Four local SRAM groups and external attachment")
== Clock, Reset and Operating State
CPU, AXI, APB, RTC and watchdog share the configured 24 MHz system clock. CLINT derives a 1 MHz tick. JTAG has its own
@@ -130,9 +132,18 @@ advertised as a qualified voltage/frequency operating point.
#source-note("rtl/tiny/integration/clock_reset_domains.json",title:"Clock/reset inventory and debug crossings")
== Bus Transactions, Arbitration and Errors
-The two initiators are CPU and DMA. They share *one active read or write transaction globally*. The fabric saves source,
-direction, address and attributes, forwards the request, and retains ownership until the terminal B or RLAST handshake.
-Read/write request arbitration is round-robin; it is not a throughput guarantee or an unrestricted liveness guarantee.
+The two active external initiators are CPU slow path and DMA. A third ingress is reserved for future SDIO integration and
+has no active producer. Each SRAM group, XPI memory endpoint and APB peripheral has independent transaction ownership.
+CPU slow traffic admits one combined operation; DMA can have one read and one write outstanding across different targets.
+Each target admits one combined read or write. CPU local instruction/data requests remain independently arbitrated within
+each SRAM group. Saved ownership includes accepted address and response buffers and lasts through the upstream B/RLAST handshake.
+Owner arbitration is round-robin with fair direction selection. Progress bounds require a running clock and peer progress.
+Separate APB bridges keep independent controls outside a FIFO-waiting peripheral's transaction.
+
+Fault accounting counts each failed transaction once, including errors before the final read beat. Simultaneous events
+increment the saturating count individually; first-fault metadata uses ascending owner, instruction before data, then read
+before write. CPU is owner0, DMA owner1, and owner2 is reserved. FAULT_DETAIL retains raw AXI response2/3. Clearing validity
+does not clear the count, and a simultaneous hardware event wins over clear.
Memory transfers support naturally aligned 1/2/4-byte beats and up to 16 INCR beats. Single-beat FIXED is also admitted.
The complete address span must remain within one decoded target and one 4 KiB page without overflow. MMIO accepts only
diff --git a/publications/datasheets/tiny/diagrams.typ b/publications/datasheets/tiny/diagrams.typ
index 4365a9ab..fe355048 100644
--- a/publications/datasheets/tiny/diagrams.typ
+++ b/publications/datasheets/tiny/diagrams.typ
@@ -26,21 +26,22 @@
})
}
#let architecture() = diagram("tiny-architecture",schematic((
- (5,4,65,19,[Hazard3 · hart 0 \ RV32IMC · A disabled]),
+ (5,4,65,19,[Hazard3 dual-port · hart 0 \ RV32IMC · A disabled]),
(104,4,63,19,[JTAG / debug transport \ External TCK domain]),
- (5,37,65,15,[AHB-Lite → AXI32 adapter]),
+ (5,37,65,15,[I/D decode + slow adapter]),
(104,37,63,15,[Four-channel DMA \ AXI32 master]),
- (30,69,112,19,[Tiny AXI32 fabric \ One active transaction globally]),
- (0,110,40,22,[128 KiB SRAM \ 32 × 4 KiB]),
+ (30,69,112,19,[Tiny AXI32 fabric \ Independent target ownership]),
+ (0,110,40,22,[128 KiB SRAM \ 4 × 32 KiB groups]),
(44,110,40,22,[XPI read path \ Flash alias / NOR]),
- (88,110,40,22,[APB4 bridge \ 16 targets]),
+ (88,110,40,22,[16 APB4 branches \ One per peripheral]),
(132,110,40,22,[Error responders \ DECERR / SLVERR]),
),(
((104,13),(70,13)),((37,23),(37,37)),
+ ((5,44),(0,44),(0,103),(6,103),(6,110)),
((37,52),(37,61),(60,61),(60,69)),((135,52),(135,61),(112,61),(112,69)),
((45,88),(45,99),(20,99),(20,110)),((70,88),(70,103),(64,103),(64,110)),
((103,88),(103,103),(108,103),(108,110)),((127,88),(127,99),(152,99),(152,110)),
-),height:134),[Tiny Gen1 integration. System logic runs from the configured 24 MHz clock; JTAG is asynchronous.])
+),height:134),[Tiny Gen1 integration. The left branch represents independent local I/D paths bypassing the external fabric. System logic uses the configured 24 MHz clock; JTAG is asynchronous.])
#let ip-diagram(entry) = {
let children=entry.nodes.slice(1)
diff --git a/publications/datasheets/tiny/source-contract.json b/publications/datasheets/tiny/source-contract.json
index d2346185..ac2214fa 100644
--- a/publications/datasheets/tiny/source-contract.json
+++ b/publications/datasheets/tiny/source-contract.json
@@ -98,7 +98,12 @@
"uart1": 26
},
"sources": [
- "rtl/ip/core/mgmt_core_wrapper.sv",
+ "rtl/tiny/top/tiny_cpu_wrapper.sv",
+ "rtl/tiny/top/tiny_cpu_mem.sv",
+ "rtl/tiny/top/tiny_sram.sv",
+ "rtl/tiny/top/tiny_sram_group.sv",
+ "rtl/tiny/top/tiny_sram_axi4.sv",
+ "rtl/tiny/top/tiny_sram_port_if.sv",
"rtl/ip/core/mgmt_debug_wrapper.sv",
"rtl/ip/core/mgmt_debug_reset.sv",
"rtl/ip/interconnect/ahbl2axi4.sv",
@@ -112,19 +117,19 @@
"app/apps/bringup/tiny.mk",
"app/apps/ci_smoke/tiny.mk",
"scripts/generate_tiny.py",
- "tests/test_tiny.py"
+ "tests/test_tiny.py",
+ "rtl/tiny/top/tiny_axi4_target.sv",
+ "rtl/tiny/top/tiny_fault_collector.sv"
],
"bindings": [
{
"file": "rtl/tiny/top/tiny_axi4_fabric.sv",
"text": [
- "(s_selected_addr.len <= 8'd15)",
- "(s_last_addr[31:12] == s_selected_addr.addr[31:12])",
- "s_accept = (s_state_q == Idle) && s_selected_valid;",
- "Response: if (s_terminal) s_state_d = Idle;",
- "s_tgt_d = s_protocol_legal ? s_decoded_target : 3'd4;",
- "assign s_protocol_legal = !s_selected_addr.lock && (s_selected_addr.size <= 3'd2) &&\n ((s_selected_addr.addr & ((32'd1 << s_selected_addr.size) - 32'd1)) == 32'd0) &&\n ((s_selected_addr.burst == 2'b01) ||\n ((s_selected_addr.burst == 2'b00) && (s_selected_addr.len == 8'd0))) &&\n (s_selected_addr.len <= 8'd15) && !s_last_addr[32] &&\n (s_last_addr[31:12] == s_selected_addr.addr[31:12]) &&\n (decode_target(\n s_last_addr[31:0]\n ) == s_decoded_target) && ((s_decoded_target != 3'd2) || (s_selected_addr.len == 8'd0));",
- "assign s_terminal = (s_state_q == Response) &&\n (s_write_q ? (s_target_bvalid[s_tgt_q] && s_bready[s_owner_q]) :\n (s_target_rvalid[s_tgt_q] && s_rready[s_owner_q] && s_target_rlast[s_tgt_q]));"
+ " function automatic logic [4:0] checked_target(\n input logic [31:0] addr_i, input logic [7:0] len_i,\n input logic [2:0] size_i, input logic [1:0] burst_i,\n input logic lock_i, input logic instruction_i);\n logic [32:0] last_addr;\n logic [4:0] decoded;\n decoded = decode_target(addr_i);\n last_addr = {1'b0, addr_i} + (({25'd0, len_i} + 33'd1) << size_i) - 33'd1;\n if (lock_i || (size_i > 3'd2) || (len_i > 8'd15) ||\n ((addr_i & ((32'd1 << size_i) - 32'd1)) != 32'd0) ||\n !((burst_i == 2'b01) || ((burst_i == 2'b00) && (len_i == 8'd0))) ||\n last_addr[32] || (last_addr[31:12] != addr_i[31:12]) ||\n (decode_target(last_addr[31:0]) != decoded) ||\n ((decoded >= 5'd5) && (decoded <= 5'd20) && (len_i != 8'd0)))\n return 5'd22;\n if (instruction_i && !(`SOC_ADDR_IS_FLASH(addr_i) ||\n ((addr_i >= 32'h5000_0000) && (addr_i < 32'h5400_0000))))\n return ((decoded >= 5'd5) && (decoded <= 5'd20)) ? 5'd22 : 5'd21;\n return decoded;\n endfunction",
+ "assign s_busy_d = (s_busy_q & ~s_done) | s_admit;",
+ "assign owner_idle_o[owner] = !(s_busy_q[2*owner] || s_busy_q[2*owner+1]);",
+ "assign s_eligible[2*owner] = rst_n_i && owner_idle_o[owner] && masters[owner].arvalid && (!masters[owner].awvalid || !s_cpu_prefer_write_q);",
+ "assign s_eligible[2*owner+1] = rst_n_i && owner_idle_o[owner] && masters[owner].awvalid && (!masters[owner].arvalid || s_cpu_prefer_write_q);"
]
},
{
@@ -177,6 +182,20 @@
"RS_DMA_CH_REG(channel, RS_DMA_CH_REG_TCD_COUNT) = UINT32_C(1);",
"current = (rs_dma_tcd_t *)(uintptr_t)current->next_ptr;"
]
+ },
+ {
+ "file": "rtl/tiny/top/tiny_axi4_target.sv",
+ "text": [
+ "assign s_accept = (s_req_q.state == Idle) && enable_i && s_valid;",
+ "assign s_terminal = s_resp_active && (s_req_q.write ? (s_b_valid && s_bready[s_req_q.owner]) : (s_r_valid && s_rready[s_req_q.owner] && s_r_data[1]));",
+ "assign fault_valid_o = s_terminal && s_req_q.error_seen;",
+ "if (!s_req_q.error_seen && (s_rin_data[3:2] != 2'b00)) begin s_req_d.error_seen = 1'b1; s_req_d.error_addr = s_req_q.read_addr; s_req_d.error_resp = s_rin_data[3:2]; end",
+ "assign s_abort_write_valid = (s_req_q.state == AbortResponse) && s_req_q.write && s_req_q.write_done && !s_req_q.b_captured;",
+ "assign s_abort_read_valid = (s_req_q.state == AbortResponse) && !s_req_q.write && (s_req_q.read_count <= {1'b0, s_saved_addr.len});",
+ ".flush_i(1'b0)",
+ "AbortWrite: begin",
+ "if (s_terminal) s_req_d.state = Idle;"
+ ]
}
]
}
diff --git a/publications/programming_reference.py b/publications/programming_reference.py
index ca4de1e9..afc9685c 100644
--- a/publications/programming_reference.py
+++ b/publications/programming_reference.py
@@ -125,6 +125,26 @@ def clean_source(text: str) -> str:
return re.sub(r"\s+", "", re.sub(r"/\*.*?\*/|//[^\n]*", "", text, flags=re.S))
+def mini_channel_enum(text: str) -> str:
+ """Select Mini's channel roles, excluding the Tiny product branch.
+
+ Helper aliases share an allocated context; they do not allocate another
+ channel. Keep their Mini bulk binding checked instead of parsing Tiny's
+ literal 6/7 values from an inactive preprocessor branch.
+ """
+ match = re.search(r"typedef enum\s*\{([^{}]*)\}\s*rs_dma_channel_t;", text, re.S)
+ if match is None:
+ raise ValueError("missing DMA channel enumeration")
+ body = re.sub(r"#ifdef RS_SOC_TINY\s*\n[^#]*#else\s*\n([^#]*)#endif", r"\1", match[1])
+ if "#" in body:
+ raise ValueError("unreviewed Mini DMA channel selection")
+ aliases = dict(re.findall(r"(RS_DMA_CHANNEL_\w+)\s*=\s*(RS_DMA_CHANNEL_\w+)\s*,", body))
+ expected = {"RS_DMA_CHANNEL_DVP", "RS_DMA_CHANNEL_I2S_TX", "RS_DMA_CHANNEL_I2S_RX"}
+ if any(name not in expected or value != "RS_DMA_CHANNEL_BULK" for name, value in aliases.items()):
+ raise ValueError("Mini DMA helper alias binding changed")
+ return body
+
+
def validate_programming(spec: dict, ids: set[str], root: Path, revision: str) -> None:
for relative in dependencies(spec):
path = root / relative
@@ -193,7 +213,7 @@ def collect_programming(root: Path, spec: dict, ids: set[str], revision: str) ->
result = copy.deepcopy(spec)
sdk_text = (root / "crt/include/retrosoc/hal/dma.h").read_text()
sdk = constants(sdk_text, "RS_DMA_REQUEST_")
- channels = constants(sdk_text, "RS_DMA_CHANNEL_")
+ channels = constants(mini_channel_enum(sdk_text), "RS_DMA_CHANNEL_")
# Capacity is selected by product; it is not a named channel allocation.
# Keep the Mini publication's role inventory independent of Tiny's count.
channels.update(constants((root / "crt/include/retrosoc/hal/dma_regs.h").read_text(),
diff --git a/publications/tiny_reference.py b/publications/tiny_reference.py
index 42c4a022..df366c3a 100644
--- a/publications/tiny_reference.py
+++ b/publications/tiny_reference.py
@@ -26,6 +26,9 @@ TOP = "rtl/tiny/top/retrosoc_tiny.sv"
RESET_TREE = "rtl/tiny/top/tiny_reset_tree.sv"
SYSCTRL = "rtl/tiny/top/tiny_sysctrl.sv"
ARCHINFO = "rtl/tiny/top/tiny_archinfo.sv"
+CPU = "rtl/tiny/top/tiny_cpu_wrapper.sv"
+CPU_MEM = "rtl/tiny/top/tiny_cpu_mem.sv"
+SRAM = "rtl/tiny/top/tiny_sram.sv"
FAMILIES = {"xpi", "sram", "gpio", "dma", "timer", "pwm", "rtc", "wdg", "uart", "i2c", "clint", "archinfo"}
MANAGED = {"publication_media", "hazard3", "cluster_common", "cluster_archinfo", "cluster_pwm", "cluster_rtc", "cluster_wdg"}
SUPPORTED_SYSCTRL = {
@@ -65,21 +68,30 @@ def facts(root: Path = ROOT) -> dict:
if any(profile.get(key) != value for key, value in required.items()):
raise ValueError("Tiny publication profile changed; review configuration and ISA boundary")
top = (root / TOP).read_text(encoding="utf-8")
- ram = instance_parameters(top, "onchip_ram", "u_sram")
+ ram_reg = instance_parameters((root / SRAM).read_text(), "onchip_ram_reg", "u_reg")
+ ram = {"CapacityKiB": ram_reg.get("CapacityKiB"),
+ "DataWidth": 8 * ram_reg.get("DataBytes", 0), "IdWidth": 1}
dma = instance_parameters(top, "apb4_dma", "u_dma")
- cpu = instance_parameters(top, "mgmt_core_wrapper", "u_cpu")
- if ram != {"CapacityKiB": 128, "DataWidth": 32, "IdWidth": 1, "Ics55SmallBanks": 1}:
+ cpu = instance_parameters(top, "tiny_cpu_wrapper", "u_cpu")
+ if ram != {"CapacityKiB": 128, "DataWidth": 32, "IdWidth": 1} or ram_reg.get("Present") != 1:
raise ValueError("Tiny SRAM parameters changed")
- # The publication remains the reviewed IHP130 compatibility profile;
- # selecting ICS55's 4 KiB geometry does not alter its reported SRAM facts.
- ram = {key: value for key, value in ram.items() if key != "Ics55SmallBanks"}
+ # P4 changes local routing, not physical geometry or the book's IHP130
+ # compatibility selection. Snapshot enforcement still precedes publication.
+ topology = read_topology(root / TOPOLOGY, root / MAP)
+ require_snippets(root, TOP, ["tiny_sram u_sram", "gen_sram_attachment", "axi4_connector u_connector",
+ ".ID_WIDTH(1)", ".DATA_WIDTH(32)"])
+ require_snippets(root, SRAM, ["group < 4", "tiny_sram_group u_group"])
if dma != {"NumChannels": 4, "MaxBurstBeats": 16, "FifoDepth": 32, "RequestMask": 0x7F9, "EnableStreams": 0}:
raise ValueError("Tiny DMA parameters changed")
- if cpu != {"ExternalIrqCount": 30, "EnableAtomics": 0, "TwoCycleBusErrors": 1, "ResetSyncStages": 5}:
+ if cpu != {"ExternalIrqCount": 30, "EnableAtomics": 0, "ResetSyncStages": 5}:
raise ValueError("Tiny CPU integration changed")
require_snippets(root, RESET_TREE, [".STAGE(5)", "assign cpu_ready_rst_n_o = &leaf_rst_n_o;"])
- require_snippets(root, "rtl/ip/core/mgmt_core_wrapper.sv", [".EXTENSION_C(1)", ".EXTENSION_A(EnableAtomics)"])
- require_snippets(root, TOP, ["u_masters_axi4_if[2]", ".axi4(u_masters_axi4_if[0])",
+ require_snippets(root, CPU, ["hazard3_cpu_2port", ".EXTENSION_C(1)", ".EXTENSION_A(EnableAtomics)"])
+ require_snippets(root, CPU_MEM, [".TwoCycleErrors(1'b1)", "ErrorFirst", "ErrorLast"])
+ require_snippets(root, TOP, ["u_masters_axi4_if[3]", "u_targets_axi4_if[23]",
+ "assign u_masters_axi4_if[2].arvalid = 1'b0;",
+ "assign u_masters_axi4_if[2].awvalid = 1'b0;",
+ ".slow_axi4(u_masters_axi4_if[0])",
".axi4(u_masters_axi4_if[1])", ".cfg_apb4(u_sram_apb4_if)",
"assign s_tick = s_tick_count_q == 5'd23;"])
require_snippets(root, SYSCTRL, ["(apb4.paddr[1:0] != 2'd0)", "(apb4.pstrb != 4'hf)",
@@ -99,6 +111,7 @@ def facts(root: Path = ROOT) -> dict:
raise ValueError("Tiny discovery parameters changed")
require_snippets(root, ARCHINFO, ["32'h5449_4e59 : u_info_apb4_if.prdata", ".device_id_valid_i(1'b0)", ".device_id_read_enable_i(1'b0)"])
return {"profile": profile, "sram": ram, "dma": dma, "cpu": cpu, "archinfo": arch,
+ "fabric": topology["fabric"],
"hardware_isa": "RV32IMC", "firmware_isa": profile["ISA"], "soc_id": 0x54494E59,
"system_hz": int(profile["EXT_CLK_HZ"]), "timebase_hz": int(profile["CLINT_TIMEBASE_HZ"])}
@@ -130,7 +143,7 @@ def sysctrl_registers(root: Path) -> dict:
offsets = {r["symbol"]: int(r["offset"], 0) for r in read(root / MAP)["sysctrl_registers"]}
definitions = {
"FAULT_STATUS": ("RW1C", "Sticky first fault. Bit 0 clears pending; a new fault can win after clear.", [("PENDING", 0, 0, "A fault record is pending."), ("WRITE", 1, 1, "Faulting transaction was a write."), ("CLASS", 2, 4, "1: decode error; 2: protocol/target error.")]),
- "FAULT_MASTER": ("RO", "Master of the captured first fault.", [("MASTER", 0, 0, "0 CPU, 1 DMA.")]),
+ "FAULT_MASTER": ("RO", "Master of the captured first fault.", [("MASTER", 0, 1, "0 CPU, 1 DMA, 2 reserved SDIO ingress; SDIO is not integrated.")]),
"FAULT_DETAIL": ("RO", "Captured AXI response code.", [("RESPONSE", 0, 1, "AXI RRESP/BRESP encoding.")]),
"PERF_CTRL": ("RW / command", "Enable counters, clear counters/snapshots or capture a coherent pair. Clear takes precedence over snapshot.", [("ENABLE", 0, 0, "Counter enable state."), ("CLEAR", 1, 1, "Write-one pulse; reads zero."), ("SNAPSHOT", 2, 2, "Write-one pulse; reads zero.")]),
"TEST_STATUS": ("RW sticky", "First full-word valid result is sticky until system reset; write mask 0x8000FF01.", [("PASS", 0, 0, "Pass when valid is set."), ("CODE", 8, 15, "Application-defined result code."), ("VALID", 31, 31, "The first valid write locks the result.")]),
diff --git a/rtl/ip/core/mgmt_core_wrapper.sv b/rtl/ip/core/mgmt_core_wrapper.sv
index 6afa424c..51bacb5b 100644
--- a/rtl/ip/core/mgmt_core_wrapper.sv
+++ b/rtl/ip/core/mgmt_core_wrapper.sv
@@ -72,36 +72,37 @@ module mgmt_core_wrapper #(
.JtagIdcode (`SOC_JTAG_IDCODE),
.ResetSyncStages(ResetSyncStages)
) u_mgmt_debug_wrapper (
- .clk_i (clk_i),
- .rst_n_i (rst_n_i),
- .bridge_idle_i (s_ahbl_idle),
- .jtag_tck_i (jtag_tck_i),
- .jtag_tms_i (jtag_tms_i),
- .jtag_tdi_i (jtag_tdi_i),
- .jtag_trst_n_i (jtag_trst_n_i),
- .jtag_tdo_o (jtag_tdo_o),
- .core_rst_n_o (s_core_rst_n),
- .dbg_req_halt_o (s_dbg_req_halt),
- .dbg_req_halt_on_reset_o (s_dbg_req_halt_on_reset),
- .dbg_req_resume_o (s_dbg_req_resume),
- .dbg_halted_i (s_dbg_halted),
- .dbg_running_i (s_dbg_running),
- .dbg_data0_rdata_o (s_dbg_data0_rdata),
- .dbg_data0_wdata_i (s_dbg_data0_wdata),
- .dbg_data0_wen_i (s_dbg_data0_wen),
- .dbg_instr_data_o (s_dbg_instr_data),
- .dbg_instr_data_vld_o (s_dbg_instr_data_vld),
- .dbg_instr_data_rdy_i (s_dbg_instr_data_rdy),
+ .clk_i(clk_i),
+ .rst_n_i(rst_n_i),
+ .bridge_idle_i(s_ahbl_idle),
+ .jtag_tck_i(jtag_tck_i),
+ .jtag_tms_i(jtag_tms_i),
+ .jtag_tdi_i(jtag_tdi_i),
+ .jtag_trst_n_i(jtag_trst_n_i),
+ .jtag_tdo_o(jtag_tdo_o),
+ .core_rst_n_o(s_core_rst_n),
+ .reset_pending_o(), // Mini retains its existing reset policy.
+ .dbg_req_halt_o(s_dbg_req_halt),
+ .dbg_req_halt_on_reset_o(s_dbg_req_halt_on_reset),
+ .dbg_req_resume_o(s_dbg_req_resume),
+ .dbg_halted_i(s_dbg_halted),
+ .dbg_running_i(s_dbg_running),
+ .dbg_data0_rdata_o(s_dbg_data0_rdata),
+ .dbg_data0_wdata_i(s_dbg_data0_wdata),
+ .dbg_data0_wen_i(s_dbg_data0_wen),
+ .dbg_instr_data_o(s_dbg_instr_data),
+ .dbg_instr_data_vld_o(s_dbg_instr_data_vld),
+ .dbg_instr_data_rdy_i(s_dbg_instr_data_rdy),
.dbg_instr_caught_exception_i(s_dbg_instr_caught_exception),
- .dbg_instr_caught_ebreak_i (s_dbg_instr_caught_ebreak),
- .dbg_sbus_addr_o (s_dbg_sbus_addr),
- .dbg_sbus_write_o (s_dbg_sbus_write),
- .dbg_sbus_size_o (s_dbg_sbus_size),
- .dbg_sbus_vld_o (s_dbg_sbus_vld),
- .dbg_sbus_rdy_i (s_dbg_sbus_rdy),
- .dbg_sbus_err_i (s_dbg_sbus_err),
- .dbg_sbus_wdata_o (s_dbg_sbus_wdata),
- .dbg_sbus_rdata_i (s_dbg_sbus_rdata)
+ .dbg_instr_caught_ebreak_i(s_dbg_instr_caught_ebreak),
+ .dbg_sbus_addr_o(s_dbg_sbus_addr),
+ .dbg_sbus_write_o(s_dbg_sbus_write),
+ .dbg_sbus_size_o(s_dbg_sbus_size),
+ .dbg_sbus_vld_o(s_dbg_sbus_vld),
+ .dbg_sbus_rdy_i(s_dbg_sbus_rdy),
+ .dbg_sbus_err_i(s_dbg_sbus_err),
+ .dbg_sbus_wdata_o(s_dbg_sbus_wdata),
+ .dbg_sbus_rdata_i(s_dbg_sbus_rdata)
);
hazard3_cpu_1port #(
diff --git a/rtl/ip/core/mgmt_debug_reset.sv b/rtl/ip/core/mgmt_debug_reset.sv
index 9505eabc..526d568f 100644
--- a/rtl/ip/core/mgmt_debug_reset.sv
+++ b/rtl/ip/core/mgmt_debug_reset.sv
@@ -19,12 +19,15 @@ module mgmt_debug_reset #(
input logic reset_req_i,
input logic bridge_idle_i,
output logic core_rst_n_o,
+ output logic reset_pending_o,
output logic reset_done_o
);
logic s_pending_d, s_pending_q;
logic s_reset_active_d, s_reset_active_q;
+ assign reset_pending_o = reset_req_i || s_pending_q || s_reset_active_q;
+
always_comb begin
s_pending_d = s_pending_q;
s_reset_active_d = s_reset_active_q;
diff --git a/rtl/ip/core/mgmt_debug_wrapper.sv b/rtl/ip/core/mgmt_debug_wrapper.sv
index 5d6bad1c..6c4faa68 100644
--- a/rtl/ip/core/mgmt_debug_wrapper.sv
+++ b/rtl/ip/core/mgmt_debug_wrapper.sv
@@ -11,11 +11,13 @@
// Hazard3 JTAG-DTM and Debug Module integration. The Debug Module is in the
// management-core clock domain and exposes abstract commands only (no SBA).
module mgmt_debug_wrapper #(
- parameter logic [31:0] JtagIdcode = 32'hDEAD_BEEF,
- parameter int ResetSyncStages = 3
+ parameter logic [31:0] JtagIdcode = 32'hDEAD_BEEF,
+ parameter int ResetSyncStages = 3,
+ parameter bit SeparateSystemReset = 1'b0
) (
input logic clk_i,
input logic rst_n_i,
+ input logic system_rst_n_i,
input logic bridge_idle_i,
input logic jtag_tck_i,
input logic jtag_tms_i,
@@ -23,6 +25,7 @@ module mgmt_debug_wrapper #(
input logic jtag_trst_n_i,
output logic jtag_tdo_o,
output logic core_rst_n_o,
+ output logic reset_pending_o,
output logic dbg_req_halt_o,
output logic dbg_req_halt_on_reset_o,
@@ -154,12 +157,13 @@ module mgmt_debug_wrapper #(
mgmt_debug_reset #(
.ResetSyncStages(ResetSyncStages)
) u_mgmt_debug_reset (
- .clk_i (clk_i),
- .rst_n_i (rst_n_i),
- .reset_req_i (s_sys_reset_req || s_hart_reset_req),
- .bridge_idle_i(bridge_idle_i),
- .core_rst_n_o (core_rst_n_o),
- .reset_done_o (s_reset_done)
+ .clk_i (clk_i),
+ .rst_n_i (SeparateSystemReset ? system_rst_n_i : rst_n_i),
+ .reset_req_i (s_sys_reset_req || s_hart_reset_req),
+ .bridge_idle_i (bridge_idle_i),
+ .core_rst_n_o (core_rst_n_o),
+ .reset_pending_o(reset_pending_o),
+ .reset_done_o (s_reset_done)
);
endmodule
diff --git a/rtl/ip/peripheral/apb4_dma.sv b/rtl/ip/peripheral/apb4_dma.sv
index 9cc49bdd..b5b5a68b 100644
--- a/rtl/ip/peripheral/apb4_dma.sv
+++ b/rtl/ip/peripheral/apb4_dma.sv
@@ -18,6 +18,7 @@ module apb4_dma #(
parameter bit EnableStreams = 1'b1
) (
// verilog_format: off -- integration ports retain established peripheral endpoint names.
+ output logic lifecycle_idle_o,
input logic clk_i,
input logic rst_n_i,
output logic dma_xfer_done_o,
@@ -179,4 +180,7 @@ module apb4_dma #(
.crypto_in_axis (crypto_in_axis),
.crypto_out_axis (crypto_out_axis)
);
+ logic s_bus_busy;
+ assign s_bus_busy = axi4.awvalid || axi4.arvalid || axi4.wvalid;
+ assign lifecycle_idle_o = !(|s_busy) && !(|s_start) && !s_bus_busy;
endmodule
diff --git a/rtl/ip/peripheral/apb4_timer.sv b/rtl/ip/peripheral/apb4_timer.sv
index 5db235bd..6fca13af 100644
--- a/rtl/ip/peripheral/apb4_timer.sv
+++ b/rtl/ip/peripheral/apb4_timer.sv
@@ -10,6 +10,7 @@
module apb4_timer (
// verilog_format: off -- preserve reviewed column alignment
+ output logic lifecycle_idle_o,
input logic clk_i,
input logic rst_n_i,
input logic debug_halted_i,
@@ -89,4 +90,5 @@ module apb4_timer (
.one_shot_done_o (s_one_shot_done)
);
+ assign lifecycle_idle_o = !s_en;
endmodule
diff --git a/rtl/ip/security/apb4_crypto.sv b/rtl/ip/security/apb4_crypto.sv
index f766b5a8..8580dbcc 100644
--- a/rtl/ip/security/apb4_crypto.sv
+++ b/rtl/ip/security/apb4_crypto.sv
@@ -7,6 +7,7 @@ module apb4_crypto #(
parameter logic [5:0] DropResponseMask = 6'd0,
parameter bit DuplicateResponse = 1'b0
) (
+ output logic lifecycle_idle_o,
input logic clk_i,
rst_n_i,
output logic dma_input_proc_o,
@@ -43,9 +44,13 @@ module apb4_crypto #(
logic [11:0] s_offset;
logic [31:0] s_wdata;
logic [ 3:0] s_strb;
- assign s_offset = s_regs_q.offset;
- assign s_wdata = s_regs_q.data;
- assign s_strb = s_regs_q.mask;
+ logic s_transport_busy;
+ assign s_transport_busy = (|s_fifo_busy) || (|s_engine_pending_q) || s_aes_key_busy;
+ assign lifecycle_idle_o = !(|s_busy) && !s_maintenance_busy && !s_transport_busy;
+
+ assign s_offset = s_regs_q.offset;
+ assign s_wdata = s_regs_q.data;
+ assign s_strb = s_regs_q.mask;
crypto_mem_req_t [5:0] s_engine_req, s_maintenance_req, s_store_req;
crypto_mem_resp_t [5:0] s_store_resp, s_engine_resp, s_maintenance_resp;
logic [ 6:0] s_mem_stat;
diff --git a/rtl/ip/serial/apb4_i2c.sv b/rtl/ip/serial/apb4_i2c.sv
index ee367af9..26372198 100644
--- a/rtl/ip/serial/apb4_i2c.sv
+++ b/rtl/ip/serial/apb4_i2c.sv
@@ -13,6 +13,7 @@ module apb4_i2c #(
parameter int RxFifoDepth = 16
) (
// verilog_format: off -- preserve reviewed column alignment
+ output logic lifecycle_idle_o,
input logic clk_i,
input logic rst_n_i,
output logic dma_tx_stall_o,
@@ -66,10 +67,12 @@ module apb4_i2c #(
logic s_recovery_failed_event;
logic s_core_cmd_flush;
- assign i2c.scl_o = 1'b0;
- assign i2c.sda_o = 1'b0;
- assign i2c.scl_oe_o = s_scl_pull_low;
- assign i2c.sda_oe_o = s_sda_pull_low;
+ assign lifecycle_idle_o = !s_en && !s_busy && !s_recovery_active;
+
+ assign i2c.scl_o = 1'b0;
+ assign i2c.sda_o = 1'b0;
+ assign i2c.scl_oe_o = s_scl_pull_low;
+ assign i2c.sda_oe_o = s_sda_pull_low;
i2c_filter u_i2c_filter (
.clk_i (clk_i),
diff --git a/rtl/ip/serial/apb4_i2s.sv b/rtl/ip/serial/apb4_i2s.sv
index be1a54f8..969de72a 100644
--- a/rtl/ip/serial/apb4_i2s.sv
+++ b/rtl/ip/serial/apb4_i2s.sv
@@ -41,6 +41,8 @@ module apb4_i2s (
output logic tx_underrun_evt_o,
output logic rx_overrun_evt_o,
output logic rx_flush_busy_o,
+ output logic lifecycle_idle_o,
+ output logic lifecycle_reset_ready_o,
apb4_if.slave apb4,
axi4_stream_if.sink tx_axis,
axi4_stream_if.source rx_axis,
@@ -120,6 +122,36 @@ module apb4_i2s (
logic s_tx_underrun_sys;
logic s_rx_overrun_sys;
logic s_tx_flush_sent_d, s_tx_flush_sent_q;
+ logic s_cfg_ready;
+ logic [31:0] s_cfg_sent_d, s_cfg_sent_q;
+ logic s_audio_used_d, s_audio_used_q;
+
+ // A disabled host which has never enabled its PHY may change host clock
+ // even when AUDIO is absent. After use, require the disabled configuration
+ // to reach AUDIO, empty FIFOs, and completed flush acknowledgements.
+ assign s_cfg_sent_d = s_cfg_ready ? s_cfg_sys : s_cfg_sent_q;
+ assign s_audio_used_d = s_audio_used_q || s_cfg_sys[`APB4_I2S__CFG_ENABLE];
+ assign lifecycle_reset_ready_o = s_tx_src_ready && !s_tx_flush_busy && !s_rx_flush_busy;
+ assign lifecycle_idle_o = !s_cfg_sys[`APB4_I2S__CFG_ENABLE] &&
+ !s_stream_tx_en && !s_stream_rx_en &&
+ (!s_audio_used_q || ((s_cfg_sent_q == s_cfg_sys) && s_cfg_ready &&
+ s_tx_level == 0 && s_rx_level == 0 && !s_tx_flush_busy && !s_rx_flush_busy));
+ dffr #(
+ .DATA_WIDTH(32)
+ ) u_cfg_sent (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .dat_i (s_cfg_sent_d),
+ .dat_o (s_cfg_sent_q)
+ );
+ dffr #(
+ .DATA_WIDTH(1)
+ ) u_audio_used (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .dat_i (s_audio_used_d),
+ .dat_o (s_audio_used_q)
+ );
assign s_cmd_sys = {s_cmd_rx_flush, s_cmd_tx_flush};
assign s_tx_src_clear = s_cmd_tx_flush && !s_tx_flush_sent_q && !s_tx_src_clear_busy;
@@ -191,7 +223,7 @@ module apb4_i2s (
.src_rst_n_i(rst_n_i),
.src_data_i (s_cfg_sys),
.src_valid_i(1'b1),
- .src_ready_o(),
+ .src_ready_o(s_cfg_ready),
.dst_clk_i (clk_aud_i),
.dst_rst_n_i(rst_aud_n_i),
.dst_data_o (s_cfg_aud),
diff --git a/rtl/ip/serial/apb4_uart.sv b/rtl/ip/serial/apb4_uart.sv
index a3ab46cb..f29f25e2 100644
--- a/rtl/ip/serial/apb4_uart.sv
+++ b/rtl/ip/serial/apb4_uart.sv
@@ -13,6 +13,7 @@ module apb4_uart #(
parameter int RxFifoDepth = 64
) (
// verilog_format: off -- preserve reviewed column alignment
+ output logic lifecycle_idle_o,
input logic clk_i,
input logic rst_n_i,
output logic dma_tx_stall_o,
@@ -53,8 +54,10 @@ module apb4_uart #(
logic s_tx_flow_blocked;
logic s_cts_change;
- assign uart.tx_o = s_tx;
- assign uart.irq_o = s_irq;
+ assign lifecycle_idle_o = !s_tx_busy && !s_rx_active && !s_tx_data_valid && !s_rx_en && !s_break;
+
+ assign uart.tx_o = s_tx;
+ assign uart.irq_o = s_irq;
uart_reg #(
.TxFifoDepth(TxFifoDepth),
diff --git a/rtl/ip/serial/apb4_ws2812.sv b/rtl/ip/serial/apb4_ws2812.sv
index e0a365d7..0b117927 100644
--- a/rtl/ip/serial/apb4_ws2812.sv
+++ b/rtl/ip/serial/apb4_ws2812.sv
@@ -12,6 +12,7 @@ module apb4_ws2812 #(
parameter int TxFifoDepth = 16
) (
// verilog_format: off -- preserve reviewed column alignment
+ output logic lifecycle_idle_o,
input logic clk_i,
input logic rst_n_i,
apb4_if.slave apb4,
@@ -33,8 +34,10 @@ module apb4_ws2812 #(
logic s_done, s_underflow, s_aborted;
logic s_irq, s_dat;
- assign ws2812.dat_o = s_dat;
- assign ws2812.irq_o = s_irq;
+ assign lifecycle_idle_o = !s_busy && !s_reset_active && !s_start && !s_data_valid;
+
+ assign ws2812.dat_o = s_dat;
+ assign ws2812.irq_o = s_irq;
ws2812_reg #(
.TxFifoDepth(TxFifoDepth)
diff --git a/rtl/ip/serial/i2s_recv.sv b/rtl/ip/serial/i2s_recv.sv
index 1b18bbeb..b9e4be7a 100644
--- a/rtl/ip/serial/i2s_recv.sv
+++ b/rtl/ip/serial/i2s_recv.sv
@@ -24,14 +24,14 @@ module i2s_recv (
logic s_lrck_trg;
logic [4:0] s_bit_cnt_d, s_bit_cnt_q;
logic [4:0] s_bit_num;
- logic s_first_bit_d, s_first_bit_q;
+ logic s_done_d, s_done_q;
logic [23:0] s_recv_data_d, s_recv_data_q;
logic [23:0] s_data_d, s_data_q;
assign s_bit_num = bitmode_i ? 5'd23 : 5'd15;
assign data_o = s_data_q;
- assign done_o = s_bit_cnt_q == s_bit_num;
+ assign done_o = s_done_q;
assign s_lrck_trg = lrck_i ^ s_lrck_dely_q;
@@ -89,26 +89,25 @@ module i2s_recv (
);
- always_comb begin
- s_first_bit_d = s_first_bit_q;
- if (s_lrck_trg) s_first_bit_d = adcdat_i;
- end
- dffer #(
+ // I2S changes LRCK one bit before the next sample's MSB. The bit
+ // sampled on that transition is the current sample's LSB, not the next
+ // sample's first bit. Publish completion after data_o has captured it.
+ assign s_done_d = sclk_pos_i && s_lrck_trg;
+ dffr #(
.DATA_WIDTH(1)
- ) u_first_bit_dffer (
+ ) u_done_dffr (
.clk_i (clk_i),
.rst_n_i(rst_n_i),
- .en_i (sclk_pos_i),
- .dat_i (s_first_bit_d),
- .dat_o (s_first_bit_q)
+ .dat_i (s_done_d),
+ .dat_o (s_done_q)
);
always_comb begin
s_data_d = s_data_q;
- if ((!s_lrck_trg) && (s_bit_cnt_q == (s_bit_num - 1'b1))) begin
- if (bitmode_i) s_data_d = {s_recv_data_d[22:0], s_first_bit_q};
- else s_data_d = {8'd0, s_recv_data_d[14:0], s_first_bit_q};
+ if (s_lrck_trg) begin
+ if (bitmode_i) s_data_d = {s_recv_data_q[22:0], adcdat_i};
+ else s_data_d = {8'd0, s_recv_data_q[14:0], adcdat_i};
end
end
dffer #(
diff --git a/rtl/ip/serial/i2s_reg.sv b/rtl/ip/serial/i2s_reg.sv
index 5b246ec4..db6cd72a 100644
--- a/rtl/ip/serial/i2s_reg.sv
+++ b/rtl/ip/serial/i2s_reg.sv
@@ -92,7 +92,7 @@ module i2s_reg (
endfunction
assign s_req = apb4.psel && apb4.penable && !s_ready_q;
- assign s_write = |apb4.pstrb;
+ assign s_write = apb4.pwrite;
assign s_accept = s_req;
assign s_offset = apb4.paddr[11:0];
assign s_aligned = apb4.paddr[1:0] == 2'b00;
diff --git a/rtl/ip/storage/apb4_sdio.sv b/rtl/ip/storage/apb4_sdio.sv
index fbb3237e..fc872bb6 100644
--- a/rtl/ip/storage/apb4_sdio.sv
+++ b/rtl/ip/storage/apb4_sdio.sv
@@ -11,17 +11,20 @@
`timescale 1ns / 1ps
module apb4_sdio #(
- parameter int InputClockHz = 72_000_000,
- parameter int AddrWidth = 32,
- parameter int DataWidth = 32,
- parameter int DescCount = 16
+ parameter int InputClockHz = 72_000_000,
+ parameter bit UseRuntimeClockHz = 1'b0,
+ parameter int AddrWidth = 32,
+ parameter int DataWidth = 32,
+ parameter int DescCount = 16
) (
// verilog_format: off -- preserve the APB, AXI, and pad column alignment
+ output logic lifecycle_idle_o,
input logic clk_i,
input logic rst_n_i,
apb4_if.slave apb4,
axi4_if.master dma_axi4,
- sdio_if.dut sdio
+ sdio_if.dut sdio,
+ input logic [31:0] clock_hz_i
// verilog_format: on
);
logic s_busy;
@@ -80,6 +83,10 @@ module apb4_sdio #(
logic s_pio_read_consume;
sdio_if u_sdio_core_if ();
+ logic s_bus_busy;
+ assign s_bus_busy = dma_axi4.awvalid || dma_axi4.arvalid || dma_axi4.wvalid;
+ assign lifecycle_idle_o = !s_busy && !s_clock_enable && !s_bus_busy;
+
assign u_sdio_core_if.cmd_di_i = sdio.cmd_di_i;
assign u_sdio_core_if.dat_di_i = sdio.dat_di_i;
assign sdio.sck_o = u_sdio_core_if.sck_o;
@@ -149,14 +156,16 @@ module apb4_sdio #(
);
sdio_core #(
- .InputClockHz(InputClockHz),
- .AddrWidth (AddrWidth),
- .DataWidth (DataWidth),
- .DescCount (DescCount)
+ .InputClockHz (InputClockHz),
+ .UseRuntimeClockHz(UseRuntimeClockHz),
+ .AddrWidth (AddrWidth),
+ .DataWidth (DataWidth),
+ .DescCount (DescCount)
) u_sdio_core (
.clk_i (clk_i),
.rst_n_i (rst_n_i),
.host_enable_i (s_host_enable),
+ .clock_hz_i (clock_hz_i),
.host_irq_enable_i (s_host_irq_enable),
.clock_enable_i (s_clock_enable),
.half_period_i (s_half_period),
diff --git a/rtl/ip/storage/apb4_xpi.sv b/rtl/ip/storage/apb4_xpi.sv
index b07e4d01..14320df7 100644
--- a/rtl/ip/storage/apb4_xpi.sv
+++ b/rtl/ip/storage/apb4_xpi.sv
@@ -20,6 +20,7 @@
module apb4_xpi (
// verilog_format: off -- preserve reviewed port alignment
+ output logic lifecycle_idle_o,
input logic clk_i,
input logic rst_n_i,
input logic dma_xfer_done_i,
@@ -176,6 +177,11 @@ module apb4_xpi (
logic s_unused_dma_xfer_done;
logic s_unused_mm_perf_split;
+ logic s_protocol_idle;
+ assign s_protocol_idle = !s_mm_busy && !s_indirect_busy && !s_core_busy;
+ assign lifecycle_idle_o = s_protocol_idle &&
+ (s_owner_q == OwnerNone) && s_tx_fifo_empty && s_rx_fifo_empty;
+
assign s_owner_q = xpi_owner_e'(s_owner_bits_q);
assign s_selected_req_valid = s_mm_busy ? s_mm_req_valid :
(s_indirect_req_valid ? s_indirect_req_valid : s_mm_req_valid);
diff --git a/rtl/ip/storage/sdio_command.sv b/rtl/ip/storage/sdio_command.sv
index 1ad70bb3..bc22b3f2 100644
--- a/rtl/ip/storage/sdio_command.sv
+++ b/rtl/ip/storage/sdio_command.sv
@@ -88,7 +88,10 @@ module sdio_command #(
assign s_frame_crc_error = s_frame_crc != s_resp_shift_next[7:1];
assign s_frame_index_error = s_resp_shift_next[45:40] != s_cmd_index_q;
- assign cmd_oe_o = (s_state_q == Send) || (s_tx_final_hold_q && !sample_tick_i);
+ // Admission can coincide with an SDCLK rising edge. Do not expose the
+ // start bit until the first scheduled launch, or the card sees it twice.
+ assign cmd_oe_o = ((s_state_q == Send) && (s_tx_started_q || launch_tick_i)) ||
+ (s_tx_final_hold_q && !sample_tick_i);
assign cmd_do_o = ((s_state_q == Send) || s_tx_final_hold_q) ? s_tx_shift_q[47] : 1'b1;
assign busy_o = (s_state_q != Idle) && (s_state_q != Complete);
assign done_o = s_done_q;
diff --git a/rtl/ip/storage/sdio_core.sv b/rtl/ip/storage/sdio_core.sv
index dc179701..833832d4 100644
--- a/rtl/ip/storage/sdio_core.sv
+++ b/rtl/ip/storage/sdio_core.sv
@@ -11,15 +11,17 @@
`timescale 1ns / 1ps
module sdio_core #(
- parameter int InputClockHz = 72_000_000,
- parameter int AddrWidth = 32,
- parameter int DataWidth = 32,
- parameter int DescCount = 16
+ parameter int InputClockHz = 72_000_000,
+ parameter bit UseRuntimeClockHz = 1'b0,
+ parameter int AddrWidth = 32,
+ parameter int DataWidth = 32,
+ parameter int DescCount = 16
) (
// verilog_format: off -- preserve the host, status, and external bus groups
input logic clk_i,
input logic rst_n_i,
input logic host_enable_i,
+ input logic [31:0] clock_hz_i,
input logic host_irq_enable_i,
input logic clock_enable_i,
input logic [15:0] half_period_i,
@@ -102,7 +104,7 @@ module sdio_core #(
logic s_data_tx_last;
logic s_data_rx_valid;
logic s_data_rx_ready;
- logic s_data_rx_stall_q;
+ logic s_data_rx_stall;
logic [ 31:0] s_data_rx_data;
logic [ 3:0] s_data_rx_strb;
logic s_data_rx_last;
@@ -174,8 +176,8 @@ module sdio_core #(
assign s_clock_en = host_enable_i && clock_enable_i &&
(s_cmd_busy || s_data_busy || s_dma_busy || cmd_start_i ||
data_start_i) &&
- !s_data_rx_stall_q &&
- !(s_data_busy && s_data_tx_wait && (s_data_oe == 4'b0000));
+ !s_data_rx_stall &&
+ !(s_data_busy && s_data_tx_wait);
assign s_total_bytes = {16'd0, block_size_i} * {16'd0, block_count_i};
assign s_abort_request = host_abort_i || dma_abort_i ||
(s_dma_done && s_dma_err) ||
@@ -200,6 +202,9 @@ module sdio_core #(
assign s_dma_data_in_strb = s_data_rx_strb;
assign s_dma_data_in_last = s_data_rx_last;
assign s_data_rx_ready = data_dma_enable_i ? s_dma_data_in_ready : !s_pio_valid_q;
+ // ReadEmit begins on the sampling edge. Stop before the very next launch
+ // if its word cannot retire; registering this condition loses one card bit.
+ assign s_data_rx_stall = s_data_rx_valid && !s_data_rx_ready;
assign s_core_busy = s_cmd_busy || s_data_busy || s_dma_busy;
assign busy_o = s_core_busy;
assign s_pio_tx_pop = !data_dma_enable_i && s_data_tx_valid && s_data_tx_ready;
@@ -233,7 +238,9 @@ module sdio_core #(
assign bytes_done_o = s_dma_bytes_done;
assign dma_error_addr_o = s_dma_err_addr;
assign dma_error_o = {24'd0, s_dma_err_code};
- assign clock_actual_o = (half_period_i == 16'd0) ? 32'd0 : InputClockHz / (2 * half_period_i);
+ logic [31:0] s_input_clock_hz;
+ assign s_input_clock_hz = UseRuntimeClockHz && (clock_hz_i != 32'd0) ? clock_hz_i : InputClockHz;
+ assign clock_actual_o = (half_period_i == 16'd0) ? 32'd0 : s_input_clock_hz / (2 * half_period_i);
assign status_o = {
23'd0,
s_clock_running,
@@ -401,7 +408,6 @@ module sdio_core #(
s_dat1_meta_q <= 1'b1;
s_dat1_sync_q <= 1'b1;
s_dat1_seen_q <= 1'b0;
- s_data_rx_stall_q <= 1'b0;
s_pio_rdata_q <= '0;
s_pio_valid_q <= 1'b0;
s_pio_tx_rd_q <= '0;
@@ -418,10 +424,9 @@ module sdio_core #(
s_pio_tx_strb_q[index] <= '0;
end
end else begin
- s_dat1_meta_q <= sdio.dat_di_i[1];
- s_dat1_sync_q <= s_dat1_meta_q;
- s_dat1_seen_q <= s_dat1_seen_d;
- s_data_rx_stall_q <= s_data_rx_valid && !s_data_rx_ready;
+ s_dat1_meta_q <= sdio.dat_di_i[1];
+ s_dat1_sync_q <= s_dat1_meta_q;
+ s_dat1_seen_q <= s_dat1_seen_d;
if (data_start_i) begin
s_pio_tx_rd_q <= '0;
diff --git a/rtl/ip/storage/sdio_data.sv b/rtl/ip/storage/sdio_data.sv
index f520cf3b..6a6b0972 100644
--- a/rtl/ip/storage/sdio_data.sv
+++ b/rtl/ip/storage/sdio_data.sv
@@ -160,7 +160,12 @@ module sdio_data #(
assign tx_ready_o = (s_state_q == WriteWaitWord) || (s_state_q == WriteNextWord) ||
((s_state_q == WritePayload) && launch_tick_i && s_tx_word_final &&
(s_tx_total_q < s_target_bytes_q));
- assign tx_wait_o = (s_state_q == WriteWaitWord) || (s_state_q == WriteNextWord);
+ // Stop before launching the next word if memory has not supplied it.
+ // Waiting only after that launch exposes an idle bit to the card and
+ // corrupts the next word's MSB when the clock resumes.
+ assign tx_wait_o = (s_state_q == WriteWaitWord) || (s_state_q == WriteNextWord) ||
+ ((s_state_q == WritePayload) && s_tx_word_final &&
+ (s_tx_total_q < s_target_bytes_q) && !tx_valid_i);
assign rx_valid_o = (s_state_q == ReadEmit);
assign rx_data_o = s_rx_pending_data_q;
assign rx_strb_o = s_rx_pending_strb_q;
diff --git a/rtl/ip/storage/sdio_dma.sv b/rtl/ip/storage/sdio_dma.sv
index 8cea0c60..e92c4b9d 100644
--- a/rtl/ip/storage/sdio_dma.sv
+++ b/rtl/ip/storage/sdio_dma.sv
@@ -516,11 +516,18 @@ module sdio_dma #(
s_err_addr_d = s_payload_addr + s_burst_offset;
end
if (s_write_final_sent_q) begin
- s_writeback_control_d = (s_desc_control_q &
- ~(32'd1 << sdio_pkg::SDIO_DESC_OWN)) |
- (s_fault_d ? (32'd1 << sdio_pkg::SDIO_DESC_ERROR) :
- (32'd1 << sdio_pkg::SDIO_DESC_DONE));
- s_state_d = WritebackStart;
+ // A retired AXI burst is not necessarily the whole descriptor.
+ // Keep ownership while subsequent bursts receive its remaining
+ // bytes, just as the memory-to-card direction does.
+ if (s_fault_d || (s_desc_progress_d >= s_desc_count_q)) begin
+ s_writeback_control_d = (s_desc_control_q &
+ ~(32'd1 << sdio_pkg::SDIO_DESC_OWN)) |
+ (s_fault_d ? (32'd1 << sdio_pkg::SDIO_DESC_ERROR) :
+ (32'd1 << sdio_pkg::SDIO_DESC_DONE));
+ s_state_d = WritebackStart;
+ end else begin
+ s_state_d = PayloadWriteStart;
+ end
end
end
end
diff --git a/rtl/mk/software.mk b/rtl/mk/software.mk
index c5611759..7815caed 100644
--- a/rtl/mk/software.mk
+++ b/rtl/mk/software.mk
@@ -123,6 +123,7 @@ CRT_SRCS := $(ROOT_PATH)/crt/arch/riscv/startup.S \
$(ROOT_PATH)/crt/src/hal/clint.c \
$(ROOT_PATH)/crt/src/hal/clock.c \
$(ROOT_PATH)/crt/src/hal/sysctrl.c \
+ $(ROOT_PATH)/crt/src/hal/rcu.c \
$(ROOT_PATH)/crt/src/hal/uart_math.c \
$(ROOT_PATH)/crt/src/hal/uart.c \
$(ROOT_PATH)/crt/src/hal/gpio_math.c \
@@ -182,7 +183,8 @@ 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
+ dma_math.c dma.c onchip_sram.c xpi.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
endif
@@ -285,12 +287,12 @@ asm: $(MPW_VARIANT_DEP) $(MEMORY_MAP_STAMP)
cp $(SW_BUILD_DIR)/asm/hello-asm.bin $(SW_BUILD_DIR)/$(ASM_FIRMWARE_NAME).bin
cp $(SW_BUILD_DIR)/asm/hello-asm.txt $(SW_BUILD_DIR)/$(ASM_FIRMWARE_NAME)_all.txt
+endif
+
$(CRYPTO_CONSTANTS_HEADER): $(ROOT_PATH)/scripts/crypto_constants.py $(ROOT_PATH)/scripts/data/crypto_constants.json
$(PYTHON) $(ROOT_PATH)/scripts/crypto_constants.py --root $(ROOT_PATH) --output $(SW_BUILD_DIR)/include
-endif
-
-CRYPTO_CONSTANTS_DEP := $(if $(filter TINY,$(SOC)),,$(CRYPTO_CONSTANTS_HEADER))
+CRYPTO_CONSTANTS_DEP := $(CRYPTO_CONSTANTS_HEADER)
$(FIRMWARE_ELF): $(MPW_VARIANT_DEP) $(MEMORY_MAP_STAMP) $(SOC_TOPOLOGY_STAMP) $(SOC_IRQ_METADATA) $(USER_EXTENSIONS_STAMP) $(ARCHINFO_METADATA_STAMP) $(VERSION_HEADER) $(CRYPTO_CONSTANTS_DEP) $(SRC_PATH) $(SW_HEADERS) $(LDS_PATH) \
$(ROOT_PATH)/rtl/mk/software.mk $(APP_MK)
diff --git a/rtl/rtl_readiness.json b/rtl/rtl_readiness.json
index 616992fc..d2973028 100644
--- a/rtl/rtl_readiness.json
+++ b/rtl/rtl_readiness.json
@@ -1,6 +1,40 @@
{
"schema_version": 1,
"targets": [
+ {
+ "name": "tiny-r2-p5",
+ "paths": ["rtl/tiny/top"],
+ "status": "prototype",
+ "baseline_revision": "3131762b35e0ebfcd196395f955e7c1005bd18e7",
+ "configuration_digest": null,
+ "configuration_profiles": ["configs/ci/ics55-tiny.mk", "configs/ci/ihp130-tiny.mk"],
+ "required_evidence": [],
+ "synthesis_intent": {
+ "registers": "Common registers and bounded response spill registers per external target",
+ "reset": "accepted CPU work drains before hart reset; system reset invalidates all sessions",
+ "clock_enable": "all fabric, APB, CPU and SRAM logic remains on direct SAFE24 SYS",
+ "memory": "unchanged 32 SRAM macros; no global SRAM transaction lock",
+ "pipeline": "one combined transaction per target, CPU one combined, DMA/SDIO one per direction"
+ },
+ "waivers": []
+ },
+ {
+ "name": "tiny-r2-p4",
+ "paths": ["rtl/tiny/top"],
+ "status": "prototype",
+ "baseline_revision": "4db27524a6282ca40d76b935415b89f043d6ea83",
+ "configuration_digest": null,
+ "configuration_profiles": ["configs/ci/ics55-tiny.mk", "configs/ci/ihp130-tiny.mk"],
+ "required_evidence": [],
+ "synthesis_intent": {
+ "registers": "Common registers; independent I/D request state and per-group responses",
+ "reset": "five-edge release; hart halt/drain leaves SRAM and unrelated DMA active",
+ "clock_enable": "CPU and all 32 main-SRAM macros use direct SAFE24 SYS",
+ "memory": "four groups of eight tc_sram_1024x32 macros; no payload reset or extra SRAM",
+ "pipeline": "AHB capture, macro issue, registered response; uncontended completion within three cycles"
+ },
+ "waivers": []
+ },
{
"name": "owned-rtl",
"paths": [
diff --git a/rtl/rtl_style_audit.json b/rtl/rtl_style_audit.json
index 1dc7ba28..7cfbeac7 100644
--- a/rtl/rtl_style_audit.json
+++ b/rtl/rtl_style_audit.json
@@ -439,8 +439,26 @@
"tiny_archinfo.sv",
"tiny_axi42apb4.sv",
"tiny_axi4_fabric.sv",
+ "tiny_axi4_target.sv",
+ "tiny_cpu_mem.sv",
+ "tiny_cpu_wrapper.sv",
+ "tiny_fault_collector.sv",
+ "tiny_clock_reset_subsystem.sv",
+ "tiny_axi4_async_bridge.sv",
+ "tiny_apb_async_bridge.sv",
+ "tiny_axi4_admission.sv",
+ "tiny_managed_lifecycle.sv",
+ "tiny_peripheral_leaf.sv",
+ "tiny_peripheral_control.sv",
+ "tiny_clock_output.sv",
+ "tiny_clock_gate.sv",
"tiny_reset_tree.sv",
- "tiny_sysctrl.sv"
+ "tiny_sram.sv",
+ "tiny_sram_axi4.sv",
+ "tiny_sram_group.sv",
+ "tiny_sram_port_if.sv",
+ "tiny_sysctrl.sv",
+ "tiny_rcu_regs.svh"
]
},
"reviewed_boundaries": [
diff --git a/rtl/tech/tc_pll.sv b/rtl/tech/tc_pll.sv
index 434178e4..81eb3830 100644
--- a/rtl/tech/tc_pll.sv
+++ b/rtl/tech/tc_pll.sv
@@ -8,6 +8,7 @@
// MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE.
// See the Mulan PSL v2 for more details.
+`timescale 1ns / 1ps
module tc_pll #(
parameter bit UseIcs55EcosPll = 1'b0,
parameter bit Ics55Parked = 1'b0
@@ -28,10 +29,10 @@ module tc_pll #(
logic [2:0] s_lock_count_d, s_lock_count_q;
logic s_lock_d, s_lock_q;
logic s_apply_seen_d, s_apply_seen_q;
- logic s_cfg_apply;
- logic s_force_lock_fail;
- time s_last_ref_edge;
- time s_ref_period;
+ logic s_cfg_apply;
+ logic s_force_lock_fail;
+ realtime s_last_ref_edge;
+ realtime s_ref_period;
function automatic int pll_multiplier(input logic [2:0] cfg_sel);
case (cfg_sel)
@@ -55,14 +56,14 @@ module tc_pll #(
always @(posedge fref_i) begin
if (s_last_ref_edge != 0) begin
- s_ref_period = $time - s_last_ref_edge;
+ s_ref_period = $realtime - s_last_ref_edge;
end
- s_last_ref_edge = $time;
+ s_last_ref_edge = $realtime;
end
always begin
@(posedge fref_i);
- wait (pll_lock_o);
+ wait (pll_lock_o && s_ref_period > 0.0);
while (pll_lock_o) begin
#(s_ref_period / (2 * pll_multiplier(s_cfg_sel_q)));
if (pll_lock_o) begin
diff --git a/rtl/tiny/Makefile b/rtl/tiny/Makefile
index d327bb2f..f0947d58 100644
--- a/rtl/tiny/Makefile
+++ b/rtl/tiny/Makefile
@@ -60,6 +60,7 @@ OPENSTA_CONFIG := $(SYN_BUILD_ROOT)/out/$(RTL_NAME)_yosys.config
NETLIST_PATH := $(OPENSTA_NETLIST)
TINY_CONVERTED := $(SIM_BUILD_ROOT)/converted_soc.v
TINY_DEPFILE := $(SIM_BUILD_ROOT)/sources.d
+TINY_TB_DEPFILE := $(SIM_BUILD_ROOT)/testbench.d
TINY_LINT_LOG := $(VARIANT_ROOT)/lint/verilator/lint.log
SOC_SIM_TIME ?= 1800
TINY_SIM_TIMEOUT ?= 20000000
@@ -69,7 +70,7 @@ IVERILOG ?= iverilog
VVP ?= vvp
include $(ROOT_PATH)/rtl/mk/software.mk
--include $(TINY_DEPFILE)
+-include $(TINY_DEPFILE) $(TINY_TB_DEPFILE)
$(TINY_CONVERTED): $(FILELIST_STAMP)
@mkdir -p $(@D)
@@ -95,6 +96,9 @@ $(SIM_BUILD_ROOT)/simv: $(TINY_CONVERTED) $(wildcard $(RTL_PATH)/dv/*.sv)
--log $(SIM_BUILD_ROOT)/compile.log --result $(SIM_BUILD_ROOT)/result-compile.json -- \
$(IVERILOG) -g2012 -gno-specify -s $(RTL_TOP) -o $@ -f $(GENERATED_FL_DIR)/def.fl \
-f $(GENERATED_FL_DIR)/inc.fl -f $(PDK_FILELIST) $(TINY_TB_FLIST) $(TINY_CONVERTED)
+ $(PYTHON) $(ROOT_PATH)/scripts/rtl/filelist_deps.py -f $(GENERATED_FL_DIR)/def.fl \
+ -f $(GENERATED_FL_DIR)/inc.fl $(TINY_TB_FLIST) \
+ --target $@ --output $(TINY_TB_DEPFILE)
comp: $(SIM_BUILD_ROOT)/simv
TINY_RUN := $(VVP) $(SIM_BUILD_ROOT)/simv
endif
@@ -200,4 +204,35 @@ tiny-r2-p3-matrix: | manifest
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
\ No newline at end of file
+.PHONY: tiny-r2-p3-image tiny-r2-p3-sim tiny-r2-p3-matrix tiny-r2-p3-report
+
+# P4 preserves old image identity and replays each selected image exactly once.
+TINY_P4_REFERENCE ?=
+TINY_P4_CASES ?= perf-o2-plain-flat,perf-o2-plain-banked
+tiny-r2-p4-replay: | manifest
+ $(PYTHON) $(ROOT_PATH)/scripts/tiny_r2_p4.py --root $(ROOT_PATH) --variant-root $(VARIANT_ROOT) \
+ --reference $(TINY_P4_REFERENCE) --cases $(TINY_P4_CASES) --simulator $(SIM_TOOL_NAME) \
+ --vvp $(VVP) --timeout $(SOC_SIM_TIME) -- \
+ 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-p4-report:
+ $(PYTHON) $(ROOT_PATH)/scripts/tiny_r2_p4.py --root $(ROOT_PATH) --variant-root $(VARIANT_ROOT) --report
+
+.PHONY: tiny-r2-p4-replay tiny-r2-p4-report
+
+# P5 compares the same P3 binaries to retained P4 observations, with its own
+# phase identity and evidence root. The historical P3/P4 commands stay intact.
+TINY_P5_REFERENCE ?=
+TINY_P5_CASES ?= perf-o2-plain-flat,perf-o2-plain-banked
+tiny-r2-p5-replay: | manifest
+ $(PYTHON) $(ROOT_PATH)/scripts/tiny_r2_p5.py --root $(ROOT_PATH) --variant-root $(VARIANT_ROOT) \
+ --reference $(TINY_P5_REFERENCE) --cases $(TINY_P5_CASES) --simulator $(SIM_TOOL_NAME) \
+ --vvp $(VVP) --timeout $(SOC_SIM_TIME) -- \
+ 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-p5-report:
+ $(PYTHON) $(ROOT_PATH)/scripts/tiny_r2_p5.py --root $(ROOT_PATH) --variant-root $(VARIANT_ROOT) --report
+
+.PHONY: tiny-r2-p5-replay tiny-r2-p5-report
\ No newline at end of file
diff --git a/rtl/tiny/README.md b/rtl/tiny/README.md
index e172e693..9cdf0d8d 100644
--- a/rtl/tiny/README.md
+++ b/rtl/tiny/README.md
@@ -7,7 +7,20 @@ Use `make CONFIG=configs/ci/ics55-tiny.mk setup` followed by
`make CONFIG=configs/ci/ics55-tiny.mk firmware sim`. IHP130/no PLL remains
explicit compatibility through `configs/ci/ihp130-tiny.mk`. See the
[platform runbook](../../docs/ip/tiny-ics55-platform.md) for source-bound
-validation; backend frequency tests are not SYS switching or timing qualification.
+validation of the historical SAFE24 platform. The
+[R2-P7 runbook](../../docs/ip/tiny-soc-r2-clock-reset.md) describes the integrated
+RCU, runtime domains and separate acceptance evidence. Backend frequency tests
+alone are not full-SoC or physical qualification.
+
+The [R2-P4 implementation](../../docs/ip/tiny-soc-r2-local-memory.md) connects the
+official dual-port CPU to four independently serviced 32 KiB local SRAM groups.
+The 32 physical macros and register ABI are unchanged. Only non-SRAM CPU work
+uses the tagged slow path. The [P5 implementation](../../docs/ip/tiny-soc-r2-concurrent-fabric.md)
+replaces external serialization with independent target ownership and APB
+branches. The [P6 candidate](../../docs/ip/tiny-soc-r2-shared-integration.md)
+activates the SDIO ingress, eight-channel DMA, RNG/CRC/WS2812/Crypto and the
+existing I2S master path. Its validation and acceptance remain in the ledger.
+Source presence alone is not completed phase or physical qualification.
`top` owns product integration, `address_map`, `pin_map` and `integration` own
canonical address, pad, IRQ and clock/reset inputs, `filelist` selects sources,
@@ -43,8 +56,8 @@ final timing closure is mandatory. Software/DMA scheduling is `TINY-R2-P3`, loca
`TINY-R2-P9`, system qualification `TINY-R2-P10` and complete-product physical
qualification `TINY-R2-P11` (historical IHP130 title; now default ICS55).
Legacy `TINY-P0` through `TINY-P12` remain in
-the contract history. The committed RTL/profile still implements the initial
-24 MHz/no-PLL/four-channel design; no faster CPU/SRAM or PLL timing support
+the contract history. The P6 candidate retains SAFE24 with eight DMA channels;
+ICS55's present PLL stays off. No faster CPU/SRAM or PLL timing support
follows from this freeze or the `HAVE_PLL` selector alone.
The R2-P2 candidate keeps that executable clock/memory configuration and adds
@@ -88,8 +101,8 @@ FIFOs, APB4 `0x1001D000..0x1001DFFF`, IRQ25 and RCU target16. DMA V2.2 adds
paced fixed-MMIO requests `SPI_TX=16` / `SPI_RX=17`; payload MMIO retains
single-beat transactions. TX borrows channel3 and RX channel2 only after
prior owners fully drain and release them. No extra AXI master, DMA channel,
-SRAM capacity or pad is added. None of these paths exists in the current
-24 MHz/no-PLL/four-channel RTL/profile.
+SRAM capacity or pad is added. SPI and its requests remain absent from the
+SAFE24 P6 candidate.
The frozen Gen1 routes add only GPIO27 ALT0 SCK, GPIO28 ALT1 MOSI, GPIO30
ALT1 MISO and GPIO31 ALT1 CS_N. Write-only display mode uses ordinary GPIO30
diff --git a/rtl/tiny/address_map/memory_map.json b/rtl/tiny/address_map/memory_map.json
index 0036f910..51f5aea5 100644
--- a/rtl/tiny/address_map/memory_map.json
+++ b/rtl/tiny/address_map/memory_map.json
@@ -300,15 +300,6 @@
"public": true,
"user_access": "none"
},
- {
- "symbol": "APB4_I2C1",
- "base": "0x10011000",
- "size": "0x00001000",
- "route": "apb4_periph",
- "kind": "active",
- "public": true,
- "user_access": "rw"
- },
{
"symbol": "APB4_GPIO_ADMIN",
"base": "0x10014000",
@@ -327,15 +318,6 @@
"public": true,
"user_access": "none"
},
- {
- "symbol": "APB4_UART1",
- "base": "0x10018000",
- "size": "0x00001000",
- "route": "apb4_periph",
- "kind": "active",
- "public": true,
- "user_access": "rw"
- },
{
"symbol": "APB4_ARCHINFO",
"base": "0x20000000",
@@ -399,6 +381,60 @@
"public": true,
"user_access": "rw",
"burst": "incr16"
+ },
+ {
+ "symbol": "APB4_RNG",
+ "base": "0x20001000",
+ "size": "0x00001000",
+ "route": "apb4_system",
+ "kind": "active",
+ "public": true,
+ "user_access": "none"
+ },
+ {
+ "symbol": "APB4_CRC",
+ "base": "0x20006000",
+ "size": "0x00001000",
+ "route": "apb4_system",
+ "kind": "active",
+ "public": true,
+ "user_access": "none"
+ },
+ {
+ "symbol": "APB4_WS2812",
+ "base": "0x10008000",
+ "size": "0x00001000",
+ "route": "apb4_periph",
+ "kind": "active",
+ "public": true,
+ "user_access": "none"
+ },
+ {
+ "symbol": "APB4_CRYPTO",
+ "base": "0x1000C000",
+ "size": "0x00001000",
+ "route": "apb4_periph",
+ "kind": "active",
+ "public": true,
+ "user_access": "none"
+ },
+ {
+ "symbol": "APB4_I2S",
+ "base": "0x10007000",
+ "size": "0x00001000",
+ "route": "apb4_periph",
+ "kind": "active",
+ "public": true,
+ "user_access": "none"
+ },
+ {
+ "symbol": "APB4_SDIO0",
+ "base": "0x1000F000",
+ "size": "0x00001000",
+ "route": "apb4_periph",
+ "kind": "active",
+ "public": true,
+ "user_access": "none"
}
],
"legacy_rib": false,
diff --git a/rtl/tiny/dv/retrosoc_tiny_tb.sv b/rtl/tiny/dv/retrosoc_tiny_tb.sv
index 8adbfac3..1cef5b6f 100644
--- a/rtl/tiny/dv/retrosoc_tiny_tb.sv
+++ b/rtl/tiny/dv/retrosoc_tiny_tb.sv
@@ -1,3 +1,4 @@
+`include "tiny_topology.svh"
// Copyright (c) 2026 Yuchi Miao
// SPDX-License-Identifier: MulanPSL-2.0
`timescale 1ns / 1ps
@@ -5,27 +6,34 @@
module retrosoc_tiny_tb;
logic s_clk_driver = 1'b0;
wire s_clk = s_clk_driver;
- logic s_reset_driver = 1'b0;
+ // Assert RESET_N after time zero. With a divided REF24 there are no REF
+ // edges while reset is held, so a declaration initialized low is not an
+ // observable asynchronous assertion for every four-state simulator process.
+ logic s_reset_driver = $test$plusargs("p7_legacy_reset_level") ? 1'b0 : 1'b1;
wire s_rst_n = s_reset_driver;
wire s_uart0_tx, s_uart1_tx;
- wire s_uart0_rx = 1'b1;
- wire s_uart1_rx = 1'b1;
- logic s_jtag_tck_driver = 1'b0;
- logic s_jtag_tms_driver = 1'b1;
- logic s_jtag_tdi_driver = 1'b0;
- wire s_jtag_tck = s_jtag_tck_driver;
- wire s_jtag_tms = s_jtag_tms_driver;
- wire s_jtag_tdi = s_jtag_tdi_driver;
- wire s_jtag_trst_n = s_rst_n;
- wire s_jtag_tdo;
- tri1 [31:0] s_gpio;
- wire s_xpi_sck;
- wire [ 3:0] s_xpi_nss;
- tri [ 3:0] s_xpi_data;
- integer s_cycles;
- integer s_max_cycles = 20000000;
- integer s_done_cycles = 0;
- logic [ 7:0] s_uart_byte;
+ wire s_uart0_rx = 1'b1;
+ wire s_uart1_rx = 1'b1;
+ logic s_jtag_tck_driver = 1'b0;
+ logic s_jtag_tms_driver = 1'b1;
+ logic s_jtag_tdi_driver = 1'b0;
+ wire s_jtag_tck = s_jtag_tck_driver;
+ wire s_jtag_tms = s_jtag_tms_driver;
+ wire s_jtag_tdi = s_jtag_tdi_driver;
+ 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;
+`ifndef RETROSOC_SOC__TINY_NETLIST
+ `include "tiny_p6_devices.svh"
+ `include "tiny_p7_observers.svh"
+`endif
+ integer s_max_cycles = 20000000;
+ integer s_done_cycles = 0;
+ logic [7:0] s_uart_byte;
`include "tiny_jtag_smoke.svh"
`ifdef RETROSOC_SOC__TINY_NETLIST
@@ -43,8 +51,12 @@ module retrosoc_tiny_tb;
wire [7:0] s_test_code = u_dut.s_test_code;
`endif
- always #20.833333 s_clk_driver = !s_clk_driver;
+ always #(500000000.0 / `SOC_EXT_CLK_HZ) s_clk_driver = !s_clk_driver;
initial begin
+ if (!$test$plusargs("p7_legacy_reset_level")) begin
+ #1;
+ s_reset_driver = 1'b0;
+ end
if ($value$plusargs("max_cycles=%d", s_max_cycles)) begin
end
repeat (20) @(posedge s_clk);
@@ -53,7 +65,8 @@ module retrosoc_tiny_tb;
for (s_cycles = 0; s_cycles < s_max_cycles; s_cycles = s_cycles + 1) begin
@(posedge s_clk);
if (u_dut.s_test_done) begin
- if (!u_dut.s_test_pass) $fatal(1, "SIM_TEST_FAIL code=%0d", s_test_code);
+ if (u_dut.s_test_pass !== 1'b1 || (^s_test_code) === 1'bx)
+ $fatal(1, "SIM_TEST_FAIL code=%0d", s_test_code);
s_done_cycles = s_done_cycles + 1;
if (s_done_cycles >= 128 && s_jtag_checks_done) begin
$display("SIM_TEST_PASS Tiny cycles=%0d", s_cycles);
@@ -64,24 +77,15 @@ module retrosoc_tiny_tb;
if (s_done_cycles != 128) $fatal(1, "SIM_TEST_TIMEOUT Tiny cycles=%0d", s_cycles);
end
-`ifdef RETROSOC_SOC__TINY_NETLIST
+`ifdef SIMU_IVERILOG
localparam bit ObserveUartAlways = 1'b1;
`else
localparam bit ObserveUartAlways = 1'b0;
`endif
-`ifdef SIMU_IVERILOG
- localparam bit ObserveUartBaseline = 1'b1;
-`else
- localparam bit ObserveUartBaseline = 1'b0;
-`endif
- // The Verilator RTL already prints accepted TX bytes. The Icarus observer is
- // opt-in for measurements; SYSCTRL remains the authoritative verdict.
+ // The Verilator RTL prints accepted TX bytes. Icarus observes serial output for
+ // all firmware measurements; SYSCTRL remains the authoritative verdict.
initial begin
- if (ObserveUartAlways || (ObserveUartBaseline && ($test$plusargs(
- "tiny_r2_baseline"
- ) || $test$plusargs(
- "tiny_r2_p3"
- )))) begin
+ if (ObserveUartAlways) begin
forever begin
@(negedge s_uart0_tx);
#1627.604;
@@ -230,57 +234,181 @@ module retrosoc_tiny_tb;
`undef _BASELINE_AXI
end
- // The baseline has one AHB port. HPROT[0] distinguishes instruction/data;
- // these observations do not assert future independent local-I/D latency.
- longint unsigned s_ahb_cycle = 0;
- longint unsigned s_ahb_start = 0;
- longint unsigned s_ahb_accept[2], s_ahb_done[2], s_ahb_latency[2];
- logic s_ahb_pending = 1'b0, s_ahb_measured = 1'b0;
- logic s_ahb_data = 1'b0, s_ahb_previous_enable = 1'b0;
- integer s_ahb_window = 0;
- wire s_ahb_enable = u_dut.u_soc.s_perf_enable && s_baseline_enabled;
- `define _BASELINE_AHB u_dut.u_soc.u_cpu.u_ahbl_if
- always @(posedge s_clk) begin
- s_ahb_cycle = s_ahb_cycle + 1;
- if (!u_dut.u_soc.s_rst_n) begin
- s_ahb_pending = 1'b0;
- s_ahb_measured = 1'b0;
- s_ahb_previous_enable = 1'b0;
- s_ahb_window = 0;
- end else if (s_baseline_enabled) begin
- if (s_ahb_enable && !s_ahb_previous_enable) begin
- s_ahb_window = s_ahb_window + 1;
- s_ahb_measured = 1'b0;
- for (int kind = 0; kind < 2; kind++) begin
- s_ahb_accept[kind] = 0;
- s_ahb_done[kind] = 0;
- s_ahb_latency[kind] = 0;
- end
+ // P5-only observations supplement the unchanged P3/P4 record formats. They
+ // are testbench measurements, not new software-visible performance counters.
+ longint unsigned s_p5_overlap = 0, s_p5_beat_overlap = 0;
+ integer s_p5_window = 0, s_p5_active_max = 0;
+ logic s_p5_previous_enable = 0;
+ wire s_p5_enable = s_baseline_enabled && u_dut.u_soc.s_perf_enable;
+ // Icarus 13 miscounts the temporary expression $countones(~a_23_bit_vector).
+ // Count declared bits explicitly and reject unknowns rather than correcting
+ // a report afterward. This function changes only opt-in P5 observations.
+ function automatic integer p5_active_targets(
+ input logic [`RETROSOC_TINY__TARGET_COUNT-1:0] idle_i);
+ integer count;
+ count = 0;
+ for (
+ integer target_index = 0; target_index < `RETROSOC_TINY__TARGET_COUNT; target_index++
+ ) begin
+ if (idle_i[target_index] === 1'b0) count++;
+ else if (idle_i[target_index] !== 1'b1) $fatal(1, "SIM_TEST_FAIL P5 unknown target state");
+ end
+ return count;
+ endfunction
+ initial begin : p5_monitor_check
+ if ($test$plusargs("tiny_r2_p5_monitor_check")) begin
+ for (integer active = 0; active <= `RETROSOC_TINY__TARGET_COUNT; active++) begin
+ if (p5_active_targets(
+ {`RETROSOC_TINY__TARGET_COUNT{1'b1}} ^ ((`RETROSOC_TINY__TARGET_COUNT'(1) << active) - `RETROSOC_TINY__TARGET_COUNT'(1))
+ ) != active)
+ $fatal(1, "SIM_TEST_FAIL P5 monitor population count");
end
- if (`_BASELINE_AHB.hready) begin
- if (s_ahb_pending && s_ahb_measured && s_ahb_enable) begin
- s_ahb_done[s_ahb_data] = s_ahb_done[s_ahb_data] + 1;
- if ((s_ahb_cycle - s_ahb_start) > s_ahb_latency[s_ahb_data])
- s_ahb_latency[s_ahb_data] = s_ahb_cycle - s_ahb_start;
- end
- s_ahb_pending = `_BASELINE_AHB.htrans[1];
- s_ahb_measured = s_ahb_pending && s_ahb_enable;
- if (s_ahb_measured) begin
- s_ahb_data = `_BASELINE_AHB.hprot[0];
- s_ahb_start = s_ahb_cycle;
- s_ahb_accept[s_ahb_data] = s_ahb_accept[s_ahb_data] + 1;
- end
- end
- if (!s_ahb_enable && s_ahb_previous_enable) begin
- for (int kind = 0; kind < 2; kind++) begin
- $display("R2_AHB window=%0d kind=%0d accepted=%0d completed=%0d latency_max=%0d",
- s_ahb_window, kind, s_ahb_accept[kind], s_ahb_done[kind], s_ahb_latency[kind]);
- end
- end
- s_ahb_previous_enable = s_ahb_enable;
+ $display("SIM_TEST_PASS Tiny P5 monitor-check counts=0..27; instrumentation only");
+ $finish;
end
end
- `undef _BASELINE_AHB
+ always @(posedge s_clk) begin
+ if (!u_dut.u_soc.s_rst_n) begin
+ s_p5_previous_enable = 0;
+ s_p5_window = 0;
+ end else if ($test$plusargs("tiny_r2_p5")) begin
+ if (s_p5_enable && !s_p5_previous_enable) begin
+ s_p5_window++;
+ s_p5_overlap = 0;
+ s_p5_beat_overlap = 0;
+ s_p5_active_max = 0;
+ end
+ if (s_p5_enable) begin
+ if (p5_active_targets(u_dut.u_soc.u_fabric.target_idle_o) > s_p5_active_max)
+ s_p5_active_max = p5_active_targets(u_dut.u_soc.u_fabric.target_idle_o);
+ if (u_dut.u_soc.u_fabric.s_busy_q[2] && u_dut.u_soc.u_fabric.s_busy_q[3]) s_p5_overlap++;
+ if (u_dut.u_soc.u_masters_axi4_if[1].rvalid && u_dut.u_soc.u_masters_axi4_if[1].rready &&
+ u_dut.u_soc.u_masters_axi4_if[1].wvalid && u_dut.u_soc.u_masters_axi4_if[1].wready)
+ s_p5_beat_overlap++;
+ end
+ if (!s_p5_enable && s_p5_previous_enable)
+ $display(
+ "R2_P5_FABRIC window=%0d active_targets_max=%0d dma_rw_overlap_cycles=%0d dma_rw_beat_overlap_cycles=%0d",
+ s_p5_window,
+ s_p5_active_max,
+ s_p5_overlap,
+ s_p5_beat_overlap
+ );
+ s_p5_previous_enable = s_p5_enable;
+ end
+ end
+
+ // Verification-only per-group issue/conflict accounting. A bank conflict is
+ // two or more eligible macro clients in one cycle, not an AXI address stall.
+ for (genvar group = 0; group < 4; group++) begin : gen_bank_observer
+ longint unsigned s_issues[3], s_waits[3];
+ longint unsigned s_conflicts = 0;
+ logic s_previous_enable = 0;
+ integer s_window = 0;
+ wire s_enable = u_dut.u_soc.s_perf_enable && s_baseline_enabled;
+ wire [2:0] s_req = u_dut.u_soc.u_sram.gen_group[group].u_group.s_req;
+ wire [2:0] s_grant = u_dut.u_soc.u_sram.gen_group[group].u_group.s_grant;
+ always @(posedge s_clk) begin
+ if (!u_dut.u_soc.s_rst_n) begin
+ s_previous_enable = 0;
+ s_window = 0;
+ end else if (s_baseline_enabled) begin
+ if (s_enable && !s_previous_enable) begin
+ s_window = s_window + 1;
+ s_conflicts = 0;
+ for (int client = 0; client < 3; client++) begin
+ s_issues[client] = 0;
+ s_waits[client] = 0;
+ end
+ end
+ if (s_enable) begin
+ if ((s_req & (s_req - 3'd1)) != 3'd0) s_conflicts = s_conflicts + 1;
+ for (int client = 0; client < 3; client++) begin
+ if (s_grant[client]) s_issues[client] = s_issues[client] + 1;
+ if (s_req[client] && !s_grant[client]) s_waits[client] = s_waits[client] + 1;
+ end
+ end
+ if (!s_enable && s_previous_enable)
+ $display(
+ "R2_P4_BANK window=%0d group=%0d issues_i=%0d issues_d=%0d issues_external=%0d conflicts=%0d wait_i=%0d wait_d=%0d wait_external=%0d",
+ s_window,
+ group,
+ s_issues[0],
+ s_issues[1],
+ s_issues[2],
+ s_conflicts,
+ s_waits[0],
+ s_waits[1],
+ s_waits[2]
+ );
+ s_previous_enable = s_enable;
+ end
+ end
+ end
+
+ // P4 retains the original per-kind record format, now observed on two
+ // independent ports. External AXI counts remain external traffic only.
+ for (genvar port_index = 0; port_index < 2; port_index++) begin : gen_ahb_observer
+ longint unsigned s_cycle = 0, s_start = 0;
+ longint unsigned s_accepted = 0, s_completed = 0, s_latency = 0;
+ longint unsigned s_local_accepted = 0, s_local_completed = 0, s_local_latency = 0;
+ longint unsigned s_admission_wait = 0;
+ logic s_pending = 0, s_measured = 0, s_local = 0, s_previous_enable = 0;
+ integer s_window = 0;
+ wire s_enable = u_dut.u_soc.s_perf_enable && s_baseline_enabled;
+ `define _P4_AHB u_dut.u_soc.u_cpu_ahbl_if[port_index]
+ always @(posedge s_clk) begin
+ s_cycle = s_cycle + 1;
+ if (!u_dut.u_soc.s_core_rst_n) begin
+ s_pending = 0;
+ s_measured = 0;
+ s_previous_enable = 0;
+ s_window = 0;
+ end else if (s_baseline_enabled) begin
+ if (s_enable && !s_previous_enable) begin
+ s_window = s_window + 1;
+ s_measured = 0;
+ s_accepted = 0;
+ s_completed = 0;
+ s_latency = 0;
+ s_local_accepted = 0;
+ s_local_completed = 0;
+ s_local_latency = 0;
+ s_admission_wait = 0;
+ end
+ if (s_enable && `_P4_AHB.htrans[1] && !`_P4_AHB.hready)
+ s_admission_wait = s_admission_wait + 1;
+ if (`_P4_AHB.hready) begin
+ if (s_pending && s_measured && s_enable) begin
+ s_completed = s_completed + 1;
+ if ((s_cycle - s_start) > s_latency) s_latency = s_cycle - s_start;
+ if (s_local) begin
+ s_local_completed = s_local_completed + 1;
+ if ((s_cycle - s_start) > s_local_latency) s_local_latency = s_cycle - s_start;
+ end
+ end
+ s_pending = `_P4_AHB.htrans[1];
+ s_measured = s_pending && s_enable;
+ if (s_measured) begin
+ s_start = s_cycle;
+ s_accepted = s_accepted + 1;
+ s_local = (`_P4_AHB.haddr >= 32'h30000000) && (`_P4_AHB.haddr < 32'h30020000);
+ if (s_local) s_local_accepted = s_local_accepted + 1;
+ end
+ end
+ if (!s_enable && s_previous_enable) begin
+ $display("R2_AHB window=%0d kind=%0d accepted=%0d completed=%0d latency_max=%0d",
+ s_window, port_index, s_accepted, s_completed, s_latency);
+ $display(
+ "R2_P4_LOCAL window=%0d port=%0d accepted=%0d completed=%0d latency_max=%0d admission_wait=%0d",
+ s_window, port_index, s_local_accepted, s_local_completed, s_local_latency,
+ s_admission_wait);
+ end
+ s_previous_enable = s_enable;
+ end
+ end
+ `undef _P4_AHB
+ end
`endif
retrosoc_tiny_asic u_dut (
diff --git a/rtl/tiny/dv/tiny_p6_devices.svh b/rtl/tiny/dv/tiny_p6_devices.svh
new file mode 100644
index 00000000..460bee1e
--- /dev/null
+++ b/rtl/tiny/dv/tiny_p6_devices.svh
@@ -0,0 +1,332 @@
+// Verification-only external devices on the Gen1 GPIO routes. No DUT state is driven.
+logic s_p6_io_trace_started = 0;
+always @(negedge u_dut.u_soc.s_i2c0_clk) begin
+ if ($test$plusargs("p6_debug_io")) begin
+ if (u_dut.u_soc.u_i2c0.s_cmd_valid && !s_p6_io_trace_started) begin
+ s_p6_io_trace_started = 1;
+ $display("P6_IO_DIAGNOSTIC start pads=%b raw=%b%b filtered=%b%b drive=%b%b", s_gpio[11:10],
+ u_dut.u_soc.u_i2c0_if.sda_i, u_dut.u_soc.u_i2c0_if.scl_i,
+ u_dut.u_soc.u_i2c0.s_sda_filtered, u_dut.u_soc.u_i2c0.s_scl_filtered,
+ u_dut.u_soc.u_i2c0.s_sda_pull_low, u_dut.u_soc.u_i2c0.s_scl_pull_low);
+ $fflush();
+ end
+ if (u_dut.u_soc.u_i2c0.s_addr_nack_event || u_dut.u_soc.u_i2c0.s_bus_timeout_event ||
+ u_dut.u_soc.u_i2c0.s_cmd_timeout_event || u_dut.u_soc.u_i2c0.s_stretch_timeout_event ||
+ u_dut.u_soc.u_i2c0.s_arb_lost_event) begin
+ $display(
+ "P6_IO_DIAGNOSTIC nack=%b bus_timeout=%b command_timeout=%b stretch=%b arbitration=%b pads=%b drive=%b%b",
+ u_dut.u_soc.u_i2c0.s_addr_nack_event, u_dut.u_soc.u_i2c0.s_bus_timeout_event,
+ u_dut.u_soc.u_i2c0.s_cmd_timeout_event, u_dut.u_soc.u_i2c0.s_stretch_timeout_event,
+ u_dut.u_soc.u_i2c0.s_arb_lost_event, s_gpio[11:10], u_dut.u_soc.u_i2c0.s_sda_pull_low,
+ u_dut.u_soc.u_i2c0.s_scl_pull_low);
+ $finish; // Diagnostic only: deliberately no SIM_TEST_PASS verdict.
+ end
+ end
+end
+`define _P6_WS u_dut.u_soc.u_ws2812
+logic s_p6_uart_rx = 1;
+logic [7:0] s_p6_uart_byte;
+logic [7:0] s_p6_uart_packet [4];
+integer s_p6_uart_index = 0, s_p6_uart_routes = 0;
+assign s_gpio_9 = s_p6_uart_rx;
+initial begin
+ forever begin
+ @(negedge s_gpio_8);
+ #1627.604;
+ for (integer bit_index = 0; bit_index < 8; bit_index++) begin
+ s_p6_uart_byte[bit_index] = s_gpio_8;
+ #1085.069;
+ end
+ if (s_p6_uart_index != 0 || s_p6_uart_byte == 8'ha5) begin
+ s_p6_uart_packet[s_p6_uart_index] = s_p6_uart_byte;
+ s_p6_uart_index++;
+ if (s_p6_uart_index == 4) begin
+ if (s_p6_uart_packet[0] != 8'ha5 || s_p6_uart_packet[1] != 8'h5a ||
+ s_p6_uart_packet[2] != 8'h96 || s_p6_uart_packet[3] != 8'h69)
+ $fatal(1, "SIM_TEST_FAIL P6 UART GPIO8 framing/data");
+ for (integer byte_index = 0; byte_index < 4; byte_index++) begin
+ s_p6_uart_rx = 0;
+ #1085.069;
+ for (integer bit_index = 0; bit_index < 8; bit_index++) begin
+ s_p6_uart_rx = s_p6_uart_packet[byte_index][bit_index];
+ #1085.069;
+ end
+ s_p6_uart_rx = 1;
+ #1085.069;
+ end
+ s_p6_uart_index = 0;
+ s_p6_uart_routes++;
+ end
+ end
+ end
+end
+integer s_p6_frames = 0, s_p6_completed = 0, s_p6_underflows = 0, s_p6_aborts = 0;
+longint unsigned s_p6_sdio_overlap = 0, s_p6_overlap_fetches = 0;
+always @(posedge u_dut.u_soc.s_exec_clk) begin
+ if (u_dut.u_soc.u_dma.s_busy[2] && u_dut.u_soc.u_sdio.s_dma_status[0]) begin
+ s_p6_sdio_overlap++;
+ if (u_dut.u_soc.u_cpu_ahbl_if[0].hready && u_dut.u_soc.u_cpu_ahbl_if[0].htrans[1])
+ s_p6_overlap_fetches++;
+ end
+end
+integer s_p6_word = 0, s_p6_bit = 0, s_p6_bit_cycle = 0, s_p6_reset_cycles = 0;
+integer s_p6_ws_wait = 0, s_p6_ws_wait_max = 0, s_p6_fifo_max = 0;
+integer s_p6_clock_stops = 0;
+integer s_p6_wire_cases = 0, s_p6_wire_matched = 0, s_p6_wire_examined = 0;
+integer s_p6_wire_edges = 0;
+logic s_p6_wire_found = 0, s_p6_wire_boundary = 0, s_p6_wire_lrck = 0;
+logic [23:0] s_p6_wire_shift = 0;
+logic [31:0] s_p6_wire_word;
+wire s_p6_audio_enabled = u_dut.u_soc.u_i2s.u_i2s_core.enable_i;
+wire s_p6_audio_wide = u_dut.u_soc.u_i2s.u_i2s_core.s_bitmode;
+// Decode actual BCLK/LRCLK/DOUT pads, independently of the receive engine.
+always @(posedge s_gpio[12] or negedge s_p6_audio_enabled) begin
+ if (!s_p6_audio_enabled) begin
+ if (s_p6_wire_found) begin
+ if (s_p6_wire_matched != 512) $fatal(1, "SIM_TEST_FAIL P6 truncated I2S wire payload");
+ s_p6_wire_cases++;
+ $display("R2_P6_I2S_WIRE bits=%0d samples=%0d", s_p6_audio_wide ? 24 : 16, s_p6_wire_matched);
+ end
+ s_p6_wire_matched = 0;
+ s_p6_wire_examined = 0;
+ s_p6_wire_edges = 0;
+ s_p6_wire_found = 0;
+ s_p6_wire_boundary = 0;
+ s_p6_wire_lrck = 0;
+ s_p6_wire_shift = 0;
+ end else begin
+ if (s_gpio[14] !== 1'b0 && s_gpio[14] !== 1'b1) $fatal(1, "SIM_TEST_FAIL P6 unknown I2S DOUT");
+ s_p6_wire_shift = {s_p6_wire_shift[22:0], s_gpio[14]};
+ s_p6_wire_edges++;
+ if (s_gpio[13] != s_p6_wire_lrck) begin
+ if (s_p6_wire_boundary && s_p6_wire_edges != (s_p6_audio_wide ? 24 : 16))
+ $fatal(1, "SIM_TEST_FAIL P6 I2S slot length");
+ s_p6_wire_boundary = 1;
+ s_p6_wire_edges = 0;
+ s_p6_wire_word = s_p6_audio_wide ? {8'd0, s_p6_wire_shift} : {16'd0, s_p6_wire_shift[15:0]};
+ if (!s_p6_wire_found && s_p6_wire_word == (s_p6_audio_wide ? 32'h120020 : 32'h2020))
+ s_p6_wire_found = 1;
+ if (s_p6_wire_found && s_p6_wire_matched < 512) begin
+ if (s_p6_wire_word != (s_p6_audio_wide ? 32'h120020 : 32'h2020) + 32'(s_p6_wire_matched))
+ $fatal(1, "SIM_TEST_FAIL P6 I2S wire ordering/data index=%0d", s_p6_wire_matched);
+ s_p6_wire_matched++;
+ end
+ s_p6_wire_examined++;
+ if (!s_p6_wire_found && s_p6_wire_examined > 40)
+ $fatal(1, "SIM_TEST_FAIL P6 missing I2S wire payload");
+ s_p6_wire_lrck = s_gpio[13];
+ end
+ end
+end
+logic s_p6_busy_previous = 0;
+logic [23:0] s_p6_pixel;
+logic s_p6_expected_high;
+logic s_p6_audio_reference = 1'b0;
+logic s_p6_audio_running = 1'b0;
+initial begin
+ repeat (200) @(negedge u_dut.u_soc.s_ref_clk);
+ if (!u_dut.u_soc.s_cpu_ready_rst_n)
+ $fatal(1, "SIM_TEST_FAIL CPU boot waited for absent AUDIO clock");
+ s_p6_audio_running = 1'b1;
+end
+// GPIO27 is a verification-board control only after the four normal WS frames.
+// Stop low and high in separate cases, then restore the source without DUT forcing.
+always @(posedge s_gpio[27]) begin
+ if (s_p6_frames == 4 && s_p6_clock_stops < 2) begin
+ wait (s_p6_audio_reference == (s_p6_clock_stops != 0));
+ s_p6_audio_running = 0;
+ repeat (4800) @(negedge u_dut.u_soc.s_ref_clk);
+ s_p6_clock_stops++;
+ s_p6_audio_running = 1;
+ end
+end
+always @(negedge u_dut.u_soc.s_ws2812_clk) begin
+ if (`_P6_WS.s_busy && !s_p6_busy_previous) begin
+ s_p6_frames++;
+ s_p6_word = 0;
+ s_p6_bit = 0;
+ s_p6_bit_cycle = 0;
+ s_p6_reset_cycles = 0;
+ end
+ if (`_P6_WS.s_underflow) s_p6_underflows++;
+ if (`_P6_WS.s_aborted) s_p6_aborts++;
+ if (`_P6_WS.s_busy && !`_P6_WS.s_reset_active && s_p6_frames <= 4) begin
+ s_p6_pixel = 24'(s_p6_word * 32'h010203);
+ s_p6_expected_high = s_p6_bit_cycle < (s_p6_pixel[23-s_p6_bit] ? 17 : 8);
+ if (s_gpio[26] !== s_p6_expected_high)
+ $fatal(
+ 1,
+ "SIM_TEST_FAIL Tiny P6 WS pad waveform frame=%0d word=%0d bit=%0d cycle=%0d",
+ s_p6_frames,
+ s_p6_word,
+ s_p6_bit,
+ s_p6_bit_cycle
+ );
+ s_p6_bit_cycle++;
+ if (s_p6_bit_cycle == 30) begin
+ s_p6_bit_cycle = 0;
+ s_p6_bit++;
+ if (s_p6_bit == 24) begin
+ s_p6_bit = 0;
+ s_p6_word++;
+ end
+ end
+ if (`_P6_WS.u_ws2812_reg.s_fifo_level > s_p6_fifo_max)
+ s_p6_fifo_max = integer'(`_P6_WS.u_ws2812_reg.s_fifo_level);
+ if (`_P6_WS.u_ws2812_reg.s_fifo_low && !`_P6_WS.u_ws2812_reg.s_tx_push) s_p6_ws_wait++;
+ else s_p6_ws_wait = 0;
+ if (s_p6_ws_wait > s_p6_ws_wait_max) s_p6_ws_wait_max = s_p6_ws_wait;
+ if (s_p6_ws_wait > 4800 || s_p6_fifo_max > 16)
+ $fatal(1, "SIM_TEST_FAIL Tiny P6 WS service budget/occupancy");
+ end else s_p6_ws_wait = 0;
+ if (`_P6_WS.s_reset_active) begin
+ if (s_gpio[26] !== 1'b0) $fatal(1, "SIM_TEST_FAIL Tiny P6 WS reset-low");
+ s_p6_reset_cycles++;
+ end
+ if (!`_P6_WS.s_busy && s_p6_busy_previous) begin
+ if (s_p6_reset_cycles < 7200) $fatal(1, "SIM_TEST_FAIL Tiny P6 WS reset interval");
+ if (s_p6_frames <= 4) begin
+ if (s_p6_word != ((s_p6_frames % 2) ? 33 : 65) || s_p6_bit != 0 || s_p6_bit_cycle != 0)
+ $fatal(1, "SIM_TEST_FAIL Tiny P6 WS exact frame length");
+ s_p6_completed++;
+ end
+ end
+ s_p6_busy_previous = `_P6_WS.s_busy;
+end
+final begin
+ if (u_dut.s_test_done && u_dut.s_test_pass && s_p6_frames != 0) begin
+ if (s_p6_sdio_overlap == 0 || s_p6_overlap_fetches == 0)
+ $fatal(1, "SIM_TEST_FAIL P6 missing actual SDIO/DMA/CPU contention");
+ $display("R2_P6_CONTENTION sdio_dma_cycles=%0d cpu_fetches=%0d", s_p6_sdio_overlap,
+ s_p6_overlap_fetches);
+ if (s_p6_completed != 4 || s_p6_underflows != 1 || s_p6_aborts != 1 ||
+ s_p6_clock_stops != 2 || s_p6_wire_cases != 2 || s_p6_uart_routes != 1)
+ $fatal(1, "SIM_TEST_FAIL Tiny P6 incomplete device coverage");
+ $display(
+ "R2_P6_DEVICES ws_frames=%0d ws_wait_max=%0d fifo_max=%0d underflows=%0d aborts=%0d clock_stops=%0d",
+ s_p6_completed, s_p6_ws_wait_max, s_p6_fifo_max, s_p6_underflows, s_p6_aborts,
+ s_p6_clock_stops);
+ end
+end
+`undef _P6_WS
+// Counts describe observed bus handshakes over the complete acceptance run.
+// Read bytes require OKAY; write bytes are accepted W strobes, not rollback claims.
+for (genvar owner = 0; owner < 3; owner++) begin : gen_p6_axi_observer
+ longint unsigned read_bytes = 0, write_bytes = 0, descriptor_bytes = 0;
+ longint unsigned ar_wait = 0, aw_wait = 0, w_wait = 0, r_wait = 0, b_wait = 0;
+ longint unsigned ar_max = 0, aw_max = 0, w_max = 0, r_max = 0, b_max = 0;
+ logic pending_read = 0, pending_write = 0, descriptor = 0;
+ logic [2:0] read_size = 0;
+ `define _P6_AXI u_dut.u_soc.u_masters_axi4_if[owner]
+ always @(posedge u_dut.u_soc.s_exec_clk) begin
+ if (!u_dut.u_soc.s_fabric_rst_n) begin
+ pending_read = 0;
+ pending_write = 0;
+ end else begin
+ if (`_P6_AXI.arvalid && `_P6_AXI.arready) begin
+ if (pending_read) $fatal(1, "SIM_TEST_FAIL P6 duplicate read admission");
+ pending_read = 1;
+ read_size = `_P6_AXI.arsize;
+ descriptor = owner == 1 && u_dut.u_soc.u_dma.s_tcd_count[64+:32] != 0 &&
+ `_P6_AXI.araddr >= u_dut.u_soc.u_dma.s_tcd_head[64+:32] &&
+ `_P6_AXI.araddr < u_dut.u_soc.u_dma.s_tcd_head[64+:32] + 32'd256;
+ end
+ if (`_P6_AXI.rvalid && `_P6_AXI.rready) begin
+ if (!pending_read) $fatal(1, "SIM_TEST_FAIL P6 unsolicited read response");
+ if (`_P6_AXI.rresp == 0) begin
+ read_bytes += 64'd1 << read_size;
+ if (descriptor) descriptor_bytes += 64'd1 << read_size;
+ end
+ if (`_P6_AXI.rlast) pending_read = 0;
+ end
+ if (`_P6_AXI.awvalid && `_P6_AXI.awready) begin
+ if (pending_write) $fatal(1, "SIM_TEST_FAIL P6 duplicate write admission");
+ pending_write = 1;
+ end
+ if (`_P6_AXI.wvalid && `_P6_AXI.wready) begin
+ if (!pending_write) $fatal(1, "SIM_TEST_FAIL P6 W without saved AW");
+ for (integer lane = 0; lane < 4; lane++) if (`_P6_AXI.wstrb[lane]) write_bytes++;
+ end
+ if (`_P6_AXI.bvalid && `_P6_AXI.bready) begin
+ if (!pending_write) $fatal(1, "SIM_TEST_FAIL P6 unsolicited write response");
+ pending_write = 0;
+ end
+ ar_wait = `_P6_AXI.arvalid && !`_P6_AXI.arready ? ar_wait + 1 : 0;
+ aw_wait = `_P6_AXI.awvalid && !`_P6_AXI.awready ? aw_wait + 1 : 0;
+ w_wait = `_P6_AXI.wvalid && !`_P6_AXI.wready ? w_wait + 1 : 0;
+ r_wait = `_P6_AXI.rvalid && !`_P6_AXI.rready ? r_wait + 1 : 0;
+ b_wait = `_P6_AXI.bvalid && !`_P6_AXI.bready ? b_wait + 1 : 0;
+ if (ar_wait > ar_max) ar_max = ar_wait;
+ if (aw_wait > aw_max) aw_max = aw_wait;
+ if (w_wait > w_max) w_max = w_wait;
+ if (r_wait > r_max) r_max = r_wait;
+ if (b_wait > b_max) b_max = b_wait;
+ end
+ end
+ final begin
+ if (u_dut.s_test_done && u_dut.s_test_pass && s_p6_frames != 0) begin
+ if (pending_read || pending_write) $fatal(1, "SIM_TEST_FAIL P6 unretired accepted work");
+ $display(
+ "R2_P6_AXI owner=%0d read_bytes=%0d write_bytes=%0d descriptor_bytes=%0d ar_stall_max=%0d aw_stall_max=%0d w_stall_max=%0d r_stall_max=%0d b_stall_max=%0d",
+ owner, read_bytes, write_bytes, descriptor_bytes, ar_max, aw_max, w_max, r_max, b_max);
+ end
+ end
+ `undef _P6_AXI
+end
+always #27.126736111 if (s_p6_audio_running) s_p6_audio_reference = !s_p6_audio_reference;
+assign s_gpio_17 = s_p6_audio_reference;
+// The first integration case is an external codec loopback, across actual pads.
+// Independent wire decoding and error injection supplement firmware readback.
+assign s_gpio_15 = s_gpio_14;
+wire s_p6_card_cmd;
+wire [3:0] s_p6_card_data;
+wire s_p6_card_irq;
+sdio_native_card_model #(
+ .MemoryInitialization(1'b1),
+ .BlockCount (1024)
+) u_p6_card (
+ .rst_n_i (s_rst_n),
+ .sck_i (s_gpio[0]),
+ .cmd_oe_i(u_dut.u_soc.u_sdio_if.cmd_oe_o),
+ .cmd_do_i(s_gpio[1]),
+ .cmd_do_o(s_p6_card_cmd),
+ .dat_oe_i(u_dut.u_soc.u_sdio_if.dat_oe_o),
+ .dat_do_i(s_gpio[5:2]),
+ .dat_do_o(s_p6_card_data),
+ .irq_o (s_p6_card_irq)
+);
+assign s_gpio_1 = u_dut.u_soc.u_sdio_if.cmd_oe_o ? 1'bz : s_p6_card_cmd;
+assign s_gpio_2 = u_dut.u_soc.u_sdio_if.dat_oe_o[0] ? 1'bz : s_p6_card_data[0];
+assign s_gpio_3 = u_dut.u_soc.u_sdio_if.dat_oe_o[1] ? 1'bz : s_p6_card_data[1];
+assign s_gpio_4 = u_dut.u_soc.u_sdio_if.dat_oe_o[2] ? 1'bz : s_p6_card_data[2];
+assign s_gpio_5 = u_dut.u_soc.u_sdio_if.dat_oe_o[3] ? 1'bz : s_p6_card_data[3];
+always @(negedge u_dut.u_soc.s_sdio_clk) begin
+ // Reuse the standalone card's explicit data-stimulus API. The wire command
+ // fixes direction/address/length; host DATA_START only schedules admission.
+ // This is repository controller integration evidence, not card compliance.
+ if (u_dut.u_soc.u_sdio.s_data_start) begin
+ if (u_p6_card.s_last_cmd != 17 && u_p6_card.s_last_cmd != 18 && u_p6_card.s_last_cmd != 25)
+ $fatal(1, "SIM_TEST_FAIL P6 unexpected SDIO data command");
+ if (u_dut.u_soc.u_sdio.s_block_size != 512 ||
+ u_dut.u_soc.u_sdio.s_block_count != (u_p6_card.s_last_cmd == 17 ? 1 : 2) ||
+ u_dut.u_soc.u_sdio.s_data_direction != (u_p6_card.s_last_cmd == 25) ||
+ u_dut.u_soc.u_sdio.s_bus_width != (u_p6_card.s_memory_width4 ? 1 : 0))
+ $fatal(1, "SIM_TEST_FAIL P6 SDIO command/data configuration disagreement");
+ u_p6_card.set_data_delay(1);
+ if (u_p6_card.s_last_cmd == 25)
+ u_p6_card.arm_write(integer'(u_p6_card.s_last_arg * 512), 1024, u_p6_card.s_memory_width4,
+ 1'b0);
+ else
+ u_p6_card.arm_read(integer'(u_p6_card.s_last_arg * 512),
+ u_p6_card.s_last_cmd == 17 ? 512 : 1024, u_p6_card.s_memory_width4, 1'b0);
+ end
+ if ($test$plusargs("p6_debug_sdio") && u_dut.u_soc.u_sdio.s_cmd_start) begin
+ $display("P6_SDIO_DIAGNOSTIC cmd=%0d argument=%h status=%h", u_dut.u_soc.u_sdio.s_cmd_index,
+ u_dut.u_soc.u_sdio.s_cmd_arg, u_dut.u_soc.u_sdio.s_status);
+ $fflush();
+ end
+ if (u_dut.u_soc.u_sdio.s_cmd_start && u_dut.u_soc.u_sdio.s_cmd_index == 13) begin
+ if (u_dut.u_soc.u_sdio.s_cmd_arg == 32'hbadc0de0) u_p6_card.inject_next_crc_error();
+ if (u_dut.u_soc.u_sdio.s_cmd_arg == 32'hbadc0de1) u_p6_card.inject_next_timeout();
+ end
+end
diff --git a/rtl/tiny/dv/tiny_p7_observers.svh b/rtl/tiny/dv/tiny_p7_observers.svh
new file mode 100644
index 00000000..9c04a5dc
--- /dev/null
+++ b/rtl/tiny/dv/tiny_p7_observers.svh
@@ -0,0 +1,65 @@
+// P7-only assertions over production paths. No DUT state is written.
+logic s_p7_monitor = 0;
+longint unsigned s_p7_gated_cycles = 0, s_p7_dma_gated_responses = 0;
+integer s_p7_warm_resets = 0;
+logic [3:0] s_p7_profiles = 0;
+initial s_p7_monitor = $test$plusargs("p7_monitor");
+initial begin
+ if ($test$plusargs("p7_monitor") || $test$plusargs("p7_cold_reset_probe")) begin
+ wait (s_rst_n === 1'b0);
+ @(posedge s_rst_n);
+ wait (u_dut.u_soc.s_cpu_boot_ready === 1'b1);
+ repeat (2) @(negedge u_dut.u_soc.s_ref_clk);
+ // Case comparisons require the exact known reset values in both simulators.
+ if (u_dut.u_soc.u_sysctrl.s_reset_cause_q !== 32'd1 ||
+ u_dut.u_soc.u_sysctrl.s_rcu_fault_q !== 32'd0 ||
+ u_dut.u_soc.s_backend_current !== 2'd0 || u_dut.u_soc.s_gate_request !== 14'd0)
+ $fatal(
+ 1,
+ "SIM_TEST_FAIL P7 cold reset state cause=%h fault=%h profile=%h gates=%h",
+ u_dut.u_soc.u_sysctrl.s_reset_cause_q,
+ u_dut.u_soc.u_sysctrl.s_rcu_fault_q,
+ u_dut.u_soc.s_backend_current,
+ u_dut.u_soc.s_gate_request
+ );
+ if ($test$plusargs("p7_cold_reset_probe")) begin
+ $display("P7_COLD_RESET_PASS");
+ $finish; // Diagnostic only; never emits the full Tiny acceptance marker.
+ end
+ end
+end
+always @(posedge u_dut.u_soc.s_exec_clk) begin
+ if (s_p7_monitor && u_dut.u_soc.s_actual_gate[0]) begin
+ s_p7_gated_cycles++;
+ if (u_dut.u_soc.u_masters_axi4_if[1].bvalid && u_dut.u_soc.u_masters_axi4_if[1].bready)
+ s_p7_dma_gated_responses++;
+ end
+end
+always @(posedge u_dut.u_soc.s_ref_clk) begin
+ if (s_p7_monitor && u_dut.u_soc.s_clock_done) begin
+ s_p7_profiles[u_dut.u_soc.s_backend_current] = 1;
+ $display("P7_CLOCK_COMMIT profile=%0d sys=%0d mem=%0d pclk=%0d", u_dut.u_soc.s_backend_current,
+ u_dut.u_soc.s_backend_sys_hz, u_dut.u_soc.s_backend_mem_hz,
+ u_dut.u_soc.s_backend_pclk_hz);
+ $fflush();
+ end
+ if (s_p7_monitor && u_dut.u_soc.s_system_reset) begin
+ s_p7_warm_resets++;
+ $display("P7_SOFTWARE_RESET request=%0d", s_p7_warm_resets);
+ $fflush();
+ end
+end
+final begin
+ if (s_p7_monitor && u_dut.s_test_done && u_dut.s_test_pass) begin
+ if (s_p7_gated_cycles == 0 || s_p7_dma_gated_responses == 0 || s_p7_warm_resets != 1)
+ $fatal(1, "SIM_TEST_FAIL P7 WFI/DMA/software-reset coverage");
+`ifdef HAVE_PLL
+ if ((s_p7_profiles & 4'b1101) != 4'b1101) $fatal(1, "SIM_TEST_FAIL P7 PLL profile coverage");
+`else
+ if ((s_p7_profiles & 4'b0011) != 4'b0011)
+ $fatal(1, "SIM_TEST_FAIL P7 external profile coverage");
+`endif
+ $display("P7_LIFECYCLE_PASS gated_cycles=%0d dma_responses_while_gated=%0d warm_resets=%0d",
+ s_p7_gated_cycles, s_p7_dma_gated_responses, s_p7_warm_resets);
+ end
+end
diff --git a/rtl/tiny/filelist/clusterip.fl b/rtl/tiny/filelist/clusterip.fl
index 33e91aeb..06ca583e 100644
--- a/rtl/tiny/filelist/clusterip.fl
+++ b/rtl/tiny/filelist/clusterip.fl
@@ -22,3 +22,10 @@
/clusterip/wdg/rtl/wdg_core.sv
/clusterip/wdg/rtl/wdg_reg.sv
/clusterip/wdg/rtl/apb4_wdg.sv
+/clusterip/rng/rtl/rng_core.sv
+/clusterip/rng/rtl/rng_reg.sv
+/clusterip/rng/rtl/rng_deterministic_source.sv
+/clusterip/rng/rtl/apb4_rng.sv
+/clusterip/crc/rtl/crc_core.sv
+/clusterip/crc/rtl/crc_reg.sv
+/clusterip/crc/rtl/apb4_crc.sv
diff --git a/rtl/tiny/filelist/commonip.fl b/rtl/tiny/filelist/commonip.fl
index d603e2c4..f11c72ee 100644
--- a/rtl/tiny/filelist/commonip.fl
+++ b/rtl/tiny/filelist/commonip.fl
@@ -6,6 +6,7 @@
/clusterip/common/rtl/interface/axi4_stream_if.sv
/clusterip/common/rtl/interface/ram_if.sv
/clusterip/common/rtl/utils/register.sv
+/clusterip/common/rtl/utils/xchecker.sv
/clusterip/common/rtl/utils/spill_register.sv
/clusterip/common/rtl/stream/round_robin_arbiter.sv
/clusterip/common/rtl/utils/lfsr.sv
@@ -18,5 +19,7 @@
/clusterip/common/rtl/cdc/cdc_2phase.sv
/clusterip/common/rtl/clkrst/rst_sync.sv
/clusterip/common/rtl/clkrst/counter.sv
+/clusterip/common/rtl/clock/safe_clock_mux.sv
+/clusterip/common/rtl/clock/clock_divider.sv
/ip/util/soc_common_fifo.sv
/ip/util/soc_common_cdc.sv
diff --git a/rtl/tiny/filelist/inc.fl b/rtl/tiny/filelist/inc.fl
index 6bfcb184..8fe3ddaa 100644
--- a/rtl/tiny/filelist/inc.fl
+++ b/rtl/tiny/filelist/inc.fl
@@ -17,3 +17,6 @@
+incdir+/ip/serial
+incdir+/ip/peripheral
+incdir+/ip/core
++incdir+/ip/security
++incdir+/clusterip/rng/rtl
++incdir+/clusterip/crc/rtl
diff --git a/rtl/tiny/filelist/ip.fl b/rtl/tiny/filelist/ip.fl
index 41aeb470..766a200b 100644
--- a/rtl/tiny/filelist/ip.fl
+++ b/rtl/tiny/filelist/ip.fl
@@ -47,3 +47,40 @@
/ip/core/mgmt_debug_wrapper.sv
/ip/core/mgmt_core_wrapper.sv
/ip/peripheral/gpio_pad_bridge.sv
+/ip/storage/sdio_if.sv
+/ip/storage/sdio_pkg.sv
+/ip/storage/sdio_define.svh
+/ip/storage/sdio_crc7.sv
+/ip/storage/sdio_crc16.sv
+/ip/storage/sdio_clock.sv
+/ip/storage/sdio_command.sv
+/ip/storage/sdio_data.sv
+/ip/storage/sdio_dma_descriptor.sv
+/ip/serial/i2s_pkg.sv
+/ip/serial/i2s_reg.sv
+/ip/serial/i2s_clkgen.sv
+/ip/serial/i2s_recv.sv
+/ip/serial/i2s_send.sv
+/ip/serial/i2s_core.sv
+/ip/serial/apb4_i2s.sv
+/ip/serial/ws2812_if.sv
+/ip/serial/ws2812_reg.sv
+/ip/serial/ws2812_core.sv
+/ip/serial/apb4_ws2812.sv
+/ip/security/crypto_pkg.sv
+/ip/security/crypto_mem_pkg.sv
+/ip/security/crypto_define.svh
+/ip/security/crypto_sram_store.sv
+/ip/security/crypto_scrubber.sv
+/ip/security/crypto_clearable_fifo.sv
+/ip/security/crypto_mem_ctrl.sv
+/ip/security/crypto_aes_sram_core.sv
+/ip/security/crypto_aes_sram_engine.sv
+/ip/security/crypto_sha2_sram_engine.sv
+/ip/security/crypto_montgomery_sram.sv
+/ip/security/crypto_rsa_sram_core.sv
+/ip/security/apb4_crypto.sv
+/ip/storage/sdio_dma.sv
+/ip/storage/sdio_reg.sv
+/ip/storage/sdio_core.sv
+/ip/storage/apb4_sdio.sv
diff --git a/rtl/tiny/filelist/tb.fl b/rtl/tiny/filelist/tb.fl
index 106d6b51..da7d4e7d 100644
--- a/rtl/tiny/filelist/tb.fl
+++ b/rtl/tiny/filelist/tb.fl
@@ -1,2 +1,3 @@
/dv/tiny_qspi_nor.sv
+/tests/rtl/sdio_native_model.sv
/dv/retrosoc_tiny_tb.sv
diff --git a/rtl/tiny/filelist/top.fl b/rtl/tiny/filelist/top.fl
index 08b0c7ab..288d99ff 100644
--- a/rtl/tiny/filelist/top.fl
+++ b/rtl/tiny/filelist/top.fl
@@ -1,7 +1,24 @@
+/top/tiny_axi4_target.sv
/top/tiny_axi4_fabric.sv
+/top/tiny_fault_collector.sv
/top/tiny_axi42apb4.sv
/top/tiny_sysctrl.sv
/top/tiny_archinfo.sv
/top/tiny_reset_tree.sv
+/top/tiny_clock_reset_subsystem.sv
+/top/tiny_axi4_async_bridge.sv
+/top/tiny_apb_async_bridge.sv
+/top/tiny_axi4_admission.sv
+/top/tiny_managed_lifecycle.sv
+/top/tiny_peripheral_leaf.sv
+/top/tiny_peripheral_control.sv
+/top/tiny_clock_output.sv
+/top/tiny_clock_gate.sv
+/top/tiny_sram_port_if.sv
+/top/tiny_sram_group.sv
+/top/tiny_sram_axi4.sv
+/top/tiny_sram.sv
+/top/tiny_cpu_mem.sv
+/top/tiny_cpu_wrapper.sv
/top/retrosoc_tiny.sv
/top/retrosoc_tiny_asic.sv
diff --git a/rtl/tiny/integration/clock_reset_domains.json b/rtl/tiny/integration/clock_reset_domains.json
index e9bb824d..ffc83066 100644
--- a/rtl/tiny/integration/clock_reset_domains.json
+++ b/rtl/tiny/integration/clock_reset_domains.json
@@ -1,31 +1,90 @@
{
"schema_version": 2,
- "inactive_macros": {
- "ics55_pll": {
- "cell": "PLL_TOP",
- "scope": "TINY-ICS55-P1: present, EN=0, no SYS connection",
- "active_clock_domain": false,
- "qualification": "no characterized timing arcs; full RCU remains R2-P7"
- }
+ "external_fabric": {
+ "phase": "TINY-R2-P7",
+ "clock_domain": "system",
+ "target_controllers": "u_soc.u_fabric.gen_target[%d].u_controller",
+ "target_count": 27,
+ "apb_bridges": "SYS frontends; acknowledged per-endpoint crossings to AON, PCLK and MEM",
+ "fabric_reset": "fabric leaf; target reset blocks admission and aborts accepted target work with DECERR",
+ "hart_reset": "CPU-only accepted-work drain; DMA and all target responses stay active",
+ "runtime_peripheral_reset": "RCU blocks admissions, drains accepted work, holds reset for five local edges, waits for local release and bus barriers"
+ },
+ "local_memory": {
+ "phase": "TINY-R2-P4",
+ "clock_domain": "system",
+ "cpu_instance": "u_soc.u_cpu.u_hazard3_cpu_2port",
+ "frontend_instance": "u_soc.u_cpu_mem",
+ "macro_instance": "u_soc.u_sram.gen_group[{group}].u_group.gen_bank[{bank}].u_ram.u_mem",
+ "groups": 4,
+ "macros_per_group": 8,
+ "hart_reset": "halt then drain accepted I/D and slow responses; SRAM/DMA stay active",
+ "main_sram_payload_reset": "none",
+ "local_uncontended_max_cycles": 3
},
"reset_ports": [
"ext_rst_n_i_pad",
"jtag_trst_n_i_pad"
],
"domains": [
+ {
+ "name": "aon",
+ "clock": "s_ref_clk",
+ "reset": "ref_rst_n_o",
+ "reset_source": "rst_n_i",
+ "reset_primitive": "rst_sync",
+ "path": "rtl/tiny/top/tiny_clock_reset_subsystem.sv",
+ "instance": "u_ref_rst",
+ "sta": {
+ "source_port": "extclk_i_pad",
+ "pin": "u_clock_buffer/clk_o",
+ "period_ns": 41.666666667,
+ "async_group": "core"
+ }
+ },
{
"name": "system",
- "clock": "clk_i",
+ "clock": "s_backend_sys_clk",
"reset": "system_rst_n_o",
"reset_source": "s_system_req_n",
"reset_primitive": "rst_sync",
"path": "rtl/tiny/top/tiny_reset_tree.sv",
"instance": "u_system_rst_sync",
"sta": {
- "source_port": "extclk_i_pad",
- "pin": "u_clock_buffer/clk_o",
+ "source_domain": "aon",
+ "pin": "u_soc.u_clock_backend.u_sys_mux/clk_o",
"period_ns": 41.666666667,
- "async_group": "system"
+ "async_group": "core"
+ }
+ },
+ {
+ "name": "memory",
+ "clock": "s_backend_mem_clk",
+ "reset": "s_backend_mem_rst_n",
+ "reset_source": "rst_n_i",
+ "reset_primitive": "rst_sync",
+ "path": "rtl/tiny/top/tiny_clock_reset_subsystem.sv",
+ "instance": "u_mem_rst",
+ "sta": {
+ "source_domain": "system",
+ "pin": "u_soc.u_clock_backend.u_mem_mux/clk_o",
+ "period_ns": 41.666666667,
+ "async_group": "core"
+ }
+ },
+ {
+ "name": "peripheral",
+ "clock": "s_backend_pclk",
+ "reset": "pclk_rst_n_o",
+ "reset_source": "rst_n_i",
+ "reset_primitive": "rst_sync",
+ "path": "rtl/tiny/top/tiny_clock_reset_subsystem.sv",
+ "instance": "u_pclk_rst",
+ "sta": {
+ "source_domain": "system",
+ "pin": "u_soc.u_clock_backend.u_pclk_mux/clk_o",
+ "period_ns": 41.666666667,
+ "async_group": "core"
}
},
{
@@ -39,31 +98,152 @@
"sta": {
"source_port": "jtag_tck_i_pad",
"object_type": "port",
- "period_ns": 100.0,
+ "period_ns": 100,
"async_group": "jtag"
}
+ },
+ {
+ "name": "audio",
+ "clock": "s_audio_clk",
+ "reset": "s_audio_rst_n",
+ "reset_source": "s_i2s_rst_n",
+ "reset_primitive": "rst_sync",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv",
+ "instance": "u_audio_rst_sync",
+ "sta": {
+ "source_port": "gpio_17_io_pad",
+ "pin": "u_soc.u_audio_clock_buffer/clk_o",
+ "period_ns": 54.253472222,
+ "async_group": "audio"
+ }
}
],
"reset_distribution": {
"release_edges": 5,
"por": "u_soc.u_reset_tree.u_por_rst_sync",
"system": "u_soc.u_reset_tree.u_system_rst_sync",
- "system_source": "por_rst_n_o && !watchdog_reset_req_i",
+ "system_source": "por_rst_n_o && !watchdog_reset_req_i && backend_reset_n_i",
"leaf_instance_pattern": "u_soc.u_reset_tree.gen_leaf[%d].u_leaf_rst_sync",
"leaf_names": [
- "fabric", "gpio", "uart0", "uart1", "timer0", "timer1", "i2c0", "i2c1",
- "xpi", "dma", "sysctrl", "clint", "sram", "archinfo", "pwm", "rtc", "wdg"
+ "fabric",
+ "gpio",
+ "uart0",
+ "timer0",
+ "timer1",
+ "i2c0",
+ "xpi",
+ "dma",
+ "sysctrl",
+ "clint",
+ "sram",
+ "archinfo",
+ "pwm",
+ "rtc",
+ "wdg",
+ "rng",
+ "crc",
+ "ws2812",
+ "crypto",
+ "i2s",
+ "sdio"
],
- "cpu_prerequisite": "all leaf resets released",
+ "cpu_prerequisite": "all global leaf resets and actual APB/DMA/XPI/RCU barriers; boot readiness latches until the next system reset",
"hart": "u_soc.u_cpu.u_mgmt_debug_wrapper.u_mgmt_debug_reset.u_core_rst_sync",
"jtag": "u_soc.u_cpu.u_mgmt_debug_wrapper.u_jtag_rst_sync",
"dmi": "u_soc.u_cpu.u_mgmt_debug_wrapper.u_dmi_rst_sync",
- "watchdog_functional_reset": "POR only; APB uses wdg leaf",
+ "watchdog_functional_reset": "REF24 five-edge release after backend retains the watchdog reset event; PCLK APB uses the wdg leaf",
"main_sram_payload_reset": "none",
- "crypto": "not integrated; physical erasure contract unchanged",
- "clock_domain": "system"
+ "crypto": "six private macro banks; reset invalidates then scrubs/readbacks; CPU does not wait for READY",
+ "clock_domain": "domain-specific",
+ "peripheral": "tiny_peripheral_leaf; five running hold edges and five release edges; I2S additionally requires actual audio-side barrier readiness"
},
"crossings": [
+ {
+ "name": "debug_pclk_status",
+ "source": "system", "destination": "peripheral", "primitive": "cdc_sync",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv", "instance": "u_debug_pclk_sync"
+ },
+ {
+ "name": "debug_aon_status",
+ "source": "system", "destination": "aon", "primitive": "cdc_sync",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv", "instance": "u_debug_ref_sync"
+ },
+ {
+ "name": "rtc_wake",
+ "source": "aon", "destination": "system", "primitive": "cdc_sync",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv", "instance": "u_rtc_wake_sync"
+ },
+ {
+ "name": "hart_reset_event",
+ "source": "system", "destination": "aon", "primitive": "cdc_2phase",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv", "instance": "u_hart_event_cdc"
+ },
+ {
+ "name": "peripheral_interrupts",
+ "source": "peripheral", "destination": "system", "primitive": "cdc_sync",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv", "instance": "u_irq_sync",
+ "notes": "sticky interrupt levels; this vector also contains AON/MEM and synchronous SYS bits"
+ },
+ {
+ "name": "wfi_permission",
+ "source": "aon", "destination": "system", "primitive": "cdc_sync",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv", "instance": "u_wfi_gate_sync"
+ },
+ {
+ "name": "admission_block_system",
+ "source": "aon", "destination": "system", "primitive": "cdc_sync",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv", "instance": "u_block_sys"
+ },
+ {
+ "name": "admission_block_dma",
+ "source": "aon", "destination": "peripheral", "primitive": "cdc_sync",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv", "instance": "u_block_pclk"
+ },
+ {
+ "name": "gate_reset_disabled",
+ "source": "aon", "destination": "system", "primitive": "cdc_sync",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv", "instance": "u_disabled_sync"
+ },
+ {
+ "name": "peripheral_idle",
+ "source": "peripheral", "destination": "aon", "primitive": "cdc_sync",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv", "instance": "u_periph_idle_sync"
+ },
+ {
+ "name": "xpi_idle",
+ "source": "memory", "destination": "aon", "primitive": "cdc_sync",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv", "instance": "u_xpi_idle_sync"
+ },
+ {
+ "name": "dma_ready",
+ "source": "peripheral", "destination": "system", "primitive": "cdc_sync",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv", "instance": "u_dma_ready_sync"
+ },
+ {
+ "name": "dma_retirement",
+ "source": "peripheral", "destination": "system", "primitive": "cdc_sync",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv", "instance": "u_dma_idle_sync"
+ },
+ {
+ "name": "fabric_retirement",
+ "source": "system", "destination": "aon", "primitive": "cdc_sync",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv", "instance": "u_bus_idle_ref"
+ },
+ {
+ "name": "gate_acknowledgements",
+ "source": "peripheral", "destination": "aon", "primitive": "cdc_sync",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv", "instance": "u_gate_feedback"
+ },
+ {
+ "name": "reset_hold_acknowledgements",
+ "source": "peripheral", "destination": "aon", "primitive": "cdc_sync",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv", "instance": "u_held_feedback"
+ },
+ {
+ "name": "reset_release_acknowledgements",
+ "source": "system", "destination": "aon", "primitive": "cdc_sync",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv", "instance": "u_ready_feedback"
+ },
{
"name": "debug_dmi",
"source": "jtag",
@@ -72,6 +252,17 @@
"path": "rtl/managed/hazard3/hdl/debug/dtm/hazard3_jtag_dtm_core.v",
"instance": "inst_hazard3_apb_async_bridge"
},
+ {
+ "name": "xpi_mem_axi",
+ "source": "system",
+ "destination": "memory",
+ "primitive": "tiny_axi4_async_bridge",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv",
+ "instance": "u_xpi_mem_cdc",
+ "source_reset": "s_fabric_rst_n",
+ "destination_reset": "s_xpi_rst_n",
+ "reset_semantics": "five-edge link release; source-owned held responses survive destination reset; drain remaining W before B and synthesize only missing R beats with DECERR; unfinished warm clear requires destination reset before re-admission"
+ },
{
"name": "debug_hard_reset",
"source": "jtag",
@@ -79,6 +270,152 @@
"primitive": "cdc_sync",
"path": "rtl/ip/core/mgmt_debug_wrapper.sv",
"instance": "u_dmihardreset_cdc_sync"
+ },
+ {
+ "name": "i2s_configuration",
+ "source": "peripheral",
+ "destination": "audio",
+ "primitive": "cdc_2phase",
+ "path": "rtl/ip/serial/apb4_i2s.sv",
+ "instance": "u_i2s_cfg_cdc"
+ },
+ {
+ "name": "i2s_commands",
+ "source": "peripheral",
+ "destination": "audio",
+ "primitive": "cdc_2phase",
+ "path": "rtl/ip/serial/apb4_i2s.sv",
+ "instance": "u_i2s_cmd_cdc"
+ },
+ {
+ "name": "i2s_tx_samples",
+ "source": "peripheral",
+ "destination": "audio",
+ "primitive": "cdc_fifo_warm_flush",
+ "path": "rtl/ip/serial/apb4_i2s.sv",
+ "instance": "u_tx_cdc_fifo"
+ },
+ {
+ "name": "i2s_rx_samples",
+ "source": "audio",
+ "destination": "peripheral",
+ "primitive": "cdc_fifo_warm_flush",
+ "path": "rtl/ip/serial/apb4_i2s.sv",
+ "instance": "u_rx_cdc_fifo"
+ },
+ {
+ "name": "i2s_tx_occupancy",
+ "source": "audio",
+ "destination": "peripheral",
+ "primitive": "cdc_2phase",
+ "path": "rtl/ip/serial/apb4_i2s.sv",
+ "instance": "u_tx_pop_cnt_cdc"
+ },
+ {
+ "name": "i2s_rx_occupancy",
+ "source": "audio",
+ "destination": "peripheral",
+ "primitive": "cdc_2phase",
+ "path": "rtl/ip/serial/apb4_i2s.sv",
+ "instance": "u_rx_push_cnt_cdc"
+ },
+ {
+ "name": "dma_axi",
+ "source": "peripheral",
+ "destination": "system",
+ "primitive": "tiny_axi4_async_bridge",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv",
+ "instance": "u_dma_cdc",
+ "source_reset": "s_dma_rst_n",
+ "destination_reset": "s_fabric_rst_n",
+ "reset_semantics": "five-edge link release; source-owned held responses survive destination reset; drain remaining W before B and synthesize only missing R beats with DECERR; unfinished warm clear requires destination reset before re-admission"
+ },
+ {
+ "name": "rcu_commands",
+ "source": "system",
+ "destination": "aon",
+ "primitive": "tiny_apb_async_bridge",
+ "path": "rtl/tiny/top/tiny_sysctrl.sv",
+ "instance": "u_rcu_cdc"
+ },
+ {
+ "name": "system_rate",
+ "source": "aon",
+ "destination": "system",
+ "primitive": "cdc_2phase",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv",
+ "instance": "u_sys_rate_cdc"
+ },
+ {
+ "name": "peripheral_rate",
+ "source": "aon",
+ "destination": "peripheral",
+ "primitive": "cdc_2phase",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv",
+ "instance": "u_pclk_rate_cdc"
+ },
+ {
+ "name": "dma_done",
+ "source": "peripheral",
+ "destination": "memory",
+ "primitive": "cdc_2phase",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv",
+ "instance": "u_dma_done_cdc"
+ },
+ {
+ "name": "xpi_requests",
+ "source": "memory",
+ "destination": "peripheral",
+ "primitive": "cdc_sync",
+ "path": "rtl/tiny/top/retrosoc_tiny.sv",
+ "instance": "u_xpi_stall_sync"
}
- ]
+ ],
+ "clock_contract": "TINY-R2-P7",
+ "macros": {
+ "ics55_pll": {
+ "cell": "PLL_TOP",
+ "scope": "single RCU-owned macro; off after cold reset; selectors 5/7",
+ "active_clock_domain": true,
+ "qualification": "digital activity/rate qualification only; no analog LOCK or characterized timing arcs"
+ }
+ },
+ "generated_apb_crossings": {
+ "generator": "scripts/generate_tiny.py",
+ "template": "tiny_apb_bridges.svh",
+ "primitive": "tiny_apb_async_bridge",
+ "source": "system",
+ "destinations": {
+ "aon": [
+ "clint",
+ "archinfo"
+ ],
+ "memory": [
+ "xpi"
+ ],
+ "peripheral": [
+ "gpio",
+ "uart0",
+ "timer0",
+ "timer1",
+ "i2c0",
+ "dma",
+ "pwm",
+ "rtc",
+ "wdg",
+ "rng",
+ "crc",
+ "ws2812",
+ "crypto",
+ "i2s"
+ ]
+ },
+ "same_clock_isolation": [
+ "sdio"
+ ],
+ "native_system": [
+ "sysctrl",
+ "sram"
+ ]
+ }
}
diff --git a/rtl/tiny/integration/soc_topology.json b/rtl/tiny/integration/soc_topology.json
index 004795d2..0692b805 100644
--- a/rtl/tiny/integration/soc_topology.json
+++ b/rtl/tiny/integration/soc_topology.json
@@ -1,9 +1,50 @@
{
"schema_version": 1,
+ "cpu": {
+ "module": "hazard3_cpu_2port",
+ "local_ports": 2,
+ "data_outstanding": 1
+ },
+ "main_sram": {
+ "groups": 4,
+ "group_bytes": 32768,
+ "macros_per_group": 8,
+ "macro_bytes": 4096,
+ "base": "0x30000000",
+ "macro_instance": "u_soc.u_sram.gen_group[{group}].u_group.gen_bank[{bank}].u_ram.u_mem",
+ "external_attachment": "per-target-P5"
+ },
+ "fabric": {
+ "owners": [
+ "cpu_slow",
+ "central_dma",
+ "sdio"
+ ],
+ "active_owners": [
+ 0,
+ 1,
+ 2
+ ],
+ "cpu_combined_outstanding": 1,
+ "dma_direction_outstanding": 1,
+ "sdio_direction_outstanding": 1,
+ "target_combined_outstanding": 1,
+ "sram_targets": [
+ 0,
+ 1,
+ 2,
+ 3
+ ],
+ "xpi_target": 4,
+ "apb_target_base": 5,
+ "decode_error_target": 25,
+ "protocol_error_target": 26,
+ "target_count": 27
+ },
"gpio_pins": 32,
"irq_vector_width": 32,
"external_irq_count": 30,
- "dma_channels": 4,
+ "dma_channels": 8,
"apb_targets": [
{
"name": "gpio",
@@ -18,12 +59,6 @@
"APB4_UART0"
]
},
- {
- "name": "uart1",
- "regions": [
- "APB4_UART1"
- ]
- },
{
"name": "timer0",
"regions": [
@@ -42,12 +77,6 @@
"APB4_I2C0"
]
},
- {
- "name": "i2c1",
- "regions": [
- "APB4_I2C1"
- ]
- },
{
"name": "xpi",
"regions": [
@@ -101,6 +130,42 @@
"regions": [
"APB4_WDG"
]
+ },
+ {
+ "name": "rng",
+ "regions": [
+ "APB4_RNG"
+ ]
+ },
+ {
+ "name": "crc",
+ "regions": [
+ "APB4_CRC"
+ ]
+ },
+ {
+ "name": "ws2812",
+ "regions": [
+ "APB4_WS2812"
+ ]
+ },
+ {
+ "name": "crypto",
+ "regions": [
+ "APB4_CRYPTO"
+ ]
+ },
+ {
+ "name": "i2s",
+ "regions": [
+ "APB4_I2S"
+ ]
+ },
+ {
+ "name": "sdio",
+ "regions": [
+ "APB4_SDIO0"
+ ]
}
],
"interrupts": [
@@ -134,11 +199,21 @@
"core_bit": 7,
"signal": "u_i2c0_if.irq_o"
},
+ {
+ "name": "i2s",
+ "core_bit": 8,
+ "signal": "u_i2s_if.irq_o"
+ },
{
"name": "xpi",
"core_bit": 9,
"signal": "s_xpi_irq"
},
+ {
+ "name": "sdio",
+ "core_bit": 10,
+ "signal": "u_sdio_if.irq_o"
+ },
{
"name": "pwm",
"core_bit": 11,
@@ -154,36 +229,44 @@
"core_bit": 14,
"signal": "u_wdg_if.irq_o"
},
+ {
+ "name": "rng",
+ "core_bit": 16,
+ "signal": "s_rng_irq"
+ },
+ {
+ "name": "ws2812",
+ "core_bit": 17,
+ "signal": "u_ws2812_if.irq_o"
+ },
{
"name": "gpio",
"core_bit": 18,
"signal": "u_gpio_if.irq_o"
},
- {
- "name": "i2c1",
- "core_bit": 19,
- "signal": "u_i2c1_if.irq_o"
- },
{
"name": "dma",
"core_bit": 20,
"signal": "s_dma_irq"
},
{
- "name": "uart1",
- "core_bit": 26,
- "signal": "u_uart1_if.irq_o"
+ "name": "crypto",
+ "core_bit": 23,
+ "signal": "s_crypto_irq"
+ },
+ {
+ "name": "rcu",
+ "core_bit": 31,
+ "signal": "s_rcu_irq"
}
],
"gpio_alt_functions": [
{
"pin": 0,
"alt0": {
- "inputs": [
- "u_uart0_if.cts_n_i"
- ],
- "do": "1'b0",
- "oe": "1'b0"
+ "inputs": [],
+ "do": "u_sdio_if.sck_o",
+ "oe": "1'b1"
},
"alt1": {
"inputs": [],
@@ -194,9 +277,11 @@
{
"pin": 1,
"alt0": {
- "inputs": [],
- "do": "u_uart0_if.rts_n_o",
- "oe": "1'b1"
+ "inputs": [
+ "u_sdio_if.cmd_di_i"
+ ],
+ "do": "u_sdio_if.cmd_do_o",
+ "oe": "u_sdio_if.cmd_oe_o"
},
"alt1": {
"inputs": [],
@@ -208,10 +293,10 @@
"pin": 2,
"alt0": {
"inputs": [
- "u_pwm_if.sync_i"
+ "u_sdio_if.dat_di_i[0]"
],
- "do": "1'b0",
- "oe": "1'b0"
+ "do": "u_sdio_if.dat_do_o[0]",
+ "oe": "u_sdio_if.dat_oe_o[0]"
},
"alt1": {
"inputs": [],
@@ -221,36 +306,243 @@
},
{
"pin": 3,
+ "alt0": {
+ "inputs": [
+ "u_sdio_if.dat_di_i[1]"
+ ],
+ "do": "u_sdio_if.dat_do_o[1]",
+ "oe": "u_sdio_if.dat_oe_o[1]"
+ },
+ "alt1": {
+ "inputs": [],
+ "do": "1'b0",
+ "oe": "1'b0"
+ }
+ },
+ {
+ "pin": 4,
+ "alt0": {
+ "inputs": [
+ "u_sdio_if.dat_di_i[2]"
+ ],
+ "do": "u_sdio_if.dat_do_o[2]",
+ "oe": "u_sdio_if.dat_oe_o[2]"
+ },
+ "alt1": {
+ "inputs": [],
+ "do": "1'b0",
+ "oe": "1'b0"
+ }
+ },
+ {
+ "pin": 5,
+ "alt0": {
+ "inputs": [
+ "u_sdio_if.dat_di_i[3]"
+ ],
+ "do": "u_sdio_if.dat_do_o[3]",
+ "oe": "u_sdio_if.dat_oe_o[3]"
+ },
+ "alt1": {
+ "inputs": [],
+ "do": "1'b0",
+ "oe": "1'b0"
+ }
+ },
+ {
+ "pin": 6,
+ "alt0": {
+ "inputs": [],
+ "do": "1'b0",
+ "oe": "1'b0"
+ },
+ "alt1": {
+ "inputs": [],
+ "do": "1'b0",
+ "oe": "1'b0"
+ }
+ },
+ {
+ "pin": 7,
+ "alt0": {
+ "inputs": [],
+ "do": "1'b0",
+ "oe": "1'b0"
+ },
+ "alt1": {
+ "inputs": [],
+ "do": "1'b0",
+ "oe": "1'b0"
+ }
+ },
+ {
+ "pin": 8,
+ "alt0": {
+ "inputs": [],
+ "do": "u_uart0_if.tx_o",
+ "oe": "1'b1"
+ },
+ "alt1": {
+ "inputs": [],
+ "do": "u_pwm_if.pwm_o[0]",
+ "oe": "u_pwm_if.oe_o[0]"
+ }
+ },
+ {
+ "pin": 9,
+ "alt0": {
+ "inputs": [
+ "u_uart0_if.rx_i"
+ ],
+ "do": "1'b0",
+ "oe": "1'b0"
+ },
+ "alt1": {
+ "inputs": [],
+ "do": "u_pwm_if.pwm_o[1]",
+ "oe": "u_pwm_if.oe_o[1]"
+ }
+ },
+ {
+ "pin": 10,
+ "alt0": {
+ "inputs": [
+ "u_i2c0_if.scl_i"
+ ],
+ "do": "u_i2c0_if.scl_o",
+ "oe": "u_i2c0_if.scl_oe_o"
+ },
+ "alt1": {
+ "inputs": [],
+ "do": "u_pwm_if.pwm_o[2]",
+ "oe": "u_pwm_if.oe_o[2]"
+ }
+ },
+ {
+ "pin": 11,
+ "alt0": {
+ "inputs": [
+ "u_i2c0_if.sda_i"
+ ],
+ "do": "u_i2c0_if.sda_o",
+ "oe": "u_i2c0_if.sda_oe_o"
+ },
+ "alt1": {
+ "inputs": [],
+ "do": "u_pwm_if.pwm_o[3]",
+ "oe": "u_pwm_if.oe_o[3]"
+ }
+ },
+ {
+ "pin": 12,
+ "alt0": {
+ "inputs": [],
+ "do": "u_i2s_if.sclk_o",
+ "oe": "1'b1"
+ },
+ "alt1": {
+ "inputs": [],
+ "do": "1'b0",
+ "oe": "1'b0"
+ }
+ },
+ {
+ "pin": 13,
+ "alt0": {
+ "inputs": [],
+ "do": "u_i2s_if.lrck_o",
+ "oe": "1'b1"
+ },
+ "alt1": {
+ "inputs": [],
+ "do": "1'b0",
+ "oe": "1'b0"
+ }
+ },
+ {
+ "pin": 14,
+ "alt0": {
+ "inputs": [],
+ "do": "u_i2s_if.dacdat_o",
+ "oe": "1'b1"
+ },
+ "alt1": {
+ "inputs": [],
+ "do": "1'b0",
+ "oe": "1'b0"
+ }
+ },
+ {
+ "pin": 15,
+ "alt0": {
+ "inputs": [
+ "u_i2s_if.adcdat_i"
+ ],
+ "do": "1'b0",
+ "oe": "1'b0"
+ },
+ "alt1": {
+ "inputs": [],
+ "do": "1'b0",
+ "oe": "1'b0"
+ }
+ },
+ {
+ "pin": 16,
+ "alt0": {
+ "inputs": [],
+ "do": "u_i2s_if.mclk_o",
+ "oe": "1'b1"
+ },
+ "alt1": {
+ "inputs": [],
+ "do": "1'b0",
+ "oe": "1'b0"
+ }
+ },
+ {
+ "pin": 17,
+ "alt0": {
+ "inputs": [
+ "s_audio_pad_clk"
+ ],
+ "do": "1'b0",
+ "oe": "1'b0"
+ },
+ "alt1": {
+ "inputs": [],
+ "do": "1'b0",
+ "oe": "1'b0"
+ }
+ },
+ {
+ "pin": 18,
"alt0": {
"inputs": [],
"do": "u_pwm_if.pwm_o[0]",
"oe": "u_pwm_if.oe_o[0]"
},
"alt1": {
- "inputs": [
- "u_i2c1_if.scl_i"
- ],
- "do": "u_i2c1_if.scl_o",
- "oe": "u_i2c1_if.scl_oe_o"
+ "inputs": [],
+ "do": "1'b0",
+ "oe": "1'b0"
}
},
{
- "pin": 4,
+ "pin": 19,
"alt0": {
"inputs": [],
"do": "u_pwm_if.pwm_o[1]",
"oe": "u_pwm_if.oe_o[1]"
},
"alt1": {
- "inputs": [
- "u_i2c1_if.sda_i"
- ],
- "do": "u_i2c1_if.sda_o",
- "oe": "u_i2c1_if.sda_oe_o"
+ "inputs": [],
+ "do": "1'b0",
+ "oe": "1'b0"
}
},
{
- "pin": 5,
+ "pin": 20,
"alt0": {
"inputs": [],
"do": "u_pwm_if.pwm_o[2]",
@@ -263,7 +555,7 @@
}
},
{
- "pin": 6,
+ "pin": 21,
"alt0": {
"inputs": [],
"do": "u_pwm_if.pwm_o[3]",
@@ -276,37 +568,7 @@
}
},
{
- "pin": 7,
- "alt0": {
- "inputs": [
- "u_i2c0_if.scl_i"
- ],
- "do": "u_i2c0_if.scl_o",
- "oe": "u_i2c0_if.scl_oe_o"
- },
- "alt1": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- }
- },
- {
- "pin": 8,
- "alt0": {
- "inputs": [
- "u_i2c0_if.sda_i"
- ],
- "do": "u_i2c0_if.sda_o",
- "oe": "u_i2c0_if.sda_oe_o"
- },
- "alt1": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- }
- },
- {
- "pin": 9,
+ "pin": 22,
"alt0": {
"inputs": [
"u_pwm_if.fault_i"
@@ -320,192 +582,27 @@
"oe": "1'b0"
}
},
- {
- "pin": 10,
- "alt0": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- },
- "alt1": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- }
- },
- {
- "pin": 11,
- "alt0": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- },
- "alt1": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- }
- },
- {
- "pin": 12,
- "alt0": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- },
- "alt1": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- }
- },
- {
- "pin": 13,
- "alt0": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- },
- "alt1": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- }
- },
- {
- "pin": 14,
- "alt0": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- },
- "alt1": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- }
- },
- {
- "pin": 15,
- "alt0": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- },
- "alt1": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- }
- },
- {
- "pin": 16,
- "alt0": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- },
- "alt1": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- }
- },
- {
- "pin": 17,
- "alt0": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- },
- "alt1": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- }
- },
- {
- "pin": 18,
- "alt0": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- },
- "alt1": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- }
- },
- {
- "pin": 19,
- "alt0": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- },
- "alt1": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- }
- },
- {
- "pin": 20,
- "alt0": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- },
- "alt1": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- }
- },
- {
- "pin": 21,
- "alt0": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- },
- "alt1": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- }
- },
- {
- "pin": 22,
- "alt0": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- },
- "alt1": {
- "inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
- }
- },
{
"pin": 23,
"alt0": {
- "inputs": [],
+ "inputs": [
+ "u_pwm_if.sync_i"
+ ],
"do": "1'b0",
"oe": "1'b0"
},
"alt1": {
"inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
+ "do": "s_clkout",
+ "oe": "s_clkout_enable"
}
},
{
"pin": 24,
"alt0": {
- "inputs": [],
+ "inputs": [
+ "u_pwm_if.capture_i[0]"
+ ],
"do": "1'b0",
"oe": "1'b0"
},
@@ -518,7 +615,9 @@
{
"pin": 25,
"alt0": {
- "inputs": [],
+ "inputs": [
+ "u_pwm_if.capture_i[1]"
+ ],
"do": "1'b0",
"oe": "1'b0"
},
@@ -532,8 +631,8 @@
"pin": 26,
"alt0": {
"inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
+ "do": "u_ws2812_if.dat_o",
+ "oe": "1'b1"
},
"alt1": {
"inputs": [],
@@ -558,8 +657,8 @@
"pin": 28,
"alt0": {
"inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
+ "do": "s_clkout",
+ "oe": "s_clkout_enable"
},
"alt1": {
"inputs": [],
@@ -571,8 +670,8 @@
"pin": 29,
"alt0": {
"inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
+ "do": "xpi.nss_o[1]",
+ "oe": "1'b1"
},
"alt1": {
"inputs": [],
@@ -584,13 +683,11 @@
"pin": 30,
"alt0": {
"inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
+ "do": "xpi.nss_o[2]",
+ "oe": "1'b1"
},
"alt1": {
- "inputs": [
- "u_pwm_if.capture_i[0]"
- ],
+ "inputs": [],
"do": "1'b0",
"oe": "1'b0"
}
@@ -599,16 +696,22 @@
"pin": 31,
"alt0": {
"inputs": [],
- "do": "1'b0",
- "oe": "1'b0"
+ "do": "xpi.nss_o[3]",
+ "oe": "1'b1"
},
"alt1": {
- "inputs": [
- "u_pwm_if.capture_i[1]"
- ],
+ "inputs": [],
"do": "1'b0",
"oe": "1'b0"
}
}
- ]
+ ],
+ "dma_request_mask": "0x31ff",
+ "private_crypto": {
+ "banks": 6,
+ "bank_bytes": 4096,
+ "macro_instance": "u_soc.u_crypto.u_storage.gen_bank[{bank}].gen_macro.u_sram.u_mem",
+ "clock_domain": "peripheral",
+ "logical_domain": "PCLK"
+ }
}
diff --git a/rtl/tiny/mk/config.mk b/rtl/tiny/mk/config.mk
index 86ee40ca..95c55e68 100644
--- a/rtl/tiny/mk/config.mk
+++ b/rtl/tiny/mk/config.mk
@@ -1,4 +1,5 @@
-# Both Tiny integrations remain SAFE24; PLL presence is not SYS selection.
+# Tiny always boots from normalized SAFE24. Dynamic selection is RCU-owned.
+override TINY_R2_P7 := YES
ifneq ($(MINI_MODE),NONE)
$(error SOC=TINY requires MINI_MODE=NONE)
endif
@@ -10,12 +11,12 @@ $(error SOC=TINY requires HAVE_HP=NO)
endif
ifeq ($(PDK),ICS55)
ifneq ($(HAVE_PLL),YES)
-$(error SOC=TINY PDK=ICS55 requires HAVE_PLL=YES with the PLL held off)
+$(error SOC=TINY PDK=ICS55 requires HAVE_PLL=YES)
endif
ifeq ($(PDK_BEHAV),YES)
$(error SOC=TINY PDK=ICS55 requires the locked technology models, PDK_BEHAV=NO)
endif
-override TINY_SAFE24_PLL_OFF := YES
+override TINY_SAFE24_PLL_OFF := NO
else
ifneq ($(HAVE_PLL),NO)
$(error SOC=TINY PDK=IHP130 requires HAVE_PLL=NO)
@@ -31,11 +32,16 @@ endif
ifneq ($(SRAM_SIZE_KIB),128)
$(error SOC=TINY first release requires SRAM_SIZE_KIB=128)
endif
-ifneq ($(EXT_CLK_HZ),24000000)
-$(error SOC=TINY first release requires EXT_CLK_HZ=24000000)
+ifeq ($(filter $(EXT_CLK_HZ),24000000 48000000 96000000),)
+$(error SOC=TINY requires external XIN of 24, 48 or 96 MHz)
endif
-ifneq ($(AUD_CLK_HZ),$(EXT_CLK_HZ))
-$(error SOC=TINY uses the system clock for RTC and watchdog)
+ifeq ($(HAVE_PLL),YES)
+ifneq ($(EXT_CLK_HZ),24000000)
+$(error SOC=TINY with PLL requires EXT_CLK_HZ=24000000)
+endif
+endif
+ifneq ($(AUD_CLK_HZ),24000000)
+$(error SOC=TINY RTC and watchdog use normalized REF24)
endif
ifneq ($(CLINT_TIMEBASE_HZ),1000000)
$(error SOC=TINY requires CLINT_TIMEBASE_HZ=1000000)
@@ -56,7 +62,7 @@ ifneq ($(ISA),RV32IM)
$(error SOC=TINY firmware uses ISA=RV32IM)
endif
ifneq ($(WS2812_P3_ACCEPTANCE),NO)
-$(error SOC=TINY does not yet integrate WS2812)
+$(error SOC=TINY uses its integrated P6 acceptance, not the Mini WS2812 P3 image)
endif
ifneq ($(filter YES,$(APU_ENABLE_P7) $(NPU_P5_ACCEPTANCE) $(NPU_P6_ACCEPTANCE)),)
$(error SOC=TINY does not include multimedia accelerators)
diff --git a/rtl/tiny/pin_map/pin_map.json b/rtl/tiny/pin_map/pin_map.json
index be65c3cb..23a086df 100644
--- a/rtl/tiny/pin_map/pin_map.json
+++ b/rtl/tiny/pin_map/pin_map.json
@@ -205,38 +205,38 @@
"jtag_tdi_i_pad": "s_jtag_tdi",
"jtag_trst_n_i_pad": "s_jtag_trst_n",
"jtag_tdo_o_pad": "s_jtag_tdo",
- "gpio_0_io_pad": "s_gpio[0]",
- "gpio_1_io_pad": "s_gpio[1]",
- "gpio_2_io_pad": "s_gpio[2]",
- "gpio_3_io_pad": "s_gpio[3]",
- "gpio_4_io_pad": "s_gpio[4]",
- "gpio_5_io_pad": "s_gpio[5]",
- "gpio_6_io_pad": "s_gpio[6]",
- "gpio_7_io_pad": "s_gpio[7]",
- "gpio_8_io_pad": "s_gpio[8]",
- "gpio_9_io_pad": "s_gpio[9]",
- "gpio_10_io_pad": "s_gpio[10]",
- "gpio_11_io_pad": "s_gpio[11]",
- "gpio_12_io_pad": "s_gpio[12]",
- "gpio_13_io_pad": "s_gpio[13]",
- "gpio_14_io_pad": "s_gpio[14]",
- "gpio_15_io_pad": "s_gpio[15]",
- "gpio_16_io_pad": "s_gpio[16]",
- "gpio_17_io_pad": "s_gpio[17]",
- "gpio_18_io_pad": "s_gpio[18]",
- "gpio_19_io_pad": "s_gpio[19]",
- "gpio_20_io_pad": "s_gpio[20]",
- "gpio_21_io_pad": "s_gpio[21]",
- "gpio_22_io_pad": "s_gpio[22]",
- "gpio_23_io_pad": "s_gpio[23]",
- "gpio_24_io_pad": "s_gpio[24]",
- "gpio_25_io_pad": "s_gpio[25]",
- "gpio_26_io_pad": "s_gpio[26]",
- "gpio_27_io_pad": "s_gpio[27]",
- "gpio_28_io_pad": "s_gpio[28]",
- "gpio_29_io_pad": "s_gpio[29]",
- "gpio_30_io_pad": "s_gpio[30]",
- "gpio_31_io_pad": "s_gpio[31]",
+ "gpio_0_io_pad": "s_gpio_0",
+ "gpio_1_io_pad": "s_gpio_1",
+ "gpio_2_io_pad": "s_gpio_2",
+ "gpio_3_io_pad": "s_gpio_3",
+ "gpio_4_io_pad": "s_gpio_4",
+ "gpio_5_io_pad": "s_gpio_5",
+ "gpio_6_io_pad": "s_gpio_6",
+ "gpio_7_io_pad": "s_gpio_7",
+ "gpio_8_io_pad": "s_gpio_8",
+ "gpio_9_io_pad": "s_gpio_9",
+ "gpio_10_io_pad": "s_gpio_10",
+ "gpio_11_io_pad": "s_gpio_11",
+ "gpio_12_io_pad": "s_gpio_12",
+ "gpio_13_io_pad": "s_gpio_13",
+ "gpio_14_io_pad": "s_gpio_14",
+ "gpio_15_io_pad": "s_gpio_15",
+ "gpio_16_io_pad": "s_gpio_16",
+ "gpio_17_io_pad": "s_gpio_17",
+ "gpio_18_io_pad": "s_gpio_18",
+ "gpio_19_io_pad": "s_gpio_19",
+ "gpio_20_io_pad": "s_gpio_20",
+ "gpio_21_io_pad": "s_gpio_21",
+ "gpio_22_io_pad": "s_gpio_22",
+ "gpio_23_io_pad": "s_gpio_23",
+ "gpio_24_io_pad": "s_gpio_24",
+ "gpio_25_io_pad": "s_gpio_25",
+ "gpio_26_io_pad": "s_gpio_26",
+ "gpio_27_io_pad": "s_gpio_27",
+ "gpio_28_io_pad": "s_gpio_28",
+ "gpio_29_io_pad": "s_gpio_29",
+ "gpio_30_io_pad": "s_gpio_30",
+ "gpio_31_io_pad": "s_gpio_31",
"uart0_tx_o_pad": "s_uart0_tx",
"uart0_rx_i_pad": "s_uart0_rx",
"uart1_tx_o_pad": "s_uart1_tx",
@@ -261,38 +261,38 @@
"jtag_tdi_i_pad": "s_jtag_tdi",
"jtag_trst_n_i_pad": "s_jtag_trst_n",
"jtag_tdo_o_pad": "s_jtag_tdo",
- "gpio_0_io_pad": "s_gpio[0]",
- "gpio_1_io_pad": "s_gpio[1]",
- "gpio_2_io_pad": "s_gpio[2]",
- "gpio_3_io_pad": "s_gpio[3]",
- "gpio_4_io_pad": "s_gpio[4]",
- "gpio_5_io_pad": "s_gpio[5]",
- "gpio_6_io_pad": "s_gpio[6]",
- "gpio_7_io_pad": "s_gpio[7]",
- "gpio_8_io_pad": "s_gpio[8]",
- "gpio_9_io_pad": "s_gpio[9]",
- "gpio_10_io_pad": "s_gpio[10]",
- "gpio_11_io_pad": "s_gpio[11]",
- "gpio_12_io_pad": "s_gpio[12]",
- "gpio_13_io_pad": "s_gpio[13]",
- "gpio_14_io_pad": "s_gpio[14]",
- "gpio_15_io_pad": "s_gpio[15]",
- "gpio_16_io_pad": "s_gpio[16]",
- "gpio_17_io_pad": "s_gpio[17]",
- "gpio_18_io_pad": "s_gpio[18]",
- "gpio_19_io_pad": "s_gpio[19]",
- "gpio_20_io_pad": "s_gpio[20]",
- "gpio_21_io_pad": "s_gpio[21]",
- "gpio_22_io_pad": "s_gpio[22]",
- "gpio_23_io_pad": "s_gpio[23]",
- "gpio_24_io_pad": "s_gpio[24]",
- "gpio_25_io_pad": "s_gpio[25]",
- "gpio_26_io_pad": "s_gpio[26]",
- "gpio_27_io_pad": "s_gpio[27]",
- "gpio_28_io_pad": "s_gpio[28]",
- "gpio_29_io_pad": "s_gpio[29]",
- "gpio_30_io_pad": "s_gpio[30]",
- "gpio_31_io_pad": "s_gpio[31]",
+ "gpio_0_io_pad": "s_gpio_0",
+ "gpio_1_io_pad": "s_gpio_1",
+ "gpio_2_io_pad": "s_gpio_2",
+ "gpio_3_io_pad": "s_gpio_3",
+ "gpio_4_io_pad": "s_gpio_4",
+ "gpio_5_io_pad": "s_gpio_5",
+ "gpio_6_io_pad": "s_gpio_6",
+ "gpio_7_io_pad": "s_gpio_7",
+ "gpio_8_io_pad": "s_gpio_8",
+ "gpio_9_io_pad": "s_gpio_9",
+ "gpio_10_io_pad": "s_gpio_10",
+ "gpio_11_io_pad": "s_gpio_11",
+ "gpio_12_io_pad": "s_gpio_12",
+ "gpio_13_io_pad": "s_gpio_13",
+ "gpio_14_io_pad": "s_gpio_14",
+ "gpio_15_io_pad": "s_gpio_15",
+ "gpio_16_io_pad": "s_gpio_16",
+ "gpio_17_io_pad": "s_gpio_17",
+ "gpio_18_io_pad": "s_gpio_18",
+ "gpio_19_io_pad": "s_gpio_19",
+ "gpio_20_io_pad": "s_gpio_20",
+ "gpio_21_io_pad": "s_gpio_21",
+ "gpio_22_io_pad": "s_gpio_22",
+ "gpio_23_io_pad": "s_gpio_23",
+ "gpio_24_io_pad": "s_gpio_24",
+ "gpio_25_io_pad": "s_gpio_25",
+ "gpio_26_io_pad": "s_gpio_26",
+ "gpio_27_io_pad": "s_gpio_27",
+ "gpio_28_io_pad": "s_gpio_28",
+ "gpio_29_io_pad": "s_gpio_29",
+ "gpio_30_io_pad": "s_gpio_30",
+ "gpio_31_io_pad": "s_gpio_31",
"uart0_tx_o_pad": "s_uart0_tx",
"uart0_rx_i_pad": "s_uart0_rx",
"uart1_tx_o_pad": "s_uart1_tx",
diff --git a/rtl/tiny/top/retrosoc_tiny.sv b/rtl/tiny/top/retrosoc_tiny.sv
index 7278291b..55b3add3 100644
--- a/rtl/tiny/top/retrosoc_tiny.sv
+++ b/rtl/tiny/top/retrosoc_tiny.sv
@@ -1,6 +1,7 @@
// Copyright (c) 2026 Yuchi Miao
// SPDX-License-Identifier: MulanPSL-2.0
`include "mmap_define.svh"
+`include "tiny_topology.svh"
module retrosoc_tiny (
input logic clk_i,
@@ -21,50 +22,136 @@ module retrosoc_tiny (
output logic [7:0] test_code_o
);
logic s_por_rst_n, s_rst_n;
- logic [16:0] s_leaf_rst_n;
+ logic [`RETROSOC_TINY__LEAF_COUNT-1:0] s_leaf_rst_n;
logic s_fabric_rst_n, s_cpu_ready_rst_n;
- logic s_master_rst_n [2];
- logic s_target_rst_n [5];
- logic s_debug_halted;
+ logic s_master_rst_n [ 3];
+ logic s_target_rst_n [`RETROSOC_TINY__TARGET_COUNT];
+ logic s_debug_halted;
+ logic s_halted_pclk, s_halted_ref, s_rtc_wake_sys;
+ logic s_core_rst_n, s_cpu_bus_idle, s_cpu_quiesce;
+ logic s_cpu_clock_idle;
logic [31:0] s_irq;
- logic s_fault_valid, s_fault_master, s_fault_write;
- logic [31:0] s_fault_addr;
- logic [ 1:0] s_fault_resp;
+ logic s_fault_write;
+ logic [ 1:0] s_fault_master;
+ logic [ 3:0] s_fault_count;
+ logic [5:0] s_external_fault_valid, s_external_fault_instruction;
+ logic [5:0][31:0] s_external_fault_addr;
+ logic [5:0][ 1:0] s_external_fault_resp;
+ logic [1:0] s_local_fault_valid, s_local_fault_write;
+ logic [ 1:0][31:0] s_local_fault_addr;
+ logic [31:0] s_fault_addr;
+ logic [ 1:0] s_fault_resp;
logic s_perf_enable, s_perf_clear;
logic [63:0] s_cpu_wait, s_dma_wait;
logic s_timer0_irq, s_timer1_irq, s_xpi_irq, s_dma_irq;
logic s_dma_done;
- logic [7:0] s_dma_stall;
+ logic [5:0] s_dma_stall;
logic [4:0] s_tick_count_d, s_tick_count_q;
logic s_tick;
- logic s_unused_uart1_tx_stall, s_unused_uart1_rx_stall;
+ logic s_audio_pad_clk, s_audio_clk, s_audio_rst_n;
+ logic s_rtc_func_rst_n, s_wdg_func_rst_n;
+ logic [31:0] s_xin_hz, s_ref_hz, s_sys_hz, s_mem_hz, s_pclk_hz, s_clint_hz;
+ logic [1:0] s_rcu_current;
+ logic s_rcu_busy;
+ logic s_clock_apply;
+ logic [1:0] s_clock_profile;
+ logic s_backend_busy, s_backend_fault;
+ logic s_backend_sys_clk, s_backend_mem_clk, s_backend_pclk;
+ logic s_backend_sys_rst_n, s_backend_mem_rst_n, s_backend_pclk_rst_n;
+ logic [31:0] s_backend_xin_hz, s_backend_ref_hz, s_backend_sys_hz;
+ logic [31:0] s_backend_mem_hz, s_backend_pclk_hz;
+ logic s_exec_clk;
+ logic s_ref_clk;
+ logic [`RETROSOC_TINY__LEAF_COUNT-1:0] s_leaf_clk;
+ logic [`RETROSOC_TINY__LEAF_COUNT-2:0] s_apb_cdc_idle, s_apb_cdc_ready;
+ logic [ 2:0] s_owner_idle;
+ logic [`RETROSOC_TINY__TARGET_COUNT-1:0] s_target_idle;
+ logic s_clock_safe;
+ logic s_xpi_cdc_busy;
+ logic s_dma_cdc_busy;
+ logic s_dma_done_pending_d, s_dma_done_pending_q;
+ logic s_dma_done_ready, s_dma_done_mem;
+ logic [ 1:0] s_xpi_stall_pclk;
+ logic [31:0] s_irq_async;
+ logic s_cpu_boot_ready;
+ logic s_dma_cdc_ready, s_dma_ready_sys, s_xpi_cdc_ready;
+ logic s_rcu_link_ready;
+ logic s_clock_block, s_block_sys, s_block_pclk;
+ logic s_clock_done, s_system_reset, s_backend_reset_n, s_pll_locked, s_pll_selected;
+ logic s_rcu_irq;
+ logic s_hart_reset, s_hart_seen_d, s_hart_seen_q, s_hart_pending_d, s_hart_pending_q;
+ logic s_hart_ack, s_hart_reset_ref;
+ logic [31:0] s_pclk_report_d, s_pclk_report_q, s_sys_report_d, s_sys_report_q;
+ logic [31:0] s_pclk_snapshot, s_sys_snapshot;
+ logic s_pclk_snapshot_valid, s_sys_snapshot_valid;
+ logic s_rate_ref_pclk_rst_n, s_rate_pclk_rst_n;
+ logic s_rate_ref_sys_rst_n, s_rate_sys_rst_n;
+ logic [31:0] s_clkout_control;
+ logic s_clkout, s_clkout_enable;
+ logic s_clkout_idle_ref;
+ logic [13:0] s_gate_request, s_local_reset, s_actual_gate, s_reset_held, s_reset_ready;
+ logic [13:0] s_gate_ref, s_held_ref, s_ready_ref, s_disabled_sys;
+ logic s_periph_apply, s_periph_reset_command, s_periph_block, s_periph_done;
+ logic [13:0] s_periph_request, s_periph_reset_done;
+ logic [4:0] s_periph_fault;
+ logic s_bus_idle_ref;
+ logic s_boot_release_d, s_boot_release_q;
+ logic s_wfi_gate_sys;
+ logic [13:0] s_periph_idle, s_periph_idle_ref;
+ logic s_xpi_idle, s_xpi_idle_ref;
+ logic s_pwm_write, s_crc_write;
+ logic [ 4:0] s_clock_fault;
+ logic [31:0] s_clock_timeout;
+ logic [ 1:0] s_backend_current;
+ logic s_dma_admission_idle, s_dma_idle_sys, s_sdio_admission_idle;
+ logic s_rng_en, s_rng_ready, s_rng_valid, s_rng_qualified, s_rng_fault, s_rng_irq;
+ logic [31:0] s_rng_data;
+ logic s_crypto_irq, s_crypto_input_ready, s_crypto_output_ready;
+ logic s_i2s_tx_stall, s_i2s_rx_stall;
+ logic s_i2s_reset_ready;
+ logic s_done_pclk_rst_n, s_done_mem_rst_n;
`include "tiny_apb_interfaces.svh"
-uart_if u_uart0_if ();
- uart_if u_uart1_if ();
+ahbl_if u_cpu_ahbl_if[2] (
+ .hclk (s_exec_clk),
+ .hresetn(s_core_rst_n)
+ );
+ tiny_sram_port_if u_cpu_local_if[2] ();
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) u_sram_axi4_if[4] (
+ .aclk (s_exec_clk),
+ .aresetn(s_sram_rst_n)
+ );
+ uart_if u_uart0_if ();
i2c_if u_i2c0_if ();
- i2c_if u_i2c1_if ();
pwm_if u_pwm_if ();
clint_if u_clint_if ();
gpio_if u_gpio_if ();
user_gpio_if u_user_gpio_if ();
dma_req_if u_dma_req_if ();
+ i2s_if u_i2s_if ();
+ ws2812_if u_ws2812_if ();
+ sdio_if u_sdio_if ();
rtc_if u_rtc_if (
- .rtc_clk_i (clk_i),
- .rtc_rst_n_i(s_rtc_rst_n)
+ .rtc_clk_i (s_ref_clk),
+ .rtc_rst_n_i(s_rtc_func_rst_n)
);
// The watchdog reset pulse must survive resetting the bus/CPU. Its timer
- // uses POR reset while its APB register domain uses the system reset.
+ // resets after the AON backend retains the watchdog request.
wdg_if u_wdg_if (
- .wdg_clk_i (clk_i),
- .wdg_rst_n_i(s_por_rst_n)
+ .wdg_clk_i (s_ref_clk),
+ .wdg_rst_n_i(s_wdg_func_rst_n)
);
axi4_if #(
.ADDR_WIDTH(32),
.DATA_WIDTH(32),
.ID_WIDTH (1),
.USER_WIDTH(1)
- ) u_masters_axi4_if[2] (
- .aclk (clk_i),
+ ) u_masters_axi4_if[3] (
+ .aclk (s_exec_clk),
.aresetn(s_master_rst_n)
);
axi4_if #(
@@ -72,52 +159,458 @@ uart_if u_uart0_if ();
.DATA_WIDTH(32),
.ID_WIDTH (1),
.USER_WIDTH(1)
- ) u_targets_axi4_if[5] (
- .aclk (clk_i),
+ ) u_dma_pclk_axi4_if (
+ .aclk (s_dma_clk),
+ .aresetn(s_dma_rst_n)
+ );
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) u_dma_admitted_axi4_if (
+ .aclk (s_backend_pclk),
+ .aresetn(s_dma_rst_n)
+ );
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) u_sdio_sys_axi4_if (
+ .aclk (s_sdio_clk),
+ .aresetn(s_sdio_rst_n)
+ );
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) u_targets_axi4_if[`RETROSOC_TINY__TARGET_COUNT] (
+ .aclk (s_exec_clk),
.aresetn(s_target_rst_n)
);
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) u_xpi_mem_axi4_if (
+ .aclk (s_backend_mem_clk),
+ .aresetn(s_backend_mem_rst_n)
+ );
axi4_stream_if #(
.DATA_WIDTH(32)
) u_i2s_tx_axis_if (
- .aclk (clk_i),
+ .aclk (s_dma_clk),
.aresetn(s_dma_rst_n)
);
axi4_stream_if #(
.DATA_WIDTH(32)
) u_i2s_rx_axis_if (
- .aclk (clk_i),
+ .aclk (s_dma_clk),
.aresetn(s_dma_rst_n)
);
axi4_stream_if #(
.DATA_WIDTH(32)
) u_dvp_rx_axis_if (
- .aclk (clk_i),
+ .aclk (s_dma_clk),
.aresetn(s_dma_rst_n)
);
axi4_stream_if #(
.DATA_WIDTH(32)
) u_crypto_tx_axis_if (
- .aclk (clk_i),
+ .aclk (s_dma_clk),
.aresetn(s_dma_rst_n)
);
axi4_stream_if #(
.DATA_WIDTH(32)
) u_crypto_rx_axis_if (
- .aclk (clk_i),
+ .aclk (s_dma_clk),
.aresetn(s_dma_rst_n)
);
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(1)
+ ) u_debug_pclk_sync (
+ .clk_i (s_backend_pclk),
+ .rst_n_i(s_backend_pclk_rst_n),
+ .dat_i (s_debug_halted),
+ .dat_o (s_halted_pclk)
+ );
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(1)
+ ) u_debug_ref_sync (
+ .clk_i (s_ref_clk),
+ .rst_n_i(s_por_rst_n),
+ .dat_i (s_debug_halted),
+ .dat_o (s_halted_ref)
+ );
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(1)
+ ) u_rtc_wake_sync (
+ .clk_i (s_exec_clk),
+ .rst_n_i(s_fabric_rst_n),
+ .dat_i (u_rtc_if.wake_o),
+ .dat_o (s_rtc_wake_sys)
+ );
+ rst_sync #(
+ .STAGE(5)
+ ) u_rtc_func_reset (
+ .clk_i (s_ref_clk),
+ .rst_n_i(s_por_rst_n && s_backend_reset_n),
+ .rst_n_o(s_rtc_func_rst_n)
+ );
+ rst_sync #(
+ .STAGE(5)
+ ) u_wdg_func_reset (
+ .clk_i (s_ref_clk),
+ .rst_n_i(s_por_rst_n && s_backend_reset_n),
+ .rst_n_o(s_wdg_func_rst_n)
+ );
+ assign s_hart_seen_d = s_hart_reset;
+ assign s_hart_pending_d = (s_hart_reset && !s_hart_seen_q) || (s_hart_pending_q && !s_hart_ack);
+ dffr #(
+ .DATA_WIDTH(2)
+ ) u_hart_event (
+ .clk_i (s_exec_clk),
+ .rst_n_i(rst_n_i),
+ .dat_i ({s_hart_pending_d, s_hart_seen_d}),
+ .dat_o ({s_hart_pending_q, s_hart_seen_q})
+ );
+ cdc_2phase #(
+ .DATA_WIDTH(1)
+ ) u_hart_event_cdc (
+ .src_clk_i (s_exec_clk),
+ .src_rst_n_i(rst_n_i),
+ .src_valid_i(s_hart_pending_q),
+ .src_ready_o(s_hart_ack),
+ .src_data_i (1'b1),
+ .dst_clk_i (s_ref_clk),
+ .dst_rst_n_i(s_por_rst_n),
+ .dst_valid_o(s_hart_reset_ref),
+ .dst_ready_i(1'b1),
+ .dst_data_o ()
+ );
+ cdc_reset_barrier #(
+ .STAGES(5)
+ ) u_pclk_rate_barrier (
+ .clk_a_i (s_ref_clk),
+ .clk_b_i (s_backend_pclk),
+ .rst_n_i (s_por_rst_n && s_backend_pclk_rst_n),
+ .release_i(1'b1),
+ .rst_a_n_o(s_rate_ref_pclk_rst_n),
+ .rst_b_n_o(s_rate_pclk_rst_n)
+ );
+ cdc_reset_barrier #(
+ .STAGES(5)
+ ) u_sys_rate_barrier (
+ .clk_a_i (s_ref_clk),
+ .clk_b_i (s_exec_clk),
+ .rst_n_i (s_por_rst_n && s_backend_sys_rst_n),
+ .release_i(1'b1),
+ .rst_a_n_o(s_rate_ref_sys_rst_n),
+ .rst_b_n_o(s_rate_sys_rst_n)
+ );
+ cdc_2phase #(
+ .DATA_WIDTH(32)
+ ) u_pclk_rate_cdc (
+ .src_clk_i (s_ref_clk),
+ .src_rst_n_i(s_rate_ref_pclk_rst_n),
+ .src_data_i (s_backend_pclk_hz),
+ .src_valid_i(1'b1),
+ .src_ready_o(),
+ .dst_clk_i (s_backend_pclk),
+ .dst_rst_n_i(s_rate_pclk_rst_n),
+ .dst_data_o (s_pclk_snapshot),
+ .dst_valid_o(s_pclk_snapshot_valid),
+ .dst_ready_i(1'b1)
+ );
+ cdc_2phase #(
+ .DATA_WIDTH(32)
+ ) u_sys_rate_cdc (
+ .src_clk_i (s_ref_clk),
+ .src_rst_n_i(s_rate_ref_sys_rst_n),
+ .src_data_i (s_backend_sys_hz),
+ .src_valid_i(1'b1),
+ .src_ready_o(),
+ .dst_clk_i (s_exec_clk),
+ .dst_rst_n_i(s_rate_sys_rst_n),
+ .dst_data_o (s_sys_snapshot),
+ .dst_valid_o(s_sys_snapshot_valid),
+ .dst_ready_i(1'b1)
+ );
+ assign s_pclk_report_d = s_pclk_snapshot_valid ? s_pclk_snapshot : s_pclk_report_q;
+ assign s_sys_report_d = s_sys_snapshot_valid ? s_sys_snapshot : s_sys_report_q;
+ dffrc #(
+ .DATA_WIDTH(32),
+ .RESET_VAL (32'd24_000_000)
+ ) u_pclk_report (
+ .clk_i (s_backend_pclk),
+ .rst_n_i(s_backend_pclk_rst_n),
+ .dat_i (s_pclk_report_d),
+ .dat_o (s_pclk_report_q)
+ );
+ dffrc #(
+ .DATA_WIDTH(32),
+ .RESET_VAL (32'd24_000_000)
+ ) u_sys_report (
+ .clk_i (s_exec_clk),
+ .rst_n_i(s_backend_sys_rst_n),
+ .dat_i (s_sys_report_d),
+ .dat_o (s_sys_report_q)
+ );
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(1)
+ ) u_clkout_idle_sync (
+ .clk_i (s_ref_clk),
+ .rst_n_i(s_por_rst_n),
+ .dat_i (!s_clkout_enable),
+ .dat_o (s_clkout_idle_ref)
+ );
+ tiny_clock_output u_clock_output (
+ .clk_ref_i (s_ref_clk),
+ .clk_sys_i (s_exec_clk),
+ .clk_mem_i (s_backend_mem_clk),
+ .clk_periph_i(s_backend_pclk),
+ .rst_n_i (s_por_rst_n && s_backend_reset_n),
+ .control_i (s_clkout_control),
+ .clk_o (s_clkout),
+ .enable_o (s_clkout_enable)
+ );
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(1)
+ ) u_wfi_gate_sync (
+ .clk_i (s_exec_clk),
+ .rst_n_i(s_fabric_rst_n),
+ .dat_i (s_gate_request[0]),
+ .dat_o (s_wfi_gate_sys)
+ );
+ assign s_reset_held[0] = 1'b0;
+ assign s_reset_ready[0] = 1'b0;
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(14)
+ ) u_gate_feedback (
+ .clk_i (s_ref_clk),
+ .rst_n_i(s_por_rst_n),
+ .dat_i (s_actual_gate),
+ .dat_o (s_gate_ref)
+ );
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(14)
+ ) u_held_feedback (
+ .clk_i (s_ref_clk),
+ .rst_n_i(s_por_rst_n),
+ .dat_i (s_reset_held),
+ .dat_o (s_held_ref)
+ );
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(14)
+ ) u_ready_feedback (
+ .clk_i (s_ref_clk),
+ .rst_n_i(s_por_rst_n),
+ .dat_i (s_reset_ready),
+ .dat_o (s_ready_ref)
+ );
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(14)
+ ) u_disabled_sync (
+ .clk_i (s_exec_clk),
+ .rst_n_i(s_fabric_rst_n),
+ .dat_i (s_gate_request | s_local_reset),
+ .dat_o (s_disabled_sys)
+ );
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(1)
+ ) u_bus_idle_ref (
+ .clk_i (s_ref_clk),
+ .rst_n_i(s_por_rst_n),
+ .dat_i (s_clock_safe),
+ .dat_o (s_bus_idle_ref)
+ );
+ tiny_peripheral_control u_peripheral_control (
+ .clk_i (s_ref_clk),
+ .rst_n_i (s_por_rst_n),
+ .warm_reset_i (!s_backend_reset_n),
+ .apply_i (s_periph_apply),
+ .reset_command_i(s_periph_reset_command),
+ .request_i (s_periph_request),
+ .idle_i (s_periph_idle_ref),
+ .bus_idle_i (s_bus_idle_ref),
+ .gate_actual_i (s_gate_ref),
+ .reset_held_i (s_held_ref),
+ .reset_ready_i (s_ready_ref),
+ .timeout_i (s_clock_timeout),
+ .gate_o (s_gate_request),
+ .reset_o (s_local_reset),
+ .block_o (s_periph_block),
+ .done_o (s_periph_done),
+ .fault_o (s_periph_fault),
+ .reset_done_o (s_periph_reset_done)
+ );
+ assign s_pwm_write = u_pwm_apb4_if.psel && u_pwm_apb4_if.penable && u_pwm_apb4_if.pready &&
+ u_pwm_apb4_if.pwrite && !u_pwm_apb4_if.pslverr && u_pwm_apb4_if.pstrb[0];
+ assign s_crc_write = u_crc_apb4_if.psel && u_crc_apb4_if.penable && u_crc_apb4_if.pready &&
+ u_crc_apb4_if.pwrite && !u_crc_apb4_if.pslverr && u_crc_apb4_if.pstrb[0];
+ tiny_managed_lifecycle u_managed_lifecycle (
+ .clk_i (s_backend_pclk),
+ .pwm_rst_n_i(s_pwm_rst_n),
+ .crc_rst_n_i(s_crc_rst_n),
+ .pwm_write_i(s_pwm_write),
+ .pwm_addr_i (u_pwm_apb4_if.paddr[11:0]),
+ .pwm_data_i (u_pwm_apb4_if.pwdata),
+ .crc_write_i(s_crc_write),
+ .crc_addr_i (u_crc_apb4_if.paddr[11:0]),
+ .crc_data_i (u_crc_apb4_if.pwdata),
+ .pwm_idle_o (s_periph_idle[6]),
+ .crc_idle_o (s_periph_idle[12])
+ );
+ assign s_periph_idle[0] = s_cpu_bus_idle;
+ assign s_periph_idle[1] = 1'b1; // GPIO has no independent transaction engine; USER is absent.
+ assign s_periph_idle[11] = !s_rng_en && !s_rng_valid;
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(14)
+ ) u_periph_idle_sync (
+ .clk_i (s_ref_clk),
+ .rst_n_i(s_por_rst_n),
+ .dat_i (s_periph_idle),
+ .dat_o (s_periph_idle_ref)
+ );
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(1)
+ ) u_xpi_idle_sync (
+ .clk_i (s_ref_clk),
+ .rst_n_i(s_por_rst_n),
+ .dat_i (s_xpi_idle),
+ .dat_o (s_xpi_idle_ref)
+ );
+ assign s_exec_clk = s_backend_sys_clk;
+ assign s_leaf_clk[0] = s_exec_clk;
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(1)
+ ) u_dma_ready_sync (
+ .clk_i (s_exec_clk),
+ .rst_n_i(s_fabric_rst_n),
+ .dat_i (s_dma_cdc_ready),
+ .dat_o (s_dma_ready_sys)
+ );
+ assign s_boot_release_d = s_boot_release_q || (s_cpu_ready_rst_n &&
+ (&s_apb_cdc_ready) && s_xpi_cdc_ready && s_dma_ready_sys && s_rcu_link_ready);
+ dffr #(
+ .DATA_WIDTH(1)
+ ) u_boot_release (
+ .clk_i (s_exec_clk),
+ .rst_n_i(s_fabric_rst_n),
+ .dat_i (s_boot_release_d),
+ .dat_o (s_boot_release_q)
+ );
+ rst_sync #(
+ .STAGE(5)
+ ) u_boot_reset (
+ .clk_i (s_exec_clk),
+ .rst_n_i(s_cpu_ready_rst_n && s_boot_release_q),
+ .rst_n_o(s_cpu_boot_ready)
+ );
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(2)
+ ) u_xpi_stall_sync (
+ .clk_i (s_dma_clk),
+ .rst_n_i(s_dma_rst_n),
+ .dat_i (s_dma_stall[1:0]),
+ .dat_o (s_xpi_stall_pclk)
+ );
+ // Central DMA emits one completion pulse. Retain it until MEM acknowledges;
+ // a new completion coincident with acknowledgement wins over clearing.
+ assign s_dma_done_pending_d = s_dma_done || (s_dma_done_pending_q && !s_dma_done_ready);
+ dffr #(
+ .DATA_WIDTH(1)
+ ) u_dma_done_pending (
+ .clk_i (s_dma_clk),
+ .rst_n_i(s_dma_rst_n),
+ .dat_i (s_dma_done_pending_d),
+ .dat_o (s_dma_done_pending_q)
+ );
+ cdc_reset_barrier #(
+ .STAGES(5)
+ ) u_dma_done_barrier (
+ .clk_a_i (s_dma_clk),
+ .clk_b_i (s_backend_mem_clk),
+ .rst_n_i (s_dma_rst_n && s_xpi_rst_n),
+ .release_i(1'b1),
+ .rst_a_n_o(s_done_pclk_rst_n),
+ .rst_b_n_o(s_done_mem_rst_n)
+ );
+ cdc_2phase #(
+ .DATA_WIDTH(1)
+ ) u_dma_done_cdc (
+ .src_clk_i (s_dma_clk),
+ .src_rst_n_i(s_done_pclk_rst_n),
+ .src_valid_i(s_dma_done_pending_q),
+ .src_ready_o(s_dma_done_ready),
+ .src_data_i (1'b1),
+ .dst_clk_i (s_backend_mem_clk),
+ .dst_rst_n_i(s_done_mem_rst_n),
+ .dst_valid_o(s_dma_done_mem),
+ .dst_ready_i(1'b1),
+ .dst_data_o ()
+ );
+ assign s_clock_safe = s_cpu_clock_idle && (&s_owner_idle) && (&s_target_idle) &&
+ (&s_apb_cdc_idle) && s_dma_idle_sys && s_sdio_admission_idle && !s_xpi_cdc_busy;
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(1)
+ ) u_block_sys (
+ .clk_i (s_exec_clk),
+ .rst_n_i(s_fabric_rst_n),
+ .dat_i (s_clock_block || s_periph_block),
+ .dat_o (s_block_sys)
+ );
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(1)
+ ) u_block_pclk (
+ .clk_i (s_dma_clk),
+ .rst_n_i(s_dma_rst_n),
+ .dat_i (s_clock_block || s_periph_block),
+ .dat_o (s_block_pclk)
+ );
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(1)
+ ) u_dma_idle_sync (
+ .clk_i (s_exec_clk),
+ .rst_n_i(s_fabric_rst_n),
+ .dat_i (s_dma_admission_idle && !s_dma_done_pending_q && s_dma_done_ready),
+ .dat_o (s_dma_idle_sys)
+ );
assign s_fabric_rst_n = s_leaf_rst_n[0];
- assign s_master_rst_n = '{s_cpu_ready_rst_n, s_dma_rst_n};
- assign s_target_rst_n = '{
- s_sram_rst_n,
- s_xpi_rst_n,
- s_fabric_rst_n,
- s_fabric_rst_n,
- s_fabric_rst_n
- };
-
- tiny_reset_tree u_reset_tree (
- .clk_i (clk_i),
+ assign s_master_rst_n = '{s_cpu_ready_rst_n, s_fabric_rst_n, s_sdio_rst_n};
+ // The fabric-side XPI target is SYS-clocked; only the MEM endpoint uses the
+ // XPI leaf reset. The async bridge invalidates accepted work if MEM resets.
+ assign s_target_rst_n[4] = s_fabric_rst_n;
+ assign s_target_rst_n[`RETROSOC_TINY__DECODE_ERROR] = s_fabric_rst_n;
+ assign s_target_rst_n[`RETROSOC_TINY__PROTOCOL_ERROR] = s_fabric_rst_n;
+ tiny_reset_tree #(
+ .LeafCount (`RETROSOC_TINY__LEAF_COUNT),
+ .SplitDomains(1'b1)
+ ) u_reset_tree (
+ .clk_i (s_exec_clk),
+ .por_clk_i (s_ref_clk),
+ .leaf_clk_i (s_leaf_clk),
+ .backend_reset_n_i (s_backend_reset_n),
.rst_n_i (rst_n_i),
.watchdog_reset_req_i(u_wdg_if.reset_req_o),
.por_rst_n_o (s_por_rst_n),
@@ -130,19 +623,22 @@ uart_if u_uart0_if ();
dffr #(
.DATA_WIDTH(5)
) u_tick_reg (
- .clk_i (clk_i),
- .rst_n_i(s_fabric_rst_n),
+ .clk_i (s_ref_clk),
+ .rst_n_i(s_clint_rst_n),
.dat_i (s_tick_count_d),
.dat_o (s_tick_count_q)
);
- mgmt_core_wrapper #(
- .ExternalIrqCount (30),
- .EnableAtomics (1'b0),
- .TwoCycleBusErrors(1'b1),
- .ResetSyncStages (5)
+ tiny_cpu_wrapper #(
+ .ExternalIrqCount(30),
+ .EnableAtomics (1'b0),
+ .ResetSyncStages (5),
+ .EnableWfiGate (1'b1),
+ .PreserveDebug (1'b1)
) u_cpu (
- .clk_i (clk_i),
- .rst_n_i (s_cpu_ready_rst_n),
+ .clk_i (s_exec_clk),
+ .rst_n_i (s_cpu_boot_ready),
+ .debug_rst_n_i (rst_n_i),
+ .hart_reset_o (s_hart_reset),
.irq_i (s_irq),
.jtag_tck_i (jtag_tck_i),
.jtag_tms_i (jtag_tms_i),
@@ -150,87 +646,250 @@ uart_if u_uart0_if ();
.jtag_trst_n_i (jtag_trst_n_i),
.jtag_tdo_o (jtag_tdo_o),
.debug_halted_o(s_debug_halted),
- .axi4 (u_masters_axi4_if[0])
+ .bus_idle_i (s_cpu_bus_idle),
+ .wfi_gate_i (s_wfi_gate_sys),
+ .gated_o (s_actual_gate[0]),
+ .core_rst_n_o (s_core_rst_n),
+ .quiesce_o (s_cpu_quiesce),
+ .cpu (u_cpu_ahbl_if)
+ );
+ tiny_cpu_mem #(
+ .ClockPause(1'b1)
+ ) u_cpu_mem (
+ .clk_i (s_exec_clk),
+ .rst_n_i (s_core_rst_n),
+ .quiesce_i (s_cpu_quiesce),
+ .clock_pause_i (s_block_sys),
+ .clock_idle_o (s_cpu_clock_idle),
+ .idle_o (s_cpu_bus_idle),
+ .local_fault_valid_o(s_local_fault_valid),
+ .local_fault_addr_o (s_local_fault_addr),
+ .local_fault_write_o(s_local_fault_write),
+ .cpu (u_cpu_ahbl_if),
+ .local_ports (u_cpu_local_if),
+ .slow_axi4 (u_masters_axi4_if[0])
);
tiny_axi4_fabric u_fabric (
- .clk_i (clk_i),
- .rst_n_i (s_fabric_rst_n),
- .masters (u_masters_axi4_if),
- .targets (u_targets_axi4_if),
- .perf_enable_i (s_perf_enable),
- .perf_clear_i (s_perf_clear),
- .fault_valid_o (s_fault_valid),
- .fault_addr_o (s_fault_addr),
- .fault_master_o(s_fault_master),
- .fault_write_o (s_fault_write),
- .fault_resp_o (s_fault_resp),
- .cpu_wait_o (s_cpu_wait),
- .dma_wait_o (s_dma_wait)
+ .clk_i (s_exec_clk),
+ .rst_n_i (s_fabric_rst_n),
+ .target_rst_n_i (s_target_rst_n),
+ .masters (u_masters_axi4_if),
+ .targets (u_targets_axi4_if),
+ .perf_enable_i (s_perf_enable),
+ .perf_clear_i (s_perf_clear),
+ .fault_valid_o (s_external_fault_valid),
+ .fault_addr_o (s_external_fault_addr),
+ .fault_resp_o (s_external_fault_resp),
+ .fault_instruction_o(s_external_fault_instruction),
+ .owner_idle_o (s_owner_idle),
+ .target_idle_o (s_target_idle),
+ .cpu_wait_o (s_cpu_wait),
+ .dma_wait_o (s_dma_wait)
);
- onchip_ram #(
- .CapacityKiB (128),
- .DataWidth (32),
- .IdWidth (1),
- .Ics55SmallBanks(1'b1)
- ) u_sram (
- .clk_i (clk_i),
- .rst_n_i (s_sram_rst_n),
- .mem_axi4 (u_targets_axi4_if[0]),
- .cfg_apb4 (u_sram_apb4_if),
- .perf_enable_i(s_perf_enable),
- .perf_clear_i (s_perf_clear)
+ for (genvar group = 0; group < 4; group++) begin : gen_sram_attachment
+ assign s_target_rst_n[group] = s_sram_rst_n;
+ axi4_connector u_connector (
+ .source(u_targets_axi4_if[group]),
+ .sink (u_sram_axi4_if[group])
+ );
+ end
+ tiny_sram u_sram (
+ .clk_i (s_exec_clk),
+ .rst_n_i (s_sram_rst_n),
+ .local_ports (u_cpu_local_if),
+ .external_ports(u_sram_axi4_if),
+ .cfg_apb4 (u_sram_apb4_if),
+ .perf_enable_i (s_perf_enable),
+ .perf_clear_i (s_perf_clear)
+ );
+ tiny_axi4_async_bridge #(
+ .DataWidth(32),
+ .IdWidth (1)
+ ) u_dma_cdc (
+ .src_clk_i (s_dma_clk),
+ .src_rst_n_i (s_dma_rst_n),
+ .dst_clk_i (s_exec_clk),
+ .dst_rst_n_i (s_fabric_rst_n),
+ .clear_i (1'b0),
+ .clear_busy_o(s_dma_cdc_busy),
+ .ready_o (s_dma_cdc_ready),
+ .epoch_o (),
+ .src_axi4 (u_dma_admitted_axi4_if),
+ .dst_axi4 (u_masters_axi4_if[1])
+ );
+ tiny_axi4_admission u_dma_admission (
+ .clk_i (s_dma_clk),
+ .rst_n_i(s_dma_rst_n),
+ .block_i(s_block_pclk),
+ .idle_o (s_dma_admission_idle),
+ .source (u_dma_pclk_axi4_if),
+ .sink (u_dma_admitted_axi4_if)
+ );
+ tiny_axi4_admission u_sdio_admission (
+ .clk_i (s_sdio_clk),
+ .rst_n_i(s_sdio_rst_n),
+ .block_i(s_block_sys),
+ .idle_o (s_sdio_admission_idle),
+ .source (u_sdio_sys_axi4_if),
+ .sink (u_masters_axi4_if[2])
+ );
+ tiny_axi4_async_bridge #(
+ .DataWidth(32),
+ .IdWidth (1)
+ ) u_xpi_mem_cdc (
+ .src_clk_i (s_exec_clk),
+ .src_rst_n_i (s_fabric_rst_n),
+ .dst_clk_i (s_backend_mem_clk),
+ .dst_rst_n_i (s_xpi_rst_n),
+ .clear_i (1'b0),
+ .clear_busy_o(s_xpi_cdc_busy),
+ .ready_o (s_xpi_cdc_ready),
+ .epoch_o (),
+ .src_axi4 (u_targets_axi4_if[4]),
+ .dst_axi4 (u_xpi_mem_axi4_if)
);
apb4_xpi u_xpi (
- .clk_i (clk_i),
- .rst_n_i (s_xpi_rst_n),
- .mem_axi4 (u_targets_axi4_if[1]),
- .apb4 (u_xpi_apb4_if),
- .xpi (xpi),
- .dma_xfer_done_i(s_dma_done),
- .dma_tx_stall_o (s_dma_stall[0]),
- .dma_rx_stall_o (s_dma_stall[1]),
- .irq_o (s_xpi_irq)
+ .lifecycle_idle_o(s_xpi_idle),
+ .clk_i (s_backend_mem_clk),
+ .rst_n_i (s_xpi_rst_n),
+ .mem_axi4 (u_xpi_mem_axi4_if),
+ .apb4 (u_xpi_apb4_if),
+ .xpi (xpi),
+ .dma_xfer_done_i (s_dma_done_mem),
+ .dma_tx_stall_o (s_dma_stall[0]),
+ .dma_rx_stall_o (s_dma_stall[1]),
+ .irq_o (s_xpi_irq)
);
- tiny_axi42apb4 u_apb_bridge (
- .clk_i (clk_i),
- .rst_n_i(s_fabric_rst_n),
- .axi4 (u_targets_axi4_if[2]),
- `include "tiny_apb_connections.svh"
- );
- axi4_error_slave #(
+ `include "tiny_apb_bridges.svh"
+axi4_error_slave #(
.Response(2'b11)
) u_decode_error (
- .clk_i (clk_i),
+ .clk_i (s_exec_clk),
.rst_n_i(s_fabric_rst_n),
- .axi4 (u_targets_axi4_if[3])
+ .axi4 (u_targets_axi4_if[`RETROSOC_TINY__DECODE_ERROR])
);
axi4_error_slave #(
.Response(2'b10)
) u_protocol_error (
- .clk_i (clk_i),
+ .clk_i (s_exec_clk),
.rst_n_i(s_fabric_rst_n),
- .axi4 (u_targets_axi4_if[4])
+ .axi4 (u_targets_axi4_if[`RETROSOC_TINY__PROTOCOL_ERROR])
);
- tiny_sysctrl u_sysctrl (
- .clk_i (clk_i),
- .rst_n_i (s_sysctrl_rst_n),
- .apb4 (u_sysctrl_apb4_if),
- .fault_valid_i (s_fault_valid),
- .fault_addr_i (s_fault_addr),
- .fault_write_i (s_fault_write),
- .fault_master_i(s_fault_master),
- .fault_resp_i (s_fault_resp),
- .cpu_wait_i (s_cpu_wait),
- .dma_wait_i (s_dma_wait),
- .rtc_wake_i (u_rtc_if.wake_o),
- .perf_enable_o (s_perf_enable),
- .perf_clear_o (s_perf_clear),
- .test_done_o (test_done_o),
- .test_pass_o (test_pass_o),
- .test_code_o (test_code_o)
+ tiny_fault_collector u_fault_collector (
+ .local_valid_i (s_local_fault_valid),
+ .local_addr_i (s_local_fault_addr),
+ .local_write_i (s_local_fault_write),
+ .external_valid_i (s_external_fault_valid),
+ .external_addr_i (s_external_fault_addr),
+ .external_resp_i (s_external_fault_resp),
+ .external_instruction_i(s_external_fault_instruction),
+ .count_o (s_fault_count),
+ .addr_o (s_fault_addr),
+ .master_o (s_fault_master),
+ .write_o (s_fault_write),
+ .resp_o (s_fault_resp)
+ );
+`ifdef HAVE_PLL
+ localparam bit TinyPllPresent = 1'b1;
+`else
+ localparam bit TinyPllPresent = 1'b0;
+`endif
+ tiny_sysctrl #(
+ .PllPresent (TinyPllPresent),
+ .PllProfiles(TinyPllPresent),
+ .XinHzValue (`SOC_EXT_CLK_HZ),
+ .TargetMask (14'h3fff)
+ ) u_sysctrl (
+ .clk_i (s_exec_clk),
+ .rst_n_i (s_sysctrl_rst_n),
+ .clk_ref_i (s_ref_clk),
+ .rst_ref_n_i (s_por_rst_n),
+ .rcu_link_ready_o(s_rcu_link_ready),
+ .apb4 (u_sysctrl_apb4_if),
+ .fault_valid_i (s_fault_count != 4'd0),
+ .fault_count_i (s_fault_count),
+ .fault_addr_i (s_fault_addr),
+ .fault_write_i (s_fault_write),
+ .fault_master_i (s_fault_master),
+ .fault_resp_i (s_fault_resp),
+ .cpu_wait_i (s_cpu_wait),
+ .dma_wait_i (s_dma_wait),
+ .rtc_wake_i (s_rtc_wake_sys),
+ .clock_done_i (s_clock_done),
+ .periph_done_i (s_periph_done),
+ .periph_fault_i (s_periph_fault),
+ .gate_actual_i (s_gate_ref),
+ .reset_done_i (s_periph_reset_done),
+ .periph_apply_o (s_periph_apply),
+ .periph_reset_o (s_periph_reset_command),
+ .periph_request_o(s_periph_request),
+ .clkout_control_o(s_clkout_control),
+ .clkout_idle_i (s_clkout_idle_ref),
+ .clock_fault_i (s_clock_fault),
+ .clock_current_i (s_backend_current),
+ .pll_locked_i (s_pll_locked),
+ .pll_selected_i (s_pll_selected),
+ .warm_reset_i (!s_backend_reset_n),
+ .system_reset_o (s_system_reset),
+ .watchdog_reset_i(u_wdg_if.reset_req_o),
+ .hart_reset_i (s_hart_reset_ref),
+ .rcu_irq_o (s_rcu_irq),
+ .clock_timeout_o (s_clock_timeout),
+ .perf_enable_o (s_perf_enable),
+ .perf_clear_o (s_perf_clear),
+ .test_done_o (test_done_o),
+ .test_pass_o (test_pass_o),
+ .test_code_o (test_code_o),
+ .xin_hz_o (s_xin_hz),
+ .ref_hz_o (s_ref_hz),
+ .sys_hz_o (s_sys_hz),
+ .mem_hz_o (s_mem_hz),
+ .pclk_hz_o (s_pclk_hz),
+ .clint_hz_o (s_clint_hz),
+ .rcu_current_o (s_rcu_current),
+ .rcu_busy_o (s_rcu_busy),
+ .clock_apply_o (s_clock_apply),
+ .clock_profile_o (s_clock_profile)
+ );
+ tiny_clock_reset_subsystem #(
+ .RefClkHz(`SOC_EXT_CLK_HZ),
+ .HavePll (TinyPllPresent)
+ ) u_clock_backend (
+ .ref_clk_i (clk_i),
+ .rst_n_i (rst_n_i),
+ .idle_i (s_clock_safe),
+ .clients_idle_i ((&s_periph_idle_ref[13:1]) && s_xpi_idle_ref),
+ .warm_reset_i (u_wdg_if.reset_req_o),
+ .system_request_i (s_system_reset),
+ .command_ack_i (s_cpu_clock_idle && s_target_idle[`RETROSOC_TINY__TARGET_SYSCTRL]),
+ .timeout_i (s_clock_timeout),
+ .ref_clk_o (s_ref_clk),
+ .ref_rst_n_o (),
+ .system_reset_n_o (s_backend_reset_n),
+ .block_o (s_clock_block),
+ .done_o (s_clock_done),
+ .fault_code_o (s_clock_fault),
+ .pll_locked_o (s_pll_locked),
+ .pll_selected_o (s_pll_selected),
+ .profile_i (s_clock_profile),
+ .apply_i (s_clock_apply),
+ .current_profile_o(s_backend_current),
+ .busy_o (s_backend_busy),
+ .fault_o (s_backend_fault),
+ .sys_clk_o (s_backend_sys_clk),
+ .mem_clk_o (s_backend_mem_clk),
+ .pclk_o (s_backend_pclk),
+ .sys_rst_n_o (s_backend_sys_rst_n),
+ .mem_rst_n_o (s_backend_mem_rst_n),
+ .pclk_rst_n_o (s_backend_pclk_rst_n),
+ .xin_hz_o (s_backend_xin_hz),
+ .ref_hz_o (s_backend_ref_hz),
+ .sys_hz_o (s_backend_sys_hz),
+ .mem_hz_o (s_backend_mem_hz),
+ .pclk_hz_o (s_backend_pclk_hz)
);
tiny_archinfo u_archinfo (
- .clk_i (clk_i),
+ .clk_i (s_ref_clk),
.rst_n_i(s_archinfo_rst_n),
.apb4 (u_archinfo_apb4_if)
);
@@ -239,7 +898,7 @@ uart_if u_uart0_if ();
.HasPullUp (gpio_pad_caps_pkg::HasPullUp),
.HasPullDown (gpio_pad_caps_pkg::HasPullDown)
) u_gpio (
- .clk_i (clk_i),
+ .clk_i (s_gpio_clk),
.rst_n_i (s_gpio_rst_n),
.apb4 (u_gpio_apb4_if),
.gpio (u_gpio_if),
@@ -251,73 +910,71 @@ uart_if u_uart0_if ();
);
assign u_user_gpio_if.do_o = '0;
assign u_user_gpio_if.oe_o = '0;
- assign u_uart0_if.rx_i = uart0_rx_i;
assign uart0_tx_o = u_uart0_if.tx_o;
- assign u_uart1_if.rx_i = uart1_rx_i;
- assign u_uart1_if.cts_n_i = 1'b0;
- assign uart1_tx_o = u_uart1_if.tx_o;
- assign u_wdg_if.debug_halted_i = s_debug_halted;
+ // Legacy ASIC observation pads are retained, not a Gen1 package binding.
+ assign uart1_tx_o = 1'b1;
+ assign u_uart0_if.cts_n_i = 1'b0;
+ assign u_wdg_if.debug_halted_i = s_halted_ref;
`include "tiny_gpio.svh"
`include "tiny_irq.svh"
-apb4_uart u_uart0 (
- .clk_i (clk_i),
- .rst_n_i (s_uart0_rst_n),
- .apb4 (u_uart0_apb4_if),
- .uart (u_uart0_if),
- .dma_tx_stall_o(s_dma_stall[2]),
- .dma_rx_stall_o(s_dma_stall[3])
+cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(32)
+ ) u_irq_sync (
+ .clk_i (s_exec_clk),
+ .rst_n_i(s_fabric_rst_n),
+ .dat_i (s_irq_async),
+ .dat_o (s_irq)
+ );
+ apb4_uart u_uart0 (
+ .lifecycle_idle_o(s_periph_idle[2]),
+ .clk_i (s_uart0_clk),
+ .rst_n_i (s_uart0_rst_n),
+ .apb4 (u_uart0_apb4_if),
+ .uart (u_uart0_if),
+ .dma_tx_stall_o (s_dma_stall[2]),
+ .dma_rx_stall_o (s_dma_stall[3])
);
apb4_i2c u_i2c0 (
- .clk_i (clk_i),
- .rst_n_i (s_i2c0_rst_n),
- .apb4 (u_i2c0_apb4_if),
- .i2c (u_i2c0_if),
- .dma_tx_stall_o(s_dma_stall[4]),
- .dma_rx_stall_o(s_dma_stall[5])
+ .lifecycle_idle_o(s_periph_idle[3]),
+ .clk_i (s_i2c0_clk),
+ .rst_n_i (s_i2c0_rst_n),
+ .apb4 (u_i2c0_apb4_if),
+ .i2c (u_i2c0_if),
+ .dma_tx_stall_o (s_dma_stall[4]),
+ .dma_rx_stall_o (s_dma_stall[5])
);
apb4_timer u_timer0 (
- .clk_i (clk_i),
- .rst_n_i (s_timer0_rst_n),
- .debug_halted_i(s_debug_halted),
- .apb4 (u_timer0_apb4_if),
- .irq_o (s_timer0_irq)
- );
- apb4_uart u_uart1 (
- .clk_i (clk_i),
- .rst_n_i (s_uart1_rst_n),
- .apb4 (u_uart1_apb4_if),
- .uart (u_uart1_if),
- .dma_tx_stall_o(s_unused_uart1_tx_stall),
- .dma_rx_stall_o(s_unused_uart1_rx_stall)
- );
- apb4_i2c u_i2c1 (
- .clk_i (clk_i),
- .rst_n_i (s_i2c1_rst_n),
- .apb4 (u_i2c1_apb4_if),
- .i2c (u_i2c1_if),
- .dma_tx_stall_o(s_dma_stall[6]),
- .dma_rx_stall_o(s_dma_stall[7])
+ .lifecycle_idle_o(s_periph_idle[4]),
+ .clk_i (s_timer0_clk),
+ .rst_n_i (s_timer0_rst_n),
+ .debug_halted_i (s_halted_pclk),
+ .apb4 (u_timer0_apb4_if),
+ .irq_o (s_timer0_irq)
);
apb4_timer u_timer1 (
- .clk_i (clk_i),
- .rst_n_i (s_timer1_rst_n),
- .debug_halted_i(s_debug_halted),
- .apb4 (u_timer1_apb4_if),
- .irq_o (s_timer1_irq)
+ .lifecycle_idle_o(s_periph_idle[5]),
+ .clk_i (s_timer1_clk),
+ .rst_n_i (s_timer1_rst_n),
+ .debug_halted_i (s_halted_pclk),
+ .apb4 (u_timer1_apb4_if),
+ .irq_o (s_timer1_irq)
);
apb4_clint u_clint (
- .clk_i (clk_i),
+ .clk_i (s_ref_clk),
.rst_n_i (s_clint_rst_n),
.timebase_tick_i(s_tick),
.apb4 (u_clint_apb4_if),
.clint (u_clint_if)
);
apb4_pwm #(
- .PCLK_HZ(24_000_000)
+ .PCLK_HZ (24_000_000),
+ .UseRuntimeClockHz(1'b1)
) u_pwm (
- .debug_halted_i(s_debug_halted),
+ .debug_halted_i(s_halted_pclk),
.apb4 (u_pwm_apb4_if),
- .pwm (u_pwm_if)
+ .pwm (u_pwm_if),
+ .pclk_hz_i (s_pclk_report_q)
);
apb4_rtc #(
.RTC_CLOCK_HZ(24_000_000)
@@ -333,45 +990,38 @@ apb4_uart u_uart0 (
.wdg (u_wdg_if)
);
apb4_dma #(
- .NumChannels (4),
+ .NumChannels (8),
.MaxBurstBeats(16),
.FifoDepth (32),
- .RequestMask (16'h07f9),
- .EnableStreams(1'b0)
+ .RequestMask (16'h31ff),
+ .EnableStreams(1'b1)
) u_dma (
- .clk_i (clk_i),
- .rst_n_i (s_dma_rst_n),
- .dma_xfer_done_o(s_dma_done),
- .irq_o (s_dma_irq),
- .hw_trg (u_dma_req_if),
- .apb4 (u_dma_apb4_if),
- .axi4 (u_masters_axi4_if[1]),
- .i2s_tx_axis (u_i2s_tx_axis_if),
- .i2s_rx_axis (u_i2s_rx_axis_if),
- .dvp_rx_axis (u_dvp_rx_axis_if),
- .crypto_in_axis (u_crypto_tx_axis_if),
- .crypto_out_axis(u_crypto_rx_axis_if)
+ .lifecycle_idle_o(s_periph_idle[9]),
+ .clk_i (s_dma_clk),
+ .rst_n_i (s_dma_rst_n),
+ .dma_xfer_done_o (s_dma_done),
+ .irq_o (s_dma_irq),
+ .hw_trg (u_dma_req_if),
+ .apb4 (u_dma_apb4_if),
+ .axi4 (u_dma_pclk_axi4_if),
+ .i2s_tx_axis (u_i2s_tx_axis_if),
+ .i2s_rx_axis (u_i2s_rx_axis_if),
+ .dvp_rx_axis (u_dvp_rx_axis_if),
+ .crypto_in_axis (u_crypto_tx_axis_if),
+ .crypto_out_axis (u_crypto_rx_axis_if)
);
- assign u_dma_req_if.i2s_tx_proc = 1'b0;
- assign u_dma_req_if.i2s_rx_proc = 1'b0;
- assign u_dma_req_if.crypto_in_proc = 1'b0;
- assign u_dma_req_if.crypto_out_proc = 1'b0;
- assign u_dma_req_if.qspi_tx_proc = !s_dma_stall[0];
- assign u_dma_req_if.qspi_rx_proc = !s_dma_stall[1];
+ assign u_dma_req_if.i2s_tx_proc = !s_i2s_tx_stall;
+ assign u_dma_req_if.i2s_rx_proc = !s_i2s_rx_stall;
+ assign u_dma_req_if.crypto_in_proc = s_crypto_input_ready;
+ assign u_dma_req_if.crypto_out_proc = s_crypto_output_ready;
+ assign u_dma_req_if.qspi_tx_proc = !s_xpi_stall_pclk[0];
+ assign u_dma_req_if.qspi_rx_proc = !s_xpi_stall_pclk[1];
assign u_dma_req_if.uart_tx_proc = !s_dma_stall[2];
assign u_dma_req_if.uart_rx_proc = !s_dma_stall[3];
assign u_dma_req_if.i2c0_tx_proc = !s_dma_stall[4];
assign u_dma_req_if.i2c0_rx_proc = !s_dma_stall[5];
- assign u_dma_req_if.i2c1_tx_proc = !s_dma_stall[6];
- assign u_dma_req_if.i2c1_rx_proc = !s_dma_stall[7];
- assign u_i2s_rx_axis_if.tvalid = '0;
- assign u_i2s_rx_axis_if.tdata = '0;
- assign u_i2s_rx_axis_if.tstrb = '0;
- assign u_i2s_rx_axis_if.tkeep = '0;
- assign u_i2s_rx_axis_if.tlast = '0;
- assign u_i2s_rx_axis_if.tid = '0;
- assign u_i2s_rx_axis_if.tdest = '0;
- assign u_i2s_rx_axis_if.tuser = '0;
+ assign u_dma_req_if.i2c1_tx_proc = 1'b0;
+ assign u_dma_req_if.i2c1_rx_proc = 1'b0;
assign u_dvp_rx_axis_if.tvalid = '0;
assign u_dvp_rx_axis_if.tdata = '0;
assign u_dvp_rx_axis_if.tstrb = '0;
@@ -380,14 +1030,87 @@ apb4_uart u_uart0 (
assign u_dvp_rx_axis_if.tid = '0;
assign u_dvp_rx_axis_if.tdest = '0;
assign u_dvp_rx_axis_if.tuser = '0;
- assign u_crypto_rx_axis_if.tvalid = '0;
- assign u_crypto_rx_axis_if.tdata = '0;
- assign u_crypto_rx_axis_if.tstrb = '0;
- assign u_crypto_rx_axis_if.tkeep = '0;
- assign u_crypto_rx_axis_if.tlast = '0;
- assign u_crypto_rx_axis_if.tid = '0;
- assign u_crypto_rx_axis_if.tdest = '0;
- assign u_crypto_rx_axis_if.tuser = '0;
- assign u_i2s_tx_axis_if.tready = 1'b0;
- assign u_crypto_tx_axis_if.tready = 1'b0;
+
+ // Host logic remains SAFE24. Audio reset waits for its own clock, never CPU boot.
+ tc_clk_buf u_audio_clock_buffer (
+ .clk_i(s_audio_pad_clk),
+ .clk_o(s_audio_clk)
+ );
+ rst_sync #(
+ .STAGE(5)
+ ) u_audio_rst_sync (
+ .clk_i (s_audio_clk),
+ .rst_n_i(s_i2s_rst_n),
+ .rst_n_o(s_audio_rst_n)
+ );
+ apb4_i2s u_i2s (
+ .lifecycle_reset_ready_o(s_i2s_reset_ready),
+ .lifecycle_idle_o (s_periph_idle[7]),
+ .clk_i (s_i2s_clk),
+ .rst_n_i (s_i2s_rst_n),
+ .clk_aud_i (s_audio_clk),
+ .rst_aud_n_i (s_audio_rst_n),
+ .dma_tx_stall_o (s_i2s_tx_stall),
+ .dma_rx_stall_o (s_i2s_rx_stall),
+ .tx_underrun_evt_o (), // Sticky status and IRQ retain these events.
+ .rx_overrun_evt_o (),
+ .rx_flush_busy_o (), // No APU consumer in Tiny.
+ .apb4 (u_i2s_apb4_if),
+ .tx_axis (u_i2s_tx_axis_if),
+ .rx_axis (u_i2s_rx_axis_if),
+ .i2s (u_i2s_if)
+ );
+ apb4_sdio #(
+ .InputClockHz (24_000_000),
+ .UseRuntimeClockHz(1'b1)
+ ) u_sdio (
+ .lifecycle_idle_o(s_periph_idle[8]),
+ .clk_i (s_sdio_clk),
+ .rst_n_i (s_sdio_rst_n),
+ .apb4 (u_sdio_apb4_if),
+ .dma_axi4 (u_sdio_sys_axi4_if),
+ .sdio (u_sdio_if),
+ .clock_hz_i (s_sys_report_q)
+ );
+ apb4_ws2812 u_ws2812 (
+ .lifecycle_idle_o(s_periph_idle[10]),
+ .clk_i (s_ws2812_clk),
+ .rst_n_i (s_ws2812_rst_n),
+ .apb4 (u_ws2812_apb4_if),
+ .ws2812 (u_ws2812_if)
+ );
+ apb4_crypto u_crypto (
+ .lifecycle_idle_o (s_periph_idle[13]),
+ .clk_i (s_crypto_clk),
+ .rst_n_i (s_crypto_rst_n),
+ .dma_input_proc_o (s_crypto_input_ready),
+ .dma_output_proc_o(s_crypto_output_ready),
+ .irq_o (s_crypto_irq),
+ .apb4 (u_crypto_apb4_if),
+ .crypto_in_axis (u_crypto_tx_axis_if),
+ .crypto_out_axis (u_crypto_rx_axis_if)
+ );
+ apb4_crc u_crc (.apb4(u_crc_apb4_if));
+ rng_deterministic_source u_rng_source (
+ .clk_i (s_rng_clk),
+ .rst_n_i (s_rng_rst_n),
+ .enable_i (s_rng_en),
+ .ready_i (s_rng_ready),
+ .valid_o (s_rng_valid),
+ .data_o (s_rng_data),
+ .qualified_o(s_rng_qualified),
+ .fault_o (s_rng_fault)
+ );
+ apb4_rng #(
+ .FIFO_DEPTH(8)
+ ) u_rng (
+ .entropy_enable_o (s_rng_en),
+ .entropy_ready_o (s_rng_ready),
+ .entropy_valid_i (s_rng_valid),
+ .entropy_data_i (s_rng_data),
+ .entropy_qualified_i(s_rng_qualified),
+ .entropy_fault_i (s_rng_fault),
+ .irq_o (s_rng_irq),
+ .apb4 (u_rng_apb4_if)
+ );
endmodule
diff --git a/rtl/tiny/top/retrosoc_tiny_asic.sv b/rtl/tiny/top/retrosoc_tiny_asic.sv
index ffa3ad18..0c89657b 100644
--- a/rtl/tiny/top/retrosoc_tiny_asic.sv
+++ b/rtl/tiny/top/retrosoc_tiny_asic.sv
@@ -14,22 +14,6 @@ module retrosoc_tiny_asic (
/* verilator lint_on UNUSEDSIGNAL */
gpio_if u_gpio_if ();
xpi_if u_xpi_if ();
-`ifdef HAVE_PLL
- // P1 preserves the macro but supplies no request or reset release. SYS is
- // always the external SAFE24 path; RCU source switching belongs to R2-P7.
- tc_pll #(
- .UseIcs55EcosPll(1'b1),
- .Ics55Parked (1'b1)
- ) u_pll (
- .fref_i (s_ext_clk),
- .rst_n_i (1'b0),
- .cfg_sel_i (3'd5),
- .cfg_apply_i (1'b0),
- .pll_capable_o(),
- .pll_lock_o (),
- .pll_clk_o ()
- );
-`endif
`include "retrosoc_asic_pad_bindings.svh"
tc_clk_buf u_clock_buffer (
.clk_i(s_ext_clk),
diff --git a/rtl/tiny/top/tiny_apb_async_bridge.sv b/rtl/tiny/top/tiny_apb_async_bridge.sv
new file mode 100644
index 00000000..948fc3d6
--- /dev/null
+++ b/rtl/tiny/top/tiny_apb_async_bridge.sv
@@ -0,0 +1,156 @@
+// Copyright (c) 2026 Yuchi Miao
+// SPDX-License-Identifier: MulanPSL-2.0
+
+// One outstanding SYS control request. Either endpoint reset flushes both
+// mailboxes. A destination reset aborts the accepted source transfer with an
+// error; it never replays it when the destination returns. The SYS endpoint
+// remains clocked so a stopped/disabled peripheral is still reachable.
+module tiny_apb_async_bridge (
+ input logic src_clk_i,
+ input logic src_rst_n_i,
+ input logic dst_clk_i,
+ input logic dst_rst_n_i,
+ input logic disabled_i,
+ output logic idle_o,
+ output logic ready_o,
+ apb4_if.slave src_apb4,
+ apb4_if.master dst_apb4
+);
+ typedef enum logic [1:0] {
+ SrcIdle,
+ SrcSend,
+ SrcWait,
+ SrcResponse
+ } src_state_e;
+ typedef enum logic [1:0] {
+ DstIdle,
+ DstSetup,
+ DstAccess,
+ DstResponse
+ } dst_state_e;
+ typedef struct packed {
+ src_state_e state;
+ logic [71:0] req;
+ logic [32:0] resp;
+ } src_t;
+ typedef struct packed {
+ dst_state_e state;
+ logic [71:0] req;
+ logic [32:0] resp;
+ } dst_t;
+ src_t s_src_d, s_src_q;
+ dst_t s_dst_d, s_dst_q;
+ logic s_link_src_rst_n, s_link_dst_rst_n;
+ logic s_req_ready, s_req_valid, s_resp_ready, s_resp_valid;
+ logic [71:0] s_req_data;
+ logic [32:0] s_resp_data;
+
+ cdc_reset_barrier #(
+ .STAGES(5)
+ ) u_barrier (
+ .clk_a_i (src_clk_i),
+ .clk_b_i (dst_clk_i),
+ .rst_n_i (src_rst_n_i && dst_rst_n_i),
+ .release_i(1'b1),
+ .rst_a_n_o(s_link_src_rst_n),
+ .rst_b_n_o(s_link_dst_rst_n)
+ );
+ cdc_2phase #(
+ .DATA_WIDTH(72)
+ ) u_req_cdc (
+ .src_clk_i (src_clk_i),
+ .src_rst_n_i(s_link_src_rst_n),
+ .src_data_i (s_src_q.req),
+ .src_valid_i(s_src_q.state == SrcSend),
+ .src_ready_o(s_req_ready),
+ .dst_clk_i (dst_clk_i),
+ .dst_rst_n_i(s_link_dst_rst_n),
+ .dst_data_o (s_req_data),
+ .dst_valid_o(s_req_valid),
+ .dst_ready_i(s_dst_q.state == DstIdle)
+ );
+ cdc_2phase #(
+ .DATA_WIDTH(33)
+ ) u_resp_cdc (
+ .src_clk_i (dst_clk_i),
+ .src_rst_n_i(s_link_dst_rst_n),
+ .src_data_i (s_dst_q.resp),
+ .src_valid_i(s_dst_q.state == DstResponse),
+ .src_ready_o(s_resp_ready),
+ .dst_clk_i (src_clk_i),
+ .dst_rst_n_i(s_link_src_rst_n),
+ .dst_data_o (s_resp_data),
+ .dst_valid_o(s_resp_valid),
+ .dst_ready_i(s_src_q.state == SrcWait)
+ );
+ assign ready_o = s_link_src_rst_n && s_req_ready;
+ assign idle_o = (s_src_q.state == SrcIdle) && s_req_ready && !s_resp_valid;
+ assign src_apb4.pready = s_src_q.state == SrcResponse;
+ assign src_apb4.pslverr = s_src_q.resp[32];
+ assign src_apb4.prdata = s_src_q.resp[31:0];
+ assign {dst_apb4.paddr, dst_apb4.pprot, dst_apb4.pwrite,
+ dst_apb4.pwdata, dst_apb4.pstrb} = s_dst_q.req;
+ assign dst_apb4.psel = s_link_dst_rst_n &&
+ ((s_dst_q.state == DstSetup) || (s_dst_q.state == DstAccess));
+ assign dst_apb4.penable = s_link_dst_rst_n && (s_dst_q.state == DstAccess);
+
+ always_comb begin
+ s_src_d = s_src_q;
+ unique case (s_src_q.state)
+ SrcIdle:
+ if (src_apb4.psel && src_apb4.penable) begin
+ s_src_d.req = {
+ src_apb4.paddr, src_apb4.pprot, src_apb4.pwrite, src_apb4.pwdata, src_apb4.pstrb
+ };
+ s_src_d.resp = {1'b1, 32'd0};
+ s_src_d.state = disabled_i ? SrcResponse : SrcSend;
+ end
+ SrcSend: if (s_req_ready) s_src_d.state = SrcWait;
+ SrcWait:
+ if (s_resp_valid) begin
+ s_src_d.resp = s_resp_data;
+ s_src_d.state = SrcResponse;
+ end
+ SrcResponse: s_src_d.state = SrcIdle;
+ default: s_src_d.state = SrcIdle;
+ endcase
+ if (((s_src_q.state == SrcSend) || (s_src_q.state == SrcWait)) && !s_link_src_rst_n) begin
+ s_src_d.resp = {1'b1, 32'd0};
+ s_src_d.state = SrcResponse;
+ end
+ end
+ always_comb begin
+ s_dst_d = s_dst_q;
+ unique case (s_dst_q.state)
+ DstIdle:
+ if (s_req_valid) begin
+ s_dst_d.req = s_req_data;
+ s_dst_d.state = DstSetup;
+ end
+ DstSetup: s_dst_d.state = DstAccess;
+ DstAccess:
+ if (dst_apb4.pready) begin
+ s_dst_d.resp = {dst_apb4.pslverr, dst_apb4.prdata};
+ s_dst_d.state = DstResponse;
+ end
+ DstResponse: if (s_resp_ready) s_dst_d.state = DstIdle;
+ default: s_dst_d.state = DstIdle;
+ endcase
+ end
+ dffr #(
+ .DATA_WIDTH($bits(src_t))
+ ) u_src (
+ .clk_i (src_clk_i),
+ .rst_n_i(src_rst_n_i),
+ .dat_i (s_src_d),
+ .dat_o (s_src_q)
+ );
+ dffr #(
+ .DATA_WIDTH($bits(dst_t))
+ ) u_dst (
+ .clk_i (dst_clk_i),
+ .rst_n_i(s_link_dst_rst_n),
+ .dat_i (s_dst_d),
+ .dat_o (s_dst_q)
+ );
+endmodule
diff --git a/rtl/tiny/top/tiny_axi42apb4.sv b/rtl/tiny/top/tiny_axi42apb4.sv
index 4cd6fdc3..d0b48b2d 100644
--- a/rtl/tiny/top/tiny_axi42apb4.sv
+++ b/rtl/tiny/top/tiny_axi42apb4.sv
@@ -4,10 +4,10 @@
// APB setup/access are separate cycles; rejected bursts never assert PSEL.
module tiny_axi42apb4 (
- input logic clk_i,
- input logic rst_n_i,
- axi4_if.slave axi4,
- `include "tiny_apb_ports.svh"
+ input logic clk_i,
+ input logic rst_n_i,
+ axi4_if.slave axi4,
+ apb4_if.master apb4
);
typedef enum logic [2:0] {
Idle,
@@ -30,8 +30,6 @@ module tiny_axi42apb4 (
logic [1:0] s_resp_d, s_resp_q;
logic [7:0] s_len_d, s_len_q;
logic [7:0] s_beat_d, s_beat_q;
- logic [15:0] s_select, s_ready, s_error;
- logic [15:0][31:0] s_rdata;
logic s_access, s_enable, s_done, s_err;
logic [31:0] s_read_data;
@@ -51,18 +49,18 @@ module tiny_axi42apb4 (
assign axi4.rvalid = (s_state_q == ReadResponse) || (s_state_q == ReadError);
assign s_access = (s_state_q == Setup) || (s_state_q == Access);
assign s_enable = s_state_q == Access;
- assign s_done = |(s_select & s_ready);
- assign s_err = |(s_select & s_error);
-
- always_comb begin
- s_select = '0;
- `include "tiny_apb_decode.svh"
-s_read_data = '0;
- for (int unsigned target = 0; target < 16; target++) begin
- if (s_select[target]) s_read_data = s_rdata[target];
- end
- end
- `include "tiny_apb_wiring.svh"
+ // The fabric owns peripheral decoding. Each bridge owns only this APB
+ // endpoint, including its setup/access and retained response state.
+ assign s_done = apb4.pready;
+ assign s_err = apb4.pslverr;
+ assign s_read_data = apb4.prdata;
+ assign apb4.paddr = s_addr_q;
+ assign apb4.pwrite = s_write_q;
+ assign apb4.pwdata = s_wdata_q;
+ assign apb4.pstrb = s_write_q ? s_wstrb_q : 4'd0;
+ assign apb4.pprot = 3'd0;
+ assign apb4.psel = s_access;
+ assign apb4.penable = s_enable;
always_comb begin
s_state_d = s_state_q;
@@ -111,12 +109,7 @@ s_read_data = '0;
s_state_d = WriteResponse;
end else s_state_d = Setup;
end
- Setup: begin
- if (s_select == 16'd0) begin
- s_resp_d = 2'b11;
- s_state_d = s_write_q ? WriteResponse : ReadResponse;
- end else s_state_d = Access;
- end
+ Setup: s_state_d = Access;
Access:
if (s_done) begin
s_resp_d = s_err ? 2'b10 : 2'b00;
diff --git a/rtl/tiny/top/tiny_axi4_admission.sv b/rtl/tiny/top/tiny_axi4_admission.sv
new file mode 100644
index 00000000..5824180a
--- /dev/null
+++ b/rtl/tiny/top/tiny_axi4_admission.sv
@@ -0,0 +1,153 @@
+// Copyright (c) 2026 Yuchi Miao
+// SPDX-License-Identifier: MulanPSL-2.0
+
+// One retained address and terminal response per direction. Blocking affects
+// only upstream acceptance; already accepted addresses remain VALID downstream
+// and write data/responses drain. Unaccepted requests never enter idle accounting.
+module tiny_axi4_admission (
+ input logic clk_i,
+ input logic rst_n_i,
+ input logic block_i,
+ output logic idle_o,
+ axi4_if.slave source,
+ axi4_if.master sink
+);
+ typedef struct packed {
+ logic [0:0] id;
+ logic [31:0] addr;
+ logic [7:0] len;
+ logic [2:0] size;
+ logic [1:0] burst;
+ logic [0:0] lock;
+ logic [3:0] cache;
+ logic [2:0] prot;
+ logic [3:0] qos;
+ logic [3:0] region;
+ logic [0:0] user;
+ } address_t;
+ address_t s_aw_d, s_aw_q, s_ar_d, s_ar_q;
+ logic [1:0] s_pending_d, s_pending_q, s_sent_d, s_sent_q;
+ assign source.awready = rst_n_i && !block_i && !s_pending_q[0];
+ assign source.arready = rst_n_i && !block_i && !s_pending_q[1];
+ assign sink.awvalid = s_pending_q[0] && !s_sent_q[0];
+ assign sink.arvalid = s_pending_q[1] && !s_sent_q[1];
+ assign sink.awid = s_aw_q.id;
+ assign sink.awaddr = s_aw_q.addr;
+ assign sink.awlen = s_aw_q.len;
+ assign sink.awsize = s_aw_q.size;
+ assign sink.awburst = s_aw_q.burst;
+ assign sink.awlock = s_aw_q.lock;
+ assign sink.awcache = s_aw_q.cache;
+ assign sink.awprot = s_aw_q.prot;
+ assign sink.awqos = s_aw_q.qos;
+ assign sink.awregion = s_aw_q.region;
+ assign sink.awuser = s_aw_q.user;
+ assign sink.arid = s_ar_q.id;
+ assign sink.araddr = s_ar_q.addr;
+ assign sink.arlen = s_ar_q.len;
+ assign sink.arsize = s_ar_q.size;
+ assign sink.arburst = s_ar_q.burst;
+ assign sink.arlock = s_ar_q.lock;
+ assign sink.arcache = s_ar_q.cache;
+ assign sink.arprot = s_ar_q.prot;
+ assign sink.arqos = s_ar_q.qos;
+ assign sink.arregion = s_ar_q.region;
+ assign sink.aruser = s_ar_q.user;
+ assign sink.wvalid = source.wvalid && s_pending_q[0] &&
+ (s_sent_q[0] || (sink.awvalid && sink.awready));
+ assign source.wready = sink.wready && s_pending_q[0] &&
+ (s_sent_q[0] || (sink.awvalid && sink.awready));
+ assign sink.wdata = source.wdata;
+ assign sink.wstrb = source.wstrb;
+ assign sink.wlast = source.wlast;
+ assign sink.wuser = source.wuser;
+ assign sink.bready = source.bready;
+ assign sink.rready = source.rready;
+ assign source.bid = sink.bid;
+ assign source.bresp = sink.bresp;
+ assign source.buser = sink.buser;
+ assign source.bvalid = sink.bvalid;
+ assign source.rid = sink.rid;
+ assign source.rdata = sink.rdata;
+ assign source.rresp = sink.rresp;
+ assign source.rlast = sink.rlast;
+ assign source.ruser = sink.ruser;
+ assign source.rvalid = sink.rvalid;
+ assign idle_o = s_pending_q == 0;
+ always_comb begin
+ s_aw_d = s_aw_q;
+ s_ar_d = s_ar_q;
+ s_pending_d = s_pending_q;
+ s_sent_d = s_sent_q;
+ if (source.bvalid && source.bready) begin
+ s_pending_d[0] = 1'b0;
+ s_sent_d[0] = 1'b0;
+ end
+ if (source.rvalid && source.rready && source.rlast) begin
+ s_pending_d[1] = 1'b0;
+ s_sent_d[1] = 1'b0;
+ end
+ if (sink.awvalid && sink.awready) s_sent_d[0] = 1'b1;
+ if (sink.arvalid && sink.arready) s_sent_d[1] = 1'b1;
+ if (source.awvalid && source.awready) begin
+ s_pending_d[0] = 1'b1;
+ s_aw_d.id = source.awid;
+ s_aw_d.addr = source.awaddr;
+ s_aw_d.len = source.awlen;
+ s_aw_d.size = source.awsize;
+ s_aw_d.burst = source.awburst;
+ s_aw_d.lock = source.awlock;
+ s_aw_d.cache = source.awcache;
+ s_aw_d.prot = source.awprot;
+ s_aw_d.qos = source.awqos;
+ s_aw_d.region = source.awregion;
+ s_aw_d.user = source.awuser;
+ end
+ if (source.arvalid && source.arready) begin
+ s_pending_d[1] = 1'b1;
+ s_ar_d.id = source.arid;
+ s_ar_d.addr = source.araddr;
+ s_ar_d.len = source.arlen;
+ s_ar_d.size = source.arsize;
+ s_ar_d.burst = source.arburst;
+ s_ar_d.lock = source.arlock;
+ s_ar_d.cache = source.arcache;
+ s_ar_d.prot = source.arprot;
+ s_ar_d.qos = source.arqos;
+ s_ar_d.region = source.arregion;
+ s_ar_d.user = source.aruser;
+ end
+ end
+ dffr #(
+ .DATA_WIDTH($bits(address_t))
+ ) u_aw (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .dat_i (s_aw_d),
+ .dat_o (s_aw_q)
+ );
+ dffr #(
+ .DATA_WIDTH($bits(address_t))
+ ) u_ar (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .dat_i (s_ar_d),
+ .dat_o (s_ar_q)
+ );
+ dffr #(
+ .DATA_WIDTH(2)
+ ) u_pending (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .dat_i (s_pending_d),
+ .dat_o (s_pending_q)
+ );
+ dffr #(
+ .DATA_WIDTH(2)
+ ) u_sent (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .dat_i (s_sent_d),
+ .dat_o (s_sent_q)
+ );
+endmodule
diff --git a/rtl/tiny/top/tiny_axi4_async_bridge.sv b/rtl/tiny/top/tiny_axi4_async_bridge.sv
new file mode 100644
index 00000000..c95a39b8
--- /dev/null
+++ b/rtl/tiny/top/tiny_axi4_async_bridge.sv
@@ -0,0 +1,432 @@
+// Copyright (c) 2026 Yuchi Miao
+// SPDX-License-Identifier: MulanPSL-2.0
+
+module tiny_axi4_async_bridge #(
+ parameter int unsigned AddrWidth = 32,
+ parameter int unsigned DataWidth = 32,
+ parameter int unsigned IdWidth = 1,
+ parameter int unsigned UserWidth = 1,
+ parameter int unsigned FifoDepth = 4
+) (
+ // verilog_format: off -- preserve the source/destination clock boundary columns
+ input logic src_clk_i,
+ input logic src_rst_n_i,
+ input logic dst_clk_i,
+ input logic dst_rst_n_i,
+ input logic clear_i,
+ output logic clear_busy_o,
+ output logic ready_o,
+ output logic [7:0] epoch_o,
+ axi4_if.slave src_axi4,
+ axi4_if.master dst_axi4
+ // verilog_format: on
+);
+ localparam int unsigned StrbWidth = DataWidth / 8;
+ localparam int unsigned AwWidth = IdWidth + AddrWidth + 8 + 3 + 2 + 1 + 4 + 3 + 4 + 4 + UserWidth;
+ localparam int unsigned WWidth = DataWidth + StrbWidth + 1 + UserWidth;
+ localparam int unsigned BWidth = IdWidth + 2 + UserWidth;
+ localparam int unsigned ArWidth = AwWidth;
+ localparam int unsigned RWidth = IdWidth + DataWidth + 2 + 1 + UserWidth;
+
+ logic [AwWidth-1:0] s_aw_src_data;
+ logic [AwWidth-1:0] s_aw_dst_data;
+ logic [ WWidth-1:0] s_w_src_data;
+ logic [ WWidth-1:0] s_w_dst_data;
+ logic [ BWidth-1:0] s_b_dst_data;
+ logic [ BWidth-1:0] s_b_src_data;
+ logic [ArWidth-1:0] s_ar_src_data;
+ logic [ArWidth-1:0] s_ar_dst_data;
+ logic [ RWidth-1:0] s_r_dst_data;
+ logic [ RWidth-1:0] s_r_src_data;
+ logic s_clear_src;
+ logic s_clear_dst;
+ logic s_clear_q;
+ logic s_clear_dst_q;
+ logic s_clear_src_pulse;
+ logic s_clear_dst_pulse;
+ logic s_dst_rst_n_src_q;
+ logic s_dst_reset_evt;
+ logic [7:0] s_epoch_d, s_epoch_q;
+ logic s_aw_src_clear_busy, s_aw_dst_clear_busy;
+ logic s_w_src_clear_busy, s_w_dst_clear_busy;
+ logic s_b_src_clear_busy, s_b_dst_clear_busy;
+ logic s_ar_src_clear_busy, s_ar_dst_clear_busy;
+ logic s_r_src_clear_busy, s_r_dst_clear_busy;
+ logic s_dst_busy_src;
+ logic s_link_src_rst_n, s_link_dst_rst_n;
+ logic s_src_link_rst_n;
+ logic s_src_accept;
+ logic s_aw_src_ready_i, s_w_src_ready_i, s_ar_src_ready_i;
+ logic s_b_src_valid, s_r_src_valid;
+ logic s_write_pending_d, s_write_pending_q;
+ logic s_read_pending_d, s_read_pending_q;
+ logic s_abort_write_d, s_abort_write_q;
+ logic s_abort_read_d, s_abort_read_q;
+ logic [IdWidth-1:0] s_write_id_d, s_write_id_q;
+ logic [IdWidth-1:0] s_read_id_d, s_read_id_q;
+ logic [8:0] s_write_left_d, s_write_left_q, s_read_left_d, s_read_left_q;
+ logic [BWidth-1:0] s_b_hold_d, s_b_hold_q;
+ logic [RWidth-1:0] s_r_hold_d, s_r_hold_q;
+ logic s_b_hold_valid_d, s_b_hold_valid_q, s_r_hold_valid_d, s_r_hold_valid_q;
+ logic s_b_take, s_r_take, s_abort_evt;
+ logic s_clear_wait_reset_d, s_clear_wait_reset_q;
+
+ cdc_reset_barrier #(
+ .STAGES(5)
+ ) u_link_barrier (
+ .clk_a_i (src_clk_i),
+ .clk_b_i (dst_clk_i),
+ .rst_n_i (src_rst_n_i && dst_rst_n_i),
+ .release_i(1'b1),
+ .rst_a_n_o(s_link_src_rst_n),
+ .rst_b_n_o(s_link_dst_rst_n)
+ );
+ assign s_src_link_rst_n = s_link_src_rst_n;
+ assign s_src_accept = s_src_link_rst_n &&
+ !clear_busy_o && !s_abort_write_q && !s_abort_read_q && !s_abort_evt && !s_clear_wait_reset_q;
+ assign ready_o = s_src_accept;
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(1)
+ ) u_busy_sync (
+ .clk_i(src_clk_i),
+ .rst_n_i(src_rst_n_i),
+ .dat_i(s_aw_dst_clear_busy || s_w_dst_clear_busy || s_b_dst_clear_busy ||
+ s_ar_dst_clear_busy || s_r_dst_clear_busy || !s_link_dst_rst_n),
+ .dat_o(s_dst_busy_src)
+ );
+
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(1)
+ ) u_clear_src_sync (
+ .clk_i (src_clk_i),
+ .rst_n_i(src_rst_n_i),
+ .dat_i (clear_i),
+ .dat_o (s_clear_src)
+ );
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(1)
+ ) u_clear_sync (
+ .clk_i (dst_clk_i),
+ .rst_n_i(dst_rst_n_i),
+ .dat_i (clear_i),
+ .dat_o (s_clear_dst)
+ );
+ assign s_clear_src_pulse = s_clear_src && !s_clear_q;
+ assign s_clear_dst_pulse = s_clear_dst && !s_clear_dst_q;
+ assign s_dst_reset_evt = s_dst_rst_n_src_q && !s_link_src_rst_n;
+ assign s_abort_evt = s_dst_reset_evt || s_clear_src_pulse;
+
+ assign src_axi4.awready = s_src_accept && !s_write_pending_q && s_aw_src_ready_i;
+ // Accepted write data must drain even when its destination session is lost.
+ // Never report B until all AWLEN+1 source beats have been consumed.
+ assign src_axi4.wready = s_write_pending_q && s_write_left_q != 0 &&
+ (s_abort_write_q || (s_src_accept && s_w_src_ready_i));
+ assign src_axi4.arready = s_src_accept && !s_read_pending_q && s_ar_src_ready_i;
+
+ // These registers belong to the surviving source session. Remote reset may
+ // discard FIFO payload, but cannot revoke VALID or rewrite a held response.
+ assign {src_axi4.bid, src_axi4.bresp, src_axi4.buser} = s_b_hold_q;
+ assign {src_axi4.rid, src_axi4.rdata, src_axi4.rresp,
+ src_axi4.rlast, src_axi4.ruser} = s_r_hold_q;
+ assign src_axi4.bvalid = s_b_hold_valid_q;
+ assign src_axi4.rvalid = s_r_hold_valid_q;
+ assign s_b_take = (!s_b_hold_valid_q || src_axi4.bready) && s_write_pending_q &&
+ !(src_axi4.bvalid && src_axi4.bready) &&
+ s_write_left_q == 0 && !s_abort_write_q && !s_abort_evt && s_src_accept;
+ assign s_r_take = (!s_r_hold_valid_q || src_axi4.rready) && s_read_pending_q &&
+ !(src_axi4.rvalid && src_axi4.rready && src_axi4.rlast) &&
+ !s_abort_read_q && !s_abort_evt && s_src_accept;
+
+ always_comb begin
+ s_write_pending_d = s_write_pending_q;
+ s_read_pending_d = s_read_pending_q;
+ s_abort_write_d = s_abort_write_q;
+ s_abort_read_d = s_abort_read_q;
+ s_write_id_d = s_write_id_q;
+ s_read_id_d = s_read_id_q;
+ s_write_left_d = s_write_left_q;
+ s_read_left_d = s_read_left_q;
+ s_b_hold_d = s_b_hold_q;
+ s_r_hold_d = s_r_hold_q;
+ s_b_hold_valid_d = s_b_hold_valid_q;
+ s_r_hold_valid_d = s_r_hold_valid_q;
+ s_clear_wait_reset_d = s_clear_wait_reset_q;
+ if (src_axi4.awvalid && src_axi4.awready) begin
+ s_write_pending_d = 1'b1;
+ s_write_id_d = src_axi4.awid;
+ s_write_left_d = {1'b0, src_axi4.awlen} + 9'd1;
+ end
+ if (src_axi4.arvalid && src_axi4.arready) begin
+ s_read_pending_d = 1'b1;
+ s_read_id_d = src_axi4.arid;
+ s_read_left_d = {1'b0, src_axi4.arlen} + 9'd1;
+ end
+ if (src_axi4.wvalid && src_axi4.wready) s_write_left_d = s_write_left_q - 9'd1;
+ if (src_axi4.bvalid && src_axi4.bready) begin
+ s_write_pending_d = 1'b0;
+ s_abort_write_d = 1'b0;
+ s_b_hold_valid_d = 1'b0;
+ end
+ if (src_axi4.rvalid && src_axi4.rready) begin
+ s_r_hold_valid_d = 1'b0;
+ s_read_left_d = s_read_left_q - 9'd1;
+ if (src_axi4.rlast) begin
+ s_read_pending_d = 1'b0;
+ s_abort_read_d = 1'b0;
+ end
+ end
+ // A source handshake is irrevocable. Abort only the work still pending
+ // after that handshake; do not create a second terminal response.
+ if (s_abort_evt) begin
+ s_abort_write_d = s_write_pending_d;
+ s_abort_read_d = s_read_pending_d;
+ end
+ // A FIFO clear alone cannot cancel a transaction already accepted by a
+ // remote AXI slave. Return local errors, but quarantine late responses
+ // until an actual destination reset establishes a new session. Normal
+ // drained clears and already exposed terminal responses need no reset.
+ if (s_clear_src_pulse &&
+ ((s_write_pending_d && !s_b_hold_valid_q) ||
+ (s_read_pending_d && !(s_r_hold_valid_q && src_axi4.rlast))))
+ s_clear_wait_reset_d = 1'b1;
+ if (s_dst_reset_evt) s_clear_wait_reset_d = 1'b0;
+ if (s_b_take && s_b_src_valid) begin
+ s_b_hold_d = s_b_src_data;
+ s_b_hold_valid_d = 1'b1;
+ end else if (!s_b_hold_valid_d && s_abort_write_d && s_write_left_d == 0) begin
+ s_b_hold_d = {s_write_id_q, 2'b11, {UserWidth{1'b0}}};
+ s_b_hold_valid_d = 1'b1;
+ end
+ if (s_r_take && s_r_src_valid) begin
+ s_r_hold_d = s_r_src_data;
+ s_r_hold_valid_d = 1'b1;
+ end else if (!s_r_hold_valid_d && s_abort_read_d && s_read_left_d != 0) begin
+ s_r_hold_d = {
+ s_read_id_q, {DataWidth{1'b0}}, 2'b11, (s_read_left_d == 9'd1), {UserWidth{1'b0}}
+ };
+ s_r_hold_valid_d = 1'b1;
+ end
+ end
+
+ assign s_aw_src_data = {
+ src_axi4.awid,
+ src_axi4.awaddr,
+ src_axi4.awlen,
+ src_axi4.awsize,
+ src_axi4.awburst,
+ src_axi4.awlock,
+ src_axi4.awcache,
+ src_axi4.awprot,
+ src_axi4.awqos,
+ src_axi4.awregion,
+ src_axi4.awuser
+ };
+ assign {
+ dst_axi4.awid,
+ dst_axi4.awaddr,
+ dst_axi4.awlen,
+ dst_axi4.awsize,
+ dst_axi4.awburst,
+ dst_axi4.awlock,
+ dst_axi4.awcache,
+ dst_axi4.awprot,
+ dst_axi4.awqos,
+ dst_axi4.awregion,
+ dst_axi4.awuser
+ } = s_aw_dst_data;
+
+ cdc_fifo_warm_flush #(
+ .DATA_WIDTH (AwWidth),
+ .BUFFER_DEPTH(FifoDepth)
+ ) u_aw_fifo (
+ .src_clk_i (src_clk_i),
+ .src_rst_n_i (s_src_link_rst_n),
+ .src_clear_i (s_clear_src_pulse),
+ .src_clear_busy_o(s_aw_src_clear_busy),
+ .src_data_i (s_aw_src_data),
+ .src_valid_i (src_axi4.awvalid && s_src_accept && !s_write_pending_q),
+ .src_ready_o (s_aw_src_ready_i),
+ .dst_clk_i (dst_clk_i),
+ .dst_rst_n_i (dst_rst_n_i),
+ .dst_clear_busy_o(s_aw_dst_clear_busy),
+ .dst_data_o (s_aw_dst_data),
+ .dst_valid_o (dst_axi4.awvalid),
+ .dst_ready_i (dst_axi4.awready)
+ );
+
+ assign s_w_src_data = {src_axi4.wdata, src_axi4.wstrb, src_axi4.wlast, src_axi4.wuser};
+ assign {dst_axi4.wdata, dst_axi4.wstrb, dst_axi4.wlast, dst_axi4.wuser} = s_w_dst_data;
+
+ cdc_fifo_warm_flush #(
+ .DATA_WIDTH (WWidth),
+ .BUFFER_DEPTH(FifoDepth)
+ ) u_w_fifo (
+ .src_clk_i(src_clk_i),
+ .src_rst_n_i(s_src_link_rst_n),
+ .src_clear_i(s_clear_src_pulse),
+ .src_clear_busy_o(s_w_src_clear_busy),
+ .src_data_i(s_w_src_data),
+ .src_valid_i(src_axi4.wvalid && s_src_accept && s_write_pending_q && s_write_left_q != 0),
+ .src_ready_o(s_w_src_ready_i),
+ .dst_clk_i(dst_clk_i),
+ .dst_rst_n_i(dst_rst_n_i),
+ .dst_clear_busy_o(s_w_dst_clear_busy),
+ .dst_data_o(s_w_dst_data),
+ .dst_valid_o(dst_axi4.wvalid),
+ .dst_ready_i(dst_axi4.wready)
+ );
+
+ assign s_b_dst_data = {dst_axi4.bid, dst_axi4.bresp, dst_axi4.buser};
+ cdc_fifo_warm_flush #(
+ .DATA_WIDTH (BWidth),
+ .BUFFER_DEPTH(FifoDepth)
+ ) u_b_fifo (
+ .src_clk_i (dst_clk_i),
+ .src_rst_n_i (dst_rst_n_i),
+ .src_clear_i (s_clear_dst_pulse),
+ .src_clear_busy_o(s_b_dst_clear_busy),
+ .src_data_i (s_b_dst_data),
+ .src_valid_i (dst_axi4.bvalid),
+ .src_ready_o (dst_axi4.bready),
+ .dst_clk_i (src_clk_i),
+ .dst_rst_n_i (s_src_link_rst_n),
+ .dst_clear_busy_o(s_b_src_clear_busy),
+ .dst_data_o (s_b_src_data),
+ .dst_valid_o (s_b_src_valid),
+ .dst_ready_i (s_b_take)
+ );
+
+ assign s_ar_src_data = {
+ src_axi4.arid,
+ src_axi4.araddr,
+ src_axi4.arlen,
+ src_axi4.arsize,
+ src_axi4.arburst,
+ src_axi4.arlock,
+ src_axi4.arcache,
+ src_axi4.arprot,
+ src_axi4.arqos,
+ src_axi4.arregion,
+ src_axi4.aruser
+ };
+ assign {
+ dst_axi4.arid,
+ dst_axi4.araddr,
+ dst_axi4.arlen,
+ dst_axi4.arsize,
+ dst_axi4.arburst,
+ dst_axi4.arlock,
+ dst_axi4.arcache,
+ dst_axi4.arprot,
+ dst_axi4.arqos,
+ dst_axi4.arregion,
+ dst_axi4.aruser
+ } = s_ar_dst_data;
+
+ cdc_fifo_warm_flush #(
+ .DATA_WIDTH (ArWidth),
+ .BUFFER_DEPTH(FifoDepth)
+ ) u_ar_fifo (
+ .src_clk_i (src_clk_i),
+ .src_rst_n_i (s_src_link_rst_n),
+ .src_clear_i (s_clear_src_pulse),
+ .src_clear_busy_o(s_ar_src_clear_busy),
+ .src_data_i (s_ar_src_data),
+ .src_valid_i (src_axi4.arvalid && s_src_accept && !s_read_pending_q),
+ .src_ready_o (s_ar_src_ready_i),
+ .dst_clk_i (dst_clk_i),
+ .dst_rst_n_i (dst_rst_n_i),
+ .dst_clear_busy_o(s_ar_dst_clear_busy),
+ .dst_data_o (s_ar_dst_data),
+ .dst_valid_o (dst_axi4.arvalid),
+ .dst_ready_i (dst_axi4.arready)
+ );
+
+ assign s_r_dst_data = {
+ dst_axi4.rid, dst_axi4.rdata, dst_axi4.rresp, dst_axi4.rlast, dst_axi4.ruser
+ };
+ cdc_fifo_warm_flush #(
+ .DATA_WIDTH (RWidth),
+ .BUFFER_DEPTH(FifoDepth)
+ ) u_r_fifo (
+ .src_clk_i (dst_clk_i),
+ .src_rst_n_i (dst_rst_n_i),
+ .src_clear_i (s_clear_dst_pulse),
+ .src_clear_busy_o(s_r_dst_clear_busy),
+ .src_data_i (s_r_dst_data),
+ .src_valid_i (dst_axi4.rvalid),
+ .src_ready_o (dst_axi4.rready),
+ .dst_clk_i (src_clk_i),
+ .dst_rst_n_i (s_src_link_rst_n),
+ .dst_clear_busy_o(s_r_src_clear_busy),
+ .dst_data_o (s_r_src_data),
+ .dst_valid_o (s_r_src_valid),
+ .dst_ready_i (s_r_take)
+ );
+
+ assign clear_busy_o = s_aw_src_clear_busy || s_w_src_clear_busy ||
+ s_b_src_clear_busy || s_ar_src_clear_busy ||
+ s_r_src_clear_busy || s_dst_busy_src;
+ assign s_epoch_d = ((s_clear_src_pulse && !clear_busy_o) || s_dst_reset_evt) ?
+ s_epoch_q + 1'b1 : s_epoch_q;
+ assign epoch_o = s_epoch_q;
+
+ always_ff @(posedge src_clk_i or negedge src_rst_n_i) begin
+ if (!src_rst_n_i) begin
+ s_clear_q <= 1'b0;
+ s_dst_rst_n_src_q <= 1'b0;
+ s_epoch_q <= '0;
+ s_write_pending_q <= 1'b0;
+ s_read_pending_q <= 1'b0;
+ s_abort_write_q <= 1'b0;
+ s_abort_read_q <= 1'b0;
+ s_write_id_q <= '0;
+ s_read_id_q <= '0;
+ s_write_left_q <= '0;
+ s_read_left_q <= '0;
+ s_b_hold_q <= '0;
+ s_r_hold_q <= '0;
+ s_b_hold_valid_q <= 1'b0;
+ s_r_hold_valid_q <= 1'b0;
+ s_clear_wait_reset_q <= 1'b0;
+ end else begin
+ s_clear_q <= s_clear_src;
+ s_dst_rst_n_src_q <= s_link_src_rst_n;
+ s_epoch_q <= s_epoch_d;
+ s_write_pending_q <= s_write_pending_d;
+ s_read_pending_q <= s_read_pending_d;
+ s_abort_write_q <= s_abort_write_d;
+ s_abort_read_q <= s_abort_read_d;
+ s_write_id_q <= s_write_id_d;
+ s_read_id_q <= s_read_id_d;
+ s_write_left_q <= s_write_left_d;
+ s_read_left_q <= s_read_left_d;
+ s_b_hold_q <= s_b_hold_d;
+ s_r_hold_q <= s_r_hold_d;
+ s_b_hold_valid_q <= s_b_hold_valid_d;
+ s_r_hold_valid_q <= s_r_hold_valid_d;
+ s_clear_wait_reset_q <= s_clear_wait_reset_d;
+ end
+ end
+
+ always_ff @(posedge dst_clk_i or negedge dst_rst_n_i) begin
+ if (!dst_rst_n_i) begin
+ s_clear_dst_q <= 1'b0;
+ end else begin
+ s_clear_dst_q <= s_clear_dst;
+ end
+ end
+
+`ifndef SYNTHESIS
+ initial begin
+ if ((AddrWidth < 1) || (DataWidth < 8) || ((DataWidth % 8) != 0) ||
+ (IdWidth < 1) || (UserWidth < 1) || (FifoDepth < 2) ||
+ ((FifoDepth & (FifoDepth - 1)) != 0)) begin
+ $fatal(1, "axi4_async_bridge: invalid interface or FIFO parameters");
+ end
+ end
+`endif
+endmodule
diff --git a/rtl/tiny/top/tiny_axi4_fabric.sv b/rtl/tiny/top/tiny_axi4_fabric.sv
index a4473d48..2446037f 100644
--- a/rtl/tiny/top/tiny_axi4_fabric.sv
+++ b/rtl/tiny/top/tiny_axi4_fabric.sv
@@ -1,297 +1,279 @@
// Copyright (c) 2026 Yuchi Miao
// SPDX-License-Identifier: MulanPSL-2.0
-
`include "mmap_define.svh"
+`include "tiny_topology.svh"
-// One accepted transaction globally. AW and W remain independent: W is
-// backpressured until the saved AW has reached its target. The owner and all
-// address attributes remain registered until the terminal response handshake.
+// Three active owners and topology-selected independent targets. CPU admits one
+// combined operation; DMA and SDIO each admit one read and one write. IDs are
+// unchanged: saved target ownership, not ID expansion, routes all responses.
module tiny_axi4_fabric (
- input logic clk_i,
- input logic rst_n_i,
- input logic perf_enable_i,
- input logic perf_clear_i,
- axi4_if.slave masters [2],
- axi4_if.master targets [5],
- output logic fault_valid_o,
- output logic [31:0] fault_addr_o,
- output logic fault_master_o,
- output logic fault_write_o,
- output logic [ 1:0] fault_resp_o,
- output logic [63:0] cpu_wait_o,
- output logic [63:0] dma_wait_o
+ input logic clk_i,
+ input logic rst_n_i,
+ input logic perf_enable_i,
+ input logic perf_clear_i,
+ input logic target_rst_n_i[`RETROSOC_TINY__TARGET_COUNT],
+ axi4_if.slave masters[3],
+ axi4_if.master targets[`RETROSOC_TINY__TARGET_COUNT],
+ output logic [5:0] fault_valid_o,
+ output logic [5:0][31:0] fault_addr_o,
+ output logic [5:0][1:0] fault_resp_o,
+ output logic [5:0] fault_instruction_o,
+ output logic [2:0] owner_idle_o,
+ output logic [`RETROSOC_TINY__TARGET_COUNT-1:0] target_idle_o,
+ output logic [63:0] cpu_wait_o,
+ output logic [63:0] dma_wait_o
);
- typedef enum logic [1:0] {
- Idle,
- Addr,
- Response
- } state_e;
- typedef struct packed {
- logic [31:0] addr;
- logic [7:0] len;
- logic [2:0] size;
- logic [1:0] burst;
- logic lock;
- logic [3:0] cache;
- logic [2:0] prot;
- logic [3:0] qos;
- logic [3:0] region;
- logic id;
- logic user;
- } addr_t;
- state_e s_state_d, s_state_q;
- logic [1:0] s_state_bits_q;
- addr_t s_addr_d, s_addr_q;
- addr_t s_read_addr [2];
- addr_t s_write_addr [2];
- addr_t s_selected_addr;
- logic s_owner_d, s_owner_q;
- logic s_write_d, s_write_q;
- logic [2:0] s_tgt_d, s_tgt_q;
- logic [3:0] s_request, s_unused_grant;
- logic [ 1:0] s_selected;
- logic s_selected_valid;
- logic s_accept;
- logic s_terminal;
- logic [32:0] s_last_addr;
- logic [ 2:0] s_decoded_target;
- logic s_protocol_legal;
- logic [1:0] s_awvalid, s_arvalid, s_wvalid, s_wlast, s_bready, s_rready;
- logic [1:0][31:0] s_wdata;
- logic [1:0][ 3:0] s_wstrb;
- logic [1:0] s_wuser;
- logic [4:0] s_target_awready, s_target_arready, s_target_wready;
- logic [4:0] s_target_bvalid, s_target_rvalid, s_target_rlast;
- logic [4:0] s_target_bid, s_target_rid, s_target_buser, s_target_ruser;
- logic [4:0][1:0] s_target_bresp, s_target_rresp;
- logic [4:0][31:0] s_target_rdata;
+ localparam logic [4:0] DecodeErrorTarget = 5'(`RETROSOC_TINY__DECODE_ERROR);
+ localparam logic [4:0] ProtocolErrorTarget = 5'(`RETROSOC_TINY__PROTOCOL_ERROR);
+ logic [5:0] s_busy_d, s_busy_q, s_admit, s_done, s_eligible;
+ logic s_cpu_prefer_write_d, s_cpu_prefer_write_q;
+ logic [2:0][4:0] s_read_target, s_write_target;
+ logic [`RETROSOC_TINY__TARGET_COUNT-1:0][2:0] s_arready, s_awready, s_wready, s_rvalid, s_bvalid;
+ logic [`RETROSOC_TINY__TARGET_COUNT-1:0][ 2:0][36:0] s_rdata;
+ logic [`RETROSOC_TINY__TARGET_COUNT-1:0][ 2:0][ 3:0] s_bdata;
+ logic [ 2:0][36:0] s_read_return;
+ logic [ 2:0][ 3:0] s_write_return;
+ logic [2:0] s_master_arready, s_master_awready, s_master_wready, s_master_rvalid, s_master_bvalid;
+ logic [2:0] s_arvalid, s_awvalid;
logic [1:0][63:0] s_wait_d, s_wait_q;
+ logic [`RETROSOC_TINY__TARGET_COUNT-1:0] s_fault_valid, s_fault_write, s_fault_instruction;
+ logic [`RETROSOC_TINY__TARGET_COUNT-1:0][31:0] s_fault_addr;
+ logic [`RETROSOC_TINY__TARGET_COUNT-1:0][1:0] s_fault_master, s_fault_resp;
- function automatic logic [2:0] decode_target(input logic [31:0] addr_i);
- if (`SOC_ADDR_IS_SRAM(addr_i)) return 3'd0;
- if (`SOC_ADDR_IS_FLASH(addr_i) || `SOC_ADDR_IS_XPI(addr_i)) return 3'd1;
- if (`SOC_ADDR_IS_APB4_PERIPH(addr_i) || `SOC_ADDR_IS_APB4_SYSTEM(addr_i)) return 3'd2;
- return 3'd3;
+ function automatic logic [4:0] decode_target(input logic [31:0] addr_i);
+ if (`SOC_ADDR_IS_SRAM(addr_i)) return {3'd0, addr_i[16:15]};
+ if (`SOC_ADDR_IS_FLASH(addr_i) || `SOC_ADDR_IS_XPI(addr_i)) return 5'd4;
+ `include "tiny_fabric_decode.svh"
+ return DecodeErrorTarget;
+ endfunction
+ function automatic logic [4:0] checked_target(input logic [31:0] addr_i, input logic [7:0] len_i,
+ input logic [2:0] size_i, input logic [1:0] burst_i,
+ input logic lock_i, input logic instruction_i);
+ logic [32:0] last_addr;
+ logic [ 4:0] decoded;
+ decoded = decode_target(addr_i);
+ last_addr = {1'b0, addr_i} + (({25'd0, len_i} + 33'd1) << size_i) - 33'd1;
+ if (lock_i || (size_i > 3'd2) || (len_i > 8'd15) ||
+ ((addr_i & ((32'd1 << size_i) - 32'd1)) != 32'd0) ||
+ !((burst_i == 2'b01) || ((burst_i == 2'b00) && (len_i == 8'd0))) ||
+ last_addr[32] || (last_addr[31:12] != addr_i[31:12]) ||
+ (decode_target(
+ last_addr[31:0]
+ ) != decoded) || ((decoded >= 5'd5) && (decoded < DecodeErrorTarget) && (len_i != 8'd0)))
+ return ProtocolErrorTarget;
+ if (instruction_i && !(
+ `SOC_ADDR_IS_FLASH(addr_i)
+ || ((addr_i >= 32'h5000_0000) && (addr_i < 32'h5400_0000)))) begin
+ if ((decoded >= 5'd5) && (decoded < DecodeErrorTarget)) return ProtocolErrorTarget;
+ return DecodeErrorTarget;
+ end
+ return decoded;
endfunction
- assign s_state_q = state_e'(s_state_bits_q);
- assign s_request = {s_awvalid[1], s_arvalid[1], s_awvalid[0], s_arvalid[0]};
- assign s_selected_addr = s_selected[0] ? s_write_addr[s_selected[1]] : s_read_addr[s_selected[1]];
- assign s_decoded_target = decode_target(s_selected_addr.addr);
- assign s_last_addr = {1'b0, s_selected_addr.addr} +
- (({25'd0, s_selected_addr.len} + 33'd1) << s_selected_addr.size) - 33'd1;
- assign s_protocol_legal = !s_selected_addr.lock && (s_selected_addr.size <= 3'd2) &&
- ((s_selected_addr.addr & ((32'd1 << s_selected_addr.size) - 32'd1)) == 32'd0) &&
- ((s_selected_addr.burst == 2'b01) ||
- ((s_selected_addr.burst == 2'b00) && (s_selected_addr.len == 8'd0))) &&
- (s_selected_addr.len <= 8'd15) && !s_last_addr[32] &&
- (s_last_addr[31:12] == s_selected_addr.addr[31:12]) &&
- (decode_target(
- s_last_addr[31:0]
- ) == s_decoded_target) && ((s_decoded_target != 3'd2) || (s_selected_addr.len == 8'd0));
- assign s_accept = (s_state_q == Idle) && s_selected_valid;
- assign s_terminal = (s_state_q == Response) &&
- (s_write_q ? (s_target_bvalid[s_tgt_q] && s_bready[s_owner_q]) :
- (s_target_rvalid[s_tgt_q] && s_rready[s_owner_q] && s_target_rlast[s_tgt_q]));
- assign fault_resp_o = s_write_q ? s_target_bresp[s_tgt_q] : s_target_rresp[s_tgt_q];
- assign fault_valid_o = s_terminal && (fault_resp_o != 2'b00);
- assign fault_addr_o = s_addr_q.addr;
- assign fault_master_o = s_owner_q;
- assign fault_write_o = s_write_q;
- assign cpu_wait_o = s_wait_q[0];
- assign dma_wait_o = s_wait_q[1];
-
- // Advance on acceptance: the chosen owner is saved until completion, so the
- // next grant after completion starts after this source/direction.
- round_robin_arbiter #(
- .CLIENTS(4)
- ) u_arbiter (
- .clk_i (clk_i),
- .rst_n_i (rst_n_i),
- .advance_i (s_accept),
- .request_i (s_request),
- .grant_o (s_unused_grant),
- .selected_o(s_selected),
- .valid_o (s_selected_valid)
- );
-
- always_comb begin
- s_state_d = s_state_q;
- s_owner_d = s_owner_q;
- s_write_d = s_write_q;
- s_addr_d = s_addr_q;
- s_tgt_d = s_tgt_q;
- unique case (s_state_q)
- Idle:
- if (s_accept) begin
- s_owner_d = s_selected[1];
- s_write_d = s_selected[0];
- s_addr_d = s_selected_addr;
- s_tgt_d = s_protocol_legal ? s_decoded_target : 3'd4;
- s_state_d = Addr;
- end
- Addr:
- if (s_write_q ? s_target_awready[s_tgt_q] : s_target_arready[s_tgt_q]) s_state_d = Response;
- Response: if (s_terminal) s_state_d = Idle;
- default: s_state_d = Idle;
- endcase
- s_wait_d = s_wait_q;
- for (int unsigned master = 0; master < 2; master++) begin
- if (perf_clear_i) s_wait_d[master] = '0;
- else if (perf_enable_i && (s_awvalid[master] || s_arvalid[master]) &&
- !(s_accept && (s_selected[1] == 1'(master))) &&
- (s_wait_q[master] != 64'hffff_ffff_ffff_ffff))
- s_wait_d[master] = s_wait_q[master] + 64'd1;
- end
- end
- for (genvar master = 0; master < 2; master++) begin : gen_master
- assign s_write_addr[master] = {
- masters[master].awaddr,
- masters[master].awlen,
- masters[master].awsize,
- masters[master].awburst,
- masters[master].awlock,
- masters[master].awcache,
- masters[master].awprot,
- masters[master].awqos,
- masters[master].awregion,
- masters[master].awid,
- masters[master].awuser
- };
- assign s_read_addr[master] = {
- masters[master].araddr,
- masters[master].arlen,
- masters[master].arsize,
- masters[master].arburst,
- masters[master].arlock,
- masters[master].arcache,
- masters[master].arprot,
- masters[master].arqos,
- masters[master].arregion,
- masters[master].arid,
- masters[master].aruser
- };
- assign s_awvalid[master] = masters[master].awvalid;
- assign s_arvalid[master] = masters[master].arvalid;
- assign s_wdata[master] = masters[master].wdata;
- assign s_wstrb[master] = masters[master].wstrb;
- assign s_wlast[master] = masters[master].wlast;
- assign s_wuser[master] = masters[master].wuser;
- assign s_wvalid[master] = masters[master].wvalid;
- assign s_bready[master] = masters[master].bready;
- assign s_rready[master] = masters[master].rready;
- assign masters[master].awready = s_accept && (s_selected == {1'(master), 1'b1});
- assign masters[master].arready = s_accept && (s_selected == {1'(master), 1'b0});
- assign masters[master].wready = (s_state_q == Response) && s_write_q &&
- (s_owner_q == 1'(master)) && s_target_wready[s_tgt_q];
- assign masters[master].bvalid = (s_state_q == Response) && s_write_q &&
- (s_owner_q == 1'(master)) && s_target_bvalid[s_tgt_q];
- assign masters[master].rvalid = (s_state_q == Response) && !s_write_q &&
- (s_owner_q == 1'(master)) && s_target_rvalid[s_tgt_q];
- assign masters[master].bid = s_target_bid[s_tgt_q];
- assign masters[master].bresp = s_target_bresp[s_tgt_q];
- assign masters[master].buser = s_target_buser[s_tgt_q];
- assign masters[master].rid = s_target_rid[s_tgt_q];
- assign masters[master].rdata = s_target_rdata[s_tgt_q];
- assign masters[master].rresp = s_target_rresp[s_tgt_q];
- assign masters[master].rlast = s_target_rlast[s_tgt_q];
- assign masters[master].ruser = s_target_ruser[s_tgt_q];
- end
- for (genvar target = 0; target < 5; target++) begin : gen_target
- assign targets[target].awaddr = s_addr_q.addr;
- assign targets[target].awlen = s_addr_q.len;
- assign targets[target].awsize = s_addr_q.size;
- assign targets[target].awburst = s_addr_q.burst;
- assign targets[target].awlock = s_addr_q.lock;
- assign targets[target].awcache = s_addr_q.cache;
- assign targets[target].awprot = s_addr_q.prot;
- assign targets[target].awqos = s_addr_q.qos;
- assign targets[target].awregion = s_addr_q.region;
- assign targets[target].awid = s_addr_q.id;
- assign targets[target].awuser = s_addr_q.user;
- assign targets[target].awvalid = (s_state_q == Addr) && s_write_q && (s_tgt_q == 3'(target));
- assign targets[target].araddr = s_addr_q.addr;
- assign targets[target].arlen = s_addr_q.len;
- assign targets[target].arsize = s_addr_q.size;
- assign targets[target].arburst = s_addr_q.burst;
- assign targets[target].arlock = s_addr_q.lock;
- assign targets[target].arcache = s_addr_q.cache;
- assign targets[target].arprot = s_addr_q.prot;
- assign targets[target].arqos = s_addr_q.qos;
- assign targets[target].arregion = s_addr_q.region;
- assign targets[target].arid = s_addr_q.id;
- assign targets[target].aruser = s_addr_q.user;
- assign targets[target].arvalid = (s_state_q == Addr) && !s_write_q && (s_tgt_q == 3'(target));
- assign targets[target].wdata = s_wdata[s_owner_q];
- assign targets[target].wstrb = s_wstrb[s_owner_q];
- assign targets[target].wlast = s_wlast[s_owner_q];
- assign targets[target].wuser = s_wuser[s_owner_q];
- assign targets[target].wvalid = (s_state_q == Response) && s_write_q &&
- (s_tgt_q == 3'(target)) && s_wvalid[s_owner_q];
- assign targets[target].bready = (s_state_q == Response) && s_write_q &&
- (s_tgt_q == 3'(target)) && s_bready[s_owner_q];
- assign targets[target].rready = (s_state_q == Response) && !s_write_q &&
- (s_tgt_q == 3'(target)) && s_rready[s_owner_q];
- assign s_target_awready[target] = targets[target].awready;
- assign s_target_arready[target] = targets[target].arready;
- assign s_target_wready[target] = targets[target].wready;
- assign s_target_bvalid[target] = targets[target].bvalid;
- assign s_target_bid[target] = targets[target].bid;
- assign s_target_bresp[target] = targets[target].bresp;
- assign s_target_buser[target] = targets[target].buser;
- assign s_target_rvalid[target] = targets[target].rvalid;
- assign s_target_rid[target] = targets[target].rid;
- assign s_target_rresp[target] = targets[target].rresp;
- assign s_target_rdata[target] = targets[target].rdata;
- assign s_target_rlast[target] = targets[target].rlast;
- assign s_target_ruser[target] = targets[target].ruser;
- end
+ assign s_busy_d = (s_busy_q & ~s_done) | s_admit;
+ assign s_cpu_prefer_write_d = s_admit[0] ? 1'b1 : s_admit[1] ? 1'b0 : s_cpu_prefer_write_q;
+ assign cpu_wait_o = s_wait_q[0];
+ assign dma_wait_o = s_wait_q[1];
dffr #(
- .DATA_WIDTH(2)
- ) u_state_reg (
+ .DATA_WIDTH(6)
+ ) u_direction_busy (
.clk_i (clk_i),
.rst_n_i(rst_n_i),
- .dat_i (s_state_d),
- .dat_o (s_state_bits_q)
+ .dat_i (s_busy_d),
+ .dat_o (s_busy_q)
);
dffr #(
.DATA_WIDTH(1)
- ) u_owner_reg (
+ ) u_cpu_direction (
.clk_i (clk_i),
.rst_n_i(rst_n_i),
- .dat_i (s_owner_d),
- .dat_o (s_owner_q)
- );
- dffr #(
- .DATA_WIDTH(1)
- ) u_write_reg (
- .clk_i (clk_i),
- .rst_n_i(rst_n_i),
- .dat_i (s_write_d),
- .dat_o (s_write_q)
- );
- dffr #(
- .DATA_WIDTH(63)
- ) u_addr_reg (
- .clk_i (clk_i),
- .rst_n_i(rst_n_i),
- .dat_i (s_addr_d),
- .dat_o (s_addr_q)
- );
- dffr #(
- .DATA_WIDTH(3)
- ) u_target_reg (
- .clk_i (clk_i),
- .rst_n_i(rst_n_i),
- .dat_i (s_tgt_d),
- .dat_o (s_tgt_q)
+ .dat_i (s_cpu_prefer_write_d),
+ .dat_o (s_cpu_prefer_write_q)
);
dffr #(
.DATA_WIDTH(128)
- ) u_wait_reg (
+ ) u_wait_counters (
.clk_i (clk_i),
.rst_n_i(rst_n_i),
.dat_i (s_wait_d),
.dat_o (s_wait_q)
);
+
+ always_comb begin
+ s_master_arready = '0;
+ s_master_awready = '0;
+ s_master_wready = '0;
+ s_master_rvalid = '0;
+ s_master_bvalid = '0;
+ s_read_return = '0;
+ s_write_return = '0;
+ fault_valid_o = '0;
+ fault_addr_o = '0;
+ fault_resp_o = '0;
+ fault_instruction_o = '0;
+ for (int target_index = 0; target_index < `RETROSOC_TINY__TARGET_COUNT; target_index++) begin
+ s_master_arready |= s_arready[target_index];
+ s_master_awready |= s_awready[target_index];
+ s_master_wready |= s_wready[target_index];
+ s_master_rvalid |= s_rvalid[target_index];
+ s_master_bvalid |= s_bvalid[target_index];
+ for (int owner = 0; owner < 3; owner++) begin
+ if (s_rvalid[target_index][owner]) s_read_return[owner] = s_rdata[target_index][owner];
+ if (s_bvalid[target_index][owner]) s_write_return[owner] = s_bdata[target_index][owner];
+ end
+ if (s_fault_valid[target_index]) begin
+ fault_valid_o[{s_fault_master[target_index], s_fault_write[target_index]}] = 1'b1;
+ fault_addr_o[{
+ s_fault_master[target_index], s_fault_write[target_index]
+ }] = s_fault_addr[target_index];
+ fault_resp_o[{
+ s_fault_master[target_index], s_fault_write[target_index]
+ }] = s_fault_resp[target_index];
+ fault_instruction_o[{
+ s_fault_master[target_index], s_fault_write[target_index]
+ }] = s_fault_instruction[target_index];
+ end
+ end
+ s_wait_d = s_wait_q;
+ for (int owner = 0; owner < 2; owner++) begin
+ if (perf_clear_i) s_wait_d[owner] = '0;
+ else if (perf_enable_i && (s_arvalid[owner] || s_awvalid[owner]) &&
+ !(s_admit[2*owner] || s_admit[2*owner+1]) && (s_wait_q[owner] != 64'hffff_ffff_ffff_ffff))
+ s_wait_d[owner] = s_wait_q[owner] + 64'd1;
+ end
+ end
+ for (genvar owner = 0; owner < 3; owner++) begin : gen_owner
+ assign s_arvalid[owner] = masters[owner].arvalid;
+ assign s_awvalid[owner] = masters[owner].awvalid;
+ assign owner_idle_o[owner] = !(s_busy_q[2*owner] || s_busy_q[2*owner+1]);
+ assign s_read_target[owner] = checked_target(
+ masters[owner].araddr,
+ masters[owner].arlen,
+ masters[owner].arsize,
+ masters[owner].arburst,
+ masters[owner].arlock,
+ (owner == 0) && masters[owner].arprot[2]
+ );
+ assign s_write_target[owner] = checked_target(
+ masters[owner].awaddr,
+ masters[owner].awlen,
+ masters[owner].awsize,
+ masters[owner].awburst,
+ masters[owner].awlock,
+ 1'b0
+ );
+ if (owner == 0) begin : gen_combined_credit
+ assign s_eligible[2*owner] = rst_n_i && owner_idle_o[owner] && masters[owner].arvalid &&
+ (!masters[owner].awvalid || !s_cpu_prefer_write_q);
+ assign s_eligible[2*owner+1] = rst_n_i && owner_idle_o[owner] && masters[owner].awvalid &&
+ (!masters[owner].arvalid || s_cpu_prefer_write_q);
+ end else begin : gen_direction_credits
+ assign s_eligible[2*owner] = rst_n_i && !s_busy_q[2*owner] && masters[owner].arvalid;
+ assign s_eligible[2*owner+1] = rst_n_i && !s_busy_q[2*owner+1] && masters[owner].awvalid;
+ end
+ assign s_admit[2*owner] = masters[owner].arvalid && masters[owner].arready;
+ assign s_admit[2*owner+1] = masters[owner].awvalid && masters[owner].awready;
+ assign s_done[2*owner] = masters[owner].rvalid && masters[owner].rready && masters[owner].rlast;
+ assign s_done[2*owner+1] = masters[owner].bvalid && masters[owner].bready;
+ assign masters[owner].arready = s_master_arready[owner];
+ assign masters[owner].awready = s_master_awready[owner];
+ assign masters[owner].wready = s_master_wready[owner];
+ assign masters[owner].rvalid = s_master_rvalid[owner];
+ assign masters[owner].bvalid = s_master_bvalid[owner];
+ assign {masters[owner].rid, masters[owner].rdata, masters[owner].rresp,
+ masters[owner].rlast, masters[owner].ruser} = s_read_return[owner];
+ assign {masters[owner].bid, masters[owner].bresp, masters[owner].buser} = s_write_return[owner];
+ end
+ for (
+ genvar target_index = 0; target_index < `RETROSOC_TINY__TARGET_COUNT; target_index++
+ ) begin : gen_target
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) u_routes[3] (
+ .aclk (clk_i),
+ .aresetn(rst_n_i)
+ );
+ for (genvar owner = 0; owner < 3; owner++) begin : gen_route
+ logic s_read_selected, s_write_selected;
+ assign s_read_selected = s_read_target[owner] == 5'(target_index);
+ assign s_write_selected = s_write_target[owner] == 5'(target_index);
+ assign u_routes[owner].arid = masters[owner].arid;
+ assign u_routes[owner].araddr = masters[owner].araddr;
+ assign u_routes[owner].arlen = masters[owner].arlen;
+ assign u_routes[owner].arsize = masters[owner].arsize;
+ assign u_routes[owner].arburst = masters[owner].arburst;
+ assign u_routes[owner].arlock = masters[owner].arlock;
+ assign u_routes[owner].arcache = masters[owner].arcache;
+ assign u_routes[owner].arprot = masters[owner].arprot;
+ assign u_routes[owner].arqos = masters[owner].arqos;
+ assign u_routes[owner].arregion = masters[owner].arregion;
+ assign u_routes[owner].aruser = masters[owner].aruser;
+ assign u_routes[owner].awid = masters[owner].awid;
+ assign u_routes[owner].awaddr = masters[owner].awaddr;
+ assign u_routes[owner].awlen = masters[owner].awlen;
+ assign u_routes[owner].awsize = masters[owner].awsize;
+ assign u_routes[owner].awburst = masters[owner].awburst;
+ assign u_routes[owner].awlock = masters[owner].awlock;
+ assign u_routes[owner].awcache = masters[owner].awcache;
+ assign u_routes[owner].awprot = masters[owner].awprot;
+ assign u_routes[owner].awqos = masters[owner].awqos;
+ assign u_routes[owner].awregion = masters[owner].awregion;
+ assign u_routes[owner].awuser = masters[owner].awuser;
+ assign u_routes[owner].arvalid = s_eligible[2*owner] && s_read_selected;
+ assign u_routes[owner].awvalid = s_eligible[2*owner+1] && s_write_selected;
+ assign u_routes[owner].wdata = masters[owner].wdata;
+ assign u_routes[owner].wstrb = masters[owner].wstrb;
+ assign u_routes[owner].wlast = masters[owner].wlast;
+ assign u_routes[owner].wuser = masters[owner].wuser;
+ assign u_routes[owner].wvalid = masters[owner].wvalid;
+ assign u_routes[owner].rready = masters[owner].rready;
+ assign u_routes[owner].bready = masters[owner].bready;
+ assign s_arready[target_index][owner] = u_routes[owner].arready;
+ assign s_awready[target_index][owner] = u_routes[owner].awready;
+ assign s_wready[target_index][owner] = u_routes[owner].wready;
+ assign s_rvalid[target_index][owner] = u_routes[owner].rvalid;
+ assign s_bvalid[target_index][owner] = u_routes[owner].bvalid;
+ assign s_rdata[target_index][owner] = {
+ u_routes[owner].rid,
+ u_routes[owner].rdata,
+ u_routes[owner].rresp,
+ u_routes[owner].rlast,
+ u_routes[owner].ruser
+ };
+ assign s_bdata[target_index][owner] = {
+ u_routes[owner].bid, u_routes[owner].bresp, u_routes[owner].buser
+ };
+ end
+ tiny_axi4_target u_controller (
+ .clk_i (clk_i),
+ .rst_n_i (rst_n_i),
+ .enable_i (target_rst_n_i[target_index]),
+ .masters (u_routes),
+ .target (targets[target_index]),
+ .idle_o (target_idle_o[target_index]),
+ .fault_valid_o (s_fault_valid[target_index]),
+ .fault_addr_o (s_fault_addr[target_index]),
+ .fault_master_o (s_fault_master[target_index]),
+ .fault_write_o (s_fault_write[target_index]),
+ .fault_instruction_o(s_fault_instruction[target_index]),
+ .fault_resp_o (s_fault_resp[target_index])
+ );
+ end
+`ifdef HAVE_SVA
+ assert property (@(posedge clk_i) disable iff (!rst_n_i) !(s_busy_q[0] && s_busy_q[1]));
+ assert property (@(posedge clk_i) disable iff (!rst_n_i) (s_admit & s_busy_q) == 6'd0);
+ assert property (@(posedge clk_i) disable iff (!rst_n_i) (s_done & ~s_busy_q) == 6'd0);
+ for (genvar owner = 0; owner < 3; owner++) begin : gen_route_assert
+ logic [`RETROSOC_TINY__TARGET_COUNT-1:0] s_reads, s_writes;
+ for (genvar t = 0; t < `RETROSOC_TINY__TARGET_COUNT; t++) begin : gen_column
+ assign s_reads[t] = s_rvalid[t][owner];
+ assign s_writes[t] = s_bvalid[t][owner];
+ end
+ assert property (@(posedge clk_i) disable iff (!rst_n_i) $onehot0(s_reads));
+ assert property (@(posedge clk_i) disable iff (!rst_n_i) $onehot0(s_writes));
+ end
+`endif
endmodule
diff --git a/rtl/tiny/top/tiny_axi4_target.sv b/rtl/tiny/top/tiny_axi4_target.sv
new file mode 100644
index 00000000..d53b1051
--- /dev/null
+++ b/rtl/tiny/top/tiny_axi4_target.sv
@@ -0,0 +1,385 @@
+// Copyright (c) 2026 Yuchi Miao
+// SPDX-License-Identifier: MulanPSL-2.0
+
+// A single target owns one transaction through the upstream terminal handshake.
+// Address capture reserves ownership even while the downstream target is stalled.
+// Per-owner direction preference and owner round robin advance on admission.
+module tiny_axi4_target (
+ input logic clk_i,
+ input logic rst_n_i,
+ input logic enable_i,
+ axi4_if.slave masters [3],
+ axi4_if.master target,
+ output logic idle_o,
+ output logic fault_valid_o,
+ output logic [31:0] fault_addr_o,
+ output logic [ 1:0] fault_master_o,
+ output logic fault_write_o,
+ output logic fault_instruction_o,
+ output logic [ 1:0] fault_resp_o
+);
+ typedef enum logic [2:0] {
+ Idle,
+ Address,
+ Response,
+ AbortWrite,
+ AbortResponse
+ } state_e;
+ typedef struct packed {
+ logic [31:0] addr;
+ logic [7:0] len;
+ logic [2:0] size;
+ logic [1:0] burst;
+ logic lock;
+ logic [3:0] cache;
+ logic [2:0] prot;
+ logic [3:0] qos;
+ logic [3:0] region;
+ logic id;
+ logic user;
+ } addr_t;
+ typedef struct packed {
+ state_e state;
+ logic [1:0] owner;
+ logic write;
+ addr_t addr;
+ logic [7:0] write_beat;
+ logic write_done;
+ logic [8:0] read_count;
+ logic b_captured;
+ logic [31:0] read_addr, error_addr;
+ logic [1:0] error_resp;
+ logic error_seen;
+ } request_t;
+ request_t s_req_d, s_req_q;
+ addr_t s_saved_addr, s_selected_addr;
+ logic [31:0] s_req_addr;
+ logic s_req_id;
+ addr_t s_read_addr[3], s_write_addr[3];
+ logic [2:0] s_arvalid, s_awvalid, s_wvalid, s_wlast, s_bready, s_rready, s_wuser;
+ logic [2:0][31:0] s_wdata;
+ logic [2:0][ 3:0] s_wstrb;
+ logic [2:0] s_owner_req, s_grant, s_prefer_write_d, s_prefer_write_q;
+ logic [1:0] s_selected;
+ logic s_valid, s_selected_write, s_accept, s_terminal;
+ logic s_r_valid, s_r_ready, s_b_valid, s_b_ready;
+ logic [36:0] s_r_data;
+ logic [ 3:0] s_b_data;
+ logic s_read_capture, s_write_capture;
+ logic s_abort_read_valid, s_abort_write_valid;
+ logic s_resp_active, s_rin_valid, s_bin_valid;
+ logic [36:0] s_rin_data;
+ logic [ 3:0] s_bin_data;
+
+ assign s_saved_addr = s_req_q.addr;
+ assign s_req_addr = s_saved_addr.addr;
+ assign s_req_id = s_saved_addr.id;
+
+ assign s_selected_write = s_awvalid[s_selected] &&
+ (s_prefer_write_q[s_selected] || !s_arvalid[s_selected]);
+ assign s_selected_addr = s_selected_write ? s_write_addr[s_selected] : s_read_addr[s_selected];
+ assign s_accept = (s_req_q.state == Idle) && enable_i && s_valid;
+ assign idle_o = (s_req_q.state == Idle) && !s_r_valid && !s_b_valid;
+ // Target reset cancels downstream work, never an already captured upstream
+ // response. Append missing error beats through the same spill registers.
+ assign s_resp_active = (s_req_q.state == Response) || (s_req_q.state == AbortResponse);
+ assign s_abort_read_valid = (s_req_q.state == AbortResponse) && !s_req_q.write &&
+ (s_req_q.read_count <= {1'b0, s_saved_addr.len});
+ assign s_abort_write_valid = (s_req_q.state == AbortResponse) && s_req_q.write &&
+ s_req_q.write_done && !s_req_q.b_captured;
+ assign s_terminal = s_resp_active &&
+ (s_req_q.write ? (s_b_valid && s_bready[s_req_q.owner]) :
+ (s_r_valid && s_rready[s_req_q.owner] && s_r_data[1]));
+ assign fault_valid_o = s_terminal && s_req_q.error_seen;
+ assign fault_addr_o = s_req_q.error_addr;
+ assign fault_master_o = s_req_q.owner;
+ assign fault_write_o = s_req_q.write;
+ assign fault_instruction_o = !s_req_q.write && s_saved_addr.prot[2];
+ assign fault_resp_o = s_req_q.error_resp;
+ assign s_rin_valid = ((s_req_q.state == Response) && enable_i &&
+ !s_req_q.write && target.rvalid) || s_abort_read_valid;
+ assign s_rin_data = s_abort_read_valid ?
+ {s_req_id, 32'd0, 2'b11,
+ (s_req_q.read_count == {1'b0, s_saved_addr.len}), 1'b0} :
+ {target.rid, target.rdata, target.rresp, target.rlast, target.ruser};
+ assign s_bin_valid = ((s_req_q.state == Response) && enable_i &&
+ s_req_q.write && target.bvalid) || s_abort_write_valid;
+ assign s_bin_data = s_abort_write_valid ? {s_req_id, 2'b11, 1'b0} :
+ {target.bid, target.bresp, target.buser};
+ assign s_read_capture = s_rin_valid && s_r_ready;
+ assign s_write_capture = s_bin_valid && s_b_ready;
+
+ round_robin_arbiter #(
+ .CLIENTS(3)
+ ) u_owner_arbiter (
+ .clk_i (clk_i),
+ .rst_n_i (rst_n_i),
+ .advance_i (s_accept),
+ .request_i (s_owner_req),
+ .grant_o (s_grant),
+ .selected_o(s_selected),
+ .valid_o (s_valid)
+ );
+ always_comb begin
+ s_prefer_write_d = s_prefer_write_q;
+ if (s_accept) s_prefer_write_d[s_selected] = !s_selected_write;
+ end
+ dffr #(
+ .DATA_WIDTH(3)
+ ) u_direction_preference (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .dat_i (s_prefer_write_d),
+ .dat_o (s_prefer_write_q)
+ );
+
+ for (genvar owner = 0; owner < 3; owner++) begin : gen_owner
+ assign s_owner_req[owner] = s_arvalid[owner] || s_awvalid[owner];
+ assign s_arvalid[owner] = masters[owner].arvalid;
+ assign s_awvalid[owner] = masters[owner].awvalid;
+ assign s_read_addr[owner] = {
+ masters[owner].araddr,
+ masters[owner].arlen,
+ masters[owner].arsize,
+ masters[owner].arburst,
+ masters[owner].arlock,
+ masters[owner].arcache,
+ masters[owner].arprot,
+ masters[owner].arqos,
+ masters[owner].arregion,
+ masters[owner].arid,
+ masters[owner].aruser
+ };
+ assign s_write_addr[owner] = {
+ masters[owner].awaddr,
+ masters[owner].awlen,
+ masters[owner].awsize,
+ masters[owner].awburst,
+ masters[owner].awlock,
+ masters[owner].awcache,
+ masters[owner].awprot,
+ masters[owner].awqos,
+ masters[owner].awregion,
+ masters[owner].awid,
+ masters[owner].awuser
+ };
+ assign s_wvalid[owner] = masters[owner].wvalid;
+ assign s_wdata[owner] = masters[owner].wdata;
+ assign s_wstrb[owner] = masters[owner].wstrb;
+ assign s_wlast[owner] = masters[owner].wlast;
+ assign s_wuser[owner] = masters[owner].wuser;
+ assign s_rready[owner] = masters[owner].rready;
+ assign s_bready[owner] = masters[owner].bready;
+ assign masters[owner].arready = s_accept && s_grant[owner] && !s_selected_write;
+ assign masters[owner].awready = s_accept && s_grant[owner] && s_selected_write;
+ assign masters[owner].wready = ((s_req_q.state == Response) && enable_i && s_req_q.write &&
+ !s_req_q.write_done && (s_req_q.owner == 2'(owner)) && target.wready) ||
+ ((s_req_q.state == AbortWrite) && !s_req_q.write_done &&
+ (s_req_q.owner == 2'(owner)));
+ assign masters[owner].rvalid = s_resp_active && !s_req_q.write &&
+ (s_req_q.owner == 2'(owner)) && s_r_valid;
+ assign {masters[owner].rid, masters[owner].rdata, masters[owner].rresp,
+ masters[owner].rlast, masters[owner].ruser} = s_r_data;
+ assign masters[owner].bvalid = s_resp_active && s_req_q.write &&
+ (s_req_q.owner == 2'(owner)) && s_b_valid;
+ assign {masters[owner].bid, masters[owner].bresp, masters[owner].buser} = s_b_data;
+ end
+
+ assign target.awaddr = s_saved_addr.addr;
+ assign target.awlen = s_saved_addr.len;
+ assign target.awsize = s_saved_addr.size;
+ assign target.awburst = s_saved_addr.burst;
+ assign target.awlock = s_saved_addr.lock;
+ assign target.awcache = s_saved_addr.cache;
+ assign target.awprot = s_saved_addr.prot;
+ assign target.awqos = s_saved_addr.qos;
+ assign target.awregion = s_saved_addr.region;
+ assign target.awid = s_saved_addr.id;
+ assign target.awuser = s_saved_addr.user;
+ assign target.araddr = s_saved_addr.addr;
+ assign target.arlen = s_saved_addr.len;
+ assign target.arsize = s_saved_addr.size;
+ assign target.arburst = s_saved_addr.burst;
+ assign target.arlock = s_saved_addr.lock;
+ assign target.arcache = s_saved_addr.cache;
+ assign target.arprot = s_saved_addr.prot;
+ assign target.arqos = s_saved_addr.qos;
+ assign target.arregion = s_saved_addr.region;
+ assign target.arid = s_saved_addr.id;
+ assign target.aruser = s_saved_addr.user;
+ assign target.awvalid = (s_req_q.state == Address) && enable_i && s_req_q.write;
+ assign target.arvalid = (s_req_q.state == Address) && enable_i && !s_req_q.write;
+ assign target.wvalid = (s_req_q.state == Response) && enable_i && s_req_q.write &&
+ !s_req_q.write_done && s_wvalid[s_req_q.owner];
+ assign target.wdata = s_wdata[s_req_q.owner];
+ assign target.wstrb = s_wstrb[s_req_q.owner];
+ assign target.wlast = s_wlast[s_req_q.owner];
+ assign target.wuser = s_wuser[s_req_q.owner];
+ assign target.rready = (s_req_q.state == Response) && enable_i && !s_req_q.write && s_r_ready;
+ assign target.bready = (s_req_q.state == Response) && enable_i && s_req_q.write && s_b_ready;
+ spill_register #(
+ .DATA_WIDTH(37)
+ ) u_read_response (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .flush_i(1'b0),
+ .valid_i(s_rin_valid),
+ .ready_o(s_r_ready),
+ .data_i (s_rin_data),
+ .valid_o(s_r_valid),
+ .ready_i(s_resp_active && !s_req_q.write && s_rready[s_req_q.owner]),
+ .data_o (s_r_data)
+ );
+ spill_register #(
+ .DATA_WIDTH(4)
+ ) u_write_response (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .flush_i(1'b0),
+ .valid_i(s_bin_valid),
+ .ready_o(s_b_ready),
+ .data_i (s_bin_data),
+ .valid_o(s_b_valid),
+ .ready_i(s_resp_active && s_req_q.write && s_bready[s_req_q.owner]),
+ .data_o (s_b_data)
+ );
+ always_comb begin
+ s_req_d = s_req_q;
+ unique case (s_req_q.state)
+ Idle:
+ if (s_accept) begin
+ s_req_d = '0;
+ s_req_d.state = Address;
+ s_req_d.owner = s_selected;
+ s_req_d.write = s_selected_write;
+ s_req_d.addr = s_selected_addr;
+ s_req_d.read_addr = s_selected_addr.addr;
+ end
+ Address: begin
+ if (!enable_i) begin
+ s_req_d.state = s_req_q.write ? AbortWrite : AbortResponse;
+ end else if (s_req_q.write ? target.awready : target.arready) s_req_d.state = Response;
+ end
+ Response: begin
+ if (!enable_i) begin
+ s_req_d.state = (s_req_q.write && !s_req_q.write_done) ? AbortWrite : AbortResponse;
+ end
+ if (target.wvalid && target.wready) begin
+ s_req_d.write_beat = s_req_q.write_beat + 8'd1;
+ if (target.wlast || (s_req_q.write_beat == s_saved_addr.len)) s_req_d.write_done = 1'b1;
+ end
+ if (s_terminal) s_req_d.state = Idle;
+ end
+ AbortWrite: begin
+ if (s_wvalid[s_req_q.owner] && !s_req_q.write_done) begin
+ s_req_d.write_beat = s_req_q.write_beat + 8'd1;
+ if (s_wlast[s_req_q.owner] || (s_req_q.write_beat == s_saved_addr.len)) begin
+ s_req_d.write_done = 1'b1;
+ s_req_d.state = AbortResponse;
+ end
+ end
+ end
+ AbortResponse: if (s_terminal) s_req_d.state = Idle;
+ default: s_req_d.state = Idle;
+ endcase
+ if (s_read_capture) begin
+ s_req_d.read_count = s_req_q.read_count + 9'd1;
+ if (s_saved_addr.burst == 2'b01)
+ s_req_d.read_addr = s_req_q.read_addr + (32'd1 << s_saved_addr.size);
+ if (!s_req_q.error_seen && (s_rin_data[3:2] != 2'b00)) begin
+ s_req_d.error_seen = 1'b1;
+ s_req_d.error_addr = s_req_q.read_addr;
+ s_req_d.error_resp = s_rin_data[3:2];
+ end
+ end
+ if (s_write_capture) begin
+ s_req_d.b_captured = 1'b1;
+ if (!s_req_q.error_seen && (s_bin_data[2:1] != 2'b00)) begin
+ s_req_d.error_seen = 1'b1;
+ s_req_d.error_addr = s_req_addr;
+ s_req_d.error_resp = s_bin_data[2:1];
+ end
+ end
+ end
+ dffr #(
+ .DATA_WIDTH($bits(request_t))
+ ) u_request (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .dat_i (s_req_d),
+ .dat_o (s_req_q)
+ );
+
+`ifdef FORMAL
+ logic s_past_valid_q = 1'b0;
+ always_ff @(posedge clk_i) begin
+ s_past_valid_q <= 1'b1;
+ if (rst_n_i) begin
+ assert ($onehot0(s_grant));
+ assert (!s_accept || idle_o);
+ assert (!fault_valid_o || s_terminal);
+ if (s_req_q.state != Idle) assert (s_req_q.owner < 2'd3);
+ end
+ if (s_past_valid_q && rst_n_i && $past(rst_n_i && !s_terminal && (s_req_q.state != Idle))) begin
+ assert (s_req_q.owner == $past(s_req_q.owner));
+ assert (s_req_q.write == $past(s_req_q.write));
+ assert (s_req_q.addr == $past(s_req_q.addr));
+ end
+ end
+ for (genvar owner = 0; owner < 3; owner++) begin : gen_fairness
+ logic [1:0] s_bypass_q;
+ always_ff @(posedge clk_i) begin
+ if (!rst_n_i || !s_owner_req[owner] || (s_accept && s_grant[owner])) s_bypass_q <= '0;
+ else if (s_accept) s_bypass_q <= s_bypass_q + 2'd1;
+ if (rst_n_i) begin
+ assert (s_bypass_q <= 2'd2);
+ assert (!(s_bypass_q == 2'd2 && s_owner_req[owner] && s_accept && !s_grant[owner]));
+ end
+ if (s_past_valid_q && rst_n_i && $past(rst_n_i)) begin
+ if ($past(masters[owner].rvalid && !masters[owner].rready)) begin
+ assert (masters[owner].rvalid);
+ assert ({masters[owner].rid, masters[owner].rdata, masters[owner].rresp,
+ masters[owner].rlast} == $past(
+ {masters[owner].rid, masters[owner].rdata, masters[owner].rresp, masters[owner].rlast}
+ ));
+ end
+ if ($past(masters[owner].bvalid && !masters[owner].bready)) begin
+ assert (masters[owner].bvalid);
+ assert ({masters[owner].bid, masters[owner].bresp} == $past(
+ {masters[owner].bid, masters[owner].bresp}
+ ));
+ end
+ end
+ end
+ end
+`endif
+`ifdef HAVE_SVA
+ assert property (@(posedge clk_i) disable iff (!rst_n_i || !enable_i)
+ target.wvalid && !target.wready |=> target.wvalid &&
+ $stable(
+ {target.wdata, target.wstrb, target.wlast, target.wuser}
+ ));
+ assert property (@(posedge clk_i) disable iff (!rst_n_i || !enable_i)
+ target.arvalid && !target.arready |=> target.arvalid &&
+ $stable(
+ {target.araddr, target.arlen, target.arid, target.arsize, target.arburst}
+ ));
+ assert property (@(posedge clk_i) disable iff (!rst_n_i || !enable_i)
+ target.awvalid && !target.awready |=> target.awvalid &&
+ $stable(
+ {target.awaddr, target.awlen, target.awid, target.awsize, target.awburst}
+ ));
+ for (genvar owner = 0; owner < 3; owner++) begin : gen_response_assert
+ assert property (@(posedge clk_i) disable iff (!rst_n_i)
+ masters[owner].rvalid && !masters[owner].rready |=> masters[owner].rvalid &&
+ $stable(
+ {masters[owner].rid, masters[owner].rdata, masters[owner].rresp, masters[owner].rlast}
+ ));
+ assert property (@(posedge clk_i) disable iff (!rst_n_i)
+ masters[owner].bvalid && !masters[owner].bready |=> masters[owner].bvalid &&
+ $stable(
+ {masters[owner].bid, masters[owner].bresp}
+ ));
+ end
+`endif
+endmodule
diff --git a/rtl/tiny/top/tiny_clock_gate.sv b/rtl/tiny/top/tiny_clock_gate.sv
new file mode 100644
index 00000000..26b8eae0
--- /dev/null
+++ b/rtl/tiny/top/tiny_clock_gate.sv
@@ -0,0 +1,21 @@
+// Copyright (c) 2026 Yuchi Miao
+// SPDX-License-Identifier: MulanPSL-2.0
+module tiny_clock_gate (
+ input logic clk_i,
+ input logic rst_n_i,
+ input logic gate_i,
+ output logic clk_o,
+ output logic gated_o
+);
+ logic s_gate_q;
+ ndffr #(
+ .DATA_WIDTH(1)
+ ) u_gate (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .dat_i (gate_i),
+ .dat_o (s_gate_q)
+ );
+ assign clk_o = clk_i && !s_gate_q;
+ assign gated_o = s_gate_q;
+endmodule
diff --git a/rtl/tiny/top/tiny_clock_output.sv b/rtl/tiny/top/tiny_clock_output.sv
new file mode 100644
index 00000000..c07321eb
--- /dev/null
+++ b/rtl/tiny/top/tiny_clock_output.sv
@@ -0,0 +1,66 @@
+// Copyright (c) 2026 Yuchi Miao
+// SPDX-License-Identifier: MulanPSL-2.0
+
+// SYSCTRL validates the source/divisor and requires disable-before-change.
+// Synchronize enable to the selected source, configuring the divider while
+// disabled. Both package routes consume this one output and enable.
+module tiny_clock_output (
+ input logic clk_ref_i,
+ input logic clk_sys_i,
+ input logic clk_mem_i,
+ input logic clk_periph_i,
+ input logic rst_n_i,
+ input logic [31:0] control_i,
+ output logic clk_o,
+ output logic enable_o
+);
+ logic s_src_clk, s_enabled, s_en_req;
+ logic [19:0] s_cfg_d, s_cfg_q;
+ // Retain the source/divisor through disable acknowledgement. Source zero
+ // only disables the output; it does not change a still-running clock.
+ assign s_cfg_d = control_i[2:0] != 0 ?
+ {1'b1, control_i[23:8], control_i[2:0]} : {1'b0, s_cfg_q[18:0]};
+ dffrc #(
+ .DATA_WIDTH(20),
+ .RESET_VAL ({1'b0, 16'd1, 3'd1})
+ ) u_config (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_n_i),
+ .dat_i (s_cfg_d),
+ .dat_o (s_cfg_q)
+ );
+ assign s_src_clk = s_cfg_q[2:0] == 3 ? clk_mem_i :
+ s_cfg_q[2:0] == 4 ? clk_periph_i : s_cfg_q[2:0] == 2 ? clk_sys_i : clk_ref_i;
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(1)
+ ) u_enable_sync (
+ .clk_i (s_src_clk),
+ .rst_n_i(rst_n_i),
+ .dat_i (s_cfg_q[19]),
+ .dat_o (s_en_req)
+ );
+ // Finish the current output high pulse before disabling the Common divider.
+ ndffr #(
+ .DATA_WIDTH(1)
+ ) u_enable (
+ .clk_i (s_src_clk),
+ .rst_n_i(rst_n_i),
+ .dat_i (s_en_req || clk_o),
+ .dat_o (s_enabled)
+ );
+ clock_divider #(
+ .DIV_WIDTH(16),
+ .RESET_DIV(16'd1)
+ ) u_divider (
+ .clk_i (s_src_clk),
+ .rst_n_i (rst_n_i),
+ .enable_i (s_enabled),
+ .div_i (s_cfg_q[18:3]),
+ .div_valid_i(!s_enabled),
+ .div_ready_o(),
+ .clk_o (clk_o),
+ .count_o ()
+ );
+ assign enable_o = s_enabled;
+endmodule
diff --git a/rtl/tiny/top/tiny_clock_reset_subsystem.sv b/rtl/tiny/top/tiny_clock_reset_subsystem.sv
new file mode 100644
index 00000000..291ec047
--- /dev/null
+++ b/rtl/tiny/top/tiny_clock_reset_subsystem.sv
@@ -0,0 +1,407 @@
+// Copyright (c) 2026 Yuchi Miao
+// SPDX-License-Identifier: MulanPSL-2.0
+
+// Control runs on normalized REF24. All live changes drain first, park SYS,
+// prepare conservative divisors, qualify the requested source, and commit last.
+// A lost PLL resets the data session before resetting the safe mux. REF/XIN
+// loss cannot be recovered by logic which depends on that same input.
+module tiny_clock_reset_subsystem #(
+ parameter int unsigned RefClkHz = 24_000_000,
+ parameter bit HavePll = 1'b0,
+ parameter int unsigned SwitchCycles = 128
+) (
+ input logic ref_clk_i,
+ input logic rst_n_i,
+ input logic [ 1:0] profile_i,
+ input logic apply_i,
+ input logic idle_i,
+ input logic clients_idle_i,
+ input logic warm_reset_i,
+ input logic system_request_i,
+ input logic command_ack_i,
+ input logic [31:0] timeout_i,
+ output logic [ 1:0] current_profile_o,
+ output logic busy_o,
+ output logic fault_o,
+ output logic [ 4:0] fault_code_o,
+ output logic done_o,
+ output logic block_o,
+ output logic ref_clk_o,
+ output logic ref_rst_n_o,
+ output logic system_reset_n_o,
+ output logic pll_locked_o,
+ output logic pll_selected_o,
+ output logic sys_clk_o,
+ output logic mem_clk_o,
+ output logic pclk_o,
+ output logic sys_rst_n_o,
+ output logic mem_rst_n_o,
+ output logic pclk_rst_n_o,
+ output logic [31:0] xin_hz_o,
+ output logic [31:0] ref_hz_o,
+ output logic [31:0] sys_hz_o,
+ output logic [31:0] mem_hz_o,
+ output logic [31:0] pclk_hz_o
+);
+ typedef enum logic [3:0] {
+ Idle,
+ Drain,
+ Park,
+ Program,
+ Unlock,
+ Lock,
+ Divide,
+ Switch,
+ Commit,
+ Recover,
+ WarmDrain
+ } state_e;
+ typedef struct packed {
+ state_e state;
+ logic [1:0] requested;
+ logic [1:0] current;
+ logic fast;
+ logic mem_div;
+ logic [1:0] pclk_div;
+ logic [2:0] pll_sel;
+ logic pll_apply;
+ logic reset_n;
+ logic done;
+ logic [4:0] fault;
+ logic [31:0] count;
+ } control_t;
+ localparam control_t ResetValue = {
+ Idle, 2'd0, 2'd0, 1'b0, 1'b0, 2'd0, 3'd5, 1'b0, 1'b1, 1'b0, 5'd0, 32'd0
+ };
+ control_t s_ctrl_d, s_ctrl_q;
+ logic s_pll_capable, s_pll_lock, s_pll_clk, s_fast_clk;
+ logic s_div2_clk, s_div4_clk, s_pclk_mid;
+ logic s_mux_rst_n;
+ logic [31:0] s_limit, s_req_hz;
+ logic s_idle_ref, s_clients_idle_ref;
+ logic s_cmd_ack_ref;
+ logic s_fault_sticky_d, s_fault_sticky_q;
+
+ if (RefClkHz == 24_000_000) begin : gen_ref_direct
+ assign ref_clk_o = ref_clk_i;
+ end else begin : gen_ref_divided
+ (* keep_hierarchy = 1 *)
+ clock_divider #(
+ .DIV_WIDTH(2),
+ .RESET_DIV(2'(RefClkHz / 48_000_000))
+ ) u_ref_div (
+ .clk_i (ref_clk_i),
+ .rst_n_i (rst_n_i),
+ .enable_i (1'b1),
+ .div_i (2'(RefClkHz / 48_000_000)),
+ .div_valid_i(1'b0),
+ .div_ready_o(),
+ .clk_o (ref_clk_o),
+ .count_o ()
+ );
+ end
+ rst_sync #(
+ .STAGE(5)
+ ) u_ref_rst (
+ .clk_i (ref_clk_o),
+ .rst_n_i(rst_n_i),
+ .rst_n_o(ref_rst_n_o)
+ );
+`ifdef HAVE_PLL
+ tc_pll #(
+ .UseIcs55EcosPll(1'b1),
+ .Ics55Parked (1'b0)
+ ) u_pll (
+ .fref_i (ref_clk_o),
+ .rst_n_i (ref_rst_n_o),
+ .cfg_sel_i (s_ctrl_q.pll_sel),
+ .cfg_apply_i (s_ctrl_q.pll_apply),
+ .pll_capable_o(s_pll_capable),
+ .pll_lock_o (s_pll_lock),
+ .pll_clk_o (s_pll_clk)
+ );
+`else
+ assign s_pll_capable = 1'b0;
+ assign s_pll_lock = 1'b0;
+ assign s_pll_clk = 1'b0;
+`endif
+ assign s_fast_clk = HavePll ? s_pll_clk : ref_clk_i;
+ assign s_mux_rst_n = ref_rst_n_o && s_ctrl_q.reset_n;
+ (* keep_hierarchy = 1 *)
+ safe_clock_mux u_sys_mux (
+ .clk0_i (ref_clk_o),
+ .clk1_i (s_fast_clk),
+ .rst_n_i (s_mux_rst_n),
+ .select_i(s_ctrl_q.fast),
+ .clk_o (sys_clk_o)
+ );
+ // Both divided sources remain live across handover, including returning to
+ // SAFE24. Disabling a selected source would deadlock Common safe_clock_mux.
+ clock_divider #(
+ .DIV_WIDTH(1),
+ .RESET_DIV(1'b1)
+ ) u_div2 (
+ .clk_i (sys_clk_o),
+ .rst_n_i (sys_rst_n_o),
+ .enable_i (1'b1),
+ .div_i (1'b1),
+ .div_valid_i(1'b0),
+ .div_ready_o(),
+ .count_o (),
+ .clk_o (s_div2_clk)
+ );
+ clock_divider #(
+ .DIV_WIDTH(2),
+ .RESET_DIV(2'd2)
+ ) u_div4 (
+ .clk_i (sys_clk_o),
+ .rst_n_i (sys_rst_n_o),
+ .enable_i (1'b1),
+ .div_i (2'd2),
+ .div_valid_i(1'b0),
+ .div_ready_o(),
+ .count_o (),
+ .clk_o (s_div4_clk)
+ );
+ (* keep_hierarchy = 1 *)
+ safe_clock_mux u_mem_mux (
+ .clk0_i (sys_clk_o),
+ .clk1_i (s_div2_clk),
+ .rst_n_i (s_mux_rst_n),
+ .select_i(s_ctrl_q.mem_div),
+ .clk_o (mem_clk_o)
+ );
+ (* keep_hierarchy = 1 *)
+ safe_clock_mux u_pclk_div2_mux (
+ .clk0_i (sys_clk_o),
+ .clk1_i (s_div2_clk),
+ .rst_n_i (s_mux_rst_n),
+ .select_i(s_ctrl_q.pclk_div != 0),
+ .clk_o (s_pclk_mid)
+ );
+ (* keep_hierarchy = 1 *)
+ safe_clock_mux u_pclk_mux (
+ .clk0_i (s_pclk_mid),
+ .clk1_i (s_div4_clk),
+ .rst_n_i (s_mux_rst_n),
+ .select_i(s_ctrl_q.pclk_div == 2),
+ .clk_o (pclk_o)
+ );
+ assign system_reset_n_o = rst_n_i && s_ctrl_q.reset_n;
+ rst_sync #(
+ .STAGE(5)
+ ) u_sys_rst (
+ .clk_i (sys_clk_o),
+ .rst_n_i(system_reset_n_o),
+ .rst_n_o(sys_rst_n_o)
+ );
+ rst_sync #(
+ .STAGE(5)
+ ) u_mem_rst (
+ .clk_i (mem_clk_o),
+ .rst_n_i(system_reset_n_o),
+ .rst_n_o(mem_rst_n_o)
+ );
+ rst_sync #(
+ .STAGE(5)
+ ) u_pclk_rst (
+ .clk_i (pclk_o),
+ .rst_n_i(system_reset_n_o),
+ .rst_n_o(pclk_rst_n_o)
+ );
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(1)
+ ) u_idle_sync (
+ .clk_i (ref_clk_o),
+ .rst_n_i(ref_rst_n_o),
+ .dat_i (idle_i),
+ .dat_o (s_idle_ref)
+ );
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(1)
+ ) u_client_sync (
+ .clk_i (ref_clk_o),
+ .rst_n_i(ref_rst_n_o),
+ .dat_i (clients_idle_i),
+ .dat_o (s_clients_idle_ref)
+ );
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(1)
+ ) u_command_ack_sync (
+ .clk_i (ref_clk_o),
+ .rst_n_i(ref_rst_n_o),
+ .dat_i (command_ack_i),
+ .dat_o (s_cmd_ack_ref)
+ );
+ assign s_limit = timeout_i == 0 ? 32'd1 : timeout_i;
+ assign s_req_hz = s_ctrl_q.requested == 2 ? 32'd192_000_000 :
+ s_ctrl_q.requested == 3 ? 32'd240_000_000 :
+ s_ctrl_q.requested == 1 ? 32'(RefClkHz) : 32'd24_000_000;
+ always_comb begin
+ s_ctrl_d = s_ctrl_q;
+ s_ctrl_d.done = 1'b0;
+ s_ctrl_d.pll_apply = 1'b0;
+ s_ctrl_d.fault = 5'd0;
+ s_ctrl_d.count = s_ctrl_q.count + 32'd1;
+ unique case (s_ctrl_q.state)
+ Idle: begin
+ s_ctrl_d.count = 0;
+ if (system_request_i) s_ctrl_d.state = WarmDrain;
+ else if (apply_i) begin
+ s_ctrl_d.requested = profile_i;
+ if ((HavePll && profile_i == 1) || (profile_i >= 2 && (!HavePll || !s_pll_capable))) begin
+ s_ctrl_d.done = 1'b1;
+ s_ctrl_d.fault[0] = 1'b1;
+ end else s_ctrl_d.state = Drain;
+ end
+ end
+ WarmDrain: begin
+ if (s_ctrl_q.count >= 16 && s_cmd_ack_ref) begin
+ s_ctrl_d.reset_n = 1'b0;
+ s_ctrl_d.fast = 1'b0;
+ s_ctrl_d.count = 0;
+ s_ctrl_d.state = Recover;
+ end else if (s_ctrl_q.count >= s_limit) begin
+ s_ctrl_d.fault[1] = 1'b1;
+ s_ctrl_d.done = 1'b1;
+ s_ctrl_d.state = Idle;
+ end
+ end
+ Drain: begin
+ // Delay covers block propagation and the accepted command's response.
+ if (s_limit >= SwitchCycles && s_ctrl_q.count >= 16 &&
+ s_idle_ref && s_clients_idle_ref) begin
+ s_ctrl_d.fast = 1'b0;
+ s_ctrl_d.count = 0;
+ s_ctrl_d.state = Park;
+ end else if (s_ctrl_q.count >= s_limit) begin
+ s_ctrl_d.fault[1] = 1'b1;
+ s_ctrl_d.done = 1'b1;
+ s_ctrl_d.state = Idle;
+ end
+ end
+ Park:
+ if (s_ctrl_q.count >= SwitchCycles) begin
+ s_ctrl_d.count = 0;
+ s_ctrl_d.state = s_ctrl_q.requested >= 2 ? Program : Divide;
+ end
+ Program: begin
+ s_ctrl_d.pll_sel = s_ctrl_q.requested == 2 ? 3'd5 : 3'd7;
+ s_ctrl_d.pll_apply = 1'b1;
+ s_ctrl_d.count = 0;
+ s_ctrl_d.state = Unlock;
+ end
+ Unlock:
+ if (!s_pll_lock) begin
+ s_ctrl_d.count = 0;
+ s_ctrl_d.state = Lock;
+ end else if (s_ctrl_q.count >= s_limit) begin
+ s_ctrl_d.requested = 0;
+ s_ctrl_d.fault[2] = 1'b1;
+ s_ctrl_d.count = 0;
+ s_ctrl_d.state = Divide;
+ end
+ Lock:
+ if (s_pll_lock) begin
+ s_ctrl_d.count = 0;
+ s_ctrl_d.state = Divide;
+ end else if (s_ctrl_q.count >= s_limit) begin
+ s_ctrl_d.requested = 0;
+ s_ctrl_d.fault[2] = 1'b1;
+ s_ctrl_d.count = 0;
+ s_ctrl_d.state = Divide;
+ end
+ Divide: begin
+ s_ctrl_d.mem_div = s_req_hz > 120_000_000;
+ s_ctrl_d.pclk_div = s_req_hz > 120_000_000 ? 2'd2 : s_req_hz > 60_000_000 ? 2'd1 : 2'd0;
+ if (s_ctrl_q.count >= SwitchCycles) begin
+ s_ctrl_d.fast = s_ctrl_q.requested != 0;
+ s_ctrl_d.count = 0;
+ s_ctrl_d.state = Switch;
+ end
+ end
+ Switch:
+ if (s_ctrl_q.count >= SwitchCycles) begin
+ s_ctrl_d.current = s_ctrl_q.requested;
+ s_ctrl_d.state = Commit;
+ s_ctrl_d.count = 0;
+ end
+ Commit:
+ if (s_ctrl_q.count >= SwitchCycles) begin
+ // Keep admissions blocked while domain-local rate snapshots cross.
+ s_ctrl_d.done = 1'b1;
+ s_ctrl_d.state = Idle;
+ end
+ Recover: begin
+ s_ctrl_d.fast = 1'b0;
+ s_ctrl_d.mem_div = 1'b0;
+ s_ctrl_d.pclk_div = 0;
+ s_ctrl_d.current = 0;
+ if (s_ctrl_q.count >= SwitchCycles && !warm_reset_i) begin
+ s_ctrl_d.reset_n = 1'b1;
+ s_ctrl_d.state = Idle;
+ end
+ end
+ default: begin
+ s_ctrl_d = ResetValue;
+ s_ctrl_d.reset_n = 1'b0;
+ s_ctrl_d.state = Recover;
+ s_ctrl_d.fault[4] = 1'b1;
+ end
+ endcase
+ if (s_ctrl_q.state != Recover && (warm_reset_i ||
+ (HavePll && s_ctrl_q.fast && !s_pll_lock))) begin
+ // Only the session-reset path may break a stopped-clock mux deadlock.
+ s_ctrl_d.reset_n = 1'b0;
+ s_ctrl_d.fast = 1'b0;
+ s_ctrl_d.count = 0;
+ s_ctrl_d.state = Recover;
+ if (!warm_reset_i) s_ctrl_d.fault[3] = 1'b1;
+ end
+ if ((apply_i || system_request_i) && s_ctrl_q.state != Idle) s_ctrl_d.fault[0] = 1'b1;
+ end
+ dffrc #(
+ .DATA_WIDTH($bits(control_t)),
+ .RESET_VAL (ResetValue)
+ ) u_control (
+ .clk_i (ref_clk_o),
+ .rst_n_i(ref_rst_n_o),
+ .dat_i (s_ctrl_d),
+ .dat_o (s_ctrl_q)
+ );
+ assign s_fault_sticky_d = s_fault_sticky_q || (s_ctrl_q.fault != 0);
+ dffr #(
+ .DATA_WIDTH(1)
+ ) u_fault_sticky (
+ .clk_i (ref_clk_o),
+ .rst_n_i(ref_rst_n_o),
+ .dat_i (s_fault_sticky_d),
+ .dat_o (s_fault_sticky_q)
+ );
+ assign current_profile_o = s_ctrl_q.current;
+ assign busy_o = s_ctrl_q.state != Idle;
+ assign block_o = s_ctrl_q.state != Idle;
+ assign fault_o = s_fault_sticky_q;
+ assign fault_code_o = s_ctrl_q.fault;
+ assign done_o = s_ctrl_q.done;
+ assign pll_locked_o = s_pll_lock;
+ assign pll_selected_o = s_ctrl_q.fast && HavePll;
+ assign xin_hz_o = RefClkHz;
+ assign ref_hz_o = 24_000_000;
+ assign sys_hz_o = s_ctrl_q.current == 2 ? 32'd192_000_000 :
+ s_ctrl_q.current == 3 ? 32'd240_000_000 :
+ s_ctrl_q.current == 1 ? 32'(RefClkHz) : 32'd24_000_000;
+ assign mem_hz_o = sys_hz_o > 120_000_000 ? sys_hz_o / 2 : sys_hz_o;
+ assign pclk_hz_o = sys_hz_o > 120_000_000 ? sys_hz_o / 4 :
+ sys_hz_o > 60_000_000 ? sys_hz_o / 2 : sys_hz_o;
+`ifndef SYNTHESIS
+ initial begin
+ if (!(RefClkHz == 24_000_000 || RefClkHz == 48_000_000 || RefClkHz == 96_000_000) ||
+ (HavePll && RefClkHz != 24_000_000) || SwitchCycles < 128)
+ $fatal(1, "Tiny clock source configuration is invalid");
+ end
+`endif
+endmodule
diff --git a/rtl/tiny/top/tiny_cpu_mem.sv b/rtl/tiny/top/tiny_cpu_mem.sv
new file mode 100644
index 00000000..5a229270
--- /dev/null
+++ b/rtl/tiny/top/tiny_cpu_mem.sv
@@ -0,0 +1,289 @@
+// Copyright (c) 2026 Yuchi Miao
+// SPDX-License-Identifier: MulanPSL-2.0
+`include "mmap_define.svh"
+
+// Independent one-operation I/D frontends. AHB write data belongs to the cycle
+// following address acceptance. Local stores complete only after macro commit.
+// Only non-SRAM work enters the tagged, non-posted slow-path merge.
+module tiny_cpu_mem #(
+ parameter bit ClockPause = 1'b0
+) (
+ input logic clk_i,
+ input logic rst_n_i,
+ input logic quiesce_i,
+ input logic clock_pause_i,
+ output logic clock_idle_o,
+ output logic idle_o,
+ output logic [1:0] local_fault_valid_o,
+ output logic [1:0][31:0] local_fault_addr_o,
+ output logic [1:0] local_fault_write_o,
+ ahbl_if.slave cpu [2],
+ tiny_sram_port_if.master local_ports [2],
+ axi4_if.master slow_axi4
+);
+ typedef enum logic [2:0] {
+ Idle,
+ WriteCapture,
+ LocalIssue,
+ LocalWait,
+ SlowIssue,
+ SlowWait,
+ ErrorFirst,
+ ErrorLast
+ } state_e;
+ typedef struct packed {
+ state_e state;
+ logic [31:0] addr, data;
+ logic [2:0] size;
+ logic write;
+ logic external_error;
+ logic [3:0] strb;
+ } request_t;
+ request_t s_req_d[2], s_req_q[2];
+ logic [1:0] s_terminal, s_err, s_slow_req, s_slow_grant, s_front_idle;
+ logic s_slow_selected, s_slow_valid, s_slow_accept;
+ typedef enum logic [1:0] {
+ SlowIdle,
+ SlowAddress,
+ SlowData,
+ SlowResponse
+ } slow_state_e;
+ typedef struct packed {
+ slow_state_e state;
+ logic owner, write, error;
+ logic [2:0] size;
+ logic [31:0] addr, data, rdata;
+ } slow_t;
+ slow_t s_slow_d, s_slow_q;
+ ahbl_if u_slow_ahbl_if (
+ .hclk (clk_i),
+ .hresetn(rst_n_i)
+ );
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) u_slow_axi4_if (
+ .aclk (clk_i),
+ .aresetn(rst_n_i)
+ );
+ logic s_adapter_idle;
+ logic s_clock_pause;
+ logic [1:0] s_clock_front_idle;
+ assign s_clock_pause = ClockPause && clock_pause_i;
+ // A pipeline-overlap address acknowledged with the preceding AHB response
+ // remains in the input register until resume. Only target admissions are
+ // blocked: every accepted bank/AXI operation and response still drains.
+ assign clock_idle_o = (&s_clock_front_idle) && (s_slow_q.state == SlowIdle) && s_adapter_idle;
+
+ for (genvar port_index = 0; port_index < 2; port_index++) begin : gen_port
+ assign local_fault_valid_o[port_index] =
+ (s_req_q[port_index].state == ErrorLast) && !s_req_q[port_index].external_error;
+ assign local_fault_addr_o[port_index] = s_req_q[port_index].addr;
+ assign local_fault_write_o[port_index] = s_req_q[port_index].write;
+ assign s_slow_req[port_index] = (s_req_q[port_index].state == SlowIssue) && !s_clock_pause;
+ assign s_clock_front_idle[port_index] = (s_req_q[port_index].state == Idle) ||
+ (s_req_q[port_index].state == LocalIssue) || (s_req_q[port_index].state == SlowIssue);
+ assign s_front_idle[port_index] = (s_req_q[port_index].state == Idle) &&
+ (quiesce_i || !cpu[port_index].htrans[1]);
+ assign s_err[port_index] = (s_req_q[port_index].state == ErrorFirst) ||
+ (s_req_q[port_index].state == ErrorLast) ||
+ ((s_req_q[port_index].state == SlowWait) &&
+ (s_slow_q.state == SlowResponse) && (s_slow_q.owner == 1'(port_index)) && s_slow_q.error);
+ assign s_terminal[port_index] = (s_req_q[port_index].state == ErrorLast) ||
+ ((s_req_q[port_index].state == LocalWait) && local_ports[port_index].rsp_valid) ||
+ ((s_req_q[port_index].state == SlowWait) &&
+ (s_slow_q.state == SlowResponse) && (s_slow_q.owner == 1'(port_index)) && !s_slow_q.error);
+ assign cpu[port_index].hready =
+ ((s_req_q[port_index].state == Idle) && !quiesce_i) || s_terminal[port_index];
+ assign cpu[port_index].hresp = s_err[port_index];
+ assign cpu[port_index].hrdata = (s_req_q[port_index].state == LocalWait) ?
+ local_ports[port_index].rdata : s_slow_q.rdata;
+ assign local_ports[port_index].req_valid = (s_req_q[port_index].state == LocalIssue) &&
+ !s_clock_pause;
+ assign local_ports[port_index].addr = s_req_q[port_index].addr;
+ assign local_ports[port_index].write = s_req_q[port_index].write;
+ assign local_ports[port_index].wdata = s_req_q[port_index].data;
+ assign local_ports[port_index].wstrb = s_req_q[port_index].strb;
+ assign local_ports[port_index].rsp_ready = s_req_q[port_index].state == LocalWait;
+
+ always_comb begin
+ s_req_d[port_index] = s_req_q[port_index];
+ unique case (s_req_q[port_index].state)
+ WriteCapture: begin
+ unique case (s_req_q[port_index].size)
+ 3'd0: begin
+ s_req_d[port_index].data = {24'd0, cpu[port_index].hwdata[7:0]} <<
+ (8 * s_req_q[port_index].addr[1:0]);
+ s_req_d[port_index].strb = 4'b0001 << s_req_q[port_index].addr[1:0];
+ end
+ 3'd1: begin
+ s_req_d[port_index].data = {16'd0, cpu[port_index].hwdata[15:0]} <<
+ (8 * s_req_q[port_index].addr[1:0]);
+ s_req_d[port_index].strb = 4'b0011 << s_req_q[port_index].addr[1:0];
+ end
+ default: begin
+ s_req_d[port_index].data = cpu[port_index].hwdata;
+ s_req_d[port_index].strb = 4'b1111;
+ end
+ endcase
+ s_req_d[port_index].state =
+ `SOC_ADDR_IS_SRAM(s_req_q[port_index].addr)
+ ? LocalIssue : SlowIssue;
+ end
+ LocalIssue:
+ if (local_ports[port_index].req_valid && local_ports[port_index].req_ready)
+ s_req_d[port_index].state = LocalWait;
+ SlowIssue:
+ if (s_slow_accept && s_slow_grant[port_index]) s_req_d[port_index].state = SlowWait;
+ SlowWait:
+ if (s_err[port_index]) begin
+ s_req_d[port_index].state = ErrorLast;
+ // The fabric already reports externally completed errors once.
+ s_req_d[port_index].external_error = 1'b1;
+ end
+ ErrorFirst: s_req_d[port_index].state = ErrorLast;
+ default: begin
+ end
+ endcase
+ if (cpu[port_index].hready) begin
+ s_req_d[port_index].state = Idle;
+ // Capture even a terminal-cycle overlap during quiesce: that address
+ // was acknowledged by HREADY and cannot be silently discarded.
+ if (cpu[port_index].htrans[1]) begin
+ s_req_d[port_index].addr = cpu[port_index].haddr;
+ s_req_d[port_index].size = cpu[port_index].hsize;
+ s_req_d[port_index].write = cpu[port_index].hwrite;
+ s_req_d[port_index].strb = '0;
+ s_req_d[port_index].external_error = 1'b0;
+ if ((cpu[port_index].htrans != 2'b10) || cpu[port_index].hmastlock ||
+ (cpu[port_index].hsize > 3'd2) ||
+ ((cpu[port_index].haddr & ((32'd1 << cpu[port_index].hsize) - 32'd1)) != 32'd0) ||
+ ((port_index == 0) && (cpu[port_index].hwrite || (cpu[port_index].hsize != 3'd2))))
+ s_req_d[port_index].state = ErrorFirst;
+ else if (cpu[port_index].hwrite) s_req_d[port_index].state = WriteCapture;
+ else
+ s_req_d[port_index].state =
+ `SOC_ADDR_IS_SRAM(cpu[port_index].haddr)
+ ? LocalIssue : SlowIssue;
+ end
+ end
+ end
+ dffr #(
+ .DATA_WIDTH($bits(request_t))
+ ) u_request (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .dat_i (s_req_d[port_index]),
+ .dat_o (s_req_q[port_index])
+ );
+ end
+
+ assign idle_o = (&s_front_idle) && (s_slow_q.state == SlowIdle) && s_adapter_idle;
+ assign s_slow_accept = (s_slow_q.state == SlowIdle) && s_slow_valid;
+ round_robin_arbiter #(
+ .CLIENTS(2)
+ ) u_slow_arbiter (
+ .clk_i (clk_i),
+ .rst_n_i (rst_n_i),
+ .advance_i (s_slow_accept),
+ .request_i (s_slow_req),
+ .grant_o (s_slow_grant),
+ .selected_o(s_slow_selected),
+ .valid_o (s_slow_valid)
+ );
+ always_comb begin
+ s_slow_d = s_slow_q;
+ unique case (s_slow_q.state)
+ SlowIdle:
+ if (s_slow_accept) begin
+ s_slow_d.owner = s_slow_selected;
+ s_slow_d.addr = s_req_q[s_slow_selected].addr;
+ s_slow_d.write = s_req_q[s_slow_selected].write;
+ s_slow_d.size = s_req_q[s_slow_selected].size;
+ // The shared AHB adapter applies lane shifting itself.
+ s_slow_d.data = s_req_q[s_slow_selected].data >> (8 * s_req_q[s_slow_selected].addr[1:0]);
+ s_slow_d.state = SlowAddress;
+ end
+ SlowAddress: if (u_slow_ahbl_if.hready) s_slow_d.state = SlowData;
+ SlowData:
+ if (u_slow_ahbl_if.hready) begin
+ s_slow_d.rdata = u_slow_ahbl_if.hrdata;
+ s_slow_d.error = u_slow_ahbl_if.hresp;
+ s_slow_d.state = SlowResponse;
+ end
+ SlowResponse: s_slow_d.state = SlowIdle;
+ default: s_slow_d.state = SlowIdle;
+ endcase
+ end
+ dffr #(
+ .DATA_WIDTH($bits(slow_t))
+ ) u_slow_state (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .dat_i (s_slow_d),
+ .dat_o (s_slow_q)
+ );
+ assign u_slow_ahbl_if.haddr = s_slow_q.addr;
+ assign u_slow_ahbl_if.hwrite = s_slow_q.write;
+ assign u_slow_ahbl_if.hsize = s_slow_q.size;
+ assign u_slow_ahbl_if.hwdata = s_slow_q.data;
+ assign u_slow_ahbl_if.htrans = (s_slow_q.state == SlowAddress) ? 2'b10 : 2'b00;
+ assign u_slow_ahbl_if.hburst = '0;
+ assign u_slow_ahbl_if.hprot = {3'b001, s_slow_q.owner};
+ assign u_slow_ahbl_if.hmastlock = 1'b0;
+ ahbl2axi4 #(
+ .TwoCycleErrors(1'b1)
+ ) u_slow_adapter (
+ .ahbl (u_slow_ahbl_if),
+ .axi4 (u_slow_axi4_if),
+ .idle_o(s_adapter_idle)
+ );
+ // Forward every channel unchanged except the saved instruction attribute.
+ // Tiny's fabric uses it to reject fetches before peripheral side effects.
+ assign slow_axi4.awid = u_slow_axi4_if.awid;
+ assign slow_axi4.awaddr = u_slow_axi4_if.awaddr;
+ assign slow_axi4.awlen = u_slow_axi4_if.awlen;
+ assign slow_axi4.awsize = u_slow_axi4_if.awsize;
+ assign slow_axi4.awburst = u_slow_axi4_if.awburst;
+ assign slow_axi4.awlock = u_slow_axi4_if.awlock;
+ assign slow_axi4.awcache = u_slow_axi4_if.awcache;
+ assign slow_axi4.awprot = {!s_slow_q.owner, u_slow_axi4_if.awprot[1:0]};
+ assign slow_axi4.awqos = u_slow_axi4_if.awqos;
+ assign slow_axi4.awregion = u_slow_axi4_if.awregion;
+ assign slow_axi4.awuser = u_slow_axi4_if.awuser;
+ assign slow_axi4.awvalid = u_slow_axi4_if.awvalid;
+ assign slow_axi4.arid = u_slow_axi4_if.arid;
+ assign slow_axi4.araddr = u_slow_axi4_if.araddr;
+ assign slow_axi4.arlen = u_slow_axi4_if.arlen;
+ assign slow_axi4.arsize = u_slow_axi4_if.arsize;
+ assign slow_axi4.arburst = u_slow_axi4_if.arburst;
+ assign slow_axi4.arlock = u_slow_axi4_if.arlock;
+ assign slow_axi4.arcache = u_slow_axi4_if.arcache;
+ assign slow_axi4.arprot = {!s_slow_q.owner, u_slow_axi4_if.arprot[1:0]};
+ assign slow_axi4.arqos = u_slow_axi4_if.arqos;
+ assign slow_axi4.arregion = u_slow_axi4_if.arregion;
+ assign slow_axi4.aruser = u_slow_axi4_if.aruser;
+ assign slow_axi4.arvalid = u_slow_axi4_if.arvalid;
+ assign slow_axi4.wdata = u_slow_axi4_if.wdata;
+ assign slow_axi4.wstrb = u_slow_axi4_if.wstrb;
+ assign slow_axi4.wlast = u_slow_axi4_if.wlast;
+ assign slow_axi4.wuser = u_slow_axi4_if.wuser;
+ assign slow_axi4.wvalid = u_slow_axi4_if.wvalid;
+ assign slow_axi4.rready = u_slow_axi4_if.rready;
+ assign slow_axi4.bready = u_slow_axi4_if.bready;
+ assign u_slow_axi4_if.awready = slow_axi4.awready;
+ assign u_slow_axi4_if.arready = slow_axi4.arready;
+ assign u_slow_axi4_if.wready = slow_axi4.wready;
+ assign u_slow_axi4_if.rvalid = slow_axi4.rvalid;
+ assign u_slow_axi4_if.rid = slow_axi4.rid;
+ assign u_slow_axi4_if.rdata = slow_axi4.rdata;
+ assign u_slow_axi4_if.rresp = slow_axi4.rresp;
+ assign u_slow_axi4_if.rlast = slow_axi4.rlast;
+ assign u_slow_axi4_if.ruser = slow_axi4.ruser;
+ assign u_slow_axi4_if.bvalid = slow_axi4.bvalid;
+ assign u_slow_axi4_if.bid = slow_axi4.bid;
+ assign u_slow_axi4_if.bresp = slow_axi4.bresp;
+ assign u_slow_axi4_if.buser = slow_axi4.buser;
+endmodule
diff --git a/rtl/tiny/top/tiny_cpu_wrapper.sv b/rtl/tiny/top/tiny_cpu_wrapper.sv
new file mode 100644
index 00000000..20bbc7f3
--- /dev/null
+++ b/rtl/tiny/top/tiny_cpu_wrapper.sv
@@ -0,0 +1,243 @@
+// Copyright (c) 2023-2026 Yuchi Miao
+// retroSoC is licensed under Mulan PSL v2.
+// You can use this software according to the terms and conditions of the Mulan PSL v2.
+// You may obtain a copy of Mulan PSL v2 at:
+// http://license.coscl.org.cn/MulanPSL2
+// THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
+// EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
+// MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
+// See the Mulan PSL v2 for more details.
+
+`include "mmap_define.svh"
+
+module tiny_cpu_wrapper #(
+ parameter int ExternalIrqCount = 30,
+ parameter bit EnableAtomics = 1'b0,
+ parameter int ResetSyncStages = 3,
+ parameter bit EnableWfiGate = 1'b0,
+ parameter bit PreserveDebug = 1'b0
+) (
+ // verilog_format: off -- preserve reviewed column alignment
+ input logic clk_i,
+ input logic rst_n_i,
+ input logic debug_rst_n_i,
+ output logic hart_reset_o,
+ input logic [ExternalIrqCount+1:0] irq_i,
+ input logic jtag_tck_i,
+ input logic jtag_tms_i,
+ input logic jtag_tdi_i,
+ input logic jtag_trst_n_i,
+ output logic jtag_tdo_o,
+ output logic debug_halted_o,
+ input logic bus_idle_i,
+ input logic wfi_gate_i,
+ output logic gated_o,
+ output logic core_rst_n_o,
+ output logic quiesce_o,
+ ahbl_if.master cpu[2]
+ // verilog_format: on
+);
+ logic s_pwrup_req;
+ logic s_debug_reset_n, s_abort_debug;
+ logic s_cpu_clk, s_cpu_clk_en, s_gate_d, s_gate_q;
+ logic s_reset_pending;
+ logic s_hart_idle;
+ logic s_core_rst_n;
+ logic s_dbg_req_halt;
+ logic s_dbg_req_halt_on_reset;
+ logic s_dbg_req_resume;
+ logic s_dbg_halted;
+ logic s_dbg_running;
+ logic [31:0] s_dbg_data0_rdata;
+ logic [31:0] s_dbg_data0_wdata;
+ logic s_dbg_data0_wen;
+ logic [31:0] s_dbg_instr_data;
+ logic s_dbg_instr_data_vld;
+ logic s_dbg_instr_data_rdy;
+ logic s_dbg_instr_caught_exception;
+ logic s_dbg_instr_caught_ebreak;
+ logic [31:0] s_dbg_sbus_addr;
+ logic s_dbg_sbus_write;
+ logic [ 1:0] s_dbg_sbus_size;
+ logic s_dbg_sbus_vld;
+ logic s_dbg_sbus_rdy;
+ logic s_dbg_sbus_err;
+ logic [31:0] s_dbg_sbus_wdata;
+ logic [31:0] s_dbg_sbus_rdata;
+
+ assign s_debug_reset_n = PreserveDebug ? debug_rst_n_i : rst_n_i;
+ assign s_abort_debug = PreserveDebug && !rst_n_i;
+ assign hart_reset_o = s_reset_pending;
+ assign s_gate_d = EnableWfiGate && wfi_gate_i && !s_cpu_clk_en && bus_idle_i &&
+ !(|irq_i) && !s_dbg_req_halt && !s_dbg_req_resume && !s_reset_pending;
+ (* keep_hierarchy = 1 *)
+ tiny_clock_gate u_cpu_gate (
+ .clk_i (clk_i),
+ .rst_n_i(s_core_rst_n),
+ .gate_i (s_gate_d),
+ .clk_o (s_cpu_clk),
+ .gated_o(s_gate_q)
+ );
+ assign gated_o = s_gate_q;
+
+ assign debug_halted_o = s_dbg_halted;
+ assign core_rst_n_o = s_core_rst_n;
+ // Request a normal debug halt before draining/resetting the core. Never cut
+ // off an accepted AHB transfer to manufacture an idle indication.
+ assign quiesce_o = s_reset_pending && s_dbg_halted;
+ assign s_hart_idle = bus_idle_i && (!s_reset_pending || s_dbg_halted);
+
+ mgmt_debug_wrapper #(
+ .JtagIdcode (`SOC_JTAG_IDCODE),
+ .ResetSyncStages (ResetSyncStages),
+ .SeparateSystemReset(PreserveDebug)
+ ) u_mgmt_debug_wrapper (
+ .clk_i (clk_i),
+ .rst_n_i (s_debug_reset_n),
+ .system_rst_n_i (rst_n_i),
+ .bridge_idle_i (s_hart_idle),
+ .jtag_tck_i (jtag_tck_i),
+ .jtag_tms_i (jtag_tms_i),
+ .jtag_tdi_i (jtag_tdi_i),
+ .jtag_trst_n_i (jtag_trst_n_i),
+ .jtag_tdo_o (jtag_tdo_o),
+ .core_rst_n_o (s_core_rst_n),
+ .reset_pending_o (s_reset_pending),
+ .dbg_req_halt_o (s_dbg_req_halt),
+ .dbg_req_halt_on_reset_o (s_dbg_req_halt_on_reset),
+ .dbg_req_resume_o (s_dbg_req_resume),
+ .dbg_halted_i (s_dbg_halted),
+ .dbg_running_i (s_dbg_running),
+ .dbg_data0_rdata_o (s_dbg_data0_rdata),
+ .dbg_data0_wdata_i (s_dbg_data0_wdata),
+ .dbg_data0_wen_i (s_dbg_data0_wen && !s_abort_debug),
+ .dbg_instr_data_o (s_dbg_instr_data),
+ .dbg_instr_data_vld_o (s_dbg_instr_data_vld),
+ .dbg_instr_data_rdy_i (s_dbg_instr_data_rdy || s_abort_debug),
+ .dbg_instr_caught_exception_i(s_dbg_instr_caught_exception || s_abort_debug),
+ .dbg_instr_caught_ebreak_i (s_dbg_instr_caught_ebreak || s_abort_debug),
+ .dbg_sbus_addr_o (s_dbg_sbus_addr),
+ .dbg_sbus_write_o (s_dbg_sbus_write),
+ .dbg_sbus_size_o (s_dbg_sbus_size),
+ .dbg_sbus_vld_o (s_dbg_sbus_vld),
+ .dbg_sbus_rdy_i (s_dbg_sbus_rdy || s_abort_debug),
+ .dbg_sbus_err_i (s_dbg_sbus_err || s_abort_debug),
+ .dbg_sbus_wdata_o (s_dbg_sbus_wdata),
+ .dbg_sbus_rdata_i (s_dbg_sbus_rdata)
+ );
+
+ hazard3_cpu_2port #(
+ .RESET_VECTOR (`SOC_CPU_RESET_ADDR),
+ .MTVEC_INIT (32'h0000_0000),
+ .EXTENSION_A (EnableAtomics),
+ .EXTENSION_C (1),
+ .EXTENSION_E (0),
+ .EXTENSION_M (1),
+ .EXTENSION_ZBA (1),
+ .EXTENSION_ZBB (1),
+ .EXTENSION_ZBC (1),
+ .EXTENSION_ZBKB (1),
+ .EXTENSION_ZBKX (1),
+ .EXTENSION_ZBS (1),
+ .EXTENSION_ZCB (0),
+ .EXTENSION_ZCLSD (0),
+ .EXTENSION_ZCMP (0),
+ .EXTENSION_ZIFENCEI (1),
+ .EXTENSION_ZILSD (1),
+ .EXTENSION_XH3BEXTM (1),
+ .EXTENSION_XH3IRQ (1),
+ .EXTENSION_XH3PMPM (0),
+ .EXTENSION_XH3POWER (EnableWfiGate),
+ .CSR_M_MANDATORY (1),
+ .CSR_M_TRAP (1),
+ .CSR_COUNTER (1),
+ .U_MODE (0),
+ .PMP_REGIONS (0),
+ .PMP_GRAIN (0),
+ .PMP_MATCH_NAPOT (1),
+ .PMP_MATCH_TOR (0),
+ .PMP_HARDWIRED (0),
+ .PMP_HARDWIRED_ADDR (0),
+ .PMP_HARDWIRED_CFG (0),
+ .DEBUG_SUPPORT (1),
+ .BREAKPOINT_TRIGGERS(2),
+ .NUM_IRQS (ExternalIrqCount),
+ .IRQ_PRIORITY_BITS (2),
+ .IRQ_INPUT_BYPASS ({ExternalIrqCount{1'b0}}),
+ .MVENDORID_VAL (32'h0),
+ .MCONFIGPTR_VAL (32'h0),
+ .REDUCED_BYPASS (0),
+ .MULDIV_UNROLL (2),
+ .MUL_FAST (1),
+ .MUL_FASTER (1),
+ .MULH_FAST (1),
+ .FAST_BRANCHCMP (1),
+ .RESET_REGFILE (1),
+ .BRANCH_PREDICTOR (1),
+ .MTVEC_WMASK (32'hfffffffd)
+ ) u_hazard3_cpu_2port (
+ .clk (s_cpu_clk),
+ .clk_always_on (clk_i),
+ .rst_n (s_core_rst_n),
+ .pwrup_req (s_pwrup_req),
+ .pwrup_ack (s_pwrup_req),
+ .clk_en (s_cpu_clk_en),
+ .unblock_out (),
+ .unblock_in (1'b0),
+ .i_haddr (cpu[0].haddr),
+ .i_hwrite (cpu[0].hwrite),
+ .i_htrans (cpu[0].htrans),
+ .i_hsize (cpu[0].hsize),
+ .i_hburst (cpu[0].hburst),
+ .i_hprot (cpu[0].hprot),
+ .i_hmastlock (cpu[0].hmastlock),
+ .i_hmaster (),
+ .i_hready (cpu[0].hready),
+ .i_hresp (cpu[0].hresp),
+ .i_hwdata (cpu[0].hwdata),
+ .i_hrdata (cpu[0].hrdata),
+ .d_haddr (cpu[1].haddr),
+ .d_hwrite (cpu[1].hwrite),
+ .d_htrans (cpu[1].htrans),
+ .d_hsize (cpu[1].hsize),
+ .d_hburst (cpu[1].hburst),
+ .d_hprot (cpu[1].hprot),
+ .d_hmastlock (cpu[1].hmastlock),
+ .d_hmaster (),
+ .d_hready (cpu[1].hready),
+ .d_hresp (cpu[1].hresp),
+ .d_hwdata (cpu[1].hwdata),
+ .d_hrdata (cpu[1].hrdata),
+ .d_hexcl (),
+ .d_hexokay (1'b1),
+ .fence_i_vld (),
+ .fence_d_vld (),
+ .fence_rdy (1'b1),
+ .dbg_req_halt (s_dbg_req_halt || s_reset_pending),
+ .dbg_req_halt_on_reset (s_dbg_req_halt_on_reset),
+ .dbg_req_resume (s_dbg_req_resume && !s_reset_pending),
+ .dbg_halted (s_dbg_halted),
+ .dbg_running (s_dbg_running),
+ .dbg_data0_rdata (s_dbg_data0_rdata),
+ .dbg_data0_wdata (s_dbg_data0_wdata),
+ .dbg_data0_wen (s_dbg_data0_wen),
+ .dbg_instr_data (s_dbg_instr_data),
+ .dbg_instr_data_vld (s_dbg_instr_data_vld && !s_reset_pending && !s_abort_debug),
+ .dbg_instr_data_rdy (s_dbg_instr_data_rdy),
+ .dbg_instr_caught_exception(s_dbg_instr_caught_exception),
+ .dbg_instr_caught_ebreak (s_dbg_instr_caught_ebreak),
+ .dbg_sbus_addr (s_dbg_sbus_addr),
+ .dbg_sbus_write (s_dbg_sbus_write),
+ .dbg_sbus_size (s_dbg_sbus_size),
+ .dbg_sbus_vld (s_dbg_sbus_vld && !s_abort_debug),
+ .dbg_sbus_rdy (s_dbg_sbus_rdy),
+ .dbg_sbus_err (s_dbg_sbus_err),
+ .dbg_sbus_wdata (s_dbg_sbus_wdata),
+ .dbg_sbus_rdata (s_dbg_sbus_rdata),
+ .mhartid_val ('0),
+ .eco_version ('0),
+ .irq (irq_i[ExternalIrqCount+1:2]),
+ .soft_irq (irq_i[0]),
+ .timer_irq (irq_i[1])
+ );
+endmodule
diff --git a/rtl/tiny/top/tiny_fault_collector.sv b/rtl/tiny/top/tiny_fault_collector.sv
new file mode 100644
index 00000000..927a0b46
--- /dev/null
+++ b/rtl/tiny/top/tiny_fault_collector.sv
@@ -0,0 +1,60 @@
+// Copyright (c) 2026 Yuchi Miao
+// SPDX-License-Identifier: MulanPSL-2.0
+
+// Terminal transaction pulses contribute independently to the count. One
+// deterministic winner supplies metadata; SYSCTRL owns validity and W1C.
+module tiny_fault_collector (
+ input logic [ 1:0] local_valid_i,
+ input logic [ 1:0][31:0] local_addr_i,
+ input logic [ 1:0] local_write_i,
+ input logic [ 5:0] external_valid_i,
+ input logic [ 5:0][31:0] external_addr_i,
+ input logic [ 5:0][ 1:0] external_resp_i,
+ input logic [ 5:0] external_instruction_i,
+ output logic [ 3:0] count_o,
+ output logic [31:0] addr_o,
+ output logic [ 1:0] master_o,
+ output logic write_o,
+ output logic [ 1:0] resp_o
+);
+ logic [7:0] s_valid, s_write, s_instruction;
+ logic [7:0][31:0] s_addr;
+ logic [7:0][1:0] s_master, s_resp;
+ logic [7:0][3:0] s_rank;
+ logic [3:0] s_best_rank;
+ assign s_valid = {external_valid_i, local_valid_i};
+ assign s_addr = {external_addr_i, local_addr_i};
+ assign s_write[1:0] = local_write_i;
+ assign s_master[1:0] = '0;
+ assign s_resp[1:0] = {2'b10, 2'b10};
+ assign s_instruction[1:0] = 2'b01;
+ for (genvar slot = 0; slot < 6; slot++) begin : gen_external
+ assign s_write[slot+2] = 1'(slot % 2);
+ assign s_master[slot+2] = 2'(slot / 2);
+ assign s_resp[slot+2] = external_resp_i[slot];
+ assign s_instruction[slot+2] = (slot < 2) && external_instruction_i[slot];
+ end
+ for (genvar slot = 0; slot < 8; slot++) begin : gen_rank
+ assign s_rank[slot] = {s_master[slot], !s_instruction[slot], s_write[slot]};
+ end
+ always_comb begin
+ count_o = '0;
+ addr_o = '0;
+ master_o = '0;
+ write_o = 1'b0;
+ resp_o = '0;
+ s_best_rank = 4'hf;
+ for (int slot = 0; slot < 8; slot++) begin
+ if (s_valid[slot]) begin
+ count_o = count_o + 4'd1;
+ if (s_rank[slot] < s_best_rank) begin
+ s_best_rank = s_rank[slot];
+ addr_o = s_addr[slot];
+ master_o = s_master[slot];
+ write_o = s_write[slot];
+ resp_o = s_resp[slot];
+ end
+ end
+ end
+ end
+endmodule
diff --git a/rtl/tiny/top/tiny_managed_lifecycle.sv b/rtl/tiny/top/tiny_managed_lifecycle.sv
new file mode 100644
index 00000000..abac6f7f
--- /dev/null
+++ b/rtl/tiny/top/tiny_managed_lifecycle.sv
@@ -0,0 +1,53 @@
+// Copyright (c) 2026 Yuchi Miao
+// SPDX-License-Identifier: MulanPSL-2.0
+
+// Product lifecycle observations for managed register-only endpoints. Track
+// only successful APB writes, respecting byte enables. This conservative
+// mirror never infers idle from quiet output pins or a missing clock. PWM
+// requires explicit global/capture disable; CRC requires FINISH or ABORT.
+module tiny_managed_lifecycle (
+ input logic clk_i,
+ input logic pwm_rst_n_i,
+ input logic crc_rst_n_i,
+ input logic pwm_write_i,
+ input logic [11:0] pwm_addr_i,
+ input logic [31:0] pwm_data_i,
+ input logic crc_write_i,
+ input logic [11:0] crc_addr_i,
+ input logic [31:0] crc_data_i,
+ output logic pwm_idle_o,
+ output logic crc_idle_o
+);
+ logic [1:0] s_pwm_d, s_pwm_q;
+ logic s_crc_d, s_crc_q;
+ always_comb begin
+ s_pwm_d = s_pwm_q;
+ if (pwm_write_i) begin
+ if (pwm_addr_i == 12'h000) s_pwm_d[0] = pwm_data_i[0];
+ if (pwm_addr_i == 12'h400) s_pwm_d[1] = pwm_data_i[0];
+ end
+ s_crc_d = s_crc_q;
+ if (crc_write_i && crc_addr_i == 12'h000) begin
+ if (crc_data_i[2:0] == 3'b001) s_crc_d = 1'b1;
+ if (crc_data_i[2:0] == 3'b010 || crc_data_i[2:0] == 3'b100) s_crc_d = 1'b0;
+ end
+ end
+ dffr #(
+ .DATA_WIDTH(2)
+ ) u_pwm_state (
+ .clk_i (clk_i),
+ .rst_n_i(pwm_rst_n_i),
+ .dat_i (s_pwm_d),
+ .dat_o (s_pwm_q)
+ );
+ dffr #(
+ .DATA_WIDTH(1)
+ ) u_crc_state (
+ .clk_i (clk_i),
+ .rst_n_i(crc_rst_n_i),
+ .dat_i (s_crc_d),
+ .dat_o (s_crc_q)
+ );
+ assign pwm_idle_o = s_pwm_q == 0;
+ assign crc_idle_o = !s_crc_q;
+endmodule
diff --git a/rtl/tiny/top/tiny_peripheral_control.sv b/rtl/tiny/top/tiny_peripheral_control.sv
new file mode 100644
index 00000000..ac28ef87
--- /dev/null
+++ b/rtl/tiny/top/tiny_peripheral_control.sv
@@ -0,0 +1,133 @@
+// Copyright (c) 2026 Yuchi Miao
+// SPDX-License-Identifier: MulanPSL-2.0
+
+// Atomic Tiny gate/reset transactions on REF24. Inputs are synchronized to
+// REF24 by the integration; gate masks exclude non-gateable boot infrastructure.
+module tiny_peripheral_control (
+ input logic clk_i,
+ input logic rst_n_i,
+ input logic warm_reset_i,
+ input logic apply_i,
+ input logic reset_command_i,
+ input logic [13:0] request_i,
+ input logic [13:0] idle_i,
+ input logic bus_idle_i,
+ input logic [13:0] gate_actual_i,
+ input logic [13:0] reset_held_i,
+ input logic [13:0] reset_ready_i,
+ input logic [31:0] timeout_i,
+ output logic [13:0] gate_o,
+ output logic [13:0] reset_o,
+ output logic block_o,
+ output logic done_o,
+ output logic [ 4:0] fault_o,
+ output logic [13:0] reset_done_o
+);
+ typedef enum logic [2:0] {
+ Idle,
+ Drain,
+ GateWait,
+ Hold,
+ Release,
+ Complete
+ } state_e;
+ typedef struct packed {
+ state_e state;
+ logic reset_command;
+ logic [13:0] request_mask, gates, previous_gates, resets, reset_done, affected;
+ logic [31:0] count;
+ logic done;
+ logic [4:0] fault;
+ } control_t;
+ control_t s_ctrl_d, s_ctrl_q;
+ logic [13:0] s_affected;
+ assign s_affected = s_ctrl_q.affected;
+ assign gate_o = s_ctrl_q.gates;
+ assign reset_o = s_ctrl_q.resets;
+ assign reset_done_o = s_ctrl_q.reset_done;
+ // Arming the WFI leaf permission must not pause an unrelated DMA job.
+ // The CPU leaf itself waits for its accepted I/D work before stopping.
+ assign block_o = (s_ctrl_q.state != Idle) && (s_affected[13:1] != 0);
+ assign done_o = s_ctrl_q.done;
+ assign fault_o = s_ctrl_q.fault;
+ always_comb begin
+ s_ctrl_d = s_ctrl_q;
+ s_ctrl_d.done = 1'b0;
+ s_ctrl_d.fault = 0;
+ s_ctrl_d.count = s_ctrl_q.count + 32'd1;
+ unique case (s_ctrl_q.state)
+ Idle: begin
+ s_ctrl_d.count = 0;
+ if (apply_i) begin
+ if (reset_command_i && (request_i == 0 || request_i[0] ||
+ (request_i & s_ctrl_q.gates) != 0)) begin
+ s_ctrl_d.done = 1'b1;
+ s_ctrl_d.fault[0] = 1'b1;
+ end else begin
+ s_ctrl_d.request_mask = request_i;
+ s_ctrl_d.previous_gates = s_ctrl_q.gates;
+ s_ctrl_d.affected = reset_command_i ? request_i : s_ctrl_q.gates ^ request_i;
+ s_ctrl_d.reset_command = reset_command_i;
+ if (reset_command_i) s_ctrl_d.reset_done = 0;
+ s_ctrl_d.state = Drain;
+ end
+ end
+ end
+ Drain: begin
+ if (s_ctrl_q.count >= 16 && ((s_affected[13:1] == 0) || (bus_idle_i && idle_i[9])) &&
+ ((idle_i[13:1] & s_affected[13:1]) == s_affected[13:1])) begin
+ s_ctrl_d.count = 0;
+ if (s_ctrl_q.reset_command) begin
+ s_ctrl_d.resets = s_ctrl_q.request_mask;
+ s_ctrl_d.state = Hold;
+ end else begin
+ s_ctrl_d.gates = s_ctrl_q.request_mask;
+ s_ctrl_d.state = GateWait;
+ end
+ end else if (s_ctrl_q.count >= timeout_i) begin
+ s_ctrl_d.fault[1] = 1'b1;
+ s_ctrl_d.done = 1'b1;
+ s_ctrl_d.state = Idle;
+ end
+ end
+ GateWait: if (gate_actual_i[13:1] == s_ctrl_q.gates[13:1]) s_ctrl_d.state = Complete;
+ Hold:
+ if ((reset_held_i & s_ctrl_q.request_mask) == s_ctrl_q.request_mask) begin
+ s_ctrl_d.resets = 0;
+ s_ctrl_d.count = 0;
+ s_ctrl_d.state = Release;
+ end
+ Release:
+ if ((reset_ready_i & s_ctrl_q.request_mask) == s_ctrl_q.request_mask) begin
+ s_ctrl_d.reset_done = s_ctrl_q.request_mask;
+ s_ctrl_d.state = Complete;
+ end
+ Complete: begin
+ s_ctrl_d.done = 1'b1;
+ s_ctrl_d.state = Idle;
+ end
+ default: begin
+ s_ctrl_d.state = Idle;
+ s_ctrl_d.fault[4] = 1'b1;
+ end
+ endcase
+ if ((s_ctrl_q.state == GateWait || s_ctrl_q.state == Hold || s_ctrl_q.state == Release) &&
+ s_ctrl_q.count >= timeout_i) begin
+ s_ctrl_d.fault[4] = 1'b1;
+ s_ctrl_d.done = 1'b1;
+ s_ctrl_d.state = Idle;
+ // A failed reset retains isolation; it has no RESET_DONE acknowledgement.
+ if (s_ctrl_q.reset_command) s_ctrl_d.resets = s_ctrl_q.request_mask;
+ else s_ctrl_d.gates = s_ctrl_q.previous_gates;
+ end
+ if (warm_reset_i) s_ctrl_d = '0;
+ end
+ dffr #(
+ .DATA_WIDTH($bits(control_t))
+ ) u_control (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .dat_i (s_ctrl_d),
+ .dat_o (s_ctrl_q)
+ );
+endmodule
diff --git a/rtl/tiny/top/tiny_peripheral_leaf.sv b/rtl/tiny/top/tiny_peripheral_leaf.sv
new file mode 100644
index 00000000..ff31010a
--- /dev/null
+++ b/rtl/tiny/top/tiny_peripheral_leaf.sv
@@ -0,0 +1,55 @@
+// Copyright (c) 2026 Yuchi Miao
+// SPDX-License-Identifier: MulanPSL-2.0
+
+// The base clock and lifecycle registers never stop. A gate only stops its
+// peripheral leaf, at a falling edge. Local reset asserts asynchronously;
+// hold/release acknowledgements require five actual leaf clock edges.
+module tiny_peripheral_leaf (
+ input logic clk_i,
+ input logic rst_n_i,
+ input logic gate_i,
+ input logic reset_i,
+ output logic clk_o,
+ output logic rst_n_o,
+ output logic gated_o,
+ output logic held_o
+);
+ logic s_gate_sync, s_gate_q;
+ logic [2:0] s_hold_d, s_hold_q;
+ cdc_sync #(
+ .STAGE (2),
+ .DATA_WIDTH(1)
+ ) u_gate_sync (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .dat_i (gate_i),
+ .dat_o (s_gate_sync)
+ );
+ ndffr #(
+ .DATA_WIDTH(1)
+ ) u_gate (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .dat_i (s_gate_sync),
+ .dat_o (s_gate_q)
+ );
+ assign clk_o = clk_i && !s_gate_q;
+ assign gated_o = s_gate_q;
+ assign s_hold_d = s_hold_q < 5 ? s_hold_q + 3'd1 : s_hold_q;
+ dffr #(
+ .DATA_WIDTH(3)
+ ) u_hold (
+ .clk_i (clk_o),
+ .rst_n_i(rst_n_i && reset_i),
+ .dat_i (s_hold_d),
+ .dat_o (s_hold_q)
+ );
+ assign held_o = s_hold_q == 5;
+ rst_sync #(
+ .STAGE(5)
+ ) u_local_reset (
+ .clk_i (clk_o),
+ .rst_n_i(rst_n_i && !reset_i),
+ .rst_n_o(rst_n_o)
+ );
+endmodule
diff --git a/rtl/tiny/top/tiny_rcu_regs.svh b/rtl/tiny/top/tiny_rcu_regs.svh
new file mode 100644
index 00000000..b71d4806
--- /dev/null
+++ b/rtl/tiny/top/tiny_rcu_regs.svh
@@ -0,0 +1,30 @@
+// Copyright (c) 2026 Yuchi Miao
+// SPDX-License-Identifier: MulanPSL-2.0
+// Handwritten Tiny RCU ABI 1.0; paired with retrosoc/hal/rcu_regs.h.
+`ifndef RETROSOC_TINY_RCU__REGS_SVH
+`define RETROSOC_TINY_RCU__REGS_SVH
+`define APB4_TINY_RCU__ID 32'h100
+`define APB4_TINY_RCU__VERSION 32'h104
+`define APB4_TINY_RCU__CAPABILITY 32'h108
+`define APB4_TINY_RCU__CLOCK_REQUEST 32'h10c
+`define APB4_TINY_RCU__CLOCK_CURRENT 32'h110
+`define APB4_TINY_RCU__COMMAND 32'h114
+`define APB4_TINY_RCU__STATUS 32'h118
+`define APB4_TINY_RCU__TIMEOUT 32'h11c
+`define APB4_TINY_RCU__GATE_REQUEST 32'h120
+`define APB4_TINY_RCU__GATE_STATUS 32'h124
+`define APB4_TINY_RCU__RESET_REQUEST 32'h128
+`define APB4_TINY_RCU__RESET_DONE 32'h12c
+`define APB4_TINY_RCU__RESET_CAUSE 32'h130
+`define APB4_TINY_RCU__FAULT 32'h134
+`define APB4_TINY_RCU__IRQ_STATE 32'h138
+`define APB4_TINY_RCU__IRQ_ENABLE 32'h13c
+`define APB4_TINY_RCU__XIN_HZ 32'h140
+`define APB4_TINY_RCU__REF_HZ 32'h144
+`define APB4_TINY_RCU__SYS_HZ 32'h148
+`define APB4_TINY_RCU__MEM_HZ 32'h14c
+`define APB4_TINY_RCU__PCLK_HZ 32'h150
+`define APB4_TINY_RCU__CLINT_HZ 32'h154
+`define APB4_TINY_RCU__TARGET_CAP 32'h158
+`define APB4_TINY_RCU__CLKOUT 32'h15c
+`endif
diff --git a/rtl/tiny/top/tiny_reset_tree.sv b/rtl/tiny/top/tiny_reset_tree.sv
index 45c0dac3..1e50da5a 100644
--- a/rtl/tiny/top/tiny_reset_tree.sv
+++ b/rtl/tiny/top/tiny_reset_tree.sv
@@ -1,12 +1,17 @@
// Copyright (c) 2026 Yuchi Miao
// SPDX-License-Identifier: MulanPSL-2.0
-// One SYS clock, separate reset loads. Leaf zero owns the fabric; the remaining
-// leaves follow the canonical APB target order. This is not a clock-domain split.
+// SYS-clocked reset distribution with an always-on POR synchronizer. The POR
+// source remains on the external reference while all functional leaves release
+// on the selected SYS clock.
module tiny_reset_tree #(
- parameter int LeafCount = 17
+ parameter int LeafCount = 17,
+ parameter bit SplitDomains = 1'b0
) (
input logic clk_i,
+ input logic por_clk_i,
+ input logic [LeafCount-1:0] leaf_clk_i,
+ input logic backend_reset_n_i,
input logic rst_n_i,
input logic watchdog_reset_req_i,
output logic por_rst_n_o,
@@ -16,7 +21,8 @@ module tiny_reset_tree #(
);
logic s_system_req_n;
- assign s_system_req_n = por_rst_n_o && !watchdog_reset_req_i;
+ assign s_system_req_n = por_rst_n_o && !watchdog_reset_req_i &&
+ (!SplitDomains || backend_reset_n_i);
// The core/debug wrapper adds its five local release edges after every
// functional endpoint is ready. No external peripheral clock is a prerequisite.
assign cpu_ready_rst_n_o = &leaf_rst_n_o;
@@ -24,7 +30,7 @@ module tiny_reset_tree #(
rst_sync #(
.STAGE(5)
) u_por_rst_sync (
- .clk_i (clk_i),
+ .clk_i (SplitDomains ? por_clk_i : clk_i),
.rst_n_i(rst_n_i),
.rst_n_o(por_rst_n_o)
);
@@ -42,7 +48,7 @@ module tiny_reset_tree #(
rst_sync #(
.STAGE(5)
) u_leaf_rst_sync (
- .clk_i (clk_i),
+ .clk_i (SplitDomains ? leaf_clk_i[leaf] : clk_i),
.rst_n_i(system_rst_n_o),
.rst_n_o(leaf_rst_n_o[leaf])
);
diff --git a/rtl/tiny/top/tiny_sram.sv b/rtl/tiny/top/tiny_sram.sv
new file mode 100644
index 00000000..0d814167
--- /dev/null
+++ b/rtl/tiny/top/tiny_sram.sv
@@ -0,0 +1,107 @@
+// Copyright (c) 2026 Yuchi Miao
+// SPDX-License-Identifier: MulanPSL-2.0
+
+module tiny_sram (
+ input logic clk_i,
+ input logic rst_n_i,
+ input logic perf_enable_i,
+ input logic perf_clear_i,
+ tiny_sram_port_if.slave local_ports [2],
+ axi4_if.slave external_ports[4],
+ apb4_if.slave cfg_apb4
+);
+ logic [3:0] s_local_ready[2], s_local_valid[2];
+ logic [31:0] s_local_data[2] [4];
+ logic [ 3:0] s_events [6];
+ logic [31:0] s_count_d[6], s_count_q[6];
+ logic [ 2:0] s_increment[6];
+ logic [32:0] s_sum [6];
+
+ for (genvar group = 0; group < 4; group++) begin : gen_group
+ tiny_sram_port_if u_ports[3] ();
+ for (genvar client = 0; client < 2; client++) begin : gen_local
+ assign u_ports[client].req_valid = local_ports[client].req_valid &&
+ (local_ports[client].addr[16:15] == 2'(group));
+ assign u_ports[client].addr = local_ports[client].addr;
+ assign u_ports[client].write = local_ports[client].write;
+ assign u_ports[client].wdata = local_ports[client].wdata;
+ assign u_ports[client].wstrb = local_ports[client].wstrb;
+ assign u_ports[client].rsp_ready = local_ports[client].rsp_ready &&
+ (local_ports[client].addr[16:15] == 2'(group));
+ assign s_local_ready[client][group] = u_ports[client].req_ready;
+ assign s_local_valid[client][group] = u_ports[client].rsp_valid;
+ assign s_local_data[client][group] = u_ports[client].rdata;
+ end
+ tiny_sram_group u_group (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .ports (u_ports)
+ );
+ tiny_sram_axi4 #(
+ .GroupBase(32'h3000_0000 + 32'(group) * 32'h8000)
+ ) u_external (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .axi4 (external_ports[group]),
+ .memory (u_ports[2])
+ );
+ assign s_events[0][group] = external_ports[group].arvalid && external_ports[group].arready;
+ assign s_events[1][group] = external_ports[group].awvalid && external_ports[group].awready;
+ assign s_events[2][group] = external_ports[group].rvalid && external_ports[group].rready;
+ assign s_events[3][group] = external_ports[group].wvalid && external_ports[group].wready;
+ assign s_events[4][group] =
+ (external_ports[group].arvalid && !external_ports[group].arready) ||
+ (external_ports[group].awvalid && !external_ports[group].awready) ||
+ (external_ports[group].wvalid && !external_ports[group].wready) ||
+ (external_ports[group].rvalid && !external_ports[group].rready) ||
+ (external_ports[group].bvalid && !external_ports[group].bready);
+ assign s_events[5][group] =
+ (external_ports[group].bvalid && external_ports[group].bready &&
+ (external_ports[group].bresp != 2'b00)) ||
+ (external_ports[group].rvalid && external_ports[group].rready &&
+ external_ports[group].rlast && (external_ports[group].rresp != 2'b00));
+ end
+ for (genvar client = 0; client < 2; client++) begin : gen_local_response
+ assign local_ports[client].req_ready = s_local_ready[client][local_ports[client].addr[16:15]];
+ assign local_ports[client].rsp_valid = s_local_valid[client][local_ports[client].addr[16:15]];
+ assign local_ports[client].rdata = s_local_data[client][local_ports[client].addr[16:15]];
+ end
+ // Preserve the public external-AXI counter meanings. Four simultaneous stalls
+ // are one stalled cycle; request/beat events are counted individually.
+ for (genvar counter = 0; counter < 6; counter++) begin : gen_counter
+ if (counter == 4) begin : gen_cycle_count
+ assign s_increment[counter] = {2'b0, |s_events[counter]};
+ end else begin : gen_event_count
+ assign s_increment[counter] = {2'b0, s_events[counter][0]} +
+ {2'b0, s_events[counter][1]} + {2'b0, s_events[counter][2]} +
+ {2'b0, s_events[counter][3]};
+ end
+ assign s_sum[counter] = {1'b0, s_count_q[counter]} + {30'd0, s_increment[counter]};
+ assign s_count_d[counter] = perf_clear_i ? 32'd0 :
+ perf_enable_i ? (s_sum[counter][32] ? 32'hffff_ffff : s_sum[counter][31:0]) :
+ s_count_q[counter];
+ dffr #(
+ .DATA_WIDTH(32)
+ ) u_counter (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .dat_i (s_count_d[counter]),
+ .dat_o (s_count_q[counter])
+ );
+ end
+ onchip_ram_reg #(
+ .Present (1'b1),
+ .CapacityKiB(128),
+ .DataBytes (4)
+ ) u_reg (
+ .clk_i (clk_i),
+ .rst_n_i (rst_n_i),
+ .apb4 (cfg_apb4),
+ .perf_read_requests_i (s_count_q[0]),
+ .perf_write_requests_i (s_count_q[1]),
+ .perf_read_beats_i (s_count_q[2]),
+ .perf_write_beats_i (s_count_q[3]),
+ .perf_stall_cycles_i (s_count_q[4]),
+ .perf_error_responses_i(s_count_q[5])
+ );
+endmodule
diff --git a/rtl/tiny/top/tiny_sram_axi4.sv b/rtl/tiny/top/tiny_sram_axi4.sv
new file mode 100644
index 00000000..9c777a70
--- /dev/null
+++ b/rtl/tiny/top/tiny_sram_axi4.sv
@@ -0,0 +1,232 @@
+// Copyright (c) 2026 Yuchi Miao
+// SPDX-License-Identifier: MulanPSL-2.0
+
+// One external transaction per group. Address ownership lasts through the
+// terminal response, but the macro is arbitrated separately for every beat.
+// W may precede AW on the bus; it is backpressured until its AW is accepted.
+module tiny_sram_axi4 #(
+ parameter logic [31:0] GroupBase = 32'h3000_0000
+) (
+ input logic clk_i,
+ input logic rst_n_i,
+ axi4_if.slave axi4,
+ tiny_sram_port_if.master memory
+);
+ typedef enum logic [2:0] {
+ Idle,
+ ReadStream,
+ ReadError,
+ WriteStream,
+ WriteResponse,
+ WriteDrain
+ } state_e;
+ typedef struct packed {
+ state_e state;
+ logic prefer_write;
+ logic id;
+ logic [31:0] addr;
+ logic [7:0] len, beat;
+ logic [2:0] size;
+ logic [1:0] burst, resp;
+ logic [8:0] issued, completed;
+ logic last;
+ } transaction_t;
+ typedef struct packed {
+ logic [31:0] addr, data;
+ logic [3:0] strb;
+ } write_t;
+ transaction_t s_transaction_d, s_transaction_q;
+ write_t s_write_input, s_write_output;
+ logic [31:0] s_next_addr;
+ logic [1:0] s_ar_resp, s_aw_resp;
+ logic [3:0] s_lane_mask;
+ logic s_expected_last, s_write_legal;
+ logic s_write_full, s_write_empty, s_write_push, s_write_pop;
+ logic s_req_accept, s_resp_accept, s_write_accept;
+
+ function automatic logic [1:0] address_response(input logic [31:0] addr_i,
+ input logic [7:0] len_i, input logic [2:0] size_i,
+ input logic [1:0] burst_i, input logic lock_i);
+ logic [32:0] bytes_count, last_addr;
+ logic wrap_legal;
+ bytes_count = ({25'd0, len_i} + 33'd1) << size_i;
+ wrap_legal = (len_i == 8'd1) || (len_i == 8'd3) || (len_i == 8'd7) || (len_i == 8'd15);
+ last_addr = {1'b0, addr_i} + bytes_count - 33'd1;
+ if (burst_i == 2'b00) last_addr = {1'b0, addr_i} + (33'd1 << size_i) - 33'd1;
+ if (burst_i == 2'b10)
+ last_addr = ({1'b0, addr_i} & ~(bytes_count - 33'd1)) + bytes_count - 33'd1;
+ if (lock_i || (len_i > 8'd15) || (size_i > 3'd2) ||
+ (burst_i == 2'b11) || ((burst_i == 2'b10) && !wrap_legal) ||
+ ((addr_i & ((32'd1 << size_i) - 32'd1)) != 32'd0) || last_addr[32] ||
+ (addr_i[31:12] != last_addr[31:12]))
+ return 2'b10;
+ if ((addr_i < GroupBase) || (last_addr[31:0] > (GroupBase + 32'h7fff))) return 2'b11;
+ return 2'b00;
+ endfunction
+
+ assign s_ar_resp = address_response(
+ axi4.araddr, axi4.arlen, axi4.arsize, axi4.arburst, axi4.arlock
+ );
+ assign s_aw_resp = address_response(
+ axi4.awaddr, axi4.awlen, axi4.awsize, axi4.awburst, axi4.awlock
+ );
+ assign axi4.arready = (s_transaction_q.state == Idle) &&
+ !(s_transaction_q.prefer_write && axi4.awvalid);
+ assign axi4.awready = (s_transaction_q.state == Idle) &&
+ (s_transaction_q.prefer_write || !axi4.arvalid);
+ assign axi4.wready = ((s_transaction_q.state == WriteStream) && !s_transaction_q.last &&
+ (!s_write_full || s_write_pop)) || (s_transaction_q.state == WriteDrain);
+ assign axi4.rvalid = (s_transaction_q.state == ReadError) ||
+ ((s_transaction_q.state == ReadStream) && memory.rsp_valid);
+ assign axi4.rid = s_transaction_q.id;
+ assign axi4.rdata = (s_transaction_q.state == ReadStream) ? memory.rdata : 32'd0;
+ assign axi4.rresp = s_transaction_q.resp;
+ assign axi4.rlast = s_transaction_q.beat == s_transaction_q.len;
+ assign axi4.ruser = '0;
+ assign axi4.bvalid = s_transaction_q.state == WriteResponse;
+ assign axi4.bid = s_transaction_q.id;
+ assign axi4.bresp = s_transaction_q.resp;
+ assign axi4.buser = '0;
+
+ // Read responses stay in the group's reserved queue until R handshakes.
+ // Issuance and retirement have independent positions within the same burst.
+ assign memory.req_valid = ((s_transaction_q.state == ReadStream) &&
+ (s_transaction_q.issued <= {1'b0, s_transaction_q.len})) ||
+ ((s_transaction_q.state == WriteStream) && !s_write_empty);
+ assign memory.addr = memory.write ? s_write_output.addr : s_transaction_q.addr;
+ assign memory.write = s_transaction_q.state == WriteStream;
+ assign memory.wdata = s_write_output.data;
+ assign memory.wstrb = s_write_output.strb;
+ assign memory.rsp_ready = ((s_transaction_q.state == ReadStream) && axi4.rready) ||
+ (s_transaction_q.state == WriteStream);
+ assign s_req_accept = memory.req_valid && memory.req_ready;
+ assign s_resp_accept = memory.rsp_valid && memory.rsp_ready;
+ assign s_write_accept = axi4.wvalid && axi4.wready;
+ assign s_write_push = (s_transaction_q.state == WriteStream) && s_write_accept && s_write_legal;
+ assign s_write_pop = (s_transaction_q.state == WriteStream) && s_req_accept;
+ assign s_write_input = {s_transaction_q.addr, axi4.wdata, axi4.wstrb};
+ // Capture W before it becomes eligible at the macro. A full queue can
+ // replace its head on the same edge that the arbiter commits that word.
+ fifo #(
+ .DATA_WIDTH ($bits(write_t)),
+ .BUFFER_DEPTH(2)
+ ) u_write_fifo (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .flush_i(1'b0),
+ .push_i (s_write_push),
+ .full_o (s_write_full),
+ .dat_i (s_write_input),
+ .pop_i (s_write_pop),
+ .empty_o(s_write_empty),
+ .dat_o (s_write_output),
+ .cnt_o () // Full/empty and accepted/completed counts cover ownership.
+ );
+ assign s_lane_mask = 4'(((32'd1 << (32'd1 << s_transaction_q.size)) - 32'd1)
+ << s_transaction_q.addr[1:0]);
+ assign s_expected_last = s_transaction_q.beat == s_transaction_q.len;
+ assign s_write_legal = ((axi4.wstrb & ~s_lane_mask) == 4'd0) && (axi4.wlast == s_expected_last);
+ axi4_addr_gen #(
+ .ADDR_WIDTH(32)
+ ) u_address (
+ .alen_i (s_transaction_q.len),
+ .asize_i (s_transaction_q.size),
+ .aburst_i(s_transaction_q.burst),
+ .addr_i (s_transaction_q.addr),
+ .addr_o (s_next_addr)
+ );
+
+ always_comb begin
+ s_transaction_d = s_transaction_q;
+ unique case (s_transaction_q.state)
+ Idle: begin
+ s_transaction_d.beat = '0;
+ s_transaction_d.issued = '0;
+ s_transaction_d.completed = '0;
+ s_transaction_d.last = 1'b0;
+ if (axi4.arvalid && axi4.arready) begin
+ s_transaction_d.prefer_write = 1'b1;
+ s_transaction_d.id = axi4.arid;
+ s_transaction_d.addr = axi4.araddr;
+ // Match the shared target's malformed-address error response length.
+ s_transaction_d.len = (s_ar_resp == 2'b10) ? 8'd0 : axi4.arlen;
+ s_transaction_d.size = axi4.arsize;
+ s_transaction_d.burst = axi4.arburst;
+ s_transaction_d.resp = s_ar_resp;
+ s_transaction_d.state = (s_ar_resp == 2'b00) ? ReadStream : ReadError;
+ end else if (axi4.awvalid && axi4.awready) begin
+ s_transaction_d.prefer_write = 1'b0;
+ s_transaction_d.id = axi4.awid;
+ s_transaction_d.addr = axi4.awaddr;
+ s_transaction_d.len = (s_aw_resp == 2'b10) ? 8'd0 : axi4.awlen;
+ s_transaction_d.size = axi4.awsize;
+ s_transaction_d.burst = axi4.awburst;
+ s_transaction_d.resp = s_aw_resp;
+ s_transaction_d.state = (s_aw_resp == 2'b00) ? WriteStream : WriteDrain;
+ end
+ end
+ ReadStream: begin
+ if (s_req_accept) begin
+ s_transaction_d.issued = s_transaction_q.issued + 9'd1;
+ s_transaction_d.addr = s_next_addr;
+ end
+ if (s_resp_accept) begin
+ s_transaction_d.completed = s_transaction_q.completed + 9'd1;
+ if (axi4.rlast) s_transaction_d.state = Idle;
+ else s_transaction_d.beat = s_transaction_q.beat + 8'd1;
+ end
+ end
+ ReadError:
+ if (axi4.rready) begin
+ if (axi4.rlast) s_transaction_d.state = Idle;
+ else s_transaction_d.beat = s_transaction_q.beat + 8'd1;
+ end
+ WriteStream: begin
+ if (s_write_accept) begin
+ s_transaction_d.last = s_expected_last || axi4.wlast;
+ s_transaction_d.beat = s_transaction_q.beat + 8'd1;
+ s_transaction_d.addr = s_next_addr;
+ if (!s_write_legal) s_transaction_d.resp = 2'b10;
+ end
+ if (s_req_accept) s_transaction_d.issued = s_transaction_q.issued + 9'd1;
+ if (s_resp_accept) s_transaction_d.completed = s_transaction_q.completed + 9'd1;
+ // Errors do not discard older queued writes. B acknowledges all
+ // accepted legal beats only after their macro-commit responses drain.
+ if (s_transaction_q.last && s_write_empty &&
+ (s_transaction_q.issued == s_transaction_q.completed))
+ s_transaction_d.state = WriteResponse;
+ end
+ WriteDrain:
+ if (axi4.wvalid) begin
+ if (s_expected_last || axi4.wlast) s_transaction_d.state = WriteResponse;
+ else s_transaction_d.beat = s_transaction_q.beat + 8'd1;
+ end
+ WriteResponse: if (axi4.bready) s_transaction_d.state = Idle;
+ default: s_transaction_d.state = Idle;
+ endcase
+ end
+ dffr #(
+ .DATA_WIDTH($bits(transaction_t))
+ ) u_transaction (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .dat_i (s_transaction_d),
+ .dat_o (s_transaction_q)
+ );
+`ifdef HAVE_SVA
+ assert property (@(posedge clk_i) disable iff (!rst_n_i)
+ s_transaction_q.completed <= s_transaction_q.issued);
+ assert property (@(posedge clk_i) disable iff (!rst_n_i)
+ axi4.bvalid |-> s_write_empty &&
+ (s_transaction_q.completed == s_transaction_q.issued));
+ assert property (@(posedge clk_i) disable iff (!rst_n_i)
+ axi4.rvalid && !axi4.rready |=> axi4.rvalid &&
+ $stable(
+ {axi4.rdata, axi4.rid, axi4.rresp, axi4.rlast}
+ ));
+ assert property (@(posedge clk_i) disable iff (!rst_n_i)
+ axi4.bvalid && !axi4.bready |=> axi4.bvalid && $stable(
+ {axi4.bid, axi4.bresp}
+ ));
+`endif
+endmodule
diff --git a/rtl/tiny/top/tiny_sram_group.sv b/rtl/tiny/top/tiny_sram_group.sv
new file mode 100644
index 00000000..a1c1c357
--- /dev/null
+++ b/rtl/tiny/top/tiny_sram_group.sv
@@ -0,0 +1,206 @@
+// Copyright (c) 2026 Yuchi Miao
+// SPDX-License-Identifier: MulanPSL-2.0
+
+// One independently arbitrated 32 KiB group. Arbitration advances on the
+// physical macro edge, never on an AXI address or burst reservation. Captured
+// responses isolate all three clients from subsequent macro output changes.
+module tiny_sram_group (
+ input logic clk_i,
+ input logic rst_n_i,
+ tiny_sram_port_if.slave ports [3]
+);
+ logic [2:0] s_req, s_grant;
+ logic [1:0] s_selected;
+ logic s_issue;
+ logic [1:0] s_rsp_valid_d;
+ logic [2:0] s_rsp_valid_q;
+ logic [31:0] s_rsp_data_d[2], s_rsp_data_q[3];
+ logic s_read_pending_d, s_read_pending_q;
+ logic [1:0] s_read_owner_d, s_read_owner_q;
+ logic [2:0] s_read_bank_d, s_read_bank_q;
+ logic [2:0] s_write;
+ logic [31:0] s_addr[3], s_wdata[3];
+ logic [ 3:0] s_wstrb [3];
+ logic [31:0] s_bank_data [8];
+ logic [ 7:0] s_bank_select;
+
+ for (genvar client = 0; client < 3; client++) begin : gen_client
+ assign s_addr[client] = ports[client].addr;
+ assign s_write[client] = ports[client].write;
+ assign s_wdata[client] = ports[client].wdata;
+ assign s_wstrb[client] = ports[client].wstrb;
+ assign ports[client].req_ready = s_grant[client];
+ assign ports[client].rsp_valid = s_rsp_valid_q[client];
+ assign ports[client].rdata = s_rsp_data_q[client];
+
+ if (client < 2) begin : gen_local_response
+ assign s_req[client] = ports[client].req_valid && !s_rsp_valid_q[client] &&
+ !(s_read_pending_q && (s_read_owner_q == 2'(client)));
+ always_comb begin
+ s_rsp_valid_d[client] = s_rsp_valid_q[client] && !ports[client].rsp_ready;
+ s_rsp_data_d[client] = s_rsp_data_q[client];
+ if (s_read_pending_q && (s_read_owner_q == 2'(client))) begin
+ s_rsp_valid_d[client] = 1'b1;
+ s_rsp_data_d[client] = s_bank_data[s_read_bank_q];
+ end
+ if (s_grant[client] && s_write[client]) begin
+ s_rsp_valid_d[client] = 1'b1;
+ s_rsp_data_d[client] = '0;
+ end
+ end
+ dffr #(
+ .DATA_WIDTH(1)
+ ) u_response_valid (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .dat_i (s_rsp_valid_d[client]),
+ .dat_o (s_rsp_valid_q[client])
+ );
+ dffr #(
+ .DATA_WIDTH(32)
+ ) u_response_data (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .dat_i (s_rsp_data_d[client]),
+ .dat_o (s_rsp_data_q[client])
+ );
+ end else begin : gen_external_response
+ logic s_empty, s_full, s_pop, s_push, s_pending;
+ logic [ 1:0] s_count;
+ logic [ 2:0] s_reserved;
+ logic [31:0] s_data;
+ assign s_pending = s_read_pending_q && (s_read_owner_q == 2'(client));
+ assign s_reserved = {1'b0, s_count} + {2'b0, s_pending};
+ assign s_pop = ports[client].rsp_ready && !s_empty;
+ // Include the macro read already in flight. A returning word must fit
+ // even if the consumer removes READY immediately after this issue.
+ // A same-client read return and write commit cannot share one FIFO push.
+ assign s_req[client] = ports[client].req_valid &&
+ ((s_reserved < 3'd2) || ((s_reserved == 3'd2) && s_pop)) &&
+ !(s_pending && s_write[client]);
+ assign s_push = s_pending || (s_grant[client] && s_write[client]);
+ assign s_data = s_pending ? s_bank_data[s_read_bank_q] : 32'd0;
+ assign s_rsp_valid_q[client] = !s_empty;
+ fifo #(
+ .DATA_WIDTH (32),
+ .BUFFER_DEPTH(2)
+ ) u_response_fifo (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .flush_i(1'b0),
+ .push_i (s_push),
+ .full_o (s_full),
+ .dat_i (s_data),
+ .pop_i (s_pop),
+ .empty_o(s_empty),
+ .dat_o (s_rsp_data_q[client]),
+ .cnt_o (s_count)
+ );
+`ifdef FORMAL
+ always_ff @(posedge clk_i) begin
+ if (rst_n_i) begin
+ assert (s_reserved <= 3'd2);
+ assert (!s_push || !s_full || s_pop);
+ assert (!(s_pending && s_grant[client] && s_write[client]));
+ end
+ end
+`endif
+`ifdef HAVE_SVA
+ assert property (@(posedge clk_i) disable iff (!rst_n_i) s_reserved <= 3'd2);
+ assert property (@(posedge clk_i) disable iff (!rst_n_i) !s_push || !s_full || s_pop);
+`endif
+ end
+ end
+
+ round_robin_arbiter #(
+ .CLIENTS(3)
+ ) u_arbiter (
+ .clk_i (clk_i),
+ .rst_n_i (rst_n_i),
+ .advance_i (s_issue),
+ .request_i (s_req),
+ .grant_o (s_grant),
+ .selected_o(s_selected),
+ .valid_o (s_issue)
+ );
+ assign s_read_pending_d = s_issue && !s_write[s_selected];
+ assign s_read_owner_d = s_selected;
+ assign s_read_bank_d = s_addr[s_selected][14:12];
+ dffr #(
+ .DATA_WIDTH(1)
+ ) u_read_pending (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .dat_i (s_read_pending_d),
+ .dat_o (s_read_pending_q)
+ );
+ dffer #(
+ .DATA_WIDTH(2)
+ ) u_read_owner (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .en_i (s_read_pending_d),
+ .dat_i (s_read_owner_d),
+ .dat_o (s_read_owner_q)
+ );
+ dffer #(
+ .DATA_WIDTH(3)
+ ) u_read_bank (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .en_i (s_read_pending_d),
+ .dat_i (s_read_bank_d),
+ .dat_o (s_read_bank_q)
+ );
+
+ for (genvar bank = 0; bank < 8; bank++) begin : gen_bank
+ assign s_bank_select[bank] = s_issue && (s_addr[s_selected][14:12] == 3'(bank));
+ tc_sram_1024x32 u_ram (
+ .clk_i (clk_i),
+ .cs_i (s_bank_select[bank]),
+ .addr_i(s_addr[s_selected][11:2]),
+ .data_i(s_wdata[s_selected]),
+ .mask_i(s_wstrb[s_selected]),
+ .wren_i(s_write[s_selected]),
+ .data_o(s_bank_data[bank])
+ );
+ end
+
+`ifdef FORMAL
+ // Service-count proof: only continuously eligible requests accumulate
+ // bypasses. No wall-clock liveness is assumed for a withheld response.
+ for (genvar client = 0; client < 3; client++) begin : gen_formal
+ logic [1:0] s_bypasses_q;
+ logic s_past_valid_q = 1'b0;
+ always_ff @(posedge clk_i) begin
+ s_past_valid_q <= 1'b1;
+ if (!rst_n_i || !s_req[client] || s_grant[client]) s_bypasses_q <= '0;
+ else if (s_issue) s_bypasses_q <= s_bypasses_q + 2'd1;
+ if (rst_n_i) begin
+ assert ($onehot0(s_grant));
+ assert (s_bypasses_q <= 2'd2);
+ assert (!(s_req[client] && (s_bypasses_q == 2'd2) && s_issue && !s_grant[client]));
+ if (client < 2)
+ assert (!(s_rsp_valid_q[client] && s_read_pending_q && (s_read_owner_q == 2'(client))));
+ end
+ if (s_past_valid_q && rst_n_i && $past(
+ rst_n_i && ports[client].rsp_valid && !ports[client].rsp_ready
+ )) begin
+ assert (ports[client].rsp_valid);
+ assert (ports[client].rdata == $past(ports[client].rdata));
+ end
+ end
+ end
+`endif
+`ifdef HAVE_SVA
+ assert property (@(posedge clk_i) disable iff (!rst_n_i) $onehot0(s_grant));
+ assert property (@(posedge clk_i) disable iff (!rst_n_i) $onehot0(s_bank_select));
+ for (genvar client = 0; client < 3; client++) begin : gen_assert
+ assert property (@(posedge clk_i) disable iff (!rst_n_i)
+ ports[client].rsp_valid && !ports[client].rsp_ready |=>
+ ports[client].rsp_valid && $stable(
+ ports[client].rdata
+ ));
+ end
+`endif
+endmodule
diff --git a/rtl/tiny/top/tiny_sram_port_if.sv b/rtl/tiny/top/tiny_sram_port_if.sv
new file mode 100644
index 00000000..ff856f59
--- /dev/null
+++ b/rtl/tiny/top/tiny_sram_port_if.sv
@@ -0,0 +1,25 @@
+// Copyright (c) 2026 Yuchi Miao
+// SPDX-License-Identifier: MulanPSL-2.0
+
+// One word operation, byte strobes, one ordered response per accepted request.
+// Local I/D retain their address through the response handshake (bank routing)
+// and have one operation outstanding. A fixed-group external client may have
+// two operations outstanding, including a reserved macro read return. It may
+// advance its request address after acceptance; responses remain in order.
+interface tiny_sram_port_if;
+ logic req_valid, req_ready;
+ logic [31:0] addr, wdata;
+ logic write;
+ logic [3:0] wstrb;
+ logic rsp_valid, rsp_ready;
+ logic [31:0] rdata;
+
+ modport master(
+ output req_valid, addr, wdata, write, wstrb, rsp_ready,
+ input req_ready, rsp_valid, rdata
+ );
+ modport slave(
+ input req_valid, addr, wdata, write, wstrb, rsp_ready,
+ output req_ready, rsp_valid, rdata
+ );
+endinterface
diff --git a/rtl/tiny/top/tiny_sysctrl.sv b/rtl/tiny/top/tiny_sysctrl.sv
index 7a0ac57a..fd730151 100644
--- a/rtl/tiny/top/tiny_sysctrl.sv
+++ b/rtl/tiny/top/tiny_sysctrl.sv
@@ -1,30 +1,95 @@
// Copyright (c) 2026 Yuchi Miao
// SPDX-License-Identifier: MulanPSL-2.0
`include "sysctrl_define.svh"
+`include "tiny_rcu_regs.svh"
-module tiny_sysctrl (
+module tiny_sysctrl #(
+ parameter bit PllPresent = 1'b0,
+ parameter bit PllProfiles = 1'b0,
+ parameter int unsigned XinHzValue = 24_000_000,
+ parameter logic [13:0] TargetMask = 14'd0
+) (
input logic clk_i,
input logic rst_n_i,
+ input logic clk_ref_i,
+ input logic rst_ref_n_i,
+ output logic rcu_link_ready_o,
apb4_if.slave apb4,
input logic fault_valid_i,
+ input logic [ 3:0] fault_count_i,
input logic [31:0] fault_addr_i,
input logic fault_write_i,
- input logic fault_master_i,
+ input logic [ 1:0] fault_master_i,
input logic [ 1:0] fault_resp_i,
input logic [63:0] cpu_wait_i,
input logic [63:0] dma_wait_i,
input logic rtc_wake_i,
+ input logic clock_done_i,
+ input logic [ 4:0] clock_fault_i,
+ input logic [ 1:0] clock_current_i,
+ input logic pll_locked_i,
+ input logic pll_selected_i,
+ input logic warm_reset_i,
+ input logic watchdog_reset_i,
+ input logic hart_reset_i,
+ output logic system_reset_o,
+ output logic rcu_irq_o,
+ output logic [31:0] clock_timeout_o,
+ input logic periph_done_i,
+ input logic [ 4:0] periph_fault_i,
+ input logic [13:0] gate_actual_i,
+ input logic [13:0] reset_done_i,
+ output logic periph_apply_o,
+ output logic periph_reset_o,
+ output logic [13:0] periph_request_o,
+ output logic [31:0] clkout_control_o,
+ input logic clkout_idle_i,
output logic perf_enable_o,
output logic perf_clear_o,
output logic test_done_o,
output logic test_pass_o,
- output logic [ 7:0] test_code_o
+ output logic [ 7:0] test_code_o,
+ output logic [31:0] xin_hz_o,
+ output logic [31:0] ref_hz_o,
+ output logic [31:0] sys_hz_o,
+ output logic [31:0] mem_hz_o,
+ output logic [31:0] pclk_hz_o,
+ output logic [31:0] clint_hz_o,
+ output logic [ 1:0] rcu_current_o,
+ output logic rcu_busy_o,
+ output logic clock_apply_o,
+ output logic [ 1:0] clock_profile_o
);
+ localparam logic [31:0] RcuId = `APB4_TINY_RCU__ID;
+ localparam logic [31:0] RcuVersion = `APB4_TINY_RCU__VERSION;
+ localparam logic [31:0] RcuCapability = `APB4_TINY_RCU__CAPABILITY;
+ localparam logic [31:0] ClockRequest = `APB4_TINY_RCU__CLOCK_REQUEST;
+ localparam logic [31:0] ClockCurrent = `APB4_TINY_RCU__CLOCK_CURRENT;
+ localparam logic [31:0] RcuCommand = `APB4_TINY_RCU__COMMAND;
+ localparam logic [31:0] RcuStatus = `APB4_TINY_RCU__STATUS;
+ localparam logic [31:0] TimeoutRefCycles = `APB4_TINY_RCU__TIMEOUT;
+ localparam logic [31:0] GateRequest = `APB4_TINY_RCU__GATE_REQUEST;
+ localparam logic [31:0] GateStatus = `APB4_TINY_RCU__GATE_STATUS;
+ localparam logic [31:0] ResetRequest = `APB4_TINY_RCU__RESET_REQUEST;
+ localparam logic [31:0] ResetDone = `APB4_TINY_RCU__RESET_DONE;
+ localparam logic [31:0] ResetCause = `APB4_TINY_RCU__RESET_CAUSE;
+ localparam logic [31:0] RcuFault = `APB4_TINY_RCU__FAULT;
+ localparam logic [31:0] RcuIrqState = `APB4_TINY_RCU__IRQ_STATE;
+ localparam logic [31:0] RcuIrqEnable = `APB4_TINY_RCU__IRQ_ENABLE;
+ localparam logic [31:0] XinHz = `APB4_TINY_RCU__XIN_HZ;
+ localparam logic [31:0] RefHz = `APB4_TINY_RCU__REF_HZ;
+ localparam logic [31:0] SysHz = `APB4_TINY_RCU__SYS_HZ;
+ localparam logic [31:0] MemHz = `APB4_TINY_RCU__MEM_HZ;
+ localparam logic [31:0] PclkHz = `APB4_TINY_RCU__PCLK_HZ;
+ localparam logic [31:0] ClintHz = `APB4_TINY_RCU__CLINT_HZ;
+ localparam logic [31:0] TargetCapability = `APB4_TINY_RCU__TARGET_CAP;
+ localparam logic [31:0] ClkoutControl = `APB4_TINY_RCU__CLKOUT;
logic [31:0] s_test_d, s_test_q;
logic [31:0] s_fault_addr_d, s_fault_addr_q;
logic [31:0] s_fault_count_d, s_fault_count_q;
logic [4:0] s_fault_stat_d, s_fault_stat_q;
- logic s_fault_master_d, s_fault_master_q;
+ logic [1:0] s_fault_master_d, s_fault_master_q;
+ logic [32:0] s_fault_sum;
logic [1:0] s_fault_resp_d, s_fault_resp_q;
logic s_perf_en_d, s_perf_en_q;
logic s_wake_d, s_wake_q;
@@ -36,23 +101,137 @@ module tiny_sysctrl (
logic [31:0] s_read_data;
logic [31:0] s_offset;
logic [31:0] s_rdata_d, s_rdata_q;
+ logic [1:0] s_clk_req_d, s_clk_req_q;
+ logic [1:0] s_clk_cur_d, s_clk_cur_q;
+ logic [1:0] s_clk_latched_d, s_clk_latched_q;
+ logic s_system_reset_d, s_system_reset_q;
+ logic s_periph_apply_d, s_periph_apply_q, s_periph_reset_d, s_periph_reset_q;
+ logic [13:0] s_periph_mask_d, s_periph_mask_q;
+ logic [3:0] s_active_cmd_d, s_active_cmd_q;
+ logic s_clock_apply_d, s_clock_apply_q;
+ logic s_rcu_busy_d, s_rcu_busy_q;
+ logic [31:0] s_timeout_ref_d, s_timeout_ref_q;
+ logic [31:0] s_gate_req_d, s_gate_req_q;
+ logic [31:0] s_gate_stat_d, s_gate_stat_q;
+ logic [31:0] s_rst_req_d, s_rst_req_q;
+ logic [31:0] s_reset_done_d, s_reset_done_q;
+ logic [31:0] s_reset_cause_d, s_reset_cause_q;
+ logic [31:0] s_rcu_fault_d, s_rcu_fault_q;
+ logic [31:0] s_rcu_irq_state_d, s_rcu_irq_state_q;
+ logic [31:0] s_rcu_irq_en_d, s_rcu_irq_en_q;
+ logic [31:0] s_clkout_control_d, s_clkout_control_q;
+ logic [31:0] s_sys_hz_d, s_sys_hz_q;
+ logic [31:0] s_mem_hz_d, s_mem_hz_q;
+ logic [31:0] s_pclk_hz_d, s_pclk_hz_q;
+ logic [31:0] s_clkout_hz;
+
+ logic s_rcu_selected, s_rcu_req, s_rcu_write, s_rcu_valid, s_rcu_writable, s_rcu_err;
+ logic s_rcu_config_err;
+ logic s_rcu_ready_d, s_rcu_ready_q, s_rcu_err_d, s_rcu_err_q;
+ logic [31:0] s_rcu_offset, s_rcu_read_data, s_rcu_rdata_d, s_rcu_rdata_q;
+ apb4_if u_rcu_src (
+ .pclk (clk_i),
+ .presetn(rst_n_i)
+ );
+ apb4_if u_rcu_dst (
+ .pclk (clk_ref_i),
+ .presetn(rst_ref_n_i)
+ );
+
+ assign s_rcu_selected = apb4.paddr[11:8] == 4'h1;
+ assign u_rcu_src.paddr = apb4.paddr;
+ assign u_rcu_src.pprot = apb4.pprot;
+ assign u_rcu_src.pwrite = apb4.pwrite;
+ assign u_rcu_src.pwdata = apb4.pwdata;
+ assign u_rcu_src.pstrb = apb4.pstrb;
+ assign u_rcu_src.psel = apb4.psel && s_rcu_selected;
+ assign u_rcu_src.penable = apb4.penable;
+ tiny_apb_async_bridge u_rcu_cdc (
+ .src_clk_i (clk_i),
+ .src_rst_n_i(rst_n_i),
+ .dst_clk_i (clk_ref_i),
+ .dst_rst_n_i(rst_ref_n_i),
+ .disabled_i (1'b0),
+ .idle_o (),
+ .ready_o (rcu_link_ready_o),
+ .src_apb4 (u_rcu_src),
+ .dst_apb4 (u_rcu_dst)
+ );
+ assign s_rcu_offset = {20'd0, u_rcu_dst.paddr[11:0]};
+ assign s_rcu_req = u_rcu_dst.psel && u_rcu_dst.penable && !s_rcu_ready_q;
+ assign s_rcu_err = (u_rcu_dst.paddr[1:0] != 0) || !s_rcu_valid ||
+ (u_rcu_dst.pwrite && (!s_rcu_writable || u_rcu_dst.pstrb != 4'hf)) ||
+ (!u_rcu_dst.pwrite && s_rcu_offset == RcuCommand) ||
+ (u_rcu_dst.pwrite && s_rcu_config_err);
+ assign s_rcu_write = s_rcu_req && u_rcu_dst.pwrite && !s_rcu_err;
+ assign s_rcu_ready_d = s_rcu_req;
+ assign s_rcu_err_d = s_rcu_req && s_rcu_err;
+ assign s_rcu_rdata_d = s_rcu_req ? s_rcu_read_data : s_rcu_rdata_q;
+ assign u_rcu_dst.pready = s_rcu_ready_q;
+ assign u_rcu_dst.pslverr = s_rcu_err_q;
+ assign u_rcu_dst.prdata = s_rcu_rdata_q;
+ dffr #(
+ .DATA_WIDTH(1)
+ ) u_rcu_ready (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_rcu_ready_d),
+ .dat_o (s_rcu_ready_q)
+ );
+ dffr #(
+ .DATA_WIDTH(1)
+ ) u_rcu_error (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_rcu_err_d),
+ .dat_o (s_rcu_err_q)
+ );
+ dffr #(
+ .DATA_WIDTH(32)
+ ) u_rcu_rdata (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_rcu_rdata_d),
+ .dat_o (s_rcu_rdata_q)
+ );
assign s_offset = {20'd0, apb4.paddr[11:0]};
- assign s_req = apb4.psel && apb4.penable && !s_ready_q;
+ assign s_req = apb4.psel && !s_rcu_selected && apb4.penable && !s_ready_q;
assign s_write = s_req && apb4.pwrite && !s_access_err;
assign s_access_err = (apb4.paddr[1:0] != 2'd0) || !s_valid ||
- (apb4.pwrite && (!s_writable || (apb4.pstrb != 4'hf)));
+ (apb4.pwrite && (!s_writable || (apb4.pstrb != 4'hf))) ||
+ (!apb4.pwrite && (s_offset == RcuCommand));
assign s_ready_d = s_req;
assign s_err_d = s_req && s_access_err;
assign s_rdata_d = s_req ? s_read_data : s_rdata_q;
- assign apb4.pready = s_ready_q;
- assign apb4.pslverr = s_err_q;
- assign apb4.prdata = s_rdata_q;
+ assign apb4.pready = s_rcu_selected ? u_rcu_src.pready : s_ready_q;
+ assign apb4.pslverr = s_rcu_selected ? u_rcu_src.pslverr : s_err_q;
+ assign apb4.prdata = s_rcu_selected ? u_rcu_src.prdata : s_rdata_q;
assign perf_enable_o = s_perf_en_q;
assign perf_clear_o = s_write && (s_offset == `APB4_SYSCTRL__PERF_CTRL) && apb4.pwdata[1];
assign test_done_o = s_test_q[31];
assign test_pass_o = s_test_q[0];
assign test_code_o = s_test_q[15:8];
+ assign xin_hz_o = XinHzValue;
+ assign ref_hz_o = 32'd24_000_000;
+ assign sys_hz_o = s_sys_hz_q;
+ assign mem_hz_o = s_mem_hz_q;
+ assign pclk_hz_o = s_pclk_hz_q;
+ assign clint_hz_o = 32'd1_000_000;
+ assign rcu_current_o = s_clk_cur_q;
+ assign rcu_busy_o = s_rcu_busy_q;
+ assign clock_apply_o = s_clock_apply_q;
+ assign clock_profile_o = s_clk_latched_q;
+ assign clock_timeout_o = s_timeout_ref_q;
+ assign clkout_control_o = s_clkout_control_q;
+ assign s_clkout_hz = u_rcu_dst.pwdata[2:0] == 2 ? s_sys_hz_q :
+ u_rcu_dst.pwdata[2:0] == 3 ? s_mem_hz_q :
+ u_rcu_dst.pwdata[2:0] == 4 ? s_pclk_hz_q : 32'd24_000_000;
+ assign periph_apply_o = s_periph_apply_q;
+ assign periph_reset_o = s_periph_reset_q;
+ assign periph_request_o = s_periph_mask_q;
+ assign system_reset_o = s_system_reset_q;
+ assign rcu_irq_o = |(s_rcu_irq_state_q[1:0] & s_rcu_irq_en_q[1:0]);
always_comb begin
s_valid = 1'b1;
@@ -68,7 +247,7 @@ module tiny_sysctrl (
end
`APB4_SYSCTRL__FAULT_ADDR: s_read_data = s_fault_addr_q;
`APB4_SYSCTRL__FAULT_COUNT: s_read_data = s_fault_count_q;
- `APB4_SYSCTRL__FAULT_MASTER: s_read_data = {31'd0, s_fault_master_q};
+ `APB4_SYSCTRL__FAULT_MASTER: s_read_data = {30'd0, s_fault_master_q};
`APB4_SYSCTRL__FAULT_DETAIL: s_read_data = {30'd0, s_fault_resp_q};
`APB4_SYSCTRL__PERF_CTRL: begin
s_read_data = {31'd0, s_perf_en_q};
@@ -89,6 +268,276 @@ module tiny_sysctrl (
default: s_valid = 1'b0;
endcase
end
+ always_comb begin
+ s_rcu_valid = 1'b1;
+ s_rcu_writable = 1'b0;
+ s_rcu_read_data = 32'd0;
+ unique case (s_rcu_offset)
+ RcuId: s_rcu_read_data = 32'h5452_4355;
+ RcuVersion: s_rcu_read_data = 32'h0001_0000;
+ // Advertise only the instantiated product backend and lifecycle paths.
+ RcuCapability:
+ s_rcu_read_data = 32'h0000_0201 |
+ (PllPresent ? 32'h100 : 32'h2) | (PllProfiles ? 32'hc : 32'd0) |
+ (TargetMask != 0 ? 32'h1800 : 32'd0);
+ ClockRequest: begin
+ s_rcu_read_data = {30'd0, s_clk_req_q};
+ s_rcu_writable = 1'b1;
+ end
+ ClockCurrent: s_rcu_read_data = {30'd0, s_clk_cur_q};
+ RcuCommand: begin
+ s_rcu_read_data = 32'd0;
+ s_rcu_writable = 1'b1;
+ end
+ RcuStatus: begin
+ s_rcu_read_data = 32'd0;
+ s_rcu_read_data[0] = s_rcu_busy_q;
+ s_rcu_read_data[1] = (s_clk_cur_q == 2'd0);
+ s_rcu_read_data[2] = pll_locked_i;
+ s_rcu_read_data[3] = pll_selected_i;
+ s_rcu_read_data[4] = |s_rcu_fault_q;
+ s_rcu_read_data[11:8] = s_active_cmd_q;
+ end
+ TimeoutRefCycles: begin
+ s_rcu_read_data = s_timeout_ref_q;
+ s_rcu_writable = 1'b1;
+ end
+ GateRequest: begin
+ s_rcu_read_data = s_gate_req_q;
+ s_rcu_writable = 1'b1;
+ end
+ GateStatus: s_rcu_read_data = {18'd0, gate_actual_i};
+ ResetRequest: begin
+ s_rcu_read_data = s_rst_req_q;
+ s_rcu_writable = 1'b1;
+ end
+ ResetDone: s_rcu_read_data = s_reset_done_q;
+ ResetCause: begin
+ s_rcu_read_data = s_reset_cause_q;
+ s_rcu_writable = 1'b1;
+ end
+ RcuFault: begin
+ s_rcu_read_data = s_rcu_fault_q;
+ s_rcu_writable = 1'b1;
+ end
+ RcuIrqState: begin
+ s_rcu_read_data = s_rcu_irq_state_q;
+ s_rcu_writable = 1'b1;
+ end
+ RcuIrqEnable: begin
+ s_rcu_read_data = s_rcu_irq_en_q;
+ s_rcu_writable = 1'b1;
+ end
+ XinHz: s_rcu_read_data = XinHzValue;
+ RefHz: s_rcu_read_data = 32'd24_000_000;
+ SysHz: s_rcu_read_data = s_sys_hz_q;
+ MemHz: s_rcu_read_data = s_mem_hz_q;
+ PclkHz: s_rcu_read_data = s_pclk_hz_q;
+ ClintHz: s_rcu_read_data = 32'd1_000_000;
+ TargetCapability: s_rcu_read_data = {18'd0, TargetMask};
+ ClkoutControl: begin
+ s_rcu_read_data = s_clkout_control_q;
+ s_rcu_writable = 1'b1;
+ end
+ default: s_rcu_valid = 1'b0;
+ endcase
+ end
+ // Validate before generating a write strobe, so every rejected operation
+ // returns PSLVERR and preserves staging as well as committed state.
+ always_comb begin
+ s_rcu_config_err = 1'b0;
+ unique case (s_rcu_offset)
+ ClockRequest: s_rcu_config_err = s_rcu_busy_q || u_rcu_dst.pwdata[31:2] != 0;
+ TimeoutRefCycles: s_rcu_config_err = s_rcu_busy_q || u_rcu_dst.pwdata == 0;
+ GateRequest:
+ s_rcu_config_err = s_rcu_busy_q || (u_rcu_dst.pwdata & ~{18'd0, TargetMask}) != 0;
+ ResetRequest:
+ s_rcu_config_err = s_rcu_busy_q || (u_rcu_dst.pwdata & ~{18'd0, TargetMask[13:1], 1'b0}) != 0;
+ ResetCause: s_rcu_config_err = u_rcu_dst.pwdata[31:6] != 0;
+ RcuFault: s_rcu_config_err = u_rcu_dst.pwdata[31:5] != 0;
+ RcuIrqState, RcuIrqEnable: s_rcu_config_err = u_rcu_dst.pwdata[31:2] != 0;
+ RcuCommand: begin
+ s_rcu_config_err = s_rcu_busy_q;
+ unique case (u_rcu_dst.pwdata)
+ 32'd1:
+ s_rcu_config_err |= s_clkout_control_q[2:0] != 0 || !clkout_idle_i ||
+ !(s_clk_req_q == 0 || (s_clk_req_q == 1 && !PllPresent) ||
+ (s_clk_req_q >= 2 && PllPresent && PllProfiles));
+ 32'd2: s_rcu_config_err |= TargetMask == 0 || (s_gate_req_q & ~{18'd0, TargetMask}) != 0;
+ 32'd4:
+ s_rcu_config_err |= s_rst_req_q == 0 ||
+ (s_rst_req_q & ~{18'd0, TargetMask[13:1], 1'b0}) != 0 ||
+ (s_rst_req_q & {18'd0, gate_actual_i}) != 0;
+ 32'd8: begin
+ end
+ default: s_rcu_config_err = 1'b1;
+ endcase
+ end
+ ClkoutControl:
+ s_rcu_config_err = s_rcu_busy_q ||
+ (u_rcu_dst.pwdata & 32'hff00_00f8) != 0 || u_rcu_dst.pwdata[2:0] > 4 ||
+ (u_rcu_dst.pwdata[2:0] != 0 && (s_clkout_control_q[2:0] != 0 ||
+ !clkout_idle_i || u_rcu_dst.pwdata[23:8] == 0 ||
+ {32'd0, s_clkout_hz} > (64'd48_000_000 * {48'd0, u_rcu_dst.pwdata[23:8]})));
+ default: begin
+ end
+ endcase
+ end
+ always_comb begin
+ s_clk_req_d = s_clk_req_q;
+ s_clk_cur_d = s_clk_cur_q;
+ s_rcu_busy_d = s_rcu_busy_q;
+ s_clk_latched_d = s_clk_latched_q;
+ s_system_reset_d = 1'b0;
+ s_periph_apply_d = 1'b0;
+ s_periph_reset_d = s_periph_reset_q;
+ s_periph_mask_d = s_periph_mask_q;
+ s_active_cmd_d = s_active_cmd_q;
+ s_clock_apply_d = 1'b0;
+ s_timeout_ref_d = s_timeout_ref_q;
+ s_gate_req_d = s_gate_req_q;
+ s_gate_stat_d = s_gate_stat_q;
+ s_rst_req_d = s_rst_req_q;
+ s_reset_done_d = s_reset_done_q;
+ s_reset_cause_d = s_reset_cause_q;
+ s_rcu_fault_d = s_rcu_fault_q;
+ s_rcu_irq_state_d = s_rcu_irq_state_q;
+ s_rcu_irq_en_d = s_rcu_irq_en_q;
+ s_clkout_control_d = s_clkout_control_q;
+ s_sys_hz_d = s_sys_hz_q;
+ s_mem_hz_d = s_mem_hz_q;
+ s_pclk_hz_d = s_pclk_hz_q;
+ if (s_rcu_write) begin
+ unique case (s_rcu_offset)
+ RcuCommand: begin
+ if (s_rcu_busy_q || !(u_rcu_dst.pwdata == 1 || u_rcu_dst.pwdata == 2 ||
+ u_rcu_dst.pwdata == 4 || u_rcu_dst.pwdata == 8)) begin
+ s_rcu_fault_d[0] = 1'b1;
+ s_rcu_irq_state_d[1] = 1'b1;
+ end else if (u_rcu_dst.pwdata == 1) begin
+ if (s_clkout_control_q[2:0] == 0 && clkout_idle_i &&
+ (s_clk_req_q == 0 || (s_clk_req_q == 1 && !PllPresent) ||
+ (s_clk_req_q >= 2 && PllPresent && PllProfiles))) begin
+ s_clk_latched_d = s_clk_req_q;
+ s_clock_apply_d = 1'b1;
+ s_rcu_busy_d = 1'b1;
+ s_active_cmd_d = 4'd1;
+ end else begin
+ s_rcu_fault_d[0] = 1'b1;
+ s_rcu_irq_state_d[1] = 1'b1;
+ end
+ end else if (u_rcu_dst.pwdata == 8) begin
+ s_system_reset_d = 1'b1;
+ s_reset_cause_d[1] = 1'b1;
+ s_rcu_busy_d = 1'b1;
+ s_active_cmd_d = 4'd8;
+ end else if (TargetMask != 0 && u_rcu_dst.pwdata == 2 &&
+ (s_gate_req_q & ~{18'd0, TargetMask}) == 0) begin
+ s_periph_mask_d = s_gate_req_q[13:0];
+ s_periph_reset_d = 1'b0;
+ s_periph_apply_d = 1'b1;
+ s_rcu_busy_d = 1'b1;
+ s_active_cmd_d = 4'd2;
+ end else if (u_rcu_dst.pwdata == 4 && s_rst_req_q != 0 &&
+ (s_rst_req_q & ~{18'd0, TargetMask[13:1], 1'b0}) == 0 &&
+ (s_rst_req_q & {18'd0, gate_actual_i}) == 0) begin
+ s_periph_mask_d = s_rst_req_q[13:0];
+ s_periph_reset_d = 1'b1;
+ s_periph_apply_d = 1'b1;
+ s_reset_done_d = 0;
+ s_rcu_busy_d = 1'b1;
+ s_active_cmd_d = 4'd4;
+ end else begin
+ s_rcu_fault_d[0] = 1'b1;
+ s_rcu_irq_state_d[1] = 1'b1;
+ end
+ end
+ ClockRequest: begin
+ if (u_rcu_dst.pwdata[31:2] == 0) s_clk_req_d = u_rcu_dst.pwdata[1:0];
+ else begin
+ s_rcu_fault_d[0] = 1'b1;
+ s_rcu_irq_state_d[1] = 1'b1;
+ end
+ end
+ TimeoutRefCycles: begin
+ if (u_rcu_dst.pwdata != 0) s_timeout_ref_d = u_rcu_dst.pwdata;
+ else begin
+ s_rcu_fault_d[0] = 1'b1;
+ s_rcu_irq_state_d[1] = 1'b1;
+ end
+ end
+ GateRequest: s_gate_req_d = u_rcu_dst.pwdata;
+ ResetRequest: s_rst_req_d = u_rcu_dst.pwdata;
+ ResetCause: s_reset_cause_d = s_reset_cause_q & ~u_rcu_dst.pwdata;
+ RcuFault: s_rcu_fault_d = s_rcu_fault_q & ~u_rcu_dst.pwdata;
+ RcuIrqState: s_rcu_irq_state_d = s_rcu_irq_state_q & ~u_rcu_dst.pwdata;
+ RcuIrqEnable: s_rcu_irq_en_d = u_rcu_dst.pwdata & 32'd3;
+ ClkoutControl: begin
+ if (s_rcu_busy_q || (u_rcu_dst.pwdata & 32'hff00_00f8) != 0 ||
+ u_rcu_dst.pwdata[2:0] > 4 ||
+ (u_rcu_dst.pwdata[2:0] != 0 &&
+ (s_clkout_control_q[2:0] != 0 || !clkout_idle_i || u_rcu_dst.pwdata[23:8] == 0 ||
+ {32'd0, s_clkout_hz} > (64'd48_000_000 * {48'd0, u_rcu_dst.pwdata[23:8]})))) begin
+ s_rcu_fault_d[0] = 1'b1;
+ s_rcu_irq_state_d[1] = 1'b1;
+ end else s_clkout_control_d = u_rcu_dst.pwdata;
+ end
+ default: begin
+ end
+ endcase
+ end
+ if (clock_done_i) begin
+ s_rcu_busy_d = 1'b0;
+ s_active_cmd_d = 0;
+ s_clk_cur_d = clock_current_i;
+ s_sys_hz_d = clock_current_i == 2 ? 32'd192_000_000 :
+ clock_current_i == 3 ? 32'd240_000_000 :
+ clock_current_i == 1 ? 32'(XinHzValue) : 32'd24_000_000;
+ s_mem_hz_d = s_sys_hz_d > 120_000_000 ? s_sys_hz_d / 2 : s_sys_hz_d;
+ s_pclk_hz_d = s_sys_hz_d > 120_000_000 ? s_sys_hz_d / 4 :
+ s_sys_hz_d > 60_000_000 ? s_sys_hz_d / 2 : s_sys_hz_d;
+ s_rcu_irq_state_d[0] = 1'b1;
+ end
+ if (periph_done_i) begin
+ s_rcu_busy_d = 1'b0;
+ s_active_cmd_d = 0;
+ s_reset_done_d = {18'd0, reset_done_i};
+ if (s_periph_reset_q && reset_done_i != 0) s_reset_cause_d[5] = 1'b1;
+ s_rcu_irq_state_d[0] = 1'b1;
+ end
+ if (warm_reset_i) begin
+ s_rcu_busy_d = 1'b0;
+ s_active_cmd_d = 0;
+ s_rcu_irq_en_d = 0;
+ s_rcu_irq_state_d[0] = 1'b0;
+ s_clk_cur_d = 0;
+ s_clk_req_d = 0;
+ s_gate_req_d = 0;
+ s_gate_stat_d = 0;
+ s_rst_req_d = 0;
+ s_reset_done_d = 0;
+ s_periph_mask_d = 0;
+ s_periph_reset_d = 1'b0;
+ s_clkout_control_d = 0;
+ s_sys_hz_d = 24_000_000;
+ s_mem_hz_d = 24_000_000;
+ s_pclk_hz_d = 24_000_000;
+ end
+ // Hardware set wins over W1C, including rejected APB operations.
+ if (s_rcu_req && s_rcu_err) begin
+ s_rcu_fault_d[0] = 1'b1;
+ s_rcu_irq_state_d[1] = 1'b1;
+ end
+ if (watchdog_reset_i) s_reset_cause_d[2] = 1'b1;
+ if (clock_fault_i[3]) s_reset_cause_d[3] = 1'b1;
+ if (hart_reset_i) s_reset_cause_d[4] = 1'b1;
+ if ((clock_fault_i | periph_fault_i) != 0) begin
+ s_rcu_fault_d = s_rcu_fault_d | {27'd0, clock_fault_i | periph_fault_i};
+ s_rcu_irq_state_d[1] = 1'b1;
+ end
+ end
+
+ assign s_fault_sum = {1'b0, s_fault_count_q} + {29'd0, fault_count_i};
always_comb begin
s_test_d = s_test_q;
s_fault_addr_d = s_fault_addr_q;
@@ -124,7 +573,7 @@ module tiny_sysctrl (
end
if (rtc_wake_i) s_wake_d = 1'b1;
if (fault_valid_i) begin
- if (s_fault_count_q != 32'hffff_ffff) s_fault_count_d = s_fault_count_q + 32'd1;
+ s_fault_count_d = s_fault_sum[32] ? 32'hffff_ffff : s_fault_sum[31:0];
if (!s_fault_stat_d[0]) begin
s_fault_stat_d = {(fault_resp_i == 2'b11) ? 3'd1 : 3'd2, fault_write_i, 1'b1};
s_fault_addr_d = fault_addr_i;
@@ -149,6 +598,147 @@ module tiny_sysctrl (
.dat_i (s_fault_addr_d),
.dat_o (s_fault_addr_q)
);
+ dffr #(
+ .DATA_WIDTH(2)
+ ) u_clock_latched (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_clk_latched_d),
+ .dat_o (s_clk_latched_q)
+ );
+ dffr #(
+ .DATA_WIDTH(20)
+ ) u_periph_command (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i ({s_active_cmd_d, s_periph_mask_d, s_periph_reset_d, s_periph_apply_d}),
+ .dat_o ({s_active_cmd_q, s_periph_mask_q, s_periph_reset_q, s_periph_apply_q})
+ );
+ dffr #(
+ .DATA_WIDTH(1)
+ ) u_system_reset (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_system_reset_d),
+ .dat_o (s_system_reset_q)
+ );
+ dffr #(2) u_rcu_clock_request (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_clk_req_d),
+ .dat_o (s_clk_req_q)
+ );
+ dffr #(2) u_rcu_clock_current (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_clk_cur_d),
+ .dat_o (s_clk_cur_q)
+ );
+ dffr #(1) u_rcu_busy (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_rcu_busy_d),
+ .dat_o (s_rcu_busy_q)
+ );
+ dffr #(1) u_clock_apply (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_clock_apply_d),
+ .dat_o (s_clock_apply_q)
+ );
+ dffrc #(
+ .DATA_WIDTH(32),
+ .RESET_VAL (32'd240000)
+ ) u_rcu_timeout (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_timeout_ref_d),
+ .dat_o (s_timeout_ref_q)
+ );
+ dffr #(32) u_rcu_gate_request (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_gate_req_d),
+ .dat_o (s_gate_req_q)
+ );
+ dffr #(32) u_rcu_gate_status (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_gate_stat_d),
+ .dat_o (s_gate_stat_q)
+ );
+ dffr #(32) u_rcu_reset_request (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_rst_req_d),
+ .dat_o (s_rst_req_q)
+ );
+ dffr #(32) u_rcu_reset_done (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_reset_done_d),
+ .dat_o (s_reset_done_q)
+ );
+ dffrc #(
+ .DATA_WIDTH(32),
+ .RESET_VAL (32'd1)
+ ) u_rcu_reset_cause (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_reset_cause_d),
+ .dat_o (s_reset_cause_q)
+ );
+ dffr #(32) u_rcu_fault (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_rcu_fault_d),
+ .dat_o (s_rcu_fault_q)
+ );
+ dffr #(32) u_rcu_irq_state (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_rcu_irq_state_d),
+ .dat_o (s_rcu_irq_state_q)
+ );
+ dffr #(32) u_rcu_irq_enable (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_rcu_irq_en_d),
+ .dat_o (s_rcu_irq_en_q)
+ );
+ dffr #(32) u_rcu_clkout (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_clkout_control_d),
+ .dat_o (s_clkout_control_q)
+ );
+ dffrc #(
+ .DATA_WIDTH(32),
+ .RESET_VAL (32'd24000000)
+ ) u_rcu_sys_hz (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_sys_hz_d),
+ .dat_o (s_sys_hz_q)
+ );
+ dffrc #(
+ .DATA_WIDTH(32),
+ .RESET_VAL (32'd24000000)
+ ) u_rcu_mem_hz (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_mem_hz_d),
+ .dat_o (s_mem_hz_q)
+ );
+ dffrc #(
+ .DATA_WIDTH(32),
+ .RESET_VAL (32'd24000000)
+ ) u_rcu_pclk_hz (
+ .clk_i (clk_ref_i),
+ .rst_n_i(rst_ref_n_i),
+ .dat_i (s_pclk_hz_d),
+ .dat_o (s_pclk_hz_q)
+ );
dffr #(
.DATA_WIDTH(32)
) u_fault_count_reg (
@@ -166,7 +756,7 @@ module tiny_sysctrl (
.dat_o (s_fault_stat_q)
);
dffr #(
- .DATA_WIDTH(1)
+ .DATA_WIDTH(2)
) u_fault_master_reg (
.clk_i (clk_i),
.rst_n_i(rst_n_i),
diff --git a/scripts/check_clock_reset_domains.py b/scripts/check_clock_reset_domains.py
index 6d0acbe7..ed6ca33b 100644
--- a/scripts/check_clock_reset_domains.py
+++ b/scripts/check_clock_reset_domains.py
@@ -16,6 +16,8 @@ CDC_PRIMITIVES = {
"async_fifo",
"async_reqack",
"axi4_async_bridge",
+ "tiny_axi4_async_bridge",
+ "tiny_apb_async_bridge",
"cdc_2phase",
"cdc_fifo",
"cdc_sync",
@@ -129,6 +131,21 @@ def validate(document_path: Path, root: Path) -> None:
raw_crossings = document.get("crossings")
if not isinstance(raw_crossings, list):
raise ValueError("crossings must be a list")
+ generated = document.get("generated_apb_crossings")
+ if generated is not None:
+ if (document.get("clock_contract") != "TINY-R2-P7" or
+ generated.get("generator") != "scripts/generate_tiny.py" or
+ generated.get("primitive") != "tiny_apb_async_bridge" or
+ generated.get("source") != "system"):
+ raise ValueError("unrecognized generated APB crossing contract")
+ expected = {"aon": ["clint", "archinfo"], "memory": ["xpi"],
+ "peripheral": ["gpio", "uart0", "timer0", "timer1", "i2c0", "dma",
+ "pwm", "rtc", "wdg", "rng", "crc", "ws2812", "crypto", "i2s"]}
+ if (generated.get("destinations") != expected or
+ generated.get("same_clock_isolation") != ["sdio"] or
+ generated.get("native_system") != ["sysctrl", "sram"] or
+ not set(expected).issubset(names)):
+ raise ValueError("P7 generated APB domain assignment mismatch")
crossing_names: set[str] = set()
for index, value in enumerate(raw_crossings):
field = f"crossings[{index}]"
diff --git a/scripts/check_simulation.py b/scripts/check_simulation.py
index 2197685e..0a4a5012 100644
--- a/scripts/check_simulation.py
+++ b/scripts/check_simulation.py
@@ -39,6 +39,19 @@ def main() -> int:
if default_failure:
rejected.append(default_failure.group(0))
+ flow_result_path = args.result.with_name("result-sim.json")
+ flow_status = None
+ if flow_result_path.is_file():
+ try:
+ flow_result = json.loads(flow_result_path.read_text(encoding="utf-8"))
+ except (OSError, json.JSONDecodeError):
+ flow_result = {}
+ flow_status = flow_result.get("status", "invalid")
+ # PIDs are namespace-local and can be reused. A durable terminal
+ # verdict, not a liveness guess from this process, establishes success.
+ if flow_status != "passed" or flow_result.get("exit_code") != 0:
+ missing.append("successful terminal run_flow result")
+
passed = not missing and not rejected
report = {
"schema_version": 1,
@@ -48,6 +61,8 @@ def main() -> int:
"missing_markers": missing,
"rejected_matches": sorted(set(rejected)),
}
+ if flow_status is not None:
+ report["flow_status"] = flow_status
atomic_write(args.result, json.dumps(report, indent=2, sort_keys=True) + "\n")
print(f"simulation check: {report['status']} ({args.result.resolve()})")
return int(not passed)
diff --git a/scripts/generate_tiny.py b/scripts/generate_tiny.py
index 8c359363..22d04c40 100644
--- a/scripts/generate_tiny.py
+++ b/scripts/generate_tiny.py
@@ -16,13 +16,36 @@ from scripts.rtl.generate_pin_map import generate as generate_pin_map # noqa: E
from scripts.setup_helpers import atomic_write # noqa: E402
TINY = ROOT / "rtl/tiny"
+RCU_TARGETS = {"gpio": 1, "uart0": 2, "i2c0": 3, "timer0": 4, "timer1": 5,
+ "pwm": 6, "i2s": 7, "sdio": 8, "dma": 9, "ws2812": 10,
+ "rng": 11, "crc": 12, "crypto": 13}
def read_topology(path: Path, memory_map: Path) -> dict:
document = json.loads(path.read_text())
regions = {r["symbol"] for r in json.loads(memory_map.read_text())["regions"]}
- if document.get("schema_version") != 1 or document.get("dma_channels") != 4:
- raise ValueError("Tiny requires topology schema 1 and four DMA channels")
+ if (document.get("schema_version") != 1 or document.get("dma_channels") != 8
+ or document.get("dma_request_mask") != "0x31ff"):
+ raise ValueError("Tiny P6 requires eight DMA channels and request mask 0x31ff")
+ if document.get("cpu") != {"module": "hazard3_cpu_2port", "local_ports": 2,
+ "data_outstanding": 1}:
+ raise ValueError("Tiny P4 requires independent I/D and one ordered data operation")
+ memory = document.get("main_sram", {})
+ if any(memory.get(key) != value for key, value in {
+ "groups": 4, "group_bytes": 32768, "macros_per_group": 8,
+ "macro_bytes": 4096, "base": "0x30000000",
+ "macro_instance": "u_soc.u_sram.gen_group[{group}].u_group.gen_bank[{bank}].u_ram.u_mem",
+ "external_attachment": "per-target-P5",
+ }.items()):
+ raise ValueError("Tiny SRAM geometry/P5 attachment differs from its frozen contract")
+ if document.get("fabric") != {
+ "owners": ["cpu_slow", "central_dma", "sdio"], "active_owners": [0, 1, 2],
+ "cpu_combined_outstanding": 1, "dma_direction_outstanding": 1,
+ "sdio_direction_outstanding": 1, "target_combined_outstanding": 1,
+ "sram_targets": [0, 1, 2, 3], "xpi_target": 4, "apb_target_base": 5,
+ "decode_error_target": 25, "protocol_error_target": 26, "target_count": 27,
+ }:
+ raise ValueError("Tiny P6 requires three active owners and 27 targets")
if document.get("irq_vector_width") != 32 or document.get("external_irq_count") != 30:
raise ValueError("Tiny requires a 32-bit IRQ vector and 30 external inputs")
names: set[str] = set()
@@ -42,14 +65,24 @@ def read_topology(path: Path, memory_map: Path) -> dict:
assigned.add(region)
if assigned != {r for r in regions if r.startswith("APB4_")}:
raise ValueError("Tiny topology must route every APB region")
- if len(document["apb_targets"]) != 16:
- raise ValueError("Tiny's fixed APB bridge requires sixteen targets")
+ if len(document["apb_targets"]) != 20:
+ raise ValueError("Tiny P6 requires twenty independent APB targets")
target_names = [target["name"] for target in document["apb_targets"]]
- if len(set(target_names)) != 16 or any(re.fullmatch(r"[a-z][a-z0-9_]*", name) is None
+ if len(set(target_names)) != 20 or any(re.fullmatch(r"[a-z][a-z0-9_]*", name) is None
for name in target_names):
raise ValueError("Tiny APB target names must be unique identifiers")
if sorted(item["pin"] for item in document["gpio_alt_functions"]) != list(range(32)):
raise ValueError("Tiny GPIO map must contain all 32 pins exactly once")
+ inputs = [signal for pin in document["gpio_alt_functions"] for mode in ("alt0", "alt1")
+ for signal in pin[mode]["inputs"]]
+ if len(inputs) != len(set(inputs)):
+ raise ValueError("Tiny peripheral inputs require one exclusive route")
+ if document.get("private_crypto") != {
+ "banks": 6, "bank_bytes": 4096,
+ "macro_instance": "u_soc.u_crypto.u_storage.gen_bank[{bank}].gen_macro.u_sram.u_mem",
+ "clock_domain": "peripheral", "logical_domain": "PCLK",
+ }:
+ raise ValueError("Tiny requires six private Crypto banks on PCLK")
return document
@@ -65,7 +98,23 @@ def generate_bindings(document: dict, output: Path, pdk: str = "IHP130",
have_pll: bool = False) -> None:
platform = platform_values(pdk, have_pll)
comment = "// Generated by scripts/generate_tiny.py; do not edit.\n"
- irq = [comment, "always_comb begin", " s_irq = '0;"]
+ constants = [comment, "`ifndef RETROSOC_TINY__TOPOLOGY_SVH",
+ "`define RETROSOC_TINY__TOPOLOGY_SVH",
+ f"`define RETROSOC_TINY__TARGET_COUNT {document['fabric']['target_count']}",
+ f"`define RETROSOC_TINY__LEAF_COUNT {len(document['apb_targets']) + 1}",
+ f"`define RETROSOC_TINY__DECODE_ERROR {document['fabric']['decode_error_target']}",
+ f"`define RETROSOC_TINY__PROTOCOL_ERROR {document['fabric']['protocol_error_target']}"]
+ constants += [f"`define RETROSOC_TINY__TARGET_{target['name'].upper()} {index + 5}"
+ for index, target in enumerate(document["apb_targets"])]
+ constants.append("`endif\n")
+ atomic_write(output / "rtl/tiny_topology.svh", "\n".join(constants))
+ # Keep board pulls independently resolved. A packed tri1 with partial
+ # external drivers loses its weak pulls in the locked Verilator 5.046.
+ board = [comment, *[f"tri1 s_gpio_{pin};" for pin in range(document["gpio_pins"])]]
+ board.append("wire [31:0] s_gpio = {" +
+ ", ".join(f"s_gpio_{pin}" for pin in reversed(range(32))) + "};\n")
+ atomic_write(output / "rtl/tiny_tb_gpio.svh", "\n".join(board))
+ irq = [comment, "always_comb begin", " s_irq_async = '0;"]
header = ["#ifndef RETROSOC_GENERATED_IRQ_METADATA_H",
"#define RETROSOC_GENERATED_IRQ_METADATA_H", "#include ",
"#define RS_SOC_IRQ_VECTOR_WIDTH UINT32_C(32)",
@@ -73,7 +122,7 @@ def generate_bindings(document: dict, output: Path, pdk: str = "IHP130",
f"#define RS_SOC_ALLOCATED_IRQ_COUNT UINT32_C({len(document['interrupts']) - 2})"]
for item in document["interrupts"]:
bit, name = item["core_bit"], item["name"].upper()
- irq.append(f" s_irq[{bit}] = {item['signal']};")
+ irq.append(f" s_irq_async[{bit}] = {item['signal']};")
header.append(f"#define RS_SOC_IRQ_{name} UINT32_C({bit})")
if bit >= 2:
header.append(f"#define RS_SOC_EXT_IRQ_{name} UINT32_C({bit - 2})")
@@ -90,7 +139,15 @@ def generate_bindings(document: dict, output: Path, pdk: str = "IHP130",
"#ifndef RETROSOC_GENERATED_SOC_FEATURES_H\n"
"#define RETROSOC_GENERATED_SOC_FEATURES_H\n"
"#define RS_SOC_TINY 1\n#define RS_SOC_HAS_PSRAM 0U\n"
- "#define RS_SOC_DMA_CHANNELS 4U\n#define RS_SOC_ID 0x54494E59U\n"
+ "#define RS_SOC_DMA_CHANNELS 8U\n#define RS_SOC_ID 0x54494E59U\n"
+ "#define RS_SOC_DMA_REQUEST_MASK 0x31FFU\n"
+ "#define RS_SOC_HAS_I2C1 0U\n#define RS_SOC_HAS_SDIO1 0U\n"
+ "#define RS_SOC_HAS_UART0 1U\n#define RS_SOC_HAS_UART1 0U\n"
+ "#define RS_SOC_HAS_I2C0 1U\n"
+ "#define RS_SOC_HAS_RNG 1U\n#define RS_SOC_HAS_CRC 1U\n"
+ "#define RS_SOC_HAS_WS2812 1U\n#define RS_SOC_HAS_CRYPTO 1U\n"
+ "#define RS_SOC_HAS_I2S 1U\n#define RS_SOC_HAS_SDIO0 1U\n"
+ "#define RS_SOC_HAS_DVP 0U\n#define RS_SOC_RNG_QUALIFIED 0U\n"
f"#define RS_SOC_TECHNOLOGY_ID 0x{platform['technology']:08X}U\n"
f"#define RS_SOC_PLL_PRESENT {platform['pll_present']}U\n"
f"#define RS_SOC_GPIO_HAS_PULLS {platform['gpio_pulls']}U\n#endif\n")
@@ -106,33 +163,60 @@ def generate_bindings(document: dict, output: Path, pdk: str = "IHP130",
gpio.extend((f"assign u_gpio_if.{mode}_do_i[{pin}] = {value['do']};",
f"assign u_gpio_if.{mode}_oe_i[{pin}] = {value['oe']};"))
atomic_write(output / "rtl/tiny_gpio.svh", "\n".join(gpio) + "\n")
- ports, declarations, routes, decode = [], [comment], [comment], [comment]
+ declarations, bridges, decode = [comment], [comment], [comment]
reset_assignments = []
for index, target in enumerate(document["apb_targets"]):
name = target["name"]
- ports.append(f"apb4_if.master {name}")
+ clock = ("s_ref_clk" if name in ("clint", "archinfo") else
+ "s_backend_mem_clk" if name == "xpi" else
+ "s_exec_clk" if name in ("sysctrl", "sram", "sdio") else "s_backend_pclk")
+ target_bit = RCU_TARGETS.get(name)
+ ip_clock = f"s_{name}_clk" if target_bit is not None else clock
+ if target_bit is not None:
+ declarations.append(f"logic {ip_clock};")
declarations.append(f"logic s_{name}_rst_n;")
- declarations.append(f"apb4_if u_{name}_apb4_if (.pclk(clk_i), .presetn(s_{name}_rst_n));")
- reset_assignments.append(f"assign s_{name}_rst_n = s_leaf_rst_n[{index + 1}];")
- routes.append(f".{name}(u_{name}_apb4_if)")
- decode.append(f"if ({' || '.join(f'`SOC_ADDR_IS_{r}(s_addr_q)' for r in target['regions'])}) "
- f"s_select = {len(document['apb_targets'])}'b{1 << index:016b};")
- atomic_write(output / "rtl/tiny_apb_ports.svh", ",\n".join(ports) + "\n")
+ declarations.append(f"apb4_if u_{name}_apb4_if (.pclk({ip_clock}), .presetn(s_{name}_rst_n));")
+ if target_bit is None:
+ reset_assignments.append(f"assign s_{name}_rst_n = s_leaf_rst_n[{index + 1}];")
+ else:
+ reset_assignments.append(f"(* keep_hierarchy = 1 *) tiny_peripheral_leaf u_{name}_lifecycle ("
+ f".clk_i({clock}), .rst_n_i(s_leaf_rst_n[{index + 1}]), "
+ f".gate_i(s_gate_request[{target_bit}]), "
+ f".reset_i(s_local_reset[{target_bit}]), "
+ f".clk_o({ip_clock}), .rst_n_o(s_{name}_rst_n), "
+ f".gated_o(s_actual_gate[{target_bit}]), "
+ f".held_o(s_reset_held[{target_bit}]));")
+ extra_ready = " && s_i2s_reset_ready" if name == "i2s" else ""
+ if name == "dma":
+ # CDC-ready outputs are observed in SYS, matching APB readiness.
+ extra_ready = " && s_dma_ready_sys && s_dma_idle_sys"
+ reset_assignments.append(f"assign s_reset_ready[{target_bit}] = s_{name}_rst_n && "
+ f"s_apb_cdc_ready[{index}]{extra_ready};")
+ reset_assignments.append(f"assign s_leaf_clk[{index + 1}] = {clock};")
+ bridges.append(f"assign s_target_rst_n[{index + 5}] = s_fabric_rst_n;")
+ crossed = clock != "s_exec_clk" or target_bit is not None
+ endpoint = f"u_{name}_sys_apb4_if" if crossed else f"u_{name}_apb4_if"
+ bridges.append(f"tiny_axi42apb4 u_{name}_bridge ("
+ f".clk_i(s_exec_clk), .rst_n_i(s_fabric_rst_n), "
+ f".axi4(u_targets_axi4_if[{index + 5}]), .apb4({endpoint}));")
+ if crossed:
+ disabled = f"s_disabled_sys[{target_bit}]" if target_bit is not None else "1'b0"
+ declarations.append(f"apb4_if {endpoint} (.pclk(s_exec_clk), .presetn(s_fabric_rst_n));")
+ bridges.append(f"tiny_apb_async_bridge u_{name}_cfg_cdc ("
+ f".src_clk_i(s_exec_clk), .src_rst_n_i(s_fabric_rst_n), "
+ f".dst_clk_i({ip_clock}), .dst_rst_n_i(s_{name}_rst_n), "
+ f".disabled_i({disabled}), "
+ f".idle_o(s_apb_cdc_idle[{index}]), "
+ f".ready_o(s_apb_cdc_ready[{index}]), "
+ f".src_apb4({endpoint}), .dst_apb4(u_{name}_apb4_if));")
+ else:
+ bridges.append(f"assign s_apb_cdc_idle[{index}] = 1'b1;")
+ bridges.append(f"assign s_apb_cdc_ready[{index}] = 1'b1;")
+ decode.append(f"if ({' || '.join(f'`SOC_ADDR_IS_{r}(addr_i)' for r in target['regions'])}) "
+ f"return 5'd{index + 5};")
atomic_write(output / "rtl/tiny_apb_interfaces.svh", "\n".join(declarations + reset_assignments) + "\n")
- atomic_write(output / "rtl/tiny_apb_connections.svh", ",\n".join(routes[1:]) + "\n")
- atomic_write(output / "rtl/tiny_apb_decode.svh", "\n".join(decode) + "\n")
- wiring = [comment]
- for index, target in enumerate(document["apb_targets"]):
- name = target["name"]
- for field, value in {"paddr": "s_addr_q", "pwrite": "s_write_q",
- "pwdata": "s_wdata_q", "pstrb": "s_wstrb_q", "pprot": "3'd0",
- "psel": f"s_select[{index}] && s_access",
- "penable": "s_enable"}.items():
- wiring.append(f"assign {name}.{field} = {value};")
- wiring.extend((f"assign s_ready[{index}] = {name}.pready;",
- f"assign s_error[{index}] = {name}.pslverr;",
- f"assign s_rdata[{index}] = {name}.prdata;"))
- atomic_write(output / "rtl/tiny_apb_wiring.svh", "\n".join(wiring) + "\n")
+ atomic_write(output / "rtl/tiny_apb_bridges.svh", "\n".join(bridges) + "\n")
+ atomic_write(output / "rtl/tiny_fabric_decode.svh", "\n".join(decode) + "\n")
def generate_filelists(output: Path, generated: Path, defines: list[str],
@@ -141,7 +225,7 @@ 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"}
+ "cache": ROOT / ".cache", "tests": ROOT / "tests"}
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 50795283..fe896652 100644
--- a/scripts/run_c_tests.py
+++ b/scripts/run_c_tests.py
@@ -43,6 +43,7 @@ TEST_SOURCES = (
"crt/src/hal/fabric_monitor.c",
"crt/src/hal/sysctrl.c",
"crt/src/hal/clock.c",
+ "crt/src/hal/rcu.c",
"crt/src/hal/resource.c",
"crt/src/hal/extension.c",
"crt/src/hal/user_ip.c",
@@ -122,6 +123,7 @@ def main() -> int:
"-DRS_NPU_TEST_MMIO",
"-DRS_FABRIC_MONITOR_TEST_MMIO",
"-DRS_RESOURCE_TEST_MMIO",
+ "-DRS_TINY_RCU_TEST_MMIO",
"-I",
str(memory_map_root / "include"),
"-I",
diff --git a/scripts/run_flow.py b/scripts/run_flow.py
index 26dcddc5..91ba1867 100644
--- a/scripts/run_flow.py
+++ b/scripts/run_flow.py
@@ -124,24 +124,22 @@ def main() -> int:
args.result.parent.mkdir(parents=True, exist_ok=True)
started = datetime.now(timezone.utc)
start_clock = time.monotonic()
- atomic_write(
- args.result,
- json.dumps(
- {
- "schema_version": 1,
- "tool": args.tool,
- "command": command,
- "started_at": started.isoformat(),
- "status": "running",
- "log": str(args.log.resolve()),
- },
- indent=2,
- sort_keys=True,
- )
- + "\n",
- )
+ running_result = {
+ "schema_version": 1,
+ "tool": args.tool,
+ "command": command,
+ "started_at": started.isoformat(),
+ "status": "running",
+ "parent_pid": os.getpid(),
+ "log": str(args.log.resolve()),
+ }
+ atomic_write(args.result, json.dumps(running_result, indent=2, sort_keys=True) + "\n")
def interrupt_flow(_signum: int, _frame: object) -> None:
+ # A launcher can deliver SIGINT followed by SIGTERM. Do not let a
+ # second signal interrupt process-group cleanup or result persistence.
+ signal.signal(signal.SIGINT, signal.SIG_IGN)
+ signal.signal(signal.SIGTERM, signal.SIG_IGN)
raise KeyboardInterrupt
signal.signal(signal.SIGINT, interrupt_flow)
@@ -169,6 +167,8 @@ def main() -> int:
pass_fds=jobserver_fds(),
start_new_session=True,
)
+ running_result["child_pid"] = process.pid
+ atomic_write(args.result, json.dumps(running_result, indent=2, sort_keys=True) + "\n")
assert process.stdout is not None
if args.stream_bytes:
while chunk := os.read(process.stdout.fileno(), 4096):
@@ -193,11 +193,25 @@ def main() -> int:
returncode = process.wait()
except KeyboardInterrupt:
error_message = "interrupted"
+ # Persist cancellation before the bounded cleanup wait. If the outer
+ # executor subsequently kills this wrapper, the file must not imply
+ # that the job is still running or that its child finished normally.
+ cancelled = dict(running_result, status="interrupted", exit_code=130,
+ interrupted_at=datetime.now(timezone.utc).isoformat(),
+ child_cleanup_pending=process is not None)
+ atomic_write(args.result, json.dumps(cancelled, indent=2, sort_keys=True) + "\n")
if process is not None:
terminate_process_group(process)
returncode = 130
except (OSError, subprocess.SubprocessError) as error:
error_message = str(error)
+ if process is not None:
+ terminate_process_group(process)
+ except BaseException as error:
+ error_message = f"unexpected {type(error).__name__}: {error}"
+ if process is not None:
+ terminate_process_group(process)
+ returncode = 1
finished = datetime.now(timezone.utc)
result = {
@@ -211,8 +225,13 @@ def main() -> int:
"peak_rss_kib": resource.getrusage(resource.RUSAGE_CHILDREN).ru_maxrss,
"exit_code": returncode,
"status": "interrupted" if returncode == 130 else ("passed" if returncode == 0 else "failed"),
+ "parent_pid": os.getpid(),
"log": str(args.log.resolve()),
}
+ if process is not None:
+ result["child_pid"] = process.pid
+ result["child_exit_code"] = process.returncode
+ result["child_cleanup_pending"] = process.returncode is None
if error_message:
result["error"] = error_message
if args.success_marker:
diff --git a/scripts/setup_tiny.py b/scripts/setup_tiny.py
index 826c9697..1af49294 100644
--- a/scripts/setup_tiny.py
+++ b/scripts/setup_tiny.py
@@ -15,7 +15,7 @@ from scripts.setup_helpers import ensure_git_repo # noqa: E402
TINY_SOURCES = (
"hazard3", "cluster_common", "cluster_archinfo", "cluster_pwm",
- "cluster_rtc", "cluster_wdg", "pdk_ihp130",
+ "cluster_rtc", "cluster_wdg", "cluster_rng", "cluster_crc", "pdk_ihp130",
)
diff --git a/scripts/tiny_ics55_platform.py b/scripts/tiny_ics55_platform.py
index 0ddba621..281765c6 100644
--- a/scripts/tiny_ics55_platform.py
+++ b/scripts/tiny_ics55_platform.py
@@ -114,10 +114,12 @@ def verify_locked_views(root: Path) -> dict:
return verified
-def inspect_netlist(document: dict) -> dict:
+def expand_netlist(document: dict) -> tuple:
+ """Resolve hierarchy aliases without assigning a product clock contract."""
modules = document["modules"]
top = "retrosoc_tiny_asic"
leaves = []
+ boundaries = {}
aliases = {}
def canonical(bit):
@@ -159,6 +161,7 @@ def inspect_netlist(document: dict) -> dict:
path = f"{scope}/{name}"
attrs = modules.get(kind, {}).get("attributes", {})
if kind in modules and not int(str(attrs.get("blackbox", "0")), 2):
+ boundaries[path] = {"path": path, "type": kind, "ports": ports}
visit(kind, path, ports)
else:
directions = cell.get("port_directions", {
@@ -168,21 +171,44 @@ def inspect_netlist(document: dict) -> dict:
"directions": directions})
visit(top, top, {})
- for c in leaves:
+ for c in [*leaves, *boundaries.values()]:
for port, bits in c["ports"].items():
c["ports"][port] = [canonical(bit) for bit in bits]
+ return modules, top, leaves, canonical, boundaries
+
+
+def inspect_netlist(document: dict) -> dict:
+ # Keep the original P1 parked-PLL/direct-SYS contract fail-closed.
+ modules, top, leaves, canonical, _ = expand_netlist(document)
clock = modules[top]["cells"]["u_clock_buffer"]["connections"]["clk_o"]
if len(clock) != 1 or not isinstance(clock[0], int):
raise ValueError("missing live SYS clock")
system = canonical(f"{top}:{clock[0]}")
if system in ("0", "1", "x", "z"):
raise ValueError("SYS is not a live clock net")
- ram = [c for c in leaves if c["type"] == RAM]
+ all_ram = [c for c in leaves if c["type"] == RAM]
+ crypto_names = {f"u_soc.u_crypto.u_storage.gen_bank[{n}].gen_macro.u_sram.u_mem"
+ for n in range(6)}
+ crypto = [c for c in all_ram if any(c["path"].replace("/", ".").endswith("." + name)
+ for name in crypto_names)]
+ observed_crypto = {name for name in crypto_names for c in crypto
+ if c["path"].replace("/", ".").endswith("." + name)}
+ if len(all_ram) != 38 or len(crypto) != 6 or observed_crypto != crypto_names:
+ raise ValueError("Tiny P6 requires exactly 32 main SRAM and six private Crypto macros")
+ if any(c["ports"].get("CLK") != [system] for c in crypto):
+ raise ValueError("private Crypto SRAM does not use SAFE24 PCLK")
+ ram = [c for c in all_ram if c not in crypto]
pll = [c for c in leaves if c["type"] == "PLL_TOP"]
if any(c["type"] == "ics55_ecos_sram_4096x32_m8" for c in leaves):
raise ValueError("Tiny instantiated the unsupported 16 KiB SRAM interface stub")
if len(ram) != 32 or len(pll) != 1:
raise ValueError("expected 32 main SRAM macros and one PLL")
+ expected_ram = {f"u_soc.u_sram.gen_group[{g}].u_group.gen_bank[{n}].u_ram.u_mem"
+ for g in range(4) for n in range(8)}
+ observed_ram = {next((name for name in expected_ram
+ if c["path"].replace("/", ".").endswith("." + name)), "") for c in ram}
+ if observed_ram != expected_ram:
+ raise ValueError("Tiny P4 SRAM group/macro hierarchy is missing or stale")
if any(c["ports"].get("CLK") != [system] for c in ram):
raise ValueError("main SRAM does not share direct SYS")
constants = {"0": 0, "1": 1}
@@ -202,7 +228,7 @@ def inspect_netlist(document: dict) -> dict:
enable = pll[0]["ports"].get("EN", [])
if len(enable) != 1 or constants.get(enable[0]) != 0:
raise ValueError("PLL enable is not proven constant zero")
- cpu = [c for c in leaves if "u_hazard3_cpu_1port" in c["path"]
+ cpu = [c for c in leaves if "u_hazard3_cpu_2port" in c["path"]
and ("CK" in c["ports"] or "CKN" in c["ports"])]
if not cpu or any(c["ports"].get("CK", c["ports"].get("CKN")) != [system] for c in cpu):
raise ValueError("CPU sequential clocks do not share direct SYS")
@@ -213,6 +239,7 @@ def inspect_netlist(document: dict) -> dict:
"system_clock_source": {"port": "extclk_i_pad", "net": reference, "path": clock_path},
"cpu_clocked_cells": len(cpu),
"sram": [{"path": c["path"], "clock": c["ports"]["CLK"]} for c in ram],
+ "crypto_sram": [{"path": c["path"], "clock": c["ports"]["CLK"]} for c in crypto],
"pll": pll[0], "pll_enable": 0, "qualification": "SAFE24 digital connectivity only"}
diff --git a/scripts/tiny_r2_p4.py b/scripts/tiny_r2_p4.py
new file mode 100644
index 00000000..455645e0
--- /dev/null
+++ b/scripts/tiny_r2_p4.py
@@ -0,0 +1,255 @@
+#!/usr/bin/env python3
+"""Replay immutable P3 images on P4 RTL without relabeling image provenance."""
+from __future__ import annotations
+
+import argparse
+from pathlib import Path
+import sys
+import subprocess
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT))
+from scripts import tiny_r2_baseline as b # noqa: E402
+from scripts import tiny_r2_p3 as p3 # noqa: E402
+
+PHASE = "TINY-R2-P4"
+HARDWARE_KEYS = ("SOC", "PDK", "HAVE_PLL", "HAVE_SRAM_IF", "HAVE_SRAM_MACRO",
+ "SRAM_SIZE_KIB", "PDK_BEHAV", "HAVE_HP", "EXT_CLK_HZ", "AUD_CLK_HZ",
+ "CLINT_TIMEBASE_HZ", "MGMT_CPU_CLK_HZ", "JTAG_IDCODE")
+LOCAL_FIELDS = {"window", "port", "accepted", "completed", "latency_max", "admission_wait"}
+BANK_FIELDS = {"window", "group", "issues_i", "issues_d", "issues_external", "conflicts",
+ "wait_i", "wait_d", "wait_external"}
+
+
+def inputs(root: Path, variant: Path) -> dict:
+ result = p3.inputs(root, variant)
+ # Publication bindings are tested source consumers, not generated PDFs.
+ names = subprocess.check_output(
+ ["git", "ls-files", "--cached", "--others", "--exclude-standard", "-z", "--", "publications"],
+ cwd=root, text=True).split("\0")
+ for name in names:
+ path = root / name
+ if name and path.is_file() and path.suffix in (".py", ".json", ".typ"):
+ result[name] = p3.sha256(path)
+ return result
+
+
+def compatible_hardware(image: dict, model: dict) -> None:
+ a, z = (c["configuration"] for c in (image, model))
+ if any(key not in a or key not in z or a[key] != z[key] for key in HARDWARE_KEYS):
+ raise ValueError("retained image and candidate hardware configuration differ")
+
+
+def parse_log(log: str, flow: dict) -> dict:
+ samples = p3.parse_log(log, flow)
+ rows = [b.parse_fields(line, LOCAL_FIELDS) for line in log.splitlines()
+ if line.startswith("R2_P4_LOCAL ")]
+ rows.sort(key=lambda row: (row["window"], row["port"]))
+ if [(r["window"], r["port"]) for r in rows] != [(w, p) for w in range(1, 9) for p in range(2)]:
+ raise ValueError("missing or duplicate P4 local observations")
+ for row in rows:
+ if row["completed"] > row["accepted"] or row["accepted"] - row["completed"] > 1:
+ raise ValueError("P4 local request/response accounting mismatch")
+ samples["local"] = rows
+ banks = [b.parse_fields(line, BANK_FIELDS) for line in log.splitlines()
+ if line.startswith("R2_P4_BANK ")]
+ banks.sort(key=lambda row: (row["window"], row["group"]))
+ if [(r["window"], r["group"]) for r in banks] != [(w, g) for w in range(1, 9) for g in range(4)]:
+ raise ValueError("missing or duplicate P4 bank observations")
+ samples["banks"] = banks
+ return samples
+
+
+def reference_cases(path: Path, names: list[str]) -> list[dict]:
+ report = b.read_json(path)
+ if report.get("phase") != p3.PHASE or report.get("status") != "measurement_complete":
+ raise ValueError("reference is not a completed P3 measurement report")
+ matrix = b.read_json(b.check_artifact(report["matrix"]))
+ if matrix.get("status") != "passed":
+ raise ValueError("reference matrix did not pass")
+ if not names or len(set(names)) != len(names):
+ raise ValueError("select distinct retained cases")
+ result = []
+ for name in names:
+ matching = [c for c in report["cases"] if c["name"] == name]
+ original = [c for c in matrix["cases"] if c["name"] == name]
+ if len(matching) != 1 or len(original) != 1 or matching[0]["image"] != original[0]["image"]:
+ raise ValueError("unknown or mismatched retained case")
+ case = matching[0]
+ p3.validate_image(b.check_artifact(case["image"]))
+ # Recheck the original logs, not just a report's claimed numeric values.
+ samples = p3.validate_simulation(original[0]["simulations"]["verilator"],
+ case["image"], "verilator")
+ if samples != case["samples"]:
+ raise ValueError("reference report differs from its retained execution")
+ result.append(case)
+ return result
+
+
+def replay(args) -> None:
+ root, variant = args.root, args.variant_root
+ parent = variant / "meta/tiny-r2-p4/replay" / args.simulator
+ directory = b.new_attempt(parent, "attempt-")
+ record = {"phase": PHASE, "status": "running", "repetitions": 1, "cases": []}
+ b.dump(parent / "latest.json", {"path": str(directory / "record.json")})
+ try:
+ reference = b.artifact(args.reference)
+ cases = reference_cases(args.reference, args.cases.split(","))
+ configuration = p3.configuration(variant)
+ for case in cases:
+ compatible_hardware(b.read_json(b.check_artifact(case["image"]))["configuration"],
+ configuration)
+ before = inputs(root, variant)
+ tools = b.tools_identity(root)
+ command = args.command[1:] if args.command[:1] == ["--"] else args.command
+ if not command:
+ raise ValueError("fresh model compilation command required")
+ p3.execute_make(root, directory, "compile-model", command)
+ if before != inputs(root, variant):
+ raise ValueError("source changed while compiling candidate model")
+ runtime = b.runtime_inputs(root, variant, args.simulator)
+ model = variant / "sim" / args.simulator / ("emu" if args.simulator == "verilator" else "simv")
+ record.update(reference=reference, configuration=configuration, tools=tools,
+ model=b.artifact(model), source_digest=b.digest_map(before),
+ source=b.snapshot(root, directory / "inputs", before | runtime),
+ compilation={s: b.artifact(directory / f"compile-model.{s}") for s in ("json", "log")})
+ launch = [str(model)]
+ if args.simulator == "iverilog":
+ record["runtime_tool"] = b.vvp_identity(args.vvp, tools["iverilog"])
+ launch.insert(0, record["runtime_tool"]["executable"]["path"])
+ for case in cases:
+ image = p3.validate_image(b.check_artifact(case["image"]))
+ begin, end = image["layout"]["descriptor_range"]
+ cmd = [*launch, "+firmware=" + image["artifacts"]["tiny_p3.hex"]["path"],
+ "+tiny_r2_p3", f"+baseline_tcd_base={begin:x}", f"+baseline_tcd_end={end:x}",
+ "+max_cycles=100000000"]
+ label = case["name"]
+ flow = b.execute(root, directory, label,
+ ["timeout", "--foreground", f"{args.timeout}s", *cmd], quiet=True)
+ samples = parse_log((directory / f"{label}.log").read_text(), flow)
+ p3.validate_image(b.check_artifact(case["image"]))
+ record["cases"].append({"name": label, "image": case["image"], "samples": samples,
+ "baseline_samples": case["samples"],
+ "command": cmd, "timeout_seconds": args.timeout,
+ "flow": b.artifact(directory / f"{label}.json"),
+ "log": b.artifact(directory / f"{label}.log")})
+ b.dump(directory / "record.json", record)
+ if before != inputs(root, variant) or runtime != b.runtime_inputs(root, variant, args.simulator):
+ raise ValueError("candidate inputs changed during execution")
+ b.check_artifact(record["model"])
+ b.check_artifact(reference)
+ for case in cases:
+ p3.validate_image(b.check_artifact(case["image"]))
+ for tool in tools.values():
+ b.check_artifact(tool["executable"])
+ record["status"] = "passed"
+ except (OSError, ValueError) as error:
+ record.update(status="failed", error=str(error))
+ raise
+ finally:
+ b.dump(directory / "record.json", record)
+ print(f"P4 {args.simulator}: one execution per image; {directory}")
+
+
+def validate_replay(path: Path, simulator: str) -> dict:
+ record = b.read_json(path)
+ if record.get("phase") != PHASE or record.get("status") != "passed" or record.get("repetitions") != 1:
+ raise ValueError("not a completed single-execution P4 replay")
+ for field in ("model", "reference"):
+ b.check_artifact(record[field])
+ for value in record["compilation"].values():
+ b.check_artifact(value)
+ for field in ("inputs", "snapshot", "patch"):
+ b.check_artifact(record["source"][field])
+ captured = b.read_json(Path(record["source"]["inputs"]["path"]))
+ if b.digest_map(captured) != record["source"]["input_digest"]:
+ raise ValueError("candidate source digest mismatch")
+ for tool in record["tools"].values():
+ b.check_artifact(tool["executable"])
+ b.check_artifact(tool["archive"])
+ prefix = [record["model"]["path"]]
+ if simulator == "iverilog":
+ runtime = b.check_artifact(record["runtime_tool"]["executable"])
+ compiler = Path(record["tools"]["iverilog"]["executable"]["path"])
+ if runtime.resolve().parent != compiler.resolve().parent:
+ raise ValueError("unmatched Icarus runtime")
+ prefix.insert(0, str(runtime))
+ reference = reference_cases(Path(record["reference"]["path"]), [c["name"] for c in record["cases"]])
+ for case, original in zip(record["cases"], reference, strict=True):
+ if case["image"] != original["image"] or case["baseline_samples"] != original["samples"]:
+ raise ValueError("comparison changed the retained image or reference measurements")
+ image = p3.validate_image(b.check_artifact(case["image"]))
+ compatible_hardware(image["configuration"], record["configuration"])
+ first, end = image["layout"]["descriptor_range"]
+ expected = [*prefix, "+firmware=" + image["artifacts"]["tiny_p3.hex"]["path"],
+ "+tiny_r2_p3", f"+baseline_tcd_base={first:x}", f"+baseline_tcd_end={end:x}",
+ "+max_cycles=100000000"]
+ flow = b.read_json(b.check_artifact(case["flow"]))
+ if case["command"] != expected or case["timeout_seconds"] <= 0 or flow["command"] != [
+ "timeout", "--foreground", f"{case['timeout_seconds']}s", *expected]:
+ raise ValueError("model/image execution provenance mismatch")
+ if parse_log(b.check_artifact(case["log"]).read_text(), flow) != case["samples"]:
+ raise ValueError("reported candidate samples differ from the log")
+ return record
+
+
+def report(args) -> None:
+ base = args.variant_root / "meta/tiny-r2-p4"
+ runs = {}
+ for simulator in ("verilator", "iverilog"):
+ pointer = b.read_json(base / "replay" / simulator / "latest.json")
+ path = Path(pointer["path"])
+ runs[simulator] = (validate_replay(path, simulator), b.artifact(path))
+ before = b.digest_map(inputs(args.root, args.variant_root))
+ if any(run[0]["source_digest"] != before for run in runs.values()):
+ raise ValueError("candidate source changed after measurement")
+ cases = {c["name"]: c for c in runs["verilator"][0]["cases"]}
+ if set(cases) != {"perf-o2-plain-flat", "perf-o2-plain-banked"}:
+ raise ValueError("P4 comparison requires both approved placement images")
+ cross = runs["iverilog"][0]["cases"]
+ if len(cross) != 1 or cross[0]["name"] != "perf-o2-plain-banked":
+ raise ValueError("P4 requires one banked Icarus execution")
+ if any(cross[0][key] != cases["perf-o2-plain-banked"][key] for key in ("image", "samples")):
+ raise ValueError("cross-simulator image/results differ")
+ comparisons = []
+ for name, case in cases.items():
+ rows = []
+ for old, new in zip(case["baseline_samples"]["cases"], case["samples"]["cases"], strict=True):
+ rows.append({"name": new["name"], "baseline_cycles": old["cycles"],
+ "candidate_cycles": new["cycles"], "baseline_instructions": old["instructions"],
+ "candidate_instructions": new["instructions"],
+ "cycle_ratio": old["cycles"] / new["cycles"]})
+ comparisons.append({"name": name, "image": case["image"], "workloads": rows,
+ "local": case["samples"]["local"], "banks": case["samples"]["banks"]})
+ b.dump(base / "report.json", {"phase": PHASE, "status": "measurement_complete",
+ "source_digest": before, "comparisons": comparisons,
+ "executions": {key: value[1] for key, value in runs.items()},
+ "qualification": "matched-binary SAFE24 simulation; not phase/physical acceptance"})
+ print(f"P4 matched-image report: {base / 'report.json'}")
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ parser.add_argument("--variant-root", type=Path, required=True)
+ parser.add_argument("--report", action="store_true")
+ parser.add_argument("--reference", type=Path)
+ parser.add_argument("--cases")
+ parser.add_argument("--simulator", choices=("verilator", "iverilog"))
+ parser.add_argument("--vvp", default="vvp")
+ parser.add_argument("--timeout", type=int, default=21600)
+ parser.add_argument("command", nargs=argparse.REMAINDER)
+ args = parser.parse_args()
+ if args.timeout <= 0:
+ parser.error("timeout must be positive")
+ if args.report:
+ report(args)
+ else:
+ if args.reference is None or not args.cases or not args.simulator:
+ parser.error("replay requires reference, cases and simulator")
+ replay(args)
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/tiny_r2_p5.py b/scripts/tiny_r2_p5.py
new file mode 100644
index 00000000..1dbf93b0
--- /dev/null
+++ b/scripts/tiny_r2_p5.py
@@ -0,0 +1,260 @@
+#!/usr/bin/env python3
+"""Replay immutable P3 images on P5 RTL without relabeling image provenance."""
+from __future__ import annotations
+
+import argparse
+from pathlib import Path
+import sys
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT))
+from scripts import tiny_r2_baseline as b # noqa: E402
+from scripts import tiny_r2_p3 as p3 # noqa: E402
+from scripts import tiny_r2_p4 as previous # noqa: E402
+
+PHASE = "TINY-R2-P5"
+HARDWARE_KEYS = ("SOC", "PDK", "HAVE_PLL", "HAVE_SRAM_IF", "HAVE_SRAM_MACRO",
+ "SRAM_SIZE_KIB", "PDK_BEHAV", "HAVE_HP", "EXT_CLK_HZ", "AUD_CLK_HZ",
+ "CLINT_TIMEBASE_HZ", "MGMT_CPU_CLK_HZ", "JTAG_IDCODE")
+FABRIC_FIELDS = {"window", "active_targets_max", "dma_rw_overlap_cycles", "dma_rw_beat_overlap_cycles"}
+
+
+def inputs(root: Path, variant: Path) -> dict:
+ result = previous.inputs(root, variant)
+ for name in ("AGENTS.md", "docs/engineering.md", "docs/rtl-coding-style.md",
+ "docs/rtl-coding-style-compliance.md", "rtl/rtl_style_manifest.json",
+ "rtl/rtl_style_audit.json", "rtl/rtl_readiness.json", ".verible-format",
+ ".github/workflows/_regression.yml"):
+ result[name] = b.sha256(root / name)
+ for directory in (root / "quality", root / "rtl/tiny/mk"):
+ for path in directory.rglob("*"):
+ if path.is_file() and path.suffix in (".json", ".mk"):
+ result[str(path.relative_to(root))] = b.sha256(path)
+ return result
+
+
+def compatible_hardware(image: dict, model: dict) -> None:
+ a, z = (c["configuration"] for c in (image, model))
+ if any(key not in a or key not in z or a[key] != z[key] for key in HARDWARE_KEYS):
+ raise ValueError("retained image and candidate hardware configuration differ")
+
+
+def parse_log(log: str, flow: dict) -> dict:
+ samples = previous.parse_log(log, flow)
+ rows = [b.parse_fields(line, FABRIC_FIELDS) for line in log.splitlines()
+ if line.startswith("R2_P5_FABRIC ")]
+ rows.sort(key=lambda row: row["window"])
+ if [row["window"] for row in rows] != list(range(1, 9)):
+ raise ValueError("missing or duplicate P5 fabric observations")
+ for row in rows:
+ if any(value < 0 for value in row.values()) or row["active_targets_max"] > 23:
+ raise ValueError("invalid P5 fabric observation")
+ if row["dma_rw_beat_overlap_cycles"] > row["dma_rw_overlap_cycles"]:
+ raise ValueError("P5 beat overlap exceeds transaction overlap")
+ if row["dma_rw_overlap_cycles"] and row["active_targets_max"] < 2:
+ raise ValueError("P5 overlap without independent targets")
+ samples["fabric"] = rows
+ return samples
+
+
+def reference_cases(path: Path, names: list[str]) -> list[dict]:
+ report = b.read_json(path)
+ if report.get("phase") != previous.PHASE or report.get("status") != "measurement_complete":
+ raise ValueError("reference is not a completed P4 measurement report")
+ if not names or len(set(names)) != len(names):
+ raise ValueError("select distinct retained cases")
+ original = previous.validate_replay(b.check_artifact(report["executions"]["verilator"]), "verilator")
+ if original["source_digest"] != report["source_digest"]:
+ raise ValueError("P4 report source differs from retained execution")
+ result = []
+ for name in names:
+ matches = [c for c in original["cases"] if c["name"] == name]
+ comparisons = [c for c in report["comparisons"] if c["name"] == name]
+ if len(matches) != 1 or len(comparisons) != 1:
+ raise ValueError("unknown retained P4 case")
+ case, comparison = matches[0], comparisons[0]
+ if comparison["image"] != case["image"]:
+ raise ValueError("P4 comparison changed the retained image")
+ rows = comparison["workloads"]
+ if len(rows) != len(case["samples"]["cases"]):
+ raise ValueError("P4 comparison workload count differs")
+ for row, sample in zip(rows, case["samples"]["cases"], strict=True):
+ if (row["name"], row["candidate_cycles"], row["candidate_instructions"]) != (
+ sample["name"], sample["cycles"], sample["instructions"]):
+ raise ValueError("P4 comparison differs from retained samples")
+ if comparison["local"] != case["samples"]["local"] or comparison["banks"] != case["samples"]["banks"]:
+ raise ValueError("P4 comparison altered local observations")
+ result.append({"name": name, "image": case["image"], "samples": case["samples"]})
+ return result
+
+
+def replay(args) -> None:
+ root, variant = args.root, args.variant_root
+ parent = variant / "meta/tiny-r2-p5/replay" / args.simulator
+ directory = b.new_attempt(parent, "attempt-")
+ record = {"phase": PHASE, "status": "running", "repetitions": 1, "cases": []}
+ b.dump(directory / "record.json", record)
+ b.dump(parent / "latest.json", {"path": str(directory / "record.json")})
+ try:
+ reference = b.artifact(args.reference)
+ cases = reference_cases(args.reference, args.cases.split(","))
+ configuration = p3.configuration(variant)
+ for case in cases:
+ compatible_hardware(b.read_json(b.check_artifact(case["image"]))["configuration"],
+ configuration)
+ before = inputs(root, variant)
+ tools = b.tools_identity(root)
+ command = args.command[1:] if args.command[:1] == ["--"] else args.command
+ if not command:
+ raise ValueError("fresh model compilation command required")
+ p3.execute_make(root, directory, "compile-model", command)
+ if before != inputs(root, variant):
+ raise ValueError("source changed while compiling candidate model")
+ runtime = b.runtime_inputs(root, variant, args.simulator)
+ model = variant / "sim" / args.simulator / ("emu" if args.simulator == "verilator" else "simv")
+ record.update(reference=reference, configuration=configuration, tools=tools,
+ model=b.artifact(model), source_digest=b.digest_map(before),
+ source=b.snapshot(root, directory / "inputs", before | runtime),
+ compilation={s: b.artifact(directory / f"compile-model.{s}") for s in ("json", "log")})
+ launch = [str(model)]
+ if args.simulator == "iverilog":
+ record["runtime_tool"] = b.vvp_identity(args.vvp, tools["iverilog"])
+ launch.insert(0, record["runtime_tool"]["executable"]["path"])
+ for case in cases:
+ image = p3.validate_image(b.check_artifact(case["image"]))
+ begin, end = image["layout"]["descriptor_range"]
+ cmd = [*launch, "+firmware=" + image["artifacts"]["tiny_p3.hex"]["path"],
+ "+tiny_r2_p3", "+tiny_r2_p5", f"+baseline_tcd_base={begin:x}", f"+baseline_tcd_end={end:x}",
+ "+max_cycles=100000000"]
+ label = case["name"]
+ flow = b.execute(root, directory, label,
+ ["timeout", "--foreground", f"{args.timeout}s", *cmd], quiet=True)
+ samples = parse_log((directory / f"{label}.log").read_text(), flow)
+ p3.validate_image(b.check_artifact(case["image"]))
+ record["cases"].append({"name": label, "image": case["image"], "samples": samples,
+ "baseline_samples": case["samples"],
+ "command": cmd, "timeout_seconds": args.timeout,
+ "flow": b.artifact(directory / f"{label}.json"),
+ "log": b.artifact(directory / f"{label}.log")})
+ b.dump(directory / "record.json", record)
+ if before != inputs(root, variant) or runtime != b.runtime_inputs(root, variant, args.simulator):
+ raise ValueError("candidate inputs changed during execution")
+ b.check_artifact(record["model"])
+ b.check_artifact(reference)
+ for case in cases:
+ p3.validate_image(b.check_artifact(case["image"]))
+ for tool in tools.values():
+ b.check_artifact(tool["executable"])
+ record["status"] = "passed"
+ except (OSError, ValueError) as error:
+ record.update(status="failed", error=str(error))
+ raise
+ finally:
+ b.dump(directory / "record.json", record)
+ print(f"P5 {args.simulator}: one execution per image; {directory}")
+
+
+def validate_replay(path: Path, simulator: str) -> dict:
+ record = b.read_json(path)
+ if record.get("phase") != PHASE or record.get("status") != "passed" or record.get("repetitions") != 1:
+ raise ValueError("not a completed single-execution P5 replay")
+ for field in ("model", "reference"):
+ b.check_artifact(record[field])
+ for value in record["compilation"].values():
+ b.check_artifact(value)
+ for field in ("inputs", "snapshot", "patch"):
+ b.check_artifact(record["source"][field])
+ captured = b.read_json(Path(record["source"]["inputs"]["path"]))
+ if b.digest_map(captured) != record["source"]["input_digest"]:
+ raise ValueError("candidate source digest mismatch")
+ for tool in record["tools"].values():
+ b.check_artifact(tool["executable"])
+ b.check_artifact(tool["archive"])
+ prefix = [record["model"]["path"]]
+ if simulator == "iverilog":
+ runtime = b.check_artifact(record["runtime_tool"]["executable"])
+ compiler = Path(record["tools"]["iverilog"]["executable"]["path"])
+ if runtime.resolve().parent != compiler.resolve().parent:
+ raise ValueError("unmatched Icarus runtime")
+ prefix.insert(0, str(runtime))
+ reference = reference_cases(Path(record["reference"]["path"]), [c["name"] for c in record["cases"]])
+ for case, original in zip(record["cases"], reference, strict=True):
+ if case["image"] != original["image"] or case["baseline_samples"] != original["samples"]:
+ raise ValueError("comparison changed the retained image or reference measurements")
+ image = p3.validate_image(b.check_artifact(case["image"]))
+ compatible_hardware(image["configuration"], record["configuration"])
+ first, end = image["layout"]["descriptor_range"]
+ expected = [*prefix, "+firmware=" + image["artifacts"]["tiny_p3.hex"]["path"],
+ "+tiny_r2_p3", "+tiny_r2_p5", f"+baseline_tcd_base={first:x}", f"+baseline_tcd_end={end:x}",
+ "+max_cycles=100000000"]
+ flow = b.read_json(b.check_artifact(case["flow"]))
+ if case["command"] != expected or case["timeout_seconds"] <= 0 or flow["command"] != [
+ "timeout", "--foreground", f"{case['timeout_seconds']}s", *expected]:
+ raise ValueError("model/image execution provenance mismatch")
+ if parse_log(b.check_artifact(case["log"]).read_text(), flow) != case["samples"]:
+ raise ValueError("reported candidate samples differ from the log")
+ return record
+
+
+def report(args) -> None:
+ base = args.variant_root / "meta/tiny-r2-p5"
+ runs = {}
+ for simulator in ("verilator", "iverilog"):
+ pointer = b.read_json(base / "replay" / simulator / "latest.json")
+ path = Path(pointer["path"])
+ runs[simulator] = (validate_replay(path, simulator), b.artifact(path))
+ before = b.digest_map(inputs(args.root, args.variant_root))
+ if any(run[0]["source_digest"] != before for run in runs.values()):
+ raise ValueError("candidate source changed after measurement")
+ cases = {c["name"]: c for c in runs["verilator"][0]["cases"]}
+ if set(cases) != {"perf-o2-plain-flat", "perf-o2-plain-banked"}:
+ raise ValueError("P5 comparison requires both approved placement images")
+ cross = runs["iverilog"][0]["cases"]
+ if len(cross) != 1 or cross[0]["name"] != "perf-o2-plain-banked":
+ raise ValueError("P5 requires one banked Icarus execution")
+ if any(cross[0][key] != cases["perf-o2-plain-banked"][key] for key in ("image", "samples")):
+ raise ValueError("cross-simulator image/results differ")
+ comparisons = []
+ for name, case in cases.items():
+ rows = []
+ for old, new in zip(case["baseline_samples"]["cases"], case["samples"]["cases"], strict=True):
+ rows.append({"name": new["name"], "baseline_cycles": old["cycles"],
+ "candidate_cycles": new["cycles"], "baseline_instructions": old["instructions"],
+ "candidate_instructions": new["instructions"],
+ "cycle_ratio": old["cycles"] / new["cycles"]})
+ comparisons.append({"name": name, "image": case["image"], "workloads": rows,
+ "local": case["samples"]["local"], "banks": case["samples"]["banks"],
+ "fabric": case["samples"]["fabric"]})
+ b.dump(base / "report.json", {"phase": PHASE, "status": "measurement_complete",
+ "reference_phase": previous.PHASE,
+ "source_digest": before, "comparisons": comparisons,
+ "executions": {key: value[1] for key, value in runs.items()},
+ "qualification": "matched-binary SAFE24 simulation; not phase/physical acceptance"})
+ print(f"P5 matched-image report: {base / 'report.json'}")
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("--root", type=Path, required=True)
+ parser.add_argument("--variant-root", type=Path, required=True)
+ parser.add_argument("--report", action="store_true")
+ parser.add_argument("--reference", type=Path)
+ parser.add_argument("--cases")
+ parser.add_argument("--simulator", choices=("verilator", "iverilog"))
+ parser.add_argument("--vvp", default="vvp")
+ parser.add_argument("--timeout", type=int, default=21600)
+ parser.add_argument("command", nargs=argparse.REMAINDER)
+ args = parser.parse_args()
+ if args.timeout <= 0:
+ parser.error("timeout must be positive")
+ if args.report:
+ report(args)
+ else:
+ if args.reference is None or not args.cases or not args.simulator:
+ parser.error("replay requires reference, cases and simulator")
+ replay(args)
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/tiny_r2_p6.py b/scripts/tiny_r2_p6.py
new file mode 100644
index 00000000..1cda97cb
--- /dev/null
+++ b/scripts/tiny_r2_p6.py
@@ -0,0 +1,386 @@
+#!/usr/bin/env python3
+"""Capture and check source-bound Tiny P6 acceptance without promoting a phase."""
+from __future__ import annotations
+
+import argparse
+from datetime import datetime, timezone
+from pathlib import Path
+import re
+import sys
+import tarfile
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT))
+from scripts import tiny_r2_baseline as b # noqa: E402
+from scripts import tiny_r2_p5 as previous # noqa: E402
+from scripts.check_simulation import DEFAULT_FAILURE # noqa: E402
+
+PHASE = "TINY-R2-P6"
+MODEL = "tests/rtl/sdio_native_model.sv"
+VERIFICATION_UPDATE_PATHS = {MODEL, "scripts/tiny_r2_p6.py", "tests/test_tiny_r2_p6.py"}
+CONSUMER_UPDATE_PATHS = {
+ "publications/programming_reference.py", "tests/test_publication_programming.py",
+ "physical/commercial/config/products/tiny.mk", "physical/commercial/tcl/common/products/tiny.tcl",
+ "tests/test_commercial_flow.py", "scripts/tiny_r2_p6.py", "tests/test_tiny_r2_p6.py",
+}
+COLLECTION_UPDATE_PATHS = {"scripts/tiny_r2_p6.py", "tests/test_tiny_r2_p6.py"}
+RECORDS = {
+ "UART": {"pads": 1, "echo_bytes": 4, "flow_rejected": 1},
+ "DMA": {"channels": 8, "rejected_requests": 5},
+ "RNG": {"unqualified": 1, "diagnostic": 1, "irq": 1},
+ "CRC": {"dma": 1, "tails": 4, "empty": 1},
+ "CRYPTO": {"init": 1, "aes_sha": 1, "dma45": 1, "rsa_public": 1, "zeroize": 1},
+ "RECOVERY": {"audio_stops": 2, "ws_underflow": 1, "ws_abort": 1},
+ "SDIO": {"dma_bytes": 2048, "pio_bytes": 512, "width1": 1, "width4": 1, "irq": 1, "errors": 2},
+}
+
+
+def parse_log(content: str, flow: dict) -> dict:
+ if (flow.get("status") != "passed" or flow.get("exit_code") != 0
+ or not re.search(r"SIM_TEST_PASS Tiny cycles=[0-9]+(?:\r?\n|$)", content)
+ or DEFAULT_FAILURE.search(content)):
+ raise ValueError("P6 requires successful command, terminal marker and no forbidden errors")
+ result = {}
+ for name, expected in RECORDS.items():
+ rows = [b.parse_fields(line, set(expected)) for line in content.splitlines()
+ if line.startswith("R2_P6_" + name + " ")]
+ if rows != [expected]:
+ raise ValueError(f"missing, duplicate or invalid P6 {name} record")
+ result[name.lower()] = rows[0]
+ fields = {"bits", "frames", "ws_frames", "background_jobs", "tcd", "elapsed_us", "cycles", "instructions"}
+ audio = [b.parse_fields(line, fields) for line in content.splitlines()
+ if line.startswith("R2_P6_AUDIO ")]
+ if [row["bits"] for row in audio] != [16, 24]:
+ raise ValueError("P6 needs exactly both audio formats")
+ for row in audio:
+ if (row["frames"] != 256 or row["ws_frames"] != 2 or
+ not 1 <= row["background_jobs"] <= 64 or
+ row["tcd"] != int(row["bits"] == 24) or not 0 < row["elapsed_us"] < 50000 or
+ not 0 < row["instructions"] <= row["cycles"]):
+ raise ValueError("invalid P6 audio/contention observation")
+ result["audio"] = audio
+ wires = [b.parse_fields(line, {"bits", "samples"}) for line in content.splitlines()
+ if line.startswith("R2_P6_I2S_WIRE ")]
+ if wires != [{"bits": 16, "samples": 512}, {"bits": 24, "samples": 512}]:
+ raise ValueError("missing or incorrect independent audio wire observations")
+ result["audio_wires"] = wires
+ fields = {"owner", "read_bytes", "write_bytes", "descriptor_bytes", "ar_stall_max",
+ "aw_stall_max", "w_stall_max", "r_stall_max", "b_stall_max"}
+ bus = sorted([b.parse_fields(line, fields) for line in content.splitlines()
+ if line.startswith("R2_P6_AXI ")], key=lambda row: row["owner"])
+ if [row["owner"] for row in bus] != [0, 1, 2]:
+ raise ValueError("P6 needs each actual AXI owner observation")
+ for row in bus:
+ if (any(v < 0 for v in row.values()) or row["read_bytes"] == 0 or row["write_bytes"] == 0
+ or row["descriptor_bytes"] > row["read_bytes"]):
+ raise ValueError("invalid P6 AXI payload/backpressure observation")
+ if bus[1]["descriptor_bytes"] == 0:
+ raise ValueError("no actual background descriptor fetch observed")
+ result["bus"] = bus
+ contention = [b.parse_fields(line, {"sdio_dma_cycles", "cpu_fetches"})
+ for line in content.splitlines() if line.startswith("R2_P6_CONTENTION ")]
+ if len(contention) != 1 or any(value <= 0 for value in contention[0].values()):
+ raise ValueError("missing actual SDIO/DMA overlap and CPU progress")
+ result["contention"] = contention[0]
+ fields = {"ws_frames", "ws_wait_max", "fifo_max", "underflows", "aborts", "clock_stops"}
+ devices = [b.parse_fields(line, fields) for line in content.splitlines()
+ if line.startswith("R2_P6_DEVICES ")]
+ if len(devices) != 1:
+ raise ValueError("missing or duplicate independent device observations")
+ d = devices[0]
+ if (d["ws_frames"] != 4 or not 0 <= d["ws_wait_max"] <= 4800 or
+ not 1 <= d["fifo_max"] <= 16 or d["underflows"] != 1 or
+ d["aborts"] != 1 or d["clock_stops"] != 2):
+ raise ValueError("P6 device coverage/service budget failed")
+ result["devices"] = d
+ return result
+
+
+def inputs(variants: list[Path]) -> dict:
+ result = {}
+ for variant in variants:
+ config = b.configuration(variant)["configuration"]
+ if config.get("APP") != "ci_smoke":
+ raise ValueError("P6 qualification requires the ci_smoke composition")
+ result.update(previous.inputs(ROOT, variant))
+ for name in (".bake.toml", ".clang-format", "docs/misra-c-2012.md",
+ "quality/misra/deviations.md", "docs/ip/dma.md", "docs/ip/i2s.md",
+ "docs/ip/sdio.md", "docs/ip/crypto.md", "docs/ip/ws2812.md",
+ "physical/commercial/config/products/tiny.mk",
+ "physical/commercial/tcl/common/products/tiny.tcl"):
+ result[name] = b.sha256(ROOT / name)
+ return result
+
+
+def current_tools() -> dict:
+ tools = b.tools_identity(ROOT)
+ tools["vvp"] = b.vvp_identity("vvp", tools["iverilog"])
+ return tools
+
+
+def checked_flow(path: Path, captured_at: str) -> dict:
+ flow = b.read_json(path)
+ if flow.get("status") != "passed" or flow.get("exit_code") != 0:
+ raise ValueError(f"unsuccessful gate: {path}")
+ if (not flow.get("started_at") or not flow.get("finished_at")
+ or flow["started_at"] < captured_at or flow["finished_at"] < flow["started_at"]):
+ raise ValueError(f"gate lacks successful post-capture chronology: {path}")
+ return flow
+
+
+def check_model_declaration_move(before: str, after: str) -> None:
+ """Permit only relocation of one unchanged continuous net declaration.
+
+ Verilator/sv2v accept its forward reference; native Icarus needs the integer
+ first. Every other source byte, including all procedural behavior, must match.
+ """
+ pattern = re.compile(r"^ wire \[3:0\] s_token_lanes = [^;]+;\n", re.M)
+ old, new = pattern.findall(before), pattern.findall(after)
+ if (len(old) != 1 or old != new or pattern.sub("", before) != pattern.sub("", after)
+ or after.index(" integer s_data_bytes;") > after.index(new[0])):
+ raise ValueError("not the permitted SDIO model declaration-only relocation")
+
+
+def snapshot_model(source: dict) -> str:
+ with tarfile.open(b.check_artifact(source["snapshot"])) as archive:
+ return archive.extractfile(MODEL).read().decode()
+
+
+def check_update(directory: Path, saved: dict, update: dict, current: dict) -> dict:
+ if (update.get("phase") != PHASE or update.get("kind") != "native_model_declaration_order"
+ or update["original_source"] != b.artifact(directory / "source.json")):
+ raise ValueError("verification update does not bind the original campaign")
+ old = b.read_json(b.check_artifact(saved["source"]["inputs"]))
+ new = b.read_json(b.check_artifact(update["source"]["inputs"]))
+ changed = {name for name in old.keys() | new.keys() if old.get(name) != new.get(name)}
+ if (MODEL not in changed or not changed <= VERIFICATION_UPDATE_PATHS
+ or sorted(changed) != update["changed_inputs"] or new != current):
+ raise ValueError("verification update changed DUT/firmware/constraints or unapproved inputs")
+ check_model_declaration_move(snapshot_model(saved["source"]), snapshot_model(update["source"]))
+ for source in (saved["source"], update["source"]):
+ for name in ("inputs", "snapshot", "patch"):
+ b.check_artifact(source[name])
+ if update["tools"] != saved["tools"] or update["captured_at"] < saved["captured_at"]:
+ raise ValueError("verification update changed tools or chronology")
+ return old
+
+
+def capture_verification_update(directory: Path, variants: list[Path]) -> None:
+ path = directory / "verification-update.json"
+ if path.exists():
+ raise ValueError("never overwrite a verification update")
+ saved = b.read_json(directory / "source.json")
+ current = inputs(variants)
+ old = b.read_json(b.check_artifact(saved["source"]["inputs"]))
+ changed = sorted(name for name in old.keys() | current.keys() if old.get(name) != current.get(name))
+ if MODEL not in changed or not set(changed) <= VERIFICATION_UPDATE_PATHS:
+ raise ValueError("only the native model and its collector tests may change")
+ check_model_declaration_move(snapshot_model(saved["source"]), (ROOT / MODEL).read_text())
+ update = {"phase": PHASE, "kind": "native_model_declaration_order",
+ "original_source": b.artifact(directory / "source.json"), "changed_inputs": changed,
+ "source": b.snapshot(ROOT, directory / "inputs-native-model", current),
+ "tools": current_tools(), "captured_at": datetime.now(timezone.utc).isoformat()}
+ check_update(directory, saved, update, current)
+ b.dump(path, update)
+
+
+def check_consumer_update(directory: Path, previous_update: dict, update: dict, current: dict) -> None:
+ """Permit only consumers not used by the running RTL/smoke flows."""
+ if (update.get("phase") != PHASE or update.get("kind") != "publication_and_commercial_consumers"
+ or update["previous_update"] != b.artifact(directory / "verification-update.json")):
+ raise ValueError("consumer update does not bind the verified model snapshot")
+ old = b.read_json(b.check_artifact(previous_update["source"]["inputs"]))
+ new = b.read_json(b.check_artifact(update["source"]["inputs"]))
+ changed = {name for name in old.keys() | new.keys() if old.get(name) != new.get(name)}
+ if (not changed or not changed <= CONSUMER_UPDATE_PATHS or
+ sorted(changed) != update["changed_inputs"] or new != current):
+ raise ValueError("consumer update changed simulation, firmware, smoke constraints or unrelated inputs")
+ for field in ("inputs", "snapshot", "patch"):
+ b.check_artifact(update["source"][field])
+ if (update["tools"] != previous_update["tools"] or
+ update["captured_at"] < previous_update["captured_at"]):
+ raise ValueError("consumer update changed tools or chronology")
+
+
+def capture_consumer_update(directory: Path, variants: list[Path]) -> None:
+ path = directory / "consumer-update.json"
+ if path.exists():
+ raise ValueError("never overwrite a consumer update")
+ previous_update = b.read_json(directory / "verification-update.json")
+ current = inputs(variants)
+ old = b.read_json(b.check_artifact(previous_update["source"]["inputs"]))
+ changed = sorted(name for name in old.keys() | current.keys() if old.get(name) != current.get(name))
+ if not changed or not set(changed) <= CONSUMER_UPDATE_PATHS:
+ raise ValueError("unexpected non-consumer source changes")
+ update = {"phase": PHASE, "kind": "publication_and_commercial_consumers",
+ "previous_update": b.artifact(directory / "verification-update.json"),
+ "changed_inputs": changed,
+ "source": b.snapshot(ROOT, directory / "inputs-consumers", current),
+ "tools": current_tools(), "captured_at": datetime.now(timezone.utc).isoformat()}
+ check_consumer_update(directory, previous_update, update, current)
+ b.dump(path, update)
+
+
+def check_collection_update(directory: Path, previous_update: dict, update: dict, current: dict) -> None:
+ if (update.get("phase") != PHASE or update.get("kind") != "terminal_log_collection"
+ or update["previous_update"] != b.artifact(directory / "consumer-update.json")):
+ raise ValueError("collection update does not bind the consumer snapshot")
+ old = b.read_json(b.check_artifact(previous_update["source"]["inputs"]))
+ new = b.read_json(b.check_artifact(update["source"]["inputs"]))
+ changed = {name for name in old.keys() | new.keys() if old.get(name) != new.get(name)}
+ if (not changed or not changed <= COLLECTION_UPDATE_PATHS or
+ sorted(changed) != update["changed_inputs"] or new != current):
+ raise ValueError("collection update changed inputs outside the collector/tests")
+ for field in ("inputs", "snapshot", "patch"):
+ b.check_artifact(update["source"][field])
+ if (update["tools"] != previous_update["tools"] or
+ update["captured_at"] < previous_update["captured_at"]):
+ raise ValueError("collection update changed tools or chronology")
+
+
+def capture_collection_update(directory: Path, variants: list[Path]) -> None:
+ path = directory / "collection-update.json"
+ if path.exists():
+ raise ValueError("never overwrite a collection update")
+ previous_update = b.read_json(directory / "consumer-update.json")
+ current = inputs(variants)
+ old = b.read_json(b.check_artifact(previous_update["source"]["inputs"]))
+ changed = sorted(name for name in old.keys() | current.keys() if old.get(name) != current.get(name))
+ if not changed or not set(changed) <= COLLECTION_UPDATE_PATHS:
+ raise ValueError("unexpected non-collector source changes")
+ update = {"phase": PHASE, "kind": "terminal_log_collection",
+ "previous_update": b.artifact(directory / "consumer-update.json"),
+ "changed_inputs": changed,
+ "source": b.snapshot(ROOT, directory / "inputs-collection", current),
+ "tools": current_tools(), "captured_at": datetime.now(timezone.utc).isoformat()}
+ check_collection_update(directory, previous_update, update, current)
+ b.dump(path, update)
+
+
+def capture(directory: Path, variants: list[Path]) -> None:
+ if (directory / "source.json").exists():
+ raise ValueError("capture needs a fresh evidence directory; never overwrite provenance")
+ if {b.configuration(v)["configuration"]["PDK"] for v in variants} != {"ICS55", "IHP130"}:
+ raise ValueError("capture both Tiny PDK input sets before validation")
+ source = b.snapshot(ROOT, directory / "inputs", inputs(variants))
+ tools = current_tools()
+ b.dump(directory / "source.json", {"phase": PHASE, "source": source, "tools": tools,
+ "captured_at": datetime.now(timezone.utc).isoformat(),
+ "variants": [str(p.resolve()) for p in variants],
+ "configurations": [b.configuration(p) for p in variants]})
+
+
+def report(directory: Path, gates: list[Path], variants: list[Path]) -> None:
+ saved = b.read_json(directory / "source.json")
+ if saved.get("phase") != PHASE:
+ raise ValueError("wrong evidence phase")
+ if saved["tools"] != current_tools():
+ raise ValueError("locked tool identity changed after capture")
+ selected = [Path(p) for p in saved["variants"]]
+ current = inputs(selected)
+ original = b.read_json(b.check_artifact(saved["source"]["inputs"]))
+ update_path = directory / "verification-update.json"
+ update = b.read_json(update_path) if update_path.exists() else None
+ consumer_path = directory / "consumer-update.json"
+ consumer = b.read_json(consumer_path) if consumer_path.exists() else None
+ collection_path = directory / "collection-update.json"
+ collection = b.read_json(collection_path) if collection_path.exists() else None
+ consumer_inputs = current
+ if collection:
+ if not consumer:
+ raise ValueError("collection update requires the consumer snapshot")
+ check_collection_update(directory, consumer, collection, current)
+ consumer_inputs = b.read_json(b.check_artifact(consumer["source"]["inputs"]))
+ verified_inputs = current
+ if consumer:
+ if not update:
+ raise ValueError("consumer update requires the model snapshot")
+ check_consumer_update(directory, update, consumer, consumer_inputs)
+ verified_inputs = b.read_json(b.check_artifact(update["source"]["inputs"]))
+ if update:
+ original = check_update(directory, saved, update, verified_inputs)
+ elif current != original:
+ raise ValueError("P6 technical inputs changed after capture")
+ for name in ("snapshot", "patch"):
+ b.check_artifact(saved["source"][name])
+ checked = {}
+ for path in gates:
+ gate = checked_flow(path, saved["captured_at"])
+ checked[str(path)] = {"result": b.artifact(path), "log": b.artifact(Path(gate["log"]))}
+ simulations = {}
+ selections = [(variant, simulator) for variant in variants for simulator in ("iverilog", "verilator")
+ if (variant / "sim" / simulator / "result-sim.json").is_file()]
+ for variant, simulator in selections:
+ config = b.configuration(variant)
+ if config["configuration"].get("APP") != "ci_smoke":
+ raise ValueError("simulation must use the complete ci_smoke composition")
+ pdk = config["configuration"]["PDK"]
+ key = pdk + "/" + simulator
+ if key in simulations or simulator not in ("iverilog", "verilator"):
+ raise ValueError("duplicate or unsupported simulation variant")
+ folder = variant / "sim" / simulator
+ model_source = update if update and simulator == "iverilog" else saved
+ flow = checked_flow(folder / "result-sim.json", model_source["captured_at"])
+ compilation = checked_flow(folder / "result-compile.json", model_source["captured_at"])
+ if flow["started_at"] < compilation["finished_at"]:
+ raise ValueError("simulation predates completed model compilation")
+ check = b.read_json(folder / "result-sim-check.json")
+ if check.get("status") != "passed":
+ raise ValueError("missing strict simulation verdict")
+ samples = parse_log((folder / "sim.log").read_text(), flow)
+ paths = [folder / name for name in ("result-sim.json", "result-sim-check.json", "sim.log",
+ "result-compile.json", "compile.log")]
+ paths += [folder / ("emu" if simulator == "verilator" else "simv"),
+ variant / "sw/firmware", variant / "sw/retrosoc_fw.hex", variant / "sw/retrosoc_fw.map",
+ variant / "sw/retrosoc_fw.bin", variant / "sw/ld2_all_sram.lds",
+ variant / "sw/memory_regions.ld",
+ variant / "meta/manifest.json"]
+ paths += sorted(path for path in (variant / "sw/include").rglob("*") if path.is_file())
+ runtime = b.runtime_inputs(ROOT, variant, simulator)
+ if update and simulator == "verilator":
+ if runtime.get(MODEL) != current[MODEL]:
+ raise ValueError("missing native card model runtime input")
+ runtime[MODEL] = original[MODEL]
+ simulations[key] = {"configuration": config, "simulator": simulator, "samples": samples,
+ "source": model_source["source"], "runtime_inputs": runtime,
+ "artifacts": [b.artifact(path) for path in paths]}
+ if set(simulations) != {p + "/" + s for p in ("ICS55", "IHP130") for s in ("iverilog", "verilator")}:
+ raise ValueError("P6 needs both simulators on both Tiny PDKs")
+ # Physical/quality reports remain independent gates; this status never
+ # promotes functional collection into phase or physical qualification.
+ b.dump(directory / "report.json", {"phase": PHASE, "status": "functional_evidence_collected",
+ "tools": saved["tools"],
+ "verification_update": b.artifact(update_path) if update else None,
+ "consumer_update": b.artifact(consumer_path) if consumer else None,
+ "collection_update": b.artifact(collection_path) if collection else None,
+ "source": saved["source"], "source_inputs_rechecked": len(current), "gates": checked,
+ "simulations": simulations, "limitations": ["human review pending", "timing observational",
+ "no QFN64/package, routed/PVT/CDC/RDC or publication qualification"]})
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("action", choices=("capture", "capture-verification-update", "capture-consumer-update",
+ "capture-collection-update", "report"))
+ parser.add_argument("--evidence-root", type=Path, required=True)
+ parser.add_argument("--variant-root", action="append", type=Path, required=True)
+ parser.add_argument("--gate", action="append", type=Path, default=[])
+ args = parser.parse_args()
+ directory = args.evidence_root.resolve()
+ if not directory.is_relative_to(ROOT / "build"):
+ parser.error("evidence must stay under the repository build root")
+ if args.action == "capture":
+ capture(directory, args.variant_root)
+ elif args.action == "capture-verification-update":
+ capture_verification_update(directory, args.variant_root)
+ elif args.action == "capture-consumer-update":
+ capture_consumer_update(directory, args.variant_root)
+ elif args.action == "capture-collection-update":
+ capture_collection_update(directory, args.variant_root)
+ else:
+ report(directory, args.gate, args.variant_root)
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/scripts/tiny_r2_p7.py b/scripts/tiny_r2_p7.py
new file mode 100644
index 00000000..082c9f41
--- /dev/null
+++ b/scripts/tiny_r2_p7.py
@@ -0,0 +1,187 @@
+#!/usr/bin/env python3
+"""P7 structural clock/macro evidence; separate from the frozen P1 audit."""
+from __future__ import annotations
+
+import argparse
+import json
+from pathlib import Path
+import sys
+
+ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(ROOT))
+from scripts import tiny_r2_baseline as b # noqa: E402
+from scripts.config_key import variant_id # noqa: E402
+from scripts.tiny_ics55_platform import expand_netlist, inspect_clock_source # noqa: E402
+
+PHASE = "TINY-R2-P7"
+RAM_TYPES = {"ICS55": ("ics55_ecos_sram_1024x32_m8", "CLK"),
+ "IHP130": ("RM_IHPSG13_1P_1024x32_c2_bm_bist", "A_CLK")}
+
+
+def inspect_netlist(document: dict, pdk: str) -> dict:
+ modules, top, leaves, canonical, boundaries = expand_netlist(document)
+
+ def boundary(suffix: str, kind: str) -> dict:
+ found = [cell for name, cell in boundaries.items()
+ if name.replace("/", ".").endswith("." + suffix)
+ and cell["type"].split("$")[0] == kind]
+ if len(found) != 1:
+ raise ValueError(f"missing or ambiguous P7 clock boundary: {suffix}")
+ return found[0]
+
+ def pin(cell: dict, name: str) -> str:
+ bits = cell["ports"].get(name, [])
+ if len(bits) != 1 or bits[0] in ("0", "1", "x", "z"):
+ raise ValueError(f"missing/constant P7 clock: {cell['path']}/{name}")
+ return bits[0]
+
+ root = boundary("u_soc.u_clock_backend.u_sys_mux", "safe_clock_mux")
+ memory = boundary("u_soc.u_clock_backend.u_mem_mux", "safe_clock_mux")
+ peripheral = boundary("u_soc.u_clock_backend.u_pclk_mux", "safe_clock_mux")
+ cpu_gate = boundary("u_soc.u_cpu.u_cpu_gate", "tiny_clock_gate")
+ crypto_gate = boundary("u_soc.u_crypto_lifecycle", "tiny_peripheral_leaf")
+ sys_clock = pin(root, "clk_o")
+ pclk = pin(peripheral, "clk_o")
+ if pin(cpu_gate, "clk_i") != sys_clock or pin(memory, "clk0_i") != sys_clock:
+ raise ValueError("CPU gate/MEM source is not common SYS")
+ if pin(crypto_gate, "clk_i") != pclk:
+ raise ValueError("private Crypto storage gate is not sourced from PCLK")
+ cpu_clock = pin(cpu_gate, "clk_o")
+ crypto_clock = pin(crypto_gate, "clk_o")
+ ram_type, ram_pin = RAM_TYPES[pdk]
+ macros = [cell for cell in leaves if cell["type"] == ram_type]
+ main_names = {f"u_soc.u_sram.gen_group[{g}].u_group.gen_bank[{n}].u_ram.u_mem"
+ for g in range(4) for n in range(8)}
+ crypto_names = {f"u_soc.u_crypto.u_storage.gen_bank[{n}].gen_macro.u_sram.u_mem"
+ for n in range(6)}
+ observed = set()
+ for cell in macros:
+ name = next((name for name in main_names | crypto_names
+ if cell["path"].replace("/", ".").endswith("." + name)), None)
+ if name is None or name in observed:
+ raise ValueError("extra, duplicated or renamed physical SRAM macro")
+ observed.add(name)
+ expected_clock = sys_clock if name in main_names else crypto_clock
+ if pin(cell, ram_pin) != expected_clock:
+ raise ValueError(f"physical SRAM clock mismatch: {name}")
+ if len(macros) != 38 or observed != main_names | crypto_names:
+ raise ValueError("P7 requires 32 main SRAM plus six private Crypto macros")
+ cpu = [cell for cell in leaves if "u_hazard3_cpu_2port" in cell["path"]
+ and ("CK" in cell["ports"] or "CKN" in cell["ports"] or "CLK" in cell["ports"])]
+ if not cpu:
+ raise ValueError("missing CPU clock endpoints")
+ for cell in cpu:
+ for name in ("CK", "CKN", "CLK"):
+ if name in cell["ports"] and pin(cell, name) not in (sys_clock, cpu_clock):
+ raise ValueError("CPU has an unapproved clock or a hidden divider")
+ pll = [cell for cell in leaves if cell["type"] == "PLL_TOP"]
+ if pdk == "ICS55":
+ if len(pll) != 1 or pin(pll[0], "CKOUT1") != pin(root, "clk1_i"):
+ raise ValueError("P7 requires one PLL CKOUT1 connected to the SYS mux")
+ if pin(pll[0], "REFCLK") != pin(root, "clk0_i"):
+ raise ValueError("PLL reference and safe SYS source differ")
+ constants = {"0": 0, "1": 1}
+ for cell in leaves:
+ if cell["type"] in ("TIELOH7R", "TIEHIH7R"):
+ for bit in cell["ports"]["Z"]:
+ constants[bit] = int(cell["type"] == "TIEHIH7R")
+ if pll[0]["ports"].get("EN", ["x"])[0] in constants:
+ raise ValueError("P7 PLL enable must be controlled, not tied on/off")
+ external = modules[top]["ports"]["extclk_i_pad"]["bits"]
+ if len(external) != 1:
+ raise ValueError("missing external reference input")
+ inspect_clock_source(leaves, pin(root, "clk0_i"),
+ canonical(f"{top}:{external[0]}"), constants)
+ elif pll:
+ raise ValueError("no-PLL compatibility instantiated a PLL")
+ return {"system_clock": sys_clock, "memory_clock": pin(memory, "clk_o"),
+ "peripheral_clock": pclk, "cpu_clock": cpu_clock,
+ "main_macros": 32, "private_crypto_macros": 6, "pll_macros": len(pll),
+ "cpu_clocked_cells": len(cpu), "macro_bindings": macros,
+ "clock_boundaries": [root, memory, peripheral, cpu_gate, crypto_gate],
+ "qualification": "structural domain binding only; no physical operating-point qualification"}
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(description=__doc__)
+ parser.add_argument("action", choices=("audit-netlist", "verify-netlist", "capture-source", "verify-source"))
+ parser.add_argument("--variant-root", type=Path, required=True)
+ parser.add_argument("--netlist", type=Path)
+ parser.add_argument("--config", type=Path)
+ parser.add_argument("--output", type=Path, required=True)
+ args = parser.parse_args()
+ try:
+ if args.action in ("capture-source", "verify-source"):
+ inputs = b.source_inputs(ROOT, "ICS55") | b.source_inputs(ROOT, "IHP130")
+ for folder in ("tests", "configs/ci"):
+ for path in (ROOT / folder).rglob("*"):
+ if path.is_file() and path.suffix in (".py", ".sv", ".svh", ".c", ".S", ".ld", ".mk"):
+ inputs[str(path.relative_to(ROOT))] = b.sha256(path)
+ contract = b.artifact(ROOT / "docs/ip/tiny-soc.md")
+ if args.action == "capture-source":
+ if args.output.exists():
+ raise ValueError("source evidence already exists; use a new attempt instead of overwriting")
+ b.dump(args.output, {"phase": PHASE, "repository": b.git_info(ROOT),
+ "specification": contract, "source_inputs": inputs,
+ "source_digest": b.digest_map(inputs), "tools": b.tools_identity(ROOT),
+ "snapshot": b.snapshot(ROOT, args.output.parent / "source", inputs)})
+ else:
+ recorded = b.read_json(args.output)
+ if (recorded.get("phase") != PHASE or recorded.get("source_inputs") != inputs or
+ recorded.get("specification") != contract):
+ raise ValueError("P7 sources changed after the retained source snapshot")
+ print(f"{PHASE} {args.action}: PASS")
+ return 0
+ if args.netlist is None or args.config is None:
+ raise ValueError("netlist actions require --netlist and --config")
+ manifest = b.configuration(args.variant_root)
+ pdk = manifest["configuration"]["PDK"]
+ config = dict(line.split("=", 1) for line in args.config.read_text().splitlines() if "=" in line)
+ for key, value in {"SOC": "TINY", "PDK": pdk,
+ "HAVE_PLL": "YES" if pdk == "ICS55" else "NO",
+ "HAVE_SRAM_MACRO": "YES", "SRAM_SIZE_KIB": "128",
+ "TOP_DESIGN": "retrosoc_tiny_asic", "PERIOD_PS": "41667"}.items():
+ if config.get(key) != value:
+ raise ValueError(f"P7 consumed configuration mismatch: {key}")
+ values = [f"{k}={v}" for k, v in manifest["configuration"].items()
+ if k not in ("SIMU", "SYNTH", "SYNTH_RECIPE", "STA")]
+ expected = variant_id(manifest["profile"], values,
+ ROOT / "dependencies/dependencies.lock.json",
+ "2000-01-01-00-00").rsplit("-", 1)[1]
+ if config.get("CONFIG_DIGEST") != expected or args.variant_root.name.rsplit("-", 1)[1] != expected:
+ raise ValueError("P7 consumed configuration digest mismatch")
+ paths = (args.netlist, Path(str(args.netlist) + ".json"), args.config)
+ if args.action == "audit-netlist" and not paths[1].is_file():
+ # A mapped Verilog export is also a valid audit input. Serialize it
+ # without optimization or remapping; retain the conversion result.
+ # This supports the pre-P7 IHP130 recipe which did not emit JSON.
+ flow = b.read_json(args.variant_root / "syn/yosys/result-synth.json")
+ if flow.get("status") != "passed" or flow.get("exit_code") != 0:
+ raise ValueError("cannot derive audit JSON from an unsuccessful synthesis")
+ b.execute(ROOT, args.output.parent, "p7-netlist-json",
+ ["timeout", "--foreground", "120s", "yosys", "-Q", "-T", "-p",
+ f'read_verilog "{paths[0]}"; write_json "{paths[1]}"'], quiet=True)
+ inputs = [b.artifact(path) for path in paths]
+ auditors = [b.artifact(ROOT / "scripts" / name) for name in
+ ("tiny_r2_p7.py", "tiny_ics55_platform.py", "tiny_r2_baseline.py", "config_key.py")]
+ if args.action == "audit-netlist":
+ result = {"phase": PHASE, "status": "passed", "pdk": pdk,
+ "audit_scripts": auditors, "inputs": inputs,
+ "configuration": manifest["configuration"], "config_digest": expected,
+ "binding": inspect_netlist(json.loads(paths[1].read_text()), pdk)}
+ b.dump(args.output, result)
+ else:
+ result = b.read_json(args.output)
+ if (result.get("status") != "passed" or result.get("phase") != PHASE or
+ result.get("audit_scripts") != auditors or result.get("config_digest") != expected or
+ result.get("inputs") != inputs):
+ raise ValueError("P7 structural evidence is missing, stale, or not passed")
+ except (OSError, ValueError, KeyError) as error:
+ print(f"{PHASE}: {error}", file=sys.stderr)
+ return 1
+ print(f"{PHASE} {args.action}: PASS (structural evidence only)")
+ return 0
+
+
+if __name__ == "__main__":
+ raise SystemExit(main())
diff --git a/tests/c/test_runtime.c b/tests/c/test_runtime.c
index a1cf4bea..664778e6 100644
--- a/tests/c/test_runtime.c
+++ b/tests/c/test_runtime.c
@@ -12,6 +12,8 @@
#include
#include
#include
+#include
+#include
#include
#include
#include
@@ -65,13 +67,56 @@ 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_tiny_rcu_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];
+static uint32_t ws_idle_on_interrupt_read;
+static uint32_t dma_irq_enabled = 8U;
+static bool dma_irq_pending;
+static rs_dma_session_t dma_irq_owner;
+static rs_status_t dma_irq_result;
+
+uint32_t rs_dma_test_irq_save(void) {
+ const uint32_t saved = dma_irq_enabled;
+ dma_irq_enabled = 0U;
+ return saved;
+}
+
+void rs_dma_test_irq_restore(uint32_t saved) {
+ dma_irq_enabled = saved;
+ if ((saved != 0U) && dma_irq_pending) {
+ dma_irq_pending = false;
+ /* Model START taking effect before an interrupt can execute. */
+ if (RS_DMA_CH_REG(3U, RS_DMA_CH_REG_CTRL) == RS_DMA_CH_CTRL_START) {
+ RS_DMA_CH_REG(3U, RS_DMA_CH_REG_STATUS) = RS_DMA_STATUS_BUSY;
+ }
+ dma_irq_result = rs_dma_session_acquire(&dma_irq_owner, 3U);
+ if (dma_irq_result == RS_OK) {
+ const rs_dma_config_t config = {.kind = RS_DMA_KIND_MM_TO_MM,
+ .source = 0x3000U,
+ .destination = 0x4000U,
+ .byte_count = 16U,
+ .width = RS_DMA_WIDTH_32,
+ .source_increment = true,
+ .destination_increment = true,
+ .burst_beats = 1U};
+ dma_irq_result = rs_dma_session_configure(&dma_irq_owner, &config);
+ }
+ }
+}
uint32_t rs_ws2812_test_read(uint32_t offset) {
- return ws_registers[offset / 4U];
+ const uint32_t value = ws_registers[offset / 4U];
+ if ((offset == 0x30U) && (ws_idle_on_interrupt_read != 0U)) {
+ ws_registers[0x18U / 4U] = 8U;
+ if (ws_idle_on_interrupt_read == 1U) {
+ ws_registers[0x30U / 4U] |= RS_WS2812_INTR_DONE;
+ }
+ ws_idle_on_interrupt_read = 0U;
+ }
+ return value;
}
void rs_ws2812_test_write(uint32_t offset, uint32_t value) {
@@ -105,13 +150,121 @@ static void ws_test_reset(void) {
ws_registers[0x18U / 4U] = 8U;
ws_registers[0x24U / 4U] = 3U;
ws_pushed = 0U;
+ ws_idle_on_interrupt_read = 0U;
+}
+
+static int test_dma_tcd_preemption(void) {
+ static _Alignas(64)
+ rs_dma_tcd_t tcd = {.source = 0x1000U,
+ .destination = 0x2000U,
+ .byte_count = 32U,
+ .y_count = 1U,
+ .control = RS_DMA_TCD_VALID | RS_DMA_TCD_SRC_INC | RS_DMA_TCD_DST_INC |
+ (UINT32_C(1) << RS_DMA_TCD_BURST_SHIFT)};
+ rs_status_t result;
+ bool valid;
+ ws_test_reset();
+ dma_irq_pending = true;
+ dma_irq_result = RS_OK;
+ result = rs_dma_submit_tcd(3U, &tcd, 1U);
+ valid = (result == RS_ETIMEOUT) && (dma_irq_result == RS_EIO) && !dma_irq_pending &&
+ (dma_irq_enabled == 8U) &&
+ (RS_DMA_CH_REG(3U, RS_DMA_CH_REG_TCD_HEAD) == (uint32_t)(uintptr_t)&tcd) &&
+ (RS_DMA_CH_REG(3U, RS_DMA_CH_REG_TCD_COUNT) == 1U) &&
+ (RS_DMA_CH_REG(3U, RS_DMA_CH_REG_SRC_ADDR) == tcd.source);
+ RS_DMA_CH_REG(3U, RS_DMA_CH_REG_STATUS) = 0U;
+ (void)rs_dma_session_release(&dma_irq_owner);
+ if (!valid) {
+ return 1;
+ }
+ /* An already leased direct channel must remain untouched on rejection. */
+ if (rs_dma_session_acquire(&dma_irq_owner, 3U) != RS_OK) {
+ return 2;
+ }
+ RS_DMA_CH_REG(3U, RS_DMA_CH_REG_SRC_ADDR) = 0x3000U;
+ result = rs_dma_submit_tcd(3U, &tcd, 1U);
+ valid = (result == RS_EIO) && (dma_irq_enabled == 8U) &&
+ (RS_DMA_CH_REG(3U, RS_DMA_CH_REG_TCD_HEAD) == 0U) &&
+ (RS_DMA_CH_REG(3U, RS_DMA_CH_REG_TCD_COUNT) == 0U) &&
+ (RS_DMA_CH_REG(3U, RS_DMA_CH_REG_SRC_ADDR) == 0x3000U);
+ (void)rs_dma_session_release(&dma_irq_owner);
+ if (!valid) {
+ return 3;
+ }
+ /* Calling from an already masked context must not enable interrupts. */
+ dma_irq_enabled = 0U;
+ result = rs_dma_submit_tcd(3U, &tcd, 1U);
+ valid = (result == RS_ETIMEOUT) && (dma_irq_enabled == 0U);
+ dma_irq_enabled = 8U;
+ return valid ? 0 : 4;
+}
+
+static int test_ws2812_completion_race(void) {
+ rs_ws2812_dma_status_t transfer;
+ for (uint32_t delay = 1U; delay <= 2U; ++delay) {
+ ws_test_reset();
+ if (rs_ws2812_dma_begin(ws_pixels, 4U, 0U, 100U) != RS_OK) {
+ return 1;
+ }
+ /* Complete after INTR_STATE was sampled, before STATUS is sampled.
+ * The second case also delays the sticky DONE latch until next service. */
+ ws_idle_on_interrupt_read = delay;
+ if (rs_ws2812_dma_service(1U) != RS_OK) {
+ return 2;
+ }
+ (void)rs_ws2812_dma_status(&transfer);
+ if (!transfer.active || transfer.draining || transfer.dma_pending) {
+ return 3;
+ }
+ ws_registers[0x30U / 4U] |= RS_WS2812_INTR_DONE;
+ if (rs_ws2812_dma_service(2U) != RS_OK) {
+ return 4;
+ }
+ (void)rs_ws2812_dma_status(&transfer);
+ if (transfer.active || (transfer.result != RS_OK) || (ws_registers[0x24U / 4U] != 3U)) {
+ return 5;
+ }
+ }
+ /* An idle core without DONE must still terminate on error, abort or the
+ * original deadline; waiting for the sticky latch cannot hide a failure. */
+ for (uint32_t failure = 0U; failure < 3U; ++failure) {
+ const rs_status_t expected = (failure == 2U) ? RS_ETIMEOUT : RS_EIO;
+ ws_test_reset();
+ if (rs_ws2812_dma_begin(ws_pixels, 4U, 0U, 3U) != RS_OK) {
+ return 6;
+ }
+ ws_idle_on_interrupt_read = 2U;
+ if (rs_ws2812_dma_service(1U) != RS_OK) {
+ return 7;
+ }
+ if (failure == 0U) {
+ ws_registers[0x2cU / 4U] = 1U;
+ } else if (failure == 1U) {
+ ws_registers[0x30U / 4U] = RS_WS2812_INTR_ABORTED;
+ }
+ if (rs_ws2812_dma_service(3U) != expected) {
+ return 8;
+ }
+ (void)rs_ws2812_dma_service(4U);
+ (void)rs_ws2812_dma_service(5U);
+ (void)rs_ws2812_dma_status(&transfer);
+ if (transfer.active || (transfer.result != expected)) {
+ return 9;
+ }
+ }
+ return 0;
}
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};
+ 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) ||
@@ -123,13 +276,11 @@ static int test_dma_sessions(void) {
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)) {
+ 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) ||
+ 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;
@@ -162,15 +313,13 @@ static int test_ws2812_refill(void) {
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)) {
+ (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)) {
+ 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. */
@@ -206,8 +355,7 @@ static int test_ws2812_refill(void) {
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)) {
+ (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
@@ -768,7 +916,11 @@ static int test_sdio_helpers(void) {
rs_sd_memory_info_t info = {0};
uint32_t value;
- if ((rs_sdio_clock_calculate(72000000U, 400000U, &clock) != RS_OK) ||
+ if ((rs_sdio_clock_calculate(24000000U, 400000U, &clock) != RS_OK) ||
+ (clock.half_period != 30U) || (clock.actual_hz != 400000U) ||
+ (rs_sdio_clock_calculate(24000000U, 12000000U, &clock) != RS_OK) ||
+ (clock.half_period != 1U) || (clock.actual_hz != 12000000U) ||
+ (rs_sdio_clock_calculate(72000000U, 400000U, &clock) != RS_OK) ||
(clock.half_period != 90U) || (clock.actual_hz != 400000U) ||
(rs_sdio_clock_calculate(100000000U, 50000000U, &clock) != RS_OK) ||
(clock.half_period != 1U) || (clock.actual_hz != 50000000U)) {
@@ -936,32 +1088,48 @@ rs_status_t rs_tiny_dma_tcd_validate(uint32_t channel, const rs_dma_tcd_t *tcd);
static int test_tiny_dma_config_validation(void) {
rs_dma_config_t config = {
- .kind = RS_DMA_KIND_MM_TO_MM, .request = RS_DMA_REQUEST_SOFTWARE,
+ .kind = RS_DMA_KIND_MM_TO_MM,
+ .request = RS_DMA_REQUEST_SOFTWARE,
.source = (uintptr_t)UINT32_C(0x30000000),
- .destination = (uintptr_t)UINT32_C(0x30001000), .byte_count = 259U,
- .width = RS_DMA_WIDTH_32, .source_increment = true, .destination_increment = true,
+ .destination = (uintptr_t)UINT32_C(0x30001000),
+ .byte_count = 259U,
+ .width = RS_DMA_WIDTH_32,
+ .source_increment = true,
+ .destination_increment = true,
.burst_beats = 16U,
};
static _Alignas(64) rs_dma_tcd_t descriptor;
if ((rs_tiny_dma_config_validate(3U, &config) != RS_OK) ||
- (rs_tiny_dma_config_validate(4U, &config) != RS_EINVAL)) {
+ (rs_tiny_dma_config_validate(7U, &config) != RS_OK) ||
+ (rs_tiny_dma_config_validate(8U, &config) != RS_EINVAL)) {
return 1;
}
config.kind = RS_DMA_KIND_MM_TO_STREAM;
config.request = RS_DMA_REQUEST_I2S_TX;
config.byte_count = 256U;
- if (rs_tiny_dma_config_validate(0U, &config) != RS_EINVAL) {
+ if (rs_tiny_dma_config_validate(6U, &config) != RS_OK) {
return 2;
}
descriptor.source = UINT32_C(0x30000000);
descriptor.byte_count = 256U;
descriptor.y_count = 1U;
descriptor.control = RS_DMA_TCD_VALID | RS_DMA_TCD_SRC_INC |
- ((uint32_t)RS_DMA_KIND_MM_TO_STREAM << RS_DMA_TCD_KIND_SHIFT) |
- ((uint32_t)RS_DMA_REQUEST_I2S_TX << RS_DMA_TCD_REQUEST_SHIFT);
- if (rs_tiny_dma_tcd_validate(0U, &descriptor) != RS_ENOTSUP) {
+ ((uint32_t)RS_DMA_KIND_MM_TO_STREAM << RS_DMA_TCD_KIND_SHIFT) |
+ ((uint32_t)RS_DMA_REQUEST_I2S_TX << RS_DMA_TCD_REQUEST_SHIFT);
+ if (rs_tiny_dma_tcd_validate(6U, &descriptor) != RS_OK) {
return 3;
}
+ config.kind = RS_DMA_KIND_STREAM_TO_MM;
+ config.request = RS_DMA_REQUEST_DVP_RX;
+ if (rs_tiny_dma_config_validate(2U, &config) != RS_EINVAL) {
+ return 4;
+ }
+ descriptor.control = RS_DMA_TCD_VALID | RS_DMA_TCD_DST_INC |
+ ((uint32_t)RS_DMA_KIND_STREAM_TO_MM << RS_DMA_TCD_KIND_SHIFT) |
+ ((uint32_t)RS_DMA_REQUEST_DVP_RX << RS_DMA_TCD_REQUEST_SHIFT);
+ if (rs_tiny_dma_tcd_validate(2U, &descriptor) != RS_ENOTSUP) {
+ return 5;
+ }
return 0;
}
@@ -2853,9 +3021,12 @@ static int test_npu_p6_reference_edge_ops(void) {
uint8_t depthwise_weights[72] = {0U};
int32_t depthwise_parameters[4] = {1, INT32_C(1073741824), 1, 0};
rs_npu_p6_memory_t depthwise_memory = {
- .arena = {.base = UINT32_C(0x1000), .bytes = sizeof(depthwise_arena), .data = depthwise_arena},
- .weights =
- {.base = UINT32_C(0x3000), .bytes = sizeof(depthwise_weights), .data = depthwise_weights},
+ .arena = {.base = UINT32_C(0x1000),
+ .bytes = sizeof(depthwise_arena),
+ .data = depthwise_arena},
+ .weights = {.base = UINT32_C(0x3000),
+ .bytes = sizeof(depthwise_weights),
+ .data = depthwise_weights},
.params =
{
.base = UINT32_C(0x2000),
@@ -2891,7 +3062,9 @@ static int test_npu_p6_reference_edge_ops(void) {
{
uint8_t average_arena[16] = {1U, 2U, 3U, 4U};
rs_npu_p6_memory_t average_memory = {
- .arena = {.base = UINT32_C(0x4000), .bytes = sizeof(average_arena), .data = average_arena},
+ .arena = {.base = UINT32_C(0x4000),
+ .bytes = sizeof(average_arena),
+ .data = average_arena},
.weights = {.base = UINT32_C(0x5000), .bytes = 1U, .data = average_arena},
.params = {.base = UINT32_C(0x6000), .bytes = 1U, .data = average_arena},
};
@@ -2978,6 +3151,61 @@ static int test_crypto_lifecycle_contract(void) {
return 0;
}
+static int test_tiny_rcu_boundaries(void) {
+ rs_tiny_rcu_snapshot_t snapshot;
+ uint32_t hz = 0U;
+ rs_tiny_rcu_test_mmio[RS_TINY_RCU_CAPABILITY_OFFSET / 4U] = 3U;
+ rs_tiny_rcu_test_mmio[RS_TINY_RCU_TARGET_CAP_OFFSET / 4U] = 0x3FFFU;
+ rs_tiny_rcu_test_mmio[RS_TINY_RCU_XIN_HZ_OFFSET / 4U] = 96000000U;
+ rs_tiny_rcu_test_mmio[RS_TINY_RCU_REF_HZ_OFFSET / 4U] = 24000000U;
+ rs_tiny_rcu_test_mmio[RS_TINY_RCU_COMMAND_OFFSET / 4U] = 0xDEADU;
+ if (rs_tiny_rcu_request_profile(RS_TINY_RCU_PROFILE_PLL192, 10U) != RS_ENOTSUP ||
+ rs_tiny_rcu_request_profile((rs_tiny_rcu_profile_t)4, 10U) != RS_EINVAL ||
+ rs_tiny_rcu_apply_gate(0U, 0U) != RS_EINVAL ||
+ rs_tiny_rcu_apply_reset(2U, 0U) != RS_EINVAL ||
+ rs_tiny_rcu_apply_reset(1U, 10U) != RS_EINVAL ||
+ rs_tiny_rcu_apply_gate(1U << 14U, 10U) != RS_ENOTSUP ||
+ rs_tiny_rcu_apply_reset(1U << 17U, 10U) != RS_ENOTSUP) {
+ return 1;
+ }
+ rs_tiny_rcu_test_mmio[RS_TINY_RCU_STATUS_OFFSET / 4U] = RS_TINY_RCU_STATUS_BUSY;
+ if (rs_tiny_rcu_get_snapshot(&snapshot) != RS_EBUSY ||
+ rs_tiny_rcu_apply_gate(2U, 10U) != RS_EBUSY ||
+ rs_tiny_rcu_request_profile(RS_TINY_RCU_PROFILE_SAFE24, 10U) != RS_EBUSY) {
+ return 2;
+ }
+ rs_tiny_rcu_test_mmio[RS_TINY_RCU_STATUS_OFFSET / 4U] = 0U;
+ rs_tiny_rcu_test_mmio[RS_TINY_RCU_GATE_STATUS_OFFSET / 4U] = 2U;
+ if (rs_tiny_rcu_apply_reset(2U, 10U) != RS_EBUSY) {
+ return 3;
+ }
+ rs_tiny_rcu_test_mmio[RS_TINY_RCU_FAULT_OFFSET / 4U] = 4U;
+ if (rs_tiny_rcu_apply_gate(2U, 10U) != RS_ETIMEOUT) {
+ return 4;
+ }
+ rs_tiny_rcu_test_mmio[RS_TINY_RCU_FAULT_OFFSET / 4U] = 8U;
+ if (rs_tiny_rcu_apply_gate(2U, 10U) != RS_EIO ||
+ rs_tiny_rcu_test_mmio[RS_TINY_RCU_COMMAND_OFFSET / 4U] != 0xDEADU) {
+ return 5;
+ }
+ rs_tiny_rcu_test_mmio[RS_TINY_RCU_FAULT_OFFSET / 4U] = 0U;
+ if (rs_tiny_rcu_profile_hz(RS_TINY_RCU_PROFILE_XIN, &hz) != RS_OK || hz != 96000000U ||
+ 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_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 ||
+ rs_tiny_rcu_set_clkout(0U, 0U) != RS_OK) {
+ return 7;
+ }
+ return 0;
+}
+
int main(void) {
const int results[] = {
test_string_helpers(),
@@ -2986,9 +3214,12 @@ int main(void) {
test_wait_helper(),
test_crypto_lifecycle_contract(),
test_clock_frequency_contract(),
+ test_tiny_rcu_boundaries(),
test_ws2812_helpers(),
test_dma_sessions(),
test_ws2812_refill(),
+ test_dma_tcd_preemption(),
+ test_ws2812_completion_race(),
test_timer_helpers(),
test_psram_helpers(),
test_sdram_helpers(),
diff --git a/tests/publication_history.py b/tests/publication_history.py
new file mode 100644
index 00000000..fd45f9ed
--- /dev/null
+++ b/tests/publication_history.py
@@ -0,0 +1,40 @@
+"""Materialize immutable, locally available publication test inputs.
+
+This is a test fixture, never a publication promotion or a current-RTL verdict.
+Each exported tree keeps its own adapter, technical contract, tests and lock.
+"""
+from __future__ import annotations
+
+import io
+import json
+import subprocess
+import tarfile
+from pathlib import Path
+
+ROOT = Path(__file__).resolve().parents[1]
+ORIGINAL_BOOK = "2805ea91833a6bede32f3a82df3d17d20eb5ec77"
+P5_CONTRACT = "b3b30a72cd6ef5340c28e5044c96cd13258ec5a6"
+
+
+def export(repository: Path, revision: str, destination: Path, paths: list[str] = ()) -> None:
+ data = subprocess.check_output(["git", "-C", str(repository), "archive", revision, *paths])
+ destination.mkdir(parents=True, exist_ok=True)
+ with tarfile.open(fileobj=io.BytesIO(data)) as archive:
+ for member in archive.getmembers():
+ target = (destination / member.name).resolve()
+ if not target.is_relative_to(destination.resolve()) or member.issym() or member.islnk():
+ raise ValueError("unsafe publication fixture archive member")
+ archive.extractall(destination)
+
+
+def snapshot(destination: Path, revision: str) -> Path:
+ export(ROOT, revision, destination, ["publications", "scripts", "rtl", "crt", "app",
+ "configs", "docs", "dependencies", "LICENSE",
+ "Makefile", "README.md", "tests"])
+ lock = json.loads((destination / "dependencies/dependencies.lock.json").read_text())
+ for name in ("cluster_common", "cluster_archinfo", "cluster_pwm", "cluster_rtc",
+ "cluster_wdg", "hazard3"):
+ source = lock["sources"][name]
+ export(ROOT / source["destination"], source["revision"], destination / source["destination"],
+ ["hdl"] if name == "hazard3" else [])
+ return destination
diff --git a/tests/rtl/i2s_tb.sv b/tests/rtl/i2s_tb.sv
index 80a2cb20..4ac05c0c 100644
--- a/tests/rtl/i2s_tb.sv
+++ b/tests/rtl/i2s_tb.sv
@@ -98,6 +98,28 @@ module i2s_tb;
end
endtask
+ task automatic zero_strobe_rx_write;
+ logic [31:0] before_status, after_status;
+ read(32'h8, before_status);
+ if (before_status[2]) $fatal(1, "missing RX data for direction test");
+ @(negedge clk_i);
+ apb4.paddr = 32'h20;
+ apb4.pwdata = 0;
+ apb4.pstrb = 0;
+ apb4.pwrite = 1;
+ apb4.psel = 1;
+ apb4.penable = 0;
+ @(negedge clk_i);
+ apb4.penable = 1;
+ while (!apb4.pready) @(negedge clk_i);
+ if (!apb4.pslverr) $fatal(1, "zero-strobe write became an RX read");
+ apb4.psel = 0;
+ apb4.penable = 0;
+ apb4.pwrite = 0;
+ read(32'h8, after_status);
+ if (before_status[23:16] != after_status[23:16]) $fatal(1, "invalid write popped RX FIFO");
+ endtask
+
always @(posedge i2s.sclk_o) sclk_edges = sclk_edges + 1;
initial begin
@@ -132,6 +154,9 @@ module i2s_tb;
repeat (4000) @(posedge clk_aud_i);
if (sclk_edges == 0) $fatal(1, "I2S SCLK did not toggle");
read(32'h0000_0008, value);
+ write(32'h0000_0000, 32'h0000_0000);
+ repeat (64) @(negedge clk_i);
+ zero_strobe_rx_write();
write(32'h0000_0000, 32'h0000_0008);
write(32'h0000_0000, 32'h0000_0009);
repeat (2000) @(posedge clk_aud_i);
diff --git a/tests/rtl/sdio_axi_memory_responder.sv b/tests/rtl/sdio_axi_memory_responder.sv
index 89e9dbe3..8cb9c6da 100644
--- a/tests/rtl/sdio_axi_memory_responder.sv
+++ b/tests/rtl/sdio_axi_memory_responder.sv
@@ -141,7 +141,17 @@ module sdio_axi_memory_responder #(
assign axi4.rresp = (s_read_error_q) ? `AXI4_RESP_SLAVE_ERROR : `AXI4_RESP_OKAY;
assign axi4.rlast = s_read_bad_last_q || ((s_read_index_q + 1'b1) >= (s_read_len_q + 1'b1));
assign axi4.ruser = '0;
- assign axi4.rvalid = s_read_active_q;
+ integer s_read_gap = 0;
+ integer s_read_gap_count = 0;
+ task automatic set_read_gap(input integer cycles_i);
+ s_read_gap = cycles_i < 0 ? 0 : cycles_i;
+ endtask
+ always @(posedge clk_i or negedge rst_n_i) begin
+ if (!rst_n_i) s_read_gap_count <= 0;
+ else if (axi4.rvalid && axi4.rready) s_read_gap_count <= s_read_gap;
+ else if (s_read_gap_count != 0) s_read_gap_count <= s_read_gap_count - 1;
+ end
+ assign axi4.rvalid = s_read_active_q && (s_read_gap_count == 0);
assign axi4.awready = !s_write_active_q && !axi4.bvalid && !s_aw_stall_q;
assign axi4.wready = s_write_active_q && !s_w_stall_q;
diff --git a/tests/rtl/sdio_command_tb.sv b/tests/rtl/sdio_command_tb.sv
index f8bb2f1d..ddc0115a 100644
--- a/tests/rtl/sdio_command_tb.sv
+++ b/tests/rtl/sdio_command_tb.sv
@@ -32,6 +32,8 @@ module sdio_command_tb;
integer launch_count;
integer captured_bits;
logic [ 47:0] captured_command;
+ tri1 command_pad;
+ assign command_pad = cmd_oe_o ? cmd_do_o : 1'bz;
always #1 clk_i = ~clk_i;
@@ -77,8 +79,8 @@ module sdio_command_tb;
end
always @(posedge sck) begin
- if (cmd_oe_o) begin
- captured_command = {captured_command[46:0], cmd_do_o};
+ if (captured_bits < 48 && (captured_bits != 0 || command_pad == 1'b0)) begin
+ captured_command = {captured_command[46:0], command_pad};
captured_bits = captured_bits + 1;
end
end
@@ -95,6 +97,7 @@ module sdio_command_tb;
start_i = 1'b0;
clock_enable_i = 1'b1;
wait (done_o);
+ @(negedge clk_i);
if (error_o || (launch_count != 48) || (captured_bits != 48) ||
(captured_command[47:46] != 2'b01) || (captured_command[0] != 1'b1) ||
(captured_command[7:1] != sdio_pkg::sdio_crc7_calc(
@@ -104,6 +107,33 @@ module sdio_command_tb;
error_o, launch_count, captured_bits, captured_command, cmd_oe_o);
end
+ // A running SDCLK can accept a command on either half-cycle. The
+ // stopped-clock first command above does not exercise this boundary.
+ for (integer phase = 0; phase < 16; phase++) begin
+ repeat (phase % 3 + 3) @(negedge clk_i);
+ captured_bits = 0;
+ captured_command = '0;
+ cmd_index_i = 6'd8;
+ cmd_arg_i = 32'h1aa ^ 32'(phase);
+ start_i = 1'b1;
+ @(negedge clk_i);
+ start_i = 1'b0;
+ wait (done_o);
+ @(negedge clk_i);
+ if (error_o || captured_bits != 48 || captured_command[47:40] != 8'h48 ||
+ captured_command[39:8] != cmd_arg_i || captured_command[0] != 1'b1 ||
+ captured_command[7:1] != sdio_pkg::sdio_crc7_calc(
+ captured_command[47:8]
+ ))
+ $fatal(
+ 1,
+ "SDIO running-clock frame phase=%0d bits=%0d frame=%h",
+ phase,
+ captured_bits,
+ captured_command
+ );
+ end
+
@(posedge clk_i);
resp_type_i = sdio_pkg::SdioRespR1;
timeout_cycles_i = 32'd4;
diff --git a/tests/rtl/sdio_native_model.sv b/tests/rtl/sdio_native_model.sv
index 58ffad58..fe68aa2a 100644
--- a/tests/rtl/sdio_native_model.sv
+++ b/tests/rtl/sdio_native_model.sv
@@ -14,13 +14,14 @@
// implements only public command/data behavior exercised by this repository;
// it is not a normative-specification replacement.
module sdio_native_card_model #(
- parameter bit SdioOnly = 1'b0,
- parameter bit HighCapacity = 1'b1,
- parameter int BlockBytes = 512,
- parameter int BlockCount = 128,
- parameter int FunctionCount = 1,
- parameter int FunctionBytes = 4096,
- parameter int BusyCycles = 2
+ parameter bit MemoryInitialization = 1'b0,
+ parameter bit SdioOnly = 1'b0,
+ parameter bit HighCapacity = 1'b1,
+ parameter int BlockBytes = 512,
+ parameter int BlockCount = 128,
+ parameter int FunctionCount = 1,
+ parameter int FunctionBytes = 4096,
+ parameter int BusyCycles = 2
) (
input logic rst_n_i,
input logic sck_i,
@@ -48,60 +49,63 @@ module sdio_native_card_model #(
WriteBusy
} data_state_e;
- logic [ 7:0] s_memory [ 0:MemoryBytes-1];
- logic [ 7:0] s_function_memory [0:FunctionStorageBytes-1];
+ logic [7:0] s_memory[0:MemoryBytes-1];
+ logic [7:0] s_function_memory[0:FunctionStorageBytes-1];
- logic [ 47:0] s_cmd_shift;
- integer s_cmd_bits;
- logic s_cmd_response_active;
- logic [135:0] s_cmd_response;
- integer s_cmd_response_length;
- integer s_cmd_response_index;
- integer s_cmd_response_delay;
- integer s_response_delay;
- integer s_data_delay_cfg;
- logic s_inject_crc_error;
- logic s_inject_timeout;
- logic s_inject_write_error;
- logic s_next_cmd_crc_error;
+ logic [47:0] s_cmd_shift;
+ integer s_cmd_bits;
+ logic s_cmd_response_active;
+ logic [135:0] s_cmd_response;
+ integer s_cmd_response_length;
+ integer s_cmd_response_index;
+ integer s_cmd_response_delay;
+ integer s_response_delay;
+ integer s_data_delay_cfg;
+ logic s_inject_crc_error;
+ logic s_inject_timeout;
+ logic s_inject_write_error;
+ logic s_next_cmd_crc_error;
- logic [ 5:0] s_last_cmd;
- logic [ 31:0] s_last_arg;
- logic s_last_cmd_write;
- logic s_last_cmd_data;
- logic s_pending_busy;
- integer s_busy_count;
+ logic [5:0] s_last_cmd;
+ logic [31:0] s_last_arg;
+ logic s_last_cmd_write;
+ logic s_last_cmd_data;
+ logic s_memory_width4;
+ logic s_pending_busy;
+ integer s_busy_count;
- data_state_e s_data_state;
- logic s_data_width4;
- logic s_data_fixed;
- integer s_data_address;
- integer s_data_bytes;
- integer s_data_byte_index;
- integer s_data_bit_index;
- integer s_data_nibble_index;
- integer s_data_crc_index;
- integer s_data_delay;
- logic [ 15:0] s_read_crc;
- logic [ 15:0] s_read_crc_lane [ 0:3];
- logic [ 15:0] s_write_crc;
- logic [ 15:0] s_write_crc_lane [ 0:3];
- logic [ 15:0] s_received_crc;
- logic [ 15:0] s_received_crc_lane [ 0:3];
- logic [ 7:0] s_write_byte;
- logic [ 7:0] s_write_byte_lane [ 0:3];
- integer s_write_bit_count;
- integer s_write_nibble_count;
- logic [ 4:0] s_write_response_token;
- integer s_write_response_index;
- logic s_write_response_started;
- logic s_data_crc_error;
- logic s_data_timeout;
- logic s_irq_pending;
- logic [ 2:0] s_cmd_function;
- logic [ 31:0] s_cmd_address;
- logic s_cmd_fixed_address;
- integer s_cmd_count;
+ data_state_e s_data_state;
+ logic s_data_width4;
+ logic s_data_fixed;
+ integer s_data_address;
+ integer s_data_bytes;
+ wire [3:0] s_token_lanes = !s_data_width4 ? 4'b0001 :
+ (s_data_bytes >= 4 ? 4'b1111 : (4'b0001 << s_data_bytes) - 1'b1);
+ integer s_data_byte_index;
+ integer s_data_bit_index;
+ integer s_data_nibble_index;
+ integer s_data_crc_index;
+ integer s_data_delay;
+ logic [15:0] s_read_crc;
+ logic [15:0] s_read_crc_lane[0:3];
+ logic [15:0] s_write_crc;
+ logic [15:0] s_write_crc_lane[0:3];
+ logic [15:0] s_received_crc;
+ logic [15:0] s_received_crc_lane[0:3];
+ logic [7:0] s_write_byte;
+ logic [7:0] s_write_byte_lane[0:3];
+ integer s_write_bit_count;
+ integer s_write_nibble_count;
+ logic [4:0] s_write_response_token;
+ integer s_write_response_index;
+ logic s_write_response_started;
+ logic s_data_crc_error;
+ logic s_data_timeout;
+ logic s_irq_pending;
+ logic [2:0] s_cmd_function;
+ logic [31:0] s_cmd_address;
+ logic s_cmd_fixed_address;
+ integer s_cmd_count;
function automatic logic [6:0] crc7_40(input logic [39:0] data_i);
logic [6:0] crc;
@@ -404,17 +408,20 @@ module sdio_native_card_model #(
endtask
task automatic command_received(input logic [47:0] frame_i);
- logic [ 5:0] index;
- logic [31:0] argument;
- logic [ 6:0] expected_crc;
- logic [39:0] body;
- logic [31:0] response_payload;
- integer address;
+ logic [ 5:0] index;
+ logic [ 31:0] argument;
+ logic [ 6:0] expected_crc;
+ logic [ 39:0] body;
+ logic [ 31:0] response_payload;
+ logic [127:0] memory_csd_body;
+ logic application_command;
+ integer address;
begin
index = frame_i[45:40];
argument = frame_i[39:8];
body = frame_i[47:8];
expected_crc = crc7_40(body);
+ application_command = s_last_cmd == 6'd55;
s_last_cmd = index;
s_last_arg = argument;
s_last_cmd_data = 1'b0;
@@ -471,7 +478,15 @@ module sdio_native_card_model #(
cmd_do_o = 1'b1;
end
6'd2, 6'd9: begin
- make_r2_response(index == 6'd9 ? {HighCapacity ? 1'b0 : 1'b0, 127'd0} : 128'd0);
+ memory_csd_body = '0;
+ if (MemoryInitialization && index == 6'd9) begin
+ // The 136-bit wire frame holds the CSD at [127:0], including
+ // its CRC/end byte. make_r2_response appends that final byte.
+ memory_csd_body[119:118] = 2'd1;
+ memory_csd_body[75:72] = 4'd9;
+ memory_csd_body[61:40] = 22'(BlockCount / 1024 - 1);
+ end
+ make_r2_response(memory_csd_body);
s_cmd_response_active = 1'b1;
s_cmd_response_index = 0;
s_cmd_response_delay = s_response_delay;
@@ -492,6 +507,15 @@ module sdio_native_card_model #(
s_busy_count = BusyCycles;
end
6'd16, 6'd55, 6'd41, 6'd6, 6'd13: begin
+ if (MemoryInitialization) begin
+ if (index == 6'd41 && application_command) response_payload = 32'hc0ff8000;
+ if (index == 6'd6) begin
+ if (application_command) s_memory_width4 = argument == 2;
+ // This deterministic card supports default speed only.
+ else
+ response_payload = 32'h00400000;
+ end
+ end
queue_response(index, response_payload, 1'b0, s_inject_crc_error);
s_inject_crc_error = 1'b0;
end
@@ -654,9 +678,10 @@ module sdio_native_card_model #(
s_last_arg = 32'd0;
s_last_cmd_write = 1'b0;
s_last_cmd_data = 1'b0;
+ s_memory_width4 = 1'b0;
s_pending_busy = 1'b0;
s_busy_count = 0;
- end else if (cmd_oe_i) begin
+ end else if ((s_cmd_bits != 0) || (cmd_oe_i && cmd_do_i == 1'b0)) begin
if (s_cmd_bits == 0) begin
s_cmd_shift = {{47{1'b0}}, cmd_do_i};
s_cmd_bits = 1;
@@ -717,6 +742,7 @@ module sdio_native_card_model #(
s_last_arg = 32'd0;
s_last_cmd_write = 1'b0;
s_last_cmd_data = 1'b0;
+ s_memory_width4 = 1'b0;
s_pending_busy = 1'b0;
s_busy_count = 0;
s_data_state = DataIdle;
@@ -828,7 +854,7 @@ module sdio_native_card_model #(
dat_do_o = 4'b1111;
for (integer lane = 0; lane < 4; lane++) begin
if (lane < s_data_bytes) begin
- dat_do_o[lane] = s_read_crc_lane[lane][15-(s_data_crc_index+1)];
+ dat_do_o[lane] = s_read_crc_lane[lane][15-s_data_crc_index];
end
end
end
@@ -838,7 +864,7 @@ module sdio_native_card_model #(
end else begin
s_data_crc_index = s_data_crc_index + 1;
dat_do_o = 4'b1111;
- dat_do_o[0] = s_read_crc[15-(s_data_crc_index+1)];
+ dat_do_o[0] = s_read_crc[15-s_data_crc_index];
end
end
WriteResponse: begin
@@ -850,7 +876,9 @@ module sdio_native_card_model #(
s_write_response_index = s_write_response_index + 1;
dat_do_o[0] = s_write_response_token[4-s_write_response_index];
end else begin
- s_data_state = (s_write_response_token == 5'b00101) ? WriteBusy : DataIdle;
+ // Keep the enum assignment portable to Icarus's native SV parser.
+ if (s_write_response_token == 5'b00101) s_data_state = WriteBusy;
+ else s_data_state = DataIdle;
s_busy_count = BusyCycles;
dat_do_o[0] = (s_write_response_token == 5'b00101) ? 1'b0 : 1'b1;
end
@@ -879,7 +907,7 @@ module sdio_native_card_model #(
end else begin
case (s_data_state)
WriteWait: begin
- if ((dat_oe_i != 4'b0000) && (dat_do_i == 4'b0000)) begin
+ if ((dat_oe_i != 4'b0000) && ((dat_do_i & s_token_lanes) == 4'b0000)) begin
s_data_state = WritePayload;
s_write_bit_count = 0;
s_write_nibble_count = 0;
@@ -903,7 +931,8 @@ module sdio_native_card_model #(
s_data_crc_error = 1'b0;
if (s_data_width4) begin
for (integer lane = 0; lane < 4; lane++) begin
- if (s_received_crc_lane[lane] != s_write_crc_lane[lane]) begin
+ if ((s_data_bytes % 4 == 0 || lane < s_data_bytes % 4) &&
+ s_received_crc_lane[lane] != s_write_crc_lane[lane]) begin
s_data_crc_error = 1'b1;
end
end
@@ -938,6 +967,9 @@ module sdio_native_card_model #(
end
initial begin
+ if (MemoryInitialization && (!HighCapacity || SdioOnly || BlockBytes != 512 ||
+ BlockCount < 1024 || BlockCount % 1024 != 0))
+ $fatal(1, "SDIO enumeration profile requires an exactly representable SDHC capacity");
if ((BlockBytes < 1) || (BlockCount < 1) || (FunctionCount < 1) || (FunctionBytes < 256)) begin
$fatal(1, "sdio_native_card_model: invalid storage geometry");
end
@@ -955,6 +987,7 @@ module sdio_native_card_model #(
s_last_arg = 32'd0;
s_last_cmd_write = 1'b0;
s_last_cmd_data = 1'b0;
+ s_memory_width4 = 1'b0;
s_pending_busy = 1'b0;
s_busy_count = 0;
s_data_state = DataIdle;
diff --git a/tests/rtl/sdio_standalone_tb.sv b/tests/rtl/sdio_standalone_tb.sv
index 6d452204..1fe49df2 100644
--- a/tests/rtl/sdio_standalone_tb.sv
+++ b/tests/rtl/sdio_standalone_tb.sv
@@ -12,7 +12,9 @@
module sdio_standalone_tb #(
parameter bit CardHighCapacity = 1'b1,
- parameter bit RunFourBit = 1'b1
+ parameter bit RunFourBit = 1'b1,
+ parameter bit PadLoopback = 1'b0,
+ parameter int CardBusyCycles = 8
);
localparam logic [31:0] HostCtrl = 32'h00C;
localparam logic [31:0] ClockCtrl = 32'h010;
@@ -65,8 +67,21 @@ module sdio_standalone_tb #(
integer timeout_count_before;
integer crc_count_before;
- assign sdio.cmd_di_i = sdio.cmd_oe_o ? sdio.cmd_do_o : card_cmd_do;
- assign sdio.dat_di_i = (sdio.dat_oe_o & sdio.dat_do_o) | (~sdio.dat_oe_o & card_dat_do);
+ tri1 command_pad, data0_pad, data1_pad, data2_pad, data3_pad;
+ wire [3:0] data_pads = {data3_pad, data2_pad, data1_pad, data0_pad};
+ assign command_pad = sdio.cmd_oe_o ? sdio.cmd_do_o : 1'bz;
+ assign command_pad = sdio.cmd_oe_o ? 1'bz : card_cmd_do;
+ assign data0_pad = sdio.dat_oe_o[0] ? sdio.dat_do_o[0] : 1'bz;
+ assign data1_pad = sdio.dat_oe_o[1] ? sdio.dat_do_o[1] : 1'bz;
+ assign data2_pad = sdio.dat_oe_o[2] ? sdio.dat_do_o[2] : 1'bz;
+ assign data3_pad = sdio.dat_oe_o[3] ? sdio.dat_do_o[3] : 1'bz;
+ assign data0_pad = sdio.dat_oe_o[0] ? 1'bz : card_dat_do[0];
+ assign data1_pad = sdio.dat_oe_o[1] ? 1'bz : card_dat_do[1];
+ assign data2_pad = sdio.dat_oe_o[2] ? 1'bz : card_dat_do[2];
+ assign data3_pad = sdio.dat_oe_o[3] ? 1'bz : card_dat_do[3];
+ assign sdio.cmd_di_i = PadLoopback ? command_pad : (sdio.cmd_oe_o ? sdio.cmd_do_o : card_cmd_do);
+ assign sdio.dat_di_i = PadLoopback ? data_pads :
+ ((sdio.dat_oe_o & sdio.dat_do_o) | (~sdio.dat_oe_o & card_dat_do));
always #5 clk_i = ~clk_i;
@@ -87,15 +102,15 @@ module sdio_standalone_tb #(
.HighCapacity(CardHighCapacity),
.BlockBytes (512),
.BlockCount (128),
- .BusyCycles (8)
+ .BusyCycles (CardBusyCycles)
) u_card (
.rst_n_i (rst_n_i),
.sck_i (sdio.sck_o),
.cmd_oe_i(sdio.cmd_oe_o),
- .cmd_do_i(sdio.cmd_do_o),
+ .cmd_do_i(PadLoopback ? command_pad : sdio.cmd_do_o),
.cmd_do_o(card_cmd_do),
.dat_oe_i(sdio.dat_oe_o),
- .dat_do_i(sdio.dat_do_o),
+ .dat_do_i(PadLoopback ? data_pads : sdio.dat_do_o),
.dat_do_o(card_dat_do),
.irq_o (card_irq)
);
@@ -204,7 +219,13 @@ module sdio_standalone_tb #(
end
end
if (timeout_guard >= 200000) begin
- $fatal(1, "data path did not drain");
+ $fatal(
+ 1,
+ "data path did not drain: data_state=%0d dma_state=%0d rx=%b%b dma_bytes=%0d card_state=%0d card_bytes=%0d clk=%b",
+ u_apb4_sdio.u_sdio_core.u_sdio_data.s_state_q,
+ u_apb4_sdio.u_sdio_core.u_sdio_dma.s_state_q, u_apb4_sdio.u_sdio_core.s_data_rx_valid,
+ u_apb4_sdio.u_sdio_core.s_data_rx_ready, u_apb4_sdio.s_bytes_done,
+ u_card.u_model.s_data_state, u_card.u_model.s_data_byte_index, sdio.sck_o);
end
end
endtask
@@ -552,6 +573,84 @@ module sdio_standalone_tb #(
$fatal(1, "memory-to-card DMA end-to-end direction failed");
end
+ // P6 exercises a full four-lane DMA payload at divider 1, with memory
+ // arbitration delays. Short one-lane tails do not cover its word pauses.
+ if (RunFourBit) begin
+ u_axi_memory.clear_memory();
+ u_axi_memory.set_read_gap(24);
+ u_axi_memory.write_word(32'h0, 32'h200);
+ u_axi_memory.write_word(32'h4, 32'd1024);
+ u_axi_memory.write_word(32'h8, 32'd0);
+ u_axi_memory.write_word(32'hc, 32'h5);
+ for (integer word_index = 0; word_index < 256; word_index++)
+ u_axi_memory.write_word(32'h200 + 32'(word_index * 4), 32'h13579bdf ^ 32'(word_index));
+ configure_dma_data(1'b1, 1'b1, 16'd512);
+ apb_write(BlockCount, 32'd2, 4'hf);
+ u_card.arm_write(24000, 1024, 1'b1, 1'b0);
+ apb_write(DataStart, 32'd1, 4'h1);
+ wait_data();
+ apb_read(DataStatus, apb_value);
+ for (integer byte_index = 0; byte_index < 1024; byte_index++) begin
+ if (u_card.read_backing(
+ 24000 + byte_index
+ ) != 8'((32'h13579bdf ^ 32'(byte_index / 4)) >> ((byte_index % 4) * 8)))
+ $fatal(
+ 1,
+ "SDIO four-lane DMA payload byte=%0d got=%h expected=%h",
+ byte_index,
+ u_card.read_backing(
+ 24000 + byte_index
+ ),
+ 8'((32'h13579bdf ^ 32'(byte_index / 4)) >> ((byte_index % 4) * 8))
+ );
+ end
+ if ((apb_value & 32'h1e) != 0)
+ $fatal(
+ 1,
+ "SDIO four-lane DMA block status=%h card_crc=%b",
+ apb_value,
+ u_card.u_model.s_data_crc_error
+ );
+ if (u_axi_memory.read_word(32'hc) != 32'h00010004)
+ $fatal(1, "SDIO four-lane DMA descriptor did not retire");
+ // Delay descriptor reads while the card sends its first receive word.
+ u_axi_memory.clear_memory();
+ u_axi_memory.write_word(32'h0, 32'h800);
+ u_axi_memory.write_word(32'h4, 32'd1024);
+ u_axi_memory.write_word(32'h8, 32'd0);
+ u_axi_memory.write_word(32'hc, 32'h5);
+ configure_dma_data(1'b1, 1'b0, 16'd512);
+ apb_write(BlockCount, 32'd2, 4'hf);
+ u_card.arm_read(24000, 1024, 1'b1, 1'b0);
+ apb_write(DataStart, 32'd1, 4'h1);
+ wait_data();
+ apb_read(DataStatus, apb_value);
+ for (integer word_index = 0; word_index < 256; word_index++) begin
+ if (u_axi_memory.read_word(
+ 32'h800 + 32'(word_index * 4)
+ ) != (32'h13579bdf ^ 32'(word_index)))
+ $fatal(
+ 1,
+ "SDIO delayed-descriptor receive word=%0d got=%h expected=%h",
+ word_index,
+ u_axi_memory.read_word(
+ 32'h800 + 32'(word_index * 4)
+ ),
+ 32'h13579bdf ^ 32'(word_index)
+ );
+ end
+ if ((apb_value & 32'h1e) != 0 || u_axi_memory.read_word(32'hc) != 32'h00010004)
+ $fatal(
+ 1,
+ "SDIO delayed-descriptor receive status=%h descriptor=%h",
+ apb_value,
+ u_axi_memory.read_word(
+ 32'hc
+ )
+ );
+ u_axi_memory.set_read_gap(0);
+ end
+
apb_read(AxiCount, apb_value);
timeout_count_before = apb_value;
u_axi_memory.clear_memory();
@@ -675,3 +774,10 @@ endmodule
module sdio_onebit_tb;
sdio_standalone_tb #(.RunFourBit(1'b0)) u_tb ();
endmodule
+
+module tiny_p6_sdio_pads_tb;
+ sdio_standalone_tb #(
+ .PadLoopback (1'b1),
+ .CardBusyCycles(2)
+ ) u_tb ();
+endmodule
diff --git a/tests/rtl/tiny_apb_async_bridge_tb.sv b/tests/rtl/tiny_apb_async_bridge_tb.sv
new file mode 100644
index 00000000..0c3790c1
--- /dev/null
+++ b/tests/rtl/tiny_apb_async_bridge_tb.sv
@@ -0,0 +1,87 @@
+`timescale 1ns / 1ps
+module tiny_apb_async_bridge_tb;
+ logic clk_i = 0, dst_clk = 0, rst_n_i = 0, dst_rst_n = 0;
+ logic run_dst = 1, disabled = 0, idle, ready;
+ logic respond = 1;
+ integer writes = 0;
+ logic [31:0] saved;
+ logic [31:0] value;
+ always #5 clk_i = !clk_i;
+ always #17 if (run_dst) dst_clk = !dst_clk;
+ apb4_if apb4 (
+ .pclk (clk_i),
+ .presetn(rst_n_i)
+ );
+ apb4_if endpoint (
+ .pclk (dst_clk),
+ .presetn(dst_rst_n)
+ );
+ tiny_apb_async_bridge u_dut (
+ .src_clk_i (clk_i),
+ .src_rst_n_i(rst_n_i),
+ .dst_clk_i (dst_clk),
+ .dst_rst_n_i(dst_rst_n),
+ .disabled_i (disabled),
+ .idle_o (idle),
+ .ready_o (ready),
+ .src_apb4 (apb4),
+ .dst_apb4 (endpoint)
+ );
+ assign endpoint.pready = endpoint.psel && endpoint.penable && respond;
+ assign endpoint.pslverr = endpoint.paddr[8];
+ assign endpoint.prdata = saved ^ endpoint.paddr;
+ always @(posedge dst_clk) begin
+ if (dst_rst_n && endpoint.psel && endpoint.penable && endpoint.pready && endpoint.pwrite) begin
+ writes = writes + 1;
+ if (endpoint.pstrb != 4'h5 || endpoint.pprot != 0) $fatal(1, "APB payload changed");
+ saved = endpoint.pwdata;
+ end
+ end
+ `include "apb4_bfm.svh"
+ initial begin
+ apb4_idle();
+ #2;
+ rst_n_i = 0;
+ dst_rst_n = 0;
+ repeat (6) @(negedge clk_i);
+ rst_n_i = 1;
+ dst_rst_n = 1;
+ wait (ready);
+ repeat (5) @(negedge clk_i);
+ apb4_write(32'h20, 32'h87654321, 4'h5, 0);
+ apb4_read(32'h20, 0, value);
+ if (value != 32'h87654301 || writes != 1) $fatal(1, "APB CDC data/duplicate");
+ apb4_read(32'h100, 1, value);
+ respond = 0;
+ fork
+ apb4_read(32'h20, 1, value);
+ begin
+ wait (endpoint.psel && endpoint.penable);
+ #3 dst_rst_n = 0;
+ repeat (15) @(negedge clk_i);
+ dst_rst_n = 1;
+ end
+ join
+ respond = 1;
+ wait (ready);
+ repeat (30) @(negedge clk_i);
+ if (endpoint.psel) $fatal(1, "reset replayed an old APB request");
+ disabled = 1;
+ @(negedge dst_clk);
+ run_dst = 0;
+ apb4_read(32'h20, 1, value);
+ apb4_write(32'h20, 32'hbad, 4'h5, 1);
+ if (writes != 1) $fatal(1, "disabled APB side effect");
+ run_dst = 1;
+ disabled = 0;
+ repeat (30) @(negedge clk_i);
+ apb4_read(32'h20, 0, value);
+ if (value != 32'h87654301) $fatal(1, "CDC restart data");
+ $display("SIM_TEST_PASS Tiny APB CDC");
+ $finish;
+ end
+ initial begin
+ #100000;
+ $fatal(1, "SIM_TEST_TIMEOUT Tiny APB CDC");
+ end
+endmodule
diff --git a/tests/rtl/tiny_axi4_admission_tb.sv b/tests/rtl/tiny_axi4_admission_tb.sv
new file mode 100644
index 00000000..0f151659
--- /dev/null
+++ b/tests/rtl/tiny_axi4_admission_tb.sv
@@ -0,0 +1,135 @@
+`timescale 1ns / 1ps
+module tiny_axi4_admission_tb;
+ logic clk = 0, rst_n = 0, block_new = 0, idle;
+ always #5 clk = !clk;
+ axi4_if source (
+ .aclk (clk),
+ .aresetn(rst_n)
+ );
+ axi4_if sink (
+ .aclk (clk),
+ .aresetn(rst_n)
+ );
+ tiny_axi4_admission u_dut (
+ .clk_i (clk),
+ .rst_n_i(rst_n),
+ .block_i(block_new),
+ .idle_o (idle),
+ .source (source),
+ .sink (sink)
+ );
+ task automatic tick;
+ @(posedge clk);
+ #1;
+ @(negedge clk);
+ endtask
+ initial begin
+ source.awvalid = 0;
+ source.arvalid = 0;
+ source.wvalid = 0;
+ source.awid = 1;
+ source.awaddr = 'h30001234;
+ source.awlen = 1;
+ source.awsize = 2;
+ source.awburst = 1;
+ source.awlock = 0;
+ source.awcache = 0;
+ source.awprot = 0;
+ source.awqos = 0;
+ source.awregion = 0;
+ source.awuser = 0;
+ source.arid = 0;
+ source.araddr = 'h30005678;
+ source.arlen = 1;
+ source.arsize = 2;
+ source.arburst = 1;
+ source.arlock = 0;
+ source.arcache = 0;
+ source.arprot = 0;
+ source.arqos = 0;
+ source.arregion = 0;
+ source.aruser = 0;
+ source.wdata = 'h12345678;
+ source.wstrb = 15;
+ source.wlast = 0;
+ source.wuser = 0;
+ source.bready = 0;
+ source.rready = 0;
+ sink.awready = 0;
+ sink.arready = 0;
+ sink.wready = 1;
+ sink.bid = 1;
+ sink.bresp = 0;
+ sink.buser = 0;
+ sink.bvalid = 0;
+ sink.rid = 0;
+ sink.rdata = 'hdeadbeef;
+ sink.rresp = 0;
+ sink.rlast = 0;
+ sink.ruser = 0;
+ sink.rvalid = 0;
+ repeat (3) tick();
+ rst_n = 1;
+ tick();
+ source.awvalid = 1;
+ source.arvalid = 1;
+ source.wvalid = 1;
+ tick();
+ block_new = 1;
+ source.awvalid = 0;
+ source.arvalid = 0;
+ source.awaddr = 0;
+ source.araddr = 0;
+ repeat (5) begin
+ tick();
+ if (idle || !sink.awvalid || !sink.arvalid || source.wready ||
+ sink.awaddr != 'h30001234 || sink.araddr != 'h30005678)
+ $fatal(1, "accepted stalled address lost when admission blocked");
+ end
+ sink.awready = 1;
+ sink.arready = 1;
+ tick();
+ sink.awready = 0;
+ sink.arready = 0;
+ source.wlast = 1;
+ tick();
+ source.wvalid = 0;
+ sink.bvalid = 1;
+ sink.rvalid = 1;
+ repeat (3) begin
+ tick();
+ if (idle || !source.bvalid || !source.rvalid || source.rdata != 'hdeadbeef)
+ $fatal(1, "response backpressure lost outstanding work");
+ end
+ source.bready = 1;
+ source.rready = 1;
+ tick();
+ sink.bvalid = 0;
+ sink.rlast = 1;
+ if (idle) $fatal(1, "read retired before RLAST");
+ tick();
+ sink.rvalid = 0;
+ source.awvalid = 1;
+ source.arvalid = 1;
+ repeat (3) begin
+ tick();
+ if (!idle || source.awready || source.arready || sink.awvalid || sink.arvalid)
+ $fatal(1, "unaccepted blocked request counted as outstanding");
+ end
+ block_new = 0;
+ tick();
+ source.awvalid = 0;
+ source.arvalid = 0;
+ if (idle || !sink.awvalid || !sink.arvalid)
+ $fatal(1, "retained upstream requests did not resume");
+ rst_n = 0;
+ tick();
+ if (!idle || sink.awvalid || sink.arvalid) $fatal(1, "reset replayed old session");
+ $display("SIM_TEST_PASS Tiny AXI admission drain/backpressure/resume");
+ $finish;
+ end
+ initial begin
+ #10000;
+ $fatal(1, "SIM_TEST_TIMEOUT admission");
+ end
+endmodule
diff --git a/tests/rtl/tiny_axi4_async_bridge_tb.sv b/tests/rtl/tiny_axi4_async_bridge_tb.sv
new file mode 100644
index 00000000..577698e0
--- /dev/null
+++ b/tests/rtl/tiny_axi4_async_bridge_tb.sv
@@ -0,0 +1,432 @@
+`timescale 1ns / 1ps
+
+module tiny_axi4_async_bridge_tb;
+ logic src_clk = 1'b0;
+ logic dst_clk = 1'b0;
+ logic src_rst_n = 1'b0;
+ logic dst_rst_n = 1'b0;
+ logic clear = 1'b0;
+ logic clear_busy;
+ logic ready;
+ integer sink_ar_count = 0;
+ integer sink_aw_count = 0;
+ integer source_r_count = 0;
+ integer source_b_count = 0;
+ logic test_reset_response;
+ logic test_write_response;
+
+ always @(posedge src_clk) begin
+ if (src_rst_n && source.rvalid && source.rready) source_r_count++;
+ if (src_rst_n && source.bvalid && source.bready) source_b_count++;
+ end
+
+ always #5 src_clk = !src_clk;
+ always #7 dst_clk = !dst_clk;
+
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) source (
+ .aclk (src_clk),
+ .aresetn(src_rst_n)
+ );
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) sink (
+ .aclk (dst_clk),
+ .aresetn(dst_rst_n)
+ );
+
+ tiny_axi4_async_bridge u_dut (
+ .src_clk_i (src_clk),
+ .src_rst_n_i (src_rst_n),
+ .dst_clk_i (dst_clk),
+ .dst_rst_n_i (dst_rst_n),
+ .clear_i (clear),
+ .clear_busy_o(clear_busy),
+ .ready_o (ready),
+ .epoch_o (),
+ .src_axi4 (source),
+ .dst_axi4 (sink)
+ );
+
+ always @(posedge dst_clk) begin
+ if (dst_rst_n && sink.arvalid && sink.arready) sink_ar_count = sink_ar_count + 1;
+ if (dst_rst_n && sink.awvalid && sink.awready) sink_aw_count = sink_aw_count + 1;
+ end
+
+ task automatic source_defaults;
+ begin
+ source.awid = 0;
+ source.awaddr = '0;
+ source.awlen = 0;
+ source.awsize = 2;
+ source.awburst = 1;
+ source.awlock = 0;
+ source.awcache = 0;
+ source.awprot = 0;
+ source.awqos = 0;
+ source.awregion = 0;
+ source.awuser = 0;
+ source.awvalid = 0;
+ source.wdata = 32'h1234_5678;
+ source.wstrb = 4'hf;
+ source.wlast = 1;
+ source.wuser = 0;
+ source.wvalid = 0;
+ source.bready = 1;
+ source.arid = 0;
+ source.araddr = '0;
+ source.arlen = 0;
+ source.arsize = 2;
+ source.arburst = 1;
+ source.arlock = 0;
+ source.arcache = 0;
+ source.arprot = 0;
+ source.arqos = 0;
+ source.arregion = 0;
+ source.aruser = 0;
+ source.arvalid = 0;
+ source.rready = 1;
+ end
+ endtask
+
+ task automatic sink_defaults;
+ begin
+ sink.awready = 1;
+ sink.wready = 1;
+ sink.bid = 0;
+ sink.bresp = 0;
+ sink.buser = 0;
+ sink.bvalid = 0;
+ sink.arready = 1;
+ sink.rid = 0;
+ sink.rdata = 32'hcafe_babe;
+ sink.rresp = 0;
+ sink.rlast = 1;
+ sink.ruser = 0;
+ sink.rvalid = 0;
+ end
+ endtask
+
+ task automatic wait_ready;
+ begin
+ wait (ready === 1'b1);
+ repeat (3) @(posedge src_clk);
+ end
+ endtask
+
+ task automatic issue_read(input logic [31:0] address);
+ begin
+ @(negedge src_clk);
+ source.araddr = address;
+ source.arvalid = 1;
+ do @(posedge src_clk); while (!source.arready);
+ @(negedge src_clk);
+ source.arvalid = 0;
+ end
+ endtask
+
+ task automatic issue_write(input logic [31:0] address);
+ begin
+ @(negedge src_clk);
+ source.awaddr = address;
+ source.awvalid = 1;
+ do @(posedge src_clk); while (!source.awready);
+ @(negedge src_clk);
+ source.awvalid = 0;
+ source.wvalid = 1;
+ do @(posedge src_clk); while (!source.wready);
+ @(negedge src_clk);
+ source.wvalid = 0;
+ end
+ endtask
+
+ initial begin
+ source_defaults();
+ sink_defaults();
+ test_reset_response = $test$plusargs("same_cycle_reset_read") ||
+ $test$plusargs("same_cycle_reset_write");
+ test_write_response = $test$plusargs("same_cycle_write") ||
+ $test$plusargs("held_write_stability") || $test$plusargs("same_cycle_reset_write");
+ repeat (5) @(posedge src_clk);
+ src_rst_n = 1;
+ dst_rst_n = 1;
+ wait_ready();
+
+ if ($test$plusargs("clear_pending")) begin
+ source.rready = 0;
+ issue_read(32'h5400_0010);
+ repeat (12) @(negedge dst_clk);
+ @(negedge src_clk);
+ clear = 1;
+ wait (source.rvalid === 1'b1);
+ @(negedge src_clk);
+ if (source.rresp !== 2'b11) $fatal(1, "SIM_TEST_FAIL clear must return error");
+ clear = 0;
+ source.rready = 1;
+ @(posedge src_clk);
+ @(negedge src_clk);
+ repeat (200) @(negedge src_clk);
+ if (source_r_count != 1 || ready || source.arready)
+ $fatal(1, "SIM_TEST_FAIL pending clear lost isolation");
+ // A delayed pre-clear response must never complete a new request.
+ @(negedge dst_clk);
+ sink.rvalid = 1;
+ do @(posedge dst_clk); while (!sink.rready);
+ @(negedge dst_clk);
+ sink.rvalid = 0;
+ repeat (20) @(negedge src_clk);
+ if (source.rvalid || source_r_count != 1)
+ $fatal(1, "SIM_TEST_FAIL late response escaped clear isolation");
+ dst_rst_n = 0;
+ repeat (8) @(negedge src_clk);
+ dst_rst_n = 1;
+ wait_ready();
+ repeat (20) @(negedge src_clk);
+ if (source.rvalid || source_r_count != 1)
+ $fatal(1, "SIM_TEST_FAIL late response replay after reset");
+ $display("SIM_TEST_PASS Tiny AXI pending-clear quarantine/recovery");
+ $finish;
+ end
+
+ if ($test$plusargs("burst_abort")) begin
+ source.arlen = 3;
+ source.arid = 1;
+ source.rready = 0;
+ issue_read(32'h5400_0100);
+ wait (sink.arvalid === 1'b1);
+ @(negedge dst_clk);
+ sink.rvalid = 1;
+ sink.rlast = 0;
+ sink.rid = 1;
+ do @(posedge dst_clk); while (!sink.rready);
+ @(negedge dst_clk);
+ sink.rvalid = 0;
+ wait (source.rvalid === 1'b1);
+ @(negedge src_clk);
+ dst_rst_n = 0;
+ repeat (8) @(negedge src_clk);
+ // Preserve the already exposed first beat; synthesize exactly the three
+ // missing error beats, with RLAST only on the original fourth beat.
+ for (integer beat = 0; beat < 4; beat++) begin
+ wait (source.rvalid === 1'b1);
+ @(negedge src_clk);
+ if (source.rid !== 1'b1 || source.rlast !== (beat == 3) ||
+ source.rresp !== (beat == 0 ? 2'b00 : 2'b11))
+ $fatal(1, "SIM_TEST_FAIL reset read burst beat=%0d", beat);
+ source.rready = 1;
+ @(posedge src_clk);
+ @(negedge src_clk);
+ source.rready = 0;
+ end
+ dst_rst_n = 1;
+ wait_ready();
+ if (source_r_count != 4 || source.rvalid) $fatal(1, "SIM_TEST_FAIL read burst count");
+
+ source.awlen = 3;
+ source.awid = 1;
+ source.bready = 0;
+ @(negedge src_clk);
+ source.awvalid = 1;
+ do @(posedge src_clk); while (!source.awready);
+ @(negedge src_clk);
+ source.awvalid = 0;
+ dst_rst_n = 0;
+ repeat (8) @(negedge src_clk);
+ for (integer beat = 0; beat < 4; beat++) begin
+ if (source.bvalid) $fatal(1, "SIM_TEST_FAIL reset B before write data drain");
+ source.wvalid = 1;
+ source.wlast = beat == 3;
+ do @(posedge src_clk); while (!source.wready);
+ @(negedge src_clk);
+ source.wvalid = 0;
+ end
+ wait (source.bvalid === 1'b1);
+ @(negedge src_clk);
+ if (source.bid !== 1'b1 || source.bresp !== 2'b11)
+ $fatal(1, "SIM_TEST_FAIL reset burst write response");
+ source.bready = 1;
+ @(posedge src_clk);
+ @(negedge src_clk);
+ dst_rst_n = 1;
+ wait_ready();
+ repeat (20) @(negedge src_clk);
+ if (source_b_count != 1 || source.bvalid) $fatal(1, "SIM_TEST_FAIL write burst count");
+ $display("SIM_TEST_PASS Tiny AXI burst abort conservation");
+ $finish;
+ end
+
+ if (test_reset_response || $test$plusargs(
+ "same_cycle_clear"
+ ) || $test$plusargs(
+ "same_cycle_write"
+ ) || $test$plusargs(
+ "held_read_stability"
+ ) || $test$plusargs(
+ "held_write_stability"
+ )) begin
+ if (test_write_response) begin
+ source.bready = 0;
+ issue_write(32'h5400_0020);
+ repeat (12) @(negedge dst_clk);
+ sink.bvalid = 1;
+ do @(posedge dst_clk); while (!sink.bready);
+ @(negedge dst_clk);
+ sink.bvalid = 0;
+ wait (source.bvalid === 1'b1);
+ end else begin
+ source.rready = 0;
+ issue_read(32'h5400_0020);
+ wait (sink.arvalid === 1'b1);
+ @(negedge dst_clk);
+ sink.rvalid = 1;
+ do @(posedge dst_clk); while (!sink.rready);
+ @(negedge dst_clk);
+ sink.rvalid = 0;
+ wait (source.rvalid === 1'b1);
+ end
+ @(negedge src_clk);
+ if ($test$plusargs("held_read_stability") || $test$plusargs("held_write_stability")) begin
+ // Source reset remains high. A stalled response is still owned by
+ // the source interface even when the remote endpoint resets.
+ dst_rst_n = 0;
+ repeat (8) begin
+ @(negedge src_clk);
+ if ($test$plusargs(
+ "held_read_stability"
+ ) && (source.rvalid !== 1'b1 || source.rresp !== 2'b00 ||
+ source.rdata !== 32'hcafe_babe || source.rlast !== 1'b1))
+ $fatal(1, "SIM_TEST_FAIL stalled source R response changed on remote reset");
+ if ($test$plusargs(
+ "held_write_stability"
+ ) && (source.bvalid !== 1'b1 || source.bresp !== 2'b00))
+ $fatal(1, "SIM_TEST_FAIL stalled source B response changed on remote reset");
+ end
+ $display("SIM_TEST_PASS Tiny AXI held response stability");
+ $finish;
+ end
+ if (test_reset_response) begin
+ dst_rst_n = 0;
+ end else begin
+ clear = 1;
+ wait (u_dut.s_clear_src_pulse === 1'b1);
+ @(negedge src_clk);
+ end
+ source.rready = 1;
+ source.bready = 1;
+ @(posedge src_clk);
+ @(negedge src_clk);
+ clear = 0;
+ repeat (200) @(negedge src_clk);
+ if (test_write_response && source_b_count != 1)
+ $fatal(1, "SIM_TEST_FAIL one AW produced %0d B handshakes during clear", source_b_count);
+ if (!test_write_response && source_r_count != 1)
+ $fatal(1, "SIM_TEST_FAIL one AR produced %0d R handshakes during clear", source_r_count);
+ $display("SIM_TEST_PASS Tiny AXI same-cycle clear conservation");
+ $finish;
+ end
+
+ issue_read(32'h5400_0040);
+ wait (sink.arvalid === 1'b1);
+ @(negedge dst_clk);
+ sink.rvalid = 1;
+ do @(posedge dst_clk); while (!sink.rready);
+ @(negedge dst_clk);
+ sink.rvalid = 0;
+ wait (source.rvalid === 1'b1);
+ if (source.rresp != 2'b00 || source.rdata != 32'hcafe_babe || !source.rlast)
+ $fatal(1, "normal Tiny AXI read response mismatch");
+ @(posedge src_clk);
+ @(negedge src_clk);
+
+ source.rready = 0;
+ issue_read(32'h5400_0060);
+ wait (sink.arvalid === 1'b1);
+ @(negedge dst_clk);
+ sink.rvalid = 1;
+ do @(posedge dst_clk); while (!sink.rready);
+ @(negedge dst_clk);
+ sink.rvalid = 0;
+ wait (source.rvalid === 1'b1);
+ dst_rst_n = 0;
+ wait (u_dut.s_abort_read_q === 1'b1);
+ #1;
+ if (!source.rvalid || source.rresp != 2'b00 || !source.rlast || source.rdata != 32'hcafe_babe)
+ $fatal(1, "remote reset changed held read response");
+ source.rready = 1;
+ do @(posedge src_clk); while (u_dut.s_abort_read_q);
+ source.rready = 0;
+ @(negedge dst_clk);
+ dst_rst_n = 1;
+ wait_ready();
+ source.rready = 1;
+
+ issue_read(32'h5400_0080);
+ wait (sink.arvalid === 1'b1);
+ sink.arready = 0;
+ @(negedge dst_clk);
+ dst_rst_n = 0;
+ wait (source.rvalid === 1'b1);
+ if (source.rresp != 2'b11 || !source.rlast || source.rdata != 0)
+ $fatal(1, "destination reset did not return read DECERR");
+ if (sink.arvalid || sink.awvalid) $fatal(1, "destination reset retained stale request");
+ @(negedge dst_clk);
+ dst_rst_n = 1;
+ sink.arready = 1;
+ wait_ready();
+ if (sink_ar_count != 2) $fatal(1, "reset replayed the read request");
+
+ sink.awready = 0;
+ sink.wready = 0;
+ issue_write(32'h5400_00c0);
+ wait (sink.awvalid === 1'b1);
+ @(negedge dst_clk);
+ dst_rst_n = 0;
+ wait (source.bvalid === 1'b1);
+ if (source.bresp != 2'b11) $fatal(1, "destination reset did not return write DECERR");
+ if (sink.arvalid || sink.awvalid || sink.wvalid)
+ $fatal(1, "destination reset retained stale write request");
+ @(negedge dst_clk);
+ dst_rst_n = 1;
+ sink.awready = 1;
+ sink.wready = 1;
+ wait_ready();
+ if (sink_aw_count != 0) $fatal(1, "reset replayed the write request");
+
+ source.bready = 0;
+ issue_write(32'h5400_00e0);
+ repeat (12) @(negedge dst_clk);
+ @(negedge dst_clk);
+ sink.bvalid = 1;
+ do @(posedge dst_clk); while (!sink.bready);
+ @(negedge dst_clk);
+ sink.bvalid = 0;
+ wait (source.bvalid === 1'b1);
+ dst_rst_n = 0;
+ wait (u_dut.s_abort_write_q === 1'b1);
+ #1;
+ if (!source.bvalid || source.bresp != 2'b00)
+ $fatal(1, "remote reset changed held write response");
+ source.bready = 1;
+ do @(posedge src_clk); while (u_dut.s_abort_write_q);
+ source.bready = 0;
+ @(negedge dst_clk);
+ dst_rst_n = 1;
+ wait_ready();
+ source.bready = 1;
+
+ $display("SIM_TEST_PASS Tiny AXI CDC reset abort/error/no-replay");
+ $finish;
+ end
+
+ initial begin
+ #100000;
+ $fatal(1, "SIM_TEST_TIMEOUT Tiny AXI CDC");
+ end
+endmodule
diff --git a/tests/rtl/tiny_axi4_fabric_tb.sv b/tests/rtl/tiny_axi4_fabric_tb.sv
index 58f0cff4..85c499b4 100644
--- a/tests/rtl/tiny_axi4_fabric_tb.sv
+++ b/tests/rtl/tiny_axi4_fabric_tb.sv
@@ -1,14 +1,16 @@
+`include "tiny_topology.svh"
`timescale 1ns / 1ps
module tiny_axi4_fabric_tb;
logic clk_i = 0;
logic rst_n_i = 0;
+ logic target_rst_n[`RETROSOC_TINY__TARGET_COUNT];
always #5 clk_i = !clk_i;
axi4_if #(
.ADDR_WIDTH(32),
.DATA_WIDTH(32),
.ID_WIDTH (1),
.USER_WIDTH(1)
- ) masters[2] (
+ ) masters[3] (
.aclk (clk_i),
.aresetn(rst_n_i)
);
@@ -17,65 +19,71 @@ module tiny_axi4_fabric_tb;
.DATA_WIDTH(32),
.ID_WIDTH (1),
.USER_WIDTH(1)
- ) targets[5] (
+ ) targets[`RETROSOC_TINY__TARGET_COUNT] (
.aclk (clk_i),
.aresetn(rst_n_i)
);
- logic fault_valid, fault_master, fault_write;
- logic [31:0] fault_addr;
- logic [ 1:0] fault_resp;
+ logic [5:0] fault_valid, fault_instruction;
+ logic [5:0][31:0] fault_addr;
+ logic [5:0][ 1:0] fault_resp;
logic [63:0] cpu_wait, dma_wait;
integer faults = 0;
- integer completions [2];
- integer target_requests[5];
- logic [1:0][31:0] awaddr;
- logic [1:0][ 7:0] awlen;
- logic [1:0][ 2:0] awsize;
- logic [1:0][ 1:0] awburst;
- logic [1:0][ 0:0] awlock;
- logic [1:0][ 0:0] awid;
- logic [1:0][ 0:0] awvalid;
- logic [1:0][31:0] wdata;
- logic [1:0][ 3:0] wstrb;
- logic [1:0][ 0:0] wlast;
- logic [1:0][ 0:0] wvalid;
- logic [1:0][ 0:0] bready;
- logic [1:0][31:0] araddr;
- logic [1:0][ 7:0] arlen;
- logic [1:0][ 2:0] arsize;
- logic [1:0][ 1:0] arburst;
- logic [1:0][ 0:0] arlock;
- logic [1:0][ 0:0] arid;
- logic [1:0][ 0:0] arvalid;
- logic [1:0][ 0:0] rready;
- logic [1:0][ 0:0] awready;
- logic [1:0][ 0:0] arready;
- logic [1:0][ 0:0] wready;
- logic [1:0][ 0:0] bvalid;
- logic [1:0][ 0:0] bid;
- logic [1:0][ 1:0] bresp;
- logic [1:0][ 0:0] rvalid;
- logic [1:0][ 0:0] rid;
- logic [1:0][ 0:0] rlast;
- logic [1:0][31:0] rdata;
- logic [1:0][ 1:0] rresp;
+ integer completions [ 3];
+ integer target_requests[`RETROSOC_TINY__TARGET_COUNT];
+ logic [2:0][31:0] awaddr;
+ logic [2:0][ 7:0] awlen;
+ logic [2:0][ 2:0] awsize;
+ logic [2:0][ 1:0] awburst;
+ logic [2:0][ 0:0] awlock;
+ logic [2:0][ 0:0] awid;
+ logic [2:0][ 0:0] awvalid;
+ logic [2:0][31:0] wdata;
+ logic [2:0][ 3:0] wstrb;
+ logic [2:0][ 0:0] wlast;
+ logic [2:0][ 0:0] wvalid;
+ logic [2:0][ 0:0] bready;
+ logic [2:0][31:0] araddr;
+ logic [2:0][ 7:0] arlen;
+ logic [2:0][ 2:0] arsize;
+ logic [2:0][ 1:0] arburst;
+ logic [2:0][ 0:0] arlock;
+ logic [2:0][ 0:0] arid;
+ logic [2:0][ 0:0] arvalid;
+ logic [2:0][ 2:0] arprot;
+ logic [2:0][ 0:0] rready;
+ logic [2:0][ 0:0] awready;
+ logic [2:0][ 0:0] arready;
+ logic [2:0][ 0:0] wready;
+ logic [2:0][ 0:0] bvalid;
+ logic [2:0][ 0:0] bid;
+ logic [2:0][ 1:0] bresp;
+ logic [2:0][ 0:0] rvalid;
+ logic [2:0][ 0:0] rid;
+ logic [2:0][ 0:0] rlast;
+ logic [2:0][31:0] rdata;
+ logic [2:0][ 1:0] rresp;
tiny_axi4_fabric u_dut (
- .clk_i (clk_i),
- .rst_n_i (rst_n_i),
- .perf_enable_i (1'b1),
- .perf_clear_i (1'b0),
- .masters (masters),
- .targets (targets),
- .fault_valid_o (fault_valid),
- .fault_addr_o (fault_addr),
- .fault_master_o(fault_master),
- .fault_write_o (fault_write),
- .fault_resp_o (fault_resp),
- .cpu_wait_o (cpu_wait),
- .dma_wait_o (dma_wait)
+ .clk_i (clk_i),
+ .rst_n_i (rst_n_i),
+ .target_rst_n_i (target_rst_n),
+ .perf_enable_i (1'b1),
+ .perf_clear_i (1'b0),
+ .masters (masters),
+ .targets (targets),
+ .fault_valid_o (fault_valid),
+ .fault_addr_o (fault_addr),
+ .fault_instruction_o(fault_instruction),
+ .owner_idle_o (),
+ .target_idle_o (),
+ .fault_resp_o (fault_resp),
+ .cpu_wait_o (cpu_wait),
+ .dma_wait_o (dma_wait)
);
- always @(posedge clk_i) if (fault_valid) faults <= faults + 1;
- for (genvar m = 0; m < 2; m++) begin : gen_master
+ for (genvar target = 0; target < `RETROSOC_TINY__TARGET_COUNT; target++) begin : gen_target_reset
+ assign target_rst_n[target] = rst_n_i;
+ end
+ always @(posedge clk_i) faults <= faults + $countones(fault_valid);
+ for (genvar m = 0; m < 3; m++) begin : gen_master
assign masters[m].awaddr = awaddr[m];
assign masters[m].awlen = awlen[m];
assign masters[m].awsize = awsize[m];
@@ -113,17 +121,17 @@ module tiny_axi4_fabric_tb;
assign masters[m].awregion = '0;
assign masters[m].awuser = '0;
assign masters[m].arcache = '0;
- assign masters[m].arprot = '0;
+ assign masters[m].arprot = arprot[m];
assign masters[m].arqos = '0;
assign masters[m].arregion = '0;
assign masters[m].aruser = '0;
assign masters[m].wuser = '0;
end
- for (genvar t = 0; t < 5; t++) begin : gen_target
+ for (genvar t = 0; t < `RETROSOC_TINY__TARGET_COUNT; t++) begin : gen_target
// Deterministic terminal responder allows independent checks of routing,
// burst length and ID preservation without sharing the fabric decoder.
axi4_error_slave #(
- .Response(t == 3 ? 2'b11 : (t == 4 ? 2'b10 : 2'b00))
+ .Response(t == `RETROSOC_TINY__DECODE_ERROR ? 2'b11 : (t == `RETROSOC_TINY__PROTOCOL_ERROR ? 2'b10 : 2'b00))
) u_response (
.clk_i (clk_i),
.rst_n_i(rst_n_i),
@@ -238,9 +246,11 @@ module tiny_axi4_fabric_tb;
arlock = '0;
arid = '0;
arvalid = '0;
+ arprot = '0;
rready = '0;
completions[0] = 0;
completions[1] = 0;
+ completions[2] = 0;
repeat (5) @(negedge clk_i);
rst_n_i = 1;
fork
@@ -263,10 +273,18 @@ module tiny_axi4_fabric_tb;
end
join
if (completions[0]!=7 || completions[1]!=6 || faults!=7 ||
- target_requests[0]!=3 || target_requests[1]!=1 || target_requests[2]!=2 ||
- target_requests[3]!=3 || target_requests[4]!=4 || cpu_wait==0 || dma_wait==0)
+ target_requests[0]!=2 || target_requests[3]!=1 || target_requests[4]!=1 ||
+ target_requests[6]!=2 || target_requests[`RETROSOC_TINY__DECODE_ERROR]!=3 || target_requests[`RETROSOC_TINY__PROTOCOL_ERROR]!=4)
$fatal(1, "accounting/fairness mismatch faults=%0d", faults);
read_request(0, 32'h30000000, 1, 2, 1, 0);
+ arprot[0] = 3'b100;
+ read_request(0, 32'h10001000, 1, 2, 1, 2);
+ read_request(0, 32'h54000000, 1, 2, 1, 3);
+ if (target_requests[6] != 2 || target_requests[4] != 1)
+ $fatal(1, "instruction request reached a peripheral or non-NSS0 window");
+ read_request(0, 32'h00000000, 1, 2, 1, 0);
+ read_request(0, 32'h50000000, 1, 2, 1, 0);
+ if (target_requests[4] != 3) $fatal(1, "NOR instruction windows not reachable");
$display("SIM_TEST_PASS Tiny fabric");
$finish;
end
diff --git a/tests/rtl/tiny_clock_external_tb.sv b/tests/rtl/tiny_clock_external_tb.sv
new file mode 100644
index 00000000..2c8a1fe0
--- /dev/null
+++ b/tests/rtl/tiny_clock_external_tb.sv
@@ -0,0 +1,159 @@
+`timescale 1ns / 1ps
+module tiny_clock_external_tb;
+ localparam integer XinHz = `TINY_TEST_XIN_HZ;
+ logic xin = 0, run_xin = 1, rst_n = 0, apply = 0;
+ logic [1:0] profile = 0, current_profile;
+ logic ref_clk, sys_clk, mem_clk, pclk, done, busy, fault;
+ logic [31:0] xin_hz, ref_hz, sys_hz, mem_hz, pclk_hz;
+ logic por_reset_n, old_por_reset_n;
+ logic [2:0] reset_outputs, old_reset_outputs;
+ integer ref_edges = 0;
+ always #(500000000.0 / XinHz) if (run_xin) xin = !xin;
+ tiny_reset_tree #(
+ .LeafCount (1),
+ .SplitDomains(1)
+ ) u_reset_tree (
+ .clk_i (sys_clk),
+ .por_clk_i (ref_clk),
+ .leaf_clk_i (sys_clk),
+ .backend_reset_n_i (1'b1),
+ .rst_n_i (rst_n),
+ .watchdog_reset_req_i(1'b0),
+ .por_rst_n_o (por_reset_n),
+ .system_rst_n_o (reset_outputs[0]),
+ .leaf_rst_n_o (reset_outputs[1]),
+ .cpu_ready_rst_n_o (reset_outputs[2])
+ );
+ tiny_reset_tree #(
+ .LeafCount (1),
+ .SplitDomains(1)
+ ) u_old_reset_tree (
+ .clk_i (sys_clk),
+ .por_clk_i (xin),
+ .leaf_clk_i (sys_clk),
+ .backend_reset_n_i (1'b1),
+ .rst_n_i (rst_n),
+ .watchdog_reset_req_i(1'b0),
+ .por_rst_n_o (old_por_reset_n),
+ .system_rst_n_o (old_reset_outputs[0]),
+ .leaf_rst_n_o (old_reset_outputs[1]),
+ .cpu_ready_rst_n_o (old_reset_outputs[2])
+ );
+ always @(posedge ref_clk or negedge rst_n) begin
+ if (!rst_n) ref_edges = 0;
+ else ref_edges++;
+ end
+ always @(posedge por_reset_n) begin
+ if (ref_edges < 5) $fatal(1, "AON reset released before five REF24 edges");
+ end
+ always @(negedge xin) begin
+ if (XinHz == 24000000 && rst_n &&
+ ({por_reset_n, reset_outputs} !== {old_por_reset_n, old_reset_outputs}))
+ $fatal(1, "24 MHz reset connection correction changed behavior");
+ end
+ tiny_clock_reset_subsystem #(
+ .RefClkHz(XinHz),
+ .HavePll (1'b0)
+ ) u_dut (
+ .ref_clk_i (xin),
+ .rst_n_i (rst_n),
+ .profile_i (profile),
+ .apply_i (apply),
+ .idle_i (1'b1),
+ .clients_idle_i (1'b1),
+ .warm_reset_i (1'b0),
+ .system_request_i (1'b0),
+ .command_ack_i (1'b1),
+ .timeout_i (32'd512),
+ .current_profile_o(current_profile),
+ .busy_o (busy),
+ .fault_o (fault),
+ .fault_code_o (),
+ .done_o (done),
+ .block_o (),
+ .ref_clk_o (ref_clk),
+ .ref_rst_n_o (),
+ .system_reset_n_o (),
+ .pll_locked_o (),
+ .pll_selected_o (),
+ .sys_clk_o (sys_clk),
+ .mem_clk_o (mem_clk),
+ .pclk_o (pclk),
+ .sys_rst_n_o (),
+ .mem_rst_n_o (),
+ .pclk_rst_n_o (),
+ .xin_hz_o (xin_hz),
+ .ref_hz_o (ref_hz),
+ .sys_hz_o (sys_hz),
+ .mem_hz_o (mem_hz),
+ .pclk_hz_o (pclk_hz)
+ );
+ task automatic command(input logic [1:0] requested);
+ @(negedge ref_clk);
+ profile = requested;
+ apply = 1;
+ @(negedge ref_clk);
+ apply = 0;
+ repeat (600) @(negedge ref_clk);
+ if (busy) $fatal(1, "external clock command stuck");
+ endtask
+ task automatic measure(input real expected_sys, input real expected_pclk);
+ realtime start_time, period;
+ @(posedge sys_clk);
+ start_time = $realtime;
+ repeat (100) @(posedge sys_clk);
+ period = ($realtime - start_time) / 100.0;
+ if (period < expected_sys - 0.01 || period > expected_sys + 0.01)
+ $fatal(1, "external SYS frequency mismatch");
+ @(posedge mem_clk);
+ start_time = $realtime;
+ repeat (100) @(posedge mem_clk);
+ period = ($realtime - start_time) / 100.0;
+ if (period < expected_sys - 0.01 || period > expected_sys + 0.01)
+ $fatal(1, "external MEM frequency mismatch");
+ @(posedge pclk);
+ start_time = $realtime;
+ repeat (100) @(posedge pclk);
+ period = ($realtime - start_time) / 100.0;
+ if (period < expected_pclk - 0.02 || period > expected_pclk + 0.02)
+ $fatal(1, "external PCLK frequency mismatch");
+ endtask
+ initial begin
+ #1 rst_n = 1;
+ #1 rst_n = 0;
+ repeat (20) @(negedge xin);
+ rst_n = 1;
+ repeat (200) @(negedge xin);
+ if (xin_hz != XinHz || ref_hz != 24000000 || sys_hz != 24000000)
+ $fatal(1, "external SAFE24 normalization failed");
+ measure(1000.0 / 24.0, 1000.0 / 24.0);
+ command(1);
+ if (fault || current_profile != 1 || sys_hz != XinHz || mem_hz != XinHz ||
+ pclk_hz != (XinHz > 60000000 ? XinHz / 2 : XinHz))
+ $fatal(1, "external profile commit failed");
+ measure(1000000000.0 / XinHz, (XinHz > 60000000 ? 2000000000.0 : 1000000000.0) / XinHz);
+ command(0);
+ measure(1000.0 / 24.0, 1000.0 / 24.0);
+ command(2);
+ if (!fault || current_profile != 0) $fatal(1, "no-PLL profile accepted a PLL request");
+ @(negedge xin);
+ run_xin = 0;
+ #1000;
+ // XIN loss has no independent safety clock. Recovery is external reset.
+ rst_n = 0;
+ #100;
+ if (por_reset_n || reset_outputs != 0) $fatal(1, "stopped-clock reset did not assert");
+ run_xin = 1;
+ repeat (20) @(negedge xin);
+ rst_n = 1;
+ repeat (200) @(negedge xin);
+ if (fault || current_profile != 0) $fatal(1, "external reset recovery failed");
+ measure(1000.0 / 24.0, 1000.0 / 24.0);
+ $display("SIM_TEST_PASS Tiny external XIN=%0d", XinHz);
+ $finish;
+ end
+ initial begin
+ #1000000;
+ $fatal(1, "SIM_TEST_TIMEOUT Tiny external clock");
+ end
+endmodule
diff --git a/tests/rtl/tiny_clock_output_tb.sv b/tests/rtl/tiny_clock_output_tb.sv
new file mode 100644
index 00000000..9b6ea09c
--- /dev/null
+++ b/tests/rtl/tiny_clock_output_tb.sv
@@ -0,0 +1,56 @@
+`timescale 1ns / 1ps
+module tiny_clock_output_tb;
+ logic ref_clk = 0, sys_clk = 0, mem_clk = 0, pclk = 0, rst_n = 0;
+ logic [31:0] control = 0;
+ wire clk_out, enabled;
+ always #20 ref_clk = !ref_clk;
+ always #2.5 sys_clk = !sys_clk;
+ always #5 mem_clk = !mem_clk;
+ always #10 pclk = !pclk;
+ tiny_clock_output u_dut (
+ .clk_ref_i (ref_clk),
+ .clk_sys_i (sys_clk),
+ .clk_mem_i (mem_clk),
+ .clk_periph_i(pclk),
+ .rst_n_i (rst_n),
+ .control_i (control),
+ .clk_o (clk_out),
+ .enable_o (enabled)
+ );
+ task automatic measure(input integer source, divisor, input realtime expected);
+ realtime previous_edge;
+ @(negedge ref_clk);
+ control = (divisor << 8) | source;
+ repeat (12) @(negedge ref_clk);
+ if (!enabled) $fatal(1, "CLKOUT enable missing");
+ @(posedge clk_out);
+ previous_edge = $realtime;
+ repeat (8) begin
+ @(posedge clk_out);
+ if ($realtime - previous_edge < expected - 0.001 ||
+ $realtime - previous_edge > expected + 0.001)
+ $fatal(1, "CLKOUT source/divisor period mismatch source=%0d", source);
+ previous_edge = $realtime;
+ end
+ @(negedge ref_clk);
+ control = 0;
+ repeat (12) @(negedge ref_clk);
+ if (enabled || clk_out) $fatal(1, "CLKOUT disable not acknowledged low");
+ endtask
+ initial begin
+ repeat (4) @(negedge ref_clk);
+ rst_n = 1;
+ repeat (4) @(negedge ref_clk);
+ if (enabled || clk_out) $fatal(1, "CLKOUT active at cold reset");
+ measure(1, 1, 80.0);
+ measure(2, 5, 50.0);
+ measure(3, 3, 60.0);
+ measure(4, 2, 80.0);
+ $display("SIM_TEST_PASS Tiny CLKOUT measured sources/divisors/off");
+ $finish;
+ end
+ initial begin
+ #100000;
+ $fatal(1, "SIM_TEST_TIMEOUT CLKOUT");
+ end
+endmodule
diff --git a/tests/rtl/tiny_clock_reset_pll_tb.sv b/tests/rtl/tiny_clock_reset_pll_tb.sv
new file mode 100644
index 00000000..7a3f9ddc
--- /dev/null
+++ b/tests/rtl/tiny_clock_reset_pll_tb.sv
@@ -0,0 +1,175 @@
+`timescale 1ns / 1ps
+
+module tiny_clock_reset_pll_tb;
+ logic ref_clk_i = 1'b0;
+ logic rst_n_i = 1'b0;
+ logic [1:0] profile = 2'd0;
+ logic apply = 1'b0;
+ logic [1:0] current_profile;
+ logic busy, fault;
+ logic idle = 1, clients_idle = 1;
+ logic reset_session_n, done, pll_selected;
+ logic [4:0] fault_code;
+ logic [4:0] observed_faults = 0;
+ always @(posedge ref_clk_i) observed_faults <= observed_faults | fault_code;
+ logic sys_clk, mem_clk, pclk;
+ logic sys_rst_n, mem_rst_n, pclk_rst_n;
+ logic [31:0] xin_hz, ref_hz, sys_hz, mem_hz, pclk_hz;
+
+ // Match the selected 24 MHz reference expected by the ICS55 PLL model.
+ always #20.833333 ref_clk_i = !ref_clk_i;
+
+ tiny_clock_reset_subsystem #(
+ .RefClkHz(24_000_000),
+ .HavePll (1'b1)
+ ) u_dut (
+ .ref_clk_i (ref_clk_i),
+ .rst_n_i (rst_n_i),
+ .profile_i (profile),
+ .apply_i (apply),
+ .idle_i (idle),
+ .clients_idle_i (clients_idle),
+ .warm_reset_i (1'b0),
+ .system_request_i (1'b0),
+ .command_ack_i (1'b1),
+`ifdef TINY_PLL_SUCCESS
+ .timeout_i (32'd10000),
+`else
+ .timeout_i (32'd512),
+`endif
+ .fault_code_o (fault_code),
+ .done_o (done),
+ .block_o (),
+ .ref_clk_o (),
+ .ref_rst_n_o (),
+ .system_reset_n_o (reset_session_n),
+ .pll_locked_o (),
+ .pll_selected_o (pll_selected),
+ .current_profile_o(current_profile),
+ .busy_o (busy),
+ .fault_o (fault),
+ .sys_clk_o (sys_clk),
+ .mem_clk_o (mem_clk),
+ .pclk_o (pclk),
+ .sys_rst_n_o (sys_rst_n),
+ .mem_rst_n_o (mem_rst_n),
+ .pclk_rst_n_o (pclk_rst_n),
+ .xin_hz_o (xin_hz),
+ .ref_hz_o (ref_hz),
+ .sys_hz_o (sys_hz),
+ .mem_hz_o (mem_hz),
+ .pclk_hz_o (pclk_hz)
+ );
+
+ task automatic apply_profile(input logic [1:0] requested);
+ begin
+ @(negedge ref_clk_i);
+ profile = requested;
+ apply = 1'b1;
+ @(negedge ref_clk_i);
+ apply = 1'b0;
+ end
+ endtask
+
+ task automatic wait_done;
+ integer cycles;
+ begin
+ cycles = 0;
+ while (!done && cycles < 12000) begin
+ @(negedge ref_clk_i);
+ cycles++;
+ end
+ if (!done || busy) $fatal(1, "backend did not finish with bounded handshake");
+ end
+ endtask
+
+ task automatic check_periods(input real sys_period, input real mem_period,
+ input real pclk_period);
+ realtime started, measured;
+ begin
+ @(posedge sys_clk);
+ started = $realtime;
+ repeat (100) @(posedge sys_clk);
+ measured = ($realtime - started) / 100.0;
+ if (measured < sys_period - 0.01 || measured > sys_period + 0.01)
+ $fatal(1, "SYS measured period %f expected %f", measured, sys_period);
+ @(posedge mem_clk);
+ started = $realtime;
+ repeat (100) @(posedge mem_clk);
+ measured = ($realtime - started) / 100.0;
+ if (measured < mem_period - 0.02 || measured > mem_period + 0.02)
+ $fatal(1, "MEM measured period %f expected %f", measured, mem_period);
+ @(posedge pclk);
+ started = $realtime;
+ repeat (100) @(posedge pclk);
+ measured = ($realtime - started) / 100.0;
+ if (measured < pclk_period - 0.04 || measured > pclk_period + 0.04)
+ $fatal(1, "PCLK measured period %f expected %f", measured, pclk_period);
+ end
+ endtask
+
+ initial begin
+ #1 rst_n_i = 1;
+ #1 rst_n_i = 0;
+ repeat (5) @(negedge ref_clk_i);
+ rst_n_i = 1'b1;
+ repeat (32) @(negedge ref_clk_i);
+ idle = 0;
+ clients_idle = 0;
+ apply_profile(2'd2);
+ repeat (30) @(negedge ref_clk_i);
+ if (!busy || current_profile != 0 || pll_selected)
+ $fatal(1, "profile changed before accepted work drained");
+ idle = 1;
+ clients_idle = 1;
+ wait_done();
+`ifdef TINY_PLL_SUCCESS
+ if (fault || current_profile != 2 || sys_hz != 192_000_000 ||
+ mem_hz != 96_000_000 || pclk_hz != 48_000_000)
+ $fatal(1, "PLL192 commit mismatch");
+ check_periods(1000.0 / 192.0, 1000.0 / 96.0, 1000.0 / 48.0);
+ apply_profile(2'd3);
+ wait_done();
+ if (fault || current_profile != 3 || sys_hz != 240_000_000 ||
+ mem_hz != 120_000_000 || pclk_hz != 60_000_000)
+ $fatal(1, "PLL240 commit mismatch");
+ check_periods(1000.0 / 240.0, 1000.0 / 120.0, 1000.0 / 60.0);
+ apply_profile(2'd0);
+ wait_done();
+ if (current_profile != 0 || sys_hz != 24_000_000) $fatal(1, "SAFE24 return failed");
+ check_periods(1000.0 / 24.0, 1000.0 / 24.0, 1000.0 / 24.0);
+`ifdef TINY_PLL_NATIVE
+ // Real locked ICS55 monitor must detect either stopped polarity, reset
+ // the affected session, and restart from REF24 without a PLL edge.
+ apply_profile(2'd2);
+ wait_done();
+ force u_dut.u_pll.gen_ecos.u_backend.pll_clk_o = 1'b0;
+ repeat (400) @(negedge ref_clk_i);
+ if (!observed_faults[3] || current_profile != 0 || !reset_session_n)
+ $fatal(1, "PLL stopped-low recovery failed");
+ check_periods(1000.0 / 24.0, 1000.0 / 24.0, 1000.0 / 24.0);
+ release u_dut.u_pll.gen_ecos.u_backend.pll_clk_o;
+ apply_profile(2'd3);
+ wait_done();
+ force u_dut.u_pll.gen_ecos.u_backend.pll_clk_o = 1'b1;
+ repeat (400) @(negedge ref_clk_i);
+ if (current_profile != 0 || !reset_session_n) $fatal(1, "PLL stopped-high recovery failed");
+ check_periods(1000.0 / 24.0, 1000.0 / 24.0, 1000.0 / 24.0);
+ release u_dut.u_pll.gen_ecos.u_backend.pll_clk_o;
+`endif
+ $display("SIM_TEST_PASS Tiny PLL transitions and measured clocks");
+`else
+ if (!fault || current_profile != 0 || observed_faults != 5'b00100 ||
+ sys_hz != 24_000_000 || mem_hz != 24_000_000 || pclk_hz != 24_000_000)
+ $fatal(1, "PLL lock timeout was not fail-closed");
+ check_periods(1000.0 / 24.0, 1000.0 / 24.0, 1000.0 / 24.0);
+ $display("SIM_TEST_PASS Tiny PLL lock-timeout backend");
+`endif
+ $finish;
+ end
+
+ initial begin
+ #3000000;
+ $fatal(1, "SIM_TEST_TIMEOUT Tiny PLL clock/reset backend");
+ end
+endmodule
diff --git a/tests/rtl/tiny_clock_reset_subsystem_tb.sv b/tests/rtl/tiny_clock_reset_subsystem_tb.sv
new file mode 100644
index 00000000..f8ec9b40
--- /dev/null
+++ b/tests/rtl/tiny_clock_reset_subsystem_tb.sv
@@ -0,0 +1,87 @@
+`timescale 1ns / 1ps
+
+module tiny_clock_reset_subsystem_tb;
+ logic ref_clk_i = 1'b0;
+ logic rst_n_i = 1'b0;
+ logic [1:0] profile = 2'd0;
+ logic apply = 1'b0;
+ logic [1:0] current_profile;
+ logic busy, fault;
+ logic sys_clk, mem_clk, pclk;
+ logic sys_rst_n, mem_rst_n, pclk_rst_n;
+ logic [31:0] xin_hz, ref_hz, sys_hz, mem_hz, pclk_hz;
+
+ always #5 ref_clk_i = !ref_clk_i;
+
+ tiny_clock_reset_subsystem #(
+ .RefClkHz(24_000_000),
+ .HavePll (1'b0)
+ ) u_dut (
+ .ref_clk_i (ref_clk_i),
+ .rst_n_i (rst_n_i),
+ .profile_i (profile),
+ .apply_i (apply),
+ .idle_i (1'b1),
+ .clients_idle_i (1'b1),
+ .warm_reset_i (1'b0),
+ .timeout_i (32'd512),
+ .system_request_i (1'b0),
+ .command_ack_i (1'b1),
+ .fault_code_o (),
+ .done_o (),
+ .block_o (),
+ .ref_clk_o (),
+ .ref_rst_n_o (),
+ .system_reset_n_o (),
+ .pll_locked_o (),
+ .pll_selected_o (),
+ .current_profile_o(current_profile),
+ .busy_o (busy),
+ .fault_o (fault),
+ .sys_clk_o (sys_clk),
+ .mem_clk_o (mem_clk),
+ .pclk_o (pclk),
+ .sys_rst_n_o (sys_rst_n),
+ .mem_rst_n_o (mem_rst_n),
+ .pclk_rst_n_o (pclk_rst_n),
+ .xin_hz_o (xin_hz),
+ .ref_hz_o (ref_hz),
+ .sys_hz_o (sys_hz),
+ .mem_hz_o (mem_hz),
+ .pclk_hz_o (pclk_hz)
+ );
+
+ task automatic apply_profile(input logic [1:0] requested);
+ begin
+ @(negedge ref_clk_i);
+ profile = requested;
+ apply = 1'b1;
+ @(negedge ref_clk_i);
+ apply = 1'b0;
+ repeat (600) @(negedge ref_clk_i);
+ end
+ endtask
+
+ initial begin
+ repeat (5) @(negedge ref_clk_i);
+ rst_n_i = 1'b1;
+ repeat (32) @(negedge ref_clk_i);
+ if (current_profile != 0 || sys_hz != 24_000_000 || mem_hz != 24_000_000 ||
+ pclk_hz != 24_000_000 || !sys_rst_n || !mem_rst_n || !pclk_rst_n) begin
+ $fatal(1, "SAFE24 backend state mismatch");
+ end
+ apply_profile(2'd1);
+ if (current_profile != 1 || fault || sys_hz != 24_000_000) begin
+ $fatal(1, "external profile handling mismatch");
+ end
+ apply_profile(2'd2);
+ if (!fault || current_profile != 1) $fatal(1, "unsupported PLL was accepted");
+ $display("SIM_TEST_PASS Tiny clock/reset backend");
+ $finish;
+ end
+
+ initial begin
+ #100000;
+ $fatal(1, "SIM_TEST_TIMEOUT Tiny clock/reset backend");
+ end
+endmodule
diff --git a/tests/rtl/tiny_p7_source_reset_tb.sv b/tests/rtl/tiny_p7_source_reset_tb.sv
new file mode 100644
index 00000000..18969d98
--- /dev/null
+++ b/tests/rtl/tiny_p7_source_reset_tb.sv
@@ -0,0 +1,415 @@
+`timescale 1ns / 1ps
+`include "tiny_topology.svh"
+
+// Production DMA master, admission, CDC and lifecycle/reset components. The
+// include is extracted from the current top and its generated bindings by the
+// Python test. Unrelated CPU/APB/SDIO clients are idle; the AXI target is a
+// backpressured behavioral slave. No reset command bypasses the controller.
+module tiny_p7_source_reset_tb;
+ logic s_ref_clk = 0, s_exec_clk = 0, s_backend_pclk = 0, s_backend_mem_clk = 0;
+ realtime sys_half = 20.833333, pclk_half = 20.833333, mem_half = 20.833333;
+ always #20.833333 s_ref_clk = !s_ref_clk;
+ always #(sys_half) s_exec_clk = !s_exec_clk;
+ always #(pclk_half) s_backend_pclk = !s_backend_pclk;
+ always #(mem_half) s_backend_mem_clk = !s_backend_mem_clk;
+ logic rst_n_i = 0, s_backend_reset_n = 1;
+ logic s_por_rst_n, s_rst_n, s_cpu_ready_rst_n, s_fabric_rst_n, s_xpi_rst_n;
+ logic [`RETROSOC_TINY__LEAF_COUNT-1:0] s_leaf_clk, s_leaf_rst_n;
+ struct packed {logic reset_req_o;} u_wdg_if;
+ assign u_wdg_if.reset_req_o = 0;
+ logic s_dma_clk, s_dma_rst_n;
+ logic s_periph_apply = 0, s_periph_reset_command = 1;
+ logic [13:0] s_periph_request = 14'h200;
+ logic [13:0] s_periph_idle, s_periph_idle_ref;
+ logic [13:0] s_gate_request, s_local_reset, s_actual_gate, s_reset_held, s_reset_ready;
+ logic [13:0] s_gate_ref, s_held_ref, s_ready_ref, s_periph_reset_done;
+ logic s_bus_idle_ref, s_periph_block, s_periph_done, s_block_pclk;
+ logic [ 4:0] s_periph_fault;
+ logic [31:0] s_clock_timeout = 256;
+ logic s_clock_block = 0, s_clock_safe, s_dma_admission_idle, s_dma_idle_sys;
+ logic s_dma_cdc_ready, s_dma_cdc_busy, s_dma_ready_sys;
+ // Completion-mailbox and unrelated clients have no work in this fixture.
+ logic s_dma_done_pending_q = 0, s_dma_done_ready = 1;
+ logic s_cpu_clock_idle = 1, s_sdio_admission_idle = 1, s_xpi_cdc_busy = 0;
+ logic [ 2:0] s_owner_idle;
+ logic [`RETROSOC_TINY__TARGET_COUNT-1:0] s_target_idle;
+ logic [`RETROSOC_TINY__LEAF_COUNT-2:0] s_apb_cdc_idle = '1, s_apb_cdc_ready = '1;
+ assign s_actual_gate[13:10] = 0;
+ assign s_actual_gate[8:0] = 0;
+ assign s_reset_held[13:10] = 0;
+ assign s_reset_held[8:0] = 0;
+ assign s_reset_ready[13:10] = 0;
+ assign s_reset_ready[8:0] = 0;
+
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) u_dma_pclk_axi4_if (
+ .aclk (s_dma_clk),
+ .aresetn(s_dma_rst_n)
+ );
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) u_dma_admitted_axi4_if (
+ .aclk (s_dma_clk),
+ .aresetn(s_dma_rst_n)
+ );
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) u_masters_axi4_if[3] (
+ .aclk (s_exec_clk),
+ .aresetn(s_fabric_rst_n)
+ );
+
+ `include "source_reset_bindings.svh"
+
+ logic rd_start = 0, wr_start = 0, rd_start_ready, wr_start_ready;
+ logic rd_busy, wr_busy, rd_valid, rd_ready = 1, rd_last, rd_done, wr_done;
+ logic wr_valid = 0, wr_ready;
+ logic [31:0] rd_addr = 32'h30000000, wr_addr = 32'h30000100;
+ logic [4:0] rd_beats = 4, wr_beats = 4;
+ logic [31:0] rd_data, wr_data = 32'hcafef00d;
+ logic [1:0] rd_resp, wr_resp;
+ logic rd_id_error, wr_id_error;
+ dma_axi4_master u_dma_master (
+ .clk_i (s_dma_clk),
+ .rst_n_i (s_dma_rst_n),
+ .clear_i (1'b0),
+ .read_start_valid_i (rd_start),
+ .read_start_ready_o (rd_start_ready),
+ .read_addr_i (rd_addr),
+ .read_beats_i (rd_beats),
+ .read_fixed_i (1'b0),
+ .read_busy_o (rd_busy),
+ .read_beat_valid_o (rd_valid),
+ .read_beat_ready_i (rd_ready),
+ .read_data_o (rd_data),
+ .read_resp_o (rd_resp),
+ .read_last_o (rd_last),
+ .read_expected_last_o(),
+ .read_id_error_o (rd_id_error),
+ .read_done_o (rd_done),
+ .write_start_valid_i (wr_start),
+ .write_start_ready_o (wr_start_ready),
+ .write_addr_i (wr_addr),
+ .write_beats_i (wr_beats),
+ .write_fixed_i (1'b0),
+ .write_busy_o (wr_busy),
+ .write_data_valid_i (wr_valid),
+ .write_data_ready_o (wr_ready),
+ .write_data_i (wr_data),
+ .write_strb_i (4'hf),
+ .write_done_o (wr_done),
+ .write_resp_o (wr_resp),
+ .write_id_error_o (wr_id_error),
+ .axi4 (u_dma_pclk_axi4_if)
+ );
+ assign s_periph_idle = {4'hf, !(rd_busy || wr_busy || rd_start || wr_start), 9'h1ff};
+
+ logic hold_ar = 0, hold_aw = 0, hold_w = 0, hold_r = 0, hold_b = 0;
+ logic target_rd = 0, target_wr = 0;
+ logic [31:0] target_addr;
+ integer target_rleft, target_wleft;
+ wire target_idle = !target_rd && !target_wr;
+ assign s_owner_idle = {1'b1, target_idle, 1'b1};
+ assign s_target_idle = {`RETROSOC_TINY__TARGET_COUNT{target_idle}};
+ assign u_masters_axi4_if[1].arready = !target_rd && !hold_ar;
+ assign u_masters_axi4_if[1].awready = !target_wr && !hold_aw;
+ assign u_masters_axi4_if[1].wready = target_wr && target_wleft != 0 && !hold_w;
+ assign u_masters_axi4_if[1].rid = 0;
+ assign u_masters_axi4_if[1].rresp = 0;
+ assign u_masters_axi4_if[1].ruser = 0;
+ assign u_masters_axi4_if[1].bid = 0;
+ assign u_masters_axi4_if[1].bresp = 0;
+ assign u_masters_axi4_if[1].buser = 0;
+ logic ar_stalled = 0, aw_stalled = 0;
+ logic [39:0] held_ar, held_aw;
+ always @(posedge s_exec_clk or negedge s_fabric_rst_n) begin
+ if (!s_fabric_rst_n) begin
+ ar_stalled <= 0;
+ aw_stalled <= 0;
+ end else begin
+ if (ar_stalled && (!u_masters_axi4_if[1].arvalid ||
+ {u_masters_axi4_if[1].araddr, u_masters_axi4_if[1].arlen} !== held_ar))
+ $fatal(1, "SIM_TEST_FAIL stalled AR revoked or changed");
+ if (aw_stalled && (!u_masters_axi4_if[1].awvalid ||
+ {u_masters_axi4_if[1].awaddr, u_masters_axi4_if[1].awlen} !== held_aw))
+ $fatal(1, "SIM_TEST_FAIL stalled AW revoked or changed");
+ ar_stalled <= u_masters_axi4_if[1].arvalid && !u_masters_axi4_if[1].arready;
+ aw_stalled <= u_masters_axi4_if[1].awvalid && !u_masters_axi4_if[1].awready;
+ held_ar <= {u_masters_axi4_if[1].araddr, u_masters_axi4_if[1].arlen};
+ held_aw <= {u_masters_axi4_if[1].awaddr, u_masters_axi4_if[1].awlen};
+ end
+ end
+ always @(posedge s_exec_clk or negedge s_fabric_rst_n) begin
+ if (!s_fabric_rst_n) begin
+ target_rd <= 0;
+ target_wr <= 0;
+ target_rleft <= 0;
+ target_wleft <= 0;
+ target_addr <= 0;
+ u_masters_axi4_if[1].rvalid <= 0;
+ u_masters_axi4_if[1].rdata <= 0;
+ u_masters_axi4_if[1].rlast <= 0;
+ u_masters_axi4_if[1].bvalid <= 0;
+ end else begin
+ if (u_masters_axi4_if[1].arvalid && u_masters_axi4_if[1].arready) begin
+ target_rd <= 1;
+ target_addr <= u_masters_axi4_if[1].araddr;
+ target_rleft <= int'(u_masters_axi4_if[1].arlen) + 1;
+ end
+ if (target_rd && !hold_r && !u_masters_axi4_if[1].rvalid) begin
+ u_masters_axi4_if[1].rvalid <= 1;
+ u_masters_axi4_if[1].rdata <= target_addr ^ 32'h12345678;
+ u_masters_axi4_if[1].rlast <= target_rleft == 1;
+ end
+ if (u_masters_axi4_if[1].rvalid && u_masters_axi4_if[1].rready) begin
+ u_masters_axi4_if[1].rvalid <= 0;
+ target_rleft <= target_rleft - 1;
+ target_addr <= target_addr + 4;
+ if (u_masters_axi4_if[1].rlast) target_rd <= 0;
+ end
+ if (u_masters_axi4_if[1].awvalid && u_masters_axi4_if[1].awready) begin
+ target_wr <= 1;
+ target_wleft <= int'(u_masters_axi4_if[1].awlen) + 1;
+ end
+ if (u_masters_axi4_if[1].wvalid && u_masters_axi4_if[1].wready) begin
+ if (u_masters_axi4_if[1].wlast !== (target_wleft == 1))
+ $fatal(1, "SIM_TEST_FAIL write burst boundary");
+ target_wleft <= target_wleft - 1;
+ end
+ if (target_wr && target_wleft == 0 && !hold_b) u_masters_axi4_if[1].bvalid <= 1;
+ if (u_masters_axi4_if[1].bvalid && u_masters_axi4_if[1].bready) begin
+ u_masters_axi4_if[1].bvalid <= 0;
+ target_wr <= 0;
+ end
+ end
+ end
+
+ // Independent scoreboard survives local source reset. Looking only at the
+ // bridge's reset-cleared pending bits would conceal an unsafe reset.
+ integer reads = 0, read_completions = 0, writes = 0, write_completions = 0;
+ integer read_beat = 0, local_resets = 0;
+ always @(posedge s_dma_clk or negedge rst_n_i or negedge s_backend_reset_n) begin
+ if (!rst_n_i || !s_backend_reset_n) begin
+ reads = 0;
+ read_completions = 0;
+ writes = 0;
+ write_completions = 0;
+ read_beat = 0;
+ end else if (s_dma_rst_n) begin
+ if (u_dma_pclk_axi4_if.arvalid && u_dma_pclk_axi4_if.arready) reads++;
+ if (u_dma_pclk_axi4_if.awvalid && u_dma_pclk_axi4_if.awready) writes++;
+ if (rd_valid && rd_ready) begin
+ if (rd_resp != 0 || rd_id_error || rd_data !== ((rd_addr + 4 * read_beat) ^ 32'h12345678) ||
+ rd_last !== (read_beat == int'(rd_beats) - 1))
+ $fatal(1, "SIM_TEST_FAIL stale or malformed read response");
+ read_beat++;
+ if (rd_last) begin
+ read_completions++;
+ read_beat = 0;
+ end
+ end
+ if (wr_done) begin
+ if (wr_resp != 0 || wr_id_error) $fatal(1, "SIM_TEST_FAIL write response");
+ write_completions++;
+ end
+ if (read_completions > reads || write_completions > writes)
+ $fatal(1, "SIM_TEST_FAIL duplicate terminal response");
+ end
+ end
+ always @(posedge s_local_reset[9]) begin
+ if (reads != read_completions || writes != write_completions || !target_idle ||
+ rd_busy || wr_busy || !s_dma_admission_idle)
+ $fatal(1, "SIM_TEST_FAIL P7_SOURCE_RESET_UNDRAINED");
+ local_resets++;
+ end
+ // SYS-source reset of XPI is never an independent reset event. Allow delta
+ // propagation of the asynchronously asserted production reset tree.
+ always @(negedge s_fabric_rst_n) begin
+ #0.001;
+ if (s_xpi_rst_n !== 0 || s_dma_rst_n !== 0)
+ $fatal(1, "SIM_TEST_FAIL common system reset left an endpoint alive");
+ end
+
+ task automatic ready;
+ wait (s_dma_rst_n && s_dma_cdc_ready && s_dma_ready_sys && s_ready_ref[9]);
+ repeat (24) @(negedge s_ref_clk);
+ endtask
+ task automatic start_read;
+ @(negedge s_dma_clk);
+ rd_addr += 32'h100;
+ rd_start = 1;
+ do @(posedge s_dma_clk); while (!rd_start_ready);
+ @(negedge s_dma_clk);
+ rd_start = 0;
+ endtask
+ task automatic start_write;
+ @(negedge s_dma_clk);
+ wr_start = 1;
+ do @(posedge s_dma_clk); while (!wr_start_ready);
+ @(negedge s_dma_clk);
+ wr_start = 0;
+ endtask
+ task automatic send_words(input integer words);
+ @(negedge s_dma_clk);
+ wr_valid = 1;
+ repeat (words) begin
+ do @(posedge s_dma_clk); while (!wr_ready);
+ @(negedge s_dma_clk);
+ end
+ wr_valid = 0;
+ endtask
+ task automatic reset_command;
+ @(negedge s_ref_clk);
+ s_periph_apply = 1;
+ @(negedge s_ref_clk);
+ s_periph_apply = 0;
+ endtask
+ task automatic completion(input logic [4:0] expected_fault);
+ while (!s_periph_done) @(negedge s_ref_clk);
+ if (s_periph_fault !== expected_fault ||
+ (expected_fault == 0 && s_periph_reset_done !== 14'h200) ||
+ (expected_fault != 0 && s_local_reset != 0))
+ $fatal(1, "SIM_TEST_FAIL reset command result fault=%h", s_periph_fault);
+ @(negedge s_ref_clk);
+ if (expected_fault == 0) ready();
+ endtask
+ task automatic require_draining;
+ repeat (48) @(negedge s_ref_clk);
+ if (!s_periph_block || s_local_reset != 0 || !s_dma_rst_n || s_periph_done)
+ $fatal(1, "SIM_TEST_FAIL reset did not preserve outstanding work");
+ endtask
+ task automatic recovery_read;
+ start_read();
+ wait (!rd_busy);
+ repeat (24) @(negedge s_ref_clk);
+ if (reads != read_completions || writes != write_completions)
+ $fatal(1, "SIM_TEST_FAIL recovery completion accounting");
+ endtask
+
+ initial begin
+ if ($test$plusargs("xin48")) begin
+ sys_half = 10.416667;
+ mem_half = 10.416667;
+ pclk_half = 10.416667;
+ end
+ if ($test$plusargs("xin96")) begin
+ sys_half = 5.208333;
+ mem_half = 5.208333;
+ pclk_half = 10.416667;
+ end
+ if ($test$plusargs("pll192")) begin
+ sys_half = 2.604167;
+ mem_half = 5.208333;
+ pclk_half = 10.416667;
+ end
+ if ($test$plusargs("pll240")) begin
+ sys_half = 2.083333;
+ mem_half = 4.166667;
+ pclk_half = 8.333333;
+ end
+ // Deliberately falsified idle observations must trip the independent
+ // scoreboard. This is a negative test, never a qualifying DUT mode.
+ if ($test$plusargs("break_drain")) begin
+ force s_bus_idle_ref = 1'b1;
+ force s_periph_idle_ref = 14'h3fff;
+ end
+ #1 rst_n_i = 1;
+ #1 rst_n_i = 0;
+ repeat (8) @(negedge s_ref_clk);
+ rst_n_i = 1;
+ ready();
+ recovery_read();
+
+ // An accepted but downstream-stalled AR survives until normal completion.
+ hold_ar = 1;
+ start_read();
+ wait (u_masters_axi4_if[1].arvalid);
+ reset_command();
+ require_draining();
+ hold_ar = 0;
+ completion(0);
+ recovery_read();
+
+ // A late R prevents local reset, even after a bounded command timeout.
+ hold_r = 1;
+ start_read();
+ wait (target_rd);
+ reset_command();
+ require_draining();
+ completion(5'b00010);
+ if (!rd_busy) $fatal(1, "SIM_TEST_FAIL timeout discarded read");
+ hold_r = 0;
+ wait (!rd_busy);
+ reset_command();
+ completion(0);
+ recovery_read();
+
+ // AW backpressure, an incomplete W burst, and a delayed B all prohibit
+ // source reset. The real DMA master owns BREADY and the original WLAST.
+ hold_aw = 1;
+ hold_b = 1;
+ start_write();
+ wait (u_masters_axi4_if[1].awvalid);
+ reset_command();
+ require_draining();
+ hold_aw = 0;
+ send_words(1);
+ require_draining();
+ hold_w = 1;
+ send_words(3);
+ require_draining();
+ hold_w = 0;
+ wait (target_wleft == 0);
+ completion(5'b00010);
+ if (!wr_busy) $fatal(1, "SIM_TEST_FAIL timeout discarded write");
+ hold_b = 0;
+ wait (!wr_busy);
+ reset_command();
+ completion(0);
+ // The first post-reset write must also receive its own response exactly once.
+ start_write();
+ send_words(4);
+ wait (!wr_busy);
+ recovery_read();
+
+ // A response already in the source holder is still outstanding when the
+ // consumer applies backpressure, even if the target is otherwise idle.
+ rd_ready = 0;
+ start_read();
+ wait (rd_valid);
+ reset_command();
+ require_draining();
+ rd_ready = 1;
+ completion(0);
+ recovery_read();
+ if (local_resets != 4) $fatal(1, "SIM_TEST_FAIL reset scenario coverage");
+
+ // The other legal reset route cancels the complete system session.
+ hold_r = 1;
+ start_read();
+ wait (target_rd);
+ @(negedge s_ref_clk);
+ s_backend_reset_n = 0;
+ repeat (8) @(negedge s_ref_clk);
+ s_backend_reset_n = 1;
+ hold_r = 0;
+ ready();
+ recovery_read();
+ $display("SIM_TEST_PASS Tiny P7 production source reset local=4 timeout=2 system=1");
+ $finish;
+ end
+ initial begin
+ #1000000;
+ $fatal(1, "SIM_TEST_TIMEOUT Tiny P7 production source reset");
+ end
+endmodule
diff --git a/tests/rtl/tiny_peripheral_control_tb.sv b/tests/rtl/tiny_peripheral_control_tb.sv
new file mode 100644
index 00000000..99457750
--- /dev/null
+++ b/tests/rtl/tiny_peripheral_control_tb.sv
@@ -0,0 +1,122 @@
+`timescale 1ns / 1ps
+module tiny_peripheral_control_tb;
+ logic clk = 0, leaf_clk = 0, rst_n = 0, run_leaf = 1;
+ logic apply = 0, reset_cmd = 0, bus_idle = 1;
+ logic [13:0] request_mask = 0, idle_mask = '1;
+ logic [13:0] gates, resets, actual, held, ready, completed;
+ logic block_new, done;
+ logic [4:0] fault;
+ logic ip_clk, ip_rst_n;
+ integer hold_edges = 0, release_edges = 0;
+ always #5 clk = !clk;
+ always #7 if (run_leaf) leaf_clk = !leaf_clk;
+ tiny_peripheral_control u_control (
+ .clk_i (clk),
+ .rst_n_i (rst_n),
+ .warm_reset_i (1'b0),
+ .apply_i (apply),
+ .reset_command_i(reset_cmd),
+ .request_i (request_mask),
+ .idle_i (idle_mask),
+ .bus_idle_i (bus_idle),
+ .gate_actual_i (actual),
+ .reset_held_i (held),
+ .reset_ready_i (ready),
+ .timeout_i (32'd128),
+ .gate_o (gates),
+ .reset_o (resets),
+ .block_o (block_new),
+ .done_o (done),
+ .fault_o (fault),
+ .reset_done_o (completed)
+ );
+ tiny_peripheral_leaf u_leaf (
+ .clk_i (leaf_clk),
+ .rst_n_i(rst_n),
+ .gate_i (gates[2]),
+ .reset_i(resets[2]),
+ .clk_o (ip_clk),
+ .rst_n_o(ip_rst_n),
+ .gated_o(actual[2]),
+ .held_o (held[2])
+ );
+ assign actual[13:3] = 0;
+ assign actual[1:0] = 0;
+ assign held[13:3] = 0;
+ assign held[1:0] = 0;
+ assign ready = {11'd0, ip_rst_n, 2'd0};
+ always @(posedge ip_clk) begin
+ if (resets[2]) hold_edges++;
+ if (!resets[2] && !ip_rst_n && hold_edges != 0) release_edges++;
+ end
+ task automatic command(input logic resetting, input logic [13:0] mask);
+ @(negedge clk);
+ reset_cmd = resetting;
+ request_mask = mask;
+ apply = 1;
+ @(negedge clk);
+ apply = 0;
+ endtask
+ task automatic completion(input logic [4:0] expected_fault);
+ integer count;
+ count = 0;
+ while (!done && count < 200) begin
+ @(negedge clk);
+ count++;
+ end
+ if (!done || block_new || fault != expected_fault)
+ $fatal(1, "lifecycle completion fault=%h expected=%h", fault, expected_fault);
+ endtask
+ initial begin
+ #1 rst_n = 1;
+ #1 rst_n = 0;
+ repeat (8) @(negedge clk);
+ rst_n = 1;
+ repeat (20) @(negedge clk);
+ // CPU WFI permission must complete without pausing an unrelated DMA job.
+ idle_mask[9] = 0;
+ bus_idle = 0;
+ command(0, 14'h1);
+ while (!done) begin
+ @(negedge clk);
+ if (block_new) $fatal(1, "CPU-only gate arm blocked independent traffic");
+ end
+ completion(0);
+ if (gates != 1) $fatal(1, "CPU WFI arm waited for unrelated DMA");
+ command(0, 0);
+ completion(0);
+ idle_mask[9] = 1;
+ bus_idle = 1;
+ idle_mask[2] = 0;
+ command(0, 14'h4);
+ completion(5'b00010);
+ if (gates != 0 || actual != 0) $fatal(1, "busy timeout changed gates");
+ idle_mask[2] = 1;
+ command(0, 14'h4);
+ completion(0);
+ if (!actual[2]) $fatal(1, "gate completed before leaf stopped");
+ repeat (10) @(negedge clk);
+ if (ip_clk) $fatal(1, "gated clock high");
+ command(1, 14'h4);
+ completion(5'b00001);
+ if (resets != 0 || completed != 0) $fatal(1, "reset accepted for gated IP");
+ command(0, 0);
+ completion(0);
+ command(1, 14'h4);
+ completion(0);
+ if (completed != 14'h4 || hold_edges < 5 || release_edges < 5 || !ip_rst_n)
+ $fatal(1, "reset acknowledged before five hold/release edges");
+ @(negedge leaf_clk);
+ run_leaf = 0;
+ command(1, 14'h4);
+ completion(5'b10000);
+ if (completed != 0 || !resets[2] || ip_rst_n)
+ $fatal(1, "missing clock acknowledged reset or removed isolation");
+ $display("SIM_TEST_PASS Tiny peripheral lifecycle");
+ $finish;
+ end
+ initial begin
+ #100000;
+ $fatal(1, "SIM_TEST_TIMEOUT Tiny peripheral lifecycle");
+ end
+endmodule
diff --git a/tests/rtl/tiny_r2_p4_cpu.S b/tests/rtl/tiny_r2_p4_cpu.S
new file mode 100644
index 00000000..ce5ae705
--- /dev/null
+++ b/tests/rtl/tiny_r2_p4_cpu.S
@@ -0,0 +1,77 @@
+.section .text
+.global _start
+.option norvc
+_start:
+ li sp, 0x3001f000
+ la t0, trap
+ csrw mtvec, t0
+ li s0, 0
+ # A C.NOP, followed by a 32-bit ADDI crossing the group boundary, C.RET.
+ li s1, 0x30007ffc
+ li s2, 3
+1: li t0, 0x05130001
+ sw t0, 0(s1)
+ li t0, 0x80820090
+ sw t0, 4(s1)
+ fence.i
+ jalr ra, s1, 0
+ li t0, 9
+ bne a0, t0, fail
+ li t0, 0x8000
+ add s1, s1, t0
+ addi s2, s2, -1
+ bnez s2, 1b
+ # Instruction-side failure returns to RA via the trap handler.
+ li t0, 0x10001000
+ jalr ra, t0, 0
+ li t0, 1
+ bne s0, t0, fail
+ # A debugger will patch this code and release the SRAM flag while halted.
+ li t0, 0x30000080
+ li t1, 0x00100513
+ sw t1, 0(t0)
+ li t1, 0x00008067
+ sw t1, 4(t0)
+ li t1, 0x3001ffe0
+ sw zero, 0(t1)
+ fence.i
+ jalr ra, t0, 0
+ li t0, 1
+ bne a0, t0, fail
+ li t0, 0x1000b084
+ li t1, 1
+ sw t1, 0(t0)
+#ifdef TINY_TEST_P7
+ csrsi 0xbf0, 1
+ li t0, 128
+ csrs mie, t0
+ wfi
+5: csrr t1, mip
+ andi t1, t1, 128
+ bnez t1, 5b
+ wfi
+#endif
+ li t0, 0x3001ffe0
+2: lw t1, 0(t0)
+ beqz t1, 2b
+ fence.i
+ li t0, 0x30000080
+ jalr ra, t0, 0
+ li t0, 2
+ bne a0, t0, fail
+ li t0, 0x1000b084
+ li t1, 0x80000001
+ sw t1, 0(t0)
+3: j 3b
+trap:
+ csrr t2, mcause
+ li t3, 1
+ bne t2, t3, fail
+ addi s0, s0, 1
+ csrw mepc, ra
+ mret
+fail:
+ li t0, 0x1000b084
+ li t1, 0x80000000
+ sw t1, 0(t0)
+4: j 4b
diff --git a/tests/rtl/tiny_r2_p4_cpu.ld b/tests/rtl/tiny_r2_p4_cpu.ld
new file mode 100644
index 00000000..51fe0f35
--- /dev/null
+++ b/tests/rtl/tiny_r2_p4_cpu.ld
@@ -0,0 +1,6 @@
+ENTRY(_start)
+SECTIONS {
+ . = 0;
+ .text : { *(.text*) }
+ /DISCARD/ : { *(.riscv.attributes) *(.comment) }
+}
diff --git a/tests/rtl/tiny_r2_p4_cpu_tb.sv b/tests/rtl/tiny_r2_p4_cpu_tb.sv
new file mode 100644
index 00000000..54b00783
--- /dev/null
+++ b/tests/rtl/tiny_r2_p4_cpu_tb.sv
@@ -0,0 +1,347 @@
+`timescale 1ns / 1ps
+module tiny_r2_p4_cpu_tb;
+ logic clk_i = 0, rst_n_i = 0;
+`ifdef TINY_TEST_P7
+ localparam bit P7Lifecycle = 1;
+`else
+ localparam bit P7Lifecycle = 0;
+`endif
+ logic cold_rst_n = 0, clock_pause = 0, clock_idle, gated;
+ logic [31:0] irq = 0;
+ always #5 clk_i = ~clk_i;
+ wire s_rst_n = rst_n_i;
+ logic s_jtag_tck_driver = 0, s_jtag_tms_driver = 1, s_jtag_tdi_driver = 0;
+ wire s_jtag_tdo;
+ logic core_rst_n, quiesce, idle, halted;
+ ahbl_if cpu[2] (
+ .hclk (clk_i),
+ .hresetn(core_rst_n)
+ );
+ tiny_sram_port_if local_ports[2] ();
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) slow (
+ .aclk (clk_i),
+ .aresetn(rst_n_i)
+ );
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) ext[4] (
+ .aclk (clk_i),
+ .aresetn(rst_n_i)
+ );
+ apb4_if cfg (
+ .pclk (clk_i),
+ .presetn(rst_n_i)
+ );
+ logic [7:0] rom [0:4095];
+ string firmware;
+ logic read_pending = 0, aw_pending = 0, write_pending = 0, hold_read = 0;
+ logic [31:0] read_data = 0, write_addr = 0;
+ logic [ 1:0] read_resp = 0;
+ integer starts = 0;
+ logic completed = 0;
+ logic [31:0] status;
+ bit ignored;
+ `include "tiny_jtag_smoke.svh"
+
+tiny_cpu_wrapper #(
+ .EnableAtomics (0),
+ .EnableWfiGate (P7Lifecycle),
+ .PreserveDebug (P7Lifecycle),
+ .ResetSyncStages(5)
+ ) u_cpu (
+ .clk_i (clk_i),
+ .rst_n_i (rst_n_i),
+ .debug_rst_n_i (cold_rst_n),
+ .wfi_gate_i (P7Lifecycle),
+ .gated_o (gated),
+ .hart_reset_o (),
+ .irq_i (irq),
+ .jtag_tck_i (s_jtag_tck_driver),
+ .jtag_tms_i (s_jtag_tms_driver),
+ .jtag_tdi_i (s_jtag_tdi_driver),
+ .jtag_trst_n_i (cold_rst_n),
+ .jtag_tdo_o (s_jtag_tdo),
+ .debug_halted_o(halted),
+ .bus_idle_i (idle),
+ .core_rst_n_o (core_rst_n),
+ .quiesce_o (quiesce),
+ .cpu (cpu)
+ );
+ tiny_cpu_mem #(
+ .ClockPause(P7Lifecycle)
+ ) u_cpu_mem (
+ .clk_i (clk_i),
+ .rst_n_i (core_rst_n),
+ .quiesce_i (quiesce),
+ .clock_pause_i (clock_pause),
+ .clock_idle_o (clock_idle),
+ .idle_o (idle),
+ .local_fault_valid_o(), // P5 fault aggregation has its own fixture.
+ .local_fault_addr_o (),
+ .local_fault_write_o(),
+ .cpu (cpu),
+ .local_ports (local_ports),
+ .slow_axi4 (slow)
+ );
+ tiny_sram u_sram (
+ .clk_i (clk_i),
+ .rst_n_i (rst_n_i),
+ .perf_enable_i (1'b0),
+ .perf_clear_i (1'b0),
+ .local_ports (local_ports),
+ .external_ports(ext),
+ .cfg_apb4 (cfg)
+ );
+ // Verification-only slow memory, not a product Boot ROM. It also supplies
+ // the terminal mailbox and instruction fault used by this CPU fixture.
+ assign slow.arready = !read_pending;
+ assign slow.rvalid = read_pending && !hold_read;
+ assign slow.rdata = read_data;
+ assign slow.rresp = read_resp;
+ assign slow.rid = 0;
+ assign slow.rlast = 1;
+ assign slow.ruser = 0;
+ assign slow.awready = !aw_pending && !write_pending;
+ assign slow.wready = aw_pending;
+ assign slow.bvalid = write_pending;
+ assign slow.bresp = 0;
+ assign slow.bid = 0;
+ assign slow.buser = 0;
+ always @(posedge clk_i) begin
+ if (!rst_n_i) begin
+ read_pending <= 0;
+ aw_pending <= 0;
+ write_pending <= 0;
+ end else begin
+ if (slow.rvalid && slow.rready) read_pending <= 0;
+ if (slow.arvalid && slow.arready) begin
+ read_pending <= 1;
+ if (slow.araddr < 4096) begin
+ read_resp <= 0;
+ read_data <= {
+ rom[slow.araddr+3], rom[slow.araddr+2], rom[slow.araddr+1], rom[slow.araddr]
+ };
+ end else begin
+ read_resp <= 3;
+ read_data <= 0;
+ if (!slow.arprot[2]) $fatal(1, "unexpected slow data read %h", slow.araddr);
+ end
+ end
+ if (slow.awvalid && slow.awready) begin
+ aw_pending <= 1;
+ write_addr <= slow.awaddr;
+ end
+ if (slow.wvalid && slow.wready) begin
+ aw_pending <= 0;
+ write_pending <= 1;
+ if (write_addr != 32'h1000b084) $fatal(1, "unexpected slow write %h", write_addr);
+ if (slow.wdata == 1) starts <= starts + 1;
+ else if (slow.wdata == 32'h80000001) completed <= 1;
+ else $fatal(1, "SIM_TEST_FAIL CPU fixture code=%h", slow.wdata);
+ end
+ if (slow.bvalid && slow.bready) write_pending <= 0;
+ end
+ end
+ for (genvar g = 0; g < 4; g++) begin : gen_init
+ initial begin
+ ext[g].awid = 0;
+ ext[g].awaddr = 0;
+ ext[g].awlen = 0;
+ ext[g].awsize = 2;
+ ext[g].awburst = 1;
+ ext[g].awlock = 0;
+ ext[g].awcache = 0;
+ ext[g].awprot = 0;
+ ext[g].awqos = 0;
+ ext[g].awregion = 0;
+ ext[g].awuser = 0;
+ ext[g].awvalid = 0;
+ ext[g].wdata = 0;
+ ext[g].wstrb = 15;
+ ext[g].wlast = 1;
+ ext[g].wuser = 0;
+ ext[g].wvalid = 0;
+ ext[g].bready = 0;
+ ext[g].arid = 0;
+ ext[g].araddr = 0;
+ ext[g].arlen = 0;
+ ext[g].arsize = 2;
+ ext[g].arburst = 1;
+ ext[g].arlock = 0;
+ ext[g].arcache = 0;
+ ext[g].arprot = 0;
+ ext[g].arqos = 0;
+ ext[g].arregion = 0;
+ ext[g].aruser = 0;
+ ext[g].arvalid = 0;
+ ext[g].rready = 0;
+ end
+ end
+ task automatic abstract_wait;
+ integer polls;
+ logic [31:0] value;
+ polls = 0;
+ do begin
+ jtag_dmi(0, 7'h16, 0, value);
+ polls++;
+ end while (value[12] && polls < 128);
+ if (value[12] || value[10:8] != 0) $fatal(1, "abstract command status %h", value);
+ endtask
+ task automatic set_gpr(input logic [4:0] regnum, input logic [31:0] value);
+ logic [31:0] unused;
+ jtag_dmi(1, 7'h04, value, unused);
+ jtag_dmi(1, 7'h17, 32'h00231000 | {27'd0, regnum}, unused);
+ abstract_wait();
+ endtask
+ task automatic execute_debug(input logic [31:0] instruction);
+ logic [31:0] unused;
+ jtag_dmi(1, 7'h20, instruction, unused);
+ jtag_dmi(1, 7'h21, 32'h00100073, unused);
+ jtag_dmi(1, 7'h17, 32'h00240000, unused);
+ abstract_wait();
+ endtask
+ task automatic debug_store(input logic [31:0] address, input logic [31:0] value);
+ set_gpr(28, address);
+ set_gpr(29, value);
+ execute_debug(32'h01de2023); // sw t4,0(t3)
+ endtask
+ initial begin
+ #10000000;
+ $display(
+ "CPU_DIAG starts=%0d completed=%b core_reset=%b halted=%b pending=%b idle=%b quiesce=%b hold=%b read=%b",
+ starts, completed, core_rst_n, halted, u_cpu.s_reset_pending, idle, quiesce, hold_read,
+ read_pending);
+ $display("CPU_DIAG I addr=%h trans=%b ready=%b D addr=%h trans=%b ready=%b slowstate=%0d",
+ cpu[0].haddr, cpu[0].htrans, cpu[0].hready, cpu[1].haddr, cpu[1].htrans,
+ cpu[1].hready, u_cpu_mem.s_slow_q.state);
+ $fatal(1, "SIM_TEST_TIMEOUT Tiny P4 CPU");
+ end
+ initial begin
+ if (!$value$plusargs("firmware=%s", firmware)) $fatal(1, "missing CPU fixture image");
+ $readmemh(firmware, rom);
+ cfg.paddr = 0;
+ cfg.pprot = 0;
+ cfg.psel = 0;
+ cfg.penable = 0;
+ cfg.pwrite = 0;
+ cfg.pwdata = 0;
+ cfg.pstrb = 15;
+ repeat (6) jtag_tick(1, 0, ignored);
+ @(negedge clk_i);
+ rst_n_i = 1;
+ cold_rst_n = 1;
+ repeat (12) jtag_tick(1, 0, ignored);
+ jtag_idle(1);
+ jtag_ir(5'h11);
+ wait (starts == 1);
+ $display("P4_CPU_STAGE boot-and-boundaries");
+ if (P7Lifecycle) begin
+ wait (gated);
+ repeat (10) @(negedge clk_i);
+ irq = 2; // Tiny wrapper timer IRQ is input bit 1.
+ wait (!gated);
+ repeat (10) @(negedge clk_i);
+ irq = 0;
+ wait (gated);
+ end
+ jtag_dmi(1, 7'h10, 1, status);
+ jtag_dmi(1, 7'h10, 32'h80000001, status);
+ wait (halted);
+ $display("P4_CPU_STAGE halted");
+ debug_store(32'h30000080, 32'h00200513);
+ debug_store(32'h3001ffe0, 1);
+ execute_debug(32'h0000100f); // FENCE.I before debugger resume
+ jtag_dmi(1, 7'h10, 32'h40000001, status);
+ jtag_dmi(1, 7'h10, 1, status);
+ wait (completed);
+ $display("P4_CPU_STAGE patched-code-executed");
+ // A different external owner retains a B response across hart reset.
+ @(negedge clk_i);
+ ext[2].awaddr = 32'h30010100;
+ ext[2].awvalid = 1;
+ do @(posedge clk_i); while (!ext[2].awready);
+ @(negedge clk_i);
+ ext[2].awvalid = 0;
+ ext[2].wdata = 32'hdecafbad;
+ ext[2].wvalid = 1;
+ do @(posedge clk_i); while (!ext[2].wready);
+ @(negedge clk_i);
+ ext[2].wvalid = 0;
+ wait (ext[2].bvalid);
+ @(negedge clk_i);
+ hold_read = 1;
+ wait (read_pending);
+ jtag_dmi(1, 7'h10, 3, status);
+ repeat (32) begin
+ @(posedge clk_i);
+ if (!core_rst_n || !ext[2].bvalid) $fatal(1, "hart reset discarded accepted work");
+ end
+ @(negedge clk_i);
+ hold_read = 0;
+ wait (!core_rst_n);
+ $display("P4_CPU_STAGE hart-reset-drained");
+ if (!ext[2].bvalid) $fatal(1, "hart reset reset external SRAM frontend");
+ jtag_dmi(1, 7'h10, 1, status);
+ wait (core_rst_n);
+ wait (starts == 2);
+ @(negedge clk_i);
+ ext[2].bready = 1;
+ @(posedge clk_i);
+ @(negedge clk_i);
+ ext[2].bready = 0;
+ ext[2].araddr = 32'h30010100;
+ ext[2].arvalid = 1;
+ do @(posedge clk_i); while (!ext[2].arready);
+ @(negedge clk_i);
+ ext[2].arvalid = 0;
+ ext[2].rready = 1;
+ do @(posedge clk_i); while (!ext[2].rvalid);
+ if (ext[2].rdata !== 32'hdecafbad || ext[2].rresp != 0)
+ $fatal(1, "SRAM payload lost on hart reset");
+ if (P7Lifecycle) begin
+ @(negedge clk_i);
+ ext[2].rready = 0;
+ // A clock pause is different from a hart reset: unaccepted requests
+ // may remain parked, and resume without being dropped or replayed.
+ clock_pause = 1;
+ wait (clock_idle);
+ repeat (20) begin
+ @(negedge clk_i);
+ if (!clock_idle || local_ports[0].req_valid || local_ports[1].req_valid)
+ $fatal(1, "CPU local admission continued while clock-paused");
+ end
+ clock_pause = 0;
+ jtag_dmi(1, 7'h10, 32'h80000001, status);
+ wait (halted);
+ set_gpr(28, 0);
+ jtag_dmi(1, 7'h04, 32'h5a1234a5, status);
+ jtag_dmi(1, 7'h20, 32'h000e2e83, status); // lw t4,0(t3)
+ jtag_dmi(1, 7'h21, 32'h00100073, status);
+ hold_read = 1;
+ jtag_dmi(1, 7'h17, 32'h00240000, status);
+ wait (read_pending);
+ @(negedge clk_i);
+ rst_n_i = 0;
+ repeat (10) @(negedge clk_i);
+ hold_read = 0;
+ rst_n_i = 1;
+ repeat (20) @(negedge clk_i);
+ jtag_dmi(0, 7'h04, 0, status);
+ if (status != 32'h5a1234a5) $fatal(1, "warm reset lost retained debug data");
+ jtag_dmi(0, 7'h16, 0, status);
+ if (status[12] || status[10:8] == 0)
+ $fatal(1, "warm reset did not terminate pending debug command with error");
+ $display("P7_CPU_LIFECYCLE_PASS irq-wake debug-wake clock-pause warm-debug-abort");
+ end
+ $display("SIM_TEST_PASS Tiny P4 CPU compressed-boundaries fence-i debug-patch hart-drain");
+ $finish;
+ end
+endmodule
diff --git a/tests/rtl/tiny_r2_p4_formal.sv b/tests/rtl/tiny_r2_p4_formal.sv
new file mode 100644
index 00000000..515907c1
--- /dev/null
+++ b/tests/rtl/tiny_r2_p4_formal.sv
@@ -0,0 +1,58 @@
+module tiny_r2_p4_formal (
+ input logic clk_i,
+ input logic rst_n_i
+);
+ logic s_started_q = 1'b0;
+ (* anyseq *) logic [2:0] s_valid, s_ready, s_write;
+ tiny_sram_port_if u_ports[3] ();
+ always_ff @(posedge clk_i) begin
+ s_started_q <= 1'b1;
+ assume (rst_n_i == s_started_q);
+ end
+ for (genvar client = 0; client < 3; client++) begin : gen_client
+ logic [2:0] s_outstanding_q = '0;
+ wire s_accept = u_ports[client].req_valid && u_ports[client].req_ready;
+ wire s_complete = u_ports[client].rsp_valid && u_ports[client].rsp_ready;
+ always_ff @(posedge clk_i) begin
+ if (!rst_n_i) s_outstanding_q <= '0;
+ else begin
+ case ({
+ s_accept, s_complete
+ })
+ 2'b10: s_outstanding_q <= s_outstanding_q + 3'd1;
+ 2'b01: s_outstanding_q <= s_outstanding_q - 3'd1;
+ default: begin
+ end
+ endcase
+ assert (s_outstanding_q <= ((client == 2) ? 3'd2 : 3'd1));
+ assert (!u_ports[client].rsp_valid || (s_outstanding_q != 3'd0));
+ end
+ end
+ assign u_ports[client].req_valid = s_valid[client];
+ assign u_ports[client].rsp_ready = s_ready[client];
+ assign u_ports[client].write = s_write[client];
+ assign u_ports[client].addr = 32'h30000000 + 32'(client) * 32'h1000;
+ assign u_ports[client].wdata = 32'(client);
+ assign u_ports[client].wstrb = 4'hf;
+ end
+ tiny_sram_group u_dut (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .ports (u_ports)
+ );
+endmodule
+
+// Abstract only the macro payload: arbitration, storage reservation, response
+// capture and all control logic are the production group implementation.
+module tc_sram_1024x32 (
+ input logic clk_i,
+ cs_i,
+ input logic [ 9:0] addr_i,
+ input logic [31:0] data_i,
+ input logic [ 3:0] mask_i,
+ input logic wren_i,
+ output logic [31:0] data_o
+);
+ (* anyseq *) logic [31:0] s_value;
+ assign data_o = s_value;
+endmodule
diff --git a/tests/rtl/tiny_r2_p4_memory_tb.sv b/tests/rtl/tiny_r2_p4_memory_tb.sv
new file mode 100644
index 00000000..5c5c44ed
--- /dev/null
+++ b/tests/rtl/tiny_r2_p4_memory_tb.sv
@@ -0,0 +1,675 @@
+`timescale 1ns / 1ps
+module tiny_r2_p4_memory_tb;
+ logic clk_i = 0, rst_n_i = 0, quiesce = 0, idle;
+ logic perf_enable = 1, perf_clear = 0;
+ always #5 clk_i = ~clk_i;
+ ahbl_if cpu[2] (
+ .hclk (clk_i),
+ .hresetn(rst_n_i)
+ );
+ tiny_sram_port_if local_ports[2] ();
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) ext[4] (
+ .aclk (clk_i),
+ .aresetn(rst_n_i)
+ );
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) slow (
+ .aclk (clk_i),
+ .aresetn(rst_n_i)
+ );
+ apb4_if cfg (
+ .pclk (clk_i),
+ .presetn(rst_n_i)
+ );
+ logic [31:0] addr[2], data[2], rdata[2];
+ logic [2:0] size [2];
+ logic [1:0] trans[2];
+ logic [1:0] wr, ready, resp;
+ logic [31:0] awaddr[4], wdata[4], araddr[4], erdata[4];
+ logic [3:0] wstrb[4];
+ logic [7:0] awlen[4], arlen[4];
+ logic [2:0] awsize[4], arsize[4];
+ logic [1:0] awburst[4], arburst[4], bresp[4], rresp[4];
+ logic [3:0] awvalid, wvalid, wlast, bready, arvalid, rready;
+ logic [3:0] awready, wready, bvalid, arready, rvalid, rlast;
+ integer parallel_issues = 0;
+ integer local_latency_max = 0;
+ logic [31:0] observed;
+ logic [31:0] seed = 32'h12345678;
+ logic [1:0] local_fault_valid, local_fault_write;
+ logic [1:0][31:0] local_fault_addr;
+ integer local_fault_count[2] = '{0, 0};
+ always @(posedge clk_i) begin
+ for (integer p = 0; p < 2; p++) if (rst_n_i && local_fault_valid[p]) local_fault_count[p]++;
+ end
+
+ tiny_cpu_mem u_cpu_mem (
+ .clk_i (clk_i),
+ .rst_n_i (rst_n_i),
+ .quiesce_i (quiesce),
+ .idle_o (idle),
+ .local_fault_valid_o(local_fault_valid),
+ .local_fault_addr_o (local_fault_addr),
+ .local_fault_write_o(local_fault_write),
+ .cpu (cpu),
+ .local_ports (local_ports),
+ .slow_axi4 (slow)
+ );
+ tiny_sram u_sram (
+ .clk_i (clk_i),
+ .rst_n_i (rst_n_i),
+ .perf_enable_i (perf_enable),
+ .perf_clear_i (perf_clear),
+ .local_ports (local_ports),
+ .external_ports(ext),
+ .cfg_apb4 (cfg)
+ );
+ axi4_error_slave #(
+ .Response(2'b11)
+ ) u_slow_error (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .axi4 (slow)
+ );
+ for (genvar p = 0; p < 2; p++) begin : gen_cpu
+ assign cpu[p].haddr = addr[p];
+ assign cpu[p].hwdata = data[p];
+ assign cpu[p].hsize = size[p];
+ assign cpu[p].htrans = trans[p];
+ assign cpu[p].hwrite = wr[p];
+ assign cpu[p].hburst = 0;
+ assign cpu[p].hprot = 4'(p);
+ assign cpu[p].hmastlock = 0;
+ assign ready[p] = cpu[p].hready;
+ assign resp[p] = cpu[p].hresp;
+ assign rdata[p] = cpu[p].hrdata;
+ end
+ for (genvar g = 0; g < 4; g++) begin : gen_ext
+ assign ext[g].awaddr = awaddr[g];
+ assign ext[g].awlen = awlen[g];
+ assign ext[g].awsize = awsize[g];
+ assign ext[g].awburst = awburst[g];
+ assign ext[g].awvalid = awvalid[g];
+ assign ext[g].wdata = wdata[g];
+ assign ext[g].wstrb = wstrb[g];
+ assign ext[g].wlast = wlast[g];
+ assign ext[g].wvalid = wvalid[g];
+ assign ext[g].bready = bready[g];
+ assign ext[g].araddr = araddr[g];
+ assign ext[g].arlen = arlen[g];
+ assign ext[g].arsize = arsize[g];
+ assign ext[g].arburst = arburst[g];
+ assign ext[g].arvalid = arvalid[g];
+ assign ext[g].rready = rready[g];
+ assign ext[g].awid = '0;
+ assign ext[g].awlock = '0;
+ assign ext[g].awcache = '0;
+ assign ext[g].awprot = '0;
+ assign ext[g].awqos = '0;
+ assign ext[g].awregion = '0;
+ assign ext[g].awuser = '0;
+ assign ext[g].wuser = '0;
+ assign ext[g].arid = '0;
+ assign ext[g].arlock = '0;
+ assign ext[g].arcache = '0;
+ assign ext[g].arprot = '0;
+ assign ext[g].arqos = '0;
+ assign ext[g].arregion = '0;
+ assign ext[g].aruser = '0;
+ assign awready[g] = ext[g].awready;
+ assign wready[g] = ext[g].wready;
+ assign bvalid[g] = ext[g].bvalid;
+ assign bresp[g] = ext[g].bresp;
+ assign arready[g] = ext[g].arready;
+ assign rvalid[g] = ext[g].rvalid;
+ assign rresp[g] = ext[g].rresp;
+ assign rlast[g] = ext[g].rlast;
+ assign erdata[g] = ext[g].rdata;
+ end
+ always @(posedge clk_i) begin
+ if ((int'(u_sram.gen_group[0].u_group.s_issue) +
+ int'(u_sram.gen_group[1].u_group.s_issue) +
+ int'(u_sram.gen_group[2].u_group.s_issue) +
+ int'(u_sram.gen_group[3].u_group.s_issue)) > 1)
+ parallel_issues++;
+ end
+ task automatic ahb(input integer p, input bit write_access, input logic [31:0] address,
+ input logic [2:0] width, input logic [31:0] value, input bit error_expected,
+ input integer max_latency, output logic [31:0] result);
+ time accepted;
+ integer error_cycles;
+ @(negedge clk_i);
+ addr[p] = address;
+ size[p] = width;
+ wr[p] = write_access;
+ trans[p] = 2;
+ do @(posedge clk_i); while (!ready[p]);
+ accepted = $time;
+ @(negedge clk_i);
+ trans[p] = 0;
+ data[p] = value;
+ error_cycles = 0;
+ do begin
+ @(posedge clk_i);
+ if (resp[p]) error_cycles++;
+ end while (!ready[p]);
+ result = rdata[p];
+ if (resp[p] != error_expected || (error_expected && error_cycles != 2))
+ $fatal(1, "AHB error protocol p=%0d addr=%h errors=%0d", p, address, error_cycles);
+ if (max_latency != 0 && (($time - accepted) / 10) > max_latency)
+ $fatal(1, "local latency p=%0d addr=%h cycles=%0d", p, address, ($time - accepted) / 10);
+ if (max_latency != 0 && (($time - accepted) / 10) > local_latency_max)
+ local_latency_max = integer'(($time - accepted) / 10);
+ @(negedge clk_i);
+ endtask
+ task automatic store(input logic [31:0] address, input logic [31:0] value,
+ input logic [2:0] width);
+ logic [31:0] unused;
+ ahb(1, 1, address, width, value, 0, 3, unused);
+ endtask
+ task automatic load(input integer p, input logic [31:0] address, input logic [31:0] value,
+ input integer max_latency);
+ logic [31:0] got;
+ ahb(p, 0, address, 2, 0, 0, max_latency, got);
+ if (got !== value)
+ $fatal(1, "local data p=%0d addr=%h got=%h expected=%h", p, address, got, value);
+ endtask
+ task automatic read_external(input integer g, input logic [31:0] address,
+ input logic [31:0] value, input integer hold_cycles,
+ input logic [1:0] expected_resp);
+ @(negedge clk_i);
+ araddr[g] = address;
+ arvalid[g] = 1;
+ do @(posedge clk_i); while (!arready[g]);
+ @(negedge clk_i);
+ arvalid[g] = 0;
+ while (!rvalid[g]) @(negedge clk_i);
+ repeat (hold_cycles) begin
+ @(posedge clk_i);
+ if (!rvalid[g] || erdata[g] !== value || rresp[g] != expected_resp || !rlast[g])
+ $fatal(1, "external response changed under backpressure");
+ @(negedge clk_i);
+ end
+ if (erdata[g] !== value || rresp[g] != expected_resp || !rlast[g])
+ $fatal(1, "external read g=%0d got=%h expected=%h resp=%b", g, erdata[g], value, rresp[g]);
+ rready[g] = 1;
+ @(posedge clk_i);
+ @(negedge clk_i);
+ rready[g] = 0;
+ endtask
+ task automatic write_external(input integer g, input logic [31:0] address,
+ input logic [31:0] value, input logic [3:0] mask,
+ input integer delay_data);
+ @(negedge clk_i);
+ awaddr[g] = address;
+ awvalid[g] = 1;
+ do @(posedge clk_i); while (!awready[g]);
+ @(negedge clk_i);
+ awvalid[g] = 0;
+ repeat (delay_data) @(negedge clk_i);
+ wdata[g] = value;
+ wstrb[g] = mask;
+ wvalid[g] = 1;
+ wlast[g] = 1;
+ do @(posedge clk_i); while (!wready[g]);
+ @(negedge clk_i);
+ wvalid[g] = 0;
+ bready[g] = 1;
+ do @(posedge clk_i); while (!bvalid[g]);
+ if (bresp[g] != 0) $fatal(1, "external write");
+ @(negedge clk_i);
+ bready[g] = 0;
+ endtask
+ task automatic config_read(input logic [11:0] offset, input logic [31:0] expected);
+ @(negedge clk_i);
+ cfg.paddr = {20'h10017, offset};
+ cfg.psel = 1;
+ cfg.penable = 0;
+ @(negedge clk_i);
+ cfg.penable = 1;
+ do @(posedge clk_i); while (!cfg.pready);
+ if (cfg.pslverr || cfg.prdata !== expected)
+ $fatal(1, "SRAM ABI/perf offset=%h got=%h expected=%h", offset, cfg.prdata, expected);
+ @(negedge clk_i);
+ cfg.psel = 0;
+ cfg.penable = 0;
+ endtask
+ task automatic burst_and_errors;
+ // Sixteen captured write beats, with W asserted before AW.
+ @(negedge clk_i);
+ wdata[0] = 32'hcafe0000;
+ wstrb[0] = 15;
+ wvalid[0] = 1;
+ wlast[0] = 0;
+ repeat (3) begin
+ @(posedge clk_i);
+ if (wready[0]) $fatal(1, "W accepted without AW ownership");
+ end
+ @(negedge clk_i);
+ awaddr[0] = 32'h30000fc0;
+ awlen[0] = 15;
+ awvalid[0] = 1;
+ do @(posedge clk_i); while (!awready[0]);
+ @(negedge clk_i);
+ awvalid[0] = 0;
+ for (integer beat = 0; beat < 16; beat++) begin
+ wdata[0] = 32'hcafe0000 + beat;
+ wlast[0] = (beat == 15);
+ do @(posedge clk_i); while (!wready[0]);
+ @(negedge clk_i);
+ end
+ wvalid[0] = 0;
+ wait (bvalid[0]);
+ load(0, 32'h30000ffc, 32'hcafe000f, 3); // B held while local read completes.
+ if (!bvalid[0] || bresp[0] != 0) $fatal(1, "B not retained");
+ @(negedge clk_i);
+ bready[0] = 1;
+ @(posedge clk_i);
+ @(negedge clk_i);
+ bready[0] = 0;
+ awlen[0] = 0;
+ araddr[0] = 32'h30000fc0;
+ arlen[0] = 15;
+ arvalid[0] = 1;
+ do @(posedge clk_i); while (!arready[0]);
+ @(negedge clk_i);
+ arvalid[0] = 0;
+ for (integer beat = 0; beat < 16; beat++) begin
+ wait (rvalid[0]);
+ repeat (beat % 4) @(negedge clk_i);
+ if (erdata[0] !== (32'hcafe0000 + beat) || rresp[0] != 0 || rlast[0] != (beat == 15))
+ $fatal(1, "burst read ordering/last at beat %0d", beat);
+ rready[0] = 1;
+ @(posedge clk_i);
+ @(negedge clk_i);
+ rready[0] = 0;
+ end
+ arlen[0] = 0;
+ // A malformed write cannot commit bytes. Test strobes and early/missing LAST.
+ for (integer bad = 0; bad < 3; bad++) begin
+ @(negedge clk_i);
+ awaddr[0] = 32'h30000fc0;
+ awvalid[0] = 1;
+ awsize[0] = (bad == 0) ? 1 : 2;
+ awlen[0] = (bad == 1) ? 3 : 0;
+ do @(posedge clk_i); while (!awready[0]);
+ @(negedge clk_i);
+ awvalid[0] = 0;
+ wvalid[0] = 1;
+ wdata[0] = 0;
+ wstrb[0] = (bad == 0) ? 4'h8 : 4'hf;
+ wlast[0] = (bad != 2);
+ do @(posedge clk_i); while (!wready[0]);
+ @(negedge clk_i);
+ wvalid[0] = 0;
+ bready[0] = 1;
+ do @(posedge clk_i); while (!bvalid[0]);
+ if (bresp[0] != 2) $fatal(1, "malformed write accepted");
+ @(negedge clk_i);
+ bready[0] = 0;
+ load(0, 32'h30000fc0, 32'hcafe0000, 3);
+ end
+ awsize[0] = 2;
+ awlen[0] = 0;
+ wlast[0] = 1;
+ wstrb[0] = 15;
+ endtask
+ task automatic back_to_back;
+ // The next address is already present when the preceding write completes.
+ @(negedge clk_i);
+ addr[1] = 32'h30000100;
+ size[1] = 2;
+ wr[1] = 1;
+ trans[1] = 2;
+ do @(posedge clk_i); while (!ready[1]);
+ for (integer word_index = 0; word_index < 8; word_index++) begin
+ @(negedge clk_i);
+ data[1] = 32'hf00d0000 + word_index;
+ addr[1] = 32'h30000104 + word_index * 4;
+ trans[1] = (word_index == 7) ? 0 : 2;
+ do @(posedge clk_i); while (!ready[1]);
+ if (resp[1]) $fatal(1, "pipelined write error");
+ end
+ @(negedge clk_i);
+ for (integer word_index = 0; word_index < 8; word_index++)
+ load(0, 32'h30000100 + word_index * 4, 32'hf00d0000 + word_index, 3);
+ endtask
+ task automatic streaming_burst;
+ time started, first_beat, previous_beat, final_write;
+ integer read_gap_max, write_gap_max;
+ read_gap_max = 0;
+ write_gap_max = 0;
+ @(negedge clk_i);
+ awaddr[0] = 32'h30000400;
+ awlen[0] = 15;
+ awvalid[0] = 1;
+ do @(posedge clk_i); while (!awready[0]);
+ started = $time;
+ @(negedge clk_i);
+ awvalid[0] = 0;
+ wvalid[0] = 1;
+ for (integer beat = 0; beat < 16; beat++) begin
+ wdata[0] = 32'hface0000 + beat;
+ wlast[0] = (beat == 15);
+ do @(posedge clk_i); while (!wready[0]);
+ if (beat == 0) first_beat = $time;
+ else begin
+ if (($time - previous_beat) != 10) $fatal(1, "write streaming bubble beat=%0d", beat);
+ write_gap_max = 1;
+ end
+ previous_beat = $time;
+ @(negedge clk_i);
+ end
+ final_write = previous_beat;
+ wvalid[0] = 0;
+ bready[0] = 1;
+ do @(posedge clk_i); while (!bvalid[0]);
+ if (bresp[0] != 0) $fatal(1, "streaming write response");
+ $display("P4_STREAM_WRITE first=%0d total=%0d b_after_last=%0d", (first_beat - started) / 10,
+ ($time - started) / 10, ($time - final_write) / 10);
+ @(negedge clk_i);
+ bready[0] = 0;
+ awlen[0] = 0;
+ araddr[0] = 32'h30000400;
+ arlen[0] = 15;
+ arvalid[0] = 1;
+ rready[0] = 1;
+ do @(posedge clk_i); while (!arready[0]);
+ started = $time;
+ @(negedge clk_i);
+ arvalid[0] = 0;
+ for (integer beat = 0; beat < 16; beat++) begin
+ do @(posedge clk_i); while (!rvalid[0]);
+ if (erdata[0] !== (32'hface0000 + beat) || rresp[0] != 0 || rlast[0] != (beat == 15))
+ $fatal(1, "streaming read ordering beat=%0d", beat);
+ if (beat == 0) first_beat = $time;
+ else begin
+ if (($time - previous_beat) != 10) $fatal(1, "read streaming bubble beat=%0d", beat);
+ read_gap_max = 1;
+ end
+ previous_beat = $time;
+ @(negedge clk_i);
+ end
+ rready[0] = 0;
+ arlen[0] = 0;
+ $display("P4_STREAM_READ first=%0d total=%0d", (first_beat - started) / 10,
+ (previous_beat - started) / 10);
+ $display("P4_STREAM read_gap_max=%0d write_gap_max=%0d beats=16", read_gap_max, write_gap_max);
+ endtask
+
+ task automatic buffered_read;
+ // Two reserved words must survive a full queue, local writes to their
+ // addresses, and READY dropping again immediately after the first pop.
+ @(negedge clk_i);
+ araddr[0] = 32'h30000400;
+ arlen[0] = 15;
+ arvalid[0] = 1;
+ do @(posedge clk_i); while (!arready[0]);
+ @(negedge clk_i);
+ arvalid[0] = 0;
+ repeat (8) @(negedge clk_i);
+ store(32'h30000400, 32'habcd0000, 2);
+ store(32'h30000404, 32'habcd0001, 2);
+ store(32'h30000408, 32'habcd0002, 2);
+ for (integer beat = 0; beat < 16; beat++) begin
+ rready[0] = 1;
+ do @(posedge clk_i); while (!rvalid[0]);
+ if (erdata[0] !== ((beat == 2) ? 32'habcd0002 : (32'hface0000 + beat)) ||
+ rresp[0] != 0 || rlast[0] != (beat == 15))
+ $fatal(1, "reserved response lost/reordered beat=%0d", beat);
+ @(negedge clk_i);
+ rready[0] = 0;
+ repeat (beat % 5 + 1) @(negedge clk_i);
+ end
+ arlen[0] = 0;
+ endtask
+
+ task automatic buffered_write_errors;
+ integer beats, stalls;
+ // Earlier queued beats must commit before an error B. Later legal beats
+ // after a bad strobe keep the existing target policy; early LAST ends W.
+ for (integer bad = 0; bad < 3; bad++) begin
+ for (integer n = 0; n < 8; n++) store(32'h30000600 + n * 4, 32'h12340000 + n, 2);
+ beats = (bad == 1) ? 4 : 8;
+ stalls = 0;
+ @(negedge clk_i);
+ awaddr[0] = 32'h30000600;
+ awlen[0] = 7;
+ awsize[0] = (bad == 0) ? 1 : 2;
+ awvalid[0] = 1;
+ do @(posedge clk_i); while (!awready[0]);
+ @(negedge clk_i);
+ awvalid[0] = 0;
+ fork
+ begin
+ for (integer beat = 0; beat < beats; beat++) begin
+ wvalid[0] = 1;
+ wdata[0] = 32'hbeefbeef;
+ wstrb[0] = (bad == 0) ? ((beat == 3) ? 4'hf : ((beat % 2) == 0 ? 4'h3 : 4'hc)) : 4'hf;
+ wlast[0] = (bad == 1) ? (beat == 3) : ((bad == 0) && (beat == 7));
+ do begin
+ @(posedge clk_i);
+ if (!wready[0]) stalls++;
+ end while (!wready[0]);
+ @(negedge clk_i);
+ wvalid[0] = 0;
+ if (beat == 4) repeat (3) @(negedge clk_i);
+ end
+ end
+ begin
+ repeat (6) load(0, 32'h30000020, 32'h98765432, 0);
+ end
+ begin
+ repeat (6) load(1, 32'h30000020, 32'h98765432, 0);
+ end
+ join
+ wait (bvalid[0]);
+ if (bresp[0] != 2) $fatal(1, "buffered malformed write response");
+ if (bad != 0 && stalls == 0) $fatal(1, "write buffer full path not exercised");
+ for (integer n = 0; n < 8; n++) begin
+ if (bad == 0) begin
+ load(0, 32'h30000600 + n * 4,
+ (n == 1) ? 32'h1234beef : ((n < 4) ? 32'hbeefbeef : (32'h12340000 + n)), 3);
+ end else begin
+ load(0, 32'h30000600 + n * 4, (n < (beats - 1)) ? 32'hbeefbeef : (32'h12340000 + n), 3);
+ end
+ end
+ @(negedge clk_i);
+ bready[0] = 1;
+ @(posedge clk_i);
+ @(negedge clk_i);
+ bready[0] = 0;
+ end
+ awlen[0] = 0;
+ awsize[0] = 2;
+ wlast[0] = 1;
+ wstrb[0] = 15;
+ endtask
+ initial begin
+ #20000000;
+ $fatal(1, "SIM_TEST_TIMEOUT Tiny P4 memory");
+ end
+ initial begin
+ wr = 0;
+ trans[0] = 0;
+ trans[1] = 0;
+ addr[0] = 0;
+ addr[1] = 0;
+ data[0] = 0;
+ data[1] = 0;
+ size[0] = 2;
+ size[1] = 2;
+ awvalid = 0;
+ wvalid = 0;
+ wlast = 0;
+ bready = 0;
+ arvalid = 0;
+ rready = 0;
+ for (integer g = 0; g < 4; g++) begin
+ awaddr[g] = 0;
+ awlen[g] = 0;
+ awsize[g] = 2;
+ awburst[g] = 1;
+ araddr[g] = 0;
+ arlen[g] = 0;
+ arsize[g] = 2;
+ arburst[g] = 1;
+ wdata[g] = 0;
+ wstrb[g] = 15;
+ end
+ cfg.paddr = 0;
+ cfg.pprot = 0;
+ cfg.psel = 0;
+ cfg.penable = 0;
+ cfg.pwrite = 0;
+ cfg.pwdata = 0;
+ cfg.pstrb = 15;
+ repeat (5) @(negedge clk_i);
+ rst_n_i = 1;
+ config_read(12'h00c, 131072);
+ config_read(12'h010, 32);
+ config_read(12'h014, 4096);
+ // First/last words of all 32 physical macros and every 32 KiB boundary.
+ for (integer bank = 0; bank < 32; bank++) begin
+ store(32'h30000000 + bank * 4096, 32'ha5000000 + bank, 2);
+ store(32'h30000ffc + bank * 4096, 32'h5a000000 + bank, 2);
+ load(0, 32'h30000000 + bank * 4096, 32'ha5000000 + bank, 3);
+ load(1, 32'h30000ffc + bank * 4096, 32'h5a000000 + bank, 3);
+ end
+ store(32'h30000020, 32'h11223344, 2);
+ store(32'h30000021, 32'haa, 0);
+ store(32'h30000022, 32'hbbcc, 1);
+ load(0, 32'h30000020, 32'hbbccaa44, 3);
+ for (integer lane = 0; lane < 4; lane++) begin
+ store(32'h30000028, 32'h11223344, 2);
+ store(32'h30000028 + lane, 32'haa, 0);
+ load(0, 32'h30000028, (32'h11223344 & ~(32'hff << (lane * 8))) | (32'haa << (lane * 8)), 3);
+ end
+ store(32'h30000028, 32'h11223344, 2);
+ store(32'h30000028, 32'haabb, 1);
+ load(0, 32'h30000028, 32'h1122aabb, 3);
+ back_to_back();
+ fork
+ load(0, 32'h30000000, 32'ha5000000, 3);
+ load(1, 32'h30008000, 32'ha5000008, 3);
+ join
+ if (parallel_issues == 0) $fatal(1, "different groups serialized");
+ fork
+ load(0, 32'h30000000, 32'ha5000000, 0);
+ load(1, 32'h30000000, 32'ha5000000, 0);
+ read_external(0, 32'h30000000, 32'ha5000000, 0, 0);
+ join
+ // A held R must retain old data while local writes use that same macro.
+ fork
+ read_external(0, 32'h30000020, 32'hbbccaa44, 31, 0);
+ begin
+ wait (rvalid[0]);
+ store(32'h30000020, 32'h98765432, 2);
+ load(0, 32'h30000020, 32'h98765432, 3);
+ end
+ join
+ // Waiting for external W does not lock the macro against CPU accesses.
+ fork
+ write_external(0, 32'h30000024, 32'hdeadbeef, 15, 31);
+ begin
+ wait (wready[0]);
+ load(0, 32'h30000020, 32'h98765432, 3);
+ end
+ join
+ load(1, 32'h30000024, 32'hdeadbeef, 3);
+ @(negedge clk_i);
+ perf_clear = 1;
+ @(negedge clk_i);
+ perf_clear = 0;
+ fork
+ read_external(0, 32'h30000000, 32'ha5000000, 7, 0);
+ read_external(1, 32'h30008000, 32'ha5000008, 7, 0);
+ read_external(2, 32'h30010000, 32'ha5000010, 7, 0);
+ read_external(3, 32'h30018000, 32'ha5000018, 7, 0);
+ join
+ config_read(12'h020, 4);
+ config_read(12'h024, 0);
+ config_read(12'h028, 4);
+ config_read(12'h030, 7); // Four parallel stalls count once per clock.
+ burst_and_errors();
+ streaming_burst();
+ buffered_read();
+ buffered_write_errors();
+ read_external(0, 32'h30008000, 0, 0, 3);
+ ahb(1, 0, 32'h70000000, 2, 0, 1, 0, observed);
+ if (local_fault_count[1] != 0) $fatal(1, "external CPU error double-counted locally");
+ ahb(1, 0, 32'h30000001, 2, 0, 1, 0, observed);
+ if (local_fault_count[1] != 1 || local_fault_addr[1] != 32'h30000001 || local_fault_write[1])
+ $fatal(1, "local data fault metadata missing");
+ ahb(0, 1, 32'h30000000, 2, 0, 1, 0, observed);
+ if (local_fault_count[0] != 1 || !local_fault_write[0])
+ $fatal(1, "local instruction fault metadata missing");
+ load(1, 32'h30000000, 32'ha5000000, 3);
+ // Deterministic word/byte readback across all groups without touching a
+ // simulator memory array: every operation travels through the real ports.
+ for (integer g = 0; g < 4; g++) begin
+ for (integer n = 0; n < 4096; n++) begin
+ seed = seed ^ (seed << 13);
+ seed = seed ^ (seed >> 17);
+ seed = seed ^ (seed << 5);
+ store(32'h30000000 + g * 32768 + (n % 8192) * 4, seed, 2);
+ load(0, 32'h30000000 + g * 32768 + (n % 8192) * 4, seed, 3);
+ end
+ end
+ store(32'h3001fff0, 32'h76543210, 2);
+ @(negedge clk_i);
+ araddr[3] = 32'h3001fff0;
+ arlen[3] = 3;
+ arvalid[3] = 1;
+ do @(posedge clk_i); while (!arready[3]);
+ @(negedge clk_i);
+ arvalid[3] = 0;
+ wait (rvalid[3]);
+ repeat (4) @(negedge clk_i);
+ @(negedge clk_i);
+ rst_n_i = 0;
+ repeat (3) @(negedge clk_i);
+ if (rvalid != 0 || bvalid != 0) $fatal(1, "system reset retained a stale response");
+ rst_n_i = 1;
+ arlen[3] = 0;
+ load(0, 32'h3001fff0, 32'h76543210, 3);
+ read_external(3, 32'h3001fff0, 32'h76543210, 0, 0);
+ // Reset after W acceptance, before the captured word reaches a macro.
+ // The cancelled word and its completion must not replay into a new session.
+ @(negedge clk_i);
+ awaddr[3] = 32'h3001fff0;
+ awlen[3] = 3;
+ awvalid[3] = 1;
+ do @(posedge clk_i); while (!awready[3]);
+ @(negedge clk_i);
+ awvalid[3] = 0;
+ wdata[3] = 32'hbad0bad0;
+ wlast[3] = 0;
+ wvalid[3] = 1;
+ do @(posedge clk_i); while (!wready[3]);
+ @(negedge clk_i);
+ wvalid[3] = 0;
+ rst_n_i = 0;
+ repeat (3) @(negedge clk_i);
+ rst_n_i = 1;
+ awlen[3] = 0;
+ wlast[3] = 1;
+ repeat (5) @(negedge clk_i);
+ if (bvalid != 0 || rvalid != 0) $fatal(1, "cancelled W response replayed");
+ read_external(3, 32'h3001fff0, 32'h76543210, 0, 0);
+ $display("P4_BUFFER backpressure errors reset conservation PASS");
+ $display("P4_MEMORY local_latency_max=%0d parallel_issue_cycles=%0d", local_latency_max,
+ parallel_issues);
+ $display("SIM_TEST_PASS Tiny P4 memory seed=12345678 random_per_group=4096");
+ $finish;
+ end
+endmodule
diff --git a/tests/rtl/tiny_r2_p5_cpu.S b/tests/rtl/tiny_r2_p5_cpu.S
new file mode 100644
index 00000000..8e03bacc
--- /dev/null
+++ b/tests/rtl/tiny_r2_p5_cpu.S
@@ -0,0 +1,144 @@
+.section .text
+.global _start
+.option norvc
+_start:
+ # Verification-only slow ROM. Copy this position-independent image to SRAM.
+ la t0, _start
+ la t1, image_end
+ li t2, 0x30000000
+1: lw t3, 0(t0)
+ sw t3, 0(t2)
+ addi t0, t0, 4
+ addi t2, t2, 4
+ bltu t0, t1, 1b
+ fence.i
+ la t0, sram_main
+ li t1, 0x30000000
+ add t0, t0, t1
+ jr t0
+sram_main:
+ li sp, 0x3001f000
+ li s0, 0x1000a100
+ li s1, 0x1000b000
+ li s2, 0x3001ffe0
+ lw t0, 0x40(s1)
+ bnez t0, mapped_done
+ li t0, 0x30010000
+ li t1, 256
+ li t2, 0x12345678
+2: sw t2, 0(t0)
+ addi t0, t0, 4
+ addi t2, t2, 1
+ addi t1, t1, -1
+ bnez t1, 2b
+ li t0, 0xe0
+ sw t0, 4(s0)
+ li t0, 0x30010000
+ sw t0, 8(s0)
+ li t0, 0x30018000
+ sw t0, 12(s0)
+ li t0, 1024
+ sw t0, 16(s0)
+ sw zero, 20(s0)
+ li t0, 16
+ sw t0, 24(s0)
+ li t0, 1
+ sw t0, 0(s0)
+ jal ra, wait_dma
+ li t0, 0x30018000
+ li t1, 256
+ li t2, 0x12345678
+3: lw t3, 0(t0)
+ bne t3, t2, fail
+ addi t0, t0, 4
+ addi t2, t2, 1
+ addi t1, t1, -1
+ bnez t1, 3b
+ # Start an XPI read whose response is withheld by the boundary model.
+ li t0, 0x50000000
+ sw t0, 8(s0)
+ li t0, 0x10009040
+ sw t0, 12(s0)
+ li t0, 0x60
+ sw t0, 4(s0)
+ li t0, 4
+ sw t0, 16(s0)
+ li t0, 1
+ sw t0, 0(s0)
+ li t0, 0x30008000
+ li t1, 64
+4: sw t1, 0(t0)
+ addi t0, t0, 4
+ addi t1, t1, -1
+ bnez t1, 4b
+ li t0, 1
+ sw t0, 0x40(s1)
+ lw t1, 0x40(s1)
+ bne t0, t1, fail
+ sw t0, 0(s2)
+ # The debugger resets this hart while DMA/XPI stays owned. PERF_CTRL is
+ # retained by SYSCTRL and selects the resume path after the ROM copy.
+5: lw t1, 0x40(s1)
+ li t0, 1
+ bne t0, t1, fail
+ j 5b
+mapped_done:
+ jal ra, wait_dma
+ li t0, 0x10009044
+ lw t1, 0(t0)
+ li t0, 0x40000000
+ bne t0, t1, fail
+ li t0, 0xe0
+ sw t0, 4(s0)
+ li t0, 0x30018000
+ sw t0, 12(s0)
+ li t0, 64
+ sw t0, 16(s0)
+ sw zero, 0x40(s1)
+ li t0, 2
+ sw t0, 0(s2)
+ li t0, 0x50000100
+ sw t0, 8(s0)
+ li t0, 1
+ sw t0, 0(s0)
+ li t1, 128
+7: addi t1, t1, -1
+ bnez t1, 7b
+ li t0, 8
+ sw t0, 0(s0)
+ lw t1, 32(s0)
+ andi t1, t1, 1
+ beqz t1, fail
+ li t0, 3
+ sw t0, 0(s2)
+ li t2, 100000
+8: lw t1, 32(s0)
+ andi t0, t1, 1
+ beqz t0, 9f
+ addi t2, t2, -1
+ bnez t2, 8b
+ j fail
+9: andi t0, t1, 8
+ beqz t0, fail
+ li t0, 0x80000001
+ sw t0, 0x84(s1)
+10: j 10b
+wait_dma:
+ li t2, 100000
+11: lw t1, 32(s0)
+ andi t0, t1, 1
+ beqz t0, 12f
+ addi t2, t2, -1
+ bnez t2, 11b
+ j fail
+12: andi t0, t1, 0x14
+ li t1, 4
+ bne t0, t1, fail
+ ret
+fail:
+ li t0, 0x1000b084
+ li t1, 0x80000000
+ sw t1, 0(t0)
+13: j 13b
+.balign 4
+image_end:
diff --git a/tests/rtl/tiny_r2_p5_fabric_formal.sv b/tests/rtl/tiny_r2_p5_fabric_formal.sv
new file mode 100644
index 00000000..998294ab
--- /dev/null
+++ b/tests/rtl/tiny_r2_p5_fabric_formal.sv
@@ -0,0 +1,129 @@
+module tiny_r2_p5_fabric_formal (
+ input logic clk_i,
+ rst_n_i
+);
+`ifdef TINY_P5_RESET_FORMAL
+ localparam bit TargetReset = 1'b1;
+`else
+ localparam bit TargetReset = 1'b0;
+`endif
+ logic started = 0;
+ (* anyseq *) logic [2:0] arvalid, awvalid, wvalid, rready, bready;
+ (* anyseq *)logic [2:0][1:0] length;
+ (* anyseq *)logic target_reset_n;
+ wire target_enable = !TargetReset || target_reset_n;
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) masters[3] (
+ .aclk (clk_i),
+ .aresetn(rst_n_i)
+ );
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) target (
+ .aclk (clk_i),
+ .aresetn(rst_n_i)
+ );
+ logic idle, fault, fault_write, fault_instruction;
+ logic [31:0] fault_addr;
+ logic [1:0] fault_owner, fault_resp;
+ always_ff @(posedge clk_i) begin
+ started <= 1;
+ assume (rst_n_i == started);
+ end
+ tiny_axi4_target u_target (
+ .clk_i (clk_i),
+ .rst_n_i (rst_n_i),
+ .enable_i (target_enable),
+ .masters (masters),
+ .target (target),
+ .idle_o (idle),
+ .fault_valid_o (fault),
+ .fault_addr_o (fault_addr),
+ .fault_master_o (fault_owner),
+ .fault_write_o (fault_write),
+ .fault_instruction_o(fault_instruction),
+ .fault_resp_o (fault_resp)
+ );
+ axi4_error_slave #(
+ .Response(2'b10)
+ ) u_endpoint (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i && target_enable),
+ .axi4 (target)
+ );
+ for (genvar m = 0; m < 3; m++) begin : gen_master
+ logic outstanding = 0;
+ logic [2:0] remaining = 0;
+ logic write_data_pending = 0;
+ wire accept=(masters[m].arvalid && masters[m].arready)||(masters[m].awvalid && masters[m].awready);
+ wire done=(masters[m].rvalid && masters[m].rready && masters[m].rlast)||
+ (masters[m].bvalid && masters[m].bready);
+ assign masters[m].araddr = 32'h30000000 + 32'(m) * 32'h100;
+ assign masters[m].awaddr = 32'h30000000 + 32'(m) * 32'h100;
+ assign masters[m].arlen = {6'd0, length[m]};
+ assign masters[m].awlen = 0;
+ assign masters[m].arsize = 2;
+ assign masters[m].awsize = 2;
+ assign masters[m].arburst = 1;
+ assign masters[m].awburst = 1;
+ assign masters[m].arlock = 0;
+ assign masters[m].awlock = 0;
+ assign masters[m].arid = 1'(m);
+ assign masters[m].awid = 1'(m);
+ assign masters[m].arcache = 0;
+ assign masters[m].awcache = 0;
+ assign masters[m].arprot = 0;
+ assign masters[m].awprot = 0;
+ assign masters[m].arqos = 0;
+ assign masters[m].awqos = 0;
+ assign masters[m].arregion = 0;
+ assign masters[m].awregion = 0;
+ assign masters[m].aruser = 0;
+ assign masters[m].awuser = 0;
+ assign masters[m].arvalid = arvalid[m] && !outstanding;
+ assign masters[m].awvalid = awvalid[m] && !outstanding;
+ assign masters[m].wvalid = wvalid[m];
+ assign masters[m].wlast = 1;
+ assign masters[m].wdata = 32'(m);
+ assign masters[m].wstrb = 15;
+ assign masters[m].wuser = 0;
+ assign masters[m].rready = rready[m];
+ assign masters[m].bready = bready[m];
+ always_ff @(posedge clk_i) begin
+ if (!rst_n_i) begin
+ outstanding <= 0;
+ remaining <= 0;
+ write_data_pending <= 0;
+ end else begin
+ if (masters[m].arvalid && masters[m].arready) remaining <= {1'b0, length[m]} + 3'd1;
+ if (masters[m].rvalid) begin
+ assert (remaining != 0);
+ assert (masters[m].rlast == (remaining == 1));
+ if (masters[m].rready) remaining <= remaining - 3'd1;
+ end
+ if (masters[m].awvalid && masters[m].awready) write_data_pending <= 1;
+ if (masters[m].wvalid && masters[m].wready) write_data_pending <= 0;
+ if (masters[m].bvalid) assert (!write_data_pending);
+ if (accept) outstanding <= 1;
+ if (done) outstanding <= 0;
+ assert (!(accept && outstanding));
+ assert (!done || outstanding);
+ assert (!(masters[m].arready && masters[m].awready));
+ if (done) begin
+ assert (fault && fault_owner == 2'(m));
+ if (TargetReset)
+ assert (fault_resp == 2 || fault_resp == 3);
+ else assert (fault_resp == 2);
+ assert (fault_addr == 32'h30000000 + 32'(m) * 32'h100);
+ end
+ end
+ end
+ end
+endmodule
diff --git a/tests/rtl/tiny_r2_p5_fabric_tb.sv b/tests/rtl/tiny_r2_p5_fabric_tb.sv
new file mode 100644
index 00000000..f8ac5184
--- /dev/null
+++ b/tests/rtl/tiny_r2_p5_fabric_tb.sv
@@ -0,0 +1,525 @@
+`include "tiny_topology.svh"
+`timescale 1ns / 1ps
+module tiny_r2_p5_fabric_tb;
+ logic clk_i = 0;
+ logic rst_n_i = 0;
+ logic target_rst_n[`RETROSOC_TINY__TARGET_COUNT];
+ always #5 clk_i = !clk_i;
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) masters[3] (
+ .aclk (clk_i),
+ .aresetn(rst_n_i)
+ );
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) targets[`RETROSOC_TINY__TARGET_COUNT] (
+ .aclk (clk_i),
+ .aresetn(rst_n_i)
+ );
+ logic [5:0] fault_valid, fault_instruction;
+ logic [5:0][31:0] fault_addr;
+ logic [5:0][ 1:0] fault_resp;
+ logic [63:0] cpu_wait, dma_wait;
+ integer faults = 0;
+ logic [`RETROSOC_TINY__TARGET_COUNT-1:0]
+ hold_address = '0, hold_read = '0, hold_write = '0, hold_b = '0;
+ integer error_beat [`RETROSOC_TINY__TARGET_COUNT];
+ logic [ 2:0] error_last_wait = '0;
+ logic release_error = 0;
+ integer simultaneous_faults = 0;
+ logic [31:0] seed = 32'h12345678;
+ integer completions [ 3];
+ integer target_requests [`RETROSOC_TINY__TARGET_COUNT];
+ logic [ 2:0][31:0] awaddr;
+ logic [ 2:0][ 7:0] awlen;
+ logic [ 2:0][ 2:0] awsize;
+ logic [ 2:0][ 1:0] awburst;
+ logic [ 2:0][ 0:0] awlock;
+ logic [ 2:0][ 0:0] awid;
+ logic [ 2:0][ 0:0] awvalid;
+ logic [ 2:0][31:0] wdata;
+ logic [ 2:0][ 3:0] wstrb;
+ logic [ 2:0][ 0:0] wlast;
+ logic [ 2:0][ 0:0] wvalid;
+ logic [ 2:0][ 0:0] bready;
+ logic [ 2:0][31:0] araddr;
+ logic [ 2:0][ 7:0] arlen;
+ logic [ 2:0][ 2:0] arsize;
+ logic [ 2:0][ 1:0] arburst;
+ logic [ 2:0][ 0:0] arlock;
+ logic [ 2:0][ 0:0] arid;
+ logic [ 2:0][ 0:0] arvalid;
+ logic [ 2:0][ 2:0] arprot;
+ logic [ 2:0][ 0:0] rready;
+ logic [ 2:0][ 0:0] awready;
+ logic [ 2:0][ 0:0] arready;
+ logic [ 2:0][ 0:0] wready;
+ logic [ 2:0][ 0:0] bvalid;
+ logic [ 2:0][ 0:0] bid;
+ logic [ 2:0][ 1:0] bresp;
+ logic [ 2:0][ 0:0] rvalid;
+ logic [ 2:0][ 0:0] rid;
+ logic [ 2:0][ 0:0] rlast;
+ logic [ 2:0][31:0] rdata;
+ logic [ 2:0][ 1:0] rresp;
+ tiny_axi4_fabric u_dut (
+ .clk_i (clk_i),
+ .rst_n_i (rst_n_i),
+ .target_rst_n_i (target_rst_n),
+ .perf_enable_i (1'b1),
+ .perf_clear_i (1'b0),
+ .masters (masters),
+ .targets (targets),
+ .fault_valid_o (fault_valid),
+ .fault_addr_o (fault_addr),
+ .fault_instruction_o(fault_instruction),
+ .owner_idle_o (),
+ .target_idle_o (),
+ .fault_resp_o (fault_resp),
+ .cpu_wait_o (cpu_wait),
+ .dma_wait_o (dma_wait)
+ );
+ for (genvar target = 0; target < `RETROSOC_TINY__TARGET_COUNT; target++) begin : gen_target_reset
+ assign target_rst_n[target] = rst_n_i;
+ end
+ always @(posedge clk_i) begin
+ faults <= faults + $countones(fault_valid);
+ if (fault_valid[2] && fault_valid[4]) begin
+ simultaneous_faults <= simultaneous_faults + 1;
+ if (fault_addr[2] != 32'h30000004 || fault_addr[4] != 32'h30008004 ||
+ fault_resp[2] != 2 || fault_resp[4] != 2)
+ $fatal(1, "first nonfinal error metadata lost");
+ end
+ end
+ for (genvar m = 0; m < 3; m++) begin : gen_master
+ assign masters[m].awaddr = awaddr[m];
+ assign masters[m].awlen = awlen[m];
+ assign masters[m].awsize = awsize[m];
+ assign masters[m].awburst = awburst[m];
+ assign masters[m].awlock = awlock[m];
+ assign masters[m].awid = awid[m];
+ assign masters[m].awvalid = awvalid[m];
+ assign masters[m].wdata = wdata[m];
+ assign masters[m].wstrb = wstrb[m];
+ assign masters[m].wlast = wlast[m];
+ assign masters[m].wvalid = wvalid[m];
+ assign masters[m].bready = bready[m];
+ assign masters[m].araddr = araddr[m];
+ assign masters[m].arlen = arlen[m];
+ assign masters[m].arsize = arsize[m];
+ assign masters[m].arburst = arburst[m];
+ assign masters[m].arlock = arlock[m];
+ assign masters[m].arid = arid[m];
+ assign masters[m].arvalid = arvalid[m];
+ assign masters[m].rready = rready[m];
+ assign awready[m] = masters[m].awready;
+ assign arready[m] = masters[m].arready;
+ assign wready[m] = masters[m].wready;
+ assign bvalid[m] = masters[m].bvalid;
+ assign bid[m] = masters[m].bid;
+ assign bresp[m] = masters[m].bresp;
+ assign rvalid[m] = masters[m].rvalid;
+ assign rid[m] = masters[m].rid;
+ assign rlast[m] = masters[m].rlast;
+ assign rdata[m] = masters[m].rdata;
+ assign rresp[m] = masters[m].rresp;
+ assign masters[m].awcache = '0;
+ assign masters[m].awprot = '0;
+ assign masters[m].awqos = '0;
+ assign masters[m].awregion = '0;
+ assign masters[m].awuser = '0;
+ assign masters[m].arcache = '0;
+ assign masters[m].arprot = arprot[m];
+ assign masters[m].arqos = '0;
+ assign masters[m].arregion = '0;
+ assign masters[m].aruser = '0;
+ assign masters[m].wuser = '0;
+ end
+ for (genvar t = 0; t < `RETROSOC_TINY__TARGET_COUNT; t++) begin : gen_target
+ tiny_p5_target_model #(
+ .ErrorResponse(t == `RETROSOC_TINY__DECODE_ERROR ? 3 : (t == `RETROSOC_TINY__PROTOCOL_ERROR ? 2 : 0))
+ ) u_response (
+ .clk_i (clk_i),
+ .rst_n_i (rst_n_i),
+ .hold_address_i(hold_address[t]),
+ .hold_read_i (hold_read[t]),
+ .hold_write_i (hold_write[t]),
+ .hold_b_i (hold_b[t]),
+ .error_beat_i (error_beat[t]),
+ .axi4 (targets[t])
+ );
+ initial begin
+ target_requests[t] = 0;
+ error_beat[t] = -1;
+ end
+ always @(posedge clk_i) begin
+ if ((targets[t].arvalid && targets[t].arready) || (targets[t].awvalid && targets[t].awready))
+ target_requests[t] <= target_requests[t] + 1;
+ end
+ end
+ task automatic read_request(input int m, input logic [31:0] addr, input int beats,
+ input logic [2:0] size, input logic [1:0] burst,
+ input logic [1:0] expected);
+ logic [35:0] held;
+ @(negedge clk_i);
+ araddr[m] = addr;
+ arlen[m] = 8'(beats - 1);
+ arsize[m] = size;
+ arburst[m] = burst;
+ arid[m] = 1'(m);
+ arvalid[m] = 1;
+ do @(posedge clk_i); while (!arready[m]);
+ @(negedge clk_i);
+ arvalid[m] = 0;
+ araddr[m] = 32'hbad00000; // The accepted address, not live pins, owns responses.
+ for (int beat = 0; beat < beats; beat++) begin
+ do @(posedge clk_i); while (!rvalid[m]);
+ held = {rid[m], rlast[m], rresp[m], rdata[m]};
+ repeat (3) begin
+ @(negedge clk_i);
+ if (!rvalid[m] || held != {rid[m], rlast[m], rresp[m], rdata[m]})
+ $fatal(1, "read changed under backpressure");
+ end
+ if (rresp[m] != expected || rid[m] != 1'(m) || rlast[m] != (beat == beats - 1))
+ $fatal(
+ 1,
+ "read response/ID/length mismatch m=%0d addr=%x beat=%0d resp=%0d",
+ m,
+ addr,
+ beat,
+ rresp[m]
+ );
+ if (expected == 0 && rdata[m] !== (32'h10000000 ^ (addr + (32'(beat) << size))))
+ $fatal(1, "read data routed from wrong target/beat");
+ rready[m] = 1;
+ @(posedge clk_i);
+ @(negedge clk_i);
+ rready[m] = 0;
+ end
+ completions[m]++;
+ endtask
+ task automatic write_request(input int m, input logic [31:0] addr, input int beats,
+ input logic [1:0] expected, input bit early_w);
+ @(negedge clk_i);
+ awaddr[m] = addr;
+ awlen[m] = 8'(beats - 1);
+ awsize[m] = 2;
+ awburst[m] = 1;
+ awid[m] = 1'(m);
+ wdata[m] = 32'hb0000000 ^ addr;
+ wstrb[m] = 4'b0101;
+ wlast[m] = (beats == 1);
+ if (early_w) begin
+ wvalid[m] = 1;
+ repeat (4) begin
+ @(posedge clk_i);
+ if (wready[m]) $fatal(1, "W accepted before AW");
+ end
+ @(negedge clk_i);
+ end
+ awvalid[m] = 1;
+ do @(posedge clk_i); while (!awready[m]);
+ @(negedge clk_i);
+ awvalid[m] = 0;
+ awaddr[m] = 32'hbad08000;
+ if (!early_w) repeat (5) @(negedge clk_i);
+ for (int beat = 0; beat < beats; beat++) begin
+ wvalid[m] = 1;
+ wlast[m] = (beat == beats - 1);
+ wdata[m] = 32'hb0000000 ^ addr ^ 32'(beat);
+ do @(posedge clk_i); while (!wready[m]);
+ @(negedge clk_i);
+ wvalid[m] = 0;
+ repeat (2) @(negedge clk_i);
+ end
+ do @(posedge clk_i); while (!bvalid[m]);
+ repeat (3) begin
+ @(negedge clk_i);
+ if (!bvalid[m] || bresp[m] != expected || bid[m] != 1'(m))
+ $fatal(1, "write response mismatch");
+ end
+ bready[m] = 1;
+ @(posedge clk_i);
+ @(negedge clk_i);
+ bready[m] = 0;
+ completions[m]++;
+ endtask
+
+ task automatic mixed_error_read(input int m, input logic [31:0] address);
+ @(negedge clk_i);
+ araddr[m] = address;
+ arlen[m] = 3;
+ arsize[m] = 2;
+ arburst[m] = 1;
+ arvalid[m] = 1;
+ arid[m] = 1'(m);
+ do @(posedge clk_i); while (!arready[m]);
+ @(negedge clk_i);
+ arvalid[m] = 0;
+ for (integer beat = 0; beat < 4; beat++) begin
+ wait (rvalid[m]);
+ if (beat == 3) begin
+ error_last_wait[m] = 1;
+ wait (release_error);
+ end
+ @(negedge clk_i);
+ if (rresp[m] != (beat == 1 ? 2 : 0) || rlast[m] != (beat == 3))
+ $fatal(1, "mixed read response was altered");
+ rready[m] = 1;
+ @(posedge clk_i);
+ @(negedge clk_i);
+ rready[m] = 0;
+ end
+ endtask
+ task automatic random_reads(input int m, input logic [31:0] initial_seed);
+ logic [31:0] state;
+ logic [31:0] address;
+ integer beats;
+ state = initial_seed;
+ for (integer n = 0; n < 32; n++) begin
+ state = state ^ (state << 13);
+ state = state ^ (state >> 17);
+ state = state ^ (state << 5);
+ address = 32'h30000000 + (state % 4) * 32768 + ((state >> 4) % 16) * 64;
+ beats = 1 + integer'((state >> 12) % 16);
+ read_request(m, address, beats, 2, 1, 0);
+ end
+ endtask
+ initial begin
+ awaddr = '0;
+ awlen = '0;
+ awsize = '0;
+ awburst = '0;
+ awlock = '0;
+ awid = '0;
+ awvalid = '0;
+ wdata = '0;
+ wstrb = '0;
+ wlast = '0;
+ wvalid = '0;
+ bready = '0;
+ araddr = '0;
+ arlen = '0;
+ arsize = '0;
+ arburst = '0;
+ arlock = '0;
+ arid = '0;
+ arvalid = '0;
+ arprot = '0;
+ rready = '0;
+ for (integer m = 0; m < 3; m++) completions[m] = 0;
+ repeat (5) @(negedge clk_i);
+ rst_n_i = 1;
+ // XPI can withhold its first response indefinitely without owning APB/SRAM.
+ hold_read[4] = 1;
+ fork
+ read_request(1, 32'h50000000, 16, 2, 1, 0);
+ begin
+ wait (target_requests[4] == 1);
+ read_request(0, 32'h1000b000, 1, 2, 1, 0);
+ read_request(0, 32'h30000000, 4, 2, 1, 0);
+ if (!u_dut.s_busy_q[2] || u_dut.target_idle_o[4])
+ $fatal(1, "XPI test did not retain ownership");
+ hold_read[4] = 0;
+ end
+ join
+ // One owner's read and write can own different targets simultaneously.
+ hold_read[0] = 1;
+ hold_b[1] = 1;
+ hold_write[1] = 1;
+ fork
+ read_request(1, 32'h30000000, 16, 2, 1, 0);
+ write_request(1, 32'h30008000, 16, 0, 1);
+ begin
+ wait (u_dut.s_busy_q[2] && u_dut.s_busy_q[3]);
+ read_request(2, 32'h30010000, 4, 2, 1, 0);
+ if (u_dut.target_idle_o[0] || u_dut.target_idle_o[1])
+ $fatal(1, "different targets serialized");
+ hold_read[0] = 0;
+ hold_b[1] = 0;
+ hold_write[1] = 0;
+ end
+ join
+ // A waiting APB FIFO branch cannot block independent control admission.
+ hold_read[`RETROSOC_TINY__TARGET_XPI] = 1;
+ fork
+ read_request(1, 32'h10009000, 1, 2, 1, 0);
+ begin
+ wait (!u_dut.target_idle_o[`RETROSOC_TINY__TARGET_XPI]);
+ read_request(0, 32'h1000a000, 1, 2, 1, 0);
+ hold_read[`RETROSOC_TINY__TARGET_XPI] = 0;
+ end
+ join
+ fork
+ begin
+ repeat (8) read_request(0, 32'h30000000, 4, 2, 1, 0);
+ end
+ begin
+ repeat (8) write_request(1, 32'h30000000, 4, 0, 0);
+ end
+ begin
+ repeat (8) read_request(2, 32'h30000000, 4, 2, 1, 0);
+ end
+ join
+ if (cpu_wait == 0 || dma_wait == 0) $fatal(1, "same-target wait counters never advanced");
+ // Even when both CPU directions request different targets together,
+ // the fabric must retain the CPU's one-combined-operation contract.
+ fork
+ read_request(0, 32'h30000000, 4, 2, 1, 0);
+ write_request(0, 32'h30008000, 4, 0, 0);
+ join
+ error_beat[0] = 1;
+ error_beat[1] = 1;
+ fork
+ mixed_error_read(1, 32'h30000000);
+ mixed_error_read(2, 32'h30008000);
+ begin
+ wait (error_last_wait[1] && error_last_wait[2]);
+ @(negedge clk_i);
+ release_error = 1;
+ end
+ join
+ if (simultaneous_faults != 1) $fatal(1, "simultaneous terminal faults lost");
+ error_beat[0] = -1;
+ error_beat[1] = -1;
+ fork
+ random_reads(0, 32'h12345678);
+ random_reads(1, 32'h87654321);
+ random_reads(2, 32'hc0ffee01);
+ join
+ // An accepted address buffered ahead of a stopped target remains owned.
+ @(negedge clk_i);
+ hold_address[0] = 1;
+ araddr[1] = 32'h30000000;
+ arlen[1] = 0;
+ arsize[1] = 2;
+ arburst[1] = 1;
+ arvalid[1] = 1;
+ do @(posedge clk_i); while (!arready[1]);
+ @(negedge clk_i);
+ arvalid[1] = 0;
+ repeat (8) @(negedge clk_i);
+ if (u_dut.owner_idle_o[1] || u_dut.target_idle_o[0]) $fatal(1, "buffered AR not outstanding");
+ arvalid = '0;
+ awvalid = '0;
+ wvalid = '0;
+ rready = '0;
+ bready = '0;
+ rst_n_i = 0;
+ repeat (5) @(negedge clk_i);
+ hold_address = '0;
+ rst_n_i = 1;
+ repeat (4) @(negedge clk_i);
+ if (rvalid != 0 || bvalid != 0 || u_dut.owner_idle_o != 7) $fatal(1, "old session replayed");
+ read_request(2, 32'h30000000, 1, 2, 1, 0);
+ $display("SIM_TEST_PASS Tiny P5 fabric seed=12345678 random_reads=96 concurrent_faults=2");
+ $finish;
+ end
+ initial begin
+ repeat (100000) @(posedge clk_i);
+ $fatal(1, "SIM_TEST_TIMEOUT Tiny P5 fabric");
+ end
+endmodule
+
+// Verification endpoint: delays only the creation of VALID, never withdraws a
+// presented response. Per-address data and write checks detect crossed routes.
+module tiny_p5_target_model #(
+ parameter logic [1:0] ErrorResponse = 0
+) (
+ input logic clk_i,
+ rst_n_i,
+ hold_address_i,
+ hold_read_i,
+ hold_write_i,
+ hold_b_i,
+ input integer error_beat_i,
+ axi4_if.slave axi4
+);
+ logic active = 0, writing = 0, write_done = 0, id = 0;
+ logic [31:0] address = 0, start_address = 0;
+ logic [7:0] length = 0, beat = 0;
+ logic [2:0] size = 0;
+ logic [1:0] burst = 0;
+ assign axi4.arready = !active && !hold_address_i;
+ assign axi4.awready = !active && !hold_address_i && !axi4.arvalid;
+ assign axi4.wready = active && writing && !write_done && !hold_write_i;
+ assign axi4.ruser = 0;
+ assign axi4.buser = 0;
+ always @(posedge clk_i) begin
+ if (!rst_n_i) begin
+ active <= 0;
+ writing <= 0;
+ write_done <= 0;
+ axi4.rvalid <= 0;
+ axi4.bvalid <= 0;
+ axi4.rdata <= 0;
+ axi4.rresp <= 0;
+ axi4.rlast <= 0;
+ axi4.rid <= 0;
+ axi4.bid <= 0;
+ axi4.bresp <= 0;
+ end else begin
+ if (axi4.arvalid && axi4.arready) begin
+ active <= 1;
+ writing <= 0;
+ address <= axi4.araddr;
+ start_address <= axi4.araddr;
+ length <= axi4.arlen;
+ beat <= 0;
+ id <= axi4.arid;
+ size <= axi4.arsize;
+ burst <= axi4.arburst;
+ end
+ if (axi4.awvalid && axi4.awready) begin
+ active <= 1;
+ writing <= 1;
+ write_done <= 0;
+ address <= axi4.awaddr;
+ start_address <= axi4.awaddr;
+ length <= axi4.awlen;
+ beat <= 0;
+ id <= axi4.awid;
+ size <= axi4.awsize;
+ burst <= axi4.awburst;
+ end
+ if (active && !writing && !axi4.rvalid && !hold_read_i) begin
+ axi4.rvalid <= 1;
+ axi4.rid <= id;
+ axi4.rdata <= 32'h10000000 ^ address;
+ axi4.rresp<=(ErrorResponse != 0) ? ErrorResponse : ((integer'(beat)==error_beat_i) ? 2 : 0);
+ axi4.rlast <= beat == length;
+ end
+ if (axi4.rvalid && axi4.rready) begin
+ axi4.rvalid <= 0;
+ if (axi4.rlast) active <= 0;
+ else begin
+ beat <= beat + 1;
+ if (burst == 1) address <= address + (32'd1 << size);
+ end
+ end
+ if (axi4.wvalid && axi4.wready) begin
+ if (axi4.wdata !== (32'hb0000000 ^ start_address ^ {24'd0, beat}))
+ $fatal(1, "write data crossed AW ownership");
+ if (axi4.wlast || beat == length) write_done <= 1;
+ else beat <= beat + 1;
+ end
+ if (active && writing && write_done && !hold_b_i) begin
+ axi4.bvalid <= 1;
+ axi4.bid <= id;
+ axi4.bresp <= ErrorResponse;
+ end
+ if (axi4.bvalid && axi4.bready) begin
+ axi4.bvalid <= 0;
+ active <= 0;
+ write_done <= 0;
+ end
+ end
+ end
+endmodule
diff --git a/tests/rtl/tiny_r2_p5_fault_formal.sv b/tests/rtl/tiny_r2_p5_fault_formal.sv
new file mode 100644
index 00000000..8d07699b
--- /dev/null
+++ b/tests/rtl/tiny_r2_p5_fault_formal.sv
@@ -0,0 +1,55 @@
+module tiny_r2_p5_fault_formal;
+ (* anyseq *)logic [ 7:0] valid;
+ (* anyseq *)logic [ 7:0][31:0] addresses;
+ (* anyseq *)logic [ 1:0] local_write;
+ (* anyseq *)logic [ 5:0] instruction;
+ (* anyseq *)logic [ 5:0][ 1:0] responses;
+ logic [ 3:0] count;
+ logic [31:0] address;
+ logic [1:0] owner, response;
+ logic write_access;
+ logic found;
+ integer expected_owner, expected_write;
+ tiny_fault_collector u_dut (
+ .local_valid_i (valid[1:0]),
+ .local_addr_i (addresses[1:0]),
+ .local_write_i (local_write),
+ .external_valid_i (valid[7:2]),
+ .external_addr_i (addresses[7:2]),
+ .external_resp_i (responses),
+ .external_instruction_i(instruction),
+ .count_o (count),
+ .addr_o (address),
+ .master_o (owner),
+ .write_o (write_access),
+ .resp_o (response)
+ );
+ always_comb begin
+ assert (count == $countones(valid));
+ found = 0;
+ expected_owner = 0;
+ expected_write = 0;
+ // Enumerate the specified lexicographic order, independently of the
+ // collector's packed rank and minimum selection implementation.
+ for (integer m = 0; m < 3; m++) begin
+ for (integer path = 0; path < 2; path++) begin
+ for (integer direction = 0; direction < 2; direction++) begin
+ for (integer event_index = 0; event_index < 8; event_index++) begin
+ expected_owner = (event_index < 2) ? 0 : ((event_index - 2) / 2);
+ expected_write=(event_index<2)?integer'(local_write[event_index]):((event_index-2)%2);
+ if(!found && valid[event_index] && expected_owner==m && expected_write==direction &&
+ ((event_index<2)?(event_index==path):
+ (((event_index<4)&&instruction[event_index-2])?(path==0):(path==1)))) begin
+ found = 1;
+ assert (owner == 2'(m));
+ assert (write_access == 1'(direction));
+ assert (address == addresses[event_index]);
+ assert (response == ((event_index < 2) ? 2'b10 : responses[event_index-2]));
+ end
+ end
+ end
+ end
+ end
+ if (valid == 0) assert ({address, owner, write_access, response} == 0);
+ end
+endmodule
diff --git a/tests/rtl/tiny_r2_p5_reset_tb.sv b/tests/rtl/tiny_r2_p5_reset_tb.sv
new file mode 100644
index 00000000..5290e2ff
--- /dev/null
+++ b/tests/rtl/tiny_r2_p5_reset_tb.sv
@@ -0,0 +1,545 @@
+`include "tiny_topology.svh"
+`timescale 1ns / 1ps
+module tiny_r2_p5_reset_tb;
+ logic clk_i = 0, rst_n_i = 0;
+ always #5 clk_i = !clk_i;
+ logic target_rst_n[`RETROSOC_TINY__TARGET_COUNT];
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) masters[3] (
+ .aclk (clk_i),
+ .aresetn(rst_n_i)
+ );
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) targets[`RETROSOC_TINY__TARGET_COUNT] (
+ .aclk (clk_i),
+ .aresetn(target_rst_n)
+ );
+ apb4_if apb (
+ .pclk (clk_i),
+ .presetn(target_rst_n[`RETROSOC_TINY__TARGET_XPI])
+ );
+ logic [`RETROSOC_TINY__TARGET_COUNT-1:0]
+ hold_address = '0, hold_read = '0, hold_write = '0, hold_b = '0;
+ integer error_beat[`RETROSOC_TINY__TARGET_COUNT];
+ integer
+ captures[`RETROSOC_TINY__TARGET_COUNT],
+ addresses[`RETROSOC_TINY__TARGET_COUNT],
+ writes[`RETROSOC_TINY__TARGET_COUNT];
+ logic [5:0] fault_valid, fault_instruction;
+ logic [ 5:0][31:0] fault_addr;
+ logic [ 5:0][ 1:0] fault_resp;
+ logic [ 2:0] owner_idle;
+ logic [`RETROSOC_TINY__TARGET_COUNT-1:0] target_idle;
+ logic [63:0] cpu_wait, dma_wait;
+ logic [2:0] arvalid = 0, awvalid = 0, wvalid = 0, rready = 0, bready = 0;
+ logic [2:0][31:0] araddr = 0, awaddr = 0, wdata = 0;
+ logic [2:0][7:0] arlen = 0, awlen = 0;
+ logic [2:0] wlast = 0;
+ wire [2:0] arready, awready, wready, rvalid, bvalid, rlast, rid, bid;
+ wire [2:0][1:0] rresp, bresp;
+ wire [2:0][31:0] rdata;
+ integer faults [6];
+ logic [5:0][31:0] last_fault_addr;
+ logic [5:0][ 1:0] last_fault_resp;
+ integer cases = 0;
+ logic dma_start = 0, dma_valid = 0, dma_start_ready, dma_ready, dma_busy, dma_done;
+ logic [31:0] dma_addr = 0, dma_data = 0;
+ logic [4:0] dma_beats = 1;
+ logic [1:0] dma_resp;
+ logic dma_id_error;
+
+ tiny_axi4_fabric u_dut (
+ .clk_i (clk_i),
+ .rst_n_i (rst_n_i),
+ .target_rst_n_i (target_rst_n),
+ .perf_enable_i (1'b0),
+ .perf_clear_i (1'b0),
+ .masters (masters),
+ .targets (targets),
+ .fault_valid_o (fault_valid),
+ .fault_addr_o (fault_addr),
+ .fault_resp_o (fault_resp),
+ .fault_instruction_o(fault_instruction),
+ .owner_idle_o (owner_idle),
+ .target_idle_o (target_idle),
+ .cpu_wait_o (cpu_wait),
+ .dma_wait_o (dma_wait)
+ );
+
+ // Production master: BREADY cannot rise until its last W handshake.
+ dma_axi4_master u_dma (
+ .clk_i (clk_i),
+ .rst_n_i (rst_n_i),
+ .clear_i (1'b0),
+ .read_start_valid_i (1'b0),
+ .read_start_ready_o (),
+ .read_addr_i (32'd0),
+ .read_beats_i (5'd1),
+ .read_fixed_i (1'b0),
+ .read_busy_o (),
+ .read_beat_valid_o (),
+ .read_beat_ready_i (1'b0),
+ .read_data_o (),
+ .read_resp_o (),
+ .read_last_o (),
+ .read_expected_last_o(),
+ .read_id_error_o (),
+ .read_done_o (),
+ .write_start_valid_i (dma_start),
+ .write_start_ready_o (dma_start_ready),
+ .write_addr_i (dma_addr),
+ .write_beats_i (dma_beats),
+ .write_fixed_i (1'b0),
+ .write_busy_o (dma_busy),
+ .write_data_valid_i (dma_valid),
+ .write_data_ready_o (dma_ready),
+ .write_data_i (dma_data),
+ .write_strb_i (4'hf),
+ .write_done_o (dma_done),
+ .write_resp_o (dma_resp),
+ .write_id_error_o (dma_id_error),
+ .axi4 (masters[1])
+ );
+
+ for (genvar m = 0; m < 3; m++) begin : gen_master
+ if (m != 1) begin : gen_manual
+ assign masters[m].araddr = araddr[m];
+ assign masters[m].arlen = arlen[m];
+ assign masters[m].arsize = 2;
+ assign masters[m].arburst = 1;
+ assign masters[m].arid = 1;
+ assign masters[m].arvalid = arvalid[m];
+ assign masters[m].arlock = 0;
+ assign masters[m].arprot = 0;
+ assign masters[m].arcache = 0;
+ assign masters[m].arqos = 0;
+ assign masters[m].arregion = 0;
+ assign masters[m].aruser = 0;
+ assign masters[m].awaddr = awaddr[m];
+ assign masters[m].awlen = awlen[m];
+ assign masters[m].awsize = 2;
+ assign masters[m].awburst = 1;
+ assign masters[m].awid = 1;
+ assign masters[m].awvalid = awvalid[m];
+ assign masters[m].awlock = 0;
+ assign masters[m].awprot = 0;
+ assign masters[m].awcache = 0;
+ assign masters[m].awqos = 0;
+ assign masters[m].awregion = 0;
+ assign masters[m].awuser = 0;
+ assign masters[m].wdata = wdata[m];
+ assign masters[m].wstrb = 15;
+ assign masters[m].wvalid = wvalid[m];
+ assign masters[m].wlast = wlast[m];
+ assign masters[m].wuser = 0;
+ assign masters[m].rready = rready[m];
+ assign masters[m].bready = bready[m];
+ end
+ assign arready[m] = masters[m].arready;
+ assign awready[m] = masters[m].awready;
+ assign wready[m] = masters[m].wready;
+ assign rvalid[m] = masters[m].rvalid;
+ assign bvalid[m] = masters[m].bvalid;
+ assign rlast[m] = masters[m].rlast;
+ assign rid[m] = masters[m].rid;
+ assign bid[m] = masters[m].bid;
+ assign rresp[m] = masters[m].rresp;
+ assign bresp[m] = masters[m].bresp;
+ assign rdata[m] = masters[m].rdata;
+
+ // Independent upstream scoreboards also run in Icarus without SVA.
+ integer rleft = 0, wleft = 0, bleft = 0;
+ logic rheld = 0, bheld = 0;
+ logic [36:0] saved_r;
+ logic [ 3:0] saved_b;
+ always @(posedge clk_i) begin
+ if (!rst_n_i) begin
+ rleft = 0;
+ wleft = 0;
+ bleft = 0;
+ rheld = 0;
+ bheld = 0;
+ end else begin
+ if (rheld && (!masters[m].rvalid ||
+ {masters[m].rid, masters[m].rdata, masters[m].rresp,
+ masters[m].rlast, masters[m].ruser} !== saved_r))
+ $fatal(1, "held R response changed");
+ if (bheld && (!masters[m].bvalid ||
+ {masters[m].bid, masters[m].bresp, masters[m].buser} !== saved_b))
+ $fatal(1, "held B response changed");
+ if (masters[m].arvalid && masters[m].arready) begin
+ if (rleft != 0) $fatal(1, "duplicate read admission");
+ rleft = integer'(masters[m].arlen) + 1;
+ end
+ if (masters[m].awvalid && masters[m].awready) begin
+ if (bleft != 0) $fatal(1, "duplicate write admission");
+ wleft = integer'(masters[m].awlen) + 1;
+ bleft = 1;
+ end
+ if (masters[m].bvalid && (bleft != 1 || wleft != 0))
+ $fatal(1, "B before complete W drain or duplicate B");
+ if (masters[m].wvalid && masters[m].wready) begin
+ if (wleft <= 0 || masters[m].wlast != (wleft == 1)) $fatal(1, "W count/last mismatch");
+ wleft--;
+ end
+ if (masters[m].rvalid) begin
+ if (rleft <= 0 || masters[m].rlast != (rleft == 1)) $fatal(1, "R count/last mismatch");
+ if (masters[m].rready) rleft--;
+ end
+ if (masters[m].bvalid && masters[m].bready) bleft--;
+ rheld = masters[m].rvalid && !masters[m].rready;
+ bheld = masters[m].bvalid && !masters[m].bready;
+ saved_r = {
+ masters[m].rid, masters[m].rdata, masters[m].rresp, masters[m].rlast, masters[m].ruser
+ };
+ saved_b = {masters[m].bid, masters[m].bresp, masters[m].buser};
+ end
+ end
+ end
+
+ for (genvar t = 0; t < `RETROSOC_TINY__TARGET_COUNT; t++) begin : gen_target
+ if (t == `RETROSOC_TINY__TARGET_XPI) begin : gen_apb
+ tiny_axi42apb4 u_bridge (
+ .clk_i (clk_i),
+ .rst_n_i(target_rst_n[t]),
+ .axi4 (targets[t]),
+ .apb4 (apb)
+ );
+ end else begin : gen_memory
+ tiny_r2_p5_reset_target_model u_model (
+ .clk_i (clk_i),
+ .rst_n_i (target_rst_n[t]),
+ .hold_address_i(hold_address[t]),
+ .hold_read_i (hold_read[t]),
+ .hold_write_i (hold_write[t]),
+ .hold_b_i (hold_b[t]),
+ .error_beat_i (error_beat[t]),
+ .axi4 (targets[t])
+ );
+ end
+ always @(posedge clk_i) begin
+ if (rst_n_i) begin
+ if (targets[t].arvalid && targets[t].arready || targets[t].awvalid && targets[t].awready)
+ addresses[t]++;
+ if (targets[t].wvalid && targets[t].wready) writes[t]++;
+ if (targets[t].rvalid && targets[t].rready) captures[t]++;
+ if (!target_rst_n[t] && (targets[t].arvalid || targets[t].awvalid ||
+ targets[t].wvalid || targets[t].rready || targets[t].bready))
+ $fatal(1, "downstream handshake during target reset");
+ end
+ end
+ end
+ assign apb.pready = apb.psel && apb.penable && !hold_read[`RETROSOC_TINY__TARGET_XPI] && !hold_b[`RETROSOC_TINY__TARGET_XPI];
+ assign apb.prdata = 32'hcafe_f00d;
+ assign apb.pslverr = 0;
+ always @(posedge clk_i) begin
+ if (rst_n_i) begin
+ for (integer e = 0; e < 6; e++) begin
+ if (fault_valid[e]) begin
+ faults[e]++;
+ last_fault_addr[e] = fault_addr[e];
+ last_fault_resp[e] = fault_resp[e];
+ end
+ end
+ end
+ end
+
+ task automatic quiet;
+ repeat (4) @(negedge clk_i);
+ if (owner_idle != 7 || target_idle != {`RETROSOC_TINY__TARGET_COUNT{1'b1}} || rvalid != 0 || bvalid != 0)
+ $fatal(1, "credit/response not retired");
+ endtask
+ task automatic receive_read(input int index, input int beats, input logic [1:0] expected,
+ input logic [31:0] data);
+ do @(posedge clk_i); while (!rvalid[0]);
+ @(negedge clk_i);
+ repeat (3) @(negedge clk_i);
+ if (rid[0] !== 1'b1 || rresp[0] !== expected || rdata[0] !== data ||
+ rlast[0] !== (index == beats - 1))
+ $fatal(
+ 1,
+ "read data/ID/response mismatch index=%0d expected=%0d got=%0d",
+ index,
+ expected,
+ rresp[0]
+ );
+ rready[0] = 1;
+ @(posedge clk_i);
+ @(negedge clk_i);
+ rready[0] = 0;
+ endtask
+ task automatic read_case(input logic [31:0] address, input int t, input int beats,
+ input bit buffered, input bit before_address, input bit inject_reset);
+ integer first_capture, prefix, before_faults, before_addresses;
+ logic [ 1:0] expected;
+ logic [31:0] data;
+ @(negedge clk_i);
+ first_capture = captures[t];
+ before_faults = faults[0];
+ before_addresses = addresses[t];
+ hold_address[t] = before_address;
+ hold_read[t] = !buffered && inject_reset;
+ araddr[0] = address;
+ arlen[0] = 8'(beats - 1);
+ arvalid[0] = 1;
+ do @(posedge clk_i); while (!arready[0]);
+ @(negedge clk_i);
+ arvalid[0] = 0;
+ if (buffered) begin
+ wait (rvalid[0]);
+ repeat (5) @(negedge clk_i);
+ end else if (!before_address) begin
+ wait (addresses[t] > before_addresses);
+ @(negedge clk_i);
+ end
+ prefix = captures[t] - first_capture;
+ if (inject_reset) begin
+ target_rst_n[t] = 0;
+ before_addresses = addresses[t];
+ repeat (3) @(negedge clk_i);
+ // Release the downstream reset while the upstream is still stalled.
+ // The cancelled session must never be issued again after release.
+ if (t != 0) target_rst_n[t] = 1;
+ hold_address[t] = 0;
+ hold_read[t] = 0;
+ repeat (4) @(negedge clk_i);
+ if (addresses[t] != before_addresses) $fatal(1, "cancelled AR replayed");
+ end
+ for (integer n = 0; n < beats; n++) begin
+ expected = inject_reset && n >= prefix ? 3 : (n == error_beat[t] ? 2 : 0);
+ data = expected == 3 ? 0 : (t == `RETROSOC_TINY__TARGET_XPI ? 32'hcafe_f00d : (address + 32'(4 * n)) ^ 32'h10000000);
+ receive_read(n, beats, expected, data);
+ end
+ target_rst_n[t] = 1;
+ quiet();
+ if (inject_reset && (prefix < beats || (error_beat[t] >= 0 && error_beat[t] < prefix))) begin
+ expected = (error_beat[t] >= 0 && error_beat[t] < prefix) ? 2 : 3;
+ if (faults[0] != before_faults + 1 || last_fault_resp[0] != expected ||
+ last_fault_addr[0] != address + 32'(4 * (expected == 2 ? error_beat[t] : prefix)))
+ $fatal(1, "first failing read beat metadata/count lost");
+ end else if (faults[0] != before_faults) $fatal(1, "spurious read fault");
+ cases++;
+ endtask
+ task automatic send_word(input int n, input int beats);
+ @(negedge clk_i);
+ wdata[2] = 32'ha5000000 + 32'(n);
+ wlast[2] = n == beats - 1;
+ wvalid[2] = 1;
+ do @(posedge clk_i); while (!wready[2]);
+ @(negedge clk_i);
+ wvalid[2] = 0;
+ endtask
+ task automatic write_case(input logic [31:0] address, input int t, input int beats,
+ input int prefix, input bit buffered, input bit before_address);
+ integer before_faults, after_writes;
+ @(negedge clk_i);
+ before_faults = faults[5];
+ hold_address[t] = before_address;
+ hold_b[t] = !buffered;
+ awaddr[2] = address;
+ awlen[2] = 8'(beats - 1);
+ awvalid[2] = 1;
+ do @(posedge clk_i); while (!awready[2]);
+ @(negedge clk_i);
+ awvalid[2] = 0;
+ for (integer n = 0; n < prefix; n++) send_word(n, beats);
+ if (buffered) begin
+ wait (bvalid[2]);
+ repeat (3) @(negedge clk_i);
+ end
+ target_rst_n[t] = 0;
+ after_writes = writes[t];
+ repeat (3) @(negedge clk_i);
+ if (t != 0) target_rst_n[t] = 1;
+ hold_address[t] = 0;
+ hold_b[t] = 0;
+ for (integer n = prefix; n < beats; n++) begin
+ if (bvalid[2]) $fatal(1, "write response before remaining data");
+ send_word(n, beats);
+ end
+ do @(posedge clk_i); while (!bvalid[2]);
+ @(negedge clk_i);
+ repeat (5) @(negedge clk_i);
+ if (bresp[2] != (buffered ? 0 : 3) || bid[2] !== 1'b1 || writes[t] != after_writes)
+ $fatal(1, "write response/ID or downstream session replay");
+ bready[2] = 1;
+ @(posedge clk_i);
+ @(negedge clk_i);
+ bready[2] = 0;
+ target_rst_n[t] = 1;
+ quiet();
+ if (faults[5] != before_faults + (buffered ? 0 : 1)) $fatal(1, "write fault count mismatch");
+ cases++;
+ endtask
+ task automatic dma_case(input logic [31:0] address, input int t, input int beats,
+ input int prefix);
+ integer before_faults, after_writes;
+ @(negedge clk_i);
+ before_faults = faults[3];
+ hold_b[t] = 1;
+ dma_addr = address;
+ dma_beats = 5'(beats);
+ dma_start = 1;
+ do @(posedge clk_i); while (!dma_start_ready);
+ @(negedge clk_i);
+ dma_start = 0;
+ wait (!owner_idle[1]);
+ @(negedge clk_i);
+ for (integer n = 0; n < beats; n++) begin
+ if (n == prefix) begin
+ target_rst_n[t] = 0;
+ after_writes = writes[t];
+ repeat (3) @(negedge clk_i);
+ target_rst_n[t] = 1;
+ hold_b[t] = 0;
+ end
+ dma_data = 32'h5a000000 + 32'(n);
+ dma_valid = 1;
+ do @(posedge clk_i); while (!dma_ready);
+ @(negedge clk_i);
+ dma_valid = 0;
+ end
+ do @(posedge clk_i); while (!dma_done);
+ if (dma_resp != 3 || dma_id_error) $fatal(1, "production DMA did not terminate with error");
+ @(negedge clk_i);
+ quiet();
+ if (dma_busy || writes[t] != after_writes || faults[3] != before_faults + 1)
+ $fatal(1, "production DMA write drain/credit/count failure");
+ cases++;
+ endtask
+
+ initial begin
+ for (integer t = 0; t < `RETROSOC_TINY__TARGET_COUNT; t++) begin
+ target_rst_n[t] = 0;
+ captures[t] = 0;
+ addresses[t] = 0;
+ writes[t] = 0;
+ error_beat[t] = -1;
+ end
+ for (integer e = 0; e < 6; e++) faults[e] = 0;
+ repeat (4) @(negedge clk_i);
+ rst_n_i = 1;
+ for (integer t = 0; t < `RETROSOC_TINY__TARGET_COUNT; t++) target_rst_n[t] = 1;
+ // SRAM and XPI: address cancellation, missing 16 beats, two captured
+ // beats under backpressure, and already captured successful completion.
+ for (integer t = 0; t < 5; t += 4) begin
+ read_case(t == 0 ? 32'h30000000 : 32'h50000000, t, 16, 0, 1, 1);
+ read_case(t == 0 ? 32'h30000000 : 32'h50000000, t, 16, 0, 0, 1);
+ read_case(t == 0 ? 32'h30000000 : 32'h50000000, t, 16, 1, 0, 1);
+ read_case(t == 0 ? 32'h30000000 : 32'h50000000, t, 1, 1, 0, 1);
+ read_case(t == 0 ? 32'h30000000 : 32'h50000000, t, 1, 1, 0, 0);
+ write_case(t == 0 ? 32'h30000000 : 32'h50000000, t, 16, 0, 0, 1);
+ write_case(t == 0 ? 32'h30000000 : 32'h50000000, t, 16, 2, 0, 0);
+ write_case(t == 0 ? 32'h30000000 : 32'h50000000, t, 16, 16, 0, 0);
+ write_case(t == 0 ? 32'h30000000 : 32'h50000000, t, 1, 1, 1, 0);
+ dma_case(t == 0 ? 32'h30000000 : 32'h50000000, t, 16, 2);
+ end
+ error_beat[0] = 1;
+ read_case(32'h30000000, 0, 16, 1, 0, 1);
+ error_beat[0] = -1;
+ // Actual APB bridge, with a withheld peripheral completion.
+ read_case(32'h10009000, `RETROSOC_TINY__TARGET_XPI, 1, 0, 1, 1);
+ read_case(32'h10009000, `RETROSOC_TINY__TARGET_XPI, 1, 0, 0, 1);
+ read_case(32'h10009000, `RETROSOC_TINY__TARGET_XPI, 1, 1, 0, 1);
+ write_case(32'h10009000, `RETROSOC_TINY__TARGET_XPI, 1, 0, 0, 1);
+ write_case(32'h10009000, `RETROSOC_TINY__TARGET_XPI, 1, 1, 0, 0);
+ write_case(32'h10009000, `RETROSOC_TINY__TARGET_XPI, 1, 1, 1, 0);
+ dma_case(32'h10009000, `RETROSOC_TINY__TARGET_XPI, 1, 0);
+ $display("P5_RESET cases=%0d production_dma=3 burst_beats=16 held_RB=1 first_fault=1", cases);
+ $display("SIM_TEST_PASS Tiny P5 target reset isolation");
+ $finish;
+ end
+ initial begin
+ repeat (20000) @(posedge clk_i);
+ $fatal(1, "SIM_TEST_TIMEOUT Tiny P5 target reset isolation cases=%0d", cases);
+ end
+endmodule
+
+module tiny_r2_p5_reset_target_model (
+ input logic clk_i,
+ rst_n_i,
+ hold_address_i,
+ hold_read_i,
+ hold_write_i,
+ hold_b_i,
+ input integer error_beat_i,
+ axi4_if.slave axi4
+);
+ logic active = 0, writing = 0, write_done = 0, id = 0;
+ logic [31:0] address = 0;
+ logic [7:0] length = 0, beat = 0;
+ assign axi4.arready = !active && !hold_address_i && rst_n_i;
+ assign axi4.awready = !active && !hold_address_i && !axi4.arvalid && rst_n_i;
+ assign axi4.wready = active && writing && !write_done && !hold_write_i && rst_n_i;
+ assign axi4.ruser = 0;
+ assign axi4.buser = 0;
+ always @(posedge clk_i or negedge rst_n_i) begin
+ if (!rst_n_i) begin
+ active <= 0;
+ writing <= 0;
+ write_done <= 0;
+ axi4.rvalid <= 0;
+ axi4.bvalid <= 0;
+ axi4.rdata <= 0;
+ axi4.rresp <= 0;
+ axi4.rlast <= 0;
+ axi4.rid <= 0;
+ axi4.bid <= 0;
+ axi4.bresp <= 0;
+ end else begin
+ if (axi4.arvalid && axi4.arready) begin
+ active <= 1;
+ writing <= 0;
+ address <= axi4.araddr;
+ length <= axi4.arlen;
+ beat <= 0;
+ id <= axi4.arid;
+ end
+ if (axi4.awvalid && axi4.awready) begin
+ active <= 1;
+ writing <= 1;
+ write_done <= 0;
+ length <= axi4.awlen;
+ beat <= 0;
+ id <= axi4.awid;
+ end
+ if (active && !writing && !axi4.rvalid && !hold_read_i) begin
+ axi4.rvalid <= 1;
+ axi4.rid <= id;
+ axi4.rdata <= 32'h10000000 ^ address;
+ axi4.rresp <= integer'(beat) == error_beat_i ? 2 : 0;
+ axi4.rlast <= beat == length;
+ end
+ if (axi4.rvalid && axi4.rready) begin
+ axi4.rvalid <= 0;
+ if (axi4.rlast) active <= 0;
+ else begin
+ beat <= beat + 1;
+ address <= address + 4;
+ end
+ end
+ if (axi4.wvalid && axi4.wready) begin
+ if (beat == length) write_done <= 1;
+ else beat <= beat + 1;
+ end
+ if (active && writing && write_done && !axi4.bvalid && !hold_b_i) begin
+ axi4.bvalid <= 1;
+ axi4.bid <= id;
+ axi4.bresp <= 0;
+ end
+ if (axi4.bvalid && axi4.bready) begin
+ axi4.bvalid <= 0;
+ active <= 0;
+ write_done <= 0;
+ end
+ end
+ end
+endmodule
diff --git a/tests/rtl/tiny_r2_p5_system_tb.sv b/tests/rtl/tiny_r2_p5_system_tb.sv
new file mode 100644
index 00000000..76fb24ad
--- /dev/null
+++ b/tests/rtl/tiny_r2_p5_system_tb.sv
@@ -0,0 +1,192 @@
+`include "tiny_topology.svh"
+`timescale 1ns / 1ps
+module tiny_r2_p5_system_tb;
+ logic clk_i = 0, rst_n_i = 0;
+ always #20.833 clk_i = ~clk_i;
+ wire s_rst_n = rst_n_i;
+ logic s_jtag_tck_driver = 0, s_jtag_tms_driver = 1, s_jtag_tdi_driver = 0;
+ wire s_jtag_tdo;
+ logic done, pass;
+ logic [7:0] code;
+ gpio_if gpio ();
+ xpi_if xpi ();
+ integer overlap_cycles = 0, progress = 0, controls = 0, controls_before = 0;
+ logic hart_reset_observed = 0;
+ logic [31:0] status;
+ bit ignored;
+ `include "tiny_jtag_smoke.svh"
+retrosoc_tiny u_dut (
+ .clk_i (clk_i),
+ .rst_n_i (rst_n_i),
+ .jtag_tck_i (s_jtag_tck_driver),
+ .jtag_tms_i (s_jtag_tms_driver),
+ .jtag_tdi_i (s_jtag_tdi_driver),
+ .jtag_trst_n_i(rst_n_i),
+ .jtag_tdo_o (s_jtag_tdo),
+ .uart0_rx_i (1'b1),
+ .uart1_rx_i (1'b1),
+ .gpio (gpio),
+ .xpi (xpi),
+ .test_done_o (done),
+ .test_pass_o (pass),
+ .test_code_o (code)
+ );
+ assign gpio.di_i = 0;
+ assign xpi.io_di_i = 0;
+ always @(posedge clk_i) begin
+ if (rst_n_i) begin
+ if(u_dut.u_masters_axi4_if[1].rvalid && u_dut.u_masters_axi4_if[1].rready &&
+ u_dut.u_masters_axi4_if[1].wvalid && u_dut.u_masters_axi4_if[1].wready)
+ overlap_cycles++;
+ if((u_dut.u_fabric.s_busy_q[2] || u_dut.u_fabric.s_busy_q[3]) &&
+ (!u_dut.u_fabric.target_idle_o[4] || !u_dut.u_fabric.target_idle_o[`RETROSOC_TINY__TARGET_XPI]) &&
+ u_dut.u_sysctrl_apb4_if.psel && u_dut.u_sysctrl_apb4_if.penable && u_dut.u_sysctrl_apb4_if.pready)
+ controls++;
+ if(u_dut.u_cpu_local_if[1].req_valid && u_dut.u_cpu_local_if[1].req_ready &&
+ u_dut.u_cpu_local_if[1].write && u_dut.u_cpu_local_if[1].addr==32'h3001ffe0) begin
+ progress = integer'(u_dut.u_cpu_local_if[1].wdata);
+ if (progress == 2) u_dut.u_xpi.hold_mapped = 1;
+ if (progress == 3) begin
+ if (!u_dut.u_fabric.s_busy_q[2]) $fatal(1, "abort discarded accepted read");
+ u_dut.u_xpi.hold_mapped = 0;
+ end
+ end
+ end
+ end
+ initial begin
+ repeat (6) jtag_tick(1, 0, ignored);
+ @(negedge clk_i);
+ rst_n_i = 1;
+ repeat (12) jtag_tick(1, 0, ignored);
+ jtag_idle(1);
+ jtag_ir(5'h11);
+ wait (progress == 1);
+ $display("P5_SYSTEM_STAGE independent-progress");
+ if (controls == 0 || overlap_cycles == 0 || !u_dut.u_fabric.s_busy_q[2])
+ $fatal(1, "CPU/control progress or real DMA overlap absent");
+ // Move the held DMA work to an independent APB branch before resetting
+ // the hart. CPU debug entry may fetch the reset/exception ROM aperture;
+ // accepted CPU work must be allowed to drain rather than discarded.
+ @(negedge clk_i);
+ u_dut.u_xpi.hold_mapped = 0;
+ wait (u_dut.u_xpi_apb4_if.psel && u_dut.u_xpi_apb4_if.penable && !u_dut.u_xpi_apb4_if.pready);
+ controls_before = controls;
+ repeat (64) @(posedge clk_i);
+ if (controls == controls_before) $fatal(1, "APB FIFO wait blocked independent control");
+ jtag_dmi(1, 7'h10, 1, status);
+ jtag_dmi(1, 7'h10, 3, status);
+ $display("P5_SYSTEM_STAGE reset-request pending=%b", u_dut.u_cpu.s_reset_pending);
+ wait (!u_dut.s_core_rst_n);
+ if (!u_dut.u_fabric.s_busy_q[3] || u_dut.u_fabric.target_idle_o[`RETROSOC_TINY__TARGET_XPI])
+ $fatal(1, "hart reset cancelled unrelated DMA");
+ hart_reset_observed = 1;
+ $display("P5_SYSTEM_STAGE hart-reset");
+ jtag_dmi(1, 7'h10, 1, status);
+ wait (u_dut.s_core_rst_n);
+ @(negedge clk_i);
+ u_dut.u_xpi.hold_apb = 0;
+ $display("P5_SYSTEM_STAGE released");
+ wait (done);
+ if (!pass || !hart_reset_observed || progress != 3)
+ $fatal(1, "SIM_TEST_FAIL P5 CPU/DMA code=%0d", code);
+ repeat (20) @(posedge clk_i);
+ $display(
+ "P5_SYSTEM dma_overlap_cycles=%0d independent_controls=%0d hart_reset=1 abort_drained=1",
+ overlap_cycles, controls);
+ $display("SIM_TEST_PASS Tiny P5 system");
+ $finish;
+ end
+ initial begin
+ repeat (500000) @(posedge clk_i);
+ $display(
+ "P5_DIAG core_rst=%b pending=%b halted=%b idle=%b quiesce=%b hold=%b busy=%b I=%h D=%h",
+ u_dut.s_core_rst_n, u_dut.u_cpu.s_reset_pending, u_dut.s_debug_halted,
+ u_dut.s_cpu_bus_idle, u_dut.s_cpu_quiesce, u_dut.u_xpi.hold_mapped,
+ u_dut.u_fabric.s_busy_q, u_dut.u_cpu_ahbl_if[0].haddr, u_dut.u_cpu_ahbl_if[1].haddr);
+ $fatal(1, "SIM_TEST_TIMEOUT Tiny P5 system progress=%0d", progress);
+ end
+endmodule
+
+// Verification-only XPI boundary substitute. The production Tiny integration,
+// CPU, DMA, SRAM, APB branches and SYSCTRL remain real. This does not qualify an
+// XPI device or replace the pin-level NOR regression. The small reset ROM is
+// only a fixture boot source, never a product Boot ROM.
+module apb4_xpi (
+ output logic lifecycle_idle_o,
+ input logic clk_i,
+ rst_n_i,
+ dma_xfer_done_i,
+ output logic dma_tx_stall_o,
+ dma_rx_stall_o,
+ irq_o,
+ apb4_if.slave apb4,
+ axi4_if.slave mem_axi4,
+ xpi_if.dut xpi
+);
+ logic [7:0] rom [0:4095];
+ string firmware;
+ logic active = 0, id = 0, hold_mapped = 1, hold_apb = 1;
+ logic [31:0] captured_write = 0;
+ logic [31:0] address = 0;
+ logic [7:0] beat = 0, length = 0;
+ assign lifecycle_idle_o = !active && !(apb4.psel && apb4.penable);
+ initial begin
+ if (!$value$plusargs("firmware=%s", firmware)) $fatal(1, "firmware required");
+ $readmemh(firmware, rom);
+ end
+ assign dma_tx_stall_o = 0;
+ assign dma_rx_stall_o = 0;
+ assign irq_o = 0;
+ assign xpi.sck_o = 0;
+ assign xpi.nss_o = '1;
+ assign xpi.io_oe_o = 0;
+ assign xpi.io_do_o = 0;
+ assign apb4.pready=apb4.psel && apb4.penable &&
+ !(apb4.pwrite && apb4.paddr[11:0]==12'h040 && hold_apb);
+ assign apb4.pslverr = 0;
+ assign apb4.prdata = (apb4.paddr[11:0] == 12'h044) ? captured_write : 32'd0;
+ assign mem_axi4.arready = !active;
+ assign mem_axi4.awready = 0;
+ assign mem_axi4.wready = 0;
+ assign mem_axi4.bvalid = 0;
+ assign mem_axi4.bid = 0;
+ assign mem_axi4.bresp = 0;
+ assign mem_axi4.buser = 0;
+ assign mem_axi4.rresp = 0;
+ assign mem_axi4.ruser = 0;
+ always @(posedge clk_i) begin
+ if (rst_n_i && apb4.psel && apb4.penable && apb4.pready && apb4.pwrite)
+ captured_write <= apb4.pwdata;
+ if (!rst_n_i) begin
+ active <= 0;
+ mem_axi4.rvalid <= 0;
+ mem_axi4.rdata <= 0;
+ mem_axi4.rid <= 0;
+ mem_axi4.rlast <= 0;
+ end else begin
+ if (mem_axi4.arvalid && mem_axi4.arready) begin
+ active <= 1;
+ address <= mem_axi4.araddr;
+ id <= mem_axi4.arid;
+ beat <= 0;
+ length <= mem_axi4.arlen;
+ end
+ if (active && !mem_axi4.rvalid && !(hold_mapped && address >= 32'h50000000)) begin
+ mem_axi4.rvalid <= 1;
+ mem_axi4.rid <= id;
+ mem_axi4.rlast <= beat == length;
+ if (address < 4096)
+ mem_axi4.rdata <= {rom[address+3], rom[address+2], rom[address+1], rom[address]};
+ else mem_axi4.rdata <= 32'h10000000 ^ address;
+ end
+ if (mem_axi4.rvalid && mem_axi4.rready) begin
+ mem_axi4.rvalid <= 0;
+ if (mem_axi4.rlast) active <= 0;
+ else begin
+ address <= address + 4;
+ beat <= beat + 1;
+ end
+ end
+ end
+ end
+endmodule
diff --git a/tests/rtl/tiny_r2_p6_bridge_tb.sv b/tests/rtl/tiny_r2_p6_bridge_tb.sv
new file mode 100644
index 00000000..8797d53a
--- /dev/null
+++ b/tests/rtl/tiny_r2_p6_bridge_tb.sv
@@ -0,0 +1,87 @@
+`timescale 1ns / 1ps
+module tiny_r2_p6_bridge_tb;
+ logic clk_i = 0, rst_n_i = 0;
+ always #5 clk_i = !clk_i;
+ axi4_if #(
+ .ADDR_WIDTH(32),
+ .DATA_WIDTH(32),
+ .ID_WIDTH (1),
+ .USER_WIDTH(1)
+ ) axi (
+ .aclk (clk_i),
+ .aresetn(rst_n_i)
+ );
+ apb4_if apb (
+ .pclk (clk_i),
+ .presetn(rst_n_i)
+ );
+ tiny_axi42apb4 u_dut (
+ .clk_i (clk_i),
+ .rst_n_i(rst_n_i),
+ .axi4 (axi),
+ .apb4 (apb)
+ );
+ assign apb.pready = apb.psel && apb.penable;
+ assign apb.prdata = 32'h13579bdf;
+ // A strict APB4 receiver rejects nonzero read strobes independently of PWRITE.
+ assign apb.pslverr = apb.psel && apb.penable && !apb.pwrite && apb.pstrb != 0;
+ task automatic write_read(input logic [3:0] mask);
+ @(negedge clk_i);
+ axi.awvalid = 1;
+ do @(posedge clk_i); while (!axi.awready);
+ @(negedge clk_i);
+ axi.awvalid = 0;
+ axi.wvalid = 1;
+ axi.wstrb = mask;
+ do @(posedge clk_i); while (!axi.wready);
+ @(negedge clk_i);
+ axi.wvalid = 0;
+ do @(posedge clk_i); while (!apb.penable);
+ if (!apb.pwrite || apb.pstrb !== mask) $fatal(1, "SIM_TEST_FAIL lost write byte lanes");
+ do @(posedge clk_i); while (!axi.bvalid);
+ if (axi.bresp != 0) $fatal(1, "SIM_TEST_FAIL APB write response");
+ @(negedge clk_i);
+ axi.arvalid = 1;
+ do @(posedge clk_i); while (!axi.arready);
+ @(negedge clk_i);
+ axi.arvalid = 0;
+ do @(posedge clk_i); while (!axi.rvalid);
+ if (axi.rresp != 0 || axi.rdata !== 32'h13579bdf || !axi.rlast)
+ $fatal(1, "SIM_TEST_FAIL stale write strobes corrupted APB read");
+ @(negedge clk_i);
+ endtask
+ initial begin
+ axi.awvalid = 0;
+ axi.awaddr = 'h10007000;
+ axi.awlen = 0;
+ axi.awsize = 2;
+ axi.awburst = 1;
+ axi.awlock = 0;
+ axi.awid = 0;
+ axi.wvalid = 0;
+ axi.wdata = 'h12345678;
+ axi.wstrb = 0;
+ axi.wlast = 1;
+ axi.bready = 1;
+ axi.arvalid = 0;
+ axi.araddr = 'h10007008;
+ axi.arlen = 0;
+ axi.arsize = 2;
+ axi.arburst = 1;
+ axi.arlock = 0;
+ axi.arid = 0;
+ axi.rready = 1;
+ repeat (4) @(negedge clk_i);
+ rst_n_i = 1;
+ write_read(15);
+ write_read(1);
+ write_read(3);
+ write_read(10);
+ $display("SIM_TEST_PASS Tiny P6 APB read strobes");
+ $finish;
+ end
+ initial begin
+ #100000;
+ $fatal(1, "SIM_TEST_TIMEOUT P6 APB strobes");
+ end
+endmodule
diff --git a/tests/rtl/tiny_r2_p6_shared_tb.sv b/tests/rtl/tiny_r2_p6_shared_tb.sv
new file mode 100644
index 00000000..4fbbf809
--- /dev/null
+++ b/tests/rtl/tiny_r2_p6_shared_tb.sv
@@ -0,0 +1,153 @@
+`timescale 1ns / 1ps
+module tiny_r2_p6_shared_tb;
+ logic clk_i = 0, clk_aud_i = 0, rst_n_i = 0, rst_aud_n_i = 0;
+ always #20.833333 clk_i = !clk_i;
+ always #27.126736111 clk_aud_i = !clk_aud_i;
+ apb4_if apb4 (
+ .pclk (clk_i),
+ .presetn(rst_n_i)
+ );
+ axi4_stream_if #(
+ .DATA_WIDTH(32)
+ ) tx (
+ .aclk (clk_i),
+ .aresetn(rst_n_i)
+ );
+ axi4_stream_if #(
+ .DATA_WIDTH(32)
+ ) rx (
+ .aclk (clk_i),
+ .aresetn(rst_n_i)
+ );
+ i2s_if i2s ();
+ logic streaming = 0, wide = 0;
+ integer sent = 0, received = 0, matched = 0, examined = 0;
+ logic found = 0;
+ logic [31:0] value;
+ assign i2s.adcdat_i = i2s.dacdat_o;
+ assign tx.tvalid = streaming;
+ assign tx.tdata = wide ? 32'h120000 + 32'(sent) :
+ {16'(16'h2001 + 2*sent), 16'(16'h2000 + 2*sent)};
+ assign tx.tstrb = 4'hf;
+ assign tx.tkeep = 4'hf;
+ assign tx.tlast = 0;
+ assign tx.tuser = 0;
+ assign tx.tid = 0;
+ assign tx.tdest = 0;
+ assign rx.tready = 1;
+ apb4_i2s u_dut (
+ .clk_i (clk_i),
+ .rst_n_i (rst_n_i),
+ .clk_aud_i (clk_aud_i),
+ .rst_aud_n_i (rst_aud_n_i),
+ .dma_tx_stall_o (),
+ .dma_rx_stall_o (),
+ .tx_underrun_evt_o(),
+ .rx_overrun_evt_o (),
+ .rx_flush_busy_o (),
+ .apb4 (apb4),
+ .tx_axis (tx),
+ .rx_axis (rx),
+ .i2s (i2s)
+ );
+ task automatic write(input logic [31:0] address, input logic [31:0] data);
+ @(negedge clk_i);
+ apb4.paddr = address;
+ apb4.pwdata = data;
+ apb4.pstrb = 15;
+ apb4.pwrite = 1;
+ apb4.psel = 1;
+ apb4.penable = 0;
+ @(negedge clk_i);
+ apb4.penable = 1;
+ do @(posedge clk_i); while (!apb4.pready);
+ if (apb4.pslverr) $fatal(1, "SIM_TEST_FAIL P6 I2S APB write %h", address);
+ @(negedge clk_i);
+ apb4.psel = 0;
+ apb4.penable = 0;
+ endtask
+ task automatic sample (input logic [31:0] observed);
+ logic [31:0] first;
+ first = wide ? 32'h120020 : 32'h2020;
+ if (!found && observed == first) found = 1;
+ if (found && matched < 512) begin
+ if (observed !== first + 32'(matched))
+ $fatal(
+ 1,
+ "SIM_TEST_FAIL P6 I2S sample bits=%0d index=%0d got=%h expected=%h",
+ wide ? 24 : 16,
+ matched,
+ observed,
+ first + 32'(matched)
+ );
+ matched++;
+ end
+ examined++;
+ if (!found && examined > 40)
+ $fatal(
+ 1,
+ "SIM_TEST_FAIL P6 I2S startup/payload missing bits=%0d last=%h",
+ wide ? 24 : 16,
+ observed
+ );
+ endtask
+ always @(posedge clk_i) begin
+ if (rst_n_i) begin
+ if (tx.tvalid && tx.tready) sent <= sent + 1;
+ if (rx.tvalid && rx.tready) begin
+ if (rx.tkeep !== 4'hf || rx.tstrb !== 4'hf)
+ $fatal(1, "SIM_TEST_FAIL P6 I2S receive qualifiers");
+ if (wide) sample (rx.tdata);
+ else begin
+ sample ({16'd0, rx.tdata[15:0]});
+ sample ({16'd0, rx.tdata[31:16]});
+ end
+ received++;
+ end
+ end
+ end
+ initial begin
+ apb4.psel = 0;
+ apb4.penable = 0;
+ apb4.pwrite = 0;
+ apb4.paddr = 0;
+ apb4.pwdata = 0;
+ apb4.pstrb = 0;
+ apb4.pprot = 0;
+ for (integer mode = 0; mode < 2; mode++) begin
+ @(negedge clk_i);
+ rst_n_i = 0;
+ rst_aud_n_i = 0;
+ streaming = 0;
+ sent = 0;
+ received = 0;
+ matched = 0;
+ examined = 0;
+ found = 0;
+ wide = mode != 0;
+ repeat (8) @(negedge clk_i);
+ rst_n_i = 1;
+ repeat (8) @(negedge clk_aud_i);
+ rst_aud_n_i = 1;
+ repeat (32) @(negedge clk_i);
+ write(4, 3);
+ repeat (256) @(negedge clk_i);
+ if (u_dut.s_tx_flush_busy || u_dut.s_rx_flush_busy)
+ $fatal(1, "SIM_TEST_FAIL P6 initial disabled audio flush");
+ write('h10, wide ? 2 : 0);
+ write('h18, 'h00200060);
+ write('hc, 3);
+ streaming = 1;
+ wait (sent >= 96);
+ write(0, 7);
+ wait (matched == 512);
+ $display("P6_I2S bits=%0d samples=%0d", wide ? 24 : 16, matched);
+ end
+ $display("SIM_TEST_PASS Tiny P6 I2S payload");
+ $finish;
+ end
+ initial begin
+ #30000000;
+ $fatal(1, "SIM_TEST_TIMEOUT P6 I2S payload");
+ end
+endmodule
diff --git a/tests/rtl/tiny_sysctrl_tb.sv b/tests/rtl/tiny_sysctrl_tb.sv
index 39433bdd..e43b5b1b 100644
--- a/tests/rtl/tiny_sysctrl_tb.sv
+++ b/tests/rtl/tiny_sysctrl_tb.sv
@@ -10,6 +10,8 @@ module tiny_sysctrl_tb;
.presetn(rst_n_i)
);
logic fault_valid = 0;
+ logic [ 3:0] fault_count = 1;
+ logic [ 1:0] fault_master = 1;
logic [31:0] fault_addr = 32'h38000000;
logic [63:0] cpu_wait = 64'h123456789abcdef0;
logic [63:0] dma_wait = 64'h1122334455667788;
@@ -17,29 +19,166 @@ module tiny_sysctrl_tb;
logic perf_enable, perf_clear, done, pass;
logic [ 7:0] code;
logic [31:0] value;
+ logic [31:0] xin_hz, ref_hz, sys_hz, mem_hz, pclk_hz, clint_hz;
+ logic [1:0] rcu_current;
+ logic rcu_busy;
+ logic clock_apply;
+ logic [1:0] clock_profile;
+ logic clock_done = 0;
+ logic [1:0] clock_current = 0;
+ logic clock_apply_seen = 0;
+ logic [4:0] rcu_fault_event = 0;
+ logic warm_reset = 0, rcu_irq;
+ always @(posedge clk_i) if (clock_apply) clock_apply_seen <= 1;
tiny_sysctrl u_dut (
- .clk_i (clk_i),
- .rst_n_i (rst_n_i),
- .apb4 (apb4),
- .fault_valid_i (fault_valid),
- .fault_addr_i (fault_addr),
- .fault_write_i (1'b1),
- .fault_master_i(1'b1),
- .fault_resp_i (2'b11),
- .cpu_wait_i (cpu_wait),
- .dma_wait_i (dma_wait),
- .rtc_wake_i (wake),
- .perf_enable_o (perf_enable),
- .perf_clear_o (perf_clear),
- .test_done_o (done),
- .test_pass_o (pass),
- .test_code_o (code)
+ .clk_i (clk_i),
+ .rst_n_i (rst_n_i),
+ .clk_ref_i (clk_i),
+ .rst_ref_n_i (rst_n_i),
+ .rcu_link_ready_o(),
+ .apb4 (apb4),
+ .fault_valid_i (fault_valid),
+ .fault_count_i (fault_count),
+ .fault_addr_i (fault_addr),
+ .fault_write_i (1'b1),
+ .fault_master_i (fault_master),
+ .fault_resp_i (2'b11),
+ .cpu_wait_i (cpu_wait),
+ .dma_wait_i (dma_wait),
+ .rtc_wake_i (wake),
+ .clock_done_i (clock_done),
+ .clock_fault_i (rcu_fault_event),
+ .clock_current_i (clock_current),
+ .periph_done_i (1'b0),
+ .periph_fault_i (5'd0),
+ .gate_actual_i (14'd0),
+ .reset_done_i (14'd0),
+ .periph_apply_o (),
+ .periph_reset_o (),
+ .periph_request_o(),
+ .clkout_control_o(),
+ .clkout_idle_i (1'b1),
+ .pll_locked_i (1'b0),
+ .pll_selected_i (1'b0),
+ .warm_reset_i (warm_reset),
+ .watchdog_reset_i(1'b0),
+ .hart_reset_i (1'b0),
+ .system_reset_o (),
+ .rcu_irq_o (rcu_irq),
+ .clock_timeout_o (),
+ .perf_enable_o (perf_enable),
+ .perf_clear_o (perf_clear),
+ .test_done_o (done),
+ .test_pass_o (pass),
+ .test_code_o (code),
+ .xin_hz_o (xin_hz),
+ .ref_hz_o (ref_hz),
+ .sys_hz_o (sys_hz),
+ .mem_hz_o (mem_hz),
+ .pclk_hz_o (pclk_hz),
+ .clint_hz_o (clint_hz),
+ .rcu_current_o (rcu_current),
+ .rcu_busy_o (rcu_busy),
+ .clock_apply_o (clock_apply),
+ .clock_profile_o (clock_profile)
);
`include "apb4_bfm.svh"
initial begin
apb4_idle();
repeat (5) @(negedge clk_i);
rst_n_i = 1;
+ repeat (40) @(negedge clk_i);
+ apb4_read(32'h100, 0, value);
+ if (value != 32'h54524355) $fatal(1, "RCU identity mismatch");
+ apb4_read(32'h104, 0, value);
+ if (value != 32'h00010000) $fatal(1, "RCU version mismatch");
+ apb4_read(32'h108, 0, value);
+ if (value != 32'h00000203) $fatal(1, "RCU capability mismatch");
+ apb4_read(32'h158, 0, value);
+ if (value != 0) $fatal(1, "unsupported target capability was advertised");
+ apb4_read(32'h148, 0, value);
+ if (value != 24_000_000 || sys_hz != 24_000_000 || mem_hz != 24_000_000 ||
+ pclk_hz != 24_000_000 || clint_hz != 1_000_000)
+ $fatal(1, "SAFE24 rates mismatch");
+ apb4_write(32'h10c, 2, 4'hf, 0);
+ apb4_write(32'h114, 1, 4'hf, 1);
+ apb4_read(32'h134, 0, value);
+ if (!value[0] || rcu_current != 0) $fatal(1, "unsupported PLL request was accepted");
+ apb4_write(32'h134, 1, 4'hf, 0);
+ apb4_write(32'h10c, 1, 4'hf, 0);
+ apb4_write(32'h114, 1, 4'hf, 0);
+ if (!clock_apply_seen || clock_profile != 1) $fatal(1, "clock apply handshake missing");
+ // APB acknowledgement is not clock completion. BUSY/current persist
+ // until the actual backend supplies its committed result.
+ apb4_read(32'h118, 0, value);
+ if (!value[0] || rcu_current != 0) $fatal(1, "RCU completed before backend");
+ apb4_write(32'h10c, 0, 4'hf, 1);
+ apb4_write(32'h11c, 1, 4'hf, 1);
+ apb4_write(32'h120, 0, 4'hf, 1);
+ apb4_write(32'h128, 0, 4'hf, 1);
+ apb4_write(32'h114, 8, 4'hf, 1);
+ apb4_read(32'h10c, 0, value);
+ if (value != 1) $fatal(1, "BUSY write changed staged profile");
+ apb4_read(32'h11c, 0, value);
+ if (value != 240000) $fatal(1, "BUSY write changed timeout");
+ apb4_write(32'h134, 1, 4'hf, 0);
+ @(negedge clk_i);
+ clock_current = 1;
+ clock_done = 1;
+ @(negedge clk_i);
+ clock_done = 0;
+ apb4_read(32'h110, 0, value);
+ if (value != 1 || rcu_current != 1 || sys_hz != 24_000_000)
+ $fatal(1, "external profile commit mismatch");
+ apb4_read(32'h118, 0, value);
+ if (value[1] || value[4]) begin
+ $display("RCU status=%h fault=%h current=%h", value, u_dut.s_rcu_fault_q, rcu_current);
+ $fatal(1, "RCU status mismatch");
+ end
+ apb4_write(32'h120, 1, 4'hf, 1);
+ apb4_write(32'h114, 2, 4'hf, 1);
+ apb4_read(32'h134, 0, value);
+ if (!value[0]) $fatal(1, "unsupported gate request was accepted");
+ apb4_write(32'h134, 1, 4'hf, 0);
+ apb4_write(32'h10c, 4, 4'hf, 1);
+ apb4_write(32'h11c, 0, 4'hf, 1);
+ apb4_write(32'h13c, 4, 4'hf, 1);
+ apb4_write(32'h130, 64, 4'hf, 1);
+ apb4_write(32'h134, 32, 4'hf, 1);
+ apb4_write(32'h138, 4, 4'hf, 1);
+ apb4_write(32'h114, 3, 4'hf, 1);
+ apb4_write(32'h15c, 1, 4'hf, 1);
+ apb4_write(32'h15c, 32'h105, 4'hf, 1);
+ apb4_write(32'h15c, 32'h101, 4'hf, 0);
+ apb4_write(32'h15c, 32'h201, 4'hf, 1);
+ apb4_write(32'h114, 1, 4'hf, 1);
+ apb4_read(32'h15c, 0, value);
+ if (value != 32'h101) $fatal(1, "live CLKOUT change was accepted");
+ apb4_write(32'h15c, 0, 4'hf, 0);
+ apb4_write(32'h134, 31, 4'hf, 0);
+ apb4_write(32'h138, 3, 4'hf, 0);
+ apb4_write(32'h13c, 3, 4'hf, 0);
+ if (rcu_irq) $fatal(1, "cleared RCU IRQ remains asserted");
+ @(negedge clk_i);
+ rcu_fault_event = 8;
+ apb4_write(32'h134, 8, 4'hf, 0);
+ apb4_write(32'h138, 2, 4'hf, 0);
+ apb4_write(32'h130, 8, 4'hf, 0);
+ @(negedge clk_i);
+ rcu_fault_event = 0;
+ apb4_read(32'h134, 0, value);
+ if (value != 8 || !rcu_irq) $fatal(1, "RCU W1C lost simultaneous fault/IRQ");
+ apb4_read(32'h130, 0, value);
+ if (!value[3]) $fatal(1, "RCU W1C lost simultaneous reset cause");
+ @(negedge clk_i);
+ warm_reset = 1;
+ @(negedge clk_i);
+ warm_reset = 0;
+ apb4_read(32'h138, 0, value);
+ if (value != 2 || rcu_irq) $fatal(1, "warm reset IRQ retention/enable mismatch");
+ apb4_read(32'h134, 0, value);
+ if (value != 8 || rcu_current != 0) $fatal(1, "warm reset fault/profile mismatch");
+ apb4_read(32'h114, 1, value);
apb4_write(`APB4_SYSCTRL__HP_CTRL, 1, 4'hf, 1);
apb4_write(`APB4_SYSCTRL__PLL_CMD, 1, 4'hf, 1);
apb4_read(32'hfff, 1, value);
@@ -73,6 +212,43 @@ module tiny_sysctrl_tb;
apb4_write(`APB4_SYSCTRL__FAULT_STATUS, 1, 4'hf, 0);
apb4_read(`APB4_SYSCTRL__FAULT_STATUS, 0, value);
if (value[0]) $fatal(1, "fault W1C failed");
+ // Multiple terminal events count independently. A hardware event on the
+ // clear edge wins and records the new first owner without clearing count.
+ fork
+ apb4_write(`APB4_SYSCTRL__FAULT_STATUS, 1, 4'hf, 0);
+ begin
+ wait (apb4.psel && apb4.penable && apb4.pwrite);
+ fault_count = 5;
+ fault_master = 2;
+ fault_addr = 32'h30018004;
+ fault_valid = 1;
+ @(posedge clk_i);
+ @(negedge clk_i);
+ fault_valid = 0;
+ end
+ join
+ apb4_read(`APB4_SYSCTRL__FAULT_COUNT, 0, value);
+ if (value != 7) $fatal(1, "concurrent fault count lost");
+ apb4_read(`APB4_SYSCTRL__FAULT_MASTER, 0, value);
+ if (value != 2) $fatal(1, "third owner truncated");
+ apb4_read(`APB4_SYSCTRL__FAULT_DETAIL, 0, value);
+ if (value != 3) $fatal(1, "raw AXI response changed");
+ apb4_read(`APB4_SYSCTRL__FAULT_STATUS, 0, value);
+ if (!value[0]) $fatal(1, "W1C defeated simultaneous hardware event");
+ // Seed only the counter corner, then exercise the actual wide adder and
+ // register update. Billions of event pulses are not useful simulation.
+ @(negedge clk_i);
+ force u_dut.u_fault_count_reg.dat_o = 32'hfffffffc;
+ fault_count = 8;
+ fault_valid = 1;
+ @(posedge clk_i);
+ @(negedge clk_i);
+ release u_dut.u_fault_count_reg.dat_o;
+ @(posedge clk_i);
+ @(negedge clk_i);
+ fault_valid = 0;
+ apb4_read(`APB4_SYSCTRL__FAULT_COUNT, 0, value);
+ if (value != 32'hffffffff) $fatal(1, "multi-event saturation wrapped");
@(negedge clk_i);
wake = 1;
@(negedge clk_i);
diff --git a/tests/rtl/tiny_watchdog_reset_tb.sv b/tests/rtl/tiny_watchdog_reset_tb.sv
new file mode 100644
index 00000000..828a3e97
--- /dev/null
+++ b/tests/rtl/tiny_watchdog_reset_tb.sv
@@ -0,0 +1,188 @@
+`timescale 1ns / 1ps
+module tiny_watchdog_reset_tb;
+ import wdg_pkg::*;
+ logic xin = 0, reset_n = 1;
+ always #20.833 xin = !xin;
+ logic ref_clk, sys_clk, pclk, backend_reset_n, por_n, system_n, ready;
+ logic [1:0] leaf_n;
+ logic wdg_reset_n, request;
+ logic [ 1:0] current_profile;
+ logic [ 4:0] clock_fault;
+ logic clock_done;
+ integer reset_events = 0;
+ logic [31:0] value;
+ wire clk_i = sys_clk;
+ apb4_if apb4 (
+ .pclk (sys_clk),
+ .presetn(leaf_n[0])
+ );
+ apb4_if wdg_source (
+ .pclk (sys_clk),
+ .presetn(leaf_n[0])
+ );
+ apb4_if wdg_apb (
+ .pclk (pclk),
+ .presetn(leaf_n[1])
+ );
+ wdg_if wdg (
+ .wdg_clk_i (ref_clk),
+ .wdg_rst_n_i(wdg_reset_n)
+ );
+ assign wdg.debug_halted_i = 1'b0;
+ assign request = wdg.reset_req_o;
+ tiny_apb_async_bridge u_wdg_cdc (
+ .src_clk_i (sys_clk),
+ .src_rst_n_i(leaf_n[0]),
+ .dst_clk_i (pclk),
+ .dst_rst_n_i(leaf_n[1]),
+ .disabled_i (1'b0),
+ .src_apb4 (wdg_source),
+ .dst_apb4 (wdg_apb)
+ );
+ tiny_clock_reset_subsystem u_backend (
+ .ref_clk_i (xin),
+ .rst_n_i (reset_n),
+ .profile_i (2'd0),
+ .apply_i (1'b0),
+ .idle_i (1'b1),
+ .clients_idle_i (1'b1),
+ .warm_reset_i (request),
+ .system_request_i (1'b0),
+ .command_ack_i (1'b1),
+ .timeout_i (32'd240000),
+ .current_profile_o(current_profile),
+ .ref_clk_o (ref_clk),
+ .sys_clk_o (sys_clk),
+ .pclk_o (pclk),
+ .system_reset_n_o (backend_reset_n),
+ .fault_code_o (clock_fault),
+ .done_o (clock_done)
+ );
+ tiny_reset_tree #(
+ .LeafCount (2),
+ .SplitDomains(1)
+ ) u_tree (
+ .clk_i (sys_clk),
+ .por_clk_i (ref_clk),
+ .leaf_clk_i ({pclk, sys_clk}),
+ .backend_reset_n_i (backend_reset_n),
+ .rst_n_i (reset_n),
+ .watchdog_reset_req_i(request),
+ .por_rst_n_o (por_n),
+ .system_rst_n_o (system_n),
+ .leaf_rst_n_o (leaf_n),
+ .cpu_ready_rst_n_o (ready)
+ );
+ // Same functional-reset dependency as the production Tiny integration.
+ rst_sync #(
+ .STAGE(5)
+ ) u_wdg_functional_reset (
+ .clk_i (ref_clk),
+ .rst_n_i(por_n && backend_reset_n),
+ .rst_n_o(wdg_reset_n)
+ );
+ apb4_wdg #(
+ .WDG_CLOCK_HZ(24000000)
+ ) u_wdg (
+ .apb4(wdg_apb),
+ .wdg (wdg)
+ );
+ tiny_sysctrl u_rcu (
+ .clk_i (sys_clk),
+ .rst_n_i (leaf_n[0]),
+ .clk_ref_i (ref_clk),
+ .rst_ref_n_i (por_n),
+ .apb4 (apb4),
+ .fault_valid_i (1'b0),
+ .fault_count_i (4'd0),
+ .fault_addr_i (32'd0),
+ .fault_write_i (1'b0),
+ .fault_master_i (2'd0),
+ .fault_resp_i (2'd0),
+ .cpu_wait_i (64'd0),
+ .dma_wait_i (64'd0),
+ .rtc_wake_i (1'b0),
+ .clock_done_i (clock_done),
+ .clock_fault_i (clock_fault),
+ .clock_current_i (current_profile),
+ .pll_locked_i (1'b0),
+ .pll_selected_i (1'b0),
+ .warm_reset_i (!backend_reset_n),
+ .watchdog_reset_i(request),
+ .hart_reset_i (1'b0),
+ .periph_done_i (1'b0),
+ .periph_fault_i (5'd0),
+ .gate_actual_i (14'd0),
+ .reset_done_i (14'd0),
+ .clkout_idle_i (1'b1)
+ );
+ always @(posedge request) if (reset_n) reset_events++;
+ task automatic watchdog_access(input logic [31:0] address, input logic write_access,
+ input logic [31:0] data, output logic [31:0] observed);
+ integer budget;
+ @(negedge clk_i);
+ wdg_source.paddr = address;
+ wdg_source.pwrite = write_access;
+ wdg_source.pwdata = data;
+ wdg_source.pstrb = write_access ? 4'hf : 4'h0;
+ wdg_source.psel = 1;
+ @(negedge clk_i);
+ wdg_source.penable = 1;
+ budget = 0;
+ while (wdg_source.pready !== 1'b1 && budget < 512) begin
+ @(negedge clk_i);
+ budget++;
+ end
+ if (budget == 512 || wdg_source.pslverr !== 0) $fatal(1, "watchdog APB failed");
+ observed = wdg_source.prdata;
+ @(negedge clk_i);
+ wdg_source.psel = 0;
+ wdg_source.penable = 0;
+ endtask
+ `include "apb4_bfm.svh"
+ initial begin
+ wdg_source.paddr = 0;
+ wdg_source.pwrite = 0;
+ wdg_source.pwdata = 0;
+ wdg_source.pstrb = 0;
+ wdg_source.psel = 0;
+ wdg_source.penable = 0;
+ wdg_source.pprot = 0;
+ apb4_idle();
+ #1 reset_n = 0;
+ repeat (20) @(negedge xin);
+ reset_n = 1;
+ wait (ready);
+ repeat (30) @(negedge ref_clk);
+ for (integer iteration = 1; iteration <= 2; iteration++) begin
+ watchdog_access(32'h008, 1, 1, value);
+ watchdog_access(32'h00c, 1, 64, value);
+ watchdog_access(32'h018, 1, 4, value);
+ watchdog_access(32'h000, 1, 1, value);
+ wait (request);
+ wait (!backend_reset_n);
+ #1;
+ if (system_n !== 0 || leaf_n !== 0 || wdg_reset_n !== 0)
+ $fatal(1, "watchdog did not reset all functional endpoints");
+ wait (backend_reset_n);
+ wait (ready && wdg_reset_n);
+ repeat (30) @(negedge ref_clk);
+ apb4_read(32'h130, 0, value);
+ if (value !== 32'd5 || request || reset_events != iteration ||
+ u_wdg.u_wdg_core.s_reset_count_q !== 0 || current_profile !== 0)
+ $fatal(1, "watchdog warm-reset cause/functional state mismatch cause=%h", value);
+ watchdog_access(32'h000, 1, 2, value);
+ repeat (30) @(negedge clk_i);
+ watchdog_access(32'h024, 0, 0, value);
+ if ((value & 3) !== 1) $fatal(1, "post-reset watchdog snapshot did not complete");
+ watchdog_access(32'h040, 0, 0, value);
+ if (value !== 0) $fatal(1, "watchdog functional reset counter did not clear");
+ end
+ $display("SIM_TEST_PASS Tiny watchdog reset cause retained and functional state reset twice");
+ $finish;
+ end
+ initial begin
+ #200000;
+ $fatal(1, "SIM_TEST_TIMEOUT Tiny watchdog reset");
+ end
+endmodule
diff --git a/tests/test_commercial_flow.py b/tests/test_commercial_flow.py
index d001e7be..ce093a44 100644
--- a/tests/test_commercial_flow.py
+++ b/tests/test_commercial_flow.py
@@ -421,7 +421,7 @@ def test_commercial_timing_contract_supports_tiny(tmp_path: Path) -> None:
check=True,
)
assert result.stdout.splitlines() == [
- "jtag,system",
+ "aon,audio,jtag,memory,peripheral,system",
"ext_rst_n_i_pad,jtag_trst_n_i_pad",
]
assert "observation {u_clock_buffer/clk_o}" in output.read_text(
@@ -612,6 +612,7 @@ set ::created_clocks_named {}
set ::generated_clocks {}
set ::clock_groups {}
set ::false_paths 0
+set ::case_selections {}
proc get_ports {args} { return [lindex $args end] }
proc get_pins {args} { return [list pin_[string map {/ _} [lindex $args end]]] }
@@ -629,6 +630,7 @@ proc set_clock_groups {args} { lappend ::clock_groups $args }
proc set_clock_uncertainty {args} {}
proc set_clock_transition {args} {}
proc set_false_path {args} { incr ::false_paths }
+proc set_case_analysis {value pins} { lappend ::case_selections [list $value $pins] }
proc all_inputs {} { return {} }
proc all_outputs {} { return {} }
proc current_design {args} { return {} }
@@ -642,6 +644,7 @@ puts "masters=$::created_clocks_named"
puts "generated=$::generated_clocks"
puts "groups=[llength $::clock_groups]"
puts "false_paths=$::false_paths"
+puts "case_selections=$::case_selections"
flow::require_commercial_clock_inventory
puts "inventory-ok"
"""
@@ -660,8 +663,8 @@ CONSTRAINT_SMOKE_CASES = {
"TINY": {
"domains": "rtl/tiny/integration/clock_reset_domains.json",
"pin_map": "rtl/tiny/pin_map/pin_map.json",
- "masters": "clk_system clk_jtag",
- "generated": "",
+ "masters": "clk_aon clk_jtag clk_audio",
+ "generated": "clk_system clk_memory clk_peripheral clk_sys_div2 clk_sys_div4 clk_clkout_source",
"groups": "1",
},
}
@@ -713,6 +716,11 @@ def test_constraint_stack_models_product_clocks(tmp_path: Path, soc: str) -> Non
assert lines["generated"].strip("-") == case["generated"]
assert lines["groups"] == case["groups"]
assert "inventory-ok" in result.stdout
+ if soc == "TINY":
+ for selection in ("u_sys_mux", "u_mem_mux", "u_pclk_div2_mux", "u_pclk_mux", "u_PLL_TOP_EN"):
+ assert selection in lines["case_selections"]
+ assert lines["case_selections"].count("{0 ") == 11
+ assert lines["case_selections"].count("{1 ") == 4
def test_commercial_make_graph_covers_both_products(tmp_path: Path) -> None:
diff --git a/tests/test_librelane_tiny_flow.py b/tests/test_librelane_tiny_flow.py
index fd2da88f..600c4259 100644
--- a/tests/test_librelane_tiny_flow.py
+++ b/tests/test_librelane_tiny_flow.py
@@ -107,10 +107,10 @@ def test_chip_config_covers_every_onchip_sram_bank(tmp_path: Path) -> None:
macro = config["MACROS"]["RM_IHPSG13_1P_1024x32_c2_bm_bist"]
assert len(macro["instances"]) == 32
assert (
- "u_soc.u_sram.gen_memory.gen_bank[0].u_ram.u_mem" in macro["instances"]
+ "u_soc.u_sram.gen_group[0].u_group.gen_bank[0].u_ram.u_mem" in macro["instances"]
)
assert (
- "u_soc.u_sram.gen_memory.gen_bank[31].u_ram.u_mem" in macro["instances"]
+ "u_soc.u_sram.gen_group[3].u_group.gen_bank[7].u_ram.u_mem" in macro["instances"]
)
for instance in macro["instances"].values():
x, y = instance["location"]
diff --git a/tests/test_publication_programming.py b/tests/test_publication_programming.py
index a9b8c235..bf7dcdf5 100644
--- a/tests/test_publication_programming.py
+++ b/tests/test_publication_programming.py
@@ -98,6 +98,21 @@ def test_channel_coverage_cannot_drop_the_boot_channel(spec):
pr.collect_programming(ROOT, spec, IDS, REFERENCE["source_revision"])
+def test_mini_channel_selection_excludes_inactive_tiny_roles():
+ header = (ROOT / "crt/include/retrosoc/hal/dma.h").read_text()
+ selected = pr.mini_channel_enum(header)
+ channels = pr.constants(selected, "RS_DMA_CHANNEL_")
+ assert sorted(channels.values()) == list(range(6))
+ assert "RS_DMA_CHANNEL_I2S_TX = RS_DMA_CHANNEL_BULK" in selected
+ # A Tiny allocation change must not become a Mini allocation or alias.
+ assert pr.mini_channel_enum(header.replace("RS_DMA_CHANNEL_I2S_TX = 6", "RS_DMA_CHANNEL_I2S_TX = 7")) == selected
+ with pytest.raises(ValueError, match="alias binding"):
+ pr.mini_channel_enum(header.replace("RS_DMA_CHANNEL_I2S_TX = RS_DMA_CHANNEL_BULK",
+ "RS_DMA_CHANNEL_I2S_TX = RS_DMA_CHANNEL_CRYPTO_IN"))
+ with pytest.raises(ValueError, match="unreviewed"):
+ pr.mini_channel_enum(header.replace("#ifdef RS_SOC_TINY", "#ifdef UNKNOWN_PRODUCT"))
+
+
def test_credit_parser_fails_closed_on_unreviewed_shape():
with pytest.raises(ValueError, match="unsupported credit function"):
pr.credit_values("function automatic int limits(input int master); return compute(master); endfunction", "limits", 8)
diff --git a/tests/test_publication_tiny.py b/tests/test_publication_tiny.py
index 01d42098..c2d6a647 100644
--- a/tests/test_publication_tiny.py
+++ b/tests/test_publication_tiny.py
@@ -1,204 +1,53 @@
-"""Tiny publication is parameter-bound, complete and isolated from Mini inputs."""
+"""Historical Tiny publication contracts remain separate from P6 source support."""
from __future__ import annotations
-import copy
-import json
-import re
+import os
from pathlib import Path
-import shutil
+import subprocess
+import sys
import pytest
from publications import tiny_reference as tiny
-from publications.register_reference import parse_definitions
+from publication_history import ORIGINAL_BOOK, P5_CONTRACT, snapshot
ROOT = Path(__file__).resolve().parents[1]
-@pytest.fixture(scope="module")
-def data():
- return tiny.collect(tiny.read(ROOT / tiny.BOOK / "tiny.json"), check_snapshot=False)
+@pytest.mark.parametrize("revision", [ORIGINAL_BOOK, P5_CONTRACT])
+def test_historical_publication_contracts(tmp_path, revision):
+ historical = snapshot(tmp_path / "source", revision)
+ result = subprocess.run(
+ [sys.executable, "-m", "pytest", "-q", "tests/test_publication_tiny.py",
+ "--basetemp=" + str(tmp_path / "tests"), "--tb=short"],
+ cwd=historical, text=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
+ env={**os.environ, "PYTHONDONTWRITEBYTECODE": "1"}, timeout=180,
+ )
+ (tmp_path / "historical-tests.log").write_text(result.stdout)
+ assert result.returncode == 0, result.stdout
+ assert " passed" in result.stdout and " skipped" not in result.stdout
-def test_tiny_configuration_and_inventory_are_derived(data):
- assert (data["facts"]["hardware_isa"], data["facts"]["firmware_isa"]) == ("RV32IMC", "RV32IM")
- assert data["facts"]["sram"] == {"CapacityKiB": 128, "DataWidth": 32, "IdWidth": 1}
- assert data["facts"]["dma"]["NumChannels"] == 4
- assert data["facts"]["dma"]["RequestMask"] == 0x7F9
- assert data["facts"]["dma"]["EnableStreams"] == 0
- assert data["facts"]["cpu"]["ResetSyncStages"] == 5
- assert tiny.RESET_TREE in data["source_paths"]
- assert len(data["catalog"]) == 14 and len(data["regions"]) == 20 and len(data["pads"]) == 52
- assert len(data["interrupts"]) == 14 and len(data["gpio"]) == 32
- assert not any(p.startswith("rtl/mini/") for p in data["source_paths"])
- assert "mpw" not in {s["destination"].split("/")[-1] for s in data["managed_sources"]}
-
-
-def test_tiny_collection_does_not_open_mini_files(monkeypatch):
- original = Path.read_text
- def read(path, *args, **kwargs):
- name = path.as_posix()
- if "/rtl/mini/" in name or ("/publications/datasheets/" in name and "/tiny/" not in name):
- raise AssertionError("Tiny opened a Mini product input: " + name)
- return original(path, *args, **kwargs)
- monkeypatch.setattr(Path, "read_text", read)
- tiny.collect(tiny.read(ROOT / tiny.BOOK / "tiny.json"), check_snapshot=False)
-
-
-def test_common_register_default_remains_mini_compatible():
- paths = ["rtl/ip/serial/apb4_uart_define.svh"]
- assert parse_definitions(paths, {}) == parse_definitions(paths, {}, "rtl/mini/address_map/memory_map.json")
- _, constants = parse_definitions(paths, {}, tiny.MAP)
- assert "SOC_SYSCTRL_TEST_STATUS_OFFSET" in constants
-
-
-def test_reset_update_does_not_bypass_reviewed_snapshot(monkeypatch):
- original = tiny.subprocess.check_output
- checked = []
-
- def changed_snapshot(command, **kwargs):
- if command[:3] == ["git", "diff", "--name-only"]:
- assert tiny.RESET_TREE in command
- checked.append(command)
- return tiny.RESET_TREE + "\n"
- return original(command, **kwargs)
-
- monkeypatch.setattr(tiny.subprocess, "check_output", changed_snapshot)
- with pytest.raises(ValueError, match="technical sources differ from reviewed snapshot"):
- tiny.collect(tiny.read(ROOT / tiny.BOOK / "tiny.json"))
- assert len(checked) == 1
-
-
-@pytest.fixture
-def source_tree(tmp_path):
- sources = [tiny.TOP, tiny.RESET_TREE, tiny.ARCHINFO, tiny.SYSCTRL, "rtl/ip/core/mgmt_core_wrapper.sv", "configs/ci/ihp130-tiny.mk",
- tiny.MAP, tiny.TOPOLOGY, (tiny.BOOK / "source-contract.json").as_posix()]
- for relative in sources:
- target = tmp_path / relative
- target.parent.mkdir(parents=True, exist_ok=True)
- shutil.copyfile(ROOT / relative, target)
- return tmp_path
-
-
-@pytest.mark.parametrize("path,before,after", [
- (tiny.TOP, ".CapacityKiB(128)", ".CapacityKiB(32)"),
- (tiny.TOP, ".Ics55SmallBanks(1'b1)", ".Ics55SmallBanks(1'b0)"),
- (tiny.TOP, ".DataWidth (32)", ".DataWidth (64)"),
- (tiny.TOP, ".NumChannels (4)", ".NumChannels (8)"),
- (tiny.TOP, ".EnableStreams(1'b0)", ".EnableStreams(1'b1)"),
- (tiny.TOP, ".EnableAtomics (1'b0)", ".EnableAtomics (1'b1)"),
- (tiny.TOP, ".ResetSyncStages (5)", ".ResetSyncStages (3)"),
- ("configs/ci/ihp130-tiny.mk", "ISA := RV32IM", "ISA := RV32IMC"),
- (tiny.ARCHINFO, "32'h5449_4e59", "32'h4d49_4e49"),
- (tiny.SYSCTRL, "!s_test_q[31] && apb4.pwdata[31]", "s_test_q[31] && apb4.pwdata[31]"),
-])
-def test_changed_hardware_or_isa_cannot_retain_publication_claim(source_tree, path, before, after):
- file = source_tree / path
- text = file.read_text(encoding="utf-8")
- # Parameter alignment is formatting, while the altered value must still
- # fail the source-bound publication contract.
- pattern = r"\s*".join(re.escape(char) for char in before if not char.isspace())
- assert re.search(pattern, text)
- file.write_text(re.sub(pattern, lambda _: after, text, count=1), encoding="utf-8")
- with pytest.raises(ValueError):
- tiny.facts(source_tree)
-
-
-@pytest.mark.parametrize("change", ["irq_missing", "irq_number", "region_address", "region_missing"])
-def test_missing_or_wrong_maps_fail(source_tree, change):
- path = source_tree / (tiny.TOPOLOGY if change.startswith("irq") else tiny.MAP)
- document = tiny.read(path)
- if change == "irq_missing":
- document["interrupts"].pop()
- elif change == "irq_number":
- document["interrupts"][-1]["core_bit"] = 27
- elif change == "region_address":
- next(r for r in document["regions"] if r["symbol"] == "SRAM")["base"] = "0x31000000"
- else:
- document["regions"].pop()
- path.write_text(json.dumps(document), encoding="utf-8")
- with pytest.raises(ValueError):
- tiny.validate_maps(source_tree, tiny.facts(source_tree))
-
-
-def test_tiny_sysctrl_and_discovery_do_not_inherit_mini_controls(data):
- control = data["registers"]["sysctrl"]["registers"]
- assert {r["name"] for r in control} == tiny.SUPPORTED_SYSCTRL
- assert not any(r["name"] in {"HP_CTRL", "PLL_CFG", "DEBUG_SELECT"} for r in control)
- test = next(r for r in control if r["name"] == "TEST_STATUS")
- assert [(f["name"], f["lsb"], f["msb"]) for f in test["fields"] if f["name"] != "Reserved"] == [
- ("PASS", 0, 0), ("CODE", 8, 15), ("VALID", 31, 31)]
- info = data["registers"]["archinfo"]["registers"]
- assert next(r for r in info if r["name"] == "SOC_ID")["reset"] == "0x54494E59"
- assert next(r for r in info if r["name"] == "SRAM_BYTES")["reset"] == "0x00020000"
- assert data["registers"]["dma"]["groups"][1]["count"] == 4
-
-
-def test_tcd_layout_is_complete_and_bounded(data):
- assert len(data["tcd"]) == 17
- assert sum(f["bytes"] for f in data["tcd"]) == 64
- assert data["tcd"][-1]["offset"] == 60
-
-
-def test_local_capabilities_keep_fabric_and_encoding_restrictions(data):
- capability = next(r for r in data["registers"]["dma"]["registers"] if r["name"] == "CAPABILITY")
- assert capability["reset"] == "0x00200401"
- burst = next(f for f in capability["fields"] if f["name"] == "MAX_BURST_FIELD")
- assert (burst["lsb"], burst["msb"], burst["reset"]) == (24, 27, "0x0")
- assert "encoding limitation" in burst["description"]
- sram = next(r for r in data["registers"]["sram"]["registers"] if r["name"] == "CAPABILITY")
- assert "rejects WRAP" in next(f for f in sram["fields"] if f["name"] == "WRAP")["description"]
-
-
-@pytest.mark.parametrize("key,value", [("document_id", "RS-MINI-DS"), ("profile", "configs/ci/ihp130.mk"), ("status", "RELEASE")])
-def test_wrong_product_configuration_cannot_enter_tiny_pipeline(key, value):
+def test_current_source_cannot_relabel_historical_publication():
config = tiny.read(ROOT / tiny.BOOK / "tiny.json")
- config[key] = value
- with pytest.raises(ValueError, match="configuration"):
- tiny.validate_configuration(config)
+ assert config["source_revision"] == "9c706ba0ff1da1028c299cfff573010a86dfd7ce"
+ with pytest.raises(ValueError, match="DMA parameters changed"):
+ tiny.collect(config)
-def test_existing_mini_output_is_never_overwritten(tmp_path, monkeypatch):
- from publications import build_tiny_datasheet as builder
- folder = tmp_path / "build/mini-existing"
- folder.mkdir(parents=True)
- manifest = folder / "manifest.json"
- manifest.write_text('{"document":{"document_id":"RS-MINI-DS"}}')
- before = manifest.read_bytes()
- monkeypatch.setattr(builder, "ROOT", tmp_path)
- with pytest.raises(ValueError, match="another product"):
- builder.validate_output(folder, {"document_id": "RS-TINY-DS"})
- assert manifest.read_bytes() == before
-
-
-@pytest.mark.parametrize("change", ["missing", "offset"])
-def test_register_inventory_cannot_silently_drop_or_move_a_definition(data, change):
- refs = copy.deepcopy(data["registers"])
- if change == "missing":
- refs["uart"]["registers"].pop()
- else:
- refs["uart"]["registers"][0]["offset"] += 4
- with pytest.raises(ValueError, match="register inventory"):
- tiny.validate_register_inventory(refs, tiny.read(ROOT / tiny.BOOK / "register-contract.json"))
-
-
-@pytest.mark.parametrize("change", ["missing", "duplicate"])
-def test_pdf_must_render_each_extracted_register_once(data, change):
- from publications.build_tiny_datasheet import validate_rendered_registers
- items = [dict(kind="tiny-register", family=family, key=r["key"], page=1)
- for family, value in data["registers"].items() for r in value["registers"]]
- assert validate_rendered_registers(data, items) == 337
- if change == "missing":
- items.pop()
- else:
- items.append(items[0])
- with pytest.raises(ValueError, match="omitted or duplicated"):
- validate_rendered_registers(data, items)
-
-
-@pytest.mark.parametrize("key,value", [("status", "qualified"), ("current_commit_reports", [{"passed": True}])])
-def test_unmatched_qualification_claim_is_rejected(data, key, value):
- evidence = copy.deepcopy(data["evidence"])
- evidence[key] = value
- with pytest.raises(ValueError, match="qualification"):
- tiny.validate_evidence(evidence)
+@pytest.mark.parametrize("before,after", [
+ ("s_req_q.write_done && !s_req_q.b_captured", "1'b1"),
+ ("s_r_valid && s_rready[s_req_q.owner] && s_r_data[1]", "s_r_valid"),
+ (".flush_i(1'b0)", ".flush_i(!enable_i)"),
+])
+def test_current_reset_binding_still_rejects_unsafe_completion(tmp_path, before, after):
+ contract = tiny.read(ROOT / tiny.BOOK / "source-contract.json")
+ binding = next(item for item in contract["bindings"]
+ if item["file"] == "rtl/tiny/top/tiny_axi4_target.sv")
+ path = tmp_path / binding["file"]
+ path.parent.mkdir(parents=True)
+ original = (ROOT / binding["file"]).read_text()
+ assert before in original
+ path.write_text(original.replace(before, after))
+ with pytest.raises(ValueError, match="Tiny behavior binding changed"):
+ tiny.require_snippets(tmp_path, binding["file"], binding["text"])
diff --git a/tests/test_script_tools.py b/tests/test_script_tools.py
index 98884b6f..ca9c2206 100644
--- a/tests/test_script_tools.py
+++ b/tests/test_script_tools.py
@@ -1969,6 +1969,8 @@ def test_run_flow_writes_structured_result(tmp_path: Path) -> None:
assert data["status"] == "passed"
assert data["exit_code"] == 0
assert data["duration_seconds"] >= 0
+ assert isinstance(data["parent_pid"], int)
+ assert isinstance(data["child_pid"], int)
assert isinstance(data["peak_rss_kib"], int)
assert data["peak_rss_kib"] > 0
assert log.read_text(encoding="utf-8") == "flow output\n"
@@ -2120,6 +2122,41 @@ def test_run_flow_streams_bytes_without_waiting_for_newline(tmp_path: Path) -> N
assert json.loads(result.read_text(encoding="utf-8"))["status"] == "passed"
+def test_run_flow_persists_cancel_before_cleanup_and_ignores_second_signal(tmp_path: Path) -> None:
+ result = tmp_path / "cancelled.json"
+ process = subprocess.Popen([
+ sys.executable, str(ROOT / "scripts/run_flow.py"), "--tool", "unit",
+ "--log", str(tmp_path / "cancelled.log"), "--result", str(result), "--",
+ sys.executable, "-u", "-c",
+ "import signal,time; signal.signal(signal.SIGTERM,signal.SIG_IGN); "
+ "print('READY',flush=True); time.sleep(30)",
+ ], text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+ try:
+ assert process.stdout is not None
+ assert process.stdout.readline().strip() == "READY"
+ process.send_signal(signal.SIGINT)
+ for _ in range(100):
+ checkpoint = json.loads(result.read_text())
+ if checkpoint["status"] == "interrupted":
+ break
+ time.sleep(0.01)
+ assert checkpoint["status"] == "interrupted"
+ assert checkpoint["child_cleanup_pending"] is True
+ assert "finished_at" not in checkpoint
+ process.send_signal(signal.SIGTERM)
+ _, stderr = process.communicate(timeout=10)
+ assert process.returncode == 130, stderr
+ final = json.loads(result.read_text())
+ assert final["status"] == "interrupted"
+ assert final["child_cleanup_pending"] is False
+ assert final["child_exit_code"] == -signal.SIGKILL
+ assert final["finished_at"] >= final["started_at"]
+ finally:
+ if process.poll() is None:
+ process.kill()
+ process.wait(timeout=5)
+
+
def test_run_flow_records_interruption(tmp_path: Path) -> None:
log = tmp_path / "interrupted.log"
result = tmp_path / "interrupted.json"
@@ -2192,6 +2229,27 @@ def test_simulation_success_marker_and_failure_detection(tmp_path: Path) -> None
assert json.loads(result.read_text(encoding="utf-8"))["status"] == "failed"
+@pytest.mark.parametrize("status,exit_code", [("running", None), ("failed", 1),
+ ("passed", 124), ("invalid", None)])
+def test_check_simulation_requires_terminal_flow(tmp_path: Path, status: str,
+ exit_code: int | None) -> None:
+ log = tmp_path / "sim.log"
+ result = tmp_path / "result-sim-check.json"
+ log.write_text("SIM_TEST_PASS code=0\n", encoding="utf-8")
+ (tmp_path / "result-sim.json").write_text(
+ json.dumps({"status": status, "exit_code": exit_code}),
+ encoding="utf-8",
+ )
+ checked = subprocess.run(
+ [sys.executable, str(ROOT / "scripts/check_simulation.py"), "--log", str(log),
+ "--result", str(result)], text=True, capture_output=True,
+ )
+ report = json.loads(result.read_text(encoding="utf-8"))
+ assert checked.returncode != 0
+ assert report["status"] == "failed"
+ assert report["flow_status"] == status
+
+
def test_warning_baseline_rejects_new_signature(tmp_path: Path) -> None:
profile = "unit-profile"
log = tmp_path / "variant/sim/verilator/verilating.log"
diff --git a/tests/test_tiny.py b/tests/test_tiny.py
index a1116257..330520f4 100644
--- a/tests/test_tiny.py
+++ b/tests/test_tiny.py
@@ -35,7 +35,13 @@ def test_tiny_sources_and_abi_are_independent(tmp_path: Path) -> None:
for index, target in enumerate(document["apb_targets"], start=1):
name = target["name"]
assert f".presetn(s_{name}_rst_n)" in interfaces
- assert f"assign s_{name}_rst_n = s_leaf_rst_n[{index}];" in interfaces
+ if name in {"gpio", "uart0", "i2c0", "timer0", "timer1", "pwm", "i2s", "sdio",
+ "dma", "ws2812", "rng", "crc", "crypto"}:
+ assert f"tiny_peripheral_leaf u_{name}_lifecycle" in interfaces
+ assert f".rst_n_i(s_leaf_rst_n[{index}])" in interfaces
+ assert f".rst_n_o(s_{name}_rst_n)" in interfaces
+ else:
+ assert f"assign s_{name}_rst_n = s_leaf_rst_n[{index}];" in interfaces
generate_filelists(filelists, output, ["+define+RETROSOC_SOC__TINY"])
generate_memory_map(TINY / "address_map/memory_map.json", output / "memory_map", "YES", 128)
resolved = parse_filelists(filelists / name for name in
@@ -51,7 +57,9 @@ def test_tiny_sources_and_abi_are_independent(tmp_path: Path) -> None:
assert "RS_SOC_PSRAM_BASE" not in sdk
irq = (output / "include/retrosoc/generated/irq_metadata.h").read_text()
assert "RS_SOC_EXTERNAL_IRQ_COUNT UINT32_C(30)" in irq
- assert "RS_SOC_EXT_IRQ_UART1 UINT32_C(24)" in irq
+ assert "RS_SOC_EXT_IRQ_UART1" not in irq
+ assert "RS_SOC_EXT_IRQ_I2C1" not in irq
+ assert "RS_SOC_EXT_IRQ_SDIO UINT32_C(8)" in irq
assert "RS_SOC_EXT_IRQ_DMA UINT32_C(18)" in irq
assert "RS_SOC_EXT_IRQ_NPU" not in irq
# A consumer of public SDK headers must see Tiny capabilities without
@@ -59,7 +67,7 @@ def test_tiny_sources_and_abi_are_independent(tmp_path: Path) -> None:
if shutil.which("cc"):
probe = tmp_path / "consumer.c"
probe.write_text('#include \n'
- '_Static_assert(RS_DMA_CHANNEL_COUNT == 4, "wrong product");\n')
+ '_Static_assert(RS_DMA_CHANNEL_COUNT == 8, "wrong product");\n')
run(["cc", "-std=c11", "-Werror", "-I" + str(ROOT / "crt/include"),
"-I" + str(output / "memory_map/include"), "-I" + str(output / "include"),
"-c", str(probe), "-o", str(tmp_path / "consumer.o")])
@@ -97,16 +105,23 @@ def test_tiny_protocol_contracts(tmp_path: Path, simulator: str, block: str) ->
pytest.skip("locked RTL/tool dependency unavailable")
generated = tmp_path / "map"
generate_memory_map(TINY / "address_map/memory_map.json", generated, "YES", 128)
+ generate_bindings(read_topology(TINY / "integration/soc_topology.json",
+ TINY / "address_map/memory_map.json"), tmp_path / "bindings")
top = "tiny_axi4_fabric_tb" if block == "fabric" else "tiny_sysctrl_tb"
sources = [COMMON / "utils/register.sv"]
if block == "fabric":
sources += [COMMON / "interface/axi4_if.sv", COMMON / "stream/round_robin_arbiter.sv",
- ROOT / "rtl/ip/interconnect/axi4_error_slave.sv", TINY / "top/tiny_axi4_fabric.sv"]
+ COMMON / "utils/spill_register.sv",
+ ROOT / "rtl/ip/interconnect/axi4_error_slave.sv",
+ TINY / "top/tiny_axi4_target.sv", TINY / "top/tiny_axi4_fabric.sv"]
else:
- sources += [COMMON / "interface/apb4_if.sv", TINY / "top/tiny_sysctrl.sv"]
+ sources += [COMMON / "interface/apb4_if.sv", COMMON / "cdc/cdc_sync.sv",
+ COMMON / "cdc/cdc_rst_ctrlr.sv", COMMON / "cdc/cdc_2phase.sv",
+ COMMON / "clkrst/rst_sync.sv", TINY / "top/tiny_apb_async_bridge.sv",
+ TINY / "top/tiny_sysctrl.sv"]
sources.append(ROOT / "tests/rtl" / f"{top}.sv")
- includes = [f"-I{p}" for p in (COMMON, COMMON / "interface", generated / "rtl",
- ROOT / "rtl/ip/peripheral", ROOT / "tests/rtl")]
+ includes = [f"-I{p}" for p in (COMMON, COMMON / "interface", generated / "rtl", tmp_path / "bindings/rtl",
+ ROOT / "rtl/ip/peripheral", TINY / "top", ROOT / "tests/rtl")]
output = tmp_path / "sim"
if simulator == "verilator":
cache = tmp_path / "cache"
diff --git a/tests/test_tiny_ics55_platform.py b/tests/test_tiny_ics55_platform.py
index 0c441912..f43d2311 100644
--- a/tests/test_tiny_ics55_platform.py
+++ b/tests/test_tiny_ics55_platform.py
@@ -109,10 +109,13 @@ def mapped_netlist():
else "input" for k in ports}}
cells = {"u_clock_buffer": cell("clock_buffer", clk_i=7, clk_o=2),
"receiver": cell("P65_1233_PBMUX", PAD=1, C=7, IE="1", OE="0", CS="1"),
- "u_soc.u_cpu.u_hazard3_cpu_1port.core.pc_reg": cell("DFF", CK=2),
+ "u_soc.u_cpu.u_hazard3_cpu_2port.core.pc_reg": cell("DFF", CK=2),
"pll": cell("PLL_TOP", EN=3, CKOUT1=4, CKOUT2=5, CKTST=6),
"low": cell("TIELOH7R", Z=3)}
- cells.update({f"bank{i}": cell(RAM, CLK=2) for i in range(32)})
+ cells.update({f"u_soc.u_sram.gen_group[{i // 8}].u_group.gen_bank[{i % 8}].u_ram.u_mem":
+ cell(RAM, CLK=2) for i in range(32)})
+ cells.update({f"u_soc.u_crypto.u_storage.gen_bank[{i}].gen_macro.u_sram.u_mem":
+ cell(RAM, CLK=2) for i in range(6)})
return {"modules": {
"retrosoc_tiny_asic": {"ports": {"extclk_i_pad": {"direction": "inout", "bits": [1]},
"jtag_tck_i_pad": {"direction": "inout", "bits": [11]}},
@@ -125,6 +128,7 @@ def mapped_netlist():
def test_safe24_structural_binding(mapped_netlist):
result = inspect_netlist(mapped_netlist)
assert len(result["sram"]) == 32 and result["cpu_clocked_cells"] == 1
+ assert len(result["crypto_sram"]) == 6
assert result["pll_enable"] == 0
assert result["system_clock_source"]["port"] == "extclk_i_pad"
assert [c["type"] for c in result["system_clock_source"]["path"]] == [
@@ -198,27 +202,44 @@ def test_hierarchical_constant_output_is_resolved(mapped_netlist):
@pytest.mark.parametrize("mutation", ["missing_pll", "missing_ram", "live_pll", "unknown_enable",
- "divided_ram", "wrong_cpu", "pll_sys"])
+ "divided_ram", "wrong_cpu", "pll_sys",
+ "missing_crypto", "misplaced_crypto", "divided_crypto"])
def test_unsafe_or_incomplete_mapping_is_rejected(mapped_netlist, mutation):
cells = mapped_netlist["modules"]["retrosoc_tiny_asic"]["cells"]
+ first_ram = "u_soc.u_sram.gen_group[0].u_group.gen_bank[0].u_ram.u_mem"
+ first_crypto = "u_soc.u_crypto.u_storage.gen_bank[0].gen_macro.u_sram.u_mem"
if mutation == "missing_pll":
del cells["pll"]
elif mutation == "missing_ram":
- del cells["bank0"]
+ del cells[first_ram]
elif mutation == "live_pll":
cells["low"]["type"] = "TIEHIH7R"
elif mutation == "unknown_enable":
cells["pll"]["connections"]["EN"] = [99]
elif mutation == "divided_ram":
- cells["bank0"]["connections"]["CLK"] = [99]
+ cells[first_ram]["connections"]["CLK"] = [99]
elif mutation == "wrong_cpu":
- cells["u_soc.u_cpu.u_hazard3_cpu_1port.core.pc_reg"]["connections"]["CK"] = [99]
+ cells["u_soc.u_cpu.u_hazard3_cpu_2port.core.pc_reg"]["connections"]["CK"] = [99]
+ elif mutation == "missing_crypto":
+ del cells[first_crypto]
+ elif mutation == "misplaced_crypto":
+ cells["unexpected_private_memory"] = cells.pop(first_crypto)
+ elif mutation == "divided_crypto":
+ cells[first_crypto]["connections"]["CLK"] = [99]
else:
cells["pll"]["connections"]["CKOUT1"] = [2]
with pytest.raises(ValueError):
inspect_netlist(mapped_netlist)
+def test_stale_p4_macro_hierarchy_is_rejected(mapped_netlist):
+ cells = mapped_netlist["modules"]["retrosoc_tiny_asic"]["cells"]
+ first_ram = "u_soc.u_sram.gen_group[0].u_group.gen_bank[0].u_ram.u_mem"
+ cells["u_soc.u_sram.gen_memory.gen_bank[0].u_ram.u_mem"] = cells.pop(first_ram)
+ with pytest.raises(ValueError, match="hierarchy"):
+ inspect_netlist(mapped_netlist)
+
+
def test_tiny_uses_32_actual_ics55_small_macros(tmp_path):
assert shutil.which("verilator"), "required simulator unavailable"
generated = tmp_path / "map"
diff --git a/tests/test_tiny_r2_p2.py b/tests/test_tiny_r2_p2.py
index 96760beb..dbccbb20 100644
--- a/tests/test_tiny_r2_p2.py
+++ b/tests/test_tiny_r2_p2.py
@@ -53,15 +53,17 @@ def test_reset_and_fifo_contracts(tmp_path: Path, simulator: str, fixture: str,
assert "SIM_TEST_FAIL" not in content and "SIM_TEST_TIMEOUT" not in content
-def test_reset_inventory_has_one_clock_and_all_existing_targets() -> None:
+def test_reset_inventory_has_active_domains_and_all_existing_targets() -> None:
topology = json.loads((ROOT / "rtl/tiny/integration/soc_topology.json").read_text())
inventory = json.loads((ROOT / "rtl/tiny/integration/clock_reset_domains.json").read_text())
reset = inventory["reset_distribution"]
assert reset["release_edges"] == 5
assert reset["leaf_names"] == ["fabric", *(target["name"] for target in topology["apb_targets"])]
- assert len(reset["leaf_names"]) == 17
- assert {domain["name"] for domain in inventory["domains"]} == {"system", "jtag"}
- assert reset["clock_domain"] == "system" and reset["main_sram_payload_reset"] == "none"
+ assert len(reset["leaf_names"]) == 21
+ assert {domain["name"] for domain in inventory["domains"]} == {
+ "aon", "system", "memory", "peripheral", "jtag", "audio"}
+ assert reset["clock_domain"] == "domain-specific" and reset["main_sram_payload_reset"] == "none"
+ assert inventory["local_memory"]["clock_domain"] == "system"
SDC = ("create_clock -name clk_system -period 41.666666667 $clk_system_pin\n"
diff --git a/tests/test_tiny_r2_p4.py b/tests/test_tiny_r2_p4.py
new file mode 100644
index 00000000..b3a8d9ae
--- /dev/null
+++ b/tests/test_tiny_r2_p4.py
@@ -0,0 +1,195 @@
+"""Tiny dual-port local-memory protocol and source-bound replay acceptance."""
+from __future__ import annotations
+
+import os
+from pathlib import Path
+import shutil
+import subprocess
+
+import pytest
+
+from scripts.check_simulation import DEFAULT_FAILURE
+from scripts.rtl.generate_memory_map import generate as generate_memory_map
+from scripts import tiny_r2_p4 as p4
+
+ROOT = Path(__file__).resolve().parents[1]
+COMMON = ROOT / "rtl/managed/clusterip/common/rtl"
+TINY = ROOT / "rtl/tiny/top"
+
+
+def run(command, directory, label, timeout=180):
+ result = subprocess.run(command, text=True, stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT, timeout=timeout,
+ env={**os.environ, "CCACHE_DISABLE": "1"})
+ (directory / f"{label}.log").write_text(result.stdout)
+ assert result.returncode == 0, result.stdout[-12000:]
+ if label != "sv2v": # Converted source contains literal assertion/error strings.
+ assert not DEFAULT_FAILURE.search(result.stdout), result.stdout[-12000:]
+ return result.stdout
+
+
+@pytest.mark.parametrize("simulator", ["verilator", "iverilog"])
+@pytest.mark.parametrize("pdk", ["BEHAV", "ICS55", "IHP130"])
+def test_four_group_local_memory(tmp_path, simulator, pdk):
+ for tool in (simulator, "sv2v", "vvp"):
+ assert shutil.which(tool), f"required tool unavailable: {tool}"
+ generate_memory_map(ROOT / "rtl/tiny/address_map/memory_map.json", tmp_path / "map", "YES", 128)
+ sources = [COMMON / p for p in (
+ "utils/register.sv", "utils/fifo.sv", "stream/round_robin_arbiter.sv", "interface/axi4_if.sv",
+ "interface/axi4_addr_gen.sv", "interface/apb4_if.sv", "interface/ahbl_if.sv")]
+ sources += [TINY / p for p in (
+ "tiny_sram_port_if.sv", "tiny_sram_group.sv", "tiny_sram_axi4.sv",
+ "tiny_sram.sv", "tiny_cpu_mem.sv")]
+ sources += [ROOT / p for p in (
+ "rtl/tech/tc_sram.sv", "rtl/ip/memory/onchip_ram_reg.sv",
+ "rtl/ip/interconnect/ahbl2axi4.sv", "rtl/ip/interconnect/axi4_error_slave.sv",
+ "tests/rtl/tiny_r2_p4_memory_tb.sv")]
+ defines = ["-DSV_ASSRT_DISABLE", f"-DPDK_{pdk}", "-DHAVE_SRAM_MACRO"]
+ vendor = []
+ if pdk == "IHP130":
+ base = ROOT / "physical/pdk/IHP-Open-PDK/ihp-sg13g2/libs.ref/sg13g2_sram/verilog"
+ vendor = [base / "RM_IHPSG13_1P_core_behavioral_bm_bist.v",
+ base / "RM_IHPSG13_1P_1024x32_c2_bm_bist.v"]
+ defines += ["-DFUNCTIONAL", "-DSYNTHESIS"]
+ elif pdk == "ICS55":
+ vendor = [ROOT / ".cache/retrosoc/pdk/ics55/sram/ics55_ecos_sram_1024x32_m8/verilog/ics55_ecos_sram_1024x32_m8_core.v"]
+ assert all(p.is_file() for p in vendor), "locked SRAM models unavailable"
+ includes = [f"-I{p}" for p in (COMMON, COMMON / "interface", tmp_path / "map/rtl",
+ ROOT / "rtl/ip/memory")]
+ top = "tiny_r2_p4_memory_tb"
+ output = tmp_path / "sim"
+ if simulator == "verilator":
+ run([simulator, "--binary", "--timing", "--assert", "-Wno-fatal", "-j", "3",
+ "-DHAVE_SVA", "--top-module", top, *defines, *includes,
+ *(str(p) for p in sources + vendor), "--Mdir", str(tmp_path / "obj"), "-o", str(output)],
+ tmp_path, "compile")
+ launch = [str(output)]
+ else:
+ # Keep vendor models outside sv2v, as in the executable Tiny flow.
+ converted = run(["sv2v", *defines, *includes, *(str(p) for p in sources)], tmp_path, "sv2v")
+ (tmp_path / "converted.v").write_text(converted)
+ run([simulator, "-g2012", "-gno-specify", "-s", top,
+ *defines, "-o", str(output), str(tmp_path / "converted.v"), *(str(p) for p in vendor)],
+ tmp_path, "compile")
+ launch = ["vvp", str(output)]
+ content = run(launch, tmp_path, "sim")
+ assert "SIM_TEST_PASS Tiny P4 memory" in content
+ assert "P4_STREAM read_gap_max=1 write_gap_max=1 beats=16" in content
+ assert "P4_BUFFER backpressure errors reset conservation PASS" in content
+
+
+@pytest.mark.parametrize("simulator", ["verilator", "iverilog"])
+@pytest.mark.parametrize("p7_lifecycle", [False, True])
+def test_actual_dual_port_cpu_debug_and_reset(tmp_path, simulator, p7_lifecycle):
+ for tool in (simulator, "sv2v", "vvp", "riscv32-unknown-elf-gcc", "riscv32-unknown-elf-objcopy"):
+ assert shutil.which(tool), f"required tool unavailable: {tool}"
+ generate_memory_map(ROOT / "rtl/tiny/address_map/memory_map.json", tmp_path / "map", "YES", 128)
+ lifecycle_defines = ["-DTINY_TEST_P7"] if p7_lifecycle else []
+ run(["riscv32-unknown-elf-gcc", *lifecycle_defines, "-march=rv32imc_zicsr_zifencei", "-mabi=ilp32",
+ "-nostdlib", "-Wl,--build-id=none", "-T", str(ROOT / "tests/rtl/tiny_r2_p4_cpu.ld"),
+ str(ROOT / "tests/rtl/tiny_r2_p4_cpu.S"), "-o", str(tmp_path / "cpu.elf")], tmp_path, "firmware")
+ run(["riscv32-unknown-elf-objcopy", "-O", "verilog", str(tmp_path / "cpu.elf"),
+ str(tmp_path / "cpu.hex")], tmp_path, "objcopy")
+ sources = [COMMON / p for p in (
+ "utils/register.sv", "utils/fifo.sv", "stream/round_robin_arbiter.sv", "interface/axi4_if.sv",
+ "interface/axi4_addr_gen.sv", "interface/apb4_if.sv", "interface/ahbl_if.sv",
+ "clkrst/rst_sync.sv", "cdc/cdc_sync.sv")]
+ sources += [ROOT / "rtl/managed/hazard3/hdl" / line.removeprefix("/hazard3/")
+ for line in (ROOT / "rtl/tiny/filelist/core_hazard3.fl").read_text().splitlines()
+ if line.startswith("/hazard3/")]
+ sources += [TINY / p for p in (
+ "tiny_sram_port_if.sv", "tiny_sram_group.sv", "tiny_sram_axi4.sv",
+ "tiny_sram.sv", "tiny_cpu_mem.sv", "tiny_clock_gate.sv", "tiny_cpu_wrapper.sv")]
+ sources += [ROOT / p for p in (
+ "rtl/tech/tc_sram.sv", "rtl/tech/tc_clk.sv", "rtl/ip/memory/onchip_ram_reg.sv",
+ "rtl/ip/interconnect/ahbl2axi4.sv", "rtl/ip/core/mgmt_debug_wrapper.sv",
+ "rtl/ip/core/mgmt_debug_reset.sv", "tests/rtl/tiny_r2_p4_cpu_tb.sv")]
+ includes = [f"-I{p}" for p in (COMMON, COMMON / "interface", tmp_path / "map/rtl",
+ ROOT / "rtl/ip/memory", ROOT / "rtl/managed/hazard3/hdl",
+ ROOT / "rtl/tiny/dv")]
+ defines = ["-DSV_ASSRT_DISABLE", "-DPDK_BEHAV", "-DSOC_JTAG_IDCODE=32'hDEADBEEF", *lifecycle_defines]
+ output = tmp_path / "sim"
+ top = "tiny_r2_p4_cpu_tb"
+ if simulator == "verilator":
+ run([simulator, "--binary", "--timing", "--assert", "-Wno-fatal", "-j", "3",
+ "-DHAVE_SVA", "--top-module", top, *defines, *includes,
+ *(str(p) for p in sources), "--Mdir", str(tmp_path / "obj"), "-o", str(output)],
+ tmp_path, "compile")
+ launch = [str(output)]
+ else:
+ converted = run(["sv2v", *defines, *includes, *(str(p) for p in sources)], tmp_path, "sv2v")
+ (tmp_path / "converted.v").write_text(converted)
+ run([simulator, "-g2012", "-s", top, "-o", str(output), str(tmp_path / "converted.v")],
+ tmp_path, "compile")
+ launch = ["vvp", str(output)]
+ content = run([*launch, f"+firmware={tmp_path / 'cpu.hex'}"], tmp_path, "sim")
+ assert "SIM_TEST_PASS Tiny P4 CPU" in content
+ if p7_lifecycle:
+ assert "P7_CPU_LIFECYCLE_PASS" in content
+
+
+def test_group_control_induction(tmp_path):
+ for tool in ("sv2v", "yosys"):
+ assert shutil.which(tool), f"required tool unavailable: {tool}"
+ sources = [COMMON / "utils/register.sv", COMMON / "utils/fifo.sv",
+ COMMON / "stream/round_robin_arbiter.sv",
+ TINY / "tiny_sram_port_if.sv", TINY / "tiny_sram_group.sv",
+ ROOT / "tests/rtl/tiny_r2_p4_formal.sv"]
+ converted = run(["sv2v", "-DFORMAL", "-DSV_ASSRT_DISABLE", f"-I{COMMON}",
+ *(str(p) for p in sources)], tmp_path, "sv2v")
+ (tmp_path / "formal.v").write_text(converted)
+ script = (f"read_verilog -formal {tmp_path / 'formal.v'}; "
+ "prep -top tiny_r2_p4_formal -flatten; memory_map; async2sync; dffunmap; opt; "
+ "sat -seq 12 -tempinduct -prove-asserts -verify -set-init-zero")
+ content = run(["yosys", "-p", script], tmp_path, "formal", timeout=180)
+ assert "Induction step proven: SUCCESS!" in content
+
+
+def p4_log():
+ from test_tiny_r2_p3 import sample_log
+ lines = [sample_log()]
+ for window in range(1, 9):
+ for port in range(2):
+ lines.append(f"R2_P4_LOCAL window={window} port={port} accepted=3 completed=3 "
+ "latency_max=3 admission_wait=0")
+ for group in range(4):
+ lines.append(f"R2_P4_BANK window={window} group={group} issues_i=2 issues_d=1 "
+ "issues_external=0 conflicts=0 wait_i=0 wait_d=0 wait_external=0")
+ return "\n".join(lines)
+
+
+def test_p4_acceptance_retains_local_and_external_accounting():
+ samples = p4.parse_log(p4_log(), {"status": "passed", "exit_code": 0})
+ assert len(samples["local"]) == 16 and len(samples["banks"]) == 32
+ assert samples["cases"][2]["checksum"] == 0xc4a58dc5
+
+
+@pytest.mark.parametrize("mutation", ["exit", "marker", "missing", "duplicate", "accounting"])
+def test_p4_rejects_incomplete_or_failed_evidence(mutation):
+ text = p4_log()
+ flow = {"status": "passed", "exit_code": 0}
+ if mutation == "exit":
+ flow["exit_code"] = 124
+ elif mutation == "marker":
+ text += "\nSIM_TEST_TIMEOUT"
+ elif mutation == "missing":
+ text = "\n".join(line for line in text.splitlines() if not line.startswith("R2_P4_BANK window=8"))
+ elif mutation == "duplicate":
+ text += "\n" + next(line for line in text.splitlines() if line.startswith("R2_P4_LOCAL"))
+ else:
+ text = text.replace("accepted=3 completed=3", "accepted=3 completed=4")
+ with pytest.raises(ValueError):
+ p4.parse_log(text, flow)
+
+
+@pytest.mark.parametrize("key", p4.HARDWARE_KEYS)
+def test_p4_rejects_mismatched_hardware(key):
+ values = dict.fromkeys(p4.HARDWARE_KEYS, "same")
+ with pytest.raises(ValueError):
+ p4.compatible_hardware({"configuration": values}, {"configuration": values | {key: "other"}})
+
+
+def test_p4_preserves_independent_image_compiler_identity():
+ hardware = dict.fromkeys(p4.HARDWARE_KEYS, "same")
+ p4.compatible_hardware({"configuration": hardware | {"SW_OPT": "O2"}},
+ {"configuration": hardware | {"SW_OPT": "APP"}})
diff --git a/tests/test_tiny_r2_p5.py b/tests/test_tiny_r2_p5.py
new file mode 100644
index 00000000..ea565906
--- /dev/null
+++ b/tests/test_tiny_r2_p5.py
@@ -0,0 +1,193 @@
+"""P5 target concurrency, fault accounting and retained-image acceptance."""
+from __future__ import annotations
+
+import shutil
+import subprocess
+import json
+from pathlib import Path
+
+import pytest
+
+from scripts.generate_tiny import generate_bindings, generate_filelists, read_topology
+from scripts.rtl.filelist import parse_filelists
+from scripts.rtl.generate_memory_map import generate as generate_memory_map
+from test_tiny_r2_p4 import run
+from scripts import tiny_r2_p5 as p5
+
+ROOT = Path(__file__).resolve().parents[1]
+COMMON = ROOT / "rtl/managed/clusterip/common/rtl"
+TINY = ROOT / "rtl/tiny/top"
+
+
+def sources(tmp_path):
+ generate_memory_map(ROOT / "rtl/tiny/address_map/memory_map.json", tmp_path / "map", "YES", 128)
+ generate_bindings(read_topology(ROOT / "rtl/tiny/integration/soc_topology.json",
+ ROOT / "rtl/tiny/address_map/memory_map.json"), tmp_path / "bindings")
+ files = [COMMON / p for p in ("utils/register.sv", "utils/spill_register.sv",
+ "utils/fifo.sv", "stream/round_robin_arbiter.sv", "interface/axi4_if.sv",
+ "interface/axi4_addr_gen.sv", "interface/apb4_if.sv", "interface/ahbl_if.sv")]
+ files += [TINY / p for p in ("tiny_axi4_target.sv", "tiny_axi4_fabric.sv",
+ "tiny_axi42apb4.sv", "tiny_fault_collector.sv", "tiny_sysctrl.sv")]
+ files += [ROOT / "rtl/ip/interconnect/axi4_error_slave.sv"]
+ includes = [f"-I{p}" for p in (COMMON, COMMON / "interface", TINY, tmp_path / "map/rtl",
+ tmp_path / "bindings/rtl", ROOT / "rtl/ip/peripheral", ROOT / "tests/rtl")]
+ return files, includes
+
+
+def simulate(tmp_path, simulator, top, files, includes, arguments=()):
+ for tool in (simulator, "sv2v", "vvp"):
+ assert shutil.which(tool), f"required tool unavailable: {tool}"
+ binary = tmp_path / "sim"
+ if simulator == "verilator":
+ run([simulator, "--binary", "--timing", "--assert", "-Wno-fatal", "-j", "3",
+ "-DHAVE_SVA", "-DSV_ASSRT_DISABLE", "--top-module", top, *includes,
+ *(str(p) for p in files), "--Mdir", str(tmp_path / "obj"), "-o", str(binary)],
+ tmp_path, "compile", timeout=300)
+ launch = [str(binary)]
+ else:
+ # Conversion warnings are diagnostics, never Verilog source text.
+ converted = subprocess.run(["sv2v", "-DSV_ASSRT_DISABLE", *includes,
+ *(str(p) for p in files)], text=True,
+ capture_output=True, timeout=300)
+ (tmp_path / "sv2v.log").write_text(converted.stderr)
+ assert converted.returncode == 0, converted.stderr
+ (tmp_path / "converted.v").write_text(converted.stdout)
+ run([simulator, "-g2012", "-s", top, "-o", str(binary), str(tmp_path / "converted.v")],
+ tmp_path, "compile")
+ launch = ["vvp", str(binary)]
+ log = run([*launch, *arguments], tmp_path, "sim", timeout=180)
+ assert "SIM_TEST_PASS Tiny P5" in log
+ return log
+
+
+@pytest.mark.parametrize("simulator", ["iverilog", "verilator"])
+def test_concurrent_fabric(tmp_path, simulator):
+ files, includes = sources(tmp_path)
+ files.append(ROOT / "tests/rtl/tiny_r2_p5_fabric_tb.sv")
+ log = simulate(tmp_path, simulator, "tiny_r2_p5_fabric_tb", files, includes)
+ assert "random_reads=96 concurrent_faults=2" in log
+
+
+@pytest.mark.parametrize("simulator", ["iverilog", "verilator"])
+def test_target_reset_isolation(tmp_path, simulator):
+ files, includes = sources(tmp_path)
+ files.append(ROOT / "rtl/ip/peripheral/dma_axi4_master.sv")
+ files.append(ROOT / "tests/rtl/tiny_r2_p5_reset_tb.sv")
+ log = simulate(tmp_path, simulator, "tiny_r2_p5_reset_tb", files, includes)
+ assert "SIM_TEST_PASS Tiny P5 target reset isolation" in log
+ assert "P5_RESET cases=28 production_dma=3 burst_beats=16 held_RB=1 first_fault=1" in log
+
+
+@pytest.mark.parametrize("simulator", ["iverilog", "verilator"])
+def test_real_cpu_dma_progress_and_abort(tmp_path, simulator):
+ sources(tmp_path)
+ firmware = tmp_path / "cpu.elf"
+ run(["riscv32-unknown-elf-gcc", "-march=rv32imc_zicsr_zifencei", "-mabi=ilp32",
+ "-nostdlib", "-Wl,--build-id=none", "-T", str(ROOT / "tests/rtl/tiny_r2_p4_cpu.ld"),
+ str(ROOT / "tests/rtl/tiny_r2_p5_cpu.S"), "-o", str(firmware)], tmp_path, "firmware")
+ run(["riscv32-unknown-elf-objcopy", "-O", "verilog", str(firmware), str(tmp_path / "cpu.hex")],
+ tmp_path, "objcopy")
+ generate_filelists(tmp_path / "filelists", tmp_path / "bindings", [])
+ resolved = parse_filelists(tmp_path / "filelists" / name for name in (
+ "inc.fl", "commonip.fl", "clusterip.fl", "core_hazard3.fl", "ip.fl", "tech.fl", "top.fl"))
+ # Only the XPI boundary is substituted; the production CPU/DMA/SoC stay real.
+ files = [p for p in resolved.files if p.name not in ("apb4_xpi.sv", "retrosoc_tiny_asic.sv")]
+ files.append(ROOT / "tests/rtl/tiny_r2_p5_system_tb.sv")
+ run(["python3", str(ROOT / "rtl/managed/clusterip/archinfo/scripts/generate_metadata.py"),
+ "--repo-root", str(ROOT), "--lock", str(ROOT / "dependencies/dependencies.lock.json"),
+ "--config-digest", "000000000000", "--output-sv", str(tmp_path / "archinfo_integration_metadata.svh"),
+ "--output-c", str(tmp_path / "archinfo_integration_metadata.h")], tmp_path, "metadata")
+ includes = [f"-I{p}" for p in (*resolved.incdirs, tmp_path, tmp_path / "map/rtl",
+ tmp_path / "bindings/rtl", ROOT / "rtl/tiny/dv")]
+ includes += ["-DPDK_BEHAV", "-DRETROSOC_SOC__TINY", "-DSOC_JTAG_IDCODE=32'hDEADBEEF",
+ "-DSOC_EXT_CLK_HZ=24000000"]
+ log = simulate(tmp_path, simulator, "tiny_r2_p5_system_tb", files, includes,
+ [f"+firmware={tmp_path / 'cpu.hex'}"])
+ assert "hart_reset=1 abort_drained=1" in log
+
+
+@pytest.mark.parametrize("kind", ["fabric", "fabric_reset", "fault"])
+def test_protocol_proofs(tmp_path, kind):
+ for tool in ("sv2v", "yosys"):
+ assert shutil.which(tool), f"required tool unavailable: {tool}"
+ files, includes = sources(tmp_path)
+ top = f"tiny_r2_p5_{'fabric' if kind == 'fabric_reset' else kind}_formal"
+ files.append(ROOT / f"tests/rtl/{top}.sv")
+ reset_define = ["-DTINY_P5_RESET_FORMAL"] if kind == "fabric_reset" else []
+ converted = subprocess.run(["sv2v", "-DFORMAL", "-DSV_ASSRT_DISABLE", *reset_define, *includes,
+ *(str(p) for p in files)], text=True, capture_output=True, timeout=180)
+ (tmp_path / "sv2v.log").write_text(converted.stderr)
+ assert converted.returncode == 0, converted.stderr
+ (tmp_path / "formal.v").write_text(converted.stdout)
+ proof = "sat -prove-asserts -verify" if kind == "fault" else (
+ "sat -seq 12 -tempinduct -prove-asserts -verify -set-init-zero")
+ script = (f"read_verilog -formal {tmp_path / 'formal.v'}; prep -top {top} -flatten; "
+ f"memory_map; async2sync; dffunmap; opt; {proof}")
+ log = run(["yosys", "-p", script], tmp_path, "formal", timeout=300)
+ assert "SUCCESS!" in log
+
+
+def p5_log():
+ from test_tiny_r2_p4 import p4_log
+ return p4_log() + "\n" + "\n".join(
+ f"R2_P5_FABRIC window={window} active_targets_max=3 "
+ "dma_rw_overlap_cycles=5 dma_rw_beat_overlap_cycles=2" for window in range(1, 9))
+
+
+def test_p5_records_keep_old_image_measurements_separate():
+ samples = p5.parse_log(p5_log(), {"status": "passed", "exit_code": 0})
+ assert len(samples["fabric"]) == 8
+ assert len(samples["local"]) == 16
+ assert p5.PHASE == "TINY-R2-P5" and p5.previous.PHASE == "TINY-R2-P4"
+
+
+@pytest.mark.parametrize("mutation", ["missing", "duplicate", "negative", "overflow", "overlap", "serialized", "failure"])
+def test_p5_rejects_invalid_concurrency_evidence(mutation):
+ content = p5_log()
+ if mutation == "missing":
+ content = "\n".join(line for line in content.splitlines() if not line.startswith("R2_P5_FABRIC window=8"))
+ elif mutation == "duplicate":
+ content += "\n" + content.splitlines()[-1]
+ elif mutation == "negative":
+ content = content.replace("active_targets_max=3", "active_targets_max=-1")
+ elif mutation == "overflow":
+ content = content.replace("active_targets_max=3", "active_targets_max=24")
+ elif mutation == "overlap":
+ content = content.replace("dma_rw_beat_overlap_cycles=2", "dma_rw_beat_overlap_cycles=6")
+ elif mutation == "serialized":
+ content = content.replace("active_targets_max=3", "active_targets_max=1")
+ else:
+ content += "\nSIM_TEST_FAIL"
+ with pytest.raises(ValueError):
+ p5.parse_log(content, {"status": "passed", "exit_code": 0})
+
+
+@pytest.mark.parametrize("mutation", ["none", "phase", "source", "cycles", "image", "local"])
+def test_p5_reference_is_bound_to_p4_execution(tmp_path, monkeypatch, mutation):
+ case = {"name": "flat", "image": {"path": "immutable"},
+ "samples": {"cases": [{"name": "dma16", "cycles": 17897, "instructions": 10}],
+ "local": [], "banks": []}}
+ report = {"phase": "TINY-R2-P4", "status": "measurement_complete", "source_digest": "original",
+ "executions": {"verilator": {}}, "comparisons": [
+ {"name": "flat", "image": {"path": "immutable"}, "local": [], "banks": [],
+ "workloads": [{"name": "dma16", "candidate_cycles": 17897, "candidate_instructions": 10}]}]}
+ monkeypatch.setattr(p5.previous, "validate_replay", lambda *args: {
+ "source_digest": "original", "cases": [case]})
+ monkeypatch.setattr(p5.b, "check_artifact", lambda artifact: tmp_path / "record.json")
+ if mutation == "phase":
+ report["phase"] = "TINY-R2-P5"
+ elif mutation == "source":
+ report["source_digest"] = "other"
+ elif mutation == "cycles":
+ report["comparisons"][0]["workloads"][0]["candidate_cycles"] = 1
+ elif mutation == "image":
+ report["comparisons"][0]["image"]["path"] = "different"
+ elif mutation == "local":
+ report["comparisons"][0]["local"] = [{}]
+ path = tmp_path / "report.json"
+ path.write_text(json.dumps(report))
+ if mutation == "none":
+ assert p5.reference_cases(path, ["flat"]) == [case]
+ else:
+ with pytest.raises(ValueError):
+ p5.reference_cases(path, ["flat"])
diff --git a/tests/test_tiny_r2_p6.py b/tests/test_tiny_r2_p6.py
new file mode 100644
index 00000000..574940e3
--- /dev/null
+++ b/tests/test_tiny_r2_p6.py
@@ -0,0 +1,547 @@
+"""P6 product integration and fail-closed SDK/macro contracts."""
+from __future__ import annotations
+
+import json
+import hashlib
+import io
+from pathlib import Path
+import re
+import shutil
+import subprocess
+import tarfile
+
+import pytest
+
+from scripts.generate_tiny import generate_bindings, read_topology
+from scripts.rtl.generate_memory_map import generate as generate_memory_map
+from scripts.check_clock_reset_domains import validate as validate_clocks
+from test_tiny_r2_p4 import run
+from test_sdio import SDIO_FULL_SOURCES, SDIO_FULL_INCDIRS
+from scripts.check_simulation import DEFAULT_FAILURE
+from scripts import tiny_r2_p6 as evidence
+
+ROOT = Path(__file__).resolve().parents[1]
+TINY = ROOT / "rtl/tiny"
+
+
+def test_native_card_model_elaborates_without_sv2v(tmp_path):
+ run(["iverilog", "-g2012", "-s", "sdio_native_card_model", "-o", str(tmp_path / "sim"),
+ str(ROOT / evidence.MODEL)], tmp_path, "compile")
+
+
+@pytest.mark.parametrize("mutation", ["none", "expression", "body", "still_forward"])
+def test_only_continuous_declaration_relocation_is_equivalent(mutation):
+ after = (ROOT / evidence.MODEL).read_text()
+ declaration = re.search(r"^ wire \[3:0\] s_token_lanes = [^;]+;\n", after, re.M).group()
+ before = after.replace(declaration, "").replace(" integer s_data_bytes;\n",
+ declaration + " integer s_data_bytes;\n")
+ if mutation == "expression":
+ after = after.replace("s_data_bytes >= 4", "s_data_bytes >= 3")
+ elif mutation == "body":
+ after = after.replace("s_data_bytes = (bytes_i < 1) ? 1 : bytes_i;",
+ "s_data_bytes = 1;")
+ assert after != (ROOT / evidence.MODEL).read_text()
+ elif mutation == "still_forward":
+ after = before
+ if mutation == "none":
+ evidence.check_model_declaration_move(before, after)
+ else:
+ with pytest.raises(ValueError):
+ evidence.check_model_declaration_move(before, after)
+
+
+@pytest.mark.parametrize("mutation", ["none", "dut", "model", "tool"])
+def test_verification_update_cannot_relabel_dut_changes(tmp_path, mutation):
+ after = (ROOT / evidence.MODEL).read_text()
+ declaration = re.search(r"^ wire \[3:0\] s_token_lanes = [^;]+;\n", after, re.M).group()
+ before = after.replace(declaration, "").replace(" integer s_data_bytes;\n",
+ declaration + " integer s_data_bytes;\n")
+ if mutation == "model":
+ after = after.replace("s_data_bytes >= 4", "s_data_bytes >= 3")
+ dut = "rtl/tiny/top/retrosoc_tiny.sv"
+ documents = [{evidence.MODEL: before, dut: "unchanged DUT"},
+ {evidence.MODEL: after, dut: "changed DUT" if mutation == "dut" else "unchanged DUT"}]
+ snapshots = []
+ maps = []
+ for index, documents_by_name in enumerate(documents):
+ folder = tmp_path / str(index)
+ folder.mkdir()
+ hashes = {name: hashlib.sha256(text.encode()).hexdigest() for name, text in documents_by_name.items()}
+ maps.append(hashes)
+ evidence.b.dump(folder / "inputs.json", hashes)
+ (folder / "patch").write_text("unit-test snapshot")
+ with tarfile.open(folder / "source.tar", "w") as archive:
+ for name, text in documents_by_name.items():
+ info = tarfile.TarInfo(name)
+ info.size = len(text.encode())
+ archive.addfile(info, io.BytesIO(text.encode()))
+ snapshots.append({key: evidence.b.artifact(folder / name) for key, name in
+ (("inputs", "inputs.json"), ("patch", "patch"), ("snapshot", "source.tar"))})
+ saved = {"source": snapshots[0], "tools": {"unit-test": 1}, "captured_at": "2026-10-12T00:00:00+00:00"}
+ evidence.b.dump(tmp_path / "source.json", saved)
+ changed = sorted(name for name in maps[0] if maps[0][name] != maps[1][name])
+ update = {"phase": evidence.PHASE, "kind": "native_model_declaration_order",
+ "original_source": evidence.b.artifact(tmp_path / "source.json"), "source": snapshots[1],
+ "tools": {"unit-test": 2 if mutation == "tool" else 1}, "changed_inputs": changed,
+ "captured_at": "2026-10-12T00:01:00+00:00"}
+ if mutation == "none":
+ assert evidence.check_update(tmp_path, saved, update, maps[1]) == maps[0]
+ else:
+ with pytest.raises(ValueError):
+ evidence.check_update(tmp_path, saved, update, maps[1])
+
+
+@pytest.mark.parametrize("scope", ["consumer", "collection"])
+@pytest.mark.parametrize("mutation", ["none", "dut", "firmware", "smoke", "model", "clock_contract", "other_consumer", "tool"])
+def test_consumer_snapshot_cannot_change_running_flow_inputs(tmp_path, mutation, scope):
+ consumer = "publications/programming_reference.py" if scope == "consumer" else "scripts/tiny_r2_p6.py"
+ forbidden = {
+ "dut": "rtl/tiny/top/retrosoc_tiny.sv", "firmware": "app/apps/ci_smoke/tiny.c",
+ "smoke": "physical/smoke/sta/opensta/generate_sdc.py", "model": evidence.MODEL,
+ "clock_contract": "rtl/tiny/integration/clock_reset_domains.json",
+ "other_consumer": "scripts/regress.py" if scope == "consumer" else "publications/programming_reference.py",
+ }
+ old = {consumer: "old", **{name: "same" for name in forbidden.values()}}
+ new = dict(old, **{consumer: "new"})
+ if mutation in forbidden:
+ new[forbidden[mutation]] = "changed"
+ snapshots = []
+ for index, values in enumerate((old, new)):
+ folder = tmp_path / str(index)
+ folder.mkdir()
+ evidence.b.dump(folder / "inputs", values)
+ for name in ("snapshot", "patch"):
+ (folder / name).write_text("unit-test consumer artifact")
+ snapshots.append({name: evidence.b.artifact(folder / name) for name in ("inputs", "snapshot", "patch")})
+ previous = {"source": snapshots[0], "tools": {"unit-test": 1}, "captured_at": "2026-10-12T00:00:00+00:00"}
+ prior = "verification-update.json" if scope == "consumer" else "consumer-update.json"
+ kind = "publication_and_commercial_consumers" if scope == "consumer" else "terminal_log_collection"
+ check = evidence.check_consumer_update if scope == "consumer" else evidence.check_collection_update
+ evidence.b.dump(tmp_path / prior, previous)
+ update = {"phase": evidence.PHASE, "kind": kind,
+ "previous_update": evidence.b.artifact(tmp_path / prior),
+ "source": snapshots[1], "tools": {"unit-test": 2 if mutation == "tool" else 1},
+ "changed_inputs": sorted(name for name in old if old[name] != new[name]),
+ "captured_at": "2026-10-12T00:01:00+00:00"}
+ if mutation == "none":
+ check(tmp_path, previous, update, new)
+ else:
+ with pytest.raises(ValueError):
+ check(tmp_path, previous, update, new)
+
+
+@pytest.mark.parametrize("simulator,top", [("verilator", "sdio_standalone_tb"),
+ ("verilator", "tiny_p6_sdio_pads_tb"),
+ ("iverilog", "tiny_p6_sdio_pads_tb")])
+def test_shared_sdio_native_data(tmp_path, simulator, top):
+ sources = [ROOT / name for name in (*SDIO_FULL_SOURCES, "tests/rtl/sdio_standalone_tb.sv")]
+ output = tmp_path / "sim"
+ includes = ["-I" + str(ROOT / p) for p in SDIO_FULL_INCDIRS]
+ if simulator == "iverilog":
+ converted = subprocess.run(["sv2v", *includes, *map(str, sources)], capture_output=True,
+ text=True, timeout=180)
+ assert converted.returncode == 0, converted.stderr
+ (tmp_path / "converted.v").write_text(converted.stdout)
+ run([simulator, "-g2012", "-s", top, "-o", str(output), str(tmp_path / "converted.v")],
+ tmp_path, "compile")
+ launch = ["vvp", str(output)]
+ else:
+ run([simulator, "--binary", "--timing", "--assert", "-Wno-fatal", "-j", "3",
+ "--top-module", top, *includes, *map(str, sources),
+ "--Mdir", str(tmp_path / "obj"), "-o", str(output)], tmp_path, "compile", timeout=180)
+ launch = [str(output)]
+ assert "SDIO standalone native SD model test passed" in run(launch, tmp_path, "sim")
+
+
+@pytest.mark.parametrize("simulator", ["iverilog", "verilator"])
+def test_shared_sdio_running_clock_commands(tmp_path, simulator):
+ sources = [ROOT / "rtl/ip/storage" / name for name in
+ ("sdio_pkg.sv", "sdio_clock.sv", "sdio_command.sv")]
+ sources += [ROOT / "tests/rtl/sdio_command_tb.sv"]
+ output = tmp_path / "sim"
+ if simulator == "iverilog":
+ converted = subprocess.run(["sv2v", *map(str, sources)], capture_output=True,
+ text=True, timeout=60)
+ assert converted.returncode == 0, converted.stderr
+ (tmp_path / "converted.v").write_text(converted.stdout)
+ run([simulator, "-g2012", "-s", "sdio_command_tb", "-o", str(output),
+ str(tmp_path / "converted.v")], tmp_path, "compile")
+ launch = ["vvp", str(output)]
+ else:
+ run([simulator, "--binary", "--timing", "-Wno-fatal", "--top-module", "sdio_command_tb",
+ *map(str, sources), "--Mdir", str(tmp_path / "obj"), "-o", str(output)],
+ tmp_path, "compile", timeout=120)
+ launch = [str(output)]
+ assert "SDIO command engine test passed" in run(launch, tmp_path, "sim")
+
+
+@pytest.mark.parametrize("simulator", ["iverilog", "verilator"])
+def test_apb_read_strobes_after_writes(tmp_path, simulator):
+ common = ROOT / "rtl/managed/clusterip/common/rtl"
+ generate_memory_map(TINY / "address_map/memory_map.json", tmp_path / "map", "YES", 128)
+ sources = [common / "utils/register.sv", common / "interface/axi4_if.sv",
+ common / "interface/apb4_if.sv", TINY / "top/tiny_axi42apb4.sv",
+ ROOT / "tests/rtl/tiny_r2_p6_bridge_tb.sv"]
+ flags = ["-DSV_ASSRT_DISABLE", "-I" + str(common), "-I" + str(common / "interface"),
+ "-I" + str(tmp_path / "map/rtl")]
+ top = "tiny_r2_p6_bridge_tb"
+ output = tmp_path / "sim"
+ if simulator == "iverilog":
+ converted = subprocess.run(["sv2v", *flags, *map(str, sources)], capture_output=True,
+ text=True, timeout=60)
+ assert converted.returncode == 0, converted.stderr
+ (tmp_path / "converted.v").write_text(converted.stdout)
+ run([simulator, "-g2012", "-s", top, "-o", str(output), str(tmp_path / "converted.v")],
+ tmp_path, "compile")
+ launch = ["vvp", str(output)]
+ else:
+ run([simulator, "--binary", "--timing", "--assert", "-Wno-fatal", "--top-module", top,
+ *flags, *map(str, sources), "--Mdir", str(tmp_path / "obj"), "-o", str(output)],
+ tmp_path, "compile", timeout=120)
+ launch = [str(output)]
+ assert "SIM_TEST_PASS Tiny P6 APB read strobes" in run(launch, tmp_path, "sim")
+
+
+def p6_log():
+ rows = ["R2_P6_" + name + " " + " ".join(f"{k}={v}" for k, v in fields.items())
+ for name, fields in evidence.RECORDS.items()]
+ rows += [f"R2_P6_AUDIO bits={bits} frames=256 ws_frames=2 background_jobs=8 "
+ f"tcd={int(bits == 24)} elapsed_us=7000 cycles=168000 instructions=40000"
+ for bits in (16, 24)]
+ rows += [f"R2_P6_I2S_WIRE bits={bits} samples=512" for bits in (16, 24)]
+ rows += [f"R2_P6_AXI owner={owner} read_bytes=1024 write_bytes=1024 "
+ f"descriptor_bytes={256 if owner == 1 else 0} ar_stall_max=3 aw_stall_max=3 "
+ "w_stall_max=3 r_stall_max=3 b_stall_max=3" for owner in range(3)]
+ rows += ["R2_P6_CONTENTION sdio_dma_cycles=100 cpu_fetches=10"]
+ return "\n".join([*rows, "R2_P6_DEVICES ws_frames=4 ws_wait_max=100 fifo_max=16 "
+ "underflows=1 aborts=1 clock_stops=2", "SIM_TEST_PASS Tiny cycles=200000"])
+
+
+def test_terminal_verdict_survives_partial_uart_line():
+ flow = {"status": "passed", "exit_code": 0}
+ content = p6_log().replace("SIM_TEST_PASS Tiny cycles=", "Tiny acceptance passed coSIM_TEST_PASS Tiny cycles=")
+ assert evidence.parse_log(content, flow)["uart"]["echo_bytes"] == 4
+ for replacement in ("SIM_TEST_PASS Tiny JTAG transport halt/resume", "Hello retroSoC!",
+ "SIM_TEST_PASS Mini cycles=200000", "SIM_TEST_PASS Tiny cycles=unknown"):
+ with pytest.raises(ValueError):
+ evidence.parse_log(content.replace("SIM_TEST_PASS Tiny cycles=200000", replacement), flow)
+
+
+@pytest.mark.parametrize("mutation", ["none", "failed_command", "missing", "duplicate", "budget",
+ "overflow", "false_qualification", "error", "no_terminal"])
+def test_p6_evidence_is_fail_closed(mutation):
+ log = p6_log()
+ flow = {"status": "passed", "exit_code": 0}
+ if mutation == "failed_command":
+ flow["exit_code"] = 1
+ elif mutation == "missing":
+ log = "\n".join(line for line in log.splitlines() if not line.startswith("R2_P6_RNG"))
+ elif mutation == "duplicate":
+ log += "\n" + log.splitlines()[0]
+ elif mutation == "budget":
+ log = log.replace("ws_wait_max=100", "ws_wait_max=4801")
+ elif mutation == "overflow":
+ log = log.replace("fifo_max=16", "fifo_max=17")
+ elif mutation == "false_qualification":
+ log = log.replace("unqualified=1", "unqualified=0")
+ elif mutation == "error":
+ log += "\nSIM_TEST_FAIL"
+ elif mutation == "no_terminal":
+ log = log.replace("SIM_TEST_PASS", "Hello retroSoC!")
+ if mutation == "none":
+ assert len(evidence.parse_log(log, flow)["audio"]) == 2
+ else:
+ with pytest.raises(ValueError):
+ evidence.parse_log(log, flow)
+
+
+@pytest.mark.parametrize("mutation", ["none", "failed", "old", "unfinished", "reversed"])
+def test_p6_gate_chronology(tmp_path, mutation):
+ flow = {"status": "passed", "exit_code": 0,
+ "started_at": "2026-10-12T01:00:00+00:00", "finished_at": "2026-10-12T01:01:00+00:00"}
+ if mutation == "failed":
+ flow["exit_code"] = 1
+ elif mutation == "old":
+ flow["started_at"] = "2026-10-11T01:00:00+00:00"
+ elif mutation == "unfinished":
+ del flow["finished_at"]
+ elif mutation == "reversed":
+ flow["finished_at"] = "2026-10-12T00:30:00+00:00"
+ path = tmp_path / "gate.json"
+ path.write_text(json.dumps(flow))
+ if mutation == "none":
+ assert evidence.checked_flow(path, "2026-10-12T00:59:00+00:00") == flow
+ else:
+ with pytest.raises(ValueError):
+ evidence.checked_flow(path, "2026-10-12T00:59:00+00:00")
+
+
+@pytest.mark.parametrize("mutation", ["none", "source", "tool", "old_model", "missing_image"])
+def test_p6_complete_collection_requires_current_inputs(tmp_path, monkeypatch, mutation):
+ """Exercise real artifact retention with synthetic, explicitly unit-test flows."""
+ directory = tmp_path / "evidence"
+ directory.mkdir()
+ source = tmp_path / "source.sv"
+ source.write_text("unit-test source\n")
+ source_inputs = {"source.sv": evidence.b.sha256(source)}
+ evidence.b.dump(directory / "inputs.json", source_inputs)
+ for name in ("patch", "snapshot"):
+ (directory / name).write_text("unit-test artifact\n")
+ saved_source = {name: evidence.b.artifact(directory / filename) for name, filename in
+ (("inputs", "inputs.json"), ("patch", "patch"), ("snapshot", "snapshot"))}
+ variants = [tmp_path / pdk for pdk in ("ICS55", "IHP130")]
+ captured = "2026-10-12T00:00:00+00:00"
+ evidence.b.dump(directory / "source.json", {"phase": evidence.PHASE, "source": saved_source,
+ "tools": {"unit-test": 1}, "captured_at": captured, "variants": list(map(str, variants))})
+ monkeypatch.setattr(evidence, "ROOT", tmp_path)
+ monkeypatch.setattr(evidence, "inputs", lambda _: {"source.sv": evidence.b.sha256(source)})
+ monkeypatch.setattr(evidence, "current_tools", lambda: {"unit-test": 2 if mutation == "tool" else 1})
+ monkeypatch.setattr(evidence.b, "configuration", lambda p: {
+ "configuration": {"PDK": p.name, "APP": "ci_smoke"}})
+ monkeypatch.setattr(evidence.b, "runtime_inputs", lambda *_: {"unit-test": "explicit fixture"})
+ for variant in variants:
+ for relative in ("sw/firmware", "sw/retrosoc_fw.hex", "sw/retrosoc_fw.map", "sw/retrosoc_fw.bin",
+ "sw/ld2_all_sram.lds", "sw/memory_regions.ld", "meta/manifest.json"):
+ path = variant / relative
+ path.parent.mkdir(parents=True, exist_ok=True)
+ path.write_text("unit-test artifact\n")
+ for simulator in ("iverilog", "verilator"):
+ folder = variant / "sim" / simulator
+ folder.mkdir(parents=True)
+ for name in ("compile.log", "simv" if simulator == "iverilog" else "emu"):
+ (folder / name).write_text("unit-test artifact\n")
+ (folder / "sim.log").write_text(p6_log())
+ evidence.b.dump(folder / "result-sim-check.json", {"status": "passed"})
+ for label, start, end in (("compile", "01", "02"), ("sim", "03", "04")):
+ evidence.b.dump(folder / f"result-{label}.json", {"status": "passed", "exit_code": 0,
+ "started_at": f"2026-10-12T00:{start}:00+00:00",
+ "finished_at": f"2026-10-12T00:{end}:00+00:00"})
+ if mutation == "source":
+ source.write_text("changed unit-test source\n")
+ elif mutation == "old_model":
+ path = variants[0] / "sim/iverilog/result-compile.json"
+ flow = evidence.b.read_json(path)
+ flow["started_at"] = "2026-10-11T00:00:00+00:00"
+ evidence.b.dump(path, flow)
+ elif mutation == "missing_image":
+ (variants[0] / "sw/retrosoc_fw.hex").unlink()
+ if mutation == "none":
+ evidence.report(directory, [], variants)
+ report = evidence.b.read_json(directory / "report.json")
+ assert report["status"] == "functional_evidence_collected"
+ assert len(report["simulations"]) == 4
+ for simulation in report["simulations"].values():
+ for artifact in simulation["artifacts"]:
+ evidence.b.check_artifact(artifact)
+ else:
+ with pytest.raises((ValueError, FileNotFoundError)):
+ evidence.report(directory, [], variants)
+ assert not (directory / "report.json").exists()
+
+
+@pytest.mark.parametrize("name", ["rng", "crc"])
+def test_locked_controller_contracts(tmp_path, name):
+ for tool in ("iverilog", "vvp", "verilator", "cc"):
+ assert shutil.which(tool), f"required tool unavailable: {tool}"
+ source = ROOT / "rtl/managed/clusterip" / name
+ result = subprocess.run(["make", "-C", str(source), f"BUILD_DIR={tmp_path}",
+ "register-check", "test"], text=True, stdout=subprocess.PIPE,
+ stderr=subprocess.STDOUT, timeout=180)
+ (tmp_path / "build.log").write_text(result.stdout)
+ assert result.returncode == 0, result.stdout[-12000:]
+ for simulator, marker in (("iverilog", f"{name.upper()}_TEST_PASS"),
+ ("verilator", f"APB4_{name.upper()}_TEST_PASS")):
+ log = (tmp_path / simulator / "test.log").read_text()
+ assert marker in log and not DEFAULT_FAILURE.search(log), log
+
+
+@pytest.mark.parametrize("simulator", ["iverilog", "verilator"])
+def test_shared_i2s_payload(tmp_path, simulator):
+ common = ROOT / "rtl/managed/clusterip/common/rtl"
+ serial = ROOT / "rtl/ip/serial"
+ for tool in (simulator, "sv2v", "vvp"):
+ assert shutil.which(tool), f"required tool unavailable: {tool}"
+ sources = [common / name for name in (
+ "interface/apb4_if.sv", "interface/axi4_stream_if.sv", "utils/register.sv",
+ "utils/bin2gray.sv", "utils/gray2bin.sv", "utils/spill_register.sv",
+ "cdc/cdc_sync.sv", "cdc/cdc_rst_ctrlr.sv", "cdc/async_reqack.sv", "cdc/cdc_fifo.sv",
+ "cdc/cdc_2phase.sv", "cdc/cdc_warm_flush.sv", "utils/edge_det.sv", "clkrst/rst_sync.sv")]
+ sources += [ROOT / "rtl/tech/tc_clk.sv"]
+ sources += [serial / name for name in ("i2s_pkg.sv", "i2s_reg.sv", "i2s_clkgen.sv",
+ "i2s_recv.sv", "i2s_send.sv", "i2s_core.sv", "apb4_i2s.sv")]
+ sources += [ROOT / "tests/rtl/tiny_r2_p6_shared_tb.sv"]
+ flags = ["-DPDK_BEHAV", "-DSV_ASSRT_DISABLE", *[f"-I{p}" for p in (
+ common, common / "interface", common / "utils", common / "cdc", common / "clkrst", serial)]]
+ top = "tiny_r2_p6_shared_tb"
+ output = tmp_path / "sim"
+ if simulator == "iverilog":
+ converted = subprocess.run(["sv2v", *flags, *map(str, sources)], capture_output=True,
+ text=True, timeout=180)
+ assert converted.returncode == 0, converted.stderr
+ (tmp_path / "converted.v").write_text(converted.stdout)
+ run([simulator, "-g2012", "-s", top, "-o", str(output), str(tmp_path / "converted.v")],
+ tmp_path, "compile", timeout=180)
+ launch = ["vvp", str(output)]
+ else:
+ run([simulator, "--binary", "--timing", "--assert", "-Wno-fatal", "-j", "3",
+ "--top-module", top, *flags, *map(str, sources), "--Mdir", str(tmp_path / "obj"),
+ "-o", str(output)], tmp_path, "compile", timeout=180)
+ launch = [str(output)]
+ content = run(launch, tmp_path, "sim", timeout=180)
+ assert "SIM_TEST_PASS Tiny P6 I2S payload" in content
+ for bits in (16, 24):
+ assert f"P6_I2S bits={bits} samples=512" in content
+
+
+def test_p6_map_irq_request_and_pin_contract():
+ topology = read_topology(TINY / "integration/soc_topology.json", TINY / "address_map/memory_map.json")
+ regions = {r["symbol"]: int(r["base"], 0) for r in json.loads(
+ (TINY / "address_map/memory_map.json").read_text())["regions"]}
+ for name, base in {"RNG": 0x20001000, "CRC": 0x20006000, "WS2812": 0x10008000,
+ "CRYPTO": 0x1000C000, "I2S": 0x10007000, "SDIO0": 0x1000F000}.items():
+ assert regions["APB4_" + name] == base
+ assert not {"APB4_UART1", "APB4_I2C1", "APB4_DVP", "APB4_SPI0", "APB4_PIOLITE"} & regions.keys()
+ irqs = {i["name"]: i["core_bit"] for i in topology["interrupts"]}
+ assert {k: irqs[k] for k in ("rng", "ws2812", "crypto", "i2s", "sdio")} == {
+ "rng": 16, "ws2812": 17, "crypto": 23, "i2s": 8, "sdio": 10}
+ assert {15, 19, 24, 25, 26, 27}.isdisjoint(irqs.values())
+ assert irqs["rcu"] == 31
+ pins = topology["gpio_alt_functions"]
+ assert pins[9]["alt0"]["inputs"] == ["u_uart0_if.rx_i"]
+ assert pins[10]["alt0"]["inputs"] == ["u_i2c0_if.scl_i"]
+ assert pins[11]["alt0"]["inputs"] == ["u_i2c0_if.sda_i"]
+ assert pins[26]["alt0"]["do"] == "u_ws2812_if.dat_o"
+ for pin in (24, 25):
+ assert pins[pin]["alt0"]["inputs"] == [f"u_pwm_if.capture_i[{pin - 24}]"]
+ assert pins[pin]["alt1"]["inputs"] == []
+ for pin in range(12, 23):
+ assert pins[pin]["alt1"]["oe"] == "1'b0"
+ assert pins[23]["alt1"]["do"] == pins[28]["alt0"]["do"] == "s_clkout"
+ assert pins[23]["alt1"]["oe"] == pins[28]["alt0"]["oe"] == "s_clkout_enable"
+ validate_clocks(TINY / "integration/clock_reset_domains.json", ROOT)
+
+
+def test_duplicate_input_routes_are_rejected(tmp_path):
+ topology = json.loads((TINY / "integration/soc_topology.json").read_text())
+ topology["gpio_alt_functions"][25]["alt1"]["inputs"] = ["u_uart0_if.rx_i"]
+ path = tmp_path / "topology.json"
+ path.write_text(json.dumps(topology))
+ with pytest.raises(ValueError, match="exclusive route"):
+ read_topology(path, TINY / "address_map/memory_map.json")
+
+
+@pytest.mark.parametrize("enabled", ["YES", "NO"])
+def test_tiny_sva_build_selection_reaches_owned_assertions(enabled):
+ result = subprocess.run(["make", "-n", "CONFIG=configs/ci/ics55-tiny.mk", "SOC=TINY",
+ "PDK=ICS55", "SIMU=VERILATOR", f"HAVE_SVA={enabled}",
+ "BUILD_TIMESTAMP=2099-01-01-00-00", "comp"], cwd=ROOT,
+ capture_output=True, text=True, check=True)
+ assert ("+define+HAVE_SVA" in result.stdout) == (enabled == "YES")
+ assert ("+define+SV_ASSRT_DISABLE" in result.stdout) == (enabled == "NO")
+
+
+@pytest.mark.parametrize("simulator", ["iverilog", "verilator"])
+def test_board_pullups_survive_partial_external_drivers(tmp_path, simulator):
+ assert shutil.which(simulator), f"required simulator unavailable: {simulator}"
+ generate_bindings(read_topology(TINY / "integration/soc_topology.json",
+ TINY / "address_map/memory_map.json"), tmp_path)
+ source = tmp_path / "board.sv"
+ drivers = "\n".join(f"assign s_gpio_{n}=oe[{n}] ? data[{n}] : 1'bz;" for n in range(32))
+ source.write_text('module board;\n`include "tiny_tb_gpio.svh"\n' +
+ "logic clk=0; logic [31:0] oe=0, data=0; always #5 clk=!clk;\n" + drivers +
+ "\nassign s_gpio_17=clk; assign s_gpio_15=s_gpio_14;\n" + r'''
+initial begin
+ #21;
+ if (s_gpio[11:10] !== 2'b11) $fatal(1, "lost board pullups");
+ oe[10]=1;
+ #10;
+ if (s_gpio[11:10] !== 2'b10) $fatal(1, "open-drain drive/readback");
+ oe[10]=0;
+ #10;
+ if (s_gpio[11:10] !== 2'b11) $fatal(1, "open-drain release/readback");
+ $display("SIM_TEST_PASS Tiny P6 board pulls");
+ $finish;
+end
+endmodule
+''')
+ output = tmp_path / "sim"
+ flags = ["-I" + str(tmp_path / "rtl")]
+ if simulator == "iverilog":
+ run([simulator, "-g2012", "-s", "board", *flags, "-o", str(output), str(source)],
+ tmp_path, "compile")
+ launch = ["vvp", str(output)]
+ else:
+ run([simulator, "--binary", "--timing", "-Wno-fatal", "--top-module", "board",
+ *flags, str(source), "--Mdir", str(tmp_path / "obj"), "-o", str(output)],
+ tmp_path, "compile", timeout=180)
+ launch = [str(output)]
+ assert "SIM_TEST_PASS Tiny P6 board pulls" in run(launch, tmp_path, "sim")
+
+
+@pytest.mark.parametrize("tiny", [True, False])
+def test_dma_sdk_request_matrix(tmp_path, tiny):
+ assert shutil.which("cc"), "host compiler required"
+ if tiny:
+ generate_bindings(read_topology(TINY / "integration/soc_topology.json",
+ TINY / "address_map/memory_map.json"), tmp_path)
+ else:
+ extension = tmp_path / "include/retrosoc/generated/user_extensions.h"
+ extension.parent.mkdir(parents=True)
+ extension.write_text("")
+ generate_memory_map(ROOT / ("rtl/tiny" if tiny else "rtl/mini") / "address_map/memory_map.json",
+ tmp_path / "map", "YES", 128 if tiny else 32)
+ probe = tmp_path / "probe.c"
+ probe.write_text(r'''
+#include
+#include
+int main(void) {
+ _Alignas(64) rs_dma_tcd_t tcd = {0};
+ rs_dma_config_t c = {.kind=RS_DMA_KIND_MM_TO_MM, .source=0x30000000U,
+ .destination=0x30010000U, .byte_count=64U, .width=RS_DMA_WIDTH_32,
+ .source_increment=true, .destination_increment=true, .burst_beats=16U};
+ for (unsigned channel=0; channel<8; ++channel) {
+ for (unsigned request=0; request<16; ++request) {
+ c.request=(rs_dma_request_t)request;
+ c.kind=(request==1 || request==12) ? RS_DMA_KIND_MM_TO_STREAM :
+ ((request==2 || request==11 || request==13) ? RS_DMA_KIND_STREAM_TO_MM :
+ RS_DMA_KIND_MM_TO_MM);
+#ifdef RS_SOC_TINY
+ bool supported=(0x31ffU & (1U << request)) != 0U;
+#else
+ bool supported=request < 14U;
+#endif
+ assert((rs_dma_config_validate(channel, &c)==RS_OK)==supported);
+ tcd.source=c.source; tcd.destination=c.destination; tcd.byte_count=64U;
+ tcd.control=RS_DMA_TCD_VALID | RS_DMA_TCD_SRC_INC | RS_DMA_TCD_DST_INC |
+ ((unsigned)c.kind << RS_DMA_TCD_KIND_SHIFT) |
+ (request << RS_DMA_TCD_REQUEST_SHIFT) | (16U << RS_DMA_TCD_BURST_SHIFT);
+ assert((rs_dma_tcd_validate(channel, &tcd)==RS_OK)==supported);
+ }
+ }
+ assert(rs_dma_config_validate(8, &c)!=RS_OK);
+ c.request=(rs_dma_request_t)16;
+ assert(rs_dma_config_validate(0, &c)!=RS_OK);
+ c.request=(rs_dma_request_t)17;
+ assert(rs_dma_config_validate(0, &c)!=RS_OK);
+ c.request=(rs_dma_request_t)255;
+ assert(rs_dma_config_validate(0, &c)!=RS_OK);
+ return 0;
+}
+''')
+ binary = tmp_path / "probe"
+ command = ["cc", "-std=c11", "-Wall", "-Wextra", "-Werror",
+ "-I" + str(ROOT / "crt/include"), "-I" + str(tmp_path / "include"),
+ "-I" + str(tmp_path / "map/include"), str(probe),
+ str(ROOT / "crt/src/hal/dma_math.c"), "-o", str(binary)]
+ subprocess.run(command, check=True, capture_output=True, text=True)
+ subprocess.run([str(binary)], check=True, capture_output=True, text=True)
+
+
+def test_p6_reset_and_macro_policy_remains_explicit():
+ text = (TINY / "top/retrosoc_tiny.sv").read_text()
+ assert ".clk_aud_i" in text and "s_audio_rst_n" in text
+ assert ".EnableStreams" in text and "16'h31ff" in text
+ assert "apb4_crypto u_crypto" in text
+ tree = (TINY / "top/tiny_reset_tree.sv").read_text()
+ assert "s_audio" not in tree and "crypto" not in tree
+ assert ".STAGE(5)" in tree
diff --git a/tests/test_tiny_r2_p7.py b/tests/test_tiny_r2_p7.py
new file mode 100644
index 00000000..a17e814a
--- /dev/null
+++ b/tests/test_tiny_r2_p7.py
@@ -0,0 +1,294 @@
+"""Tiny P7 clock, reset and crossing contracts; behavioral evidence only."""
+
+from __future__ import annotations
+
+import os
+import re
+import shutil
+import subprocess
+from pathlib import Path
+
+import pytest
+
+from scripts.check_simulation import DEFAULT_FAILURE
+from scripts.generate_tiny import generate_bindings, read_topology
+from scripts.tiny_r2_p7 import inspect_netlist
+from scripts.rtl.generate_memory_map import generate as generate_memory_map
+from test_tiny_ics55_platform import mapped_netlist # noqa: F401 -- shared synthetic fixture
+
+ROOT = Path(__file__).resolve().parents[1]
+COMMON = ROOT / "rtl/managed/clusterip/common/rtl"
+
+
+def run_rtl(tmp_path: Path, simulator: str, fixture: str, extra: list[Path],
+ defines: list[str] | None = None, plusargs: list[str] | None = None,
+ packages: list[Path] | None = None) -> str:
+ for tool in (simulator, *([] if simulator == "verilator" else ["sv2v", "vvp"])):
+ assert shutil.which(tool), f"required tool unavailable: {tool}"
+ # Put the fixture first so its explicit timescale applies during sv2v.
+ sources = [*(packages or []), ROOT / f"tests/rtl/{fixture}.sv",
+ *[COMMON / p for p in (
+ "utils/register.sv", "utils/xchecker.sv", "utils/bin2gray.sv",
+ "utils/gray2bin.sv", "interface/apb4_if.sv", "interface/axi4_if.sv",
+ "cdc/cdc_sync.sv", "cdc/cdc_rst_ctrlr.sv", "cdc/cdc_2phase.sv",
+ "clkrst/rst_sync.sv", "clock/safe_clock_mux.sv", "clock/clock_divider.sv")],
+ *extra]
+ includes = [f"-I{p}" for p in (COMMON, COMMON / "interface", ROOT / "tests/rtl",
+ ROOT / "rtl/tiny/top")]
+ args = ["-DSV_ASSRT_DISABLE", *(defines or []), *includes, *(str(p) for p in sources)]
+ output = tmp_path / "sim"
+
+ def execute(command: list[str], log: str, timeout: int = 120) -> str:
+ result = subprocess.run(command, cwd=ROOT, capture_output=True, text=True,
+ env={**os.environ, "CCACHE_DISABLE": "1"}, timeout=timeout)
+ content = result.stdout + result.stderr
+ (tmp_path / log).write_text(content)
+ assert result.returncode == 0, content
+ return content
+
+ if simulator == "verilator":
+ execute([simulator, "--binary", "--timing", "--assert", "--timescale", "1ns/1ps",
+ "-Wno-fatal", "--top-module", fixture, *args, "--Mdir", str(tmp_path / "obj"),
+ "-o", str(output)], "compile.log")
+ launch = [str(output)]
+ else:
+ converted = subprocess.run(["sv2v", *args], cwd=ROOT, capture_output=True,
+ text=True, timeout=60)
+ assert converted.returncode == 0, converted.stderr
+ # sv2v strips timescale directives; restore the fixture's declared
+ # units so fractional PLL/reference delays are not rounded to seconds.
+ (tmp_path / "converted.v").write_text("`timescale 1ns / 1ps\n" + converted.stdout)
+ execute([simulator, "-g2012", "-s", fixture, "-o", str(output),
+ str(tmp_path / "converted.v")], "compile.log")
+ launch = ["vvp", str(output)]
+ content = execute([*launch, *(plusargs or [])], "sim.log", 30)
+ assert not DEFAULT_FAILURE.search(content), content
+ assert "SIM_TEST_PASS" in content, content
+ return content
+
+
+@pytest.mark.parametrize("simulator", ["verilator", "iverilog"])
+def test_tiny_apb_async_bridge(tmp_path: Path, simulator: str) -> None:
+ run_rtl(tmp_path, simulator, "tiny_apb_async_bridge_tb",
+ [ROOT / "rtl/tiny/top/tiny_apb_async_bridge.sv"])
+
+
+@pytest.mark.parametrize("simulator", ["verilator", "iverilog"])
+@pytest.mark.parametrize("scenario", ["reset", "same_cycle_clear", "same_cycle_write",
+ "held_read_stability", "held_write_stability", "burst_abort",
+ "clear_pending", "same_cycle_reset_read", "same_cycle_reset_write"])
+def test_tiny_axi4_async_bridge_reset_abort(tmp_path: Path, simulator: str, scenario: str) -> None:
+ run_rtl(tmp_path, simulator, "tiny_axi4_async_bridge_tb", [
+ COMMON / "cdc/async_reqack.sv",
+ COMMON / "cdc/cdc_warm_flush.sv",
+ COMMON / "cdc/cdc_fifo.sv",
+ COMMON / "utils/spill_register.sv",
+ ROOT / "rtl/tiny/top/tiny_axi4_async_bridge.sv",
+ ], plusargs=[] if scenario == "reset" else [f"+{scenario}"])
+
+
+def source_reset_bindings(tmp_path: Path) -> None:
+ """Exercise production reset/admission wiring, not a copied test policy."""
+ top = (ROOT / "rtl/tiny/top/retrosoc_tiny.sv").read_text()
+ topology = read_topology(ROOT / "rtl/tiny/integration/soc_topology.json",
+ ROOT / "rtl/tiny/address_map/memory_map.json")
+ generate_bindings(topology, tmp_path / "bindings")
+ generated = (tmp_path / "bindings/rtl/tiny_apb_interfaces.svh").read_text()
+
+ def instance(source: str, module: str, name: str) -> str:
+ matches = re.findall(rf"\b{module}\s*(?:#\([^;]*?\)\s*)?{name}\s*\([^;]*?\);", source)
+ assert len(matches) == 1, (module, name)
+ return matches[0]
+
+ blocks = [instance(top, module, name) for module, name in (
+ ("tiny_reset_tree", "u_reset_tree"),
+ ("tiny_peripheral_control", "u_peripheral_control"),
+ ("tiny_axi4_admission", "u_dma_admission"),
+ ("tiny_axi4_async_bridge", "u_dma_cdc"),
+ *(("cdc_sync", name) for name in ("u_block_pclk", "u_dma_idle_sync",
+ "u_dma_ready_sync", "u_bus_idle_ref",
+ "u_periph_idle_sync", "u_gate_feedback",
+ "u_held_feedback", "u_ready_feedback")),
+ )]
+ blocks.append(instance(generated, "tiny_peripheral_leaf", "u_dma_lifecycle"))
+ for source, lhs in ((top, "s_fabric_rst_n"), (top, "s_clock_safe"),
+ (generated, "s_xpi_rst_n"), (generated, "s_reset_ready[9]")):
+ matches = re.findall(rf"\bassign\s+{re.escape(lhs)}\s*=[^;]*;", source)
+ assert len(matches) == 1, lhs
+ blocks.extend(matches)
+ blocks += re.findall(r"\bassign\s+s_leaf_clk\[\d+\]\s*=[^;]*;", generated)
+ blocks.append("assign s_leaf_clk[0] = s_exec_clk;")
+ (tmp_path / "source_reset_bindings.svh").write_text("\n".join(blocks) + "\n")
+
+
+@pytest.mark.parametrize("simulator", ["verilator", "iverilog"])
+def test_tiny_production_source_reset_contract(tmp_path: Path, simulator: str) -> None:
+ source_reset_bindings(tmp_path)
+ run_rtl(tmp_path, simulator, "tiny_p7_source_reset_tb", [
+ COMMON / "cdc/async_reqack.sv", COMMON / "cdc/cdc_warm_flush.sv",
+ COMMON / "cdc/cdc_fifo.sv", COMMON / "utils/spill_register.sv",
+ ROOT / "rtl/ip/peripheral/dma_axi4_master.sv",
+ *[ROOT / "rtl/tiny/top" / name for name in (
+ "tiny_reset_tree.sv", "tiny_peripheral_control.sv", "tiny_peripheral_leaf.sv",
+ "tiny_axi4_admission.sv", "tiny_axi4_async_bridge.sv")],
+ ], defines=[f"-I{tmp_path}", f"-I{tmp_path / 'bindings/rtl'}"])
+ launch = [str(tmp_path / "sim")] if simulator == "verilator" else ["vvp", str(tmp_path / "sim")]
+ # Compile once per simulator; reuse the binary at every supported domain
+ # ratio. These are digital boundary tests, not PDK/frequency qualification.
+ for mode in ("xin48", "xin96", "pll192", "pll240", "break_drain"):
+ result = subprocess.run([*launch, f"+{mode}"], cwd=ROOT, text=True,
+ capture_output=True, timeout=30)
+ content = result.stdout + result.stderr
+ (tmp_path / f"{mode}.log").write_text(content)
+ if mode == "break_drain":
+ # sv2v may lower $fatal to $finish(1), which vvp can return as
+ # exit zero. The strict Tiny verdict must still reject the log.
+ assert DEFAULT_FAILURE.search(content) and "P7_SOURCE_RESET_UNDRAINED" in content, content
+ assert "SIM_TEST_PASS" not in content, content
+ else:
+ assert result.returncode == 0 and not DEFAULT_FAILURE.search(content), content
+ assert "SIM_TEST_PASS Tiny P7 production source reset" in content, content
+
+
+@pytest.mark.parametrize("simulator", ["verilator", "iverilog"])
+def test_tiny_watchdog_reset_chain(tmp_path: Path, simulator: str) -> None:
+ generate_memory_map(ROOT / "rtl/tiny/address_map/memory_map.json", tmp_path / "map", "YES", 128)
+ run_rtl(tmp_path, simulator, "tiny_watchdog_reset_tb", [
+ COMMON / "clkrst/counter.sv",
+ COMMON / "cdc/async_reqack.sv", COMMON / "cdc/cdc_fifo.sv",
+ *[ROOT / "rtl/managed/clusterip/wdg/rtl" / name for name in (
+ "wdg_if.sv", "wdg_core.sv", "wdg_reg.sv", "apb4_wdg.sv")],
+ *[ROOT / "rtl/tiny/top" / name for name in (
+ "tiny_clock_reset_subsystem.sv", "tiny_reset_tree.sv",
+ "tiny_apb_async_bridge.sv", "tiny_sysctrl.sv")]],
+ [f"-I{ROOT / 'rtl/ip/peripheral'}", f"-I{tmp_path / 'map/rtl'}",
+ f"-I{ROOT / 'rtl/managed/clusterip/wdg/rtl'}"],
+ packages=[ROOT / "rtl/managed/clusterip/wdg/rtl/wdg_pkg.sv"])
+
+
+@pytest.mark.parametrize("simulator", ["verilator", "iverilog"])
+@pytest.mark.parametrize("module", ["tiny_axi4_admission", "tiny_clock_output"])
+def test_tiny_p7_boundaries(tmp_path: Path, simulator: str, module: str) -> None:
+ run_rtl(tmp_path, simulator, f"{module}_tb", [ROOT / f"rtl/tiny/top/{module}.sv"])
+
+
+@pytest.mark.parametrize("simulator", ["verilator", "iverilog"])
+def test_tiny_peripheral_control(tmp_path: Path, simulator: str) -> None:
+ run_rtl(tmp_path, simulator, "tiny_peripheral_control_tb",
+ [ROOT / "rtl/tiny/top/tiny_peripheral_leaf.sv",
+ ROOT / "rtl/tiny/top/tiny_peripheral_control.sv"])
+
+
+@pytest.mark.parametrize("simulator", ["verilator", "iverilog"])
+def test_tiny_clock_reset_backend_safe24(tmp_path: Path, simulator: str) -> None:
+ run_rtl(tmp_path, simulator, "tiny_clock_reset_subsystem_tb",
+ [ROOT / "rtl/tiny/top/tiny_clock_reset_subsystem.sv"])
+
+
+@pytest.mark.parametrize("simulator", ["verilator", "iverilog"])
+@pytest.mark.parametrize("success", [False, True])
+def test_tiny_clock_reset_backend_pll(tmp_path: Path, simulator: str, success: bool) -> None:
+ run_rtl(tmp_path, simulator, "tiny_clock_reset_pll_tb",
+ [ROOT / "rtl/tech/tc_pll.sv", ROOT / "rtl/tiny/top/tiny_clock_reset_subsystem.sv"],
+ ["-DPDK_BEHAV", "-DHAVE_PLL", *(["-DTINY_PLL_SUCCESS"] if success else [])],
+ [] if success else ["+pll_lock_fail"])
+
+
+@pytest.mark.parametrize("simulator", ["verilator", "iverilog"])
+def test_tiny_clock_reset_ics55_model(tmp_path: Path, simulator: str) -> None:
+ run_rtl(tmp_path, simulator, "tiny_clock_reset_pll_tb",
+ [ROOT / "rtl/tech/tc_pll.sv", ROOT / "rtl/tech/tc_pll_ics55_ecos.sv",
+ ROOT / "rtl/tiny/top/tiny_clock_reset_subsystem.sv",
+ ROOT / ".cache/retrosoc/sources/ics55_ecos_pll/verilog/PLL_TOP.behavioral.v"],
+ ["-DPDK_ICS55", "-DRETROSOC_SOC__TINY", "-DHAVE_PLL", "-DTINY_PLL_SUCCESS",
+ "-DTINY_PLL_NATIVE"])
+
+
+def test_tiny_rcu_register_parity() -> None:
+ sv = (ROOT / "rtl/tiny/top/tiny_rcu_regs.svh").read_text()
+ c = (ROOT / "crt/include/retrosoc/hal/rcu_regs.h").read_text()
+ sv_values = {name: int(value, 16) for name, value in
+ re.findall(r"`define APB4_TINY_RCU__(\w+)\s+32'h([0-9a-f]+)", sv)}
+ c_values = {name: int(value, 16) for name, value in
+ re.findall(r"#define RS_TINY_RCU_(\w+)_OFFSET\s+UINT32_C\(0x([0-9A-F]+)\)", c)}
+ assert len(sv_values) == 24
+ assert sv_values == c_values
+ assert sorted(sv_values.values()) == list(range(0x100, 0x160, 4))
+
+
+@pytest.mark.parametrize("simulator", ["verilator", "iverilog"])
+@pytest.mark.parametrize("xin_hz", [24000000, 48000000, 96000000])
+def test_tiny_external_sources(tmp_path: Path, simulator: str, xin_hz: int) -> None:
+ run_rtl(tmp_path, simulator, "tiny_clock_external_tb",
+ [ROOT / "rtl/tiny/top/tiny_clock_reset_subsystem.sv",
+ ROOT / "rtl/tiny/top/tiny_reset_tree.sv"],
+ [f"-DTINY_TEST_XIN_HZ={xin_hz}"])
+
+
+def test_aon_reset_uses_normalized_reference() -> None:
+ top = (ROOT / "rtl/tiny/top/retrosoc_tiny.sv").read_text()
+ assert re.search(r"\.por_clk_i\s*\(s_ref_clk\)", top)
+
+
+def test_tiny_xpi_cdc_has_split_reset_endpoints() -> None:
+ top = (ROOT / "rtl/tiny/top/retrosoc_tiny.sv").read_text()
+ assert re.search(r"assign s_target_rst_n\[4\]\s*=\s*s_fabric_rst_n", top)
+ bridge = re.search(r"u_xpi_mem_cdc\s*\(.*?\n\s*\);", top, re.DOTALL)
+ assert bridge is not None
+ assert re.search(r"\.src_rst_n_i\s*\(s_fabric_rst_n\)", bridge.group(0))
+ assert re.search(r"\.dst_rst_n_i\s*\(s_xpi_rst_n\)", bridge.group(0))
+
+
+@pytest.fixture
+def p7_netlist(mapped_netlist): # noqa: F811 -- pytest injects the imported fixture
+ modules = mapped_netlist["modules"]
+ cells = modules["retrosoc_tiny_asic"]["cells"]
+
+ def boundary(name, kind, **ports):
+ modules[kind] = {"ports": {key: {"direction": "output" if key.endswith("_o") else "input",
+ "bits": [i + 100]} for i, key in enumerate(ports)},
+ "cells": {}}
+ cells[name] = {"type": kind, "connections": {key: [value] for key, value in ports.items()}}
+
+ boundary("u_soc.u_clock_backend.u_sys_mux", "safe_clock_mux$sys", clk0_i=2, clk1_i=4, clk_o=20)
+ boundary("u_soc.u_clock_backend.u_mem_mux", "safe_clock_mux$mem", clk0_i=20, clk1_i=24, clk_o=21)
+ boundary("u_soc.u_clock_backend.u_pclk_mux", "safe_clock_mux$pclk", clk0_i=20, clk1_i=25, clk_o=22)
+ boundary("u_soc.u_cpu.u_cpu_gate", "tiny_clock_gate$cpu", clk_i=20, clk_o=23)
+ boundary("u_soc.u_crypto_lifecycle", "tiny_peripheral_leaf$crypto", clk_i=22, clk_o=26)
+ for name, cell in cells.items():
+ if "u_soc.u_sram" in name:
+ cell["connections"]["CLK"] = [20]
+ if "u_soc.u_crypto.u_storage" in name:
+ cell["connections"]["CLK"] = [26]
+ if "u_hazard3_cpu_2port" in name:
+ cell["connections"]["CK"] = [23]
+ cells["pll"]["connections"].update(REFCLK=[2], EN=[30])
+ return mapped_netlist
+
+
+def test_p7_clock_bindings(p7_netlist) -> None:
+ binding = inspect_netlist(p7_netlist, "ICS55")
+ assert binding["main_macros"] == 32
+ assert binding["private_crypto_macros"] == 6
+ assert binding["pll_macros"] == 1
+
+
+@pytest.mark.parametrize("mutation", ["half_rate_sram", "cpu_divider", "crypto_sys",
+ "pll_bypass", "pll_tied", "fake_safe_source"])
+def test_p7_clock_binding_rejects_mismatch(p7_netlist, mutation: str) -> None:
+ cells = p7_netlist["modules"]["retrosoc_tiny_asic"]["cells"]
+ if mutation == "half_rate_sram":
+ cells["u_soc.u_sram.gen_group[0].u_group.gen_bank[0].u_ram.u_mem"]["connections"]["CLK"] = [21]
+ elif mutation == "cpu_divider":
+ cells["u_soc.u_cpu.u_hazard3_cpu_2port.core.pc_reg"]["connections"]["CK"] = [21]
+ elif mutation == "crypto_sys":
+ cells["u_soc.u_crypto_lifecycle"]["connections"]["clk_i"] = [20]
+ elif mutation == "pll_bypass":
+ cells["u_soc.u_clock_backend.u_sys_mux"]["connections"]["clk1_i"] = [2]
+ elif mutation == "pll_tied":
+ cells["pll"]["connections"]["EN"] = [3]
+ else:
+ cells["u_soc.u_clock_backend.u_sys_mux"]["connections"]["clk0_i"] = [31]
+ cells["pll"]["connections"]["REFCLK"] = [31]
+ with pytest.raises(ValueError):
+ inspect_netlist(p7_netlist, "ICS55")